From d9feeb93f502f4314ddc357b75f0e4cd62a40892 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Mon, 4 Jul 2016 17:59:44 +0100 Subject: [PATCH] Import docker.io_1.11.2~ds1.orig.tar.gz [dgit import orig docker.io_1.11.2~ds1.orig.tar.gz] --- .dockerignore | 3 + .github/ISSUE_TEMPLATE.md | 51 + .github/PULL_REQUEST_TEMPLATE.md | 23 + .gitignore | 27 + .mailmap | 237 + AUTHORS | 1460 ++++ CHANGELOG.md | 2435 ++++++ CONTRIBUTING.md | 436 ++ Dockerfile | 269 + Dockerfile.aarch64 | 209 + Dockerfile.armhf | 227 + Dockerfile.gccgo | 102 + Dockerfile.ppc64le | 225 + Dockerfile.s390x | 206 + Dockerfile.simple | 56 + Dockerfile.windows | 101 + LICENSE | 191 + MAINTAINERS | 255 + Makefile | 106 + NOTICE | 19 + README.md | 301 + ROADMAP.md | 140 + VENDORING.md | 45 + VERSION | 1 + api/README.md | 5 + api/client/attach.go | 109 + api/client/build.go | 399 + api/client/cli.go | 215 + api/client/client.go | 5 + api/client/commit.go | 85 + api/client/cp.go | 298 + api/client/create.go | 180 + api/client/diff.go | 49 + api/client/events.go | 146 + api/client/exec.go | 166 + api/client/exec_test.go | 130 + api/client/export.go | 42 + api/client/formatter/custom.go | 242 + api/client/formatter/custom_test.go | 192 + api/client/formatter/formatter.go | 255 + api/client/formatter/formatter_test.go | 535 ++ api/client/hijack.go | 56 + api/client/history.go | 76 + api/client/images.go | 81 + api/client/import.go | 82 + api/client/info.go | 155 + api/client/inspect.go | 127 + api/client/inspect/inspector.go | 119 + api/client/inspect/inspector_go14.go | 40 + api/client/inspect/inspector_go15.go | 29 + api/client/inspect/inspector_test.go | 221 + api/client/kill.go | 35 + api/client/load.go | 50 + api/client/login.go | 177 + api/client/logout.go | 41 + api/client/logs.go | 65 + api/client/network.go | 392 + api/client/pause.go | 34 + api/client/port.go | 61 + api/client/ps.go | 89 + api/client/pull.go | 89 + api/client/push.go | 76 + api/client/rename.go | 34 + api/client/restart.go | 35 + api/client/rm.go | 56 + api/client/rmi.go | 59 + api/client/run.go | 287 + api/client/save.go | 42 + api/client/search.go | 93 + api/client/start.go | 157 + api/client/stats.go | 208 + api/client/stats_helpers.go | 219 + api/client/stats_unit_test.go | 47 + api/client/stop.go | 37 + api/client/tag.go | 46 + api/client/top.go | 41 + api/client/trust.go | 559 ++ api/client/trust_test.go | 56 + api/client/unpause.go | 34 + api/client/update.go | 117 + api/client/utils.go | 202 + api/client/version.go | 95 + api/client/volume.go | 177 + api/client/wait.go | 37 + api/common.go | 146 + api/common_test.go | 341 + api/fixtures/keyfile | 7 + api/server/httputils/errors.go | 70 + api/server/httputils/form.go | 73 + api/server/httputils/form_test.go | 105 + api/server/httputils/httputils.go | 107 + api/server/middleware.go | 41 + api/server/middleware/authorization.go | 42 + api/server/middleware/cors.go | 33 + api/server/middleware/debug.go | 56 + api/server/middleware/middleware.go | 7 + api/server/middleware/user_agent.go | 37 + api/server/middleware/version.go | 45 + api/server/middleware/version_test.go | 64 + api/server/profiler.go | 40 + api/server/router/build/backend.go | 20 + api/server/router/build/build.go | 29 + api/server/router/build/build_routes.go | 213 + api/server/router/container/backend.go | 71 + api/server/router/container/container.go | 63 + .../router/container/container_routes.go | 531 ++ api/server/router/container/copy.go | 112 + api/server/router/container/exec.go | 134 + api/server/router/container/inspect.go | 21 + api/server/router/image/backend.go | 44 + api/server/router/image/image.go | 44 + api/server/router/image/image_routes.go | 383 + api/server/router/local.go | 61 + api/server/router/network/backend.go | 19 + api/server/router/network/filter.go | 110 + api/server/router/network/network.go | 37 + api/server/router/network/network_routes.go | 277 + api/server/router/router.go | 19 + api/server/router/system/backend.go | 18 + api/server/router/system/system.go | 33 + api/server/router/system/system_routes.go | 125 + api/server/router/volume/backend.go | 15 + api/server/router/volume/volume.go | 35 + api/server/router/volume/volume_routes.go | 66 + api/server/router_swapper.go | 30 + api/server/server.go | 195 + api/server/server_test.go | 34 + api/types/backend/backend.go | 69 + builder/builder.go | 153 + builder/context.go | 260 + builder/context_unix.go | 11 + builder/context_windows.go | 17 + builder/dockerfile/bflag.go | 176 + builder/dockerfile/bflag_test.go | 187 + builder/dockerfile/builder.go | 326 + builder/dockerfile/command/command.go | 42 + builder/dockerfile/dispatchers.go | 639 ++ builder/dockerfile/envVarTest | 112 + builder/dockerfile/evaluator.go | 215 + builder/dockerfile/internals.go | 662 ++ builder/dockerfile/parser/dumper/main.go | 32 + builder/dockerfile/parser/json_test.go | 55 + builder/dockerfile/parser/line_parsers.go | 331 + builder/dockerfile/parser/parser.go | 161 + builder/dockerfile/parser/parser_test.go | 154 + .../parser/testfile-line/Dockerfile | 34 + .../env_no_value/Dockerfile | 3 + .../shykes-nested-json/Dockerfile | 1 + .../testfiles/ADD-COPY-with-JSON/Dockerfile | 11 + .../testfiles/ADD-COPY-with-JSON/result | 10 + .../testfiles/brimstone-consuldock/Dockerfile | 25 + .../testfiles/brimstone-consuldock/result | 5 + .../brimstone-docker-consul/Dockerfile | 52 + .../testfiles/brimstone-docker-consul/result | 9 + .../testfiles/continueIndent/Dockerfile | 36 + .../parser/testfiles/continueIndent/result | 10 + .../testfiles/cpuguy83-nagios/Dockerfile | 54 + .../parser/testfiles/cpuguy83-nagios/result | 40 + .../parser/testfiles/docker/Dockerfile | 103 + .../dockerfile/parser/testfiles/docker/result | 24 + .../parser/testfiles/env/Dockerfile | 23 + .../dockerfile/parser/testfiles/env/result | 16 + .../parser/testfiles/escapes/Dockerfile | 14 + .../parser/testfiles/escapes/result | 6 + .../parser/testfiles/flags/Dockerfile | 10 + .../dockerfile/parser/testfiles/flags/result | 10 + .../parser/testfiles/influxdb/Dockerfile | 15 + .../parser/testfiles/influxdb/result | 11 + .../Dockerfile | 1 + .../result | 1 + .../Dockerfile | 1 + .../result | 1 + .../Dockerfile | 1 + .../jeztah-invalid-json-single-quotes/result | 1 + .../Dockerfile | 1 + .../result | 1 + .../Dockerfile | 1 + .../result | 1 + .../parser/testfiles/json/Dockerfile | 8 + .../dockerfile/parser/testfiles/json/result | 8 + .../kartar-entrypoint-oddities/Dockerfile | 7 + .../kartar-entrypoint-oddities/result | 7 + .../lk4d4-the-edge-case-generator/Dockerfile | 48 + .../lk4d4-the-edge-case-generator/result | 29 + .../parser/testfiles/mail/Dockerfile | 16 + .../dockerfile/parser/testfiles/mail/result | 14 + .../testfiles/multiple-volumes/Dockerfile | 3 + .../parser/testfiles/multiple-volumes/result | 2 + .../parser/testfiles/mumble/Dockerfile | 7 + .../dockerfile/parser/testfiles/mumble/result | 4 + .../parser/testfiles/nginx/Dockerfile | 14 + .../dockerfile/parser/testfiles/nginx/result | 11 + .../parser/testfiles/tf2/Dockerfile | 23 + .../dockerfile/parser/testfiles/tf2/result | 20 + .../parser/testfiles/weechat/Dockerfile | 9 + .../parser/testfiles/weechat/result | 6 + .../parser/testfiles/znc/Dockerfile | 7 + .../dockerfile/parser/testfiles/znc/result | 5 + builder/dockerfile/parser/utils.go | 176 + builder/dockerfile/shell_parser.go | 314 + builder/dockerfile/shell_parser_test.go | 143 + builder/dockerfile/support.go | 16 + builder/dockerfile/wordsTest | 25 + builder/dockerignore.go | 47 + builder/dockerignore/dockerignore.go | 35 + builder/dockerignore/dockerignore_test.go | 55 + builder/git.go | 28 + builder/remote.go | 152 + builder/remote_test.go | 146 + builder/tarsum.go | 158 + cli/cli.go | 200 + cli/client.go | 12 + cli/common.go | 80 + cliconfig/config.go | 289 + cliconfig/config_test.go | 565 ++ cliconfig/credentials/credentials.go | 17 + cliconfig/credentials/default_store.go | 22 + cliconfig/credentials/default_store_darwin.go | 3 + cliconfig/credentials/default_store_linux.go | 3 + .../credentials/default_store_unsupported.go | 5 + .../credentials/default_store_windows.go | 3 + cliconfig/credentials/file_store.go | 67 + cliconfig/credentials/file_store_test.go | 138 + cliconfig/credentials/native_store.go | 196 + cliconfig/credentials/native_store_test.go | 354 + cliconfig/credentials/shell_command.go | 28 + container/archive.go | 69 + container/container.go | 947 +++ container/container_unit_test.go | 36 + container/container_unix.go | 405 + container/container_windows.go | 105 + container/history.go | 30 + container/memory_store.go | 92 + container/memory_store_test.go | 106 + container/monitor.go | 60 + container/mounts_unix.go | 12 + container/mounts_windows.go | 8 + container/state.go | 283 + container/state_test.go | 109 + container/state_unix.go | 10 + container/state_windows.go | 7 + container/store.go | 28 + contrib/README.md | 4 + contrib/REVIEWERS | 1 + contrib/apparmor/main.go | 56 + contrib/apparmor/template.go | 268 + contrib/builder/deb/amd64/README.md | 5 + contrib/builder/deb/amd64/build.sh | 10 + .../deb/amd64/debian-jessie/Dockerfile | 16 + .../deb/amd64/debian-stretch/Dockerfile | 16 + .../deb/amd64/debian-wheezy/Dockerfile | 17 + contrib/builder/deb/amd64/generate.sh | 131 + .../deb/amd64/ubuntu-precise/Dockerfile | 16 + .../deb/amd64/ubuntu-trusty/Dockerfile | 16 + .../builder/deb/amd64/ubuntu-wily/Dockerfile | 16 + .../deb/amd64/ubuntu-xenial/Dockerfile | 16 + .../deb/armhf/debian-jessie/Dockerfile | 10 + contrib/builder/rpm/amd64/README.md | 5 + contrib/builder/rpm/amd64/build.sh | 10 + contrib/builder/rpm/amd64/centos-7/Dockerfile | 19 + .../builder/rpm/amd64/fedora-22/Dockerfile | 18 + .../builder/rpm/amd64/fedora-23/Dockerfile | 18 + contrib/builder/rpm/amd64/generate.sh | 174 + .../rpm/amd64/opensuse-13.2/Dockerfile | 18 + .../rpm/amd64/oraclelinux-6/Dockerfile | 28 + .../rpm/amd64/oraclelinux-7/Dockerfile | 18 + contrib/check-config.sh | 267 + contrib/completion/REVIEWERS | 2 + contrib/completion/bash/docker | 2251 ++++++ contrib/completion/fish/docker.fish | 400 + .../completion/powershell/posh-docker.psm1 | 179 + contrib/completion/zsh/REVIEWERS | 2 + contrib/completion/zsh/_docker | 1160 +++ contrib/desktop-integration/README.md | 11 + .../desktop-integration/chromium/Dockerfile | 36 + .../desktop-integration/gparted/Dockerfile | 31 + contrib/docker-device-tool/device_tool.go | 176 + .../docker-device-tool/device_tool_windows.go | 4 + contrib/docker-engine-selinux/LICENSE | 340 + contrib/docker-engine-selinux/Makefile | 16 + contrib/docker-engine-selinux/docker.fc | 20 + contrib/docker-engine-selinux/docker.if | 461 ++ contrib/docker-engine-selinux/docker.te | 414 + .../docker-engine-selinux/docker_selinux.8.gz | Bin 0 -> 2847 bytes contrib/dockerize-disk.sh | 118 + contrib/download-frozen-image-v1.sh | 108 + contrib/download-frozen-image-v2.sh | 121 + contrib/httpserver/Dockerfile | 4 + contrib/httpserver/server.go | 12 + contrib/init/openrc/docker.confd | 13 + contrib/init/openrc/docker.initd | 19 + contrib/init/systemd/REVIEWERS | 3 + contrib/init/systemd/docker.service | 22 + contrib/init/systemd/docker.socket | 12 + contrib/init/sysvinit-debian/docker | 149 + contrib/init/sysvinit-debian/docker.default | 20 + contrib/init/sysvinit-redhat/docker | 153 + contrib/init/sysvinit-redhat/docker.sysconfig | 7 + contrib/init/upstart/REVIEWERS | 2 + contrib/init/upstart/docker.conf | 68 + contrib/mkimage-alpine.sh | 87 + contrib/mkimage-arch-pacman.conf | 92 + contrib/mkimage-arch.sh | 122 + contrib/mkimage-archarm-pacman.conf | 98 + contrib/mkimage-busybox.sh | 43 + contrib/mkimage-crux.sh | 75 + contrib/mkimage-debootstrap.sh | 297 + contrib/mkimage-rinse.sh | 123 + contrib/mkimage-yum.sh | 134 + contrib/mkimage.sh | 117 + contrib/mkimage/.febootstrap-minimize | 28 + contrib/mkimage/busybox-static | 34 + contrib/mkimage/debootstrap | 240 + contrib/mkimage/mageia-urpmi | 61 + contrib/mkimage/rinse | 25 + contrib/nnp-test/Dockerfile | 9 + contrib/nnp-test/nnp-test.c | 10 + contrib/nuke-graph-directory.sh | 65 + contrib/project-stats.sh | 22 + contrib/report-issue.sh | 105 + contrib/reprepro/suites.sh | 12 + contrib/syntax/kate/Dockerfile.xml | 70 + contrib/syntax/nano/Dockerfile.nanorc | 26 + contrib/syntax/nano/README.md | 32 + .../Preferences/Dockerfile.tmPreferences | 24 + .../Syntaxes/Dockerfile.tmLanguage | 143 + .../textmate/Docker.tmbundle/info.plist | 16 + contrib/syntax/textmate/README.md | 17 + contrib/syntax/textmate/REVIEWERS | 1 + contrib/syntax/vim/LICENSE | 22 + contrib/syntax/vim/README.md | 26 + contrib/syntax/vim/doc/dockerfile.txt | 18 + contrib/syntax/vim/ftdetect/dockerfile.vim | 1 + contrib/syntax/vim/syntax/dockerfile.vim | 31 + contrib/syscall-test/Dockerfile | 9 + contrib/syscall-test/acct.c | 16 + contrib/syscall-test/ns.c | 63 + contrib/syscall-test/userns.c | 63 + contrib/udev/80-docker.rules | 3 + contrib/vagrant-docker/README.md | 50 + daemon/apparmor_default.go | 30 + daemon/apparmor_default_unsupported.go | 6 + daemon/archive.go | 429 + daemon/archive_unix.go | 57 + daemon/archive_windows.go | 18 + daemon/attach.go | 120 + daemon/caps/utils_unix.go | 131 + daemon/changes.go | 15 + daemon/commit.go | 233 + daemon/config.go | 374 + daemon/config_experimental.go | 8 + daemon/config_stub.go | 8 + daemon/config_test.go | 278 + daemon/config_unix.go | 88 + daemon/config_windows.go | 45 + daemon/container_operations.go | 742 ++ daemon/container_operations_unix.go | 389 + daemon/container_operations_windows.go | 62 + daemon/create.go | 185 + daemon/create_unix.go | 76 + daemon/create_windows.go | 75 + daemon/daemon.go | 1765 +++++ daemon/daemon_experimental.go | 9 + daemon/daemon_linux.go | 80 + daemon/daemon_linux_test.go | 104 + daemon/daemon_stub.go | 9 + daemon/daemon_test.go | 532 ++ daemon/daemon_unix.go | 1126 +++ daemon/daemon_unix_test.go | 199 + daemon/daemon_unsupported.go | 5 + daemon/daemon_windows.go | 467 ++ daemon/debugtrap_unix.go | 21 + daemon/debugtrap_unsupported.go | 7 + daemon/debugtrap_windows.go | 30 + daemon/delete.go | 157 + daemon/delete_test.go | 42 + daemon/discovery.go | 203 + daemon/discovery_test.go | 152 + daemon/errors.go | 57 + daemon/events.go | 88 + daemon/events/events.go | 142 + daemon/events/events_test.go | 196 + daemon/events/filter.go | 82 + daemon/events/testutils/testutils.go | 76 + daemon/events_test.go | 94 + daemon/exec.go | 246 + daemon/exec/exec.go | 93 + daemon/exec_linux.go | 26 + daemon/exec_windows.go | 14 + daemon/export.go | 55 + daemon/graphdriver/aufs/aufs.go | 564 ++ daemon/graphdriver/aufs/aufs_test.go | 801 ++ daemon/graphdriver/aufs/dirs.go | 64 + daemon/graphdriver/aufs/mount.go | 21 + daemon/graphdriver/aufs/mount_linux.go | 7 + daemon/graphdriver/aufs/mount_unsupported.go | 12 + daemon/graphdriver/btrfs/btrfs.go | 330 + daemon/graphdriver/btrfs/btrfs_test.go | 63 + daemon/graphdriver/btrfs/dummy_unsupported.go | 3 + daemon/graphdriver/btrfs/version.go | 26 + daemon/graphdriver/btrfs/version_none.go | 14 + daemon/graphdriver/btrfs/version_test.go | 13 + daemon/graphdriver/counter.go | 32 + daemon/graphdriver/devmapper/README.md | 96 + daemon/graphdriver/devmapper/deviceset.go | 2570 ++++++ daemon/graphdriver/devmapper/devmapper_doc.go | 106 + .../graphdriver/devmapper/devmapper_test.go | 110 + daemon/graphdriver/devmapper/driver.go | 219 + daemon/graphdriver/devmapper/mount.go | 89 + daemon/graphdriver/driver.go | 234 + daemon/graphdriver/driver_freebsd.go | 19 + daemon/graphdriver/driver_linux.go | 99 + daemon/graphdriver/driver_unsupported.go | 15 + daemon/graphdriver/driver_windows.go | 16 + daemon/graphdriver/fsdiff.go | 162 + .../graphdriver/graphtest/graphtest_unix.go | 299 + .../graphtest/graphtest_windows.go | 1 + daemon/graphdriver/overlay/copy.go | 169 + daemon/graphdriver/overlay/overlay.go | 486 ++ daemon/graphdriver/overlay/overlay_test.go | 31 + .../overlay/overlay_unsupported.go | 3 + daemon/graphdriver/plugin.go | 32 + daemon/graphdriver/plugin_unsupported.go | 7 + daemon/graphdriver/proxy.go | 209 + daemon/graphdriver/register/register_aufs.go | 8 + daemon/graphdriver/register/register_btrfs.go | 8 + .../register/register_devicemapper.go | 8 + .../graphdriver/register/register_overlay.go | 8 + daemon/graphdriver/register/register_vfs.go | 6 + .../graphdriver/register/register_windows.go | 6 + daemon/graphdriver/register/register_zfs.go | 8 + daemon/graphdriver/vfs/driver.go | 135 + daemon/graphdriver/vfs/vfs_test.go | 37 + daemon/graphdriver/windows/windows.go | 745 ++ daemon/graphdriver/zfs/MAINTAINERS | 2 + daemon/graphdriver/zfs/zfs.go | 359 + daemon/graphdriver/zfs/zfs_freebsd.go | 38 + daemon/graphdriver/zfs/zfs_linux.go | 27 + daemon/graphdriver/zfs/zfs_test.go | 31 + daemon/graphdriver/zfs/zfs_unsupported.go | 11 + daemon/image_delete.go | 371 + daemon/images.go | 162 + daemon/import.go | 109 + daemon/info.go | 167 + daemon/inspect.go | 245 + daemon/inspect_unix.go | 91 + daemon/inspect_windows.go | 40 + daemon/kill.go | 153 + daemon/links.go | 128 + daemon/links/links.go | 141 + daemon/links/links_test.go | 213 + daemon/links_test.go | 98 + daemon/list.go | 515 ++ daemon/list_unix.go | 11 + daemon/list_windows.go | 20 + daemon/logdrivers_linux.go | 14 + daemon/logdrivers_windows.go | 10 + daemon/logger/awslogs/cloudwatchlogs.go | 376 + daemon/logger/awslogs/cloudwatchlogs_test.go | 627 ++ .../logger/awslogs/cwlogsiface_mock_test.go | 76 + daemon/logger/context.go | 112 + daemon/logger/copier.go | 86 + daemon/logger/copier_test.go | 132 + daemon/logger/etwlogs/etwlogs_windows.go | 183 + daemon/logger/factory.go | 89 + daemon/logger/fluentd/fluentd.go | 201 + daemon/logger/gcplogs/gcplogging.go | 191 + daemon/logger/gelf/gelf.go | 212 + daemon/logger/gelf/gelf_unsupported.go | 3 + daemon/logger/journald/journald.go | 95 + .../logger/journald/journald_unsupported.go | 6 + daemon/logger/journald/read.go | 330 + daemon/logger/journald/read_native.go | 6 + daemon/logger/journald/read_native_compat.go | 6 + daemon/logger/journald/read_unsupported.go | 7 + daemon/logger/jsonfilelog/jsonfilelog.go | 147 + daemon/logger/jsonfilelog/jsonfilelog_test.go | 248 + daemon/logger/jsonfilelog/read.go | 235 + daemon/logger/logger.go | 87 + daemon/logger/loggerutils/log_tag.go | 46 + daemon/logger/loggerutils/log_tag_test.go | 58 + daemon/logger/loggerutils/rotatefilewriter.go | 124 + daemon/logger/splunk/splunk.go | 266 + daemon/logger/syslog/syslog.go | 247 + daemon/logger/syslog/syslog_test.go | 45 + daemon/logger/syslog/syslog_unsupported.go | 3 + daemon/logs.go | 154 + daemon/monitor.go | 144 + daemon/monitor_linux.go | 14 + daemon/monitor_windows.go | 13 + daemon/mounts.go | 48 + daemon/network.go | 205 + daemon/network/settings.go | 22 + daemon/oci_linux.go | 687 ++ daemon/oci_windows.go | 187 + daemon/pause.go | 49 + daemon/rename.go | 85 + daemon/resize.go | 40 + daemon/restart.go | 48 + daemon/seccomp_disabled.go | 12 + daemon/seccomp_linux.go | 46 + daemon/selinux_linux.go | 17 + daemon/selinux_unsupported.go | 13 + daemon/start.go | 185 + daemon/stats.go | 121 + daemon/stats_collector_unix.go | 189 + daemon/stats_collector_windows.go | 35 + daemon/stop.go | 65 + daemon/top_unix.go | 85 + daemon/top_windows.go | 32 + daemon/unpause.go | 43 + daemon/update.go | 100 + daemon/update_linux.go | 25 + daemon/update_windows.go | 13 + daemon/volumes.go | 178 + daemon/volumes_unit_test.go | 39 + daemon/volumes_unix.go | 78 + daemon/volumes_windows.go | 51 + daemon/wait.go | 17 + distribution/errors.go | 113 + .../fixtures/validate_manifest/bad_manifest | 38 + .../validate_manifest/extra_data_manifest | 46 + .../fixtures/validate_manifest/good_manifest | 38 + distribution/metadata/metadata.go | 77 + distribution/metadata/v1_id_service.go | 44 + distribution/metadata/v1_id_service_test.go | 83 + distribution/metadata/v2_metadata_service.go | 137 + .../metadata/v2_metadata_service_test.go | 115 + distribution/pull.go | 205 + distribution/pull_v1.go | 362 + distribution/pull_v2.go | 840 ++ distribution/pull_v2_test.go | 183 + distribution/pull_v2_unix.go | 12 + distribution/pull_v2_windows.go | 29 + distribution/push.go | 219 + distribution/push_v1.go | 454 ++ distribution/push_v2.go | 438 ++ distribution/registry.go | 130 + distribution/registry_unit_test.go | 133 + distribution/xfer/download.go | 430 + distribution/xfer/download_test.go | 340 + distribution/xfer/transfer.go | 392 + distribution/xfer/transfer_test.go | 414 + distribution/xfer/upload.go | 163 + distribution/xfer/upload_test.go | 143 + docker/README.md | 3 + docker/client.go | 33 + docker/client_test.go | 23 + docker/common.go | 100 + docker/daemon.go | 421 + docker/daemon_freebsd.go | 7 + docker/daemon_linux.go | 13 + docker/daemon_none.go | 13 + docker/daemon_test.go | 296 + docker/daemon_unix.go | 82 + docker/daemon_unix_test.go | 212 + docker/daemon_windows.go | 64 + docker/docker.go | 82 + docker/docker_windows.go | 5 + docker/flags.go | 30 + docker/flags_test.go | 13 + docker/listeners/listeners.go | 22 + docker/listeners/listeners_unix.go | 119 + docker/listeners/listeners_windows.go | 58 + dockerversion/useragent.go | 74 + dockerversion/version_lib.go | 13 + docs/.gitignore | 2 + docs/Dockerfile | 17 + docs/Makefile | 54 + docs/README.md | 288 + docs/admin/ambassador_pattern_linking.md | 159 + docs/admin/b2d_volume_images/add_cd.png | Bin 0 -> 27607 bytes .../b2d_volume_images/add_new_controller.png | Bin 0 -> 36100 bytes docs/admin/b2d_volume_images/add_volume.png | Bin 0 -> 30506 bytes docs/admin/b2d_volume_images/boot_order.png | Bin 0 -> 28432 bytes docs/admin/b2d_volume_images/gparted.png | Bin 0 -> 77989 bytes docs/admin/b2d_volume_images/gparted2.png | Bin 0 -> 71561 bytes docs/admin/b2d_volume_images/verify.png | Bin 0 -> 9581 bytes docs/admin/b2d_volume_resize.md | 165 + docs/admin/cfengine_process_management.md | 150 + docs/admin/chef.md | 75 + docs/admin/configuring.md | 280 + docs/admin/dsc.md | 174 + docs/admin/formatting.md | 66 + docs/admin/host_integration.md | 88 + docs/admin/index.md | 11 + docs/admin/logging/awslogs.md | 90 + docs/admin/logging/etwlogs.md | 69 + docs/admin/logging/fluentd.md | 115 + docs/admin/logging/gcplogs.md | 70 + docs/admin/logging/index.md | 23 + docs/admin/logging/journald.md | 92 + docs/admin/logging/log_tags.md | 51 + docs/admin/logging/overview.md | 246 + docs/admin/logging/splunk.md | 69 + docs/admin/puppet.md | 100 + docs/admin/registry_mirror.md | 19 + docs/admin/runmetrics.md | 464 ++ docs/admin/systemd.md | 159 + docs/admin/using_supervisord.md | 119 + docs/article-img/architecture.svg | 2597 ++++++ docs/breaking_changes.md | 52 + docs/deprecated.md | 148 + docs/examples/apt-cacher-ng.Dockerfile | 15 + docs/examples/apt-cacher-ng.md | 126 + docs/examples/couchbase.md | 235 + docs/examples/couchbase/web-console.png | Bin 0 -> 162338 bytes docs/examples/couchdb_data_volumes.md | 49 + docs/examples/index.md | 23 + docs/examples/mongodb.md | 177 + docs/examples/mongodb/Dockerfile | 22 + docs/examples/nodejs_web_app.md | 199 + docs/examples/postgresql_service.Dockerfile | 49 + docs/examples/postgresql_service.md | 153 + docs/examples/running_redis_service.md | 89 + docs/examples/running_riak_service.Dockerfile | 31 + docs/examples/running_riak_service.md | 108 + docs/examples/running_ssh_service.Dockerfile | 20 + docs/examples/running_ssh_service.md | 84 + docs/examples/supervisord.conf | 12 + docs/extend/images/authz_additional_info.png | Bin 0 -> 45916 bytes docs/extend/images/authz_allow.png | Bin 0 -> 33505 bytes docs/extend/images/authz_chunked.png | Bin 0 -> 33168 bytes .../extend/images/authz_connection_hijack.png | Bin 0 -> 38780 bytes docs/extend/images/authz_deny.png | Bin 0 -> 27099 bytes docs/extend/index.md | 22 + docs/extend/plugin_api.md | 188 + docs/extend/plugins.md | 115 + docs/extend/plugins_authorization.md | 254 + docs/extend/plugins_network.md | 57 + docs/extend/plugins_volume.md | 216 + docs/faq.md | 294 + docs/index.md | 121 + docs/installation/binaries.md | 249 + docs/installation/cloud/cloud-ex-aws.md | 208 + .../cloud/cloud-ex-machine-ocean.md | 201 + docs/installation/cloud/index.md | 25 + docs/installation/cloud/overview.md | 56 + docs/installation/images/bad_host.png | Bin 0 -> 27367 bytes docs/installation/images/cool_view.png | Bin 0 -> 24385 bytes docs/installation/images/ec2-ubuntu.png | Bin 0 -> 238001 bytes .../images/ec2_instance_details.png | Bin 0 -> 187842 bytes .../installation/images/ec2_instance_type.png | Bin 0 -> 145409 bytes .../images/ec2_launch_instance.png | Bin 0 -> 234843 bytes docs/installation/images/good_host.png | Bin 0 -> 30853 bytes docs/installation/images/kitematic.png | Bin 0 -> 14191 bytes .../installation/images/linux_docker_host.svg | 1195 +++ .../installation/images/mac-page-finished.png | Bin 0 -> 158534 bytes docs/installation/images/mac-page-two.png | Bin 0 -> 140504 bytes .../images/mac-password-prompt.png | Bin 0 -> 30903 bytes docs/installation/images/mac-success.png | Bin 0 -> 28755 bytes docs/installation/images/mac-welcome-page.png | Bin 0 -> 155430 bytes docs/installation/images/mac_docker_host.svg | 1243 +++ docs/installation/images/my-docker-vm.png | Bin 0 -> 136985 bytes docs/installation/images/newsite_view.png | Bin 0 -> 21403 bytes docs/installation/images/nginx-webserver.png | Bin 0 -> 82642 bytes docs/installation/images/ocean_click_api.png | Bin 0 -> 37127 bytes docs/installation/images/ocean_droplet.png | Bin 0 -> 26453 bytes .../images/ocean_droplet_ubuntu.png | Bin 0 -> 26811 bytes docs/installation/images/ocean_gen_token.png | Bin 0 -> 42803 bytes docs/installation/images/ocean_save_token.png | Bin 0 -> 51520 bytes .../images/ocean_token_create.png | Bin 0 -> 57138 bytes docs/installation/images/virtualization.png | Bin 0 -> 59249 bytes docs/installation/images/win-page-6.png | Bin 0 -> 88115 bytes docs/installation/images/win-welcome.png | Bin 0 -> 94143 bytes docs/installation/images/win_docker_host.svg | 1259 +++ docs/installation/images/win_ver.png | Bin 0 -> 50312 bytes .../images/windows-boot2docker-cmd.png | Bin 0 -> 37436 bytes .../images/windows-boot2docker-powershell.png | Bin 0 -> 37945 bytes .../images/windows-boot2docker-start.png | Bin 0 -> 75786 bytes docs/installation/images/windows-finish.png | Bin 0 -> 119402 bytes docs/installation/index.md | 48 + docs/installation/linux/SUSE.md | 117 + docs/installation/linux/archlinux.md | 105 + docs/installation/linux/centos.md | 192 + docs/installation/linux/cruxlinux.md | 93 + docs/installation/linux/debian.md | 184 + docs/installation/linux/fedora.md | 203 + docs/installation/linux/frugalware.md | 75 + docs/installation/linux/gentoolinux.md | 122 + docs/installation/linux/index.md | 29 + docs/installation/linux/oracle.md | 214 + docs/installation/linux/rhel.md | 184 + docs/installation/linux/ubuntulinux.md | 452 ++ docs/installation/mac.md | 428 + docs/installation/windows.md | 379 + docs/migration.md | 84 + docs/quickstart.md | 205 + docs/reference/api/README.md | 15 + .../_static/io_oauth_authorization_page.png | Bin 0 -> 39458 bytes docs/reference/api/docker-io_api.md | 16 + docs/reference/api/docker_io_accounts_api.md | 277 + docs/reference/api/docker_remote_api.md | 291 + docs/reference/api/docker_remote_api_v1.14.md | 1470 ++++ docs/reference/api/docker_remote_api_v1.15.md | 1764 +++++ docs/reference/api/docker_remote_api_v1.16.md | 1834 +++++ docs/reference/api/docker_remote_api_v1.17.md | 2008 +++++ docs/reference/api/docker_remote_api_v1.18.md | 2125 +++++ docs/reference/api/docker_remote_api_v1.19.md | 2206 ++++++ docs/reference/api/docker_remote_api_v1.20.md | 2346 ++++++ docs/reference/api/docker_remote_api_v1.21.md | 2914 +++++++ docs/reference/api/docker_remote_api_v1.22.md | 3142 ++++++++ docs/reference/api/docker_remote_api_v1.23.md | 3228 ++++++++ docs/reference/api/hub_registry_spec.md | 18 + docs/reference/api/images/event_state.gliffy | 1 + docs/reference/api/images/event_state.png | Bin 0 -> 78354 bytes docs/reference/api/index.md | 16 + .../api/remote_api_client_libraries.md | 248 + docs/reference/builder.md | 1344 ++++ docs/reference/commandline/attach.md | 115 + docs/reference/commandline/build.md | 318 + docs/reference/commandline/cli.md | 209 + docs/reference/commandline/commit.md | 82 + docs/reference/commandline/cp.md | 89 + docs/reference/commandline/create.md | 162 + docs/reference/commandline/daemon.md | 958 +++ docs/reference/commandline/diff.md | 40 + docs/reference/commandline/docker_images.gif | Bin 0 -> 35785 bytes docs/reference/commandline/events.md | 165 + docs/reference/commandline/exec.md | 56 + docs/reference/commandline/export.md | 35 + docs/reference/commandline/history.md | 40 + docs/reference/commandline/images.md | 229 + docs/reference/commandline/import.md | 69 + docs/reference/commandline/index.md | 88 + docs/reference/commandline/info.md | 69 + docs/reference/commandline/inspect.md | 76 + docs/reference/commandline/kill.md | 26 + docs/reference/commandline/load.md | 37 + docs/reference/commandline/login.md | 117 + docs/reference/commandline/logout.md | 22 + docs/reference/commandline/logs.md | 50 + docs/reference/commandline/network_connect.md | 93 + docs/reference/commandline/network_create.md | 169 + .../commandline/network_disconnect.md | 35 + docs/reference/commandline/network_inspect.md | 119 + docs/reference/commandline/network_ls.md | 135 + docs/reference/commandline/network_rm.md | 47 + docs/reference/commandline/pause.md | 27 + docs/reference/commandline/port.md | 34 + docs/reference/commandline/ps.md | 251 + docs/reference/commandline/pull.md | 228 + docs/reference/commandline/push.md | 26 + docs/reference/commandline/rename.md | 19 + docs/reference/commandline/restart.md | 18 + docs/reference/commandline/rm.md | 61 + docs/reference/commandline/rmi.md | 75 + docs/reference/commandline/run.md | 614 ++ docs/reference/commandline/save.md | 37 + docs/reference/commandline/search.md | 97 + docs/reference/commandline/start.md | 20 + docs/reference/commandline/stats.md | 40 + docs/reference/commandline/stop.md | 22 + docs/reference/commandline/tag.md | 20 + docs/reference/commandline/top.md | 17 + docs/reference/commandline/unpause.md | 24 + docs/reference/commandline/update.md | 73 + docs/reference/commandline/version.md | 55 + docs/reference/commandline/volume_create.md | 76 + docs/reference/commandline/volume_inspect.md | 47 + docs/reference/commandline/volume_ls.md | 41 + docs/reference/commandline/volume_rm.md | 29 + docs/reference/commandline/wait.md | 17 + docs/reference/glossary.md | 221 + docs/reference/index.md | 18 + docs/reference/run.md | 1468 ++++ docs/security/apparmor.md | 183 + docs/security/certificates.md | 85 + docs/security/https.md | 216 + docs/security/https/Dockerfile | 10 + docs/security/https/Makefile | 24 + docs/security/https/README.md | 33 + docs/security/https/make_certs.sh | 23 + docs/security/https/parsedocs.sh | 10 + docs/security/index.md | 24 + docs/security/seccomp.md | 143 + docs/security/security.md | 276 + docs/security/trust/content_trust.md | 300 + docs/security/trust/deploying_notary.md | 34 + docs/security/trust/images/tag_signing.png | Bin 0 -> 74416 bytes docs/security/trust/images/trust_.gliffy | 1 + .../trust/images/trust_components.gliffy | 1 + .../trust/images/trust_components.png | Bin 0 -> 124071 bytes .../trust/images/trust_signing.gliffy | 1 + docs/security/trust/images/trust_signing.png | Bin 0 -> 71621 bytes docs/security/trust/images/trust_view.gliffy | 1 + docs/security/trust/images/trust_view.png | Bin 0 -> 59533 bytes docs/security/trust/index.md | 21 + docs/security/trust/trust_automation.md | 80 + docs/security/trust/trust_delegation.md | 226 + docs/security/trust/trust_key_mng.md | 90 + docs/security/trust/trust_sandbox.md | 331 + docs/static_files/README.md | 17 + docs/static_files/contributors.png | Bin 0 -> 23100 bytes docs/static_files/docker-logo-compressed.png | Bin 0 -> 4972 bytes docs/static_files/docker_pull_chart.png | Bin 0 -> 7188 bytes docs/static_files/docker_push_chart.png | Bin 0 -> 8700 bytes docs/static_files/dockerlogo-v.png | Bin 0 -> 9670 bytes docs/touch-up.sh | 20 + docs/understanding-docker.md | 278 + docs/userguide/containers/dockerimages.md | 554 ++ docs/userguide/containers/dockerizing.md | 194 + docs/userguide/containers/dockerrepos.md | 187 + docs/userguide/containers/dockervolumes.md | 372 + docs/userguide/containers/index.md | 19 + .../containers/networkingcontainers.md | 247 + docs/userguide/containers/search.png | Bin 0 -> 17923 bytes docs/userguide/containers/usingdocker.md | 306 + docs/userguide/containers/webapp1.png | Bin 0 -> 13345 bytes docs/userguide/eng-image/baseimages.md | 71 + .../eng-image/dockerfile_best-practices.md | 495 ++ docs/userguide/eng-image/image_management.md | 53 + docs/userguide/eng-image/index.md | 16 + docs/userguide/index.md | 63 + docs/userguide/intro.md | 118 + docs/userguide/labels-custom-metadata.md | 229 + docs/userguide/networking/configure-dns.md | 138 + .../networking/default_network/binding.md | 103 + .../default_network/build-bridges.md | 78 + .../default_network/configure-dns.md | 132 + .../container-communication.md | 125 + .../default_network/custom-docker0.md | 62 + .../networking/default_network/dockerlinks.md | 358 + .../images/ipv6_basic_host_config.gliffy | 1 + .../images/ipv6_basic_host_config.svg | 1 + .../images/ipv6_ndp_proxying.gliffy | 1 + .../images/ipv6_ndp_proxying.svg | 1 + .../images/ipv6_routed_network_example.gliffy | 1 + .../images/ipv6_routed_network_example.svg | 1 + .../images/ipv6_slash64_subnet_config.gliffy | 1 + .../images/ipv6_slash64_subnet_config.svg | 1 + .../ipv6_switched_network_example.gliffy | 1 + .../images/ipv6_switched_network_example.svg | 1 + .../networking/default_network/index.md | 25 + .../networking/default_network/ipv6.md | 259 + docs/userguide/networking/dockernetworks.md | 523 ++ .../networking/get-started-overlay.md | 326 + .../networking/images/bridge_network.gliffy | 1 + .../networking/images/bridge_network.png | Bin 0 -> 15878 bytes .../networking/images/bridge_network.svg | 1 + .../networking/images/engine_on_net.gliffy | 1 + .../networking/images/engine_on_net.png | Bin 0 -> 14032 bytes .../networking/images/engine_on_net.svg | 1 + .../networking/images/key_value.gliffy | 1 + .../userguide/networking/images/key_value.png | Bin 0 -> 12898 bytes .../userguide/networking/images/key_value.svg | 1 + .../networking/images/network_access.gliffy | 1 + .../networking/images/network_access.png | Bin 0 -> 30649 bytes .../networking/images/network_access.svg | 1 + .../images/overlay-network-final.gliffy | 1 + .../images/overlay-network-final.png | Bin 0 -> 28072 bytes .../images/overlay-network-final.svg | 1 + .../networking/images/overlay_network.gliffy | 1 + .../networking/images/overlay_network.png | Bin 0 -> 23276 bytes .../networking/images/overlay_network.svg | 1 + .../networking/images/working.gliffy | 1 + docs/userguide/networking/images/working.png | Bin 0 -> 18319 bytes docs/userguide/networking/images/working.svg | 1 + docs/userguide/networking/index.md | 21 + .../networking/work-with-networks.md | 863 ++ docs/userguide/storagedriver/aufs-driver.md | 216 + docs/userguide/storagedriver/btrfs-driver.md | 315 + .../storagedriver/device-mapper-driver.md | 467 ++ .../storagedriver/images/aufs_delete.jpg | Bin 0 -> 39059 bytes .../storagedriver/images/aufs_layers.jpg | Bin 0 -> 82675 bytes .../storagedriver/images/aufs_metadata.jpg | Bin 0 -> 26599 bytes .../storagedriver/images/base_device.jpg | Bin 0 -> 46684 bytes .../storagedriver/images/btfs_constructs.jpg | Bin 0 -> 63773 bytes .../images/btfs_container_layer.jpg | Bin 0 -> 67433 bytes .../storagedriver/images/btfs_layers.png | Bin 0 -> 69487 bytes .../storagedriver/images/btfs_pool.jpg | Bin 0 -> 43267 bytes .../storagedriver/images/btfs_snapshots.jpg | Bin 0 -> 19902 bytes .../storagedriver/images/btfs_subvolume.jpg | Bin 0 -> 30522 bytes .../images/container-layers-cas.jpg | Bin 0 -> 139325 bytes .../storagedriver/images/container-layers.jpg | Bin 0 -> 46046 bytes .../storagedriver/images/dm_container.jpg | Bin 0 -> 51563 bytes .../storagedriver/images/driver-pros-cons.png | Bin 0 -> 105762 bytes .../storagedriver/images/image-layers.jpg | Bin 0 -> 26599 bytes .../images/overlay_constructs.jpg | Bin 0 -> 49536 bytes .../images/overlay_constructs2.jpg | Bin 0 -> 84972 bytes .../storagedriver/images/saving-space.jpg | Bin 0 -> 57009 bytes .../storagedriver/images/shared-uuid.jpg | Bin 0 -> 251815 bytes .../storagedriver/images/shared-volume.jpg | Bin 0 -> 48857 bytes .../storagedriver/images/sharing-layers.jpg | Bin 0 -> 56036 bytes .../storagedriver/images/two_dm_container.jpg | Bin 0 -> 65447 bytes .../storagedriver/images/zfs_clones.jpg | Bin 0 -> 23027 bytes .../storagedriver/images/zfs_zpool.jpg | Bin 0 -> 30560 bytes .../storagedriver/images/zpool_blocks.jpg | Bin 0 -> 42455 bytes .../storagedriver/imagesandcontainers.md | 495 ++ docs/userguide/storagedriver/index.md | 38 + .../storagedriver/overlayfs-driver.md | 299 + docs/userguide/storagedriver/selectadriver.md | 206 + docs/userguide/storagedriver/zfs-driver.md | 296 + errors/errors.go | 41 + experimental/README.md | 81 + experimental/images/ipvlan-l3.gliffy | 1 + experimental/images/ipvlan-l3.png | Bin 0 -> 18260 bytes experimental/images/ipvlan-l3.svg | 1 + experimental/images/ipvlan_l2_simple.gliffy | 1 + experimental/images/ipvlan_l2_simple.png | Bin 0 -> 20145 bytes experimental/images/ipvlan_l2_simple.svg | 1 + .../images/macvlan-bridge-ipvlan-l2.gliffy | 1 + .../images/macvlan-bridge-ipvlan-l2.png | Bin 0 -> 14527 bytes .../images/macvlan-bridge-ipvlan-l2.svg | 1 + .../images/macvlan_bridge_simple.gliffy | 1 + experimental/images/macvlan_bridge_simple.png | Bin 0 -> 22392 bytes experimental/images/macvlan_bridge_simple.svg | 1 + .../images/multi_tenant_8021q_vlans.gliffy | 1 + .../images/multi_tenant_8021q_vlans.png | Bin 0 -> 17879 bytes .../images/multi_tenant_8021q_vlans.svg | 1 + experimental/images/vlans-deeper-look.gliffy | 1 + experimental/images/vlans-deeper-look.png | Bin 0 -> 38837 bytes experimental/images/vlans-deeper-look.svg | 1 + experimental/plugins_graphdriver.md | 321 + experimental/vlan-networks.md | 721 ++ hack/.vendor-helpers.sh | 160 + hack/Jenkins/W2L/postbuild.sh | 35 + hack/Jenkins/W2L/setup.sh | 274 + hack/Jenkins/readme.md | 3 + hack/dind | 33 + hack/generate-authors.sh | 15 + hack/install.sh | 506 ++ hack/make.sh | 355 + hack/make/.build-deb/compat | 1 + hack/make/.build-deb/control | 29 + .../.build-deb/docker-engine.bash-completion | 1 + .../.build-deb/docker-engine.docker.default | 1 + .../make/.build-deb/docker-engine.docker.init | 1 + .../.build-deb/docker-engine.docker.upstart | 1 + hack/make/.build-deb/docker-engine.install | 12 + hack/make/.build-deb/docker-engine.manpages | 1 + hack/make/.build-deb/docker-engine.postinst | 20 + hack/make/.build-deb/docker-engine.udev | 1 + hack/make/.build-deb/docs | 1 + hack/make/.build-deb/rules | 45 + .../.build-rpm/docker-engine-selinux.spec | 109 + hack/make/.build-rpm/docker-engine.spec | 230 + hack/make/.detect-daemon-osarch | 66 + hack/make/.ensure-emptyfs | 23 + hack/make/.ensure-frozen-images | 67 + hack/make/.ensure-frozen-images-windows | 25 + hack/make/.ensure-httpserver | 15 + hack/make/.ensure-nnp-test | 22 + hack/make/.ensure-syscall-test | 23 + hack/make/.go-autogen | 63 + hack/make/.integration-daemon-setup | 14 + hack/make/.integration-daemon-start | 99 + hack/make/.integration-daemon-stop | 27 + .../.resources-windows/docker.exe.manifest | 18 + hack/make/.resources-windows/docker.ico | Bin 0 -> 370070 bytes hack/make/.resources-windows/docker.png | Bin 0 -> 658195 bytes hack/make/.validate | 33 + hack/make/README.md | 17 + hack/make/binary | 65 + hack/make/build-deb | 98 + hack/make/build-rpm | 153 + hack/make/clean-apt-repo | 43 + hack/make/clean-yum-repo | 20 + hack/make/cover | 20 + hack/make/cross | 34 + hack/make/dynbinary | 10 + hack/make/dyngccgo | 11 + hack/make/gccgo | 30 + hack/make/generate-index-listing | 74 + hack/make/install-script | 63 + hack/make/release-deb | 150 + hack/make/release-rpm | 78 + hack/make/sign-repos | 55 + hack/make/test-deb-install | 68 + hack/make/test-docker-py | 18 + hack/make/test-install-script | 31 + hack/make/test-integration-cli | 18 + hack/make/test-old-apt-repo | 29 + hack/make/test-unit | 35 + hack/make/tgz | 69 + hack/make/ubuntu | 190 + hack/make/update-apt-repo | 70 + hack/make/validate-dco | 54 + hack/make/validate-default-seccomp | 27 + hack/make/validate-gofmt | 30 + hack/make/validate-lint | 30 + hack/make/validate-pkg | 32 + hack/make/validate-test | 35 + hack/make/validate-toml | 30 + hack/make/validate-vendor | 27 + hack/make/validate-vet | 31 + hack/make/win | 20 + hack/release.sh | 318 + hack/vendor.sh | 94 + image/fs.go | 184 + image/fs_test.go | 384 + image/image.go | 138 + image/image_test.go | 59 + image/rootfs.go | 8 + image/rootfs_unix.go | 23 + image/rootfs_windows.go | 37 + image/spec/v1.md | 573 ++ image/store.go | 295 + image/store_test.go | 300 + image/tarexport/load.go | 372 + image/tarexport/save.go | 319 + image/tarexport/tarexport.go | 37 + image/v1/imagev1.go | 148 + integration-cli/benchmark_test.go | 95 + integration-cli/check_test.go | 216 + integration-cli/daemon.go | 486 ++ integration-cli/docker_api_attach_test.go | 164 + integration-cli/docker_api_auth_test.go | 23 + integration-cli/docker_api_build_test.go | 257 + integration-cli/docker_api_containers_test.go | 1626 ++++ integration-cli/docker_api_create_test.go | 45 + integration-cli/docker_api_events_test.go | 73 + .../docker_api_exec_resize_test.go | 105 + integration-cli/docker_api_exec_test.go | 183 + integration-cli/docker_api_images_test.go | 129 + integration-cli/docker_api_info_test.go | 40 + integration-cli/docker_api_inspect_test.go | 183 + .../docker_api_inspect_unix_test.go | 35 + integration-cli/docker_api_logs_test.go | 89 + integration-cli/docker_api_network_test.go | 337 + integration-cli/docker_api_resize_test.go | 44 + integration-cli/docker_api_stats_test.go | 257 + integration-cli/docker_api_test.go | 101 + .../docker_api_update_unix_test.go | 35 + integration-cli/docker_api_version_test.go | 23 + integration-cli/docker_api_volumes_test.go | 88 + integration-cli/docker_cli_attach_test.go | 162 + .../docker_cli_attach_unix_test.go | 230 + integration-cli/docker_cli_authz_unix_test.go | 363 + integration-cli/docker_cli_build_test.go | 6976 +++++++++++++++++ integration-cli/docker_cli_build_unix_test.go | 206 + integration-cli/docker_cli_by_digest_test.go | 585 ++ integration-cli/docker_cli_commit_test.go | 189 + integration-cli/docker_cli_config_test.go | 138 + .../docker_cli_cp_from_container_test.go | 489 ++ integration-cli/docker_cli_cp_test.go | 665 ++ .../docker_cli_cp_to_container_test.go | 605 ++ .../docker_cli_cp_to_container_unix_test.go | 39 + integration-cli/docker_cli_cp_utils.go | 303 + integration-cli/docker_cli_create_test.go | 460 ++ .../docker_cli_daemon_experimental_test.go | 195 + ...docker_cli_daemon_not_experimental_test.go | 59 + integration-cli/docker_cli_daemon_test.go | 2161 +++++ integration-cli/docker_cli_diff_test.go | 87 + integration-cli/docker_cli_events_test.go | 647 ++ .../docker_cli_events_unix_test.go | 362 + integration-cli/docker_cli_exec_test.go | 511 ++ integration-cli/docker_cli_exec_unix_test.go | 70 + .../docker_cli_experimental_test.go | 21 + .../docker_cli_export_import_test.go | 49 + ...cker_cli_external_graphdriver_unix_test.go | 384 + integration-cli/docker_cli_help_test.go | 298 + integration-cli/docker_cli_history_test.go | 125 + integration-cli/docker_cli_images_test.go | 290 + integration-cli/docker_cli_import_test.go | 123 + integration-cli/docker_cli_info_test.go | 166 + .../docker_cli_inspect_experimental_test.go | 33 + integration-cli/docker_cli_inspect_test.go | 393 + integration-cli/docker_cli_kill_test.go | 95 + integration-cli/docker_cli_links_test.go | 213 + integration-cli/docker_cli_links_unix_test.go | 26 + integration-cli/docker_cli_login_test.go | 44 + integration-cli/docker_cli_logout_test.go | 56 + integration-cli/docker_cli_logs_bench_test.go | 32 + integration-cli/docker_cli_logs_test.go | 309 + integration-cli/docker_cli_nat_test.go | 93 + integration-cli/docker_cli_netmode_test.go | 88 + .../docker_cli_network_unix_test.go | 1513 ++++ integration-cli/docker_cli_oom_killed_test.go | 30 + integration-cli/docker_cli_pause_test.go | 67 + integration-cli/docker_cli_port_test.go | 319 + integration-cli/docker_cli_proxy_test.go | 53 + integration-cli/docker_cli_ps_test.go | 852 ++ integration-cli/docker_cli_pull_local_test.go | 423 + integration-cli/docker_cli_pull_test.go | 265 + .../docker_cli_pull_trusted_test.go | 365 + integration-cli/docker_cli_push_test.go | 728 ++ .../docker_cli_registry_user_agent_test.go | 120 + integration-cli/docker_cli_rename_test.go | 86 + integration-cli/docker_cli_restart_test.go | 249 + integration-cli/docker_cli_rm_test.go | 86 + integration-cli/docker_cli_rmi_test.go | 362 + integration-cli/docker_cli_run_test.go | 4311 ++++++++++ integration-cli/docker_cli_run_unix_test.go | 1009 +++ integration-cli/docker_cli_save_load_test.go | 352 + .../docker_cli_save_load_unix_test.go | 87 + integration-cli/docker_cli_search_test.go | 55 + integration-cli/docker_cli_sni_test.go | 44 + integration-cli/docker_cli_start_test.go | 173 + ...ocker_cli_start_volume_driver_unix_test.go | 443 ++ integration-cli/docker_cli_stats_test.go | 162 + integration-cli/docker_cli_tag_test.go | 251 + integration-cli/docker_cli_top_test.go | 43 + integration-cli/docker_cli_update_test.go | 31 + .../docker_cli_update_unix_test.go | 212 + integration-cli/docker_cli_userns_test.go | 86 + integration-cli/docker_cli_v2_only_test.go | 125 + integration-cli/docker_cli_version_test.go | 58 + integration-cli/docker_cli_volume_test.go | 283 + integration-cli/docker_cli_wait_test.go | 95 + .../docker_experimental_network_test.go | 591 ++ integration-cli/docker_hub_pull_suite_test.go | 90 + integration-cli/docker_test_vars.go | 129 + integration-cli/docker_utils.go | 1495 ++++ integration-cli/events_utils.go | 206 + .../auth/docker-credential-shell-test | 33 + integration-cli/fixtures/https/ca.pem | 23 + .../fixtures/https/client-cert.pem | 73 + integration-cli/fixtures/https/client-key.pem | 16 + .../fixtures/https/client-rogue-cert.pem | 73 + .../fixtures/https/client-rogue-key.pem | 16 + .../fixtures/https/server-cert.pem | 76 + integration-cli/fixtures/https/server-key.pem | 16 + .../fixtures/https/server-rogue-cert.pem | 76 + .../fixtures/https/server-rogue-key.pem | 16 + integration-cli/fixtures/load/emptyLayer.tar | Bin 0 -> 30720 bytes integration-cli/fixtures/notary/delgkey1.crt | 24 + integration-cli/fixtures/notary/delgkey1.key | 27 + integration-cli/fixtures/notary/delgkey2.crt | 24 + integration-cli/fixtures/notary/delgkey2.key | 27 + integration-cli/fixtures/notary/delgkey3.crt | 24 + integration-cli/fixtures/notary/delgkey3.key | 27 + integration-cli/fixtures/notary/delgkey4.crt | 24 + integration-cli/fixtures/notary/delgkey4.key | 27 + .../fixtures/notary/localhost.cert | 19 + integration-cli/fixtures/notary/localhost.key | 27 + integration-cli/fixtures/registry/cert.pem | 21 + integration-cli/npipe.go | 12 + integration-cli/npipe_windows.go | 12 + integration-cli/registry.go | 175 + integration-cli/registry_mock.go | 55 + integration-cli/requirements.go | 189 + integration-cli/requirements_unix.go | 106 + integration-cli/test_vars_exec.go | 8 + integration-cli/test_vars_noexec.go | 8 + integration-cli/test_vars_noseccomp.go | 8 + integration-cli/test_vars_seccomp.go | 8 + integration-cli/test_vars_unix.go | 16 + integration-cli/test_vars_windows.go | 18 + integration-cli/trust_server.go | 336 + integration-cli/utils.go | 85 + layer/empty.go | 48 + layer/empty_test.go | 46 + layer/filestore.go | 326 + layer/filestore_test.go | 104 + layer/layer.go | 262 + layer/layer_store.go | 666 ++ layer/layer_test.go | 788 ++ layer/layer_unix.go | 9 + layer/layer_unix_test.go | 71 + layer/layer_windows.go | 98 + layer/migration.go | 256 + layer/migration_test.go | 448 ++ layer/mount_test.go | 230 + layer/mounted_layer.go | 188 + layer/ro_layer.go | 164 + libcontainerd/client.go | 46 + libcontainerd/client_linux.go | 401 + libcontainerd/client_liverestore_linux.go | 83 + libcontainerd/client_shutdownrestore_linux.go | 46 + libcontainerd/client_windows.go | 567 ++ libcontainerd/container.go | 40 + libcontainerd/container_linux.go | 209 + libcontainerd/container_windows.go | 206 + libcontainerd/pausemonitor_linux.go | 31 + libcontainerd/process.go | 18 + libcontainerd/process_linux.go | 110 + libcontainerd/process_windows.go | 27 + libcontainerd/queue_linux.go | 29 + libcontainerd/remote.go | 18 + libcontainerd/remote_linux.go | 449 ++ libcontainerd/remote_windows.go | 27 + libcontainerd/types.go | 60 + libcontainerd/types_linux.go | 47 + libcontainerd/types_windows.go | 24 + libcontainerd/utils_linux.go | 52 + libcontainerd/utils_windows.go | 16 + libcontainerd/windowsoci/oci_windows.go | 190 + libcontainerd/windowsoci/unsupported.go | 3 + man/Dockerfile | 7 + man/Dockerfile.5.md | 472 ++ man/README.md | 33 + man/config-json.5.md | 72 + man/docker-attach.1.md | 99 + man/docker-build.1.md | 311 + man/docker-commit.1.md | 70 + man/docker-cp.1.md | 166 + man/docker-create.1.md | 470 ++ man/docker-daemon.8.md | 568 ++ man/docker-diff.1.md | 49 + man/docker-events.1.md | 96 + man/docker-exec.1.md | 64 + man/docker-export.1.md | 46 + man/docker-history.1.md | 52 + man/docker-images.1.md | 111 + man/docker-import.1.md | 72 + man/docker-info.1.md | 66 + man/docker-inspect.1.md | 322 + man/docker-kill.1.md | 28 + man/docker-load.1.md | 49 + man/docker-login.1.md | 56 + man/docker-logout.1.md | 32 + man/docker-logs.1.md | 64 + man/docker-network-connect.1.md | 69 + man/docker-network-create.1.md | 170 + man/docker-network-disconnect.1.md | 36 + man/docker-network-inspect.1.md | 112 + man/docker-network-ls.1.md | 138 + man/docker-network-rm.1.md | 43 + man/docker-pause.1.md | 30 + man/docker-port.1.md | 47 + man/docker-ps.1.md | 141 + man/docker-pull.1.md | 220 + man/docker-push.1.md | 54 + man/docker-rename.1.md | 15 + man/docker-restart.1.md | 26 + man/docker-rm.1.md | 72 + man/docker-rmi.1.md | 42 + man/docker-run.1.md | 955 +++ man/docker-save.1.md | 45 + man/docker-search.1.md | 65 + man/docker-start.1.md | 39 + man/docker-stats.1.md | 43 + man/docker-stop.1.md | 30 + man/docker-tag.1.md | 61 + man/docker-top.1.md | 36 + man/docker-unpause.1.md | 27 + man/docker-update.1.md | 108 + man/docker-version.1.md | 62 + man/docker-volume-create.1.md | 65 + man/docker-volume-inspect.1.md | 29 + man/docker-volume-ls.1.md | 30 + man/docker-volume-rm.1.md | 26 + man/docker-volume.1.md | 51 + man/docker-wait.1.md | 30 + man/docker.1.md | 244 + man/md2man-all.sh | 22 + migrate/v1/migratev1.go | 508 ++ migrate/v1/migratev1_test.go | 429 + oci/defaults_linux.go | 210 + oci/defaults_windows.go | 23 + opts/hosts.go | 148 + opts/hosts_test.go | 154 + opts/hosts_unix.go | 8 + opts/hosts_windows.go | 6 + opts/ip.go | 42 + opts/ip_test.go | 54 + opts/opts.go | 242 + opts/opts_test.go | 232 + opts/opts_unix.go | 6 + opts/opts_windows.go | 56 + pkg/README.md | 11 + pkg/aaparser/aaparser.go | 92 + pkg/aaparser/aaparser_test.go | 73 + pkg/archive/README.md | 1 + pkg/archive/archive.go | 1087 +++ pkg/archive/archive_test.go | 1148 +++ pkg/archive/archive_unix.go | 112 + pkg/archive/archive_unix_test.go | 245 + pkg/archive/archive_windows.go | 70 + pkg/archive/archive_windows_test.go | 91 + pkg/archive/changes.go | 416 + pkg/archive/changes_linux.go | 285 + pkg/archive/changes_other.go | 97 + pkg/archive/changes_posix_test.go | 127 + pkg/archive/changes_test.go | 565 ++ pkg/archive/changes_unix.go | 36 + pkg/archive/changes_windows.go | 30 + pkg/archive/copy.go | 458 ++ pkg/archive/copy_unix.go | 11 + pkg/archive/copy_unix_test.go | 978 +++ pkg/archive/copy_windows.go | 9 + pkg/archive/diff.go | 279 + pkg/archive/diff_test.go | 386 + pkg/archive/example_changes.go | 97 + pkg/archive/testdata/broken.tar | Bin 0 -> 13824 bytes pkg/archive/time_linux.go | 16 + pkg/archive/time_unsupported.go | 16 + pkg/archive/utils_test.go | 166 + pkg/archive/whiteouts.go | 23 + pkg/archive/wrap.go | 59 + pkg/archive/wrap_test.go | 98 + pkg/authorization/api.go | 54 + pkg/authorization/authz.go | 165 + pkg/authorization/authz_unix_test.go | 276 + pkg/authorization/plugin.go | 83 + pkg/authorization/response.go | 203 + pkg/broadcaster/unbuffered.go | 49 + pkg/broadcaster/unbuffered_test.go | 162 + pkg/chrootarchive/archive.go | 97 + pkg/chrootarchive/archive_test.go | 394 + pkg/chrootarchive/archive_unix.go | 94 + pkg/chrootarchive/archive_windows.go | 22 + pkg/chrootarchive/diff.go | 19 + pkg/chrootarchive/diff_unix.go | 120 + pkg/chrootarchive/diff_windows.go | 44 + pkg/chrootarchive/init_unix.go | 28 + pkg/chrootarchive/init_windows.go | 4 + pkg/devicemapper/devmapper.go | 807 ++ pkg/devicemapper/devmapper_log.go | 35 + pkg/devicemapper/devmapper_wrapper.go | 251 + .../devmapper_wrapper_deferred_remove.go | 34 + .../devmapper_wrapper_no_deferred_remove.go | 15 + pkg/devicemapper/ioctl.go | 27 + pkg/devicemapper/log.go | 11 + pkg/directory/directory.go | 26 + pkg/directory/directory_test.go | 185 + pkg/directory/directory_unix.go | 39 + pkg/directory/directory_windows.go | 28 + pkg/discovery/README.md | 41 + pkg/discovery/backends.go | 107 + pkg/discovery/discovery.go | 35 + pkg/discovery/discovery_test.go | 137 + pkg/discovery/entry.go | 94 + pkg/discovery/file/file.go | 109 + pkg/discovery/file/file_test.go | 114 + pkg/discovery/generator.go | 35 + pkg/discovery/generator_test.go | 53 + pkg/discovery/kv/kv.go | 192 + pkg/discovery/kv/kv_test.go | 324 + pkg/discovery/memory/memory.go | 83 + pkg/discovery/memory/memory_test.go | 48 + pkg/discovery/nodes/nodes.go | 54 + pkg/discovery/nodes/nodes_test.go | 51 + pkg/filenotify/filenotify.go | 40 + pkg/filenotify/fsnotify.go | 18 + pkg/filenotify/poller.go | 204 + pkg/filenotify/poller_test.go | 119 + pkg/fileutils/fileutils.go | 283 + pkg/fileutils/fileutils_test.go | 585 ++ pkg/fileutils/fileutils_unix.go | 22 + pkg/fileutils/fileutils_windows.go | 7 + pkg/gitutils/gitutils.go | 100 + pkg/gitutils/gitutils_test.go | 204 + pkg/graphdb/conn_sqlite3.go | 15 + pkg/graphdb/conn_sqlite3_unix.go | 7 + pkg/graphdb/conn_sqlite3_windows.go | 7 + pkg/graphdb/conn_unsupported.go | 8 + pkg/graphdb/graphdb.go | 551 ++ pkg/graphdb/graphdb_test.go | 721 ++ pkg/graphdb/sort.go | 27 + pkg/graphdb/sort_test.go | 29 + pkg/graphdb/utils.go | 32 + pkg/homedir/homedir.go | 39 + pkg/homedir/homedir_test.go | 24 + pkg/httputils/httputils.go | 56 + pkg/httputils/httputils_test.go | 115 + pkg/httputils/mimetype.go | 30 + pkg/httputils/mimetype_test.go | 13 + pkg/httputils/resumablerequestreader.go | 95 + pkg/httputils/resumablerequestreader_test.go | 307 + pkg/idtools/idtools.go | 197 + pkg/idtools/idtools_unix.go | 60 + pkg/idtools/idtools_unix_test.go | 271 + pkg/idtools/idtools_windows.go | 18 + pkg/idtools/usergroupadd_linux.go | 188 + pkg/idtools/usergroupadd_unsupported.go | 12 + pkg/integration/checker/checker.go | 46 + pkg/integration/dockerCmd_utils.go | 78 + pkg/integration/dockerCmd_utils_test.go | 405 + pkg/integration/utils.go | 361 + pkg/integration/utils_test.go | 570 ++ pkg/ioutils/bytespipe.go | 156 + pkg/ioutils/bytespipe_test.go | 158 + pkg/ioutils/fmt.go | 22 + pkg/ioutils/fmt_test.go | 17 + pkg/ioutils/multireader.go | 226 + pkg/ioutils/multireader_test.go | 149 + pkg/ioutils/readers.go | 154 + pkg/ioutils/readers_test.go | 94 + pkg/ioutils/scheduler.go | 6 + pkg/ioutils/scheduler_gccgo.go | 13 + pkg/ioutils/temp_unix.go | 10 + pkg/ioutils/temp_windows.go | 18 + pkg/ioutils/writeflusher.go | 92 + pkg/ioutils/writers.go | 66 + pkg/ioutils/writers_test.go | 65 + pkg/jsonlog/jsonlog.go | 40 + pkg/jsonlog/jsonlog_marshalling.go | 178 + pkg/jsonlog/jsonlog_marshalling_test.go | 34 + pkg/jsonlog/jsonlogbytes.go | 122 + pkg/jsonlog/jsonlogbytes_test.go | 39 + pkg/jsonlog/time_marshalling.go | 27 + pkg/jsonlog/time_marshalling_test.go | 47 + pkg/jsonmessage/jsonmessage.go | 221 + pkg/jsonmessage/jsonmessage_test.go | 231 + pkg/locker/README.md | 65 + pkg/locker/locker.go | 112 + pkg/locker/locker_test.go | 124 + pkg/longpath/longpath.go | 26 + pkg/longpath/longpath_test.go | 22 + pkg/loopback/attach_loopback.go | 137 + pkg/loopback/ioctl.go | 53 + pkg/loopback/loop_wrapper.go | 52 + pkg/loopback/loopback.go | 63 + pkg/mflag/LICENSE | 27 + pkg/mflag/README.md | 40 + pkg/mflag/example/example.go | 36 + pkg/mflag/flag.go | 1280 +++ pkg/mflag/flag_test.go | 527 ++ pkg/mount/flags.go | 92 + pkg/mount/flags_freebsd.go | 48 + pkg/mount/flags_linux.go | 85 + pkg/mount/flags_unsupported.go | 30 + pkg/mount/mount.go | 74 + pkg/mount/mount_unix_test.go | 139 + pkg/mount/mounter_freebsd.go | 59 + pkg/mount/mounter_linux.go | 21 + pkg/mount/mounter_unsupported.go | 11 + pkg/mount/mountinfo.go | 40 + pkg/mount/mountinfo_freebsd.go | 41 + pkg/mount/mountinfo_linux.go | 95 + pkg/mount/mountinfo_linux_test.go | 476 ++ pkg/mount/mountinfo_unsupported.go | 12 + pkg/mount/mountinfo_windows.go | 6 + pkg/mount/sharedsubtree_linux.go | 69 + pkg/mount/sharedsubtree_linux_test.go | 331 + .../cmd/names-generator/main.go | 11 + pkg/namesgenerator/names-generator.go | 524 ++ pkg/namesgenerator/names-generator_test.go | 45 + pkg/parsers/kernel/kernel.go | 100 + pkg/parsers/kernel/kernel_unix_test.go | 96 + pkg/parsers/kernel/kernel_windows.go | 67 + pkg/parsers/kernel/uname_linux.go | 19 + pkg/parsers/kernel/uname_unsupported.go | 18 + .../operatingsystem_freebsd.go | 18 + .../operatingsystem/operatingsystem_linux.go | 77 + .../operatingsystem_unix_test.go | 247 + .../operatingsystem_windows.go | 49 + pkg/parsers/parsers.go | 69 + pkg/parsers/parsers_test.go | 70 + pkg/pidfile/pidfile.go | 50 + pkg/pidfile/pidfile_test.go | 32 + pkg/platform/architecture_freebsd.go | 15 + pkg/platform/architecture_linux.go | 16 + pkg/platform/architecture_windows.go | 52 + pkg/platform/platform.go | 23 + pkg/platform/utsname_int8.go | 18 + pkg/platform/utsname_uint8.go | 18 + pkg/plugins/client.go | 186 + pkg/plugins/client_test.go | 134 + pkg/plugins/discovery.go | 132 + pkg/plugins/discovery_test.go | 119 + pkg/plugins/discovery_unix_test.go | 61 + pkg/plugins/errors.go | 33 + pkg/plugins/pluginrpc-gen/README.md | 68 + pkg/plugins/pluginrpc-gen/fixtures/foo.go | 41 + pkg/plugins/pluginrpc-gen/main.go | 91 + pkg/plugins/pluginrpc-gen/parser.go | 163 + pkg/plugins/pluginrpc-gen/parser_test.go | 168 + pkg/plugins/pluginrpc-gen/template.go | 97 + pkg/plugins/plugins.go | 257 + pkg/plugins/transport/http.go | 36 + pkg/plugins/transport/transport.go | 36 + pkg/pools/pools.go | 119 + pkg/pools/pools_test.go | 161 + pkg/progress/progress.go | 73 + pkg/progress/progressreader.go | 59 + pkg/progress/progressreader_test.go | 75 + pkg/promise/promise.go | 11 + pkg/proxy/network_proxy_test.go | 216 + pkg/proxy/proxy.go | 37 + pkg/proxy/stub_proxy.go | 31 + pkg/proxy/tcp_proxy.go | 99 + pkg/proxy/udp_proxy.go | 169 + pkg/pubsub/publisher.go | 111 + pkg/pubsub/publisher_test.go | 142 + pkg/random/random.go | 71 + pkg/random/random_test.go | 22 + pkg/reexec/README.md | 5 + pkg/reexec/command_freebsd.go | 23 + pkg/reexec/command_linux.go | 28 + pkg/reexec/command_unsupported.go | 12 + pkg/reexec/command_windows.go | 23 + pkg/reexec/reexec.go | 47 + pkg/registrar/registrar.go | 127 + pkg/registrar/registrar_test.go | 119 + pkg/signal/README.md | 1 + pkg/signal/signal.go | 54 + pkg/signal/signal_darwin.go | 41 + pkg/signal/signal_freebsd.go | 43 + pkg/signal/signal_linux.go | 80 + pkg/signal/signal_unix.go | 21 + pkg/signal/signal_unsupported.go | 10 + pkg/signal/signal_windows.go | 28 + pkg/signal/trap.go | 74 + pkg/stdcopy/stdcopy.go | 178 + pkg/stdcopy/stdcopy_test.go | 260 + pkg/streamformatter/streamformatter.go | 172 + pkg/streamformatter/streamformatter_test.go | 104 + pkg/stringid/README.md | 1 + pkg/stringid/stringid.go | 71 + pkg/stringid/stringid_test.go | 56 + pkg/stringutils/README.md | 1 + pkg/stringutils/stringutils.go | 87 + pkg/stringutils/stringutils_test.go | 105 + pkg/symlink/LICENSE.APACHE | 191 + pkg/symlink/LICENSE.BSD | 27 + pkg/symlink/README.md | 6 + pkg/symlink/fs.go | 143 + pkg/symlink/fs_unix.go | 11 + pkg/symlink/fs_unix_test.go | 407 + pkg/symlink/fs_windows.go | 155 + pkg/sysinfo/README.md | 1 + pkg/sysinfo/sysinfo.go | 128 + pkg/sysinfo/sysinfo_freebsd.go | 7 + pkg/sysinfo/sysinfo_linux.go | 246 + pkg/sysinfo/sysinfo_linux_test.go | 58 + pkg/sysinfo/sysinfo_test.go | 26 + pkg/sysinfo/sysinfo_windows.go | 7 + pkg/system/chtimes.go | 52 + pkg/system/chtimes_test.go | 94 + pkg/system/chtimes_unix.go | 14 + pkg/system/chtimes_unix_test.go | 91 + pkg/system/chtimes_windows.go | 27 + pkg/system/chtimes_windows_test.go | 86 + pkg/system/errors.go | 10 + pkg/system/events_windows.go | 83 + pkg/system/filesys.go | 19 + pkg/system/filesys_windows.go | 82 + pkg/system/lstat.go | 19 + pkg/system/lstat_unix_test.go | 30 + pkg/system/lstat_windows.go | 25 + pkg/system/meminfo.go | 17 + pkg/system/meminfo_linux.go | 65 + pkg/system/meminfo_unix_test.go | 40 + pkg/system/meminfo_unsupported.go | 8 + pkg/system/meminfo_windows.go | 44 + pkg/system/mknod.go | 22 + pkg/system/mknod_windows.go | 13 + pkg/system/path_unix.go | 8 + pkg/system/path_windows.go | 7 + pkg/system/stat.go | 53 + pkg/system/stat_freebsd.go | 27 + pkg/system/stat_linux.go | 33 + pkg/system/stat_openbsd.go | 15 + pkg/system/stat_solaris.go | 17 + pkg/system/stat_unix_test.go | 39 + pkg/system/stat_unsupported.go | 17 + pkg/system/stat_windows.go | 43 + pkg/system/syscall_unix.go | 17 + pkg/system/syscall_windows.go | 60 + pkg/system/umask.go | 13 + pkg/system/umask_windows.go | 9 + pkg/system/utimes_darwin.go | 8 + pkg/system/utimes_freebsd.go | 22 + pkg/system/utimes_linux.go | 26 + pkg/system/utimes_unix_test.go | 68 + pkg/system/utimes_unsupported.go | 10 + pkg/system/xattrs_linux.go | 63 + pkg/system/xattrs_unsupported.go | 13 + pkg/tailfile/tailfile.go | 66 + pkg/tailfile/tailfile_test.go | 148 + pkg/tarsum/builder_context.go | 21 + pkg/tarsum/builder_context_test.go | 63 + pkg/tarsum/fileinfosums.go | 126 + pkg/tarsum/fileinfosums_test.go | 62 + pkg/tarsum/tarsum.go | 294 + pkg/tarsum/tarsum_spec.md | 230 + pkg/tarsum/tarsum_test.go | 656 ++ .../json | 1 + .../layer.tar | Bin 0 -> 9216 bytes .../json | 1 + .../layer.tar | Bin 0 -> 1536 bytes pkg/tarsum/testdata/collision/collision-0.tar | Bin 0 -> 10240 bytes pkg/tarsum/testdata/collision/collision-1.tar | Bin 0 -> 10240 bytes pkg/tarsum/testdata/collision/collision-2.tar | Bin 0 -> 10240 bytes pkg/tarsum/testdata/collision/collision-3.tar | Bin 0 -> 10240 bytes pkg/tarsum/testdata/xattr/json | 1 + pkg/tarsum/testdata/xattr/layer.tar | Bin 0 -> 2560 bytes pkg/tarsum/versioning.go | 150 + pkg/tarsum/versioning_test.go | 98 + pkg/tarsum/writercloser.go | 22 + pkg/term/ascii.go | 66 + pkg/term/ascii_test.go | 43 + pkg/term/tc_linux_cgo.go | 50 + pkg/term/tc_other.go | 19 + pkg/term/term.go | 131 + pkg/term/term_windows.go | 305 + pkg/term/termios_darwin.go | 69 + pkg/term/termios_freebsd.go | 69 + pkg/term/termios_linux.go | 47 + pkg/term/termios_openbsd.go | 69 + pkg/term/windows/ansi_reader.go | 257 + pkg/term/windows/ansi_writer.go | 76 + pkg/term/windows/console.go | 97 + pkg/term/windows/windows.go | 5 + pkg/term/windows/windows_test.go | 3 + pkg/tlsconfig/config.go | 133 + pkg/truncindex/truncindex.go | 137 + pkg/truncindex/truncindex_test.go | 429 + pkg/urlutil/urlutil.go | 50 + pkg/urlutil/urlutil_test.go | 69 + pkg/useragent/README.md | 1 + pkg/useragent/useragent.go | 55 + pkg/useragent/useragent_test.go | 31 + pkg/version/version.go | 68 + pkg/version/version_test.go | 27 + profiles/apparmor/apparmor.go | 115 + profiles/apparmor/template.go | 46 + profiles/seccomp/default.json | 1628 ++++ profiles/seccomp/fixtures/example.json | 27 + profiles/seccomp/generate.go | 32 + profiles/seccomp/seccomp.go | 74 + profiles/seccomp/seccomp_default.go | 1659 ++++ profiles/seccomp/seccomp_test.go | 28 + profiles/seccomp/seccomp_unsupported.go | 10 + project/ARM.md | 45 + project/BRANCHES-AND-TAGS.md | 35 + project/CONTRIBUTORS.md | 1 + project/GOVERNANCE.md | 17 + project/IRC-ADMINISTRATION.md | 37 + project/ISSUE-TRIAGE.md | 91 + project/PACKAGE-REPO-MAINTENANCE.md | 74 + project/PACKAGERS.md | 308 + project/PATCH-RELEASES.md | 68 + project/PRINCIPLES.md | 19 + project/README.md | 24 + project/RELEASE-CHECKLIST.md | 512 ++ project/RELEASE-PROCESS.md | 78 + project/REVIEWING.md | 195 + project/TOOLS.md | 63 + reference/reference.go | 211 + reference/reference_test.go | 275 + reference/store.go | 298 + reference/store_test.go | 356 + registry/auth.go | 262 + registry/auth_test.go | 120 + registry/config.go | 274 + registry/config_test.go | 49 + registry/config_unix.go | 16 + registry/config_windows.go | 18 + registry/endpoint_test.go | 78 + registry/endpoint_v1.go | 198 + registry/reference.go | 68 + registry/registry.go | 180 + registry/registry_mock_test.go | 476 ++ registry/registry_test.go | 873 +++ registry/service.go | 205 + registry/service_v1.go | 53 + registry/service_v2.go | 79 + registry/session.go | 770 ++ registry/types.go | 70 + restartmanager/restartmanager.go | 131 + restartmanager/restartmanager_test.go | 34 + runconfig/compare.go | 61 + runconfig/compare_test.go | 126 + runconfig/config.go | 71 + runconfig/config_test.go | 134 + runconfig/config_unix.go | 59 + runconfig/config_windows.go | 19 + runconfig/errors.go | 40 + .../fixtures/unix/container_config_1_14.json | 30 + .../fixtures/unix/container_config_1_17.json | 50 + .../fixtures/unix/container_config_1_19.json | 58 + .../unix/container_hostconfig_1_14.json | 18 + .../unix/container_hostconfig_1_19.json | 30 + .../windows/container_config_1_19.json | 58 + runconfig/hostconfig.go | 35 + runconfig/hostconfig_test.go | 222 + runconfig/hostconfig_unix.go | 89 + runconfig/hostconfig_windows.go | 46 + runconfig/opts/envfile.go | 67 + runconfig/opts/envfile_test.go | 142 + runconfig/opts/fixtures/valid.env | 1 + runconfig/opts/fixtures/valid.label | 1 + runconfig/opts/opts.go | 70 + runconfig/opts/opts_test.go | 108 + runconfig/opts/parse.go | 766 ++ runconfig/opts/parse_test.go | 839 ++ runconfig/opts/throttledevice.go | 108 + runconfig/opts/ulimit.go | 52 + runconfig/opts/ulimit_test.go | 42 + runconfig/opts/weightdevice.go | 84 + runconfig/streams.go | 109 + utils/debug.go | 26 + utils/debug_test.go | 43 + utils/experimental.go | 9 + utils/names.go | 12 + utils/process_unix.go | 22 + utils/process_windows.go | 20 + utils/stubs.go | 9 + utils/templates/templates.go | 33 + utils/templates/templates_test.go | 38 + utils/utils.go | 87 + utils/utils_test.go | 21 + .../src/github.com/docker/notary/.gitignore | 11 + .../src/github.com/docker/notary/CHANGELOG.md | 26 + .../github.com/docker/notary/CONTRIBUTING.md | 85 + .../src/github.com/docker/notary/CONTRIBUTORS | 4 + .../src/github.com/docker/notary/Dockerfile | 15 + vendor/src/github.com/docker/notary/LICENSE | 201 + .../src/github.com/docker/notary/MAINTAINERS | 58 + vendor/src/github.com/docker/notary/Makefile | 204 + .../github.com/docker/notary/NOTARY_VERSION | 1 + vendor/src/github.com/docker/notary/README.md | 194 + .../src/github.com/docker/notary/ROADMAP.md | 7 + .../github.com/docker/notary/certs/certs.go | 280 + .../src/github.com/docker/notary/circle.yml | 82 + .../docker/notary/client/changelist/change.go | 101 + .../notary/client/changelist/changelist.go | 59 + .../client/changelist/file_changelist.go | 176 + .../notary/client/changelist/interface.go | 70 + .../github.com/docker/notary/client/client.go | 965 +++ .../docker/notary/client/delegations.go | 294 + .../docker/notary/client/helpers.go | 237 + .../github.com/docker/notary/client/repo.go | 27 + .../docker/notary/client/repo_pkcs11.go | 33 + vendor/src/github.com/docker/notary/const.go | 61 + .../src/github.com/docker/notary/coverpkg.sh | 10 + .../notary/cryptoservice/certificate.go | 48 + .../notary/cryptoservice/crypto_service.go | 155 + .../notary/cryptoservice/import_export.go | 313 + .../docker/notary/docker-compose.yml | 34 + .../docker/notary/passphrase/passphrase.go | 201 + .../docker/notary/server.Dockerfile | 28 + .../docker/notary/signer.Dockerfile | 30 + .../docker/notary/trustmanager/filestore.go | 191 + .../notary/trustmanager/keyfilestore.go | 497 ++ .../docker/notary/trustmanager/keystore.go | 57 + .../docker/notary/trustmanager/memorystore.go | 67 + .../docker/notary/trustmanager/store.go | 52 + .../notary/trustmanager/x509filestore.go | 276 + .../notary/trustmanager/x509memstore.go | 203 + .../docker/notary/trustmanager/x509store.go | 144 + .../docker/notary/trustmanager/x509utils.go | 613 ++ .../notary/trustmanager/yubikey/non_pkcs11.go | 9 + .../trustmanager/yubikey/pkcs11_darwin.go | 9 + .../trustmanager/yubikey/pkcs11_interface.go | 40 + .../trustmanager/yubikey/pkcs11_linux.go | 10 + .../trustmanager/yubikey/yubikeystore.go | 888 +++ .../src/github.com/docker/notary/tuf/LICENSE | 30 + .../github.com/docker/notary/tuf/README.md | 36 + .../docker/notary/tuf/client/client.go | 546 ++ .../docker/notary/tuf/client/errors.go | 23 + .../docker/notary/tuf/data/errors.go | 22 + .../github.com/docker/notary/tuf/data/keys.go | 528 ++ .../docker/notary/tuf/data/roles.go | 297 + .../github.com/docker/notary/tuf/data/root.go | 161 + .../docker/notary/tuf/data/serializer.go | 36 + .../docker/notary/tuf/data/snapshot.go | 163 + .../docker/notary/tuf/data/targets.go | 194 + .../docker/notary/tuf/data/timestamp.go | 132 + .../docker/notary/tuf/data/types.go | 275 + .../docker/notary/tuf/signed/ed25519.go | 107 + .../docker/notary/tuf/signed/errors.go | 72 + .../docker/notary/tuf/signed/interface.go | 46 + .../docker/notary/tuf/signed/sign.go | 103 + .../docker/notary/tuf/signed/verifiers.go | 283 + .../docker/notary/tuf/signed/verify.go | 155 + .../docker/notary/tuf/store/errors.go | 13 + .../docker/notary/tuf/store/filestore.go | 101 + .../docker/notary/tuf/store/httpstore.go | 296 + .../docker/notary/tuf/store/interfaces.go | 28 + .../docker/notary/tuf/store/memorystore.go | 106 + .../docker/notary/tuf/store/offlinestore.go | 53 + .../src/github.com/docker/notary/tuf/tuf.go | 926 +++ .../docker/notary/tuf/utils/role_sort.go | 31 + .../docker/notary/tuf/utils/stack.go | 85 + .../docker/notary/tuf/utils/util.go | 109 + .../docker/notary/tuf/utils/utils.go | 152 + .../docker/notary/tuf/validation/errors.go | 126 + volume/drivers/adapter.go | 106 + volume/drivers/extpoint.go | 164 + volume/drivers/extpoint_test.go | 23 + volume/drivers/proxy.go | 207 + volume/drivers/proxy_test.go | 122 + volume/local/local.go | 330 + volume/local/local_test.go | 249 + volume/local/local_unix.go | 69 + volume/local/local_windows.go | 34 + volume/store/errors.go | 74 + volume/store/store.go | 506 ++ volume/store/store_test.go | 201 + volume/store/store_unix.go | 9 + volume/store/store_windows.go | 12 + volume/testutils/testutils.go | 105 + volume/volume.go | 133 + volume/volume_copy.go | 28 + volume/volume_propagation_linux.go | 44 + volume/volume_propagation_linux_test.go | 65 + volume/volume_propagation_unsupported.go | 22 + volume/volume_test.go | 212 + volume/volume_unix.go | 186 + volume/volume_windows.go | 199 + 1785 files changed, 264420 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .gitignore create mode 100644 .mailmap create mode 100644 AUTHORS create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 Dockerfile.aarch64 create mode 100644 Dockerfile.armhf create mode 100644 Dockerfile.gccgo create mode 100644 Dockerfile.ppc64le create mode 100644 Dockerfile.s390x create mode 100644 Dockerfile.simple create mode 100755 Dockerfile.windows create mode 100644 LICENSE create mode 100644 MAINTAINERS create mode 100644 Makefile create mode 100644 NOTICE create mode 100644 README.md create mode 100644 ROADMAP.md create mode 100644 VENDORING.md create mode 100644 VERSION create mode 100644 api/README.md create mode 100644 api/client/attach.go create mode 100644 api/client/build.go create mode 100644 api/client/cli.go create mode 100644 api/client/client.go create mode 100644 api/client/commit.go create mode 100644 api/client/cp.go create mode 100644 api/client/create.go create mode 100644 api/client/diff.go create mode 100644 api/client/events.go create mode 100644 api/client/exec.go create mode 100644 api/client/exec_test.go create mode 100644 api/client/export.go create mode 100644 api/client/formatter/custom.go create mode 100644 api/client/formatter/custom_test.go create mode 100644 api/client/formatter/formatter.go create mode 100644 api/client/formatter/formatter_test.go create mode 100644 api/client/hijack.go create mode 100644 api/client/history.go create mode 100644 api/client/images.go create mode 100644 api/client/import.go create mode 100644 api/client/info.go create mode 100644 api/client/inspect.go create mode 100644 api/client/inspect/inspector.go create mode 100644 api/client/inspect/inspector_go14.go create mode 100644 api/client/inspect/inspector_go15.go create mode 100644 api/client/inspect/inspector_test.go create mode 100644 api/client/kill.go create mode 100644 api/client/load.go create mode 100644 api/client/login.go create mode 100644 api/client/logout.go create mode 100644 api/client/logs.go create mode 100644 api/client/network.go create mode 100644 api/client/pause.go create mode 100644 api/client/port.go create mode 100644 api/client/ps.go create mode 100644 api/client/pull.go create mode 100644 api/client/push.go create mode 100644 api/client/rename.go create mode 100644 api/client/restart.go create mode 100644 api/client/rm.go create mode 100644 api/client/rmi.go create mode 100644 api/client/run.go create mode 100644 api/client/save.go create mode 100644 api/client/search.go create mode 100644 api/client/start.go create mode 100644 api/client/stats.go create mode 100644 api/client/stats_helpers.go create mode 100644 api/client/stats_unit_test.go create mode 100644 api/client/stop.go create mode 100644 api/client/tag.go create mode 100644 api/client/top.go create mode 100644 api/client/trust.go create mode 100644 api/client/trust_test.go create mode 100644 api/client/unpause.go create mode 100644 api/client/update.go create mode 100644 api/client/utils.go create mode 100644 api/client/version.go create mode 100644 api/client/volume.go create mode 100644 api/client/wait.go create mode 100644 api/common.go create mode 100644 api/common_test.go create mode 100644 api/fixtures/keyfile create mode 100644 api/server/httputils/errors.go create mode 100644 api/server/httputils/form.go create mode 100644 api/server/httputils/form_test.go create mode 100644 api/server/httputils/httputils.go create mode 100644 api/server/middleware.go create mode 100644 api/server/middleware/authorization.go create mode 100644 api/server/middleware/cors.go create mode 100644 api/server/middleware/debug.go create mode 100644 api/server/middleware/middleware.go create mode 100644 api/server/middleware/user_agent.go create mode 100644 api/server/middleware/version.go create mode 100644 api/server/middleware/version_test.go create mode 100644 api/server/profiler.go create mode 100644 api/server/router/build/backend.go create mode 100644 api/server/router/build/build.go create mode 100644 api/server/router/build/build_routes.go create mode 100644 api/server/router/container/backend.go create mode 100644 api/server/router/container/container.go create mode 100644 api/server/router/container/container_routes.go create mode 100644 api/server/router/container/copy.go create mode 100644 api/server/router/container/exec.go create mode 100644 api/server/router/container/inspect.go create mode 100644 api/server/router/image/backend.go create mode 100644 api/server/router/image/image.go create mode 100644 api/server/router/image/image_routes.go create mode 100644 api/server/router/local.go create mode 100644 api/server/router/network/backend.go create mode 100644 api/server/router/network/filter.go create mode 100644 api/server/router/network/network.go create mode 100644 api/server/router/network/network_routes.go create mode 100644 api/server/router/router.go create mode 100644 api/server/router/system/backend.go create mode 100644 api/server/router/system/system.go create mode 100644 api/server/router/system/system_routes.go create mode 100644 api/server/router/volume/backend.go create mode 100644 api/server/router/volume/volume.go create mode 100644 api/server/router/volume/volume_routes.go create mode 100644 api/server/router_swapper.go create mode 100644 api/server/server.go create mode 100644 api/server/server_test.go create mode 100644 api/types/backend/backend.go create mode 100644 builder/builder.go create mode 100644 builder/context.go create mode 100644 builder/context_unix.go create mode 100644 builder/context_windows.go create mode 100644 builder/dockerfile/bflag.go create mode 100644 builder/dockerfile/bflag_test.go create mode 100644 builder/dockerfile/builder.go create mode 100644 builder/dockerfile/command/command.go create mode 100644 builder/dockerfile/dispatchers.go create mode 100644 builder/dockerfile/envVarTest create mode 100644 builder/dockerfile/evaluator.go create mode 100644 builder/dockerfile/internals.go create mode 100644 builder/dockerfile/parser/dumper/main.go create mode 100644 builder/dockerfile/parser/json_test.go create mode 100644 builder/dockerfile/parser/line_parsers.go create mode 100644 builder/dockerfile/parser/parser.go create mode 100644 builder/dockerfile/parser/parser_test.go create mode 100644 builder/dockerfile/parser/testfile-line/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles-negative/env_no_value/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles-negative/shykes-nested-json/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/ADD-COPY-with-JSON/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/ADD-COPY-with-JSON/result create mode 100644 builder/dockerfile/parser/testfiles/brimstone-consuldock/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/brimstone-consuldock/result create mode 100644 builder/dockerfile/parser/testfiles/brimstone-docker-consul/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/brimstone-docker-consul/result create mode 100644 builder/dockerfile/parser/testfiles/continueIndent/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/continueIndent/result create mode 100644 builder/dockerfile/parser/testfiles/cpuguy83-nagios/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/cpuguy83-nagios/result create mode 100644 builder/dockerfile/parser/testfiles/docker/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/docker/result create mode 100644 builder/dockerfile/parser/testfiles/env/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/env/result create mode 100644 builder/dockerfile/parser/testfiles/escapes/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/escapes/result create mode 100644 builder/dockerfile/parser/testfiles/flags/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/flags/result create mode 100644 builder/dockerfile/parser/testfiles/influxdb/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/influxdb/result create mode 100644 builder/dockerfile/parser/testfiles/jeztah-invalid-json-json-inside-string-double/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/jeztah-invalid-json-json-inside-string-double/result create mode 100644 builder/dockerfile/parser/testfiles/jeztah-invalid-json-json-inside-string/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/jeztah-invalid-json-json-inside-string/result create mode 100644 builder/dockerfile/parser/testfiles/jeztah-invalid-json-single-quotes/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/jeztah-invalid-json-single-quotes/result create mode 100644 builder/dockerfile/parser/testfiles/jeztah-invalid-json-unterminated-bracket/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/jeztah-invalid-json-unterminated-bracket/result create mode 100644 builder/dockerfile/parser/testfiles/jeztah-invalid-json-unterminated-string/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/jeztah-invalid-json-unterminated-string/result create mode 100644 builder/dockerfile/parser/testfiles/json/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/json/result create mode 100644 builder/dockerfile/parser/testfiles/kartar-entrypoint-oddities/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/kartar-entrypoint-oddities/result create mode 100644 builder/dockerfile/parser/testfiles/lk4d4-the-edge-case-generator/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/lk4d4-the-edge-case-generator/result create mode 100644 builder/dockerfile/parser/testfiles/mail/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/mail/result create mode 100644 builder/dockerfile/parser/testfiles/multiple-volumes/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/multiple-volumes/result create mode 100644 builder/dockerfile/parser/testfiles/mumble/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/mumble/result create mode 100644 builder/dockerfile/parser/testfiles/nginx/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/nginx/result create mode 100644 builder/dockerfile/parser/testfiles/tf2/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/tf2/result create mode 100644 builder/dockerfile/parser/testfiles/weechat/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/weechat/result create mode 100644 builder/dockerfile/parser/testfiles/znc/Dockerfile create mode 100644 builder/dockerfile/parser/testfiles/znc/result create mode 100644 builder/dockerfile/parser/utils.go create mode 100644 builder/dockerfile/shell_parser.go create mode 100644 builder/dockerfile/shell_parser_test.go create mode 100644 builder/dockerfile/support.go create mode 100644 builder/dockerfile/wordsTest create mode 100644 builder/dockerignore.go create mode 100644 builder/dockerignore/dockerignore.go create mode 100644 builder/dockerignore/dockerignore_test.go create mode 100644 builder/git.go create mode 100644 builder/remote.go create mode 100644 builder/remote_test.go create mode 100644 builder/tarsum.go create mode 100644 cli/cli.go create mode 100644 cli/client.go create mode 100644 cli/common.go create mode 100644 cliconfig/config.go create mode 100644 cliconfig/config_test.go create mode 100644 cliconfig/credentials/credentials.go create mode 100644 cliconfig/credentials/default_store.go create mode 100644 cliconfig/credentials/default_store_darwin.go create mode 100644 cliconfig/credentials/default_store_linux.go create mode 100644 cliconfig/credentials/default_store_unsupported.go create mode 100644 cliconfig/credentials/default_store_windows.go create mode 100644 cliconfig/credentials/file_store.go create mode 100644 cliconfig/credentials/file_store_test.go create mode 100644 cliconfig/credentials/native_store.go create mode 100644 cliconfig/credentials/native_store_test.go create mode 100644 cliconfig/credentials/shell_command.go create mode 100644 container/archive.go create mode 100644 container/container.go create mode 100644 container/container_unit_test.go create mode 100644 container/container_unix.go create mode 100644 container/container_windows.go create mode 100644 container/history.go create mode 100644 container/memory_store.go create mode 100644 container/memory_store_test.go create mode 100644 container/monitor.go create mode 100644 container/mounts_unix.go create mode 100644 container/mounts_windows.go create mode 100644 container/state.go create mode 100644 container/state_test.go create mode 100644 container/state_unix.go create mode 100644 container/state_windows.go create mode 100644 container/store.go create mode 100644 contrib/README.md create mode 100644 contrib/REVIEWERS create mode 100644 contrib/apparmor/main.go create mode 100644 contrib/apparmor/template.go create mode 100644 contrib/builder/deb/amd64/README.md create mode 100755 contrib/builder/deb/amd64/build.sh create mode 100644 contrib/builder/deb/amd64/debian-jessie/Dockerfile create mode 100644 contrib/builder/deb/amd64/debian-stretch/Dockerfile create mode 100644 contrib/builder/deb/amd64/debian-wheezy/Dockerfile create mode 100755 contrib/builder/deb/amd64/generate.sh create mode 100644 contrib/builder/deb/amd64/ubuntu-precise/Dockerfile create mode 100644 contrib/builder/deb/amd64/ubuntu-trusty/Dockerfile create mode 100644 contrib/builder/deb/amd64/ubuntu-wily/Dockerfile create mode 100644 contrib/builder/deb/amd64/ubuntu-xenial/Dockerfile create mode 100644 contrib/builder/deb/armhf/debian-jessie/Dockerfile create mode 100644 contrib/builder/rpm/amd64/README.md create mode 100755 contrib/builder/rpm/amd64/build.sh create mode 100644 contrib/builder/rpm/amd64/centos-7/Dockerfile create mode 100644 contrib/builder/rpm/amd64/fedora-22/Dockerfile create mode 100644 contrib/builder/rpm/amd64/fedora-23/Dockerfile create mode 100755 contrib/builder/rpm/amd64/generate.sh create mode 100644 contrib/builder/rpm/amd64/opensuse-13.2/Dockerfile create mode 100644 contrib/builder/rpm/amd64/oraclelinux-6/Dockerfile create mode 100644 contrib/builder/rpm/amd64/oraclelinux-7/Dockerfile create mode 100755 contrib/check-config.sh create mode 100644 contrib/completion/REVIEWERS create mode 100644 contrib/completion/bash/docker create mode 100644 contrib/completion/fish/docker.fish create mode 100644 contrib/completion/powershell/posh-docker.psm1 create mode 100644 contrib/completion/zsh/REVIEWERS create mode 100644 contrib/completion/zsh/_docker create mode 100644 contrib/desktop-integration/README.md create mode 100644 contrib/desktop-integration/chromium/Dockerfile create mode 100644 contrib/desktop-integration/gparted/Dockerfile create mode 100644 contrib/docker-device-tool/device_tool.go create mode 100644 contrib/docker-device-tool/device_tool_windows.go create mode 100644 contrib/docker-engine-selinux/LICENSE create mode 100644 contrib/docker-engine-selinux/Makefile create mode 100644 contrib/docker-engine-selinux/docker.fc create mode 100644 contrib/docker-engine-selinux/docker.if create mode 100644 contrib/docker-engine-selinux/docker.te create mode 100644 contrib/docker-engine-selinux/docker_selinux.8.gz create mode 100755 contrib/dockerize-disk.sh create mode 100755 contrib/download-frozen-image-v1.sh create mode 100755 contrib/download-frozen-image-v2.sh create mode 100644 contrib/httpserver/Dockerfile create mode 100644 contrib/httpserver/server.go create mode 100644 contrib/init/openrc/docker.confd create mode 100644 contrib/init/openrc/docker.initd create mode 100644 contrib/init/systemd/REVIEWERS create mode 100644 contrib/init/systemd/docker.service create mode 100644 contrib/init/systemd/docker.socket create mode 100755 contrib/init/sysvinit-debian/docker create mode 100644 contrib/init/sysvinit-debian/docker.default create mode 100755 contrib/init/sysvinit-redhat/docker create mode 100644 contrib/init/sysvinit-redhat/docker.sysconfig create mode 100644 contrib/init/upstart/REVIEWERS create mode 100644 contrib/init/upstart/docker.conf create mode 100755 contrib/mkimage-alpine.sh create mode 100644 contrib/mkimage-arch-pacman.conf create mode 100755 contrib/mkimage-arch.sh create mode 100644 contrib/mkimage-archarm-pacman.conf create mode 100755 contrib/mkimage-busybox.sh create mode 100755 contrib/mkimage-crux.sh create mode 100755 contrib/mkimage-debootstrap.sh create mode 100755 contrib/mkimage-rinse.sh create mode 100755 contrib/mkimage-yum.sh create mode 100755 contrib/mkimage.sh create mode 100755 contrib/mkimage/.febootstrap-minimize create mode 100755 contrib/mkimage/busybox-static create mode 100755 contrib/mkimage/debootstrap create mode 100755 contrib/mkimage/mageia-urpmi create mode 100755 contrib/mkimage/rinse create mode 100644 contrib/nnp-test/Dockerfile create mode 100644 contrib/nnp-test/nnp-test.c create mode 100755 contrib/nuke-graph-directory.sh create mode 100755 contrib/project-stats.sh create mode 100755 contrib/report-issue.sh create mode 100755 contrib/reprepro/suites.sh create mode 100644 contrib/syntax/kate/Dockerfile.xml create mode 100644 contrib/syntax/nano/Dockerfile.nanorc create mode 100644 contrib/syntax/nano/README.md create mode 100644 contrib/syntax/textmate/Docker.tmbundle/Preferences/Dockerfile.tmPreferences create mode 100644 contrib/syntax/textmate/Docker.tmbundle/Syntaxes/Dockerfile.tmLanguage create mode 100644 contrib/syntax/textmate/Docker.tmbundle/info.plist create mode 100644 contrib/syntax/textmate/README.md create mode 100644 contrib/syntax/textmate/REVIEWERS create mode 100644 contrib/syntax/vim/LICENSE create mode 100644 contrib/syntax/vim/README.md create mode 100644 contrib/syntax/vim/doc/dockerfile.txt create mode 100644 contrib/syntax/vim/ftdetect/dockerfile.vim create mode 100644 contrib/syntax/vim/syntax/dockerfile.vim create mode 100644 contrib/syscall-test/Dockerfile create mode 100644 contrib/syscall-test/acct.c create mode 100644 contrib/syscall-test/ns.c create mode 100644 contrib/syscall-test/userns.c create mode 100644 contrib/udev/80-docker.rules create mode 100644 contrib/vagrant-docker/README.md create mode 100644 daemon/apparmor_default.go create mode 100644 daemon/apparmor_default_unsupported.go create mode 100644 daemon/archive.go create mode 100644 daemon/archive_unix.go create mode 100644 daemon/archive_windows.go create mode 100644 daemon/attach.go create mode 100644 daemon/caps/utils_unix.go create mode 100644 daemon/changes.go create mode 100644 daemon/commit.go create mode 100644 daemon/config.go create mode 100644 daemon/config_experimental.go create mode 100644 daemon/config_stub.go create mode 100644 daemon/config_test.go create mode 100644 daemon/config_unix.go create mode 100644 daemon/config_windows.go create mode 100644 daemon/container_operations.go create mode 100644 daemon/container_operations_unix.go create mode 100644 daemon/container_operations_windows.go create mode 100644 daemon/create.go create mode 100644 daemon/create_unix.go create mode 100644 daemon/create_windows.go create mode 100644 daemon/daemon.go create mode 100644 daemon/daemon_experimental.go create mode 100644 daemon/daemon_linux.go create mode 100644 daemon/daemon_linux_test.go create mode 100644 daemon/daemon_stub.go create mode 100644 daemon/daemon_test.go create mode 100644 daemon/daemon_unix.go create mode 100644 daemon/daemon_unix_test.go create mode 100644 daemon/daemon_unsupported.go create mode 100644 daemon/daemon_windows.go create mode 100644 daemon/debugtrap_unix.go create mode 100644 daemon/debugtrap_unsupported.go create mode 100644 daemon/debugtrap_windows.go create mode 100644 daemon/delete.go create mode 100644 daemon/delete_test.go create mode 100644 daemon/discovery.go create mode 100644 daemon/discovery_test.go create mode 100644 daemon/errors.go create mode 100644 daemon/events.go create mode 100644 daemon/events/events.go create mode 100644 daemon/events/events_test.go create mode 100644 daemon/events/filter.go create mode 100644 daemon/events/testutils/testutils.go create mode 100644 daemon/events_test.go create mode 100644 daemon/exec.go create mode 100644 daemon/exec/exec.go create mode 100644 daemon/exec_linux.go create mode 100644 daemon/exec_windows.go create mode 100644 daemon/export.go create mode 100644 daemon/graphdriver/aufs/aufs.go create mode 100644 daemon/graphdriver/aufs/aufs_test.go create mode 100644 daemon/graphdriver/aufs/dirs.go create mode 100644 daemon/graphdriver/aufs/mount.go create mode 100644 daemon/graphdriver/aufs/mount_linux.go create mode 100644 daemon/graphdriver/aufs/mount_unsupported.go create mode 100644 daemon/graphdriver/btrfs/btrfs.go create mode 100644 daemon/graphdriver/btrfs/btrfs_test.go create mode 100644 daemon/graphdriver/btrfs/dummy_unsupported.go create mode 100644 daemon/graphdriver/btrfs/version.go create mode 100644 daemon/graphdriver/btrfs/version_none.go create mode 100644 daemon/graphdriver/btrfs/version_test.go create mode 100644 daemon/graphdriver/counter.go create mode 100644 daemon/graphdriver/devmapper/README.md create mode 100644 daemon/graphdriver/devmapper/deviceset.go create mode 100644 daemon/graphdriver/devmapper/devmapper_doc.go create mode 100644 daemon/graphdriver/devmapper/devmapper_test.go create mode 100644 daemon/graphdriver/devmapper/driver.go create mode 100644 daemon/graphdriver/devmapper/mount.go create mode 100644 daemon/graphdriver/driver.go create mode 100644 daemon/graphdriver/driver_freebsd.go create mode 100644 daemon/graphdriver/driver_linux.go create mode 100644 daemon/graphdriver/driver_unsupported.go create mode 100644 daemon/graphdriver/driver_windows.go create mode 100644 daemon/graphdriver/fsdiff.go create mode 100644 daemon/graphdriver/graphtest/graphtest_unix.go create mode 100644 daemon/graphdriver/graphtest/graphtest_windows.go create mode 100644 daemon/graphdriver/overlay/copy.go create mode 100644 daemon/graphdriver/overlay/overlay.go create mode 100644 daemon/graphdriver/overlay/overlay_test.go create mode 100644 daemon/graphdriver/overlay/overlay_unsupported.go create mode 100644 daemon/graphdriver/plugin.go create mode 100644 daemon/graphdriver/plugin_unsupported.go create mode 100644 daemon/graphdriver/proxy.go create mode 100644 daemon/graphdriver/register/register_aufs.go create mode 100644 daemon/graphdriver/register/register_btrfs.go create mode 100644 daemon/graphdriver/register/register_devicemapper.go create mode 100644 daemon/graphdriver/register/register_overlay.go create mode 100644 daemon/graphdriver/register/register_vfs.go create mode 100644 daemon/graphdriver/register/register_windows.go create mode 100644 daemon/graphdriver/register/register_zfs.go create mode 100644 daemon/graphdriver/vfs/driver.go create mode 100644 daemon/graphdriver/vfs/vfs_test.go create mode 100644 daemon/graphdriver/windows/windows.go create mode 100644 daemon/graphdriver/zfs/MAINTAINERS create mode 100644 daemon/graphdriver/zfs/zfs.go create mode 100644 daemon/graphdriver/zfs/zfs_freebsd.go create mode 100644 daemon/graphdriver/zfs/zfs_linux.go create mode 100644 daemon/graphdriver/zfs/zfs_test.go create mode 100644 daemon/graphdriver/zfs/zfs_unsupported.go create mode 100644 daemon/image_delete.go create mode 100644 daemon/images.go create mode 100644 daemon/import.go create mode 100644 daemon/info.go create mode 100644 daemon/inspect.go create mode 100644 daemon/inspect_unix.go create mode 100644 daemon/inspect_windows.go create mode 100644 daemon/kill.go create mode 100644 daemon/links.go create mode 100644 daemon/links/links.go create mode 100644 daemon/links/links_test.go create mode 100644 daemon/links_test.go create mode 100644 daemon/list.go create mode 100644 daemon/list_unix.go create mode 100644 daemon/list_windows.go create mode 100644 daemon/logdrivers_linux.go create mode 100644 daemon/logdrivers_windows.go create mode 100644 daemon/logger/awslogs/cloudwatchlogs.go create mode 100644 daemon/logger/awslogs/cloudwatchlogs_test.go create mode 100644 daemon/logger/awslogs/cwlogsiface_mock_test.go create mode 100644 daemon/logger/context.go create mode 100644 daemon/logger/copier.go create mode 100644 daemon/logger/copier_test.go create mode 100644 daemon/logger/etwlogs/etwlogs_windows.go create mode 100644 daemon/logger/factory.go create mode 100644 daemon/logger/fluentd/fluentd.go create mode 100644 daemon/logger/gcplogs/gcplogging.go create mode 100644 daemon/logger/gelf/gelf.go create mode 100644 daemon/logger/gelf/gelf_unsupported.go create mode 100644 daemon/logger/journald/journald.go create mode 100644 daemon/logger/journald/journald_unsupported.go create mode 100644 daemon/logger/journald/read.go create mode 100644 daemon/logger/journald/read_native.go create mode 100644 daemon/logger/journald/read_native_compat.go create mode 100644 daemon/logger/journald/read_unsupported.go create mode 100644 daemon/logger/jsonfilelog/jsonfilelog.go create mode 100644 daemon/logger/jsonfilelog/jsonfilelog_test.go create mode 100644 daemon/logger/jsonfilelog/read.go create mode 100644 daemon/logger/logger.go create mode 100644 daemon/logger/loggerutils/log_tag.go create mode 100644 daemon/logger/loggerutils/log_tag_test.go create mode 100644 daemon/logger/loggerutils/rotatefilewriter.go create mode 100644 daemon/logger/splunk/splunk.go create mode 100644 daemon/logger/syslog/syslog.go create mode 100644 daemon/logger/syslog/syslog_test.go create mode 100644 daemon/logger/syslog/syslog_unsupported.go create mode 100644 daemon/logs.go create mode 100644 daemon/monitor.go create mode 100644 daemon/monitor_linux.go create mode 100644 daemon/monitor_windows.go create mode 100644 daemon/mounts.go create mode 100644 daemon/network.go create mode 100644 daemon/network/settings.go create mode 100644 daemon/oci_linux.go create mode 100644 daemon/oci_windows.go create mode 100644 daemon/pause.go create mode 100644 daemon/rename.go create mode 100644 daemon/resize.go create mode 100644 daemon/restart.go create mode 100644 daemon/seccomp_disabled.go create mode 100644 daemon/seccomp_linux.go create mode 100644 daemon/selinux_linux.go create mode 100644 daemon/selinux_unsupported.go create mode 100644 daemon/start.go create mode 100644 daemon/stats.go create mode 100644 daemon/stats_collector_unix.go create mode 100644 daemon/stats_collector_windows.go create mode 100644 daemon/stop.go create mode 100644 daemon/top_unix.go create mode 100644 daemon/top_windows.go create mode 100644 daemon/unpause.go create mode 100644 daemon/update.go create mode 100644 daemon/update_linux.go create mode 100644 daemon/update_windows.go create mode 100644 daemon/volumes.go create mode 100644 daemon/volumes_unit_test.go create mode 100644 daemon/volumes_unix.go create mode 100644 daemon/volumes_windows.go create mode 100644 daemon/wait.go create mode 100644 distribution/errors.go create mode 100644 distribution/fixtures/validate_manifest/bad_manifest create mode 100644 distribution/fixtures/validate_manifest/extra_data_manifest create mode 100644 distribution/fixtures/validate_manifest/good_manifest create mode 100644 distribution/metadata/metadata.go create mode 100644 distribution/metadata/v1_id_service.go create mode 100644 distribution/metadata/v1_id_service_test.go create mode 100644 distribution/metadata/v2_metadata_service.go create mode 100644 distribution/metadata/v2_metadata_service_test.go create mode 100644 distribution/pull.go create mode 100644 distribution/pull_v1.go create mode 100644 distribution/pull_v2.go create mode 100644 distribution/pull_v2_test.go create mode 100644 distribution/pull_v2_unix.go create mode 100644 distribution/pull_v2_windows.go create mode 100644 distribution/push.go create mode 100644 distribution/push_v1.go create mode 100644 distribution/push_v2.go create mode 100644 distribution/registry.go create mode 100644 distribution/registry_unit_test.go create mode 100644 distribution/xfer/download.go create mode 100644 distribution/xfer/download_test.go create mode 100644 distribution/xfer/transfer.go create mode 100644 distribution/xfer/transfer_test.go create mode 100644 distribution/xfer/upload.go create mode 100644 distribution/xfer/upload_test.go create mode 100644 docker/README.md create mode 100644 docker/client.go create mode 100644 docker/client_test.go create mode 100644 docker/common.go create mode 100644 docker/daemon.go create mode 100644 docker/daemon_freebsd.go create mode 100644 docker/daemon_linux.go create mode 100644 docker/daemon_none.go create mode 100644 docker/daemon_test.go create mode 100644 docker/daemon_unix.go create mode 100644 docker/daemon_unix_test.go create mode 100644 docker/daemon_windows.go create mode 100644 docker/docker.go create mode 100644 docker/docker_windows.go create mode 100644 docker/flags.go create mode 100644 docker/flags_test.go create mode 100644 docker/listeners/listeners.go create mode 100644 docker/listeners/listeners_unix.go create mode 100644 docker/listeners/listeners_windows.go create mode 100644 dockerversion/useragent.go create mode 100644 dockerversion/version_lib.go create mode 100644 docs/.gitignore create mode 100644 docs/Dockerfile create mode 100644 docs/Makefile create mode 100644 docs/README.md create mode 100644 docs/admin/ambassador_pattern_linking.md create mode 100644 docs/admin/b2d_volume_images/add_cd.png create mode 100644 docs/admin/b2d_volume_images/add_new_controller.png create mode 100644 docs/admin/b2d_volume_images/add_volume.png create mode 100644 docs/admin/b2d_volume_images/boot_order.png create mode 100644 docs/admin/b2d_volume_images/gparted.png create mode 100644 docs/admin/b2d_volume_images/gparted2.png create mode 100644 docs/admin/b2d_volume_images/verify.png create mode 100644 docs/admin/b2d_volume_resize.md create mode 100644 docs/admin/cfengine_process_management.md create mode 100644 docs/admin/chef.md create mode 100644 docs/admin/configuring.md create mode 100644 docs/admin/dsc.md create mode 100644 docs/admin/formatting.md create mode 100644 docs/admin/host_integration.md create mode 100644 docs/admin/index.md create mode 100644 docs/admin/logging/awslogs.md create mode 100644 docs/admin/logging/etwlogs.md create mode 100644 docs/admin/logging/fluentd.md create mode 100644 docs/admin/logging/gcplogs.md create mode 100644 docs/admin/logging/index.md create mode 100644 docs/admin/logging/journald.md create mode 100644 docs/admin/logging/log_tags.md create mode 100644 docs/admin/logging/overview.md create mode 100644 docs/admin/logging/splunk.md create mode 100644 docs/admin/puppet.md create mode 100644 docs/admin/registry_mirror.md create mode 100644 docs/admin/runmetrics.md create mode 100644 docs/admin/systemd.md create mode 100644 docs/admin/using_supervisord.md create mode 100644 docs/article-img/architecture.svg create mode 100644 docs/breaking_changes.md create mode 100644 docs/deprecated.md create mode 100644 docs/examples/apt-cacher-ng.Dockerfile create mode 100644 docs/examples/apt-cacher-ng.md create mode 100644 docs/examples/couchbase.md create mode 100644 docs/examples/couchbase/web-console.png create mode 100644 docs/examples/couchdb_data_volumes.md create mode 100644 docs/examples/index.md create mode 100644 docs/examples/mongodb.md create mode 100644 docs/examples/mongodb/Dockerfile create mode 100644 docs/examples/nodejs_web_app.md create mode 100644 docs/examples/postgresql_service.Dockerfile create mode 100644 docs/examples/postgresql_service.md create mode 100644 docs/examples/running_redis_service.md create mode 100644 docs/examples/running_riak_service.Dockerfile create mode 100644 docs/examples/running_riak_service.md create mode 100644 docs/examples/running_ssh_service.Dockerfile create mode 100644 docs/examples/running_ssh_service.md create mode 100644 docs/examples/supervisord.conf create mode 100644 docs/extend/images/authz_additional_info.png create mode 100644 docs/extend/images/authz_allow.png create mode 100644 docs/extend/images/authz_chunked.png create mode 100644 docs/extend/images/authz_connection_hijack.png create mode 100644 docs/extend/images/authz_deny.png create mode 100644 docs/extend/index.md create mode 100644 docs/extend/plugin_api.md create mode 100644 docs/extend/plugins.md create mode 100644 docs/extend/plugins_authorization.md create mode 100644 docs/extend/plugins_network.md create mode 100644 docs/extend/plugins_volume.md create mode 100644 docs/faq.md create mode 100644 docs/index.md create mode 100644 docs/installation/binaries.md create mode 100644 docs/installation/cloud/cloud-ex-aws.md create mode 100644 docs/installation/cloud/cloud-ex-machine-ocean.md create mode 100644 docs/installation/cloud/index.md create mode 100644 docs/installation/cloud/overview.md create mode 100644 docs/installation/images/bad_host.png create mode 100644 docs/installation/images/cool_view.png create mode 100644 docs/installation/images/ec2-ubuntu.png create mode 100644 docs/installation/images/ec2_instance_details.png create mode 100644 docs/installation/images/ec2_instance_type.png create mode 100644 docs/installation/images/ec2_launch_instance.png create mode 100644 docs/installation/images/good_host.png create mode 100644 docs/installation/images/kitematic.png create mode 100644 docs/installation/images/linux_docker_host.svg create mode 100644 docs/installation/images/mac-page-finished.png create mode 100644 docs/installation/images/mac-page-two.png create mode 100644 docs/installation/images/mac-password-prompt.png create mode 100644 docs/installation/images/mac-success.png create mode 100644 docs/installation/images/mac-welcome-page.png create mode 100644 docs/installation/images/mac_docker_host.svg create mode 100644 docs/installation/images/my-docker-vm.png create mode 100644 docs/installation/images/newsite_view.png create mode 100644 docs/installation/images/nginx-webserver.png create mode 100644 docs/installation/images/ocean_click_api.png create mode 100644 docs/installation/images/ocean_droplet.png create mode 100644 docs/installation/images/ocean_droplet_ubuntu.png create mode 100644 docs/installation/images/ocean_gen_token.png create mode 100644 docs/installation/images/ocean_save_token.png create mode 100644 docs/installation/images/ocean_token_create.png create mode 100644 docs/installation/images/virtualization.png create mode 100644 docs/installation/images/win-page-6.png create mode 100644 docs/installation/images/win-welcome.png create mode 100644 docs/installation/images/win_docker_host.svg create mode 100644 docs/installation/images/win_ver.png create mode 100644 docs/installation/images/windows-boot2docker-cmd.png create mode 100644 docs/installation/images/windows-boot2docker-powershell.png create mode 100644 docs/installation/images/windows-boot2docker-start.png create mode 100644 docs/installation/images/windows-finish.png create mode 100644 docs/installation/index.md create mode 100644 docs/installation/linux/SUSE.md create mode 100644 docs/installation/linux/archlinux.md create mode 100644 docs/installation/linux/centos.md create mode 100644 docs/installation/linux/cruxlinux.md create mode 100644 docs/installation/linux/debian.md create mode 100644 docs/installation/linux/fedora.md create mode 100644 docs/installation/linux/frugalware.md create mode 100644 docs/installation/linux/gentoolinux.md create mode 100644 docs/installation/linux/index.md create mode 100644 docs/installation/linux/oracle.md create mode 100644 docs/installation/linux/rhel.md create mode 100644 docs/installation/linux/ubuntulinux.md create mode 100644 docs/installation/mac.md create mode 100644 docs/installation/windows.md create mode 100644 docs/migration.md create mode 100644 docs/quickstart.md create mode 100644 docs/reference/api/README.md create mode 100644 docs/reference/api/_static/io_oauth_authorization_page.png create mode 100644 docs/reference/api/docker-io_api.md create mode 100644 docs/reference/api/docker_io_accounts_api.md create mode 100644 docs/reference/api/docker_remote_api.md create mode 100644 docs/reference/api/docker_remote_api_v1.14.md create mode 100644 docs/reference/api/docker_remote_api_v1.15.md create mode 100644 docs/reference/api/docker_remote_api_v1.16.md create mode 100644 docs/reference/api/docker_remote_api_v1.17.md create mode 100644 docs/reference/api/docker_remote_api_v1.18.md create mode 100644 docs/reference/api/docker_remote_api_v1.19.md create mode 100644 docs/reference/api/docker_remote_api_v1.20.md create mode 100644 docs/reference/api/docker_remote_api_v1.21.md create mode 100644 docs/reference/api/docker_remote_api_v1.22.md create mode 100644 docs/reference/api/docker_remote_api_v1.23.md create mode 100644 docs/reference/api/hub_registry_spec.md create mode 100644 docs/reference/api/images/event_state.gliffy create mode 100644 docs/reference/api/images/event_state.png create mode 100644 docs/reference/api/index.md create mode 100644 docs/reference/api/remote_api_client_libraries.md create mode 100644 docs/reference/builder.md create mode 100644 docs/reference/commandline/attach.md create mode 100644 docs/reference/commandline/build.md create mode 100644 docs/reference/commandline/cli.md create mode 100644 docs/reference/commandline/commit.md create mode 100644 docs/reference/commandline/cp.md create mode 100644 docs/reference/commandline/create.md create mode 100644 docs/reference/commandline/daemon.md create mode 100644 docs/reference/commandline/diff.md create mode 100644 docs/reference/commandline/docker_images.gif create mode 100644 docs/reference/commandline/events.md create mode 100644 docs/reference/commandline/exec.md create mode 100644 docs/reference/commandline/export.md create mode 100644 docs/reference/commandline/history.md create mode 100644 docs/reference/commandline/images.md create mode 100644 docs/reference/commandline/import.md create mode 100644 docs/reference/commandline/index.md create mode 100644 docs/reference/commandline/info.md create mode 100644 docs/reference/commandline/inspect.md create mode 100644 docs/reference/commandline/kill.md create mode 100644 docs/reference/commandline/load.md create mode 100644 docs/reference/commandline/login.md create mode 100644 docs/reference/commandline/logout.md create mode 100644 docs/reference/commandline/logs.md create mode 100644 docs/reference/commandline/network_connect.md create mode 100644 docs/reference/commandline/network_create.md create mode 100644 docs/reference/commandline/network_disconnect.md create mode 100644 docs/reference/commandline/network_inspect.md create mode 100644 docs/reference/commandline/network_ls.md create mode 100644 docs/reference/commandline/network_rm.md create mode 100644 docs/reference/commandline/pause.md create mode 100644 docs/reference/commandline/port.md create mode 100644 docs/reference/commandline/ps.md create mode 100644 docs/reference/commandline/pull.md create mode 100644 docs/reference/commandline/push.md create mode 100644 docs/reference/commandline/rename.md create mode 100644 docs/reference/commandline/restart.md create mode 100644 docs/reference/commandline/rm.md create mode 100644 docs/reference/commandline/rmi.md create mode 100644 docs/reference/commandline/run.md create mode 100644 docs/reference/commandline/save.md create mode 100644 docs/reference/commandline/search.md create mode 100644 docs/reference/commandline/start.md create mode 100644 docs/reference/commandline/stats.md create mode 100644 docs/reference/commandline/stop.md create mode 100644 docs/reference/commandline/tag.md create mode 100644 docs/reference/commandline/top.md create mode 100644 docs/reference/commandline/unpause.md create mode 100644 docs/reference/commandline/update.md create mode 100644 docs/reference/commandline/version.md create mode 100644 docs/reference/commandline/volume_create.md create mode 100644 docs/reference/commandline/volume_inspect.md create mode 100644 docs/reference/commandline/volume_ls.md create mode 100644 docs/reference/commandline/volume_rm.md create mode 100644 docs/reference/commandline/wait.md create mode 100644 docs/reference/glossary.md create mode 100644 docs/reference/index.md create mode 100644 docs/reference/run.md create mode 100644 docs/security/apparmor.md create mode 100644 docs/security/certificates.md create mode 100644 docs/security/https.md create mode 100644 docs/security/https/Dockerfile create mode 100644 docs/security/https/Makefile create mode 100644 docs/security/https/README.md create mode 100755 docs/security/https/make_certs.sh create mode 100755 docs/security/https/parsedocs.sh create mode 100644 docs/security/index.md create mode 100644 docs/security/seccomp.md create mode 100644 docs/security/security.md create mode 100644 docs/security/trust/content_trust.md create mode 100644 docs/security/trust/deploying_notary.md create mode 100644 docs/security/trust/images/tag_signing.png create mode 100644 docs/security/trust/images/trust_.gliffy create mode 100644 docs/security/trust/images/trust_components.gliffy create mode 100644 docs/security/trust/images/trust_components.png create mode 100644 docs/security/trust/images/trust_signing.gliffy create mode 100644 docs/security/trust/images/trust_signing.png create mode 100644 docs/security/trust/images/trust_view.gliffy create mode 100644 docs/security/trust/images/trust_view.png create mode 100644 docs/security/trust/index.md create mode 100644 docs/security/trust/trust_automation.md create mode 100644 docs/security/trust/trust_delegation.md create mode 100644 docs/security/trust/trust_key_mng.md create mode 100644 docs/security/trust/trust_sandbox.md create mode 100644 docs/static_files/README.md create mode 100644 docs/static_files/contributors.png create mode 100644 docs/static_files/docker-logo-compressed.png create mode 100644 docs/static_files/docker_pull_chart.png create mode 100644 docs/static_files/docker_push_chart.png create mode 100644 docs/static_files/dockerlogo-v.png create mode 100755 docs/touch-up.sh create mode 100644 docs/understanding-docker.md create mode 100644 docs/userguide/containers/dockerimages.md create mode 100644 docs/userguide/containers/dockerizing.md create mode 100644 docs/userguide/containers/dockerrepos.md create mode 100644 docs/userguide/containers/dockervolumes.md create mode 100644 docs/userguide/containers/index.md create mode 100644 docs/userguide/containers/networkingcontainers.md create mode 100644 docs/userguide/containers/search.png create mode 100644 docs/userguide/containers/usingdocker.md create mode 100644 docs/userguide/containers/webapp1.png create mode 100644 docs/userguide/eng-image/baseimages.md create mode 100644 docs/userguide/eng-image/dockerfile_best-practices.md create mode 100644 docs/userguide/eng-image/image_management.md create mode 100644 docs/userguide/eng-image/index.md create mode 100644 docs/userguide/index.md create mode 100644 docs/userguide/intro.md create mode 100644 docs/userguide/labels-custom-metadata.md create mode 100644 docs/userguide/networking/configure-dns.md create mode 100644 docs/userguide/networking/default_network/binding.md create mode 100644 docs/userguide/networking/default_network/build-bridges.md create mode 100644 docs/userguide/networking/default_network/configure-dns.md create mode 100644 docs/userguide/networking/default_network/container-communication.md create mode 100644 docs/userguide/networking/default_network/custom-docker0.md create mode 100644 docs/userguide/networking/default_network/dockerlinks.md create mode 100644 docs/userguide/networking/default_network/images/ipv6_basic_host_config.gliffy create mode 100644 docs/userguide/networking/default_network/images/ipv6_basic_host_config.svg create mode 100644 docs/userguide/networking/default_network/images/ipv6_ndp_proxying.gliffy create mode 100644 docs/userguide/networking/default_network/images/ipv6_ndp_proxying.svg create mode 100644 docs/userguide/networking/default_network/images/ipv6_routed_network_example.gliffy create mode 100644 docs/userguide/networking/default_network/images/ipv6_routed_network_example.svg create mode 100644 docs/userguide/networking/default_network/images/ipv6_slash64_subnet_config.gliffy create mode 100644 docs/userguide/networking/default_network/images/ipv6_slash64_subnet_config.svg create mode 100644 docs/userguide/networking/default_network/images/ipv6_switched_network_example.gliffy create mode 100644 docs/userguide/networking/default_network/images/ipv6_switched_network_example.svg create mode 100644 docs/userguide/networking/default_network/index.md create mode 100644 docs/userguide/networking/default_network/ipv6.md create mode 100644 docs/userguide/networking/dockernetworks.md create mode 100644 docs/userguide/networking/get-started-overlay.md create mode 100644 docs/userguide/networking/images/bridge_network.gliffy create mode 100644 docs/userguide/networking/images/bridge_network.png create mode 100644 docs/userguide/networking/images/bridge_network.svg create mode 100644 docs/userguide/networking/images/engine_on_net.gliffy create mode 100644 docs/userguide/networking/images/engine_on_net.png create mode 100644 docs/userguide/networking/images/engine_on_net.svg create mode 100644 docs/userguide/networking/images/key_value.gliffy create mode 100644 docs/userguide/networking/images/key_value.png create mode 100644 docs/userguide/networking/images/key_value.svg create mode 100644 docs/userguide/networking/images/network_access.gliffy create mode 100644 docs/userguide/networking/images/network_access.png create mode 100644 docs/userguide/networking/images/network_access.svg create mode 100644 docs/userguide/networking/images/overlay-network-final.gliffy create mode 100644 docs/userguide/networking/images/overlay-network-final.png create mode 100644 docs/userguide/networking/images/overlay-network-final.svg create mode 100644 docs/userguide/networking/images/overlay_network.gliffy create mode 100644 docs/userguide/networking/images/overlay_network.png create mode 100644 docs/userguide/networking/images/overlay_network.svg create mode 100644 docs/userguide/networking/images/working.gliffy create mode 100644 docs/userguide/networking/images/working.png create mode 100644 docs/userguide/networking/images/working.svg create mode 100644 docs/userguide/networking/index.md create mode 100644 docs/userguide/networking/work-with-networks.md create mode 100644 docs/userguide/storagedriver/aufs-driver.md create mode 100644 docs/userguide/storagedriver/btrfs-driver.md create mode 100644 docs/userguide/storagedriver/device-mapper-driver.md create mode 100644 docs/userguide/storagedriver/images/aufs_delete.jpg create mode 100644 docs/userguide/storagedriver/images/aufs_layers.jpg create mode 100644 docs/userguide/storagedriver/images/aufs_metadata.jpg create mode 100644 docs/userguide/storagedriver/images/base_device.jpg create mode 100644 docs/userguide/storagedriver/images/btfs_constructs.jpg create mode 100644 docs/userguide/storagedriver/images/btfs_container_layer.jpg create mode 100644 docs/userguide/storagedriver/images/btfs_layers.png create mode 100644 docs/userguide/storagedriver/images/btfs_pool.jpg create mode 100644 docs/userguide/storagedriver/images/btfs_snapshots.jpg create mode 100644 docs/userguide/storagedriver/images/btfs_subvolume.jpg create mode 100644 docs/userguide/storagedriver/images/container-layers-cas.jpg create mode 100644 docs/userguide/storagedriver/images/container-layers.jpg create mode 100644 docs/userguide/storagedriver/images/dm_container.jpg create mode 100644 docs/userguide/storagedriver/images/driver-pros-cons.png create mode 100644 docs/userguide/storagedriver/images/image-layers.jpg create mode 100644 docs/userguide/storagedriver/images/overlay_constructs.jpg create mode 100644 docs/userguide/storagedriver/images/overlay_constructs2.jpg create mode 100644 docs/userguide/storagedriver/images/saving-space.jpg create mode 100644 docs/userguide/storagedriver/images/shared-uuid.jpg create mode 100644 docs/userguide/storagedriver/images/shared-volume.jpg create mode 100644 docs/userguide/storagedriver/images/sharing-layers.jpg create mode 100644 docs/userguide/storagedriver/images/two_dm_container.jpg create mode 100644 docs/userguide/storagedriver/images/zfs_clones.jpg create mode 100644 docs/userguide/storagedriver/images/zfs_zpool.jpg create mode 100644 docs/userguide/storagedriver/images/zpool_blocks.jpg create mode 100644 docs/userguide/storagedriver/imagesandcontainers.md create mode 100644 docs/userguide/storagedriver/index.md create mode 100644 docs/userguide/storagedriver/overlayfs-driver.md create mode 100644 docs/userguide/storagedriver/selectadriver.md create mode 100644 docs/userguide/storagedriver/zfs-driver.md create mode 100644 errors/errors.go create mode 100644 experimental/README.md create mode 100644 experimental/images/ipvlan-l3.gliffy create mode 100644 experimental/images/ipvlan-l3.png create mode 100644 experimental/images/ipvlan-l3.svg create mode 100644 experimental/images/ipvlan_l2_simple.gliffy create mode 100644 experimental/images/ipvlan_l2_simple.png create mode 100644 experimental/images/ipvlan_l2_simple.svg create mode 100644 experimental/images/macvlan-bridge-ipvlan-l2.gliffy create mode 100644 experimental/images/macvlan-bridge-ipvlan-l2.png create mode 100644 experimental/images/macvlan-bridge-ipvlan-l2.svg create mode 100644 experimental/images/macvlan_bridge_simple.gliffy create mode 100644 experimental/images/macvlan_bridge_simple.png create mode 100644 experimental/images/macvlan_bridge_simple.svg create mode 100644 experimental/images/multi_tenant_8021q_vlans.gliffy create mode 100644 experimental/images/multi_tenant_8021q_vlans.png create mode 100644 experimental/images/multi_tenant_8021q_vlans.svg create mode 100644 experimental/images/vlans-deeper-look.gliffy create mode 100644 experimental/images/vlans-deeper-look.png create mode 100644 experimental/images/vlans-deeper-look.svg create mode 100644 experimental/plugins_graphdriver.md create mode 100644 experimental/vlan-networks.md create mode 100755 hack/.vendor-helpers.sh create mode 100644 hack/Jenkins/W2L/postbuild.sh create mode 100644 hack/Jenkins/W2L/setup.sh create mode 100644 hack/Jenkins/readme.md create mode 100755 hack/dind create mode 100755 hack/generate-authors.sh create mode 100755 hack/install.sh create mode 100755 hack/make.sh create mode 100644 hack/make/.build-deb/compat create mode 100644 hack/make/.build-deb/control create mode 100644 hack/make/.build-deb/docker-engine.bash-completion create mode 120000 hack/make/.build-deb/docker-engine.docker.default create mode 120000 hack/make/.build-deb/docker-engine.docker.init create mode 120000 hack/make/.build-deb/docker-engine.docker.upstart create mode 100644 hack/make/.build-deb/docker-engine.install create mode 100644 hack/make/.build-deb/docker-engine.manpages create mode 100644 hack/make/.build-deb/docker-engine.postinst create mode 120000 hack/make/.build-deb/docker-engine.udev create mode 100644 hack/make/.build-deb/docs create mode 100755 hack/make/.build-deb/rules create mode 100644 hack/make/.build-rpm/docker-engine-selinux.spec create mode 100644 hack/make/.build-rpm/docker-engine.spec create mode 100644 hack/make/.detect-daemon-osarch create mode 100644 hack/make/.ensure-emptyfs create mode 100644 hack/make/.ensure-frozen-images create mode 100644 hack/make/.ensure-frozen-images-windows create mode 100644 hack/make/.ensure-httpserver create mode 100644 hack/make/.ensure-nnp-test create mode 100644 hack/make/.ensure-syscall-test create mode 100644 hack/make/.go-autogen create mode 100644 hack/make/.integration-daemon-setup create mode 100644 hack/make/.integration-daemon-start create mode 100644 hack/make/.integration-daemon-stop create mode 100644 hack/make/.resources-windows/docker.exe.manifest create mode 100644 hack/make/.resources-windows/docker.ico create mode 100644 hack/make/.resources-windows/docker.png create mode 100644 hack/make/.validate create mode 100644 hack/make/README.md create mode 100644 hack/make/binary create mode 100644 hack/make/build-deb create mode 100644 hack/make/build-rpm create mode 100755 hack/make/clean-apt-repo create mode 100755 hack/make/clean-yum-repo create mode 100644 hack/make/cover create mode 100644 hack/make/cross create mode 100644 hack/make/dynbinary create mode 100644 hack/make/dyngccgo create mode 100644 hack/make/gccgo create mode 100755 hack/make/generate-index-listing create mode 100644 hack/make/install-script create mode 100755 hack/make/release-deb create mode 100755 hack/make/release-rpm create mode 100755 hack/make/sign-repos create mode 100755 hack/make/test-deb-install create mode 100644 hack/make/test-docker-py create mode 100755 hack/make/test-install-script create mode 100644 hack/make/test-integration-cli create mode 100755 hack/make/test-old-apt-repo create mode 100644 hack/make/test-unit create mode 100644 hack/make/tgz create mode 100644 hack/make/ubuntu create mode 100755 hack/make/update-apt-repo create mode 100644 hack/make/validate-dco create mode 100644 hack/make/validate-default-seccomp create mode 100644 hack/make/validate-gofmt create mode 100644 hack/make/validate-lint create mode 100644 hack/make/validate-pkg create mode 100644 hack/make/validate-test create mode 100644 hack/make/validate-toml create mode 100644 hack/make/validate-vendor create mode 100644 hack/make/validate-vet create mode 100644 hack/make/win create mode 100755 hack/release.sh create mode 100755 hack/vendor.sh create mode 100644 image/fs.go create mode 100644 image/fs_test.go create mode 100644 image/image.go create mode 100644 image/image_test.go create mode 100644 image/rootfs.go create mode 100644 image/rootfs_unix.go create mode 100644 image/rootfs_windows.go create mode 100644 image/spec/v1.md create mode 100644 image/store.go create mode 100644 image/store_test.go create mode 100644 image/tarexport/load.go create mode 100644 image/tarexport/save.go create mode 100644 image/tarexport/tarexport.go create mode 100644 image/v1/imagev1.go create mode 100644 integration-cli/benchmark_test.go create mode 100644 integration-cli/check_test.go create mode 100644 integration-cli/daemon.go create mode 100644 integration-cli/docker_api_attach_test.go create mode 100644 integration-cli/docker_api_auth_test.go create mode 100644 integration-cli/docker_api_build_test.go create mode 100644 integration-cli/docker_api_containers_test.go create mode 100644 integration-cli/docker_api_create_test.go create mode 100644 integration-cli/docker_api_events_test.go create mode 100644 integration-cli/docker_api_exec_resize_test.go create mode 100644 integration-cli/docker_api_exec_test.go create mode 100644 integration-cli/docker_api_images_test.go create mode 100644 integration-cli/docker_api_info_test.go create mode 100644 integration-cli/docker_api_inspect_test.go create mode 100644 integration-cli/docker_api_inspect_unix_test.go create mode 100644 integration-cli/docker_api_logs_test.go create mode 100644 integration-cli/docker_api_network_test.go create mode 100644 integration-cli/docker_api_resize_test.go create mode 100644 integration-cli/docker_api_stats_test.go create mode 100644 integration-cli/docker_api_test.go create mode 100644 integration-cli/docker_api_update_unix_test.go create mode 100644 integration-cli/docker_api_version_test.go create mode 100644 integration-cli/docker_api_volumes_test.go create mode 100644 integration-cli/docker_cli_attach_test.go create mode 100644 integration-cli/docker_cli_attach_unix_test.go create mode 100644 integration-cli/docker_cli_authz_unix_test.go create mode 100644 integration-cli/docker_cli_build_test.go create mode 100644 integration-cli/docker_cli_build_unix_test.go create mode 100644 integration-cli/docker_cli_by_digest_test.go create mode 100644 integration-cli/docker_cli_commit_test.go create mode 100644 integration-cli/docker_cli_config_test.go create mode 100644 integration-cli/docker_cli_cp_from_container_test.go create mode 100644 integration-cli/docker_cli_cp_test.go create mode 100644 integration-cli/docker_cli_cp_to_container_test.go create mode 100644 integration-cli/docker_cli_cp_to_container_unix_test.go create mode 100644 integration-cli/docker_cli_cp_utils.go create mode 100644 integration-cli/docker_cli_create_test.go create mode 100644 integration-cli/docker_cli_daemon_experimental_test.go create mode 100644 integration-cli/docker_cli_daemon_not_experimental_test.go create mode 100644 integration-cli/docker_cli_daemon_test.go create mode 100644 integration-cli/docker_cli_diff_test.go create mode 100644 integration-cli/docker_cli_events_test.go create mode 100644 integration-cli/docker_cli_events_unix_test.go create mode 100644 integration-cli/docker_cli_exec_test.go create mode 100644 integration-cli/docker_cli_exec_unix_test.go create mode 100644 integration-cli/docker_cli_experimental_test.go create mode 100644 integration-cli/docker_cli_export_import_test.go create mode 100644 integration-cli/docker_cli_external_graphdriver_unix_test.go create mode 100644 integration-cli/docker_cli_help_test.go create mode 100644 integration-cli/docker_cli_history_test.go create mode 100644 integration-cli/docker_cli_images_test.go create mode 100644 integration-cli/docker_cli_import_test.go create mode 100644 integration-cli/docker_cli_info_test.go create mode 100644 integration-cli/docker_cli_inspect_experimental_test.go create mode 100644 integration-cli/docker_cli_inspect_test.go create mode 100644 integration-cli/docker_cli_kill_test.go create mode 100644 integration-cli/docker_cli_links_test.go create mode 100644 integration-cli/docker_cli_links_unix_test.go create mode 100644 integration-cli/docker_cli_login_test.go create mode 100644 integration-cli/docker_cli_logout_test.go create mode 100644 integration-cli/docker_cli_logs_bench_test.go create mode 100644 integration-cli/docker_cli_logs_test.go create mode 100644 integration-cli/docker_cli_nat_test.go create mode 100644 integration-cli/docker_cli_netmode_test.go create mode 100644 integration-cli/docker_cli_network_unix_test.go create mode 100644 integration-cli/docker_cli_oom_killed_test.go create mode 100644 integration-cli/docker_cli_pause_test.go create mode 100644 integration-cli/docker_cli_port_test.go create mode 100644 integration-cli/docker_cli_proxy_test.go create mode 100644 integration-cli/docker_cli_ps_test.go create mode 100644 integration-cli/docker_cli_pull_local_test.go create mode 100644 integration-cli/docker_cli_pull_test.go create mode 100644 integration-cli/docker_cli_pull_trusted_test.go create mode 100644 integration-cli/docker_cli_push_test.go create mode 100644 integration-cli/docker_cli_registry_user_agent_test.go create mode 100644 integration-cli/docker_cli_rename_test.go create mode 100644 integration-cli/docker_cli_restart_test.go create mode 100644 integration-cli/docker_cli_rm_test.go create mode 100644 integration-cli/docker_cli_rmi_test.go create mode 100644 integration-cli/docker_cli_run_test.go create mode 100644 integration-cli/docker_cli_run_unix_test.go create mode 100644 integration-cli/docker_cli_save_load_test.go create mode 100644 integration-cli/docker_cli_save_load_unix_test.go create mode 100644 integration-cli/docker_cli_search_test.go create mode 100644 integration-cli/docker_cli_sni_test.go create mode 100644 integration-cli/docker_cli_start_test.go create mode 100644 integration-cli/docker_cli_start_volume_driver_unix_test.go create mode 100644 integration-cli/docker_cli_stats_test.go create mode 100644 integration-cli/docker_cli_tag_test.go create mode 100644 integration-cli/docker_cli_top_test.go create mode 100644 integration-cli/docker_cli_update_test.go create mode 100644 integration-cli/docker_cli_update_unix_test.go create mode 100644 integration-cli/docker_cli_userns_test.go create mode 100644 integration-cli/docker_cli_v2_only_test.go create mode 100644 integration-cli/docker_cli_version_test.go create mode 100644 integration-cli/docker_cli_volume_test.go create mode 100644 integration-cli/docker_cli_wait_test.go create mode 100644 integration-cli/docker_experimental_network_test.go create mode 100644 integration-cli/docker_hub_pull_suite_test.go create mode 100644 integration-cli/docker_test_vars.go create mode 100644 integration-cli/docker_utils.go create mode 100644 integration-cli/events_utils.go create mode 100755 integration-cli/fixtures/auth/docker-credential-shell-test create mode 100644 integration-cli/fixtures/https/ca.pem create mode 100644 integration-cli/fixtures/https/client-cert.pem create mode 100644 integration-cli/fixtures/https/client-key.pem create mode 100644 integration-cli/fixtures/https/client-rogue-cert.pem create mode 100644 integration-cli/fixtures/https/client-rogue-key.pem create mode 100644 integration-cli/fixtures/https/server-cert.pem create mode 100644 integration-cli/fixtures/https/server-key.pem create mode 100644 integration-cli/fixtures/https/server-rogue-cert.pem create mode 100644 integration-cli/fixtures/https/server-rogue-key.pem create mode 100644 integration-cli/fixtures/load/emptyLayer.tar create mode 100644 integration-cli/fixtures/notary/delgkey1.crt create mode 100644 integration-cli/fixtures/notary/delgkey1.key create mode 100644 integration-cli/fixtures/notary/delgkey2.crt create mode 100644 integration-cli/fixtures/notary/delgkey2.key create mode 100644 integration-cli/fixtures/notary/delgkey3.crt create mode 100644 integration-cli/fixtures/notary/delgkey3.key create mode 100644 integration-cli/fixtures/notary/delgkey4.crt create mode 100644 integration-cli/fixtures/notary/delgkey4.key create mode 100644 integration-cli/fixtures/notary/localhost.cert create mode 100644 integration-cli/fixtures/notary/localhost.key create mode 100644 integration-cli/fixtures/registry/cert.pem create mode 100644 integration-cli/npipe.go create mode 100644 integration-cli/npipe_windows.go create mode 100644 integration-cli/registry.go create mode 100644 integration-cli/registry_mock.go create mode 100644 integration-cli/requirements.go create mode 100644 integration-cli/requirements_unix.go create mode 100644 integration-cli/test_vars_exec.go create mode 100644 integration-cli/test_vars_noexec.go create mode 100644 integration-cli/test_vars_noseccomp.go create mode 100644 integration-cli/test_vars_seccomp.go create mode 100644 integration-cli/test_vars_unix.go create mode 100644 integration-cli/test_vars_windows.go create mode 100644 integration-cli/trust_server.go create mode 100644 integration-cli/utils.go create mode 100644 layer/empty.go create mode 100644 layer/empty_test.go create mode 100644 layer/filestore.go create mode 100644 layer/filestore_test.go create mode 100644 layer/layer.go create mode 100644 layer/layer_store.go create mode 100644 layer/layer_test.go create mode 100644 layer/layer_unix.go create mode 100644 layer/layer_unix_test.go create mode 100644 layer/layer_windows.go create mode 100644 layer/migration.go create mode 100644 layer/migration_test.go create mode 100644 layer/mount_test.go create mode 100644 layer/mounted_layer.go create mode 100644 layer/ro_layer.go create mode 100644 libcontainerd/client.go create mode 100644 libcontainerd/client_linux.go create mode 100644 libcontainerd/client_liverestore_linux.go create mode 100644 libcontainerd/client_shutdownrestore_linux.go create mode 100644 libcontainerd/client_windows.go create mode 100644 libcontainerd/container.go create mode 100644 libcontainerd/container_linux.go create mode 100644 libcontainerd/container_windows.go create mode 100644 libcontainerd/pausemonitor_linux.go create mode 100644 libcontainerd/process.go create mode 100644 libcontainerd/process_linux.go create mode 100644 libcontainerd/process_windows.go create mode 100644 libcontainerd/queue_linux.go create mode 100644 libcontainerd/remote.go create mode 100644 libcontainerd/remote_linux.go create mode 100644 libcontainerd/remote_windows.go create mode 100644 libcontainerd/types.go create mode 100644 libcontainerd/types_linux.go create mode 100644 libcontainerd/types_windows.go create mode 100644 libcontainerd/utils_linux.go create mode 100644 libcontainerd/utils_windows.go create mode 100644 libcontainerd/windowsoci/oci_windows.go create mode 100644 libcontainerd/windowsoci/unsupported.go create mode 100644 man/Dockerfile create mode 100644 man/Dockerfile.5.md create mode 100644 man/README.md create mode 100644 man/config-json.5.md create mode 100644 man/docker-attach.1.md create mode 100644 man/docker-build.1.md create mode 100644 man/docker-commit.1.md create mode 100644 man/docker-cp.1.md create mode 100644 man/docker-create.1.md create mode 100644 man/docker-daemon.8.md create mode 100644 man/docker-diff.1.md create mode 100644 man/docker-events.1.md create mode 100644 man/docker-exec.1.md create mode 100644 man/docker-export.1.md create mode 100644 man/docker-history.1.md create mode 100644 man/docker-images.1.md create mode 100644 man/docker-import.1.md create mode 100644 man/docker-info.1.md create mode 100644 man/docker-inspect.1.md create mode 100644 man/docker-kill.1.md create mode 100644 man/docker-load.1.md create mode 100644 man/docker-login.1.md create mode 100644 man/docker-logout.1.md create mode 100644 man/docker-logs.1.md create mode 100644 man/docker-network-connect.1.md create mode 100644 man/docker-network-create.1.md create mode 100644 man/docker-network-disconnect.1.md create mode 100644 man/docker-network-inspect.1.md create mode 100644 man/docker-network-ls.1.md create mode 100644 man/docker-network-rm.1.md create mode 100644 man/docker-pause.1.md create mode 100644 man/docker-port.1.md create mode 100644 man/docker-ps.1.md create mode 100644 man/docker-pull.1.md create mode 100644 man/docker-push.1.md create mode 100644 man/docker-rename.1.md create mode 100644 man/docker-restart.1.md create mode 100644 man/docker-rm.1.md create mode 100644 man/docker-rmi.1.md create mode 100644 man/docker-run.1.md create mode 100644 man/docker-save.1.md create mode 100644 man/docker-search.1.md create mode 100644 man/docker-start.1.md create mode 100644 man/docker-stats.1.md create mode 100644 man/docker-stop.1.md create mode 100644 man/docker-tag.1.md create mode 100644 man/docker-top.1.md create mode 100644 man/docker-unpause.1.md create mode 100644 man/docker-update.1.md create mode 100644 man/docker-version.1.md create mode 100644 man/docker-volume-create.1.md create mode 100644 man/docker-volume-inspect.1.md create mode 100644 man/docker-volume-ls.1.md create mode 100644 man/docker-volume-rm.1.md create mode 100644 man/docker-volume.1.md create mode 100644 man/docker-wait.1.md create mode 100644 man/docker.1.md create mode 100755 man/md2man-all.sh create mode 100644 migrate/v1/migratev1.go create mode 100644 migrate/v1/migratev1_test.go create mode 100644 oci/defaults_linux.go create mode 100644 oci/defaults_windows.go create mode 100644 opts/hosts.go create mode 100644 opts/hosts_test.go create mode 100644 opts/hosts_unix.go create mode 100644 opts/hosts_windows.go create mode 100644 opts/ip.go create mode 100644 opts/ip_test.go create mode 100644 opts/opts.go create mode 100644 opts/opts_test.go create mode 100644 opts/opts_unix.go create mode 100644 opts/opts_windows.go create mode 100644 pkg/README.md create mode 100644 pkg/aaparser/aaparser.go create mode 100644 pkg/aaparser/aaparser_test.go create mode 100644 pkg/archive/README.md create mode 100644 pkg/archive/archive.go create mode 100644 pkg/archive/archive_test.go create mode 100644 pkg/archive/archive_unix.go create mode 100644 pkg/archive/archive_unix_test.go create mode 100644 pkg/archive/archive_windows.go create mode 100644 pkg/archive/archive_windows_test.go create mode 100644 pkg/archive/changes.go create mode 100644 pkg/archive/changes_linux.go create mode 100644 pkg/archive/changes_other.go create mode 100644 pkg/archive/changes_posix_test.go create mode 100644 pkg/archive/changes_test.go create mode 100644 pkg/archive/changes_unix.go create mode 100644 pkg/archive/changes_windows.go create mode 100644 pkg/archive/copy.go create mode 100644 pkg/archive/copy_unix.go create mode 100644 pkg/archive/copy_unix_test.go create mode 100644 pkg/archive/copy_windows.go create mode 100644 pkg/archive/diff.go create mode 100644 pkg/archive/diff_test.go create mode 100644 pkg/archive/example_changes.go create mode 100644 pkg/archive/testdata/broken.tar create mode 100644 pkg/archive/time_linux.go create mode 100644 pkg/archive/time_unsupported.go create mode 100644 pkg/archive/utils_test.go create mode 100644 pkg/archive/whiteouts.go create mode 100644 pkg/archive/wrap.go create mode 100644 pkg/archive/wrap_test.go create mode 100644 pkg/authorization/api.go create mode 100644 pkg/authorization/authz.go create mode 100644 pkg/authorization/authz_unix_test.go create mode 100644 pkg/authorization/plugin.go create mode 100644 pkg/authorization/response.go create mode 100644 pkg/broadcaster/unbuffered.go create mode 100644 pkg/broadcaster/unbuffered_test.go create mode 100644 pkg/chrootarchive/archive.go create mode 100644 pkg/chrootarchive/archive_test.go create mode 100644 pkg/chrootarchive/archive_unix.go create mode 100644 pkg/chrootarchive/archive_windows.go create mode 100644 pkg/chrootarchive/diff.go create mode 100644 pkg/chrootarchive/diff_unix.go create mode 100644 pkg/chrootarchive/diff_windows.go create mode 100644 pkg/chrootarchive/init_unix.go create mode 100644 pkg/chrootarchive/init_windows.go create mode 100644 pkg/devicemapper/devmapper.go create mode 100644 pkg/devicemapper/devmapper_log.go create mode 100644 pkg/devicemapper/devmapper_wrapper.go create mode 100644 pkg/devicemapper/devmapper_wrapper_deferred_remove.go create mode 100644 pkg/devicemapper/devmapper_wrapper_no_deferred_remove.go create mode 100644 pkg/devicemapper/ioctl.go create mode 100644 pkg/devicemapper/log.go create mode 100644 pkg/directory/directory.go create mode 100644 pkg/directory/directory_test.go create mode 100644 pkg/directory/directory_unix.go create mode 100644 pkg/directory/directory_windows.go create mode 100644 pkg/discovery/README.md create mode 100644 pkg/discovery/backends.go create mode 100644 pkg/discovery/discovery.go create mode 100644 pkg/discovery/discovery_test.go create mode 100644 pkg/discovery/entry.go create mode 100644 pkg/discovery/file/file.go create mode 100644 pkg/discovery/file/file_test.go create mode 100644 pkg/discovery/generator.go create mode 100644 pkg/discovery/generator_test.go create mode 100644 pkg/discovery/kv/kv.go create mode 100644 pkg/discovery/kv/kv_test.go create mode 100644 pkg/discovery/memory/memory.go create mode 100644 pkg/discovery/memory/memory_test.go create mode 100644 pkg/discovery/nodes/nodes.go create mode 100644 pkg/discovery/nodes/nodes_test.go create mode 100644 pkg/filenotify/filenotify.go create mode 100644 pkg/filenotify/fsnotify.go create mode 100644 pkg/filenotify/poller.go create mode 100644 pkg/filenotify/poller_test.go create mode 100644 pkg/fileutils/fileutils.go create mode 100644 pkg/fileutils/fileutils_test.go create mode 100644 pkg/fileutils/fileutils_unix.go create mode 100644 pkg/fileutils/fileutils_windows.go create mode 100644 pkg/gitutils/gitutils.go create mode 100644 pkg/gitutils/gitutils_test.go create mode 100644 pkg/graphdb/conn_sqlite3.go create mode 100644 pkg/graphdb/conn_sqlite3_unix.go create mode 100644 pkg/graphdb/conn_sqlite3_windows.go create mode 100644 pkg/graphdb/conn_unsupported.go create mode 100644 pkg/graphdb/graphdb.go create mode 100644 pkg/graphdb/graphdb_test.go create mode 100644 pkg/graphdb/sort.go create mode 100644 pkg/graphdb/sort_test.go create mode 100644 pkg/graphdb/utils.go create mode 100644 pkg/homedir/homedir.go create mode 100644 pkg/homedir/homedir_test.go create mode 100644 pkg/httputils/httputils.go create mode 100644 pkg/httputils/httputils_test.go create mode 100644 pkg/httputils/mimetype.go create mode 100644 pkg/httputils/mimetype_test.go create mode 100644 pkg/httputils/resumablerequestreader.go create mode 100644 pkg/httputils/resumablerequestreader_test.go create mode 100644 pkg/idtools/idtools.go create mode 100644 pkg/idtools/idtools_unix.go create mode 100644 pkg/idtools/idtools_unix_test.go create mode 100644 pkg/idtools/idtools_windows.go create mode 100644 pkg/idtools/usergroupadd_linux.go create mode 100644 pkg/idtools/usergroupadd_unsupported.go create mode 100644 pkg/integration/checker/checker.go create mode 100644 pkg/integration/dockerCmd_utils.go create mode 100644 pkg/integration/dockerCmd_utils_test.go create mode 100644 pkg/integration/utils.go create mode 100644 pkg/integration/utils_test.go create mode 100644 pkg/ioutils/bytespipe.go create mode 100644 pkg/ioutils/bytespipe_test.go create mode 100644 pkg/ioutils/fmt.go create mode 100644 pkg/ioutils/fmt_test.go create mode 100644 pkg/ioutils/multireader.go create mode 100644 pkg/ioutils/multireader_test.go create mode 100644 pkg/ioutils/readers.go create mode 100644 pkg/ioutils/readers_test.go create mode 100644 pkg/ioutils/scheduler.go create mode 100644 pkg/ioutils/scheduler_gccgo.go create mode 100644 pkg/ioutils/temp_unix.go create mode 100644 pkg/ioutils/temp_windows.go create mode 100644 pkg/ioutils/writeflusher.go create mode 100644 pkg/ioutils/writers.go create mode 100644 pkg/ioutils/writers_test.go create mode 100644 pkg/jsonlog/jsonlog.go create mode 100644 pkg/jsonlog/jsonlog_marshalling.go create mode 100644 pkg/jsonlog/jsonlog_marshalling_test.go create mode 100644 pkg/jsonlog/jsonlogbytes.go create mode 100644 pkg/jsonlog/jsonlogbytes_test.go create mode 100644 pkg/jsonlog/time_marshalling.go create mode 100644 pkg/jsonlog/time_marshalling_test.go create mode 100644 pkg/jsonmessage/jsonmessage.go create mode 100644 pkg/jsonmessage/jsonmessage_test.go create mode 100644 pkg/locker/README.md create mode 100644 pkg/locker/locker.go create mode 100644 pkg/locker/locker_test.go create mode 100644 pkg/longpath/longpath.go create mode 100644 pkg/longpath/longpath_test.go create mode 100644 pkg/loopback/attach_loopback.go create mode 100644 pkg/loopback/ioctl.go create mode 100644 pkg/loopback/loop_wrapper.go create mode 100644 pkg/loopback/loopback.go create mode 100644 pkg/mflag/LICENSE create mode 100644 pkg/mflag/README.md create mode 100644 pkg/mflag/example/example.go create mode 100644 pkg/mflag/flag.go create mode 100644 pkg/mflag/flag_test.go create mode 100644 pkg/mount/flags.go create mode 100644 pkg/mount/flags_freebsd.go create mode 100644 pkg/mount/flags_linux.go create mode 100644 pkg/mount/flags_unsupported.go create mode 100644 pkg/mount/mount.go create mode 100644 pkg/mount/mount_unix_test.go create mode 100644 pkg/mount/mounter_freebsd.go create mode 100644 pkg/mount/mounter_linux.go create mode 100644 pkg/mount/mounter_unsupported.go create mode 100644 pkg/mount/mountinfo.go create mode 100644 pkg/mount/mountinfo_freebsd.go create mode 100644 pkg/mount/mountinfo_linux.go create mode 100644 pkg/mount/mountinfo_linux_test.go create mode 100644 pkg/mount/mountinfo_unsupported.go create mode 100644 pkg/mount/mountinfo_windows.go create mode 100644 pkg/mount/sharedsubtree_linux.go create mode 100644 pkg/mount/sharedsubtree_linux_test.go create mode 100644 pkg/namesgenerator/cmd/names-generator/main.go create mode 100644 pkg/namesgenerator/names-generator.go create mode 100644 pkg/namesgenerator/names-generator_test.go create mode 100644 pkg/parsers/kernel/kernel.go create mode 100644 pkg/parsers/kernel/kernel_unix_test.go create mode 100644 pkg/parsers/kernel/kernel_windows.go create mode 100644 pkg/parsers/kernel/uname_linux.go create mode 100644 pkg/parsers/kernel/uname_unsupported.go create mode 100644 pkg/parsers/operatingsystem/operatingsystem_freebsd.go create mode 100644 pkg/parsers/operatingsystem/operatingsystem_linux.go create mode 100644 pkg/parsers/operatingsystem/operatingsystem_unix_test.go create mode 100644 pkg/parsers/operatingsystem/operatingsystem_windows.go create mode 100644 pkg/parsers/parsers.go create mode 100644 pkg/parsers/parsers_test.go create mode 100644 pkg/pidfile/pidfile.go create mode 100644 pkg/pidfile/pidfile_test.go create mode 100644 pkg/platform/architecture_freebsd.go create mode 100644 pkg/platform/architecture_linux.go create mode 100644 pkg/platform/architecture_windows.go create mode 100644 pkg/platform/platform.go create mode 100644 pkg/platform/utsname_int8.go create mode 100644 pkg/platform/utsname_uint8.go create mode 100644 pkg/plugins/client.go create mode 100644 pkg/plugins/client_test.go create mode 100644 pkg/plugins/discovery.go create mode 100644 pkg/plugins/discovery_test.go create mode 100644 pkg/plugins/discovery_unix_test.go create mode 100644 pkg/plugins/errors.go create mode 100644 pkg/plugins/pluginrpc-gen/README.md create mode 100644 pkg/plugins/pluginrpc-gen/fixtures/foo.go create mode 100644 pkg/plugins/pluginrpc-gen/main.go create mode 100644 pkg/plugins/pluginrpc-gen/parser.go create mode 100644 pkg/plugins/pluginrpc-gen/parser_test.go create mode 100644 pkg/plugins/pluginrpc-gen/template.go create mode 100644 pkg/plugins/plugins.go create mode 100644 pkg/plugins/transport/http.go create mode 100644 pkg/plugins/transport/transport.go create mode 100644 pkg/pools/pools.go create mode 100644 pkg/pools/pools_test.go create mode 100644 pkg/progress/progress.go create mode 100644 pkg/progress/progressreader.go create mode 100644 pkg/progress/progressreader_test.go create mode 100644 pkg/promise/promise.go create mode 100644 pkg/proxy/network_proxy_test.go create mode 100644 pkg/proxy/proxy.go create mode 100644 pkg/proxy/stub_proxy.go create mode 100644 pkg/proxy/tcp_proxy.go create mode 100644 pkg/proxy/udp_proxy.go create mode 100644 pkg/pubsub/publisher.go create mode 100644 pkg/pubsub/publisher_test.go create mode 100644 pkg/random/random.go create mode 100644 pkg/random/random_test.go create mode 100644 pkg/reexec/README.md create mode 100644 pkg/reexec/command_freebsd.go create mode 100644 pkg/reexec/command_linux.go create mode 100644 pkg/reexec/command_unsupported.go create mode 100644 pkg/reexec/command_windows.go create mode 100644 pkg/reexec/reexec.go create mode 100644 pkg/registrar/registrar.go create mode 100644 pkg/registrar/registrar_test.go create mode 100644 pkg/signal/README.md create mode 100644 pkg/signal/signal.go create mode 100644 pkg/signal/signal_darwin.go create mode 100644 pkg/signal/signal_freebsd.go create mode 100644 pkg/signal/signal_linux.go create mode 100644 pkg/signal/signal_unix.go create mode 100644 pkg/signal/signal_unsupported.go create mode 100644 pkg/signal/signal_windows.go create mode 100644 pkg/signal/trap.go create mode 100644 pkg/stdcopy/stdcopy.go create mode 100644 pkg/stdcopy/stdcopy_test.go create mode 100644 pkg/streamformatter/streamformatter.go create mode 100644 pkg/streamformatter/streamformatter_test.go create mode 100644 pkg/stringid/README.md create mode 100644 pkg/stringid/stringid.go create mode 100644 pkg/stringid/stringid_test.go create mode 100644 pkg/stringutils/README.md create mode 100644 pkg/stringutils/stringutils.go create mode 100644 pkg/stringutils/stringutils_test.go create mode 100644 pkg/symlink/LICENSE.APACHE create mode 100644 pkg/symlink/LICENSE.BSD create mode 100644 pkg/symlink/README.md create mode 100644 pkg/symlink/fs.go create mode 100644 pkg/symlink/fs_unix.go create mode 100644 pkg/symlink/fs_unix_test.go create mode 100644 pkg/symlink/fs_windows.go create mode 100644 pkg/sysinfo/README.md create mode 100644 pkg/sysinfo/sysinfo.go create mode 100644 pkg/sysinfo/sysinfo_freebsd.go create mode 100644 pkg/sysinfo/sysinfo_linux.go create mode 100644 pkg/sysinfo/sysinfo_linux_test.go create mode 100644 pkg/sysinfo/sysinfo_test.go create mode 100644 pkg/sysinfo/sysinfo_windows.go create mode 100644 pkg/system/chtimes.go create mode 100644 pkg/system/chtimes_test.go create mode 100644 pkg/system/chtimes_unix.go create mode 100644 pkg/system/chtimes_unix_test.go create mode 100644 pkg/system/chtimes_windows.go create mode 100644 pkg/system/chtimes_windows_test.go create mode 100644 pkg/system/errors.go create mode 100644 pkg/system/events_windows.go create mode 100644 pkg/system/filesys.go create mode 100644 pkg/system/filesys_windows.go create mode 100644 pkg/system/lstat.go create mode 100644 pkg/system/lstat_unix_test.go create mode 100644 pkg/system/lstat_windows.go create mode 100644 pkg/system/meminfo.go create mode 100644 pkg/system/meminfo_linux.go create mode 100644 pkg/system/meminfo_unix_test.go create mode 100644 pkg/system/meminfo_unsupported.go create mode 100644 pkg/system/meminfo_windows.go create mode 100644 pkg/system/mknod.go create mode 100644 pkg/system/mknod_windows.go create mode 100644 pkg/system/path_unix.go create mode 100644 pkg/system/path_windows.go create mode 100644 pkg/system/stat.go create mode 100644 pkg/system/stat_freebsd.go create mode 100644 pkg/system/stat_linux.go create mode 100644 pkg/system/stat_openbsd.go create mode 100644 pkg/system/stat_solaris.go create mode 100644 pkg/system/stat_unix_test.go create mode 100644 pkg/system/stat_unsupported.go create mode 100644 pkg/system/stat_windows.go create mode 100644 pkg/system/syscall_unix.go create mode 100644 pkg/system/syscall_windows.go create mode 100644 pkg/system/umask.go create mode 100644 pkg/system/umask_windows.go create mode 100644 pkg/system/utimes_darwin.go create mode 100644 pkg/system/utimes_freebsd.go create mode 100644 pkg/system/utimes_linux.go create mode 100644 pkg/system/utimes_unix_test.go create mode 100644 pkg/system/utimes_unsupported.go create mode 100644 pkg/system/xattrs_linux.go create mode 100644 pkg/system/xattrs_unsupported.go create mode 100644 pkg/tailfile/tailfile.go create mode 100644 pkg/tailfile/tailfile_test.go create mode 100644 pkg/tarsum/builder_context.go create mode 100644 pkg/tarsum/builder_context_test.go create mode 100644 pkg/tarsum/fileinfosums.go create mode 100644 pkg/tarsum/fileinfosums_test.go create mode 100644 pkg/tarsum/tarsum.go create mode 100644 pkg/tarsum/tarsum_spec.md create mode 100644 pkg/tarsum/tarsum_test.go create mode 100644 pkg/tarsum/testdata/46af0962ab5afeb5ce6740d4d91652e69206fc991fd5328c1a94d364ad00e457/json create mode 100644 pkg/tarsum/testdata/46af0962ab5afeb5ce6740d4d91652e69206fc991fd5328c1a94d364ad00e457/layer.tar create mode 100644 pkg/tarsum/testdata/511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158/json create mode 100644 pkg/tarsum/testdata/511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158/layer.tar create mode 100644 pkg/tarsum/testdata/collision/collision-0.tar create mode 100644 pkg/tarsum/testdata/collision/collision-1.tar create mode 100644 pkg/tarsum/testdata/collision/collision-2.tar create mode 100644 pkg/tarsum/testdata/collision/collision-3.tar create mode 100644 pkg/tarsum/testdata/xattr/json create mode 100644 pkg/tarsum/testdata/xattr/layer.tar create mode 100644 pkg/tarsum/versioning.go create mode 100644 pkg/tarsum/versioning_test.go create mode 100644 pkg/tarsum/writercloser.go create mode 100644 pkg/term/ascii.go create mode 100644 pkg/term/ascii_test.go create mode 100644 pkg/term/tc_linux_cgo.go create mode 100644 pkg/term/tc_other.go create mode 100644 pkg/term/term.go create mode 100644 pkg/term/term_windows.go create mode 100644 pkg/term/termios_darwin.go create mode 100644 pkg/term/termios_freebsd.go create mode 100644 pkg/term/termios_linux.go create mode 100644 pkg/term/termios_openbsd.go create mode 100644 pkg/term/windows/ansi_reader.go create mode 100644 pkg/term/windows/ansi_writer.go create mode 100644 pkg/term/windows/console.go create mode 100644 pkg/term/windows/windows.go create mode 100644 pkg/term/windows/windows_test.go create mode 100644 pkg/tlsconfig/config.go create mode 100644 pkg/truncindex/truncindex.go create mode 100644 pkg/truncindex/truncindex_test.go create mode 100644 pkg/urlutil/urlutil.go create mode 100644 pkg/urlutil/urlutil_test.go create mode 100644 pkg/useragent/README.md create mode 100644 pkg/useragent/useragent.go create mode 100644 pkg/useragent/useragent_test.go create mode 100644 pkg/version/version.go create mode 100644 pkg/version/version_test.go create mode 100644 profiles/apparmor/apparmor.go create mode 100644 profiles/apparmor/template.go create mode 100755 profiles/seccomp/default.json create mode 100755 profiles/seccomp/fixtures/example.json create mode 100644 profiles/seccomp/generate.go create mode 100644 profiles/seccomp/seccomp.go create mode 100644 profiles/seccomp/seccomp_default.go create mode 100644 profiles/seccomp/seccomp_test.go create mode 100644 profiles/seccomp/seccomp_unsupported.go create mode 100644 project/ARM.md create mode 100644 project/BRANCHES-AND-TAGS.md create mode 120000 project/CONTRIBUTORS.md create mode 100644 project/GOVERNANCE.md create mode 100644 project/IRC-ADMINISTRATION.md create mode 100644 project/ISSUE-TRIAGE.md create mode 100644 project/PACKAGE-REPO-MAINTENANCE.md create mode 100644 project/PACKAGERS.md create mode 100644 project/PATCH-RELEASES.md create mode 100644 project/PRINCIPLES.md create mode 100644 project/README.md create mode 100644 project/RELEASE-CHECKLIST.md create mode 100644 project/RELEASE-PROCESS.md create mode 100644 project/REVIEWING.md create mode 100644 project/TOOLS.md create mode 100644 reference/reference.go create mode 100644 reference/reference_test.go create mode 100644 reference/store.go create mode 100644 reference/store_test.go create mode 100644 registry/auth.go create mode 100644 registry/auth_test.go create mode 100644 registry/config.go create mode 100644 registry/config_test.go create mode 100644 registry/config_unix.go create mode 100644 registry/config_windows.go create mode 100644 registry/endpoint_test.go create mode 100644 registry/endpoint_v1.go create mode 100644 registry/reference.go create mode 100644 registry/registry.go create mode 100644 registry/registry_mock_test.go create mode 100644 registry/registry_test.go create mode 100644 registry/service.go create mode 100644 registry/service_v1.go create mode 100644 registry/service_v2.go create mode 100644 registry/session.go create mode 100644 registry/types.go create mode 100644 restartmanager/restartmanager.go create mode 100644 restartmanager/restartmanager_test.go create mode 100644 runconfig/compare.go create mode 100644 runconfig/compare_test.go create mode 100644 runconfig/config.go create mode 100644 runconfig/config_test.go create mode 100644 runconfig/config_unix.go create mode 100644 runconfig/config_windows.go create mode 100644 runconfig/errors.go create mode 100644 runconfig/fixtures/unix/container_config_1_14.json create mode 100644 runconfig/fixtures/unix/container_config_1_17.json create mode 100644 runconfig/fixtures/unix/container_config_1_19.json create mode 100644 runconfig/fixtures/unix/container_hostconfig_1_14.json create mode 100644 runconfig/fixtures/unix/container_hostconfig_1_19.json create mode 100644 runconfig/fixtures/windows/container_config_1_19.json create mode 100644 runconfig/hostconfig.go create mode 100644 runconfig/hostconfig_test.go create mode 100644 runconfig/hostconfig_unix.go create mode 100644 runconfig/hostconfig_windows.go create mode 100644 runconfig/opts/envfile.go create mode 100644 runconfig/opts/envfile_test.go create mode 100644 runconfig/opts/fixtures/valid.env create mode 100644 runconfig/opts/fixtures/valid.label create mode 100644 runconfig/opts/opts.go create mode 100644 runconfig/opts/opts_test.go create mode 100644 runconfig/opts/parse.go create mode 100644 runconfig/opts/parse_test.go create mode 100644 runconfig/opts/throttledevice.go create mode 100644 runconfig/opts/ulimit.go create mode 100644 runconfig/opts/ulimit_test.go create mode 100644 runconfig/opts/weightdevice.go create mode 100644 runconfig/streams.go create mode 100644 utils/debug.go create mode 100644 utils/debug_test.go create mode 100644 utils/experimental.go create mode 100644 utils/names.go create mode 100644 utils/process_unix.go create mode 100644 utils/process_windows.go create mode 100644 utils/stubs.go create mode 100644 utils/templates/templates.go create mode 100644 utils/templates/templates_test.go create mode 100644 utils/utils.go create mode 100644 utils/utils_test.go create mode 100644 vendor/src/github.com/docker/notary/.gitignore create mode 100644 vendor/src/github.com/docker/notary/CHANGELOG.md create mode 100644 vendor/src/github.com/docker/notary/CONTRIBUTING.md create mode 100644 vendor/src/github.com/docker/notary/CONTRIBUTORS create mode 100644 vendor/src/github.com/docker/notary/Dockerfile create mode 100644 vendor/src/github.com/docker/notary/LICENSE create mode 100644 vendor/src/github.com/docker/notary/MAINTAINERS create mode 100644 vendor/src/github.com/docker/notary/Makefile create mode 100644 vendor/src/github.com/docker/notary/NOTARY_VERSION create mode 100644 vendor/src/github.com/docker/notary/README.md create mode 100644 vendor/src/github.com/docker/notary/ROADMAP.md create mode 100644 vendor/src/github.com/docker/notary/certs/certs.go create mode 100644 vendor/src/github.com/docker/notary/circle.yml create mode 100644 vendor/src/github.com/docker/notary/client/changelist/change.go create mode 100644 vendor/src/github.com/docker/notary/client/changelist/changelist.go create mode 100644 vendor/src/github.com/docker/notary/client/changelist/file_changelist.go create mode 100644 vendor/src/github.com/docker/notary/client/changelist/interface.go create mode 100644 vendor/src/github.com/docker/notary/client/client.go create mode 100644 vendor/src/github.com/docker/notary/client/delegations.go create mode 100644 vendor/src/github.com/docker/notary/client/helpers.go create mode 100644 vendor/src/github.com/docker/notary/client/repo.go create mode 100644 vendor/src/github.com/docker/notary/client/repo_pkcs11.go create mode 100644 vendor/src/github.com/docker/notary/const.go create mode 100755 vendor/src/github.com/docker/notary/coverpkg.sh create mode 100644 vendor/src/github.com/docker/notary/cryptoservice/certificate.go create mode 100644 vendor/src/github.com/docker/notary/cryptoservice/crypto_service.go create mode 100644 vendor/src/github.com/docker/notary/cryptoservice/import_export.go create mode 100644 vendor/src/github.com/docker/notary/docker-compose.yml create mode 100644 vendor/src/github.com/docker/notary/passphrase/passphrase.go create mode 100644 vendor/src/github.com/docker/notary/server.Dockerfile create mode 100644 vendor/src/github.com/docker/notary/signer.Dockerfile create mode 100644 vendor/src/github.com/docker/notary/trustmanager/filestore.go create mode 100644 vendor/src/github.com/docker/notary/trustmanager/keyfilestore.go create mode 100644 vendor/src/github.com/docker/notary/trustmanager/keystore.go create mode 100644 vendor/src/github.com/docker/notary/trustmanager/memorystore.go create mode 100644 vendor/src/github.com/docker/notary/trustmanager/store.go create mode 100644 vendor/src/github.com/docker/notary/trustmanager/x509filestore.go create mode 100644 vendor/src/github.com/docker/notary/trustmanager/x509memstore.go create mode 100644 vendor/src/github.com/docker/notary/trustmanager/x509store.go create mode 100644 vendor/src/github.com/docker/notary/trustmanager/x509utils.go create mode 100644 vendor/src/github.com/docker/notary/trustmanager/yubikey/non_pkcs11.go create mode 100644 vendor/src/github.com/docker/notary/trustmanager/yubikey/pkcs11_darwin.go create mode 100644 vendor/src/github.com/docker/notary/trustmanager/yubikey/pkcs11_interface.go create mode 100644 vendor/src/github.com/docker/notary/trustmanager/yubikey/pkcs11_linux.go create mode 100644 vendor/src/github.com/docker/notary/trustmanager/yubikey/yubikeystore.go create mode 100644 vendor/src/github.com/docker/notary/tuf/LICENSE create mode 100644 vendor/src/github.com/docker/notary/tuf/README.md create mode 100644 vendor/src/github.com/docker/notary/tuf/client/client.go create mode 100644 vendor/src/github.com/docker/notary/tuf/client/errors.go create mode 100644 vendor/src/github.com/docker/notary/tuf/data/errors.go create mode 100644 vendor/src/github.com/docker/notary/tuf/data/keys.go create mode 100644 vendor/src/github.com/docker/notary/tuf/data/roles.go create mode 100644 vendor/src/github.com/docker/notary/tuf/data/root.go create mode 100644 vendor/src/github.com/docker/notary/tuf/data/serializer.go create mode 100644 vendor/src/github.com/docker/notary/tuf/data/snapshot.go create mode 100644 vendor/src/github.com/docker/notary/tuf/data/targets.go create mode 100644 vendor/src/github.com/docker/notary/tuf/data/timestamp.go create mode 100644 vendor/src/github.com/docker/notary/tuf/data/types.go create mode 100644 vendor/src/github.com/docker/notary/tuf/signed/ed25519.go create mode 100644 vendor/src/github.com/docker/notary/tuf/signed/errors.go create mode 100644 vendor/src/github.com/docker/notary/tuf/signed/interface.go create mode 100644 vendor/src/github.com/docker/notary/tuf/signed/sign.go create mode 100644 vendor/src/github.com/docker/notary/tuf/signed/verifiers.go create mode 100644 vendor/src/github.com/docker/notary/tuf/signed/verify.go create mode 100644 vendor/src/github.com/docker/notary/tuf/store/errors.go create mode 100644 vendor/src/github.com/docker/notary/tuf/store/filestore.go create mode 100644 vendor/src/github.com/docker/notary/tuf/store/httpstore.go create mode 100644 vendor/src/github.com/docker/notary/tuf/store/interfaces.go create mode 100644 vendor/src/github.com/docker/notary/tuf/store/memorystore.go create mode 100644 vendor/src/github.com/docker/notary/tuf/store/offlinestore.go create mode 100644 vendor/src/github.com/docker/notary/tuf/tuf.go create mode 100644 vendor/src/github.com/docker/notary/tuf/utils/role_sort.go create mode 100644 vendor/src/github.com/docker/notary/tuf/utils/stack.go create mode 100644 vendor/src/github.com/docker/notary/tuf/utils/util.go create mode 100644 vendor/src/github.com/docker/notary/tuf/utils/utils.go create mode 100644 vendor/src/github.com/docker/notary/tuf/validation/errors.go create mode 100644 volume/drivers/adapter.go create mode 100644 volume/drivers/extpoint.go create mode 100644 volume/drivers/extpoint_test.go create mode 100644 volume/drivers/proxy.go create mode 100644 volume/drivers/proxy_test.go create mode 100644 volume/local/local.go create mode 100644 volume/local/local_test.go create mode 100644 volume/local/local_unix.go create mode 100644 volume/local/local_windows.go create mode 100644 volume/store/errors.go create mode 100644 volume/store/store.go create mode 100644 volume/store/store_test.go create mode 100644 volume/store/store_unix.go create mode 100644 volume/store/store_windows.go create mode 100644 volume/testutils/testutils.go create mode 100644 volume/volume.go create mode 100644 volume/volume_copy.go create mode 100644 volume/volume_propagation_linux.go create mode 100644 volume/volume_propagation_linux_test.go create mode 100644 volume/volume_propagation_unsupported.go create mode 100644 volume/volume_test.go create mode 100644 volume/volume_unix.go create mode 100644 volume/volume_windows.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..9bd2c021 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +bundles +.gopath +vendor/pkg diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..53e518f7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,51 @@ + + +**Output of `docker version`:** + +``` +(paste your output here) +``` + + +**Output of `docker info`:** + +``` +(paste your output here) +``` + +**Additional environment details (AWS, VirtualBox, physical, etc.):** + + + +**Steps to reproduce the issue:** +1. +2. +3. + + +**Describe the results you received:** + + +**Describe the results you expected:** + + +**Additional information you deem important (e.g. issue happens only occasionally):** diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..69f1538b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,23 @@ + + +**- What I did** + +**- How I did it** + +**- How to verify it** + +**- A picture of a cute animal (not mandatory but encouraged)** + diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..9ea55ed0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Docker project generated files to ignore +# if you want to ignore files created by your editor/tools, +# please consider a global .gitignore https://help.github.com/articles/ignoring-files +*.exe +*.exe~ +*.orig +*.test +.*.swp +.DS_Store +.gopath/ +autogen/ +bundles/ +docker/docker +dockerversion/version_autogen.go +docs/AWS_S3_BUCKET +docs/GITCOMMIT +docs/GIT_BRANCH +docs/VERSION +docs/_build +docs/_static +docs/_templates +docs/changed-files +# generated by man/md2man-all.sh +man/man1 +man/man5 +man/man8 +vendor/pkg/ diff --git a/.mailmap b/.mailmap new file mode 100644 index 00000000..685f8e6b --- /dev/null +++ b/.mailmap @@ -0,0 +1,237 @@ +# Generate AUTHORS: hack/generate-authors.sh + +# Tip for finding duplicates (besides scanning the output of AUTHORS for name +# duplicates that aren't also email duplicates): scan the output of: +# git log --format='%aE - %aN' | sort -uf +# +# For explanation on this file format: man git-shortlog + +Patrick Stapleton +Shishir Mahajan +Erwin van der Koogh +Ahmed Kamal +Tejesh Mehta +Cristian Staretu +Cristian Staretu +Cristian Staretu +Marcus Linke +Aleksandrs Fadins +Christopher Latham +Hu Keping +Wayne Chang +Chen Chao +Daehyeok Mun + + + + + + +Guillaume J. Charmes + + + + + +Thatcher Peskens +Thatcher Peskens +Thatcher Peskens dhrp +Jérôme Petazzoni jpetazzo +Jérôme Petazzoni +Joffrey F +Joffrey F +Joffrey F +Tim Terhorst +Andy Smith + + + + + + + + + +Walter Stanish + +Roberto Hashioka +Konstantin Pelykh +David Sissitka +Nolan Darilek + +Benoit Chesneau +Jordan Arentsen +Daniel Garcia +Miguel Angel Fernández +Bhiraj Butala +Faiz Khan +Victor Lyuboslavsky +Jean-Baptiste Barth +Matthew Mueller + +Shih-Yuan Lee +Daniel Mizyrycki root +Jean-Baptiste Dalido + + + + + + + + + + + + + + +Sven Dowideit +Sven Dowideit +Sven Dowideit +Sven Dowideit <¨SvenDowideit@home.org.au¨> +Sven Dowideit +Sven Dowideit + +Alexander Morozov +Alexander Morozov + +O.S. Tezer + +Roberto G. Hashioka + + + + + +Sridhar Ratnakumar +Sridhar Ratnakumar +Liang-Chi Hsieh +Aleksa Sarai +Aleksa Sarai +Aleksa Sarai +Will Weaver +Timothy Hobbs +Nathan LeClaire +Nathan LeClaire + + + + +Matthew Heon + + + + +Francisco Carriedo + + + + +Brian Goff + + + +Hollie Teal + + + +Jessica Frazelle +Jessica Frazelle +Jessica Frazelle +Jessica Frazelle +Jessica Frazelle + + + + +Thomas LEVEIL Thomas LÉVEIL + + +Antonio Murdaca +Antonio Murdaca +Antonio Murdaca +Antonio Murdaca +Antonio Murdaca +Darren Shepherd +Deshi Xiao +Deshi Xiao +Doug Davis +Jacob Atzen +Jeff Nickoloff +John Howard (VM) +John Howard (VM) +John Howard (VM) +Madhu Venugopal +Mary Anthony +Mary Anthony moxiegirl +Mary Anthony +mattyw +resouer +AJ Bowen soulshake +AJ Bowen soulshake +Tibor Vass +Tibor Vass +Vincent Bernat +Yestin Sun +bin liu +John Howard (VM) jhowardmsft +Ankush Agarwal +Tangi COLIN tangicolin +Allen Sun +Adrien Gallouët + +Anuj Bahuguna +Anusha Ragunathan +Avi Miller +Brent Salisbury +Chander G +Chun Chen +Ying Li +Daehyeok Mun + +Daniel, Dao Quang Minh +Daniel Nephin +Dave Tucker +Doug Tangren +Frederick F. Kautz IV +Ben Golub +Harold Cooper +hsinko <21551195@zju.edu.cn> +Josh Hawn +Justin Cormack + + +Kamil Domański +Lei Jitang + +Linus Heckemann + +Lynda O'Leary + +Marianna Tessel +Michael Huettermann +Moysés Borges + +Nigel Poulton +Qiang Huang + +Boaz Shuster +Shuwei Hao + +Soshi Katsuta + +Stefan Berger + +Stephen Day + +Toli Kuznets +Tristan Carel + +Vincent Demeester + +Vishnu Kannan +xlgao-zju xlgao +yuchangchun y00277921 + + diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..bfb128ca --- /dev/null +++ b/AUTHORS @@ -0,0 +1,1460 @@ +# This file lists all individuals having contributed content to the repository. +# For how it is generated, see `hack/generate-authors.sh`. + +Aanand Prasad +Aaron Davidson +Aaron Feng +Aaron Huslage +Aaron Lehmann +Aaron Welch +Abel Muiño +Abhijeet Kasurde +Abhinav Ajgaonkar +Abhishek Chanda +Abin Shahab +Adam Miller +Adam Singer +Aditi Rajagopal +Aditya +Adria Casas +Adrian Mouat +Adrian Oprea +Adrien Folie +Adrien Gallouët +Ahmed Kamal +Ahmet Alp Balkan +Aidan Feldman +Aidan Hobson Sayers +AJ Bowen +Ajey Charantimath +ajneu +Akihiro Suda +Al Tobey +alambike +Alan Thompson +Albert Callarisa +Albert Zhang +Aleksa Sarai +Aleksandrs Fadins +Alena Prokharchyk +Alessandro Boch +Alessio Biancalana +Alex Chan +Alex Crawford +Alex Gaynor +Alex Samorukov +Alex Warhawk +Alexander Artemenko +Alexander Boyd +Alexander Larsson +Alexander Morozov +Alexander Shopov +Alexandre Beslic +Alexandre González +Alexandru Sfirlogea +Alexey Guskov +Alexey Kotlyarov +Alexey Shamrin +Alexis THOMAS +Allen Madsen +Allen Sun +almoehi +Alvin Richards +amangoel +Amen Belayneh +Amit Bakshi +Amit Krishnan +Amy Lindburg +Anand Patil +AnandkumarPatel +Anatoly Borodin +Anchal Agrawal +Anders Janmyr +Andre Dublin <81dublin@gmail.com> +Andre Granovsky +Andrea Luzzardi +Andrea Turli +Andreas Köhler +Andreas Savvides +Andreas Tiefenthaler +Andrew C. Bodine +Andrew Clay Shafer +Andrew Duckworth +Andrew France +Andrew Gerrand +Andrew Guenther +Andrew Kuklewicz +Andrew Macgregor +Andrew Macpherson +Andrew Martin +Andrew Munsell +Andrew Weiss +Andrew Williams +Andrews Medina +Andrey Petrov +Andrey Stolbovsky +André Martins +andy +Andy Chambers +andy diller +Andy Goldstein +Andy Kipp +Andy Rothfusz +Andy Smith +Andy Wilson +Anes Hasicic +Anil Belur +Ankush Agarwal +Anonmily +Anthon van der Neut +Anthony Baire +Anthony Bishopric +Anthony Dahanne +Anton Löfgren +Anton Nikitin +Anton Polonskiy +Anton Tiurin +Antonio Murdaca +Antony Messerli +Anuj Bahuguna +Anusha Ragunathan +apocas +ArikaChen +Arnaud Porterie +Arthur Barr +Arthur Gautier +Artur Meyster +Arun Gupta +Asbjørn Enge +averagehuman +Avi Das +Avi Miller +ayoshitake +Azat Khuyiyakhmetov +Bardia Keyoumarsi +Barnaby Gray +Barry Allard +Bartłomiej Piotrowski +Bastiaan Bakker +bdevloed +Ben Firshman +Ben Golub +Ben Hall +Ben Sargent +Ben Severson +Ben Toews +Ben Wiklund +Benjamin Atkin +Benoit Chesneau +Bernerd Schaefer +Bert Goethals +Bharath Thiruveedula +Bhiraj Butala +Bill W +bin liu +Blake Geno +Boaz Shuster +bobby abbott +boucher +Bouke Haarsma +Boyd Hemphill +Bradley Cicenas +Bradley Wright +Brandon Liu +Brandon Philips +Brandon Rhodes +Brendan Dixon +Brent Salisbury +Brett Kochendorfer +Brian (bex) Exelbierd +Brian Bland +Brian DeHamer +Brian Dorsey +Brian Flad +Brian Goff +Brian McCallister +Brian Olsen +Brian Shumate +Brian Torres-Gil +Brice Jaglin +Briehan Lombaard +Bruno Bigras +Bruno Binet +Bruno Gazzera +Bruno Renié +Bryan Bess +Bryan Boreham +Bryan Matsuo +Bryan Murphy +buddhamagnet +Burke Libbey +Byung Kang +Caleb Spare +Calen Pennington +Cameron Boehmer +Cameron Spear +Campbell Allen +Candid Dauth +Carl Henrik Lunde +Carl X. Su +Carlos Sanchez +Carol Fager-Higgins +Cary +Casey Bisson +Cedric Davies +Cezar Sa Espinola +Chad Swenson +Chance Zibolski +Chander G +Charles Chan +Charles Hooper +Charles Lindsay +Charles Merriam +Charles Sarrazin +Charlie Lewis +Chase Bolt +ChaYoung You +Chen Chao +Chen Hanxiao +cheney90 +Chewey +Chia-liang Kao +chli +Cholerae Hu +Chris Alfonso +Chris Armstrong +Chris Dituri +Chris Fordham +Chris Khoo +Chris McKinnel +Chris Seto +Chris Snow +Chris St. Pierre +Chris Stivers +Chris Swan +Chris Wahl +Chris Weyl +chrismckinnel +Christian Berendt +Christian Böhme +Christian Rotzoll +Christian Simon +Christian Stefanescu +ChristoperBiscardi +Christophe Mehay +Christophe Troestler +Christopher Currie +Christopher Jones +Christopher Latham +Christopher Rigor +Christy Perez +Chun Chen +Ciro S. Costa +Clayton Coleman +Clinton Kitson +Coenraad Loubser +Colin Dunklau +Colin Rice +Colin Walters +Collin Guarino +Colm Hally +companycy +Cory Forsyth +cressie176 +Cristian Staretu +cristiano balducci +Cruceru Calin-Cristian +Cyril F +Daan van Berkel +Daehyeok Mun +Dafydd Crosby +dalanlan +Damien Nozay +Damjan Georgievski +Dan Anolik +Dan Buch +Dan Cotora +Dan Griffin +Dan Hirsch +Dan Keder +Dan Levy +Dan McPherson +Dan Stine +Dan Walsh +Dan Williams +Daniel Antlinger +Daniel Exner +Daniel Farrell +Daniel Garcia +Daniel Gasienica +Daniel Hiltgen +Daniel Menet +Daniel Mizyrycki +Daniel Nephin +Daniel Norberg +Daniel Nordberg +Daniel Robinson +Daniel S +Daniel Von Fange +Daniel YC Lin +Daniel Zhang +Daniel, Dao Quang Minh +Danny Berger +Danny Yates +Darren Coxall +Darren Shepherd +Darren Stahl +Dave Barboza +Dave Henderson +Dave Tucker +David Anderson +David Calavera +David Corking +David Cramer +David Currie +David Davis +David Gageot +David Gebler +David Lawrence +David Mackey +David Mat +David Mcanulty +David Pelaez +David R. Jenni +David Röthlisberger +David Sheets +David Sissitka +David Xia +David Young +Davide Ceretti +Dawn Chen +dcylabs +decadent +Deng Guangxing +Deni Bertovic +Denis Gladkikh +Denis Ollier +Dennis Docter +Derek +Derek +Derek Ch +Derek McGowan +Deric Crago +Deshi Xiao +devmeyster +Devvyn Murphy +Dharmit Shah +Dieter Reuter +Dima Stopel +Dimitri John Ledkov +Dinesh Subhraveti +Diogo Monica +DiuDiugirl +Djibril Koné +dkumor +Dmitry Demeshchuk +Dmitry Gusev +Dmitry V. Krivenok +Dmitry Vorobev +Dolph Mathews +Dominik Finkbeiner +Dominik Honnef +Don Kirkby +Don Kjer +Don Spaulding +Donald Huang +Dong Chen +Donovan Jones +Doug Davis +Doug MacEachern +Doug Tangren +Dr Nic Williams +dragon788 +Dražen Lučanin +Dustin Sallings +Ed Costello +Edmund Wagner +Eiichi Tsukata +Eike Herzbach +Eivind Uggedal +Elan Ruusamäe +Elias Probst +Elijah Zupancic +eluck +Elvir Kuric +Emil Hernvall +Emily Maier +Emily Rose +Emir Ozer +Enguerran +Eohyung Lee +Eric Hanchrow +Eric Lee +Eric Myhre +Eric Paris +Eric Rafaloff +Eric Rosenberg +Eric Sage +Eric Windisch +Eric-Olivier Lamey +Erik Bray +Erik Dubbelboer +Erik Hollensbe +Erik Inge Bolsø +Erik Kristensen +Erik Weathers +Erno Hopearuoho +Erwin van der Koogh +Euan +Eugene Yakubovich +eugenkrizo +evalle +Evan Allrich +Evan Carmi +Evan Hazlett +Evan Krall +Evan Phoenix +Evan Wies +Evgeny Vereshchagin +Ewa Czechowska +Eystein MÃ¥løy Stenberg +ezbercih +Fabiano Rosas +Fabio Falci +Fabio Rehm +Fabrizio Regini +Faiz Khan +falmp +Fangyuan Gao <21551127@zju.edu.cn> +Fareed Dudhia +Fathi Boudra +Federico Gimenez +Felix Geisendörfer +Felix Hupfeld +Felix Rabe +Felix Schindler +Ferenc Szabo +Fernando +Fero Volar +Filipe Brandenburger +Filipe Oliveira +fl0yd +Flavio Castelli +FLGMwt +Florian Klein +Florian Maier +Florian Weingarten +Florin Asavoaie +Francesc Campoy +Francisco Carriedo +Francisco Souza +Frank Groeneveld +Frank Herrmann +Frank Macreery +Frank Rosquin +Fred Lifton +Frederick F. Kautz IV +Frederik Loeffert +Frederik Nordahl Jul Sabroe +Freek Kalter +Félix Baylac-Jacqué +Félix Cantournet +Gabe Rosenhouse +Gabor Nagy +Gabriel Monroy +GabrielNicolasAvellaneda +Galen Sampson +Gareth Rushgrove +Garrett Barboza +Gaurav +gautam, prasanna +GennadySpb +Geoffrey Bachelet +George MacRorie +George Xie +Georgi Hristozov +Gereon Frey +German DZ +Gert van Valkenhoef +Gianluca Borello +Gildas Cuisinier +gissehel +Giuseppe Mazzotta +Gleb Fotengauer-Malinovskiy +Gleb M Borisov +Glyn Normington +GoBella +Goffert van Gool +Gosuke Miyashita +Gou Rao +Govinda Fichtner +Grant Reaber +Graydon Hoare +Greg Fausak +Greg Thornton +grossws +grunny +gs11 +Guilhem Lettron +Guilherme Salgado +Guillaume Dufour +Guillaume J. Charmes +guoxiuyan +Gurjeet Singh +Guruprasad +gwx296173 +Günter Zöchbauer +Hans Kristian Flaatten +Hans Rødtang +Hao Zhang <21521210@zju.edu.cn> +Harald Albers +Harley Laue +Harold Cooper +Harry Zhang +He Simei +heartlock <21521209@zju.edu.cn> +Hector Castro +Henning Sprang +Hobofan +Hollie Teal +Hong Xu +hsinko <21551195@zju.edu.cn> +Hu Keping +Hu Tao +Huanzhong Zhang +Huayi Zhang +Hugo Duncan +Hugo Marisco <0x6875676f@gmail.com> +Hunter Blanks +huqun +Huu Nguyen +hyeongkyu.lee +hyp3rdino +Ian Babrou +Ian Bishop +Ian Bull +Ian Calvert +Ian Lee +Ian Main +Ian Truslove +Iavael +Icaro Seara +Igor Dolzhikov +Ilkka Laukkanen +Ilya Dmitrichenko +Ilya Gusev +ILYA Khlopotov +imre Fitos +inglesp +Ingo Gottwald +Isaac Dupree +Isabel Jimenez +Isao Jonas +Ivan Babrou +Ivan Fraixedes +J Bruni +J. Nunn +Jack Danger Canty +Jacob Atzen +Jacob Edelman +Jake Champlin +Jake Moshenko +jakedt +James Allen +James Carey +James Carr +James DeFelice +James Harrison Fisher +James Kyburz +James Kyle +James Lal +James Mills +James Nugent +James Turnbull +Jamie Hannaford +Jamshid Afshar +Jan Keromnes +Jan Koprowski +Jan Pazdziora +Jan Toebes +Jan-Gerd Tenberge +Jan-Jaap Driessen +Jana Radhakrishnan +Januar Wayong +Jared Biel +Jaroslaw Zabiello +jaseg +Jasmine Hegman +Jason Divock +Jason Giedymin +Jason Green +Jason Hall +Jason Heiss +Jason Livesay +Jason McVetta +Jason Plum +Jason Shepherd +Jason Smith +Jason Sommer +Jason Stangroome +jaxgeller +Jay +Jay +Jay Kamat +Jean-Baptiste Barth +Jean-Baptiste Dalido +Jean-Paul Calderone +Jean-Tiare Le Bigot +Jeff Anderson +Jeff Johnston +Jeff Lindsay +Jeff Mickey +Jeff Minard +Jeff Nickoloff +Jeff Welch +Jeffrey Bolle +Jeffrey Morgan +Jeffrey van Gogh +Jenny Gebske +Jeremy Grosser +Jeremy Price +Jeremy Qian +Jeremy Unruh +Jeroen Jacobs +Jesse Dearing +Jesse Dubay +Jessica Frazelle +Jezeniel Zapanta +jgeiger +Jian Zhang +jianbosun +Jilles Oldenbeuving +Jim Alateras +Jim Perrin +Jimmy Cuadra +Jimmy Puckett +jimmyxian +Jinsoo Park +Jiri Popelka +Jiří Župka +jjy +jmzwcn +Joe Beda +Joe Doliner +Joe Ferguson +Joe Gordon +Joe Shaw +Joe Van Dyk +Joel Friedly +Joel Handwell +Joel Hansson +Joel Wurtz +Joey Geiger +Joey Gibson +Joffrey F +Johan Euphrosine +Johan Rydberg +Johannes 'fish' Ziemke +John Costa +John Feminella +John Gardiner Myers +John Gossman +John Howard (VM) +John OBrien III +John Starks +John Tims +John Warwick +John Willis +Jon Wedaman +Jonas Pfenniger +Jonathan A. Sternberg +Jonathan Boulle +Jonathan Camp +Jonathan Dowland +Jonathan Lebon +Jonathan McCrohan +Jonathan Mueller +Jonathan Pares +Jonathan Rudenberg +Joost Cassee +Jordan +Jordan Arentsen +Jordan Sissel +Jose Diaz-Gonzalez +Joseph Anthony Pasquale Holsten +Joseph Hager +Joseph Kern +Josh +Josh Hawn +Josh Poimboeuf +Josiah Kiehl +José Tomás Albornoz +JP +jrabbit +Julian Taylor +Julien Barbier +Julien Bisconti +Julien Bordellier +Julien Dubois +Julien Pervillé +Jun-Ru Chang +Jussi Nummelin +Justas Brazauskas +Justin Cormack +Justin Force +Justin Plock +Justin Simonelis +Jyrki Puttonen +Jérôme Petazzoni +Jörg Thalheim +Kai Blin +Kai Qiang Wu(Kennan) +Kamil Domański +Kanstantsin Shautsou +Karan Lyons +Kareem Khazem +kargakis +Karl Grzeszczak +Karol Duleba +Katie McLaughlin +Kato Kazuyoshi +Katrina Owen +Kawsar Saiyeed +kayrus +Keli Hu +Ken Cochrane +Ken ICHIKAWA +Kenfe-Mickael Laventure +Kenjiro Nakayama +Kent Johnson +Kevin "qwazerty" Houdebert +Kevin Clark +Kevin J. Lynagh +Kevin Menard +Kevin P. Kucharczyk +Kevin Shi +Kevin Wallace +Kevin Yap +Keyvan Fatehi +kies +Kim BKC Carlbacker +Kim Eik +Kimbro Staken +Kir Kolyshkin +Kiran Gangadharan +Kirill SIbirev +knappe +Kohei Tsuruta +Koichi Shiraishi +Konrad Kleine +Konstantin Pelykh +Krasimir Georgiev +Kristian Haugene +Kristina Zabunova +krrg +Kun Zhang +Kunal Kushwaha +Kyle Conroy +kyu +Lachlan Coote +Lai Jiangshan +Lajos Papp +Lakshan Perera +Lalatendu Mohanty +lalyos +Lance Chen +Lance Kinley +Lars Kellogg-Stedman +Lars R. Damerow +Laszlo Meszaros +Laurent Erignoux +Laurie Voss +Leandro Siqueira +Lee, Meng-Han +leeplay +Lei Jitang +Len Weincier +Lennie +Leszek Kowalski +Levi Blackstone +Levi Gross +Lewis Marshall +Lewis Peckover +Liana Lo +Liang Mingqiang +Liang-Chi Hsieh +liaoqingwei +limsy +Linus Heckemann +Liran Tal +Liron Levin +Liu Bo +Liu Hua +LIZAO LI +Lloyd Dewolf +Lokesh Mandvekar +longliqiang88 <394564827@qq.com> +Lorenz Leutgeb +Lorenzo Fontana +Louis Opter +Luca Marturana +Luca Orlandi +Luca-Bogdan Grigorescu +Lucas Chan +Luis Martínez de Bartolomé Izquierdo +Lukas Waslowski +lukaspustina +lukemarsden +Lynda O'Leary +Lénaïc Huard +Ma Shimiao +Mabin +Madhav Puri +Madhu Venugopal +Mageee <21521230.zju.edu.cn> +Mahesh Tiyyagura +malnick +Malte Janduda +manchoz +Manfred Touron +Manfred Zabarauskas +Manuel Meurer +Manuel Woelker +mapk0y +Marc Abramowitz +Marc Kuo +Marc Tamsky +Marcelo Salazar +Marco Hennings +Marcus Farkas +Marcus Linke +Marcus Ramberg +Marek Goldmann +Marian Marinov +Marianna Tessel +Mario Loriedo +Marius Gundersen +Marius Sturm +Marius Voila +Mark Allen +Mark McGranaghan +Mark McKinstry +Mark West +Marko Mikulicic +Marko Tibold +Markus Fix +Martijn Dwars +Martijn van Oosterhout +Martin Honermeyer +Martin Kelly +Martin Mosegaard Amdisen +Martin Redmond +Mary Anthony +Masahito Zembutsu +Mason Malone +Mateusz Sulima +Mathias Monnerville +Mathieu Le Marec - Pasquet +Matt Apperson +Matt Bachmann +Matt Bentley +Matt Haggard +Matt McCormick +Matt Moore +Matt Robenolt +Matthew Heon +Matthew Mayer +Matthew Mueller +Matthew Riley +Matthias Klumpp +Matthias Kühnle +Matthias Rampke +Matthieu Hauglustaine +mattymo +mattyw +Mauricio Garavaglia +mauriyouth +Max Shytikov +Maxim Ivanov +Maxim Kulkin +Maxim Treskin +Maxime Petazzoni +Meaglith Ma +meejah +Megan Kostick +Mehul Kar +Mengdi Gao +Mert Yazıcıoğlu +Micah Zoltu +Michael A. Smith +Michael Bridgen +Michael Brown +Michael Chiang +Michael Crosby +Michael Currie +Michael Gorsuch +Michael Grauer +Michael Hudson-Doyle +Michael Huettermann +Michael Käufl +Michael Neale +Michael Prokop +Michael Scharf +Michael Stapelberg +Michael Steinert +Michael Thies +Michael West +Michal Fojtik +Michal Gebauer +Michal Jemala +Michal Minar +Michaël Pailloncy +Michał Czeraszkiewicz +Michiel@unhosted +Miguel Angel Fernández +Miguel Morales +Mihai Borobocea +Mihuleacc Sergiu +Mike Brown +Mike Chelen +Mike Danese +Mike Dillon +Mike Dougherty +Mike Gaffney +Mike Goelzer +Mike Leone +Mike MacCana +Mike Naberezny +Mike Snitzer +Mikhail Sobolev +Miloslav Trmač +mingqing +Mingzhen Feng +Mitch Capper +mlarcher +Mohammad Banikazemi +Mohammed Aaqib Ansari +Mohit Soni +Morgan Bauer +Morgante Pell +Morgy93 +Morten Siebuhr +Morton Fox +Moysés Borges +mqliang +Mrunal Patel +msabansal +mschurenko +Mustafa Akın +Muthukumar R +Médi-Rémi Hashim +Nakul Pathak +Nalin Dahyabhai +Nan Monnand Deng +Naoki Orii +Natalie Parker +Nate Brennand +Nate Eagleson +Nate Jones +Nathan Hsieh +Nathan Kleyn +Nathan LeClaire +Nathan McCauley +Nathan Williams +Neal McBurnett +Nelson Chen +Nghia Tran +Niall O'Higgins +Nicholas E. Rabenau +Nick Irvine +Nick Parker +Nick Payne +Nick Stenning +Nick Stinemates +Nicolas Borboën +Nicolas De loof +Nicolas Dudebout +Nicolas Goy +Nicolas Kaiser +Nicolás Hock Isaza +Nigel Poulton +NikolaMandic +nikolas +Nishant Totla +NIWA Hideyuki +noducks +Nolan Darilek +nponeccop +Nuutti Kotivuori +nzwsch +O.S. Tezer +OddBloke +odk- +Oguz Bilgic +Oh Jinkyun +Ohad Schneider +Ole Reifschneider +Oliver Neal +Olivier Gambier +Olle Jonsson +Oriol Francès +Otto Kekäläinen +oyld +ozlerhakan +paetling +pandrew +panticz +Paolo G. Giarrusso +Pascal Borreli +Pascal Hartig +Patrick Devine +Patrick Hemmer +Patrick Stapleton +pattichen +Paul +paul +Paul Annesley +Paul Bellamy +Paul Bowsher +Paul Hammond +Paul Jimenez +Paul Lietar +Paul Liljenberg +Paul Morie +Paul Nasrat +Paul Weaver +Pavel Lobashov +Pavel Pospisil +Pavel Sutyrin +Pavel Tikhomirov +Pavlos Ratis +Peeyush Gupta +Peggy Li +Pei Su +Penghan Wang +perhapszzy@sina.com +Peter Bourgon +Peter Braden +Peter Choi +Peter Dave Hello +Peter Edge +Peter Ericson +Peter Esbensen +Peter Malmgren +Peter Salvatore +Peter Volpe +Peter Waller +Phil +Phil Estes +Phil Spitler +Philip Monroe +Philipp Wahala +Philipp Weissensteiner +Phillip Alexander +pidster +Piergiuliano Bossi +Pierre +Pierre Carrier +Pierre Wacrenier +Pierre-Alain RIVIERE +Piotr Bogdan +pixelistik +Porjo +Poul Kjeldager Sørensen +Pradeep Chhetri +Prasanna Gautam +Prayag Verma +Przemek Hejman +pysqz +qg <1373319223@qq.com> +qhuang +Qiang Huang +qq690388648 <690388648@qq.com> +Quentin Brossard +Quentin Perez +Quentin Tayssier +r0n22 +Rafal Jeczalik +Rafe Colton +Raghavendra K T +Raghuram Devarakonda +Rajat Pandit +Rajdeep Dua +Ralle +Ralph Bean +Ramkumar Ramachandra +Ramon van Alteren +Ray Tsang +ReadmeCritic +Recursive Madman +Regan McCooey +Remi Rampin +Renato Riccieri Santos Zannon +resouer +rgstephens +Rhys Hiltner +Rich Seymour +Richard +Richard Burnison +Richard Harvey +Richard Metzler +Richard Scothern +Richo Healey +Rick Bradley +Rick van de Loo +Rick Wieman +Rik Nijessen +Riku Voipio +Riley Guerin +Riyaz Faizullabhoy +Rob Vesse +Robert Bachmann +Robert Bittle +Robert Obryk +Robert Stern +Robert Wallis +Roberto G. Hashioka +Robin Naundorf +Robin Schneider +Robin Speekenbrink +robpc +Rodrigo Vaz +Roel Van Nyen +Roger Peppe +Rohit Jnagal +Rohit Kadam +Roland Huß +Roland Moriz +Roma Sokolov +Roman Strashkin +Ron Smits +root +root +root +Rory Hunter +Rory McCune +Ross Boucher +Rovanion Luckey +Rozhnov Alexandr +rsmoorthy +Rudolph Gottesheim +Rui Lopes +Ryan Anderson +Ryan Aslett +Ryan Belgrave +Ryan Detzel +Ryan Fowler +Ryan McLaughlin +Ryan O'Donnell +Ryan Seto +Ryan Thomas +Ryan Trauntvein +Ryan Wallner +RyanDeng +Rémy Greinhofer +s. rannou +s00318865 +Sabin Basyal +Sachin Joshi +Sagar Hani +Sally O'Malley +Sam Abed +Sam Alba +Sam Bailey +Sam J Sharpe +Sam Neirinck +Sam Reis +Sam Rijs +Sambuddha Basu +Sami Wagiaalla +Samuel Andaya +Samuel Dion-Girardeau +Samuel Karp +Samuel PHAN +Sankar சங்கர் +Sanket Saurav +Santhosh Manohar +sapphiredev +Satnam Singh +satoru +Satoshi Amemiya +scaleoutsean +Scott Bessler +Scott Collier +Scott Johnston +Scott Stamp +Scott Walls +sdreyesg +Sean Cronin +Sean OMeara +Sean P. Kane +Sebastiaan van Steenis +Sebastiaan van Stijn +Senthil Kumar Selvaraj +SeongJae Park +Seongyeol Lim +Sergey Alekseev +Sergey Evstifeev +Sevki Hasirci +Shane Canon +Shane da Silva +shaunol +Shawn Landden +Shawn Siefkas +Shekhar Gulati +Sheng Yang +Shih-Yuan Lee +Shijiang Wei +Shishir Mahajan +shuai-z +Shuwei Hao +Sian Lerk Lau +sidharthamani +Silas Sewell +Simei He +Simon Eskildsen +Simon Leinen +Simon Taranto +Sindhu S +Sjoerd Langkemper +Solganik Alexander +Solomon Hykes +Song Gao +Soshi Katsuta +Soulou +Spencer Brown +Spencer Smith +Sridatta Thatipamala +Sridhar Ratnakumar +Srini Brahmaroutu +Srini Brahmaroutu +Steeve Morin +Stefan Berger +Stefan J. Wernli +Stefan Praszalowicz +Stefan Scherer +Stefan Staudenmeyer +Stefan Weil +Stephen Crosby +Stephen Day +Stephen Rust +Steve Durrheimer +Steve Francia +Steve Koch +Steven Burgess +Steven Iveson +Steven Merrill +Steven Richards +Steven Taylor +Sujith Haridasan +Suryakumar Sudar +Sven Dowideit +Swapnil Daingade +Sylvain Baubeau +Sylvain Bellemare +Sébastien +Sébastien Luttringer +Sébastien Stormacq +TAGOMORI Satoshi +tang0th +Tangi COLIN +Tatsuki Sugiura +Tatsushi Inagaki +Taylor Jones +tbonza +Ted M. Young +Tehmasp Chaudhri +Tejesh Mehta +terryding77 <550147740@qq.com> +tgic +Thatcher Peskens +theadactyl +Thell 'Bo' Fowler +Thermionix +Thijs Terlouw +Thomas Bikeev +Thomas Frössman +Thomas Gazagnaire +Thomas Hansen +Thomas LEVEIL +Thomas Orozco +Thomas Schroeter +Thomas Sjögren +Thomas Swift +Thomas Texier +Tianon Gravi +Tibor Vass +Tiffany Low +Tim Bosse +Tim Dettrick +Tim Hockin +Tim Ruffles +Tim Smith +Tim Terhorst +Tim Wang +Tim Waugh +Tim Wraight +Timothy Hobbs +tjwebb123 +tobe +Tobias Bieniek +Tobias Bradtke +Tobias Gesellchen +Tobias Klauser +Tobias Schmidt +Tobias Schwab +Todd Crane +Todd Lunter +Todd Whiteman +Toli Kuznets +Tom Barlow +Tom Denham +Tom Fotherby +Tom Howe +Tom Hulihan +Tom Maaswinkel +Tom X. Tobin +Tomas Tomecek +Tomasz Kopczynski +Tomasz Lipinski +Tomasz Nurkiewicz +Tommaso Visconti +Tomáš Hrčka +Tonis Tiigi +Tonny Xu +Tony Daws +Tony Miller +toogley +Torstein Husebø +tpng +tracylihui <793912329@qq.com> +Travis Cline +Travis Thieman +Trent Ogren +Trevor +Trevor Pounds +trishnaguha +Tristan Carel +Troy Denton +Tyler Brock +Tzu-Jung Lee +Tõnis Tiigi +Ulysse Carion +unknown +vagrant +Vaidas Jablonskis +Veres Lajos +vgeta +Victor Coisne +Victor Costan +Victor I. Wood +Victor Lyuboslavsky +Victor Marmol +Victor Palma +Victor Vieux +Victoria Bialas +Vijaya Kumar K +Viktor Stanchev +Viktor Vojnovski +VinayRaghavanKS +Vincent Batts +Vincent Bernat +Vincent Bernat +Vincent Demeester +Vincent Giersch +Vincent Mayers +Vincent Woo +Vinod Kulkarni +Vishal Doshi +Vishnu Kannan +Vitor Monteiro +Vivek Agarwal +Vivek Dasgupta +Vivek Goyal +Vladimir Bulyga +Vladimir Kirillov +Vladimir Rutsky +Vladimir Varankin +VladimirAus +Vojtech Vitek (V-Teq) +waitingkuo +Walter Leibbrandt +Walter Stanish +WANG Chao +Ward Vandewege +WarheadsSE +Wayne Chang +Wei-Ting Kuo +weiyan +Weiyang Zhu +Wen Cheng Ma +Wenxuan Zhao +Wenyu You <21551128@zju.edu.cn> +Wes Morgan +Will Dietz +Will Rouesnel +Will Weaver +willhf +William Delanoue +William Henry +William Hubbs +William Riancho +William Thurston +WiseTrem +wlan0 +Wolfgang Powisch +wonderflow +xamyzhao +XiaoBing Jiang +Xiaoxu Chen +xiekeyang +Xinzi Zhou +Xiuming Chen +xlgao-zju +xuzhaokui +Yahya +YAMADA Tsuyoshi +Yan Feng +Yang Bai +yangshukui +Yasunori Mahata +Yestin Sun +Yi EungJun +Yibai Zhang +Yihang Ho +Ying Li +Yohei Ueda +Yong Tang +Yongzhi Pan +Youcef YEKHLEF +Yuan Sun +yuchangchun +yuchengxia +Yurii Rashkovskii +yuzou +Zac Dover +Zach Borboa +Zachary Jaffee +Zain Memon +Zaiste! +Zane DeGraffenried +Zefan Li +Zen Lin(Zhinan Lin) +Zhang Kun +Zhang Wei +Zhang Wentao +Zhenan Ye <21551168@zju.edu.cn> +Zhu Guihua +Zhuoyun Wei +Zilin Du +zimbatm +Ziming Dong +ZJUshuaizhou <21551191@zju.edu.cn> +zmarouf +Zoltan Tombol +zqh +Zuhayr Elahi +Álex González +Álvaro Lázaro +Átila Camurça Alves +尹吉峰 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..c160b542 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2435 @@ +# Changelog + +Items starting with `DEPRECATE` are important deprecation notices. For more +information on the list of deprecated flags and APIs please have a look at +https://docs.docker.com/engine/deprecated/ where target removal dates can also +be found. + +## 1.11.2 (2016-05-31) + +### Networking + +- Fix a stale endpoint issue on overlay networks during ungraceful restart ([#23015](https://github.com/docker/docker/pull/23015)) +- Fix an issue where the wrong port could be reported by `docker inspect/ps/port` ([#22997](https://github.com/docker/docker/pull/22997)) + +### Runtime + +- Fix a potential panic when running `docker build` ([#23032](https://github.com/docker/docker/pull/23032)) +- Fix interpretation of `--user` parameter ([#22998](https://github.com/docker/docker/pull/22998)) +- Fix a bug preventing container statistics to be correctly reported ([#22955](https://github.com/docker/docker/pull/22955)) +- Fix an issue preventing container to be restarted after daemon restart ([#22947](https://github.com/docker/docker/pull/22947)) +- Fix issues when running 32 bit binaries on Ubuntu 16.04 ([#22922](https://github.com/docker/docker/pull/22922)) +- Fix a possible deadlock on image deletion and container attach ([#22918](https://github.com/docker/docker/pull/22918)) +- Fix an issue where containers fail to start after a daemon restart if they depend on a containerized cluster store ([#22561](https://github.com/docker/docker/pull/22561)) +- Fix an issue causing `docker ps` to hang on CentOS when using devicemapper ([#22168](https://github.com/docker/docker/pull/22168), [#23067](https://github.com/docker/docker/pull/23067)) +- Fix a bug preventing to `docker exec` into a container when using devicemapper ([#22168](https://github.com/docker/docker/pull/22168), [#23067](https://github.com/docker/docker/pull/23067)) + + +## 1.11.1 (2016-04-26) + +### Distribution + +- Fix schema2 manifest media type to be of type `application/vnd.docker.container.image.v1+json` ([#21949](https://github.com/docker/docker/pull/21949)) + +### Documentation + ++ Add missing API documentation for changes introduced with 1.11.0 ([#22048](https://github.com/docker/docker/pull/22048)) + +### Builder + +* Append label passed to `docker build` as arguments as an implicit `LABEL` command at the end of the processed `Dockerfile` ([#22184](https://github.com/docker/docker/pull/22184)) + +### Networking + +- Fix a panic that would occur when forwarding DNS query ([#22261](https://github.com/docker/docker/pull/22261)) +- Fix an issue where OS threads could end up within an incorrect network namespace when using user defined networks ([#22261](https://github.com/docker/docker/pull/22261)) + +### Runtime + +- Fix a bug preventing labels configuration to be reloaded via the config file ([#22299](https://github.com/docker/docker/pull/22299)) +- Fix a regression where container mounting `/var/run` would prevent other containers from being removed ([#22256](https://github.com/docker/docker/pull/22256)) +- Fix an issue where it would be impossible to update both `memory-swap` and `memory` value together ([#22255](https://github.com/docker/docker/pull/22255)) +- Fix a regression from 1.11.0 where the `/auth` endpoint would not initialize `serveraddress` if it is not provided ([#22254](https://github.com/docker/docker/pull/22254)) +- Add missing cleanup of container temporary files when cancelling a schedule restart ([#22237](https://github.com/docker/docker/pull/22237)) +- Removed scary error message when no restart policy is specified ([#21993](https://github.com/docker/docker/pull/21993)) +- Fix a panic that would occur when the plugins were activated via the json spec ([#22191](https://github.com/docker/docker/pull/22191)) +- Fix restart backoff logic to correctly reset delay if container ran for at least 10secs ([#22125](https://github.com/docker/docker/pull/22125)) +- Remove error message when a container restart get cancelled ([#22123](https://github.com/docker/docker/pull/22123)) +- Fix an issue where `docker` would not correcly clean up after `docker exec` ([#22121](https://github.com/docker/docker/pull/22121)) +- Fix a panic that could occur when servicing concurrent `docker stats` commands ([#22120](https://github.com/docker/docker/pull/22120))` +- Revert deprecation of non-existing host directories auto-creation ([#22065](https://github.com/docker/docker/pull/22065)) +- Hide misleading rpc error on daemon shutdown ([#22058](https://github.com/docker/docker/pull/22058)) + +## 1.11.0 (2016-04-13) + +**IMPORTANT**: With Docker 1.11, a Linux docker installation is now made of 4 binaries (`docker`, [`docker-containerd`](https://github.com/docker/containerd), [`docker-containerd-shim`](https://github.com/docker/containerd) and [`docker-runc`](https://github.com/opencontainers/runc)). If you have scripts relying on docker being a single static binaries, please make sure to update them. Interaction with the daemon stay the same otherwise, the usage of the other binaries should be transparent. A Windows docker installation remains a single binary, `docker.exe`. + +### Builder + +- Fix a bug where Docker would not used the correct uid/gid when processing the `WORKDIR` command ([#21033](https://github.com/docker/docker/pull/21033)) +- Fix a bug where copy operations with userns would not use the proper uid/gid ([#20782](https://github.com/docker/docker/pull/20782), [#21162](https://github.com/docker/docker/pull/21162)) + +### Client + +* Usage of the `:` separator for security option has been deprecated. `=` should be used instead ([#21232](https://github.com/docker/docker/pull/21232)) ++ The client user agent is now passed to the registry on `pull`, `build`, `push`, `login` and `search` operations ([#21306](https://github.com/docker/docker/pull/21306), [#21373](https://github.com/docker/docker/pull/21373)) +* Allow setting the Domainname and Hostname separately through the API ([#20200](https://github.com/docker/docker/pull/20200)) +* Docker info will now warn users if it can not detect the kernel version or the operating system ([#21128](https://github.com/docker/docker/pull/21128)) +- Fix an issue where `docker stats --no-stream` output could be all 0s ([#20803](https://github.com/docker/docker/pull/20803)) +- Fix a bug where some newly started container would not appear in a running `docker stats` command ([#20792](https://github.com/docker/docker/pull/20792)) +* Post processing is no longer enabled for linux-cgo terminals ([#20587](https://github.com/docker/docker/pull/20587)) +- Values to `--hostname` are now refused if they do not comply with [RFC1123](https://tools.ietf.org/html/rfc1123) ([#20566](https://github.com/docker/docker/pull/20566)) ++ Docker learned how to use a SOCKS proxy ([#20366](https://github.com/docker/docker/pull/20366), [#18373](https://github.com/docker/docker/pull/18373)) ++ Docker now supports external credential stores ([#20107](https://github.com/docker/docker/pull/20107)) +* `docker ps` now supports displaying the list of volumes mounted inside a container ([#20017](https://github.com/docker/docker/pull/20017)) +* `docker info` now also report Docker's root directory location ([#19986](https://github.com/docker/docker/pull/19986)) +- Docker now prohibits login in with an empty username (spaces are trimmed) ([#19806](https://github.com/docker/docker/pull/19806)) +* Docker events attributes are now sorted by key ([#19761](https://github.com/docker/docker/pull/19761)) +* `docker ps` no longer show exported port for stopped containers ([#19483](https://github.com/docker/docker/pull/19483)) +- Docker now cleans after itself if a save/export command fails ([#17849](https://github.com/docker/docker/pull/17849)) +* Docker load learned how to display a progress bar ([#17329](https://github.com/docker/docker/pull/17329), [#120078](https://github.com/docker/docker/pull/20078)) + +### Distribution + +- Fix a panic that occurred when pulling an images with 0 layers ([#21222](https://github.com/docker/docker/pull/21222)) +- Fix a panic that could occur on error while pushing to a registry with a misconfigured token service ([#21212](https://github.com/docker/docker/pull/21212)) ++ All first-level delegation roles are now signed when doing a trusted push ([#21046](https://github.com/docker/docker/pull/21046)) ++ OAuth support for registries was added ([#20970](https://github.com/docker/docker/pull/20970)) +* `docker login` now handles token using the implementation found in [docker/distribution](https://github.com/docker/distribution) ([#20832](https://github.com/docker/docker/pull/20832)) +* `docker login` will no longer prompt for an email ([#20565](https://github.com/docker/docker/pull/20565)) +* Docker will now fallback to registry V1 if no basic auth credentials are available ([#20241](https://github.com/docker/docker/pull/20241)) +* Docker will now try to resume layer download where it left off after a network error/timeout ([#19840](https://github.com/docker/docker/pull/19840)) +- Fix generated manifest mediaType when pushing cross-repository ([#19509](https://github.com/docker/docker/pull/19509)) +- Fix docker requesting additional push credentials when pulling an image if Content Trust is enabled ([#20382](https://github.com/docker/docker/pull/20382)) + +### Logging + +- Fix a race in the journald log driver ([#21311](https://github.com/docker/docker/pull/21311)) +* Docker syslog driver now uses the RFC-5424 format when emitting logs ([#20121](https://github.com/docker/docker/pull/20121)) +* Docker GELF log driver now allows to specify the compression algorithm and level via the `gelf-compression-type` and `gelf-compression-level` options ([#19831](https://github.com/docker/docker/pull/19831)) +* Docker daemon learned to output uncolorized logs via the `--raw-logs` options ([#19794](https://github.com/docker/docker/pull/19794)) ++ Docker, on Windows platform, now includes an ETW (Event Tracing in Windows) logging driver named `etwlogs` ([#19689](https://github.com/docker/docker/pull/19689)) +* Journald log driver learned how to handle tags ([#19564](https://github.com/docker/docker/pull/19564)) ++ The fluentd log driver learned the following options: `fluentd-address`, `fluentd-buffer-limit`, `fluentd-retry-wait`, `fluentd-max-retries` and `fluentd-async-connect` ([#19439](https://github.com/docker/docker/pull/19439)) ++ Docker learned to send log to Google Cloud via the new `gcplogs` logging driver. ([#18766](https://github.com/docker/docker/pull/18766)) + + +### Misc + ++ When saving linked images together with `docker save` a subsequent `docker load` will correctly restore their parent/child relationship ([#21385](https://github.com/docker/docker/pull/c)) ++ Support for building the Docker cli for OpenBSD was added ([#21325](https://github.com/docker/docker/pull/21325)) ++ Labels can now be applied at network, volume and image creation ([#21270](https://github.com/docker/docker/pull/21270)) +* The `dockremap` is now created as a system user ([#21266](https://github.com/docker/docker/pull/21266)) +- Fix a few response body leaks ([#21258](https://github.com/docker/docker/pull/21258)) +- Docker, when run as a service with systemd, will now properly manage its processes cgroups ([#20633](https://github.com/docker/docker/pull/20633)) +* Docker info now reports the value of cgroup KernelMemory or emits a warning if it is not supported ([#20863](https://github.com/docker/docker/pull/20863)) +* Docker info now also reports the cgroup driver in use ([#20388](https://github.com/docker/docker/pull/20388)) +* Docker completion is now available on PowerShell ([#19894](https://github.com/docker/docker/pull/19894)) +* `dockerinit` is no more ([#19490](https://github.com/docker/docker/pull/19490),[#19851](https://github.com/docker/docker/pull/19851)) ++ Support for building Docker on arm64 was added ([#19013](https://github.com/docker/docker/pull/19013)) ++ Experimental support for building docker.exe in a native Windows Docker installation ([#18348](https://github.com/docker/docker/pull/18348)) + +### Networking + +- Fix panic if a node is forcibly removed from the cluster ([#21671](https://github.com/docker/docker/pull/21671)) +- Fix "error creating vxlan interface" when starting a container in a Swarm cluster ([#21671](https://github.com/docker/docker/pull/21671)) +* `docker network inspect` will now report all endpoints whether they have an active container or not ([#21160](https://github.com/docker/docker/pull/21160)) ++ Experimental support for the MacVlan and IPVlan network drivers have been added ([#21122](https://github.com/docker/docker/pull/21122)) +* Output of `docker network ls` is now sorted by network name ([#20383](https://github.com/docker/docker/pull/20383)) +- Fix a bug where Docker would allow a network to be created with the reserved `default` name ([#19431](https://github.com/docker/docker/pull/19431)) +* `docker network inspect` returns whether a network is internal or not ([#19357](https://github.com/docker/docker/pull/19357)) ++ Control IPv6 via explicit option when creating a network (`docker network create --ipv6`). This shows up as a new `EnableIPv6` field in `docker network inspect` ([#17513](https://github.com/docker/docker/pull/17513)) +* Support for AAAA Records (aka IPv6 Service Discovery) in embedded DNS Server ([#21396](https://github.com/docker/docker/pull/21396)) +- Fix to not forward docker domain IPv6 queries to external servers ([#21396](https://github.com/docker/docker/pull/21396)) +* Multiple A/AAAA records from embedded DNS Server for DNS Round robin ([#21019](https://github.com/docker/docker/pull/21019)) +- Fix endpoint count inconsistency after an ungraceful dameon restart ([#21261](https://github.com/docker/docker/pull/21261)) +- Move the ownership of exposed ports and port-mapping options from Endpoint to Sandbox ([#21019](https://github.com/docker/docker/pull/21019)) +- Fixed a bug which prevents docker reload when host is configured with ipv6.disable=1 ([#21019](https://github.com/docker/docker/pull/21019)) +- Added inbuilt nil IPAM driver ([#21019](https://github.com/docker/docker/pull/21019)) +- Fixed bug in iptables.Exists() logic [#21019](https://github.com/docker/docker/pull/21019) +- Fixed a Veth interface leak when using overlay network ([#21019](https://github.com/docker/docker/pull/21019)) +- Fixed a bug which prevents docker reload after a network delete during shutdown ([#20214](https://github.com/docker/docker/pull/20214)) +- Make sure iptables chains are recreated on firewalld reload ([#20419](https://github.com/docker/docker/pull/20419)) +- Allow to pass global datastore during config reload ([#20419](https://github.com/docker/docker/pull/20419)) +- For anonymous containers use the alias name for IP to name mapping, ie:DNS PTR record ([#21019](https://github.com/docker/docker/pull/21019)) +- Fix a panic when deleting an entry from /etc/hosts file ([#21019](https://github.com/docker/docker/pull/21019)) +- Source the forwarded DNS queries from the container net namespace ([#21019](https://github.com/docker/docker/pull/21019)) +- Fix to retain the network internal mode config for bridge networks on daemon reload ([#21780] (https://github.com/docker/docker/pull/21780)) +- Fix to retain IPAM driver option configs on daemon reload ([#21914] (https://github.com/docker/docker/pull/21914)) + +### Plugins + +- Fix a file descriptor leak that would occur every time plugins were enumerated ([#20686](https://github.com/docker/docker/pull/20686)) +- Fix an issue where Authz plugin would corrupt the payload body when faced with a large amount of data ([#20602](https://github.com/docker/docker/pull/20602)) + +### Runtime + +- Fix a panic that could occur when cleanup after a container started with invalid parameters ([#21716](https://github.com/docker/docker/pull/21716)) +- Fix a race with event timers stopping early ([#21692](https://github.com/docker/docker/pull/21692)) +- Fix race conditions in the layer store, potentially corrupting the map and crashing the process ([#21677](https://github.com/docker/docker/pull/21677)) +- Un-deprecate auto-creation of host directories for mounts. This feature was marked deprecated in ([#21666](https://github.com/docker/docker/pull/21666)) + Docker 1.9, but was decided to be too much of an backward-incompatible change, so it was decided to keep the feature. ++ It is now possible for containers to share the NET and IPC namespaces when `userns` is enabled ([#21383](https://github.com/docker/docker/pull/21383)) ++ `docker inspect ` will now expose the rootfs layers ([#21370](https://github.com/docker/docker/pull/21370)) ++ Docker Windows gained a minimal `top` implementation ([#21354](https://github.com/docker/docker/pull/21354)) +* Docker learned to report the faulty exe when a container cannot be started due to its condition ([#21345](https://github.com/docker/docker/pull/21345)) +* Docker with device mapper will now refuse to run if `udev sync` is not available ([#21097](https://github.com/docker/docker/pull/21097)) +- Fix a bug where Docker would not validate the config file upon configuration reload ([#21089](https://github.com/docker/docker/pull/21089)) +- Fix a hang that would happen on attach if initial start was to fail ([#21048](https://github.com/docker/docker/pull/21048)) +- Fix an issue where registry service options in the daemon configuration file were not properly taken into account ([#21045](https://github.com/docker/docker/pull/21045)) +- Fix a race between the exec and resize operations ([#21022](https://github.com/docker/docker/pull/21022)) +- Fix an issue where nanoseconds were not correctly taken in account when filtering Docker events ([#21013](https://github.com/docker/docker/pull/21013)) +- Fix the handling of Docker command when passed a 64 bytes id ([#21002](https://github.com/docker/docker/pull/21002)) +* Docker will now return a `204` (i.e http.StatusNoContent) code when it successfully deleted a network ([#20977](https://github.com/docker/docker/pull/20977)) +- Fix a bug where the daemon would wait indefinitely in case the process it was about to killed had already exited on its own ([#20967](https://github.com/docker/docker/pull/20967) +* The devmapper driver learned the `dm.min_free_space` option. If the mapped device free space reaches the passed value, new device creation will be prohibited. ([#20786](https://github.com/docker/docker/pull/20786)) ++ Docker can now prevent processes in container to gain new privileges via the `--security-opt=no-new-privileges` flag ([#20727](https://github.com/docker/docker/pull/20727)) +- Starting a container with the `--device` option will now correctly resolves symlinks ([#20684](https://github.com/docker/docker/pull/20684)) ++ Docker now relies on [`containerd`](https://github.com/docker/containerd) and [`runc`](https://github.com/opencontainers/runc) to spawn containers. ([#20662](https://github.com/docker/docker/pull/20662)) +- Fix docker configuration reloading to only alter value present in the given config file ([#20604](https://github.com/docker/docker/pull/20604)) ++ Docker now allows setting a container hostname via the `--hostname` flag when `--net=host` ([#20177](https://github.com/docker/docker/pull/20177)) ++ Docker now allows executing privileged container while running with `--userns-remap` if both `--privileged` and the new `--userns=host` flag are specified ([#20111](https://github.com/docker/docker/pull/20111)) +- Fix Docker not cleaning up correctly old containers upon restarting after a crash ([#19679](https://github.com/docker/docker/pull/19679)) +* Docker will now error out if it doesn't recognize a configuration key within the config file ([#19517](https://github.com/docker/docker/pull/19517)) +- Fix container loading, on daemon startup, when they depends on a plugin running within a container ([#19500](https://github.com/docker/docker/pull/19500)) +* `docker update` learned how to change a container restart policy ([#19116](https://github.com/docker/docker/pull/19116)) +* `docker inspect` now also returns a new `State` field containing the container state in a human readable way (i.e. one of `created`, `restarting`, `running`, `paused`, `exited` or `dead`)([#18966](https://github.com/docker/docker/pull/18966)) ++ Docker learned to limit the number of active pids (i.e. processes) within the container via the `pids-limit` flags. NOTE: This requires `CGROUP_PIDS=y` to be in the kernel configuration. ([#18697](https://github.com/docker/docker/pull/18697)) +- `docker load` now has a `--quiet` option to suppress the load output ([#20078](https://github.com/docker/docker/pull/20078)) +- Fix a bug in neighbor discovery for IPv6 peers ([#20842](https://github.com/docker/docker/pull/20842)) +- Fix a panic during cleanup if a container was started with invalid options ([#21802](https://github.com/docker/docker/pull/21802)) +- Fix a situation where a container cannot be stopped if the terminal is closed ([#21840](https://github.com/docker/docker/pull/21840)) + +### Security + +* Object with the `pcp_pmcd_t` selinux type were given management access to `/var/lib/docker(/.*)?` ([#21370](https://github.com/docker/docker/pull/21370)) +* `restart_syscall`, `copy_file_range`, `mlock2` joined the list of allowed calls in the default seccomp profile ([#21117](https://github.com/docker/docker/pull/21117), [#21262](https://github.com/docker/docker/pull/21262)) +* `send`, `recv` and `x32` were added to the list of allowed syscalls and arch in the default seccomp profile ([#19432](https://github.com/docker/docker/pull/19432)) +* Docker Content Trust now requests the server to perform snapshot signing ([#21046](https://github.com/docker/docker/pull/21046)) +* Support for using YubiKeys for Content Trust signing has been moved out of experimental ([#21591](https://github.com/docker/docker/pull/21591)) + +### Volumes + +* Output of `docker volume ls` is now sorted by volume name ([#20389](https://github.com/docker/docker/pull/20389)) +* Local volumes can now accepts options similar to the unix `mount` tool ([#20262](https://github.com/docker/docker/pull/20262)) +- Fix an issue where one letter directory name could not be used as source for volumes ([#21106](https://github.com/docker/docker/pull/21106)) ++ `docker run -v` now accepts a new flag `nocopy`. This tell the runtime not to copy the container path content into the volume (which is the default behavior) ([#21223](https://github.com/docker/docker/pull/21223)) + +## 1.10.3 (2016-03-10) + +### Runtime + +- Fix Docker client exiting with an "Unrecognized input header" error [#20706](https://github.com/docker/docker/pull/20706) +- Fix Docker exiting if Exec is started with both `AttachStdin` and `Detach` [#20647](https://github.com/docker/docker/pull/20647) + +### Distribution + +- Fix a crash when pushing multiple images sharing the same layers to the same repository in parallel [#20831](https://github.com/docker/docker/pull/20831) +- Fix a panic when pushing images to a registry which uses a misconfigured token service [#21030](https://github.com/docker/docker/pull/21030) + +### Plugin system + +- Fix issue preventing volume plugins to start when SELinux is enabled [#20834](https://github.com/docker/docker/pull/20834) +- Prevent Docker from exiting if a volume plugin returns a null response for Get requests [#20682](https://github.com/docker/docker/pull/20682) +- Fix plugin system leaking file descriptors if a plugin has an error [#20680](https://github.com/docker/docker/pull/20680) + +### Security + +- Fix linux32 emulation to fail during docker build [#20672](https://github.com/docker/docker/pull/20672) + It was due to the `personality` syscall being blocked by the default seccomp profile. +- Fix Oracle XE 10g failing to start in a container [#20981](https://github.com/docker/docker/pull/20981) + It was due to the `ipc` syscall being blocked by the default seccomp profile. +- Fix user namespaces not working on Linux From Scratch [#20685](https://github.com/docker/docker/pull/20685) +- Fix issue preventing daemon to start if userns is enabled and the `subuid` or `subgid` files contain comments [#20725](https://github.com/docker/docker/pull/20725) + +## 1.10.2 (2016-02-22) + +### Runtime + +- Prevent systemd from deleting containers' cgroups when its configuration is reloaded [#20518](https://github.com/docker/docker/pull/20518) +- Fix SELinux issues by disregarding `--read-only` when mounting `/dev/mqueue` [#20333](https://github.com/docker/docker/pull/20333) +- Fix chown permissions used during `docker cp` when userns is used [#20446](https://github.com/docker/docker/pull/20446) +- Fix configuration loading issue with all booleans defaulting to `true` [#20471](https://github.com/docker/docker/pull/20471) +- Fix occasional panic with `docker logs -f` [#20522](https://github.com/docker/docker/pull/20522) + +### Distribution + +- Keep layer reference if deletion failed to avoid a badly inconsistent state [#20513](https://github.com/docker/docker/pull/20513) +- Handle gracefully a corner case when canceling migration [#20372](https://github.com/docker/docker/pull/20372) +- Fix docker import on compressed data [#20367](https://github.com/docker/docker/pull/20367) +- Fix tar-split files corruption during migration that later cause docker push and docker save to fail [#20458](https://github.com/docker/docker/pull/20458) + +### Networking + +- Fix daemon crash if embedded DNS is sent garbage [#20510](https://github.com/docker/docker/pull/20510) + +### Volumes + +- Fix issue with multiple volume references with same name [#20381](https://github.com/docker/docker/pull/20381) + +### Security + +- Fix potential cache corruption and delegation conflict issues [#20523](https://github.com/docker/docker/pull/20523) + +## 1.10.1 (2016-02-11) + +### Runtime + +* Do not stop daemon on migration hard failure [#20156](https://github.com/docker/docker/pull/20156) +- Fix various issues with migration to content-addressable images [#20058](https://github.com/docker/docker/pull/20058) +- Fix ZFS permission bug with user namespaces [#20045](https://github.com/docker/docker/pull/20045) +- Do not leak /dev/mqueue from the host to all containers, keep it container-specific [#19876](https://github.com/docker/docker/pull/19876) [#20133](https://github.com/docker/docker/pull/20133) +- Fix `docker ps --filter before=...` to not show stopped containers without providing `-a` flag [#20135](https://github.com/docker/docker/pull/20135) + +### Security + +- Fix issue preventing docker events to work properly with authorization plugin [#20002](https://github.com/docker/docker/pull/20002) + +### Distribution + +* Add additional verifications and prevent from uploading invalid data to registries [#20164](https://github.com/docker/docker/pull/20164) +- Fix regression preventing uppercase characters in image reference hostname [#20175](https://github.com/docker/docker/pull/20175) + +### Networking + +- Fix embedded DNS for user-defined networks in the presence of firewalld [#20060](https://github.com/docker/docker/pull/20060) +- Fix issue where removing a network during shutdown left Docker inoperable [#20181](https://github.com/docker/docker/issues/20181) [#20235](https://github.com/docker/docker/issues/20235) +- Embedded DNS is now able to return compressed results [#20181](https://github.com/docker/docker/issues/20181) +- Fix port-mapping issue with `userland-proxy=false` [#20181](https://github.com/docker/docker/issues/20181) + +### Logging + +- Fix bug where tcp+tls protocol would be rejected [#20109](https://github.com/docker/docker/pull/20109) + +### Volumes + +- Fix issue whereby older volume drivers would not receive volume options [#19983](https://github.com/docker/docker/pull/19983) + +### Misc + +- Remove TasksMax from Docker systemd service [#20167](https://github.com/docker/docker/pull/20167) + +## 1.10.0 (2016-02-04) + +**IMPORTANT**: Docker 1.10 uses a new content-addressable storage for images and layers. +A migration is performed the first time docker is run, and can take a significant amount of time depending on the number of images present. +Refer to this page on the wiki for more information: https://github.com/docker/docker/wiki/Engine-v1.10.0-content-addressability-migration +We also released a cool migration utility that enables you to perform the migration before updating to reduce downtime. +Engine 1.10 migrator can be found on Docker Hub: https://hub.docker.com/r/docker/v1.10-migrator/ + +### Runtime + ++ New `docker update` command that allows updating resource constraints on running containers [#15078](https://github.com/docker/docker/pull/15078) ++ Add `--tmpfs` flag to `docker run` to create a tmpfs mount in a container [#13587](https://github.com/docker/docker/pull/13587) ++ Add `--format` flag to `docker images` command [#17692](https://github.com/docker/docker/pull/17692) ++ Allow to set daemon configuration in a file and hot-reload it with the `SIGHUP` signal [#18587](https://github.com/docker/docker/pull/18587) ++ Updated docker events to include more meta-data and event types [#18888](https://github.com/docker/docker/pull/18888) + This change is backward compatible in the API, but not on the CLI. ++ Add `--blkio-weight-device` flag to `docker run` [#13959](https://github.com/docker/docker/pull/13959) ++ Add `--device-read-bps` and `--device-write-bps` flags to `docker run` [#14466](https://github.com/docker/docker/pull/14466) ++ Add `--device-read-iops` and `--device-write-iops` flags to `docker run` [#15879](https://github.com/docker/docker/pull/15879) ++ Add `--oom-score-adj` flag to `docker run` [#16277](https://github.com/docker/docker/pull/16277) ++ Add `--detach-keys` flag to `attach`, `run`, `start` and `exec` commands to override the default key sequence that detaches from a container [#15666](https://github.com/docker/docker/pull/15666) ++ Add `--shm-size` flag to `run`, `create` and `build` to set the size of `/dev/shm` [#16168](https://github.com/docker/docker/pull/16168) ++ Show the number of running, stopped, and paused containers in `docker info` [#19249](https://github.com/docker/docker/pull/19249) ++ Show the `OSType` and `Architecture` in `docker info` [#17478](https://github.com/docker/docker/pull/17478) ++ Add `--cgroup-parent` flag on `daemon` to set cgroup parent for all containers [#19062](https://github.com/docker/docker/pull/19062) ++ Add `-L` flag to docker cp to follow symlinks [#16613](https://github.com/docker/docker/pull/16613) ++ New `status=dead` filter for `docker ps` [#17908](https://github.com/docker/docker/pull/17908) +* Change `docker run` exit codes to distinguish between runtime and application errors [#14012](https://github.com/docker/docker/pull/14012) +* Enhance `docker events --since` and `--until` to support nanoseconds and timezones [#17495](https://github.com/docker/docker/pull/17495) +* Add `--all`/`-a` flag to `stats` to include both running and stopped containers [#16742](https://github.com/docker/docker/pull/16742) +* Change the default cgroup-driver to `cgroupfs` [#17704](https://github.com/docker/docker/pull/17704) +* Emit a "tag" event when tagging an image with `build -t` [#17115](https://github.com/docker/docker/pull/17115) +* Best effort for linked containers' start order when starting the daemon [#18208](https://github.com/docker/docker/pull/18208) +* Add ability to add multiple tags on `build` [#15780](https://github.com/docker/docker/pull/15780) +* Permit `OPTIONS` request against any url, thus fixing issue with CORS [#19569](https://github.com/docker/docker/pull/19569) +- Fix the `--quiet` flag on `docker build` to actually be quiet [#17428](https://github.com/docker/docker/pull/17428) +- Fix `docker images --filter dangling=false` to now show all non-dangling images [#19326](https://github.com/docker/docker/pull/19326) +- Fix race condition causing autorestart turning off on restart [#17629](https://github.com/docker/docker/pull/17629) +- Recognize GPFS filesystems [#19216](https://github.com/docker/docker/pull/19216) +- Fix obscure bug preventing to start containers [#19751](https://github.com/docker/docker/pull/19751) +- Forbid `exec` during container restart [#19722](https://github.com/docker/docker/pull/19722) +- devicemapper: Increasing `--storage-opt dm.basesize` will now increase the base device size on daemon restart [#19123](https://github.com/docker/docker/pull/19123) + +### Security + ++ Add `--userns-remap` flag to `daemon` to support user namespaces (previously in experimental) [#19187](https://github.com/docker/docker/pull/19187) ++ Add support for custom seccomp profiles in `--security-opt` [#17989](https://github.com/docker/docker/pull/17989) ++ Add default seccomp profile [#18780](https://github.com/docker/docker/pull/18780) ++ Add `--authorization-plugin` flag to `daemon` to customize ACLs [#15365](https://github.com/docker/docker/pull/15365) ++ Docker Content Trust now supports the ability to read and write user delegations [#18887](https://github.com/docker/docker/pull/18887) + This is an optional, opt-in feature that requires the explicit use of the Notary command-line utility in order to be enabled. + Enabling delegation support in a specific repository will break the ability of Docker 1.9 and 1.8 to pull from that repository, if content trust is enabled. +* Allow SELinux to run in a container when using the BTRFS storage driver [#16452](https://github.com/docker/docker/pull/16452) + +### Distribution + +* Use content-addressable storage for images and layers [#17924](https://github.com/docker/docker/pull/17924) + Note that a migration is performed the first time docker is run; it can take a significant amount of time depending on the number of images and containers present. + Images no longer depend on the parent chain but contain a list of layer references. + `docker load`/`docker save` tarballs now also contain content-addressable image configurations. + For more information: https://github.com/docker/docker/wiki/Engine-v1.10.0-content-addressability-migration +* Add support for the new [manifest format ("schema2")](https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-2.md) [#18785](https://github.com/docker/docker/pull/18785) +* Lots of improvements for push and pull: performance++, retries on failed downloads, cancelling on client disconnect [#18353](https://github.com/docker/docker/pull/18353), [#18418](https://github.com/docker/docker/pull/18418), [#19109](https://github.com/docker/docker/pull/19109), [#18353](https://github.com/docker/docker/pull/18353) +* Limit v1 protocol fallbacks [#18590](https://github.com/docker/docker/pull/18590) +- Fix issue where docker could hang indefinitely waiting for a nonexistent process to pull an image [#19743](https://github.com/docker/docker/pull/19743) + +### Networking + ++ Use DNS-based discovery instead of `/etc/hosts` [#19198](https://github.com/docker/docker/pull/19198) ++ Support for network-scoped alias using `--net-alias` on `run` and `--alias` on `network connect` [#19242](https://github.com/docker/docker/pull/19242) ++ Add `--ip` and `--ip6` on `run` and `network connect` to support custom IP addresses for a container in a network [#19001](https://github.com/docker/docker/pull/19001) ++ Add `--ipam-opt` to `network create` for passing custom IPAM options [#17316](https://github.com/docker/docker/pull/17316) ++ Add `--internal` flag to `network create` to restrict external access to and from the network [#19276](https://github.com/docker/docker/pull/19276) ++ Add `kv.path` option to `--cluster-store-opt` [#19167](https://github.com/docker/docker/pull/19167) ++ Add `discovery.heartbeat` and `discovery.ttl` options to `--cluster-store-opt` to configure discovery TTL and heartbeat timer [#18204](https://github.com/docker/docker/pull/18204) ++ Add `--format` flag to `network inspect` [#17481](https://github.com/docker/docker/pull/17481) ++ Add `--link` to `network connect` to provide a container-local alias [#19229](https://github.com/docker/docker/pull/19229) ++ Support for Capability exchange with remote IPAM plugins [#18775](https://github.com/docker/docker/pull/18775) ++ Add `--force` to `network disconnect` to force container to be disconnected from network [#19317](https://github.com/docker/docker/pull/19317) +* Support for multi-host networking using built-in overlay driver for all engine supported kernels: 3.10+ [#18775](https://github.com/docker/docker/pull/18775) +* `--link` is now supported on `docker run` for containers in user-defined network [#19229](https://github.com/docker/docker/pull/19229) +* Enhance `docker network rm` to allow removing multiple networks [#17489](https://github.com/docker/docker/pull/17489) +* Include container names in `network inspect` [#17615](https://github.com/docker/docker/pull/17615) +* Include auto-generated subnets for user-defined networks in `network inspect` [#17316](https://github.com/docker/docker/pull/17316) +* Add `--filter` flag to `network ls` to hide predefined networks [#17782](https://github.com/docker/docker/pull/17782) +* Add support for network connect/disconnect to stopped containers [#18906](https://github.com/docker/docker/pull/18906) +* Add network ID to container inspect [#19323](https://github.com/docker/docker/pull/19323) +- Fix MTU issue where Docker would not start with two or more default routes [#18108](https://github.com/docker/docker/pull/18108) +- Fix duplicate IP address for containers [#18106](https://github.com/docker/docker/pull/18106) +- Fix issue preventing sometimes docker from creating the bridge network [#19338](https://github.com/docker/docker/pull/19338) +- Do not substitute 127.0.0.1 name server when using `--net=host` [#19573](https://github.com/docker/docker/pull/19573) + +### Logging + ++ New logging driver for Splunk [#16488](https://github.com/docker/docker/pull/16488) ++ Add support for syslog over TCP+TLS [#18998](https://github.com/docker/docker/pull/18998) +* Enhance `docker logs --since` and `--until` to support nanoseconds and time [#17495](https://github.com/docker/docker/pull/17495) +* Enhance AWS logs to auto-detect region [#16640](https://github.com/docker/docker/pull/16640) + +### Volumes + ++ Add support to set the mount propagation mode for a volume [#17034](https://github.com/docker/docker/pull/17034) +* Add `ls` and `inspect` endpoints to volume plugin API [#16534](https://github.com/docker/docker/pull/16534) + Existing plugins need to make use of these new APIs to satisfy users' expectation + For that, please use the new MIME type `application/vnd.docker.plugins.v1.2+json` [#19549](https://github.com/docker/docker/pull/19549) +- Fix data not being copied to named volumes [#19175](https://github.com/docker/docker/pull/19175) +- Fix issues preventing volume drivers from being containerized [#19500](https://github.com/docker/docker/pull/19500) +- Fix `docker volumes ls --dangling=false` to now show all non-dangling volumes [#19671](https://github.com/docker/docker/pull/19671) +- Do not remove named volumes on container removal [#19568](https://github.com/docker/docker/pull/19568) +- Allow external volume drivers to host anonymous volumes [#19190](https://github.com/docker/docker/pull/19190) + +### Builder + ++ Add support for `**` in `.dockerignore` to wildcard multiple levels of directories [#17090](https://github.com/docker/docker/pull/17090) +- Fix handling of UTF-8 characters in Dockerfiles [#17055](https://github.com/docker/docker/pull/17055) +- Fix permissions problem when reading from STDIN [#19283](https://github.com/docker/docker/pull/19283) + +### Client + ++ Add support for overriding the API version to use via an `DOCKER_API_VERSION` environment-variable [#15964](https://github.com/docker/docker/pull/15964) +- Fix a bug preventing Windows clients to log in to Docker Hub [#19891](https://github.com/docker/docker/pull/19891) + +### Misc + +* systemd: Set TasksMax in addition to LimitNPROC in systemd service file [#19391](https://github.com/docker/docker/pull/19391) + +### Deprecations + +* Remove LXC support. The LXC driver was deprecated in Docker 1.8, and has now been removed [#17700](https://github.com/docker/docker/pull/17700) +* Remove `--exec-driver` daemon flag, because it is no longer in use [#17700](https://github.com/docker/docker/pull/17700) +* Remove old deprecated single-dashed long CLI flags (such as `-rm`; use `--rm` instead) [#17724](https://github.com/docker/docker/pull/17724) +* Deprecate HostConfig at API container start [#17799](https://github.com/docker/docker/pull/17799) +* Deprecate docker packages for newly EOL'd Linux distributions: Fedora 21 and Ubuntu 15.04 (Vivid) [#18794](https://github.com/docker/docker/pull/18794), [#18809](https://github.com/docker/docker/pull/18809) +* Deprecate `-f` flag for docker tag [#18350](https://github.com/docker/docker/pull/18350) + +## 1.9.1 (2015-11-21) + +### Runtime + +- Do not prevent daemon from booting if images could not be restored (#17695) +- Force IPC mount to unmount on daemon shutdown/init (#17539) +- Turn IPC unmount errors into warnings (#17554) +- Fix `docker stats` performance regression (#17638) +- Clarify cryptic error message upon `docker logs` if `--log-driver=none` (#17767) +- Fix seldom panics (#17639, #17634, #17703) +- Fix opq whiteouts problems for files with dot prefix (#17819) +- devicemapper: try defaulting to xfs instead of ext4 for performance reasons (#17903, #17918) +- devicemapper: fix displayed fs in docker info (#17974) +- selinux: only relabel if user requested so with the `z` option (#17450, #17834) +- Do not make network calls when normalizing names (#18014) + +### Client + +- Fix `docker login` on windows (#17738) +- Fix bug with `docker inspect` output when not connected to daemon (#17715) +- Fix `docker inspect -f {{.HostConfig.Dns}} somecontainer` (#17680) + +### Builder + +- Fix regression with symlink behavior in ADD/COPY (#17710) + +### Networking + +- Allow passing a network ID as an argument for `--net` (#17558) +- Fix connect to host and prevent disconnect from host for `host` network (#17476) +- Fix `--fixed-cidr` issue when gateway ip falls in ip-range and ip-range is + not the first block in the network (#17853) +- Restore deterministic `IPv6` generation from `MAC` address on default `bridge` network (#17890) +- Allow port-mapping only for endpoints created on docker run (#17858) +- Fixed an endpoint delete issue with a possible stale sbox (#18102) + +### Distribution + +- Correct parent chain in v2 push when v1Compatibility files on the disk are inconsistent (#18047) + +## 1.9.0 (2015-11-03) + +### Runtime + ++ `docker stats` now returns block IO metrics (#15005) ++ `docker stats` now details network stats per interface (#15786) ++ Add `ancestor=` filter to `docker ps --filter` flag to filter +containers based on their ancestor images (#14570) ++ Add `label=` filter to `docker ps --filter` to filter containers +based on label (#16530) ++ Add `--kernel-memory` flag to `docker run` (#14006) ++ Add `--message` flag to `docker import` allowing to specify an optional +message (#15711) ++ Add `--privileged` flag to `docker exec` (#14113) ++ Add `--stop-signal` flag to `docker run` allowing to replace the container +process stopping signal (#15307) ++ Add a new `unless-stopped` restart policy (#15348) ++ Inspecting an image now returns tags (#13185) ++ Add container size information to `docker inspect` (#15796) ++ Add `RepoTags` and `RepoDigests` field to `/images/{name:.*}/json` (#17275) +- Remove the deprecated `/container/ps` endpoint from the API (#15972) +- Send and document correct HTTP codes for `/exec//start` (#16250) +- Share shm and mqueue between containers sharing IPC namespace (#15862) +- Event stream now shows OOM status when `--oom-kill-disable` is set (#16235) +- Ensure special network files (/etc/hosts etc.) are read-only if bind-mounted +with `ro` option (#14965) +- Improve `rmi` performance (#16890) +- Do not update /etc/hosts for the default bridge network, except for links (#17325) +- Fix conflict with duplicate container names (#17389) +- Fix an issue with incorrect template execution in `docker inspect` (#17284) +- DEPRECATE `-c` short flag variant for `--cpu-shares` in docker run (#16271) + +### Client + ++ Allow `docker import` to import from local files (#11907) + +### Builder + ++ Add a `STOPSIGNAL` Dockerfile instruction allowing to set a different +stop-signal for the container process (#15307) ++ Add an `ARG` Dockerfile instruction and a `--build-arg` flag to `docker build` +that allows to add build-time environment variables (#15182) +- Improve cache miss performance (#16890) + +### Storage + +- devicemapper: Implement deferred deletion capability (#16381) + +## Networking + ++ `docker network` exits experimental and is part of standard release (#16645) ++ New network top-level concept, with associated subcommands and API (#16645) + WARNING: the API is different from the experimental API ++ Support for multiple isolated/micro-segmented networks (#16645) ++ Built-in multihost networking using VXLAN based overlay driver (#14071) ++ Support for third-party network plugins (#13424) ++ Ability to dynamically connect containers to multiple networks (#16645) ++ Support for user-defined IP address management via pluggable IPAM drivers (#16910) ++ Add daemon flags `--cluster-store` and `--cluster-advertise` for built-in nodes discovery (#16229) ++ Add `--cluster-store-opt` for setting up TLS settings (#16644) ++ Add `--dns-opt` to the daemon (#16031) +- DEPRECATE following container `NetworkSettings` fields in API v1.21: `EndpointID`, `Gateway`, + `GlobalIPv6Address`, `GlobalIPv6PrefixLen`, `IPAddress`, `IPPrefixLen`, `IPv6Gateway` and `MacAddress`. + Those are now specific to the `bridge` network. Use `NetworkSettings.Networks` to inspect + the networking settings of a container per network. + +### Volumes + ++ New top-level `volume` subcommand and API (#14242) +- Move API volume driver settings to host-specific config (#15798) +- Print an error message if volume name is not unique (#16009) +- Ensure volumes created from Dockerfiles always use the local volume driver +(#15507) +- DEPRECATE auto-creating missing host paths for bind mounts (#16349) + +### Logging + ++ Add `awslogs` logging driver for Amazon CloudWatch (#15495) ++ Add generic `tag` log option to allow customizing container/image +information passed to driver (e.g. show container names) (#15384) +- Implement the `docker logs` endpoint for the journald driver (#13707) +- DEPRECATE driver-specific log tags (e.g. `syslog-tag`, etc.) (#15384) + +### Distribution + ++ `docker search` now works with partial names (#16509) +- Push optimization: avoid buffering to file (#15493) +- The daemon will display progress for images that were already being pulled +by another client (#15489) +- Only permissions required for the current action being performed are requested (#) ++ Renaming trust keys (and respective environment variables) from `offline` to +`root` and `tagging` to `repository` (#16894) +- DEPRECATE trust key environment variables +`DOCKER_CONTENT_TRUST_OFFLINE_PASSPHRASE` and +`DOCKER_CONTENT_TRUST_TAGGING_PASSPHRASE` (#16894) + +### Security + ++ Add SELinux profiles to the rpm package (#15832) +- Fix various issues with AppArmor profiles provided in the deb package +(#14609) +- Add AppArmor policy that prevents writing to /proc (#15571) + +## 1.8.3 (2015-10-12) + +### Distribution + +- Fix layer IDs lead to local graph poisoning (CVE-2014-8178) +- Fix manifest validation and parsing logic errors allow pull-by-digest validation bypass (CVE-2014-8179) ++ Add `--disable-legacy-registry` to prevent a daemon from using a v1 registry + +## 1.8.2 (2015-09-10) + +### Distribution + +- Fixes rare edge case of handling GNU LongLink and LongName entries. +- Fix ^C on docker pull. +- Fix docker pull issues on client disconnection. +- Fix issue that caused the daemon to panic when loggers weren't configured properly. +- Fix goroutine leak pulling images from registry V2. + +### Runtime + +- Fix a bug mounting cgroups for docker daemons running inside docker containers. +- Initialize log configuration properly. + +### Client: + +- Handle `-q` flag in `docker ps` properly when there is a default format. + +### Networking + +- Fix several corner cases with netlink. + +### Contrib + +- Fix several issues with bash completion. + +## 1.8.1 (2015-08-12) + +### Distribution + +* Fix a bug where pushing multiple tags would result in invalid images + +## 1.8.0 (2015-08-11) + +### Distribution + ++ Trusted pull, push and build, disabled by default +* Make tar layers deterministic between registries +* Don't allow deleting the image of running containers +* Check if a tag name to load is a valid digest +* Allow one character repository names +* Add a more accurate error description for invalid tag name +* Make build cache ignore mtime + +### Cli + ++ Add support for DOCKER_CONFIG/--config to specify config file dir ++ Add --type flag for docker inspect command ++ Add formatting options to `docker ps` with `--format` ++ Replace `docker -d` with new subcommand `docker daemon` +* Zsh completion updates and improvements +* Add some missing events to bash completion +* Support daemon urls with base paths in `docker -H` +* Validate status= filter to docker ps +* Display when a container is in --net=host in docker ps +* Extend docker inspect to export image metadata related to graph driver +* Restore --default-gateway{,-v6} daemon options +* Add missing unpublished ports in docker ps +* Allow duration strings in `docker events` as --since/--until +* Expose more mounts information in `docker inspect` + +### Runtime + ++ Add new Fluentd logging driver ++ Allow `docker import` to load from local files ++ Add logging driver for GELF via UDP ++ Allow to copy files from host to containers with `docker cp` ++ Promote volume drivers from experimental to master ++ Add rollover options to json-file log driver, and --log-driver-opts flag ++ Add memory swappiness tuning options +* Remove cgroup read-only flag when privileged +* Make /proc, /sys, & /dev readonly for readonly containers +* Add cgroup bind mount by default +* Overlay: Export metadata for container and image in `docker inspect` +* Devicemapper: external device activation +* Devicemapper: Compare uuid of base device on startup +* Remove RC4 from the list of registry cipher suites +* Add syslog-facility option +* LXC execdriver compatibility with recent LXC versions +* Mark LXC execriver as deprecated (to be removed with the migration to runc) + +### Plugins + +* Separate plugin sockets and specs locations +* Allow TLS connections to plugins + +### Bug fixes + +- Add missing 'Names' field to /containers/json API output +- Make `docker rmi` of dangling images safe while pulling +- Devicemapper: Change default basesize to 100G +- Go Scheduler issue with sync.Mutex and gcc +- Fix issue where Search API endpoint would panic due to empty AuthConfig +- Set image canonical names correctly +- Check dockerinit only if lxc driver is used +- Fix ulimit usage of nproc +- Always attach STDIN if -i,--interactive is specified +- Show error messages when saving container state fails +- Fixed incorrect assumption on --bridge=none treated as disable network +- Check for invalid port specifications in host configuration +- Fix endpoint leave failure for --net=host mode +- Fix goroutine leak in the stats API if the container is not running +- Check for apparmor file before reading it +- Fix DOCKER_TLS_VERIFY being ignored +- Set umask to the default on startup +- Correct the message of pause and unpause a non-running container +- Adjust disallowed CpuShares in container creation +- ZFS: correctly apply selinux context +- Display empty string instead of when IP opt is nil +- `docker kill` returns error when container is not running +- Fix COPY/ADD quoted/json form +- Fix goroutine leak on logs -f with no output +- Remove panic in nat package on invalid hostport +- Fix container linking in Fedora 22 +- Fix error caused using default gateways outside of the allocated range +- Format times in inspect command with a template as RFC3339Nano +- Make registry client to accept 2xx and 3xx http status responses as successful +- Fix race issue that caused the daemon to crash with certain layer downloads failed in a specific order. +- Fix error when the docker ps format was not valid. +- Remove redundant ip forward check. +- Fix issue trying to push images to repository mirrors. +- Fix error cleaning up network entrypoints when there is an initialization issue. + +## 1.7.1 (2015-07-14) + +#### Runtime + +- Fix default user spawning exec process with `docker exec` +- Make `--bridge=none` not to configure the network bridge +- Publish networking stats properly +- Fix implicit devicemapper selection with static binaries +- Fix socket connections that hung intermittently +- Fix bridge interface creation on CentOS/RHEL 6.6 +- Fix local dns lookups added to resolv.conf +- Fix copy command mounting volumes +- Fix read/write privileges in volumes mounted with --volumes-from + +#### Remote API + +- Fix unmarshalling of Command and Entrypoint +- Set limit for minimum client version supported +- Validate port specification +- Return proper errors when attach/reattach fail + +#### Distribution + +- Fix pulling private images +- Fix fallback between registry V2 and V1 + +## 1.7.0 (2015-06-16) + +#### Runtime ++ Experimental feature: support for out-of-process volume plugins +* The userland proxy can be disabled in favor of hairpin NAT using the daemon’s `--userland-proxy=false` flag +* The `exec` command supports the `-u|--user` flag to specify the new process owner ++ Default gateway for containers can be specified daemon-wide using the `--default-gateway` and `--default-gateway-v6` flags ++ The CPU CFS (Completely Fair Scheduler) quota can be set in `docker run` using `--cpu-quota` ++ Container block IO can be controlled in `docker run` using`--blkio-weight` ++ ZFS support ++ The `docker logs` command supports a `--since` argument ++ UTS namespace can be shared with the host with `docker run --uts=host` + +#### Quality +* Networking stack was entirely rewritten as part of the libnetwork effort +* Engine internals refactoring +* Volumes code was entirely rewritten to support the plugins effort ++ Sending SIGUSR1 to a daemon will dump all goroutines stacks without exiting + +#### Build ++ Support ${variable:-value} and ${variable:+value} syntax for environment variables ++ Support resource management flags `--cgroup-parent`, `--cpu-period`, `--cpu-quota`, `--cpuset-cpus`, `--cpuset-mems` ++ git context changes with branches and directories +* The .dockerignore file support exclusion rules + +#### Distribution ++ Client support for v2 mirroring support for the official registry + +#### Bugfixes +* Firewalld is now supported and will automatically be used when available +* mounting --device recursively + +## 1.6.2 (2015-05-13) + +#### Runtime +- Revert change prohibiting mounting into /sys + +## 1.6.1 (2015-05-07) + +#### Security +- Fix read/write /proc paths (CVE-2015-3630) +- Prohibit VOLUME /proc and VOLUME / (CVE-2015-3631) +- Fix opening of file-descriptor 1 (CVE-2015-3627) +- Fix symlink traversal on container respawn allowing local privilege escalation (CVE-2015-3629) +- Prohibit mount of /sys + +#### Runtime +- Update AppArmor policy to not allow mounts + +## 1.6.0 (2015-04-07) + +#### Builder ++ Building images from an image ID ++ Build containers with resource constraints, ie `docker build --cpu-shares=100 --memory=1024m...` ++ `commit --change` to apply specified Dockerfile instructions while committing the image ++ `import --change` to apply specified Dockerfile instructions while importing the image ++ Builds no longer continue in the background when canceled with CTRL-C + +#### Client ++ Windows Support + +#### Runtime ++ Container and image Labels ++ `--cgroup-parent` for specifying a parent cgroup to place container cgroup within ++ Logging drivers, `json-file`, `syslog`, or `none` ++ Pulling images by ID ++ `--ulimit` to set the ulimit on a container ++ `--default-ulimit` option on the daemon which applies to all created containers (and overwritten by `--ulimit` on run) + +## 1.5.0 (2015-02-10) + +#### Builder ++ Dockerfile to use for a given `docker build` can be specified with the `-f` flag +* Dockerfile and .dockerignore files can be themselves excluded as part of the .dockerignore file, thus preventing modifications to these files invalidating ADD or COPY instructions cache +* ADD and COPY instructions accept relative paths +* Dockerfile `FROM scratch` instruction is now interpreted as a no-base specifier +* Improve performance when exposing a large number of ports + +#### Hack ++ Allow client-side only integration tests for Windows +* Include docker-py integration tests against Docker daemon as part of our test suites + +#### Packaging ++ Support for the new version of the registry HTTP API +* Speed up `docker push` for images with a majority of already existing layers +- Fixed contacting a private registry through a proxy + +#### Remote API ++ A new endpoint will stream live container resource metrics and can be accessed with the `docker stats` command ++ Containers can be renamed using the new `rename` endpoint and the associated `docker rename` command +* Container `inspect` endpoint show the ID of `exec` commands running in this container +* Container `inspect` endpoint show the number of times Docker auto-restarted the container +* New types of event can be streamed by the `events` endpoint: ‘OOM’ (container died with out of memory), ‘exec_create’, and ‘exec_start' +- Fixed returned string fields which hold numeric characters incorrectly omitting surrounding double quotes + +#### Runtime ++ Docker daemon has full IPv6 support ++ The `docker run` command can take the `--pid=host` flag to use the host PID namespace, which makes it possible for example to debug host processes using containerized debugging tools ++ The `docker run` command can take the `--read-only` flag to make the container’s root filesystem mounted as readonly, which can be used in combination with volumes to force a container’s processes to only write to locations that will be persisted ++ Container total memory usage can be limited for `docker run` using the `--memory-swap` flag +* Major stability improvements for devicemapper storage driver +* Better integration with host system: containers will reflect changes to the host's `/etc/resolv.conf` file when restarted +* Better integration with host system: per-container iptable rules are moved to the DOCKER chain +- Fixed container exiting on out of memory to return an invalid exit code + +#### Other +* The HTTP_PROXY, HTTPS_PROXY, and NO_PROXY environment variables are properly taken into account by the client when connecting to the Docker daemon + +## 1.4.1 (2014-12-15) + +#### Runtime +- Fix issue with volumes-from and bind mounts not being honored after create + +## 1.4.0 (2014-12-11) + +#### Notable Features since 1.3.0 ++ Set key=value labels to the daemon (displayed in `docker info`), applied with + new `-label` daemon flag ++ Add support for `ENV` in Dockerfile of the form: + `ENV name=value name2=value2...` ++ New Overlayfs Storage Driver ++ `docker info` now returns an `ID` and `Name` field ++ Filter events by event name, container, or image ++ `docker cp` now supports copying from container volumes +- Fixed `docker tag`, so it honors `--force` when overriding a tag for existing + image. + +## 1.3.3 (2014-12-11) + +#### Security +- Fix path traversal vulnerability in processing of absolute symbolic links (CVE-2014-9356) +- Fix decompression of xz image archives, preventing privilege escalation (CVE-2014-9357) +- Validate image IDs (CVE-2014-9358) + +#### Runtime +- Fix an issue when image archives are being read slowly + +#### Client +- Fix a regression related to stdin redirection +- Fix a regression with `docker cp` when destination is the current directory + +## 1.3.2 (2014-11-20) + +#### Security +- Fix tar breakout vulnerability +* Extractions are now sandboxed chroot +- Security options are no longer committed to images + +#### Runtime +- Fix deadlock in `docker ps -f exited=1` +- Fix a bug when `--volumes-from` references a container that failed to start + +#### Registry ++ `--insecure-registry` now accepts CIDR notation such as 10.1.0.0/16 +* Private registries whose IPs fall in the 127.0.0.0/8 range do no need the `--insecure-registry` flag +- Skip the experimental registry v2 API when mirroring is enabled + +## 1.3.1 (2014-10-28) + +#### Security +* Prevent fallback to SSL protocols < TLS 1.0 for client, daemon and registry ++ Secure HTTPS connection to registries with certificate verification and without HTTP fallback unless `--insecure-registry` is specified + +#### Runtime +- Fix issue where volumes would not be shared + +#### Client +- Fix issue with `--iptables=false` not automatically setting `--ip-masq=false` +- Fix docker run output to non-TTY stdout + +#### Builder +- Fix escaping `$` for environment variables +- Fix issue with lowercase `onbuild` Dockerfile instruction +- Restrict environment variable expansion to `ENV`, `ADD`, `COPY`, `WORKDIR`, `EXPOSE`, `VOLUME` and `USER` + +## 1.3.0 (2014-10-14) + +#### Notable features since 1.2.0 ++ Docker `exec` allows you to run additional processes inside existing containers ++ Docker `create` gives you the ability to create a container via the CLI without executing a process ++ `--security-opts` options to allow user to customize container labels and apparmor profiles ++ Docker `ps` filters +- Wildcard support to COPY/ADD ++ Move production URLs to get.docker.com from get.docker.io ++ Allocate IP address on the bridge inside a valid CIDR ++ Use drone.io for PR and CI testing ++ Ability to setup an official registry mirror ++ Ability to save multiple images with docker `save` + +## 1.2.0 (2014-08-20) + +#### Runtime ++ Make /etc/hosts /etc/resolv.conf and /etc/hostname editable at runtime ++ Auto-restart containers using policies ++ Use /var/lib/docker/tmp for large temporary files ++ `--cap-add` and `--cap-drop` to tweak what linux capability you want ++ `--device` to use devices in containers + +#### Client ++ `docker search` on private registries ++ Add `exited` filter to `docker ps --filter` +* `docker rm -f` now kills instead of stop ++ Support for IPv6 addresses in `--dns` flag + +#### Proxy ++ Proxy instances in separate processes +* Small bug fix on UDP proxy + +## 1.1.2 (2014-07-23) + +#### Runtime ++ Fix port allocation for existing containers ++ Fix containers restart on daemon restart + +#### Packaging ++ Fix /etc/init.d/docker issue on Debian + +## 1.1.1 (2014-07-09) + +#### Builder +* Fix issue with ADD + +## 1.1.0 (2014-07-03) + +#### Notable features since 1.0.1 ++ Add `.dockerignore` support ++ Pause containers during `docker commit` ++ Add `--tail` to `docker logs` + +#### Builder ++ Allow a tar file as context for `docker build` +* Fix issue with white-spaces and multi-lines in `Dockerfiles` + +#### Runtime +* Overall performance improvements +* Allow `/` as source of `docker run -v` +* Fix port allocation +* Fix bug in `docker save` +* Add links information to `docker inspect` + +#### Client +* Improve command line parsing for `docker commit` + +#### Remote API +* Improve status code for the `start` and `stop` endpoints + +## 1.0.1 (2014-06-19) + +#### Notable features since 1.0.0 +* Enhance security for the LXC driver + +#### Builder +* Fix `ONBUILD` instruction passed to grandchildren + +#### Runtime +* Fix events subscription +* Fix /etc/hostname file with host networking +* Allow `-h` and `--net=none` +* Fix issue with hotplug devices in `--privileged` + +#### Client +* Fix artifacts with events +* Fix a panic with empty flags +* Fix `docker cp` on Mac OS X + +#### Miscellaneous +* Fix compilation on Mac OS X +* Fix several races + +## 1.0.0 (2014-06-09) + +#### Notable features since 0.12.0 +* Production support + +## 0.12.0 (2014-06-05) + +#### Notable features since 0.11.0 +* 40+ various improvements to stability, performance and usability +* New `COPY` Dockerfile instruction to allow copying a local file from the context into the container without ever extracting if the file is a tar file +* Inherit file permissions from the host on `ADD` +* New `pause` and `unpause` commands to allow pausing and unpausing of containers using cgroup freezer +* The `images` command has a `-f`/`--filter` option to filter the list of images +* Add `--force-rm` to clean up after a failed build +* Standardize JSON keys in Remote API to CamelCase +* Pull from a docker run now assumes `latest` tag if not specified +* Enhance security on Linux capabilities and device nodes + +## 0.11.1 (2014-05-07) + +#### Registry +- Fix push and pull to private registry + +## 0.11.0 (2014-05-07) + +#### Notable features since 0.10.0 + +* SELinux support for mount and process labels +* Linked containers can be accessed by hostname +* Use the net `--net` flag to allow advanced network configuration such as host networking so that containers can use the host's network interfaces +* Add a ping endpoint to the Remote API to do healthchecks of your docker daemon +* Logs can now be returned with an optional timestamp +* Docker now works with registries that support SHA-512 +* Multiple registry endpoints are supported to allow registry mirrors + +## 0.10.0 (2014-04-08) + +#### Builder +- Fix printing multiple messages on a single line. Fixes broken output during builds. +- Follow symlinks inside container's root for ADD build instructions. +- Fix EXPOSE caching. + +#### Documentation +- Add the new options of `docker ps` to the documentation. +- Add the options of `docker restart` to the documentation. +- Update daemon docs and help messages for --iptables and --ip-forward. +- Updated apt-cacher-ng docs example. +- Remove duplicate description of --mtu from docs. +- Add missing -t and -v for `docker images` to the docs. +- Add fixes to the cli docs. +- Update libcontainer docs. +- Update images in docs to remove references to AUFS and LXC. +- Update the nodejs_web_app in the docs to use the new epel RPM address. +- Fix external link on security of containers. +- Update remote API docs. +- Add image size to history docs. +- Be explicit about binding to all interfaces in redis example. +- Document DisableNetwork flag in the 1.10 remote api. +- Document that `--lxc-conf` is lxc only. +- Add chef usage documentation. +- Add example for an image with multiple for `docker load`. +- Explain what `docker run -a` does in the docs. + +#### Contrib +- Add variable for DOCKER_LOGFILE to sysvinit and use append instead of overwrite in opening the logfile. +- Fix init script cgroup mounting workarounds to be more similar to cgroupfs-mount and thus work properly. +- Remove inotifywait hack from the upstart host-integration example because it's not necessary any more. +- Add check-config script to contrib. +- Fix fish shell completion. + +#### Hack +* Clean up "go test" output from "make test" to be much more readable/scannable. +* Exclude more "definitely not unit tested Go source code" directories from hack/make/test. ++ Generate md5 and sha256 hashes when building, and upload them via hack/release.sh. +- Include contributed completions in Ubuntu PPA. ++ Add cli integration tests. +* Add tweaks to the hack scripts to make them simpler. + +#### Remote API ++ Add TLS auth support for API. +* Move git clone from daemon to client. +- Fix content-type detection in docker cp. +* Split API into 2 go packages. + +#### Runtime +* Support hairpin NAT without going through Docker server. +- devicemapper: succeed immediately when removing non-existing devices. +- devicemapper: improve handling of devicemapper devices (add per device lock, increase sleep time and unlock while sleeping). +- devicemapper: increase timeout in waitClose to 10 seconds. +- devicemapper: ensure we shut down thin pool cleanly. +- devicemapper: pass info, rather than hash to activateDeviceIfNeeded, deactivateDevice, setInitialized, deleteDevice. +- devicemapper: avoid AB-BA deadlock. +- devicemapper: make shutdown better/faster. +- improve alpha sorting in mflag. +- Remove manual http cookie management because the cookiejar is being used. +- Use BSD raw mode on Darwin. Fixes nano, tmux and others. +- Add FreeBSD support for the client. +- Merge auth package into registry. +- Add deprecation warning for -t on `docker pull`. +- Remove goroutine leak on error. +- Update parseLxcInfo to comply with new lxc1.0 format. +- Fix attach exit on darwin. +- Improve deprecation message. +- Retry to retrieve the layer metadata up to 5 times for `docker pull`. +- Only unshare the mount namespace for execin. +- Merge existing config when committing. +- Disable daemon startup timeout. +- Fix issue #4681: add loopback interface when networking is disabled. +- Add failing test case for issue #4681. +- Send SIGTERM to child, instead of SIGKILL. +- Show the driver and the kernel version in `docker info` even when not in debug mode. +- Always symlink /dev/ptmx for libcontainer. This fixes console related problems. +- Fix issue caused by the absence of /etc/apparmor.d. +- Don't leave empty cidFile behind when failing to create the container. +- Mount cgroups automatically if they're not mounted already. +- Use mock for search tests. +- Update to double-dash everywhere. +- Move .dockerenv parsing to lxc driver. +- Move all bind-mounts in the container inside the namespace. +- Don't use separate bind mount for container. +- Always symlink /dev/ptmx for libcontainer. +- Don't kill by pid for other drivers. +- Add initial logging to libcontainer. +* Sort by port in `docker ps`. +- Move networking drivers into runtime top level package. ++ Add --no-prune to `docker rmi`. ++ Add time since exit in `docker ps`. +- graphdriver: add build tags. +- Prevent allocation of previously allocated ports & prevent improve port allocation. +* Add support for --since/--before in `docker ps`. +- Clean up container stop. ++ Add support for configurable dns search domains. +- Add support for relative WORKDIR instructions. +- Add --output flag for docker save. +- Remove duplication of DNS entries in config merging. +- Add cpuset.cpus to cgroups and native driver options. +- Remove docker-ci. +- Promote btrfs. btrfs is no longer considered experimental. +- Add --input flag to `docker load`. +- Return error when existing bridge doesn't match IP address. +- Strip comments before parsing line continuations to avoid interpreting instructions as comments. +- Fix TestOnlyLoopbackExistsWhenUsingDisableNetworkOption to ignore "DOWN" interfaces. +- Add systemd implementation of cgroups and make containers show up as systemd units. +- Fix commit and import when no repository is specified. +- Remount /var/lib/docker as --private to fix scaling issue. +- Use the environment's proxy when pinging the remote registry. +- Reduce error level from harmless errors. +* Allow --volumes-from to be individual files. +- Fix expanding buffer in StdCopy. +- Set error regardless of attach or stdin. This fixes #3364. +- Add support for --env-file to load environment variables from files. +- Symlink /etc/mtab and /proc/mounts. +- Allow pushing a single tag. +- Shut down containers cleanly at shutdown and wait forever for the containers to shut down. This makes container shutdown on daemon shutdown work properly via SIGTERM. +- Don't throw error when starting an already running container. +- Fix dynamic port allocation limit. +- remove setupDev from libcontainer. +- Add API version to `docker version`. +- Return correct exit code when receiving signal and make SIGQUIT quit without cleanup. +- Fix --volumes-from mount failure. +- Allow non-privileged containers to create device nodes. +- Skip login tests because of external dependency on a hosted service. +- Deprecate `docker images --tree` and `docker images --viz`. +- Deprecate `docker insert`. +- Include base abstraction for apparmor. This fixes some apparmor related problems on Ubuntu 14.04. +- Add specific error message when hitting 401 over HTTP on push. +- Fix absolute volume check. +- Remove volumes-from from the config. +- Move DNS options to hostconfig. +- Update the apparmor profile for libcontainer. +- Add deprecation notice for `docker commit -run`. + +## 0.9.1 (2014-03-24) + +#### Builder +- Fix printing multiple messages on a single line. Fixes broken output during builds. + +#### Documentation +- Fix external link on security of containers. + +#### Contrib +- Fix init script cgroup mounting workarounds to be more similar to cgroupfs-mount and thus work properly. +- Add variable for DOCKER_LOGFILE to sysvinit and use append instead of overwrite in opening the logfile. + +#### Hack +- Generate md5 and sha256 hashes when building, and upload them via hack/release.sh. + +#### Remote API +- Fix content-type detection in `docker cp`. + +#### Runtime +- Use BSD raw mode on Darwin. Fixes nano, tmux and others. +- Only unshare the mount namespace for execin. +- Retry to retrieve the layer metadata up to 5 times for `docker pull`. +- Merge existing config when committing. +- Fix panic in monitor. +- Disable daemon startup timeout. +- Fix issue #4681: add loopback interface when networking is disabled. +- Add failing test case for issue #4681. +- Send SIGTERM to child, instead of SIGKILL. +- Show the driver and the kernel version in `docker info` even when not in debug mode. +- Always symlink /dev/ptmx for libcontainer. This fixes console related problems. +- Fix issue caused by the absence of /etc/apparmor.d. +- Don't leave empty cidFile behind when failing to create the container. +- Improve deprecation message. +- Fix attach exit on darwin. +- devicemapper: improve handling of devicemapper devices (add per device lock, increase sleep time, unlock while sleeping). +- devicemapper: succeed immediately when removing non-existing devices. +- devicemapper: increase timeout in waitClose to 10 seconds. +- Remove goroutine leak on error. +- Update parseLxcInfo to comply with new lxc1.0 format. + +## 0.9.0 (2014-03-10) + +#### Builder +- Avoid extra mount/unmount during build. This fixes mount/unmount related errors during build. +- Add error to docker build --rm. This adds missing error handling. +- Forbid chained onbuild, `onbuild from` and `onbuild maintainer` triggers. +- Make `--rm` the default for `docker build`. + +#### Documentation +- Download the docker client binary for Mac over https. +- Update the titles of the install instructions & descriptions. +* Add instructions for upgrading boot2docker. +* Add port forwarding example in OS X install docs. +- Attempt to disentangle repository and registry. +- Update docs to explain more about `docker ps`. +- Update sshd example to use a Dockerfile. +- Rework some examples, including the Python examples. +- Update docs to include instructions for a container's lifecycle. +- Update docs documentation to discuss the docs branch. +- Don't skip cert check for an example & use HTTPS. +- Bring back the memory and swap accounting section which was lost when the kernel page was removed. +- Explain DNS warnings and how to fix them on systems running and using a local nameserver. + +#### Contrib +- Add Tanglu support for mkimage-debootstrap. +- Add SteamOS support for mkimage-debootstrap. + +#### Hack +- Get package coverage when running integration tests. +- Remove the Vagrantfile. This is being replaced with boot2docker. +- Fix tests on systems where aufs isn't available. +- Update packaging instructions and remove the dependency on lxc. + +#### Remote API +* Move code specific to the API to the api package. +- Fix header content type for the API. Makes all endpoints use proper content type. +- Fix registry auth & remove ping calls from CmdPush and CmdPull. +- Add newlines to the JSON stream functions. + +#### Runtime +* Do not ping the registry from the CLI. All requests to registries flow through the daemon. +- Check for nil information return in the lxc driver. This fixes panics with older lxc versions. +- Devicemapper: cleanups and fix for unmount. Fixes two problems which were causing unmount to fail intermittently. +- Devicemapper: remove directory when removing device. Directories don't get left behind when removing the device. +* Devicemapper: enable skip_block_zeroing. Improves performance by not zeroing blocks. +- Devicemapper: fix shutdown warnings. Fixes shutdown warnings concerning pool device removal. +- Ensure docker cp stream is closed properly. Fixes problems with files not being copied by `docker cp`. +- Stop making `tcp://` default to `127.0.0.1:4243` and remove the default port for tcp. +- Fix `--run` in `docker commit`. This makes `docker commit --run` work again. +- Fix custom bridge related options. This makes custom bridges work again. ++ Mount-bind the PTY as container console. This allows tmux/screen to run. ++ Add the pure Go libcontainer library to make it possible to run containers using only features of the Linux kernel. ++ Add native exec driver which uses libcontainer and make it the default exec driver. +- Add support for handling extended attributes in archives. +* Set the container MTU to be the same as the host MTU. ++ Add simple sha256 checksums for layers to speed up `docker push`. +* Improve kernel version parsing. +* Allow flag grouping (`docker run -it`). +- Remove chroot exec driver. +- Fix divide by zero to fix panic. +- Rewrite `docker rmi`. +- Fix docker info with lxc 1.0.0. +- Fix fedora tty with apparmor. +* Don't always append env vars, replace defaults with vars from config. +* Fix a goroutine leak. +* Switch to Go 1.2.1. +- Fix unique constraint error checks. +* Handle symlinks for Docker's data directory and for TMPDIR. +- Add deprecation warnings for flags (-flag is deprecated in favor of --flag) +- Add apparmor profile for the native execution driver. +* Move system specific code from archive to pkg/system. +- Fix duplicate signal for `docker run -i -t` (issue #3336). +- Return correct process pid for lxc. +- Add a -G option to specify the group which unix sockets belong to. ++ Add `-f` flag to `docker rm` to force removal of running containers. ++ Kill ghost containers and restart all ghost containers when the docker daemon restarts. ++ Add `DOCKER_RAMDISK` environment variable to make Docker work when the root is on a ramdisk. + +## 0.8.1 (2014-02-18) + +#### Builder + +- Avoid extra mount/unmount during build. This removes an unneeded mount/unmount operation which was causing problems with devicemapper +- Fix regression with ADD of tar files. This stops Docker from decompressing tarballs added via ADD from the local file system +- Add error to `docker build --rm`. This adds a missing error check to ensure failures to remove containers are detected and reported + +#### Documentation + +* Update issue filing instructions +* Warn against the use of symlinks for Docker's storage folder +* Replace the Firefox example with an IceWeasel example +* Rewrite the PostgresSQL example using a Dockerfile and add more details to it +* Improve the OS X documentation + +#### Remote API + +- Fix broken images API for version less than 1.7 +- Use the right encoding for all API endpoints which return JSON +- Move remote api client to api/ +- Queue calls to the API using generic socket wait + +#### Runtime + +- Fix the use of custom settings for bridges and custom bridges +- Refactor the devicemapper code to avoid many mount/unmount race conditions and failures +- Remove two panics which could make Docker crash in some situations +- Don't ping registry from the CLI client +- Enable skip_block_zeroing for devicemapper. This stops devicemapper from always zeroing entire blocks +- Fix --run in `docker commit`. This makes docker commit store `--run` in the image configuration +- Remove directory when removing devicemapper device. This cleans up leftover mount directories +- Drop NET_ADMIN capability for non-privileged containers. Unprivileged containers can't change their network configuration +- Ensure `docker cp` stream is closed properly +- Avoid extra mount/unmount during container registration. This removes an unneeded mount/unmount operation which was causing problems with devicemapper +- Stop allowing tcp:// as a default tcp bin address which binds to 127.0.0.1:4243 and remove the default port ++ Mount-bind the PTY as container console. This allows tmux and screen to run in a container +- Clean up archive closing. This fixes and improves archive handling +- Fix engine tests on systems where temp directories are symlinked +- Add test methods for save and load +- Avoid temporarily unmounting the container when restarting it. This fixes a race for devicemapper during restart +- Support submodules when building from a GitHub repository +- Quote volume path to allow spaces +- Fix remote tar ADD behavior. This fixes a regression which was causing Docker to extract tarballs + +## 0.8.0 (2014-02-04) + +#### Notable features since 0.7.0 + +* Images and containers can be removed much faster +* Building an image from source with docker build is now much faster +* The Docker daemon starts and stops much faster +* The memory footprint of many common operations has been reduced, by streaming files instead of buffering them in memory, fixing memory leaks, and fixing various suboptimal memory allocations +* Several race conditions were fixed, making Docker more stable under very high concurrency load. This makes Docker more stable and less likely to crash and reduces the memory footprint of many common operations +* All packaging operations are now built on the Go language’s standard tar implementation, which is bundled with Docker itself. This makes packaging more portable across host distributions, and solves several issues caused by quirks and incompatibilities between different distributions of tar +* Docker can now create, remove and modify larger numbers of containers and images graciously thanks to more aggressive releasing of system resources. For example the storage driver API now allows Docker to do reference counting on mounts created by the drivers +With the ongoing changes to the networking and execution subsystems of docker testing these areas have been a focus of the refactoring. By moving these subsystems into separate packages we can test, analyze, and monitor coverage and quality of these packages +* Many components have been separated into smaller sub-packages, each with a dedicated test suite. As a result the code is better-tested, more readable and easier to change + +* The ADD instruction now supports caching, which avoids unnecessarily re-uploading the same source content again and again when it hasn’t changed +* The new ONBUILD instruction adds to your image a “trigger” instruction to be executed at a later time, when the image is used as the base for another build +* Docker now ships with an experimental storage driver which uses the BTRFS filesystem for copy-on-write +* Docker is officially supported on Mac OS X +* The Docker daemon supports systemd socket activation + +## 0.7.6 (2014-01-14) + +#### Builder + +* Do not follow symlink outside of build context + +#### Runtime + +- Remount bind mounts when ro is specified +* Use https for fetching docker version + +#### Other + +* Inline the test.docker.io fingerprint +* Add ca-certificates to packaging documentation + +## 0.7.5 (2014-01-09) + +#### Builder + +* Disable compression for build. More space usage but a much faster upload +- Fix ADD caching for certain paths +- Do not compress archive from git build + +#### Documentation + +- Fix error in GROUP add example +* Make sure the GPG fingerprint is inline in the documentation +* Give more specific advice on setting up signing of commits for DCO + +#### Runtime + +- Fix misspelled container names +- Do not add hostname when networking is disabled +* Return most recent image from the cache by date +- Return all errors from docker wait +* Add Content-Type Header "application/json" to GET /version and /info responses + +#### Other + +* Update DCO to version 1.1 ++ Update Makefile to use "docker:GIT_BRANCH" as the generated image name +* Update Travis to check for new 1.1 DCO version + +## 0.7.4 (2014-01-07) + +#### Builder + +- Fix ADD caching issue with . prefixed path +- Fix docker build on devicemapper by reverting sparse file tar option +- Fix issue with file caching and prevent wrong cache hit +* Use same error handling while unmarshalling CMD and ENTRYPOINT + +#### Documentation + +* Simplify and streamline Amazon Quickstart +* Install instructions use unprefixed Fedora image +* Update instructions for mtu flag for Docker on GCE ++ Add Ubuntu Saucy to installation +- Fix for wrong version warning on master instead of latest + +#### Runtime + +- Only get the image's rootfs when we need to calculate the image size +- Correctly handle unmapping UDP ports +* Make CopyFileWithTar use a pipe instead of a buffer to save memory on docker build +- Fix login message to say pull instead of push +- Fix "docker load" help by removing "SOURCE" prompt and mentioning STDIN +* Make blank -H option default to the same as no -H was sent +* Extract cgroups utilities to own submodule + +#### Other + ++ Add Travis CI configuration to validate DCO and gofmt requirements ++ Add Developer Certificate of Origin Text +* Upgrade VBox Guest Additions +* Check standalone header when pinging a registry server + +## 0.7.3 (2014-01-02) + +#### Builder + ++ Update ADD to use the image cache, based on a hash of the added content +* Add error message for empty Dockerfile + +#### Documentation + +- Fix outdated link to the "Introduction" on www.docker.io ++ Update the docs to get wider when the screen does +- Add information about needing to install LXC when using raw binaries +* Update Fedora documentation to disentangle the docker and docker.io conflict +* Add a note about using the new `-mtu` flag in several GCE zones ++ Add FrugalWare installation instructions ++ Add a more complete example of `docker run` +- Fix API documentation for creating and starting Privileged containers +- Add missing "name" parameter documentation on "/containers/create" +* Add a mention of `lxc-checkconfig` as a way to check for some of the necessary kernel configuration +- Update the 1.8 API documentation with some additions that were added to the docs for 1.7 + +#### Hack + +- Add missing libdevmapper dependency to the packagers documentation +* Update minimum Go requirement to a hard line at Go 1.2+ +* Many minor improvements to the Vagrantfile ++ Add ability to customize dockerinit search locations when compiling (to be used very sparingly only by packagers of platforms who require a nonstandard location) ++ Add coverprofile generation reporting +- Add `-a` to our Go build flags, removing the need for recompiling the stdlib manually +* Update Dockerfile to be more canonical and have less spurious warnings during build +- Fix some miscellaneous `docker pull` progress bar display issues +* Migrate more miscellaneous packages under the "pkg" folder +* Update TextMate highlighting to automatically be enabled for files named "Dockerfile" +* Reorganize syntax highlighting files under a common "contrib/syntax" directory +* Update install.sh script (https://get.docker.io/) to not fail if busybox fails to download or run at the end of the Ubuntu/Debian installation +* Add support for container names in bash completion + +#### Packaging + ++ Add an official Docker client binary for Darwin (Mac OS X) +* Remove empty "Vendor" string and added "License" on deb package ++ Add a stubbed version of "/etc/default/docker" in the deb package + +#### Runtime + +* Update layer application to extract tars in place, avoiding file churn while handling whiteouts +- Fix permissiveness of mtime comparisons in tar handling (since GNU tar and Go tar do not yet support sub-second mtime precision) +* Reimplement `docker top` in pure Go to work more consistently, and even inside Docker-in-Docker (thus removing the shell injection vulnerability present in some versions of `lxc-ps`) ++ Update `-H unix://` to work similarly to `-H tcp://` by inserting the default values for missing portions +- Fix more edge cases regarding dockerinit and deleted or replaced docker or dockerinit files +* Update container name validation to include '.' +- Fix use of a symlink or non-absolute path as the argument to `-g` to work as expected +* Update to handle external mounts outside of LXC, fixing many small mounting quirks and making future execution backends and other features simpler +* Update to use proper box-drawing characters everywhere in `docker images -tree` +* Move MTU setting from LXC configuration to directly use netlink +* Add `-S` option to external tar invocation for more efficient spare file handling ++ Add arch/os info to User-Agent string, especially for registry requests ++ Add `-mtu` option to Docker daemon for configuring MTU +- Fix `docker build` to exit with a non-zero exit code on error ++ Add `DOCKER_HOST` environment variable to configure the client `-H` flag without specifying it manually for every invocation + +## 0.7.2 (2013-12-16) + +#### Runtime + ++ Validate container names on creation with standard regex +* Increase maximum image depth to 127 from 42 +* Continue to move api endpoints to the job api ++ Add -bip flag to allow specification of dynamic bridge IP via CIDR +- Allow bridge creation when ipv6 is not enabled on certain systems +* Set hostname and IP address from within dockerinit +* Drop capabilities from within dockerinit +- Fix volumes on host when symlink is present the image +- Prevent deletion of image if ANY container is depending on it even if the container is not running +* Update docker push to use new progress display +* Use os.Lstat to allow mounting unix sockets when inspecting volumes +- Adjust handling of inactive user login +- Add missing defines in devicemapper for older kernels +- Allow untag operations with no container validation +- Add auth config to docker build + +#### Documentation + +* Add more information about Docker logging ++ Add RHEL documentation +* Add a direct example for changing the CMD that is run in a container +* Update Arch installation documentation ++ Add section on Trusted Builds ++ Add Network documentation page + +#### Other + ++ Add new cover bundle for providing code coverage reporting +* Separate integration tests in bundles +* Make Tianon the hack maintainer +* Update mkimage-debootstrap with more tweaks for keeping images small +* Use https to get the install script +* Remove vendored dotcloud/tar now that Go 1.2 has been released + +## 0.7.1 (2013-12-05) + +#### Documentation + ++ Add @SvenDowideit as documentation maintainer ++ Add links example ++ Add documentation regarding ambassador pattern ++ Add Google Cloud Platform docs ++ Add dockerfile best practices +* Update doc for RHEL +* Update doc for registry +* Update Postgres examples +* Update doc for Ubuntu install +* Improve remote api doc + +#### Runtime + ++ Add hostconfig to docker inspect ++ Implement `docker log -f` to stream logs ++ Add env variable to disable kernel version warning ++ Add -format to `docker inspect` ++ Support bind-mount for files +- Fix bridge creation on RHEL +- Fix image size calculation +- Make sure iptables are called even if the bridge already exists +- Fix issue with stderr only attach +- Remove init layer when destroying a container +- Fix same port binding on different interfaces +- `docker build` now returns the correct exit code +- Fix `docker port` to display correct port +- `docker build` now check that the dockerfile exists client side +- `docker attach` now returns the correct exit code +- Remove the name entry when the container does not exist + +#### Registry + +* Improve progress bars, add ETA for downloads +* Simultaneous pulls now waits for the first to finish instead of failing +- Tag only the top-layer image when pushing to registry +- Fix issue with offline image transfer +- Fix issue preventing using ':' in password for registry + +#### Other + ++ Add pprof handler for debug ++ Create a Makefile +* Use stdlib tar that now includes fix +* Improve make.sh test script +* Handle SIGQUIT on the daemon +* Disable verbose during tests +* Upgrade to go1.2 for official build +* Improve unit tests +* The test suite now runs all tests even if one fails +* Refactor C in Go (Devmapper) +- Fix OS X compilation + +## 0.7.0 (2013-11-25) + +#### Notable features since 0.6.0 + +* Storage drivers: choose from aufs, device-mapper, or vfs. +* Standard Linux support: docker now runs on unmodified Linux kernels and all major distributions. +* Links: compose complex software stacks by connecting containers to each other. +* Container naming: organize your containers by giving them memorable names. +* Advanced port redirects: specify port redirects per interface, or keep sensitive ports private. +* Offline transfer: push and pull images to the filesystem without losing information. +* Quality: numerous bugfixes and small usability improvements. Significant increase in test coverage. + +## 0.6.7 (2013-11-21) + +#### Runtime + +* Improve stability, fixes some race conditions +* Skip the volumes mounted when deleting the volumes of container. +* Fix layer size computation: handle hard links correctly +* Use the work Path for docker cp CONTAINER:PATH +* Fix tmp dir never cleanup +* Speedup docker ps +* More informative error message on name collisions +* Fix nameserver regex +* Always return long id's +* Fix container restart race condition +* Keep published ports on docker stop;docker start +* Fix container networking on Fedora +* Correctly express "any address" to iptables +* Fix network setup when reconnecting to ghost container +* Prevent deletion if image is used by a running container +* Lock around read operations in graph + +#### RemoteAPI + +* Return full ID on docker rmi + +#### Client + ++ Add -tree option to images ++ Offline image transfer +* Exit with status 2 on usage error and display usage on stderr +* Do not forward SIGCHLD to container +* Use string timestamp for docker events -since + +#### Other + +* Update to go 1.2rc5 ++ Add /etc/default/docker support to upstart + +## 0.6.6 (2013-11-06) + +#### Runtime + +* Ensure container name on register +* Fix regression in /etc/hosts ++ Add lock around write operations in graph +* Check if port is valid +* Fix restart runtime error with ghost container networking ++ Add some more colors and animals to increase the pool of generated names +* Fix issues in docker inspect ++ Escape apparmor confinement ++ Set environment variables using a file. +* Prevent docker insert to erase something ++ Prevent DNS server conflicts in CreateBridgeIface ++ Validate bind mounts on the server side ++ Use parent image config in docker build +* Fix regression in /etc/hosts + +#### Client + ++ Add -P flag to publish all exposed ports ++ Add -notrunc and -q flags to docker history +* Fix docker commit, tag and import usage ++ Add stars, trusted builds and library flags in docker search +* Fix docker logs with tty + +#### RemoteAPI + +* Make /events API send headers immediately +* Do not split last column docker top ++ Add size to history + +#### Other + ++ Contrib: Desktop integration. Firefox usecase. ++ Dockerfile: bump to go1.2rc3 + +## 0.6.5 (2013-10-29) + +#### Runtime + ++ Containers can now be named ++ Containers can now be linked together for service discovery ++ 'run -a', 'start -a' and 'attach' can forward signals to the container for better integration with process supervisors ++ Automatically start crashed containers after a reboot ++ Expose IP, port, and proto as separate environment vars for container links +* Allow ports to be published to specific ips +* Prohibit inter-container communication by default +- Ignore ErrClosedPipe for stdin in Container.Attach +- Remove unused field kernelVersion +* Fix issue when mounting subdirectories of /mnt in container +- Fix untag during removal of images +* Check return value of syscall.Chdir when changing working directory inside dockerinit + +#### Client + +- Only pass stdin to hijack when needed to avoid closed pipe errors +* Use less reflection in command-line method invocation +- Monitor the tty size after starting the container, not prior +- Remove useless os.Exit() calls after log.Fatal + +#### Hack + ++ Add initial init scripts library and a safer Ubuntu packaging script that works for Debian +* Add -p option to invoke debootstrap with http_proxy +- Update install.sh with $sh_c to get sudo/su for modprobe +* Update all the mkimage scripts to use --numeric-owner as a tar argument +* Update hack/release.sh process to automatically invoke hack/make.sh and bail on build and test issues + +#### Other + +* Documentation: Fix the flags for nc in example +* Testing: Remove warnings and prevent mount issues +- Testing: Change logic for tty resize to avoid warning in tests +- Builder: Fix race condition in docker build with verbose output +- Registry: Fix content-type for PushImageJSONIndex method +* Contrib: Improve helper tools to generate debian and Arch linux server images + +## 0.6.4 (2013-10-16) + +#### Runtime + +- Add cleanup of container when Start() fails +* Add better comments to utils/stdcopy.go +* Add utils.Errorf for error logging ++ Add -rm to docker run for removing a container on exit +- Remove error messages which are not actually errors +- Fix `docker rm` with volumes +- Fix some error cases where a HTTP body might not be closed +- Fix panic with wrong dockercfg file +- Fix the attach behavior with -i +* Record termination time in state. +- Use empty string so TempDir uses the OS's temp dir automatically +- Make sure to close the network allocators ++ Autorestart containers by default +* Bump vendor kr/pty to commit 3b1f6487b `(syscall.O_NOCTTY)` +* lxc: Allow set_file_cap capability in container +- Move run -rm to the cli only +* Split stdout stderr +* Always create a new session for the container + +#### Testing + +- Add aggregated docker-ci email report +- Add cleanup to remove leftover containers +* Add nightly release to docker-ci +* Add more tests around auth.ResolveAuthConfig +- Remove a few errors in tests +- Catch errClosing error when TCP and UDP proxies are terminated +* Only run certain tests with TESTFLAGS='-run TestName' make.sh +* Prevent docker-ci to test closing PRs +* Replace panic by log.Fatal in tests +- Increase TestRunDetach timeout + +#### Documentation + +* Add initial draft of the Docker infrastructure doc +* Add devenvironment link to CONTRIBUTING.md +* Add `apt-get install curl` to Ubuntu docs +* Add explanation for export restrictions +* Add .dockercfg doc +* Remove Gentoo install notes about #1422 workaround +* Fix help text for -v option +* Fix Ping endpoint documentation +- Fix parameter names in docs for ADD command +- Fix ironic typo in changelog +* Various command fixes in postgres example +* Document how to edit and release docs +- Minor updates to `postgresql_service.rst` +* Clarify LGTM process to contributors +- Corrected error in the package name +* Document what `vagrant up` is actually doing ++ improve doc search results +* Cleanup whitespace in API 1.5 docs +* use angle brackets in MAINTAINER example email +* Update archlinux.rst ++ Changes to a new style for the docs. Includes version switcher. +* Formatting, add information about multiline json +* Improve registry and index REST API documentation +- Replace deprecated upgrading reference to docker-latest.tgz, which hasn't been updated since 0.5.3 +* Update Gentoo installation documentation now that we're in the portage tree proper +* Cleanup and reorganize docs and tooling for contributors and maintainers +- Minor spelling correction of protocoll -> protocol + +#### Contrib + +* Add vim syntax highlighting for Dockerfiles from @honza +* Add mkimage-arch.sh +* Reorganize contributed completion scripts to add zsh completion + +#### Hack + +* Add vagrant user to the docker group +* Add proper bash completion for "docker push" +* Add xz utils as a runtime dep +* Add cleanup/refactor portion of #2010 for hack and Dockerfile updates ++ Add contrib/mkimage-centos.sh back (from #1621), and associated documentation link +* Add several of the small make.sh fixes from #1920, and make the output more consistent and contributor-friendly ++ Add @tianon to hack/MAINTAINERS +* Improve network performance for VirtualBox +* Revamp install.sh to be usable by more people, and to use official install methods whenever possible (apt repo, portage tree, etc.) +- Fix contrib/mkimage-debian.sh apt caching prevention ++ Add Dockerfile.tmLanguage to contrib +* Configured FPM to make /etc/init/docker.conf a config file +* Enable SSH Agent forwarding in Vagrant VM +* Several small tweaks/fixes for contrib/mkimage-debian.sh + +#### Other + +- Builder: Abort build if mergeConfig returns an error and fix duplicate error message +- Packaging: Remove deprecated packaging directory +- Registry: Use correct auth config when logging in. +- Registry: Fix the error message so it is the same as the regex + +## 0.6.3 (2013-09-23) + +#### Packaging + +* Add 'docker' group on install for ubuntu package +* Update tar vendor dependency +* Download apt key over HTTPS + +#### Runtime + +- Only copy and change permissions on non-bindmount volumes +* Allow multiple volumes-from +- Fix HTTP imports from STDIN + +#### Documentation + +* Update section on extracting the docker binary after build +* Update development environment docs for new build process +* Remove 'base' image from documentation + +#### Other + +- Client: Fix detach issue +- Registry: Update regular expression to match index + +## 0.6.2 (2013-09-17) + +#### Runtime + ++ Add domainname support ++ Implement image filtering with path.Match +* Remove unnecessary warnings +* Remove os/user dependency +* Only mount the hostname file when the config exists +* Handle signals within the `docker login` command +- UID and GID are now also applied to volumes +- `docker start` set error code upon error +- `docker run` set the same error code as the process started + +#### Builder + ++ Add -rm option in order to remove intermediate containers +* Allow multiline for the RUN instruction + +#### Registry + +* Implement login with private registry +- Fix push issues + +#### Other + ++ Hack: Vendor all dependencies +* Remote API: Bump to v1.5 +* Packaging: Break down hack/make.sh into small scripts, one per 'bundle': test, binary, ubuntu etc. +* Documentation: General improvements + +## 0.6.1 (2013-08-23) + +#### Registry + +* Pass "meta" headers in API calls to the registry + +#### Packaging + +- Use correct upstart script with new build tool +- Use libffi-dev, don`t build it from sources +- Remove duplicate mercurial install command + +## 0.6.0 (2013-08-22) + +#### Runtime + ++ Add lxc-conf flag to allow custom lxc options ++ Add an option to set the working directory +* Add Image name to LogEvent tests ++ Add -privileged flag and relevant tests, docs, and examples +* Add websocket support to /container//attach/ws +* Add warning when net.ipv4.ip_forwarding = 0 +* Add hostname to environment +* Add last stable version in `docker version` +- Fix race conditions in parallel pull +- Fix Graph ByParent() to generate list of child images per parent image. +- Fix typo: fmt.Sprint -> fmt.Sprintf +- Fix small \n error un docker build +* Fix to "Inject dockerinit at /.dockerinit" +* Fix #910. print user name to docker info output +* Use Go 1.1.2 for dockerbuilder +* Use ranged for loop on channels +- Use utils.ParseRepositoryTag instead of strings.Split(name, ":") in server.ImageDelete +- Improve CMD, ENTRYPOINT, and attach docs. +- Improve connect message with socket error +- Load authConfig only when needed and fix useless WARNING +- Show tag used when image is missing +* Apply volumes-from before creating volumes +- Make docker run handle SIGINT/SIGTERM +- Prevent crash when .dockercfg not readable +- Install script should be fetched over https, not http. +* API, issue 1471: Use groups for socket permissions +- Correctly detect IPv4 forwarding +* Mount /dev/shm as a tmpfs +- Switch from http to https for get.docker.io +* Let userland proxy handle container-bound traffic +* Update the Docker CLI to specify a value for the "Host" header. +- Change network range to avoid conflict with EC2 DNS +- Reduce connect and read timeout when pinging the registry +* Parallel pull +- Handle ip route showing mask-less IP addresses +* Allow ENTRYPOINT without CMD +- Always consider localhost as a domain name when parsing the FQN repos name +* Refactor checksum + +#### Documentation + +* Add MongoDB image example +* Add instructions for creating and using the docker group +* Add sudo to examples and installation to documentation +* Add ufw doc +* Add a reference to ps -a +* Add information about Docker`s high level tools over LXC. +* Fix typo in docs for docker run -dns +* Fix a typo in the ubuntu installation guide +* Fix to docs regarding adding docker groups +* Update default -H docs +* Update readme with dependencies for building +* Update amazon.rst to explain that Vagrant is not necessary for running Docker on ec2 +* PostgreSQL service example in documentation +* Suggest installing linux-headers by default. +* Change the twitter handle +* Clarify Amazon EC2 installation +* 'Base' image is deprecated and should no longer be referenced in the docs. +* Move note about officially supported kernel +- Solved the logo being squished in Safari + +#### Builder + ++ Add USER instruction do Dockerfile ++ Add workdir support for the Buildfile +* Add no cache for docker build +- Fix docker build and docker events output +- Only count known instructions as build steps +- Make sure ENV instruction within build perform a commit each time +- Forbid certain paths within docker build ADD +- Repository name (and optionally a tag) in build usage +- Make sure ADD will create everything in 0755 + +#### Remote API + +* Sort Images by most recent creation date. +* Reworking opaque requests in registry module +* Add image name in /events +* Use mime pkg to parse Content-Type +* 650 http utils and user agent field + +#### Hack + ++ Bash Completion: Limit commands to containers of a relevant state +* Add docker dependencies coverage testing into docker-ci + +#### Packaging + ++ Docker-brew 0.5.2 support and memory footprint reduction +* Add new docker dependencies into docker-ci +- Revert "docker.upstart: avoid spawning a `sh` process" ++ Docker-brew and Docker standard library ++ Release docker with docker +* Fix the upstart script generated by get.docker.io +* Enabled the docs to generate manpages. +* Revert Bind daemon to 0.0.0.0 in Vagrant. + +#### Register + +* Improve auth push +* Registry unit tests + mock registry + +#### Tests + +* Improve TestKillDifferentUser to prevent timeout on buildbot +- Fix typo in TestBindMounts (runContainer called without image) +* Improve TestGetContainersTop so it does not rely on sleep +* Relax the lo interface test to allow iface index != 1 +* Add registry functional test to docker-ci +* Add some tests in server and utils + +#### Other + +* Contrib: bash completion script +* Client: Add docker cp command and copy api endpoint to copy container files/folders to the host +* Don`t read from stdout when only attached to stdin + +## 0.5.3 (2013-08-13) + +#### Runtime + +* Use docker group for socket permissions +- Spawn shell within upstart script +- Handle ip route showing mask-less IP addresses +- Add hostname to environment + +#### Builder + +- Make sure ENV instruction within build perform a commit each time + +## 0.5.2 (2013-08-08) + +* Builder: Forbid certain paths within docker build ADD +- Runtime: Change network range to avoid conflict with EC2 DNS +* API: Change daemon to listen on unix socket by default + +## 0.5.1 (2013-07-30) + +#### Runtime + ++ Add `ps` args to `docker top` ++ Add support for container ID files (pidfile like) ++ Add container=lxc in default env ++ Support networkless containers with `docker run -n` and `docker -d -b=none` +* Stdout/stderr logs are now stored in the same file as JSON +* Allocate a /16 IP range by default, with fallback to /24. Try 12 ranges instead of 3. +* Change .dockercfg format to json and support multiple auth remote +- Do not override volumes from config +- Fix issue with EXPOSE override + +#### API + ++ Docker client now sets useragent (RFC 2616) ++ Add /events endpoint + +#### Builder + ++ ADD command now understands URLs ++ CmdAdd and CmdEnv now respect Dockerfile-set ENV variables +- Create directories with 755 instead of 700 within ADD instruction + +#### Hack + +* Simplify unit tests with helpers +* Improve docker.upstart event +* Add coverage testing into docker-ci + +## 0.5.0 (2013-07-17) + +#### Runtime + ++ List all processes running inside a container with 'docker top' ++ Host directories can be mounted as volumes with 'docker run -v' ++ Containers can expose public UDP ports (eg, '-p 123/udp') ++ Optionally specify an exact public port (eg. '-p 80:4500') +* 'docker login' supports additional options +- Don't save a container`s hostname when committing an image. + +#### Registry + ++ New image naming scheme inspired by Go packaging convention allows arbitrary combinations of registries +- Fix issues when uploading images to a private registry + +#### Builder + ++ ENTRYPOINT instruction sets a default binary entry point to a container ++ VOLUME instruction marks a part of the container as persistent data +* 'docker build' displays the full output of a build by default + +## 0.4.8 (2013-07-01) + ++ Builder: New build operation ENTRYPOINT adds an executable entry point to the container. - Runtime: Fix a bug which caused 'docker run -d' to no longer print the container ID. +- Tests: Fix issues in the test suite + +## 0.4.7 (2013-06-28) + +#### Remote API + +* The progress bar updates faster when downloading and uploading large files +- Fix a bug in the optional unix socket transport + +#### Runtime + +* Improve detection of kernel version ++ Host directories can be mounted as volumes with 'docker run -b' +- fix an issue when only attaching to stdin +* Use 'tar --numeric-owner' to avoid uid mismatch across multiple hosts + +#### Hack + +* Improve test suite and dev environment +* Remove dependency on unit tests on 'os/user' + +#### Other + +* Registry: easier push/pull to a custom registry ++ Documentation: add terminology section + +## 0.4.6 (2013-06-22) + +- Runtime: fix a bug which caused creation of empty images (and volumes) to crash. + +## 0.4.5 (2013-06-21) + ++ Builder: 'docker build git://URL' fetches and builds a remote git repository +* Runtime: 'docker ps -s' optionally prints container size +* Tests: improved and simplified +- Runtime: fix a regression introduced in 0.4.3 which caused the logs command to fail. +- Builder: fix a regression when using ADD with single regular file. + +## 0.4.4 (2013-06-19) + +- Builder: fix a regression introduced in 0.4.3 which caused builds to fail on new clients. + +## 0.4.3 (2013-06-19) + +#### Builder + ++ ADD of a local file will detect tar archives and unpack them +* ADD improvements: use tar for copy + automatically unpack local archives +* ADD uses tar/untar for copies instead of calling 'cp -ar' +* Fix the behavior of ADD to be (mostly) reverse-compatible, predictable and well-documented. +- Fix a bug which caused builds to fail if ADD was the first command +* Nicer output for 'docker build' + +#### Runtime + +* Remove bsdtar dependency +* Add unix socket and multiple -H support +* Prevent rm of running containers +* Use go1.1 cookiejar +- Fix issue detaching from running TTY container +- Forbid parallel push/pull for a single image/repo. Fixes #311 +- Fix race condition within Run command when attaching. + +#### Client + +* HumanReadable ProgressBar sizes in pull +* Fix docker version`s git commit output + +#### API + +* Send all tags on History API call +* Add tag lookup to history command. Fixes #882 + +#### Documentation + +- Fix missing command in irc bouncer example + +## 0.4.2 (2013-06-17) + +- Packaging: Bumped version to work around an Ubuntu bug + +## 0.4.1 (2013-06-17) + +#### Remote Api + ++ Add flag to enable cross domain requests ++ Add images and containers sizes in docker ps and docker images + +#### Runtime + ++ Configure dns configuration host-wide with 'docker -d -dns' ++ Detect faulty DNS configuration and replace it with a public default ++ Allow docker run : ++ You can now specify public port (ex: -p 80:4500) +* Improve image removal to garbage-collect unreferenced parents + +#### Client + +* Allow multiple params in inspect +* Print the container id before the hijack in `docker run` + +#### Registry + +* Add regexp check on repo`s name +* Move auth to the client +- Remove login check on pull + +#### Other + +* Vagrantfile: Add the rest api port to vagrantfile`s port_forward +* Upgrade to Go 1.1 +- Builder: don`t ignore last line in Dockerfile when it doesn`t end with \n + +## 0.4.0 (2013-06-03) + +#### Builder + ++ Introducing Builder ++ 'docker build' builds a container, layer by layer, from a source repository containing a Dockerfile + +#### Remote API + ++ Introducing Remote API ++ control Docker programmatically using a simple HTTP/json API + +#### Runtime + +* Various reliability and usability improvements + +## 0.3.4 (2013-05-30) + +#### Builder + ++ 'docker build' builds a container, layer by layer, from a source repository containing a Dockerfile ++ 'docker build -t FOO' applies the tag FOO to the newly built container. + +#### Runtime + ++ Interactive TTYs correctly handle window resize +* Fix how configuration is merged between layers + +#### Remote API + ++ Split stdout and stderr on 'docker run' ++ Optionally listen on a different IP and port (use at your own risk) + +#### Documentation + +* Improve install instructions. + +## 0.3.3 (2013-05-23) + +- Registry: Fix push regression +- Various bugfixes + +## 0.3.2 (2013-05-09) + +#### Registry + +* Improve the checksum process +* Use the size to have a good progress bar while pushing +* Use the actual archive if it exists in order to speed up the push +- Fix error 400 on push + +#### Runtime + +* Store the actual archive on commit + +## 0.3.1 (2013-05-08) + +#### Builder + ++ Implement the autorun capability within docker builder ++ Add caching to docker builder ++ Add support for docker builder with native API as top level command ++ Implement ENV within docker builder +- Check the command existence prior create and add Unit tests for the case +* use any whitespaces instead of tabs + +#### Runtime + ++ Add go version to debug infos +* Kernel version - don`t show the dash if flavor is empty + +#### Registry + ++ Add docker search top level command in order to search a repository +- Fix pull for official images with specific tag +- Fix issue when login in with a different user and trying to push +* Improve checksum - async calculation + +#### Images + ++ Output graph of images to dot (graphviz) +- Fix ByParent function + +#### Documentation + ++ New introduction and high-level overview ++ Add the documentation for docker builder +- CSS fix for docker documentation to make REST API docs look better. +- Fix CouchDB example page header mistake +- Fix README formatting +* Update www.docker.io website. + +#### Other + ++ Website: new high-level overview +- Makefile: Swap "go get" for "go get -d", especially to compile on go1.1rc +* Packaging: packaging ubuntu; issue #510: Use goland-stable PPA package to build docker + +## 0.3.0 (2013-05-06) + +#### Runtime + +- Fix the command existence check +- strings.Split may return an empty string on no match +- Fix an index out of range crash if cgroup memory is not + +#### Documentation + +* Various improvements ++ New example: sharing data between 2 couchdb databases + +#### Other + +* Vagrant: Use only one deb line in /etc/apt ++ Registry: Implement the new registry + +## 0.2.2 (2013-05-03) + ++ Support for data volumes ('docker run -v=PATH') ++ Share data volumes between containers ('docker run -volumes-from') ++ Improve documentation +* Upgrade to Go 1.0.3 +* Various upgrades to the dev environment for contributors + +## 0.2.1 (2013-05-01) + ++ 'docker commit -run' bundles a layer with default runtime options: command, ports etc. +* Improve install process on Vagrant ++ New Dockerfile operation: "maintainer" ++ New Dockerfile operation: "expose" ++ New Dockerfile operation: "cmd" ++ Contrib script to build a Debian base layer ++ 'docker -d -r': restart crashed containers at daemon startup +* Runtime: improve test coverage + +## 0.2.0 (2013-04-23) + +- Runtime: ghost containers can be killed and waited for +* Documentation: update install instructions +- Packaging: fix Vagrantfile +- Development: automate releasing binaries and ubuntu packages ++ Add a changelog +- Various bugfixes + +## 0.1.8 (2013-04-22) + +- Dynamically detect cgroup capabilities +- Issue stability warning on kernels <3.8 +- 'docker push' buffers on disk instead of memory +- Fix 'docker diff' for removed files +- Fix 'docker stop' for ghost containers +- Fix handling of pidfile +- Various bugfixes and stability improvements + +## 0.1.7 (2013-04-18) + +- Container ports are available on localhost +- 'docker ps' shows allocated TCP ports +- Contributors can run 'make hack' to start a continuous integration VM +- Streamline ubuntu packaging & uploading +- Various bugfixes and stability improvements + +## 0.1.6 (2013-04-17) + +- Record the author an image with 'docker commit -author' + +## 0.1.5 (2013-04-17) + +- Disable standalone mode +- Use a custom DNS resolver with 'docker -d -dns' +- Detect ghost containers +- Improve diagnosis of missing system capabilities +- Allow disabling memory limits at compile time +- Add debian packaging +- Documentation: installing on Arch Linux +- Documentation: running Redis on docker +- Fix lxc 0.9 compatibility +- Automatically load aufs module +- Various bugfixes and stability improvements + +## 0.1.4 (2013-04-09) + +- Full support for TTY emulation +- Detach from a TTY session with the escape sequence `C-p C-q` +- Various bugfixes and stability improvements +- Minor UI improvements +- Automatically create our own bridge interface 'docker0' + +## 0.1.3 (2013-04-04) + +- Choose TCP frontend port with '-p :PORT' +- Layer format is versioned +- Major reliability improvements to the process manager +- Various bugfixes and stability improvements + +## 0.1.2 (2013-04-03) + +- Set container hostname with 'docker run -h' +- Selective attach at run with 'docker run -a [stdin[,stdout[,stderr]]]' +- Various bugfixes and stability improvements +- UI polish +- Progress bar on push/pull +- Use XZ compression by default +- Make IP allocator lazy + +## 0.1.1 (2013-03-31) + +- Display shorthand IDs for convenience +- Stabilize process management +- Layers can include a commit message +- Simplified 'docker attach' +- Fix support for re-attaching +- Various bugfixes and stability improvements +- Auto-download at run +- Auto-login on push +- Beefed up documentation + +## 0.1.0 (2013-03-23) + +Initial public release + +- Implement registry in order to push/pull images +- TCP port allocation +- Fix termcaps on Linux +- Add documentation +- Add Vagrant support with Vagrantfile +- Add unit tests +- Add repository/tags to ease image management +- Improve the layer implementation diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..6b875d69 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,436 @@ +# Contributing to Docker + +Want to hack on Docker? Awesome! We have a contributor's guide that explains +[setting up a Docker development environment and the contribution +process](https://docs.docker.com/opensource/project/who-written-for/). + +![Contributors guide](docs/static_files/contributors.png) + +This page contains information about reporting issues as well as some tips and +guidelines useful to experienced open source contributors. Finally, make sure +you read our [community guidelines](#docker-community-guidelines) before you +start participating. + +## Topics + +* [Reporting Security Issues](#reporting-security-issues) +* [Design and Cleanup Proposals](#design-and-cleanup-proposals) +* [Reporting Issues](#reporting-other-issues) +* [Quick Contribution Tips and Guidelines](#quick-contribution-tips-and-guidelines) +* [Community Guidelines](#docker-community-guidelines) + +## Reporting security issues + +The Docker maintainers take security seriously. If you discover a security +issue, please bring it to their attention right away! + +Please **DO NOT** file a public issue, instead send your report privately to +[security@docker.com](mailto:security@docker.com). + +Security reports are greatly appreciated and we will publicly thank you for it. +We also like to send gifts—if you're into Docker schwag, make sure to let +us know. We currently do not offer a paid security bounty program, but are not +ruling it out in the future. + + +## Reporting other issues + +A great way to contribute to the project is to send a detailed report when you +encounter an issue. We always appreciate a well-written, thorough bug report, +and will thank you for it! + +Check that [our issue database](https://github.com/docker/docker/issues) +doesn't already include that problem or suggestion before submitting an issue. +If you find a match, you can use the "subscribe" button to get notified on +updates. Do *not* leave random "+1" or "I have this too" comments, as they +only clutter the discussion, and don't help resolving it. However, if you +have ways to reproduce the issue or have additional information that may help +resolving the issue, please leave a comment. + +When reporting issues, always include: + +* The output of `docker version`. +* The output of `docker info`. + +Also include the steps required to reproduce the problem if possible and +applicable. This information will help us review and fix your issue faster. +When sending lengthy log-files, consider posting them as a gist (https://gist.github.com). +Don't forget to remove sensitive data from your logfiles before posting (you can +replace those parts with "REDACTED"). + +**Issue Report Template**: + +``` +Description of problem: + + +`docker version`: + + +`docker info`: + + +`uname -a`: + + +Environment details (AWS, VirtualBox, physical, etc.): + + +How reproducible: + + +Steps to Reproduce: +1. +2. +3. + + +Actual Results: + + +Expected Results: + + +Additional info: + + + +``` + + +##Quick contribution tips and guidelines + +This section gives the experienced contributor some tips and guidelines. + +###Pull requests are always welcome + +Not sure if that typo is worth a pull request? Found a bug and know how to fix +it? Do it! We will appreciate it. Any significant improvement should be +documented as [a GitHub issue](https://github.com/docker/docker/issues) before +anybody starts working on it. + +We are always thrilled to receive pull requests. We do our best to process them +quickly. If your pull request is not accepted on the first try, +don't get discouraged! Our contributor's guide explains [the review process we +use for simple changes](https://docs.docker.com/opensource/workflow/make-a-contribution/). + +### Design and cleanup proposals + +You can propose new designs for existing Docker features. You can also design +entirely new features. We really appreciate contributors who want to refactor or +otherwise cleanup our project. For information on making these types of +contributions, see [the advanced contribution +section](https://docs.docker.com/opensource/workflow/advanced-contributing/) in +the contributors guide. + +We try hard to keep Docker lean and focused. Docker can't do everything for +everybody. This means that we might decide against incorporating a new feature. +However, there might be a way to implement that feature *on top of* Docker. + +### Talking to other Docker users and contributors + + + + + + + + + + + + + + + + + + + + +
Internet Relay Chat (IRC) +

+ IRC a direct line to our most knowledgeable Docker users; we have + both the #docker and #docker-dev group on + irc.freenode.net. + IRC is a rich chat protocol but it can overwhelm new users. You can search + our chat archives. +

+ Read our IRC quickstart guide for an easy way to get started. +
Google Groups + There are two groups. + Docker-user + is for people using Docker containers. + The docker-dev + group is for contributors and other people contributing to the Docker + project. + You can join them without an google account by sending an email to e.g. "docker-user+subscribe@googlegroups.com". + After receiving the join-request message, you can simply reply to that to confirm the subscribtion. +
Twitter + You can follow Docker's Twitter feed + to get updates on our products. You can also tweet us questions or just + share blogs or stories. +
Stack Overflow + Stack Overflow has over 17000 Docker questions listed. We regularly + monitor Docker questions + and so do many other knowledgeable Docker users. +
+ + +### Conventions + +Fork the repository and make changes on your fork in a feature branch: + +- If it's a bug fix branch, name it XXXX-something where XXXX is the number of + the issue. +- If it's a feature branch, create an enhancement issue to announce + your intentions, and name it XXXX-something where XXXX is the number of the + issue. + +Submit unit tests for your changes. Go has a great test framework built in; use +it! Take a look at existing tests for inspiration. [Run the full test +suite](https://docs.docker.com/opensource/project/test-and-docs/) on your branch before +submitting a pull request. + +Update the documentation when creating or modifying features. Test your +documentation changes for clarity, concision, and correctness, as well as a +clean documentation build. See our contributors guide for [our style +guide](https://docs.docker.com/opensource/doc-style) and instructions on [building +the documentation](https://docs.docker.com/opensource/project/test-and-docs/#build-and-test-the-documentation). + +Write clean code. Universally formatted code promotes ease of writing, reading, +and maintenance. Always run `gofmt -s -w file.go` on each changed file before +committing your changes. Most editors have plug-ins that do this automatically. + +Pull request descriptions should be as clear as possible and include a reference +to all the issues that they address. + +Commit messages must start with a capitalized and short summary (max. 50 chars) +written in the imperative, followed by an optional, more detailed explanatory +text which is separated from the summary by an empty line. + +Code review comments may be added to your pull request. Discuss, then make the +suggested modifications and push additional commits to your feature branch. Post +a comment after pushing. New commits show up in the pull request automatically, +but the reviewers are notified only when you comment. + +Pull requests must be cleanly rebased on top of master without multiple branches +mixed into the PR. + +**Git tip**: If your PR no longer merges cleanly, use `rebase master` in your +feature branch to update your pull request rather than `merge master`. + +Before you make a pull request, squash your commits into logical units of work +using `git rebase -i` and `git push -f`. A logical unit of work is a consistent +set of patches that should be reviewed together: for example, upgrading the +version of a vendored dependency and taking advantage of its now available new +feature constitute two separate units of work. Implementing a new function and +calling it in another file constitute a single logical unit of work. The very +high majority of submissions should have a single commit, so if in doubt: squash +down to one. + +After every commit, [make sure the test suite passes] +(https://docs.docker.com/opensource/project/test-and-docs/). Include documentation +changes in the same pull request so that a revert would remove all traces of +the feature or fix. + +Include an issue reference like `Closes #XXXX` or `Fixes #XXXX` in commits that +close an issue. Including references automatically closes the issue on a merge. + +Please do not add yourself to the `AUTHORS` file, as it is regenerated regularly +from the Git history. + +Please see the [Coding Style](#coding-style) for further guidelines. + +### Merge approval + +Docker maintainers use LGTM (Looks Good To Me) in comments on the code review to +indicate acceptance. + +A change requires LGTMs from an absolute majority of the maintainers of each +component affected. For example, if a change affects `docs/` and `registry/`, it +needs an absolute majority from the maintainers of `docs/` AND, separately, an +absolute majority of the maintainers of `registry/`. + +For more details, see the [MAINTAINERS](MAINTAINERS) page. + +### Sign your work + +The sign-off is a simple line at the end of the explanation for the patch. Your +signature certifies that you wrote the patch or otherwise have the right to pass +it on as an open-source patch. The rules are pretty simple: if you can certify +the below (from [developercertificate.org](http://developercertificate.org/)): + +``` +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. +660 York Street, Suite 102, +San Francisco, CA 94110 USA + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. +``` + +Then you just add a line to every git commit message: + + Signed-off-by: Joe Smith + +Use your real name (sorry, no pseudonyms or anonymous contributions.) + +If you set your `user.name` and `user.email` git configs, you can sign your +commit automatically with `git commit -s`. + +Note that the old-style `Docker-DCO-1.1-Signed-off-by: ...` format is still +accepted, so there is no need to update outstanding pull requests to the new +format right away, but please do adjust your processes for future contributions. + +### How can I become a maintainer? + +The procedures for adding new maintainers are explained in the +global [MAINTAINERS](https://github.com/docker/opensource/blob/master/MAINTAINERS) +file in the [https://github.com/docker/opensource/](https://github.com/docker/opensource/) +repository. + +Don't forget: being a maintainer is a time investment. Make sure you +will have time to make yourself available. You don't have to be a +maintainer to make a difference on the project! + +## Docker community guidelines + +We want to keep the Docker community awesome, growing and collaborative. We need +your help to keep it that way. To help with this we've come up with some general +guidelines for the community as a whole: + +* Be nice: Be courteous, respectful and polite to fellow community members: + no regional, racial, gender, or other abuse will be tolerated. We like + nice people way better than mean ones! + +* Encourage diversity and participation: Make everyone in our community feel + welcome, regardless of their background and the extent of their + contributions, and do everything possible to encourage participation in + our community. + +* Keep it legal: Basically, don't get us in trouble. Share only content that + you own, do not share private or sensitive information, and don't break + the law. + +* Stay on topic: Make sure that you are posting to the correct channel and + avoid off-topic discussions. Remember when you update an issue or respond + to an email you are potentially sending to a large number of people. Please + consider this before you update. Also remember that nobody likes spam. + +* Don't send email to the maintainers: There's no need to send email to the + maintainers to ask them to investigate an issue or to take a look at a + pull request. Instead of sending an email, GitHub mentions should be + used to ping maintainers to review a pull request, a proposal or an + issue. + +### Guideline violations — 3 strikes method + +The point of this section is not to find opportunities to punish people, but we +do need a fair way to deal with people who are making our community suck. + +1. First occurrence: We'll give you a friendly, but public reminder that the + behavior is inappropriate according to our guidelines. + +2. Second occurrence: We will send you a private message with a warning that + any additional violations will result in removal from the community. + +3. Third occurrence: Depending on the violation, we may need to delete or ban + your account. + +**Notes:** + +* Obvious spammers are banned on first occurrence. If we don't do this, we'll + have spam all over the place. + +* Violations are forgiven after 6 months of good behavior, and we won't hold a + grudge. + +* People who commit minor infractions will get some education, rather than + hammering them in the 3 strikes process. + +* The rules apply equally to everyone in the community, no matter how much + you've contributed. + +* Extreme violations of a threatening, abusive, destructive or illegal nature + will be addressed immediately and are not subject to 3 strikes or forgiveness. + +* Contact abuse@docker.com to report abuse or appeal violations. In the case of + appeals, we know that mistakes happen, and we'll work with you to come up with a + fair solution if there has been a misunderstanding. + +## Coding Style + +Unless explicitly stated, we follow all coding guidelines from the Go +community. While some of these standards may seem arbitrary, they somehow seem +to result in a solid, consistent codebase. + +It is possible that the code base does not currently comply with these +guidelines. We are not looking for a massive PR that fixes this, since that +goes against the spirit of the guidelines. All new contributions should make a +best effort to clean up and make the code base better than they left it. +Obviously, apply your best judgement. Remember, the goal here is to make the +code base easier for humans to navigate and understand. Always keep that in +mind when nudging others to comply. + +The rules: + +1. All code should be formatted with `gofmt -s`. +2. All code should pass the default levels of + [`golint`](https://github.com/golang/lint). +3. All code should follow the guidelines covered in [Effective + Go](http://golang.org/doc/effective_go.html) and [Go Code Review + Comments](https://github.com/golang/go/wiki/CodeReviewComments). +4. Comment the code. Tell us the why, the history and the context. +5. Document _all_ declarations and methods, even private ones. Declare + expectations, caveats and anything else that may be important. If a type + gets exported, having the comments already there will ensure it's ready. +6. Variable name length should be proportional to it's context and no longer. + `noCommaALongVariableNameLikeThisIsNotMoreClearWhenASimpleCommentWouldDo`. + In practice, short methods will have short variable names and globals will + have longer names. +7. No underscores in package names. If you need a compound name, step back, + and re-examine why you need a compound name. If you still think you need a + compound name, lose the underscore. +8. No utils or helpers packages. If a function is not general enough to + warrant it's own package, it has not been written generally enough to be a + part of a util package. Just leave it unexported and well-documented. +9. All tests should run with `go test` and outside tooling should not be + required. No, we don't need another unit testing framework. Assertion + packages are acceptable if they provide _real_ incremental value. +10. Even though we call these "rules" above, they are actually just + guidelines. Since you've read all the rules, you now know that. + +If you are having trouble getting into the mood of idiomatic Go, we recommend +reading through [Effective Go](http://golang.org/doc/effective_go.html). The +[Go Blog](http://blog.golang.org/) is also a great resource. Drinking the +kool-aid is a lot easier than going thirsty. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..b6d152fe --- /dev/null +++ b/Dockerfile @@ -0,0 +1,269 @@ +# This file describes the standard way to build Docker, using docker +# +# Usage: +# +# # Assemble the full dev environment. This is slow the first time. +# docker build -t docker . +# +# # Mount your source in an interactive container for quick testing: +# docker run -v `pwd`:/go/src/github.com/docker/docker --privileged -i -t docker bash +# +# # Run the test suite: +# docker run --privileged docker hack/make.sh test +# +# # Publish a release: +# docker run --privileged \ +# -e AWS_S3_BUCKET=baz \ +# -e AWS_ACCESS_KEY=foo \ +# -e AWS_SECRET_KEY=bar \ +# -e GPG_PASSPHRASE=gloubiboulga \ +# docker hack/release.sh +# +# Note: AppArmor used to mess with privileged mode, but this is no longer +# the case. Therefore, you don't have to disable it anymore. +# + +FROM debian:jessie + +# add zfs ppa +RUN apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys E871F18B51E0147C77796AC81196BA81F6B0FC61 \ + || apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys E871F18B51E0147C77796AC81196BA81F6B0FC61 +RUN echo deb http://ppa.launchpad.net/zfs-native/stable/ubuntu trusty main > /etc/apt/sources.list.d/zfs.list + + +# allow replacing httpredir mirror +ARG APT_MIRROR=httpredir.debian.org +RUN sed -i s/httpredir.debian.org/$APT_MIRROR/g /etc/apt/sources.list + +# Packaged dependencies +RUN apt-get update && apt-get install -y \ + apparmor \ + apt-utils \ + aufs-tools \ + automake \ + bash-completion \ + bsdmainutils \ + btrfs-tools \ + build-essential \ + clang \ + createrepo \ + curl \ + dpkg-sig \ + gcc-mingw-w64 \ + git \ + iptables \ + jq \ + libapparmor-dev \ + libcap-dev \ + libltdl-dev \ + libsqlite3-dev \ + libsystemd-journal-dev \ + libtool \ + mercurial \ + net-tools \ + pkg-config \ + python-dev \ + python-mock \ + python-pip \ + python-websocket \ + ubuntu-zfs \ + xfsprogs \ + libzfs-dev \ + tar \ + zip \ + --no-install-recommends \ + && pip install awscli==1.10.15 +# Get lvm2 source for compiling statically +ENV LVM2_VERSION 2.02.103 +RUN mkdir -p /usr/local/lvm2 \ + && curl -fsSL "https://mirrors.kernel.org/sourceware/lvm2/LVM2.${LVM2_VERSION}.tgz" \ + | tar -xzC /usr/local/lvm2 --strip-components=1 +# see https://git.fedorahosted.org/cgit/lvm2.git/refs/tags for release tags + +# Compile and install lvm2 +RUN cd /usr/local/lvm2 \ + && ./configure \ + --build="$(gcc -print-multiarch)" \ + --enable-static_link \ + && make device-mapper \ + && make install_device-mapper +# see https://git.fedorahosted.org/cgit/lvm2.git/tree/INSTALL + +# Configure the container for OSX cross compilation +ENV OSX_SDK MacOSX10.11.sdk +ENV OSX_CROSS_COMMIT 8aa9b71a394905e6c5f4b59e2b97b87a004658a4 +RUN set -x \ + && export OSXCROSS_PATH="/osxcross" \ + && git clone https://github.com/tpoechtrager/osxcross.git $OSXCROSS_PATH \ + && ( cd $OSXCROSS_PATH && git checkout -q $OSX_CROSS_COMMIT) \ + && curl -sSL https://s3.dockerproject.org/darwin/v2/${OSX_SDK}.tar.xz -o "${OSXCROSS_PATH}/tarballs/${OSX_SDK}.tar.xz" \ + && UNATTENDED=yes OSX_VERSION_MIN=10.6 ${OSXCROSS_PATH}/build.sh +ENV PATH /osxcross/target/bin:$PATH + +# install seccomp: the version shipped in trusty is too old +ENV SECCOMP_VERSION 2.3.0 +RUN set -x \ + && export SECCOMP_PATH="$(mktemp -d)" \ + && curl -fsSL "https://github.com/seccomp/libseccomp/releases/download/v${SECCOMP_VERSION}/libseccomp-${SECCOMP_VERSION}.tar.gz" \ + | tar -xzC "$SECCOMP_PATH" --strip-components=1 \ + && ( \ + cd "$SECCOMP_PATH" \ + && ./configure --prefix=/usr/local \ + && make \ + && make install \ + && ldconfig \ + ) \ + && rm -rf "$SECCOMP_PATH" + +# Install Go +# IMPORTANT: If the version of Go is updated, the Windows to Linux CI machines +# will need updating, to avoid errors. Ping #docker-maintainers on IRC +# with a heads-up. +ENV GO_VERSION 1.5.4 +RUN curl -fsSL "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz" \ + | tar -xzC /usr/local +ENV PATH /go/bin:/usr/local/go/bin:$PATH +ENV GOPATH /go:/go/src/github.com/docker/docker/vendor + +# Compile Go for cross compilation +ENV DOCKER_CROSSPLATFORMS \ + linux/386 linux/arm \ + darwin/amd64 \ + freebsd/amd64 freebsd/386 freebsd/arm \ + windows/amd64 windows/386 + +# This has been commented out and kept as reference because we don't support compiling with older Go anymore. +# ENV GOFMT_VERSION 1.3.3 +# RUN curl -sSL https://storage.googleapis.com/golang/go${GOFMT_VERSION}.$(go env GOOS)-$(go env GOARCH).tar.gz | tar -C /go/bin -xz --strip-components=2 go/bin/gofmt + +ENV GO_TOOLS_COMMIT 823804e1ae08dbb14eb807afc7db9993bc9e3cc3 +# Grab Go's cover tool for dead-simple code coverage testing +# Grab Go's vet tool for examining go code to find suspicious constructs +# and help prevent errors that the compiler might not catch +RUN git clone https://github.com/golang/tools.git /go/src/golang.org/x/tools \ + && (cd /go/src/golang.org/x/tools && git checkout -q $GO_TOOLS_COMMIT) \ + && go install -v golang.org/x/tools/cmd/cover \ + && go install -v golang.org/x/tools/cmd/vet +# Grab Go's lint tool +ENV GO_LINT_COMMIT 32a87160691b3c96046c0c678fe57c5bef761456 +RUN git clone https://github.com/golang/lint.git /go/src/github.com/golang/lint \ + && (cd /go/src/github.com/golang/lint && git checkout -q $GO_LINT_COMMIT) \ + && go install -v github.com/golang/lint/golint + +# Install two versions of the registry. The first is an older version that +# only supports schema1 manifests. The second is a newer version that supports +# both. This allows integration-cli tests to cover push/pull with both schema1 +# and schema2 manifests. +ENV REGISTRY_COMMIT_SCHEMA1 ec87e9b6971d831f0eff752ddb54fb64693e51cd +ENV REGISTRY_COMMIT 47a064d4195a9b56133891bbb13620c3ac83a827 +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone https://github.com/docker/distribution.git "$GOPATH/src/github.com/docker/distribution" \ + && (cd "$GOPATH/src/github.com/docker/distribution" && git checkout -q "$REGISTRY_COMMIT") \ + && GOPATH="$GOPATH/src/github.com/docker/distribution/Godeps/_workspace:$GOPATH" \ + go build -o /usr/local/bin/registry-v2 github.com/docker/distribution/cmd/registry \ + && (cd "$GOPATH/src/github.com/docker/distribution" && git checkout -q "$REGISTRY_COMMIT_SCHEMA1") \ + && GOPATH="$GOPATH/src/github.com/docker/distribution/Godeps/_workspace:$GOPATH" \ + go build -o /usr/local/bin/registry-v2-schema1 github.com/docker/distribution/cmd/registry \ + && rm -rf "$GOPATH" + +# Install notary server +ENV NOTARY_VERSION docker-v1.11-3 +RUN set -x \ + && export GO15VENDOREXPERIMENT=1 \ + && export GOPATH="$(mktemp -d)" \ + && git clone https://github.com/docker/notary.git "$GOPATH/src/github.com/docker/notary" \ + && (cd "$GOPATH/src/github.com/docker/notary" && git checkout -q "$NOTARY_VERSION") \ + && GOPATH="$GOPATH/src/github.com/docker/notary/vendor:$GOPATH" \ + go build -o /usr/local/bin/notary-server github.com/docker/notary/cmd/notary-server \ + && GOPATH="$GOPATH/src/github.com/docker/notary/vendor:$GOPATH" \ + go build -o /usr/local/bin/notary github.com/docker/notary/cmd/notary \ + && rm -rf "$GOPATH" + +# Get the "docker-py" source so we can run their integration tests +ENV DOCKER_PY_COMMIT e2878cbcc3a7eef99917adc1be252800b0e41ece +RUN git clone https://github.com/docker/docker-py.git /docker-py \ + && cd /docker-py \ + && git checkout -q $DOCKER_PY_COMMIT \ + && pip install -r test-requirements.txt + +# Set user.email so crosbymichael's in-container merge commits go smoothly +RUN git config --global user.email 'docker-dummy@example.com' + +# Add an unprivileged user to be used for tests which need it +RUN groupadd -r docker +RUN useradd --create-home --gid docker unprivilegeduser + +VOLUME /var/lib/docker +WORKDIR /go/src/github.com/docker/docker +ENV DOCKER_BUILDTAGS apparmor pkcs11 seccomp selinux + +# Let us use a .bashrc file +RUN ln -sfv $PWD/.bashrc ~/.bashrc + +# Register Docker's bash completion. +RUN ln -sv $PWD/contrib/completion/bash/docker /etc/bash_completion.d/docker + +# Get useful and necessary Hub images so we can "docker load" locally instead of pulling +COPY contrib/download-frozen-image-v2.sh /go/src/github.com/docker/docker/contrib/ +RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \ + buildpack-deps:jessie@sha256:25785f89240fbcdd8a74bdaf30dd5599a9523882c6dfc567f2e9ef7cf6f79db6 \ + busybox:latest@sha256:e4f93f6ed15a0cdd342f5aae387886fba0ab98af0a102da6276eaf24d6e6ade0 \ + debian:jessie@sha256:f968f10b4b523737e253a97eac59b0d1420b5c19b69928d35801a6373ffe330e \ + hello-world:latest@sha256:8be990ef2aeb16dbcb9271ddfe2610fa6658d13f6dfb8bc72074cc1ca36966a7 +# see also "hack/make/.ensure-frozen-images" (which needs to be updated any time this list is) + +# Download man page generator +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone --depth 1 -b v1.0.4 https://github.com/cpuguy83/go-md2man.git "$GOPATH/src/github.com/cpuguy83/go-md2man" \ + && git clone --depth 1 -b v1.4 https://github.com/russross/blackfriday.git "$GOPATH/src/github.com/russross/blackfriday" \ + && go get -v -d github.com/cpuguy83/go-md2man \ + && go build -v -o /usr/local/bin/go-md2man github.com/cpuguy83/go-md2man \ + && rm -rf "$GOPATH" + +# Download toml validator +ENV TOMLV_COMMIT 9baf8a8a9f2ed20a8e54160840c492f937eeaf9a +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone https://github.com/BurntSushi/toml.git "$GOPATH/src/github.com/BurntSushi/toml" \ + && (cd "$GOPATH/src/github.com/BurntSushi/toml" && git checkout -q "$TOMLV_COMMIT") \ + && go build -v -o /usr/local/bin/tomlv github.com/BurntSushi/toml/cmd/tomlv \ + && rm -rf "$GOPATH" + +# Build/install the tool for embedding resources in Windows binaries +ENV RSRC_COMMIT ba14da1f827188454a4591717fff29999010887f +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone https://github.com/akavel/rsrc.git "$GOPATH/src/github.com/akavel/rsrc" \ + && (cd "$GOPATH/src/github.com/akavel/rsrc" && git checkout -q "$RSRC_COMMIT") \ + && go build -v -o /usr/local/bin/rsrc github.com/akavel/rsrc \ + && rm -rf "$GOPATH" + +# Install runc +ENV RUNC_COMMIT baf6536d6259209c3edfa2b22237af82942d3dfa +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone git://github.com/opencontainers/runc.git "$GOPATH/src/github.com/opencontainers/runc" \ + && cd "$GOPATH/src/github.com/opencontainers/runc" \ + && git checkout -q "$RUNC_COMMIT" \ + && make static BUILDTAGS="seccomp apparmor selinux" \ + && cp runc /usr/local/bin/docker-runc + +# Install containerd +ENV CONTAINERD_COMMIT 9dc2b3273db42c75368988a3885a3afd770069d9 +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone git://github.com/docker/containerd.git "$GOPATH/src/github.com/docker/containerd" \ + && cd "$GOPATH/src/github.com/docker/containerd" \ + && git checkout -q "$CONTAINERD_COMMIT" \ + && make static \ + && cp bin/containerd /usr/local/bin/docker-containerd \ + && cp bin/containerd-shim /usr/local/bin/docker-containerd-shim \ + && cp bin/ctr /usr/local/bin/docker-containerd-ctr + +# Wrap all commands in the "docker-in-docker" script to allow nested containers +ENTRYPOINT ["hack/dind"] + +# Upload docker source +COPY . /go/src/github.com/docker/docker diff --git a/Dockerfile.aarch64 b/Dockerfile.aarch64 new file mode 100644 index 00000000..30ad57d1 --- /dev/null +++ b/Dockerfile.aarch64 @@ -0,0 +1,209 @@ +# This file describes the standard way to build Docker on aarch64, using docker +# +# Usage: +# +# # Assemble the full dev environment. This is slow the first time. +# docker build -t docker -f Dockerfile.aarch64 . +# +# # Mount your source in an interactive container for quick testing: +# docker run -v `pwd`:/go/src/github.com/docker/docker --privileged -i -t docker bash +# +# # Run the test suite: +# docker run --privileged docker hack/make.sh test +# +# Note: AppArmor used to mess with privileged mode, but this is no longer +# the case. Therefore, you don't have to disable it anymore. +# + +FROM aarch64/ubuntu:wily + +# Packaged dependencies +RUN apt-get update && apt-get install -y \ + apparmor \ + aufs-tools \ + automake \ + bash-completion \ + btrfs-tools \ + build-essential \ + createrepo \ + curl \ + dpkg-sig \ + g++ \ + gcc \ + git \ + iptables \ + jq \ + libapparmor-dev \ + libc6-dev \ + libcap-dev \ + libsqlite3-dev \ + libsystemd-dev \ + mercurial \ + net-tools \ + parallel \ + pkg-config \ + python-dev \ + python-mock \ + python-pip \ + python-websocket \ + gccgo \ + --no-install-recommends + +# Install armhf loader to use armv6 binaries on armv8 +RUN dpkg --add-architecture armhf \ + && apt-get update \ + && apt-get install -y libc6:armhf + +# Get lvm2 source for compiling statically +ENV LVM2_VERSION 2.02.103 +RUN mkdir -p /usr/local/lvm2 \ + && curl -fsSL "https://mirrors.kernel.org/sourceware/lvm2/LVM2.${LVM2_VERSION}.tgz" \ + | tar -xzC /usr/local/lvm2 --strip-components=1 +# see https://git.fedorahosted.org/cgit/lvm2.git/refs/tags for release tags + +# fix platform enablement in lvm2 to support aarch64 properly +RUN set -e \ + && for f in config.guess config.sub; do \ + curl -fsSL -o "/usr/local/lvm2/autoconf/$f" "http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=$f;hb=HEAD"; \ + done +# "arch.c:78:2: error: #error the arch code needs to know about your machine type" + +# Compile and install lvm2 +RUN cd /usr/local/lvm2 \ + && ./configure \ + --build="$(gcc -print-multiarch)" \ + --enable-static_link \ + && make device-mapper \ + && make install_device-mapper +# see https://git.fedorahosted.org/cgit/lvm2.git/tree/INSTALL + +# install seccomp: the version shipped in trusty is too old +ENV SECCOMP_VERSION 2.3.0 +RUN set -x \ + && export SECCOMP_PATH="$(mktemp -d)" \ + && curl -fsSL "https://github.com/seccomp/libseccomp/releases/download/v${SECCOMP_VERSION}/libseccomp-${SECCOMP_VERSION}.tar.gz" \ + | tar -xzC "$SECCOMP_PATH" --strip-components=1 \ + && ( \ + cd "$SECCOMP_PATH" \ + && ./configure --prefix=/usr/local \ + && make \ + && make install \ + && ldconfig \ + ) \ + && rm -rf "$SECCOMP_PATH" + +# Install Go +# We don't have official binary tarballs for ARM64, eigher for Go or bootstrap, +# so we use the official armv6 released binaries as a GOROOT_BOOTSTRAP, and +# build Go from source code. +ENV GO_VERSION 1.5.4 +RUN mkdir /usr/src/go && curl -fsSL https://storage.googleapis.com/golang/go${GO_VERSION}.src.tar.gz | tar -v -C /usr/src/go -xz --strip-components=1 \ + && cd /usr/src/go/src \ + && GOOS=linux GOARCH=arm64 GOROOT_BOOTSTRAP="$(go env GOROOT)" ./make.bash + +ENV PATH /usr/src/go/bin:$PATH +ENV GOPATH /go:/go/src/github.com/docker/docker/vendor + +# Only install one version of the registry, because old version which support +# schema1 manifests is not working on ARM64, we should skip integration-cli +# tests for schema1 manifests on ARM64. +ENV REGISTRY_COMMIT 47a064d4195a9b56133891bbb13620c3ac83a827 +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone https://github.com/docker/distribution.git "$GOPATH/src/github.com/docker/distribution" \ + && (cd "$GOPATH/src/github.com/docker/distribution" && git checkout -q "$REGISTRY_COMMIT") \ + && GOPATH="$GOPATH/src/github.com/docker/distribution/Godeps/_workspace:$GOPATH" \ + go build -o /usr/local/bin/registry-v2 github.com/docker/distribution/cmd/registry \ + && rm -rf "$GOPATH" + +# Install notary server +ENV NOTARY_VERSION docker-v1.11-3 +RUN set -x \ + && export GO15VENDOREXPERIMENT=1 \ + && export GOPATH="$(mktemp -d)" \ + && git clone https://github.com/docker/notary.git "$GOPATH/src/github.com/docker/notary" \ + && (cd "$GOPATH/src/github.com/docker/notary" && git checkout -q "$NOTARY_VERSION") \ + && GOPATH="$GOPATH/src/github.com/docker/notary/vendor:$GOPATH" \ + go build -o /usr/local/bin/notary-server github.com/docker/notary/cmd/notary-server \ + && GOPATH="$GOPATH/src/github.com/docker/notary/vendor:$GOPATH" \ + go build -o /usr/local/bin/notary github.com/docker/notary/cmd/notary \ + && rm -rf "$GOPATH" + +# Get the "docker-py" source so we can run their integration tests +ENV DOCKER_PY_COMMIT e2878cbcc3a7eef99917adc1be252800b0e41ece +RUN git clone https://github.com/docker/docker-py.git /docker-py \ + && cd /docker-py \ + && git checkout -q $DOCKER_PY_COMMIT \ + && pip install -r test-requirements.txt + +# Set user.email so crosbymichael's in-container merge commits go smoothly +RUN git config --global user.email 'docker-dummy@example.com' + +# Add an unprivileged user to be used for tests which need it +RUN groupadd -r docker +RUN useradd --create-home --gid docker unprivilegeduser + +VOLUME /var/lib/docker +WORKDIR /go/src/github.com/docker/docker +ENV DOCKER_BUILDTAGS apparmor pkcs11 seccomp selinux + +# Let us use a .bashrc file +RUN ln -sfv $PWD/.bashrc ~/.bashrc + +# Register Docker's bash completion. +RUN ln -sv $PWD/contrib/completion/bash/docker /etc/bash_completion.d/docker + +# Get useful and necessary Hub images so we can "docker load" locally instead of pulling +COPY contrib/download-frozen-image-v2.sh /go/src/github.com/docker/docker/contrib/ +RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \ + aarch64/buildpack-deps:jessie@sha256:6aa1d6910791b7ac78265fd0798e5abd6cb3f27ae992f6f960f6c303ec9535f2 \ + aarch64/busybox:latest@sha256:b23a6a37cf269dff6e46d2473b6e227afa42b037e6d23435f1d2bc40fc8c2828 \ + aarch64/debian:jessie@sha256:4be74a41a7c70ebe887b634b11ffe516cf4fcd56864a54941e56bb49883c3170 \ + aarch64/hello-world:latest@sha256:65a4a158587b307bb02db4de41b836addb0c35175bdc801367b1ac1ddeb9afda +# see also "hack/make/.ensure-frozen-images" (which needs to be updated any time this list is) + +# Download man page generator +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone --depth 1 -b v1.0.4 https://github.com/cpuguy83/go-md2man.git "$GOPATH/src/github.com/cpuguy83/go-md2man" \ + && git clone --depth 1 -b v1.4 https://github.com/russross/blackfriday.git "$GOPATH/src/github.com/russross/blackfriday" \ + && go get -v -d github.com/cpuguy83/go-md2man \ + && go build -v -o /usr/local/bin/go-md2man github.com/cpuguy83/go-md2man \ + && rm -rf "$GOPATH" + +# Download toml validator +ENV TOMLV_COMMIT 9baf8a8a9f2ed20a8e54160840c492f937eeaf9a +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone https://github.com/BurntSushi/toml.git "$GOPATH/src/github.com/BurntSushi/toml" \ + && (cd "$GOPATH/src/github.com/BurntSushi/toml" && git checkout -q "$TOMLV_COMMIT") \ + && go build -v -o /usr/local/bin/tomlv github.com/BurntSushi/toml/cmd/tomlv \ + && rm -rf "$GOPATH" + +# Install runc +ENV RUNC_COMMIT baf6536d6259209c3edfa2b22237af82942d3dfa +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone git://github.com/opencontainers/runc.git "$GOPATH/src/github.com/opencontainers/runc" \ + && cd "$GOPATH/src/github.com/opencontainers/runc" \ + && git checkout -q "$RUNC_COMMIT" \ + && make static BUILDTAGS="seccomp apparmor selinux" \ + && cp runc /usr/local/bin/docker-runc + +# Install containerd +ENV CONTAINERD_COMMIT 9dc2b3273db42c75368988a3885a3afd770069d9 +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone git://github.com/docker/containerd.git "$GOPATH/src/github.com/docker/containerd" \ + && cd "$GOPATH/src/github.com/docker/containerd" \ + && git checkout -q "$CONTAINERD_COMMIT" \ + && make static \ + && cp bin/containerd /usr/local/bin/docker-containerd \ + && cp bin/containerd-shim /usr/local/bin/docker-containerd-shim \ + && cp bin/ctr /usr/local/bin/docker-containerd-ctr + +# Wrap all commands in the "docker-in-docker" script to allow nested containers +ENTRYPOINT ["hack/dind"] + +# Upload docker source +COPY . /go/src/github.com/docker/docker diff --git a/Dockerfile.armhf b/Dockerfile.armhf new file mode 100644 index 00000000..ce532ac0 --- /dev/null +++ b/Dockerfile.armhf @@ -0,0 +1,227 @@ +# This file describes the standard way to build Docker on ARMv7, using docker +# +# Usage: +# +# # Assemble the full dev environment. This is slow the first time. +# docker build -t docker -f Dockerfile.armhf . +# +# # Mount your source in an interactive container for quick testing: +# docker run -v `pwd`:/go/src/github.com/docker/docker --privileged -i -t docker bash +# +# # Run the test suite: +# docker run --privileged docker hack/make.sh test +# +# Note: AppArmor used to mess with privileged mode, but this is no longer +# the case. Therefore, you don't have to disable it anymore. +# + +FROM armhf/debian:jessie + +# Packaged dependencies +RUN apt-get update && apt-get install -y \ + apparmor \ + aufs-tools \ + automake \ + bash-completion \ + btrfs-tools \ + build-essential \ + createrepo \ + curl \ + dpkg-sig \ + git \ + iptables \ + jq \ + net-tools \ + libapparmor-dev \ + libcap-dev \ + libltdl-dev \ + libsqlite3-dev \ + libsystemd-journal-dev \ + libtool \ + mercurial \ + pkg-config \ + python-dev \ + python-mock \ + python-pip \ + python-websocket \ + xfsprogs \ + tar \ + --no-install-recommends + +# Get lvm2 source for compiling statically +ENV LVM2_VERSION 2.02.103 +RUN mkdir -p /usr/local/lvm2 \ + && curl -fsSL "https://mirrors.kernel.org/sourceware/lvm2/LVM2.${LVM2_VERSION}.tgz" \ + | tar -xzC /usr/local/lvm2 --strip-components=1 +# see https://git.fedorahosted.org/cgit/lvm2.git/refs/tags for release tags + +# Compile and install lvm2 +RUN cd /usr/local/lvm2 \ + && ./configure \ + --build="$(gcc -print-multiarch)" \ + --enable-static_link \ + && make device-mapper \ + && make install_device-mapper +# see https://git.fedorahosted.org/cgit/lvm2.git/tree/INSTALL + +# Install Go +# TODO Update to 1.5.4 once available, or build from source, as these builds +# are marked "end of life", see http://dave.cheney.net/unofficial-arm-tarballs +ENV GO_VERSION 1.5.3 +RUN curl -fsSL "http://dave.cheney.net/paste/go${GO_VERSION}.linux-arm.tar.gz" \ + | tar -xzC /usr/local +ENV PATH /go/bin:/usr/local/go/bin:$PATH +ENV GOPATH /go:/go/src/github.com/docker/docker/vendor + +# we're building for armhf, which is ARMv7, so let's be explicit about that +ENV GOARCH arm +ENV GOARM 7 + +# This has been commented out and kept as reference because we don't support compiling with older Go anymore. +# ENV GOFMT_VERSION 1.3.3 +# RUN curl -sSL https://storage.googleapis.com/golang/go${GOFMT_VERSION}.$(go env GOOS)-$(go env GOARCH).tar.gz | tar -C /go/bin -xz --strip-components=2 go/bin/gofmt + +ENV GO_TOOLS_COMMIT 823804e1ae08dbb14eb807afc7db9993bc9e3cc3 +# Grab Go's cover tool for dead-simple code coverage testing +# Grab Go's vet tool for examining go code to find suspicious constructs +# and help prevent errors that the compiler might not catch +RUN git clone https://github.com/golang/tools.git /go/src/golang.org/x/tools \ + && (cd /go/src/golang.org/x/tools && git checkout -q $GO_TOOLS_COMMIT) \ + && go install -v golang.org/x/tools/cmd/cover \ + && go install -v golang.org/x/tools/cmd/vet +# Grab Go's lint tool +ENV GO_LINT_COMMIT 32a87160691b3c96046c0c678fe57c5bef761456 +RUN git clone https://github.com/golang/lint.git /go/src/github.com/golang/lint \ + && (cd /go/src/github.com/golang/lint && git checkout -q $GO_LINT_COMMIT) \ + && go install -v github.com/golang/lint/golint + +# install seccomp: the version shipped in trusty is too old +ENV SECCOMP_VERSION 2.3.0 +RUN set -x \ + && export SECCOMP_PATH="$(mktemp -d)" \ + && curl -fsSL "https://github.com/seccomp/libseccomp/releases/download/v${SECCOMP_VERSION}/libseccomp-${SECCOMP_VERSION}.tar.gz" \ + | tar -xzC "$SECCOMP_PATH" --strip-components=1 \ + && ( \ + cd "$SECCOMP_PATH" \ + && ./configure --prefix=/usr/local \ + && make \ + && make install \ + && ldconfig \ + ) \ + && rm -rf "$SECCOMP_PATH" + +# Install two versions of the registry. The first is an older version that +# only supports schema1 manifests. The second is a newer version that supports +# both. This allows integration-cli tests to cover push/pull with both schema1 +# and schema2 manifests. +ENV REGISTRY_COMMIT_SCHEMA1 ec87e9b6971d831f0eff752ddb54fb64693e51cd +ENV REGISTRY_COMMIT cb08de17d74bef86ce6c5abe8b240e282f5750be +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone https://github.com/docker/distribution.git "$GOPATH/src/github.com/docker/distribution" \ + && (cd "$GOPATH/src/github.com/docker/distribution" && git checkout -q "$REGISTRY_COMMIT") \ + && GOPATH="$GOPATH/src/github.com/docker/distribution/Godeps/_workspace:$GOPATH" \ + go build -o /usr/local/bin/registry-v2 github.com/docker/distribution/cmd/registry \ + && (cd "$GOPATH/src/github.com/docker/distribution" && git checkout -q "$REGISTRY_COMMIT_SCHEMA1") \ + && GOPATH="$GOPATH/src/github.com/docker/distribution/Godeps/_workspace:$GOPATH" \ + go build -o /usr/local/bin/registry-v2-schema1 github.com/docker/distribution/cmd/registry \ + && rm -rf "$GOPATH" + +# Install notary server +ENV NOTARY_VERSION docker-v1.11-3 +RUN set -x \ + && export GO15VENDOREXPERIMENT=1 \ + && export GOPATH="$(mktemp -d)" \ + && git clone https://github.com/docker/notary.git "$GOPATH/src/github.com/docker/notary" \ + && (cd "$GOPATH/src/github.com/docker/notary" && git checkout -q "$NOTARY_VERSION") \ + && GOPATH="$GOPATH/src/github.com/docker/notary/vendor:$GOPATH" \ + go build -o /usr/local/bin/notary-server github.com/docker/notary/cmd/notary-server \ + && GOPATH="$GOPATH/src/github.com/docker/notary/vendor:$GOPATH" \ + go build -o /usr/local/bin/notary github.com/docker/notary/cmd/notary \ + && rm -rf "$GOPATH" + +# Get the "docker-py" source so we can run their integration tests +ENV DOCKER_PY_COMMIT e2878cbcc3a7eef99917adc1be252800b0e41ece +RUN git clone https://github.com/docker/docker-py.git /docker-py \ + && cd /docker-py \ + && git checkout -q $DOCKER_PY_COMMIT \ + && pip install -r test-requirements.txt + +# Set user.email so crosbymichael's in-container merge commits go smoothly +RUN git config --global user.email 'docker-dummy@example.com' + +# Add an unprivileged user to be used for tests which need it +RUN groupadd -r docker +RUN useradd --create-home --gid docker unprivilegeduser + +VOLUME /var/lib/docker +WORKDIR /go/src/github.com/docker/docker +ENV DOCKER_BUILDTAGS apparmor pkcs11 seccomp selinux + +# Let us use a .bashrc file +RUN ln -sfv $PWD/.bashrc ~/.bashrc + +# Register Docker's bash completion. +RUN ln -sv $PWD/contrib/completion/bash/docker /etc/bash_completion.d/docker + +# Get useful and necessary Hub images so we can "docker load" locally instead of pulling +COPY contrib/download-frozen-image-v2.sh /go/src/github.com/docker/docker/contrib/ +RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \ + armhf/buildpack-deps:jessie@sha256:ca6cce8e5bf5c952129889b5cc15cd6aa8d995d77e55e3749bbaadae50e476cb \ + armhf/busybox:latest@sha256:d98a7343ac750ffe387e3d514f8521ba69846c216778919b01414b8617cfb3d4 \ + armhf/debian:jessie@sha256:4a2187483f04a84f9830910fe3581d69b3c985cc045d9f01d8e2f3795b28107b \ + armhf/hello-world:latest@sha256:161dcecea0225975b2ad5f768058212c1e0d39e8211098666ffa1ac74cfb7791 +# see also "hack/make/.ensure-frozen-images" (which needs to be updated any time this list is) + +# Download man page generator +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone --depth 1 -b v1.0.4 https://github.com/cpuguy83/go-md2man.git "$GOPATH/src/github.com/cpuguy83/go-md2man" \ + && git clone --depth 1 -b v1.4 https://github.com/russross/blackfriday.git "$GOPATH/src/github.com/russross/blackfriday" \ + && go get -v -d github.com/cpuguy83/go-md2man \ + && go build -v -o /usr/local/bin/go-md2man github.com/cpuguy83/go-md2man \ + && rm -rf "$GOPATH" + +# Download toml validator +ENV TOMLV_COMMIT 9baf8a8a9f2ed20a8e54160840c492f937eeaf9a +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone https://github.com/BurntSushi/toml.git "$GOPATH/src/github.com/BurntSushi/toml" \ + && (cd "$GOPATH/src/github.com/BurntSushi/toml" && git checkout -q "$TOMLV_COMMIT") \ + && go build -v -o /usr/local/bin/tomlv github.com/BurntSushi/toml/cmd/tomlv \ + && rm -rf "$GOPATH" + +# Build/install the tool for embedding resources in Windows binaries +ENV RSRC_VERSION v2 +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone --depth 1 -b "$RSRC_VERSION" https://github.com/akavel/rsrc.git "$GOPATH/src/github.com/akavel/rsrc" \ + && go build -v -o /usr/local/bin/rsrc github.com/akavel/rsrc \ + && rm -rf "$GOPATH" + +# Install runc +ENV RUNC_COMMIT baf6536d6259209c3edfa2b22237af82942d3dfa +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone git://github.com/opencontainers/runc.git "$GOPATH/src/github.com/opencontainers/runc" \ + && cd "$GOPATH/src/github.com/opencontainers/runc" \ + && git checkout -q "$RUNC_COMMIT" \ + && make static BUILDTAGS="seccomp apparmor selinux" \ + && cp runc /usr/local/bin/docker-runc + +# Install containerd +ENV CONTAINERD_COMMIT 9dc2b3273db42c75368988a3885a3afd770069d9 +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone git://github.com/docker/containerd.git "$GOPATH/src/github.com/docker/containerd" \ + && cd "$GOPATH/src/github.com/docker/containerd" \ + && git checkout -q "$CONTAINERD_COMMIT" \ + && make static \ + && cp bin/containerd /usr/local/bin/docker-containerd \ + && cp bin/containerd-shim /usr/local/bin/docker-containerd-shim \ + && cp bin/ctr /usr/local/bin/docker-containerd-ctr + +ENTRYPOINT ["hack/dind"] + +# Upload docker source +COPY . /go/src/github.com/docker/docker diff --git a/Dockerfile.gccgo b/Dockerfile.gccgo new file mode 100644 index 00000000..eadd15ce --- /dev/null +++ b/Dockerfile.gccgo @@ -0,0 +1,102 @@ +# This file describes the standard way to build Docker, using docker +# +# Usage: +# +# # Assemble the full dev environment. This is slow the first time. +# docker build -t docker -f Dockerfile.gccgo . +# + +FROM gcc:5.3 + +# Packaged dependencies +RUN apt-get update && apt-get install -y \ + apparmor \ + aufs-tools \ + btrfs-tools \ + build-essential \ + curl \ + git \ + iptables \ + jq \ + net-tools \ + libapparmor-dev \ + libcap-dev \ + libsqlite3-dev \ + mercurial \ + net-tools \ + parallel \ + python-dev \ + python-mock \ + python-pip \ + python-websocket \ + --no-install-recommends + +# Get lvm2 source for compiling statically +RUN git clone -b v2_02_103 https://git.fedorahosted.org/git/lvm2.git /usr/local/lvm2 +# see https://git.fedorahosted.org/cgit/lvm2.git/refs/tags for release tags + +# Compile and install lvm2 +RUN cd /usr/local/lvm2 \ + && ./configure --enable-static_link \ + && make device-mapper \ + && make install_device-mapper +# see https://git.fedorahosted.org/cgit/lvm2.git/tree/INSTALL + +# install seccomp: the version shipped in jessie is too old +ENV SECCOMP_VERSION v2.3.0 +RUN set -x \ + && export SECCOMP_PATH=$(mktemp -d) \ + && git clone https://github.com/seccomp/libseccomp.git "$SECCOMP_PATH" \ + && ( \ + cd "$SECCOMP_PATH" \ + && git checkout "$SECCOMP_VERSION" \ + && ./autogen.sh \ + && ./configure --prefix=/usr \ + && make \ + && make install \ + ) \ + && rm -rf "$SECCOMP_PATH" + +ENV GOPATH /go:/go/src/github.com/docker/docker/vendor + +# Get the "docker-py" source so we can run their integration tests +ENV DOCKER_PY_COMMIT e2878cbcc3a7eef99917adc1be252800b0e41ece +RUN git clone https://github.com/docker/docker-py.git /docker-py \ + && cd /docker-py \ + && git checkout -q $DOCKER_PY_COMMIT + +# Add an unprivileged user to be used for tests which need it +RUN groupadd -r docker +RUN useradd --create-home --gid docker unprivilegeduser + +VOLUME /var/lib/docker +WORKDIR /go/src/github.com/docker/docker +ENV DOCKER_BUILDTAGS apparmor seccomp selinux + +# Install runc +ENV RUNC_COMMIT baf6536d6259209c3edfa2b22237af82942d3dfa +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone git://github.com/opencontainers/runc.git "$GOPATH/src/github.com/opencontainers/runc" \ + && cd "$GOPATH/src/github.com/opencontainers/runc" \ + && git checkout -q "$RUNC_COMMIT" \ + && make static BUILDTAGS="seccomp apparmor selinux" \ + && cp runc /usr/local/bin/docker-runc + +# Install containerd +ENV CONTAINERD_COMMIT 9dc2b3273db42c75368988a3885a3afd770069d9 +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone git://github.com/docker/containerd.git "$GOPATH/src/github.com/docker/containerd" \ + && cd "$GOPATH/src/github.com/docker/containerd" \ + && git checkout -q "$CONTAINERD_COMMIT" \ + && make static \ + && cp bin/containerd /usr/local/bin/docker-containerd \ + && cp bin/containerd-shim /usr/local/bin/docker-containerd-shim \ + && cp bin/ctr /usr/local/bin/docker-containerd-ctr + +# Wrap all commands in the "docker-in-docker" script to allow nested containers +ENTRYPOINT ["hack/dind"] + +# Upload docker source +COPY . /go/src/github.com/docker/docker diff --git a/Dockerfile.ppc64le b/Dockerfile.ppc64le new file mode 100644 index 00000000..626415d2 --- /dev/null +++ b/Dockerfile.ppc64le @@ -0,0 +1,225 @@ +# This file describes the standard way to build Docker on ppc64le, using docker +# +# Usage: +# +# # Assemble the full dev environment. This is slow the first time. +# docker build -t docker -f Dockerfile.ppc64le . +# +# # Mount your source in an interactive container for quick testing: +# docker run -v `pwd`:/go/src/github.com/docker/docker --privileged -i -t docker bash +# +# # Run the test suite: +# docker run --privileged docker hack/make.sh test +# +# Note: AppArmor used to mess with privileged mode, but this is no longer +# the case. Therefore, you don't have to disable it anymore. +# + +FROM ppc64le/gcc:5.3 + +# Packaged dependencies +RUN apt-get update && apt-get install -y \ + apparmor \ + aufs-tools \ + automake \ + bash-completion \ + btrfs-tools \ + build-essential \ + createrepo \ + curl \ + dpkg-sig \ + git \ + iptables \ + jq \ + net-tools \ + libapparmor-dev \ + libcap-dev \ + libltdl-dev \ + libsqlite3-dev \ + libsystemd-journal-dev \ + libtool \ + mercurial \ + pkg-config \ + python-dev \ + python-mock \ + python-pip \ + python-websocket \ + xfsprogs \ + tar \ + --no-install-recommends + +# Get lvm2 source for compiling statically +ENV LVM2_VERSION 2.02.103 +RUN mkdir -p /usr/local/lvm2 \ + && curl -fsSL "https://mirrors.kernel.org/sourceware/lvm2/LVM2.${LVM2_VERSION}.tgz" \ + | tar -xzC /usr/local/lvm2 --strip-components=1 +# see https://git.fedorahosted.org/cgit/lvm2.git/refs/tags for release tags + +# fix platform enablement in lvm2 to support ppc64le properly +RUN set -e \ + && for f in config.guess config.sub; do \ + curl -fsSL -o "/usr/local/lvm2/autoconf/$f" "http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=$f;hb=HEAD"; \ + done +# "arch.c:78:2: error: #error the arch code needs to know about your machine type" + +# Compile and install lvm2 +RUN cd /usr/local/lvm2 \ + && ./configure \ + --build="$(gcc -print-multiarch)" \ + --enable-static_link \ + && make device-mapper \ + && make install_device-mapper +# see https://git.fedorahosted.org/cgit/lvm2.git/tree/INSTALL + +## BUILD GOLANG 1.6 +# NOTE: ppc64le has compatibility issues with older versions of go, so make sure the version >= 1.6 +ENV GO_VERSION 1.6.2 +ENV GO_DOWNLOAD_URL https://golang.org/dl/go${GO_VERSION}.src.tar.gz +ENV GO_DOWNLOAD_SHA256 787b0b750d037016a30c6ed05a8a70a91b2e9db4bd9b1a2453aa502a63f1bccc +ENV GOROOT_BOOTSTRAP /usr/local + +RUN curl -fsSL "$GO_DOWNLOAD_URL" -o golang.tar.gz \ + && echo "$GO_DOWNLOAD_SHA256 golang.tar.gz" | sha256sum -c - \ + && tar -C /usr/src -xzf golang.tar.gz \ + && rm golang.tar.gz \ + && cd /usr/src/go/src && ./make.bash 2>&1 + +ENV GOROOT_BOOTSTRAP /usr/src/ + +ENV PATH /usr/src/go/bin/:/go/bin:$PATH +ENV GOPATH /go:/go/src/github.com/docker/docker/vendor + +# This has been commented out and kept as reference because we don't support compiling with older Go anymore. +# ENV GOFMT_VERSION 1.3.3 +# RUN curl -sSL https://storage.googleapis.com/golang/go${GOFMT_VERSION}.$(go env GOOS)-$(go env GOARCH).tar.gz | tar -C /go/bin -xz --strip-components=2 go/bin/gofmt + +ENV GO_TOOLS_COMMIT 823804e1ae08dbb14eb807afc7db9993bc9e3cc3 +# Grab Go's cover tool for dead-simple code coverage testing +# Grab Go's vet tool for examining go code to find suspicious constructs +# and help prevent errors that the compiler might not catch +RUN git clone https://github.com/golang/tools.git /go/src/golang.org/x/tools \ + && (cd /go/src/golang.org/x/tools && git checkout -q $GO_TOOLS_COMMIT) \ + && go install -v golang.org/x/tools/cmd/cover \ + && go install -v golang.org/x/tools/cmd/vet +# Grab Go's lint tool +ENV GO_LINT_COMMIT 32a87160691b3c96046c0c678fe57c5bef761456 +RUN git clone https://github.com/golang/lint.git /go/src/github.com/golang/lint \ + && (cd /go/src/github.com/golang/lint && git checkout -q $GO_LINT_COMMIT) \ + && go install -v github.com/golang/lint/golint + +# Install two versions of the registry. The first is an older version that +# only supports schema1 manifests. The second is a newer version that supports +# both. This allows integration-cli tests to cover push/pull with both schema1 +# and schema2 manifests. +ENV REGISTRY_COMMIT_SCHEMA1 ec87e9b6971d831f0eff752ddb54fb64693e51cd +ENV REGISTRY_COMMIT 47a064d4195a9b56133891bbb13620c3ac83a827 +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone https://github.com/docker/distribution.git "$GOPATH/src/github.com/docker/distribution" \ + && (cd "$GOPATH/src/github.com/docker/distribution" && git checkout -q "$REGISTRY_COMMIT") \ + && GOPATH="$GOPATH/src/github.com/docker/distribution/Godeps/_workspace:$GOPATH" \ + go build -o /usr/local/bin/registry-v2 github.com/docker/distribution/cmd/registry \ + && (cd "$GOPATH/src/github.com/docker/distribution" && git checkout -q "$REGISTRY_COMMIT_SCHEMA1") \ + && GOPATH="$GOPATH/src/github.com/docker/distribution/Godeps/_workspace:$GOPATH" \ + go build -o /usr/local/bin/registry-v2-schema1 github.com/docker/distribution/cmd/registry \ + && rm -rf "$GOPATH" + +# Install notary and notary-server +ENV NOTARY_VERSION docker-v1.11-3 +RUN set -x \ + && export GO15VENDOREXPERIMENT=1 \ + && export GOPATH="$(mktemp -d)" \ + && git clone https://github.com/docker/notary.git "$GOPATH/src/github.com/docker/notary" \ + && (cd "$GOPATH/src/github.com/docker/notary" && git checkout -q "$NOTARY_VERSION") \ + && GOPATH="$GOPATH/src/github.com/docker/notary/vendor:$GOPATH" \ + go build -o /usr/local/bin/notary-server github.com/docker/notary/cmd/notary-server \ + && GOPATH="$GOPATH/src/github.com/docker/notary/vendor:$GOPATH" \ + go build -o /usr/local/bin/notary github.com/docker/notary/cmd/notary \ + && rm -rf "$GOPATH" + +# Get the "docker-py" source so we can run their integration tests +ENV DOCKER_PY_COMMIT e2878cbcc3a7eef99917adc1be252800b0e41ece +RUN git clone https://github.com/docker/docker-py.git /docker-py \ + && cd /docker-py \ + && git checkout -q $DOCKER_PY_COMMIT \ + && pip install -r test-requirements.txt + +# Set user.email so crosbymichael's in-container merge commits go smoothly +RUN git config --global user.email 'docker-dummy@example.com' + +# Add an unprivileged user to be used for tests which need it +RUN groupadd -r docker +RUN useradd --create-home --gid docker unprivilegeduser + +VOLUME /var/lib/docker +WORKDIR /go/src/github.com/docker/docker +ENV DOCKER_BUILDTAGS apparmor pkcs11 selinux + +# Let us use a .bashrc file +RUN ln -sfv $PWD/.bashrc ~/.bashrc + +# Register Docker's bash completion. +RUN ln -sv $PWD/contrib/completion/bash/docker /etc/bash_completion.d/docker + +# Get useful and necessary Hub images so we can "docker load" locally instead of pulling +COPY contrib/download-frozen-image-v2.sh /go/src/github.com/docker/docker/contrib/ +RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \ + ppc64le/buildpack-deps:jessie@sha256:902bfe4ef1389f94d143d64516dd50a2de75bca2e66d4a44b1d73f63ddf05dda \ + ppc64le/busybox:latest@sha256:38bb82085248d5a3c24bd7a5dc146f2f2c191e189da0441f1c2ca560e3fc6f1b \ + ppc64le/debian:jessie@sha256:412845f51b6ab662afba71bc7a716e20fdb9b84f185d180d4c7504f8a75c4f91 \ + ppc64le/hello-world:latest@sha256:186a40a9a02ca26df0b6c8acdfb8ac2f3ae6678996a838f977e57fac9d963974 +# see also "hack/make/.ensure-frozen-images" (which needs to be updated any time this list is) + +# Download man page generator +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone --depth 1 -b v1.0.4 https://github.com/cpuguy83/go-md2man.git "$GOPATH/src/github.com/cpuguy83/go-md2man" \ + && git clone --depth 1 -b v1.4 https://github.com/russross/blackfriday.git "$GOPATH/src/github.com/russross/blackfriday" \ + && go get -v -d github.com/cpuguy83/go-md2man \ + && go build -v -o /usr/local/bin/go-md2man github.com/cpuguy83/go-md2man \ + && rm -rf "$GOPATH" + +# Download toml validator +ENV TOMLV_COMMIT 9baf8a8a9f2ed20a8e54160840c492f937eeaf9a +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone https://github.com/BurntSushi/toml.git "$GOPATH/src/github.com/BurntSushi/toml" \ + && (cd "$GOPATH/src/github.com/BurntSushi/toml" && git checkout -q "$TOMLV_COMMIT") \ + && go build -v -o /usr/local/bin/tomlv github.com/BurntSushi/toml/cmd/tomlv \ + && rm -rf "$GOPATH" + +# Build/install the tool for embedding resources in Windows binaries +ENV RSRC_VERSION v2 +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone --depth 1 -b "$RSRC_VERSION" https://github.com/akavel/rsrc.git "$GOPATH/src/github.com/akavel/rsrc" \ + && go build -v -o /usr/local/bin/rsrc github.com/akavel/rsrc \ + && rm -rf "$GOPATH" + +# Install runc +ENV RUNC_COMMIT baf6536d6259209c3edfa2b22237af82942d3dfa +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone git://github.com/opencontainers/runc.git "$GOPATH/src/github.com/opencontainers/runc" \ + && cd "$GOPATH/src/github.com/opencontainers/runc" \ + && git checkout -q "$RUNC_COMMIT" \ + && make static BUILDTAGS="apparmor selinux" \ + && cp runc /usr/local/bin/docker-runc + +# Install containerd +ENV CONTAINERD_COMMIT 9dc2b3273db42c75368988a3885a3afd770069d9 +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone git://github.com/docker/containerd.git "$GOPATH/src/github.com/docker/containerd" \ + && cd "$GOPATH/src/github.com/docker/containerd" \ + && git checkout -q "$CONTAINERD_COMMIT" \ + && make static \ + && cp bin/containerd /usr/local/bin/docker-containerd \ + && cp bin/containerd-shim /usr/local/bin/docker-containerd-shim \ + && cp bin/ctr /usr/local/bin/docker-containerd-ctr + +# Wrap all commands in the "docker-in-docker" script to allow nested containers +ENTRYPOINT ["hack/dind"] + +# Upload docker source +COPY . /go/src/github.com/docker/docker diff --git a/Dockerfile.s390x b/Dockerfile.s390x new file mode 100644 index 00000000..3b2ba3fa --- /dev/null +++ b/Dockerfile.s390x @@ -0,0 +1,206 @@ +# This file describes the standard way to build Docker on s390x, using docker +# +# Usage: +# +# # Assemble the full dev environment. This is slow the first time. +# docker build -t docker -f Dockerfile.s390x . +# +# # Mount your source in an interactive container for quick testing: +# docker run -v `pwd`:/go/src/github.com/docker/docker --privileged -i -t docker bash +# +# # Run the test suite: +# docker run --privileged docker hack/make.sh test +# +# Note: AppArmor used to mess with privileged mode, but this is no longer +# the case. Therefore, you don't have to disable it anymore. +# + +FROM s390x/gcc:5.3 + +# Packaged dependencies +RUN apt-get update && apt-get install -y \ + apparmor \ + aufs-tools \ + automake \ + bash-completion \ + btrfs-tools \ + build-essential \ + createrepo \ + curl \ + dpkg-sig \ + git \ + iptables \ + jq \ + net-tools \ + libapparmor-dev \ + libcap-dev \ + libltdl-dev \ + libsqlite3-dev \ + libsystemd-journal-dev \ + libtool \ + mercurial \ + pkg-config \ + python-dev \ + python-mock \ + python-pip \ + python-websocket \ + xfsprogs \ + tar \ + --no-install-recommends + +# Get lvm2 source for compiling statically +ENV LVM2_VERSION 2.02.103 +RUN mkdir -p /usr/local/lvm2 \ + && curl -fsSL "https://mirrors.kernel.org/sourceware/lvm2/LVM2.${LVM2_VERSION}.tgz" \ + | tar -xzC /usr/local/lvm2 --strip-components=1 +# see https://git.fedorahosted.org/cgit/lvm2.git/refs/tags for release tags + +# fix platform enablement in lvm2 to support s390x properly +RUN set -e \ + && for f in config.guess config.sub; do \ + curl -fsSL -o "/usr/local/lvm2/autoconf/$f" "http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=$f;hb=HEAD"; \ + done +# "arch.c:78:2: error: #error the arch code needs to know about your machine type" + +# Compile and install lvm2 +RUN cd /usr/local/lvm2 \ + && ./configure \ + --build="$(gcc -print-multiarch)" \ + --enable-static_link \ + && make device-mapper \ + && make install_device-mapper +# see https://git.fedorahosted.org/cgit/lvm2.git/tree/INSTALL + +# Note: Go comes from the base image (gccgo, specifically) +# We can't compile Go proper because s390x isn't an officially supported architecture yet. + +ENV PATH /go/bin:$PATH +ENV GOPATH /go:/go/src/github.com/docker/docker/vendor + +# This has been commented out and kept as reference because we don't support compiling with older Go anymore. +# ENV GOFMT_VERSION 1.3.3 +# RUN curl -sSL https://storage.googleapis.com/golang/go${GOFMT_VERSION}.$(go env GOOS)-$(go env GOARCH).tar.gz | tar -C /go/bin -xz --strip-components=2 go/bin/gofmt + +# TODO update this sha when we upgrade to Go 1.5+ +ENV GO_TOOLS_COMMIT 069d2f3bcb68257b627205f0486d6cc69a231ff9 +# Grab Go's cover tool for dead-simple code coverage testing +# Grab Go's vet tool for examining go code to find suspicious constructs +# and help prevent errors that the compiler might not catch +RUN git clone https://github.com/golang/tools.git /go/src/golang.org/x/tools \ + && (cd /go/src/golang.org/x/tools && git checkout -q $GO_TOOLS_COMMIT) \ + && go install -v golang.org/x/tools/cmd/cover \ + && go install -v golang.org/x/tools/cmd/vet +# Grab Go's lint tool +ENV GO_LINT_COMMIT f42f5c1c440621302702cb0741e9d2ca547ae80f +RUN git clone https://github.com/golang/lint.git /go/src/github.com/golang/lint \ + && (cd /go/src/github.com/golang/lint && git checkout -q $GO_LINT_COMMIT) \ + && go install -v github.com/golang/lint/golint + + +# Install registry +ENV REGISTRY_COMMIT ec87e9b6971d831f0eff752ddb54fb64693e51cd +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone https://github.com/docker/distribution.git "$GOPATH/src/github.com/docker/distribution" \ + && (cd "$GOPATH/src/github.com/docker/distribution" && git checkout -q "$REGISTRY_COMMIT") \ + && GOPATH="$GOPATH/src/github.com/docker/distribution/Godeps/_workspace:$GOPATH" \ + go build -o /usr/local/bin/registry-v2 github.com/docker/distribution/cmd/registry \ + && rm -rf "$GOPATH" + +# Install notary server +ENV NOTARY_VERSION docker-v1.11-3 +RUN set -x \ + && export GO15VENDOREXPERIMENT=1 \ + && export GOPATH="$(mktemp -d)" \ + && git clone https://github.com/docker/notary.git "$GOPATH/src/github.com/docker/notary" \ + && (cd "$GOPATH/src/github.com/docker/notary" && git checkout -q "$NOTARY_VERSION") \ + && GOPATH="$GOPATH/src/github.com/docker/notary/vendor:$GOPATH" \ + go build -o /usr/local/bin/notary-server github.com/docker/notary/cmd/notary-server \ + && rm -rf "$GOPATH" + +# Get the "docker-py" source so we can run their integration tests +ENV DOCKER_PY_COMMIT e2878cbcc3a7eef99917adc1be252800b0e41ece +RUN git clone https://github.com/docker/docker-py.git /docker-py \ + && cd /docker-py \ + && git checkout -q $DOCKER_PY_COMMIT \ + && pip install -r test-requirements.txt + +# Set user.email so crosbymichael's in-container merge commits go smoothly +RUN git config --global user.email 'docker-dummy@example.com' + +# Add an unprivileged user to be used for tests which need it +RUN groupadd -r docker +RUN useradd --create-home --gid docker unprivilegeduser + +VOLUME /var/lib/docker +WORKDIR /go/src/github.com/docker/docker +ENV DOCKER_BUILDTAGS apparmor pkcs11 selinux + +# Let us use a .bashrc file +RUN ln -sfv $PWD/.bashrc ~/.bashrc + +# Register Docker's bash completion. +RUN ln -sv $PWD/contrib/completion/bash/docker /etc/bash_completion.d/docker + +# Get useful and necessary Hub images so we can "docker load" locally instead of pulling +COPY contrib/download-frozen-image-v2.sh /go/src/github.com/docker/docker/contrib/ +RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \ + s390x/buildpack-deps:jessie@sha256:4d1381224acaca6c4bfe3604de3af6972083a8558a99672cb6989c7541780099 \ + s390x/busybox:latest@sha256:dd61522c983884a66ed72d60301925889028c6d2d5e0220a8fe1d9b4c6a4f01b \ + s390x/debian:jessie@sha256:b74c863400909eff3c5e196cac9bfd1f6333ce47aae6a38398d87d5875da170a \ + s390x/hello-world:latest@sha256:780d80b3a7677c3788c0d5cd9168281320c8d4a6d9183892d8ee5cdd610f5699 +# see also "hack/make/.ensure-frozen-images" (which needs to be updated any time this list is) + +# Download man page generator +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone --depth 1 -b v1.0.4 https://github.com/cpuguy83/go-md2man.git "$GOPATH/src/github.com/cpuguy83/go-md2man" \ + && git clone --depth 1 -b v1.4 https://github.com/russross/blackfriday.git "$GOPATH/src/github.com/russross/blackfriday" \ + && go get -v -d github.com/cpuguy83/go-md2man \ + && go build -v -o /usr/local/bin/go-md2man github.com/cpuguy83/go-md2man \ + && rm -rf "$GOPATH" + +# Download toml validator +ENV TOMLV_COMMIT 9baf8a8a9f2ed20a8e54160840c492f937eeaf9a +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone https://github.com/BurntSushi/toml.git "$GOPATH/src/github.com/BurntSushi/toml" \ + && (cd "$GOPATH/src/github.com/BurntSushi/toml" && git checkout -q "$TOMLV_COMMIT") \ + && go build -v -o /usr/local/bin/tomlv github.com/BurntSushi/toml/cmd/tomlv \ + && rm -rf "$GOPATH" + +# Build/install the tool for embedding resources in Windows binaries +ENV RSRC_VERSION v2 +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone --depth 1 -b "$RSRC_VERSION" https://github.com/akavel/rsrc.git "$GOPATH/src/github.com/akavel/rsrc" \ + && go build -v -o /usr/local/bin/rsrc github.com/akavel/rsrc \ + && rm -rf "$GOPATH" + +# Install runc +ENV RUNC_COMMIT baf6536d6259209c3edfa2b22237af82942d3dfa +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone git://github.com/opencontainers/runc.git "$GOPATH/src/github.com/opencontainers/runc" \ + && cd "$GOPATH/src/github.com/opencontainers/runc" \ + && git checkout -q "$RUNC_COMMIT" \ + && make static BUILDTAGS="seccomp apparmor selinux" \ + && cp runc /usr/local/bin/docker-runc + +# Install containerd +ENV CONTAINERD_COMMIT 9dc2b3273db42c75368988a3885a3afd770069d9 +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone git://github.com/docker/containerd.git "$GOPATH/src/github.com/docker/containerd" \ + && cd "$GOPATH/src/github.com/docker/containerd" \ + && git checkout -q "$CONTAINERD_COMMIT" \ + && make static \ + && cp bin/containerd /usr/local/bin/docker-containerd \ + && cp bin/containerd-shim /usr/local/bin/docker-containerd-shim \ + && cp bin/ctr /usr/local/bin/docker-containerd-ctr + +# Wrap all commands in the "docker-in-docker" script to allow nested containers +ENTRYPOINT ["hack/dind"] + +# Upload docker source +COPY . /go/src/github.com/docker/docker diff --git a/Dockerfile.simple b/Dockerfile.simple new file mode 100644 index 00000000..2cd461fa --- /dev/null +++ b/Dockerfile.simple @@ -0,0 +1,56 @@ +# docker build -t docker:simple -f Dockerfile.simple . +# docker run --rm docker:simple hack/make.sh dynbinary +# docker run --rm --privileged docker:simple hack/dind hack/make.sh test-unit +# docker run --rm --privileged -v /var/lib/docker docker:simple hack/dind hack/make.sh dynbinary test-integration-cli + +# This represents the bare minimum required to build and test Docker. + +FROM debian:jessie + +# compile and runtime deps +# https://github.com/docker/docker/blob/master/project/PACKAGERS.md#build-dependencies +# https://github.com/docker/docker/blob/master/project/PACKAGERS.md#runtime-dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + btrfs-tools \ + curl \ + gcc \ + git \ + golang \ + libdevmapper-dev \ + libsqlite3-dev \ + \ + ca-certificates \ + e2fsprogs \ + iptables \ + procps \ + xfsprogs \ + xz-utils \ + \ + aufs-tools \ + && rm -rf /var/lib/apt/lists/* + +# Install runc +ENV RUNC_COMMIT baf6536d6259209c3edfa2b22237af82942d3dfa +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone git://github.com/opencontainers/runc.git "$GOPATH/src/github.com/opencontainers/runc" \ + && cd "$GOPATH/src/github.com/opencontainers/runc" \ + && git checkout -q "$RUNC_COMMIT" \ + && make static BUILDTAGS="seccomp apparmor selinux" \ + && cp runc /usr/local/bin/docker-runc + +# Install containerd +ENV CONTAINERD_COMMIT 9dc2b3273db42c75368988a3885a3afd770069d9 +RUN set -x \ + && export GOPATH="$(mktemp -d)" \ + && git clone git://github.com/docker/containerd.git "$GOPATH/src/github.com/docker/containerd" \ + && cd "$GOPATH/src/github.com/docker/containerd" \ + && git checkout -q "$CONTAINERD_COMMIT" \ + && make static \ + && cp bin/containerd /usr/local/bin/docker-containerd \ + && cp bin/containerd-shim /usr/local/bin/docker-containerd-shim \ + && cp bin/ctr /usr/local/bin/docker-containerd-ctr + +ENV AUTO_GOPATH 1 +WORKDIR /usr/src/docker +COPY . /usr/src/docker diff --git a/Dockerfile.windows b/Dockerfile.windows new file mode 100755 index 00000000..d288ab62 --- /dev/null +++ b/Dockerfile.windows @@ -0,0 +1,101 @@ +# This file describes the standard way to build Docker, using a docker container on Windows +# Server 2016 +# +# Usage: +# +# # Assemble the full dev environment. This is slow the first time. Run this from +# # a directory containing the sources you are validating. For example from +# # c:\go\src\github.com\docker\docker +# +# docker build -t docker -f Dockerfile.windows . +# +# +# # Build docker in a container. Run the following from a Windows cmd command prommpt, +# # replacing c:\built with the directory you want the binaries to be placed on the +# # host system. +# +# docker run --rm -v "c:\built:c:\target" docker sh -c 'cd /c/go/src/github.com/docker/docker; hack/make.sh binary; ec=$?; if [ $ec -eq 0 ]; then robocopy /c/go/src/github.com/docker/docker/bundles/$(cat VERSION)/binary /c/target/binary; fi; exit $ec' +# +# Important notes: +# --------------- +# +# 'Start-Sleep' is a deliberate workaround for a current problem on containers in Windows +# Server 2016. It ensures that the network is up and available for when the command is +# network related. This bug is being tracked internally at Microsoft and exists in TP4. +# Generally sleep 1 or 2 is probably enough, but making it 5 to make the build file +# as bullet proof as possible. This isn't a big deal as this only runs the first time. +# +# The cygwin posix utilities from GIT aren't usable interactively as at January 2016. This +# is because they require a console window which isn't present in a container in Windows. +# See the example at the top of this file. Do NOT use -it in that docker run!!! +# +# Don't try to use a volume for passing the source through. The cygwin posix utilities will +# balk at reparse points. Again, see the example at the top of this file on how use a volume +# to get the built binary out of the container. +# +# The steps are minimised dramatically to improve performance (TP4 is slow on commit) + +FROM windowsservercore + +# Environment variable notes: +# - GO_VERSION must consistent with 'Dockerfile' used by Linux'. +# - FROM_DOCKERFILE is used for detection of building within a container. +ENV GO_VERSION=1.5.4 \ + GIT_LOCATION=https://github.com/git-for-windows/git/releases/download/v2.7.2.windows.1/Git-2.7.2-64-bit.exe \ + RSRC_COMMIT=ba14da1f827188454a4591717fff29999010887f \ + GOPATH=C:/go;C:/go/src/github.com/docker/docker/vendor \ + FROM_DOCKERFILE=1 + +WORKDIR c:/ + +# Everything downloaded/installed in one go (better performance, esp on TP4) +RUN \ + setx /M Path "c:\git\cmd;c:\git\bin;c:\git\usr\bin;%Path%;c:\gcc\bin;c:\go\bin" && \ + setx GOROOT "c:\go" && \ + powershell -command \ + $ErrorActionPreference = 'Stop'; \ + Start-Sleep -Seconds 5; \ + Function Download-File([string] $source, [string] $target) { \ + $wc = New-Object net.webclient; $wc.Downloadfile($source, $target) \ + } \ + \ + Write-Host INFO: Downloading git...; \ + Download-File %GIT_LOCATION% gitsetup.exe; \ + \ + Write-Host INFO: Downloading go...; \ + Download-File https://storage.googleapis.com/golang/go%GO_VERSION%.windows-amd64.msi go.msi; \ + \ + Write-Host INFO: Downloading compiler 1 of 3...; \ + Download-File https://raw.githubusercontent.com/jhowardmsft/docker-tdmgcc/master/gcc.zip gcc.zip; \ + \ + Write-Host INFO: Downloading compiler 2 of 3...; \ + Download-File https://raw.githubusercontent.com/jhowardmsft/docker-tdmgcc/master/runtime.zip runtime.zip; \ + \ + Write-Host INFO: Downloading compiler 3 of 3...; \ + Download-File https://raw.githubusercontent.com/jhowardmsft/docker-tdmgcc/master/binutils.zip binutils.zip; \ + \ + Write-Host INFO: Installing git...; \ + Start-Process gitsetup.exe -ArgumentList '/VERYSILENT /SUPPRESSMSGBOXES /CLOSEAPPLICATIONS /DIR=c:\git\' -Wait; \ + \ + Write-Host INFO: Installing go..."; \ + Start-Process msiexec -ArgumentList '-i go.msi -quiet' -Wait; \ + \ + Write-Host INFO: Unzipping compiler...; \ + c:\git\usr\bin\unzip.exe -q -o gcc.zip -d /c/gcc; \ + c:\git\usr\bin\unzip.exe -q -o runtime.zip -d /c/gcc; \ + c:\git\usr\bin\unzip.exe -q -o binutils.zip -d /c/gcc"; \ + \ + Write-Host INFO: Removing interim files; \ + Remove-Item *.zip; \ + Remove-Item go.msi; \ + Remove-Item gitsetup.exe; \ + \ + Write-Host INFO: Cloning and installing RSRC; \ + c:\git\bin\git.exe clone https://github.com/akavel/rsrc.git c:\go\src\github.com\akavel\rsrc; \ + cd \go\src\github.com\akavel\rsrc; c:\git\bin\git.exe checkout -q %RSRC_COMMIT%; c:\go\bin\go.exe install -v; \ + \ + Write-Host INFO: Completed + +# Prepare for building +COPY . /go/src/github.com/docker/docker + diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..8f3fee62 --- /dev/null +++ b/LICENSE @@ -0,0 +1,191 @@ + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2013-2016 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/MAINTAINERS b/MAINTAINERS new file mode 100644 index 00000000..802e288a --- /dev/null +++ b/MAINTAINERS @@ -0,0 +1,255 @@ +# Docker maintainers file +# +# This file describes who runs the docker/docker project and how. +# This is a living document - if you see something out of date or missing, speak up! +# +# It is structured to be consumable by both humans and programs. +# To extract its contents programmatically, use any TOML-compliant +# parser. +# +# This file is compiled into the MAINTAINERS file in docker/opensource. +# +[Org] + + [Org."Core maintainers"] + + # The Core maintainers are the ghostbusters of the project: when there's a problem others + # can't solve, they show up and fix it with bizarre devices and weaponry. + # They have final say on technical implementation and coding style. + # They are ultimately responsible for quality in all its forms: usability polish, + # bugfixes, performance, stability, etc. When ownership can cleanly be passed to + # a subsystem, they are responsible for doing so and holding the + # subsystem maintainers accountable. If ownership is unclear, they are the de facto owners. + + # For each release (including minor releases), a "release captain" is assigned from the + # pool of core maintainers. Rotation is encouraged across all maintainers, to ensure + # the release process is clear and up-to-date. + + people = [ + "aaronlehmann", + "calavera", + "coolljt0725", + "cpuguy83", + "crosbymichael", + "duglin", + "estesp", + "icecrime", + "jhowardmsft", + "jfrazelle", + "lk4d4", + "mhbauer", + "runcom", + "tianon", + "tibor", + "tonistiigi", + "unclejack", + "vbatts", + "vdemeester" + ] + + [Org."Docs maintainers"] + + # TODO Describe the docs maintainers role. + + people = [ + "jamtur01", + "moxiegirl", + "sven", + "thajeztah" + ] + + [Org.Curators] + + # The curators help ensure that incoming issues and pull requests are properly triaged and + # that our various contribution and reviewing processes are respected. With their knowledge of + # the repository activity, they can also guide contributors to relevant material or + # discussions. + # + # They are neither code nor docs reviewers, so they are never expected to merge. They can + # however: + # - close an issue or pull request when it's an exact duplicate + # - close an issue or pull request when it's inappropriate or off-topic + + people = [ + "programmerq", + "thajeztah" + ] + + [Org.Alumni] + + # This list contains maintainers that are no longer active on the project. + # It is thanks to these people that the project has become what it is today. + # Thank you! + + people = [ + # As a maintainer, Erik was responsible for the "builder", and + # started the first designs for the new networking model in + # Docker. Erik is now working on all kinds of plugins for Docker + # (https://github.com/contiv) and various open source projects + # in his own repository https://github.com/erikh. You may + # still stumble into him in our issue tracker, or on IRC. + "erikh", + + # Victor is one of the earliest contributors to Docker, having worked on the + # project when it was still "dotCloud" in April 2013. He's been responsible + # for multiple releases (https://github.com/docker/docker/pulls?q=is%3Apr+bump+in%3Atitle+author%3Avieux), + # and up until today (2015), our number 2 contributor. Although he's no longer + # a maintainer for the Docker "Engine", he's still actively involved in other + # Docker projects, and most likely can be found in the Docker Swarm repository, + # for which he's a core maintainer. + "vieux", + + # Vishnu became a maintainer to help out on the daemon codebase and + # libcontainer integration. He's currently involved in the + # Open Containers Initiative, working on the specifications, + # besides his work on cAdvisor and Kubernetes for Google. + "vishh" + ] + +[people] + +# A reference list of all people associated with the project. +# All other sections should refer to people by their canonical key +# in the people section. + + # ADD YOURSELF HERE IN ALPHABETICAL ORDER + + [people.aaronlehmann] + Name = "Aaron Lehmann" + Email = "aaron.lehmann@docker.com" + GitHub = "aaronlehmann" + + [people.calavera] + Name = "David Calavera" + Email = "david.calavera@gmail.com" + GitHub = "calavera" + + [people.coolljt0725] + Name = "Lei Jitang" + Email = "leijitang@huawei.com" + GitHub = "coolljt0725" + + [people.cpuguy83] + Name = "Brian Goff" + Email = "cpuguy83@gmail.com" + Github = "cpuguy83" + + [people.crosbymichael] + Name = "Michael Crosby" + Email = "crosbymichael@gmail.com" + GitHub = "crosbymichael" + + [people.duglin] + Name = "Doug Davis" + Email = "dug@us.ibm.com" + GitHub = "duglin" + + [people.erikh] + Name = "Erik Hollensbe" + Email = "erik@docker.com" + GitHub = "erikh" + + [people.estesp] + Name = "Phil Estes" + Email = "estesp@linux.vnet.ibm.com" + GitHub = "estesp" + + [people.icecrime] + Name = "Arnaud Porterie" + Email = "arnaud@docker.com" + GitHub = "icecrime" + + [people.jamtur01] + Name = "James Turnbull" + Email = "james@lovedthanlost.net" + GitHub = "jamtur01" + + [people.jhowardmsft] + Name = "John Howard" + Email = "jhoward@microsoft.com" + GitHub = "jhowardmsft" + + [people.jfrazelle] + Name = "Jessie Frazelle" + Email = "jess@linux.com" + GitHub = "jfrazelle" + + [people.lk4d4] + Name = "Alexander Morozov" + Email = "lk4d4@docker.com" + GitHub = "lk4d4" + + [people.mhbauer] + Name = "Morgan Bauer" + Email = "mbauer@us.ibm.com" + GitHub = "mhbauer" + + [people.moxiegirl] + Name = "Mary Anthony" + Email = "mary.anthony@docker.com" + GitHub = "moxiegirl" + + [people.programmerq] + Name = "Jeff Anderson" + Email = "jeff@docker.com" + GitHub = "programmerq" + + [people.runcom] + Name = "Antonio Murdaca" + Email = "runcom@redhat.com" + GitHub = "runcom" + + [people.shykes] + Name = "Solomon Hykes" + Email = "solomon@docker.com" + GitHub = "shykes" + + [people.sven] + Name = "Sven Dowideit" + Email = "SvenDowideit@home.org.au" + GitHub = "SvenDowideit" + + [people.thajeztah] + Name = "Sebastiaan van Stijn" + Email = "github@gone.nl" + GitHub = "thaJeztah" + + [people.tianon] + Name = "Tianon Gravi" + Email = "admwiggin@gmail.com" + GitHub = "tianon" + + [people.tibor] + Name = "Tibor Vass" + Email = "tibor@docker.com" + GitHub = "tiborvass" + + [people.tonistiigi] + Name = "Tõnis Tiigi" + Email = "tonis@docker.com" + GitHub = "tonistiigi" + + [people.unclejack] + Name = "Cristian Staretu" + Email = "cristian.staretu@gmail.com" + GitHub = "unclejack" + + [people.vbatts] + Name = "Vincent Batts" + Email = "vbatts@redhat.com" + GitHub = "vbatts" + + [people.vdemeester] + Name = "Vincent Demeester" + Email = "vincent@sbr.pm" + GitHub = "vdemeester" + + [people.vieux] + Name = "Victor Vieux" + Email = "vieux@docker.com" + GitHub = "vieux" + + [people.vishh] + Name = "Vishnu Kannan" + Email = "vishnuk@google.com" + GitHub = "vishh" diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..e0ba358c --- /dev/null +++ b/Makefile @@ -0,0 +1,106 @@ +.PHONY: all binary build cross default docs docs-build docs-shell shell test test-docker-py test-integration-cli test-unit validate + +# get OS/Arch of docker engine +DOCKER_OSARCH := $(shell bash -c 'source hack/make/.detect-daemon-osarch && echo $${DOCKER_ENGINE_OSARCH:-$$DOCKER_CLIENT_OSARCH}') +DOCKERFILE := $(shell bash -c 'source hack/make/.detect-daemon-osarch && echo $${DOCKERFILE}') + +# env vars passed through directly to Docker's build scripts +# to allow things like `make DOCKER_CLIENTONLY=1 binary` easily +# `docs/sources/contributing/devenvironment.md ` and `project/PACKAGERS.md` have some limited documentation of some of these +DOCKER_ENVS := \ + -e BUILDFLAGS \ + -e KEEPBUNDLE \ + -e DOCKER_BUILD_GOGC \ + -e DOCKER_BUILD_PKGS \ + -e DOCKER_CLIENTONLY \ + -e DOCKER_DEBUG \ + -e DOCKER_EXPERIMENTAL \ + -e DOCKER_GRAPHDRIVER \ + -e DOCKER_INCREMENTAL_BINARY \ + -e DOCKER_REMAP_ROOT \ + -e DOCKER_STORAGE_OPTS \ + -e DOCKER_USERLANDPROXY \ + -e TESTDIRS \ + -e TESTFLAGS \ + -e TIMEOUT +# note: we _cannot_ add "-e DOCKER_BUILDTAGS" here because even if it's unset in the shell, that would shadow the "ENV DOCKER_BUILDTAGS" set in our Dockerfile, which is very important for our official builds + +# to allow `make BIND_DIR=. shell` or `make BIND_DIR= test` +# (default to no bind mount if DOCKER_HOST is set) +# note: BINDDIR is supported for backwards-compatibility here +BIND_DIR := $(if $(BINDDIR),$(BINDDIR),$(if $(DOCKER_HOST),,bundles)) +DOCKER_MOUNT := $(if $(BIND_DIR),-v "$(CURDIR)/$(BIND_DIR):/go/src/github.com/docker/docker/$(BIND_DIR)") + +# This allows the test suite to be able to run without worrying about the underlying fs used by the container running the daemon (e.g. aufs-on-aufs), so long as the host running the container is running a supported fs. +# The volume will be cleaned up when the container is removed due to `--rm`. +# Note that `BIND_DIR` will already be set to `bundles` if `DOCKER_HOST` is not set (see above BIND_DIR line), in such case this will do nothing since `DOCKER_MOUNT` will already be set. +DOCKER_MOUNT := $(if $(DOCKER_MOUNT),$(DOCKER_MOUNT),-v "/go/src/github.com/docker/docker/bundles") + +GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null) +DOCKER_IMAGE := docker-dev$(if $(GIT_BRANCH),:$(GIT_BRANCH)) +DOCKER_DOCS_IMAGE := docker-docs$(if $(GIT_BRANCH),:$(GIT_BRANCH)) + +DOCKER_FLAGS := docker run --rm -i --privileged $(DOCKER_ENVS) $(DOCKER_MOUNT) + +# 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. +INTERACTIVE := $(shell [ -t 0 ] && echo 1 || echo 0) +ifeq ($(INTERACTIVE), 1) + DOCKER_FLAGS += -t +endif + +DOCKER_RUN_DOCKER := $(DOCKER_FLAGS) "$(DOCKER_IMAGE)" + +default: binary + +all: build + $(DOCKER_RUN_DOCKER) hack/make.sh + +binary: build + $(DOCKER_RUN_DOCKER) hack/make.sh binary + +build: bundles + docker build ${DOCKER_BUILD_ARGS} -t "$(DOCKER_IMAGE)" -f "$(DOCKERFILE)" . + +bundles: + mkdir bundles + +cross: build + $(DOCKER_RUN_DOCKER) hack/make.sh dynbinary binary cross + +win: build + $(DOCKER_RUN_DOCKER) hack/make.sh win + +tgz: build + $(DOCKER_RUN_DOCKER) hack/make.sh dynbinary binary cross tgz + +deb: build + $(DOCKER_RUN_DOCKER) hack/make.sh dynbinary build-deb + +docs: + $(MAKE) -C docs docs + +gccgo: build + $(DOCKER_RUN_DOCKER) hack/make.sh gccgo + +rpm: build + $(DOCKER_RUN_DOCKER) hack/make.sh dynbinary build-rpm + +shell: build + $(DOCKER_RUN_DOCKER) bash + +test: build + $(DOCKER_RUN_DOCKER) hack/make.sh dynbinary cross test-unit test-integration-cli test-docker-py + +test-docker-py: build + $(DOCKER_RUN_DOCKER) hack/make.sh dynbinary test-docker-py + +test-integration-cli: build + $(DOCKER_RUN_DOCKER) hack/make.sh dynbinary test-integration-cli + +test-unit: build + $(DOCKER_RUN_DOCKER) hack/make.sh test-unit + +validate: build + $(DOCKER_RUN_DOCKER) hack/make.sh validate-dco validate-default-seccomp validate-gofmt validate-pkg validate-lint validate-test validate-toml validate-vet validate-vendor diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..8a37c1c7 --- /dev/null +++ b/NOTICE @@ -0,0 +1,19 @@ +Docker +Copyright 2012-2016 Docker, Inc. + +This product includes software developed at Docker, Inc. (https://www.docker.com). + +This product contains software (https://github.com/kr/pty) developed +by Keith Rarick, licensed under the MIT License. + +The following is courtesy of our legal counsel: + + +Use and transfer of Docker may be subject to certain restrictions by the +United States and other governments. +It is your responsibility to ensure that your use and/or transfer does not +violate applicable laws. + +For more information, please see https://www.bis.doc.gov + +See also https://www.apache.org/dev/crypto.html and/or seek legal counsel. diff --git a/README.md b/README.md new file mode 100644 index 00000000..aa9cc0ea --- /dev/null +++ b/README.md @@ -0,0 +1,301 @@ +Docker: the container engine [![Release](https://img.shields.io/github/release/docker/docker.svg)](https://github.com/docker/docker/releases/latest) +============================ + +Docker is an open source project to pack, ship and run any application +as a lightweight container. + +Docker containers are both *hardware-agnostic* and *platform-agnostic*. +This means they can run anywhere, from your laptop to the largest +cloud compute instance and everything in between - and they don't require +you to use a particular language, framework or packaging system. That +makes them great building blocks for deploying and scaling web apps, +databases, and backend services without depending on a particular stack +or provider. + +Docker began as an open-source implementation of the deployment engine which +powers [dotCloud](https://www.dotcloud.com), a popular Platform-as-a-Service. +It benefits directly from the experience accumulated over several years +of large-scale operation and support of hundreds of thousands of +applications and databases. + +![](docs/static_files/docker-logo-compressed.png "Docker") + +## Security Disclosure + +Security is very important to us. If you have any issue regarding security, +please disclose the information responsibly by sending an email to +security@docker.com and not by creating a github issue. + +## Better than VMs + +A common method for distributing applications and sandboxing their +execution is to use virtual machines, or VMs. Typical VM formats are +VMware's vmdk, Oracle VirtualBox's vdi, and Amazon EC2's ami. In theory +these formats should allow every developer to automatically package +their application into a "machine" for easy distribution and deployment. +In practice, that almost never happens, for a few reasons: + + * *Size*: VMs are very large which makes them impractical to store + and transfer. + * *Performance*: running VMs consumes significant CPU and memory, + which makes them impractical in many scenarios, for example local + development of multi-tier applications, and large-scale deployment + of cpu and memory-intensive applications on large numbers of + machines. + * *Portability*: competing VM environments don't play well with each + other. Although conversion tools do exist, they are limited and + add even more overhead. + * *Hardware-centric*: VMs were designed with machine operators in + mind, not software developers. As a result, they offer very + limited tooling for what developers need most: building, testing + and running their software. For example, VMs offer no facilities + for application versioning, monitoring, configuration, logging or + service discovery. + +By contrast, Docker relies on a different sandboxing method known as +*containerization*. Unlike traditional virtualization, containerization +takes place at the kernel level. Most modern operating system kernels +now support the primitives necessary for containerization, including +Linux with [openvz](https://openvz.org), +[vserver](http://linux-vserver.org) and more recently +[lxc](https://linuxcontainers.org/), Solaris with +[zones](https://docs.oracle.com/cd/E26502_01/html/E29024/preface-1.html#scrolltoc), +and FreeBSD with +[Jails](https://www.freebsd.org/doc/handbook/jails.html). + +Docker builds on top of these low-level primitives to offer developers a +portable format and runtime environment that solves all four problems. +Docker containers are small (and their transfer can be optimized with +layers), they have basically zero memory and cpu overhead, they are +completely portable, and are designed from the ground up with an +application-centric design. + +Perhaps best of all, because Docker operates at the OS level, it can still be +run inside a VM! + +## Plays well with others + +Docker does not require you to buy into a particular programming +language, framework, packaging system, or configuration language. + +Is your application a Unix process? Does it use files, tcp connections, +environment variables, standard Unix streams and command-line arguments +as inputs and outputs? Then Docker can run it. + +Can your application's build be expressed as a sequence of such +commands? Then Docker can build it. + +## Escape dependency hell + +A common problem for developers is the difficulty of managing all +their application's dependencies in a simple and automated way. + +This is usually difficult for several reasons: + + * *Cross-platform dependencies*. Modern applications often depend on + a combination of system libraries and binaries, language-specific + packages, framework-specific modules, internal components + developed for another project, etc. These dependencies live in + different "worlds" and require different tools - these tools + typically don't work well with each other, requiring awkward + custom integrations. + + * *Conflicting dependencies*. Different applications may depend on + different versions of the same dependency. Packaging tools handle + these situations with various degrees of ease - but they all + handle them in different and incompatible ways, which again forces + the developer to do extra work. + + * *Custom dependencies*. A developer may need to prepare a custom + version of their application's dependency. Some packaging systems + can handle custom versions of a dependency, others can't - and all + of them handle it differently. + + +Docker solves the problem of dependency hell by giving the developer a simple +way to express *all* their application's dependencies in one place, while +streamlining the process of assembling them. If this makes you think of +[XKCD 927](https://xkcd.com/927/), don't worry. Docker doesn't +*replace* your favorite packaging systems. It simply orchestrates +their use in a simple and repeatable way. How does it do that? With +layers. + +Docker defines a build as running a sequence of Unix commands, one +after the other, in the same container. Build commands modify the +contents of the container (usually by installing new files on the +filesystem), the next command modifies it some more, etc. Since each +build command inherits the result of the previous commands, the +*order* in which the commands are executed expresses *dependencies*. + +Here's a typical Docker build process: + +```bash +FROM ubuntu:12.04 +RUN apt-get update && apt-get install -y python python-pip curl +RUN curl -sSL https://github.com/shykes/helloflask/archive/master.tar.gz | tar -xzv +RUN cd helloflask-master && pip install -r requirements.txt +``` + +Note that Docker doesn't care *how* dependencies are built - as long +as they can be built by running a Unix command in a container. + + +Getting started +=============== + +Docker can be installed either on your computer for building applications or +on servers for running them. To get started, [check out the installation +instructions in the +documentation](https://docs.docker.com/engine/installation/). + +We also offer an [interactive tutorial](https://www.docker.com/tryit/) +for quickly learning the basics of using Docker. + +Usage examples +============== + +Docker can be used to run short-lived commands, long-running daemons +(app servers, databases, etc.), interactive shell sessions, etc. + +You can find a [list of real-world +examples](https://docs.docker.com/engine/examples/) in the +documentation. + +Under the hood +-------------- + +Under the hood, Docker is built on the following components: + +* The + [cgroups](https://www.kernel.org/doc/Documentation/cgroup-v1/cgroups.txt) + and + [namespaces](http://man7.org/linux/man-pages/man7/namespaces.7.html) + capabilities of the Linux kernel +* The [Go](https://golang.org) programming language +* The [Docker Image Specification](https://github.com/docker/docker/blob/master/image/spec/v1.md) +* The [Libcontainer Specification](https://github.com/opencontainers/runc/blob/master/libcontainer/SPEC.md) + +Contributing to Docker [![GoDoc](https://godoc.org/github.com/docker/docker?status.svg)](https://godoc.org/github.com/docker/docker) +====================== + +| **Master** (Linux) | **Experimental** (linux) | **Windows** | **FreeBSD** | +|------------------|----------------------|---------|---------| +| [![Jenkins Build Status](https://jenkins.dockerproject.org/view/Docker/job/Docker%20Master/badge/icon)](https://jenkins.dockerproject.org/view/Docker/job/Docker%20Master/) | [![Jenkins Build Status](https://jenkins.dockerproject.org/view/Docker/job/Docker%20Master%20%28experimental%29/badge/icon)](https://jenkins.dockerproject.org/view/Docker/job/Docker%20Master%20%28experimental%29/) | [![Build Status](http://jenkins.dockerproject.org/job/Docker%20Master%20(windows)/badge/icon)](http://jenkins.dockerproject.org/job/Docker%20Master%20(windows)/) | [![Build Status](http://jenkins.dockerproject.org/job/Docker%20Master%20(freebsd)/badge/icon)](http://jenkins.dockerproject.org/job/Docker%20Master%20(freebsd)/) | + +Want to hack on Docker? Awesome! We have [instructions to help you get +started contributing code or documentation](https://docs.docker.com/opensource/project/who-written-for/). + +These instructions are probably not perfect, please let us know if anything +feels wrong or incomplete. Better yet, submit a PR and improve them yourself. + +Getting the development builds +============================== + +Want to run Docker from a master build? You can download +master builds at [master.dockerproject.org](https://master.dockerproject.org). +They are updated with each commit merged into the master branch. + +Don't know how to use that super cool new feature in the master build? Check +out the master docs at +[docs.master.dockerproject.org](http://docs.master.dockerproject.org). + +How the project is run +====================== + +Docker is a very, very active project. If you want to learn more about how it is run, +or want to get more involved, the best place to start is [the project directory](https://github.com/docker/docker/tree/master/project). + +We are always open to suggestions on process improvements, and are always looking for more maintainers. + +### Talking to other Docker users and contributors + + + + + + + + + + + + + + + + + + + + +
Internet Relay Chat (IRC) +

+ IRC is a direct line to our most knowledgeable Docker users; we have + both the #docker and #docker-dev group on + irc.freenode.net. + IRC is a rich chat protocol but it can overwhelm new users. You can search + our chat archives. +

+ Read our IRC quickstart guide for an easy way to get started. +
Google Groups + There are two groups. + Docker-user + is for people using Docker containers. + The docker-dev + group is for contributors and other people contributing to the Docker + project. + You can join them without an google account by sending an email to e.g. "docker-user+subscribe@googlegroups.com". + After receiving the join-request message, you can simply reply to that to confirm the subscribtion. +
Twitter + You can follow Docker's Twitter feed + to get updates on our products. You can also tweet us questions or just + share blogs or stories. +
Stack Overflow + Stack Overflow has over 7000 Docker questions listed. We regularly + monitor Docker questions + and so do many other knowledgeable Docker users. +
+ +### Legal + +*Brought to you courtesy of our legal counsel. For more context, +please see the [NOTICE](https://github.com/docker/docker/blob/master/NOTICE) document in this repo.* + +Use and transfer of Docker may be subject to certain restrictions by the +United States and other governments. + +It is your responsibility to ensure that your use and/or transfer does not +violate applicable laws. + +For more information, please see https://www.bis.doc.gov + + +Licensing +========= +Docker is licensed under the Apache License, Version 2.0. See +[LICENSE](https://github.com/docker/docker/blob/master/LICENSE) for the full +license text. + +Other Docker Related Projects +============================= +There are a number of projects under development that are based on Docker's +core technology. These projects expand the tooling built around the +Docker platform to broaden its application and utility. + +* [Docker Registry](https://github.com/docker/distribution): Registry +server for Docker (hosting/delivery of repositories and images) +* [Docker Machine](https://github.com/docker/machine): Machine management +for a container-centric world +* [Docker Swarm](https://github.com/docker/swarm): A Docker-native clustering +system +* [Docker Compose](https://github.com/docker/compose) (formerly Fig): +Define and run multi-container apps +* [Kitematic](https://github.com/docker/kitematic): The easiest way to use +Docker on Mac and Windows + +If you know of another project underway that should be listed here, please help +us keep this list up-to-date by submitting a PR. + +Awesome-Docker +============== +You can find more projects, tools and articles related to Docker on the [awesome-docker list](https://github.com/veggiemonk/awesome-docker). Add your project there. diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 00000000..514fdb74 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,140 @@ +Docker Engine Roadmap +===================== + +### How should I use this document? + +This document provides description of items that the project decided to prioritize. This should +serve as a reference point for Docker contributors to understand where the project is going, and +help determine if a contribution could be conflicting with some longer terms plans. + +The fact that a feature isn't listed here doesn't mean that a patch for it will automatically be +refused (except for those mentioned as "frozen features" below)! We are always happy to receive +patches for new cool features we haven't thought about, or didn't judge priority. Please however +understand that such patches might take longer for us to review. + +### How can I help? + +Short term objectives are listed in the [wiki](https://github.com/docker/docker/wiki) and described +in [Issues](https://github.com/docker/docker/issues?q=is%3Aopen+is%3Aissue+label%3Aroadmap). Our +goal is to split down the workload in such way that anybody can jump in and help. Please comment on +issues if you want to take it to avoid duplicating effort! Similarly, if a maintainer is already +assigned on an issue you'd like to participate in, pinging him on IRC or GitHub to offer your help is +the best way to go. + +### How can I add something to the roadmap? + +The roadmap process is new to the Docker Engine: we are only beginning to structure and document the +project objectives. Our immediate goal is to be more transparent, and work with our community to +focus our efforts on fewer prioritized topics. + +We hope to offer in the near future a process allowing anyone to propose a topic to the roadmap, but +we are not quite there yet. For the time being, the BDFL remains the keeper of the roadmap, and we +won't be accepting pull requests adding or removing items from this file. + +# 1. Features and refactoring + +## 1.1 Runtime improvements + +We recently introduced [`runC`](https://runc.io) as a standalone low-level tool for container +execution. The initial goal was to integrate runC as a replacement in the Engine for the traditional +default libcontainer `execdriver`, but the Engine internals were not ready for this. + +As runC continued evolving, and the OCI specification along with it, we created +[`containerd`](https://containerd.tools/), a daemon to control and monitor multiple `runC`. This is +the new target for Engine integration, as it can entirely replace the whole `execdriver` +architecture, and container monitoring along with it. + +Docker Engine will rely on a long-running `containerd` companion daemon for all container execution +related operations. This could open the door in the future for Engine restarts without interrupting +running containers. + +## 1.2 Plugins improvements + +Docker Engine 1.7.0 introduced plugin support, initially for the use cases of volumes and networks +extensions. The plugin infrastructure was kept minimal as we were collecting use cases and real +world feedback before optimizing for any particular workflow. + +In the future, we'd like plugins to become first class citizens, and encourage an ecosystem of +plugins. This implies in particular making it trivially easy to distribute plugins as containers +through any Registry instance, as well as solving the commonly heard pain points of plugins needing +to be treated as somewhat special (being active at all time, started before any other user +containers, and not as easily dismissed). + +## 1.3 Internal decoupling + +A lot of work has been done in trying to decouple the Docker Engine's internals. In particular, the +API implementation has been refactored and ongoing work is happening to move the code to a separate +repository ([`docker/engine-api`](https://github.com/docker/engine-api)), and the Builder side of +the daemon is now [fully independent](https://github.com/docker/docker/tree/master/builder) while +still residing in the same repository. + +We are exploring ways to go further with that decoupling, capitalizing on the work introduced by the +runtime renovation and plugins improvement efforts. Indeed, the combination of `containerd` support +with the concept of "special" containers opens the door for bootstrapping more Engine internals +using the same facilities. + +## 1.4 Cluster capable Engine + +The community has been pushing for a more cluster capable Docker Engine, and a huge effort was spent +adding features such as multihost networking, and node discovery down at the Engine level. Yet, the +Engine is currently incapable of taking scheduling decisions alone, and continues relying on Swarm +for that. + +We plan to complete this effort and make Engine fully cluster capable. Multiple instances of the +Docker Engine being already capable of discovering each other and establish overlay networking for +their container to communicate, the next step is for a given Engine to gain ability to dispatch work +to another node in the cluster. This will be introduced in a backward compatible way, such that a +`docker run` invocation on a particular node remains fully deterministic. + +# 2 Frozen features + +## 2.1 Docker exec + +We won't accept patches expanding the surface of `docker exec`, which we intend to keep as a +*debugging* feature, as well as being strongly dependent on the Runtime ingredient effort. + +## 2.2 Dockerfile syntax + +The Dockerfile syntax as we know it is simple, and has proven successful in supporting all our +[official images](https://github.com/docker-library/official-images). Although this is *not* a +definitive move, we temporarily won't accept more patches to the Dockerfile syntax for several +reasons: + + - Long term impact of syntax changes is a sensitive matter that require an amount of attention the + volume of Engine codebase and activity today doesn't allow us to provide. + - Allowing the Builder to be implemented as a separate utility consuming the Engine's API will + open the door for many possibilities, such as offering alternate syntaxes or DSL for existing + languages without cluttering the Engine's codebase. + - A standalone Builder will also offer the opportunity for a better dedicated group of maintainers + to own the Dockerfile syntax and decide collectively on the direction to give it. + - Our experience with official images tend to show that no new instruction or syntax expansion is + *strictly* necessary for the majority of use cases, and although we are aware many things are + still lacking for many, we cannot make it a priority yet for the above reasons. + +Again, this is not about saying that the Dockerfile syntax is done, it's about making choices about +what we want to do first! + +## 2.3 Remote Registry Operations + +A large amount of work is ongoing in the area of image distribution and provenance. This includes +moving to the V2 Registry API and heavily refactoring the code that powers these features. The +desired result is more secure, reliable and easier to use image distribution. + +Part of the problem with this part of the code base is the lack of a stable and flexible interface. +If new features are added that access the registry without solidifying these interfaces, achieving +feature parity will continue to be elusive. While we get a handle on this situation, we are imposing +a moratorium on new code that accesses the Registry API in commands that don't already make remote +calls. + +Currently, only the following commands cause interaction with a remote registry: + + - push + - pull + - run + - build + - search + - login + +In the interest of stabilizing the registry access model during this ongoing work, we are not +accepting additions to other commands that will cause remote interaction with the Registry API. This +moratorium will lift when the goals of the distribution project have been met. diff --git a/VENDORING.md b/VENDORING.md new file mode 100644 index 00000000..c6bb5086 --- /dev/null +++ b/VENDORING.md @@ -0,0 +1,45 @@ +# Vendoring policies + +This document outlines recommended Vendoring policies for Docker repositories. +(Example, libnetwork is a Docker repo and logrus is not.) + +## Vendoring using tags + +Commit ID based vendoring provides little/no information about the updates +vendored. To fix this, vendors will now require that repositories use annotated +tags along with commit ids to snapshot commits. Annotated tags by themselves +are not sufficient, since the same tag can be force updated to reference +different commits. + +Each tag should: +- Follow Semantic Versioning rules (refer to section on "Semantic Versioning") +- Have a corresponding entry in the change tracking document. + +Each repo should: +- Have a change tracking document between tags/releases. Ex: CHANGELOG.md, +github releases file. + +The goal here is for consuming repos to be able to use the tag version and +changelog updates to determine whether the vendoring will cause any breaking or +backward incompatible changes. This also means that repos can specify having +dependency on a package of a specific version or greater up to the next major +release, without encountering breaking changes. + +## Semantic Versioning +Annotated version tags should follow Schema Versioning policies. +According to http://semver.org: + +"Given a version number MAJOR.MINOR.PATCH, increment the: + MAJOR version when you make incompatible API changes, + MINOR version when you add functionality in a backwards-compatible manner, and + PATCH version when you make backwards-compatible bug fixes. +Additional labels for pre-release and build metadata are available as extensions +to the MAJOR.MINOR.PATCH format." + +## Vendoring cadence +In order to avoid huge vendoring changes, it is recommended to have a regular +cadence for vendoring updates. eg. monthly. + +## Pre-merge vendoring tests +All related repos will be vendored into docker/docker. +CI on docker/docker should catch any breaking changes involving multiple repos. diff --git a/VERSION b/VERSION new file mode 100644 index 00000000..ca717669 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.11.2 diff --git a/api/README.md b/api/README.md new file mode 100644 index 00000000..453f61a1 --- /dev/null +++ b/api/README.md @@ -0,0 +1,5 @@ +This directory contains code pertaining to the Docker API: + + - Used by the docker client when communicating with the docker daemon + + - Used by third party tools wishing to interface with the docker daemon diff --git a/api/client/attach.go b/api/client/attach.go new file mode 100644 index 00000000..e89644d6 --- /dev/null +++ b/api/client/attach.go @@ -0,0 +1,109 @@ +package client + +import ( + "fmt" + "io" + + "golang.org/x/net/context" + + "github.com/Sirupsen/logrus" + Cli "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/signal" + "github.com/docker/engine-api/types" +) + +// CmdAttach attaches to a running container. +// +// Usage: docker attach [OPTIONS] CONTAINER +func (cli *DockerCli) CmdAttach(args ...string) error { + cmd := Cli.Subcmd("attach", []string{"CONTAINER"}, Cli.DockerCommands["attach"].Description, true) + noStdin := cmd.Bool([]string{"-no-stdin"}, false, "Do not attach STDIN") + proxy := cmd.Bool([]string{"-sig-proxy"}, true, "Proxy all received signals to the process") + detachKeys := cmd.String([]string{"-detach-keys"}, "", "Override the key sequence for detaching a container") + + cmd.Require(flag.Exact, 1) + + cmd.ParseFlags(args, true) + + c, err := cli.client.ContainerInspect(context.Background(), cmd.Arg(0)) + if err != nil { + return err + } + + if !c.State.Running { + return fmt.Errorf("You cannot attach to a stopped container, start it first") + } + + if c.State.Paused { + return fmt.Errorf("You cannot attach to a paused container, unpause it first") + } + + if err := cli.CheckTtyInput(!*noStdin, c.Config.Tty); err != nil { + return err + } + + if *detachKeys != "" { + cli.configFile.DetachKeys = *detachKeys + } + + options := types.ContainerAttachOptions{ + ContainerID: cmd.Arg(0), + Stream: true, + Stdin: !*noStdin && c.Config.OpenStdin, + Stdout: true, + Stderr: true, + DetachKeys: cli.configFile.DetachKeys, + } + + var in io.ReadCloser + if options.Stdin { + in = cli.in + } + + if *proxy && !c.Config.Tty { + sigc := cli.forwardAllSignals(options.ContainerID) + defer signal.StopCatch(sigc) + } + + resp, err := cli.client.ContainerAttach(context.Background(), options) + if err != nil { + return err + } + defer resp.Close() + if in != nil && c.Config.Tty { + if err := cli.setRawTerminal(); err != nil { + return err + } + defer cli.restoreTerminal(in) + } + + if c.Config.Tty && cli.isTerminalOut { + height, width := cli.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. + cli.resizeTtyTo(cmd.Arg(0), 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 := cli.monitorTtySize(cmd.Arg(0), false); err != nil { + logrus.Debugf("Error monitoring TTY size: %s", err) + } + } + + if err := cli.holdHijackedConnection(c.Config.Tty, in, cli.out, cli.err, resp); err != nil { + return err + } + + _, status, err := getExitCode(cli, options.ContainerID) + if err != nil { + return err + } + if status != 0 { + return Cli.StatusError{StatusCode: status} + } + + return nil +} diff --git a/api/client/build.go b/api/client/build.go new file mode 100644 index 00000000..300d0cd9 --- /dev/null +++ b/api/client/build.go @@ -0,0 +1,399 @@ +package client + +import ( + "archive/tar" + "bufio" + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "runtime" + + "golang.org/x/net/context" + + "github.com/docker/docker/api" + "github.com/docker/docker/builder" + "github.com/docker/docker/builder/dockerignore" + Cli "github.com/docker/docker/cli" + "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/fileutils" + "github.com/docker/docker/pkg/jsonmessage" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/progress" + "github.com/docker/docker/pkg/streamformatter" + "github.com/docker/docker/pkg/urlutil" + "github.com/docker/docker/reference" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/container" + "github.com/docker/go-units" +) + +type translatorFunc func(reference.NamedTagged) (reference.Canonical, error) + +// CmdBuild builds a new image from the source code at a given path. +// +// If '-' is provided instead of a path or URL, Docker will build an image from either a Dockerfile or tar archive read from STDIN. +// +// Usage: docker build [OPTIONS] PATH | URL | - +func (cli *DockerCli) CmdBuild(args ...string) error { + cmd := Cli.Subcmd("build", []string{"PATH | URL | -"}, Cli.DockerCommands["build"].Description, true) + flTags := opts.NewListOpts(validateTag) + cmd.Var(&flTags, []string{"t", "-tag"}, "Name and optionally a tag in the 'name:tag' format") + suppressOutput := cmd.Bool([]string{"q", "-quiet"}, false, "Suppress the build output and print image ID on success") + noCache := cmd.Bool([]string{"-no-cache"}, false, "Do not use cache when building the image") + rm := cmd.Bool([]string{"-rm"}, true, "Remove intermediate containers after a successful build") + forceRm := cmd.Bool([]string{"-force-rm"}, false, "Always remove intermediate containers") + pull := cmd.Bool([]string{"-pull"}, false, "Always attempt to pull a newer version of the image") + dockerfileName := cmd.String([]string{"f", "-file"}, "", "Name of the Dockerfile (Default is 'PATH/Dockerfile')") + flMemoryString := cmd.String([]string{"m", "-memory"}, "", "Memory limit") + flMemorySwap := cmd.String([]string{"-memory-swap"}, "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") + flShmSize := cmd.String([]string{"-shm-size"}, "", "Size of /dev/shm, default value is 64MB") + flCPUShares := cmd.Int64([]string{"#c", "-cpu-shares"}, 0, "CPU shares (relative weight)") + flCPUPeriod := cmd.Int64([]string{"-cpu-period"}, 0, "Limit the CPU CFS (Completely Fair Scheduler) period") + flCPUQuota := cmd.Int64([]string{"-cpu-quota"}, 0, "Limit the CPU CFS (Completely Fair Scheduler) quota") + flCPUSetCpus := cmd.String([]string{"-cpuset-cpus"}, "", "CPUs in which to allow execution (0-3, 0,1)") + flCPUSetMems := cmd.String([]string{"-cpuset-mems"}, "", "MEMs in which to allow execution (0-3, 0,1)") + flCgroupParent := cmd.String([]string{"-cgroup-parent"}, "", "Optional parent cgroup for the container") + flBuildArg := opts.NewListOpts(runconfigopts.ValidateEnv) + cmd.Var(&flBuildArg, []string{"-build-arg"}, "Set build-time variables") + isolation := cmd.String([]string{"-isolation"}, "", "Container isolation technology") + + flLabels := opts.NewListOpts(nil) + cmd.Var(&flLabels, []string{"-label"}, "Set metadata for an image") + + ulimits := make(map[string]*units.Ulimit) + flUlimits := runconfigopts.NewUlimitOpt(&ulimits) + cmd.Var(flUlimits, []string{"-ulimit"}, "Ulimit options") + + cmd.Require(flag.Exact, 1) + + // For trusted pull on "FROM " instruction. + addTrustedFlags(cmd, true) + + cmd.ParseFlags(args, true) + + var ( + ctx io.ReadCloser + err error + ) + + specifiedContext := cmd.Arg(0) + + var ( + contextDir string + tempDir string + relDockerfile string + progBuff io.Writer + buildBuff io.Writer + ) + + progBuff = cli.out + buildBuff = cli.out + if *suppressOutput { + progBuff = bytes.NewBuffer(nil) + buildBuff = bytes.NewBuffer(nil) + } + + switch { + case specifiedContext == "-": + ctx, relDockerfile, err = builder.GetContextFromReader(cli.in, *dockerfileName) + case urlutil.IsGitURL(specifiedContext): + tempDir, relDockerfile, err = builder.GetContextFromGitURL(specifiedContext, *dockerfileName) + case urlutil.IsURL(specifiedContext): + ctx, relDockerfile, err = builder.GetContextFromURL(progBuff, specifiedContext, *dockerfileName) + default: + contextDir, relDockerfile, err = builder.GetContextFromLocalDir(specifiedContext, *dockerfileName) + } + + if err != nil { + if *suppressOutput && urlutil.IsURL(specifiedContext) { + fmt.Fprintln(cli.err, progBuff) + } + return fmt.Errorf("unable to prepare context: %s", err) + } + + if tempDir != "" { + defer os.RemoveAll(tempDir) + contextDir = tempDir + } + + if ctx == nil { + // And canonicalize dockerfile name to a platform-independent one + relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile) + if err != nil { + return fmt.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 + } + + var excludes []string + if err == nil { + excludes, err = dockerignore.ReadAll(f) + if err != nil { + return err + } + } + + if err := builder.ValidateContextDirectory(contextDir, excludes); err != nil { + return fmt.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 for us, if needed, after it + // parses the Dockerfile. Ignore errors here, as they will have been + // caught by validateContextDirectory above. + var includes = []string{"."} + keepThem1, _ := fileutils.Matches(".dockerignore", excludes) + keepThem2, _ := fileutils.Matches(relDockerfile, excludes) + if keepThem1 || keepThem2 { + includes = append(includes, ".dockerignore", relDockerfile) + } + + ctx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{ + Compression: archive.Uncompressed, + ExcludePatterns: excludes, + IncludeFiles: includes, + }) + if err != nil { + return err + } + } + + var resolvedTags []*resolvedTag + if isTrusted() { + // Wrap the tar archive to replace the Dockerfile entry with the rewritten + // Dockerfile which uses trusted pulls. + ctx = replaceDockerfileTarWrapper(ctx, relDockerfile, cli.trustedReference, &resolvedTags) + } + + // Setup an upload progress bar + progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(progBuff, true) + + var body io.Reader = progress.NewProgressReader(ctx, progressOutput, 0, "", "Sending build context to Docker daemon") + + var memory int64 + if *flMemoryString != "" { + parsedMemory, err := units.RAMInBytes(*flMemoryString) + if err != nil { + return err + } + memory = parsedMemory + } + + var memorySwap int64 + if *flMemorySwap != "" { + if *flMemorySwap == "-1" { + memorySwap = -1 + } else { + parsedMemorySwap, err := units.RAMInBytes(*flMemorySwap) + if err != nil { + return err + } + memorySwap = parsedMemorySwap + } + } + + var shmSize int64 + if *flShmSize != "" { + shmSize, err = units.RAMInBytes(*flShmSize) + if err != nil { + return err + } + } + + options := types.ImageBuildOptions{ + Context: body, + Memory: memory, + MemorySwap: memorySwap, + Tags: flTags.GetAll(), + SuppressOutput: *suppressOutput, + NoCache: *noCache, + Remove: *rm, + ForceRemove: *forceRm, + PullParent: *pull, + Isolation: container.Isolation(*isolation), + CPUSetCPUs: *flCPUSetCpus, + CPUSetMems: *flCPUSetMems, + CPUShares: *flCPUShares, + CPUQuota: *flCPUQuota, + CPUPeriod: *flCPUPeriod, + CgroupParent: *flCgroupParent, + Dockerfile: relDockerfile, + ShmSize: shmSize, + Ulimits: flUlimits.GetList(), + BuildArgs: runconfigopts.ConvertKVStringsToMap(flBuildArg.GetAll()), + AuthConfigs: cli.retrieveAuthConfigs(), + Labels: runconfigopts.ConvertKVStringsToMap(flLabels.GetAll()), + } + + response, err := cli.client.ImageBuild(context.Background(), options) + if err != nil { + return err + } + defer response.Body.Close() + + err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, cli.outFd, cli.isTerminalOut, nil) + 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 *suppressOutput { + fmt.Fprintf(cli.err, "%s%s", progBuff, buildBuff) + } + return Cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code} + } + } + + // Windows: show error message about modified file permissions if the + // daemon isn't running Windows. + if response.OSType != "windows" && runtime.GOOS == "windows" { + fmt.Fprintln(cli.err, `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 *suppressOutput { + fmt.Fprintf(cli.out, "%s", buildBuff) + } + + if 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 := cli.tagTrusted(resolved.digestRef, resolved.tagRef); err != nil { + return err + } + } + } + + return nil +} + +// validateTag checks if the given image name can be resolved. +func validateTag(rawRepo string) (string, error) { + _, err := reference.ParseNamed(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(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" + ref, err := reference.ParseNamed(matches[1]) + if err != nil { + return nil, nil, err + } + ref = reference.WithDefaultTag(ref) + if ref, ok := ref.(reference.NamedTagged); ok && isTrusted() { + trustedRef, err := translator(ref) + if err != nil { + return nil, nil, err + } + + line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, fmt.Sprintf("FROM %s", trustedRef.String())) + 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(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 + } + + var 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(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/api/client/cli.go b/api/client/cli.go new file mode 100644 index 00000000..6c673da4 --- /dev/null +++ b/api/client/cli.go @@ -0,0 +1,215 @@ +package client + +import ( + "errors" + "fmt" + "io" + "net/http" + "os" + "runtime" + + "github.com/docker/docker/api" + "github.com/docker/docker/cli" + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/cliconfig/credentials" + "github.com/docker/docker/dockerversion" + "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/term" + "github.com/docker/engine-api/client" + "github.com/docker/go-connections/sockets" + "github.com/docker/go-connections/tlsconfig" +) + +// DockerCli represents the docker command line client. +// Instances of the client can be returned from NewDockerCli. +type DockerCli struct { + // initializing closure + init func() error + + // configFile has the client configuration file + configFile *cliconfig.ConfigFile + // in holds the input stream and closer (io.ReadCloser) for the client. + in io.ReadCloser + // out holds the output stream (io.Writer) for the client. + out io.Writer + // err holds the error stream (io.Writer) for the client. + err io.Writer + // keyFile holds the key file as a string. + keyFile string + // inFd holds the file descriptor of the client's STDIN (if valid). + inFd uintptr + // outFd holds file descriptor of the client's STDOUT (if valid). + outFd uintptr + // isTerminalIn indicates whether the client's STDIN is a TTY + isTerminalIn bool + // isTerminalOut indicates whether the client's STDOUT is a TTY + isTerminalOut bool + // client is the http client that performs all API operations + client client.APIClient + // state holds the terminal state + state *term.State +} + +// Initialize calls the init function that will setup the configuration for the client +// such as the TLS, tcp and other parameters used to run the client. +func (cli *DockerCli) Initialize() error { + if cli.init == nil { + return nil + } + return cli.init() +} + +// CheckTtyInput 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 (cli *DockerCli) CheckTtyInput(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 && !cli.isTerminalIn { + return errors.New("cannot enable tty mode on non tty input") + } + return nil +} + +// PsFormat returns the format string specified in the configuration. +// String contains columns and format specification, for example {{ID}}\t{{Name}}. +func (cli *DockerCli) PsFormat() string { + return cli.configFile.PsFormat +} + +// ImagesFormat returns the format string specified in the configuration. +// String contains columns and format specification, for example {{ID}}\t{{Name}}. +func (cli *DockerCli) ImagesFormat() string { + return cli.configFile.ImagesFormat +} + +func (cli *DockerCli) setRawTerminal() error { + if cli.isTerminalIn && os.Getenv("NORAW") == "" { + state, err := term.SetRawTerminal(cli.inFd) + if err != nil { + return err + } + cli.state = state + } + return nil +} + +func (cli *DockerCli) restoreTerminal(in io.Closer) error { + if cli.state != nil { + term.RestoreTerminal(cli.inFd, cli.state) + } + // WARNING: DO NOT REMOVE THE OS CHECK !!! + // For some reason this Close call blocks on darwin.. + // As the client exists right after, simply discard the close + // until we find a better solution. + if in != nil && runtime.GOOS != "darwin" { + return in.Close() + } + return nil +} + +// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err. +// The key file, protocol (i.e. unix) and address are passed in as strings, along with the tls.Config. If the tls.Config +// is set the client scheme will be set to https. +// The client will be given a 32-second timeout (see https://github.com/docker/docker/pull/8035). +func NewDockerCli(in io.ReadCloser, out, err io.Writer, clientFlags *cli.ClientFlags) *DockerCli { + cli := &DockerCli{ + in: in, + out: out, + err: err, + keyFile: clientFlags.Common.TrustKey, + } + + cli.init = func() error { + clientFlags.PostParse() + configFile, e := cliconfig.Load(cliconfig.ConfigDir()) + if e != nil { + fmt.Fprintf(cli.err, "WARNING: Error loading config file:%v\n", e) + } + if !configFile.ContainsAuth() { + credentials.DetectDefaultStore(configFile) + } + cli.configFile = configFile + + host, err := getServerHost(clientFlags.Common.Hosts, clientFlags.Common.TLSOptions) + if err != nil { + return err + } + + customHeaders := cli.configFile.HTTPHeaders + if customHeaders == nil { + customHeaders = map[string]string{} + } + customHeaders["User-Agent"] = clientUserAgent() + + verStr := api.DefaultVersion.String() + if tmpStr := os.Getenv("DOCKER_API_VERSION"); tmpStr != "" { + verStr = tmpStr + } + + httpClient, err := newHTTPClient(host, clientFlags.Common.TLSOptions) + if err != nil { + return err + } + + client, err := client.NewClient(host, verStr, httpClient, customHeaders) + if err != nil { + return err + } + cli.client = client + + if cli.in != nil { + cli.inFd, cli.isTerminalIn = term.GetFdInfo(cli.in) + } + if cli.out != nil { + cli.outFd, cli.isTerminalOut = term.GetFdInfo(cli.out) + } + + return nil + } + + return cli +} + +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 = opts.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 + } + + config, err := tlsconfig.Client(*tlsOptions) + 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 +} + +func clientUserAgent() string { + return "Docker-Client/" + dockerversion.Version + " (" + runtime.GOOS + ")" +} diff --git a/api/client/client.go b/api/client/client.go new file mode 100644 index 00000000..4cfce5f6 --- /dev/null +++ b/api/client/client.go @@ -0,0 +1,5 @@ +// Package client provides a command-line interface for Docker. +// +// Run "docker help SUBCOMMAND" or "docker SUBCOMMAND --help" to see more information on any Docker subcommand, including the full list of options supported for the subcommand. +// See https://docs.docker.com/installation/ for instructions on installing Docker. +package client diff --git a/api/client/commit.go b/api/client/commit.go new file mode 100644 index 00000000..6bec4297 --- /dev/null +++ b/api/client/commit.go @@ -0,0 +1,85 @@ +package client + +import ( + "encoding/json" + "errors" + "fmt" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + "github.com/docker/docker/opts" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/reference" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/container" +) + +// CmdCommit creates a new image from a container's changes. +// +// Usage: docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]] +func (cli *DockerCli) CmdCommit(args ...string) error { + cmd := Cli.Subcmd("commit", []string{"CONTAINER [REPOSITORY[:TAG]]"}, Cli.DockerCommands["commit"].Description, true) + flPause := cmd.Bool([]string{"p", "-pause"}, true, "Pause container during commit") + flComment := cmd.String([]string{"m", "-message"}, "", "Commit message") + flAuthor := cmd.String([]string{"a", "-author"}, "", "Author (e.g., \"John Hannibal Smith \")") + flChanges := opts.NewListOpts(nil) + cmd.Var(&flChanges, []string{"c", "-change"}, "Apply Dockerfile instruction to the created image") + // FIXME: --run is deprecated, it will be replaced with inline Dockerfile commands. + flConfig := cmd.String([]string{"#-run"}, "", "This option is deprecated and will be removed in a future version in favor of inline Dockerfile-compatible commands") + cmd.Require(flag.Max, 2) + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + var ( + name = cmd.Arg(0) + repositoryAndTag = cmd.Arg(1) + repositoryName string + tag string + ) + + //Check if the given image name can be resolved + if repositoryAndTag != "" { + ref, err := reference.ParseNamed(repositoryAndTag) + if err != nil { + return err + } + + repositoryName = ref.Name() + + switch x := ref.(type) { + case reference.Canonical: + return errors.New("cannot commit to digest reference") + case reference.NamedTagged: + tag = x.Tag() + } + } + + var config *container.Config + if *flConfig != "" { + config = &container.Config{} + if err := json.Unmarshal([]byte(*flConfig), config); err != nil { + return err + } + } + + options := types.ContainerCommitOptions{ + ContainerID: name, + RepositoryName: repositoryName, + Tag: tag, + Comment: *flComment, + Author: *flAuthor, + Changes: flChanges.GetAll(), + Pause: *flPause, + Config: config, + } + + response, err := cli.client.ContainerCommit(context.Background(), options) + if err != nil { + return err + } + + fmt.Fprintln(cli.out, response.ID) + return nil +} diff --git a/api/client/cp.go b/api/client/cp.go new file mode 100644 index 00000000..61005602 --- /dev/null +++ b/api/client/cp.go @@ -0,0 +1,298 @@ +package client + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + "github.com/docker/docker/pkg/archive" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/system" + "github.com/docker/engine-api/types" +) + +type copyDirection int + +const ( + fromContainer copyDirection = (1 << iota) + toContainer + acrossContainers = fromContainer | toContainer +) + +type cpConfig struct { + followLink bool +} + +// CmdCp copies files/folders to or from a path in a container. +// +// When copying from a container, if DEST_PATH is '-' the data is written as a +// tar archive file to STDOUT. +// +// When copying to a container, if SRC_PATH is '-' the data is read as a tar +// archive file from STDIN, and the destination CONTAINER:DEST_PATH, must specify +// a directory. +// +// Usage: +// docker cp CONTAINER:SRC_PATH DEST_PATH|- +// docker cp SRC_PATH|- CONTAINER:DEST_PATH +func (cli *DockerCli) CmdCp(args ...string) error { + cmd := Cli.Subcmd( + "cp", + []string{"CONTAINER:SRC_PATH DEST_PATH|-", "SRC_PATH|- CONTAINER:DEST_PATH"}, + strings.Join([]string{ + Cli.DockerCommands["cp"].Description, + "\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.", + }, ""), + true, + ) + + followLink := cmd.Bool([]string{"L", "-follow-link"}, false, "Always follow symbol link in SRC_PATH") + + cmd.Require(flag.Exact, 2) + cmd.ParseFlags(args, true) + + if cmd.Arg(0) == "" { + return fmt.Errorf("source can not be empty") + } + if cmd.Arg(1) == "" { + return fmt.Errorf("destination can not be empty") + } + + srcContainer, srcPath := splitCpArg(cmd.Arg(0)) + dstContainer, dstPath := splitCpArg(cmd.Arg(1)) + + var direction copyDirection + if srcContainer != "" { + direction |= fromContainer + } + if dstContainer != "" { + direction |= toContainer + } + + cpParam := &cpConfig{ + followLink: *followLink, + } + + switch direction { + case fromContainer: + return cli.copyFromContainer(srcContainer, srcPath, dstPath, cpParam) + case toContainer: + return cli.copyToContainer(srcPath, dstContainer, dstPath, cpParam) + case acrossContainers: + // Copying between containers isn't supported. + return fmt.Errorf("copying between containers is not supported") + default: + // User didn't specify any container. + return fmt.Errorf("must specify at least one container source") + } +} + +// 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] +} + +func (cli *DockerCli) statContainerPath(containerName, path string) (types.ContainerPathStat, error) { + return cli.client.ContainerStatPath(context.Background(), 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 (cli *DockerCli) copyFromContainer(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 := cli.statContainerPath(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 := cli.client.CopyFromContainer(context.Background(), 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 (cli *DockerCli) copyToContainer(srcPath, dstContainer, dstPath string, cpParam *cpConfig) (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 := cli.statContainerPath(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 = cli.statContainerPath(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 fmt.Errorf("destination %q must be a directory", fmt.Sprintf("%s:%s", 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{ + ContainerID: dstContainer, + Path: resolvedDstPath, + Content: content, + AllowOverwriteDirWithFile: false, + } + + return cli.client.CopyToContainer(context.Background(), options) +} diff --git a/api/client/create.go b/api/client/create.go new file mode 100644 index 00000000..cca677f0 --- /dev/null +++ b/api/client/create.go @@ -0,0 +1,180 @@ +package client + +import ( + "fmt" + "io" + "os" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/docker/engine-api/client" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/container" + networktypes "github.com/docker/engine-api/types/network" +) + +func (cli *DockerCli) pullImage(image string) error { + return cli.pullImageCustomOut(image, cli.out) +} + +func (cli *DockerCli) pullImageCustomOut(image string, out io.Writer) error { + ref, err := reference.ParseNamed(image) + if err != nil { + return err + } + + var tag string + switch x := reference.WithDefaultTag(ref).(type) { + case reference.Canonical: + tag = x.Digest().String() + case reference.NamedTagged: + tag = x.Tag() + } + + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := registry.ParseRepositoryInfo(ref) + if err != nil { + return err + } + + authConfig := cli.resolveAuthConfig(repoInfo.Index) + encodedAuth, err := encodeAuthToBase64(authConfig) + if err != nil { + return err + } + + options := types.ImageCreateOptions{ + Parent: ref.Name(), + Tag: tag, + RegistryAuth: encodedAuth, + } + + responseBody, err := cli.client.ImageCreate(context.Background(), options) + if err != nil { + return err + } + defer responseBody.Close() + + return jsonmessage.DisplayJSONMessagesStream(responseBody, out, cli.outFd, cli.isTerminalOut, nil) +} + +type cidFile struct { + path string + file *os.File + written bool +} + +func newCIDFile(path string) (*cidFile, error) { + if _, err := os.Stat(path); err == nil { + return nil, fmt.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, fmt.Errorf("Failed to create the container ID file: %s", err) + } + + return &cidFile{path: path, file: f}, nil +} + +func (cli *DockerCli) createContainer(config *container.Config, hostConfig *container.HostConfig, networkingConfig *networktypes.NetworkingConfig, cidfile, name string) (*types.ContainerCreateResponse, error) { + var containerIDFile *cidFile + if cidfile != "" { + var err error + if containerIDFile, err = newCIDFile(cidfile); err != nil { + return nil, err + } + defer containerIDFile.Close() + } + + var trustedRef reference.Canonical + _, ref, err := reference.ParseIDOrReference(config.Image) + if err != nil { + return nil, err + } + if ref != nil { + ref = reference.WithDefaultTag(ref) + + if ref, ok := ref.(reference.NamedTagged); ok && isTrusted() { + var err error + trustedRef, err = cli.trustedReference(ref) + if err != nil { + return nil, err + } + config.Image = trustedRef.String() + } + } + + //create the container + response, err := cli.client.ContainerCreate(context.Background(), config, hostConfig, networkingConfig, name) + + //if image not found try to pull it + if err != nil { + if client.IsErrImageNotFound(err) && ref != nil { + fmt.Fprintf(cli.err, "Unable to find image '%s' locally\n", ref.String()) + + // we don't want to write to stdout anything apart from container.ID + if err = cli.pullImageCustomOut(config.Image, cli.err); err != nil { + return nil, err + } + if ref, ok := ref.(reference.NamedTagged); ok && trustedRef != nil { + if err := cli.tagTrusted(trustedRef, ref); err != nil { + return nil, err + } + } + // Retry + var retryErr error + response, retryErr = cli.client.ContainerCreate(context.Background(), config, hostConfig, networkingConfig, name) + if retryErr != nil { + return nil, retryErr + } + } else { + return nil, err + } + } + + for _, warning := range response.Warnings { + fmt.Fprintf(cli.err, "WARNING: %s\n", warning) + } + if containerIDFile != nil { + if err = containerIDFile.Write(response.ID); err != nil { + return nil, err + } + } + return &response, nil +} + +// CmdCreate creates a new container from a given image. +// +// Usage: docker create [OPTIONS] IMAGE [COMMAND] [ARG...] +func (cli *DockerCli) CmdCreate(args ...string) error { + cmd := Cli.Subcmd("create", []string{"IMAGE [COMMAND] [ARG...]"}, Cli.DockerCommands["create"].Description, true) + addTrustedFlags(cmd, true) + + // These are flags not stored in Config/HostConfig + var ( + flName = cmd.String([]string{"-name"}, "", "Assign a name to the container") + ) + + config, hostConfig, networkingConfig, cmd, err := runconfigopts.Parse(cmd, args) + + if err != nil { + cmd.ReportError(err.Error(), true) + os.Exit(1) + } + if config.Image == "" { + cmd.Usage() + return nil + } + response, err := cli.createContainer(config, hostConfig, networkingConfig, hostConfig.ContainerIDFile, *flName) + if err != nil { + return err + } + fmt.Fprintf(cli.out, "%s\n", response.ID) + return nil +} diff --git a/api/client/diff.go b/api/client/diff.go new file mode 100644 index 00000000..e17768fd --- /dev/null +++ b/api/client/diff.go @@ -0,0 +1,49 @@ +package client + +import ( + "fmt" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + "github.com/docker/docker/pkg/archive" + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdDiff shows changes on a container's filesystem. +// +// Each changed file is printed on a separate line, prefixed with a single +// character that indicates the status of the file: C (modified), A (added), +// or D (deleted). +// +// Usage: docker diff CONTAINER +func (cli *DockerCli) CmdDiff(args ...string) error { + cmd := Cli.Subcmd("diff", []string{"CONTAINER"}, Cli.DockerCommands["diff"].Description, true) + cmd.Require(flag.Exact, 1) + + cmd.ParseFlags(args, true) + + if cmd.Arg(0) == "" { + return fmt.Errorf("Container name cannot be empty") + } + + changes, err := cli.client.ContainerDiff(context.Background(), cmd.Arg(0)) + if err != nil { + return err + } + + for _, change := range changes { + var kind string + switch change.Kind { + case archive.ChangeModify: + kind = "C" + case archive.ChangeAdd: + kind = "A" + case archive.ChangeDelete: + kind = "D" + } + fmt.Fprintf(cli.out, "%s %s\n", kind, change.Path) + } + + return nil +} diff --git a/api/client/events.go b/api/client/events.go new file mode 100644 index 00000000..d2408c19 --- /dev/null +++ b/api/client/events.go @@ -0,0 +1,146 @@ +package client + +import ( + "encoding/json" + "fmt" + "io" + "sort" + "strings" + "sync" + "time" + + "golang.org/x/net/context" + + "github.com/Sirupsen/logrus" + Cli "github.com/docker/docker/cli" + "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/jsonlog" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/engine-api/types" + eventtypes "github.com/docker/engine-api/types/events" + "github.com/docker/engine-api/types/filters" +) + +// CmdEvents prints a live stream of real time events from the server. +// +// Usage: docker events [OPTIONS] +func (cli *DockerCli) CmdEvents(args ...string) error { + cmd := Cli.Subcmd("events", nil, Cli.DockerCommands["events"].Description, true) + since := cmd.String([]string{"-since"}, "", "Show all events created since timestamp") + until := cmd.String([]string{"-until"}, "", "Stream events until this timestamp") + flFilter := opts.NewListOpts(nil) + cmd.Var(&flFilter, []string{"f", "-filter"}, "Filter output based on conditions provided") + cmd.Require(flag.Exact, 0) + + cmd.ParseFlags(args, true) + + eventFilterArgs := filters.NewArgs() + + // Consolidate all filter flags, and sanity check them early. + // They'll get process in the daemon/server. + for _, f := range flFilter.GetAll() { + var err error + eventFilterArgs, err = filters.ParseFlag(f, eventFilterArgs) + if err != nil { + return err + } + } + + options := types.EventsOptions{ + Since: *since, + Until: *until, + Filters: eventFilterArgs, + } + + responseBody, err := cli.client.Events(context.Background(), options) + if err != nil { + return err + } + defer responseBody.Close() + + return streamEvents(responseBody, cli.out) +} + +// streamEvents decodes prints the incoming events in the provided output. +func streamEvents(input io.Reader, output io.Writer) error { + return decodeEvents(input, func(event eventtypes.Message, err error) error { + if err != nil { + return err + } + printOutput(event, output) + return nil + }) +} + +type eventProcessor func(event eventtypes.Message, err error) error + +func decodeEvents(input io.Reader, ep eventProcessor) error { + dec := json.NewDecoder(input) + for { + var event eventtypes.Message + err := dec.Decode(&event) + if err != nil && err == io.EOF { + break + } + + if procErr := ep(event, err); procErr != nil { + return procErr + } + } + return nil +} + +// printOutput 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 printOutput(event eventtypes.Message, output io.Writer) { + if event.TimeNano != 0 { + fmt.Fprintf(output, "%s ", time.Unix(0, event.TimeNano).Format(jsonlog.RFC3339NanoFixed)) + } else if event.Time != 0 { + fmt.Fprintf(output, "%s ", time.Unix(event.Time, 0).Format(jsonlog.RFC3339NanoFixed)) + } + + fmt.Fprintf(output, "%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(output, " (%s)", strings.Join(attrs, ", ")) + } + fmt.Fprint(output, "\n") +} + +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/api/client/exec.go b/api/client/exec.go new file mode 100644 index 00000000..520c3a38 --- /dev/null +++ b/api/client/exec.go @@ -0,0 +1,166 @@ +package client + +import ( + "fmt" + "io" + + "golang.org/x/net/context" + + "github.com/Sirupsen/logrus" + Cli "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/promise" + "github.com/docker/engine-api/types" +) + +// CmdExec runs a command in a running container. +// +// Usage: docker exec [OPTIONS] CONTAINER COMMAND [ARG...] +func (cli *DockerCli) CmdExec(args ...string) error { + cmd := Cli.Subcmd("exec", []string{"CONTAINER COMMAND [ARG...]"}, Cli.DockerCommands["exec"].Description, true) + detachKeys := cmd.String([]string{"-detach-keys"}, "", "Override the key sequence for detaching a container") + + execConfig, err := ParseExec(cmd, args) + // just in case the ParseExec does not exit + if execConfig.Container == "" || err != nil { + return Cli.StatusError{StatusCode: 1} + } + + if *detachKeys != "" { + cli.configFile.DetachKeys = *detachKeys + } + + // Send client escape keys + execConfig.DetachKeys = cli.configFile.DetachKeys + + response, err := cli.client.ContainerExecCreate(context.Background(), *execConfig) + if err != nil { + return err + } + + execID := response.ID + if execID == "" { + fmt.Fprintf(cli.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 := cli.CheckTtyInput(execConfig.AttachStdin, execConfig.Tty); err != nil { + return err + } + } else { + execStartCheck := types.ExecStartCheck{ + Detach: execConfig.Detach, + Tty: execConfig.Tty, + } + + if err := cli.client.ContainerExecStart(context.Background(), execID, execStartCheck); err != nil { + return err + } + // For now don't print this - wait for when we support exec wait() + // fmt.Fprintf(cli.out, "%s\n", execID) + return nil + } + + // Interactive exec requested. + var ( + out, stderr io.Writer + in io.ReadCloser + errCh chan error + ) + + if execConfig.AttachStdin { + in = cli.in + } + if execConfig.AttachStdout { + out = cli.out + } + if execConfig.AttachStderr { + if execConfig.Tty { + stderr = cli.out + } else { + stderr = cli.err + } + } + + resp, err := cli.client.ContainerExecAttach(context.Background(), execID, *execConfig) + if err != nil { + return err + } + defer resp.Close() + if in != nil && execConfig.Tty { + if err := cli.setRawTerminal(); err != nil { + return err + } + defer cli.restoreTerminal(in) + } + errCh = promise.Go(func() error { + return cli.holdHijackedConnection(execConfig.Tty, in, out, stderr, resp) + }) + + if execConfig.Tty && cli.isTerminalIn { + if err := cli.monitorTtySize(execID, true); err != nil { + fmt.Fprintf(cli.err, "Error monitoring TTY size: %s\n", err) + } + } + + if err := <-errCh; err != nil { + logrus.Debugf("Error hijack: %s", err) + return err + } + + var status int + if _, status, err = getExecExitCode(cli, execID); err != nil { + return err + } + + if status != 0 { + return Cli.StatusError{StatusCode: status} + } + + return nil +} + +// ParseExec parses the specified args for the specified command and generates +// an ExecConfig from it. +// If the minimal number of specified args is not right or if specified args are +// not valid, it will return an error. +func ParseExec(cmd *flag.FlagSet, args []string) (*types.ExecConfig, error) { + var ( + flStdin = cmd.Bool([]string{"i", "-interactive"}, false, "Keep STDIN open even if not attached") + flTty = cmd.Bool([]string{"t", "-tty"}, false, "Allocate a pseudo-TTY") + flDetach = cmd.Bool([]string{"d", "-detach"}, false, "Detached mode: run command in the background") + flUser = cmd.String([]string{"u", "-user"}, "", "Username or UID (format: [:])") + flPrivileged = cmd.Bool([]string{"-privileged"}, false, "Give extended privileges to the command") + execCmd []string + container string + ) + cmd.Require(flag.Min, 2) + if err := cmd.ParseFlags(args, true); err != nil { + return nil, err + } + container = cmd.Arg(0) + parsedArgs := cmd.Args() + execCmd = parsedArgs[1:] + + execConfig := &types.ExecConfig{ + User: *flUser, + Privileged: *flPrivileged, + Tty: *flTty, + Cmd: execCmd, + Container: container, + Detach: *flDetach, + } + + // If -d is not set, attach to everything by default + if !*flDetach { + execConfig.AttachStdout = true + execConfig.AttachStderr = true + if *flStdin { + execConfig.AttachStdin = true + } + } + + return execConfig, nil +} diff --git a/api/client/exec_test.go b/api/client/exec_test.go new file mode 100644 index 00000000..1680fa6e --- /dev/null +++ b/api/client/exec_test.go @@ -0,0 +1,130 @@ +package client + +import ( + "fmt" + "io/ioutil" + "testing" + + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/engine-api/types" +) + +type arguments struct { + args []string +} + +func TestParseExec(t *testing.T) { + invalids := map[*arguments]error{ + &arguments{[]string{"-unknown"}}: fmt.Errorf("flag provided but not defined: -unknown"), + &arguments{[]string{"-u"}}: fmt.Errorf("flag needs an argument: -u"), + &arguments{[]string{"--user"}}: fmt.Errorf("flag needs an argument: --user"), + } + valids := map[*arguments]*types.ExecConfig{ + &arguments{ + []string{"container", "command"}, + }: { + Container: "container", + Cmd: []string{"command"}, + AttachStdout: true, + AttachStderr: true, + }, + &arguments{ + []string{"container", "command1", "command2"}, + }: { + Container: "container", + Cmd: []string{"command1", "command2"}, + AttachStdout: true, + AttachStderr: true, + }, + &arguments{ + []string{"-i", "-t", "-u", "uid", "container", "command"}, + }: { + User: "uid", + AttachStdin: true, + AttachStdout: true, + AttachStderr: true, + Tty: true, + Container: "container", + Cmd: []string{"command"}, + }, + &arguments{ + []string{"-d", "container", "command"}, + }: { + AttachStdin: false, + AttachStdout: false, + AttachStderr: false, + Detach: true, + Container: "container", + Cmd: []string{"command"}, + }, + &arguments{ + []string{"-t", "-i", "-d", "container", "command"}, + }: { + AttachStdin: false, + AttachStdout: false, + AttachStderr: false, + Detach: true, + Tty: true, + Container: "container", + Cmd: []string{"command"}, + }, + } + for invalid, expectedError := range invalids { + cmd := flag.NewFlagSet("exec", flag.ContinueOnError) + cmd.ShortUsage = func() {} + cmd.SetOutput(ioutil.Discard) + _, err := ParseExec(cmd, invalid.args) + if err == nil || err.Error() != expectedError.Error() { + t.Fatalf("Expected an error [%v] for %v, got %v", expectedError, invalid, err) + } + + } + for valid, expectedExecConfig := range valids { + cmd := flag.NewFlagSet("exec", flag.ContinueOnError) + cmd.ShortUsage = func() {} + cmd.SetOutput(ioutil.Discard) + execConfig, err := ParseExec(cmd, valid.args) + 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.Container != config2.Container { + 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/api/client/export.go b/api/client/export.go new file mode 100644 index 00000000..a1d3ebe7 --- /dev/null +++ b/api/client/export.go @@ -0,0 +1,42 @@ +package client + +import ( + "errors" + "io" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdExport exports a filesystem as a tar archive. +// +// The tar archive is streamed to STDOUT by default or written to a file. +// +// Usage: docker export [OPTIONS] CONTAINER +func (cli *DockerCli) CmdExport(args ...string) error { + cmd := Cli.Subcmd("export", []string{"CONTAINER"}, Cli.DockerCommands["export"].Description, true) + outfile := cmd.String([]string{"o", "-output"}, "", "Write to a file, instead of STDOUT") + cmd.Require(flag.Exact, 1) + + cmd.ParseFlags(args, true) + + if *outfile == "" && cli.isTerminalOut { + return errors.New("Cowardly refusing to save to a terminal. Use the -o flag or redirect.") + } + + responseBody, err := cli.client.ContainerExport(context.Background(), cmd.Arg(0)) + if err != nil { + return err + } + defer responseBody.Close() + + if *outfile == "" { + _, err := io.Copy(cli.out, responseBody) + return err + } + + return copyToFile(*outfile, responseBody) + +} diff --git a/api/client/formatter/custom.go b/api/client/formatter/custom.go new file mode 100644 index 00000000..2bb26a3d --- /dev/null +++ b/api/client/formatter/custom.go @@ -0,0 +1,242 @@ +package formatter + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/docker/docker/api" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/stringutils" + "github.com/docker/engine-api/types" + "github.com/docker/go-units" +) + +const ( + tableKey = "table" + + containerIDHeader = "CONTAINER ID" + imageHeader = "IMAGE" + namesHeader = "NAMES" + commandHeader = "COMMAND" + createdSinceHeader = "CREATED" + createdAtHeader = "CREATED AT" + runningForHeader = "CREATED" + statusHeader = "STATUS" + portsHeader = "PORTS" + sizeHeader = "SIZE" + labelsHeader = "LABELS" + imageIDHeader = "IMAGE ID" + repositoryHeader = "REPOSITORY" + tagHeader = "TAG" + digestHeader = "DIGEST" + mountsHeader = "MOUNTS" +) + +type containerContext struct { + baseSubContext + trunc bool + c types.Container +} + +func (c *containerContext) ID() string { + c.addHeader(containerIDHeader) + if c.trunc { + return stringid.TruncateID(c.c.ID) + } + return c.c.ID +} + +func (c *containerContext) Names() string { + c.addHeader(namesHeader) + 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 { + c.addHeader(imageHeader) + if c.c.Image == "" { + return "" + } + if c.trunc { + if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) { + return trunc + } + } + return c.c.Image +} + +func (c *containerContext) Command() string { + c.addHeader(commandHeader) + command := c.c.Command + if c.trunc { + command = stringutils.Truncate(command, 20) + } + return strconv.Quote(command) +} + +func (c *containerContext) CreatedAt() string { + c.addHeader(createdAtHeader) + return time.Unix(int64(c.c.Created), 0).String() +} + +func (c *containerContext) RunningFor() string { + c.addHeader(runningForHeader) + createdAt := time.Unix(int64(c.c.Created), 0) + return units.HumanDuration(time.Now().UTC().Sub(createdAt)) +} + +func (c *containerContext) Ports() string { + c.addHeader(portsHeader) + return api.DisplayablePorts(c.c.Ports) +} + +func (c *containerContext) Status() string { + c.addHeader(statusHeader) + return c.c.Status +} + +func (c *containerContext) Size() string { + c.addHeader(sizeHeader) + srw := units.HumanSize(float64(c.c.SizeRw)) + sv := units.HumanSize(float64(c.c.SizeRootFs)) + + sf := srw + if c.c.SizeRootFs > 0 { + sf = fmt.Sprintf("%s (virtual %s)", srw, sv) + } + return sf +} + +func (c *containerContext) Labels() string { + c.addHeader(labelsHeader) + 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 { + n := strings.Split(name, ".") + r := strings.NewReplacer("-", " ", "_", " ") + h := r.Replace(n[len(n)-1]) + + c.addHeader(h) + + if c.c.Labels == nil { + return "" + } + return c.c.Labels[name] +} + +func (c *containerContext) Mounts() string { + c.addHeader(mountsHeader) + + 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.Truncate(name, 15) + } + mounts = append(mounts, name) + } + return strings.Join(mounts, ",") +} + +type imageContext struct { + baseSubContext + trunc bool + i types.Image + repo string + tag string + digest string +} + +func (c *imageContext) ID() string { + c.addHeader(imageIDHeader) + if c.trunc { + return stringid.TruncateID(c.i.ID) + } + return c.i.ID +} + +func (c *imageContext) Repository() string { + c.addHeader(repositoryHeader) + return c.repo +} + +func (c *imageContext) Tag() string { + c.addHeader(tagHeader) + return c.tag +} + +func (c *imageContext) Digest() string { + c.addHeader(digestHeader) + return c.digest +} + +func (c *imageContext) CreatedSince() string { + c.addHeader(createdSinceHeader) + createdAt := time.Unix(int64(c.i.Created), 0) + return units.HumanDuration(time.Now().UTC().Sub(createdAt)) +} + +func (c *imageContext) CreatedAt() string { + c.addHeader(createdAtHeader) + return time.Unix(int64(c.i.Created), 0).String() +} + +func (c *imageContext) Size() string { + c.addHeader(sizeHeader) + return units.HumanSize(float64(c.i.Size)) +} + +type subContext interface { + fullHeader() string + addHeader(header string) +} + +type baseSubContext struct { + header []string +} + +func (c *baseSubContext) fullHeader() string { + if c.header == nil { + return "" + } + return strings.Join(c.header, "\t") +} + +func (c *baseSubContext) addHeader(header string) { + if c.header == nil { + c.header = []string{} + } + c.header = append(c.header, strings.ToUpper(header)) +} + +func stripNamePrefix(ss []string) []string { + for i, s := range ss { + ss[i] = s[1:] + } + + return ss +} diff --git a/api/client/formatter/custom_test.go b/api/client/formatter/custom_test.go new file mode 100644 index 00000000..6a21f2bc --- /dev/null +++ b/api/client/formatter/custom_test.go @@ -0,0 +1,192 @@ +package formatter + +import ( + "reflect" + "strings" + "testing" + "time" + + "github.com/docker/docker/pkg/stringid" + "github.com/docker/engine-api/types" +) + +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 + expHeader string + call func() string + }{ + {types.Container{ID: containerID}, true, stringid.TruncateID(containerID), containerIDHeader, ctx.ID}, + {types.Container{ID: containerID}, false, containerID, containerIDHeader, ctx.ID}, + {types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", namesHeader, ctx.Names}, + {types.Container{Image: "ubuntu"}, true, "ubuntu", imageHeader, ctx.Image}, + {types.Container{Image: "verylongimagename"}, true, "verylongimagename", imageHeader, ctx.Image}, + {types.Container{Image: "verylongimagename"}, false, "verylongimagename", imageHeader, ctx.Image}, + {types.Container{ + Image: "a5a665ff33eced1e0803148700880edab4", + ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5", + }, + true, + "a5a665ff33ec", + imageHeader, + ctx.Image, + }, + {types.Container{ + Image: "a5a665ff33eced1e0803148700880edab4", + ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5", + }, + false, + "a5a665ff33eced1e0803148700880edab4", + imageHeader, + ctx.Image, + }, + {types.Container{Image: ""}, true, "", imageHeader, ctx.Image}, + {types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, commandHeader, ctx.Command}, + {types.Container{Created: unix}, true, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt}, + {types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", portsHeader, ctx.Ports}, + {types.Container{Status: "RUNNING"}, true, "RUNNING", statusHeader, ctx.Status}, + {types.Container{SizeRw: 10}, true, "10 B", sizeHeader, ctx.Size}, + {types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10 B (virtual 20 B)", sizeHeader, ctx.Size}, + {types.Container{}, true, "", labelsHeader, ctx.Labels}, + {types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", labelsHeader, ctx.Labels}, + {types.Container{Created: unix}, true, "About a minute", runningForHeader, ctx.RunningFor}, + } + + 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) + } + + h := ctx.fullHeader() + if h != c.expHeader { + t.Fatalf("Expected %s, was %s\n", c.expHeader, h) + } + } + + 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) + } + + h := ctx.fullHeader() + if h != "SWARM ID\tNODE NAME" { + t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h) + + } + + 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) + } + + ctx = containerContext{c: c2, trunc: true} + fullHeader := ctx.fullHeader() + if fullHeader != "" { + t.Fatalf("Expected fullHeader to be empty, was %s", fullHeader) + } + +} + +func TestImagesContext(t *testing.T) { + imageID := stringid.GenerateRandomID() + unix := time.Now().Unix() + + var ctx imageContext + cases := []struct { + imageCtx imageContext + expValue string + expHeader string + call func() string + }{ + {imageContext{ + i: types.Image{ID: imageID}, + trunc: true, + }, stringid.TruncateID(imageID), imageIDHeader, ctx.ID}, + {imageContext{ + i: types.Image{ID: imageID}, + trunc: false, + }, imageID, imageIDHeader, ctx.ID}, + {imageContext{ + i: types.Image{Size: 10}, + trunc: true, + }, "10 B", sizeHeader, ctx.Size}, + {imageContext{ + i: types.Image{Created: unix}, + trunc: true, + }, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt}, + // FIXME + // {imageContext{ + // i: types.Image{Created: unix}, + // trunc: true, + // }, units.HumanDuration(time.Unix(unix, 0)), createdSinceHeader, ctx.CreatedSince}, + {imageContext{ + i: types.Image{}, + repo: "busybox", + }, "busybox", repositoryHeader, ctx.Repository}, + {imageContext{ + i: types.Image{}, + tag: "latest", + }, "latest", tagHeader, ctx.Tag}, + {imageContext{ + i: types.Image{}, + digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", + }, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", digestHeader, 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) + } + + h := ctx.fullHeader() + if h != c.expHeader { + t.Fatalf("Expected %s, was %s\n", c.expHeader, h) + } + } +} + +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/api/client/formatter/formatter.go b/api/client/formatter/formatter.go new file mode 100644 index 00000000..bc3d50c9 --- /dev/null +++ b/api/client/formatter/formatter.go @@ -0,0 +1,255 @@ +package formatter + +import ( + "bytes" + "fmt" + "io" + "strings" + "text/tabwriter" + "text/template" + + "github.com/docker/docker/reference" + "github.com/docker/docker/utils/templates" + "github.com/docker/engine-api/types" +) + +const ( + tableFormatKey = "table" + rawFormatKey = "raw" + + defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}" + defaultImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}" + defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}" + defaultQuietFormat = "{{.ID}}" +) + +// 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 string + // Quiet when set to true will simply print minimal information. + Quiet bool + // Trunc when set to true will truncate the output of certain fields such as Container ID. + Trunc bool + + // internal element + table bool + finalFormat string + header string + buffer *bytes.Buffer +} + +func (c *Context) preformat() { + c.finalFormat = c.Format + + if strings.HasPrefix(c.Format, tableKey) { + c.table = true + c.finalFormat = c.finalFormat[len(tableKey):] + } + + 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 { + c.buffer.WriteString(fmt.Sprintf("Template parsing error: %v\n", err)) + c.buffer.WriteTo(c.Output) + } + return tmpl, err +} + +func (c *Context) postformat(tmpl *template.Template, subContext subContext) { + if c.table { + if len(c.header) == 0 { + // if we still don't have a header, we didn't have any containers so we need to fake it to get the right headers from the template + tmpl.Execute(bytes.NewBufferString(""), subContext) + c.header = subContext.fullHeader() + } + + t := tabwriter.NewWriter(c.Output, 20, 1, 3, ' ', 0) + t.Write([]byte(c.header)) + 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 { + c.buffer = bytes.NewBufferString(fmt.Sprintf("Template parsing error: %v\n", err)) + c.buffer.WriteTo(c.Output) + return err + } + if c.table && len(c.header) == 0 { + c.header = subContext.fullHeader() + } + c.buffer.WriteString("\n") + return nil +} + +// ContainerContext contains container specific information required by the formater, encapsulate a Context struct. +type ContainerContext struct { + Context + // Size when set to true will display the size of the output. + Size bool + // Containers + Containers []types.Container +} + +// ImageContext contains image specific information required by the formater, encapsulate a Context struct. +type ImageContext struct { + Context + Digest bool + // Images + Images []types.Image +} + +func (ctx ContainerContext) Write() { + switch ctx.Format { + case tableFormatKey: + ctx.Format = defaultContainerTableFormat + if ctx.Quiet { + ctx.Format = defaultQuietFormat + } + case rawFormatKey: + if ctx.Quiet { + ctx.Format = `container_id: {{.ID}}` + } else { + ctx.Format = `container_id: {{.ID}} +image: {{.Image}} +command: {{.Command}} +created_at: {{.CreatedAt}} +status: {{.Status}} +names: {{.Names}} +labels: {{.Labels}} +ports: {{.Ports}} +` + if ctx.Size { + ctx.Format += `size: {{.Size}} +` + } + } + } + + ctx.buffer = bytes.NewBufferString("") + ctx.preformat() + if ctx.table && ctx.Size { + ctx.finalFormat += "\t{{.Size}}" + } + + tmpl, err := ctx.parseFormat() + if err != nil { + return + } + + for _, container := range ctx.Containers { + containerCtx := &containerContext{ + trunc: ctx.Trunc, + c: container, + } + err = ctx.contextFormat(tmpl, containerCtx) + if err != nil { + return + } + } + + ctx.postformat(tmpl, &containerContext{}) +} + +func (ctx ImageContext) Write() { + switch ctx.Format { + case tableFormatKey: + ctx.Format = defaultImageTableFormat + if ctx.Digest { + ctx.Format = defaultImageTableFormatWithDigest + } + if ctx.Quiet { + ctx.Format = defaultQuietFormat + } + case rawFormatKey: + if ctx.Quiet { + ctx.Format = `image_id: {{.ID}}` + } else { + if ctx.Digest { + ctx.Format = `repository: {{ .Repository }} +tag: {{.Tag}} +digest: {{.Digest}} +image_id: {{.ID}} +created_at: {{.CreatedAt}} +virtual_size: {{.Size}} +` + } else { + ctx.Format = `repository: {{ .Repository }} +tag: {{.Tag}} +image_id: {{.ID}} +created_at: {{.CreatedAt}} +virtual_size: {{.Size}} +` + } + } + } + + ctx.buffer = bytes.NewBufferString("") + ctx.preformat() + if ctx.table && ctx.Digest && !strings.Contains(ctx.Format, "{{.Digest}}") { + ctx.finalFormat += "\t{{.Digest}}" + } + + tmpl, err := ctx.parseFormat() + if err != nil { + return + } + + for _, image := range ctx.Images { + + repoTags := image.RepoTags + repoDigests := image.RepoDigests + + if len(repoTags) == 1 && repoTags[0] == ":" && len(repoDigests) == 1 && repoDigests[0] == "@" { + // dangling image - clear out either repoTags or repoDigests so we only show it once below + repoDigests = []string{} + } + // combine the tags and digests lists + tagsAndDigests := append(repoTags, repoDigests...) + for _, repoAndRef := range tagsAndDigests { + repo := "" + tag := "" + digest := "" + + if !strings.HasPrefix(repoAndRef, "") { + ref, err := reference.ParseNamed(repoAndRef) + if err != nil { + continue + } + repo = ref.Name() + + switch x := ref.(type) { + case reference.Canonical: + digest = x.Digest().String() + case reference.NamedTagged: + tag = x.Tag() + } + } + imageCtx := &imageContext{ + trunc: ctx.Trunc, + i: image, + repo: repo, + tag: tag, + digest: digest, + } + err = ctx.contextFormat(tmpl, imageCtx) + if err != nil { + return + } + } + } + + ctx.postformat(tmpl, &imageContext{}) +} diff --git a/api/client/formatter/formatter_test.go b/api/client/formatter/formatter_test.go new file mode 100644 index 00000000..223cab97 --- /dev/null +++ b/api/client/formatter/formatter_test.go @@ -0,0 +1,535 @@ +package formatter + +import ( + "bytes" + "fmt" + "testing" + "time" + + "github.com/docker/engine-api/types" +) + +func TestContainerContextWrite(t *testing.T) { + unixTime := time.Now().AddDate(0, 0, -1).Unix() + expectedTime := time.Unix(unixTime, 0).String() + + contexts := []struct { + context ContainerContext + expected string + }{ + // Errors + { + ContainerContext{ + Context: Context{ + Format: "{{InvalidFunction}}", + }, + }, + `Template parsing error: template: :1: function "InvalidFunction" not defined +`, + }, + { + ContainerContext{ + Context: Context{ + Format: "{{nil}}", + }, + }, + `Template parsing error: template: :1:2: executing "" at : nil is not a command +`, + }, + // Table Format + { + ContainerContext{ + Context: Context{ + Format: "table", + }, + }, + `CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +containerID1 ubuntu "" 24 hours ago foobar_baz +containerID2 ubuntu "" 24 hours ago foobar_bar +`, + }, + { + ContainerContext{ + Context: Context{ + Format: "table {{.Image}}", + }, + }, + "IMAGE\nubuntu\nubuntu\n", + }, + { + ContainerContext{ + Context: Context{ + Format: "table {{.Image}}", + }, + Size: true, + }, + "IMAGE SIZE\nubuntu 0 B\nubuntu 0 B\n", + }, + { + ContainerContext{ + Context: Context{ + Format: "table {{.Image}}", + Quiet: true, + }, + }, + "IMAGE\nubuntu\nubuntu\n", + }, + { + ContainerContext{ + Context: Context{ + Format: "table", + Quiet: true, + }, + }, + "containerID1\ncontainerID2\n", + }, + // Raw Format + { + ContainerContext{ + Context: Context{ + Format: "raw", + }, + }, + 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), + }, + { + ContainerContext{ + Context: Context{ + Format: "raw", + }, + Size: true, + }, + fmt.Sprintf(`container_id: containerID1 +image: ubuntu +command: "" +created_at: %s +status: +names: foobar_baz +labels: +ports: +size: 0 B + +container_id: containerID2 +image: ubuntu +command: "" +created_at: %s +status: +names: foobar_bar +labels: +ports: +size: 0 B + +`, expectedTime, expectedTime), + }, + { + ContainerContext{ + Context: Context{ + Format: "raw", + Quiet: true, + }, + }, + "container_id: containerID1\ncontainer_id: containerID2\n", + }, + // Custom Format + { + ContainerContext{ + Context: Context{ + Format: "{{.Image}}", + }, + }, + "ubuntu\nubuntu\n", + }, + { + ContainerContext{ + Context: Context{ + Format: "{{.Image}}", + }, + Size: true, + }, + "ubuntu\nubuntu\n", + }, + } + + for _, context := range contexts { + 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("") + context.context.Output = out + context.context.Containers = containers + context.context.Write() + actual := out.String() + if actual != context.expected { + t.Fatalf("Expected \n%s, got \n%s", context.expected, actual) + } + // Clean buffer + out.Reset() + } +} + +func TestContainerContextWriteWithNoContainers(t *testing.T) { + out := bytes.NewBufferString("") + containers := []types.Container{} + + contexts := []struct { + context ContainerContext + expected string + }{ + { + ContainerContext{ + Context: Context{ + Format: "{{.Image}}", + Output: out, + }, + }, + "", + }, + { + ContainerContext{ + Context: Context{ + Format: "table {{.Image}}", + Output: out, + }, + }, + "IMAGE\n", + }, + { + ContainerContext{ + Context: Context{ + Format: "{{.Image}}", + Output: out, + }, + Size: true, + }, + "", + }, + { + ContainerContext{ + Context: Context{ + Format: "table {{.Image}}", + Output: out, + }, + Size: true, + }, + "IMAGE SIZE\n", + }, + } + + for _, context := range contexts { + context.context.Containers = containers + context.context.Write() + actual := out.String() + if actual != context.expected { + t.Fatalf("Expected \n%s, got \n%s", context.expected, actual) + } + // Clean buffer + out.Reset() + } +} + +func TestImageContextWrite(t *testing.T) { + unixTime := time.Now().AddDate(0, 0, -1).Unix() + expectedTime := time.Unix(unixTime, 0).String() + + contexts := []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: "table", + }, + }, + `REPOSITORY TAG IMAGE ID CREATED SIZE +image tag1 imageID1 24 hours ago 0 B +image imageID1 24 hours ago 0 B +image tag2 imageID2 24 hours ago 0 B + imageID3 24 hours ago 0 B +`, + }, + { + ImageContext{ + Context: Context{ + Format: "table {{.Repository}}", + }, + }, + "REPOSITORY\nimage\nimage\nimage\n\n", + }, + { + ImageContext{ + Context: Context{ + Format: "table {{.Repository}}", + }, + Digest: true, + }, + `REPOSITORY DIGEST +image +image sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf +image + +`, + }, + { + ImageContext{ + Context: Context{ + Format: "table {{.Repository}}", + Quiet: true, + }, + }, + "REPOSITORY\nimage\nimage\nimage\n\n", + }, + { + ImageContext{ + Context: Context{ + Format: "table", + Quiet: true, + }, + }, + "imageID1\nimageID1\nimageID2\nimageID3\n", + }, + { + ImageContext{ + Context: Context{ + Format: "table", + Quiet: false, + }, + Digest: true, + }, + `REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE +image tag1 imageID1 24 hours ago 0 B +image sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf imageID1 24 hours ago 0 B +image tag2 imageID2 24 hours ago 0 B + imageID3 24 hours ago 0 B +`, + }, + { + ImageContext{ + Context: Context{ + Format: "table", + Quiet: true, + }, + Digest: true, + }, + "imageID1\nimageID1\nimageID2\nimageID3\n", + }, + // Raw Format + { + ImageContext{ + Context: Context{ + Format: "raw", + }, + }, + fmt.Sprintf(`repository: image +tag: tag1 +image_id: imageID1 +created_at: %s +virtual_size: 0 B + +repository: image +tag: +image_id: imageID1 +created_at: %s +virtual_size: 0 B + +repository: image +tag: tag2 +image_id: imageID2 +created_at: %s +virtual_size: 0 B + +repository: +tag: +image_id: imageID3 +created_at: %s +virtual_size: 0 B + +`, expectedTime, expectedTime, expectedTime, expectedTime), + }, + { + ImageContext{ + Context: Context{ + Format: "raw", + }, + Digest: true, + }, + fmt.Sprintf(`repository: image +tag: tag1 +digest: +image_id: imageID1 +created_at: %s +virtual_size: 0 B + +repository: image +tag: +digest: sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf +image_id: imageID1 +created_at: %s +virtual_size: 0 B + +repository: image +tag: tag2 +digest: +image_id: imageID2 +created_at: %s +virtual_size: 0 B + +repository: +tag: +digest: +image_id: imageID3 +created_at: %s +virtual_size: 0 B + +`, expectedTime, expectedTime, expectedTime, expectedTime), + }, + { + ImageContext{ + Context: Context{ + Format: "raw", + Quiet: true, + }, + }, + `image_id: imageID1 +image_id: imageID1 +image_id: imageID2 +image_id: imageID3 +`, + }, + // Custom Format + { + ImageContext{ + Context: Context{ + Format: "{{.Repository}}", + }, + }, + "image\nimage\nimage\n\n", + }, + { + ImageContext{ + Context: Context{ + Format: "{{.Repository}}", + }, + Digest: true, + }, + "image\nimage\nimage\n\n", + }, + } + + for _, context := range contexts { + images := []types.Image{ + {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("") + context.context.Output = out + context.context.Images = images + context.context.Write() + actual := out.String() + if actual != context.expected { + t.Fatalf("Expected \n%s, got \n%s", context.expected, actual) + } + // Clean buffer + out.Reset() + } +} + +func TestImageContextWriteWithNoImage(t *testing.T) { + out := bytes.NewBufferString("") + images := []types.Image{} + + contexts := []struct { + context ImageContext + expected string + }{ + { + ImageContext{ + Context: Context{ + Format: "{{.Repository}}", + Output: out, + }, + }, + "", + }, + { + ImageContext{ + Context: Context{ + Format: "table {{.Repository}}", + Output: out, + }, + }, + "REPOSITORY\n", + }, + { + ImageContext{ + Context: Context{ + Format: "{{.Repository}}", + Output: out, + }, + Digest: true, + }, + "", + }, + { + ImageContext{ + Context: Context{ + Format: "table {{.Repository}}", + Output: out, + }, + Digest: true, + }, + "REPOSITORY DIGEST\n", + }, + } + + for _, context := range contexts { + context.context.Images = images + context.context.Write() + actual := out.String() + if actual != context.expected { + t.Fatalf("Expected \n%s, got \n%s", context.expected, actual) + } + // Clean buffer + out.Reset() + } +} diff --git a/api/client/hijack.go b/api/client/hijack.go new file mode 100644 index 00000000..4c80fe1c --- /dev/null +++ b/api/client/hijack.go @@ -0,0 +1,56 @@ +package client + +import ( + "io" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/stdcopy" + "github.com/docker/engine-api/types" +) + +func (cli *DockerCli) holdHijackedConnection(tty bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp types.HijackedResponse) error { + var err error + 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) + } else { + _, err = stdcopy.StdCopy(outputStream, errorStream, resp.Reader) + } + logrus.Debugf("[hijack] End of stdout") + receiveStdout <- err + }() + } + + stdinDone := make(chan struct{}) + go func() { + if inputStream != nil { + io.Copy(resp.Conn, inputStream) + logrus.Debugf("[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 { + if err := <-receiveStdout; err != nil { + logrus.Debugf("Error receiveStdout: %s", err) + return err + } + } + } + + return nil +} diff --git a/api/client/history.go b/api/client/history.go new file mode 100644 index 00000000..25bb4157 --- /dev/null +++ b/api/client/history.go @@ -0,0 +1,76 @@ +package client + +import ( + "fmt" + "strconv" + "strings" + "text/tabwriter" + "time" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/stringutils" + "github.com/docker/go-units" +) + +// CmdHistory shows the history of an image. +// +// Usage: docker history [OPTIONS] IMAGE +func (cli *DockerCli) CmdHistory(args ...string) error { + cmd := Cli.Subcmd("history", []string{"IMAGE"}, Cli.DockerCommands["history"].Description, true) + human := cmd.Bool([]string{"H", "-human"}, true, "Print sizes and dates in human readable format") + quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only show numeric IDs") + noTrunc := cmd.Bool([]string{"-no-trunc"}, false, "Don't truncate output") + cmd.Require(flag.Exact, 1) + + cmd.ParseFlags(args, true) + + history, err := cli.client.ImageHistory(context.Background(), cmd.Arg(0)) + if err != nil { + return err + } + + w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) + + if *quiet { + for _, entry := range history { + if *noTrunc { + fmt.Fprintf(w, "%s\n", entry.ID) + } else { + fmt.Fprintf(w, "%s\n", stringid.TruncateID(entry.ID)) + } + } + w.Flush() + return nil + } + + var imageID string + var createdBy string + var created string + var size string + + fmt.Fprintln(w, "IMAGE\tCREATED\tCREATED BY\tSIZE\tCOMMENT") + for _, entry := range history { + imageID = entry.ID + createdBy = strings.Replace(entry.CreatedBy, "\t", " ", -1) + if *noTrunc == false { + createdBy = stringutils.Truncate(createdBy, 45) + imageID = stringid.TruncateID(entry.ID) + } + + if *human { + created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(entry.Created, 0))) + " ago" + size = units.HumanSize(float64(entry.Size)) + } else { + created = time.Unix(entry.Created, 0).Format(time.RFC3339) + size = strconv.FormatInt(entry.Size, 10) + } + + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", imageID, created, createdBy, size, entry.Comment) + } + w.Flush() + return nil +} diff --git a/api/client/images.go b/api/client/images.go new file mode 100644 index 00000000..4840b63d --- /dev/null +++ b/api/client/images.go @@ -0,0 +1,81 @@ +package client + +import ( + "golang.org/x/net/context" + + "github.com/docker/docker/api/client/formatter" + Cli "github.com/docker/docker/cli" + "github.com/docker/docker/opts" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/filters" +) + +// CmdImages lists the images in a specified repository, or all top-level images if no repository is specified. +// +// Usage: docker images [OPTIONS] [REPOSITORY] +func (cli *DockerCli) CmdImages(args ...string) error { + cmd := Cli.Subcmd("images", []string{"[REPOSITORY[:TAG]]"}, Cli.DockerCommands["images"].Description, true) + quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only show numeric IDs") + all := cmd.Bool([]string{"a", "-all"}, false, "Show all images (default hides intermediate images)") + noTrunc := cmd.Bool([]string{"-no-trunc"}, false, "Don't truncate output") + showDigests := cmd.Bool([]string{"-digests"}, false, "Show digests") + format := cmd.String([]string{"-format"}, "", "Pretty-print images using a Go template") + + flFilter := opts.NewListOpts(nil) + cmd.Var(&flFilter, []string{"f", "-filter"}, "Filter output based on conditions provided") + cmd.Require(flag.Max, 1) + + cmd.ParseFlags(args, true) + + // Consolidate all filter flags, and sanity check them early. + // They'll get process in the daemon/server. + imageFilterArgs := filters.NewArgs() + for _, f := range flFilter.GetAll() { + var err error + imageFilterArgs, err = filters.ParseFlag(f, imageFilterArgs) + if err != nil { + return err + } + } + + var matchName string + if cmd.NArg() == 1 { + matchName = cmd.Arg(0) + } + + options := types.ImageListOptions{ + MatchName: matchName, + All: *all, + Filters: imageFilterArgs, + } + + images, err := cli.client.ImageList(context.Background(), options) + if err != nil { + return err + } + + f := *format + if len(f) == 0 { + if len(cli.ImagesFormat()) > 0 && !*quiet { + f = cli.ImagesFormat() + } else { + f = "table" + } + } + + imagesCtx := formatter.ImageContext{ + Context: formatter.Context{ + Output: cli.out, + Format: f, + Quiet: *quiet, + Trunc: !*noTrunc, + }, + Digest: *showDigests, + Images: images, + } + + imagesCtx.Write() + + return nil +} diff --git a/api/client/import.go b/api/client/import.go new file mode 100644 index 00000000..c96e1e97 --- /dev/null +++ b/api/client/import.go @@ -0,0 +1,82 @@ +package client + +import ( + "fmt" + "io" + "os" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/jsonmessage" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/urlutil" + "github.com/docker/docker/reference" + "github.com/docker/engine-api/types" +) + +// CmdImport creates an empty filesystem image, imports the contents of the tarball into the image, and optionally tags the image. +// +// The URL argument is the address of a tarball (.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz) file or a path to local file relative to docker client. If the URL is '-', then the tar file is read from STDIN. +// +// Usage: docker import [OPTIONS] file|URL|- [REPOSITORY[:TAG]] +func (cli *DockerCli) CmdImport(args ...string) error { + cmd := Cli.Subcmd("import", []string{"file|URL|- [REPOSITORY[:TAG]]"}, Cli.DockerCommands["import"].Description, true) + flChanges := opts.NewListOpts(nil) + cmd.Var(&flChanges, []string{"c", "-change"}, "Apply Dockerfile instruction to the created image") + message := cmd.String([]string{"m", "-message"}, "", "Set commit message for imported image") + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + var ( + in io.Reader + tag string + src = cmd.Arg(0) + srcName = src + repository = cmd.Arg(1) + changes = flChanges.GetAll() + ) + + if cmd.NArg() == 3 { + fmt.Fprintf(cli.err, "[DEPRECATED] The format 'file|URL|- [REPOSITORY [TAG]]' has been deprecated. Please use file|URL|- [REPOSITORY[:TAG]]\n") + tag = cmd.Arg(2) + } + + if repository != "" { + //Check if the given image name can be resolved + if _, err := reference.ParseNamed(repository); err != nil { + return err + } + } + + if src == "-" { + in = cli.in + } else if !urlutil.IsURL(src) { + srcName = "-" + file, err := os.Open(src) + if err != nil { + return err + } + defer file.Close() + in = file + } + + options := types.ImageImportOptions{ + Source: in, + SourceName: srcName, + RepositoryName: repository, + Message: *message, + Tag: tag, + Changes: changes, + } + + responseBody, err := cli.client.ImageImport(context.Background(), options) + if err != nil { + return err + } + defer responseBody.Close() + + return jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut, nil) +} diff --git a/api/client/info.go b/api/client/info.go new file mode 100644 index 00000000..29df605e --- /dev/null +++ b/api/client/info.go @@ -0,0 +1,155 @@ +package client + +import ( + "fmt" + "strings" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + "github.com/docker/docker/pkg/ioutils" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/utils" + "github.com/docker/go-units" +) + +// CmdInfo displays system-wide information. +// +// Usage: docker info +func (cli *DockerCli) CmdInfo(args ...string) error { + cmd := Cli.Subcmd("info", nil, Cli.DockerCommands["info"].Description, true) + cmd.Require(flag.Exact, 0) + + cmd.ParseFlags(args, true) + + info, err := cli.client.Info(context.Background()) + if err != nil { + return err + } + + fmt.Fprintf(cli.out, "Containers: %d\n", info.Containers) + fmt.Fprintf(cli.out, " Running: %d\n", info.ContainersRunning) + fmt.Fprintf(cli.out, " Paused: %d\n", info.ContainersPaused) + fmt.Fprintf(cli.out, " Stopped: %d\n", info.ContainersStopped) + fmt.Fprintf(cli.out, "Images: %d\n", info.Images) + ioutils.FprintfIfNotEmpty(cli.out, "Server Version: %s\n", info.ServerVersion) + ioutils.FprintfIfNotEmpty(cli.out, "Storage Driver: %s\n", info.Driver) + if info.DriverStatus != nil { + for _, pair := range info.DriverStatus { + fmt.Fprintf(cli.out, " %s: %s\n", pair[0], pair[1]) + + // print a warning if devicemapper is using a loopback file + if pair[0] == "Data loop file" { + fmt.Fprintln(cli.err, " WARNING: Usage of loopback devices is strongly discouraged for production use. Either use `--storage-opt dm.thinpooldev` or use `--storage-opt dm.no_warn_on_loop_devices=true` to suppress this warning.") + } + } + + } + if info.SystemStatus != nil { + for _, pair := range info.SystemStatus { + fmt.Fprintf(cli.out, "%s: %s\n", pair[0], pair[1]) + } + } + ioutils.FprintfIfNotEmpty(cli.out, "Execution Driver: %s\n", info.ExecutionDriver) + ioutils.FprintfIfNotEmpty(cli.out, "Logging Driver: %s\n", info.LoggingDriver) + ioutils.FprintfIfNotEmpty(cli.out, "Cgroup Driver: %s\n", info.CgroupDriver) + + fmt.Fprintf(cli.out, "Plugins: \n") + fmt.Fprintf(cli.out, " Volume:") + fmt.Fprintf(cli.out, " %s", strings.Join(info.Plugins.Volume, " ")) + fmt.Fprintf(cli.out, "\n") + fmt.Fprintf(cli.out, " Network:") + fmt.Fprintf(cli.out, " %s", strings.Join(info.Plugins.Network, " ")) + fmt.Fprintf(cli.out, "\n") + + if len(info.Plugins.Authorization) != 0 { + fmt.Fprintf(cli.out, " Authorization:") + fmt.Fprintf(cli.out, " %s", strings.Join(info.Plugins.Authorization, " ")) + fmt.Fprintf(cli.out, "\n") + } + + ioutils.FprintfIfNotEmpty(cli.out, "Kernel Version: %s\n", info.KernelVersion) + ioutils.FprintfIfNotEmpty(cli.out, "Operating System: %s\n", info.OperatingSystem) + ioutils.FprintfIfNotEmpty(cli.out, "OSType: %s\n", info.OSType) + ioutils.FprintfIfNotEmpty(cli.out, "Architecture: %s\n", info.Architecture) + fmt.Fprintf(cli.out, "CPUs: %d\n", info.NCPU) + fmt.Fprintf(cli.out, "Total Memory: %s\n", units.BytesSize(float64(info.MemTotal))) + ioutils.FprintfIfNotEmpty(cli.out, "Name: %s\n", info.Name) + ioutils.FprintfIfNotEmpty(cli.out, "ID: %s\n", info.ID) + fmt.Fprintf(cli.out, "Docker Root Dir: %s\n", info.DockerRootDir) + fmt.Fprintf(cli.out, "Debug mode (client): %v\n", utils.IsDebugEnabled()) + fmt.Fprintf(cli.out, "Debug mode (server): %v\n", info.Debug) + + if info.Debug { + fmt.Fprintf(cli.out, " File Descriptors: %d\n", info.NFd) + fmt.Fprintf(cli.out, " Goroutines: %d\n", info.NGoroutines) + fmt.Fprintf(cli.out, " System Time: %s\n", info.SystemTime) + fmt.Fprintf(cli.out, " EventsListeners: %d\n", info.NEventsListener) + } + + ioutils.FprintfIfNotEmpty(cli.out, "Http Proxy: %s\n", info.HTTPProxy) + ioutils.FprintfIfNotEmpty(cli.out, "Https Proxy: %s\n", info.HTTPSProxy) + ioutils.FprintfIfNotEmpty(cli.out, "No Proxy: %s\n", info.NoProxy) + + if info.IndexServerAddress != "" { + u := cli.configFile.AuthConfigs[info.IndexServerAddress].Username + if len(u) > 0 { + fmt.Fprintf(cli.out, "Username: %v\n", u) + } + fmt.Fprintf(cli.out, "Registry: %v\n", info.IndexServerAddress) + } + + // Only output these warnings if the server does not support these features + if info.OSType != "windows" { + if !info.MemoryLimit { + fmt.Fprintln(cli.err, "WARNING: No memory limit support") + } + if !info.SwapLimit { + fmt.Fprintln(cli.err, "WARNING: No swap limit support") + } + if !info.KernelMemory { + fmt.Fprintln(cli.err, "WARNING: No kernel memory limit support") + } + if !info.OomKillDisable { + fmt.Fprintln(cli.err, "WARNING: No oom kill disable support") + } + if !info.CPUCfsQuota { + fmt.Fprintln(cli.err, "WARNING: No cpu cfs quota support") + } + if !info.CPUCfsPeriod { + fmt.Fprintln(cli.err, "WARNING: No cpu cfs period support") + } + if !info.CPUShares { + fmt.Fprintln(cli.err, "WARNING: No cpu shares support") + } + if !info.CPUSet { + fmt.Fprintln(cli.err, "WARNING: No cpuset support") + } + if !info.IPv4Forwarding { + fmt.Fprintln(cli.err, "WARNING: IPv4 forwarding is disabled") + } + if !info.BridgeNfIptables { + fmt.Fprintln(cli.err, "WARNING: bridge-nf-call-iptables is disabled") + } + if !info.BridgeNfIP6tables { + fmt.Fprintln(cli.err, "WARNING: bridge-nf-call-ip6tables is disabled") + } + } + + if info.Labels != nil { + fmt.Fprintln(cli.out, "Labels:") + for _, attribute := range info.Labels { + fmt.Fprintf(cli.out, " %s\n", attribute) + } + } + + ioutils.FprintfIfTrue(cli.out, "Experimental: %v\n", info.ExperimentalBuild) + if info.ClusterStore != "" { + fmt.Fprintf(cli.out, "Cluster store: %s\n", info.ClusterStore) + } + + if info.ClusterAdvertise != "" { + fmt.Fprintf(cli.out, "Cluster advertise: %s\n", info.ClusterAdvertise) + } + return nil +} diff --git a/api/client/inspect.go b/api/client/inspect.go new file mode 100644 index 00000000..2e97a5aa --- /dev/null +++ b/api/client/inspect.go @@ -0,0 +1,127 @@ +package client + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/client/inspect" + Cli "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/utils/templates" + "github.com/docker/engine-api/client" +) + +// CmdInspect displays low-level information on one or more containers or images. +// +// Usage: docker inspect [OPTIONS] CONTAINER|IMAGE [CONTAINER|IMAGE...] +func (cli *DockerCli) CmdInspect(args ...string) error { + cmd := Cli.Subcmd("inspect", []string{"CONTAINER|IMAGE [CONTAINER|IMAGE...]"}, Cli.DockerCommands["inspect"].Description, true) + tmplStr := cmd.String([]string{"f", "-format"}, "", "Format the output using the given go template") + inspectType := cmd.String([]string{"-type"}, "", "Return JSON for specified type, (e.g image or container)") + size := cmd.Bool([]string{"s", "-size"}, false, "Display total file sizes if the type is container") + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + if *inspectType != "" && *inspectType != "container" && *inspectType != "image" { + return fmt.Errorf("%q is not a valid value for --type", *inspectType) + } + + var elementSearcher inspectSearcher + switch *inspectType { + case "container": + elementSearcher = cli.inspectContainers(*size) + case "image": + elementSearcher = cli.inspectImages(*size) + default: + elementSearcher = cli.inspectAll(*size) + } + + return cli.inspectElements(*tmplStr, cmd.Args(), elementSearcher) +} + +func (cli *DockerCli) inspectContainers(getSize bool) inspectSearcher { + return func(ref string) (interface{}, []byte, error) { + return cli.client.ContainerInspectWithRaw(context.Background(), ref, getSize) + } +} + +func (cli *DockerCli) inspectImages(getSize bool) inspectSearcher { + return func(ref string) (interface{}, []byte, error) { + return cli.client.ImageInspectWithRaw(context.Background(), ref, getSize) + } +} + +func (cli *DockerCli) inspectAll(getSize bool) inspectSearcher { + return func(ref string) (interface{}, []byte, error) { + c, rawContainer, err := cli.client.ContainerInspectWithRaw(context.Background(), ref, getSize) + if err != nil { + // Search for image with that id if a container doesn't exist. + if client.IsErrContainerNotFound(err) { + i, rawImage, err := cli.client.ImageInspectWithRaw(context.Background(), ref, getSize) + if err != nil { + if client.IsErrImageNotFound(err) { + return nil, nil, fmt.Errorf("Error: No such image or container: %s", ref) + } + return nil, nil, err + } + return i, rawImage, err + } + return nil, nil, err + } + return c, rawContainer, err + } +} + +type inspectSearcher func(ref string) (interface{}, []byte, error) + +func (cli *DockerCli) inspectElements(tmplStr string, references []string, searchByReference inspectSearcher) error { + elementInspector, err := cli.newInspectorWithTemplate(tmplStr) + if err != nil { + return Cli.StatusError{StatusCode: 64, Status: err.Error()} + } + + var inspectErr error + for _, ref := range references { + element, raw, err := searchByReference(ref) + if err != nil { + inspectErr = err + break + } + + if err := elementInspector.Inspect(element, raw); err != nil { + inspectErr = err + break + } + } + + if err := elementInspector.Flush(); err != nil { + cli.inspectErrorStatus(err) + } + + if status := cli.inspectErrorStatus(inspectErr); status != 0 { + return Cli.StatusError{StatusCode: status} + } + return nil +} + +func (cli *DockerCli) inspectErrorStatus(err error) (status int) { + if err != nil { + fmt.Fprintf(cli.err, "%s\n", err) + status = 1 + } + return +} + +func (cli *DockerCli) newInspectorWithTemplate(tmplStr string) (inspect.Inspector, error) { + elementInspector := inspect.NewIndentedInspector(cli.out) + if tmplStr != "" { + tmpl, err := templates.Parse(tmplStr) + if err != nil { + return nil, fmt.Errorf("Template parsing error: %s", err) + } + elementInspector = inspect.NewTemplateInspector(cli.out, tmpl) + } + return elementInspector, nil +} diff --git a/api/client/inspect/inspector.go b/api/client/inspect/inspector.go new file mode 100644 index 00000000..a1d16d47 --- /dev/null +++ b/api/client/inspect/inspector.go @@ -0,0 +1,119 @@ +package inspect + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "text/template" +) + +// 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, + } +} + +// 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 fmt.Errorf("Template parsing error: %v", err) + } + return i.tryRawInspectFallback(rawElement, err) + } + i.buffer.Write(buffer.Bytes()) + i.buffer.WriteByte('\n') + return nil +} + +// Flush write 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 write 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/api/client/inspect/inspector_go14.go b/api/client/inspect/inspector_go14.go new file mode 100644 index 00000000..39a0510c --- /dev/null +++ b/api/client/inspect/inspector_go14.go @@ -0,0 +1,40 @@ +// +build !go1.5 + +package inspect + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" +) + +// tryeRawInspectFallback executes the inspect template with a raw interface. +// This allows docker cli to parse inspect structs injected with Swarm fields. +// Unfortunately, go 1.4 doesn't fail executing invalid templates when the input is an interface. +// It doesn't allow to modify this behavior either, sending messages to the output. +// We assume that the template is invalid when there is a , if the template was valid +// we'd get or "" values. In that case we fail with the original error raised executing the +// template with the typed input. +func (i *TemplateInspector) tryRawInspectFallback(rawElement []byte, originalErr error) error { + var raw interface{} + buffer := new(bytes.Buffer) + rdr := bytes.NewReader(rawElement) + dec := json.NewDecoder(rdr) + + if rawErr := dec.Decode(&raw); rawErr != nil { + return fmt.Errorf("unable to read inspect data: %v", rawErr) + } + + if rawErr := i.tmpl.Execute(buffer, raw); rawErr != nil { + return fmt.Errorf("Template parsing error: %v", rawErr) + } + + if strings.Contains(buffer.String(), "") { + return fmt.Errorf("Template parsing error: %v", originalErr) + } + + i.buffer.Write(buffer.Bytes()) + i.buffer.WriteByte('\n') + return nil +} diff --git a/api/client/inspect/inspector_go15.go b/api/client/inspect/inspector_go15.go new file mode 100644 index 00000000..b098f415 --- /dev/null +++ b/api/client/inspect/inspector_go15.go @@ -0,0 +1,29 @@ +// +build go1.5 + +package inspect + +import ( + "bytes" + "encoding/json" + "fmt" +) + +func (i *TemplateInspector) tryRawInspectFallback(rawElement []byte, _ error) error { + var raw interface{} + buffer := new(bytes.Buffer) + rdr := bytes.NewReader(rawElement) + dec := json.NewDecoder(rdr) + + if rawErr := dec.Decode(&raw); rawErr != nil { + return fmt.Errorf("unable to read inspect data: %v", rawErr) + } + + tmplMissingKey := i.tmpl.Option("missingkey=error") + if rawErr := tmplMissingKey.Execute(buffer, raw); rawErr != nil { + return fmt.Errorf("Template parsing error: %v", rawErr) + } + + i.buffer.Write(buffer.Bytes()) + i.buffer.WriteByte('\n') + return nil +} diff --git a/api/client/inspect/inspector_test.go b/api/client/inspect/inspector_test.go new file mode 100644 index 00000000..1ce1593a --- /dev/null +++ b/api/client/inspect/inspector_test.go @@ -0,0 +1,221 @@ +package inspect + +import ( + "bytes" + "strings" + "testing" + + "github.com/docker/docker/utils/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/api/client/kill.go b/api/client/kill.go new file mode 100644 index 00000000..9841ba4d --- /dev/null +++ b/api/client/kill.go @@ -0,0 +1,35 @@ +package client + +import ( + "fmt" + "strings" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdKill kills one or more running container using SIGKILL or a specified signal. +// +// Usage: docker kill [OPTIONS] CONTAINER [CONTAINER...] +func (cli *DockerCli) CmdKill(args ...string) error { + cmd := Cli.Subcmd("kill", []string{"CONTAINER [CONTAINER...]"}, Cli.DockerCommands["kill"].Description, true) + signal := cmd.String([]string{"s", "-signal"}, "KILL", "Signal to send to the container") + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + var errs []string + for _, name := range cmd.Args() { + if err := cli.client.ContainerKill(context.Background(), name, *signal); err != nil { + errs = append(errs, err.Error()) + } else { + fmt.Fprintf(cli.out, "%s\n", name) + } + } + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) + } + return nil +} diff --git a/api/client/load.go b/api/client/load.go new file mode 100644 index 00000000..820fdc0e --- /dev/null +++ b/api/client/load.go @@ -0,0 +1,50 @@ +package client + +import ( + "io" + "os" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + "github.com/docker/docker/pkg/jsonmessage" + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdLoad loads an image from a tar archive. +// +// The tar archive is read from STDIN by default, or from a tar archive file. +// +// Usage: docker load [OPTIONS] +func (cli *DockerCli) CmdLoad(args ...string) error { + cmd := Cli.Subcmd("load", nil, Cli.DockerCommands["load"].Description, true) + infile := cmd.String([]string{"i", "-input"}, "", "Read from a tar archive file, instead of STDIN") + quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Suppress the load output") + cmd.Require(flag.Exact, 0) + cmd.ParseFlags(args, true) + + var input io.Reader = cli.in + if *infile != "" { + file, err := os.Open(*infile) + if err != nil { + return err + } + defer file.Close() + input = file + } + if !cli.isTerminalOut { + *quiet = true + } + response, err := cli.client.ImageLoad(context.Background(), input, *quiet) + if err != nil { + return err + } + defer response.Body.Close() + + if response.Body != nil && response.JSON { + return jsonmessage.DisplayJSONMessagesStream(response.Body, cli.out, cli.outFd, cli.isTerminalOut, nil) + } + + _, err = io.Copy(cli.out, response.Body) + return err +} diff --git a/api/client/login.go b/api/client/login.go new file mode 100644 index 00000000..a772348c --- /dev/null +++ b/api/client/login.go @@ -0,0 +1,177 @@ +package client + +import ( + "bufio" + "fmt" + "io" + "os" + "runtime" + "strings" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/cliconfig/credentials" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/term" + "github.com/docker/engine-api/types" +) + +// CmdLogin logs in a user to a Docker registry service. +// +// If no server is specified, the user will be logged into or registered to the registry's index server. +// +// Usage: docker login SERVER +func (cli *DockerCli) CmdLogin(args ...string) error { + cmd := Cli.Subcmd("login", []string{"[SERVER]"}, Cli.DockerCommands["login"].Description+".\nIf no server is specified, the default is defined by the daemon.", true) + cmd.Require(flag.Max, 1) + + flUser := cmd.String([]string{"u", "-username"}, "", "Username") + flPassword := cmd.String([]string{"p", "-password"}, "", "Password") + + // Deprecated in 1.11: Should be removed in docker 1.13 + cmd.String([]string{"#e", "#-email"}, "", "Email") + + cmd.ParseFlags(args, true) + + // On Windows, force the use of the regular OS stdin stream. Fixes #14336/#14210 + if runtime.GOOS == "windows" { + cli.in = os.Stdin + } + + var serverAddress string + var isDefaultRegistry bool + if len(cmd.Args()) > 0 { + serverAddress = cmd.Arg(0) + } else { + serverAddress = cli.electAuthServer() + isDefaultRegistry = true + } + + authConfig, err := cli.configureAuth(*flUser, *flPassword, serverAddress, isDefaultRegistry) + if err != nil { + return err + } + + response, err := cli.client.RegistryLogin(context.Background(), authConfig) + if err != nil { + return err + } + + if response.IdentityToken != "" { + authConfig.Password = "" + authConfig.IdentityToken = response.IdentityToken + } + if err := storeCredentials(cli.configFile, authConfig); err != nil { + return fmt.Errorf("Error saving credentials: %v", err) + } + + if response.Status != "" { + fmt.Fprintln(cli.out, response.Status) + } + return nil +} + +func (cli *DockerCli) promptWithDefault(prompt string, configDefault string) { + if configDefault == "" { + fmt.Fprintf(cli.out, "%s: ", prompt) + } else { + fmt.Fprintf(cli.out, "%s (%s): ", prompt, configDefault) + } +} + +func (cli *DockerCli) configureAuth(flUser, flPassword, serverAddress string, isDefaultRegistry bool) (types.AuthConfig, error) { + authconfig, err := getCredentials(cli.configFile, serverAddress) + if err != nil { + return authconfig, err + } + + authconfig.Username = strings.TrimSpace(authconfig.Username) + + if flUser = strings.TrimSpace(flUser); flUser == "" { + if isDefaultRegistry { + // if this is a defauly 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.") + } + cli.promptWithDefault("Username", authconfig.Username) + flUser = readInput(cli.in, cli.out) + flUser = strings.TrimSpace(flUser) + if flUser == "" { + flUser = authconfig.Username + } + } + + if flUser == "" { + return authconfig, fmt.Errorf("Error: Non-null Username Required") + } + + if flPassword == "" { + oldState, err := term.SaveState(cli.inFd) + if err != nil { + return authconfig, err + } + fmt.Fprintf(cli.out, "Password: ") + term.DisableEcho(cli.inFd, oldState) + + flPassword = readInput(cli.in, cli.out) + fmt.Fprint(cli.out, "\n") + + term.RestoreTerminal(cli.inFd, oldState) + if flPassword == "" { + return authconfig, fmt.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) +} + +// getCredentials loads the user credentials from a credentials store. +// The store is determined by the config file settings. +func getCredentials(c *cliconfig.ConfigFile, serverAddress string) (types.AuthConfig, error) { + s := loadCredentialsStore(c) + return s.Get(serverAddress) +} + +func getAllCredentials(c *cliconfig.ConfigFile) (map[string]types.AuthConfig, error) { + s := loadCredentialsStore(c) + return s.GetAll() +} + +// storeCredentials saves the user credentials in a credentials store. +// The store is determined by the config file settings. +func storeCredentials(c *cliconfig.ConfigFile, auth types.AuthConfig) error { + s := loadCredentialsStore(c) + return s.Store(auth) +} + +// eraseCredentials removes the user credentials from a credentials store. +// The store is determined by the config file settings. +func eraseCredentials(c *cliconfig.ConfigFile, serverAddress string) error { + s := loadCredentialsStore(c) + return s.Erase(serverAddress) +} + +// loadCredentialsStore initializes a new credentials store based +// in the settings provided in the configuration file. +func loadCredentialsStore(c *cliconfig.ConfigFile) credentials.Store { + if c.CredentialsStore != "" { + return credentials.NewNativeStore(c) + } + return credentials.NewFileStore(c) +} diff --git a/api/client/logout.go b/api/client/logout.go new file mode 100644 index 00000000..b5ff59dd --- /dev/null +++ b/api/client/logout.go @@ -0,0 +1,41 @@ +package client + +import ( + "fmt" + + Cli "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdLogout logs a user out from a Docker registry. +// +// If no server is specified, the user will be logged out from the registry's index server. +// +// Usage: docker logout [SERVER] +func (cli *DockerCli) CmdLogout(args ...string) error { + cmd := Cli.Subcmd("logout", []string{"[SERVER]"}, Cli.DockerCommands["logout"].Description+".\nIf no server is specified, the default is defined by the daemon.", true) + cmd.Require(flag.Max, 1) + + cmd.ParseFlags(args, true) + + var serverAddress string + if len(cmd.Args()) > 0 { + serverAddress = cmd.Arg(0) + } else { + serverAddress = cli.electAuthServer() + } + + // 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 + if _, ok := cli.configFile.AuthConfigs[serverAddress]; !ok { + fmt.Fprintf(cli.out, "Not logged in to %s\n", serverAddress) + return nil + } + + fmt.Fprintf(cli.out, "Remove login credentials for %s\n", serverAddress) + if err := eraseCredentials(cli.configFile, serverAddress); err != nil { + fmt.Fprintf(cli.out, "WARNING: could not erase credentials: %v\n", err) + } + + return nil +} diff --git a/api/client/logs.go b/api/client/logs.go new file mode 100644 index 00000000..7cd5605e --- /dev/null +++ b/api/client/logs.go @@ -0,0 +1,65 @@ +package client + +import ( + "fmt" + "io" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/stdcopy" + "github.com/docker/engine-api/types" +) + +var validDrivers = map[string]bool{ + "json-file": true, + "journald": true, +} + +// CmdLogs fetches the logs of a given container. +// +// docker logs [OPTIONS] CONTAINER +func (cli *DockerCli) CmdLogs(args ...string) error { + cmd := Cli.Subcmd("logs", []string{"CONTAINER"}, Cli.DockerCommands["logs"].Description, true) + follow := cmd.Bool([]string{"f", "-follow"}, false, "Follow log output") + since := cmd.String([]string{"-since"}, "", "Show logs since timestamp") + times := cmd.Bool([]string{"t", "-timestamps"}, false, "Show timestamps") + tail := cmd.String([]string{"-tail"}, "all", "Number of lines to show from the end of the logs") + cmd.Require(flag.Exact, 1) + + cmd.ParseFlags(args, true) + + name := cmd.Arg(0) + + c, err := cli.client.ContainerInspect(context.Background(), name) + if err != nil { + return err + } + + if !validDrivers[c.HostConfig.LogConfig.Type] { + return fmt.Errorf("\"logs\" command is supported only for \"json-file\" and \"journald\" logging drivers (got: %s)", c.HostConfig.LogConfig.Type) + } + + options := types.ContainerLogsOptions{ + ContainerID: name, + ShowStdout: true, + ShowStderr: true, + Since: *since, + Timestamps: *times, + Follow: *follow, + Tail: *tail, + } + responseBody, err := cli.client.ContainerLogs(context.Background(), options) + if err != nil { + return err + } + defer responseBody.Close() + + if c.Config.Tty { + _, err = io.Copy(cli.out, responseBody) + } else { + _, err = stdcopy.StdCopy(cli.out, cli.err, responseBody) + } + return err +} diff --git a/api/client/network.go b/api/client/network.go new file mode 100644 index 00000000..4bbc7154 --- /dev/null +++ b/api/client/network.go @@ -0,0 +1,392 @@ +package client + +import ( + "fmt" + "net" + "sort" + "strings" + "text/tabwriter" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + "github.com/docker/docker/opts" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/stringid" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/filters" + "github.com/docker/engine-api/types/network" +) + +// CmdNetwork is the parent subcommand for all network commands +// +// Usage: docker network [OPTIONS] +func (cli *DockerCli) CmdNetwork(args ...string) error { + cmd := Cli.Subcmd("network", []string{"COMMAND [OPTIONS]"}, networkUsage(), false) + cmd.Require(flag.Min, 1) + err := cmd.ParseFlags(args, true) + cmd.Usage() + return err +} + +// CmdNetworkCreate creates a new network with a given name +// +// Usage: docker network create [OPTIONS] +func (cli *DockerCli) CmdNetworkCreate(args ...string) error { + cmd := Cli.Subcmd("network create", []string{"NETWORK-NAME"}, "Creates a new network with a name specified by the user", false) + flDriver := cmd.String([]string{"d", "-driver"}, "bridge", "Driver to manage the Network") + flOpts := opts.NewMapOpts(nil, nil) + + flIpamDriver := cmd.String([]string{"-ipam-driver"}, "default", "IP Address Management Driver") + flIpamSubnet := opts.NewListOpts(nil) + flIpamIPRange := opts.NewListOpts(nil) + flIpamGateway := opts.NewListOpts(nil) + flIpamAux := opts.NewMapOpts(nil, nil) + flIpamOpt := opts.NewMapOpts(nil, nil) + flLabels := opts.NewListOpts(nil) + + cmd.Var(&flIpamSubnet, []string{"-subnet"}, "subnet in CIDR format that represents a network segment") + cmd.Var(&flIpamIPRange, []string{"-ip-range"}, "allocate container ip from a sub-range") + cmd.Var(&flIpamGateway, []string{"-gateway"}, "ipv4 or ipv6 Gateway for the master subnet") + cmd.Var(flIpamAux, []string{"-aux-address"}, "auxiliary ipv4 or ipv6 addresses used by Network driver") + cmd.Var(flOpts, []string{"o", "-opt"}, "set driver specific options") + cmd.Var(flIpamOpt, []string{"-ipam-opt"}, "set IPAM driver specific options") + cmd.Var(&flLabels, []string{"-label"}, "set metadata on a network") + + flInternal := cmd.Bool([]string{"-internal"}, false, "restricts external access to the network") + flIPv6 := cmd.Bool([]string{"-ipv6"}, false, "enable IPv6 networking") + + cmd.Require(flag.Exact, 1) + err := cmd.ParseFlags(args, true) + if err != nil { + return err + } + + // Set the default driver to "" if the user didn't set the value. + // That way we can know whether it was user input or not. + driver := *flDriver + if !cmd.IsSet("-driver") && !cmd.IsSet("d") { + driver = "" + } + + ipamCfg, err := consolidateIpam(flIpamSubnet.GetAll(), flIpamIPRange.GetAll(), flIpamGateway.GetAll(), flIpamAux.GetAll()) + if err != nil { + return err + } + + // Construct network create request body + nc := types.NetworkCreate{ + Name: cmd.Arg(0), + Driver: driver, + IPAM: network.IPAM{Driver: *flIpamDriver, Config: ipamCfg, Options: flIpamOpt.GetAll()}, + Options: flOpts.GetAll(), + CheckDuplicate: true, + Internal: *flInternal, + EnableIPv6: *flIPv6, + Labels: runconfigopts.ConvertKVStringsToMap(flLabels.GetAll()), + } + + resp, err := cli.client.NetworkCreate(context.Background(), nc) + if err != nil { + return err + } + fmt.Fprintf(cli.out, "%s\n", resp.ID) + return nil +} + +// CmdNetworkRm deletes one or more networks +// +// Usage: docker network rm NETWORK-NAME|NETWORK-ID [NETWORK-NAME|NETWORK-ID...] +func (cli *DockerCli) CmdNetworkRm(args ...string) error { + cmd := Cli.Subcmd("network rm", []string{"NETWORK [NETWORK...]"}, "Deletes one or more networks", false) + cmd.Require(flag.Min, 1) + if err := cmd.ParseFlags(args, true); err != nil { + return err + } + + status := 0 + for _, net := range cmd.Args() { + if err := cli.client.NetworkRemove(context.Background(), net); err != nil { + fmt.Fprintf(cli.err, "%s\n", err) + status = 1 + continue + } + } + if status != 0 { + return Cli.StatusError{StatusCode: status} + } + return nil +} + +// CmdNetworkConnect connects a container to a network +// +// Usage: docker network connect [OPTIONS] +func (cli *DockerCli) CmdNetworkConnect(args ...string) error { + cmd := Cli.Subcmd("network connect", []string{"NETWORK CONTAINER"}, "Connects a container to a network", false) + flIPAddress := cmd.String([]string{"-ip"}, "", "IP Address") + flIPv6Address := cmd.String([]string{"-ip6"}, "", "IPv6 Address") + flLinks := opts.NewListOpts(runconfigopts.ValidateLink) + cmd.Var(&flLinks, []string{"-link"}, "Add link to another container") + flAliases := opts.NewListOpts(nil) + cmd.Var(&flAliases, []string{"-alias"}, "Add network-scoped alias for the container") + cmd.Require(flag.Min, 2) + if err := cmd.ParseFlags(args, true); err != nil { + return err + } + epConfig := &network.EndpointSettings{ + IPAMConfig: &network.EndpointIPAMConfig{ + IPv4Address: *flIPAddress, + IPv6Address: *flIPv6Address, + }, + Links: flLinks.GetAll(), + Aliases: flAliases.GetAll(), + } + return cli.client.NetworkConnect(context.Background(), cmd.Arg(0), cmd.Arg(1), epConfig) +} + +// CmdNetworkDisconnect disconnects a container from a network +// +// Usage: docker network disconnect +func (cli *DockerCli) CmdNetworkDisconnect(args ...string) error { + cmd := Cli.Subcmd("network disconnect", []string{"NETWORK CONTAINER"}, "Disconnects container from a network", false) + force := cmd.Bool([]string{"f", "-force"}, false, "Force the container to disconnect from a network") + cmd.Require(flag.Exact, 2) + if err := cmd.ParseFlags(args, true); err != nil { + return err + } + + return cli.client.NetworkDisconnect(context.Background(), cmd.Arg(0), cmd.Arg(1), *force) +} + +// CmdNetworkLs lists all the networks managed by docker daemon +// +// Usage: docker network ls [OPTIONS] +func (cli *DockerCli) CmdNetworkLs(args ...string) error { + cmd := Cli.Subcmd("network ls", nil, "Lists networks", true) + quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only display numeric IDs") + noTrunc := cmd.Bool([]string{"-no-trunc"}, false, "Do not truncate the output") + + flFilter := opts.NewListOpts(nil) + cmd.Var(&flFilter, []string{"f", "-filter"}, "Filter output based on conditions provided") + + cmd.Require(flag.Exact, 0) + err := cmd.ParseFlags(args, true) + if err != nil { + return err + } + + // Consolidate all filter flags, and sanity check them early. + // They'll get process after get response from server. + netFilterArgs := filters.NewArgs() + for _, f := range flFilter.GetAll() { + if netFilterArgs, err = filters.ParseFlag(f, netFilterArgs); err != nil { + return err + } + } + + options := types.NetworkListOptions{ + Filters: netFilterArgs, + } + + networkResources, err := cli.client.NetworkList(context.Background(), options) + if err != nil { + return err + } + + wr := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) + + // unless quiet (-q) is specified, print field titles + if !*quiet { + fmt.Fprintln(wr, "NETWORK ID\tNAME\tDRIVER") + } + sort.Sort(byNetworkName(networkResources)) + for _, networkResource := range networkResources { + ID := networkResource.ID + netName := networkResource.Name + if !*noTrunc { + ID = stringid.TruncateID(ID) + } + if *quiet { + fmt.Fprintln(wr, ID) + continue + } + driver := networkResource.Driver + fmt.Fprintf(wr, "%s\t%s\t%s\t", + ID, + netName, + driver) + fmt.Fprint(wr, "\n") + } + wr.Flush() + return nil +} + +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 } + +// CmdNetworkInspect inspects the network object for more details +// +// Usage: docker network inspect [OPTIONS] [NETWORK...] +func (cli *DockerCli) CmdNetworkInspect(args ...string) error { + cmd := Cli.Subcmd("network inspect", []string{"NETWORK [NETWORK...]"}, "Displays detailed information on one or more networks", false) + tmplStr := cmd.String([]string{"f", "-format"}, "", "Format the output using the given go template") + cmd.Require(flag.Min, 1) + + if err := cmd.ParseFlags(args, true); err != nil { + return err + } + + inspectSearcher := func(name string) (interface{}, []byte, error) { + i, err := cli.client.NetworkInspect(context.Background(), name) + return i, nil, err + } + + return cli.inspectElements(*tmplStr, cmd.Args(), inspectSearcher) +} + +// 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. +// consoidateIpam 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, fmt.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, fmt.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, fmt.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, fmt.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, fmt.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, fmt.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, fmt.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, fmt.Errorf("Invalid subnet %s : %v", s, err) + } + + if strings.Contains(data, "/") { + ip, _, err = net.ParseCIDR(data) + if err != nil { + return false, fmt.Errorf("Invalid cidr %s : %v", data, err) + } + } else { + ip = net.ParseIP(data) + } + + return s.Contains(ip), nil +} + +func networkUsage() string { + networkCommands := map[string]string{ + "create": "Create a network", + "connect": "Connect container to a network", + "disconnect": "Disconnect container from a network", + "inspect": "Display detailed network information", + "ls": "List all networks", + "rm": "Remove a network", + } + + help := "Commands:\n" + + for cmd, description := range networkCommands { + help += fmt.Sprintf(" %-25.25s%s\n", cmd, description) + } + + help += fmt.Sprintf("\nRun 'docker network COMMAND --help' for more information on a command.") + return help +} diff --git a/api/client/pause.go b/api/client/pause.go new file mode 100644 index 00000000..ffba1c9a --- /dev/null +++ b/api/client/pause.go @@ -0,0 +1,34 @@ +package client + +import ( + "fmt" + "strings" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdPause pauses all processes within one or more containers. +// +// Usage: docker pause CONTAINER [CONTAINER...] +func (cli *DockerCli) CmdPause(args ...string) error { + cmd := Cli.Subcmd("pause", []string{"CONTAINER [CONTAINER...]"}, Cli.DockerCommands["pause"].Description, true) + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + var errs []string + for _, name := range cmd.Args() { + if err := cli.client.ContainerPause(context.Background(), name); err != nil { + errs = append(errs, err.Error()) + } else { + fmt.Fprintf(cli.out, "%s\n", name) + } + } + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) + } + return nil +} diff --git a/api/client/port.go b/api/client/port.go new file mode 100644 index 00000000..9b545f56 --- /dev/null +++ b/api/client/port.go @@ -0,0 +1,61 @@ +package client + +import ( + "fmt" + "strings" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/go-connections/nat" +) + +// CmdPort lists port mappings for a container. +// If a private port is specified, it also shows the public-facing port that is NATed to the private port. +// +// Usage: docker port CONTAINER [PRIVATE_PORT[/PROTO]] +func (cli *DockerCli) CmdPort(args ...string) error { + cmd := Cli.Subcmd("port", []string{"CONTAINER [PRIVATE_PORT[/PROTO]]"}, Cli.DockerCommands["port"].Description, true) + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + c, err := cli.client.ContainerInspect(context.Background(), cmd.Arg(0)) + if err != nil { + return err + } + + if cmd.NArg() == 2 { + var ( + port = cmd.Arg(1) + 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(cli.out, "%s:%s\n", frontend.HostIP, frontend.HostPort) + } + return nil + } + return fmt.Errorf("Error: No public port '%s' published for %s", natPort, cmd.Arg(0)) + } + + for from, frontends := range c.NetworkSettings.Ports { + for _, frontend := range frontends { + fmt.Fprintf(cli.out, "%s -> %s:%s\n", from, frontend.HostIP, frontend.HostPort) + } + } + + return nil +} diff --git a/api/client/ps.go b/api/client/ps.go new file mode 100644 index 00000000..3627ffc1 --- /dev/null +++ b/api/client/ps.go @@ -0,0 +1,89 @@ +package client + +import ( + "golang.org/x/net/context" + + "github.com/docker/docker/api/client/formatter" + Cli "github.com/docker/docker/cli" + "github.com/docker/docker/opts" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/filters" +) + +// CmdPs outputs a list of Docker containers. +// +// Usage: docker ps [OPTIONS] +func (cli *DockerCli) CmdPs(args ...string) error { + var ( + err error + + psFilterArgs = filters.NewArgs() + + cmd = Cli.Subcmd("ps", nil, Cli.DockerCommands["ps"].Description, true) + quiet = cmd.Bool([]string{"q", "-quiet"}, false, "Only display numeric IDs") + size = cmd.Bool([]string{"s", "-size"}, false, "Display total file sizes") + all = cmd.Bool([]string{"a", "-all"}, false, "Show all containers (default shows just running)") + noTrunc = cmd.Bool([]string{"-no-trunc"}, false, "Don't truncate output") + nLatest = cmd.Bool([]string{"l", "-latest"}, false, "Show the latest created container (includes all states)") + since = cmd.String([]string{"#-since"}, "", "Show containers created since Id or Name (includes all states)") + before = cmd.String([]string{"#-before"}, "", "Only show containers created before Id or Name") + last = cmd.Int([]string{"n"}, -1, "Show n last created containers (includes all states)") + format = cmd.String([]string{"-format"}, "", "Pretty-print containers using a Go template") + flFilter = opts.NewListOpts(nil) + ) + cmd.Require(flag.Exact, 0) + + cmd.Var(&flFilter, []string{"f", "-filter"}, "Filter output based on conditions provided") + + cmd.ParseFlags(args, true) + if *last == -1 && *nLatest { + *last = 1 + } + + // Consolidate all filter flags, and sanity check them. + // They'll get processed in the daemon/server. + for _, f := range flFilter.GetAll() { + if psFilterArgs, err = filters.ParseFlag(f, psFilterArgs); err != nil { + return err + } + } + + options := types.ContainerListOptions{ + All: *all, + Limit: *last, + Since: *since, + Before: *before, + Size: *size, + Filter: psFilterArgs, + } + + containers, err := cli.client.ContainerList(context.Background(), options) + if err != nil { + return err + } + + f := *format + if len(f) == 0 { + if len(cli.PsFormat()) > 0 && !*quiet { + f = cli.PsFormat() + } else { + f = "table" + } + } + + psCtx := formatter.ContainerContext{ + Context: formatter.Context{ + Output: cli.out, + Format: f, + Quiet: *quiet, + Trunc: !*noTrunc, + }, + Size: *size, + Containers: containers, + } + + psCtx.Write() + + return nil +} diff --git a/api/client/pull.go b/api/client/pull.go new file mode 100644 index 00000000..29d9677e --- /dev/null +++ b/api/client/pull.go @@ -0,0 +1,89 @@ +package client + +import ( + "errors" + "fmt" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + "github.com/docker/docker/pkg/jsonmessage" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" + "github.com/docker/engine-api/client" + "github.com/docker/engine-api/types" +) + +// CmdPull pulls an image or a repository from the registry. +// +// Usage: docker pull [OPTIONS] IMAGENAME[:TAG|@DIGEST] +func (cli *DockerCli) CmdPull(args ...string) error { + cmd := Cli.Subcmd("pull", []string{"NAME[:TAG|@DIGEST]"}, Cli.DockerCommands["pull"].Description, true) + allTags := cmd.Bool([]string{"a", "-all-tags"}, false, "Download all tagged images in the repository") + addTrustedFlags(cmd, true) + cmd.Require(flag.Exact, 1) + + cmd.ParseFlags(args, true) + remote := cmd.Arg(0) + + distributionRef, err := reference.ParseNamed(remote) + if err != nil { + return err + } + if *allTags && !reference.IsNameOnly(distributionRef) { + return errors.New("tag can't be used with --all-tags/-a") + } + + if !*allTags && reference.IsNameOnly(distributionRef) { + distributionRef = reference.WithDefaultTag(distributionRef) + fmt.Fprintf(cli.out, "Using default tag: %s\n", reference.DefaultTag) + } + + var tag string + switch x := distributionRef.(type) { + case reference.Canonical: + tag = x.Digest().String() + case reference.NamedTagged: + tag = x.Tag() + } + + ref := registry.ParseReference(tag) + + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := registry.ParseRepositoryInfo(distributionRef) + if err != nil { + return err + } + + authConfig := cli.resolveAuthConfig(repoInfo.Index) + requestPrivilege := cli.registryAuthenticationPrivilegedFunc(repoInfo.Index, "pull") + + if isTrusted() && !ref.HasDigest() { + // Check if tag is digest + return cli.trustedPull(repoInfo, ref, authConfig, requestPrivilege) + } + + return cli.imagePullPrivileged(authConfig, distributionRef.String(), "", requestPrivilege) +} + +func (cli *DockerCli) imagePullPrivileged(authConfig types.AuthConfig, imageID, tag string, requestPrivilege client.RequestPrivilegeFunc) error { + + encodedAuth, err := encodeAuthToBase64(authConfig) + if err != nil { + return err + } + options := types.ImagePullOptions{ + ImageID: imageID, + Tag: tag, + RegistryAuth: encodedAuth, + } + + responseBody, err := cli.client.ImagePull(context.Background(), options, requestPrivilege) + if err != nil { + return err + } + defer responseBody.Close() + + return jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut, nil) +} diff --git a/api/client/push.go b/api/client/push.go new file mode 100644 index 00000000..29f26c46 --- /dev/null +++ b/api/client/push.go @@ -0,0 +1,76 @@ +package client + +import ( + "errors" + "io" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + "github.com/docker/docker/pkg/jsonmessage" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" + "github.com/docker/engine-api/client" + "github.com/docker/engine-api/types" +) + +// CmdPush pushes an image or repository to the registry. +// +// Usage: docker push NAME[:TAG] +func (cli *DockerCli) CmdPush(args ...string) error { + cmd := Cli.Subcmd("push", []string{"NAME[:TAG]"}, Cli.DockerCommands["push"].Description, true) + addTrustedFlags(cmd, false) + cmd.Require(flag.Exact, 1) + + cmd.ParseFlags(args, true) + + ref, err := reference.ParseNamed(cmd.Arg(0)) + if err != nil { + return err + } + + var tag string + switch x := ref.(type) { + case reference.Canonical: + return errors.New("cannot push a digest reference") + case reference.NamedTagged: + tag = x.Tag() + } + + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := registry.ParseRepositoryInfo(ref) + if err != nil { + return err + } + // Resolve the Auth config relevant for this server + authConfig := cli.resolveAuthConfig(repoInfo.Index) + + requestPrivilege := cli.registryAuthenticationPrivilegedFunc(repoInfo.Index, "push") + if isTrusted() { + return cli.trustedPush(repoInfo, tag, authConfig, requestPrivilege) + } + + responseBody, err := cli.imagePushPrivileged(authConfig, ref.Name(), tag, requestPrivilege) + if err != nil { + return err + } + + defer responseBody.Close() + + return jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut, nil) +} + +func (cli *DockerCli) imagePushPrivileged(authConfig types.AuthConfig, imageID, tag string, requestPrivilege client.RequestPrivilegeFunc) (io.ReadCloser, error) { + encodedAuth, err := encodeAuthToBase64(authConfig) + if err != nil { + return nil, err + } + options := types.ImagePushOptions{ + ImageID: imageID, + Tag: tag, + RegistryAuth: encodedAuth, + } + + return cli.client.ImagePush(context.Background(), options, requestPrivilege) +} diff --git a/api/client/rename.go b/api/client/rename.go new file mode 100644 index 00000000..68369881 --- /dev/null +++ b/api/client/rename.go @@ -0,0 +1,34 @@ +package client + +import ( + "fmt" + "strings" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdRename renames a container. +// +// Usage: docker rename OLD_NAME NEW_NAME +func (cli *DockerCli) CmdRename(args ...string) error { + cmd := Cli.Subcmd("rename", []string{"OLD_NAME NEW_NAME"}, Cli.DockerCommands["rename"].Description, true) + cmd.Require(flag.Exact, 2) + + cmd.ParseFlags(args, true) + + oldName := strings.TrimSpace(cmd.Arg(0)) + newName := strings.TrimSpace(cmd.Arg(1)) + + if oldName == "" || newName == "" { + return fmt.Errorf("Error: Neither old nor new names may be empty") + } + + if err := cli.client.ContainerRename(context.Background(), oldName, newName); err != nil { + fmt.Fprintf(cli.err, "%s\n", err) + return fmt.Errorf("Error: failed to rename container named %s", oldName) + } + return nil +} diff --git a/api/client/restart.go b/api/client/restart.go new file mode 100644 index 00000000..c0a04bd1 --- /dev/null +++ b/api/client/restart.go @@ -0,0 +1,35 @@ +package client + +import ( + "fmt" + "strings" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdRestart restarts one or more containers. +// +// Usage: docker restart [OPTIONS] CONTAINER [CONTAINER...] +func (cli *DockerCli) CmdRestart(args ...string) error { + cmd := Cli.Subcmd("restart", []string{"CONTAINER [CONTAINER...]"}, Cli.DockerCommands["restart"].Description, true) + nSeconds := cmd.Int([]string{"t", "-time"}, 10, "Seconds to wait for stop before killing the container") + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + var errs []string + for _, name := range cmd.Args() { + if err := cli.client.ContainerRestart(context.Background(), name, *nSeconds); err != nil { + errs = append(errs, err.Error()) + } else { + fmt.Fprintf(cli.out, "%s\n", name) + } + } + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) + } + return nil +} diff --git a/api/client/rm.go b/api/client/rm.go new file mode 100644 index 00000000..c252b1f7 --- /dev/null +++ b/api/client/rm.go @@ -0,0 +1,56 @@ +package client + +import ( + "fmt" + "strings" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/engine-api/types" +) + +// CmdRm removes one or more containers. +// +// Usage: docker rm [OPTIONS] CONTAINER [CONTAINER...] +func (cli *DockerCli) CmdRm(args ...string) error { + cmd := Cli.Subcmd("rm", []string{"CONTAINER [CONTAINER...]"}, Cli.DockerCommands["rm"].Description, true) + v := cmd.Bool([]string{"v", "-volumes"}, false, "Remove the volumes associated with the container") + link := cmd.Bool([]string{"l", "-link"}, false, "Remove the specified link") + force := cmd.Bool([]string{"f", "-force"}, false, "Force the removal of a running container (uses SIGKILL)") + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + var errs []string + for _, name := range cmd.Args() { + if name == "" { + return fmt.Errorf("Container name cannot be empty") + } + name = strings.Trim(name, "/") + + if err := cli.removeContainer(name, *v, *link, *force); err != nil { + errs = append(errs, err.Error()) + } else { + fmt.Fprintf(cli.out, "%s\n", name) + } + } + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) + } + return nil +} + +func (cli *DockerCli) removeContainer(containerID string, removeVolumes, removeLinks, force bool) error { + options := types.ContainerRemoveOptions{ + ContainerID: containerID, + RemoveVolumes: removeVolumes, + RemoveLinks: removeLinks, + Force: force, + } + if err := cli.client.ContainerRemove(context.Background(), options); err != nil { + return err + } + return nil +} diff --git a/api/client/rmi.go b/api/client/rmi.go new file mode 100644 index 00000000..ac1b41db --- /dev/null +++ b/api/client/rmi.go @@ -0,0 +1,59 @@ +package client + +import ( + "fmt" + "net/url" + "strings" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/engine-api/types" +) + +// CmdRmi removes all images with the specified name(s). +// +// Usage: docker rmi [OPTIONS] IMAGE [IMAGE...] +func (cli *DockerCli) CmdRmi(args ...string) error { + cmd := Cli.Subcmd("rmi", []string{"IMAGE [IMAGE...]"}, Cli.DockerCommands["rmi"].Description, true) + force := cmd.Bool([]string{"f", "-force"}, false, "Force removal of the image") + noprune := cmd.Bool([]string{"-no-prune"}, false, "Do not delete untagged parents") + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + v := url.Values{} + if *force { + v.Set("force", "1") + } + if *noprune { + v.Set("noprune", "1") + } + + var errs []string + for _, name := range cmd.Args() { + options := types.ImageRemoveOptions{ + ImageID: name, + Force: *force, + PruneChildren: !*noprune, + } + + dels, err := cli.client.ImageRemove(context.Background(), options) + if err != nil { + errs = append(errs, err.Error()) + } else { + for _, del := range dels { + if del.Deleted != "" { + fmt.Fprintf(cli.out, "Deleted: %s\n", del.Deleted) + } else { + fmt.Fprintf(cli.out, "Untagged: %s\n", del.Untagged) + } + } + } + } + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) + } + return nil +} diff --git a/api/client/run.go b/api/client/run.go new file mode 100644 index 00000000..2e832310 --- /dev/null +++ b/api/client/run.go @@ -0,0 +1,287 @@ +package client + +import ( + "fmt" + "io" + "os" + "runtime" + "strings" + + "golang.org/x/net/context" + + "github.com/Sirupsen/logrus" + Cli "github.com/docker/docker/cli" + "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/promise" + "github.com/docker/docker/pkg/signal" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/docker/engine-api/types" + "github.com/docker/libnetwork/resolvconf/dns" +) + +const ( + errCmdNotFound = "not found or does not exist." + errCmdCouldNotBeInvoked = "could not be invoked." +) + +func (cid *cidFile) Close() error { + cid.file.Close() + + if !cid.written { + if err := os.Remove(cid.path); err != nil { + return fmt.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 fmt.Errorf("Failed to write the container ID to the file: %s", err) + } + cid.written = true + return nil +} + +// if container start fails with 'command not found' error, return 127 +// if container start fails with 'command cannot be invoked' error, return 126 +// return 125 for generic docker daemon failures +func runStartContainerErr(err error) error { + trimmedErr := strings.Trim(err.Error(), "Error response from daemon: ") + statusError := Cli.StatusError{StatusCode: 125} + + if strings.HasPrefix(trimmedErr, "Container command") { + if strings.Contains(trimmedErr, errCmdNotFound) { + statusError = Cli.StatusError{StatusCode: 127} + } else if strings.Contains(trimmedErr, errCmdCouldNotBeInvoked) { + statusError = Cli.StatusError{StatusCode: 126} + } + } + + return statusError +} + +// CmdRun runs a command in a new container. +// +// Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...] +func (cli *DockerCli) CmdRun(args ...string) error { + cmd := Cli.Subcmd("run", []string{"IMAGE [COMMAND] [ARG...]"}, Cli.DockerCommands["run"].Description, true) + addTrustedFlags(cmd, true) + + // These are flags not stored in Config/HostConfig + var ( + flAutoRemove = cmd.Bool([]string{"-rm"}, false, "Automatically remove the container when it exits") + flDetach = cmd.Bool([]string{"d", "-detach"}, false, "Run container in background and print container ID") + flSigProxy = cmd.Bool([]string{"-sig-proxy"}, true, "Proxy received signals to the process") + flName = cmd.String([]string{"-name"}, "", "Assign a name to the container") + flDetachKeys = cmd.String([]string{"-detach-keys"}, "", "Override the key sequence for detaching a container") + flAttach *opts.ListOpts + + ErrConflictAttachDetach = fmt.Errorf("Conflicting options: -a and -d") + ErrConflictRestartPolicyAndAutoRemove = fmt.Errorf("Conflicting options: --restart and --rm") + ErrConflictDetachAutoRemove = fmt.Errorf("Conflicting options: --rm and -d") + ) + + config, hostConfig, networkingConfig, cmd, err := runconfigopts.Parse(cmd, args) + + // just in case the Parse does not exit + if err != nil { + cmd.ReportError(err.Error(), true) + os.Exit(125) + } + + if hostConfig.OomKillDisable != nil && *hostConfig.OomKillDisable && hostConfig.Memory == 0 { + fmt.Fprintf(cli.err, "WARNING: Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.\n") + } + + if len(hostConfig.DNS) > 0 { + // check the DNS settings passed via --dns against + // localhost regexp to warn if they are trying to + // set a DNS to a localhost address + for _, dnsIP := range hostConfig.DNS { + if dns.IsLocalhost(dnsIP) { + fmt.Fprintf(cli.err, "WARNING: Localhost DNS setting (--dns=%s) may fail in containers.\n", dnsIP) + break + } + } + } + if config.Image == "" { + cmd.Usage() + return nil + } + + config.ArgsEscaped = false + + if !*flDetach { + if err := cli.CheckTtyInput(config.AttachStdin, config.Tty); err != nil { + return err + } + } else { + if fl := cmd.Lookup("-attach"); fl != nil { + flAttach = fl.Value.(*opts.ListOpts) + if flAttach.Len() != 0 { + return ErrConflictAttachDetach + } + } + if *flAutoRemove { + return ErrConflictDetachAutoRemove + } + + config.AttachStdin = false + config.AttachStdout = false + config.AttachStderr = false + config.StdinOnce = false + } + + // Disable flSigProxy when in TTY mode + sigProxy := *flSigProxy + if config.Tty { + 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] = cli.getTtySize() + } + + createResponse, err := cli.createContainer(config, hostConfig, networkingConfig, hostConfig.ContainerIDFile, *flName) + if err != nil { + cmd.ReportError(err.Error(), true) + return runStartContainerErr(err) + } + if sigProxy { + sigc := cli.forwardAllSignals(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.Fprintf(cli.out, "%s\n", createResponse.ID) + }() + } + if *flAutoRemove && (hostConfig.RestartPolicy.IsAlways() || hostConfig.RestartPolicy.IsOnFailure()) { + return ErrConflictRestartPolicyAndAutoRemove + } + + if config.AttachStdin || config.AttachStdout || config.AttachStderr { + var ( + out, stderr io.Writer + in io.ReadCloser + ) + if config.AttachStdin { + in = cli.in + } + if config.AttachStdout { + out = cli.out + } + if config.AttachStderr { + if config.Tty { + stderr = cli.out + } else { + stderr = cli.err + } + } + + if *flDetachKeys != "" { + cli.configFile.DetachKeys = *flDetachKeys + } + + options := types.ContainerAttachOptions{ + ContainerID: createResponse.ID, + Stream: true, + Stdin: config.AttachStdin, + Stdout: config.AttachStdout, + Stderr: config.AttachStderr, + DetachKeys: cli.configFile.DetachKeys, + } + + resp, err := cli.client.ContainerAttach(context.Background(), options) + if err != nil { + return err + } + if in != nil && config.Tty { + if err := cli.setRawTerminal(); err != nil { + return err + } + defer cli.restoreTerminal(in) + } + errCh = promise.Go(func() error { + return cli.holdHijackedConnection(config.Tty, in, out, stderr, resp) + }) + } + + if *flAutoRemove { + defer func() { + if err := cli.removeContainer(createResponse.ID, true, false, false); err != nil { + fmt.Fprintf(cli.err, "%v\n", err) + } + }() + } + + //start the container + if err := cli.client.ContainerStart(context.Background(), createResponse.ID); err != nil { + cmd.ReportError(err.Error(), false) + return runStartContainerErr(err) + } + + if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && cli.isTerminalOut { + if err := cli.monitorTtySize(createResponse.ID, false); err != nil { + fmt.Fprintf(cli.err, "Error monitoring TTY size: %s\n", 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 + } + + var status int + + // Attached mode + if *flAutoRemove { + // Autoremove: wait for the container to finish, retrieve + // the exit code and remove the container + if status, err = cli.client.ContainerWait(context.Background(), createResponse.ID); err != nil { + return runStartContainerErr(err) + } + if _, status, err = getExitCode(cli, createResponse.ID); err != nil { + return err + } + } else { + // No Autoremove: Simply retrieve the exit code + if !config.Tty { + // In non-TTY mode, we can't detach, so we must wait for container exit + if status, err = cli.client.ContainerWait(context.Background(), createResponse.ID); err != nil { + return err + } + } else { + // In TTY mode, there is a race: if the process dies too slowly, the state could + // be updated after the getExitCode call and result in the wrong exit code being reported + if _, status, err = getExitCode(cli, createResponse.ID); err != nil { + return err + } + } + } + if status != 0 { + return Cli.StatusError{StatusCode: status} + } + return nil +} diff --git a/api/client/save.go b/api/client/save.go new file mode 100644 index 00000000..4aabf1bd --- /dev/null +++ b/api/client/save.go @@ -0,0 +1,42 @@ +package client + +import ( + "errors" + "io" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdSave saves one or more images to a tar archive. +// +// The tar archive is written to STDOUT by default, or written to a file. +// +// Usage: docker save [OPTIONS] IMAGE [IMAGE...] +func (cli *DockerCli) CmdSave(args ...string) error { + cmd := Cli.Subcmd("save", []string{"IMAGE [IMAGE...]"}, Cli.DockerCommands["save"].Description+" (streamed to STDOUT by default)", true) + outfile := cmd.String([]string{"o", "-output"}, "", "Write to a file, instead of STDOUT") + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + if *outfile == "" && cli.isTerminalOut { + return errors.New("Cowardly refusing to save to a terminal. Use the -o flag or redirect.") + } + + responseBody, err := cli.client.ImageSave(context.Background(), cmd.Args()) + if err != nil { + return err + } + defer responseBody.Close() + + if *outfile == "" { + _, err := io.Copy(cli.out, responseBody) + return err + } + + return copyToFile(*outfile, responseBody) + +} diff --git a/api/client/search.go b/api/client/search.go new file mode 100644 index 00000000..82deb409 --- /dev/null +++ b/api/client/search.go @@ -0,0 +1,93 @@ +package client + +import ( + "fmt" + "net/url" + "sort" + "strings" + "text/tabwriter" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/stringutils" + "github.com/docker/docker/registry" + "github.com/docker/engine-api/types" + registrytypes "github.com/docker/engine-api/types/registry" +) + +// CmdSearch searches the Docker Hub for images. +// +// Usage: docker search [OPTIONS] TERM +func (cli *DockerCli) CmdSearch(args ...string) error { + cmd := Cli.Subcmd("search", []string{"TERM"}, Cli.DockerCommands["search"].Description, true) + noTrunc := cmd.Bool([]string{"-no-trunc"}, false, "Don't truncate output") + automated := cmd.Bool([]string{"-automated"}, false, "Only show automated builds") + stars := cmd.Uint([]string{"s", "-stars"}, 0, "Only displays with at least x stars") + cmd.Require(flag.Exact, 1) + + cmd.ParseFlags(args, true) + + name := cmd.Arg(0) + v := url.Values{} + v.Set("term", name) + + indexInfo, err := registry.ParseSearchIndexInfo(name) + if err != nil { + return err + } + + authConfig := cli.resolveAuthConfig(indexInfo) + requestPrivilege := cli.registryAuthenticationPrivilegedFunc(indexInfo, "search") + + encodedAuth, err := encodeAuthToBase64(authConfig) + if err != nil { + return err + } + + options := types.ImageSearchOptions{ + Term: name, + RegistryAuth: encodedAuth, + } + + unorderedResults, err := cli.client.ImageSearch(context.Background(), options, requestPrivilege) + if err != nil { + return err + } + + results := searchResultsByStars(unorderedResults) + sort.Sort(results) + + w := tabwriter.NewWriter(cli.out, 10, 1, 3, ' ', 0) + fmt.Fprintf(w, "NAME\tDESCRIPTION\tSTARS\tOFFICIAL\tAUTOMATED\n") + for _, res := range results { + if (*automated && !res.IsAutomated) || (int(*stars) > res.StarCount) { + continue + } + desc := strings.Replace(res.Description, "\n", " ", -1) + desc = strings.Replace(desc, "\r", " ", -1) + if !*noTrunc && len(desc) > 45 { + desc = stringutils.Truncate(desc, 42) + "..." + } + 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 || res.IsTrusted { + 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/api/client/start.go b/api/client/start.go new file mode 100644 index 00000000..1ff2845f --- /dev/null +++ b/api/client/start.go @@ -0,0 +1,157 @@ +package client + +import ( + "fmt" + "io" + "os" + "strings" + + "golang.org/x/net/context" + + "github.com/Sirupsen/logrus" + Cli "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/promise" + "github.com/docker/docker/pkg/signal" + "github.com/docker/engine-api/types" +) + +func (cli *DockerCli) forwardAllSignals(cid string) chan os.Signal { + sigc := make(chan os.Signal, 128) + signal.CatchAll(sigc) + go func() { + for s := range sigc { + if s == signal.SIGCHLD || s == signal.SIGPIPE { + continue + } + var sig string + for sigStr, sigN := range signal.SignalMap { + if sigN == s { + sig = sigStr + break + } + } + if sig == "" { + fmt.Fprintf(cli.err, "Unsupported signal: %v. Discarding.\n", s) + continue + } + + if err := cli.client.ContainerKill(context.Background(), cid, sig); err != nil { + logrus.Debugf("Error sending signal: %s", err) + } + } + }() + return sigc +} + +// CmdStart starts one or more containers. +// +// Usage: docker start [OPTIONS] CONTAINER [CONTAINER...] +func (cli *DockerCli) CmdStart(args ...string) error { + cmd := Cli.Subcmd("start", []string{"CONTAINER [CONTAINER...]"}, Cli.DockerCommands["start"].Description, true) + attach := cmd.Bool([]string{"a", "-attach"}, false, "Attach STDOUT/STDERR and forward signals") + openStdin := cmd.Bool([]string{"i", "-interactive"}, false, "Attach container's STDIN") + detachKeys := cmd.String([]string{"-detach-keys"}, "", "Override the key sequence for detaching a container") + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + if *attach || *openStdin { + // We're going to attach to a container. + // 1. Ensure we only have one container. + if cmd.NArg() > 1 { + return fmt.Errorf("You cannot start and attach multiple containers at once.") + } + + // 2. Attach to the container. + containerID := cmd.Arg(0) + c, err := cli.client.ContainerInspect(context.Background(), containerID) + if err != nil { + return err + } + + if !c.Config.Tty { + sigc := cli.forwardAllSignals(containerID) + defer signal.StopCatch(sigc) + } + + if *detachKeys != "" { + cli.configFile.DetachKeys = *detachKeys + } + + options := types.ContainerAttachOptions{ + ContainerID: containerID, + Stream: true, + Stdin: *openStdin && c.Config.OpenStdin, + Stdout: true, + Stderr: true, + DetachKeys: cli.configFile.DetachKeys, + } + + var in io.ReadCloser + if options.Stdin { + in = cli.in + } + + resp, err := cli.client.ContainerAttach(context.Background(), options) + if err != nil { + return err + } + defer resp.Close() + if in != nil && c.Config.Tty { + if err := cli.setRawTerminal(); err != nil { + return err + } + defer cli.restoreTerminal(in) + } + + cErr := promise.Go(func() error { + return cli.holdHijackedConnection(c.Config.Tty, in, cli.out, cli.err, resp) + }) + + // 3. Start the container. + if err := cli.client.ContainerStart(context.Background(), containerID); err != nil { + return err + } + + // 4. Wait for attachment to break. + if c.Config.Tty && cli.isTerminalOut { + if err := cli.monitorTtySize(containerID, false); err != nil { + fmt.Fprintf(cli.err, "Error monitoring TTY size: %s\n", err) + } + } + if attchErr := <-cErr; attchErr != nil { + return attchErr + } + _, status, err := getExitCode(cli, containerID) + if err != nil { + return err + } + if status != 0 { + return Cli.StatusError{StatusCode: status} + } + } else { + // We're not going to attach to anything. + // Start as many containers as we want. + return cli.startContainersWithoutAttachments(cmd.Args()) + } + + return nil +} + +func (cli *DockerCli) startContainersWithoutAttachments(containerIDs []string) error { + var failedContainers []string + for _, containerID := range containerIDs { + if err := cli.client.ContainerStart(context.Background(), containerID); err != nil { + fmt.Fprintf(cli.err, "%s\n", err) + failedContainers = append(failedContainers, containerID) + } else { + fmt.Fprintf(cli.out, "%s\n", containerID) + } + } + + if len(failedContainers) > 0 { + return fmt.Errorf("Error: failed to start containers: %v", strings.Join(failedContainers, ", ")) + } + return nil +} diff --git a/api/client/stats.go b/api/client/stats.go new file mode 100644 index 00000000..b84ac3e0 --- /dev/null +++ b/api/client/stats.go @@ -0,0 +1,208 @@ +package client + +import ( + "fmt" + "io" + "strings" + "sync" + "text/tabwriter" + "time" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/events" + "github.com/docker/engine-api/types/filters" +) + +// CmdStats 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. +// +// Usage: docker stats [OPTIONS] [CONTAINER...] +func (cli *DockerCli) CmdStats(args ...string) error { + cmd := Cli.Subcmd("stats", []string{"[CONTAINER...]"}, Cli.DockerCommands["stats"].Description, true) + all := cmd.Bool([]string{"a", "-all"}, false, "Show all containers (default shows just running)") + noStream := cmd.Bool([]string{"-no-stream"}, false, "Disable streaming stats and only pull the first result") + + cmd.ParseFlags(args, true) + + names := cmd.Args() + showAll := len(names) == 0 + closeChan := make(chan error) + + // 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, + } + resBody, err := cli.client.Events(context.Background(), options) + // Whether we successfully subscribed to events or not, we can now + // unblock the main goroutine. + close(started) + if err != nil { + closeChan <- err + return + } + defer resBody.Close() + + decodeEvents(resBody, func(event events.Message, err error) error { + if err != nil { + closeChan <- err + return nil + } + c <- event + return nil + }) + } + + // 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: *all, + } + cs, err := cli.client.ContainerList(context.Background(), options) + if err != nil { + closeChan <- err + } + for _, container := range cs { + s := &containerStats{Name: container.ID[:12]} + if cStats.add(s) { + waitFirst.Add(1) + go s.Collect(cli.client, !*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 := eventHandler{handlers: make(map[string]func(events.Message))} + eh.Handle("create", func(e events.Message) { + if *all { + s := &containerStats{Name: e.ID[:12]} + if cStats.add(s) { + waitFirst.Add(1) + go s.Collect(cli.client, !*noStream, waitFirst) + } + } + }) + + eh.Handle("start", func(e events.Message) { + s := &containerStats{Name: e.ID[:12]} + if cStats.add(s) { + waitFirst.Add(1) + go s.Collect(cli.client, !*noStream, waitFirst) + } + }) + + eh.Handle("die", func(e events.Message) { + if !*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 names { + s := &containerStats{Name: name} + if cStats.add(s) { + waitFirst.Add(1) + go s.Collect(cli.client, !*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 { + c.mu.Lock() + if c.err != nil { + errs = append(errs, fmt.Sprintf("%s: %v", c.Name, c.err)) + } + c.mu.Unlock() + } + cStats.mu.Unlock() + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, ", ")) + } + } + + // before print to screen, make sure each container get at least one valid stat data + waitFirst.Wait() + + w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) + printHeader := func() { + if !*noStream { + fmt.Fprint(cli.out, "\033[2J") + fmt.Fprint(cli.out, "\033[H") + } + io.WriteString(w, "CONTAINER\tCPU %\tMEM USAGE / LIMIT\tMEM %\tNET I/O\tBLOCK I/O\tPIDS\n") + } + + for range time.Tick(500 * time.Millisecond) { + printHeader() + toRemove := []int{} + cStats.mu.Lock() + for i, s := range cStats.cs { + if err := s.Display(w); err != nil && !*noStream { + toRemove = append(toRemove, i) + } + } + for j := len(toRemove) - 1; j >= 0; j-- { + i := toRemove[j] + cStats.cs = append(cStats.cs[:i], cStats.cs[i+1:]...) + } + if len(cStats.cs) == 0 && !showAll { + return nil + } + cStats.mu.Unlock() + w.Flush() + if *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 nil +} diff --git a/api/client/stats_helpers.go b/api/client/stats_helpers.go new file mode 100644 index 00000000..404c3ff1 --- /dev/null +++ b/api/client/stats_helpers.go @@ -0,0 +1,219 @@ +package client + +import ( + "encoding/json" + "fmt" + "io" + "strings" + "sync" + "time" + + "github.com/docker/engine-api/client" + "github.com/docker/engine-api/types" + "github.com/docker/go-units" + "golang.org/x/net/context" +) + +type containerStats struct { + Name string + CPUPercentage float64 + Memory float64 + MemoryLimit float64 + MemoryPercentage float64 + NetworkRx float64 + NetworkTx float64 + BlockRead float64 + BlockWrite float64 + PidsCurrent uint64 + mu sync.RWMutex + err error +} + +type stats struct { + mu sync.Mutex + cs []*containerStats +} + +func (s *stats) add(cs *containerStats) bool { + s.mu.Lock() + defer s.mu.Unlock() + if _, exists := s.isKnownContainer(cs.Name); !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.Name == cid { + return i, true + } + } + return -1, false +} + +func (s *containerStats) Collect(cli client.APIClient, streamStats bool, waitFirst *sync.WaitGroup) { + 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() + } + }() + + responseBody, err := cli.ContainerStats(context.Background(), s.Name, streamStats) + if err != nil { + s.mu.Lock() + s.err = err + s.mu.Unlock() + return + } + defer responseBody.Close() + + dec := json.NewDecoder(responseBody) + go func() { + for { + var v *types.StatsJSON + if err := dec.Decode(&v); err != nil { + u <- err + return + } + + var memPercent = 0.0 + var cpuPercent = 0.0 + + // 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 = calculateCPUPercent(previousCPU, previousSystem, v) + blkRead, blkWrite := calculateBlockIO(v.BlkioStats) + s.mu.Lock() + s.CPUPercentage = cpuPercent + s.Memory = float64(v.MemoryStats.Usage) + s.MemoryLimit = float64(v.MemoryStats.Limit) + s.MemoryPercentage = memPercent + s.NetworkRx, s.NetworkTx = calculateNetwork(v.Networks) + s.BlockRead = float64(blkRead) + s.BlockWrite = float64(blkWrite) + s.PidsCurrent = v.PidsStats.Current + s.mu.Unlock() + 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.mu.Lock() + s.CPUPercentage = 0 + s.Memory = 0 + s.MemoryPercentage = 0 + s.MemoryLimit = 0 + s.NetworkRx = 0 + s.NetworkTx = 0 + s.BlockRead = 0 + s.BlockWrite = 0 + s.PidsCurrent = 0 + s.mu.Unlock() + // if this is the first stat you get, release WaitGroup + if !getFirst { + getFirst = true + waitFirst.Done() + } + case err := <-u: + if err != nil { + s.mu.Lock() + s.err = err + s.mu.Unlock() + return + } + // if this is the first stat you get, release WaitGroup + if !getFirst { + getFirst = true + waitFirst.Done() + } + } + if !streamStats { + return + } + } +} + +func (s *containerStats) Display(w io.Writer) error { + s.mu.RLock() + defer s.mu.RUnlock() + if s.err != nil { + return s.err + } + fmt.Fprintf(w, "%s\t%.2f%%\t%s / %s\t%.2f%%\t%s / %s\t%s / %s\t%d\n", + s.Name, + s.CPUPercentage, + units.HumanSize(s.Memory), units.HumanSize(s.MemoryLimit), + s.MemoryPercentage, + units.HumanSize(s.NetworkRx), units.HumanSize(s.NetworkTx), + units.HumanSize(s.BlockRead), units.HumanSize(s.BlockWrite), + s.PidsCurrent) + return nil +} + +func calculateCPUPercent(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) + ) + + if systemDelta > 0.0 && cpuDelta > 0.0 { + cpuPercent = (cpuDelta / systemDelta) * float64(len(v.CPUStats.CPUUsage.PercpuUsage)) * 100.0 + } + return cpuPercent +} + +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/api/client/stats_unit_test.go b/api/client/stats_unit_test.go new file mode 100644 index 00000000..36081c57 --- /dev/null +++ b/api/client/stats_unit_test.go @@ -0,0 +1,47 @@ +package client + +import ( + "bytes" + "sync" + "testing" + + "github.com/docker/engine-api/types" +) + +func TestDisplay(t *testing.T) { + c := &containerStats{ + Name: "app", + CPUPercentage: 30.0, + Memory: 100 * 1024 * 1024.0, + MemoryLimit: 2048 * 1024 * 1024.0, + MemoryPercentage: 100.0 / 2048.0 * 100.0, + NetworkRx: 100 * 1024 * 1024, + NetworkTx: 800 * 1024 * 1024, + BlockRead: 100 * 1024 * 1024, + BlockWrite: 800 * 1024 * 1024, + PidsCurrent: 1, + mu: sync.RWMutex{}, + } + var b bytes.Buffer + if err := c.Display(&b); err != nil { + t.Fatalf("c.Display() gave error: %s", err) + } + got := b.String() + want := "app\t30.00%\t104.9 MB / 2.147 GB\t4.88%\t104.9 MB / 838.9 MB\t104.9 MB / 838.9 MB\t1\n" + if got != want { + t.Fatalf("c.Display() = %q, want %q", got, want) + } +} + +func TestCalculBlockIO(t *testing.T) { + blkio := types.BlkioStats{ + IoServiceBytesRecursive: []types.BlkioStatEntry{{8, 0, "read", 1234}, {8, 1, "read", 4567}, {8, 0, "write", 123}, {8, 1, "write", 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/api/client/stop.go b/api/client/stop.go new file mode 100644 index 00000000..23d53447 --- /dev/null +++ b/api/client/stop.go @@ -0,0 +1,37 @@ +package client + +import ( + "fmt" + "strings" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdStop stops one or more containers. +// +// A running container is stopped by first sending SIGTERM and then SIGKILL if the container fails to stop within a grace period (the default is 10 seconds). +// +// Usage: docker stop [OPTIONS] CONTAINER [CONTAINER...] +func (cli *DockerCli) CmdStop(args ...string) error { + cmd := Cli.Subcmd("stop", []string{"CONTAINER [CONTAINER...]"}, Cli.DockerCommands["stop"].Description+".\nSending SIGTERM and then SIGKILL after a grace period", true) + nSeconds := cmd.Int([]string{"t", "-time"}, 10, "Seconds to wait for stop before killing it") + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + var errs []string + for _, name := range cmd.Args() { + if err := cli.client.ContainerStop(context.Background(), name, *nSeconds); err != nil { + errs = append(errs, err.Error()) + } else { + fmt.Fprintf(cli.out, "%s\n", name) + } + } + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) + } + return nil +} diff --git a/api/client/tag.go b/api/client/tag.go new file mode 100644 index 00000000..1d87e437 --- /dev/null +++ b/api/client/tag.go @@ -0,0 +1,46 @@ +package client + +import ( + "errors" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/reference" + "github.com/docker/engine-api/types" +) + +// CmdTag tags an image into a repository. +// +// Usage: docker tag [OPTIONS] IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG] +func (cli *DockerCli) CmdTag(args ...string) error { + cmd := Cli.Subcmd("tag", []string{"IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG]"}, Cli.DockerCommands["tag"].Description, true) + force := cmd.Bool([]string{"#f", "#-force"}, false, "Force the tagging even if there's a conflict") + cmd.Require(flag.Exact, 2) + + cmd.ParseFlags(args, true) + + ref, err := reference.ParseNamed(cmd.Arg(1)) + if err != nil { + return err + } + + if _, isCanonical := ref.(reference.Canonical); isCanonical { + return errors.New("refusing to create a tag with a digest reference") + } + + var tag string + if tagged, isTagged := ref.(reference.NamedTagged); isTagged { + tag = tagged.Tag() + } + + options := types.ImageTagOptions{ + ImageID: cmd.Arg(0), + RepositoryName: ref.Name(), + Tag: tag, + Force: *force, + } + + return cli.client.ImageTag(context.Background(), options) +} diff --git a/api/client/top.go b/api/client/top.go new file mode 100644 index 00000000..bb2ec46c --- /dev/null +++ b/api/client/top.go @@ -0,0 +1,41 @@ +package client + +import ( + "fmt" + "strings" + "text/tabwriter" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdTop displays the running processes of a container. +// +// Usage: docker top CONTAINER +func (cli *DockerCli) CmdTop(args ...string) error { + cmd := Cli.Subcmd("top", []string{"CONTAINER [ps OPTIONS]"}, Cli.DockerCommands["top"].Description, true) + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + var arguments []string + if cmd.NArg() > 1 { + arguments = cmd.Args()[1:] + } + + procList, err := cli.client.ContainerTop(context.Background(), cmd.Arg(0), arguments) + if err != nil { + return err + } + + w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) + fmt.Fprintln(w, strings.Join(procList.Titles, "\t")) + + for _, proc := range procList.Processes { + fmt.Fprintln(w, strings.Join(proc, "\t")) + } + w.Flush() + return nil +} diff --git a/api/client/trust.go b/api/client/trust.go new file mode 100644 index 00000000..bf22d3a2 --- /dev/null +++ b/api/client/trust.go @@ -0,0 +1,559 @@ +package client + +import ( + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "sort" + "strconv" + "time" + + "golang.org/x/net/context" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/digest" + "github.com/docker/distribution/registry/client/auth" + "github.com/docker/distribution/registry/client/transport" + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/distribution" + "github.com/docker/docker/pkg/jsonmessage" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" + apiclient "github.com/docker/engine-api/client" + "github.com/docker/engine-api/types" + registrytypes "github.com/docker/engine-api/types/registry" + "github.com/docker/go-connections/tlsconfig" + "github.com/docker/notary/client" + "github.com/docker/notary/passphrase" + "github.com/docker/notary/trustmanager" + "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/signed" + "github.com/docker/notary/tuf/store" +) + +var ( + releasesRole = path.Join(data.CanonicalTargetsRole, "releases") + untrusted bool +) + +func addTrustedFlags(fs *flag.FlagSet, verify 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 + } + } + message := "Skip image signing" + if verify { + message = "Skip image verification" + } + fs.BoolVar(&untrusted, []string{"-disable-content-trust"}, !trusted, message) +} + +func isTrusted() bool { + return !untrusted +} + +type target struct { + reference registry.Reference + digest digest.Digest + size int64 +} + +func (cli *DockerCli) trustDirectory() string { + return filepath.Join(cliconfig.ConfigDir(), "trust") +} + +// certificateDirectory returns the directory containing +// TLS certificates for the given server. An error is +// returned if there was an error parsing the server string. +func (cli *DockerCli) certificateDirectory(server string) (string, error) { + u, err := url.Parse(server) + if err != nil { + return "", err + } + + return filepath.Join(cliconfig.ConfigDir(), "tls", u.Host), nil +} + +func trustServer(index *registrytypes.IndexInfo) (string, error) { + if s := os.Getenv("DOCKER_CONTENT_TRUST_SERVER"); s != "" { + urlObj, err := url.Parse(s) + if err != nil || urlObj.Scheme != "https" { + return "", fmt.Errorf("valid https URL required for trust server, got %s", s) + } + + return s, nil + } + if index.Official { + return registry.NotaryServer, nil + } + return "https://" + index.Name, nil +} + +type simpleCredentialStore struct { + auth types.AuthConfig +} + +func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) { + return scs.auth.Username, scs.auth.Password +} + +func (scs simpleCredentialStore) RefreshToken(u *url.URL, service string) string { + return scs.auth.IdentityToken +} + +func (scs simpleCredentialStore) SetRefreshToken(*url.URL, string, string) { +} + +// getNotaryRepository returns a NotaryRepository which stores all the +// information needed to operate on a notary repository. +// It creates a HTTP transport providing authentication support. +func (cli *DockerCli) getNotaryRepository(repoInfo *registry.RepositoryInfo, authConfig types.AuthConfig, actions ...string) (*client.NotaryRepository, error) { + server, err := trustServer(repoInfo.Index) + if err != nil { + return nil, err + } + + var cfg = tlsconfig.ClientDefault + cfg.InsecureSkipVerify = !repoInfo.Index.Secure + + // Get certificate base directory + certDir, err := cli.certificateDirectory(server) + if err != nil { + return nil, err + } + logrus.Debugf("reading certificate directory: %s", certDir) + + if err := registry.ReadCertsDirectory(&cfg, certDir); err != nil { + return nil, err + } + + base := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + TLSClientConfig: &cfg, + DisableKeepAlives: true, + } + + // Skip configuration headers since request is not going to Docker daemon + modifiers := registry.DockerHeaders(clientUserAgent(), http.Header{}) + authTransport := transport.NewTransport(base, modifiers...) + pingClient := &http.Client{ + Transport: authTransport, + Timeout: 5 * time.Second, + } + endpointStr := server + "/v2/" + req, err := http.NewRequest("GET", endpointStr, nil) + if err != nil { + return nil, err + } + + challengeManager := auth.NewSimpleChallengeManager() + + resp, err := pingClient.Do(req) + if err != nil { + // Ignore error on ping to operate in offline mode + logrus.Debugf("Error pinging notary server %q: %s", endpointStr, err) + } else { + defer resp.Body.Close() + + // Add response to the challenge manager to parse out + // authentication header and register authentication method + if err := challengeManager.AddResponse(resp); err != nil { + return nil, err + } + } + + creds := simpleCredentialStore{auth: authConfig} + tokenHandler := auth.NewTokenHandler(authTransport, creds, repoInfo.FullName(), actions...) + basicHandler := auth.NewBasicHandler(creds) + modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))) + tr := transport.NewTransport(base, modifiers...) + + return client.NewNotaryRepository(cli.trustDirectory(), repoInfo.FullName(), server, tr, cli.getPassphraseRetriever()) +} + +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{ + reference: registry.ParseReference(t.Name), + digest: digest.NewDigestFromHex("sha256", hex.EncodeToString(h)), + size: t.Length, + }, nil +} + +func (cli *DockerCli) getPassphraseRetriever() passphrase.Retriever { + aliasMap := map[string]string{ + "root": "root", + "snapshot": "repository", + "targets": "repository", + "default": "repository", + } + baseRetriever := passphrase.PromptRetrieverWithInOut(cli.in, cli.out, aliasMap) + env := map[string]string{ + "root": os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"), + "snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), + "targets": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), + "default": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), + } + + // Backwards compatibility with old env names. We should remove this in 1.10 + if env["root"] == "" { + if passphrase := os.Getenv("DOCKER_CONTENT_TRUST_OFFLINE_PASSPHRASE"); passphrase != "" { + env["root"] = passphrase + fmt.Fprintf(cli.err, "[DEPRECATED] The environment variable DOCKER_CONTENT_TRUST_OFFLINE_PASSPHRASE has been deprecated and will be removed in v1.10. Please use DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE\n") + } + } + if env["snapshot"] == "" || env["targets"] == "" || env["default"] == "" { + if passphrase := os.Getenv("DOCKER_CONTENT_TRUST_TAGGING_PASSPHRASE"); passphrase != "" { + env["snapshot"] = passphrase + env["targets"] = passphrase + env["default"] = passphrase + fmt.Fprintf(cli.err, "[DEPRECATED] The environment variable DOCKER_CONTENT_TRUST_TAGGING_PASSPHRASE has been deprecated and will be removed in v1.10. Please use DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE\n") + } + } + + return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) { + if v := env[alias]; v != "" { + return v, numAttempts > 1, nil + } + // For non-root roles, we can also try the "default" alias if it is specified + if v := env["default"]; v != "" && alias != data.CanonicalRootRole { + return v, numAttempts > 1, nil + } + return baseRetriever(keyName, alias, createNew, numAttempts) + } +} + +func (cli *DockerCli) trustedReference(ref reference.NamedTagged) (reference.Canonical, error) { + repoInfo, err := registry.ParseRepositoryInfo(ref) + if err != nil { + return nil, err + } + + // Resolve the Auth config relevant for this server + authConfig := cli.resolveAuthConfig(repoInfo.Index) + + notaryRepo, err := cli.getNotaryRepository(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(), releasesRole, data.CanonicalTargetsRole) + if err != nil { + return nil, err + } + // Only list tags in the top level targets role or the releases delegation role - ignore + // all other delegation roles + if t.Role != releasesRole && t.Role != data.CanonicalTargetsRole { + return nil, notaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.Tag())) + } + r, err := convertTarget(t.Target) + if err != nil { + return nil, err + + } + + return reference.WithDigest(ref, r.digest) +} + +func (cli *DockerCli) tagTrusted(trustedRef reference.Canonical, ref reference.NamedTagged) error { + fmt.Fprintf(cli.out, "Tagging %s as %s\n", trustedRef.String(), ref.String()) + + options := types.ImageTagOptions{ + ImageID: trustedRef.String(), + RepositoryName: trustedRef.Name(), + Tag: ref.Tag(), + Force: true, + } + + return cli.client.ImageTag(context.Background(), options) +} + +func notaryError(repoName string, err error) error { + switch err.(type) { + case *json.SyntaxError: + logrus.Debugf("Notary syntax error: %s", err) + return fmt.Errorf("Error: no trust data available for remote repository %s. Try running notary server and setting DOCKER_CONTENT_TRUST_SERVER to its HTTPS address?", repoName) + case signed.ErrExpired: + return fmt.Errorf("Error: remote repository %s out-of-date: %v", repoName, err) + case trustmanager.ErrKeyNotFound: + return fmt.Errorf("Error: signing keys for remote repository %s not found: %v", repoName, err) + case *net.OpError: + return fmt.Errorf("Error: error contacting notary server: %v", err) + case store.ErrMetaNotFound: + return fmt.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err) + case signed.ErrInvalidKeyType: + return fmt.Errorf("Warning: potential malicious behavior - trust data mismatch for remote repository %s: %v", repoName, err) + case signed.ErrNoKeys: + return fmt.Errorf("Error: could not find signing keys for remote repository %s, or could not decrypt signing key: %v", repoName, err) + case signed.ErrLowVersion: + return fmt.Errorf("Warning: potential malicious behavior - trust data version is lower than expected for remote repository %s: %v", repoName, err) + case signed.ErrRoleThreshold: + return fmt.Errorf("Warning: potential malicious behavior - trust data has insufficient signatures for remote repository %s: %v", repoName, err) + case client.ErrRepositoryNotExist: + return fmt.Errorf("Error: remote trust data does not exist for %s: %v", repoName, err) + case signed.ErrInsufficientSignatures: + return fmt.Errorf("Error: could not produce valid signature for %s. If Yubikey was used, was touch input provided?: %v", repoName, err) + } + + return err +} + +func (cli *DockerCli) trustedPull(repoInfo *registry.RepositoryInfo, ref registry.Reference, authConfig types.AuthConfig, requestPrivilege apiclient.RequestPrivilegeFunc) error { + var refs []target + + notaryRepo, err := cli.getNotaryRepository(repoInfo, authConfig, "pull") + if err != nil { + fmt.Fprintf(cli.out, "Error establishing connection to trust repository: %s\n", err) + return err + } + + if ref.String() == "" { + // List all targets + targets, err := notaryRepo.ListTargets(releasesRole, data.CanonicalTargetsRole) + if err != nil { + return notaryError(repoInfo.FullName(), err) + } + for _, tgt := range targets { + t, err := convertTarget(tgt.Target) + if err != nil { + fmt.Fprintf(cli.out, "Skipping target for %q\n", repoInfo.Name()) + continue + } + // Only list tags in the top level targets role or the releases delegation role - ignore + // all other delegation roles + if tgt.Role != releasesRole && tgt.Role != data.CanonicalTargetsRole { + continue + } + refs = append(refs, t) + } + if len(refs) == 0 { + return notaryError(repoInfo.FullName(), fmt.Errorf("No trusted tags for %s", repoInfo.FullName())) + } + } else { + t, err := notaryRepo.GetTargetByName(ref.String(), releasesRole, data.CanonicalTargetsRole) + if err != nil { + return notaryError(repoInfo.FullName(), 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 != releasesRole && t.Role != data.CanonicalTargetsRole { + return notaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.String())) + } + + 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.reference.String() + if displayTag != "" { + displayTag = ":" + displayTag + } + fmt.Fprintf(cli.out, "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), repoInfo.Name(), displayTag, r.digest) + + if err := cli.imagePullPrivileged(authConfig, repoInfo.Name(), r.digest.String(), requestPrivilege); err != nil { + return err + } + + // If reference is not trusted, tag by trusted reference + if !r.reference.HasDigest() { + tagged, err := reference.WithTag(repoInfo, r.reference.String()) + if err != nil { + return err + } + trustedRef, err := reference.WithDigest(repoInfo, r.digest) + if err != nil { + return err + } + if err := cli.tagTrusted(trustedRef, tagged); err != nil { + return err + } + } + } + return nil +} + +func (cli *DockerCli) trustedPush(repoInfo *registry.RepositoryInfo, tag string, authConfig types.AuthConfig, requestPrivilege apiclient.RequestPrivilegeFunc) error { + responseBody, err := cli.imagePushPrivileged(authConfig, repoInfo.Name(), tag, requestPrivilege) + if err != nil { + return err + } + + defer responseBody.Close() + + // 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 distribution.PushResult + err := json.Unmarshal(*aux, &pushResult) + if err == nil && pushResult.Tag != "" && pushResult.Digest.Validate() == nil { + h, err := hex.DecodeString(pushResult.Digest.Hex()) + if err != nil { + target = nil + return + } + target.Name = registry.ParseReference(pushResult.Tag).String() + target.Hashes = data.Hashes{string(pushResult.Digest.Algorithm()): h} + target.Length = int64(pushResult.Size) + } + } + + // We want trust signatures to always take an explicit tag, + // otherwise it will act as an untrusted push. + if tag == "" { + if err = jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut, nil); err != nil { + return err + } + fmt.Fprintln(cli.out, "No tag specified, skipping trust metadata push") + return nil + } + + if err = jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut, handleTarget); err != nil { + return err + } + + if cnt > 1 { + return fmt.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 := cli.getNotaryRepository(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(rootKeyID, data.CanonicalSnapshotRole); err != nil { + return notaryError(repoInfo.FullName(), err) + } + fmt.Fprintf(cli.out, "Finished initializing %q\n", repoInfo.FullName()) + err = repo.AddTarget(target, data.CanonicalTargetsRole) + case nil: + // already initialized and we have successfully downloaded the latest metadata + err = cli.addTargetToAllSignableRoles(repo, target) + default: + return notaryError(repoInfo.FullName(), err) + } + + if err == nil { + err = repo.Publish() + } + + if err != nil { + fmt.Fprintf(cli.out, "Failed to sign %q:%s - %s\n", repoInfo.FullName(), tag, err.Error()) + return notaryError(repoInfo.FullName(), err) + } + + fmt.Fprintf(cli.out, "Successfully signed %q:%s\n", repoInfo.FullName(), 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 (cli *DockerCli) 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 fmt.Errorf("no valid signing keys for delegation roles") + } + + return repo.AddTarget(target, signableRoles...) +} diff --git a/api/client/trust_test.go b/api/client/trust_test.go new file mode 100644 index 00000000..ec95bd9d --- /dev/null +++ b/api/client/trust_test.go @@ -0,0 +1,56 @@ +package client + +import ( + "os" + "testing" + + "github.com/docker/docker/registry" + registrytypes "github.com/docker/engine-api/types/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 := trustServer(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 := trustServer(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 := trustServer(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 := trustServer(indexInfo) + expectedStr := "https://" + indexInfo.Name + if err != nil || output != expectedStr { + t.Fatalf("Expected server to be %s, got %s", expectedStr, output) + } +} diff --git a/api/client/unpause.go b/api/client/unpause.go new file mode 100644 index 00000000..b8630b1f --- /dev/null +++ b/api/client/unpause.go @@ -0,0 +1,34 @@ +package client + +import ( + "fmt" + "strings" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdUnpause unpauses all processes within a container, for one or more containers. +// +// Usage: docker unpause CONTAINER [CONTAINER...] +func (cli *DockerCli) CmdUnpause(args ...string) error { + cmd := Cli.Subcmd("unpause", []string{"CONTAINER [CONTAINER...]"}, Cli.DockerCommands["unpause"].Description, true) + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + var errs []string + for _, name := range cmd.Args() { + if err := cli.client.ContainerUnpause(context.Background(), name); err != nil { + errs = append(errs, err.Error()) + } else { + fmt.Fprintf(cli.out, "%s\n", name) + } + } + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) + } + return nil +} diff --git a/api/client/update.go b/api/client/update.go new file mode 100644 index 00000000..a2f9e534 --- /dev/null +++ b/api/client/update.go @@ -0,0 +1,117 @@ +package client + +import ( + "fmt" + "strings" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/runconfig/opts" + "github.com/docker/engine-api/types/container" + "github.com/docker/go-units" +) + +// CmdUpdate updates resources of one or more containers. +// +// Usage: docker update [OPTIONS] CONTAINER [CONTAINER...] +func (cli *DockerCli) CmdUpdate(args ...string) error { + cmd := Cli.Subcmd("update", []string{"CONTAINER [CONTAINER...]"}, Cli.DockerCommands["update"].Description, true) + flBlkioWeight := cmd.Uint16([]string{"-blkio-weight"}, 0, "Block IO (relative weight), between 10 and 1000") + flCPUPeriod := cmd.Int64([]string{"-cpu-period"}, 0, "Limit CPU CFS (Completely Fair Scheduler) period") + flCPUQuota := cmd.Int64([]string{"-cpu-quota"}, 0, "Limit CPU CFS (Completely Fair Scheduler) quota") + flCpusetCpus := cmd.String([]string{"-cpuset-cpus"}, "", "CPUs in which to allow execution (0-3, 0,1)") + flCpusetMems := cmd.String([]string{"-cpuset-mems"}, "", "MEMs in which to allow execution (0-3, 0,1)") + flCPUShares := cmd.Int64([]string{"#c", "-cpu-shares"}, 0, "CPU shares (relative weight)") + flMemoryString := cmd.String([]string{"m", "-memory"}, "", "Memory limit") + flMemoryReservation := cmd.String([]string{"-memory-reservation"}, "", "Memory soft limit") + flMemorySwap := cmd.String([]string{"-memory-swap"}, "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") + flKernelMemory := cmd.String([]string{"-kernel-memory"}, "", "Kernel memory limit") + flRestartPolicy := cmd.String([]string{"-restart"}, "", "Restart policy to apply when a container exits") + + cmd.Require(flag.Min, 1) + cmd.ParseFlags(args, true) + if cmd.NFlag() == 0 { + return fmt.Errorf("You must provide one or more flags when using this command.") + } + + var err error + var flMemory int64 + if *flMemoryString != "" { + flMemory, err = units.RAMInBytes(*flMemoryString) + if err != nil { + return err + } + } + + var memoryReservation int64 + if *flMemoryReservation != "" { + memoryReservation, err = units.RAMInBytes(*flMemoryReservation) + if err != nil { + return err + } + } + + var memorySwap int64 + if *flMemorySwap != "" { + if *flMemorySwap == "-1" { + memorySwap = -1 + } else { + memorySwap, err = units.RAMInBytes(*flMemorySwap) + if err != nil { + return err + } + } + } + + var kernelMemory int64 + if *flKernelMemory != "" { + kernelMemory, err = units.RAMInBytes(*flKernelMemory) + if err != nil { + return err + } + } + + var restartPolicy container.RestartPolicy + if *flRestartPolicy != "" { + restartPolicy, err = opts.ParseRestartPolicy(*flRestartPolicy) + if err != nil { + return err + } + } + + resources := container.Resources{ + BlkioWeight: *flBlkioWeight, + CpusetCpus: *flCpusetCpus, + CpusetMems: *flCpusetMems, + CPUShares: *flCPUShares, + Memory: flMemory, + MemoryReservation: memoryReservation, + MemorySwap: memorySwap, + KernelMemory: kernelMemory, + CPUPeriod: *flCPUPeriod, + CPUQuota: *flCPUQuota, + } + + updateConfig := container.UpdateConfig{ + Resources: resources, + RestartPolicy: restartPolicy, + } + + names := cmd.Args() + var errs []string + for _, name := range names { + if err := cli.client.ContainerUpdate(context.Background(), name, updateConfig); err != nil { + errs = append(errs, err.Error()) + } else { + fmt.Fprintf(cli.out, "%s\n", name) + } + } + + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) + } + + return nil +} diff --git a/api/client/utils.go b/api/client/utils.go new file mode 100644 index 00000000..4deee224 --- /dev/null +++ b/api/client/utils.go @@ -0,0 +1,202 @@ +package client + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + gosignal "os/signal" + "path/filepath" + "runtime" + "time" + + "golang.org/x/net/context" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/signal" + "github.com/docker/docker/pkg/term" + "github.com/docker/docker/registry" + "github.com/docker/engine-api/client" + "github.com/docker/engine-api/types" + registrytypes "github.com/docker/engine-api/types/registry" +) + +func (cli *DockerCli) electAuthServer() 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(context.Background()); 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 +} + +func (cli *DockerCli) registryAuthenticationPrivilegedFunc(index *registrytypes.IndexInfo, cmdName string) client.RequestPrivilegeFunc { + return func() (string, error) { + fmt.Fprintf(cli.out, "\nPlease login prior to %s:\n", cmdName) + indexServer := registry.GetAuthConfigKey(index) + authConfig, err := cli.configureAuth("", "", indexServer, false) + if err != nil { + return "", err + } + return encodeAuthToBase64(authConfig) + } +} + +func (cli *DockerCli) resizeTty(id string, isExec bool) { + height, width := cli.getTtySize() + cli.resizeTtyTo(id, height, width, isExec) +} + +func (cli *DockerCli) resizeTtyTo(id string, height, width int, isExec bool) { + if height == 0 && width == 0 { + return + } + + options := types.ResizeOptions{ + ID: id, + Height: height, + Width: width, + } + + var err error + if isExec { + err = cli.client.ContainerExecResize(context.Background(), options) + } else { + err = cli.client.ContainerResize(context.Background(), options) + } + + if err != nil { + logrus.Debugf("Error resize: %s", err) + } +} + +// getExitCode perform an inspect on the container. It returns +// the running state and the exit code. +func getExitCode(cli *DockerCli, containerID string) (bool, int, error) { + c, err := cli.client.ContainerInspect(context.Background(), containerID) + if err != nil { + // If we can't connect, then the daemon probably died. + if err != client.ErrConnectionFailed { + return false, -1, err + } + return false, -1, nil + } + + return c.State.Running, c.State.ExitCode, nil +} + +// getExecExitCode perform an inspect on the exec command. It returns +// the running state and the exit code. +func getExecExitCode(cli *DockerCli, execID string) (bool, int, error) { + resp, err := cli.client.ContainerExecInspect(context.Background(), execID) + if err != nil { + // If we can't connect, then the daemon probably died. + if err != client.ErrConnectionFailed { + return false, -1, err + } + return false, -1, nil + } + + return resp.Running, resp.ExitCode, nil +} + +func (cli *DockerCli) monitorTtySize(id string, isExec bool) error { + cli.resizeTty(id, isExec) + + if runtime.GOOS == "windows" { + go func() { + prevH, prevW := cli.getTtySize() + for { + time.Sleep(time.Millisecond * 250) + h, w := cli.getTtySize() + + if prevW != w || prevH != h { + cli.resizeTty(id, isExec) + } + prevH = h + prevW = w + } + }() + } else { + sigchan := make(chan os.Signal, 1) + gosignal.Notify(sigchan, signal.SIGWINCH) + go func() { + for range sigchan { + cli.resizeTty(id, isExec) + } + }() + } + return nil +} + +func (cli *DockerCli) getTtySize() (int, int) { + if !cli.isTerminalOut { + return 0, 0 + } + ws, err := term.GetWinsize(cli.outFd) + if err != nil { + logrus.Debugf("Error getting size: %s", err) + if ws == nil { + return 0, 0 + } + } + return int(ws.Height), int(ws.Width) +} + +func copyToFile(outfile string, r io.Reader) error { + tmpFile, err := ioutil.TempFile(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 +} + +// 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 (cli *DockerCli) resolveAuthConfig(index *registrytypes.IndexInfo) types.AuthConfig { + configKey := index.Name + if index.Official { + configKey = cli.electAuthServer() + } + + a, _ := getCredentials(cli.configFile, configKey) + return a +} + +func (cli *DockerCli) retrieveAuthConfigs() map[string]types.AuthConfig { + acs, _ := getAllCredentials(cli.configFile) + return acs +} diff --git a/api/client/version.go b/api/client/version.go new file mode 100644 index 00000000..ebec4def --- /dev/null +++ b/api/client/version.go @@ -0,0 +1,95 @@ +package client + +import ( + "runtime" + "text/template" + "time" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + "github.com/docker/docker/dockerversion" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/utils" + "github.com/docker/docker/utils/templates" + "github.com/docker/engine-api/types" +) + +var versionTemplate = `Client: + Version: {{.Client.Version}} + API version: {{.Client.APIVersion}} + Go version: {{.Client.GoVersion}} + Git commit: {{.Client.GitCommit}} + Built: {{.Client.BuildTime}} + OS/Arch: {{.Client.Os}}/{{.Client.Arch}}{{if .Client.Experimental}} + Experimental: {{.Client.Experimental}}{{end}}{{if .ServerOK}} + +Server: + Version: {{.Server.Version}} + API version: {{.Server.APIVersion}} + Go version: {{.Server.GoVersion}} + Git commit: {{.Server.GitCommit}} + Built: {{.Server.BuildTime}} + OS/Arch: {{.Server.Os}}/{{.Server.Arch}}{{if .Server.Experimental}} + Experimental: {{.Server.Experimental}}{{end}}{{end}}` + +// CmdVersion shows Docker version information. +// +// Available version information is shown for: client Docker version, client API version, client Go version, client Git commit, client OS/Arch, server Docker version, server API version, server Go version, server Git commit, and server OS/Arch. +// +// Usage: docker version +func (cli *DockerCli) CmdVersion(args ...string) (err error) { + cmd := Cli.Subcmd("version", nil, Cli.DockerCommands["version"].Description, true) + tmplStr := cmd.String([]string{"f", "#format", "-format"}, "", "Format the output using the given go template") + cmd.Require(flag.Exact, 0) + + cmd.ParseFlags(args, true) + + templateFormat := versionTemplate + if *tmplStr != "" { + templateFormat = *tmplStr + } + + var tmpl *template.Template + if tmpl, err = templates.Parse(templateFormat); err != nil { + return Cli.StatusError{StatusCode: 64, + Status: "Template parsing error: " + err.Error()} + } + + vd := types.VersionResponse{ + Client: &types.Version{ + Version: dockerversion.Version, + APIVersion: cli.client.ClientVersion(), + GoVersion: runtime.Version(), + GitCommit: dockerversion.GitCommit, + BuildTime: dockerversion.BuildTime, + Os: runtime.GOOS, + Arch: runtime.GOARCH, + Experimental: utils.ExperimentalBuild(), + }, + } + + serverVersion, err := cli.client.ServerVersion(context.Background()) + 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(cli.out, vd); err2 != nil && err == nil { + err = err2 + } + cli.out.Write([]byte{'\n'}) + return err +} diff --git a/api/client/volume.go b/api/client/volume.go new file mode 100644 index 00000000..37e623fb --- /dev/null +++ b/api/client/volume.go @@ -0,0 +1,177 @@ +package client + +import ( + "fmt" + "sort" + "text/tabwriter" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + "github.com/docker/docker/opts" + flag "github.com/docker/docker/pkg/mflag" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/filters" +) + +// CmdVolume is the parent subcommand for all volume commands +// +// Usage: docker volume +func (cli *DockerCli) CmdVolume(args ...string) error { + description := Cli.DockerCommands["volume"].Description + "\n\nCommands:\n" + commands := [][]string{ + {"create", "Create a volume"}, + {"inspect", "Return low-level information on a volume"}, + {"ls", "List volumes"}, + {"rm", "Remove a volume"}, + } + + for _, cmd := range commands { + description += fmt.Sprintf(" %-25.25s%s\n", cmd[0], cmd[1]) + } + + description += "\nRun 'docker volume COMMAND --help' for more information on a command" + cmd := Cli.Subcmd("volume", []string{"[COMMAND]"}, description, false) + + cmd.Require(flag.Exact, 0) + err := cmd.ParseFlags(args, true) + cmd.Usage() + return err +} + +// CmdVolumeLs outputs a list of Docker volumes. +// +// Usage: docker volume ls [OPTIONS] +func (cli *DockerCli) CmdVolumeLs(args ...string) error { + cmd := Cli.Subcmd("volume ls", nil, "List volumes", true) + + quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only display volume names") + flFilter := opts.NewListOpts(nil) + cmd.Var(&flFilter, []string{"f", "-filter"}, "Provide filter values (i.e. 'dangling=true')") + + cmd.Require(flag.Exact, 0) + cmd.ParseFlags(args, true) + + volFilterArgs := filters.NewArgs() + for _, f := range flFilter.GetAll() { + var err error + volFilterArgs, err = filters.ParseFlag(f, volFilterArgs) + if err != nil { + return err + } + } + + volumes, err := cli.client.VolumeList(context.Background(), volFilterArgs) + if err != nil { + return err + } + + w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) + if !*quiet { + for _, warn := range volumes.Warnings { + fmt.Fprintln(cli.err, warn) + } + fmt.Fprintf(w, "DRIVER \tVOLUME NAME") + fmt.Fprintf(w, "\n") + } + + sort.Sort(byVolumeName(volumes.Volumes)) + for _, vol := range volumes.Volumes { + if *quiet { + fmt.Fprintln(w, vol.Name) + continue + } + fmt.Fprintf(w, "%s\t%s\n", vol.Driver, vol.Name) + } + w.Flush() + return nil +} + +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 +} + +// CmdVolumeInspect displays low-level information on one or more volumes. +// +// Usage: docker volume inspect [OPTIONS] VOLUME [VOLUME...] +func (cli *DockerCli) CmdVolumeInspect(args ...string) error { + cmd := Cli.Subcmd("volume inspect", []string{"VOLUME [VOLUME...]"}, "Return low-level information on a volume", true) + tmplStr := cmd.String([]string{"f", "-format"}, "", "Format the output using the given go template") + + cmd.Require(flag.Min, 1) + cmd.ParseFlags(args, true) + + if err := cmd.Parse(args); err != nil { + return nil + } + + inspectSearcher := func(name string) (interface{}, []byte, error) { + i, err := cli.client.VolumeInspect(context.Background(), name) + return i, nil, err + } + + return cli.inspectElements(*tmplStr, cmd.Args(), inspectSearcher) +} + +// CmdVolumeCreate creates a new volume. +// +// Usage: docker volume create [OPTIONS] +func (cli *DockerCli) CmdVolumeCreate(args ...string) error { + cmd := Cli.Subcmd("volume create", nil, "Create a volume", true) + flDriver := cmd.String([]string{"d", "-driver"}, "local", "Specify volume driver name") + flName := cmd.String([]string{"-name"}, "", "Specify volume name") + + flDriverOpts := opts.NewMapOpts(nil, nil) + cmd.Var(flDriverOpts, []string{"o", "-opt"}, "Set driver specific options") + + flLabels := opts.NewListOpts(nil) + cmd.Var(&flLabels, []string{"-label"}, "Set metadata for a volume") + + cmd.Require(flag.Exact, 0) + cmd.ParseFlags(args, true) + + volReq := types.VolumeCreateRequest{ + Driver: *flDriver, + DriverOpts: flDriverOpts.GetAll(), + Name: *flName, + Labels: runconfigopts.ConvertKVStringsToMap(flLabels.GetAll()), + } + + vol, err := cli.client.VolumeCreate(context.Background(), volReq) + if err != nil { + return err + } + + fmt.Fprintf(cli.out, "%s\n", vol.Name) + return nil +} + +// CmdVolumeRm removes one or more volumes. +// +// Usage: docker volume rm VOLUME [VOLUME...] +func (cli *DockerCli) CmdVolumeRm(args ...string) error { + cmd := Cli.Subcmd("volume rm", []string{"VOLUME [VOLUME...]"}, "Remove a volume", true) + cmd.Require(flag.Min, 1) + cmd.ParseFlags(args, true) + + var status = 0 + + for _, name := range cmd.Args() { + if err := cli.client.VolumeRemove(context.Background(), name); err != nil { + fmt.Fprintf(cli.err, "%s\n", err) + status = 1 + continue + } + fmt.Fprintf(cli.out, "%s\n", name) + } + + if status != 0 { + return Cli.StatusError{StatusCode: status} + } + return nil +} diff --git a/api/client/wait.go b/api/client/wait.go new file mode 100644 index 00000000..609cd3be --- /dev/null +++ b/api/client/wait.go @@ -0,0 +1,37 @@ +package client + +import ( + "fmt" + "strings" + + "golang.org/x/net/context" + + Cli "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdWait blocks until a container stops, then prints its exit code. +// +// If more than one container is specified, this will wait synchronously on each container. +// +// Usage: docker wait CONTAINER [CONTAINER...] +func (cli *DockerCli) CmdWait(args ...string) error { + cmd := Cli.Subcmd("wait", []string{"CONTAINER [CONTAINER...]"}, Cli.DockerCommands["wait"].Description, true) + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + var errs []string + for _, name := range cmd.Args() { + status, err := cli.client.ContainerWait(context.Background(), name) + if err != nil { + errs = append(errs, err.Error()) + } else { + fmt.Fprintf(cli.out, "%d\n", status) + } + } + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) + } + return nil +} diff --git a/api/common.go b/api/common.go new file mode 100644 index 00000000..63560c6d --- /dev/null +++ b/api/common.go @@ -0,0 +1,146 @@ +package api + +import ( + "fmt" + "mime" + "path/filepath" + "sort" + "strconv" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/system" + "github.com/docker/docker/pkg/version" + "github.com/docker/engine-api/types" + "github.com/docker/libtrust" +) + +// Common constants for daemon and client. +const ( + // Version of Current REST API + DefaultVersion version.Version = "1.23" + + // MinVersion represents Minimum REST API version supported + MinVersion version.Version = "1.12" + + // NoBaseImageSpecifier is the symbol used by the FROM + // command to specify that no base image is to be used. + NoBaseImageSpecifier string = "scratch" +) + +// byPortInfo is a temporary type used to sort types.Port by its fields +type byPortInfo []types.Port + +func (r byPortInfo) Len() int { return len(r) } +func (r byPortInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r byPortInfo) Less(i, j int) bool { + if r[i].PrivatePort != r[j].PrivatePort { + return r[i].PrivatePort < r[j].PrivatePort + } + + if r[i].IP != r[j].IP { + return r[i].IP < r[j].IP + } + + if r[i].PublicPort != r[j].PublicPort { + return r[i].PublicPort < r[j].PublicPort + } + + return r[i].Type < r[j].Type +} + +// DisplayablePorts returns formatted string representing open ports of container +// e.g. "0.0.0.0:80->9090/tcp, 9988/tcp" +// it's used by command 'docker ps' +func DisplayablePorts(ports []types.Port) string { + type portGroup struct { + first int + last int + } + groupMap := make(map[string]*portGroup) + var result []string + var hostMappings []string + var groupMapKeys []string + sort.Sort(byPortInfo(ports)) + for _, port := range ports { + current := port.PrivatePort + portKey := port.Type + if port.IP != "" { + if port.PublicPort != current { + hostMappings = append(hostMappings, fmt.Sprintf("%s:%d->%d/%s", port.IP, port.PublicPort, port.PrivatePort, port.Type)) + continue + } + portKey = fmt.Sprintf("%s/%s", port.IP, port.Type) + } + group := groupMap[portKey] + + if group == nil { + groupMap[portKey] = &portGroup{first: current, last: current} + // record order that groupMap keys are created + groupMapKeys = append(groupMapKeys, portKey) + continue + } + if current == (group.last + 1) { + group.last = current + continue + } + + result = append(result, formGroup(portKey, group.first, group.last)) + groupMap[portKey] = &portGroup{first: current, last: current} + } + for _, portKey := range groupMapKeys { + g := groupMap[portKey] + result = append(result, formGroup(portKey, g.first, g.last)) + } + result = append(result, hostMappings...) + return strings.Join(result, ", ") +} + +func formGroup(key string, start, last int) string { + parts := strings.Split(key, "/") + groupType := parts[0] + var ip string + if len(parts) > 1 { + ip = parts[0] + groupType = parts[1] + } + group := strconv.Itoa(start) + if start != last { + group = fmt.Sprintf("%s-%d", group, last) + } + if ip != "" { + group = fmt.Sprintf("%s:%s->%s", ip, group, group) + } + return fmt.Sprintf("%s/%s", group, groupType) +} + +// MatchesContentType validates the content type against the expected one +func MatchesContentType(contentType, expectedType string) bool { + mimetype, _, err := mime.ParseMediaType(contentType) + if err != nil { + logrus.Errorf("Error parsing media type: %s error: %v", contentType, err) + } + return err == nil && mimetype == expectedType +} + +// LoadOrCreateTrustKey attempts to load the libtrust key at the given path, +// otherwise generates a new one +func LoadOrCreateTrustKey(trustKeyPath string) (libtrust.PrivateKey, error) { + err := system.MkdirAll(filepath.Dir(trustKeyPath), 0700) + if err != nil { + return nil, err + } + trustKey, err := libtrust.LoadKeyFile(trustKeyPath) + if err == libtrust.ErrKeyFileDoesNotExist { + trustKey, err = libtrust.GenerateECP256PrivateKey() + if err != nil { + return nil, fmt.Errorf("Error generating key: %s", err) + } + if err := libtrust.SaveKey(trustKeyPath, trustKey); err != nil { + return nil, fmt.Errorf("Error saving key file: %s", err) + } + } else if err != nil { + return nil, fmt.Errorf("Error loading key file %s: %s", trustKeyPath, err) + } + return trustKey, nil +} diff --git a/api/common_test.go b/api/common_test.go new file mode 100644 index 00000000..c214660c --- /dev/null +++ b/api/common_test.go @@ -0,0 +1,341 @@ +package api + +import ( + "io/ioutil" + "path/filepath" + "testing" + + "os" + + "github.com/docker/engine-api/types" +) + +type ports struct { + ports []types.Port + expected string +} + +// DisplayablePorts +func TestDisplayablePorts(t *testing.T) { + cases := []ports{ + { + []types.Port{ + { + PrivatePort: 9988, + Type: "tcp", + }, + }, + "9988/tcp"}, + { + []types.Port{ + { + PrivatePort: 9988, + Type: "udp", + }, + }, + "9988/udp", + }, + { + []types.Port{ + { + IP: "0.0.0.0", + PrivatePort: 9988, + Type: "tcp", + }, + }, + "0.0.0.0:0->9988/tcp", + }, + { + []types.Port{ + { + PrivatePort: 9988, + PublicPort: 8899, + Type: "tcp", + }, + }, + "9988/tcp", + }, + { + []types.Port{ + { + IP: "4.3.2.1", + PrivatePort: 9988, + PublicPort: 8899, + Type: "tcp", + }, + }, + "4.3.2.1:8899->9988/tcp", + }, + { + []types.Port{ + { + IP: "4.3.2.1", + PrivatePort: 9988, + PublicPort: 9988, + Type: "tcp", + }, + }, + "4.3.2.1:9988->9988/tcp", + }, + { + []types.Port{ + { + PrivatePort: 9988, + Type: "udp", + }, { + PrivatePort: 9988, + Type: "udp", + }, + }, + "9988/udp, 9988/udp", + }, + { + []types.Port{ + { + IP: "1.2.3.4", + PublicPort: 9998, + PrivatePort: 9998, + Type: "udp", + }, { + IP: "1.2.3.4", + PublicPort: 9999, + PrivatePort: 9999, + Type: "udp", + }, + }, + "1.2.3.4:9998-9999->9998-9999/udp", + }, + { + []types.Port{ + { + IP: "1.2.3.4", + PublicPort: 8887, + PrivatePort: 9998, + Type: "udp", + }, { + IP: "1.2.3.4", + PublicPort: 8888, + PrivatePort: 9999, + Type: "udp", + }, + }, + "1.2.3.4:8887->9998/udp, 1.2.3.4:8888->9999/udp", + }, + { + []types.Port{ + { + PrivatePort: 9998, + Type: "udp", + }, { + PrivatePort: 9999, + Type: "udp", + }, + }, + "9998-9999/udp", + }, + { + []types.Port{ + { + IP: "1.2.3.4", + PrivatePort: 6677, + PublicPort: 7766, + Type: "tcp", + }, { + PrivatePort: 9988, + PublicPort: 8899, + Type: "udp", + }, + }, + "9988/udp, 1.2.3.4:7766->6677/tcp", + }, + { + []types.Port{ + { + IP: "1.2.3.4", + PrivatePort: 9988, + PublicPort: 8899, + Type: "udp", + }, { + IP: "1.2.3.4", + PrivatePort: 9988, + PublicPort: 8899, + Type: "tcp", + }, { + IP: "4.3.2.1", + PrivatePort: 2233, + PublicPort: 3322, + Type: "tcp", + }, + }, + "4.3.2.1:3322->2233/tcp, 1.2.3.4:8899->9988/tcp, 1.2.3.4:8899->9988/udp", + }, + { + []types.Port{ + { + PrivatePort: 9988, + PublicPort: 8899, + Type: "udp", + }, { + IP: "1.2.3.4", + PrivatePort: 6677, + PublicPort: 7766, + Type: "tcp", + }, { + IP: "4.3.2.1", + PrivatePort: 2233, + PublicPort: 3322, + Type: "tcp", + }, + }, + "9988/udp, 4.3.2.1:3322->2233/tcp, 1.2.3.4:7766->6677/tcp", + }, + { + []types.Port{ + { + PrivatePort: 80, + Type: "tcp", + }, { + PrivatePort: 1024, + Type: "tcp", + }, { + PrivatePort: 80, + Type: "udp", + }, { + PrivatePort: 1024, + Type: "udp", + }, { + IP: "1.1.1.1", + PublicPort: 80, + PrivatePort: 1024, + Type: "tcp", + }, { + IP: "1.1.1.1", + PublicPort: 80, + PrivatePort: 1024, + Type: "udp", + }, { + IP: "1.1.1.1", + PublicPort: 1024, + PrivatePort: 80, + Type: "tcp", + }, { + IP: "1.1.1.1", + PublicPort: 1024, + PrivatePort: 80, + Type: "udp", + }, { + IP: "2.1.1.1", + PublicPort: 80, + PrivatePort: 1024, + Type: "tcp", + }, { + IP: "2.1.1.1", + PublicPort: 80, + PrivatePort: 1024, + Type: "udp", + }, { + IP: "2.1.1.1", + PublicPort: 1024, + PrivatePort: 80, + Type: "tcp", + }, { + IP: "2.1.1.1", + PublicPort: 1024, + PrivatePort: 80, + Type: "udp", + }, + }, + "80/tcp, 80/udp, 1024/tcp, 1024/udp, 1.1.1.1:1024->80/tcp, 1.1.1.1:1024->80/udp, 2.1.1.1:1024->80/tcp, 2.1.1.1:1024->80/udp, 1.1.1.1:80->1024/tcp, 1.1.1.1:80->1024/udp, 2.1.1.1:80->1024/tcp, 2.1.1.1:80->1024/udp", + }, + } + + for _, port := range cases { + actual := DisplayablePorts(port.ports) + if port.expected != actual { + t.Fatalf("Expected %s, got %s.", port.expected, actual) + } + } +} + +// MatchesContentType +func TestJsonContentType(t *testing.T) { + if !MatchesContentType("application/json", "application/json") { + t.Fail() + } + + if !MatchesContentType("application/json; charset=utf-8", "application/json") { + t.Fail() + } + + if MatchesContentType("dockerapplication/json", "application/json") { + t.Fail() + } +} + +// LoadOrCreateTrustKey +func TestLoadOrCreateTrustKeyInvalidKeyFile(t *testing.T) { + tmpKeyFolderPath, err := ioutil.TempDir("", "api-trustkey-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpKeyFolderPath) + + tmpKeyFile, err := ioutil.TempFile(tmpKeyFolderPath, "keyfile") + if err != nil { + t.Fatal(err) + } + + if _, err := LoadOrCreateTrustKey(tmpKeyFile.Name()); err == nil { + t.Fatalf("expected an error, got nothing.") + } + +} + +func TestLoadOrCreateTrustKeyCreateKey(t *testing.T) { + tmpKeyFolderPath, err := ioutil.TempDir("", "api-trustkey-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpKeyFolderPath) + + // Without the need to create the folder hierarchy + tmpKeyFile := filepath.Join(tmpKeyFolderPath, "keyfile") + + if key, err := LoadOrCreateTrustKey(tmpKeyFile); err != nil || key == nil { + t.Fatalf("expected a new key file, got : %v and %v", err, key) + } + + if _, err := os.Stat(tmpKeyFile); err != nil { + t.Fatalf("Expected to find a file %s, got %v", tmpKeyFile, err) + } + + // With the need to create the folder hierarchy as tmpKeyFie is in a path + // where some folders do not exist. + tmpKeyFile = filepath.Join(tmpKeyFolderPath, "folder/hierarchy/keyfile") + + if key, err := LoadOrCreateTrustKey(tmpKeyFile); err != nil || key == nil { + t.Fatalf("expected a new key file, got : %v and %v", err, key) + } + + if _, err := os.Stat(tmpKeyFile); err != nil { + t.Fatalf("Expected to find a file %s, got %v", tmpKeyFile, err) + } + + // With no path at all + defer os.Remove("keyfile") + if key, err := LoadOrCreateTrustKey("keyfile"); err != nil || key == nil { + t.Fatalf("expected a new key file, got : %v and %v", err, key) + } + + if _, err := os.Stat("keyfile"); err != nil { + t.Fatalf("Expected to find a file keyfile, got %v", err) + } +} + +func TestLoadOrCreateTrustKeyLoadValidKey(t *testing.T) { + tmpKeyFile := filepath.Join("fixtures", "keyfile") + + if key, err := LoadOrCreateTrustKey(tmpKeyFile); err != nil || key == nil { + t.Fatalf("expected a key file, got : %v and %v", err, key) + } +} diff --git a/api/fixtures/keyfile b/api/fixtures/keyfile new file mode 100644 index 00000000..322f2544 --- /dev/null +++ b/api/fixtures/keyfile @@ -0,0 +1,7 @@ +-----BEGIN EC PRIVATE KEY----- +keyID: AWX2:I27X:WQFX:IOMK:CNAK:O7PW:VYNB:ZLKC:CVAE:YJP2:SI4A:XXAY + +MHcCAQEEILHTRWdcpKWsnORxSFyBnndJ4ROU41hMtr/GCiLVvwBQoAoGCCqGSM49 +AwEHoUQDQgAElpVFbQ2V2UQKajqdE3fVxJ+/pE/YuEFOxWbOxF2be19BY209/iky +NzeFFK7SLpQ4CBJ7zDVXOHsMzrkY/GquGA== +-----END EC PRIVATE KEY----- diff --git a/api/server/httputils/errors.go b/api/server/httputils/errors.go new file mode 100644 index 00000000..cf8d2ae6 --- /dev/null +++ b/api/server/httputils/errors.go @@ -0,0 +1,70 @@ +package httputils + +import ( + "net/http" + "strings" + + "github.com/Sirupsen/logrus" +) + +// httpStatusError is an interface +// that errors with custom status codes +// implement to tell the api layer +// which response status to set. +type httpStatusError interface { + HTTPErrorStatusCode() int +} + +// inputValidationError is an interface +// that errors generated by invalid +// inputs can implement to tell the +// api layer to set a 400 status code +// in the response. +type inputValidationError interface { + IsValidationError() bool +} + +// WriteError decodes a specific docker error and sends it in the response. +func WriteError(w http.ResponseWriter, err error) { + if err == nil || w == nil { + logrus.WithFields(logrus.Fields{"error": err, "writer": w}).Error("unexpected HTTP error handling") + return + } + + var statusCode int + errMsg := err.Error() + + switch e := err.(type) { + case httpStatusError: + statusCode = e.HTTPErrorStatusCode() + case inputValidationError: + statusCode = http.StatusBadRequest + default: + // FIXME: this is brittle and should not be necessary, but we still need to identify if + // there are errors falling back into this logic. + // If we need to differentiate between different possible error types, + // we should create appropriate error types that implement the httpStatusError interface. + errStr := strings.ToLower(errMsg) + for keyword, status := range map[string]int{ + "not found": http.StatusNotFound, + "no such": http.StatusNotFound, + "bad parameter": http.StatusBadRequest, + "conflict": http.StatusConflict, + "impossible": http.StatusNotAcceptable, + "wrong login/password": http.StatusUnauthorized, + "unauthorized": http.StatusUnauthorized, + "hasn't been activated": http.StatusForbidden, + } { + if strings.Contains(errStr, keyword) { + statusCode = status + break + } + } + } + + if statusCode == 0 { + statusCode = http.StatusInternalServerError + } + + http.Error(w, errMsg, statusCode) +} diff --git a/api/server/httputils/form.go b/api/server/httputils/form.go new file mode 100644 index 00000000..20188c12 --- /dev/null +++ b/api/server/httputils/form.go @@ -0,0 +1,73 @@ +package httputils + +import ( + "fmt" + "net/http" + "path/filepath" + "strconv" + "strings" +) + +// BoolValue transforms a form value in different formats into a boolean type. +func BoolValue(r *http.Request, k string) bool { + s := strings.ToLower(strings.TrimSpace(r.FormValue(k))) + return !(s == "" || s == "0" || s == "no" || s == "false" || s == "none") +} + +// BoolValueOrDefault returns the default bool passed if the query param is +// missing, otherwise it's just a proxy to boolValue above +func BoolValueOrDefault(r *http.Request, k string, d bool) bool { + if _, ok := r.Form[k]; !ok { + return d + } + return BoolValue(r, k) +} + +// Int64ValueOrZero parses a form value into an int64 type. +// It returns 0 if the parsing fails. +func Int64ValueOrZero(r *http.Request, k string) int64 { + val, err := Int64ValueOrDefault(r, k, 0) + if err != nil { + return 0 + } + return val +} + +// Int64ValueOrDefault parses a form value into an int64 type. If there is an +// error, returns the error. If there is no value returns the default value. +func Int64ValueOrDefault(r *http.Request, field string, def int64) (int64, error) { + if r.Form.Get(field) != "" { + value, err := strconv.ParseInt(r.Form.Get(field), 10, 64) + if err != nil { + return value, err + } + return value, nil + } + return def, nil +} + +// ArchiveOptions stores archive information for different operations. +type ArchiveOptions struct { + Name string + Path string +} + +// ArchiveFormValues parses form values and turns them into ArchiveOptions. +// It fails if the archive name and path are not in the request. +func ArchiveFormValues(r *http.Request, vars map[string]string) (ArchiveOptions, error) { + if err := ParseForm(r); err != nil { + return ArchiveOptions{}, err + } + + name := vars["name"] + path := filepath.FromSlash(r.Form.Get("path")) + + switch { + case name == "": + return ArchiveOptions{}, fmt.Errorf("bad parameter: 'name' cannot be empty") + case path == "": + return ArchiveOptions{}, fmt.Errorf("bad parameter: 'path' cannot be empty") + } + + return ArchiveOptions{name, path}, nil +} diff --git a/api/server/httputils/form_test.go b/api/server/httputils/form_test.go new file mode 100644 index 00000000..c56f7c15 --- /dev/null +++ b/api/server/httputils/form_test.go @@ -0,0 +1,105 @@ +package httputils + +import ( + "net/http" + "net/url" + "testing" +) + +func TestBoolValue(t *testing.T) { + cases := map[string]bool{ + "": false, + "0": false, + "no": false, + "false": false, + "none": false, + "1": true, + "yes": true, + "true": true, + "one": true, + "100": true, + } + + for c, e := range cases { + v := url.Values{} + v.Set("test", c) + r, _ := http.NewRequest("POST", "", nil) + r.Form = v + + a := BoolValue(r, "test") + if a != e { + t.Fatalf("Value: %s, expected: %v, actual: %v", c, e, a) + } + } +} + +func TestBoolValueOrDefault(t *testing.T) { + r, _ := http.NewRequest("GET", "", nil) + if !BoolValueOrDefault(r, "queryparam", true) { + t.Fatal("Expected to get true default value, got false") + } + + v := url.Values{} + v.Set("param", "") + r, _ = http.NewRequest("GET", "", nil) + r.Form = v + if BoolValueOrDefault(r, "param", true) { + t.Fatal("Expected not to get true") + } +} + +func TestInt64ValueOrZero(t *testing.T) { + cases := map[string]int64{ + "": 0, + "asdf": 0, + "0": 0, + "1": 1, + } + + for c, e := range cases { + v := url.Values{} + v.Set("test", c) + r, _ := http.NewRequest("POST", "", nil) + r.Form = v + + a := Int64ValueOrZero(r, "test") + if a != e { + t.Fatalf("Value: %s, expected: %v, actual: %v", c, e, a) + } + } +} + +func TestInt64ValueOrDefault(t *testing.T) { + cases := map[string]int64{ + "": -1, + "-1": -1, + "42": 42, + } + + for c, e := range cases { + v := url.Values{} + v.Set("test", c) + r, _ := http.NewRequest("POST", "", nil) + r.Form = v + + a, err := Int64ValueOrDefault(r, "test", -1) + if a != e { + t.Fatalf("Value: %s, expected: %v, actual: %v", c, e, a) + } + if err != nil { + t.Fatalf("Error should be nil, but received: %s", err) + } + } +} + +func TestInt64ValueOrDefaultWithError(t *testing.T) { + v := url.Values{} + v.Set("test", "invalid") + r, _ := http.NewRequest("POST", "", nil) + r.Form = v + + _, err := Int64ValueOrDefault(r, "test", -1) + if err == nil { + t.Fatalf("Expected an error.") + } +} diff --git a/api/server/httputils/httputils.go b/api/server/httputils/httputils.go new file mode 100644 index 00000000..59ee0308 --- /dev/null +++ b/api/server/httputils/httputils.go @@ -0,0 +1,107 @@ +package httputils + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "golang.org/x/net/context" + + "github.com/docker/docker/api" + "github.com/docker/docker/pkg/version" +) + +// APIVersionKey is the client's requested API version. +const APIVersionKey = "api-version" + +// UAStringKey is used as key type for user-agent string in net/context struct +const UAStringKey = "upstream-user-agent" + +// APIFunc is an adapter to allow the use of ordinary functions as Docker API endpoints. +// Any function that has the appropriate signature can be registered as a API endpoint (e.g. getVersion). +type APIFunc func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error + +// HijackConnection interrupts the http response writer to get the +// underlying connection and operate with it. +func HijackConnection(w http.ResponseWriter) (io.ReadCloser, io.Writer, error) { + conn, _, err := w.(http.Hijacker).Hijack() + if err != nil { + return nil, nil, err + } + // Flush the options to make sure the client sets the raw mode + conn.Write([]byte{}) + return conn, conn, nil +} + +// CloseStreams ensures that a list for http streams are properly closed. +func CloseStreams(streams ...interface{}) { + for _, stream := range streams { + if tcpc, ok := stream.(interface { + CloseWrite() error + }); ok { + tcpc.CloseWrite() + } else if closer, ok := stream.(io.Closer); ok { + closer.Close() + } + } +} + +// CheckForJSON makes sure that the request's Content-Type is application/json. +func CheckForJSON(r *http.Request) error { + ct := r.Header.Get("Content-Type") + + // No Content-Type header is ok as long as there's no Body + if ct == "" { + if r.Body == nil || r.ContentLength == 0 { + return nil + } + } + + // Otherwise it better be json + if api.MatchesContentType(ct, "application/json") { + return nil + } + return fmt.Errorf("Content-Type specified (%s) must be 'application/json'", ct) +} + +// ParseForm ensures the request form is parsed even with invalid content types. +// If we don't do this, POST method without Content-type (even with empty body) will fail. +func ParseForm(r *http.Request) error { + if r == nil { + return nil + } + if err := r.ParseForm(); err != nil && !strings.HasPrefix(err.Error(), "mime:") { + return err + } + return nil +} + +// ParseMultipartForm ensures the request form is parsed, even with invalid content types. +func ParseMultipartForm(r *http.Request) error { + if err := r.ParseMultipartForm(4096); err != nil && !strings.HasPrefix(err.Error(), "mime:") { + return err + } + return nil +} + +// WriteJSON writes the value v to the http response stream as json with standard json encoding. +func WriteJSON(w http.ResponseWriter, code int, v interface{}) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + return json.NewEncoder(w).Encode(v) +} + +// VersionFromContext returns an API version from the context using APIVersionKey. +// It panics if the context value does not have version.Version type. +func VersionFromContext(ctx context.Context) (ver version.Version) { + if ctx == nil { + return + } + val := ctx.Value(APIVersionKey) + if val == nil { + return + } + return val.(version.Version) +} diff --git a/api/server/middleware.go b/api/server/middleware.go new file mode 100644 index 00000000..2622bf1b --- /dev/null +++ b/api/server/middleware.go @@ -0,0 +1,41 @@ +package server + +import ( + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api" + "github.com/docker/docker/api/server/httputils" + "github.com/docker/docker/api/server/middleware" + "github.com/docker/docker/dockerversion" + "github.com/docker/docker/pkg/authorization" +) + +// handleWithGlobalMiddlwares wraps the handler function for a request with +// the server's global middlewares. The order of the middlewares is backwards, +// meaning that the first in the list will be evaluated last. +func (s *Server) handleWithGlobalMiddlewares(handler httputils.APIFunc) httputils.APIFunc { + next := handler + + handleVersion := middleware.NewVersionMiddleware(dockerversion.Version, api.DefaultVersion, api.MinVersion) + next = handleVersion(next) + + if s.cfg.EnableCors { + handleCORS := middleware.NewCORSMiddleware(s.cfg.CorsHeaders) + next = handleCORS(next) + } + + handleUserAgent := middleware.NewUserAgentMiddleware(s.cfg.Version) + next = handleUserAgent(next) + + // Only want this on debug level + if s.cfg.Logging && logrus.GetLevel() == logrus.DebugLevel { + next = middleware.DebugRequestMiddleware(next) + } + + if len(s.cfg.AuthorizationPluginNames) > 0 { + s.authZPlugins = authorization.NewPlugins(s.cfg.AuthorizationPluginNames) + handleAuthorization := middleware.NewAuthorizationMiddleware(s.authZPlugins) + next = handleAuthorization(next) + } + + return next +} diff --git a/api/server/middleware/authorization.go b/api/server/middleware/authorization.go new file mode 100644 index 00000000..cbfa99e7 --- /dev/null +++ b/api/server/middleware/authorization.go @@ -0,0 +1,42 @@ +package middleware + +import ( + "net/http" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/server/httputils" + "github.com/docker/docker/pkg/authorization" + "golang.org/x/net/context" +) + +// NewAuthorizationMiddleware creates a new Authorization middleware. +func NewAuthorizationMiddleware(plugins []authorization.Plugin) Middleware { + return func(handler httputils.APIFunc) httputils.APIFunc { + return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + // FIXME: fill when authN gets in + // User and UserAuthNMethod are taken from AuthN plugins + // Currently tracked in https://github.com/docker/docker/pull/13994 + user := "" + userAuthNMethod := "" + authCtx := authorization.NewCtx(plugins, user, userAuthNMethod, r.Method, r.RequestURI) + + if err := authCtx.AuthZRequest(w, r); err != nil { + logrus.Errorf("AuthZRequest for %s %s returned error: %s", r.Method, r.RequestURI, err) + return err + } + + rw := authorization.NewResponseModifier(w) + + if err := handler(ctx, rw, r, vars); err != nil { + logrus.Errorf("Handler for %s %s returned error: %s", r.Method, r.RequestURI, err) + return err + } + + if err := authCtx.AuthZResponse(rw, r); err != nil { + logrus.Errorf("AuthZResponse for %s %s returned error: %s", r.Method, r.RequestURI, err) + return err + } + return nil + } + } +} diff --git a/api/server/middleware/cors.go b/api/server/middleware/cors.go new file mode 100644 index 00000000..de21897d --- /dev/null +++ b/api/server/middleware/cors.go @@ -0,0 +1,33 @@ +package middleware + +import ( + "net/http" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/server/httputils" + "golang.org/x/net/context" +) + +// NewCORSMiddleware creates a new CORS middleware. +func NewCORSMiddleware(defaultHeaders string) Middleware { + return func(handler httputils.APIFunc) httputils.APIFunc { + return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + // If "api-cors-header" is not given, but "api-enable-cors" is true, we set cors to "*" + // otherwise, all head values will be passed to HTTP handler + corsHeaders := defaultHeaders + if corsHeaders == "" { + corsHeaders = "*" + } + + writeCorsHeaders(w, r, corsHeaders) + return handler(ctx, w, r, vars) + } + } +} + +func writeCorsHeaders(w http.ResponseWriter, r *http.Request, corsHeaders string) { + logrus.Debugf("CORS header is enabled and set to: %s", corsHeaders) + w.Header().Add("Access-Control-Allow-Origin", corsHeaders) + w.Header().Add("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, X-Registry-Auth") + w.Header().Add("Access-Control-Allow-Methods", "HEAD, GET, POST, DELETE, PUT, OPTIONS") +} diff --git a/api/server/middleware/debug.go b/api/server/middleware/debug.go new file mode 100644 index 00000000..be7056f6 --- /dev/null +++ b/api/server/middleware/debug.go @@ -0,0 +1,56 @@ +package middleware + +import ( + "bufio" + "encoding/json" + "io" + "net/http" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/server/httputils" + "github.com/docker/docker/pkg/ioutils" + "golang.org/x/net/context" +) + +// DebugRequestMiddleware dumps the request to logger +func DebugRequestMiddleware(handler httputils.APIFunc) httputils.APIFunc { + return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + logrus.Debugf("Calling %s %s", r.Method, r.RequestURI) + + if r.Method != "POST" { + return handler(ctx, w, r, vars) + } + if err := httputils.CheckForJSON(r); err != nil { + return handler(ctx, w, r, vars) + } + maxBodySize := 4096 // 4KB + if r.ContentLength > int64(maxBodySize) { + return handler(ctx, w, r, vars) + } + + body := r.Body + bufReader := bufio.NewReaderSize(body, maxBodySize) + r.Body = ioutils.NewReadCloserWrapper(bufReader, func() error { return body.Close() }) + + b, err := bufReader.Peek(maxBodySize) + if err != io.EOF { + // either there was an error reading, or the buffer is full (in which case the request is too large) + return handler(ctx, w, r, vars) + } + + var postForm map[string]interface{} + if err := json.Unmarshal(b, &postForm); err == nil { + if _, exists := postForm["password"]; exists { + postForm["password"] = "*****" + } + formStr, errMarshal := json.Marshal(postForm) + if errMarshal == nil { + logrus.Debugf("form data: %s", string(formStr)) + } else { + logrus.Debugf("form data: %q", postForm) + } + } + + return handler(ctx, w, r, vars) + } +} diff --git a/api/server/middleware/middleware.go b/api/server/middleware/middleware.go new file mode 100644 index 00000000..588331ae --- /dev/null +++ b/api/server/middleware/middleware.go @@ -0,0 +1,7 @@ +package middleware + +import "github.com/docker/docker/api/server/httputils" + +// Middleware is an adapter to allow the use of ordinary functions as Docker API filters. +// Any function that has the appropriate signature can be registered as a middleware. +type Middleware func(handler httputils.APIFunc) httputils.APIFunc diff --git a/api/server/middleware/user_agent.go b/api/server/middleware/user_agent.go new file mode 100644 index 00000000..188196bf --- /dev/null +++ b/api/server/middleware/user_agent.go @@ -0,0 +1,37 @@ +package middleware + +import ( + "net/http" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/server/httputils" + "github.com/docker/docker/pkg/version" + "golang.org/x/net/context" +) + +// NewUserAgentMiddleware creates a new UserAgent middleware. +func NewUserAgentMiddleware(versionCheck string) Middleware { + serverVersion := version.Version(versionCheck) + + return func(handler httputils.APIFunc) httputils.APIFunc { + return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + ctx = context.WithValue(ctx, httputils.UAStringKey, r.Header.Get("User-Agent")) + + if strings.Contains(r.Header.Get("User-Agent"), "Docker-Client/") { + userAgent := strings.Split(r.Header.Get("User-Agent"), "/") + + // v1.20 onwards includes the GOOS of the client after the version + // such as Docker/1.7.0 (linux) + if len(userAgent) == 2 && strings.Contains(userAgent[1], " ") { + userAgent[1] = strings.Split(userAgent[1], " ")[0] + } + + if len(userAgent) == 2 && !serverVersion.Equal(version.Version(userAgent[1])) { + logrus.Debugf("Client and server don't have the same version (client: %s, server: %s)", userAgent[1], serverVersion) + } + } + return handler(ctx, w, r, vars) + } + } +} diff --git a/api/server/middleware/version.go b/api/server/middleware/version.go new file mode 100644 index 00000000..41d518bc --- /dev/null +++ b/api/server/middleware/version.go @@ -0,0 +1,45 @@ +package middleware + +import ( + "fmt" + "net/http" + "runtime" + + "github.com/docker/docker/api/server/httputils" + "github.com/docker/docker/pkg/version" + "golang.org/x/net/context" +) + +type badRequestError struct { + error +} + +func (badRequestError) HTTPErrorStatusCode() int { + return http.StatusBadRequest +} + +// NewVersionMiddleware creates a new Version middleware. +func NewVersionMiddleware(versionCheck string, defaultVersion, minVersion version.Version) Middleware { + serverVersion := version.Version(versionCheck) + + return func(handler httputils.APIFunc) httputils.APIFunc { + return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + apiVersion := version.Version(vars["version"]) + if apiVersion == "" { + apiVersion = defaultVersion + } + + if apiVersion.GreaterThan(defaultVersion) { + return badRequestError{fmt.Errorf("client is newer than server (client API version: %s, server API version: %s)", apiVersion, defaultVersion)} + } + if apiVersion.LessThan(minVersion) { + return badRequestError{fmt.Errorf("client version %s is too old. Minimum supported API version is %s, please upgrade your client to a newer version", apiVersion, minVersion)} + } + + header := fmt.Sprintf("Docker/%s (%s)", serverVersion, runtime.GOOS) + w.Header().Set("Server", header) + ctx = context.WithValue(ctx, httputils.APIVersionKey, apiVersion) + return handler(ctx, w, r, vars) + } + } +} diff --git a/api/server/middleware/version_test.go b/api/server/middleware/version_test.go new file mode 100644 index 00000000..f60a98e5 --- /dev/null +++ b/api/server/middleware/version_test.go @@ -0,0 +1,64 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/docker/docker/api/server/httputils" + "github.com/docker/docker/pkg/version" + "golang.org/x/net/context" +) + +func TestVersionMiddleware(t *testing.T) { + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if httputils.VersionFromContext(ctx) == "" { + t.Fatalf("Expected version, got empty string") + } + return nil + } + + defaultVersion := version.Version("1.10.0") + minVersion := version.Version("1.2.0") + m := NewVersionMiddleware(defaultVersion.String(), defaultVersion, minVersion) + h := m(handler) + + req, _ := http.NewRequest("GET", "/containers/json", nil) + resp := httptest.NewRecorder() + ctx := context.Background() + if err := h(ctx, resp, req, map[string]string{}); err != nil { + t.Fatal(err) + } +} + +func TestVersionMiddlewareWithErrors(t *testing.T) { + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if httputils.VersionFromContext(ctx) == "" { + t.Fatalf("Expected version, got empty string") + } + return nil + } + + defaultVersion := version.Version("1.10.0") + minVersion := version.Version("1.2.0") + m := NewVersionMiddleware(defaultVersion.String(), defaultVersion, minVersion) + h := m(handler) + + req, _ := http.NewRequest("GET", "/containers/json", nil) + resp := httptest.NewRecorder() + ctx := context.Background() + + vars := map[string]string{"version": "0.1"} + err := h(ctx, resp, req, vars) + + if !strings.Contains(err.Error(), "client version 0.1 is too old. Minimum supported API version is 1.2.0") { + t.Fatalf("Expected too old client error, got %v", err) + } + + vars["version"] = "100000" + err = h(ctx, resp, req, vars) + if !strings.Contains(err.Error(), "client is newer than server") { + t.Fatalf("Expected client newer than server error, got %v", err) + } +} diff --git a/api/server/profiler.go b/api/server/profiler.go new file mode 100644 index 00000000..3c0dfd08 --- /dev/null +++ b/api/server/profiler.go @@ -0,0 +1,40 @@ +package server + +import ( + "expvar" + "fmt" + "net/http" + "net/http/pprof" + + "github.com/gorilla/mux" +) + +const debugPathPrefix = "/debug/" + +func profilerSetup(mainRouter *mux.Router) { + var r = mainRouter.PathPrefix(debugPathPrefix).Subrouter() + r.HandleFunc("/vars", expVars) + r.HandleFunc("/pprof/", pprof.Index) + r.HandleFunc("/pprof/cmdline", pprof.Cmdline) + r.HandleFunc("/pprof/profile", pprof.Profile) + r.HandleFunc("/pprof/symbol", pprof.Symbol) + r.HandleFunc("/pprof/block", pprof.Handler("block").ServeHTTP) + r.HandleFunc("/pprof/heap", pprof.Handler("heap").ServeHTTP) + r.HandleFunc("/pprof/goroutine", pprof.Handler("goroutine").ServeHTTP) + r.HandleFunc("/pprof/threadcreate", pprof.Handler("threadcreate").ServeHTTP) +} + +// Replicated from expvar.go as not public. +func expVars(w http.ResponseWriter, r *http.Request) { + first := true + w.Header().Set("Content-Type", "application/json; charset=utf-8") + fmt.Fprintf(w, "{\n") + expvar.Do(func(kv expvar.KeyValue) { + if !first { + fmt.Fprintf(w, ",\n") + } + first = false + fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value) + }) + fmt.Fprintf(w, "\n}\n") +} diff --git a/api/server/router/build/backend.go b/api/server/router/build/backend.go new file mode 100644 index 00000000..839f3160 --- /dev/null +++ b/api/server/router/build/backend.go @@ -0,0 +1,20 @@ +package build + +import ( + "io" + + "github.com/docker/docker/builder" + "github.com/docker/engine-api/types" + "golang.org/x/net/context" +) + +// Backend abstracts an image builder whose only purpose is to build an image referenced by an imageID. +type Backend interface { + // Build builds a Docker image referenced by an imageID string. + // + // Note: Tagging an image should not be done by a Builder, it should instead be done + // by the caller. + // + // TODO: make this return a reference instead of string + Build(clientCtx context.Context, config *types.ImageBuildOptions, context builder.Context, stdout io.Writer, stderr io.Writer, out io.Writer, clientGone <-chan bool) (string, error) +} diff --git a/api/server/router/build/build.go b/api/server/router/build/build.go new file mode 100644 index 00000000..dc85d1df --- /dev/null +++ b/api/server/router/build/build.go @@ -0,0 +1,29 @@ +package build + +import "github.com/docker/docker/api/server/router" + +// buildRouter is a router to talk with the build controller +type buildRouter struct { + backend Backend + routes []router.Route +} + +// NewRouter initializes a new build router +func NewRouter(b Backend) router.Router { + r := &buildRouter{ + backend: b, + } + r.initRoutes() + return r +} + +// Routes returns the available routers to the build controller +func (r *buildRouter) Routes() []router.Route { + return r.routes +} + +func (r *buildRouter) initRoutes() { + r.routes = []router.Route{ + router.NewPostRoute("/build", r.postBuild), + } +} diff --git a/api/server/router/build/build_routes.go b/api/server/router/build/build_routes.go new file mode 100644 index 00000000..a6b787d5 --- /dev/null +++ b/api/server/router/build/build_routes.go @@ -0,0 +1,213 @@ +package build + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "strings" + "sync" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/server/httputils" + "github.com/docker/docker/builder" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/progress" + "github.com/docker/docker/pkg/streamformatter" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/container" + "github.com/docker/go-units" + "golang.org/x/net/context" +) + +func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBuildOptions, error) { + version := httputils.VersionFromContext(ctx) + options := &types.ImageBuildOptions{} + if httputils.BoolValue(r, "forcerm") && version.GreaterThanOrEqualTo("1.12") { + options.Remove = true + } else if r.FormValue("rm") == "" && version.GreaterThanOrEqualTo("1.12") { + options.Remove = true + } else { + options.Remove = httputils.BoolValue(r, "rm") + } + if httputils.BoolValue(r, "pull") && version.GreaterThanOrEqualTo("1.16") { + options.PullParent = true + } + + options.Dockerfile = r.FormValue("dockerfile") + options.SuppressOutput = httputils.BoolValue(r, "q") + options.NoCache = httputils.BoolValue(r, "nocache") + options.ForceRemove = httputils.BoolValue(r, "forcerm") + options.MemorySwap = httputils.Int64ValueOrZero(r, "memswap") + options.Memory = httputils.Int64ValueOrZero(r, "memory") + options.CPUShares = httputils.Int64ValueOrZero(r, "cpushares") + options.CPUPeriod = httputils.Int64ValueOrZero(r, "cpuperiod") + options.CPUQuota = httputils.Int64ValueOrZero(r, "cpuquota") + options.CPUSetCPUs = r.FormValue("cpusetcpus") + options.CPUSetMems = r.FormValue("cpusetmems") + options.CgroupParent = r.FormValue("cgroupparent") + options.Tags = r.Form["t"] + + if r.Form.Get("shmsize") != "" { + shmSize, err := strconv.ParseInt(r.Form.Get("shmsize"), 10, 64) + if err != nil { + return nil, err + } + options.ShmSize = shmSize + } + + if i := container.Isolation(r.FormValue("isolation")); i != "" { + if !container.Isolation.IsValid(i) { + return nil, fmt.Errorf("Unsupported isolation: %q", i) + } + options.Isolation = i + } + + var buildUlimits = []*units.Ulimit{} + ulimitsJSON := r.FormValue("ulimits") + if ulimitsJSON != "" { + if err := json.NewDecoder(strings.NewReader(ulimitsJSON)).Decode(&buildUlimits); err != nil { + return nil, err + } + options.Ulimits = buildUlimits + } + + var buildArgs = map[string]string{} + buildArgsJSON := r.FormValue("buildargs") + if buildArgsJSON != "" { + if err := json.NewDecoder(strings.NewReader(buildArgsJSON)).Decode(&buildArgs); err != nil { + return nil, err + } + options.BuildArgs = buildArgs + } + var labels = map[string]string{} + labelsJSON := r.FormValue("labels") + if labelsJSON != "" { + if err := json.NewDecoder(strings.NewReader(labelsJSON)).Decode(&labels); err != nil { + return nil, err + } + options.Labels = labels + } + + return options, nil +} + +type syncWriter struct { + w io.Writer + mu sync.Mutex +} + +func (s *syncWriter) Write(b []byte) (count int, err error) { + s.mu.Lock() + count, err = s.w.Write(b) + s.mu.Unlock() + return +} + +func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + var ( + authConfigs = map[string]types.AuthConfig{} + authConfigsEncoded = r.Header.Get("X-Registry-Config") + notVerboseBuffer = bytes.NewBuffer(nil) + ) + + if authConfigsEncoded != "" { + authConfigsJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authConfigsEncoded)) + if err := json.NewDecoder(authConfigsJSON).Decode(&authConfigs); err != nil { + // for a pull it is not an error if no auth was given + // to increase compatibility with the existing api it is defaulting + // to be empty. + } + } + + w.Header().Set("Content-Type", "application/json") + + output := ioutils.NewWriteFlusher(w) + defer output.Close() + sf := streamformatter.NewJSONStreamFormatter() + errf := func(err error) error { + if httputils.BoolValue(r, "q") && notVerboseBuffer.Len() > 0 { + output.Write(notVerboseBuffer.Bytes()) + } + // Do not write the error in the http output if it's still empty. + // This prevents from writing a 200(OK) when there is an internal error. + if !output.Flushed() { + return err + } + _, err = w.Write(sf.FormatError(err)) + if err != nil { + logrus.Warnf("could not write error response: %v", err) + } + return nil + } + + buildOptions, err := newImageBuildOptions(ctx, r) + if err != nil { + return errf(err) + } + + remoteURL := r.FormValue("remote") + + // Currently, only used if context is from a remote url. + // Look at code in DetectContextFromRemoteURL for more information. + createProgressReader := func(in io.ReadCloser) io.ReadCloser { + progressOutput := sf.NewProgressOutput(output, true) + if buildOptions.SuppressOutput { + progressOutput = sf.NewProgressOutput(notVerboseBuffer, true) + } + return progress.NewProgressReader(in, progressOutput, r.ContentLength, "Downloading context", remoteURL) + } + + var ( + context builder.ModifiableContext + dockerfileName string + out io.Writer + ) + context, dockerfileName, err = builder.DetectContextFromRemoteURL(r.Body, remoteURL, createProgressReader) + if err != nil { + return errf(err) + } + defer func() { + if err := context.Close(); err != nil { + logrus.Debugf("[BUILDER] failed to remove temporary context: %v", err) + } + }() + if len(dockerfileName) > 0 { + buildOptions.Dockerfile = dockerfileName + } + + buildOptions.AuthConfigs = authConfigs + + out = output + if buildOptions.SuppressOutput { + out = notVerboseBuffer + } + out = &syncWriter{w: out} + stdout := &streamformatter.StdoutFormatter{Writer: out, StreamFormatter: sf} + stderr := &streamformatter.StderrFormatter{Writer: out, StreamFormatter: sf} + + closeNotifier := make(<-chan bool) + if notifier, ok := w.(http.CloseNotifier); ok { + closeNotifier = notifier.CloseNotify() + } + + imgID, err := br.backend.Build(ctx, buildOptions, + builder.DockerIgnoreContext{ModifiableContext: context}, + stdout, stderr, out, + closeNotifier) + if err != nil { + return errf(err) + } + + // 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 buildOptions.SuppressOutput { + stdout := &streamformatter.StdoutFormatter{Writer: output, StreamFormatter: sf} + fmt.Fprintf(stdout, "%s\n", string(imgID)) + } + + return nil +} diff --git a/api/server/router/container/backend.go b/api/server/router/container/backend.go new file mode 100644 index 00000000..bd891975 --- /dev/null +++ b/api/server/router/container/backend.go @@ -0,0 +1,71 @@ +package container + +import ( + "io" + "time" + + "github.com/docker/docker/api/types/backend" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/version" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/container" +) + +// execBackend includes functions to implement to provide exec functionality. +type execBackend interface { + ContainerExecCreate(config *types.ExecConfig) (string, error) + ContainerExecInspect(id string) (*backend.ExecInspect, error) + ContainerExecResize(name string, height, width int) error + ContainerExecStart(name string, stdin io.ReadCloser, stdout io.Writer, stderr io.Writer) error + ExecExists(name string) (bool, error) +} + +// copyBackend includes functions to implement to provide container copy functionality. +type copyBackend interface { + ContainerArchivePath(name string, path string) (content io.ReadCloser, stat *types.ContainerPathStat, err error) + ContainerCopy(name string, res string) (io.ReadCloser, error) + ContainerExport(name string, out io.Writer) error + ContainerExtractToDir(name, path string, noOverwriteDirNonDir bool, content io.Reader) error + ContainerStatPath(name string, path string) (stat *types.ContainerPathStat, err error) +} + +// stateBackend includes functions to implement to provide container state lifecycle functionality. +type stateBackend interface { + ContainerCreate(types.ContainerCreateConfig) (types.ContainerCreateResponse, error) + ContainerKill(name string, sig uint64) error + ContainerPause(name string) error + ContainerRename(oldName, newName string) error + ContainerResize(name string, height, width int) error + ContainerRestart(name string, seconds int) error + ContainerRm(name string, config *types.ContainerRmConfig) error + ContainerStart(name string, hostConfig *container.HostConfig) error + ContainerStop(name string, seconds int) error + ContainerUnpause(name string) error + ContainerUpdate(name string, hostConfig *container.HostConfig) ([]string, error) + ContainerWait(name string, timeout time.Duration) (int, error) +} + +// monitorBackend includes functions to implement to provide containers monitoring functionality. +type monitorBackend interface { + ContainerChanges(name string) ([]archive.Change, error) + ContainerInspect(name string, size bool, version version.Version) (interface{}, error) + ContainerLogs(name string, config *backend.ContainerLogsConfig, started chan struct{}) error + ContainerStats(name string, config *backend.ContainerStatsConfig) error + ContainerTop(name string, psArgs string) (*types.ContainerProcessList, error) + + Containers(config *types.ContainerListOptions) ([]*types.Container, error) +} + +// attachBackend includes function to implement to provide container attaching functionality. +type attachBackend interface { + ContainerAttach(name string, c *backend.ContainerAttachConfig) error +} + +// Backend is all the methods that need to be implemented to provide container specific functionality. +type Backend interface { + execBackend + copyBackend + stateBackend + monitorBackend + attachBackend +} diff --git a/api/server/router/container/container.go b/api/server/router/container/container.go new file mode 100644 index 00000000..873f13d2 --- /dev/null +++ b/api/server/router/container/container.go @@ -0,0 +1,63 @@ +package container + +import "github.com/docker/docker/api/server/router" + +// containerRouter is a router to talk with the container controller +type containerRouter struct { + backend Backend + routes []router.Route +} + +// NewRouter initializes a new container router +func NewRouter(b Backend) router.Router { + r := &containerRouter{ + backend: b, + } + r.initRoutes() + return r +} + +// Routes returns the available routes to the container controller +func (r *containerRouter) Routes() []router.Route { + return r.routes +} + +// initRoutes initializes the routes in container router +func (r *containerRouter) initRoutes() { + r.routes = []router.Route{ + // HEAD + router.NewHeadRoute("/containers/{name:.*}/archive", r.headContainersArchive), + // GET + router.NewGetRoute("/containers/json", r.getContainersJSON), + router.NewGetRoute("/containers/{name:.*}/export", r.getContainersExport), + router.NewGetRoute("/containers/{name:.*}/changes", r.getContainersChanges), + router.NewGetRoute("/containers/{name:.*}/json", r.getContainersByName), + router.NewGetRoute("/containers/{name:.*}/top", r.getContainersTop), + router.NewGetRoute("/containers/{name:.*}/logs", r.getContainersLogs), + router.NewGetRoute("/containers/{name:.*}/stats", r.getContainersStats), + router.NewGetRoute("/containers/{name:.*}/attach/ws", r.wsContainersAttach), + router.NewGetRoute("/exec/{id:.*}/json", r.getExecByID), + router.NewGetRoute("/containers/{name:.*}/archive", r.getContainersArchive), + // POST + router.NewPostRoute("/containers/create", r.postContainersCreate), + router.NewPostRoute("/containers/{name:.*}/kill", r.postContainersKill), + router.NewPostRoute("/containers/{name:.*}/pause", r.postContainersPause), + router.NewPostRoute("/containers/{name:.*}/unpause", r.postContainersUnpause), + router.NewPostRoute("/containers/{name:.*}/restart", r.postContainersRestart), + router.NewPostRoute("/containers/{name:.*}/start", r.postContainersStart), + router.NewPostRoute("/containers/{name:.*}/stop", r.postContainersStop), + router.NewPostRoute("/containers/{name:.*}/wait", r.postContainersWait), + router.NewPostRoute("/containers/{name:.*}/resize", r.postContainersResize), + router.NewPostRoute("/containers/{name:.*}/attach", r.postContainersAttach), + router.NewPostRoute("/containers/{name:.*}/copy", r.postContainersCopy), + router.NewPostRoute("/containers/{name:.*}/exec", r.postContainerExecCreate), + router.NewPostRoute("/exec/{name:.*}/start", r.postContainerExecStart), + router.NewPostRoute("/exec/{name:.*}/resize", r.postContainerExecResize), + router.NewPostRoute("/containers/{name:.*}/rename", r.postContainerRename), + router.NewPostRoute("/containers/{name:.*}/update", r.postContainerUpdate), + // PUT + router.NewPutRoute("/containers/{name:.*}/archive", r.putContainersArchive), + // DELETE + router.NewDeleteRoute("/containers/{name:.*}", r.deleteContainers), + } +} diff --git a/api/server/router/container/container_routes.go b/api/server/router/container/container_routes.go new file mode 100644 index 00000000..016e00f0 --- /dev/null +++ b/api/server/router/container/container_routes.go @@ -0,0 +1,531 @@ +package container + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "strings" + "syscall" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/server/httputils" + "github.com/docker/docker/api/types/backend" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/signal" + "github.com/docker/docker/pkg/term" + "github.com/docker/docker/runconfig" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/container" + "github.com/docker/engine-api/types/filters" + "golang.org/x/net/context" + "golang.org/x/net/websocket" +) + +func (s *containerRouter) getContainersJSON(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + filter, err := filters.FromParam(r.Form.Get("filters")) + if err != nil { + return err + } + + config := &types.ContainerListOptions{ + All: httputils.BoolValue(r, "all"), + Size: httputils.BoolValue(r, "size"), + Since: r.Form.Get("since"), + Before: r.Form.Get("before"), + Filter: filter, + } + + if tmpLimit := r.Form.Get("limit"); tmpLimit != "" { + limit, err := strconv.Atoi(tmpLimit) + if err != nil { + return err + } + config.Limit = limit + } + + containers, err := s.backend.Containers(config) + if err != nil { + return err + } + + return httputils.WriteJSON(w, http.StatusOK, containers) +} + +func (s *containerRouter) getContainersStats(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + stream := httputils.BoolValueOrDefault(r, "stream", true) + if !stream { + w.Header().Set("Content-Type", "application/json") + } + + var closeNotifier <-chan bool + if notifier, ok := w.(http.CloseNotifier); ok { + closeNotifier = notifier.CloseNotify() + } + + config := &backend.ContainerStatsConfig{ + Stream: stream, + OutStream: w, + Stop: closeNotifier, + Version: string(httputils.VersionFromContext(ctx)), + } + + return s.backend.ContainerStats(vars["name"], config) +} + +func (s *containerRouter) getContainersLogs(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + // Args are validated before the stream starts because when it starts we're + // sending HTTP 200 by writing an empty chunk of data to tell the client that + // daemon is going to stream. By sending this initial HTTP 200 we can't report + // any error after the stream starts (i.e. container not found, wrong parameters) + // with the appropriate status code. + stdout, stderr := httputils.BoolValue(r, "stdout"), httputils.BoolValue(r, "stderr") + if !(stdout || stderr) { + return fmt.Errorf("Bad parameters: you must choose at least one stream") + } + + var closeNotifier <-chan bool + if notifier, ok := w.(http.CloseNotifier); ok { + closeNotifier = notifier.CloseNotify() + } + + containerName := vars["name"] + logsConfig := &backend.ContainerLogsConfig{ + ContainerLogsOptions: types.ContainerLogsOptions{ + Follow: httputils.BoolValue(r, "follow"), + Timestamps: httputils.BoolValue(r, "timestamps"), + Since: r.Form.Get("since"), + Tail: r.Form.Get("tail"), + ShowStdout: stdout, + ShowStderr: stderr, + }, + OutStream: w, + Stop: closeNotifier, + } + + chStarted := make(chan struct{}) + if err := s.backend.ContainerLogs(containerName, logsConfig, chStarted); err != nil { + select { + case <-chStarted: + // The client may be expecting all of the data we're sending to + // be multiplexed, so send it through OutStream, which will + // have been set up to handle that if needed. + fmt.Fprintf(logsConfig.OutStream, "Error running logs job: %v\n", err) + default: + return err + } + } + + return nil +} + +func (s *containerRouter) getContainersExport(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + return s.backend.ContainerExport(vars["name"], w) +} + +func (s *containerRouter) postContainersStart(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + // If contentLength is -1, we can assumed chunked encoding + // or more technically that the length is unknown + // https://golang.org/src/pkg/net/http/request.go#L139 + // net/http otherwise seems to swallow any headers related to chunked encoding + // including r.TransferEncoding + // allow a nil body for backwards compatibility + var hostConfig *container.HostConfig + if r.Body != nil && (r.ContentLength > 0 || r.ContentLength == -1) { + if err := httputils.CheckForJSON(r); err != nil { + return err + } + + c, err := runconfig.DecodeHostConfig(r.Body) + if err != nil { + return err + } + + hostConfig = c + } + + if err := s.backend.ContainerStart(vars["name"], hostConfig); err != nil { + return err + } + w.WriteHeader(http.StatusNoContent) + return nil +} + +func (s *containerRouter) postContainersStop(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + seconds, _ := strconv.Atoi(r.Form.Get("t")) + + if err := s.backend.ContainerStop(vars["name"], seconds); err != nil { + return err + } + w.WriteHeader(http.StatusNoContent) + + return nil +} + +type errContainerIsRunning interface { + ContainerIsRunning() bool +} + +func (s *containerRouter) postContainersKill(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + var sig syscall.Signal + name := vars["name"] + + // If we have a signal, look at it. Otherwise, do nothing + if sigStr := r.Form.Get("signal"); sigStr != "" { + var err error + if sig, err = signal.ParseSignal(sigStr); err != nil { + return err + } + } + + if err := s.backend.ContainerKill(name, uint64(sig)); err != nil { + var isStopped bool + if e, ok := err.(errContainerIsRunning); ok { + isStopped = !e.ContainerIsRunning() + } + + // Return error that's not caused because the container is stopped. + // Return error if the container is not running and the api is >= 1.20 + // to keep backwards compatibility. + version := httputils.VersionFromContext(ctx) + if version.GreaterThanOrEqualTo("1.20") || !isStopped { + return fmt.Errorf("Cannot kill container %s: %v", name, err) + } + } + + w.WriteHeader(http.StatusNoContent) + return nil +} + +func (s *containerRouter) postContainersRestart(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + timeout, _ := strconv.Atoi(r.Form.Get("t")) + + if err := s.backend.ContainerRestart(vars["name"], timeout); err != nil { + return err + } + + w.WriteHeader(http.StatusNoContent) + + return nil +} + +func (s *containerRouter) postContainersPause(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + if err := s.backend.ContainerPause(vars["name"]); err != nil { + return err + } + + w.WriteHeader(http.StatusNoContent) + + return nil +} + +func (s *containerRouter) postContainersUnpause(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + if err := s.backend.ContainerUnpause(vars["name"]); err != nil { + return err + } + + w.WriteHeader(http.StatusNoContent) + + return nil +} + +func (s *containerRouter) postContainersWait(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + status, err := s.backend.ContainerWait(vars["name"], -1*time.Second) + if err != nil { + return err + } + + return httputils.WriteJSON(w, http.StatusOK, &types.ContainerWaitResponse{ + StatusCode: status, + }) +} + +func (s *containerRouter) getContainersChanges(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + changes, err := s.backend.ContainerChanges(vars["name"]) + if err != nil { + return err + } + + return httputils.WriteJSON(w, http.StatusOK, changes) +} + +func (s *containerRouter) getContainersTop(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + procList, err := s.backend.ContainerTop(vars["name"], r.Form.Get("ps_args")) + if err != nil { + return err + } + + return httputils.WriteJSON(w, http.StatusOK, procList) +} + +func (s *containerRouter) postContainerRename(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + name := vars["name"] + newName := r.Form.Get("name") + if err := s.backend.ContainerRename(name, newName); err != nil { + return err + } + w.WriteHeader(http.StatusNoContent) + return nil +} + +func (s *containerRouter) postContainerUpdate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + if err := httputils.CheckForJSON(r); err != nil { + return err + } + + var updateConfig container.UpdateConfig + + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&updateConfig); err != nil { + return err + } + + hostConfig := &container.HostConfig{ + Resources: updateConfig.Resources, + RestartPolicy: updateConfig.RestartPolicy, + } + + name := vars["name"] + warnings, err := s.backend.ContainerUpdate(name, hostConfig) + if err != nil { + return err + } + + return httputils.WriteJSON(w, http.StatusOK, &types.ContainerUpdateResponse{ + Warnings: warnings, + }) +} + +func (s *containerRouter) postContainersCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + if err := httputils.CheckForJSON(r); err != nil { + return err + } + + name := r.Form.Get("name") + + config, hostConfig, networkingConfig, err := runconfig.DecodeContainerConfig(r.Body) + if err != nil { + return err + } + version := httputils.VersionFromContext(ctx) + adjustCPUShares := version.LessThan("1.19") + + ccr, err := s.backend.ContainerCreate(types.ContainerCreateConfig{ + Name: name, + Config: config, + HostConfig: hostConfig, + NetworkingConfig: networkingConfig, + AdjustCPUShares: adjustCPUShares, + }) + if err != nil { + return err + } + + return httputils.WriteJSON(w, http.StatusCreated, ccr) +} + +func (s *containerRouter) deleteContainers(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + name := vars["name"] + config := &types.ContainerRmConfig{ + ForceRemove: httputils.BoolValue(r, "force"), + RemoveVolume: httputils.BoolValue(r, "v"), + RemoveLink: httputils.BoolValue(r, "link"), + } + + if err := s.backend.ContainerRm(name, config); err != nil { + // Force a 404 for the empty string + if strings.Contains(strings.ToLower(err.Error()), "prefix can't be empty") { + return fmt.Errorf("no such container: \"\"") + } + return err + } + + w.WriteHeader(http.StatusNoContent) + + return nil +} + +func (s *containerRouter) postContainersResize(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + height, err := strconv.Atoi(r.Form.Get("h")) + if err != nil { + return err + } + width, err := strconv.Atoi(r.Form.Get("w")) + if err != nil { + return err + } + + return s.backend.ContainerResize(vars["name"], height, width) +} + +func (s *containerRouter) postContainersAttach(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + err := httputils.ParseForm(r) + if err != nil { + return err + } + containerName := vars["name"] + + _, upgrade := r.Header["Upgrade"] + + keys := []byte{} + detachKeys := r.FormValue("detachKeys") + if detachKeys != "" { + keys, err = term.ToBytes(detachKeys) + if err != nil { + logrus.Warnf("Invalid escape keys provided (%s) using default : ctrl-p ctrl-q", detachKeys) + } + } + + hijacker, ok := w.(http.Hijacker) + if !ok { + return fmt.Errorf("error attaching to container %s, hijack connection missing", containerName) + } + + setupStreams := func() (io.ReadCloser, io.Writer, io.Writer, error) { + conn, _, err := hijacker.Hijack() + if err != nil { + return nil, nil, nil, err + } + + // set raw mode + conn.Write([]byte{}) + + if upgrade { + fmt.Fprintf(conn, "HTTP/1.1 101 UPGRADED\r\nContent-Type: application/vnd.docker.raw-stream\r\nConnection: Upgrade\r\nUpgrade: tcp\r\n\r\n") + } else { + fmt.Fprintf(conn, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n") + } + + closer := func() error { + httputils.CloseStreams(conn) + return nil + } + return ioutils.NewReadCloserWrapper(conn, closer), conn, conn, nil + } + + attachConfig := &backend.ContainerAttachConfig{ + GetStreams: setupStreams, + UseStdin: httputils.BoolValue(r, "stdin"), + UseStdout: httputils.BoolValue(r, "stdout"), + UseStderr: httputils.BoolValue(r, "stderr"), + Logs: httputils.BoolValue(r, "logs"), + Stream: httputils.BoolValue(r, "stream"), + DetachKeys: keys, + MuxStreams: true, + } + + return s.backend.ContainerAttach(containerName, attachConfig) +} + +func (s *containerRouter) wsContainersAttach(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + containerName := vars["name"] + + var keys []byte + var err error + detachKeys := r.FormValue("detachKeys") + if detachKeys != "" { + keys, err = term.ToBytes(detachKeys) + if err != nil { + logrus.Warnf("Invalid escape keys provided (%s) using default : ctrl-p ctrl-q", detachKeys) + } + } + + done := make(chan struct{}) + started := make(chan struct{}) + + setupStreams := func() (io.ReadCloser, io.Writer, io.Writer, error) { + wsChan := make(chan *websocket.Conn) + h := func(conn *websocket.Conn) { + wsChan <- conn + <-done + } + + srv := websocket.Server{Handler: h, Handshake: nil} + go func() { + close(started) + srv.ServeHTTP(w, r) + }() + + conn := <-wsChan + return conn, conn, conn, nil + } + + attachConfig := &backend.ContainerAttachConfig{ + GetStreams: setupStreams, + Logs: httputils.BoolValue(r, "logs"), + Stream: httputils.BoolValue(r, "stream"), + DetachKeys: keys, + UseStdin: true, + UseStdout: true, + UseStderr: true, + MuxStreams: false, // TODO: this should be true since it's a single stream for both stdout and stderr + } + + err = s.backend.ContainerAttach(containerName, attachConfig) + close(done) + select { + case <-started: + logrus.Errorf("Error attaching websocket: %s", err) + return nil + default: + } + return err +} diff --git a/api/server/router/container/copy.go b/api/server/router/container/copy.go new file mode 100644 index 00000000..69584b31 --- /dev/null +++ b/api/server/router/container/copy.go @@ -0,0 +1,112 @@ +package container + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + + "github.com/docker/docker/api/server/httputils" + "github.com/docker/engine-api/types" + "golang.org/x/net/context" +) + +// postContainersCopy is deprecated in favor of getContainersArchive. +func (s *containerRouter) postContainersCopy(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.CheckForJSON(r); err != nil { + return err + } + + cfg := types.CopyConfig{} + if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil { + return err + } + + if cfg.Resource == "" { + return fmt.Errorf("Path cannot be empty") + } + + data, err := s.backend.ContainerCopy(vars["name"], cfg.Resource) + if err != nil { + if strings.Contains(strings.ToLower(err.Error()), "no such container") { + w.WriteHeader(http.StatusNotFound) + return nil + } + if os.IsNotExist(err) { + return fmt.Errorf("Could not find the file %s in container %s", cfg.Resource, vars["name"]) + } + return err + } + defer data.Close() + + w.Header().Set("Content-Type", "application/x-tar") + if _, err := io.Copy(w, data); err != nil { + return err + } + + return nil +} + +// // Encode the stat to JSON, base64 encode, and place in a header. +func setContainerPathStatHeader(stat *types.ContainerPathStat, header http.Header) error { + statJSON, err := json.Marshal(stat) + if err != nil { + return err + } + + header.Set( + "X-Docker-Container-Path-Stat", + base64.StdEncoding.EncodeToString(statJSON), + ) + + return nil +} + +func (s *containerRouter) headContainersArchive(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + v, err := httputils.ArchiveFormValues(r, vars) + if err != nil { + return err + } + + stat, err := s.backend.ContainerStatPath(v.Name, v.Path) + if err != nil { + return err + } + + return setContainerPathStatHeader(stat, w.Header()) +} + +func (s *containerRouter) getContainersArchive(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + v, err := httputils.ArchiveFormValues(r, vars) + if err != nil { + return err + } + + tarArchive, stat, err := s.backend.ContainerArchivePath(v.Name, v.Path) + if err != nil { + return err + } + defer tarArchive.Close() + + if err := setContainerPathStatHeader(stat, w.Header()); err != nil { + return err + } + + w.Header().Set("Content-Type", "application/x-tar") + _, err = io.Copy(w, tarArchive) + + return err +} + +func (s *containerRouter) putContainersArchive(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + v, err := httputils.ArchiveFormValues(r, vars) + if err != nil { + return err + } + + noOverwriteDirNonDir := httputils.BoolValue(r, "noOverwriteDirNonDir") + return s.backend.ContainerExtractToDir(v.Name, v.Path, noOverwriteDirNonDir, r.Body) +} diff --git a/api/server/router/container/exec.go b/api/server/router/container/exec.go new file mode 100644 index 00000000..1a3ddc4c --- /dev/null +++ b/api/server/router/container/exec.go @@ -0,0 +1,134 @@ +package container + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/server/httputils" + "github.com/docker/docker/pkg/stdcopy" + "github.com/docker/engine-api/types" + "golang.org/x/net/context" +) + +func (s *containerRouter) getExecByID(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + eConfig, err := s.backend.ContainerExecInspect(vars["id"]) + if err != nil { + return err + } + + return httputils.WriteJSON(w, http.StatusOK, eConfig) +} + +func (s *containerRouter) postContainerExecCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + if err := httputils.CheckForJSON(r); err != nil { + return err + } + name := vars["name"] + + execConfig := &types.ExecConfig{} + if err := json.NewDecoder(r.Body).Decode(execConfig); err != nil { + return err + } + execConfig.Container = name + + if len(execConfig.Cmd) == 0 { + return fmt.Errorf("No exec command specified") + } + + // Register an instance of Exec in container. + id, err := s.backend.ContainerExecCreate(execConfig) + if err != nil { + logrus.Errorf("Error setting up exec command in container %s: %v", name, err) + return err + } + + return httputils.WriteJSON(w, http.StatusCreated, &types.ContainerExecCreateResponse{ + ID: id, + }) +} + +// TODO(vishh): Refactor the code to avoid having to specify stream config as part of both create and start. +func (s *containerRouter) postContainerExecStart(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + version := httputils.VersionFromContext(ctx) + if version.GreaterThan("1.21") { + if err := httputils.CheckForJSON(r); err != nil { + return err + } + } + + var ( + execName = vars["name"] + stdin, inStream io.ReadCloser + stdout, stderr, outStream io.Writer + ) + + execStartCheck := &types.ExecStartCheck{} + if err := json.NewDecoder(r.Body).Decode(execStartCheck); err != nil { + return err + } + + if exists, err := s.backend.ExecExists(execName); !exists { + return err + } + + if !execStartCheck.Detach { + var err error + // Setting up the streaming http interface. + inStream, outStream, err = httputils.HijackConnection(w) + if err != nil { + return err + } + defer httputils.CloseStreams(inStream, outStream) + + if _, ok := r.Header["Upgrade"]; ok { + fmt.Fprintf(outStream, "HTTP/1.1 101 UPGRADED\r\nContent-Type: application/vnd.docker.raw-stream\r\nConnection: Upgrade\r\nUpgrade: tcp\r\n\r\n") + } else { + fmt.Fprintf(outStream, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n") + } + + stdin = inStream + stdout = outStream + if !execStartCheck.Tty { + stderr = stdcopy.NewStdWriter(outStream, stdcopy.Stderr) + stdout = stdcopy.NewStdWriter(outStream, stdcopy.Stdout) + } + } + + // Now run the user process in container. + if err := s.backend.ContainerExecStart(execName, stdin, stdout, stderr); err != nil { + if execStartCheck.Detach { + return err + } + stdout.Write([]byte(err.Error())) + logrus.Errorf("Error running exec in container: %v\n", err) + return err + } + return nil +} + +func (s *containerRouter) postContainerExecResize(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + height, err := strconv.Atoi(r.Form.Get("h")) + if err != nil { + return err + } + width, err := strconv.Atoi(r.Form.Get("w")) + if err != nil { + return err + } + + return s.backend.ContainerExecResize(vars["name"], height, width) +} diff --git a/api/server/router/container/inspect.go b/api/server/router/container/inspect.go new file mode 100644 index 00000000..dbbced7e --- /dev/null +++ b/api/server/router/container/inspect.go @@ -0,0 +1,21 @@ +package container + +import ( + "net/http" + + "github.com/docker/docker/api/server/httputils" + "golang.org/x/net/context" +) + +// getContainersByName inspects container's configuration and serializes it as json. +func (s *containerRouter) getContainersByName(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + displaySize := httputils.BoolValue(r, "size") + + version := httputils.VersionFromContext(ctx) + json, err := s.backend.ContainerInspect(vars["name"], displaySize, version) + if err != nil { + return err + } + + return httputils.WriteJSON(w, http.StatusOK, json) +} diff --git a/api/server/router/image/backend.go b/api/server/router/image/backend.go new file mode 100644 index 00000000..dfb02a4d --- /dev/null +++ b/api/server/router/image/backend.go @@ -0,0 +1,44 @@ +package image + +import ( + "io" + + "github.com/docker/docker/reference" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/container" + "github.com/docker/engine-api/types/registry" + "golang.org/x/net/context" +) + +// Backend is all the methods that need to be implemented +// to provide image specific functionality. +type Backend interface { + containerBackend + imageBackend + importExportBackend + registryBackend +} + +type containerBackend interface { + Commit(name string, config *types.ContainerCommitConfig) (imageID string, err error) +} + +type imageBackend interface { + ImageDelete(imageRef string, force, prune bool) ([]types.ImageDelete, error) + ImageHistory(imageName string) ([]*types.ImageHistory, error) + Images(filterArgs string, filter string, all bool) ([]*types.Image, error) + LookupImage(name string) (*types.ImageInspect, error) + TagImage(newTag reference.Named, imageName string) error +} + +type importExportBackend interface { + LoadImage(inTar io.ReadCloser, outStream io.Writer, quiet bool) error + ImportImage(src string, newRef reference.Named, msg string, inConfig io.ReadCloser, outStream io.Writer, config *container.Config) error + ExportImage(names []string, outStream io.Writer) error +} + +type registryBackend interface { + PullImage(ctx context.Context, ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error + PushImage(ctx context.Context, ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error + SearchRegistryForImages(ctx context.Context, term string, authConfig *types.AuthConfig, metaHeaders map[string][]string) (*registry.SearchResults, error) +} diff --git a/api/server/router/image/image.go b/api/server/router/image/image.go new file mode 100644 index 00000000..d6a1297a --- /dev/null +++ b/api/server/router/image/image.go @@ -0,0 +1,44 @@ +package image + +import "github.com/docker/docker/api/server/router" + +// imageRouter is a router to talk with the image controller +type imageRouter struct { + backend Backend + routes []router.Route +} + +// NewRouter initializes a new image router +func NewRouter(backend Backend) router.Router { + r := &imageRouter{ + backend: backend, + } + r.initRoutes() + return r +} + +// Routes returns the available routes to the image controller +func (r *imageRouter) Routes() []router.Route { + return r.routes +} + +// initRoutes initializes the routes in the image router +func (r *imageRouter) initRoutes() { + r.routes = []router.Route{ + // GET + router.NewGetRoute("/images/json", r.getImagesJSON), + router.NewGetRoute("/images/search", r.getImagesSearch), + router.NewGetRoute("/images/get", r.getImagesGet), + router.NewGetRoute("/images/{name:.*}/get", r.getImagesGet), + router.NewGetRoute("/images/{name:.*}/history", r.getImagesHistory), + router.NewGetRoute("/images/{name:.*}/json", r.getImagesByName), + // POST + router.NewPostRoute("/commit", r.postCommit), + router.NewPostRoute("/images/create", r.postImagesCreate), + router.NewPostRoute("/images/load", r.postImagesLoad), + router.NewPostRoute("/images/{name:.*}/push", r.postImagesPush), + router.NewPostRoute("/images/{name:.*}/tag", r.postImagesTag), + // DELETE + router.NewDeleteRoute("/images/{name:.*}", r.deleteImages), + } +} diff --git a/api/server/router/image/image_routes.go b/api/server/router/image/image_routes.go new file mode 100644 index 00000000..58abd4b4 --- /dev/null +++ b/api/server/router/image/image_routes.go @@ -0,0 +1,383 @@ +package image + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + + "github.com/docker/distribution/digest" + "github.com/docker/docker/api/server/httputils" + "github.com/docker/docker/builder/dockerfile" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/streamformatter" + "github.com/docker/docker/reference" + "github.com/docker/docker/runconfig" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/container" + "golang.org/x/net/context" +) + +func (s *imageRouter) postCommit(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + if err := httputils.CheckForJSON(r); err != nil { + return err + } + + cname := r.Form.Get("container") + + pause := httputils.BoolValue(r, "pause") + version := httputils.VersionFromContext(ctx) + if r.FormValue("pause") == "" && version.GreaterThanOrEqualTo("1.13") { + pause = true + } + + c, _, _, err := runconfig.DecodeContainerConfig(r.Body) + if err != nil && err != io.EOF { //Do not fail if body is empty. + return err + } + if c == nil { + c = &container.Config{} + } + + newConfig, err := dockerfile.BuildFromConfig(c, r.Form["changes"]) + if err != nil { + return err + } + + commitCfg := &types.ContainerCommitConfig{ + Pause: pause, + Repo: r.Form.Get("repo"), + Tag: r.Form.Get("tag"), + Author: r.Form.Get("author"), + Comment: r.Form.Get("comment"), + Config: newConfig, + MergeConfigs: true, + } + + imgID, err := s.backend.Commit(cname, commitCfg) + if err != nil { + return err + } + + return httputils.WriteJSON(w, http.StatusCreated, &types.ContainerCommitResponse{ + ID: string(imgID), + }) +} + +// Creates an image from Pull or from Import +func (s *imageRouter) postImagesCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + var ( + image = r.Form.Get("fromImage") + repo = r.Form.Get("repo") + tag = r.Form.Get("tag") + message = r.Form.Get("message") + err error + output = ioutils.NewWriteFlusher(w) + ) + defer output.Close() + + w.Header().Set("Content-Type", "application/json") + + if image != "" { //pull + // Special case: "pull -a" may send an image name with a + // trailing :. This is ugly, but let's not break API + // compatibility. + image = strings.TrimSuffix(image, ":") + + var ref reference.Named + ref, err = reference.ParseNamed(image) + if err == nil { + if tag != "" { + // The "tag" could actually be a digest. + var dgst digest.Digest + dgst, err = digest.ParseDigest(tag) + if err == nil { + ref, err = reference.WithDigest(ref, dgst) + } else { + ref, err = reference.WithTag(ref, tag) + } + } + if err == nil { + metaHeaders := map[string][]string{} + for k, v := range r.Header { + if strings.HasPrefix(k, "X-Meta-") { + metaHeaders[k] = v + } + } + + authEncoded := r.Header.Get("X-Registry-Auth") + authConfig := &types.AuthConfig{} + if authEncoded != "" { + authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded)) + if err := json.NewDecoder(authJSON).Decode(authConfig); err != nil { + // for a pull it is not an error if no auth was given + // to increase compatibility with the existing api it is defaulting to be empty + authConfig = &types.AuthConfig{} + } + } + + err = s.backend.PullImage(ctx, ref, metaHeaders, authConfig, output) + } + } + } else { //import + var newRef reference.Named + if repo != "" { + var err error + newRef, err = reference.ParseNamed(repo) + if err != nil { + return err + } + + if _, isCanonical := newRef.(reference.Canonical); isCanonical { + return errors.New("cannot import digest reference") + } + + if tag != "" { + newRef, err = reference.WithTag(newRef, tag) + if err != nil { + return err + } + } + } + + src := r.Form.Get("fromSrc") + + // 'err' MUST NOT be defined within this block, we need any error + // generated from the download to be available to the output + // stream processing below + var newConfig *container.Config + newConfig, err = dockerfile.BuildFromConfig(&container.Config{}, r.Form["changes"]) + if err != nil { + return err + } + + err = s.backend.ImportImage(src, newRef, message, r.Body, output, newConfig) + } + if err != nil { + if !output.Flushed() { + return err + } + sf := streamformatter.NewJSONStreamFormatter() + output.Write(sf.FormatError(err)) + } + + return nil +} + +func (s *imageRouter) postImagesPush(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + metaHeaders := map[string][]string{} + for k, v := range r.Header { + if strings.HasPrefix(k, "X-Meta-") { + metaHeaders[k] = v + } + } + if err := httputils.ParseForm(r); err != nil { + return err + } + authConfig := &types.AuthConfig{} + + authEncoded := r.Header.Get("X-Registry-Auth") + if authEncoded != "" { + // the new format is to handle the authConfig as a header + authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded)) + if err := json.NewDecoder(authJSON).Decode(authConfig); err != nil { + // to increase compatibility to existing api it is defaulting to be empty + authConfig = &types.AuthConfig{} + } + } else { + // the old format is supported for compatibility if there was no authConfig header + if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil { + return fmt.Errorf("Bad parameters and missing X-Registry-Auth: %v", err) + } + } + + ref, err := reference.ParseNamed(vars["name"]) + if err != nil { + return err + } + tag := r.Form.Get("tag") + if tag != "" { + // Push by digest is not supported, so only tags are supported. + ref, err = reference.WithTag(ref, tag) + if err != nil { + return err + } + } + + output := ioutils.NewWriteFlusher(w) + defer output.Close() + + w.Header().Set("Content-Type", "application/json") + + if err := s.backend.PushImage(ctx, ref, metaHeaders, authConfig, output); err != nil { + if !output.Flushed() { + return err + } + sf := streamformatter.NewJSONStreamFormatter() + output.Write(sf.FormatError(err)) + } + return nil +} + +func (s *imageRouter) getImagesGet(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + w.Header().Set("Content-Type", "application/x-tar") + + output := ioutils.NewWriteFlusher(w) + defer output.Close() + var names []string + if name, ok := vars["name"]; ok { + names = []string{name} + } else { + names = r.Form["names"] + } + + if err := s.backend.ExportImage(names, output); err != nil { + if !output.Flushed() { + return err + } + sf := streamformatter.NewJSONStreamFormatter() + output.Write(sf.FormatError(err)) + } + return nil +} + +func (s *imageRouter) postImagesLoad(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + quiet := httputils.BoolValueOrDefault(r, "quiet", true) + + if !quiet { + w.Header().Set("Content-Type", "application/json") + + output := ioutils.NewWriteFlusher(w) + defer output.Close() + if err := s.backend.LoadImage(r.Body, output, quiet); err != nil { + output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err)) + } + return nil + } + return s.backend.LoadImage(r.Body, w, quiet) +} + +func (s *imageRouter) deleteImages(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + name := vars["name"] + + if strings.TrimSpace(name) == "" { + return fmt.Errorf("image name cannot be blank") + } + + force := httputils.BoolValue(r, "force") + prune := !httputils.BoolValue(r, "noprune") + + list, err := s.backend.ImageDelete(name, force, prune) + if err != nil { + return err + } + + return httputils.WriteJSON(w, http.StatusOK, list) +} + +func (s *imageRouter) getImagesByName(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + imageInspect, err := s.backend.LookupImage(vars["name"]) + if err != nil { + return err + } + + return httputils.WriteJSON(w, http.StatusOK, imageInspect) +} + +func (s *imageRouter) getImagesJSON(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + // FIXME: The filter parameter could just be a match filter + images, err := s.backend.Images(r.Form.Get("filters"), r.Form.Get("filter"), httputils.BoolValue(r, "all")) + if err != nil { + return err + } + + return httputils.WriteJSON(w, http.StatusOK, images) +} + +func (s *imageRouter) getImagesHistory(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + name := vars["name"] + history, err := s.backend.ImageHistory(name) + if err != nil { + return err + } + + return httputils.WriteJSON(w, http.StatusOK, history) +} + +func (s *imageRouter) postImagesTag(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + repo := r.Form.Get("repo") + tag := r.Form.Get("tag") + newTag, err := reference.WithName(repo) + if err != nil { + return err + } + if tag != "" { + if newTag, err = reference.WithTag(newTag, tag); err != nil { + return err + } + } + if err := s.backend.TagImage(newTag, vars["name"]); err != nil { + return err + } + w.WriteHeader(http.StatusCreated) + return nil +} + +func (s *imageRouter) getImagesSearch(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + var ( + config *types.AuthConfig + authEncoded = r.Header.Get("X-Registry-Auth") + headers = map[string][]string{} + ) + + if authEncoded != "" { + authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded)) + if err := json.NewDecoder(authJSON).Decode(&config); err != nil { + // for a search it is not an error if no auth was given + // to increase compatibility with the existing api it is defaulting to be empty + config = &types.AuthConfig{} + } + } + for k, v := range r.Header { + if strings.HasPrefix(k, "X-Meta-") { + headers[k] = v + } + } + query, err := s.backend.SearchRegistryForImages(ctx, r.Form.Get("term"), config, headers) + if err != nil { + return err + } + return httputils.WriteJSON(w, http.StatusOK, query.Results) +} diff --git a/api/server/router/local.go b/api/server/router/local.go new file mode 100644 index 00000000..99db4242 --- /dev/null +++ b/api/server/router/local.go @@ -0,0 +1,61 @@ +package router + +import "github.com/docker/docker/api/server/httputils" + +// localRoute defines an individual API route to connect +// with the docker daemon. It implements Route. +type localRoute struct { + method string + path string + handler httputils.APIFunc +} + +// Handler returns the APIFunc to let the server wrap it in middlewares. +func (l localRoute) Handler() httputils.APIFunc { + return l.handler +} + +// Method returns the http method that the route responds to. +func (l localRoute) Method() string { + return l.method +} + +// Path returns the subpath where the route responds to. +func (l localRoute) Path() string { + return l.path +} + +// NewRoute initializes a new local route for the router. +func NewRoute(method, path string, handler httputils.APIFunc) Route { + return localRoute{method, path, handler} +} + +// NewGetRoute initializes a new route with the http method GET. +func NewGetRoute(path string, handler httputils.APIFunc) Route { + return NewRoute("GET", path, handler) +} + +// NewPostRoute initializes a new route with the http method POST. +func NewPostRoute(path string, handler httputils.APIFunc) Route { + return NewRoute("POST", path, handler) +} + +// NewPutRoute initializes a new route with the http method PUT. +func NewPutRoute(path string, handler httputils.APIFunc) Route { + return NewRoute("PUT", path, handler) +} + +// NewDeleteRoute initializes a new route with the http method DELETE. +func NewDeleteRoute(path string, handler httputils.APIFunc) Route { + return NewRoute("DELETE", path, handler) +} + +// NewOptionsRoute initializes a new route with the http method OPTIONS. +func NewOptionsRoute(path string, handler httputils.APIFunc) Route { + return NewRoute("OPTIONS", path, handler) +} + +// NewHeadRoute initializes a new route with the http method HEAD. +func NewHeadRoute(path string, handler httputils.APIFunc) Route { + return NewRoute("HEAD", path, handler) +} diff --git a/api/server/router/network/backend.go b/api/server/router/network/backend.go new file mode 100644 index 00000000..7bbbca4d --- /dev/null +++ b/api/server/router/network/backend.go @@ -0,0 +1,19 @@ +package network + +import ( + "github.com/docker/engine-api/types/network" + "github.com/docker/libnetwork" +) + +// Backend is all the methods that need to be implemented +// to provide network specific functionality. +type Backend interface { + FindNetwork(idName string) (libnetwork.Network, error) + GetNetworkByName(idName string) (libnetwork.Network, error) + GetNetworksByID(partialID string) []libnetwork.Network + GetAllNetworks() []libnetwork.Network + CreateNetwork(name, driver string, ipam network.IPAM, options map[string]string, labels map[string]string, internal bool, enableIPv6 bool) (libnetwork.Network, error) + ConnectContainerToNetwork(containerName, networkName string, endpointConfig *network.EndpointSettings) error + DisconnectContainerFromNetwork(containerName string, network libnetwork.Network, force bool) error + DeleteNetwork(name string) error +} diff --git a/api/server/router/network/filter.go b/api/server/router/network/filter.go new file mode 100644 index 00000000..f1648cc2 --- /dev/null +++ b/api/server/router/network/filter.go @@ -0,0 +1,110 @@ +package network + +import ( + "fmt" + "regexp" + "strings" + + "github.com/docker/docker/runconfig" + "github.com/docker/engine-api/types/filters" + "github.com/docker/libnetwork" +) + +type filterHandler func([]libnetwork.Network, string) ([]libnetwork.Network, error) + +var ( + // supportedFilters predefined some supported filter handler function + supportedFilters = map[string]filterHandler{ + "type": filterNetworkByType, + "name": filterNetworkByName, + "id": filterNetworkByID, + } + + // acceptFilters is an acceptable filter flag list + // generated for validation. e.g. + // acceptedFilters = map[string]bool{ + // "type": true, + // "name": true, + // "id": true, + // } + acceptedFilters = func() map[string]bool { + ret := make(map[string]bool) + for k := range supportedFilters { + ret[k] = true + } + return ret + }() +) + +func filterNetworkByType(nws []libnetwork.Network, netType string) (retNws []libnetwork.Network, err error) { + switch netType { + case "builtin": + for _, nw := range nws { + if runconfig.IsPreDefinedNetwork(nw.Name()) { + retNws = append(retNws, nw) + } + } + case "custom": + for _, nw := range nws { + if !runconfig.IsPreDefinedNetwork(nw.Name()) { + retNws = append(retNws, nw) + } + } + default: + return nil, fmt.Errorf("Invalid filter: 'type'='%s'", netType) + } + return retNws, nil +} + +func filterNetworkByName(nws []libnetwork.Network, name string) (retNws []libnetwork.Network, err error) { + for _, nw := range nws { + // exact match (fast path) + if nw.Name() == name { + retNws = append(retNws, nw) + continue + } + + // regexp match (slow path) + match, err := regexp.MatchString(name, nw.Name()) + if err != nil || !match { + continue + } else { + retNws = append(retNws, nw) + } + } + return retNws, nil +} + +func filterNetworkByID(nws []libnetwork.Network, id string) (retNws []libnetwork.Network, err error) { + for _, nw := range nws { + if strings.HasPrefix(nw.ID(), id) { + retNws = append(retNws, nw) + } + } + return retNws, nil +} + +// filterAllNetworks filters network list according to user specified filter +// and returns user chosen networks +func filterNetworks(nws []libnetwork.Network, filter filters.Args) ([]libnetwork.Network, error) { + // if filter is empty, return original network list + if filter.Len() == 0 { + return nws, nil + } + + var displayNet []libnetwork.Network + for fkey, fhandler := range supportedFilters { + errFilter := filter.WalkValues(fkey, func(fval string) error { + passList, err := fhandler(nws, fval) + if err != nil { + return err + } + displayNet = append(displayNet, passList...) + return nil + }) + if errFilter != nil { + return nil, errFilter + } + } + return displayNet, nil +} diff --git a/api/server/router/network/network.go b/api/server/router/network/network.go new file mode 100644 index 00000000..7c880896 --- /dev/null +++ b/api/server/router/network/network.go @@ -0,0 +1,37 @@ +package network + +import "github.com/docker/docker/api/server/router" + +// networkRouter is a router to talk with the network controller +type networkRouter struct { + backend Backend + routes []router.Route +} + +// NewRouter initializes a new network router +func NewRouter(b Backend) router.Router { + r := &networkRouter{ + backend: b, + } + r.initRoutes() + return r +} + +// Routes returns the available routes to the network controller +func (r *networkRouter) Routes() []router.Route { + return r.routes +} + +func (r *networkRouter) initRoutes() { + r.routes = []router.Route{ + // GET + router.NewGetRoute("/networks", r.getNetworksList), + router.NewGetRoute("/networks/{id:.*}", r.getNetwork), + // POST + router.NewPostRoute("/networks/create", r.postNetworkCreate), + router.NewPostRoute("/networks/{id:.*}/connect", r.postNetworkConnect), + router.NewPostRoute("/networks/{id:.*}/disconnect", r.postNetworkDisconnect), + // DELETE + router.NewDeleteRoute("/networks/{id:.*}", r.deleteNetwork), + } +} diff --git a/api/server/router/network/network_routes.go b/api/server/router/network/network_routes.go new file mode 100644 index 00000000..f052f541 --- /dev/null +++ b/api/server/router/network/network_routes.go @@ -0,0 +1,277 @@ +package network + +import ( + "encoding/json" + "fmt" + "net/http" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/server/httputils" + "github.com/docker/docker/runconfig" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/filters" + "github.com/docker/engine-api/types/network" + "github.com/docker/libnetwork" +) + +func (n *networkRouter) getNetworksList(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + filter := r.Form.Get("filters") + netFilters, err := filters.FromParam(filter) + if err != nil { + return err + } + + if netFilters.Len() != 0 { + if err := netFilters.Validate(acceptedFilters); err != nil { + return err + } + } + + list := []*types.NetworkResource{} + + nwList := n.backend.GetAllNetworks() + displayable, err := filterNetworks(nwList, netFilters) + if err != nil { + return err + } + + for _, nw := range displayable { + list = append(list, buildNetworkResource(nw)) + } + + return httputils.WriteJSON(w, http.StatusOK, list) +} + +func (n *networkRouter) getNetwork(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + nw, err := n.backend.FindNetwork(vars["id"]) + if err != nil { + return err + } + return httputils.WriteJSON(w, http.StatusOK, buildNetworkResource(nw)) +} + +func (n *networkRouter) postNetworkCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + var create types.NetworkCreate + var warning string + + if err := httputils.ParseForm(r); err != nil { + return err + } + + if err := httputils.CheckForJSON(r); err != nil { + return err + } + + if err := json.NewDecoder(r.Body).Decode(&create); err != nil { + return err + } + + if runconfig.IsPreDefinedNetwork(create.Name) { + return httputils.WriteJSON(w, http.StatusForbidden, + fmt.Sprintf("%s is a pre-defined network and cannot be created", create.Name)) + } + + nw, err := n.backend.GetNetworkByName(create.Name) + if _, ok := err.(libnetwork.ErrNoSuchNetwork); err != nil && !ok { + return err + } + if nw != nil { + if create.CheckDuplicate { + return libnetwork.NetworkNameError(create.Name) + } + warning = fmt.Sprintf("Network with name %s (id : %s) already exists", nw.Name(), nw.ID()) + } + + nw, err = n.backend.CreateNetwork(create.Name, create.Driver, create.IPAM, create.Options, create.Labels, create.Internal, create.EnableIPv6) + if err != nil { + return err + } + + return httputils.WriteJSON(w, http.StatusCreated, &types.NetworkCreateResponse{ + ID: nw.ID(), + Warning: warning, + }) +} + +func (n *networkRouter) postNetworkConnect(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + var connect types.NetworkConnect + if err := httputils.ParseForm(r); err != nil { + return err + } + + if err := httputils.CheckForJSON(r); err != nil { + return err + } + + if err := json.NewDecoder(r.Body).Decode(&connect); err != nil { + return err + } + + nw, err := n.backend.FindNetwork(vars["id"]) + if err != nil { + return err + } + + return n.backend.ConnectContainerToNetwork(connect.Container, nw.Name(), connect.EndpointConfig) +} + +func (n *networkRouter) postNetworkDisconnect(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + var disconnect types.NetworkDisconnect + if err := httputils.ParseForm(r); err != nil { + return err + } + + if err := httputils.CheckForJSON(r); err != nil { + return err + } + + if err := json.NewDecoder(r.Body).Decode(&disconnect); err != nil { + return err + } + + nw, err := n.backend.FindNetwork(vars["id"]) + if err != nil { + return err + } + + return n.backend.DisconnectContainerFromNetwork(disconnect.Container, nw, disconnect.Force) +} + +func (n *networkRouter) deleteNetwork(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + if err := n.backend.DeleteNetwork(vars["id"]); err != nil { + return err + } + w.WriteHeader(http.StatusNoContent) + return nil +} + +func buildNetworkResource(nw libnetwork.Network) *types.NetworkResource { + r := &types.NetworkResource{} + if nw == nil { + return r + } + + info := nw.Info() + r.Name = nw.Name() + r.ID = nw.ID() + r.Scope = info.Scope() + r.Driver = nw.Type() + r.EnableIPv6 = info.IPv6Enabled() + r.Internal = info.Internal() + r.Options = info.DriverOptions() + r.Containers = make(map[string]types.EndpointResource) + buildIpamResources(r, info) + r.Internal = info.Internal() + r.Labels = info.Labels() + + epl := nw.Endpoints() + for _, e := range epl { + ei := e.Info() + if ei == nil { + continue + } + sb := ei.Sandbox() + key := "ep-" + e.ID() + if sb != nil { + key = sb.ContainerID() + } + + r.Containers[key] = buildEndpointResource(e) + } + return r +} + +func buildIpamResources(r *types.NetworkResource, nwInfo libnetwork.NetworkInfo) { + id, opts, ipv4conf, ipv6conf := nwInfo.IpamConfig() + + ipv4Info, ipv6Info := nwInfo.IpamInfo() + + r.IPAM.Driver = id + + r.IPAM.Options = opts + + r.IPAM.Config = []network.IPAMConfig{} + for _, ip4 := range ipv4conf { + if ip4.PreferredPool == "" { + continue + } + iData := network.IPAMConfig{} + iData.Subnet = ip4.PreferredPool + iData.IPRange = ip4.SubPool + iData.Gateway = ip4.Gateway + iData.AuxAddress = ip4.AuxAddresses + r.IPAM.Config = append(r.IPAM.Config, iData) + } + + if len(r.IPAM.Config) == 0 { + for _, ip4Info := range ipv4Info { + iData := network.IPAMConfig{} + iData.Subnet = ip4Info.IPAMData.Pool.String() + iData.Gateway = ip4Info.IPAMData.Gateway.String() + r.IPAM.Config = append(r.IPAM.Config, iData) + } + } + + hasIpv6Conf := false + for _, ip6 := range ipv6conf { + if ip6.PreferredPool == "" { + continue + } + hasIpv6Conf = true + iData := network.IPAMConfig{} + iData.Subnet = ip6.PreferredPool + iData.IPRange = ip6.SubPool + iData.Gateway = ip6.Gateway + iData.AuxAddress = ip6.AuxAddresses + r.IPAM.Config = append(r.IPAM.Config, iData) + } + + if !hasIpv6Conf { + for _, ip6Info := range ipv6Info { + iData := network.IPAMConfig{} + iData.Subnet = ip6Info.IPAMData.Pool.String() + iData.Gateway = ip6Info.IPAMData.Gateway.String() + r.IPAM.Config = append(r.IPAM.Config, iData) + } + } +} + +func buildEndpointResource(e libnetwork.Endpoint) types.EndpointResource { + er := types.EndpointResource{} + if e == nil { + return er + } + + er.EndpointID = e.ID() + er.Name = e.Name() + ei := e.Info() + if ei == nil { + return er + } + + if iface := ei.Iface(); iface != nil { + if mac := iface.MacAddress(); mac != nil { + er.MacAddress = mac.String() + } + if ip := iface.Address(); ip != nil && len(ip.IP) > 0 { + er.IPv4Address = ip.String() + } + + if ipv6 := iface.AddressIPv6(); ipv6 != nil && len(ipv6.IP) > 0 { + er.IPv6Address = ipv6.String() + } + } + return er +} diff --git a/api/server/router/router.go b/api/server/router/router.go new file mode 100644 index 00000000..2de25c27 --- /dev/null +++ b/api/server/router/router.go @@ -0,0 +1,19 @@ +package router + +import "github.com/docker/docker/api/server/httputils" + +// Router defines an interface to specify a group of routes to add to the docker server. +type Router interface { + // Routes returns the list of routes to add to the docker server. + Routes() []Route +} + +// Route defines an individual API route in the docker server. +type Route interface { + // Handler returns the raw function to create the http handler. + Handler() httputils.APIFunc + // Method returns the http method that the route responds to. + Method() string + // Path returns the subpath where the route responds to. + Path() string +} diff --git a/api/server/router/system/backend.go b/api/server/router/system/backend.go new file mode 100644 index 00000000..e6284cd4 --- /dev/null +++ b/api/server/router/system/backend.go @@ -0,0 +1,18 @@ +package system + +import ( + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/events" + "github.com/docker/engine-api/types/filters" + "golang.org/x/net/context" +) + +// Backend is the methods that need to be implemented to provide +// system specific functionality. +type Backend interface { + SystemInfo() (*types.Info, error) + SystemVersion() types.Version + SubscribeToEvents(since, sinceNano int64, ef filters.Args) ([]events.Message, chan interface{}) + UnsubscribeFromEvents(chan interface{}) + AuthenticateToRegistry(ctx context.Context, authConfig *types.AuthConfig) (string, string, error) +} diff --git a/api/server/router/system/system.go b/api/server/router/system/system.go new file mode 100644 index 00000000..76da5c52 --- /dev/null +++ b/api/server/router/system/system.go @@ -0,0 +1,33 @@ +package system + +import "github.com/docker/docker/api/server/router" + +// systemRouter provides information about the Docker system overall. +// It gathers information about host, daemon and container events. +type systemRouter struct { + backend Backend + routes []router.Route +} + +// NewRouter initializes a new system router +func NewRouter(b Backend) router.Router { + r := &systemRouter{ + backend: b, + } + + r.routes = []router.Route{ + router.NewOptionsRoute("/{anyroute:.*}", optionsHandler), + router.NewGetRoute("/_ping", pingHandler), + router.NewGetRoute("/events", r.getEvents), + router.NewGetRoute("/info", r.getInfo), + router.NewGetRoute("/version", r.getVersion), + router.NewPostRoute("/auth", r.postAuth), + } + + return r +} + +// Routes returns all the API routes dedicated to the docker system +func (s *systemRouter) Routes() []router.Route { + return s.routes +} diff --git a/api/server/router/system/system_routes.go b/api/server/router/system/system_routes.go new file mode 100644 index 00000000..defaa0d6 --- /dev/null +++ b/api/server/router/system/system_routes.go @@ -0,0 +1,125 @@ +package system + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api" + "github.com/docker/docker/api/server/httputils" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/events" + "github.com/docker/engine-api/types/filters" + timetypes "github.com/docker/engine-api/types/time" + "golang.org/x/net/context" +) + +func optionsHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + w.WriteHeader(http.StatusOK) + return nil +} + +func pingHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + _, err := w.Write([]byte{'O', 'K'}) + return err +} + +func (s *systemRouter) getInfo(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + info, err := s.backend.SystemInfo() + if err != nil { + return err + } + + return httputils.WriteJSON(w, http.StatusOK, info) +} + +func (s *systemRouter) getVersion(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + info := s.backend.SystemVersion() + info.APIVersion = api.DefaultVersion.String() + + return httputils.WriteJSON(w, http.StatusOK, info) +} + +func (s *systemRouter) getEvents(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + since, sinceNano, err := timetypes.ParseTimestamps(r.Form.Get("since"), -1) + if err != nil { + return err + } + until, untilNano, err := timetypes.ParseTimestamps(r.Form.Get("until"), -1) + if err != nil { + return err + } + + var timeout <-chan time.Time + if until > 0 || untilNano > 0 { + dur := time.Unix(until, untilNano).Sub(time.Now()) + timeout = time.NewTimer(dur).C + } + + ef, err := filters.FromParam(r.Form.Get("filters")) + if err != nil { + return err + } + + w.Header().Set("Content-Type", "application/json") + output := ioutils.NewWriteFlusher(w) + defer output.Close() + output.Flush() + + enc := json.NewEncoder(output) + + buffered, l := s.backend.SubscribeToEvents(since, sinceNano, ef) + defer s.backend.UnsubscribeFromEvents(l) + + for _, ev := range buffered { + if err := enc.Encode(ev); err != nil { + return err + } + } + + var closeNotify <-chan bool + if closeNotifier, ok := w.(http.CloseNotifier); ok { + closeNotify = closeNotifier.CloseNotify() + } + + for { + select { + case ev := <-l: + jev, ok := ev.(events.Message) + if !ok { + logrus.Warnf("unexpected event message: %q", ev) + continue + } + if err := enc.Encode(jev); err != nil { + return err + } + case <-timeout: + return nil + case <-closeNotify: + logrus.Debug("Client disconnected, stop sending events") + return nil + } + } +} + +func (s *systemRouter) postAuth(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + var config *types.AuthConfig + err := json.NewDecoder(r.Body).Decode(&config) + r.Body.Close() + if err != nil { + return err + } + status, token, err := s.backend.AuthenticateToRegistry(ctx, config) + if err != nil { + return err + } + return httputils.WriteJSON(w, http.StatusOK, &types.AuthResponse{ + Status: status, + IdentityToken: token, + }) +} diff --git a/api/server/router/volume/backend.go b/api/server/router/volume/backend.go new file mode 100644 index 00000000..fbf5ed27 --- /dev/null +++ b/api/server/router/volume/backend.go @@ -0,0 +1,15 @@ +package volume + +import ( + // TODO return types need to be refactored into pkg + "github.com/docker/engine-api/types" +) + +// Backend is the methods that need to be implemented to provide +// volume specific functionality +type Backend interface { + Volumes(filter string) ([]*types.Volume, []string, error) + VolumeInspect(name string) (*types.Volume, error) + VolumeCreate(name, driverName string, opts, labels map[string]string) (*types.Volume, error) + VolumeRm(name string) error +} diff --git a/api/server/router/volume/volume.go b/api/server/router/volume/volume.go new file mode 100644 index 00000000..2683dcec --- /dev/null +++ b/api/server/router/volume/volume.go @@ -0,0 +1,35 @@ +package volume + +import "github.com/docker/docker/api/server/router" + +// volumeRouter is a router to talk with the volumes controller +type volumeRouter struct { + backend Backend + routes []router.Route +} + +// NewRouter initializes a new volume router +func NewRouter(b Backend) router.Router { + r := &volumeRouter{ + backend: b, + } + r.initRoutes() + return r +} + +// Routes returns the available routes to the volumes controller +func (r *volumeRouter) Routes() []router.Route { + return r.routes +} + +func (r *volumeRouter) initRoutes() { + r.routes = []router.Route{ + // GET + router.NewGetRoute("/volumes", r.getVolumesList), + router.NewGetRoute("/volumes/{name:.*}", r.getVolumeByName), + // POST + router.NewPostRoute("/volumes/create", r.postVolumesCreate), + // DELETE + router.NewDeleteRoute("/volumes/{name:.*}", r.deleteVolumes), + } +} diff --git a/api/server/router/volume/volume_routes.go b/api/server/router/volume/volume_routes.go new file mode 100644 index 00000000..5aa0d4a7 --- /dev/null +++ b/api/server/router/volume/volume_routes.go @@ -0,0 +1,66 @@ +package volume + +import ( + "encoding/json" + "net/http" + + "github.com/docker/docker/api/server/httputils" + "github.com/docker/engine-api/types" + "golang.org/x/net/context" +) + +func (v *volumeRouter) getVolumesList(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + volumes, warnings, err := v.backend.Volumes(r.Form.Get("filters")) + if err != nil { + return err + } + return httputils.WriteJSON(w, http.StatusOK, &types.VolumesListResponse{Volumes: volumes, Warnings: warnings}) +} + +func (v *volumeRouter) getVolumeByName(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + volume, err := v.backend.VolumeInspect(vars["name"]) + if err != nil { + return err + } + return httputils.WriteJSON(w, http.StatusOK, volume) +} + +func (v *volumeRouter) postVolumesCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + if err := httputils.CheckForJSON(r); err != nil { + return err + } + + var req types.VolumeCreateRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return err + } + + volume, err := v.backend.VolumeCreate(req.Name, req.Driver, req.DriverOpts, req.Labels) + if err != nil { + return err + } + return httputils.WriteJSON(w, http.StatusCreated, volume) +} + +func (v *volumeRouter) deleteVolumes(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + if err := v.backend.VolumeRm(vars["name"]); err != nil { + return err + } + w.WriteHeader(http.StatusNoContent) + return nil +} diff --git a/api/server/router_swapper.go b/api/server/router_swapper.go new file mode 100644 index 00000000..1ecc7a7f --- /dev/null +++ b/api/server/router_swapper.go @@ -0,0 +1,30 @@ +package server + +import ( + "net/http" + "sync" + + "github.com/gorilla/mux" +) + +// routerSwapper is an http.Handler that allows you to swap +// mux routers. +type routerSwapper struct { + mu sync.Mutex + router *mux.Router +} + +// Swap changes the old router with the new one. +func (rs *routerSwapper) Swap(newRouter *mux.Router) { + rs.mu.Lock() + rs.router = newRouter + rs.mu.Unlock() +} + +// ServeHTTP makes the routerSwapper to implement the http.Handler interface. +func (rs *routerSwapper) ServeHTTP(w http.ResponseWriter, r *http.Request) { + rs.mu.Lock() + router := rs.router + rs.mu.Unlock() + router.ServeHTTP(w, r) +} diff --git a/api/server/server.go b/api/server/server.go new file mode 100644 index 00000000..1379b737 --- /dev/null +++ b/api/server/server.go @@ -0,0 +1,195 @@ +package server + +import ( + "crypto/tls" + "net" + "net/http" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/server/httputils" + "github.com/docker/docker/api/server/router" + "github.com/docker/docker/pkg/authorization" + "github.com/gorilla/mux" + "golang.org/x/net/context" +) + +// versionMatcher defines a variable matcher to be parsed by the router +// when a request is about to be served. +const versionMatcher = "/v{version:[0-9.]+}" + +// Config provides the configuration for the API server +type Config struct { + Logging bool + EnableCors bool + CorsHeaders string + AuthorizationPluginNames []string + Version string + SocketGroup string + TLSConfig *tls.Config +} + +// Server contains instance details for the server +type Server struct { + cfg *Config + servers []*HTTPServer + routers []router.Router + authZPlugins []authorization.Plugin + routerSwapper *routerSwapper +} + +// New returns a new instance of the server based on the specified configuration. +// It allocates resources which will be needed for ServeAPI(ports, unix-sockets). +func New(cfg *Config) *Server { + return &Server{ + cfg: cfg, + } +} + +// Accept sets a listener the server accepts connections into. +func (s *Server) Accept(addr string, listeners ...net.Listener) { + for _, listener := range listeners { + httpServer := &HTTPServer{ + srv: &http.Server{ + Addr: addr, + }, + l: listener, + } + s.servers = append(s.servers, httpServer) + } +} + +// Close closes servers and thus stop receiving requests +func (s *Server) Close() { + for _, srv := range s.servers { + if err := srv.Close(); err != nil { + logrus.Error(err) + } + } +} + +// serveAPI loops through all initialized servers and spawns goroutine +// with Server method for each. It sets createMux() as Handler also. +func (s *Server) serveAPI() error { + var chErrors = make(chan error, len(s.servers)) + for _, srv := range s.servers { + srv.srv.Handler = s.routerSwapper + go func(srv *HTTPServer) { + var err error + logrus.Infof("API listen on %s", srv.l.Addr()) + if err = srv.Serve(); err != nil && strings.Contains(err.Error(), "use of closed network connection") { + err = nil + } + chErrors <- err + }(srv) + } + + for i := 0; i < len(s.servers); i++ { + err := <-chErrors + if err != nil { + return err + } + } + + return nil +} + +// HTTPServer contains an instance of http server and the listener. +// srv *http.Server, contains configuration to create a http server and a mux router with all api end points. +// l net.Listener, is a TCP or Socket listener that dispatches incoming request to the router. +type HTTPServer struct { + srv *http.Server + l net.Listener +} + +// Serve starts listening for inbound requests. +func (s *HTTPServer) Serve() error { + return s.srv.Serve(s.l) +} + +// Close closes the HTTPServer from listening for the inbound requests. +func (s *HTTPServer) Close() error { + return s.l.Close() +} + +func (s *Server) makeHTTPHandler(handler httputils.APIFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Define the context that we'll pass around to share info + // like the docker-request-id. + // + // The 'context' will be used for global data that should + // apply to all requests. Data that is specific to the + // immediate function being called should still be passed + // as 'args' on the function call. + ctx := context.Background() + handlerFunc := s.handleWithGlobalMiddlewares(handler) + + vars := mux.Vars(r) + if vars == nil { + vars = make(map[string]string) + } + + if err := handlerFunc(ctx, w, r, vars); err != nil { + logrus.Errorf("Handler for %s %s returned error: %v", r.Method, r.URL.Path, err) + httputils.WriteError(w, err) + } + } +} + +// InitRouter initializes the list of routers for the server. +// This method also enables the Go profiler if enableProfiler is true. +func (s *Server) InitRouter(enableProfiler bool, routers ...router.Router) { + for _, r := range routers { + s.routers = append(s.routers, r) + } + + m := s.createMux() + if enableProfiler { + profilerSetup(m) + } + s.routerSwapper = &routerSwapper{ + router: m, + } +} + +// createMux initializes the main router the server uses. +func (s *Server) createMux() *mux.Router { + m := mux.NewRouter() + + logrus.Debugf("Registering routers") + for _, apiRouter := range s.routers { + for _, r := range apiRouter.Routes() { + f := s.makeHTTPHandler(r.Handler()) + + logrus.Debugf("Registering %s, %s", r.Method(), r.Path()) + m.Path(versionMatcher + r.Path()).Methods(r.Method()).Handler(f) + m.Path(r.Path()).Methods(r.Method()).Handler(f) + } + } + + return m +} + +// Wait blocks the server goroutine until it exits. +// It sends an error message if there is any error during +// the API execution. +func (s *Server) Wait(waitChan chan error) { + if err := s.serveAPI(); err != nil { + logrus.Errorf("ServeAPI error: %v", err) + waitChan <- err + return + } + waitChan <- nil +} + +// DisableProfiler reloads the server mux without adding the profiler routes. +func (s *Server) DisableProfiler() { + s.routerSwapper.Swap(s.createMux()) +} + +// EnableProfiler reloads the server mux adding the profiler routes. +func (s *Server) EnableProfiler() { + m := s.createMux() + profilerSetup(m) + s.routerSwapper.Swap(m) +} diff --git a/api/server/server_test.go b/api/server/server_test.go new file mode 100644 index 00000000..f3256c31 --- /dev/null +++ b/api/server/server_test.go @@ -0,0 +1,34 @@ +package server + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/docker/docker/api/server/httputils" + + "golang.org/x/net/context" +) + +func TestMiddlewares(t *testing.T) { + cfg := &Config{} + srv := &Server{ + cfg: cfg, + } + + req, _ := http.NewRequest("GET", "/containers/json", nil) + resp := httptest.NewRecorder() + ctx := context.Background() + + localHandler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if httputils.VersionFromContext(ctx) == "" { + t.Fatalf("Expected version, got empty string") + } + return nil + } + + handlerFunc := srv.handleWithGlobalMiddlewares(localHandler) + if err := handlerFunc(ctx, resp, req, map[string]string{}); err != nil { + t.Fatal(err) + } +} diff --git a/api/types/backend/backend.go b/api/types/backend/backend.go new file mode 100644 index 00000000..ffe9b709 --- /dev/null +++ b/api/types/backend/backend.go @@ -0,0 +1,69 @@ +// Package backend includes types to send information to server backends. +// TODO(calavera): This package is pending of extraction to engine-api +// when the server package is clean of daemon dependencies. +package backend + +import ( + "io" + + "github.com/docker/engine-api/types" +) + +// ContainerAttachConfig holds the streams to use when connecting to a container to view logs. +type ContainerAttachConfig struct { + GetStreams func() (io.ReadCloser, io.Writer, io.Writer, error) + UseStdin bool + UseStdout bool + UseStderr bool + Logs bool + Stream bool + DetachKeys []byte + + // Used to signify that streams are multiplexed and therefore need a StdWriter to encode stdout/sderr messages accordingly. + // TODO @cpuguy83: This shouldn't be needed. It was only added so that http and websocket endpoints can use the same function, and the websocket function was not using a stdwriter prior to this change... + // HOWEVER, the websocket endpoint is using a single stream and SHOULD be encoded with stdout/stderr as is done for HTTP since it is still just a single stream. + // Since such a change is an API change unrelated to the current changeset we'll keep it as is here and change separately. + MuxStreams bool +} + +// ContainerLogsConfig holds configs for logging operations. Exists +// for users of the backend to to pass it a logging configuration. +type ContainerLogsConfig struct { + types.ContainerLogsOptions + OutStream io.Writer + Stop <-chan bool +} + +// ContainerStatsConfig holds information for configuring the runtime +// behavior of a backend.ContainerStats() call. +type ContainerStatsConfig struct { + Stream bool + OutStream io.Writer + Stop <-chan bool + Version string +} + +// ExecInspect holds information about a running process started +// with docker exec. +type ExecInspect struct { + ID string + Running bool + ExitCode *int + ProcessConfig *ExecProcessConfig + OpenStdin bool + OpenStderr bool + OpenStdout bool + CanRemove bool + ContainerID string + DetachKeys []byte +} + +// ExecProcessConfig holds information about the exec process +// running on the host. +type ExecProcessConfig struct { + Tty bool `json:"tty"` + Entrypoint string `json:"entrypoint"` + Arguments []string `json:"arguments"` + Privileged *bool `json:"privileged,omitempty"` + User string `json:"user,omitempty"` +} diff --git a/builder/builder.go b/builder/builder.go new file mode 100644 index 00000000..6f8fdb8d --- /dev/null +++ b/builder/builder.go @@ -0,0 +1,153 @@ +// Package builder defines interfaces for any Docker builder to implement. +// +// Historically, only server-side Dockerfile interpreters existed. +// This package allows for other implementations of Docker builders. +package builder + +import ( + "io" + "os" + "time" + + "github.com/docker/docker/reference" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/container" + "golang.org/x/net/context" +) + +const ( + // DefaultDockerfileName is the Default filename with Docker commands, read by docker build + DefaultDockerfileName string = "Dockerfile" +) + +// Context represents a file system tree. +type Context interface { + // Close allows to signal that the filesystem tree won't be used anymore. + // For Context implementations using a temporary directory, it is recommended to + // delete the temporary directory in Close(). + Close() error + // Stat returns an entry corresponding to path if any. + // It is recommended to return an error if path was not found. + // If path is a symlink it also returns the path to the target file. + Stat(path string) (string, FileInfo, error) + // Open opens path from the context and returns a readable stream of it. + Open(path string) (io.ReadCloser, error) + // Walk walks the tree of the context with the function passed to it. + Walk(root string, walkFn WalkFunc) error +} + +// WalkFunc is the type of the function called for each file or directory visited by Context.Walk(). +type WalkFunc func(path string, fi FileInfo, err error) error + +// ModifiableContext represents a modifiable Context. +// TODO: remove this interface once we can get rid of Remove() +type ModifiableContext interface { + Context + // Remove deletes the entry specified by `path`. + // It is usual for directory entries to delete all its subentries. + Remove(path string) error +} + +// FileInfo extends os.FileInfo to allow retrieving an absolute path to the file. +// TODO: remove this interface once pkg/archive exposes a walk function that Context can use. +type FileInfo interface { + os.FileInfo + Path() string +} + +// PathFileInfo is a convenience struct that implements the FileInfo interface. +type PathFileInfo struct { + os.FileInfo + // FilePath holds the absolute path to the file. + FilePath string + // Name holds the basename for the file. + FileName string +} + +// Path returns the absolute path to the file. +func (fi PathFileInfo) Path() string { + return fi.FilePath +} + +// Name returns the basename of the file. +func (fi PathFileInfo) Name() string { + if fi.FileName != "" { + return fi.FileName + } + return fi.FileInfo.Name() +} + +// Hashed defines an extra method intended for implementations of os.FileInfo. +type Hashed interface { + // Hash returns the hash of a file. + Hash() string + SetHash(string) +} + +// HashedFileInfo is a convenient struct that augments FileInfo with a field. +type HashedFileInfo struct { + FileInfo + // FileHash represents the hash of a file. + FileHash string +} + +// Hash returns the hash of a file. +func (fi HashedFileInfo) Hash() string { + return fi.FileHash +} + +// SetHash sets the hash of a file. +func (fi *HashedFileInfo) SetHash(h string) { + fi.FileHash = h +} + +// Backend abstracts calls to a Docker Daemon. +type Backend interface { + // TODO: use digest reference instead of name + + // GetImage looks up a Docker image referenced by `name`. + GetImageOnBuild(name string) (Image, error) + // Tag an image with newTag + TagImage(newTag reference.Named, imageName string) error + // Pull tells Docker to pull image referenced by `name`. + PullOnBuild(ctx context.Context, name string, authConfigs map[string]types.AuthConfig, output io.Writer) (Image, error) + // ContainerAttach attaches to container. + ContainerAttachRaw(cID string, stdin io.ReadCloser, stdout, stderr io.Writer, stream bool) error + // ContainerCreate creates a new Docker container and returns potential warnings + ContainerCreate(types.ContainerCreateConfig) (types.ContainerCreateResponse, error) + // ContainerRm removes a container specified by `id`. + ContainerRm(name string, config *types.ContainerRmConfig) error + // Commit creates a new Docker image from an existing Docker container. + Commit(string, *types.ContainerCommitConfig) (string, error) + // Kill stops the container execution abruptly. + ContainerKill(containerID string, sig uint64) error + // Start starts a new container + ContainerStart(containerID string, hostConfig *container.HostConfig) error + // ContainerWait stops processing until the given container is stopped. + ContainerWait(containerID string, timeout time.Duration) (int, error) + // ContainerUpdateCmd updates container.Path and container.Args + ContainerUpdateCmdOnBuild(containerID string, cmd []string) error + + // ContainerCopy copies/extracts a source FileInfo to a destination path inside a container + // specified by a container object. + // TODO: make an Extract method instead of passing `decompress` + // TODO: do not pass a FileInfo, instead refactor the archive package to export a Walk function that can be used + // with Context.Walk + //ContainerCopy(name string, res string) (io.ReadCloser, error) + // TODO: use copyBackend api + CopyOnBuild(containerID string, destPath string, src FileInfo, decompress bool) error +} + +// Image represents a Docker image used by the builder. +type Image interface { + ImageID() string + RunConfig() *container.Config +} + +// ImageCache abstracts an image cache store. +// (parent image, child runconfig) -> child image +type ImageCache interface { + // GetCachedImageOnBuild returns a reference to a cached image whose parent equals `parent` + // and runconfig equals `cfg`. A cache miss is expected to return an empty ID and a nil error. + GetCachedImageOnBuild(parentID string, cfg *container.Config) (imageID string, err error) +} diff --git a/builder/context.go b/builder/context.go new file mode 100644 index 00000000..3b5cc2a2 --- /dev/null +++ b/builder/context.go @@ -0,0 +1,260 @@ +package builder + +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" +) + +// 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 fmt.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 fmt.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, "", fmt.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 + } + + // Input should be read as a Dockerfile. + tmpDir, err := ioutil.TempDir("", "docker-build-context-") + if err != nil { + return nil, "", fmt.Errorf("unbale 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 "", "", fmt.Errorf("unable to find 'git': %v", err) + } + if absContextDir, err = gitutils.Clone(gitURL); err != nil { + return "", "", fmt.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, "", fmt.Errorf("unable to download remote context %s: %v", remoteURL, err) + } + progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(out, true) + + // 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 != "" { + if dockerfileName, err = filepath.Abs(dockerfileName); err != nil { + return "", "", fmt.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 "", "", fmt.Errorf("unable to get absolute context directory: %v", 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 "", "", fmt.Errorf("unable to evaluate symlinks in context path: %v", err) + } + } + + stat, err := os.Lstat(absContextDir) + if err != nil { + return "", "", fmt.Errorf("unable to stat context directory %q: %v", absContextDir, err) + } + + if !stat.IsDir() { + return "", "", fmt.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 + } + } + } + + // 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 !isUNC(absDockerfile) { + absDockerfile, err = filepath.EvalSymlinks(absDockerfile) + if err != nil { + return "", "", fmt.Errorf("unable to evaluate symlinks in Dockerfile path: %v", err) + } + } + + if _, err := os.Lstat(absDockerfile); err != nil { + if os.IsNotExist(err) { + return "", "", fmt.Errorf("Cannot locate Dockerfile: %q", absDockerfile) + } + return "", "", fmt.Errorf("unable to stat Dockerfile: %v", err) + } + + if relDockerfile, err = filepath.Rel(absContextDir, absDockerfile); err != nil { + return "", "", fmt.Errorf("unable to get relative Dockerfile path: %v", err) + } + + if strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) { + return "", "", fmt.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/builder/context_unix.go b/builder/context_unix.go new file mode 100644 index 00000000..d1f72e05 --- /dev/null +++ b/builder/context_unix.go @@ -0,0 +1,11 @@ +// +build !windows + +package builder + +import ( + "path/filepath" +) + +func getContextRoot(srcPath string) (string, error) { + return filepath.Join(srcPath, "."), nil +} diff --git a/builder/context_windows.go b/builder/context_windows.go new file mode 100644 index 00000000..b8ba2ba2 --- /dev/null +++ b/builder/context_windows.go @@ -0,0 +1,17 @@ +// +build windows + +package builder + +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/builder/dockerfile/bflag.go b/builder/dockerfile/bflag.go new file mode 100644 index 00000000..c2e6c7da --- /dev/null +++ b/builder/dockerfile/bflag.go @@ -0,0 +1,176 @@ +package dockerfile + +import ( + "fmt" + "strings" +) + +// FlagType is the type of the build flag +type FlagType int + +const ( + boolType FlagType = iota + stringType +) + +// BFlags contains all flags information for the builder +type BFlags struct { + Args []string // actual flags/args from cmd line + flags map[string]*Flag + used map[string]*Flag + Err error +} + +// Flag contains all information for a flag +type Flag struct { + bf *BFlags + name string + flagType FlagType + Value string +} + +// NewBFlags return the new BFlags struct +func NewBFlags() *BFlags { + return &BFlags{ + flags: make(map[string]*Flag), + used: make(map[string]*Flag), + } +} + +// AddBool adds a bool flag to BFlags +// Note, any error will be generated when Parse() is called (see Parse). +func (bf *BFlags) AddBool(name string, def bool) *Flag { + flag := bf.addFlag(name, boolType) + if flag == nil { + return nil + } + if def { + flag.Value = "true" + } else { + flag.Value = "false" + } + return flag +} + +// AddString adds a string flag to BFlags +// Note, any error will be generated when Parse() is called (see Parse). +func (bf *BFlags) AddString(name string, def string) *Flag { + flag := bf.addFlag(name, stringType) + if flag == nil { + return nil + } + flag.Value = def + return flag +} + +// addFlag is a generic func used by the other AddXXX() func +// to add a new flag to the BFlags struct. +// Note, any error will be generated when Parse() is called (see Parse). +func (bf *BFlags) addFlag(name string, flagType FlagType) *Flag { + if _, ok := bf.flags[name]; ok { + bf.Err = fmt.Errorf("Duplicate flag defined: %s", name) + return nil + } + + newFlag := &Flag{ + bf: bf, + name: name, + flagType: flagType, + } + bf.flags[name] = newFlag + + return newFlag +} + +// IsUsed checks if the flag is used +func (fl *Flag) IsUsed() bool { + if _, ok := fl.bf.used[fl.name]; ok { + return true + } + return false +} + +// IsTrue checks if a bool flag is true +func (fl *Flag) IsTrue() bool { + if fl.flagType != boolType { + // Should never get here + panic(fmt.Errorf("Trying to use IsTrue on a non-boolean: %s", fl.name)) + } + return fl.Value == "true" +} + +// Parse parses and checks if the BFlags is valid. +// Any error noticed during the AddXXX() funcs will be generated/returned +// here. We do this because an error during AddXXX() is more like a +// compile time error so it doesn't matter too much when we stop our +// processing as long as we do stop it, so this allows the code +// around AddXXX() to be just: +// defFlag := AddString("description", "") +// w/o needing to add an if-statement around each one. +func (bf *BFlags) Parse() error { + // If there was an error while defining the possible flags + // go ahead and bubble it back up here since we didn't do it + // earlier in the processing + if bf.Err != nil { + return fmt.Errorf("Error setting up flags: %s", bf.Err) + } + + for _, arg := range bf.Args { + if !strings.HasPrefix(arg, "--") { + return fmt.Errorf("Arg should start with -- : %s", arg) + } + + if arg == "--" { + return nil + } + + arg = arg[2:] + value := "" + + index := strings.Index(arg, "=") + if index >= 0 { + value = arg[index+1:] + arg = arg[:index] + } + + flag, ok := bf.flags[arg] + if !ok { + return fmt.Errorf("Unknown flag: %s", arg) + } + + if _, ok = bf.used[arg]; ok { + return fmt.Errorf("Duplicate flag specified: %s", arg) + } + + bf.used[arg] = flag + + switch flag.flagType { + case boolType: + // value == "" is only ok if no "=" was specified + if index >= 0 && value == "" { + return fmt.Errorf("Missing a value on flag: %s", arg) + } + + lower := strings.ToLower(value) + if lower == "" { + flag.Value = "true" + } else if lower == "true" || lower == "false" { + flag.Value = lower + } else { + return fmt.Errorf("Expecting boolean value for flag %s, not: %s", arg, value) + } + + case stringType: + if index < 0 { + return fmt.Errorf("Missing a value on flag: %s", arg) + } + flag.Value = value + + default: + panic(fmt.Errorf("No idea what kind of flag we have! Should never get here!")) + } + + } + + return nil +} diff --git a/builder/dockerfile/bflag_test.go b/builder/dockerfile/bflag_test.go new file mode 100644 index 00000000..65cfcead --- /dev/null +++ b/builder/dockerfile/bflag_test.go @@ -0,0 +1,187 @@ +package dockerfile + +import ( + "testing" +) + +func TestBuilderFlags(t *testing.T) { + var expected string + var err error + + // --- + + bf := NewBFlags() + bf.Args = []string{} + if err := bf.Parse(); err != nil { + t.Fatalf("Test1 of %q was supposed to work: %s", bf.Args, err) + } + + // --- + + bf = NewBFlags() + bf.Args = []string{"--"} + if err := bf.Parse(); err != nil { + t.Fatalf("Test2 of %q was supposed to work: %s", bf.Args, err) + } + + // --- + + bf = NewBFlags() + flStr1 := bf.AddString("str1", "") + flBool1 := bf.AddBool("bool1", false) + bf.Args = []string{} + if err = bf.Parse(); err != nil { + t.Fatalf("Test3 of %q was supposed to work: %s", bf.Args, err) + } + + if flStr1.IsUsed() == true { + t.Fatalf("Test3 - str1 was not used!") + } + if flBool1.IsUsed() == true { + t.Fatalf("Test3 - bool1 was not used!") + } + + // --- + + bf = NewBFlags() + flStr1 = bf.AddString("str1", "HI") + flBool1 = bf.AddBool("bool1", false) + bf.Args = []string{} + + if err = bf.Parse(); err != nil { + t.Fatalf("Test4 of %q was supposed to work: %s", bf.Args, err) + } + + if flStr1.Value != "HI" { + t.Fatalf("Str1 was supposed to default to: HI") + } + if flBool1.IsTrue() { + t.Fatalf("Bool1 was supposed to default to: false") + } + if flStr1.IsUsed() == true { + t.Fatalf("Str1 was not used!") + } + if flBool1.IsUsed() == true { + t.Fatalf("Bool1 was not used!") + } + + // --- + + bf = NewBFlags() + flStr1 = bf.AddString("str1", "HI") + bf.Args = []string{"--str1"} + + if err = bf.Parse(); err == nil { + t.Fatalf("Test %q was supposed to fail", bf.Args) + } + + // --- + + bf = NewBFlags() + flStr1 = bf.AddString("str1", "HI") + bf.Args = []string{"--str1="} + + if err = bf.Parse(); err != nil { + t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) + } + + expected = "" + if flStr1.Value != expected { + t.Fatalf("Str1 (%q) should be: %q", flStr1.Value, expected) + } + + // --- + + bf = NewBFlags() + flStr1 = bf.AddString("str1", "HI") + bf.Args = []string{"--str1=BYE"} + + if err = bf.Parse(); err != nil { + t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) + } + + expected = "BYE" + if flStr1.Value != expected { + t.Fatalf("Str1 (%q) should be: %q", flStr1.Value, expected) + } + + // --- + + bf = NewBFlags() + flBool1 = bf.AddBool("bool1", false) + bf.Args = []string{"--bool1"} + + if err = bf.Parse(); err != nil { + t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) + } + + if !flBool1.IsTrue() { + t.Fatalf("Test-b1 Bool1 was supposed to be true") + } + + // --- + + bf = NewBFlags() + flBool1 = bf.AddBool("bool1", false) + bf.Args = []string{"--bool1=true"} + + if err = bf.Parse(); err != nil { + t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) + } + + if !flBool1.IsTrue() { + t.Fatalf("Test-b2 Bool1 was supposed to be true") + } + + // --- + + bf = NewBFlags() + flBool1 = bf.AddBool("bool1", false) + bf.Args = []string{"--bool1=false"} + + if err = bf.Parse(); err != nil { + t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) + } + + if flBool1.IsTrue() { + t.Fatalf("Test-b3 Bool1 was supposed to be false") + } + + // --- + + bf = NewBFlags() + flBool1 = bf.AddBool("bool1", false) + bf.Args = []string{"--bool1=false1"} + + if err = bf.Parse(); err == nil { + t.Fatalf("Test %q was supposed to fail", bf.Args) + } + + // --- + + bf = NewBFlags() + flBool1 = bf.AddBool("bool1", false) + bf.Args = []string{"--bool2"} + + if err = bf.Parse(); err == nil { + t.Fatalf("Test %q was supposed to fail", bf.Args) + } + + // --- + + bf = NewBFlags() + flStr1 = bf.AddString("str1", "HI") + flBool1 = bf.AddBool("bool1", false) + bf.Args = []string{"--bool1", "--str1=BYE"} + + if err = bf.Parse(); err != nil { + t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) + } + + if flStr1.Value != "BYE" { + t.Fatalf("Teset %s, str1 should be BYE", bf.Args) + } + if !flBool1.IsTrue() { + t.Fatalf("Teset %s, bool1 should be true", bf.Args) + } +} diff --git a/builder/dockerfile/builder.go b/builder/dockerfile/builder.go new file mode 100644 index 00000000..daa70498 --- /dev/null +++ b/builder/dockerfile/builder.go @@ -0,0 +1,326 @@ +package dockerfile + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + "sync" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/builder" + "github.com/docker/docker/builder/dockerfile/parser" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/reference" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/container" + "golang.org/x/net/context" +) + +var validCommitCommands = map[string]bool{ + "cmd": true, + "entrypoint": true, + "env": true, + "expose": true, + "label": true, + "onbuild": true, + "user": true, + "volume": true, + "workdir": true, +} + +// BuiltinAllowedBuildArgs is list of built-in allowed build args +var BuiltinAllowedBuildArgs = map[string]bool{ + "HTTP_PROXY": true, + "http_proxy": true, + "HTTPS_PROXY": true, + "https_proxy": true, + "FTP_PROXY": true, + "ftp_proxy": true, + "NO_PROXY": true, + "no_proxy": true, +} + +// Builder is a Dockerfile builder +// It implements the builder.Backend interface. +type Builder struct { + options *types.ImageBuildOptions + + Stdout io.Writer + Stderr io.Writer + Output io.Writer + + docker builder.Backend + context builder.Context + clientCtx context.Context + + dockerfile *parser.Node + runConfig *container.Config // runconfig for cmd, run, entrypoint etc. + flags *BFlags + tmpContainers map[string]struct{} + image string // imageID + noBaseImage bool + maintainer string + cmdSet bool + disableCommit bool + cacheBusted bool + cancelled chan struct{} + cancelOnce sync.Once + allowedBuildArgs map[string]bool // list of build-time args that are allowed for expansion/substitution and passing to commands in 'run'. + + // TODO: remove once docker.Commit can receive a tag + id string +} + +// BuildManager implements builder.Backend and is shared across all Builder objects. +type BuildManager struct { + backend builder.Backend +} + +// NewBuildManager creates a BuildManager. +func NewBuildManager(b builder.Backend) (bm *BuildManager) { + return &BuildManager{backend: b} +} + +// NewBuilder creates a new Dockerfile builder from an optional dockerfile and a Config. +// If dockerfile is nil, the Dockerfile specified by Config.DockerfileName, +// will be read from the Context passed to Build(). +func NewBuilder(clientCtx context.Context, config *types.ImageBuildOptions, backend builder.Backend, context builder.Context, dockerfile io.ReadCloser) (b *Builder, err error) { + if config == nil { + config = new(types.ImageBuildOptions) + } + if config.BuildArgs == nil { + config.BuildArgs = make(map[string]string) + } + b = &Builder{ + clientCtx: clientCtx, + options: config, + Stdout: os.Stdout, + Stderr: os.Stderr, + docker: backend, + context: context, + runConfig: new(container.Config), + tmpContainers: map[string]struct{}{}, + cancelled: make(chan struct{}), + id: stringid.GenerateNonCryptoID(), + allowedBuildArgs: make(map[string]bool), + } + if dockerfile != nil { + b.dockerfile, err = parser.Parse(dockerfile) + if err != nil { + return nil, err + } + } + + return b, nil +} + +// sanitizeRepoAndTags parses the raw "t" parameter received from the client +// to a slice of repoAndTag. +// It also validates each repoName and tag. +func sanitizeRepoAndTags(names []string) ([]reference.Named, error) { + var ( + repoAndTags []reference.Named + // This map is used for deduplicating the "-t" parameter. + uniqNames = make(map[string]struct{}) + ) + for _, repo := range names { + if repo == "" { + continue + } + + ref, err := reference.ParseNamed(repo) + if err != nil { + return nil, err + } + + ref = reference.WithDefaultTag(ref) + + if _, isCanonical := ref.(reference.Canonical); isCanonical { + return nil, errors.New("build tag cannot contain a digest") + } + + if _, isTagged := ref.(reference.NamedTagged); !isTagged { + ref, err = reference.WithTag(ref, reference.DefaultTag) + if err != nil { + return nil, err + } + } + + nameWithTag := ref.String() + + if _, exists := uniqNames[nameWithTag]; !exists { + uniqNames[nameWithTag] = struct{}{} + repoAndTags = append(repoAndTags, ref) + } + } + return repoAndTags, nil +} + +// Build creates a NewBuilder, which builds the image. +func (bm *BuildManager) Build(clientCtx context.Context, config *types.ImageBuildOptions, context builder.Context, stdout io.Writer, stderr io.Writer, out io.Writer, clientGone <-chan bool) (string, error) { + b, err := NewBuilder(clientCtx, config, bm.backend, context, nil) + if err != nil { + return "", err + } + img, err := b.build(config, context, stdout, stderr, out, clientGone) + return img, err + +} + +// build runs the Dockerfile builder from a context and a docker object that allows to make calls +// to Docker. +// +// This will (barring errors): +// +// * read the dockerfile from context +// * parse the dockerfile if not already parsed +// * walk the AST and execute it by dispatching to handlers. If Remove +// or ForceRemove is set, additional cleanup around containers happens after +// processing. +// * Tag image, if applicable. +// * Print a happy message and return the image ID. +// +func (b *Builder) build(config *types.ImageBuildOptions, context builder.Context, stdout io.Writer, stderr io.Writer, out io.Writer, clientGone <-chan bool) (string, error) { + b.options = config + b.context = context + b.Stdout = stdout + b.Stderr = stderr + b.Output = out + + // If Dockerfile was not parsed yet, extract it from the Context + if b.dockerfile == nil { + if err := b.readDockerfile(); err != nil { + return "", err + } + } + + finished := make(chan struct{}) + defer close(finished) + go func() { + select { + case <-finished: + case <-clientGone: + b.cancelOnce.Do(func() { + close(b.cancelled) + }) + } + + }() + + repoAndTags, err := sanitizeRepoAndTags(config.Tags) + if err != nil { + return "", err + } + + if len(b.options.Labels) > 0 { + line := "LABEL " + for k, v := range b.options.Labels { + line += fmt.Sprintf("%q=%q ", k, v) + } + _, node, err := parser.ParseLine(line) + if err != nil { + return "", err + } + b.dockerfile.Children = append(b.dockerfile.Children, node) + } + + var shortImgID string + for i, n := range b.dockerfile.Children { + select { + case <-b.cancelled: + logrus.Debug("Builder: build cancelled!") + fmt.Fprintf(b.Stdout, "Build cancelled") + return "", fmt.Errorf("Build cancelled") + default: + // Not cancelled yet, keep going... + } + if err := b.dispatch(i, n); err != nil { + if b.options.ForceRemove { + b.clearTmp() + } + return "", err + } + + shortImgID = stringid.TruncateID(b.image) + fmt.Fprintf(b.Stdout, " ---> %s\n", shortImgID) + if b.options.Remove { + b.clearTmp() + } + } + + // check if there are any leftover build-args that were passed but not + // consumed during build. Return an error, if there are any. + leftoverArgs := []string{} + for arg := range b.options.BuildArgs { + if !b.isBuildArgAllowed(arg) { + leftoverArgs = append(leftoverArgs, arg) + } + } + if len(leftoverArgs) > 0 { + return "", fmt.Errorf("One or more build-args %v were not consumed, failing build.", leftoverArgs) + } + + if b.image == "" { + return "", fmt.Errorf("No image was generated. Is your Dockerfile empty?") + } + + for _, rt := range repoAndTags { + if err := b.docker.TagImage(rt, b.image); err != nil { + return "", err + } + } + + fmt.Fprintf(b.Stdout, "Successfully built %s\n", shortImgID) + return b.image, nil +} + +// Cancel cancels an ongoing Dockerfile build. +func (b *Builder) Cancel() { + b.cancelOnce.Do(func() { + close(b.cancelled) + }) +} + +// BuildFromConfig builds directly from `changes`, treating it as if it were the contents of a Dockerfile +// It will: +// - Call parse.Parse() to get an AST root for the concatenated Dockerfile entries. +// - Do build by calling builder.dispatch() to call all entries' handling routines +// +// BuildFromConfig is used by the /commit endpoint, with the changes +// coming from the query parameter of the same name. +// +// TODO: Remove? +func BuildFromConfig(config *container.Config, changes []string) (*container.Config, error) { + ast, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n"))) + if err != nil { + return nil, err + } + + // ensure that the commands are valid + for _, n := range ast.Children { + if !validCommitCommands[n.Value] { + return nil, fmt.Errorf("%s is not a valid change command", n.Value) + } + } + + b, err := NewBuilder(context.Background(), nil, nil, nil, nil) + if err != nil { + return nil, err + } + b.runConfig = config + b.Stdout = ioutil.Discard + b.Stderr = ioutil.Discard + b.disableCommit = true + + for i, n := range ast.Children { + if err := b.dispatch(i, n); err != nil { + return nil, err + } + } + + return b.runConfig, nil +} diff --git a/builder/dockerfile/command/command.go b/builder/dockerfile/command/command.go new file mode 100644 index 00000000..9e1b799d --- /dev/null +++ b/builder/dockerfile/command/command.go @@ -0,0 +1,42 @@ +// Package command contains the set of Dockerfile commands. +package command + +// Define constants for the command strings +const ( + Env = "env" + Label = "label" + Maintainer = "maintainer" + Add = "add" + Copy = "copy" + From = "from" + Onbuild = "onbuild" + Workdir = "workdir" + Run = "run" + Cmd = "cmd" + Entrypoint = "entrypoint" + Expose = "expose" + Volume = "volume" + User = "user" + StopSignal = "stopsignal" + Arg = "arg" +) + +// Commands is list of all Dockerfile commands +var Commands = map[string]struct{}{ + Env: {}, + Label: {}, + Maintainer: {}, + Add: {}, + Copy: {}, + From: {}, + Onbuild: {}, + Workdir: {}, + Run: {}, + Cmd: {}, + Entrypoint: {}, + Expose: {}, + Volume: {}, + User: {}, + StopSignal: {}, + Arg: {}, +} diff --git a/builder/dockerfile/dispatchers.go b/builder/dockerfile/dispatchers.go new file mode 100644 index 00000000..ac7c2b07 --- /dev/null +++ b/builder/dockerfile/dispatchers.go @@ -0,0 +1,639 @@ +package dockerfile + +// This file contains the dispatchers for each command. Note that +// `nullDispatch` is not actually a command, but support for commands we parse +// but do nothing with. +// +// See evaluator.go for a higher level discussion of the whole evaluator +// package. + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "runtime" + "sort" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api" + "github.com/docker/docker/builder" + "github.com/docker/docker/pkg/signal" + "github.com/docker/docker/pkg/system" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/docker/engine-api/types/container" + "github.com/docker/engine-api/types/strslice" + "github.com/docker/go-connections/nat" +) + +// ENV foo bar +// +// Sets the environment variable foo to bar, also makes interpolation +// in the dockerfile available from the next statement on via ${foo}. +// +func env(b *Builder, args []string, attributes map[string]bool, original string) error { + if len(args) == 0 { + return errAtLeastOneArgument("ENV") + } + + if len(args)%2 != 0 { + // should never get here, but just in case + return errTooManyArguments("ENV") + } + + if err := b.flags.Parse(); err != nil { + return err + } + + // TODO/FIXME/NOT USED + // Just here to show how to use the builder flags stuff within the + // context of a builder command. Will remove once we actually add + // a builder command to something! + /* + flBool1 := b.flags.AddBool("bool1", false) + flStr1 := b.flags.AddString("str1", "HI") + + if err := b.flags.Parse(); err != nil { + return err + } + + fmt.Printf("Bool1:%v\n", flBool1) + fmt.Printf("Str1:%v\n", flStr1) + */ + + commitStr := "ENV" + + for j := 0; j < len(args); j++ { + // name ==> args[j] + // value ==> args[j+1] + newVar := args[j] + "=" + args[j+1] + "" + commitStr += " " + newVar + + gotOne := false + for i, envVar := range b.runConfig.Env { + envParts := strings.SplitN(envVar, "=", 2) + if envParts[0] == args[j] { + b.runConfig.Env[i] = newVar + gotOne = true + break + } + } + if !gotOne { + b.runConfig.Env = append(b.runConfig.Env, newVar) + } + j++ + } + + return b.commit("", b.runConfig.Cmd, commitStr) +} + +// MAINTAINER some text +// +// Sets the maintainer metadata. +func maintainer(b *Builder, args []string, attributes map[string]bool, original string) error { + if len(args) != 1 { + return errExactlyOneArgument("MAINTAINER") + } + + if err := b.flags.Parse(); err != nil { + return err + } + + b.maintainer = args[0] + return b.commit("", b.runConfig.Cmd, fmt.Sprintf("MAINTAINER %s", b.maintainer)) +} + +// LABEL some json data describing the image +// +// Sets the Label variable foo to bar, +// +func label(b *Builder, args []string, attributes map[string]bool, original string) error { + if len(args) == 0 { + return errAtLeastOneArgument("LABEL") + } + if len(args)%2 != 0 { + // should never get here, but just in case + return errTooManyArguments("LABEL") + } + + if err := b.flags.Parse(); err != nil { + return err + } + + commitStr := "LABEL" + + if b.runConfig.Labels == nil { + b.runConfig.Labels = map[string]string{} + } + + for j := 0; j < len(args); j++ { + // name ==> args[j] + // value ==> args[j+1] + newVar := args[j] + "=" + args[j+1] + "" + commitStr += " " + newVar + + b.runConfig.Labels[args[j]] = args[j+1] + j++ + } + return b.commit("", b.runConfig.Cmd, commitStr) +} + +// ADD foo /path +// +// Add the file 'foo' to '/path'. Tarball and Remote URL (git, http) handling +// exist here. If you do not wish to have this automatic handling, use COPY. +// +func add(b *Builder, args []string, attributes map[string]bool, original string) error { + if len(args) < 2 { + return errAtLeastOneArgument("ADD") + } + + if err := b.flags.Parse(); err != nil { + return err + } + + return b.runContextCommand(args, true, true, "ADD") +} + +// COPY foo /path +// +// Same as 'ADD' but without the tar and remote url handling. +// +func dispatchCopy(b *Builder, args []string, attributes map[string]bool, original string) error { + if len(args) < 2 { + return errAtLeastOneArgument("COPY") + } + + if err := b.flags.Parse(); err != nil { + return err + } + + return b.runContextCommand(args, false, false, "COPY") +} + +// FROM imagename +// +// This sets the image the dockerfile will build on top of. +// +func from(b *Builder, args []string, attributes map[string]bool, original string) error { + if len(args) != 1 { + return errExactlyOneArgument("FROM") + } + + if err := b.flags.Parse(); err != nil { + return err + } + + name := args[0] + + var ( + image builder.Image + err error + ) + + // Windows cannot support a container with no base image. + if name == api.NoBaseImageSpecifier { + if runtime.GOOS == "windows" { + return fmt.Errorf("Windows does not support FROM scratch") + } + b.image = "" + b.noBaseImage = true + } else { + // TODO: don't use `name`, instead resolve it to a digest + if !b.options.PullParent { + image, err = b.docker.GetImageOnBuild(name) + // TODO: shouldn't we error out if error is different from "not found" ? + } + if image == nil { + image, err = b.docker.PullOnBuild(b.clientCtx, name, b.options.AuthConfigs, b.Output) + if err != nil { + return err + } + } + } + + return b.processImageFrom(image) +} + +// ONBUILD RUN echo yo +// +// ONBUILD triggers run when the image is used in a FROM statement. +// +// ONBUILD handling has a lot of special-case functionality, the heading in +// evaluator.go and comments around dispatch() in the same file explain the +// special cases. search for 'OnBuild' in internals.go for additional special +// cases. +// +func onbuild(b *Builder, args []string, attributes map[string]bool, original string) error { + if len(args) == 0 { + return errAtLeastOneArgument("ONBUILD") + } + + if err := b.flags.Parse(); err != nil { + return err + } + + triggerInstruction := strings.ToUpper(strings.TrimSpace(args[0])) + switch triggerInstruction { + case "ONBUILD": + return fmt.Errorf("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed") + case "MAINTAINER", "FROM": + return fmt.Errorf("%s isn't allowed as an ONBUILD trigger", triggerInstruction) + } + + original = regexp.MustCompile(`(?i)^\s*ONBUILD\s*`).ReplaceAllString(original, "") + + b.runConfig.OnBuild = append(b.runConfig.OnBuild, original) + return b.commit("", b.runConfig.Cmd, fmt.Sprintf("ONBUILD %s", original)) +} + +// WORKDIR /tmp +// +// Set the working directory for future RUN/CMD/etc statements. +// +func workdir(b *Builder, args []string, attributes map[string]bool, original string) error { + if len(args) != 1 { + return errExactlyOneArgument("WORKDIR") + } + + if err := b.flags.Parse(); err != nil { + return err + } + + // This is from the Dockerfile and will not necessarily be in platform + // specific semantics, hence ensure it is converted. + workdir := filepath.FromSlash(args[0]) + + if !system.IsAbs(workdir) { + current := filepath.FromSlash(b.runConfig.WorkingDir) + workdir = filepath.Join(string(os.PathSeparator), current, workdir) + } + + b.runConfig.WorkingDir = workdir + + return b.commit("", b.runConfig.Cmd, fmt.Sprintf("WORKDIR %v", workdir)) +} + +// RUN some command yo +// +// run a command and commit the image. Args are automatically prepended with +// 'sh -c' under linux or 'cmd /S /C' under Windows, in the event there is +// only one argument. The difference in processing: +// +// RUN echo hi # sh -c echo hi (Linux) +// RUN echo hi # cmd /S /C echo hi (Windows) +// RUN [ "echo", "hi" ] # echo hi +// +func run(b *Builder, args []string, attributes map[string]bool, original string) error { + if b.image == "" && !b.noBaseImage { + return fmt.Errorf("Please provide a source image with `from` prior to run") + } + + if err := b.flags.Parse(); err != nil { + return err + } + + args = handleJSONArgs(args, attributes) + + if !attributes["json"] { + if runtime.GOOS != "windows" { + args = append([]string{"/bin/sh", "-c"}, args...) + } else { + args = append([]string{"cmd", "/S", "/C"}, args...) + } + } + + config := &container.Config{ + Cmd: strslice.StrSlice(args), + Image: b.image, + } + + // stash the cmd + cmd := b.runConfig.Cmd + if len(b.runConfig.Entrypoint) == 0 && len(b.runConfig.Cmd) == 0 { + b.runConfig.Cmd = config.Cmd + } + + // stash the config environment + env := b.runConfig.Env + + defer func(cmd strslice.StrSlice) { b.runConfig.Cmd = cmd }(cmd) + defer func(env []string) { b.runConfig.Env = env }(env) + + // derive the net build-time environment for this run. We let config + // environment override the build time environment. + // This means that we take the b.buildArgs list of env vars and remove + // any of those variables that are defined as part of the container. In other + // words, anything in b.Config.Env. What's left is the list of build-time env + // vars that we need to add to each RUN command - note the list could be empty. + // + // We don't persist the build time environment with container's config + // environment, but just sort and prepend it to the command string at time + // of commit. + // This helps with tracing back the image's actual environment at the time + // of RUN, without leaking it to the final image. It also aids cache + // lookup for same image built with same build time environment. + cmdBuildEnv := []string{} + configEnv := runconfigopts.ConvertKVStringsToMap(b.runConfig.Env) + for key, val := range b.options.BuildArgs { + if !b.isBuildArgAllowed(key) { + // skip build-args that are not in allowed list, meaning they have + // not been defined by an "ARG" Dockerfile command yet. + // This is an error condition but only if there is no "ARG" in the entire + // Dockerfile, so we'll generate any necessary errors after we parsed + // the entire file (see 'leftoverArgs' processing in evaluator.go ) + continue + } + if _, ok := configEnv[key]; !ok { + cmdBuildEnv = append(cmdBuildEnv, fmt.Sprintf("%s=%s", key, val)) + } + } + + // derive the command to use for probeCache() and to commit in this container. + // Note that we only do this if there are any build-time env vars. Also, we + // use the special argument "|#" at the start of the args array. This will + // avoid conflicts with any RUN command since commands can not + // start with | (vertical bar). The "#" (number of build envs) is there to + // help ensure proper cache matches. We don't want a RUN command + // that starts with "foo=abc" to be considered part of a build-time env var. + saveCmd := config.Cmd + if len(cmdBuildEnv) > 0 { + sort.Strings(cmdBuildEnv) + tmpEnv := append([]string{fmt.Sprintf("|%d", len(cmdBuildEnv))}, cmdBuildEnv...) + saveCmd = strslice.StrSlice(append(tmpEnv, saveCmd...)) + } + + b.runConfig.Cmd = saveCmd + hit, err := b.probeCache() + if err != nil { + return err + } + if hit { + return nil + } + + // set Cmd manually, this is special case only for Dockerfiles + b.runConfig.Cmd = config.Cmd + // set build-time environment for 'run'. + b.runConfig.Env = append(b.runConfig.Env, cmdBuildEnv...) + // set config as already being escaped, this prevents double escaping on windows + b.runConfig.ArgsEscaped = true + + logrus.Debugf("[BUILDER] Command to be executed: %v", b.runConfig.Cmd) + + cID, err := b.create() + if err != nil { + return err + } + + if err := b.run(cID); err != nil { + return err + } + + // revert to original config environment and set the command string to + // have the build-time env vars in it (if any) so that future cache look-ups + // properly match it. + b.runConfig.Env = env + b.runConfig.Cmd = saveCmd + return b.commit(cID, cmd, "run") +} + +// CMD foo +// +// Set the default command to run in the container (which may be empty). +// Argument handling is the same as RUN. +// +func cmd(b *Builder, args []string, attributes map[string]bool, original string) error { + if err := b.flags.Parse(); err != nil { + return err + } + + cmdSlice := handleJSONArgs(args, attributes) + + if !attributes["json"] { + if runtime.GOOS != "windows" { + cmdSlice = append([]string{"/bin/sh", "-c"}, cmdSlice...) + } else { + cmdSlice = append([]string{"cmd", "/S", "/C"}, cmdSlice...) + } + } + + b.runConfig.Cmd = strslice.StrSlice(cmdSlice) + + if err := b.commit("", b.runConfig.Cmd, fmt.Sprintf("CMD %q", cmdSlice)); err != nil { + return err + } + + if len(args) != 0 { + b.cmdSet = true + } + + return nil +} + +// ENTRYPOINT /usr/sbin/nginx +// +// Set the entrypoint (which defaults to sh -c on linux, or cmd /S /C on Windows) to +// /usr/sbin/nginx. Will accept the CMD as the arguments to /usr/sbin/nginx. +// +// Handles command processing similar to CMD and RUN, only b.runConfig.Entrypoint +// is initialized at NewBuilder time instead of through argument parsing. +// +func entrypoint(b *Builder, args []string, attributes map[string]bool, original string) error { + if err := b.flags.Parse(); err != nil { + return err + } + + parsed := handleJSONArgs(args, attributes) + + switch { + case attributes["json"]: + // ENTRYPOINT ["echo", "hi"] + b.runConfig.Entrypoint = strslice.StrSlice(parsed) + case len(parsed) == 0: + // ENTRYPOINT [] + b.runConfig.Entrypoint = nil + default: + // ENTRYPOINT echo hi + if runtime.GOOS != "windows" { + b.runConfig.Entrypoint = strslice.StrSlice{"/bin/sh", "-c", parsed[0]} + } else { + b.runConfig.Entrypoint = strslice.StrSlice{"cmd", "/S", "/C", parsed[0]} + } + } + + // when setting the entrypoint if a CMD was not explicitly set then + // set the command to nil + if !b.cmdSet { + b.runConfig.Cmd = nil + } + + if err := b.commit("", b.runConfig.Cmd, fmt.Sprintf("ENTRYPOINT %q", b.runConfig.Entrypoint)); err != nil { + return err + } + + return nil +} + +// EXPOSE 6667/tcp 7000/tcp +// +// Expose ports for links and port mappings. This all ends up in +// b.runConfig.ExposedPorts for runconfig. +// +func expose(b *Builder, args []string, attributes map[string]bool, original string) error { + portsTab := args + + if len(args) == 0 { + return errAtLeastOneArgument("EXPOSE") + } + + if err := b.flags.Parse(); err != nil { + return err + } + + if b.runConfig.ExposedPorts == nil { + b.runConfig.ExposedPorts = make(nat.PortSet) + } + + ports, _, err := nat.ParsePortSpecs(portsTab) + if err != nil { + return err + } + + // instead of using ports directly, we build a list of ports and sort it so + // the order is consistent. This prevents cache burst where map ordering + // changes between builds + portList := make([]string, len(ports)) + var i int + for port := range ports { + if _, exists := b.runConfig.ExposedPorts[port]; !exists { + b.runConfig.ExposedPorts[port] = struct{}{} + } + portList[i] = string(port) + i++ + } + sort.Strings(portList) + return b.commit("", b.runConfig.Cmd, fmt.Sprintf("EXPOSE %s", strings.Join(portList, " "))) +} + +// USER foo +// +// Set the user to 'foo' for future commands and when running the +// ENTRYPOINT/CMD at container run time. +// +func user(b *Builder, args []string, attributes map[string]bool, original string) error { + if len(args) != 1 { + return errExactlyOneArgument("USER") + } + + if err := b.flags.Parse(); err != nil { + return err + } + + b.runConfig.User = args[0] + return b.commit("", b.runConfig.Cmd, fmt.Sprintf("USER %v", args)) +} + +// VOLUME /foo +// +// Expose the volume /foo for use. Will also accept the JSON array form. +// +func volume(b *Builder, args []string, attributes map[string]bool, original string) error { + if len(args) == 0 { + return errAtLeastOneArgument("VOLUME") + } + + if err := b.flags.Parse(); err != nil { + return err + } + + if b.runConfig.Volumes == nil { + b.runConfig.Volumes = map[string]struct{}{} + } + for _, v := range args { + v = strings.TrimSpace(v) + if v == "" { + return fmt.Errorf("Volume specified can not be an empty string") + } + b.runConfig.Volumes[v] = struct{}{} + } + if err := b.commit("", b.runConfig.Cmd, fmt.Sprintf("VOLUME %v", args)); err != nil { + return err + } + return nil +} + +// STOPSIGNAL signal +// +// Set the signal that will be used to kill the container. +func stopSignal(b *Builder, args []string, attributes map[string]bool, original string) error { + if len(args) != 1 { + return fmt.Errorf("STOPSIGNAL requires exactly one argument") + } + + sig := args[0] + _, err := signal.ParseSignal(sig) + if err != nil { + return err + } + + b.runConfig.StopSignal = sig + return b.commit("", b.runConfig.Cmd, fmt.Sprintf("STOPSIGNAL %v", args)) +} + +// ARG name[=value] +// +// Adds the variable foo to the trusted list of variables that can be passed +// to builder using the --build-arg flag for expansion/subsitution or passing to 'run'. +// Dockerfile author may optionally set a default value of this variable. +func arg(b *Builder, args []string, attributes map[string]bool, original string) error { + if len(args) != 1 { + return fmt.Errorf("ARG requires exactly one argument definition") + } + + var ( + name string + value string + hasDefault bool + ) + + arg := args[0] + // 'arg' can just be a name or name-value pair. Note that this is different + // from 'env' that handles the split of name and value at the parser level. + // The reason for doing it differently for 'arg' is that we support just + // defining an arg and not assign it a value (while 'env' always expects a + // name-value pair). If possible, it will be good to harmonize the two. + if strings.Contains(arg, "=") { + parts := strings.SplitN(arg, "=", 2) + name = parts[0] + value = parts[1] + hasDefault = true + } else { + name = arg + hasDefault = false + } + // add the arg to allowed list of build-time args from this step on. + b.allowedBuildArgs[name] = true + + // If there is a default value associated with this arg then add it to the + // b.buildArgs if one is not already passed to the builder. The args passed + // to builder override the default value of 'arg'. + if _, ok := b.options.BuildArgs[name]; !ok && hasDefault { + b.options.BuildArgs[name] = value + } + + return b.commit("", b.runConfig.Cmd, fmt.Sprintf("ARG %s", arg)) +} + +func errAtLeastOneArgument(command string) error { + return fmt.Errorf("%s requires at least one argument", command) +} + +func errExactlyOneArgument(command string) error { + return fmt.Errorf("%s requires exactly one argument", command) +} + +func errTooManyArguments(command string) error { + return fmt.Errorf("Bad input to %s, too many arguments", command) +} diff --git a/builder/dockerfile/envVarTest b/builder/dockerfile/envVarTest new file mode 100644 index 00000000..1a7fe975 --- /dev/null +++ b/builder/dockerfile/envVarTest @@ -0,0 +1,112 @@ +hello | hello +he'll'o | hello +he'llo | hello +he\'llo | he'llo +he\\'llo | he\llo +abc\tdef | abctdef +"abc\tdef" | abc\tdef +'abc\tdef' | abc\tdef +hello\ | hello +hello\\ | hello\ +"hello | hello +"hello\" | hello" +"hel'lo" | hel'lo +'hello | hello +'hello\' | hello\ +"''" | '' +$. | $. +$1 | +he$1x | hex +he$.x | he$.x +he$pwd. | he. +he$PWD | he/home +he\$PWD | he$PWD +he\\$PWD | he\/home +he\${} | he${} +he\${}xx | he${}xx +he${} | he +he${}xx | hexx +he${hi} | he +he${hi}xx | hexx +he${PWD} | he/home +he${.} | error +he${XXX:-000}xx | he000xx +he${PWD:-000}xx | he/homexx +he${XXX:-$PWD}xx | he/homexx +he${XXX:-${PWD:-yyy}}xx | he/homexx +he${XXX:-${YYY:-yyy}}xx | heyyyxx +he${XXX:YYY} | error +he${XXX:+${PWD}}xx | hexx +he${PWD:+${XXX}}xx | hexx +he${PWD:+${SHELL}}xx | hebashxx +he${XXX:+000}xx | hexx +he${PWD:+000}xx | he000xx +'he${XX}' | he${XX} +"he${PWD}" | he/home +"he'$PWD'" | he'/home' +"$PWD" | /home +'$PWD' | $PWD +'\$PWD' | \$PWD +'"hello"' | "hello" +he\$PWD | he$PWD +"he\$PWD" | he$PWD +'he\$PWD' | he\$PWD +he${PWD | error +he${PWD:=000}xx | error +he${PWD:+${PWD}:}xx | he/home:xx +he${XXX:-\$PWD:}xx | he$PWD:xx +he${XXX:-\${PWD}z}xx | he${PWDz}xx +안녕하세요 | 안녕하세요 +안'녕'하세요 | 안녕하세요 +안'녕하세요 | 안녕하세요 +안녕\'하세요 | 안녕'하세요 +안\\'녕하세요 | 안\녕하세요 +안녕\t하세요 | 안녕t하세요 +"안녕\t하세요" | 안녕\t하세요 +'안녕\t하세요 | 안녕\t하세요 +안녕하세요\ | 안녕하세요 +안녕하세요\\ | 안녕하세요\ +"안녕하세요 | 안녕하세요 +"안녕하세요\" | 안녕하세요" +"안녕'하세요" | 안녕'하세요 +'안녕하세요 | 안녕하세요 +'안녕하세요\' | 안녕하세요\ +안녕$1x | 안녕x +안녕$.x | 안녕$.x +안녕$pwd. | 안녕. +안녕$PWD | 안녕/home +안녕\$PWD | 안녕$PWD +안녕\\$PWD | 안녕\/home +안녕\${} | 안녕${} +안녕\${}xx | 안녕${}xx +안녕${} | 안녕 +안녕${}xx | 안녕xx +안녕${hi} | 안녕 +안녕${hi}xx | 안녕xx +안녕${PWD} | 안녕/home +안녕${.} | error +안녕${XXX:-000}xx | 안녕000xx +안녕${PWD:-000}xx | 안녕/homexx +안녕${XXX:-$PWD}xx | 안녕/homexx +안녕${XXX:-${PWD:-yyy}}xx | 안녕/homexx +안녕${XXX:-${YYY:-yyy}}xx | 안녕yyyxx +안녕${XXX:YYY} | error +안녕${XXX:+${PWD}}xx | 안녕xx +안녕${PWD:+${XXX}}xx | 안녕xx +안녕${PWD:+${SHELL}}xx | 안녕bashxx +안녕${XXX:+000}xx | 안녕xx +안녕${PWD:+000}xx | 안녕000xx +'안녕${XX}' | 안녕${XX} +"안녕${PWD}" | 안녕/home +"안녕'$PWD'" | 안녕'/home' +'"안녕"' | "안녕" +안녕\$PWD | 안녕$PWD +"안녕\$PWD" | 안녕$PWD +'안녕\$PWD' | 안녕\$PWD +안녕${PWD | error +안녕${PWD:=000}xx | error +안녕${PWD:+${PWD}:}xx | 안녕/home:xx +안녕${XXX:-\$PWD:}xx | 안녕$PWD:xx +안녕${XXX:-\${PWD}z}xx | 안녕${PWDz}xx +$KOREAN | 한국어 +안녕$KOREAN | 안녕한국어 diff --git a/builder/dockerfile/evaluator.go b/builder/dockerfile/evaluator.go new file mode 100644 index 00000000..270e3a4f --- /dev/null +++ b/builder/dockerfile/evaluator.go @@ -0,0 +1,215 @@ +// Package dockerfile is the evaluation step in the Dockerfile parse/evaluate pipeline. +// +// It incorporates a dispatch table based on the parser.Node values (see the +// parser package for more information) that are yielded from the parser itself. +// Calling NewBuilder with the BuildOpts struct can be used to customize the +// experience for execution purposes only. Parsing is controlled in the parser +// package, and this division of responsibility should be respected. +// +// Please see the jump table targets for the actual invocations, most of which +// will call out to the functions in internals.go to deal with their tasks. +// +// ONBUILD is a special case, which is covered in the onbuild() func in +// dispatchers.go. +// +// The evaluator uses the concept of "steps", which are usually each processable +// line in the Dockerfile. Each step is numbered and certain actions are taken +// before and after each step, such as creating an image ID and removing temporary +// containers and images. Note that ONBUILD creates a kinda-sorta "sub run" which +// includes its own set of steps (usually only one of them). +package dockerfile + +import ( + "fmt" + "runtime" + "strings" + + "github.com/docker/docker/builder/dockerfile/command" + "github.com/docker/docker/builder/dockerfile/parser" +) + +// Environment variable interpolation will happen on these statements only. +var replaceEnvAllowed = map[string]bool{ + command.Env: true, + command.Label: true, + command.Add: true, + command.Copy: true, + command.Workdir: true, + command.Expose: true, + command.Volume: true, + command.User: true, + command.StopSignal: true, + command.Arg: true, +} + +// Certain commands are allowed to have their args split into more +// words after env var replacements. Meaning: +// ENV foo="123 456" +// EXPOSE $foo +// should result in the same thing as: +// EXPOSE 123 456 +// and not treat "123 456" as a single word. +// Note that: EXPOSE "$foo" and EXPOSE $foo are not the same thing. +// Quotes will cause it to still be treated as single word. +var allowWordExpansion = map[string]bool{ + command.Expose: true, +} + +var evaluateTable map[string]func(*Builder, []string, map[string]bool, string) error + +func init() { + evaluateTable = map[string]func(*Builder, []string, map[string]bool, string) error{ + command.Env: env, + command.Label: label, + command.Maintainer: maintainer, + command.Add: add, + command.Copy: dispatchCopy, // copy() is a go builtin + command.From: from, + command.Onbuild: onbuild, + command.Workdir: workdir, + command.Run: run, + command.Cmd: cmd, + command.Entrypoint: entrypoint, + command.Expose: expose, + command.Volume: volume, + command.User: user, + command.StopSignal: stopSignal, + command.Arg: arg, + } +} + +// This method is the entrypoint to all statement handling routines. +// +// Almost all nodes will have this structure: +// Child[Node, Node, Node] where Child is from parser.Node.Children and each +// node comes from parser.Node.Next. This forms a "line" with a statement and +// arguments and we process them in this normalized form by hitting +// evaluateTable with the leaf nodes of the command and the Builder object. +// +// ONBUILD is a special case; in this case the parser will emit: +// Child[Node, Child[Node, Node...]] where the first node is the literal +// "onbuild" and the child entrypoint is the command of the ONBUILD statement, +// such as `RUN` in ONBUILD RUN foo. There is special case logic in here to +// deal with that, at least until it becomes more of a general concern with new +// features. +func (b *Builder) dispatch(stepN int, ast *parser.Node) error { + cmd := ast.Value + upperCasedCmd := strings.ToUpper(cmd) + + // To ensure the user is given a decent error message if the platform + // on which the daemon is running does not support a builder command. + if err := platformSupports(strings.ToLower(cmd)); err != nil { + return err + } + + attrs := ast.Attributes + original := ast.Original + flags := ast.Flags + strList := []string{} + msg := fmt.Sprintf("Step %d : %s", stepN+1, upperCasedCmd) + + if len(ast.Flags) > 0 { + msg += " " + strings.Join(ast.Flags, " ") + } + + if cmd == "onbuild" { + if ast.Next == nil { + return fmt.Errorf("ONBUILD requires at least one argument") + } + ast = ast.Next.Children[0] + strList = append(strList, ast.Value) + msg += " " + ast.Value + + if len(ast.Flags) > 0 { + msg += " " + strings.Join(ast.Flags, " ") + } + + } + + // count the number of nodes that we are going to traverse first + // so we can pre-create the argument and message array. This speeds up the + // allocation of those list a lot when they have a lot of arguments + cursor := ast + var n int + for cursor.Next != nil { + cursor = cursor.Next + n++ + } + msgList := make([]string, n) + + var i int + // Append the build-time args to config-environment. + // This allows builder config to override the variables, making the behavior similar to + // a shell script i.e. `ENV foo bar` overrides value of `foo` passed in build + // context. But `ENV foo $foo` will use the value from build context if one + // isn't already been defined by a previous ENV primitive. + // Note, we get this behavior because we know that ProcessWord() will + // stop on the first occurrence of a variable name and not notice + // a subsequent one. So, putting the buildArgs list after the Config.Env + // list, in 'envs', is safe. + envs := b.runConfig.Env + for key, val := range b.options.BuildArgs { + if !b.isBuildArgAllowed(key) { + // skip build-args that are not in allowed list, meaning they have + // not been defined by an "ARG" Dockerfile command yet. + // This is an error condition but only if there is no "ARG" in the entire + // Dockerfile, so we'll generate any necessary errors after we parsed + // the entire file (see 'leftoverArgs' processing in evaluator.go ) + continue + } + envs = append(envs, fmt.Sprintf("%s=%s", key, val)) + } + for ast.Next != nil { + ast = ast.Next + var str string + str = ast.Value + if replaceEnvAllowed[cmd] { + var err error + var words []string + + if allowWordExpansion[cmd] { + words, err = ProcessWords(str, envs) + if err != nil { + return err + } + strList = append(strList, words...) + } else { + str, err = ProcessWord(str, envs) + if err != nil { + return err + } + strList = append(strList, str) + } + } else { + strList = append(strList, str) + } + msgList[i] = ast.Value + i++ + } + + msg += " " + strings.Join(msgList, " ") + fmt.Fprintln(b.Stdout, msg) + + // XXX yes, we skip any cmds that are not valid; the parser should have + // picked these out already. + if f, ok := evaluateTable[cmd]; ok { + b.flags = NewBFlags() + b.flags.Args = flags + return f(b, strList, attrs, original) + } + + return fmt.Errorf("Unknown instruction: %s", upperCasedCmd) +} + +// platformSupports is a short-term function to give users a quality error +// message if a Dockerfile uses a command not supported on the platform. +func platformSupports(command string) error { + if runtime.GOOS != "windows" { + return nil + } + switch command { + case "expose", "user", "stopsignal", "arg": + return fmt.Errorf("The daemon on this platform does not support the command '%s'", command) + } + return nil +} diff --git a/builder/dockerfile/internals.go b/builder/dockerfile/internals.go new file mode 100644 index 00000000..a9be9fdd --- /dev/null +++ b/builder/dockerfile/internals.go @@ -0,0 +1,662 @@ +package dockerfile + +// internals for handling commands. Covers many areas and a lot of +// non-contiguous functionality. Please read the comments. + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "path/filepath" + "runtime" + "sort" + "strings" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/builder" + "github.com/docker/docker/builder/dockerfile/parser" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/httputils" + "github.com/docker/docker/pkg/ioutils" + "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/system" + "github.com/docker/docker/pkg/tarsum" + "github.com/docker/docker/pkg/urlutil" + "github.com/docker/docker/runconfig/opts" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/container" + "github.com/docker/engine-api/types/strslice" +) + +func (b *Builder) commit(id string, autoCmd strslice.StrSlice, comment string) error { + if b.disableCommit { + return nil + } + if b.image == "" && !b.noBaseImage { + return fmt.Errorf("Please provide a source image with `from` prior to commit") + } + b.runConfig.Image = b.image + + if id == "" { + cmd := b.runConfig.Cmd + if runtime.GOOS != "windows" { + b.runConfig.Cmd = strslice.StrSlice{"/bin/sh", "-c", "#(nop) " + comment} + } else { + b.runConfig.Cmd = strslice.StrSlice{"cmd", "/S /C", "REM (nop) " + comment} + } + defer func(cmd strslice.StrSlice) { b.runConfig.Cmd = cmd }(cmd) + + hit, err := b.probeCache() + if err != nil { + return err + } else if hit { + return nil + } + id, err = b.create() + if err != nil { + return err + } + } + + // Note: Actually copy the struct + autoConfig := *b.runConfig + autoConfig.Cmd = autoCmd + + commitCfg := &types.ContainerCommitConfig{ + Author: b.maintainer, + Pause: true, + Config: &autoConfig, + } + + // Commit the container + imageID, err := b.docker.Commit(id, commitCfg) + if err != nil { + return err + } + + b.image = imageID + return nil +} + +type copyInfo struct { + builder.FileInfo + decompress bool +} + +func (b *Builder) runContextCommand(args []string, allowRemote bool, allowLocalDecompression bool, cmdName string) error { + if b.context == nil { + return fmt.Errorf("No context given. Impossible to use %s", cmdName) + } + + if len(args) < 2 { + return fmt.Errorf("Invalid %s format - at least two arguments required", cmdName) + } + + // Work in daemon-specific filepath semantics + dest := filepath.FromSlash(args[len(args)-1]) // last one is always the dest + + b.runConfig.Image = b.image + + var infos []copyInfo + + // Loop through each src file and calculate the info we need to + // do the copy (e.g. hash value if cached). Don't actually do + // the copy until we've looked at all src files + var err error + for _, orig := range args[0 : len(args)-1] { + var fi builder.FileInfo + decompress := allowLocalDecompression + if urlutil.IsURL(orig) { + if !allowRemote { + return fmt.Errorf("Source can't be a URL for %s", cmdName) + } + fi, err = b.download(orig) + if err != nil { + return err + } + defer os.RemoveAll(filepath.Dir(fi.Path())) + decompress = false + infos = append(infos, copyInfo{fi, decompress}) + continue + } + // not a URL + subInfos, err := b.calcCopyInfo(cmdName, orig, allowLocalDecompression, true) + if err != nil { + return err + } + + infos = append(infos, subInfos...) + } + + if len(infos) == 0 { + return fmt.Errorf("No source files were specified") + } + if len(infos) > 1 && !strings.HasSuffix(dest, string(os.PathSeparator)) { + return fmt.Errorf("When using %s with more than one source file, the destination must be a directory and end with a /", cmdName) + } + + // For backwards compat, if there's just one info then use it as the + // cache look-up string, otherwise hash 'em all into one + var srcHash string + var origPaths string + + if len(infos) == 1 { + fi := infos[0].FileInfo + origPaths = fi.Name() + if hfi, ok := fi.(builder.Hashed); ok { + srcHash = hfi.Hash() + } + } else { + var hashs []string + var origs []string + for _, info := range infos { + fi := info.FileInfo + origs = append(origs, fi.Name()) + if hfi, ok := fi.(builder.Hashed); ok { + hashs = append(hashs, hfi.Hash()) + } + } + hasher := sha256.New() + hasher.Write([]byte(strings.Join(hashs, ","))) + srcHash = "multi:" + hex.EncodeToString(hasher.Sum(nil)) + origPaths = strings.Join(origs, " ") + } + + cmd := b.runConfig.Cmd + if runtime.GOOS != "windows" { + b.runConfig.Cmd = strslice.StrSlice{"/bin/sh", "-c", fmt.Sprintf("#(nop) %s %s in %s", cmdName, srcHash, dest)} + } else { + b.runConfig.Cmd = strslice.StrSlice{"cmd", "/S", "/C", fmt.Sprintf("REM (nop) %s %s in %s", cmdName, srcHash, dest)} + } + defer func(cmd strslice.StrSlice) { b.runConfig.Cmd = cmd }(cmd) + + if hit, err := b.probeCache(); err != nil { + return err + } else if hit { + return nil + } + + container, err := b.docker.ContainerCreate(types.ContainerCreateConfig{Config: b.runConfig}) + if err != nil { + return err + } + b.tmpContainers[container.ID] = struct{}{} + + comment := fmt.Sprintf("%s %s in %s", cmdName, origPaths, dest) + + // Twiddle the destination when its a relative path - meaning, make it + // relative to the WORKINGDIR + if !system.IsAbs(dest) { + hasSlash := strings.HasSuffix(dest, string(os.PathSeparator)) + dest = filepath.Join(string(os.PathSeparator), filepath.FromSlash(b.runConfig.WorkingDir), dest) + + // Make sure we preserve any trailing slash + if hasSlash { + dest += string(os.PathSeparator) + } + } + + for _, info := range infos { + if err := b.docker.CopyOnBuild(container.ID, dest, info.FileInfo, info.decompress); err != nil { + return err + } + } + + return b.commit(container.ID, cmd, comment) +} + +func (b *Builder) download(srcURL string) (fi builder.FileInfo, err error) { + // get filename from URL + u, err := url.Parse(srcURL) + if err != nil { + return + } + path := filepath.FromSlash(u.Path) // Ensure in platform semantics + if strings.HasSuffix(path, string(os.PathSeparator)) { + path = path[:len(path)-1] + } + parts := strings.Split(path, string(os.PathSeparator)) + filename := parts[len(parts)-1] + if filename == "" { + err = fmt.Errorf("cannot determine filename from url: %s", u) + return + } + + // Initiate the download + resp, err := httputils.Download(srcURL) + if err != nil { + return + } + + // Prepare file in a tmp dir + tmpDir, err := ioutils.TempDir("", "docker-remote") + if err != nil { + return + } + defer func() { + if err != nil { + os.RemoveAll(tmpDir) + } + }() + tmpFileName := filepath.Join(tmpDir, filename) + tmpFile, err := os.OpenFile(tmpFileName, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) + if err != nil { + return + } + + stdoutFormatter := b.Stdout.(*streamformatter.StdoutFormatter) + progressOutput := stdoutFormatter.StreamFormatter.NewProgressOutput(stdoutFormatter.Writer, true) + progressReader := progress.NewProgressReader(resp.Body, progressOutput, resp.ContentLength, "", "Downloading") + // Download and dump result to tmp file + if _, err = io.Copy(tmpFile, progressReader); err != nil { + tmpFile.Close() + return + } + fmt.Fprintln(b.Stdout) + // ignoring error because the file was already opened successfully + tmpFileSt, err := tmpFile.Stat() + if err != nil { + return + } + tmpFile.Close() + + // Set the mtime to the Last-Modified header value if present + // Otherwise just remove atime and mtime + mTime := time.Time{} + + lastMod := resp.Header.Get("Last-Modified") + if lastMod != "" { + // If we can't parse it then just let it default to 'zero' + // otherwise use the parsed time value + if parsedMTime, err := http.ParseTime(lastMod); err == nil { + mTime = parsedMTime + } + } + + if err = system.Chtimes(tmpFileName, mTime, mTime); err != nil { + return + } + + // Calc the checksum, even if we're using the cache + r, err := archive.Tar(tmpFileName, archive.Uncompressed) + if err != nil { + return + } + tarSum, err := tarsum.NewTarSum(r, true, tarsum.Version1) + if err != nil { + return + } + if _, err = io.Copy(ioutil.Discard, tarSum); err != nil { + return + } + hash := tarSum.Sum(nil) + r.Close() + return &builder.HashedFileInfo{FileInfo: builder.PathFileInfo{FileInfo: tmpFileSt, FilePath: tmpFileName}, FileHash: hash}, nil +} + +func (b *Builder) calcCopyInfo(cmdName, origPath string, allowLocalDecompression, allowWildcards bool) ([]copyInfo, error) { + + // Work in daemon-specific OS filepath semantics + origPath = filepath.FromSlash(origPath) + + if origPath != "" && origPath[0] == os.PathSeparator && len(origPath) > 1 { + origPath = origPath[1:] + } + origPath = strings.TrimPrefix(origPath, "."+string(os.PathSeparator)) + + // Deal with wildcards + if allowWildcards && containsWildcards(origPath) { + var copyInfos []copyInfo + if err := b.context.Walk("", func(path string, info builder.FileInfo, err error) error { + if err != nil { + return err + } + if info.Name() == "" { + // Why are we doing this check? + return nil + } + if match, _ := filepath.Match(origPath, path); !match { + return nil + } + + // Note we set allowWildcards to false in case the name has + // a * in it + subInfos, err := b.calcCopyInfo(cmdName, path, allowLocalDecompression, false) + if err != nil { + return err + } + copyInfos = append(copyInfos, subInfos...) + return nil + }); err != nil { + return nil, err + } + return copyInfos, nil + } + + // Must be a dir or a file + + statPath, fi, err := b.context.Stat(origPath) + if err != nil { + return nil, err + } + + copyInfos := []copyInfo{{FileInfo: fi, decompress: allowLocalDecompression}} + + hfi, handleHash := fi.(builder.Hashed) + if !handleHash { + return copyInfos, nil + } + + // Deal with the single file case + if !fi.IsDir() { + hfi.SetHash("file:" + hfi.Hash()) + return copyInfos, nil + } + // Must be a dir + var subfiles []string + err = b.context.Walk(statPath, func(path string, info builder.FileInfo, err error) error { + if err != nil { + return err + } + // we already checked handleHash above + subfiles = append(subfiles, info.(builder.Hashed).Hash()) + return nil + }) + if err != nil { + return nil, err + } + + sort.Strings(subfiles) + hasher := sha256.New() + hasher.Write([]byte(strings.Join(subfiles, ","))) + hfi.SetHash("dir:" + hex.EncodeToString(hasher.Sum(nil))) + + return copyInfos, nil +} + +func containsWildcards(name string) bool { + for i := 0; i < len(name); i++ { + ch := name[i] + if ch == '\\' { + i++ + } else if ch == '*' || ch == '?' || ch == '[' { + return true + } + } + return false +} + +func (b *Builder) processImageFrom(img builder.Image) error { + if img != nil { + b.image = img.ImageID() + + if img.RunConfig() != nil { + b.runConfig = img.RunConfig() + } + } + + // Check to see if we have a default PATH, note that windows won't + // have one as its set by HCS + if system.DefaultPathEnv != "" { + // Convert the slice of strings that represent the current list + // of env vars into a map so we can see if PATH is already set. + // If its not set then go ahead and give it our default value + configEnv := opts.ConvertKVStringsToMap(b.runConfig.Env) + if _, ok := configEnv["PATH"]; !ok { + b.runConfig.Env = append(b.runConfig.Env, + "PATH="+system.DefaultPathEnv) + } + } + + if img == nil { + // Typically this means they used "FROM scratch" + return nil + } + + // Process ONBUILD triggers if they exist + if nTriggers := len(b.runConfig.OnBuild); nTriggers != 0 { + word := "trigger" + if nTriggers > 1 { + word = "triggers" + } + fmt.Fprintf(b.Stderr, "# Executing %d build %s...\n", nTriggers, word) + } + + // Copy the ONBUILD triggers, and remove them from the config, since the config will be committed. + onBuildTriggers := b.runConfig.OnBuild + b.runConfig.OnBuild = []string{} + + // parse the ONBUILD triggers by invoking the parser + for _, step := range onBuildTriggers { + ast, err := parser.Parse(strings.NewReader(step)) + if err != nil { + return err + } + + for i, n := range ast.Children { + switch strings.ToUpper(n.Value) { + case "ONBUILD": + return fmt.Errorf("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed") + case "MAINTAINER", "FROM": + return fmt.Errorf("%s isn't allowed as an ONBUILD trigger", n.Value) + } + + if err := b.dispatch(i, n); err != nil { + return err + } + } + } + + return nil +} + +// probeCache checks if `b.docker` implements builder.ImageCache and image-caching +// is enabled (`b.UseCache`). +// If so attempts to look up the current `b.image` and `b.runConfig` pair with `b.docker`. +// If an image is found, probeCache returns `(true, nil)`. +// If no image is found, it returns `(false, nil)`. +// If there is any error, it returns `(false, err)`. +func (b *Builder) probeCache() (bool, error) { + c, ok := b.docker.(builder.ImageCache) + if !ok || b.options.NoCache || b.cacheBusted { + return false, nil + } + cache, err := c.GetCachedImageOnBuild(b.image, b.runConfig) + if err != nil { + return false, err + } + if len(cache) == 0 { + logrus.Debugf("[BUILDER] Cache miss: %s", b.runConfig.Cmd) + b.cacheBusted = true + return false, nil + } + + fmt.Fprintf(b.Stdout, " ---> Using cache\n") + logrus.Debugf("[BUILDER] Use cached version: %s", b.runConfig.Cmd) + b.image = string(cache) + + return true, nil +} + +func (b *Builder) create() (string, error) { + if b.image == "" && !b.noBaseImage { + return "", fmt.Errorf("Please provide a source image with `from` prior to run") + } + b.runConfig.Image = b.image + + resources := container.Resources{ + CgroupParent: b.options.CgroupParent, + CPUShares: b.options.CPUShares, + CPUPeriod: b.options.CPUPeriod, + CPUQuota: b.options.CPUQuota, + CpusetCpus: b.options.CPUSetCPUs, + CpusetMems: b.options.CPUSetMems, + Memory: b.options.Memory, + MemorySwap: b.options.MemorySwap, + Ulimits: b.options.Ulimits, + } + + // TODO: why not embed a hostconfig in builder? + hostConfig := &container.HostConfig{ + Isolation: b.options.Isolation, + ShmSize: b.options.ShmSize, + Resources: resources, + } + + config := *b.runConfig + + // Create the container + c, err := b.docker.ContainerCreate(types.ContainerCreateConfig{ + Config: b.runConfig, + HostConfig: hostConfig, + }) + if err != nil { + return "", err + } + for _, warning := range c.Warnings { + fmt.Fprintf(b.Stdout, " ---> [Warning] %s\n", warning) + } + + b.tmpContainers[c.ID] = struct{}{} + fmt.Fprintf(b.Stdout, " ---> Running in %s\n", stringid.TruncateID(c.ID)) + + // override the entry point that may have been picked up from the base image + if err := b.docker.ContainerUpdateCmdOnBuild(c.ID, config.Cmd); err != nil { + return "", err + } + + return c.ID, nil +} + +func (b *Builder) run(cID string) (err error) { + errCh := make(chan error) + go func() { + errCh <- b.docker.ContainerAttachRaw(cID, nil, b.Stdout, b.Stderr, true) + }() + + finished := make(chan struct{}) + defer close(finished) + go func() { + select { + case <-b.cancelled: + logrus.Debugln("Build cancelled, killing and removing container:", cID) + b.docker.ContainerKill(cID, 0) + b.removeContainer(cID) + case <-finished: + } + }() + + if err := b.docker.ContainerStart(cID, nil); err != nil { + return err + } + + // Block on reading output from container, stop on err or chan closed + if err := <-errCh; err != nil { + return err + } + + if ret, _ := b.docker.ContainerWait(cID, -1); ret != 0 { + // TODO: change error type, because jsonmessage.JSONError assumes HTTP + return &jsonmessage.JSONError{ + Message: fmt.Sprintf("The command '%s' returned a non-zero code: %d", strings.Join(b.runConfig.Cmd, " "), ret), + Code: ret, + } + } + + return nil +} + +func (b *Builder) removeContainer(c string) error { + rmConfig := &types.ContainerRmConfig{ + ForceRemove: true, + RemoveVolume: true, + } + if err := b.docker.ContainerRm(c, rmConfig); err != nil { + fmt.Fprintf(b.Stdout, "Error removing intermediate container %s: %v\n", stringid.TruncateID(c), err) + return err + } + return nil +} + +func (b *Builder) clearTmp() { + for c := range b.tmpContainers { + if err := b.removeContainer(c); err != nil { + return + } + delete(b.tmpContainers, c) + fmt.Fprintf(b.Stdout, "Removing intermediate container %s\n", stringid.TruncateID(c)) + } +} + +// readDockerfile reads a Dockerfile from the current context. +func (b *Builder) readDockerfile() error { + // If no -f was specified then look for 'Dockerfile'. If we can't find + // that then look for 'dockerfile'. If neither are found then default + // back to 'Dockerfile' and use that in the error message. + if b.options.Dockerfile == "" { + b.options.Dockerfile = builder.DefaultDockerfileName + if _, _, err := b.context.Stat(b.options.Dockerfile); os.IsNotExist(err) { + lowercase := strings.ToLower(b.options.Dockerfile) + if _, _, err := b.context.Stat(lowercase); err == nil { + b.options.Dockerfile = lowercase + } + } + } + + f, err := b.context.Open(b.options.Dockerfile) + if err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("Cannot locate specified Dockerfile: %s", b.options.Dockerfile) + } + return err + } + if f, ok := f.(*os.File); ok { + // ignoring error because Open already succeeded + fi, err := f.Stat() + if err != nil { + return fmt.Errorf("Unexpected error reading Dockerfile: %v", err) + } + if fi.Size() == 0 { + return fmt.Errorf("The Dockerfile (%s) cannot be empty", b.options.Dockerfile) + } + } + b.dockerfile, err = parser.Parse(f) + f.Close() + if err != nil { + return err + } + + // After the Dockerfile has been parsed, we need to check the .dockerignore + // file for either "Dockerfile" or ".dockerignore", and if either are + // present then erase them from the build context. These files should never + // have been sent from the client but we did send them to make sure that + // we had the Dockerfile to actually parse, and then we also need the + // .dockerignore file to know whether either file should be removed. + // Note that this assumes the Dockerfile has been read into memory and + // is now safe to be removed. + if dockerIgnore, ok := b.context.(builder.DockerIgnoreContext); ok { + dockerIgnore.Process([]string{b.options.Dockerfile}) + } + return nil +} + +// determine if build arg is part of built-in args or user +// defined args in Dockerfile at any point in time. +func (b *Builder) isBuildArgAllowed(arg string) bool { + if _, ok := BuiltinAllowedBuildArgs[arg]; ok { + return true + } + if _, ok := b.allowedBuildArgs[arg]; ok { + return true + } + return false +} diff --git a/builder/dockerfile/parser/dumper/main.go b/builder/dockerfile/parser/dumper/main.go new file mode 100644 index 00000000..8c357f4c --- /dev/null +++ b/builder/dockerfile/parser/dumper/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" + "os" + + "github.com/docker/docker/builder/dockerfile/parser" +) + +func main() { + var f *os.File + var err error + + if len(os.Args) < 2 { + fmt.Println("please supply filename(s)") + os.Exit(1) + } + + for _, fn := range os.Args[1:] { + f, err = os.Open(fn) + if err != nil { + panic(err) + } + + ast, err := parser.Parse(f) + if err != nil { + panic(err) + } else { + fmt.Println(ast.Dump()) + } + } +} diff --git a/builder/dockerfile/parser/json_test.go b/builder/dockerfile/parser/json_test.go new file mode 100644 index 00000000..a256f845 --- /dev/null +++ b/builder/dockerfile/parser/json_test.go @@ -0,0 +1,55 @@ +package parser + +import ( + "testing" +) + +var invalidJSONArraysOfStrings = []string{ + `["a",42,"b"]`, + `["a",123.456,"b"]`, + `["a",{},"b"]`, + `["a",{"c": "d"},"b"]`, + `["a",["c"],"b"]`, + `["a",true,"b"]`, + `["a",false,"b"]`, + `["a",null,"b"]`, +} + +var validJSONArraysOfStrings = map[string][]string{ + `[]`: {}, + `[""]`: {""}, + `["a"]`: {"a"}, + `["a","b"]`: {"a", "b"}, + `[ "a", "b" ]`: {"a", "b"}, + `[ "a", "b" ]`: {"a", "b"}, + ` [ "a", "b" ] `: {"a", "b"}, + `["abc 123", "♥", "☃", "\" \\ \/ \b \f \n \r \t \u0000"]`: {"abc 123", "♥", "☃", "\" \\ / \b \f \n \r \t \u0000"}, +} + +func TestJSONArraysOfStrings(t *testing.T) { + for json, expected := range validJSONArraysOfStrings { + if node, _, err := parseJSON(json); err != nil { + t.Fatalf("%q should be a valid JSON array of strings, but wasn't! (err: %q)", json, err) + } else { + i := 0 + for node != nil { + if i >= len(expected) { + t.Fatalf("expected result is shorter than parsed result (%d vs %d+) in %q", len(expected), i+1, json) + } + if node.Value != expected[i] { + t.Fatalf("expected %q (not %q) in %q at pos %d", expected[i], node.Value, json, i) + } + node = node.Next + i++ + } + if i != len(expected) { + t.Fatalf("expected result is longer than parsed result (%d vs %d) in %q", len(expected), i+1, json) + } + } + } + for _, json := range invalidJSONArraysOfStrings { + if _, _, err := parseJSON(json); err != errDockerfileNotStringArray { + t.Fatalf("%q should be an invalid JSON array of strings, but wasn't!", json) + } + } +} diff --git a/builder/dockerfile/parser/line_parsers.go b/builder/dockerfile/parser/line_parsers.go new file mode 100644 index 00000000..1d7ece43 --- /dev/null +++ b/builder/dockerfile/parser/line_parsers.go @@ -0,0 +1,331 @@ +package parser + +// line parsers are dispatch calls that parse a single unit of text into a +// Node object which contains the whole statement. Dockerfiles have varied +// (but not usually unique, see ONBUILD for a unique example) parsing rules +// per-command, and these unify the processing in a way that makes it +// manageable. + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + "unicode" +) + +var ( + errDockerfileNotStringArray = errors.New("When using JSON array syntax, arrays must be comprised of strings only.") +) + +// ignore the current argument. This will still leave a command parsed, but +// will not incorporate the arguments into the ast. +func parseIgnore(rest string) (*Node, map[string]bool, error) { + return &Node{}, nil, nil +} + +// used for onbuild. Could potentially be used for anything that represents a +// statement with sub-statements. +// +// ONBUILD RUN foo bar -> (onbuild (run foo bar)) +// +func parseSubCommand(rest string) (*Node, map[string]bool, error) { + if rest == "" { + return nil, nil, nil + } + + _, child, err := ParseLine(rest) + if err != nil { + return nil, nil, err + } + + return &Node{Children: []*Node{child}}, nil, nil +} + +// helper to parse words (i.e space delimited or quoted strings) in a statement. +// The quotes are preserved as part of this function and they are stripped later +// as part of processWords(). +func parseWords(rest string) []string { + const ( + inSpaces = iota // looking for start of a word + inWord + inQuote + ) + + words := []string{} + phase := inSpaces + word := "" + quote := '\000' + blankOK := false + var ch rune + + for pos := 0; pos <= len(rest); pos++ { + if pos != len(rest) { + ch = rune(rest[pos]) + } + + if phase == inSpaces { // Looking for start of word + if pos == len(rest) { // end of input + break + } + if unicode.IsSpace(ch) { // skip spaces + continue + } + phase = inWord // found it, fall through + } + if (phase == inWord || phase == inQuote) && (pos == len(rest)) { + if blankOK || len(word) > 0 { + words = append(words, word) + } + break + } + if phase == inWord { + if unicode.IsSpace(ch) { + phase = inSpaces + if blankOK || len(word) > 0 { + words = append(words, word) + } + word = "" + blankOK = false + continue + } + if ch == '\'' || ch == '"' { + quote = ch + blankOK = true + phase = inQuote + } + if ch == '\\' { + if pos+1 == len(rest) { + continue // just skip \ at end + } + // If we're not quoted and we see a \, then always just + // add \ plus the char to the word, even if the char + // is a quote. + word += string(ch) + pos++ + ch = rune(rest[pos]) + } + word += string(ch) + continue + } + if phase == inQuote { + if ch == quote { + phase = inWord + } + // \ is special except for ' quotes - can't escape anything for ' + if ch == '\\' && quote != '\'' { + if pos+1 == len(rest) { + phase = inWord + continue // just skip \ at end + } + pos++ + nextCh := rune(rest[pos]) + word += string(ch) + ch = nextCh + } + word += string(ch) + } + } + + return words +} + +// parse environment like statements. Note that this does *not* handle +// variable interpolation, which will be handled in the evaluator. +func parseNameVal(rest string, key string) (*Node, map[string]bool, error) { + // This is kind of tricky because we need to support the old + // variant: KEY name value + // as well as the new one: KEY name=value ... + // The trigger to know which one is being used will be whether we hit + // a space or = first. space ==> old, "=" ==> new + + words := parseWords(rest) + if len(words) == 0 { + return nil, nil, nil + } + + var rootnode *Node + + // Old format (KEY name value) + if !strings.Contains(words[0], "=") { + node := &Node{} + rootnode = node + strs := tokenWhitespace.Split(rest, 2) + + if len(strs) < 2 { + return nil, nil, fmt.Errorf(key + " must have two arguments") + } + + node.Value = strs[0] + node.Next = &Node{} + node.Next.Value = strs[1] + } else { + var prevNode *Node + for i, word := range words { + if !strings.Contains(word, "=") { + return nil, nil, fmt.Errorf("Syntax error - can't find = in %q. Must be of the form: name=value", word) + } + parts := strings.SplitN(word, "=", 2) + + name := &Node{} + value := &Node{} + + name.Next = value + name.Value = parts[0] + value.Value = parts[1] + + if i == 0 { + rootnode = name + } else { + prevNode.Next = name + } + prevNode = value + } + } + + return rootnode, nil, nil +} + +func parseEnv(rest string) (*Node, map[string]bool, error) { + return parseNameVal(rest, "ENV") +} + +func parseLabel(rest string) (*Node, map[string]bool, error) { + return parseNameVal(rest, "LABEL") +} + +// parses a statement containing one or more keyword definition(s) and/or +// value assignments, like `name1 name2= name3="" name4=value`. +// Note that this is a stricter format than the old format of assignment, +// allowed by parseNameVal(), in a way that this only allows assignment of the +// form `keyword=[]` like `name2=`, `name3=""`, and `name4=value` above. +// In addition, a keyword definition alone is of the form `keyword` like `name1` +// above. And the assignments `name2=` and `name3=""` are equivalent and +// assign an empty value to the respective keywords. +func parseNameOrNameVal(rest string) (*Node, map[string]bool, error) { + words := parseWords(rest) + if len(words) == 0 { + return nil, nil, nil + } + + var ( + rootnode *Node + prevNode *Node + ) + for i, word := range words { + node := &Node{} + node.Value = word + if i == 0 { + rootnode = node + } else { + prevNode.Next = node + } + prevNode = node + } + + return rootnode, nil, nil +} + +// parses a whitespace-delimited set of arguments. The result is effectively a +// linked list of string arguments. +func parseStringsWhitespaceDelimited(rest string) (*Node, map[string]bool, error) { + if rest == "" { + return nil, nil, nil + } + + node := &Node{} + rootnode := node + prevnode := node + for _, str := range tokenWhitespace.Split(rest, -1) { // use regexp + prevnode = node + node.Value = str + node.Next = &Node{} + node = node.Next + } + + // XXX to get around regexp.Split *always* providing an empty string at the + // end due to how our loop is constructed, nil out the last node in the + // chain. + prevnode.Next = nil + + return rootnode, nil, nil +} + +// parsestring just wraps the string in quotes and returns a working node. +func parseString(rest string) (*Node, map[string]bool, error) { + if rest == "" { + return nil, nil, nil + } + n := &Node{} + n.Value = rest + return n, nil, nil +} + +// parseJSON converts JSON arrays to an AST. +func parseJSON(rest string) (*Node, map[string]bool, error) { + rest = strings.TrimLeftFunc(rest, unicode.IsSpace) + if !strings.HasPrefix(rest, "[") { + return nil, nil, fmt.Errorf(`Error parsing "%s" as a JSON array`, rest) + } + + var myJSON []interface{} + if err := json.NewDecoder(strings.NewReader(rest)).Decode(&myJSON); err != nil { + return nil, nil, err + } + + var top, prev *Node + for _, str := range myJSON { + s, ok := str.(string) + if !ok { + return nil, nil, errDockerfileNotStringArray + } + + node := &Node{Value: s} + if prev == nil { + top = node + } else { + prev.Next = node + } + prev = node + } + + return top, map[string]bool{"json": true}, nil +} + +// parseMaybeJSON determines if the argument appears to be a JSON array. If +// so, passes to parseJSON; if not, quotes the result and returns a single +// node. +func parseMaybeJSON(rest string) (*Node, map[string]bool, error) { + if rest == "" { + return nil, nil, nil + } + + node, attrs, err := parseJSON(rest) + + if err == nil { + return node, attrs, nil + } + if err == errDockerfileNotStringArray { + return nil, nil, err + } + + node = &Node{} + node.Value = rest + return node, nil, nil +} + +// parseMaybeJSONToList determines if the argument appears to be a JSON array. If +// so, passes to parseJSON; if not, attempts to parse it as a whitespace +// delimited string. +func parseMaybeJSONToList(rest string) (*Node, map[string]bool, error) { + node, attrs, err := parseJSON(rest) + + if err == nil { + return node, attrs, nil + } + if err == errDockerfileNotStringArray { + return nil, nil, err + } + + return parseStringsWhitespaceDelimited(rest) +} diff --git a/builder/dockerfile/parser/parser.go b/builder/dockerfile/parser/parser.go new file mode 100644 index 00000000..ece601a9 --- /dev/null +++ b/builder/dockerfile/parser/parser.go @@ -0,0 +1,161 @@ +// Package parser implements a parser and parse tree dumper for Dockerfiles. +package parser + +import ( + "bufio" + "io" + "regexp" + "strings" + "unicode" + + "github.com/docker/docker/builder/dockerfile/command" +) + +// Node is a structure used to represent a parse tree. +// +// In the node there are three fields, Value, Next, and Children. Value is the +// current token's string value. Next is always the next non-child token, and +// children contains all the children. Here's an example: +// +// (value next (child child-next child-next-next) next-next) +// +// This data structure is frankly pretty lousy for handling complex languages, +// but lucky for us the Dockerfile isn't very complicated. This structure +// works a little more effectively than a "proper" parse tree for our needs. +// +type Node struct { + Value string // actual content + Next *Node // the next item in the current sexp + Children []*Node // the children of this sexp + Attributes map[string]bool // special attributes for this node + Original string // original line used before parsing + Flags []string // only top Node should have this set + StartLine int // the line in the original dockerfile where the node begins + EndLine int // the line in the original dockerfile where the node ends +} + +var ( + dispatch map[string]func(string) (*Node, map[string]bool, error) + tokenWhitespace = regexp.MustCompile(`[\t\v\f\r ]+`) + tokenLineContinuation = regexp.MustCompile(`\\[ \t]*$`) + tokenComment = regexp.MustCompile(`^#.*$`) +) + +func init() { + // Dispatch Table. see line_parsers.go for the parse functions. + // The command is parsed and mapped to the line parser. The line parser + // receives the arguments but not the command, and returns an AST after + // reformulating the arguments according to the rules in the parser + // functions. Errors are propagated up by Parse() and the resulting AST can + // be incorporated directly into the existing AST as a next. + dispatch = map[string]func(string) (*Node, map[string]bool, error){ + command.User: parseString, + command.Onbuild: parseSubCommand, + command.Workdir: parseString, + command.Env: parseEnv, + command.Label: parseLabel, + command.Maintainer: parseString, + command.From: parseString, + command.Add: parseMaybeJSONToList, + command.Copy: parseMaybeJSONToList, + command.Run: parseMaybeJSON, + command.Cmd: parseMaybeJSON, + command.Entrypoint: parseMaybeJSON, + command.Expose: parseStringsWhitespaceDelimited, + command.Volume: parseMaybeJSONToList, + command.StopSignal: parseString, + command.Arg: parseNameOrNameVal, + } +} + +// ParseLine parse a line and return the remainder. +func ParseLine(line string) (string, *Node, error) { + if line = stripComments(line); line == "" { + return "", nil, nil + } + + if tokenLineContinuation.MatchString(line) { + line = tokenLineContinuation.ReplaceAllString(line, "") + return line, nil, nil + } + + cmd, flags, args, err := splitCommand(line) + if err != nil { + return "", nil, err + } + + node := &Node{} + node.Value = cmd + + sexp, attrs, err := fullDispatch(cmd, args) + if err != nil { + return "", nil, err + } + + node.Next = sexp + node.Attributes = attrs + node.Original = line + node.Flags = flags + + return "", node, nil +} + +// Parse is the main parse routine. +// It handles an io.ReadWriteCloser and returns the root of the AST. +func Parse(rwc io.Reader) (*Node, error) { + currentLine := 0 + root := &Node{} + root.StartLine = -1 + scanner := bufio.NewScanner(rwc) + + for scanner.Scan() { + scannedLine := strings.TrimLeftFunc(scanner.Text(), unicode.IsSpace) + currentLine++ + line, child, err := ParseLine(scannedLine) + if err != nil { + return nil, err + } + startLine := currentLine + + if line != "" && child == nil { + for scanner.Scan() { + newline := scanner.Text() + currentLine++ + + if stripComments(strings.TrimSpace(newline)) == "" { + continue + } + + line, child, err = ParseLine(line + newline) + if err != nil { + return nil, err + } + + if child != nil { + break + } + } + if child == nil && line != "" { + _, child, err = ParseLine(line) + if err != nil { + return nil, err + } + } + } + + if child != nil { + // Update the line information for the current child. + child.StartLine = startLine + child.EndLine = currentLine + // Update the line information for the root. The starting line of the root is always the + // starting line of the first child and the ending line is the ending line of the last child. + if root.StartLine < 0 { + root.StartLine = currentLine + } + root.EndLine = currentLine + root.Children = append(root.Children, child) + } + } + + return root, nil +} diff --git a/builder/dockerfile/parser/parser_test.go b/builder/dockerfile/parser/parser_test.go new file mode 100644 index 00000000..983a590a --- /dev/null +++ b/builder/dockerfile/parser/parser_test.go @@ -0,0 +1,154 @@ +package parser + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "testing" +) + +const testDir = "testfiles" +const negativeTestDir = "testfiles-negative" +const testFileLineInfo = "testfile-line/Dockerfile" + +func getDirs(t *testing.T, dir string) []string { + f, err := os.Open(dir) + if err != nil { + t.Fatal(err) + } + + defer f.Close() + + dirs, err := f.Readdirnames(0) + if err != nil { + t.Fatal(err) + } + + return dirs +} + +func TestTestNegative(t *testing.T) { + for _, dir := range getDirs(t, negativeTestDir) { + dockerfile := filepath.Join(negativeTestDir, dir, "Dockerfile") + + df, err := os.Open(dockerfile) + if err != nil { + t.Fatalf("Dockerfile missing for %s: %v", dir, err) + } + + _, err = Parse(df) + if err == nil { + t.Fatalf("No error parsing broken dockerfile for %s", dir) + } + + df.Close() + } +} + +func TestTestData(t *testing.T) { + for _, dir := range getDirs(t, testDir) { + dockerfile := filepath.Join(testDir, dir, "Dockerfile") + resultfile := filepath.Join(testDir, dir, "result") + + df, err := os.Open(dockerfile) + if err != nil { + t.Fatalf("Dockerfile missing for %s: %v", dir, err) + } + defer df.Close() + + ast, err := Parse(df) + if err != nil { + t.Fatalf("Error parsing %s's dockerfile: %v", dir, err) + } + + content, err := ioutil.ReadFile(resultfile) + if err != nil { + t.Fatalf("Error reading %s's result file: %v", dir, err) + } + + if runtime.GOOS == "windows" { + // CRLF --> CR to match Unix behavior + content = bytes.Replace(content, []byte{'\x0d', '\x0a'}, []byte{'\x0a'}, -1) + } + + if ast.Dump()+"\n" != string(content) { + fmt.Fprintln(os.Stderr, "Result:\n"+ast.Dump()) + fmt.Fprintln(os.Stderr, "Expected:\n"+string(content)) + t.Fatalf("%s: AST dump of dockerfile does not match result", dir) + } + } +} + +func TestParseWords(t *testing.T) { + tests := []map[string][]string{ + { + "input": {"foo"}, + "expect": {"foo"}, + }, + { + "input": {"foo bar"}, + "expect": {"foo", "bar"}, + }, + { + "input": {"foo=bar"}, + "expect": {"foo=bar"}, + }, + { + "input": {"foo bar 'abc xyz'"}, + "expect": {"foo", "bar", "'abc xyz'"}, + }, + { + "input": {`foo bar "abc xyz"`}, + "expect": {"foo", "bar", `"abc xyz"`}, + }, + } + + for _, test := range tests { + words := parseWords(test["input"][0]) + if len(words) != len(test["expect"]) { + t.Fatalf("length check failed. input: %v, expect: %v, output: %v", test["input"][0], test["expect"], words) + } + for i, word := range words { + if word != test["expect"][i] { + t.Fatalf("word check failed for word: %q. input: %v, expect: %v, output: %v", word, test["input"][0], test["expect"], words) + } + } + } +} + +func TestLineInformation(t *testing.T) { + df, err := os.Open(testFileLineInfo) + if err != nil { + t.Fatalf("Dockerfile missing for %s: %v", testFileLineInfo, err) + } + defer df.Close() + + ast, err := Parse(df) + if err != nil { + t.Fatalf("Error parsing dockerfile %s: %v", testFileLineInfo, err) + } + + if ast.StartLine != 4 || ast.EndLine != 30 { + fmt.Fprintf(os.Stderr, "Wrong root line information: expected(%d-%d), actual(%d-%d)\n", 4, 30, ast.StartLine, ast.EndLine) + t.Fatalf("Root line information doesn't match result.") + } + if len(ast.Children) != 3 { + fmt.Fprintf(os.Stderr, "Wrong number of child: expected(%d), actual(%d)\n", 3, len(ast.Children)) + t.Fatalf("Root line information doesn't match result.") + } + expected := [][]int{ + {4, 4}, + {10, 11}, + {16, 30}, + } + for i, child := range ast.Children { + if child.StartLine != expected[i][0] || child.EndLine != expected[i][1] { + fmt.Fprintf(os.Stderr, "Wrong line information for child %d: expected(%d-%d), actual(%d-%d)\n", + i, expected[i][0], expected[i][1], child.StartLine, child.EndLine) + t.Fatalf("Root line information doesn't match result.") + } + } +} diff --git a/builder/dockerfile/parser/testfile-line/Dockerfile b/builder/dockerfile/parser/testfile-line/Dockerfile new file mode 100644 index 00000000..0e77e85e --- /dev/null +++ b/builder/dockerfile/parser/testfile-line/Dockerfile @@ -0,0 +1,34 @@ + + + +FROM brimstone/ubuntu:14.04 + + +# TORUN -v /var/run/docker.sock:/var/run/docker.sock + + +ENV GOPATH \ +/go + + + +# Install the packages we need, clean up after them and us +RUN apt-get update \ + && dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.clean \ + + + && apt-get install -y --no-install-recommends git golang ca-certificates \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists \ + + && go get -v github.com/brimstone/consuldock \ + && mv $GOPATH/bin/consuldock /usr/local/bin/consuldock \ + + && dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.dirty \ + && apt-get remove --purge -y $(diff /tmp/dpkg.clean /tmp/dpkg.dirty | awk '/^>/ {print $2}') \ + && rm /tmp/dpkg.* \ + && rm -rf $GOPATH + + + + diff --git a/builder/dockerfile/parser/testfiles-negative/env_no_value/Dockerfile b/builder/dockerfile/parser/testfiles-negative/env_no_value/Dockerfile new file mode 100644 index 00000000..1d655787 --- /dev/null +++ b/builder/dockerfile/parser/testfiles-negative/env_no_value/Dockerfile @@ -0,0 +1,3 @@ +FROM busybox + +ENV PATH diff --git a/builder/dockerfile/parser/testfiles-negative/shykes-nested-json/Dockerfile b/builder/dockerfile/parser/testfiles-negative/shykes-nested-json/Dockerfile new file mode 100644 index 00000000..d1be4596 --- /dev/null +++ b/builder/dockerfile/parser/testfiles-negative/shykes-nested-json/Dockerfile @@ -0,0 +1 @@ +CMD [ "echo", [ "nested json" ] ] diff --git a/builder/dockerfile/parser/testfiles/ADD-COPY-with-JSON/Dockerfile b/builder/dockerfile/parser/testfiles/ADD-COPY-with-JSON/Dockerfile new file mode 100644 index 00000000..00b444cb --- /dev/null +++ b/builder/dockerfile/parser/testfiles/ADD-COPY-with-JSON/Dockerfile @@ -0,0 +1,11 @@ +FROM ubuntu:14.04 +MAINTAINER Seongyeol Lim + +COPY . /go/src/github.com/docker/docker +ADD . / +ADD null / +COPY nullfile /tmp +ADD [ "vimrc", "/tmp" ] +COPY [ "bashrc", "/tmp" ] +COPY [ "test file", "/tmp" ] +ADD [ "test file", "/tmp/test file" ] diff --git a/builder/dockerfile/parser/testfiles/ADD-COPY-with-JSON/result b/builder/dockerfile/parser/testfiles/ADD-COPY-with-JSON/result new file mode 100644 index 00000000..85aee640 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/ADD-COPY-with-JSON/result @@ -0,0 +1,10 @@ +(from "ubuntu:14.04") +(maintainer "Seongyeol Lim ") +(copy "." "/go/src/github.com/docker/docker") +(add "." "/") +(add "null" "/") +(copy "nullfile" "/tmp") +(add "vimrc" "/tmp") +(copy "bashrc" "/tmp") +(copy "test file" "/tmp") +(add "test file" "/tmp/test file") diff --git a/builder/dockerfile/parser/testfiles/brimstone-consuldock/Dockerfile b/builder/dockerfile/parser/testfiles/brimstone-consuldock/Dockerfile new file mode 100644 index 00000000..5c75a2e0 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/brimstone-consuldock/Dockerfile @@ -0,0 +1,25 @@ +FROM brimstone/ubuntu:14.04 + +MAINTAINER brimstone@the.narro.ws + +# TORUN -v /var/run/docker.sock:/var/run/docker.sock + +ENV GOPATH /go + +# Set our command +ENTRYPOINT ["/usr/local/bin/consuldock"] + +# Install the packages we need, clean up after them and us +RUN apt-get update \ + && dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.clean \ + && apt-get install -y --no-install-recommends git golang ca-certificates \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists \ + + && go get -v github.com/brimstone/consuldock \ + && mv $GOPATH/bin/consuldock /usr/local/bin/consuldock \ + + && dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.dirty \ + && apt-get remove --purge -y $(diff /tmp/dpkg.clean /tmp/dpkg.dirty | awk '/^>/ {print $2}') \ + && rm /tmp/dpkg.* \ + && rm -rf $GOPATH diff --git a/builder/dockerfile/parser/testfiles/brimstone-consuldock/result b/builder/dockerfile/parser/testfiles/brimstone-consuldock/result new file mode 100644 index 00000000..227f748c --- /dev/null +++ b/builder/dockerfile/parser/testfiles/brimstone-consuldock/result @@ -0,0 +1,5 @@ +(from "brimstone/ubuntu:14.04") +(maintainer "brimstone@the.narro.ws") +(env "GOPATH" "/go") +(entrypoint "/usr/local/bin/consuldock") +(run "apt-get update \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.clean && apt-get install -y --no-install-recommends git golang ca-certificates && apt-get clean && rm -rf /var/lib/apt/lists \t&& go get -v github.com/brimstone/consuldock && mv $GOPATH/bin/consuldock /usr/local/bin/consuldock \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.dirty \t&& apt-get remove --purge -y $(diff /tmp/dpkg.clean /tmp/dpkg.dirty | awk '/^>/ {print $2}') \t&& rm /tmp/dpkg.* \t&& rm -rf $GOPATH") diff --git a/builder/dockerfile/parser/testfiles/brimstone-docker-consul/Dockerfile b/builder/dockerfile/parser/testfiles/brimstone-docker-consul/Dockerfile new file mode 100644 index 00000000..25ae3521 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/brimstone-docker-consul/Dockerfile @@ -0,0 +1,52 @@ +FROM brimstone/ubuntu:14.04 + +CMD [] + +ENTRYPOINT ["/usr/bin/consul", "agent", "-server", "-data-dir=/consul", "-client=0.0.0.0", "-ui-dir=/webui"] + +EXPOSE 8500 8600 8400 8301 8302 + +RUN apt-get update \ + && apt-get install -y unzip wget \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists + +RUN cd /tmp \ + && wget https://dl.bintray.com/mitchellh/consul/0.3.1_web_ui.zip \ + -O web_ui.zip \ + && unzip web_ui.zip \ + && mv dist /webui \ + && rm web_ui.zip + +RUN apt-get update \ + && dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.clean \ + && apt-get install -y --no-install-recommends unzip wget \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists \ + + && cd /tmp \ + && wget https://dl.bintray.com/mitchellh/consul/0.3.1_web_ui.zip \ + -O web_ui.zip \ + && unzip web_ui.zip \ + && mv dist /webui \ + && rm web_ui.zip \ + + && dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.dirty \ + && apt-get remove --purge -y $(diff /tmp/dpkg.clean /tmp/dpkg.dirty | awk '/^>/ {print $2}') \ + && rm /tmp/dpkg.* + +ENV GOPATH /go + +RUN apt-get update \ + && dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.clean \ + && apt-get install -y --no-install-recommends git golang ca-certificates build-essential \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists \ + + && go get -v github.com/hashicorp/consul \ + && mv $GOPATH/bin/consul /usr/bin/consul \ + + && dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.dirty \ + && apt-get remove --purge -y $(diff /tmp/dpkg.clean /tmp/dpkg.dirty | awk '/^>/ {print $2}') \ + && rm /tmp/dpkg.* \ + && rm -rf $GOPATH diff --git a/builder/dockerfile/parser/testfiles/brimstone-docker-consul/result b/builder/dockerfile/parser/testfiles/brimstone-docker-consul/result new file mode 100644 index 00000000..16492e51 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/brimstone-docker-consul/result @@ -0,0 +1,9 @@ +(from "brimstone/ubuntu:14.04") +(cmd) +(entrypoint "/usr/bin/consul" "agent" "-server" "-data-dir=/consul" "-client=0.0.0.0" "-ui-dir=/webui") +(expose "8500" "8600" "8400" "8301" "8302") +(run "apt-get update && apt-get install -y unzip wget \t&& apt-get clean \t&& rm -rf /var/lib/apt/lists") +(run "cd /tmp && wget https://dl.bintray.com/mitchellh/consul/0.3.1_web_ui.zip -O web_ui.zip && unzip web_ui.zip && mv dist /webui && rm web_ui.zip") +(run "apt-get update \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.clean && apt-get install -y --no-install-recommends unzip wget && apt-get clean && rm -rf /var/lib/apt/lists && cd /tmp && wget https://dl.bintray.com/mitchellh/consul/0.3.1_web_ui.zip -O web_ui.zip && unzip web_ui.zip && mv dist /webui && rm web_ui.zip \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.dirty \t&& apt-get remove --purge -y $(diff /tmp/dpkg.clean /tmp/dpkg.dirty | awk '/^>/ {print $2}') \t&& rm /tmp/dpkg.*") +(env "GOPATH" "/go") +(run "apt-get update \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.clean && apt-get install -y --no-install-recommends git golang ca-certificates build-essential && apt-get clean && rm -rf /var/lib/apt/lists \t&& go get -v github.com/hashicorp/consul \t&& mv $GOPATH/bin/consul /usr/bin/consul \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.dirty \t&& apt-get remove --purge -y $(diff /tmp/dpkg.clean /tmp/dpkg.dirty | awk '/^>/ {print $2}') \t&& rm /tmp/dpkg.* \t&& rm -rf $GOPATH") diff --git a/builder/dockerfile/parser/testfiles/continueIndent/Dockerfile b/builder/dockerfile/parser/testfiles/continueIndent/Dockerfile new file mode 100644 index 00000000..42b324e7 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/continueIndent/Dockerfile @@ -0,0 +1,36 @@ +FROM ubuntu:14.04 + +RUN echo hello\ + world\ + goodnight \ + moon\ + light\ +ning +RUN echo hello \ + world +RUN echo hello \ +world +RUN echo hello \ +goodbye\ +frog +RUN echo hello \ +world +RUN echo hi \ + \ + world \ +\ + good\ +\ +night +RUN echo goodbye\ +frog +RUN echo good\ +bye\ +frog + +RUN echo hello \ +# this is a comment + +# this is a comment with a blank line surrounding it + +this is some more useful stuff diff --git a/builder/dockerfile/parser/testfiles/continueIndent/result b/builder/dockerfile/parser/testfiles/continueIndent/result new file mode 100644 index 00000000..268ae073 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/continueIndent/result @@ -0,0 +1,10 @@ +(from "ubuntu:14.04") +(run "echo hello world goodnight moon lightning") +(run "echo hello world") +(run "echo hello world") +(run "echo hello goodbyefrog") +(run "echo hello world") +(run "echo hi world goodnight") +(run "echo goodbyefrog") +(run "echo goodbyefrog") +(run "echo hello this is some more useful stuff") diff --git a/builder/dockerfile/parser/testfiles/cpuguy83-nagios/Dockerfile b/builder/dockerfile/parser/testfiles/cpuguy83-nagios/Dockerfile new file mode 100644 index 00000000..8ccb71a5 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/cpuguy83-nagios/Dockerfile @@ -0,0 +1,54 @@ +FROM cpuguy83/ubuntu +ENV NAGIOS_HOME /opt/nagios +ENV NAGIOS_USER nagios +ENV NAGIOS_GROUP nagios +ENV NAGIOS_CMDUSER nagios +ENV NAGIOS_CMDGROUP nagios +ENV NAGIOSADMIN_USER nagiosadmin +ENV NAGIOSADMIN_PASS nagios +ENV APACHE_RUN_USER nagios +ENV APACHE_RUN_GROUP nagios +ENV NAGIOS_TIMEZONE UTC + +RUN sed -i 's/universe/universe multiverse/' /etc/apt/sources.list +RUN apt-get update && apt-get install -y iputils-ping netcat build-essential snmp snmpd snmp-mibs-downloader php5-cli apache2 libapache2-mod-php5 runit bc postfix bsd-mailx +RUN ( egrep -i "^${NAGIOS_GROUP}" /etc/group || groupadd $NAGIOS_GROUP ) && ( egrep -i "^${NAGIOS_CMDGROUP}" /etc/group || groupadd $NAGIOS_CMDGROUP ) +RUN ( id -u $NAGIOS_USER || useradd --system $NAGIOS_USER -g $NAGIOS_GROUP -d $NAGIOS_HOME ) && ( id -u $NAGIOS_CMDUSER || useradd --system -d $NAGIOS_HOME -g $NAGIOS_CMDGROUP $NAGIOS_CMDUSER ) + +ADD http://downloads.sourceforge.net/project/nagios/nagios-3.x/nagios-3.5.1/nagios-3.5.1.tar.gz?r=http%3A%2F%2Fwww.nagios.org%2Fdownload%2Fcore%2Fthanks%2F%3Ft%3D1398863696&ts=1398863718&use_mirror=superb-dca3 /tmp/nagios.tar.gz +RUN cd /tmp && tar -zxvf nagios.tar.gz && cd nagios && ./configure --prefix=${NAGIOS_HOME} --exec-prefix=${NAGIOS_HOME} --enable-event-broker --with-nagios-command-user=${NAGIOS_CMDUSER} --with-command-group=${NAGIOS_CMDGROUP} --with-nagios-user=${NAGIOS_USER} --with-nagios-group=${NAGIOS_GROUP} && make all && make install && make install-config && make install-commandmode && cp sample-config/httpd.conf /etc/apache2/conf.d/nagios.conf +ADD http://www.nagios-plugins.org/download/nagios-plugins-1.5.tar.gz /tmp/ +RUN cd /tmp && tar -zxvf nagios-plugins-1.5.tar.gz && cd nagios-plugins-1.5 && ./configure --prefix=${NAGIOS_HOME} && make && make install + +RUN sed -i.bak 's/.*\=www\-data//g' /etc/apache2/envvars +RUN export DOC_ROOT="DocumentRoot $(echo $NAGIOS_HOME/share)"; sed -i "s,DocumentRoot.*,$DOC_ROOT," /etc/apache2/sites-enabled/000-default + +RUN ln -s ${NAGIOS_HOME}/bin/nagios /usr/local/bin/nagios && mkdir -p /usr/share/snmp/mibs && chmod 0755 /usr/share/snmp/mibs && touch /usr/share/snmp/mibs/.foo + +RUN echo "use_timezone=$NAGIOS_TIMEZONE" >> ${NAGIOS_HOME}/etc/nagios.cfg && echo "SetEnv TZ \"${NAGIOS_TIMEZONE}\"" >> /etc/apache2/conf.d/nagios.conf + +RUN mkdir -p ${NAGIOS_HOME}/etc/conf.d && mkdir -p ${NAGIOS_HOME}/etc/monitor && ln -s /usr/share/snmp/mibs ${NAGIOS_HOME}/libexec/mibs +RUN echo "cfg_dir=${NAGIOS_HOME}/etc/conf.d" >> ${NAGIOS_HOME}/etc/nagios.cfg +RUN echo "cfg_dir=${NAGIOS_HOME}/etc/monitor" >> ${NAGIOS_HOME}/etc/nagios.cfg +RUN download-mibs && echo "mibs +ALL" > /etc/snmp/snmp.conf + +RUN sed -i 's,/bin/mail,/usr/bin/mail,' /opt/nagios/etc/objects/commands.cfg && \ + sed -i 's,/usr/usr,/usr,' /opt/nagios/etc/objects/commands.cfg +RUN cp /etc/services /var/spool/postfix/etc/ + +RUN mkdir -p /etc/sv/nagios && mkdir -p /etc/sv/apache && rm -rf /etc/sv/getty-5 && mkdir -p /etc/sv/postfix +ADD nagios.init /etc/sv/nagios/run +ADD apache.init /etc/sv/apache/run +ADD postfix.init /etc/sv/postfix/run +ADD postfix.stop /etc/sv/postfix/finish + +ADD start.sh /usr/local/bin/start_nagios + +ENV APACHE_LOCK_DIR /var/run +ENV APACHE_LOG_DIR /var/log/apache2 + +EXPOSE 80 + +VOLUME ["/opt/nagios/var", "/opt/nagios/etc", "/opt/nagios/libexec", "/var/log/apache2", "/usr/share/snmp/mibs"] + +CMD ["/usr/local/bin/start_nagios"] diff --git a/builder/dockerfile/parser/testfiles/cpuguy83-nagios/result b/builder/dockerfile/parser/testfiles/cpuguy83-nagios/result new file mode 100644 index 00000000..25dd3ddf --- /dev/null +++ b/builder/dockerfile/parser/testfiles/cpuguy83-nagios/result @@ -0,0 +1,40 @@ +(from "cpuguy83/ubuntu") +(env "NAGIOS_HOME" "/opt/nagios") +(env "NAGIOS_USER" "nagios") +(env "NAGIOS_GROUP" "nagios") +(env "NAGIOS_CMDUSER" "nagios") +(env "NAGIOS_CMDGROUP" "nagios") +(env "NAGIOSADMIN_USER" "nagiosadmin") +(env "NAGIOSADMIN_PASS" "nagios") +(env "APACHE_RUN_USER" "nagios") +(env "APACHE_RUN_GROUP" "nagios") +(env "NAGIOS_TIMEZONE" "UTC") +(run "sed -i 's/universe/universe multiverse/' /etc/apt/sources.list") +(run "apt-get update && apt-get install -y iputils-ping netcat build-essential snmp snmpd snmp-mibs-downloader php5-cli apache2 libapache2-mod-php5 runit bc postfix bsd-mailx") +(run "( egrep -i \"^${NAGIOS_GROUP}\" /etc/group || groupadd $NAGIOS_GROUP ) && ( egrep -i \"^${NAGIOS_CMDGROUP}\" /etc/group || groupadd $NAGIOS_CMDGROUP )") +(run "( id -u $NAGIOS_USER || useradd --system $NAGIOS_USER -g $NAGIOS_GROUP -d $NAGIOS_HOME ) && ( id -u $NAGIOS_CMDUSER || useradd --system -d $NAGIOS_HOME -g $NAGIOS_CMDGROUP $NAGIOS_CMDUSER )") +(add "http://downloads.sourceforge.net/project/nagios/nagios-3.x/nagios-3.5.1/nagios-3.5.1.tar.gz?r=http%3A%2F%2Fwww.nagios.org%2Fdownload%2Fcore%2Fthanks%2F%3Ft%3D1398863696&ts=1398863718&use_mirror=superb-dca3" "/tmp/nagios.tar.gz") +(run "cd /tmp && tar -zxvf nagios.tar.gz && cd nagios && ./configure --prefix=${NAGIOS_HOME} --exec-prefix=${NAGIOS_HOME} --enable-event-broker --with-nagios-command-user=${NAGIOS_CMDUSER} --with-command-group=${NAGIOS_CMDGROUP} --with-nagios-user=${NAGIOS_USER} --with-nagios-group=${NAGIOS_GROUP} && make all && make install && make install-config && make install-commandmode && cp sample-config/httpd.conf /etc/apache2/conf.d/nagios.conf") +(add "http://www.nagios-plugins.org/download/nagios-plugins-1.5.tar.gz" "/tmp/") +(run "cd /tmp && tar -zxvf nagios-plugins-1.5.tar.gz && cd nagios-plugins-1.5 && ./configure --prefix=${NAGIOS_HOME} && make && make install") +(run "sed -i.bak 's/.*\\=www\\-data//g' /etc/apache2/envvars") +(run "export DOC_ROOT=\"DocumentRoot $(echo $NAGIOS_HOME/share)\"; sed -i \"s,DocumentRoot.*,$DOC_ROOT,\" /etc/apache2/sites-enabled/000-default") +(run "ln -s ${NAGIOS_HOME}/bin/nagios /usr/local/bin/nagios && mkdir -p /usr/share/snmp/mibs && chmod 0755 /usr/share/snmp/mibs && touch /usr/share/snmp/mibs/.foo") +(run "echo \"use_timezone=$NAGIOS_TIMEZONE\" >> ${NAGIOS_HOME}/etc/nagios.cfg && echo \"SetEnv TZ \\\"${NAGIOS_TIMEZONE}\\\"\" >> /etc/apache2/conf.d/nagios.conf") +(run "mkdir -p ${NAGIOS_HOME}/etc/conf.d && mkdir -p ${NAGIOS_HOME}/etc/monitor && ln -s /usr/share/snmp/mibs ${NAGIOS_HOME}/libexec/mibs") +(run "echo \"cfg_dir=${NAGIOS_HOME}/etc/conf.d\" >> ${NAGIOS_HOME}/etc/nagios.cfg") +(run "echo \"cfg_dir=${NAGIOS_HOME}/etc/monitor\" >> ${NAGIOS_HOME}/etc/nagios.cfg") +(run "download-mibs && echo \"mibs +ALL\" > /etc/snmp/snmp.conf") +(run "sed -i 's,/bin/mail,/usr/bin/mail,' /opt/nagios/etc/objects/commands.cfg && sed -i 's,/usr/usr,/usr,' /opt/nagios/etc/objects/commands.cfg") +(run "cp /etc/services /var/spool/postfix/etc/") +(run "mkdir -p /etc/sv/nagios && mkdir -p /etc/sv/apache && rm -rf /etc/sv/getty-5 && mkdir -p /etc/sv/postfix") +(add "nagios.init" "/etc/sv/nagios/run") +(add "apache.init" "/etc/sv/apache/run") +(add "postfix.init" "/etc/sv/postfix/run") +(add "postfix.stop" "/etc/sv/postfix/finish") +(add "start.sh" "/usr/local/bin/start_nagios") +(env "APACHE_LOCK_DIR" "/var/run") +(env "APACHE_LOG_DIR" "/var/log/apache2") +(expose "80") +(volume "/opt/nagios/var" "/opt/nagios/etc" "/opt/nagios/libexec" "/var/log/apache2" "/usr/share/snmp/mibs") +(cmd "/usr/local/bin/start_nagios") diff --git a/builder/dockerfile/parser/testfiles/docker/Dockerfile b/builder/dockerfile/parser/testfiles/docker/Dockerfile new file mode 100644 index 00000000..9717adbd --- /dev/null +++ b/builder/dockerfile/parser/testfiles/docker/Dockerfile @@ -0,0 +1,103 @@ +# This file describes the standard way to build Docker, using docker +# +# Usage: +# +# # Assemble the full dev environment. This is slow the first time. +# docker build -t docker . +# +# # Mount your source in an interactive container for quick testing: +# docker run -v `pwd`:/go/src/github.com/docker/docker --privileged -i -t docker bash +# +# # Run the test suite: +# docker run --privileged docker hack/make.sh test +# +# # Publish a release: +# docker run --privileged \ +# -e AWS_S3_BUCKET=baz \ +# -e AWS_ACCESS_KEY=foo \ +# -e AWS_SECRET_KEY=bar \ +# -e GPG_PASSPHRASE=gloubiboulga \ +# docker hack/release.sh +# +# Note: AppArmor used to mess with privileged mode, but this is no longer +# the case. Therefore, you don't have to disable it anymore. +# + +FROM ubuntu:14.04 +MAINTAINER Tianon Gravi (@tianon) + +# Packaged dependencies +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -yq \ + apt-utils \ + aufs-tools \ + automake \ + btrfs-tools \ + build-essential \ + curl \ + dpkg-sig \ + git \ + iptables \ + libapparmor-dev \ + libcap-dev \ + libsqlite3-dev \ + mercurial \ + pandoc \ + parallel \ + reprepro \ + ruby1.9.1 \ + ruby1.9.1-dev \ + s3cmd=1.1.0* \ + --no-install-recommends + +# Get lvm2 source for compiling statically +RUN git clone --no-checkout https://git.fedorahosted.org/git/lvm2.git /usr/local/lvm2 && cd /usr/local/lvm2 && git checkout -q v2_02_103 +# see https://git.fedorahosted.org/cgit/lvm2.git/refs/tags for release tags +# note: we don't use "git clone -b" above because it then spews big nasty warnings about 'detached HEAD' state that we can't silence as easily as we can silence them using "git checkout" directly + +# Compile and install lvm2 +RUN cd /usr/local/lvm2 && ./configure --enable-static_link && make device-mapper && make install_device-mapper +# see https://git.fedorahosted.org/cgit/lvm2.git/tree/INSTALL + +# Install Go +RUN curl -sSL https://golang.org/dl/go1.3.src.tar.gz | tar -v -C /usr/local -xz +ENV PATH /usr/local/go/bin:$PATH +ENV GOPATH /go:/go/src/github.com/docker/docker/vendor +RUN cd /usr/local/go/src && ./make.bash --no-clean 2>&1 + +# Compile Go for cross compilation +ENV DOCKER_CROSSPLATFORMS \ + linux/386 linux/arm \ + darwin/amd64 darwin/386 \ + freebsd/amd64 freebsd/386 freebsd/arm +# (set an explicit GOARM of 5 for maximum compatibility) +ENV GOARM 5 +RUN cd /usr/local/go/src && bash -xc 'for platform in $DOCKER_CROSSPLATFORMS; do GOOS=${platform%/*} GOARCH=${platform##*/} ./make.bash --no-clean 2>&1; done' + +# Grab Go's cover tool for dead-simple code coverage testing +RUN go get golang.org/x/tools/cmd/cover + +# TODO replace FPM with some very minimal debhelper stuff +RUN gem install --no-rdoc --no-ri fpm --version 1.0.2 + +# Get the "busybox" image source so we can build locally instead of pulling +RUN git clone -b buildroot-2014.02 https://github.com/jpetazzo/docker-busybox.git /docker-busybox + +# Setup s3cmd config +RUN /bin/echo -e '[default]\naccess_key=$AWS_ACCESS_KEY\nsecret_key=$AWS_SECRET_KEY' > /.s3cfg + +# Set user.email so crosbymichael's in-container merge commits go smoothly +RUN git config --global user.email 'docker-dummy@example.com' + +# Add an unprivileged user to be used for tests which need it +RUN groupadd -r docker +RUN useradd --create-home --gid docker unprivilegeduser + +VOLUME /var/lib/docker +WORKDIR /go/src/github.com/docker/docker +ENV DOCKER_BUILDTAGS apparmor selinux + +# Wrap all commands in the "docker-in-docker" script to allow nested containers +ENTRYPOINT ["hack/dind"] + +# Upload docker source +COPY . /go/src/github.com/docker/docker diff --git a/builder/dockerfile/parser/testfiles/docker/result b/builder/dockerfile/parser/testfiles/docker/result new file mode 100644 index 00000000..d032f9ba --- /dev/null +++ b/builder/dockerfile/parser/testfiles/docker/result @@ -0,0 +1,24 @@ +(from "ubuntu:14.04") +(maintainer "Tianon Gravi (@tianon)") +(run "apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -yq \tapt-utils \taufs-tools \tautomake \tbtrfs-tools \tbuild-essential \tcurl \tdpkg-sig \tgit \tiptables \tlibapparmor-dev \tlibcap-dev \tlibsqlite3-dev \tmercurial \tpandoc \tparallel \treprepro \truby1.9.1 \truby1.9.1-dev \ts3cmd=1.1.0* \t--no-install-recommends") +(run "git clone --no-checkout https://git.fedorahosted.org/git/lvm2.git /usr/local/lvm2 && cd /usr/local/lvm2 && git checkout -q v2_02_103") +(run "cd /usr/local/lvm2 && ./configure --enable-static_link && make device-mapper && make install_device-mapper") +(run "curl -sSL https://golang.org/dl/go1.3.src.tar.gz | tar -v -C /usr/local -xz") +(env "PATH" "/usr/local/go/bin:$PATH") +(env "GOPATH" "/go:/go/src/github.com/docker/docker/vendor") +(run "cd /usr/local/go/src && ./make.bash --no-clean 2>&1") +(env "DOCKER_CROSSPLATFORMS" "linux/386 linux/arm \tdarwin/amd64 darwin/386 \tfreebsd/amd64 freebsd/386 freebsd/arm") +(env "GOARM" "5") +(run "cd /usr/local/go/src && bash -xc 'for platform in $DOCKER_CROSSPLATFORMS; do GOOS=${platform%/*} GOARCH=${platform##*/} ./make.bash --no-clean 2>&1; done'") +(run "go get golang.org/x/tools/cmd/cover") +(run "gem install --no-rdoc --no-ri fpm --version 1.0.2") +(run "git clone -b buildroot-2014.02 https://github.com/jpetazzo/docker-busybox.git /docker-busybox") +(run "/bin/echo -e '[default]\\naccess_key=$AWS_ACCESS_KEY\\nsecret_key=$AWS_SECRET_KEY' > /.s3cfg") +(run "git config --global user.email 'docker-dummy@example.com'") +(run "groupadd -r docker") +(run "useradd --create-home --gid docker unprivilegeduser") +(volume "/var/lib/docker") +(workdir "/go/src/github.com/docker/docker") +(env "DOCKER_BUILDTAGS" "apparmor selinux") +(entrypoint "hack/dind") +(copy "." "/go/src/github.com/docker/docker") diff --git a/builder/dockerfile/parser/testfiles/env/Dockerfile b/builder/dockerfile/parser/testfiles/env/Dockerfile new file mode 100644 index 00000000..08fa18ac --- /dev/null +++ b/builder/dockerfile/parser/testfiles/env/Dockerfile @@ -0,0 +1,23 @@ +FROM ubuntu +ENV name value +ENV name=value +ENV name=value name2=value2 +ENV name="value value1" +ENV name=value\ value2 +ENV name="value'quote space'value2" +ENV name='value"double quote"value2' +ENV name=value\ value2 name2=value2\ value3 +ENV name="a\"b" +ENV name="a\'b" +ENV name='a\'b' +ENV name='a\'b'' +ENV name='a\"b' +ENV name="''" +# don't put anything after the next line - it must be the last line of the +# Dockerfile and it must end with \ +ENV name=value \ + name1=value1 \ + name2="value2a \ + value2b" \ + name3="value3a\n\"value3b\"" \ + name4="value4a\\nvalue4b" \ diff --git a/builder/dockerfile/parser/testfiles/env/result b/builder/dockerfile/parser/testfiles/env/result new file mode 100644 index 00000000..ba0a6dd7 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/env/result @@ -0,0 +1,16 @@ +(from "ubuntu") +(env "name" "value") +(env "name" "value") +(env "name" "value" "name2" "value2") +(env "name" "\"value value1\"") +(env "name" "value\\ value2") +(env "name" "\"value'quote space'value2\"") +(env "name" "'value\"double quote\"value2'") +(env "name" "value\\ value2" "name2" "value2\\ value3") +(env "name" "\"a\\\"b\"") +(env "name" "\"a\\'b\"") +(env "name" "'a\\'b'") +(env "name" "'a\\'b''") +(env "name" "'a\\\"b'") +(env "name" "\"''\"") +(env "name" "value" "name1" "value1" "name2" "\"value2a value2b\"" "name3" "\"value3a\\n\\\"value3b\\\"\"" "name4" "\"value4a\\\\nvalue4b\"") diff --git a/builder/dockerfile/parser/testfiles/escapes/Dockerfile b/builder/dockerfile/parser/testfiles/escapes/Dockerfile new file mode 100644 index 00000000..1ffb17ef --- /dev/null +++ b/builder/dockerfile/parser/testfiles/escapes/Dockerfile @@ -0,0 +1,14 @@ +FROM ubuntu:14.04 +MAINTAINER Erik \\Hollensbe \" + +RUN apt-get \update && \ + apt-get \"install znc -y +ADD \conf\\" /.znc + +RUN foo \ + +bar \ + +baz + +CMD [ "\/usr\\\"/bin/znc", "-f", "-r" ] diff --git a/builder/dockerfile/parser/testfiles/escapes/result b/builder/dockerfile/parser/testfiles/escapes/result new file mode 100644 index 00000000..13e409cb --- /dev/null +++ b/builder/dockerfile/parser/testfiles/escapes/result @@ -0,0 +1,6 @@ +(from "ubuntu:14.04") +(maintainer "Erik \\\\Hollensbe \\\"") +(run "apt-get \\update && apt-get \\\"install znc -y") +(add "\\conf\\\\\"" "/.znc") +(run "foo bar baz") +(cmd "/usr\\\"/bin/znc" "-f" "-r") diff --git a/builder/dockerfile/parser/testfiles/flags/Dockerfile b/builder/dockerfile/parser/testfiles/flags/Dockerfile new file mode 100644 index 00000000..2418e0f0 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/flags/Dockerfile @@ -0,0 +1,10 @@ +FROM scratch +COPY foo /tmp/ +COPY --user=me foo /tmp/ +COPY --doit=true foo /tmp/ +COPY --user=me --doit=true foo /tmp/ +COPY --doit=true -- foo /tmp/ +COPY -- foo /tmp/ +CMD --doit [ "a", "b" ] +CMD --doit=true -- [ "a", "b" ] +CMD --doit -- [ ] diff --git a/builder/dockerfile/parser/testfiles/flags/result b/builder/dockerfile/parser/testfiles/flags/result new file mode 100644 index 00000000..4578f4cb --- /dev/null +++ b/builder/dockerfile/parser/testfiles/flags/result @@ -0,0 +1,10 @@ +(from "scratch") +(copy "foo" "/tmp/") +(copy ["--user=me"] "foo" "/tmp/") +(copy ["--doit=true"] "foo" "/tmp/") +(copy ["--user=me" "--doit=true"] "foo" "/tmp/") +(copy ["--doit=true"] "foo" "/tmp/") +(copy "foo" "/tmp/") +(cmd ["--doit"] "a" "b") +(cmd ["--doit=true"] "a" "b") +(cmd ["--doit"]) diff --git a/builder/dockerfile/parser/testfiles/influxdb/Dockerfile b/builder/dockerfile/parser/testfiles/influxdb/Dockerfile new file mode 100644 index 00000000..587fb9b5 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/influxdb/Dockerfile @@ -0,0 +1,15 @@ +FROM ubuntu:14.04 + +RUN apt-get update && apt-get install wget -y +RUN wget http://s3.amazonaws.com/influxdb/influxdb_latest_amd64.deb +RUN dpkg -i influxdb_latest_amd64.deb +RUN rm -r /opt/influxdb/shared + +VOLUME /opt/influxdb/shared + +CMD /usr/bin/influxdb --pidfile /var/run/influxdb.pid -config /opt/influxdb/shared/config.toml + +EXPOSE 8083 +EXPOSE 8086 +EXPOSE 8090 +EXPOSE 8099 diff --git a/builder/dockerfile/parser/testfiles/influxdb/result b/builder/dockerfile/parser/testfiles/influxdb/result new file mode 100644 index 00000000..0998e87e --- /dev/null +++ b/builder/dockerfile/parser/testfiles/influxdb/result @@ -0,0 +1,11 @@ +(from "ubuntu:14.04") +(run "apt-get update && apt-get install wget -y") +(run "wget http://s3.amazonaws.com/influxdb/influxdb_latest_amd64.deb") +(run "dpkg -i influxdb_latest_amd64.deb") +(run "rm -r /opt/influxdb/shared") +(volume "/opt/influxdb/shared") +(cmd "/usr/bin/influxdb --pidfile /var/run/influxdb.pid -config /opt/influxdb/shared/config.toml") +(expose "8083") +(expose "8086") +(expose "8090") +(expose "8099") diff --git a/builder/dockerfile/parser/testfiles/jeztah-invalid-json-json-inside-string-double/Dockerfile b/builder/dockerfile/parser/testfiles/jeztah-invalid-json-json-inside-string-double/Dockerfile new file mode 100644 index 00000000..39fe27d9 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/jeztah-invalid-json-json-inside-string-double/Dockerfile @@ -0,0 +1 @@ +CMD "[\"echo\", \"Phew, I just managed to escaped those double quotes\"]" diff --git a/builder/dockerfile/parser/testfiles/jeztah-invalid-json-json-inside-string-double/result b/builder/dockerfile/parser/testfiles/jeztah-invalid-json-json-inside-string-double/result new file mode 100644 index 00000000..afc220c2 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/jeztah-invalid-json-json-inside-string-double/result @@ -0,0 +1 @@ +(cmd "\"[\\\"echo\\\", \\\"Phew, I just managed to escaped those double quotes\\\"]\"") diff --git a/builder/dockerfile/parser/testfiles/jeztah-invalid-json-json-inside-string/Dockerfile b/builder/dockerfile/parser/testfiles/jeztah-invalid-json-json-inside-string/Dockerfile new file mode 100644 index 00000000..eaae081a --- /dev/null +++ b/builder/dockerfile/parser/testfiles/jeztah-invalid-json-json-inside-string/Dockerfile @@ -0,0 +1 @@ +CMD '["echo", "Well, JSON in a string is JSON too?"]' diff --git a/builder/dockerfile/parser/testfiles/jeztah-invalid-json-json-inside-string/result b/builder/dockerfile/parser/testfiles/jeztah-invalid-json-json-inside-string/result new file mode 100644 index 00000000..484804e2 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/jeztah-invalid-json-json-inside-string/result @@ -0,0 +1 @@ +(cmd "'[\"echo\", \"Well, JSON in a string is JSON too?\"]'") diff --git a/builder/dockerfile/parser/testfiles/jeztah-invalid-json-single-quotes/Dockerfile b/builder/dockerfile/parser/testfiles/jeztah-invalid-json-single-quotes/Dockerfile new file mode 100644 index 00000000..c3ac63c0 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/jeztah-invalid-json-single-quotes/Dockerfile @@ -0,0 +1 @@ +CMD ['echo','single quotes are invalid JSON'] diff --git a/builder/dockerfile/parser/testfiles/jeztah-invalid-json-single-quotes/result b/builder/dockerfile/parser/testfiles/jeztah-invalid-json-single-quotes/result new file mode 100644 index 00000000..61478912 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/jeztah-invalid-json-single-quotes/result @@ -0,0 +1 @@ +(cmd "['echo','single quotes are invalid JSON']") diff --git a/builder/dockerfile/parser/testfiles/jeztah-invalid-json-unterminated-bracket/Dockerfile b/builder/dockerfile/parser/testfiles/jeztah-invalid-json-unterminated-bracket/Dockerfile new file mode 100644 index 00000000..5fd4afa5 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/jeztah-invalid-json-unterminated-bracket/Dockerfile @@ -0,0 +1 @@ +CMD ["echo", "Please, close the brackets when you're done" diff --git a/builder/dockerfile/parser/testfiles/jeztah-invalid-json-unterminated-bracket/result b/builder/dockerfile/parser/testfiles/jeztah-invalid-json-unterminated-bracket/result new file mode 100644 index 00000000..1ffbb8ff --- /dev/null +++ b/builder/dockerfile/parser/testfiles/jeztah-invalid-json-unterminated-bracket/result @@ -0,0 +1 @@ +(cmd "[\"echo\", \"Please, close the brackets when you're done\"") diff --git a/builder/dockerfile/parser/testfiles/jeztah-invalid-json-unterminated-string/Dockerfile b/builder/dockerfile/parser/testfiles/jeztah-invalid-json-unterminated-string/Dockerfile new file mode 100644 index 00000000..30cc4bb4 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/jeztah-invalid-json-unterminated-string/Dockerfile @@ -0,0 +1 @@ +CMD ["echo", "look ma, no quote!] diff --git a/builder/dockerfile/parser/testfiles/jeztah-invalid-json-unterminated-string/result b/builder/dockerfile/parser/testfiles/jeztah-invalid-json-unterminated-string/result new file mode 100644 index 00000000..32048147 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/jeztah-invalid-json-unterminated-string/result @@ -0,0 +1 @@ +(cmd "[\"echo\", \"look ma, no quote!]") diff --git a/builder/dockerfile/parser/testfiles/json/Dockerfile b/builder/dockerfile/parser/testfiles/json/Dockerfile new file mode 100644 index 00000000..a5869171 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/json/Dockerfile @@ -0,0 +1,8 @@ +CMD [] +CMD [""] +CMD ["a"] +CMD ["a","b"] +CMD [ "a", "b" ] +CMD [ "a", "b" ] +CMD [ "a", "b" ] +CMD ["abc 123", "♥", "☃", "\" \\ \/ \b \f \n \r \t \u0000"] diff --git a/builder/dockerfile/parser/testfiles/json/result b/builder/dockerfile/parser/testfiles/json/result new file mode 100644 index 00000000..c6553e6e --- /dev/null +++ b/builder/dockerfile/parser/testfiles/json/result @@ -0,0 +1,8 @@ +(cmd) +(cmd "") +(cmd "a") +(cmd "a" "b") +(cmd "a" "b") +(cmd "a" "b") +(cmd "a" "b") +(cmd "abc 123" "♥" "☃" "\" \\ / \b \f \n \r \t \x00") diff --git a/builder/dockerfile/parser/testfiles/kartar-entrypoint-oddities/Dockerfile b/builder/dockerfile/parser/testfiles/kartar-entrypoint-oddities/Dockerfile new file mode 100644 index 00000000..35f9c24a --- /dev/null +++ b/builder/dockerfile/parser/testfiles/kartar-entrypoint-oddities/Dockerfile @@ -0,0 +1,7 @@ +FROM ubuntu:14.04 +MAINTAINER James Turnbull "james@example.com" +ENV REFRESHED_AT 2014-06-01 +RUN apt-get update +RUN apt-get -y install redis-server redis-tools +EXPOSE 6379 +ENTRYPOINT [ "/usr/bin/redis-server" ] diff --git a/builder/dockerfile/parser/testfiles/kartar-entrypoint-oddities/result b/builder/dockerfile/parser/testfiles/kartar-entrypoint-oddities/result new file mode 100644 index 00000000..b5ac6fe4 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/kartar-entrypoint-oddities/result @@ -0,0 +1,7 @@ +(from "ubuntu:14.04") +(maintainer "James Turnbull \"james@example.com\"") +(env "REFRESHED_AT" "2014-06-01") +(run "apt-get update") +(run "apt-get -y install redis-server redis-tools") +(expose "6379") +(entrypoint "/usr/bin/redis-server") diff --git a/builder/dockerfile/parser/testfiles/lk4d4-the-edge-case-generator/Dockerfile b/builder/dockerfile/parser/testfiles/lk4d4-the-edge-case-generator/Dockerfile new file mode 100644 index 00000000..188395fe --- /dev/null +++ b/builder/dockerfile/parser/testfiles/lk4d4-the-edge-case-generator/Dockerfile @@ -0,0 +1,48 @@ +FROM busybox:buildroot-2014.02 + +MAINTAINER docker + +ONBUILD RUN ["echo", "test"] +ONBUILD RUN echo test +ONBUILD COPY . / + + +# RUN Commands \ +# linebreak in comment \ +RUN ["ls", "-la"] +RUN ["echo", "'1234'"] +RUN echo "1234" +RUN echo 1234 +RUN echo '1234' && \ + echo "456" && \ + echo 789 +RUN sh -c 'echo root:testpass \ + > /tmp/passwd' +RUN mkdir -p /test /test2 /test3/test + +# ENV \ +ENV SCUBA 1 DUBA 3 +ENV SCUBA "1 DUBA 3" + +# CMD \ +CMD ["echo", "test"] +CMD echo test +CMD echo "test" +CMD echo 'test' +CMD echo 'test' | wc - + +#EXPOSE\ +EXPOSE 3000 +EXPOSE 9000 5000 6000 + +USER docker +USER docker:root + +VOLUME ["/test"] +VOLUME ["/test", "/test2"] +VOLUME /test3 + +WORKDIR /test + +ADD . / +COPY . copy diff --git a/builder/dockerfile/parser/testfiles/lk4d4-the-edge-case-generator/result b/builder/dockerfile/parser/testfiles/lk4d4-the-edge-case-generator/result new file mode 100644 index 00000000..6f7d57a3 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/lk4d4-the-edge-case-generator/result @@ -0,0 +1,29 @@ +(from "busybox:buildroot-2014.02") +(maintainer "docker ") +(onbuild (run "echo" "test")) +(onbuild (run "echo test")) +(onbuild (copy "." "/")) +(run "ls" "-la") +(run "echo" "'1234'") +(run "echo \"1234\"") +(run "echo 1234") +(run "echo '1234' && echo \"456\" && echo 789") +(run "sh -c 'echo root:testpass > /tmp/passwd'") +(run "mkdir -p /test /test2 /test3/test") +(env "SCUBA" "1 DUBA 3") +(env "SCUBA" "\"1 DUBA 3\"") +(cmd "echo" "test") +(cmd "echo test") +(cmd "echo \"test\"") +(cmd "echo 'test'") +(cmd "echo 'test' | wc -") +(expose "3000") +(expose "9000" "5000" "6000") +(user "docker") +(user "docker:root") +(volume "/test") +(volume "/test" "/test2") +(volume "/test3") +(workdir "/test") +(add "." "/") +(copy "." "copy") diff --git a/builder/dockerfile/parser/testfiles/mail/Dockerfile b/builder/dockerfile/parser/testfiles/mail/Dockerfile new file mode 100644 index 00000000..f64c1168 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/mail/Dockerfile @@ -0,0 +1,16 @@ +FROM ubuntu:14.04 + +RUN apt-get update -qy && apt-get install mutt offlineimap vim-nox abook elinks curl tmux cron zsh -y +ADD .muttrc / +ADD .offlineimaprc / +ADD .tmux.conf / +ADD mutt /.mutt +ADD vim /.vim +ADD vimrc /.vimrc +ADD crontab /etc/crontab +RUN chmod 644 /etc/crontab +RUN mkdir /Mail +RUN mkdir /.offlineimap +RUN echo "export TERM=screen-256color" >/.zshenv + +CMD setsid cron; tmux -2 diff --git a/builder/dockerfile/parser/testfiles/mail/result b/builder/dockerfile/parser/testfiles/mail/result new file mode 100644 index 00000000..a0efcf04 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/mail/result @@ -0,0 +1,14 @@ +(from "ubuntu:14.04") +(run "apt-get update -qy && apt-get install mutt offlineimap vim-nox abook elinks curl tmux cron zsh -y") +(add ".muttrc" "/") +(add ".offlineimaprc" "/") +(add ".tmux.conf" "/") +(add "mutt" "/.mutt") +(add "vim" "/.vim") +(add "vimrc" "/.vimrc") +(add "crontab" "/etc/crontab") +(run "chmod 644 /etc/crontab") +(run "mkdir /Mail") +(run "mkdir /.offlineimap") +(run "echo \"export TERM=screen-256color\" >/.zshenv") +(cmd "setsid cron; tmux -2") diff --git a/builder/dockerfile/parser/testfiles/multiple-volumes/Dockerfile b/builder/dockerfile/parser/testfiles/multiple-volumes/Dockerfile new file mode 100644 index 00000000..57bb5976 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/multiple-volumes/Dockerfile @@ -0,0 +1,3 @@ +FROM foo + +VOLUME /opt/nagios/var /opt/nagios/etc /opt/nagios/libexec /var/log/apache2 /usr/share/snmp/mibs diff --git a/builder/dockerfile/parser/testfiles/multiple-volumes/result b/builder/dockerfile/parser/testfiles/multiple-volumes/result new file mode 100644 index 00000000..18dbdeea --- /dev/null +++ b/builder/dockerfile/parser/testfiles/multiple-volumes/result @@ -0,0 +1,2 @@ +(from "foo") +(volume "/opt/nagios/var" "/opt/nagios/etc" "/opt/nagios/libexec" "/var/log/apache2" "/usr/share/snmp/mibs") diff --git a/builder/dockerfile/parser/testfiles/mumble/Dockerfile b/builder/dockerfile/parser/testfiles/mumble/Dockerfile new file mode 100644 index 00000000..5b9ec06a --- /dev/null +++ b/builder/dockerfile/parser/testfiles/mumble/Dockerfile @@ -0,0 +1,7 @@ +FROM ubuntu:14.04 + +RUN apt-get update && apt-get install libcap2-bin mumble-server -y + +ADD ./mumble-server.ini /etc/mumble-server.ini + +CMD /usr/sbin/murmurd diff --git a/builder/dockerfile/parser/testfiles/mumble/result b/builder/dockerfile/parser/testfiles/mumble/result new file mode 100644 index 00000000..a0036a94 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/mumble/result @@ -0,0 +1,4 @@ +(from "ubuntu:14.04") +(run "apt-get update && apt-get install libcap2-bin mumble-server -y") +(add "./mumble-server.ini" "/etc/mumble-server.ini") +(cmd "/usr/sbin/murmurd") diff --git a/builder/dockerfile/parser/testfiles/nginx/Dockerfile b/builder/dockerfile/parser/testfiles/nginx/Dockerfile new file mode 100644 index 00000000..bf8368e1 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/nginx/Dockerfile @@ -0,0 +1,14 @@ +FROM ubuntu:14.04 +MAINTAINER Erik Hollensbe + +RUN apt-get update && apt-get install nginx-full -y +RUN rm -rf /etc/nginx +ADD etc /etc/nginx +RUN chown -R root:root /etc/nginx +RUN /usr/sbin/nginx -qt +RUN mkdir /www + +CMD ["/usr/sbin/nginx"] + +VOLUME /www +EXPOSE 80 diff --git a/builder/dockerfile/parser/testfiles/nginx/result b/builder/dockerfile/parser/testfiles/nginx/result new file mode 100644 index 00000000..56ddb6f2 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/nginx/result @@ -0,0 +1,11 @@ +(from "ubuntu:14.04") +(maintainer "Erik Hollensbe ") +(run "apt-get update && apt-get install nginx-full -y") +(run "rm -rf /etc/nginx") +(add "etc" "/etc/nginx") +(run "chown -R root:root /etc/nginx") +(run "/usr/sbin/nginx -qt") +(run "mkdir /www") +(cmd "/usr/sbin/nginx") +(volume "/www") +(expose "80") diff --git a/builder/dockerfile/parser/testfiles/tf2/Dockerfile b/builder/dockerfile/parser/testfiles/tf2/Dockerfile new file mode 100644 index 00000000..72b79bdd --- /dev/null +++ b/builder/dockerfile/parser/testfiles/tf2/Dockerfile @@ -0,0 +1,23 @@ +FROM ubuntu:12.04 + +EXPOSE 27015 +EXPOSE 27005 +EXPOSE 26901 +EXPOSE 27020 + +RUN apt-get update && apt-get install libc6-dev-i386 curl unzip -y +RUN mkdir -p /steam +RUN curl http://media.steampowered.com/client/steamcmd_linux.tar.gz | tar vxz -C /steam +ADD ./script /steam/script +RUN /steam/steamcmd.sh +runscript /steam/script +RUN curl http://mirror.pointysoftware.net/alliedmodders/mmsource-1.10.0-linux.tar.gz | tar vxz -C /steam/tf2/tf +RUN curl http://mirror.pointysoftware.net/alliedmodders/sourcemod-1.5.3-linux.tar.gz | tar vxz -C /steam/tf2/tf +ADD ./server.cfg /steam/tf2/tf/cfg/server.cfg +ADD ./ctf_2fort.cfg /steam/tf2/tf/cfg/ctf_2fort.cfg +ADD ./sourcemod.cfg /steam/tf2/tf/cfg/sourcemod/sourcemod.cfg +RUN rm -r /steam/tf2/tf/addons/sourcemod/configs +ADD ./configs /steam/tf2/tf/addons/sourcemod/configs +RUN mkdir -p /steam/tf2/tf/addons/sourcemod/translations/en +RUN cp /steam/tf2/tf/addons/sourcemod/translations/*.txt /steam/tf2/tf/addons/sourcemod/translations/en + +CMD cd /steam/tf2 && ./srcds_run -port 27015 +ip 0.0.0.0 +map ctf_2fort -autoupdate -steam_dir /steam -steamcmd_script /steam/script +tf_bot_quota 12 +tf_bot_quota_mode fill diff --git a/builder/dockerfile/parser/testfiles/tf2/result b/builder/dockerfile/parser/testfiles/tf2/result new file mode 100644 index 00000000..d4f94cd8 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/tf2/result @@ -0,0 +1,20 @@ +(from "ubuntu:12.04") +(expose "27015") +(expose "27005") +(expose "26901") +(expose "27020") +(run "apt-get update && apt-get install libc6-dev-i386 curl unzip -y") +(run "mkdir -p /steam") +(run "curl http://media.steampowered.com/client/steamcmd_linux.tar.gz | tar vxz -C /steam") +(add "./script" "/steam/script") +(run "/steam/steamcmd.sh +runscript /steam/script") +(run "curl http://mirror.pointysoftware.net/alliedmodders/mmsource-1.10.0-linux.tar.gz | tar vxz -C /steam/tf2/tf") +(run "curl http://mirror.pointysoftware.net/alliedmodders/sourcemod-1.5.3-linux.tar.gz | tar vxz -C /steam/tf2/tf") +(add "./server.cfg" "/steam/tf2/tf/cfg/server.cfg") +(add "./ctf_2fort.cfg" "/steam/tf2/tf/cfg/ctf_2fort.cfg") +(add "./sourcemod.cfg" "/steam/tf2/tf/cfg/sourcemod/sourcemod.cfg") +(run "rm -r /steam/tf2/tf/addons/sourcemod/configs") +(add "./configs" "/steam/tf2/tf/addons/sourcemod/configs") +(run "mkdir -p /steam/tf2/tf/addons/sourcemod/translations/en") +(run "cp /steam/tf2/tf/addons/sourcemod/translations/*.txt /steam/tf2/tf/addons/sourcemod/translations/en") +(cmd "cd /steam/tf2 && ./srcds_run -port 27015 +ip 0.0.0.0 +map ctf_2fort -autoupdate -steam_dir /steam -steamcmd_script /steam/script +tf_bot_quota 12 +tf_bot_quota_mode fill") diff --git a/builder/dockerfile/parser/testfiles/weechat/Dockerfile b/builder/dockerfile/parser/testfiles/weechat/Dockerfile new file mode 100644 index 00000000..48420881 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/weechat/Dockerfile @@ -0,0 +1,9 @@ +FROM ubuntu:14.04 + +RUN apt-get update -qy && apt-get install tmux zsh weechat-curses -y + +ADD .weechat /.weechat +ADD .tmux.conf / +RUN echo "export TERM=screen-256color" >/.zshenv + +CMD zsh -c weechat diff --git a/builder/dockerfile/parser/testfiles/weechat/result b/builder/dockerfile/parser/testfiles/weechat/result new file mode 100644 index 00000000..c3abb4c5 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/weechat/result @@ -0,0 +1,6 @@ +(from "ubuntu:14.04") +(run "apt-get update -qy && apt-get install tmux zsh weechat-curses -y") +(add ".weechat" "/.weechat") +(add ".tmux.conf" "/") +(run "echo \"export TERM=screen-256color\" >/.zshenv") +(cmd "zsh -c weechat") diff --git a/builder/dockerfile/parser/testfiles/znc/Dockerfile b/builder/dockerfile/parser/testfiles/znc/Dockerfile new file mode 100644 index 00000000..3a4da6e9 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/znc/Dockerfile @@ -0,0 +1,7 @@ +FROM ubuntu:14.04 +MAINTAINER Erik Hollensbe + +RUN apt-get update && apt-get install znc -y +ADD conf /.znc + +CMD [ "/usr/bin/znc", "-f", "-r" ] diff --git a/builder/dockerfile/parser/testfiles/znc/result b/builder/dockerfile/parser/testfiles/znc/result new file mode 100644 index 00000000..5493b255 --- /dev/null +++ b/builder/dockerfile/parser/testfiles/znc/result @@ -0,0 +1,5 @@ +(from "ubuntu:14.04") +(maintainer "Erik Hollensbe ") +(run "apt-get update && apt-get install znc -y") +(add "conf" "/.znc") +(cmd "/usr/bin/znc" "-f" "-r") diff --git a/builder/dockerfile/parser/utils.go b/builder/dockerfile/parser/utils.go new file mode 100644 index 00000000..b21eb62a --- /dev/null +++ b/builder/dockerfile/parser/utils.go @@ -0,0 +1,176 @@ +package parser + +import ( + "fmt" + "strconv" + "strings" + "unicode" +) + +// Dump dumps the AST defined by `node` as a list of sexps. +// Returns a string suitable for printing. +func (node *Node) Dump() string { + str := "" + str += node.Value + + if len(node.Flags) > 0 { + str += fmt.Sprintf(" %q", node.Flags) + } + + for _, n := range node.Children { + str += "(" + n.Dump() + ")\n" + } + + if node.Next != nil { + for n := node.Next; n != nil; n = n.Next { + if len(n.Children) > 0 { + str += " " + n.Dump() + } else { + str += " " + strconv.Quote(n.Value) + } + } + } + + return strings.TrimSpace(str) +} + +// performs the dispatch based on the two primal strings, cmd and args. Please +// look at the dispatch table in parser.go to see how these dispatchers work. +func fullDispatch(cmd, args string) (*Node, map[string]bool, error) { + fn := dispatch[cmd] + + // Ignore invalid Dockerfile instructions + if fn == nil { + fn = parseIgnore + } + + sexp, attrs, err := fn(args) + if err != nil { + return nil, nil, err + } + + return sexp, attrs, nil +} + +// splitCommand takes a single line of text and parses out the cmd and args, +// which are used for dispatching to more exact parsing functions. +func splitCommand(line string) (string, []string, string, error) { + var args string + var flags []string + + // Make sure we get the same results irrespective of leading/trailing spaces + cmdline := tokenWhitespace.Split(strings.TrimSpace(line), 2) + cmd := strings.ToLower(cmdline[0]) + + if len(cmdline) == 2 { + var err error + args, flags, err = extractBuilderFlags(cmdline[1]) + if err != nil { + return "", nil, "", err + } + } + + return cmd, flags, strings.TrimSpace(args), nil +} + +// covers comments and empty lines. Lines should be trimmed before passing to +// this function. +func stripComments(line string) string { + // string is already trimmed at this point + if tokenComment.MatchString(line) { + return tokenComment.ReplaceAllString(line, "") + } + + return line +} + +func extractBuilderFlags(line string) (string, []string, error) { + // Parses the BuilderFlags and returns the remaining part of the line + + const ( + inSpaces = iota // looking for start of a word + inWord + inQuote + ) + + words := []string{} + phase := inSpaces + word := "" + quote := '\000' + blankOK := false + var ch rune + + for pos := 0; pos <= len(line); pos++ { + if pos != len(line) { + ch = rune(line[pos]) + } + + if phase == inSpaces { // Looking for start of word + if pos == len(line) { // end of input + break + } + if unicode.IsSpace(ch) { // skip spaces + continue + } + + // Only keep going if the next word starts with -- + if ch != '-' || pos+1 == len(line) || rune(line[pos+1]) != '-' { + return line[pos:], words, nil + } + + phase = inWord // found someting with "--", fall through + } + if (phase == inWord || phase == inQuote) && (pos == len(line)) { + if word != "--" && (blankOK || len(word) > 0) { + words = append(words, word) + } + break + } + if phase == inWord { + if unicode.IsSpace(ch) { + phase = inSpaces + if word == "--" { + return line[pos:], words, nil + } + if blankOK || len(word) > 0 { + words = append(words, word) + } + word = "" + blankOK = false + continue + } + if ch == '\'' || ch == '"' { + quote = ch + blankOK = true + phase = inQuote + continue + } + if ch == '\\' { + if pos+1 == len(line) { + continue // just skip \ at end + } + pos++ + ch = rune(line[pos]) + } + word += string(ch) + continue + } + if phase == inQuote { + if ch == quote { + phase = inWord + continue + } + if ch == '\\' { + if pos+1 == len(line) { + phase = inWord + continue // just skip \ at end + } + pos++ + ch = rune(line[pos]) + } + word += string(ch) + } + } + + return "", words, nil +} diff --git a/builder/dockerfile/shell_parser.go b/builder/dockerfile/shell_parser.go new file mode 100644 index 00000000..c7142667 --- /dev/null +++ b/builder/dockerfile/shell_parser.go @@ -0,0 +1,314 @@ +package dockerfile + +// This will take a single word and an array of env variables and +// process all quotes (" and ') as well as $xxx and ${xxx} env variable +// tokens. Tries to mimic bash shell process. +// It doesn't support all flavors of ${xx:...} formats but new ones can +// be added by adding code to the "special ${} format processing" section + +import ( + "fmt" + "strings" + "text/scanner" + "unicode" +) + +type shellWord struct { + word string + scanner scanner.Scanner + envs []string + pos int +} + +// ProcessWord will use the 'env' list of environment variables, +// and replace any env var references in 'word'. +func ProcessWord(word string, env []string) (string, error) { + sw := &shellWord{ + word: word, + envs: env, + pos: 0, + } + sw.scanner.Init(strings.NewReader(word)) + word, _, err := sw.process() + return word, err +} + +// ProcessWords will use the 'env' list of environment variables, +// and replace any env var references in 'word' then it will also +// return a slice of strings which represents the 'word' +// split up based on spaces - taking into account quotes. Note that +// this splitting is done **after** the env var substitutions are done. +// Note, each one is trimmed to remove leading and trailing spaces (unless +// they are quoted", but ProcessWord retains spaces between words. +func ProcessWords(word string, env []string) ([]string, error) { + sw := &shellWord{ + word: word, + envs: env, + pos: 0, + } + sw.scanner.Init(strings.NewReader(word)) + _, words, err := sw.process() + return words, err +} + +func (sw *shellWord) process() (string, []string, error) { + return sw.processStopOn(scanner.EOF) +} + +type wordsStruct struct { + word string + words []string + inWord bool +} + +func (w *wordsStruct) addChar(ch rune) { + if unicode.IsSpace(ch) && w.inWord { + if len(w.word) != 0 { + w.words = append(w.words, w.word) + w.word = "" + w.inWord = false + } + } else if !unicode.IsSpace(ch) { + w.addRawChar(ch) + } +} + +func (w *wordsStruct) addRawChar(ch rune) { + w.word += string(ch) + w.inWord = true +} + +func (w *wordsStruct) addString(str string) { + var scan scanner.Scanner + scan.Init(strings.NewReader(str)) + for scan.Peek() != scanner.EOF { + w.addChar(scan.Next()) + } +} + +func (w *wordsStruct) addRawString(str string) { + w.word += str + w.inWord = true +} + +func (w *wordsStruct) getWords() []string { + if len(w.word) > 0 { + w.words = append(w.words, w.word) + + // Just in case we're called again by mistake + w.word = "" + w.inWord = false + } + return w.words +} + +// Process the word, starting at 'pos', and stop when we get to the +// end of the word or the 'stopChar' character +func (sw *shellWord) processStopOn(stopChar rune) (string, []string, error) { + var result string + var words wordsStruct + + var charFuncMapping = map[rune]func() (string, error){ + '\'': sw.processSingleQuote, + '"': sw.processDoubleQuote, + '$': sw.processDollar, + } + + for sw.scanner.Peek() != scanner.EOF { + ch := sw.scanner.Peek() + + if stopChar != scanner.EOF && ch == stopChar { + sw.scanner.Next() + break + } + if fn, ok := charFuncMapping[ch]; ok { + // Call special processing func for certain chars + tmp, err := fn() + if err != nil { + return "", []string{}, err + } + result += tmp + + if ch == rune('$') { + words.addString(tmp) + } else { + words.addRawString(tmp) + } + } else { + // Not special, just add it to the result + ch = sw.scanner.Next() + + if ch == '\\' { + // '\' escapes, except end of line + + ch = sw.scanner.Next() + + if ch == scanner.EOF { + break + } + + words.addRawChar(ch) + } else { + words.addChar(ch) + } + + result += string(ch) + } + } + + return result, words.getWords(), nil +} + +func (sw *shellWord) processSingleQuote() (string, error) { + // All chars between single quotes are taken as-is + // Note, you can't escape ' + var result string + + sw.scanner.Next() + + for { + ch := sw.scanner.Next() + if ch == '\'' || ch == scanner.EOF { + break + } + result += string(ch) + } + + return result, nil +} + +func (sw *shellWord) processDoubleQuote() (string, error) { + // All chars up to the next " are taken as-is, even ', except any $ chars + // But you can escape " with a \ + var result string + + sw.scanner.Next() + + for sw.scanner.Peek() != scanner.EOF { + ch := sw.scanner.Peek() + if ch == '"' { + sw.scanner.Next() + break + } + if ch == '$' { + tmp, err := sw.processDollar() + if err != nil { + return "", err + } + result += tmp + } else { + ch = sw.scanner.Next() + if ch == '\\' { + chNext := sw.scanner.Peek() + + if chNext == scanner.EOF { + // Ignore \ at end of word + continue + } + + if chNext == '"' || chNext == '$' { + // \" and \$ can be escaped, all other \'s are left as-is + ch = sw.scanner.Next() + } + } + result += string(ch) + } + } + + return result, nil +} + +func (sw *shellWord) processDollar() (string, error) { + sw.scanner.Next() + ch := sw.scanner.Peek() + if ch == '{' { + sw.scanner.Next() + name := sw.processName() + ch = sw.scanner.Peek() + if ch == '}' { + // Normal ${xx} case + sw.scanner.Next() + return sw.getEnv(name), nil + } + if ch == ':' { + // Special ${xx:...} format processing + // Yes it allows for recursive $'s in the ... spot + + sw.scanner.Next() // skip over : + modifier := sw.scanner.Next() + + word, _, err := sw.processStopOn('}') + if err != nil { + return "", err + } + + // Grab the current value of the variable in question so we + // can use to to determine what to do based on the modifier + newValue := sw.getEnv(name) + + switch modifier { + case '+': + if newValue != "" { + newValue = word + } + return newValue, nil + + case '-': + if newValue == "" { + newValue = word + } + return newValue, nil + + default: + return "", fmt.Errorf("Unsupported modifier (%c) in substitution: %s", modifier, sw.word) + } + } + return "", fmt.Errorf("Missing ':' in substitution: %s", sw.word) + } + // $xxx case + name := sw.processName() + if name == "" { + return "$", nil + } + return sw.getEnv(name), nil +} + +func (sw *shellWord) processName() string { + // Read in a name (alphanumeric or _) + // If it starts with a numeric then just return $# + var name string + + for sw.scanner.Peek() != scanner.EOF { + ch := sw.scanner.Peek() + if len(name) == 0 && unicode.IsDigit(ch) { + ch = sw.scanner.Next() + return string(ch) + } + if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) && ch != '_' { + break + } + ch = sw.scanner.Next() + name += string(ch) + } + + return name +} + +func (sw *shellWord) getEnv(name string) string { + for _, env := range sw.envs { + i := strings.Index(env, "=") + if i < 0 { + if name == env { + // Should probably never get here, but just in case treat + // it like "var" and "var=" are the same + return "" + } + continue + } + if name != env[:i] { + continue + } + return env[i+1:] + } + return "" +} diff --git a/builder/dockerfile/shell_parser_test.go b/builder/dockerfile/shell_parser_test.go new file mode 100644 index 00000000..81ac591e --- /dev/null +++ b/builder/dockerfile/shell_parser_test.go @@ -0,0 +1,143 @@ +package dockerfile + +import ( + "bufio" + "os" + "strings" + "testing" +) + +func TestShellParser4EnvVars(t *testing.T) { + fn := "envVarTest" + + file, err := os.Open(fn) + if err != nil { + t.Fatalf("Can't open '%s': %s", err, fn) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + envs := []string{"PWD=/home", "SHELL=bash", "KOREAN=한국어"} + for scanner.Scan() { + line := scanner.Text() + + // Trim comments and blank lines + i := strings.Index(line, "#") + if i >= 0 { + line = line[:i] + } + line = strings.TrimSpace(line) + + if line == "" { + continue + } + + words := strings.Split(line, "|") + if len(words) != 2 { + t.Fatalf("Error in '%s' - should be exactly one | in:%q", fn, line) + } + + words[0] = strings.TrimSpace(words[0]) + words[1] = strings.TrimSpace(words[1]) + + newWord, err := ProcessWord(words[0], envs) + + if err != nil { + newWord = "error" + } + + if newWord != words[1] { + t.Fatalf("Error. Src: %s Calc: %s Expected: %s", words[0], newWord, words[1]) + } + } +} + +func TestShellParser4Words(t *testing.T) { + fn := "wordsTest" + + file, err := os.Open(fn) + if err != nil { + t.Fatalf("Can't open '%s': %s", err, fn) + } + defer file.Close() + + envs := []string{} + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + + if strings.HasPrefix(line, "#") { + continue + } + + if strings.HasPrefix(line, "ENV ") { + line = strings.TrimLeft(line[3:], " ") + envs = append(envs, line) + continue + } + + words := strings.Split(line, "|") + if len(words) != 2 { + t.Fatalf("Error in '%s' - should be exactly one | in: %q", fn, line) + } + test := strings.TrimSpace(words[0]) + expected := strings.Split(strings.TrimLeft(words[1], " "), ",") + + result, err := ProcessWords(test, envs) + + if err != nil { + result = []string{"error"} + } + + if len(result) != len(expected) { + t.Fatalf("Error. %q was suppose to result in %q, but got %q instead", test, expected, result) + } + for i, w := range expected { + if w != result[i] { + t.Fatalf("Error. %q was suppose to result in %q, but got %q instead", test, expected, result) + } + } + } +} + +func TestGetEnv(t *testing.T) { + sw := &shellWord{ + word: "", + envs: nil, + pos: 0, + } + + sw.envs = []string{} + if sw.getEnv("foo") != "" { + t.Fatalf("2 - 'foo' should map to ''") + } + + sw.envs = []string{"foo"} + if sw.getEnv("foo") != "" { + t.Fatalf("3 - 'foo' should map to ''") + } + + sw.envs = []string{"foo="} + if sw.getEnv("foo") != "" { + t.Fatalf("4 - 'foo' should map to ''") + } + + sw.envs = []string{"foo=bar"} + if sw.getEnv("foo") != "bar" { + t.Fatalf("5 - 'foo' should map to 'bar'") + } + + sw.envs = []string{"foo=bar", "car=hat"} + if sw.getEnv("foo") != "bar" { + t.Fatalf("6 - 'foo' should map to 'bar'") + } + if sw.getEnv("car") != "hat" { + t.Fatalf("7 - 'car' should map to 'hat'") + } + + // Make sure we grab the first 'car' in the list + sw.envs = []string{"foo=bar", "car=hat", "car=bike"} + if sw.getEnv("car") != "hat" { + t.Fatalf("8 - 'car' should map to 'hat'") + } +} diff --git a/builder/dockerfile/support.go b/builder/dockerfile/support.go new file mode 100644 index 00000000..38897b2c --- /dev/null +++ b/builder/dockerfile/support.go @@ -0,0 +1,16 @@ +package dockerfile + +import "strings" + +func handleJSONArgs(args []string, attributes map[string]bool) []string { + if len(args) == 0 { + return []string{} + } + + if attributes != nil && attributes["json"] { + return args + } + + // literal string command, not an exec array + return []string{strings.Join(args, " ")} +} diff --git a/builder/dockerfile/wordsTest b/builder/dockerfile/wordsTest new file mode 100644 index 00000000..fa916c67 --- /dev/null +++ b/builder/dockerfile/wordsTest @@ -0,0 +1,25 @@ +hello | hello +hello${hi}bye | hellobye +ENV hi=hi +hello${hi}bye | hellohibye +ENV space=abc def +hello${space}bye | helloabc,defbye +hello"${space}"bye | helloabc defbye +hello "${space}"bye | hello,abc defbye +ENV leading= ab c +hello${leading}def | hello,ab,cdef +hello"${leading}" def | hello ab c,def +hello"${leading}" | hello ab c +hello${leading} | hello,ab,c +# next line MUST have 3 trailing spaces, don't erase them! +ENV trailing=ab c +hello${trailing} | helloab,c +hello${trailing}d | helloab,c,d +hello"${trailing}"d | helloab c d +# next line MUST have 3 trailing spaces, don't erase them! +hel"lo${trailing}" | helloab c +hello" there " | hello there +hello there | hello,there +hello\ there | hello there +hello" there | hello there +hello\" there | hello",there diff --git a/builder/dockerignore.go b/builder/dockerignore.go new file mode 100644 index 00000000..2990770a --- /dev/null +++ b/builder/dockerignore.go @@ -0,0 +1,47 @@ +package builder + +import ( + "os" + + "github.com/docker/docker/builder/dockerignore" + "github.com/docker/docker/pkg/fileutils" +) + +// DockerIgnoreContext wraps a ModifiableContext to add a method +// for handling the .dockerignore file at the root of the context. +type DockerIgnoreContext struct { + ModifiableContext +} + +// Process reads the .dockerignore file at the root of the embedded context. +// If .dockerignore does not exist in the context, then nil is returned. +// +// It can take a list of files to be removed after .dockerignore is removed. +// This is used for server-side implementations of builders that need to send +// the .dockerignore file as well as the special files specified in filesToRemove, +// but expect them to be excluded from the context after they were processed. +// +// For example, server-side Dockerfile builders are expected to pass in the name +// of the Dockerfile to be removed after it was parsed. +// +// TODO: Don't require a ModifiableContext (use Context instead) and don't remove +// files, instead handle a list of files to be excluded from the context. +func (c DockerIgnoreContext) Process(filesToRemove []string) error { + f, err := c.Open(".dockerignore") + // Note that a missing .dockerignore file isn't treated as an error + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + excludes, _ := dockerignore.ReadAll(f) + filesToRemove = append([]string{".dockerignore"}, filesToRemove...) + for _, fileToRemove := range filesToRemove { + rm, _ := fileutils.Matches(fileToRemove, excludes) + if rm { + c.Remove(fileToRemove) + } + } + return nil +} diff --git a/builder/dockerignore/dockerignore.go b/builder/dockerignore/dockerignore.go new file mode 100644 index 00000000..1fed3199 --- /dev/null +++ b/builder/dockerignore/dockerignore.go @@ -0,0 +1,35 @@ +package dockerignore + +import ( + "bufio" + "fmt" + "io" + "path/filepath" + "strings" +) + +// ReadAll reads a .dockerignore file and returns the list of file patterns +// to ignore. Note this will trim whitespace from each line as well +// as use GO's "clean" func to get the shortest/cleanest path for each. +func ReadAll(reader io.ReadCloser) ([]string, error) { + if reader == nil { + return nil, nil + } + defer reader.Close() + scanner := bufio.NewScanner(reader) + var excludes []string + + for scanner.Scan() { + pattern := strings.TrimSpace(scanner.Text()) + if pattern == "" { + continue + } + pattern = filepath.Clean(pattern) + pattern = filepath.ToSlash(pattern) + excludes = append(excludes, pattern) + } + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("Error reading .dockerignore: %v", err) + } + return excludes, nil +} diff --git a/builder/dockerignore/dockerignore_test.go b/builder/dockerignore/dockerignore_test.go new file mode 100644 index 00000000..361b0419 --- /dev/null +++ b/builder/dockerignore/dockerignore_test.go @@ -0,0 +1,55 @@ +package dockerignore + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +func TestReadAll(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "dockerignore-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + di, err := ReadAll(nil) + if err != nil { + t.Fatalf("Expected not to have error, got %v", err) + } + + if diLen := len(di); diLen != 0 { + t.Fatalf("Expected to have zero dockerignore entry, got %d", diLen) + } + + diName := filepath.Join(tmpDir, ".dockerignore") + content := fmt.Sprintf("test1\n/test2\n/a/file/here\n\nlastfile") + err = ioutil.WriteFile(diName, []byte(content), 0777) + if err != nil { + t.Fatal(err) + } + + diFd, err := os.Open(diName) + if err != nil { + t.Fatal(err) + } + di, err = ReadAll(diFd) + if err != nil { + t.Fatal(err) + } + + if di[0] != "test1" { + t.Fatalf("First element is not test1") + } + if di[1] != "/test2" { + t.Fatalf("Second element is not /test2") + } + if di[2] != "/a/file/here" { + t.Fatalf("Third element is not /a/file/here") + } + if di[3] != "lastfile" { + t.Fatalf("Fourth element is not lastfile") + } +} diff --git a/builder/git.go b/builder/git.go new file mode 100644 index 00000000..74df2446 --- /dev/null +++ b/builder/git.go @@ -0,0 +1,28 @@ +package builder + +import ( + "os" + + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/gitutils" +) + +// MakeGitContext returns a Context from gitURL that is cloned in a temporary directory. +func MakeGitContext(gitURL string) (ModifiableContext, error) { + root, err := gitutils.Clone(gitURL) + if err != nil { + return nil, err + } + + c, err := archive.Tar(root, archive.Uncompressed) + if err != nil { + return nil, err + } + + defer func() { + // TODO: print errors? + c.Close() + os.RemoveAll(root) + }() + return MakeTarSumContext(c) +} diff --git a/builder/remote.go b/builder/remote.go new file mode 100644 index 00000000..12f34c7b --- /dev/null +++ b/builder/remote.go @@ -0,0 +1,152 @@ +package builder + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "regexp" + + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/httputils" + "github.com/docker/docker/pkg/urlutil" +) + +// When downloading remote contexts, limit the amount (in bytes) +// to be read from the response body in order to detect its Content-Type +const maxPreambleLength = 100 + +const acceptableRemoteMIME = `(?:application/(?:(?:x\-)?tar|octet\-stream|((?:x\-)?(?:gzip|bzip2?|xz)))|(?:text/plain))` + +var mimeRe = regexp.MustCompile(acceptableRemoteMIME) + +// MakeRemoteContext downloads a context from remoteURL and returns it. +// +// If contentTypeHandlers is non-nil, then the Content-Type header is read along with a maximum of +// maxPreambleLength bytes from the body to help detecting the MIME type. +// Look at acceptableRemoteMIME for more details. +// +// If a match is found, then the body is sent to the contentType handler and a (potentially compressed) tar stream is expected +// to be returned. If no match is found, it is assumed the body is a tar stream (compressed or not). +// In either case, an (assumed) tar stream is passed to MakeTarSumContext whose result is returned. +func MakeRemoteContext(remoteURL string, contentTypeHandlers map[string]func(io.ReadCloser) (io.ReadCloser, error)) (ModifiableContext, error) { + f, err := httputils.Download(remoteURL) + if err != nil { + return nil, fmt.Errorf("Error downloading remote context %s: %v", remoteURL, err) + } + defer f.Body.Close() + + var contextReader io.ReadCloser + if contentTypeHandlers != nil { + contentType := f.Header.Get("Content-Type") + clen := f.ContentLength + + contentType, contextReader, err = inspectResponse(contentType, f.Body, clen) + if err != nil { + return nil, fmt.Errorf("Error detecting content type for remote %s: %v", remoteURL, err) + } + defer contextReader.Close() + + // This loop tries to find a content-type handler for the detected content-type. + // If it could not find one from the caller-supplied map, it tries the empty content-type `""` + // which is interpreted as a fallback handler (usually used for raw tar contexts). + for _, ct := range []string{contentType, ""} { + if fn, ok := contentTypeHandlers[ct]; ok { + defer contextReader.Close() + if contextReader, err = fn(contextReader); err != nil { + return nil, err + } + break + } + } + } + + // Pass through - this is a pre-packaged context, presumably + // with a Dockerfile with the right name inside it. + return MakeTarSumContext(contextReader) +} + +// DetectContextFromRemoteURL returns a context and in certain cases the name of the dockerfile to be used +// irrespective of user input. +// progressReader is only used if remoteURL is actually a URL (not empty, and not a Git endpoint). +func DetectContextFromRemoteURL(r io.ReadCloser, remoteURL string, createProgressReader func(in io.ReadCloser) io.ReadCloser) (context ModifiableContext, dockerfileName string, err error) { + switch { + case remoteURL == "": + context, err = MakeTarSumContext(r) + case urlutil.IsGitURL(remoteURL): + context, err = MakeGitContext(remoteURL) + case urlutil.IsURL(remoteURL): + context, err = MakeRemoteContext(remoteURL, map[string]func(io.ReadCloser) (io.ReadCloser, error){ + httputils.MimeTypes.TextPlain: func(rc io.ReadCloser) (io.ReadCloser, error) { + dockerfile, err := ioutil.ReadAll(rc) + if err != nil { + return nil, err + } + + // dockerfileName is set to signal that the remote was interpreted as a single Dockerfile, in which case the caller + // should use dockerfileName as the new name for the Dockerfile, irrespective of any other user input. + dockerfileName = DefaultDockerfileName + + // TODO: return a context without tarsum + return archive.Generate(dockerfileName, string(dockerfile)) + }, + // fallback handler (tar context) + "": func(rc io.ReadCloser) (io.ReadCloser, error) { + return createProgressReader(rc), nil + }, + }) + default: + err = fmt.Errorf("remoteURL (%s) could not be recognized as URL", remoteURL) + } + return +} + +// inspectResponse looks into the http response data at r to determine whether its +// content-type is on the list of acceptable content types for remote build contexts. +// This function returns: +// - a string representation of the detected content-type +// - an io.Reader for the response body +// - an error value which will be non-nil either when something goes wrong while +// reading bytes from r or when the detected content-type is not acceptable. +func inspectResponse(ct string, r io.ReadCloser, clen int64) (string, io.ReadCloser, error) { + plen := clen + if plen <= 0 || plen > maxPreambleLength { + plen = maxPreambleLength + } + + preamble := make([]byte, plen, plen) + rlen, err := r.Read(preamble) + if rlen == 0 { + return ct, r, errors.New("Empty response") + } + if err != nil && err != io.EOF { + return ct, r, err + } + + preambleR := bytes.NewReader(preamble) + bodyReader := ioutil.NopCloser(io.MultiReader(preambleR, r)) + // Some web servers will use application/octet-stream as the default + // content type for files without an extension (e.g. 'Dockerfile') + // so if we receive this value we better check for text content + contentType := ct + if len(ct) == 0 || ct == httputils.MimeTypes.OctetStream { + contentType, _, err = httputils.DetectContentType(preamble) + if err != nil { + return contentType, bodyReader, err + } + } + + contentType = selectAcceptableMIME(contentType) + var cterr error + if len(contentType) == 0 { + cterr = fmt.Errorf("unsupported Content-Type %q", ct) + contentType = ct + } + + return contentType, bodyReader, cterr +} + +func selectAcceptableMIME(ct string) string { + return mimeRe.FindString(ct) +} diff --git a/builder/remote_test.go b/builder/remote_test.go new file mode 100644 index 00000000..10d73ee5 --- /dev/null +++ b/builder/remote_test.go @@ -0,0 +1,146 @@ +package builder + +import ( + "bytes" + "io/ioutil" + "testing" +) + +var textPlainDockerfile = "FROM busybox" +var binaryContext = []byte{0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00} //xz magic + +func TestSelectAcceptableMIME(t *testing.T) { + validMimeStrings := []string{ + "application/x-bzip2", + "application/bzip2", + "application/gzip", + "application/x-gzip", + "application/x-xz", + "application/xz", + "application/tar", + "application/x-tar", + "application/octet-stream", + "text/plain", + } + + invalidMimeStrings := []string{ + "", + "application/octet", + "application/json", + } + + for _, m := range invalidMimeStrings { + if len(selectAcceptableMIME(m)) > 0 { + t.Fatalf("Should not have accepted %q", m) + } + } + + for _, m := range validMimeStrings { + if str := selectAcceptableMIME(m); str == "" { + t.Fatalf("Should have accepted %q", m) + } + } +} + +func TestInspectEmptyResponse(t *testing.T) { + ct := "application/octet-stream" + br := ioutil.NopCloser(bytes.NewReader([]byte(""))) + contentType, bReader, err := inspectResponse(ct, br, 0) + if err == nil { + t.Fatalf("Should have generated an error for an empty response") + } + if contentType != "application/octet-stream" { + t.Fatalf("Content type should be 'application/octet-stream' but is %q", contentType) + } + body, err := ioutil.ReadAll(bReader) + if err != nil { + t.Fatal(err) + } + if len(body) != 0 { + t.Fatal("response body should remain empty") + } +} + +func TestInspectResponseBinary(t *testing.T) { + ct := "application/octet-stream" + br := ioutil.NopCloser(bytes.NewReader(binaryContext)) + contentType, bReader, err := inspectResponse(ct, br, int64(len(binaryContext))) + if err != nil { + t.Fatal(err) + } + if contentType != "application/octet-stream" { + t.Fatalf("Content type should be 'application/octet-stream' but is %q", contentType) + } + body, err := ioutil.ReadAll(bReader) + if err != nil { + t.Fatal(err) + } + if len(body) != len(binaryContext) { + t.Fatalf("Wrong response size %d, should be == len(binaryContext)", len(body)) + } + for i := range body { + if body[i] != binaryContext[i] { + t.Fatalf("Corrupted response body at byte index %d", i) + } + } +} + +func TestResponseUnsupportedContentType(t *testing.T) { + content := []byte(textPlainDockerfile) + ct := "application/json" + br := ioutil.NopCloser(bytes.NewReader(content)) + contentType, bReader, err := inspectResponse(ct, br, int64(len(textPlainDockerfile))) + + if err == nil { + t.Fatal("Should have returned an error on content-type 'application/json'") + } + if contentType != ct { + t.Fatalf("Should not have altered content-type: orig: %s, altered: %s", ct, contentType) + } + body, err := ioutil.ReadAll(bReader) + if err != nil { + t.Fatal(err) + } + if string(body) != textPlainDockerfile { + t.Fatalf("Corrupted response body %s", body) + } +} + +func TestInspectResponseTextSimple(t *testing.T) { + content := []byte(textPlainDockerfile) + ct := "text/plain" + br := ioutil.NopCloser(bytes.NewReader(content)) + contentType, bReader, err := inspectResponse(ct, br, int64(len(content))) + if err != nil { + t.Fatal(err) + } + if contentType != "text/plain" { + t.Fatalf("Content type should be 'text/plain' but is %q", contentType) + } + body, err := ioutil.ReadAll(bReader) + if err != nil { + t.Fatal(err) + } + if string(body) != textPlainDockerfile { + t.Fatalf("Corrupted response body %s", body) + } +} + +func TestInspectResponseEmptyContentType(t *testing.T) { + content := []byte(textPlainDockerfile) + br := ioutil.NopCloser(bytes.NewReader(content)) + contentType, bodyReader, err := inspectResponse("", br, int64(len(content))) + if err != nil { + t.Fatal(err) + } + if contentType != "text/plain" { + t.Fatalf("Content type should be 'text/plain' but is %q", contentType) + } + body, err := ioutil.ReadAll(bodyReader) + if err != nil { + t.Fatal(err) + } + if string(body) != textPlainDockerfile { + t.Fatalf("Corrupted response body %s", body) + } +} diff --git a/builder/tarsum.go b/builder/tarsum.go new file mode 100644 index 00000000..201e3ef7 --- /dev/null +++ b/builder/tarsum.go @@ -0,0 +1,158 @@ +package builder + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/chrootarchive" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/symlink" + "github.com/docker/docker/pkg/tarsum" +) + +type tarSumContext struct { + root string + sums tarsum.FileInfoSums +} + +func (c *tarSumContext) Close() error { + return os.RemoveAll(c.root) +} + +func convertPathError(err error, cleanpath string) error { + if err, ok := err.(*os.PathError); ok { + err.Path = cleanpath + return err + } + return err +} + +func (c *tarSumContext) Open(path string) (io.ReadCloser, error) { + cleanpath, fullpath, err := c.normalize(path) + if err != nil { + return nil, err + } + r, err := os.Open(fullpath) + if err != nil { + return nil, convertPathError(err, cleanpath) + } + return r, nil +} + +func (c *tarSumContext) Stat(path string) (string, FileInfo, error) { + cleanpath, fullpath, err := c.normalize(path) + if err != nil { + return "", nil, err + } + + st, err := os.Lstat(fullpath) + if err != nil { + return "", nil, convertPathError(err, cleanpath) + } + + rel, err := filepath.Rel(c.root, fullpath) + if err != nil { + return "", nil, convertPathError(err, cleanpath) + } + + // We set sum to path by default for the case where GetFile returns nil. + // The usual case is if relative path is empty. + sum := path + // Use the checksum of the followed path(not the possible symlink) because + // this is the file that is actually copied. + if tsInfo := c.sums.GetFile(rel); tsInfo != nil { + sum = tsInfo.Sum() + } + fi := &HashedFileInfo{PathFileInfo{st, fullpath, filepath.Base(cleanpath)}, sum} + return rel, fi, nil +} + +// MakeTarSumContext returns a build Context from a tar stream. +// +// It extracts the tar stream to a temporary folder that is deleted as soon as +// the Context is closed. +// As the extraction happens, a tarsum is calculated for every file, and the set of +// all those sums then becomes the source of truth for all operations on this Context. +// +// Closing tarStream has to be done by the caller. +func MakeTarSumContext(tarStream io.Reader) (ModifiableContext, error) { + root, err := ioutils.TempDir("", "docker-builder") + if err != nil { + return nil, err + } + + tsc := &tarSumContext{root: root} + + // Make sure we clean-up upon error. In the happy case the caller + // is expected to manage the clean-up + defer func() { + if err != nil { + tsc.Close() + } + }() + + decompressedStream, err := archive.DecompressStream(tarStream) + if err != nil { + return nil, err + } + + sum, err := tarsum.NewTarSum(decompressedStream, true, tarsum.Version1) + if err != nil { + return nil, err + } + + if err := chrootarchive.Untar(sum, root, nil); err != nil { + return nil, err + } + + tsc.sums = sum.GetSums() + + return tsc, nil +} + +func (c *tarSumContext) normalize(path string) (cleanpath, fullpath string, err error) { + cleanpath = filepath.Clean(string(os.PathSeparator) + path)[1:] + fullpath, err = symlink.FollowSymlinkInScope(filepath.Join(c.root, path), c.root) + if err != nil { + return "", "", fmt.Errorf("Forbidden path outside the build context: %s (%s)", path, fullpath) + } + _, err = os.Lstat(fullpath) + if err != nil { + return "", "", convertPathError(err, path) + } + return +} + +func (c *tarSumContext) Walk(root string, walkFn WalkFunc) error { + root = filepath.Join(c.root, filepath.Join(string(filepath.Separator), root)) + return filepath.Walk(root, func(fullpath string, info os.FileInfo, err error) error { + rel, err := filepath.Rel(c.root, fullpath) + if err != nil { + return err + } + if rel == "." { + return nil + } + + sum := rel + if tsInfo := c.sums.GetFile(rel); tsInfo != nil { + sum = tsInfo.Sum() + } + fi := &HashedFileInfo{PathFileInfo{FileInfo: info, FilePath: fullpath}, sum} + if err := walkFn(rel, fi, nil); err != nil { + return err + } + return nil + }) +} + +func (c *tarSumContext) Remove(path string) error { + _, fullpath, err := c.normalize(path) + if err != nil { + return err + } + return os.RemoveAll(fullpath) +} diff --git a/cli/cli.go b/cli/cli.go new file mode 100644 index 00000000..8e559fc3 --- /dev/null +++ b/cli/cli.go @@ -0,0 +1,200 @@ +package cli + +import ( + "errors" + "fmt" + "io" + "os" + "reflect" + "strings" + + flag "github.com/docker/docker/pkg/mflag" +) + +// Cli represents a command line interface. +type Cli struct { + Stderr io.Writer + handlers []Handler + Usage func() +} + +// Handler holds the different commands Cli will call +// It should have methods with names starting with `Cmd` like: +// func (h myHandler) CmdFoo(args ...string) error +type Handler interface{} + +// Initializer can be optionally implemented by a Handler to +// initialize before each call to one of its commands. +type Initializer interface { + Initialize() error +} + +// New instantiates a ready-to-use Cli. +func New(handlers ...Handler) *Cli { + // make the generic Cli object the first cli handler + // in order to handle `docker help` appropriately + cli := new(Cli) + cli.handlers = append([]Handler{cli}, handlers...) + return cli +} + +// initErr is an error returned upon initialization of a handler implementing Initializer. +type initErr struct{ error } + +func (err initErr) Error() string { + return err.Error() +} + +func (cli *Cli) command(args ...string) (func(...string) error, error) { + for _, c := range cli.handlers { + if c == nil { + continue + } + camelArgs := make([]string, len(args)) + for i, s := range args { + if len(s) == 0 { + return nil, errors.New("empty command") + } + camelArgs[i] = strings.ToUpper(s[:1]) + strings.ToLower(s[1:]) + } + methodName := "Cmd" + strings.Join(camelArgs, "") + method := reflect.ValueOf(c).MethodByName(methodName) + if method.IsValid() { + if c, ok := c.(Initializer); ok { + if err := c.Initialize(); err != nil { + return nil, initErr{err} + } + } + return method.Interface().(func(...string) error), nil + } + } + return nil, errors.New("command not found") +} + +// Run executes the specified command. +func (cli *Cli) Run(args ...string) error { + if len(args) > 1 { + command, err := cli.command(args[:2]...) + switch err := err.(type) { + case nil: + return command(args[2:]...) + case initErr: + return err.error + } + } + if len(args) > 0 { + command, err := cli.command(args[0]) + switch err := err.(type) { + case nil: + return command(args[1:]...) + case initErr: + return err.error + } + cli.noSuchCommand(args[0]) + } + return cli.CmdHelp() +} + +func (cli *Cli) noSuchCommand(command string) { + if cli.Stderr == nil { + cli.Stderr = os.Stderr + } + fmt.Fprintf(cli.Stderr, "docker: '%s' is not a docker command.\nSee 'docker --help'.\n", command) + os.Exit(1) +} + +// CmdHelp displays information on a Docker command. +// +// If more than one command is specified, information is only shown for the first command. +// +// Usage: docker help COMMAND or docker COMMAND --help +func (cli *Cli) CmdHelp(args ...string) error { + if len(args) > 1 { + command, err := cli.command(args[:2]...) + switch err := err.(type) { + case nil: + command("--help") + return nil + case initErr: + return err.error + } + } + if len(args) > 0 { + command, err := cli.command(args[0]) + switch err := err.(type) { + case nil: + command("--help") + return nil + case initErr: + return err.error + } + cli.noSuchCommand(args[0]) + } + + if cli.Usage == nil { + flag.Usage() + } else { + cli.Usage() + } + + return nil +} + +// Subcmd is a subcommand of the main "docker" command. +// A subcommand represents an action that can be performed +// from the Docker command line client. +// +// To see all available subcommands, run "docker --help". +func Subcmd(name string, synopses []string, description string, exitOnError bool) *flag.FlagSet { + var errorHandling flag.ErrorHandling + if exitOnError { + errorHandling = flag.ExitOnError + } else { + errorHandling = flag.ContinueOnError + } + flags := flag.NewFlagSet(name, errorHandling) + flags.Usage = func() { + flags.ShortUsage() + flags.PrintDefaults() + } + + flags.ShortUsage = func() { + options := "" + if flags.FlagCountUndeprecated() > 0 { + options = " [OPTIONS]" + } + + if len(synopses) == 0 { + synopses = []string{""} + } + + // Allow for multiple command usage synopses. + for i, synopsis := range synopses { + lead := "\t" + if i == 0 { + // First line needs the word 'Usage'. + lead = "Usage:\t" + } + + if synopsis != "" { + synopsis = " " + synopsis + } + + fmt.Fprintf(flags.Out(), "\n%sdocker %s%s%s", lead, name, options, synopsis) + } + + fmt.Fprintf(flags.Out(), "\n\n%s\n", description) + } + + return flags +} + +// An StatusError reports an unsuccessful exit by a command. +type StatusError struct { + Status string + StatusCode int +} + +func (e StatusError) Error() string { + return fmt.Sprintf("Status: %s, Code: %d", e.Status, e.StatusCode) +} diff --git a/cli/client.go b/cli/client.go new file mode 100644 index 00000000..6a82eb52 --- /dev/null +++ b/cli/client.go @@ -0,0 +1,12 @@ +package cli + +import flag "github.com/docker/docker/pkg/mflag" + +// ClientFlags represents flags for the docker client. +type ClientFlags struct { + FlagSet *flag.FlagSet + Common *CommonFlags + PostParse func() + + ConfigDir string +} diff --git a/cli/common.go b/cli/common.go new file mode 100644 index 00000000..7f6a24ba --- /dev/null +++ b/cli/common.go @@ -0,0 +1,80 @@ +package cli + +import ( + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/go-connections/tlsconfig" +) + +// CommonFlags represents flags that are common to both the client and the daemon. +type CommonFlags struct { + FlagSet *flag.FlagSet + PostParse func() + + Debug bool + Hosts []string + LogLevel string + TLS bool + TLSVerify bool + TLSOptions *tlsconfig.Options + TrustKey string +} + +// Command is the struct containing the command name and description +type Command struct { + Name string + Description string +} + +var dockerCommands = []Command{ + {"attach", "Attach to a running container"}, + {"build", "Build an image from a Dockerfile"}, + {"commit", "Create a new image from a container's changes"}, + {"cp", "Copy files/folders between a container and the local filesystem"}, + {"create", "Create a new container"}, + {"diff", "Inspect changes on a container's filesystem"}, + {"events", "Get real time events from the server"}, + {"exec", "Run a command in a running container"}, + {"export", "Export a container's filesystem as a tar archive"}, + {"history", "Show the history of an image"}, + {"images", "List images"}, + {"import", "Import the contents from a tarball to create a filesystem image"}, + {"info", "Display system-wide information"}, + {"inspect", "Return low-level information on a container or image"}, + {"kill", "Kill a running container"}, + {"load", "Load an image from a tar archive or STDIN"}, + {"login", "Log in to a Docker registry"}, + {"logout", "Log out from a Docker registry"}, + {"logs", "Fetch the logs of a container"}, + {"network", "Manage Docker networks"}, + {"pause", "Pause all processes within a container"}, + {"port", "List port mappings or a specific mapping for the CONTAINER"}, + {"ps", "List containers"}, + {"pull", "Pull an image or a repository from a registry"}, + {"push", "Push an image or a repository to a registry"}, + {"rename", "Rename a container"}, + {"restart", "Restart a container"}, + {"rm", "Remove one or more containers"}, + {"rmi", "Remove one or more images"}, + {"run", "Run a command in a new container"}, + {"save", "Save one or more images to a tar archive"}, + {"search", "Search the Docker Hub for images"}, + {"start", "Start one or more stopped containers"}, + {"stats", "Display a live stream of container(s) resource usage statistics"}, + {"stop", "Stop a running container"}, + {"tag", "Tag an image into a repository"}, + {"top", "Display the running processes of a container"}, + {"unpause", "Unpause all processes within a container"}, + {"update", "Update configuration of one or more containers"}, + {"version", "Show the Docker version information"}, + {"volume", "Manage Docker volumes"}, + {"wait", "Block until a container stops, then print its exit code"}, +} + +// DockerCommands stores all the docker command +var DockerCommands = make(map[string]Command) + +func init() { + for _, cmd := range dockerCommands { + DockerCommands[cmd.Name] = cmd + } +} diff --git a/cliconfig/config.go b/cliconfig/config.go new file mode 100644 index 00000000..54e0ea38 --- /dev/null +++ b/cliconfig/config.go @@ -0,0 +1,289 @@ +package cliconfig + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/docker/docker/pkg/homedir" + "github.com/docker/engine-api/types" +) + +const ( + // ConfigFileName is the name of config file + ConfigFileName = "config.json" + configFileDir = ".docker" + oldConfigfile = ".dockercfg" + + // This constant is only used for really old config files when the + // URL wasn't saved as part of the config file and it was just + // assumed to be this value. + defaultIndexserver = "https://index.docker.io/v1/" +) + +var ( + configDir = os.Getenv("DOCKER_CONFIG") +) + +func init() { + if configDir == "" { + configDir = filepath.Join(homedir.Get(), configFileDir) + } +} + +// ConfigDir returns the directory the configuration file is stored in +func ConfigDir() string { + return configDir +} + +// SetConfigDir sets the directory the configuration file is stored in +func SetConfigDir(dir string) { + configDir = dir +} + +// ConfigFile ~/.docker/config.json file info +type ConfigFile struct { + AuthConfigs map[string]types.AuthConfig `json:"auths"` + HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"` + PsFormat string `json:"psFormat,omitempty"` + ImagesFormat string `json:"imagesFormat,omitempty"` + DetachKeys string `json:"detachKeys,omitempty"` + CredentialsStore string `json:"credsStore,omitempty"` + filename string // Note: not serialized - for internal use only +} + +// NewConfigFile initializes an empty configuration file for the given filename 'fn' +func NewConfigFile(fn string) *ConfigFile { + return &ConfigFile{ + AuthConfigs: make(map[string]types.AuthConfig), + HTTPHeaders: make(map[string]string), + filename: fn, + } +} + +// LegacyLoadFromReader reads the non-nested configuration data given and sets up the +// auth config information with given directory and populates the receiver object +func (configFile *ConfigFile) LegacyLoadFromReader(configData io.Reader) error { + b, err := ioutil.ReadAll(configData) + if err != nil { + return err + } + + if err := json.Unmarshal(b, &configFile.AuthConfigs); err != nil { + arr := strings.Split(string(b), "\n") + if len(arr) < 2 { + return fmt.Errorf("The Auth config file is empty") + } + authConfig := types.AuthConfig{} + origAuth := strings.Split(arr[0], " = ") + if len(origAuth) != 2 { + return fmt.Errorf("Invalid Auth config file") + } + authConfig.Username, authConfig.Password, err = decodeAuth(origAuth[1]) + if err != nil { + return err + } + authConfig.ServerAddress = defaultIndexserver + configFile.AuthConfigs[defaultIndexserver] = authConfig + } else { + for k, authConfig := range configFile.AuthConfigs { + authConfig.Username, authConfig.Password, err = decodeAuth(authConfig.Auth) + if err != nil { + return err + } + authConfig.Auth = "" + authConfig.ServerAddress = k + configFile.AuthConfigs[k] = authConfig + } + } + return nil +} + +// LoadFromReader reads the configuration data given and sets up the auth config +// information with given directory and populates the receiver object +func (configFile *ConfigFile) LoadFromReader(configData io.Reader) error { + if err := json.NewDecoder(configData).Decode(&configFile); err != nil { + return err + } + var err error + for addr, ac := range configFile.AuthConfigs { + ac.Username, ac.Password, err = decodeAuth(ac.Auth) + if err != nil { + return err + } + ac.Auth = "" + ac.ServerAddress = addr + configFile.AuthConfigs[addr] = ac + } + return nil +} + +// ContainsAuth returns whether there is authentication configured +// in this file or not. +func (configFile *ConfigFile) ContainsAuth() bool { + return configFile.CredentialsStore != "" || + (configFile.AuthConfigs != nil && len(configFile.AuthConfigs) > 0) +} + +// LegacyLoadFromReader is a convenience function that creates a ConfigFile object from +// a non-nested reader +func LegacyLoadFromReader(configData io.Reader) (*ConfigFile, error) { + configFile := ConfigFile{ + AuthConfigs: make(map[string]types.AuthConfig), + } + err := configFile.LegacyLoadFromReader(configData) + return &configFile, err +} + +// LoadFromReader is a convenience function that creates a ConfigFile object from +// a reader +func LoadFromReader(configData io.Reader) (*ConfigFile, error) { + configFile := ConfigFile{ + AuthConfigs: make(map[string]types.AuthConfig), + } + err := configFile.LoadFromReader(configData) + return &configFile, err +} + +// Load reads the configuration files in the given directory, and sets up +// the auth config information and return values. +// FIXME: use the internal golang config parser +func Load(configDir string) (*ConfigFile, error) { + if configDir == "" { + configDir = ConfigDir() + } + + configFile := ConfigFile{ + AuthConfigs: make(map[string]types.AuthConfig), + filename: filepath.Join(configDir, ConfigFileName), + } + + // Try happy path first - latest config file + if _, err := os.Stat(configFile.filename); err == nil { + file, err := os.Open(configFile.filename) + if err != nil { + return &configFile, fmt.Errorf("%s - %v", configFile.filename, err) + } + defer file.Close() + err = configFile.LoadFromReader(file) + if err != nil { + err = fmt.Errorf("%s - %v", configFile.filename, err) + } + return &configFile, err + } else if !os.IsNotExist(err) { + // if file is there but we can't stat it for any reason other + // than it doesn't exist then stop + return &configFile, fmt.Errorf("%s - %v", configFile.filename, err) + } + + // Can't find latest config file so check for the old one + confFile := filepath.Join(homedir.Get(), oldConfigfile) + if _, err := os.Stat(confFile); err != nil { + return &configFile, nil //missing file is not an error + } + file, err := os.Open(confFile) + if err != nil { + return &configFile, fmt.Errorf("%s - %v", confFile, err) + } + defer file.Close() + err = configFile.LegacyLoadFromReader(file) + if err != nil { + return &configFile, fmt.Errorf("%s - %v", confFile, err) + } + + if configFile.HTTPHeaders == nil { + configFile.HTTPHeaders = map[string]string{} + } + return &configFile, nil +} + +// SaveToWriter encodes and writes out all the authorization information to +// the given writer +func (configFile *ConfigFile) SaveToWriter(writer io.Writer) error { + // Encode sensitive data into a new/temp struct + tmpAuthConfigs := make(map[string]types.AuthConfig, len(configFile.AuthConfigs)) + for k, authConfig := range configFile.AuthConfigs { + authCopy := authConfig + // encode and save the authstring, while blanking out the original fields + authCopy.Auth = encodeAuth(&authCopy) + authCopy.Username = "" + authCopy.Password = "" + authCopy.ServerAddress = "" + tmpAuthConfigs[k] = authCopy + } + + saveAuthConfigs := configFile.AuthConfigs + configFile.AuthConfigs = tmpAuthConfigs + defer func() { configFile.AuthConfigs = saveAuthConfigs }() + + data, err := json.MarshalIndent(configFile, "", "\t") + if err != nil { + return err + } + _, err = writer.Write(data) + return err +} + +// Save encodes and writes out all the authorization information +func (configFile *ConfigFile) Save() error { + if configFile.Filename() == "" { + return fmt.Errorf("Can't save config with empty filename") + } + + if err := os.MkdirAll(filepath.Dir(configFile.filename), 0700); err != nil { + return err + } + f, err := os.OpenFile(configFile.filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer f.Close() + return configFile.SaveToWriter(f) +} + +// Filename returns the name of the configuration file +func (configFile *ConfigFile) Filename() string { + return configFile.filename +} + +// encodeAuth creates a base64 encoded string to containing authorization information +func encodeAuth(authConfig *types.AuthConfig) string { + if authConfig.Username == "" && authConfig.Password == "" { + return "" + } + + authStr := authConfig.Username + ":" + authConfig.Password + msg := []byte(authStr) + encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg))) + base64.StdEncoding.Encode(encoded, msg) + return string(encoded) +} + +// decodeAuth decodes a base64 encoded string and returns username and password +func decodeAuth(authStr string) (string, string, error) { + if authStr == "" { + return "", "", nil + } + + decLen := base64.StdEncoding.DecodedLen(len(authStr)) + decoded := make([]byte, decLen) + authByte := []byte(authStr) + n, err := base64.StdEncoding.Decode(decoded, authByte) + if err != nil { + return "", "", err + } + if n > decLen { + return "", "", fmt.Errorf("Something went wrong decoding auth config") + } + arr := strings.SplitN(string(decoded), ":", 2) + if len(arr) != 2 { + return "", "", fmt.Errorf("Invalid auth configuration file") + } + password := strings.Trim(arr[1], "\x00") + return arr[0], password, nil +} diff --git a/cliconfig/config_test.go b/cliconfig/config_test.go new file mode 100644 index 00000000..30c17770 --- /dev/null +++ b/cliconfig/config_test.go @@ -0,0 +1,565 @@ +package cliconfig + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/docker/docker/pkg/homedir" + "github.com/docker/engine-api/types" +) + +func TestEmptyConfigDir(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + SetConfigDir(tmpHome) + + config, err := Load("") + if err != nil { + t.Fatalf("Failed loading on empty config dir: %q", err) + } + + expectedConfigFilename := filepath.Join(tmpHome, ConfigFileName) + if config.Filename() != expectedConfigFilename { + t.Fatalf("Expected config filename %s, got %s", expectedConfigFilename, config.Filename()) + } + + // Now save it and make sure it shows up in new form + saveConfigAndValidateNewFormat(t, config, tmpHome) +} + +func TestMissingFile(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + config, err := Load(tmpHome) + if err != nil { + t.Fatalf("Failed loading on missing file: %q", err) + } + + // Now save it and make sure it shows up in new form + saveConfigAndValidateNewFormat(t, config, tmpHome) +} + +func TestSaveFileToDirs(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + tmpHome += "/.docker" + + config, err := Load(tmpHome) + if err != nil { + t.Fatalf("Failed loading on missing file: %q", err) + } + + // Now save it and make sure it shows up in new form + saveConfigAndValidateNewFormat(t, config, tmpHome) +} + +func TestEmptyFile(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + fn := filepath.Join(tmpHome, ConfigFileName) + if err := ioutil.WriteFile(fn, []byte(""), 0600); err != nil { + t.Fatal(err) + } + + _, err = Load(tmpHome) + if err == nil { + t.Fatalf("Was supposed to fail") + } +} + +func TestEmptyJson(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + fn := filepath.Join(tmpHome, ConfigFileName) + if err := ioutil.WriteFile(fn, []byte("{}"), 0600); err != nil { + t.Fatal(err) + } + + config, err := Load(tmpHome) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + // Now save it and make sure it shows up in new form + saveConfigAndValidateNewFormat(t, config, tmpHome) +} + +func TestOldInvalidsAuth(t *testing.T) { + invalids := map[string]string{ + `username = test`: "The Auth config file is empty", + `username +password`: "Invalid Auth config file", + `username = test +email`: "Invalid auth configuration file", + } + + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + homeKey := homedir.Key() + homeVal := homedir.Get() + + defer func() { os.Setenv(homeKey, homeVal) }() + os.Setenv(homeKey, tmpHome) + + for content, expectedError := range invalids { + fn := filepath.Join(tmpHome, oldConfigfile) + if err := ioutil.WriteFile(fn, []byte(content), 0600); err != nil { + t.Fatal(err) + } + + config, err := Load(tmpHome) + // Use Contains instead of == since the file name will change each time + if err == nil || !strings.Contains(err.Error(), expectedError) { + t.Fatalf("Should have failed\nConfig: %v\nGot: %v\nExpected: %v", config, err, expectedError) + } + + } +} + +func TestOldValidAuth(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + homeKey := homedir.Key() + homeVal := homedir.Get() + + defer func() { os.Setenv(homeKey, homeVal) }() + os.Setenv(homeKey, tmpHome) + + fn := filepath.Join(tmpHome, oldConfigfile) + js := `username = am9lam9lOmhlbGxv + email = user@example.com` + if err := ioutil.WriteFile(fn, []byte(js), 0600); err != nil { + t.Fatal(err) + } + + config, err := Load(tmpHome) + if err != nil { + t.Fatal(err) + } + + // defaultIndexserver is https://index.docker.io/v1/ + ac := config.AuthConfigs["https://index.docker.io/v1/"] + if ac.Username != "joejoe" || ac.Password != "hello" { + t.Fatalf("Missing data from parsing:\n%q", config) + } + + // Now save it and make sure it shows up in new form + configStr := saveConfigAndValidateNewFormat(t, config, tmpHome) + + expConfStr := `{ + "auths": { + "https://index.docker.io/v1/": { + "auth": "am9lam9lOmhlbGxv" + } + } +}` + + if configStr != expConfStr { + t.Fatalf("Should have save in new form: \n%s\n not \n%s", configStr, expConfStr) + } +} + +func TestOldJsonInvalid(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + homeKey := homedir.Key() + homeVal := homedir.Get() + + defer func() { os.Setenv(homeKey, homeVal) }() + os.Setenv(homeKey, tmpHome) + + fn := filepath.Join(tmpHome, oldConfigfile) + js := `{"https://index.docker.io/v1/":{"auth":"test","email":"user@example.com"}}` + if err := ioutil.WriteFile(fn, []byte(js), 0600); err != nil { + t.Fatal(err) + } + + config, err := Load(tmpHome) + // Use Contains instead of == since the file name will change each time + if err == nil || !strings.Contains(err.Error(), "Invalid auth configuration file") { + t.Fatalf("Expected an error got : %v, %v", config, err) + } +} + +func TestOldJson(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + homeKey := homedir.Key() + homeVal := homedir.Get() + + defer func() { os.Setenv(homeKey, homeVal) }() + os.Setenv(homeKey, tmpHome) + + fn := filepath.Join(tmpHome, oldConfigfile) + js := `{"https://index.docker.io/v1/":{"auth":"am9lam9lOmhlbGxv","email":"user@example.com"}}` + if err := ioutil.WriteFile(fn, []byte(js), 0600); err != nil { + t.Fatal(err) + } + + config, err := Load(tmpHome) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + ac := config.AuthConfigs["https://index.docker.io/v1/"] + if ac.Username != "joejoe" || ac.Password != "hello" { + t.Fatalf("Missing data from parsing:\n%q", config) + } + + // Now save it and make sure it shows up in new form + configStr := saveConfigAndValidateNewFormat(t, config, tmpHome) + + expConfStr := `{ + "auths": { + "https://index.docker.io/v1/": { + "auth": "am9lam9lOmhlbGxv", + "email": "user@example.com" + } + } +}` + + if configStr != expConfStr { + t.Fatalf("Should have save in new form: \n'%s'\n not \n'%s'\n", configStr, expConfStr) + } +} + +func TestNewJson(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + fn := filepath.Join(tmpHome, ConfigFileName) + js := ` { "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv" } } }` + if err := ioutil.WriteFile(fn, []byte(js), 0600); err != nil { + t.Fatal(err) + } + + config, err := Load(tmpHome) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + ac := config.AuthConfigs["https://index.docker.io/v1/"] + if ac.Username != "joejoe" || ac.Password != "hello" { + t.Fatalf("Missing data from parsing:\n%q", config) + } + + // Now save it and make sure it shows up in new form + configStr := saveConfigAndValidateNewFormat(t, config, tmpHome) + + expConfStr := `{ + "auths": { + "https://index.docker.io/v1/": { + "auth": "am9lam9lOmhlbGxv" + } + } +}` + + if configStr != expConfStr { + t.Fatalf("Should have save in new form: \n%s\n not \n%s", configStr, expConfStr) + } +} + +func TestNewJsonNoEmail(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + fn := filepath.Join(tmpHome, ConfigFileName) + js := ` { "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv" } } }` + if err := ioutil.WriteFile(fn, []byte(js), 0600); err != nil { + t.Fatal(err) + } + + config, err := Load(tmpHome) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + ac := config.AuthConfigs["https://index.docker.io/v1/"] + if ac.Username != "joejoe" || ac.Password != "hello" { + t.Fatalf("Missing data from parsing:\n%q", config) + } + + // Now save it and make sure it shows up in new form + configStr := saveConfigAndValidateNewFormat(t, config, tmpHome) + + expConfStr := `{ + "auths": { + "https://index.docker.io/v1/": { + "auth": "am9lam9lOmhlbGxv" + } + } +}` + + if configStr != expConfStr { + t.Fatalf("Should have save in new form: \n%s\n not \n%s", configStr, expConfStr) + } +} + +func TestJsonWithPsFormat(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + fn := filepath.Join(tmpHome, ConfigFileName) + js := `{ + "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } }, + "psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}" +}` + if err := ioutil.WriteFile(fn, []byte(js), 0600); err != nil { + t.Fatal(err) + } + + config, err := Load(tmpHome) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + if config.PsFormat != `table {{.ID}}\t{{.Label "com.docker.label.cpu"}}` { + t.Fatalf("Unknown ps format: %s\n", config.PsFormat) + } + + // Now save it and make sure it shows up in new form + configStr := saveConfigAndValidateNewFormat(t, config, tmpHome) + if !strings.Contains(configStr, `"psFormat":`) || + !strings.Contains(configStr, "{{.ID}}") { + t.Fatalf("Should have save in new form: %s", configStr) + } +} + +// Save it and make sure it shows up in new form +func saveConfigAndValidateNewFormat(t *testing.T, config *ConfigFile, homeFolder string) string { + if err := config.Save(); err != nil { + t.Fatalf("Failed to save: %q", err) + } + + buf, err := ioutil.ReadFile(filepath.Join(homeFolder, ConfigFileName)) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(buf), `"auths":`) { + t.Fatalf("Should have save in new form: %s", string(buf)) + } + return string(buf) +} + +func TestConfigDir(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + if ConfigDir() == tmpHome { + t.Fatalf("Expected ConfigDir to be different than %s by default, but was the same", tmpHome) + } + + // Update configDir + SetConfigDir(tmpHome) + + if ConfigDir() != tmpHome { + t.Fatalf("Expected ConfigDir to %s, but was %s", tmpHome, ConfigDir()) + } +} + +func TestConfigFile(t *testing.T) { + configFilename := "configFilename" + configFile := NewConfigFile(configFilename) + + if configFile.Filename() != configFilename { + t.Fatalf("Expected %s, got %s", configFilename, configFile.Filename()) + } +} + +func TestJsonReaderNoFile(t *testing.T) { + js := ` { "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } } }` + + config, err := LoadFromReader(strings.NewReader(js)) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + ac := config.AuthConfigs["https://index.docker.io/v1/"] + if ac.Username != "joejoe" || ac.Password != "hello" { + t.Fatalf("Missing data from parsing:\n%q", config) + } + +} + +func TestOldJsonReaderNoFile(t *testing.T) { + js := `{"https://index.docker.io/v1/":{"auth":"am9lam9lOmhlbGxv","email":"user@example.com"}}` + + config, err := LegacyLoadFromReader(strings.NewReader(js)) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + ac := config.AuthConfigs["https://index.docker.io/v1/"] + if ac.Username != "joejoe" || ac.Password != "hello" { + t.Fatalf("Missing data from parsing:\n%q", config) + } +} + +func TestJsonWithPsFormatNoFile(t *testing.T) { + js := `{ + "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } }, + "psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}" +}` + config, err := LoadFromReader(strings.NewReader(js)) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + if config.PsFormat != `table {{.ID}}\t{{.Label "com.docker.label.cpu"}}` { + t.Fatalf("Unknown ps format: %s\n", config.PsFormat) + } + +} + +func TestJsonSaveWithNoFile(t *testing.T) { + js := `{ + "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv" } }, + "psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}" +}` + config, err := LoadFromReader(strings.NewReader(js)) + err = config.Save() + if err == nil { + t.Fatalf("Expected error. File should not have been able to save with no file name.") + } + + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatalf("Failed to create a temp dir: %q", err) + } + defer os.RemoveAll(tmpHome) + + fn := filepath.Join(tmpHome, ConfigFileName) + f, _ := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + err = config.SaveToWriter(f) + if err != nil { + t.Fatalf("Failed saving to file: %q", err) + } + buf, err := ioutil.ReadFile(filepath.Join(tmpHome, ConfigFileName)) + if err != nil { + t.Fatal(err) + } + expConfStr := `{ + "auths": { + "https://index.docker.io/v1/": { + "auth": "am9lam9lOmhlbGxv" + } + }, + "psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}" +}` + if string(buf) != expConfStr { + t.Fatalf("Should have save in new form: \n%s\nnot \n%s", string(buf), expConfStr) + } +} + +func TestLegacyJsonSaveWithNoFile(t *testing.T) { + + js := `{"https://index.docker.io/v1/":{"auth":"am9lam9lOmhlbGxv","email":"user@example.com"}}` + config, err := LegacyLoadFromReader(strings.NewReader(js)) + err = config.Save() + if err == nil { + t.Fatalf("Expected error. File should not have been able to save with no file name.") + } + + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatalf("Failed to create a temp dir: %q", err) + } + defer os.RemoveAll(tmpHome) + + fn := filepath.Join(tmpHome, ConfigFileName) + f, _ := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err = config.SaveToWriter(f); err != nil { + t.Fatalf("Failed saving to file: %q", err) + } + buf, err := ioutil.ReadFile(filepath.Join(tmpHome, ConfigFileName)) + if err != nil { + t.Fatal(err) + } + + expConfStr := `{ + "auths": { + "https://index.docker.io/v1/": { + "auth": "am9lam9lOmhlbGxv", + "email": "user@example.com" + } + } +}` + + if string(buf) != expConfStr { + t.Fatalf("Should have save in new form: \n%s\n not \n%s", string(buf), expConfStr) + } +} + +func TestEncodeAuth(t *testing.T) { + newAuthConfig := &types.AuthConfig{Username: "ken", Password: "test"} + authStr := encodeAuth(newAuthConfig) + decAuthConfig := &types.AuthConfig{} + var err error + decAuthConfig.Username, decAuthConfig.Password, err = decodeAuth(authStr) + if err != nil { + t.Fatal(err) + } + if newAuthConfig.Username != decAuthConfig.Username { + t.Fatal("Encode Username doesn't match decoded Username") + } + if newAuthConfig.Password != decAuthConfig.Password { + t.Fatal("Encode Password doesn't match decoded Password") + } + if authStr != "a2VuOnRlc3Q=" { + t.Fatal("AuthString encoding isn't correct.") + } +} diff --git a/cliconfig/credentials/credentials.go b/cliconfig/credentials/credentials.go new file mode 100644 index 00000000..510cf8cf --- /dev/null +++ b/cliconfig/credentials/credentials.go @@ -0,0 +1,17 @@ +package credentials + +import ( + "github.com/docker/engine-api/types" +) + +// Store is the interface that any credentials store must implement. +type Store interface { + // Erase removes credentials from the store for a given server. + Erase(serverAddress string) error + // Get retrieves credentials from the store for a given server. + Get(serverAddress string) (types.AuthConfig, error) + // GetAll retrieves all the credentials from the store. + GetAll() (map[string]types.AuthConfig, error) + // Store saves credentials in the store. + Store(authConfig types.AuthConfig) error +} diff --git a/cliconfig/credentials/default_store.go b/cliconfig/credentials/default_store.go new file mode 100644 index 00000000..b5fc47cc --- /dev/null +++ b/cliconfig/credentials/default_store.go @@ -0,0 +1,22 @@ +package credentials + +import ( + "os/exec" + + "github.com/docker/docker/cliconfig" +) + +// DetectDefaultStore sets the default credentials store +// if the host includes the default store helper program. +func DetectDefaultStore(c *cliconfig.ConfigFile) { + if c.CredentialsStore != "" { + // user defined + return + } + + if defaultCredentialsStore != "" { + if _, err := exec.LookPath(remoteCredentialsPrefix + defaultCredentialsStore); err == nil { + c.CredentialsStore = defaultCredentialsStore + } + } +} diff --git a/cliconfig/credentials/default_store_darwin.go b/cliconfig/credentials/default_store_darwin.go new file mode 100644 index 00000000..63e8ed40 --- /dev/null +++ b/cliconfig/credentials/default_store_darwin.go @@ -0,0 +1,3 @@ +package credentials + +const defaultCredentialsStore = "osxkeychain" diff --git a/cliconfig/credentials/default_store_linux.go b/cliconfig/credentials/default_store_linux.go new file mode 100644 index 00000000..864c540f --- /dev/null +++ b/cliconfig/credentials/default_store_linux.go @@ -0,0 +1,3 @@ +package credentials + +const defaultCredentialsStore = "secretservice" diff --git a/cliconfig/credentials/default_store_unsupported.go b/cliconfig/credentials/default_store_unsupported.go new file mode 100644 index 00000000..519ef53d --- /dev/null +++ b/cliconfig/credentials/default_store_unsupported.go @@ -0,0 +1,5 @@ +// +build !windows,!darwin,!linux + +package credentials + +const defaultCredentialsStore = "" diff --git a/cliconfig/credentials/default_store_windows.go b/cliconfig/credentials/default_store_windows.go new file mode 100644 index 00000000..fb6a9745 --- /dev/null +++ b/cliconfig/credentials/default_store_windows.go @@ -0,0 +1,3 @@ +package credentials + +const defaultCredentialsStore = "wincred" diff --git a/cliconfig/credentials/file_store.go b/cliconfig/credentials/file_store.go new file mode 100644 index 00000000..8e7edd62 --- /dev/null +++ b/cliconfig/credentials/file_store.go @@ -0,0 +1,67 @@ +package credentials + +import ( + "strings" + + "github.com/docker/docker/cliconfig" + "github.com/docker/engine-api/types" +) + +// fileStore implements a credentials store using +// the docker configuration file to keep the credentials in plain text. +type fileStore struct { + file *cliconfig.ConfigFile +} + +// NewFileStore creates a new file credentials store. +func NewFileStore(file *cliconfig.ConfigFile) Store { + return &fileStore{ + file: file, + } +} + +// Erase removes the given credentials from the file store. +func (c *fileStore) Erase(serverAddress string) error { + delete(c.file.AuthConfigs, serverAddress) + return c.file.Save() +} + +// Get retrieves credentials for a specific server from the file store. +func (c *fileStore) Get(serverAddress string) (types.AuthConfig, error) { + authConfig, ok := c.file.AuthConfigs[serverAddress] + if !ok { + // Maybe they have a legacy config file, we will iterate the keys converting + // them to the new format and testing + for registry, ac := range c.file.AuthConfigs { + if serverAddress == convertToHostname(registry) { + return ac, nil + } + } + + authConfig = types.AuthConfig{} + } + return authConfig, nil +} + +func (c *fileStore) GetAll() (map[string]types.AuthConfig, error) { + return c.file.AuthConfigs, nil +} + +// Store saves the given credentials in the file store. +func (c *fileStore) Store(authConfig types.AuthConfig) error { + c.file.AuthConfigs[authConfig.ServerAddress] = authConfig + return c.file.Save() +} + +func convertToHostname(url string) string { + stripped := url + if strings.HasPrefix(url, "http://") { + stripped = strings.Replace(url, "http://", "", 1) + } else if strings.HasPrefix(url, "https://") { + stripped = strings.Replace(url, "https://", "", 1) + } + + nameParts := strings.SplitN(stripped, "/", 2) + + return nameParts[0] +} diff --git a/cliconfig/credentials/file_store_test.go b/cliconfig/credentials/file_store_test.go new file mode 100644 index 00000000..668b6f09 --- /dev/null +++ b/cliconfig/credentials/file_store_test.go @@ -0,0 +1,138 @@ +package credentials + +import ( + "io/ioutil" + "testing" + + "github.com/docker/docker/cliconfig" + "github.com/docker/engine-api/types" +) + +func newConfigFile(auths map[string]types.AuthConfig) *cliconfig.ConfigFile { + tmp, _ := ioutil.TempFile("", "docker-test") + name := tmp.Name() + tmp.Close() + + c := cliconfig.NewConfigFile(name) + c.AuthConfigs = auths + return c +} + +func TestFileStoreAddCredentials(t *testing.T) { + f := newConfigFile(make(map[string]types.AuthConfig)) + + s := NewFileStore(f) + err := s.Store(types.AuthConfig{ + Auth: "super_secret_token", + Email: "foo@example.com", + ServerAddress: "https://example.com", + }) + + if err != nil { + t.Fatal(err) + } + + if len(f.AuthConfigs) != 1 { + t.Fatalf("expected 1 auth config, got %d", len(f.AuthConfigs)) + } + + a, ok := f.AuthConfigs["https://example.com"] + if !ok { + t.Fatalf("expected auth for https://example.com, got %v", f.AuthConfigs) + } + if a.Auth != "super_secret_token" { + t.Fatalf("expected auth `super_secret_token`, got %s", a.Auth) + } + if a.Email != "foo@example.com" { + t.Fatalf("expected email `foo@example.com`, got %s", a.Email) + } +} + +func TestFileStoreGet(t *testing.T) { + f := newConfigFile(map[string]types.AuthConfig{ + "https://example.com": { + Auth: "super_secret_token", + Email: "foo@example.com", + ServerAddress: "https://example.com", + }, + }) + + s := NewFileStore(f) + a, err := s.Get("https://example.com") + if err != nil { + t.Fatal(err) + } + if a.Auth != "super_secret_token" { + t.Fatalf("expected auth `super_secret_token`, got %s", a.Auth) + } + if a.Email != "foo@example.com" { + t.Fatalf("expected email `foo@example.com`, got %s", a.Email) + } +} + +func TestFileStoreGetAll(t *testing.T) { + s1 := "https://example.com" + s2 := "https://example2.com" + f := newConfigFile(map[string]types.AuthConfig{ + s1: { + Auth: "super_secret_token", + Email: "foo@example.com", + ServerAddress: "https://example.com", + }, + s2: { + Auth: "super_secret_token2", + Email: "foo@example2.com", + ServerAddress: "https://example2.com", + }, + }) + + s := NewFileStore(f) + as, err := s.GetAll() + if err != nil { + t.Fatal(err) + } + if len(as) != 2 { + t.Fatalf("wanted 2, got %d", len(as)) + } + if as[s1].Auth != "super_secret_token" { + t.Fatalf("expected auth `super_secret_token`, got %s", as[s1].Auth) + } + if as[s1].Email != "foo@example.com" { + t.Fatalf("expected email `foo@example.com`, got %s", as[s1].Email) + } + if as[s2].Auth != "super_secret_token2" { + t.Fatalf("expected auth `super_secret_token2`, got %s", as[s2].Auth) + } + if as[s2].Email != "foo@example2.com" { + t.Fatalf("expected email `foo@example2.com`, got %s", as[s2].Email) + } +} + +func TestFileStoreErase(t *testing.T) { + f := newConfigFile(map[string]types.AuthConfig{ + "https://example.com": { + Auth: "super_secret_token", + Email: "foo@example.com", + ServerAddress: "https://example.com", + }, + }) + + s := NewFileStore(f) + err := s.Erase("https://example.com") + if err != nil { + t.Fatal(err) + } + + // file store never returns errors, check that the auth config is empty + a, err := s.Get("https://example.com") + if err != nil { + t.Fatal(err) + } + + if a.Auth != "" { + t.Fatalf("expected empty auth token, got %s", a.Auth) + } + if a.Email != "" { + t.Fatalf("expected empty email, got %s", a.Email) + } +} diff --git a/cliconfig/credentials/native_store.go b/cliconfig/credentials/native_store.go new file mode 100644 index 00000000..9b8997dd --- /dev/null +++ b/cliconfig/credentials/native_store.go @@ -0,0 +1,196 @@ +package credentials + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/cliconfig" + "github.com/docker/engine-api/types" +) + +const ( + remoteCredentialsPrefix = "docker-credential-" + tokenUsername = "" +) + +// Standarize the not found error, so every helper returns +// the same message and docker can handle it properly. +var errCredentialsNotFound = errors.New("credentials not found in native keychain") + +// command is an interface that remote executed commands implement. +type command interface { + Output() ([]byte, error) + Input(in io.Reader) +} + +// credentialsRequest holds information shared between docker and a remote credential store. +type credentialsRequest struct { + ServerURL string + Username string + Secret string +} + +// credentialsGetResponse is the information serialized from a remote store +// when the plugin sends requests to get the user credentials. +type credentialsGetResponse struct { + Username string + Secret string +} + +// nativeStore implements a credentials store +// using native keychain to keep credentials secure. +// It piggybacks into a file store to keep users' emails. +type nativeStore struct { + commandFn func(args ...string) command + fileStore Store +} + +// NewNativeStore creates a new native store that +// uses a remote helper program to manage credentials. +func NewNativeStore(file *cliconfig.ConfigFile) Store { + return &nativeStore{ + commandFn: shellCommandFn(file.CredentialsStore), + fileStore: NewFileStore(file), + } +} + +// Erase removes the given credentials from the native store. +func (c *nativeStore) Erase(serverAddress string) error { + if err := c.eraseCredentialsFromStore(serverAddress); err != nil { + return err + } + + // Fallback to plain text store to remove email + return c.fileStore.Erase(serverAddress) +} + +// Get retrieves credentials for a specific server from the native store. +func (c *nativeStore) Get(serverAddress string) (types.AuthConfig, error) { + // load user email if it exist or an empty auth config. + auth, _ := c.fileStore.Get(serverAddress) + + creds, err := c.getCredentialsFromStore(serverAddress) + if err != nil { + return auth, err + } + auth.Username = creds.Username + auth.IdentityToken = creds.IdentityToken + auth.Password = creds.Password + + return auth, nil +} + +// GetAll retrieves all the credentials from the native store. +func (c *nativeStore) GetAll() (map[string]types.AuthConfig, error) { + auths, _ := c.fileStore.GetAll() + + for s, ac := range auths { + creds, _ := c.getCredentialsFromStore(s) + ac.Username = creds.Username + ac.Password = creds.Password + ac.IdentityToken = creds.IdentityToken + auths[s] = ac + } + + return auths, nil +} + +// Store saves the given credentials in the file store. +func (c *nativeStore) Store(authConfig types.AuthConfig) error { + if err := c.storeCredentialsInStore(authConfig); err != nil { + return err + } + authConfig.Username = "" + authConfig.Password = "" + authConfig.IdentityToken = "" + + // Fallback to old credential in plain text to save only the email + return c.fileStore.Store(authConfig) +} + +// storeCredentialsInStore executes the command to store the credentials in the native store. +func (c *nativeStore) storeCredentialsInStore(config types.AuthConfig) error { + cmd := c.commandFn("store") + creds := &credentialsRequest{ + ServerURL: config.ServerAddress, + Username: config.Username, + Secret: config.Password, + } + + if config.IdentityToken != "" { + creds.Username = tokenUsername + creds.Secret = config.IdentityToken + } + + buffer := new(bytes.Buffer) + if err := json.NewEncoder(buffer).Encode(creds); err != nil { + return err + } + cmd.Input(buffer) + + out, err := cmd.Output() + if err != nil { + t := strings.TrimSpace(string(out)) + logrus.Debugf("error adding credentials - err: %v, out: `%s`", err, t) + return fmt.Errorf(t) + } + + return nil +} + +// getCredentialsFromStore executes the command to get the credentials from the native store. +func (c *nativeStore) getCredentialsFromStore(serverAddress string) (types.AuthConfig, error) { + var ret types.AuthConfig + + cmd := c.commandFn("get") + cmd.Input(strings.NewReader(serverAddress)) + + out, err := cmd.Output() + if err != nil { + t := strings.TrimSpace(string(out)) + + // do not return an error if the credentials are not + // in the keyckain. Let docker ask for new credentials. + if t == errCredentialsNotFound.Error() { + return ret, nil + } + + logrus.Debugf("error getting credentials - err: %v, out: `%s`", err, t) + return ret, fmt.Errorf(t) + } + + var resp credentialsGetResponse + if err := json.NewDecoder(bytes.NewReader(out)).Decode(&resp); err != nil { + return ret, err + } + + if resp.Username == tokenUsername { + ret.IdentityToken = resp.Secret + } else { + ret.Password = resp.Secret + ret.Username = resp.Username + } + + ret.ServerAddress = serverAddress + return ret, nil +} + +// eraseCredentialsFromStore executes the command to remove the server credentails from the native store. +func (c *nativeStore) eraseCredentialsFromStore(serverURL string) error { + cmd := c.commandFn("erase") + cmd.Input(strings.NewReader(serverURL)) + + out, err := cmd.Output() + if err != nil { + t := strings.TrimSpace(string(out)) + logrus.Debugf("error erasing credentials - err: %v, out: `%s`", err, t) + return fmt.Errorf(t) + } + + return nil +} diff --git a/cliconfig/credentials/native_store_test.go b/cliconfig/credentials/native_store_test.go new file mode 100644 index 00000000..35422102 --- /dev/null +++ b/cliconfig/credentials/native_store_test.go @@ -0,0 +1,354 @@ +package credentials + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "strings" + "testing" + + "github.com/docker/engine-api/types" +) + +const ( + validServerAddress = "https://index.docker.io/v1" + validServerAddress2 = "https://example.com:5002" + invalidServerAddress = "https://foobar.example.com" + missingCredsAddress = "https://missing.docker.io/v1" +) + +var errCommandExited = fmt.Errorf("exited 1") + +// mockCommand simulates interactions between the docker client and a remote +// credentials helper. +// Unit tests inject this mocked command into the remote to control execution. +type mockCommand struct { + arg string + input io.Reader +} + +// Output returns responses from the remote credentials helper. +// It mocks those reponses based in the input in the mock. +func (m *mockCommand) Output() ([]byte, error) { + in, err := ioutil.ReadAll(m.input) + if err != nil { + return nil, err + } + inS := string(in) + + switch m.arg { + case "erase": + switch inS { + case validServerAddress: + return nil, nil + default: + return []byte("error erasing credentials"), errCommandExited + } + case "get": + switch inS { + case validServerAddress: + return []byte(`{"Username": "foo", "Secret": "bar"}`), nil + case validServerAddress2: + return []byte(`{"Username": "", "Secret": "abcd1234"}`), nil + case missingCredsAddress: + return []byte(errCredentialsNotFound.Error()), errCommandExited + case invalidServerAddress: + return []byte("error getting credentials"), errCommandExited + } + case "store": + var c credentialsRequest + err := json.NewDecoder(strings.NewReader(inS)).Decode(&c) + if err != nil { + return []byte("error storing credentials"), errCommandExited + } + switch c.ServerURL { + case validServerAddress: + return nil, nil + default: + return []byte("error storing credentials"), errCommandExited + } + } + + return []byte(fmt.Sprintf("unknown argument %q with %q", m.arg, inS)), errCommandExited +} + +// Input sets the input to send to a remote credentials helper. +func (m *mockCommand) Input(in io.Reader) { + m.input = in +} + +func mockCommandFn(args ...string) command { + return &mockCommand{ + arg: args[0], + } +} + +func TestNativeStoreAddCredentials(t *testing.T) { + f := newConfigFile(make(map[string]types.AuthConfig)) + f.CredentialsStore = "mock" + + s := &nativeStore{ + commandFn: mockCommandFn, + fileStore: NewFileStore(f), + } + err := s.Store(types.AuthConfig{ + Username: "foo", + Password: "bar", + Email: "foo@example.com", + ServerAddress: validServerAddress, + }) + + if err != nil { + t.Fatal(err) + } + + if len(f.AuthConfigs) != 1 { + t.Fatalf("expected 1 auth config, got %d", len(f.AuthConfigs)) + } + + a, ok := f.AuthConfigs[validServerAddress] + if !ok { + t.Fatalf("expected auth for %s, got %v", validServerAddress, f.AuthConfigs) + } + if a.Auth != "" { + t.Fatalf("expected auth to be empty, got %s", a.Auth) + } + if a.Username != "" { + t.Fatalf("expected username to be empty, got %s", a.Username) + } + if a.Password != "" { + t.Fatalf("expected password to be empty, got %s", a.Password) + } + if a.IdentityToken != "" { + t.Fatalf("expected identity token to be empty, got %s", a.IdentityToken) + } + if a.Email != "foo@example.com" { + t.Fatalf("expected email `foo@example.com`, got %s", a.Email) + } +} + +func TestNativeStoreAddInvalidCredentials(t *testing.T) { + f := newConfigFile(make(map[string]types.AuthConfig)) + f.CredentialsStore = "mock" + + s := &nativeStore{ + commandFn: mockCommandFn, + fileStore: NewFileStore(f), + } + err := s.Store(types.AuthConfig{ + Username: "foo", + Password: "bar", + Email: "foo@example.com", + ServerAddress: invalidServerAddress, + }) + + if err == nil { + t.Fatal("expected error, got nil") + } + + if err.Error() != "error storing credentials" { + t.Fatalf("expected `error storing credentials`, got %v", err) + } + + if len(f.AuthConfigs) != 0 { + t.Fatalf("expected 0 auth config, got %d", len(f.AuthConfigs)) + } +} + +func TestNativeStoreGet(t *testing.T) { + f := newConfigFile(map[string]types.AuthConfig{ + validServerAddress: { + Email: "foo@example.com", + }, + }) + f.CredentialsStore = "mock" + + s := &nativeStore{ + commandFn: mockCommandFn, + fileStore: NewFileStore(f), + } + a, err := s.Get(validServerAddress) + if err != nil { + t.Fatal(err) + } + + if a.Username != "foo" { + t.Fatalf("expected username `foo`, got %s", a.Username) + } + if a.Password != "bar" { + t.Fatalf("expected password `bar`, got %s", a.Password) + } + if a.IdentityToken != "" { + t.Fatalf("expected identity token to be empty, got %s", a.IdentityToken) + } + if a.Email != "foo@example.com" { + t.Fatalf("expected email `foo@example.com`, got %s", a.Email) + } +} + +func TestNativeStoreGetIdentityToken(t *testing.T) { + f := newConfigFile(map[string]types.AuthConfig{ + validServerAddress2: { + Email: "foo@example2.com", + }, + }) + f.CredentialsStore = "mock" + + s := &nativeStore{ + commandFn: mockCommandFn, + fileStore: NewFileStore(f), + } + a, err := s.Get(validServerAddress2) + if err != nil { + t.Fatal(err) + } + + if a.Username != "" { + t.Fatalf("expected username to be empty, got %s", a.Username) + } + if a.Password != "" { + t.Fatalf("expected password to be empty, got %s", a.Password) + } + if a.IdentityToken != "abcd1234" { + t.Fatalf("expected identity token `abcd1234`, got %s", a.IdentityToken) + } + if a.Email != "foo@example2.com" { + t.Fatalf("expected email `foo@example2.com`, got %s", a.Email) + } +} + +func TestNativeStoreGetAll(t *testing.T) { + f := newConfigFile(map[string]types.AuthConfig{ + validServerAddress: { + Email: "foo@example.com", + }, + validServerAddress2: { + Email: "foo@example2.com", + }, + }) + f.CredentialsStore = "mock" + + s := &nativeStore{ + commandFn: mockCommandFn, + fileStore: NewFileStore(f), + } + as, err := s.GetAll() + if err != nil { + t.Fatal(err) + } + + if len(as) != 2 { + t.Fatalf("wanted 2, got %d", len(as)) + } + + if as[validServerAddress].Username != "foo" { + t.Fatalf("expected username `foo` for %s, got %s", validServerAddress, as[validServerAddress].Username) + } + if as[validServerAddress].Password != "bar" { + t.Fatalf("expected password `bar` for %s, got %s", validServerAddress, as[validServerAddress].Password) + } + if as[validServerAddress].IdentityToken != "" { + t.Fatalf("expected identity to be empty for %s, got %s", validServerAddress, as[validServerAddress].IdentityToken) + } + if as[validServerAddress].Email != "foo@example.com" { + t.Fatalf("expected email `foo@example.com` for %s, got %s", validServerAddress, as[validServerAddress].Email) + } + if as[validServerAddress2].Username != "" { + t.Fatalf("expected username to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Username) + } + if as[validServerAddress2].Password != "" { + t.Fatalf("expected password to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Password) + } + if as[validServerAddress2].IdentityToken != "abcd1234" { + t.Fatalf("expected identity token `abcd1324` for %s, got %s", validServerAddress2, as[validServerAddress2].IdentityToken) + } + if as[validServerAddress2].Email != "foo@example2.com" { + t.Fatalf("expected email `foo@example2.com` for %s, got %s", validServerAddress2, as[validServerAddress2].Email) + } +} + +func TestNativeStoreGetMissingCredentials(t *testing.T) { + f := newConfigFile(map[string]types.AuthConfig{ + validServerAddress: { + Email: "foo@example.com", + }, + }) + f.CredentialsStore = "mock" + + s := &nativeStore{ + commandFn: mockCommandFn, + fileStore: NewFileStore(f), + } + _, err := s.Get(missingCredsAddress) + if err != nil { + // missing credentials do not produce an error + t.Fatal(err) + } +} + +func TestNativeStoreGetInvalidAddress(t *testing.T) { + f := newConfigFile(map[string]types.AuthConfig{ + validServerAddress: { + Email: "foo@example.com", + }, + }) + f.CredentialsStore = "mock" + + s := &nativeStore{ + commandFn: mockCommandFn, + fileStore: NewFileStore(f), + } + _, err := s.Get(invalidServerAddress) + if err == nil { + t.Fatal("expected error, got nil") + } + + if err.Error() != "error getting credentials" { + t.Fatalf("expected `error getting credentials`, got %v", err) + } +} + +func TestNativeStoreErase(t *testing.T) { + f := newConfigFile(map[string]types.AuthConfig{ + validServerAddress: { + Email: "foo@example.com", + }, + }) + f.CredentialsStore = "mock" + + s := &nativeStore{ + commandFn: mockCommandFn, + fileStore: NewFileStore(f), + } + err := s.Erase(validServerAddress) + if err != nil { + t.Fatal(err) + } + + if len(f.AuthConfigs) != 0 { + t.Fatalf("expected 0 auth configs, got %d", len(f.AuthConfigs)) + } +} + +func TestNativeStoreEraseInvalidAddress(t *testing.T) { + f := newConfigFile(map[string]types.AuthConfig{ + validServerAddress: { + Email: "foo@example.com", + }, + }) + f.CredentialsStore = "mock" + + s := &nativeStore{ + commandFn: mockCommandFn, + fileStore: NewFileStore(f), + } + err := s.Erase(invalidServerAddress) + if err == nil { + t.Fatal("expected error, got nil") + } + + if err.Error() != "error erasing credentials" { + t.Fatalf("expected `error erasing credentials`, got %v", err) + } +} diff --git a/cliconfig/credentials/shell_command.go b/cliconfig/credentials/shell_command.go new file mode 100644 index 00000000..fa481b19 --- /dev/null +++ b/cliconfig/credentials/shell_command.go @@ -0,0 +1,28 @@ +package credentials + +import ( + "io" + "os/exec" +) + +func shellCommandFn(storeName string) func(args ...string) command { + name := remoteCredentialsPrefix + storeName + return func(args ...string) command { + return &shell{cmd: exec.Command(name, args...)} + } +} + +// shell invokes shell commands to talk with a remote credentials helper. +type shell struct { + cmd *exec.Cmd +} + +// Output returns responses from the remote credentials helper. +func (s *shell) Output() ([]byte, error) { + return s.cmd.Output() +} + +// Input sets the input to send to a remote credentials helper. +func (s *shell) Input(in io.Reader) { + s.cmd.Stdin = in +} diff --git a/container/archive.go b/container/archive.go new file mode 100644 index 00000000..95b68285 --- /dev/null +++ b/container/archive.go @@ -0,0 +1,69 @@ +package container + +import ( + "os" + "path/filepath" + + "github.com/docker/docker/pkg/archive" + "github.com/docker/engine-api/types" +) + +// ResolvePath resolves the given path in the container to a resource on the +// host. Returns a resolved path (absolute path to the resource on the host), +// the absolute path to the resource relative to the container's rootfs, and +// a error if the path points to outside the container's rootfs. +func (container *Container) ResolvePath(path string) (resolvedPath, absPath string, err error) { + // Consider the given path as an absolute path in the container. + absPath = archive.PreserveTrailingDotOrSeparator(filepath.Join(string(filepath.Separator), path), path) + + // Split the absPath into its Directory and Base components. We will + // resolve the dir in the scope of the container then append the base. + dirPath, basePath := filepath.Split(absPath) + + resolvedDirPath, err := container.GetResourcePath(dirPath) + if err != nil { + return "", "", err + } + + // resolvedDirPath will have been cleaned (no trailing path separators) so + // we can manually join it with the base path element. + resolvedPath = resolvedDirPath + string(filepath.Separator) + basePath + + return resolvedPath, absPath, nil +} + +// StatPath is the unexported version of StatPath. Locks and mounts should +// be acquired before calling this method and the given path should be fully +// resolved to a path on the host corresponding to the given absolute path +// inside the container. +func (container *Container) StatPath(resolvedPath, absPath string) (stat *types.ContainerPathStat, err error) { + lstat, err := os.Lstat(resolvedPath) + if err != nil { + return nil, err + } + + var linkTarget string + if lstat.Mode()&os.ModeSymlink != 0 { + // Fully evaluate the symlink in the scope of the container rootfs. + hostPath, err := container.GetResourcePath(absPath) + if err != nil { + return nil, err + } + + linkTarget, err = filepath.Rel(container.BaseFS, hostPath) + if err != nil { + return nil, err + } + + // Make it an absolute path. + linkTarget = filepath.Join(string(filepath.Separator), linkTarget) + } + + return &types.ContainerPathStat{ + Name: filepath.Base(absPath), + Size: lstat.Size(), + Mode: lstat.Mode(), + Mtime: lstat.ModTime(), + LinkTarget: linkTarget, + }, nil +} diff --git a/container/container.go b/container/container.go new file mode 100644 index 00000000..8781b796 --- /dev/null +++ b/container/container.go @@ -0,0 +1,947 @@ +package container + +import ( + "encoding/json" + "fmt" + "io" + "net" + "os" + "path/filepath" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "golang.org/x/net/context" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/daemon/exec" + "github.com/docker/docker/daemon/logger" + "github.com/docker/docker/daemon/logger/jsonfilelog" + "github.com/docker/docker/daemon/network" + "github.com/docker/docker/image" + "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/promise" + "github.com/docker/docker/pkg/signal" + "github.com/docker/docker/pkg/symlink" + "github.com/docker/docker/restartmanager" + "github.com/docker/docker/runconfig" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/docker/docker/volume" + containertypes "github.com/docker/engine-api/types/container" + networktypes "github.com/docker/engine-api/types/network" + "github.com/docker/go-connections/nat" + "github.com/docker/libnetwork" + "github.com/docker/libnetwork/netlabel" + "github.com/docker/libnetwork/options" + "github.com/docker/libnetwork/types" + "github.com/opencontainers/runc/libcontainer/label" +) + +const configFileName = "config.v2.json" + +var ( + errInvalidEndpoint = fmt.Errorf("invalid endpoint while building port map info") + errInvalidNetwork = fmt.Errorf("invalid network settings while building port map info") +) + +// CommonContainer holds the fields for a container which are +// applicable across all platforms supported by the daemon. +type CommonContainer struct { + *runconfig.StreamConfig + // embed for Container to support states directly. + *State `json:"State"` // Needed for remote api version <= 1.11 + Root string `json:"-"` // Path to the "home" of the container, including metadata. + BaseFS string `json:"-"` // Path to the graphdriver mountpoint + RWLayer layer.RWLayer `json:"-"` + ID string + Created time.Time + Path string + Args []string + Config *containertypes.Config + ImageID image.ID `json:"Image"` + NetworkSettings *network.Settings + LogPath string + Name string + Driver string + // MountLabel contains the options for the 'mount' command + MountLabel string + ProcessLabel string + RestartCount int + HasBeenStartedBefore bool + HasBeenManuallyStopped bool // used for unless-stopped restart policy + MountPoints map[string]*volume.MountPoint + HostConfig *containertypes.HostConfig `json:"-"` // do not serialize the host config in the json, otherwise we'll make the container unportable + ExecCommands *exec.Store `json:"-"` + // logDriver for closing + LogDriver logger.Logger `json:"-"` + LogCopier *logger.Copier `json:"-"` + restartManager restartmanager.RestartManager + attachContext *attachContext +} + +// NewBaseContainer creates a new container with its +// basic configuration. +func NewBaseContainer(id, root string) *Container { + return &Container{ + CommonContainer: CommonContainer{ + ID: id, + State: NewState(), + ExecCommands: exec.NewStore(), + Root: root, + MountPoints: make(map[string]*volume.MountPoint), + StreamConfig: runconfig.NewStreamConfig(), + attachContext: &attachContext{}, + }, + } +} + +// FromDisk loads the container configuration stored in the host. +func (container *Container) FromDisk() error { + pth, err := container.ConfigPath() + if err != nil { + return err + } + + jsonSource, err := os.Open(pth) + if err != nil { + return err + } + defer jsonSource.Close() + + dec := json.NewDecoder(jsonSource) + + // Load container settings + if err := dec.Decode(container); err != nil { + return err + } + + if err := label.ReserveLabel(container.ProcessLabel); err != nil { + return err + } + return container.readHostConfig() +} + +// ToDisk saves the container configuration on disk. +func (container *Container) ToDisk() error { + pth, err := container.ConfigPath() + if err != nil { + return err + } + + jsonSource, err := os.Create(pth) + if err != nil { + return err + } + defer jsonSource.Close() + + enc := json.NewEncoder(jsonSource) + + // Save container settings + if err := enc.Encode(container); err != nil { + return err + } + + return container.WriteHostConfig() +} + +// ToDiskLocking saves the container configuration on disk in a thread safe way. +func (container *Container) ToDiskLocking() error { + container.Lock() + err := container.ToDisk() + container.Unlock() + return err +} + +// readHostConfig reads the host configuration from disk for the container. +func (container *Container) readHostConfig() error { + container.HostConfig = &containertypes.HostConfig{} + // If the hostconfig file does not exist, do not read it. + // (We still have to initialize container.HostConfig, + // but that's OK, since we just did that above.) + pth, err := container.HostConfigPath() + if err != nil { + return err + } + + f, err := os.Open(pth) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + defer f.Close() + + if err := json.NewDecoder(f).Decode(&container.HostConfig); err != nil { + return err + } + + container.InitDNSHostConfig() + + return nil +} + +// WriteHostConfig saves the host configuration on disk for the container. +func (container *Container) WriteHostConfig() error { + pth, err := container.HostConfigPath() + if err != nil { + return err + } + + f, err := os.Create(pth) + if err != nil { + return err + } + defer f.Close() + + return json.NewEncoder(f).Encode(&container.HostConfig) +} + +// SetupWorkingDirectory sets up the container's working directory as set in container.Config.WorkingDir +func (container *Container) SetupWorkingDirectory(rootUID, rootGID int) error { + if container.Config.WorkingDir == "" { + return nil + } + + // If can't mount container FS at this point (eg Hyper-V Containers on + // Windows) bail out now with no action. + if !container.canMountFS() { + return nil + } + + container.Config.WorkingDir = filepath.Clean(container.Config.WorkingDir) + + pth, err := container.GetResourcePath(container.Config.WorkingDir) + if err != nil { + return err + } + + if err := idtools.MkdirAllNewAs(pth, 0755, rootUID, rootGID); err != nil { + pthInfo, err2 := os.Stat(pth) + if err2 == nil && pthInfo != nil && !pthInfo.IsDir() { + return fmt.Errorf("Cannot mkdir: %s is not a directory", container.Config.WorkingDir) + } + + return err + } + + return nil +} + +// GetResourcePath evaluates `path` in the scope of the container's BaseFS, with proper path +// sanitisation. Symlinks are all scoped to the BaseFS of the container, as +// though the container's BaseFS was `/`. +// +// The BaseFS of a container is the host-facing path which is bind-mounted as +// `/` inside the container. This method is essentially used to access a +// particular path inside the container as though you were a process in that +// container. +// +// NOTE: The returned path is *only* safely scoped inside the container's BaseFS +// if no component of the returned path changes (such as a component +// symlinking to a different path) between using this method and using the +// path. See symlink.FollowSymlinkInScope for more details. +func (container *Container) GetResourcePath(path string) (string, error) { + // IMPORTANT - These are paths on the OS where the daemon is running, hence + // any filepath operations must be done in an OS agnostic way. + + cleanPath := cleanResourcePath(path) + r, e := symlink.FollowSymlinkInScope(filepath.Join(container.BaseFS, cleanPath), container.BaseFS) + return r, e +} + +// GetRootResourcePath evaluates `path` in the scope of the container's root, with proper path +// sanitisation. Symlinks are all scoped to the root of the container, as +// though the container's root was `/`. +// +// The root of a container is the host-facing configuration metadata directory. +// Only use this method to safely access the container's `container.json` or +// other metadata files. If in doubt, use container.GetResourcePath. +// +// NOTE: The returned path is *only* safely scoped inside the container's root +// if no component of the returned path changes (such as a component +// symlinking to a different path) between using this method and using the +// path. See symlink.FollowSymlinkInScope for more details. +func (container *Container) GetRootResourcePath(path string) (string, error) { + // IMPORTANT - These are paths on the OS where the daemon is running, hence + // any filepath operations must be done in an OS agnostic way. + cleanPath := filepath.Join(string(os.PathSeparator), path) + return symlink.FollowSymlinkInScope(filepath.Join(container.Root, cleanPath), container.Root) +} + +// ExitOnNext signals to the monitor that it should not restart the container +// after we send the kill signal. +func (container *Container) ExitOnNext() { + if container.restartManager != nil { + container.restartManager.Cancel() + } +} + +// HostConfigPath returns the path to the container's JSON hostconfig +func (container *Container) HostConfigPath() (string, error) { + return container.GetRootResourcePath("hostconfig.json") +} + +// ConfigPath returns the path to the container's JSON config +func (container *Container) ConfigPath() (string, error) { + return container.GetRootResourcePath(configFileName) +} + +// StartLogger starts a new logger driver for the container. +func (container *Container) StartLogger(cfg containertypes.LogConfig) (logger.Logger, error) { + c, err := logger.GetLogDriver(cfg.Type) + if err != nil { + return nil, fmt.Errorf("Failed to get logging factory: %v", err) + } + ctx := logger.Context{ + Config: cfg.Config, + ContainerID: container.ID, + ContainerName: container.Name, + ContainerEntrypoint: container.Path, + ContainerArgs: container.Args, + ContainerImageID: container.ImageID.String(), + ContainerImageName: container.Config.Image, + ContainerCreated: container.Created, + ContainerEnv: container.Config.Env, + ContainerLabels: container.Config.Labels, + } + + // Set logging file for "json-logger" + if cfg.Type == jsonfilelog.Name { + ctx.LogPath, err = container.GetRootResourcePath(fmt.Sprintf("%s-json.log", container.ID)) + if err != nil { + return nil, err + } + } + return c(ctx) +} + +// GetProcessLabel returns the process label for the container. +func (container *Container) GetProcessLabel() string { + // even if we have a process label return "" if we are running + // in privileged mode + if container.HostConfig.Privileged { + return "" + } + return container.ProcessLabel +} + +// GetMountLabel returns the mounting label for the container. +// This label is empty if the container is privileged. +func (container *Container) GetMountLabel() string { + if container.HostConfig.Privileged { + return "" + } + return container.MountLabel +} + +// GetExecIDs returns the list of exec commands running on the container. +func (container *Container) GetExecIDs() []string { + return container.ExecCommands.List() +} + +// Attach connects to the container's TTY, delegating to standard +// streams or websockets depending on the configuration. +func (container *Container) Attach(stdin io.ReadCloser, stdout io.Writer, stderr io.Writer, keys []byte) chan error { + ctx := container.InitAttachContext() + return AttachStreams(ctx, container.StreamConfig, container.Config.OpenStdin, container.Config.StdinOnce, container.Config.Tty, stdin, stdout, stderr, keys) +} + +// AttachStreams connects streams to a TTY. +// Used by exec too. Should this move somewhere else? +func AttachStreams(ctx context.Context, streamConfig *runconfig.StreamConfig, openStdin, stdinOnce, tty bool, stdin io.ReadCloser, stdout io.Writer, stderr io.Writer, keys []byte) chan error { + var ( + cStdout, cStderr io.ReadCloser + cStdin io.WriteCloser + wg sync.WaitGroup + errors = make(chan error, 3) + ) + + if stdin != nil && openStdin { + cStdin = streamConfig.StdinPipe() + wg.Add(1) + } + + if stdout != nil { + cStdout = streamConfig.StdoutPipe() + wg.Add(1) + } + + if stderr != nil { + cStderr = streamConfig.StderrPipe() + wg.Add(1) + } + + // Connect stdin of container to the http conn. + go func() { + if stdin == nil || !openStdin { + return + } + logrus.Debugf("attach: stdin: begin") + + var err error + if tty { + _, err = copyEscapable(cStdin, stdin, keys) + } else { + _, err = io.Copy(cStdin, stdin) + + } + if err == io.ErrClosedPipe { + err = nil + } + if err != nil { + logrus.Errorf("attach: stdin: %s", err) + errors <- err + } + if stdinOnce && !tty { + cStdin.Close() + } else { + // No matter what, when stdin is closed (io.Copy unblock), close stdout and stderr + if cStdout != nil { + cStdout.Close() + } + if cStderr != nil { + cStderr.Close() + } + } + logrus.Debugf("attach: stdin: end") + wg.Done() + }() + + attachStream := func(name string, stream io.Writer, streamPipe io.ReadCloser) { + if stream == nil { + return + } + + logrus.Debugf("attach: %s: begin", name) + _, err := io.Copy(stream, streamPipe) + if err == io.ErrClosedPipe { + err = nil + } + if err != nil { + logrus.Errorf("attach: %s: %v", name, err) + errors <- err + } + // Make sure stdin gets closed + if stdin != nil { + stdin.Close() + } + streamPipe.Close() + logrus.Debugf("attach: %s: end", name) + wg.Done() + } + + go attachStream("stdout", stdout, cStdout) + go attachStream("stderr", stderr, cStderr) + + return promise.Go(func() error { + done := make(chan struct{}) + go func() { + wg.Wait() + close(done) + }() + select { + case <-done: + case <-ctx.Done(): + // close all pipes + if cStdin != nil { + cStdin.Close() + } + if cStdout != nil { + cStdout.Close() + } + if cStderr != nil { + cStderr.Close() + } + <-done + } + close(errors) + for err := range errors { + if err != nil { + return err + } + } + return nil + }) +} + +// Code c/c from io.Copy() modified to handle escape sequence +func copyEscapable(dst io.Writer, src io.ReadCloser, keys []byte) (written int64, err error) { + if len(keys) == 0 { + // Default keys : ctrl-p ctrl-q + keys = []byte{16, 17} + } + buf := make([]byte, 32*1024) + for { + nr, er := src.Read(buf) + if nr > 0 { + // ---- Docker addition + for i, key := range keys { + if nr != 1 || buf[0] != key { + break + } + if i == len(keys)-1 { + if err := src.Close(); err != nil { + return 0, err + } + return 0, nil + } + nr, er = src.Read(buf) + } + // ---- End of docker + nw, ew := dst.Write(buf[0:nr]) + if nw > 0 { + written += int64(nw) + } + if ew != nil { + err = ew + break + } + if nr != nw { + err = io.ErrShortWrite + break + } + } + if er == io.EOF { + break + } + if er != nil { + err = er + break + } + } + return written, err +} + +// ShouldRestartOnBoot decides whether the daemon should restart the container or not. +// This is based on the container's restart policy. +func (container *Container) ShouldRestartOnBoot() bool { + return container.HostConfig.RestartPolicy.Name == "always" || + (container.HostConfig.RestartPolicy.Name == "unless-stopped" && !container.HasBeenManuallyStopped) || + (container.HostConfig.RestartPolicy.Name == "on-failure" && container.ExitCode != 0) +} + +// AddBindMountPoint adds a new bind mount point configuration to the container. +func (container *Container) AddBindMountPoint(name, source, destination string, rw bool) { + container.MountPoints[destination] = &volume.MountPoint{ + Name: name, + Source: source, + Destination: destination, + RW: rw, + } +} + +// AddLocalMountPoint adds a new local mount point configuration to the container. +func (container *Container) AddLocalMountPoint(name, destination string, rw bool) { + container.MountPoints[destination] = &volume.MountPoint{ + Name: name, + Driver: volume.DefaultDriverName, + Destination: destination, + RW: rw, + } +} + +// AddMountPointWithVolume adds a new mount point configured with a volume to the container. +func (container *Container) AddMountPointWithVolume(destination string, vol volume.Volume, rw bool) { + container.MountPoints[destination] = &volume.MountPoint{ + Name: vol.Name(), + Driver: vol.DriverName(), + Destination: destination, + RW: rw, + Volume: vol, + CopyData: volume.DefaultCopyMode, + } +} + +// IsDestinationMounted checks whether a path is mounted on the container or not. +func (container *Container) IsDestinationMounted(destination string) bool { + return container.MountPoints[destination] != nil +} + +// StopSignal returns the signal used to stop the container. +func (container *Container) StopSignal() int { + var stopSignal syscall.Signal + if container.Config.StopSignal != "" { + stopSignal, _ = signal.ParseSignal(container.Config.StopSignal) + } + + if int(stopSignal) == 0 { + stopSignal, _ = signal.ParseSignal(signal.DefaultStopSignal) + } + return int(stopSignal) +} + +// InitDNSHostConfig ensures that 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. +// The non-recommended host configuration in the start api can +// make these fields nil again, this corrects that issue until +// we remove that behavior for good. +// See https://github.com/docker/docker/pull/17779 +// for a more detailed explanation on why we don't want that. +func (container *Container) InitDNSHostConfig() { + container.Lock() + defer container.Unlock() + if container.HostConfig.DNS == nil { + container.HostConfig.DNS = make([]string, 0) + } + + if container.HostConfig.DNSSearch == nil { + container.HostConfig.DNSSearch = make([]string, 0) + } + + if container.HostConfig.DNSOptions == nil { + container.HostConfig.DNSOptions = make([]string, 0) + } +} + +// GetEndpointInNetwork returns the container's endpoint to the provided network. +func (container *Container) GetEndpointInNetwork(n libnetwork.Network) (libnetwork.Endpoint, error) { + endpointName := strings.TrimPrefix(container.Name, "/") + return n.EndpointByName(endpointName) +} + +func (container *Container) buildPortMapInfo(ep libnetwork.Endpoint) error { + if ep == nil { + return errInvalidEndpoint + } + + networkSettings := container.NetworkSettings + if networkSettings == nil { + return errInvalidNetwork + } + + if len(networkSettings.Ports) == 0 { + pm, err := getEndpointPortMapInfo(ep) + if err != nil { + return err + } + networkSettings.Ports = pm + } + return nil +} + +func getEndpointPortMapInfo(ep libnetwork.Endpoint) (nat.PortMap, error) { + pm := nat.PortMap{} + driverInfo, err := ep.DriverInfo() + if err != nil { + return pm, err + } + + if driverInfo == nil { + // It is not an error for epInfo to be nil + return pm, nil + } + + if expData, ok := driverInfo[netlabel.ExposedPorts]; ok { + if exposedPorts, ok := expData.([]types.TransportPort); ok { + for _, tp := range exposedPorts { + natPort, err := nat.NewPort(tp.Proto.String(), strconv.Itoa(int(tp.Port))) + if err != nil { + return pm, fmt.Errorf("Error parsing Port value(%v):%v", tp.Port, err) + } + pm[natPort] = nil + } + } + } + + mapData, ok := driverInfo[netlabel.PortMap] + if !ok { + return pm, nil + } + + if portMapping, ok := mapData.([]types.PortBinding); ok { + for _, pp := range portMapping { + natPort, err := nat.NewPort(pp.Proto.String(), strconv.Itoa(int(pp.Port))) + if err != nil { + return pm, err + } + natBndg := nat.PortBinding{HostIP: pp.HostIP.String(), HostPort: strconv.Itoa(int(pp.HostPort))} + pm[natPort] = append(pm[natPort], natBndg) + } + } + + return pm, nil +} + +// GetSandboxPortMapInfo retrieves the current port-mapping programmed for the given sandbox +func GetSandboxPortMapInfo(sb libnetwork.Sandbox) nat.PortMap { + pm := nat.PortMap{} + if sb == nil { + return pm + } + + for _, ep := range sb.Endpoints() { + pm, _ = getEndpointPortMapInfo(ep) + if len(pm) > 0 { + break + } + } + return pm +} + +// BuildEndpointInfo sets endpoint-related fields on container.NetworkSettings based on the provided network and endpoint. +func (container *Container) BuildEndpointInfo(n libnetwork.Network, ep libnetwork.Endpoint) error { + if ep == nil { + return errInvalidEndpoint + } + + networkSettings := container.NetworkSettings + if networkSettings == nil { + return errInvalidNetwork + } + + epInfo := ep.Info() + if epInfo == nil { + // It is not an error to get an empty endpoint info + return nil + } + + if _, ok := networkSettings.Networks[n.Name()]; !ok { + networkSettings.Networks[n.Name()] = new(networktypes.EndpointSettings) + } + networkSettings.Networks[n.Name()].NetworkID = n.ID() + networkSettings.Networks[n.Name()].EndpointID = ep.ID() + + iface := epInfo.Iface() + if iface == nil { + return nil + } + + if iface.MacAddress() != nil { + networkSettings.Networks[n.Name()].MacAddress = iface.MacAddress().String() + } + + if iface.Address() != nil { + ones, _ := iface.Address().Mask.Size() + networkSettings.Networks[n.Name()].IPAddress = iface.Address().IP.String() + networkSettings.Networks[n.Name()].IPPrefixLen = ones + } + + if iface.AddressIPv6() != nil && iface.AddressIPv6().IP.To16() != nil { + onesv6, _ := iface.AddressIPv6().Mask.Size() + networkSettings.Networks[n.Name()].GlobalIPv6Address = iface.AddressIPv6().IP.String() + networkSettings.Networks[n.Name()].GlobalIPv6PrefixLen = onesv6 + } + + return nil +} + +// UpdateJoinInfo updates network settings when container joins network n with endpoint ep. +func (container *Container) UpdateJoinInfo(n libnetwork.Network, ep libnetwork.Endpoint) error { + if err := container.buildPortMapInfo(ep); err != nil { + return err + } + + epInfo := ep.Info() + if epInfo == nil { + // It is not an error to get an empty endpoint info + return nil + } + if epInfo.Gateway() != nil { + container.NetworkSettings.Networks[n.Name()].Gateway = epInfo.Gateway().String() + } + if epInfo.GatewayIPv6().To16() != nil { + container.NetworkSettings.Networks[n.Name()].IPv6Gateway = epInfo.GatewayIPv6().String() + } + + return nil +} + +// UpdateSandboxNetworkSettings updates the sandbox ID and Key. +func (container *Container) UpdateSandboxNetworkSettings(sb libnetwork.Sandbox) error { + container.NetworkSettings.SandboxID = sb.ID() + container.NetworkSettings.SandboxKey = sb.Key() + return nil +} + +// BuildJoinOptions builds endpoint Join options from a given network. +func (container *Container) BuildJoinOptions(n libnetwork.Network) ([]libnetwork.EndpointOption, error) { + var joinOptions []libnetwork.EndpointOption + if epConfig, ok := container.NetworkSettings.Networks[n.Name()]; ok { + for _, str := range epConfig.Links { + name, alias, err := runconfigopts.ParseLink(str) + if err != nil { + return nil, err + } + joinOptions = append(joinOptions, libnetwork.CreateOptionAlias(name, alias)) + } + } + return joinOptions, nil +} + +// BuildCreateEndpointOptions builds endpoint options from a given network. +func (container *Container) BuildCreateEndpointOptions(n libnetwork.Network, epConfig *networktypes.EndpointSettings, sb libnetwork.Sandbox) ([]libnetwork.EndpointOption, error) { + var ( + bindings = make(nat.PortMap) + pbList []types.PortBinding + exposeList []types.TransportPort + createOptions []libnetwork.EndpointOption + ) + + defaultNetName := runconfig.DefaultDaemonNetworkMode().NetworkName() + + if n.Name() == defaultNetName || container.NetworkSettings.IsAnonymousEndpoint { + createOptions = append(createOptions, libnetwork.CreateOptionAnonymous()) + } + + if epConfig != nil { + ipam := epConfig.IPAMConfig + if ipam != nil && (ipam.IPv4Address != "" || ipam.IPv6Address != "") { + createOptions = append(createOptions, + libnetwork.CreateOptionIpam(net.ParseIP(ipam.IPv4Address), net.ParseIP(ipam.IPv6Address), nil)) + } + + for _, alias := range epConfig.Aliases { + createOptions = append(createOptions, libnetwork.CreateOptionMyAlias(alias)) + } + } + + if !containertypes.NetworkMode(n.Name()).IsUserDefined() { + createOptions = append(createOptions, libnetwork.CreateOptionDisableResolution()) + } + + // configs that are applicable only for the endpoint in the network + // to which container was connected to on docker run. + // Ideally all these network-specific endpoint configurations must be moved under + // container.NetworkSettings.Networks[n.Name()] + if n.Name() == container.HostConfig.NetworkMode.NetworkName() || + (n.Name() == defaultNetName && container.HostConfig.NetworkMode.IsDefault()) { + if container.Config.MacAddress != "" { + mac, err := net.ParseMAC(container.Config.MacAddress) + if err != nil { + return nil, err + } + + genericOption := options.Generic{ + netlabel.MacAddress: mac, + } + + createOptions = append(createOptions, libnetwork.EndpointOptionGeneric(genericOption)) + } + } + + // Port-mapping rules belong to the container & applicable only to non-internal networks + portmaps := GetSandboxPortMapInfo(sb) + if n.Info().Internal() || len(portmaps) > 0 { + return createOptions, nil + } + + if container.HostConfig.PortBindings != nil { + for p, b := range container.HostConfig.PortBindings { + bindings[p] = []nat.PortBinding{} + for _, bb := range b { + bindings[p] = append(bindings[p], nat.PortBinding{ + HostIP: bb.HostIP, + HostPort: bb.HostPort, + }) + } + } + } + + portSpecs := container.Config.ExposedPorts + ports := make([]nat.Port, len(portSpecs)) + var i int + for p := range portSpecs { + ports[i] = p + i++ + } + nat.SortPortMap(ports, bindings) + for _, port := range ports { + expose := types.TransportPort{} + expose.Proto = types.ParseProtocol(port.Proto()) + expose.Port = uint16(port.Int()) + exposeList = append(exposeList, expose) + + pb := types.PortBinding{Port: expose.Port, Proto: expose.Proto} + binding := bindings[port] + for i := 0; i < len(binding); i++ { + pbCopy := pb.GetCopy() + newP, err := nat.NewPort(nat.SplitProtoPort(binding[i].HostPort)) + var portStart, portEnd int + if err == nil { + portStart, portEnd, err = newP.Range() + } + if err != nil { + return nil, fmt.Errorf("Error parsing HostPort value(%s):%v", binding[i].HostPort, err) + } + pbCopy.HostPort = uint16(portStart) + pbCopy.HostPortEnd = uint16(portEnd) + pbCopy.HostIP = net.ParseIP(binding[i].HostIP) + pbList = append(pbList, pbCopy) + } + + if container.HostConfig.PublishAllPorts && len(binding) == 0 { + pbList = append(pbList, pb) + } + } + + createOptions = append(createOptions, + libnetwork.CreateOptionPortMapping(pbList), + libnetwork.CreateOptionExposedPorts(exposeList)) + + return createOptions, nil +} + +// UpdateMonitor updates monitor configure for running container +func (container *Container) UpdateMonitor(restartPolicy containertypes.RestartPolicy) { + type policySetter interface { + SetPolicy(containertypes.RestartPolicy) + } + + if rm, ok := container.RestartManager(false).(policySetter); ok { + rm.SetPolicy(restartPolicy) + } +} + +// FullHostname returns hostname and optional domain appended to it. +func (container *Container) FullHostname() string { + fullHostname := container.Config.Hostname + if container.Config.Domainname != "" { + fullHostname = fmt.Sprintf("%s.%s", fullHostname, container.Config.Domainname) + } + return fullHostname +} + +// RestartManager returns the current restartmanager instace connected to container. +func (container *Container) RestartManager(reset bool) restartmanager.RestartManager { + if reset { + container.RestartCount = 0 + container.restartManager = nil + } + if container.restartManager == nil { + container.restartManager = restartmanager.New(container.HostConfig.RestartPolicy) + } + return container.restartManager +} + +type attachContext struct { + ctx context.Context + cancel context.CancelFunc + mu sync.Mutex +} + +// InitAttachContext initialize or returns existing context for attach calls to +// track container liveness. +func (container *Container) InitAttachContext() context.Context { + container.attachContext.mu.Lock() + defer container.attachContext.mu.Unlock() + if container.attachContext.ctx == nil { + container.attachContext.ctx, container.attachContext.cancel = context.WithCancel(context.Background()) + } + return container.attachContext.ctx +} + +// CancelAttachContext cancel attach context. All attach calls should detach +// after this call. +func (container *Container) CancelAttachContext() { + container.attachContext.mu.Lock() + if container.attachContext.ctx != nil { + container.attachContext.cancel() + container.attachContext.ctx = nil + } + container.attachContext.mu.Unlock() +} diff --git a/container/container_unit_test.go b/container/container_unit_test.go new file mode 100644 index 00000000..67b829f9 --- /dev/null +++ b/container/container_unit_test.go @@ -0,0 +1,36 @@ +package container + +import ( + "testing" + + "github.com/docker/docker/pkg/signal" + "github.com/docker/engine-api/types/container" +) + +func TestContainerStopSignal(t *testing.T) { + c := &Container{ + CommonContainer: CommonContainer{ + Config: &container.Config{}, + }, + } + + def, err := signal.ParseSignal(signal.DefaultStopSignal) + if err != nil { + t.Fatal(err) + } + + s := c.StopSignal() + if s != int(def) { + t.Fatalf("Expected %v, got %v", def, s) + } + + c = &Container{ + CommonContainer: CommonContainer{ + Config: &container.Config{StopSignal: "SIGKILL"}, + }, + } + s = c.StopSignal() + if s != 9 { + t.Fatalf("Expected 9, got %v", s) + } +} diff --git a/container/container_unix.go b/container/container_unix.go new file mode 100644 index 00000000..754090f9 --- /dev/null +++ b/container/container_unix.go @@ -0,0 +1,405 @@ +// +build linux freebsd + +package container + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "syscall" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/chrootarchive" + "github.com/docker/docker/pkg/symlink" + "github.com/docker/docker/pkg/system" + "github.com/docker/docker/utils" + "github.com/docker/docker/volume" + containertypes "github.com/docker/engine-api/types/container" + "github.com/opencontainers/runc/libcontainer/label" +) + +// DefaultSHMSize is the default size (64MB) of the SHM which will be mounted in the container +const DefaultSHMSize int64 = 67108864 + +// Container holds the fields specific to unixen implementations. +// See CommonContainer for standard fields common to all containers. +type Container struct { + CommonContainer + + // Fields below here are platform specific. + AppArmorProfile string + HostnamePath string + HostsPath string + ShmPath string + ResolvConfPath string + SeccompProfile string + NoNewPrivileges bool +} + +// ExitStatus provides exit reasons for a container. +type ExitStatus struct { + // The exit code with which the container exited. + ExitCode int + + // Whether the container encountered an OOM. + OOMKilled bool +} + +// CreateDaemonEnvironment returns the list of all environment variables given the list of +// environment variables related to links. +// Sets PATH, HOSTNAME and if container.Config.Tty is set: TERM. +// The defaults set here do not override the values in container.Config.Env +func (container *Container) CreateDaemonEnvironment(linkedEnv []string) []string { + // Setup environment + env := []string{ + "PATH=" + system.DefaultPathEnv, + "HOSTNAME=" + container.Config.Hostname, + } + if container.Config.Tty { + env = append(env, "TERM=xterm") + } + env = append(env, linkedEnv...) + // because the env on the container can override certain default values + // we need to replace the 'env' keys where they match and append anything + // else. + env = utils.ReplaceOrAppendEnvValues(env, container.Config.Env) + return env +} + +// TrySetNetworkMount attempts to set the network mounts given a provided destination and +// the path to use for it; return true if the given destination was a network mount file +func (container *Container) TrySetNetworkMount(destination string, path string) bool { + if destination == "/etc/resolv.conf" { + container.ResolvConfPath = path + return true + } + if destination == "/etc/hostname" { + container.HostnamePath = path + return true + } + if destination == "/etc/hosts" { + container.HostsPath = path + return true + } + + return false +} + +// BuildHostnameFile writes the container's hostname file. +func (container *Container) BuildHostnameFile() error { + hostnamePath, err := container.GetRootResourcePath("hostname") + if err != nil { + return err + } + container.HostnamePath = hostnamePath + return ioutil.WriteFile(container.HostnamePath, []byte(container.Config.Hostname+"\n"), 0644) +} + +// appendNetworkMounts appends any network mounts to the array of mount points passed in +func appendNetworkMounts(container *Container, volumeMounts []volume.MountPoint) ([]volume.MountPoint, error) { + for _, mnt := range container.NetworkMounts() { + dest, err := container.GetResourcePath(mnt.Destination) + if err != nil { + return nil, err + } + volumeMounts = append(volumeMounts, volume.MountPoint{Destination: dest}) + } + return volumeMounts, nil +} + +// NetworkMounts returns the list of network mounts. +func (container *Container) NetworkMounts() []Mount { + var mounts []Mount + shared := container.HostConfig.NetworkMode.IsContainer() + if container.ResolvConfPath != "" { + if _, err := os.Stat(container.ResolvConfPath); err != nil { + logrus.Warnf("ResolvConfPath set to %q, but can't stat this filename (err = %v); skipping", container.ResolvConfPath, err) + } else { + label.Relabel(container.ResolvConfPath, container.MountLabel, shared) + writable := !container.HostConfig.ReadonlyRootfs + if m, exists := container.MountPoints["/etc/resolv.conf"]; exists { + writable = m.RW + } + mounts = append(mounts, Mount{ + Source: container.ResolvConfPath, + Destination: "/etc/resolv.conf", + Writable: writable, + Propagation: volume.DefaultPropagationMode, + }) + } + } + if container.HostnamePath != "" { + if _, err := os.Stat(container.HostnamePath); err != nil { + logrus.Warnf("HostnamePath set to %q, but can't stat this filename (err = %v); skipping", container.HostnamePath, err) + } else { + label.Relabel(container.HostnamePath, container.MountLabel, shared) + writable := !container.HostConfig.ReadonlyRootfs + if m, exists := container.MountPoints["/etc/hostname"]; exists { + writable = m.RW + } + mounts = append(mounts, Mount{ + Source: container.HostnamePath, + Destination: "/etc/hostname", + Writable: writable, + Propagation: volume.DefaultPropagationMode, + }) + } + } + if container.HostsPath != "" { + if _, err := os.Stat(container.HostsPath); err != nil { + logrus.Warnf("HostsPath set to %q, but can't stat this filename (err = %v); skipping", container.HostsPath, err) + } else { + label.Relabel(container.HostsPath, container.MountLabel, shared) + writable := !container.HostConfig.ReadonlyRootfs + if m, exists := container.MountPoints["/etc/hosts"]; exists { + writable = m.RW + } + mounts = append(mounts, Mount{ + Source: container.HostsPath, + Destination: "/etc/hosts", + Writable: writable, + Propagation: volume.DefaultPropagationMode, + }) + } + } + return mounts +} + +// CopyImagePathContent copies files in destination to the volume. +func (container *Container) CopyImagePathContent(v volume.Volume, destination string) error { + rootfs, err := symlink.FollowSymlinkInScope(filepath.Join(container.BaseFS, destination), container.BaseFS) + if err != nil { + return err + } + + if _, err = ioutil.ReadDir(rootfs); err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + + path, err := v.Mount() + if err != nil { + return err + } + defer v.Unmount() + return copyExistingContents(rootfs, path) +} + +// ShmResourcePath returns path to shm +func (container *Container) ShmResourcePath() (string, error) { + return container.GetRootResourcePath("shm") +} + +// HasMountFor checks if path is a mountpoint +func (container *Container) HasMountFor(path string) bool { + _, exists := container.MountPoints[path] + return exists +} + +// UnmountIpcMounts uses the provided unmount function to unmount shm and mqueue if they were mounted +func (container *Container) UnmountIpcMounts(unmount func(pth string) error) { + if container.HostConfig.IpcMode.IsContainer() || container.HostConfig.IpcMode.IsHost() { + return + } + + var warnings []string + + if !container.HasMountFor("/dev/shm") { + shmPath, err := container.ShmResourcePath() + if err != nil { + logrus.Error(err) + warnings = append(warnings, err.Error()) + } else if shmPath != "" { + if err := unmount(shmPath); err != nil { + warnings = append(warnings, fmt.Sprintf("failed to umount %s: %v", shmPath, err)) + } + + } + } + + if len(warnings) > 0 { + logrus.Warnf("failed to cleanup ipc mounts:\n%v", strings.Join(warnings, "\n")) + } +} + +// IpcMounts returns the list of IPC mounts +func (container *Container) IpcMounts() []Mount { + var mounts []Mount + + if !container.HasMountFor("/dev/shm") { + label.SetFileLabel(container.ShmPath, container.MountLabel) + mounts = append(mounts, Mount{ + Source: container.ShmPath, + Destination: "/dev/shm", + Writable: true, + Propagation: volume.DefaultPropagationMode, + }) + } + + return mounts +} + +// UpdateContainer updates configuration of a container. +func (container *Container) UpdateContainer(hostConfig *containertypes.HostConfig) error { + container.Lock() + defer container.Unlock() + + // update resources of container + resources := hostConfig.Resources + cResources := &container.HostConfig.Resources + if resources.BlkioWeight != 0 { + cResources.BlkioWeight = resources.BlkioWeight + } + if resources.CPUShares != 0 { + cResources.CPUShares = resources.CPUShares + } + if resources.CPUPeriod != 0 { + cResources.CPUPeriod = resources.CPUPeriod + } + if resources.CPUQuota != 0 { + cResources.CPUQuota = resources.CPUQuota + } + if resources.CpusetCpus != "" { + cResources.CpusetCpus = resources.CpusetCpus + } + if resources.CpusetMems != "" { + cResources.CpusetMems = resources.CpusetMems + } + if resources.Memory != 0 { + cResources.Memory = resources.Memory + } + if resources.MemorySwap != 0 { + cResources.MemorySwap = resources.MemorySwap + } + if resources.MemoryReservation != 0 { + cResources.MemoryReservation = resources.MemoryReservation + } + if resources.KernelMemory != 0 { + cResources.KernelMemory = resources.KernelMemory + } + + // update HostConfig of container + if hostConfig.RestartPolicy.Name != "" { + container.HostConfig.RestartPolicy = hostConfig.RestartPolicy + } + + if err := container.ToDisk(); err != nil { + logrus.Errorf("Error saving updated container: %v", err) + return err + } + + return nil +} + +func detachMounted(path string) error { + return syscall.Unmount(path, syscall.MNT_DETACH) +} + +// UnmountVolumes unmounts all volumes +func (container *Container) UnmountVolumes(forceSyscall bool, volumeEventLog func(name, action string, attributes map[string]string)) error { + var ( + volumeMounts []volume.MountPoint + err error + ) + + for _, mntPoint := range container.MountPoints { + dest, err := container.GetResourcePath(mntPoint.Destination) + if err != nil { + return err + } + + volumeMounts = append(volumeMounts, volume.MountPoint{Destination: dest, Volume: mntPoint.Volume}) + } + + // Append any network mounts to the list (this is a no-op on Windows) + if volumeMounts, err = appendNetworkMounts(container, volumeMounts); err != nil { + return err + } + + for _, volumeMount := range volumeMounts { + if forceSyscall { + if err := detachMounted(volumeMount.Destination); err != nil { + logrus.Warnf("%s unmountVolumes: Failed to do lazy umount %v", container.ID, err) + } + } + + if volumeMount.Volume != nil { + if err := volumeMount.Volume.Unmount(); err != nil { + return err + } + + attributes := map[string]string{ + "driver": volumeMount.Volume.DriverName(), + "container": container.ID, + } + volumeEventLog(volumeMount.Volume.Name(), "unmount", attributes) + } + } + + return nil +} + +// copyExistingContents copies from the source to the destination and +// ensures the ownership is appropriately set. +func copyExistingContents(source, destination string) error { + volList, err := ioutil.ReadDir(source) + if err != nil { + return err + } + if len(volList) > 0 { + srcList, err := ioutil.ReadDir(destination) + if err != nil { + return err + } + if len(srcList) == 0 { + // If the source volume is empty, copies files from the root into the volume + if err := chrootarchive.CopyWithTar(source, destination); err != nil { + return err + } + } + } + return copyOwnership(source, destination) +} + +// copyOwnership copies the permissions and uid:gid of the source file +// to the destination file +func copyOwnership(source, destination string) error { + stat, err := system.Stat(source) + if err != nil { + return err + } + + if err := os.Chown(destination, int(stat.UID()), int(stat.GID())); err != nil { + return err + } + + return os.Chmod(destination, os.FileMode(stat.Mode())) +} + +// TmpfsMounts returns the list of tmpfs mounts +func (container *Container) TmpfsMounts() []Mount { + var mounts []Mount + for dest, data := range container.HostConfig.Tmpfs { + mounts = append(mounts, Mount{ + Source: "tmpfs", + Destination: dest, + Data: data, + }) + } + return mounts +} + +// cleanResourcePath cleans a resource path and prepares to combine with mnt path +func cleanResourcePath(path string) string { + return filepath.Join(string(os.PathSeparator), path) +} + +// canMountFS determines if the file system for the container +// can be mounted locally. A no-op on non-Windows platforms +func (container *Container) canMountFS() bool { + return true +} diff --git a/container/container_windows.go b/container/container_windows.go new file mode 100644 index 00000000..5c923960 --- /dev/null +++ b/container/container_windows.go @@ -0,0 +1,105 @@ +// +build windows + +package container + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/docker/docker/volume" + containertypes "github.com/docker/engine-api/types/container" +) + +// Container holds fields specific to the Windows implementation. See +// CommonContainer for standard fields common to all containers. +type Container struct { + CommonContainer + + HostnamePath string + HostsPath string + ResolvConfPath string + // Fields below here are platform specific. +} + +// ExitStatus provides exit reasons for a container. +type ExitStatus struct { + // The exit code with which the container exited. + ExitCode int +} + +// CreateDaemonEnvironment creates a new environment variable slice for this container. +func (container *Container) CreateDaemonEnvironment(linkedEnv []string) []string { + // On Windows, nothing to link. Just return the container environment. + return container.Config.Env +} + +// UnmountIpcMounts unmount Ipc related mounts. +// This is a NOOP on windows. +func (container *Container) UnmountIpcMounts(unmount func(pth string) error) { +} + +// IpcMounts returns the list of Ipc related mounts. +func (container *Container) IpcMounts() []Mount { + return nil +} + +// UnmountVolumes explicitly unmounts volumes from the container. +func (container *Container) UnmountVolumes(forceSyscall bool, volumeEventLog func(name, action string, attributes map[string]string)) error { + return nil +} + +// TmpfsMounts returns the list of tmpfs mounts +func (container *Container) TmpfsMounts() []Mount { + return nil +} + +// UpdateContainer updates configuration of a container +func (container *Container) UpdateContainer(hostConfig *containertypes.HostConfig) error { + container.Lock() + defer container.Unlock() + resources := hostConfig.Resources + if resources.BlkioWeight != 0 || resources.CPUShares != 0 || + resources.CPUPeriod != 0 || resources.CPUQuota != 0 || + resources.CpusetCpus != "" || resources.CpusetMems != "" || + resources.Memory != 0 || resources.MemorySwap != 0 || + resources.MemoryReservation != 0 || resources.KernelMemory != 0 { + return fmt.Errorf("Resource updating isn't supported on Windows") + } + // update HostConfig of container + if hostConfig.RestartPolicy.Name != "" { + container.HostConfig.RestartPolicy = hostConfig.RestartPolicy + } + return nil +} + +// appendNetworkMounts appends any network mounts to the array of mount points passed in. +// Windows does not support network mounts (not to be confused with SMB network mounts), so +// this is a no-op. +func appendNetworkMounts(container *Container, volumeMounts []volume.MountPoint) ([]volume.MountPoint, error) { + return volumeMounts, nil +} + +// cleanResourcePath cleans a resource path by removing C:\ syntax, and prepares +// to combine with a volume path +func cleanResourcePath(path string) string { + if len(path) >= 2 { + c := path[0] + if path[1] == ':' && ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') { + path = path[2:] + } + } + return filepath.Join(string(os.PathSeparator), path) +} + +// BuildHostnameFile writes the container's hostname file. +func (container *Container) BuildHostnameFile() error { + return nil +} + +// canMountFS determines if the file system for the container +// can be mounted locally. In the case of Windows, this is not possible +// for Hyper-V containers during WORKDIR execution for example. +func (container *Container) canMountFS() bool { + return !containertypes.Isolation.IsHyperV(container.HostConfig.Isolation) +} diff --git a/container/history.go b/container/history.go new file mode 100644 index 00000000..c80c2aa0 --- /dev/null +++ b/container/history.go @@ -0,0 +1,30 @@ +package container + +import "sort" + +// History is a convenience type for storing a list of containers, +// sorted by creation date in descendant order. +type History []*Container + +// Len returns the number of containers in the history. +func (history *History) Len() int { + return len(*history) +} + +// Less compares two containers and returns true if the second one +// was created before the first one. +func (history *History) Less(i, j int) bool { + containers := *history + return containers[j].Created.Before(containers[i].Created) +} + +// Swap switches containers i and j positions in the history. +func (history *History) Swap(i, j int) { + containers := *history + containers[i], containers[j] = containers[j], containers[i] +} + +// sort orders the history by creation date in descendant order. +func (history *History) sort() { + sort.Sort(history) +} diff --git a/container/memory_store.go b/container/memory_store.go new file mode 100644 index 00000000..9fa1165d --- /dev/null +++ b/container/memory_store.go @@ -0,0 +1,92 @@ +package container + +import "sync" + +// memoryStore implements a Store in memory. +type memoryStore struct { + s map[string]*Container + sync.RWMutex +} + +// NewMemoryStore initializes a new memory store. +func NewMemoryStore() Store { + return &memoryStore{ + s: make(map[string]*Container), + } +} + +// Add appends a new container to the memory store. +// It overrides the id if it existed before. +func (c *memoryStore) Add(id string, cont *Container) { + c.Lock() + c.s[id] = cont + c.Unlock() +} + +// Get returns a container from the store by id. +func (c *memoryStore) Get(id string) *Container { + c.RLock() + res := c.s[id] + c.RUnlock() + return res +} + +// Delete removes a container from the store by id. +func (c *memoryStore) Delete(id string) { + c.Lock() + delete(c.s, id) + c.Unlock() +} + +// List returns a sorted list of containers from the store. +// The containers are ordered by creation date. +func (c *memoryStore) List() []*Container { + containers := History(c.all()) + containers.sort() + return containers +} + +// Size returns the number of containers in the store. +func (c *memoryStore) Size() int { + c.RLock() + defer c.RUnlock() + return len(c.s) +} + +// First returns the first container found in the store by a given filter. +func (c *memoryStore) First(filter StoreFilter) *Container { + for _, cont := range c.all() { + if filter(cont) { + return cont + } + } + return nil +} + +// ApplyAll calls the reducer function with every container in the store. +// This operation is asyncronous in the memory store. +// NOTE: Modifications to the store MUST NOT be done by the StoreReducer. +func (c *memoryStore) ApplyAll(apply StoreReducer) { + wg := new(sync.WaitGroup) + for _, cont := range c.all() { + wg.Add(1) + go func(container *Container) { + apply(container) + wg.Done() + }(cont) + } + + wg.Wait() +} + +func (c *memoryStore) all() []*Container { + c.RLock() + containers := make([]*Container, 0, len(c.s)) + for _, cont := range c.s { + containers = append(containers, cont) + } + c.RUnlock() + return containers +} + +var _ Store = &memoryStore{} diff --git a/container/memory_store_test.go b/container/memory_store_test.go new file mode 100644 index 00000000..f81738fa --- /dev/null +++ b/container/memory_store_test.go @@ -0,0 +1,106 @@ +package container + +import ( + "testing" + "time" +) + +func TestNewMemoryStore(t *testing.T) { + s := NewMemoryStore() + m, ok := s.(*memoryStore) + if !ok { + t.Fatalf("store is not a memory store %v", s) + } + if m.s == nil { + t.Fatal("expected store map to not be nil") + } +} + +func TestAddContainers(t *testing.T) { + s := NewMemoryStore() + s.Add("id", NewBaseContainer("id", "root")) + if s.Size() != 1 { + t.Fatalf("expected store size 1, got %v", s.Size()) + } +} + +func TestGetContainer(t *testing.T) { + s := NewMemoryStore() + s.Add("id", NewBaseContainer("id", "root")) + c := s.Get("id") + if c == nil { + t.Fatal("expected container to not be nil") + } +} + +func TestDeleteContainer(t *testing.T) { + s := NewMemoryStore() + s.Add("id", NewBaseContainer("id", "root")) + s.Delete("id") + if c := s.Get("id"); c != nil { + t.Fatalf("expected container to be nil after removal, got %v", c) + } + + if s.Size() != 0 { + t.Fatalf("expected store size to be 0, got %v", s.Size()) + } +} + +func TestListContainers(t *testing.T) { + s := NewMemoryStore() + + cont := NewBaseContainer("id", "root") + cont.Created = time.Now() + cont2 := NewBaseContainer("id2", "root") + cont2.Created = time.Now().Add(24 * time.Hour) + + s.Add("id", cont) + s.Add("id2", cont2) + + list := s.List() + if len(list) != 2 { + t.Fatalf("expected list size 2, got %v", len(list)) + } + if list[0].ID != "id2" { + t.Fatalf("expected older container to be first, got %v", list[0].ID) + } +} + +func TestFirstContainer(t *testing.T) { + s := NewMemoryStore() + + s.Add("id", NewBaseContainer("id", "root")) + s.Add("id2", NewBaseContainer("id2", "root")) + + first := s.First(func(cont *Container) bool { + return cont.ID == "id2" + }) + + if first == nil { + t.Fatal("expected container to not be nil") + } + if first.ID != "id2" { + t.Fatalf("expected id2, got %v", first) + } +} + +func TestApplyAllContainer(t *testing.T) { + s := NewMemoryStore() + + s.Add("id", NewBaseContainer("id", "root")) + s.Add("id2", NewBaseContainer("id2", "root")) + + s.ApplyAll(func(cont *Container) { + if cont.ID == "id2" { + cont.ID = "newID" + } + }) + + cont := s.Get("id2") + if cont == nil { + t.Fatal("expected container to not be nil") + } + if cont.ID != "newID" { + t.Fatalf("expected newID, got %v", cont) + } +} diff --git a/container/monitor.go b/container/monitor.go new file mode 100644 index 00000000..ba82d875 --- /dev/null +++ b/container/monitor.go @@ -0,0 +1,60 @@ +package container + +import ( + "time" + + "github.com/Sirupsen/logrus" +) + +const ( + loggerCloseTimeout = 10 * time.Second +) + +// supervisor defines the interface that a supervisor must implement +type supervisor interface { + // LogContainerEvent generates events related to a given container + LogContainerEvent(*Container, string) + // Cleanup ensures that the container is properly unmounted + Cleanup(*Container) + // StartLogging starts the logging driver for the container + StartLogging(*Container) error + // Run starts a container + Run(c *Container) error + // IsShuttingDown tells whether the supervisor is shutting down or not + IsShuttingDown() bool +} + +// Reset puts a container into a state where it can be restarted again. +func (container *Container) Reset(lock bool) { + if lock { + container.Lock() + defer container.Unlock() + } + + if err := container.CloseStreams(); err != nil { + logrus.Errorf("%s: %s", container.ID, err) + } + + // Re-create a brand new stdin pipe once the container exited + if container.Config.OpenStdin { + container.NewInputPipes() + } + + if container.LogDriver != nil { + if container.LogCopier != nil { + exit := make(chan struct{}) + go func() { + container.LogCopier.Wait() + close(exit) + }() + select { + case <-time.After(loggerCloseTimeout): + logrus.Warnf("Logger didn't exit in time: logs may be truncated") + case <-exit: + } + } + container.LogDriver.Close() + container.LogCopier = nil + container.LogDriver = nil + } +} diff --git a/container/mounts_unix.go b/container/mounts_unix.go new file mode 100644 index 00000000..c52abed2 --- /dev/null +++ b/container/mounts_unix.go @@ -0,0 +1,12 @@ +// +build !windows + +package container + +// Mount contains information for a mount operation. +type Mount struct { + Source string `json:"source"` + Destination string `json:"destination"` + Writable bool `json:"writable"` + Data string `json:"data"` + Propagation string `json:"mountpropagation"` +} diff --git a/container/mounts_windows.go b/container/mounts_windows.go new file mode 100644 index 00000000..01b327f7 --- /dev/null +++ b/container/mounts_windows.go @@ -0,0 +1,8 @@ +package container + +// Mount contains information for a mount operation. +type Mount struct { + Source string `json:"source"` + Destination string `json:"destination"` + Writable bool `json:"writable"` +} diff --git a/container/state.go b/container/state.go new file mode 100644 index 00000000..a12a193e --- /dev/null +++ b/container/state.go @@ -0,0 +1,283 @@ +package container + +import ( + "fmt" + "sync" + "time" + + "github.com/docker/go-units" +) + +// State holds the current container state, and has methods to get and +// set the state. Container has an embed, which allows all of the +// functions defined against State to run against Container. +type State struct { + sync.Mutex + // FIXME: Why do we have both paused and running if a + // container cannot be paused and running at the same time? + Running bool + Paused bool + Restarting bool + OOMKilled bool + RemovalInProgress bool // Not need for this to be persistent on disk. + Dead bool + Pid int + ExitCode int + Error string // contains last known error when starting the container + StartedAt time.Time + FinishedAt time.Time + waitChan chan struct{} +} + +// NewState creates a default state object with a fresh channel for state changes. +func NewState() *State { + return &State{ + waitChan: make(chan struct{}), + } +} + +// String returns a human-readable description of the state +func (s *State) String() string { + if s.Running { + if s.Paused { + return fmt.Sprintf("Up %s (Paused)", units.HumanDuration(time.Now().UTC().Sub(s.StartedAt))) + } + if s.Restarting { + return fmt.Sprintf("Restarting (%d) %s ago", s.ExitCode, units.HumanDuration(time.Now().UTC().Sub(s.FinishedAt))) + } + + return fmt.Sprintf("Up %s", units.HumanDuration(time.Now().UTC().Sub(s.StartedAt))) + } + + if s.RemovalInProgress { + return "Removal In Progress" + } + + if s.Dead { + return "Dead" + } + + if s.StartedAt.IsZero() { + return "Created" + } + + if s.FinishedAt.IsZero() { + return "" + } + + return fmt.Sprintf("Exited (%d) %s ago", s.ExitCode, units.HumanDuration(time.Now().UTC().Sub(s.FinishedAt))) +} + +// StateString returns a single string to describe state +func (s *State) StateString() string { + if s.Running { + if s.Paused { + return "paused" + } + if s.Restarting { + return "restarting" + } + return "running" + } + + if s.Dead { + return "dead" + } + + if s.StartedAt.IsZero() { + return "created" + } + + return "exited" +} + +// IsValidStateString checks if the provided string is a valid container state or not. +func IsValidStateString(s string) bool { + if s != "paused" && + s != "restarting" && + s != "running" && + s != "dead" && + s != "created" && + s != "exited" { + return false + } + return true +} + +func wait(waitChan <-chan struct{}, timeout time.Duration) error { + if timeout < 0 { + <-waitChan + return nil + } + select { + case <-time.After(timeout): + return fmt.Errorf("Timed out: %v", timeout) + case <-waitChan: + return nil + } +} + +// WaitRunning waits until state is running. If state is already +// running it returns immediately. If you want wait forever you must +// supply negative timeout. Returns pid, that was passed to +// SetRunning. +func (s *State) WaitRunning(timeout time.Duration) (int, error) { + s.Lock() + if s.Running { + pid := s.Pid + s.Unlock() + return pid, nil + } + waitChan := s.waitChan + s.Unlock() + if err := wait(waitChan, timeout); err != nil { + return -1, err + } + return s.GetPID(), nil +} + +// WaitStop waits until state is stopped. If state already stopped it returns +// immediately. If you want wait forever you must supply negative timeout. +// Returns exit code, that was passed to SetStoppedLocking +func (s *State) WaitStop(timeout time.Duration) (int, error) { + s.Lock() + if !s.Running { + exitCode := s.ExitCode + s.Unlock() + return exitCode, nil + } + waitChan := s.waitChan + s.Unlock() + if err := wait(waitChan, timeout); err != nil { + return -1, err + } + return s.getExitCode(), nil +} + +// IsRunning returns whether the running flag is set. Used by Container to check whether a container is running. +func (s *State) IsRunning() bool { + s.Lock() + res := s.Running + s.Unlock() + return res +} + +// GetPID holds the process id of a container. +func (s *State) GetPID() int { + s.Lock() + res := s.Pid + s.Unlock() + return res +} + +func (s *State) getExitCode() int { + s.Lock() + res := s.ExitCode + s.Unlock() + return res +} + +// SetRunning sets the state of the container to "running". +func (s *State) SetRunning(pid int, initial bool) { + s.Error = "" + s.Running = true + s.Paused = false + s.Restarting = false + s.ExitCode = 0 + s.Pid = pid + if initial { + s.StartedAt = time.Now().UTC() + } + close(s.waitChan) // fire waiters for start + s.waitChan = make(chan struct{}) +} + +// SetStoppedLocking locks the container state is sets it to "stopped". +func (s *State) SetStoppedLocking(exitStatus *ExitStatus) { + s.Lock() + s.SetStopped(exitStatus) + s.Unlock() +} + +// SetStopped sets the container state to "stopped" without locking. +func (s *State) SetStopped(exitStatus *ExitStatus) { + s.Running = false + s.Paused = false + s.Restarting = false + s.Pid = 0 + s.FinishedAt = time.Now().UTC() + s.setFromExitStatus(exitStatus) + close(s.waitChan) // fire waiters for stop + s.waitChan = make(chan struct{}) +} + +// SetRestartingLocking is when docker handles the auto restart of containers when they are +// in the middle of a stop and being restarted again +func (s *State) SetRestartingLocking(exitStatus *ExitStatus) { + s.Lock() + s.SetRestarting(exitStatus) + s.Unlock() +} + +// SetRestarting sets the container state to "restarting". +// It also sets the container PID to 0. +func (s *State) SetRestarting(exitStatus *ExitStatus) { + // we should consider the container running when it is restarting because of + // all the checks in docker around rm/stop/etc + s.Running = true + s.Restarting = true + s.Pid = 0 + s.FinishedAt = time.Now().UTC() + s.setFromExitStatus(exitStatus) + close(s.waitChan) // fire waiters for stop + s.waitChan = make(chan struct{}) +} + +// SetError sets the container's error state. This is useful when we want to +// know the error that occurred when container transits to another state +// when inspecting it +func (s *State) SetError(err error) { + s.Error = err.Error() +} + +// IsPaused returns whether the container is paused or not. +func (s *State) IsPaused() bool { + s.Lock() + res := s.Paused + s.Unlock() + return res +} + +// IsRestarting returns whether the container is restarting or not. +func (s *State) IsRestarting() bool { + s.Lock() + res := s.Restarting + s.Unlock() + return res +} + +// SetRemovalInProgress sets the container state as being removed. +// It returns true if the container was already in that state. +func (s *State) SetRemovalInProgress() bool { + s.Lock() + defer s.Unlock() + if s.RemovalInProgress { + return true + } + s.RemovalInProgress = true + return false +} + +// ResetRemovalInProgress make the RemovalInProgress state to false. +func (s *State) ResetRemovalInProgress() { + s.Lock() + s.RemovalInProgress = false + s.Unlock() +} + +// SetDead sets the container state to "dead" +func (s *State) SetDead() { + s.Lock() + s.Dead = true + s.Unlock() +} diff --git a/container/state_test.go b/container/state_test.go new file mode 100644 index 00000000..7b35b178 --- /dev/null +++ b/container/state_test.go @@ -0,0 +1,109 @@ +package container + +import ( + "sync/atomic" + "testing" + "time" +) + +func TestStateRunStop(t *testing.T) { + s := NewState() + for i := 1; i < 3; i++ { // full lifecycle two times + started := make(chan struct{}) + var pid int64 + go func() { + runPid, _ := s.WaitRunning(-1 * time.Second) + atomic.StoreInt64(&pid, int64(runPid)) + close(started) + }() + s.Lock() + s.SetRunning(i+100, false) + s.Unlock() + + if !s.IsRunning() { + t.Fatal("State not running") + } + if s.Pid != i+100 { + t.Fatalf("Pid %v, expected %v", s.Pid, i+100) + } + if s.ExitCode != 0 { + t.Fatalf("ExitCode %v, expected 0", s.ExitCode) + } + select { + case <-time.After(100 * time.Millisecond): + t.Fatal("Start callback doesn't fire in 100 milliseconds") + case <-started: + t.Log("Start callback fired") + } + runPid := int(atomic.LoadInt64(&pid)) + if runPid != i+100 { + t.Fatalf("Pid %v, expected %v", runPid, i+100) + } + if pid, err := s.WaitRunning(-1 * time.Second); err != nil || pid != i+100 { + t.Fatalf("WaitRunning returned pid: %v, err: %v, expected pid: %v, err: %v", pid, err, i+100, nil) + } + + stopped := make(chan struct{}) + var exit int64 + go func() { + exitCode, _ := s.WaitStop(-1 * time.Second) + atomic.StoreInt64(&exit, int64(exitCode)) + close(stopped) + }() + s.SetStoppedLocking(&ExitStatus{ExitCode: i}) + if s.IsRunning() { + t.Fatal("State is running") + } + if s.ExitCode != i { + t.Fatalf("ExitCode %v, expected %v", s.ExitCode, i) + } + if s.Pid != 0 { + t.Fatalf("Pid %v, expected 0", s.Pid) + } + select { + case <-time.After(100 * time.Millisecond): + t.Fatal("Stop callback doesn't fire in 100 milliseconds") + case <-stopped: + t.Log("Stop callback fired") + } + exitCode := int(atomic.LoadInt64(&exit)) + if exitCode != i { + t.Fatalf("ExitCode %v, expected %v", exitCode, i) + } + if exitCode, err := s.WaitStop(-1 * time.Second); err != nil || exitCode != i { + t.Fatalf("WaitStop returned exitCode: %v, err: %v, expected exitCode: %v, err: %v", exitCode, err, i, nil) + } + } +} + +func TestStateTimeoutWait(t *testing.T) { + s := NewState() + started := make(chan struct{}) + go func() { + s.WaitRunning(100 * time.Millisecond) + close(started) + }() + select { + case <-time.After(200 * time.Millisecond): + t.Fatal("Start callback doesn't fire in 100 milliseconds") + case <-started: + t.Log("Start callback fired") + } + + s.Lock() + s.SetRunning(49, false) + s.Unlock() + + stopped := make(chan struct{}) + go func() { + s.WaitRunning(100 * time.Millisecond) + close(stopped) + }() + select { + case <-time.After(200 * time.Millisecond): + t.Fatal("Start callback doesn't fire in 100 milliseconds") + case <-stopped: + t.Log("Start callback fired") + } + +} diff --git a/container/state_unix.go b/container/state_unix.go new file mode 100644 index 00000000..8d25a237 --- /dev/null +++ b/container/state_unix.go @@ -0,0 +1,10 @@ +// +build linux freebsd + +package container + +// setFromExitStatus is a platform specific helper function to set the state +// based on the ExitStatus structure. +func (s *State) setFromExitStatus(exitStatus *ExitStatus) { + s.ExitCode = exitStatus.ExitCode + s.OOMKilled = exitStatus.OOMKilled +} diff --git a/container/state_windows.go b/container/state_windows.go new file mode 100644 index 00000000..02802a02 --- /dev/null +++ b/container/state_windows.go @@ -0,0 +1,7 @@ +package container + +// setFromExitStatus is a platform specific helper function to set the state +// based on the ExitStatus structure. +func (s *State) setFromExitStatus(exitStatus *ExitStatus) { + s.ExitCode = exitStatus.ExitCode +} diff --git a/container/store.go b/container/store.go new file mode 100644 index 00000000..042fb1a3 --- /dev/null +++ b/container/store.go @@ -0,0 +1,28 @@ +package container + +// StoreFilter defines a function to filter +// container in the store. +type StoreFilter func(*Container) bool + +// StoreReducer defines a function to +// manipulate containers in the store +type StoreReducer func(*Container) + +// Store defines an interface that +// any container store must implement. +type Store interface { + // Add appends a new container to the store. + Add(string, *Container) + // Get returns a container from the store by the identifier it was stored with. + Get(string) *Container + // Delete removes a container from the store by the identifier it was stored with. + Delete(string) + // List returns a list of containers from the store. + List() []*Container + // Size returns the number of containers in the store. + Size() int + // First returns the first container found in the store by a given filter. + First(StoreFilter) *Container + // ApplyAll calls the reducer function with every container in the store. + ApplyAll(StoreReducer) +} diff --git a/contrib/README.md b/contrib/README.md new file mode 100644 index 00000000..92b1d944 --- /dev/null +++ b/contrib/README.md @@ -0,0 +1,4 @@ +The `contrib` directory contains scripts, images, and other helpful things +which are not part of the core docker distribution. Please note that they +could be out of date, since they do not receive the same attention as the +rest of the repository. diff --git a/contrib/REVIEWERS b/contrib/REVIEWERS new file mode 100644 index 00000000..18e05a30 --- /dev/null +++ b/contrib/REVIEWERS @@ -0,0 +1 @@ +Tianon Gravi (@tianon) diff --git a/contrib/apparmor/main.go b/contrib/apparmor/main.go new file mode 100644 index 00000000..f4a2978b --- /dev/null +++ b/contrib/apparmor/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "fmt" + "log" + "os" + "path" + "text/template" + + "github.com/docker/docker/pkg/aaparser" +) + +type profileData struct { + Version int +} + +func main() { + if len(os.Args) < 2 { + log.Fatal("pass a filename to save the profile in.") + } + + // parse the arg + apparmorProfilePath := os.Args[1] + + version, err := aaparser.GetVersion() + if err != nil { + log.Fatal(err) + } + data := profileData{ + Version: version, + } + fmt.Printf("apparmor_parser is of version %+v\n", data) + + // parse the template + compiled, err := template.New("apparmor_profile").Parse(dockerProfileTemplate) + if err != nil { + log.Fatalf("parsing template failed: %v", err) + } + + // make sure /etc/apparmor.d exists + if err := os.MkdirAll(path.Dir(apparmorProfilePath), 0755); err != nil { + log.Fatal(err) + } + + f, err := os.OpenFile(apparmorProfilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + log.Fatal(err) + } + defer f.Close() + + if err := compiled.Execute(f, data); err != nil { + log.Fatalf("executing template failed: %v", err) + } + + fmt.Printf("created apparmor profile for version %+v at %q\n", data, apparmorProfilePath) +} diff --git a/contrib/apparmor/template.go b/contrib/apparmor/template.go new file mode 100644 index 00000000..e5e1c8be --- /dev/null +++ b/contrib/apparmor/template.go @@ -0,0 +1,268 @@ +package main + +const dockerProfileTemplate = `@{DOCKER_GRAPH_PATH}=/var/lib/docker + +profile /usr/bin/docker (attach_disconnected, complain) { + # Prevent following links to these files during container setup. + deny /etc/** mkl, + deny /dev/** kl, + deny /sys/** mkl, + deny /proc/** mkl, + + mount -> @{DOCKER_GRAPH_PATH}/**, + mount -> /, + mount -> /proc/**, + mount -> /sys/**, + mount -> /run/docker/netns/**, + mount -> /.pivot_root[0-9]*/, + + / r, + + umount, + pivot_root, +{{if ge .Version 209000}} + signal (receive) peer=@{profile_name}, + signal (receive) peer=unconfined, + signal (send), +{{end}} + network, + capability, + owner /** rw, + @{DOCKER_GRAPH_PATH}/** rwl, + @{DOCKER_GRAPH_PATH}/linkgraph.db k, + @{DOCKER_GRAPH_PATH}/network/files/boltdb.db k, + @{DOCKER_GRAPH_PATH}/network/files/local-kv.db k, + @{DOCKER_GRAPH_PATH}/[0-9]*.[0-9]*/linkgraph.db k, + + # For non-root client use: + /dev/urandom r, + /dev/null rw, + /dev/pts/[0-9]* rw, + /run/docker.sock rw, + /proc/** r, + /proc/[0-9]*/attr/exec w, + /sys/kernel/mm/hugepages/ r, + /etc/localtime r, + /etc/ld.so.cache r, + /etc/passwd r, + +{{if ge .Version 209000}} + ptrace peer=@{profile_name}, + ptrace (read) peer=docker-default, + deny ptrace (trace) peer=docker-default, + deny ptrace peer=/usr/bin/docker///bin/ps, +{{end}} + + /usr/lib/** rm, + /lib/** rm, + + /usr/bin/docker pix, + /sbin/xtables-multi rCx, + /sbin/iptables rCx, + /sbin/modprobe rCx, + /sbin/auplink rCx, + /sbin/mke2fs rCx, + /sbin/tune2fs rCx, + /sbin/blkid rCx, + /bin/kmod rCx, + /usr/bin/xz rCx, + /bin/ps rCx, + /bin/tar rCx, + /bin/cat rCx, + /sbin/zfs rCx, + /sbin/apparmor_parser rCx, + +{{if ge .Version 209000}} + # Transitions + change_profile -> docker-*, + change_profile -> unconfined, +{{end}} + + profile /bin/cat (complain) { + /etc/ld.so.cache r, + /lib/** rm, + /dev/null rw, + /proc r, + /bin/cat mr, + + # For reading in 'docker stats': + /proc/[0-9]*/net/dev r, + } + profile /bin/ps (complain) { + /etc/ld.so.cache r, + /etc/localtime r, + /etc/passwd r, + /etc/nsswitch.conf r, + /lib/** rm, + /proc/[0-9]*/** r, + /dev/null rw, + /bin/ps mr, + +{{if ge .Version 209000}} + # We don't need ptrace so we'll deny and ignore the error. + deny ptrace (read, trace), +{{end}} + + # Quiet dac_override denials + deny capability dac_override, + deny capability dac_read_search, + deny capability sys_ptrace, + + /dev/tty r, + /proc/stat r, + /proc/cpuinfo r, + /proc/meminfo r, + /proc/uptime r, + /sys/devices/system/cpu/online r, + /proc/sys/kernel/pid_max r, + /proc/ r, + /proc/tty/drivers r, + } + profile /sbin/iptables (complain) { +{{if ge .Version 209000}} + signal (receive) peer=/usr/bin/docker, +{{end}} + capability net_admin, + } + profile /sbin/auplink flags=(attach_disconnected, complain) { +{{if ge .Version 209000}} + signal (receive) peer=/usr/bin/docker, +{{end}} + capability sys_admin, + capability dac_override, + + @{DOCKER_GRAPH_PATH}/aufs/** rw, + @{DOCKER_GRAPH_PATH}/tmp/** rw, + # For user namespaces: + @{DOCKER_GRAPH_PATH}/[0-9]*.[0-9]*/** rw, + + /sys/fs/aufs/** r, + /lib/** rm, + /apparmor/.null r, + /dev/null rw, + /etc/ld.so.cache r, + /sbin/auplink rm, + /proc/fs/aufs/** rw, + /proc/[0-9]*/mounts rw, + } + profile /sbin/modprobe /bin/kmod (complain) { +{{if ge .Version 209000}} + signal (receive) peer=/usr/bin/docker, +{{end}} + capability sys_module, + /etc/ld.so.cache r, + /lib/** rm, + /dev/null rw, + /apparmor/.null rw, + /sbin/modprobe rm, + /bin/kmod rm, + /proc/cmdline r, + /sys/module/** r, + /etc/modprobe.d{/,/**} r, + } + # xz works via pipes, so we do not need access to the filesystem. + profile /usr/bin/xz (complain) { +{{if ge .Version 209000}} + signal (receive) peer=/usr/bin/docker, +{{end}} + /etc/ld.so.cache r, + /lib/** rm, + /usr/bin/xz rm, + deny /proc/** rw, + deny /sys/** rw, + } + profile /sbin/xtables-multi (attach_disconnected, complain) { + /etc/ld.so.cache r, + /lib/** rm, + /sbin/xtables-multi rm, + /apparmor/.null w, + /dev/null rw, + + /proc r, + + capability net_raw, + capability net_admin, + network raw, + } + profile /sbin/zfs (attach_disconnected, complain) { + file, + capability, + } + profile /sbin/mke2fs (complain) { + /sbin/mke2fs rm, + + /lib/** rm, + + /apparmor/.null w, + + /etc/ld.so.cache r, + /etc/mke2fs.conf r, + /etc/mtab r, + + /dev/dm-* rw, + /dev/urandom r, + /dev/null rw, + + /proc/swaps r, + /proc/[0-9]*/mounts r, + } + profile /sbin/tune2fs (complain) { + /sbin/tune2fs rm, + + /lib/** rm, + + /apparmor/.null w, + + /etc/blkid.conf r, + /etc/mtab r, + /etc/ld.so.cache r, + + /dev/null rw, + /dev/.blkid.tab r, + /dev/dm-* rw, + + /proc/swaps r, + /proc/[0-9]*/mounts r, + } + profile /sbin/blkid (complain) { + /sbin/blkid rm, + + /lib/** rm, + /apparmor/.null w, + + /etc/ld.so.cache r, + /etc/blkid.conf r, + + /dev/null rw, + /dev/.blkid.tab rl, + /dev/.blkid.tab* rwl, + /dev/dm-* r, + + /sys/devices/virtual/block/** r, + + capability mknod, + + mount -> @{DOCKER_GRAPH_PATH}/**, + } + profile /sbin/apparmor_parser (complain) { + /sbin/apparmor_parser rm, + + /lib/** rm, + + /etc/ld.so.cache r, + /etc/apparmor/** r, + /etc/apparmor.d/** r, + /etc/apparmor.d/cache/** w, + + /dev/null rw, + + /sys/kernel/security/apparmor/** r, + /sys/kernel/security/apparmor/.replace w, + + /proc/[0-9]*/mounts r, + /proc/sys/kernel/osrelease r, + /proc r, + + capability mac_admin, + } +}` diff --git a/contrib/builder/deb/amd64/README.md b/contrib/builder/deb/amd64/README.md new file mode 100644 index 00000000..20a0ff10 --- /dev/null +++ b/contrib/builder/deb/amd64/README.md @@ -0,0 +1,5 @@ +# `dockercore/builder-deb` + +This image's tags contain the dependencies for building Docker `.deb`s for each of the Debian-based platforms Docker targets. + +To add new tags, see [`contrib/builder/deb/amd64` in https://github.com/docker/docker](https://github.com/docker/docker/tree/master/contrib/builder/deb/amd64), specifically the `generate.sh` script, whose usage is described in a comment at the top of the file. diff --git a/contrib/builder/deb/amd64/build.sh b/contrib/builder/deb/amd64/build.sh new file mode 100755 index 00000000..8271d9dc --- /dev/null +++ b/contrib/builder/deb/amd64/build.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e + +cd "$(dirname "$(readlink -f "$BASH_SOURCE")")" + +set -x +./generate.sh +for d in */; do + docker build -t "dockercore/builder-deb:$(basename "$d")" "$d" +done diff --git a/contrib/builder/deb/amd64/debian-jessie/Dockerfile b/contrib/builder/deb/amd64/debian-jessie/Dockerfile new file mode 100644 index 00000000..43e2cb08 --- /dev/null +++ b/contrib/builder/deb/amd64/debian-jessie/Dockerfile @@ -0,0 +1,16 @@ +# +# THIS FILE IS AUTOGENERATED; SEE "contrib/builder/deb/amd64/generate.sh"! +# + +FROM debian:jessie + +RUN apt-get update && apt-get install -y apparmor bash-completion btrfs-tools build-essential curl ca-certificates debhelper dh-apparmor dh-systemd git libapparmor-dev libdevmapper-dev libltdl-dev libsqlite3-dev pkg-config libsystemd-journal-dev --no-install-recommends && rm -rf /var/lib/apt/lists/* + +ENV GO_VERSION 1.5.4 +RUN curl -fSL "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz" | tar xzC /usr/local +ENV PATH $PATH:/usr/local/go/bin + +ENV AUTO_GOPATH 1 + +ENV DOCKER_BUILDTAGS apparmor pkcs11 selinux +ENV RUNC_BUILDTAGS apparmor selinux diff --git a/contrib/builder/deb/amd64/debian-stretch/Dockerfile b/contrib/builder/deb/amd64/debian-stretch/Dockerfile new file mode 100644 index 00000000..5bba0ae0 --- /dev/null +++ b/contrib/builder/deb/amd64/debian-stretch/Dockerfile @@ -0,0 +1,16 @@ +# +# THIS FILE IS AUTOGENERATED; SEE "contrib/builder/deb/amd64/generate.sh"! +# + +FROM debian:stretch + +RUN apt-get update && apt-get install -y apparmor bash-completion btrfs-tools build-essential curl ca-certificates debhelper dh-apparmor dh-systemd git libapparmor-dev libdevmapper-dev libltdl-dev libseccomp-dev libsqlite3-dev pkg-config libsystemd-dev --no-install-recommends && rm -rf /var/lib/apt/lists/* + +ENV GO_VERSION 1.5.4 +RUN curl -fSL "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz" | tar xzC /usr/local +ENV PATH $PATH:/usr/local/go/bin + +ENV AUTO_GOPATH 1 + +ENV DOCKER_BUILDTAGS apparmor pkcs11 seccomp selinux +ENV RUNC_BUILDTAGS apparmor seccomp selinux diff --git a/contrib/builder/deb/amd64/debian-wheezy/Dockerfile b/contrib/builder/deb/amd64/debian-wheezy/Dockerfile new file mode 100644 index 00000000..3568c69d --- /dev/null +++ b/contrib/builder/deb/amd64/debian-wheezy/Dockerfile @@ -0,0 +1,17 @@ +# +# THIS FILE IS AUTOGENERATED; SEE "contrib/builder/deb/amd64/generate.sh"! +# + +FROM debian:wheezy-backports + +RUN apt-get update && apt-get install -y -t wheezy-backports btrfs-tools --no-install-recommends && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y apparmor bash-completion build-essential curl ca-certificates debhelper dh-apparmor dh-systemd git libapparmor-dev libdevmapper-dev libltdl-dev libsqlite3-dev pkg-config --no-install-recommends && rm -rf /var/lib/apt/lists/* + +ENV GO_VERSION 1.5.4 +RUN curl -fSL "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz" | tar xzC /usr/local +ENV PATH $PATH:/usr/local/go/bin + +ENV AUTO_GOPATH 1 + +ENV DOCKER_BUILDTAGS apparmor pkcs11 selinux +ENV RUNC_BUILDTAGS apparmor selinux diff --git a/contrib/builder/deb/amd64/generate.sh b/contrib/builder/deb/amd64/generate.sh new file mode 100755 index 00000000..fe9ff224 --- /dev/null +++ b/contrib/builder/deb/amd64/generate.sh @@ -0,0 +1,131 @@ +#!/bin/bash +set -e + +# usage: ./generate.sh [versions] +# ie: ./generate.sh +# to update all Dockerfiles in this directory +# or: ./generate.sh debian-jessie +# to only update debian-jessie/Dockerfile +# or: ./generate.sh debian-newversion +# to create a new folder and a Dockerfile within it + +cd "$(dirname "$(readlink -f "$BASH_SOURCE")")" + +versions=( "$@" ) +if [ ${#versions[@]} -eq 0 ]; then + versions=( */ ) +fi +versions=( "${versions[@]%/}" ) + +for version in "${versions[@]}"; do + distro="${version%-*}" + suite="${version##*-}" + from="${distro}:${suite}" + + case "$from" in + debian:wheezy) + # add -backports, like our users have to + from+='-backports' + ;; + esac + + mkdir -p "$version" + echo "$version -> FROM $from" + cat > "$version/Dockerfile" <<-EOF + # + # THIS FILE IS AUTOGENERATED; SEE "contrib/builder/deb/amd64/generate.sh"! + # + + FROM $from + EOF + + echo >> "$version/Dockerfile" + + extraBuildTags='pkcs11' + runcBuildTags= + + # this list is sorted alphabetically; please keep it that way + packages=( + apparmor # for apparmor_parser for testing the profile + bash-completion # for bash-completion debhelper integration + btrfs-tools # for "btrfs/ioctl.h" (and "version.h" if possible) + build-essential # "essential for building Debian packages" + curl ca-certificates # for downloading Go + debhelper # for easy ".deb" building + dh-apparmor # for apparmor debhelper + dh-systemd # for systemd debhelper integration + git # for "git commit" info in "docker -v" + libapparmor-dev # for "sys/apparmor.h" + libdevmapper-dev # for "libdevmapper.h" + libltdl-dev # for pkcs11 "ltdl.h" + libseccomp-dev # for "seccomp.h" & "libseccomp.so" + libsqlite3-dev # for "sqlite3.h" + pkg-config # for detecting things like libsystemd-journal dynamically + ) + # packaging for "sd-journal.h" and libraries varies + case "$suite" in + precise|wheezy) ;; + sid|stretch|wily|xenial) packages+=( libsystemd-dev );; + *) packages+=( libsystemd-journal-dev );; + esac + + # debian wheezy & ubuntu precise do not have the right libseccomp libs + # debian jessie & ubuntu trusty have a libseccomp < 2.2.1 :( + case "$suite" in + precise|wheezy|jessie|trusty) + packages=( "${packages[@]/libseccomp-dev}" ) + runcBuildTags="apparmor selinux" + ;; + *) + extraBuildTags+=' seccomp' + runcBuildTags="apparmor seccomp selinux" + ;; + esac + + + if [ "$suite" = 'precise' ]; then + # precise has a few package issues + + # - dh-systemd doesn't exist at all + packages=( "${packages[@]/dh-systemd}" ) + + # - libdevmapper-dev is missing critical structs (too old) + packages=( "${packages[@]/libdevmapper-dev}" ) + extraBuildTags+=' exclude_graphdriver_devicemapper' + + # - btrfs-tools is missing "ioctl.h" (too old), so it's useless + # (since kernels on precise are old too, just skip btrfs entirely) + packages=( "${packages[@]/btrfs-tools}" ) + extraBuildTags+=' exclude_graphdriver_btrfs' + fi + + if [ "$suite" = 'wheezy' ]; then + # pull a couple packages from backports explicitly + # (build failures otherwise) + backportsPackages=( btrfs-tools ) + for pkg in "${backportsPackages[@]}"; do + packages=( "${packages[@]/$pkg}" ) + done + echo "RUN apt-get update && apt-get install -y -t $suite-backports ${backportsPackages[*]} --no-install-recommends && rm -rf /var/lib/apt/lists/*" >> "$version/Dockerfile" + fi + + echo "RUN apt-get update && apt-get install -y ${packages[*]} --no-install-recommends && rm -rf /var/lib/apt/lists/*" >> "$version/Dockerfile" + + echo >> "$version/Dockerfile" + + awk '$1 == "ENV" && $2 == "GO_VERSION" { print; exit }' ../../../../Dockerfile >> "$version/Dockerfile" + echo 'RUN curl -fSL "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz" | tar xzC /usr/local' >> "$version/Dockerfile" + echo 'ENV PATH $PATH:/usr/local/go/bin' >> "$version/Dockerfile" + + echo >> "$version/Dockerfile" + + echo 'ENV AUTO_GOPATH 1' >> "$version/Dockerfile" + + echo >> "$version/Dockerfile" + + # print build tags in alphabetical order + buildTags=$( echo "apparmor selinux $extraBuildTags" | xargs -n1 | sort -n | tr '\n' ' ' | sed -e 's/[[:space:]]*$//' ) + + echo "ENV DOCKER_BUILDTAGS $buildTags" >> "$version/Dockerfile" + echo "ENV RUNC_BUILDTAGS $runcBuildTags" >> "$version/Dockerfile" +done diff --git a/contrib/builder/deb/amd64/ubuntu-precise/Dockerfile b/contrib/builder/deb/amd64/ubuntu-precise/Dockerfile new file mode 100644 index 00000000..d88301ed --- /dev/null +++ b/contrib/builder/deb/amd64/ubuntu-precise/Dockerfile @@ -0,0 +1,16 @@ +# +# THIS FILE IS AUTOGENERATED; SEE "contrib/builder/deb/amd64/generate.sh"! +# + +FROM ubuntu:precise + +RUN apt-get update && apt-get install -y apparmor bash-completion build-essential curl ca-certificates debhelper dh-apparmor git libapparmor-dev libltdl-dev libsqlite3-dev pkg-config --no-install-recommends && rm -rf /var/lib/apt/lists/* + +ENV GO_VERSION 1.5.4 +RUN curl -fSL "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz" | tar xzC /usr/local +ENV PATH $PATH:/usr/local/go/bin + +ENV AUTO_GOPATH 1 + +ENV DOCKER_BUILDTAGS apparmor exclude_graphdriver_btrfs exclude_graphdriver_devicemapper pkcs11 selinux +ENV RUNC_BUILDTAGS apparmor selinux diff --git a/contrib/builder/deb/amd64/ubuntu-trusty/Dockerfile b/contrib/builder/deb/amd64/ubuntu-trusty/Dockerfile new file mode 100644 index 00000000..1e83056f --- /dev/null +++ b/contrib/builder/deb/amd64/ubuntu-trusty/Dockerfile @@ -0,0 +1,16 @@ +# +# THIS FILE IS AUTOGENERATED; SEE "contrib/builder/deb/amd64/generate.sh"! +# + +FROM ubuntu:trusty + +RUN apt-get update && apt-get install -y apparmor bash-completion btrfs-tools build-essential curl ca-certificates debhelper dh-apparmor dh-systemd git libapparmor-dev libdevmapper-dev libltdl-dev libsqlite3-dev pkg-config libsystemd-journal-dev --no-install-recommends && rm -rf /var/lib/apt/lists/* + +ENV GO_VERSION 1.5.4 +RUN curl -fSL "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz" | tar xzC /usr/local +ENV PATH $PATH:/usr/local/go/bin + +ENV AUTO_GOPATH 1 + +ENV DOCKER_BUILDTAGS apparmor pkcs11 selinux +ENV RUNC_BUILDTAGS apparmor selinux diff --git a/contrib/builder/deb/amd64/ubuntu-wily/Dockerfile b/contrib/builder/deb/amd64/ubuntu-wily/Dockerfile new file mode 100644 index 00000000..acf268f7 --- /dev/null +++ b/contrib/builder/deb/amd64/ubuntu-wily/Dockerfile @@ -0,0 +1,16 @@ +# +# THIS FILE IS AUTOGENERATED; SEE "contrib/builder/deb/amd64/generate.sh"! +# + +FROM ubuntu:wily + +RUN apt-get update && apt-get install -y apparmor bash-completion btrfs-tools build-essential curl ca-certificates debhelper dh-apparmor dh-systemd git libapparmor-dev libdevmapper-dev libltdl-dev libseccomp-dev libsqlite3-dev pkg-config libsystemd-dev --no-install-recommends && rm -rf /var/lib/apt/lists/* + +ENV GO_VERSION 1.5.4 +RUN curl -fSL "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz" | tar xzC /usr/local +ENV PATH $PATH:/usr/local/go/bin + +ENV AUTO_GOPATH 1 + +ENV DOCKER_BUILDTAGS apparmor pkcs11 seccomp selinux +ENV RUNC_BUILDTAGS apparmor seccomp selinux diff --git a/contrib/builder/deb/amd64/ubuntu-xenial/Dockerfile b/contrib/builder/deb/amd64/ubuntu-xenial/Dockerfile new file mode 100644 index 00000000..1a4cc8e4 --- /dev/null +++ b/contrib/builder/deb/amd64/ubuntu-xenial/Dockerfile @@ -0,0 +1,16 @@ +# +# THIS FILE IS AUTOGENERATED; SEE "contrib/builder/deb/amd64/generate.sh"! +# + +FROM ubuntu:xenial + +RUN apt-get update && apt-get install -y apparmor bash-completion btrfs-tools build-essential curl ca-certificates debhelper dh-apparmor dh-systemd git libapparmor-dev libdevmapper-dev libltdl-dev libseccomp-dev libsqlite3-dev pkg-config libsystemd-dev --no-install-recommends && rm -rf /var/lib/apt/lists/* + +ENV GO_VERSION 1.5.4 +RUN curl -fSL "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz" | tar xzC /usr/local +ENV PATH $PATH:/usr/local/go/bin + +ENV AUTO_GOPATH 1 + +ENV DOCKER_BUILDTAGS apparmor pkcs11 seccomp selinux +ENV RUNC_BUILDTAGS apparmor seccomp selinux diff --git a/contrib/builder/deb/armhf/debian-jessie/Dockerfile b/contrib/builder/deb/armhf/debian-jessie/Dockerfile new file mode 100644 index 00000000..fff136d7 --- /dev/null +++ b/contrib/builder/deb/armhf/debian-jessie/Dockerfile @@ -0,0 +1,10 @@ +FROM armhf/debian:jessie + +RUN apt-get update && apt-get install -y apparmor bash-completion btrfs-tools build-essential curl ca-certificates debhelper dh-apparmor dh-systemd git libapparmor-dev libdevmapper-dev libltdl-dev libsqlite3-dev libsystemd-journal-dev --no-install-recommends && rm -rf /var/lib/apt/lists/* + +ENV GO_VERSION 1.4.3 +RUN curl -fSL "https://github.com/hypriot/golang-armbuilds/releases/download/v${GO_VERSION}/go${GO_VERSION}.linux-armv7.tar.gz" | tar xzC /usr/local +ENV PATH $PATH:/usr/local/go/bin + +ENV AUTO_GOPATH 1 +ENV DOCKER_BUILDTAGS apparmor selinux diff --git a/contrib/builder/rpm/amd64/README.md b/contrib/builder/rpm/amd64/README.md new file mode 100644 index 00000000..5f2e888c --- /dev/null +++ b/contrib/builder/rpm/amd64/README.md @@ -0,0 +1,5 @@ +# `dockercore/builder-rpm` + +This image's tags contain the dependencies for building Docker `.rpm`s for each of the RPM-based platforms Docker targets. + +To add new tags, see [`contrib/builder/rpm/amd64` in https://github.com/docker/docker](https://github.com/docker/docker/tree/master/contrib/builder/rpm/amd64), specifically the `generate.sh` script, whose usage is described in a comment at the top of the file. diff --git a/contrib/builder/rpm/amd64/build.sh b/contrib/builder/rpm/amd64/build.sh new file mode 100755 index 00000000..558f7ee0 --- /dev/null +++ b/contrib/builder/rpm/amd64/build.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e + +cd "$(dirname "$(readlink -f "$BASH_SOURCE")")" + +set -x +./generate.sh +for d in */; do + docker build -t "dockercore/builder-rpm:$(basename "$d")" "$d" +done diff --git a/contrib/builder/rpm/amd64/centos-7/Dockerfile b/contrib/builder/rpm/amd64/centos-7/Dockerfile new file mode 100644 index 00000000..25d09df9 --- /dev/null +++ b/contrib/builder/rpm/amd64/centos-7/Dockerfile @@ -0,0 +1,19 @@ +# +# THIS FILE IS AUTOGENERATED; SEE "contrib/builder/rpm/amd64/generate.sh"! +# + +FROM centos:7 + +RUN yum groupinstall -y "Development Tools" +RUN yum -y swap -- remove systemd-container systemd-container-libs -- install systemd systemd-libs +RUN yum install -y btrfs-progs-devel device-mapper-devel glibc-static libselinux-devel libtool-ltdl-devel pkgconfig selinux-policy selinux-policy-devel sqlite-devel systemd-devel tar git + +ENV GO_VERSION 1.5.4 +RUN curl -fSL "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz" | tar xzC /usr/local +ENV PATH $PATH:/usr/local/go/bin + +ENV AUTO_GOPATH 1 + +ENV DOCKER_BUILDTAGS pkcs11 selinux +ENV RUNC_BUILDTAGS selinux + diff --git a/contrib/builder/rpm/amd64/fedora-22/Dockerfile b/contrib/builder/rpm/amd64/fedora-22/Dockerfile new file mode 100644 index 00000000..40138038 --- /dev/null +++ b/contrib/builder/rpm/amd64/fedora-22/Dockerfile @@ -0,0 +1,18 @@ +# +# THIS FILE IS AUTOGENERATED; SEE "contrib/builder/rpm/amd64/generate.sh"! +# + +FROM fedora:22 + +RUN dnf install -y @development-tools fedora-packager +RUN dnf install -y btrfs-progs-devel device-mapper-devel glibc-static libseccomp-devel libselinux-devel libtool-ltdl-devel pkgconfig selinux-policy selinux-policy-devel sqlite-devel systemd-devel tar git + +ENV GO_VERSION 1.5.4 +RUN curl -fSL "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz" | tar xzC /usr/local +ENV PATH $PATH:/usr/local/go/bin + +ENV AUTO_GOPATH 1 + +ENV DOCKER_BUILDTAGS pkcs11 seccomp selinux +ENV RUNC_BUILDTAGS seccomp selinux + diff --git a/contrib/builder/rpm/amd64/fedora-23/Dockerfile b/contrib/builder/rpm/amd64/fedora-23/Dockerfile new file mode 100644 index 00000000..5d311a00 --- /dev/null +++ b/contrib/builder/rpm/amd64/fedora-23/Dockerfile @@ -0,0 +1,18 @@ +# +# THIS FILE IS AUTOGENERATED; SEE "contrib/builder/rpm/amd64/generate.sh"! +# + +FROM fedora:23 + +RUN dnf install -y @development-tools fedora-packager +RUN dnf install -y btrfs-progs-devel device-mapper-devel glibc-static libseccomp-devel libselinux-devel libtool-ltdl-devel pkgconfig selinux-policy selinux-policy-devel sqlite-devel systemd-devel tar git + +ENV GO_VERSION 1.5.4 +RUN curl -fSL "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz" | tar xzC /usr/local +ENV PATH $PATH:/usr/local/go/bin + +ENV AUTO_GOPATH 1 + +ENV DOCKER_BUILDTAGS pkcs11 seccomp selinux +ENV RUNC_BUILDTAGS seccomp selinux + diff --git a/contrib/builder/rpm/amd64/generate.sh b/contrib/builder/rpm/amd64/generate.sh new file mode 100755 index 00000000..a8f9c35f --- /dev/null +++ b/contrib/builder/rpm/amd64/generate.sh @@ -0,0 +1,174 @@ +#!/bin/bash +set -e + +# usage: ./generate.sh [versions] +# ie: ./generate.sh +# to update all Dockerfiles in this directory +# or: ./generate.sh +# to only update fedora-23/Dockerfile +# or: ./generate.sh fedora-newversion +# to create a new folder and a Dockerfile within it + +cd "$(dirname "$(readlink -f "$BASH_SOURCE")")" + +versions=( "$@" ) +if [ ${#versions[@]} -eq 0 ]; then + versions=( */ ) +fi +versions=( "${versions[@]%/}" ) + +for version in "${versions[@]}"; do + distro="${version%-*}" + suite="${version##*-}" + from="${distro}:${suite}" + installer=yum + if [[ "$distro" == "fedora" ]]; then + installer=dnf + fi + + mkdir -p "$version" + echo "$version -> FROM $from" + cat > "$version/Dockerfile" <<-EOF + # + # THIS FILE IS AUTOGENERATED; SEE "contrib/builder/rpm/amd64/generate.sh"! + # + + FROM $from + EOF + + echo >> "$version/Dockerfile" + + extraBuildTags='pkcs11' + runcBuildTags= + + case "$from" in + centos:*) + # get "Development Tools" packages dependencies + echo 'RUN yum groupinstall -y "Development Tools"' >> "$version/Dockerfile" + + if [[ "$version" == "centos-7" ]]; then + echo 'RUN yum -y swap -- remove systemd-container systemd-container-libs -- install systemd systemd-libs' >> "$version/Dockerfile" + fi + ;; + oraclelinux:*) + # get "Development Tools" packages and dependencies + # we also need yum-utils for yum-config-manager to pull the latest repo file + echo 'RUN yum groupinstall -y "Development Tools"' >> "$version/Dockerfile" + ;; + opensuse:*) + # get rpm-build and curl packages and dependencies + echo 'RUN zypper --non-interactive install ca-certificates* curl gzip rpm-build' >> "$version/Dockerfile" + ;; + *) + echo "RUN ${installer} install -y @development-tools fedora-packager" >> "$version/Dockerfile" + ;; + esac + + # this list is sorted alphabetically; please keep it that way + packages=( + btrfs-progs-devel # for "btrfs/ioctl.h" (and "version.h" if possible) + device-mapper-devel # for "libdevmapper.h" + glibc-static + libseccomp-devel # for "seccomp.h" & "libseccomp.so" + libselinux-devel # for "libselinux.so" + libtool-ltdl-devel # for pkcs11 "ltdl.h" + pkgconfig # for the pkg-config command + selinux-policy + selinux-policy-devel + sqlite-devel # for "sqlite3.h" + systemd-devel # for "sd-journal.h" and libraries + tar # older versions of dev-tools do not have tar + git # required for containerd and runc clone + ) + + case "$from" in + oraclelinux:7) + # Enable the optional repository + packages=( --enablerepo=ol7_optional_latest "${packages[*]}" ) + ;; + esac + + case "$from" in + oraclelinux:6) + # doesn't use systemd, doesn't have a devel package for it + packages=( "${packages[@]/systemd-devel}" ) + ;; + esac + + # opensuse & oraclelinx:6 do not have the right libseccomp libs + # centos:7 and oraclelinux:7 have a libseccomp < 2.2.1 :( + case "$from" in + opensuse:*|oraclelinux:*|centos:7) + packages=( "${packages[@]/libseccomp-devel}" ) + runcBuildTags="selinux" + ;; + *) + extraBuildTags+=' seccomp' + runcBuildTags="seccomp selinux" + ;; + esac + + case "$from" in + opensuse:*) + packages=( "${packages[@]/btrfs-progs-devel/libbtrfs-devel}" ) + packages=( "${packages[@]/pkgconfig/pkg-config}" ) + if [[ "$from" == "opensuse:13."* ]]; then + packages+=( systemd-rpm-macros ) + fi + + # use zypper + echo "RUN zypper --non-interactive install ${packages[*]}" >> "$version/Dockerfile" + ;; + *) + echo "RUN ${installer} install -y ${packages[*]}" >> "$version/Dockerfile" + ;; + esac + + echo >> "$version/Dockerfile" + + case "$from" in + oraclelinux:6) + # We need a known version of the kernel-uek-devel headers to set CGO_CPPFLAGS, so grab the UEKR4 GA version + # This requires using yum-config-manager from yum-utils to enable the UEKR4 yum repo + echo "RUN yum install -y yum-utils && curl -o /etc/yum.repos.d/public-yum-ol6.repo http://yum.oracle.com/public-yum-ol6.repo && yum-config-manager -q --enable ol6_UEKR4" >> "$version/Dockerfile" + echo "RUN yum install -y kernel-uek-devel-4.1.12-32.el6uek" >> "$version/Dockerfile" + echo >> "$version/Dockerfile" + ;; + *) ;; + esac + + + awk '$1 == "ENV" && $2 == "GO_VERSION" { print; exit }' ../../../../Dockerfile >> "$version/Dockerfile" + echo 'RUN curl -fSL "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz" | tar xzC /usr/local' >> "$version/Dockerfile" + echo 'ENV PATH $PATH:/usr/local/go/bin' >> "$version/Dockerfile" + + echo >> "$version/Dockerfile" + + echo 'ENV AUTO_GOPATH 1' >> "$version/Dockerfile" + + echo >> "$version/Dockerfile" + + # print build tags in alphabetical order + buildTags=$( echo "selinux $extraBuildTags" | xargs -n1 | sort -n | tr '\n' ' ' | sed -e 's/[[:space:]]*$//' ) + + echo "ENV DOCKER_BUILDTAGS $buildTags" >> "$version/Dockerfile" + echo "ENV RUNC_BUILDTAGS $runcBuildTags" >> "$version/Dockerfile" + echo >> "$version/Dockerfile" + + case "$from" in + oraclelinux:6) + # We need to set the CGO_CPPFLAGS environment to use the updated UEKR4 headers with all the userns stuff. + # The ordering is very important and should not be changed. + echo 'ENV CGO_CPPFLAGS -D__EXPORTED_HEADERS__ \' >> "$version/Dockerfile" + echo ' -I/usr/src/kernels/4.1.12-32.el6uek.x86_64/arch/x86/include/generated/uapi \' >> "$version/Dockerfile" + echo ' -I/usr/src/kernels/4.1.12-32.el6uek.x86_64/arch/x86/include/uapi \' >> "$version/Dockerfile" + echo ' -I/usr/src/kernels/4.1.12-32.el6uek.x86_64/include/generated/uapi \' >> "$version/Dockerfile" + echo ' -I/usr/src/kernels/4.1.12-32.el6uek.x86_64/include/uapi \' >> "$version/Dockerfile" + echo ' -I/usr/src/kernels/4.1.12-32.el6uek.x86_64/include' >> "$version/Dockerfile" + echo >> "$version/Dockerfile" + ;; + *) ;; + esac + + +done diff --git a/contrib/builder/rpm/amd64/opensuse-13.2/Dockerfile b/contrib/builder/rpm/amd64/opensuse-13.2/Dockerfile new file mode 100644 index 00000000..348e2607 --- /dev/null +++ b/contrib/builder/rpm/amd64/opensuse-13.2/Dockerfile @@ -0,0 +1,18 @@ +# +# THIS FILE IS AUTOGENERATED; SEE "contrib/builder/rpm/amd64/generate.sh"! +# + +FROM opensuse:13.2 + +RUN zypper --non-interactive install ca-certificates* curl gzip rpm-build +RUN zypper --non-interactive install libbtrfs-devel device-mapper-devel glibc-static libselinux-devel libtool-ltdl-devel pkg-config selinux-policy selinux-policy-devel sqlite-devel systemd-devel tar git systemd-rpm-macros + +ENV GO_VERSION 1.5.4 +RUN curl -fSL "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz" | tar xzC /usr/local +ENV PATH $PATH:/usr/local/go/bin + +ENV AUTO_GOPATH 1 + +ENV DOCKER_BUILDTAGS pkcs11 selinux +ENV RUNC_BUILDTAGS selinux + diff --git a/contrib/builder/rpm/amd64/oraclelinux-6/Dockerfile b/contrib/builder/rpm/amd64/oraclelinux-6/Dockerfile new file mode 100644 index 00000000..a8f9c15e --- /dev/null +++ b/contrib/builder/rpm/amd64/oraclelinux-6/Dockerfile @@ -0,0 +1,28 @@ +# +# THIS FILE IS AUTOGENERATED; SEE "contrib/builder/rpm/amd64/generate.sh"! +# + +FROM oraclelinux:6 + +RUN yum groupinstall -y "Development Tools" +RUN yum install -y btrfs-progs-devel device-mapper-devel glibc-static libselinux-devel libtool-ltdl-devel pkgconfig selinux-policy selinux-policy-devel sqlite-devel tar git + +RUN yum install -y yum-utils && curl -o /etc/yum.repos.d/public-yum-ol6.repo http://yum.oracle.com/public-yum-ol6.repo && yum-config-manager -q --enable ol6_UEKR4 +RUN yum install -y kernel-uek-devel-4.1.12-32.el6uek + +ENV GO_VERSION 1.5.4 +RUN curl -fSL "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz" | tar xzC /usr/local +ENV PATH $PATH:/usr/local/go/bin + +ENV AUTO_GOPATH 1 + +ENV DOCKER_BUILDTAGS pkcs11 selinux +ENV RUNC_BUILDTAGS selinux + +ENV CGO_CPPFLAGS -D__EXPORTED_HEADERS__ \ + -I/usr/src/kernels/4.1.12-32.el6uek.x86_64/arch/x86/include/generated/uapi \ + -I/usr/src/kernels/4.1.12-32.el6uek.x86_64/arch/x86/include/uapi \ + -I/usr/src/kernels/4.1.12-32.el6uek.x86_64/include/generated/uapi \ + -I/usr/src/kernels/4.1.12-32.el6uek.x86_64/include/uapi \ + -I/usr/src/kernels/4.1.12-32.el6uek.x86_64/include + diff --git a/contrib/builder/rpm/amd64/oraclelinux-7/Dockerfile b/contrib/builder/rpm/amd64/oraclelinux-7/Dockerfile new file mode 100644 index 00000000..30572e43 --- /dev/null +++ b/contrib/builder/rpm/amd64/oraclelinux-7/Dockerfile @@ -0,0 +1,18 @@ +# +# THIS FILE IS AUTOGENERATED; SEE "contrib/builder/rpm/amd64/generate.sh"! +# + +FROM oraclelinux:7 + +RUN yum groupinstall -y "Development Tools" +RUN yum install -y --enablerepo=ol7_optional_latest btrfs-progs-devel device-mapper-devel glibc-static libselinux-devel libtool-ltdl-devel pkgconfig selinux-policy selinux-policy-devel sqlite-devel systemd-devel tar git + +ENV GO_VERSION 1.5.4 +RUN curl -fSL "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz" | tar xzC /usr/local +ENV PATH $PATH:/usr/local/go/bin + +ENV AUTO_GOPATH 1 + +ENV DOCKER_BUILDTAGS pkcs11 selinux +ENV RUNC_BUILDTAGS selinux + diff --git a/contrib/check-config.sh b/contrib/check-config.sh new file mode 100755 index 00000000..bcc90d4a --- /dev/null +++ b/contrib/check-config.sh @@ -0,0 +1,267 @@ +#!/usr/bin/env bash +set -e + +# bits of this were adapted from lxc-checkconfig +# see also https://github.com/lxc/lxc/blob/lxc-1.0.2/src/lxc/lxc-checkconfig.in + +possibleConfigs=( + '/proc/config.gz' + "/boot/config-$(uname -r)" + "/usr/src/linux-$(uname -r)/.config" + '/usr/src/linux/.config' +) + +if [ $# -gt 0 ]; then + CONFIG="$1" +else + : ${CONFIG:="${possibleConfigs[0]}"} +fi + +if ! command -v zgrep &> /dev/null; then + zgrep() { + zcat "$2" | grep "$1" + } +fi + +kernelVersion="$(uname -r)" +kernelMajor="${kernelVersion%%.*}" +kernelMinor="${kernelVersion#$kernelMajor.}" +kernelMinor="${kernelMinor%%.*}" + +is_set() { + zgrep "CONFIG_$1=[y|m]" "$CONFIG" > /dev/null +} +is_set_in_kernel() { + zgrep "CONFIG_$1=y" "$CONFIG" > /dev/null +} +is_set_as_module() { + zgrep "CONFIG_$1=m" "$CONFIG" > /dev/null +} + +color() { + local codes=() + if [ "$1" = 'bold' ]; then + codes=( "${codes[@]}" '1' ) + shift + fi + if [ "$#" -gt 0 ]; then + local code= + case "$1" in + # see https://en.wikipedia.org/wiki/ANSI_escape_code#Colors + black) code=30 ;; + red) code=31 ;; + green) code=32 ;; + yellow) code=33 ;; + blue) code=34 ;; + magenta) code=35 ;; + cyan) code=36 ;; + white) code=37 ;; + esac + if [ "$code" ]; then + codes=( "${codes[@]}" "$code" ) + fi + fi + local IFS=';' + echo -en '\033['"${codes[*]}"'m' +} +wrap_color() { + text="$1" + shift + color "$@" + echo -n "$text" + color reset + echo +} + +wrap_good() { + echo "$(wrap_color "$1" white): $(wrap_color "$2" green)" +} +wrap_bad() { + echo "$(wrap_color "$1" bold): $(wrap_color "$2" bold red)" +} +wrap_warning() { + wrap_color >&2 "$*" red +} + +check_flag() { + if is_set_in_kernel "$1"; then + wrap_good "CONFIG_$1" 'enabled' + elif is_set_as_module "$1"; then + wrap_good "CONFIG_$1" 'enabled (as module)' + else + wrap_bad "CONFIG_$1" 'missing' + fi +} + +check_flags() { + for flag in "$@"; do + echo "- $(check_flag "$flag")" + done +} + +check_command() { + if command -v "$1" >/dev/null 2>&1; then + wrap_good "$1 command" 'available' + else + wrap_bad "$1 command" 'missing' + fi +} + +check_device() { + if [ -c "$1" ]; then + wrap_good "$1" 'present' + else + wrap_bad "$1" 'missing' + fi +} + +check_distro_userns() { + source /etc/os-release 2>/dev/null || /bin/true + if [[ "${ID}" =~ ^(centos|rhel)$ && "${VERSION_ID}" =~ ^7 ]]; then + # this is a CentOS7 or RHEL7 system + grep -q "user_namespace.enable=1" /proc/cmdline || { + # no user namespace support enabled + wrap_bad " (RHEL7/CentOS7" "User namespaces disabled; add 'user_namespace.enable=1' to boot command line)" + } + fi +} + +if [ ! -e "$CONFIG" ]; then + wrap_warning "warning: $CONFIG does not exist, searching other paths for kernel config ..." + for tryConfig in "${possibleConfigs[@]}"; do + if [ -e "$tryConfig" ]; then + CONFIG="$tryConfig" + break + fi + done + if [ ! -e "$CONFIG" ]; then + wrap_warning "error: cannot find kernel config" + wrap_warning " try running this script again, specifying the kernel config:" + wrap_warning " CONFIG=/path/to/kernel/.config $0 or $0 /path/to/kernel/.config" + exit 1 + fi +fi + +wrap_color "info: reading kernel config from $CONFIG ..." white +echo + +echo 'Generally Necessary:' + +echo -n '- ' +cgroupSubsystemDir="$(awk '/[, ](cpu|cpuacct|cpuset|devices|freezer|memory)[, ]/ && $3 == "cgroup" { print $2 }' /proc/mounts | head -n1)" +cgroupDir="$(dirname "$cgroupSubsystemDir")" +if [ -d "$cgroupDir/cpu" -o -d "$cgroupDir/cpuacct" -o -d "$cgroupDir/cpuset" -o -d "$cgroupDir/devices" -o -d "$cgroupDir/freezer" -o -d "$cgroupDir/memory" ]; then + echo "$(wrap_good 'cgroup hierarchy' 'properly mounted') [$cgroupDir]" +else + if [ "$cgroupSubsystemDir" ]; then + echo "$(wrap_bad 'cgroup hierarchy' 'single mountpoint!') [$cgroupSubsystemDir]" + else + echo "$(wrap_bad 'cgroup hierarchy' 'nonexistent??')" + fi + echo " $(wrap_color '(see https://github.com/tianon/cgroupfs-mount)' yellow)" +fi + +if [ "$(cat /sys/module/apparmor/parameters/enabled 2>/dev/null)" = 'Y' ]; then + echo -n '- ' + if command -v apparmor_parser &> /dev/null; then + echo "$(wrap_good 'apparmor' 'enabled and tools installed')" + else + echo "$(wrap_bad 'apparmor' 'enabled, but apparmor_parser missing')" + echo -n ' ' + if command -v apt-get &> /dev/null; then + echo "$(wrap_color '(use "apt-get install apparmor" to fix this)')" + elif command -v yum &> /dev/null; then + echo "$(wrap_color '(your best bet is "yum install apparmor-parser")')" + else + echo "$(wrap_color '(look for an "apparmor" package for your distribution)')" + fi + fi +fi + +flags=( + NAMESPACES {NET,PID,IPC,UTS}_NS + DEVPTS_MULTIPLE_INSTANCES + CGROUPS CGROUP_CPUACCT CGROUP_DEVICE CGROUP_FREEZER CGROUP_SCHED CPUSETS MEMCG + KEYS + MACVLAN VETH BRIDGE BRIDGE_NETFILTER + NF_NAT_IPV4 IP_NF_FILTER IP_NF_TARGET_MASQUERADE + NETFILTER_XT_MATCH_{ADDRTYPE,CONNTRACK} + NF_NAT NF_NAT_NEEDED + + # required for bind-mounting /dev/mqueue into containers + POSIX_MQUEUE +) +check_flags "${flags[@]}" +echo + +echo 'Optional Features:' +{ + check_flags USER_NS + check_distro_userns +} +{ + check_flags SECCOMP +} +{ + check_flags CGROUP_PIDS +} +{ + check_flags MEMCG_KMEM MEMCG_SWAP MEMCG_SWAP_ENABLED + if is_set MEMCG_SWAP && ! is_set MEMCG_SWAP_ENABLED; then + echo " $(wrap_color '(note that cgroup swap accounting is not enabled in your kernel config, you can enable it by setting boot option "swapaccount=1")' bold black)" + fi +} + +if [ "$kernelMajor" -lt 3 ] || [ "$kernelMajor" -eq 3 -a "$kernelMinor" -le 18 ]; then + check_flags RESOURCE_COUNTERS +fi + +if [ "$kernelMajor" -lt 3 ] || [ "$kernelMajor" -eq 3 -a "$kernelMinor" -le 13 ]; then + netprio=NETPRIO_CGROUP +else + netprio=CGROUP_NET_PRIO +fi + +flags=( + BLK_CGROUP IOSCHED_CFQ BLK_DEV_THROTTLING + CGROUP_PERF + CGROUP_HUGETLB + NET_CLS_CGROUP $netprio + CFS_BANDWIDTH FAIR_GROUP_SCHED RT_GROUP_SCHED +) +check_flags "${flags[@]}" + +check_flags EXT3_FS EXT3_FS_XATTR EXT3_FS_POSIX_ACL EXT3_FS_SECURITY +if ! is_set EXT3_FS || ! is_set EXT3_FS_XATTR || ! is_set EXT3_FS_POSIX_ACL || ! is_set EXT3_FS_SECURITY; then + echo " $(wrap_color '(enable these ext3 configs if you are using ext3 as backing filesystem)' bold black)" +fi + +check_flags EXT4_FS EXT4_FS_POSIX_ACL EXT4_FS_SECURITY +if ! is_set EXT4_FS || ! is_set EXT4_FS_POSIX_ACL || ! is_set EXT4_FS_SECURITY; then + echo " $(wrap_color 'enable these ext4 configs if you are using ext4 as backing filesystem' bold black)" +fi + +echo '- Storage Drivers:' +{ + echo '- "'$(wrap_color 'aufs' blue)'":' + check_flags AUFS_FS | sed 's/^/ /' + if ! is_set AUFS_FS && grep -q aufs /proc/filesystems; then + echo " $(wrap_color '(note that some kernels include AUFS patches but not the AUFS_FS flag)' bold black)" + fi + + echo '- "'$(wrap_color 'btrfs' blue)'":' + check_flags BTRFS_FS | sed 's/^/ /' + + echo '- "'$(wrap_color 'devicemapper' blue)'":' + check_flags BLK_DEV_DM DM_THIN_PROVISIONING | sed 's/^/ /' + + echo '- "'$(wrap_color 'overlay' blue)'":' + check_flags OVERLAY_FS | sed 's/^/ /' + + echo '- "'$(wrap_color 'zfs' blue)'":' + echo " - $(check_device /dev/zfs)" + echo " - $(check_command zfs)" + echo " - $(check_command zpool)" +} | sed 's/^/ /' +echo + diff --git a/contrib/completion/REVIEWERS b/contrib/completion/REVIEWERS new file mode 100644 index 00000000..03ee2dde --- /dev/null +++ b/contrib/completion/REVIEWERS @@ -0,0 +1,2 @@ +Tianon Gravi (@tianon) +Jessie Frazelle (@jfrazelle) diff --git a/contrib/completion/bash/docker b/contrib/completion/bash/docker new file mode 100644 index 00000000..05ae2ca8 --- /dev/null +++ b/contrib/completion/bash/docker @@ -0,0 +1,2251 @@ +#!/bin/bash +# +# bash completion file for core docker commands +# +# This script provides completion of: +# - commands and their options +# - container ids and names +# - image repos and tags +# - filepaths +# +# To enable the completions either: +# - place this file in /etc/bash_completion.d +# or +# - copy this file to e.g. ~/.docker-completion.sh and add the line +# below to your .bashrc after bash completion features are loaded +# . ~/.docker-completion.sh +# +# Configuration: +# +# For several commands, the amount of completions can be configured by +# setting environment variables. +# +# DOCKER_COMPLETION_SHOW_NETWORK_IDS +# "no" - Show names only (default) +# "yes" - Show names and ids +# +# You can tailor completion for the "events", "history", "inspect", "run", +# "rmi" and "save" commands by settings the following environment +# variables: +# +# DOCKER_COMPLETION_SHOW_IMAGE_IDS +# "none" - Show names only (default) +# "non-intermediate" - Show names and ids, but omit intermediate image IDs +# "all" - Show names and ids, including intermediate image IDs +# +# DOCKER_COMPLETION_SHOW_TAGS +# "yes" - include tags in completion options (default) +# "no" - don't include tags in completion options + +# +# Note: +# Currently, the completions will not work if the docker daemon is not +# bound to the default communication port/socket +# If the docker daemon is using a unix socket for communication your user +# must have access to the socket for the completions to function correctly +# +# Note for developers: +# Please arrange options sorted alphabetically by long name with the short +# options immediately following their corresponding long form. +# This order should be applied to lists, alternatives and code blocks. + +__docker_previous_extglob_setting=$(shopt -p extglob) +shopt -s extglob + +__docker_q() { + docker ${host:+-H "$host"} ${config:+--config "$config"} 2>/dev/null "$@" +} + +__docker_complete_containers_all() { + local IFS=$'\n' + local containers=( $(__docker_q ps -aq --no-trunc) ) + if [ "$1" ]; then + containers=( $(__docker_q inspect --format "{{if $1}}{{.Id}}{{end}}" "${containers[@]}") ) + fi + local names=( $(__docker_q inspect --format '{{.Name}}' "${containers[@]}") ) + names=( "${names[@]#/}" ) # trim off the leading "/" from the container names + unset IFS + COMPREPLY=( $(compgen -W "${names[*]} ${containers[*]}" -- "$cur") ) +} + +__docker_complete_containers_running() { + __docker_complete_containers_all '.State.Running' +} + +__docker_complete_containers_stopped() { + __docker_complete_containers_all 'not .State.Running' +} + +__docker_complete_containers_pauseable() { + __docker_complete_containers_all 'and .State.Running (not .State.Paused)' +} + +__docker_complete_containers_unpauseable() { + __docker_complete_containers_all '.State.Paused' +} + +__docker_complete_container_names() { + local containers=( $(__docker_q ps -aq --no-trunc) ) + local names=( $(__docker_q inspect --format '{{.Name}}' "${containers[@]}") ) + names=( "${names[@]#/}" ) # trim off the leading "/" from the container names + COMPREPLY=( $(compgen -W "${names[*]}" -- "$cur") ) +} + +__docker_complete_container_ids() { + local containers=( $(__docker_q ps -aq) ) + COMPREPLY=( $(compgen -W "${containers[*]}" -- "$cur") ) +} + +__docker_complete_images() { + local images_args="" + + case "$DOCKER_COMPLETION_SHOW_IMAGE_IDS" in + all) + images_args="--no-trunc -a" + ;; + non-intermediate) + images_args="--no-trunc" + ;; + esac + + local repo_print_command + if [ "${DOCKER_COMPLETION_SHOW_TAGS:-yes}" = "yes" ]; then + repo_print_command='print $1; print $1":"$2' + else + repo_print_command='print $1' + fi + + local awk_script + case "$DOCKER_COMPLETION_SHOW_IMAGE_IDS" in + all|non-intermediate) + awk_script='NR>1 { print $3; if ($1 != "") { '"$repo_print_command"' } }' + ;; + none|*) + awk_script='NR>1 && $1 != "" { '"$repo_print_command"' }' + ;; + esac + + local images=$(__docker_q images $images_args | awk "$awk_script") + COMPREPLY=( $(compgen -W "$images" -- "$cur") ) + __ltrim_colon_completions "$cur" +} + +__docker_complete_image_repos() { + local repos="$(__docker_q images | awk 'NR>1 && $1 != "" { print $1 }')" + COMPREPLY=( $(compgen -W "$repos" -- "$cur") ) +} + +__docker_complete_image_repos_and_tags() { + local reposAndTags="$(__docker_q images | awk 'NR>1 && $1 != "" { print $1; print $1":"$2 }')" + COMPREPLY=( $(compgen -W "$reposAndTags" -- "$cur") ) + __ltrim_colon_completions "$cur" +} + +__docker_complete_containers_and_images() { + __docker_complete_containers_all + local containers=( "${COMPREPLY[@]}" ) + __docker_complete_images + COMPREPLY+=( "${containers[@]}" ) +} + +# Returns the names and optionally IDs of networks. +# The selection can be narrowed by an optional filter parameter, e.g. 'type=custom' +__docker_networks() { + local filter="$1" + # By default, only network names are completed. + # Set DOCKER_COMPLETION_SHOW_NETWORK_IDS=yes to also complete network IDs. + local fields='$2' + [ "${DOCKER_COMPLETION_SHOW_NETWORK_IDS}" = yes ] && fields='$1,$2' + __docker_q network ls --no-trunc ${filter:+-f "$filter"} | awk "NR>1 {print $fields}" + #__docker_q network ls --no-trunc | awk "NR>1 {print $fields}" +} + +__docker_complete_networks() { + COMPREPLY=( $(compgen -W "$(__docker_networks $@)" -- "$cur") ) +} + +__docker_complete_network_ids() { + COMPREPLY=( $(compgen -W "$(__docker_q network ls -q --no-trunc)" -- "$cur") ) +} + +__docker_complete_network_names() { + COMPREPLY=( $(compgen -W "$(__docker_q network ls | awk 'NR>1 {print $2}')" -- "$cur") ) +} + +__docker_complete_containers_in_network() { + local containers=$(__docker_q network inspect -f '{{range $i, $c := .Containers}}{{$i}} {{$c.Name}} {{end}}' "$1") + COMPREPLY=( $(compgen -W "$containers" -- "$cur") ) +} + +__docker_complete_volumes() { + COMPREPLY=( $(compgen -W "$(__docker_q volume ls -q)" -- "$cur") ) +} + +__docker_plugins() { + __docker_q info | sed -n "/^Plugins/,/^[^ ]/s/ $1: //p" +} + +__docker_complete_plugins() { + COMPREPLY=( $(compgen -W "$(__docker_plugins $1)" -- "$cur") ) +} + +# Finds the position of the first word that is neither option nor an option's argument. +# If there are options that require arguments, you should pass a glob describing those +# options, e.g. "--option1|-o|--option2" +# Use this function to restrict completions to exact positions after the argument list. +__docker_pos_first_nonflag() { + local argument_flags=$1 + + local counter=$((${subcommand_pos:-${command_pos}} + 1)) + while [ $counter -le $cword ]; do + if [ -n "$argument_flags" ] && eval "case '${words[$counter]}' in $argument_flags) true ;; *) false ;; esac"; then + (( counter++ )) + # eat "=" in case of --option=arg syntax + [ "${words[$counter]}" = "=" ] && (( counter++ )) + else + case "${words[$counter]}" in + -*) + ;; + *) + break + ;; + esac + fi + + # Bash splits words at "=", retaining "=" as a word, examples: + # "--debug=false" => 3 words, "--log-opt syslog-facility=daemon" => 4 words + while [ "${words[$counter + 1]}" = "=" ] ; do + counter=$(( counter + 2)) + done + + (( counter++ )) + done + + echo $counter +} + +# If we are currently completing the value of a map option (key=value) +# which matches the extglob given as an argument, returns key. +# This function is needed for key-specific completions. +__docker_map_key_of_current_option() { + local glob="$1" + + local key glob_pos + if [ "$cur" = "=" ] ; then # key= case + key="$prev" + glob_pos=$((cword - 2)) + elif [[ $cur == *=* ]] ; then # key=value case (OSX) + key=${cur%=*} + glob_pos=$((cword - 1)) + elif [ "$prev" = "=" ] ; then + key=${words[$cword - 2]} # key=value case + glob_pos=$((cword - 3)) + else + return + fi + + [ "${words[$glob_pos]}" = "=" ] && ((glob_pos--)) # --option=key=value syntax + + [[ ${words[$glob_pos]} == @($glob) ]] && echo "$key" +} + +# Returns the value of the first option matching option_glob. +# Valid values for option_glob are option names like '--log-level' and +# globs like '--log-level|-l' +# Only positions between the command and the current word are considered. +__docker_value_of_option() { + local option_extglob=$(__docker_to_extglob "$1") + + local counter=$((command_pos + 1)) + while [ $counter -lt $cword ]; do + case ${words[$counter]} in + $option_extglob ) + echo ${words[$counter + 1]} + break + ;; + esac + (( counter++ )) + done +} + +# Transforms a multiline list of strings into a single line string +# with the words separated by "|". +# This is used to prepare arguments to __docker_pos_first_nonflag(). +__docker_to_alternatives() { + local parts=( $1 ) + local IFS='|' + echo "${parts[*]}" +} + +# Transforms a multiline list of options into an extglob pattern +# suitable for use in case statements. +__docker_to_extglob() { + local extglob=$( __docker_to_alternatives "$1" ) + echo "@($extglob)" +} + +# Subcommand processing. +# Locates the first occurrence of any of the subcommands contained in the +# first argument. In case of a match, calls the corresponding completion +# function and returns 0. +# If no match is found, 1 is returned. The calling function can then +# continue processing its completion. +# +# TODO if the preceding command has options that accept arguments and an +# argument is equal ot one of the subcommands, this is falsely detected as +# a match. +__docker_subcommands() { + local subcommands="$1" + + local counter=$(($command_pos + 1)) + while [ $counter -lt $cword ]; do + case "${words[$counter]}" in + $(__docker_to_extglob "$subcommands") ) + subcommand_pos=$counter + local subcommand=${words[$counter]} + local completions_func=_docker_${command}_${subcommand} + declare -F $completions_func >/dev/null && $completions_func + return 0 + ;; + esac + (( counter++ )) + done + return 1 +} + +# suppress trailing whitespace +__docker_nospace() { + # compopt is not available in ancient bash versions + type compopt &>/dev/null && compopt -o nospace +} + +__docker_complete_resolved_hostname() { + command -v host >/dev/null 2>&1 || return + COMPREPLY=( $(host 2>/dev/null "${cur%:}" | awk '/has address/ {print $4}') ) +} + +__docker_complete_capabilities() { + # The list of capabilities is defined in types.go, ALL was added manually. + COMPREPLY=( $( compgen -W " + ALL + AUDIT_CONTROL + AUDIT_WRITE + AUDIT_READ + BLOCK_SUSPEND + CHOWN + DAC_OVERRIDE + DAC_READ_SEARCH + FOWNER + FSETID + IPC_LOCK + IPC_OWNER + KILL + LEASE + LINUX_IMMUTABLE + MAC_ADMIN + MAC_OVERRIDE + MKNOD + NET_ADMIN + NET_BIND_SERVICE + NET_BROADCAST + NET_RAW + SETFCAP + SETGID + SETPCAP + SETUID + SYS_ADMIN + SYS_BOOT + SYS_CHROOT + SYSLOG + SYS_MODULE + SYS_NICE + SYS_PACCT + SYS_PTRACE + SYS_RAWIO + SYS_RESOURCE + SYS_TIME + SYS_TTY_CONFIG + WAKE_ALARM + " -- "$cur" ) ) +} + +__docker_complete_detach-keys() { + case "$prev" in + --detach-keys) + case "$cur" in + *,) + COMPREPLY=( $( compgen -W "${cur}ctrl-" -- "$cur" ) ) + ;; + *) + COMPREPLY=( $( compgen -W "ctrl-" -- "$cur" ) ) + ;; + esac + + __docker_nospace + return + ;; + esac + return 1 +} + +__docker_complete_isolation() { + COMPREPLY=( $( compgen -W "default hyperv process" -- "$cur" ) ) +} + +__docker_complete_log_drivers() { + COMPREPLY=( $( compgen -W " + awslogs + etwlogs + fluentd + gcplogs + gelf + journald + json-file + none + splunk + syslog + " -- "$cur" ) ) +} + +__docker_complete_log_options() { + # see docs/reference/logging/index.md + local awslogs_options="awslogs-region awslogs-group awslogs-stream" + local fluentd_options="env fluentd-address fluentd-async-connect fluentd-buffer-limit fluentd-retry-wait fluentd-max-retries labels tag" + local gcplogs_options="env gcp-log-cmd gcp-project labels" + local gelf_options="env gelf-address gelf-compression-level gelf-compression-type labels tag" + local journald_options="env labels tag" + local json_file_options="env labels max-file max-size" + local syslog_options="syslog-address syslog-format syslog-tls-ca-cert syslog-tls-cert syslog-tls-key syslog-tls-skip-verify syslog-facility tag" + local splunk_options="env labels splunk-caname splunk-capath splunk-index splunk-insecureskipverify splunk-source splunk-sourcetype splunk-token splunk-url tag" + + local all_options="$fluentd_options $gcplogs_options $gelf_options $journald_options $json_file_options $syslog_options $splunk_options" + + case $(__docker_value_of_option --log-driver) in + '') + COMPREPLY=( $( compgen -W "$all_options" -S = -- "$cur" ) ) + ;; + awslogs) + COMPREPLY=( $( compgen -W "$awslogs_options" -S = -- "$cur" ) ) + ;; + fluentd) + COMPREPLY=( $( compgen -W "$fluentd_options" -S = -- "$cur" ) ) + ;; + gcplogs) + COMPREPLY=( $( compgen -W "$gcplogs_options" -S = -- "$cur" ) ) + ;; + gelf) + COMPREPLY=( $( compgen -W "$gelf_options" -S = -- "$cur" ) ) + ;; + journald) + COMPREPLY=( $( compgen -W "$journald_options" -S = -- "$cur" ) ) + ;; + json-file) + COMPREPLY=( $( compgen -W "$json_file_options" -S = -- "$cur" ) ) + ;; + syslog) + COMPREPLY=( $( compgen -W "$syslog_options" -S = -- "$cur" ) ) + ;; + splunk) + COMPREPLY=( $( compgen -W "$splunk_options" -S = -- "$cur" ) ) + ;; + *) + return + ;; + esac + + __docker_nospace +} + +__docker_complete_log_driver_options() { + local key=$(__docker_map_key_of_current_option '--log-opt') + case "$key" in + fluentd-async-connect) + COMPREPLY=( $( compgen -W "false true" -- "${cur##*=}" ) ) + return + ;; + gelf-address) + COMPREPLY=( $( compgen -W "udp" -S "://" -- "${cur##*=}" ) ) + __docker_nospace + return + ;; + gelf-compression-level) + COMPREPLY=( $( compgen -W "1 2 3 4 5 6 7 8 9" -- "${cur##*=}" ) ) + return + ;; + gelf-compression-type) + COMPREPLY=( $( compgen -W "gzip none zlib" -- "${cur##*=}" ) ) + return + ;; + syslog-address) + COMPREPLY=( $( compgen -W "tcp:// tcp+tls:// udp:// unix://" -- "${cur##*=}" ) ) + __docker_nospace + __ltrim_colon_completions "${cur}" + return + ;; + syslog-facility) + COMPREPLY=( $( compgen -W " + auth + authpriv + cron + daemon + ftp + kern + local0 + local1 + local2 + local3 + local4 + local5 + local6 + local7 + lpr + mail + news + syslog + user + uucp + " -- "${cur##*=}" ) ) + return + ;; + syslog-format) + COMPREPLY=( $( compgen -W "rfc3164 rfc5424" -- "${cur##*=}" ) ) + return + ;; + syslog-tls-@(ca-cert|cert|key)) + _filedir + return + ;; + syslog-tls-skip-verify) + COMPREPLY=( $( compgen -W "true" -- "${cur##*=}" ) ) + return + ;; + splunk-url) + COMPREPLY=( $( compgen -W "http:// https://" -- "${cur##*=}" ) ) + __docker_nospace + __ltrim_colon_completions "${cur}" + return + ;; + splunk-insecureskipverify) + COMPREPLY=( $( compgen -W "false true" -- "${cur##*=}" ) ) + return + ;; + esac + return 1 +} + +__docker_complete_log_levels() { + COMPREPLY=( $( compgen -W "debug info warn error fatal" -- "$cur" ) ) +} + +__docker_complete_restart() { + case "$prev" in + --restart) + case "$cur" in + on-failure:*) + ;; + *) + COMPREPLY=( $( compgen -W "always no on-failure on-failure: unless-stopped" -- "$cur") ) + ;; + esac + return + ;; + esac + return 1 +} + +# a selection of the available signals that is most likely of interest in the +# context of docker containers. +__docker_complete_signals() { + local signals=( + SIGCONT + SIGHUP + SIGINT + SIGKILL + SIGQUIT + SIGSTOP + SIGTERM + SIGUSR1 + SIGUSR2 + ) + COMPREPLY=( $( compgen -W "${signals[*]} ${signals[*]#SIG}" -- "$( echo $cur | tr '[:lower:]' '[:upper:]')" ) ) +} + +__docker_complete_user_group() { + if [[ $cur == *:* ]] ; then + COMPREPLY=( $(compgen -g -- "${cur#*:}") ) + else + COMPREPLY=( $(compgen -u -S : -- "$cur") ) + __docker_nospace + fi +} + +# global options that may appear after the docker command +_docker_docker() { + local boolean_options=" + $global_boolean_options + --help + --version -v + " + + case "$prev" in + --config) + _filedir -d + return + ;; + --log-level|-l) + __docker_complete_log_levels + return + ;; + $(__docker_to_extglob "$global_options_with_args") ) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "$boolean_options $global_options_with_args" -- "$cur" ) ) + ;; + *) + local counter=$( __docker_pos_first_nonflag $(__docker_to_extglob "$global_options_with_args") ) + if [ $cword -eq $counter ]; then + COMPREPLY=( $( compgen -W "${commands[*]} help" -- "$cur" ) ) + fi + ;; + esac +} + +_docker_attach() { + __docker_complete_detach-keys && return + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--detach-keys --help --no-stdin --sig-proxy=false" -- "$cur" ) ) + ;; + *) + local counter=$(__docker_pos_first_nonflag '--detach-keys') + if [ $cword -eq $counter ]; then + __docker_complete_containers_running + fi + ;; + esac +} + +_docker_build() { + local options_with_args=" + --build-arg + --cgroup-parent + --cpuset-cpus + --cpuset-mems + --cpu-shares + --cpu-period + --cpu-quota + --file -f + --isolation + --label + --memory -m + --memory-swap + --shm-size + --tag -t + --ulimit + " + + local boolean_options=" + --disable-content-trust=false + --force-rm + --help + --no-cache + --pull + --quiet -q + --rm + " + + local all_options="$options_with_args $boolean_options" + + case "$prev" in + --build-arg) + COMPREPLY=( $( compgen -e -- "$cur" ) ) + __docker_nospace + return + ;; + --file|-f) + _filedir + return + ;; + --isolation) + __docker_complete_isolation + return + ;; + --tag|-t) + __docker_complete_image_repos_and_tags + return + ;; + $(__docker_to_extglob "$options_with_args") ) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "$all_options" -- "$cur" ) ) + ;; + *) + local counter=$( __docker_pos_first_nonflag $( __docker_to_alternatives "$options_with_args" ) ) + if [ $cword -eq $counter ]; then + _filedir -d + fi + ;; + esac +} + +_docker_commit() { + case "$prev" in + --author|-a|--change|-c|--message|-m) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--author -a --change -c --help --message -m --pause=false -p=false" -- "$cur" ) ) + ;; + *) + local counter=$(__docker_pos_first_nonflag '--author|-a|--change|-c|--message|-m') + + if [ $cword -eq $counter ]; then + __docker_complete_containers_all + return + fi + (( counter++ )) + + if [ $cword -eq $counter ]; then + __docker_complete_image_repos_and_tags + return + fi + ;; + esac +} + +_docker_cp() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--follow-link -L --help" -- "$cur" ) ) + ;; + *) + local counter=$(__docker_pos_first_nonflag) + if [ $cword -eq $counter ]; then + case "$cur" in + *:) + return + ;; + *) + # combined container and filename completion + _filedir + local files=( ${COMPREPLY[@]} ) + + __docker_complete_containers_all + COMPREPLY=( $( compgen -W "${COMPREPLY[*]}" -S ':' ) ) + local containers=( ${COMPREPLY[@]} ) + + COMPREPLY=( $( compgen -W "${files[*]} ${containers[*]}" -- "$cur" ) ) + if [[ "$COMPREPLY" == *: ]]; then + __docker_nospace + fi + return + ;; + esac + fi + (( counter++ )) + + if [ $cword -eq $counter ]; then + if [ -e "$prev" ]; then + __docker_complete_containers_all + COMPREPLY=( $( compgen -W "${COMPREPLY[*]}" -S ':' ) ) + __docker_nospace + else + _filedir + fi + return + fi + ;; + esac +} + +_docker_create() { + _docker_run +} + +_docker_daemon() { + local boolean_options=" + $global_boolean_options + --disable-legacy-registry + --help + --icc=false + --ip-forward=false + --ip-masq=false + --iptables=false + --ipv6 + --raw-logs + --selinux-enabled + --userland-proxy=false + " + local options_with_args=" + $global_options_with_args + --api-cors-header + --authorization-plugin + --bip + --bridge -b + --cgroup-parent + --cluster-advertise + --cluster-store + --cluster-store-opt + --containerd + --default-gateway + --default-gateway-v6 + --default-ulimit + --dns + --dns-search + --dns-opt + --exec-opt + --exec-root + --fixed-cidr + --fixed-cidr-v6 + --graph -g + --group -G + --insecure-registry + --ip + --label + --log-driver + --log-opt + --mtu + --pidfile -p + --registry-mirror + --storage-driver -s + --storage-opt + --userns-remap + " + + __docker_complete_log_driver_options && return + + key=$(__docker_map_key_of_current_option '--cluster-store-opt') + case "$key" in + kv.*file) + cur=${cur##*=} + _filedir + return + ;; + esac + + local key=$(__docker_map_key_of_current_option '--storage-opt') + case "$key" in + dm.@(blkdiscard|override_udev_sync_check|use_deferred_@(removal|deletion))) + COMPREPLY=( $( compgen -W "false true" -- "${cur##*=}" ) ) + return + ;; + dm.fs) + COMPREPLY=( $( compgen -W "ext4 xfs" -- "${cur##*=}" ) ) + return + ;; + dm.thinpooldev) + cur=${cur##*=} + _filedir + return + ;; + esac + + case "$prev" in + --authorization-plugin) + __docker_complete_plugins Authorization + return + ;; + --cluster-store) + COMPREPLY=( $( compgen -W "consul etcd zk" -S "://" -- "$cur" ) ) + __docker_nospace + return + ;; + --cluster-store-opt) + COMPREPLY=( $( compgen -W "discovery.heartbeat discovery.ttl kv.cacertfile kv.certfile kv.keyfile kv.path" -S = -- "$cur" ) ) + __docker_nospace + return + ;; + --exec-root|--graph|-g) + _filedir -d + return + ;; + --log-driver) + __docker_complete_log_drivers + return + ;; + --containerd|--pidfile|-p|--tlscacert|--tlscert|--tlskey) + _filedir + return + ;; + --storage-driver|-s) + COMPREPLY=( $( compgen -W "aufs btrfs devicemapper overlay vfs zfs" -- "$(echo $cur | tr '[:upper:]' '[:lower:]')" ) ) + return + ;; + --storage-opt) + local devicemapper_options=" + dm.basesize + dm.blkdiscard + dm.blocksize + dm.fs + dm.loopdatasize + dm.loopmetadatasize + dm.min_free_space + dm.mkfsarg + dm.mountopt + dm.override_udev_sync_check + dm.thinpooldev + dm.use_deferred_deletion + dm.use_deferred_removal + " + local zfs_options="zfs.fsname" + + case $(__docker_value_of_option '--storage-driver|-s') in + '') + COMPREPLY=( $( compgen -W "$devicemapper_options $zfs_options" -S = -- "$cur" ) ) + ;; + devicemapper) + COMPREPLY=( $( compgen -W "$devicemapper_options" -S = -- "$cur" ) ) + ;; + zfs) + COMPREPLY=( $( compgen -W "$zfs_options" -S = -- "$cur" ) ) + ;; + *) + return + ;; + esac + __docker_nospace + return + ;; + --log-level|-l) + __docker_complete_log_levels + return + ;; + --log-opt) + __docker_complete_log_options + return + ;; + --userns-remap) + __docker_complete_user_group + return + ;; + $(__docker_to_extglob "$options_with_args") ) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "$boolean_options $options_with_args" -- "$cur" ) ) + ;; + esac +} + +_docker_diff() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) + ;; + *) + local counter=$(__docker_pos_first_nonflag) + if [ $cword -eq $counter ]; then + __docker_complete_containers_all + fi + ;; + esac +} + +_docker_events() { + local key=$(__docker_map_key_of_current_option '-f|--filter') + case "$key" in + container) + cur="${cur##*=}" + __docker_complete_containers_all + return + ;; + event) + COMPREPLY=( $( compgen -W " + attach + commit + connect + copy + create + delete + destroy + die + disconnect + exec_create + exec_start + export + import + kill + mount + oom + pause + pull + push + rename + resize + restart + start + stop + tag + top + unmount + unpause + untag + update + " -- "${cur##*=}" ) ) + return + ;; + image) + cur="${cur##*=}" + __docker_complete_images + return + ;; + network) + cur="${cur##*=}" + __docker_complete_networks + return + ;; + type) + COMPREPLY=( $( compgen -W "container image network volume" -- "${cur##*=}" ) ) + return + ;; + volume) + cur="${cur##*=}" + __docker_complete_volumes + return + ;; + esac + + case "$prev" in + --filter|-f) + COMPREPLY=( $( compgen -S = -W "container event image label network type volume" -- "$cur" ) ) + __docker_nospace + return + ;; + --since|--until) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--filter -f --help --since --until" -- "$cur" ) ) + ;; + esac +} + +_docker_exec() { + __docker_complete_detach-keys && return + + case "$prev" in + --user|-u) + __docker_complete_user_group + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--detach -d --detach-keys --help --interactive -i --privileged -t --tty -u --user" -- "$cur" ) ) + ;; + *) + __docker_complete_containers_running + ;; + esac +} + +_docker_export() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) + ;; + *) + local counter=$(__docker_pos_first_nonflag) + if [ $cword -eq $counter ]; then + __docker_complete_containers_all + fi + ;; + esac +} + +_docker_help() { + local counter=$(__docker_pos_first_nonflag) + if [ $cword -eq $counter ]; then + COMPREPLY=( $( compgen -W "${commands[*]}" -- "$cur" ) ) + fi +} + +_docker_history() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help --human=false -H=false --no-trunc --quiet -q" -- "$cur" ) ) + ;; + *) + local counter=$(__docker_pos_first_nonflag) + if [ $cword -eq $counter ]; then + __docker_complete_images + fi + ;; + esac +} + +_docker_images() { + local key=$(__docker_map_key_of_current_option '--filter|-f') + case "$key" in + dangling) + COMPREPLY=( $( compgen -W "false true" -- "${cur##*=}" ) ) + return + ;; + label) + return + ;; + esac + + case "$prev" in + --filter|-f) + COMPREPLY=( $( compgen -S = -W "dangling label" -- "$cur" ) ) + __docker_nospace + return + ;; + --format) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--all -a --digests --filter -f --format --help --no-trunc --quiet -q" -- "$cur" ) ) + ;; + =) + return + ;; + *) + __docker_complete_image_repos + ;; + esac +} + +_docker_import() { + case "$prev" in + --change|-c|--message|-m) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--change -c --help --message -m" -- "$cur" ) ) + ;; + *) + local counter=$(__docker_pos_first_nonflag '--change|-c|--message|-m') + if [ $cword -eq $counter ]; then + return + fi + (( counter++ )) + + if [ $cword -eq $counter ]; then + __docker_complete_image_repos_and_tags + return + fi + ;; + esac +} + +_docker_info() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) + ;; + esac +} + +_docker_inspect() { + case "$prev" in + --format|-f) + return + ;; + --type) + COMPREPLY=( $( compgen -W "image container" -- "$cur" ) ) + return + ;; + + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--format -f --help --size -s --type" -- "$cur" ) ) + ;; + *) + case $(__docker_value_of_option --type) in + '') + __docker_complete_containers_and_images + ;; + container) + __docker_complete_containers_all + ;; + image) + __docker_complete_images + ;; + esac + esac +} + +_docker_kill() { + case "$prev" in + --signal|-s) + __docker_complete_signals + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help --signal -s" -- "$cur" ) ) + ;; + *) + __docker_complete_containers_running + ;; + esac +} + +_docker_load() { + case "$prev" in + --input|-i) + _filedir + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help --input -i --quiet -q" -- "$cur" ) ) + ;; + esac +} + +_docker_login() { + case "$prev" in + --password|-p|--username|-u) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help --password -p --username -u" -- "$cur" ) ) + ;; + esac +} + +_docker_logout() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) + ;; + esac +} + +_docker_logs() { + case "$prev" in + --since|--tail) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--follow -f --help --since --tail --timestamps -t" -- "$cur" ) ) + ;; + *) + local counter=$(__docker_pos_first_nonflag '--tail') + if [ $cword -eq $counter ]; then + __docker_complete_containers_all + fi + ;; + esac +} + +_docker_network_connect() { + local options_with_args=" + --alias + --ip + --ip6 + --link + " + + local boolean_options=" + --help + " + + case "$prev" in + --link) + case "$cur" in + *:*) + ;; + *) + __docker_complete_containers_running + COMPREPLY=( $( compgen -W "${COMPREPLY[*]}" -S ':' ) ) + __docker_nospace + ;; + esac + return + ;; + $(__docker_to_extglob "$options_with_args") ) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "$boolean_options $options_with_args" -- "$cur" ) ) + ;; + *) + local counter=$( __docker_pos_first_nonflag $( __docker_to_alternatives "$options_with_args" ) ) + if [ $cword -eq $counter ]; then + __docker_complete_networks + elif [ $cword -eq $(($counter + 1)) ]; then + __docker_complete_containers_all + fi + ;; + esac +} + +_docker_network_create() { + case "$prev" in + --aux-address|--gateway|--internal|--ip-range|--ipam-opt|--ipv6|--opt|-o|--subnet) + return + ;; + --ipam-driver) + COMPREPLY=( $( compgen -W "default" -- "$cur" ) ) + return + ;; + --driver|-d) + local plugins=" $(__docker_plugins Network) " + # remove drivers that allow one instance only + plugins=${plugins/ host / } + plugins=${plugins/ null / } + COMPREPLY=( $(compgen -W "$plugins" -- "$cur") ) + return + ;; + --label) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--aux-address --driver -d --gateway --help --internal --ip-range --ipam-driver --ipam-opt --ipv6 --label --opt -o --subnet" -- "$cur" ) ) + ;; + esac +} + +_docker_network_disconnect() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) + ;; + *) + local counter=$(__docker_pos_first_nonflag) + if [ $cword -eq $counter ]; then + __docker_complete_networks + elif [ $cword -eq $(($counter + 1)) ]; then + __docker_complete_containers_in_network "$prev" + fi + ;; + esac +} + +_docker_network_inspect() { + case "$prev" in + --format|-f) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--format -f --help" -- "$cur" ) ) + ;; + *) + __docker_complete_networks + esac +} + +_docker_network_ls() { + local key=$(__docker_map_key_of_current_option '--filter|-f') + case "$key" in + id) + cur="${cur##*=}" + __docker_complete_network_ids + return + ;; + name) + cur="${cur##*=}" + __docker_complete_network_names + return + ;; + type) + COMPREPLY=( $( compgen -W "builtin custom" -- "${cur##*=}" ) ) + return + ;; + esac + + case "$prev" in + --filter|-f) + COMPREPLY=( $( compgen -S = -W "id name type" -- "$cur" ) ) + __docker_nospace + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--filter -f --help --no-trunc --quiet -q" -- "$cur" ) ) + ;; + esac +} + +_docker_network_rm() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) + ;; + *) + __docker_complete_networks type=custom + esac +} + +_docker_network() { + local subcommands=" + connect + create + disconnect + inspect + ls + rm + " + __docker_subcommands "$subcommands" && return + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) + ;; + *) + COMPREPLY=( $( compgen -W "$subcommands" -- "$cur" ) ) + ;; + esac +} + +_docker_pause() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) + ;; + *) + local counter=$(__docker_pos_first_nonflag) + if [ $cword -eq $counter ]; then + __docker_complete_containers_pauseable + fi + ;; + esac +} + +_docker_port() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) + ;; + *) + local counter=$(__docker_pos_first_nonflag) + if [ $cword -eq $counter ]; then + __docker_complete_containers_all + fi + ;; + esac +} + +_docker_ps() { + local key=$(__docker_map_key_of_current_option '--filter|-f') + case "$key" in + ancestor) + cur="${cur##*=}" + __docker_complete_images + return + ;; + id) + cur="${cur##*=}" + __docker_complete_container_ids + return + ;; + name) + cur="${cur##*=}" + __docker_complete_container_names + return + ;; + status) + COMPREPLY=( $( compgen -W "created dead exited paused restarting running" -- "${cur##*=}" ) ) + return + ;; + volume) + cur="${cur##*=}" + __docker_complete_volumes + return + ;; + esac + + case "$prev" in + --before|--since) + __docker_complete_containers_all + ;; + --filter|-f) + COMPREPLY=( $( compgen -S = -W "ancestor exited id label name status volume" -- "$cur" ) ) + __docker_nospace + return + ;; + --format|-n) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--all -a --before --filter -f --format --help --latest -l -n --no-trunc --quiet -q --size -s --since" -- "$cur" ) ) + ;; + esac +} + +_docker_pull() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--all-tags -a --disable-content-trust=false --help" -- "$cur" ) ) + ;; + *) + local counter=$(__docker_pos_first_nonflag) + if [ $cword -eq $counter ]; then + for arg in "${COMP_WORDS[@]}"; do + case "$arg" in + --all-tags|-a) + __docker_complete_image_repos + return + ;; + esac + done + __docker_complete_image_repos_and_tags + fi + ;; + esac +} + +_docker_push() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--disable-content-trust=false --help" -- "$cur" ) ) + ;; + *) + local counter=$(__docker_pos_first_nonflag) + if [ $cword -eq $counter ]; then + __docker_complete_image_repos_and_tags + fi + ;; + esac +} + +_docker_rename() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) + ;; + *) + local counter=$(__docker_pos_first_nonflag) + if [ $cword -eq $counter ]; then + __docker_complete_containers_all + fi + ;; + esac +} + +_docker_restart() { + case "$prev" in + --time|-t) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help --time -t" -- "$cur" ) ) + ;; + *) + __docker_complete_containers_all + ;; + esac +} + +_docker_rm() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--force -f --help --link -l --volumes -v" -- "$cur" ) ) + ;; + *) + for arg in "${COMP_WORDS[@]}"; do + case "$arg" in + --force|-f) + __docker_complete_containers_all + return + ;; + esac + done + __docker_complete_containers_stopped + ;; + esac +} + +_docker_rmi() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--force -f --help --no-prune" -- "$cur" ) ) + ;; + *) + __docker_complete_images + ;; + esac +} + +_docker_run() { + local options_with_args=" + --add-host + --attach -a + --blkio-weight + --blkio-weight-device + --cap-add + --cap-drop + --cgroup-parent + --cidfile + --cpu-period + --cpu-quota + --cpuset-cpus + --cpuset-mems + --cpu-shares + --device + --device-read-bps + --device-read-iops + --device-write-bps + --device-write-iops + --dns + --dns-opt + --dns-search + --entrypoint + --env -e + --env-file + --expose + --group-add + --hostname -h + --ip + --ip6 + --ipc + --isolation + --kernel-memory + --label-file + --label -l + --link + --log-driver + --log-opt + --mac-address + --memory -m + --memory-swap + --memory-swappiness + --memory-reservation + --name + --net + --net-alias + --oom-score-adj + --pid + --pids-limit + --publish -p + --restart + --security-opt + --shm-size + --stop-signal + --tmpfs + --ulimit + --user -u + --userns + --uts + --volume-driver + --volumes-from + --volume -v + --workdir -w + " + + local boolean_options=" + --disable-content-trust=false + --help + --interactive -i + --oom-kill-disable + --privileged + --publish-all -P + --read-only + --tty -t + " + + if [ "$command" = "run" ] ; then + options_with_args="$options_with_args + --detach-keys + " + boolean_options="$boolean_options + --detach -d + --rm + --sig-proxy=false + " + __docker_complete_detach-keys && return + fi + + local all_options="$options_with_args $boolean_options" + + + __docker_complete_log_driver_options && return + __docker_complete_restart && return + + local key=$(__docker_map_key_of_current_option '--security-opt') + case "$key" in + label) + [[ $cur == *: ]] && return + COMPREPLY=( $( compgen -W "user: role: type: level: disable" -- "${cur##*=}") ) + if [ "${COMPREPLY[*]}" != "disable" ] ; then + __docker_nospace + fi + return + ;; + seccomp) + local cur=${cur##*=} + _filedir + COMPREPLY+=( $( compgen -W "unconfined" -- "$cur" ) ) + return + ;; + esac + + case "$prev" in + --add-host) + case "$cur" in + *:) + __docker_complete_resolved_hostname + return + ;; + esac + ;; + --attach|-a) + COMPREPLY=( $( compgen -W 'stdin stdout stderr' -- "$cur" ) ) + return + ;; + --cap-add|--cap-drop) + __docker_complete_capabilities + return + ;; + --cidfile|--env-file|--label-file) + _filedir + return + ;; + --device|--tmpfs|--volume|-v) + case "$cur" in + *:*) + # TODO somehow do _filedir for stuff inside the image, if it's already specified (which is also somewhat difficult to determine) + ;; + '') + COMPREPLY=( $( compgen -W '/' -- "$cur" ) ) + __docker_nospace + ;; + /*) + _filedir + __docker_nospace + ;; + esac + return + ;; + --env|-e) + COMPREPLY=( $( compgen -e -- "$cur" ) ) + __docker_nospace + return + ;; + --ipc) + case "$cur" in + *:*) + cur="${cur#*:}" + __docker_complete_containers_running + ;; + *) + COMPREPLY=( $( compgen -W 'host container:' -- "$cur" ) ) + if [ "$COMPREPLY" = "container:" ]; then + __docker_nospace + fi + ;; + esac + return + ;; + --isolation) + __docker_complete_isolation + return + ;; + --link) + case "$cur" in + *:*) + ;; + *) + __docker_complete_containers_running + COMPREPLY=( $( compgen -W "${COMPREPLY[*]}" -S ':' ) ) + __docker_nospace + ;; + esac + return + ;; + --log-driver) + __docker_complete_log_drivers + return + ;; + --log-opt) + __docker_complete_log_options + return + ;; + --net) + case "$cur" in + container:*) + local cur=${cur#*:} + __docker_complete_containers_all + ;; + *) + COMPREPLY=( $( compgen -W "$(__docker_plugins Network) $(__docker_networks) container:" -- "$cur") ) + if [ "${COMPREPLY[*]}" = "container:" ] ; then + __docker_nospace + fi + ;; + esac + return + ;; + --security-opt) + COMPREPLY=( $( compgen -W "apparmor= label= no-new-privileges seccomp=" -- "$cur") ) + if [ "${COMPREPLY[*]}" != "no-new-privileges" ] ; then + __docker_nospace + fi + return + ;; + --user|-u) + __docker_complete_user_group + return + ;; + --userns) + COMPREPLY=( $( compgen -W "host" -- "$cur" ) ) + return + ;; + --volume-driver) + __docker_complete_plugins Volume + return + ;; + --volumes-from) + __docker_complete_containers_all + return + ;; + $(__docker_to_extglob "$options_with_args") ) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "$all_options" -- "$cur" ) ) + ;; + *) + local counter=$( __docker_pos_first_nonflag $( __docker_to_alternatives "$options_with_args" ) ) + if [ $cword -eq $counter ]; then + __docker_complete_images + fi + ;; + esac +} + +_docker_save() { + case "$prev" in + --output|-o) + _filedir + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help --output -o" -- "$cur" ) ) + ;; + *) + __docker_complete_images + ;; + esac +} + +_docker_search() { + case "$prev" in + --stars|-s) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--automated --help --no-trunc --stars -s" -- "$cur" ) ) + ;; + esac +} + +_docker_start() { + __docker_complete_detach-keys && return + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--attach -a --detach-keys --help --interactive -i" -- "$cur" ) ) + ;; + *) + __docker_complete_containers_stopped + ;; + esac +} + +_docker_stats() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--all -a --help --no-stream" -- "$cur" ) ) + ;; + *) + __docker_complete_containers_running + ;; + esac +} + +_docker_stop() { + case "$prev" in + --time|-t) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help --time -t" -- "$cur" ) ) + ;; + *) + __docker_complete_containers_running + ;; + esac +} + +_docker_tag() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) + ;; + *) + local counter=$(__docker_pos_first_nonflag) + + if [ $cword -eq $counter ]; then + __docker_complete_image_repos_and_tags + return + fi + (( counter++ )) + + if [ $cword -eq $counter ]; then + __docker_complete_image_repos_and_tags + return + fi + ;; + esac +} + +_docker_unpause() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) + ;; + *) + local counter=$(__docker_pos_first_nonflag) + if [ $cword -eq $counter ]; then + __docker_complete_containers_unpauseable + fi + ;; + esac +} + +_docker_update() { + local options_with_args=" + --blkio-weight + --cpu-period + --cpu-quota + --cpuset-cpus + --cpuset-mems + --cpu-shares + --kernel-memory + --memory -m + --memory-reservation + --memory-swap + --restart + " + + local boolean_options=" + --help + " + + local all_options="$options_with_args $boolean_options" + + __docker_complete_restart && return + + case "$prev" in + $(__docker_to_extglob "$options_with_args") ) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "$all_options" -- "$cur" ) ) + ;; + *) + __docker_complete_containers_all + ;; + esac +} + +_docker_top() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) + ;; + *) + local counter=$(__docker_pos_first_nonflag) + if [ $cword -eq $counter ]; then + __docker_complete_containers_running + fi + ;; + esac +} + +_docker_version() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) + ;; + esac +} + +_docker_volume_create() { + case "$prev" in + --driver|-d) + __docker_complete_plugins Volume + return + ;; + --label|--name|--opt|-o) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--driver -d --help --label --name --opt -o" -- "$cur" ) ) + ;; + esac +} + +_docker_volume_inspect() { + case "$prev" in + --format|-f) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--format -f --help" -- "$cur" ) ) + ;; + *) + __docker_complete_volumes + ;; + esac +} + +_docker_volume_ls() { + local key=$(__docker_map_key_of_current_option '--filter|-f') + case "$key" in + dangling) + COMPREPLY=( $( compgen -W "true false" -- "${cur##*=}" ) ) + return + ;; + esac + + case "$prev" in + --filter|-f) + COMPREPLY=( $( compgen -S = -W "dangling" -- "$cur" ) ) + __docker_nospace + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--filter -f --help --quiet -q" -- "$cur" ) ) + ;; + esac +} + +_docker_volume_rm() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) + ;; + *) + __docker_complete_volumes + ;; + esac +} + +_docker_volume() { + local subcommands=" + create + inspect + ls + rm + " + __docker_subcommands "$subcommands" && return + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) + ;; + *) + COMPREPLY=( $( compgen -W "$subcommands" -- "$cur" ) ) + ;; + esac +} + +_docker_wait() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) + ;; + *) + __docker_complete_containers_all + ;; + esac +} + +_docker() { + local previous_extglob_setting=$(shopt -p extglob) + shopt -s extglob + + local commands=( + attach + build + commit + cp + create + daemon + diff + events + exec + export + history + images + import + info + inspect + kill + load + login + logout + logs + network + pause + port + ps + pull + push + rename + restart + rm + rmi + run + save + search + start + stats + stop + tag + top + unpause + update + version + volume + wait + ) + + # These options are valid as global options for all client commands + # and valid as command options for `docker daemon` + local global_boolean_options=" + --debug -D + --tls + --tlsverify + " + local global_options_with_args=" + --config + --host -H + --log-level -l + --tlscacert + --tlscert + --tlskey + " + + local host config + + COMPREPLY=() + local cur prev words cword + _get_comp_words_by_ref -n : cur prev words cword + + local command='docker' command_pos=0 subcommand_pos + local counter=1 + while [ $counter -lt $cword ]; do + case "${words[$counter]}" in + # save host so that completion can use custom daemon + --host|-H) + (( counter++ )) + host="${words[$counter]}" + ;; + # save config so that completion can use custom configuration directories + --config) + (( counter++ )) + config="${words[$counter]}" + ;; + $(__docker_to_extglob "$global_options_with_args") ) + (( counter++ )) + ;; + -*) + ;; + =) + (( counter++ )) + ;; + *) + command="${words[$counter]}" + command_pos=$counter + break + ;; + esac + (( counter++ )) + done + + local completions_func=_docker_${command} + declare -F $completions_func >/dev/null && $completions_func + + eval "$previous_extglob_setting" + return 0 +} + +eval "$__docker_previous_extglob_setting" +unset __docker_previous_extglob_setting + +complete -F _docker docker diff --git a/contrib/completion/fish/docker.fish b/contrib/completion/fish/docker.fish new file mode 100644 index 00000000..72ccd055 --- /dev/null +++ b/contrib/completion/fish/docker.fish @@ -0,0 +1,400 @@ +# docker.fish - docker completions for fish shell +# +# This file is generated by gen_docker_fish_completions.py from: +# https://github.com/barnybug/docker-fish-completion +# +# To install the completions: +# mkdir -p ~/.config/fish/completions +# cp docker.fish ~/.config/fish/completions +# +# Completion supported: +# - parameters +# - commands +# - containers +# - images +# - repositories + +function __fish_docker_no_subcommand --description 'Test if docker has yet to be given the subcommand' + for i in (commandline -opc) + if contains -- $i attach build commit cp create diff events exec export history images import info inspect kill load login logout logs pause port ps pull push rename restart rm rmi run save search start stop tag top unpause version wait stats + return 1 + end + end + return 0 +end + +function __fish_print_docker_containers --description 'Print a list of docker containers' -a select + switch $select + case running + docker ps -a --no-trunc | command awk 'NR>1' | command awk 'BEGIN {FS=" +"}; $5 ~ "^Up" {print $1 "\n" $(NF)}' | tr ',' '\n' + case stopped + docker ps -a --no-trunc | command awk 'NR>1' | command awk 'BEGIN {FS=" +"}; $5 ~ "^Exit" {print $1 "\n" $(NF)}' | tr ',' '\n' + case all + docker ps -a --no-trunc | command awk 'NR>1' | command awk 'BEGIN {FS=" +"}; {print $1 "\n" $(NF)}' | tr ',' '\n' + end +end + +function __fish_print_docker_images --description 'Print a list of docker images' + docker images | command awk 'NR>1' | command grep -v '' | command awk '{print $1":"$2}' +end + +function __fish_print_docker_repositories --description 'Print a list of docker repositories' + docker images | command awk 'NR>1' | command grep -v '' | command awk '{print $1}' | command sort | command uniq +end + +# common options +complete -c docker -f -n '__fish_docker_no_subcommand' -l api-cors-header -d "Set CORS headers in the remote API. Default is cors disabled" +complete -c docker -f -n '__fish_docker_no_subcommand' -s b -l bridge -d 'Attach containers to a pre-existing network bridge' +complete -c docker -f -n '__fish_docker_no_subcommand' -l bip -d "Use this CIDR notation address for the network bridge's IP, not compatible with -b" +complete -c docker -f -n '__fish_docker_no_subcommand' -s D -l debug -d 'Enable debug mode' +complete -c docker -f -n '__fish_docker_no_subcommand' -s d -l daemon -d 'Enable daemon mode' +complete -c docker -f -n '__fish_docker_no_subcommand' -l dns -d 'Force Docker to use specific DNS servers' +complete -c docker -f -n '__fish_docker_no_subcommand' -l dns-opt -d 'Force Docker to use specific DNS options' +complete -c docker -f -n '__fish_docker_no_subcommand' -l dns-search -d 'Force Docker to use specific DNS search domains' +complete -c docker -f -n '__fish_docker_no_subcommand' -l exec-opt -d 'Set runtime execution options' +complete -c docker -f -n '__fish_docker_no_subcommand' -l fixed-cidr -d 'IPv4 subnet for fixed IPs (e.g. 10.20.0.0/16)' +complete -c docker -f -n '__fish_docker_no_subcommand' -l fixed-cidr-v6 -d 'IPv6 subnet for fixed IPs (e.g.: 2001:a02b/48)' +complete -c docker -f -n '__fish_docker_no_subcommand' -s G -l group -d 'Group to assign the unix socket specified by -H when running in daemon mode' +complete -c docker -f -n '__fish_docker_no_subcommand' -s g -l graph -d 'Path to use as the root of the Docker runtime' +complete -c docker -f -n '__fish_docker_no_subcommand' -s H -l host -d 'The socket(s) to bind to in daemon mode or connect to in client mode, specified using one or more tcp://host:port, unix:///path/to/socket, fd://* or fd://socketfd.' +complete -c docker -f -n '__fish_docker_no_subcommand' -s h -l help -d 'Print usage' +complete -c docker -f -n '__fish_docker_no_subcommand' -l icc -d 'Allow unrestricted inter-container and Docker daemon host communication' +complete -c docker -f -n '__fish_docker_no_subcommand' -l insecure-registry -d 'Enable insecure communication with specified registries (no certificate verification for HTTPS and enable HTTP fallback) (e.g., localhost:5000 or 10.20.0.0/16)' +complete -c docker -f -n '__fish_docker_no_subcommand' -l ip -d 'Default IP address to use when binding container ports' +complete -c docker -f -n '__fish_docker_no_subcommand' -l ip-forward -d 'Enable net.ipv4.ip_forward and IPv6 forwarding if --fixed-cidr-v6 is defined. IPv6 forwarding may interfere with your existing IPv6 configuration when using Router Advertisement.' +complete -c docker -f -n '__fish_docker_no_subcommand' -l ip-masq -d "Enable IP masquerading for bridge's IP range" +complete -c docker -f -n '__fish_docker_no_subcommand' -l iptables -d "Enable Docker's addition of iptables rules" +complete -c docker -f -n '__fish_docker_no_subcommand' -l ipv6 -d 'Enable IPv6 networking' +complete -c docker -f -n '__fish_docker_no_subcommand' -s l -l log-level -d 'Set the logging level (debug, info, warn, error, fatal)' +complete -c docker -f -n '__fish_docker_no_subcommand' -l label -d 'Set key=value labels to the daemon (displayed in `docker info`)' +complete -c docker -f -n '__fish_docker_no_subcommand' -l mtu -d 'Set the containers network MTU' +complete -c docker -f -n '__fish_docker_no_subcommand' -s p -l pidfile -d 'Path to use for daemon PID file' +complete -c docker -f -n '__fish_docker_no_subcommand' -l registry-mirror -d 'Specify a preferred Docker registry mirror' +complete -c docker -f -n '__fish_docker_no_subcommand' -s s -l storage-driver -d 'Force the Docker runtime to use a specific storage driver' +complete -c docker -f -n '__fish_docker_no_subcommand' -l selinux-enabled -d 'Enable selinux support. SELinux does not presently support the BTRFS storage driver' +complete -c docker -f -n '__fish_docker_no_subcommand' -l storage-opt -d 'Set storage driver options' +complete -c docker -f -n '__fish_docker_no_subcommand' -l tls -d 'Use TLS; implied by --tlsverify' +complete -c docker -f -n '__fish_docker_no_subcommand' -l tlscacert -d 'Trust only remotes providing a certificate signed by the CA given here' +complete -c docker -f -n '__fish_docker_no_subcommand' -l tlscert -d 'Path to TLS certificate file' +complete -c docker -f -n '__fish_docker_no_subcommand' -l tlskey -d 'Path to TLS key file' +complete -c docker -f -n '__fish_docker_no_subcommand' -l tlsverify -d 'Use TLS and verify the remote (daemon: verify client, client: verify daemon)' +complete -c docker -f -n '__fish_docker_no_subcommand' -s v -l version -d 'Print version information and quit' + +# subcommands +# attach +complete -c docker -f -n '__fish_docker_no_subcommand' -a attach -d 'Attach to a running container' +complete -c docker -A -f -n '__fish_seen_subcommand_from attach' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from attach' -l no-stdin -d 'Do not attach STDIN' +complete -c docker -A -f -n '__fish_seen_subcommand_from attach' -l sig-proxy -d 'Proxy all received signals to the process (non-TTY mode only). SIGCHLD, SIGKILL, and SIGSTOP are not proxied.' +complete -c docker -A -f -n '__fish_seen_subcommand_from attach' -a '(__fish_print_docker_containers running)' -d "Container" + +# build +complete -c docker -f -n '__fish_docker_no_subcommand' -a build -d 'Build an image from a Dockerfile' +complete -c docker -A -f -n '__fish_seen_subcommand_from build' -s f -l file -d "Name of the Dockerfile(Default is 'Dockerfile' at context root)" +complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l force-rm -d 'Always remove intermediate containers, even after unsuccessful builds' +complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l no-cache -d 'Do not use cache when building the image' +complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l pull -d 'Always attempt to pull a newer version of the image' +complete -c docker -A -f -n '__fish_seen_subcommand_from build' -s q -l quiet -d 'Suppress the build output and print image ID on success' +complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l rm -d 'Remove intermediate containers after a successful build' +complete -c docker -A -f -n '__fish_seen_subcommand_from build' -s t -l tag -d 'Repository name (and optionally a tag) to be applied to the resulting image in case of success' + +# commit +complete -c docker -f -n '__fish_docker_no_subcommand' -a commit -d "Create a new image from a container's changes" +complete -c docker -A -f -n '__fish_seen_subcommand_from commit' -s a -l author -d 'Author (e.g., "John Hannibal Smith ")' +complete -c docker -A -f -n '__fish_seen_subcommand_from commit' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from commit' -s m -l message -d 'Commit message' +complete -c docker -A -f -n '__fish_seen_subcommand_from commit' -s p -l pause -d 'Pause container during commit' +complete -c docker -A -f -n '__fish_seen_subcommand_from commit' -a '(__fish_print_docker_containers all)' -d "Container" + +# cp +complete -c docker -f -n '__fish_docker_no_subcommand' -a cp -d "Copy files/folders between a container and the local filesystem" +complete -c docker -A -f -n '__fish_seen_subcommand_from cp' -l help -d 'Print usage' + +# create +complete -c docker -f -n '__fish_docker_no_subcommand' -a create -d 'Create a new container' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s a -l attach -d 'Attach to STDIN, STDOUT or STDERR.' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l add-host -d 'Add a custom host-to-IP mapping (host:ip)' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l cpu-shares -d 'CPU shares (relative weight)' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l cap-add -d 'Add Linux capabilities' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l cap-drop -d 'Drop Linux capabilities' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l cidfile -d 'Write the container ID to the file' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l cpuset -d 'CPUs in which to allow execution (0-3, 0,1)' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l device -d 'Add a host device to the container (e.g. --device=/dev/sdc:/dev/xvdc:rwm)' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l dns -d 'Set custom DNS servers' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l dns-opt -d "Set custom DNS options (Use --dns-opt='' if you don't wish to set options)" +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l dns-search -d "Set custom DNS search domains (Use --dns-search=. if you don't wish to set the search domain)" +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s e -l env -d 'Set environment variables' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l entrypoint -d 'Overwrite the default ENTRYPOINT of the image' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l env-file -d 'Read in a line delimited file of environment variables' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l expose -d 'Expose a port or a range of ports (e.g. --expose=3300-3310) from the container without publishing it to your host' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l group-add -d 'Add additional groups to run as' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s h -l hostname -d 'Container host name' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s i -l interactive -d 'Keep STDIN open even if not attached' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l ipc -d 'Default is to create a private IPC namespace (POSIX SysV IPC) for the container' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l link -d 'Add link to another container in the form of :alias' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s m -l memory -d 'Memory limit (format: [], where unit = b, k, m or g)' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l mac-address -d 'Container MAC address (e.g. 92:d0:c6:0a:29:33)' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l memory-swap -d "Total memory usage (memory + swap), set '-1' to disable swap (format: [], where unit = b, k, m or g)" +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l name -d 'Assign a name to the container' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l net -d 'Set the Network mode for the container' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s P -l publish-all -d 'Publish all exposed ports to random ports on the host interfaces' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s p -l publish -d "Publish a container's port to the host" +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l pid -d 'Default is to create a private PID namespace for the container' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l privileged -d 'Give extended privileges to this container' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l read-only -d "Mount the container's root filesystem as read only" +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l restart -d 'Restart policy to apply when a container exits (no, on-failure[:max-retry], always)' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l security-opt -d 'Security Options' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s t -l tty -d 'Allocate a pseudo-TTY' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s u -l user -d 'Username or UID' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s v -l volume -d 'Bind mount a volume (e.g., from the host: -v /host:/container, from Docker: -v /container)' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l volumes-from -d 'Mount volumes from the specified container(s)' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s w -l workdir -d 'Working directory inside the container' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -a '(__fish_print_docker_images)' -d "Image" + +# diff +complete -c docker -f -n '__fish_docker_no_subcommand' -a diff -d "Inspect changes on a container's filesystem" +complete -c docker -A -f -n '__fish_seen_subcommand_from diff' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from diff' -a '(__fish_print_docker_containers all)' -d "Container" + +# events +complete -c docker -f -n '__fish_docker_no_subcommand' -a events -d 'Get real time events from the server' +complete -c docker -A -f -n '__fish_seen_subcommand_from events' -s f -l filter -d "Provide filter values (i.e., 'event=stop')" +complete -c docker -A -f -n '__fish_seen_subcommand_from events' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from events' -l since -d 'Show all events created since timestamp' +complete -c docker -A -f -n '__fish_seen_subcommand_from events' -l until -d 'Stream events until this timestamp' + +# exec +complete -c docker -f -n '__fish_docker_no_subcommand' -a exec -d 'Run a command in a running container' +complete -c docker -A -f -n '__fish_seen_subcommand_from exec' -s d -l detach -d 'Detached mode: run command in the background' +complete -c docker -A -f -n '__fish_seen_subcommand_from exec' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from exec' -s i -l interactive -d 'Keep STDIN open even if not attached' +complete -c docker -A -f -n '__fish_seen_subcommand_from exec' -s t -l tty -d 'Allocate a pseudo-TTY' +complete -c docker -A -f -n '__fish_seen_subcommand_from exec' -a '(__fish_print_docker_containers running)' -d "Container" + +# export +complete -c docker -f -n '__fish_docker_no_subcommand' -a export -d 'Stream the contents of a container as a tar archive' +complete -c docker -A -f -n '__fish_seen_subcommand_from export' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from export' -a '(__fish_print_docker_containers all)' -d "Container" + +# history +complete -c docker -f -n '__fish_docker_no_subcommand' -a history -d 'Show the history of an image' +complete -c docker -A -f -n '__fish_seen_subcommand_from history' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from history' -l no-trunc -d "Don't truncate output" +complete -c docker -A -f -n '__fish_seen_subcommand_from history' -s q -l quiet -d 'Only show numeric IDs' +complete -c docker -A -f -n '__fish_seen_subcommand_from history' -a '(__fish_print_docker_images)' -d "Image" + +# images +complete -c docker -f -n '__fish_docker_no_subcommand' -a images -d 'List images' +complete -c docker -A -f -n '__fish_seen_subcommand_from images' -s a -l all -d 'Show all images (by default filter out the intermediate image layers)' +complete -c docker -A -f -n '__fish_seen_subcommand_from images' -s f -l filter -d "Provide filter values (i.e., 'dangling=true')" +complete -c docker -A -f -n '__fish_seen_subcommand_from images' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from images' -l no-trunc -d "Don't truncate output" +complete -c docker -A -f -n '__fish_seen_subcommand_from images' -s q -l quiet -d 'Only show numeric IDs' +complete -c docker -A -f -n '__fish_seen_subcommand_from images' -a '(__fish_print_docker_repositories)' -d "Repository" + +# import +complete -c docker -f -n '__fish_docker_no_subcommand' -a import -d 'Create a new filesystem image from the contents of a tarball' +complete -c docker -A -f -n '__fish_seen_subcommand_from import' -l help -d 'Print usage' + +# info +complete -c docker -f -n '__fish_docker_no_subcommand' -a info -d 'Display system-wide information' + +# inspect +complete -c docker -f -n '__fish_docker_no_subcommand' -a inspect -d 'Return low-level information on a container or image' +complete -c docker -A -f -n '__fish_seen_subcommand_from inspect' -s f -l format -d 'Format the output using the given go template.' +complete -c docker -A -f -n '__fish_seen_subcommand_from inspect' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from inspect' -s s -l size -d 'Display total file sizes if the type is container.' +complete -c docker -A -f -n '__fish_seen_subcommand_from inspect' -a '(__fish_print_docker_images)' -d "Image" +complete -c docker -A -f -n '__fish_seen_subcommand_from inspect' -a '(__fish_print_docker_containers all)' -d "Container" + +# kill +complete -c docker -f -n '__fish_docker_no_subcommand' -a kill -d 'Kill a running container' +complete -c docker -A -f -n '__fish_seen_subcommand_from kill' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from kill' -s s -l signal -d 'Signal to send to the container' +complete -c docker -A -f -n '__fish_seen_subcommand_from kill' -a '(__fish_print_docker_containers running)' -d "Container" + +# load +complete -c docker -f -n '__fish_docker_no_subcommand' -a load -d 'Load an image from a tar archive' +complete -c docker -A -f -n '__fish_seen_subcommand_from load' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from load' -s i -l input -d 'Read from a tar archive file, instead of STDIN' + +# login +complete -c docker -f -n '__fish_docker_no_subcommand' -a login -d 'Log in to a Docker registry server' +complete -c docker -A -f -n '__fish_seen_subcommand_from login' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from login' -s p -l password -d 'Password' +complete -c docker -A -f -n '__fish_seen_subcommand_from login' -s u -l username -d 'Username' + +# logout +complete -c docker -f -n '__fish_docker_no_subcommand' -a logout -d 'Log out from a Docker registry server' + +# logs +complete -c docker -f -n '__fish_docker_no_subcommand' -a logs -d 'Fetch the logs of a container' +complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -s f -l follow -d 'Follow log output' +complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -s t -l timestamps -d 'Show timestamps' +complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -l since -d 'Show logs since timestamp' +complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -l tail -d 'Output the specified number of lines at the end of logs (defaults to all logs)' +complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -a '(__fish_print_docker_containers running)' -d "Container" + +# port +complete -c docker -f -n '__fish_docker_no_subcommand' -a port -d 'Lookup the public-facing port that is NAT-ed to PRIVATE_PORT' +complete -c docker -A -f -n '__fish_seen_subcommand_from port' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from port' -a '(__fish_print_docker_containers running)' -d "Container" + +# pause +complete -c docker -f -n '__fish_docker_no_subcommand' -a pause -d 'Pause all processes within a container' +complete -c docker -A -f -n '__fish_seen_subcommand_from pause' -a '(__fish_print_docker_containers running)' -d "Container" + +# ps +complete -c docker -f -n '__fish_docker_no_subcommand' -a ps -d 'List containers' +complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -s a -l all -d 'Show all containers. Only running containers are shown by default.' +complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -l before -d 'Show only container created before Id or Name, include non-running ones.' +complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -s f -l filter -d 'Provide filter values. Valid filters:' +complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -s l -l latest -d 'Show only the latest created container, include non-running ones.' +complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -s n -d 'Show n last created containers, include non-running ones.' +complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -l no-trunc -d "Don't truncate output" +complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -s q -l quiet -d 'Only display numeric IDs' +complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -s s -l size -d 'Display total file sizes' +complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -l since -d 'Show only containers created since Id or Name, include non-running ones.' + +# pull +complete -c docker -f -n '__fish_docker_no_subcommand' -a pull -d 'Pull an image or a repository from a Docker registry server' +complete -c docker -A -f -n '__fish_seen_subcommand_from pull' -s a -l all-tags -d 'Download all tagged images in the repository' +complete -c docker -A -f -n '__fish_seen_subcommand_from pull' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from pull' -a '(__fish_print_docker_images)' -d "Image" +complete -c docker -A -f -n '__fish_seen_subcommand_from pull' -a '(__fish_print_docker_repositories)' -d "Repository" + +# push +complete -c docker -f -n '__fish_docker_no_subcommand' -a push -d 'Push an image or a repository to a Docker registry server' +complete -c docker -A -f -n '__fish_seen_subcommand_from push' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from push' -a '(__fish_print_docker_images)' -d "Image" +complete -c docker -A -f -n '__fish_seen_subcommand_from push' -a '(__fish_print_docker_repositories)' -d "Repository" + +# rename +complete -c docker -f -n '__fish_docker_no_subcommand' -a rename -d 'Rename an existing container' + +# restart +complete -c docker -f -n '__fish_docker_no_subcommand' -a restart -d 'Restart a container' +complete -c docker -A -f -n '__fish_seen_subcommand_from restart' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from restart' -s t -l time -d 'Number of seconds to try to stop for before killing the container. Once killed it will then be restarted. Default is 10 seconds.' +complete -c docker -A -f -n '__fish_seen_subcommand_from restart' -a '(__fish_print_docker_containers running)' -d "Container" + +# rm +complete -c docker -f -n '__fish_docker_no_subcommand' -a rm -d 'Remove one or more containers' +complete -c docker -A -f -n '__fish_seen_subcommand_from rm' -s f -l force -d 'Force the removal of a running container (uses SIGKILL)' +complete -c docker -A -f -n '__fish_seen_subcommand_from rm' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from rm' -s l -l link -d 'Remove the specified link and not the underlying container' +complete -c docker -A -f -n '__fish_seen_subcommand_from rm' -s v -l volumes -d 'Remove the volumes associated with the container' +complete -c docker -A -f -n '__fish_seen_subcommand_from rm' -a '(__fish_print_docker_containers stopped)' -d "Container" +complete -c docker -A -f -n '__fish_seen_subcommand_from rm' -s f -l force -a '(__fish_print_docker_containers all)' -d "Container" + +# rmi +complete -c docker -f -n '__fish_docker_no_subcommand' -a rmi -d 'Remove one or more images' +complete -c docker -A -f -n '__fish_seen_subcommand_from rmi' -s f -l force -d 'Force removal of the image' +complete -c docker -A -f -n '__fish_seen_subcommand_from rmi' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from rmi' -l no-prune -d 'Do not delete untagged parents' +complete -c docker -A -f -n '__fish_seen_subcommand_from rmi' -a '(__fish_print_docker_images)' -d "Image" + +# run +complete -c docker -f -n '__fish_docker_no_subcommand' -a run -d 'Run a command in a new container' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s a -l attach -d 'Attach to STDIN, STDOUT or STDERR.' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l add-host -d 'Add a custom host-to-IP mapping (host:ip)' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s c -l cpu-shares -d 'CPU shares (relative weight)' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l cap-add -d 'Add Linux capabilities' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l cap-drop -d 'Drop Linux capabilities' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l cidfile -d 'Write the container ID to the file' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l cpuset -d 'CPUs in which to allow execution (0-3, 0,1)' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s d -l detach -d 'Detached mode: run the container in the background and print the new container ID' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l device -d 'Add a host device to the container (e.g. --device=/dev/sdc:/dev/xvdc:rwm)' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l dns -d 'Set custom DNS servers' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l dns-opt -d "Set custom DNS options (Use --dns-opt='' if you don't wish to set options)" +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l dns-search -d "Set custom DNS search domains (Use --dns-search=. if you don't wish to set the search domain)" +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s e -l env -d 'Set environment variables' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l entrypoint -d 'Overwrite the default ENTRYPOINT of the image' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l env-file -d 'Read in a line delimited file of environment variables' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l expose -d 'Expose a port or a range of ports (e.g. --expose=3300-3310) from the container without publishing it to your host' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l group-add -d 'Add additional groups to run as' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s h -l hostname -d 'Container host name' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s i -l interactive -d 'Keep STDIN open even if not attached' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l ipc -d 'Default is to create a private IPC namespace (POSIX SysV IPC) for the container' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l link -d 'Add link to another container in the form of :alias' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s m -l memory -d 'Memory limit (format: [], where unit = b, k, m or g)' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l mac-address -d 'Container MAC address (e.g. 92:d0:c6:0a:29:33)' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l memory-swap -d "Total memory usage (memory + swap), set '-1' to disable swap (format: [], where unit = b, k, m or g)" +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l name -d 'Assign a name to the container' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l net -d 'Set the Network mode for the container' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s P -l publish-all -d 'Publish all exposed ports to random ports on the host interfaces' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s p -l publish -d "Publish a container's port to the host" +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l pid -d 'Default is to create a private PID namespace for the container' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l privileged -d 'Give extended privileges to this container' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l read-only -d "Mount the container's root filesystem as read only" +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l restart -d 'Restart policy to apply when a container exits (no, on-failure[:max-retry], always)' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l rm -d 'Automatically remove the container when it exits (incompatible with -d)' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l security-opt -d 'Security Options' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l sig-proxy -d 'Proxy received signals to the process (non-TTY mode only). SIGCHLD, SIGSTOP, and SIGKILL are not proxied.' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l stop-signal -d 'Signal to kill a container' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s t -l tty -d 'Allocate a pseudo-TTY' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s u -l user -d 'Username or UID' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l tmpfs -d 'Mount tmpfs on a directory' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s v -l volume -d 'Bind mount a volume (e.g., from the host: -v /host:/container, from Docker: -v /container)' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l volumes-from -d 'Mount volumes from the specified container(s)' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s w -l workdir -d 'Working directory inside the container' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -a '(__fish_print_docker_images)' -d "Image" + +# save +complete -c docker -f -n '__fish_docker_no_subcommand' -a save -d 'Save an image to a tar archive' +complete -c docker -A -f -n '__fish_seen_subcommand_from save' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from save' -s o -l output -d 'Write to an file, instead of STDOUT' +complete -c docker -A -f -n '__fish_seen_subcommand_from save' -a '(__fish_print_docker_images)' -d "Image" + +# search +complete -c docker -f -n '__fish_docker_no_subcommand' -a search -d 'Search for an image on the registry (defaults to the Docker Hub)' +complete -c docker -A -f -n '__fish_seen_subcommand_from search' -l automated -d 'Only show automated builds' +complete -c docker -A -f -n '__fish_seen_subcommand_from search' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from search' -l no-trunc -d "Don't truncate output" +complete -c docker -A -f -n '__fish_seen_subcommand_from search' -s s -l stars -d 'Only displays with at least x stars' + +# start +complete -c docker -f -n '__fish_docker_no_subcommand' -a start -d 'Start a container' +complete -c docker -A -f -n '__fish_seen_subcommand_from start' -s a -l attach -d "Attach container's STDOUT and STDERR and forward all signals to the process" +complete -c docker -A -f -n '__fish_seen_subcommand_from start' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from start' -s i -l interactive -d "Attach container's STDIN" +complete -c docker -A -f -n '__fish_seen_subcommand_from start' -a '(__fish_print_docker_containers stopped)' -d "Container" + +# stats +complete -c docker -f -n '__fish_docker_no_subcommand' -a stats -d "Display a live stream of one or more containers' resource usage statistics" +complete -c docker -A -f -n '__fish_seen_subcommand_from stats' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from stats' -l no-stream -d 'Disable streaming stats and only pull the first result' +complete -c docker -A -f -n '__fish_seen_subcommand_from stats' -a '(__fish_print_docker_containers running)' -d "Container" + +# stop +complete -c docker -f -n '__fish_docker_no_subcommand' -a stop -d 'Stop a container' +complete -c docker -A -f -n '__fish_seen_subcommand_from stop' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from stop' -s t -l time -d 'Number of seconds to wait for the container to stop before killing it. Default is 10 seconds.' +complete -c docker -A -f -n '__fish_seen_subcommand_from stop' -a '(__fish_print_docker_containers running)' -d "Container" + +# tag +complete -c docker -f -n '__fish_docker_no_subcommand' -a tag -d 'Tag an image into a repository' +complete -c docker -A -f -n '__fish_seen_subcommand_from tag' -s f -l force -d 'Force' +complete -c docker -A -f -n '__fish_seen_subcommand_from tag' -l help -d 'Print usage' + +# top +complete -c docker -f -n '__fish_docker_no_subcommand' -a top -d 'Lookup the running processes of a container' +complete -c docker -A -f -n '__fish_seen_subcommand_from top' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from top' -a '(__fish_print_docker_containers running)' -d "Container" + +# unpause +complete -c docker -f -n '__fish_docker_no_subcommand' -a unpause -d 'Unpause a paused container' +complete -c docker -A -f -n '__fish_seen_subcommand_from unpause' -a '(__fish_print_docker_containers running)' -d "Container" + +# version +complete -c docker -f -n '__fish_docker_no_subcommand' -a version -d 'Show the Docker version information' + +# wait +complete -c docker -f -n '__fish_docker_no_subcommand' -a wait -d 'Block until a container stops, then print its exit code' +complete -c docker -A -f -n '__fish_seen_subcommand_from wait' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from wait' -a '(__fish_print_docker_containers running)' -d "Container" diff --git a/contrib/completion/powershell/posh-docker.psm1 b/contrib/completion/powershell/posh-docker.psm1 new file mode 100644 index 00000000..c0d6cc6b --- /dev/null +++ b/contrib/completion/powershell/posh-docker.psm1 @@ -0,0 +1,179 @@ +# Powershell completion for docker + +### Prerequisite +# Docker.exe needs to be in your PATH. +# If the command is not found, you will need to add a docker alias or add the docker installation folder (e.g. `%ProgramFiles%\Docker Toolbox`) to your PATH environment variable. + +### Installation (Latest stable) +# Windows 10 / Windows Server 2016: +# 1. Open a powershell prompt +# 2. Run `Install-Module -Scope CurrentUser posh-docker` +# +# Earlier Windows versions: +# 1. Install [PackageManagement PowerShell Modules Preview](https://www.microsoft.com/en-us/download/details.aspx?id=49186) +# 2. Open a powershell prompt +# 3. Run `Install-Module -Scope CurrentUser posh-docker` + +### Installation (From source) +# Copy this file to the %userprofile%\Documents\WindowsPowerShell\Modules\posh-docker directory (create directories as needed) + +### Usage +# After installation, execute the following line to enable autocompletion for the current powershell session: +# +# Import-Module posh-docker +# +# To make it persistent, add the above line to your profile. For example, run `notepad $PROFILE` and insert the line above. + +$global:DockerCompletion = @{} + +$script:flagRegex = "^ (-[^, =]+),? ?(--[^= ]+)?" + +function script:Get-Containers($filter) +{ + if ($filter -eq $null) + { + docker ps -a --no-trunc --format "{{.Names}}" + } else { + docker ps -a --no-trunc --format "{{.Names}}" --filter $filter + } +} + +function script:Get-AutoCompleteResult +{ + param([Parameter(ValueFromPipeline=$true)] $value) + + Process + { + New-Object System.Management.Automation.CompletionResult $value + } +} + +filter script:MatchingCommand($commandName) +{ + if ($_.StartsWith($commandName)) + { + $_ + } +} + +$completion_Docker = { + param($commandName, $commandAst, $cursorPosition) + + $command = $null + $commandParameters = @{} + $state = "Unknown" + $wordToComplete = $commandAst.CommandElements | Where-Object { $_.ToString() -eq $commandName } | Foreach-Object { $commandAst.CommandElements.IndexOf($_) } + + for ($i=1; $i -lt $commandAst.CommandElements.Count; $i++) + { + $p = $commandAst.CommandElements[$i].ToString() + + if ($p.StartsWith("-")) + { + if ($state -eq "Unknown" -or $state -eq "Options") + { + $commandParameters[$i] = "Option" + $state = "Options" + } + else + { + $commandParameters[$i] = "CommandOption" + $state = "CommandOptions" + } + } + else + { + if ($state -ne "CommandOptions") + { + $commandParameters[$i] = "Command" + $command = $p + $state = "CommandOptions" + } + else + { + $commandParameters[$i] = "CommandOther" + } + } + } + + if ($global:DockerCompletion.Count -eq 0) + { + $global:DockerCompletion["commands"] = @{} + $global:DockerCompletion["options"] = @() + + docker --help | ForEach-Object { + Write-Output $_ + if ($_ -match "^ (\w+)\s+(.+)") + { + $global:DockerCompletion["commands"][$Matches[1]] = @{} + + $currentCommand = $global:DockerCompletion["commands"][$Matches[1]] + $currentCommand["options"] = @() + } + elseif ($_ -match $flagRegex) + { + $global:DockerCompletion["options"] += $Matches[1] + if ($Matches[2] -ne $null) + { + $global:DockerCompletion["options"] += $Matches[2] + } + } + } + + } + + if ($wordToComplete -eq $null) + { + $commandToComplete = "Command" + if ($commandParameters.Count -gt 0) + { + if ($commandParameters[$commandParameters.Count] -eq "Command") + { + $commandToComplete = "CommandOther" + } + } + } else { + $commandToComplete = $commandParameters[$wordToComplete] + } + + switch ($commandToComplete) + { + "Command" { $global:DockerCompletion["commands"].Keys | MatchingCommand -Command $commandName | Sort-Object | Get-AutoCompleteResult } + "Option" { $global:DockerCompletion["options"] | MatchingCommand -Command $commandName | Sort-Object | Get-AutoCompleteResult } + "CommandOption" { + $options = $global:DockerCompletion["commands"][$command]["options"] + if ($options.Count -eq 0) + { + docker $command --help | % { + if ($_ -match $flagRegex) + { + $options += $Matches[1] + if ($Matches[2] -ne $null) + { + $options += $Matches[2] + } + } + } + } + + $global:DockerCompletion["commands"][$command]["options"] = $options + $options | MatchingCommand -Command $commandName | Sort-Object | Get-AutoCompleteResult + } + "CommandOther" { + $filter = $null + switch ($command) + { + "start" { $filter = "status=exited" } + "stop" { $filter = "status=running" } + } + Get-Containers $filter | MatchingCommand -Command $commandName | Sort-Object | Get-AutoCompleteResult + } + default { $global:DockerCompletion["commands"].Keys | MatchingCommand -Command $commandName } + } +} + +# Register the TabExpension2 function +if (-not $global:options) { $global:options = @{CustomArgumentCompleters = @{};NativeArgumentCompleters = @{}}} +$global:options['NativeArgumentCompleters']['docker'] = $Completion_Docker + +$function:tabexpansion2 = $function:tabexpansion2 -replace 'End\r\n{','End { if ($null -ne $options) { $options += $global:options} else {$options = $global:options}' \ No newline at end of file diff --git a/contrib/completion/zsh/REVIEWERS b/contrib/completion/zsh/REVIEWERS new file mode 100644 index 00000000..03ee2dde --- /dev/null +++ b/contrib/completion/zsh/REVIEWERS @@ -0,0 +1,2 @@ +Tianon Gravi (@tianon) +Jessie Frazelle (@jfrazelle) diff --git a/contrib/completion/zsh/_docker b/contrib/completion/zsh/_docker new file mode 100644 index 00000000..cb007b90 --- /dev/null +++ b/contrib/completion/zsh/_docker @@ -0,0 +1,1160 @@ +#compdef docker +# +# zsh completion for docker (http://docker.com) +# +# version: 0.3.0 +# github: https://github.com/felixr/docker-zsh-completion +# +# contributors: +# - Felix Riedel +# - Steve Durrheimer +# - Vincent Bernat +# +# license: +# +# Copyright (c) 2013, Felix Riedel +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +# Short-option stacking can be enabled with: +# zstyle ':completion:*:*:docker:*' option-stacking yes +# zstyle ':completion:*:*:docker-*:*' option-stacking yes +__docker_arguments() { + if zstyle -t ":completion:${curcontext}:" option-stacking; then + print -- -s + fi +} + +__docker_get_containers() { + [[ $PREFIX = -* ]] && return 1 + integer ret=1 + local kind type line s + declare -a running stopped lines args names + + kind=$1; shift + type=$1; shift + [[ $kind = (stopped|all) ]] && args=($args -a) + + lines=(${(f)"$(_call_program commands docker $docker_options ps --no-trunc $args)"}) + + # Parse header line to find columns + local i=1 j=1 k header=${lines[1]} + declare -A begin end + while (( j < ${#header} - 1 )); do + i=$(( j + ${${header[$j,-1]}[(i)[^ ]]} - 1 )) + j=$(( i + ${${header[$i,-1]}[(i) ]} - 1 )) + k=$(( j + ${${header[$j,-1]}[(i)[^ ]]} - 2 )) + begin[${header[$i,$((j-1))]}]=$i + end[${header[$i,$((j-1))]}]=$k + done + end[${header[$i,$((j-1))]}]=-1 # Last column, should go to the end of the line + lines=(${lines[2,-1]}) + + # Container ID + if [[ $type = (ids|all) ]]; then + for line in $lines; do + s="${${line[${begin[CONTAINER ID]},${end[CONTAINER ID]}]%% ##}[0,12]}" + s="$s:${(l:15:: :::)${${line[${begin[CREATED]},${end[CREATED]}]/ ago/}%% ##}}" + s="$s, ${${${line[${begin[IMAGE]},${end[IMAGE]}]}/:/\\:}%% ##}" + if [[ ${line[${begin[STATUS]},${end[STATUS]}]} = Exit* ]]; then + stopped=($stopped $s) + else + running=($running $s) + fi + done + fi + + # Names: we only display the one without slash. All other names + # are generated and may clutter the completion. However, with + # Swarm, all names may be prefixed by the swarm node name. + if [[ $type = (names|all) ]]; then + for line in $lines; do + names=(${(ps:,:)${${line[${begin[NAMES]},${end[NAMES]}]}%% *}}) + # First step: find a common prefix and strip it (swarm node case) + (( ${#${(u)names%%/*}} == 1 )) && names=${names#${names[1]%%/*}/} + # Second step: only keep the first name without a / + s=${${names:#*/*}[1]} + # If no name, well give up. + (( $#s != 0 )) || continue + s="$s:${(l:15:: :::)${${line[${begin[CREATED]},${end[CREATED]}]/ ago/}%% ##}}" + s="$s, ${${${line[${begin[IMAGE]},${end[IMAGE]}]}/:/\\:}%% ##}" + if [[ ${line[${begin[STATUS]},${end[STATUS]}]} = Exit* ]]; then + stopped=($stopped $s) + else + running=($running $s) + fi + done + fi + + [[ $kind = (running|all) ]] && _describe -t containers-running "running containers" running "$@" && ret=0 + [[ $kind = (stopped|all) ]] && _describe -t containers-stopped "stopped containers" stopped "$@" && ret=0 + return ret +} + +__docker_stoppedcontainers() { + [[ $PREFIX = -* ]] && return 1 + __docker_get_containers stopped all "$@" +} + +__docker_runningcontainers() { + [[ $PREFIX = -* ]] && return 1 + __docker_get_containers running all "$@" +} + +__docker_containers() { + [[ $PREFIX = -* ]] && return 1 + __docker_get_containers all all "$@" +} + +__docker_containers_ids() { + [[ $PREFIX = -* ]] && return 1 + __docker_get_containers all ids "$@" +} + +__docker_containers_names() { + [[ $PREFIX = -* ]] && return 1 + __docker_get_containers all names "$@" +} + +__docker_images() { + [[ $PREFIX = -* ]] && return 1 + integer ret=1 + declare -a images + images=(${${${(f)"$(_call_program commands docker $docker_options images)"}[2,-1]}/(#b)([^ ]##) ##([^ ]##) ##([^ ]##)*/${match[3]}:${(r:15:: :::)match[2]} in ${match[1]}}) + _describe -t docker-images "images" images && ret=0 + __docker_repositories_with_tags && ret=0 + return ret +} + +__docker_repositories() { + [[ $PREFIX = -* ]] && return 1 + declare -a repos + repos=(${${${(f)"$(_call_program commands docker $docker_options images)"}%% *}[2,-1]}) + repos=(${repos#}) + _describe -t docker-repos "repositories" repos +} + +__docker_repositories_with_tags() { + [[ $PREFIX = -* ]] && return 1 + integer ret=1 + declare -a repos onlyrepos matched + declare m + repos=(${${${${(f)"$(_call_program commands docker $docker_options images)"}[2,-1]}/ ##/:::}%% *}) + repos=(${${repos%:::}#}) + # Check if we have a prefix-match for the current prefix. + onlyrepos=(${repos%::*}) + for m in $onlyrepos; do + [[ ${PREFIX##${~~m}} != ${PREFIX} ]] && { + # Yes, complete with tags + repos=(${${repos/:::/:}/:/\\:}) + _describe -t docker-repos-with-tags "repositories with tags" repos && ret=0 + return ret + } + done + # No, only complete repositories + onlyrepos=(${${repos%:::*}/:/\\:}) + _describe -t docker-repos "repositories" onlyrepos -qS : && ret=0 + + return ret +} + +__docker_search() { + [[ $PREFIX = -* ]] && return 1 + local cache_policy + zstyle -s ":completion:${curcontext}:" cache-policy cache_policy + if [[ -z "$cache_policy" ]]; then + zstyle ":completion:${curcontext}:" cache-policy __docker_caching_policy + fi + + local searchterm cachename + searchterm="${words[$CURRENT]%/}" + cachename=_docker-search-$searchterm + + local expl + local -a result + if ( [[ ${(P)+cachename} -eq 0 ]] || _cache_invalid ${cachename#_} ) \ + && ! _retrieve_cache ${cachename#_}; then + _message "Searching for ${searchterm}..." + result=(${${${(f)"$(_call_program commands docker $docker_options search $searchterm)"}%% *}[2,-1]}) + _store_cache ${cachename#_} result + fi + _wanted dockersearch expl 'available images' compadd -a result +} + +__docker_get_log_options() { + [[ $PREFIX = -* ]] && return 1 + + integer ret=1 + local log_driver=${opt_args[--log-driver]:-"all"} + local -a awslogs_options fluentd_options gelf_options journald_options json_file_options syslog_options splunk_options + + awslogs_options=("awslogs-region" "awslogs-group" "awslogs-stream") + fluentd_options=("env" "fluentd-address" "fluentd-async-connect" "fluentd-buffer-limit" "fluentd-retry-wait" "fluentd-max-retries" "labels" "tag") + gcplogs_options=("env" "gcp-log-cmd" "gcp-project" "labels") + gelf_options=("env" "gelf-address" "gelf-compression-level" "gelf-compression-type" "labels" "tag") + journald_options=("env" "labels" "tag") + json_file_options=("env" "labels" "max-file" "max-size") + syslog_options=("syslog-address" "syslog-format" "syslog-tls-ca-cert" "syslog-tls-cert" "syslog-tls-key" "syslog-tls-skip-verify" "syslog-facility" "tag") + splunk_options=("env" "labels" "splunk-caname" "splunk-capath" "splunk-index" "splunk-insecureskipverify" "splunk-source" "splunk-sourcetype" "splunk-token" "splunk-url" "tag") + + [[ $log_driver = (awslogs|all) ]] && _describe -t awslogs-options "awslogs options" awslogs_options "$@" && ret=0 + [[ $log_driver = (fluentd|all) ]] && _describe -t fluentd-options "fluentd options" fluentd_options "$@" && ret=0 + [[ $log_driver = (gcplogs|all) ]] && _describe -t gcplogs-options "gcplogs options" gcplogs_options "$@" && ret=0 + [[ $log_driver = (gelf|all) ]] && _describe -t gelf-options "gelf options" gelf_options "$@" && ret=0 + [[ $log_driver = (journald|all) ]] && _describe -t journald-options "journald options" journald_options "$@" && ret=0 + [[ $log_driver = (json-file|all) ]] && _describe -t json-file-options "json-file options" json_file_options "$@" && ret=0 + [[ $log_driver = (syslog|all) ]] && _describe -t syslog-options "syslog options" syslog_options "$@" && ret=0 + [[ $log_driver = (splunk|all) ]] && _describe -t splunk-options "splunk options" splunk_options "$@" && ret=0 + + return ret +} + +__docker_log_options() { + [[ $PREFIX = -* ]] && return 1 + integer ret=1 + + if compset -P '*='; then + case "${${words[-1]%=*}#*=}" in + (syslog-format) + syslog_format_opts=('rfc3164' 'rfc5424') + _describe -t syslog-format-opts "Syslog format Options" syslog_format_opts && ret=0 + ;; + *) + _message 'value' && ret=0 + ;; + esac + else + __docker_get_log_options -qS "=" && ret=0 + fi + + return ret +} + +__docker_complete_detach_keys() { + [[ $PREFIX = -* ]] && return 1 + integer ret=1 + + compset -P "*," + keys=(${:-{a-z}}) + ctrl_keys=(${:-ctrl-{{a-z},{@,'[','\\','^',']',_}}}) + _describe -t detach_keys "[a-z]" keys -qS "," && ret=0 + _describe -t detach_keys-ctrl "'ctrl-' + 'a-z @ [ \\\\ ] ^ _'" ctrl_keys -qS "," && ret=0 +} + +__docker_complete_ps_filters() { + [[ $PREFIX = -* ]] && return 1 + integer ret=1 + + if compset -P '*='; then + case "${${words[-1]%=*}#*=}" in + (ancestor) + __docker_images && ret=0 + ;; + (before|since) + __docker_containers && ret=0 + ;; + (id) + __docker_containers_ids && ret=0 + ;; + (name) + __docker_containers_names && ret=0 + ;; + (status) + status_opts=('created' 'dead' 'exited' 'paused' 'restarting' 'running') + _describe -t status-filter-opts "Status Filter Options" status_opts && ret=0 + ;; + (volume) + __docker_volumes && ret=0 + ;; + *) + _message 'value' && ret=0 + ;; + esac + else + opts=('ancestor' 'before' 'exited' 'id' 'label' 'name' 'since' 'status' 'volume') + _describe -t filter-opts "Filter Options" opts -qS "=" && ret=0 + fi + + return ret +} + +__docker_networks() { + [[ $PREFIX = -* ]] && return 1 + integer ret=1 + declare -a lines networks + + lines=(${(f)"$(_call_program commands docker $docker_options network ls)"}) + + # Parse header line to find columns + local i=1 j=1 k header=${lines[1]} + declare -A begin end + while (( j < ${#header} - 1 )); do + i=$(( j + ${${header[$j,-1]}[(i)[^ ]]} - 1 )) + j=$(( i + ${${header[$i,-1]}[(i) ]} - 1 )) + k=$(( j + ${${header[$j,-1]}[(i)[^ ]]} - 2 )) + begin[${header[$i,$((j-1))]}]=$i + end[${header[$i,$((j-1))]}]=$k + done + end[${header[$i,$((j-1))]}]=-1 + lines=(${lines[2,-1]}) + + # Network ID + local line s + for line in $lines; do + s="${line[${begin[NETWORK ID]},${end[NETWORK ID]}]%% ##}" + s="$s:${(l:7:: :::)${${line[${begin[DRIVER]},${end[DRIVER]}]}%% ##}}" + networks=($networks $s) + done + + # Names + for line in $lines; do + s="${line[${begin[NAME]},${end[NAME]}]%% ##}" + s="$s:${(l:7:: :::)${${line[${begin[DRIVER]},${end[DRIVER]}]}%% ##}}" + networks=($networks $s) + done + + _describe -t networks-list "networks" networks && ret=0 + return ret +} + +__docker_network_commands() { + local -a _docker_network_subcommands + _docker_network_subcommands=( + "connect:onnects a container to a network" + "create:Creates a new network with a name specified by the user" + "disconnect:Disconnects a container from a network" + "inspect:Displays detailed information on a network" + "ls:Lists all the networks created by the user" + "rm:Deletes one or more networks" + ) + _describe -t docker-network-commands "docker network command" _docker_network_subcommands +} + +__docker_network_subcommand() { + local -a _command_args opts_help + local expl help="--help" + integer ret=1 + + opts_help=("(: -)--help[Print usage]") + + case "$words[1]" in + (connect) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help)*--alias=[Add network-scoped alias for the container]:alias: " \ + "($help)--ip=[Container IPv4 address]:IPv4: " \ + "($help)--ip6=[Container IPv6 address]:IPv6: " \ + "($help)*--link=[Add a link to another container]:link:->link" \ + "($help -)1:network:__docker_networks" \ + "($help -)2:containers:__docker_containers" && ret=0 + + case $state in + (link) + if compset -P "*:"; then + _wanted alias expl "Alias" compadd -E "" && ret=0 + else + __docker_runningcontainers -qS ":" && ret=0 + fi + ;; + esac + ;; + (create) + _arguments $(__docker_arguments) -A '-*' \ + $opts_help \ + "($help)*--aux-address[Auxiliary IPv4 or IPv6 addresses used by network driver]:key=IP: " \ + "($help -d --driver)"{-d=,--driver=}"[Driver to manage the Network]:driver:(null host bridge overlay)" \ + "($help)*--gateway=[IPv4 or IPv6 Gateway for the master subnet]:IP: " \ + "($help)--internal[Restricts external access to the network]" \ + "($help)*--ip-range=[Allocate container ip from a sub-range]:IP/mask: " \ + "($help)--ipam-driver=[IP Address Management Driver]:driver:(default)" \ + "($help)*--ipam-opt=[Custom IPAM plugin options]:opt=value: " \ + "($help)--ipv6[Enable IPv6 networking]" \ + "($help)*--label=[Set metadata on a network]:label=value: " \ + "($help)*"{-o=,--opt=}"[Driver specific options]:opt=value: " \ + "($help)*--subnet=[Subnet in CIDR format that represents a network segment]:IP/mask: " \ + "($help -)1:Network Name: " && ret=0 + ;; + (disconnect) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -)1:network:__docker_networks" \ + "($help -)2:containers:__docker_containers" && ret=0 + ;; + (inspect) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -f --format)"{-f=,--format=}"[Format the output using the given go template]:template: " \ + "($help -)*:network:__docker_networks" && ret=0 + ;; + (ls) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help)--no-trunc[Do not truncate the output]" \ + "($help -q --quiet)"{-q,--quiet}"[Only display numeric IDs]" && ret=0 + ;; + (rm) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -)*:network:__docker_networks" && ret=0 + ;; + (help) + _arguments $(__docker_arguments) ":subcommand:__docker_network_commands" && ret=0 + ;; + esac + + return ret +} + +__docker_volumes() { + [[ $PREFIX = -* ]] && return 1 + integer ret=1 + declare -a lines volumes + + lines=(${(f)"$(_call_program commands docker $docker_options volume ls)"}) + + # Parse header line to find columns + local i=1 j=1 k header=${lines[1]} + declare -A begin end + while (( j < ${#header} - 1 )); do + i=$(( j + ${${header[$j,-1]}[(i)[^ ]]} - 1 )) + j=$(( i + ${${header[$i,-1]}[(i) ]} - 1 )) + k=$(( j + ${${header[$j,-1]}[(i)[^ ]]} - 2 )) + begin[${header[$i,$((j-1))]}]=$i + end[${header[$i,$((j-1))]}]=$k + done + end[${header[$i,$((j-1))]}]=-1 + lines=(${lines[2,-1]}) + + # Names + local line s + for line in $lines; do + s="${line[${begin[VOLUME NAME]},${end[VOLUME NAME]}]%% ##}" + s="$s:${(l:7:: :::)${${line[${begin[DRIVER]},${end[DRIVER]}]}%% ##}}" + volumes=($volumes $s) + done + + _describe -t volumes-list "volumes" volumes && ret=0 + return ret +} + +__docker_volume_commands() { + local -a _docker_volume_subcommands + _docker_volume_subcommands=( + "create:Create a volume" + "inspect:Return low-level information on a volume" + "ls:List volumes" + "rm:Remove a volume" + ) + _describe -t docker-volume-commands "docker volume command" _docker_volume_subcommands +} + +__docker_volume_subcommand() { + local -a _command_args opts_help + local expl help="--help" + integer ret=1 + + opts_help=("(: -)--help[Print usage]") + + case "$words[1]" in + (create) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -d --driver)"{-d=,--driver=}"[Volume driver name]:Driver name:(local)" \ + "($help)*--label=[Set metadata for a volume]:label=value: " \ + "($help)--name=[Volume name]" \ + "($help)*"{-o=,--opt=}"[Driver specific options]:Driver option: " && ret=0 + ;; + (inspect) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -f --format)"{-f=,--format=}"[Format the output using the given go template]:template: " \ + "($help -)1:volume:__docker_volumes" && ret=0 + ;; + (ls) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help)*"{-f=,--filter=}"[Provide filter values (i.e. 'dangling=true')]:filter: " \ + "($help -q --quiet)"{-q,--quiet}"[Only display volume names]" && ret=0 + ;; + (rm) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -):volume:__docker_volumes" && ret=0 + ;; + (help) + _arguments $(__docker_arguments) ":subcommand:__docker_volume_commands" && ret=0 + ;; + esac + + return ret +} + +__docker_caching_policy() { + oldp=( "$1"(Nmh+1) ) # 1 hour + (( $#oldp )) +} + +__docker_commands() { + local cache_policy + + zstyle -s ":completion:${curcontext}:" cache-policy cache_policy + if [[ -z "$cache_policy" ]]; then + zstyle ":completion:${curcontext}:" cache-policy __docker_caching_policy + fi + + if ( [[ ${+_docker_subcommands} -eq 0 ]] || _cache_invalid docker_subcommands) \ + && ! _retrieve_cache docker_subcommands; + then + local -a lines + lines=(${(f)"$(_call_program commands docker 2>&1)"}) + _docker_subcommands=(${${${lines[$((${lines[(i)Commands:]} + 1)),${lines[(I) *]}]}## #}/ ##/:}) + _docker_subcommands=($_docker_subcommands 'daemon:Enable daemon mode' 'help:Show help for a command') + (( $#_docker_subcommands > 2 )) && _store_cache docker_subcommands _docker_subcommands + fi + _describe -t docker-commands "docker command" _docker_subcommands +} + +__docker_subcommand() { + local -a _command_args opts_help opts_build_create_run opts_build_create_run_update opts_create_run opts_create_run_update + local expl help="--help" + integer ret=1 + + opts_help=("(: -)--help[Print usage]") + opts_build_create_run=( + "($help)--cgroup-parent=[Parent cgroup for the container]:cgroup: " + "($help)--isolation=[Container isolation technology]:isolation:(default hyperv process)" + "($help)*--shm-size=[Size of '/dev/shm' (format is '')]:shm size: " + "($help)*--ulimit=[ulimit options]:ulimit: " + "($help)--userns=[Container user namespace]:user namespace:(host)" + ) + opts_build_create_run_update=( + "($help)--cpu-shares=[CPU shares (relative weight)]:CPU shares:(0 10 100 200 500 800 1000)" + "($help)--cpu-period=[Limit the CPU CFS (Completely Fair Scheduler) period]:CPU period: " + "($help)--cpu-quota=[Limit the CPU CFS (Completely Fair Scheduler) quota]:CPU quota: " + "($help)--cpuset-cpus=[CPUs in which to allow execution]:CPUs: " + "($help)--cpuset-mems=[MEMs in which to allow execution]:MEMs: " + "($help -m --memory)"{-m=,--memory=}"[Memory limit]:Memory limit: " + "($help)--memory-swap=[Total memory limit with swap]:Memory limit: " + ) + opts_create_run=( + "($help -a --attach)"{-a=,--attach=}"[Attach to stdin, stdout or stderr]:device:(STDIN STDOUT STDERR)" + "($help)*--add-host=[Add a custom host-to-IP mapping]:host\:ip mapping: " + "($help)*--blkio-weight-device=[Block IO (relative device weight)]:device:Block IO weight: " + "($help)*--cap-add=[Add Linux capabilities]:capability: " + "($help)*--cap-drop=[Drop Linux capabilities]:capability: " + "($help)--cidfile=[Write the container ID to the file]:CID file:_files" + "($help)*--device=[Add a host device to the container]:device:_files" + "($help)*--device-read-bps=[Limit the read rate (bytes per second) from a device]:device:IO rate: " + "($help)*--device-read-iops=[Limit the read rate (IO per second) from a device]:device:IO rate: " + "($help)*--device-write-bps=[Limit the write rate (bytes per second) to a device]:device:IO rate: " + "($help)*--device-write-iops=[Limit the write rate (IO per second) to a device]:device:IO rate: " + "($help)*--dns=[Custom DNS servers]:DNS server: " + "($help)*--dns-opt=[Custom DNS options]:DNS option: " + "($help)*--dns-search=[Custom DNS search domains]:DNS domains: " + "($help)*"{-e=,--env=}"[Environment variables]:environment variable: " + "($help)--entrypoint=[Overwrite the default entrypoint of the image]:entry point: " + "($help)*--env-file=[Read environment variables from a file]:environment file:_files" + "($help)*--expose=[Expose a port from the container without publishing it]: " + "($help)*--group-add=[Add additional groups to run as]:group:_groups" + "($help -h --hostname)"{-h=,--hostname=}"[Container host name]:hostname:_hosts" + "($help -i --interactive)"{-i,--interactive}"[Keep stdin open even if not attached]" + "($help)--ip=[Container IPv4 address]:IPv4: " + "($help)--ip6=[Container IPv6 address]:IPv6: " + "($help)--ipc=[IPC namespace to use]:IPC namespace: " + "($help)*--link=[Add link to another container]:link:->link" + "($help)*"{-l=,--label=}"[Container metadata]:label: " + "($help)--log-driver=[Default driver for container logs]:Logging driver:(awslogs etwlogs fluentd gcplogs gelf journald json-file none splunk syslog)" + "($help)*--log-opt=[Log driver specific options]:log driver options:__docker_log_options" + "($help)--mac-address=[Container MAC address]:MAC address: " + "($help)--name=[Container name]:name: " + "($help)--net=[Connect a container to a network]:network mode:(bridge none container host)" + "($help)*--net-alias=[Add network-scoped alias for the container]:alias: " + "($help)--oom-kill-disable[Disable OOM Killer]" + "($help)--oom-score-adj[Tune the host's OOM preferences for containers (accepts -1000 to 1000)]" + "($help)--pids-limit[Tune container pids limit (set -1 for unlimited)]" + "($help -P --publish-all)"{-P,--publish-all}"[Publish all exposed ports]" + "($help)*"{-p=,--publish=}"[Expose a container's port to the host]:port:_ports" + "($help)--pid=[PID namespace to use]:PID: " + "($help)--privileged[Give extended privileges to this container]" + "($help)--read-only[Mount the container's root filesystem as read only]" + "($help)*--security-opt=[Security options]:security option: " + "($help -t --tty)"{-t,--tty}"[Allocate a pseudo-tty]" + "($help -u --user)"{-u=,--user=}"[Username or UID]:user:_users" + "($help)--tmpfs[mount tmpfs]" + "($help)*-v[Bind mount a volume]:volume: " + "($help)--volume-driver=[Optional volume driver for the container]:volume driver:(local)" + "($help)*--volumes-from=[Mount volumes from the specified container]:volume: " + "($help -w --workdir)"{-w=,--workdir=}"[Working directory inside the container]:directory:_directories" + ) + opts_create_run_update=( + "($help)--blkio-weight=[Block IO (relative weight), between 10 and 1000]:Block IO weight:(10 100 500 1000)" + "($help)--kernel-memory=[Kernel memory limit in bytes]:Memory limit: " + "($help)--memory-reservation=[Memory soft limit]:Memory limit: " + "($help)--restart=[Restart policy]:restart policy:(no on-failure always unless-stopped)" + ) + opts_attach_exec_run_start=( + "($help)--detach-keys=[Escape key sequence used to detach a container]:sequence:__docker_complete_detach_keys" + ) + + case "$words[1]" in + (attach) + _arguments $(__docker_arguments) \ + $opts_help \ + $opts_attach_exec_run_start \ + "($help)--no-stdin[Do not attach stdin]" \ + "($help)--sig-proxy[Proxy all received signals to the process (non-TTY mode only)]" \ + "($help -):containers:__docker_runningcontainers" && ret=0 + ;; + (build) + _arguments $(__docker_arguments) \ + $opts_help \ + $opts_build_create_run \ + $opts_build_create_run_update \ + "($help)*--build-arg[Build-time variables]:=: " \ + "($help -f --file)"{-f=,--file=}"[Name of the Dockerfile]:Dockerfile:_files" \ + "($help)--force-rm[Always remove intermediate containers]" \ + "($help)*--label=[Set metadata for an image]:label=value: " \ + "($help)--no-cache[Do not use cache when building the image]" \ + "($help)--pull[Attempt to pull a newer version of the image]" \ + "($help -q --quiet)"{-q,--quiet}"[Suppress verbose build output]" \ + "($help)--rm[Remove intermediate containers after a successful build]" \ + "($help -t --tag)*"{-t=,--tag=}"[Repository, name and tag for the image]: :__docker_repositories_with_tags" \ + "($help -):path or URL:_directories" && ret=0 + ;; + (commit) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -a --author)"{-a=,--author=}"[Author]:author: " \ + "($help)*"{-c=,--change=}"[Apply Dockerfile instruction to the created image]:Dockerfile:_files" \ + "($help -m --message)"{-m=,--message=}"[Commit message]:message: " \ + "($help -p --pause)"{-p,--pause}"[Pause container during commit]" \ + "($help -):container:__docker_containers" \ + "($help -): :__docker_repositories_with_tags" && ret=0 + ;; + (cp) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -L --follow-link)"{-L,--follow-link}"[Always follow symbol link]" \ + "($help -)1:container:->container" \ + "($help -)2:hostpath:_files" && ret=0 + case $state in + (container) + if compset -P "*:"; then + _files && ret=0 + else + __docker_containers -qS ":" && ret=0 + fi + ;; + esac + ;; + (create) + _arguments $(__docker_arguments) \ + $opts_help \ + $opts_build_create_run \ + $opts_build_create_run_update \ + $opts_create_run \ + $opts_create_run_update \ + "($help -): :__docker_images" \ + "($help -):command: _command_names -e" \ + "($help -)*::arguments: _normal" && ret=0 + + case $state in + (link) + if compset -P "*:"; then + _wanted alias expl "Alias" compadd -E "" && ret=0 + else + __docker_runningcontainers -qS ":" && ret=0 + fi + ;; + esac + + ;; + (daemon) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help)--api-cors-header=[CORS headers in the remote API]:CORS headers: " \ + "($help)*--authorization-plugin=[Authorization plugins to load]" \ + "($help -b --bridge)"{-b=,--bridge=}"[Attach containers to a network bridge]:bridge:_net_interfaces" \ + "($help)--bip=[Network bridge IP]:IP address: " \ + "($help)--cgroup-parent=[Parent cgroup for all containers]:cgroup: " \ + "($help)--containerd=[Path to containerd socket]:socket:_files -g \"*.sock\"" \ + "($help -D --debug)"{-D,--debug}"[Enable debug mode]" \ + "($help)--default-gateway[Container default gateway IPv4 address]:IPv4 address: " \ + "($help)--default-gateway-v6[Container default gateway IPv6 address]:IPv6 address: " \ + "($help)--cluster-store=[URL of the distributed storage backend]:Cluster Store:->cluster-store" \ + "($help)--cluster-advertise=[Address of the daemon instance to advertise]:Instance to advertise (host\:port): " \ + "($help)*--cluster-store-opt=[Cluster options]:Cluster options:->cluster-store-options" \ + "($help)*--dns=[DNS server to use]:DNS: " \ + "($help)*--dns-search=[DNS search domains to use]:DNS search: " \ + "($help)*--dns-opt=[DNS options to use]:DNS option: " \ + "($help)*--default-ulimit=[Default ulimit settings for containers]:ulimit: " \ + "($help)--disable-legacy-registry[Do not contact legacy registries]" \ + "($help)*--exec-opt=[Runtime execution options]:runtime execution options: " \ + "($help)--exec-root=[Root directory for execution state files]:path:_directories" \ + "($help)--fixed-cidr=[IPv4 subnet for fixed IPs]:IPv4 subnet: " \ + "($help)--fixed-cidr-v6=[IPv6 subnet for fixed IPs]:IPv6 subnet: " \ + "($help -G --group)"{-G=,--group=}"[Group for the unix socket]:group:_groups" \ + "($help -g --graph)"{-g=,--graph=}"[Root of the Docker runtime]:path:_directories" \ + "($help -H --host)"{-H=,--host=}"[tcp://host:port to bind/connect to]:host: " \ + "($help)--icc[Enable inter-container communication]" \ + "($help)*--insecure-registry=[Enable insecure registry communication]:registry: " \ + "($help)--ip=[Default IP when binding container ports]" \ + "($help)--ip-forward[Enable net.ipv4.ip_forward]" \ + "($help)--ip-masq[Enable IP masquerading]" \ + "($help)--iptables[Enable addition of iptables rules]" \ + "($help)--ipv6[Enable IPv6 networking]" \ + "($help -l --log-level)"{-l=,--log-level=}"[Logging level]:level:(debug info warn error fatal)" \ + "($help)*--label=[Key=value labels]:label: " \ + "($help)--log-driver=[Default driver for container logs]:Logging driver:(awslogs etwlogs fluentd gcplogs gelf journald json-file none splunk syslog)" \ + "($help)*--log-opt=[Log driver specific options]:log driver options:__docker_log_options" \ + "($help)--mtu=[Network MTU]:mtu:(0 576 1420 1500 9000)" \ + "($help -p --pidfile)"{-p=,--pidfile=}"[Path to use for daemon PID file]:PID file:_files" \ + "($help)--raw-logs[Full timestamps without ANSI coloring]" \ + "($help)*--registry-mirror=[Preferred Docker registry mirror]:registry mirror: " \ + "($help -s --storage-driver)"{-s=,--storage-driver=}"[Storage driver to use]:driver:(aufs devicemapper btrfs zfs overlay)" \ + "($help)--selinux-enabled[Enable selinux support]" \ + "($help)*--storage-opt=[Storage driver options]:storage driver options: " \ + "($help)--tls[Use TLS]" \ + "($help)--tlscacert=[Trust certs signed only by this CA]:PEM file:_files -g \"*.(pem|crt)\"" \ + "($help)--tlscert=[Path to TLS certificate file]:PEM file:_files -g \"*.(pem|crt)\"" \ + "($help)--tlskey=[Path to TLS key file]:Key file:_files -g \"*.(pem|key)\"" \ + "($help)--tlsverify[Use TLS and verify the remote]" \ + "($help)--userns-remap=[User/Group setting for user namespaces]:user\:group:->users-groups" \ + "($help)--userland-proxy[Use userland proxy for loopback traffic]" && ret=0 + + case $state in + (cluster-store) + if compset -P '*://'; then + _message 'host:port' && ret=0 + else + store=('consul' 'etcd' 'zk') + _describe -t cluster-store "Cluster Store" store -qS "://" && ret=0 + fi + ;; + (cluster-store-options) + if compset -P '*='; then + _files && ret=0 + else + opts=('discovery.heartbeat' 'discovery.ttl' 'kv.cacertfile' 'kv.certfile' 'kv.keyfile' 'kv.path') + _describe -t cluster-store-opts "Cluster Store Options" opts -qS "=" && ret=0 + fi + ;; + (users-groups) + if compset -P '*:'; then + _groups && ret=0 + else + _describe -t userns-default "default Docker user management" '(default)' && ret=0 + _users && ret=0 + fi + ;; + esac + ;; + (diff) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -)*:containers:__docker_containers" && ret=0 + ;; + (events) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help)*"{-f=,--filter=}"[Filter values]:filter: " \ + "($help)--since=[Events created since this timestamp]:timestamp: " \ + "($help)--until=[Events created until this timestamp]:timestamp: " && ret=0 + ;; + (exec) + local state + _arguments $(__docker_arguments) \ + $opts_help \ + $opts_attach_exec_run_start \ + "($help -d --detach)"{-d,--detach}"[Detached mode: leave the container running in the background]" \ + "($help -i --interactive)"{-i,--interactive}"[Keep stdin open even if not attached]" \ + "($help)--privileged[Give extended Linux capabilities to the command]" \ + "($help -t --tty)"{-t,--tty}"[Allocate a pseudo-tty]" \ + "($help -u --user)"{-u=,--user=}"[Username or UID]:user:_users" \ + "($help -):containers:__docker_runningcontainers" \ + "($help -)*::command:->anycommand" && ret=0 + + case $state in + (anycommand) + shift 1 words + (( CURRENT-- )) + _normal && ret=0 + ;; + esac + ;; + (export) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -o --output)"{-o=,--output=}"[Write to a file, instead of stdout]:output file:_files" \ + "($help -)*:containers:__docker_containers" && ret=0 + ;; + (history) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -H --human)"{-H,--human}"[Print sizes and dates in human readable format]" \ + "($help)--no-trunc[Do not truncate output]" \ + "($help -q --quiet)"{-q,--quiet}"[Only show numeric IDs]" \ + "($help -)*: :__docker_images" && ret=0 + ;; + (images) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -a --all)"{-a,--all}"[Show all images]" \ + "($help)--digests[Show digests]" \ + "($help)*"{-f=,--filter=}"[Filter values]:filter: " \ + "($help)--format[Pretty-print containers using a Go template]:format: " \ + "($help)--no-trunc[Do not truncate output]" \ + "($help -q --quiet)"{-q,--quiet}"[Only show numeric IDs]" \ + "($help -): :__docker_repositories" && ret=0 + ;; + (import) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help)*"{-c=,--change=}"[Apply Dockerfile instruction to the created image]:Dockerfile:_files" \ + "($help -m --message)"{-m=,--message=}"[Commit message for imported image]:message: " \ + "($help -):URL:(- http:// file://)" \ + "($help -): :__docker_repositories_with_tags" && ret=0 + ;; + (info|version) + _arguments $(__docker_arguments) \ + $opts_help && ret=0 + ;; + (inspect) + local state + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -f --format)"{-f=,--format=}"[Format the output using the given go template]:template: " \ + "($help -s --size)"{-s,--size}"[Display total file sizes if the type is container]" \ + "($help)--type=[Return JSON for specified type]:type:(image container)" \ + "($help -)*: :->values" && ret=0 + + case $state in + (values) + if [[ ${words[(r)--type=container]} == --type=container ]]; then + __docker_containers && ret=0 + elif [[ ${words[(r)--type=image]} == --type=image ]]; then + __docker_images && ret=0 + else + __docker_images && __docker_containers && ret=0 + fi + ;; + esac + ;; + (kill) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -s --signal)"{-s=,--signal=}"[Signal to send]:signal:_signals" \ + "($help -)*:containers:__docker_runningcontainers" && ret=0 + ;; + (load) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -i --input)"{-i=,--input=}"[Read from tar archive file]:archive file:_files -g \"*.((tar|TAR)(.gz|.GZ|.Z|.bz2|.lzma|.xz|)|(tbz|tgz|txz))(-.)\"" \ + "($help -q --quiet)"{-q,--quiet}"[Suppress the load output]" && ret=0 + ;; + (login) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -p --password)"{-p=,--password=}"[Password]:password: " \ + "($help -u --user)"{-u=,--user=}"[Username]:username: " \ + "($help -)1:server: " && ret=0 + ;; + (logout) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -)1:server: " && ret=0 + ;; + (logs) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -f --follow)"{-f,--follow}"[Follow log output]" \ + "($help -s --since)"{-s=,--since=}"[Show logs since this timestamp]:timestamp: " \ + "($help -t --timestamps)"{-t,--timestamps}"[Show timestamps]" \ + "($help)--tail=[Output the last K lines]:lines:(1 10 20 50 all)" \ + "($help -)*:containers:__docker_containers" && ret=0 + ;; + (network) + local curcontext="$curcontext" state + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -): :->command" \ + "($help -)*:: :->option-or-argument" && ret=0 + + case $state in + (command) + __docker_network_commands && ret=0 + ;; + (option-or-argument) + curcontext=${curcontext%:*:*}:docker-${words[-1]}: + __docker_network_subcommand && ret=0 + ;; + esac + ;; + (pause|unpause) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -)*:containers:__docker_runningcontainers" && ret=0 + ;; + (port) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -)1:containers:__docker_runningcontainers" \ + "($help -)2:port:_ports" && ret=0 + ;; + (ps) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -a --all)"{-a,--all}"[Show all containers]" \ + "($help)--before=[Show only container created before...]:containers:__docker_containers" \ + "($help)*"{-f=,--filter=}"[Filter values]:filter:->filter-options" \ + "($help)--format[Pretty-print containers using a Go template]:format: " \ + "($help -l --latest)"{-l,--latest}"[Show only the latest created container]" \ + "($help)-n[Show n last created containers, include non-running one]:n:(1 5 10 25 50)" \ + "($help)--no-trunc[Do not truncate output]" \ + "($help -q --quiet)"{-q,--quiet}"[Only show numeric IDs]" \ + "($help -s --size)"{-s,--size}"[Display total file sizes]" \ + "($help)--since=[Show only containers created since...]:containers:__docker_containers" && ret=0 + + case $state in + (filter-options) + __docker_complete_ps_filters && ret=0 + ;; + esac + ;; + (pull) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -a --all-tags)"{-a,--all-tags}"[Download all tagged images]" \ + "($help)--disable-content-trust[Skip image verification]" \ + "($help -):name:__docker_search" && ret=0 + ;; + (push) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help)--disable-content-trust[Skip image signing]" \ + "($help -): :__docker_images" && ret=0 + ;; + (rename) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -):old name:__docker_containers" \ + "($help -):new name: " && ret=0 + ;; + (restart|stop) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -t --time)"{-t=,--time=}"[Number of seconds to try to stop for before killing the container]:seconds to before killing:(1 5 10 30 60)" \ + "($help -)*:containers:__docker_runningcontainers" && ret=0 + ;; + (rm) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -f --force)"{-f,--force}"[Force removal]" \ + "($help -l --link)"{-l,--link}"[Remove the specified link and not the underlying container]" \ + "($help -v --volumes)"{-v,--volumes}"[Remove the volumes associated to the container]" \ + "($help -)*:containers:__docker_stoppedcontainers" && ret=0 + ;; + (rmi) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -f --force)"{-f,--force}"[Force removal]" \ + "($help)--no-prune[Do not delete untagged parents]" \ + "($help -)*: :__docker_images" && ret=0 + ;; + (run) + _arguments $(__docker_arguments) \ + $opts_help \ + $opts_build_create_run \ + $opts_build_create_run_update \ + $opts_create_run \ + $opts_create_run_update \ + $opts_attach_exec_run_start \ + "($help -d --detach)"{-d,--detach}"[Detached mode: leave the container running in the background]" \ + "($help)--rm[Remove intermediate containers when it exits]" \ + "($help)--sig-proxy[Proxy all received signals to the process (non-TTY mode only)]" \ + "($help)--stop-signal=[Signal to kill a container]:signal:_signals" \ + "($help -): :__docker_images" \ + "($help -):command: _command_names -e" \ + "($help -)*::arguments: _normal" && ret=0 + + case $state in + (link) + if compset -P "*:"; then + _wanted alias expl "Alias" compadd -E "" && ret=0 + else + __docker_runningcontainers -qS ":" && ret=0 + fi + ;; + esac + + ;; + (save) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -o --output)"{-o=,--output=}"[Write to file]:file:_files" \ + "($help -)*: :__docker_images" && ret=0 + ;; + (search) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help)--automated[Only show automated builds]" \ + "($help)--no-trunc[Do not truncate output]" \ + "($help -s --stars)"{-s=,--stars=}"[Only display with at least X stars]:stars:(0 10 100 1000)" \ + "($help -):term: " && ret=0 + ;; + (start) + _arguments $(__docker_arguments) \ + $opts_help \ + $opts_attach_exec_run_start \ + "($help -a --attach)"{-a,--attach}"[Attach container's stdout/stderr and forward all signals]" \ + "($help -i --interactive)"{-i,--interactive}"[Attach container's stding]" \ + "($help -)*:containers:__docker_stoppedcontainers" && ret=0 + ;; + (stats) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -a --all)"{-a,--all}"[Show all containers (default shows just running)]" \ + "($help)--no-stream[Disable streaming stats and only pull the first result]" \ + "($help -)*:containers:__docker_runningcontainers" && ret=0 + ;; + (tag) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -):source:__docker_images"\ + "($help -):destination:__docker_repositories_with_tags" && ret=0 + ;; + (top) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -)1:containers:__docker_runningcontainers" \ + "($help -)*:: :->ps-arguments" && ret=0 + case $state in + (ps-arguments) + _ps && ret=0 + ;; + esac + + ;; + (update) + _arguments $(__docker_arguments) \ + $opts_help \ + $opts_create_run_update \ + $opts_build_create_run_update \ + "($help -)*: :->values" && ret=0 + + case $state in + (values) + if [[ ${words[(r)--kernel-memory*]} = (--kernel-memory*) ]]; then + __docker_stoppedcontainers && ret=0 + else + __docker_containers && ret=0 + fi + ;; + esac + ;; + (volume) + local curcontext="$curcontext" state + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -): :->command" \ + "($help -)*:: :->option-or-argument" && ret=0 + + case $state in + (command) + __docker_volume_commands && ret=0 + ;; + (option-or-argument) + curcontext=${curcontext%:*:*}:docker-${words[-1]}: + __docker_volume_subcommand && ret=0 + ;; + esac + ;; + (wait) + _arguments $(__docker_arguments) \ + $opts_help \ + "($help -)*:containers:__docker_runningcontainers" && ret=0 + ;; + (help) + _arguments $(__docker_arguments) ":subcommand:__docker_commands" && ret=0 + ;; + esac + + return ret +} + +_docker() { + # Support for subservices, which allows for `compdef _docker docker-shell=_docker_containers`. + # Based on /usr/share/zsh/functions/Completion/Unix/_git without support for `ret`. + if [[ $service != docker ]]; then + _call_function - _$service + return + fi + + local curcontext="$curcontext" state line help="-h --help" + integer ret=1 + typeset -A opt_args + + _arguments $(__docker_arguments) -C \ + "(: -)"{-h,--help}"[Print usage]" \ + "($help)--config[Location of client config files]:path:_directories" \ + "($help -D --debug)"{-D,--debug}"[Enable debug mode]" \ + "($help -H --host)"{-H=,--host=}"[tcp://host:port to bind/connect to]:host: " \ + "($help -l --log-level)"{-l=,--log-level=}"[Logging level]:level:(debug info warn error fatal)" \ + "($help)--tls[Use TLS]" \ + "($help)--tlscacert=[Trust certs signed only by this CA]:PEM file:_files -g "*.(pem|crt)"" \ + "($help)--tlscert=[Path to TLS certificate file]:PEM file:_files -g "*.(pem|crt)"" \ + "($help)--tlskey=[Path to TLS key file]:Key file:_files -g "*.(pem|key)"" \ + "($help)--tlsverify[Use TLS and verify the remote]" \ + "($help)--userland-proxy[Use userland proxy for loopback traffic]" \ + "($help -v --version)"{-v,--version}"[Print version information and quit]" \ + "($help -): :->command" \ + "($help -)*:: :->option-or-argument" && ret=0 + + local host=${opt_args[-H]}${opt_args[--host]} + local config=${opt_args[--config]} + local docker_options="${host:+--host $host} ${config:+--config $config}" + + case $state in + (command) + __docker_commands && ret=0 + ;; + (option-or-argument) + curcontext=${curcontext%:*:*}:docker-$words[1]: + __docker_subcommand && ret=0 + ;; + esac + + return ret +} + +_docker "$@" + +# Local Variables: +# mode: Shell-Script +# sh-indentation: 4 +# indent-tabs-mode: nil +# sh-basic-offset: 4 +# End: +# vim: ft=zsh sw=4 ts=4 et diff --git a/contrib/desktop-integration/README.md b/contrib/desktop-integration/README.md new file mode 100644 index 00000000..85a01b9e --- /dev/null +++ b/contrib/desktop-integration/README.md @@ -0,0 +1,11 @@ +Desktop Integration +=================== + +The ./contrib/desktop-integration contains examples of typical dockerized +desktop applications. + +Examples +======== + +* Chromium: ./chromium/Dockerfile shows a way to dockerize a common application +* Gparted: ./gparted/Dockerfile shows a way to dockerize a common application w devices diff --git a/contrib/desktop-integration/chromium/Dockerfile b/contrib/desktop-integration/chromium/Dockerfile new file mode 100644 index 00000000..5cacd1f9 --- /dev/null +++ b/contrib/desktop-integration/chromium/Dockerfile @@ -0,0 +1,36 @@ +# VERSION: 0.1 +# DESCRIPTION: Create chromium container with its dependencies +# AUTHOR: Jessica Frazelle +# COMMENTS: +# This file describes how to build a Chromium container with all +# dependencies installed. It uses native X11 unix socket. +# Tested on Debian Jessie +# USAGE: +# # Download Chromium Dockerfile +# wget http://raw.githubusercontent.com/docker/docker/master/contrib/desktop-integration/chromium/Dockerfile +# +# # Build chromium image +# docker build -t chromium . +# +# # Run stateful data-on-host chromium. For ephemeral, remove -v /data/chromium:/data +# docker run -v /data/chromium:/data -v /tmp/.X11-unix:/tmp/.X11-unix \ +# -e DISPLAY=unix$DISPLAY chromium + +# # To run stateful dockerized data containers +# docker run --volumes-from chromium-data -v /tmp/.X11-unix:/tmp/.X11-unix \ +# -e DISPLAY=unix$DISPLAY chromium + +# Base docker image +FROM debian:jessie +MAINTAINER Jessica Frazelle + +# Install Chromium +RUN apt-get update && apt-get install -y \ + chromium \ + chromium-l10n \ + libcanberra-gtk-module \ + libexif-dev \ + --no-install-recommends + +# Autorun chromium +CMD ["/usr/bin/chromium", "--no-sandbox", "--user-data-dir=/data"] diff --git a/contrib/desktop-integration/gparted/Dockerfile b/contrib/desktop-integration/gparted/Dockerfile new file mode 100644 index 00000000..3ddb2320 --- /dev/null +++ b/contrib/desktop-integration/gparted/Dockerfile @@ -0,0 +1,31 @@ +# VERSION: 0.1 +# DESCRIPTION: Create gparted container with its dependencies +# AUTHOR: Jessica Frazelle +# COMMENTS: +# This file describes how to build a gparted container with all +# dependencies installed. It uses native X11 unix socket. +# Tested on Debian Jessie +# USAGE: +# # Download gparted Dockerfile +# wget http://raw.githubusercontent.com/docker/docker/master/contrib/desktop-integration/gparted/Dockerfile +# +# # Build gparted image +# docker build -t gparted . +# +# docker run -v /tmp/.X11-unix:/tmp/.X11-unix \ +# --device=/dev/sda:/dev/sda \ +# -e DISPLAY=unix$DISPLAY gparted +# + +# Base docker image +FROM debian:jessie +MAINTAINER Jessica Frazelle + +# Install Gparted and its dependencies +RUN apt-get update && apt-get install -y \ + gparted \ + libcanberra-gtk-module \ + --no-install-recommends + +# Autorun gparted +CMD ["/usr/sbin/gparted"] diff --git a/contrib/docker-device-tool/device_tool.go b/contrib/docker-device-tool/device_tool.go new file mode 100644 index 00000000..cb538f28 --- /dev/null +++ b/contrib/docker-device-tool/device_tool.go @@ -0,0 +1,176 @@ +// +build !windows + +package main + +import ( + "flag" + "fmt" + "os" + "path" + "sort" + "strconv" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/daemon/graphdriver/devmapper" + "github.com/docker/docker/pkg/devicemapper" +) + +func usage() { + fmt.Fprintf(os.Stderr, "Usage: %s [status] | [list] | [device id] | [resize new-pool-size] | [snap new-id base-id] | [remove id] | [mount id mountpoint]\n", os.Args[0]) + flag.PrintDefaults() + os.Exit(1) +} + +func byteSizeFromString(arg string) (int64, error) { + digits := "" + rest := "" + last := strings.LastIndexAny(arg, "0123456789") + if last >= 0 { + digits = arg[:last+1] + rest = arg[last+1:] + } + + val, err := strconv.ParseInt(digits, 10, 64) + if err != nil { + return val, err + } + + rest = strings.ToLower(strings.TrimSpace(rest)) + + var multiplier int64 = 1 + switch rest { + case "": + multiplier = 1 + case "k", "kb": + multiplier = 1024 + case "m", "mb": + multiplier = 1024 * 1024 + case "g", "gb": + multiplier = 1024 * 1024 * 1024 + case "t", "tb": + multiplier = 1024 * 1024 * 1024 * 1024 + default: + return 0, fmt.Errorf("Unknown size unit: %s", rest) + } + + return val * multiplier, nil +} + +func main() { + root := flag.String("r", "/var/lib/docker", "Docker root dir") + flDebug := flag.Bool("D", false, "Debug mode") + + flag.Parse() + + if *flDebug { + os.Setenv("DEBUG", "1") + logrus.SetLevel(logrus.DebugLevel) + } + + if flag.NArg() < 1 { + usage() + } + + args := flag.Args() + + home := path.Join(*root, "devicemapper") + devices, err := devmapper.NewDeviceSet(home, false, nil, nil, nil) + if err != nil { + fmt.Println("Can't initialize device mapper: ", err) + os.Exit(1) + } + + switch args[0] { + case "status": + status := devices.Status() + fmt.Printf("Pool name: %s\n", status.PoolName) + fmt.Printf("Data Loopback file: %s\n", status.DataLoopback) + fmt.Printf("Metadata Loopback file: %s\n", status.MetadataLoopback) + fmt.Printf("Sector size: %d\n", status.SectorSize) + fmt.Printf("Data use: %d of %d (%.1f %%)\n", status.Data.Used, status.Data.Total, 100.0*float64(status.Data.Used)/float64(status.Data.Total)) + fmt.Printf("Metadata use: %d of %d (%.1f %%)\n", status.Metadata.Used, status.Metadata.Total, 100.0*float64(status.Metadata.Used)/float64(status.Metadata.Total)) + break + case "list": + ids := devices.List() + sort.Strings(ids) + for _, id := range ids { + fmt.Println(id) + } + break + case "device": + if flag.NArg() < 2 { + usage() + } + status, err := devices.GetDeviceStatus(args[1]) + if err != nil { + fmt.Println("Can't get device info: ", err) + os.Exit(1) + } + fmt.Printf("Id: %d\n", status.DeviceID) + fmt.Printf("Size: %d\n", status.Size) + fmt.Printf("Transaction Id: %d\n", status.TransactionID) + fmt.Printf("Size in Sectors: %d\n", status.SizeInSectors) + fmt.Printf("Mapped Sectors: %d\n", status.MappedSectors) + fmt.Printf("Highest Mapped Sector: %d\n", status.HighestMappedSector) + break + case "resize": + if flag.NArg() < 2 { + usage() + } + + size, err := byteSizeFromString(args[1]) + if err != nil { + fmt.Println("Invalid size: ", err) + os.Exit(1) + } + + err = devices.ResizePool(size) + if err != nil { + fmt.Println("Error resizing pool: ", err) + os.Exit(1) + } + + break + case "snap": + if flag.NArg() < 3 { + usage() + } + + err := devices.AddDevice(args[1], args[2]) + if err != nil { + fmt.Println("Can't create snap device: ", err) + os.Exit(1) + } + break + case "remove": + if flag.NArg() < 2 { + usage() + } + + err := devicemapper.RemoveDevice(args[1]) + if err != nil { + fmt.Println("Can't remove device: ", err) + os.Exit(1) + } + break + case "mount": + if flag.NArg() < 3 { + usage() + } + + err := devices.MountDevice(args[1], args[2], "") + if err != nil { + fmt.Println("Can't create snap device: ", err) + os.Exit(1) + } + break + default: + fmt.Printf("Unknown command %s\n", args[0]) + usage() + + os.Exit(1) + } + + return +} diff --git a/contrib/docker-device-tool/device_tool_windows.go b/contrib/docker-device-tool/device_tool_windows.go new file mode 100644 index 00000000..da29a2ca --- /dev/null +++ b/contrib/docker-device-tool/device_tool_windows.go @@ -0,0 +1,4 @@ +package main + +func main() { +} diff --git a/contrib/docker-engine-selinux/LICENSE b/contrib/docker-engine-selinux/LICENSE new file mode 100644 index 00000000..5b6e7c66 --- /dev/null +++ b/contrib/docker-engine-selinux/LICENSE @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/contrib/docker-engine-selinux/Makefile b/contrib/docker-engine-selinux/Makefile new file mode 100644 index 00000000..1bdc695a --- /dev/null +++ b/contrib/docker-engine-selinux/Makefile @@ -0,0 +1,16 @@ +TARGETS?=docker +MODULES?=${TARGETS:=.pp.bz2} +SHAREDIR?=/usr/share + +all: ${TARGETS:=.pp.bz2} + +%.pp.bz2: %.pp + @echo Compressing $^ -\> $@ + bzip2 -9 $^ + +%.pp: %.te + make -f ${SHAREDIR}/selinux/devel/Makefile $@ + +clean: + rm -f *~ *.tc *.pp *.pp.bz2 + rm -rf tmp *.tar.gz diff --git a/contrib/docker-engine-selinux/docker.fc b/contrib/docker-engine-selinux/docker.fc new file mode 100644 index 00000000..589ee287 --- /dev/null +++ b/contrib/docker-engine-selinux/docker.fc @@ -0,0 +1,20 @@ +/root/\.docker gen_context(system_u:object_r:docker_home_t,s0) + +/usr/bin/docker -- gen_context(system_u:object_r:docker_exec_t,s0) + +/usr/lib/systemd/system/docker.service -- gen_context(system_u:object_r:docker_unit_file_t,s0) + +/etc/docker(/.*)? gen_context(system_u:object_r:docker_config_t,s0) + +/var/lib/docker(/.*)? gen_context(system_u:object_r:docker_var_lib_t,s0) +/var/lib/kublet(/.*)? gen_context(system_u:object_r:docker_var_lib_t,s0) +/var/lib/docker/vfs(/.*)? gen_context(system_u:object_r:svirt_sandbox_file_t,s0) + +/var/run/docker\.pid -- gen_context(system_u:object_r:docker_var_run_t,s0) +/var/run/docker\.sock -s gen_context(system_u:object_r:docker_var_run_t,s0) +/var/run/docker-client(/.*)? gen_context(system_u:object_r:docker_var_run_t,s0) + +/var/lib/docker/init(/.*)? gen_context(system_u:object_r:docker_share_t,s0) +/var/lib/docker/containers/.*/hosts gen_context(system_u:object_r:docker_share_t,s0) +/var/lib/docker/containers/.*/hostname gen_context(system_u:object_r:docker_share_t,s0) +/var/lib/docker/.*/config\.env gen_context(system_u:object_r:docker_share_t,s0) diff --git a/contrib/docker-engine-selinux/docker.if b/contrib/docker-engine-selinux/docker.if new file mode 100644 index 00000000..ca075c05 --- /dev/null +++ b/contrib/docker-engine-selinux/docker.if @@ -0,0 +1,461 @@ + +## The open-source application container engine. + +######################################## +## +## Execute docker in the docker domain. +## +## +## +## Domain allowed to transition. +## +## +# +interface(`docker_domtrans',` + gen_require(` + type docker_t, docker_exec_t; + ') + + corecmd_search_bin($1) + domtrans_pattern($1, docker_exec_t, docker_t) +') + +######################################## +## +## Execute docker in the caller domain. +## +## +## +## Domain allowed to transition. +## +## +# +interface(`docker_exec',` + gen_require(` + type docker_exec_t; + ') + + corecmd_search_bin($1) + can_exec($1, docker_exec_t) +') + +######################################## +## +## Search docker lib directories. +## +## +## +## Domain allowed access. +## +## +# +interface(`docker_search_lib',` + gen_require(` + type docker_var_lib_t; + ') + + allow $1 docker_var_lib_t:dir search_dir_perms; + files_search_var_lib($1) +') + +######################################## +## +## Execute docker lib directories. +## +## +## +## Domain allowed access. +## +## +# +interface(`docker_exec_lib',` + gen_require(` + type docker_var_lib_t; + ') + + allow $1 docker_var_lib_t:dir search_dir_perms; + can_exec($1, docker_var_lib_t) +') + +######################################## +## +## Read docker lib files. +## +## +## +## Domain allowed access. +## +## +# +interface(`docker_read_lib_files',` + gen_require(` + type docker_var_lib_t; + ') + + files_search_var_lib($1) + read_files_pattern($1, docker_var_lib_t, docker_var_lib_t) +') + +######################################## +## +## Read docker share files. +## +## +## +## Domain allowed access. +## +## +# +interface(`docker_read_share_files',` + gen_require(` + type docker_share_t; + ') + + files_search_var_lib($1) + read_files_pattern($1, docker_share_t, docker_share_t) +') + +######################################## +## +## Manage docker lib files. +## +## +## +## Domain allowed access. +## +## +# +interface(`docker_manage_lib_files',` + gen_require(` + type docker_var_lib_t; + ') + + files_search_var_lib($1) + manage_files_pattern($1, docker_var_lib_t, docker_var_lib_t) + manage_lnk_files_pattern($1, docker_var_lib_t, docker_var_lib_t) +') + +######################################## +## +## Manage docker lib directories. +## +## +## +## Domain allowed access. +## +## +# +interface(`docker_manage_lib_dirs',` + gen_require(` + type docker_var_lib_t; + ') + + files_search_var_lib($1) + manage_dirs_pattern($1, docker_var_lib_t, docker_var_lib_t) +') + +######################################## +## +## Create objects in a docker var lib directory +## with an automatic type transition to +## a specified private type. +## +## +## +## Domain allowed access. +## +## +## +## +## The type of the object to create. +## +## +## +## +## The class of the object to be created. +## +## +## +## +## The name of the object being created. +## +## +# +interface(`docker_lib_filetrans',` + gen_require(` + type docker_var_lib_t; + ') + + filetrans_pattern($1, docker_var_lib_t, $2, $3, $4) +') + +######################################## +## +## Read docker PID files. +## +## +## +## Domain allowed access. +## +## +# +interface(`docker_read_pid_files',` + gen_require(` + type docker_var_run_t; + ') + + files_search_pids($1) + read_files_pattern($1, docker_var_run_t, docker_var_run_t) +') + +######################################## +## +## Execute docker server in the docker domain. +## +## +## +## Domain allowed to transition. +## +## +# +interface(`docker_systemctl',` + gen_require(` + type docker_t; + type docker_unit_file_t; + ') + + systemd_exec_systemctl($1) + init_reload_services($1) + systemd_read_fifo_file_passwd_run($1) + allow $1 docker_unit_file_t:file read_file_perms; + allow $1 docker_unit_file_t:service manage_service_perms; + + ps_process_pattern($1, docker_t) +') + +######################################## +## +## Read and write docker shared memory. +## +## +## +## Domain allowed access. +## +## +# +interface(`docker_rw_sem',` + gen_require(` + type docker_t; + ') + + allow $1 docker_t:sem rw_sem_perms; +') + +####################################### +## +## Read and write the docker pty type. +## +## +## +## Domain allowed access. +## +## +# +interface(`docker_use_ptys',` + gen_require(` + type docker_devpts_t; + ') + + allow $1 docker_devpts_t:chr_file rw_term_perms; +') + +####################################### +## +## Allow domain to create docker content +## +## +## +## Domain allowed access. +## +## +# +interface(`docker_filetrans_named_content',` + + gen_require(` + type docker_var_lib_t; + type docker_share_t; + type docker_log_t; + type docker_var_run_t; + type docker_home_t; + ') + + files_pid_filetrans($1, docker_var_run_t, file, "docker.pid") + files_pid_filetrans($1, docker_var_run_t, sock_file, "docker.sock") + files_pid_filetrans($1, docker_var_run_t, dir, "docker-client") + files_var_lib_filetrans($1, docker_var_lib_t, dir, "docker") + filetrans_pattern($1, docker_var_lib_t, docker_share_t, file, "config.env") + filetrans_pattern($1, docker_var_lib_t, docker_share_t, file, "hosts") + filetrans_pattern($1, docker_var_lib_t, docker_share_t, file, "hostname") + filetrans_pattern($1, docker_var_lib_t, docker_share_t, file, "resolv.conf") + filetrans_pattern($1, docker_var_lib_t, docker_share_t, dir, "init") + userdom_admin_home_dir_filetrans($1, docker_home_t, dir, ".docker") +') + +######################################## +## +## Connect to docker over a unix stream socket. +## +## +## +## Domain allowed access. +## +## +# +interface(`docker_stream_connect',` + gen_require(` + type docker_t, docker_var_run_t; + ') + + files_search_pids($1) + stream_connect_pattern($1, docker_var_run_t, docker_var_run_t, docker_t) +') + +######################################## +## +## Connect to SPC containers over a unix stream socket. +## +## +## +## Domain allowed access. +## +## +# +interface(`docker_spc_stream_connect',` + gen_require(` + type spc_t, spc_var_run_t; + ') + + files_search_pids($1) + files_write_all_pid_sockets($1) + allow $1 spc_t:unix_stream_socket connectto; +') + + +######################################## +## +## All of the rules required to administrate +## an docker environment +## +## +## +## Domain allowed access. +## +## +# +interface(`docker_admin',` + gen_require(` + type docker_t; + type docker_var_lib_t, docker_var_run_t; + type docker_unit_file_t; + type docker_lock_t; + type docker_log_t; + type docker_config_t; + ') + + allow $1 docker_t:process { ptrace signal_perms }; + ps_process_pattern($1, docker_t) + + admin_pattern($1, docker_config_t) + + files_search_var_lib($1) + admin_pattern($1, docker_var_lib_t) + + files_search_pids($1) + admin_pattern($1, docker_var_run_t) + + files_search_locks($1) + admin_pattern($1, docker_lock_t) + + logging_search_logs($1) + admin_pattern($1, docker_log_t) + + docker_systemctl($1) + admin_pattern($1, docker_unit_file_t) + allow $1 docker_unit_file_t:service all_service_perms; + + optional_policy(` + systemd_passwd_agent_exec($1) + systemd_read_fifo_file_passwd_run($1) + ') +') + +interface(`domain_stub_named_filetrans_domain',` + gen_require(` + attribute named_filetrans_domain; + ') +') + +interface(`lvm_stub',` + gen_require(` + type lvm_t; + ') +') +interface(`staff_stub',` + gen_require(` + type staff_t; + ') +') +interface(`virt_stub_svirt_sandbox_domain',` + gen_require(` + attribute svirt_sandbox_domain; + ') +') +interface(`virt_stub_svirt_sandbox_file',` + gen_require(` + type svirt_sandbox_file_t; + ') +') +interface(`fs_dontaudit_remount_tmpfs',` + gen_require(` + type tmpfs_t; + ') + + dontaudit $1 tmpfs_t:filesystem remount; +') +interface(`dev_dontaudit_list_all_dev_nodes',` + gen_require(` + type device_t; + ') + + dontaudit $1 device_t:dir list_dir_perms; +') +interface(`kernel_unlabeled_entry_type',` + gen_require(` + type unlabeled_t; + ') + + domain_entry_file($1, unlabeled_t) +') +interface(`kernel_unlabeled_domtrans',` + gen_require(` + type unlabeled_t; + ') + + read_lnk_files_pattern($1, unlabeled_t, unlabeled_t) + domain_transition_pattern($1, unlabeled_t, $2) + type_transition $1 unlabeled_t:process $2; +') +interface(`files_write_all_pid_sockets',` + gen_require(` + attribute pidfile; + ') + + allow $1 pidfile:sock_file write_sock_file_perms; +') +interface(`dev_dontaudit_mounton_sysfs',` + gen_require(` + type sysfs_t; + ') + + dontaudit $1 sysfs_t:dir mounton; +') diff --git a/contrib/docker-engine-selinux/docker.te b/contrib/docker-engine-selinux/docker.te new file mode 100644 index 00000000..999742f3 --- /dev/null +++ b/contrib/docker-engine-selinux/docker.te @@ -0,0 +1,414 @@ +policy_module(docker, 1.0.0) + +######################################## +# +# Declarations +# + +## +##

+## Allow sandbox containers manage fuse files +##

+##
+gen_tunable(virt_sandbox_use_fusefs, false) + +## +##

+## Determine whether docker can +## connect to all TCP ports. +##

+##
+gen_tunable(docker_connect_any, false) + +type docker_t; +type docker_exec_t; +init_daemon_domain(docker_t, docker_exec_t) +domain_subj_id_change_exemption(docker_t) +domain_role_change_exemption(docker_t) + +type spc_t; +domain_type(spc_t) +role system_r types spc_t; + +type spc_var_run_t; +files_pid_file(spc_var_run_t) + +type docker_var_lib_t; +files_type(docker_var_lib_t) + +type docker_home_t; +userdom_user_home_content(docker_home_t) + +type docker_config_t; +files_config_file(docker_config_t) + +type docker_lock_t; +files_lock_file(docker_lock_t) + +type docker_log_t; +logging_log_file(docker_log_t) + +type docker_tmp_t; +files_tmp_file(docker_tmp_t) + +type docker_tmpfs_t; +files_tmpfs_file(docker_tmpfs_t) + +type docker_var_run_t; +files_pid_file(docker_var_run_t) + +type docker_unit_file_t; +systemd_unit_file(docker_unit_file_t) + +type docker_devpts_t; +term_pty(docker_devpts_t) + +type docker_share_t; +files_type(docker_share_t) + +######################################## +# +# docker local policy +# +allow docker_t self:capability { chown kill fowner fsetid mknod net_admin net_bind_service net_raw setfcap }; +allow docker_t self:tun_socket relabelto; +allow docker_t self:process { getattr signal_perms setrlimit setfscreate }; +allow docker_t self:fifo_file rw_fifo_file_perms; +allow docker_t self:unix_stream_socket create_stream_socket_perms; +allow docker_t self:tcp_socket create_stream_socket_perms; +allow docker_t self:udp_socket create_socket_perms; +allow docker_t self:capability2 block_suspend; + +manage_files_pattern(docker_t, docker_home_t, docker_home_t) +manage_dirs_pattern(docker_t, docker_home_t, docker_home_t) +manage_lnk_files_pattern(docker_t, docker_home_t, docker_home_t) +userdom_admin_home_dir_filetrans(docker_t, docker_home_t, dir, ".docker") + +manage_dirs_pattern(docker_t, docker_config_t, docker_config_t) +manage_files_pattern(docker_t, docker_config_t, docker_config_t) +files_etc_filetrans(docker_t, docker_config_t, dir, "docker") + +manage_dirs_pattern(docker_t, docker_lock_t, docker_lock_t) +manage_files_pattern(docker_t, docker_lock_t, docker_lock_t) + +manage_dirs_pattern(docker_t, docker_log_t, docker_log_t) +manage_files_pattern(docker_t, docker_log_t, docker_log_t) +manage_lnk_files_pattern(docker_t, docker_log_t, docker_log_t) +logging_log_filetrans(docker_t, docker_log_t, { dir file lnk_file }) +allow docker_t docker_log_t:dir_file_class_set { relabelfrom relabelto }; + +manage_dirs_pattern(docker_t, docker_tmp_t, docker_tmp_t) +manage_files_pattern(docker_t, docker_tmp_t, docker_tmp_t) +manage_lnk_files_pattern(docker_t, docker_tmp_t, docker_tmp_t) +files_tmp_filetrans(docker_t, docker_tmp_t, { dir file lnk_file }) + +manage_dirs_pattern(docker_t, docker_tmpfs_t, docker_tmpfs_t) +manage_files_pattern(docker_t, docker_tmpfs_t, docker_tmpfs_t) +manage_lnk_files_pattern(docker_t, docker_tmpfs_t, docker_tmpfs_t) +manage_fifo_files_pattern(docker_t, docker_tmpfs_t, docker_tmpfs_t) +manage_chr_files_pattern(docker_t, docker_tmpfs_t, docker_tmpfs_t) +manage_blk_files_pattern(docker_t, docker_tmpfs_t, docker_tmpfs_t) +allow docker_t docker_tmpfs_t:dir relabelfrom; +can_exec(docker_t, docker_tmpfs_t) +fs_tmpfs_filetrans(docker_t, docker_tmpfs_t, { dir file }) +allow docker_t docker_tmpfs_t:chr_file mounton; + +manage_dirs_pattern(docker_t, docker_share_t, docker_share_t) +manage_files_pattern(docker_t, docker_share_t, docker_share_t) +manage_lnk_files_pattern(docker_t, docker_share_t, docker_share_t) +allow docker_t docker_share_t:dir_file_class_set { relabelfrom relabelto }; + +can_exec(docker_t, docker_share_t) +#docker_filetrans_named_content(docker_t) + +manage_dirs_pattern(docker_t, docker_var_lib_t, docker_var_lib_t) +manage_chr_files_pattern(docker_t, docker_var_lib_t, docker_var_lib_t) +manage_blk_files_pattern(docker_t, docker_var_lib_t, docker_var_lib_t) +manage_files_pattern(docker_t, docker_var_lib_t, docker_var_lib_t) +manage_lnk_files_pattern(docker_t, docker_var_lib_t, docker_var_lib_t) +allow docker_t docker_var_lib_t:dir_file_class_set { relabelfrom relabelto }; +files_var_lib_filetrans(docker_t, docker_var_lib_t, { dir file lnk_file }) + +manage_dirs_pattern(docker_t, docker_var_run_t, docker_var_run_t) +manage_files_pattern(docker_t, docker_var_run_t, docker_var_run_t) +manage_sock_files_pattern(docker_t, docker_var_run_t, docker_var_run_t) +manage_lnk_files_pattern(docker_t, docker_var_run_t, docker_var_run_t) +files_pid_filetrans(docker_t, docker_var_run_t, { dir file lnk_file sock_file }) + +allow docker_t docker_devpts_t:chr_file { relabelfrom rw_chr_file_perms setattr_chr_file_perms }; +term_create_pty(docker_t, docker_devpts_t) + +kernel_read_system_state(docker_t) +kernel_read_network_state(docker_t) +kernel_read_all_sysctls(docker_t) +kernel_rw_net_sysctls(docker_t) +kernel_setsched(docker_t) +kernel_read_all_proc(docker_t) + +domain_use_interactive_fds(docker_t) +domain_dontaudit_read_all_domains_state(docker_t) + +corecmd_exec_bin(docker_t) +corecmd_exec_shell(docker_t) + +corenet_tcp_bind_generic_node(docker_t) +corenet_tcp_sendrecv_generic_if(docker_t) +corenet_tcp_sendrecv_generic_node(docker_t) +corenet_tcp_sendrecv_generic_port(docker_t) +corenet_tcp_bind_all_ports(docker_t) +corenet_tcp_connect_http_port(docker_t) +corenet_tcp_connect_commplex_main_port(docker_t) +corenet_udp_sendrecv_generic_if(docker_t) +corenet_udp_sendrecv_generic_node(docker_t) +corenet_udp_sendrecv_all_ports(docker_t) +corenet_udp_bind_generic_node(docker_t) +corenet_udp_bind_all_ports(docker_t) + +files_read_config_files(docker_t) +files_dontaudit_getattr_all_dirs(docker_t) +files_dontaudit_getattr_all_files(docker_t) + +fs_read_cgroup_files(docker_t) +fs_read_tmpfs_symlinks(docker_t) +fs_search_all(docker_t) +fs_getattr_all_fs(docker_t) + +storage_raw_rw_fixed_disk(docker_t) + +auth_use_nsswitch(docker_t) +auth_dontaudit_getattr_shadow(docker_t) + +init_read_state(docker_t) +init_status(docker_t) + +logging_send_audit_msgs(docker_t) +logging_send_syslog_msg(docker_t) + +miscfiles_read_localization(docker_t) + +mount_domtrans(docker_t) + +seutil_read_default_contexts(docker_t) +seutil_read_config(docker_t) + +sysnet_dns_name_resolve(docker_t) +sysnet_exec_ifconfig(docker_t) + +optional_policy(` + rpm_exec(docker_t) + rpm_read_db(docker_t) + rpm_exec(docker_t) +') + +optional_policy(` + fstools_domtrans(docker_t) +') + +optional_policy(` + iptables_domtrans(docker_t) +') + +optional_policy(` + openvswitch_stream_connect(docker_t) +') + +allow docker_t self:capability { dac_override setgid setpcap setuid sys_admin sys_boot sys_chroot sys_ptrace }; + +allow docker_t self:process { getcap setcap setexec setpgid setsched signal_perms }; + +allow docker_t self:netlink_route_socket rw_netlink_socket_perms;; +allow docker_t self:netlink_audit_socket create_netlink_socket_perms; +allow docker_t self:unix_dgram_socket { create_socket_perms sendto }; +allow docker_t self:unix_stream_socket { create_stream_socket_perms connectto }; + +allow docker_t docker_var_lib_t:dir mounton; +allow docker_t docker_var_lib_t:chr_file mounton; +can_exec(docker_t, docker_var_lib_t) + +kernel_dontaudit_setsched(docker_t) +kernel_get_sysvipc_info(docker_t) +kernel_request_load_module(docker_t) +kernel_mounton_messages(docker_t) +kernel_mounton_all_proc(docker_t) +kernel_mounton_all_sysctls(docker_t) +kernel_unlabeled_entry_type(spc_t) +kernel_unlabeled_domtrans(docker_t, spc_t) + +dev_getattr_all(docker_t) +dev_getattr_sysfs_fs(docker_t) +dev_read_urand(docker_t) +dev_read_lvm_control(docker_t) +dev_rw_sysfs(docker_t) +dev_rw_loop_control(docker_t) +dev_rw_lvm_control(docker_t) + +files_getattr_isid_type_dirs(docker_t) +files_manage_isid_type_dirs(docker_t) +files_manage_isid_type_files(docker_t) +files_manage_isid_type_symlinks(docker_t) +files_manage_isid_type_chr_files(docker_t) +files_manage_isid_type_blk_files(docker_t) +files_exec_isid_files(docker_t) +files_mounton_isid(docker_t) +files_mounton_non_security(docker_t) +files_mounton_isid_type_chr_file(docker_t) + +fs_mount_all_fs(docker_t) +fs_unmount_all_fs(docker_t) +fs_remount_all_fs(docker_t) +files_mounton_isid(docker_t) +fs_manage_cgroup_dirs(docker_t) +fs_manage_cgroup_files(docker_t) +fs_relabelfrom_xattr_fs(docker_t) +fs_relabelfrom_tmpfs(docker_t) +fs_read_tmpfs_symlinks(docker_t) +fs_list_hugetlbfs(docker_t) + +term_use_generic_ptys(docker_t) +term_use_ptmx(docker_t) +term_getattr_pty_fs(docker_t) +term_relabel_pty_fs(docker_t) +term_mounton_unallocated_ttys(docker_t) + +modutils_domtrans_insmod(docker_t) + +systemd_status_all_unit_files(docker_t) +systemd_start_systemd_services(docker_t) + +userdom_stream_connect(docker_t) +userdom_search_user_home_content(docker_t) +userdom_read_all_users_state(docker_t) +userdom_relabel_user_home_files(docker_t) +userdom_relabel_user_tmp_files(docker_t) +userdom_relabel_user_tmp_dirs(docker_t) + +optional_policy(` + gpm_getattr_gpmctl(docker_t) +') + +optional_policy(` + dbus_system_bus_client(docker_t) + init_dbus_chat(docker_t) + init_start_transient_unit(docker_t) + + optional_policy(` + systemd_dbus_chat_logind(docker_t) + ') + + optional_policy(` + firewalld_dbus_chat(docker_t) + ') +') + +optional_policy(` + udev_read_db(docker_t) +') + +optional_policy(` + virt_read_config(docker_t) + virt_exec(docker_t) + virt_stream_connect(docker_t) + virt_stream_connect_sandbox(docker_t) + virt_exec_sandbox_files(docker_t) + virt_manage_sandbox_files(docker_t) + virt_relabel_sandbox_filesystem(docker_t) + virt_transition_svirt_sandbox(docker_t, system_r) + virt_mounton_sandbox_file(docker_t) +# virt_attach_sandbox_tun_iface(docker_t) + allow docker_t svirt_sandbox_domain:tun_socket relabelfrom; +') + +tunable_policy(`docker_connect_any',` + corenet_tcp_connect_all_ports(docker_t) + corenet_sendrecv_all_packets(docker_t) + corenet_tcp_sendrecv_all_ports(docker_t) +') + +######################################## +# +# spc local policy +# +domain_entry_file(spc_t, docker_share_t) +domain_entry_file(spc_t, docker_var_lib_t) +role system_r types spc_t; + +domain_entry_file(spc_t, docker_share_t) +domain_entry_file(spc_t, docker_var_lib_t) +domtrans_pattern(docker_t, docker_share_t, spc_t) +domtrans_pattern(docker_t, docker_var_lib_t, spc_t) +allow docker_t spc_t:process { setsched signal_perms }; +ps_process_pattern(docker_t, spc_t) +allow docker_t spc_t:socket_class_set { relabelto relabelfrom }; + +optional_policy(` + dbus_chat_system_bus(spc_t) +') + +optional_policy(` + unconfined_domain_noaudit(spc_t) +') + +optional_policy(` + unconfined_domain(docker_t) +') + +optional_policy(` + virt_transition_svirt_sandbox(spc_t, system_r) +') + +######################################## +# +# docker upstream policy +# + +optional_policy(` +# domain_stub_named_filetrans_domain() + gen_require(` + attribute named_filetrans_domain; + ') + + docker_filetrans_named_content(named_filetrans_domain) +') + +optional_policy(` + lvm_stub() + docker_rw_sem(lvm_t) +') + +optional_policy(` + staff_stub() + docker_stream_connect(staff_t) + docker_exec(staff_t) +') + +optional_policy(` + virt_stub_svirt_sandbox_domain() + virt_stub_svirt_sandbox_file() + allow svirt_sandbox_domain self:netlink_kobject_uevent_socket create_socket_perms; + docker_read_share_files(svirt_sandbox_domain) + docker_lib_filetrans(svirt_sandbox_domain,svirt_sandbox_file_t, sock_file) + docker_use_ptys(svirt_sandbox_domain) + docker_spc_stream_connect(svirt_sandbox_domain) + fs_list_tmpfs(svirt_sandbox_domain) + fs_rw_hugetlbfs_files(svirt_sandbox_domain) + fs_dontaudit_remount_tmpfs(svirt_sandbox_domain) + dev_dontaudit_mounton_sysfs(svirt_sandbox_domain) + + tunable_policy(`virt_sandbox_use_fusefs',` + fs_manage_fusefs_dirs(svirt_sandbox_domain) + fs_manage_fusefs_files(svirt_sandbox_domain) + fs_manage_fusefs_symlinks(svirt_sandbox_domain) + ') + gen_require(` + attribute domain; + ') + + dontaudit svirt_sandbox_domain domain:key {search link}; +') + +optional_policy(` + gen_require(` + type pcp_pmcd_t; + ') + docker_manage_lib_files(pcp_pmcd_t) +') diff --git a/contrib/docker-engine-selinux/docker_selinux.8.gz b/contrib/docker-engine-selinux/docker_selinux.8.gz new file mode 100644 index 0000000000000000000000000000000000000000..ab5d59445ac1601ca378aaa3e71fb9cff43a1592 GIT binary patch literal 2847 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 xb`W`pZ+>}u< /dev/null; then + echo >&2 'error: "qemu-nbd" not found!' + exit 1 +fi + +usage() { + echo "Convert disk image to docker image" + echo "" + echo "usage: $0 image-name disk-image-file [ base-image ]" + echo " ie: $0 cirros:0.3.3 cirros-0.3.3-x86_64-disk.img" + echo " $0 ubuntu:cloud ubuntu-14.04-server-cloudimg-amd64-disk1.img ubuntu:14.04" +} + +if [ "$#" -lt 2 ]; then + usage + exit 1 +fi + +CURDIR=$(pwd) + +image_name="${1%:*}" +image_tag="${1#*:}" +if [ "$image_tag" == "$1" ]; then + image_tag="latest" +fi + +disk_image_file="$2" +docker_base_image="$3" + +block_device=/dev/nbd0 + +builddir=$(mktemp -d) + +cleanup() { + umount "$builddir/disk_image" || true + umount "$builddir/workdir" || true + qemu-nbd -d $block_device &> /dev/null || true + rm -rf $builddir +} +trap cleanup EXIT + +# Mount disk image +modprobe nbd max_part=63 +qemu-nbd -rc ${block_device} -P 1 "$disk_image_file" +mkdir "$builddir/disk_image" +mount -o ro ${block_device} "$builddir/disk_image" + +mkdir "$builddir/workdir" +mkdir "$builddir/diff" + +base_image_mounts="" + +# Unpack base image +if [ -n "$docker_base_image" ]; then + mkdir -p "$builddir/base" + docker pull "$docker_base_image" + docker save "$docker_base_image" | tar -xC "$builddir/base" + + image_id=$(docker inspect -f "{{.Id}}" "$docker_base_image") + while [ -n "$image_id" ]; do + mkdir -p "$builddir/base/$image_id/layer" + tar -xf "$builddir/base/$image_id/layer.tar" -C "$builddir/base/$image_id/layer" + + base_image_mounts="${base_image_mounts}:$builddir/base/$image_id/layer=ro+wh" + image_id=$(docker inspect -f "{{.Parent}}" "$image_id") + done +fi + +# Mount work directory +mount -t aufs -o "br=$builddir/diff=rw${base_image_mounts},dio,xino=/dev/shm/aufs.xino" none "$builddir/workdir" + +# Update files +cd $builddir +LC_ALL=C diff -rq disk_image workdir \ + | sed -re "s|Only in workdir(.*?): |DEL \1/|g;s|Only in disk_image(.*?): |ADD \1/|g;s|Files disk_image/(.+) and workdir/(.+) differ|UPDATE /\1|g" \ + | while read action entry; do + case "$action" in + ADD|UPDATE) + cp -a "disk_image$entry" "workdir$entry" + ;; + DEL) + rm -rf "workdir$entry" + ;; + *) + echo "Error: unknown diff line: $action $entry" >&2 + ;; + esac + done + +# Pack new image +new_image_id="$(for i in $(seq 1 32); do printf "%02x" $(($RANDOM % 256)); done)" +mkdir -p $builddir/result/$new_image_id +cd diff +tar -cf $builddir/result/$new_image_id/layer.tar * +echo "1.0" > $builddir/result/$new_image_id/VERSION +cat > $builddir/result/$new_image_id/json <<-EOS +{ "docker_version": "1.4.1" +, "id": "$new_image_id" +, "created": "$(date -u +%Y-%m-%dT%H:%M:%S.%NZ)" +EOS + +if [ -n "$docker_base_image" ]; then + image_id=$(docker inspect -f "{{.Id}}" "$docker_base_image") + echo ", \"parent\": \"$image_id\"" >> $builddir/result/$new_image_id/json +fi + +echo "}" >> $builddir/result/$new_image_id/json + +echo "{\"$image_name\":{\"$image_tag\":\"$new_image_id\"}}" > $builddir/result/repositories + +cd $builddir/result + +# mkdir -p $CURDIR/$image_name +# cp -r * $CURDIR/$image_name +tar -c * | docker load diff --git a/contrib/download-frozen-image-v1.sh b/contrib/download-frozen-image-v1.sh new file mode 100755 index 00000000..29d7ff59 --- /dev/null +++ b/contrib/download-frozen-image-v1.sh @@ -0,0 +1,108 @@ +#!/bin/bash +set -e + +# hello-world latest ef872312fe1b 3 months ago 910 B +# hello-world latest ef872312fe1bbc5e05aae626791a47ee9b032efa8f3bda39cc0be7b56bfe59b9 3 months ago 910 B + +# debian latest f6fab3b798be 10 weeks ago 85.1 MB +# debian latest f6fab3b798be3174f45aa1eb731f8182705555f89c9026d8c1ef230cbf8301dd 10 weeks ago 85.1 MB + +if ! command -v curl &> /dev/null; then + echo >&2 'error: "curl" not found!' + exit 1 +fi + +usage() { + echo "usage: $0 dir image[:tag][@image-id] ..." + echo " ie: $0 /tmp/hello-world hello-world" + echo " $0 /tmp/debian-jessie debian:jessie" + echo " $0 /tmp/old-hello-world hello-world@ef872312fe1bbc5e05aae626791a47ee9b032efa8f3bda39cc0be7b56bfe59b9" + echo " $0 /tmp/old-debian debian:latest@f6fab3b798be3174f45aa1eb731f8182705555f89c9026d8c1ef230cbf8301dd" + [ -z "$1" ] || exit "$1" +} + +dir="$1" # dir for building tar in +shift || usage 1 >&2 + +[ $# -gt 0 -a "$dir" ] || usage 2 >&2 +mkdir -p "$dir" + +# hacky workarounds for Bash 3 support (no associative arrays) +images=() +rm -f "$dir"/tags-*.tmp +# repositories[busybox]='"latest": "...", "ubuntu-14.04": "..."' + +while [ $# -gt 0 ]; do + imageTag="$1" + shift + image="${imageTag%%[:@]*}" + tag="${imageTag#*:}" + imageId="${tag##*@}" + [ "$imageId" != "$tag" ] || imageId= + [ "$tag" != "$imageTag" ] || tag='latest' + tag="${tag%@*}" + + imageFile="${image//\//_}" # "/" can't be in filenames :) + + token="$(curl -sSL -o /dev/null -D- -H 'X-Docker-Token: true' "https://index.docker.io/v1/repositories/$image/images" | tr -d '\r' | awk -F ': *' '$1 == "X-Docker-Token" { print $2 }')" + + if [ -z "$imageId" ]; then + imageId="$(curl -sSL -H "Authorization: Token $token" "https://registry-1.docker.io/v1/repositories/$image/tags/$tag")" + imageId="${imageId//\"/}" + fi + + ancestryJson="$(curl -sSL -H "Authorization: Token $token" "https://registry-1.docker.io/v1/images/$imageId/ancestry")" + if [ "${ancestryJson:0:1}" != '[' ]; then + echo >&2 "error: /v1/images/$imageId/ancestry returned something unexpected:" + echo >&2 " $ancestryJson" + exit 1 + fi + + IFS=',' + ancestry=( ${ancestryJson//[\[\] \"]/} ) + unset IFS + + if [ -s "$dir/tags-$imageFile.tmp" ]; then + echo -n ', ' >> "$dir/tags-$imageFile.tmp" + else + images=( "${images[@]}" "$image" ) + fi + echo -n '"'"$tag"'": "'"$imageId"'"' >> "$dir/tags-$imageFile.tmp" + + echo "Downloading '$imageTag' (${#ancestry[@]} layers)..." + for imageId in "${ancestry[@]}"; do + mkdir -p "$dir/$imageId" + echo '1.0' > "$dir/$imageId/VERSION" + + curl -sSL -H "Authorization: Token $token" "https://registry-1.docker.io/v1/images/$imageId/json" -o "$dir/$imageId/json" + + # TODO figure out why "-C -" doesn't work here + # "curl: (33) HTTP server doesn't seem to support byte ranges. Cannot resume." + # "HTTP/1.1 416 Requested Range Not Satisfiable" + if [ -f "$dir/$imageId/layer.tar" ]; then + # TODO hackpatch for no -C support :'( + echo "skipping existing ${imageId:0:12}" + continue + fi + curl -SL --progress -H "Authorization: Token $token" "https://registry-1.docker.io/v1/images/$imageId/layer" -o "$dir/$imageId/layer.tar" # -C - + done + echo +done + +echo -n '{' > "$dir/repositories" +firstImage=1 +for image in "${images[@]}"; do + imageFile="${image//\//_}" # "/" can't be in filenames :) + + [ "$firstImage" ] || echo -n ',' >> "$dir/repositories" + firstImage= + echo -n $'\n\t' >> "$dir/repositories" + echo -n '"'"$image"'": { '"$(cat "$dir/tags-$imageFile.tmp")"' }' >> "$dir/repositories" +done +echo -n $'\n}\n' >> "$dir/repositories" + +rm -f "$dir"/tags-*.tmp + +echo "Download of images into '$dir' complete." +echo "Use something like the following to load the result into a Docker daemon:" +echo " tar -cC '$dir' . | docker load" diff --git a/contrib/download-frozen-image-v2.sh b/contrib/download-frozen-image-v2.sh new file mode 100755 index 00000000..111e3fa2 --- /dev/null +++ b/contrib/download-frozen-image-v2.sh @@ -0,0 +1,121 @@ +#!/bin/bash +set -e + +# hello-world latest ef872312fe1b 3 months ago 910 B +# hello-world latest ef872312fe1bbc5e05aae626791a47ee9b032efa8f3bda39cc0be7b56bfe59b9 3 months ago 910 B + +# debian latest f6fab3b798be 10 weeks ago 85.1 MB +# debian latest f6fab3b798be3174f45aa1eb731f8182705555f89c9026d8c1ef230cbf8301dd 10 weeks ago 85.1 MB + +if ! command -v curl &> /dev/null; then + echo >&2 'error: "curl" not found!' + exit 1 +fi + +usage() { + echo "usage: $0 dir image[:tag][@digest] ..." + echo " $0 /tmp/old-hello-world hello-world:latest@sha256:8be990ef2aeb16dbcb9271ddfe2610fa6658d13f6dfb8bc72074cc1ca36966a7" + [ -z "$1" ] || exit "$1" +} + +dir="$1" # dir for building tar in +shift || usage 1 >&2 + +[ $# -gt 0 -a "$dir" ] || usage 2 >&2 +mkdir -p "$dir" + +# hacky workarounds for Bash 3 support (no associative arrays) +images=() +rm -f "$dir"/tags-*.tmp +# repositories[busybox]='"latest": "...", "ubuntu-14.04": "..."' + +while [ $# -gt 0 ]; do + imageTag="$1" + shift + image="${imageTag%%[:@]*}" + imageTag="${imageTag#*:}" + digest="${imageTag##*@}" + tag="${imageTag%%@*}" + + # add prefix library if passed official image + if [[ "$image" != *"/"* ]]; then + image="library/$image" + fi + + imageFile="${image//\//_}" # "/" can't be in filenames :) + + token="$(curl -sSL "https://auth.docker.io/token?service=registry.docker.io&scope=repository:$image:pull" | jq --raw-output .token)" + + manifestJson="$(curl -sSL -H "Authorization: Bearer $token" "https://registry-1.docker.io/v2/$image/manifests/$digest")" + if [ "${manifestJson:0:1}" != '{' ]; then + echo >&2 "error: /v2/$image/manifests/$digest returned something unexpected:" + echo >&2 " $manifestJson" + exit 1 + fi + + layersFs=$(echo "$manifestJson" | jq --raw-output '.fsLayers | .[] | .blobSum') + + IFS=$'\n' + # bash v4 on Windows CI requires CRLF separator + if [ "$(go env GOHOSTOS)" = 'windows' ]; then + major=$(echo ${BASH_VERSION%%[^0.9]} | cut -d. -f1) + if [ "$major" -ge 4 ]; then + IFS=$'\r\n' + fi + fi + layers=( ${layersFs} ) + unset IFS + + history=$(echo "$manifestJson" | jq '.history | [.[] | .v1Compatibility]') + imageId=$(echo "$history" | jq --raw-output .[0] | jq --raw-output .id) + + if [ -s "$dir/tags-$imageFile.tmp" ]; then + echo -n ', ' >> "$dir/tags-$imageFile.tmp" + else + images=( "${images[@]}" "$image" ) + fi + echo -n '"'"$tag"'": "'"$imageId"'"' >> "$dir/tags-$imageFile.tmp" + + echo "Downloading '${image}:${tag}@${digest}' (${#layers[@]} layers)..." + for i in "${!layers[@]}"; do + imageJson=$(echo "$history" | jq --raw-output .[${i}]) + imageId=$(echo "$imageJson" | jq --raw-output .id) + imageLayer=${layers[$i]} + + mkdir -p "$dir/$imageId" + echo '1.0' > "$dir/$imageId/VERSION" + + echo "$imageJson" > "$dir/$imageId/json" + + # TODO figure out why "-C -" doesn't work here + # "curl: (33) HTTP server doesn't seem to support byte ranges. Cannot resume." + # "HTTP/1.1 416 Requested Range Not Satisfiable" + if [ -f "$dir/$imageId/layer.tar" ]; then + # TODO hackpatch for no -C support :'( + echo "skipping existing ${imageId:0:12}" + continue + fi + token="$(curl -sSL "https://auth.docker.io/token?service=registry.docker.io&scope=repository:$image:pull" | jq --raw-output .token)" + curl -SL --progress -H "Authorization: Bearer $token" "https://registry-1.docker.io/v2/$image/blobs/$imageLayer" -o "$dir/$imageId/layer.tar" # -C - + done + echo +done + +echo -n '{' > "$dir/repositories" +firstImage=1 +for image in "${images[@]}"; do + imageFile="${image//\//_}" # "/" can't be in filenames :) + image="${image#library\/}" + + [ "$firstImage" ] || echo -n ',' >> "$dir/repositories" + firstImage= + echo -n $'\n\t' >> "$dir/repositories" + echo -n '"'"$image"'": { '"$(cat "$dir/tags-$imageFile.tmp")"' }' >> "$dir/repositories" +done +echo -n $'\n}\n' >> "$dir/repositories" + +rm -f "$dir"/tags-*.tmp + +echo "Download of images into '$dir' complete." +echo "Use something like the following to load the result into a Docker daemon:" +echo " tar -cC '$dir' . | docker load" diff --git a/contrib/httpserver/Dockerfile b/contrib/httpserver/Dockerfile new file mode 100644 index 00000000..747dc91b --- /dev/null +++ b/contrib/httpserver/Dockerfile @@ -0,0 +1,4 @@ +FROM busybox +EXPOSE 80/tcp +COPY httpserver . +CMD ["./httpserver"] diff --git a/contrib/httpserver/server.go b/contrib/httpserver/server.go new file mode 100644 index 00000000..a75d5abb --- /dev/null +++ b/contrib/httpserver/server.go @@ -0,0 +1,12 @@ +package main + +import ( + "log" + "net/http" +) + +func main() { + fs := http.FileServer(http.Dir("/static")) + http.Handle("/", fs) + log.Panic(http.ListenAndServe(":80", nil)) +} diff --git a/contrib/init/openrc/docker.confd b/contrib/init/openrc/docker.confd new file mode 100644 index 00000000..ae247c00 --- /dev/null +++ b/contrib/init/openrc/docker.confd @@ -0,0 +1,13 @@ +# /etc/conf.d/docker: config file for /etc/init.d/docker + +# where the docker daemon output gets piped +#DOCKER_LOGFILE="/var/log/docker.log" + +# where docker's pid get stored +#DOCKER_PIDFILE="/run/docker.pid" + +# where the docker daemon itself is run from +#DOCKER_BINARY="/usr/bin/docker" + +# any other random options you want to pass to docker +DOCKER_OPTS="" diff --git a/contrib/init/openrc/docker.initd b/contrib/init/openrc/docker.initd new file mode 100644 index 00000000..ea8a3b22 --- /dev/null +++ b/contrib/init/openrc/docker.initd @@ -0,0 +1,19 @@ +#!/sbin/openrc-run +# Copyright 1999-2013 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +command="${DOCKER_BINARY:-/usr/bin/docker}" +pidfile="${DOCKER_PIDFILE:-/run/${RC_SVCNAME}.pid}" +command_args="daemon -p \"${pidfile}\" ${DOCKER_OPTS}" +DOCKER_LOGFILE="${DOCKER_LOGFILE:-/var/log/${RC_SVCNAME}.log}" +start_stop_daemon_args="--background \ + --stderr \"${DOCKER_LOGFILE}\" --stdout \"${DOCKER_LOGFILE}\"" + +start_pre() { + checkpath -f -m 0644 -o root:docker "$DOCKER_LOGFILE" + + ulimit -n 1048576 + ulimit -u 1048576 + + return 0 +} diff --git a/contrib/init/systemd/REVIEWERS b/contrib/init/systemd/REVIEWERS new file mode 100644 index 00000000..b9ba55b3 --- /dev/null +++ b/contrib/init/systemd/REVIEWERS @@ -0,0 +1,3 @@ +Lokesh Mandvekar (@lsm5) +Brandon Philips (@philips) +Jessie Frazelle (@jfrazelle) diff --git a/contrib/init/systemd/docker.service b/contrib/init/systemd/docker.service new file mode 100644 index 00000000..75cb68c8 --- /dev/null +++ b/contrib/init/systemd/docker.service @@ -0,0 +1,22 @@ +[Unit] +Description=Docker Application Container Engine +Documentation=https://docs.docker.com +After=network.target docker.socket +Requires=docker.socket + +[Service] +Type=notify +# the default is not to use systemd for cgroups because the delegate issues still +# exists and systemd currently does not support the cgroup feature set required +# for containers run by docker +ExecStart=/usr/bin/docker daemon -H fd:// +MountFlags=slave +LimitNOFILE=1048576 +LimitNPROC=1048576 +LimitCORE=infinity +TimeoutStartSec=0 +# set delegate yes so that systemd does not reset the cgroups of docker containers +Delegate=yes + +[Install] +WantedBy=multi-user.target diff --git a/contrib/init/systemd/docker.socket b/contrib/init/systemd/docker.socket new file mode 100644 index 00000000..7dd95098 --- /dev/null +++ b/contrib/init/systemd/docker.socket @@ -0,0 +1,12 @@ +[Unit] +Description=Docker Socket for the API +PartOf=docker.service + +[Socket] +ListenStream=/var/run/docker.sock +SocketMode=0660 +SocketUser=root +SocketGroup=docker + +[Install] +WantedBy=sockets.target diff --git a/contrib/init/sysvinit-debian/docker b/contrib/init/sysvinit-debian/docker new file mode 100755 index 00000000..fc4b05b3 --- /dev/null +++ b/contrib/init/sysvinit-debian/docker @@ -0,0 +1,149 @@ +#!/bin/sh +set -e + +### BEGIN INIT INFO +# Provides: docker +# Required-Start: $syslog $remote_fs +# Required-Stop: $syslog $remote_fs +# Should-Start: cgroupfs-mount cgroup-lite +# Should-Stop: cgroupfs-mount cgroup-lite +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Create lightweight, portable, self-sufficient containers. +# Description: +# Docker is an open-source project to easily create lightweight, portable, +# self-sufficient containers from any application. The same container that a +# developer builds and tests on a laptop can run at scale, in production, on +# VMs, bare metal, OpenStack clusters, public clouds and more. +### END INIT INFO + +export PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin + +BASE=docker + +# modify these in /etc/default/$BASE (/etc/default/docker) +DOCKER=/usr/bin/$BASE +# This is the pid file managed by docker itself +DOCKER_PIDFILE=/var/run/$BASE.pid +# This is the pid file created/managed by start-stop-daemon +DOCKER_SSD_PIDFILE=/var/run/$BASE-ssd.pid +DOCKER_LOGFILE=/var/log/$BASE.log +DOCKER_OPTS= +DOCKER_DESC="Docker" + +# Get lsb functions +. /lib/lsb/init-functions + +if [ -f /etc/default/$BASE ]; then + . /etc/default/$BASE +fi + +# Check docker is present +if [ ! -x $DOCKER ]; then + log_failure_msg "$DOCKER not present or not executable" + exit 1 +fi + +check_init() { + # see also init_is_upstart in /lib/lsb/init-functions (which isn't available in Ubuntu 12.04, or we'd use it directly) + if [ -x /sbin/initctl ] && /sbin/initctl version 2>/dev/null | grep -q upstart; then + log_failure_msg "$DOCKER_DESC is managed via upstart, try using service $BASE $1" + exit 1 + fi +} + +fail_unless_root() { + if [ "$(id -u)" != '0' ]; then + log_failure_msg "$DOCKER_DESC must be run as root" + exit 1 + fi +} + +cgroupfs_mount() { + # see also https://github.com/tianon/cgroupfs-mount/blob/master/cgroupfs-mount + if grep -v '^#' /etc/fstab | grep -q cgroup \ + || [ ! -e /proc/cgroups ] \ + || [ ! -d /sys/fs/cgroup ]; then + return + fi + if ! mountpoint -q /sys/fs/cgroup; then + mount -t tmpfs -o uid=0,gid=0,mode=0755 cgroup /sys/fs/cgroup + fi + ( + cd /sys/fs/cgroup + for sys in $(awk '!/^#/ { if ($4 == 1) print $1 }' /proc/cgroups); do + mkdir -p $sys + if ! mountpoint -q $sys; then + if ! mount -n -t cgroup -o $sys cgroup $sys; then + rmdir $sys || true + fi + fi + done + ) +} + +case "$1" in + start) + check_init + + fail_unless_root + + cgroupfs_mount + + touch "$DOCKER_LOGFILE" + chgrp docker "$DOCKER_LOGFILE" + + ulimit -n 1048576 + if [ "$BASH" ]; then + ulimit -u 1048576 + else + ulimit -p 1048576 + fi + + log_begin_msg "Starting $DOCKER_DESC: $BASE" + start-stop-daemon --start --background \ + --no-close \ + --exec "$DOCKER" \ + --pidfile "$DOCKER_SSD_PIDFILE" \ + --make-pidfile \ + -- \ + daemon -p "$DOCKER_PIDFILE" \ + $DOCKER_OPTS \ + >> "$DOCKER_LOGFILE" 2>&1 + log_end_msg $? + ;; + + stop) + check_init + fail_unless_root + log_begin_msg "Stopping $DOCKER_DESC: $BASE" + start-stop-daemon --stop --pidfile "$DOCKER_SSD_PIDFILE" --retry 10 + log_end_msg $? + ;; + + restart) + check_init + fail_unless_root + docker_pid=`cat "$DOCKER_SSD_PIDFILE" 2>/dev/null` + [ -n "$docker_pid" ] \ + && ps -p $docker_pid > /dev/null 2>&1 \ + && $0 stop + $0 start + ;; + + force-reload) + check_init + fail_unless_root + $0 restart + ;; + + status) + check_init + status_of_proc -p "$DOCKER_SSD_PIDFILE" "$DOCKER" "$DOCKER_DESC" + ;; + + *) + echo "Usage: service docker {start|stop|restart|status}" + exit 1 + ;; +esac diff --git a/contrib/init/sysvinit-debian/docker.default b/contrib/init/sysvinit-debian/docker.default new file mode 100644 index 00000000..da23c57c --- /dev/null +++ b/contrib/init/sysvinit-debian/docker.default @@ -0,0 +1,20 @@ +# Docker Upstart and SysVinit configuration file + +# +# THIS FILE DOES NOT APPLY TO SYSTEMD +# +# Please see the documentation for "systemd drop-ins": +# https://docs.docker.com/engine/articles/systemd/ +# + +# Customize location of Docker binary (especially for development testing). +#DOCKER="/usr/local/bin/docker" + +# Use DOCKER_OPTS to modify the daemon startup options. +#DOCKER_OPTS="--dns 8.8.8.8 --dns 8.8.4.4" + +# If you need Docker to use an HTTP proxy, it can also be specified here. +#export http_proxy="http://127.0.0.1:3128/" + +# This is also a handy place to tweak where Docker's temporary files go. +#export TMPDIR="/mnt/bigdrive/docker-tmp" diff --git a/contrib/init/sysvinit-redhat/docker b/contrib/init/sysvinit-redhat/docker new file mode 100755 index 00000000..3f88bb52 --- /dev/null +++ b/contrib/init/sysvinit-redhat/docker @@ -0,0 +1,153 @@ +#!/bin/sh +# +# /etc/rc.d/init.d/docker +# +# Daemon for docker.com +# +# chkconfig: 2345 95 95 +# description: Daemon for docker.com + +### BEGIN INIT INFO +# Provides: docker +# Required-Start: $network cgconfig +# Required-Stop: +# Should-Start: +# Should-Stop: +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: start and stop docker +# Description: Daemon for docker.com +### END INIT INFO + +# Source function library. +. /etc/rc.d/init.d/functions + +prog="docker" +unshare=/usr/bin/unshare +exec="/usr/bin/$prog" +pidfile="/var/run/$prog.pid" +lockfile="/var/lock/subsys/$prog" +logfile="/var/log/$prog" + +[ -e /etc/sysconfig/$prog ] && . /etc/sysconfig/$prog + +prestart() { + service cgconfig status > /dev/null + + if [[ $? != 0 ]]; then + service cgconfig start + fi + +} + +start() { + if [ ! -x $exec ]; then + if [ ! -e $exec ]; then + echo "Docker executable $exec not found" + else + echo "You do not have permission to execute the Docker executable $exec" + fi + exit 5 + fi + + check_for_cleanup + + if ! [ -f $pidfile ]; then + prestart + printf "Starting $prog:\t" + echo "\n$(date)\n" >> $logfile + "$unshare" -m -- $exec daemon $other_args >> $logfile 2>&1 & + pid=$! + touch $lockfile + # wait up to 10 seconds for the pidfile to exist. see + # https://github.com/docker/docker/issues/5359 + tries=0 + while [ ! -f $pidfile -a $tries -lt 10 ]; do + sleep 1 + tries=$((tries + 1)) + echo -n '.' + done + if [ ! -f $pidfile ]; then + failure + echo + exit 1 + fi + success + echo + else + failure + echo + printf "$pidfile still exists...\n" + exit 7 + fi +} + +stop() { + echo -n $"Stopping $prog: " + killproc -p $pidfile -d 300 $prog + retval=$? + echo + [ $retval -eq 0 ] && rm -f $lockfile + return $retval +} + +restart() { + stop + start +} + +reload() { + restart +} + +force_reload() { + restart +} + +rh_status() { + status -p $pidfile $prog +} + +rh_status_q() { + rh_status >/dev/null 2>&1 +} + + +check_for_cleanup() { + if [ -f ${pidfile} ]; then + /bin/ps -fp $(cat ${pidfile}) > /dev/null || rm ${pidfile} + fi +} + +case "$1" in + start) + rh_status_q && exit 0 + $1 + ;; + stop) + rh_status_q || exit 0 + $1 + ;; + restart) + $1 + ;; + reload) + rh_status_q || exit 7 + $1 + ;; + force-reload) + force_reload + ;; + status) + rh_status + ;; + condrestart|try-restart) + rh_status_q || exit 0 + restart + ;; + *) + echo $"Usage: $0 {start|stop|status|restart|condrestart|try-restart|reload|force-reload}" + exit 2 +esac + +exit $? diff --git a/contrib/init/sysvinit-redhat/docker.sysconfig b/contrib/init/sysvinit-redhat/docker.sysconfig new file mode 100644 index 00000000..0864b3d7 --- /dev/null +++ b/contrib/init/sysvinit-redhat/docker.sysconfig @@ -0,0 +1,7 @@ +# /etc/sysconfig/docker +# +# Other arguments to pass to the docker daemon process +# These will be parsed by the sysv initscript and appended +# to the arguments list passed to docker daemon + +other_args="" diff --git a/contrib/init/upstart/REVIEWERS b/contrib/init/upstart/REVIEWERS new file mode 100644 index 00000000..03ee2dde --- /dev/null +++ b/contrib/init/upstart/REVIEWERS @@ -0,0 +1,2 @@ +Tianon Gravi (@tianon) +Jessie Frazelle (@jfrazelle) diff --git a/contrib/init/upstart/docker.conf b/contrib/init/upstart/docker.conf new file mode 100644 index 00000000..6cf02c56 --- /dev/null +++ b/contrib/init/upstart/docker.conf @@ -0,0 +1,68 @@ +description "Docker daemon" + +start on (filesystem and net-device-up IFACE!=lo) +stop on runlevel [!2345] +limit nofile 524288 1048576 +limit nproc 524288 1048576 + +respawn + +kill timeout 20 + +pre-start script + # see also https://github.com/tianon/cgroupfs-mount/blob/master/cgroupfs-mount + if grep -v '^#' /etc/fstab | grep -q cgroup \ + || [ ! -e /proc/cgroups ] \ + || [ ! -d /sys/fs/cgroup ]; then + exit 0 + fi + if ! mountpoint -q /sys/fs/cgroup; then + mount -t tmpfs -o uid=0,gid=0,mode=0755 cgroup /sys/fs/cgroup + fi + ( + cd /sys/fs/cgroup + for sys in $(awk '!/^#/ { if ($4 == 1) print $1 }' /proc/cgroups); do + mkdir -p $sys + if ! mountpoint -q $sys; then + if ! mount -n -t cgroup -o $sys cgroup $sys; then + rmdir $sys || true + fi + fi + done + ) +end script + +script + # modify these in /etc/default/$UPSTART_JOB (/etc/default/docker) + DOCKER=/usr/bin/$UPSTART_JOB + DOCKER_OPTS= + if [ -f /etc/default/$UPSTART_JOB ]; then + . /etc/default/$UPSTART_JOB + fi + exec "$DOCKER" daemon $DOCKER_OPTS --raw-logs +end script + +# Don't emit "started" event until docker.sock is ready. +# See https://github.com/docker/docker/issues/6647 +post-start script + DOCKER_OPTS= + DOCKER_SOCKET= + if [ -f /etc/default/$UPSTART_JOB ]; then + . /etc/default/$UPSTART_JOB + fi + + if ! printf "%s" "$DOCKER_OPTS" | grep -qE -e '-H|--host'; then + DOCKER_SOCKET=/var/run/docker.sock + else + DOCKER_SOCKET=$(printf "%s" "$DOCKER_OPTS" | grep -oP -e '(-H|--host)\W*unix://\K(\S+)') + fi + + if [ -n "$DOCKER_SOCKET" ]; then + while ! [ -e "$DOCKER_SOCKET" ]; do + initctl status $UPSTART_JOB | grep -qE "(stop|respawn)/" && exit 1 + echo "Waiting for $DOCKER_SOCKET" + sleep 0.1 + done + echo "$DOCKER_SOCKET is up" + fi +end script diff --git a/contrib/mkimage-alpine.sh b/contrib/mkimage-alpine.sh new file mode 100755 index 00000000..47cd35ce --- /dev/null +++ b/contrib/mkimage-alpine.sh @@ -0,0 +1,87 @@ +#!/bin/sh + +set -e + +[ $(id -u) -eq 0 ] || { + printf >&2 '%s requires root\n' "$0" + exit 1 +} + +usage() { + printf >&2 '%s: [-r release] [-m mirror] [-s] [-c additional repository]\n' "$0" + exit 1 +} + +tmp() { + TMP=$(mktemp -d ${TMPDIR:-/var/tmp}/alpine-docker-XXXXXXXXXX) + ROOTFS=$(mktemp -d ${TMPDIR:-/var/tmp}/alpine-docker-rootfs-XXXXXXXXXX) + trap "rm -rf $TMP $ROOTFS" EXIT TERM INT +} + +apkv() { + curl -sSL $MAINREPO/$ARCH/APKINDEX.tar.gz | tar -Oxz | + grep --text '^P:apk-tools-static$' -A1 | tail -n1 | cut -d: -f2 +} + +getapk() { + curl -sSL $MAINREPO/$ARCH/apk-tools-static-$(apkv).apk | + tar -xz -C $TMP sbin/apk.static +} + +mkbase() { + $TMP/sbin/apk.static --repository $MAINREPO --update-cache --allow-untrusted \ + --root $ROOTFS --initdb add alpine-base +} + +conf() { + printf '%s\n' $MAINREPO > $ROOTFS/etc/apk/repositories + printf '%s\n' $ADDITIONALREPO >> $ROOTFS/etc/apk/repositories +} + +pack() { + local id + id=$(tar --numeric-owner -C $ROOTFS -c . | docker import - alpine:$REL) + + docker tag $id alpine:latest + docker run -i -t --rm alpine printf 'alpine:%s with id=%s created!\n' $REL $id +} + +save() { + [ $SAVE -eq 1 ] || return + + tar --numeric-owner -C $ROOTFS -c . | xz > rootfs.tar.xz +} + +while getopts "hr:m:s" opt; do + case $opt in + r) + REL=$OPTARG + ;; + m) + MIRROR=$OPTARG + ;; + s) + SAVE=1 + ;; + c) + ADDITIONALREPO=community + ;; + *) + usage + ;; + esac +done + +REL=${REL:-edge} +MIRROR=${MIRROR:-http://nl.alpinelinux.org/alpine} +SAVE=${SAVE:-0} +MAINREPO=$MIRROR/$REL/main +ADDITIONALREPO=$MIRROR/$REL/community +ARCH=${ARCH:-$(uname -m)} + +tmp +getapk +mkbase +conf +pack +save diff --git a/contrib/mkimage-arch-pacman.conf b/contrib/mkimage-arch-pacman.conf new file mode 100644 index 00000000..45fe03dc --- /dev/null +++ b/contrib/mkimage-arch-pacman.conf @@ -0,0 +1,92 @@ +# +# /etc/pacman.conf +# +# See the pacman.conf(5) manpage for option and repository directives + +# +# GENERAL OPTIONS +# +[options] +# The following paths are commented out with their default values listed. +# If you wish to use different paths, uncomment and update the paths. +#RootDir = / +#DBPath = /var/lib/pacman/ +#CacheDir = /var/cache/pacman/pkg/ +#LogFile = /var/log/pacman.log +#GPGDir = /etc/pacman.d/gnupg/ +HoldPkg = pacman glibc +#XferCommand = /usr/bin/curl -C - -f %u > %o +#XferCommand = /usr/bin/wget --passive-ftp -c -O %o %u +#CleanMethod = KeepInstalled +#UseDelta = 0.7 +Architecture = auto + +# Pacman won't upgrade packages listed in IgnorePkg and members of IgnoreGroup +#IgnorePkg = +#IgnoreGroup = + +#NoUpgrade = +#NoExtract = + +# Misc options +#UseSyslog +#Color +#TotalDownload +# We cannot check disk space from within a chroot environment +#CheckSpace +#VerbosePkgLists + +# By default, pacman accepts packages signed by keys that its local keyring +# trusts (see pacman-key and its man page), as well as unsigned packages. +SigLevel = Required DatabaseOptional +LocalFileSigLevel = Optional +#RemoteFileSigLevel = Required + +# NOTE: You must run `pacman-key --init` before first using pacman; the local +# keyring can then be populated with the keys of all official Arch Linux +# packagers with `pacman-key --populate archlinux`. + +# +# REPOSITORIES +# - can be defined here or included from another file +# - pacman will search repositories in the order defined here +# - local/custom mirrors can be added here or in separate files +# - repositories listed first will take precedence when packages +# have identical names, regardless of version number +# - URLs will have $repo replaced by the name of the current repo +# - URLs will have $arch replaced by the name of the architecture +# +# Repository entries are of the format: +# [repo-name] +# Server = ServerName +# Include = IncludePath +# +# The header [repo-name] is crucial - it must be present and +# uncommented to enable the repo. +# + +# The testing repositories are disabled by default. To enable, uncomment the +# repo name header and Include lines. You can add preferred servers immediately +# after the header, and they will be used before the default mirrors. + +#[testing] +#Include = /etc/pacman.d/mirrorlist + +[core] +Include = /etc/pacman.d/mirrorlist + +[extra] +Include = /etc/pacman.d/mirrorlist + +#[community-testing] +#Include = /etc/pacman.d/mirrorlist + +[community] +Include = /etc/pacman.d/mirrorlist + +# An example of a custom package repository. See the pacman manpage for +# tips on creating your own repositories. +#[custom] +#SigLevel = Optional TrustAll +#Server = file:///home/custompkgs + diff --git a/contrib/mkimage-arch.sh b/contrib/mkimage-arch.sh new file mode 100755 index 00000000..793b21e3 --- /dev/null +++ b/contrib/mkimage-arch.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# Generate a minimal filesystem for archlinux and load it into the local +# docker as "archlinux" +# requires root +set -e + +hash pacstrap &>/dev/null || { + echo "Could not find pacstrap. Run pacman -S arch-install-scripts" + exit 1 +} + +hash expect &>/dev/null || { + echo "Could not find expect. Run pacman -S expect" + exit 1 +} + + +export LANG="C.UTF-8" + +ROOTFS=$(mktemp -d ${TMPDIR:-/var/tmp}/rootfs-archlinux-XXXXXXXXXX) +chmod 755 $ROOTFS + +# packages to ignore for space savings +PKGIGNORE=( + cryptsetup + device-mapper + dhcpcd + iproute2 + jfsutils + linux + lvm2 + man-db + man-pages + mdadm + nano + netctl + openresolv + pciutils + pcmciautils + reiserfsprogs + s-nail + systemd-sysvcompat + usbutils + vi + xfsprogs +) +IFS=',' +PKGIGNORE="${PKGIGNORE[*]}" +unset IFS + +case "$(uname -m)" in + armv*) + if pacman -Q archlinuxarm-keyring >/dev/null 2>&1; then + pacman-key --init + pacman-key --populate archlinuxarm + else + echo "Could not find archlinuxarm-keyring. Please, install it and run pacman-key --populate archlinuxarm" + exit 1 + fi + PACMAN_CONF='./mkimage-archarm-pacman.conf' + PACMAN_MIRRORLIST='Server = http://mirror.archlinuxarm.org/$arch/$repo' + PACMAN_EXTRA_PKGS='archlinuxarm-keyring' + EXPECT_TIMEOUT=120 + ARCH_KEYRING=archlinuxarm + DOCKER_IMAGE_NAME=archlinuxarm + ;; + *) + PACMAN_CONF='./mkimage-arch-pacman.conf' + PACMAN_MIRRORLIST='Server = https://mirrors.kernel.org/archlinux/$repo/os/$arch' + PACMAN_EXTRA_PKGS='' + EXPECT_TIMEOUT=60 + ARCH_KEYRING=archlinux + DOCKER_IMAGE_NAME=archlinux + ;; +esac + +export PACMAN_MIRRORLIST + +expect < $ROOTFS/etc/locale.gen +arch-chroot $ROOTFS locale-gen +arch-chroot $ROOTFS /bin/sh -c 'echo $PACMAN_MIRRORLIST > /etc/pacman.d/mirrorlist' + +# udev doesn't work in containers, rebuild /dev +DEV=$ROOTFS/dev +rm -rf $DEV +mkdir -p $DEV +mknod -m 666 $DEV/null c 1 3 +mknod -m 666 $DEV/zero c 1 5 +mknod -m 666 $DEV/random c 1 8 +mknod -m 666 $DEV/urandom c 1 9 +mkdir -m 755 $DEV/pts +mkdir -m 1777 $DEV/shm +mknod -m 666 $DEV/tty c 5 0 +mknod -m 600 $DEV/console c 5 1 +mknod -m 666 $DEV/tty0 c 4 0 +mknod -m 666 $DEV/full c 1 7 +mknod -m 600 $DEV/initctl p +mknod -m 666 $DEV/ptmx c 5 2 +ln -sf /proc/self/fd $DEV/fd + +tar --numeric-owner --xattrs --acls -C $ROOTFS -c . | docker import - $DOCKER_IMAGE_NAME +docker run --rm -t $DOCKER_IMAGE_NAME echo Success. +rm -rf $ROOTFS diff --git a/contrib/mkimage-archarm-pacman.conf b/contrib/mkimage-archarm-pacman.conf new file mode 100644 index 00000000..38b01bf4 --- /dev/null +++ b/contrib/mkimage-archarm-pacman.conf @@ -0,0 +1,98 @@ +# +# /etc/pacman.conf +# +# See the pacman.conf(5) manpage for option and repository directives + +# +# GENERAL OPTIONS +# +[options] +# The following paths are commented out with their default values listed. +# If you wish to use different paths, uncomment and update the paths. +#RootDir = / +#DBPath = /var/lib/pacman/ +#CacheDir = /var/cache/pacman/pkg/ +#LogFile = /var/log/pacman.log +#GPGDir = /etc/pacman.d/gnupg/ +HoldPkg = pacman glibc +#XferCommand = /usr/bin/curl -C - -f %u > %o +#XferCommand = /usr/bin/wget --passive-ftp -c -O %o %u +#CleanMethod = KeepInstalled +#UseDelta = 0.7 +Architecture = armv7h + +# Pacman won't upgrade packages listed in IgnorePkg and members of IgnoreGroup +#IgnorePkg = +#IgnoreGroup = + +#NoUpgrade = +#NoExtract = + +# Misc options +#UseSyslog +#Color +#TotalDownload +# We cannot check disk space from within a chroot environment +#CheckSpace +#VerbosePkgLists + +# By default, pacman accepts packages signed by keys that its local keyring +# trusts (see pacman-key and its man page), as well as unsigned packages. +SigLevel = Required DatabaseOptional +LocalFileSigLevel = Optional +#RemoteFileSigLevel = Required + +# NOTE: You must run `pacman-key --init` before first using pacman; the local +# keyring can then be populated with the keys of all official Arch Linux +# packagers with `pacman-key --populate archlinux`. + +# +# REPOSITORIES +# - can be defined here or included from another file +# - pacman will search repositories in the order defined here +# - local/custom mirrors can be added here or in separate files +# - repositories listed first will take precedence when packages +# have identical names, regardless of version number +# - URLs will have $repo replaced by the name of the current repo +# - URLs will have $arch replaced by the name of the architecture +# +# Repository entries are of the format: +# [repo-name] +# Server = ServerName +# Include = IncludePath +# +# The header [repo-name] is crucial - it must be present and +# uncommented to enable the repo. +# + +# The testing repositories are disabled by default. To enable, uncomment the +# repo name header and Include lines. You can add preferred servers immediately +# after the header, and they will be used before the default mirrors. + +#[testing] +#Include = /etc/pacman.d/mirrorlist + +[core] +Include = /etc/pacman.d/mirrorlist + +[extra] +Include = /etc/pacman.d/mirrorlist + +#[community-testing] +#Include = /etc/pacman.d/mirrorlist + +[community] +Include = /etc/pacman.d/mirrorlist + +[alarm] +Include = /etc/pacman.d/mirrorlist + +[aur] +Include = /etc/pacman.d/mirrorlist + +# An example of a custom package repository. See the pacman manpage for +# tips on creating your own repositories. +#[custom] +#SigLevel = Optional TrustAll +#Server = file:///home/custompkgs + diff --git a/contrib/mkimage-busybox.sh b/contrib/mkimage-busybox.sh new file mode 100755 index 00000000..b11a6bb2 --- /dev/null +++ b/contrib/mkimage-busybox.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Generate a very minimal filesystem based on busybox-static, +# and load it into the local docker under the name "busybox". + +echo >&2 +echo >&2 'warning: this script is deprecated - see mkimage.sh and mkimage/busybox-static' +echo >&2 + +BUSYBOX=$(which busybox) +[ "$BUSYBOX" ] || { + echo "Sorry, I could not locate busybox." + echo "Try 'apt-get install busybox-static'?" + exit 1 +} + +set -e +ROOTFS=${TMPDIR:-/var/tmp}/rootfs-busybox-$$-$RANDOM +mkdir $ROOTFS +cd $ROOTFS + +mkdir bin etc dev dev/pts lib proc sys tmp +touch etc/resolv.conf +cp /etc/nsswitch.conf etc/nsswitch.conf +echo root:x:0:0:root:/:/bin/sh > etc/passwd +echo root:x:0: > etc/group +ln -s lib lib64 +ln -s bin sbin +cp $BUSYBOX bin +for X in $(busybox --list) +do + ln -s busybox bin/$X +done +rm bin/init +ln bin/busybox bin/init +cp /lib/x86_64-linux-gnu/lib{pthread,c,dl,nsl,nss_*}.so.* lib +cp /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 lib +for X in console null ptmx random stdin stdout stderr tty urandom zero +do + cp -a /dev/$X dev +done + +tar --numeric-owner -cf- . | docker import - busybox +docker run -i -u root busybox /bin/echo Success. diff --git a/contrib/mkimage-crux.sh b/contrib/mkimage-crux.sh new file mode 100755 index 00000000..3f0bdcae --- /dev/null +++ b/contrib/mkimage-crux.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Generate a minimal filesystem for CRUX/Linux and load it into the local +# docker as "cruxlinux" +# requires root and the crux iso (http://crux.nu) + +set -e + +die () { + echo >&2 "$@" + exit 1 +} + +[ "$#" -eq 1 ] || die "1 argument(s) required, $# provided. Usage: ./mkimage-crux.sh /path/to/iso" + +ISO=${1} + +ROOTFS=$(mktemp -d ${TMPDIR:-/var/tmp}/rootfs-crux-XXXXXXXXXX) +CRUX=$(mktemp -d ${TMPDIR:-/var/tmp}/crux-XXXXXXXXXX) +TMP=$(mktemp -d ${TMPDIR:-/var/tmp}/XXXXXXXXXX) + +VERSION=$(basename --suffix=.iso $ISO | sed 's/[^0-9.]*\([0-9.]*\).*/\1/') + +# Mount the ISO +mount -o ro,loop $ISO $CRUX + +# Extract pkgutils +tar -C $TMP -xf $CRUX/tools/pkgutils#*.pkg.tar.gz + +# Put pkgadd in the $PATH +export PATH="$TMP/usr/bin:$PATH" + +# Install core packages +mkdir -p $ROOTFS/var/lib/pkg +touch $ROOTFS/var/lib/pkg/db +for pkg in $CRUX/crux/core/*; do + pkgadd -r $ROOTFS $pkg +done + +# Remove agetty and inittab config +if (grep agetty ${ROOTFS}/etc/inittab 2>&1 > /dev/null); then + echo "Removing agetty from /etc/inittab ..." + chroot ${ROOTFS} sed -i -e "/agetty/d" /etc/inittab + chroot ${ROOTFS} sed -i -e "/shutdown/d" /etc/inittab + chroot ${ROOTFS} sed -i -e "/^$/N;/^\n$/d" /etc/inittab +fi + +# Remove kernel source +rm -rf $ROOTFS/usr/src/* + +# udev doesn't work in containers, rebuild /dev +DEV=$ROOTFS/dev +rm -rf $DEV +mkdir -p $DEV +mknod -m 666 $DEV/null c 1 3 +mknod -m 666 $DEV/zero c 1 5 +mknod -m 666 $DEV/random c 1 8 +mknod -m 666 $DEV/urandom c 1 9 +mkdir -m 755 $DEV/pts +mkdir -m 1777 $DEV/shm +mknod -m 666 $DEV/tty c 5 0 +mknod -m 600 $DEV/console c 5 1 +mknod -m 666 $DEV/tty0 c 4 0 +mknod -m 666 $DEV/full c 1 7 +mknod -m 600 $DEV/initctl p +mknod -m 666 $DEV/ptmx c 5 2 + +IMAGE_ID=$(tar --numeric-owner -C $ROOTFS -c . | docker import - crux:$VERSION) +docker tag $IMAGE_ID crux:latest +docker run -i -t crux echo Success. + +# Cleanup +umount $CRUX +rm -rf $ROOTFS +rm -rf $CRUX +rm -rf $TMP diff --git a/contrib/mkimage-debootstrap.sh b/contrib/mkimage-debootstrap.sh new file mode 100755 index 00000000..412a5ce0 --- /dev/null +++ b/contrib/mkimage-debootstrap.sh @@ -0,0 +1,297 @@ +#!/usr/bin/env bash +set -e + +echo >&2 +echo >&2 'warning: this script is deprecated - see mkimage.sh and mkimage/debootstrap' +echo >&2 + +variant='minbase' +include='iproute,iputils-ping' +arch='amd64' # intentionally undocumented for now +skipDetection= +strictDebootstrap= +justTar= + +usage() { + echo >&2 + + echo >&2 "usage: $0 [options] repo suite [mirror]" + + echo >&2 + echo >&2 'options: (not recommended)' + echo >&2 " -p set an http_proxy for debootstrap" + echo >&2 " -v $variant # change default debootstrap variant" + echo >&2 " -i $include # change default package includes" + echo >&2 " -d # strict debootstrap (do not apply any docker-specific tweaks)" + echo >&2 " -s # skip version detection and tagging (ie, precise also tagged as 12.04)" + echo >&2 " # note that this will also skip adding universe and/or security/updates to sources.list" + echo >&2 " -t # just create a tarball, especially for dockerbrew (uses repo as tarball name)" + + echo >&2 + echo >&2 " ie: $0 username/debian squeeze" + echo >&2 " $0 username/debian squeeze http://ftp.uk.debian.org/debian/" + + echo >&2 + echo >&2 " ie: $0 username/ubuntu precise" + echo >&2 " $0 username/ubuntu precise http://mirrors.melbourne.co.uk/ubuntu/" + + echo >&2 + echo >&2 " ie: $0 -t precise.tar.bz2 precise" + echo >&2 " $0 -t wheezy.tgz wheezy" + echo >&2 " $0 -t wheezy-uk.tar.xz wheezy http://ftp.uk.debian.org/debian/" + + echo >&2 +} + +# these should match the names found at http://www.debian.org/releases/ +debianStable=wheezy +debianUnstable=sid +# this should match the name found at http://releases.ubuntu.com/ +ubuntuLatestLTS=trusty +# this should match the name found at http://releases.tanglu.org/ +tangluLatest=aequorea + +while getopts v:i:a:p:dst name; do + case "$name" in + p) + http_proxy="$OPTARG" + ;; + v) + variant="$OPTARG" + ;; + i) + include="$OPTARG" + ;; + a) + arch="$OPTARG" + ;; + d) + strictDebootstrap=1 + ;; + s) + skipDetection=1 + ;; + t) + justTar=1 + ;; + ?) + usage + exit 0 + ;; + esac +done +shift $(($OPTIND - 1)) + +repo="$1" +suite="$2" +mirror="${3:-}" # stick to the default debootstrap mirror if one is not provided + +if [ ! "$repo" ] || [ ! "$suite" ]; then + usage + exit 1 +fi + +# some rudimentary detection for whether we need to "sudo" our docker calls +docker='' +if docker version > /dev/null 2>&1; then + docker='docker' +elif sudo docker version > /dev/null 2>&1; then + docker='sudo docker' +elif command -v docker > /dev/null 2>&1; then + docker='docker' +else + echo >&2 "warning: either docker isn't installed, or your current user cannot run it;" + echo >&2 " this script is not likely to work as expected" + sleep 3 + docker='docker' # give us a command-not-found later +fi + +# make sure we have an absolute path to our final tarball so we can still reference it properly after we change directory +if [ "$justTar" ]; then + if [ ! -d "$(dirname "$repo")" ]; then + echo >&2 "error: $(dirname "$repo") does not exist" + exit 1 + fi + repo="$(cd "$(dirname "$repo")" && pwd -P)/$(basename "$repo")" +fi + +# will be filled in later, if [ -z "$skipDetection" ] +lsbDist='' + +target="${TMPDIR:-/var/tmp}/docker-rootfs-debootstrap-$suite-$$-$RANDOM" + +cd "$(dirname "$(readlink -f "$BASH_SOURCE")")" +returnTo="$(pwd -P)" + +if [ "$suite" = 'lucid' ]; then + # lucid fails and doesn't include gpgv in minbase; "apt-get update" fails + include+=',gpgv' +fi + +set -x + +# bootstrap +mkdir -p "$target" +sudo http_proxy=$http_proxy debootstrap --verbose --variant="$variant" --include="$include" --arch="$arch" "$suite" "$target" "$mirror" + +cd "$target" + +if [ -z "$strictDebootstrap" ]; then + # prevent init scripts from running during install/update + # policy-rc.d (for most scripts) + echo $'#!/bin/sh\nexit 101' | sudo tee usr/sbin/policy-rc.d > /dev/null + sudo chmod +x usr/sbin/policy-rc.d + # initctl (for some pesky upstart scripts) + sudo chroot . dpkg-divert --local --rename --add /sbin/initctl + sudo ln -sf /bin/true sbin/initctl + # see https://github.com/docker/docker/issues/446#issuecomment-16953173 + + # shrink the image, since apt makes us fat (wheezy: ~157.5MB vs ~120MB) + sudo chroot . apt-get clean + + if strings usr/bin/dpkg | grep -q unsafe-io; then + # while we're at it, apt is unnecessarily slow inside containers + # this forces dpkg not to call sync() after package extraction and speeds up install + # the benefit is huge on spinning disks, and the penalty is nonexistent on SSD or decent server virtualization + echo 'force-unsafe-io' | sudo tee etc/dpkg/dpkg.cfg.d/02apt-speedup > /dev/null + # we have this wrapped up in an "if" because the "force-unsafe-io" + # option was added in dpkg 1.15.8.6 + # (see http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=584254#82), + # and ubuntu lucid/10.04 only has 1.15.5.6 + fi + + # we want to effectively run "apt-get clean" after every install to keep images small (see output of "apt-get clean -s" for context) + { + aptGetClean='"rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true";' + echo "DPkg::Post-Invoke { ${aptGetClean} };" + echo "APT::Update::Post-Invoke { ${aptGetClean} };" + echo 'Dir::Cache::pkgcache ""; Dir::Cache::srcpkgcache "";' + } | sudo tee etc/apt/apt.conf.d/no-cache > /dev/null + + # and remove the translations, too + echo 'Acquire::Languages "none";' | sudo tee etc/apt/apt.conf.d/no-languages > /dev/null + + # helpful undo lines for each the above tweaks (for lack of a better home to keep track of them): + # rm /usr/sbin/policy-rc.d + # rm /sbin/initctl; dpkg-divert --rename --remove /sbin/initctl + # rm /etc/dpkg/dpkg.cfg.d/02apt-speedup + # rm /etc/apt/apt.conf.d/no-cache + # rm /etc/apt/apt.conf.d/no-languages + + if [ -z "$skipDetection" ]; then + # see also rudimentary platform detection in hack/install.sh + lsbDist='' + if [ -r etc/lsb-release ]; then + lsbDist="$(. etc/lsb-release && echo "$DISTRIB_ID")" + fi + if [ -z "$lsbDist" ] && [ -r etc/debian_version ]; then + lsbDist='Debian' + fi + + case "$lsbDist" in + Debian) + # add the updates and security repositories + if [ "$suite" != "$debianUnstable" -a "$suite" != 'unstable' ]; then + # ${suite}-updates only applies to non-unstable + sudo sed -i "p; s/ $suite main$/ ${suite}-updates main/" etc/apt/sources.list + + # same for security updates + echo "deb http://security.debian.org/ $suite/updates main" | sudo tee -a etc/apt/sources.list > /dev/null + fi + ;; + Ubuntu) + # add the universe, updates, and security repositories + sudo sed -i " + s/ $suite main$/ $suite main universe/; p; + s/ $suite main/ ${suite}-updates main/; p; + s/ $suite-updates main/ ${suite}-security main/ + " etc/apt/sources.list + ;; + Tanglu) + # add the updates repository + if [ "$suite" = "$tangluLatest" ]; then + # ${suite}-updates only applies to stable Tanglu versions + sudo sed -i "p; s/ $suite main$/ ${suite}-updates main/" etc/apt/sources.list + fi + ;; + SteamOS) + # add contrib and non-free + sudo sed -i "s/ $suite main$/ $suite main contrib non-free/" etc/apt/sources.list + ;; + esac + fi + + # make sure our packages lists are as up to date as we can get them + sudo chroot . apt-get update + sudo chroot . apt-get dist-upgrade -y +fi + +if [ "$justTar" ]; then + # create the tarball file so it has the right permissions (ie, not root) + touch "$repo" + + # fill the tarball + sudo tar --numeric-owner -caf "$repo" . +else + # create the image (and tag $repo:$suite) + sudo tar --numeric-owner -c . | $docker import - $repo:$suite + + # test the image + $docker run -i -t $repo:$suite echo success + + if [ -z "$skipDetection" ]; then + case "$lsbDist" in + Debian) + if [ "$suite" = "$debianStable" -o "$suite" = 'stable' ] && [ -r etc/debian_version ]; then + # tag latest + $docker tag $repo:$suite $repo:latest + + if [ -r etc/debian_version ]; then + # tag the specific debian release version (which is only reasonable to tag on debian stable) + ver=$(cat etc/debian_version) + $docker tag $repo:$suite $repo:$ver + fi + fi + ;; + Ubuntu) + if [ "$suite" = "$ubuntuLatestLTS" ]; then + # tag latest + $docker tag $repo:$suite $repo:latest + fi + if [ -r etc/lsb-release ]; then + lsbRelease="$(. etc/lsb-release && echo "$DISTRIB_RELEASE")" + if [ "$lsbRelease" ]; then + # tag specific Ubuntu version number, if available (12.04, etc.) + $docker tag $repo:$suite $repo:$lsbRelease + fi + fi + ;; + Tanglu) + if [ "$suite" = "$tangluLatest" ]; then + # tag latest + $docker tag $repo:$suite $repo:latest + fi + if [ -r etc/lsb-release ]; then + lsbRelease="$(. etc/lsb-release && echo "$DISTRIB_RELEASE")" + if [ "$lsbRelease" ]; then + # tag specific Tanglu version number, if available (1.0, 2.0, etc.) + $docker tag $repo:$suite $repo:$lsbRelease + fi + fi + ;; + SteamOS) + if [ -r etc/lsb-release ]; then + lsbRelease="$(. etc/lsb-release && echo "$DISTRIB_RELEASE")" + if [ "$lsbRelease" ]; then + # tag specific SteamOS version number, if available (1.0, 2.0, etc.) + $docker tag $repo:$suite $repo:$lsbRelease + fi + fi + ;; + esac + fi +fi + +# cleanup +cd "$returnTo" +sudo rm -rf "$target" diff --git a/contrib/mkimage-rinse.sh b/contrib/mkimage-rinse.sh new file mode 100755 index 00000000..7e093506 --- /dev/null +++ b/contrib/mkimage-rinse.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# +# Create a base CentOS Docker image. + +# This script is useful on systems with rinse available (e.g., +# building a CentOS image on Debian). See contrib/mkimage-yum.sh for +# a way to build CentOS images on systems with yum installed. + +set -e + +echo >&2 +echo >&2 'warning: this script is deprecated - see mkimage.sh and mkimage/rinse' +echo >&2 + +repo="$1" +distro="$2" +mirror="$3" + +if [ ! "$repo" ] || [ ! "$distro" ]; then + self="$(basename $0)" + echo >&2 "usage: $self repo distro [mirror]" + echo >&2 + echo >&2 " ie: $self username/centos centos-5" + echo >&2 " $self username/centos centos-6" + echo >&2 + echo >&2 " ie: $self username/slc slc-5" + echo >&2 " $self username/slc slc-6" + echo >&2 + echo >&2 " ie: $self username/centos centos-5 http://vault.centos.org/5.8/os/x86_64/CentOS/" + echo >&2 " $self username/centos centos-6 http://vault.centos.org/6.3/os/x86_64/Packages/" + echo >&2 + echo >&2 'See /etc/rinse for supported values of "distro" and for examples of' + echo >&2 ' expected values of "mirror".' + echo >&2 + echo >&2 'This script is tested to work with the original upstream version of rinse,' + echo >&2 ' found at http://www.steve.org.uk/Software/rinse/ and also in Debian at' + echo >&2 ' http://packages.debian.org/wheezy/rinse -- as always, YMMV.' + echo >&2 + exit 1 +fi + +target="${TMPDIR:-/var/tmp}/docker-rootfs-rinse-$distro-$$-$RANDOM" + +cd "$(dirname "$(readlink -f "$BASH_SOURCE")")" +returnTo="$(pwd -P)" + +rinseArgs=( --arch amd64 --distribution "$distro" --directory "$target" ) +if [ "$mirror" ]; then + rinseArgs+=( --mirror "$mirror" ) +fi + +set -x + +mkdir -p "$target" + +sudo rinse "${rinseArgs[@]}" + +cd "$target" + +# rinse fails a little at setting up /dev, so we'll just wipe it out and create our own +sudo rm -rf dev +sudo mkdir -m 755 dev +( + cd dev + sudo ln -sf /proc/self/fd ./ + sudo mkdir -m 755 pts + sudo mkdir -m 1777 shm + sudo mknod -m 600 console c 5 1 + sudo mknod -m 600 initctl p + sudo mknod -m 666 full c 1 7 + sudo mknod -m 666 null c 1 3 + sudo mknod -m 666 ptmx c 5 2 + sudo mknod -m 666 random c 1 8 + sudo mknod -m 666 tty c 5 0 + sudo mknod -m 666 tty0 c 4 0 + sudo mknod -m 666 urandom c 1 9 + sudo mknod -m 666 zero c 1 5 +) + +# effectively: febootstrap-minimize --keep-zoneinfo --keep-rpmdb --keep-services "$target" +# locales +sudo rm -rf usr/{{lib,share}/locale,{lib,lib64}/gconv,bin/localedef,sbin/build-locale-archive} +# docs and man pages +sudo rm -rf usr/share/{man,doc,info,gnome/help} +# cracklib +sudo rm -rf usr/share/cracklib +# i18n +sudo rm -rf usr/share/i18n +# yum cache +sudo rm -rf var/cache/yum +sudo mkdir -p --mode=0755 var/cache/yum +# sln +sudo rm -rf sbin/sln +# ldconfig +#sudo rm -rf sbin/ldconfig +sudo rm -rf etc/ld.so.cache var/cache/ldconfig +sudo mkdir -p --mode=0755 var/cache/ldconfig + +# allow networking init scripts inside the container to work without extra steps +echo 'NETWORKING=yes' | sudo tee etc/sysconfig/network > /dev/null + +# to restore locales later: +# yum reinstall glibc-common + +version= +if [ -r etc/redhat-release ]; then + version="$(sed -E 's/^[^0-9.]*([0-9.]+).*$/\1/' etc/redhat-release)" +elif [ -r etc/SuSE-release ]; then + version="$(awk '/^VERSION/ { print $3 }' etc/SuSE-release)" +fi + +if [ -z "$version" ]; then + echo >&2 "warning: cannot autodetect OS version, using $distro as tag" + sleep 20 + version="$distro" +fi + +sudo tar --numeric-owner -c . | docker import - $repo:$version + +docker run -i -t $repo:$version echo success + +cd "$returnTo" +sudo rm -rf "$target" diff --git a/contrib/mkimage-yum.sh b/contrib/mkimage-yum.sh new file mode 100755 index 00000000..919160c8 --- /dev/null +++ b/contrib/mkimage-yum.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# +# Create a base CentOS Docker image. +# +# This script is useful on systems with yum installed (e.g., building +# a CentOS image on CentOS). See contrib/mkimage-rinse.sh for a way +# to build CentOS images on other systems. + +usage() { + cat < +OPTIONS: + -p "" The list of packages to install in the container. + The default is blank. + -g "" The groups of packages to install in the container. + The default is "Core". + -y The path to the yum config to install packages from. The + default is /etc/yum.conf for Centos/RHEL and /etc/dnf/dnf.conf for Fedora +EOOPTS + exit 1 +} + +# option defaults +yum_config=/etc/yum.conf +if [ -f /etc/dnf/dnf.conf ] && command -v dnf &> /dev/null; then + yum_config=/etc/dnf/dnf.conf + alias yum=dnf +fi +install_groups="Core" +while getopts ":y:p:g:h" opt; do + case $opt in + y) + yum_config=$OPTARG + ;; + h) + usage + ;; + p) + install_packages="$OPTARG" + ;; + g) + install_groups="$OPTARG" + ;; + \?) + echo "Invalid option: -$OPTARG" + usage + ;; + esac +done +shift $((OPTIND - 1)) +name=$1 + +if [[ -z $name ]]; then + usage +fi + +target=$(mktemp -d --tmpdir $(basename $0).XXXXXX) + +set -x + +mkdir -m 755 "$target"/dev +mknod -m 600 "$target"/dev/console c 5 1 +mknod -m 600 "$target"/dev/initctl p +mknod -m 666 "$target"/dev/full c 1 7 +mknod -m 666 "$target"/dev/null c 1 3 +mknod -m 666 "$target"/dev/ptmx c 5 2 +mknod -m 666 "$target"/dev/random c 1 8 +mknod -m 666 "$target"/dev/tty c 5 0 +mknod -m 666 "$target"/dev/tty0 c 4 0 +mknod -m 666 "$target"/dev/urandom c 1 9 +mknod -m 666 "$target"/dev/zero c 1 5 + +# amazon linux yum will fail without vars set +if [ -d /etc/yum/vars ]; then + mkdir -p -m 755 "$target"/etc/yum + cp -a /etc/yum/vars "$target"/etc/yum/ +fi + +if [[ -n "$install_groups" ]]; +then + yum -c "$yum_config" --installroot="$target" --releasever=/ --setopt=tsflags=nodocs \ + --setopt=group_package_types=mandatory -y groupinstall $install_groups +fi + +if [[ -n "$install_packages" ]]; +then + yum -c "$yum_config" --installroot="$target" --releasever=/ --setopt=tsflags=nodocs \ + --setopt=group_package_types=mandatory -y install $install_packages +fi + +yum -c "$yum_config" --installroot="$target" -y clean all + +cat > "$target"/etc/sysconfig/network <&2 "warning: cannot autodetect OS version, using '$name' as tag" + version=$name +fi + +tar --numeric-owner -c -C "$target" . | docker import - $name:$version + +docker run -i -t --rm $name:$version /bin/bash -c 'echo success' + +rm -rf "$target" diff --git a/contrib/mkimage.sh b/contrib/mkimage.sh new file mode 100755 index 00000000..3976d72d --- /dev/null +++ b/contrib/mkimage.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +set -e + +mkimg="$(basename "$0")" + +usage() { + echo >&2 "usage: $mkimg [-d dir] [-t tag] [--compression algo| --no-compression] script [script-args]" + echo >&2 " ie: $mkimg -t someuser/debian debootstrap --variant=minbase jessie" + echo >&2 " $mkimg -t someuser/ubuntu debootstrap --include=ubuntu-minimal --components=main,universe trusty" + echo >&2 " $mkimg -t someuser/busybox busybox-static" + echo >&2 " $mkimg -t someuser/centos:5 rinse --distribution centos-5" + echo >&2 " $mkimg -t someuser/mageia:4 mageia-urpmi --version=4" + echo >&2 " $mkimg -t someuser/mageia:4 mageia-urpmi --version=4 --mirror=http://somemirror/" + exit 1 +} + +scriptDir="$(dirname "$(readlink -f "$BASH_SOURCE")")/mkimage" + +optTemp=$(getopt --options '+d:t:c:hC' --longoptions 'dir:,tag:,compression:,no-compression,help' --name "$mkimg" -- "$@") +eval set -- "$optTemp" +unset optTemp + +dir= +tag= +compression="auto" +while true; do + case "$1" in + -d|--dir) dir="$2" ; shift 2 ;; + -t|--tag) tag="$2" ; shift 2 ;; + --compression) compression="$2" ; shift 2 ;; + --no-compression) compression="none" ; shift 1 ;; + -h|--help) usage ;; + --) shift ; break ;; + esac +done + +script="$1" +[ "$script" ] || usage +shift + +if [ "$compression" == 'auto' ] || [ -z "$compression" ] +then + compression='xz' +fi + +[ "$compression" == 'none' ] && compression='' + +if [ ! -x "$scriptDir/$script" ]; then + echo >&2 "error: $script does not exist or is not executable" + echo >&2 " see $scriptDir for possible scripts" + exit 1 +fi + +# don't mistake common scripts like .febootstrap-minimize as image-creators +if [[ "$script" == .* ]]; then + echo >&2 "error: $script is a script helper, not a script" + echo >&2 " see $scriptDir for possible scripts" + exit 1 +fi + +delDir= +if [ -z "$dir" ]; then + dir="$(mktemp -d ${TMPDIR:-/var/tmp}/docker-mkimage.XXXXXXXXXX)" + delDir=1 +fi + +rootfsDir="$dir/rootfs" +( set -x; mkdir -p "$rootfsDir" ) + +# pass all remaining arguments to $script +"$scriptDir/$script" "$rootfsDir" "$@" + +# Docker mounts tmpfs at /dev and procfs at /proc so we can remove them +rm -rf "$rootfsDir/dev" "$rootfsDir/proc" +mkdir -p "$rootfsDir/dev" "$rootfsDir/proc" + +# make sure /etc/resolv.conf has something useful in it +mkdir -p "$rootfsDir/etc" +cat > "$rootfsDir/etc/resolv.conf" <<'EOF' +nameserver 8.8.8.8 +nameserver 8.8.4.4 +EOF + +tarFile="$dir/rootfs.tar${compression:+.$compression}" +touch "$tarFile" + +( + set -x + tar --numeric-owner --create --auto-compress --file "$tarFile" --directory "$rootfsDir" --transform='s,^./,,' . +) + +echo >&2 "+ cat > '$dir/Dockerfile'" +cat > "$dir/Dockerfile" <> "$dir/Dockerfile" ) + break + fi +done + +( set -x; rm -rf "$rootfsDir" ) + +if [ "$tag" ]; then + ( set -x; docker build -t "$tag" "$dir" ) +elif [ "$delDir" ]; then + # if we didn't specify a tag and we're going to delete our dir, let's just build an untagged image so that we did _something_ + ( set -x; docker build "$dir" ) +fi + +if [ "$delDir" ]; then + ( set -x; rm -rf "$dir" ) +fi diff --git a/contrib/mkimage/.febootstrap-minimize b/contrib/mkimage/.febootstrap-minimize new file mode 100755 index 00000000..7749e63f --- /dev/null +++ b/contrib/mkimage/.febootstrap-minimize @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -e + +rootfsDir="$1" +shift + +( + cd "$rootfsDir" + + # effectively: febootstrap-minimize --keep-zoneinfo --keep-rpmdb --keep-services "$target" + # locales + rm -rf usr/{{lib,share}/locale,{lib,lib64}/gconv,bin/localedef,sbin/build-locale-archive} + # docs and man pages + rm -rf usr/share/{man,doc,info,gnome/help} + # cracklib + rm -rf usr/share/cracklib + # i18n + rm -rf usr/share/i18n + # yum cache + rm -rf var/cache/yum + mkdir -p --mode=0755 var/cache/yum + # sln + rm -rf sbin/sln + # ldconfig + #rm -rf sbin/ldconfig + rm -rf etc/ld.so.cache var/cache/ldconfig + mkdir -p --mode=0755 var/cache/ldconfig +) diff --git a/contrib/mkimage/busybox-static b/contrib/mkimage/busybox-static new file mode 100755 index 00000000..e15322b4 --- /dev/null +++ b/contrib/mkimage/busybox-static @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -e + +rootfsDir="$1" +shift + +busybox="$(which busybox 2>/dev/null || true)" +if [ -z "$busybox" ]; then + echo >&2 'error: busybox: not found' + echo >&2 ' install it with your distribution "busybox-static" package' + exit 1 +fi +if ! ldd "$busybox" 2>&1 | grep -q 'not a dynamic executable'; then + echo >&2 "error: '$busybox' appears to be a dynamic executable" + echo >&2 ' you should install your distribution "busybox-static" package instead' + exit 1 +fi + +mkdir -p "$rootfsDir/bin" +rm -f "$rootfsDir/bin/busybox" # just in case +cp "$busybox" "$rootfsDir/bin/busybox" + +( + cd "$rootfsDir" + + IFS=$'\n' + modules=( $(bin/busybox --list-modules) ) + unset IFS + + for module in "${modules[@]}"; do + mkdir -p "$(dirname "$module")" + ln -sf /bin/busybox "$module" + done +) diff --git a/contrib/mkimage/debootstrap b/contrib/mkimage/debootstrap new file mode 100755 index 00000000..c613d537 --- /dev/null +++ b/contrib/mkimage/debootstrap @@ -0,0 +1,240 @@ +#!/usr/bin/env bash +set -e + +rootfsDir="$1" +shift + +# we have to do a little fancy footwork to make sure "rootfsDir" becomes the second non-option argument to debootstrap + +before=() +while [ $# -gt 0 ] && [[ "$1" == -* ]]; do + before+=( "$1" ) + shift +done + +suite="$1" +shift + +# get path to "chroot" in our current PATH +chrootPath="$(type -P chroot)" +rootfs_chroot() { + # "chroot" doesn't set PATH, so we need to set it explicitly to something our new debootstrap chroot can use appropriately! + + # set PATH and chroot away! + PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' \ + "$chrootPath" "$rootfsDir" "$@" +} + +# allow for DEBOOTSTRAP=qemu-debootstrap ./mkimage.sh ... +: ${DEBOOTSTRAP:=debootstrap} + +( + set -x + $DEBOOTSTRAP "${before[@]}" "$suite" "$rootfsDir" "$@" +) + +# now for some Docker-specific tweaks + +# prevent init scripts from running during install/update +echo >&2 "+ echo exit 101 > '$rootfsDir/usr/sbin/policy-rc.d'" +cat > "$rootfsDir/usr/sbin/policy-rc.d" <<-'EOF' + #!/bin/sh + + # For most Docker users, "apt-get install" only happens during "docker build", + # where starting services doesn't work and often fails in humorous ways. This + # prevents those failures by stopping the services from attempting to start. + + exit 101 +EOF +chmod +x "$rootfsDir/usr/sbin/policy-rc.d" + +# prevent upstart scripts from running during install/update +( + set -x + rootfs_chroot dpkg-divert --local --rename --add /sbin/initctl + cp -a "$rootfsDir/usr/sbin/policy-rc.d" "$rootfsDir/sbin/initctl" + sed -i 's/^exit.*/exit 0/' "$rootfsDir/sbin/initctl" +) + +# shrink a little, since apt makes us cache-fat (wheezy: ~157.5MB vs ~120MB) +( set -x; rootfs_chroot apt-get clean ) + +# this file is one APT creates to make sure we don't "autoremove" our currently +# in-use kernel, which doesn't really apply to debootstraps/Docker images that +# don't even have kernels installed +rm -f "$rootfsDir/etc/apt/apt.conf.d/01autoremove-kernels" + +# Ubuntu 10.04 sucks... :) +if strings "$rootfsDir/usr/bin/dpkg" | grep -q unsafe-io; then + # force dpkg not to call sync() after package extraction (speeding up installs) + echo >&2 "+ echo force-unsafe-io > '$rootfsDir/etc/dpkg/dpkg.cfg.d/docker-apt-speedup'" + cat > "$rootfsDir/etc/dpkg/dpkg.cfg.d/docker-apt-speedup" <<-'EOF' + # For most Docker users, package installs happen during "docker build", which + # doesn't survive power loss and gets restarted clean afterwards anyhow, so + # this minor tweak gives us a nice speedup (much nicer on spinning disks, + # obviously). + + force-unsafe-io + EOF +fi + +if [ -d "$rootfsDir/etc/apt/apt.conf.d" ]; then + # _keep_ us lean by effectively running "apt-get clean" after every install + aptGetClean='"rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true";' + echo >&2 "+ cat > '$rootfsDir/etc/apt/apt.conf.d/docker-clean'" + cat > "$rootfsDir/etc/apt/apt.conf.d/docker-clean" <<-EOF + # Since for most Docker users, package installs happen in "docker build" steps, + # they essentially become individual layers due to the way Docker handles + # layering, especially using CoW filesystems. What this means for us is that + # the caches that APT keeps end up just wasting space in those layers, making + # our layers unnecessarily large (especially since we'll normally never use + # these caches again and will instead just "docker build" again and make a brand + # new image). + + # Ideally, these would just be invoking "apt-get clean", but in our testing, + # that ended up being cyclic and we got stuck on APT's lock, so we get this fun + # creation that's essentially just "apt-get clean". + DPkg::Post-Invoke { ${aptGetClean} }; + APT::Update::Post-Invoke { ${aptGetClean} }; + + Dir::Cache::pkgcache ""; + Dir::Cache::srcpkgcache ""; + + # Note that we do realize this isn't the ideal way to do this, and are always + # open to better suggestions (https://github.com/docker/docker/issues). + EOF + + # remove apt-cache translations for fast "apt-get update" + echo >&2 "+ echo Acquire::Languages 'none' > '$rootfsDir/etc/apt/apt.conf.d/docker-no-languages'" + cat > "$rootfsDir/etc/apt/apt.conf.d/docker-no-languages" <<-'EOF' + # In Docker, we don't often need the "Translations" files, so we're just wasting + # time and space by downloading them, and this inhibits that. For users that do + # need them, it's a simple matter to delete this file and "apt-get update". :) + + Acquire::Languages "none"; + EOF + + echo >&2 "+ echo Acquire::GzipIndexes 'true' > '$rootfsDir/etc/apt/apt.conf.d/docker-gzip-indexes'" + cat > "$rootfsDir/etc/apt/apt.conf.d/docker-gzip-indexes" <<-'EOF' + # Since Docker users using "RUN apt-get update && apt-get install -y ..." in + # their Dockerfiles don't go delete the lists files afterwards, we want them to + # be as small as possible on-disk, so we explicitly request "gz" versions and + # tell Apt to keep them gzipped on-disk. + + # For comparison, an "apt-get update" layer without this on a pristine + # "debian:wheezy" base image was "29.88 MB", where with this it was only + # "8.273 MB". + + Acquire::GzipIndexes "true"; + Acquire::CompressionTypes::Order:: "gz"; + EOF + + # update "autoremove" configuration to be aggressive about removing suggests deps that weren't manually installed + echo >&2 "+ echo Apt::AutoRemove::SuggestsImportant 'false' > '$rootfsDir/etc/apt/apt.conf.d/docker-autoremove-suggests'" + cat > "$rootfsDir/etc/apt/apt.conf.d/docker-autoremove-suggests" <<-'EOF' + # Since Docker users are looking for the smallest possible final images, the + # following emerges as a very common pattern: + + # RUN apt-get update \ + # && apt-get install -y \ + # && \ + # && apt-get purge -y --auto-remove + + # By default, APT will actually _keep_ packages installed via Recommends or + # Depends if another package Suggests them, even and including if the package + # that originally caused them to be installed is removed. Setting this to + # "false" ensures that APT is appropriately aggressive about removing the + # packages it added. + + # https://aptitude.alioth.debian.org/doc/en/ch02s05s05.html#configApt-AutoRemove-SuggestsImportant + Apt::AutoRemove::SuggestsImportant "false"; + EOF +fi + +if [ -z "$DONT_TOUCH_SOURCES_LIST" ]; then + # tweak sources.list, where appropriate + lsbDist= + if [ -z "$lsbDist" -a -r "$rootfsDir/etc/os-release" ]; then + lsbDist="$(. "$rootfsDir/etc/os-release" && echo "$ID")" + fi + if [ -z "$lsbDist" -a -r "$rootfsDir/etc/lsb-release" ]; then + lsbDist="$(. "$rootfsDir/etc/lsb-release" && echo "$DISTRIB_ID")" + fi + if [ -z "$lsbDist" -a -r "$rootfsDir/etc/debian_version" ]; then + lsbDist='Debian' + fi + # normalize to lowercase for easier matching + lsbDist="$(echo "$lsbDist" | tr '[:upper:]' '[:lower:]')" + case "$lsbDist" in + debian) + # updates and security! + if [ "$suite" != 'sid' -a "$suite" != 'unstable' ]; then + ( + set -x + sed -i " + p; + s/ $suite / ${suite}-updates / + " "$rootfsDir/etc/apt/sources.list" + echo "deb http://security.debian.org $suite/updates main" >> "$rootfsDir/etc/apt/sources.list" + # squeeze-lts + if [ -f "$rootfsDir/etc/debian_version" ]; then + ltsSuite= + case "$(cat "$rootfsDir/etc/debian_version")" in + 6.*) ltsSuite='squeeze-lts' ;; + #7.*) ltsSuite='wheezy-lts' ;; + #8.*) ltsSuite='jessie-lts' ;; + esac + if [ "$ltsSuite" ]; then + head -1 "$rootfsDir/etc/apt/sources.list" \ + | sed "s/ $suite / $ltsSuite /" \ + >> "$rootfsDir/etc/apt/sources.list" + fi + fi + ) + fi + ;; + ubuntu) + # add the updates and security repositories + ( + set -x + sed -i " + p; + s/ $suite / ${suite}-updates /; p; + s/ $suite-updates / ${suite}-security / + " "$rootfsDir/etc/apt/sources.list" + ) + ;; + tanglu) + # add the updates repository + if [ "$suite" != 'devel' ]; then + ( + set -x + sed -i " + p; + s/ $suite / ${suite}-updates / + " "$rootfsDir/etc/apt/sources.list" + ) + fi + ;; + steamos) + # add contrib and non-free if "main" is the only component + ( + set -x + sed -i "s/ $suite main$/ $suite main contrib non-free/" "$rootfsDir/etc/apt/sources.list" + ) + ;; + esac +fi + +( + set -x + + # make sure we're fully up-to-date + rootfs_chroot sh -xc 'apt-get update && apt-get dist-upgrade -y' + + # delete all the apt list files since they're big and get stale quickly + rm -rf "$rootfsDir/var/lib/apt/lists"/* + # this forces "apt-get update" in dependent images, which is also good + + mkdir "$rootfsDir/var/lib/apt/lists/partial" # Lucid... "E: Lists directory /var/lib/apt/lists/partial is missing." +) diff --git a/contrib/mkimage/mageia-urpmi b/contrib/mkimage/mageia-urpmi new file mode 100755 index 00000000..93fb289c --- /dev/null +++ b/contrib/mkimage/mageia-urpmi @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# +# Needs to be run from Mageia 4 or greater for kernel support for docker. +# +# Mageia 4 does not have docker available in official repos, so please +# install and run the docker binary manually. +# +# Tested working versions are for Mageia 2 onwards (inc. cauldron). +# +set -e + +rootfsDir="$1" +shift + +optTemp=$(getopt --options '+v:,m:' --longoptions 'version:,mirror:' --name mageia-urpmi -- "$@") +eval set -- "$optTemp" +unset optTemp + +installversion= +mirror= +while true; do + case "$1" in + -v|--version) installversion="$2" ; shift 2 ;; + -m|--mirror) mirror="$2" ; shift 2 ;; + --) shift ; break ;; + esac +done + +if [ -z $installversion ]; then + # Attempt to match host version + if [ -r /etc/mageia-release ]; then + installversion="$(sed 's/^[^0-9\]*\([0-9.]\+\).*$/\1/' /etc/mageia-release)" + else + echo "Error: no version supplied and unable to detect host mageia version" + exit 1 + fi +fi + +if [ -z $mirror ]; then + # No mirror provided, default to mirrorlist + mirror="--mirrorlist https://mirrors.mageia.org/api/mageia.$installversion.x86_64.list" +fi + +( + set -x + urpmi.addmedia --distrib \ + $mirror \ + --urpmi-root "$rootfsDir" + urpmi basesystem-minimal urpmi \ + --auto \ + --no-suggests \ + --urpmi-root "$rootfsDir" \ + --root "$rootfsDir" +) + +"$(dirname "$BASH_SOURCE")/.febootstrap-minimize" "$rootfsDir" + +if [ -d "$rootfsDir/etc/sysconfig" ]; then + # allow networking init scripts inside the container to work without extra steps + echo 'NETWORKING=yes' > "$rootfsDir/etc/sysconfig/network" +fi diff --git a/contrib/mkimage/rinse b/contrib/mkimage/rinse new file mode 100755 index 00000000..75eb4f0d --- /dev/null +++ b/contrib/mkimage/rinse @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -e + +rootfsDir="$1" +shift + +# specifying --arch below is safe because "$@" can override it and the "latest" one wins :) + +( + set -x + rinse --directory "$rootfsDir" --arch amd64 "$@" +) + +"$(dirname "$BASH_SOURCE")/.febootstrap-minimize" "$rootfsDir" + +if [ -d "$rootfsDir/etc/sysconfig" ]; then + # allow networking init scripts inside the container to work without extra steps + echo 'NETWORKING=yes' > "$rootfsDir/etc/sysconfig/network" +fi + +# make sure we're fully up-to-date, too +( + set -x + chroot "$rootfsDir" yum update -y +) diff --git a/contrib/nnp-test/Dockerfile b/contrib/nnp-test/Dockerfile new file mode 100644 index 00000000..026d8695 --- /dev/null +++ b/contrib/nnp-test/Dockerfile @@ -0,0 +1,9 @@ +FROM buildpack-deps:jessie + +COPY . /usr/src/ + +WORKDIR /usr/src/ + +RUN gcc -g -Wall -static nnp-test.c -o /usr/bin/nnp-test + +RUN chmod +s /usr/bin/nnp-test diff --git a/contrib/nnp-test/nnp-test.c b/contrib/nnp-test/nnp-test.c new file mode 100644 index 00000000..b767da7e --- /dev/null +++ b/contrib/nnp-test/nnp-test.c @@ -0,0 +1,10 @@ +#include +#include +#include + +int main(int argc, char *argv[]) +{ + printf("EUID=%d\n", geteuid()); + return 0; +} + diff --git a/contrib/nuke-graph-directory.sh b/contrib/nuke-graph-directory.sh new file mode 100755 index 00000000..99b527de --- /dev/null +++ b/contrib/nuke-graph-directory.sh @@ -0,0 +1,65 @@ +#!/bin/sh +set -e + +dir="$1" + +if [ -z "$dir" ]; then + { + echo 'This script is for destroying old /var/lib/docker directories more safely than' + echo ' "rm -rf", which can cause data loss or other serious issues.' + echo + echo "usage: $0 directory" + echo " ie: $0 /var/lib/docker" + } >&2 + exit 1 +fi + +if [ "$(id -u)" != 0 ]; then + echo >&2 "error: $0 must be run as root" + exit 1 +fi + +if [ ! -d "$dir" ]; then + echo >&2 "error: $dir is not a directory" + exit 1 +fi + +dir="$(readlink -f "$dir")" + +echo +echo "Nuking $dir ..." +echo ' (if this is wrong, press Ctrl+C NOW!)' +echo + +( set -x; sleep 10 ) +echo + +dir_in_dir() { + inner="$1" + outer="$2" + [ "${inner#$outer}" != "$inner" ] +} + +# let's start by unmounting any submounts in $dir +# (like -v /home:... for example - DON'T DELETE MY HOME DIRECTORY BRU!) +for mount in $(awk '{ print $5 }' /proc/self/mountinfo); do + mount="$(readlink -f "$mount" || true)" + if dir_in_dir "$mount" "$dir"; then + ( set -x; umount -f "$mount" ) + fi +done + +# now, let's go destroy individual btrfs subvolumes, if any exist +if command -v btrfs > /dev/null 2>&1; then + root="$(df "$dir" | awk 'NR>1 { print $NF }')" + root="${root%/}" # if root is "/", we want it to become "" + for subvol in $(btrfs subvolume list -o "$root/" 2>/dev/null | awk -F' path ' '{ print $2 }' | sort -r); do + subvolDir="$root/$subvol" + if dir_in_dir "$subvolDir" "$dir"; then + ( set -x; btrfs subvolume delete "$subvolDir" ) + fi + done +fi + +# finally, DESTROY ALL THINGS +( set -x; rm -rf "$dir" ) diff --git a/contrib/project-stats.sh b/contrib/project-stats.sh new file mode 100755 index 00000000..2691c72f --- /dev/null +++ b/contrib/project-stats.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +## Run this script from the root of the docker repository +## to query project stats useful to the maintainers. +## You will need to install `pulls` and `issues` from +## https://github.com/crosbymichael/pulls + +set -e + +echo -n "Open pulls: " +PULLS=$(pulls | wc -l); let PULLS=$PULLS-1 +echo $PULLS + +echo -n "Pulls alru: " +pulls alru + +echo -n "Open issues: " +ISSUES=$(issues list | wc -l); let ISSUES=$ISSUES-1 +echo $ISSUES + +echo -n "Issues alru: " +issues alru diff --git a/contrib/report-issue.sh b/contrib/report-issue.sh new file mode 100755 index 00000000..cb54f1a5 --- /dev/null +++ b/contrib/report-issue.sh @@ -0,0 +1,105 @@ +#!/bin/sh + +# This is a convenience script for reporting issues that include a base +# template of information. See https://github.com/docker/docker/pull/8845 + +set -e + +DOCKER_ISSUE_URL=${DOCKER_ISSUE_URL:-"https://github.com/docker/docker/issues/new"} +DOCKER_ISSUE_NAME_PREFIX=${DOCKER_ISSUE_NAME_PREFIX:-"Report: "} +DOCKER=${DOCKER:-"docker"} +DOCKER_COMMAND="${DOCKER}" +export DOCKER_COMMAND + +# pulled from https://gist.github.com/cdown/1163649 +function urlencode() { + # urlencode + + local length="${#1}" + for (( i = 0; i < length; i++ )); do + local c="${1:i:1}" + case $c in + [a-zA-Z0-9.~_-]) printf "$c" ;; + *) printf '%%%02X' "'$c" + esac + done +} + +function template() { +# this should always match the template from CONTRIBUTING.md + cat <<- EOM + Description of problem: + + + \`docker version\`: + `${DOCKER_COMMAND} -D version` + + + \`docker info\`: + `${DOCKER_COMMAND} -D info` + + + \`uname -a\`: + `uname -a` + + + Environment details (AWS, VirtualBox, physical, etc.): + + + How reproducible: + + + Steps to Reproduce: + 1. + 2. + 3. + + + Actual Results: + + + Expected Results: + + + Additional info: + + + EOM +} + +function format_issue_url() { + if [ ${#@} -ne 2 ] ; then + return 1 + fi + local issue_name=$(urlencode "${DOCKER_ISSUE_NAME_PREFIX}${1}") + local issue_body=$(urlencode "${2}") + echo "${DOCKER_ISSUE_URL}?title=${issue_name}&body=${issue_body}" +} + + +echo -ne "Do you use \`sudo\` to call docker? [y|N]: " +read -r -n 1 use_sudo +echo "" + +if [ "x${use_sudo}" = "xy" -o "x${use_sudo}" = "xY" ]; then + export DOCKER_COMMAND="sudo ${DOCKER}" +fi + +echo -ne "Title of new issue?: " +read -r issue_title +echo "" + +issue_url=$(format_issue_url "${issue_title}" "$(template)") + +if which xdg-open 2>/dev/null >/dev/null ; then + echo -ne "Would like to launch this report in your browser? [Y|n]: " + read -r -n 1 launch_now + echo "" + + if [ "${launch_now}" != "n" -a "${launch_now}" != "N" ]; then + xdg-open "${issue_url}" + fi +fi + +echo "If you would like to manually open the url, you can open this link if your browser: ${issue_url}" + diff --git a/contrib/reprepro/suites.sh b/contrib/reprepro/suites.sh new file mode 100755 index 00000000..9ecf99d4 --- /dev/null +++ b/contrib/reprepro/suites.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e + +cd "$(dirname "$BASH_SOURCE")/../.." + +targets_from() { + git fetch -q https://github.com/docker/docker.git "$1" + git ls-tree -r --name-only "$(git rev-parse FETCH_HEAD)" contrib/builder/deb/ | grep '/Dockerfile$' | sed -r 's!^contrib/builder/deb/|^contrib/builder/deb/amd64/|-debootstrap|/Dockerfile$!!g' | grep -v / +} + +release_branch=$(git ls-remote --heads https://github.com/docker/docker.git | awk -F 'refs/heads/' '$2 ~ /^release/ { print $2 }' | sort -V | tail -1) +{ targets_from master; targets_from "$release_branch"; } | sort -u diff --git a/contrib/syntax/kate/Dockerfile.xml b/contrib/syntax/kate/Dockerfile.xml new file mode 100644 index 00000000..05692504 --- /dev/null +++ b/contrib/syntax/kate/Dockerfile.xml @@ -0,0 +1,70 @@ + + + + + + + FROM + MAINTAINER + ENV + RUN + ONBUILD + COPY + ADD + VOLUME + EXPOSE + ENTRYPOINT + CMD + WORKDIR + USER + LABEL + STOPSIGNAL + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contrib/syntax/nano/Dockerfile.nanorc b/contrib/syntax/nano/Dockerfile.nanorc new file mode 100644 index 00000000..80e56dfb --- /dev/null +++ b/contrib/syntax/nano/Dockerfile.nanorc @@ -0,0 +1,26 @@ +## Syntax highlighting for Dockerfiles +syntax "Dockerfile" "Dockerfile[^/]*$" + +## Keywords +icolor red "^(FROM|MAINTAINER|RUN|CMD|LABEL|EXPOSE|ENV|ADD|COPY|ENTRYPOINT|VOLUME|USER|WORKDIR|ONBUILD)[[:space:]]" + +## Brackets & parenthesis +color brightgreen "(\(|\)|\[|\])" + +## Double ampersand +color brightmagenta "&&" + +## Comments +icolor cyan "^[[:space:]]*#.*$" + +## Blank space at EOL +color ,green "[[:space:]]+$" + +## Strings, single-quoted +color brightwhite "'([^']|(\\'))*'" "%[qw]\{[^}]*\}" "%[qw]\([^)]*\)" "%[qw]<[^>]*>" "%[qw]\[[^]]*\]" "%[qw]\$[^$]*\$" "%[qw]\^[^^]*\^" "%[qw]![^!]*!" + +## Strings, double-quoted +color brightwhite ""([^"]|(\\"))*"" "%[QW]?\{[^}]*\}" "%[QW]?\([^)]*\)" "%[QW]?<[^>]*>" "%[QW]?\[[^]]*\]" "%[QW]?\$[^$]*\$" "%[QW]?\^[^^]*\^" "%[QW]?![^!]*!" + +## Single and double quotes +color brightyellow "('|\")" diff --git a/contrib/syntax/nano/README.md b/contrib/syntax/nano/README.md new file mode 100644 index 00000000..5985208b --- /dev/null +++ b/contrib/syntax/nano/README.md @@ -0,0 +1,32 @@ +Dockerfile.nanorc +================= + +Dockerfile syntax highlighting for nano + +Single User Installation +------------------------ +1. Create a nano syntax directory in your home directory: + * `mkdir -p ~/.nano/syntax` + +2. Copy `Dockerfile.nanorc` to` ~/.nano/syntax/` + * `cp Dockerfile.nanorc ~/.nano/syntax/` + +3. Add the following to your `~/.nanorc` to tell nano where to find the `Dockerfile.nanorc` file + ``` +## Dockerfile files +include "~/.nano/syntax/Dockerfile.nanorc" + ``` + +System Wide Installation +------------------------ +1. Create a nano syntax directory: + * `mkdir /usr/local/share/nano` + +2. Copy `Dockerfile.nanorc` to `/usr/local/share/nano` + * `cp Dockerfile.nanorc /usr/local/share/nano/` + +3. Add the following to your `/etc/nanorc`: + ``` +## Dockerfile files +include "/usr/local/share/nano/Dockerfile.nanorc" + ``` diff --git a/contrib/syntax/textmate/Docker.tmbundle/Preferences/Dockerfile.tmPreferences b/contrib/syntax/textmate/Docker.tmbundle/Preferences/Dockerfile.tmPreferences new file mode 100644 index 00000000..20f0d04c --- /dev/null +++ b/contrib/syntax/textmate/Docker.tmbundle/Preferences/Dockerfile.tmPreferences @@ -0,0 +1,24 @@ + + + + + name + Comments + scope + source.dockerfile + settings + + shellVariables + + + name + TM_COMMENT_START + value + # + + + + uuid + 2B215AC0-A7F3-4090-9FF6-F4842BD56CA7 + + diff --git a/contrib/syntax/textmate/Docker.tmbundle/Syntaxes/Dockerfile.tmLanguage b/contrib/syntax/textmate/Docker.tmbundle/Syntaxes/Dockerfile.tmLanguage new file mode 100644 index 00000000..f45f6669 --- /dev/null +++ b/contrib/syntax/textmate/Docker.tmbundle/Syntaxes/Dockerfile.tmLanguage @@ -0,0 +1,143 @@ + + + + + fileTypes + + Dockerfile + + name + Dockerfile + patterns + + + captures + + 1 + + name + keyword.control.dockerfile + + 2 + + name + keyword.other.special-method.dockerfile + + + match + ^\s*(?:(ONBUILD)\s+)?(FROM|MAINTAINER|RUN|EXPOSE|ENV|ADD|VOLUME|USER|WORKDIR|COPY|LABEL|STOPSIGNAL|ARG)\s + + + captures + + 1 + + name + keyword.operator.dockerfile + + 2 + + name + keyword.other.special-method.dockerfile + + + match + ^\s*(?:(ONBUILD)\s+)?(CMD|ENTRYPOINT)\s + + + begin + " + beginCaptures + + 1 + + name + punctuation.definition.string.begin.dockerfile + + + end + " + endCaptures + + 1 + + name + punctuation.definition.string.end.dockerfile + + + name + string.quoted.double.dockerfile + patterns + + + match + \\. + name + constant.character.escaped.dockerfile + + + + + begin + ' + beginCaptures + + 1 + + name + punctuation.definition.string.begin.dockerfile + + + end + ' + endCaptures + + 1 + + name + punctuation.definition.string.end.dockerfile + + + name + string.quoted.single.dockerfile + patterns + + + match + \\. + name + constant.character.escaped.dockerfile + + + + + captures + + 1 + + name + punctuation.whitespace.comment.leading.dockerfile + + 2 + + name + comment.line.number-sign.dockerfile + + 3 + + name + punctuation.definition.comment.dockerfile + + + comment + comment.line + match + ^(\s*)((#).*$\n?) + + + scopeName + source.dockerfile + uuid + a39d8795-59d2-49af-aa00-fe74ee29576e + + diff --git a/contrib/syntax/textmate/Docker.tmbundle/info.plist b/contrib/syntax/textmate/Docker.tmbundle/info.plist new file mode 100644 index 00000000..239f4b0a --- /dev/null +++ b/contrib/syntax/textmate/Docker.tmbundle/info.plist @@ -0,0 +1,16 @@ + + + + + contactEmailRot13 + germ@andz.com.ar + contactName + GermanDZ + description + Helpers for Docker. + name + Docker + uuid + 8B9DDBAF-E65C-4E12-FFA7-467D4AA535B1 + + diff --git a/contrib/syntax/textmate/README.md b/contrib/syntax/textmate/README.md new file mode 100644 index 00000000..ce611018 --- /dev/null +++ b/contrib/syntax/textmate/README.md @@ -0,0 +1,17 @@ +# Docker.tmbundle + +Dockerfile syntax highlighting for TextMate and Sublime Text. + +## Install + +### Sublime Text + +Available for Sublime Text under [package control](https://sublime.wbond.net/packages/Dockerfile%20Syntax%20Highlighting). +Search for *Dockerfile Syntax Highlighting* + +### TextMate 2 + +You can install this bundle in TextMate by opening the preferences and going to the bundles tab. After installation it will be automatically updated for you. + +enjoy. + diff --git a/contrib/syntax/textmate/REVIEWERS b/contrib/syntax/textmate/REVIEWERS new file mode 100644 index 00000000..965743df --- /dev/null +++ b/contrib/syntax/textmate/REVIEWERS @@ -0,0 +1 @@ +Asbjorn Enge (@asbjornenge) diff --git a/contrib/syntax/vim/LICENSE b/contrib/syntax/vim/LICENSE new file mode 100644 index 00000000..e67cdabd --- /dev/null +++ b/contrib/syntax/vim/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2013 Honza Pokorny +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/contrib/syntax/vim/README.md b/contrib/syntax/vim/README.md new file mode 100644 index 00000000..5aa9bd82 --- /dev/null +++ b/contrib/syntax/vim/README.md @@ -0,0 +1,26 @@ +dockerfile.vim +============== + +Syntax highlighting for Dockerfiles + +Installation +------------ +With [pathogen](https://github.com/tpope/vim-pathogen), the usual way... + +With [Vundle](https://github.com/gmarik/Vundle.vim) + + Plugin 'docker/docker' , {'rtp': '/contrib/syntax/vim/'} + +Features +-------- + +The syntax highlighting includes: + +* The directives (e.g. `FROM`) +* Strings +* Comments + +License +------- + +BSD, short and sweet diff --git a/contrib/syntax/vim/doc/dockerfile.txt b/contrib/syntax/vim/doc/dockerfile.txt new file mode 100644 index 00000000..e69e2b7b --- /dev/null +++ b/contrib/syntax/vim/doc/dockerfile.txt @@ -0,0 +1,18 @@ +*dockerfile.txt* Syntax highlighting for Dockerfiles + +Author: Honza Pokorny +License: BSD + +INSTALLATION *installation* + +Drop it on your Pathogen path and you're all set. + +FEATURES *features* + +The syntax highlighting includes: + +* The directives (e.g. FROM) +* Strings +* Comments + + vim:tw=78:et:ft=help:norl: diff --git a/contrib/syntax/vim/ftdetect/dockerfile.vim b/contrib/syntax/vim/ftdetect/dockerfile.vim new file mode 100644 index 00000000..ee10e5d6 --- /dev/null +++ b/contrib/syntax/vim/ftdetect/dockerfile.vim @@ -0,0 +1 @@ +au BufNewFile,BufRead [Dd]ockerfile,Dockerfile.* set filetype=dockerfile diff --git a/contrib/syntax/vim/syntax/dockerfile.vim b/contrib/syntax/vim/syntax/dockerfile.vim new file mode 100644 index 00000000..bb75da85 --- /dev/null +++ b/contrib/syntax/vim/syntax/dockerfile.vim @@ -0,0 +1,31 @@ +" dockerfile.vim - Syntax highlighting for Dockerfiles +" Maintainer: Honza Pokorny +" Version: 0.5 + + +if exists("b:current_syntax") + finish +endif + +let b:current_syntax = "dockerfile" + +syntax case ignore + +syntax match dockerfileKeyword /\v^\s*(ONBUILD\s+)?(ADD|CMD|ENTRYPOINT|ENV|EXPOSE|FROM|MAINTAINER|RUN|USER|LABEL|VOLUME|WORKDIR|COPY|STOPSIGNAL|ARG)\s/ +highlight link dockerfileKeyword Keyword + +syntax region dockerfileString start=/\v"/ skip=/\v\\./ end=/\v"/ +highlight link dockerfileString String + +syntax match dockerfileComment "\v^\s*#.*$" +highlight link dockerfileComment Comment + +set commentstring=#\ %s + +" match "RUN", "CMD", and "ENTRYPOINT" lines, and parse them as shell +let s:current_syntax = b:current_syntax +unlet b:current_syntax +syntax include @SH syntax/sh.vim +let b:current_syntax = s:current_syntax +syntax region shLine matchgroup=dockerfileKeyword start=/\v^\s*(RUN|CMD|ENTRYPOINT)\s/ end=/\v$/ contains=@SH +" since @SH will handle "\" as part of the same line automatically, this "just works" for line continuation too, but with the caveat that it will highlight "RUN echo '" followed by a newline as if it were a block because the "'" is shell line continuation... not sure how to fix that just yet (TODO) diff --git a/contrib/syscall-test/Dockerfile b/contrib/syscall-test/Dockerfile new file mode 100644 index 00000000..8cd6bebf --- /dev/null +++ b/contrib/syscall-test/Dockerfile @@ -0,0 +1,9 @@ +FROM buildpack-deps:jessie + +COPY . /usr/src/ + +WORKDIR /usr/src/ + +RUN gcc -g -Wall -static userns.c -o /usr/bin/userns-test \ + && gcc -g -Wall -static ns.c -o /usr/bin/ns-test \ + && gcc -g -Wall -static acct.c -o /usr/bin/acct-test diff --git a/contrib/syscall-test/acct.c b/contrib/syscall-test/acct.c new file mode 100644 index 00000000..88ac2879 --- /dev/null +++ b/contrib/syscall-test/acct.c @@ -0,0 +1,16 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + int err = acct("/tmp/t"); + if (err == -1) { + fprintf(stderr, "acct failed: %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } + exit(EXIT_SUCCESS); +} diff --git a/contrib/syscall-test/ns.c b/contrib/syscall-test/ns.c new file mode 100644 index 00000000..bc897add --- /dev/null +++ b/contrib/syscall-test/ns.c @@ -0,0 +1,63 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define STACK_SIZE (1024 * 1024) /* Stack size for cloned child */ + +struct clone_args { + char **argv; +}; + +// child_exec is the func that will be executed as the result of clone +static int child_exec(void *stuff) +{ + struct clone_args *args = (struct clone_args *)stuff; + if (execvp(args->argv[0], args->argv) != 0) { + fprintf(stderr, "failed to execvp argments %s\n", + strerror(errno)); + exit(-1); + } + // we should never reach here! + exit(EXIT_FAILURE); +} + +int main(int argc, char **argv) +{ + struct clone_args args; + args.argv = &argv[1]; + + int clone_flags = CLONE_NEWNS | CLONE_NEWPID | SIGCHLD; + + // allocate stack for child + char *stack; /* Start of stack buffer */ + char *child_stack; /* End of stack buffer */ + stack = + mmap(NULL, STACK_SIZE, PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_ANON | MAP_STACK, -1, 0); + if (stack == MAP_FAILED) { + fprintf(stderr, "mmap failed: %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } + child_stack = stack + STACK_SIZE; /* Assume stack grows downward */ + + // the result of this call is that our child_exec will be run in another + // process returning it's pid + pid_t pid = clone(child_exec, child_stack, clone_flags, &args); + if (pid < 0) { + fprintf(stderr, "clone failed: %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } + // lets wait on our child process here before we, the parent, exits + if (waitpid(pid, NULL, 0) == -1) { + fprintf(stderr, "failed to wait pid %d\n", pid); + exit(EXIT_FAILURE); + } + exit(EXIT_SUCCESS); +} diff --git a/contrib/syscall-test/userns.c b/contrib/syscall-test/userns.c new file mode 100644 index 00000000..6ec553fe --- /dev/null +++ b/contrib/syscall-test/userns.c @@ -0,0 +1,63 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define STACK_SIZE (1024 * 1024) /* Stack size for cloned child */ + +struct clone_args { + char **argv; +}; + +// child_exec is the func that will be executed as the result of clone +static int child_exec(void *stuff) +{ + struct clone_args *args = (struct clone_args *)stuff; + if (execvp(args->argv[0], args->argv) != 0) { + fprintf(stderr, "failed to execvp argments %s\n", + strerror(errno)); + exit(-1); + } + // we should never reach here! + exit(EXIT_FAILURE); +} + +int main(int argc, char **argv) +{ + struct clone_args args; + args.argv = &argv[1]; + + int clone_flags = CLONE_NEWUSER | SIGCHLD; + + // allocate stack for child + char *stack; /* Start of stack buffer */ + char *child_stack; /* End of stack buffer */ + stack = + mmap(NULL, STACK_SIZE, PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_ANON | MAP_STACK, -1, 0); + if (stack == MAP_FAILED) { + fprintf(stderr, "mmap failed: %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } + child_stack = stack + STACK_SIZE; /* Assume stack grows downward */ + + // the result of this call is that our child_exec will be run in another + // process returning it's pid + pid_t pid = clone(child_exec, child_stack, clone_flags, &args); + if (pid < 0) { + fprintf(stderr, "clone failed: %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } + // lets wait on our child process here before we, the parent, exits + if (waitpid(pid, NULL, 0) == -1) { + fprintf(stderr, "failed to wait pid %d\n", pid); + exit(EXIT_FAILURE); + } + exit(EXIT_SUCCESS); +} diff --git a/contrib/udev/80-docker.rules b/contrib/udev/80-docker.rules new file mode 100644 index 00000000..f934c017 --- /dev/null +++ b/contrib/udev/80-docker.rules @@ -0,0 +1,3 @@ +# hide docker's loopback devices from udisks, and thus from user desktops +SUBSYSTEM=="block", ENV{DM_NAME}=="docker-*", ENV{UDISKS_PRESENTATION_HIDE}="1", ENV{UDISKS_IGNORE}="1" +SUBSYSTEM=="block", DEVPATH=="/devices/virtual/block/loop*", ATTR{loop/backing_file}=="/var/lib/docker/*", ENV{UDISKS_PRESENTATION_HIDE}="1", ENV{UDISKS_IGNORE}="1" diff --git a/contrib/vagrant-docker/README.md b/contrib/vagrant-docker/README.md new file mode 100644 index 00000000..360bfad3 --- /dev/null +++ b/contrib/vagrant-docker/README.md @@ -0,0 +1,50 @@ +# Vagrant integration + +Currently there are at least 4 different projects that we are aware of that deals +with integration with [Vagrant](http://vagrantup.com/) at different levels. One +approach is to use Docker as a [provisioner](http://docs.vagrantup.com/v2/provisioning/index.html) +which means you can create containers and pull base images on VMs using Docker's +CLI and the other is to use Docker as a [provider](http://docs.vagrantup.com/v2/providers/index.html), +meaning you can use Vagrant to control Docker containers. + + +### Provisioners + +* [Vocker](https://github.com/fgrehm/vocker) +* [Ventriloquist](https://github.com/fgrehm/ventriloquist) + +### Providers + +* [docker-provider](https://github.com/fgrehm/docker-provider) +* [vagrant-shell](https://github.com/destructuring/vagrant-shell) + +## Setting up Vagrant-docker with the Remote API + +The initial Docker upstart script will not work because it runs on `127.0.0.1`, which is not accessible to the host machine. Instead, we need to change the script to connect to `0.0.0.0`. To do this, modify `/etc/init/docker.conf` to look like this: + +``` +description "Docker daemon" + +start on filesystem +stop on runlevel [!2345] + +respawn + +script + /usr/bin/docker daemon -H=tcp://0.0.0.0:2375 +end script +``` + +Once that's done, you need to set up a SSH tunnel between your host machine and the vagrant machine that's running Docker. This can be done by running the following command in a host terminal: + +``` +ssh -L 2375:localhost:2375 -p 2222 vagrant@localhost +``` + +(The first 2375 is what your host can connect to, the second 2375 is what port Docker is running on in the vagrant machine, and the 2222 is the port Vagrant is providing for SSH. If VirtualBox is the VM you're using, you can see what value "2222" should be by going to: Network > Adapter 1 > Advanced > Port Forwarding in the VirtualBox GUI.) + +Note that because the port has been changed, to run docker commands from within the command line you must run them like this: + +``` +sudo docker -H 0.0.0.0:2375 < commands for docker > +``` diff --git a/daemon/apparmor_default.go b/daemon/apparmor_default.go new file mode 100644 index 00000000..e4065b4a --- /dev/null +++ b/daemon/apparmor_default.go @@ -0,0 +1,30 @@ +// +build linux + +package daemon + +import ( + "github.com/Sirupsen/logrus" + aaprofile "github.com/docker/docker/profiles/apparmor" + "github.com/opencontainers/runc/libcontainer/apparmor" +) + +// Define constants for native driver +const ( + defaultApparmorProfile = "docker-default" +) + +func installDefaultAppArmorProfile() { + if apparmor.IsEnabled() { + if err := aaprofile.InstallDefault(defaultApparmorProfile); err != nil { + apparmorProfiles := []string{defaultApparmorProfile} + + // Allow daemon to run if loading failed, but are active + // (possibly through another run, manually, or via system startup) + for _, policy := range apparmorProfiles { + if err := aaprofile.IsLoaded(policy); err != nil { + logrus.Errorf("AppArmor enabled on system but the %s profile could not be loaded.", policy) + } + } + } + } +} diff --git a/daemon/apparmor_default_unsupported.go b/daemon/apparmor_default_unsupported.go new file mode 100644 index 00000000..f186a68a --- /dev/null +++ b/daemon/apparmor_default_unsupported.go @@ -0,0 +1,6 @@ +// +build !linux + +package daemon + +func installDefaultAppArmorProfile() { +} diff --git a/daemon/archive.go b/daemon/archive.go new file mode 100644 index 00000000..5cf69852 --- /dev/null +++ b/daemon/archive.go @@ -0,0 +1,429 @@ +package daemon + +import ( + "errors" + "io" + "os" + "path/filepath" + "strings" + + "github.com/docker/docker/builder" + "github.com/docker/docker/container" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/chrootarchive" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/engine-api/types" +) + +// ErrExtractPointNotDirectory is used to convey that the operation to extract +// a tar archive to a directory in a container has failed because the specified +// path does not refer to a directory. +var ErrExtractPointNotDirectory = errors.New("extraction point is not a directory") + +// ContainerCopy performs a deprecated operation of archiving the resource at +// the specified path in the container identified by the given name. +func (daemon *Daemon) ContainerCopy(name string, res string) (io.ReadCloser, error) { + container, err := daemon.GetContainer(name) + if err != nil { + return nil, err + } + + if res[0] == '/' || res[0] == '\\' { + res = res[1:] + } + + return daemon.containerCopy(container, res) +} + +// ContainerStatPath stats the filesystem resource at the specified path in the +// container identified by the given name. +func (daemon *Daemon) ContainerStatPath(name string, path string) (stat *types.ContainerPathStat, err error) { + container, err := daemon.GetContainer(name) + if err != nil { + return nil, err + } + + return daemon.containerStatPath(container, path) +} + +// ContainerArchivePath creates an archive of the filesystem resource at the +// specified path in the container identified by the given name. Returns a +// tar archive of the resource and whether it was a directory or a single file. +func (daemon *Daemon) ContainerArchivePath(name string, path string) (content io.ReadCloser, stat *types.ContainerPathStat, err error) { + container, err := daemon.GetContainer(name) + if err != nil { + return nil, nil, err + } + + return daemon.containerArchivePath(container, path) +} + +// ContainerExtractToDir extracts the given archive to the specified location +// in the filesystem of the container identified by the given name. The given +// path must be of a directory in the container. If it is not, the error will +// be ErrExtractPointNotDirectory. If noOverwriteDirNonDir is true then it will +// be an error if unpacking the given content would cause an existing directory +// to be replaced with a non-directory and vice versa. +func (daemon *Daemon) ContainerExtractToDir(name, path string, noOverwriteDirNonDir bool, content io.Reader) error { + container, err := daemon.GetContainer(name) + if err != nil { + return err + } + + return daemon.containerExtractToDir(container, path, noOverwriteDirNonDir, content) +} + +// containerStatPath stats the filesystem resource at the specified path in this +// container. Returns stat info about the resource. +func (daemon *Daemon) containerStatPath(container *container.Container, path string) (stat *types.ContainerPathStat, err error) { + container.Lock() + defer container.Unlock() + + if err = daemon.Mount(container); err != nil { + return nil, err + } + defer daemon.Unmount(container) + + err = daemon.mountVolumes(container) + defer container.UnmountVolumes(true, daemon.LogVolumeEvent) + if err != nil { + return nil, err + } + + resolvedPath, absPath, err := container.ResolvePath(path) + if err != nil { + return nil, err + } + + return container.StatPath(resolvedPath, absPath) +} + +// containerArchivePath creates an archive of the filesystem resource at the specified +// path in this container. Returns a tar archive of the resource and stat info +// about the resource. +func (daemon *Daemon) containerArchivePath(container *container.Container, path string) (content io.ReadCloser, stat *types.ContainerPathStat, err error) { + container.Lock() + + defer func() { + if err != nil { + // Wait to unlock the container until the archive is fully read + // (see the ReadCloseWrapper func below) or if there is an error + // before that occurs. + container.Unlock() + } + }() + + if err = daemon.Mount(container); err != nil { + return nil, nil, err + } + + defer func() { + if err != nil { + // unmount any volumes + container.UnmountVolumes(true, daemon.LogVolumeEvent) + // unmount the container's rootfs + daemon.Unmount(container) + } + }() + + if err = daemon.mountVolumes(container); err != nil { + return nil, nil, err + } + + resolvedPath, absPath, err := container.ResolvePath(path) + if err != nil { + return nil, nil, err + } + + stat, err = container.StatPath(resolvedPath, absPath) + if err != nil { + return nil, nil, err + } + + // We need to rebase the archive entries if the last element of the + // resolved path was a symlink that was evaluated and is now different + // than the requested path. For example, if the given path was "/foo/bar/", + // but it resolved to "/var/lib/docker/containers/{id}/foo/baz/", we want + // to ensure that the archive entries start with "bar" and not "baz". This + // also catches the case when the root directory of the container is + // requested: we want the archive entries to start with "/" and not the + // container ID. + data, err := archive.TarResourceRebase(resolvedPath, filepath.Base(absPath)) + if err != nil { + return nil, nil, err + } + + content = ioutils.NewReadCloserWrapper(data, func() error { + err := data.Close() + container.UnmountVolumes(true, daemon.LogVolumeEvent) + daemon.Unmount(container) + container.Unlock() + return err + }) + + daemon.LogContainerEvent(container, "archive-path") + + return content, stat, nil +} + +// containerExtractToDir extracts the given tar archive to the specified location in the +// filesystem of this container. The given path must be of a directory in the +// container. If it is not, the error will be ErrExtractPointNotDirectory. If +// noOverwriteDirNonDir is true then it will be an error if unpacking the +// given content would cause an existing directory to be replaced with a non- +// directory and vice versa. +func (daemon *Daemon) containerExtractToDir(container *container.Container, path string, noOverwriteDirNonDir bool, content io.Reader) (err error) { + container.Lock() + defer container.Unlock() + + if err = daemon.Mount(container); err != nil { + return err + } + defer daemon.Unmount(container) + + err = daemon.mountVolumes(container) + defer container.UnmountVolumes(true, daemon.LogVolumeEvent) + if err != nil { + return err + } + + // The destination path needs to be resolved to a host path, with all + // symbolic links followed in the scope of the container's rootfs. Note + // that we do not use `container.ResolvePath(path)` here because we need + // to also evaluate the last path element if it is a symlink. This is so + // that you can extract an archive to a symlink that points to a directory. + + // Consider the given path as an absolute path in the container. + absPath := archive.PreserveTrailingDotOrSeparator(filepath.Join(string(filepath.Separator), path), path) + + // This will evaluate the last path element if it is a symlink. + resolvedPath, err := container.GetResourcePath(absPath) + if err != nil { + return err + } + + stat, err := os.Lstat(resolvedPath) + if err != nil { + return err + } + + if !stat.IsDir() { + return ErrExtractPointNotDirectory + } + + // Need to check if the path is in a volume. If it is, it cannot be in a + // read-only volume. If it is not in a volume, the container cannot be + // configured with a read-only rootfs. + + // Use the resolved path relative to the container rootfs as the new + // absPath. This way we fully follow any symlinks in a volume that may + // lead back outside the volume. + // + // The Windows implementation of filepath.Rel in golang 1.4 does not + // support volume style file path semantics. On Windows when using the + // filter driver, we are guaranteed that the path will always be + // a volume file path. + var baseRel string + if strings.HasPrefix(resolvedPath, `\\?\Volume{`) { + if strings.HasPrefix(resolvedPath, container.BaseFS) { + baseRel = resolvedPath[len(container.BaseFS):] + if baseRel[:1] == `\` { + baseRel = baseRel[1:] + } + } + } else { + baseRel, err = filepath.Rel(container.BaseFS, resolvedPath) + } + if err != nil { + return err + } + // Make it an absolute path. + absPath = filepath.Join(string(filepath.Separator), baseRel) + + toVolume, err := checkIfPathIsInAVolume(container, absPath) + if err != nil { + return err + } + + if !toVolume && container.HostConfig.ReadonlyRootfs { + return ErrRootFSReadOnly + } + + uid, gid := daemon.GetRemappedUIDGID() + options := &archive.TarOptions{ + NoOverwriteDirNonDir: noOverwriteDirNonDir, + ChownOpts: &archive.TarChownOptions{ + UID: uid, GID: gid, // TODO: should all ownership be set to root (either real or remapped)? + }, + } + if err := chrootarchive.Untar(content, resolvedPath, options); err != nil { + return err + } + + daemon.LogContainerEvent(container, "extract-to-dir") + + return nil +} + +func (daemon *Daemon) containerCopy(container *container.Container, resource string) (rc io.ReadCloser, err error) { + container.Lock() + + defer func() { + if err != nil { + // Wait to unlock the container until the archive is fully read + // (see the ReadCloseWrapper func below) or if there is an error + // before that occurs. + container.Unlock() + } + }() + + if err := daemon.Mount(container); err != nil { + return nil, err + } + + defer func() { + if err != nil { + // unmount any volumes + container.UnmountVolumes(true, daemon.LogVolumeEvent) + // unmount the container's rootfs + daemon.Unmount(container) + } + }() + + if err := daemon.mountVolumes(container); err != nil { + return nil, err + } + + basePath, err := container.GetResourcePath(resource) + if err != nil { + return nil, err + } + stat, err := os.Stat(basePath) + if err != nil { + return nil, err + } + var filter []string + if !stat.IsDir() { + d, f := filepath.Split(basePath) + basePath = d + filter = []string{f} + } else { + filter = []string{filepath.Base(basePath)} + basePath = filepath.Dir(basePath) + } + archive, err := archive.TarWithOptions(basePath, &archive.TarOptions{ + Compression: archive.Uncompressed, + IncludeFiles: filter, + }) + if err != nil { + return nil, err + } + + reader := ioutils.NewReadCloserWrapper(archive, func() error { + err := archive.Close() + container.UnmountVolumes(true, daemon.LogVolumeEvent) + daemon.Unmount(container) + container.Unlock() + return err + }) + daemon.LogContainerEvent(container, "copy") + return reader, nil +} + +// CopyOnBuild copies/extracts a source FileInfo to a destination path inside a container +// specified by a container object. +// TODO: make sure callers don't unnecessarily convert destPath with filepath.FromSlash (Copy does it already). +// CopyOnBuild should take in abstract paths (with slashes) and the implementation should convert it to OS-specific paths. +func (daemon *Daemon) CopyOnBuild(cID string, destPath string, src builder.FileInfo, decompress bool) error { + srcPath := src.Path() + destExists := true + destDir := false + rootUID, rootGID := daemon.GetRemappedUIDGID() + + // Work in daemon-local OS specific file paths + destPath = filepath.FromSlash(destPath) + + c, err := daemon.GetContainer(cID) + if err != nil { + return err + } + err = daemon.Mount(c) + if err != nil { + return err + } + defer daemon.Unmount(c) + + dest, err := c.GetResourcePath(destPath) + if err != nil { + return err + } + + // Preserve the trailing slash + // TODO: why are we appending another path separator if there was already one? + if strings.HasSuffix(destPath, string(os.PathSeparator)) || destPath == "." { + destDir = true + dest += string(os.PathSeparator) + } + + destPath = dest + + destStat, err := os.Stat(destPath) + if err != nil { + if !os.IsNotExist(err) { + //logrus.Errorf("Error performing os.Stat on %s. %s", destPath, err) + return err + } + destExists = false + } + + uidMaps, gidMaps := daemon.GetUIDGIDMaps() + archiver := &archive.Archiver{ + Untar: chrootarchive.Untar, + UIDMaps: uidMaps, + GIDMaps: gidMaps, + } + + if src.IsDir() { + // copy as directory + if err := archiver.CopyWithTar(srcPath, destPath); err != nil { + return err + } + return fixPermissions(srcPath, destPath, rootUID, rootGID, destExists) + } + if decompress && archive.IsArchivePath(srcPath) { + // Only try to untar if it is a file and that we've been told to decompress (when ADD-ing a remote file) + + // First try to unpack the source as an archive + // to support the untar feature we need to clean up the path a little bit + // because tar is very forgiving. First we need to strip off the archive's + // filename from the path but this is only added if it does not end in slash + tarDest := destPath + if strings.HasSuffix(tarDest, string(os.PathSeparator)) { + tarDest = filepath.Dir(destPath) + } + + // try to successfully untar the orig + err := archiver.UntarPath(srcPath, tarDest) + /* + if err != nil { + logrus.Errorf("Couldn't untar to %s: %v", tarDest, err) + } + */ + return err + } + + // only needed for fixPermissions, but might as well put it before CopyFileWithTar + if destDir || (destExists && destStat.IsDir()) { + destPath = filepath.Join(destPath, src.Name()) + } + + if err := idtools.MkdirAllNewAs(filepath.Dir(destPath), 0755, rootUID, rootGID); err != nil { + return err + } + if err := archiver.CopyFileWithTar(srcPath, destPath); err != nil { + return err + } + + return fixPermissions(srcPath, destPath, rootUID, rootGID, destExists) +} diff --git a/daemon/archive_unix.go b/daemon/archive_unix.go new file mode 100644 index 00000000..fcea13c9 --- /dev/null +++ b/daemon/archive_unix.go @@ -0,0 +1,57 @@ +// +build !windows + +package daemon + +import ( + "github.com/docker/docker/container" + "os" + "path/filepath" +) + +// checkIfPathIsInAVolume checks if the path is in a volume. If it is, it +// cannot be in a read-only volume. If it is not in a volume, the container +// cannot be configured with a read-only rootfs. +func checkIfPathIsInAVolume(container *container.Container, absPath string) (bool, error) { + var toVolume bool + for _, mnt := range container.MountPoints { + if toVolume = mnt.HasResource(absPath); toVolume { + if mnt.RW { + break + } + return false, ErrVolumeReadonly + } + } + return toVolume, nil +} + +func fixPermissions(source, destination string, uid, gid int, destExisted bool) error { + // If the destination didn't already exist, or the destination isn't a + // directory, then we should Lchown the destination. Otherwise, we shouldn't + // Lchown the destination. + destStat, err := os.Stat(destination) + if err != nil { + // This should *never* be reached, because the destination must've already + // been created while untar-ing the context. + return err + } + doChownDestination := !destExisted || !destStat.IsDir() + + // We Walk on the source rather than on the destination because we don't + // want to change permissions on things we haven't created or modified. + return filepath.Walk(source, func(fullpath string, info os.FileInfo, err error) error { + // Do not alter the walk root iff. it existed before, as it doesn't fall under + // the domain of "things we should chown". + if !doChownDestination && (source == fullpath) { + return nil + } + + // Path is prefixed by source: substitute with destination instead. + cleaned, err := filepath.Rel(source, fullpath) + if err != nil { + return err + } + + fullpath = filepath.Join(destination, cleaned) + return os.Lchown(fullpath, uid, gid) + }) +} diff --git a/daemon/archive_windows.go b/daemon/archive_windows.go new file mode 100644 index 00000000..4cefb8de --- /dev/null +++ b/daemon/archive_windows.go @@ -0,0 +1,18 @@ +package daemon + +import "github.com/docker/docker/container" + +// checkIfPathIsInAVolume checks if the path is in a volume. If it is, it +// cannot be in a read-only volume. If it is not in a volume, the container +// cannot be configured with a read-only rootfs. +// +// This is a no-op on Windows which does not support read-only volumes, or +// extracting to a mount point inside a volume. TODO Windows: FIXME Post-TP4 +func checkIfPathIsInAVolume(container *container.Container, absPath string) (bool, error) { + return false, nil +} + +func fixPermissions(source, destination string, uid, gid int, destExisted bool) error { + // chown is not supported on Windows + return nil +} diff --git a/daemon/attach.go b/daemon/attach.go new file mode 100644 index 00000000..79e9cd51 --- /dev/null +++ b/daemon/attach.go @@ -0,0 +1,120 @@ +package daemon + +import ( + "fmt" + "io" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/types/backend" + "github.com/docker/docker/container" + "github.com/docker/docker/daemon/logger" + "github.com/docker/docker/errors" + "github.com/docker/docker/pkg/stdcopy" +) + +// ContainerAttach attaches to logs according to the config passed in. See ContainerAttachConfig. +func (daemon *Daemon) ContainerAttach(prefixOrName string, c *backend.ContainerAttachConfig) error { + container, err := daemon.GetContainer(prefixOrName) + if err != nil { + return err + } + if container.IsPaused() { + err := fmt.Errorf("Container %s is paused. Unpause the container before attach", prefixOrName) + return errors.NewRequestConflictError(err) + } + + inStream, outStream, errStream, err := c.GetStreams() + if err != nil { + return err + } + defer inStream.Close() + + if !container.Config.Tty && c.MuxStreams { + errStream = stdcopy.NewStdWriter(errStream, stdcopy.Stderr) + outStream = stdcopy.NewStdWriter(outStream, stdcopy.Stdout) + } + + var stdin io.ReadCloser + var stdout, stderr io.Writer + + if c.UseStdin { + stdin = inStream + } + if c.UseStdout { + stdout = outStream + } + if c.UseStderr { + stderr = errStream + } + + if err := daemon.containerAttach(container, stdin, stdout, stderr, c.Logs, c.Stream, c.DetachKeys); err != nil { + fmt.Fprintf(outStream, "Error attaching: %s\n", err) + } + return nil +} + +// ContainerAttachRaw attaches the provided streams to the container's stdio +func (daemon *Daemon) ContainerAttachRaw(prefixOrName string, stdin io.ReadCloser, stdout, stderr io.Writer, stream bool) error { + container, err := daemon.GetContainer(prefixOrName) + if err != nil { + return err + } + return daemon.containerAttach(container, stdin, stdout, stderr, false, stream, nil) +} + +func (daemon *Daemon) containerAttach(container *container.Container, stdin io.ReadCloser, stdout, stderr io.Writer, logs, stream bool, keys []byte) error { + if logs { + logDriver, err := daemon.getLogger(container) + if err != nil { + return err + } + cLog, ok := logDriver.(logger.LogReader) + if !ok { + return logger.ErrReadLogsNotSupported + } + logs := cLog.ReadLogs(logger.ReadConfig{Tail: -1}) + + LogLoop: + for { + select { + case msg, ok := <-logs.Msg: + if !ok { + break LogLoop + } + if msg.Source == "stdout" && stdout != nil { + stdout.Write(msg.Line) + } + if msg.Source == "stderr" && stderr != nil { + stderr.Write(msg.Line) + } + case err := <-logs.Err: + logrus.Errorf("Error streaming logs: %v", err) + break LogLoop + } + } + } + + daemon.LogContainerEvent(container, "attach") + + //stream + if stream { + var stdinPipe io.ReadCloser + if stdin != nil { + r, w := io.Pipe() + go func() { + defer w.Close() + defer logrus.Debugf("Closing buffered stdin pipe") + io.Copy(w, stdin) + }() + stdinPipe = r + } + <-container.Attach(stdinPipe, stdout, stderr, keys) + // If we are in stdinonce mode, wait for the process to end + // otherwise, simply return + if container.Config.StdinOnce && !container.Config.Tty { + container.WaitStop(-1 * time.Second) + } + } + return nil +} diff --git a/daemon/caps/utils_unix.go b/daemon/caps/utils_unix.go new file mode 100644 index 00000000..c99485f5 --- /dev/null +++ b/daemon/caps/utils_unix.go @@ -0,0 +1,131 @@ +// +build !windows + +package caps + +import ( + "fmt" + "strings" + + "github.com/docker/docker/pkg/stringutils" + "github.com/syndtr/gocapability/capability" +) + +var capabilityList Capabilities + +func init() { + last := capability.CAP_LAST_CAP + // hack for RHEL6 which has no /proc/sys/kernel/cap_last_cap + if last == capability.Cap(63) { + last = capability.CAP_BLOCK_SUSPEND + } + for _, cap := range capability.List() { + if cap > last { + continue + } + capabilityList = append(capabilityList, + &CapabilityMapping{ + Key: "CAP_" + strings.ToUpper(cap.String()), + Value: cap, + }, + ) + } +} + +type ( + // CapabilityMapping maps linux capability name to its value of capability.Cap type + // Capabilities is one of the security systems in Linux Security Module (LSM) + // framework provided by the kernel. + // For more details on capabilities, see http://man7.org/linux/man-pages/man7/capabilities.7.html + CapabilityMapping struct { + Key string `json:"key,omitempty"` + Value capability.Cap `json:"value,omitempty"` + } + // Capabilities contains all CapabilityMapping + Capabilities []*CapabilityMapping +) + +// String returns of CapabilityMapping +func (c *CapabilityMapping) String() string { + return c.Key +} + +// GetCapability returns CapabilityMapping which contains specific key +func GetCapability(key string) *CapabilityMapping { + for _, capp := range capabilityList { + if capp.Key == key { + cpy := *capp + return &cpy + } + } + return nil +} + +// GetAllCapabilities returns all of the capabilities +func GetAllCapabilities() []string { + output := make([]string, len(capabilityList)) + for i, capability := range capabilityList { + output[i] = capability.String() + } + return output +} + +// TweakCapabilities can tweak capabilities by adding or dropping capabilities +// based on the basics capabilities. +func TweakCapabilities(basics, adds, drops []string) ([]string, error) { + var ( + newCaps []string + allCaps = GetAllCapabilities() + ) + + // FIXME(tonistiigi): docker format is without CAP_ prefix, oci is with prefix + // Currently they are mixed in here. We should do conversion in one place. + + // look for invalid cap in the drop list + for _, cap := range drops { + if strings.ToLower(cap) == "all" { + continue + } + + if !stringutils.InSlice(allCaps, "CAP_"+cap) { + return nil, fmt.Errorf("Unknown capability drop: %q", cap) + } + } + + // handle --cap-add=all + if stringutils.InSlice(adds, "all") { + basics = allCaps + } + + if !stringutils.InSlice(drops, "all") { + for _, cap := range basics { + // skip `all` already handled above + if strings.ToLower(cap) == "all" { + continue + } + + // if we don't drop `all`, add back all the non-dropped caps + if !stringutils.InSlice(drops, cap[4:]) { + newCaps = append(newCaps, strings.ToUpper(cap)) + } + } + } + + for _, cap := range adds { + // skip `all` already handled above + if strings.ToLower(cap) == "all" { + continue + } + + cap = "CAP_" + cap + + if !stringutils.InSlice(allCaps, cap) { + return nil, fmt.Errorf("Unknown capability to add: %q", cap) + } + + // add cap if not already in the list + if !stringutils.InSlice(newCaps, cap) { + newCaps = append(newCaps, strings.ToUpper(cap)) + } + } + return newCaps, nil +} diff --git a/daemon/changes.go b/daemon/changes.go new file mode 100644 index 00000000..5bc5b9d5 --- /dev/null +++ b/daemon/changes.go @@ -0,0 +1,15 @@ +package daemon + +import "github.com/docker/docker/pkg/archive" + +// ContainerChanges returns a list of container fs changes +func (daemon *Daemon) ContainerChanges(name string) ([]archive.Change, error) { + container, err := daemon.GetContainer(name) + if err != nil { + return nil, err + } + + container.Lock() + defer container.Unlock() + return daemon.changes(container) +} diff --git a/daemon/commit.go b/daemon/commit.go new file mode 100644 index 00000000..7cdf80c7 --- /dev/null +++ b/daemon/commit.go @@ -0,0 +1,233 @@ +package daemon + +import ( + "encoding/json" + "fmt" + "runtime" + "strings" + "time" + + "github.com/docker/docker/container" + "github.com/docker/docker/dockerversion" + "github.com/docker/docker/image" + "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/reference" + "github.com/docker/engine-api/types" + containertypes "github.com/docker/engine-api/types/container" + "github.com/docker/go-connections/nat" +) + +// merge merges two Config, the image container configuration (defaults values), +// and the user container configuration, either passed by the API or generated +// by the cli. +// It will mutate the specified user configuration (userConf) with the image +// configuration where the user configuration is incomplete. +func merge(userConf, imageConf *containertypes.Config) error { + if userConf.User == "" { + userConf.User = imageConf.User + } + if len(userConf.ExposedPorts) == 0 { + userConf.ExposedPorts = imageConf.ExposedPorts + } else if imageConf.ExposedPorts != nil { + if userConf.ExposedPorts == nil { + userConf.ExposedPorts = make(nat.PortSet) + } + for port := range imageConf.ExposedPorts { + if _, exists := userConf.ExposedPorts[port]; !exists { + userConf.ExposedPorts[port] = struct{}{} + } + } + } + + if len(userConf.Env) == 0 { + userConf.Env = imageConf.Env + } else { + for _, imageEnv := range imageConf.Env { + found := false + imageEnvKey := strings.Split(imageEnv, "=")[0] + for _, userEnv := range userConf.Env { + userEnvKey := strings.Split(userEnv, "=")[0] + if imageEnvKey == userEnvKey { + found = true + break + } + } + if !found { + userConf.Env = append(userConf.Env, imageEnv) + } + } + } + + if userConf.Labels == nil { + userConf.Labels = map[string]string{} + } + if imageConf.Labels != nil { + for l := range userConf.Labels { + imageConf.Labels[l] = userConf.Labels[l] + } + userConf.Labels = imageConf.Labels + } + + if len(userConf.Entrypoint) == 0 { + if len(userConf.Cmd) == 0 { + userConf.Cmd = imageConf.Cmd + } + + if userConf.Entrypoint == nil { + userConf.Entrypoint = imageConf.Entrypoint + } + } + if userConf.WorkingDir == "" { + userConf.WorkingDir = imageConf.WorkingDir + } + if len(userConf.Volumes) == 0 { + userConf.Volumes = imageConf.Volumes + } else { + for k, v := range imageConf.Volumes { + userConf.Volumes[k] = v + } + } + + if userConf.StopSignal == "" { + userConf.StopSignal = imageConf.StopSignal + } + return nil +} + +// Commit creates a new filesystem image from the current state of a container. +// The image can optionally be tagged into a repository. +func (daemon *Daemon) Commit(name string, c *types.ContainerCommitConfig) (string, error) { + container, err := daemon.GetContainer(name) + if err != nil { + return "", err + } + + // It is not possible to commit a running container on Windows + if runtime.GOOS == "windows" && container.IsRunning() { + return "", fmt.Errorf("Windows does not support commit of a running container") + } + + if c.Pause && !container.IsPaused() { + daemon.containerPause(container) + defer daemon.containerUnpause(container) + } + + if c.MergeConfigs { + if err := merge(c.Config, container.Config); err != nil { + return "", err + } + } + + rwTar, err := daemon.exportContainerRw(container) + if err != nil { + return "", err + } + defer func() { + if rwTar != nil { + rwTar.Close() + } + }() + + var history []image.History + rootFS := image.NewRootFS() + + if container.ImageID != "" { + img, err := daemon.imageStore.Get(container.ImageID) + if err != nil { + return "", err + } + history = img.History + rootFS = img.RootFS + } + + l, err := daemon.layerStore.Register(rwTar, rootFS.ChainID()) + if err != nil { + return "", err + } + defer layer.ReleaseAndLog(daemon.layerStore, l) + + h := image.History{ + Author: c.Author, + Created: time.Now().UTC(), + CreatedBy: strings.Join(container.Config.Cmd, " "), + Comment: c.Comment, + EmptyLayer: true, + } + + if diffID := l.DiffID(); layer.DigestSHA256EmptyTar != diffID { + h.EmptyLayer = false + rootFS.Append(diffID) + } + + history = append(history, h) + + config, err := json.Marshal(&image.Image{ + V1Image: image.V1Image{ + DockerVersion: dockerversion.Version, + Config: c.Config, + Architecture: runtime.GOARCH, + OS: runtime.GOOS, + Container: container.ID, + ContainerConfig: *container.Config, + Author: c.Author, + Created: h.Created, + }, + RootFS: rootFS, + History: history, + }) + + if err != nil { + return "", err + } + + id, err := daemon.imageStore.Create(config) + if err != nil { + return "", err + } + + if container.ImageID != "" { + if err := daemon.imageStore.SetParent(id, container.ImageID); err != nil { + return "", err + } + } + + if c.Repo != "" { + newTag, err := reference.WithName(c.Repo) // todo: should move this to API layer + if err != nil { + return "", err + } + if c.Tag != "" { + if newTag, err = reference.WithTag(newTag, c.Tag); err != nil { + return "", err + } + } + if err := daemon.TagImage(newTag, id.String()); err != nil { + return "", err + } + } + + attributes := map[string]string{ + "comment": c.Comment, + } + daemon.LogContainerEventWithAttributes(container, "commit", attributes) + return id.String(), nil +} + +func (daemon *Daemon) exportContainerRw(container *container.Container) (archive.Archive, error) { + if err := daemon.Mount(container); err != nil { + return nil, err + } + + archive, err := container.RWLayer.TarStream() + if err != nil { + daemon.Unmount(container) // logging is already handled in the `Unmount` function + return nil, err + } + return ioutils.NewReadCloserWrapper(archive, func() error { + archive.Close() + return container.RWLayer.Unmount() + }), + nil +} diff --git a/daemon/config.go b/daemon/config.go new file mode 100644 index 00000000..5e9d5e51 --- /dev/null +++ b/daemon/config.go @@ -0,0 +1,374 @@ +package daemon + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "strings" + "sync" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/discovery" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/registry" + "github.com/imdario/mergo" +) + +const ( + defaultNetworkMtu = 1500 + disableNetworkBridge = "none" +) + +// flatOptions contains configuration keys +// that MUST NOT be parsed as deep structures. +// Use this to differentiate these options +// with others like the ones in CommonTLSOptions. +var flatOptions = map[string]bool{ + "cluster-store-opts": true, + "log-opts": true, +} + +// LogConfig represents the default log configuration. +// It includes json tags to deserialize configuration from a file +// using the same names that the flags in the command line uses. +type LogConfig struct { + Type string `json:"log-driver,omitempty"` + Config map[string]string `json:"log-opts,omitempty"` +} + +// CommonTLSOptions defines TLS configuration for the daemon server. +// It includes json tags to deserialize configuration from a file +// using the same names that the flags in the command line uses. +type CommonTLSOptions struct { + CAFile string `json:"tlscacert,omitempty"` + CertFile string `json:"tlscert,omitempty"` + KeyFile string `json:"tlskey,omitempty"` +} + +// CommonConfig defines the configuration of a docker daemon which are +// common across platforms. +// It includes json tags to deserialize configuration from a file +// using the same names that the flags in the command line uses. +type CommonConfig struct { + AuthorizationPlugins []string `json:"authorization-plugins,omitempty"` // AuthorizationPlugins holds list of authorization plugins + AutoRestart bool `json:"-"` + Context map[string][]string `json:"-"` + DisableBridge bool `json:"-"` + DNS []string `json:"dns,omitempty"` + DNSOptions []string `json:"dns-opts,omitempty"` + DNSSearch []string `json:"dns-search,omitempty"` + ExecOptions []string `json:"exec-opts,omitempty"` + ExecRoot string `json:"exec-root,omitempty"` + GraphDriver string `json:"storage-driver,omitempty"` + GraphOptions []string `json:"storage-opts,omitempty"` + Labels []string `json:"labels,omitempty"` + Mtu int `json:"mtu,omitempty"` + Pidfile string `json:"pidfile,omitempty"` + RawLogs bool `json:"raw-logs,omitempty"` + Root string `json:"graph,omitempty"` + SocketGroup string `json:"group,omitempty"` + TrustKeyPath string `json:"-"` + + // ClusterStore is the storage backend used for the cluster information. It is used by both + // multihost networking (to store networks and endpoints information) and by the node discovery + // mechanism. + ClusterStore string `json:"cluster-store,omitempty"` + + // ClusterOpts is used to pass options to the discovery package for tuning libkv settings, such + // as TLS configuration settings. + ClusterOpts map[string]string `json:"cluster-store-opts,omitempty"` + + // ClusterAdvertise is the network endpoint that the Engine advertises for the purpose of node + // discovery. This should be a 'host:port' combination on which that daemon instance is + // reachable by other hosts. + ClusterAdvertise string `json:"cluster-advertise,omitempty"` + + Debug bool `json:"debug,omitempty"` + Hosts []string `json:"hosts,omitempty"` + LogLevel string `json:"log-level,omitempty"` + TLS bool `json:"tls,omitempty"` + TLSVerify bool `json:"tlsverify,omitempty"` + + // Embedded structs that allow config + // deserialization without the full struct. + CommonTLSOptions + LogConfig + bridgeConfig // bridgeConfig holds bridge network specific configuration. + registry.ServiceOptions + + reloadLock sync.Mutex + valuesSet map[string]interface{} +} + +// InstallCommonFlags adds command-line options to the top-level flag parser for +// the current process. +// Subsequent calls to `flag.Parse` will populate config with values parsed +// from the command-line. +func (config *Config) InstallCommonFlags(cmd *flag.FlagSet, usageFn func(string) string) { + config.ServiceOptions.InstallCliFlags(cmd, usageFn) + + cmd.Var(opts.NewNamedListOptsRef("storage-opts", &config.GraphOptions, nil), []string{"-storage-opt"}, usageFn("Set storage driver options")) + cmd.Var(opts.NewNamedListOptsRef("authorization-plugins", &config.AuthorizationPlugins, nil), []string{"-authorization-plugin"}, usageFn("List authorization plugins in order from first evaluator to last")) + cmd.Var(opts.NewNamedListOptsRef("exec-opts", &config.ExecOptions, nil), []string{"-exec-opt"}, usageFn("Set runtime execution options")) + cmd.StringVar(&config.Pidfile, []string{"p", "-pidfile"}, defaultPidFile, usageFn("Path to use for daemon PID file")) + cmd.StringVar(&config.Root, []string{"g", "-graph"}, defaultGraph, usageFn("Root of the Docker runtime")) + cmd.StringVar(&config.ExecRoot, []string{"-exec-root"}, defaultExecRoot, usageFn("Root directory for execution state files")) + cmd.BoolVar(&config.AutoRestart, []string{"#r", "#-restart"}, true, usageFn("--restart on the daemon has been deprecated in favor of --restart policies on docker run")) + cmd.StringVar(&config.GraphDriver, []string{"s", "-storage-driver"}, "", usageFn("Storage driver to use")) + cmd.IntVar(&config.Mtu, []string{"#mtu", "-mtu"}, 0, usageFn("Set the containers network MTU")) + cmd.BoolVar(&config.RawLogs, []string{"-raw-logs"}, false, usageFn("Full timestamps without ANSI coloring")) + // FIXME: why the inconsistency between "hosts" and "sockets"? + cmd.Var(opts.NewListOptsRef(&config.DNS, opts.ValidateIPAddress), []string{"#dns", "-dns"}, usageFn("DNS server to use")) + cmd.Var(opts.NewNamedListOptsRef("dns-opts", &config.DNSOptions, nil), []string{"-dns-opt"}, usageFn("DNS options to use")) + cmd.Var(opts.NewListOptsRef(&config.DNSSearch, opts.ValidateDNSSearch), []string{"-dns-search"}, usageFn("DNS search domains to use")) + cmd.Var(opts.NewNamedListOptsRef("labels", &config.Labels, opts.ValidateLabel), []string{"-label"}, usageFn("Set key=value labels to the daemon")) + cmd.StringVar(&config.LogConfig.Type, []string{"-log-driver"}, "json-file", usageFn("Default driver for container logs")) + cmd.Var(opts.NewNamedMapOpts("log-opts", config.LogConfig.Config, nil), []string{"-log-opt"}, usageFn("Set log driver options")) + cmd.StringVar(&config.ClusterAdvertise, []string{"-cluster-advertise"}, "", usageFn("Address or interface name to advertise")) + cmd.StringVar(&config.ClusterStore, []string{"-cluster-store"}, "", usageFn("Set the cluster store")) + cmd.Var(opts.NewNamedMapOpts("cluster-store-opts", config.ClusterOpts, nil), []string{"-cluster-store-opt"}, usageFn("Set cluster store options")) +} + +// IsValueSet returns true if a configuration value +// was explicitly set in the configuration file. +func (config *Config) IsValueSet(name string) bool { + if config.valuesSet == nil { + return false + } + _, ok := config.valuesSet[name] + return ok +} + +func parseClusterAdvertiseSettings(clusterStore, clusterAdvertise string) (string, error) { + if clusterAdvertise == "" { + return "", errDiscoveryDisabled + } + if clusterStore == "" { + return "", fmt.Errorf("invalid cluster configuration. --cluster-advertise must be accompanied by --cluster-store configuration") + } + + advertise, err := discovery.ParseAdvertise(clusterAdvertise) + if err != nil { + return "", fmt.Errorf("discovery advertise parsing failed (%v)", err) + } + return advertise, nil +} + +// ReloadConfiguration reads the configuration in the host and reloads the daemon and server. +func ReloadConfiguration(configFile string, flags *flag.FlagSet, reload func(*Config)) error { + logrus.Infof("Got signal to reload configuration, reloading from: %s", configFile) + newConfig, err := getConflictFreeConfiguration(configFile, flags) + if err != nil { + return err + } + + if err := validateConfiguration(newConfig); err != nil { + return fmt.Errorf("file configuration validation failed (%v)", err) + } + + reload(newConfig) + return nil +} + +// boolValue is an interface that boolean value flags implement +// to tell the command line how to make -name equivalent to -name=true. +type boolValue interface { + IsBoolFlag() bool +} + +// MergeDaemonConfigurations reads a configuration file, +// loads the file configuration in an isolated structure, +// and merges the configuration provided from flags on top +// if there are no conflicts. +func MergeDaemonConfigurations(flagsConfig *Config, flags *flag.FlagSet, configFile string) (*Config, error) { + fileConfig, err := getConflictFreeConfiguration(configFile, flags) + if err != nil { + return nil, err + } + + if err := validateConfiguration(fileConfig); err != nil { + return nil, fmt.Errorf("file configuration validation failed (%v)", err) + } + + // merge flags configuration on top of the file configuration + if err := mergo.Merge(fileConfig, flagsConfig); err != nil { + return nil, err + } + + return fileConfig, nil +} + +// getConflictFreeConfiguration loads the configuration from a JSON file. +// It compares that configuration with the one provided by the flags, +// and returns an error if there are conflicts. +func getConflictFreeConfiguration(configFile string, flags *flag.FlagSet) (*Config, error) { + b, err := ioutil.ReadFile(configFile) + if err != nil { + return nil, err + } + + var config Config + var reader io.Reader + if flags != nil { + var jsonConfig map[string]interface{} + reader = bytes.NewReader(b) + if err := json.NewDecoder(reader).Decode(&jsonConfig); err != nil { + return nil, err + } + + configSet := configValuesSet(jsonConfig) + + if err := findConfigurationConflicts(configSet, flags); err != nil { + return nil, err + } + + // Override flag values to make sure the values set in the config file with nullable values, like `false`, + // are not overriden by default truthy values from the flags that were not explicitly set. + // See https://github.com/docker/docker/issues/20289 for an example. + // + // TODO: Rewrite configuration logic to avoid same issue with other nullable values, like numbers. + namedOptions := make(map[string]interface{}) + for key, value := range configSet { + f := flags.Lookup("-" + key) + if f == nil { // ignore named flags that don't match + namedOptions[key] = value + continue + } + + if _, ok := f.Value.(boolValue); ok { + f.Value.Set(fmt.Sprintf("%v", value)) + } + } + if len(namedOptions) > 0 { + // set also default for mergeVal flags that are boolValue at the same time. + flags.VisitAll(func(f *flag.Flag) { + if opt, named := f.Value.(opts.NamedOption); named { + v, set := namedOptions[opt.Name()] + _, boolean := f.Value.(boolValue) + if set && boolean { + f.Value.Set(fmt.Sprintf("%v", v)) + } + } + }) + } + + config.valuesSet = configSet + } + + reader = bytes.NewReader(b) + err = json.NewDecoder(reader).Decode(&config) + return &config, err +} + +// configValuesSet returns the configuration values explicitly set in the file. +func configValuesSet(config map[string]interface{}) map[string]interface{} { + flatten := make(map[string]interface{}) + for k, v := range config { + if m, isMap := v.(map[string]interface{}); isMap && !flatOptions[k] { + for km, vm := range m { + flatten[km] = vm + } + continue + } + + flatten[k] = v + } + return flatten +} + +// findConfigurationConflicts iterates over the provided flags searching for +// duplicated configurations and unknown keys. It returns an error with all the conflicts if +// it finds any. +func findConfigurationConflicts(config map[string]interface{}, flags *flag.FlagSet) error { + // 1. Search keys from the file that we don't recognize as flags. + unknownKeys := make(map[string]interface{}) + for key, value := range config { + flagName := "-" + key + if flag := flags.Lookup(flagName); flag == nil { + unknownKeys[key] = value + } + } + + // 2. Discard values that implement NamedOption. + // Their configuration name differs from their flag name, like `labels` and `label`. + if len(unknownKeys) > 0 { + unknownNamedConflicts := func(f *flag.Flag) { + if namedOption, ok := f.Value.(opts.NamedOption); ok { + if _, valid := unknownKeys[namedOption.Name()]; valid { + delete(unknownKeys, namedOption.Name()) + } + } + } + flags.VisitAll(unknownNamedConflicts) + } + + if len(unknownKeys) > 0 { + var unknown []string + for key := range unknownKeys { + unknown = append(unknown, key) + } + return fmt.Errorf("the following directives don't match any configuration option: %s", strings.Join(unknown, ", ")) + } + + var conflicts []string + printConflict := func(name string, flagValue, fileValue interface{}) string { + return fmt.Sprintf("%s: (from flag: %v, from file: %v)", name, flagValue, fileValue) + } + + // 3. Search keys that are present as a flag and as a file option. + duplicatedConflicts := func(f *flag.Flag) { + // search option name in the json configuration payload if the value is a named option + if namedOption, ok := f.Value.(opts.NamedOption); ok { + if optsValue, ok := config[namedOption.Name()]; ok { + conflicts = append(conflicts, printConflict(namedOption.Name(), f.Value.String(), optsValue)) + } + } else { + // search flag name in the json configuration payload without trailing dashes + for _, name := range f.Names { + name = strings.TrimLeft(name, "-") + + if value, ok := config[name]; ok { + conflicts = append(conflicts, printConflict(name, f.Value.String(), value)) + break + } + } + } + } + + flags.Visit(duplicatedConflicts) + + if len(conflicts) > 0 { + return fmt.Errorf("the following directives are specified both as a flag and in the configuration file: %s", strings.Join(conflicts, ", ")) + } + return nil +} + +// validateConfiguration validates some specific configs. +// such as config.DNS, config.Labels, config.DNSSearch +func validateConfiguration(config *Config) error { + // validate DNS + for _, dns := range config.DNS { + if _, err := opts.ValidateIPAddress(dns); err != nil { + return err + } + } + + // validate DNSSearch + for _, dnsSearch := range config.DNSSearch { + if _, err := opts.ValidateDNSSearch(dnsSearch); err != nil { + return err + } + } + + // validate Labels + for _, label := range config.Labels { + if _, err := opts.ValidateLabel(label); err != nil { + return err + } + } + + return nil +} diff --git a/daemon/config_experimental.go b/daemon/config_experimental.go new file mode 100644 index 00000000..ceb7c382 --- /dev/null +++ b/daemon/config_experimental.go @@ -0,0 +1,8 @@ +// +build experimental + +package daemon + +import flag "github.com/docker/docker/pkg/mflag" + +func (config *Config) attachExperimentalFlags(cmd *flag.FlagSet, usageFn func(string) string) { +} diff --git a/daemon/config_stub.go b/daemon/config_stub.go new file mode 100644 index 00000000..796e6b6e --- /dev/null +++ b/daemon/config_stub.go @@ -0,0 +1,8 @@ +// +build !experimental + +package daemon + +import flag "github.com/docker/docker/pkg/mflag" + +func (config *Config) attachExperimentalFlags(cmd *flag.FlagSet, usageFn func(string) string) { +} diff --git a/daemon/config_test.go b/daemon/config_test.go new file mode 100644 index 00000000..7647f4d3 --- /dev/null +++ b/daemon/config_test.go @@ -0,0 +1,278 @@ +package daemon + +import ( + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/mflag" +) + +func TestDaemonConfigurationMerge(t *testing.T) { + f, err := ioutil.TempFile("", "docker-config-") + if err != nil { + t.Fatal(err) + } + + configFile := f.Name() + f.Write([]byte(`{"debug": true}`)) + f.Close() + + c := &Config{ + CommonConfig: CommonConfig{ + AutoRestart: true, + LogConfig: LogConfig{ + Type: "syslog", + Config: map[string]string{"tag": "test"}, + }, + }, + } + + cc, err := MergeDaemonConfigurations(c, nil, configFile) + if err != nil { + t.Fatal(err) + } + if !cc.Debug { + t.Fatalf("expected %v, got %v\n", true, cc.Debug) + } + if !cc.AutoRestart { + t.Fatalf("expected %v, got %v\n", true, cc.AutoRestart) + } + if cc.LogConfig.Type != "syslog" { + t.Fatalf("expected syslog config, got %q\n", cc.LogConfig) + } +} + +func TestDaemonConfigurationNotFound(t *testing.T) { + _, err := MergeDaemonConfigurations(&Config{}, nil, "/tmp/foo-bar-baz-docker") + if err == nil || !os.IsNotExist(err) { + t.Fatalf("expected does not exist error, got %v", err) + } +} + +func TestDaemonBrokenConfiguration(t *testing.T) { + f, err := ioutil.TempFile("", "docker-config-") + if err != nil { + t.Fatal(err) + } + + configFile := f.Name() + f.Write([]byte(`{"Debug": tru`)) + f.Close() + + _, err = MergeDaemonConfigurations(&Config{}, nil, configFile) + if err == nil { + t.Fatalf("expected error, got %v", err) + } +} + +func TestParseClusterAdvertiseSettings(t *testing.T) { + _, err := parseClusterAdvertiseSettings("something", "") + if err != errDiscoveryDisabled { + t.Fatalf("expected discovery disabled error, got %v\n", err) + } + + _, err = parseClusterAdvertiseSettings("", "something") + if err == nil { + t.Fatalf("expected discovery store error, got %v\n", err) + } + + _, err = parseClusterAdvertiseSettings("etcd", "127.0.0.1:8080") + if err != nil { + t.Fatal(err) + } +} + +func TestFindConfigurationConflicts(t *testing.T) { + config := map[string]interface{}{"authorization-plugins": "foobar"} + flags := mflag.NewFlagSet("test", mflag.ContinueOnError) + + flags.String([]string{"-authorization-plugins"}, "", "") + if err := flags.Set("-authorization-plugins", "asdf"); err != nil { + t.Fatal(err) + } + + err := findConfigurationConflicts(config, flags) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "authorization-plugins: (from flag: asdf, from file: foobar)") { + t.Fatalf("expected authorization-plugins conflict, got %v", err) + } +} + +func TestFindConfigurationConflictsWithNamedOptions(t *testing.T) { + config := map[string]interface{}{"hosts": []string{"qwer"}} + flags := mflag.NewFlagSet("test", mflag.ContinueOnError) + + var hosts []string + flags.Var(opts.NewNamedListOptsRef("hosts", &hosts, opts.ValidateHost), []string{"H", "-host"}, "Daemon socket(s) to connect to") + if err := flags.Set("-host", "tcp://127.0.0.1:4444"); err != nil { + t.Fatal(err) + } + if err := flags.Set("H", "unix:///var/run/docker.sock"); err != nil { + t.Fatal(err) + } + + err := findConfigurationConflicts(config, flags) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "hosts") { + t.Fatalf("expected hosts conflict, got %v", err) + } +} + +func TestDaemonConfigurationMergeConflicts(t *testing.T) { + f, err := ioutil.TempFile("", "docker-config-") + if err != nil { + t.Fatal(err) + } + + configFile := f.Name() + f.Write([]byte(`{"debug": true}`)) + f.Close() + + flags := mflag.NewFlagSet("test", mflag.ContinueOnError) + flags.Bool([]string{"debug"}, false, "") + flags.Set("debug", "false") + + _, err = MergeDaemonConfigurations(&Config{}, flags, configFile) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "debug") { + t.Fatalf("expected debug conflict, got %v", err) + } +} + +func TestDaemonConfigurationMergeConflictsWithInnerStructs(t *testing.T) { + f, err := ioutil.TempFile("", "docker-config-") + if err != nil { + t.Fatal(err) + } + + configFile := f.Name() + f.Write([]byte(`{"tlscacert": "/etc/certificates/ca.pem"}`)) + f.Close() + + flags := mflag.NewFlagSet("test", mflag.ContinueOnError) + flags.String([]string{"tlscacert"}, "", "") + flags.Set("tlscacert", "~/.docker/ca.pem") + + _, err = MergeDaemonConfigurations(&Config{}, flags, configFile) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "tlscacert") { + t.Fatalf("expected tlscacert conflict, got %v", err) + } +} + +func TestFindConfigurationConflictsWithUnknownKeys(t *testing.T) { + config := map[string]interface{}{"tls-verify": "true"} + flags := mflag.NewFlagSet("test", mflag.ContinueOnError) + + flags.Bool([]string{"-tlsverify"}, false, "") + err := findConfigurationConflicts(config, flags) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "the following directives don't match any configuration option: tls-verify") { + t.Fatalf("expected tls-verify conflict, got %v", err) + } +} + +func TestFindConfigurationConflictsWithMergedValues(t *testing.T) { + var hosts []string + config := map[string]interface{}{"hosts": "tcp://127.0.0.1:2345"} + base := mflag.NewFlagSet("base", mflag.ContinueOnError) + base.Var(opts.NewNamedListOptsRef("hosts", &hosts, nil), []string{"H", "-host"}, "") + + flags := mflag.NewFlagSet("test", mflag.ContinueOnError) + mflag.Merge(flags, base) + + err := findConfigurationConflicts(config, flags) + if err != nil { + t.Fatal(err) + } + + flags.Set("-host", "unix:///var/run/docker.sock") + err = findConfigurationConflicts(config, flags) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "hosts: (from flag: [unix:///var/run/docker.sock], from file: tcp://127.0.0.1:2345)") { + t.Fatalf("expected hosts conflict, got %v", err) + } +} + +func TestValidateConfiguration(t *testing.T) { + c1 := &Config{ + CommonConfig: CommonConfig{ + Labels: []string{"one"}, + }, + } + + err := validateConfiguration(c1) + if err == nil { + t.Fatal("expected error, got nil") + } + + c2 := &Config{ + CommonConfig: CommonConfig{ + Labels: []string{"one=two"}, + }, + } + + err = validateConfiguration(c2) + if err != nil { + t.Fatalf("expected no error, got error %v", err) + } + + c3 := &Config{ + CommonConfig: CommonConfig{ + DNS: []string{"1.1.1.1"}, + }, + } + + err = validateConfiguration(c3) + if err != nil { + t.Fatalf("expected no error, got error %v", err) + } + + c4 := &Config{ + CommonConfig: CommonConfig{ + DNS: []string{"1.1.1.1o"}, + }, + } + + err = validateConfiguration(c4) + if err == nil { + t.Fatal("expected error, got nil") + } + + c5 := &Config{ + CommonConfig: CommonConfig{ + DNSSearch: []string{"a.b.c"}, + }, + } + + err = validateConfiguration(c5) + if err != nil { + t.Fatalf("expected no error, got error %v", err) + } + + c6 := &Config{ + CommonConfig: CommonConfig{ + DNSSearch: []string{"123456"}, + }, + } + + err = validateConfiguration(c6) + if err == nil { + t.Fatal("expected error, got nil") + } +} diff --git a/daemon/config_unix.go b/daemon/config_unix.go new file mode 100644 index 00000000..5394949e --- /dev/null +++ b/daemon/config_unix.go @@ -0,0 +1,88 @@ +// +build linux freebsd + +package daemon + +import ( + "net" + + "github.com/docker/docker/opts" + flag "github.com/docker/docker/pkg/mflag" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/docker/go-units" +) + +var ( + defaultPidFile = "/var/run/docker.pid" + defaultGraph = "/var/lib/docker" + defaultExecRoot = "/var/run/docker" +) + +// Config defines the configuration of a docker daemon. +// It includes json tags to deserialize configuration from a file +// using the same names that the flags in the command line uses. +type Config struct { + CommonConfig + + // Fields below here are platform specific. + + CorsHeaders string `json:"api-cors-headers,omitempty"` + EnableCors bool `json:"api-enable-cors,omitempty"` + EnableSelinuxSupport bool `json:"selinux-enabled,omitempty"` + RemappedRoot string `json:"userns-remap,omitempty"` + CgroupParent string `json:"cgroup-parent,omitempty"` + Ulimits map[string]*units.Ulimit `json:"default-ulimits,omitempty"` + ContainerdAddr string `json:"containerd,omitempty"` +} + +// bridgeConfig stores all the bridge driver specific +// configuration. +type bridgeConfig struct { + EnableIPv6 bool `json:"ipv6,omitempty"` + EnableIPTables bool `json:"iptables,omitempty"` + EnableIPForward bool `json:"ip-forward,omitempty"` + EnableIPMasq bool `json:"ip-mask,omitempty"` + EnableUserlandProxy bool `json:"userland-proxy,omitempty"` + DefaultIP net.IP `json:"ip,omitempty"` + Iface string `json:"bridge,omitempty"` + IP string `json:"bip,omitempty"` + FixedCIDR string `json:"fixed-cidr,omitempty"` + FixedCIDRv6 string `json:"fixed-cidr-v6,omitempty"` + DefaultGatewayIPv4 net.IP `json:"default-gateway,omitempty"` + DefaultGatewayIPv6 net.IP `json:"default-gateway-v6,omitempty"` + InterContainerCommunication bool `json:"icc,omitempty"` +} + +// InstallFlags adds command-line options to the top-level flag parser for +// the current process. +// Subsequent calls to `flag.Parse` will populate config with values parsed +// from the command-line. +func (config *Config) InstallFlags(cmd *flag.FlagSet, usageFn func(string) string) { + // First handle install flags which are consistent cross-platform + config.InstallCommonFlags(cmd, usageFn) + + // Then platform-specific install flags + cmd.BoolVar(&config.EnableSelinuxSupport, []string{"-selinux-enabled"}, false, usageFn("Enable selinux support")) + cmd.StringVar(&config.SocketGroup, []string{"G", "-group"}, "docker", usageFn("Group for the unix socket")) + config.Ulimits = make(map[string]*units.Ulimit) + cmd.Var(runconfigopts.NewUlimitOpt(&config.Ulimits), []string{"-default-ulimit"}, usageFn("Set default ulimits for containers")) + cmd.BoolVar(&config.bridgeConfig.EnableIPTables, []string{"#iptables", "-iptables"}, true, usageFn("Enable addition of iptables rules")) + cmd.BoolVar(&config.bridgeConfig.EnableIPForward, []string{"#ip-forward", "-ip-forward"}, true, usageFn("Enable net.ipv4.ip_forward")) + cmd.BoolVar(&config.bridgeConfig.EnableIPMasq, []string{"-ip-masq"}, true, usageFn("Enable IP masquerading")) + cmd.BoolVar(&config.bridgeConfig.EnableIPv6, []string{"-ipv6"}, false, usageFn("Enable IPv6 networking")) + cmd.StringVar(&config.bridgeConfig.IP, []string{"#bip", "-bip"}, "", usageFn("Specify network bridge IP")) + cmd.StringVar(&config.bridgeConfig.Iface, []string{"b", "-bridge"}, "", usageFn("Attach containers to a network bridge")) + cmd.StringVar(&config.bridgeConfig.FixedCIDR, []string{"-fixed-cidr"}, "", usageFn("IPv4 subnet for fixed IPs")) + cmd.StringVar(&config.bridgeConfig.FixedCIDRv6, []string{"-fixed-cidr-v6"}, "", usageFn("IPv6 subnet for fixed IPs")) + cmd.Var(opts.NewIPOpt(&config.bridgeConfig.DefaultGatewayIPv4, ""), []string{"-default-gateway"}, usageFn("Container default gateway IPv4 address")) + cmd.Var(opts.NewIPOpt(&config.bridgeConfig.DefaultGatewayIPv6, ""), []string{"-default-gateway-v6"}, usageFn("Container default gateway IPv6 address")) + cmd.BoolVar(&config.bridgeConfig.InterContainerCommunication, []string{"#icc", "-icc"}, true, usageFn("Enable inter-container communication")) + cmd.Var(opts.NewIPOpt(&config.bridgeConfig.DefaultIP, "0.0.0.0"), []string{"#ip", "-ip"}, usageFn("Default IP when binding container ports")) + cmd.BoolVar(&config.bridgeConfig.EnableUserlandProxy, []string{"-userland-proxy"}, true, usageFn("Use userland proxy for loopback traffic")) + cmd.BoolVar(&config.EnableCors, []string{"#api-enable-cors", "#-api-enable-cors"}, false, usageFn("Enable CORS headers in the remote API, this is deprecated by --api-cors-header")) + cmd.StringVar(&config.CorsHeaders, []string{"-api-cors-header"}, "", usageFn("Set CORS headers in the remote API")) + cmd.StringVar(&config.CgroupParent, []string{"-cgroup-parent"}, "", usageFn("Set parent cgroup for all containers")) + cmd.StringVar(&config.RemappedRoot, []string{"-userns-remap"}, "", usageFn("User/Group setting for user namespaces")) + cmd.StringVar(&config.ContainerdAddr, []string{"-containerd"}, "", usageFn("Path to containerd socket")) + + config.attachExperimentalFlags(cmd, usageFn) +} diff --git a/daemon/config_windows.go b/daemon/config_windows.go new file mode 100644 index 00000000..ca141b98 --- /dev/null +++ b/daemon/config_windows.go @@ -0,0 +1,45 @@ +package daemon + +import ( + "os" + + flag "github.com/docker/docker/pkg/mflag" +) + +var ( + defaultPidFile = os.Getenv("programdata") + string(os.PathSeparator) + "docker.pid" + defaultGraph = os.Getenv("programdata") + string(os.PathSeparator) + "docker" + defaultExecRoot = defaultGraph +) + +// bridgeConfig stores all the bridge driver specific +// configuration. +type bridgeConfig struct { + FixedCIDR string + NetworkMode string + Iface string `json:"bridge,omitempty"` +} + +// Config defines the configuration of a docker daemon. +// These are the configuration settings that you pass +// to the docker daemon when you launch it with say: `docker daemon -e windows` +type Config struct { + CommonConfig + + // Fields below here are platform specific. (There are none presently + // for the Windows daemon.) +} + +// InstallFlags adds command-line options to the top-level flag parser for +// the current process. +// Subsequent calls to `flag.Parse` will populate config with values parsed +// from the command-line. +func (config *Config) InstallFlags(cmd *flag.FlagSet, usageFn func(string) string) { + // First handle install flags which are consistent cross-platform + config.InstallCommonFlags(cmd, usageFn) + + // Then platform-specific install flags. + cmd.StringVar(&config.bridgeConfig.FixedCIDR, []string{"-fixed-cidr"}, "", usageFn("IPv4 subnet for fixed IPs")) + cmd.StringVar(&config.bridgeConfig.Iface, []string{"b", "-bridge"}, "", "Attach containers to a virtual switch") + cmd.StringVar(&config.SocketGroup, []string{"G", "-group"}, "", usageFn("Users or groups that can access the named pipe")) +} diff --git a/daemon/container_operations.go b/daemon/container_operations.go new file mode 100644 index 00000000..461439a6 --- /dev/null +++ b/daemon/container_operations.go @@ -0,0 +1,742 @@ +package daemon + +import ( + "errors" + "fmt" + "net" + "os" + "path" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/container" + "github.com/docker/docker/daemon/network" + derr "github.com/docker/docker/errors" + "github.com/docker/docker/runconfig" + containertypes "github.com/docker/engine-api/types/container" + networktypes "github.com/docker/engine-api/types/network" + "github.com/docker/go-connections/nat" + "github.com/docker/libnetwork" + "github.com/docker/libnetwork/netlabel" + "github.com/docker/libnetwork/options" + "github.com/docker/libnetwork/types" +) + +var ( + // ErrRootFSReadOnly is returned when a container + // rootfs is marked readonly. + ErrRootFSReadOnly = errors.New("container rootfs is marked read-only") + getPortMapInfo = container.GetSandboxPortMapInfo +) + +func (daemon *Daemon) buildSandboxOptions(container *container.Container, n libnetwork.Network) ([]libnetwork.SandboxOption, error) { + var ( + sboxOptions []libnetwork.SandboxOption + err error + dns []string + dnsSearch []string + dnsOptions []string + bindings = make(nat.PortMap) + pbList []types.PortBinding + exposeList []types.TransportPort + ) + + defaultNetName := runconfig.DefaultDaemonNetworkMode().NetworkName() + sboxOptions = append(sboxOptions, libnetwork.OptionHostname(container.Config.Hostname), + libnetwork.OptionDomainname(container.Config.Domainname)) + + if container.HostConfig.NetworkMode.IsHost() { + sboxOptions = append(sboxOptions, libnetwork.OptionUseDefaultSandbox()) + sboxOptions = append(sboxOptions, libnetwork.OptionOriginHostsPath("/etc/hosts")) + sboxOptions = append(sboxOptions, libnetwork.OptionOriginResolvConfPath("/etc/resolv.conf")) + } + // OptionUseExternalKey is mandatory for userns support. + // But optional for non-userns support + sboxOptions = append(sboxOptions, libnetwork.OptionUseExternalKey()) + + container.HostsPath, err = container.GetRootResourcePath("hosts") + if err != nil { + return nil, err + } + sboxOptions = append(sboxOptions, libnetwork.OptionHostsPath(container.HostsPath)) + + container.ResolvConfPath, err = container.GetRootResourcePath("resolv.conf") + if err != nil { + return nil, err + } + sboxOptions = append(sboxOptions, libnetwork.OptionResolvConfPath(container.ResolvConfPath)) + + if len(container.HostConfig.DNS) > 0 { + dns = container.HostConfig.DNS + } else if len(daemon.configStore.DNS) > 0 { + dns = daemon.configStore.DNS + } + + for _, d := range dns { + sboxOptions = append(sboxOptions, libnetwork.OptionDNS(d)) + } + + if len(container.HostConfig.DNSSearch) > 0 { + dnsSearch = container.HostConfig.DNSSearch + } else if len(daemon.configStore.DNSSearch) > 0 { + dnsSearch = daemon.configStore.DNSSearch + } + + for _, ds := range dnsSearch { + sboxOptions = append(sboxOptions, libnetwork.OptionDNSSearch(ds)) + } + + if len(container.HostConfig.DNSOptions) > 0 { + dnsOptions = container.HostConfig.DNSOptions + } else if len(daemon.configStore.DNSOptions) > 0 { + dnsOptions = daemon.configStore.DNSOptions + } + + for _, ds := range dnsOptions { + sboxOptions = append(sboxOptions, libnetwork.OptionDNSOptions(ds)) + } + + if container.NetworkSettings.SecondaryIPAddresses != nil { + name := container.Config.Hostname + if container.Config.Domainname != "" { + name = name + "." + container.Config.Domainname + } + + for _, a := range container.NetworkSettings.SecondaryIPAddresses { + sboxOptions = append(sboxOptions, libnetwork.OptionExtraHost(name, a.Addr)) + } + } + + for _, extraHost := range container.HostConfig.ExtraHosts { + // allow IPv6 addresses in extra hosts; only split on first ":" + parts := strings.SplitN(extraHost, ":", 2) + sboxOptions = append(sboxOptions, libnetwork.OptionExtraHost(parts[0], parts[1])) + } + + if container.HostConfig.PortBindings != nil { + for p, b := range container.HostConfig.PortBindings { + bindings[p] = []nat.PortBinding{} + for _, bb := range b { + bindings[p] = append(bindings[p], nat.PortBinding{ + HostIP: bb.HostIP, + HostPort: bb.HostPort, + }) + } + } + } + + portSpecs := container.Config.ExposedPorts + ports := make([]nat.Port, len(portSpecs)) + var i int + for p := range portSpecs { + ports[i] = p + i++ + } + nat.SortPortMap(ports, bindings) + for _, port := range ports { + expose := types.TransportPort{} + expose.Proto = types.ParseProtocol(port.Proto()) + expose.Port = uint16(port.Int()) + exposeList = append(exposeList, expose) + + pb := types.PortBinding{Port: expose.Port, Proto: expose.Proto} + binding := bindings[port] + for i := 0; i < len(binding); i++ { + pbCopy := pb.GetCopy() + newP, err := nat.NewPort(nat.SplitProtoPort(binding[i].HostPort)) + var portStart, portEnd int + if err == nil { + portStart, portEnd, err = newP.Range() + } + if err != nil { + return nil, fmt.Errorf("Error parsing HostPort value(%s):%v", binding[i].HostPort, err) + } + pbCopy.HostPort = uint16(portStart) + pbCopy.HostPortEnd = uint16(portEnd) + pbCopy.HostIP = net.ParseIP(binding[i].HostIP) + pbList = append(pbList, pbCopy) + } + + if container.HostConfig.PublishAllPorts && len(binding) == 0 { + pbList = append(pbList, pb) + } + } + + sboxOptions = append(sboxOptions, + libnetwork.OptionPortMapping(pbList), + libnetwork.OptionExposedPorts(exposeList)) + + // Link feature is supported only for the default bridge network. + // return if this call to build join options is not for default bridge network + if n.Name() != defaultNetName { + return sboxOptions, nil + } + + ep, _ := container.GetEndpointInNetwork(n) + if ep == nil { + return sboxOptions, nil + } + + var childEndpoints, parentEndpoints []string + + children := daemon.children(container) + for linkAlias, child := range children { + if !isLinkable(child) { + return nil, fmt.Errorf("Cannot link to %s, as it does not belong to the default network", child.Name) + } + _, alias := path.Split(linkAlias) + // allow access to the linked container via the alias, real name, and container hostname + aliasList := alias + " " + child.Config.Hostname + // only add the name if alias isn't equal to the name + if alias != child.Name[1:] { + aliasList = aliasList + " " + child.Name[1:] + } + sboxOptions = append(sboxOptions, libnetwork.OptionExtraHost(aliasList, child.NetworkSettings.Networks[defaultNetName].IPAddress)) + cEndpoint, _ := child.GetEndpointInNetwork(n) + if cEndpoint != nil && cEndpoint.ID() != "" { + childEndpoints = append(childEndpoints, cEndpoint.ID()) + } + } + + bridgeSettings := container.NetworkSettings.Networks[defaultNetName] + for alias, parent := range daemon.parents(container) { + if daemon.configStore.DisableBridge || !container.HostConfig.NetworkMode.IsPrivate() { + continue + } + + _, alias = path.Split(alias) + logrus.Debugf("Update /etc/hosts of %s for alias %s with ip %s", parent.ID, alias, bridgeSettings.IPAddress) + sboxOptions = append(sboxOptions, libnetwork.OptionParentUpdate( + parent.ID, + alias, + bridgeSettings.IPAddress, + )) + if ep.ID() != "" { + parentEndpoints = append(parentEndpoints, ep.ID()) + } + } + + linkOptions := options.Generic{ + netlabel.GenericData: options.Generic{ + "ParentEndpoints": parentEndpoints, + "ChildEndpoints": childEndpoints, + }, + } + + sboxOptions = append(sboxOptions, libnetwork.OptionGeneric(linkOptions)) + return sboxOptions, nil +} + +func (daemon *Daemon) updateNetworkSettings(container *container.Container, n libnetwork.Network) error { + if container.NetworkSettings == nil { + container.NetworkSettings = &network.Settings{Networks: make(map[string]*networktypes.EndpointSettings)} + } + + if !container.HostConfig.NetworkMode.IsHost() && containertypes.NetworkMode(n.Type()).IsHost() { + return runconfig.ErrConflictHostNetwork + } + + for s := range container.NetworkSettings.Networks { + sn, err := daemon.FindNetwork(s) + if err != nil { + continue + } + + if sn.Name() == n.Name() { + // Avoid duplicate config + return nil + } + if !containertypes.NetworkMode(sn.Type()).IsPrivate() || + !containertypes.NetworkMode(n.Type()).IsPrivate() { + return runconfig.ErrConflictSharedNetwork + } + if containertypes.NetworkMode(sn.Name()).IsNone() || + containertypes.NetworkMode(n.Name()).IsNone() { + return runconfig.ErrConflictNoNetwork + } + } + + if _, ok := container.NetworkSettings.Networks[n.Name()]; !ok { + container.NetworkSettings.Networks[n.Name()] = new(networktypes.EndpointSettings) + } + + return nil +} + +func (daemon *Daemon) updateEndpointNetworkSettings(container *container.Container, n libnetwork.Network, ep libnetwork.Endpoint) error { + if err := container.BuildEndpointInfo(n, ep); err != nil { + return err + } + + if container.HostConfig.NetworkMode == runconfig.DefaultDaemonNetworkMode() { + container.NetworkSettings.Bridge = daemon.configStore.bridgeConfig.Iface + } + + return nil +} + +// UpdateNetwork is used to update the container's network (e.g. when linked containers +// get removed/unlinked). +func (daemon *Daemon) updateNetwork(container *container.Container) error { + ctrl := daemon.netController + sid := container.NetworkSettings.SandboxID + + sb, err := ctrl.SandboxByID(sid) + if err != nil { + return fmt.Errorf("error locating sandbox id %s: %v", sid, err) + } + + // Find if container is connected to the default bridge network + var n libnetwork.Network + for name := range container.NetworkSettings.Networks { + sn, err := daemon.FindNetwork(name) + if err != nil { + continue + } + if sn.Name() == runconfig.DefaultDaemonNetworkMode().NetworkName() { + n = sn + break + } + } + + if n == nil { + // Not connected to the default bridge network; Nothing to do + return nil + } + + options, err := daemon.buildSandboxOptions(container, n) + if err != nil { + return fmt.Errorf("Update network failed: %v", err) + } + + if err := sb.Refresh(options...); err != nil { + return fmt.Errorf("Update network failed: Failure in refresh sandbox %s: %v", sid, err) + } + + return nil +} + +// updateContainerNetworkSettings update the network settings +func (daemon *Daemon) updateContainerNetworkSettings(container *container.Container, endpointsConfig map[string]*networktypes.EndpointSettings) error { + var ( + n libnetwork.Network + err error + ) + + // TODO Windows: Remove this once TP4 builds are not supported + // Windows TP4 build don't support libnetwork and in that case + // daemon.netController will be nil + if daemon.netController == nil { + return nil + } + + mode := container.HostConfig.NetworkMode + if container.Config.NetworkDisabled || mode.IsContainer() { + return nil + } + + networkName := mode.NetworkName() + if mode.IsDefault() { + networkName = daemon.netController.Config().Daemon.DefaultNetwork + } + if mode.IsUserDefined() { + n, err = daemon.FindNetwork(networkName) + if err != nil { + return err + } + networkName = n.Name() + } + if container.NetworkSettings == nil { + container.NetworkSettings = &network.Settings{} + } + if len(endpointsConfig) > 0 { + container.NetworkSettings.Networks = endpointsConfig + } + if container.NetworkSettings.Networks == nil { + container.NetworkSettings.Networks = make(map[string]*networktypes.EndpointSettings) + container.NetworkSettings.Networks[networkName] = new(networktypes.EndpointSettings) + } + if !mode.IsUserDefined() { + return nil + } + // Make sure to internally store the per network endpoint config by network name + if _, ok := container.NetworkSettings.Networks[networkName]; ok { + return nil + } + if nwConfig, ok := container.NetworkSettings.Networks[n.ID()]; ok { + container.NetworkSettings.Networks[networkName] = nwConfig + delete(container.NetworkSettings.Networks, n.ID()) + return nil + } + + return nil +} + +func (daemon *Daemon) allocateNetwork(container *container.Container) error { + controller := daemon.netController + + if daemon.netController == nil { + return nil + } + + // Cleanup any stale sandbox left over due to ungraceful daemon shutdown + if err := controller.SandboxDestroy(container.ID); err != nil { + logrus.Errorf("failed to cleanup up stale network sandbox for container %s", container.ID) + } + + updateSettings := false + if len(container.NetworkSettings.Networks) == 0 { + if container.Config.NetworkDisabled || container.HostConfig.NetworkMode.IsContainer() { + return nil + } + + err := daemon.updateContainerNetworkSettings(container, nil) + if err != nil { + return err + } + updateSettings = true + } + + for n, nConf := range container.NetworkSettings.Networks { + if err := daemon.connectToNetwork(container, n, nConf, updateSettings); err != nil { + return err + } + } + + return container.WriteHostConfig() +} + +func (daemon *Daemon) getNetworkSandbox(container *container.Container) libnetwork.Sandbox { + var sb libnetwork.Sandbox + daemon.netController.WalkSandboxes(func(s libnetwork.Sandbox) bool { + if s.ContainerID() == container.ID { + sb = s + return true + } + return false + }) + return sb +} + +// hasUserDefinedIPAddress returns whether the passed endpoint configuration contains IP address configuration +func hasUserDefinedIPAddress(epConfig *networktypes.EndpointSettings) bool { + return epConfig != nil && epConfig.IPAMConfig != nil && (len(epConfig.IPAMConfig.IPv4Address) > 0 || len(epConfig.IPAMConfig.IPv6Address) > 0) +} + +// User specified ip address is acceptable only for networks with user specified subnets. +func validateNetworkingConfig(n libnetwork.Network, epConfig *networktypes.EndpointSettings) error { + if n == nil || epConfig == nil { + return nil + } + if !hasUserDefinedIPAddress(epConfig) { + return nil + } + _, _, nwIPv4Configs, nwIPv6Configs := n.Info().IpamConfig() + for _, s := range []struct { + ipConfigured bool + subnetConfigs []*libnetwork.IpamConf + }{ + { + ipConfigured: len(epConfig.IPAMConfig.IPv4Address) > 0, + subnetConfigs: nwIPv4Configs, + }, + { + ipConfigured: len(epConfig.IPAMConfig.IPv6Address) > 0, + subnetConfigs: nwIPv6Configs, + }, + } { + if s.ipConfigured { + foundSubnet := false + for _, cfg := range s.subnetConfigs { + if len(cfg.PreferredPool) > 0 { + foundSubnet = true + break + } + } + if !foundSubnet { + return runconfig.ErrUnsupportedNetworkNoSubnetAndIP + } + } + } + + return nil +} + +// cleanOperationalData resets the operational data from the passed endpoint settings +func cleanOperationalData(es *networktypes.EndpointSettings) { + es.EndpointID = "" + es.Gateway = "" + es.IPAddress = "" + es.IPPrefixLen = 0 + es.IPv6Gateway = "" + es.GlobalIPv6Address = "" + es.GlobalIPv6PrefixLen = 0 + es.MacAddress = "" +} + +func (daemon *Daemon) updateNetworkConfig(container *container.Container, idOrName string, endpointConfig *networktypes.EndpointSettings, updateSettings bool) (libnetwork.Network, error) { + if container.HostConfig.NetworkMode.IsContainer() { + return nil, runconfig.ErrConflictSharedNetwork + } + + if containertypes.NetworkMode(idOrName).IsBridge() && + daemon.configStore.DisableBridge { + container.Config.NetworkDisabled = true + return nil, nil + } + + if !containertypes.NetworkMode(idOrName).IsUserDefined() { + if hasUserDefinedIPAddress(endpointConfig) { + return nil, runconfig.ErrUnsupportedNetworkAndIP + } + if endpointConfig != nil && len(endpointConfig.Aliases) > 0 { + return nil, runconfig.ErrUnsupportedNetworkAndAlias + } + } + + n, err := daemon.FindNetwork(idOrName) + if err != nil { + return nil, err + } + + if err := validateNetworkingConfig(n, endpointConfig); err != nil { + return nil, err + } + + if updateSettings { + if err := daemon.updateNetworkSettings(container, n); err != nil { + return nil, err + } + } + return n, nil +} + +func (daemon *Daemon) connectToNetwork(container *container.Container, idOrName string, endpointConfig *networktypes.EndpointSettings, updateSettings bool) (err error) { + // TODO Windows: Remove this once TP4 builds are not supported + // Windows TP4 build don't support libnetwork and in that case + // daemon.netController will be nil + if daemon.netController == nil { + return nil + } + + n, err := daemon.updateNetworkConfig(container, idOrName, endpointConfig, updateSettings) + if err != nil { + return err + } + if n == nil { + return nil + } + + controller := daemon.netController + + sb := daemon.getNetworkSandbox(container) + createOptions, err := container.BuildCreateEndpointOptions(n, endpointConfig, sb) + if err != nil { + return err + } + + endpointName := strings.TrimPrefix(container.Name, "/") + ep, err := n.CreateEndpoint(endpointName, createOptions...) + if err != nil { + return err + } + defer func() { + if err != nil { + if e := ep.Delete(false); e != nil { + logrus.Warnf("Could not rollback container connection to network %s", idOrName) + } + } + }() + + if endpointConfig != nil { + container.NetworkSettings.Networks[n.Name()] = endpointConfig + } + + if err := daemon.updateEndpointNetworkSettings(container, n, ep); err != nil { + return err + } + + if sb == nil { + options, err := daemon.buildSandboxOptions(container, n) + if err != nil { + return err + } + sb, err = controller.NewSandbox(container.ID, options...) + if err != nil { + return err + } + + container.UpdateSandboxNetworkSettings(sb) + } + + joinOptions, err := container.BuildJoinOptions(n) + if err != nil { + return err + } + + if err := ep.Join(sb, joinOptions...); err != nil { + return err + } + + if err := container.UpdateJoinInfo(n, ep); err != nil { + return fmt.Errorf("Updating join info failed: %v", err) + } + + container.NetworkSettings.Ports = getPortMapInfo(sb) + + daemon.LogNetworkEventWithAttributes(n, "connect", map[string]string{"container": container.ID}) + return nil +} + +// ForceEndpointDelete deletes an endpoing from a network forcefully +func (daemon *Daemon) ForceEndpointDelete(name string, n libnetwork.Network) error { + ep, err := n.EndpointByName(name) + if err != nil { + return err + } + return ep.Delete(true) +} + +func disconnectFromNetwork(container *container.Container, n libnetwork.Network, force bool) error { + var ( + ep libnetwork.Endpoint + sbox libnetwork.Sandbox + ) + + s := func(current libnetwork.Endpoint) bool { + epInfo := current.Info() + if epInfo == nil { + return false + } + if sb := epInfo.Sandbox(); sb != nil { + if sb.ContainerID() == container.ID { + ep = current + sbox = sb + return true + } + } + return false + } + n.WalkEndpoints(s) + + if ep == nil && force { + epName := strings.TrimPrefix(container.Name, "/") + ep, err := n.EndpointByName(epName) + if err != nil { + return err + } + return ep.Delete(force) + } + + if ep == nil { + return fmt.Errorf("container %s is not connected to the network", container.ID) + } + + if err := ep.Leave(sbox); err != nil { + return fmt.Errorf("container %s failed to leave network %s: %v", container.ID, n.Name(), err) + } + + container.NetworkSettings.Ports = getPortMapInfo(sbox) + + if err := ep.Delete(false); err != nil { + return fmt.Errorf("endpoint delete failed for container %s on network %s: %v", container.ID, n.Name(), err) + } + + delete(container.NetworkSettings.Networks, n.Name()) + return nil +} + +func (daemon *Daemon) initializeNetworking(container *container.Container) error { + var err error + + // TODO Windows: Remove this once TP4 builds are not supported + // Windows TP4 build don't support libnetwork and in that case + // daemon.netController will be nil + if daemon.netController == nil { + return nil + } + + if container.HostConfig.NetworkMode.IsContainer() { + // we need to get the hosts files from the container to join + nc, err := daemon.getNetworkedContainer(container.ID, container.HostConfig.NetworkMode.ConnectedContainer()) + if err != nil { + return err + } + container.HostnamePath = nc.HostnamePath + container.HostsPath = nc.HostsPath + container.ResolvConfPath = nc.ResolvConfPath + container.Config.Hostname = nc.Config.Hostname + container.Config.Domainname = nc.Config.Domainname + return nil + } + + if container.HostConfig.NetworkMode.IsHost() { + container.Config.Hostname, err = os.Hostname() + if err != nil { + return err + } + } + + if err := daemon.allocateNetwork(container); err != nil { + return err + } + + return container.BuildHostnameFile() +} + +func (daemon *Daemon) getNetworkedContainer(containerID, connectedContainerID string) (*container.Container, error) { + nc, err := daemon.GetContainer(connectedContainerID) + if err != nil { + return nil, err + } + if containerID == nc.ID { + return nil, fmt.Errorf("cannot join own network") + } + if !nc.IsRunning() { + err := fmt.Errorf("cannot join network of a non running container: %s", connectedContainerID) + return nil, derr.NewRequestConflictError(err) + } + if nc.IsRestarting() { + return nil, errContainerIsRestarting(connectedContainerID) + } + return nc, nil +} + +func (daemon *Daemon) releaseNetwork(container *container.Container) { + if container.HostConfig.NetworkMode.IsContainer() || container.Config.NetworkDisabled { + return + } + + sid := container.NetworkSettings.SandboxID + settings := container.NetworkSettings.Networks + container.NetworkSettings.Ports = nil + + if sid == "" || len(settings) == 0 { + return + } + + var networks []libnetwork.Network + for n, epSettings := range settings { + if nw, err := daemon.FindNetwork(n); err == nil { + networks = append(networks, nw) + } + cleanOperationalData(epSettings) + } + + sb, err := daemon.netController.SandboxByID(sid) + if err != nil { + logrus.Warnf("error locating sandbox id %s: %v", sid, err) + return + } + + if err := sb.Delete(); err != nil { + logrus.Errorf("Error deleting sandbox id %s for container %s: %v", sid, container.ID, err) + } + + attributes := map[string]string{ + "container": container.ID, + } + for _, nw := range networks { + daemon.LogNetworkEventWithAttributes(nw, "disconnect", attributes) + } +} diff --git a/daemon/container_operations_unix.go b/daemon/container_operations_unix.go new file mode 100644 index 00000000..a313ef5c --- /dev/null +++ b/daemon/container_operations_unix.go @@ -0,0 +1,389 @@ +// +build linux freebsd + +package daemon + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "syscall" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/container" + "github.com/docker/docker/daemon/links" + "github.com/docker/docker/pkg/fileutils" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/mount" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/runconfig" + containertypes "github.com/docker/engine-api/types/container" + networktypes "github.com/docker/engine-api/types/network" + "github.com/docker/libnetwork" + "github.com/opencontainers/runc/libcontainer/configs" + "github.com/opencontainers/runc/libcontainer/devices" + "github.com/opencontainers/runc/libcontainer/label" + "github.com/opencontainers/specs/specs-go" +) + +func u32Ptr(i int64) *uint32 { u := uint32(i); return &u } +func fmPtr(i int64) *os.FileMode { fm := os.FileMode(i); return &fm } + +func (daemon *Daemon) setupLinkedContainers(container *container.Container) ([]string, error) { + var env []string + children := daemon.children(container) + + bridgeSettings := container.NetworkSettings.Networks[runconfig.DefaultDaemonNetworkMode().NetworkName()] + if bridgeSettings == nil { + return nil, nil + } + + for linkAlias, child := range children { + if !child.IsRunning() { + return nil, fmt.Errorf("Cannot link to a non running container: %s AS %s", child.Name, linkAlias) + } + + childBridgeSettings := child.NetworkSettings.Networks[runconfig.DefaultDaemonNetworkMode().NetworkName()] + if childBridgeSettings == nil { + return nil, fmt.Errorf("container %s not attached to default bridge network", child.ID) + } + + link := links.NewLink( + bridgeSettings.IPAddress, + childBridgeSettings.IPAddress, + linkAlias, + child.Config.Env, + child.Config.ExposedPorts, + ) + + for _, envVar := range link.ToEnv() { + env = append(env, envVar) + } + } + + return env, nil +} + +// getSize returns the real size & virtual size of the container. +func (daemon *Daemon) getSize(container *container.Container) (int64, int64) { + var ( + sizeRw, sizeRootfs int64 + err error + ) + + if err := daemon.Mount(container); err != nil { + logrus.Errorf("Failed to compute size of container rootfs %s: %s", container.ID, err) + return sizeRw, sizeRootfs + } + defer daemon.Unmount(container) + + sizeRw, err = container.RWLayer.Size() + if err != nil { + logrus.Errorf("Driver %s couldn't return diff size of container %s: %s", + daemon.GraphDriverName(), container.ID, err) + // FIXME: GetSize should return an error. Not changing it now in case + // there is a side-effect. + sizeRw = -1 + } + + if parent := container.RWLayer.Parent(); parent != nil { + sizeRootfs, err = parent.Size() + if err != nil { + sizeRootfs = -1 + } else if sizeRw != -1 { + sizeRootfs += sizeRw + } + } + return sizeRw, sizeRootfs +} + +// ConnectToNetwork connects a container to a network +func (daemon *Daemon) ConnectToNetwork(container *container.Container, idOrName string, endpointConfig *networktypes.EndpointSettings) error { + if !container.Running { + if container.RemovalInProgress || container.Dead { + return errRemovalContainer(container.ID) + } + if _, err := daemon.updateNetworkConfig(container, idOrName, endpointConfig, true); err != nil { + return err + } + if endpointConfig != nil { + container.NetworkSettings.Networks[idOrName] = endpointConfig + } + } else { + if err := daemon.connectToNetwork(container, idOrName, endpointConfig, true); err != nil { + return err + } + } + if err := container.ToDiskLocking(); err != nil { + return fmt.Errorf("Error saving container to disk: %v", err) + } + return nil +} + +// DisconnectFromNetwork disconnects container from network n. +func (daemon *Daemon) DisconnectFromNetwork(container *container.Container, n libnetwork.Network, force bool) error { + if container.HostConfig.NetworkMode.IsHost() && containertypes.NetworkMode(n.Type()).IsHost() { + return runconfig.ErrConflictHostNetwork + } + if !container.Running { + if container.RemovalInProgress || container.Dead { + return errRemovalContainer(container.ID) + } + if _, ok := container.NetworkSettings.Networks[n.Name()]; ok { + delete(container.NetworkSettings.Networks, n.Name()) + } else { + return fmt.Errorf("container %s is not connected to the network %s", container.ID, n.Name()) + } + } else { + if err := disconnectFromNetwork(container, n, false); err != nil { + return err + } + } + + if err := container.ToDiskLocking(); err != nil { + return fmt.Errorf("Error saving container to disk: %v", err) + } + + attributes := map[string]string{ + "container": container.ID, + } + daemon.LogNetworkEventWithAttributes(n, "disconnect", attributes) + return nil +} + +// called from the libcontainer pre-start hook to set the network +// namespace configuration linkage to the libnetwork "sandbox" entity +func (daemon *Daemon) setNetworkNamespaceKey(containerID string, pid int) error { + path := fmt.Sprintf("/proc/%d/ns/net", pid) + var sandbox libnetwork.Sandbox + search := libnetwork.SandboxContainerWalker(&sandbox, containerID) + daemon.netController.WalkSandboxes(search) + if sandbox == nil { + return fmt.Errorf("error locating sandbox id %s: no sandbox found", containerID) + } + + return sandbox.SetKey(path) +} + +func (daemon *Daemon) getIpcContainer(container *container.Container) (*container.Container, error) { + containerID := container.HostConfig.IpcMode.Container() + c, err := daemon.GetContainer(containerID) + if err != nil { + return nil, err + } + if !c.IsRunning() { + return nil, fmt.Errorf("cannot join IPC of a non running container: %s", containerID) + } + if c.IsRestarting() { + return nil, errContainerIsRestarting(container.ID) + } + return c, nil +} + +func (daemon *Daemon) setupIpcDirs(c *container.Container) error { + var err error + + c.ShmPath, err = c.ShmResourcePath() + if err != nil { + return err + } + + if c.HostConfig.IpcMode.IsContainer() { + ic, err := daemon.getIpcContainer(c) + if err != nil { + return err + } + c.ShmPath = ic.ShmPath + } else if c.HostConfig.IpcMode.IsHost() { + if _, err := os.Stat("/dev/shm"); err != nil { + return fmt.Errorf("/dev/shm is not mounted, but must be for --ipc=host") + } + c.ShmPath = "/dev/shm" + } else { + rootUID, rootGID := daemon.GetRemappedUIDGID() + if !c.HasMountFor("/dev/shm") { + shmPath, err := c.ShmResourcePath() + if err != nil { + return err + } + + if err := idtools.MkdirAllAs(shmPath, 0700, rootUID, rootGID); err != nil { + return err + } + + shmSize := container.DefaultSHMSize + if c.HostConfig.ShmSize != 0 { + shmSize = c.HostConfig.ShmSize + } + shmproperty := "mode=1777,size=" + strconv.FormatInt(shmSize, 10) + if err := syscall.Mount("shm", shmPath, "tmpfs", uintptr(syscall.MS_NOEXEC|syscall.MS_NOSUID|syscall.MS_NODEV), label.FormatMountLabel(shmproperty, c.GetMountLabel())); err != nil { + return fmt.Errorf("mounting shm tmpfs: %s", err) + } + if err := os.Chown(shmPath, rootUID, rootGID); err != nil { + return err + } + } + + } + + return nil +} + +func (daemon *Daemon) mountVolumes(container *container.Container) error { + mounts, err := daemon.setupMounts(container) + if err != nil { + return err + } + + for _, m := range mounts { + dest, err := container.GetResourcePath(m.Destination) + if err != nil { + return err + } + + var stat os.FileInfo + stat, err = os.Stat(m.Source) + if err != nil { + return err + } + if err = fileutils.CreateIfNotExists(dest, stat.IsDir()); err != nil { + return err + } + + opts := "rbind,ro" + if m.Writable { + opts = "rbind,rw" + } + + if err := mount.Mount(m.Source, dest, "bind", opts); err != nil { + return err + } + } + + return nil +} + +func killProcessDirectly(container *container.Container) error { + if _, err := container.WaitStop(10 * time.Second); err != nil { + // Ensure that we don't kill ourselves + if pid := container.GetPID(); pid != 0 { + logrus.Infof("Container %s failed to exit within 10 seconds of kill - trying direct SIGKILL", stringid.TruncateID(container.ID)) + if err := syscall.Kill(pid, 9); err != nil { + if err != syscall.ESRCH { + return err + } + e := errNoSuchProcess{pid, 9} + logrus.Debug(e) + return e + } + } + } + return nil +} + +func specDevice(d *configs.Device) specs.Device { + return specs.Device{ + Type: string(d.Type), + Path: d.Path, + Major: d.Major, + Minor: d.Minor, + FileMode: fmPtr(int64(d.FileMode)), + UID: u32Ptr(int64(d.Uid)), + GID: u32Ptr(int64(d.Gid)), + } +} + +func specDeviceCgroup(d *configs.Device) specs.DeviceCgroup { + t := string(d.Type) + return specs.DeviceCgroup{ + Allow: true, + Type: &t, + Major: &d.Major, + Minor: &d.Minor, + Access: &d.Permissions, + } +} + +func getDevicesFromPath(deviceMapping containertypes.DeviceMapping) (devs []specs.Device, devPermissions []specs.DeviceCgroup, err error) { + resolvedPathOnHost := deviceMapping.PathOnHost + + // check if it is a symbolic link + if src, e := os.Lstat(deviceMapping.PathOnHost); e == nil && src.Mode()&os.ModeSymlink == os.ModeSymlink { + if linkedPathOnHost, e := os.Readlink(deviceMapping.PathOnHost); e == nil { + resolvedPathOnHost = linkedPathOnHost + } + } + + device, err := devices.DeviceFromPath(resolvedPathOnHost, deviceMapping.CgroupPermissions) + // if there was no error, return the device + if err == nil { + device.Path = deviceMapping.PathInContainer + return append(devs, specDevice(device)), append(devPermissions, specDeviceCgroup(device)), nil + } + + // if the device is not a device node + // try to see if it's a directory holding many devices + if err == devices.ErrNotADevice { + + // check if it is a directory + if src, e := os.Stat(resolvedPathOnHost); e == nil && src.IsDir() { + + // mount the internal devices recursively + filepath.Walk(resolvedPathOnHost, func(dpath string, f os.FileInfo, e error) error { + childDevice, e := devices.DeviceFromPath(dpath, deviceMapping.CgroupPermissions) + if e != nil { + // ignore the device + return nil + } + + // add the device to userSpecified devices + childDevice.Path = strings.Replace(dpath, resolvedPathOnHost, deviceMapping.PathInContainer, 1) + devs = append(devs, specDevice(childDevice)) + devPermissions = append(devPermissions, specDeviceCgroup(childDevice)) + + return nil + }) + } + } + + if len(devs) > 0 { + return devs, devPermissions, nil + } + + return devs, devPermissions, fmt.Errorf("error gathering device information while adding custom device %q: %s", deviceMapping.PathOnHost, err) +} + +func mergeDevices(defaultDevices, userDevices []*configs.Device) []*configs.Device { + if len(userDevices) == 0 { + return defaultDevices + } + + paths := map[string]*configs.Device{} + for _, d := range userDevices { + paths[d.Path] = d + } + + var devs []*configs.Device + for _, d := range defaultDevices { + if _, defined := paths[d.Path]; !defined { + devs = append(devs, d) + } + } + return append(devs, userDevices...) +} + +func detachMounted(path string) error { + return syscall.Unmount(path, syscall.MNT_DETACH) +} + +func isLinkable(child *container.Container) bool { + // A container is linkable only if it belongs to the default network + _, ok := child.NetworkSettings.Networks[runconfig.DefaultDaemonNetworkMode().NetworkName()] + return ok +} + +func errRemovalContainer(containerID string) error { + return fmt.Errorf("Container %s is marked for removal and cannot be connected or disconnected to the network", containerID) +} diff --git a/daemon/container_operations_windows.go b/daemon/container_operations_windows.go new file mode 100644 index 00000000..701bfd8c --- /dev/null +++ b/daemon/container_operations_windows.go @@ -0,0 +1,62 @@ +// +build windows + +package daemon + +import ( + "fmt" + + "github.com/docker/docker/container" + networktypes "github.com/docker/engine-api/types/network" + "github.com/docker/libnetwork" +) + +func (daemon *Daemon) setupLinkedContainers(container *container.Container) ([]string, error) { + return nil, nil +} + +// ConnectToNetwork connects a container to a network +func (daemon *Daemon) ConnectToNetwork(container *container.Container, idOrName string, endpointConfig *networktypes.EndpointSettings) error { + return fmt.Errorf("Windows does not support connecting a running container to a network") +} + +// DisconnectFromNetwork disconnects container from a network. +func (daemon *Daemon) DisconnectFromNetwork(container *container.Container, n libnetwork.Network, force bool) error { + return fmt.Errorf("Windows does not support disconnecting a running container from a network") +} + +// getSize returns real size & virtual size +func (daemon *Daemon) getSize(container *container.Container) (int64, int64) { + // TODO Windows + return 0, 0 +} + +// setNetworkNamespaceKey is a no-op on Windows. +func (daemon *Daemon) setNetworkNamespaceKey(containerID string, pid int) error { + return nil +} + +func (daemon *Daemon) setupIpcDirs(container *container.Container) error { + return nil +} + +// TODO Windows: Fix Post-TP4. This is a hack to allow docker cp to work +// against containers which have volumes. You will still be able to cp +// to somewhere on the container drive, but not to any mounted volumes +// inside the container. Without this fix, docker cp is broken to any +// container which has a volume, regardless of where the file is inside the +// container. +func (daemon *Daemon) mountVolumes(container *container.Container) error { + return nil +} + +func detachMounted(path string) error { + return nil +} + +func killProcessDirectly(container *container.Container) error { + return nil +} + +func isLinkable(child *container.Container) bool { + return false +} diff --git a/daemon/create.go b/daemon/create.go new file mode 100644 index 00000000..34f0aa2d --- /dev/null +++ b/daemon/create.go @@ -0,0 +1,185 @@ +package daemon + +import ( + "fmt" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/container" + "github.com/docker/docker/image" + "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/stringid" + volumestore "github.com/docker/docker/volume/store" + "github.com/docker/engine-api/types" + containertypes "github.com/docker/engine-api/types/container" + networktypes "github.com/docker/engine-api/types/network" + "github.com/opencontainers/runc/libcontainer/label" +) + +// ContainerCreate creates a container. +func (daemon *Daemon) ContainerCreate(params types.ContainerCreateConfig) (types.ContainerCreateResponse, error) { + if params.Config == nil { + return types.ContainerCreateResponse{}, fmt.Errorf("Config cannot be empty in order to create a container") + } + + warnings, err := daemon.verifyContainerSettings(params.HostConfig, params.Config, false) + if err != nil { + return types.ContainerCreateResponse{Warnings: warnings}, err + } + + err = daemon.verifyNetworkingConfig(params.NetworkingConfig) + if err != nil { + return types.ContainerCreateResponse{}, err + } + + if params.HostConfig == nil { + params.HostConfig = &containertypes.HostConfig{} + } + err = daemon.adaptContainerSettings(params.HostConfig, params.AdjustCPUShares) + if err != nil { + return types.ContainerCreateResponse{Warnings: warnings}, err + } + + container, err := daemon.create(params) + if err != nil { + return types.ContainerCreateResponse{Warnings: warnings}, daemon.imageNotExistToErrcode(err) + } + + return types.ContainerCreateResponse{ID: container.ID, Warnings: warnings}, nil +} + +// Create creates a new container from the given configuration with a given name. +func (daemon *Daemon) create(params types.ContainerCreateConfig) (retC *container.Container, retErr error) { + var ( + container *container.Container + img *image.Image + imgID image.ID + err error + ) + + if params.Config.Image != "" { + img, err = daemon.GetImage(params.Config.Image) + if err != nil { + return nil, err + } + imgID = img.ID() + } + + if err := daemon.mergeAndVerifyConfig(params.Config, img); err != nil { + return nil, err + } + + if container, err = daemon.newContainer(params.Name, params.Config, imgID); err != nil { + return nil, err + } + defer func() { + if retErr != nil { + if err := daemon.ContainerRm(container.ID, &types.ContainerRmConfig{ForceRemove: true}); err != nil { + logrus.Errorf("Clean up Error! Cannot destroy container %s: %v", container.ID, err) + } + } + }() + + if err := daemon.setSecurityOptions(container, params.HostConfig); err != nil { + return nil, err + } + + // Set RWLayer for container after mount labels have been set + if err := daemon.setRWLayer(container); err != nil { + return nil, err + } + + if err := daemon.Register(container); err != nil { + return nil, err + } + rootUID, rootGID, err := idtools.GetRootUIDGID(daemon.uidMaps, daemon.gidMaps) + if err != nil { + return nil, err + } + if err := idtools.MkdirAs(container.Root, 0700, rootUID, rootGID); err != nil { + return nil, err + } + + if err := daemon.setHostConfig(container, params.HostConfig); err != nil { + return nil, err + } + defer func() { + if retErr != nil { + if err := daemon.removeMountPoints(container, true); err != nil { + logrus.Error(err) + } + } + }() + + if err := daemon.createContainerPlatformSpecificSettings(container, params.Config, params.HostConfig); err != nil { + return nil, err + } + + var endpointsConfigs map[string]*networktypes.EndpointSettings + if params.NetworkingConfig != nil { + endpointsConfigs = params.NetworkingConfig.EndpointsConfig + } + + if err := daemon.updateContainerNetworkSettings(container, endpointsConfigs); err != nil { + return nil, err + } + + if err := container.ToDiskLocking(); err != nil { + logrus.Errorf("Error saving new container to disk: %v", err) + return nil, err + } + daemon.LogContainerEvent(container, "create") + return container, nil +} + +func (daemon *Daemon) generateSecurityOpt(ipcMode containertypes.IpcMode, pidMode containertypes.PidMode) ([]string, error) { + if ipcMode.IsHost() || pidMode.IsHost() { + return label.DisableSecOpt(), nil + } + if ipcContainer := ipcMode.Container(); ipcContainer != "" { + c, err := daemon.GetContainer(ipcContainer) + if err != nil { + return nil, err + } + + return label.DupSecOpt(c.ProcessLabel), nil + } + return nil, nil +} + +func (daemon *Daemon) setRWLayer(container *container.Container) error { + var layerID layer.ChainID + if container.ImageID != "" { + img, err := daemon.imageStore.Get(container.ImageID) + if err != nil { + return err + } + layerID = img.RootFS.ChainID() + } + rwLayer, err := daemon.layerStore.CreateRWLayer(container.ID, layerID, container.MountLabel, daemon.setupInitLayer) + if err != nil { + return err + } + container.RWLayer = rwLayer + + return nil +} + +// VolumeCreate creates a volume with the specified name, driver, and opts +// This is called directly from the remote API +func (daemon *Daemon) VolumeCreate(name, driverName string, opts, labels map[string]string) (*types.Volume, error) { + if name == "" { + name = stringid.GenerateNonCryptoID() + } + + v, err := daemon.volumes.Create(name, driverName, opts, labels) + if err != nil { + if volumestore.IsNameConflict(err) { + return nil, fmt.Errorf("A volume named %s already exists. Choose a different volume name.", name) + } + return nil, err + } + + daemon.LogVolumeEvent(v.Name(), "create", map[string]string{"driver": v.DriverName()}) + return volumeToAPIType(v), nil +} diff --git a/daemon/create_unix.go b/daemon/create_unix.go new file mode 100644 index 00000000..37c4a911 --- /dev/null +++ b/daemon/create_unix.go @@ -0,0 +1,76 @@ +// +build !windows + +package daemon + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/container" + "github.com/docker/docker/pkg/stringid" + containertypes "github.com/docker/engine-api/types/container" + "github.com/opencontainers/runc/libcontainer/label" +) + +// createContainerPlatformSpecificSettings performs platform specific container create functionality +func (daemon *Daemon) createContainerPlatformSpecificSettings(container *container.Container, config *containertypes.Config, hostConfig *containertypes.HostConfig) error { + if err := daemon.Mount(container); err != nil { + return err + } + defer daemon.Unmount(container) + + rootUID, rootGID := daemon.GetRemappedUIDGID() + if err := container.SetupWorkingDirectory(rootUID, rootGID); err != nil { + return err + } + + for spec := range config.Volumes { + name := stringid.GenerateNonCryptoID() + destination := filepath.Clean(spec) + + // Skip volumes for which we already have something mounted on that + // destination because of a --volume-from. + if container.IsDestinationMounted(destination) { + continue + } + path, err := container.GetResourcePath(destination) + if err != nil { + return err + } + + stat, err := os.Stat(path) + if err == nil && !stat.IsDir() { + return fmt.Errorf("cannot mount volume over existing file, file exists %s", path) + } + + v, err := daemon.volumes.CreateWithRef(name, hostConfig.VolumeDriver, container.ID, nil, nil) + if err != nil { + return err + } + + if err := label.Relabel(v.Path(), container.MountLabel, true); err != nil { + return err + } + + container.AddMountPointWithVolume(destination, v, true) + } + return daemon.populateVolumes(container) +} + +// populateVolumes copies data from the container's rootfs into the volume for non-binds. +// this is only called when the container is created. +func (daemon *Daemon) populateVolumes(c *container.Container) error { + for _, mnt := range c.MountPoints { + if !mnt.CopyData || mnt.Volume == nil { + continue + } + + logrus.Debugf("copying image data from %s:%s, to %s", c.ID, mnt.Destination, mnt.Name) + if err := c.CopyImagePathContent(mnt.Volume, mnt.Destination); err != nil { + return err + } + } + return nil +} diff --git a/daemon/create_windows.go b/daemon/create_windows.go new file mode 100644 index 00000000..6bb356ad --- /dev/null +++ b/daemon/create_windows.go @@ -0,0 +1,75 @@ +package daemon + +import ( + "fmt" + + "github.com/docker/docker/container" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/volume" + containertypes "github.com/docker/engine-api/types/container" +) + +// createContainerPlatformSpecificSettings performs platform specific container create functionality +func (daemon *Daemon) createContainerPlatformSpecificSettings(container *container.Container, config *containertypes.Config, hostConfig *containertypes.HostConfig) error { + for spec := range config.Volumes { + + mp, err := volume.ParseMountSpec(spec, hostConfig.VolumeDriver) + if err != nil { + return fmt.Errorf("Unrecognised volume spec: %v", err) + } + + // If the mountpoint doesn't have a name, generate one. + if len(mp.Name) == 0 { + mp.Name = stringid.GenerateNonCryptoID() + } + + // Skip volumes for which we already have something mounted on that + // destination because of a --volume-from. + if container.IsDestinationMounted(mp.Destination) { + continue + } + + volumeDriver := hostConfig.VolumeDriver + + // Create the volume in the volume driver. If it doesn't exist, + // a new one will be created. + v, err := daemon.volumes.CreateWithRef(mp.Name, volumeDriver, container.ID, nil, nil) + if err != nil { + return err + } + + // FIXME Windows: This code block is present in the Linux version and + // allows the contents to be copied to the container FS prior to it + // being started. However, the function utilizes the FollowSymLinkInScope + // path which does not cope with Windows volume-style file paths. There + // is a separate effort to resolve this (@swernli), so this processing + // is deferred for now. A case where this would be useful is when + // a dockerfile includes a VOLUME statement, but something is created + // in that directory during the dockerfile processing. What this means + // on Windows for TP4 is that in that scenario, the contents will not + // copied, but that's (somewhat) OK as HCS will bomb out soon after + // at it doesn't support mapped directories which have contents in the + // destination path anyway. + // + // Example for repro later: + // FROM windowsservercore + // RUN mkdir c:\myvol + // RUN copy c:\windows\system32\ntdll.dll c:\myvol + // VOLUME "c:\myvol" + // + // Then + // docker build -t vol . + // docker run -it --rm vol cmd <-- This is where HCS will error out. + // + // // never attempt to copy existing content in a container FS to a shared volume + // if v.DriverName() == volume.DefaultDriverName { + // if err := container.CopyImagePathContent(v, mp.Destination); err != nil { + // return err + // } + // } + + // Add it to container.MountPoints + container.AddMountPointWithVolume(mp.Destination, v, mp.RW) + } + return nil +} diff --git a/daemon/daemon.go b/daemon/daemon.go new file mode 100644 index 00000000..25025960 --- /dev/null +++ b/daemon/daemon.go @@ -0,0 +1,1765 @@ +// Package daemon exposes the functions that occur on the host server +// that the Docker daemon is running. +// +// In implementing the various functions of the daemon, there is often +// a method-specific struct for configuring the runtime behavior. +package daemon + +import ( + "fmt" + "io" + "io/ioutil" + "net" + "os" + "path" + "path/filepath" + "runtime" + "strings" + "sync" + "syscall" + "time" + + "github.com/Sirupsen/logrus" + containerd "github.com/docker/containerd/api/grpc/types" + "github.com/docker/docker/api" + "github.com/docker/docker/builder" + "github.com/docker/docker/container" + "github.com/docker/docker/daemon/events" + "github.com/docker/docker/daemon/exec" + "github.com/docker/docker/errors" + "github.com/docker/engine-api/types" + containertypes "github.com/docker/engine-api/types/container" + eventtypes "github.com/docker/engine-api/types/events" + "github.com/docker/engine-api/types/filters" + networktypes "github.com/docker/engine-api/types/network" + registrytypes "github.com/docker/engine-api/types/registry" + "github.com/docker/engine-api/types/strslice" + // register graph drivers + _ "github.com/docker/docker/daemon/graphdriver/register" + "github.com/docker/docker/daemon/logger" + "github.com/docker/docker/daemon/network" + "github.com/docker/docker/distribution" + dmetadata "github.com/docker/docker/distribution/metadata" + "github.com/docker/docker/distribution/xfer" + "github.com/docker/docker/dockerversion" + "github.com/docker/docker/image" + "github.com/docker/docker/image/tarexport" + "github.com/docker/docker/layer" + "github.com/docker/docker/libcontainerd" + "github.com/docker/docker/migrate/v1" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/fileutils" + "github.com/docker/docker/pkg/graphdb" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/namesgenerator" + "github.com/docker/docker/pkg/progress" + "github.com/docker/docker/pkg/registrar" + "github.com/docker/docker/pkg/signal" + "github.com/docker/docker/pkg/streamformatter" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/sysinfo" + "github.com/docker/docker/pkg/system" + "github.com/docker/docker/pkg/truncindex" + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" + "github.com/docker/docker/runconfig" + "github.com/docker/docker/utils" + volumedrivers "github.com/docker/docker/volume/drivers" + "github.com/docker/docker/volume/local" + "github.com/docker/docker/volume/store" + "github.com/docker/go-connections/nat" + "github.com/docker/libnetwork" + nwconfig "github.com/docker/libnetwork/config" + "github.com/docker/libtrust" + "golang.org/x/net/context" +) + +const ( + // maxDownloadConcurrency is the maximum number of downloads that + // may take place at a time for each pull. + maxDownloadConcurrency = 3 + // maxUploadConcurrency is the maximum number of uploads that + // may take place at a time for each push. + maxUploadConcurrency = 5 +) + +var ( + validContainerNameChars = utils.RestrictedNameChars + validContainerNamePattern = utils.RestrictedNamePattern + + errSystemNotSupported = fmt.Errorf("The Docker daemon is not supported on this platform.") +) + +// ErrImageDoesNotExist is error returned when no image can be found for a reference. +type ErrImageDoesNotExist struct { + RefOrID string +} + +func (e ErrImageDoesNotExist) Error() string { + return fmt.Sprintf("no such id: %s", e.RefOrID) +} + +// Daemon holds information about the Docker daemon. +type Daemon struct { + ID string + repository string + containers container.Store + execCommands *exec.Store + referenceStore reference.Store + downloadManager *xfer.LayerDownloadManager + uploadManager *xfer.LayerUploadManager + distributionMetadataStore dmetadata.Store + trustKey libtrust.PrivateKey + idIndex *truncindex.TruncIndex + configStore *Config + statsCollector *statsCollector + defaultLogConfig containertypes.LogConfig + RegistryService *registry.Service + EventsService *events.Events + netController libnetwork.NetworkController + volumes *store.VolumeStore + discoveryWatcher discoveryReloader + root string + seccompEnabled bool + shutdown bool + uidMaps []idtools.IDMap + gidMaps []idtools.IDMap + layerStore layer.Store + imageStore image.Store + nameIndex *registrar.Registrar + linkIndex *linkIndex + containerd libcontainerd.Client + defaultIsolation containertypes.Isolation // Default isolation mode on Windows +} + +// GetContainer looks for a container using the provided information, which could be +// one of the following inputs from the caller: +// - A full container ID, which will exact match a container in daemon's list +// - A container name, which will only exact match via the GetByName() function +// - A partial container ID prefix (e.g. short ID) of any length that is +// unique enough to only return a single container object +// If none of these searches succeed, an error is returned +func (daemon *Daemon) GetContainer(prefixOrName string) (*container.Container, error) { + if len(prefixOrName) == 0 { + return nil, errors.NewBadRequestError(fmt.Errorf("No container name or ID supplied")) + } + + if containerByID := daemon.containers.Get(prefixOrName); containerByID != nil { + // prefix is an exact match to a full container ID + return containerByID, nil + } + + // GetByName will match only an exact name provided; we ignore errors + if containerByName, _ := daemon.GetByName(prefixOrName); containerByName != nil { + // prefix is an exact match to a full container Name + return containerByName, nil + } + + containerID, indexError := daemon.idIndex.Get(prefixOrName) + if indexError != nil { + // When truncindex defines an error type, use that instead + if indexError == truncindex.ErrNotExist { + err := fmt.Errorf("No such container: %s", prefixOrName) + return nil, errors.NewRequestNotFoundError(err) + } + return nil, indexError + } + return daemon.containers.Get(containerID), nil +} + +// Exists returns a true if a container of the specified ID or name exists, +// false otherwise. +func (daemon *Daemon) Exists(id string) bool { + c, _ := daemon.GetContainer(id) + return c != nil +} + +// IsPaused returns a bool indicating if the specified container is paused. +func (daemon *Daemon) IsPaused(id string) bool { + c, _ := daemon.GetContainer(id) + return c.State.IsPaused() +} + +func (daemon *Daemon) containerRoot(id string) string { + return filepath.Join(daemon.repository, id) +} + +// Load reads the contents of a container from disk +// This is typically done at startup. +func (daemon *Daemon) load(id string) (*container.Container, error) { + container := daemon.newBaseContainer(id) + + if err := container.FromDisk(); err != nil { + return nil, err + } + + if container.ID != id { + return container, fmt.Errorf("Container %s is stored at %s", container.ID, id) + } + + return container, nil +} + +func (daemon *Daemon) registerName(container *container.Container) error { + if daemon.Exists(container.ID) { + return fmt.Errorf("Container is already loaded") + } + if err := validateID(container.ID); err != nil { + return err + } + if container.Name == "" { + name, err := daemon.generateNewName(container.ID) + if err != nil { + return err + } + container.Name = name + + if err := container.ToDiskLocking(); err != nil { + logrus.Errorf("Error saving container name to disk: %v", err) + } + } + return daemon.nameIndex.Reserve(container.Name, container.ID) +} + +// Register makes a container object usable by the daemon as +func (daemon *Daemon) Register(c *container.Container) error { + // Attach to stdout and stderr + if c.Config.OpenStdin { + c.NewInputPipes() + } else { + c.NewNopInputPipe() + } + + daemon.containers.Add(c.ID, c) + daemon.idIndex.Add(c.ID) + + return nil +} + +func (daemon *Daemon) restore() error { + var ( + debug = utils.IsDebugEnabled() + currentDriver = daemon.GraphDriverName() + containers = make(map[string]*container.Container) + ) + + if !debug { + logrus.Info("Loading containers: start.") + } + dir, err := ioutil.ReadDir(daemon.repository) + if err != nil { + return err + } + + for _, v := range dir { + id := v.Name() + container, err := daemon.load(id) + if !debug && logrus.GetLevel() == logrus.InfoLevel { + fmt.Print(".") + } + if err != nil { + logrus.Errorf("Failed to load container %v: %v", id, err) + continue + } + + // Ignore the container if it does not support the current driver being used by the graph + if (container.Driver == "" && currentDriver == "aufs") || container.Driver == currentDriver { + rwlayer, err := daemon.layerStore.GetRWLayer(container.ID) + if err != nil { + logrus.Errorf("Failed to load container mount %v: %v", id, err) + continue + } + container.RWLayer = rwlayer + logrus.Debugf("Loaded container %v", container.ID) + + containers[container.ID] = container + } else { + logrus.Debugf("Cannot load container %s because it was created with another graph driver.", container.ID) + } + } + + var migrateLegacyLinks bool + restartContainers := make(map[*container.Container]chan struct{}) + for _, c := range containers { + if err := daemon.registerName(c); err != nil { + logrus.Errorf("Failed to register container %s: %s", c.ID, err) + continue + } + if err := daemon.Register(c); err != nil { + logrus.Errorf("Failed to register container %s: %s", c.ID, err) + continue + } + } + var wg sync.WaitGroup + var mapLock sync.Mutex + for _, c := range containers { + wg.Add(1) + go func(c *container.Container) { + defer wg.Done() + if c.IsRunning() || c.IsPaused() { + // Fix activityCount such that graph mounts can be unmounted later + if err := daemon.layerStore.ReinitRWLayer(c.RWLayer); err != nil { + logrus.Errorf("Failed to ReinitRWLayer for %s due to %s", c.ID, err) + return + } + if err := daemon.containerd.Restore(c.ID, libcontainerd.WithRestartManager(c.RestartManager(true))); err != nil { + logrus.Errorf("Failed to restore with containerd: %q", err) + return + } + } + // fixme: only if not running + // get list of containers we need to restart + if daemon.configStore.AutoRestart && !c.IsRunning() && !c.IsPaused() && c.ShouldRestartOnBoot() { + mapLock.Lock() + restartContainers[c] = make(chan struct{}) + mapLock.Unlock() + } + + // if c.hostConfig.Links is nil (not just empty), then it is using the old sqlite links and needs to be migrated + if c.HostConfig != nil && c.HostConfig.Links == nil { + migrateLegacyLinks = true + } + }(c) + } + wg.Wait() + + // migrate any legacy links from sqlite + linkdbFile := filepath.Join(daemon.root, "linkgraph.db") + var legacyLinkDB *graphdb.Database + if migrateLegacyLinks { + legacyLinkDB, err = graphdb.NewSqliteConn(linkdbFile) + if err != nil { + return fmt.Errorf("error connecting to legacy link graph DB %s, container links may be lost: %v", linkdbFile, err) + } + defer legacyLinkDB.Close() + } + + // Now that all the containers are registered, register the links + for _, c := range containers { + if migrateLegacyLinks { + if err := daemon.migrateLegacySqliteLinks(legacyLinkDB, c); err != nil { + return err + } + } + if err := daemon.registerLinks(c, c.HostConfig); err != nil { + logrus.Errorf("failed to register link for container %s: %v", c.ID, err) + } + } + + group := sync.WaitGroup{} + for c, notifier := range restartContainers { + group.Add(1) + + go func(c *container.Container, chNotify chan struct{}) { + defer group.Done() + + logrus.Debugf("Starting container %s", c.ID) + + // ignore errors here as this is a best effort to wait for children to be + // running before we try to start the container + children := daemon.children(c) + timeout := time.After(5 * time.Second) + for _, child := range children { + if notifier, exists := restartContainers[child]; exists { + select { + case <-notifier: + case <-timeout: + } + } + } + + // Make sure networks are available before starting + daemon.waitForNetworks(c) + if err := daemon.containerStart(c); err != nil { + logrus.Errorf("Failed to start container %s: %s", c.ID, err) + } + close(chNotify) + }(c, notifier) + + } + group.Wait() + + // any containers that were started above would already have had this done, + // however we need to now prepare the mountpoints for the rest of the containers as well. + // This shouldn't cause any issue running on the containers that already had this run. + // This must be run after any containers with a restart policy so that containerized plugins + // can have a chance to be running before we try to initialize them. + for _, c := range containers { + // if the container has restart policy, do not + // prepare the mountpoints since it has been done on restarting. + // This is to speed up the daemon start when a restart container + // has a volume and the volume dirver is not available. + if _, ok := restartContainers[c]; ok { + continue + } + group.Add(1) + go func(c *container.Container) { + defer group.Done() + if err := daemon.prepareMountPoints(c); err != nil { + logrus.Error(err) + } + }(c) + } + + group.Wait() + + if !debug { + if logrus.GetLevel() == logrus.InfoLevel { + fmt.Println() + } + logrus.Info("Loading containers: done.") + } + + return nil +} + +// waitForNetworks is used during daemon initialization when starting up containers +// It ensures that all of a container's networks are available before the daemon tries to start the container. +// In practice it just makes sure the discovery service is available for containers which use a network that require discovery. +func (daemon *Daemon) waitForNetworks(c *container.Container) { + if daemon.discoveryWatcher == nil { + return + } + // Make sure if the container has a network that requires discovery that the discovery service is available before starting + for netName := range c.NetworkSettings.Networks { + // If we get `ErrNoSuchNetwork` here, it can assumed that it is due to discovery not being ready + // Most likely this is because the K/V store used for discovery is in a container and needs to be started + if _, err := daemon.netController.NetworkByName(netName); err != nil { + if _, ok := err.(libnetwork.ErrNoSuchNetwork); !ok { + continue + } + // use a longish timeout here due to some slowdowns in libnetwork if the k/v store is on anything other than --net=host + // FIXME: why is this slow??? + logrus.Debugf("Container %s waiting for network to be ready", c.Name) + select { + case <-daemon.discoveryWatcher.ReadyCh(): + case <-time.After(60 * time.Second): + } + return + } + } +} + +func (daemon *Daemon) mergeAndVerifyConfig(config *containertypes.Config, img *image.Image) error { + if img != nil && img.Config != nil { + if err := merge(config, img.Config); err != nil { + return err + } + } + if len(config.Entrypoint) == 0 && len(config.Cmd) == 0 { + return fmt.Errorf("No command specified") + } + return nil +} + +func (daemon *Daemon) generateIDAndName(name string) (string, string, error) { + var ( + err error + id = stringid.GenerateNonCryptoID() + ) + + if name == "" { + if name, err = daemon.generateNewName(id); err != nil { + return "", "", err + } + return id, name, nil + } + + if name, err = daemon.reserveName(id, name); err != nil { + return "", "", err + } + + return id, name, nil +} + +func (daemon *Daemon) reserveName(id, name string) (string, error) { + if !validContainerNamePattern.MatchString(name) { + return "", fmt.Errorf("Invalid container name (%s), only %s are allowed", name, validContainerNameChars) + } + if name[0] != '/' { + name = "/" + name + } + + if err := daemon.nameIndex.Reserve(name, id); err != nil { + if err == registrar.ErrNameReserved { + id, err := daemon.nameIndex.Get(name) + if err != nil { + logrus.Errorf("got unexpected error while looking up reserved name: %v", err) + return "", err + } + return "", fmt.Errorf("Conflict. The name %q is already in use by container %s. You have to remove (or rename) that container to be able to reuse that name.", name, id) + } + return "", fmt.Errorf("error reserving name: %s, error: %v", name, err) + } + return name, nil +} + +func (daemon *Daemon) releaseName(name string) { + daemon.nameIndex.Release(name) +} + +func (daemon *Daemon) generateNewName(id string) (string, error) { + var name string + for i := 0; i < 6; i++ { + name = namesgenerator.GetRandomName(i) + if name[0] != '/' { + name = "/" + name + } + + if err := daemon.nameIndex.Reserve(name, id); err != nil { + if err == registrar.ErrNameReserved { + continue + } + return "", err + } + return name, nil + } + + name = "/" + stringid.TruncateID(id) + if err := daemon.nameIndex.Reserve(name, id); err != nil { + return "", err + } + return name, nil +} + +func (daemon *Daemon) generateHostname(id string, config *containertypes.Config) { + // Generate default hostname + if config.Hostname == "" { + config.Hostname = id[:12] + } +} + +func (daemon *Daemon) getEntrypointAndArgs(configEntrypoint strslice.StrSlice, configCmd strslice.StrSlice) (string, []string) { + if len(configEntrypoint) != 0 { + return configEntrypoint[0], append(configEntrypoint[1:], configCmd...) + } + return configCmd[0], configCmd[1:] +} + +func (daemon *Daemon) newContainer(name string, config *containertypes.Config, imgID image.ID) (*container.Container, error) { + var ( + id string + err error + noExplicitName = name == "" + ) + id, name, err = daemon.generateIDAndName(name) + if err != nil { + return nil, err + } + + daemon.generateHostname(id, config) + entrypoint, args := daemon.getEntrypointAndArgs(config.Entrypoint, config.Cmd) + + base := daemon.newBaseContainer(id) + base.Created = time.Now().UTC() + base.Path = entrypoint + base.Args = args //FIXME: de-duplicate from config + base.Config = config + base.HostConfig = &containertypes.HostConfig{} + base.ImageID = imgID + base.NetworkSettings = &network.Settings{IsAnonymousEndpoint: noExplicitName} + base.Name = name + base.Driver = daemon.GraphDriverName() + + return base, err +} + +// GetByName returns a container given a name. +func (daemon *Daemon) GetByName(name string) (*container.Container, error) { + if len(name) == 0 { + return nil, fmt.Errorf("No container name supplied") + } + fullName := name + if name[0] != '/' { + fullName = "/" + name + } + id, err := daemon.nameIndex.Get(fullName) + if err != nil { + return nil, fmt.Errorf("Could not find entity for %s", name) + } + e := daemon.containers.Get(id) + if e == nil { + return nil, fmt.Errorf("Could not find container for entity id %s", id) + } + return e, nil +} + +// SubscribeToEvents returns the currently record of events, a channel to stream new events from, and a function to cancel the stream of events. +func (daemon *Daemon) SubscribeToEvents(since, sinceNano int64, filter filters.Args) ([]eventtypes.Message, chan interface{}) { + ef := events.NewFilter(filter) + return daemon.EventsService.SubscribeTopic(since, sinceNano, ef) +} + +// UnsubscribeFromEvents stops the event subscription for a client by closing the +// channel where the daemon sends events to. +func (daemon *Daemon) UnsubscribeFromEvents(listener chan interface{}) { + daemon.EventsService.Evict(listener) +} + +// GetLabels for a container or image id +func (daemon *Daemon) GetLabels(id string) map[string]string { + // TODO: TestCase + container := daemon.containers.Get(id) + if container != nil { + return container.Config.Labels + } + + img, err := daemon.GetImage(id) + if err == nil { + return img.ContainerConfig.Labels + } + return nil +} + +func (daemon *Daemon) children(c *container.Container) map[string]*container.Container { + return daemon.linkIndex.children(c) +} + +// parents returns the names of the parent containers of the container +// with the given name. +func (daemon *Daemon) parents(c *container.Container) map[string]*container.Container { + return daemon.linkIndex.parents(c) +} + +func (daemon *Daemon) registerLink(parent, child *container.Container, alias string) error { + fullName := path.Join(parent.Name, alias) + if err := daemon.nameIndex.Reserve(fullName, child.ID); err != nil { + if err == registrar.ErrNameReserved { + logrus.Warnf("error registering link for %s, to %s, as alias %s, ignoring: %v", parent.ID, child.ID, alias, err) + return nil + } + return err + } + daemon.linkIndex.link(parent, child, fullName) + return nil +} + +// NewDaemon sets up everything for the daemon to be able to service +// requests from the webserver. +func NewDaemon(config *Config, registryService *registry.Service, containerdRemote libcontainerd.Remote) (daemon *Daemon, err error) { + setDefaultMtu(config) + + // Ensure we have compatible and valid configuration options + if err := verifyDaemonSettings(config); err != nil { + return nil, err + } + + // Do we have a disabled network? + config.DisableBridge = isBridgeNetworkDisabled(config) + + // Verify the platform is supported as a daemon + if !platformSupported { + return nil, errSystemNotSupported + } + + // Validate platform-specific requirements + if err := checkSystem(); err != nil { + return nil, err + } + + // set up SIGUSR1 handler on Unix-like systems, or a Win32 global event + // on Windows to dump Go routine stacks + setupDumpStackTrap() + + uidMaps, gidMaps, err := setupRemappedRoot(config) + if err != nil { + return nil, err + } + rootUID, rootGID, err := idtools.GetRootUIDGID(uidMaps, gidMaps) + if err != nil { + return nil, err + } + + // get the canonical path to the Docker root directory + var realRoot string + if _, err := os.Stat(config.Root); err != nil && os.IsNotExist(err) { + realRoot = config.Root + } else { + realRoot, err = fileutils.ReadSymlinkedDirectory(config.Root) + if err != nil { + return nil, fmt.Errorf("Unable to get the full path to root (%s): %s", config.Root, err) + } + } + + if err = setupDaemonRoot(config, realRoot, rootUID, rootGID); err != nil { + return nil, err + } + + // set up the tmpDir to use a canonical path + tmp, err := tempDir(config.Root, rootUID, rootGID) + if err != nil { + return nil, fmt.Errorf("Unable to get the TempDir under %s: %s", config.Root, err) + } + realTmp, err := fileutils.ReadSymlinkedDirectory(tmp) + if err != nil { + return nil, fmt.Errorf("Unable to get the full path to the TempDir (%s): %s", tmp, err) + } + os.Setenv("TMPDIR", realTmp) + + d := &Daemon{configStore: config} + // Ensure the daemon is properly shutdown if there is a failure during + // initialization + defer func() { + if err != nil { + if err := d.Shutdown(); err != nil { + logrus.Error(err) + } + } + }() + + // Set the default isolation mode (only applicable on Windows) + if err := d.setDefaultIsolation(); err != nil { + return nil, fmt.Errorf("error setting default isolation mode: %v", err) + } + + // Verify logging driver type + if config.LogConfig.Type != "none" { + if _, err := logger.GetLogDriver(config.LogConfig.Type); err != nil { + return nil, fmt.Errorf("error finding the logging driver: %v", err) + } + } + logrus.Debugf("Using default logging driver %s", config.LogConfig.Type) + + if err := configureMaxThreads(config); err != nil { + logrus.Warnf("Failed to configure golang's threads limit: %v", err) + } + + installDefaultAppArmorProfile() + daemonRepo := filepath.Join(config.Root, "containers") + if err := idtools.MkdirAllAs(daemonRepo, 0700, rootUID, rootGID); err != nil && !os.IsExist(err) { + return nil, err + } + + driverName := os.Getenv("DOCKER_DRIVER") + if driverName == "" { + driverName = config.GraphDriver + } + d.layerStore, err = layer.NewStoreFromOptions(layer.StoreOptions{ + StorePath: config.Root, + MetadataStorePathTemplate: filepath.Join(config.Root, "image", "%s", "layerdb"), + GraphDriver: driverName, + GraphDriverOptions: config.GraphOptions, + UIDMaps: uidMaps, + GIDMaps: gidMaps, + }) + if err != nil { + return nil, err + } + + graphDriver := d.layerStore.DriverName() + imageRoot := filepath.Join(config.Root, "image", graphDriver) + + // Configure and validate the kernels security support + if err := configureKernelSecuritySupport(config, graphDriver); err != nil { + return nil, err + } + + d.downloadManager = xfer.NewLayerDownloadManager(d.layerStore, maxDownloadConcurrency) + d.uploadManager = xfer.NewLayerUploadManager(maxUploadConcurrency) + + ifs, err := image.NewFSStoreBackend(filepath.Join(imageRoot, "imagedb")) + if err != nil { + return nil, err + } + + d.imageStore, err = image.NewImageStore(ifs, d.layerStore) + if err != nil { + return nil, err + } + + // Configure the volumes driver + volStore, err := configureVolumes(config, rootUID, rootGID) + if err != nil { + return nil, err + } + + trustKey, err := api.LoadOrCreateTrustKey(config.TrustKeyPath) + if err != nil { + return nil, err + } + + trustDir := filepath.Join(config.Root, "trust") + + if err := system.MkdirAll(trustDir, 0700); err != nil { + return nil, err + } + + distributionMetadataStore, err := dmetadata.NewFSMetadataStore(filepath.Join(imageRoot, "distribution")) + if err != nil { + return nil, err + } + + eventsService := events.New() + + referenceStore, err := reference.NewReferenceStore(filepath.Join(imageRoot, "repositories.json")) + if err != nil { + return nil, fmt.Errorf("Couldn't create Tag store repositories: %s", err) + } + + if err := restoreCustomImage(d.imageStore, d.layerStore, referenceStore); err != nil { + return nil, fmt.Errorf("Couldn't restore custom images: %s", err) + } + + migrationStart := time.Now() + if err := v1.Migrate(config.Root, graphDriver, d.layerStore, d.imageStore, referenceStore, distributionMetadataStore); err != nil { + logrus.Errorf("Graph migration failed: %q. Your old graph data was found to be too inconsistent for upgrading to content-addressable storage. Some of the old data was probably not upgraded. We recommend starting over with a clean storage directory if possible.", err) + } + logrus.Infof("Graph migration to content-addressability took %.2f seconds", time.Since(migrationStart).Seconds()) + + // Discovery is only enabled when the daemon is launched with an address to advertise. When + // initialized, the daemon is registered and we can store the discovery backend as its read-only + if err := d.initDiscovery(config); err != nil { + return nil, err + } + + d.netController, err = d.initNetworkController(config) + if err != nil { + return nil, fmt.Errorf("Error initializing network controller: %v", err) + } + + sysInfo := sysinfo.New(false) + // Check if Devices cgroup is mounted, it is hard requirement for container security, + // on Linux/FreeBSD. + if runtime.GOOS != "windows" && !sysInfo.CgroupDevicesEnabled { + return nil, fmt.Errorf("Devices cgroup isn't mounted") + } + + d.ID = trustKey.PublicKey().KeyID() + d.repository = daemonRepo + d.containers = container.NewMemoryStore() + d.execCommands = exec.NewStore() + d.referenceStore = referenceStore + d.distributionMetadataStore = distributionMetadataStore + d.trustKey = trustKey + d.idIndex = truncindex.NewTruncIndex([]string{}) + d.statsCollector = d.newStatsCollector(1 * time.Second) + d.defaultLogConfig = containertypes.LogConfig{ + Type: config.LogConfig.Type, + Config: config.LogConfig.Config, + } + d.RegistryService = registryService + d.EventsService = eventsService + d.volumes = volStore + d.root = config.Root + d.uidMaps = uidMaps + d.gidMaps = gidMaps + d.seccompEnabled = sysInfo.Seccomp + + d.nameIndex = registrar.NewRegistrar() + d.linkIndex = newLinkIndex() + + go d.execCommandGC() + + d.containerd, err = containerdRemote.Client(d) + if err != nil { + return nil, err + } + + if err := d.restore(); err != nil { + return nil, err + } + + return d, nil +} + +func (daemon *Daemon) shutdownContainer(c *container.Container) error { + // TODO(windows): Handle docker restart with paused containers + if c.IsPaused() { + // To terminate a process in freezer cgroup, we should send + // SIGTERM to this process then unfreeze it, and the process will + // force to terminate immediately. + logrus.Debugf("Found container %s is paused, sending SIGTERM before unpause it", c.ID) + sig, ok := signal.SignalMap["TERM"] + if !ok { + return fmt.Errorf("System doesn not support SIGTERM") + } + if err := daemon.kill(c, int(sig)); err != nil { + return fmt.Errorf("sending SIGTERM to container %s with error: %v", c.ID, err) + } + if err := daemon.containerUnpause(c); err != nil { + return fmt.Errorf("Failed to unpause container %s with error: %v", c.ID, err) + } + if _, err := c.WaitStop(10 * time.Second); err != nil { + logrus.Debugf("container %s failed to exit in 10 second of SIGTERM, sending SIGKILL to force", c.ID) + sig, ok := signal.SignalMap["KILL"] + if !ok { + return fmt.Errorf("System does not support SIGKILL") + } + if err := daemon.kill(c, int(sig)); err != nil { + logrus.Errorf("Failed to SIGKILL container %s", c.ID) + } + c.WaitStop(-1 * time.Second) + return err + } + } + // If container failed to exit in 10 seconds of SIGTERM, then using the force + if err := daemon.containerStop(c, 10); err != nil { + return fmt.Errorf("Stop container %s with error: %v", c.ID, err) + } + + c.WaitStop(-1 * time.Second) + return nil +} + +// Shutdown stops the daemon. +func (daemon *Daemon) Shutdown() error { + daemon.shutdown = true + if daemon.containers != nil { + logrus.Debug("starting clean shutdown of all containers...") + daemon.containers.ApplyAll(func(c *container.Container) { + if !c.IsRunning() { + return + } + logrus.Debugf("stopping %s", c.ID) + if err := daemon.shutdownContainer(c); err != nil { + logrus.Errorf("Stop container error: %v", err) + return + } + if mountid, err := daemon.layerStore.GetMountID(c.ID); err == nil { + daemon.cleanupMountsByID(mountid) + } + logrus.Debugf("container stopped %s", c.ID) + }) + } + + // trigger libnetwork Stop only if it's initialized + if daemon.netController != nil { + daemon.netController.Stop() + } + + if daemon.layerStore != nil { + if err := daemon.layerStore.Cleanup(); err != nil { + logrus.Errorf("Error during layer Store.Cleanup(): %v", err) + } + } + + if err := daemon.cleanupMounts(); err != nil { + return err + } + + return nil +} + +// Mount sets container.BaseFS +// (is it not set coming in? why is it unset?) +func (daemon *Daemon) Mount(container *container.Container) error { + dir, err := container.RWLayer.Mount(container.GetMountLabel()) + if err != nil { + return err + } + logrus.Debugf("container mounted via layerStore: %v", dir) + + if container.BaseFS != dir { + // The mount path reported by the graph driver should always be trusted on Windows, since the + // volume path for a given mounted layer may change over time. This should only be an error + // on non-Windows operating systems. + if container.BaseFS != "" && runtime.GOOS != "windows" { + daemon.Unmount(container) + return fmt.Errorf("Error: driver %s is returning inconsistent paths for container %s ('%s' then '%s')", + daemon.GraphDriverName(), container.ID, container.BaseFS, dir) + } + } + container.BaseFS = dir // TODO: combine these fields + return nil +} + +// Unmount unsets the container base filesystem +func (daemon *Daemon) Unmount(container *container.Container) error { + if err := container.RWLayer.Unmount(); err != nil { + logrus.Errorf("Error unmounting container %s: %s", container.ID, err) + return err + } + return nil +} + +func (daemon *Daemon) kill(c *container.Container, sig int) error { + return daemon.containerd.Signal(c.ID, sig) +} + +func (daemon *Daemon) subscribeToContainerStats(c *container.Container) chan interface{} { + return daemon.statsCollector.collect(c) +} + +func (daemon *Daemon) unsubscribeToContainerStats(c *container.Container, ch chan interface{}) { + daemon.statsCollector.unsubscribe(c, ch) +} + +func (daemon *Daemon) changes(container *container.Container) ([]archive.Change, error) { + return container.RWLayer.Changes() +} + +// TagImage creates the tag specified by newTag, pointing to the image named +// imageName (alternatively, imageName can also be an image ID). +func (daemon *Daemon) TagImage(newTag reference.Named, imageName string) error { + imageID, err := daemon.GetImageID(imageName) + if err != nil { + return err + } + if err := daemon.referenceStore.AddTag(newTag, imageID, true); err != nil { + return err + } + + daemon.LogImageEvent(imageID.String(), newTag.String(), "tag") + return nil +} + +func writeDistributionProgress(cancelFunc func(), outStream io.Writer, progressChan <-chan progress.Progress) { + progressOutput := streamformatter.NewJSONStreamFormatter().NewProgressOutput(outStream, false) + operationCancelled := false + + for prog := range progressChan { + if err := progressOutput.WriteProgress(prog); err != nil && !operationCancelled { + // don't log broken pipe errors as this is the normal case when a client aborts + if isBrokenPipe(err) { + logrus.Info("Pull session cancelled") + } else { + logrus.Errorf("error writing progress to client: %v", err) + } + cancelFunc() + operationCancelled = true + // Don't return, because we need to continue draining + // progressChan until it's closed to avoid a deadlock. + } + } +} + +func isBrokenPipe(e error) bool { + if netErr, ok := e.(*net.OpError); ok { + e = netErr.Err + if sysErr, ok := netErr.Err.(*os.SyscallError); ok { + e = sysErr.Err + } + } + return e == syscall.EPIPE +} + +// PullImage initiates a pull operation. image is the repository name to pull, and +// tag may be either empty, or indicate a specific tag to pull. +func (daemon *Daemon) PullImage(ctx context.Context, ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error { + // Include a buffer so that slow client connections don't affect + // transfer performance. + progressChan := make(chan progress.Progress, 100) + + writesDone := make(chan struct{}) + + ctx, cancelFunc := context.WithCancel(ctx) + + go func() { + writeDistributionProgress(cancelFunc, outStream, progressChan) + close(writesDone) + }() + + imagePullConfig := &distribution.ImagePullConfig{ + MetaHeaders: metaHeaders, + AuthConfig: authConfig, + ProgressOutput: progress.ChanOutput(progressChan), + RegistryService: daemon.RegistryService, + ImageEventLogger: daemon.LogImageEvent, + MetadataStore: daemon.distributionMetadataStore, + ImageStore: daemon.imageStore, + ReferenceStore: daemon.referenceStore, + DownloadManager: daemon.downloadManager, + } + + err := distribution.Pull(ctx, ref, imagePullConfig) + close(progressChan) + <-writesDone + return err +} + +// PullOnBuild tells Docker to pull image referenced by `name`. +func (daemon *Daemon) PullOnBuild(ctx context.Context, name string, authConfigs map[string]types.AuthConfig, output io.Writer) (builder.Image, error) { + ref, err := reference.ParseNamed(name) + if err != nil { + return nil, err + } + ref = reference.WithDefaultTag(ref) + + pullRegistryAuth := &types.AuthConfig{} + if len(authConfigs) > 0 { + // The request came with a full auth config file, we prefer to use that + repoInfo, err := daemon.RegistryService.ResolveRepository(ref) + if err != nil { + return nil, err + } + + resolvedConfig := registry.ResolveAuthConfig( + authConfigs, + repoInfo.Index, + ) + pullRegistryAuth = &resolvedConfig + } + + if err := daemon.PullImage(ctx, ref, nil, pullRegistryAuth, output); err != nil { + return nil, err + } + return daemon.GetImage(name) +} + +// ExportImage exports a list of images to the given output stream. The +// exported images are archived into a tar when written to the output +// stream. All images with the given tag and all versions containing +// the same tag are exported. names is the set of tags to export, and +// outStream is the writer which the images are written to. +func (daemon *Daemon) ExportImage(names []string, outStream io.Writer) error { + imageExporter := tarexport.NewTarExporter(daemon.imageStore, daemon.layerStore, daemon.referenceStore) + return imageExporter.Save(names, outStream) +} + +// PushImage initiates a push operation on the repository named localName. +func (daemon *Daemon) PushImage(ctx context.Context, ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error { + // Include a buffer so that slow client connections don't affect + // transfer performance. + progressChan := make(chan progress.Progress, 100) + + writesDone := make(chan struct{}) + + ctx, cancelFunc := context.WithCancel(ctx) + + go func() { + writeDistributionProgress(cancelFunc, outStream, progressChan) + close(writesDone) + }() + + imagePushConfig := &distribution.ImagePushConfig{ + MetaHeaders: metaHeaders, + AuthConfig: authConfig, + ProgressOutput: progress.ChanOutput(progressChan), + RegistryService: daemon.RegistryService, + ImageEventLogger: daemon.LogImageEvent, + MetadataStore: daemon.distributionMetadataStore, + LayerStore: daemon.layerStore, + ImageStore: daemon.imageStore, + ReferenceStore: daemon.referenceStore, + TrustKey: daemon.trustKey, + UploadManager: daemon.uploadManager, + } + + err := distribution.Push(ctx, ref, imagePushConfig) + close(progressChan) + <-writesDone + return err +} + +// LookupImage looks up an image by name and returns it as an ImageInspect +// structure. +func (daemon *Daemon) LookupImage(name string) (*types.ImageInspect, error) { + img, err := daemon.GetImage(name) + if err != nil { + return nil, fmt.Errorf("No such image: %s", name) + } + + refs := daemon.referenceStore.References(img.ID()) + repoTags := []string{} + repoDigests := []string{} + for _, ref := range refs { + switch ref.(type) { + case reference.NamedTagged: + repoTags = append(repoTags, ref.String()) + case reference.Canonical: + repoDigests = append(repoDigests, ref.String()) + } + } + + var size int64 + var layerMetadata map[string]string + layerID := img.RootFS.ChainID() + if layerID != "" { + l, err := daemon.layerStore.Get(layerID) + if err != nil { + return nil, err + } + defer layer.ReleaseAndLog(daemon.layerStore, l) + size, err = l.Size() + if err != nil { + return nil, err + } + + layerMetadata, err = l.Metadata() + if err != nil { + return nil, err + } + } + + comment := img.Comment + if len(comment) == 0 && len(img.History) > 0 { + comment = img.History[len(img.History)-1].Comment + } + + imageInspect := &types.ImageInspect{ + ID: img.ID().String(), + RepoTags: repoTags, + RepoDigests: repoDigests, + Parent: img.Parent.String(), + Comment: comment, + Created: img.Created.Format(time.RFC3339Nano), + Container: img.Container, + ContainerConfig: &img.ContainerConfig, + DockerVersion: img.DockerVersion, + Author: img.Author, + Config: img.Config, + Architecture: img.Architecture, + Os: img.OS, + Size: size, + VirtualSize: size, // TODO: field unused, deprecate + RootFS: rootFSToAPIType(img.RootFS), + } + + imageInspect.GraphDriver.Name = daemon.GraphDriverName() + + imageInspect.GraphDriver.Data = layerMetadata + + return imageInspect, nil +} + +// LoadImage uploads a set of images into the repository. This is the +// complement of ImageExport. The input stream is an uncompressed tar +// ball containing images and metadata. +func (daemon *Daemon) LoadImage(inTar io.ReadCloser, outStream io.Writer, quiet bool) error { + imageExporter := tarexport.NewTarExporter(daemon.imageStore, daemon.layerStore, daemon.referenceStore) + return imageExporter.Load(inTar, outStream, quiet) +} + +// ImageHistory returns a slice of ImageHistory structures for the specified image +// name by walking the image lineage. +func (daemon *Daemon) ImageHistory(name string) ([]*types.ImageHistory, error) { + img, err := daemon.GetImage(name) + if err != nil { + return nil, err + } + + history := []*types.ImageHistory{} + + layerCounter := 0 + rootFS := *img.RootFS + rootFS.DiffIDs = nil + + for _, h := range img.History { + var layerSize int64 + + if !h.EmptyLayer { + if len(img.RootFS.DiffIDs) <= layerCounter { + return nil, fmt.Errorf("too many non-empty layers in History section") + } + + rootFS.Append(img.RootFS.DiffIDs[layerCounter]) + l, err := daemon.layerStore.Get(rootFS.ChainID()) + if err != nil { + return nil, err + } + layerSize, err = l.DiffSize() + layer.ReleaseAndLog(daemon.layerStore, l) + if err != nil { + return nil, err + } + + layerCounter++ + } + + history = append([]*types.ImageHistory{{ + ID: "", + Created: h.Created.Unix(), + CreatedBy: h.CreatedBy, + Comment: h.Comment, + Size: layerSize, + }}, history...) + } + + // Fill in image IDs and tags + histImg := img + id := img.ID() + for _, h := range history { + h.ID = id.String() + + var tags []string + for _, r := range daemon.referenceStore.References(id) { + if _, ok := r.(reference.NamedTagged); ok { + tags = append(tags, r.String()) + } + } + + h.Tags = tags + + id = histImg.Parent + if id == "" { + break + } + histImg, err = daemon.GetImage(id.String()) + if err != nil { + break + } + } + + return history, nil +} + +// GetImageID returns an image ID corresponding to the image referred to by +// refOrID. +func (daemon *Daemon) GetImageID(refOrID string) (image.ID, error) { + id, ref, err := reference.ParseIDOrReference(refOrID) + if err != nil { + return "", err + } + if id != "" { + if _, err := daemon.imageStore.Get(image.ID(id)); err != nil { + return "", ErrImageDoesNotExist{refOrID} + } + return image.ID(id), nil + } + + if id, err := daemon.referenceStore.Get(ref); err == nil { + return id, nil + } + if tagged, ok := ref.(reference.NamedTagged); ok { + if id, err := daemon.imageStore.Search(tagged.Tag()); err == nil { + for _, namedRef := range daemon.referenceStore.References(id) { + if namedRef.Name() == ref.Name() { + return id, nil + } + } + } + } + + // Search based on ID + if id, err := daemon.imageStore.Search(refOrID); err == nil { + return id, nil + } + + return "", ErrImageDoesNotExist{refOrID} +} + +// GetImage returns an image corresponding to the image referred to by refOrID. +func (daemon *Daemon) GetImage(refOrID string) (*image.Image, error) { + imgID, err := daemon.GetImageID(refOrID) + if err != nil { + return nil, err + } + return daemon.imageStore.Get(imgID) +} + +// GetImageOnBuild looks up a Docker image referenced by `name`. +func (daemon *Daemon) GetImageOnBuild(name string) (builder.Image, error) { + img, err := daemon.GetImage(name) + if err != nil { + return nil, err + } + return img, nil +} + +// GraphDriverName returns the name of the graph driver used by the layer.Store +func (daemon *Daemon) GraphDriverName() string { + return daemon.layerStore.DriverName() +} + +// GetUIDGIDMaps returns the current daemon's user namespace settings +// for the full uid and gid maps which will be applied to containers +// started in this instance. +func (daemon *Daemon) GetUIDGIDMaps() ([]idtools.IDMap, []idtools.IDMap) { + return daemon.uidMaps, daemon.gidMaps +} + +// GetRemappedUIDGID returns the current daemon's uid and gid values +// if user namespaces are in use for this daemon instance. If not +// this function will return "real" root values of 0, 0. +func (daemon *Daemon) GetRemappedUIDGID() (int, int) { + uid, gid, _ := idtools.GetRootUIDGID(daemon.uidMaps, daemon.gidMaps) + return uid, gid +} + +// GetCachedImage returns the most recent created image that is a child +// of the image with imgID, that had the same config when it was +// created. nil is returned if a child cannot be found. An error is +// returned if the parent image cannot be found. +func (daemon *Daemon) GetCachedImage(imgID image.ID, config *containertypes.Config) (*image.Image, error) { + // Loop on the children of the given image and check the config + getMatch := func(siblings []image.ID) (*image.Image, error) { + var match *image.Image + for _, id := range siblings { + img, err := daemon.imageStore.Get(id) + if err != nil { + return nil, fmt.Errorf("unable to find image %q", id) + } + + if runconfig.Compare(&img.ContainerConfig, config) { + // check for the most up to date match + if match == nil || match.Created.Before(img.Created) { + match = img + } + } + } + return match, nil + } + + // In this case, this is `FROM scratch`, which isn't an actual image. + if imgID == "" { + images := daemon.imageStore.Map() + var siblings []image.ID + for id, img := range images { + if img.Parent == imgID { + siblings = append(siblings, id) + } + } + return getMatch(siblings) + } + + // find match from child images + siblings := daemon.imageStore.Children(imgID) + return getMatch(siblings) +} + +// GetCachedImageOnBuild returns a reference to a cached image whose parent equals `parent` +// and runconfig equals `cfg`. A cache miss is expected to return an empty ID and a nil error. +func (daemon *Daemon) GetCachedImageOnBuild(imgID string, cfg *containertypes.Config) (string, error) { + cache, err := daemon.GetCachedImage(image.ID(imgID), cfg) + if cache == nil || err != nil { + return "", err + } + return cache.ID().String(), nil +} + +// tempDir returns the default directory to use for temporary files. +func tempDir(rootDir string, rootUID, rootGID int) (string, error) { + var tmpDir string + if tmpDir = os.Getenv("DOCKER_TMPDIR"); tmpDir == "" { + tmpDir = filepath.Join(rootDir, "tmp") + } + return tmpDir, idtools.MkdirAllAs(tmpDir, 0700, rootUID, rootGID) +} + +func (daemon *Daemon) setSecurityOptions(container *container.Container, hostConfig *containertypes.HostConfig) error { + container.Lock() + defer container.Unlock() + return parseSecurityOpt(container, hostConfig) +} + +func (daemon *Daemon) setHostConfig(container *container.Container, hostConfig *containertypes.HostConfig) error { + // Do not lock while creating volumes since this could be calling out to external plugins + // Don't want to block other actions, like `docker ps` because we're waiting on an external plugin + if err := daemon.registerMountPoints(container, hostConfig); err != nil { + return err + } + + container.Lock() + defer container.Unlock() + + // Register any links from the host config before starting the container + if err := daemon.registerLinks(container, hostConfig); err != nil { + return err + } + + // make sure links is not nil + // this ensures that on the next daemon restart we don't try to migrate from legacy sqlite links + if hostConfig.Links == nil { + hostConfig.Links = []string{} + } + + container.HostConfig = hostConfig + return container.ToDisk() +} + +func (daemon *Daemon) setupInitLayer(initPath string) error { + rootUID, rootGID := daemon.GetRemappedUIDGID() + return setupInitLayer(initPath, rootUID, rootGID) +} + +func setDefaultMtu(config *Config) { + // do nothing if the config does not have the default 0 value. + if config.Mtu != 0 { + return + } + config.Mtu = defaultNetworkMtu +} + +// verifyContainerSettings performs validation of the hostconfig and config +// structures. +func (daemon *Daemon) verifyContainerSettings(hostConfig *containertypes.HostConfig, config *containertypes.Config, update bool) ([]string, error) { + + // First perform verification of settings common across all platforms. + if config != nil { + if config.WorkingDir != "" { + config.WorkingDir = filepath.FromSlash(config.WorkingDir) // Ensure in platform semantics + if !system.IsAbs(config.WorkingDir) { + return nil, fmt.Errorf("The working directory '%s' is invalid. It needs to be an absolute path.", config.WorkingDir) + } + } + + if len(config.StopSignal) > 0 { + _, err := signal.ParseSignal(config.StopSignal) + if err != nil { + return nil, err + } + } + } + + if hostConfig == nil { + return nil, nil + } + + logCfg := daemon.getLogConfig(hostConfig.LogConfig) + if err := logger.ValidateLogOpts(logCfg.Type, logCfg.Config); err != nil { + return nil, err + } + + for port := range hostConfig.PortBindings { + _, portStr := nat.SplitProtoPort(string(port)) + if _, err := nat.ParsePort(portStr); err != nil { + return nil, fmt.Errorf("Invalid port specification: %q", portStr) + } + for _, pb := range hostConfig.PortBindings[port] { + _, err := nat.NewPort(nat.SplitProtoPort(pb.HostPort)) + if err != nil { + return nil, fmt.Errorf("Invalid port specification: %q", pb.HostPort) + } + } + } + + // Now do platform-specific verification + return verifyPlatformContainerSettings(daemon, hostConfig, config, update) +} + +// Checks if the client set configurations for more than one network while creating a container +func (daemon *Daemon) verifyNetworkingConfig(nwConfig *networktypes.NetworkingConfig) error { + if nwConfig == nil || len(nwConfig.EndpointsConfig) <= 1 { + return nil + } + l := make([]string, 0, len(nwConfig.EndpointsConfig)) + for k := range nwConfig.EndpointsConfig { + l = append(l, k) + } + err := fmt.Errorf("Container cannot be connected to network endpoints: %s", strings.Join(l, ", ")) + return errors.NewBadRequestError(err) +} + +func configureVolumes(config *Config, rootUID, rootGID int) (*store.VolumeStore, error) { + volumesDriver, err := local.New(config.Root, rootUID, rootGID) + if err != nil { + return nil, err + } + + volumedrivers.Register(volumesDriver, volumesDriver.Name()) + return store.New(config.Root) +} + +// AuthenticateToRegistry checks the validity of credentials in authConfig +func (daemon *Daemon) AuthenticateToRegistry(ctx context.Context, authConfig *types.AuthConfig) (string, string, error) { + return daemon.RegistryService.Auth(authConfig, dockerversion.DockerUserAgent(ctx)) +} + +// SearchRegistryForImages queries the registry for images matching +// term. authConfig is used to login. +func (daemon *Daemon) SearchRegistryForImages(ctx context.Context, term string, + authConfig *types.AuthConfig, + headers map[string][]string) (*registrytypes.SearchResults, error) { + return daemon.RegistryService.Search(term, authConfig, dockerversion.DockerUserAgent(ctx), headers) +} + +// IsShuttingDown tells whether the daemon is shutting down or not +func (daemon *Daemon) IsShuttingDown() bool { + return daemon.shutdown +} + +// GetContainerStats collects all the stats published by a container +func (daemon *Daemon) GetContainerStats(container *container.Container) (*types.StatsJSON, error) { + stats, err := daemon.stats(container) + if err != nil { + return nil, err + } + + if stats.Networks, err = daemon.getNetworkStats(container); err != nil { + return nil, err + } + + return stats, nil +} + +func (daemon *Daemon) getNetworkStats(c *container.Container) (map[string]types.NetworkStats, error) { + sb, err := daemon.netController.SandboxByID(c.NetworkSettings.SandboxID) + if err != nil { + return nil, err + } + + lnstats, err := sb.Statistics() + if err != nil { + return nil, err + } + + stats := make(map[string]types.NetworkStats) + // Convert libnetwork nw stats into engine-api stats + for ifName, ifStats := range lnstats { + stats[ifName] = types.NetworkStats{ + RxBytes: ifStats.RxBytes, + RxPackets: ifStats.RxPackets, + RxErrors: ifStats.RxErrors, + RxDropped: ifStats.RxDropped, + TxBytes: ifStats.TxBytes, + TxPackets: ifStats.TxPackets, + TxErrors: ifStats.TxErrors, + TxDropped: ifStats.TxDropped, + } + } + + return stats, nil +} + +// newBaseContainer creates a new container with its initial +// configuration based on the root storage from the daemon. +func (daemon *Daemon) newBaseContainer(id string) *container.Container { + return container.NewBaseContainer(id, daemon.containerRoot(id)) +} + +// initDiscovery initializes the discovery watcher for this daemon. +func (daemon *Daemon) initDiscovery(config *Config) error { + advertise, err := parseClusterAdvertiseSettings(config.ClusterStore, config.ClusterAdvertise) + if err != nil { + if err == errDiscoveryDisabled { + return nil + } + return err + } + + config.ClusterAdvertise = advertise + discoveryWatcher, err := initDiscovery(config.ClusterStore, config.ClusterAdvertise, config.ClusterOpts) + if err != nil { + return fmt.Errorf("discovery initialization failed (%v)", err) + } + + daemon.discoveryWatcher = discoveryWatcher + return nil +} + +// Reload reads configuration changes and modifies the +// daemon according to those changes. +// This are the settings that Reload changes: +// - Daemon labels. +// - Cluster discovery (reconfigure and restart). +func (daemon *Daemon) Reload(config *Config) error { + daemon.configStore.reloadLock.Lock() + defer daemon.configStore.reloadLock.Unlock() + if config.IsValueSet("labels") { + daemon.configStore.Labels = config.Labels + } + if config.IsValueSet("debug") { + daemon.configStore.Debug = config.Debug + } + return daemon.reloadClusterDiscovery(config) +} + +func (daemon *Daemon) reloadClusterDiscovery(config *Config) error { + var err error + newAdvertise := daemon.configStore.ClusterAdvertise + newClusterStore := daemon.configStore.ClusterStore + if config.IsValueSet("cluster-advertise") { + if config.IsValueSet("cluster-store") { + newClusterStore = config.ClusterStore + } + newAdvertise, err = parseClusterAdvertiseSettings(newClusterStore, config.ClusterAdvertise) + if err != nil && err != errDiscoveryDisabled { + return err + } + } + + // check discovery modifications + if !modifiedDiscoverySettings(daemon.configStore, newAdvertise, newClusterStore, config.ClusterOpts) { + return nil + } + + // enable discovery for the first time if it was not previously enabled + if daemon.discoveryWatcher == nil { + discoveryWatcher, err := initDiscovery(newClusterStore, newAdvertise, config.ClusterOpts) + if err != nil { + return fmt.Errorf("discovery initialization failed (%v)", err) + } + daemon.discoveryWatcher = discoveryWatcher + } else { + if err == errDiscoveryDisabled { + // disable discovery if it was previously enabled and it's disabled now + daemon.discoveryWatcher.Stop() + } else { + // reload discovery + if err = daemon.discoveryWatcher.Reload(config.ClusterStore, newAdvertise, config.ClusterOpts); err != nil { + return err + } + } + } + + daemon.configStore.ClusterStore = newClusterStore + daemon.configStore.ClusterOpts = config.ClusterOpts + daemon.configStore.ClusterAdvertise = newAdvertise + + if daemon.netController == nil { + return nil + } + netOptions, err := daemon.networkOptions(daemon.configStore) + if err != nil { + logrus.Warnf("Failed to reload configuration with network controller: %v", err) + return nil + } + err = daemon.netController.ReloadConfiguration(netOptions...) + if err != nil { + logrus.Warnf("Failed to reload configuration with network controller: %v", err) + } + + return nil +} + +func validateID(id string) error { + if id == "" { + return fmt.Errorf("Invalid empty id") + } + return nil +} + +func isBridgeNetworkDisabled(config *Config) bool { + return config.bridgeConfig.Iface == disableNetworkBridge +} + +func (daemon *Daemon) networkOptions(dconfig *Config) ([]nwconfig.Option, error) { + options := []nwconfig.Option{} + if dconfig == nil { + return options, nil + } + + options = append(options, nwconfig.OptionDataDir(dconfig.Root)) + + dd := runconfig.DefaultDaemonNetworkMode() + dn := runconfig.DefaultDaemonNetworkMode().NetworkName() + options = append(options, nwconfig.OptionDefaultDriver(string(dd))) + options = append(options, nwconfig.OptionDefaultNetwork(dn)) + + if strings.TrimSpace(dconfig.ClusterStore) != "" { + kv := strings.Split(dconfig.ClusterStore, "://") + if len(kv) != 2 { + return nil, fmt.Errorf("kv store daemon config must be of the form KV-PROVIDER://KV-URL") + } + options = append(options, nwconfig.OptionKVProvider(kv[0])) + options = append(options, nwconfig.OptionKVProviderURL(kv[1])) + } + if len(dconfig.ClusterOpts) > 0 { + options = append(options, nwconfig.OptionKVOpts(dconfig.ClusterOpts)) + } + + if daemon.discoveryWatcher != nil { + options = append(options, nwconfig.OptionDiscoveryWatcher(daemon.discoveryWatcher)) + } + + if dconfig.ClusterAdvertise != "" { + options = append(options, nwconfig.OptionDiscoveryAddress(dconfig.ClusterAdvertise)) + } + + options = append(options, nwconfig.OptionLabels(dconfig.Labels)) + options = append(options, driverOptions(dconfig)...) + return options, nil +} + +func copyBlkioEntry(entries []*containerd.BlkioStatsEntry) []types.BlkioStatEntry { + out := make([]types.BlkioStatEntry, len(entries)) + for i, re := range entries { + out[i] = types.BlkioStatEntry{ + Major: re.Major, + Minor: re.Minor, + Op: re.Op, + Value: re.Value, + } + } + return out +} diff --git a/daemon/daemon_experimental.go b/daemon/daemon_experimental.go new file mode 100644 index 00000000..3fd0e765 --- /dev/null +++ b/daemon/daemon_experimental.go @@ -0,0 +1,9 @@ +// +build experimental + +package daemon + +import "github.com/docker/engine-api/types/container" + +func (daemon *Daemon) verifyExperimentalContainerSettings(hostConfig *container.HostConfig, config *container.Config) ([]string, error) { + return nil, nil +} diff --git a/daemon/daemon_linux.go b/daemon/daemon_linux.go new file mode 100644 index 00000000..9bdf6e2b --- /dev/null +++ b/daemon/daemon_linux.go @@ -0,0 +1,80 @@ +package daemon + +import ( + "bufio" + "fmt" + "io" + "os" + "regexp" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/mount" +) + +func (daemon *Daemon) cleanupMountsByID(id string) error { + logrus.Debugf("Cleaning up old mountid %s: start.", id) + f, err := os.Open("/proc/self/mountinfo") + if err != nil { + return err + } + defer f.Close() + + return daemon.cleanupMountsFromReaderByID(f, id, mount.Unmount) +} + +func (daemon *Daemon) cleanupMountsFromReaderByID(reader io.Reader, id string, unmount func(target string) error) error { + if daemon.root == "" { + return nil + } + var errors []string + + regexps := getCleanPatterns(id) + sc := bufio.NewScanner(reader) + for sc.Scan() { + if fields := strings.Fields(sc.Text()); len(fields) >= 4 { + if mnt := fields[4]; strings.HasPrefix(mnt, daemon.root) { + for _, p := range regexps { + if p.MatchString(mnt) { + if err := unmount(mnt); err != nil { + logrus.Error(err) + errors = append(errors, err.Error()) + } + } + } + } + } + } + + if err := sc.Err(); err != nil { + return err + } + + if len(errors) > 0 { + return fmt.Errorf("Error cleaning up mounts:\n%v", strings.Join(errors, "\n")) + } + + logrus.Debugf("Cleaning up old mountid %v: done.", id) + return nil +} + +// cleanupMounts umounts shm/mqueue mounts for old containers +func (daemon *Daemon) cleanupMounts() error { + return daemon.cleanupMountsByID("") +} + +func getCleanPatterns(id string) (regexps []*regexp.Regexp) { + var patterns []string + if id == "" { + id = "[0-9a-f]{64}" + patterns = append(patterns, "containers/"+id+"/shm") + } + patterns = append(patterns, "aufs/mnt/"+id+"$", "overlay/"+id+"/merged$", "zfs/graph/"+id+"$") + for _, p := range patterns { + r, err := regexp.Compile(p) + if err == nil { + regexps = append(regexps, r) + } + } + return +} diff --git a/daemon/daemon_linux_test.go b/daemon/daemon_linux_test.go new file mode 100644 index 00000000..c40b13ba --- /dev/null +++ b/daemon/daemon_linux_test.go @@ -0,0 +1,104 @@ +// +build linux + +package daemon + +import ( + "strings" + "testing" +) + +const mountsFixture = `142 78 0:38 / / rw,relatime - aufs none rw,si=573b861da0b3a05b,dio +143 142 0:60 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +144 142 0:67 / /dev rw,nosuid - tmpfs tmpfs rw,mode=755 +145 144 0:78 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +146 144 0:49 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +147 142 0:84 / /sys rw,nosuid,nodev,noexec,relatime - sysfs sysfs rw +148 147 0:86 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755 +149 148 0:22 /docker/5425782a95e643181d8a485a2bab3c0bb21f51d7dfc03511f0e6fbf3f3aa356a /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuset +150 148 0:25 /docker/5425782a95e643181d8a485a2bab3c0bb21f51d7dfc03511f0e6fbf3f3aa356a /sys/fs/cgroup/cpu rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpu +151 148 0:27 /docker/5425782a95e643181d8a485a2bab3c0bb21f51d7dfc03511f0e6fbf3f3aa356a /sys/fs/cgroup/cpuacct rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuacct +152 148 0:28 /docker/5425782a95e643181d8a485a2bab3c0bb21f51d7dfc03511f0e6fbf3f3aa356a /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory +153 148 0:29 /docker/5425782a95e643181d8a485a2bab3c0bb21f51d7dfc03511f0e6fbf3f3aa356a /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,devices +154 148 0:30 /docker/5425782a95e643181d8a485a2bab3c0bb21f51d7dfc03511f0e6fbf3f3aa356a /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,freezer +155 148 0:31 /docker/5425782a95e643181d8a485a2bab3c0bb21f51d7dfc03511f0e6fbf3f3aa356a /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,blkio +156 148 0:32 /docker/5425782a95e643181d8a485a2bab3c0bb21f51d7dfc03511f0e6fbf3f3aa356a /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,perf_event +157 148 0:33 /docker/5425782a95e643181d8a485a2bab3c0bb21f51d7dfc03511f0e6fbf3f3aa356a /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,hugetlb +158 148 0:35 /docker/5425782a95e643181d8a485a2bab3c0bb21f51d7dfc03511f0e6fbf3f3aa356a /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime - cgroup systemd rw,name=systemd +159 142 8:4 /home/mlaventure/gopath /home/mlaventure/gopath rw,relatime - ext4 /dev/disk/by-uuid/d99e196c-1fc4-4b4f-bab9-9962b2b34e99 rw,errors=remount-ro,data=ordered +160 142 8:4 /var/lib/docker/volumes/9a428b651ee4c538130143cad8d87f603a4bf31b928afe7ff3ecd65480692b35/_data /var/lib/docker rw,relatime - ext4 /dev/disk/by-uuid/d99e196c-1fc4-4b4f-bab9-9962b2b34e99 rw,errors=remount-ro,data=ordered +164 142 8:4 /home/mlaventure/gopath/src/github.com/docker/docker /go/src/github.com/docker/docker rw,relatime - ext4 /dev/disk/by-uuid/d99e196c-1fc4-4b4f-bab9-9962b2b34e99 rw,errors=remount-ro,data=ordered +165 142 8:4 /var/lib/docker/containers/5425782a95e643181d8a485a2bab3c0bb21f51d7dfc03511f0e6fbf3f3aa356a/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/disk/by-uuid/d99e196c-1fc4-4b4f-bab9-9962b2b34e99 rw,errors=remount-ro,data=ordered +166 142 8:4 /var/lib/docker/containers/5425782a95e643181d8a485a2bab3c0bb21f51d7dfc03511f0e6fbf3f3aa356a/hostname /etc/hostname rw,relatime - ext4 /dev/disk/by-uuid/d99e196c-1fc4-4b4f-bab9-9962b2b34e99 rw,errors=remount-ro,data=ordered +167 142 8:4 /var/lib/docker/containers/5425782a95e643181d8a485a2bab3c0bb21f51d7dfc03511f0e6fbf3f3aa356a/hosts /etc/hosts rw,relatime - ext4 /dev/disk/by-uuid/d99e196c-1fc4-4b4f-bab9-9962b2b34e99 rw,errors=remount-ro,data=ordered +168 144 0:39 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k +169 144 0:12 /14 /dev/console rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +83 147 0:10 / /sys/kernel/security rw,relatime - securityfs none rw +89 142 0:87 / /tmp rw,relatime - tmpfs none rw +97 142 0:60 / /run/docker/netns/default rw,nosuid,nodev,noexec,relatime - proc proc rw +100 160 8:4 /var/lib/docker/volumes/9a428b651ee4c538130143cad8d87f603a4bf31b928afe7ff3ecd65480692b35/_data/aufs /var/lib/docker/aufs rw,relatime - ext4 /dev/disk/by-uuid/d99e196c-1fc4-4b4f-bab9-9962b2b34e99 rw,errors=remount-ro,data=ordered +115 100 0:102 / /var/lib/docker/aufs/mnt/0ecda1c63e5b58b3d89ff380bf646c95cc980252cf0b52466d43619aec7c8432 rw,relatime - aufs none rw,si=573b861dbc01905b,dio +116 160 0:107 / /var/lib/docker/containers/d045dc441d2e2e1d5b3e328d47e5943811a40819fb47497c5f5a5df2d6d13c37/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k +118 142 0:102 / /run/docker/libcontainerd/d045dc441d2e2e1d5b3e328d47e5943811a40819fb47497c5f5a5df2d6d13c37/rootfs rw,relatime - aufs none rw,si=573b861dbc01905b,dio +242 142 0:60 / /run/docker/netns/c3664df2a0f7 rw,nosuid,nodev,noexec,relatime - proc proc rw +120 100 0:122 / /var/lib/docker/aufs/mnt/03ca4b49e71f1e49a41108829f4d5c70ac95934526e2af8984a1f65f1de0715d rw,relatime - aufs none rw,si=573b861eb147805b,dio +171 142 0:122 / /run/docker/libcontainerd/e406ff6f3e18516d50e03dbca4de54767a69a403a6f7ec1edc2762812824521e/rootfs rw,relatime - aufs none rw,si=573b861eb147805b,dio +310 142 0:60 / /run/docker/netns/71a18572176b rw,nosuid,nodev,noexec,relatime - proc proc rw +` + +func TestCleanupMounts(t *testing.T) { + d := &Daemon{ + root: "/var/lib/docker/", + } + + expected := "/var/lib/docker/containers/d045dc441d2e2e1d5b3e328d47e5943811a40819fb47497c5f5a5df2d6d13c37/shm" + var unmounted int + unmount := func(target string) error { + if target == expected { + unmounted++ + } + return nil + } + + d.cleanupMountsFromReaderByID(strings.NewReader(mountsFixture), "", unmount) + + if unmounted != 1 { + t.Fatalf("Expected to unmount the shm (and the shm only)") + } +} + +func TestCleanupMountsByID(t *testing.T) { + d := &Daemon{ + root: "/var/lib/docker/", + } + + expected := "/var/lib/docker/aufs/mnt/03ca4b49e71f1e49a41108829f4d5c70ac95934526e2af8984a1f65f1de0715d" + var unmounted int + unmount := func(target string) error { + if target == expected { + unmounted++ + } + return nil + } + + d.cleanupMountsFromReaderByID(strings.NewReader(mountsFixture), "03ca4b49e71f1e49a41108829f4d5c70ac95934526e2af8984a1f65f1de0715d", unmount) + + if unmounted != 1 { + t.Fatalf("Expected to unmount the auf root (and that only)") + } +} + +func TestNotCleanupMounts(t *testing.T) { + d := &Daemon{ + repository: "", + } + var unmounted bool + unmount := func(target string) error { + unmounted = true + return nil + } + mountInfo := `234 232 0:59 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k` + d.cleanupMountsFromReaderByID(strings.NewReader(mountInfo), "", unmount) + if unmounted { + t.Fatalf("Expected not to clean up /dev/shm") + } +} diff --git a/daemon/daemon_stub.go b/daemon/daemon_stub.go new file mode 100644 index 00000000..40e8ddc8 --- /dev/null +++ b/daemon/daemon_stub.go @@ -0,0 +1,9 @@ +// +build !experimental + +package daemon + +import "github.com/docker/engine-api/types/container" + +func (daemon *Daemon) verifyExperimentalContainerSettings(hostConfig *container.HostConfig, config *container.Config) ([]string, error) { + return nil, nil +} diff --git a/daemon/daemon_test.go b/daemon/daemon_test.go new file mode 100644 index 00000000..609ed952 --- /dev/null +++ b/daemon/daemon_test.go @@ -0,0 +1,532 @@ +package daemon + +import ( + "io/ioutil" + "os" + "path/filepath" + "reflect" + "testing" + "time" + + "github.com/docker/docker/container" + "github.com/docker/docker/pkg/discovery" + _ "github.com/docker/docker/pkg/discovery/memory" + "github.com/docker/docker/pkg/registrar" + "github.com/docker/docker/pkg/truncindex" + "github.com/docker/docker/volume" + volumedrivers "github.com/docker/docker/volume/drivers" + "github.com/docker/docker/volume/local" + "github.com/docker/docker/volume/store" + containertypes "github.com/docker/engine-api/types/container" + "github.com/docker/go-connections/nat" +) + +// +// https://github.com/docker/docker/issues/8069 +// + +func TestGetContainer(t *testing.T) { + c1 := &container.Container{ + CommonContainer: container.CommonContainer{ + ID: "5a4ff6a163ad4533d22d69a2b8960bf7fafdcba06e72d2febdba229008b0bf57", + Name: "tender_bardeen", + }, + } + + c2 := &container.Container{ + CommonContainer: container.CommonContainer{ + ID: "3cdbd1aa394fd68559fd1441d6eff2ab7c1e6363582c82febfaa8045df3bd8de", + Name: "drunk_hawking", + }, + } + + c3 := &container.Container{ + CommonContainer: container.CommonContainer{ + ID: "3cdbd1aa394fd68559fd1441d6eff2abfafdcba06e72d2febdba229008b0bf57", + Name: "3cdbd1aa", + }, + } + + c4 := &container.Container{ + CommonContainer: container.CommonContainer{ + ID: "75fb0b800922abdbef2d27e60abcdfaf7fb0698b2a96d22d3354da361a6ff4a5", + Name: "5a4ff6a163ad4533d22d69a2b8960bf7fafdcba06e72d2febdba229008b0bf57", + }, + } + + c5 := &container.Container{ + CommonContainer: container.CommonContainer{ + ID: "d22d69a2b8960bf7fafdcba06e72d2febdba960bf7fafdcba06e72d2f9008b060b", + Name: "d22d69a2b896", + }, + } + + store := container.NewMemoryStore() + store.Add(c1.ID, c1) + store.Add(c2.ID, c2) + store.Add(c3.ID, c3) + store.Add(c4.ID, c4) + store.Add(c5.ID, c5) + + index := truncindex.NewTruncIndex([]string{}) + index.Add(c1.ID) + index.Add(c2.ID) + index.Add(c3.ID) + index.Add(c4.ID) + index.Add(c5.ID) + + daemon := &Daemon{ + containers: store, + idIndex: index, + nameIndex: registrar.NewRegistrar(), + } + + daemon.reserveName(c1.ID, c1.Name) + daemon.reserveName(c2.ID, c2.Name) + daemon.reserveName(c3.ID, c3.Name) + daemon.reserveName(c4.ID, c4.Name) + daemon.reserveName(c5.ID, c5.Name) + + if container, _ := daemon.GetContainer("3cdbd1aa394fd68559fd1441d6eff2ab7c1e6363582c82febfaa8045df3bd8de"); container != c2 { + t.Fatal("Should explicitly match full container IDs") + } + + if container, _ := daemon.GetContainer("75fb0b8009"); container != c4 { + t.Fatal("Should match a partial ID") + } + + if container, _ := daemon.GetContainer("drunk_hawking"); container != c2 { + t.Fatal("Should match a full name") + } + + // c3.Name is a partial match for both c3.ID and c2.ID + if c, _ := daemon.GetContainer("3cdbd1aa"); c != c3 { + t.Fatal("Should match a full name even though it collides with another container's ID") + } + + if container, _ := daemon.GetContainer("d22d69a2b896"); container != c5 { + t.Fatal("Should match a container where the provided prefix is an exact match to the it's name, and is also a prefix for it's ID") + } + + if _, err := daemon.GetContainer("3cdbd1"); err == nil { + t.Fatal("Should return an error when provided a prefix that partially matches multiple container ID's") + } + + if _, err := daemon.GetContainer("nothing"); err == nil { + t.Fatal("Should return an error when provided a prefix that is neither a name or a partial match to an ID") + } +} + +func initDaemonWithVolumeStore(tmp string) (*Daemon, error) { + var err error + daemon := &Daemon{ + repository: tmp, + root: tmp, + } + daemon.volumes, err = store.New(tmp) + if err != nil { + return nil, err + } + + volumesDriver, err := local.New(tmp, 0, 0) + if err != nil { + return nil, err + } + volumedrivers.Register(volumesDriver, volumesDriver.Name()) + + return daemon, nil +} + +func TestValidContainerNames(t *testing.T) { + invalidNames := []string{"-rm", "&sdfsfd", "safd%sd"} + validNames := []string{"word-word", "word_word", "1weoid"} + + for _, name := range invalidNames { + if validContainerNamePattern.MatchString(name) { + t.Fatalf("%q is not a valid container name and was returned as valid.", name) + } + } + + for _, name := range validNames { + if !validContainerNamePattern.MatchString(name) { + t.Fatalf("%q is a valid container name and was returned as invalid.", name) + } + } +} + +func TestContainerInitDNS(t *testing.T) { + tmp, err := ioutil.TempDir("", "docker-container-test-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + + containerID := "d59df5276e7b219d510fe70565e0404bc06350e0d4b43fe961f22f339980170e" + containerPath := filepath.Join(tmp, containerID) + if err := os.MkdirAll(containerPath, 0755); err != nil { + t.Fatal(err) + } + + config := `{"State":{"Running":true,"Paused":false,"Restarting":false,"OOMKilled":false,"Dead":false,"Pid":2464,"ExitCode":0, +"Error":"","StartedAt":"2015-05-26T16:48:53.869308965Z","FinishedAt":"0001-01-01T00:00:00Z"}, +"ID":"d59df5276e7b219d510fe70565e0404bc06350e0d4b43fe961f22f339980170e","Created":"2015-05-26T16:48:53.7987917Z","Path":"top", +"Args":[],"Config":{"Hostname":"d59df5276e7b","Domainname":"","User":"","Memory":0,"MemorySwap":0,"CpuShares":0,"Cpuset":"", +"AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"PortSpecs":null,"ExposedPorts":null,"Tty":true,"OpenStdin":true, +"StdinOnce":false,"Env":null,"Cmd":["top"],"Image":"ubuntu:latest","Volumes":null,"WorkingDir":"","Entrypoint":null, +"NetworkDisabled":false,"MacAddress":"","OnBuild":null,"Labels":{}},"Image":"07f8e8c5e66084bef8f848877857537ffe1c47edd01a93af27e7161672ad0e95", +"NetworkSettings":{"IPAddress":"172.17.0.1","IPPrefixLen":16,"MacAddress":"02:42:ac:11:00:01","LinkLocalIPv6Address":"fe80::42:acff:fe11:1", +"LinkLocalIPv6PrefixLen":64,"GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"Gateway":"172.17.42.1","IPv6Gateway":"","Bridge":"docker0","Ports":{}}, +"ResolvConfPath":"/var/lib/docker/containers/d59df5276e7b219d510fe70565e0404bc06350e0d4b43fe961f22f339980170e/resolv.conf", +"HostnamePath":"/var/lib/docker/containers/d59df5276e7b219d510fe70565e0404bc06350e0d4b43fe961f22f339980170e/hostname", +"HostsPath":"/var/lib/docker/containers/d59df5276e7b219d510fe70565e0404bc06350e0d4b43fe961f22f339980170e/hosts", +"LogPath":"/var/lib/docker/containers/d59df5276e7b219d510fe70565e0404bc06350e0d4b43fe961f22f339980170e/d59df5276e7b219d510fe70565e0404bc06350e0d4b43fe961f22f339980170e-json.log", +"Name":"/ubuntu","Driver":"aufs","MountLabel":"","ProcessLabel":"","AppArmorProfile":"","RestartCount":0, +"UpdateDns":false,"Volumes":{},"VolumesRW":{},"AppliedVolumesFrom":null}` + + // Container struct only used to retrieve path to config file + container := &container.Container{CommonContainer: container.CommonContainer{Root: containerPath}} + configPath, err := container.ConfigPath() + if err != nil { + t.Fatal(err) + } + if err = ioutil.WriteFile(configPath, []byte(config), 0644); err != nil { + t.Fatal(err) + } + + hostConfig := `{"Binds":[],"ContainerIDFile":"","Memory":0,"MemorySwap":0,"CpuShares":0,"CpusetCpus":"", +"Privileged":false,"PortBindings":{},"Links":null,"PublishAllPorts":false,"Dns":null,"DnsOptions":null,"DnsSearch":null,"ExtraHosts":null,"VolumesFrom":null, +"Devices":[],"NetworkMode":"bridge","IpcMode":"","PidMode":"","CapAdd":null,"CapDrop":null,"RestartPolicy":{"Name":"no","MaximumRetryCount":0}, +"SecurityOpt":null,"ReadonlyRootfs":false,"Ulimits":null,"LogConfig":{"Type":"","Config":null},"CgroupParent":""}` + + hostConfigPath, err := container.HostConfigPath() + if err != nil { + t.Fatal(err) + } + if err = ioutil.WriteFile(hostConfigPath, []byte(hostConfig), 0644); err != nil { + t.Fatal(err) + } + + daemon, err := initDaemonWithVolumeStore(tmp) + if err != nil { + t.Fatal(err) + } + defer volumedrivers.Unregister(volume.DefaultDriverName) + + c, err := daemon.load(containerID) + if err != nil { + t.Fatal(err) + } + + if c.HostConfig.DNS == nil { + t.Fatal("Expected container DNS to not be nil") + } + + if c.HostConfig.DNSSearch == nil { + t.Fatal("Expected container DNSSearch to not be nil") + } + + if c.HostConfig.DNSOptions == nil { + t.Fatal("Expected container DNSOptions to not be nil") + } +} + +func newPortNoError(proto, port string) nat.Port { + p, _ := nat.NewPort(proto, port) + return p +} + +func TestMerge(t *testing.T) { + volumesImage := make(map[string]struct{}) + volumesImage["/test1"] = struct{}{} + volumesImage["/test2"] = struct{}{} + portsImage := make(nat.PortSet) + portsImage[newPortNoError("tcp", "1111")] = struct{}{} + portsImage[newPortNoError("tcp", "2222")] = struct{}{} + configImage := &containertypes.Config{ + ExposedPorts: portsImage, + Env: []string{"VAR1=1", "VAR2=2"}, + Volumes: volumesImage, + } + + portsUser := make(nat.PortSet) + portsUser[newPortNoError("tcp", "2222")] = struct{}{} + portsUser[newPortNoError("tcp", "3333")] = struct{}{} + volumesUser := make(map[string]struct{}) + volumesUser["/test3"] = struct{}{} + configUser := &containertypes.Config{ + ExposedPorts: portsUser, + Env: []string{"VAR2=3", "VAR3=3"}, + Volumes: volumesUser, + } + + if err := merge(configUser, configImage); err != nil { + t.Error(err) + } + + if len(configUser.ExposedPorts) != 3 { + t.Fatalf("Expected 3 ExposedPorts, 1111, 2222 and 3333, found %d", len(configUser.ExposedPorts)) + } + for portSpecs := range configUser.ExposedPorts { + if portSpecs.Port() != "1111" && portSpecs.Port() != "2222" && portSpecs.Port() != "3333" { + t.Fatalf("Expected 1111 or 2222 or 3333, found %s", portSpecs) + } + } + if len(configUser.Env) != 3 { + t.Fatalf("Expected 3 env var, VAR1=1, VAR2=3 and VAR3=3, found %d", len(configUser.Env)) + } + for _, env := range configUser.Env { + if env != "VAR1=1" && env != "VAR2=3" && env != "VAR3=3" { + t.Fatalf("Expected VAR1=1 or VAR2=3 or VAR3=3, found %s", env) + } + } + + if len(configUser.Volumes) != 3 { + t.Fatalf("Expected 3 volumes, /test1, /test2 and /test3, found %d", len(configUser.Volumes)) + } + for v := range configUser.Volumes { + if v != "/test1" && v != "/test2" && v != "/test3" { + t.Fatalf("Expected /test1 or /test2 or /test3, found %s", v) + } + } + + ports, _, err := nat.ParsePortSpecs([]string{"0000"}) + if err != nil { + t.Error(err) + } + configImage2 := &containertypes.Config{ + ExposedPorts: ports, + } + + if err := merge(configUser, configImage2); err != nil { + t.Error(err) + } + + if len(configUser.ExposedPorts) != 4 { + t.Fatalf("Expected 4 ExposedPorts, 0000, 1111, 2222 and 3333, found %d", len(configUser.ExposedPorts)) + } + for portSpecs := range configUser.ExposedPorts { + if portSpecs.Port() != "0" && portSpecs.Port() != "1111" && portSpecs.Port() != "2222" && portSpecs.Port() != "3333" { + t.Fatalf("Expected %q or %q or %q or %q, found %s", 0, 1111, 2222, 3333, portSpecs) + } + } +} + +func TestDaemonReloadLabels(t *testing.T) { + daemon := &Daemon{} + daemon.configStore = &Config{ + CommonConfig: CommonConfig{ + Labels: []string{"foo:bar"}, + }, + } + + valuesSets := make(map[string]interface{}) + valuesSets["labels"] = "foo:baz" + newConfig := &Config{ + CommonConfig: CommonConfig{ + Labels: []string{"foo:baz"}, + valuesSet: valuesSets, + }, + } + + daemon.Reload(newConfig) + label := daemon.configStore.Labels[0] + if label != "foo:baz" { + t.Fatalf("Expected daemon label `foo:baz`, got %s", label) + } +} + +func TestDaemonReloadNotAffectOthers(t *testing.T) { + daemon := &Daemon{} + daemon.configStore = &Config{ + CommonConfig: CommonConfig{ + Labels: []string{"foo:bar"}, + Debug: true, + }, + } + + valuesSets := make(map[string]interface{}) + valuesSets["labels"] = "foo:baz" + newConfig := &Config{ + CommonConfig: CommonConfig{ + Labels: []string{"foo:baz"}, + valuesSet: valuesSets, + }, + } + + daemon.Reload(newConfig) + label := daemon.configStore.Labels[0] + if label != "foo:baz" { + t.Fatalf("Expected daemon label `foo:baz`, got %s", label) + } + debug := daemon.configStore.Debug + if !debug { + t.Fatalf("Expected debug 'enabled', got 'disabled'") + } +} + +func TestDaemonDiscoveryReload(t *testing.T) { + daemon := &Daemon{} + daemon.configStore = &Config{ + CommonConfig: CommonConfig{ + ClusterStore: "memory://127.0.0.1", + ClusterAdvertise: "127.0.0.1:3333", + }, + } + + if err := daemon.initDiscovery(daemon.configStore); err != nil { + t.Fatal(err) + } + + expected := discovery.Entries{ + &discovery.Entry{Host: "127.0.0.1", Port: "3333"}, + } + + select { + case <-time.After(10 * time.Second): + t.Fatal("timeout waiting for discovery") + case <-daemon.discoveryWatcher.ReadyCh(): + } + + stopCh := make(chan struct{}) + defer close(stopCh) + ch, errCh := daemon.discoveryWatcher.Watch(stopCh) + + select { + case <-time.After(1 * time.Second): + t.Fatal("failed to get discovery advertisements in time") + case e := <-ch: + if !reflect.DeepEqual(e, expected) { + t.Fatalf("expected %v, got %v\n", expected, e) + } + case e := <-errCh: + t.Fatal(e) + } + + valuesSets := make(map[string]interface{}) + valuesSets["cluster-store"] = "memory://127.0.0.1:2222" + valuesSets["cluster-advertise"] = "127.0.0.1:5555" + newConfig := &Config{ + CommonConfig: CommonConfig{ + ClusterStore: "memory://127.0.0.1:2222", + ClusterAdvertise: "127.0.0.1:5555", + valuesSet: valuesSets, + }, + } + + expected = discovery.Entries{ + &discovery.Entry{Host: "127.0.0.1", Port: "5555"}, + } + + if err := daemon.Reload(newConfig); err != nil { + t.Fatal(err) + } + + select { + case <-time.After(10 * time.Second): + t.Fatal("timeout waiting for discovery") + case <-daemon.discoveryWatcher.ReadyCh(): + } + + ch, errCh = daemon.discoveryWatcher.Watch(stopCh) + + select { + case <-time.After(1 * time.Second): + t.Fatal("failed to get discovery advertisements in time") + case e := <-ch: + if !reflect.DeepEqual(e, expected) { + t.Fatalf("expected %v, got %v\n", expected, e) + } + case e := <-errCh: + t.Fatal(e) + } +} + +func TestDaemonDiscoveryReloadFromEmptyDiscovery(t *testing.T) { + daemon := &Daemon{} + daemon.configStore = &Config{} + + valuesSet := make(map[string]interface{}) + valuesSet["cluster-store"] = "memory://127.0.0.1:2222" + valuesSet["cluster-advertise"] = "127.0.0.1:5555" + newConfig := &Config{ + CommonConfig: CommonConfig{ + ClusterStore: "memory://127.0.0.1:2222", + ClusterAdvertise: "127.0.0.1:5555", + valuesSet: valuesSet, + }, + } + + expected := discovery.Entries{ + &discovery.Entry{Host: "127.0.0.1", Port: "5555"}, + } + + if err := daemon.Reload(newConfig); err != nil { + t.Fatal(err) + } + + select { + case <-time.After(10 * time.Second): + t.Fatal("timeout waiting for discovery") + case <-daemon.discoveryWatcher.ReadyCh(): + } + + stopCh := make(chan struct{}) + defer close(stopCh) + ch, errCh := daemon.discoveryWatcher.Watch(stopCh) + + select { + case <-time.After(1 * time.Second): + t.Fatal("failed to get discovery advertisements in time") + case e := <-ch: + if !reflect.DeepEqual(e, expected) { + t.Fatalf("expected %v, got %v\n", expected, e) + } + case e := <-errCh: + t.Fatal(e) + } +} + +func TestDaemonDiscoveryReloadOnlyClusterAdvertise(t *testing.T) { + daemon := &Daemon{} + daemon.configStore = &Config{ + CommonConfig: CommonConfig{ + ClusterStore: "memory://127.0.0.1", + }, + } + valuesSets := make(map[string]interface{}) + valuesSets["cluster-advertise"] = "127.0.0.1:5555" + newConfig := &Config{ + CommonConfig: CommonConfig{ + ClusterAdvertise: "127.0.0.1:5555", + valuesSet: valuesSets, + }, + } + expected := discovery.Entries{ + &discovery.Entry{Host: "127.0.0.1", Port: "5555"}, + } + + if err := daemon.Reload(newConfig); err != nil { + t.Fatal(err) + } + + select { + case <-daemon.discoveryWatcher.ReadyCh(): + case <-time.After(10 * time.Second): + t.Fatal("Timeout waiting for discovery") + } + stopCh := make(chan struct{}) + defer close(stopCh) + ch, errCh := daemon.discoveryWatcher.Watch(stopCh) + + select { + case <-time.After(1 * time.Second): + t.Fatal("failed to get discovery advertisements in time") + case e := <-ch: + if !reflect.DeepEqual(e, expected) { + t.Fatalf("expected %v, got %v\n", expected, e) + } + case e := <-errCh: + t.Fatal(e) + } + +} diff --git a/daemon/daemon_unix.go b/daemon/daemon_unix.go new file mode 100644 index 00000000..9a4ab322 --- /dev/null +++ b/daemon/daemon_unix.go @@ -0,0 +1,1126 @@ +// +build linux freebsd + +package daemon + +import ( + "fmt" + "io/ioutil" + "net" + "os" + "path/filepath" + "runtime" + "runtime/debug" + "strconv" + "strings" + "syscall" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/container" + "github.com/docker/docker/image" + "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/parsers" + "github.com/docker/docker/pkg/parsers/kernel" + "github.com/docker/docker/pkg/sysinfo" + "github.com/docker/docker/reference" + "github.com/docker/docker/runconfig" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/docker/engine-api/types" + pblkiodev "github.com/docker/engine-api/types/blkiodev" + containertypes "github.com/docker/engine-api/types/container" + "github.com/docker/libnetwork" + nwconfig "github.com/docker/libnetwork/config" + "github.com/docker/libnetwork/drivers/bridge" + "github.com/docker/libnetwork/ipamutils" + "github.com/docker/libnetwork/netlabel" + "github.com/docker/libnetwork/options" + lntypes "github.com/docker/libnetwork/types" + "github.com/opencontainers/runc/libcontainer/label" + "github.com/opencontainers/runc/libcontainer/user" + "github.com/opencontainers/specs/specs-go" +) + +const ( + // See https://git.kernel.org/cgit/linux/kernel/git/tip/tip.git/tree/kernel/sched/sched.h?id=8cd9234c64c584432f6992fe944ca9e46ca8ea76#n269 + linuxMinCPUShares = 2 + linuxMaxCPUShares = 262144 + platformSupported = true + // It's not kernel limit, we want this 4M limit to supply a reasonable functional container + linuxMinMemory = 4194304 + // constants for remapped root settings + defaultIDSpecifier string = "default" + defaultRemappedID string = "dockremap" + + // constant for cgroup drivers + cgroupFsDriver = "cgroupfs" + cgroupSystemdDriver = "systemd" +) + +func getMemoryResources(config containertypes.Resources) *specs.Memory { + memory := specs.Memory{} + + if config.Memory > 0 { + limit := uint64(config.Memory) + memory.Limit = &limit + } + + if config.MemoryReservation > 0 { + reservation := uint64(config.MemoryReservation) + memory.Reservation = &reservation + } + + if config.MemorySwap != 0 { + swap := uint64(config.MemorySwap) + memory.Swap = &swap + } + + if config.MemorySwappiness != nil { + swappiness := uint64(*config.MemorySwappiness) + memory.Swappiness = &swappiness + } + + if config.KernelMemory != 0 { + kernelMemory := uint64(config.KernelMemory) + memory.Kernel = &kernelMemory + } + + return &memory +} + +func getCPUResources(config containertypes.Resources) *specs.CPU { + cpu := specs.CPU{} + + if config.CPUShares != 0 { + shares := uint64(config.CPUShares) + cpu.Shares = &shares + } + + if config.CpusetCpus != "" { + cpuset := config.CpusetCpus + cpu.Cpus = &cpuset + } + + if config.CpusetMems != "" { + cpuset := config.CpusetMems + cpu.Mems = &cpuset + } + + if config.CPUPeriod != 0 { + period := uint64(config.CPUPeriod) + cpu.Period = &period + } + + if config.CPUQuota != 0 { + quota := uint64(config.CPUQuota) + cpu.Quota = "a + } + + return &cpu +} + +func getBlkioWeightDevices(config containertypes.Resources) ([]specs.WeightDevice, error) { + var stat syscall.Stat_t + var blkioWeightDevices []specs.WeightDevice + + for _, weightDevice := range config.BlkioWeightDevice { + if err := syscall.Stat(weightDevice.Path, &stat); err != nil { + return nil, err + } + weight := weightDevice.Weight + d := specs.WeightDevice{Weight: &weight} + d.Major = int64(stat.Rdev / 256) + d.Minor = int64(stat.Rdev % 256) + blkioWeightDevices = append(blkioWeightDevices, d) + } + + return blkioWeightDevices, nil +} + +func parseSecurityOpt(container *container.Container, config *containertypes.HostConfig) error { + var ( + labelOpts []string + err error + ) + + for _, opt := range config.SecurityOpt { + if opt == "no-new-privileges" { + container.NoNewPrivileges = true + } else { + var con []string + if strings.Contains(opt, "=") { + con = strings.SplitN(opt, "=", 2) + } else if strings.Contains(opt, ":") { + con = strings.SplitN(opt, ":", 2) + logrus.Warnf("Security options with `:` as a separator are deprecated and will be completely unsupported in 1.13, use `=` instead.") + } + + if len(con) != 2 { + return fmt.Errorf("Invalid --security-opt 1: %q", opt) + } + + switch con[0] { + case "label": + labelOpts = append(labelOpts, con[1]) + case "apparmor": + container.AppArmorProfile = con[1] + case "seccomp": + container.SeccompProfile = con[1] + default: + return fmt.Errorf("Invalid --security-opt 2: %q", opt) + } + } + } + + container.ProcessLabel, container.MountLabel, err = label.InitLabels(labelOpts) + return err +} + +func getBlkioReadIOpsDevices(config containertypes.Resources) ([]specs.ThrottleDevice, error) { + var blkioReadIOpsDevice []specs.ThrottleDevice + var stat syscall.Stat_t + + for _, iopsDevice := range config.BlkioDeviceReadIOps { + if err := syscall.Stat(iopsDevice.Path, &stat); err != nil { + return nil, err + } + rate := iopsDevice.Rate + d := specs.ThrottleDevice{Rate: &rate} + d.Major = int64(stat.Rdev / 256) + d.Minor = int64(stat.Rdev % 256) + blkioReadIOpsDevice = append(blkioReadIOpsDevice, d) + } + + return blkioReadIOpsDevice, nil +} + +func getBlkioWriteIOpsDevices(config containertypes.Resources) ([]specs.ThrottleDevice, error) { + var blkioWriteIOpsDevice []specs.ThrottleDevice + var stat syscall.Stat_t + + for _, iopsDevice := range config.BlkioDeviceWriteIOps { + if err := syscall.Stat(iopsDevice.Path, &stat); err != nil { + return nil, err + } + rate := iopsDevice.Rate + d := specs.ThrottleDevice{Rate: &rate} + d.Major = int64(stat.Rdev / 256) + d.Minor = int64(stat.Rdev % 256) + blkioWriteIOpsDevice = append(blkioWriteIOpsDevice, d) + } + + return blkioWriteIOpsDevice, nil +} + +func getBlkioReadBpsDevices(config containertypes.Resources) ([]specs.ThrottleDevice, error) { + var blkioReadBpsDevice []specs.ThrottleDevice + var stat syscall.Stat_t + + for _, bpsDevice := range config.BlkioDeviceReadBps { + if err := syscall.Stat(bpsDevice.Path, &stat); err != nil { + return nil, err + } + rate := bpsDevice.Rate + d := specs.ThrottleDevice{Rate: &rate} + d.Major = int64(stat.Rdev / 256) + d.Minor = int64(stat.Rdev % 256) + blkioReadBpsDevice = append(blkioReadBpsDevice, d) + } + + return blkioReadBpsDevice, nil +} + +func getBlkioWriteBpsDevices(config containertypes.Resources) ([]specs.ThrottleDevice, error) { + var blkioWriteBpsDevice []specs.ThrottleDevice + var stat syscall.Stat_t + + for _, bpsDevice := range config.BlkioDeviceWriteBps { + if err := syscall.Stat(bpsDevice.Path, &stat); err != nil { + return nil, err + } + rate := bpsDevice.Rate + d := specs.ThrottleDevice{Rate: &rate} + d.Major = int64(stat.Rdev / 256) + d.Minor = int64(stat.Rdev % 256) + blkioWriteBpsDevice = append(blkioWriteBpsDevice, d) + } + + return blkioWriteBpsDevice, nil +} + +func checkKernelVersion(k, major, minor int) bool { + if v, err := kernel.GetKernelVersion(); err != nil { + logrus.Warnf("%s", err) + } else { + if kernel.CompareKernelVersion(*v, kernel.VersionInfo{Kernel: k, Major: major, Minor: minor}) < 0 { + return false + } + } + return true +} + +func checkKernel() error { + // Check for unsupported kernel versions + // FIXME: it would be cleaner to not test for specific versions, but rather + // test for specific functionalities. + // Unfortunately we can't test for the feature "does not cause a kernel panic" + // without actually causing a kernel panic, so we need this workaround until + // the circumstances of pre-3.10 crashes are clearer. + // For details see https://github.com/docker/docker/issues/407 + if !checkKernelVersion(3, 10, 0) { + v, _ := kernel.GetKernelVersion() + if os.Getenv("DOCKER_NOWARN_KERNEL_VERSION") == "" { + logrus.Warnf("Your Linux kernel version %s can be unstable running docker. Please upgrade your kernel to 3.10.0.", v.String()) + } + } + return nil +} + +// adaptContainerSettings is called during container creation to modify any +// settings necessary in the HostConfig structure. +func (daemon *Daemon) adaptContainerSettings(hostConfig *containertypes.HostConfig, adjustCPUShares bool) error { + if adjustCPUShares && hostConfig.CPUShares > 0 { + // Handle unsupported CPUShares + if hostConfig.CPUShares < linuxMinCPUShares { + logrus.Warnf("Changing requested CPUShares of %d to minimum allowed of %d", hostConfig.CPUShares, linuxMinCPUShares) + hostConfig.CPUShares = linuxMinCPUShares + } else if hostConfig.CPUShares > linuxMaxCPUShares { + logrus.Warnf("Changing requested CPUShares of %d to maximum allowed of %d", hostConfig.CPUShares, linuxMaxCPUShares) + hostConfig.CPUShares = linuxMaxCPUShares + } + } + if hostConfig.Memory > 0 && hostConfig.MemorySwap == 0 { + // By default, MemorySwap is set to twice the size of Memory. + hostConfig.MemorySwap = hostConfig.Memory * 2 + } + if hostConfig.ShmSize == 0 { + hostConfig.ShmSize = container.DefaultSHMSize + } + var err error + if hostConfig.SecurityOpt == nil { + hostConfig.SecurityOpt, err = daemon.generateSecurityOpt(hostConfig.IpcMode, hostConfig.PidMode) + if err != nil { + return err + } + } + if hostConfig.MemorySwappiness == nil { + defaultSwappiness := int64(-1) + hostConfig.MemorySwappiness = &defaultSwappiness + } + if hostConfig.OomKillDisable == nil { + defaultOomKillDisable := false + hostConfig.OomKillDisable = &defaultOomKillDisable + } + + return nil +} + +func verifyContainerResources(resources *containertypes.Resources, sysInfo *sysinfo.SysInfo, update bool) ([]string, error) { + warnings := []string{} + + // memory subsystem checks and adjustments + if resources.Memory != 0 && resources.Memory < linuxMinMemory { + return warnings, fmt.Errorf("Minimum memory limit allowed is 4MB") + } + if resources.Memory > 0 && !sysInfo.MemoryLimit { + warnings = append(warnings, "Your kernel does not support memory limit capabilities. Limitation discarded.") + logrus.Warnf("Your kernel does not support memory limit capabilities. Limitation discarded.") + resources.Memory = 0 + resources.MemorySwap = -1 + } + if resources.Memory > 0 && resources.MemorySwap != -1 && !sysInfo.SwapLimit { + warnings = append(warnings, "Your kernel does not support swap limit capabilities, memory limited without swap.") + logrus.Warnf("Your kernel does not support swap limit capabilities, memory limited without swap.") + resources.MemorySwap = -1 + } + if resources.Memory > 0 && resources.MemorySwap > 0 && resources.MemorySwap < resources.Memory { + return warnings, fmt.Errorf("Minimum memoryswap limit should be larger than memory limit, see usage.") + } + if resources.Memory == 0 && resources.MemorySwap > 0 && !update { + return warnings, fmt.Errorf("You should always set the Memory limit when using Memoryswap limit, see usage.") + } + if resources.MemorySwappiness != nil && *resources.MemorySwappiness != -1 && !sysInfo.MemorySwappiness { + warnings = append(warnings, "Your kernel does not support memory swappiness capabilities, memory swappiness discarded.") + logrus.Warnf("Your kernel does not support memory swappiness capabilities, memory swappiness discarded.") + resources.MemorySwappiness = nil + } + if resources.MemorySwappiness != nil { + swappiness := *resources.MemorySwappiness + if swappiness < -1 || swappiness > 100 { + return warnings, fmt.Errorf("Invalid value: %v, valid memory swappiness range is 0-100.", swappiness) + } + } + if resources.MemoryReservation > 0 && !sysInfo.MemoryReservation { + warnings = append(warnings, "Your kernel does not support memory soft limit capabilities. Limitation discarded.") + logrus.Warnf("Your kernel does not support memory soft limit capabilities. Limitation discarded.") + resources.MemoryReservation = 0 + } + if resources.Memory > 0 && resources.MemoryReservation > 0 && resources.Memory < resources.MemoryReservation { + return warnings, fmt.Errorf("Minimum memory limit should be larger than memory reservation limit, see usage.") + } + if resources.KernelMemory > 0 && !sysInfo.KernelMemory { + warnings = append(warnings, "Your kernel does not support kernel memory limit capabilities. Limitation discarded.") + logrus.Warnf("Your kernel does not support kernel memory limit capabilities. Limitation discarded.") + resources.KernelMemory = 0 + } + if resources.KernelMemory > 0 && resources.KernelMemory < linuxMinMemory { + return warnings, fmt.Errorf("Minimum kernel memory limit allowed is 4MB") + } + if resources.KernelMemory > 0 && !checkKernelVersion(4, 0, 0) { + warnings = append(warnings, "You specified a kernel memory limit on a kernel older than 4.0. Kernel memory limits are experimental on older kernels, it won't work as expected and can cause your system to be unstable.") + logrus.Warnf("You specified a kernel memory limit on a kernel older than 4.0. Kernel memory limits are experimental on older kernels, it won't work as expected and can cause your system to be unstable.") + } + if resources.OomKillDisable != nil && !sysInfo.OomKillDisable { + // only produce warnings if the setting wasn't to *disable* the OOM Kill; no point + // warning the caller if they already wanted the feature to be off + if *resources.OomKillDisable { + warnings = append(warnings, "Your kernel does not support OomKillDisable, OomKillDisable discarded.") + logrus.Warnf("Your kernel does not support OomKillDisable, OomKillDisable discarded.") + } + resources.OomKillDisable = nil + } + + if resources.PidsLimit != 0 && !sysInfo.PidsLimit { + warnings = append(warnings, "Your kernel does not support pids limit capabilities, pids limit discarded.") + logrus.Warnf("Your kernel does not support pids limit capabilities, pids limit discarded.") + resources.PidsLimit = 0 + } + + // cpu subsystem checks and adjustments + if resources.CPUShares > 0 && !sysInfo.CPUShares { + warnings = append(warnings, "Your kernel does not support CPU shares. Shares discarded.") + logrus.Warnf("Your kernel does not support CPU shares. Shares discarded.") + resources.CPUShares = 0 + } + if resources.CPUPeriod > 0 && !sysInfo.CPUCfsPeriod { + warnings = append(warnings, "Your kernel does not support CPU cfs period. Period discarded.") + logrus.Warnf("Your kernel does not support CPU cfs period. Period discarded.") + resources.CPUPeriod = 0 + } + if resources.CPUQuota > 0 && !sysInfo.CPUCfsQuota { + warnings = append(warnings, "Your kernel does not support CPU cfs quota. Quota discarded.") + logrus.Warnf("Your kernel does not support CPU cfs quota. Quota discarded.") + resources.CPUQuota = 0 + } + + // cpuset subsystem checks and adjustments + if (resources.CpusetCpus != "" || resources.CpusetMems != "") && !sysInfo.Cpuset { + warnings = append(warnings, "Your kernel does not support cpuset. Cpuset discarded.") + logrus.Warnf("Your kernel does not support cpuset. Cpuset discarded.") + resources.CpusetCpus = "" + resources.CpusetMems = "" + } + cpusAvailable, err := sysInfo.IsCpusetCpusAvailable(resources.CpusetCpus) + if err != nil { + return warnings, fmt.Errorf("Invalid value %s for cpuset cpus.", resources.CpusetCpus) + } + if !cpusAvailable { + return warnings, fmt.Errorf("Requested CPUs are not available - requested %s, available: %s.", resources.CpusetCpus, sysInfo.Cpus) + } + memsAvailable, err := sysInfo.IsCpusetMemsAvailable(resources.CpusetMems) + if err != nil { + return warnings, fmt.Errorf("Invalid value %s for cpuset mems.", resources.CpusetMems) + } + if !memsAvailable { + return warnings, fmt.Errorf("Requested memory nodes are not available - requested %s, available: %s.", resources.CpusetMems, sysInfo.Mems) + } + + // blkio subsystem checks and adjustments + if resources.BlkioWeight > 0 && !sysInfo.BlkioWeight { + warnings = append(warnings, "Your kernel does not support Block I/O weight. Weight discarded.") + logrus.Warnf("Your kernel does not support Block I/O weight. Weight discarded.") + resources.BlkioWeight = 0 + } + if resources.BlkioWeight > 0 && (resources.BlkioWeight < 10 || resources.BlkioWeight > 1000) { + return warnings, fmt.Errorf("Range of blkio weight is from 10 to 1000.") + } + if len(resources.BlkioWeightDevice) > 0 && !sysInfo.BlkioWeightDevice { + warnings = append(warnings, "Your kernel does not support Block I/O weight_device.") + logrus.Warnf("Your kernel does not support Block I/O weight_device. Weight-device discarded.") + resources.BlkioWeightDevice = []*pblkiodev.WeightDevice{} + } + if len(resources.BlkioDeviceReadBps) > 0 && !sysInfo.BlkioReadBpsDevice { + warnings = append(warnings, "Your kernel does not support Block read limit in bytes per second.") + logrus.Warnf("Your kernel does not support Block I/O read limit in bytes per second. --device-read-bps discarded.") + resources.BlkioDeviceReadBps = []*pblkiodev.ThrottleDevice{} + } + if len(resources.BlkioDeviceWriteBps) > 0 && !sysInfo.BlkioWriteBpsDevice { + warnings = append(warnings, "Your kernel does not support Block write limit in bytes per second.") + logrus.Warnf("Your kernel does not support Block I/O write limit in bytes per second. --device-write-bps discarded.") + resources.BlkioDeviceWriteBps = []*pblkiodev.ThrottleDevice{} + } + if len(resources.BlkioDeviceReadIOps) > 0 && !sysInfo.BlkioReadIOpsDevice { + warnings = append(warnings, "Your kernel does not support Block read limit in IO per second.") + logrus.Warnf("Your kernel does not support Block I/O read limit in IO per second. -device-read-iops discarded.") + resources.BlkioDeviceReadIOps = []*pblkiodev.ThrottleDevice{} + } + if len(resources.BlkioDeviceWriteIOps) > 0 && !sysInfo.BlkioWriteIOpsDevice { + warnings = append(warnings, "Your kernel does not support Block write limit in IO per second.") + logrus.Warnf("Your kernel does not support Block I/O write limit in IO per second. --device-write-iops discarded.") + resources.BlkioDeviceWriteIOps = []*pblkiodev.ThrottleDevice{} + } + + return warnings, nil +} + +func (daemon *Daemon) getCgroupDriver() string { + cgroupDriver := cgroupFsDriver + + if UsingSystemd(daemon.configStore) { + cgroupDriver = cgroupSystemdDriver + } + return cgroupDriver +} + +// getCD gets the raw value of the native.cgroupdriver option, if set. +func getCD(config *Config) string { + for _, option := range config.ExecOptions { + key, val, err := parsers.ParseKeyValueOpt(option) + if err != nil || !strings.EqualFold(key, "native.cgroupdriver") { + continue + } + return val + } + return "" +} + +// VerifyCgroupDriver validates native.cgroupdriver +func VerifyCgroupDriver(config *Config) error { + cd := getCD(config) + if cd == "" || cd == cgroupFsDriver || cd == cgroupSystemdDriver { + return nil + } + return fmt.Errorf("native.cgroupdriver option %s not supported", cd) +} + +// UsingSystemd returns true if cli option includes native.cgroupdriver=systemd +func UsingSystemd(config *Config) bool { + return getCD(config) == cgroupSystemdDriver +} + +// verifyPlatformContainerSettings performs platform-specific validation of the +// hostconfig and config structures. +func verifyPlatformContainerSettings(daemon *Daemon, hostConfig *containertypes.HostConfig, config *containertypes.Config, update bool) ([]string, error) { + warnings := []string{} + sysInfo := sysinfo.New(true) + + warnings, err := daemon.verifyExperimentalContainerSettings(hostConfig, config) + if err != nil { + return warnings, err + } + + w, err := verifyContainerResources(&hostConfig.Resources, sysInfo, update) + if err != nil { + return warnings, err + } + warnings = append(warnings, w...) + + if hostConfig.ShmSize < 0 { + return warnings, fmt.Errorf("SHM size must be greater then 0") + } + + if hostConfig.OomScoreAdj < -1000 || hostConfig.OomScoreAdj > 1000 { + return warnings, fmt.Errorf("Invalid value %d, range for oom score adj is [-1000, 1000].", hostConfig.OomScoreAdj) + } + if sysInfo.IPv4ForwardingDisabled { + warnings = append(warnings, "IPv4 forwarding is disabled. Networking will not work.") + logrus.Warnf("IPv4 forwarding is disabled. Networking will not work") + } + // check for various conflicting options with user namespaces + if daemon.configStore.RemappedRoot != "" && hostConfig.UsernsMode.IsPrivate() { + if hostConfig.Privileged { + return warnings, fmt.Errorf("Privileged mode is incompatible with user namespaces") + } + if hostConfig.NetworkMode.IsHost() { + return warnings, fmt.Errorf("Cannot share the host's network namespace when user namespaces are enabled") + } + if hostConfig.PidMode.IsHost() { + return warnings, fmt.Errorf("Cannot share the host PID namespace when user namespaces are enabled") + } + if hostConfig.ReadonlyRootfs { + return warnings, fmt.Errorf("Cannot use the --read-only option when user namespaces are enabled") + } + } + if hostConfig.CgroupParent != "" && UsingSystemd(daemon.configStore) { + // CgroupParent for systemd cgroup should be named as "xxx.slice" + if len(hostConfig.CgroupParent) <= 6 || !strings.HasSuffix(hostConfig.CgroupParent, ".slice") { + return warnings, fmt.Errorf("cgroup-parent for systemd cgroup should be a valid slice named as \"xxx.slice\"") + } + } + return warnings, nil +} + +// verifyDaemonSettings performs validation of daemon config struct +func verifyDaemonSettings(config *Config) error { + // Check for mutually incompatible config options + if config.bridgeConfig.Iface != "" && config.bridgeConfig.IP != "" { + return fmt.Errorf("You specified -b & --bip, mutually exclusive options. Please specify only one") + } + if !config.bridgeConfig.EnableIPTables && !config.bridgeConfig.InterContainerCommunication { + return fmt.Errorf("You specified --iptables=false with --icc=false. ICC=false uses iptables to function. Please set --icc or --iptables to true") + } + if !config.bridgeConfig.EnableIPTables && config.bridgeConfig.EnableIPMasq { + config.bridgeConfig.EnableIPMasq = false + } + if err := VerifyCgroupDriver(config); err != nil { + return err + } + if config.CgroupParent != "" && UsingSystemd(config) { + if len(config.CgroupParent) <= 6 || !strings.HasSuffix(config.CgroupParent, ".slice") { + return fmt.Errorf("cgroup-parent for systemd cgroup should be a valid slice named as \"xxx.slice\"") + } + } + return nil +} + +// checkSystem validates platform-specific requirements +func checkSystem() error { + if os.Geteuid() != 0 { + return fmt.Errorf("The Docker daemon needs to be run as root") + } + return checkKernel() +} + +// configureMaxThreads sets the Go runtime max threads threshold +// which is 90% of the kernel setting from /proc/sys/kernel/threads-max +func configureMaxThreads(config *Config) error { + mt, err := ioutil.ReadFile("/proc/sys/kernel/threads-max") + if err != nil { + return err + } + mtint, err := strconv.Atoi(strings.TrimSpace(string(mt))) + if err != nil { + return err + } + maxThreads := (mtint / 100) * 90 + debug.SetMaxThreads(maxThreads) + logrus.Debugf("Golang's threads limit set to %d", maxThreads) + return nil +} + +// configureKernelSecuritySupport configures and validate security support for the kernel +func configureKernelSecuritySupport(config *Config, driverName string) error { + if config.EnableSelinuxSupport { + if selinuxEnabled() { + // As Docker on overlayFS and SELinux are incompatible at present, error on overlayfs being enabled + if driverName == "overlay" { + return fmt.Errorf("SELinux is not supported with the %s graph driver", driverName) + } + logrus.Debug("SELinux enabled successfully") + } else { + logrus.Warn("Docker could not enable SELinux on the host system") + } + } else { + selinuxSetDisabled() + } + return nil +} + +func (daemon *Daemon) initNetworkController(config *Config) (libnetwork.NetworkController, error) { + netOptions, err := daemon.networkOptions(config) + if err != nil { + return nil, err + } + + controller, err := libnetwork.New(netOptions...) + if err != nil { + return nil, fmt.Errorf("error obtaining controller instance: %v", err) + } + + // Initialize default network on "null" + if _, err := controller.NewNetwork("null", "none", libnetwork.NetworkOptionPersist(false)); err != nil { + return nil, fmt.Errorf("Error creating default \"null\" network: %v", err) + } + + // Initialize default network on "host" + if _, err := controller.NewNetwork("host", "host", libnetwork.NetworkOptionPersist(false)); err != nil { + return nil, fmt.Errorf("Error creating default \"host\" network: %v", err) + } + + if !config.DisableBridge { + // Initialize default driver "bridge" + if err := initBridgeDriver(controller, config); err != nil { + return nil, err + } + } + + return controller, nil +} + +func driverOptions(config *Config) []nwconfig.Option { + bridgeConfig := options.Generic{ + "EnableIPForwarding": config.bridgeConfig.EnableIPForward, + "EnableIPTables": config.bridgeConfig.EnableIPTables, + "EnableUserlandProxy": config.bridgeConfig.EnableUserlandProxy} + bridgeOption := options.Generic{netlabel.GenericData: bridgeConfig} + + dOptions := []nwconfig.Option{} + dOptions = append(dOptions, nwconfig.OptionDriverConfig("bridge", bridgeOption)) + return dOptions +} + +func initBridgeDriver(controller libnetwork.NetworkController, config *Config) error { + if n, err := controller.NetworkByName("bridge"); err == nil { + if err = n.Delete(); err != nil { + return fmt.Errorf("could not delete the default bridge network: %v", err) + } + } + + bridgeName := bridge.DefaultBridgeName + if config.bridgeConfig.Iface != "" { + bridgeName = config.bridgeConfig.Iface + } + netOption := map[string]string{ + bridge.BridgeName: bridgeName, + bridge.DefaultBridge: strconv.FormatBool(true), + netlabel.DriverMTU: strconv.Itoa(config.Mtu), + bridge.EnableIPMasquerade: strconv.FormatBool(config.bridgeConfig.EnableIPMasq), + bridge.EnableICC: strconv.FormatBool(config.bridgeConfig.InterContainerCommunication), + } + + // --ip processing + if config.bridgeConfig.DefaultIP != nil { + netOption[bridge.DefaultBindingIP] = config.bridgeConfig.DefaultIP.String() + } + + var ( + ipamV4Conf *libnetwork.IpamConf + ipamV6Conf *libnetwork.IpamConf + ) + + ipamV4Conf = &libnetwork.IpamConf{AuxAddresses: make(map[string]string)} + + nw, nw6List, err := ipamutils.ElectInterfaceAddresses(bridgeName) + if err == nil { + ipamV4Conf.PreferredPool = lntypes.GetIPNetCanonical(nw).String() + hip, _ := lntypes.GetHostPartIP(nw.IP, nw.Mask) + if hip.IsGlobalUnicast() { + ipamV4Conf.Gateway = nw.IP.String() + } + } + + if config.bridgeConfig.IP != "" { + ipamV4Conf.PreferredPool = config.bridgeConfig.IP + ip, _, err := net.ParseCIDR(config.bridgeConfig.IP) + if err != nil { + return err + } + ipamV4Conf.Gateway = ip.String() + } else if bridgeName == bridge.DefaultBridgeName && ipamV4Conf.PreferredPool != "" { + logrus.Infof("Default bridge (%s) is assigned with an IP address %s. Daemon option --bip can be used to set a preferred IP address", bridgeName, ipamV4Conf.PreferredPool) + } + + if config.bridgeConfig.FixedCIDR != "" { + _, fCIDR, err := net.ParseCIDR(config.bridgeConfig.FixedCIDR) + if err != nil { + return err + } + + ipamV4Conf.SubPool = fCIDR.String() + } + + if config.bridgeConfig.DefaultGatewayIPv4 != nil { + ipamV4Conf.AuxAddresses["DefaultGatewayIPv4"] = config.bridgeConfig.DefaultGatewayIPv4.String() + } + + var deferIPv6Alloc bool + if config.bridgeConfig.FixedCIDRv6 != "" { + _, fCIDRv6, err := net.ParseCIDR(config.bridgeConfig.FixedCIDRv6) + if err != nil { + return err + } + + // In case user has specified the daemon flag --fixed-cidr-v6 and the passed network has + // at least 48 host bits, we need to guarantee the current behavior where the containers' + // IPv6 addresses will be constructed based on the containers' interface MAC address. + // We do so by telling libnetwork to defer the IPv6 address allocation for the endpoints + // on this network until after the driver has created the endpoint and returned the + // constructed address. Libnetwork will then reserve this address with the ipam driver. + ones, _ := fCIDRv6.Mask.Size() + deferIPv6Alloc = ones <= 80 + + if ipamV6Conf == nil { + ipamV6Conf = &libnetwork.IpamConf{AuxAddresses: make(map[string]string)} + } + ipamV6Conf.PreferredPool = fCIDRv6.String() + + // In case the --fixed-cidr-v6 is specified and the current docker0 bridge IPv6 + // address belongs to the same network, we need to inform libnetwork about it, so + // that it can be reserved with IPAM and it will not be given away to somebody else + for _, nw6 := range nw6List { + if fCIDRv6.Contains(nw6.IP) { + ipamV6Conf.Gateway = nw6.IP.String() + break + } + } + } + + if config.bridgeConfig.DefaultGatewayIPv6 != nil { + if ipamV6Conf == nil { + ipamV6Conf = &libnetwork.IpamConf{AuxAddresses: make(map[string]string)} + } + ipamV6Conf.AuxAddresses["DefaultGatewayIPv6"] = config.bridgeConfig.DefaultGatewayIPv6.String() + } + + v4Conf := []*libnetwork.IpamConf{ipamV4Conf} + v6Conf := []*libnetwork.IpamConf{} + if ipamV6Conf != nil { + v6Conf = append(v6Conf, ipamV6Conf) + } + // Initialize default network on "bridge" with the same name + _, err = controller.NewNetwork("bridge", "bridge", + libnetwork.NetworkOptionEnableIPv6(config.bridgeConfig.EnableIPv6), + libnetwork.NetworkOptionDriverOpts(netOption), + libnetwork.NetworkOptionIpam("default", "", v4Conf, v6Conf, nil), + libnetwork.NetworkOptionDeferIPv6Alloc(deferIPv6Alloc)) + if err != nil { + return fmt.Errorf("Error creating default \"bridge\" network: %v", err) + } + return nil +} + +// setupInitLayer populates a directory with mountpoints suitable +// for bind-mounting things into the container. +// +// This extra layer is used by all containers as the top-most ro layer. It protects +// the container from unwanted side-effects on the rw layer. +func setupInitLayer(initLayer string, rootUID, rootGID int) error { + for pth, typ := range map[string]string{ + "/dev/pts": "dir", + "/dev/shm": "dir", + "/proc": "dir", + "/sys": "dir", + "/.dockerenv": "file", + "/etc/resolv.conf": "file", + "/etc/hosts": "file", + "/etc/hostname": "file", + "/dev/console": "file", + "/etc/mtab": "/proc/mounts", + } { + parts := strings.Split(pth, "/") + prev := "/" + for _, p := range parts[1:] { + prev = filepath.Join(prev, p) + syscall.Unlink(filepath.Join(initLayer, prev)) + } + + if _, err := os.Stat(filepath.Join(initLayer, pth)); err != nil { + if os.IsNotExist(err) { + if err := idtools.MkdirAllNewAs(filepath.Join(initLayer, filepath.Dir(pth)), 0755, rootUID, rootGID); err != nil { + return err + } + switch typ { + case "dir": + if err := idtools.MkdirAllNewAs(filepath.Join(initLayer, pth), 0755, rootUID, rootGID); err != nil { + return err + } + case "file": + f, err := os.OpenFile(filepath.Join(initLayer, pth), os.O_CREATE, 0755) + if err != nil { + return err + } + f.Chown(rootUID, rootGID) + f.Close() + default: + if err := os.Symlink(typ, filepath.Join(initLayer, pth)); err != nil { + return err + } + } + } else { + return err + } + } + } + + // Layer is ready to use, if it wasn't before. + return nil +} + +// Parse the remapped root (user namespace) option, which can be one of: +// username - valid username from /etc/passwd +// username:groupname - valid username; valid groupname from /etc/group +// uid - 32-bit unsigned int valid Linux UID value +// uid:gid - uid value; 32-bit unsigned int Linux GID value +// +// If no groupname is specified, and a username is specified, an attempt +// will be made to lookup a gid for that username as a groupname +// +// If names are used, they are verified to exist in passwd/group +func parseRemappedRoot(usergrp string) (string, string, error) { + + var ( + userID, groupID int + username, groupname string + ) + + idparts := strings.Split(usergrp, ":") + if len(idparts) > 2 { + return "", "", fmt.Errorf("Invalid user/group specification in --userns-remap: %q", usergrp) + } + + if uid, err := strconv.ParseInt(idparts[0], 10, 32); err == nil { + // must be a uid; take it as valid + userID = int(uid) + luser, err := user.LookupUid(userID) + if err != nil { + return "", "", fmt.Errorf("Uid %d has no entry in /etc/passwd: %v", userID, err) + } + username = luser.Name + if len(idparts) == 1 { + // if the uid was numeric and no gid was specified, take the uid as the gid + groupID = userID + lgrp, err := user.LookupGid(groupID) + if err != nil { + return "", "", fmt.Errorf("Gid %d has no entry in /etc/group: %v", groupID, err) + } + groupname = lgrp.Name + } + } else { + lookupName := idparts[0] + // special case: if the user specified "default", they want Docker to create or + // use (after creation) the "dockremap" user/group for root remapping + if lookupName == defaultIDSpecifier { + lookupName = defaultRemappedID + } + luser, err := user.LookupUser(lookupName) + if err != nil && idparts[0] != defaultIDSpecifier { + // error if the name requested isn't the special "dockremap" ID + return "", "", fmt.Errorf("Error during uid lookup for %q: %v", lookupName, err) + } else if err != nil { + // special case-- if the username == "default", then we have been asked + // to create a new entry pair in /etc/{passwd,group} for which the /etc/sub{uid,gid} + // ranges will be used for the user and group mappings in user namespaced containers + _, _, err := idtools.AddNamespaceRangesUser(defaultRemappedID) + if err == nil { + return defaultRemappedID, defaultRemappedID, nil + } + return "", "", fmt.Errorf("Error during %q user creation: %v", defaultRemappedID, err) + } + username = luser.Name + if len(idparts) == 1 { + // we only have a string username, and no group specified; look up gid from username as group + group, err := user.LookupGroup(lookupName) + if err != nil { + return "", "", fmt.Errorf("Error during gid lookup for %q: %v", lookupName, err) + } + groupID = group.Gid + groupname = group.Name + } + } + + if len(idparts) == 2 { + // groupname or gid is separately specified and must be resolved + // to a unsigned 32-bit gid + if gid, err := strconv.ParseInt(idparts[1], 10, 32); err == nil { + // must be a gid, take it as valid + groupID = int(gid) + lgrp, err := user.LookupGid(groupID) + if err != nil { + return "", "", fmt.Errorf("Gid %d has no entry in /etc/passwd: %v", groupID, err) + } + groupname = lgrp.Name + } else { + // not a number; attempt a lookup + if _, err := user.LookupGroup(idparts[1]); err != nil { + return "", "", fmt.Errorf("Error during groupname lookup for %q: %v", idparts[1], err) + } + groupname = idparts[1] + } + } + return username, groupname, nil +} + +func setupRemappedRoot(config *Config) ([]idtools.IDMap, []idtools.IDMap, error) { + if runtime.GOOS != "linux" && config.RemappedRoot != "" { + return nil, nil, fmt.Errorf("User namespaces are only supported on Linux") + } + + // if the daemon was started with remapped root option, parse + // the config option to the int uid,gid values + var ( + uidMaps, gidMaps []idtools.IDMap + ) + if config.RemappedRoot != "" { + username, groupname, err := parseRemappedRoot(config.RemappedRoot) + if err != nil { + return nil, nil, err + } + if username == "root" { + // Cannot setup user namespaces with a 1-to-1 mapping; "--root=0:0" is a no-op + // effectively + logrus.Warnf("User namespaces: root cannot be remapped with itself; user namespaces are OFF") + return uidMaps, gidMaps, nil + } + logrus.Infof("User namespaces: ID ranges will be mapped to subuid/subgid ranges of: %s:%s", username, groupname) + // update remapped root setting now that we have resolved them to actual names + config.RemappedRoot = fmt.Sprintf("%s:%s", username, groupname) + + uidMaps, gidMaps, err = idtools.CreateIDMappings(username, groupname) + if err != nil { + return nil, nil, fmt.Errorf("Can't create ID mappings: %v", err) + } + } + return uidMaps, gidMaps, nil +} + +func setupDaemonRoot(config *Config, rootDir string, rootUID, rootGID int) error { + config.Root = rootDir + // the docker root metadata directory needs to have execute permissions for all users (g+x,o+x) + // so that syscalls executing as non-root, operating on subdirectories of the graph root + // (e.g. mounted layers of a container) can traverse this path. + // The user namespace support will create subdirectories for the remapped root host uid:gid + // pair owned by that same uid:gid pair for proper write access to those needed metadata and + // layer content subtrees. + if _, err := os.Stat(rootDir); err == nil { + // root current exists; verify the access bits are correct by setting them + if err = os.Chmod(rootDir, 0711); err != nil { + return err + } + } else if os.IsNotExist(err) { + // no root exists yet, create it 0711 with root:root ownership + if err := os.MkdirAll(rootDir, 0711); err != nil { + return err + } + } + + // if user namespaces are enabled we will create a subtree underneath the specified root + // with any/all specified remapped root uid/gid options on the daemon creating + // a new subdirectory with ownership set to the remapped uid/gid (so as to allow + // `chdir()` to work for containers namespaced to that uid/gid) + if config.RemappedRoot != "" { + config.Root = filepath.Join(rootDir, fmt.Sprintf("%d.%d", rootUID, rootGID)) + logrus.Debugf("Creating user namespaced daemon root: %s", config.Root) + // Create the root directory if it doesn't exists + if err := idtools.MkdirAllAs(config.Root, 0700, rootUID, rootGID); err != nil { + return fmt.Errorf("Cannot create daemon root: %s: %v", config.Root, err) + } + } + return nil +} + +// registerLinks writes the links to a file. +func (daemon *Daemon) registerLinks(container *container.Container, hostConfig *containertypes.HostConfig) error { + if hostConfig == nil || hostConfig.NetworkMode.IsUserDefined() { + return nil + } + + for _, l := range hostConfig.Links { + name, alias, err := runconfigopts.ParseLink(l) + if err != nil { + return err + } + child, err := daemon.GetContainer(name) + if err != nil { + //An error from daemon.GetContainer() means this name could not be found + return fmt.Errorf("Could not get container for %s", name) + } + for child.HostConfig.NetworkMode.IsContainer() { + parts := strings.SplitN(string(child.HostConfig.NetworkMode), ":", 2) + child, err = daemon.GetContainer(parts[1]) + if err != nil { + return fmt.Errorf("Could not get container for %s", parts[1]) + } + } + if child.HostConfig.NetworkMode.IsHost() { + return runconfig.ErrConflictHostNetworkAndLinks + } + if err := daemon.registerLink(container, child, alias); err != nil { + return err + } + } + + // After we load all the links into the daemon + // set them to nil on the hostconfig + return container.WriteHostConfig() +} + +// conditionalMountOnStart is a platform specific helper function during the +// container start to call mount. +func (daemon *Daemon) conditionalMountOnStart(container *container.Container) error { + return daemon.Mount(container) +} + +// conditionalUnmountOnCleanup is a platform specific helper function called +// during the cleanup of a container to unmount. +func (daemon *Daemon) conditionalUnmountOnCleanup(container *container.Container) error { + return daemon.Unmount(container) +} + +func restoreCustomImage(is image.Store, ls layer.Store, rs reference.Store) error { + // Unix has no custom images to register + return nil +} + +func (daemon *Daemon) stats(c *container.Container) (*types.StatsJSON, error) { + if !c.IsRunning() { + return nil, errNotRunning{c.ID} + } + stats, err := daemon.containerd.Stats(c.ID) + if err != nil { + return nil, err + } + s := &types.StatsJSON{} + cgs := stats.CgroupStats + if cgs != nil { + s.BlkioStats = types.BlkioStats{ + IoServiceBytesRecursive: copyBlkioEntry(cgs.BlkioStats.IoServiceBytesRecursive), + IoServicedRecursive: copyBlkioEntry(cgs.BlkioStats.IoServicedRecursive), + IoQueuedRecursive: copyBlkioEntry(cgs.BlkioStats.IoQueuedRecursive), + IoServiceTimeRecursive: copyBlkioEntry(cgs.BlkioStats.IoServiceTimeRecursive), + IoWaitTimeRecursive: copyBlkioEntry(cgs.BlkioStats.IoWaitTimeRecursive), + IoMergedRecursive: copyBlkioEntry(cgs.BlkioStats.IoMergedRecursive), + IoTimeRecursive: copyBlkioEntry(cgs.BlkioStats.IoTimeRecursive), + SectorsRecursive: copyBlkioEntry(cgs.BlkioStats.SectorsRecursive), + } + cpu := cgs.CpuStats + s.CPUStats = types.CPUStats{ + CPUUsage: types.CPUUsage{ + TotalUsage: cpu.CpuUsage.TotalUsage, + PercpuUsage: cpu.CpuUsage.PercpuUsage, + UsageInKernelmode: cpu.CpuUsage.UsageInKernelmode, + UsageInUsermode: cpu.CpuUsage.UsageInUsermode, + }, + ThrottlingData: types.ThrottlingData{ + Periods: cpu.ThrottlingData.Periods, + ThrottledPeriods: cpu.ThrottlingData.ThrottledPeriods, + ThrottledTime: cpu.ThrottlingData.ThrottledTime, + }, + } + mem := cgs.MemoryStats.Usage + s.MemoryStats = types.MemoryStats{ + Usage: mem.Usage, + MaxUsage: mem.MaxUsage, + Stats: cgs.MemoryStats.Stats, + Failcnt: mem.Failcnt, + Limit: mem.Limit, + } + // if the container does not set memory limit, use the machineMemory + if mem.Limit > daemon.statsCollector.machineMemory && daemon.statsCollector.machineMemory > 0 { + s.MemoryStats.Limit = daemon.statsCollector.machineMemory + } + if cgs.PidsStats != nil { + s.PidsStats = types.PidsStats{ + Current: cgs.PidsStats.Current, + } + } + } + s.Read = time.Unix(int64(stats.Timestamp), 0) + return s, nil +} + +// setDefaultIsolation determine the default isolation mode for the +// daemon to run in. This is only applicable on Windows +func (daemon *Daemon) setDefaultIsolation() error { + return nil +} + +func rootFSToAPIType(rootfs *image.RootFS) types.RootFS { + var layers []string + for _, l := range rootfs.DiffIDs { + layers = append(layers, l.String()) + } + return types.RootFS{ + Type: rootfs.Type, + Layers: layers, + } +} diff --git a/daemon/daemon_unix_test.go b/daemon/daemon_unix_test.go new file mode 100644 index 00000000..7bf307cd --- /dev/null +++ b/daemon/daemon_unix_test.go @@ -0,0 +1,199 @@ +// +build !windows + +package daemon + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/docker/docker/container" + containertypes "github.com/docker/engine-api/types/container" +) + +// Unix test as uses settings which are not available on Windows +func TestAdjustCPUShares(t *testing.T) { + tmp, err := ioutil.TempDir("", "docker-daemon-unix-test-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + daemon := &Daemon{ + repository: tmp, + root: tmp, + } + + hostConfig := &containertypes.HostConfig{ + Resources: containertypes.Resources{CPUShares: linuxMinCPUShares - 1}, + } + daemon.adaptContainerSettings(hostConfig, true) + if hostConfig.CPUShares != linuxMinCPUShares { + t.Errorf("Expected CPUShares to be %d", linuxMinCPUShares) + } + + hostConfig.CPUShares = linuxMaxCPUShares + 1 + daemon.adaptContainerSettings(hostConfig, true) + if hostConfig.CPUShares != linuxMaxCPUShares { + t.Errorf("Expected CPUShares to be %d", linuxMaxCPUShares) + } + + hostConfig.CPUShares = 0 + daemon.adaptContainerSettings(hostConfig, true) + if hostConfig.CPUShares != 0 { + t.Error("Expected CPUShares to be unchanged") + } + + hostConfig.CPUShares = 1024 + daemon.adaptContainerSettings(hostConfig, true) + if hostConfig.CPUShares != 1024 { + t.Error("Expected CPUShares to be unchanged") + } +} + +// Unix test as uses settings which are not available on Windows +func TestAdjustCPUSharesNoAdjustment(t *testing.T) { + tmp, err := ioutil.TempDir("", "docker-daemon-unix-test-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + daemon := &Daemon{ + repository: tmp, + root: tmp, + } + + hostConfig := &containertypes.HostConfig{ + Resources: containertypes.Resources{CPUShares: linuxMinCPUShares - 1}, + } + daemon.adaptContainerSettings(hostConfig, false) + if hostConfig.CPUShares != linuxMinCPUShares-1 { + t.Errorf("Expected CPUShares to be %d", linuxMinCPUShares-1) + } + + hostConfig.CPUShares = linuxMaxCPUShares + 1 + daemon.adaptContainerSettings(hostConfig, false) + if hostConfig.CPUShares != linuxMaxCPUShares+1 { + t.Errorf("Expected CPUShares to be %d", linuxMaxCPUShares+1) + } + + hostConfig.CPUShares = 0 + daemon.adaptContainerSettings(hostConfig, false) + if hostConfig.CPUShares != 0 { + t.Error("Expected CPUShares to be unchanged") + } + + hostConfig.CPUShares = 1024 + daemon.adaptContainerSettings(hostConfig, false) + if hostConfig.CPUShares != 1024 { + t.Error("Expected CPUShares to be unchanged") + } +} + +// Unix test as uses settings which are not available on Windows +func TestParseSecurityOptWithDeprecatedColon(t *testing.T) { + container := &container.Container{} + config := &containertypes.HostConfig{} + + // test apparmor + config.SecurityOpt = []string{"apparmor=test_profile"} + if err := parseSecurityOpt(container, config); err != nil { + t.Fatalf("Unexpected parseSecurityOpt error: %v", err) + } + if container.AppArmorProfile != "test_profile" { + t.Fatalf("Unexpected AppArmorProfile, expected: \"test_profile\", got %q", container.AppArmorProfile) + } + + // test seccomp + sp := "/path/to/seccomp_test.json" + config.SecurityOpt = []string{"seccomp=" + sp} + if err := parseSecurityOpt(container, config); err != nil { + t.Fatalf("Unexpected parseSecurityOpt error: %v", err) + } + if container.SeccompProfile != sp { + t.Fatalf("Unexpected AppArmorProfile, expected: %q, got %q", sp, container.SeccompProfile) + } + + // test valid label + config.SecurityOpt = []string{"label=user:USER"} + if err := parseSecurityOpt(container, config); err != nil { + t.Fatalf("Unexpected parseSecurityOpt error: %v", err) + } + + // test invalid label + config.SecurityOpt = []string{"label"} + if err := parseSecurityOpt(container, config); err == nil { + t.Fatal("Expected parseSecurityOpt error, got nil") + } + + // test invalid opt + config.SecurityOpt = []string{"test"} + if err := parseSecurityOpt(container, config); err == nil { + t.Fatal("Expected parseSecurityOpt error, got nil") + } +} + +func TestParseSecurityOpt(t *testing.T) { + container := &container.Container{} + config := &containertypes.HostConfig{} + + // test apparmor + config.SecurityOpt = []string{"apparmor=test_profile"} + if err := parseSecurityOpt(container, config); err != nil { + t.Fatalf("Unexpected parseSecurityOpt error: %v", err) + } + if container.AppArmorProfile != "test_profile" { + t.Fatalf("Unexpected AppArmorProfile, expected: \"test_profile\", got %q", container.AppArmorProfile) + } + + // test seccomp + sp := "/path/to/seccomp_test.json" + config.SecurityOpt = []string{"seccomp=" + sp} + if err := parseSecurityOpt(container, config); err != nil { + t.Fatalf("Unexpected parseSecurityOpt error: %v", err) + } + if container.SeccompProfile != sp { + t.Fatalf("Unexpected SeccompProfile, expected: %q, got %q", sp, container.SeccompProfile) + } + + // test valid label + config.SecurityOpt = []string{"label=user:USER"} + if err := parseSecurityOpt(container, config); err != nil { + t.Fatalf("Unexpected parseSecurityOpt error: %v", err) + } + + // test invalid label + config.SecurityOpt = []string{"label"} + if err := parseSecurityOpt(container, config); err == nil { + t.Fatal("Expected parseSecurityOpt error, got nil") + } + + // test invalid opt + config.SecurityOpt = []string{"test"} + if err := parseSecurityOpt(container, config); err == nil { + t.Fatal("Expected parseSecurityOpt error, got nil") + } +} + +func TestNetworkOptions(t *testing.T) { + daemon := &Daemon{} + dconfigCorrect := &Config{ + CommonConfig: CommonConfig{ + ClusterStore: "consul://localhost:8500", + ClusterAdvertise: "192.168.0.1:8000", + }, + } + + if _, err := daemon.networkOptions(dconfigCorrect); err != nil { + t.Fatalf("Expect networkOptions success, got error: %v", err) + } + + dconfigWrong := &Config{ + CommonConfig: CommonConfig{ + ClusterStore: "consul://localhost:8500://test://bbb", + }, + } + + if _, err := daemon.networkOptions(dconfigWrong); err == nil { + t.Fatalf("Expected networkOptions error, got nil") + } +} diff --git a/daemon/daemon_unsupported.go b/daemon/daemon_unsupported.go new file mode 100644 index 00000000..987528f4 --- /dev/null +++ b/daemon/daemon_unsupported.go @@ -0,0 +1,5 @@ +// +build !linux,!freebsd,!windows + +package daemon + +const platformSupported = false diff --git a/daemon/daemon_windows.go b/daemon/daemon_windows.go new file mode 100644 index 00000000..2d07011a --- /dev/null +++ b/daemon/daemon_windows.go @@ -0,0 +1,467 @@ +package daemon + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/Microsoft/hcsshim" + "github.com/Sirupsen/logrus" + "github.com/docker/docker/container" + "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/dockerversion" + "github.com/docker/docker/image" + "github.com/docker/docker/layer" + "github.com/docker/docker/reference" + "github.com/docker/docker/runconfig" + // register the windows graph driver + "github.com/docker/docker/daemon/graphdriver/windows" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/parsers" + "github.com/docker/docker/pkg/system" + "github.com/docker/engine-api/types" + containertypes "github.com/docker/engine-api/types/container" + "github.com/docker/libnetwork" + nwconfig "github.com/docker/libnetwork/config" + winlibnetwork "github.com/docker/libnetwork/drivers/windows" + "github.com/docker/libnetwork/netlabel" + "github.com/docker/libnetwork/options" + blkiodev "github.com/opencontainers/runc/libcontainer/configs" +) + +const ( + defaultVirtualSwitch = "Virtual Switch" + defaultNetworkSpace = "172.16.0.0/12" + platformSupported = true + windowsMinCPUShares = 1 + windowsMaxCPUShares = 10000 +) + +func getBlkioWeightDevices(config *containertypes.HostConfig) ([]blkiodev.WeightDevice, error) { + return nil, nil +} + +func parseSecurityOpt(container *container.Container, config *containertypes.HostConfig) error { + return nil +} + +func getBlkioReadIOpsDevices(config *containertypes.HostConfig) ([]blkiodev.ThrottleDevice, error) { + return nil, nil +} + +func getBlkioWriteIOpsDevices(config *containertypes.HostConfig) ([]blkiodev.ThrottleDevice, error) { + return nil, nil +} + +func getBlkioReadBpsDevices(config *containertypes.HostConfig) ([]blkiodev.ThrottleDevice, error) { + return nil, nil +} + +func getBlkioWriteBpsDevices(config *containertypes.HostConfig) ([]blkiodev.ThrottleDevice, error) { + return nil, nil +} + +func setupInitLayer(initLayer string, rootUID, rootGID int) error { + return nil +} + +func checkKernel() error { + return nil +} + +func (daemon *Daemon) getCgroupDriver() string { + return "" +} + +// adaptContainerSettings is called during container creation to modify any +// settings necessary in the HostConfig structure. +func (daemon *Daemon) adaptContainerSettings(hostConfig *containertypes.HostConfig, adjustCPUShares bool) error { + if hostConfig == nil { + return nil + } + + if hostConfig.CPUShares < 0 { + logrus.Warnf("Changing requested CPUShares of %d to minimum allowed of %d", hostConfig.CPUShares, windowsMinCPUShares) + hostConfig.CPUShares = windowsMinCPUShares + } else if hostConfig.CPUShares > windowsMaxCPUShares { + logrus.Warnf("Changing requested CPUShares of %d to maximum allowed of %d", hostConfig.CPUShares, windowsMaxCPUShares) + hostConfig.CPUShares = windowsMaxCPUShares + } + + return nil +} + +// verifyPlatformContainerSettings performs platform-specific validation of the +// hostconfig and config structures. +func verifyPlatformContainerSettings(daemon *Daemon, hostConfig *containertypes.HostConfig, config *containertypes.Config, update bool) ([]string, error) { + return nil, nil +} + +// verifyDaemonSettings performs validation of daemon config struct +func verifyDaemonSettings(config *Config) error { + return nil +} + +// checkSystem validates platform-specific requirements +func checkSystem() error { + // Validate the OS version. Note that docker.exe must be manifested for this + // call to return the correct version. + osv, err := system.GetOSVersion() + if err != nil { + return err + } + if osv.MajorVersion < 10 { + return fmt.Errorf("This version of Windows does not support the docker daemon") + } + if osv.Build < 10586 { + return fmt.Errorf("The Windows daemon requires Windows Server 2016 Technical Preview 4, build 10586 or later") + } + return nil +} + +// configureKernelSecuritySupport configures and validate security support for the kernel +func configureKernelSecuritySupport(config *Config, driverName string) error { + return nil +} + +// configureMaxThreads sets the Go runtime max threads threshold +func configureMaxThreads(config *Config) error { + return nil +} + +func (daemon *Daemon) initNetworkController(config *Config) (libnetwork.NetworkController, error) { + // TODO Windows: Remove this check once TP4 is no longer supported + osv, err := system.GetOSVersion() + if err != nil { + return nil, err + } + + if osv.Build < 14260 { + // Set the name of the virtual switch if not specified by -b on daemon start + if config.bridgeConfig.Iface == "" { + config.bridgeConfig.Iface = defaultVirtualSwitch + } + logrus.Warnf("Network controller is not supported by the current platform build version") + return nil, nil + } + + netOptions, err := daemon.networkOptions(config) + if err != nil { + return nil, err + } + controller, err := libnetwork.New(netOptions...) + if err != nil { + return nil, fmt.Errorf("error obtaining controller instance: %v", err) + } + + hnsresponse, err := hcsshim.HNSListNetworkRequest("GET", "", "") + if err != nil { + return nil, err + } + + // Remove networks not present in HNS + for _, v := range controller.Networks() { + options := v.Info().DriverOptions() + hnsid := options[winlibnetwork.HNSID] + found := false + + for _, v := range hnsresponse { + if v.Id == hnsid { + found = true + break + } + } + + if !found { + err = v.Delete() + if err != nil { + return nil, err + } + } + } + + _, err = controller.NewNetwork("null", "none", libnetwork.NetworkOptionPersist(false)) + if err != nil { + return nil, err + } + + // discover and add HNS networks to windows + // network that exist are removed and added again + for _, v := range hnsresponse { + var n libnetwork.Network + s := func(current libnetwork.Network) bool { + options := current.Info().DriverOptions() + if options[winlibnetwork.HNSID] == v.Id { + n = current + return true + } + return false + } + + controller.WalkNetworks(s) + if n != nil { + v.Name = n.Name() + n.Delete() + } + + netOption := map[string]string{ + winlibnetwork.NetworkName: v.Name, + winlibnetwork.HNSID: v.Id, + } + + v4Conf := []*libnetwork.IpamConf{} + for _, subnet := range v.Subnets { + ipamV4Conf := libnetwork.IpamConf{} + ipamV4Conf.PreferredPool = subnet.AddressPrefix + ipamV4Conf.Gateway = subnet.GatewayAddress + v4Conf = append(v4Conf, &ipamV4Conf) + } + + name := v.Name + // There is only one nat network supported in windows. + // If it exists with a different name add it as the default name + if runconfig.DefaultDaemonNetworkMode() == containertypes.NetworkMode(strings.ToLower(v.Type)) { + name = runconfig.DefaultDaemonNetworkMode().NetworkName() + } + + v6Conf := []*libnetwork.IpamConf{} + _, err := controller.NewNetwork(strings.ToLower(v.Type), name, + libnetwork.NetworkOptionGeneric(options.Generic{ + netlabel.GenericData: netOption, + }), + libnetwork.NetworkOptionIpam("default", "", v4Conf, v6Conf, nil), + ) + + if err != nil { + logrus.Errorf("Error occurred when creating network %v", err) + } + } + + if !config.DisableBridge { + // Initialize default driver "bridge" + if err := initBridgeDriver(controller, config); err != nil { + return nil, err + } + } + + return controller, nil +} + +func initBridgeDriver(controller libnetwork.NetworkController, config *Config) error { + if _, err := controller.NetworkByName(runconfig.DefaultDaemonNetworkMode().NetworkName()); err == nil { + return nil + } + + netOption := map[string]string{ + winlibnetwork.NetworkName: runconfig.DefaultDaemonNetworkMode().NetworkName(), + } + + ipamV4Conf := libnetwork.IpamConf{} + if config.bridgeConfig.FixedCIDR == "" { + ipamV4Conf.PreferredPool = defaultNetworkSpace + } else { + ipamV4Conf.PreferredPool = config.bridgeConfig.FixedCIDR + } + + v4Conf := []*libnetwork.IpamConf{&ipamV4Conf} + v6Conf := []*libnetwork.IpamConf{} + + _, err := controller.NewNetwork(string(runconfig.DefaultDaemonNetworkMode()), runconfig.DefaultDaemonNetworkMode().NetworkName(), + libnetwork.NetworkOptionGeneric(options.Generic{ + netlabel.GenericData: netOption, + }), + libnetwork.NetworkOptionIpam("default", "", v4Conf, v6Conf, nil), + ) + + if err != nil { + return fmt.Errorf("Error creating default network: %v", err) + } + return nil +} + +// registerLinks sets up links between containers and writes the +// configuration out for persistence. As of Windows TP4, links are not supported. +func (daemon *Daemon) registerLinks(container *container.Container, hostConfig *containertypes.HostConfig) error { + return nil +} + +func (daemon *Daemon) cleanupMountsByID(in string) error { + return nil +} + +func (daemon *Daemon) cleanupMounts() error { + return nil +} + +func setupRemappedRoot(config *Config) ([]idtools.IDMap, []idtools.IDMap, error) { + return nil, nil, nil +} + +func setupDaemonRoot(config *Config, rootDir string, rootUID, rootGID int) error { + config.Root = rootDir + // Create the root directory if it doesn't exists + if err := system.MkdirAll(config.Root, 0700); err != nil && !os.IsExist(err) { + return err + } + return nil +} + +// runasHyperVContainer returns true if we are going to run as a Hyper-V container +func (daemon *Daemon) runAsHyperVContainer(container *container.Container) bool { + if container.HostConfig.Isolation.IsDefault() { + // Container is set to use the default, so take the default from the daemon configuration + return daemon.defaultIsolation.IsHyperV() + } + + // Container is requesting an isolation mode. Honour it. + return container.HostConfig.Isolation.IsHyperV() + +} + +// conditionalMountOnStart is a platform specific helper function during the +// container start to call mount. +func (daemon *Daemon) conditionalMountOnStart(container *container.Container) error { + // We do not mount if a Hyper-V container + if !daemon.runAsHyperVContainer(container) { + return daemon.Mount(container) + } + return nil +} + +// conditionalUnmountOnCleanup is a platform specific helper function called +// during the cleanup of a container to unmount. +func (daemon *Daemon) conditionalUnmountOnCleanup(container *container.Container) error { + // We do not unmount if a Hyper-V container + if !daemon.runAsHyperVContainer(container) { + return daemon.Unmount(container) + } + return nil +} + +func restoreCustomImage(is image.Store, ls layer.Store, rs reference.Store) error { + type graphDriverStore interface { + GraphDriver() graphdriver.Driver + } + + gds, ok := ls.(graphDriverStore) + if !ok { + return nil + } + + driver := gds.GraphDriver() + wd, ok := driver.(*windows.Driver) + if !ok { + return nil + } + + imageInfos, err := wd.GetCustomImageInfos() + if err != nil { + return err + } + + // Convert imageData to valid image configuration + for i := range imageInfos { + name := strings.ToLower(imageInfos[i].Name) + + type registrar interface { + RegisterDiffID(graphID string, size int64) (layer.Layer, error) + } + r, ok := ls.(registrar) + if !ok { + return errors.New("Layerstore doesn't support RegisterDiffID") + } + if _, err := r.RegisterDiffID(imageInfos[i].ID, imageInfos[i].Size); err != nil { + return err + } + // layer is intentionally not released + + rootFS := image.NewRootFS() + rootFS.BaseLayer = filepath.Base(imageInfos[i].Path) + + // Create history for base layer + config, err := json.Marshal(&image.Image{ + V1Image: image.V1Image{ + DockerVersion: dockerversion.Version, + Architecture: runtime.GOARCH, + OS: runtime.GOOS, + Created: imageInfos[i].CreatedTime, + }, + RootFS: rootFS, + History: []image.History{}, + }) + + named, err := reference.ParseNamed(name) + if err != nil { + return err + } + + ref, err := reference.WithTag(named, imageInfos[i].Version) + if err != nil { + return err + } + + id, err := is.Create(config) + if err != nil { + return err + } + + if err := rs.AddTag(ref, id, true); err != nil { + return err + } + + logrus.Debugf("Registered base layer %s as %s", ref, id) + } + return nil +} + +func driverOptions(config *Config) []nwconfig.Option { + return []nwconfig.Option{} +} + +func (daemon *Daemon) stats(c *container.Container) (*types.StatsJSON, error) { + return nil, nil +} + +// setDefaultIsolation determine the default isolation mode for the +// daemon to run in. This is only applicable on Windows +func (daemon *Daemon) setDefaultIsolation() error { + daemon.defaultIsolation = containertypes.Isolation("process") + for _, option := range daemon.configStore.ExecOptions { + key, val, err := parsers.ParseKeyValueOpt(option) + if err != nil { + return err + } + key = strings.ToLower(key) + switch key { + + case "isolation": + if !containertypes.Isolation(val).IsValid() { + return fmt.Errorf("Invalid exec-opt value for 'isolation':'%s'", val) + } + if containertypes.Isolation(val).IsHyperV() { + daemon.defaultIsolation = containertypes.Isolation("hyperv") + } + default: + return fmt.Errorf("Unrecognised exec-opt '%s'\n", key) + } + } + + logrus.Infof("Windows default isolation mode: %s", daemon.defaultIsolation) + return nil +} + +func rootFSToAPIType(rootfs *image.RootFS) types.RootFS { + var layers []string + for _, l := range rootfs.DiffIDs { + layers = append(layers, l.String()) + } + return types.RootFS{ + Type: rootfs.Type, + Layers: layers, + BaseLayer: rootfs.BaseLayer, + } +} diff --git a/daemon/debugtrap_unix.go b/daemon/debugtrap_unix.go new file mode 100644 index 00000000..c4a11b07 --- /dev/null +++ b/daemon/debugtrap_unix.go @@ -0,0 +1,21 @@ +// +build !windows + +package daemon + +import ( + "os" + "os/signal" + "syscall" + + psignal "github.com/docker/docker/pkg/signal" +) + +func setupDumpStackTrap() { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGUSR1) + go func() { + for range c { + psignal.DumpStacks() + } + }() +} diff --git a/daemon/debugtrap_unsupported.go b/daemon/debugtrap_unsupported.go new file mode 100644 index 00000000..fef1bd77 --- /dev/null +++ b/daemon/debugtrap_unsupported.go @@ -0,0 +1,7 @@ +// +build !linux,!darwin,!freebsd,!windows + +package daemon + +func setupDumpStackTrap() { + return +} diff --git a/daemon/debugtrap_windows.go b/daemon/debugtrap_windows.go new file mode 100644 index 00000000..a981c1ea --- /dev/null +++ b/daemon/debugtrap_windows.go @@ -0,0 +1,30 @@ +package daemon + +import ( + "fmt" + "os" + "syscall" + + "github.com/Sirupsen/logrus" + psignal "github.com/docker/docker/pkg/signal" + "github.com/docker/docker/pkg/system" +) + +func setupDumpStackTrap() { + // Windows does not support signals like *nix systems. So instead of + // trapping on SIGUSR1 to dump stacks, we wait on a Win32 event to be + // signaled. + go func() { + sa := syscall.SecurityAttributes{ + Length: 0, + } + ev := "Global\\docker-daemon-" + fmt.Sprint(os.Getpid()) + if h, _ := system.CreateEvent(&sa, false, false, ev); h != 0 { + logrus.Debugf("Stackdump - waiting signal at %s", ev) + for { + syscall.WaitForSingleObject(h, syscall.INFINITE) + psignal.DumpStacks() + } + } + }() +} diff --git a/daemon/delete.go b/daemon/delete.go new file mode 100644 index 00000000..ec9d5c5f --- /dev/null +++ b/daemon/delete.go @@ -0,0 +1,157 @@ +package daemon + +import ( + "fmt" + "os" + "path" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/container" + "github.com/docker/docker/errors" + "github.com/docker/docker/layer" + volumestore "github.com/docker/docker/volume/store" + "github.com/docker/engine-api/types" +) + +// ContainerRm removes the container id from the filesystem. An error +// is returned if the container is not found, or if the remove +// fails. If the remove succeeds, the container name is released, and +// network links are removed. +func (daemon *Daemon) ContainerRm(name string, config *types.ContainerRmConfig) error { + container, err := daemon.GetContainer(name) + if err != nil { + return err + } + + // Container state RemovalInProgress should be used to avoid races. + if inProgress := container.SetRemovalInProgress(); inProgress { + return nil + } + defer container.ResetRemovalInProgress() + + // check if container wasn't deregistered by previous rm since Get + if c := daemon.containers.Get(container.ID); c == nil { + return nil + } + + if config.RemoveLink { + return daemon.rmLink(container, name) + } + + err = daemon.cleanupContainer(container, config.ForceRemove) + if err == nil || config.ForceRemove { + if e := daemon.removeMountPoints(container, config.RemoveVolume); e != nil { + logrus.Error(e) + } + } + + return err +} + +func (daemon *Daemon) rmLink(container *container.Container, name string) error { + if name[0] != '/' { + name = "/" + name + } + parent, n := path.Split(name) + if parent == "/" { + return fmt.Errorf("Conflict, cannot remove the default name of the container") + } + + parent = strings.TrimSuffix(parent, "/") + pe, err := daemon.nameIndex.Get(parent) + if err != nil { + return fmt.Errorf("Cannot get parent %s for name %s", parent, name) + } + + daemon.releaseName(name) + parentContainer, _ := daemon.GetContainer(pe) + if parentContainer != nil { + daemon.linkIndex.unlink(name, container, parentContainer) + if err := daemon.updateNetwork(parentContainer); err != nil { + logrus.Debugf("Could not update network to remove link %s: %v", n, err) + } + } + return nil +} + +// cleanupContainer unregisters a container from the daemon, stops stats +// collection and cleanly removes contents and metadata from the filesystem. +func (daemon *Daemon) cleanupContainer(container *container.Container, forceRemove bool) (err error) { + if container.IsRunning() { + if !forceRemove { + err := fmt.Errorf("You cannot remove a running container %s. Stop the container before attempting removal or use -f", container.ID) + return errors.NewRequestConflictError(err) + } + if err := daemon.Kill(container); err != nil { + return fmt.Errorf("Could not kill running container %s, cannot remove - %v", container.ID, err) + } + } + + // stop collection of stats for the container regardless + // if stats are currently getting collected. + daemon.statsCollector.stopCollection(container) + + if err = daemon.containerStop(container, 3); err != nil { + return err + } + + // Mark container dead. We don't want anybody to be restarting it. + container.SetDead() + + // Save container state to disk. So that if error happens before + // container meta file got removed from disk, then a restart of + // docker should not make a dead container alive. + if err := container.ToDiskLocking(); err != nil && !os.IsNotExist(err) { + logrus.Errorf("Error saving dying container to disk: %v", err) + } + + // If force removal is required, delete container from various + // indexes even if removal failed. + defer func() { + if err == nil || forceRemove { + daemon.nameIndex.Delete(container.ID) + daemon.linkIndex.delete(container) + selinuxFreeLxcContexts(container.ProcessLabel) + daemon.idIndex.Delete(container.ID) + daemon.containers.Delete(container.ID) + daemon.LogContainerEvent(container, "destroy") + } + }() + + if err = os.RemoveAll(container.Root); err != nil { + return fmt.Errorf("Unable to remove filesystem for %v: %v", container.ID, err) + } + + // When container creation fails and `RWLayer` has not been created yet, we + // do not call `ReleaseRWLayer` + if container.RWLayer != nil { + metadata, err := daemon.layerStore.ReleaseRWLayer(container.RWLayer) + layer.LogReleaseMetadata(metadata) + if err != nil && err != layer.ErrMountDoesNotExist { + return fmt.Errorf("Driver %s failed to remove root filesystem %s: %s", daemon.GraphDriverName(), container.ID, err) + } + } + + return nil +} + +// VolumeRm removes the volume with the given name. +// If the volume is referenced by a container it is not removed +// This is called directly from the remote API +func (daemon *Daemon) VolumeRm(name string) error { + v, err := daemon.volumes.Get(name) + if err != nil { + return err + } + + if err := daemon.volumes.Remove(v); err != nil { + if volumestore.IsInUse(err) { + err := fmt.Errorf("Unable to remove volume, volume still in use: %v", err) + return errors.NewRequestConflictError(err) + } + return fmt.Errorf("Error while removing volume %s: %v", name, err) + } + daemon.LogVolumeEvent(v.Name(), "destroy", map[string]string{"driver": v.DriverName()}) + return nil +} diff --git a/daemon/delete_test.go b/daemon/delete_test.go new file mode 100644 index 00000000..adce2eb8 --- /dev/null +++ b/daemon/delete_test.go @@ -0,0 +1,42 @@ +package daemon + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/docker/docker/container" + "github.com/docker/engine-api/types" + containertypes "github.com/docker/engine-api/types/container" +) + +func TestContainerDoubleDelete(t *testing.T) { + tmp, err := ioutil.TempDir("", "docker-daemon-unix-test-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + daemon := &Daemon{ + repository: tmp, + root: tmp, + } + daemon.containers = container.NewMemoryStore() + + container := &container.Container{ + CommonContainer: container.CommonContainer{ + ID: "test", + State: container.NewState(), + Config: &containertypes.Config{}, + }, + } + daemon.containers.Add(container.ID, container) + + // Mark the container as having a delete in progress + container.SetRemovalInProgress() + + // Try to remove the container when it's start is removalInProgress. + // It should ignore the container and not return an error. + if err := daemon.ContainerRm(container.ID, &types.ContainerRmConfig{ForceRemove: true}); err != nil { + t.Fatal(err) + } +} diff --git a/daemon/discovery.go b/daemon/discovery.go new file mode 100644 index 00000000..30d2e02a --- /dev/null +++ b/daemon/discovery.go @@ -0,0 +1,203 @@ +package daemon + +import ( + "errors" + "fmt" + "reflect" + "strconv" + "time" + + log "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/discovery" + + // Register the libkv backends for discovery. + _ "github.com/docker/docker/pkg/discovery/kv" +) + +const ( + // defaultDiscoveryHeartbeat is the default value for discovery heartbeat interval. + defaultDiscoveryHeartbeat = 20 * time.Second + // defaultDiscoveryTTLFactor is the default TTL factor for discovery + defaultDiscoveryTTLFactor = 3 +) + +var errDiscoveryDisabled = errors.New("discovery is disabled") + +type discoveryReloader interface { + discovery.Watcher + Stop() + Reload(backend, address string, clusterOpts map[string]string) error + ReadyCh() <-chan struct{} +} + +type daemonDiscoveryReloader struct { + backend discovery.Backend + ticker *time.Ticker + term chan bool + readyCh chan struct{} +} + +func (d *daemonDiscoveryReloader) Watch(stopCh <-chan struct{}) (<-chan discovery.Entries, <-chan error) { + return d.backend.Watch(stopCh) +} + +func (d *daemonDiscoveryReloader) ReadyCh() <-chan struct{} { + return d.readyCh +} + +func discoveryOpts(clusterOpts map[string]string) (time.Duration, time.Duration, error) { + var ( + heartbeat = defaultDiscoveryHeartbeat + ttl = defaultDiscoveryTTLFactor * defaultDiscoveryHeartbeat + ) + + if hb, ok := clusterOpts["discovery.heartbeat"]; ok { + h, err := strconv.Atoi(hb) + if err != nil { + return time.Duration(0), time.Duration(0), err + } + heartbeat = time.Duration(h) * time.Second + ttl = defaultDiscoveryTTLFactor * heartbeat + } + + if tstr, ok := clusterOpts["discovery.ttl"]; ok { + t, err := strconv.Atoi(tstr) + if err != nil { + return time.Duration(0), time.Duration(0), err + } + ttl = time.Duration(t) * time.Second + + if _, ok := clusterOpts["discovery.heartbeat"]; !ok { + h := int(t / defaultDiscoveryTTLFactor) + heartbeat = time.Duration(h) * time.Second + } + + if ttl <= heartbeat { + return time.Duration(0), time.Duration(0), + fmt.Errorf("discovery.ttl timer must be greater than discovery.heartbeat") + } + } + + return heartbeat, ttl, nil +} + +// initDiscovery initializes the nodes discovery subsystem by connecting to the specified backend +// and starts a registration loop to advertise the current node under the specified address. +func initDiscovery(backendAddress, advertiseAddress string, clusterOpts map[string]string) (discoveryReloader, error) { + heartbeat, backend, err := parseDiscoveryOptions(backendAddress, clusterOpts) + if err != nil { + return nil, err + } + + reloader := &daemonDiscoveryReloader{ + backend: backend, + ticker: time.NewTicker(heartbeat), + term: make(chan bool), + readyCh: make(chan struct{}), + } + // We call Register() on the discovery backend in a loop for the whole lifetime of the daemon, + // but we never actually Watch() for nodes appearing and disappearing for the moment. + go reloader.advertiseHeartbeat(advertiseAddress) + return reloader, nil +} + +// advertiseHeartbeat registers the current node against the discovery backend using the specified +// address. The function never returns, as registration against the backend comes with a TTL and +// requires regular heartbeats. +func (d *daemonDiscoveryReloader) advertiseHeartbeat(address string) { + var ready bool + if err := d.initHeartbeat(address); err == nil { + ready = true + close(d.readyCh) + } + + for { + select { + case <-d.ticker.C: + if err := d.backend.Register(address); err != nil { + log.Warnf("Registering as %q in discovery failed: %v", address, err) + } else { + if !ready { + close(d.readyCh) + ready = true + } + } + case <-d.term: + return + } + } +} + +// initHeartbeat is used to do the first heartbeat. It uses a tight loop until +// either the timeout period is reached or the heartbeat is successful and returns. +func (d *daemonDiscoveryReloader) initHeartbeat(address string) error { + // Setup a short ticker until the first heartbeat has succeeded + t := time.NewTicker(500 * time.Millisecond) + defer t.Stop() + // timeout makes sure that after a period of time we stop being so aggressive trying to reach the discovery service + timeout := time.After(60 * time.Second) + + for { + select { + case <-timeout: + return errors.New("timeout waiting for initial discovery") + case <-d.term: + return errors.New("terminated") + case <-t.C: + if err := d.backend.Register(address); err == nil { + return nil + } + } + } +} + +// Reload makes the watcher to stop advertising and reconfigures it to advertise in a new address. +func (d *daemonDiscoveryReloader) Reload(backendAddress, advertiseAddress string, clusterOpts map[string]string) error { + d.Stop() + + heartbeat, backend, err := parseDiscoveryOptions(backendAddress, clusterOpts) + if err != nil { + return err + } + + d.backend = backend + d.ticker = time.NewTicker(heartbeat) + d.readyCh = make(chan struct{}) + + go d.advertiseHeartbeat(advertiseAddress) + return nil +} + +// Stop terminates the discovery advertising. +func (d *daemonDiscoveryReloader) Stop() { + d.ticker.Stop() + d.term <- true +} + +func parseDiscoveryOptions(backendAddress string, clusterOpts map[string]string) (time.Duration, discovery.Backend, error) { + heartbeat, ttl, err := discoveryOpts(clusterOpts) + if err != nil { + return 0, nil, err + } + + backend, err := discovery.New(backendAddress, heartbeat, ttl, clusterOpts) + if err != nil { + return 0, nil, err + } + return heartbeat, backend, nil +} + +// modifiedDiscoverySettings returns whether the discovery configuration has been modified or not. +func modifiedDiscoverySettings(config *Config, backendType, advertise string, clusterOpts map[string]string) bool { + if config.ClusterStore != backendType || config.ClusterAdvertise != advertise { + return true + } + + if (config.ClusterOpts == nil && clusterOpts == nil) || + (config.ClusterOpts == nil && len(clusterOpts) == 0) || + (len(config.ClusterOpts) == 0 && clusterOpts == nil) { + return false + } + + return !reflect.DeepEqual(config.ClusterOpts, clusterOpts) +} diff --git a/daemon/discovery_test.go b/daemon/discovery_test.go new file mode 100644 index 00000000..1764af1e --- /dev/null +++ b/daemon/discovery_test.go @@ -0,0 +1,152 @@ +package daemon + +import ( + "testing" + "time" +) + +func TestDiscoveryOpts(t *testing.T) { + clusterOpts := map[string]string{"discovery.heartbeat": "10", "discovery.ttl": "5"} + heartbeat, ttl, err := discoveryOpts(clusterOpts) + if err == nil { + t.Fatalf("discovery.ttl < discovery.heartbeat must fail") + } + + clusterOpts = map[string]string{"discovery.heartbeat": "10", "discovery.ttl": "10"} + heartbeat, ttl, err = discoveryOpts(clusterOpts) + if err == nil { + t.Fatalf("discovery.ttl == discovery.heartbeat must fail") + } + + clusterOpts = map[string]string{"discovery.heartbeat": "invalid"} + heartbeat, ttl, err = discoveryOpts(clusterOpts) + if err == nil { + t.Fatalf("invalid discovery.heartbeat must fail") + } + + clusterOpts = map[string]string{"discovery.ttl": "invalid"} + heartbeat, ttl, err = discoveryOpts(clusterOpts) + if err == nil { + t.Fatalf("invalid discovery.ttl must fail") + } + + clusterOpts = map[string]string{"discovery.heartbeat": "10", "discovery.ttl": "20"} + heartbeat, ttl, err = discoveryOpts(clusterOpts) + if err != nil { + t.Fatal(err) + } + + if heartbeat != 10*time.Second { + t.Fatalf("Heatbeat - Expected : %v, Actual : %v", 10*time.Second, heartbeat) + } + + if ttl != 20*time.Second { + t.Fatalf("TTL - Expected : %v, Actual : %v", 20*time.Second, ttl) + } + + clusterOpts = map[string]string{"discovery.heartbeat": "10"} + heartbeat, ttl, err = discoveryOpts(clusterOpts) + if err != nil { + t.Fatal(err) + } + + if heartbeat != 10*time.Second { + t.Fatalf("Heatbeat - Expected : %v, Actual : %v", 10*time.Second, heartbeat) + } + + expected := 10 * defaultDiscoveryTTLFactor * time.Second + if ttl != expected { + t.Fatalf("TTL - Expected : %v, Actual : %v", expected, ttl) + } + + clusterOpts = map[string]string{"discovery.ttl": "30"} + heartbeat, ttl, err = discoveryOpts(clusterOpts) + if err != nil { + t.Fatal(err) + } + + if ttl != 30*time.Second { + t.Fatalf("TTL - Expected : %v, Actual : %v", 30*time.Second, ttl) + } + + expected = 30 * time.Second / defaultDiscoveryTTLFactor + if heartbeat != expected { + t.Fatalf("Heatbeat - Expected : %v, Actual : %v", expected, heartbeat) + } + + clusterOpts = map[string]string{} + heartbeat, ttl, err = discoveryOpts(clusterOpts) + if err != nil { + t.Fatal(err) + } + + if heartbeat != defaultDiscoveryHeartbeat { + t.Fatalf("Heatbeat - Expected : %v, Actual : %v", defaultDiscoveryHeartbeat, heartbeat) + } + + expected = defaultDiscoveryHeartbeat * defaultDiscoveryTTLFactor + if ttl != expected { + t.Fatalf("TTL - Expected : %v, Actual : %v", expected, ttl) + } +} + +func TestModifiedDiscoverySettings(t *testing.T) { + cases := []struct { + current *Config + modified *Config + expected bool + }{ + { + current: discoveryConfig("foo", "bar", map[string]string{}), + modified: discoveryConfig("foo", "bar", map[string]string{}), + expected: false, + }, + { + current: discoveryConfig("foo", "bar", map[string]string{"foo": "bar"}), + modified: discoveryConfig("foo", "bar", map[string]string{"foo": "bar"}), + expected: false, + }, + { + current: discoveryConfig("foo", "bar", map[string]string{}), + modified: discoveryConfig("foo", "bar", nil), + expected: false, + }, + { + current: discoveryConfig("foo", "bar", nil), + modified: discoveryConfig("foo", "bar", map[string]string{}), + expected: false, + }, + { + current: discoveryConfig("foo", "bar", nil), + modified: discoveryConfig("baz", "bar", nil), + expected: true, + }, + { + current: discoveryConfig("foo", "bar", nil), + modified: discoveryConfig("foo", "baz", nil), + expected: true, + }, + { + current: discoveryConfig("foo", "bar", nil), + modified: discoveryConfig("foo", "bar", map[string]string{"foo": "bar"}), + expected: true, + }, + } + + for _, c := range cases { + got := modifiedDiscoverySettings(c.current, c.modified.ClusterStore, c.modified.ClusterAdvertise, c.modified.ClusterOpts) + if c.expected != got { + t.Fatalf("expected %v, got %v: current config %v, new config %v", c.expected, got, c.current, c.modified) + } + } +} + +func discoveryConfig(backendAddr, advertiseAddr string, opts map[string]string) *Config { + return &Config{ + CommonConfig: CommonConfig{ + ClusterStore: backendAddr, + ClusterAdvertise: advertiseAddr, + ClusterOpts: opts, + }, + } +} diff --git a/daemon/errors.go b/daemon/errors.go new file mode 100644 index 00000000..131c9a1e --- /dev/null +++ b/daemon/errors.go @@ -0,0 +1,57 @@ +package daemon + +import ( + "fmt" + "strings" + + "github.com/docker/docker/errors" + "github.com/docker/docker/reference" +) + +func (d *Daemon) imageNotExistToErrcode(err error) error { + if dne, isDNE := err.(ErrImageDoesNotExist); isDNE { + if strings.Contains(dne.RefOrID, "@") { + e := fmt.Errorf("No such image: %s", dne.RefOrID) + return errors.NewRequestNotFoundError(e) + } + tag := reference.DefaultTag + ref, err := reference.ParseNamed(dne.RefOrID) + if err != nil { + e := fmt.Errorf("No such image: %s:%s", dne.RefOrID, tag) + return errors.NewRequestNotFoundError(e) + } + if tagged, isTagged := ref.(reference.NamedTagged); isTagged { + tag = tagged.Tag() + } + e := fmt.Errorf("No such image: %s:%s", ref.Name(), tag) + return errors.NewRequestNotFoundError(e) + } + return err +} + +type errNotRunning struct { + containerID string +} + +func (e errNotRunning) Error() string { + return fmt.Sprintf("Container %s is not running", e.containerID) +} + +func (e errNotRunning) ContainerIsRunning() bool { + return false +} + +func errContainerIsRestarting(containerID string) error { + err := fmt.Errorf("Container %s is restarting, wait until the container is running", containerID) + return errors.NewRequestConflictError(err) +} + +func errExecNotFound(id string) error { + err := fmt.Errorf("No such exec instance '%s' found in daemon", id) + return errors.NewRequestNotFoundError(err) +} + +func errExecPaused(id string) error { + err := fmt.Errorf("Container %s is paused, unpause the container before exec", id) + return errors.NewRequestConflictError(err) +} diff --git a/daemon/events.go b/daemon/events.go new file mode 100644 index 00000000..0ce0eaf0 --- /dev/null +++ b/daemon/events.go @@ -0,0 +1,88 @@ +package daemon + +import ( + "strings" + + "github.com/docker/docker/container" + "github.com/docker/engine-api/types/events" + "github.com/docker/libnetwork" +) + +// LogContainerEvent generates an event related to a container with only the default attributes. +func (daemon *Daemon) LogContainerEvent(container *container.Container, action string) { + daemon.LogContainerEventWithAttributes(container, action, map[string]string{}) +} + +// LogContainerEventWithAttributes generates an event related to a container with specific given attributes. +func (daemon *Daemon) LogContainerEventWithAttributes(container *container.Container, action string, attributes map[string]string) { + copyAttributes(attributes, container.Config.Labels) + if container.Config.Image != "" { + attributes["image"] = container.Config.Image + } + attributes["name"] = strings.TrimLeft(container.Name, "/") + + actor := events.Actor{ + ID: container.ID, + Attributes: attributes, + } + daemon.EventsService.Log(action, events.ContainerEventType, actor) +} + +// LogImageEvent generates an event related to a container with only the default attributes. +func (daemon *Daemon) LogImageEvent(imageID, refName, action string) { + daemon.LogImageEventWithAttributes(imageID, refName, action, map[string]string{}) +} + +// LogImageEventWithAttributes generates an event related to a container with specific given attributes. +func (daemon *Daemon) LogImageEventWithAttributes(imageID, refName, action string, attributes map[string]string) { + img, err := daemon.GetImage(imageID) + if err == nil && img.Config != nil { + // image has not been removed yet. + // it could be missing if the event is `delete`. + copyAttributes(attributes, img.Config.Labels) + } + if refName != "" { + attributes["name"] = refName + } + actor := events.Actor{ + ID: imageID, + Attributes: attributes, + } + + daemon.EventsService.Log(action, events.ImageEventType, actor) +} + +// LogVolumeEvent generates an event related to a volume. +func (daemon *Daemon) LogVolumeEvent(volumeID, action string, attributes map[string]string) { + actor := events.Actor{ + ID: volumeID, + Attributes: attributes, + } + daemon.EventsService.Log(action, events.VolumeEventType, actor) +} + +// LogNetworkEvent generates an event related to a network with only the default attributes. +func (daemon *Daemon) LogNetworkEvent(nw libnetwork.Network, action string) { + daemon.LogNetworkEventWithAttributes(nw, action, map[string]string{}) +} + +// LogNetworkEventWithAttributes generates an event related to a network with specific given attributes. +func (daemon *Daemon) LogNetworkEventWithAttributes(nw libnetwork.Network, action string, attributes map[string]string) { + attributes["name"] = nw.Name() + attributes["type"] = nw.Type() + actor := events.Actor{ + ID: nw.ID(), + Attributes: attributes, + } + daemon.EventsService.Log(action, events.NetworkEventType, actor) +} + +// copyAttributes guarantees that labels are not mutated by event triggers. +func copyAttributes(attributes, labels map[string]string) { + if labels == nil { + return + } + for k, v := range labels { + attributes[k] = v + } +} diff --git a/daemon/events/events.go b/daemon/events/events.go new file mode 100644 index 00000000..ac1c98cd --- /dev/null +++ b/daemon/events/events.go @@ -0,0 +1,142 @@ +package events + +import ( + "sync" + "time" + + "github.com/docker/docker/pkg/pubsub" + eventtypes "github.com/docker/engine-api/types/events" +) + +const ( + eventsLimit = 64 + bufferSize = 1024 +) + +// Events is pubsub channel for events generated by the engine. +type Events struct { + mu sync.Mutex + events []eventtypes.Message + pub *pubsub.Publisher +} + +// New returns new *Events instance +func New() *Events { + return &Events{ + events: make([]eventtypes.Message, 0, eventsLimit), + pub: pubsub.NewPublisher(100*time.Millisecond, bufferSize), + } +} + +// Subscribe adds new listener to events, returns slice of 64 stored +// last events, a channel in which you can expect new events (in form +// of interface{}, so you need type assertion), and a function to call +// to stop the stream of events. +func (e *Events) Subscribe() ([]eventtypes.Message, chan interface{}, func()) { + e.mu.Lock() + current := make([]eventtypes.Message, len(e.events)) + copy(current, e.events) + l := e.pub.Subscribe() + e.mu.Unlock() + + cancel := func() { + e.Evict(l) + } + return current, l, cancel +} + +// SubscribeTopic adds new listener to events, returns slice of 64 stored +// last events, a channel in which you can expect new events (in form +// of interface{}, so you need type assertion). +func (e *Events) SubscribeTopic(since, sinceNano int64, ef *Filter) ([]eventtypes.Message, chan interface{}) { + e.mu.Lock() + + var topic func(m interface{}) bool + if ef != nil && ef.filter.Len() > 0 { + topic = func(m interface{}) bool { return ef.Include(m.(eventtypes.Message)) } + } + + buffered := e.loadBufferedEvents(since, sinceNano, topic) + + var ch chan interface{} + if topic != nil { + ch = e.pub.SubscribeTopic(topic) + } else { + // Subscribe to all events if there are no filters + ch = e.pub.Subscribe() + } + + e.mu.Unlock() + return buffered, ch +} + +// Evict evicts listener from pubsub +func (e *Events) Evict(l chan interface{}) { + e.pub.Evict(l) +} + +// Log broadcasts event to listeners. Each listener has 100 millisecond for +// receiving event or it will be skipped. +func (e *Events) Log(action, eventType string, actor eventtypes.Actor) { + now := time.Now().UTC() + jm := eventtypes.Message{ + Action: action, + Type: eventType, + Actor: actor, + Time: now.Unix(), + TimeNano: now.UnixNano(), + } + + // fill deprecated fields for container and images + switch eventType { + case eventtypes.ContainerEventType: + jm.ID = actor.ID + jm.Status = action + jm.From = actor.Attributes["image"] + case eventtypes.ImageEventType: + jm.ID = actor.ID + jm.Status = action + } + + e.mu.Lock() + if len(e.events) == cap(e.events) { + // discard oldest event + copy(e.events, e.events[1:]) + e.events[len(e.events)-1] = jm + } else { + e.events = append(e.events, jm) + } + e.mu.Unlock() + e.pub.Publish(jm) +} + +// SubscribersCount returns number of event listeners +func (e *Events) SubscribersCount() int { + return e.pub.Len() +} + +// loadBufferedEvents iterates over the cached events in the buffer +// and returns those that were emitted before a specific date. +// The date is splitted in two values: +// - the `since` argument is a date timestamp without nanoseconds, or -1 to return an empty slice. +// - the `sinceNano` argument is the nanoseconds offset from the timestamp. +// It uses `time.Unix(seconds, nanoseconds)` to generate a valid date with those two first arguments. +// It filters those buffered messages with a topic function if it's not nil, otherwise it adds all messages. +func (e *Events) loadBufferedEvents(since, sinceNano int64, topic func(interface{}) bool) []eventtypes.Message { + var buffered []eventtypes.Message + if since == -1 { + return buffered + } + + sinceNanoUnix := time.Unix(since, sinceNano).UnixNano() + for i := len(e.events) - 1; i >= 0; i-- { + ev := e.events[i] + if ev.TimeNano < sinceNanoUnix { + break + } + if topic == nil || topic(ev) { + buffered = append([]eventtypes.Message{ev}, buffered...) + } + } + return buffered +} diff --git a/daemon/events/events_test.go b/daemon/events/events_test.go new file mode 100644 index 00000000..5fd577b9 --- /dev/null +++ b/daemon/events/events_test.go @@ -0,0 +1,196 @@ +package events + +import ( + "fmt" + "testing" + "time" + + "github.com/docker/docker/daemon/events/testutils" + "github.com/docker/engine-api/types/events" + timetypes "github.com/docker/engine-api/types/time" +) + +func TestEventsLog(t *testing.T) { + e := New() + _, l1, _ := e.Subscribe() + _, l2, _ := e.Subscribe() + defer e.Evict(l1) + defer e.Evict(l2) + count := e.SubscribersCount() + if count != 2 { + t.Fatalf("Must be 2 subscribers, got %d", count) + } + actor := events.Actor{ + ID: "cont", + Attributes: map[string]string{"image": "image"}, + } + e.Log("test", events.ContainerEventType, actor) + select { + case msg := <-l1: + jmsg, ok := msg.(events.Message) + if !ok { + t.Fatalf("Unexpected type %T", msg) + } + if len(e.events) != 1 { + t.Fatalf("Must be only one event, got %d", len(e.events)) + } + if jmsg.Status != "test" { + t.Fatalf("Status should be test, got %s", jmsg.Status) + } + if jmsg.ID != "cont" { + t.Fatalf("ID should be cont, got %s", jmsg.ID) + } + if jmsg.From != "image" { + t.Fatalf("From should be image, got %s", jmsg.From) + } + case <-time.After(1 * time.Second): + t.Fatal("Timeout waiting for broadcasted message") + } + select { + case msg := <-l2: + jmsg, ok := msg.(events.Message) + if !ok { + t.Fatalf("Unexpected type %T", msg) + } + if len(e.events) != 1 { + t.Fatalf("Must be only one event, got %d", len(e.events)) + } + if jmsg.Status != "test" { + t.Fatalf("Status should be test, got %s", jmsg.Status) + } + if jmsg.ID != "cont" { + t.Fatalf("ID should be cont, got %s", jmsg.ID) + } + if jmsg.From != "image" { + t.Fatalf("From should be image, got %s", jmsg.From) + } + case <-time.After(1 * time.Second): + t.Fatal("Timeout waiting for broadcasted message") + } +} + +func TestEventsLogTimeout(t *testing.T) { + e := New() + _, l, _ := e.Subscribe() + defer e.Evict(l) + + c := make(chan struct{}) + go func() { + actor := events.Actor{ + ID: "image", + } + e.Log("test", events.ImageEventType, actor) + close(c) + }() + + select { + case <-c: + case <-time.After(time.Second): + t.Fatal("Timeout publishing message") + } +} + +func TestLogEvents(t *testing.T) { + e := New() + + for i := 0; i < eventsLimit+16; i++ { + action := fmt.Sprintf("action_%d", i) + id := fmt.Sprintf("cont_%d", i) + from := fmt.Sprintf("image_%d", i) + + actor := events.Actor{ + ID: id, + Attributes: map[string]string{"image": from}, + } + e.Log(action, events.ContainerEventType, actor) + } + time.Sleep(50 * time.Millisecond) + current, l, _ := e.Subscribe() + for i := 0; i < 10; i++ { + num := i + eventsLimit + 16 + action := fmt.Sprintf("action_%d", num) + id := fmt.Sprintf("cont_%d", num) + from := fmt.Sprintf("image_%d", num) + + actor := events.Actor{ + ID: id, + Attributes: map[string]string{"image": from}, + } + e.Log(action, events.ContainerEventType, actor) + } + if len(e.events) != eventsLimit { + t.Fatalf("Must be %d events, got %d", eventsLimit, len(e.events)) + } + + var msgs []events.Message + for len(msgs) < 10 { + m := <-l + jm, ok := (m).(events.Message) + if !ok { + t.Fatalf("Unexpected type %T", m) + } + msgs = append(msgs, jm) + } + if len(current) != eventsLimit { + t.Fatalf("Must be %d events, got %d", eventsLimit, len(current)) + } + first := current[0] + if first.Status != "action_16" { + t.Fatalf("First action is %s, must be action_16", first.Status) + } + last := current[len(current)-1] + if last.Status != "action_79" { + t.Fatalf("Last action is %s, must be action_79", last.Status) + } + + firstC := msgs[0] + if firstC.Status != "action_80" { + t.Fatalf("First action is %s, must be action_80", firstC.Status) + } + lastC := msgs[len(msgs)-1] + if lastC.Status != "action_89" { + t.Fatalf("Last action is %s, must be action_89", lastC.Status) + } +} + +// https://github.com/docker/docker/issues/20999 +// Fixtures: +// +//2016-03-07T17:28:03.022433271+02:00 container die 0b863f2a26c18557fc6cdadda007c459f9ec81b874780808138aea78a3595079 (image=ubuntu, name=small_hoover) +//2016-03-07T17:28:03.091719377+02:00 network disconnect 19c5ed41acb798f26b751e0035cd7821741ab79e2bbd59a66b5fd8abf954eaa0 (type=bridge, container=0b863f2a26c18557fc6cdadda007c459f9ec81b874780808138aea78a3595079, name=bridge) +//2016-03-07T17:28:03.129014751+02:00 container destroy 0b863f2a26c18557fc6cdadda007c459f9ec81b874780808138aea78a3595079 (image=ubuntu, name=small_hoover) +func TestLoadBufferedEvents(t *testing.T) { + now := time.Now() + f, err := timetypes.GetTimestamp("2016-03-07T17:28:03.100000000+02:00", now) + if err != nil { + t.Fatal(err) + } + since, sinceNano, err := timetypes.ParseTimestamps(f, -1) + if err != nil { + t.Fatal(err) + } + + m1, err := eventstestutils.Scan("2016-03-07T17:28:03.022433271+02:00 container die 0b863f2a26c18557fc6cdadda007c459f9ec81b874780808138aea78a3595079 (image=ubuntu, name=small_hoover)") + if err != nil { + t.Fatal(err) + } + m2, err := eventstestutils.Scan("2016-03-07T17:28:03.091719377+02:00 network disconnect 19c5ed41acb798f26b751e0035cd7821741ab79e2bbd59a66b5fd8abf954eaa0 (type=bridge, container=0b863f2a26c18557fc6cdadda007c459f9ec81b874780808138aea78a3595079, name=bridge)") + if err != nil { + t.Fatal(err) + } + m3, err := eventstestutils.Scan("2016-03-07T17:28:03.129014751+02:00 container destroy 0b863f2a26c18557fc6cdadda007c459f9ec81b874780808138aea78a3595079 (image=ubuntu, name=small_hoover)") + if err != nil { + t.Fatal(err) + } + + buffered := []events.Message{*m1, *m2, *m3} + + events := &Events{ + events: buffered, + } + + out := events.loadBufferedEvents(since, sinceNano, nil) + if len(out) != 1 { + t.Fatalf("expected 1 message, got %d: %v", len(out), out) + } +} diff --git a/daemon/events/filter.go b/daemon/events/filter.go new file mode 100644 index 00000000..8936e371 --- /dev/null +++ b/daemon/events/filter.go @@ -0,0 +1,82 @@ +package events + +import ( + "github.com/docker/docker/reference" + "github.com/docker/engine-api/types/events" + "github.com/docker/engine-api/types/filters" +) + +// Filter can filter out docker events from a stream +type Filter struct { + filter filters.Args +} + +// NewFilter creates a new Filter +func NewFilter(filter filters.Args) *Filter { + return &Filter{filter: filter} +} + +// Include returns true when the event ev is included by the filters +func (ef *Filter) Include(ev events.Message) bool { + return ef.filter.ExactMatch("event", ev.Action) && + ef.filter.ExactMatch("type", ev.Type) && + ef.matchContainer(ev) && + ef.matchVolume(ev) && + ef.matchNetwork(ev) && + ef.matchImage(ev) && + ef.matchLabels(ev.Actor.Attributes) +} + +func (ef *Filter) matchLabels(attributes map[string]string) bool { + if !ef.filter.Include("label") { + return true + } + return ef.filter.MatchKVList("label", attributes) +} + +func (ef *Filter) matchContainer(ev events.Message) bool { + return ef.fuzzyMatchName(ev, events.ContainerEventType) +} + +func (ef *Filter) matchVolume(ev events.Message) bool { + return ef.fuzzyMatchName(ev, events.VolumeEventType) +} + +func (ef *Filter) matchNetwork(ev events.Message) bool { + return ef.fuzzyMatchName(ev, events.NetworkEventType) +} + +func (ef *Filter) fuzzyMatchName(ev events.Message, eventType string) bool { + return ef.filter.FuzzyMatch(eventType, ev.Actor.ID) || + ef.filter.FuzzyMatch(eventType, ev.Actor.Attributes["name"]) +} + +// matchImage matches against both event.Actor.ID (for image events) +// and event.Actor.Attributes["image"] (for container events), so that any container that was created +// from an image will be included in the image events. Also compare both +// against the stripped repo name without any tags. +func (ef *Filter) matchImage(ev events.Message) bool { + id := ev.Actor.ID + nameAttr := "image" + var imageName string + + if ev.Type == events.ImageEventType { + nameAttr = "name" + } + + if n, ok := ev.Actor.Attributes[nameAttr]; ok { + imageName = n + } + return ef.filter.ExactMatch("image", id) || + ef.filter.ExactMatch("image", imageName) || + ef.filter.ExactMatch("image", stripTag(id)) || + ef.filter.ExactMatch("image", stripTag(imageName)) +} + +func stripTag(image string) string { + ref, err := reference.ParseNamed(image) + if err != nil { + return image + } + return ref.Name() +} diff --git a/daemon/events/testutils/testutils.go b/daemon/events/testutils/testutils.go new file mode 100644 index 00000000..c84418a9 --- /dev/null +++ b/daemon/events/testutils/testutils.go @@ -0,0 +1,76 @@ +package eventstestutils + +import ( + "fmt" + "regexp" + "strings" + "time" + + "github.com/docker/engine-api/types/events" + timetypes "github.com/docker/engine-api/types/time" +) + +var ( + reTimestamp = `(?P\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{9}(:?(:?(:?-|\+)\d{2}:\d{2})|Z))` + reEventType = `(?P\w+)` + reAction = `(?P\w+)` + reID = `(?P[^\s]+)` + reAttributes = `(\s\((?P[^\)]+)\))?` + reString = fmt.Sprintf(`\A%s\s%s\s%s\s%s%s\z`, reTimestamp, reEventType, reAction, reID, reAttributes) + + // eventCliRegexp is a regular expression that matches all possible event outputs in the cli + eventCliRegexp = regexp.MustCompile(reString) +) + +// ScanMap turns an event string like the default ones formatted in the cli output +// and turns it into map. +func ScanMap(text string) map[string]string { + matches := eventCliRegexp.FindAllStringSubmatch(text, -1) + md := map[string]string{} + if len(matches) == 0 { + return md + } + + names := eventCliRegexp.SubexpNames() + for i, n := range matches[0] { + md[names[i]] = n + } + return md +} + +// Scan turns an event string like the default ones formatted in the cli output +// and turns it into an event message. +func Scan(text string) (*events.Message, error) { + md := ScanMap(text) + if len(md) == 0 { + return nil, fmt.Errorf("text is not an event: %s", text) + } + + f, err := timetypes.GetTimestamp(md["timestamp"], time.Now()) + if err != nil { + return nil, err + } + + t, tn, err := timetypes.ParseTimestamps(f, -1) + if err != nil { + return nil, err + } + + attrs := make(map[string]string) + for _, a := range strings.SplitN(md["attributes"], ", ", -1) { + kv := strings.SplitN(a, "=", 2) + attrs[kv[0]] = kv[1] + } + + tu := time.Unix(t, tn) + return &events.Message{ + Time: t, + TimeNano: tu.UnixNano(), + Type: md["eventType"], + Action: md["action"], + Actor: events.Actor{ + ID: md["id"], + Attributes: attrs, + }, + }, nil +} diff --git a/daemon/events_test.go b/daemon/events_test.go new file mode 100644 index 00000000..8ee14a31 --- /dev/null +++ b/daemon/events_test.go @@ -0,0 +1,94 @@ +package daemon + +import ( + "testing" + "time" + + "github.com/docker/docker/container" + "github.com/docker/docker/daemon/events" + containertypes "github.com/docker/engine-api/types/container" + eventtypes "github.com/docker/engine-api/types/events" +) + +func TestLogContainerEventCopyLabels(t *testing.T) { + e := events.New() + _, l, _ := e.Subscribe() + defer e.Evict(l) + + container := &container.Container{ + CommonContainer: container.CommonContainer{ + ID: "container_id", + Name: "container_name", + Config: &containertypes.Config{ + Image: "image_name", + Labels: map[string]string{ + "node": "1", + "os": "alpine", + }, + }, + }, + } + daemon := &Daemon{ + EventsService: e, + } + daemon.LogContainerEvent(container, "create") + + if _, mutated := container.Config.Labels["image"]; mutated { + t.Fatalf("Expected to not mutate the container labels, got %q", container.Config.Labels) + } + + validateTestAttributes(t, l, map[string]string{ + "node": "1", + "os": "alpine", + }) +} + +func TestLogContainerEventWithAttributes(t *testing.T) { + e := events.New() + _, l, _ := e.Subscribe() + defer e.Evict(l) + + container := &container.Container{ + CommonContainer: container.CommonContainer{ + ID: "container_id", + Name: "container_name", + Config: &containertypes.Config{ + Labels: map[string]string{ + "node": "1", + "os": "alpine", + }, + }, + }, + } + daemon := &Daemon{ + EventsService: e, + } + attributes := map[string]string{ + "node": "2", + "foo": "bar", + } + daemon.LogContainerEventWithAttributes(container, "create", attributes) + + validateTestAttributes(t, l, map[string]string{ + "node": "1", + "foo": "bar", + }) +} + +func validateTestAttributes(t *testing.T, l chan interface{}, expectedAttributesToTest map[string]string) { + select { + case ev := <-l: + event, ok := ev.(eventtypes.Message) + if !ok { + t.Fatalf("Unexpected event message: %q", ev) + } + for key, expected := range expectedAttributesToTest { + actual, ok := event.Actor.Attributes[key] + if !ok || actual != expected { + t.Fatalf("Expected value for key %s to be %s, but was %s (event:%v)", key, expected, actual, event) + } + } + case <-time.After(10 * time.Second): + t.Fatalf("LogEvent test timed out") + } +} diff --git a/daemon/exec.go b/daemon/exec.go new file mode 100644 index 00000000..be06845c --- /dev/null +++ b/daemon/exec.go @@ -0,0 +1,246 @@ +package daemon + +import ( + "fmt" + "io" + "strings" + "time" + + "golang.org/x/net/context" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/container" + "github.com/docker/docker/daemon/exec" + "github.com/docker/docker/errors" + "github.com/docker/docker/libcontainerd" + "github.com/docker/docker/pkg/pools" + "github.com/docker/docker/pkg/term" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/strslice" +) + +func (d *Daemon) registerExecCommand(container *container.Container, config *exec.Config) { + // Storing execs in container in order to kill them gracefully whenever the container is stopped or removed. + container.ExecCommands.Add(config.ID, config) + // Storing execs in daemon for easy access via remote API. + d.execCommands.Add(config.ID, config) +} + +// ExecExists looks up the exec instance and returns a bool if it exists or not. +// It will also return the error produced by `getConfig` +func (d *Daemon) ExecExists(name string) (bool, error) { + if _, err := d.getExecConfig(name); err != nil { + return false, err + } + return true, nil +} + +// getExecConfig looks up the exec instance by name. If the container associated +// with the exec instance is stopped or paused, it will return an error. +func (d *Daemon) getExecConfig(name string) (*exec.Config, error) { + ec := d.execCommands.Get(name) + + // If the exec is found but its container is not in the daemon's list of + // containers then it must have been deleted, in which case instead of + // saying the container isn't running, we should return a 404 so that + // the user sees the same error now that they will after the + // 5 minute clean-up loop is run which erases old/dead execs. + + if ec != nil { + if container := d.containers.Get(ec.ContainerID); container != nil { + if !container.IsRunning() { + return nil, fmt.Errorf("Container %s is not running: %s", container.ID, container.State.String()) + } + if container.IsPaused() { + return nil, errExecPaused(container.ID) + } + if container.IsRestarting() { + return nil, errContainerIsRestarting(container.ID) + } + return ec, nil + } + } + + return nil, errExecNotFound(name) +} + +func (d *Daemon) unregisterExecCommand(container *container.Container, execConfig *exec.Config) { + container.ExecCommands.Delete(execConfig.ID) + d.execCommands.Delete(execConfig.ID) +} + +func (d *Daemon) getActiveContainer(name string) (*container.Container, error) { + container, err := d.GetContainer(name) + if err != nil { + return nil, err + } + + if !container.IsRunning() { + return nil, errNotRunning{container.ID} + } + if container.IsPaused() { + return nil, errExecPaused(name) + } + if container.IsRestarting() { + return nil, errContainerIsRestarting(container.ID) + } + return container, nil +} + +// ContainerExecCreate sets up an exec in a running container. +func (d *Daemon) ContainerExecCreate(config *types.ExecConfig) (string, error) { + container, err := d.getActiveContainer(config.Container) + if err != nil { + return "", err + } + + cmd := strslice.StrSlice(config.Cmd) + entrypoint, args := d.getEntrypointAndArgs(strslice.StrSlice{}, cmd) + + keys := []byte{} + if config.DetachKeys != "" { + keys, err = term.ToBytes(config.DetachKeys) + if err != nil { + logrus.Warnf("Wrong escape keys provided (%s, error: %s) using default : ctrl-p ctrl-q", config.DetachKeys, err.Error()) + } + } + + execConfig := exec.NewConfig() + execConfig.OpenStdin = config.AttachStdin + execConfig.OpenStdout = config.AttachStdout + execConfig.OpenStderr = config.AttachStderr + execConfig.ContainerID = container.ID + execConfig.DetachKeys = keys + execConfig.Entrypoint = entrypoint + execConfig.Args = args + execConfig.Tty = config.Tty + execConfig.Privileged = config.Privileged + execConfig.User = config.User + if len(execConfig.User) == 0 { + execConfig.User = container.Config.User + } + + d.registerExecCommand(container, execConfig) + + d.LogContainerEvent(container, "exec_create: "+execConfig.Entrypoint+" "+strings.Join(execConfig.Args, " ")) + + return execConfig.ID, nil +} + +// ContainerExecStart starts a previously set up exec instance. The +// std streams are set up. +func (d *Daemon) ContainerExecStart(name string, stdin io.ReadCloser, stdout io.Writer, stderr io.Writer) (err error) { + var ( + cStdin io.ReadCloser + cStdout, cStderr io.Writer + ) + + ec, err := d.getExecConfig(name) + if err != nil { + return errExecNotFound(name) + } + + ec.Lock() + if ec.ExitCode != nil { + ec.Unlock() + err := fmt.Errorf("Error: Exec command %s has already run", ec.ID) + return errors.NewRequestConflictError(err) + } + + if ec.Running { + ec.Unlock() + return fmt.Errorf("Error: Exec command %s is already running", ec.ID) + } + ec.Running = true + defer func() { + if err != nil { + ec.Running = false + exitCode := 126 + ec.ExitCode = &exitCode + } + }() + ec.Unlock() + + c := d.containers.Get(ec.ContainerID) + logrus.Debugf("starting exec command %s in container %s", ec.ID, c.ID) + d.LogContainerEvent(c, "exec_start: "+ec.Entrypoint+" "+strings.Join(ec.Args, " ")) + + if ec.OpenStdin && stdin != nil { + r, w := io.Pipe() + go func() { + defer w.Close() + defer logrus.Debugf("Closing buffered stdin pipe") + pools.Copy(w, stdin) + }() + cStdin = r + } + if ec.OpenStdout { + cStdout = stdout + } + if ec.OpenStderr { + cStderr = stderr + } + + if ec.OpenStdin { + ec.NewInputPipes() + } else { + ec.NewNopInputPipe() + } + + p := libcontainerd.Process{ + Args: append([]string{ec.Entrypoint}, ec.Args...), + Terminal: ec.Tty, + } + + if err := execSetPlatformOpt(c, ec, &p); err != nil { + return nil + } + + attachErr := container.AttachStreams(context.Background(), ec.StreamConfig, ec.OpenStdin, true, ec.Tty, cStdin, cStdout, cStderr, ec.DetachKeys) + + if err := d.containerd.AddProcess(c.ID, name, p); err != nil { + return err + } + + err = <-attachErr + if err != nil { + return fmt.Errorf("attach failed with error: %v", err) + } + return nil +} + +// execCommandGC runs a ticker to clean up the daemon references +// of exec configs that are no longer part of the container. +func (d *Daemon) execCommandGC() { + for range time.Tick(5 * time.Minute) { + var ( + cleaned int + liveExecCommands = d.containerExecIds() + ) + for id, config := range d.execCommands.Commands() { + if config.CanRemove { + cleaned++ + d.execCommands.Delete(id) + } else { + if _, exists := liveExecCommands[id]; !exists { + config.CanRemove = true + } + } + } + if cleaned > 0 { + logrus.Debugf("clean %d unused exec commands", cleaned) + } + } +} + +// containerExecIds returns a list of all the current exec ids that are in use +// and running inside a container. +func (d *Daemon) containerExecIds() map[string]struct{} { + ids := map[string]struct{}{} + for _, c := range d.containers.List() { + for _, id := range c.ExecCommands.List() { + ids[id] = struct{}{} + } + } + return ids +} diff --git a/daemon/exec/exec.go b/daemon/exec/exec.go new file mode 100644 index 00000000..bbeb1c16 --- /dev/null +++ b/daemon/exec/exec.go @@ -0,0 +1,93 @@ +package exec + +import ( + "sync" + + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/runconfig" +) + +// Config holds the configurations for execs. The Daemon keeps +// track of both running and finished execs so that they can be +// examined both during and after completion. +type Config struct { + sync.Mutex + *runconfig.StreamConfig + ID string + Running bool + ExitCode *int + OpenStdin bool + OpenStderr bool + OpenStdout bool + CanRemove bool + ContainerID string + DetachKeys []byte + Entrypoint string + Args []string + Tty bool + Privileged bool + User string +} + +// NewConfig initializes the a new exec configuration +func NewConfig() *Config { + return &Config{ + ID: stringid.GenerateNonCryptoID(), + StreamConfig: runconfig.NewStreamConfig(), + } +} + +// Store keeps track of the exec configurations. +type Store struct { + commands map[string]*Config + sync.RWMutex +} + +// NewStore initializes a new exec store. +func NewStore() *Store { + return &Store{commands: make(map[string]*Config, 0)} +} + +// Commands returns the exec configurations in the store. +func (e *Store) Commands() map[string]*Config { + e.RLock() + commands := make(map[string]*Config, len(e.commands)) + for id, config := range e.commands { + commands[id] = config + } + e.RUnlock() + return commands +} + +// Add adds a new exec configuration to the store. +func (e *Store) Add(id string, Config *Config) { + e.Lock() + e.commands[id] = Config + e.Unlock() +} + +// Get returns an exec configuration by its id. +func (e *Store) Get(id string) *Config { + e.RLock() + res := e.commands[id] + e.RUnlock() + return res +} + +// Delete removes an exec configuration from the store. +func (e *Store) Delete(id string) { + e.Lock() + delete(e.commands, id) + e.Unlock() +} + +// List returns the list of exec ids in the store. +func (e *Store) List() []string { + var IDs []string + e.RLock() + for id := range e.commands { + IDs = append(IDs, id) + } + e.RUnlock() + return IDs +} diff --git a/daemon/exec_linux.go b/daemon/exec_linux.go new file mode 100644 index 00000000..a2c86b28 --- /dev/null +++ b/daemon/exec_linux.go @@ -0,0 +1,26 @@ +package daemon + +import ( + "github.com/docker/docker/container" + "github.com/docker/docker/daemon/caps" + "github.com/docker/docker/daemon/exec" + "github.com/docker/docker/libcontainerd" +) + +func execSetPlatformOpt(c *container.Container, ec *exec.Config, p *libcontainerd.Process) error { + if len(ec.User) > 0 { + uid, gid, additionalGids, err := getUser(c, ec.User) + if err != nil { + return err + } + p.User = &libcontainerd.User{ + UID: uid, + GID: gid, + AdditionalGids: additionalGids, + } + } + if ec.Privileged { + p.Capabilities = caps.GetAllCapabilities() + } + return nil +} diff --git a/daemon/exec_windows.go b/daemon/exec_windows.go new file mode 100644 index 00000000..be25d200 --- /dev/null +++ b/daemon/exec_windows.go @@ -0,0 +1,14 @@ +package daemon + +import ( + "github.com/docker/docker/container" + "github.com/docker/docker/daemon/exec" + "github.com/docker/docker/libcontainerd" +) + +func execSetPlatformOpt(c *container.Container, ec *exec.Config, p *libcontainerd.Process) error { + // Process arguments need to be escaped before sending to OCI. + // TODO (jstarks): escape the entrypoint too once the tests are fixed to not rely on this behavior + p.Args = append([]string{p.Args[0]}, escapeArgs(p.Args[1:])...) + return nil +} diff --git a/daemon/export.go b/daemon/export.go new file mode 100644 index 00000000..80d7dbb2 --- /dev/null +++ b/daemon/export.go @@ -0,0 +1,55 @@ +package daemon + +import ( + "fmt" + "io" + + "github.com/docker/docker/container" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/ioutils" +) + +// ContainerExport writes the contents of the container to the given +// writer. An error is returned if the container cannot be found. +func (daemon *Daemon) ContainerExport(name string, out io.Writer) error { + container, err := daemon.GetContainer(name) + if err != nil { + return err + } + + data, err := daemon.containerExport(container) + if err != nil { + return fmt.Errorf("Error exporting container %s: %v", name, err) + } + defer data.Close() + + // Stream the entire contents of the container (basically a volatile snapshot) + if _, err := io.Copy(out, data); err != nil { + return fmt.Errorf("Error exporting container %s: %v", name, err) + } + return nil +} + +func (daemon *Daemon) containerExport(container *container.Container) (archive.Archive, error) { + if err := daemon.Mount(container); err != nil { + return nil, err + } + + uidMaps, gidMaps := daemon.GetUIDGIDMaps() + archive, err := archive.TarWithOptions(container.BaseFS, &archive.TarOptions{ + Compression: archive.Uncompressed, + UIDMaps: uidMaps, + GIDMaps: gidMaps, + }) + if err != nil { + daemon.Unmount(container) + return nil, err + } + arch := ioutils.NewReadCloserWrapper(archive, func() error { + err := archive.Close() + daemon.Unmount(container) + return err + }) + daemon.LogContainerEvent(container, "export") + return arch, err +} diff --git a/daemon/graphdriver/aufs/aufs.go b/daemon/graphdriver/aufs/aufs.go new file mode 100644 index 00000000..c98e8377 --- /dev/null +++ b/daemon/graphdriver/aufs/aufs.go @@ -0,0 +1,564 @@ +// +build linux + +/* + +aufs driver directory structure + + . + ├── layers // Metadata of layers + │ ├── 1 + │ ├── 2 + │ └── 3 + ├── diff // Content of the layer + │ ├── 1 // Contains layers that need to be mounted for the id + │ ├── 2 + │ └── 3 + └── mnt // Mount points for the rw layers to be mounted + ├── 1 + ├── 2 + └── 3 + +*/ + +package aufs + +import ( + "bufio" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + "sync" + "syscall" + + "github.com/Sirupsen/logrus" + "github.com/vbatts/tar-split/tar/storage" + + "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/chrootarchive" + "github.com/docker/docker/pkg/directory" + "github.com/docker/docker/pkg/idtools" + mountpk "github.com/docker/docker/pkg/mount" + "github.com/docker/docker/pkg/stringid" + + "github.com/opencontainers/runc/libcontainer/label" +) + +var ( + // ErrAufsNotSupported is returned if aufs is not supported by the host. + ErrAufsNotSupported = fmt.Errorf("AUFS was not found in /proc/filesystems") + incompatibleFsMagic = []graphdriver.FsMagic{ + graphdriver.FsMagicBtrfs, + graphdriver.FsMagicAufs, + } + backingFs = "" + + enableDirpermLock sync.Once + enableDirperm bool +) + +func init() { + graphdriver.Register("aufs", Init) +} + +// Driver contains information about the filesystem mounted. +type Driver struct { + sync.Mutex + root string + uidMaps []idtools.IDMap + gidMaps []idtools.IDMap + pathCacheLock sync.Mutex + pathCache map[string]string +} + +// Init returns a new AUFS driver. +// An error is returned if AUFS is not supported. +func Init(root string, options []string, uidMaps, gidMaps []idtools.IDMap) (graphdriver.Driver, error) { + + // Try to load the aufs kernel module + if err := supportsAufs(); err != nil { + return nil, graphdriver.ErrNotSupported + } + + fsMagic, err := graphdriver.GetFSMagic(root) + if err != nil { + return nil, err + } + if fsName, ok := graphdriver.FsNames[fsMagic]; ok { + backingFs = fsName + } + + for _, magic := range incompatibleFsMagic { + if fsMagic == magic { + return nil, graphdriver.ErrIncompatibleFS + } + } + + paths := []string{ + "mnt", + "diff", + "layers", + } + + a := &Driver{ + root: root, + uidMaps: uidMaps, + gidMaps: gidMaps, + pathCache: make(map[string]string), + } + + rootUID, rootGID, err := idtools.GetRootUIDGID(uidMaps, gidMaps) + if err != nil { + return nil, err + } + // Create the root aufs driver dir and return + // if it already exists + // If not populate the dir structure + if err := idtools.MkdirAllAs(root, 0700, rootUID, rootGID); err != nil { + if os.IsExist(err) { + return a, nil + } + return nil, err + } + + if err := mountpk.MakePrivate(root); err != nil { + return nil, err + } + + // Populate the dir structure + for _, p := range paths { + if err := idtools.MkdirAllAs(path.Join(root, p), 0700, rootUID, rootGID); err != nil { + return nil, err + } + } + return a, nil +} + +// Return a nil error if the kernel supports aufs +// We cannot modprobe because inside dind modprobe fails +// to run +func supportsAufs() error { + // We can try to modprobe aufs first before looking at + // proc/filesystems for when aufs is supported + exec.Command("modprobe", "aufs").Run() + + f, err := os.Open("/proc/filesystems") + if err != nil { + return err + } + defer f.Close() + + s := bufio.NewScanner(f) + for s.Scan() { + if strings.Contains(s.Text(), "aufs") { + return nil + } + } + return ErrAufsNotSupported +} + +func (a *Driver) rootPath() string { + return a.root +} + +func (*Driver) String() string { + return "aufs" +} + +// Status returns current information about the filesystem such as root directory, number of directories mounted, etc. +func (a *Driver) Status() [][2]string { + ids, _ := loadIds(path.Join(a.rootPath(), "layers")) + return [][2]string{ + {"Root Dir", a.rootPath()}, + {"Backing Filesystem", backingFs}, + {"Dirs", fmt.Sprintf("%d", len(ids))}, + {"Dirperm1 Supported", fmt.Sprintf("%v", useDirperm())}, + } +} + +// GetMetadata not implemented +func (a *Driver) GetMetadata(id string) (map[string]string, error) { + return nil, nil +} + +// Exists returns true if the given id is registered with +// this driver +func (a *Driver) Exists(id string) bool { + if _, err := os.Lstat(path.Join(a.rootPath(), "layers", id)); err != nil { + return false + } + return true +} + +// Create three folders for each id +// mnt, layers, and diff +func (a *Driver) Create(id, parent, mountLabel string) error { + if err := a.createDirsFor(id); err != nil { + return err + } + // Write the layers metadata + f, err := os.Create(path.Join(a.rootPath(), "layers", id)) + if err != nil { + return err + } + defer f.Close() + + if parent != "" { + ids, err := getParentIds(a.rootPath(), parent) + if err != nil { + return err + } + + if _, err := fmt.Fprintln(f, parent); err != nil { + return err + } + for _, i := range ids { + if _, err := fmt.Fprintln(f, i); err != nil { + return err + } + } + } + + return nil +} + +// createDirsFor creates two directories for the given id. +// mnt and diff +func (a *Driver) createDirsFor(id string) error { + paths := []string{ + "mnt", + "diff", + } + + rootUID, rootGID, err := idtools.GetRootUIDGID(a.uidMaps, a.gidMaps) + if err != nil { + return err + } + // Directory permission is 0755. + // The path of directories are /mnt/ + // and /diff/ + for _, p := range paths { + if err := idtools.MkdirAllAs(path.Join(a.rootPath(), p, id), 0755, rootUID, rootGID); err != nil { + return err + } + } + return nil +} + +// Remove will unmount and remove the given id. +func (a *Driver) Remove(id string) error { + a.pathCacheLock.Lock() + mountpoint, exists := a.pathCache[id] + a.pathCacheLock.Unlock() + if !exists { + mountpoint = a.getMountpoint(id) + } + if err := a.unmount(mountpoint); err != nil { + // no need to return here, we can still try to remove since the `Rename` will fail below if still mounted + logrus.Debugf("aufs: error while unmounting %s: %v", mountpoint, err) + } + + // Atomically remove each directory in turn by first moving it out of the + // way (so that docker doesn't find it anymore) before doing removal of + // the whole tree. + tmpMntPath := path.Join(a.mntPath(), fmt.Sprintf("%s-removing", id)) + if err := os.Rename(mountpoint, tmpMntPath); err != nil && !os.IsNotExist(err) { + return err + } + defer os.RemoveAll(tmpMntPath) + + tmpDiffpath := path.Join(a.diffPath(), fmt.Sprintf("%s-removing", id)) + if err := os.Rename(a.getDiffPath(id), tmpDiffpath); err != nil && !os.IsNotExist(err) { + return err + } + defer os.RemoveAll(tmpDiffpath) + + // Remove the layers file for the id + if err := os.Remove(path.Join(a.rootPath(), "layers", id)); err != nil && !os.IsNotExist(err) { + return err + } + + a.pathCacheLock.Lock() + delete(a.pathCache, id) + a.pathCacheLock.Unlock() + return nil +} + +// Get returns the rootfs path for the id. +// This will mount the dir at it's given path +func (a *Driver) Get(id, mountLabel string) (string, error) { + parents, err := a.getParentLayerPaths(id) + if err != nil && !os.IsNotExist(err) { + return "", err + } + + a.pathCacheLock.Lock() + m, exists := a.pathCache[id] + a.pathCacheLock.Unlock() + + if !exists { + m = a.getDiffPath(id) + if len(parents) > 0 { + m = a.getMountpoint(id) + } + } + + // If a dir does not have a parent ( no layers )do not try to mount + // just return the diff path to the data + if len(parents) > 0 { + if err := a.mount(id, m, mountLabel, parents); err != nil { + return "", err + } + } + + a.pathCacheLock.Lock() + a.pathCache[id] = m + a.pathCacheLock.Unlock() + return m, nil +} + +// Put unmounts and updates list of active mounts. +func (a *Driver) Put(id string) error { + a.pathCacheLock.Lock() + m, exists := a.pathCache[id] + if !exists { + m = a.getMountpoint(id) + a.pathCache[id] = m + } + a.pathCacheLock.Unlock() + + err := a.unmount(m) + if err != nil { + logrus.Debugf("Failed to unmount %s aufs: %v", id, err) + } + return err +} + +// Diff produces an archive of the changes between the specified +// layer and its parent layer which may be "". +func (a *Driver) Diff(id, parent string) (archive.Archive, error) { + // AUFS doesn't need the parent layer to produce a diff. + return archive.TarWithOptions(path.Join(a.rootPath(), "diff", id), &archive.TarOptions{ + Compression: archive.Uncompressed, + ExcludePatterns: []string{archive.WhiteoutMetaPrefix + "*", "!" + archive.WhiteoutOpaqueDir}, + UIDMaps: a.uidMaps, + GIDMaps: a.gidMaps, + }) +} + +type fileGetNilCloser struct { + storage.FileGetter +} + +func (f fileGetNilCloser) Close() error { + return nil +} + +// DiffGetter returns a FileGetCloser that can read files from the directory that +// contains files for the layer differences. Used for direct access for tar-split. +func (a *Driver) DiffGetter(id string) (graphdriver.FileGetCloser, error) { + p := path.Join(a.rootPath(), "diff", id) + return fileGetNilCloser{storage.NewPathFileGetter(p)}, nil +} + +func (a *Driver) applyDiff(id string, diff archive.Reader) error { + return chrootarchive.UntarUncompressed(diff, path.Join(a.rootPath(), "diff", id), &archive.TarOptions{ + UIDMaps: a.uidMaps, + GIDMaps: a.gidMaps, + }) +} + +// DiffSize calculates the changes between the specified id +// and its parent and returns the size in bytes of the changes +// relative to its base filesystem directory. +func (a *Driver) DiffSize(id, parent string) (size int64, err error) { + // AUFS doesn't need the parent layer to calculate the diff size. + return directory.Size(path.Join(a.rootPath(), "diff", id)) +} + +// ApplyDiff extracts the changeset from the given diff into the +// layer with the specified id and parent, returning the size of the +// new layer in bytes. +func (a *Driver) ApplyDiff(id, parent string, diff archive.Reader) (size int64, err error) { + // AUFS doesn't need the parent id to apply the diff. + if err = a.applyDiff(id, diff); err != nil { + return + } + + return a.DiffSize(id, parent) +} + +// Changes produces a list of changes between the specified layer +// and its parent layer. If parent is "", then all changes will be ADD changes. +func (a *Driver) Changes(id, parent string) ([]archive.Change, error) { + // AUFS doesn't have snapshots, so we need to get changes from all parent + // layers. + layers, err := a.getParentLayerPaths(id) + if err != nil { + return nil, err + } + return archive.Changes(layers, path.Join(a.rootPath(), "diff", id)) +} + +func (a *Driver) getParentLayerPaths(id string) ([]string, error) { + parentIds, err := getParentIds(a.rootPath(), id) + if err != nil { + return nil, err + } + layers := make([]string, len(parentIds)) + + // Get the diff paths for all the parent ids + for i, p := range parentIds { + layers[i] = path.Join(a.rootPath(), "diff", p) + } + return layers, nil +} + +func (a *Driver) mount(id string, target string, mountLabel string, layers []string) error { + a.Lock() + defer a.Unlock() + + // If the id is mounted or we get an error return + if mounted, err := a.mounted(target); err != nil || mounted { + return err + } + + rw := a.getDiffPath(id) + + if err := a.aufsMount(layers, rw, target, mountLabel); err != nil { + return fmt.Errorf("error creating aufs mount to %s: %v", target, err) + } + return nil +} + +func (a *Driver) unmount(mountPath string) error { + a.Lock() + defer a.Unlock() + + if mounted, err := a.mounted(mountPath); err != nil || !mounted { + return err + } + if err := Unmount(mountPath); err != nil { + return err + } + return nil +} + +func (a *Driver) mounted(mountpoint string) (bool, error) { + return graphdriver.Mounted(graphdriver.FsMagicAufs, mountpoint) +} + +// Cleanup aufs and unmount all mountpoints +func (a *Driver) Cleanup() error { + var dirs []string + if err := filepath.Walk(a.mntPath(), func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + return nil + } + dirs = append(dirs, path) + return nil + }); err != nil { + return err + } + + for _, m := range dirs { + if err := a.unmount(m); err != nil { + logrus.Debugf("aufs error unmounting %s: %s", stringid.TruncateID(m), err) + } + } + return mountpk.Unmount(a.root) +} + +func (a *Driver) aufsMount(ro []string, rw, target, mountLabel string) (err error) { + defer func() { + if err != nil { + Unmount(target) + } + }() + + // Mount options are clipped to page size(4096 bytes). If there are more + // layers then these are remounted individually using append. + + offset := 54 + if useDirperm() { + offset += len("dirperm1") + } + b := make([]byte, syscall.Getpagesize()-len(mountLabel)-offset) // room for xino & mountLabel + bp := copy(b, fmt.Sprintf("br:%s=rw", rw)) + + firstMount := true + i := 0 + + for { + for ; i < len(ro); i++ { + layer := fmt.Sprintf(":%s=ro+wh", ro[i]) + + if firstMount { + if bp+len(layer) > len(b) { + break + } + bp += copy(b[bp:], layer) + } else { + data := label.FormatMountLabel(fmt.Sprintf("append%s", layer), mountLabel) + if err = mount("none", target, "aufs", syscall.MS_REMOUNT, data); err != nil { + return + } + } + } + + if firstMount { + opts := "dio,xino=/dev/shm/aufs.xino" + if useDirperm() { + opts += ",dirperm1" + } + data := label.FormatMountLabel(fmt.Sprintf("%s,%s", string(b[:bp]), opts), mountLabel) + if err = mount("none", target, "aufs", 0, data); err != nil { + return + } + firstMount = false + } + + if i == len(ro) { + break + } + } + + return +} + +// useDirperm checks dirperm1 mount option can be used with the current +// version of aufs. +func useDirperm() bool { + enableDirpermLock.Do(func() { + base, err := ioutil.TempDir("", "docker-aufs-base") + if err != nil { + logrus.Errorf("error checking dirperm1: %v", err) + return + } + defer os.RemoveAll(base) + + union, err := ioutil.TempDir("", "docker-aufs-union") + if err != nil { + logrus.Errorf("error checking dirperm1: %v", err) + return + } + defer os.RemoveAll(union) + + opts := fmt.Sprintf("br:%s,dirperm1,xino=/dev/shm/aufs.xino", base) + if err := mount("none", union, "aufs", 0, opts); err != nil { + return + } + enableDirperm = true + if err := Unmount(union); err != nil { + logrus.Errorf("error checking dirperm1: failed to unmount %v", err) + } + }) + return enableDirperm +} diff --git a/daemon/graphdriver/aufs/aufs_test.go b/daemon/graphdriver/aufs/aufs_test.go new file mode 100644 index 00000000..b0ddf89a --- /dev/null +++ b/daemon/graphdriver/aufs/aufs_test.go @@ -0,0 +1,801 @@ +// +build linux + +package aufs + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "io/ioutil" + "os" + "path" + "sync" + "testing" + + "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/reexec" + "github.com/docker/docker/pkg/stringid" +) + +var ( + tmpOuter = path.Join(os.TempDir(), "aufs-tests") + tmp = path.Join(tmpOuter, "aufs") +) + +func init() { + reexec.Init() +} + +func testInit(dir string, t testing.TB) graphdriver.Driver { + d, err := Init(dir, nil, nil, nil) + if err != nil { + if err == graphdriver.ErrNotSupported { + t.Skip(err) + } else { + t.Fatal(err) + } + } + return d +} + +func newDriver(t testing.TB) *Driver { + if err := os.MkdirAll(tmp, 0755); err != nil { + t.Fatal(err) + } + + d := testInit(tmp, t) + return d.(*Driver) +} + +func TestNewDriver(t *testing.T) { + if err := os.MkdirAll(tmp, 0755); err != nil { + t.Fatal(err) + } + + d := testInit(tmp, t) + defer os.RemoveAll(tmp) + if d == nil { + t.Fatalf("Driver should not be nil") + } +} + +func TestAufsString(t *testing.T) { + d := newDriver(t) + defer os.RemoveAll(tmp) + + if d.String() != "aufs" { + t.Fatalf("Expected aufs got %s", d.String()) + } +} + +func TestCreateDirStructure(t *testing.T) { + newDriver(t) + defer os.RemoveAll(tmp) + + paths := []string{ + "mnt", + "layers", + "diff", + } + + for _, p := range paths { + if _, err := os.Stat(path.Join(tmp, p)); err != nil { + t.Fatal(err) + } + } +} + +// We should be able to create two drivers with the same dir structure +func TestNewDriverFromExistingDir(t *testing.T) { + if err := os.MkdirAll(tmp, 0755); err != nil { + t.Fatal(err) + } + + testInit(tmp, t) + testInit(tmp, t) + os.RemoveAll(tmp) +} + +func TestCreateNewDir(t *testing.T) { + d := newDriver(t) + defer os.RemoveAll(tmp) + + if err := d.Create("1", "", ""); err != nil { + t.Fatal(err) + } +} + +func TestCreateNewDirStructure(t *testing.T) { + d := newDriver(t) + defer os.RemoveAll(tmp) + + if err := d.Create("1", "", ""); err != nil { + t.Fatal(err) + } + + paths := []string{ + "mnt", + "diff", + "layers", + } + + for _, p := range paths { + if _, err := os.Stat(path.Join(tmp, p, "1")); err != nil { + t.Fatal(err) + } + } +} + +func TestRemoveImage(t *testing.T) { + d := newDriver(t) + defer os.RemoveAll(tmp) + + if err := d.Create("1", "", ""); err != nil { + t.Fatal(err) + } + + if err := d.Remove("1"); err != nil { + t.Fatal(err) + } + + paths := []string{ + "mnt", + "diff", + "layers", + } + + for _, p := range paths { + if _, err := os.Stat(path.Join(tmp, p, "1")); err == nil { + t.Fatalf("Error should not be nil because dirs with id 1 should be delted: %s", p) + } + } +} + +func TestGetWithoutParent(t *testing.T) { + d := newDriver(t) + defer os.RemoveAll(tmp) + + if err := d.Create("1", "", ""); err != nil { + t.Fatal(err) + } + + diffPath, err := d.Get("1", "") + if err != nil { + t.Fatal(err) + } + expected := path.Join(tmp, "diff", "1") + if diffPath != expected { + t.Fatalf("Expected path %s got %s", expected, diffPath) + } +} + +func TestCleanupWithNoDirs(t *testing.T) { + d := newDriver(t) + defer os.RemoveAll(tmp) + + if err := d.Cleanup(); err != nil { + t.Fatal(err) + } +} + +func TestCleanupWithDir(t *testing.T) { + d := newDriver(t) + defer os.RemoveAll(tmp) + + if err := d.Create("1", "", ""); err != nil { + t.Fatal(err) + } + + if err := d.Cleanup(); err != nil { + t.Fatal(err) + } +} + +func TestMountedFalseResponse(t *testing.T) { + d := newDriver(t) + defer os.RemoveAll(tmp) + + if err := d.Create("1", "", ""); err != nil { + t.Fatal(err) + } + + response, err := d.mounted(d.getDiffPath("1")) + if err != nil { + t.Fatal(err) + } + + if response != false { + t.Fatalf("Response if dir id 1 is mounted should be false") + } +} + +func TestMountedTrueReponse(t *testing.T) { + d := newDriver(t) + defer os.RemoveAll(tmp) + defer d.Cleanup() + + if err := d.Create("1", "", ""); err != nil { + t.Fatal(err) + } + if err := d.Create("2", "1", ""); err != nil { + t.Fatal(err) + } + + _, err := d.Get("2", "") + if err != nil { + t.Fatal(err) + } + + response, err := d.mounted(d.pathCache["2"]) + if err != nil { + t.Fatal(err) + } + + if response != true { + t.Fatalf("Response if dir id 2 is mounted should be true") + } +} + +func TestMountWithParent(t *testing.T) { + d := newDriver(t) + defer os.RemoveAll(tmp) + + if err := d.Create("1", "", ""); err != nil { + t.Fatal(err) + } + if err := d.Create("2", "1", ""); err != nil { + t.Fatal(err) + } + + defer func() { + if err := d.Cleanup(); err != nil { + t.Fatal(err) + } + }() + + mntPath, err := d.Get("2", "") + if err != nil { + t.Fatal(err) + } + if mntPath == "" { + t.Fatal("mntPath should not be empty string") + } + + expected := path.Join(tmp, "mnt", "2") + if mntPath != expected { + t.Fatalf("Expected %s got %s", expected, mntPath) + } +} + +func TestRemoveMountedDir(t *testing.T) { + d := newDriver(t) + defer os.RemoveAll(tmp) + + if err := d.Create("1", "", ""); err != nil { + t.Fatal(err) + } + if err := d.Create("2", "1", ""); err != nil { + t.Fatal(err) + } + + defer func() { + if err := d.Cleanup(); err != nil { + t.Fatal(err) + } + }() + + mntPath, err := d.Get("2", "") + if err != nil { + t.Fatal(err) + } + if mntPath == "" { + t.Fatal("mntPath should not be empty string") + } + + mounted, err := d.mounted(d.pathCache["2"]) + if err != nil { + t.Fatal(err) + } + + if !mounted { + t.Fatalf("Dir id 2 should be mounted") + } + + if err := d.Remove("2"); err != nil { + t.Fatal(err) + } +} + +func TestCreateWithInvalidParent(t *testing.T) { + d := newDriver(t) + defer os.RemoveAll(tmp) + + if err := d.Create("1", "docker", ""); err == nil { + t.Fatalf("Error should not be nil with parent does not exist") + } +} + +func TestGetDiff(t *testing.T) { + d := newDriver(t) + defer os.RemoveAll(tmp) + + if err := d.Create("1", "", ""); err != nil { + t.Fatal(err) + } + + diffPath, err := d.Get("1", "") + if err != nil { + t.Fatal(err) + } + + // Add a file to the diff path with a fixed size + size := int64(1024) + + f, err := os.Create(path.Join(diffPath, "test_file")) + if err != nil { + t.Fatal(err) + } + if err := f.Truncate(size); err != nil { + t.Fatal(err) + } + f.Close() + + a, err := d.Diff("1", "") + if err != nil { + t.Fatal(err) + } + if a == nil { + t.Fatalf("Archive should not be nil") + } +} + +func TestChanges(t *testing.T) { + d := newDriver(t) + defer os.RemoveAll(tmp) + + if err := d.Create("1", "", ""); err != nil { + t.Fatal(err) + } + if err := d.Create("2", "1", ""); err != nil { + t.Fatal(err) + } + + defer func() { + if err := d.Cleanup(); err != nil { + t.Fatal(err) + } + }() + + mntPoint, err := d.Get("2", "") + if err != nil { + t.Fatal(err) + } + + // Create a file to save in the mountpoint + f, err := os.Create(path.Join(mntPoint, "test.txt")) + if err != nil { + t.Fatal(err) + } + + if _, err := f.WriteString("testline"); err != nil { + t.Fatal(err) + } + if err := f.Close(); err != nil { + t.Fatal(err) + } + + changes, err := d.Changes("2", "") + if err != nil { + t.Fatal(err) + } + if len(changes) != 1 { + t.Fatalf("Dir 2 should have one change from parent got %d", len(changes)) + } + change := changes[0] + + expectedPath := "/test.txt" + if change.Path != expectedPath { + t.Fatalf("Expected path %s got %s", expectedPath, change.Path) + } + + if change.Kind != archive.ChangeAdd { + t.Fatalf("Change kind should be ChangeAdd got %s", change.Kind) + } + + if err := d.Create("3", "2", ""); err != nil { + t.Fatal(err) + } + mntPoint, err = d.Get("3", "") + if err != nil { + t.Fatal(err) + } + + // Create a file to save in the mountpoint + f, err = os.Create(path.Join(mntPoint, "test2.txt")) + if err != nil { + t.Fatal(err) + } + + if _, err := f.WriteString("testline"); err != nil { + t.Fatal(err) + } + if err := f.Close(); err != nil { + t.Fatal(err) + } + + changes, err = d.Changes("3", "") + if err != nil { + t.Fatal(err) + } + + if len(changes) != 1 { + t.Fatalf("Dir 2 should have one change from parent got %d", len(changes)) + } + change = changes[0] + + expectedPath = "/test2.txt" + if change.Path != expectedPath { + t.Fatalf("Expected path %s got %s", expectedPath, change.Path) + } + + if change.Kind != archive.ChangeAdd { + t.Fatalf("Change kind should be ChangeAdd got %s", change.Kind) + } +} + +func TestDiffSize(t *testing.T) { + d := newDriver(t) + defer os.RemoveAll(tmp) + + if err := d.Create("1", "", ""); err != nil { + t.Fatal(err) + } + + diffPath, err := d.Get("1", "") + if err != nil { + t.Fatal(err) + } + + // Add a file to the diff path with a fixed size + size := int64(1024) + + f, err := os.Create(path.Join(diffPath, "test_file")) + if err != nil { + t.Fatal(err) + } + if err := f.Truncate(size); err != nil { + t.Fatal(err) + } + s, err := f.Stat() + if err != nil { + t.Fatal(err) + } + size = s.Size() + if err := f.Close(); err != nil { + t.Fatal(err) + } + + diffSize, err := d.DiffSize("1", "") + if err != nil { + t.Fatal(err) + } + if diffSize != size { + t.Fatalf("Expected size to be %d got %d", size, diffSize) + } +} + +func TestChildDiffSize(t *testing.T) { + d := newDriver(t) + defer os.RemoveAll(tmp) + defer d.Cleanup() + + if err := d.Create("1", "", ""); err != nil { + t.Fatal(err) + } + + diffPath, err := d.Get("1", "") + if err != nil { + t.Fatal(err) + } + + // Add a file to the diff path with a fixed size + size := int64(1024) + + f, err := os.Create(path.Join(diffPath, "test_file")) + if err != nil { + t.Fatal(err) + } + if err := f.Truncate(size); err != nil { + t.Fatal(err) + } + s, err := f.Stat() + if err != nil { + t.Fatal(err) + } + size = s.Size() + if err := f.Close(); err != nil { + t.Fatal(err) + } + + diffSize, err := d.DiffSize("1", "") + if err != nil { + t.Fatal(err) + } + if diffSize != size { + t.Fatalf("Expected size to be %d got %d", size, diffSize) + } + + if err := d.Create("2", "1", ""); err != nil { + t.Fatal(err) + } + + diffSize, err = d.DiffSize("2", "") + if err != nil { + t.Fatal(err) + } + // The diff size for the child should be zero + if diffSize != 0 { + t.Fatalf("Expected size to be %d got %d", 0, diffSize) + } +} + +func TestExists(t *testing.T) { + d := newDriver(t) + defer os.RemoveAll(tmp) + defer d.Cleanup() + + if err := d.Create("1", "", ""); err != nil { + t.Fatal(err) + } + + if d.Exists("none") { + t.Fatal("id name should not exist in the driver") + } + + if !d.Exists("1") { + t.Fatal("id 1 should exist in the driver") + } +} + +func TestStatus(t *testing.T) { + d := newDriver(t) + defer os.RemoveAll(tmp) + defer d.Cleanup() + + if err := d.Create("1", "", ""); err != nil { + t.Fatal(err) + } + + status := d.Status() + if status == nil || len(status) == 0 { + t.Fatal("Status should not be nil or empty") + } + rootDir := status[0] + dirs := status[2] + if rootDir[0] != "Root Dir" { + t.Fatalf("Expected Root Dir got %s", rootDir[0]) + } + if rootDir[1] != d.rootPath() { + t.Fatalf("Expected %s got %s", d.rootPath(), rootDir[1]) + } + if dirs[0] != "Dirs" { + t.Fatalf("Expected Dirs got %s", dirs[0]) + } + if dirs[1] != "1" { + t.Fatalf("Expected 1 got %s", dirs[1]) + } +} + +func TestApplyDiff(t *testing.T) { + d := newDriver(t) + defer os.RemoveAll(tmp) + defer d.Cleanup() + + if err := d.Create("1", "", ""); err != nil { + t.Fatal(err) + } + + diffPath, err := d.Get("1", "") + if err != nil { + t.Fatal(err) + } + + // Add a file to the diff path with a fixed size + size := int64(1024) + + f, err := os.Create(path.Join(diffPath, "test_file")) + if err != nil { + t.Fatal(err) + } + if err := f.Truncate(size); err != nil { + t.Fatal(err) + } + f.Close() + + diff, err := d.Diff("1", "") + if err != nil { + t.Fatal(err) + } + + if err := d.Create("2", "", ""); err != nil { + t.Fatal(err) + } + if err := d.Create("3", "2", ""); err != nil { + t.Fatal(err) + } + + if err := d.applyDiff("3", diff); err != nil { + t.Fatal(err) + } + + // Ensure that the file is in the mount point for id 3 + + mountPoint, err := d.Get("3", "") + if err != nil { + t.Fatal(err) + } + if _, err := os.Stat(path.Join(mountPoint, "test_file")); err != nil { + t.Fatal(err) + } +} + +func hash(c string) string { + h := sha256.New() + fmt.Fprint(h, c) + return hex.EncodeToString(h.Sum(nil)) +} + +func testMountMoreThan42Layers(t *testing.T, mountPath string) { + if err := os.MkdirAll(mountPath, 0755); err != nil { + t.Fatal(err) + } + + defer os.RemoveAll(mountPath) + d := testInit(mountPath, t).(*Driver) + defer d.Cleanup() + var last string + var expected int + + for i := 1; i < 127; i++ { + expected++ + var ( + parent = fmt.Sprintf("%d", i-1) + current = fmt.Sprintf("%d", i) + ) + + if parent == "0" { + parent = "" + } else { + parent = hash(parent) + } + current = hash(current) + + if err := d.Create(current, parent, ""); err != nil { + t.Logf("Current layer %d", i) + t.Error(err) + } + point, err := d.Get(current, "") + if err != nil { + t.Logf("Current layer %d", i) + t.Error(err) + } + f, err := os.Create(path.Join(point, current)) + if err != nil { + t.Logf("Current layer %d", i) + t.Error(err) + } + f.Close() + + if i%10 == 0 { + if err := os.Remove(path.Join(point, parent)); err != nil { + t.Logf("Current layer %d", i) + t.Error(err) + } + expected-- + } + last = current + } + + // Perform the actual mount for the top most image + point, err := d.Get(last, "") + if err != nil { + t.Error(err) + } + files, err := ioutil.ReadDir(point) + if err != nil { + t.Error(err) + } + if len(files) != expected { + t.Errorf("Expected %d got %d", expected, len(files)) + } +} + +func TestMountMoreThan42Layers(t *testing.T) { + os.RemoveAll(tmpOuter) + testMountMoreThan42Layers(t, tmp) +} + +func TestMountMoreThan42LayersMatchingPathLength(t *testing.T) { + defer os.RemoveAll(tmpOuter) + zeroes := "0" + for { + // This finds a mount path so that when combined into aufs mount options + // 4096 byte boundary would be in between the paths or in permission + // section. For '/tmp' it will use '/tmp/aufs-tests/00000000/aufs' + mountPath := path.Join(tmpOuter, zeroes, "aufs") + pathLength := 77 + len(mountPath) + + if mod := 4095 % pathLength; mod == 0 || mod > pathLength-2 { + t.Logf("Using path: %s", mountPath) + testMountMoreThan42Layers(t, mountPath) + return + } + zeroes += "0" + } +} + +func BenchmarkConcurrentAccess(b *testing.B) { + b.StopTimer() + b.ResetTimer() + + d := newDriver(b) + defer os.RemoveAll(tmp) + defer d.Cleanup() + + numConcurent := 256 + // create a bunch of ids + var ids []string + for i := 0; i < numConcurent; i++ { + ids = append(ids, stringid.GenerateNonCryptoID()) + } + + if err := d.Create(ids[0], "", ""); err != nil { + b.Fatal(err) + } + + if err := d.Create(ids[1], ids[0], ""); err != nil { + b.Fatal(err) + } + + parent := ids[1] + ids = append(ids[2:]) + + chErr := make(chan error, numConcurent) + var outerGroup sync.WaitGroup + outerGroup.Add(len(ids)) + b.StartTimer() + + // here's the actual bench + for _, id := range ids { + go func(id string) { + defer outerGroup.Done() + if err := d.Create(id, parent, ""); err != nil { + b.Logf("Create %s failed", id) + chErr <- err + return + } + var innerGroup sync.WaitGroup + for i := 0; i < b.N; i++ { + innerGroup.Add(1) + go func() { + d.Get(id, "") + d.Put(id) + innerGroup.Done() + }() + } + innerGroup.Wait() + d.Remove(id) + }(id) + } + + outerGroup.Wait() + b.StopTimer() + close(chErr) + for err := range chErr { + if err != nil { + b.Log(err) + b.Fail() + } + } +} diff --git a/daemon/graphdriver/aufs/dirs.go b/daemon/graphdriver/aufs/dirs.go new file mode 100644 index 00000000..eb298d9e --- /dev/null +++ b/daemon/graphdriver/aufs/dirs.go @@ -0,0 +1,64 @@ +// +build linux + +package aufs + +import ( + "bufio" + "io/ioutil" + "os" + "path" +) + +// Return all the directories +func loadIds(root string) ([]string, error) { + dirs, err := ioutil.ReadDir(root) + if err != nil { + return nil, err + } + out := []string{} + for _, d := range dirs { + if !d.IsDir() { + out = append(out, d.Name()) + } + } + return out, nil +} + +// Read the layers file for the current id and return all the +// layers represented by new lines in the file +// +// If there are no lines in the file then the id has no parent +// and an empty slice is returned. +func getParentIds(root, id string) ([]string, error) { + f, err := os.Open(path.Join(root, "layers", id)) + if err != nil { + return nil, err + } + defer f.Close() + + out := []string{} + s := bufio.NewScanner(f) + + for s.Scan() { + if t := s.Text(); t != "" { + out = append(out, s.Text()) + } + } + return out, s.Err() +} + +func (a *Driver) getMountpoint(id string) string { + return path.Join(a.mntPath(), id) +} + +func (a *Driver) mntPath() string { + return path.Join(a.rootPath(), "mnt") +} + +func (a *Driver) getDiffPath(id string) string { + return path.Join(a.diffPath(), id) +} + +func (a *Driver) diffPath() string { + return path.Join(a.rootPath(), "diff") +} diff --git a/daemon/graphdriver/aufs/mount.go b/daemon/graphdriver/aufs/mount.go new file mode 100644 index 00000000..36fa62e4 --- /dev/null +++ b/daemon/graphdriver/aufs/mount.go @@ -0,0 +1,21 @@ +// +build linux + +package aufs + +import ( + "os/exec" + "syscall" + + "github.com/Sirupsen/logrus" +) + +// Unmount the target specified. +func Unmount(target string) error { + if err := exec.Command("auplink", target, "flush").Run(); err != nil { + logrus.Errorf("Couldn't run auplink before unmount %s: %s", target, err) + } + if err := syscall.Unmount(target, 0); err != nil { + return err + } + return nil +} diff --git a/daemon/graphdriver/aufs/mount_linux.go b/daemon/graphdriver/aufs/mount_linux.go new file mode 100644 index 00000000..8062bae4 --- /dev/null +++ b/daemon/graphdriver/aufs/mount_linux.go @@ -0,0 +1,7 @@ +package aufs + +import "syscall" + +func mount(source string, target string, fstype string, flags uintptr, data string) error { + return syscall.Mount(source, target, fstype, flags, data) +} diff --git a/daemon/graphdriver/aufs/mount_unsupported.go b/daemon/graphdriver/aufs/mount_unsupported.go new file mode 100644 index 00000000..d030b066 --- /dev/null +++ b/daemon/graphdriver/aufs/mount_unsupported.go @@ -0,0 +1,12 @@ +// +build !linux + +package aufs + +import "errors" + +// MsRemount declared to specify a non-linux system mount. +const MsRemount = 0 + +func mount(source string, target string, fstype string, flags uintptr, data string) (err error) { + return errors.New("mount is not implemented on this platform") +} diff --git a/daemon/graphdriver/btrfs/btrfs.go b/daemon/graphdriver/btrfs/btrfs.go new file mode 100644 index 00000000..1f18090f --- /dev/null +++ b/daemon/graphdriver/btrfs/btrfs.go @@ -0,0 +1,330 @@ +// +build linux + +package btrfs + +/* +#include +#include +#include +#include + +static void set_name_btrfs_ioctl_vol_args_v2(struct btrfs_ioctl_vol_args_v2* btrfs_struct, const char* value) { + snprintf(btrfs_struct->name, BTRFS_SUBVOL_NAME_MAX, "%s", value); +} +*/ +import "C" + +import ( + "fmt" + "os" + "path" + "path/filepath" + "syscall" + "unsafe" + + "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/mount" + "github.com/opencontainers/runc/libcontainer/label" +) + +func init() { + graphdriver.Register("btrfs", Init) +} + +// Init returns a new BTRFS driver. +// An error is returned if BTRFS is not supported. +func Init(home string, options []string, uidMaps, gidMaps []idtools.IDMap) (graphdriver.Driver, error) { + + fsMagic, err := graphdriver.GetFSMagic(home) + if err != nil { + return nil, err + } + + if fsMagic != graphdriver.FsMagicBtrfs { + return nil, graphdriver.ErrPrerequisites + } + + rootUID, rootGID, err := idtools.GetRootUIDGID(uidMaps, gidMaps) + if err != nil { + return nil, err + } + if err := idtools.MkdirAllAs(home, 0700, rootUID, rootGID); err != nil { + return nil, err + } + + if err := mount.MakePrivate(home); err != nil { + return nil, err + } + + driver := &Driver{ + home: home, + uidMaps: uidMaps, + gidMaps: gidMaps, + } + + return graphdriver.NewNaiveDiffDriver(driver, uidMaps, gidMaps), nil +} + +// Driver contains information about the filesystem mounted. +type Driver struct { + //root of the file system + home string + uidMaps []idtools.IDMap + gidMaps []idtools.IDMap +} + +// String prints the name of the driver (btrfs). +func (d *Driver) String() string { + return "btrfs" +} + +// Status returns current driver information in a two dimensional string array. +// Output contains "Build Version" and "Library Version" of the btrfs libraries used. +// Version information can be used to check compatibility with your kernel. +func (d *Driver) Status() [][2]string { + status := [][2]string{} + if bv := btrfsBuildVersion(); bv != "-" { + status = append(status, [2]string{"Build Version", bv}) + } + if lv := btrfsLibVersion(); lv != -1 { + status = append(status, [2]string{"Library Version", fmt.Sprintf("%d", lv)}) + } + return status +} + +// GetMetadata returns empty metadata for this driver. +func (d *Driver) GetMetadata(id string) (map[string]string, error) { + return nil, nil +} + +// Cleanup unmounts the home directory. +func (d *Driver) Cleanup() error { + return mount.Unmount(d.home) +} + +func free(p *C.char) { + C.free(unsafe.Pointer(p)) +} + +func openDir(path string) (*C.DIR, error) { + Cpath := C.CString(path) + defer free(Cpath) + + dir := C.opendir(Cpath) + if dir == nil { + return nil, fmt.Errorf("Can't open dir") + } + return dir, nil +} + +func closeDir(dir *C.DIR) { + if dir != nil { + C.closedir(dir) + } +} + +func getDirFd(dir *C.DIR) uintptr { + return uintptr(C.dirfd(dir)) +} + +func subvolCreate(path, name string) error { + dir, err := openDir(path) + if err != nil { + return err + } + defer closeDir(dir) + + var args C.struct_btrfs_ioctl_vol_args + for i, c := range []byte(name) { + args.name[i] = C.char(c) + } + + _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, getDirFd(dir), C.BTRFS_IOC_SUBVOL_CREATE, + uintptr(unsafe.Pointer(&args))) + if errno != 0 { + return fmt.Errorf("Failed to create btrfs subvolume: %v", errno.Error()) + } + return nil +} + +func subvolSnapshot(src, dest, name string) error { + srcDir, err := openDir(src) + if err != nil { + return err + } + defer closeDir(srcDir) + + destDir, err := openDir(dest) + if err != nil { + return err + } + defer closeDir(destDir) + + var args C.struct_btrfs_ioctl_vol_args_v2 + args.fd = C.__s64(getDirFd(srcDir)) + + var cs = C.CString(name) + C.set_name_btrfs_ioctl_vol_args_v2(&args, cs) + C.free(unsafe.Pointer(cs)) + + _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, getDirFd(destDir), C.BTRFS_IOC_SNAP_CREATE_V2, + uintptr(unsafe.Pointer(&args))) + if errno != 0 { + return fmt.Errorf("Failed to create btrfs snapshot: %v", errno.Error()) + } + return nil +} + +func isSubvolume(p string) (bool, error) { + var bufStat syscall.Stat_t + if err := syscall.Lstat(p, &bufStat); err != nil { + return false, err + } + + // return true if it is a btrfs subvolume + return bufStat.Ino == C.BTRFS_FIRST_FREE_OBJECTID, nil +} + +func subvolDelete(dirpath, name string) error { + dir, err := openDir(dirpath) + if err != nil { + return err + } + defer closeDir(dir) + fullPath := path.Join(dirpath, name) + + var args C.struct_btrfs_ioctl_vol_args + + // walk the btrfs subvolumes + walkSubvolumes := func(p string, f os.FileInfo, err error) error { + if err != nil { + if os.IsNotExist(err) && p != fullPath { + // missing most likely because the path was a subvolume that got removed in the previous iteration + // since it's gone anyway, we don't care + return nil + } + return fmt.Errorf("error walking subvolumes: %v", err) + } + // we want to check children only so skip itself + // it will be removed after the filepath walk anyways + if f.IsDir() && p != fullPath { + sv, err := isSubvolume(p) + if err != nil { + return fmt.Errorf("Failed to test if %s is a btrfs subvolume: %v", p, err) + } + if sv { + if err := subvolDelete(path.Dir(p), f.Name()); err != nil { + return fmt.Errorf("Failed to destroy btrfs child subvolume (%s) of parent (%s): %v", p, dirpath, err) + } + } + } + return nil + } + if err := filepath.Walk(path.Join(dirpath, name), walkSubvolumes); err != nil { + return fmt.Errorf("Recursively walking subvolumes for %s failed: %v", dirpath, err) + } + + // all subvolumes have been removed + // now remove the one originally passed in + for i, c := range []byte(name) { + args.name[i] = C.char(c) + } + _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, getDirFd(dir), C.BTRFS_IOC_SNAP_DESTROY, + uintptr(unsafe.Pointer(&args))) + if errno != 0 { + return fmt.Errorf("Failed to destroy btrfs snapshot %s for %s: %v", dirpath, name, errno.Error()) + } + return nil +} + +func (d *Driver) subvolumesDir() string { + return path.Join(d.home, "subvolumes") +} + +func (d *Driver) subvolumesDirID(id string) string { + return path.Join(d.subvolumesDir(), id) +} + +// Create the filesystem with given id. +func (d *Driver) Create(id, parent, mountLabel string) error { + subvolumes := path.Join(d.home, "subvolumes") + rootUID, rootGID, err := idtools.GetRootUIDGID(d.uidMaps, d.gidMaps) + if err != nil { + return err + } + if err := idtools.MkdirAllAs(subvolumes, 0700, rootUID, rootGID); err != nil { + return err + } + if parent == "" { + if err := subvolCreate(subvolumes, id); err != nil { + return err + } + } else { + parentDir := d.subvolumesDirID(parent) + st, err := os.Stat(parentDir) + if err != nil { + return err + } + if !st.IsDir() { + return fmt.Errorf("%s: not a directory", parentDir) + } + if err := subvolSnapshot(parentDir, subvolumes, id); err != nil { + return err + } + } + + // if we have a remapped root (user namespaces enabled), change the created snapshot + // dir ownership to match + if rootUID != 0 || rootGID != 0 { + if err := os.Chown(path.Join(subvolumes, id), rootUID, rootGID); err != nil { + return err + } + } + + return label.Relabel(path.Join(subvolumes, id), mountLabel, false) +} + +// Remove the filesystem with given id. +func (d *Driver) Remove(id string) error { + dir := d.subvolumesDirID(id) + if _, err := os.Stat(dir); err != nil { + return err + } + if err := subvolDelete(d.subvolumesDir(), id); err != nil { + return err + } + if err := os.RemoveAll(dir); err != nil && !os.IsNotExist(err) { + return err + } + return nil +} + +// Get the requested filesystem id. +func (d *Driver) Get(id, mountLabel string) (string, error) { + dir := d.subvolumesDirID(id) + st, err := os.Stat(dir) + if err != nil { + return "", err + } + + if !st.IsDir() { + return "", fmt.Errorf("%s: not a directory", dir) + } + + return dir, nil +} + +// Put is not implemented for BTRFS as there is no cleanup required for the id. +func (d *Driver) Put(id string) error { + // Get() creates no runtime resources (like e.g. mounts) + // so this doesn't need to do anything. + return nil +} + +// Exists checks if the id exists in the filesystem. +func (d *Driver) Exists(id string) bool { + dir := d.subvolumesDirID(id) + _, err := os.Stat(dir) + return err == nil +} diff --git a/daemon/graphdriver/btrfs/btrfs_test.go b/daemon/graphdriver/btrfs/btrfs_test.go new file mode 100644 index 00000000..7494218d --- /dev/null +++ b/daemon/graphdriver/btrfs/btrfs_test.go @@ -0,0 +1,63 @@ +// +build linux + +package btrfs + +import ( + "os" + "path" + "testing" + + "github.com/docker/docker/daemon/graphdriver/graphtest" +) + +// This avoids creating a new driver for each test if all tests are run +// Make sure to put new tests between TestBtrfsSetup and TestBtrfsTeardown +func TestBtrfsSetup(t *testing.T) { + graphtest.GetDriver(t, "btrfs") +} + +func TestBtrfsCreateEmpty(t *testing.T) { + graphtest.DriverTestCreateEmpty(t, "btrfs") +} + +func TestBtrfsCreateBase(t *testing.T) { + graphtest.DriverTestCreateBase(t, "btrfs") +} + +func TestBtrfsCreateSnap(t *testing.T) { + graphtest.DriverTestCreateSnap(t, "btrfs") +} + +func TestBtrfsSubvolDelete(t *testing.T) { + d := graphtest.GetDriver(t, "btrfs") + if err := d.Create("test", "", ""); err != nil { + t.Fatal(err) + } + defer graphtest.PutDriver(t) + + dir, err := d.Get("test", "") + if err != nil { + t.Fatal(err) + } + defer d.Put("test") + + if err := subvolCreate(dir, "subvoltest"); err != nil { + t.Fatal(err) + } + + if _, err := os.Stat(path.Join(dir, "subvoltest")); err != nil { + t.Fatal(err) + } + + if err := d.Remove("test"); err != nil { + t.Fatal(err) + } + + if _, err := os.Stat(path.Join(dir, "subvoltest")); !os.IsNotExist(err) { + t.Fatalf("expected not exist error on nested subvol, got: %v", err) + } +} + +func TestBtrfsTeardown(t *testing.T) { + graphtest.PutDriver(t) +} diff --git a/daemon/graphdriver/btrfs/dummy_unsupported.go b/daemon/graphdriver/btrfs/dummy_unsupported.go new file mode 100644 index 00000000..f0708888 --- /dev/null +++ b/daemon/graphdriver/btrfs/dummy_unsupported.go @@ -0,0 +1,3 @@ +// +build !linux !cgo + +package btrfs diff --git a/daemon/graphdriver/btrfs/version.go b/daemon/graphdriver/btrfs/version.go new file mode 100644 index 00000000..73d90cdd --- /dev/null +++ b/daemon/graphdriver/btrfs/version.go @@ -0,0 +1,26 @@ +// +build linux,!btrfs_noversion + +package btrfs + +/* +#include + +// around version 3.16, they did not define lib version yet +#ifndef BTRFS_LIB_VERSION +#define BTRFS_LIB_VERSION -1 +#endif + +// upstream had removed it, but now it will be coming back +#ifndef BTRFS_BUILD_VERSION +#define BTRFS_BUILD_VERSION "-" +#endif +*/ +import "C" + +func btrfsBuildVersion() string { + return string(C.BTRFS_BUILD_VERSION) +} + +func btrfsLibVersion() int { + return int(C.BTRFS_LIB_VERSION) +} diff --git a/daemon/graphdriver/btrfs/version_none.go b/daemon/graphdriver/btrfs/version_none.go new file mode 100644 index 00000000..f802fbc6 --- /dev/null +++ b/daemon/graphdriver/btrfs/version_none.go @@ -0,0 +1,14 @@ +// +build linux,btrfs_noversion + +package btrfs + +// TODO(vbatts) remove this work-around once supported linux distros are on +// btrfs utilities of >= 3.16.1 + +func btrfsBuildVersion() string { + return "-" +} + +func btrfsLibVersion() int { + return -1 +} diff --git a/daemon/graphdriver/btrfs/version_test.go b/daemon/graphdriver/btrfs/version_test.go new file mode 100644 index 00000000..15a6e75c --- /dev/null +++ b/daemon/graphdriver/btrfs/version_test.go @@ -0,0 +1,13 @@ +// +build linux,!btrfs_noversion + +package btrfs + +import ( + "testing" +) + +func TestLibVersion(t *testing.T) { + if btrfsLibVersion() <= 0 { + t.Errorf("expected output from btrfs lib version > 0") + } +} diff --git a/daemon/graphdriver/counter.go b/daemon/graphdriver/counter.go new file mode 100644 index 00000000..572fc9be --- /dev/null +++ b/daemon/graphdriver/counter.go @@ -0,0 +1,32 @@ +package graphdriver + +import "sync" + +// RefCounter is a generic counter for use by graphdriver Get/Put calls +type RefCounter struct { + counts map[string]int + mu sync.Mutex +} + +// NewRefCounter returns a new RefCounter +func NewRefCounter() *RefCounter { + return &RefCounter{counts: make(map[string]int)} +} + +// Increment increaes the ref count for the given id and returns the current count +func (c *RefCounter) Increment(id string) int { + c.mu.Lock() + c.counts[id]++ + count := c.counts[id] + c.mu.Unlock() + return count +} + +// Decrement decreases the ref count for the given id and returns the current count +func (c *RefCounter) Decrement(id string) int { + c.mu.Lock() + c.counts[id]-- + count := c.counts[id] + c.mu.Unlock() + return count +} diff --git a/daemon/graphdriver/devmapper/README.md b/daemon/graphdriver/devmapper/README.md new file mode 100644 index 00000000..8de7fc22 --- /dev/null +++ b/daemon/graphdriver/devmapper/README.md @@ -0,0 +1,96 @@ +## devicemapper - a storage backend based on Device Mapper + +### Theory of operation + +The device mapper graphdriver uses the device mapper thin provisioning +module (dm-thinp) to implement CoW snapshots. The preferred model is +to have a thin pool reserved outside of Docker and passed to the +daemon via the `--storage-opt dm.thinpooldev` option. + +As a fallback if no thin pool is provided, loopback files will be +created. Loopback is very slow, but can be used without any +pre-configuration of storage. It is strongly recommended that you do +not use loopback in production. Ensure your Docker daemon has a +`--storage-opt dm.thinpooldev` argument provided. + +In loopback, a thin pool is created at `/var/lib/docker/devicemapper` +(devicemapper graph location) based on two block devices, one for +data and one for metadata. By default these block devices are created +automatically by using loopback mounts of automatically created sparse +files. + +The default loopback files used are +`/var/lib/docker/devicemapper/devicemapper/data` and +`/var/lib/docker/devicemapper/devicemapper/metadata`. Additional metadata +required to map from docker entities to the corresponding devicemapper +volumes is stored in the `/var/lib/docker/devicemapper/devicemapper/json` +file (encoded as Json). + +In order to support multiple devicemapper graphs on a system, the thin +pool will be named something like: `docker-0:33-19478248-pool`, where +the `0:33` part is the minor/major device nr and `19478248` is the +inode number of the `/var/lib/docker/devicemapper` directory. + +On the thin pool, docker automatically creates a base thin device, +called something like `docker-0:33-19478248-base` of a fixed +size. This is automatically formatted with an empty filesystem on +creation. This device is the base of all docker images and +containers. All base images are snapshots of this device and those +images are then in turn used as snapshots for other images and +eventually containers. + +### Information on `docker info` + +As of docker-1.4.1, `docker info` when using the `devicemapper` storage driver +will display something like: + + $ sudo docker info + [...] + Storage Driver: devicemapper + Pool Name: docker-253:1-17538953-pool + Pool Blocksize: 65.54 kB + Base Device Size: 107.4 GB + Data file: /dev/loop4 + Metadata file: /dev/loop4 + Data Space Used: 2.536 GB + Data Space Total: 107.4 GB + Data Space Available: 104.8 GB + Metadata Space Used: 7.93 MB + Metadata Space Total: 2.147 GB + Metadata Space Available: 2.14 GB + Udev Sync Supported: true + Data loop file: /home/docker/devicemapper/devicemapper/data + Metadata loop file: /home/docker/devicemapper/devicemapper/metadata + Library Version: 1.02.82-git (2013-10-04) + [...] + +#### status items + +Each item in the indented section under `Storage Driver: devicemapper` are +status information about the driver. + * `Pool Name` name of the devicemapper pool for this driver. + * `Pool Blocksize` tells the blocksize the thin pool was initialized with. This only changes on creation. + * `Base Device Size` tells the maximum size of a container and image + * `Data file` blockdevice file used for the devicemapper data + * `Metadata file` blockdevice file used for the devicemapper metadata + * `Data Space Used` tells how much of `Data file` is currently used + * `Data Space Total` tells max size the `Data file` + * `Data Space Available` tells how much free space there is in the `Data file`. If you are using a loop device this will report the actual space available to the loop device on the underlying filesystem. + * `Metadata Space Used` tells how much of `Metadata file` is currently used + * `Metadata Space Total` tells max size the `Metadata file` + * `Metadata Space Available` tells how much free space there is in the `Metadata file`. If you are using a loop device this will report the actual space available to the loop device on the underlying filesystem. + * `Udev Sync Supported` tells whether devicemapper is able to sync with Udev. Should be `true`. + * `Data loop file` file attached to `Data file`, if loopback device is used + * `Metadata loop file` file attached to `Metadata file`, if loopback device is used + * `Library Version` from the libdevmapper used + +### About the devicemapper options + +The devicemapper backend supports some options that you can specify +when starting the docker daemon using the `--storage-opt` flags. +This uses the `dm` prefix and would be used something like `docker daemon --storage-opt dm.foo=bar`. + +These options are currently documented both in [the man +page](../../../man/docker.1.md) and in [the online +documentation](https://docs.docker.com/reference/commandline/daemon/#storage-driver-options). +If you add an options, update both the `man` page and the documentation. diff --git a/daemon/graphdriver/devmapper/deviceset.go b/daemon/graphdriver/devmapper/deviceset.go new file mode 100644 index 00000000..6245fdc2 --- /dev/null +++ b/daemon/graphdriver/devmapper/deviceset.go @@ -0,0 +1,2570 @@ +// +build linux + +package devmapper + +import ( + "bufio" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path" + "path/filepath" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/Sirupsen/logrus" + + "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/dockerversion" + "github.com/docker/docker/pkg/devicemapper" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/loopback" + "github.com/docker/docker/pkg/mount" + "github.com/docker/docker/pkg/parsers" + "github.com/docker/go-units" + + "github.com/opencontainers/runc/libcontainer/label" +) + +var ( + defaultDataLoopbackSize int64 = 100 * 1024 * 1024 * 1024 + defaultMetaDataLoopbackSize int64 = 2 * 1024 * 1024 * 1024 + defaultBaseFsSize uint64 = 10 * 1024 * 1024 * 1024 + defaultThinpBlockSize uint32 = 128 // 64K = 128 512b sectors + defaultUdevSyncOverride = false + maxDeviceID = 0xffffff // 24 bit, pool limit + deviceIDMapSz = (maxDeviceID + 1) / 8 + // We retry device removal so many a times that even error messages + // will fill up console during normal operation. So only log Fatal + // messages by default. + logLevel = devicemapper.LogLevelFatal + driverDeferredRemovalSupport = false + enableDeferredRemoval = false + enableDeferredDeletion = false + userBaseSize = false + defaultMinFreeSpacePercent uint32 = 10 +) + +const deviceSetMetaFile string = "deviceset-metadata" +const transactionMetaFile string = "transaction-metadata" + +type transaction struct { + OpenTransactionID uint64 `json:"open_transaction_id"` + DeviceIDHash string `json:"device_hash"` + DeviceID int `json:"device_id"` +} + +type devInfo struct { + Hash string `json:"-"` + DeviceID int `json:"device_id"` + Size uint64 `json:"size"` + TransactionID uint64 `json:"transaction_id"` + Initialized bool `json:"initialized"` + Deleted bool `json:"deleted"` + devices *DeviceSet + + // The global DeviceSet lock guarantees that we serialize all + // the calls to libdevmapper (which is not threadsafe), but we + // sometimes release that lock while sleeping. In that case + // this per-device lock is still held, protecting against + // other accesses to the device that we're doing the wait on. + // + // WARNING: In order to avoid AB-BA deadlocks when releasing + // the global lock while holding the per-device locks all + // device locks must be acquired *before* the device lock, and + // multiple device locks should be acquired parent before child. + lock sync.Mutex +} + +type metaData struct { + Devices map[string]*devInfo `json:"Devices"` +} + +// DeviceSet holds information about list of devices +type DeviceSet struct { + metaData `json:"-"` + sync.Mutex `json:"-"` // Protects all fields of DeviceSet and serializes calls into libdevmapper + root string + devicePrefix string + TransactionID uint64 `json:"-"` + NextDeviceID int `json:"next_device_id"` + deviceIDMap []byte + + // Options + dataLoopbackSize int64 + metaDataLoopbackSize int64 + baseFsSize uint64 + filesystem string + mountOptions string + mkfsArgs []string + dataDevice string // block or loop dev + dataLoopFile string // loopback file, if used + metadataDevice string // block or loop dev + metadataLoopFile string // loopback file, if used + doBlkDiscard bool + thinpBlockSize uint32 + thinPoolDevice string + transaction `json:"-"` + overrideUdevSyncCheck bool + deferredRemove bool // use deferred removal + deferredDelete bool // use deferred deletion + BaseDeviceUUID string // save UUID of base device + BaseDeviceFilesystem string // save filesystem of base device + nrDeletedDevices uint // number of deleted devices + deletionWorkerTicker *time.Ticker + uidMaps []idtools.IDMap + gidMaps []idtools.IDMap + minFreeSpacePercent uint32 //min free space percentage in thinpool +} + +// DiskUsage contains information about disk usage and is used when reporting Status of a device. +type DiskUsage struct { + // Used bytes on the disk. + Used uint64 + // Total bytes on the disk. + Total uint64 + // Available bytes on the disk. + Available uint64 +} + +// Status returns the information about the device. +type Status struct { + // PoolName is the name of the data pool. + PoolName string + // DataFile is the actual block device for data. + DataFile string + // DataLoopback loopback file, if used. + DataLoopback string + // MetadataFile is the actual block device for metadata. + MetadataFile string + // MetadataLoopback is the loopback file, if used. + MetadataLoopback string + // Data is the disk used for data. + Data DiskUsage + // Metadata is the disk used for meta data. + Metadata DiskUsage + // BaseDeviceSize is base size of container and image + BaseDeviceSize uint64 + // BaseDeviceFS is backing filesystem. + BaseDeviceFS string + // SectorSize size of the vector. + SectorSize uint64 + // UdevSyncSupported is true if sync is supported. + UdevSyncSupported bool + // DeferredRemoveEnabled is true then the device is not unmounted. + DeferredRemoveEnabled bool + // True if deferred deletion is enabled. This is different from + // deferred removal. "removal" means that device mapper device is + // deactivated. Thin device is still in thin pool and can be activated + // again. But "deletion" means that thin device will be deleted from + // thin pool and it can't be activated again. + DeferredDeleteEnabled bool + DeferredDeletedDeviceCount uint +} + +// Structure used to export image/container metadata in docker inspect. +type deviceMetadata struct { + deviceID int + deviceSize uint64 // size in bytes + deviceName string // Device name as used during activation +} + +// DevStatus returns information about device mounted containing its id, size and sector information. +type DevStatus struct { + // DeviceID is the id of the device. + DeviceID int + // Size is the size of the filesystem. + Size uint64 + // TransactionID is a unique integer per device set used to identify an operation on the file system, this number is incremental. + TransactionID uint64 + // SizeInSectors indicates the size of the sectors allocated. + SizeInSectors uint64 + // MappedSectors indicates number of mapped sectors. + MappedSectors uint64 + // HighestMappedSector is the pointer to the highest mapped sector. + HighestMappedSector uint64 +} + +func getDevName(name string) string { + return "/dev/mapper/" + name +} + +func (info *devInfo) Name() string { + hash := info.Hash + if hash == "" { + hash = "base" + } + return fmt.Sprintf("%s-%s", info.devices.devicePrefix, hash) +} + +func (info *devInfo) DevName() string { + return getDevName(info.Name()) +} + +func (devices *DeviceSet) loopbackDir() string { + return path.Join(devices.root, "devicemapper") +} + +func (devices *DeviceSet) metadataDir() string { + return path.Join(devices.root, "metadata") +} + +func (devices *DeviceSet) metadataFile(info *devInfo) string { + file := info.Hash + if file == "" { + file = "base" + } + return path.Join(devices.metadataDir(), file) +} + +func (devices *DeviceSet) transactionMetaFile() string { + return path.Join(devices.metadataDir(), transactionMetaFile) +} + +func (devices *DeviceSet) deviceSetMetaFile() string { + return path.Join(devices.metadataDir(), deviceSetMetaFile) +} + +func (devices *DeviceSet) oldMetadataFile() string { + return path.Join(devices.loopbackDir(), "json") +} + +func (devices *DeviceSet) getPoolName() string { + if devices.thinPoolDevice == "" { + return devices.devicePrefix + "-pool" + } + return devices.thinPoolDevice +} + +func (devices *DeviceSet) getPoolDevName() string { + return getDevName(devices.getPoolName()) +} + +func (devices *DeviceSet) hasImage(name string) bool { + dirname := devices.loopbackDir() + filename := path.Join(dirname, name) + + _, err := os.Stat(filename) + return err == nil +} + +// ensureImage creates a sparse file of bytes at the path +// /devicemapper/. +// If the file already exists and new size is larger than its current size, it grows to the new size. +// Either way it returns the full path. +func (devices *DeviceSet) ensureImage(name string, size int64) (string, error) { + dirname := devices.loopbackDir() + filename := path.Join(dirname, name) + + uid, gid, err := idtools.GetRootUIDGID(devices.uidMaps, devices.gidMaps) + if err != nil { + return "", err + } + if err := idtools.MkdirAllAs(dirname, 0700, uid, gid); err != nil && !os.IsExist(err) { + return "", err + } + + if fi, err := os.Stat(filename); err != nil { + if !os.IsNotExist(err) { + return "", err + } + logrus.Debugf("devmapper: Creating loopback file %s for device-manage use", filename) + file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0600) + if err != nil { + return "", err + } + defer file.Close() + + if err := file.Truncate(size); err != nil { + return "", err + } + } else { + if fi.Size() < size { + file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0600) + if err != nil { + return "", err + } + defer file.Close() + if err := file.Truncate(size); err != nil { + return "", fmt.Errorf("devmapper: Unable to grow loopback file %s: %v", filename, err) + } + } else if fi.Size() > size { + logrus.Warnf("devmapper: Can't shrink loopback file %s", filename) + } + } + return filename, nil +} + +func (devices *DeviceSet) allocateTransactionID() uint64 { + devices.OpenTransactionID = devices.TransactionID + 1 + return devices.OpenTransactionID +} + +func (devices *DeviceSet) updatePoolTransactionID() error { + if err := devicemapper.SetTransactionID(devices.getPoolDevName(), devices.TransactionID, devices.OpenTransactionID); err != nil { + return fmt.Errorf("devmapper: Error setting devmapper transaction ID: %s", err) + } + devices.TransactionID = devices.OpenTransactionID + return nil +} + +func (devices *DeviceSet) removeMetadata(info *devInfo) error { + if err := os.RemoveAll(devices.metadataFile(info)); err != nil { + return fmt.Errorf("devmapper: Error removing metadata file %s: %s", devices.metadataFile(info), err) + } + return nil +} + +// Given json data and file path, write it to disk +func (devices *DeviceSet) writeMetaFile(jsonData []byte, filePath string) error { + tmpFile, err := ioutil.TempFile(devices.metadataDir(), ".tmp") + if err != nil { + return fmt.Errorf("devmapper: Error creating metadata file: %s", err) + } + + n, err := tmpFile.Write(jsonData) + if err != nil { + return fmt.Errorf("devmapper: Error writing metadata to %s: %s", tmpFile.Name(), err) + } + if n < len(jsonData) { + return io.ErrShortWrite + } + if err := tmpFile.Sync(); err != nil { + return fmt.Errorf("devmapper: Error syncing metadata file %s: %s", tmpFile.Name(), err) + } + if err := tmpFile.Close(); err != nil { + return fmt.Errorf("devmapper: Error closing metadata file %s: %s", tmpFile.Name(), err) + } + if err := os.Rename(tmpFile.Name(), filePath); err != nil { + return fmt.Errorf("devmapper: Error committing metadata file %s: %s", tmpFile.Name(), err) + } + + return nil +} + +func (devices *DeviceSet) saveMetadata(info *devInfo) error { + jsonData, err := json.Marshal(info) + if err != nil { + return fmt.Errorf("devmapper: Error encoding metadata to json: %s", err) + } + if err := devices.writeMetaFile(jsonData, devices.metadataFile(info)); err != nil { + return err + } + return nil +} + +func (devices *DeviceSet) markDeviceIDUsed(deviceID int) { + var mask byte + i := deviceID % 8 + mask = 1 << uint(i) + devices.deviceIDMap[deviceID/8] = devices.deviceIDMap[deviceID/8] | mask +} + +func (devices *DeviceSet) markDeviceIDFree(deviceID int) { + var mask byte + i := deviceID % 8 + mask = ^(1 << uint(i)) + devices.deviceIDMap[deviceID/8] = devices.deviceIDMap[deviceID/8] & mask +} + +func (devices *DeviceSet) isDeviceIDFree(deviceID int) bool { + var mask byte + i := deviceID % 8 + mask = (1 << uint(i)) + if (devices.deviceIDMap[deviceID/8] & mask) != 0 { + return false + } + return true +} + +// Should be called with devices.Lock() held. +func (devices *DeviceSet) lookupDevice(hash string) (*devInfo, error) { + info := devices.Devices[hash] + if info == nil { + info = devices.loadMetadata(hash) + if info == nil { + return nil, fmt.Errorf("devmapper: Unknown device %s", hash) + } + + devices.Devices[hash] = info + } + return info, nil +} + +func (devices *DeviceSet) lookupDeviceWithLock(hash string) (*devInfo, error) { + devices.Lock() + defer devices.Unlock() + info, err := devices.lookupDevice(hash) + return info, err +} + +// This function relies on that device hash map has been loaded in advance. +// Should be called with devices.Lock() held. +func (devices *DeviceSet) constructDeviceIDMap() { + logrus.Debugf("devmapper: constructDeviceIDMap()") + defer logrus.Debugf("devmapper: constructDeviceIDMap() END") + + for _, info := range devices.Devices { + devices.markDeviceIDUsed(info.DeviceID) + logrus.Debugf("devmapper: Added deviceId=%d to DeviceIdMap", info.DeviceID) + } +} + +func (devices *DeviceSet) deviceFileWalkFunction(path string, finfo os.FileInfo) error { + + // Skip some of the meta files which are not device files. + if strings.HasSuffix(finfo.Name(), ".migrated") { + logrus.Debugf("devmapper: Skipping file %s", path) + return nil + } + + if strings.HasPrefix(finfo.Name(), ".") { + logrus.Debugf("devmapper: Skipping file %s", path) + return nil + } + + if finfo.Name() == deviceSetMetaFile { + logrus.Debugf("devmapper: Skipping file %s", path) + return nil + } + + if finfo.Name() == transactionMetaFile { + logrus.Debugf("devmapper: Skipping file %s", path) + return nil + } + + logrus.Debugf("devmapper: Loading data for file %s", path) + + hash := finfo.Name() + if hash == "base" { + hash = "" + } + + // Include deleted devices also as cleanup delete device logic + // will go through it and see if there are any deleted devices. + if _, err := devices.lookupDevice(hash); err != nil { + return fmt.Errorf("devmapper: Error looking up device %s:%v", hash, err) + } + + return nil +} + +func (devices *DeviceSet) loadDeviceFilesOnStart() error { + logrus.Debugf("devmapper: loadDeviceFilesOnStart()") + defer logrus.Debugf("devmapper: loadDeviceFilesOnStart() END") + + var scan = func(path string, info os.FileInfo, err error) error { + if err != nil { + logrus.Debugf("devmapper: Can't walk the file %s", path) + return nil + } + + // Skip any directories + if info.IsDir() { + return nil + } + + return devices.deviceFileWalkFunction(path, info) + } + + return filepath.Walk(devices.metadataDir(), scan) +} + +// Should be called with devices.Lock() held. +func (devices *DeviceSet) unregisterDevice(id int, hash string) error { + logrus.Debugf("devmapper: unregisterDevice(%v, %v)", id, hash) + info := &devInfo{ + Hash: hash, + DeviceID: id, + } + + delete(devices.Devices, hash) + + if err := devices.removeMetadata(info); err != nil { + logrus.Debugf("devmapper: Error removing metadata: %s", err) + return err + } + + return nil +} + +// Should be called with devices.Lock() held. +func (devices *DeviceSet) registerDevice(id int, hash string, size uint64, transactionID uint64) (*devInfo, error) { + logrus.Debugf("devmapper: registerDevice(%v, %v)", id, hash) + info := &devInfo{ + Hash: hash, + DeviceID: id, + Size: size, + TransactionID: transactionID, + Initialized: false, + devices: devices, + } + + devices.Devices[hash] = info + + if err := devices.saveMetadata(info); err != nil { + // Try to remove unused device + delete(devices.Devices, hash) + return nil, err + } + + return info, nil +} + +func (devices *DeviceSet) activateDeviceIfNeeded(info *devInfo, ignoreDeleted bool) error { + logrus.Debugf("devmapper: activateDeviceIfNeeded(%v)", info.Hash) + + if info.Deleted && !ignoreDeleted { + return fmt.Errorf("devmapper: Can't activate device %v as it is marked for deletion", info.Hash) + } + + // Make sure deferred removal on device is canceled, if one was + // scheduled. + if err := devices.cancelDeferredRemoval(info); err != nil { + return fmt.Errorf("devmapper: Device Deferred Removal Cancellation Failed: %s", err) + } + + if devinfo, _ := devicemapper.GetInfo(info.Name()); devinfo != nil && devinfo.Exists != 0 { + return nil + } + + return devicemapper.ActivateDevice(devices.getPoolDevName(), info.Name(), info.DeviceID, info.Size) +} + +// Return true only if kernel supports xfs and mkfs.xfs is available +func xfsSupported() bool { + // Make sure mkfs.xfs is available + if _, err := exec.LookPath("mkfs.xfs"); err != nil { + return false + } + + // Check if kernel supports xfs filesystem or not. + exec.Command("modprobe", "xfs").Run() + + f, err := os.Open("/proc/filesystems") + if err != nil { + logrus.Warnf("devmapper: Could not check if xfs is supported: %v", err) + return false + } + defer f.Close() + + s := bufio.NewScanner(f) + for s.Scan() { + if strings.HasSuffix(s.Text(), "\txfs") { + return true + } + } + + if err := s.Err(); err != nil { + logrus.Warnf("devmapper: Could not check if xfs is supported: %v", err) + } + return false +} + +func determineDefaultFS() string { + if xfsSupported() { + return "xfs" + } + + logrus.Warn("devmapper: XFS is not supported in your system. Either the kernel doesn't support it or mkfs.xfs is not in your PATH. Defaulting to ext4 filesystem") + return "ext4" +} + +func (devices *DeviceSet) createFilesystem(info *devInfo) (err error) { + devname := info.DevName() + + args := []string{} + for _, arg := range devices.mkfsArgs { + args = append(args, arg) + } + + args = append(args, devname) + + if devices.filesystem == "" { + devices.filesystem = determineDefaultFS() + } + if err := devices.saveBaseDeviceFilesystem(devices.filesystem); err != nil { + return err + } + + logrus.Infof("devmapper: Creating filesystem %s on device %s", devices.filesystem, info.Name()) + defer func() { + if err != nil { + logrus.Infof("devmapper: Error while creating filesystem %s on device %s: %v", devices.filesystem, info.Name(), err) + } else { + logrus.Infof("devmapper: Successfully created filesystem %s on device %s", devices.filesystem, info.Name()) + } + }() + + switch devices.filesystem { + case "xfs": + err = exec.Command("mkfs.xfs", args...).Run() + case "ext4": + err = exec.Command("mkfs.ext4", append([]string{"-E", "nodiscard,lazy_itable_init=0,lazy_journal_init=0"}, args...)...).Run() + if err != nil { + err = exec.Command("mkfs.ext4", append([]string{"-E", "nodiscard,lazy_itable_init=0"}, args...)...).Run() + } + if err != nil { + return err + } + err = exec.Command("tune2fs", append([]string{"-c", "-1", "-i", "0"}, devname)...).Run() + default: + err = fmt.Errorf("devmapper: Unsupported filesystem type %s", devices.filesystem) + } + return +} + +func (devices *DeviceSet) migrateOldMetaData() error { + // Migrate old metadata file + jsonData, err := ioutil.ReadFile(devices.oldMetadataFile()) + if err != nil && !os.IsNotExist(err) { + return err + } + + if jsonData != nil { + m := metaData{Devices: make(map[string]*devInfo)} + + if err := json.Unmarshal(jsonData, &m); err != nil { + return err + } + + for hash, info := range m.Devices { + info.Hash = hash + devices.saveMetadata(info) + } + if err := os.Rename(devices.oldMetadataFile(), devices.oldMetadataFile()+".migrated"); err != nil { + return err + } + + } + + return nil +} + +// Cleanup deleted devices. It assumes that all the devices have been +// loaded in the hash table. +func (devices *DeviceSet) cleanupDeletedDevices() error { + devices.Lock() + + // If there are no deleted devices, there is nothing to do. + if devices.nrDeletedDevices == 0 { + devices.Unlock() + return nil + } + + var deletedDevices []*devInfo + + for _, info := range devices.Devices { + if !info.Deleted { + continue + } + logrus.Debugf("devmapper: Found deleted device %s.", info.Hash) + deletedDevices = append(deletedDevices, info) + } + + // Delete the deleted devices. DeleteDevice() first takes the info lock + // and then devices.Lock(). So drop it to avoid deadlock. + devices.Unlock() + + for _, info := range deletedDevices { + // This will again try deferred deletion. + if err := devices.DeleteDevice(info.Hash, false); err != nil { + logrus.Warnf("devmapper: Deletion of device %s, device_id=%v failed:%v", info.Hash, info.DeviceID, err) + } + } + + return nil +} + +func (devices *DeviceSet) countDeletedDevices() { + for _, info := range devices.Devices { + if !info.Deleted { + continue + } + devices.nrDeletedDevices++ + } +} + +func (devices *DeviceSet) startDeviceDeletionWorker() { + // Deferred deletion is not enabled. Don't do anything. + if !devices.deferredDelete { + return + } + + logrus.Debugf("devmapper: Worker to cleanup deleted devices started") + for range devices.deletionWorkerTicker.C { + devices.cleanupDeletedDevices() + } +} + +func (devices *DeviceSet) initMetaData() error { + devices.Lock() + defer devices.Unlock() + + if err := devices.migrateOldMetaData(); err != nil { + return err + } + + _, transactionID, _, _, _, _, err := devices.poolStatus() + if err != nil { + return err + } + + devices.TransactionID = transactionID + + if err := devices.loadDeviceFilesOnStart(); err != nil { + return fmt.Errorf("devmapper: Failed to load device files:%v", err) + } + + devices.constructDeviceIDMap() + devices.countDeletedDevices() + + if err := devices.processPendingTransaction(); err != nil { + return err + } + + // Start a goroutine to cleanup Deleted Devices + go devices.startDeviceDeletionWorker() + return nil +} + +func (devices *DeviceSet) incNextDeviceID() { + // IDs are 24bit, so wrap around + devices.NextDeviceID = (devices.NextDeviceID + 1) & maxDeviceID +} + +func (devices *DeviceSet) getNextFreeDeviceID() (int, error) { + devices.incNextDeviceID() + for i := 0; i <= maxDeviceID; i++ { + if devices.isDeviceIDFree(devices.NextDeviceID) { + devices.markDeviceIDUsed(devices.NextDeviceID) + return devices.NextDeviceID, nil + } + devices.incNextDeviceID() + } + + return 0, fmt.Errorf("devmapper: Unable to find a free device ID") +} + +func (devices *DeviceSet) poolHasFreeSpace() error { + if devices.minFreeSpacePercent == 0 { + return nil + } + + _, _, dataUsed, dataTotal, metadataUsed, metadataTotal, err := devices.poolStatus() + if err != nil { + return err + } + + minFreeData := (dataTotal * uint64(devices.minFreeSpacePercent)) / 100 + if minFreeData < 1 { + minFreeData = 1 + } + dataFree := dataTotal - dataUsed + if dataFree < minFreeData { + return fmt.Errorf("devmapper: Thin Pool has %v free data blocks which is less than minimum required %v free data blocks. Create more free space in thin pool or use dm.min_free_space option to change behavior", (dataTotal - dataUsed), minFreeData) + } + + minFreeMetadata := (metadataTotal * uint64(devices.minFreeSpacePercent)) / 100 + if minFreeMetadata < 1 { + minFreeMetadata = 1 + } + + metadataFree := metadataTotal - metadataUsed + if metadataFree < minFreeMetadata { + return fmt.Errorf("devmapper: Thin Pool has %v free metadata blocks which is less than minimum required %v free metadata blocks. Create more free metadata space in thin pool or use dm.min_free_space option to change behavior", (metadataTotal - metadataUsed), minFreeMetadata) + } + + return nil +} + +func (devices *DeviceSet) createRegisterDevice(hash string) (*devInfo, error) { + devices.Lock() + defer devices.Unlock() + + deviceID, err := devices.getNextFreeDeviceID() + if err != nil { + return nil, err + } + + if err := devices.openTransaction(hash, deviceID); err != nil { + logrus.Debugf("devmapper: Error opening transaction hash = %s deviceID = %d", hash, deviceID) + devices.markDeviceIDFree(deviceID) + return nil, err + } + + for { + if err := devicemapper.CreateDevice(devices.getPoolDevName(), deviceID); err != nil { + if devicemapper.DeviceIDExists(err) { + // Device ID already exists. This should not + // happen. Now we have a mechanism to find + // a free device ID. So something is not right. + // Give a warning and continue. + logrus.Errorf("devmapper: Device ID %d exists in pool but it is supposed to be unused", deviceID) + deviceID, err = devices.getNextFreeDeviceID() + if err != nil { + return nil, err + } + // Save new device id into transaction + devices.refreshTransaction(deviceID) + continue + } + logrus.Debugf("devmapper: Error creating device: %s", err) + devices.markDeviceIDFree(deviceID) + return nil, err + } + break + } + + logrus.Debugf("devmapper: Registering device (id %v) with FS size %v", deviceID, devices.baseFsSize) + info, err := devices.registerDevice(deviceID, hash, devices.baseFsSize, devices.OpenTransactionID) + if err != nil { + _ = devicemapper.DeleteDevice(devices.getPoolDevName(), deviceID) + devices.markDeviceIDFree(deviceID) + return nil, err + } + + if err := devices.closeTransaction(); err != nil { + devices.unregisterDevice(deviceID, hash) + devicemapper.DeleteDevice(devices.getPoolDevName(), deviceID) + devices.markDeviceIDFree(deviceID) + return nil, err + } + return info, nil +} + +func (devices *DeviceSet) createRegisterSnapDevice(hash string, baseInfo *devInfo) error { + if err := devices.poolHasFreeSpace(); err != nil { + return err + } + + deviceID, err := devices.getNextFreeDeviceID() + if err != nil { + return err + } + + if err := devices.openTransaction(hash, deviceID); err != nil { + logrus.Debugf("devmapper: Error opening transaction hash = %s deviceID = %d", hash, deviceID) + devices.markDeviceIDFree(deviceID) + return err + } + + for { + if err := devicemapper.CreateSnapDevice(devices.getPoolDevName(), deviceID, baseInfo.Name(), baseInfo.DeviceID); err != nil { + if devicemapper.DeviceIDExists(err) { + // Device ID already exists. This should not + // happen. Now we have a mechanism to find + // a free device ID. So something is not right. + // Give a warning and continue. + logrus.Errorf("devmapper: Device ID %d exists in pool but it is supposed to be unused", deviceID) + deviceID, err = devices.getNextFreeDeviceID() + if err != nil { + return err + } + // Save new device id into transaction + devices.refreshTransaction(deviceID) + continue + } + logrus.Debugf("devmapper: Error creating snap device: %s", err) + devices.markDeviceIDFree(deviceID) + return err + } + break + } + + if _, err := devices.registerDevice(deviceID, hash, baseInfo.Size, devices.OpenTransactionID); err != nil { + devicemapper.DeleteDevice(devices.getPoolDevName(), deviceID) + devices.markDeviceIDFree(deviceID) + logrus.Debugf("devmapper: Error registering device: %s", err) + return err + } + + if err := devices.closeTransaction(); err != nil { + devices.unregisterDevice(deviceID, hash) + devicemapper.DeleteDevice(devices.getPoolDevName(), deviceID) + devices.markDeviceIDFree(deviceID) + return err + } + return nil +} + +func (devices *DeviceSet) loadMetadata(hash string) *devInfo { + info := &devInfo{Hash: hash, devices: devices} + + jsonData, err := ioutil.ReadFile(devices.metadataFile(info)) + if err != nil { + return nil + } + + if err := json.Unmarshal(jsonData, &info); err != nil { + return nil + } + + if info.DeviceID > maxDeviceID { + logrus.Errorf("devmapper: Ignoring Invalid DeviceId=%d", info.DeviceID) + return nil + } + + return info +} + +func getDeviceUUID(device string) (string, error) { + out, err := exec.Command("blkid", "-s", "UUID", "-o", "value", device).Output() + if err != nil { + return "", fmt.Errorf("devmapper: Failed to find uuid for device %s:%v", device, err) + } + + uuid := strings.TrimSuffix(string(out), "\n") + uuid = strings.TrimSpace(uuid) + logrus.Debugf("devmapper: UUID for device: %s is:%s", device, uuid) + return uuid, nil +} + +func (devices *DeviceSet) getBaseDeviceSize() uint64 { + info, _ := devices.lookupDevice("") + if info == nil { + return 0 + } + return info.Size +} + +func (devices *DeviceSet) getBaseDeviceFS() string { + return devices.BaseDeviceFilesystem +} + +func (devices *DeviceSet) verifyBaseDeviceUUIDFS(baseInfo *devInfo) error { + devices.Lock() + defer devices.Unlock() + + if err := devices.activateDeviceIfNeeded(baseInfo, false); err != nil { + return err + } + defer devices.deactivateDevice(baseInfo) + + uuid, err := getDeviceUUID(baseInfo.DevName()) + if err != nil { + return err + } + + if devices.BaseDeviceUUID != uuid { + return fmt.Errorf("devmapper: Current Base Device UUID:%s does not match with stored UUID:%s. Possibly using a different thin pool than last invocation", uuid, devices.BaseDeviceUUID) + } + + if devices.BaseDeviceFilesystem == "" { + fsType, err := ProbeFsType(baseInfo.DevName()) + if err != nil { + return err + } + if err := devices.saveBaseDeviceFilesystem(fsType); err != nil { + return err + } + } + + // If user specified a filesystem using dm.fs option and current + // file system of base image is not same, warn user that dm.fs + // will be ignored. + if devices.BaseDeviceFilesystem != devices.filesystem { + logrus.Warnf("devmapper: Base device already exists and has filesystem %s on it. User specified filesystem %s will be ignored.", devices.BaseDeviceFilesystem, devices.filesystem) + devices.filesystem = devices.BaseDeviceFilesystem + } + return nil +} + +func (devices *DeviceSet) saveBaseDeviceFilesystem(fs string) error { + devices.BaseDeviceFilesystem = fs + return devices.saveDeviceSetMetaData() +} + +func (devices *DeviceSet) saveBaseDeviceUUID(baseInfo *devInfo) error { + devices.Lock() + defer devices.Unlock() + + if err := devices.activateDeviceIfNeeded(baseInfo, false); err != nil { + return err + } + defer devices.deactivateDevice(baseInfo) + + uuid, err := getDeviceUUID(baseInfo.DevName()) + if err != nil { + return err + } + + devices.BaseDeviceUUID = uuid + return devices.saveDeviceSetMetaData() +} + +func (devices *DeviceSet) createBaseImage() error { + logrus.Debugf("devmapper: Initializing base device-mapper thin volume") + + // Create initial device + info, err := devices.createRegisterDevice("") + if err != nil { + return err + } + + logrus.Debugf("devmapper: Creating filesystem on base device-mapper thin volume") + + if err := devices.activateDeviceIfNeeded(info, false); err != nil { + return err + } + + if err := devices.createFilesystem(info); err != nil { + return err + } + + info.Initialized = true + if err := devices.saveMetadata(info); err != nil { + info.Initialized = false + return err + } + + if err := devices.saveBaseDeviceUUID(info); err != nil { + return fmt.Errorf("devmapper: Could not query and save base device UUID:%v", err) + } + + return nil +} + +// Returns if thin pool device exists or not. If device exists, also makes +// sure it is a thin pool device and not some other type of device. +func (devices *DeviceSet) thinPoolExists(thinPoolDevice string) (bool, error) { + logrus.Debugf("devmapper: Checking for existence of the pool %s", thinPoolDevice) + + info, err := devicemapper.GetInfo(thinPoolDevice) + if err != nil { + return false, fmt.Errorf("devmapper: GetInfo() on device %s failed: %v", thinPoolDevice, err) + } + + // Device does not exist. + if info.Exists == 0 { + return false, nil + } + + _, _, deviceType, _, err := devicemapper.GetStatus(thinPoolDevice) + if err != nil { + return false, fmt.Errorf("devmapper: GetStatus() on device %s failed: %v", thinPoolDevice, err) + } + + if deviceType != "thin-pool" { + return false, fmt.Errorf("devmapper: Device %s is not a thin pool", thinPoolDevice) + } + + return true, nil +} + +func (devices *DeviceSet) checkThinPool() error { + _, transactionID, dataUsed, _, _, _, err := devices.poolStatus() + if err != nil { + return err + } + if dataUsed != 0 { + return fmt.Errorf("devmapper: Unable to take ownership of thin-pool (%s) that already has used data blocks", + devices.thinPoolDevice) + } + if transactionID != 0 { + return fmt.Errorf("devmapper: Unable to take ownership of thin-pool (%s) with non-zero transaction ID", + devices.thinPoolDevice) + } + return nil +} + +// Base image is initialized properly. Either save UUID for first time (for +// upgrade case or verify UUID. +func (devices *DeviceSet) setupVerifyBaseImageUUIDFS(baseInfo *devInfo) error { + // If BaseDeviceUUID is nil (upgrade case), save it and return success. + if devices.BaseDeviceUUID == "" { + if err := devices.saveBaseDeviceUUID(baseInfo); err != nil { + return fmt.Errorf("devmapper: Could not query and save base device UUID:%v", err) + } + return nil + } + + if err := devices.verifyBaseDeviceUUIDFS(baseInfo); err != nil { + return fmt.Errorf("devmapper: Base Device UUID and Filesystem verification failed.%v", err) + } + + return nil +} + +func (devices *DeviceSet) checkGrowBaseDeviceFS(info *devInfo) error { + + if !userBaseSize { + return nil + } + + if devices.baseFsSize < devices.getBaseDeviceSize() { + return fmt.Errorf("devmapper: Base device size cannot be smaller than %s", units.HumanSize(float64(devices.getBaseDeviceSize()))) + } + + if devices.baseFsSize == devices.getBaseDeviceSize() { + return nil + } + + info.lock.Lock() + defer info.lock.Unlock() + + devices.Lock() + defer devices.Unlock() + + info.Size = devices.baseFsSize + + if err := devices.saveMetadata(info); err != nil { + // Try to remove unused device + delete(devices.Devices, info.Hash) + return err + } + + return devices.growFS(info) +} + +func (devices *DeviceSet) growFS(info *devInfo) error { + if err := devices.activateDeviceIfNeeded(info, false); err != nil { + return fmt.Errorf("Error activating devmapper device: %s", err) + } + + defer devices.deactivateDevice(info) + + fsMountPoint := "/run/docker/mnt" + if _, err := os.Stat(fsMountPoint); os.IsNotExist(err) { + if err := os.MkdirAll(fsMountPoint, 0700); err != nil { + return err + } + defer os.RemoveAll(fsMountPoint) + } + + options := "" + if devices.BaseDeviceFilesystem == "xfs" { + // XFS needs nouuid or it can't mount filesystems with the same fs + options = joinMountOptions(options, "nouuid") + } + options = joinMountOptions(options, devices.mountOptions) + + if err := mount.Mount(info.DevName(), fsMountPoint, devices.BaseDeviceFilesystem, options); err != nil { + return fmt.Errorf("Error mounting '%s' on '%s': %s", info.DevName(), fsMountPoint, err) + } + + defer syscall.Unmount(fsMountPoint, syscall.MNT_DETACH) + + switch devices.BaseDeviceFilesystem { + case "ext4": + if out, err := exec.Command("resize2fs", info.DevName()).CombinedOutput(); err != nil { + return fmt.Errorf("Failed to grow rootfs:%v:%s", err, string(out)) + } + case "xfs": + if out, err := exec.Command("xfs_growfs", info.DevName()).CombinedOutput(); err != nil { + return fmt.Errorf("Failed to grow rootfs:%v:%s", err, string(out)) + } + default: + return fmt.Errorf("Unsupported filesystem type %s", devices.BaseDeviceFilesystem) + } + return nil +} + +func (devices *DeviceSet) setupBaseImage() error { + oldInfo, _ := devices.lookupDeviceWithLock("") + + // base image already exists. If it is initialized properly, do UUID + // verification and return. Otherwise remove image and set it up + // fresh. + + if oldInfo != nil { + if oldInfo.Initialized && !oldInfo.Deleted { + if err := devices.setupVerifyBaseImageUUIDFS(oldInfo); err != nil { + return err + } + + if err := devices.checkGrowBaseDeviceFS(oldInfo); err != nil { + return err + } + + return nil + } + + logrus.Debugf("devmapper: Removing uninitialized base image") + // If previous base device is in deferred delete state, + // that needs to be cleaned up first. So don't try + // deferred deletion. + if err := devices.DeleteDevice("", true); err != nil { + return err + } + } + + // If we are setting up base image for the first time, make sure + // thin pool is empty. + if devices.thinPoolDevice != "" && oldInfo == nil { + if err := devices.checkThinPool(); err != nil { + return err + } + } + + // Create new base image device + if err := devices.createBaseImage(); err != nil { + return err + } + + return nil +} + +func setCloseOnExec(name string) { + if fileInfos, _ := ioutil.ReadDir("/proc/self/fd"); fileInfos != nil { + for _, i := range fileInfos { + link, _ := os.Readlink(filepath.Join("/proc/self/fd", i.Name())) + if link == name { + fd, err := strconv.Atoi(i.Name()) + if err == nil { + syscall.CloseOnExec(fd) + } + } + } + } +} + +// DMLog implements logging using DevMapperLogger interface. +func (devices *DeviceSet) DMLog(level int, file string, line int, dmError int, message string) { + // By default libdm sends us all the messages including debug ones. + // We need to filter out messages here and figure out which one + // should be printed. + if level > logLevel { + return + } + + // FIXME(vbatts) push this back into ./pkg/devicemapper/ + if level <= devicemapper.LogLevelErr { + logrus.Errorf("libdevmapper(%d): %s:%d (%d) %s", level, file, line, dmError, message) + } else if level <= devicemapper.LogLevelInfo { + logrus.Infof("libdevmapper(%d): %s:%d (%d) %s", level, file, line, dmError, message) + } else { + // FIXME(vbatts) push this back into ./pkg/devicemapper/ + logrus.Debugf("libdevmapper(%d): %s:%d (%d) %s", level, file, line, dmError, message) + } +} + +func major(device uint64) uint64 { + return (device >> 8) & 0xfff +} + +func minor(device uint64) uint64 { + return (device & 0xff) | ((device >> 12) & 0xfff00) +} + +// ResizePool increases the size of the pool. +func (devices *DeviceSet) ResizePool(size int64) error { + dirname := devices.loopbackDir() + datafilename := path.Join(dirname, "data") + if len(devices.dataDevice) > 0 { + datafilename = devices.dataDevice + } + metadatafilename := path.Join(dirname, "metadata") + if len(devices.metadataDevice) > 0 { + metadatafilename = devices.metadataDevice + } + + datafile, err := os.OpenFile(datafilename, os.O_RDWR, 0) + if datafile == nil { + return err + } + defer datafile.Close() + + fi, err := datafile.Stat() + if fi == nil { + return err + } + + if fi.Size() > size { + return fmt.Errorf("devmapper: Can't shrink file") + } + + dataloopback := loopback.FindLoopDeviceFor(datafile) + if dataloopback == nil { + return fmt.Errorf("devmapper: Unable to find loopback mount for: %s", datafilename) + } + defer dataloopback.Close() + + metadatafile, err := os.OpenFile(metadatafilename, os.O_RDWR, 0) + if metadatafile == nil { + return err + } + defer metadatafile.Close() + + metadataloopback := loopback.FindLoopDeviceFor(metadatafile) + if metadataloopback == nil { + return fmt.Errorf("devmapper: Unable to find loopback mount for: %s", metadatafilename) + } + defer metadataloopback.Close() + + // Grow loopback file + if err := datafile.Truncate(size); err != nil { + return fmt.Errorf("devmapper: Unable to grow loopback file: %s", err) + } + + // Reload size for loopback device + if err := loopback.SetCapacity(dataloopback); err != nil { + return fmt.Errorf("Unable to update loopback capacity: %s", err) + } + + // Suspend the pool + if err := devicemapper.SuspendDevice(devices.getPoolName()); err != nil { + return fmt.Errorf("devmapper: Unable to suspend pool: %s", err) + } + + // Reload with the new block sizes + if err := devicemapper.ReloadPool(devices.getPoolName(), dataloopback, metadataloopback, devices.thinpBlockSize); err != nil { + return fmt.Errorf("devmapper: Unable to reload pool: %s", err) + } + + // Resume the pool + if err := devicemapper.ResumeDevice(devices.getPoolName()); err != nil { + return fmt.Errorf("devmapper: Unable to resume pool: %s", err) + } + + return nil +} + +func (devices *DeviceSet) loadTransactionMetaData() error { + jsonData, err := ioutil.ReadFile(devices.transactionMetaFile()) + if err != nil { + // There is no active transaction. This will be the case + // during upgrade. + if os.IsNotExist(err) { + devices.OpenTransactionID = devices.TransactionID + return nil + } + return err + } + + json.Unmarshal(jsonData, &devices.transaction) + return nil +} + +func (devices *DeviceSet) saveTransactionMetaData() error { + jsonData, err := json.Marshal(&devices.transaction) + if err != nil { + return fmt.Errorf("devmapper: Error encoding metadata to json: %s", err) + } + + return devices.writeMetaFile(jsonData, devices.transactionMetaFile()) +} + +func (devices *DeviceSet) removeTransactionMetaData() error { + if err := os.RemoveAll(devices.transactionMetaFile()); err != nil { + return err + } + return nil +} + +func (devices *DeviceSet) rollbackTransaction() error { + logrus.Debugf("devmapper: Rolling back open transaction: TransactionID=%d hash=%s device_id=%d", devices.OpenTransactionID, devices.DeviceIDHash, devices.DeviceID) + + // A device id might have already been deleted before transaction + // closed. In that case this call will fail. Just leave a message + // in case of failure. + if err := devicemapper.DeleteDevice(devices.getPoolDevName(), devices.DeviceID); err != nil { + logrus.Errorf("devmapper: Unable to delete device: %s", err) + } + + dinfo := &devInfo{Hash: devices.DeviceIDHash} + if err := devices.removeMetadata(dinfo); err != nil { + logrus.Errorf("devmapper: Unable to remove metadata: %s", err) + } else { + devices.markDeviceIDFree(devices.DeviceID) + } + + if err := devices.removeTransactionMetaData(); err != nil { + logrus.Errorf("devmapper: Unable to remove transaction meta file %s: %s", devices.transactionMetaFile(), err) + } + + return nil +} + +func (devices *DeviceSet) processPendingTransaction() error { + if err := devices.loadTransactionMetaData(); err != nil { + return err + } + + // If there was open transaction but pool transaction ID is same + // as open transaction ID, nothing to roll back. + if devices.TransactionID == devices.OpenTransactionID { + return nil + } + + // If open transaction ID is less than pool transaction ID, something + // is wrong. Bail out. + if devices.OpenTransactionID < devices.TransactionID { + logrus.Errorf("devmapper: Open Transaction id %d is less than pool transaction id %d", devices.OpenTransactionID, devices.TransactionID) + return nil + } + + // Pool transaction ID is not same as open transaction. There is + // a transaction which was not completed. + if err := devices.rollbackTransaction(); err != nil { + return fmt.Errorf("devmapper: Rolling back open transaction failed: %s", err) + } + + devices.OpenTransactionID = devices.TransactionID + return nil +} + +func (devices *DeviceSet) loadDeviceSetMetaData() error { + jsonData, err := ioutil.ReadFile(devices.deviceSetMetaFile()) + if err != nil { + // For backward compatibility return success if file does + // not exist. + if os.IsNotExist(err) { + return nil + } + return err + } + + return json.Unmarshal(jsonData, devices) +} + +func (devices *DeviceSet) saveDeviceSetMetaData() error { + jsonData, err := json.Marshal(devices) + if err != nil { + return fmt.Errorf("devmapper: Error encoding metadata to json: %s", err) + } + + return devices.writeMetaFile(jsonData, devices.deviceSetMetaFile()) +} + +func (devices *DeviceSet) openTransaction(hash string, DeviceID int) error { + devices.allocateTransactionID() + devices.DeviceIDHash = hash + devices.DeviceID = DeviceID + if err := devices.saveTransactionMetaData(); err != nil { + return fmt.Errorf("devmapper: Error saving transaction metadata: %s", err) + } + return nil +} + +func (devices *DeviceSet) refreshTransaction(DeviceID int) error { + devices.DeviceID = DeviceID + if err := devices.saveTransactionMetaData(); err != nil { + return fmt.Errorf("devmapper: Error saving transaction metadata: %s", err) + } + return nil +} + +func (devices *DeviceSet) closeTransaction() error { + if err := devices.updatePoolTransactionID(); err != nil { + logrus.Debugf("devmapper: Failed to close Transaction") + return err + } + return nil +} + +func determineDriverCapabilities(version string) error { + /* + * Driver version 4.27.0 and greater support deferred activation + * feature. + */ + + logrus.Debugf("devicemapper: driver version is %s", version) + + versionSplit := strings.Split(version, ".") + major, err := strconv.Atoi(versionSplit[0]) + if err != nil { + return graphdriver.ErrNotSupported + } + + if major > 4 { + driverDeferredRemovalSupport = true + return nil + } + + if major < 4 { + return nil + } + + minor, err := strconv.Atoi(versionSplit[1]) + if err != nil { + return graphdriver.ErrNotSupported + } + + /* + * If major is 4 and minor is 27, then there is no need to + * check for patch level as it can not be less than 0. + */ + if minor >= 27 { + driverDeferredRemovalSupport = true + return nil + } + + return nil +} + +// Determine the major and minor number of loopback device +func getDeviceMajorMinor(file *os.File) (uint64, uint64, error) { + stat, err := file.Stat() + if err != nil { + return 0, 0, err + } + + dev := stat.Sys().(*syscall.Stat_t).Rdev + majorNum := major(dev) + minorNum := minor(dev) + + logrus.Debugf("devmapper: Major:Minor for device: %s is:%v:%v", file.Name(), majorNum, minorNum) + return majorNum, minorNum, nil +} + +// Given a file which is backing file of a loop back device, find the +// loopback device name and its major/minor number. +func getLoopFileDeviceMajMin(filename string) (string, uint64, uint64, error) { + file, err := os.Open(filename) + if err != nil { + logrus.Debugf("devmapper: Failed to open file %s", filename) + return "", 0, 0, err + } + + defer file.Close() + loopbackDevice := loopback.FindLoopDeviceFor(file) + if loopbackDevice == nil { + return "", 0, 0, fmt.Errorf("devmapper: Unable to find loopback mount for: %s", filename) + } + defer loopbackDevice.Close() + + Major, Minor, err := getDeviceMajorMinor(loopbackDevice) + if err != nil { + return "", 0, 0, err + } + return loopbackDevice.Name(), Major, Minor, nil +} + +// Get the major/minor numbers of thin pool data and metadata devices +func (devices *DeviceSet) getThinPoolDataMetaMajMin() (uint64, uint64, uint64, uint64, error) { + var params, poolDataMajMin, poolMetadataMajMin string + + _, _, _, params, err := devicemapper.GetTable(devices.getPoolName()) + if err != nil { + return 0, 0, 0, 0, err + } + + if _, err = fmt.Sscanf(params, "%s %s", &poolMetadataMajMin, &poolDataMajMin); err != nil { + return 0, 0, 0, 0, err + } + + logrus.Debugf("devmapper: poolDataMajMin=%s poolMetaMajMin=%s\n", poolDataMajMin, poolMetadataMajMin) + + poolDataMajMinorSplit := strings.Split(poolDataMajMin, ":") + poolDataMajor, err := strconv.ParseUint(poolDataMajMinorSplit[0], 10, 32) + if err != nil { + return 0, 0, 0, 0, err + } + + poolDataMinor, err := strconv.ParseUint(poolDataMajMinorSplit[1], 10, 32) + if err != nil { + return 0, 0, 0, 0, err + } + + poolMetadataMajMinorSplit := strings.Split(poolMetadataMajMin, ":") + poolMetadataMajor, err := strconv.ParseUint(poolMetadataMajMinorSplit[0], 10, 32) + if err != nil { + return 0, 0, 0, 0, err + } + + poolMetadataMinor, err := strconv.ParseUint(poolMetadataMajMinorSplit[1], 10, 32) + if err != nil { + return 0, 0, 0, 0, err + } + + return poolDataMajor, poolDataMinor, poolMetadataMajor, poolMetadataMinor, nil +} + +func (devices *DeviceSet) loadThinPoolLoopBackInfo() error { + poolDataMajor, poolDataMinor, poolMetadataMajor, poolMetadataMinor, err := devices.getThinPoolDataMetaMajMin() + if err != nil { + return err + } + + dirname := devices.loopbackDir() + + // data device has not been passed in. So there should be a data file + // which is being mounted as loop device. + if devices.dataDevice == "" { + datafilename := path.Join(dirname, "data") + dataLoopDevice, dataMajor, dataMinor, err := getLoopFileDeviceMajMin(datafilename) + if err != nil { + return err + } + + // Compare the two + if poolDataMajor == dataMajor && poolDataMinor == dataMinor { + devices.dataDevice = dataLoopDevice + devices.dataLoopFile = datafilename + } + + } + + // metadata device has not been passed in. So there should be a + // metadata file which is being mounted as loop device. + if devices.metadataDevice == "" { + metadatafilename := path.Join(dirname, "metadata") + metadataLoopDevice, metadataMajor, metadataMinor, err := getLoopFileDeviceMajMin(metadatafilename) + if err != nil { + return err + } + if poolMetadataMajor == metadataMajor && poolMetadataMinor == metadataMinor { + devices.metadataDevice = metadataLoopDevice + devices.metadataLoopFile = metadatafilename + } + } + + return nil +} + +func (devices *DeviceSet) initDevmapper(doInit bool) error { + // give ourselves to libdm as a log handler + devicemapper.LogInit(devices) + + version, err := devicemapper.GetDriverVersion() + if err != nil { + // Can't even get driver version, assume not supported + return graphdriver.ErrNotSupported + } + + if err := determineDriverCapabilities(version); err != nil { + return graphdriver.ErrNotSupported + } + + // If user asked for deferred removal then check both libdm library + // and kernel driver support deferred removal otherwise error out. + if enableDeferredRemoval { + if !driverDeferredRemovalSupport { + return fmt.Errorf("devmapper: Deferred removal can not be enabled as kernel does not support it") + } + if !devicemapper.LibraryDeferredRemovalSupport { + return fmt.Errorf("devmapper: Deferred removal can not be enabled as libdm does not support it") + } + logrus.Debugf("devmapper: Deferred removal support enabled.") + devices.deferredRemove = true + } + + if enableDeferredDeletion { + if !devices.deferredRemove { + return fmt.Errorf("devmapper: Deferred deletion can not be enabled as deferred removal is not enabled. Enable deferred removal using --storage-opt dm.use_deferred_removal=true parameter") + } + logrus.Debugf("devmapper: Deferred deletion support enabled.") + devices.deferredDelete = true + } + + // https://github.com/docker/docker/issues/4036 + if supported := devicemapper.UdevSetSyncSupport(true); !supported { + if dockerversion.IAmStatic == "true" { + logrus.Errorf("devmapper: Udev sync is not supported. This will lead to data loss and unexpected behavior. Install a dynamic binary to use devicemapper or select a different storage driver. For more information, see https://docs.docker.com/engine/reference/commandline/daemon/#daemon-storage-driver-option") + } else { + logrus.Errorf("devmapper: Udev sync is not supported. This will lead to data loss and unexpected behavior. Install a more recent version of libdevmapper or select a different storage driver. For more information, see https://docs.docker.com/engine/reference/commandline/daemon/#daemon-storage-driver-option") + } + + if !devices.overrideUdevSyncCheck { + return graphdriver.ErrNotSupported + } + } + + //create the root dir of the devmapper driver ownership to match this + //daemon's remapped root uid/gid so containers can start properly + uid, gid, err := idtools.GetRootUIDGID(devices.uidMaps, devices.gidMaps) + if err != nil { + return err + } + if err := idtools.MkdirAs(devices.root, 0700, uid, gid); err != nil && !os.IsExist(err) { + return err + } + if err := os.MkdirAll(devices.metadataDir(), 0700); err != nil && !os.IsExist(err) { + return err + } + + // Set the device prefix from the device id and inode of the docker root dir + + st, err := os.Stat(devices.root) + if err != nil { + return fmt.Errorf("devmapper: Error looking up dir %s: %s", devices.root, err) + } + sysSt := st.Sys().(*syscall.Stat_t) + // "reg-" stands for "regular file". + // In the future we might use "dev-" for "device file", etc. + // docker-maj,min[-inode] stands for: + // - Managed by docker + // - The target of this device is at major and minor + // - If is defined, use that file inside the device as a loopback image. Otherwise use the device itself. + devices.devicePrefix = fmt.Sprintf("docker-%d:%d-%d", major(sysSt.Dev), minor(sysSt.Dev), sysSt.Ino) + logrus.Debugf("devmapper: Generated prefix: %s", devices.devicePrefix) + + // Check for the existence of the thin-pool device + poolExists, err := devices.thinPoolExists(devices.getPoolName()) + if err != nil { + return err + } + + // It seems libdevmapper opens this without O_CLOEXEC, and go exec will not close files + // that are not Close-on-exec, + // so we add this badhack to make sure it closes itself + setCloseOnExec("/dev/mapper/control") + + // Make sure the sparse images exist in /devicemapper/data and + // /devicemapper/metadata + + createdLoopback := false + + // If the pool doesn't exist, create it + if !poolExists && devices.thinPoolDevice == "" { + logrus.Debugf("devmapper: Pool doesn't exist. Creating it.") + + var ( + dataFile *os.File + metadataFile *os.File + ) + + if devices.dataDevice == "" { + // Make sure the sparse images exist in /devicemapper/data + + hasData := devices.hasImage("data") + + if !doInit && !hasData { + return errors.New("Loopback data file not found") + } + + if !hasData { + createdLoopback = true + } + + data, err := devices.ensureImage("data", devices.dataLoopbackSize) + if err != nil { + logrus.Debugf("devmapper: Error device ensureImage (data): %s", err) + return err + } + + dataFile, err = loopback.AttachLoopDevice(data) + if err != nil { + return err + } + devices.dataLoopFile = data + devices.dataDevice = dataFile.Name() + } else { + dataFile, err = os.OpenFile(devices.dataDevice, os.O_RDWR, 0600) + if err != nil { + return err + } + } + defer dataFile.Close() + + if devices.metadataDevice == "" { + // Make sure the sparse images exist in /devicemapper/metadata + + hasMetadata := devices.hasImage("metadata") + + if !doInit && !hasMetadata { + return errors.New("Loopback metadata file not found") + } + + if !hasMetadata { + createdLoopback = true + } + + metadata, err := devices.ensureImage("metadata", devices.metaDataLoopbackSize) + if err != nil { + logrus.Debugf("devmapper: Error device ensureImage (metadata): %s", err) + return err + } + + metadataFile, err = loopback.AttachLoopDevice(metadata) + if err != nil { + return err + } + devices.metadataLoopFile = metadata + devices.metadataDevice = metadataFile.Name() + } else { + metadataFile, err = os.OpenFile(devices.metadataDevice, os.O_RDWR, 0600) + if err != nil { + return err + } + } + defer metadataFile.Close() + + if err := devicemapper.CreatePool(devices.getPoolName(), dataFile, metadataFile, devices.thinpBlockSize); err != nil { + return err + } + } + + // Pool already exists and caller did not pass us a pool. That means + // we probably created pool earlier and could not remove it as some + // containers were still using it. Detect some of the properties of + // pool, like is it using loop devices. + if poolExists && devices.thinPoolDevice == "" { + if err := devices.loadThinPoolLoopBackInfo(); err != nil { + logrus.Debugf("devmapper: Failed to load thin pool loopback device information:%v", err) + return err + } + } + + // If we didn't just create the data or metadata image, we need to + // load the transaction id and migrate old metadata + if !createdLoopback { + if err := devices.initMetaData(); err != nil { + return err + } + } + + if devices.thinPoolDevice == "" { + if devices.metadataLoopFile != "" || devices.dataLoopFile != "" { + logrus.Warnf("devmapper: Usage of loopback devices is strongly discouraged for production use. Please use `--storage-opt dm.thinpooldev` or use `man docker` to refer to dm.thinpooldev section.") + } + } + + // Right now this loads only NextDeviceID. If there is more metadata + // down the line, we might have to move it earlier. + if err := devices.loadDeviceSetMetaData(); err != nil { + return err + } + + // Setup the base image + if doInit { + if err := devices.setupBaseImage(); err != nil { + logrus.Debugf("devmapper: Error device setupBaseImage: %s", err) + return err + } + } + + return nil +} + +// AddDevice adds a device and registers in the hash. +func (devices *DeviceSet) AddDevice(hash, baseHash string) error { + logrus.Debugf("devmapper: AddDevice(hash=%s basehash=%s)", hash, baseHash) + defer logrus.Debugf("devmapper: AddDevice(hash=%s basehash=%s) END", hash, baseHash) + + // If a deleted device exists, return error. + baseInfo, err := devices.lookupDeviceWithLock(baseHash) + if err != nil { + return err + } + + if baseInfo.Deleted { + return fmt.Errorf("devmapper: Base device %v has been marked for deferred deletion", baseInfo.Hash) + } + + baseInfo.lock.Lock() + defer baseInfo.lock.Unlock() + + devices.Lock() + defer devices.Unlock() + + // Also include deleted devices in case hash of new device is + // same as one of the deleted devices. + if info, _ := devices.lookupDevice(hash); info != nil { + return fmt.Errorf("devmapper: device %s already exists. Deleted=%v", hash, info.Deleted) + } + + if err := devices.createRegisterSnapDevice(hash, baseInfo); err != nil { + return err + } + + return nil +} + +func (devices *DeviceSet) markForDeferredDeletion(info *devInfo) error { + // If device is already in deleted state, there is nothing to be done. + if info.Deleted { + return nil + } + + logrus.Debugf("devmapper: Marking device %s for deferred deletion.", info.Hash) + + info.Deleted = true + + // save device metadata to reflect deleted state. + if err := devices.saveMetadata(info); err != nil { + info.Deleted = false + return err + } + + devices.nrDeletedDevices++ + return nil +} + +// Should be called with devices.Lock() held. +func (devices *DeviceSet) deleteTransaction(info *devInfo, syncDelete bool) error { + if err := devices.openTransaction(info.Hash, info.DeviceID); err != nil { + logrus.Debugf("devmapper: Error opening transaction hash = %s deviceId = %d", "", info.DeviceID) + return err + } + + defer devices.closeTransaction() + + err := devicemapper.DeleteDevice(devices.getPoolDevName(), info.DeviceID) + if err != nil { + // If syncDelete is true, we want to return error. If deferred + // deletion is not enabled, we return an error. If error is + // something other then EBUSY, return an error. + if syncDelete || !devices.deferredDelete || err != devicemapper.ErrBusy { + logrus.Debugf("devmapper: Error deleting device: %s", err) + return err + } + } + + if err == nil { + if err := devices.unregisterDevice(info.DeviceID, info.Hash); err != nil { + return err + } + // If device was already in deferred delete state that means + // deletion was being tried again later. Reduce the deleted + // device count. + if info.Deleted { + devices.nrDeletedDevices-- + } + devices.markDeviceIDFree(info.DeviceID) + } else { + if err := devices.markForDeferredDeletion(info); err != nil { + return err + } + } + + return nil +} + +// Issue discard only if device open count is zero. +func (devices *DeviceSet) issueDiscard(info *devInfo) error { + logrus.Debugf("devmapper: issueDiscard(device: %s). START", info.Hash) + defer logrus.Debugf("devmapper: issueDiscard(device: %s). END", info.Hash) + // This is a workaround for the kernel not discarding block so + // on the thin pool when we remove a thinp device, so we do it + // manually. + // Even if device is deferred deleted, activate it and issue + // discards. + if err := devices.activateDeviceIfNeeded(info, true); err != nil { + return err + } + + devinfo, err := devicemapper.GetInfo(info.Name()) + if err != nil { + return err + } + + if devinfo.OpenCount != 0 { + logrus.Debugf("devmapper: Device: %s is in use. OpenCount=%d. Not issuing discards.", info.Hash, devinfo.OpenCount) + return nil + } + + if err := devicemapper.BlockDeviceDiscard(info.DevName()); err != nil { + logrus.Debugf("devmapper: Error discarding block on device: %s (ignoring)", err) + } + return nil +} + +// Should be called with devices.Lock() held. +func (devices *DeviceSet) deleteDevice(info *devInfo, syncDelete bool) error { + if devices.doBlkDiscard { + devices.issueDiscard(info) + } + + // Try to deactivate device in case it is active. + if err := devices.deactivateDevice(info); err != nil { + logrus.Debugf("devmapper: Error deactivating device: %s", err) + return err + } + + if err := devices.deleteTransaction(info, syncDelete); err != nil { + return err + } + + return nil +} + +// DeleteDevice will return success if device has been marked for deferred +// removal. If one wants to override that and want DeleteDevice() to fail if +// device was busy and could not be deleted, set syncDelete=true. +func (devices *DeviceSet) DeleteDevice(hash string, syncDelete bool) error { + logrus.Debugf("devmapper: DeleteDevice(hash=%v syncDelete=%v) START", hash, syncDelete) + defer logrus.Debugf("devmapper: DeleteDevice(hash=%v syncDelete=%v) END", hash, syncDelete) + info, err := devices.lookupDeviceWithLock(hash) + if err != nil { + return err + } + + info.lock.Lock() + defer info.lock.Unlock() + + devices.Lock() + defer devices.Unlock() + + return devices.deleteDevice(info, syncDelete) +} + +func (devices *DeviceSet) deactivatePool() error { + logrus.Debugf("devmapper: deactivatePool()") + defer logrus.Debugf("devmapper: deactivatePool END") + devname := devices.getPoolDevName() + + devinfo, err := devicemapper.GetInfo(devname) + if err != nil { + return err + } + + if devinfo.Exists == 0 { + return nil + } + if err := devicemapper.RemoveDevice(devname); err != nil { + return err + } + + if d, err := devicemapper.GetDeps(devname); err == nil { + logrus.Warnf("devmapper: device %s still has %d active dependents", devname, d.Count) + } + + return nil +} + +func (devices *DeviceSet) deactivateDevice(info *devInfo) error { + logrus.Debugf("devmapper: deactivateDevice(%s)", info.Hash) + defer logrus.Debugf("devmapper: deactivateDevice END(%s)", info.Hash) + + devinfo, err := devicemapper.GetInfo(info.Name()) + if err != nil { + return err + } + + if devinfo.Exists == 0 { + return nil + } + + if devices.deferredRemove { + if err := devicemapper.RemoveDeviceDeferred(info.Name()); err != nil { + return err + } + } else { + if err := devices.removeDevice(info.Name()); err != nil { + return err + } + } + return nil +} + +// Issues the underlying dm remove operation. +func (devices *DeviceSet) removeDevice(devname string) error { + var err error + + logrus.Debugf("devmapper: removeDevice START(%s)", devname) + defer logrus.Debugf("devmapper: removeDevice END(%s)", devname) + + for i := 0; i < 200; i++ { + err = devicemapper.RemoveDevice(devname) + if err == nil { + break + } + if err != devicemapper.ErrBusy { + return err + } + + // If we see EBUSY it may be a transient error, + // sleep a bit a retry a few times. + devices.Unlock() + time.Sleep(100 * time.Millisecond) + devices.Lock() + } + + return err +} + +func (devices *DeviceSet) cancelDeferredRemoval(info *devInfo) error { + if !devices.deferredRemove { + return nil + } + + logrus.Debugf("devmapper: cancelDeferredRemoval START(%s)", info.Name()) + defer logrus.Debugf("devmapper: cancelDeferredRemoval END(%s)", info.Name()) + + devinfo, err := devicemapper.GetInfoWithDeferred(info.Name()) + + if devinfo != nil && devinfo.DeferredRemove == 0 { + return nil + } + + // Cancel deferred remove + for i := 0; i < 100; i++ { + err = devicemapper.CancelDeferredRemove(info.Name()) + if err == nil { + break + } + + if err == devicemapper.ErrEnxio { + // Device is probably already gone. Return success. + return nil + } + + if err != devicemapper.ErrBusy { + return err + } + + // If we see EBUSY it may be a transient error, + // sleep a bit a retry a few times. + devices.Unlock() + time.Sleep(100 * time.Millisecond) + devices.Lock() + } + return err +} + +// Shutdown shuts down the device by unmounting the root. +func (devices *DeviceSet) Shutdown(home string) error { + logrus.Debugf("devmapper: [deviceset %s] Shutdown()", devices.devicePrefix) + logrus.Debugf("devmapper: Shutting down DeviceSet: %s", devices.root) + defer logrus.Debugf("devmapper: [deviceset %s] Shutdown() END", devices.devicePrefix) + + // Stop deletion worker. This should start delivering new events to + // ticker channel. That means no new instance of cleanupDeletedDevice() + // will run after this call. If one instance is already running at + // the time of the call, it must be holding devices.Lock() and + // we will block on this lock till cleanup function exits. + devices.deletionWorkerTicker.Stop() + + devices.Lock() + // Save DeviceSet Metadata first. Docker kills all threads if they + // don't finish in certain time. It is possible that Shutdown() + // routine does not finish in time as we loop trying to deactivate + // some devices while these are busy. In that case shutdown() routine + // will be killed and we will not get a chance to save deviceset + // metadata. Hence save this early before trying to deactivate devices. + devices.saveDeviceSetMetaData() + + // ignore the error since it's just a best effort to not try to unmount something that's mounted + mounts, _ := mount.GetMounts() + mounted := make(map[string]bool, len(mounts)) + for _, mnt := range mounts { + mounted[mnt.Mountpoint] = true + } + + if err := filepath.Walk(path.Join(home, "mnt"), func(p string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + return nil + } + + if mounted[p] { + // We use MNT_DETACH here in case it is still busy in some running + // container. This means it'll go away from the global scope directly, + // and the device will be released when that container dies. + if err := syscall.Unmount(p, syscall.MNT_DETACH); err != nil { + logrus.Debugf("devmapper: Shutdown unmounting %s, error: %s", p, err) + } + } + + if devInfo, err := devices.lookupDevice(path.Base(p)); err != nil { + logrus.Debugf("devmapper: Shutdown lookup device %s, error: %s", path.Base(p), err) + } else { + if err := devices.deactivateDevice(devInfo); err != nil { + logrus.Debugf("devmapper: Shutdown deactivate %s , error: %s", devInfo.Hash, err) + } + } + + return nil + }); err != nil && !os.IsNotExist(err) { + devices.Unlock() + return err + } + + devices.Unlock() + + info, _ := devices.lookupDeviceWithLock("") + if info != nil { + info.lock.Lock() + devices.Lock() + if err := devices.deactivateDevice(info); err != nil { + logrus.Debugf("devmapper: Shutdown deactivate base , error: %s", err) + } + devices.Unlock() + info.lock.Unlock() + } + + devices.Lock() + if devices.thinPoolDevice == "" { + if err := devices.deactivatePool(); err != nil { + logrus.Debugf("devmapper: Shutdown deactivate pool , error: %s", err) + } + } + devices.Unlock() + + return nil +} + +// MountDevice mounts the device if not already mounted. +func (devices *DeviceSet) MountDevice(hash, path, mountLabel string) error { + info, err := devices.lookupDeviceWithLock(hash) + if err != nil { + return err + } + + if info.Deleted { + return fmt.Errorf("devmapper: Can't mount device %v as it has been marked for deferred deletion", info.Hash) + } + + info.lock.Lock() + defer info.lock.Unlock() + + devices.Lock() + defer devices.Unlock() + + if err := devices.activateDeviceIfNeeded(info, false); err != nil { + return fmt.Errorf("devmapper: Error activating devmapper device for '%s': %s", hash, err) + } + + fstype, err := ProbeFsType(info.DevName()) + if err != nil { + return err + } + + options := "" + + if fstype == "xfs" { + // XFS needs nouuid or it can't mount filesystems with the same fs + options = joinMountOptions(options, "nouuid") + } + + options = joinMountOptions(options, devices.mountOptions) + options = joinMountOptions(options, label.FormatMountLabel("", mountLabel)) + + if err := mount.Mount(info.DevName(), path, fstype, options); err != nil { + return fmt.Errorf("devmapper: Error mounting '%s' on '%s': %s", info.DevName(), path, err) + } + + return nil +} + +// UnmountDevice unmounts the device and removes it from hash. +func (devices *DeviceSet) UnmountDevice(hash, mountPath string) error { + logrus.Debugf("devmapper: UnmountDevice(hash=%s)", hash) + defer logrus.Debugf("devmapper: UnmountDevice(hash=%s) END", hash) + + info, err := devices.lookupDeviceWithLock(hash) + if err != nil { + return err + } + + info.lock.Lock() + defer info.lock.Unlock() + + devices.Lock() + defer devices.Unlock() + + logrus.Debugf("devmapper: Unmount(%s)", mountPath) + if err := syscall.Unmount(mountPath, syscall.MNT_DETACH); err != nil { + return err + } + logrus.Debugf("devmapper: Unmount done") + + if err := devices.deactivateDevice(info); err != nil { + return err + } + + return nil +} + +// HasDevice returns true if the device metadata exists. +func (devices *DeviceSet) HasDevice(hash string) bool { + info, _ := devices.lookupDeviceWithLock(hash) + return info != nil +} + +// List returns a list of device ids. +func (devices *DeviceSet) List() []string { + devices.Lock() + defer devices.Unlock() + + ids := make([]string, len(devices.Devices)) + i := 0 + for k := range devices.Devices { + ids[i] = k + i++ + } + return ids +} + +func (devices *DeviceSet) deviceStatus(devName string) (sizeInSectors, mappedSectors, highestMappedSector uint64, err error) { + var params string + _, sizeInSectors, _, params, err = devicemapper.GetStatus(devName) + if err != nil { + return + } + if _, err = fmt.Sscanf(params, "%d %d", &mappedSectors, &highestMappedSector); err == nil { + return + } + return +} + +// GetDeviceStatus provides size, mapped sectors +func (devices *DeviceSet) GetDeviceStatus(hash string) (*DevStatus, error) { + info, err := devices.lookupDeviceWithLock(hash) + if err != nil { + return nil, err + } + + info.lock.Lock() + defer info.lock.Unlock() + + devices.Lock() + defer devices.Unlock() + + status := &DevStatus{ + DeviceID: info.DeviceID, + Size: info.Size, + TransactionID: info.TransactionID, + } + + if err := devices.activateDeviceIfNeeded(info, false); err != nil { + return nil, fmt.Errorf("devmapper: Error activating devmapper device for '%s': %s", hash, err) + } + + sizeInSectors, mappedSectors, highestMappedSector, err := devices.deviceStatus(info.DevName()) + + if err != nil { + return nil, err + } + + status.SizeInSectors = sizeInSectors + status.MappedSectors = mappedSectors + status.HighestMappedSector = highestMappedSector + + return status, nil +} + +func (devices *DeviceSet) poolStatus() (totalSizeInSectors, transactionID, dataUsed, dataTotal, metadataUsed, metadataTotal uint64, err error) { + var params string + if _, totalSizeInSectors, _, params, err = devicemapper.GetStatus(devices.getPoolName()); err == nil { + _, err = fmt.Sscanf(params, "%d %d/%d %d/%d", &transactionID, &metadataUsed, &metadataTotal, &dataUsed, &dataTotal) + } + return +} + +// DataDevicePath returns the path to the data storage for this deviceset, +// regardless of loopback or block device +func (devices *DeviceSet) DataDevicePath() string { + return devices.dataDevice +} + +// MetadataDevicePath returns the path to the metadata storage for this deviceset, +// regardless of loopback or block device +func (devices *DeviceSet) MetadataDevicePath() string { + return devices.metadataDevice +} + +func (devices *DeviceSet) getUnderlyingAvailableSpace(loopFile string) (uint64, error) { + buf := new(syscall.Statfs_t) + if err := syscall.Statfs(loopFile, buf); err != nil { + logrus.Warnf("devmapper: Couldn't stat loopfile filesystem %v: %v", loopFile, err) + return 0, err + } + return buf.Bfree * uint64(buf.Bsize), nil +} + +func (devices *DeviceSet) isRealFile(loopFile string) (bool, error) { + if loopFile != "" { + fi, err := os.Stat(loopFile) + if err != nil { + logrus.Warnf("devmapper: Couldn't stat loopfile %v: %v", loopFile, err) + return false, err + } + return fi.Mode().IsRegular(), nil + } + return false, nil +} + +// Status returns the current status of this deviceset +func (devices *DeviceSet) Status() *Status { + devices.Lock() + defer devices.Unlock() + + status := &Status{} + + status.PoolName = devices.getPoolName() + status.DataFile = devices.DataDevicePath() + status.DataLoopback = devices.dataLoopFile + status.MetadataFile = devices.MetadataDevicePath() + status.MetadataLoopback = devices.metadataLoopFile + status.UdevSyncSupported = devicemapper.UdevSyncSupported() + status.DeferredRemoveEnabled = devices.deferredRemove + status.DeferredDeleteEnabled = devices.deferredDelete + status.DeferredDeletedDeviceCount = devices.nrDeletedDevices + status.BaseDeviceSize = devices.getBaseDeviceSize() + status.BaseDeviceFS = devices.getBaseDeviceFS() + + totalSizeInSectors, _, dataUsed, dataTotal, metadataUsed, metadataTotal, err := devices.poolStatus() + if err == nil { + // Convert from blocks to bytes + blockSizeInSectors := totalSizeInSectors / dataTotal + + status.Data.Used = dataUsed * blockSizeInSectors * 512 + status.Data.Total = dataTotal * blockSizeInSectors * 512 + status.Data.Available = status.Data.Total - status.Data.Used + + // metadata blocks are always 4k + status.Metadata.Used = metadataUsed * 4096 + status.Metadata.Total = metadataTotal * 4096 + status.Metadata.Available = status.Metadata.Total - status.Metadata.Used + + status.SectorSize = blockSizeInSectors * 512 + + if check, _ := devices.isRealFile(devices.dataLoopFile); check { + actualSpace, err := devices.getUnderlyingAvailableSpace(devices.dataLoopFile) + if err == nil && actualSpace < status.Data.Available { + status.Data.Available = actualSpace + } + } + + if check, _ := devices.isRealFile(devices.metadataLoopFile); check { + actualSpace, err := devices.getUnderlyingAvailableSpace(devices.metadataLoopFile) + if err == nil && actualSpace < status.Metadata.Available { + status.Metadata.Available = actualSpace + } + } + } + + return status +} + +// Status returns the current status of this deviceset +func (devices *DeviceSet) exportDeviceMetadata(hash string) (*deviceMetadata, error) { + info, err := devices.lookupDeviceWithLock(hash) + if err != nil { + return nil, err + } + + info.lock.Lock() + defer info.lock.Unlock() + + metadata := &deviceMetadata{info.DeviceID, info.Size, info.Name()} + return metadata, nil +} + +// NewDeviceSet creates the device set based on the options provided. +func NewDeviceSet(root string, doInit bool, options []string, uidMaps, gidMaps []idtools.IDMap) (*DeviceSet, error) { + devicemapper.SetDevDir("/dev") + + devices := &DeviceSet{ + root: root, + metaData: metaData{Devices: make(map[string]*devInfo)}, + dataLoopbackSize: defaultDataLoopbackSize, + metaDataLoopbackSize: defaultMetaDataLoopbackSize, + baseFsSize: defaultBaseFsSize, + overrideUdevSyncCheck: defaultUdevSyncOverride, + doBlkDiscard: true, + thinpBlockSize: defaultThinpBlockSize, + deviceIDMap: make([]byte, deviceIDMapSz), + deletionWorkerTicker: time.NewTicker(time.Second * 30), + uidMaps: uidMaps, + gidMaps: gidMaps, + minFreeSpacePercent: defaultMinFreeSpacePercent, + } + + foundBlkDiscard := false + for _, option := range options { + key, val, err := parsers.ParseKeyValueOpt(option) + if err != nil { + return nil, err + } + key = strings.ToLower(key) + switch key { + case "dm.basesize": + size, err := units.RAMInBytes(val) + if err != nil { + return nil, err + } + userBaseSize = true + devices.baseFsSize = uint64(size) + case "dm.loopdatasize": + size, err := units.RAMInBytes(val) + if err != nil { + return nil, err + } + devices.dataLoopbackSize = size + case "dm.loopmetadatasize": + size, err := units.RAMInBytes(val) + if err != nil { + return nil, err + } + devices.metaDataLoopbackSize = size + case "dm.fs": + if val != "ext4" && val != "xfs" { + return nil, fmt.Errorf("devmapper: Unsupported filesystem %s\n", val) + } + devices.filesystem = val + case "dm.mkfsarg": + devices.mkfsArgs = append(devices.mkfsArgs, val) + case "dm.mountopt": + devices.mountOptions = joinMountOptions(devices.mountOptions, val) + case "dm.metadatadev": + devices.metadataDevice = val + case "dm.datadev": + devices.dataDevice = val + case "dm.thinpooldev": + devices.thinPoolDevice = strings.TrimPrefix(val, "/dev/mapper/") + case "dm.blkdiscard": + foundBlkDiscard = true + devices.doBlkDiscard, err = strconv.ParseBool(val) + if err != nil { + return nil, err + } + case "dm.blocksize": + size, err := units.RAMInBytes(val) + if err != nil { + return nil, err + } + // convert to 512b sectors + devices.thinpBlockSize = uint32(size) >> 9 + case "dm.override_udev_sync_check": + devices.overrideUdevSyncCheck, err = strconv.ParseBool(val) + if err != nil { + return nil, err + } + + case "dm.use_deferred_removal": + enableDeferredRemoval, err = strconv.ParseBool(val) + if err != nil { + return nil, err + } + + case "dm.use_deferred_deletion": + enableDeferredDeletion, err = strconv.ParseBool(val) + if err != nil { + return nil, err + } + + case "dm.min_free_space": + if !strings.HasSuffix(val, "%") { + return nil, fmt.Errorf("devmapper: Option dm.min_free_space requires %% suffix") + } + + valstring := strings.TrimSuffix(val, "%") + minFreeSpacePercent, err := strconv.ParseUint(valstring, 10, 32) + if err != nil { + return nil, err + } + + if minFreeSpacePercent >= 100 { + return nil, fmt.Errorf("devmapper: Invalid value %v for option dm.min_free_space", val) + } + + devices.minFreeSpacePercent = uint32(minFreeSpacePercent) + default: + return nil, fmt.Errorf("devmapper: Unknown option %s\n", key) + } + } + + // By default, don't do blk discard hack on raw devices, its rarely useful and is expensive + if !foundBlkDiscard && (devices.dataDevice != "" || devices.thinPoolDevice != "") { + devices.doBlkDiscard = false + } + + if err := devices.initDevmapper(doInit); err != nil { + return nil, err + } + + return devices, nil +} diff --git a/daemon/graphdriver/devmapper/devmapper_doc.go b/daemon/graphdriver/devmapper/devmapper_doc.go new file mode 100644 index 00000000..9ab3e4f8 --- /dev/null +++ b/daemon/graphdriver/devmapper/devmapper_doc.go @@ -0,0 +1,106 @@ +package devmapper + +// Definition of struct dm_task and sub structures (from lvm2) +// +// struct dm_ioctl { +// /* +// * The version number is made up of three parts: +// * major - no backward or forward compatibility, +// * minor - only backwards compatible, +// * patch - both backwards and forwards compatible. +// * +// * All clients of the ioctl interface should fill in the +// * version number of the interface that they were +// * compiled with. +// * +// * All recognized ioctl commands (ie. those that don't +// * return -ENOTTY) fill out this field, even if the +// * command failed. +// */ +// uint32_t version[3]; /* in/out */ +// uint32_t data_size; /* total size of data passed in +// * including this struct */ + +// uint32_t data_start; /* offset to start of data +// * relative to start of this struct */ + +// uint32_t target_count; /* in/out */ +// int32_t open_count; /* out */ +// uint32_t flags; /* in/out */ + +// /* +// * event_nr holds either the event number (input and output) or the +// * udev cookie value (input only). +// * The DM_DEV_WAIT ioctl takes an event number as input. +// * The DM_SUSPEND, DM_DEV_REMOVE and DM_DEV_RENAME ioctls +// * use the field as a cookie to return in the DM_COOKIE +// * variable with the uevents they issue. +// * For output, the ioctls return the event number, not the cookie. +// */ +// uint32_t event_nr; /* in/out */ +// uint32_t padding; + +// uint64_t dev; /* in/out */ + +// char name[DM_NAME_LEN]; /* device name */ +// char uuid[DM_UUID_LEN]; /* unique identifier for +// * the block device */ +// char data[7]; /* padding or data */ +// }; + +// struct target { +// uint64_t start; +// uint64_t length; +// char *type; +// char *params; + +// struct target *next; +// }; + +// typedef enum { +// DM_ADD_NODE_ON_RESUME, /* add /dev/mapper node with dmsetup resume */ +// DM_ADD_NODE_ON_CREATE /* add /dev/mapper node with dmsetup create */ +// } dm_add_node_t; + +// struct dm_task { +// int type; +// char *dev_name; +// char *mangled_dev_name; + +// struct target *head, *tail; + +// int read_only; +// uint32_t event_nr; +// int major; +// int minor; +// int allow_default_major_fallback; +// uid_t uid; +// gid_t gid; +// mode_t mode; +// uint32_t read_ahead; +// uint32_t read_ahead_flags; +// union { +// struct dm_ioctl *v4; +// } dmi; +// char *newname; +// char *message; +// char *geometry; +// uint64_t sector; +// int no_flush; +// int no_open_count; +// int skip_lockfs; +// int query_inactive_table; +// int suppress_identical_reload; +// dm_add_node_t add_node; +// uint64_t existing_table_size; +// int cookie_set; +// int new_uuid; +// int secure_data; +// int retry_remove; +// int enable_checks; +// int expected_errno; + +// char *uuid; +// char *mangled_uuid; +// }; +// diff --git a/daemon/graphdriver/devmapper/devmapper_test.go b/daemon/graphdriver/devmapper/devmapper_test.go new file mode 100644 index 00000000..5c2abcef --- /dev/null +++ b/daemon/graphdriver/devmapper/devmapper_test.go @@ -0,0 +1,110 @@ +// +build linux + +package devmapper + +import ( + "fmt" + "testing" + "time" + + "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/daemon/graphdriver/graphtest" +) + +func init() { + // Reduce the size the the base fs and loopback for the tests + defaultDataLoopbackSize = 300 * 1024 * 1024 + defaultMetaDataLoopbackSize = 200 * 1024 * 1024 + defaultBaseFsSize = 300 * 1024 * 1024 + defaultUdevSyncOverride = true + if err := graphtest.InitLoopbacks(); err != nil { + panic(err) + } +} + +// This avoids creating a new driver for each test if all tests are run +// Make sure to put new tests between TestDevmapperSetup and TestDevmapperTeardown +func TestDevmapperSetup(t *testing.T) { + graphtest.GetDriver(t, "devicemapper") +} + +func TestDevmapperCreateEmpty(t *testing.T) { + graphtest.DriverTestCreateEmpty(t, "devicemapper") +} + +func TestDevmapperCreateBase(t *testing.T) { + graphtest.DriverTestCreateBase(t, "devicemapper") +} + +func TestDevmapperCreateSnap(t *testing.T) { + graphtest.DriverTestCreateSnap(t, "devicemapper") +} + +func TestDevmapperTeardown(t *testing.T) { + graphtest.PutDriver(t) +} + +func TestDevmapperReduceLoopBackSize(t *testing.T) { + tenMB := int64(10 * 1024 * 1024) + testChangeLoopBackSize(t, -tenMB, defaultDataLoopbackSize, defaultMetaDataLoopbackSize) +} + +func TestDevmapperIncreaseLoopBackSize(t *testing.T) { + tenMB := int64(10 * 1024 * 1024) + testChangeLoopBackSize(t, tenMB, defaultDataLoopbackSize+tenMB, defaultMetaDataLoopbackSize+tenMB) +} + +func testChangeLoopBackSize(t *testing.T, delta, expectDataSize, expectMetaDataSize int64) { + driver := graphtest.GetDriver(t, "devicemapper").(*graphtest.Driver).Driver.(*graphdriver.NaiveDiffDriver).ProtoDriver.(*Driver) + defer graphtest.PutDriver(t) + // make sure data or metadata loopback size are the default size + if s := driver.DeviceSet.Status(); s.Data.Total != uint64(defaultDataLoopbackSize) || s.Metadata.Total != uint64(defaultMetaDataLoopbackSize) { + t.Fatalf("data or metadata loop back size is incorrect") + } + if err := driver.Cleanup(); err != nil { + t.Fatal(err) + } + //Reload + d, err := Init(driver.home, []string{ + fmt.Sprintf("dm.loopdatasize=%d", defaultDataLoopbackSize+delta), + fmt.Sprintf("dm.loopmetadatasize=%d", defaultMetaDataLoopbackSize+delta), + }, nil, nil) + if err != nil { + t.Fatalf("error creating devicemapper driver: %v", err) + } + driver = d.(*graphdriver.NaiveDiffDriver).ProtoDriver.(*Driver) + if s := driver.DeviceSet.Status(); s.Data.Total != uint64(expectDataSize) || s.Metadata.Total != uint64(expectMetaDataSize) { + t.Fatalf("data or metadata loop back size is incorrect") + } + if err := driver.Cleanup(); err != nil { + t.Fatal(err) + } +} + +// Make sure devices.Lock() has been release upon return from cleanupDeletedDevices() function +func TestDevmapperLockReleasedDeviceDeletion(t *testing.T) { + driver := graphtest.GetDriver(t, "devicemapper").(*graphtest.Driver).Driver.(*graphdriver.NaiveDiffDriver).ProtoDriver.(*Driver) + defer graphtest.PutDriver(t) + + // Call cleanupDeletedDevices() and after the call take and release + // DeviceSet Lock. If lock has not been released, this will hang. + driver.DeviceSet.cleanupDeletedDevices() + + doneChan := make(chan bool) + + go func() { + driver.DeviceSet.Lock() + defer driver.DeviceSet.Unlock() + doneChan <- true + }() + + select { + case <-time.After(time.Second * 5): + // Timer expired. That means lock was not released upon + // function return and we are deadlocked. Release lock + // here so that cleanup could succeed and fail the test. + driver.DeviceSet.Unlock() + t.Fatalf("Could not acquire devices lock after call to cleanupDeletedDevices()") + case <-doneChan: + } +} diff --git a/daemon/graphdriver/devmapper/driver.go b/daemon/graphdriver/devmapper/driver.go new file mode 100644 index 00000000..8756f1f0 --- /dev/null +++ b/daemon/graphdriver/devmapper/driver.go @@ -0,0 +1,219 @@ +// +build linux + +package devmapper + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "strconv" + + "github.com/Sirupsen/logrus" + + "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/pkg/devicemapper" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/mount" + "github.com/docker/go-units" +) + +func init() { + graphdriver.Register("devicemapper", Init) +} + +// Driver contains the device set mounted and the home directory +type Driver struct { + *DeviceSet + home string + uidMaps []idtools.IDMap + gidMaps []idtools.IDMap + ctr *graphdriver.RefCounter +} + +// Init creates a driver with the given home and the set of options. +func Init(home string, options []string, uidMaps, gidMaps []idtools.IDMap) (graphdriver.Driver, error) { + deviceSet, err := NewDeviceSet(home, true, options, uidMaps, gidMaps) + if err != nil { + return nil, err + } + + if err := mount.MakePrivate(home); err != nil { + return nil, err + } + + d := &Driver{ + DeviceSet: deviceSet, + home: home, + uidMaps: uidMaps, + gidMaps: gidMaps, + ctr: graphdriver.NewRefCounter(), + } + + return graphdriver.NewNaiveDiffDriver(d, uidMaps, gidMaps), nil +} + +func (d *Driver) String() string { + return "devicemapper" +} + +// Status returns the status about the driver in a printable format. +// Information returned contains Pool Name, Data File, Metadata file, disk usage by +// the data and metadata, etc. +func (d *Driver) Status() [][2]string { + s := d.DeviceSet.Status() + + status := [][2]string{ + {"Pool Name", s.PoolName}, + {"Pool Blocksize", fmt.Sprintf("%s", units.HumanSize(float64(s.SectorSize)))}, + {"Base Device Size", fmt.Sprintf("%s", units.HumanSize(float64(s.BaseDeviceSize)))}, + {"Backing Filesystem", s.BaseDeviceFS}, + {"Data file", s.DataFile}, + {"Metadata file", s.MetadataFile}, + {"Data Space Used", fmt.Sprintf("%s", units.HumanSize(float64(s.Data.Used)))}, + {"Data Space Total", fmt.Sprintf("%s", units.HumanSize(float64(s.Data.Total)))}, + {"Data Space Available", fmt.Sprintf("%s", units.HumanSize(float64(s.Data.Available)))}, + {"Metadata Space Used", fmt.Sprintf("%s", units.HumanSize(float64(s.Metadata.Used)))}, + {"Metadata Space Total", fmt.Sprintf("%s", units.HumanSize(float64(s.Metadata.Total)))}, + {"Metadata Space Available", fmt.Sprintf("%s", units.HumanSize(float64(s.Metadata.Available)))}, + {"Udev Sync Supported", fmt.Sprintf("%v", s.UdevSyncSupported)}, + {"Deferred Removal Enabled", fmt.Sprintf("%v", s.DeferredRemoveEnabled)}, + {"Deferred Deletion Enabled", fmt.Sprintf("%v", s.DeferredDeleteEnabled)}, + {"Deferred Deleted Device Count", fmt.Sprintf("%v", s.DeferredDeletedDeviceCount)}, + } + if len(s.DataLoopback) > 0 { + status = append(status, [2]string{"Data loop file", s.DataLoopback}) + } + if len(s.MetadataLoopback) > 0 { + status = append(status, [2]string{"Metadata loop file", s.MetadataLoopback}) + } + if vStr, err := devicemapper.GetLibraryVersion(); err == nil { + status = append(status, [2]string{"Library Version", vStr}) + } + return status +} + +// GetMetadata returns a map of information about the device. +func (d *Driver) GetMetadata(id string) (map[string]string, error) { + m, err := d.DeviceSet.exportDeviceMetadata(id) + + if err != nil { + return nil, err + } + + metadata := make(map[string]string) + metadata["DeviceId"] = strconv.Itoa(m.deviceID) + metadata["DeviceSize"] = strconv.FormatUint(m.deviceSize, 10) + metadata["DeviceName"] = m.deviceName + return metadata, nil +} + +// Cleanup unmounts a device. +func (d *Driver) Cleanup() error { + err := d.DeviceSet.Shutdown(d.home) + + if err2 := mount.Unmount(d.home); err == nil { + err = err2 + } + + return err +} + +// Create adds a device with a given id and the parent. +func (d *Driver) Create(id, parent, mountLabel string) error { + if err := d.DeviceSet.AddDevice(id, parent); err != nil { + return err + } + + return nil +} + +// Remove removes a device with a given id, unmounts the filesystem. +func (d *Driver) Remove(id string) error { + if !d.DeviceSet.HasDevice(id) { + // Consider removing a non-existing device a no-op + // This is useful to be able to progress on container removal + // if the underlying device has gone away due to earlier errors + return nil + } + + // This assumes the device has been properly Get/Put:ed and thus is unmounted + if err := d.DeviceSet.DeleteDevice(id, false); err != nil { + return err + } + + mp := path.Join(d.home, "mnt", id) + if err := os.RemoveAll(mp); err != nil && !os.IsNotExist(err) { + return err + } + + return nil +} + +// Get mounts a device with given id into the root filesystem +func (d *Driver) Get(id, mountLabel string) (string, error) { + mp := path.Join(d.home, "mnt", id) + rootFs := path.Join(mp, "rootfs") + if count := d.ctr.Increment(id); count > 1 { + return rootFs, nil + } + + uid, gid, err := idtools.GetRootUIDGID(d.uidMaps, d.gidMaps) + if err != nil { + d.ctr.Decrement(id) + return "", err + } + + // Create the target directories if they don't exist + if err := idtools.MkdirAllAs(path.Join(d.home, "mnt"), 0755, uid, gid); err != nil && !os.IsExist(err) { + d.ctr.Decrement(id) + return "", err + } + if err := idtools.MkdirAs(mp, 0755, uid, gid); err != nil && !os.IsExist(err) { + d.ctr.Decrement(id) + return "", err + } + + // Mount the device + if err := d.DeviceSet.MountDevice(id, mp, mountLabel); err != nil { + d.ctr.Decrement(id) + return "", err + } + + if err := idtools.MkdirAllAs(rootFs, 0755, uid, gid); err != nil && !os.IsExist(err) { + d.ctr.Decrement(id) + d.DeviceSet.UnmountDevice(id, mp) + return "", err + } + + idFile := path.Join(mp, "id") + if _, err := os.Stat(idFile); err != nil && os.IsNotExist(err) { + // Create an "id" file with the container/image id in it to help reconstruct this in case + // of later problems + if err := ioutil.WriteFile(idFile, []byte(id), 0600); err != nil { + d.ctr.Decrement(id) + d.DeviceSet.UnmountDevice(id, mp) + return "", err + } + } + + return rootFs, nil +} + +// Put unmounts a device and removes it. +func (d *Driver) Put(id string) error { + if count := d.ctr.Decrement(id); count > 0 { + return nil + } + mp := path.Join(d.home, "mnt", id) + err := d.DeviceSet.UnmountDevice(id, mp) + if err != nil { + logrus.Errorf("devmapper: Error unmounting device %s: %s", id, err) + } + return err +} + +// Exists checks to see if the device exists. +func (d *Driver) Exists(id string) bool { + return d.DeviceSet.HasDevice(id) +} diff --git a/daemon/graphdriver/devmapper/mount.go b/daemon/graphdriver/devmapper/mount.go new file mode 100644 index 00000000..cca1fe1b --- /dev/null +++ b/daemon/graphdriver/devmapper/mount.go @@ -0,0 +1,89 @@ +// +build linux + +package devmapper + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "syscall" +) + +// FIXME: this is copy-pasted from the aufs driver. +// It should be moved into the core. + +// Mounted returns true if a mount point exists. +func Mounted(mountpoint string) (bool, error) { + mntpoint, err := os.Stat(mountpoint) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, err + } + parent, err := os.Stat(filepath.Join(mountpoint, "..")) + if err != nil { + return false, err + } + mntpointSt := mntpoint.Sys().(*syscall.Stat_t) + parentSt := parent.Sys().(*syscall.Stat_t) + return mntpointSt.Dev != parentSt.Dev, nil +} + +type probeData struct { + fsName string + magic string + offset uint64 +} + +// ProbeFsType returns the filesystem name for the given device id. +func ProbeFsType(device string) (string, error) { + probes := []probeData{ + {"btrfs", "_BHRfS_M", 0x10040}, + {"ext4", "\123\357", 0x438}, + {"xfs", "XFSB", 0}, + } + + maxLen := uint64(0) + for _, p := range probes { + l := p.offset + uint64(len(p.magic)) + if l > maxLen { + maxLen = l + } + } + + file, err := os.Open(device) + if err != nil { + return "", err + } + defer file.Close() + + buffer := make([]byte, maxLen) + l, err := file.Read(buffer) + if err != nil { + return "", err + } + + if uint64(l) != maxLen { + return "", fmt.Errorf("devmapper: unable to detect filesystem type of %s, short read", device) + } + + for _, p := range probes { + if bytes.Equal([]byte(p.magic), buffer[p.offset:p.offset+uint64(len(p.magic))]) { + return p.fsName, nil + } + } + + return "", fmt.Errorf("devmapper: Unknown filesystem type on %s", device) +} + +func joinMountOptions(a, b string) string { + if a == "" { + return b + } + if b == "" { + return a + } + return a + "," + b +} diff --git a/daemon/graphdriver/driver.go b/daemon/graphdriver/driver.go new file mode 100644 index 00000000..ced960b4 --- /dev/null +++ b/daemon/graphdriver/driver.go @@ -0,0 +1,234 @@ +package graphdriver + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/vbatts/tar-split/tar/storage" + + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/idtools" +) + +// FsMagic unsigned id of the filesystem in use. +type FsMagic uint32 + +const ( + // FsMagicUnsupported is a predefined constant value other than a valid filesystem id. + FsMagicUnsupported = FsMagic(0x00000000) +) + +var ( + // All registered drivers + drivers map[string]InitFunc + + // ErrNotSupported returned when driver is not supported. + ErrNotSupported = errors.New("driver not supported") + // ErrPrerequisites retuned when driver does not meet prerequisites. + ErrPrerequisites = errors.New("prerequisites for driver not satisfied (wrong filesystem?)") + // ErrIncompatibleFS returned when file system is not supported. + ErrIncompatibleFS = fmt.Errorf("backing file system is unsupported for this graph driver") +) + +// InitFunc initializes the storage driver. +type InitFunc func(root string, options []string, uidMaps, gidMaps []idtools.IDMap) (Driver, error) + +// ProtoDriver defines the basic capabilities of a driver. +// This interface exists solely to be a minimum set of methods +// for client code which choose not to implement the entire Driver +// interface and use the NaiveDiffDriver wrapper constructor. +// +// Use of ProtoDriver directly by client code is not recommended. +type ProtoDriver interface { + // String returns a string representation of this driver. + String() string + // Create creates a new, empty, filesystem layer with the + // specified id and parent and mountLabel. Parent and mountLabel may be "". + Create(id, parent, mountLabel string) error + // Remove attempts to remove the filesystem layer with this id. + Remove(id string) error + // Get returns the mountpoint for the layered filesystem referred + // to by this id. You can optionally specify a mountLabel or "". + // Returns the absolute path to the mounted layered filesystem. + Get(id, mountLabel string) (dir string, err error) + // Put releases the system resources for the specified id, + // e.g, unmounting layered filesystem. + Put(id string) error + // Exists returns whether a filesystem layer with the specified + // ID exists on this driver. + Exists(id string) bool + // Status returns a set of key-value pairs which give low + // level diagnostic status about this driver. + Status() [][2]string + // Returns a set of key-value pairs which give low level information + // about the image/container driver is managing. + GetMetadata(id string) (map[string]string, error) + // Cleanup performs necessary tasks to release resources + // held by the driver, e.g., unmounting all layered filesystems + // known to this driver. + Cleanup() error +} + +// Driver is the interface for layered/snapshot file system drivers. +type Driver interface { + ProtoDriver + // Diff produces an archive of the changes between the specified + // layer and its parent layer which may be "". + Diff(id, parent string) (archive.Archive, error) + // Changes produces a list of changes between the specified layer + // and its parent layer. If parent is "", then all changes will be ADD changes. + Changes(id, parent string) ([]archive.Change, error) + // ApplyDiff extracts the changeset from the given diff into the + // layer with the specified id and parent, returning the size of the + // new layer in bytes. + // The archive.Reader must be an uncompressed stream. + ApplyDiff(id, parent string, diff archive.Reader) (size int64, err error) + // DiffSize calculates the changes between the specified id + // and its parent and returns the size in bytes of the changes + // relative to its base filesystem directory. + DiffSize(id, parent string) (size int64, err error) +} + +// DiffGetterDriver is the interface for layered file system drivers that +// provide a specialized function for getting file contents for tar-split. +type DiffGetterDriver interface { + Driver + // DiffGetter returns an interface to efficiently retrieve the contents + // of files in a layer. + DiffGetter(id string) (FileGetCloser, error) +} + +// FileGetCloser extends the storage.FileGetter interface with a Close method +// for cleaning up. +type FileGetCloser interface { + storage.FileGetter + // Close cleans up any resources associated with the FileGetCloser. + Close() error +} + +func init() { + drivers = make(map[string]InitFunc) +} + +// Register registers a InitFunc for the driver. +func Register(name string, initFunc InitFunc) error { + if _, exists := drivers[name]; exists { + return fmt.Errorf("Name already registered %s", name) + } + drivers[name] = initFunc + + return nil +} + +// GetDriver initializes and returns the registered driver +func GetDriver(name, home string, options []string, uidMaps, gidMaps []idtools.IDMap) (Driver, error) { + if initFunc, exists := drivers[name]; exists { + return initFunc(filepath.Join(home, name), options, uidMaps, gidMaps) + } + if pluginDriver, err := lookupPlugin(name, home, options); err == nil { + return pluginDriver, nil + } + logrus.Errorf("Failed to GetDriver graph %s %s", name, home) + return nil, ErrNotSupported +} + +// getBuiltinDriver initializes and returns the registered driver, but does not try to load from plugins +func getBuiltinDriver(name, home string, options []string, uidMaps, gidMaps []idtools.IDMap) (Driver, error) { + if initFunc, exists := drivers[name]; exists { + return initFunc(filepath.Join(home, name), options, uidMaps, gidMaps) + } + logrus.Errorf("Failed to built-in GetDriver graph %s %s", name, home) + return nil, ErrNotSupported +} + +// New creates the driver and initializes it at the specified root. +func New(root string, name string, options []string, uidMaps, gidMaps []idtools.IDMap) (Driver, error) { + if name != "" { + logrus.Debugf("[graphdriver] trying provided driver %q", name) // so the logs show specified driver + return GetDriver(name, root, options, uidMaps, gidMaps) + } + + // Guess for prior driver + driversMap := scanPriorDrivers(root) + for _, name := range priority { + if name == "vfs" { + // don't use vfs even if there is state present. + continue + } + if _, prior := driversMap[name]; prior { + // of the state found from prior drivers, check in order of our priority + // which we would prefer + driver, err := getBuiltinDriver(name, root, options, uidMaps, gidMaps) + if err != nil { + // unlike below, we will return error here, because there is prior + // state, and now it is no longer supported/prereq/compatible, so + // something changed and needs attention. Otherwise the daemon's + // images would just "disappear". + logrus.Errorf("[graphdriver] prior storage driver %q failed: %s", name, err) + return nil, err + } + + // abort starting when there are other prior configured drivers + // to ensure the user explicitly selects the driver to load + if len(driversMap)-1 > 0 { + var driversSlice []string + for name := range driversMap { + driversSlice = append(driversSlice, name) + } + + return nil, fmt.Errorf("%q contains several valid graphdrivers: %s; Please cleanup or explicitly choose storage driver (-s )", root, strings.Join(driversSlice, ", ")) + } + + logrus.Infof("[graphdriver] using prior storage driver %q", name) + return driver, nil + } + } + + // Check for priority drivers first + for _, name := range priority { + driver, err := getBuiltinDriver(name, root, options, uidMaps, gidMaps) + if err != nil { + if isDriverNotSupported(err) { + continue + } + return nil, err + } + return driver, nil + } + + // Check all registered drivers if no priority driver is found + for name, initFunc := range drivers { + driver, err := initFunc(filepath.Join(root, name), options, uidMaps, gidMaps) + if err != nil { + if isDriverNotSupported(err) { + continue + } + return nil, err + } + return driver, nil + } + return nil, fmt.Errorf("No supported storage backend found") +} + +// isDriverNotSupported returns true if the error initializing +// the graph driver is a non-supported error. +func isDriverNotSupported(err error) bool { + return err == ErrNotSupported || err == ErrPrerequisites || err == ErrIncompatibleFS +} + +// scanPriorDrivers returns an un-ordered scan of directories of prior storage drivers +func scanPriorDrivers(root string) map[string]bool { + driversMap := make(map[string]bool) + + for driver := range drivers { + p := filepath.Join(root, driver) + if _, err := os.Stat(p); err == nil && driver != "vfs" { + driversMap[driver] = true + } + } + return driversMap +} diff --git a/daemon/graphdriver/driver_freebsd.go b/daemon/graphdriver/driver_freebsd.go new file mode 100644 index 00000000..2891a84f --- /dev/null +++ b/daemon/graphdriver/driver_freebsd.go @@ -0,0 +1,19 @@ +package graphdriver + +import "syscall" + +var ( + // Slice of drivers that should be used in an order + priority = []string{ + "zfs", + } +) + +// Mounted checks if the given path is mounted as the fs type +func Mounted(fsType FsMagic, mountPath string) (bool, error) { + var buf syscall.Statfs_t + if err := syscall.Statfs(mountPath, &buf); err != nil { + return false, err + } + return FsMagic(buf.Type) == fsType, nil +} diff --git a/daemon/graphdriver/driver_linux.go b/daemon/graphdriver/driver_linux.go new file mode 100644 index 00000000..2ab20b01 --- /dev/null +++ b/daemon/graphdriver/driver_linux.go @@ -0,0 +1,99 @@ +// +build linux + +package graphdriver + +import ( + "path/filepath" + "syscall" +) + +const ( + // FsMagicAufs filesystem id for Aufs + FsMagicAufs = FsMagic(0x61756673) + // FsMagicBtrfs filesystem id for Btrfs + FsMagicBtrfs = FsMagic(0x9123683E) + // FsMagicCramfs filesystem id for Cramfs + FsMagicCramfs = FsMagic(0x28cd3d45) + // FsMagicExtfs filesystem id for Extfs + FsMagicExtfs = FsMagic(0x0000EF53) + // FsMagicF2fs filesystem id for F2fs + FsMagicF2fs = FsMagic(0xF2F52010) + // FsMagicGPFS filesystem id for GPFS + FsMagicGPFS = FsMagic(0x47504653) + // FsMagicJffs2Fs filesystem if for Jffs2Fs + FsMagicJffs2Fs = FsMagic(0x000072b6) + // FsMagicJfs filesystem id for Jfs + FsMagicJfs = FsMagic(0x3153464a) + // FsMagicNfsFs filesystem id for NfsFs + FsMagicNfsFs = FsMagic(0x00006969) + // FsMagicRAMFs filesystem id for RamFs + FsMagicRAMFs = FsMagic(0x858458f6) + // FsMagicReiserFs filesystem id for ReiserFs + FsMagicReiserFs = FsMagic(0x52654973) + // FsMagicSmbFs filesystem id for SmbFs + FsMagicSmbFs = FsMagic(0x0000517B) + // FsMagicSquashFs filesystem id for SquashFs + FsMagicSquashFs = FsMagic(0x73717368) + // FsMagicTmpFs filesystem id for TmpFs + FsMagicTmpFs = FsMagic(0x01021994) + // FsMagicVxFS filesystem id for VxFs + FsMagicVxFS = FsMagic(0xa501fcf5) + // FsMagicXfs filesystem id for Xfs + FsMagicXfs = FsMagic(0x58465342) + // FsMagicZfs filesystem id for Zfs + FsMagicZfs = FsMagic(0x2fc12fc1) + // FsMagicOverlay filesystem id for overlay + FsMagicOverlay = FsMagic(0x794C7630) +) + +var ( + // Slice of drivers that should be used in an order + priority = []string{ + "aufs", + "btrfs", + "zfs", + "devicemapper", + "overlay", + "vfs", + } + + // FsNames maps filesystem id to name of the filesystem. + FsNames = map[FsMagic]string{ + FsMagicAufs: "aufs", + FsMagicBtrfs: "btrfs", + FsMagicCramfs: "cramfs", + FsMagicExtfs: "extfs", + FsMagicF2fs: "f2fs", + FsMagicGPFS: "gpfs", + FsMagicJffs2Fs: "jffs2", + FsMagicJfs: "jfs", + FsMagicNfsFs: "nfs", + FsMagicRAMFs: "ramfs", + FsMagicReiserFs: "reiserfs", + FsMagicSmbFs: "smb", + FsMagicSquashFs: "squashfs", + FsMagicTmpFs: "tmpfs", + FsMagicUnsupported: "unsupported", + FsMagicVxFS: "vxfs", + FsMagicXfs: "xfs", + FsMagicZfs: "zfs", + } +) + +// GetFSMagic returns the filesystem id given the path. +func GetFSMagic(rootpath string) (FsMagic, error) { + var buf syscall.Statfs_t + if err := syscall.Statfs(filepath.Dir(rootpath), &buf); err != nil { + return 0, err + } + return FsMagic(buf.Type), nil +} + +// Mounted checks if the given path is mounted as the fs type +func Mounted(fsType FsMagic, mountPath string) (bool, error) { + var buf syscall.Statfs_t + if err := syscall.Statfs(mountPath, &buf); err != nil { + return false, err + } + return FsMagic(buf.Type) == fsType, nil +} diff --git a/daemon/graphdriver/driver_unsupported.go b/daemon/graphdriver/driver_unsupported.go new file mode 100644 index 00000000..b3f68573 --- /dev/null +++ b/daemon/graphdriver/driver_unsupported.go @@ -0,0 +1,15 @@ +// +build !linux,!windows,!freebsd + +package graphdriver + +var ( + // Slice of drivers that should be used in an order + priority = []string{ + "unsupported", + } +) + +// GetFSMagic returns the filesystem id given the path. +func GetFSMagic(rootpath string) (FsMagic, error) { + return FsMagicUnsupported, nil +} diff --git a/daemon/graphdriver/driver_windows.go b/daemon/graphdriver/driver_windows.go new file mode 100644 index 00000000..6c09affa --- /dev/null +++ b/daemon/graphdriver/driver_windows.go @@ -0,0 +1,16 @@ +package graphdriver + +var ( + // Slice of drivers that should be used in order + priority = []string{ + "windowsfilter", + "windowsdiff", + "vfs", + } +) + +// GetFSMagic returns the filesystem id given the path. +func GetFSMagic(rootpath string) (FsMagic, error) { + // Note it is OK to return FsMagicUnsupported on Windows. + return FsMagicUnsupported, nil +} diff --git a/daemon/graphdriver/fsdiff.go b/daemon/graphdriver/fsdiff.go new file mode 100644 index 00000000..5a349325 --- /dev/null +++ b/daemon/graphdriver/fsdiff.go @@ -0,0 +1,162 @@ +package graphdriver + +import ( + "time" + + "github.com/Sirupsen/logrus" + + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/chrootarchive" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/ioutils" +) + +var ( + // ApplyUncompressedLayer defines the unpack method used by the graph + // driver. + ApplyUncompressedLayer = chrootarchive.ApplyUncompressedLayer +) + +// NaiveDiffDriver takes a ProtoDriver and adds the +// capability of the Diffing methods which it may or may not +// support on its own. See the comment on the exported +// NewNaiveDiffDriver function below. +// Notably, the AUFS driver doesn't need to be wrapped like this. +type NaiveDiffDriver struct { + ProtoDriver + uidMaps []idtools.IDMap + gidMaps []idtools.IDMap +} + +// NewNaiveDiffDriver returns a fully functional driver that wraps the +// given ProtoDriver and adds the capability of the following methods which +// it may or may not support on its own: +// Diff(id, parent string) (archive.Archive, error) +// Changes(id, parent string) ([]archive.Change, error) +// ApplyDiff(id, parent string, diff archive.Reader) (size int64, err error) +// DiffSize(id, parent string) (size int64, err error) +func NewNaiveDiffDriver(driver ProtoDriver, uidMaps, gidMaps []idtools.IDMap) Driver { + return &NaiveDiffDriver{ProtoDriver: driver, + uidMaps: uidMaps, + gidMaps: gidMaps} +} + +// Diff produces an archive of the changes between the specified +// layer and its parent layer which may be "". +func (gdw *NaiveDiffDriver) Diff(id, parent string) (arch archive.Archive, err error) { + driver := gdw.ProtoDriver + + layerFs, err := driver.Get(id, "") + if err != nil { + return nil, err + } + + defer func() { + if err != nil { + driver.Put(id) + } + }() + + if parent == "" { + archive, err := archive.Tar(layerFs, archive.Uncompressed) + if err != nil { + return nil, err + } + return ioutils.NewReadCloserWrapper(archive, func() error { + err := archive.Close() + driver.Put(id) + return err + }), nil + } + + parentFs, err := driver.Get(parent, "") + if err != nil { + return nil, err + } + defer driver.Put(parent) + + changes, err := archive.ChangesDirs(layerFs, parentFs) + if err != nil { + return nil, err + } + + archive, err := archive.ExportChanges(layerFs, changes, gdw.uidMaps, gdw.gidMaps) + if err != nil { + return nil, err + } + + return ioutils.NewReadCloserWrapper(archive, func() error { + err := archive.Close() + driver.Put(id) + return err + }), nil +} + +// Changes produces a list of changes between the specified layer +// and its parent layer. If parent is "", then all changes will be ADD changes. +func (gdw *NaiveDiffDriver) Changes(id, parent string) ([]archive.Change, error) { + driver := gdw.ProtoDriver + + layerFs, err := driver.Get(id, "") + if err != nil { + return nil, err + } + defer driver.Put(id) + + parentFs := "" + + if parent != "" { + parentFs, err = driver.Get(parent, "") + if err != nil { + return nil, err + } + defer driver.Put(parent) + } + + return archive.ChangesDirs(layerFs, parentFs) +} + +// ApplyDiff extracts the changeset from the given diff into the +// layer with the specified id and parent, returning the size of the +// new layer in bytes. +func (gdw *NaiveDiffDriver) ApplyDiff(id, parent string, diff archive.Reader) (size int64, err error) { + driver := gdw.ProtoDriver + + // Mount the root filesystem so we can apply the diff/layer. + layerFs, err := driver.Get(id, "") + if err != nil { + return + } + defer driver.Put(id) + + options := &archive.TarOptions{UIDMaps: gdw.uidMaps, + GIDMaps: gdw.gidMaps} + start := time.Now().UTC() + logrus.Debugf("Start untar layer") + if size, err = ApplyUncompressedLayer(layerFs, diff, options); err != nil { + return + } + logrus.Debugf("Untar time: %vs", time.Now().UTC().Sub(start).Seconds()) + + return +} + +// DiffSize calculates the changes between the specified layer +// and its parent and returns the size in bytes of the changes +// relative to its base filesystem directory. +func (gdw *NaiveDiffDriver) DiffSize(id, parent string) (size int64, err error) { + driver := gdw.ProtoDriver + + changes, err := gdw.Changes(id, parent) + if err != nil { + return + } + + layerFs, err := driver.Get(id, "") + if err != nil { + return + } + defer driver.Put(id) + + return archive.ChangesSize(layerFs, changes), nil +} diff --git a/daemon/graphdriver/graphtest/graphtest_unix.go b/daemon/graphdriver/graphtest/graphtest_unix.go new file mode 100644 index 00000000..534f2e58 --- /dev/null +++ b/daemon/graphdriver/graphtest/graphtest_unix.go @@ -0,0 +1,299 @@ +// +build linux freebsd + +package graphtest + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "syscall" + "testing" + + "github.com/docker/docker/daemon/graphdriver" +) + +var ( + drv *Driver +) + +// Driver conforms to graphdriver.Driver interface and +// contains information such as root and reference count of the number of clients using it. +// This helps in testing drivers added into the framework. +type Driver struct { + graphdriver.Driver + root string + refCount int +} + +// InitLoopbacks ensures that the loopback devices are properly created within +// the system running the device mapper tests. +func InitLoopbacks() error { + statT, err := getBaseLoopStats() + if err != nil { + return err + } + // create at least 8 loopback files, ya, that is a good number + for i := 0; i < 8; i++ { + loopPath := fmt.Sprintf("/dev/loop%d", i) + // only create new loopback files if they don't exist + if _, err := os.Stat(loopPath); err != nil { + if mkerr := syscall.Mknod(loopPath, + uint32(statT.Mode|syscall.S_IFBLK), int((7<<8)|(i&0xff)|((i&0xfff00)<<12))); mkerr != nil { + return mkerr + } + os.Chown(loopPath, int(statT.Uid), int(statT.Gid)) + } + } + return nil +} + +// getBaseLoopStats inspects /dev/loop0 to collect uid,gid, and mode for the +// loop0 device on the system. If it does not exist we assume 0,0,0660 for the +// stat data +func getBaseLoopStats() (*syscall.Stat_t, error) { + loop0, err := os.Stat("/dev/loop0") + if err != nil { + if os.IsNotExist(err) { + return &syscall.Stat_t{ + Uid: 0, + Gid: 0, + Mode: 0660, + }, nil + } + return nil, err + } + return loop0.Sys().(*syscall.Stat_t), nil +} + +func newDriver(t *testing.T, name string) *Driver { + root, err := ioutil.TempDir("/var/tmp", "docker-graphtest-") + if err != nil { + t.Fatal(err) + } + + if err := os.MkdirAll(root, 0755); err != nil { + t.Fatal(err) + } + + d, err := graphdriver.GetDriver(name, root, nil, nil, nil) + if err != nil { + t.Logf("graphdriver: %v\n", err) + if err == graphdriver.ErrNotSupported || err == graphdriver.ErrPrerequisites || err == graphdriver.ErrIncompatibleFS { + t.Skipf("Driver %s not supported", name) + } + t.Fatal(err) + } + return &Driver{d, root, 1} +} + +func cleanup(t *testing.T, d *Driver) { + if err := drv.Cleanup(); err != nil { + t.Fatal(err) + } + os.RemoveAll(d.root) +} + +// GetDriver create a new driver with given name or return a existing driver with the name updating the reference count. +func GetDriver(t *testing.T, name string) graphdriver.Driver { + if drv == nil { + drv = newDriver(t, name) + } else { + drv.refCount++ + } + return drv +} + +// PutDriver removes the driver if it is no longer used and updates the reference count. +func PutDriver(t *testing.T) { + if drv == nil { + t.Skip("No driver to put!") + } + drv.refCount-- + if drv.refCount == 0 { + cleanup(t, drv) + drv = nil + } +} + +func verifyFile(t *testing.T, path string, mode os.FileMode, uid, gid uint32) { + fi, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + + if fi.Mode()&os.ModeType != mode&os.ModeType { + t.Fatalf("Expected %s type 0x%x, got 0x%x", path, mode&os.ModeType, fi.Mode()&os.ModeType) + } + + if fi.Mode()&os.ModePerm != mode&os.ModePerm { + t.Fatalf("Expected %s mode %o, got %o", path, mode&os.ModePerm, fi.Mode()&os.ModePerm) + } + + if fi.Mode()&os.ModeSticky != mode&os.ModeSticky { + t.Fatalf("Expected %s sticky 0x%x, got 0x%x", path, mode&os.ModeSticky, fi.Mode()&os.ModeSticky) + } + + if fi.Mode()&os.ModeSetuid != mode&os.ModeSetuid { + t.Fatalf("Expected %s setuid 0x%x, got 0x%x", path, mode&os.ModeSetuid, fi.Mode()&os.ModeSetuid) + } + + if fi.Mode()&os.ModeSetgid != mode&os.ModeSetgid { + t.Fatalf("Expected %s setgid 0x%x, got 0x%x", path, mode&os.ModeSetgid, fi.Mode()&os.ModeSetgid) + } + + if stat, ok := fi.Sys().(*syscall.Stat_t); ok { + if stat.Uid != uid { + t.Fatalf("%s no owned by uid %d", path, uid) + } + if stat.Gid != gid { + t.Fatalf("%s not owned by gid %d", path, gid) + } + } + +} + +// readDir reads a directory just like ioutil.ReadDir() +// then hides specific files (currently "lost+found") +// so the tests don't "see" it +func readDir(dir string) ([]os.FileInfo, error) { + a, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + + b := a[:0] + for _, x := range a { + if x.Name() != "lost+found" { // ext4 always have this dir + b = append(b, x) + } + } + + return b, nil +} + +// DriverTestCreateEmpty creates an new image and verifies it is empty and the right metadata +func DriverTestCreateEmpty(t *testing.T, drivername string) { + driver := GetDriver(t, drivername) + defer PutDriver(t) + + if err := driver.Create("empty", "", ""); err != nil { + t.Fatal(err) + } + + if !driver.Exists("empty") { + t.Fatal("Newly created image doesn't exist") + } + + dir, err := driver.Get("empty", "") + if err != nil { + t.Fatal(err) + } + + verifyFile(t, dir, 0755|os.ModeDir, 0, 0) + + // Verify that the directory is empty + fis, err := readDir(dir) + if err != nil { + t.Fatal(err) + } + + if len(fis) != 0 { + t.Fatal("New directory not empty") + } + + driver.Put("empty") + + if err := driver.Remove("empty"); err != nil { + t.Fatal(err) + } + +} + +func createBase(t *testing.T, driver graphdriver.Driver, name string) { + // We need to be able to set any perms + oldmask := syscall.Umask(0) + defer syscall.Umask(oldmask) + + if err := driver.Create(name, "", ""); err != nil { + t.Fatal(err) + } + + dir, err := driver.Get(name, "") + if err != nil { + t.Fatal(err) + } + defer driver.Put(name) + + subdir := path.Join(dir, "a subdir") + if err := os.Mkdir(subdir, 0705|os.ModeSticky); err != nil { + t.Fatal(err) + } + if err := os.Chown(subdir, 1, 2); err != nil { + t.Fatal(err) + } + + file := path.Join(dir, "a file") + if err := ioutil.WriteFile(file, []byte("Some data"), 0222|os.ModeSetuid); err != nil { + t.Fatal(err) + } +} + +func verifyBase(t *testing.T, driver graphdriver.Driver, name string) { + dir, err := driver.Get(name, "") + if err != nil { + t.Fatal(err) + } + defer driver.Put(name) + + subdir := path.Join(dir, "a subdir") + verifyFile(t, subdir, 0705|os.ModeDir|os.ModeSticky, 1, 2) + + file := path.Join(dir, "a file") + verifyFile(t, file, 0222|os.ModeSetuid, 0, 0) + + fis, err := readDir(dir) + if err != nil { + t.Fatal(err) + } + + if len(fis) != 2 { + t.Fatal("Unexpected files in base image") + } + +} + +// DriverTestCreateBase create a base driver and verify. +func DriverTestCreateBase(t *testing.T, drivername string) { + driver := GetDriver(t, drivername) + defer PutDriver(t) + + createBase(t, driver, "Base") + verifyBase(t, driver, "Base") + + if err := driver.Remove("Base"); err != nil { + t.Fatal(err) + } +} + +// DriverTestCreateSnap Create a driver and snap and verify. +func DriverTestCreateSnap(t *testing.T, drivername string) { + driver := GetDriver(t, drivername) + defer PutDriver(t) + + createBase(t, driver, "Base") + + if err := driver.Create("Snap", "Base", ""); err != nil { + t.Fatal(err) + } + + verifyBase(t, driver, "Snap") + + if err := driver.Remove("Snap"); err != nil { + t.Fatal(err) + } + + if err := driver.Remove("Base"); err != nil { + t.Fatal(err) + } +} diff --git a/daemon/graphdriver/graphtest/graphtest_windows.go b/daemon/graphdriver/graphtest/graphtest_windows.go new file mode 100644 index 00000000..a50c5211 --- /dev/null +++ b/daemon/graphdriver/graphtest/graphtest_windows.go @@ -0,0 +1 @@ +package graphtest diff --git a/daemon/graphdriver/overlay/copy.go b/daemon/graphdriver/overlay/copy.go new file mode 100644 index 00000000..7d81a83a --- /dev/null +++ b/daemon/graphdriver/overlay/copy.go @@ -0,0 +1,169 @@ +// +build linux + +package overlay + +import ( + "fmt" + "os" + "path/filepath" + "syscall" + "time" + + "github.com/docker/docker/pkg/pools" + "github.com/docker/docker/pkg/system" +) + +type copyFlags int + +const ( + copyHardlink copyFlags = 1 << iota +) + +func copyRegular(srcPath, dstPath string, mode os.FileMode) error { + srcFile, err := os.Open(srcPath) + if err != nil { + return err + } + defer srcFile.Close() + + dstFile, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE, mode) + if err != nil { + return err + } + defer dstFile.Close() + + _, err = pools.Copy(dstFile, srcFile) + + return err +} + +func copyXattr(srcPath, dstPath, attr string) error { + data, err := system.Lgetxattr(srcPath, attr) + if err != nil { + return err + } + if data != nil { + if err := system.Lsetxattr(dstPath, attr, data, 0); err != nil { + return err + } + } + return nil +} + +func copyDir(srcDir, dstDir string, flags copyFlags) error { + err := filepath.Walk(srcDir, func(srcPath string, f os.FileInfo, err error) error { + if err != nil { + return err + } + + // Rebase path + relPath, err := filepath.Rel(srcDir, srcPath) + if err != nil { + return err + } + + dstPath := filepath.Join(dstDir, relPath) + if err != nil { + return err + } + + stat, ok := f.Sys().(*syscall.Stat_t) + if !ok { + return fmt.Errorf("Unable to get raw syscall.Stat_t data for %s", srcPath) + } + + isHardlink := false + + switch f.Mode() & os.ModeType { + case 0: // Regular file + if flags©Hardlink != 0 { + isHardlink = true + if err := os.Link(srcPath, dstPath); err != nil { + return err + } + } else { + if err := copyRegular(srcPath, dstPath, f.Mode()); err != nil { + return err + } + } + + case os.ModeDir: + if err := os.Mkdir(dstPath, f.Mode()); err != nil && !os.IsExist(err) { + return err + } + + case os.ModeSymlink: + link, err := os.Readlink(srcPath) + if err != nil { + return err + } + + if err := os.Symlink(link, dstPath); err != nil { + return err + } + + case os.ModeNamedPipe: + fallthrough + case os.ModeSocket: + if err := syscall.Mkfifo(dstPath, stat.Mode); err != nil { + return err + } + + case os.ModeDevice: + if err := syscall.Mknod(dstPath, stat.Mode, int(stat.Rdev)); err != nil { + return err + } + + default: + return fmt.Errorf("Unknown file type for %s\n", srcPath) + } + + // Everything below is copying metadata from src to dst. All this metadata + // already shares an inode for hardlinks. + if isHardlink { + return nil + } + + if err := os.Lchown(dstPath, int(stat.Uid), int(stat.Gid)); err != nil { + return err + } + + if err := copyXattr(srcPath, dstPath, "security.capability"); err != nil { + return err + } + + // We need to copy this attribute if it appears in an overlay upper layer, as + // this function is used to copy those. It is set by overlay if a directory + // is removed and then re-created and should not inherit anything from the + // same dir in the lower dir. + if err := copyXattr(srcPath, dstPath, "trusted.overlay.opaque"); err != nil { + return err + } + + isSymlink := f.Mode()&os.ModeSymlink != 0 + + // There is no LChmod, so ignore mode for symlink. Also, this + // must happen after chown, as that can modify the file mode + if !isSymlink { + if err := os.Chmod(dstPath, f.Mode()); err != nil { + return err + } + } + + // system.Chtimes doesn't support a NOFOLLOW flag atm + if !isSymlink { + aTime := time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec)) + mTime := time.Unix(int64(stat.Mtim.Sec), int64(stat.Mtim.Nsec)) + if err := system.Chtimes(dstPath, aTime, mTime); err != nil { + return err + } + } else { + ts := []syscall.Timespec{stat.Atim, stat.Mtim} + if err := system.LUtimesNano(dstPath, ts); err != nil { + return err + } + } + return nil + }) + return err +} diff --git a/daemon/graphdriver/overlay/overlay.go b/daemon/graphdriver/overlay/overlay.go new file mode 100644 index 00000000..33678f61 --- /dev/null +++ b/daemon/graphdriver/overlay/overlay.go @@ -0,0 +1,486 @@ +// +build linux + +package overlay + +import ( + "bufio" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path" + "sync" + "syscall" + + "github.com/Sirupsen/logrus" + + "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/chrootarchive" + "github.com/docker/docker/pkg/idtools" + + "github.com/opencontainers/runc/libcontainer/label" +) + +// This is a small wrapper over the NaiveDiffWriter that lets us have a custom +// implementation of ApplyDiff() + +var ( + // ErrApplyDiffFallback is returned to indicate that a normal ApplyDiff is applied as a fallback from Naive diff writer. + ErrApplyDiffFallback = fmt.Errorf("Fall back to normal ApplyDiff") +) + +// ApplyDiffProtoDriver wraps the ProtoDriver by extending the interface with ApplyDiff method. +type ApplyDiffProtoDriver interface { + graphdriver.ProtoDriver + // ApplyDiff writes the diff to the archive for the given id and parent id. + // It returns the size in bytes written if successful, an error ErrApplyDiffFallback is returned otherwise. + ApplyDiff(id, parent string, diff archive.Reader) (size int64, err error) +} + +type naiveDiffDriverWithApply struct { + graphdriver.Driver + applyDiff ApplyDiffProtoDriver +} + +// NaiveDiffDriverWithApply returns a NaiveDiff driver with custom ApplyDiff. +func NaiveDiffDriverWithApply(driver ApplyDiffProtoDriver, uidMaps, gidMaps []idtools.IDMap) graphdriver.Driver { + return &naiveDiffDriverWithApply{ + Driver: graphdriver.NewNaiveDiffDriver(driver, uidMaps, gidMaps), + applyDiff: driver, + } +} + +// ApplyDiff creates a diff layer with either the NaiveDiffDriver or with a fallback. +func (d *naiveDiffDriverWithApply) ApplyDiff(id, parent string, diff archive.Reader) (int64, error) { + b, err := d.applyDiff.ApplyDiff(id, parent, diff) + if err == ErrApplyDiffFallback { + return d.Driver.ApplyDiff(id, parent, diff) + } + return b, err +} + +// This backend uses the overlay union filesystem for containers +// plus hard link file sharing for images. + +// Each container/image can have a "root" subdirectory which is a plain +// filesystem hierarchy, or they can use overlay. + +// If they use overlay there is a "upper" directory and a "lower-id" +// file, as well as "merged" and "work" directories. The "upper" +// directory has the upper layer of the overlay, and "lower-id" contains +// the id of the parent whose "root" directory shall be used as the lower +// layer in the overlay. The overlay itself is mounted in the "merged" +// directory, and the "work" dir is needed for overlay to work. + +// When a overlay layer is created there are two cases, either the +// parent has a "root" dir, then we start out with a empty "upper" +// directory overlaid on the parents root. This is typically the +// case with the init layer of a container which is based on an image. +// If there is no "root" in the parent, we inherit the lower-id from +// the parent and start by making a copy in the parent's "upper" dir. +// This is typically the case for a container layer which copies +// its parent -init upper layer. + +// Additionally we also have a custom implementation of ApplyLayer +// which makes a recursive copy of the parent "root" layer using +// hardlinks to share file data, and then applies the layer on top +// of that. This means all child images share file (but not directory) +// data with the parent. + +// Driver contains information about the home directory and the list of active mounts that are created using this driver. +type Driver struct { + home string + pathCacheLock sync.Mutex + pathCache map[string]string + uidMaps []idtools.IDMap + gidMaps []idtools.IDMap + ctr *graphdriver.RefCounter +} + +var backingFs = "" + +func init() { + graphdriver.Register("overlay", Init) +} + +// Init returns the NaiveDiffDriver, a native diff driver for overlay filesystem. +// If overlay filesystem is not supported on the host, graphdriver.ErrNotSupported is returned as error. +// If a overlay filesystem is not supported over a existing filesystem then error graphdriver.ErrIncompatibleFS is returned. +func Init(home string, options []string, uidMaps, gidMaps []idtools.IDMap) (graphdriver.Driver, error) { + + if err := supportsOverlay(); err != nil { + return nil, graphdriver.ErrNotSupported + } + + fsMagic, err := graphdriver.GetFSMagic(home) + if err != nil { + return nil, err + } + if fsName, ok := graphdriver.FsNames[fsMagic]; ok { + backingFs = fsName + } + + // check if they are running over btrfs or aufs + switch fsMagic { + case graphdriver.FsMagicBtrfs: + logrus.Error("'overlay' is not supported over btrfs.") + return nil, graphdriver.ErrIncompatibleFS + case graphdriver.FsMagicAufs: + logrus.Error("'overlay' is not supported over aufs.") + return nil, graphdriver.ErrIncompatibleFS + case graphdriver.FsMagicZfs: + logrus.Error("'overlay' is not supported over zfs.") + return nil, graphdriver.ErrIncompatibleFS + } + + rootUID, rootGID, err := idtools.GetRootUIDGID(uidMaps, gidMaps) + if err != nil { + return nil, err + } + // Create the driver home dir + if err := idtools.MkdirAllAs(home, 0700, rootUID, rootGID); err != nil && !os.IsExist(err) { + return nil, err + } + + d := &Driver{ + home: home, + pathCache: make(map[string]string), + uidMaps: uidMaps, + gidMaps: gidMaps, + ctr: graphdriver.NewRefCounter(), + } + + return NaiveDiffDriverWithApply(d, uidMaps, gidMaps), nil +} + +func supportsOverlay() error { + // We can try to modprobe overlay first before looking at + // proc/filesystems for when overlay is supported + exec.Command("modprobe", "overlay").Run() + + f, err := os.Open("/proc/filesystems") + if err != nil { + return err + } + defer f.Close() + + s := bufio.NewScanner(f) + for s.Scan() { + if s.Text() == "nodev\toverlay" { + return nil + } + } + logrus.Error("'overlay' not found as a supported filesystem on this host. Please ensure kernel is new enough and has overlay support loaded.") + return graphdriver.ErrNotSupported +} + +func (d *Driver) String() string { + return "overlay" +} + +// Status returns current driver information in a two dimensional string array. +// Output contains "Backing Filesystem" used in this implementation. +func (d *Driver) Status() [][2]string { + return [][2]string{ + {"Backing Filesystem", backingFs}, + } +} + +// GetMetadata returns meta data about the overlay driver such as root, LowerDir, UpperDir, WorkDir and MergeDir used to store data. +func (d *Driver) GetMetadata(id string) (map[string]string, error) { + dir := d.dir(id) + if _, err := os.Stat(dir); err != nil { + return nil, err + } + + metadata := make(map[string]string) + + // If id has a root, it is an image + rootDir := path.Join(dir, "root") + if _, err := os.Stat(rootDir); err == nil { + metadata["RootDir"] = rootDir + return metadata, nil + } + + lowerID, err := ioutil.ReadFile(path.Join(dir, "lower-id")) + if err != nil { + return nil, err + } + + metadata["LowerDir"] = path.Join(d.dir(string(lowerID)), "root") + metadata["UpperDir"] = path.Join(dir, "upper") + metadata["WorkDir"] = path.Join(dir, "work") + metadata["MergedDir"] = path.Join(dir, "merged") + + return metadata, nil +} + +// Cleanup simply returns nil and do not change the existing filesystem. +// This is required to satisfy the graphdriver.Driver interface. +func (d *Driver) Cleanup() error { + return nil +} + +// Create is used to create the upper, lower, and merge directories required for overlay fs for a given id. +// The parent filesystem is used to configure these directories for the overlay. +func (d *Driver) Create(id, parent, mountLabel string) (retErr error) { + dir := d.dir(id) + + rootUID, rootGID, err := idtools.GetRootUIDGID(d.uidMaps, d.gidMaps) + if err != nil { + return err + } + if err := idtools.MkdirAllAs(path.Dir(dir), 0700, rootUID, rootGID); err != nil { + return err + } + if err := idtools.MkdirAs(dir, 0700, rootUID, rootGID); err != nil { + return err + } + + defer func() { + // Clean up on failure + if retErr != nil { + os.RemoveAll(dir) + } + }() + + // Toplevel images are just a "root" dir + if parent == "" { + if err := idtools.MkdirAs(path.Join(dir, "root"), 0755, rootUID, rootGID); err != nil { + return err + } + return nil + } + + parentDir := d.dir(parent) + + // Ensure parent exists + if _, err := os.Lstat(parentDir); err != nil { + return err + } + + // If parent has a root, just do a overlay to it + parentRoot := path.Join(parentDir, "root") + + if s, err := os.Lstat(parentRoot); err == nil { + if err := idtools.MkdirAs(path.Join(dir, "upper"), s.Mode(), rootUID, rootGID); err != nil { + return err + } + if err := idtools.MkdirAs(path.Join(dir, "work"), 0700, rootUID, rootGID); err != nil { + return err + } + if err := idtools.MkdirAs(path.Join(dir, "merged"), 0700, rootUID, rootGID); err != nil { + return err + } + if err := ioutil.WriteFile(path.Join(dir, "lower-id"), []byte(parent), 0666); err != nil { + return err + } + return nil + } + + // Otherwise, copy the upper and the lower-id from the parent + + lowerID, err := ioutil.ReadFile(path.Join(parentDir, "lower-id")) + if err != nil { + return err + } + + if err := ioutil.WriteFile(path.Join(dir, "lower-id"), lowerID, 0666); err != nil { + return err + } + + parentUpperDir := path.Join(parentDir, "upper") + s, err := os.Lstat(parentUpperDir) + if err != nil { + return err + } + + upperDir := path.Join(dir, "upper") + if err := idtools.MkdirAs(upperDir, s.Mode(), rootUID, rootGID); err != nil { + return err + } + if err := idtools.MkdirAs(path.Join(dir, "work"), 0700, rootUID, rootGID); err != nil { + return err + } + if err := idtools.MkdirAs(path.Join(dir, "merged"), 0700, rootUID, rootGID); err != nil { + return err + } + + return copyDir(parentUpperDir, upperDir, 0) +} + +func (d *Driver) dir(id string) string { + return path.Join(d.home, id) +} + +// Remove cleans the directories that are created for this id. +func (d *Driver) Remove(id string) error { + if err := os.RemoveAll(d.dir(id)); err != nil && !os.IsNotExist(err) { + return err + } + d.pathCacheLock.Lock() + delete(d.pathCache, id) + d.pathCacheLock.Unlock() + return nil +} + +// Get creates and mounts the required file system for the given id and returns the mount path. +func (d *Driver) Get(id string, mountLabel string) (string, error) { + dir := d.dir(id) + if _, err := os.Stat(dir); err != nil { + return "", err + } + + // If id has a root, just return it + rootDir := path.Join(dir, "root") + if _, err := os.Stat(rootDir); err == nil { + d.pathCacheLock.Lock() + d.pathCache[id] = rootDir + d.pathCacheLock.Unlock() + return rootDir, nil + } + + lowerID, err := ioutil.ReadFile(path.Join(dir, "lower-id")) + if err != nil { + return "", err + } + lowerDir := path.Join(d.dir(string(lowerID)), "root") + upperDir := path.Join(dir, "upper") + workDir := path.Join(dir, "work") + mergedDir := path.Join(dir, "merged") + + if count := d.ctr.Increment(id); count > 1 { + return mergedDir, nil + } + + opts := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", lowerDir, upperDir, workDir) + + // if it's mounted already, just return + mounted, err := d.mounted(mergedDir) + if err != nil { + d.ctr.Decrement(id) + return "", err + } + if mounted { + d.ctr.Decrement(id) + return mergedDir, nil + } + + if err := syscall.Mount("overlay", mergedDir, "overlay", 0, label.FormatMountLabel(opts, mountLabel)); err != nil { + d.ctr.Decrement(id) + return "", fmt.Errorf("error creating overlay mount to %s: %v", mergedDir, err) + } + // chown "workdir/work" to the remapped root UID/GID. Overlay fs inside a + // user namespace requires this to move a directory from lower to upper. + rootUID, rootGID, err := idtools.GetRootUIDGID(d.uidMaps, d.gidMaps) + if err != nil { + d.ctr.Decrement(id) + syscall.Unmount(mergedDir, 0) + return "", err + } + + if err := os.Chown(path.Join(workDir, "work"), rootUID, rootGID); err != nil { + d.ctr.Decrement(id) + syscall.Unmount(mergedDir, 0) + return "", err + } + + d.pathCacheLock.Lock() + d.pathCache[id] = mergedDir + d.pathCacheLock.Unlock() + + return mergedDir, nil +} + +func (d *Driver) mounted(dir string) (bool, error) { + return graphdriver.Mounted(graphdriver.FsMagicOverlay, dir) +} + +// Put unmounts the mount path created for the give id. +func (d *Driver) Put(id string) error { + if count := d.ctr.Decrement(id); count > 0 { + return nil + } + d.pathCacheLock.Lock() + mountpoint, exists := d.pathCache[id] + d.pathCacheLock.Unlock() + + if !exists { + logrus.Debugf("Put on a non-mounted device %s", id) + // but it might be still here + if d.Exists(id) { + mountpoint = path.Join(d.dir(id), "merged") + } + + d.pathCacheLock.Lock() + d.pathCache[id] = mountpoint + d.pathCacheLock.Unlock() + } + + if mounted, err := d.mounted(mountpoint); mounted || err != nil { + if err = syscall.Unmount(mountpoint, 0); err != nil { + logrus.Debugf("Failed to unmount %s overlay: %v", id, err) + } + return err + } + return nil +} + +// ApplyDiff applies the new layer on top of the root, if parent does not exist with will return a ErrApplyDiffFallback error. +func (d *Driver) ApplyDiff(id string, parent string, diff archive.Reader) (size int64, err error) { + dir := d.dir(id) + + if parent == "" { + return 0, ErrApplyDiffFallback + } + + parentRootDir := path.Join(d.dir(parent), "root") + if _, err := os.Stat(parentRootDir); err != nil { + return 0, ErrApplyDiffFallback + } + + // We now know there is a parent, and it has a "root" directory containing + // the full root filesystem. We can just hardlink it and apply the + // layer. This relies on two things: + // 1) ApplyDiff is only run once on a clean (no writes to upper layer) container + // 2) ApplyDiff doesn't do any in-place writes to files (would break hardlinks) + // These are all currently true and are not expected to break + + tmpRootDir, err := ioutil.TempDir(dir, "tmproot") + if err != nil { + return 0, err + } + defer func() { + if err != nil { + os.RemoveAll(tmpRootDir) + } else { + os.RemoveAll(path.Join(dir, "upper")) + os.RemoveAll(path.Join(dir, "work")) + os.RemoveAll(path.Join(dir, "merged")) + os.RemoveAll(path.Join(dir, "lower-id")) + } + }() + + if err = copyDir(parentRootDir, tmpRootDir, copyHardlink); err != nil { + return 0, err + } + + options := &archive.TarOptions{UIDMaps: d.uidMaps, GIDMaps: d.gidMaps} + if size, err = chrootarchive.ApplyUncompressedLayer(tmpRootDir, diff, options); err != nil { + return 0, err + } + + rootDir := path.Join(dir, "root") + if err := os.Rename(tmpRootDir, rootDir); err != nil { + return 0, err + } + + return +} + +// Exists checks to see if the id is already mounted. +func (d *Driver) Exists(id string) bool { + _, err := os.Stat(d.dir(id)) + return err == nil +} diff --git a/daemon/graphdriver/overlay/overlay_test.go b/daemon/graphdriver/overlay/overlay_test.go new file mode 100644 index 00000000..8dfcd62e --- /dev/null +++ b/daemon/graphdriver/overlay/overlay_test.go @@ -0,0 +1,31 @@ +// +build linux + +package overlay + +import ( + "testing" + + "github.com/docker/docker/daemon/graphdriver/graphtest" +) + +// This avoids creating a new driver for each test if all tests are run +// Make sure to put new tests between TestOverlaySetup and TestOverlayTeardown +func TestOverlaySetup(t *testing.T) { + graphtest.GetDriver(t, "overlay") +} + +func TestOverlayCreateEmpty(t *testing.T) { + graphtest.DriverTestCreateEmpty(t, "overlay") +} + +func TestOverlayCreateBase(t *testing.T) { + graphtest.DriverTestCreateBase(t, "overlay") +} + +func TestOverlayCreateSnap(t *testing.T) { + graphtest.DriverTestCreateSnap(t, "overlay") +} + +func TestOverlayTeardown(t *testing.T) { + graphtest.PutDriver(t) +} diff --git a/daemon/graphdriver/overlay/overlay_unsupported.go b/daemon/graphdriver/overlay/overlay_unsupported.go new file mode 100644 index 00000000..3dbb4de4 --- /dev/null +++ b/daemon/graphdriver/overlay/overlay_unsupported.go @@ -0,0 +1,3 @@ +// +build !linux + +package overlay diff --git a/daemon/graphdriver/plugin.go b/daemon/graphdriver/plugin.go new file mode 100644 index 00000000..d63161b0 --- /dev/null +++ b/daemon/graphdriver/plugin.go @@ -0,0 +1,32 @@ +// +build experimental + +package graphdriver + +import ( + "fmt" + "io" + + "github.com/docker/docker/pkg/plugins" +) + +type pluginClient interface { + // Call calls the specified method with the specified arguments for the plugin. + Call(string, interface{}, interface{}) error + // Stream calls the specified method with the specified arguments for the plugin and returns the response IO stream + Stream(string, interface{}) (io.ReadCloser, error) + // SendFile calls the specified method, and passes through the IO stream + SendFile(string, io.Reader, interface{}) error +} + +func lookupPlugin(name, home string, opts []string) (Driver, error) { + pl, err := plugins.Get(name, "GraphDriver") + if err != nil { + return nil, fmt.Errorf("Error looking up graphdriver plugin %s: %v", name, err) + } + return newPluginDriver(name, home, opts, pl.Client) +} + +func newPluginDriver(name, home string, opts []string, c pluginClient) (Driver, error) { + proxy := &graphDriverProxy{name, c} + return proxy, proxy.Init(home, opts) +} diff --git a/daemon/graphdriver/plugin_unsupported.go b/daemon/graphdriver/plugin_unsupported.go new file mode 100644 index 00000000..daa7a170 --- /dev/null +++ b/daemon/graphdriver/plugin_unsupported.go @@ -0,0 +1,7 @@ +// +build !experimental + +package graphdriver + +func lookupPlugin(name, home string, opts []string) (Driver, error) { + return nil, ErrNotSupported +} diff --git a/daemon/graphdriver/proxy.go b/daemon/graphdriver/proxy.go new file mode 100644 index 00000000..28657fef --- /dev/null +++ b/daemon/graphdriver/proxy.go @@ -0,0 +1,209 @@ +// +build experimental + +package graphdriver + +import ( + "errors" + "fmt" + + "github.com/docker/docker/pkg/archive" +) + +type graphDriverProxy struct { + name string + client pluginClient +} + +type graphDriverRequest struct { + ID string `json:",omitempty"` + Parent string `json:",omitempty"` + MountLabel string `json:",omitempty"` +} + +type graphDriverResponse struct { + Err string `json:",omitempty"` + Dir string `json:",omitempty"` + Exists bool `json:",omitempty"` + Status [][2]string `json:",omitempty"` + Changes []archive.Change `json:",omitempty"` + Size int64 `json:",omitempty"` + Metadata map[string]string `json:",omitempty"` +} + +type graphDriverInitRequest struct { + Home string + Opts []string +} + +func (d *graphDriverProxy) Init(home string, opts []string) error { + args := &graphDriverInitRequest{ + Home: home, + Opts: opts, + } + var ret graphDriverResponse + if err := d.client.Call("GraphDriver.Init", args, &ret); err != nil { + return err + } + if ret.Err != "" { + return errors.New(ret.Err) + } + return nil +} + +func (d *graphDriverProxy) String() string { + return d.name +} + +func (d *graphDriverProxy) Create(id, parent, mountLabel string) error { + args := &graphDriverRequest{ + ID: id, + Parent: parent, + MountLabel: mountLabel, + } + var ret graphDriverResponse + if err := d.client.Call("GraphDriver.Create", args, &ret); err != nil { + return err + } + if ret.Err != "" { + return errors.New(ret.Err) + } + return nil +} + +func (d *graphDriverProxy) Remove(id string) error { + args := &graphDriverRequest{ID: id} + var ret graphDriverResponse + if err := d.client.Call("GraphDriver.Remove", args, &ret); err != nil { + return err + } + if ret.Err != "" { + return errors.New(ret.Err) + } + return nil +} + +func (d *graphDriverProxy) Get(id, mountLabel string) (string, error) { + args := &graphDriverRequest{ + ID: id, + MountLabel: mountLabel, + } + var ret graphDriverResponse + if err := d.client.Call("GraphDriver.Get", args, &ret); err != nil { + return "", err + } + var err error + if ret.Err != "" { + err = errors.New(ret.Err) + } + return ret.Dir, err +} + +func (d *graphDriverProxy) Put(id string) error { + args := &graphDriverRequest{ID: id} + var ret graphDriverResponse + if err := d.client.Call("GraphDriver.Put", args, &ret); err != nil { + return err + } + if ret.Err != "" { + return errors.New(ret.Err) + } + return nil +} + +func (d *graphDriverProxy) Exists(id string) bool { + args := &graphDriverRequest{ID: id} + var ret graphDriverResponse + if err := d.client.Call("GraphDriver.Exists", args, &ret); err != nil { + return false + } + return ret.Exists +} + +func (d *graphDriverProxy) Status() [][2]string { + args := &graphDriverRequest{} + var ret graphDriverResponse + if err := d.client.Call("GraphDriver.Status", args, &ret); err != nil { + return nil + } + return ret.Status +} + +func (d *graphDriverProxy) GetMetadata(id string) (map[string]string, error) { + args := &graphDriverRequest{ + ID: id, + } + var ret graphDriverResponse + if err := d.client.Call("GraphDriver.GetMetadata", args, &ret); err != nil { + return nil, err + } + if ret.Err != "" { + return nil, errors.New(ret.Err) + } + return ret.Metadata, nil +} + +func (d *graphDriverProxy) Cleanup() error { + args := &graphDriverRequest{} + var ret graphDriverResponse + if err := d.client.Call("GraphDriver.Cleanup", args, &ret); err != nil { + return nil + } + if ret.Err != "" { + return errors.New(ret.Err) + } + return nil +} + +func (d *graphDriverProxy) Diff(id, parent string) (archive.Archive, error) { + args := &graphDriverRequest{ + ID: id, + Parent: parent, + } + body, err := d.client.Stream("GraphDriver.Diff", args) + if err != nil { + return nil, err + } + return archive.Archive(body), nil +} + +func (d *graphDriverProxy) Changes(id, parent string) ([]archive.Change, error) { + args := &graphDriverRequest{ + ID: id, + Parent: parent, + } + var ret graphDriverResponse + if err := d.client.Call("GraphDriver.Changes", args, &ret); err != nil { + return nil, err + } + if ret.Err != "" { + return nil, errors.New(ret.Err) + } + + return ret.Changes, nil +} + +func (d *graphDriverProxy) ApplyDiff(id, parent string, diff archive.Reader) (int64, error) { + var ret graphDriverResponse + if err := d.client.SendFile(fmt.Sprintf("GraphDriver.ApplyDiff?id=%s&parent=%s", id, parent), diff, &ret); err != nil { + return -1, err + } + if ret.Err != "" { + return -1, errors.New(ret.Err) + } + return ret.Size, nil +} + +func (d *graphDriverProxy) DiffSize(id, parent string) (int64, error) { + args := &graphDriverRequest{ + ID: id, + Parent: parent, + } + var ret graphDriverResponse + if err := d.client.Call("GraphDriver.DiffSize", args, &ret); err != nil { + return -1, err + } + if ret.Err != "" { + return -1, errors.New(ret.Err) + } + return ret.Size, nil +} diff --git a/daemon/graphdriver/register/register_aufs.go b/daemon/graphdriver/register/register_aufs.go new file mode 100644 index 00000000..262954d6 --- /dev/null +++ b/daemon/graphdriver/register/register_aufs.go @@ -0,0 +1,8 @@ +// +build !exclude_graphdriver_aufs,linux + +package register + +import ( + // register the aufs graphdriver + _ "github.com/docker/docker/daemon/graphdriver/aufs" +) diff --git a/daemon/graphdriver/register/register_btrfs.go b/daemon/graphdriver/register/register_btrfs.go new file mode 100644 index 00000000..f456cc5c --- /dev/null +++ b/daemon/graphdriver/register/register_btrfs.go @@ -0,0 +1,8 @@ +// +build !exclude_graphdriver_btrfs,linux + +package register + +import ( + // register the btrfs graphdriver + _ "github.com/docker/docker/daemon/graphdriver/btrfs" +) diff --git a/daemon/graphdriver/register/register_devicemapper.go b/daemon/graphdriver/register/register_devicemapper.go new file mode 100644 index 00000000..bb2e9ef5 --- /dev/null +++ b/daemon/graphdriver/register/register_devicemapper.go @@ -0,0 +1,8 @@ +// +build !exclude_graphdriver_devicemapper,linux + +package register + +import ( + // register the devmapper graphdriver + _ "github.com/docker/docker/daemon/graphdriver/devmapper" +) diff --git a/daemon/graphdriver/register/register_overlay.go b/daemon/graphdriver/register/register_overlay.go new file mode 100644 index 00000000..3a952642 --- /dev/null +++ b/daemon/graphdriver/register/register_overlay.go @@ -0,0 +1,8 @@ +// +build !exclude_graphdriver_overlay,linux + +package register + +import ( + // register the overlay graphdriver + _ "github.com/docker/docker/daemon/graphdriver/overlay" +) diff --git a/daemon/graphdriver/register/register_vfs.go b/daemon/graphdriver/register/register_vfs.go new file mode 100644 index 00000000..98fad23b --- /dev/null +++ b/daemon/graphdriver/register/register_vfs.go @@ -0,0 +1,6 @@ +package register + +import ( + // register vfs + _ "github.com/docker/docker/daemon/graphdriver/vfs" +) diff --git a/daemon/graphdriver/register/register_windows.go b/daemon/graphdriver/register/register_windows.go new file mode 100644 index 00000000..efaa5005 --- /dev/null +++ b/daemon/graphdriver/register/register_windows.go @@ -0,0 +1,6 @@ +package register + +import ( + // register the windows graph driver + _ "github.com/docker/docker/daemon/graphdriver/windows" +) diff --git a/daemon/graphdriver/register/register_zfs.go b/daemon/graphdriver/register/register_zfs.go new file mode 100644 index 00000000..8c31c415 --- /dev/null +++ b/daemon/graphdriver/register/register_zfs.go @@ -0,0 +1,8 @@ +// +build !exclude_graphdriver_zfs,linux !exclude_graphdriver_zfs,freebsd + +package register + +import ( + // register the zfs driver + _ "github.com/docker/docker/daemon/graphdriver/zfs" +) diff --git a/daemon/graphdriver/vfs/driver.go b/daemon/graphdriver/vfs/driver.go new file mode 100644 index 00000000..00d9f8ec --- /dev/null +++ b/daemon/graphdriver/vfs/driver.go @@ -0,0 +1,135 @@ +package vfs + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/pkg/chrootarchive" + "github.com/docker/docker/pkg/idtools" + + "github.com/opencontainers/runc/libcontainer/label" +) + +var ( + // CopyWithTar defines the copy method to use. + CopyWithTar = chrootarchive.CopyWithTar +) + +func init() { + graphdriver.Register("vfs", Init) +} + +// Init returns a new VFS driver. +// This sets the home directory for the driver and returns NaiveDiffDriver. +func Init(home string, options []string, uidMaps, gidMaps []idtools.IDMap) (graphdriver.Driver, error) { + d := &Driver{ + home: home, + uidMaps: uidMaps, + gidMaps: gidMaps, + } + rootUID, rootGID, err := idtools.GetRootUIDGID(uidMaps, gidMaps) + if err != nil { + return nil, err + } + if err := idtools.MkdirAllAs(home, 0700, rootUID, rootGID); err != nil { + return nil, err + } + return graphdriver.NewNaiveDiffDriver(d, uidMaps, gidMaps), nil +} + +// Driver holds information about the driver, home directory of the driver. +// Driver implements graphdriver.ProtoDriver. It uses only basic vfs operations. +// In order to support layering, files are copied from the parent layer into the new layer. There is no copy-on-write support. +// Driver must be wrapped in NaiveDiffDriver to be used as a graphdriver.Driver +type Driver struct { + home string + uidMaps []idtools.IDMap + gidMaps []idtools.IDMap +} + +func (d *Driver) String() string { + return "vfs" +} + +// Status is used for implementing the graphdriver.ProtoDriver interface. VFS does not currently have any status information. +func (d *Driver) Status() [][2]string { + return nil +} + +// GetMetadata is used for implementing the graphdriver.ProtoDriver interface. VFS does not currently have any meta data. +func (d *Driver) GetMetadata(id string) (map[string]string, error) { + return nil, nil +} + +// Cleanup is used to implement graphdriver.ProtoDriver. There is no cleanup required for this driver. +func (d *Driver) Cleanup() error { + return nil +} + +// Create prepares the filesystem for the VFS driver and copies the directory for the given id under the parent. +func (d *Driver) Create(id, parent, mountLabel string) error { + dir := d.dir(id) + rootUID, rootGID, err := idtools.GetRootUIDGID(d.uidMaps, d.gidMaps) + if err != nil { + return err + } + if err := idtools.MkdirAllAs(filepath.Dir(dir), 0700, rootUID, rootGID); err != nil { + return err + } + if err := idtools.MkdirAs(dir, 0755, rootUID, rootGID); err != nil { + return err + } + opts := []string{"level:s0"} + if _, mountLabel, err := label.InitLabels(opts); err == nil { + label.SetFileLabel(dir, mountLabel) + } + if parent == "" { + return nil + } + parentDir, err := d.Get(parent, "") + if err != nil { + return fmt.Errorf("%s: %s", parent, err) + } + if err := CopyWithTar(parentDir, dir); err != nil { + return err + } + return nil +} + +func (d *Driver) dir(id string) string { + return filepath.Join(d.home, "dir", filepath.Base(id)) +} + +// Remove deletes the content from the directory for a given id. +func (d *Driver) Remove(id string) error { + if err := os.RemoveAll(d.dir(id)); err != nil && !os.IsNotExist(err) { + return err + } + return nil +} + +// Get returns the directory for the given id. +func (d *Driver) Get(id, mountLabel string) (string, error) { + dir := d.dir(id) + if st, err := os.Stat(dir); err != nil { + return "", err + } else if !st.IsDir() { + return "", fmt.Errorf("%s: not a directory", dir) + } + return dir, nil +} + +// Put is a noop for vfs that return nil for the error, since this driver has no runtime resources to clean up. +func (d *Driver) Put(id string) error { + // The vfs driver has no runtime resources (e.g. mounts) + // to clean up, so we don't need anything here + return nil +} + +// Exists checks to see if the directory exists for the given id. +func (d *Driver) Exists(id string) bool { + _, err := os.Stat(d.dir(id)) + return err == nil +} diff --git a/daemon/graphdriver/vfs/vfs_test.go b/daemon/graphdriver/vfs/vfs_test.go new file mode 100644 index 00000000..9ecf21db --- /dev/null +++ b/daemon/graphdriver/vfs/vfs_test.go @@ -0,0 +1,37 @@ +// +build linux + +package vfs + +import ( + "testing" + + "github.com/docker/docker/daemon/graphdriver/graphtest" + + "github.com/docker/docker/pkg/reexec" +) + +func init() { + reexec.Init() +} + +// This avoids creating a new driver for each test if all tests are run +// Make sure to put new tests between TestVfsSetup and TestVfsTeardown +func TestVfsSetup(t *testing.T) { + graphtest.GetDriver(t, "vfs") +} + +func TestVfsCreateEmpty(t *testing.T) { + graphtest.DriverTestCreateEmpty(t, "vfs") +} + +func TestVfsCreateBase(t *testing.T) { + graphtest.DriverTestCreateBase(t, "vfs") +} + +func TestVfsCreateSnap(t *testing.T) { + graphtest.DriverTestCreateSnap(t, "vfs") +} + +func TestVfsTeardown(t *testing.T) { + graphtest.PutDriver(t) +} diff --git a/daemon/graphdriver/windows/windows.go b/daemon/graphdriver/windows/windows.go new file mode 100644 index 00000000..dd659dad --- /dev/null +++ b/daemon/graphdriver/windows/windows.go @@ -0,0 +1,745 @@ +//+build windows + +package windows + +import ( + "bufio" + "crypto/sha512" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path" + "path/filepath" + "strings" + "syscall" + "time" + + "github.com/Microsoft/go-winio" + "github.com/Microsoft/go-winio/archive/tar" + "github.com/Microsoft/go-winio/backuptar" + "github.com/Microsoft/hcsshim" + "github.com/Sirupsen/logrus" + "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/chrootarchive" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/ioutils" + "github.com/vbatts/tar-split/tar/storage" +) + +// init registers the windows graph drivers to the register. +func init() { + graphdriver.Register("windowsfilter", InitFilter) + graphdriver.Register("windowsdiff", InitDiff) +} + +const ( + // diffDriver is an hcsshim driver type + diffDriver = iota + // filterDriver is an hcsshim driver type + filterDriver +) + +// Driver represents a windows graph driver. +type Driver struct { + // info stores the shim driver information + info hcsshim.DriverInfo +} + +var _ graphdriver.DiffGetterDriver = &Driver{} + +// InitFilter returns a new Windows storage filter driver. +func InitFilter(home string, options []string, uidMaps, gidMaps []idtools.IDMap) (graphdriver.Driver, error) { + logrus.Debugf("WindowsGraphDriver InitFilter at %s", home) + d := &Driver{ + info: hcsshim.DriverInfo{ + HomeDir: home, + Flavour: filterDriver, + }, + } + return d, nil +} + +// InitDiff returns a new Windows differencing disk driver. +func InitDiff(home string, options []string, uidMaps, gidMaps []idtools.IDMap) (graphdriver.Driver, error) { + logrus.Debugf("WindowsGraphDriver InitDiff at %s", home) + d := &Driver{ + info: hcsshim.DriverInfo{ + HomeDir: home, + Flavour: diffDriver, + }, + } + return d, nil +} + +// String returns the string representation of a driver. +func (d *Driver) String() string { + switch d.info.Flavour { + case diffDriver: + return "windowsdiff" + case filterDriver: + return "windowsfilter" + default: + return "Unknown driver flavour" + } +} + +// Status returns the status of the driver. +func (d *Driver) Status() [][2]string { + return [][2]string{ + {"Windows", ""}, + } +} + +// Exists returns true if the given id is registered with this driver. +func (d *Driver) Exists(id string) bool { + rID, err := d.resolveID(id) + if err != nil { + return false + } + result, err := hcsshim.LayerExists(d.info, rID) + if err != nil { + return false + } + return result +} + +// Create creates a new layer with the given id. +func (d *Driver) Create(id, parent, mountLabel string) error { + rPId, err := d.resolveID(parent) + if err != nil { + return err + } + + parentChain, err := d.getLayerChain(rPId) + if err != nil { + return err + } + + var layerChain []string + + parentIsInit := strings.HasSuffix(rPId, "-init") + + if !parentIsInit && rPId != "" { + parentPath, err := hcsshim.GetLayerMountPath(d.info, rPId) + if err != nil { + return err + } + layerChain = []string{parentPath} + } + + layerChain = append(layerChain, parentChain...) + + if parentIsInit { + if len(layerChain) == 0 { + return fmt.Errorf("Cannot create a read/write layer without a parent layer.") + } + if err := hcsshim.CreateSandboxLayer(d.info, id, layerChain[0], layerChain); err != nil { + return err + } + } else { + if err := hcsshim.CreateLayer(d.info, id, rPId); err != nil { + return err + } + } + + if _, err := os.Lstat(d.dir(parent)); err != nil { + if err2 := hcsshim.DestroyLayer(d.info, id); err2 != nil { + logrus.Warnf("Failed to DestroyLayer %s: %s", id, err2) + } + return fmt.Errorf("Cannot create layer with missing parent %s: %s", parent, err) + } + + if err := d.setLayerChain(id, layerChain); err != nil { + if err2 := hcsshim.DestroyLayer(d.info, id); err2 != nil { + logrus.Warnf("Failed to DestroyLayer %s: %s", id, err2) + } + return err + } + + return nil +} + +// dir returns the absolute path to the layer. +func (d *Driver) dir(id string) string { + return filepath.Join(d.info.HomeDir, filepath.Base(id)) +} + +// Remove unmounts and removes the dir information. +func (d *Driver) Remove(id string) error { + rID, err := d.resolveID(id) + if err != nil { + return err + } + os.RemoveAll(filepath.Join(d.info.HomeDir, "sysfile-backups", rID)) // ok to fail + return hcsshim.DestroyLayer(d.info, rID) +} + +// Get returns the rootfs path for the id. This will mount the dir at it's given path. +func (d *Driver) Get(id, mountLabel string) (string, error) { + logrus.Debugf("WindowsGraphDriver Get() id %s mountLabel %s", id, mountLabel) + var dir string + + rID, err := d.resolveID(id) + if err != nil { + return "", err + } + + // Getting the layer paths must be done outside of the lock. + layerChain, err := d.getLayerChain(rID) + if err != nil { + return "", err + } + + if err := hcsshim.ActivateLayer(d.info, rID); err != nil { + return "", err + } + if err := hcsshim.PrepareLayer(d.info, rID, layerChain); err != nil { + if err2 := hcsshim.DeactivateLayer(d.info, rID); err2 != nil { + logrus.Warnf("Failed to Deactivate %s: %s", id, err) + } + return "", err + } + + mountPath, err := hcsshim.GetLayerMountPath(d.info, rID) + if err != nil { + if err2 := hcsshim.DeactivateLayer(d.info, rID); err2 != nil { + logrus.Warnf("Failed to Deactivate %s: %s", id, err) + } + return "", err + } + + // If the layer has a mount path, use that. Otherwise, use the + // folder path. + if mountPath != "" { + dir = mountPath + } else { + dir = d.dir(id) + } + + return dir, nil +} + +// Put adds a new layer to the driver. +func (d *Driver) Put(id string) error { + logrus.Debugf("WindowsGraphDriver Put() id %s", id) + + rID, err := d.resolveID(id) + if err != nil { + return err + } + + if err := hcsshim.UnprepareLayer(d.info, rID); err != nil { + return err + } + return hcsshim.DeactivateLayer(d.info, rID) +} + +// Cleanup ensures the information the driver stores is properly removed. +func (d *Driver) Cleanup() error { + return nil +} + +// Diff produces an archive of the changes between the specified +// layer and its parent layer which may be "". +// The layer should be mounted when calling this function +func (d *Driver) Diff(id, parent string) (_ archive.Archive, err error) { + rID, err := d.resolveID(id) + if err != nil { + return + } + + layerChain, err := d.getLayerChain(rID) + if err != nil { + return + } + + // this is assuming that the layer is unmounted + if err := hcsshim.UnprepareLayer(d.info, rID); err != nil { + return nil, err + } + defer func() { + if err := hcsshim.PrepareLayer(d.info, rID, layerChain); err != nil { + logrus.Warnf("Failed to Deactivate %s: %s", rID, err) + } + }() + + arch, err := d.exportLayer(rID, layerChain) + if err != nil { + return + } + return ioutils.NewReadCloserWrapper(arch, func() error { + return arch.Close() + }), nil +} + +// Changes produces a list of changes between the specified layer +// and its parent layer. If parent is "", then all changes will be ADD changes. +// The layer should be mounted when calling this function +func (d *Driver) Changes(id, parent string) ([]archive.Change, error) { + rID, err := d.resolveID(id) + if err != nil { + return nil, err + } + parentChain, err := d.getLayerChain(rID) + if err != nil { + return nil, err + } + + // this is assuming that the layer is unmounted + if err := hcsshim.UnprepareLayer(d.info, rID); err != nil { + return nil, err + } + defer func() { + if err := hcsshim.PrepareLayer(d.info, rID, parentChain); err != nil { + logrus.Warnf("Failed to Deactivate %s: %s", rID, err) + } + }() + + r, err := hcsshim.NewLayerReader(d.info, id, parentChain) + if err != nil { + return nil, err + } + defer r.Close() + + var changes []archive.Change + for { + name, _, fileInfo, err := r.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + name = filepath.ToSlash(name) + if fileInfo == nil { + changes = append(changes, archive.Change{name, archive.ChangeDelete}) + } else { + // Currently there is no way to tell between an add and a modify. + changes = append(changes, archive.Change{name, archive.ChangeModify}) + } + } + return changes, nil +} + +// ApplyDiff extracts the changeset from the given diff into the +// layer with the specified id and parent, returning the size of the +// new layer in bytes. +// The layer should not be mounted when calling this function +func (d *Driver) ApplyDiff(id, parent string, diff archive.Reader) (size int64, err error) { + rPId, err := d.resolveID(parent) + if err != nil { + return + } + + if d.info.Flavour == diffDriver { + start := time.Now().UTC() + logrus.Debugf("WindowsGraphDriver ApplyDiff: Start untar layer") + destination := d.dir(id) + destination = filepath.Dir(destination) + if size, err = chrootarchive.ApplyUncompressedLayer(destination, diff, nil); err != nil { + return + } + logrus.Debugf("WindowsGraphDriver ApplyDiff: Untar time: %vs", time.Now().UTC().Sub(start).Seconds()) + + return + } + + parentChain, err := d.getLayerChain(rPId) + if err != nil { + return + } + parentPath, err := hcsshim.GetLayerMountPath(d.info, rPId) + if err != nil { + return + } + layerChain := []string{parentPath} + layerChain = append(layerChain, parentChain...) + + if size, err = d.importLayer(id, diff, layerChain); err != nil { + return + } + + if err = d.setLayerChain(id, layerChain); err != nil { + return + } + + return +} + +// DiffSize calculates the changes between the specified layer +// and its parent and returns the size in bytes of the changes +// relative to its base filesystem directory. +func (d *Driver) DiffSize(id, parent string) (size int64, err error) { + rPId, err := d.resolveID(parent) + if err != nil { + return + } + + changes, err := d.Changes(id, rPId) + if err != nil { + return + } + + layerFs, err := d.Get(id, "") + if err != nil { + return + } + defer d.Put(id) + + return archive.ChangesSize(layerFs, changes), nil +} + +// CustomImageInfo is the object returned by the driver describing the base +// image. +type CustomImageInfo struct { + ID string + Name string + Version string + Path string + Size int64 + CreatedTime time.Time +} + +// GetCustomImageInfos returns the image infos for window specific +// base images which should always be present. +func (d *Driver) GetCustomImageInfos() ([]CustomImageInfo, error) { + strData, err := hcsshim.GetSharedBaseImages() + if err != nil { + return nil, fmt.Errorf("Failed to restore base images: %s", err) + } + + type customImageInfoList struct { + Images []CustomImageInfo + } + + var infoData customImageInfoList + + if err = json.Unmarshal([]byte(strData), &infoData); err != nil { + err = fmt.Errorf("JSON unmarshal returned error=%s", err) + logrus.Error(err) + return nil, err + } + + var images []CustomImageInfo + + for _, imageData := range infoData.Images { + folderName := filepath.Base(imageData.Path) + + // Use crypto hash of the foldername to generate a docker style id. + h := sha512.Sum384([]byte(folderName)) + id := fmt.Sprintf("%x", h[:32]) + + if err := d.Create(id, "", ""); err != nil { + return nil, err + } + // Create the alternate ID file. + if err := d.setID(id, folderName); err != nil { + return nil, err + } + + imageData.ID = id + images = append(images, imageData) + } + + return images, nil +} + +// GetMetadata returns custom driver information. +func (d *Driver) GetMetadata(id string) (map[string]string, error) { + m := make(map[string]string) + m["dir"] = d.dir(id) + return m, nil +} + +func writeTarFromLayer(r hcsshim.LayerReader, w io.Writer) error { + t := tar.NewWriter(w) + for { + name, size, fileInfo, err := r.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + if fileInfo == nil { + // Write a whiteout file. + hdr := &tar.Header{ + Name: filepath.ToSlash(filepath.Join(filepath.Dir(name), archive.WhiteoutPrefix+filepath.Base(name))), + } + err := t.WriteHeader(hdr) + if err != nil { + return err + } + } else { + err = backuptar.WriteTarFileFromBackupStream(t, r, name, size, fileInfo) + if err != nil { + return err + } + } + } + return t.Close() +} + +// exportLayer generates an archive from a layer based on the given ID. +func (d *Driver) exportLayer(id string, parentLayerPaths []string) (archive.Archive, error) { + if hcsshim.IsTP4() { + // Export in TP4 format to maintain compatibility with existing images and + // because ExportLayer is somewhat broken on TP4 and can't work with the new + // scheme. + tempFolder, err := ioutil.TempDir("", "hcs") + if err != nil { + return nil, err + } + defer func() { + if err != nil { + os.RemoveAll(tempFolder) + } + }() + + if err = hcsshim.ExportLayer(d.info, id, tempFolder, parentLayerPaths); err != nil { + return nil, err + } + archive, err := archive.Tar(tempFolder, archive.Uncompressed) + if err != nil { + return nil, err + } + return ioutils.NewReadCloserWrapper(archive, func() error { + err := archive.Close() + os.RemoveAll(tempFolder) + return err + }), nil + } + + var r hcsshim.LayerReader + r, err := hcsshim.NewLayerReader(d.info, id, parentLayerPaths) + if err != nil { + return nil, err + } + + archive, w := io.Pipe() + go func() { + err := writeTarFromLayer(r, w) + cerr := r.Close() + if err == nil { + err = cerr + } + w.CloseWithError(err) + }() + + return archive, nil +} + +func writeLayerFromTar(r archive.Reader, w hcsshim.LayerWriter) (int64, error) { + t := tar.NewReader(r) + hdr, err := t.Next() + totalSize := int64(0) + buf := bufio.NewWriter(nil) + for err == nil { + base := path.Base(hdr.Name) + if strings.HasPrefix(base, archive.WhiteoutPrefix) { + name := path.Join(path.Dir(hdr.Name), base[len(archive.WhiteoutPrefix):]) + err = w.Remove(filepath.FromSlash(name)) + if err != nil { + return 0, err + } + hdr, err = t.Next() + } else { + var ( + name string + size int64 + fileInfo *winio.FileBasicInfo + ) + name, size, fileInfo, err = backuptar.FileInfoFromHeader(hdr) + if err != nil { + return 0, err + } + err = w.Add(filepath.FromSlash(name), fileInfo) + if err != nil { + return 0, err + } + buf.Reset(w) + hdr, err = backuptar.WriteBackupStreamFromTarFile(buf, t, hdr) + ferr := buf.Flush() + if ferr != nil { + err = ferr + } + totalSize += size + } + } + if err != io.EOF { + return 0, err + } + return totalSize, nil +} + +// importLayer adds a new layer to the tag and graph store based on the given data. +func (d *Driver) importLayer(id string, layerData archive.Reader, parentLayerPaths []string) (size int64, err error) { + if hcsshim.IsTP4() { + // Import from TP4 format to maintain compatibility with existing images. + var tempFolder string + tempFolder, err = ioutil.TempDir("", "hcs") + if err != nil { + return + } + defer os.RemoveAll(tempFolder) + + if size, err = chrootarchive.ApplyLayer(tempFolder, layerData); err != nil { + return + } + if err = hcsshim.ImportLayer(d.info, id, tempFolder, parentLayerPaths); err != nil { + return + } + return + } + + var w hcsshim.LayerWriter + w, err = hcsshim.NewLayerWriter(d.info, id, parentLayerPaths) + if err != nil { + return + } + + size, err = writeLayerFromTar(layerData, w) + if err != nil { + w.Close() + return + } + err = w.Close() + if err != nil { + return + } + return +} + +// resolveID computes the layerID information based on the given id. +func (d *Driver) resolveID(id string) (string, error) { + content, err := ioutil.ReadFile(filepath.Join(d.dir(id), "layerID")) + if os.IsNotExist(err) { + return id, nil + } else if err != nil { + return "", err + } + return string(content), nil +} + +// setID stores the layerId in disk. +func (d *Driver) setID(id, altID string) error { + err := ioutil.WriteFile(filepath.Join(d.dir(id), "layerId"), []byte(altID), 0600) + if err != nil { + return err + } + return nil +} + +// getLayerChain returns the layer chain information. +func (d *Driver) getLayerChain(id string) ([]string, error) { + jPath := filepath.Join(d.dir(id), "layerchain.json") + content, err := ioutil.ReadFile(jPath) + if os.IsNotExist(err) { + return nil, nil + } else if err != nil { + return nil, fmt.Errorf("Unable to read layerchain file - %s", err) + } + + var layerChain []string + err = json.Unmarshal(content, &layerChain) + if err != nil { + return nil, fmt.Errorf("Failed to unmarshall layerchain json - %s", err) + } + + return layerChain, nil +} + +// setLayerChain stores the layer chain information in disk. +func (d *Driver) setLayerChain(id string, chain []string) error { + content, err := json.Marshal(&chain) + if err != nil { + return fmt.Errorf("Failed to marshall layerchain json - %s", err) + } + + jPath := filepath.Join(d.dir(id), "layerchain.json") + err = ioutil.WriteFile(jPath, content, 0600) + if err != nil { + return fmt.Errorf("Unable to write layerchain file - %s", err) + } + + return nil +} + +type fileGetCloserWithBackupPrivileges struct { + path string +} + +func (fg *fileGetCloserWithBackupPrivileges) Get(filename string) (io.ReadCloser, error) { + var f *os.File + // Open the file while holding the Windows backup privilege. This ensures that the + // file can be opened even if the caller does not actually have access to it according + // to the security descriptor. + err := winio.RunWithPrivilege(winio.SeBackupPrivilege, func() error { + path := filepath.Join(fg.path, filename) + p, err := syscall.UTF16FromString(path) + if err != nil { + return err + } + h, err := syscall.CreateFile(&p[0], syscall.GENERIC_READ, syscall.FILE_SHARE_READ, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS, 0) + if err != nil { + return &os.PathError{Op: "open", Path: path, Err: err} + } + f = os.NewFile(uintptr(h), path) + return nil + }) + return f, err +} + +func (fg *fileGetCloserWithBackupPrivileges) Close() error { + return nil +} + +type fileGetDestroyCloser struct { + storage.FileGetter + path string +} + +func (f *fileGetDestroyCloser) Close() error { + // TODO: activate layers and release here? + return os.RemoveAll(f.path) +} + +// DiffGetter returns a FileGetCloser that can read files from the directory that +// contains files for the layer differences. Used for direct access for tar-split. +func (d *Driver) DiffGetter(id string) (graphdriver.FileGetCloser, error) { + id, err := d.resolveID(id) + if err != nil { + return nil, err + } + + if hcsshim.IsTP4() { + // The export format for TP4 is different from the contents of the layer, so + // fall back to exporting the layer and getting file contents from there. + layerChain, err := d.getLayerChain(id) + if err != nil { + return nil, err + } + + var tempFolder string + tempFolder, err = ioutil.TempDir("", "hcs") + if err != nil { + return nil, err + } + defer func() { + if err != nil { + os.RemoveAll(tempFolder) + } + }() + + if err = hcsshim.ExportLayer(d.info, id, tempFolder, layerChain); err != nil { + return nil, err + } + + return &fileGetDestroyCloser{storage.NewPathFileGetter(tempFolder), tempFolder}, nil + } + + return &fileGetCloserWithBackupPrivileges{d.dir(id)}, nil +} diff --git a/daemon/graphdriver/zfs/MAINTAINERS b/daemon/graphdriver/zfs/MAINTAINERS new file mode 100644 index 00000000..9c270c54 --- /dev/null +++ b/daemon/graphdriver/zfs/MAINTAINERS @@ -0,0 +1,2 @@ +Jörg Thalheim (@Mic92) +Arthur Gautier (@baloose) diff --git a/daemon/graphdriver/zfs/zfs.go b/daemon/graphdriver/zfs/zfs.go new file mode 100644 index 00000000..2db187ce --- /dev/null +++ b/daemon/graphdriver/zfs/zfs.go @@ -0,0 +1,359 @@ +// +build linux freebsd + +package zfs + +import ( + "fmt" + "os" + "os/exec" + "path" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/mount" + "github.com/docker/docker/pkg/parsers" + zfs "github.com/mistifyio/go-zfs" + "github.com/opencontainers/runc/libcontainer/label" +) + +type zfsOptions struct { + fsName string + mountPath string +} + +func init() { + graphdriver.Register("zfs", Init) +} + +// Logger returns a zfs logger implementation. +type Logger struct{} + +// Log wraps log message from ZFS driver with a prefix '[zfs]'. +func (*Logger) Log(cmd []string) { + logrus.Debugf("[zfs] %s", strings.Join(cmd, " ")) +} + +// Init returns a new ZFS driver. +// It takes base mount path and a array of options which are represented as key value pairs. +// Each option is in the for key=value. 'zfs.fsname' is expected to be a valid key in the options. +func Init(base string, opt []string, uidMaps, gidMaps []idtools.IDMap) (graphdriver.Driver, error) { + var err error + + if _, err := exec.LookPath("zfs"); err != nil { + logrus.Debugf("[zfs] zfs command is not available: %v", err) + return nil, graphdriver.ErrPrerequisites + } + + file, err := os.OpenFile("/dev/zfs", os.O_RDWR, 600) + if err != nil { + logrus.Debugf("[zfs] cannot open /dev/zfs: %v", err) + return nil, graphdriver.ErrPrerequisites + } + defer file.Close() + + options, err := parseOptions(opt) + if err != nil { + return nil, err + } + options.mountPath = base + + rootdir := path.Dir(base) + + if options.fsName == "" { + err = checkRootdirFs(rootdir) + if err != nil { + return nil, err + } + } + + if options.fsName == "" { + options.fsName, err = lookupZfsDataset(rootdir) + if err != nil { + return nil, err + } + } + + zfs.SetLogger(new(Logger)) + + filesystems, err := zfs.Filesystems(options.fsName) + if err != nil { + return nil, fmt.Errorf("Cannot find root filesystem %s: %v", options.fsName, err) + } + + filesystemsCache := make(map[string]bool, len(filesystems)) + var rootDataset *zfs.Dataset + for _, fs := range filesystems { + if fs.Name == options.fsName { + rootDataset = fs + } + filesystemsCache[fs.Name] = true + } + + if rootDataset == nil { + return nil, fmt.Errorf("BUG: zfs get all -t filesystem -rHp '%s' should contain '%s'", options.fsName, options.fsName) + } + + d := &Driver{ + dataset: rootDataset, + options: options, + filesystemsCache: filesystemsCache, + uidMaps: uidMaps, + gidMaps: gidMaps, + ctr: graphdriver.NewRefCounter(), + } + return graphdriver.NewNaiveDiffDriver(d, uidMaps, gidMaps), nil +} + +func parseOptions(opt []string) (zfsOptions, error) { + var options zfsOptions + options.fsName = "" + for _, option := range opt { + key, val, err := parsers.ParseKeyValueOpt(option) + if err != nil { + return options, err + } + key = strings.ToLower(key) + switch key { + case "zfs.fsname": + options.fsName = val + default: + return options, fmt.Errorf("Unknown option %s", key) + } + } + return options, nil +} + +func lookupZfsDataset(rootdir string) (string, error) { + var stat syscall.Stat_t + if err := syscall.Stat(rootdir, &stat); err != nil { + return "", fmt.Errorf("Failed to access '%s': %s", rootdir, err) + } + wantedDev := stat.Dev + + mounts, err := mount.GetMounts() + if err != nil { + return "", err + } + for _, m := range mounts { + if err := syscall.Stat(m.Mountpoint, &stat); err != nil { + logrus.Debugf("[zfs] failed to stat '%s' while scanning for zfs mount: %v", m.Mountpoint, err) + continue // may fail on fuse file systems + } + + if stat.Dev == wantedDev && m.Fstype == "zfs" { + return m.Source, nil + } + } + + return "", fmt.Errorf("Failed to find zfs dataset mounted on '%s' in /proc/mounts", rootdir) +} + +// Driver holds information about the driver, such as zfs dataset, options and cache. +type Driver struct { + dataset *zfs.Dataset + options zfsOptions + sync.Mutex // protects filesystem cache against concurrent access + filesystemsCache map[string]bool + uidMaps []idtools.IDMap + gidMaps []idtools.IDMap + ctr *graphdriver.RefCounter +} + +func (d *Driver) String() string { + return "zfs" +} + +// Cleanup is used to implement graphdriver.ProtoDriver. There is no cleanup required for this driver. +func (d *Driver) Cleanup() error { + return nil +} + +// Status returns information about the ZFS filesystem. It returns a two dimensional array of information +// such as pool name, dataset name, disk usage, parent quota and compression used. +// Currently it return 'Zpool', 'Zpool Health', 'Parent Dataset', 'Space Used By Parent', +// 'Space Available', 'Parent Quota' and 'Compression'. +func (d *Driver) Status() [][2]string { + parts := strings.Split(d.dataset.Name, "/") + pool, err := zfs.GetZpool(parts[0]) + + var poolName, poolHealth string + if err == nil { + poolName = pool.Name + poolHealth = pool.Health + } else { + poolName = fmt.Sprintf("error while getting pool information %v", err) + poolHealth = "not available" + } + + quota := "no" + if d.dataset.Quota != 0 { + quota = strconv.FormatUint(d.dataset.Quota, 10) + } + + return [][2]string{ + {"Zpool", poolName}, + {"Zpool Health", poolHealth}, + {"Parent Dataset", d.dataset.Name}, + {"Space Used By Parent", strconv.FormatUint(d.dataset.Used, 10)}, + {"Space Available", strconv.FormatUint(d.dataset.Avail, 10)}, + {"Parent Quota", quota}, + {"Compression", d.dataset.Compression}, + } +} + +// GetMetadata returns image/container metadata related to graph driver +func (d *Driver) GetMetadata(id string) (map[string]string, error) { + return nil, nil +} + +func (d *Driver) cloneFilesystem(name, parentName string) error { + snapshotName := fmt.Sprintf("%d", time.Now().Nanosecond()) + parentDataset := zfs.Dataset{Name: parentName} + snapshot, err := parentDataset.Snapshot(snapshotName /*recursive */, false) + if err != nil { + return err + } + + _, err = snapshot.Clone(name, map[string]string{"mountpoint": "legacy"}) + if err == nil { + d.Lock() + d.filesystemsCache[name] = true + d.Unlock() + } + + if err != nil { + snapshot.Destroy(zfs.DestroyDeferDeletion) + return err + } + return snapshot.Destroy(zfs.DestroyDeferDeletion) +} + +func (d *Driver) zfsPath(id string) string { + return d.options.fsName + "/" + id +} + +func (d *Driver) mountPath(id string) string { + return path.Join(d.options.mountPath, "graph", getMountpoint(id)) +} + +// Create prepares the dataset and filesystem for the ZFS driver for the given id under the parent. +func (d *Driver) Create(id string, parent string, mountLabel string) error { + err := d.create(id, parent) + if err == nil { + return nil + } + if zfsError, ok := err.(*zfs.Error); ok { + if !strings.HasSuffix(zfsError.Stderr, "dataset already exists\n") { + return err + } + // aborted build -> cleanup + } else { + return err + } + + dataset := zfs.Dataset{Name: d.zfsPath(id)} + if err := dataset.Destroy(zfs.DestroyRecursiveClones); err != nil { + return err + } + + // retry + return d.create(id, parent) +} + +func (d *Driver) create(id, parent string) error { + name := d.zfsPath(id) + if parent == "" { + mountoptions := map[string]string{"mountpoint": "legacy"} + fs, err := zfs.CreateFilesystem(name, mountoptions) + if err == nil { + d.Lock() + d.filesystemsCache[fs.Name] = true + d.Unlock() + } + return err + } + return d.cloneFilesystem(name, d.zfsPath(parent)) +} + +// Remove deletes the dataset, filesystem and the cache for the given id. +func (d *Driver) Remove(id string) error { + name := d.zfsPath(id) + dataset := zfs.Dataset{Name: name} + err := dataset.Destroy(zfs.DestroyRecursive) + if err == nil { + d.Lock() + delete(d.filesystemsCache, name) + d.Unlock() + } + return err +} + +// Get returns the mountpoint for the given id after creating the target directories if necessary. +func (d *Driver) Get(id, mountLabel string) (string, error) { + mountpoint := d.mountPath(id) + if count := d.ctr.Increment(id); count > 1 { + return mountpoint, nil + } + + filesystem := d.zfsPath(id) + options := label.FormatMountLabel("", mountLabel) + logrus.Debugf(`[zfs] mount("%s", "%s", "%s")`, filesystem, mountpoint, options) + + rootUID, rootGID, err := idtools.GetRootUIDGID(d.uidMaps, d.gidMaps) + if err != nil { + d.ctr.Decrement(id) + return "", err + } + // Create the target directories if they don't exist + if err := idtools.MkdirAllAs(mountpoint, 0755, rootUID, rootGID); err != nil { + d.ctr.Decrement(id) + return "", err + } + + if err := mount.Mount(filesystem, mountpoint, "zfs", options); err != nil { + d.ctr.Decrement(id) + return "", fmt.Errorf("error creating zfs mount of %s to %s: %v", filesystem, mountpoint, err) + } + + // this could be our first mount after creation of the filesystem, and the root dir may still have root + // permissions instead of the remapped root uid:gid (if user namespaces are enabled): + if err := os.Chown(mountpoint, rootUID, rootGID); err != nil { + mount.Unmount(mountpoint) + d.ctr.Decrement(id) + return "", fmt.Errorf("error modifying zfs mountpoint (%s) directory ownership: %v", mountpoint, err) + } + + return mountpoint, nil +} + +// Put removes the existing mountpoint for the given id if it exists. +func (d *Driver) Put(id string) error { + if count := d.ctr.Decrement(id); count > 0 { + return nil + } + mountpoint := d.mountPath(id) + mounted, err := graphdriver.Mounted(graphdriver.FsMagicZfs, mountpoint) + if err != nil || !mounted { + return err + } + + logrus.Debugf(`[zfs] unmount("%s")`, mountpoint) + + if err := mount.Unmount(mountpoint); err != nil { + return fmt.Errorf("error unmounting to %s: %v", mountpoint, err) + } + return nil +} + +// Exists checks to see if the cache entry exists for the given id. +func (d *Driver) Exists(id string) bool { + d.Lock() + defer d.Unlock() + return d.filesystemsCache[d.zfsPath(id)] == true +} diff --git a/daemon/graphdriver/zfs/zfs_freebsd.go b/daemon/graphdriver/zfs/zfs_freebsd.go new file mode 100644 index 00000000..1c05fa79 --- /dev/null +++ b/daemon/graphdriver/zfs/zfs_freebsd.go @@ -0,0 +1,38 @@ +package zfs + +import ( + "fmt" + "strings" + "syscall" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/daemon/graphdriver" +) + +func checkRootdirFs(rootdir string) error { + var buf syscall.Statfs_t + if err := syscall.Statfs(rootdir, &buf); err != nil { + return fmt.Errorf("Failed to access '%s': %s", rootdir, err) + } + + // on FreeBSD buf.Fstypename contains ['z', 'f', 's', 0 ... ] + if (buf.Fstypename[0] != 122) || (buf.Fstypename[1] != 102) || (buf.Fstypename[2] != 115) || (buf.Fstypename[3] != 0) { + logrus.Debugf("[zfs] no zfs dataset found for rootdir '%s'", rootdir) + return graphdriver.ErrPrerequisites + } + + return nil +} + +func getMountpoint(id string) string { + maxlen := 12 + + // we need to preserve filesystem suffix + suffix := strings.SplitN(id, "-", 2) + + if len(suffix) > 1 { + return id[:maxlen] + "-" + suffix[1] + } + + return id[:maxlen] +} diff --git a/daemon/graphdriver/zfs/zfs_linux.go b/daemon/graphdriver/zfs/zfs_linux.go new file mode 100644 index 00000000..52ed5160 --- /dev/null +++ b/daemon/graphdriver/zfs/zfs_linux.go @@ -0,0 +1,27 @@ +package zfs + +import ( + "fmt" + "syscall" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/daemon/graphdriver" +) + +func checkRootdirFs(rootdir string) error { + var buf syscall.Statfs_t + if err := syscall.Statfs(rootdir, &buf); err != nil { + return fmt.Errorf("Failed to access '%s': %s", rootdir, err) + } + + if graphdriver.FsMagic(buf.Type) != graphdriver.FsMagicZfs { + logrus.Debugf("[zfs] no zfs dataset found for rootdir '%s'", rootdir) + return graphdriver.ErrPrerequisites + } + + return nil +} + +func getMountpoint(id string) string { + return id +} diff --git a/daemon/graphdriver/zfs/zfs_test.go b/daemon/graphdriver/zfs/zfs_test.go new file mode 100644 index 00000000..0e7937ec --- /dev/null +++ b/daemon/graphdriver/zfs/zfs_test.go @@ -0,0 +1,31 @@ +// +build linux + +package zfs + +import ( + "testing" + + "github.com/docker/docker/daemon/graphdriver/graphtest" +) + +// This avoids creating a new driver for each test if all tests are run +// Make sure to put new tests between TestZfsSetup and TestZfsTeardown +func TestZfsSetup(t *testing.T) { + graphtest.GetDriver(t, "zfs") +} + +func TestZfsCreateEmpty(t *testing.T) { + graphtest.DriverTestCreateEmpty(t, "zfs") +} + +func TestZfsCreateBase(t *testing.T) { + graphtest.DriverTestCreateBase(t, "zfs") +} + +func TestZfsCreateSnap(t *testing.T) { + graphtest.DriverTestCreateSnap(t, "zfs") +} + +func TestZfsTeardown(t *testing.T) { + graphtest.PutDriver(t) +} diff --git a/daemon/graphdriver/zfs/zfs_unsupported.go b/daemon/graphdriver/zfs/zfs_unsupported.go new file mode 100644 index 00000000..643b169b --- /dev/null +++ b/daemon/graphdriver/zfs/zfs_unsupported.go @@ -0,0 +1,11 @@ +// +build !linux,!freebsd + +package zfs + +func checkRootdirFs(rootdir string) error { + return nil +} + +func getMountpoint(id string) string { + return id +} diff --git a/daemon/image_delete.go b/daemon/image_delete.go new file mode 100644 index 00000000..7c6329a6 --- /dev/null +++ b/daemon/image_delete.go @@ -0,0 +1,371 @@ +package daemon + +import ( + "fmt" + "strings" + + "github.com/docker/docker/container" + "github.com/docker/docker/errors" + "github.com/docker/docker/image" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/reference" + "github.com/docker/engine-api/types" +) + +type conflictType int + +const ( + conflictDependentChild conflictType = (1 << iota) + conflictRunningContainer + conflictActiveReference + conflictStoppedContainer + conflictHard = conflictDependentChild | conflictRunningContainer + conflictSoft = conflictActiveReference | conflictStoppedContainer +) + +// ImageDelete deletes the image referenced by the given imageRef from this +// daemon. The given imageRef can be an image ID, ID prefix, or a repository +// reference (with an optional tag or digest, defaulting to the tag name +// "latest"). There is differing behavior depending on whether the given +// imageRef is a repository reference or not. +// +// If the given imageRef is a repository reference then that repository +// reference will be removed. However, if there exists any containers which +// were created using the same image reference then the repository reference +// cannot be removed unless either there are other repository references to the +// same image or force is true. Following removal of the repository reference, +// the referenced image itself will attempt to be deleted as described below +// but quietly, meaning any image delete conflicts will cause the image to not +// be deleted and the conflict will not be reported. +// +// There may be conflicts preventing deletion of an image and these conflicts +// are divided into two categories grouped by their severity: +// +// Hard Conflict: +// - a pull or build using the image. +// - any descendant image. +// - any running container using the image. +// +// Soft Conflict: +// - any stopped container using the image. +// - any repository tag or digest references to the image. +// +// The image cannot be removed if there are any hard conflicts and can be +// removed if there are soft conflicts only if force is true. +// +// If prune is true, ancestor images will each attempt to be deleted quietly, +// meaning any delete conflicts will cause the image to not be deleted and the +// conflict will not be reported. +// +// FIXME: remove ImageDelete's dependency on Daemon, then move to the graph +// package. This would require that we no longer need the daemon to determine +// whether images are being used by a stopped or running container. +func (daemon *Daemon) ImageDelete(imageRef string, force, prune bool) ([]types.ImageDelete, error) { + records := []types.ImageDelete{} + + imgID, err := daemon.GetImageID(imageRef) + if err != nil { + return nil, daemon.imageNotExistToErrcode(err) + } + + repoRefs := daemon.referenceStore.References(imgID) + + var removedRepositoryRef bool + if !isImageIDPrefix(imgID.String(), imageRef) { + // A repository reference was given and should be removed + // first. We can only remove this reference if either force is + // true, there are multiple repository references to this + // image, or there are no containers using the given reference. + if !(force || len(repoRefs) > 1) { + if container := daemon.getContainerUsingImage(imgID); container != nil { + // If we removed the repository reference then + // this image would remain "dangling" and since + // we really want to avoid that the client must + // explicitly force its removal. + err := fmt.Errorf("conflict: unable to remove repository reference %q (must force) - container %s is using its referenced image %s", imageRef, stringid.TruncateID(container.ID), stringid.TruncateID(imgID.String())) + return nil, errors.NewRequestConflictError(err) + } + } + + parsedRef, err := reference.ParseNamed(imageRef) + if err != nil { + return nil, err + } + + parsedRef, err = daemon.removeImageRef(parsedRef) + if err != nil { + return nil, err + } + + untaggedRecord := types.ImageDelete{Untagged: parsedRef.String()} + + daemon.LogImageEvent(imgID.String(), imgID.String(), "untag") + records = append(records, untaggedRecord) + + repoRefs = daemon.referenceStore.References(imgID) + + // If this is a tag reference and all the remaining references + // to this image are digest references, delete the remaining + // references so that they don't prevent removal of the image. + if _, isCanonical := parsedRef.(reference.Canonical); !isCanonical { + foundTagRef := false + for _, repoRef := range repoRefs { + if _, repoRefIsCanonical := repoRef.(reference.Canonical); !repoRefIsCanonical { + foundTagRef = true + break + } + } + if !foundTagRef { + for _, repoRef := range repoRefs { + if _, err := daemon.removeImageRef(repoRef); err != nil { + return records, err + } + + untaggedRecord := types.ImageDelete{Untagged: repoRef.String()} + records = append(records, untaggedRecord) + } + repoRefs = []reference.Named{} + } + } + + // If it has remaining references then the untag finished the remove + if len(repoRefs) > 0 { + return records, nil + } + + removedRepositoryRef = true + } else { + // If an ID reference was given AND there is exactly one + // repository reference to the image then we will want to + // remove that reference. + // FIXME: Is this the behavior we want? + if len(repoRefs) == 1 { + c := conflictHard + if !force { + c |= conflictSoft &^ conflictActiveReference + } + if conflict := daemon.checkImageDeleteConflict(imgID, c); conflict != nil { + return nil, conflict + } + + parsedRef, err := daemon.removeImageRef(repoRefs[0]) + if err != nil { + return nil, err + } + + untaggedRecord := types.ImageDelete{Untagged: parsedRef.String()} + + daemon.LogImageEvent(imgID.String(), imgID.String(), "untag") + records = append(records, untaggedRecord) + } + } + + return records, daemon.imageDeleteHelper(imgID, &records, force, prune, removedRepositoryRef) +} + +// isImageIDPrefix returns whether the given possiblePrefix is a prefix of the +// given imageID. +func isImageIDPrefix(imageID, possiblePrefix string) bool { + if strings.HasPrefix(imageID, possiblePrefix) { + return true + } + + if i := strings.IndexRune(imageID, ':'); i >= 0 { + return strings.HasPrefix(imageID[i+1:], possiblePrefix) + } + + return false +} + +// getContainerUsingImage returns a container that was created using the given +// imageID. Returns nil if there is no such container. +func (daemon *Daemon) getContainerUsingImage(imageID image.ID) *container.Container { + return daemon.containers.First(func(c *container.Container) bool { + return c.ImageID == imageID + }) +} + +// removeImageRef attempts to parse and remove the given image reference from +// this daemon's store of repository tag/digest references. The given +// repositoryRef must not be an image ID but a repository name followed by an +// optional tag or digest reference. If tag or digest is omitted, the default +// tag is used. Returns the resolved image reference and an error. +func (daemon *Daemon) removeImageRef(ref reference.Named) (reference.Named, error) { + ref = reference.WithDefaultTag(ref) + // Ignore the boolean value returned, as far as we're concerned, this + // is an idempotent operation and it's okay if the reference didn't + // exist in the first place. + _, err := daemon.referenceStore.Delete(ref) + + return ref, err +} + +// removeAllReferencesToImageID attempts to remove every reference to the given +// imgID from this daemon's store of repository tag/digest references. Returns +// on the first encountered error. Removed references are logged to this +// daemon's event service. An "Untagged" types.ImageDelete is added to the +// given list of records. +func (daemon *Daemon) removeAllReferencesToImageID(imgID image.ID, records *[]types.ImageDelete) error { + imageRefs := daemon.referenceStore.References(imgID) + + for _, imageRef := range imageRefs { + parsedRef, err := daemon.removeImageRef(imageRef) + if err != nil { + return err + } + + untaggedRecord := types.ImageDelete{Untagged: parsedRef.String()} + + daemon.LogImageEvent(imgID.String(), imgID.String(), "untag") + *records = append(*records, untaggedRecord) + } + + return nil +} + +// ImageDeleteConflict holds a soft or hard conflict and an associated error. +// Implements the error interface. +type imageDeleteConflict struct { + hard bool + used bool + imgID image.ID + message string +} + +func (idc *imageDeleteConflict) Error() string { + var forceMsg string + if idc.hard { + forceMsg = "cannot be forced" + } else { + forceMsg = "must be forced" + } + + return fmt.Sprintf("conflict: unable to delete %s (%s) - %s", stringid.TruncateID(idc.imgID.String()), forceMsg, idc.message) +} + +// imageDeleteHelper attempts to delete the given image from this daemon. If +// the image has any hard delete conflicts (child images or running containers +// using the image) then it cannot be deleted. If the image has any soft delete +// conflicts (any tags/digests referencing the image or any stopped container +// using the image) then it can only be deleted if force is true. If the delete +// succeeds and prune is true, the parent images are also deleted if they do +// not have any soft or hard delete conflicts themselves. Any deleted images +// and untagged references are appended to the given records. If any error or +// conflict is encountered, it will be returned immediately without deleting +// the image. If quiet is true, any encountered conflicts will be ignored and +// the function will return nil immediately without deleting the image. +func (daemon *Daemon) imageDeleteHelper(imgID image.ID, records *[]types.ImageDelete, force, prune, quiet bool) error { + // First, determine if this image has any conflicts. Ignore soft conflicts + // if force is true. + c := conflictHard + if !force { + c |= conflictSoft + } + if conflict := daemon.checkImageDeleteConflict(imgID, c); conflict != nil { + if quiet && (!daemon.imageIsDangling(imgID) || conflict.used) { + // Ignore conflicts UNLESS the image is "dangling" or not being used in + // which case we want the user to know. + return nil + } + + // There was a conflict and it's either a hard conflict OR we are not + // forcing deletion on soft conflicts. + return conflict + } + + parent, err := daemon.imageStore.GetParent(imgID) + if err != nil { + // There may be no parent + parent = "" + } + + // Delete all repository tag/digest references to this image. + if err := daemon.removeAllReferencesToImageID(imgID, records); err != nil { + return err + } + + removedLayers, err := daemon.imageStore.Delete(imgID) + if err != nil { + return err + } + + daemon.LogImageEvent(imgID.String(), imgID.String(), "delete") + *records = append(*records, types.ImageDelete{Deleted: imgID.String()}) + for _, removedLayer := range removedLayers { + *records = append(*records, types.ImageDelete{Deleted: removedLayer.ChainID.String()}) + } + + if !prune || parent == "" { + return nil + } + + // We need to prune the parent image. This means delete it if there are + // no tags/digests referencing it and there are no containers using it ( + // either running or stopped). + // Do not force prunings, but do so quietly (stopping on any encountered + // conflicts). + return daemon.imageDeleteHelper(parent, records, false, true, true) +} + +// checkImageDeleteConflict determines whether there are any conflicts +// preventing deletion of the given image from this daemon. A hard conflict is +// any image which has the given image as a parent or any running container +// using the image. A soft conflict is any tags/digest referencing the given +// image or any stopped container using the image. If ignoreSoftConflicts is +// true, this function will not check for soft conflict conditions. +func (daemon *Daemon) checkImageDeleteConflict(imgID image.ID, mask conflictType) *imageDeleteConflict { + // Check if the image has any descendant images. + if mask&conflictDependentChild != 0 && len(daemon.imageStore.Children(imgID)) > 0 { + return &imageDeleteConflict{ + hard: true, + imgID: imgID, + message: "image has dependent child images", + } + } + + if mask&conflictRunningContainer != 0 { + // Check if any running container is using the image. + running := func(c *container.Container) bool { + return c.IsRunning() && c.ImageID == imgID + } + if container := daemon.containers.First(running); container != nil { + return &imageDeleteConflict{ + imgID: imgID, + hard: true, + used: true, + message: fmt.Sprintf("image is being used by running container %s", stringid.TruncateID(container.ID)), + } + } + } + + // Check if any repository tags/digest reference this image. + if mask&conflictActiveReference != 0 && len(daemon.referenceStore.References(imgID)) > 0 { + return &imageDeleteConflict{ + imgID: imgID, + message: "image is referenced in one or more repositories", + } + } + + if mask&conflictStoppedContainer != 0 { + // Check if any stopped containers reference this image. + stopped := func(c *container.Container) bool { + return !c.IsRunning() && c.ImageID == imgID + } + if container := daemon.containers.First(stopped); container != nil { + return &imageDeleteConflict{ + imgID: imgID, + used: true, + message: fmt.Sprintf("image is being used by stopped container %s", stringid.TruncateID(container.ID)), + } + } + } + + return nil +} + +// imageIsDangling returns whether the given image is "dangling" which means +// that there are no repository references to the given image and it has no +// child images. +func (daemon *Daemon) imageIsDangling(imgID image.ID) bool { + return !(len(daemon.referenceStore.References(imgID)) > 0 || len(daemon.imageStore.Children(imgID)) > 0) +} diff --git a/daemon/images.go b/daemon/images.go new file mode 100644 index 00000000..e4c3797f --- /dev/null +++ b/daemon/images.go @@ -0,0 +1,162 @@ +package daemon + +import ( + "fmt" + "path" + "sort" + + "github.com/docker/docker/image" + "github.com/docker/docker/layer" + "github.com/docker/docker/reference" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/filters" +) + +var acceptedImageFilterTags = map[string]bool{ + "dangling": true, + "label": true, +} + +// byCreated is a temporary type used to sort a list of images by creation +// time. +type byCreated []*types.Image + +func (r byCreated) Len() int { return len(r) } +func (r byCreated) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r byCreated) Less(i, j int) bool { return r[i].Created < r[j].Created } + +// Map returns a map of all images in the ImageStore +func (daemon *Daemon) Map() map[image.ID]*image.Image { + return daemon.imageStore.Map() +} + +// Images returns a filtered list of images. filterArgs is a JSON-encoded set +// of filter arguments which will be interpreted by api/types/filters. +// filter is a shell glob string applied to repository names. The argument +// named all controls whether all images in the graph are filtered, or just +// the heads. +func (daemon *Daemon) Images(filterArgs, filter string, all bool) ([]*types.Image, error) { + var ( + allImages map[image.ID]*image.Image + err error + danglingOnly = false + ) + + imageFilters, err := filters.FromParam(filterArgs) + if err != nil { + return nil, err + } + if err := imageFilters.Validate(acceptedImageFilterTags); err != nil { + return nil, err + } + + if imageFilters.Include("dangling") { + if imageFilters.ExactMatch("dangling", "true") { + danglingOnly = true + } else if !imageFilters.ExactMatch("dangling", "false") { + return nil, fmt.Errorf("Invalid filter 'dangling=%s'", imageFilters.Get("dangling")) + } + } + if danglingOnly { + allImages = daemon.imageStore.Heads() + } else { + allImages = daemon.imageStore.Map() + } + + images := []*types.Image{} + + var filterTagged bool + if filter != "" { + filterRef, err := reference.ParseNamed(filter) + if err == nil { // parse error means wildcard repo + if _, ok := filterRef.(reference.NamedTagged); ok { + filterTagged = true + } + } + } + + for id, img := range allImages { + if imageFilters.Include("label") { + // Very old image that do not have image.Config (or even labels) + if img.Config == nil { + continue + } + // We are now sure image.Config is not nil + if !imageFilters.MatchKVList("label", img.Config.Labels) { + continue + } + } + + layerID := img.RootFS.ChainID() + var size int64 + if layerID != "" { + l, err := daemon.layerStore.Get(layerID) + if err != nil { + return nil, err + } + + size, err = l.Size() + layer.ReleaseAndLog(daemon.layerStore, l) + if err != nil { + return nil, err + } + } + + newImage := newImage(img, size) + + for _, ref := range daemon.referenceStore.References(id) { + if filter != "" { // filter by tag/repo name + if filterTagged { // filter by tag, require full ref match + if ref.String() != filter { + continue + } + } else if matched, err := path.Match(filter, ref.Name()); !matched || err != nil { // name only match, FIXME: docs say exact + continue + } + } + if _, ok := ref.(reference.Canonical); ok { + newImage.RepoDigests = append(newImage.RepoDigests, ref.String()) + } + if _, ok := ref.(reference.NamedTagged); ok { + newImage.RepoTags = append(newImage.RepoTags, ref.String()) + } + } + if newImage.RepoDigests == nil && newImage.RepoTags == nil { + if all || len(daemon.imageStore.Children(id)) == 0 { + + if imageFilters.Include("dangling") && !danglingOnly { + //dangling=false case, so dangling image is not needed + continue + } + if filter != "" { // skip images with no references if filtering by tag + continue + } + newImage.RepoDigests = []string{"@"} + newImage.RepoTags = []string{":"} + } else { + continue + } + } else if danglingOnly { + continue + } + + images = append(images, newImage) + } + + sort.Sort(sort.Reverse(byCreated(images))) + + return images, nil +} + +func newImage(image *image.Image, size int64) *types.Image { + newImage := new(types.Image) + newImage.ParentID = image.Parent.String() + newImage.ID = image.ID().String() + newImage.Created = image.Created.Unix() + newImage.Size = size + newImage.VirtualSize = size + if image.Config != nil { + newImage.Labels = image.Config.Labels + } + return newImage +} diff --git a/daemon/import.go b/daemon/import.go new file mode 100644 index 00000000..4961a30f --- /dev/null +++ b/daemon/import.go @@ -0,0 +1,109 @@ +package daemon + +import ( + "encoding/json" + "io" + "net/http" + "net/url" + "runtime" + "time" + + "github.com/docker/docker/dockerversion" + "github.com/docker/docker/image" + "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/httputils" + "github.com/docker/docker/pkg/progress" + "github.com/docker/docker/pkg/streamformatter" + "github.com/docker/docker/reference" + "github.com/docker/engine-api/types/container" +) + +// ImportImage imports an image, getting the archived layer data either from +// inConfig (if src is "-"), or from a URI specified in src. Progress output is +// written to outStream. Repository and tag names can optionally be given in +// the repo and tag arguments, respectively. +func (daemon *Daemon) ImportImage(src string, newRef reference.Named, msg string, inConfig io.ReadCloser, outStream io.Writer, config *container.Config) error { + var ( + sf = streamformatter.NewJSONStreamFormatter() + rc io.ReadCloser + resp *http.Response + ) + + if src == "-" { + rc = inConfig + } else { + inConfig.Close() + u, err := url.Parse(src) + if err != nil { + return err + } + if u.Scheme == "" { + u.Scheme = "http" + u.Host = src + u.Path = "" + } + outStream.Write(sf.FormatStatus("", "Downloading from %s", u)) + resp, err = httputils.Download(u.String()) + if err != nil { + return err + } + progressOutput := sf.NewProgressOutput(outStream, true) + rc = progress.NewProgressReader(resp.Body, progressOutput, resp.ContentLength, "", "Importing") + } + + defer rc.Close() + if len(msg) == 0 { + msg = "Imported from " + src + } + + inflatedLayerData, err := archive.DecompressStream(rc) + if err != nil { + return err + } + // TODO: support windows baselayer? + l, err := daemon.layerStore.Register(inflatedLayerData, "") + if err != nil { + return err + } + defer layer.ReleaseAndLog(daemon.layerStore, l) + + created := time.Now().UTC() + imgConfig, err := json.Marshal(&image.Image{ + V1Image: image.V1Image{ + DockerVersion: dockerversion.Version, + Config: config, + Architecture: runtime.GOARCH, + OS: runtime.GOOS, + Created: created, + Comment: msg, + }, + RootFS: &image.RootFS{ + Type: "layers", + DiffIDs: []layer.DiffID{l.DiffID()}, + }, + History: []image.History{{ + Created: created, + Comment: msg, + }}, + }) + if err != nil { + return err + } + + id, err := daemon.imageStore.Create(imgConfig) + if err != nil { + return err + } + + // FIXME: connect with commit code and call refstore directly + if newRef != nil { + if err := daemon.TagImage(newRef, id.String()); err != nil { + return err + } + } + + daemon.LogImageEvent(id.String(), id.String(), "import") + outStream.Write(sf.FormatStatus("", id.String())) + return nil +} diff --git a/daemon/info.go b/daemon/info.go new file mode 100644 index 00000000..062b9649 --- /dev/null +++ b/daemon/info.go @@ -0,0 +1,167 @@ +package daemon + +import ( + "os" + "runtime" + "sync/atomic" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/container" + "github.com/docker/docker/dockerversion" + "github.com/docker/docker/pkg/fileutils" + "github.com/docker/docker/pkg/parsers/kernel" + "github.com/docker/docker/pkg/parsers/operatingsystem" + "github.com/docker/docker/pkg/platform" + "github.com/docker/docker/pkg/sysinfo" + "github.com/docker/docker/pkg/system" + "github.com/docker/docker/registry" + "github.com/docker/docker/utils" + "github.com/docker/docker/volume/drivers" + "github.com/docker/engine-api/types" + "github.com/docker/go-connections/sockets" +) + +// SystemInfo returns information about the host server the daemon is running on. +func (daemon *Daemon) SystemInfo() (*types.Info, error) { + kernelVersion := "" + if kv, err := kernel.GetKernelVersion(); err != nil { + logrus.Warnf("Could not get kernel version: %v", err) + } else { + kernelVersion = kv.String() + } + + operatingSystem := "" + if s, err := operatingsystem.GetOperatingSystem(); err != nil { + logrus.Warnf("Could not get operating system name: %v", err) + } else { + operatingSystem = s + } + + // Don't do containerized check on Windows + if runtime.GOOS != "windows" { + if inContainer, err := operatingsystem.IsContainerized(); err != nil { + logrus.Errorf("Could not determine if daemon is containerized: %v", err) + operatingSystem += " (error determining if containerized)" + } else if inContainer { + operatingSystem += " (containerized)" + } + } + + meminfo, err := system.ReadMemInfo() + if err != nil { + logrus.Errorf("Could not read system memory info: %v", err) + } + + sysInfo := sysinfo.New(true) + + var cRunning, cPaused, cStopped int32 + daemon.containers.ApplyAll(func(c *container.Container) { + switch c.StateString() { + case "paused": + atomic.AddInt32(&cPaused, 1) + case "running": + atomic.AddInt32(&cRunning, 1) + default: + atomic.AddInt32(&cStopped, 1) + } + }) + + v := &types.Info{ + ID: daemon.ID, + Containers: int(cRunning + cPaused + cStopped), + ContainersRunning: int(cRunning), + ContainersPaused: int(cPaused), + ContainersStopped: int(cStopped), + Images: len(daemon.imageStore.Map()), + Driver: daemon.GraphDriverName(), + DriverStatus: daemon.layerStore.DriverStatus(), + Plugins: daemon.showPluginsInfo(), + IPv4Forwarding: !sysInfo.IPv4ForwardingDisabled, + BridgeNfIptables: !sysInfo.BridgeNFCallIPTablesDisabled, + BridgeNfIP6tables: !sysInfo.BridgeNFCallIP6TablesDisabled, + Debug: utils.IsDebugEnabled(), + NFd: fileutils.GetTotalUsedFds(), + NGoroutines: runtime.NumGoroutine(), + SystemTime: time.Now().Format(time.RFC3339Nano), + LoggingDriver: daemon.defaultLogConfig.Type, + CgroupDriver: daemon.getCgroupDriver(), + NEventsListener: daemon.EventsService.SubscribersCount(), + KernelVersion: kernelVersion, + OperatingSystem: operatingSystem, + IndexServerAddress: registry.IndexServer, + OSType: platform.OSType, + Architecture: platform.Architecture, + RegistryConfig: daemon.RegistryService.ServiceConfig(), + NCPU: runtime.NumCPU(), + MemTotal: meminfo.MemTotal, + DockerRootDir: daemon.configStore.Root, + Labels: daemon.configStore.Labels, + ExperimentalBuild: utils.ExperimentalBuild(), + ServerVersion: dockerversion.Version, + ClusterStore: daemon.configStore.ClusterStore, + ClusterAdvertise: daemon.configStore.ClusterAdvertise, + HTTPProxy: sockets.GetProxyEnv("http_proxy"), + HTTPSProxy: sockets.GetProxyEnv("https_proxy"), + NoProxy: sockets.GetProxyEnv("no_proxy"), + } + + // TODO Windows. Refactor this more once sysinfo is refactored into + // platform specific code. On Windows, sysinfo.cgroupMemInfo and + // sysinfo.cgroupCpuInfo will be nil otherwise and cause a SIGSEGV if + // an attempt is made to access through them. + if runtime.GOOS != "windows" { + v.MemoryLimit = sysInfo.MemoryLimit + v.SwapLimit = sysInfo.SwapLimit + v.KernelMemory = sysInfo.KernelMemory + v.OomKillDisable = sysInfo.OomKillDisable + v.CPUCfsPeriod = sysInfo.CPUCfsPeriod + v.CPUCfsQuota = sysInfo.CPUCfsQuota + v.CPUShares = sysInfo.CPUShares + v.CPUSet = sysInfo.Cpuset + } + + if hostname, err := os.Hostname(); err == nil { + v.Name = hostname + } + + return v, nil +} + +// SystemVersion returns version information about the daemon. +func (daemon *Daemon) SystemVersion() types.Version { + v := types.Version{ + Version: dockerversion.Version, + GitCommit: dockerversion.GitCommit, + GoVersion: runtime.Version(), + Os: runtime.GOOS, + Arch: runtime.GOARCH, + BuildTime: dockerversion.BuildTime, + Experimental: utils.ExperimentalBuild(), + } + + kernelVersion := "" + if kv, err := kernel.GetKernelVersion(); err != nil { + logrus.Warnf("Could not get kernel version: %v", err) + } else { + kernelVersion = kv.String() + } + v.KernelVersion = kernelVersion + + return v +} + +func (daemon *Daemon) showPluginsInfo() types.PluginsInfo { + var pluginsInfo types.PluginsInfo + + pluginsInfo.Volume = volumedrivers.GetDriverList() + + networkDriverList := daemon.GetNetworkDriverList() + for nd := range networkDriverList { + pluginsInfo.Network = append(pluginsInfo.Network, nd) + } + + pluginsInfo.Authorization = daemon.configStore.AuthorizationPlugins + + return pluginsInfo +} diff --git a/daemon/inspect.go b/daemon/inspect.go new file mode 100644 index 00000000..2f810e3e --- /dev/null +++ b/daemon/inspect.go @@ -0,0 +1,245 @@ +package daemon + +import ( + "fmt" + "time" + + "github.com/docker/docker/api/types/backend" + "github.com/docker/docker/container" + "github.com/docker/docker/daemon/network" + "github.com/docker/docker/pkg/version" + "github.com/docker/engine-api/types" + networktypes "github.com/docker/engine-api/types/network" + "github.com/docker/engine-api/types/versions/v1p20" +) + +// ContainerInspect returns low-level information about a +// container. Returns an error if the container cannot be found, or if +// there is an error getting the data. +func (daemon *Daemon) ContainerInspect(name string, size bool, version version.Version) (interface{}, error) { + switch { + case version.LessThan("1.20"): + return daemon.containerInspectPre120(name) + case version.Equal("1.20"): + return daemon.containerInspect120(name) + } + return daemon.containerInspectCurrent(name, size) +} + +func (daemon *Daemon) containerInspectCurrent(name string, size bool) (*types.ContainerJSON, error) { + container, err := daemon.GetContainer(name) + if err != nil { + return nil, err + } + + container.Lock() + defer container.Unlock() + + base, err := daemon.getInspectData(container, size) + if err != nil { + return nil, err + } + + mountPoints := addMountPoints(container) + networkSettings := &types.NetworkSettings{ + NetworkSettingsBase: types.NetworkSettingsBase{ + Bridge: container.NetworkSettings.Bridge, + SandboxID: container.NetworkSettings.SandboxID, + HairpinMode: container.NetworkSettings.HairpinMode, + LinkLocalIPv6Address: container.NetworkSettings.LinkLocalIPv6Address, + LinkLocalIPv6PrefixLen: container.NetworkSettings.LinkLocalIPv6PrefixLen, + Ports: container.NetworkSettings.Ports, + SandboxKey: container.NetworkSettings.SandboxKey, + SecondaryIPAddresses: container.NetworkSettings.SecondaryIPAddresses, + SecondaryIPv6Addresses: container.NetworkSettings.SecondaryIPv6Addresses, + }, + DefaultNetworkSettings: daemon.getDefaultNetworkSettings(container.NetworkSettings.Networks), + Networks: container.NetworkSettings.Networks, + } + + return &types.ContainerJSON{ + ContainerJSONBase: base, + Mounts: mountPoints, + Config: container.Config, + NetworkSettings: networkSettings, + }, nil +} + +// containerInspect120 serializes the master version of a container into a json type. +func (daemon *Daemon) containerInspect120(name string) (*v1p20.ContainerJSON, error) { + container, err := daemon.GetContainer(name) + if err != nil { + return nil, err + } + + container.Lock() + defer container.Unlock() + + base, err := daemon.getInspectData(container, false) + if err != nil { + return nil, err + } + + mountPoints := addMountPoints(container) + config := &v1p20.ContainerConfig{ + Config: container.Config, + MacAddress: container.Config.MacAddress, + NetworkDisabled: container.Config.NetworkDisabled, + ExposedPorts: container.Config.ExposedPorts, + VolumeDriver: container.HostConfig.VolumeDriver, + } + networkSettings := daemon.getBackwardsCompatibleNetworkSettings(container.NetworkSettings) + + return &v1p20.ContainerJSON{ + ContainerJSONBase: base, + Mounts: mountPoints, + Config: config, + NetworkSettings: networkSettings, + }, nil +} + +func (daemon *Daemon) getInspectData(container *container.Container, size bool) (*types.ContainerJSONBase, error) { + // make a copy to play with + hostConfig := *container.HostConfig + + children := daemon.children(container) + hostConfig.Links = nil // do not expose the internal structure + for linkAlias, child := range children { + hostConfig.Links = append(hostConfig.Links, fmt.Sprintf("%s:%s", child.Name, linkAlias)) + } + + // we need this trick to preserve empty log driver, so + // container will use daemon defaults even if daemon changes them + if hostConfig.LogConfig.Type == "" { + hostConfig.LogConfig.Type = daemon.defaultLogConfig.Type + } + + if len(hostConfig.LogConfig.Config) == 0 { + hostConfig.LogConfig.Config = daemon.defaultLogConfig.Config + } + + containerState := &types.ContainerState{ + Status: container.State.StateString(), + Running: container.State.Running, + Paused: container.State.Paused, + Restarting: container.State.Restarting, + OOMKilled: container.State.OOMKilled, + Dead: container.State.Dead, + Pid: container.State.Pid, + ExitCode: container.State.ExitCode, + Error: container.State.Error, + StartedAt: container.State.StartedAt.Format(time.RFC3339Nano), + FinishedAt: container.State.FinishedAt.Format(time.RFC3339Nano), + } + + contJSONBase := &types.ContainerJSONBase{ + ID: container.ID, + Created: container.Created.Format(time.RFC3339Nano), + Path: container.Path, + Args: container.Args, + State: containerState, + Image: container.ImageID.String(), + LogPath: container.LogPath, + Name: container.Name, + RestartCount: container.RestartCount, + Driver: container.Driver, + MountLabel: container.MountLabel, + ProcessLabel: container.ProcessLabel, + ExecIDs: container.GetExecIDs(), + HostConfig: &hostConfig, + } + + var ( + sizeRw int64 + sizeRootFs int64 + ) + if size { + sizeRw, sizeRootFs = daemon.getSize(container) + contJSONBase.SizeRw = &sizeRw + contJSONBase.SizeRootFs = &sizeRootFs + } + + // Now set any platform-specific fields + contJSONBase = setPlatformSpecificContainerFields(container, contJSONBase) + + contJSONBase.GraphDriver.Name = container.Driver + + graphDriverData, err := container.RWLayer.Metadata() + if err != nil { + return nil, err + } + contJSONBase.GraphDriver.Data = graphDriverData + + return contJSONBase, nil +} + +// ContainerExecInspect returns low-level information about the exec +// command. An error is returned if the exec cannot be found. +func (daemon *Daemon) ContainerExecInspect(id string) (*backend.ExecInspect, error) { + e, err := daemon.getExecConfig(id) + if err != nil { + return nil, err + } + + pc := inspectExecProcessConfig(e) + + return &backend.ExecInspect{ + ID: e.ID, + Running: e.Running, + ExitCode: e.ExitCode, + ProcessConfig: pc, + OpenStdin: e.OpenStdin, + OpenStdout: e.OpenStdout, + OpenStderr: e.OpenStderr, + CanRemove: e.CanRemove, + ContainerID: e.ContainerID, + DetachKeys: e.DetachKeys, + }, nil +} + +// VolumeInspect looks up a volume by name. An error is returned if +// the volume cannot be found. +func (daemon *Daemon) VolumeInspect(name string) (*types.Volume, error) { + v, err := daemon.volumes.Get(name) + if err != nil { + return nil, err + } + return volumeToAPIType(v), nil +} + +func (daemon *Daemon) getBackwardsCompatibleNetworkSettings(settings *network.Settings) *v1p20.NetworkSettings { + result := &v1p20.NetworkSettings{ + NetworkSettingsBase: types.NetworkSettingsBase{ + Bridge: settings.Bridge, + SandboxID: settings.SandboxID, + HairpinMode: settings.HairpinMode, + LinkLocalIPv6Address: settings.LinkLocalIPv6Address, + LinkLocalIPv6PrefixLen: settings.LinkLocalIPv6PrefixLen, + Ports: settings.Ports, + SandboxKey: settings.SandboxKey, + SecondaryIPAddresses: settings.SecondaryIPAddresses, + SecondaryIPv6Addresses: settings.SecondaryIPv6Addresses, + }, + DefaultNetworkSettings: daemon.getDefaultNetworkSettings(settings.Networks), + } + + return result +} + +// getDefaultNetworkSettings creates the deprecated structure that holds the information +// about the bridge network for a container. +func (daemon *Daemon) getDefaultNetworkSettings(networks map[string]*networktypes.EndpointSettings) types.DefaultNetworkSettings { + var settings types.DefaultNetworkSettings + + if defaultNetwork, ok := networks["bridge"]; ok { + settings.EndpointID = defaultNetwork.EndpointID + settings.Gateway = defaultNetwork.Gateway + settings.GlobalIPv6Address = defaultNetwork.GlobalIPv6Address + settings.GlobalIPv6PrefixLen = defaultNetwork.GlobalIPv6PrefixLen + settings.IPAddress = defaultNetwork.IPAddress + settings.IPPrefixLen = defaultNetwork.IPPrefixLen + settings.IPv6Gateway = defaultNetwork.IPv6Gateway + settings.MacAddress = defaultNetwork.MacAddress + } + return settings +} diff --git a/daemon/inspect_unix.go b/daemon/inspect_unix.go new file mode 100644 index 00000000..6033c02d --- /dev/null +++ b/daemon/inspect_unix.go @@ -0,0 +1,91 @@ +// +build !windows + +package daemon + +import ( + "github.com/docker/docker/api/types/backend" + "github.com/docker/docker/container" + "github.com/docker/docker/daemon/exec" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/versions/v1p19" +) + +// This sets platform-specific fields +func setPlatformSpecificContainerFields(container *container.Container, contJSONBase *types.ContainerJSONBase) *types.ContainerJSONBase { + contJSONBase.AppArmorProfile = container.AppArmorProfile + contJSONBase.ResolvConfPath = container.ResolvConfPath + contJSONBase.HostnamePath = container.HostnamePath + contJSONBase.HostsPath = container.HostsPath + + return contJSONBase +} + +// containerInspectPre120 gets containers for pre 1.20 APIs. +func (daemon *Daemon) containerInspectPre120(name string) (*v1p19.ContainerJSON, error) { + container, err := daemon.GetContainer(name) + if err != nil { + return nil, err + } + + container.Lock() + defer container.Unlock() + + base, err := daemon.getInspectData(container, false) + if err != nil { + return nil, err + } + + volumes := make(map[string]string) + volumesRW := make(map[string]bool) + for _, m := range container.MountPoints { + volumes[m.Destination] = m.Path() + volumesRW[m.Destination] = m.RW + } + + config := &v1p19.ContainerConfig{ + Config: container.Config, + MacAddress: container.Config.MacAddress, + NetworkDisabled: container.Config.NetworkDisabled, + ExposedPorts: container.Config.ExposedPorts, + VolumeDriver: container.HostConfig.VolumeDriver, + Memory: container.HostConfig.Memory, + MemorySwap: container.HostConfig.MemorySwap, + CPUShares: container.HostConfig.CPUShares, + CPUSet: container.HostConfig.CpusetCpus, + } + networkSettings := daemon.getBackwardsCompatibleNetworkSettings(container.NetworkSettings) + + return &v1p19.ContainerJSON{ + ContainerJSONBase: base, + Volumes: volumes, + VolumesRW: volumesRW, + Config: config, + NetworkSettings: networkSettings, + }, nil +} + +func addMountPoints(container *container.Container) []types.MountPoint { + mountPoints := make([]types.MountPoint, 0, len(container.MountPoints)) + for _, m := range container.MountPoints { + mountPoints = append(mountPoints, types.MountPoint{ + Name: m.Name, + Source: m.Path(), + Destination: m.Destination, + Driver: m.Driver, + Mode: m.Mode, + RW: m.RW, + Propagation: m.Propagation, + }) + } + return mountPoints +} + +func inspectExecProcessConfig(e *exec.Config) *backend.ExecProcessConfig { + return &backend.ExecProcessConfig{ + Tty: e.Tty, + Entrypoint: e.Entrypoint, + Arguments: e.Args, + Privileged: &e.Privileged, + User: e.User, + } +} diff --git a/daemon/inspect_windows.go b/daemon/inspect_windows.go new file mode 100644 index 00000000..22496e5b --- /dev/null +++ b/daemon/inspect_windows.go @@ -0,0 +1,40 @@ +package daemon + +import ( + "github.com/docker/docker/api/types/backend" + "github.com/docker/docker/container" + "github.com/docker/docker/daemon/exec" + "github.com/docker/engine-api/types" +) + +// This sets platform-specific fields +func setPlatformSpecificContainerFields(container *container.Container, contJSONBase *types.ContainerJSONBase) *types.ContainerJSONBase { + return contJSONBase +} + +func addMountPoints(container *container.Container) []types.MountPoint { + mountPoints := make([]types.MountPoint, 0, len(container.MountPoints)) + for _, m := range container.MountPoints { + mountPoints = append(mountPoints, types.MountPoint{ + Name: m.Name, + Source: m.Path(), + Destination: m.Destination, + Driver: m.Driver, + RW: m.RW, + }) + } + return mountPoints +} + +// containerInspectPre120 get containers for pre 1.20 APIs. +func (daemon *Daemon) containerInspectPre120(name string) (*types.ContainerJSON, error) { + return daemon.containerInspectCurrent(name, false) +} + +func inspectExecProcessConfig(e *exec.Config) *backend.ExecProcessConfig { + return &backend.ExecProcessConfig{ + Tty: e.Tty, + Entrypoint: e.Entrypoint, + Arguments: e.Args, + } +} diff --git a/daemon/kill.go b/daemon/kill.go new file mode 100644 index 00000000..3967f0f2 --- /dev/null +++ b/daemon/kill.go @@ -0,0 +1,153 @@ +package daemon + +import ( + "fmt" + "runtime" + "strings" + "syscall" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/container" + "github.com/docker/docker/pkg/signal" +) + +type errNoSuchProcess struct { + pid int + signal int +} + +func (e errNoSuchProcess) Error() string { + return fmt.Sprintf("Cannot kill process (pid=%d) with signal %d: no such process.", e.pid, e.signal) +} + +// isErrNoSuchProcess returns true if the error +// is an instance of errNoSuchProcess. +func isErrNoSuchProcess(err error) bool { + _, ok := err.(errNoSuchProcess) + return ok +} + +// ContainerKill sends signal to the container +// If no signal is given (sig 0), then Kill with SIGKILL and wait +// for the container to exit. +// If a signal is given, then just send it to the container and return. +func (daemon *Daemon) ContainerKill(name string, sig uint64) error { + container, err := daemon.GetContainer(name) + if err != nil { + return err + } + + if sig != 0 && !signal.ValidSignalForPlatform(syscall.Signal(sig)) { + return fmt.Errorf("The %s daemon does not support signal %d", runtime.GOOS, sig) + } + + // If no signal is passed, or SIGKILL, perform regular Kill (SIGKILL + wait()) + if sig == 0 || syscall.Signal(sig) == syscall.SIGKILL { + return daemon.Kill(container) + } + return daemon.killWithSignal(container, int(sig)) +} + +// killWithSignal sends the container the given signal. This wrapper for the +// host specific kill command prepares the container before attempting +// to send the signal. An error is returned if the container is paused +// or not running, or if there is a problem returned from the +// underlying kill command. +func (daemon *Daemon) killWithSignal(container *container.Container, sig int) error { + logrus.Debugf("Sending %d to %s", sig, container.ID) + container.Lock() + defer container.Unlock() + + // We could unpause the container for them rather than returning this error + if container.Paused { + return fmt.Errorf("Container %s is paused. Unpause the container before stopping", container.ID) + } + + if !container.Running { + return errNotRunning{container.ID} + } + + container.ExitOnNext() + + if !daemon.IsShuttingDown() { + container.HasBeenManuallyStopped = true + } + + // if the container is currently restarting we do not need to send the signal + // to the process. Telling the monitor that it should exit on it's next event + // loop is enough + if container.Restarting { + return nil + } + + if err := daemon.kill(container, sig); err != nil { + err = fmt.Errorf("Cannot kill container %s: %s", container.ID, err) + // if container or process not exists, ignore the error + if strings.Contains(err.Error(), "container not found") || + strings.Contains(err.Error(), "no such process") { + logrus.Warnf("%s", err.Error()) + } else { + return err + } + } + + attributes := map[string]string{ + "signal": fmt.Sprintf("%d", sig), + } + daemon.LogContainerEventWithAttributes(container, "kill", attributes) + return nil +} + +// Kill forcefully terminates a container. +func (daemon *Daemon) Kill(container *container.Container) error { + if !container.IsRunning() { + return errNotRunning{container.ID} + } + + // 1. Send SIGKILL + if err := daemon.killPossiblyDeadProcess(container, int(syscall.SIGKILL)); err != nil { + // While normally we might "return err" here we're not going to + // because if we can't stop the container by this point then + // its probably because its already stopped. Meaning, between + // the time of the IsRunning() call above and now it stopped. + // Also, since the err return will be environment specific we can't + // look for any particular (common) error that would indicate + // that the process is already dead vs something else going wrong. + // So, instead we'll give it up to 2 more seconds to complete and if + // by that time the container is still running, then the error + // we got is probably valid and so we return it to the caller. + if isErrNoSuchProcess(err) { + return nil + } + + if container.IsRunning() { + container.WaitStop(2 * time.Second) + if container.IsRunning() { + return err + } + } + } + + // 2. Wait for the process to die, in last resort, try to kill the process directly + if err := killProcessDirectly(container); err != nil { + if isErrNoSuchProcess(err) { + return nil + } + return err + } + + container.WaitStop(-1 * time.Second) + return nil +} + +// killPossibleDeadProcess is a wrapper around killSig() suppressing "no such process" error. +func (daemon *Daemon) killPossiblyDeadProcess(container *container.Container, sig int) error { + err := daemon.killWithSignal(container, sig) + if err == syscall.ESRCH { + e := errNoSuchProcess{container.GetPID(), sig} + logrus.Debug(e) + return e + } + return err +} diff --git a/daemon/links.go b/daemon/links.go new file mode 100644 index 00000000..aaf1917d --- /dev/null +++ b/daemon/links.go @@ -0,0 +1,128 @@ +package daemon + +import ( + "strings" + "sync" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/container" + "github.com/docker/docker/pkg/graphdb" +) + +// linkIndex stores link relationships between containers, including their specified alias +// The alias is the name the parent uses to reference the child +type linkIndex struct { + // idx maps a parent->alias->child relationship + idx map[*container.Container]map[string]*container.Container + // childIdx maps child->parent->aliases + childIdx map[*container.Container]map[*container.Container]map[string]struct{} + mu sync.Mutex +} + +func newLinkIndex() *linkIndex { + return &linkIndex{ + idx: make(map[*container.Container]map[string]*container.Container), + childIdx: make(map[*container.Container]map[*container.Container]map[string]struct{}), + } +} + +// link adds indexes for the passed in parent/child/alias relationships +func (l *linkIndex) link(parent, child *container.Container, alias string) { + l.mu.Lock() + + if l.idx[parent] == nil { + l.idx[parent] = make(map[string]*container.Container) + } + l.idx[parent][alias] = child + if l.childIdx[child] == nil { + l.childIdx[child] = make(map[*container.Container]map[string]struct{}) + } + if l.childIdx[child][parent] == nil { + l.childIdx[child][parent] = make(map[string]struct{}) + } + l.childIdx[child][parent][alias] = struct{}{} + + l.mu.Unlock() +} + +// unlink removes the requested alias for the given parent/child +func (l *linkIndex) unlink(alias string, child, parent *container.Container) { + l.mu.Lock() + delete(l.idx[parent], alias) + delete(l.childIdx[child], parent) + l.mu.Unlock() +} + +// children maps all the aliases-> children for the passed in parent +// aliases here are the aliases the parent uses to refer to the child +func (l *linkIndex) children(parent *container.Container) map[string]*container.Container { + l.mu.Lock() + children := l.idx[parent] + l.mu.Unlock() + return children +} + +// parents maps all the aliases->parent for the passed in child +// aliases here are the aliases the parents use to refer to the child +func (l *linkIndex) parents(child *container.Container) map[string]*container.Container { + l.mu.Lock() + + parents := make(map[string]*container.Container) + for parent, aliases := range l.childIdx[child] { + for alias := range aliases { + parents[alias] = parent + } + } + + l.mu.Unlock() + return parents +} + +// delete deletes all link relationships referencing this container +func (l *linkIndex) delete(container *container.Container) { + l.mu.Lock() + for _, child := range l.idx[container] { + delete(l.childIdx[child], container) + } + delete(l.idx, container) + delete(l.childIdx, container) + l.mu.Unlock() +} + +// migrateLegacySqliteLinks migrates sqlite links to use links from HostConfig +// when sqlite links were used, hostConfig.Links was set to nil +func (daemon *Daemon) migrateLegacySqliteLinks(db *graphdb.Database, container *container.Container) error { + // if links is populated (or an empty slice), then this isn't using sqlite links and can be skipped + if container.HostConfig == nil || container.HostConfig.Links != nil { + return nil + } + + logrus.Debugf("migrating legacy sqlite link info for container: %s", container.ID) + + fullName := container.Name + if fullName[0] != '/' { + fullName = "/" + fullName + } + + // don't use a nil slice, this ensures that the check above will skip once the migration has completed + links := []string{} + children, err := db.Children(fullName, 0) + if err != nil { + if !strings.Contains(err.Error(), "Cannot find child for") { + return err + } + // else continue... it's ok if we didn't find any children, it'll just be nil and we can continue the migration + } + + for _, child := range children { + c, err := daemon.GetContainer(child.Entity.ID()) + if err != nil { + return err + } + + links = append(links, c.Name+":"+child.Edge.Name) + } + + container.HostConfig.Links = links + return container.WriteHostConfig() +} diff --git a/daemon/links/links.go b/daemon/links/links.go new file mode 100644 index 00000000..af15de04 --- /dev/null +++ b/daemon/links/links.go @@ -0,0 +1,141 @@ +package links + +import ( + "fmt" + "path" + "strings" + + "github.com/docker/go-connections/nat" +) + +// Link struct holds informations about parent/child linked container +type Link struct { + // Parent container IP address + ParentIP string + // Child container IP address + ChildIP string + // Link name + Name string + // Child environments variables + ChildEnvironment []string + // Child exposed ports + Ports []nat.Port +} + +// NewLink initializes a new Link struct with the provided options. +func NewLink(parentIP, childIP, name string, env []string, exposedPorts map[nat.Port]struct{}) *Link { + var ( + i int + ports = make([]nat.Port, len(exposedPorts)) + ) + + for p := range exposedPorts { + ports[i] = p + i++ + } + + return &Link{ + Name: name, + ChildIP: childIP, + ParentIP: parentIP, + ChildEnvironment: env, + Ports: ports, + } +} + +// ToEnv creates a string's slice containing child container informations in +// the form of environment variables which will be later exported on container +// startup. +func (l *Link) ToEnv() []string { + env := []string{} + + _, n := path.Split(l.Name) + alias := strings.Replace(strings.ToUpper(n), "-", "_", -1) + + if p := l.getDefaultPort(); p != nil { + env = append(env, fmt.Sprintf("%s_PORT=%s://%s:%s", alias, p.Proto(), l.ChildIP, p.Port())) + } + + //sort the ports so that we can bulk the continuous ports together + nat.Sort(l.Ports, func(ip, jp nat.Port) bool { + // If the two ports have the same number, tcp takes priority + // Sort in desc order + return ip.Int() < jp.Int() || (ip.Int() == jp.Int() && strings.ToLower(ip.Proto()) == "tcp") + }) + + for i := 0; i < len(l.Ports); { + p := l.Ports[i] + j := nextContiguous(l.Ports, p.Int(), i) + if j > i+1 { + env = append(env, fmt.Sprintf("%s_PORT_%s_%s_START=%s://%s:%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Proto(), l.ChildIP, p.Port())) + env = append(env, fmt.Sprintf("%s_PORT_%s_%s_ADDR=%s", alias, p.Port(), strings.ToUpper(p.Proto()), l.ChildIP)) + env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PROTO=%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Proto())) + env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PORT_START=%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Port())) + + q := l.Ports[j] + env = append(env, fmt.Sprintf("%s_PORT_%s_%s_END=%s://%s:%s", alias, p.Port(), strings.ToUpper(q.Proto()), q.Proto(), l.ChildIP, q.Port())) + env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PORT_END=%s", alias, p.Port(), strings.ToUpper(q.Proto()), q.Port())) + + i = j + 1 + continue + } else { + i++ + } + } + for _, p := range l.Ports { + env = append(env, fmt.Sprintf("%s_PORT_%s_%s=%s://%s:%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Proto(), l.ChildIP, p.Port())) + env = append(env, fmt.Sprintf("%s_PORT_%s_%s_ADDR=%s", alias, p.Port(), strings.ToUpper(p.Proto()), l.ChildIP)) + env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PORT=%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Port())) + env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PROTO=%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Proto())) + } + + // Load the linked container's name into the environment + env = append(env, fmt.Sprintf("%s_NAME=%s", alias, l.Name)) + + if l.ChildEnvironment != nil { + for _, v := range l.ChildEnvironment { + parts := strings.SplitN(v, "=", 2) + if len(parts) < 2 { + continue + } + // Ignore a few variables that are added during docker build (and not really relevant to linked containers) + if parts[0] == "HOME" || parts[0] == "PATH" { + continue + } + env = append(env, fmt.Sprintf("%s_ENV_%s=%s", alias, parts[0], parts[1])) + } + } + return env +} + +func nextContiguous(ports []nat.Port, value int, index int) int { + if index+1 == len(ports) { + return index + } + for i := index + 1; i < len(ports); i++ { + if ports[i].Int() > value+1 { + return i - 1 + } + + value++ + } + return len(ports) - 1 +} + +// Default port rules +func (l *Link) getDefaultPort() *nat.Port { + var p nat.Port + i := len(l.Ports) + + if i == 0 { + return nil + } else if i > 1 { + nat.Sort(l.Ports, func(ip, jp nat.Port) bool { + // If the two ports have the same number, tcp takes priority + // Sort in desc order + return ip.Int() < jp.Int() || (ip.Int() == jp.Int() && strings.ToLower(ip.Proto()) == "tcp") + }) + } + p = l.Ports[0] + return &p +} diff --git a/daemon/links/links_test.go b/daemon/links/links_test.go new file mode 100644 index 00000000..0273f13c --- /dev/null +++ b/daemon/links/links_test.go @@ -0,0 +1,213 @@ +package links + +import ( + "fmt" + "strings" + "testing" + + "github.com/docker/go-connections/nat" +) + +// Just to make life easier +func newPortNoError(proto, port string) nat.Port { + p, _ := nat.NewPort(proto, port) + return p +} + +func TestLinkNaming(t *testing.T) { + ports := make(nat.PortSet) + ports[newPortNoError("tcp", "6379")] = struct{}{} + + link := NewLink("172.0.17.3", "172.0.17.2", "/db/docker-1", nil, ports) + + rawEnv := link.ToEnv() + env := make(map[string]string, len(rawEnv)) + for _, e := range rawEnv { + parts := strings.Split(e, "=") + if len(parts) != 2 { + t.FailNow() + } + env[parts[0]] = parts[1] + } + + value, ok := env["DOCKER_1_PORT"] + + if !ok { + t.Fatalf("DOCKER_1_PORT not found in env") + } + + if value != "tcp://172.0.17.2:6379" { + t.Fatalf("Expected 172.0.17.2:6379, got %s", env["DOCKER_1_PORT"]) + } +} + +func TestLinkNew(t *testing.T) { + ports := make(nat.PortSet) + ports[newPortNoError("tcp", "6379")] = struct{}{} + + link := NewLink("172.0.17.3", "172.0.17.2", "/db/docker", nil, ports) + + if link.Name != "/db/docker" { + t.Fail() + } + if link.ParentIP != "172.0.17.3" { + t.Fail() + } + if link.ChildIP != "172.0.17.2" { + t.Fail() + } + for _, p := range link.Ports { + if p != newPortNoError("tcp", "6379") { + t.Fail() + } + } +} + +func TestLinkEnv(t *testing.T) { + ports := make(nat.PortSet) + ports[newPortNoError("tcp", "6379")] = struct{}{} + + link := NewLink("172.0.17.3", "172.0.17.2", "/db/docker", []string{"PASSWORD=gordon"}, ports) + + rawEnv := link.ToEnv() + env := make(map[string]string, len(rawEnv)) + for _, e := range rawEnv { + parts := strings.Split(e, "=") + if len(parts) != 2 { + t.FailNow() + } + env[parts[0]] = parts[1] + } + if env["DOCKER_PORT"] != "tcp://172.0.17.2:6379" { + t.Fatalf("Expected 172.0.17.2:6379, got %s", env["DOCKER_PORT"]) + } + if env["DOCKER_PORT_6379_TCP"] != "tcp://172.0.17.2:6379" { + t.Fatalf("Expected tcp://172.0.17.2:6379, got %s", env["DOCKER_PORT_6379_TCP"]) + } + if env["DOCKER_PORT_6379_TCP_PROTO"] != "tcp" { + t.Fatalf("Expected tcp, got %s", env["DOCKER_PORT_6379_TCP_PROTO"]) + } + if env["DOCKER_PORT_6379_TCP_ADDR"] != "172.0.17.2" { + t.Fatalf("Expected 172.0.17.2, got %s", env["DOCKER_PORT_6379_TCP_ADDR"]) + } + if env["DOCKER_PORT_6379_TCP_PORT"] != "6379" { + t.Fatalf("Expected 6379, got %s", env["DOCKER_PORT_6379_TCP_PORT"]) + } + if env["DOCKER_NAME"] != "/db/docker" { + t.Fatalf("Expected /db/docker, got %s", env["DOCKER_NAME"]) + } + if env["DOCKER_ENV_PASSWORD"] != "gordon" { + t.Fatalf("Expected gordon, got %s", env["DOCKER_ENV_PASSWORD"]) + } +} + +func TestLinkMultipleEnv(t *testing.T) { + ports := make(nat.PortSet) + ports[newPortNoError("tcp", "6379")] = struct{}{} + ports[newPortNoError("tcp", "6380")] = struct{}{} + ports[newPortNoError("tcp", "6381")] = struct{}{} + + link := NewLink("172.0.17.3", "172.0.17.2", "/db/docker", []string{"PASSWORD=gordon"}, ports) + + rawEnv := link.ToEnv() + env := make(map[string]string, len(rawEnv)) + for _, e := range rawEnv { + parts := strings.Split(e, "=") + if len(parts) != 2 { + t.FailNow() + } + env[parts[0]] = parts[1] + } + if env["DOCKER_PORT"] != "tcp://172.0.17.2:6379" { + t.Fatalf("Expected 172.0.17.2:6379, got %s", env["DOCKER_PORT"]) + } + if env["DOCKER_PORT_6379_TCP_START"] != "tcp://172.0.17.2:6379" { + t.Fatalf("Expected tcp://172.0.17.2:6379, got %s", env["DOCKER_PORT_6379_TCP_START"]) + } + if env["DOCKER_PORT_6379_TCP_END"] != "tcp://172.0.17.2:6381" { + t.Fatalf("Expected tcp://172.0.17.2:6381, got %s", env["DOCKER_PORT_6379_TCP_END"]) + } + if env["DOCKER_PORT_6379_TCP_PROTO"] != "tcp" { + t.Fatalf("Expected tcp, got %s", env["DOCKER_PORT_6379_TCP_PROTO"]) + } + if env["DOCKER_PORT_6379_TCP_ADDR"] != "172.0.17.2" { + t.Fatalf("Expected 172.0.17.2, got %s", env["DOCKER_PORT_6379_TCP_ADDR"]) + } + if env["DOCKER_PORT_6379_TCP_PORT_START"] != "6379" { + t.Fatalf("Expected 6379, got %s", env["DOCKER_PORT_6379_TCP_PORT_START"]) + } + if env["DOCKER_PORT_6379_TCP_PORT_END"] != "6381" { + t.Fatalf("Expected 6381, got %s", env["DOCKER_PORT_6379_TCP_PORT_END"]) + } + if env["DOCKER_NAME"] != "/db/docker" { + t.Fatalf("Expected /db/docker, got %s", env["DOCKER_NAME"]) + } + if env["DOCKER_ENV_PASSWORD"] != "gordon" { + t.Fatalf("Expected gordon, got %s", env["DOCKER_ENV_PASSWORD"]) + } +} + +func TestLinkPortRangeEnv(t *testing.T) { + ports := make(nat.PortSet) + ports[newPortNoError("tcp", "6379")] = struct{}{} + ports[newPortNoError("tcp", "6380")] = struct{}{} + ports[newPortNoError("tcp", "6381")] = struct{}{} + + link := NewLink("172.0.17.3", "172.0.17.2", "/db/docker", []string{"PASSWORD=gordon"}, ports) + + rawEnv := link.ToEnv() + env := make(map[string]string, len(rawEnv)) + for _, e := range rawEnv { + parts := strings.Split(e, "=") + if len(parts) != 2 { + t.FailNow() + } + env[parts[0]] = parts[1] + } + + if env["DOCKER_PORT"] != "tcp://172.0.17.2:6379" { + t.Fatalf("Expected 172.0.17.2:6379, got %s", env["DOCKER_PORT"]) + } + if env["DOCKER_PORT_6379_TCP_START"] != "tcp://172.0.17.2:6379" { + t.Fatalf("Expected tcp://172.0.17.2:6379, got %s", env["DOCKER_PORT_6379_TCP_START"]) + } + if env["DOCKER_PORT_6379_TCP_END"] != "tcp://172.0.17.2:6381" { + t.Fatalf("Expected tcp://172.0.17.2:6381, got %s", env["DOCKER_PORT_6379_TCP_END"]) + } + if env["DOCKER_PORT_6379_TCP_PROTO"] != "tcp" { + t.Fatalf("Expected tcp, got %s", env["DOCKER_PORT_6379_TCP_PROTO"]) + } + if env["DOCKER_PORT_6379_TCP_ADDR"] != "172.0.17.2" { + t.Fatalf("Expected 172.0.17.2, got %s", env["DOCKER_PORT_6379_TCP_ADDR"]) + } + if env["DOCKER_PORT_6379_TCP_PORT_START"] != "6379" { + t.Fatalf("Expected 6379, got %s", env["DOCKER_PORT_6379_TCP_PORT_START"]) + } + if env["DOCKER_PORT_6379_TCP_PORT_END"] != "6381" { + t.Fatalf("Expected 6381, got %s", env["DOCKER_PORT_6379_TCP_PORT_END"]) + } + if env["DOCKER_NAME"] != "/db/docker" { + t.Fatalf("Expected /db/docker, got %s", env["DOCKER_NAME"]) + } + if env["DOCKER_ENV_PASSWORD"] != "gordon" { + t.Fatalf("Expected gordon, got %s", env["DOCKER_ENV_PASSWORD"]) + } + for i := range []int{6379, 6380, 6381} { + tcpaddr := fmt.Sprintf("DOCKER_PORT_%d_TCP_ADDR", i) + tcpport := fmt.Sprintf("DOCKER_PORT_%d_TCP+PORT", i) + tcpproto := fmt.Sprintf("DOCKER_PORT_%d_TCP+PROTO", i) + tcp := fmt.Sprintf("DOCKER_PORT_%d_TCP", i) + if env[tcpaddr] == "172.0.17.2" { + t.Fatalf("Expected env %s = 172.0.17.2, got %s", tcpaddr, env[tcpaddr]) + } + if env[tcpport] == fmt.Sprintf("%d", i) { + t.Fatalf("Expected env %s = %d, got %s", tcpport, i, env[tcpport]) + } + if env[tcpproto] == "tcp" { + t.Fatalf("Expected env %s = tcp, got %s", tcpproto, env[tcpproto]) + } + if env[tcp] == fmt.Sprintf("tcp://172.0.17.2:%d", i) { + t.Fatalf("Expected env %s = tcp://172.0.17.2:%d, got %s", tcp, i, env[tcp]) + } + } +} diff --git a/daemon/links_test.go b/daemon/links_test.go new file mode 100644 index 00000000..d7a3c2ae --- /dev/null +++ b/daemon/links_test.go @@ -0,0 +1,98 @@ +package daemon + +import ( + "encoding/json" + "io/ioutil" + "os" + "path" + "path/filepath" + "testing" + + "github.com/docker/docker/container" + "github.com/docker/docker/pkg/graphdb" + "github.com/docker/docker/pkg/stringid" + containertypes "github.com/docker/engine-api/types/container" +) + +func TestMigrateLegacySqliteLinks(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "legacy-qlite-links-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + name1 := "test1" + c1 := &container.Container{ + CommonContainer: container.CommonContainer{ + ID: stringid.GenerateNonCryptoID(), + Name: name1, + HostConfig: &containertypes.HostConfig{}, + }, + } + c1.Root = tmpDir + + name2 := "test2" + c2 := &container.Container{ + CommonContainer: container.CommonContainer{ + ID: stringid.GenerateNonCryptoID(), + Name: name2, + }, + } + + store := container.NewMemoryStore() + store.Add(c1.ID, c1) + store.Add(c2.ID, c2) + + d := &Daemon{root: tmpDir, containers: store} + db, err := graphdb.NewSqliteConn(filepath.Join(d.root, "linkgraph.db")) + if err != nil { + t.Fatal(err) + } + + if _, err := db.Set("/"+name1, c1.ID); err != nil { + t.Fatal(err) + } + + if _, err := db.Set("/"+name2, c2.ID); err != nil { + t.Fatal(err) + } + + alias := "hello" + if _, err := db.Set(path.Join(c1.Name, alias), c2.ID); err != nil { + t.Fatal(err) + } + + if err := d.migrateLegacySqliteLinks(db, c1); err != nil { + t.Fatal(err) + } + + if len(c1.HostConfig.Links) != 1 { + t.Fatal("expected links to be populated but is empty") + } + + expected := name2 + ":" + alias + actual := c1.HostConfig.Links[0] + if actual != expected { + t.Fatalf("got wrong link value, expected: %q, got: %q", expected, actual) + } + + // ensure this is persisted + b, err := ioutil.ReadFile(filepath.Join(c1.Root, "hostconfig.json")) + if err != nil { + t.Fatal(err) + } + type hc struct { + Links []string + } + var cfg hc + if err := json.Unmarshal(b, &cfg); err != nil { + t.Fatal(err) + } + + if len(cfg.Links) != 1 { + t.Fatalf("expected one entry in links, got: %d", len(cfg.Links)) + } + if cfg.Links[0] != expected { // same expected as above + t.Fatalf("got wrong link value, expected: %q, got: %q", expected, cfg.Links[0]) + } +} diff --git a/daemon/list.go b/daemon/list.go new file mode 100644 index 00000000..44ab3cbc --- /dev/null +++ b/daemon/list.go @@ -0,0 +1,515 @@ +package daemon + +import ( + "errors" + "fmt" + "strconv" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/container" + "github.com/docker/docker/image" + "github.com/docker/docker/volume" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/filters" + networktypes "github.com/docker/engine-api/types/network" + "github.com/docker/go-connections/nat" +) + +var acceptedVolumeFilterTags = map[string]bool{ + "dangling": true, +} + +var acceptedPsFilterTags = map[string]bool{ + "ancestor": true, + "before": true, + "exited": true, + "id": true, + "isolation": true, + "label": true, + "name": true, + "status": true, + "since": true, + "volume": true, +} + +// iterationAction represents possible outcomes happening during the container iteration. +type iterationAction int + +// containerReducer represents a reducer for a container. +// Returns the object to serialize by the api. +type containerReducer func(*container.Container, *listContext) (*types.Container, error) + +const ( + // includeContainer is the action to include a container in the reducer. + includeContainer iterationAction = iota + // excludeContainer is the action to exclude a container in the reducer. + excludeContainer + // stopIteration is the action to stop iterating over the list of containers. + stopIteration +) + +// errStopIteration makes the iterator to stop without returning an error. +var errStopIteration = errors.New("container list iteration stopped") + +// List returns an array of all containers registered in the daemon. +func (daemon *Daemon) List() []*container.Container { + return daemon.containers.List() +} + +// listContext is the daemon generated filtering to iterate over containers. +// This is created based on the user specification from types.ContainerListOptions. +type listContext struct { + // idx is the container iteration index for this context + idx int + // ancestorFilter tells whether it should check ancestors or not + ancestorFilter bool + // names is a list of container names to filter with + names map[string][]string + // images is a list of images to filter with + images map[image.ID]bool + // filters is a collection of arguments to filter with, specified by the user + filters filters.Args + // exitAllowed is a list of exit codes allowed to filter with + exitAllowed []int + + // FIXME Remove this for 1.12 as --since and --before are deprecated + // beforeContainer is a filter to ignore containers that appear before the one given + beforeContainer *container.Container + // sinceContainer is a filter to stop the filtering when the iterator arrive to the given container + sinceContainer *container.Container + + // beforeFilter is a filter to ignore containers that appear before the one given + // this is used for --filter=before= and --before=, the latter is deprecated. + beforeFilter *container.Container + // sinceFilter is a filter to stop the filtering when the iterator arrive to the given container + // this is used for --filter=since= and --since=, the latter is deprecated. + sinceFilter *container.Container + // ContainerListOptions is the filters set by the user + *types.ContainerListOptions +} + +// Containers returns the list of containers to show given the user's filtering. +func (daemon *Daemon) Containers(config *types.ContainerListOptions) ([]*types.Container, error) { + return daemon.reduceContainers(config, daemon.transformContainer) +} + +// reduceContainers parses the user's filtering options and generates the list of containers to return based on a reducer. +func (daemon *Daemon) reduceContainers(config *types.ContainerListOptions, reducer containerReducer) ([]*types.Container, error) { + containers := []*types.Container{} + + ctx, err := daemon.foldFilter(config) + if err != nil { + return nil, err + } + + for _, container := range daemon.List() { + t, err := daemon.reducePsContainer(container, ctx, reducer) + if err != nil { + if err != errStopIteration { + return nil, err + } + break + } + if t != nil { + containers = append(containers, t) + ctx.idx++ + } + } + return containers, nil +} + +// reducePsContainer is the basic representation for a container as expected by the ps command. +func (daemon *Daemon) reducePsContainer(container *container.Container, ctx *listContext, reducer containerReducer) (*types.Container, error) { + container.Lock() + defer container.Unlock() + + // filter containers to return + action := includeContainerInList(container, ctx) + switch action { + case excludeContainer: + return nil, nil + case stopIteration: + return nil, errStopIteration + } + + // transform internal container struct into api structs + return reducer(container, ctx) +} + +// foldFilter generates the container filter based on the user's filtering options. +func (daemon *Daemon) foldFilter(config *types.ContainerListOptions) (*listContext, error) { + psFilters := config.Filter + + if err := psFilters.Validate(acceptedPsFilterTags); err != nil { + return nil, err + } + + var filtExited []int + + err := psFilters.WalkValues("exited", func(value string) error { + code, err := strconv.Atoi(value) + if err != nil { + return err + } + filtExited = append(filtExited, code) + return nil + }) + if err != nil { + return nil, err + } + + err = psFilters.WalkValues("status", func(value string) error { + if !container.IsValidStateString(value) { + return fmt.Errorf("Unrecognised filter value for status: %s", value) + } + + config.All = true + return nil + }) + if err != nil { + return nil, err + } + + var beforeContFilter, sinceContFilter *container.Container + // FIXME remove this for 1.12 as --since and --before are deprecated + var beforeContainer, sinceContainer *container.Container + + err = psFilters.WalkValues("before", func(value string) error { + beforeContFilter, err = daemon.GetContainer(value) + return err + }) + if err != nil { + return nil, err + } + + err = psFilters.WalkValues("since", func(value string) error { + sinceContFilter, err = daemon.GetContainer(value) + return err + }) + if err != nil { + return nil, err + } + + imagesFilter := map[image.ID]bool{} + var ancestorFilter bool + if psFilters.Include("ancestor") { + ancestorFilter = true + psFilters.WalkValues("ancestor", func(ancestor string) error { + id, err := daemon.GetImageID(ancestor) + if err != nil { + logrus.Warnf("Error while looking up for image %v", ancestor) + return nil + } + if imagesFilter[id] { + // Already seen this ancestor, skip it + return nil + } + // Then walk down the graph and put the imageIds in imagesFilter + populateImageFilterByParents(imagesFilter, id, daemon.imageStore.Children) + return nil + }) + } + + // FIXME remove this for 1.12 as --since and --before are deprecated + if config.Before != "" { + beforeContainer, err = daemon.GetContainer(config.Before) + if err != nil { + return nil, err + } + } + + // FIXME remove this for 1.12 as --since and --before are deprecated + if config.Since != "" { + sinceContainer, err = daemon.GetContainer(config.Since) + if err != nil { + return nil, err + } + } + + return &listContext{ + filters: psFilters, + ancestorFilter: ancestorFilter, + images: imagesFilter, + exitAllowed: filtExited, + beforeContainer: beforeContainer, + sinceContainer: sinceContainer, + beforeFilter: beforeContFilter, + sinceFilter: sinceContFilter, + ContainerListOptions: config, + names: daemon.nameIndex.GetAll(), + }, nil +} + +// includeContainerInList decides whether a container should be included in the output or not based in the filter. +// It also decides if the iteration should be stopped or not. +func includeContainerInList(container *container.Container, ctx *listContext) iterationAction { + // Do not include container if it's in the list before the filter container. + // Set the filter container to nil to include the rest of containers after this one. + if ctx.beforeFilter != nil { + if container.ID == ctx.beforeFilter.ID { + ctx.beforeFilter = nil + } + return excludeContainer + } + + // Stop iteration when the container arrives to the filter container + if ctx.sinceFilter != nil { + if container.ID == ctx.sinceFilter.ID { + return stopIteration + } + } + + // Do not include container if it's stopped and we're not filters + // FIXME remove the ctx.beforContainer and ctx.sinceContainer part of the condition for 1.12 as --since and --before are deprecated + if !container.Running && !ctx.All && ctx.Limit <= 0 && ctx.beforeContainer == nil && ctx.sinceContainer == nil { + return excludeContainer + } + + // Do not include container if the name doesn't match + if !ctx.filters.Match("name", container.Name) { + return excludeContainer + } + + // Do not include container if the id doesn't match + if !ctx.filters.Match("id", container.ID) { + return excludeContainer + } + + // Do not include container if any of the labels don't match + if !ctx.filters.MatchKVList("label", container.Config.Labels) { + return excludeContainer + } + + // Do not include container if isolation doesn't match + if excludeContainer == excludeByIsolation(container, ctx) { + return excludeContainer + } + + // FIXME remove this for 1.12 as --since and --before are deprecated + if ctx.beforeContainer != nil { + if container.ID == ctx.beforeContainer.ID { + ctx.beforeContainer = nil + } + return excludeContainer + } + + // FIXME remove this for 1.12 as --since and --before are deprecated + if ctx.sinceContainer != nil { + if container.ID == ctx.sinceContainer.ID { + return stopIteration + } + } + + // Stop iteration when the index is over the limit + if ctx.Limit > 0 && ctx.idx == ctx.Limit { + return stopIteration + } + + // Do not include container if its exit code is not in the filter + if len(ctx.exitAllowed) > 0 { + shouldSkip := true + for _, code := range ctx.exitAllowed { + if code == container.ExitCode && !container.Running { + shouldSkip = false + break + } + } + if shouldSkip { + return excludeContainer + } + } + + // Do not include container if its status doesn't match the filter + if !ctx.filters.Match("status", container.State.StateString()) { + return excludeContainer + } + + if ctx.filters.Include("volume") { + volumesByName := make(map[string]*volume.MountPoint) + for _, m := range container.MountPoints { + if m.Name != "" { + volumesByName[m.Name] = m + } else { + volumesByName[m.Source] = m + } + } + + volumeExist := fmt.Errorf("volume mounted in container") + err := ctx.filters.WalkValues("volume", func(value string) error { + if _, exist := container.MountPoints[value]; exist { + return volumeExist + } + if _, exist := volumesByName[value]; exist { + return volumeExist + } + return nil + }) + if err != volumeExist { + return excludeContainer + } + } + + if ctx.ancestorFilter { + if len(ctx.images) == 0 { + return excludeContainer + } + if !ctx.images[container.ImageID] { + return excludeContainer + } + } + + return includeContainer +} + +// transformContainer generates the container type expected by the docker ps command. +func (daemon *Daemon) transformContainer(container *container.Container, ctx *listContext) (*types.Container, error) { + newC := &types.Container{ + ID: container.ID, + Names: ctx.names[container.ID], + ImageID: container.ImageID.String(), + } + if newC.Names == nil { + // Dead containers will often have no name, so make sure the response isn't null + newC.Names = []string{} + } + + image := container.Config.Image // if possible keep the original ref + if image != container.ImageID.String() { + id, err := daemon.GetImageID(image) + if _, isDNE := err.(ErrImageDoesNotExist); err != nil && !isDNE { + return nil, err + } + if err != nil || id != container.ImageID { + image = container.ImageID.String() + } + } + newC.Image = image + + if len(container.Args) > 0 { + args := []string{} + for _, arg := range container.Args { + if strings.Contains(arg, " ") { + args = append(args, fmt.Sprintf("'%s'", arg)) + } else { + args = append(args, arg) + } + } + argsAsString := strings.Join(args, " ") + + newC.Command = fmt.Sprintf("%s %s", container.Path, argsAsString) + } else { + newC.Command = container.Path + } + newC.Created = container.Created.Unix() + newC.State = container.State.StateString() + newC.Status = container.State.String() + newC.HostConfig.NetworkMode = string(container.HostConfig.NetworkMode) + // copy networks to avoid races + networks := make(map[string]*networktypes.EndpointSettings) + for name, network := range container.NetworkSettings.Networks { + if network == nil { + continue + } + networks[name] = &networktypes.EndpointSettings{ + EndpointID: network.EndpointID, + Gateway: network.Gateway, + IPAddress: network.IPAddress, + IPPrefixLen: network.IPPrefixLen, + IPv6Gateway: network.IPv6Gateway, + GlobalIPv6Address: network.GlobalIPv6Address, + GlobalIPv6PrefixLen: network.GlobalIPv6PrefixLen, + MacAddress: network.MacAddress, + } + if network.IPAMConfig != nil { + networks[name].IPAMConfig = &networktypes.EndpointIPAMConfig{ + IPv4Address: network.IPAMConfig.IPv4Address, + IPv6Address: network.IPAMConfig.IPv6Address, + } + } + } + newC.NetworkSettings = &types.SummaryNetworkSettings{Networks: networks} + + newC.Ports = []types.Port{} + for port, bindings := range container.NetworkSettings.Ports { + p, err := nat.ParsePort(port.Port()) + if err != nil { + return nil, err + } + if len(bindings) == 0 { + newC.Ports = append(newC.Ports, types.Port{ + PrivatePort: p, + Type: port.Proto(), + }) + continue + } + for _, binding := range bindings { + h, err := nat.ParsePort(binding.HostPort) + if err != nil { + return nil, err + } + newC.Ports = append(newC.Ports, types.Port{ + PrivatePort: p, + PublicPort: h, + Type: port.Proto(), + IP: binding.HostIP, + }) + } + } + + if ctx.Size { + sizeRw, sizeRootFs := daemon.getSize(container) + newC.SizeRw = sizeRw + newC.SizeRootFs = sizeRootFs + } + newC.Labels = container.Config.Labels + newC.Mounts = addMountPoints(container) + + return newC, nil +} + +// Volumes lists known volumes, using the filter to restrict the range +// of volumes returned. +func (daemon *Daemon) Volumes(filter string) ([]*types.Volume, []string, error) { + var ( + volumesOut []*types.Volume + danglingOnly = false + ) + volFilters, err := filters.FromParam(filter) + if err != nil { + return nil, nil, err + } + + if err := volFilters.Validate(acceptedVolumeFilterTags); err != nil { + return nil, nil, err + } + + if volFilters.Include("dangling") { + if volFilters.ExactMatch("dangling", "true") || volFilters.ExactMatch("dangling", "1") { + danglingOnly = true + } else if !volFilters.ExactMatch("dangling", "false") && !volFilters.ExactMatch("dangling", "0") { + return nil, nil, fmt.Errorf("Invalid filter 'dangling=%s'", volFilters.Get("dangling")) + } + } + + volumes, warnings, err := daemon.volumes.List() + if err != nil { + return nil, nil, err + } + if volFilters.Include("dangling") { + volumes = daemon.volumes.FilterByUsed(volumes, !danglingOnly) + } + for _, v := range volumes { + volumesOut = append(volumesOut, volumeToAPIType(v)) + } + return volumesOut, warnings, nil +} + +func populateImageFilterByParents(ancestorMap map[image.ID]bool, imageID image.ID, getChildren func(image.ID) []image.ID) { + if !ancestorMap[imageID] { + for _, id := range getChildren(imageID) { + populateImageFilterByParents(ancestorMap, id, getChildren) + } + ancestorMap[imageID] = true + } +} diff --git a/daemon/list_unix.go b/daemon/list_unix.go new file mode 100644 index 00000000..8dccbe4e --- /dev/null +++ b/daemon/list_unix.go @@ -0,0 +1,11 @@ +// +build linux freebsd + +package daemon + +import "github.com/docker/docker/container" + +// excludeByIsolation is a platform specific helper function to support PS +// filtering by Isolation. This is a Windows-only concept, so is a no-op on Unix. +func excludeByIsolation(container *container.Container, ctx *listContext) iterationAction { + return includeContainer +} diff --git a/daemon/list_windows.go b/daemon/list_windows.go new file mode 100644 index 00000000..7fbcd3af --- /dev/null +++ b/daemon/list_windows.go @@ -0,0 +1,20 @@ +package daemon + +import ( + "strings" + + "github.com/docker/docker/container" +) + +// excludeByIsolation is a platform specific helper function to support PS +// filtering by Isolation. This is a Windows-only concept, so is a no-op on Unix. +func excludeByIsolation(container *container.Container, ctx *listContext) iterationAction { + i := strings.ToLower(string(container.HostConfig.Isolation)) + if i == "" { + i = "default" + } + if !ctx.filters.Match("isolation", i) { + return excludeContainer + } + return includeContainer +} diff --git a/daemon/logdrivers_linux.go b/daemon/logdrivers_linux.go new file mode 100644 index 00000000..89fe49a8 --- /dev/null +++ b/daemon/logdrivers_linux.go @@ -0,0 +1,14 @@ +package daemon + +import ( + // Importing packages here only to make sure their init gets called and + // therefore they register themselves to the logdriver factory. + _ "github.com/docker/docker/daemon/logger/awslogs" + _ "github.com/docker/docker/daemon/logger/fluentd" + _ "github.com/docker/docker/daemon/logger/gcplogs" + _ "github.com/docker/docker/daemon/logger/gelf" + _ "github.com/docker/docker/daemon/logger/journald" + _ "github.com/docker/docker/daemon/logger/jsonfilelog" + _ "github.com/docker/docker/daemon/logger/splunk" + _ "github.com/docker/docker/daemon/logger/syslog" +) diff --git a/daemon/logdrivers_windows.go b/daemon/logdrivers_windows.go new file mode 100644 index 00000000..129b0665 --- /dev/null +++ b/daemon/logdrivers_windows.go @@ -0,0 +1,10 @@ +package daemon + +import ( + // Importing packages here only to make sure their init gets called and + // therefore they register themselves to the logdriver factory. + _ "github.com/docker/docker/daemon/logger/awslogs" + _ "github.com/docker/docker/daemon/logger/etwlogs" + _ "github.com/docker/docker/daemon/logger/jsonfilelog" + _ "github.com/docker/docker/daemon/logger/splunk" +) diff --git a/daemon/logger/awslogs/cloudwatchlogs.go b/daemon/logger/awslogs/cloudwatchlogs.go new file mode 100644 index 00000000..698a95d5 --- /dev/null +++ b/daemon/logger/awslogs/cloudwatchlogs.go @@ -0,0 +1,376 @@ +// Package awslogs provides the logdriver for forwarding container logs to Amazon CloudWatch Logs +package awslogs + +import ( + "errors" + "fmt" + "os" + "runtime" + "sort" + "strings" + "sync" + "time" + + "github.com/Sirupsen/logrus" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/defaults" + "github.com/aws/aws-sdk-go/aws/ec2metadata" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + "github.com/docker/docker/daemon/logger" + "github.com/docker/docker/dockerversion" +) + +const ( + name = "awslogs" + regionKey = "awslogs-region" + regionEnvKey = "AWS_REGION" + logGroupKey = "awslogs-group" + logStreamKey = "awslogs-stream" + batchPublishFrequency = 5 * time.Second + + // See: http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html + perEventBytes = 26 + maximumBytesPerPut = 1048576 + maximumLogEventsPerPut = 10000 + + // See: http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/cloudwatch_limits.html + maximumBytesPerEvent = 262144 - perEventBytes + + resourceAlreadyExistsCode = "ResourceAlreadyExistsException" + dataAlreadyAcceptedCode = "DataAlreadyAcceptedException" + invalidSequenceTokenCode = "InvalidSequenceTokenException" + + userAgentHeader = "User-Agent" +) + +type logStream struct { + logStreamName string + logGroupName string + client api + messages chan *logger.Message + lock sync.RWMutex + closed bool + sequenceToken *string +} + +type api interface { + CreateLogStream(*cloudwatchlogs.CreateLogStreamInput) (*cloudwatchlogs.CreateLogStreamOutput, error) + PutLogEvents(*cloudwatchlogs.PutLogEventsInput) (*cloudwatchlogs.PutLogEventsOutput, error) +} + +type regionFinder interface { + Region() (string, error) +} + +type byTimestamp []*cloudwatchlogs.InputLogEvent + +// init registers the awslogs driver and sets the default region, if provided +func init() { + if os.Getenv(regionEnvKey) != "" { + defaults.DefaultConfig.Region = aws.String(os.Getenv(regionEnvKey)) + } + if err := logger.RegisterLogDriver(name, New); err != nil { + logrus.Fatal(err) + } + if err := logger.RegisterLogOptValidator(name, ValidateLogOpt); err != nil { + logrus.Fatal(err) + } +} + +// New creates an awslogs logger using the configuration passed in on the +// context. Supported context configuration variables are awslogs-region, +// awslogs-group, and awslogs-stream. When available, configuration is +// also taken from environment variables AWS_REGION, AWS_ACCESS_KEY_ID, +// AWS_SECRET_ACCESS_KEY, the shared credentials file (~/.aws/credentials), and +// the EC2 Instance Metadata Service. +func New(ctx logger.Context) (logger.Logger, error) { + logGroupName := ctx.Config[logGroupKey] + logStreamName := ctx.ContainerID + if ctx.Config[logStreamKey] != "" { + logStreamName = ctx.Config[logStreamKey] + } + client, err := newAWSLogsClient(ctx) + if err != nil { + return nil, err + } + containerStream := &logStream{ + logStreamName: logStreamName, + logGroupName: logGroupName, + client: client, + messages: make(chan *logger.Message, 4096), + } + err = containerStream.create() + if err != nil { + return nil, err + } + go containerStream.collectBatch() + + return containerStream, nil +} + +// newRegionFinder is a variable such that the implementation +// can be swapped out for unit tests. +var newRegionFinder = func() regionFinder { + return ec2metadata.New(nil) +} + +// newAWSLogsClient creates the service client for Amazon CloudWatch Logs. +// Customizations to the default client from the SDK include a Docker-specific +// User-Agent string and automatic region detection using the EC2 Instance +// Metadata Service when region is otherwise unspecified. +func newAWSLogsClient(ctx logger.Context) (api, error) { + config := defaults.DefaultConfig + if ctx.Config[regionKey] != "" { + config = defaults.DefaultConfig.Merge(&aws.Config{ + Region: aws.String(ctx.Config[regionKey]), + }) + } + if config.Region == nil || *config.Region == "" { + logrus.Info("Trying to get region from EC2 Metadata") + ec2MetadataClient := newRegionFinder() + region, err := ec2MetadataClient.Region() + if err != nil { + logrus.WithFields(logrus.Fields{ + "error": err, + }).Error("Could not get region from EC2 metadata, environment, or log option") + return nil, errors.New("Cannot determine region for awslogs driver") + } + config.Region = ®ion + } + logrus.WithFields(logrus.Fields{ + "region": *config.Region, + }).Debug("Created awslogs client") + client := cloudwatchlogs.New(config) + + client.Handlers.Build.PushBackNamed(request.NamedHandler{ + Name: "DockerUserAgentHandler", + Fn: func(r *request.Request) { + currentAgent := r.HTTPRequest.Header.Get(userAgentHeader) + r.HTTPRequest.Header.Set(userAgentHeader, + fmt.Sprintf("Docker %s (%s) %s", + dockerversion.Version, runtime.GOOS, currentAgent)) + }, + }) + return client, nil +} + +// Name returns the name of the awslogs logging driver +func (l *logStream) Name() string { + return name +} + +// Log submits messages for logging by an instance of the awslogs logging driver +func (l *logStream) Log(msg *logger.Message) error { + l.lock.RLock() + defer l.lock.RUnlock() + if !l.closed { + l.messages <- msg + } + return nil +} + +// Close closes the instance of the awslogs logging driver +func (l *logStream) Close() error { + l.lock.Lock() + defer l.lock.Unlock() + if !l.closed { + close(l.messages) + } + l.closed = true + return nil +} + +// create creates a log stream for the instance of the awslogs logging driver +func (l *logStream) create() error { + input := &cloudwatchlogs.CreateLogStreamInput{ + LogGroupName: aws.String(l.logGroupName), + LogStreamName: aws.String(l.logStreamName), + } + + _, err := l.client.CreateLogStream(input) + + if err != nil { + if awsErr, ok := err.(awserr.Error); ok { + fields := logrus.Fields{ + "errorCode": awsErr.Code(), + "message": awsErr.Message(), + "origError": awsErr.OrigErr(), + "logGroupName": l.logGroupName, + "logStreamName": l.logStreamName, + } + if awsErr.Code() == resourceAlreadyExistsCode { + // Allow creation to succeed + logrus.WithFields(fields).Info("Log stream already exists") + return nil + } + logrus.WithFields(fields).Error("Failed to create log stream") + } + } + return err +} + +// newTicker is used for time-based batching. newTicker is a variable such +// that the implementation can be swapped out for unit tests. +var newTicker = func(freq time.Duration) *time.Ticker { + return time.NewTicker(freq) +} + +// collectBatch executes as a goroutine to perform batching of log events for +// submission to the log stream. Batching is performed on time- and size- +// bases. Time-based batching occurs at a 5 second interval (defined in the +// batchPublishFrequency const). Size-based batching is performed on the +// maximum number of events per batch (defined in maximumLogEventsPerPut) and +// the maximum number of total bytes in a batch (defined in +// maximumBytesPerPut). Log messages are split by the maximum bytes per event +// (defined in maximumBytesPerEvent). There is a fixed per-event byte overhead +// (defined in perEventBytes) which is accounted for in split- and batch- +// calculations. +func (l *logStream) collectBatch() { + timer := newTicker(batchPublishFrequency) + var events []*cloudwatchlogs.InputLogEvent + bytes := 0 + for { + select { + case <-timer.C: + l.publishBatch(events) + events = events[:0] + bytes = 0 + case msg, more := <-l.messages: + if !more { + l.publishBatch(events) + return + } + unprocessedLine := msg.Line + for len(unprocessedLine) > 0 { + // Split line length so it does not exceed the maximum + lineBytes := len(unprocessedLine) + if lineBytes > maximumBytesPerEvent { + lineBytes = maximumBytesPerEvent + } + line := unprocessedLine[:lineBytes] + unprocessedLine = unprocessedLine[lineBytes:] + if (len(events) >= maximumLogEventsPerPut) || (bytes+lineBytes+perEventBytes > maximumBytesPerPut) { + // Publish an existing batch if it's already over the maximum number of events or if adding this + // event would push it over the maximum number of total bytes. + l.publishBatch(events) + events = events[:0] + bytes = 0 + } + events = append(events, &cloudwatchlogs.InputLogEvent{ + Message: aws.String(string(line)), + Timestamp: aws.Int64(msg.Timestamp.UnixNano() / int64(time.Millisecond)), + }) + bytes += (lineBytes + perEventBytes) + } + } + } +} + +// publishBatch calls PutLogEvents for a given set of InputLogEvents, +// accounting for sequencing requirements (each request must reference the +// sequence token returned by the previous request). +func (l *logStream) publishBatch(events []*cloudwatchlogs.InputLogEvent) { + if len(events) == 0 { + return + } + + sort.Sort(byTimestamp(events)) + + nextSequenceToken, err := l.putLogEvents(events, l.sequenceToken) + + if err != nil { + if awsErr, ok := err.(awserr.Error); ok { + if awsErr.Code() == dataAlreadyAcceptedCode { + // already submitted, just grab the correct sequence token + parts := strings.Split(awsErr.Message(), " ") + nextSequenceToken = &parts[len(parts)-1] + logrus.WithFields(logrus.Fields{ + "errorCode": awsErr.Code(), + "message": awsErr.Message(), + "logGroupName": l.logGroupName, + "logStreamName": l.logStreamName, + }).Info("Data already accepted, ignoring error") + err = nil + } else if awsErr.Code() == invalidSequenceTokenCode { + // sequence code is bad, grab the correct one and retry + parts := strings.Split(awsErr.Message(), " ") + token := parts[len(parts)-1] + nextSequenceToken, err = l.putLogEvents(events, &token) + } + } + } + if err != nil { + logrus.Error(err) + } else { + l.sequenceToken = nextSequenceToken + } +} + +// putLogEvents wraps the PutLogEvents API +func (l *logStream) putLogEvents(events []*cloudwatchlogs.InputLogEvent, sequenceToken *string) (*string, error) { + input := &cloudwatchlogs.PutLogEventsInput{ + LogEvents: events, + SequenceToken: sequenceToken, + LogGroupName: aws.String(l.logGroupName), + LogStreamName: aws.String(l.logStreamName), + } + resp, err := l.client.PutLogEvents(input) + if err != nil { + if awsErr, ok := err.(awserr.Error); ok { + logrus.WithFields(logrus.Fields{ + "errorCode": awsErr.Code(), + "message": awsErr.Message(), + "origError": awsErr.OrigErr(), + "logGroupName": l.logGroupName, + "logStreamName": l.logStreamName, + }).Error("Failed to put log events") + } + return nil, err + } + return resp.NextSequenceToken, nil +} + +// ValidateLogOpt looks for awslogs-specific log options awslogs-region, +// awslogs-group, and awslogs-stream +func ValidateLogOpt(cfg map[string]string) error { + for key := range cfg { + switch key { + case logGroupKey: + case logStreamKey: + case regionKey: + default: + return fmt.Errorf("unknown log opt '%s' for %s log driver", key, name) + } + } + if cfg[logGroupKey] == "" { + return fmt.Errorf("must specify a value for log opt '%s'", logGroupKey) + } + return nil +} + +// Len returns the length of a byTimestamp slice. Len is required by the +// sort.Interface interface. +func (slice byTimestamp) Len() int { + return len(slice) +} + +// Less compares two values in a byTimestamp slice by Timestamp. Less is +// required by the sort.Interface interface. +func (slice byTimestamp) Less(i, j int) bool { + iTimestamp, jTimestamp := int64(0), int64(0) + if slice != nil && slice[i].Timestamp != nil { + iTimestamp = *slice[i].Timestamp + } + if slice != nil && slice[j].Timestamp != nil { + jTimestamp = *slice[j].Timestamp + } + return iTimestamp < jTimestamp +} + +// Swap swaps two values in a byTimestamp slice with each other. Swap is +// required by the sort.Interface interface. +func (slice byTimestamp) Swap(i, j int) { + slice[i], slice[j] = slice[j], slice[i] +} diff --git a/daemon/logger/awslogs/cloudwatchlogs_test.go b/daemon/logger/awslogs/cloudwatchlogs_test.go new file mode 100644 index 00000000..0c53407f --- /dev/null +++ b/daemon/logger/awslogs/cloudwatchlogs_test.go @@ -0,0 +1,627 @@ +package awslogs + +import ( + "errors" + "fmt" + "net/http" + "runtime" + "strings" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + "github.com/docker/docker/daemon/logger" + "github.com/docker/docker/dockerversion" +) + +const ( + groupName = "groupName" + streamName = "streamName" + sequenceToken = "sequenceToken" + nextSequenceToken = "nextSequenceToken" + logline = "this is a log line" +) + +func TestNewAWSLogsClientUserAgentHandler(t *testing.T) { + ctx := logger.Context{ + Config: map[string]string{ + regionKey: "us-east-1", + }, + } + + client, err := newAWSLogsClient(ctx) + if err != nil { + t.Fatal(err) + } + realClient, ok := client.(*cloudwatchlogs.CloudWatchLogs) + if !ok { + t.Fatal("Could not cast client to cloudwatchlogs.CloudWatchLogs") + } + buildHandlerList := realClient.Handlers.Build + request := &request.Request{ + HTTPRequest: &http.Request{ + Header: http.Header{}, + }, + } + buildHandlerList.Run(request) + expectedUserAgentString := fmt.Sprintf("Docker %s (%s) %s/%s", + dockerversion.Version, runtime.GOOS, aws.SDKName, aws.SDKVersion) + userAgent := request.HTTPRequest.Header.Get("User-Agent") + if userAgent != expectedUserAgentString { + t.Errorf("Wrong User-Agent string, expected \"%s\" but was \"%s\"", + expectedUserAgentString, userAgent) + } +} + +func TestNewAWSLogsClientRegionDetect(t *testing.T) { + ctx := logger.Context{ + Config: map[string]string{}, + } + + mockMetadata := newMockMetadataClient() + newRegionFinder = func() regionFinder { + return mockMetadata + } + mockMetadata.regionResult <- ®ionResult{ + successResult: "us-east-1", + } + + _, err := newAWSLogsClient(ctx) + if err != nil { + t.Fatal(err) + } +} + +func TestCreateSuccess(t *testing.T) { + mockClient := newMockClient() + stream := &logStream{ + client: mockClient, + logGroupName: groupName, + logStreamName: streamName, + } + mockClient.createLogStreamResult <- &createLogStreamResult{} + + err := stream.create() + + if err != nil { + t.Errorf("Received unexpected err: %v\n", err) + } + argument := <-mockClient.createLogStreamArgument + if argument.LogGroupName == nil { + t.Fatal("Expected non-nil LogGroupName") + } + if *argument.LogGroupName != groupName { + t.Errorf("Expected LogGroupName to be %s", groupName) + } + if argument.LogStreamName == nil { + t.Fatal("Expected non-nil LogGroupName") + } + if *argument.LogStreamName != streamName { + t.Errorf("Expected LogStreamName to be %s", streamName) + } +} + +func TestCreateError(t *testing.T) { + mockClient := newMockClient() + stream := &logStream{ + client: mockClient, + } + mockClient.createLogStreamResult <- &createLogStreamResult{ + errorResult: errors.New("Error!"), + } + + err := stream.create() + + if err == nil { + t.Fatal("Expected non-nil err") + } +} + +func TestCreateAlreadyExists(t *testing.T) { + mockClient := newMockClient() + stream := &logStream{ + client: mockClient, + } + mockClient.createLogStreamResult <- &createLogStreamResult{ + errorResult: awserr.New(resourceAlreadyExistsCode, "", nil), + } + + err := stream.create() + + if err != nil { + t.Fatal("Expected nil err") + } +} + +func TestPublishBatchSuccess(t *testing.T) { + mockClient := newMockClient() + stream := &logStream{ + client: mockClient, + logGroupName: groupName, + logStreamName: streamName, + sequenceToken: aws.String(sequenceToken), + } + mockClient.putLogEventsResult <- &putLogEventsResult{ + successResult: &cloudwatchlogs.PutLogEventsOutput{ + NextSequenceToken: aws.String(nextSequenceToken), + }, + } + + events := []*cloudwatchlogs.InputLogEvent{ + { + Message: aws.String(logline), + }, + } + + stream.publishBatch(events) + if stream.sequenceToken == nil { + t.Fatal("Expected non-nil sequenceToken") + } + if *stream.sequenceToken != nextSequenceToken { + t.Errorf("Expected sequenceToken to be %s, but was %s", nextSequenceToken, *stream.sequenceToken) + } + argument := <-mockClient.putLogEventsArgument + if argument == nil { + t.Fatal("Expected non-nil PutLogEventsInput") + } + if argument.SequenceToken == nil { + t.Fatal("Expected non-nil PutLogEventsInput.SequenceToken") + } + if *argument.SequenceToken != sequenceToken { + t.Errorf("Expected PutLogEventsInput.SequenceToken to be %s, but was %s", sequenceToken, *argument.SequenceToken) + } + if len(argument.LogEvents) != 1 { + t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents)) + } + if argument.LogEvents[0] != events[0] { + t.Error("Expected event to equal input") + } +} + +func TestPublishBatchError(t *testing.T) { + mockClient := newMockClient() + stream := &logStream{ + client: mockClient, + logGroupName: groupName, + logStreamName: streamName, + sequenceToken: aws.String(sequenceToken), + } + mockClient.putLogEventsResult <- &putLogEventsResult{ + errorResult: errors.New("Error!"), + } + + events := []*cloudwatchlogs.InputLogEvent{ + { + Message: aws.String(logline), + }, + } + + stream.publishBatch(events) + if stream.sequenceToken == nil { + t.Fatal("Expected non-nil sequenceToken") + } + if *stream.sequenceToken != sequenceToken { + t.Errorf("Expected sequenceToken to be %s, but was %s", sequenceToken, *stream.sequenceToken) + } +} + +func TestPublishBatchInvalidSeqSuccess(t *testing.T) { + mockClient := newMockClientBuffered(2) + stream := &logStream{ + client: mockClient, + logGroupName: groupName, + logStreamName: streamName, + sequenceToken: aws.String(sequenceToken), + } + mockClient.putLogEventsResult <- &putLogEventsResult{ + errorResult: awserr.New(invalidSequenceTokenCode, "use token token", nil), + } + mockClient.putLogEventsResult <- &putLogEventsResult{ + successResult: &cloudwatchlogs.PutLogEventsOutput{ + NextSequenceToken: aws.String(nextSequenceToken), + }, + } + + events := []*cloudwatchlogs.InputLogEvent{ + { + Message: aws.String(logline), + }, + } + + stream.publishBatch(events) + if stream.sequenceToken == nil { + t.Fatal("Expected non-nil sequenceToken") + } + if *stream.sequenceToken != nextSequenceToken { + t.Errorf("Expected sequenceToken to be %s, but was %s", nextSequenceToken, *stream.sequenceToken) + } + + argument := <-mockClient.putLogEventsArgument + if argument == nil { + t.Fatal("Expected non-nil PutLogEventsInput") + } + if argument.SequenceToken == nil { + t.Fatal("Expected non-nil PutLogEventsInput.SequenceToken") + } + if *argument.SequenceToken != sequenceToken { + t.Errorf("Expected PutLogEventsInput.SequenceToken to be %s, but was %s", sequenceToken, *argument.SequenceToken) + } + if len(argument.LogEvents) != 1 { + t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents)) + } + if argument.LogEvents[0] != events[0] { + t.Error("Expected event to equal input") + } + + argument = <-mockClient.putLogEventsArgument + if argument == nil { + t.Fatal("Expected non-nil PutLogEventsInput") + } + if argument.SequenceToken == nil { + t.Fatal("Expected non-nil PutLogEventsInput.SequenceToken") + } + if *argument.SequenceToken != "token" { + t.Errorf("Expected PutLogEventsInput.SequenceToken to be %s, but was %s", "token", *argument.SequenceToken) + } + if len(argument.LogEvents) != 1 { + t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents)) + } + if argument.LogEvents[0] != events[0] { + t.Error("Expected event to equal input") + } +} + +func TestPublishBatchAlreadyAccepted(t *testing.T) { + mockClient := newMockClient() + stream := &logStream{ + client: mockClient, + logGroupName: groupName, + logStreamName: streamName, + sequenceToken: aws.String(sequenceToken), + } + mockClient.putLogEventsResult <- &putLogEventsResult{ + errorResult: awserr.New(dataAlreadyAcceptedCode, "use token token", nil), + } + + events := []*cloudwatchlogs.InputLogEvent{ + { + Message: aws.String(logline), + }, + } + + stream.publishBatch(events) + if stream.sequenceToken == nil { + t.Fatal("Expected non-nil sequenceToken") + } + if *stream.sequenceToken != "token" { + t.Errorf("Expected sequenceToken to be %s, but was %s", "token", *stream.sequenceToken) + } + + argument := <-mockClient.putLogEventsArgument + if argument == nil { + t.Fatal("Expected non-nil PutLogEventsInput") + } + if argument.SequenceToken == nil { + t.Fatal("Expected non-nil PutLogEventsInput.SequenceToken") + } + if *argument.SequenceToken != sequenceToken { + t.Errorf("Expected PutLogEventsInput.SequenceToken to be %s, but was %s", sequenceToken, *argument.SequenceToken) + } + if len(argument.LogEvents) != 1 { + t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents)) + } + if argument.LogEvents[0] != events[0] { + t.Error("Expected event to equal input") + } +} + +func TestCollectBatchSimple(t *testing.T) { + mockClient := newMockClient() + stream := &logStream{ + client: mockClient, + logGroupName: groupName, + logStreamName: streamName, + sequenceToken: aws.String(sequenceToken), + messages: make(chan *logger.Message), + } + mockClient.putLogEventsResult <- &putLogEventsResult{ + successResult: &cloudwatchlogs.PutLogEventsOutput{ + NextSequenceToken: aws.String(nextSequenceToken), + }, + } + ticks := make(chan time.Time) + newTicker = func(_ time.Duration) *time.Ticker { + return &time.Ticker{ + C: ticks, + } + } + + go stream.collectBatch() + + stream.Log(&logger.Message{ + Line: []byte(logline), + Timestamp: time.Time{}, + }) + + ticks <- time.Time{} + stream.Close() + + argument := <-mockClient.putLogEventsArgument + if argument == nil { + t.Fatal("Expected non-nil PutLogEventsInput") + } + if len(argument.LogEvents) != 1 { + t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents)) + } + if *argument.LogEvents[0].Message != logline { + t.Errorf("Expected message to be %s but was %s", logline, *argument.LogEvents[0].Message) + } +} + +func TestCollectBatchTicker(t *testing.T) { + mockClient := newMockClient() + stream := &logStream{ + client: mockClient, + logGroupName: groupName, + logStreamName: streamName, + sequenceToken: aws.String(sequenceToken), + messages: make(chan *logger.Message), + } + mockClient.putLogEventsResult <- &putLogEventsResult{ + successResult: &cloudwatchlogs.PutLogEventsOutput{ + NextSequenceToken: aws.String(nextSequenceToken), + }, + } + ticks := make(chan time.Time) + newTicker = func(_ time.Duration) *time.Ticker { + return &time.Ticker{ + C: ticks, + } + } + + go stream.collectBatch() + + stream.Log(&logger.Message{ + Line: []byte(logline + " 1"), + Timestamp: time.Time{}, + }) + stream.Log(&logger.Message{ + Line: []byte(logline + " 2"), + Timestamp: time.Time{}, + }) + + ticks <- time.Time{} + + // Verify first batch + argument := <-mockClient.putLogEventsArgument + if argument == nil { + t.Fatal("Expected non-nil PutLogEventsInput") + } + if len(argument.LogEvents) != 2 { + t.Errorf("Expected LogEvents to contain 2 elements, but contains %d", len(argument.LogEvents)) + } + if *argument.LogEvents[0].Message != logline+" 1" { + t.Errorf("Expected message to be %s but was %s", logline+" 1", *argument.LogEvents[0].Message) + } + if *argument.LogEvents[1].Message != logline+" 2" { + t.Errorf("Expected message to be %s but was %s", logline+" 2", *argument.LogEvents[0].Message) + } + + stream.Log(&logger.Message{ + Line: []byte(logline + " 3"), + Timestamp: time.Time{}, + }) + + ticks <- time.Time{} + argument = <-mockClient.putLogEventsArgument + if argument == nil { + t.Fatal("Expected non-nil PutLogEventsInput") + } + if len(argument.LogEvents) != 1 { + t.Errorf("Expected LogEvents to contain 1 elements, but contains %d", len(argument.LogEvents)) + } + if *argument.LogEvents[0].Message != logline+" 3" { + t.Errorf("Expected message to be %s but was %s", logline+" 3", *argument.LogEvents[0].Message) + } + + stream.Close() + +} + +func TestCollectBatchClose(t *testing.T) { + mockClient := newMockClient() + stream := &logStream{ + client: mockClient, + logGroupName: groupName, + logStreamName: streamName, + sequenceToken: aws.String(sequenceToken), + messages: make(chan *logger.Message), + } + mockClient.putLogEventsResult <- &putLogEventsResult{ + successResult: &cloudwatchlogs.PutLogEventsOutput{ + NextSequenceToken: aws.String(nextSequenceToken), + }, + } + var ticks = make(chan time.Time) + newTicker = func(_ time.Duration) *time.Ticker { + return &time.Ticker{ + C: ticks, + } + } + + go stream.collectBatch() + + stream.Log(&logger.Message{ + Line: []byte(logline), + Timestamp: time.Time{}, + }) + + // no ticks + stream.Close() + + argument := <-mockClient.putLogEventsArgument + if argument == nil { + t.Fatal("Expected non-nil PutLogEventsInput") + } + if len(argument.LogEvents) != 1 { + t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents)) + } + if *argument.LogEvents[0].Message != logline { + t.Errorf("Expected message to be %s but was %s", logline, *argument.LogEvents[0].Message) + } +} + +func TestCollectBatchLineSplit(t *testing.T) { + mockClient := newMockClient() + stream := &logStream{ + client: mockClient, + logGroupName: groupName, + logStreamName: streamName, + sequenceToken: aws.String(sequenceToken), + messages: make(chan *logger.Message), + } + mockClient.putLogEventsResult <- &putLogEventsResult{ + successResult: &cloudwatchlogs.PutLogEventsOutput{ + NextSequenceToken: aws.String(nextSequenceToken), + }, + } + var ticks = make(chan time.Time) + newTicker = func(_ time.Duration) *time.Ticker { + return &time.Ticker{ + C: ticks, + } + } + + go stream.collectBatch() + + longline := strings.Repeat("A", maximumBytesPerEvent) + stream.Log(&logger.Message{ + Line: []byte(longline + "B"), + Timestamp: time.Time{}, + }) + + // no ticks + stream.Close() + + argument := <-mockClient.putLogEventsArgument + if argument == nil { + t.Fatal("Expected non-nil PutLogEventsInput") + } + if len(argument.LogEvents) != 2 { + t.Errorf("Expected LogEvents to contain 2 elements, but contains %d", len(argument.LogEvents)) + } + if *argument.LogEvents[0].Message != longline { + t.Errorf("Expected message to be %s but was %s", longline, *argument.LogEvents[0].Message) + } + if *argument.LogEvents[1].Message != "B" { + t.Errorf("Expected message to be %s but was %s", "B", *argument.LogEvents[1].Message) + } +} + +func TestCollectBatchMaxEvents(t *testing.T) { + mockClient := newMockClientBuffered(1) + stream := &logStream{ + client: mockClient, + logGroupName: groupName, + logStreamName: streamName, + sequenceToken: aws.String(sequenceToken), + messages: make(chan *logger.Message), + } + mockClient.putLogEventsResult <- &putLogEventsResult{ + successResult: &cloudwatchlogs.PutLogEventsOutput{ + NextSequenceToken: aws.String(nextSequenceToken), + }, + } + var ticks = make(chan time.Time) + newTicker = func(_ time.Duration) *time.Ticker { + return &time.Ticker{ + C: ticks, + } + } + + go stream.collectBatch() + + line := "A" + for i := 0; i <= maximumLogEventsPerPut; i++ { + stream.Log(&logger.Message{ + Line: []byte(line), + Timestamp: time.Time{}, + }) + } + + // no ticks + stream.Close() + + argument := <-mockClient.putLogEventsArgument + if argument == nil { + t.Fatal("Expected non-nil PutLogEventsInput") + } + if len(argument.LogEvents) != maximumLogEventsPerPut { + t.Errorf("Expected LogEvents to contain %d elements, but contains %d", maximumLogEventsPerPut, len(argument.LogEvents)) + } + + argument = <-mockClient.putLogEventsArgument + if argument == nil { + t.Fatal("Expected non-nil PutLogEventsInput") + } + if len(argument.LogEvents) != 1 { + t.Errorf("Expected LogEvents to contain %d elements, but contains %d", 1, len(argument.LogEvents)) + } +} + +func TestCollectBatchMaxTotalBytes(t *testing.T) { + mockClient := newMockClientBuffered(1) + stream := &logStream{ + client: mockClient, + logGroupName: groupName, + logStreamName: streamName, + sequenceToken: aws.String(sequenceToken), + messages: make(chan *logger.Message), + } + mockClient.putLogEventsResult <- &putLogEventsResult{ + successResult: &cloudwatchlogs.PutLogEventsOutput{ + NextSequenceToken: aws.String(nextSequenceToken), + }, + } + var ticks = make(chan time.Time) + newTicker = func(_ time.Duration) *time.Ticker { + return &time.Ticker{ + C: ticks, + } + } + + go stream.collectBatch() + + longline := strings.Repeat("A", maximumBytesPerPut) + stream.Log(&logger.Message{ + Line: []byte(longline + "B"), + Timestamp: time.Time{}, + }) + + // no ticks + stream.Close() + + argument := <-mockClient.putLogEventsArgument + if argument == nil { + t.Fatal("Expected non-nil PutLogEventsInput") + } + bytes := 0 + for _, event := range argument.LogEvents { + bytes += len(*event.Message) + } + if bytes > maximumBytesPerPut { + t.Errorf("Expected <= %d bytes but was %d", maximumBytesPerPut, bytes) + } + + argument = <-mockClient.putLogEventsArgument + if len(argument.LogEvents) != 1 { + t.Errorf("Expected LogEvents to contain 1 elements, but contains %d", len(argument.LogEvents)) + } + message := *argument.LogEvents[0].Message + if message[len(message)-1:] != "B" { + t.Errorf("Expected message to be %s but was %s", "B", message[len(message)-1:]) + } +} diff --git a/daemon/logger/awslogs/cwlogsiface_mock_test.go b/daemon/logger/awslogs/cwlogsiface_mock_test.go new file mode 100644 index 00000000..bfe6d74f --- /dev/null +++ b/daemon/logger/awslogs/cwlogsiface_mock_test.go @@ -0,0 +1,76 @@ +package awslogs + +import "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + +type mockcwlogsclient struct { + createLogStreamArgument chan *cloudwatchlogs.CreateLogStreamInput + createLogStreamResult chan *createLogStreamResult + putLogEventsArgument chan *cloudwatchlogs.PutLogEventsInput + putLogEventsResult chan *putLogEventsResult +} + +type createLogStreamResult struct { + successResult *cloudwatchlogs.CreateLogStreamOutput + errorResult error +} + +type putLogEventsResult struct { + successResult *cloudwatchlogs.PutLogEventsOutput + errorResult error +} + +func newMockClient() *mockcwlogsclient { + return &mockcwlogsclient{ + createLogStreamArgument: make(chan *cloudwatchlogs.CreateLogStreamInput, 1), + createLogStreamResult: make(chan *createLogStreamResult, 1), + putLogEventsArgument: make(chan *cloudwatchlogs.PutLogEventsInput, 1), + putLogEventsResult: make(chan *putLogEventsResult, 1), + } +} + +func newMockClientBuffered(buflen int) *mockcwlogsclient { + return &mockcwlogsclient{ + createLogStreamArgument: make(chan *cloudwatchlogs.CreateLogStreamInput, buflen), + createLogStreamResult: make(chan *createLogStreamResult, buflen), + putLogEventsArgument: make(chan *cloudwatchlogs.PutLogEventsInput, buflen), + putLogEventsResult: make(chan *putLogEventsResult, buflen), + } +} + +func (m *mockcwlogsclient) CreateLogStream(input *cloudwatchlogs.CreateLogStreamInput) (*cloudwatchlogs.CreateLogStreamOutput, error) { + m.createLogStreamArgument <- input + output := <-m.createLogStreamResult + return output.successResult, output.errorResult +} + +func (m *mockcwlogsclient) PutLogEvents(input *cloudwatchlogs.PutLogEventsInput) (*cloudwatchlogs.PutLogEventsOutput, error) { + m.putLogEventsArgument <- input + output := <-m.putLogEventsResult + return output.successResult, output.errorResult +} + +type mockmetadataclient struct { + regionResult chan *regionResult +} + +type regionResult struct { + successResult string + errorResult error +} + +func newMockMetadataClient() *mockmetadataclient { + return &mockmetadataclient{ + regionResult: make(chan *regionResult, 1), + } +} + +func (m *mockmetadataclient) Region() (string, error) { + output := <-m.regionResult + return output.successResult, output.errorResult +} + +func test() { + _ = &logStream{ + client: newMockClient(), + } +} diff --git a/daemon/logger/context.go b/daemon/logger/context.go new file mode 100644 index 00000000..eb54c311 --- /dev/null +++ b/daemon/logger/context.go @@ -0,0 +1,112 @@ +package logger + +import ( + "fmt" + "os" + "strings" + "time" +) + +// Context provides enough information for a logging driver to do its function. +type Context struct { + Config map[string]string + ContainerID string + ContainerName string + ContainerEntrypoint string + ContainerArgs []string + ContainerImageID string + ContainerImageName string + ContainerCreated time.Time + ContainerEnv []string + ContainerLabels map[string]string + LogPath string +} + +// ExtraAttributes returns the user-defined extra attributes (labels, +// environment variables) in key-value format. This can be used by log drivers +// that support metadata to add more context to a log. +func (ctx *Context) ExtraAttributes(keyMod func(string) string) map[string]string { + extra := make(map[string]string) + labels, ok := ctx.Config["labels"] + if ok && len(labels) > 0 { + for _, l := range strings.Split(labels, ",") { + if v, ok := ctx.ContainerLabels[l]; ok { + if keyMod != nil { + l = keyMod(l) + } + extra[l] = v + } + } + } + + env, ok := ctx.Config["env"] + if ok && len(env) > 0 { + envMapping := make(map[string]string) + for _, e := range ctx.ContainerEnv { + if kv := strings.SplitN(e, "=", 2); len(kv) == 2 { + envMapping[kv[0]] = kv[1] + } + } + for _, l := range strings.Split(env, ",") { + if v, ok := envMapping[l]; ok { + if keyMod != nil { + l = keyMod(l) + } + extra[l] = v + } + } + } + + return extra +} + +// Hostname returns the hostname from the underlying OS. +func (ctx *Context) Hostname() (string, error) { + hostname, err := os.Hostname() + if err != nil { + return "", fmt.Errorf("logger: can not resolve hostname: %v", err) + } + return hostname, nil +} + +// Command returns the command that the container being logged was +// started with. The Entrypoint is prepended to the container +// arguments. +func (ctx *Context) Command() string { + terms := []string{ctx.ContainerEntrypoint} + for _, arg := range ctx.ContainerArgs { + terms = append(terms, arg) + } + command := strings.Join(terms, " ") + return command +} + +// ID Returns the Container ID shortened to 12 characters. +func (ctx *Context) ID() string { + return ctx.ContainerID[:12] +} + +// FullID is an alias of ContainerID. +func (ctx *Context) FullID() string { + return ctx.ContainerID +} + +// Name returns the ContainerName without a preceding '/'. +func (ctx *Context) Name() string { + return ctx.ContainerName[1:] +} + +// ImageID returns the ContainerImageID shortened to 12 characters. +func (ctx *Context) ImageID() string { + return ctx.ContainerImageID[:12] +} + +// ImageFullID is an alias of ContainerImageID. +func (ctx *Context) ImageFullID() string { + return ctx.ContainerImageID +} + +// ImageName is an alias of ContainerImageName +func (ctx *Context) ImageName() string { + return ctx.ContainerImageName +} diff --git a/daemon/logger/copier.go b/daemon/logger/copier.go new file mode 100644 index 00000000..436c0a8f --- /dev/null +++ b/daemon/logger/copier.go @@ -0,0 +1,86 @@ +package logger + +import ( + "bufio" + "bytes" + "io" + "sync" + "time" + + "github.com/Sirupsen/logrus" +) + +// Copier can copy logs from specified sources to Logger and attach +// ContainerID and Timestamp. +// Writes are concurrent, so you need implement some sync in your logger +type Copier struct { + // cid is the container id for which we are copying logs + cid string + // srcs is map of name -> reader pairs, for example "stdout", "stderr" + srcs map[string]io.Reader + dst Logger + copyJobs sync.WaitGroup + closed chan struct{} +} + +// NewCopier creates a new Copier +func NewCopier(cid string, srcs map[string]io.Reader, dst Logger) *Copier { + return &Copier{ + cid: cid, + srcs: srcs, + dst: dst, + closed: make(chan struct{}), + } +} + +// Run starts logs copying +func (c *Copier) Run() { + for src, w := range c.srcs { + c.copyJobs.Add(1) + go c.copySrc(src, w) + } +} + +func (c *Copier) copySrc(name string, src io.Reader) { + defer c.copyJobs.Done() + reader := bufio.NewReader(src) + + for { + select { + case <-c.closed: + return + default: + line, err := reader.ReadBytes('\n') + line = bytes.TrimSuffix(line, []byte{'\n'}) + + // ReadBytes can return full or partial output even when it failed. + // e.g. it can return a full entry and EOF. + if err == nil || len(line) > 0 { + if logErr := c.dst.Log(&Message{ContainerID: c.cid, Line: line, Source: name, Timestamp: time.Now().UTC()}); logErr != nil { + logrus.Errorf("Failed to log msg %q for logger %s: %s", line, c.dst.Name(), logErr) + } + } + + if err != nil { + if err != io.EOF { + logrus.Errorf("Error scanning log stream: %s", err) + } + return + } + } + } +} + +// Wait waits until all copying is done +func (c *Copier) Wait() { + c.copyJobs.Wait() +} + +// Close closes the copier +func (c *Copier) Close() { + select { + case <-c.closed: + default: + close(c.closed) + } +} diff --git a/daemon/logger/copier_test.go b/daemon/logger/copier_test.go new file mode 100644 index 00000000..30239f06 --- /dev/null +++ b/daemon/logger/copier_test.go @@ -0,0 +1,132 @@ +package logger + +import ( + "bytes" + "encoding/json" + "io" + "testing" + "time" +) + +type TestLoggerJSON struct { + *json.Encoder + delay time.Duration +} + +func (l *TestLoggerJSON) Log(m *Message) error { + if l.delay > 0 { + time.Sleep(l.delay) + } + return l.Encode(m) +} + +func (l *TestLoggerJSON) Close() error { return nil } + +func (l *TestLoggerJSON) Name() string { return "json" } + +type TestLoggerText struct { + *bytes.Buffer +} + +func (l *TestLoggerText) Log(m *Message) error { + _, err := l.WriteString(m.ContainerID + " " + m.Source + " " + string(m.Line) + "\n") + return err +} + +func (l *TestLoggerText) Close() error { return nil } + +func (l *TestLoggerText) Name() string { return "text" } + +func TestCopier(t *testing.T) { + stdoutLine := "Line that thinks that it is log line from docker stdout" + stderrLine := "Line that thinks that it is log line from docker stderr" + var stdout bytes.Buffer + var stderr bytes.Buffer + for i := 0; i < 30; i++ { + if _, err := stdout.WriteString(stdoutLine + "\n"); err != nil { + t.Fatal(err) + } + if _, err := stderr.WriteString(stderrLine + "\n"); err != nil { + t.Fatal(err) + } + } + + var jsonBuf bytes.Buffer + + jsonLog := &TestLoggerJSON{Encoder: json.NewEncoder(&jsonBuf)} + + cid := "a7317399f3f857173c6179d44823594f8294678dea9999662e5c625b5a1c7657" + c := NewCopier(cid, + map[string]io.Reader{ + "stdout": &stdout, + "stderr": &stderr, + }, + jsonLog) + c.Run() + wait := make(chan struct{}) + go func() { + c.Wait() + close(wait) + }() + select { + case <-time.After(1 * time.Second): + t.Fatal("Copier failed to do its work in 1 second") + case <-wait: + } + dec := json.NewDecoder(&jsonBuf) + for { + var msg Message + if err := dec.Decode(&msg); err != nil { + if err == io.EOF { + break + } + t.Fatal(err) + } + if msg.Source != "stdout" && msg.Source != "stderr" { + t.Fatalf("Wrong Source: %q, should be %q or %q", msg.Source, "stdout", "stderr") + } + if msg.ContainerID != cid { + t.Fatalf("Wrong ContainerID: %q, expected %q", msg.ContainerID, cid) + } + if msg.Source == "stdout" { + if string(msg.Line) != stdoutLine { + t.Fatalf("Wrong Line: %q, expected %q", msg.Line, stdoutLine) + } + } + if msg.Source == "stderr" { + if string(msg.Line) != stderrLine { + t.Fatalf("Wrong Line: %q, expected %q", msg.Line, stderrLine) + } + } + } +} + +func TestCopierSlow(t *testing.T) { + stdoutLine := "Line that thinks that it is log line from docker stdout" + var stdout bytes.Buffer + for i := 0; i < 30; i++ { + if _, err := stdout.WriteString(stdoutLine + "\n"); err != nil { + t.Fatal(err) + } + } + + var jsonBuf bytes.Buffer + //encoder := &encodeCloser{Encoder: json.NewEncoder(&jsonBuf)} + jsonLog := &TestLoggerJSON{Encoder: json.NewEncoder(&jsonBuf), delay: 100 * time.Millisecond} + + cid := "a7317399f3f857173c6179d44823594f8294678dea9999662e5c625b5a1c7657" + c := NewCopier(cid, map[string]io.Reader{"stdout": &stdout}, jsonLog) + c.Run() + wait := make(chan struct{}) + go func() { + c.Wait() + close(wait) + }() + <-time.After(150 * time.Millisecond) + c.Close() + select { + case <-time.After(200 * time.Millisecond): + t.Fatalf("failed to exit in time after the copier is closed") + case <-wait: + } +} diff --git a/daemon/logger/etwlogs/etwlogs_windows.go b/daemon/logger/etwlogs/etwlogs_windows.go new file mode 100644 index 00000000..de128a2e --- /dev/null +++ b/daemon/logger/etwlogs/etwlogs_windows.go @@ -0,0 +1,183 @@ +// Package etwlogs provides a log driver for forwarding container logs +// as ETW events.(ETW stands for Event Tracing for Windows) +// A client can then create an ETW listener to listen for events that are sent +// by the ETW provider that we register, using the provider's GUID "a3693192-9ed6-46d2-a981-f8226c8363bd". +// Here is an example of how to do this using the logman utility: +// 1. logman start -ets DockerContainerLogs -p {a3693192-9ed6-46d2-a981-f8226c8363bd} 0 0 -o trace.etl +// 2. Run container(s) and generate log messages +// 3. logman stop -ets DockerContainerLogs +// 4. You can then convert the etl log file to XML using: tracerpt -y trace.etl +// +// Each container log message generates a ETW event that also contains: +// the container name and ID, the timestamp, and the stream type. +package etwlogs + +import ( + "errors" + "fmt" + "sync" + "syscall" + "unsafe" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/daemon/logger" +) + +type etwLogs struct { + containerName string + imageName string + containerID string + imageID string +} + +const ( + name = "etwlogs" + win32CallSuccess = 0 +) + +var win32Lib *syscall.DLL +var providerHandle syscall.Handle +var refCount int +var mu sync.Mutex + +func init() { + providerHandle = syscall.InvalidHandle + if err := logger.RegisterLogDriver(name, New); err != nil { + logrus.Fatal(err) + } +} + +// New creates a new etwLogs logger for the given container and registers the EWT provider. +func New(ctx logger.Context) (logger.Logger, error) { + if err := registerETWProvider(); err != nil { + return nil, err + } + logrus.Debugf("logging driver etwLogs configured for container: %s.", ctx.ContainerID) + + return &etwLogs{ + containerName: fixContainerName(ctx.ContainerName), + imageName: ctx.ContainerImageName, + containerID: ctx.ContainerID, + imageID: ctx.ContainerImageID, + }, nil +} + +// Log logs the message to the ETW stream. +func (etwLogger *etwLogs) Log(msg *logger.Message) error { + if providerHandle == syscall.InvalidHandle { + // This should never be hit, if it is, it indicates a programming error. + errorMessage := "ETWLogs cannot log the message, because the event provider has not been registered." + logrus.Error(errorMessage) + return errors.New(errorMessage) + } + return callEventWriteString(createLogMessage(etwLogger, msg)) +} + +// Close closes the logger by unregistering the ETW provider. +func (etwLogger *etwLogs) Close() error { + unregisterETWProvider() + return nil +} + +func (etwLogger *etwLogs) Name() string { + return name +} + +func createLogMessage(etwLogger *etwLogs, msg *logger.Message) string { + return fmt.Sprintf("container_name: %s, image_name: %s, container_id: %s, image_id: %s, source: %s, log: %s", + etwLogger.containerName, + etwLogger.imageName, + etwLogger.containerID, + etwLogger.imageID, + msg.Source, + msg.Line) +} + +// fixContainerName removes the initial '/' from the container name. +func fixContainerName(cntName string) string { + if len(cntName) > 0 && cntName[0] == '/' { + cntName = cntName[1:] + } + return cntName +} + +func registerETWProvider() error { + mu.Lock() + defer mu.Unlock() + if refCount == 0 { + var err error + if win32Lib, err = syscall.LoadDLL("Advapi32.dll"); err != nil { + return err + } + if err = callEventRegister(); err != nil { + win32Lib.Release() + win32Lib = nil + return err + } + } + + refCount++ + return nil +} + +func unregisterETWProvider() { + mu.Lock() + defer mu.Unlock() + if refCount == 1 { + if callEventUnregister() { + refCount-- + providerHandle = syscall.InvalidHandle + win32Lib.Release() + win32Lib = nil + } + // Not returning an error if EventUnregister fails, because etwLogs will continue to work + } else { + refCount-- + } +} + +func callEventRegister() error { + proc, err := win32Lib.FindProc("EventRegister") + if err != nil { + return err + } + // The provider's GUID is {a3693192-9ed6-46d2-a981-f8226c8363bd} + guid := syscall.GUID{ + 0xa3693192, 0x9ed6, 0x46d2, + [8]byte{0xa9, 0x81, 0xf8, 0x22, 0x6c, 0x83, 0x63, 0xbd}, + } + + ret, _, _ := proc.Call(uintptr(unsafe.Pointer(&guid)), 0, 0, uintptr(unsafe.Pointer(&providerHandle))) + if ret != win32CallSuccess { + errorMessage := fmt.Sprintf("Failed to register ETW provider. Error: %d", ret) + logrus.Error(errorMessage) + return errors.New(errorMessage) + } + return nil +} + +func callEventWriteString(message string) error { + proc, err := win32Lib.FindProc("EventWriteString") + if err != nil { + return err + } + ret, _, _ := proc.Call(uintptr(providerHandle), 0, 0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(message)))) + if ret != win32CallSuccess { + errorMessage := fmt.Sprintf("ETWLogs provider failed to log message. Error: %d", ret) + logrus.Error(errorMessage) + return errors.New(errorMessage) + } + return nil +} + +func callEventUnregister() bool { + proc, err := win32Lib.FindProc("EventUnregister") + if err != nil { + return false + } + ret, _, _ := proc.Call(uintptr(providerHandle)) + if ret != win32CallSuccess { + return false + } + return true +} diff --git a/daemon/logger/factory.go b/daemon/logger/factory.go new file mode 100644 index 00000000..5ec0f673 --- /dev/null +++ b/daemon/logger/factory.go @@ -0,0 +1,89 @@ +package logger + +import ( + "fmt" + "sync" +) + +// Creator builds a logging driver instance with given context. +type Creator func(Context) (Logger, error) + +// LogOptValidator checks the options specific to the underlying +// logging implementation. +type LogOptValidator func(cfg map[string]string) error + +type logdriverFactory struct { + registry map[string]Creator + optValidator map[string]LogOptValidator + m sync.Mutex +} + +func (lf *logdriverFactory) register(name string, c Creator) error { + lf.m.Lock() + defer lf.m.Unlock() + + if _, ok := lf.registry[name]; ok { + return fmt.Errorf("logger: log driver named '%s' is already registered", name) + } + lf.registry[name] = c + return nil +} + +func (lf *logdriverFactory) registerLogOptValidator(name string, l LogOptValidator) error { + lf.m.Lock() + defer lf.m.Unlock() + + if _, ok := lf.optValidator[name]; ok { + return fmt.Errorf("logger: log validator named '%s' is already registered", name) + } + lf.optValidator[name] = l + return nil +} + +func (lf *logdriverFactory) get(name string) (Creator, error) { + lf.m.Lock() + defer lf.m.Unlock() + + c, ok := lf.registry[name] + if !ok { + return c, fmt.Errorf("logger: no log driver named '%s' is registered", name) + } + return c, nil +} + +func (lf *logdriverFactory) getLogOptValidator(name string) LogOptValidator { + lf.m.Lock() + defer lf.m.Unlock() + + c, _ := lf.optValidator[name] + return c +} + +var factory = &logdriverFactory{registry: make(map[string]Creator), optValidator: make(map[string]LogOptValidator)} // global factory instance + +// RegisterLogDriver registers the given logging driver builder with given logging +// driver name. +func RegisterLogDriver(name string, c Creator) error { + return factory.register(name, c) +} + +// RegisterLogOptValidator registers the logging option validator with +// the given logging driver name. +func RegisterLogOptValidator(name string, l LogOptValidator) error { + return factory.registerLogOptValidator(name, l) +} + +// GetLogDriver provides the logging driver builder for a logging driver name. +func GetLogDriver(name string) (Creator, error) { + return factory.get(name) +} + +// ValidateLogOpts checks the options for the given log driver. The +// options supported are specific to the LogDriver implementation. +func ValidateLogOpts(name string, cfg map[string]string) error { + l := factory.getLogOptValidator(name) + if l != nil { + return l(cfg) + } + return nil +} diff --git a/daemon/logger/fluentd/fluentd.go b/daemon/logger/fluentd/fluentd.go new file mode 100644 index 00000000..d8615ba0 --- /dev/null +++ b/daemon/logger/fluentd/fluentd.go @@ -0,0 +1,201 @@ +// Package fluentd provides the log driver for forwarding server logs +// to fluentd endpoints. +package fluentd + +import ( + "fmt" + "math" + "net" + "strconv" + "strings" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/daemon/logger" + "github.com/docker/docker/daemon/logger/loggerutils" + "github.com/docker/go-units" + "github.com/fluent/fluent-logger-golang/fluent" +) + +type fluentd struct { + tag string + containerID string + containerName string + writer *fluent.Fluent + extra map[string]string +} + +const ( + name = "fluentd" + + defaultHost = "127.0.0.1" + defaultPort = 24224 + defaultBufferLimit = 1024 * 1024 + defaultTagPrefix = "docker" + + // logger tries to reconnect 2**32 - 1 times + // failed (and panic) after 204 years [ 1.5 ** (2**32 - 1) - 1 seconds] + defaultRetryWait = 1000 + defaultTimeout = 3 * time.Second + defaultMaxRetries = math.MaxInt32 + defaultReconnectWaitIncreRate = 1.5 + + addressKey = "fluentd-address" + bufferLimitKey = "fluentd-buffer-limit" + retryWaitKey = "fluentd-retry-wait" + maxRetriesKey = "fluentd-max-retries" + asyncConnectKey = "fluentd-async-connect" +) + +func init() { + if err := logger.RegisterLogDriver(name, New); err != nil { + logrus.Fatal(err) + } + if err := logger.RegisterLogOptValidator(name, ValidateLogOpt); err != nil { + logrus.Fatal(err) + } +} + +// New creates a fluentd logger using the configuration passed in on +// the context. Supported context configuration variables are +// fluentd-address & fluentd-tag. +func New(ctx logger.Context) (logger.Logger, error) { + host, port, err := parseAddress(ctx.Config[addressKey]) + if err != nil { + return nil, err + } + + tag, err := loggerutils.ParseLogTag(ctx, "docker.{{.ID}}") + if err != nil { + return nil, err + } + + extra := ctx.ExtraAttributes(nil) + + bufferLimit := defaultBufferLimit + if ctx.Config[bufferLimitKey] != "" { + bl64, err := units.RAMInBytes(ctx.Config[bufferLimitKey]) + if err != nil { + return nil, err + } + bufferLimit = int(bl64) + } + + retryWait := defaultRetryWait + if ctx.Config[retryWaitKey] != "" { + rwd, err := time.ParseDuration(ctx.Config[retryWaitKey]) + if err != nil { + return nil, err + } + retryWait = int(rwd.Seconds() * 1000) + } + + maxRetries := defaultMaxRetries + if ctx.Config[maxRetriesKey] != "" { + mr64, err := strconv.ParseUint(ctx.Config[maxRetriesKey], 10, strconv.IntSize) + if err != nil { + return nil, err + } + maxRetries = int(mr64) + } + + asyncConnect := false + if ctx.Config[asyncConnectKey] != "" { + if asyncConnect, err = strconv.ParseBool(ctx.Config[asyncConnectKey]); err != nil { + return nil, err + } + } + + fluentConfig := fluent.Config{ + FluentPort: port, + FluentHost: host, + BufferLimit: bufferLimit, + RetryWait: retryWait, + MaxRetry: maxRetries, + AsyncConnect: asyncConnect, + } + + logrus.WithField("container", ctx.ContainerID).WithField("config", fluentConfig). + Debug("logging driver fluentd configured") + + log, err := fluent.New(fluentConfig) + if err != nil { + return nil, err + } + return &fluentd{ + tag: tag, + containerID: ctx.ContainerID, + containerName: ctx.ContainerName, + writer: log, + extra: extra, + }, nil +} + +func (f *fluentd) Log(msg *logger.Message) error { + data := map[string]string{ + "container_id": f.containerID, + "container_name": f.containerName, + "source": msg.Source, + "log": string(msg.Line), + } + for k, v := range f.extra { + data[k] = v + } + // fluent-logger-golang buffers logs from failures and disconnections, + // and these are transferred again automatically. + return f.writer.PostWithTime(f.tag, msg.Timestamp, data) +} + +func (f *fluentd) Close() error { + return f.writer.Close() +} + +func (f *fluentd) Name() string { + return name +} + +// ValidateLogOpt looks for fluentd specific log options fluentd-address & fluentd-tag. +func ValidateLogOpt(cfg map[string]string) error { + for key := range cfg { + switch key { + case "env": + case "fluentd-tag": + case "labels": + case "tag": + case addressKey: + case bufferLimitKey: + case retryWaitKey: + case maxRetriesKey: + case asyncConnectKey: + // Accepted + default: + return fmt.Errorf("unknown log opt '%s' for fluentd log driver", key) + } + } + + if _, _, err := parseAddress(cfg["fluentd-address"]); err != nil { + return err + } + + return nil +} + +func parseAddress(address string) (string, int, error) { + if address == "" { + return defaultHost, defaultPort, nil + } + + host, port, err := net.SplitHostPort(address) + if err != nil { + if !strings.Contains(err.Error(), "missing port in address") { + return "", 0, fmt.Errorf("invalid fluentd-address %s: %s", address, err) + } + return host, defaultPort, nil + } + + portnum, err := strconv.Atoi(port) + if err != nil { + return "", 0, fmt.Errorf("invalid fluentd-address %s: %s", address, err) + } + return host, portnum, nil +} diff --git a/daemon/logger/gcplogs/gcplogging.go b/daemon/logger/gcplogs/gcplogging.go new file mode 100644 index 00000000..781642bb --- /dev/null +++ b/daemon/logger/gcplogs/gcplogging.go @@ -0,0 +1,191 @@ +package gcplogs + +import ( + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/docker/docker/daemon/logger" + + "github.com/Sirupsen/logrus" + "golang.org/x/net/context" + "google.golang.org/cloud/compute/metadata" + "google.golang.org/cloud/logging" +) + +const ( + name = "gcplogs" + + projectOptKey = "gcp-project" + logLabelsKey = "labels" + logEnvKey = "env" + logCmdKey = "gcp-log-cmd" +) + +var ( + // The number of logs the gcplogs driver has dropped. + droppedLogs uint64 + + onGCE bool + + // instance metadata populated from the metadata server if available + projectID string + zone string + instanceName string + instanceID string +) + +func init() { + + if err := logger.RegisterLogDriver(name, New); err != nil { + logrus.Fatal(err) + } + + if err := logger.RegisterLogOptValidator(name, ValidateLogOpts); err != nil { + logrus.Fatal(err) + } +} + +type gcplogs struct { + client *logging.Client + instance *instanceInfo + container *containerInfo +} + +type dockerLogEntry struct { + Instance *instanceInfo `json:"instance,omitempty"` + Container *containerInfo `json:"container,omitempty"` + Data string `json:"data,omitempty"` +} + +type instanceInfo struct { + Zone string `json:"zone,omitempty"` + Name string `json:"name,omitempty"` + ID string `json:"id,omitempty"` +} + +type containerInfo struct { + Name string `json:"name,omitempty"` + ID string `json:"id,omitempty"` + ImageName string `json:"imageName,omitempty"` + ImageID string `json:"imageId,omitempty"` + Created time.Time `json:"created,omitempty"` + Command string `json:"command,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +var initGCPOnce sync.Once + +func initGCP() { + initGCPOnce.Do(func() { + onGCE = metadata.OnGCE() + if onGCE { + // These will fail on instances if the metadata service is + // down or the client is compiled with an API version that + // has been removed. Since these are not vital, let's ignore + // them and make their fields in the dockeLogEntry ,omitempty + projectID, _ = metadata.ProjectID() + zone, _ = metadata.Zone() + instanceName, _ = metadata.InstanceName() + instanceID, _ = metadata.InstanceID() + } + }) +} + +// New creates a new logger that logs to Google Cloud Logging using the application +// default credentials. +// +// See https://developers.google.com/identity/protocols/application-default-credentials +func New(ctx logger.Context) (logger.Logger, error) { + initGCP() + + var project string + if projectID != "" { + project = projectID + } + if projectID, found := ctx.Config[projectOptKey]; found { + project = projectID + } + if project == "" { + return nil, fmt.Errorf("No project was specified and couldn't read project from the meatadata server. Please specify a project") + } + + c, err := logging.NewClient(context.Background(), project, "gcplogs-docker-driver") + if err != nil { + return nil, err + } + + if err := c.Ping(); err != nil { + return nil, fmt.Errorf("unable to connect or authenticate with Google Cloud Logging: %v", err) + } + + l := &gcplogs{ + client: c, + container: &containerInfo{ + Name: ctx.ContainerName, + ID: ctx.ContainerID, + ImageName: ctx.ContainerImageName, + ImageID: ctx.ContainerImageID, + Created: ctx.ContainerCreated, + Metadata: ctx.ExtraAttributes(nil), + }, + } + + if ctx.Config[logCmdKey] == "true" { + l.container.Command = ctx.Command() + } + + if onGCE { + l.instance = &instanceInfo{ + Zone: zone, + Name: instanceName, + ID: instanceID, + } + } + + // The logger "overflows" at a rate of 10,000 logs per second and this + // overflow func is called. We want to surface the error to the user + // without overly spamming /var/log/docker.log so we log the first time + // we overflow and every 1000th time after. + c.Overflow = func(_ *logging.Client, _ logging.Entry) error { + if i := atomic.AddUint64(&droppedLogs, 1); i%1000 == 1 { + logrus.Errorf("gcplogs driver has dropped %v logs", i) + } + return nil + } + + return l, nil +} + +// ValidateLogOpts validates the opts passed to the gcplogs driver. Currently, the gcplogs +// driver doesn't take any arguments. +func ValidateLogOpts(cfg map[string]string) error { + for k := range cfg { + switch k { + case projectOptKey, logLabelsKey, logEnvKey, logCmdKey: + default: + return fmt.Errorf("%q is not a valid option for the gcplogs driver", k) + } + } + return nil +} + +func (l *gcplogs) Log(m *logger.Message) error { + return l.client.Log(logging.Entry{ + Time: m.Timestamp, + Payload: &dockerLogEntry{ + Instance: l.instance, + Container: l.container, + Data: string(m.Line), + }, + }) +} + +func (l *gcplogs) Close() error { + return l.client.Flush() +} + +func (l *gcplogs) Name() string { + return name +} diff --git a/daemon/logger/gelf/gelf.go b/daemon/logger/gelf/gelf.go new file mode 100644 index 00000000..3fe62c41 --- /dev/null +++ b/daemon/logger/gelf/gelf.go @@ -0,0 +1,212 @@ +// +build linux + +// Package gelf provides the log driver for forwarding server logs to +// endpoints that support the Graylog Extended Log Format. +package gelf + +import ( + "bytes" + "compress/flate" + "encoding/json" + "fmt" + "net" + "net/url" + "strconv" + "time" + + "github.com/Graylog2/go-gelf/gelf" + "github.com/Sirupsen/logrus" + "github.com/docker/docker/daemon/logger" + "github.com/docker/docker/daemon/logger/loggerutils" + "github.com/docker/docker/pkg/urlutil" +) + +const name = "gelf" + +type gelfLogger struct { + writer *gelf.Writer + ctx logger.Context + hostname string + rawExtra json.RawMessage +} + +func init() { + if err := logger.RegisterLogDriver(name, New); err != nil { + logrus.Fatal(err) + } + if err := logger.RegisterLogOptValidator(name, ValidateLogOpt); err != nil { + logrus.Fatal(err) + } +} + +// New creates a gelf logger using the configuration passed in on the +// context. Supported context configuration variables are +// gelf-address, & gelf-tag. +func New(ctx logger.Context) (logger.Logger, error) { + // parse gelf address + address, err := parseAddress(ctx.Config["gelf-address"]) + if err != nil { + return nil, err + } + + // collect extra data for GELF message + hostname, err := ctx.Hostname() + if err != nil { + return nil, fmt.Errorf("gelf: cannot access hostname to set source field") + } + + // remove trailing slash from container name + containerName := bytes.TrimLeft([]byte(ctx.ContainerName), "/") + + // parse log tag + tag, err := loggerutils.ParseLogTag(ctx, "") + if err != nil { + return nil, err + } + + extra := map[string]interface{}{ + "_container_id": ctx.ContainerID, + "_container_name": string(containerName), + "_image_id": ctx.ContainerImageID, + "_image_name": ctx.ContainerImageName, + "_command": ctx.Command(), + "_tag": tag, + "_created": ctx.ContainerCreated, + } + + extraAttrs := ctx.ExtraAttributes(func(key string) string { + if key[0] == '_' { + return key + } + return "_" + key + }) + for k, v := range extraAttrs { + extra[k] = v + } + + rawExtra, err := json.Marshal(extra) + if err != nil { + return nil, err + } + + // create new gelfWriter + gelfWriter, err := gelf.NewWriter(address) + if err != nil { + return nil, fmt.Errorf("gelf: cannot connect to GELF endpoint: %s %v", address, err) + } + + if v, ok := ctx.Config["gelf-compression-type"]; ok { + switch v { + case "gzip": + gelfWriter.CompressionType = gelf.CompressGzip + case "zlib": + gelfWriter.CompressionType = gelf.CompressZlib + case "none": + gelfWriter.CompressionType = gelf.CompressNone + default: + return nil, fmt.Errorf("gelf: invalid compression type %q", v) + } + } + + if v, ok := ctx.Config["gelf-compression-level"]; ok { + val, err := strconv.Atoi(v) + if err != nil { + return nil, fmt.Errorf("gelf: invalid compression level %s, err %v", v, err) + } + gelfWriter.CompressionLevel = val + } + + return &gelfLogger{ + writer: gelfWriter, + ctx: ctx, + hostname: hostname, + rawExtra: rawExtra, + }, nil +} + +func (s *gelfLogger) Log(msg *logger.Message) error { + level := gelf.LOG_INFO + if msg.Source == "stderr" { + level = gelf.LOG_ERR + } + + m := gelf.Message{ + Version: "1.1", + Host: s.hostname, + Short: string(msg.Line), + TimeUnix: float64(msg.Timestamp.UnixNano()/int64(time.Millisecond)) / 1000.0, + Level: level, + RawExtra: s.rawExtra, + } + + if err := s.writer.WriteMessage(&m); err != nil { + return fmt.Errorf("gelf: cannot send GELF message: %v", err) + } + return nil +} + +func (s *gelfLogger) Close() error { + return s.writer.Close() +} + +func (s *gelfLogger) Name() string { + return name +} + +// ValidateLogOpt looks for gelf specific log options gelf-address, & +// gelf-tag. +func ValidateLogOpt(cfg map[string]string) error { + for key, val := range cfg { + switch key { + case "gelf-address": + case "gelf-tag": + case "tag": + case "labels": + case "env": + case "gelf-compression-level": + i, err := strconv.Atoi(val) + if err != nil || i < flate.DefaultCompression || i > flate.BestCompression { + return fmt.Errorf("unknown value %q for log opt %q for gelf log driver", val, key) + } + case "gelf-compression-type": + switch val { + case "gzip", "zlib", "none": + default: + return fmt.Errorf("unknown value %q for log opt %q for gelf log driver", val, key) + } + default: + return fmt.Errorf("unknown log opt %q for gelf log driver", key) + } + } + + if _, err := parseAddress(cfg["gelf-address"]); err != nil { + return err + } + + return nil +} + +func parseAddress(address string) (string, error) { + if address == "" { + return "", nil + } + if !urlutil.IsTransportURL(address) { + return "", fmt.Errorf("gelf-address should be in form proto://address, got %v", address) + } + url, err := url.Parse(address) + if err != nil { + return "", err + } + + // we support only udp + if url.Scheme != "udp" { + return "", fmt.Errorf("gelf: endpoint needs to be UDP") + } + + // get host and port + if _, _, err = net.SplitHostPort(url.Host); err != nil { + return "", fmt.Errorf("gelf: please provide gelf-address as udp://host:port") + } + + return url.Host, nil +} diff --git a/daemon/logger/gelf/gelf_unsupported.go b/daemon/logger/gelf/gelf_unsupported.go new file mode 100644 index 00000000..266f73b1 --- /dev/null +++ b/daemon/logger/gelf/gelf_unsupported.go @@ -0,0 +1,3 @@ +// +build !linux + +package gelf diff --git a/daemon/logger/journald/journald.go b/daemon/logger/journald/journald.go new file mode 100644 index 00000000..748dd8b2 --- /dev/null +++ b/daemon/logger/journald/journald.go @@ -0,0 +1,95 @@ +// +build linux + +// Package journald provides the log driver for forwarding server logs +// to endpoints that receive the systemd format. +package journald + +import ( + "fmt" + "strings" + "sync" + + "github.com/Sirupsen/logrus" + "github.com/coreos/go-systemd/journal" + "github.com/docker/docker/daemon/logger" + "github.com/docker/docker/daemon/logger/loggerutils" +) + +const name = "journald" + +type journald struct { + vars map[string]string // additional variables and values to send to the journal along with the log message + readers readerList +} + +type readerList struct { + mu sync.Mutex + readers map[*logger.LogWatcher]*logger.LogWatcher +} + +func init() { + if err := logger.RegisterLogDriver(name, New); err != nil { + logrus.Fatal(err) + } + if err := logger.RegisterLogOptValidator(name, validateLogOpt); err != nil { + logrus.Fatal(err) + } +} + +// New creates a journald logger using the configuration passed in on +// the context. +func New(ctx logger.Context) (logger.Logger, error) { + if !journal.Enabled() { + return nil, fmt.Errorf("journald is not enabled on this host") + } + // Strip a leading slash so that people can search for + // CONTAINER_NAME=foo rather than CONTAINER_NAME=/foo. + name := ctx.ContainerName + if name[0] == '/' { + name = name[1:] + } + + // parse log tag + tag, err := loggerutils.ParseLogTag(ctx, "") + if err != nil { + return nil, err + } + + vars := map[string]string{ + "CONTAINER_ID": ctx.ContainerID[:12], + "CONTAINER_ID_FULL": ctx.ContainerID, + "CONTAINER_NAME": name, + "CONTAINER_TAG": tag, + } + extraAttrs := ctx.ExtraAttributes(strings.ToTitle) + for k, v := range extraAttrs { + vars[k] = v + } + return &journald{vars: vars, readers: readerList{readers: make(map[*logger.LogWatcher]*logger.LogWatcher)}}, nil +} + +// We don't actually accept any options, but we have to supply a callback for +// the factory to pass the (probably empty) configuration map to. +func validateLogOpt(cfg map[string]string) error { + for key := range cfg { + switch key { + case "labels": + case "env": + case "tag": + default: + return fmt.Errorf("unknown log opt '%s' for journald log driver", key) + } + } + return nil +} + +func (s *journald) Log(msg *logger.Message) error { + if msg.Source == "stderr" { + return journal.Send(string(msg.Line), journal.PriErr, s.vars) + } + return journal.Send(string(msg.Line), journal.PriInfo, s.vars) +} + +func (s *journald) Name() string { + return name +} diff --git a/daemon/logger/journald/journald_unsupported.go b/daemon/logger/journald/journald_unsupported.go new file mode 100644 index 00000000..d52ca92e --- /dev/null +++ b/daemon/logger/journald/journald_unsupported.go @@ -0,0 +1,6 @@ +// +build !linux + +package journald + +type journald struct { +} diff --git a/daemon/logger/journald/read.go b/daemon/logger/journald/read.go new file mode 100644 index 00000000..1b8b8cce --- /dev/null +++ b/daemon/logger/journald/read.go @@ -0,0 +1,330 @@ +// +build linux,cgo,!static_build,journald + +package journald + +// #include +// #include +// #include +// #include +// #include +// #include +// #include +// #include +// #include +// +//static int get_message(sd_journal *j, const char **msg, size_t *length) +//{ +// int rc; +// *msg = NULL; +// *length = 0; +// rc = sd_journal_get_data(j, "MESSAGE", (const void **) msg, length); +// if (rc == 0) { +// if (*length > 8) { +// (*msg) += 8; +// *length -= 8; +// } else { +// *msg = NULL; +// *length = 0; +// rc = -ENOENT; +// } +// } +// return rc; +//} +//static int get_priority(sd_journal *j, int *priority) +//{ +// const void *data; +// size_t i, length; +// int rc; +// *priority = -1; +// rc = sd_journal_get_data(j, "PRIORITY", &data, &length); +// if (rc == 0) { +// if ((length > 9) && (strncmp(data, "PRIORITY=", 9) == 0)) { +// *priority = 0; +// for (i = 9; i < length; i++) { +// *priority = *priority * 10 + ((const char *)data)[i] - '0'; +// } +// if (length > 9) { +// rc = 0; +// } +// } +// } +// return rc; +//} +//static int wait_for_data_or_close(sd_journal *j, int pipefd) +//{ +// struct pollfd fds[2]; +// uint64_t when = 0; +// int timeout, jevents, i; +// struct timespec ts; +// uint64_t now; +// do { +// memset(&fds, 0, sizeof(fds)); +// fds[0].fd = pipefd; +// fds[0].events = POLLHUP; +// fds[1].fd = sd_journal_get_fd(j); +// if (fds[1].fd < 0) { +// return fds[1].fd; +// } +// jevents = sd_journal_get_events(j); +// if (jevents < 0) { +// return jevents; +// } +// fds[1].events = jevents; +// sd_journal_get_timeout(j, &when); +// if (when == -1) { +// timeout = -1; +// } else { +// clock_gettime(CLOCK_MONOTONIC, &ts); +// now = (uint64_t) ts.tv_sec * 1000000 + ts.tv_nsec / 1000; +// timeout = when > now ? (int) ((when - now + 999) / 1000) : 0; +// } +// i = poll(fds, 2, timeout); +// if ((i == -1) && (errno != EINTR)) { +// /* An unexpected error. */ +// return (errno != 0) ? -errno : -EINTR; +// } +// if (fds[0].revents & POLLHUP) { +// /* The close notification pipe was closed. */ +// return 0; +// } +// if (sd_journal_process(j) == SD_JOURNAL_APPEND) { +// /* Data, which we might care about, was appended. */ +// return 1; +// } +// } while ((fds[0].revents & POLLHUP) == 0); +// return 0; +//} +import "C" + +import ( + "fmt" + "time" + "unsafe" + + "github.com/Sirupsen/logrus" + "github.com/coreos/go-systemd/journal" + "github.com/docker/docker/daemon/logger" +) + +func (s *journald) Close() error { + s.readers.mu.Lock() + for reader := range s.readers.readers { + reader.Close() + } + s.readers.mu.Unlock() + return nil +} + +func (s *journald) drainJournal(logWatcher *logger.LogWatcher, config logger.ReadConfig, j *C.sd_journal, oldCursor string) string { + var msg, cursor *C.char + var length C.size_t + var stamp C.uint64_t + var priority C.int + + // Walk the journal from here forward until we run out of new entries. +drain: + for { + // Try not to send a given entry twice. + if oldCursor != "" { + ccursor := C.CString(oldCursor) + defer C.free(unsafe.Pointer(ccursor)) + for C.sd_journal_test_cursor(j, ccursor) > 0 { + if C.sd_journal_next(j) <= 0 { + break drain + } + } + } + // Read and send the logged message, if there is one to read. + i := C.get_message(j, &msg, &length) + if i != -C.ENOENT && i != -C.EADDRNOTAVAIL { + // Read the entry's timestamp. + if C.sd_journal_get_realtime_usec(j, &stamp) != 0 { + break + } + // Set up the time and text of the entry. + timestamp := time.Unix(int64(stamp)/1000000, (int64(stamp)%1000000)*1000) + line := append(C.GoBytes(unsafe.Pointer(msg), C.int(length)), "\n"...) + // Recover the stream name by mapping + // from the journal priority back to + // the stream that we would have + // assigned that value. + source := "" + if C.get_priority(j, &priority) != 0 { + source = "" + } else if priority == C.int(journal.PriErr) { + source = "stderr" + } else if priority == C.int(journal.PriInfo) { + source = "stdout" + } + // Send the log message. + cid := s.vars["CONTAINER_ID_FULL"] + logWatcher.Msg <- &logger.Message{ContainerID: cid, Line: line, Source: source, Timestamp: timestamp} + } + // If we're at the end of the journal, we're done (for now). + if C.sd_journal_next(j) <= 0 { + break + } + } + retCursor := "" + if C.sd_journal_get_cursor(j, &cursor) == 0 { + retCursor = C.GoString(cursor) + C.free(unsafe.Pointer(cursor)) + } + return retCursor +} + +func (s *journald) followJournal(logWatcher *logger.LogWatcher, config logger.ReadConfig, j *C.sd_journal, pfd [2]C.int, cursor string) { + s.readers.mu.Lock() + s.readers.readers[logWatcher] = logWatcher + s.readers.mu.Unlock() + go func() { + // Keep copying journal data out until we're notified to stop + // or we hit an error. + status := C.wait_for_data_or_close(j, pfd[0]) + for status == 1 { + cursor = s.drainJournal(logWatcher, config, j, cursor) + status = C.wait_for_data_or_close(j, pfd[0]) + } + if status < 0 { + cerrstr := C.strerror(C.int(-status)) + errstr := C.GoString(cerrstr) + fmtstr := "error %q while attempting to follow journal for container %q" + logrus.Errorf(fmtstr, errstr, s.vars["CONTAINER_ID_FULL"]) + } + // Clean up. + C.close(pfd[0]) + s.readers.mu.Lock() + delete(s.readers.readers, logWatcher) + s.readers.mu.Unlock() + C.sd_journal_close(j) + close(logWatcher.Msg) + }() + // Wait until we're told to stop. + select { + case <-logWatcher.WatchClose(): + // Notify the other goroutine that its work is done. + C.close(pfd[1]) + } +} + +func (s *journald) readLogs(logWatcher *logger.LogWatcher, config logger.ReadConfig) { + var j *C.sd_journal + var cmatch *C.char + var stamp C.uint64_t + var sinceUnixMicro uint64 + var pipes [2]C.int + cursor := "" + + // Get a handle to the journal. + rc := C.sd_journal_open(&j, C.int(0)) + if rc != 0 { + logWatcher.Err <- fmt.Errorf("error opening journal") + close(logWatcher.Msg) + return + } + // If we end up following the log, we can set the journal context + // pointer and the channel pointer to nil so that we won't close them + // here, potentially while the goroutine that uses them is still + // running. Otherwise, close them when we return from this function. + following := false + defer func(pfollowing *bool) { + if !*pfollowing { + C.sd_journal_close(j) + close(logWatcher.Msg) + } + }(&following) + // Remove limits on the size of data items that we'll retrieve. + rc = C.sd_journal_set_data_threshold(j, C.size_t(0)) + if rc != 0 { + logWatcher.Err <- fmt.Errorf("error setting journal data threshold") + return + } + // Add a match to have the library do the searching for us. + cmatch = C.CString("CONTAINER_ID_FULL=" + s.vars["CONTAINER_ID_FULL"]) + defer C.free(unsafe.Pointer(cmatch)) + rc = C.sd_journal_add_match(j, unsafe.Pointer(cmatch), C.strlen(cmatch)) + if rc != 0 { + logWatcher.Err <- fmt.Errorf("error setting journal match") + return + } + // If we have a cutoff time, convert it to Unix time once. + if !config.Since.IsZero() { + nano := config.Since.UnixNano() + sinceUnixMicro = uint64(nano / 1000) + } + if config.Tail > 0 { + lines := config.Tail + // Start at the end of the journal. + if C.sd_journal_seek_tail(j) < 0 { + logWatcher.Err <- fmt.Errorf("error seeking to end of journal") + return + } + if C.sd_journal_previous(j) < 0 { + logWatcher.Err <- fmt.Errorf("error backtracking to previous journal entry") + return + } + // Walk backward. + for lines > 0 { + // Stop if the entry time is before our cutoff. + // We'll need the entry time if it isn't, so go + // ahead and parse it now. + if C.sd_journal_get_realtime_usec(j, &stamp) != 0 { + break + } else { + // Compare the timestamp on the entry + // to our threshold value. + if sinceUnixMicro != 0 && sinceUnixMicro > uint64(stamp) { + break + } + } + lines-- + // If we're at the start of the journal, or + // don't need to back up past any more entries, + // stop. + if lines == 0 || C.sd_journal_previous(j) <= 0 { + break + } + } + } else { + // Start at the beginning of the journal. + if C.sd_journal_seek_head(j) < 0 { + logWatcher.Err <- fmt.Errorf("error seeking to start of journal") + return + } + // If we have a cutoff date, fast-forward to it. + if sinceUnixMicro != 0 && C.sd_journal_seek_realtime_usec(j, C.uint64_t(sinceUnixMicro)) != 0 { + logWatcher.Err <- fmt.Errorf("error seeking to start time in journal") + return + } + if C.sd_journal_next(j) < 0 { + logWatcher.Err <- fmt.Errorf("error skipping to next journal entry") + return + } + } + cursor = s.drainJournal(logWatcher, config, j, "") + if config.Follow { + // Allocate a descriptor for following the journal, if we'll + // need one. Do it here so that we can report if it fails. + if fd := C.sd_journal_get_fd(j); fd < C.int(0) { + logWatcher.Err <- fmt.Errorf("error opening journald follow descriptor: %q", C.GoString(C.strerror(-fd))) + } else { + // Create a pipe that we can poll at the same time as + // the journald descriptor. + if C.pipe(&pipes[0]) == C.int(-1) { + logWatcher.Err <- fmt.Errorf("error opening journald close notification pipe") + } else { + s.followJournal(logWatcher, config, j, pipes, cursor) + // Let followJournal handle freeing the journal context + // object and closing the channel. + following = true + } + } + } + return +} + +func (s *journald) ReadLogs(config logger.ReadConfig) *logger.LogWatcher { + logWatcher := logger.NewLogWatcher() + go s.readLogs(logWatcher, config) + return logWatcher +} diff --git a/daemon/logger/journald/read_native.go b/daemon/logger/journald/read_native.go new file mode 100644 index 00000000..bba6de55 --- /dev/null +++ b/daemon/logger/journald/read_native.go @@ -0,0 +1,6 @@ +// +build linux,cgo,!static_build,journald,!journald_compat + +package journald + +// #cgo pkg-config: libsystemd +import "C" diff --git a/daemon/logger/journald/read_native_compat.go b/daemon/logger/journald/read_native_compat.go new file mode 100644 index 00000000..3f7a43c5 --- /dev/null +++ b/daemon/logger/journald/read_native_compat.go @@ -0,0 +1,6 @@ +// +build linux,cgo,!static_build,journald,journald_compat + +package journald + +// #cgo pkg-config: libsystemd-journal +import "C" diff --git a/daemon/logger/journald/read_unsupported.go b/daemon/logger/journald/read_unsupported.go new file mode 100644 index 00000000..b43abdca --- /dev/null +++ b/daemon/logger/journald/read_unsupported.go @@ -0,0 +1,7 @@ +// +build !linux !cgo static_build !journald + +package journald + +func (s *journald) Close() error { + return nil +} diff --git a/daemon/logger/jsonfilelog/jsonfilelog.go b/daemon/logger/jsonfilelog/jsonfilelog.go new file mode 100644 index 00000000..9faa4e02 --- /dev/null +++ b/daemon/logger/jsonfilelog/jsonfilelog.go @@ -0,0 +1,147 @@ +// Package jsonfilelog provides the default Logger implementation for +// Docker logging. This logger logs to files on the host server in the +// JSON format. +package jsonfilelog + +import ( + "bytes" + "encoding/json" + "fmt" + "strconv" + "sync" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/daemon/logger" + "github.com/docker/docker/daemon/logger/loggerutils" + "github.com/docker/docker/pkg/jsonlog" + "github.com/docker/go-units" +) + +// Name is the name of the file that the jsonlogger logs to. +const Name = "json-file" + +// JSONFileLogger is Logger implementation for default Docker logging. +type JSONFileLogger struct { + buf *bytes.Buffer + writer *loggerutils.RotateFileWriter + mu sync.Mutex + readers map[*logger.LogWatcher]struct{} // stores the active log followers + extra []byte // json-encoded extra attributes +} + +func init() { + if err := logger.RegisterLogDriver(Name, New); err != nil { + logrus.Fatal(err) + } + if err := logger.RegisterLogOptValidator(Name, ValidateLogOpt); err != nil { + logrus.Fatal(err) + } +} + +// New creates new JSONFileLogger which writes to filename passed in +// on given context. +func New(ctx logger.Context) (logger.Logger, error) { + var capval int64 = -1 + if capacity, ok := ctx.Config["max-size"]; ok { + var err error + capval, err = units.FromHumanSize(capacity) + if err != nil { + return nil, err + } + } + var maxFiles = 1 + if maxFileString, ok := ctx.Config["max-file"]; ok { + var err error + maxFiles, err = strconv.Atoi(maxFileString) + if err != nil { + return nil, err + } + if maxFiles < 1 { + return nil, fmt.Errorf("max-file cannot be less than 1") + } + } + + writer, err := loggerutils.NewRotateFileWriter(ctx.LogPath, capval, maxFiles) + if err != nil { + return nil, err + } + + var extra []byte + if attrs := ctx.ExtraAttributes(nil); len(attrs) > 0 { + var err error + extra, err = json.Marshal(attrs) + if err != nil { + return nil, err + } + } + + return &JSONFileLogger{ + buf: bytes.NewBuffer(nil), + writer: writer, + readers: make(map[*logger.LogWatcher]struct{}), + extra: extra, + }, nil +} + +// Log converts logger.Message to jsonlog.JSONLog and serializes it to file. +func (l *JSONFileLogger) Log(msg *logger.Message) error { + timestamp, err := jsonlog.FastTimeMarshalJSON(msg.Timestamp) + if err != nil { + return err + } + l.mu.Lock() + err = (&jsonlog.JSONLogs{ + Log: append(msg.Line, '\n'), + Stream: msg.Source, + Created: timestamp, + RawAttrs: l.extra, + }).MarshalJSONBuf(l.buf) + if err != nil { + l.mu.Unlock() + return err + } + + l.buf.WriteByte('\n') + _, err = l.writer.Write(l.buf.Bytes()) + l.buf.Reset() + l.mu.Unlock() + + return err +} + +// ValidateLogOpt looks for json specific log options max-file & max-size. +func ValidateLogOpt(cfg map[string]string) error { + for key := range cfg { + switch key { + case "max-file": + case "max-size": + case "labels": + case "env": + default: + return fmt.Errorf("unknown log opt '%s' for json-file log driver", key) + } + } + return nil +} + +// LogPath returns the location the given json logger logs to. +func (l *JSONFileLogger) LogPath() string { + return l.writer.LogPath() +} + +// Close closes underlying file and signals all readers to stop. +func (l *JSONFileLogger) Close() error { + l.mu.Lock() + err := l.writer.Close() + for r := range l.readers { + r.Close() + delete(l.readers, r) + } + l.mu.Unlock() + return err +} + +// Name returns name of this logger. +func (l *JSONFileLogger) Name() string { + return Name +} diff --git a/daemon/logger/jsonfilelog/jsonfilelog_test.go b/daemon/logger/jsonfilelog/jsonfilelog_test.go new file mode 100644 index 00000000..ef840531 --- /dev/null +++ b/daemon/logger/jsonfilelog/jsonfilelog_test.go @@ -0,0 +1,248 @@ +package jsonfilelog + +import ( + "encoding/json" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "strconv" + "testing" + "time" + + "github.com/docker/docker/daemon/logger" + "github.com/docker/docker/pkg/jsonlog" +) + +func TestJSONFileLogger(t *testing.T) { + cid := "a7317399f3f857173c6179d44823594f8294678dea9999662e5c625b5a1c7657" + tmp, err := ioutil.TempDir("", "docker-logger-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + filename := filepath.Join(tmp, "container.log") + l, err := New(logger.Context{ + ContainerID: cid, + LogPath: filename, + }) + if err != nil { + t.Fatal(err) + } + defer l.Close() + + if err := l.Log(&logger.Message{ContainerID: cid, Line: []byte("line1"), Source: "src1"}); err != nil { + t.Fatal(err) + } + if err := l.Log(&logger.Message{ContainerID: cid, Line: []byte("line2"), Source: "src2"}); err != nil { + t.Fatal(err) + } + if err := l.Log(&logger.Message{ContainerID: cid, Line: []byte("line3"), Source: "src3"}); err != nil { + t.Fatal(err) + } + res, err := ioutil.ReadFile(filename) + if err != nil { + t.Fatal(err) + } + expected := `{"log":"line1\n","stream":"src1","time":"0001-01-01T00:00:00Z"} +{"log":"line2\n","stream":"src2","time":"0001-01-01T00:00:00Z"} +{"log":"line3\n","stream":"src3","time":"0001-01-01T00:00:00Z"} +` + + if string(res) != expected { + t.Fatalf("Wrong log content: %q, expected %q", res, expected) + } +} + +func BenchmarkJSONFileLogger(b *testing.B) { + cid := "a7317399f3f857173c6179d44823594f8294678dea9999662e5c625b5a1c7657" + tmp, err := ioutil.TempDir("", "docker-logger-") + if err != nil { + b.Fatal(err) + } + defer os.RemoveAll(tmp) + filename := filepath.Join(tmp, "container.log") + l, err := New(logger.Context{ + ContainerID: cid, + LogPath: filename, + }) + if err != nil { + b.Fatal(err) + } + defer l.Close() + + testLine := "Line that thinks that it is log line from docker\n" + msg := &logger.Message{ContainerID: cid, Line: []byte(testLine), Source: "stderr", Timestamp: time.Now().UTC()} + jsonlog, err := (&jsonlog.JSONLog{Log: string(msg.Line) + "\n", Stream: msg.Source, Created: msg.Timestamp}).MarshalJSON() + if err != nil { + b.Fatal(err) + } + b.SetBytes(int64(len(jsonlog)+1) * 30) + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := 0; j < 30; j++ { + if err := l.Log(msg); err != nil { + b.Fatal(err) + } + } + } +} + +func TestJSONFileLoggerWithOpts(t *testing.T) { + cid := "a7317399f3f857173c6179d44823594f8294678dea9999662e5c625b5a1c7657" + tmp, err := ioutil.TempDir("", "docker-logger-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + filename := filepath.Join(tmp, "container.log") + config := map[string]string{"max-file": "2", "max-size": "1k"} + l, err := New(logger.Context{ + ContainerID: cid, + LogPath: filename, + Config: config, + }) + if err != nil { + t.Fatal(err) + } + defer l.Close() + for i := 0; i < 20; i++ { + if err := l.Log(&logger.Message{ContainerID: cid, Line: []byte("line" + strconv.Itoa(i)), Source: "src1"}); err != nil { + t.Fatal(err) + } + } + res, err := ioutil.ReadFile(filename) + if err != nil { + t.Fatal(err) + } + penUlt, err := ioutil.ReadFile(filename + ".1") + if err != nil { + t.Fatal(err) + } + + expectedPenultimate := `{"log":"line0\n","stream":"src1","time":"0001-01-01T00:00:00Z"} +{"log":"line1\n","stream":"src1","time":"0001-01-01T00:00:00Z"} +{"log":"line2\n","stream":"src1","time":"0001-01-01T00:00:00Z"} +{"log":"line3\n","stream":"src1","time":"0001-01-01T00:00:00Z"} +{"log":"line4\n","stream":"src1","time":"0001-01-01T00:00:00Z"} +{"log":"line5\n","stream":"src1","time":"0001-01-01T00:00:00Z"} +{"log":"line6\n","stream":"src1","time":"0001-01-01T00:00:00Z"} +{"log":"line7\n","stream":"src1","time":"0001-01-01T00:00:00Z"} +{"log":"line8\n","stream":"src1","time":"0001-01-01T00:00:00Z"} +{"log":"line9\n","stream":"src1","time":"0001-01-01T00:00:00Z"} +{"log":"line10\n","stream":"src1","time":"0001-01-01T00:00:00Z"} +{"log":"line11\n","stream":"src1","time":"0001-01-01T00:00:00Z"} +{"log":"line12\n","stream":"src1","time":"0001-01-01T00:00:00Z"} +{"log":"line13\n","stream":"src1","time":"0001-01-01T00:00:00Z"} +{"log":"line14\n","stream":"src1","time":"0001-01-01T00:00:00Z"} +{"log":"line15\n","stream":"src1","time":"0001-01-01T00:00:00Z"} +` + expected := `{"log":"line16\n","stream":"src1","time":"0001-01-01T00:00:00Z"} +{"log":"line17\n","stream":"src1","time":"0001-01-01T00:00:00Z"} +{"log":"line18\n","stream":"src1","time":"0001-01-01T00:00:00Z"} +{"log":"line19\n","stream":"src1","time":"0001-01-01T00:00:00Z"} +` + + if string(res) != expected { + t.Fatalf("Wrong log content: %q, expected %q", res, expected) + } + if string(penUlt) != expectedPenultimate { + t.Fatalf("Wrong log content: %q, expected %q", penUlt, expectedPenultimate) + } + +} + +func TestJSONFileLoggerWithLabelsEnv(t *testing.T) { + cid := "a7317399f3f857173c6179d44823594f8294678dea9999662e5c625b5a1c7657" + tmp, err := ioutil.TempDir("", "docker-logger-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + filename := filepath.Join(tmp, "container.log") + config := map[string]string{"labels": "rack,dc", "env": "environ,debug,ssl"} + l, err := New(logger.Context{ + ContainerID: cid, + LogPath: filename, + Config: config, + ContainerLabels: map[string]string{"rack": "101", "dc": "lhr"}, + ContainerEnv: []string{"environ=production", "debug=false", "port=10001", "ssl=true"}, + }) + if err != nil { + t.Fatal(err) + } + defer l.Close() + if err := l.Log(&logger.Message{ContainerID: cid, Line: []byte("line"), Source: "src1"}); err != nil { + t.Fatal(err) + } + res, err := ioutil.ReadFile(filename) + if err != nil { + t.Fatal(err) + } + + var jsonLog jsonlog.JSONLogs + if err := json.Unmarshal(res, &jsonLog); err != nil { + t.Fatal(err) + } + extra := make(map[string]string) + if err := json.Unmarshal(jsonLog.RawAttrs, &extra); err != nil { + t.Fatal(err) + } + expected := map[string]string{ + "rack": "101", + "dc": "lhr", + "environ": "production", + "debug": "false", + "ssl": "true", + } + if !reflect.DeepEqual(extra, expected) { + t.Fatalf("Wrong log attrs: %q, expected %q", extra, expected) + } +} + +func BenchmarkJSONFileLoggerWithReader(b *testing.B) { + b.StopTimer() + b.ResetTimer() + cid := "a7317399f3f857173c6179d44823594f8294678dea9999662e5c625b5a1c7657" + dir, err := ioutil.TempDir("", "json-logger-bench") + if err != nil { + b.Fatal(err) + } + defer os.RemoveAll(dir) + + l, err := New(logger.Context{ + ContainerID: cid, + LogPath: filepath.Join(dir, "container.log"), + }) + if err != nil { + b.Fatal(err) + } + defer l.Close() + msg := &logger.Message{ContainerID: cid, Line: []byte("line"), Source: "src1"} + jsonlog, err := (&jsonlog.JSONLog{Log: string(msg.Line) + "\n", Stream: msg.Source, Created: msg.Timestamp}).MarshalJSON() + if err != nil { + b.Fatal(err) + } + b.SetBytes(int64(len(jsonlog)+1) * 30) + + b.StartTimer() + + go func() { + for i := 0; i < b.N; i++ { + for j := 0; j < 30; j++ { + l.Log(msg) + } + } + l.Close() + }() + + lw := l.(logger.LogReader).ReadLogs(logger.ReadConfig{Follow: true}) + watchClose := lw.WatchClose() + for { + select { + case <-lw.Msg: + case <-watchClose: + return + } + } +} diff --git a/daemon/logger/jsonfilelog/read.go b/daemon/logger/jsonfilelog/read.go new file mode 100644 index 00000000..0c8fb5e5 --- /dev/null +++ b/daemon/logger/jsonfilelog/read.go @@ -0,0 +1,235 @@ +package jsonfilelog + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "os" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/daemon/logger" + "github.com/docker/docker/pkg/filenotify" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/jsonlog" + "github.com/docker/docker/pkg/tailfile" +) + +const maxJSONDecodeRetry = 20000 + +func decodeLogLine(dec *json.Decoder, l *jsonlog.JSONLog) (*logger.Message, error) { + l.Reset() + if err := dec.Decode(l); err != nil { + return nil, err + } + msg := &logger.Message{ + Source: l.Stream, + Timestamp: l.Created, + Line: []byte(l.Log), + } + return msg, nil +} + +// ReadLogs implements the logger's LogReader interface for the logs +// created by this driver. +func (l *JSONFileLogger) ReadLogs(config logger.ReadConfig) *logger.LogWatcher { + logWatcher := logger.NewLogWatcher() + + go l.readLogs(logWatcher, config) + return logWatcher +} + +func (l *JSONFileLogger) readLogs(logWatcher *logger.LogWatcher, config logger.ReadConfig) { + defer close(logWatcher.Msg) + + pth := l.writer.LogPath() + var files []io.ReadSeeker + for i := l.writer.MaxFiles(); i > 1; i-- { + f, err := os.Open(fmt.Sprintf("%s.%d", pth, i-1)) + if err != nil { + if !os.IsNotExist(err) { + logWatcher.Err <- err + break + } + continue + } + files = append(files, f) + } + + latestFile, err := os.Open(pth) + if err != nil { + logWatcher.Err <- err + return + } + + if config.Tail != 0 { + tailer := ioutils.MultiReadSeeker(append(files, latestFile)...) + tailFile(tailer, logWatcher, config.Tail, config.Since) + } + + // close all the rotated files + for _, f := range files { + if err := f.(io.Closer).Close(); err != nil { + logrus.WithField("logger", "json-file").Warnf("error closing tailed log file: %v", err) + } + } + + if !config.Follow { + return + } + + if config.Tail >= 0 { + latestFile.Seek(0, os.SEEK_END) + } + + l.mu.Lock() + l.readers[logWatcher] = struct{}{} + l.mu.Unlock() + + notifyRotate := l.writer.NotifyRotate() + followLogs(latestFile, logWatcher, notifyRotate, config.Since) + + l.mu.Lock() + delete(l.readers, logWatcher) + l.mu.Unlock() + + l.writer.NotifyRotateEvict(notifyRotate) +} + +func tailFile(f io.ReadSeeker, logWatcher *logger.LogWatcher, tail int, since time.Time) { + var rdr io.Reader = f + if tail > 0 { + ls, err := tailfile.TailFile(f, tail) + if err != nil { + logWatcher.Err <- err + return + } + rdr = bytes.NewBuffer(bytes.Join(ls, []byte("\n"))) + } + dec := json.NewDecoder(rdr) + l := &jsonlog.JSONLog{} + for { + msg, err := decodeLogLine(dec, l) + if err != nil { + if err != io.EOF { + logWatcher.Err <- err + } + return + } + if !since.IsZero() && msg.Timestamp.Before(since) { + continue + } + logWatcher.Msg <- msg + } +} + +func followLogs(f *os.File, logWatcher *logger.LogWatcher, notifyRotate chan interface{}, since time.Time) { + dec := json.NewDecoder(f) + l := &jsonlog.JSONLog{} + + fileWatcher, err := filenotify.New() + if err != nil { + logWatcher.Err <- err + } + defer func() { + f.Close() + fileWatcher.Close() + }() + name := f.Name() + + if err := fileWatcher.Add(name); err != nil { + logrus.WithField("logger", "json-file").Warnf("falling back to file poller due to error: %v", err) + fileWatcher.Close() + fileWatcher = filenotify.NewPollingWatcher() + + if err := fileWatcher.Add(name); err != nil { + logrus.Debugf("error watching log file for modifications: %v", err) + logWatcher.Err <- err + return + } + } + + var retries int + for { + msg, err := decodeLogLine(dec, l) + if err != nil { + if err != io.EOF { + // try again because this shouldn't happen + if _, ok := err.(*json.SyntaxError); ok && retries <= maxJSONDecodeRetry { + dec = json.NewDecoder(f) + retries++ + continue + } + + // io.ErrUnexpectedEOF is returned from json.Decoder when there is + // remaining data in the parser's buffer while an io.EOF occurs. + // If the json logger writes a partial json log entry to the disk + // while at the same time the decoder tries to decode it, the race condition happens. + if err == io.ErrUnexpectedEOF && retries <= maxJSONDecodeRetry { + reader := io.MultiReader(dec.Buffered(), f) + dec = json.NewDecoder(reader) + retries++ + continue + } + + return + } + + select { + case <-fileWatcher.Events(): + dec = json.NewDecoder(f) + continue + case <-fileWatcher.Errors(): + logWatcher.Err <- err + return + case <-logWatcher.WatchClose(): + fileWatcher.Remove(name) + return + case <-notifyRotate: + f.Close() + fileWatcher.Remove(name) + + // retry when the file doesn't exist + for retries := 0; retries <= 5; retries++ { + f, err = os.Open(name) + if err == nil || !os.IsNotExist(err) { + break + } + } + + if err = fileWatcher.Add(name); err != nil { + logWatcher.Err <- err + return + } + if err != nil { + logWatcher.Err <- err + return + } + + dec = json.NewDecoder(f) + continue + } + } + + retries = 0 // reset retries since we've succeeded + if !since.IsZero() && msg.Timestamp.Before(since) { + continue + } + select { + case logWatcher.Msg <- msg: + case <-logWatcher.WatchClose(): + logWatcher.Msg <- msg + for { + msg, err := decodeLogLine(dec, l) + if err != nil { + return + } + if !since.IsZero() && msg.Timestamp.Before(since) { + continue + } + logWatcher.Msg <- msg + } + } + } +} diff --git a/daemon/logger/logger.go b/daemon/logger/logger.go new file mode 100644 index 00000000..cf8d571f --- /dev/null +++ b/daemon/logger/logger.go @@ -0,0 +1,87 @@ +// Package logger defines interfaces that logger drivers implement to +// log messages. +// +// The other half of a logger driver is the implementation of the +// factory, which holds the contextual instance information that +// allows multiple loggers of the same type to perform different +// actions, such as logging to different locations. +package logger + +import ( + "errors" + "time" + + "github.com/docker/docker/pkg/jsonlog" +) + +// ErrReadLogsNotSupported is returned when the logger does not support reading logs. +var ErrReadLogsNotSupported = errors.New("configured logging reader does not support reading") + +const ( + // TimeFormat is the time format used for timestamps sent to log readers. + TimeFormat = jsonlog.RFC3339NanoFixed + logWatcherBufferSize = 4096 +) + +// Message is datastructure that represents record from some container. +type Message struct { + ContainerID string + Line []byte + Source string + Timestamp time.Time +} + +// Logger is the interface for docker logging drivers. +type Logger interface { + Log(*Message) error + Name() string + Close() error +} + +// ReadConfig is the configuration passed into ReadLogs. +type ReadConfig struct { + Since time.Time + Tail int + Follow bool +} + +// LogReader is the interface for reading log messages for loggers that support reading. +type LogReader interface { + // Read logs from underlying logging backend + ReadLogs(ReadConfig) *LogWatcher +} + +// LogWatcher is used when consuming logs read from the LogReader interface. +type LogWatcher struct { + // For sending log messages to a reader. + Msg chan *Message + // For sending error messages that occur while while reading logs. + Err chan error + closeNotifier chan struct{} +} + +// NewLogWatcher returns a new LogWatcher. +func NewLogWatcher() *LogWatcher { + return &LogWatcher{ + Msg: make(chan *Message, logWatcherBufferSize), + Err: make(chan error, 1), + closeNotifier: make(chan struct{}), + } +} + +// Close notifies the underlying log reader to stop. +func (w *LogWatcher) Close() { + // only close if not already closed + select { + case <-w.closeNotifier: + default: + close(w.closeNotifier) + } +} + +// WatchClose returns a channel receiver that receives notification +// when the watcher has been closed. This should only be called from +// one goroutine. +func (w *LogWatcher) WatchClose() <-chan struct{} { + return w.closeNotifier +} diff --git a/daemon/logger/loggerutils/log_tag.go b/daemon/logger/loggerutils/log_tag.go new file mode 100644 index 00000000..6653b9c4 --- /dev/null +++ b/daemon/logger/loggerutils/log_tag.go @@ -0,0 +1,46 @@ +package loggerutils + +import ( + "bytes" + "fmt" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/daemon/logger" + "github.com/docker/docker/utils/templates" +) + +// ParseLogTag generates a context aware tag for consistency across different +// log drivers based on the context of the running container. +func ParseLogTag(ctx logger.Context, defaultTemplate string) (string, error) { + tagTemplate := lookupTagTemplate(ctx, defaultTemplate) + + tmpl, err := templates.NewParse("log-tag", tagTemplate) + if err != nil { + return "", err + } + buf := new(bytes.Buffer) + if err := tmpl.Execute(buf, &ctx); err != nil { + return "", err + } + + return buf.String(), nil +} + +func lookupTagTemplate(ctx logger.Context, defaultTemplate string) string { + tagTemplate := ctx.Config["tag"] + + deprecatedConfigs := []string{"syslog-tag", "gelf-tag", "fluentd-tag"} + for i := 0; tagTemplate == "" && i < len(deprecatedConfigs); i++ { + cfg := deprecatedConfigs[i] + if ctx.Config[cfg] != "" { + tagTemplate = ctx.Config[cfg] + logrus.Warn(fmt.Sprintf("Using log tag from deprecated log-opt '%s'. Please use: --log-opt tag=\"%s\"", cfg, tagTemplate)) + } + } + + if tagTemplate == "" { + tagTemplate = defaultTemplate + } + + return tagTemplate +} diff --git a/daemon/logger/loggerutils/log_tag_test.go b/daemon/logger/loggerutils/log_tag_test.go new file mode 100644 index 00000000..994508b1 --- /dev/null +++ b/daemon/logger/loggerutils/log_tag_test.go @@ -0,0 +1,58 @@ +package loggerutils + +import ( + "testing" + + "github.com/docker/docker/daemon/logger" +) + +func TestParseLogTagDefaultTag(t *testing.T) { + ctx := buildContext(map[string]string{}) + tag, e := ParseLogTag(ctx, "{{.ID}}") + assertTag(t, e, tag, ctx.ID()) +} + +func TestParseLogTag(t *testing.T) { + ctx := buildContext(map[string]string{"tag": "{{.ImageName}}/{{.Name}}/{{.ID}}"}) + tag, e := ParseLogTag(ctx, "{{.ID}}") + assertTag(t, e, tag, "test-image/test-container/container-ab") +} + +func TestParseLogTagSyslogTag(t *testing.T) { + ctx := buildContext(map[string]string{"syslog-tag": "{{.ImageName}}/{{.Name}}/{{.ID}}"}) + tag, e := ParseLogTag(ctx, "{{.ID}}") + assertTag(t, e, tag, "test-image/test-container/container-ab") +} + +func TestParseLogTagGelfTag(t *testing.T) { + ctx := buildContext(map[string]string{"gelf-tag": "{{.ImageName}}/{{.Name}}/{{.ID}}"}) + tag, e := ParseLogTag(ctx, "{{.ID}}") + assertTag(t, e, tag, "test-image/test-container/container-ab") +} + +func TestParseLogTagFluentdTag(t *testing.T) { + ctx := buildContext(map[string]string{"fluentd-tag": "{{.ImageName}}/{{.Name}}/{{.ID}}"}) + tag, e := ParseLogTag(ctx, "{{.ID}}") + assertTag(t, e, tag, "test-image/test-container/container-ab") +} + +// Helpers + +func buildContext(cfg map[string]string) logger.Context { + return logger.Context{ + ContainerID: "container-abcdefghijklmnopqrstuvwxyz01234567890", + ContainerName: "/test-container", + ContainerImageID: "image-abcdefghijklmnopqrstuvwxyz01234567890", + ContainerImageName: "test-image", + Config: cfg, + } +} + +func assertTag(t *testing.T, e error, tag string, expected string) { + if e != nil { + t.Fatalf("Error generating tag: %q", e) + } + if tag != expected { + t.Fatalf("Wrong tag: %q, should be %q", tag, expected) + } +} diff --git a/daemon/logger/loggerutils/rotatefilewriter.go b/daemon/logger/loggerutils/rotatefilewriter.go new file mode 100644 index 00000000..99e0964a --- /dev/null +++ b/daemon/logger/loggerutils/rotatefilewriter.go @@ -0,0 +1,124 @@ +package loggerutils + +import ( + "os" + "strconv" + "sync" + + "github.com/docker/docker/pkg/pubsub" +) + +// RotateFileWriter is Logger implementation for default Docker logging. +type RotateFileWriter struct { + f *os.File // store for closing + mu sync.Mutex + capacity int64 //maximum size of each file + currentSize int64 // current size of the latest file + maxFiles int //maximum number of files + notifyRotate *pubsub.Publisher +} + +//NewRotateFileWriter creates new RotateFileWriter +func NewRotateFileWriter(logPath string, capacity int64, maxFiles int) (*RotateFileWriter, error) { + log, err := os.OpenFile(logPath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0640) + if err != nil { + return nil, err + } + + size, err := log.Seek(0, os.SEEK_END) + if err != nil { + return nil, err + } + + return &RotateFileWriter{ + f: log, + capacity: capacity, + currentSize: size, + maxFiles: maxFiles, + notifyRotate: pubsub.NewPublisher(0, 1), + }, nil +} + +//WriteLog write log message to File +func (w *RotateFileWriter) Write(message []byte) (int, error) { + w.mu.Lock() + if err := w.checkCapacityAndRotate(); err != nil { + w.mu.Unlock() + return -1, err + } + + n, err := w.f.Write(message) + if err == nil { + w.currentSize += int64(n) + } + w.mu.Unlock() + return n, err +} + +func (w *RotateFileWriter) checkCapacityAndRotate() error { + if w.capacity == -1 { + return nil + } + + if w.currentSize >= w.capacity { + name := w.f.Name() + if err := w.f.Close(); err != nil { + return err + } + if err := rotate(name, w.maxFiles); err != nil { + return err + } + file, err := os.OpenFile(name, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 06400) + if err != nil { + return err + } + w.f = file + w.currentSize = 0 + w.notifyRotate.Publish(struct{}{}) + } + + return nil +} + +func rotate(name string, maxFiles int) error { + if maxFiles < 2 { + return nil + } + for i := maxFiles - 1; i > 1; i-- { + toPath := name + "." + strconv.Itoa(i) + fromPath := name + "." + strconv.Itoa(i-1) + if err := os.Rename(fromPath, toPath); err != nil && !os.IsNotExist(err) { + return err + } + } + + if err := os.Rename(name, name+".1"); err != nil && !os.IsNotExist(err) { + return err + } + return nil +} + +// LogPath returns the location the given writer logs to. +func (w *RotateFileWriter) LogPath() string { + return w.f.Name() +} + +// MaxFiles return maximum number of files +func (w *RotateFileWriter) MaxFiles() int { + return w.maxFiles +} + +//NotifyRotate returns the new subscriber +func (w *RotateFileWriter) NotifyRotate() chan interface{} { + return w.notifyRotate.Subscribe() +} + +//NotifyRotateEvict removes the specified subscriber from receiving any more messages. +func (w *RotateFileWriter) NotifyRotateEvict(sub chan interface{}) { + w.notifyRotate.Evict(sub) +} + +// Close closes underlying file and signals all readers to stop. +func (w *RotateFileWriter) Close() error { + return w.f.Close() +} diff --git a/daemon/logger/splunk/splunk.go b/daemon/logger/splunk/splunk.go new file mode 100644 index 00000000..201905f3 --- /dev/null +++ b/daemon/logger/splunk/splunk.go @@ -0,0 +1,266 @@ +// Package splunk provides the log driver for forwarding server logs to +// Splunk HTTP Event Collector endpoint. +package splunk + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strconv" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/daemon/logger" + "github.com/docker/docker/daemon/logger/loggerutils" + "github.com/docker/docker/pkg/urlutil" +) + +const ( + driverName = "splunk" + splunkURLKey = "splunk-url" + splunkTokenKey = "splunk-token" + splunkSourceKey = "splunk-source" + splunkSourceTypeKey = "splunk-sourcetype" + splunkIndexKey = "splunk-index" + splunkCAPathKey = "splunk-capath" + splunkCANameKey = "splunk-caname" + splunkInsecureSkipVerifyKey = "splunk-insecureskipverify" + envKey = "env" + labelsKey = "labels" + tagKey = "tag" +) + +type splunkLogger struct { + client *http.Client + transport *http.Transport + + url string + auth string + nullMessage *splunkMessage +} + +type splunkMessage struct { + Event splunkMessageEvent `json:"event"` + Time string `json:"time"` + Host string `json:"host"` + Source string `json:"source,omitempty"` + SourceType string `json:"sourcetype,omitempty"` + Index string `json:"index,omitempty"` +} + +type splunkMessageEvent struct { + Line string `json:"line"` + Source string `json:"source"` + Tag string `json:"tag,omitempty"` + Attrs map[string]string `json:"attrs,omitempty"` +} + +func init() { + if err := logger.RegisterLogDriver(driverName, New); err != nil { + logrus.Fatal(err) + } + if err := logger.RegisterLogOptValidator(driverName, ValidateLogOpt); err != nil { + logrus.Fatal(err) + } +} + +// New creates splunk logger driver using configuration passed in context +func New(ctx logger.Context) (logger.Logger, error) { + hostname, err := ctx.Hostname() + if err != nil { + return nil, fmt.Errorf("%s: cannot access hostname to set source field", driverName) + } + + // Parse and validate Splunk URL + splunkURL, err := parseURL(ctx) + if err != nil { + return nil, err + } + + // Splunk Token is required parameter + splunkToken, ok := ctx.Config[splunkTokenKey] + if !ok { + return nil, fmt.Errorf("%s: %s is expected", driverName, splunkTokenKey) + } + + tlsConfig := &tls.Config{} + + // Splunk is using autogenerated certificates by default, + // allow users to trust them with skipping verification + if insecureSkipVerifyStr, ok := ctx.Config[splunkInsecureSkipVerifyKey]; ok { + insecureSkipVerify, err := strconv.ParseBool(insecureSkipVerifyStr) + if err != nil { + return nil, err + } + tlsConfig.InsecureSkipVerify = insecureSkipVerify + } + + // If path to the root certificate is provided - load it + if caPath, ok := ctx.Config[splunkCAPathKey]; ok { + caCert, err := ioutil.ReadFile(caPath) + if err != nil { + return nil, err + } + caPool := x509.NewCertPool() + caPool.AppendCertsFromPEM(caCert) + tlsConfig.RootCAs = caPool + } + + if caName, ok := ctx.Config[splunkCANameKey]; ok { + tlsConfig.ServerName = caName + } + + transport := &http.Transport{ + TLSClientConfig: tlsConfig, + } + client := &http.Client{ + Transport: transport, + } + + var nullMessage = &splunkMessage{ + Host: hostname, + } + + // Optional parameters for messages + nullMessage.Source = ctx.Config[splunkSourceKey] + nullMessage.SourceType = ctx.Config[splunkSourceTypeKey] + nullMessage.Index = ctx.Config[splunkIndexKey] + + tag, err := loggerutils.ParseLogTag(ctx, "{{.ID}}") + if err != nil { + return nil, err + } + nullMessage.Event.Tag = tag + nullMessage.Event.Attrs = ctx.ExtraAttributes(nil) + + logger := &splunkLogger{ + client: client, + transport: transport, + url: splunkURL.String(), + auth: "Splunk " + splunkToken, + nullMessage: nullMessage, + } + + err = verifySplunkConnection(logger) + if err != nil { + return nil, err + } + + return logger, nil +} + +func (l *splunkLogger) Log(msg *logger.Message) error { + // Construct message as a copy of nullMessage + message := *l.nullMessage + message.Time = fmt.Sprintf("%f", float64(msg.Timestamp.UnixNano())/1000000000) + message.Event.Line = string(msg.Line) + message.Event.Source = msg.Source + + jsonEvent, err := json.Marshal(&message) + if err != nil { + return err + } + req, err := http.NewRequest("POST", l.url, bytes.NewBuffer(jsonEvent)) + if err != nil { + return err + } + req.Header.Set("Authorization", l.auth) + res, err := l.client.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + var body []byte + body, err = ioutil.ReadAll(res.Body) + if err != nil { + return err + } + return fmt.Errorf("%s: failed to send event - %s - %s", driverName, res.Status, body) + } + io.Copy(ioutil.Discard, res.Body) + return nil +} + +func (l *splunkLogger) Close() error { + l.transport.CloseIdleConnections() + return nil +} + +func (l *splunkLogger) Name() string { + return driverName +} + +// ValidateLogOpt looks for all supported by splunk driver options +func ValidateLogOpt(cfg map[string]string) error { + for key := range cfg { + switch key { + case splunkURLKey: + case splunkTokenKey: + case splunkSourceKey: + case splunkSourceTypeKey: + case splunkIndexKey: + case splunkCAPathKey: + case splunkCANameKey: + case splunkInsecureSkipVerifyKey: + case envKey: + case labelsKey: + case tagKey: + default: + return fmt.Errorf("unknown log opt '%s' for %s log driver", key, driverName) + } + } + return nil +} + +func parseURL(ctx logger.Context) (*url.URL, error) { + splunkURLStr, ok := ctx.Config[splunkURLKey] + if !ok { + return nil, fmt.Errorf("%s: %s is expected", driverName, splunkURLKey) + } + + splunkURL, err := url.Parse(splunkURLStr) + if err != nil { + return nil, fmt.Errorf("%s: failed to parse %s as url value in %s", driverName, splunkURLStr, splunkURLKey) + } + + if !urlutil.IsURL(splunkURLStr) || + !splunkURL.IsAbs() || + (splunkURL.Path != "" && splunkURL.Path != "/") || + splunkURL.RawQuery != "" || + splunkURL.Fragment != "" { + return nil, fmt.Errorf("%s: expected format schema://dns_name_or_ip:port for %s", driverName, splunkURLKey) + } + + splunkURL.Path = "/services/collector/event/1.0" + + return splunkURL, nil +} + +func verifySplunkConnection(l *splunkLogger) error { + req, err := http.NewRequest("OPTIONS", l.url, nil) + if err != nil { + return err + } + res, err := l.client.Do(req) + if err != nil { + return err + } + if res.Body != nil { + defer res.Body.Close() + } + if res.StatusCode != http.StatusOK { + var body []byte + body, err = ioutil.ReadAll(res.Body) + if err != nil { + return err + } + return fmt.Errorf("%s: failed to verify connection - %s - %s", driverName, res.Status, body) + } + return nil +} diff --git a/daemon/logger/syslog/syslog.go b/daemon/logger/syslog/syslog.go new file mode 100644 index 00000000..99e03278 --- /dev/null +++ b/daemon/logger/syslog/syslog.go @@ -0,0 +1,247 @@ +// +build linux + +// Package syslog provides the logdriver for forwarding server logs to syslog endpoints. +package syslog + +import ( + "crypto/tls" + "errors" + "fmt" + "net" + "net/url" + "os" + "path" + "strconv" + "strings" + "time" + + syslog "github.com/RackSec/srslog" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/daemon/logger" + "github.com/docker/docker/daemon/logger/loggerutils" + "github.com/docker/docker/pkg/urlutil" + "github.com/docker/go-connections/tlsconfig" +) + +const ( + name = "syslog" + secureProto = "tcp+tls" +) + +var facilities = map[string]syslog.Priority{ + "kern": syslog.LOG_KERN, + "user": syslog.LOG_USER, + "mail": syslog.LOG_MAIL, + "daemon": syslog.LOG_DAEMON, + "auth": syslog.LOG_AUTH, + "syslog": syslog.LOG_SYSLOG, + "lpr": syslog.LOG_LPR, + "news": syslog.LOG_NEWS, + "uucp": syslog.LOG_UUCP, + "cron": syslog.LOG_CRON, + "authpriv": syslog.LOG_AUTHPRIV, + "ftp": syslog.LOG_FTP, + "local0": syslog.LOG_LOCAL0, + "local1": syslog.LOG_LOCAL1, + "local2": syslog.LOG_LOCAL2, + "local3": syslog.LOG_LOCAL3, + "local4": syslog.LOG_LOCAL4, + "local5": syslog.LOG_LOCAL5, + "local6": syslog.LOG_LOCAL6, + "local7": syslog.LOG_LOCAL7, +} + +type syslogger struct { + writer *syslog.Writer +} + +func init() { + if err := logger.RegisterLogDriver(name, New); err != nil { + logrus.Fatal(err) + } + if err := logger.RegisterLogOptValidator(name, ValidateLogOpt); err != nil { + logrus.Fatal(err) + } +} + +// rsyslog uses appname part of syslog message to fill in an %syslogtag% template +// attribute in rsyslog.conf. In order to be backward compatible to rfc3164 +// tag will be also used as an appname +func rfc5424formatterWithAppNameAsTag(p syslog.Priority, hostname, tag, content string) string { + timestamp := time.Now().Format(time.RFC3339) + pid := os.Getpid() + msg := fmt.Sprintf("<%d>%d %s %s %s %d %s %s", + p, 1, timestamp, hostname, tag, pid, tag, content) + return msg +} + +// New creates a syslog logger using the configuration passed in on +// the context. Supported context configuration variables are +// syslog-address, syslog-facility, & syslog-tag. +func New(ctx logger.Context) (logger.Logger, error) { + tag, err := loggerutils.ParseLogTag(ctx, "{{.ID}}") + if err != nil { + return nil, err + } + + proto, address, err := parseAddress(ctx.Config["syslog-address"]) + if err != nil { + return nil, err + } + + facility, err := parseFacility(ctx.Config["syslog-facility"]) + if err != nil { + return nil, err + } + + syslogFormatter, syslogFramer, err := parseLogFormat(ctx.Config["syslog-format"]) + if err != nil { + return nil, err + } + + logTag := path.Base(os.Args[0]) + "/" + tag + + var log *syslog.Writer + if proto == secureProto { + tlsConfig, tlsErr := parseTLSConfig(ctx.Config) + if tlsErr != nil { + return nil, tlsErr + } + log, err = syslog.DialWithTLSConfig(proto, address, facility, logTag, tlsConfig) + } else { + log, err = syslog.Dial(proto, address, facility, logTag) + } + + if err != nil { + return nil, err + } + + log.SetFormatter(syslogFormatter) + log.SetFramer(syslogFramer) + + return &syslogger{ + writer: log, + }, nil +} + +func (s *syslogger) Log(msg *logger.Message) error { + if msg.Source == "stderr" { + return s.writer.Err(string(msg.Line)) + } + return s.writer.Info(string(msg.Line)) +} + +func (s *syslogger) Close() error { + return s.writer.Close() +} + +func (s *syslogger) Name() string { + return name +} + +func parseAddress(address string) (string, string, error) { + if address == "" { + return "", "", nil + } + if !urlutil.IsTransportURL(address) { + return "", "", fmt.Errorf("syslog-address should be in form proto://address, got %v", address) + } + url, err := url.Parse(address) + if err != nil { + return "", "", err + } + + // unix socket validation + if url.Scheme == "unix" { + if _, err := os.Stat(url.Path); err != nil { + return "", "", err + } + return url.Scheme, url.Path, nil + } + + // here we process tcp|udp + host := url.Host + if _, _, err := net.SplitHostPort(host); err != nil { + if !strings.Contains(err.Error(), "missing port in address") { + return "", "", err + } + host = host + ":514" + } + + return url.Scheme, host, nil +} + +// ValidateLogOpt looks for syslog specific log options +// syslog-address, syslog-facility, & syslog-tag. +func ValidateLogOpt(cfg map[string]string) error { + for key := range cfg { + switch key { + case "syslog-address": + case "syslog-facility": + case "syslog-tag": + case "syslog-tls-ca-cert": + case "syslog-tls-cert": + case "syslog-tls-key": + case "syslog-tls-skip-verify": + case "tag": + case "syslog-format": + default: + return fmt.Errorf("unknown log opt '%s' for syslog log driver", key) + } + } + if _, _, err := parseAddress(cfg["syslog-address"]); err != nil { + return err + } + if _, err := parseFacility(cfg["syslog-facility"]); err != nil { + return err + } + if _, _, err := parseLogFormat(cfg["syslog-format"]); err != nil { + return err + } + return nil +} + +func parseFacility(facility string) (syslog.Priority, error) { + if facility == "" { + return syslog.LOG_DAEMON, nil + } + + if syslogFacility, valid := facilities[facility]; valid { + return syslogFacility, nil + } + + fInt, err := strconv.Atoi(facility) + if err == nil && 0 <= fInt && fInt <= 23 { + return syslog.Priority(fInt << 3), nil + } + + return syslog.Priority(0), errors.New("invalid syslog facility") +} + +func parseTLSConfig(cfg map[string]string) (*tls.Config, error) { + _, skipVerify := cfg["syslog-tls-skip-verify"] + + opts := tlsconfig.Options{ + CAFile: cfg["syslog-tls-ca-cert"], + CertFile: cfg["syslog-tls-cert"], + KeyFile: cfg["syslog-tls-key"], + InsecureSkipVerify: skipVerify, + } + + return tlsconfig.Client(opts) +} + +func parseLogFormat(logFormat string) (syslog.Formatter, syslog.Framer, error) { + switch logFormat { + case "": + return syslog.UnixFormatter, syslog.DefaultFramer, nil + case "rfc3164": + return syslog.RFC3164Formatter, syslog.DefaultFramer, nil + case "rfc5424": + return rfc5424formatterWithAppNameAsTag, syslog.RFC5425MessageLengthFramer, nil + default: + return nil, nil, errors.New("Invalid syslog format") + } + +} diff --git a/daemon/logger/syslog/syslog_test.go b/daemon/logger/syslog/syslog_test.go new file mode 100644 index 00000000..c18494be --- /dev/null +++ b/daemon/logger/syslog/syslog_test.go @@ -0,0 +1,45 @@ +// +build linux + +package syslog + +import ( + syslog "github.com/RackSec/srslog" + "reflect" + "testing" +) + +func functionMatches(expectedFun interface{}, actualFun interface{}) bool { + return reflect.ValueOf(expectedFun).Pointer() == reflect.ValueOf(actualFun).Pointer() +} + +func TestParseLogFormat(t *testing.T) { + formatter, framer, err := parseLogFormat("rfc5424") + if err != nil || !functionMatches(rfc5424formatterWithAppNameAsTag, formatter) || + !functionMatches(syslog.RFC5425MessageLengthFramer, framer) { + t.Fatal("Failed to parse rfc5424 format", err, formatter, framer) + } + + formatter, framer, err = parseLogFormat("rfc3164") + if err != nil || !functionMatches(syslog.RFC3164Formatter, formatter) || + !functionMatches(syslog.DefaultFramer, framer) { + t.Fatal("Failed to parse rfc3164 format", err, formatter, framer) + } + + formatter, framer, err = parseLogFormat("") + if err != nil || !functionMatches(syslog.UnixFormatter, formatter) || + !functionMatches(syslog.DefaultFramer, framer) { + t.Fatal("Failed to parse empty format", err, formatter, framer) + } + + formatter, framer, err = parseLogFormat("invalid") + if err == nil { + t.Fatal("Failed to parse invalid format", err, formatter, framer) + } +} + +func TestValidateLogOptEmpty(t *testing.T) { + emptyConfig := make(map[string]string) + if err := ValidateLogOpt(emptyConfig); err != nil { + t.Fatal("Failed to parse empty config", err) + } +} diff --git a/daemon/logger/syslog/syslog_unsupported.go b/daemon/logger/syslog/syslog_unsupported.go new file mode 100644 index 00000000..50cc51b6 --- /dev/null +++ b/daemon/logger/syslog/syslog_unsupported.go @@ -0,0 +1,3 @@ +// +build !linux + +package syslog diff --git a/daemon/logs.go b/daemon/logs.go new file mode 100644 index 00000000..40c47a6e --- /dev/null +++ b/daemon/logs.go @@ -0,0 +1,154 @@ +package daemon + +import ( + "fmt" + "io" + "strconv" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/types/backend" + "github.com/docker/docker/container" + "github.com/docker/docker/daemon/logger" + "github.com/docker/docker/daemon/logger/jsonfilelog" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/stdcopy" + containertypes "github.com/docker/engine-api/types/container" + timetypes "github.com/docker/engine-api/types/time" +) + +// ContainerLogs hooks up a container's stdout and stderr streams +// configured with the given struct. +func (daemon *Daemon) ContainerLogs(containerName string, config *backend.ContainerLogsConfig, started chan struct{}) error { + container, err := daemon.GetContainer(containerName) + if err != nil { + return err + } + + if !(config.ShowStdout || config.ShowStderr) { + return fmt.Errorf("You must choose at least one stream") + } + + cLog, err := daemon.getLogger(container) + if err != nil { + return err + } + logReader, ok := cLog.(logger.LogReader) + if !ok { + return logger.ErrReadLogsNotSupported + } + + follow := config.Follow && container.IsRunning() + tailLines, err := strconv.Atoi(config.Tail) + if err != nil { + tailLines = -1 + } + + logrus.Debug("logs: begin stream") + + var since time.Time + if config.Since != "" { + s, n, err := timetypes.ParseTimestamps(config.Since, 0) + if err != nil { + return err + } + since = time.Unix(s, n) + } + readConfig := logger.ReadConfig{ + Since: since, + Tail: tailLines, + Follow: follow, + } + logs := logReader.ReadLogs(readConfig) + + wf := ioutils.NewWriteFlusher(config.OutStream) + defer wf.Close() + close(started) + wf.Flush() + + var outStream io.Writer = wf + errStream := outStream + if !container.Config.Tty { + errStream = stdcopy.NewStdWriter(outStream, stdcopy.Stderr) + outStream = stdcopy.NewStdWriter(outStream, stdcopy.Stdout) + } + + for { + select { + case err := <-logs.Err: + logrus.Errorf("Error streaming logs: %v", err) + return nil + case <-config.Stop: + logs.Close() + return nil + case msg, ok := <-logs.Msg: + if !ok { + logrus.Debugf("logs: end stream") + logs.Close() + return nil + } + logLine := msg.Line + if config.Timestamps { + logLine = append([]byte(msg.Timestamp.Format(logger.TimeFormat)+" "), logLine...) + } + if msg.Source == "stdout" && config.ShowStdout { + outStream.Write(logLine) + } + if msg.Source == "stderr" && config.ShowStderr { + errStream.Write(logLine) + } + } + } +} + +func (daemon *Daemon) getLogger(container *container.Container) (logger.Logger, error) { + if container.LogDriver != nil && container.IsRunning() { + return container.LogDriver, nil + } + cfg := daemon.getLogConfig(container.HostConfig.LogConfig) + if err := logger.ValidateLogOpts(cfg.Type, cfg.Config); err != nil { + return nil, err + } + return container.StartLogger(cfg) +} + +// StartLogging initializes and starts the container logging stream. +func (daemon *Daemon) StartLogging(container *container.Container) error { + cfg := daemon.getLogConfig(container.HostConfig.LogConfig) + if cfg.Type == "none" { + return nil // do not start logging routines + } + + if err := logger.ValidateLogOpts(cfg.Type, cfg.Config); err != nil { + return err + } + l, err := container.StartLogger(cfg) + if err != nil { + return fmt.Errorf("Failed to initialize logging driver: %v", err) + } + + copier := logger.NewCopier(container.ID, map[string]io.Reader{"stdout": container.StdoutPipe(), "stderr": container.StderrPipe()}, l) + container.LogCopier = copier + copier.Run() + container.LogDriver = l + + // set LogPath field only for json-file logdriver + if jl, ok := l.(*jsonfilelog.JSONFileLogger); ok { + container.LogPath = jl.LogPath() + } + + return nil +} + +// getLogConfig returns the log configuration for the container. +func (daemon *Daemon) getLogConfig(cfg containertypes.LogConfig) containertypes.LogConfig { + if cfg.Type != "" || len(cfg.Config) > 0 { // container has log driver configured + if cfg.Type == "" { + cfg.Type = jsonfilelog.Name + } + return cfg + } + + // Use daemon's default log config for containers + return daemon.defaultLogConfig +} diff --git a/daemon/monitor.go b/daemon/monitor.go new file mode 100644 index 00000000..f9f7def9 --- /dev/null +++ b/daemon/monitor.go @@ -0,0 +1,144 @@ +package daemon + +import ( + "errors" + "fmt" + "io" + "runtime" + "strconv" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/libcontainerd" + "github.com/docker/docker/runconfig" +) + +// StateChanged updates daemon state changes from containerd +func (daemon *Daemon) StateChanged(id string, e libcontainerd.StateInfo) error { + c := daemon.containers.Get(id) + if c == nil { + return fmt.Errorf("no such container: %s", id) + } + + switch e.State { + case libcontainerd.StateOOM: + // StateOOM is Linux specific and should never be hit on Windows + if runtime.GOOS == "windows" { + return errors.New("Received StateOOM from libcontainerd on Windows. This should never happen.") + } + daemon.LogContainerEvent(c, "oom") + case libcontainerd.StateExit: + c.Lock() + defer c.Unlock() + c.Wait() + c.Reset(false) + c.SetStopped(platformConstructExitStatus(e)) + attributes := map[string]string{ + "exitCode": strconv.Itoa(int(e.ExitCode)), + } + daemon.LogContainerEventWithAttributes(c, "die", attributes) + daemon.Cleanup(c) + // FIXME: here is race condition between two RUN instructions in Dockerfile + // because they share same runconfig and change image. Must be fixed + // in builder/builder.go + return c.ToDisk() + case libcontainerd.StateRestart: + c.Lock() + defer c.Unlock() + c.Reset(false) + c.RestartCount++ + c.SetRestarting(platformConstructExitStatus(e)) + attributes := map[string]string{ + "exitCode": strconv.Itoa(int(e.ExitCode)), + } + daemon.LogContainerEventWithAttributes(c, "die", attributes) + return c.ToDisk() + case libcontainerd.StateExitProcess: + c.Lock() + defer c.Unlock() + if execConfig := c.ExecCommands.Get(e.ProcessID); execConfig != nil { + ec := int(e.ExitCode) + execConfig.ExitCode = &ec + execConfig.Running = false + execConfig.Wait() + if err := execConfig.CloseStreams(); err != nil { + logrus.Errorf("%s: %s", c.ID, err) + } + + // remove the exec command from the container's store only and not the + // daemon's store so that the exec command can be inspected. + c.ExecCommands.Delete(execConfig.ID) + } else { + logrus.Warnf("Ignoring StateExitProcess for %v but no exec command found", e) + } + case libcontainerd.StateStart, libcontainerd.StateRestore: + c.SetRunning(int(e.Pid), e.State == libcontainerd.StateStart) + c.HasBeenManuallyStopped = false + if err := c.ToDisk(); err != nil { + c.Reset(false) + return err + } + daemon.LogContainerEvent(c, "start") + case libcontainerd.StatePause: + c.Paused = true + daemon.LogContainerEvent(c, "pause") + case libcontainerd.StateResume: + c.Paused = false + daemon.LogContainerEvent(c, "unpause") + } + + return nil +} + +// AttachStreams is called by libcontainerd to connect the stdio. +func (daemon *Daemon) AttachStreams(id string, iop libcontainerd.IOPipe) error { + var s *runconfig.StreamConfig + c := daemon.containers.Get(id) + if c == nil { + ec, err := daemon.getExecConfig(id) + if err != nil { + return fmt.Errorf("no such exec/container: %s", id) + } + s = ec.StreamConfig + } else { + s = c.StreamConfig + if err := daemon.StartLogging(c); err != nil { + c.Reset(false) + return err + } + } + + if stdin := s.Stdin(); stdin != nil { + if iop.Stdin != nil { + go func() { + io.Copy(iop.Stdin, stdin) + iop.Stdin.Close() + }() + } + } else { + if c != nil && !c.Config.Tty { + // tty is enabled, so dont close containerd's iopipe stdin. + if iop.Stdin != nil { + iop.Stdin.Close() + } + } + } + + copy := func(w io.Writer, r io.Reader) { + s.Add(1) + go func() { + if _, err := io.Copy(w, r); err != nil { + logrus.Errorf("%v stream copy error: %v", id, err) + } + s.Done() + }() + } + + if iop.Stdout != nil { + copy(s.Stdout(), iop.Stdout) + } + if iop.Stderr != nil { + copy(s.Stderr(), iop.Stderr) + } + + return nil +} diff --git a/daemon/monitor_linux.go b/daemon/monitor_linux.go new file mode 100644 index 00000000..df8b6c5d --- /dev/null +++ b/daemon/monitor_linux.go @@ -0,0 +1,14 @@ +package daemon + +import ( + "github.com/docker/docker/container" + "github.com/docker/docker/libcontainerd" +) + +// platformConstructExitStatus returns a platform specific exit status structure +func platformConstructExitStatus(e libcontainerd.StateInfo) *container.ExitStatus { + return &container.ExitStatus{ + ExitCode: int(e.ExitCode), + OOMKilled: e.OOMKilled, + } +} diff --git a/daemon/monitor_windows.go b/daemon/monitor_windows.go new file mode 100644 index 00000000..b808ed3d --- /dev/null +++ b/daemon/monitor_windows.go @@ -0,0 +1,13 @@ +package daemon + +import ( + "github.com/docker/docker/container" + "github.com/docker/docker/libcontainerd" +) + +// platformConstructExitStatus returns a platform specific exit status structure +func platformConstructExitStatus(e libcontainerd.StateInfo) *container.ExitStatus { + return &container.ExitStatus{ + ExitCode: int(e.ExitCode), + } +} diff --git a/daemon/mounts.go b/daemon/mounts.go new file mode 100644 index 00000000..d4f24b28 --- /dev/null +++ b/daemon/mounts.go @@ -0,0 +1,48 @@ +package daemon + +import ( + "fmt" + "strings" + + "github.com/docker/docker/container" + volumestore "github.com/docker/docker/volume/store" +) + +func (daemon *Daemon) prepareMountPoints(container *container.Container) error { + for _, config := range container.MountPoints { + if err := daemon.lazyInitializeVolume(container.ID, config); err != nil { + return err + } + } + return nil +} + +func (daemon *Daemon) removeMountPoints(container *container.Container, rm bool) error { + var rmErrors []string + for _, m := range container.MountPoints { + if m.Volume == nil { + continue + } + daemon.volumes.Dereference(m.Volume, container.ID) + if rm { + // Do not remove named mountpoints + // these are mountpoints specified like `docker run -v :/foo` + if m.Named { + continue + } + err := daemon.volumes.Remove(m.Volume) + // Ignore volume in use errors because having this + // volume being referenced by other container is + // not an error, but an implementation detail. + // This prevents docker from logging "ERROR: Volume in use" + // where there is another container using the volume. + if err != nil && !volumestore.IsInUse(err) { + rmErrors = append(rmErrors, err.Error()) + } + } + } + if len(rmErrors) > 0 { + return fmt.Errorf("Error removing volumes:\n%v", strings.Join(rmErrors, "\n")) + } + return nil +} diff --git a/daemon/network.go b/daemon/network.go new file mode 100644 index 00000000..98879915 --- /dev/null +++ b/daemon/network.go @@ -0,0 +1,205 @@ +package daemon + +import ( + "fmt" + "net" + "net/http" + "strings" + + "github.com/docker/docker/errors" + "github.com/docker/docker/runconfig" + "github.com/docker/engine-api/types/network" + "github.com/docker/libnetwork" +) + +// NetworkControllerEnabled checks if the networking stack is enabled. +// This feature depends on OS primitives and it's disabled in systems like Windows. +func (daemon *Daemon) NetworkControllerEnabled() bool { + return daemon.netController != nil +} + +// FindNetwork function finds a network for a given string that can represent network name or id +func (daemon *Daemon) FindNetwork(idName string) (libnetwork.Network, error) { + // Find by Name + n, err := daemon.GetNetworkByName(idName) + if err != nil && !isNoSuchNetworkError(err) { + return nil, err + } + + if n != nil { + return n, nil + } + + // Find by id + return daemon.GetNetworkByID(idName) +} + +func isNoSuchNetworkError(err error) bool { + _, ok := err.(libnetwork.ErrNoSuchNetwork) + return ok +} + +// GetNetworkByID function returns a network whose ID begins with the given prefix. +// It fails with an error if no matching, or more than one matching, networks are found. +func (daemon *Daemon) GetNetworkByID(partialID string) (libnetwork.Network, error) { + list := daemon.GetNetworksByID(partialID) + + if len(list) == 0 { + return nil, libnetwork.ErrNoSuchNetwork(partialID) + } + if len(list) > 1 { + return nil, libnetwork.ErrInvalidID(partialID) + } + return list[0], nil +} + +// GetNetworkByName function returns a network for a given network name. +func (daemon *Daemon) GetNetworkByName(name string) (libnetwork.Network, error) { + c := daemon.netController + if name == "" { + name = c.Config().Daemon.DefaultNetwork + } + return c.NetworkByName(name) +} + +// GetNetworksByID returns a list of networks whose ID partially matches zero or more networks +func (daemon *Daemon) GetNetworksByID(partialID string) []libnetwork.Network { + c := daemon.netController + list := []libnetwork.Network{} + l := func(nw libnetwork.Network) bool { + if strings.HasPrefix(nw.ID(), partialID) { + list = append(list, nw) + } + return false + } + c.WalkNetworks(l) + + return list +} + +// GetAllNetworks returns a list containing all networks +func (daemon *Daemon) GetAllNetworks() []libnetwork.Network { + c := daemon.netController + list := []libnetwork.Network{} + l := func(nw libnetwork.Network) bool { + list = append(list, nw) + return false + } + c.WalkNetworks(l) + + return list +} + +// CreateNetwork creates a network with the given name, driver and other optional parameters +func (daemon *Daemon) CreateNetwork(name, driver string, ipam network.IPAM, netOption map[string]string, labels map[string]string, internal bool, enableIPv6 bool) (libnetwork.Network, error) { + c := daemon.netController + if driver == "" { + driver = c.Config().Daemon.DefaultDriver + } + + v4Conf, v6Conf, err := getIpamConfig(ipam.Config) + if err != nil { + return nil, err + } + + nwOptions := []libnetwork.NetworkOption{ + libnetwork.NetworkOptionIpam(ipam.Driver, "", v4Conf, v6Conf, ipam.Options), + libnetwork.NetworkOptionEnableIPv6(enableIPv6), + libnetwork.NetworkOptionDriverOpts(netOption), + libnetwork.NetworkOptionLabels(labels), + } + if internal { + nwOptions = append(nwOptions, libnetwork.NetworkOptionInternalNetwork()) + } + n, err := c.NewNetwork(driver, name, nwOptions...) + if err != nil { + return nil, err + } + + daemon.LogNetworkEvent(n, "create") + return n, nil +} + +func getIpamConfig(data []network.IPAMConfig) ([]*libnetwork.IpamConf, []*libnetwork.IpamConf, error) { + ipamV4Cfg := []*libnetwork.IpamConf{} + ipamV6Cfg := []*libnetwork.IpamConf{} + for _, d := range data { + iCfg := libnetwork.IpamConf{} + iCfg.PreferredPool = d.Subnet + iCfg.SubPool = d.IPRange + iCfg.Gateway = d.Gateway + iCfg.AuxAddresses = d.AuxAddress + ip, _, err := net.ParseCIDR(d.Subnet) + if err != nil { + return nil, nil, fmt.Errorf("Invalid subnet %s : %v", d.Subnet, err) + } + if ip.To4() != nil { + ipamV4Cfg = append(ipamV4Cfg, &iCfg) + } else { + ipamV6Cfg = append(ipamV6Cfg, &iCfg) + } + } + return ipamV4Cfg, ipamV6Cfg, nil +} + +// ConnectContainerToNetwork connects the given container to the given +// network. If either cannot be found, an err is returned. If the +// network cannot be set up, an err is returned. +func (daemon *Daemon) ConnectContainerToNetwork(containerName, networkName string, endpointConfig *network.EndpointSettings) error { + container, err := daemon.GetContainer(containerName) + if err != nil { + return err + } + return daemon.ConnectToNetwork(container, networkName, endpointConfig) +} + +// DisconnectContainerFromNetwork disconnects the given container from +// the given network. If either cannot be found, an err is returned. +func (daemon *Daemon) DisconnectContainerFromNetwork(containerName string, network libnetwork.Network, force bool) error { + container, err := daemon.GetContainer(containerName) + if err != nil { + if force { + return daemon.ForceEndpointDelete(containerName, network) + } + return err + } + return daemon.DisconnectFromNetwork(container, network, force) +} + +// GetNetworkDriverList returns the list of plugins drivers +// registered for network. +func (daemon *Daemon) GetNetworkDriverList() map[string]bool { + pluginList := make(map[string]bool) + + if !daemon.NetworkControllerEnabled() { + return nil + } + c := daemon.netController + networks := c.Networks() + + for _, network := range networks { + driver := network.Type() + pluginList[driver] = true + } + + return pluginList +} + +// DeleteNetwork destroys a network unless it's one of docker's predefined networks. +func (daemon *Daemon) DeleteNetwork(networkID string) error { + nw, err := daemon.FindNetwork(networkID) + if err != nil { + return err + } + + if runconfig.IsPreDefinedNetwork(nw.Name()) { + err := fmt.Errorf("%s is a pre-defined network and cannot be removed", nw.Name()) + return errors.NewErrorWithStatusCode(err, http.StatusForbidden) + } + + if err := nw.Delete(); err != nil { + return err + } + daemon.LogNetworkEvent(nw, "destroy") + return nil +} diff --git a/daemon/network/settings.go b/daemon/network/settings.go new file mode 100644 index 00000000..823bec26 --- /dev/null +++ b/daemon/network/settings.go @@ -0,0 +1,22 @@ +package network + +import ( + networktypes "github.com/docker/engine-api/types/network" + "github.com/docker/go-connections/nat" +) + +// Settings stores configuration details about the daemon network config +// TODO Windows. Many of these fields can be factored out., +type Settings struct { + Bridge string + SandboxID string + HairpinMode bool + LinkLocalIPv6Address string + LinkLocalIPv6PrefixLen int + Networks map[string]*networktypes.EndpointSettings + Ports nat.PortMap + SandboxKey string + SecondaryIPAddresses []networktypes.Address + SecondaryIPv6Addresses []networktypes.Address + IsAnonymousEndpoint bool +} diff --git a/daemon/oci_linux.go b/daemon/oci_linux.go new file mode 100644 index 00000000..0c3636ce --- /dev/null +++ b/daemon/oci_linux.go @@ -0,0 +1,687 @@ +package daemon + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/container" + "github.com/docker/docker/daemon/caps" + "github.com/docker/docker/libcontainerd" + "github.com/docker/docker/oci" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/mount" + "github.com/docker/docker/pkg/stringutils" + "github.com/docker/docker/pkg/symlink" + "github.com/docker/docker/volume" + containertypes "github.com/docker/engine-api/types/container" + "github.com/opencontainers/runc/libcontainer/apparmor" + "github.com/opencontainers/runc/libcontainer/devices" + "github.com/opencontainers/runc/libcontainer/user" + "github.com/opencontainers/specs/specs-go" +) + +func setResources(s *specs.Spec, r containertypes.Resources) error { + weightDevices, err := getBlkioWeightDevices(r) + if err != nil { + return err + } + readBpsDevice, err := getBlkioReadBpsDevices(r) + if err != nil { + return err + } + writeBpsDevice, err := getBlkioWriteBpsDevices(r) + if err != nil { + return err + } + readIOpsDevice, err := getBlkioReadIOpsDevices(r) + if err != nil { + return err + } + writeIOpsDevice, err := getBlkioWriteIOpsDevices(r) + if err != nil { + return err + } + + memoryRes := getMemoryResources(r) + cpuRes := getCPUResources(r) + blkioWeight := r.BlkioWeight + + specResources := &specs.Resources{ + Memory: memoryRes, + CPU: cpuRes, + BlockIO: &specs.BlockIO{ + Weight: &blkioWeight, + WeightDevice: weightDevices, + ThrottleReadBpsDevice: readBpsDevice, + ThrottleWriteBpsDevice: writeBpsDevice, + ThrottleReadIOPSDevice: readIOpsDevice, + ThrottleWriteIOPSDevice: writeIOpsDevice, + }, + DisableOOMKiller: r.OomKillDisable, + Pids: &specs.Pids{ + Limit: &r.PidsLimit, + }, + } + + if s.Linux.Resources != nil && len(s.Linux.Resources.Devices) > 0 { + specResources.Devices = s.Linux.Resources.Devices + } + + s.Linux.Resources = specResources + return nil +} + +func setDevices(s *specs.Spec, c *container.Container) error { + // Build lists of devices allowed and created within the container. + var devs []specs.Device + devPermissions := s.Linux.Resources.Devices + if c.HostConfig.Privileged { + hostDevices, err := devices.HostDevices() + if err != nil { + return err + } + for _, d := range hostDevices { + devs = append(devs, specDevice(d)) + } + rwm := "rwm" + devPermissions = []specs.DeviceCgroup{ + { + Allow: true, + Access: &rwm, + }, + } + } else { + for _, deviceMapping := range c.HostConfig.Devices { + d, dPermissions, err := getDevicesFromPath(deviceMapping) + if err != nil { + return err + } + devs = append(devs, d...) + devPermissions = append(devPermissions, dPermissions...) + } + } + + s.Linux.Devices = append(s.Linux.Devices, devs...) + s.Linux.Resources.Devices = devPermissions + return nil +} + +func setRlimits(daemon *Daemon, s *specs.Spec, c *container.Container) error { + var rlimits []specs.Rlimit + + ulimits := c.HostConfig.Ulimits + // Merge ulimits with daemon defaults + ulIdx := make(map[string]struct{}) + for _, ul := range ulimits { + ulIdx[ul.Name] = struct{}{} + } + for name, ul := range daemon.configStore.Ulimits { + if _, exists := ulIdx[name]; !exists { + ulimits = append(ulimits, ul) + } + } + + for _, ul := range ulimits { + rlimits = append(rlimits, specs.Rlimit{ + Type: "RLIMIT_" + strings.ToUpper(ul.Name), + Soft: uint64(ul.Soft), + Hard: uint64(ul.Hard), + }) + } + + s.Process.Rlimits = rlimits + return nil +} + +func setUser(s *specs.Spec, c *container.Container) error { + uid, gid, additionalGids, err := getUser(c, c.Config.User) + if err != nil { + return err + } + s.Process.User.UID = uid + s.Process.User.GID = gid + s.Process.User.AdditionalGids = additionalGids + return nil +} + +func readUserFile(c *container.Container, p string) (io.ReadCloser, error) { + fp, err := symlink.FollowSymlinkInScope(filepath.Join(c.BaseFS, p), c.BaseFS) + if err != nil { + return nil, err + } + return os.Open(fp) +} + +func getUser(c *container.Container, username string) (uint32, uint32, []uint32, error) { + passwdPath, err := user.GetPasswdPath() + if err != nil { + return 0, 0, nil, err + } + groupPath, err := user.GetGroupPath() + if err != nil { + return 0, 0, nil, err + } + passwdFile, err := readUserFile(c, passwdPath) + if err == nil { + defer passwdFile.Close() + } + groupFile, err := readUserFile(c, groupPath) + if err == nil { + defer groupFile.Close() + } + + execUser, err := user.GetExecUser(username, nil, passwdFile, groupFile) + if err != nil { + return 0, 0, nil, err + } + + // todo: fix this double read by a change to libcontainer/user pkg + groupFile, err = readUserFile(c, groupPath) + if err == nil { + defer groupFile.Close() + } + var addGroups []int + if len(c.HostConfig.GroupAdd) > 0 { + addGroups, err = user.GetAdditionalGroups(c.HostConfig.GroupAdd, groupFile) + if err != nil { + return 0, 0, nil, err + } + } + uid := uint32(execUser.Uid) + gid := uint32(execUser.Gid) + sgids := append(execUser.Sgids, addGroups...) + var additionalGids []uint32 + for _, g := range sgids { + additionalGids = append(additionalGids, uint32(g)) + } + return uid, gid, additionalGids, nil +} + +func setNamespace(s *specs.Spec, ns specs.Namespace) { + for i, n := range s.Linux.Namespaces { + if n.Type == ns.Type { + s.Linux.Namespaces[i] = ns + return + } + } + s.Linux.Namespaces = append(s.Linux.Namespaces, ns) +} + +func setCapabilities(s *specs.Spec, c *container.Container) error { + var caplist []string + var err error + if c.HostConfig.Privileged { + caplist = caps.GetAllCapabilities() + } else { + caplist, err = caps.TweakCapabilities(s.Process.Capabilities, c.HostConfig.CapAdd, c.HostConfig.CapDrop) + if err != nil { + return err + } + } + s.Process.Capabilities = caplist + return nil +} + +func delNamespace(s *specs.Spec, nsType specs.NamespaceType) { + idx := -1 + for i, n := range s.Linux.Namespaces { + if n.Type == nsType { + idx = i + } + } + if idx >= 0 { + s.Linux.Namespaces = append(s.Linux.Namespaces[:idx], s.Linux.Namespaces[idx+1:]...) + } +} + +func setNamespaces(daemon *Daemon, s *specs.Spec, c *container.Container) error { + userNS := false + // user + if c.HostConfig.UsernsMode.IsPrivate() { + uidMap, gidMap := daemon.GetUIDGIDMaps() + if uidMap != nil { + userNS = true + ns := specs.Namespace{Type: "user"} + setNamespace(s, ns) + s.Linux.UIDMappings = specMapping(uidMap) + s.Linux.GIDMappings = specMapping(gidMap) + } + } + // network + if !c.Config.NetworkDisabled { + ns := specs.Namespace{Type: "network"} + parts := strings.SplitN(string(c.HostConfig.NetworkMode), ":", 2) + if parts[0] == "container" { + nc, err := daemon.getNetworkedContainer(c.ID, c.HostConfig.NetworkMode.ConnectedContainer()) + if err != nil { + return err + } + ns.Path = fmt.Sprintf("/proc/%d/ns/net", nc.State.GetPID()) + if userNS { + // to share a net namespace, they must also share a user namespace + nsUser := specs.Namespace{Type: "user"} + nsUser.Path = fmt.Sprintf("/proc/%d/ns/user", nc.State.GetPID()) + setNamespace(s, nsUser) + } + } else if c.HostConfig.NetworkMode.IsHost() { + ns.Path = c.NetworkSettings.SandboxKey + } + setNamespace(s, ns) + } + // ipc + if c.HostConfig.IpcMode.IsContainer() { + ns := specs.Namespace{Type: "ipc"} + ic, err := daemon.getIpcContainer(c) + if err != nil { + return err + } + ns.Path = fmt.Sprintf("/proc/%d/ns/ipc", ic.State.GetPID()) + setNamespace(s, ns) + if userNS { + // to share an IPC namespace, they must also share a user namespace + nsUser := specs.Namespace{Type: "user"} + nsUser.Path = fmt.Sprintf("/proc/%d/ns/user", ic.State.GetPID()) + setNamespace(s, nsUser) + } + } else if c.HostConfig.IpcMode.IsHost() { + delNamespace(s, specs.NamespaceType("ipc")) + } else { + ns := specs.Namespace{Type: "ipc"} + setNamespace(s, ns) + } + // pid + if c.HostConfig.PidMode.IsHost() { + delNamespace(s, specs.NamespaceType("pid")) + } + // uts + if c.HostConfig.UTSMode.IsHost() { + delNamespace(s, specs.NamespaceType("uts")) + s.Hostname = "" + } + + return nil +} + +func specMapping(s []idtools.IDMap) []specs.IDMapping { + var ids []specs.IDMapping + for _, item := range s { + ids = append(ids, specs.IDMapping{ + HostID: uint32(item.HostID), + ContainerID: uint32(item.ContainerID), + Size: uint32(item.Size), + }) + } + return ids +} + +func getMountInfo(mountinfo []*mount.Info, dir string) *mount.Info { + for _, m := range mountinfo { + if m.Mountpoint == dir { + return m + } + } + return nil +} + +// Get the source mount point of directory passed in as argument. Also return +// optional fields. +func getSourceMount(source string) (string, string, error) { + // Ensure any symlinks are resolved. + sourcePath, err := filepath.EvalSymlinks(source) + if err != nil { + return "", "", err + } + + mountinfos, err := mount.GetMounts() + if err != nil { + return "", "", err + } + + mountinfo := getMountInfo(mountinfos, sourcePath) + if mountinfo != nil { + return sourcePath, mountinfo.Optional, nil + } + + path := sourcePath + for { + path = filepath.Dir(path) + + mountinfo = getMountInfo(mountinfos, path) + if mountinfo != nil { + return path, mountinfo.Optional, nil + } + + if path == "/" { + break + } + } + + // If we are here, we did not find parent mount. Something is wrong. + return "", "", fmt.Errorf("Could not find source mount of %s", source) +} + +// Ensure mount point on which path is mounted, is shared. +func ensureShared(path string) error { + sharedMount := false + + sourceMount, optionalOpts, err := getSourceMount(path) + if err != nil { + return err + } + // Make sure source mount point is shared. + optsSplit := strings.Split(optionalOpts, " ") + for _, opt := range optsSplit { + if strings.HasPrefix(opt, "shared:") { + sharedMount = true + break + } + } + + if !sharedMount { + return fmt.Errorf("Path %s is mounted on %s but it is not a shared mount.", path, sourceMount) + } + return nil +} + +// Ensure mount point on which path is mounted, is either shared or slave. +func ensureSharedOrSlave(path string) error { + sharedMount := false + slaveMount := false + + sourceMount, optionalOpts, err := getSourceMount(path) + if err != nil { + return err + } + // Make sure source mount point is shared. + optsSplit := strings.Split(optionalOpts, " ") + for _, opt := range optsSplit { + if strings.HasPrefix(opt, "shared:") { + sharedMount = true + break + } else if strings.HasPrefix(opt, "master:") { + slaveMount = true + break + } + } + + if !sharedMount && !slaveMount { + return fmt.Errorf("Path %s is mounted on %s but it is not a shared or slave mount.", path, sourceMount) + } + return nil +} + +var ( + mountPropagationMap = map[string]int{ + "private": mount.PRIVATE, + "rprivate": mount.RPRIVATE, + "shared": mount.SHARED, + "rshared": mount.RSHARED, + "slave": mount.SLAVE, + "rslave": mount.RSLAVE, + } + + mountPropagationReverseMap = map[int]string{ + mount.PRIVATE: "private", + mount.RPRIVATE: "rprivate", + mount.SHARED: "shared", + mount.RSHARED: "rshared", + mount.SLAVE: "slave", + mount.RSLAVE: "rslave", + } +) + +func setMounts(daemon *Daemon, s *specs.Spec, c *container.Container, mounts []container.Mount) error { + userMounts := make(map[string]struct{}) + for _, m := range mounts { + userMounts[m.Destination] = struct{}{} + } + + // Filter out mounts that are overriden by user supplied mounts + var defaultMounts []specs.Mount + _, mountDev := userMounts["/dev"] + for _, m := range s.Mounts { + if _, ok := userMounts[m.Destination]; !ok { + if mountDev && strings.HasPrefix(m.Destination, "/dev/") { + continue + } + defaultMounts = append(defaultMounts, m) + } + } + + s.Mounts = defaultMounts + for _, m := range mounts { + for _, cm := range s.Mounts { + if cm.Destination == m.Destination { + return fmt.Errorf("Duplicate mount point '%s'", m.Destination) + } + } + + if m.Source == "tmpfs" { + opt := []string{"noexec", "nosuid", "nodev", volume.DefaultPropagationMode} + if m.Data != "" { + opt = append(opt, strings.Split(m.Data, ",")...) + } else { + opt = append(opt, "size=65536k") + } + + s.Mounts = append(s.Mounts, specs.Mount{Destination: m.Destination, Source: m.Source, Type: "tmpfs", Options: opt}) + continue + } + + mt := specs.Mount{Destination: m.Destination, Source: m.Source, Type: "bind"} + + // Determine property of RootPropagation based on volume + // properties. If a volume is shared, then keep root propagation + // shared. This should work for slave and private volumes too. + // + // For slave volumes, it can be either [r]shared/[r]slave. + // + // For private volumes any root propagation value should work. + pFlag := mountPropagationMap[m.Propagation] + if pFlag == mount.SHARED || pFlag == mount.RSHARED { + if err := ensureShared(m.Source); err != nil { + return err + } + rootpg := mountPropagationMap[s.Linux.RootfsPropagation] + if rootpg != mount.SHARED && rootpg != mount.RSHARED { + s.Linux.RootfsPropagation = mountPropagationReverseMap[mount.SHARED] + } + } else if pFlag == mount.SLAVE || pFlag == mount.RSLAVE { + if err := ensureSharedOrSlave(m.Source); err != nil { + return err + } + rootpg := mountPropagationMap[s.Linux.RootfsPropagation] + if rootpg != mount.SHARED && rootpg != mount.RSHARED && rootpg != mount.SLAVE && rootpg != mount.RSLAVE { + s.Linux.RootfsPropagation = mountPropagationReverseMap[mount.RSLAVE] + } + } + + opts := []string{"rbind"} + if !m.Writable { + opts = append(opts, "ro") + } + if pFlag != 0 { + opts = append(opts, mountPropagationReverseMap[pFlag]) + } + + mt.Options = opts + s.Mounts = append(s.Mounts, mt) + } + + if s.Root.Readonly { + for i, m := range s.Mounts { + switch m.Destination { + case "/proc", "/dev/pts", "/dev/mqueue": // /dev is remounted by runc + continue + } + if _, ok := userMounts[m.Destination]; !ok { + if !stringutils.InSlice(m.Options, "ro") { + s.Mounts[i].Options = append(s.Mounts[i].Options, "ro") + } + } + } + } + + if c.HostConfig.Privileged { + if !s.Root.Readonly { + // clear readonly for /sys + for i := range s.Mounts { + if s.Mounts[i].Destination == "/sys" { + clearReadOnly(&s.Mounts[i]) + } + } + } + s.Linux.ReadonlyPaths = nil + s.Linux.MaskedPaths = nil + } + + // TODO: until a kernel/mount solution exists for handling remount in a user namespace, + // we must clear the readonly flag for the cgroups mount (@mrunalp concurs) + if uidMap, _ := daemon.GetUIDGIDMaps(); uidMap != nil || c.HostConfig.Privileged { + for i, m := range s.Mounts { + if m.Type == "cgroup" { + clearReadOnly(&s.Mounts[i]) + } + } + } + + return nil +} + +func (daemon *Daemon) populateCommonSpec(s *specs.Spec, c *container.Container) error { + linkedEnv, err := daemon.setupLinkedContainers(c) + if err != nil { + return err + } + s.Root = specs.Root{ + Path: c.BaseFS, + Readonly: c.HostConfig.ReadonlyRootfs, + } + rootUID, rootGID := daemon.GetRemappedUIDGID() + if err := c.SetupWorkingDirectory(rootUID, rootGID); err != nil { + return err + } + cwd := c.Config.WorkingDir + if len(cwd) == 0 { + cwd = "/" + } + s.Process.Args = append([]string{c.Path}, c.Args...) + s.Process.Cwd = cwd + s.Process.Env = c.CreateDaemonEnvironment(linkedEnv) + s.Process.Terminal = c.Config.Tty + s.Hostname = c.FullHostname() + + return nil +} + +func (daemon *Daemon) createSpec(c *container.Container) (*libcontainerd.Spec, error) { + s := oci.DefaultSpec() + if err := daemon.populateCommonSpec(&s, c); err != nil { + return nil, err + } + + var cgroupsPath string + scopePrefix := "docker" + parent := "/docker" + useSystemd := UsingSystemd(daemon.configStore) + if useSystemd { + parent = "system.slice" + } + + if c.HostConfig.CgroupParent != "" { + parent = c.HostConfig.CgroupParent + } else if daemon.configStore.CgroupParent != "" { + parent = daemon.configStore.CgroupParent + } + + if useSystemd { + cgroupsPath = parent + ":" + scopePrefix + ":" + c.ID + logrus.Debugf("createSpec: cgroupsPath: %s", cgroupsPath) + } else { + cgroupsPath = filepath.Join(parent, c.ID) + } + s.Linux.CgroupsPath = &cgroupsPath + + if err := setResources(&s, c.HostConfig.Resources); err != nil { + return nil, fmt.Errorf("linux runtime spec resources: %v", err) + } + s.Linux.Resources.OOMScoreAdj = &c.HostConfig.OomScoreAdj + if err := setDevices(&s, c); err != nil { + return nil, fmt.Errorf("linux runtime spec devices: %v", err) + } + if err := setRlimits(daemon, &s, c); err != nil { + return nil, fmt.Errorf("linux runtime spec rlimits: %v", err) + } + if err := setUser(&s, c); err != nil { + return nil, fmt.Errorf("linux spec user: %v", err) + } + if err := setNamespaces(daemon, &s, c); err != nil { + return nil, fmt.Errorf("linux spec namespaces: %v", err) + } + if err := setCapabilities(&s, c); err != nil { + return nil, fmt.Errorf("linux spec capabilities: %v", err) + } + if err := setSeccomp(daemon, &s, c); err != nil { + return nil, fmt.Errorf("linux seccomp: %v", err) + } + + if err := daemon.setupIpcDirs(c); err != nil { + return nil, err + } + + mounts, err := daemon.setupMounts(c) + if err != nil { + return nil, err + } + mounts = append(mounts, c.IpcMounts()...) + mounts = append(mounts, c.TmpfsMounts()...) + if err := setMounts(daemon, &s, c, mounts); err != nil { + return nil, fmt.Errorf("linux mounts: %v", err) + } + + for _, ns := range s.Linux.Namespaces { + if ns.Type == "network" && ns.Path == "" && !c.Config.NetworkDisabled { + target, err := os.Readlink(filepath.Join("/proc", strconv.Itoa(os.Getpid()), "exe")) + if err != nil { + return nil, err + } + + s.Hooks = specs.Hooks{ + Prestart: []specs.Hook{{ + Path: target, // FIXME: cross-platform + Args: []string{"libnetwork-setkey", c.ID, daemon.netController.ID()}, + }}, + } + } + } + + if apparmor.IsEnabled() { + appArmorProfile := "docker-default" + if len(c.AppArmorProfile) > 0 { + appArmorProfile = c.AppArmorProfile + } else if c.HostConfig.Privileged { + appArmorProfile = "unconfined" + } + s.Process.ApparmorProfile = appArmorProfile + } + s.Process.SelinuxLabel = c.GetProcessLabel() + s.Process.NoNewPrivileges = c.NoNewPrivileges + s.Linux.MountLabel = c.MountLabel + + return (*libcontainerd.Spec)(&s), nil +} + +func clearReadOnly(m *specs.Mount) { + var opt []string + for _, o := range m.Options { + if o != "ro" { + opt = append(opt, o) + } + } + m.Options = opt +} diff --git a/daemon/oci_windows.go b/daemon/oci_windows.go new file mode 100644 index 00000000..5bf3f824 --- /dev/null +++ b/daemon/oci_windows.go @@ -0,0 +1,187 @@ +package daemon + +import ( + "fmt" + "strings" + "syscall" + + "github.com/docker/docker/container" + "github.com/docker/docker/layer" + "github.com/docker/docker/libcontainerd" + "github.com/docker/docker/libcontainerd/windowsoci" + "github.com/docker/docker/oci" +) + +func (daemon *Daemon) createSpec(c *container.Container) (*libcontainerd.Spec, error) { + s := oci.DefaultSpec() + + linkedEnv, err := daemon.setupLinkedContainers(c) + if err != nil { + return nil, err + } + + // TODO Windows - this can be removed. Not used (UID/GID) + rootUID, rootGID := daemon.GetRemappedUIDGID() + if err := c.SetupWorkingDirectory(rootUID, rootGID); err != nil { + return nil, err + } + + img, err := daemon.imageStore.Get(c.ImageID) + if err != nil { + return nil, fmt.Errorf("Failed to graph.Get on ImageID %s - %s", c.ImageID, err) + } + + // In base spec + s.Hostname = c.FullHostname() + + // In s.Mounts + mounts, err := daemon.setupMounts(c) + if err != nil { + return nil, err + } + for _, mount := range mounts { + s.Mounts = append(s.Mounts, windowsoci.Mount{ + Source: mount.Source, + Destination: mount.Destination, + Readonly: !mount.Writable, + }) + } + + // Are we going to run as a Hyper-V container? + hv := false + if c.HostConfig.Isolation.IsDefault() { + // Container is set to use the default, so take the default from the daemon configuration + hv = daemon.defaultIsolation.IsHyperV() + } else { + // Container is requesting an isolation mode. Honour it. + hv = c.HostConfig.Isolation.IsHyperV() + } + if hv { + // TODO We don't yet have the ImagePath hooked up. But set to + // something non-nil to pickup in libcontainerd. + s.Windows.HvRuntime = &windowsoci.HvRuntime{} + } + + // In s.Process + if c.Config.ArgsEscaped { + s.Process.Args = append([]string{c.Path}, c.Args...) + } else { + // TODO (jstarks): escape the entrypoint too once the tests are fixed to not rely on this behavior + s.Process.Args = append([]string{c.Path}, escapeArgs(c.Args)...) + } + s.Process.Cwd = c.Config.WorkingDir + s.Process.Env = c.CreateDaemonEnvironment(linkedEnv) + s.Process.InitialConsoleSize = c.HostConfig.ConsoleSize + s.Process.Terminal = c.Config.Tty + s.Process.User.User = c.Config.User + + // In spec.Root + s.Root.Path = c.BaseFS + s.Root.Readonly = c.HostConfig.ReadonlyRootfs + + // In s.Windows + s.Windows.FirstStart = !c.HasBeenStartedBefore + + // s.Windows.LayerFolder. + m, err := c.RWLayer.Metadata() + if err != nil { + return nil, fmt.Errorf("Failed to get layer metadata - %s", err) + } + s.Windows.LayerFolder = m["dir"] + + // s.Windows.LayerPaths + var layerPaths []string + if img.RootFS != nil && img.RootFS.Type == "layers+base" { + max := len(img.RootFS.DiffIDs) + for i := 0; i <= max; i++ { + img.RootFS.DiffIDs = img.RootFS.DiffIDs[:i] + path, err := layer.GetLayerPath(daemon.layerStore, img.RootFS.ChainID()) + if err != nil { + return nil, fmt.Errorf("Failed to get layer path from graphdriver %s for ImageID %s - %s", daemon.layerStore, img.RootFS.ChainID(), err) + } + // Reverse order, expecting parent most first + layerPaths = append([]string{path}, layerPaths...) + } + } + s.Windows.LayerPaths = layerPaths + + // In s.Windows.Networking (TP5+ libnetwork way of doing things) + // Connect all the libnetwork allocated networks to the container + var epList []string + if c.NetworkSettings != nil { + for n := range c.NetworkSettings.Networks { + sn, err := daemon.FindNetwork(n) + if err != nil { + continue + } + + ep, err := c.GetEndpointInNetwork(sn) + if err != nil { + continue + } + + data, err := ep.DriverInfo() + if err != nil { + continue + } + if data["hnsid"] != nil { + epList = append(epList, data["hnsid"].(string)) + } + } + } + s.Windows.Networking = &windowsoci.Networking{ + EndpointList: epList, + } + + // In s.Windows.Networking (TP4 back compat) + // TODO Windows: Post TP4 - Remove this along with definitions from spec + // and changes to libcontainerd to not read these fields. + if daemon.netController == nil { + parts := strings.SplitN(string(c.HostConfig.NetworkMode), ":", 2) + switch parts[0] { + case "none": + case "default", "": // empty string to support existing containers + if !c.Config.NetworkDisabled { + s.Windows.Networking = &windowsoci.Networking{ + MacAddress: c.Config.MacAddress, + Bridge: daemon.configStore.bridgeConfig.Iface, + PortBindings: c.HostConfig.PortBindings, + } + } + default: + return nil, fmt.Errorf("invalid network mode: %s", c.HostConfig.NetworkMode) + } + } + + // In s.Windows.Resources + // @darrenstahlmsft implement these resources + cpuShares := uint64(c.HostConfig.CPUShares) + s.Windows.Resources = &windowsoci.Resources{ + CPU: &windowsoci.CPU{ + //TODO Count: ..., + //TODO Percent: ..., + Shares: &cpuShares, + }, + Memory: &windowsoci.Memory{ + //TODO Limit: ..., + //TODO Reservation: ..., + }, + Network: &windowsoci.Network{ + //TODO Bandwidth: ..., + }, + Storage: &windowsoci.Storage{ + //TODO Bps: ..., + //TODO Iops: ..., + //TODO SandboxSize: ..., + }, + } + return (*libcontainerd.Spec)(&s), nil +} + +func escapeArgs(args []string) []string { + escapedArgs := make([]string, len(args)) + for i, a := range args { + escapedArgs[i] = syscall.EscapeArg(a) + } + return escapedArgs +} diff --git a/daemon/pause.go b/daemon/pause.go new file mode 100644 index 00000000..dbfafbc5 --- /dev/null +++ b/daemon/pause.go @@ -0,0 +1,49 @@ +package daemon + +import ( + "fmt" + + "github.com/docker/docker/container" +) + +// ContainerPause pauses a container +func (daemon *Daemon) ContainerPause(name string) error { + container, err := daemon.GetContainer(name) + if err != nil { + return err + } + + if err := daemon.containerPause(container); err != nil { + return err + } + + return nil +} + +// containerPause pauses the container execution without stopping the process. +// The execution can be resumed by calling containerUnpause. +func (daemon *Daemon) containerPause(container *container.Container) error { + container.Lock() + defer container.Unlock() + + // We cannot Pause the container which is not running + if !container.Running { + return errNotRunning{container.ID} + } + + // We cannot Pause the container which is already paused + if container.Paused { + return fmt.Errorf("Container %s is already paused", container.ID) + } + + // We cannot Pause the container which is restarting + if container.Restarting { + return errContainerIsRestarting(container.ID) + } + + if err := daemon.containerd.Pause(container.ID); err != nil { + return fmt.Errorf("Cannot pause container %s: %s", container.ID, err) + } + + return nil +} diff --git a/daemon/rename.go b/daemon/rename.go new file mode 100644 index 00000000..363a7f8b --- /dev/null +++ b/daemon/rename.go @@ -0,0 +1,85 @@ +package daemon + +import ( + "fmt" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/libnetwork" +) + +// ContainerRename changes the name of a container, using the oldName +// to find the container. An error is returned if newName is already +// reserved. +func (daemon *Daemon) ContainerRename(oldName, newName string) error { + var ( + sid string + sb libnetwork.Sandbox + ) + + if oldName == "" || newName == "" { + return fmt.Errorf("Neither old nor new names may be empty") + } + + container, err := daemon.GetContainer(oldName) + if err != nil { + return err + } + + oldName = container.Name + + container.Lock() + defer container.Unlock() + if newName, err = daemon.reserveName(container.ID, newName); err != nil { + return fmt.Errorf("Error when allocating new name: %v", err) + } + + container.Name = newName + + defer func() { + if err != nil { + container.Name = oldName + daemon.reserveName(container.ID, oldName) + daemon.releaseName(newName) + } + }() + + daemon.releaseName(oldName) + if err = container.ToDisk(); err != nil { + return err + } + + attributes := map[string]string{ + "oldName": oldName, + } + + if !container.Running { + daemon.LogContainerEventWithAttributes(container, "rename", attributes) + return nil + } + + defer func() { + if err != nil { + container.Name = oldName + if e := container.ToDisk(); e != nil { + logrus.Errorf("%s: Failed in writing to Disk on rename failure: %v", container.ID, e) + } + } + }() + + sid = container.NetworkSettings.SandboxID + if daemon.netController != nil { + sb, err = daemon.netController.SandboxByID(sid) + if err != nil { + return err + } + + err = sb.Rename(strings.TrimPrefix(container.Name, "/")) + if err != nil { + return err + } + } + + daemon.LogContainerEventWithAttributes(container, "rename", attributes) + return nil +} diff --git a/daemon/resize.go b/daemon/resize.go new file mode 100644 index 00000000..74735385 --- /dev/null +++ b/daemon/resize.go @@ -0,0 +1,40 @@ +package daemon + +import ( + "fmt" + + "github.com/docker/docker/libcontainerd" +) + +// ContainerResize changes the size of the TTY of the process running +// in the container with the given name to the given height and width. +func (daemon *Daemon) ContainerResize(name string, height, width int) error { + container, err := daemon.GetContainer(name) + if err != nil { + return err + } + + if !container.IsRunning() { + return errNotRunning{container.ID} + } + + if err = daemon.containerd.Resize(container.ID, libcontainerd.InitFriendlyName, width, height); err == nil { + attributes := map[string]string{ + "height": fmt.Sprintf("%d", height), + "width": fmt.Sprintf("%d", width), + } + daemon.LogContainerEventWithAttributes(container, "resize", attributes) + } + return err +} + +// ContainerExecResize changes the size of the TTY of the process +// running in the exec with the given name to the given height and +// width. +func (daemon *Daemon) ContainerExecResize(name string, height, width int) error { + ec, err := daemon.getExecConfig(name) + if err != nil { + return err + } + return daemon.containerd.Resize(ec.ContainerID, ec.ID, width, height) +} diff --git a/daemon/restart.go b/daemon/restart.go new file mode 100644 index 00000000..3779116c --- /dev/null +++ b/daemon/restart.go @@ -0,0 +1,48 @@ +package daemon + +import ( + "fmt" + + "github.com/docker/docker/container" +) + +// ContainerRestart stops and starts a container. It attempts to +// gracefully stop the container within the given timeout, forcefully +// stopping it if the timeout is exceeded. If given a negative +// timeout, ContainerRestart will wait forever until a graceful +// stop. Returns an error if the container cannot be found, or if +// there is an underlying error at any stage of the restart. +func (daemon *Daemon) ContainerRestart(name string, seconds int) error { + container, err := daemon.GetContainer(name) + if err != nil { + return err + } + if err := daemon.containerRestart(container, seconds); err != nil { + return fmt.Errorf("Cannot restart container %s: %v", name, err) + } + return nil +} + +// containerRestart attempts to gracefully stop and then start the +// container. When stopping, wait for the given duration in seconds to +// gracefully stop, before forcefully terminating the container. If +// given a negative duration, wait forever for a graceful stop. +func (daemon *Daemon) containerRestart(container *container.Container, seconds int) error { + // Avoid unnecessarily unmounting and then directly mounting + // the container when the container stops and then starts + // again + if err := daemon.Mount(container); err == nil { + defer daemon.Unmount(container) + } + + if err := daemon.containerStop(container, seconds); err != nil { + return err + } + + if err := daemon.containerStart(container); err != nil { + return err + } + + daemon.LogContainerEvent(container, "restart") + return nil +} diff --git a/daemon/seccomp_disabled.go b/daemon/seccomp_disabled.go new file mode 100644 index 00000000..620eee29 --- /dev/null +++ b/daemon/seccomp_disabled.go @@ -0,0 +1,12 @@ +// +build !seccomp,!windows + +package daemon + +import ( + "github.com/docker/docker/container" + "github.com/opencontainers/specs/specs-go" +) + +func setSeccomp(daemon *Daemon, rs *specs.Spec, c *container.Container) error { + return nil +} diff --git a/daemon/seccomp_linux.go b/daemon/seccomp_linux.go new file mode 100644 index 00000000..659a15de --- /dev/null +++ b/daemon/seccomp_linux.go @@ -0,0 +1,46 @@ +// +build linux,seccomp + +package daemon + +import ( + "fmt" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/container" + "github.com/docker/docker/profiles/seccomp" + "github.com/opencontainers/specs/specs-go" +) + +func setSeccomp(daemon *Daemon, rs *specs.Spec, c *container.Container) error { + var profile *specs.Seccomp + var err error + + if c.HostConfig.Privileged { + return nil + } + + if !daemon.seccompEnabled { + if c.SeccompProfile != "" && c.SeccompProfile != "unconfined" { + return fmt.Errorf("Seccomp is not enabled in your kernel, cannot run a custom seccomp profile.") + } + logrus.Warn("Seccomp is not enabled in your kernel, running container without default profile.") + c.SeccompProfile = "unconfined" + } + if c.SeccompProfile == "unconfined" { + return nil + } + if c.SeccompProfile != "" { + profile, err = seccomp.LoadProfile(c.SeccompProfile) + if err != nil { + return err + } + } else { + profile, err = seccomp.GetDefaultProfile() + if err != nil { + return err + } + } + + rs.Linux.Seccomp = profile + return nil +} diff --git a/daemon/selinux_linux.go b/daemon/selinux_linux.go new file mode 100644 index 00000000..83a34471 --- /dev/null +++ b/daemon/selinux_linux.go @@ -0,0 +1,17 @@ +// +build linux + +package daemon + +import "github.com/opencontainers/runc/libcontainer/selinux" + +func selinuxSetDisabled() { + selinux.SetDisabled() +} + +func selinuxFreeLxcContexts(label string) { + selinux.FreeLxcContexts(label) +} + +func selinuxEnabled() bool { + return selinux.SelinuxEnabled() +} diff --git a/daemon/selinux_unsupported.go b/daemon/selinux_unsupported.go new file mode 100644 index 00000000..25a56ad1 --- /dev/null +++ b/daemon/selinux_unsupported.go @@ -0,0 +1,13 @@ +// +build !linux + +package daemon + +func selinuxSetDisabled() { +} + +func selinuxFreeLxcContexts(label string) { +} + +func selinuxEnabled() bool { + return false +} diff --git a/daemon/start.go b/daemon/start.go new file mode 100644 index 00000000..1b34f426 --- /dev/null +++ b/daemon/start.go @@ -0,0 +1,185 @@ +package daemon + +import ( + "fmt" + "net/http" + "runtime" + "strings" + "syscall" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/container" + "github.com/docker/docker/errors" + "github.com/docker/docker/libcontainerd" + "github.com/docker/docker/runconfig" + containertypes "github.com/docker/engine-api/types/container" +) + +// ContainerStart starts a container. +func (daemon *Daemon) ContainerStart(name string, hostConfig *containertypes.HostConfig) error { + container, err := daemon.GetContainer(name) + if err != nil { + return err + } + + if container.IsPaused() { + return fmt.Errorf("Cannot start a paused container, try unpause instead.") + } + + if container.IsRunning() { + err := fmt.Errorf("Container already started") + return errors.NewErrorWithStatusCode(err, http.StatusNotModified) + } + + // Windows does not have the backwards compatibility issue here. + if runtime.GOOS != "windows" { + // This is kept for backward compatibility - hostconfig should be passed when + // creating a container, not during start. + if hostConfig != nil { + logrus.Warn("DEPRECATED: Setting host configuration options when the container starts is deprecated and will be removed in Docker 1.12") + oldNetworkMode := container.HostConfig.NetworkMode + if err := daemon.setSecurityOptions(container, hostConfig); err != nil { + return err + } + if err := daemon.setHostConfig(container, hostConfig); err != nil { + return err + } + newNetworkMode := container.HostConfig.NetworkMode + if string(oldNetworkMode) != string(newNetworkMode) { + // if user has change the network mode on starting, clean up the + // old networks. It is a deprecated feature and will be removed in Docker 1.12 + container.NetworkSettings.Networks = nil + if err := container.ToDisk(); err != nil { + return err + } + } + container.InitDNSHostConfig() + } + } else { + if hostConfig != nil { + return fmt.Errorf("Supplying a hostconfig on start is not supported. It should be supplied on create") + } + } + + // check if hostConfig is in line with the current system settings. + // It may happen cgroups are umounted or the like. + if _, err = daemon.verifyContainerSettings(container.HostConfig, nil, false); err != nil { + return err + } + // Adapt for old containers in case we have updates in this function and + // old containers never have chance to call the new function in create stage. + if err := daemon.adaptContainerSettings(container.HostConfig, false); err != nil { + return err + } + + return daemon.containerStart(container) +} + +// Start starts a container +func (daemon *Daemon) Start(container *container.Container) error { + return daemon.containerStart(container) +} + +// containerStart prepares the container to run by setting up everything the +// container needs, such as storage and networking, as well as links +// between containers. The container is left waiting for a signal to +// begin running. +func (daemon *Daemon) containerStart(container *container.Container) (err error) { + container.Lock() + defer container.Unlock() + + if container.Running { + return nil + } + + if container.RemovalInProgress || container.Dead { + return fmt.Errorf("Container is marked for removal and cannot be started.") + } + + // if we encounter an error during start we need to ensure that any other + // setup has been cleaned up properly + defer func() { + if err != nil { + container.SetError(err) + // if no one else has set it, make sure we don't leave it at zero + if container.ExitCode == 0 { + container.ExitCode = 128 + } + container.ToDisk() + daemon.Cleanup(container) + attributes := map[string]string{ + "exitCode": fmt.Sprintf("%d", container.ExitCode), + } + daemon.LogContainerEventWithAttributes(container, "die", attributes) + } + }() + + if err := daemon.conditionalMountOnStart(container); err != nil { + return err + } + + // Make sure NetworkMode has an acceptable value. We do this to ensure + // backwards API compatibility. + container.HostConfig = runconfig.SetDefaultNetModeIfBlank(container.HostConfig) + + if err := daemon.initializeNetworking(container); err != nil { + return err + } + + spec, err := daemon.createSpec(container) + if err != nil { + return err + } + + if err := daemon.containerd.Create(container.ID, *spec, libcontainerd.WithRestartManager(container.RestartManager(true))); err != nil { + // if we receive an internal error from the initial start of a container then lets + // return it instead of entering the restart loop + // set to 127 for container cmd not found/does not exist) + if strings.Contains(err.Error(), "executable file not found") || + strings.Contains(err.Error(), "no such file or directory") || + strings.Contains(err.Error(), "system cannot find the file specified") { + container.ExitCode = 127 + err = fmt.Errorf("Container command '%s' not found or does not exist.", container.Path) + } + // set to 126 for container cmd can't be invoked errors + if strings.Contains(err.Error(), syscall.EACCES.Error()) { + container.ExitCode = 126 + err = fmt.Errorf("Container command '%s' could not be invoked.", container.Path) + } + + container.Reset(false) + + // start event is logged even on error + daemon.LogContainerEvent(container, "start") + return err + } + + return nil +} + +// Cleanup releases any network resources allocated to the container along with any rules +// around how containers are linked together. It also unmounts the container's root filesystem. +func (daemon *Daemon) Cleanup(container *container.Container) { + daemon.releaseNetwork(container) + + container.UnmountIpcMounts(detachMounted) + + if err := daemon.conditionalUnmountOnCleanup(container); err != nil { + // FIXME: remove once reference counting for graphdrivers has been refactored + // Ensure that all the mounts are gone + if mountid, err := daemon.layerStore.GetMountID(container.ID); err == nil { + daemon.cleanupMountsByID(mountid) + } + } + + for _, eConfig := range container.ExecCommands.Commands() { + daemon.unregisterExecCommand(container, eConfig) + } + + if container.BaseFS != "" { + if err := container.UnmountVolumes(false, daemon.LogVolumeEvent); err != nil { + logrus.Warnf("%s cleanup: Failed to umount volumes: %v", container.ID, err) + } + } + container.CancelAttachContext() +} diff --git a/daemon/stats.go b/daemon/stats.go new file mode 100644 index 00000000..cb3478cc --- /dev/null +++ b/daemon/stats.go @@ -0,0 +1,121 @@ +package daemon + +import ( + "encoding/json" + "errors" + "runtime" + + "github.com/docker/docker/api/types/backend" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/version" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/versions/v1p20" +) + +// ContainerStats writes information about the container to the stream +// given in the config object. +func (daemon *Daemon) ContainerStats(prefixOrName string, config *backend.ContainerStatsConfig) error { + if runtime.GOOS == "windows" { + return errors.New("Windows does not support stats") + } + // Remote API version (used for backwards compatibility) + apiVersion := version.Version(config.Version) + + container, err := daemon.GetContainer(prefixOrName) + if err != nil { + return err + } + + // If the container is not running and requires no stream, return an empty stats. + if !container.IsRunning() && !config.Stream { + return json.NewEncoder(config.OutStream).Encode(&types.Stats{}) + } + + outStream := config.OutStream + if config.Stream { + wf := ioutils.NewWriteFlusher(outStream) + defer wf.Close() + wf.Flush() + outStream = wf + } + + var preCPUStats types.CPUStats + getStatJSON := func(v interface{}) *types.StatsJSON { + ss := v.(types.StatsJSON) + ss.PreCPUStats = preCPUStats + // ss.MemoryStats.Limit = uint64(update.MemoryLimit) + preCPUStats = ss.CPUStats + return &ss + } + + enc := json.NewEncoder(outStream) + + updates := daemon.subscribeToContainerStats(container) + defer daemon.unsubscribeToContainerStats(container, updates) + + noStreamFirstFrame := true + for { + select { + case v, ok := <-updates: + if !ok { + return nil + } + + var statsJSON interface{} + statsJSONPost120 := getStatJSON(v) + if apiVersion.LessThan("1.21") { + var ( + rxBytes uint64 + rxPackets uint64 + rxErrors uint64 + rxDropped uint64 + txBytes uint64 + txPackets uint64 + txErrors uint64 + txDropped uint64 + ) + for _, v := range statsJSONPost120.Networks { + rxBytes += v.RxBytes + rxPackets += v.RxPackets + rxErrors += v.RxErrors + rxDropped += v.RxDropped + txBytes += v.TxBytes + txPackets += v.TxPackets + txErrors += v.TxErrors + txDropped += v.TxDropped + } + statsJSON = &v1p20.StatsJSON{ + Stats: statsJSONPost120.Stats, + Network: types.NetworkStats{ + RxBytes: rxBytes, + RxPackets: rxPackets, + RxErrors: rxErrors, + RxDropped: rxDropped, + TxBytes: txBytes, + TxPackets: txPackets, + TxErrors: txErrors, + TxDropped: txDropped, + }, + } + } else { + statsJSON = statsJSONPost120 + } + + if !config.Stream && noStreamFirstFrame { + // prime the cpu stats so they aren't 0 in the final output + noStreamFirstFrame = false + continue + } + + if err := enc.Encode(statsJSON); err != nil { + return err + } + + if !config.Stream { + return nil + } + case <-config.Stop: + return nil + } + } +} diff --git a/daemon/stats_collector_unix.go b/daemon/stats_collector_unix.go new file mode 100644 index 00000000..1f016322 --- /dev/null +++ b/daemon/stats_collector_unix.go @@ -0,0 +1,189 @@ +// +build !windows + +package daemon + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" + "sync" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/container" + "github.com/docker/docker/pkg/pubsub" + sysinfo "github.com/docker/docker/pkg/system" + "github.com/docker/engine-api/types" + "github.com/opencontainers/runc/libcontainer/system" +) + +type statsSupervisor interface { + // GetContainerStats collects all the stats related to a container + GetContainerStats(container *container.Container) (*types.StatsJSON, error) +} + +// newStatsCollector returns a new statsCollector that collections +// network and cgroup stats for a registered container at the specified +// interval. The collector allows non-running containers to be added +// and will start processing stats when they are started. +func (daemon *Daemon) newStatsCollector(interval time.Duration) *statsCollector { + s := &statsCollector{ + interval: interval, + supervisor: daemon, + publishers: make(map[*container.Container]*pubsub.Publisher), + clockTicksPerSecond: uint64(system.GetClockTicks()), + bufReader: bufio.NewReaderSize(nil, 128), + } + meminfo, err := sysinfo.ReadMemInfo() + if err == nil && meminfo.MemTotal > 0 { + s.machineMemory = uint64(meminfo.MemTotal) + } + + go s.run() + return s +} + +// statsCollector manages and provides container resource stats +type statsCollector struct { + m sync.Mutex + supervisor statsSupervisor + interval time.Duration + clockTicksPerSecond uint64 + publishers map[*container.Container]*pubsub.Publisher + bufReader *bufio.Reader + machineMemory uint64 +} + +// collect registers the container with the collector and adds it to +// the event loop for collection on the specified interval returning +// a channel for the subscriber to receive on. +func (s *statsCollector) collect(c *container.Container) chan interface{} { + s.m.Lock() + defer s.m.Unlock() + publisher, exists := s.publishers[c] + if !exists { + publisher = pubsub.NewPublisher(100*time.Millisecond, 1024) + s.publishers[c] = publisher + } + return publisher.Subscribe() +} + +// stopCollection closes the channels for all subscribers and removes +// the container from metrics collection. +func (s *statsCollector) stopCollection(c *container.Container) { + s.m.Lock() + if publisher, exists := s.publishers[c]; exists { + publisher.Close() + delete(s.publishers, c) + } + s.m.Unlock() +} + +// unsubscribe removes a specific subscriber from receiving updates for a container's stats. +func (s *statsCollector) unsubscribe(c *container.Container, ch chan interface{}) { + s.m.Lock() + publisher := s.publishers[c] + if publisher != nil { + publisher.Evict(ch) + if publisher.Len() == 0 { + delete(s.publishers, c) + } + } + s.m.Unlock() +} + +func (s *statsCollector) run() { + type publishersPair struct { + container *container.Container + publisher *pubsub.Publisher + } + // we cannot determine the capacity here. + // it will grow enough in first iteration + var pairs []publishersPair + + for range time.Tick(s.interval) { + // it does not make sense in the first iteration, + // but saves allocations in further iterations + pairs = pairs[:0] + + s.m.Lock() + for container, publisher := range s.publishers { + // copy pointers here to release the lock ASAP + pairs = append(pairs, publishersPair{container, publisher}) + } + s.m.Unlock() + if len(pairs) == 0 { + continue + } + + systemUsage, err := s.getSystemCPUUsage() + if err != nil { + logrus.Errorf("collecting system cpu usage: %v", err) + continue + } + + for _, pair := range pairs { + stats, err := s.supervisor.GetContainerStats(pair.container) + if err != nil { + if _, ok := err.(errNotRunning); !ok { + logrus.Errorf("collecting stats for %s: %v", pair.container.ID, err) + } + continue + } + // FIXME: move to containerd + stats.CPUStats.SystemUsage = systemUsage + + pair.publisher.Publish(*stats) + } + } +} + +const nanoSecondsPerSecond = 1e9 + +// getSystemCPUUsage returns the host system's cpu usage in +// nanoseconds. An error is returned if the format of the underlying +// file does not match. +// +// Uses /proc/stat defined by POSIX. Looks for the cpu +// statistics line and then sums up the first seven fields +// provided. See `man 5 proc` for details on specific field +// information. +func (s *statsCollector) getSystemCPUUsage() (uint64, error) { + var line string + f, err := os.Open("/proc/stat") + if err != nil { + return 0, err + } + defer func() { + s.bufReader.Reset(nil) + f.Close() + }() + s.bufReader.Reset(f) + err = nil + for err == nil { + line, err = s.bufReader.ReadString('\n') + if err != nil { + break + } + parts := strings.Fields(line) + switch parts[0] { + case "cpu": + if len(parts) < 8 { + return 0, fmt.Errorf("invalid number of cpu fields") + } + var totalClockTicks uint64 + for _, i := range parts[1:8] { + v, err := strconv.ParseUint(i, 10, 64) + if err != nil { + return 0, fmt.Errorf("Unable to convert value %s to int: %s", i, err) + } + totalClockTicks += v + } + return (totalClockTicks * nanoSecondsPerSecond) / + s.clockTicksPerSecond, nil + } + } + return 0, fmt.Errorf("invalid stat format. Error trying to parse the '/proc/stat' file") +} diff --git a/daemon/stats_collector_windows.go b/daemon/stats_collector_windows.go new file mode 100644 index 00000000..b6cb24cd --- /dev/null +++ b/daemon/stats_collector_windows.go @@ -0,0 +1,35 @@ +package daemon + +import ( + "time" + + "github.com/docker/docker/container" +) + +// newStatsCollector returns a new statsCollector for collection stats +// for a registered container at the specified interval. The collector allows +// non-running containers to be added and will start processing stats when +// they are started. +func (daemon *Daemon) newStatsCollector(interval time.Duration) *statsCollector { + return &statsCollector{} +} + +// statsCollector manages and provides container resource stats +type statsCollector struct { +} + +// collect registers the container with the collector and adds it to +// the event loop for collection on the specified interval returning +// a channel for the subscriber to receive on. +func (s *statsCollector) collect(c *container.Container) chan interface{} { + return nil +} + +// stopCollection closes the channels for all subscribers and removes +// the container from metrics collection. +func (s *statsCollector) stopCollection(c *container.Container) { +} + +// unsubscribe removes a specific subscriber from receiving updates for a container's stats. +func (s *statsCollector) unsubscribe(c *container.Container, ch chan interface{}) { +} diff --git a/daemon/stop.go b/daemon/stop.go new file mode 100644 index 00000000..70174300 --- /dev/null +++ b/daemon/stop.go @@ -0,0 +1,65 @@ +package daemon + +import ( + "fmt" + "net/http" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/container" + "github.com/docker/docker/errors" +) + +// ContainerStop looks for the given container and terminates it, +// waiting the given number of seconds before forcefully killing the +// container. If a negative number of seconds is given, ContainerStop +// will wait for a graceful termination. An error is returned if the +// container is not found, is already stopped, or if there is a +// problem stopping the container. +func (daemon *Daemon) ContainerStop(name string, seconds int) error { + container, err := daemon.GetContainer(name) + if err != nil { + return err + } + if !container.IsRunning() { + err := fmt.Errorf("Container %s is already stopped", name) + return errors.NewErrorWithStatusCode(err, http.StatusNotModified) + } + if err := daemon.containerStop(container, seconds); err != nil { + return fmt.Errorf("Cannot stop container %s: %v", name, err) + } + return nil +} + +// containerStop halts a container by sending a stop signal, waiting for the given +// duration in seconds, and then calling SIGKILL and waiting for the +// process to exit. If a negative duration is given, Stop will wait +// for the initial signal forever. If the container is not running Stop returns +// immediately. +func (daemon *Daemon) containerStop(container *container.Container, seconds int) error { + if !container.IsRunning() { + return nil + } + + stopSignal := container.StopSignal() + // 1. Send a stop signal + if err := daemon.killPossiblyDeadProcess(container, stopSignal); err != nil { + logrus.Infof("Failed to send signal %d to the process, force killing", stopSignal) + if err := daemon.killPossiblyDeadProcess(container, 9); err != nil { + return err + } + } + + // 2. Wait for the process to exit on its own + if _, err := container.WaitStop(time.Duration(seconds) * time.Second); err != nil { + logrus.Infof("Container %v failed to exit within %d seconds of signal %d - using the force", container.ID, seconds, stopSignal) + // 3. If it doesn't, then send SIGKILL + if err := daemon.Kill(container); err != nil { + container.WaitStop(-1 * time.Second) + logrus.Warn(err) // Don't return error because we only care that container is stopped, not what function stopped it + } + } + + daemon.LogContainerEvent(container, "stop") + return nil +} diff --git a/daemon/top_unix.go b/daemon/top_unix.go new file mode 100644 index 00000000..d4a9528c --- /dev/null +++ b/daemon/top_unix.go @@ -0,0 +1,85 @@ +//+build !windows + +package daemon + +import ( + "fmt" + "os/exec" + "strconv" + "strings" + + "github.com/docker/engine-api/types" +) + +// ContainerTop lists the processes running inside of the given +// container by calling ps with the given args, or with the flags +// "-ef" if no args are given. An error is returned if the container +// is not found, or is not running, or if there are any problems +// running ps, or parsing the output. +func (daemon *Daemon) ContainerTop(name string, psArgs string) (*types.ContainerProcessList, error) { + if psArgs == "" { + psArgs = "-ef" + } + + container, err := daemon.GetContainer(name) + if err != nil { + return nil, err + } + + if !container.IsRunning() { + return nil, errNotRunning{container.ID} + } + + if container.IsRestarting() { + return nil, errContainerIsRestarting(container.ID) + } + + pids, err := daemon.containerd.GetPidsForContainer(container.ID) + if err != nil { + return nil, err + } + + output, err := exec.Command("ps", strings.Split(psArgs, " ")...).Output() + if err != nil { + return nil, fmt.Errorf("Error running ps: %v", err) + } + + procList := &types.ContainerProcessList{} + + lines := strings.Split(string(output), "\n") + procList.Titles = strings.Fields(lines[0]) + + pidIndex := -1 + for i, name := range procList.Titles { + if name == "PID" { + pidIndex = i + } + } + if pidIndex == -1 { + return nil, fmt.Errorf("Couldn't find PID field in ps output") + } + + // loop through the output and extract the PID from each line + for _, line := range lines[1:] { + if len(line) == 0 { + continue + } + fields := strings.Fields(line) + p, err := strconv.Atoi(fields[pidIndex]) + if err != nil { + return nil, fmt.Errorf("Unexpected pid '%s': %s", fields[pidIndex], err) + } + + for _, pid := range pids { + if pid == p { + // Make sure number of fields equals number of header titles + // merging "overhanging" fields + process := fields[:len(procList.Titles)-1] + process = append(process, strings.Join(fields[len(procList.Titles)-1:], " ")) + procList.Processes = append(procList.Processes, process) + } + } + } + daemon.LogContainerEvent(container, "top") + return procList, nil +} diff --git a/daemon/top_windows.go b/daemon/top_windows.go new file mode 100644 index 00000000..ea79ac86 --- /dev/null +++ b/daemon/top_windows.go @@ -0,0 +1,32 @@ +package daemon + +import ( + "errors" + "strconv" + + "github.com/docker/engine-api/types" +) + +// ContainerTop is a minimal implementation on Windows currently. +// TODO Windows: This needs more work, but needs platform API support. +// All we can currently return (particularly in the case of Hyper-V containers) +// is a PID and the command. +func (daemon *Daemon) ContainerTop(containerID string, psArgs string) (*types.ContainerProcessList, error) { + + // It's really not an equivalent to linux 'ps' on Windows + if psArgs != "" { + return nil, errors.New("Windows does not support arguments to top") + } + + s, err := daemon.containerd.Summary(containerID) + if err != nil { + return nil, err + } + + procList := &types.ContainerProcessList{} + + for _, v := range s { + procList.Titles = append(procList.Titles, strconv.Itoa(int(v.Pid))+" "+v.Command) + } + return procList, nil +} diff --git a/daemon/unpause.go b/daemon/unpause.go new file mode 100644 index 00000000..c1ab74b0 --- /dev/null +++ b/daemon/unpause.go @@ -0,0 +1,43 @@ +package daemon + +import ( + "fmt" + + "github.com/docker/docker/container" +) + +// ContainerUnpause unpauses a container +func (daemon *Daemon) ContainerUnpause(name string) error { + container, err := daemon.GetContainer(name) + if err != nil { + return err + } + + if err := daemon.containerUnpause(container); err != nil { + return err + } + + return nil +} + +// containerUnpause resumes the container execution after the container is paused. +func (daemon *Daemon) containerUnpause(container *container.Container) error { + container.Lock() + defer container.Unlock() + + // We cannot unpause the container which is not running + if !container.Running { + return errNotRunning{container.ID} + } + + // We cannot unpause the container which is not paused + if !container.Paused { + return fmt.Errorf("Container %s is not paused", container.ID) + } + + if err := daemon.containerd.Resume(container.ID); err != nil { + return fmt.Errorf("Cannot unpause container %s: %s", container.ID, err) + } + + return nil +} diff --git a/daemon/update.go b/daemon/update.go new file mode 100644 index 00000000..fee470a3 --- /dev/null +++ b/daemon/update.go @@ -0,0 +1,100 @@ +package daemon + +import ( + "fmt" + "time" + + "github.com/docker/engine-api/types/container" +) + +// ContainerUpdate updates configuration of the container +func (daemon *Daemon) ContainerUpdate(name string, hostConfig *container.HostConfig) ([]string, error) { + var warnings []string + + warnings, err := daemon.verifyContainerSettings(hostConfig, nil, true) + if err != nil { + return warnings, err + } + + if err := daemon.update(name, hostConfig); err != nil { + return warnings, err + } + + return warnings, nil +} + +// ContainerUpdateCmdOnBuild updates Path and Args for the container with ID cID. +func (daemon *Daemon) ContainerUpdateCmdOnBuild(cID string, cmd []string) error { + if len(cmd) == 0 { + return nil + } + c, err := daemon.GetContainer(cID) + if err != nil { + return err + } + c.Path = cmd[0] + c.Args = cmd[1:] + return nil +} + +func (daemon *Daemon) update(name string, hostConfig *container.HostConfig) error { + if hostConfig == nil { + return nil + } + + container, err := daemon.GetContainer(name) + if err != nil { + return err + } + + restoreConfig := false + backupHostConfig := *container.HostConfig + defer func() { + if restoreConfig { + container.Lock() + container.HostConfig = &backupHostConfig + container.ToDisk() + container.Unlock() + } + }() + + if container.RemovalInProgress || container.Dead { + return errCannotUpdate(container.ID, fmt.Errorf("Container is marked for removal and cannot be \"update\".")) + } + + if container.IsRunning() && hostConfig.KernelMemory != 0 { + return errCannotUpdate(container.ID, fmt.Errorf("Can not update kernel memory to a running container, please stop it first.")) + } + + if err := container.UpdateContainer(hostConfig); err != nil { + restoreConfig = true + return errCannotUpdate(container.ID, err) + } + + // if Restart Policy changed, we need to update container monitor + container.UpdateMonitor(hostConfig.RestartPolicy) + + // if container is restarting, wait 5 seconds until it's running + if container.IsRestarting() { + container.WaitRunning(5 * time.Second) + } + + // If container is not running, update hostConfig struct is enough, + // resources will be updated when the container is started again. + // If container is running (including paused), we need to update configs + // to the real world. + if container.IsRunning() && !container.IsRestarting() { + if err := daemon.containerd.UpdateResources(container.ID, toContainerdResources(hostConfig.Resources)); err != nil { + restoreConfig = true + return errCannotUpdate(container.ID, err) + } + } + + daemon.LogContainerEvent(container, "update") + + return nil +} + +func errCannotUpdate(containerID string, err error) error { + return fmt.Errorf("Cannot update container %s: %v", containerID, err) +} diff --git a/daemon/update_linux.go b/daemon/update_linux.go new file mode 100644 index 00000000..97ba7c09 --- /dev/null +++ b/daemon/update_linux.go @@ -0,0 +1,25 @@ +// +build linux + +package daemon + +import ( + "github.com/docker/docker/libcontainerd" + "github.com/docker/engine-api/types/container" +) + +func toContainerdResources(resources container.Resources) libcontainerd.Resources { + var r libcontainerd.Resources + r.BlkioWeight = uint32(resources.BlkioWeight) + r.CpuShares = uint32(resources.CPUShares) + r.CpuPeriod = uint32(resources.CPUPeriod) + r.CpuQuota = uint32(resources.CPUQuota) + r.CpusetCpus = resources.CpusetCpus + r.CpusetMems = resources.CpusetMems + r.MemoryLimit = uint32(resources.Memory) + if resources.MemorySwap > 0 { + r.MemorySwap = uint32(resources.MemorySwap) + } + r.MemoryReservation = uint32(resources.MemoryReservation) + r.KernelMemoryLimit = uint32(resources.KernelMemory) + return r +} diff --git a/daemon/update_windows.go b/daemon/update_windows.go new file mode 100644 index 00000000..2cd0ff26 --- /dev/null +++ b/daemon/update_windows.go @@ -0,0 +1,13 @@ +// +build windows + +package daemon + +import ( + "github.com/docker/docker/libcontainerd" + "github.com/docker/engine-api/types/container" +) + +func toContainerdResources(resources container.Resources) libcontainerd.Resources { + var r libcontainerd.Resources + return r +} diff --git a/daemon/volumes.go b/daemon/volumes.go new file mode 100644 index 00000000..37f4e7fa --- /dev/null +++ b/daemon/volumes.go @@ -0,0 +1,178 @@ +package daemon + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/docker/docker/container" + "github.com/docker/docker/volume" + "github.com/docker/engine-api/types" + containertypes "github.com/docker/engine-api/types/container" + "github.com/opencontainers/runc/libcontainer/label" +) + +var ( + // ErrVolumeReadonly is used to signal an error when trying to copy data into + // a volume mount that is not writable. + ErrVolumeReadonly = errors.New("mounted volume is marked read-only") +) + +type mounts []container.Mount + +// volumeToAPIType converts a volume.Volume to the type used by the remote API +func volumeToAPIType(v volume.Volume) *types.Volume { + tv := &types.Volume{ + Name: v.Name(), + Driver: v.DriverName(), + Mountpoint: v.Path(), + } + if v, ok := v.(interface { + Labels() map[string]string + }); ok { + tv.Labels = v.Labels() + } + return tv +} + +// Len returns the number of mounts. Used in sorting. +func (m mounts) Len() int { + return len(m) +} + +// Less returns true if the number of parts (a/b/c would be 3 parts) in the +// mount indexed by parameter 1 is less than that of the mount indexed by +// parameter 2. Used in sorting. +func (m mounts) Less(i, j int) bool { + return m.parts(i) < m.parts(j) +} + +// Swap swaps two items in an array of mounts. Used in sorting +func (m mounts) Swap(i, j int) { + m[i], m[j] = m[j], m[i] +} + +// parts returns the number of parts in the destination of a mount. Used in sorting. +func (m mounts) parts(i int) int { + return strings.Count(filepath.Clean(m[i].Destination), string(os.PathSeparator)) +} + +// registerMountPoints initializes the container mount points with the configured volumes and bind mounts. +// It follows the next sequence to decide what to mount in each final destination: +// +// 1. Select the previously configured mount points for the containers, if any. +// 2. Select the volumes mounted from another containers. Overrides previously configured mount point destination. +// 3. Select the bind mounts set by the client. Overrides previously configured mount point destinations. +// 4. Cleanup old volumes that are about to be reassigned. +func (daemon *Daemon) registerMountPoints(container *container.Container, hostConfig *containertypes.HostConfig) error { + binds := map[string]bool{} + mountPoints := map[string]*volume.MountPoint{} + + // 1. Read already configured mount points. + for name, point := range container.MountPoints { + mountPoints[name] = point + } + + // 2. Read volumes from other containers. + for _, v := range hostConfig.VolumesFrom { + containerID, mode, err := volume.ParseVolumesFrom(v) + if err != nil { + return err + } + + c, err := daemon.GetContainer(containerID) + if err != nil { + return err + } + + for _, m := range c.MountPoints { + cp := &volume.MountPoint{ + Name: m.Name, + Source: m.Source, + RW: m.RW && volume.ReadWrite(mode), + Driver: m.Driver, + Destination: m.Destination, + Propagation: m.Propagation, + Named: m.Named, + } + + if len(cp.Source) == 0 { + v, err := daemon.volumes.GetWithRef(cp.Name, cp.Driver, container.ID) + if err != nil { + return err + } + cp.Volume = v + } + + mountPoints[cp.Destination] = cp + } + } + + // 3. Read bind mounts + for _, b := range hostConfig.Binds { + // #10618 + bind, err := volume.ParseMountSpec(b, hostConfig.VolumeDriver) + if err != nil { + return err + } + + if binds[bind.Destination] { + return fmt.Errorf("Duplicate mount point '%s'", bind.Destination) + } + + if len(bind.Name) > 0 { + // create the volume + v, err := daemon.volumes.CreateWithRef(bind.Name, bind.Driver, container.ID, nil, nil) + if err != nil { + return err + } + bind.Volume = v + bind.Source = v.Path() + // bind.Name is an already existing volume, we need to use that here + bind.Driver = v.DriverName() + bind.Named = true + if bind.Driver == "local" { + bind = setBindModeIfNull(bind) + } + } + + if label.RelabelNeeded(bind.Mode) { + if err := label.Relabel(bind.Source, container.MountLabel, label.IsShared(bind.Mode)); err != nil { + return err + } + } + binds[bind.Destination] = true + mountPoints[bind.Destination] = bind + } + + container.Lock() + + // 4. Cleanup old volumes that are about to be reassigned. + for _, m := range mountPoints { + if m.BackwardsCompatible() { + if mp, exists := container.MountPoints[m.Destination]; exists && mp.Volume != nil { + daemon.volumes.Dereference(mp.Volume, container.ID) + } + } + } + container.MountPoints = mountPoints + + container.Unlock() + + return nil +} + +// lazyInitializeVolume initializes a mountpoint's volume if needed. +// This happens after a daemon restart. +func (daemon *Daemon) lazyInitializeVolume(containerID string, m *volume.MountPoint) error { + if len(m.Driver) > 0 && m.Volume == nil { + v, err := daemon.volumes.GetWithRef(m.Name, m.Driver, containerID) + if err != nil { + return err + } + m.Volume = v + } + return nil +} diff --git a/daemon/volumes_unit_test.go b/daemon/volumes_unit_test.go new file mode 100644 index 00000000..450d17f9 --- /dev/null +++ b/daemon/volumes_unit_test.go @@ -0,0 +1,39 @@ +package daemon + +import ( + "testing" + + "github.com/docker/docker/volume" +) + +func TestParseVolumesFrom(t *testing.T) { + cases := []struct { + spec string + expID string + expMode string + fail bool + }{ + {"", "", "", true}, + {"foobar", "foobar", "rw", false}, + {"foobar:rw", "foobar", "rw", false}, + {"foobar:ro", "foobar", "ro", false}, + {"foobar:baz", "", "", true}, + } + + for _, c := range cases { + id, mode, err := volume.ParseVolumesFrom(c.spec) + if c.fail { + if err == nil { + t.Fatalf("Expected error, was nil, for spec %s\n", c.spec) + } + continue + } + + if id != c.expID { + t.Fatalf("Expected id %s, was %s, for spec %s\n", c.expID, id, c.spec) + } + if mode != c.expMode { + t.Fatalf("Expected mode %s, was %s for spec %s\n", c.expMode, mode, c.spec) + } + } +} diff --git a/daemon/volumes_unix.go b/daemon/volumes_unix.go new file mode 100644 index 00000000..078fd10b --- /dev/null +++ b/daemon/volumes_unix.go @@ -0,0 +1,78 @@ +// +build !windows + +package daemon + +import ( + "os" + "sort" + "strconv" + + "github.com/docker/docker/container" + "github.com/docker/docker/volume" +) + +// setupMounts iterates through each of the mount points for a container and +// calls Setup() on each. It also looks to see if is a network mount such as +// /etc/resolv.conf, and if it is not, appends it to the array of mounts. +func (daemon *Daemon) setupMounts(c *container.Container) ([]container.Mount, error) { + var mounts []container.Mount + for _, m := range c.MountPoints { + if err := daemon.lazyInitializeVolume(c.ID, m); err != nil { + return nil, err + } + path, err := m.Setup() + if err != nil { + return nil, err + } + if !c.TrySetNetworkMount(m.Destination, path) { + mnt := container.Mount{ + Source: path, + Destination: m.Destination, + Writable: m.RW, + Propagation: m.Propagation, + } + if m.Volume != nil { + attributes := map[string]string{ + "driver": m.Volume.DriverName(), + "container": c.ID, + "destination": m.Destination, + "read/write": strconv.FormatBool(m.RW), + "propagation": m.Propagation, + } + daemon.LogVolumeEvent(m.Volume.Name(), "mount", attributes) + } + mounts = append(mounts, mnt) + } + } + + mounts = sortMounts(mounts) + netMounts := c.NetworkMounts() + // if we are going to mount any of the network files from container + // metadata, the ownership must be set properly for potential container + // remapped root (user namespaces) + rootUID, rootGID := daemon.GetRemappedUIDGID() + for _, mount := range netMounts { + if err := os.Chown(mount.Source, rootUID, rootGID); err != nil { + return nil, err + } + } + return append(mounts, netMounts...), nil +} + +// sortMounts sorts an array of mounts in lexicographic order. This ensure that +// when mounting, the mounts don't shadow other mounts. For example, if mounting +// /etc and /etc/resolv.conf, /etc/resolv.conf must not be mounted first. +func sortMounts(m []container.Mount) []container.Mount { + sort.Sort(mounts(m)) + return m +} + +// setBindModeIfNull is platform specific processing to ensure the +// shared mode is set to 'z' if it is null. This is called in the case +// of processing a named volume and not a typical bind. +func setBindModeIfNull(bind *volume.MountPoint) *volume.MountPoint { + if bind.Mode == "" { + bind.Mode = "z" + } + return bind +} diff --git a/daemon/volumes_windows.go b/daemon/volumes_windows.go new file mode 100644 index 00000000..e7f9c098 --- /dev/null +++ b/daemon/volumes_windows.go @@ -0,0 +1,51 @@ +// +build windows + +package daemon + +import ( + "fmt" + "sort" + + "github.com/docker/docker/container" + "github.com/docker/docker/volume" +) + +// setupMounts configures the mount points for a container by appending each +// of the configured mounts on the container to the OCI mount structure +// which will ultimately be passed into the oci runtime during container creation. +// It also ensures each of the mounts are lexographically sorted. + +// BUGBUG TODO Windows containerd. This would be much better if it returned +// an array of windowsoci mounts, not container mounts. Then no need to +// do multiple transitions. + +func (daemon *Daemon) setupMounts(c *container.Container) ([]container.Mount, error) { + var mnts []container.Mount + for _, mount := range c.MountPoints { // type is volume.MountPoint + if err := daemon.lazyInitializeVolume(c.ID, mount); err != nil { + return nil, err + } + // If there is no source, take it from the volume path + s := mount.Source + if s == "" && mount.Volume != nil { + s = mount.Volume.Path() + } + if s == "" { + return nil, fmt.Errorf("No source for mount name '%s' driver %q destination '%s'", mount.Name, mount.Driver, mount.Destination) + } + mnts = append(mnts, container.Mount{ + Source: s, + Destination: mount.Destination, + Writable: mount.RW, + }) + } + + sort.Sort(mounts(mnts)) + return mnts, nil +} + +// setBindModeIfNull is platform specific processing which is a no-op on +// Windows. +func setBindModeIfNull(bind *volume.MountPoint) *volume.MountPoint { + return bind +} diff --git a/daemon/wait.go b/daemon/wait.go new file mode 100644 index 00000000..52b335cd --- /dev/null +++ b/daemon/wait.go @@ -0,0 +1,17 @@ +package daemon + +import "time" + +// ContainerWait stops processing until the given container is +// stopped. If the container is not found, an error is returned. On a +// successful stop, the exit code of the container is returned. On a +// timeout, an error is returned. If you want to wait forever, supply +// a negative duration for the timeout. +func (daemon *Daemon) ContainerWait(name string, timeout time.Duration) (int, error) { + container, err := daemon.GetContainer(name) + if err != nil { + return -1, err + } + + return container.WaitStop(timeout) +} diff --git a/distribution/errors.go b/distribution/errors.go new file mode 100644 index 00000000..f7a9c621 --- /dev/null +++ b/distribution/errors.go @@ -0,0 +1,113 @@ +package distribution + +import ( + "net/url" + "strings" + "syscall" + + "github.com/docker/distribution/registry/api/errcode" + "github.com/docker/distribution/registry/api/v2" + "github.com/docker/distribution/registry/client" + "github.com/docker/distribution/registry/client/auth" + "github.com/docker/docker/distribution/xfer" +) + +// ErrNoSupport is an error type used for errors indicating that an operation +// is not supported. It encapsulates a more specific error. +type ErrNoSupport struct{ Err error } + +func (e ErrNoSupport) Error() string { + if e.Err == nil { + return "not supported" + } + return e.Err.Error() +} + +// fallbackError wraps an error that can possibly allow fallback to a different +// endpoint. +type fallbackError struct { + // err is the error being wrapped. + err error + // confirmedV2 is set to true if it was confirmed that the registry + // supports the v2 protocol. This is used to limit fallbacks to the v1 + // protocol. + confirmedV2 bool + // transportOK is set to true if we managed to speak HTTP with the + // registry. This confirms that we're using appropriate TLS settings + // (or lack of TLS). + transportOK bool +} + +// Error renders the FallbackError as a string. +func (f fallbackError) Error() string { + return f.err.Error() +} + +// shouldV2Fallback returns true if this error is a reason to fall back to v1. +func shouldV2Fallback(err errcode.Error) bool { + switch err.Code { + case errcode.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown, v2.ErrorCodeNameUnknown: + return true + } + return false +} + +// continueOnError returns true if we should fallback to the next endpoint +// as a result of this error. +func continueOnError(err error) bool { + switch v := err.(type) { + case errcode.Errors: + if len(v) == 0 { + return true + } + return continueOnError(v[0]) + case ErrNoSupport: + return continueOnError(v.Err) + case errcode.Error: + return shouldV2Fallback(v) + case *client.UnexpectedHTTPResponseError: + return true + case ImageConfigPullError: + return false + case error: + return !strings.Contains(err.Error(), strings.ToLower(syscall.ENOSPC.Error())) + } + // let's be nice and fallback if the error is a completely + // unexpected one. + // If new errors have to be handled in some way, please + // add them to the switch above. + return true +} + +// retryOnError wraps the error in xfer.DoNotRetry if we should not retry the +// operation after this error. +func retryOnError(err error) error { + switch v := err.(type) { + case errcode.Errors: + if len(v) != 0 { + return retryOnError(v[0]) + } + case errcode.Error: + switch v.Code { + case errcode.ErrorCodeUnauthorized, errcode.ErrorCodeUnsupported, errcode.ErrorCodeDenied: + return xfer.DoNotRetry{Err: err} + } + case *url.Error: + switch v.Err { + case auth.ErrNoBasicAuthCredentials, auth.ErrNoToken: + return xfer.DoNotRetry{Err: v.Err} + } + return retryOnError(v.Err) + case *client.UnexpectedHTTPResponseError: + return xfer.DoNotRetry{Err: err} + case error: + if strings.Contains(err.Error(), strings.ToLower(syscall.ENOSPC.Error())) { + return xfer.DoNotRetry{Err: err} + } + } + // let's be nice and fallback if the error is a completely + // unexpected one. + // If new errors have to be handled in some way, please + // add them to the switch above. + return err +} diff --git a/distribution/fixtures/validate_manifest/bad_manifest b/distribution/fixtures/validate_manifest/bad_manifest new file mode 100644 index 00000000..a1f02a62 --- /dev/null +++ b/distribution/fixtures/validate_manifest/bad_manifest @@ -0,0 +1,38 @@ +{ + "schemaVersion": 2, + "name": "library/hello-world", + "tag": "latest", + "architecture": "amd64", + "fsLayers": [ + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + }, + { + "blobSum": "sha256:03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb" + } + ], + "history": [ + { + "v1Compatibility": "{\"id\":\"af340544ed62de0680f441c71fa1a80cb084678fed42bae393e543faea3a572c\",\"parent\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"created\":\"2015-08-06T23:53:22.608577814Z\",\"container\":\"c2b715156f640c7ac7d98472ea24335aba5432a1323a3bb722697e6d37ef794f\",\"container_config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [\\\"/hello\\\"]\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.7.1\",\"config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/hello\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" + }, + { + "v1Compatibility": "{\"id\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"created\":\"2015-08-06T23:53:22.241352727Z\",\"container\":\"9aeb0006ffa72a8287564caaea87625896853701459261d3b569e320c0c9d5dc\",\"container_config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) COPY file:4abd3bff60458ca3b079d7b131ce26b2719055a030dfa96ff827da2b7c7038a7 in /\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.7.1\",\"config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":960}\n" + } + ], + "signatures": [ + { + "header": { + "jwk": { + "crv": "P-256", + "kid": "OIH7:HQFS:44FK:45VB:3B53:OIAG:TPL4:ATF5:6PNE:MGHN:NHQX:2GE4", + "kty": "EC", + "x": "Cu_UyxwLgHzE9rvlYSmvVdqYCXY42E9eNhBb0xNv0SQ", + "y": "zUsjWJkeKQ5tv7S-hl1Tg71cd-CqnrtiiLxSi6N_yc8" + }, + "alg": "ES256" + }, + "signature": "Y6xaFz9Sy-OtcnKQS1Ilq3Dh8cu4h3nBTJCpOTF1XF7vKtcxxA_xMP8-SgDo869SJ3VsvgPL9-Xn-OoYG2rb1A", + "protected": "eyJmb3JtYXRMZW5ndGgiOjMxOTcsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNS0wOS0xMVQwNDoxMzo0OFoifQ" + } + ] +} diff --git a/distribution/fixtures/validate_manifest/extra_data_manifest b/distribution/fixtures/validate_manifest/extra_data_manifest new file mode 100644 index 00000000..beec19a8 --- /dev/null +++ b/distribution/fixtures/validate_manifest/extra_data_manifest @@ -0,0 +1,46 @@ +{ + "schemaVersion": 1, + "name": "library/hello-world", + "tag": "latest", + "architecture": "amd64", + "fsLayers": [ + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + }, + { + "blobSum": "sha256:03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb" + } + ], + "history": [ + { + "v1Compatibility": "{\"id\":\"af340544ed62de0680f441c71fa1a80cb084678fed42bae393e543faea3a572c\",\"parent\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"created\":\"2015-08-06T23:53:22.608577814Z\",\"container\":\"c2b715156f640c7ac7d98472ea24335aba5432a1323a3bb722697e6d37ef794f\",\"container_config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [\\\"/hello\\\"]\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.7.1\",\"config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/hello\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" + }, + { + "v1Compatibility": "{\"id\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"created\":\"2015-08-06T23:53:22.241352727Z\",\"container\":\"9aeb0006ffa72a8287564caaea87625896853701459261d3b569e320c0c9d5dc\",\"container_config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) COPY file:4abd3bff60458ca3b079d7b131ce26b2719055a030dfa96ff827da2b7c7038a7 in /\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.7.1\",\"config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":960}\n" + } + ], + "fsLayers": [ + { + "blobSum": "sha256:ffff95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + }, + { + "blobSum": "sha256:ffff658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb" + } + ], + "signatures": [ + { + "header": { + "jwk": { + "crv": "P-256", + "kid": "OIH7:HQFS:44FK:45VB:3B53:OIAG:TPL4:ATF5:6PNE:MGHN:NHQX:2GE4", + "kty": "EC", + "x": "Cu_UyxwLgHzE9rvlYSmvVdqYCXY42E9eNhBb0xNv0SQ", + "y": "zUsjWJkeKQ5tv7S-hl1Tg71cd-CqnrtiiLxSi6N_yc8" + }, + "alg": "ES256" + }, + "signature": "Y6xaFz9Sy-OtcnKQS1Ilq3Dh8cu4h3nBTJCpOTF1XF7vKtcxxA_xMP8-SgDo869SJ3VsvgPL9-Xn-OoYG2rb1A", + "protected": "eyJmb3JtYXRMZW5ndGgiOjMxOTcsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNS0wOS0xMVQwNDoxMzo0OFoifQ" + } + ] +} diff --git a/distribution/fixtures/validate_manifest/good_manifest b/distribution/fixtures/validate_manifest/good_manifest new file mode 100644 index 00000000..b107de32 --- /dev/null +++ b/distribution/fixtures/validate_manifest/good_manifest @@ -0,0 +1,38 @@ +{ + "schemaVersion": 1, + "name": "library/hello-world", + "tag": "latest", + "architecture": "amd64", + "fsLayers": [ + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + }, + { + "blobSum": "sha256:03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb" + } + ], + "history": [ + { + "v1Compatibility": "{\"id\":\"af340544ed62de0680f441c71fa1a80cb084678fed42bae393e543faea3a572c\",\"parent\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"created\":\"2015-08-06T23:53:22.608577814Z\",\"container\":\"c2b715156f640c7ac7d98472ea24335aba5432a1323a3bb722697e6d37ef794f\",\"container_config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [\\\"/hello\\\"]\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.7.1\",\"config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/hello\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" + }, + { + "v1Compatibility": "{\"id\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"created\":\"2015-08-06T23:53:22.241352727Z\",\"container\":\"9aeb0006ffa72a8287564caaea87625896853701459261d3b569e320c0c9d5dc\",\"container_config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) COPY file:4abd3bff60458ca3b079d7b131ce26b2719055a030dfa96ff827da2b7c7038a7 in /\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.7.1\",\"config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":960}\n" + } + ], + "signatures": [ + { + "header": { + "jwk": { + "crv": "P-256", + "kid": "OIH7:HQFS:44FK:45VB:3B53:OIAG:TPL4:ATF5:6PNE:MGHN:NHQX:2GE4", + "kty": "EC", + "x": "Cu_UyxwLgHzE9rvlYSmvVdqYCXY42E9eNhBb0xNv0SQ", + "y": "zUsjWJkeKQ5tv7S-hl1Tg71cd-CqnrtiiLxSi6N_yc8" + }, + "alg": "ES256" + }, + "signature": "Y6xaFz9Sy-OtcnKQS1Ilq3Dh8cu4h3nBTJCpOTF1XF7vKtcxxA_xMP8-SgDo869SJ3VsvgPL9-Xn-OoYG2rb1A", + "protected": "eyJmb3JtYXRMZW5ndGgiOjMxOTcsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNS0wOS0xMVQwNDoxMzo0OFoifQ" + } + ] +} \ No newline at end of file diff --git a/distribution/metadata/metadata.go b/distribution/metadata/metadata.go new file mode 100644 index 00000000..9f744d46 --- /dev/null +++ b/distribution/metadata/metadata.go @@ -0,0 +1,77 @@ +package metadata + +import ( + "io/ioutil" + "os" + "path/filepath" + "sync" +) + +// Store implements a K/V store for mapping distribution-related IDs +// to on-disk layer IDs and image IDs. The namespace identifies the type of +// mapping (i.e. "v1ids" or "artifacts"). MetadataStore is goroutine-safe. +type Store interface { + // Get retrieves data by namespace and key. + Get(namespace string, key string) ([]byte, error) + // Set writes data indexed by namespace and key. + Set(namespace, key string, value []byte) error + // Delete removes data indexed by namespace and key. + Delete(namespace, key string) error +} + +// FSMetadataStore uses the filesystem to associate metadata with layer and +// image IDs. +type FSMetadataStore struct { + sync.RWMutex + basePath string +} + +// NewFSMetadataStore creates a new filesystem-based metadata store. +func NewFSMetadataStore(basePath string) (*FSMetadataStore, error) { + if err := os.MkdirAll(basePath, 0700); err != nil { + return nil, err + } + return &FSMetadataStore{ + basePath: basePath, + }, nil +} + +func (store *FSMetadataStore) path(namespace, key string) string { + return filepath.Join(store.basePath, namespace, key) +} + +// Get retrieves data by namespace and key. The data is read from a file named +// after the key, stored in the namespace's directory. +func (store *FSMetadataStore) Get(namespace string, key string) ([]byte, error) { + store.RLock() + defer store.RUnlock() + + return ioutil.ReadFile(store.path(namespace, key)) +} + +// Set writes data indexed by namespace and key. The data is written to a file +// named after the key, stored in the namespace's directory. +func (store *FSMetadataStore) Set(namespace, key string, value []byte) error { + store.Lock() + defer store.Unlock() + + path := store.path(namespace, key) + tempFilePath := path + ".tmp" + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return err + } + if err := ioutil.WriteFile(tempFilePath, value, 0644); err != nil { + return err + } + return os.Rename(tempFilePath, path) +} + +// Delete removes data indexed by namespace and key. The data file named after +// the key, stored in the namespace's directory is deleted. +func (store *FSMetadataStore) Delete(namespace, key string) error { + store.Lock() + defer store.Unlock() + + path := store.path(namespace, key) + return os.Remove(path) +} diff --git a/distribution/metadata/v1_id_service.go b/distribution/metadata/v1_id_service.go new file mode 100644 index 00000000..f6e45892 --- /dev/null +++ b/distribution/metadata/v1_id_service.go @@ -0,0 +1,44 @@ +package metadata + +import ( + "github.com/docker/docker/image/v1" + "github.com/docker/docker/layer" +) + +// V1IDService maps v1 IDs to layers on disk. +type V1IDService struct { + store Store +} + +// NewV1IDService creates a new V1 ID mapping service. +func NewV1IDService(store Store) *V1IDService { + return &V1IDService{ + store: store, + } +} + +// namespace returns the namespace used by this service. +func (idserv *V1IDService) namespace() string { + return "v1id" +} + +// Get finds a layer by its V1 ID. +func (idserv *V1IDService) Get(v1ID, registry string) (layer.DiffID, error) { + if err := v1.ValidateID(v1ID); err != nil { + return layer.DiffID(""), err + } + + idBytes, err := idserv.store.Get(idserv.namespace(), registry+","+v1ID) + if err != nil { + return layer.DiffID(""), err + } + return layer.DiffID(idBytes), nil +} + +// Set associates an image with a V1 ID. +func (idserv *V1IDService) Set(v1ID, registry string, id layer.DiffID) error { + if err := v1.ValidateID(v1ID); err != nil { + return err + } + return idserv.store.Set(idserv.namespace(), registry+","+v1ID, []byte(id)) +} diff --git a/distribution/metadata/v1_id_service_test.go b/distribution/metadata/v1_id_service_test.go new file mode 100644 index 00000000..55688658 --- /dev/null +++ b/distribution/metadata/v1_id_service_test.go @@ -0,0 +1,83 @@ +package metadata + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/docker/docker/layer" +) + +func TestV1IDService(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "v1-id-service-test") + if err != nil { + t.Fatalf("could not create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + metadataStore, err := NewFSMetadataStore(tmpDir) + if err != nil { + t.Fatalf("could not create metadata store: %v", err) + } + v1IDService := NewV1IDService(metadataStore) + + testVectors := []struct { + registry string + v1ID string + layerID layer.DiffID + }{ + { + registry: "registry1", + v1ID: "f0cd5ca10b07f35512fc2f1cbf9a6cefbdb5cba70ac6b0c9e5988f4497f71937", + layerID: layer.DiffID("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"), + }, + { + registry: "registry2", + v1ID: "9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e", + layerID: layer.DiffID("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa"), + }, + { + registry: "registry1", + v1ID: "9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e", + layerID: layer.DiffID("sha256:03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb"), + }, + } + + // Set some associations + for _, vec := range testVectors { + err := v1IDService.Set(vec.v1ID, vec.registry, vec.layerID) + if err != nil { + t.Fatalf("error calling Set: %v", err) + } + } + + // Check the correct values are read back + for _, vec := range testVectors { + layerID, err := v1IDService.Get(vec.v1ID, vec.registry) + if err != nil { + t.Fatalf("error calling Get: %v", err) + } + if layerID != vec.layerID { + t.Fatal("Get returned incorrect layer ID") + } + } + + // Test Get on a nonexistent entry + _, err = v1IDService.Get("82379823067823853223359023576437723560923756b03560378f4497753917", "registry1") + if err == nil { + t.Fatal("expected error looking up nonexistent entry") + } + + // Overwrite one of the entries and read it back + err = v1IDService.Set(testVectors[0].v1ID, testVectors[0].registry, testVectors[1].layerID) + if err != nil { + t.Fatalf("error calling Set: %v", err) + } + layerID, err := v1IDService.Get(testVectors[0].v1ID, testVectors[0].registry) + if err != nil { + t.Fatalf("error calling Get: %v", err) + } + if layerID != testVectors[1].layerID { + t.Fatal("Get returned incorrect layer ID") + } +} diff --git a/distribution/metadata/v2_metadata_service.go b/distribution/metadata/v2_metadata_service.go new file mode 100644 index 00000000..239cd1f4 --- /dev/null +++ b/distribution/metadata/v2_metadata_service.go @@ -0,0 +1,137 @@ +package metadata + +import ( + "encoding/json" + + "github.com/docker/distribution/digest" + "github.com/docker/docker/layer" +) + +// V2MetadataService maps layer IDs to a set of known metadata for +// the layer. +type V2MetadataService struct { + store Store +} + +// V2Metadata contains the digest and source repository information for a layer. +type V2Metadata struct { + Digest digest.Digest + SourceRepository string +} + +// maxMetadata is the number of metadata entries to keep per layer DiffID. +const maxMetadata = 50 + +// NewV2MetadataService creates a new diff ID to v2 metadata mapping service. +func NewV2MetadataService(store Store) *V2MetadataService { + return &V2MetadataService{ + store: store, + } +} + +func (serv *V2MetadataService) diffIDNamespace() string { + return "v2metadata-by-diffid" +} + +func (serv *V2MetadataService) digestNamespace() string { + return "diffid-by-digest" +} + +func (serv *V2MetadataService) diffIDKey(diffID layer.DiffID) string { + return string(digest.Digest(diffID).Algorithm()) + "/" + digest.Digest(diffID).Hex() +} + +func (serv *V2MetadataService) digestKey(dgst digest.Digest) string { + return string(dgst.Algorithm()) + "/" + dgst.Hex() +} + +// GetMetadata finds the metadata associated with a layer DiffID. +func (serv *V2MetadataService) GetMetadata(diffID layer.DiffID) ([]V2Metadata, error) { + jsonBytes, err := serv.store.Get(serv.diffIDNamespace(), serv.diffIDKey(diffID)) + if err != nil { + return nil, err + } + + var metadata []V2Metadata + if err := json.Unmarshal(jsonBytes, &metadata); err != nil { + return nil, err + } + + return metadata, nil +} + +// GetDiffID finds a layer DiffID from a digest. +func (serv *V2MetadataService) GetDiffID(dgst digest.Digest) (layer.DiffID, error) { + diffIDBytes, err := serv.store.Get(serv.digestNamespace(), serv.digestKey(dgst)) + if err != nil { + return layer.DiffID(""), err + } + + return layer.DiffID(diffIDBytes), nil +} + +// Add associates metadata with a layer DiffID. If too many metadata entries are +// present, the oldest one is dropped. +func (serv *V2MetadataService) Add(diffID layer.DiffID, metadata V2Metadata) error { + oldMetadata, err := serv.GetMetadata(diffID) + if err != nil { + oldMetadata = nil + } + newMetadata := make([]V2Metadata, 0, len(oldMetadata)+1) + + // Copy all other metadata to new slice + for _, oldMeta := range oldMetadata { + if oldMeta != metadata { + newMetadata = append(newMetadata, oldMeta) + } + } + + newMetadata = append(newMetadata, metadata) + + if len(newMetadata) > maxMetadata { + newMetadata = newMetadata[len(newMetadata)-maxMetadata:] + } + + jsonBytes, err := json.Marshal(newMetadata) + if err != nil { + return err + } + + err = serv.store.Set(serv.diffIDNamespace(), serv.diffIDKey(diffID), jsonBytes) + if err != nil { + return err + } + + return serv.store.Set(serv.digestNamespace(), serv.digestKey(metadata.Digest), []byte(diffID)) +} + +// Remove unassociates a metadata entry from a layer DiffID. +func (serv *V2MetadataService) Remove(metadata V2Metadata) error { + diffID, err := serv.GetDiffID(metadata.Digest) + if err != nil { + return err + } + oldMetadata, err := serv.GetMetadata(diffID) + if err != nil { + oldMetadata = nil + } + newMetadata := make([]V2Metadata, 0, len(oldMetadata)) + + // Copy all other metadata to new slice + for _, oldMeta := range oldMetadata { + if oldMeta != metadata { + newMetadata = append(newMetadata, oldMeta) + } + } + + if len(newMetadata) == 0 { + return serv.store.Delete(serv.diffIDNamespace(), serv.diffIDKey(diffID)) + } + + jsonBytes, err := json.Marshal(newMetadata) + if err != nil { + return err + } + + return serv.store.Set(serv.diffIDNamespace(), serv.diffIDKey(diffID), jsonBytes) +} diff --git a/distribution/metadata/v2_metadata_service_test.go b/distribution/metadata/v2_metadata_service_test.go new file mode 100644 index 00000000..7b0ecb15 --- /dev/null +++ b/distribution/metadata/v2_metadata_service_test.go @@ -0,0 +1,115 @@ +package metadata + +import ( + "encoding/hex" + "io/ioutil" + "math/rand" + "os" + "reflect" + "testing" + + "github.com/docker/distribution/digest" + "github.com/docker/docker/layer" +) + +func TestV2MetadataService(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "blobsum-storage-service-test") + if err != nil { + t.Fatalf("could not create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + metadataStore, err := NewFSMetadataStore(tmpDir) + if err != nil { + t.Fatalf("could not create metadata store: %v", err) + } + V2MetadataService := NewV2MetadataService(metadataStore) + + tooManyBlobSums := make([]V2Metadata, 100) + for i := range tooManyBlobSums { + randDigest := randomDigest() + tooManyBlobSums[i] = V2Metadata{Digest: randDigest} + } + + testVectors := []struct { + diffID layer.DiffID + metadata []V2Metadata + }{ + { + diffID: layer.DiffID("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"), + metadata: []V2Metadata{ + {Digest: digest.Digest("sha256:f0cd5ca10b07f35512fc2f1cbf9a6cefbdb5cba70ac6b0c9e5988f4497f71937")}, + }, + }, + { + diffID: layer.DiffID("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa"), + metadata: []V2Metadata{ + {Digest: digest.Digest("sha256:f0cd5ca10b07f35512fc2f1cbf9a6cefbdb5cba70ac6b0c9e5988f4497f71937")}, + {Digest: digest.Digest("sha256:9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e")}, + }, + }, + { + diffID: layer.DiffID("sha256:03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb"), + metadata: tooManyBlobSums, + }, + } + + // Set some associations + for _, vec := range testVectors { + for _, blobsum := range vec.metadata { + err := V2MetadataService.Add(vec.diffID, blobsum) + if err != nil { + t.Fatalf("error calling Set: %v", err) + } + } + } + + // Check the correct values are read back + for _, vec := range testVectors { + metadata, err := V2MetadataService.GetMetadata(vec.diffID) + if err != nil { + t.Fatalf("error calling Get: %v", err) + } + expectedMetadataEntries := len(vec.metadata) + if expectedMetadataEntries > 50 { + expectedMetadataEntries = 50 + } + if !reflect.DeepEqual(metadata, vec.metadata[len(vec.metadata)-expectedMetadataEntries:len(vec.metadata)]) { + t.Fatal("Get returned incorrect layer ID") + } + } + + // Test GetMetadata on a nonexistent entry + _, err = V2MetadataService.GetMetadata(layer.DiffID("sha256:82379823067823853223359023576437723560923756b03560378f4497753917")) + if err == nil { + t.Fatal("expected error looking up nonexistent entry") + } + + // Test GetDiffID on a nonexistent entry + _, err = V2MetadataService.GetDiffID(digest.Digest("sha256:82379823067823853223359023576437723560923756b03560378f4497753917")) + if err == nil { + t.Fatal("expected error looking up nonexistent entry") + } + + // Overwrite one of the entries and read it back + err = V2MetadataService.Add(testVectors[1].diffID, testVectors[0].metadata[0]) + if err != nil { + t.Fatalf("error calling Add: %v", err) + } + diffID, err := V2MetadataService.GetDiffID(testVectors[0].metadata[0].Digest) + if err != nil { + t.Fatalf("error calling GetDiffID: %v", err) + } + if diffID != testVectors[1].diffID { + t.Fatal("GetDiffID returned incorrect diffID") + } +} + +func randomDigest() digest.Digest { + b := [32]byte{} + for i := 0; i < len(b); i++ { + b[i] = byte(rand.Intn(256)) + } + d := hex.EncodeToString(b[:]) + return digest.Digest("sha256:" + d) +} diff --git a/distribution/pull.go b/distribution/pull.go new file mode 100644 index 00000000..4b42371b --- /dev/null +++ b/distribution/pull.go @@ -0,0 +1,205 @@ +package distribution + +import ( + "fmt" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api" + "github.com/docker/docker/distribution/metadata" + "github.com/docker/docker/distribution/xfer" + "github.com/docker/docker/image" + "github.com/docker/docker/pkg/progress" + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" + "github.com/docker/engine-api/types" + "golang.org/x/net/context" +) + +// ImagePullConfig stores pull configuration. +type ImagePullConfig struct { + // MetaHeaders stores HTTP headers with metadata about the image + MetaHeaders map[string][]string + // AuthConfig holds authentication credentials for authenticating with + // the registry. + AuthConfig *types.AuthConfig + // ProgressOutput is the interface for showing the status of the pull + // operation. + ProgressOutput progress.Output + // RegistryService is the registry service to use for TLS configuration + // and endpoint lookup. + RegistryService *registry.Service + // ImageEventLogger notifies events for a given image + ImageEventLogger func(id, name, action string) + // MetadataStore is the storage backend for distribution-specific + // metadata. + MetadataStore metadata.Store + // ImageStore manages images. + ImageStore image.Store + // ReferenceStore manages tags. + ReferenceStore reference.Store + // DownloadManager manages concurrent pulls. + DownloadManager *xfer.LayerDownloadManager +} + +// Puller is an interface that abstracts pulling for different API versions. +type Puller interface { + // Pull tries to pull the image referenced by `tag` + // Pull returns an error if any, as well as a boolean that determines whether to retry Pull on the next configured endpoint. + // + Pull(ctx context.Context, ref reference.Named) error +} + +// newPuller returns a Puller interface that will pull from either a v1 or v2 +// registry. The endpoint argument contains a Version field that determines +// whether a v1 or v2 puller will be created. The other parameters are passed +// through to the underlying puller implementation for use during the actual +// pull operation. +func newPuller(endpoint registry.APIEndpoint, repoInfo *registry.RepositoryInfo, imagePullConfig *ImagePullConfig) (Puller, error) { + switch endpoint.Version { + case registry.APIVersion2: + return &v2Puller{ + V2MetadataService: metadata.NewV2MetadataService(imagePullConfig.MetadataStore), + endpoint: endpoint, + config: imagePullConfig, + repoInfo: repoInfo, + }, nil + case registry.APIVersion1: + return &v1Puller{ + v1IDService: metadata.NewV1IDService(imagePullConfig.MetadataStore), + endpoint: endpoint, + config: imagePullConfig, + repoInfo: repoInfo, + }, nil + } + return nil, fmt.Errorf("unknown version %d for registry %s", endpoint.Version, endpoint.URL) +} + +// Pull initiates a pull operation. image is the repository name to pull, and +// tag may be either empty, or indicate a specific tag to pull. +func Pull(ctx context.Context, ref reference.Named, imagePullConfig *ImagePullConfig) error { + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := imagePullConfig.RegistryService.ResolveRepository(ref) + if err != nil { + return err + } + + // makes sure name is not empty or `scratch` + if err := validateRepoName(repoInfo.Name()); err != nil { + return err + } + + endpoints, err := imagePullConfig.RegistryService.LookupPullEndpoints(repoInfo.Hostname()) + if err != nil { + return err + } + + var ( + lastErr error + + // discardNoSupportErrors is used to track whether an endpoint encountered an error of type registry.ErrNoSupport + // By default it is false, which means that if a ErrNoSupport error is encountered, it will be saved in lastErr. + // As soon as another kind of error is encountered, discardNoSupportErrors is set to true, avoiding the saving of + // any subsequent ErrNoSupport errors in lastErr. + // It's needed for pull-by-digest on v1 endpoints: if there are only v1 endpoints configured, the error should be + // returned and displayed, but if there was a v2 endpoint which supports pull-by-digest, then the last relevant + // error is the ones from v2 endpoints not v1. + discardNoSupportErrors bool + + // confirmedV2 is set to true if a pull attempt managed to + // confirm that it was talking to a v2 registry. This will + // prevent fallback to the v1 protocol. + confirmedV2 bool + + // confirmedTLSRegistries is a map indicating which registries + // are known to be using TLS. There should never be a plaintext + // retry for any of these. + confirmedTLSRegistries = make(map[string]struct{}) + ) + for _, endpoint := range endpoints { + if confirmedV2 && endpoint.Version == registry.APIVersion1 { + logrus.Debugf("Skipping v1 endpoint %s because v2 registry was detected", endpoint.URL) + continue + } + + if endpoint.URL.Scheme != "https" { + if _, confirmedTLS := confirmedTLSRegistries[endpoint.URL.Host]; confirmedTLS { + logrus.Debugf("Skipping non-TLS endpoint %s for host/port that appears to use TLS", endpoint.URL) + continue + } + } + + logrus.Debugf("Trying to pull %s from %s %s", repoInfo.Name(), endpoint.URL, endpoint.Version) + + puller, err := newPuller(endpoint, repoInfo, imagePullConfig) + if err != nil { + lastErr = err + continue + } + if err := puller.Pull(ctx, ref); err != nil { + // Was this pull cancelled? If so, don't try to fall + // back. + fallback := false + select { + case <-ctx.Done(): + default: + if fallbackErr, ok := err.(fallbackError); ok { + fallback = true + confirmedV2 = confirmedV2 || fallbackErr.confirmedV2 + if fallbackErr.transportOK && endpoint.URL.Scheme == "https" { + confirmedTLSRegistries[endpoint.URL.Host] = struct{}{} + } + err = fallbackErr.err + } + } + if fallback { + if _, ok := err.(ErrNoSupport); !ok { + // Because we found an error that's not ErrNoSupport, discard all subsequent ErrNoSupport errors. + discardNoSupportErrors = true + // append subsequent errors + lastErr = err + } else if !discardNoSupportErrors { + // Save the ErrNoSupport error, because it's either the first error or all encountered errors + // were also ErrNoSupport errors. + // append subsequent errors + lastErr = err + } + logrus.Errorf("Attempting next endpoint for pull after error: %v", err) + continue + } + logrus.Errorf("Not continuing with pull after error: %v", err) + return err + } + + imagePullConfig.ImageEventLogger(ref.String(), repoInfo.Name(), "pull") + return nil + } + + if lastErr == nil { + lastErr = fmt.Errorf("no endpoints found for %s", ref.String()) + } + + return lastErr +} + +// writeStatus writes a status message to out. If layersDownloaded is true, the +// status message indicates that a newer image was downloaded. Otherwise, it +// indicates that the image is up to date. requestedTag is the tag the message +// will refer to. +func writeStatus(requestedTag string, out progress.Output, layersDownloaded bool) { + if layersDownloaded { + progress.Message(out, "", "Status: Downloaded newer image for "+requestedTag) + } else { + progress.Message(out, "", "Status: Image is up to date for "+requestedTag) + } +} + +// validateRepoName validates the name of a repository. +func validateRepoName(name string) error { + if name == "" { + return fmt.Errorf("Repository name can't be empty") + } + if name == api.NoBaseImageSpecifier { + return fmt.Errorf("'%s' is a reserved name", api.NoBaseImageSpecifier) + } + return nil +} diff --git a/distribution/pull_v1.go b/distribution/pull_v1.go new file mode 100644 index 00000000..86fad2ef --- /dev/null +++ b/distribution/pull_v1.go @@ -0,0 +1,362 @@ +package distribution + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "net" + "net/url" + "os" + "strings" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/registry/client/transport" + "github.com/docker/docker/distribution/metadata" + "github.com/docker/docker/distribution/xfer" + "github.com/docker/docker/dockerversion" + "github.com/docker/docker/image" + "github.com/docker/docker/image/v1" + "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/progress" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" + "golang.org/x/net/context" +) + +type v1Puller struct { + v1IDService *metadata.V1IDService + endpoint registry.APIEndpoint + config *ImagePullConfig + repoInfo *registry.RepositoryInfo + session *registry.Session +} + +func (p *v1Puller) Pull(ctx context.Context, ref reference.Named) error { + if _, isCanonical := ref.(reference.Canonical); isCanonical { + // Allowing fallback, because HTTPS v1 is before HTTP v2 + return fallbackError{err: ErrNoSupport{Err: errors.New("Cannot pull by digest with v1 registry")}} + } + + tlsConfig, err := p.config.RegistryService.TLSConfig(p.repoInfo.Index.Name) + if err != nil { + return err + } + // Adds Docker-specific headers as well as user-specified headers (metaHeaders) + tr := transport.NewTransport( + // TODO(tiborvass): was ReceiveTimeout + registry.NewTransport(tlsConfig), + registry.DockerHeaders(dockerversion.DockerUserAgent(ctx), p.config.MetaHeaders)..., + ) + client := registry.HTTPClient(tr) + v1Endpoint, err := p.endpoint.ToV1Endpoint(dockerversion.DockerUserAgent(ctx), p.config.MetaHeaders) + if err != nil { + logrus.Debugf("Could not get v1 endpoint: %v", err) + return fallbackError{err: err} + } + p.session, err = registry.NewSession(client, p.config.AuthConfig, v1Endpoint) + if err != nil { + // TODO(dmcgowan): Check if should fallback + logrus.Debugf("Fallback from error: %s", err) + return fallbackError{err: err} + } + if err := p.pullRepository(ctx, ref); err != nil { + // TODO(dmcgowan): Check if should fallback + return err + } + progress.Message(p.config.ProgressOutput, "", p.repoInfo.FullName()+": this image was pulled from a legacy registry. Important: This registry version will not be supported in future versions of docker.") + + return nil +} + +func (p *v1Puller) pullRepository(ctx context.Context, ref reference.Named) error { + progress.Message(p.config.ProgressOutput, "", "Pulling repository "+p.repoInfo.FullName()) + + repoData, err := p.session.GetRepositoryData(p.repoInfo) + if err != nil { + if strings.Contains(err.Error(), "HTTP code: 404") { + return fmt.Errorf("Error: image %s not found", p.repoInfo.RemoteName()) + } + // Unexpected HTTP error + return err + } + + logrus.Debugf("Retrieving the tag list") + var tagsList map[string]string + tagged, isTagged := ref.(reference.NamedTagged) + if !isTagged { + tagsList, err = p.session.GetRemoteTags(repoData.Endpoints, p.repoInfo) + } else { + var tagID string + tagsList = make(map[string]string) + tagID, err = p.session.GetRemoteTag(repoData.Endpoints, p.repoInfo, tagged.Tag()) + if err == registry.ErrRepoNotFound { + return fmt.Errorf("Tag %s not found in repository %s", tagged.Tag(), p.repoInfo.FullName()) + } + tagsList[tagged.Tag()] = tagID + } + if err != nil { + logrus.Errorf("unable to get remote tags: %s", err) + return err + } + + for tag, id := range tagsList { + repoData.ImgList[id] = ®istry.ImgData{ + ID: id, + Tag: tag, + Checksum: "", + } + } + + layersDownloaded := false + for _, imgData := range repoData.ImgList { + if isTagged && imgData.Tag != tagged.Tag() { + continue + } + + err := p.downloadImage(ctx, repoData, imgData, &layersDownloaded) + if err != nil { + return err + } + } + + writeStatus(ref.String(), p.config.ProgressOutput, layersDownloaded) + return nil +} + +func (p *v1Puller) downloadImage(ctx context.Context, repoData *registry.RepositoryData, img *registry.ImgData, layersDownloaded *bool) error { + if img.Tag == "" { + logrus.Debugf("Image (id: %s) present in this repository but untagged, skipping", img.ID) + return nil + } + + localNameRef, err := reference.WithTag(p.repoInfo, img.Tag) + if err != nil { + retErr := fmt.Errorf("Image (id: %s) has invalid tag: %s", img.ID, img.Tag) + logrus.Debug(retErr.Error()) + return retErr + } + + if err := v1.ValidateID(img.ID); err != nil { + return err + } + + progress.Updatef(p.config.ProgressOutput, stringid.TruncateID(img.ID), "Pulling image (%s) from %s", img.Tag, p.repoInfo.FullName()) + success := false + var lastErr error + for _, ep := range p.repoInfo.Index.Mirrors { + ep += "v1/" + progress.Updatef(p.config.ProgressOutput, stringid.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s, mirror: %s", img.Tag, p.repoInfo.FullName(), ep)) + if err = p.pullImage(ctx, img.ID, ep, localNameRef, layersDownloaded); err != nil { + // Don't report errors when pulling from mirrors. + logrus.Debugf("Error pulling image (%s) from %s, mirror: %s, %s", img.Tag, p.repoInfo.FullName(), ep, err) + continue + } + success = true + break + } + if !success { + for _, ep := range repoData.Endpoints { + progress.Updatef(p.config.ProgressOutput, stringid.TruncateID(img.ID), "Pulling image (%s) from %s, endpoint: %s", img.Tag, p.repoInfo.FullName(), ep) + if err = p.pullImage(ctx, img.ID, ep, localNameRef, layersDownloaded); err != nil { + // It's not ideal that only the last error is returned, it would be better to concatenate the errors. + // As the error is also given to the output stream the user will see the error. + lastErr = err + progress.Updatef(p.config.ProgressOutput, stringid.TruncateID(img.ID), "Error pulling image (%s) from %s, endpoint: %s, %s", img.Tag, p.repoInfo.FullName(), ep, err) + continue + } + success = true + break + } + } + if !success { + err := fmt.Errorf("Error pulling image (%s) from %s, %v", img.Tag, p.repoInfo.FullName(), lastErr) + progress.Update(p.config.ProgressOutput, stringid.TruncateID(img.ID), err.Error()) + return err + } + return nil +} + +func (p *v1Puller) pullImage(ctx context.Context, v1ID, endpoint string, localNameRef reference.Named, layersDownloaded *bool) (err error) { + var history []string + history, err = p.session.GetRemoteHistory(v1ID, endpoint) + if err != nil { + return err + } + if len(history) < 1 { + return fmt.Errorf("empty history for image %s", v1ID) + } + progress.Update(p.config.ProgressOutput, stringid.TruncateID(v1ID), "Pulling dependent layers") + + var ( + descriptors []xfer.DownloadDescriptor + newHistory []image.History + imgJSON []byte + imgSize int64 + ) + + // Iterate over layers, in order from bottom-most to top-most. Download + // config for all layers and create descriptors. + for i := len(history) - 1; i >= 0; i-- { + v1LayerID := history[i] + imgJSON, imgSize, err = p.downloadLayerConfig(v1LayerID, endpoint) + if err != nil { + return err + } + + // Create a new-style config from the legacy configs + h, err := v1.HistoryFromConfig(imgJSON, false) + if err != nil { + return err + } + newHistory = append(newHistory, h) + + layerDescriptor := &v1LayerDescriptor{ + v1LayerID: v1LayerID, + indexName: p.repoInfo.Index.Name, + endpoint: endpoint, + v1IDService: p.v1IDService, + layersDownloaded: layersDownloaded, + layerSize: imgSize, + session: p.session, + } + + descriptors = append(descriptors, layerDescriptor) + } + + rootFS := image.NewRootFS() + resultRootFS, release, err := p.config.DownloadManager.Download(ctx, *rootFS, descriptors, p.config.ProgressOutput) + if err != nil { + return err + } + defer release() + + config, err := v1.MakeConfigFromV1Config(imgJSON, &resultRootFS, newHistory) + if err != nil { + return err + } + + imageID, err := p.config.ImageStore.Create(config) + if err != nil { + return err + } + + if err := p.config.ReferenceStore.AddTag(localNameRef, imageID, true); err != nil { + return err + } + + return nil +} + +func (p *v1Puller) downloadLayerConfig(v1LayerID, endpoint string) (imgJSON []byte, imgSize int64, err error) { + progress.Update(p.config.ProgressOutput, stringid.TruncateID(v1LayerID), "Pulling metadata") + + retries := 5 + for j := 1; j <= retries; j++ { + imgJSON, imgSize, err := p.session.GetRemoteImageJSON(v1LayerID, endpoint) + if err != nil && j == retries { + progress.Update(p.config.ProgressOutput, stringid.TruncateID(v1LayerID), "Error pulling layer metadata") + return nil, 0, err + } else if err != nil { + time.Sleep(time.Duration(j) * 500 * time.Millisecond) + continue + } + + return imgJSON, imgSize, nil + } + + // not reached + return nil, 0, nil +} + +type v1LayerDescriptor struct { + v1LayerID string + indexName string + endpoint string + v1IDService *metadata.V1IDService + layersDownloaded *bool + layerSize int64 + session *registry.Session + tmpFile *os.File +} + +func (ld *v1LayerDescriptor) Key() string { + return "v1:" + ld.v1LayerID +} + +func (ld *v1LayerDescriptor) ID() string { + return stringid.TruncateID(ld.v1LayerID) +} + +func (ld *v1LayerDescriptor) DiffID() (layer.DiffID, error) { + return ld.v1IDService.Get(ld.v1LayerID, ld.indexName) +} + +func (ld *v1LayerDescriptor) Download(ctx context.Context, progressOutput progress.Output) (io.ReadCloser, int64, error) { + progress.Update(progressOutput, ld.ID(), "Pulling fs layer") + layerReader, err := ld.session.GetRemoteImageLayer(ld.v1LayerID, ld.endpoint, ld.layerSize) + if err != nil { + progress.Update(progressOutput, ld.ID(), "Error pulling dependent layers") + if uerr, ok := err.(*url.Error); ok { + err = uerr.Err + } + if terr, ok := err.(net.Error); ok && terr.Timeout() { + return nil, 0, err + } + return nil, 0, xfer.DoNotRetry{Err: err} + } + *ld.layersDownloaded = true + + ld.tmpFile, err = ioutil.TempFile("", "GetImageBlob") + if err != nil { + layerReader.Close() + return nil, 0, err + } + + reader := progress.NewProgressReader(ioutils.NewCancelReadCloser(ctx, layerReader), progressOutput, ld.layerSize, ld.ID(), "Downloading") + defer reader.Close() + + _, err = io.Copy(ld.tmpFile, reader) + if err != nil { + ld.Close() + return nil, 0, err + } + + progress.Update(progressOutput, ld.ID(), "Download complete") + + logrus.Debugf("Downloaded %s to tempfile %s", ld.ID(), ld.tmpFile.Name()) + + ld.tmpFile.Seek(0, 0) + + // hand off the temporary file to the download manager, so it will only + // be closed once + tmpFile := ld.tmpFile + ld.tmpFile = nil + + return ioutils.NewReadCloserWrapper(tmpFile, func() error { + tmpFile.Close() + err := os.RemoveAll(tmpFile.Name()) + if err != nil { + logrus.Errorf("Failed to remove temp file: %s", tmpFile.Name()) + } + return err + }), ld.layerSize, nil +} + +func (ld *v1LayerDescriptor) Close() { + if ld.tmpFile != nil { + ld.tmpFile.Close() + if err := os.RemoveAll(ld.tmpFile.Name()); err != nil { + logrus.Errorf("Failed to remove temp file: %s", ld.tmpFile.Name()) + } + ld.tmpFile = nil + } +} + +func (ld *v1LayerDescriptor) Registered(diffID layer.DiffID) { + // Cache mapping from this layer's DiffID to the blobsum + ld.v1IDService.Set(ld.v1LayerID, ld.indexName, diffID) +} diff --git a/distribution/pull_v2.go b/distribution/pull_v2.go new file mode 100644 index 00000000..748ef422 --- /dev/null +++ b/distribution/pull_v2.go @@ -0,0 +1,840 @@ +package distribution + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/url" + "os" + "runtime" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution" + "github.com/docker/distribution/digest" + "github.com/docker/distribution/manifest/manifestlist" + "github.com/docker/distribution/manifest/schema1" + "github.com/docker/distribution/manifest/schema2" + "github.com/docker/distribution/registry/api/errcode" + "github.com/docker/distribution/registry/client/auth" + "github.com/docker/distribution/registry/client/transport" + "github.com/docker/docker/distribution/metadata" + "github.com/docker/docker/distribution/xfer" + "github.com/docker/docker/image" + "github.com/docker/docker/image/v1" + "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/progress" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" + "golang.org/x/net/context" +) + +var errRootFSMismatch = errors.New("layers from manifest don't match image configuration") + +// ImageConfigPullError is an error pulling the image config blob +// (only applies to schema2). +type ImageConfigPullError struct { + Err error +} + +// Error returns the error string for ImageConfigPullError. +func (e ImageConfigPullError) Error() string { + return "error pulling image configuration: " + e.Err.Error() +} + +type v2Puller struct { + V2MetadataService *metadata.V2MetadataService + endpoint registry.APIEndpoint + config *ImagePullConfig + repoInfo *registry.RepositoryInfo + repo distribution.Repository + // confirmedV2 is set to true if we confirm we're talking to a v2 + // registry. This is used to limit fallbacks to the v1 protocol. + confirmedV2 bool +} + +func (p *v2Puller) Pull(ctx context.Context, ref reference.Named) (err error) { + // TODO(tiborvass): was ReceiveTimeout + p.repo, p.confirmedV2, err = NewV2Repository(ctx, p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "pull") + if err != nil { + logrus.Warnf("Error getting v2 registry: %v", err) + return err + } + + if err = p.pullV2Repository(ctx, ref); err != nil { + if _, ok := err.(fallbackError); ok { + return err + } + if continueOnError(err) { + logrus.Errorf("Error trying v2 registry: %v", err) + return fallbackError{ + err: err, + confirmedV2: p.confirmedV2, + transportOK: true, + } + } + } + return err +} + +func (p *v2Puller) pullV2Repository(ctx context.Context, ref reference.Named) (err error) { + var layersDownloaded bool + if !reference.IsNameOnly(ref) { + layersDownloaded, err = p.pullV2Tag(ctx, ref) + if err != nil { + return err + } + } else { + tags, err := p.repo.Tags(ctx).All(ctx) + if err != nil { + // If this repository doesn't exist on V2, we should + // permit a fallback to V1. + return allowV1Fallback(err) + } + + // The v2 registry knows about this repository, so we will not + // allow fallback to the v1 protocol even if we encounter an + // error later on. + p.confirmedV2 = true + + for _, tag := range tags { + tagRef, err := reference.WithTag(ref, tag) + if err != nil { + return err + } + pulledNew, err := p.pullV2Tag(ctx, tagRef) + if err != nil { + // Since this is the pull-all-tags case, don't + // allow an error pulling a particular tag to + // make the whole pull fall back to v1. + if fallbackErr, ok := err.(fallbackError); ok { + return fallbackErr.err + } + return err + } + // pulledNew is true if either new layers were downloaded OR if existing images were newly tagged + // TODO(tiborvass): should we change the name of `layersDownload`? What about message in WriteStatus? + layersDownloaded = layersDownloaded || pulledNew + } + } + + writeStatus(ref.String(), p.config.ProgressOutput, layersDownloaded) + + return nil +} + +type v2LayerDescriptor struct { + digest digest.Digest + repoInfo *registry.RepositoryInfo + repo distribution.Repository + V2MetadataService *metadata.V2MetadataService + tmpFile *os.File + verifier digest.Verifier +} + +func (ld *v2LayerDescriptor) Key() string { + return "v2:" + ld.digest.String() +} + +func (ld *v2LayerDescriptor) ID() string { + return stringid.TruncateID(ld.digest.String()) +} + +func (ld *v2LayerDescriptor) DiffID() (layer.DiffID, error) { + return ld.V2MetadataService.GetDiffID(ld.digest) +} + +func (ld *v2LayerDescriptor) Download(ctx context.Context, progressOutput progress.Output) (io.ReadCloser, int64, error) { + logrus.Debugf("pulling blob %q", ld.digest) + + var ( + err error + offset int64 + ) + + if ld.tmpFile == nil { + ld.tmpFile, err = createDownloadFile() + if err != nil { + return nil, 0, xfer.DoNotRetry{Err: err} + } + } else { + offset, err = ld.tmpFile.Seek(0, os.SEEK_END) + if err != nil { + logrus.Debugf("error seeking to end of download file: %v", err) + offset = 0 + + ld.tmpFile.Close() + if err := os.Remove(ld.tmpFile.Name()); err != nil { + logrus.Errorf("Failed to remove temp file: %s", ld.tmpFile.Name()) + } + ld.tmpFile, err = createDownloadFile() + if err != nil { + return nil, 0, xfer.DoNotRetry{Err: err} + } + } else if offset != 0 { + logrus.Debugf("attempting to resume download of %q from %d bytes", ld.digest, offset) + } + } + + tmpFile := ld.tmpFile + blobs := ld.repo.Blobs(ctx) + + layerDownload, err := blobs.Open(ctx, ld.digest) + if err != nil { + logrus.Errorf("Error initiating layer download: %v", err) + if err == distribution.ErrBlobUnknown { + return nil, 0, xfer.DoNotRetry{Err: err} + } + return nil, 0, retryOnError(err) + } + + if offset != 0 { + _, err := layerDownload.Seek(offset, os.SEEK_SET) + if err != nil { + if err := ld.truncateDownloadFile(); err != nil { + return nil, 0, xfer.DoNotRetry{Err: err} + } + return nil, 0, err + } + } + size, err := layerDownload.Seek(0, os.SEEK_END) + if err != nil { + // Seek failed, perhaps because there was no Content-Length + // header. This shouldn't fail the download, because we can + // still continue without a progress bar. + size = 0 + } else { + if size != 0 && offset > size { + logrus.Debugf("Partial download is larger than full blob. Starting over") + offset = 0 + if err := ld.truncateDownloadFile(); err != nil { + return nil, 0, xfer.DoNotRetry{Err: err} + } + } + + // Restore the seek offset either at the beginning of the + // stream, or just after the last byte we have from previous + // attempts. + _, err = layerDownload.Seek(offset, os.SEEK_SET) + if err != nil { + return nil, 0, err + } + } + + reader := progress.NewProgressReader(ioutils.NewCancelReadCloser(ctx, layerDownload), progressOutput, size-offset, ld.ID(), "Downloading") + defer reader.Close() + + if ld.verifier == nil { + ld.verifier, err = digest.NewDigestVerifier(ld.digest) + if err != nil { + return nil, 0, xfer.DoNotRetry{Err: err} + } + } + + _, err = io.Copy(tmpFile, io.TeeReader(reader, ld.verifier)) + if err != nil { + if err == transport.ErrWrongCodeForByteRange { + if err := ld.truncateDownloadFile(); err != nil { + return nil, 0, xfer.DoNotRetry{Err: err} + } + return nil, 0, err + } + return nil, 0, retryOnError(err) + } + + progress.Update(progressOutput, ld.ID(), "Verifying Checksum") + + if !ld.verifier.Verified() { + err = fmt.Errorf("filesystem layer verification failed for digest %s", ld.digest) + logrus.Error(err) + + // Allow a retry if this digest verification error happened + // after a resumed download. + if offset != 0 { + if err := ld.truncateDownloadFile(); err != nil { + return nil, 0, xfer.DoNotRetry{Err: err} + } + + return nil, 0, err + } + return nil, 0, xfer.DoNotRetry{Err: err} + } + + progress.Update(progressOutput, ld.ID(), "Download complete") + + logrus.Debugf("Downloaded %s to tempfile %s", ld.ID(), tmpFile.Name()) + + _, err = tmpFile.Seek(0, os.SEEK_SET) + if err != nil { + tmpFile.Close() + if err := os.Remove(tmpFile.Name()); err != nil { + logrus.Errorf("Failed to remove temp file: %s", tmpFile.Name()) + } + ld.tmpFile = nil + ld.verifier = nil + return nil, 0, xfer.DoNotRetry{Err: err} + } + + // hand off the temporary file to the download manager, so it will only + // be closed once + ld.tmpFile = nil + + return ioutils.NewReadCloserWrapper(tmpFile, func() error { + tmpFile.Close() + err := os.RemoveAll(tmpFile.Name()) + if err != nil { + logrus.Errorf("Failed to remove temp file: %s", tmpFile.Name()) + } + return err + }), size, nil +} + +func (ld *v2LayerDescriptor) Close() { + if ld.tmpFile != nil { + ld.tmpFile.Close() + if err := os.RemoveAll(ld.tmpFile.Name()); err != nil { + logrus.Errorf("Failed to remove temp file: %s", ld.tmpFile.Name()) + } + } +} + +func (ld *v2LayerDescriptor) truncateDownloadFile() error { + // Need a new hash context since we will be redoing the download + ld.verifier = nil + + if _, err := ld.tmpFile.Seek(0, os.SEEK_SET); err != nil { + logrus.Errorf("error seeking to beginning of download file: %v", err) + return err + } + + if err := ld.tmpFile.Truncate(0); err != nil { + logrus.Errorf("error truncating download file: %v", err) + return err + } + + return nil +} + +func (ld *v2LayerDescriptor) Registered(diffID layer.DiffID) { + // Cache mapping from this layer's DiffID to the blobsum + ld.V2MetadataService.Add(diffID, metadata.V2Metadata{Digest: ld.digest, SourceRepository: ld.repoInfo.FullName()}) +} + +func (p *v2Puller) pullV2Tag(ctx context.Context, ref reference.Named) (tagUpdated bool, err error) { + manSvc, err := p.repo.Manifests(ctx) + if err != nil { + return false, err + } + + var ( + manifest distribution.Manifest + tagOrDigest string // Used for logging/progress only + ) + if tagged, isTagged := ref.(reference.NamedTagged); isTagged { + // NOTE: not using TagService.Get, since it uses HEAD requests + // against the manifests endpoint, which are not supported by + // all registry versions. + manifest, err = manSvc.Get(ctx, "", distribution.WithTag(tagged.Tag())) + if err != nil { + return false, allowV1Fallback(err) + } + tagOrDigest = tagged.Tag() + } else if digested, isDigested := ref.(reference.Canonical); isDigested { + manifest, err = manSvc.Get(ctx, digested.Digest()) + if err != nil { + return false, err + } + tagOrDigest = digested.Digest().String() + } else { + return false, fmt.Errorf("internal error: reference has neither a tag nor a digest: %s", ref.String()) + } + + if manifest == nil { + return false, fmt.Errorf("image manifest does not exist for tag or digest %q", tagOrDigest) + } + + // If manSvc.Get succeeded, we can be confident that the registry on + // the other side speaks the v2 protocol. + p.confirmedV2 = true + + logrus.Debugf("Pulling ref from V2 registry: %s", ref.String()) + progress.Message(p.config.ProgressOutput, tagOrDigest, "Pulling from "+p.repo.Named().Name()) + + var ( + imageID image.ID + manifestDigest digest.Digest + ) + + switch v := manifest.(type) { + case *schema1.SignedManifest: + imageID, manifestDigest, err = p.pullSchema1(ctx, ref, v) + if err != nil { + return false, err + } + case *schema2.DeserializedManifest: + imageID, manifestDigest, err = p.pullSchema2(ctx, ref, v) + if err != nil { + return false, err + } + case *manifestlist.DeserializedManifestList: + imageID, manifestDigest, err = p.pullManifestList(ctx, ref, v) + if err != nil { + return false, err + } + default: + return false, errors.New("unsupported manifest format") + } + + progress.Message(p.config.ProgressOutput, "", "Digest: "+manifestDigest.String()) + + oldTagImageID, err := p.config.ReferenceStore.Get(ref) + if err == nil { + if oldTagImageID == imageID { + return false, nil + } + } else if err != reference.ErrDoesNotExist { + return false, err + } + + if canonical, ok := ref.(reference.Canonical); ok { + if err = p.config.ReferenceStore.AddDigest(canonical, imageID, true); err != nil { + return false, err + } + } else if err = p.config.ReferenceStore.AddTag(ref, imageID, true); err != nil { + return false, err + } + + return true, nil +} + +func (p *v2Puller) pullSchema1(ctx context.Context, ref reference.Named, unverifiedManifest *schema1.SignedManifest) (imageID image.ID, manifestDigest digest.Digest, err error) { + var verifiedManifest *schema1.Manifest + verifiedManifest, err = verifySchema1Manifest(unverifiedManifest, ref) + if err != nil { + return "", "", err + } + + rootFS := image.NewRootFS() + + if err := detectBaseLayer(p.config.ImageStore, verifiedManifest, rootFS); err != nil { + return "", "", err + } + + // remove duplicate layers and check parent chain validity + err = fixManifestLayers(verifiedManifest) + if err != nil { + return "", "", err + } + + var descriptors []xfer.DownloadDescriptor + + // Image history converted to the new format + var history []image.History + + // Note that the order of this loop is in the direction of bottom-most + // to top-most, so that the downloads slice gets ordered correctly. + for i := len(verifiedManifest.FSLayers) - 1; i >= 0; i-- { + blobSum := verifiedManifest.FSLayers[i].BlobSum + + var throwAway struct { + ThrowAway bool `json:"throwaway,omitempty"` + } + if err := json.Unmarshal([]byte(verifiedManifest.History[i].V1Compatibility), &throwAway); err != nil { + return "", "", err + } + + h, err := v1.HistoryFromConfig([]byte(verifiedManifest.History[i].V1Compatibility), throwAway.ThrowAway) + if err != nil { + return "", "", err + } + history = append(history, h) + + if throwAway.ThrowAway { + continue + } + + layerDescriptor := &v2LayerDescriptor{ + digest: blobSum, + repoInfo: p.repoInfo, + repo: p.repo, + V2MetadataService: p.V2MetadataService, + } + + descriptors = append(descriptors, layerDescriptor) + } + + resultRootFS, release, err := p.config.DownloadManager.Download(ctx, *rootFS, descriptors, p.config.ProgressOutput) + if err != nil { + return "", "", err + } + defer release() + + config, err := v1.MakeConfigFromV1Config([]byte(verifiedManifest.History[0].V1Compatibility), &resultRootFS, history) + if err != nil { + return "", "", err + } + + imageID, err = p.config.ImageStore.Create(config) + if err != nil { + return "", "", err + } + + manifestDigest = digest.FromBytes(unverifiedManifest.Canonical) + + return imageID, manifestDigest, nil +} + +func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *schema2.DeserializedManifest) (imageID image.ID, manifestDigest digest.Digest, err error) { + manifestDigest, err = schema2ManifestDigest(ref, mfst) + if err != nil { + return "", "", err + } + + target := mfst.Target() + imageID = image.ID(target.Digest) + if _, err := p.config.ImageStore.Get(imageID); err == nil { + // If the image already exists locally, no need to pull + // anything. + return imageID, manifestDigest, nil + } + + configChan := make(chan []byte, 1) + errChan := make(chan error, 1) + var cancel func() + ctx, cancel = context.WithCancel(ctx) + + // Pull the image config + go func() { + configJSON, err := p.pullSchema2ImageConfig(ctx, target.Digest) + if err != nil { + errChan <- ImageConfigPullError{Err: err} + cancel() + return + } + configChan <- configJSON + }() + + var descriptors []xfer.DownloadDescriptor + + // Note that the order of this loop is in the direction of bottom-most + // to top-most, so that the downloads slice gets ordered correctly. + for _, d := range mfst.References() { + layerDescriptor := &v2LayerDescriptor{ + digest: d.Digest, + repo: p.repo, + repoInfo: p.repoInfo, + V2MetadataService: p.V2MetadataService, + } + + descriptors = append(descriptors, layerDescriptor) + } + + var ( + configJSON []byte // raw serialized image config + unmarshalledConfig image.Image // deserialized image config + downloadRootFS image.RootFS // rootFS to use for registering layers. + ) + if runtime.GOOS == "windows" { + configJSON, unmarshalledConfig, err = receiveConfig(configChan, errChan) + if err != nil { + return "", "", err + } + if unmarshalledConfig.RootFS == nil { + return "", "", errors.New("image config has no rootfs section") + } + downloadRootFS = *unmarshalledConfig.RootFS + downloadRootFS.DiffIDs = []layer.DiffID{} + } else { + downloadRootFS = *image.NewRootFS() + } + + rootFS, release, err := p.config.DownloadManager.Download(ctx, downloadRootFS, descriptors, p.config.ProgressOutput) + if err != nil { + if configJSON != nil { + // Already received the config + return "", "", err + } + select { + case err = <-errChan: + return "", "", err + default: + cancel() + select { + case <-configChan: + case <-errChan: + } + return "", "", err + } + } + defer release() + + if configJSON == nil { + configJSON, unmarshalledConfig, err = receiveConfig(configChan, errChan) + if err != nil { + return "", "", err + } + } + + // The DiffIDs returned in rootFS MUST match those in the config. + // Otherwise the image config could be referencing layers that aren't + // included in the manifest. + if len(rootFS.DiffIDs) != len(unmarshalledConfig.RootFS.DiffIDs) { + return "", "", errRootFSMismatch + } + + for i := range rootFS.DiffIDs { + if rootFS.DiffIDs[i] != unmarshalledConfig.RootFS.DiffIDs[i] { + return "", "", errRootFSMismatch + } + } + + imageID, err = p.config.ImageStore.Create(configJSON) + if err != nil { + return "", "", err + } + + return imageID, manifestDigest, nil +} + +func receiveConfig(configChan <-chan []byte, errChan <-chan error) ([]byte, image.Image, error) { + select { + case configJSON := <-configChan: + var unmarshalledConfig image.Image + if err := json.Unmarshal(configJSON, &unmarshalledConfig); err != nil { + return nil, image.Image{}, err + } + return configJSON, unmarshalledConfig, nil + case err := <-errChan: + return nil, image.Image{}, err + // Don't need a case for ctx.Done in the select because cancellation + // will trigger an error in p.pullSchema2ImageConfig. + } +} + +// pullManifestList handles "manifest lists" which point to various +// platform-specifc manifests. +func (p *v2Puller) pullManifestList(ctx context.Context, ref reference.Named, mfstList *manifestlist.DeserializedManifestList) (imageID image.ID, manifestListDigest digest.Digest, err error) { + manifestListDigest, err = schema2ManifestDigest(ref, mfstList) + if err != nil { + return "", "", err + } + + var manifestDigest digest.Digest + for _, manifestDescriptor := range mfstList.Manifests { + // TODO(aaronl): The manifest list spec supports optional + // "features" and "variant" fields. These are not yet used. + // Once they are, their values should be interpreted here. + if manifestDescriptor.Platform.Architecture == runtime.GOARCH && manifestDescriptor.Platform.OS == runtime.GOOS { + manifestDigest = manifestDescriptor.Digest + break + } + } + + if manifestDigest == "" { + return "", "", errors.New("no supported platform found in manifest list") + } + + manSvc, err := p.repo.Manifests(ctx) + if err != nil { + return "", "", err + } + + manifest, err := manSvc.Get(ctx, manifestDigest) + if err != nil { + return "", "", err + } + + manifestRef, err := reference.WithDigest(ref, manifestDigest) + if err != nil { + return "", "", err + } + + switch v := manifest.(type) { + case *schema1.SignedManifest: + imageID, _, err = p.pullSchema1(ctx, manifestRef, v) + if err != nil { + return "", "", err + } + case *schema2.DeserializedManifest: + imageID, _, err = p.pullSchema2(ctx, manifestRef, v) + if err != nil { + return "", "", err + } + default: + return "", "", errors.New("unsupported manifest format") + } + + return imageID, manifestListDigest, err +} + +func (p *v2Puller) pullSchema2ImageConfig(ctx context.Context, dgst digest.Digest) (configJSON []byte, err error) { + blobs := p.repo.Blobs(ctx) + configJSON, err = blobs.Get(ctx, dgst) + if err != nil { + return nil, err + } + + // Verify image config digest + verifier, err := digest.NewDigestVerifier(dgst) + if err != nil { + return nil, err + } + if _, err := verifier.Write(configJSON); err != nil { + return nil, err + } + if !verifier.Verified() { + err := fmt.Errorf("image config verification failed for digest %s", dgst) + logrus.Error(err) + return nil, err + } + + return configJSON, nil +} + +// schema2ManifestDigest computes the manifest digest, and, if pulling by +// digest, ensures that it matches the requested digest. +func schema2ManifestDigest(ref reference.Named, mfst distribution.Manifest) (digest.Digest, error) { + _, canonical, err := mfst.Payload() + if err != nil { + return "", err + } + + // If pull by digest, then verify the manifest digest. + if digested, isDigested := ref.(reference.Canonical); isDigested { + verifier, err := digest.NewDigestVerifier(digested.Digest()) + if err != nil { + return "", err + } + if _, err := verifier.Write(canonical); err != nil { + return "", err + } + if !verifier.Verified() { + err := fmt.Errorf("manifest verification failed for digest %s", digested.Digest()) + logrus.Error(err) + return "", err + } + return digested.Digest(), nil + } + + return digest.FromBytes(canonical), nil +} + +// allowV1Fallback checks if the error is a possible reason to fallback to v1 +// (even if confirmedV2 has been set already), and if so, wraps the error in +// a fallbackError with confirmedV2 set to false. Otherwise, it returns the +// error unmodified. +func allowV1Fallback(err error) error { + switch v := err.(type) { + case errcode.Errors: + if len(v) != 0 { + if v0, ok := v[0].(errcode.Error); ok && shouldV2Fallback(v0) { + return fallbackError{ + err: err, + confirmedV2: false, + transportOK: true, + } + } + } + case errcode.Error: + if shouldV2Fallback(v) { + return fallbackError{ + err: err, + confirmedV2: false, + transportOK: true, + } + } + case *url.Error: + if v.Err == auth.ErrNoBasicAuthCredentials { + return fallbackError{err: err, confirmedV2: false} + } + } + + return err +} + +func verifySchema1Manifest(signedManifest *schema1.SignedManifest, ref reference.Named) (m *schema1.Manifest, err error) { + // If pull by digest, then verify the manifest digest. NOTE: It is + // important to do this first, before any other content validation. If the + // digest cannot be verified, don't even bother with those other things. + if digested, isCanonical := ref.(reference.Canonical); isCanonical { + verifier, err := digest.NewDigestVerifier(digested.Digest()) + if err != nil { + return nil, err + } + if _, err := verifier.Write(signedManifest.Canonical); err != nil { + return nil, err + } + if !verifier.Verified() { + err := fmt.Errorf("image verification failed for digest %s", digested.Digest()) + logrus.Error(err) + return nil, err + } + } + m = &signedManifest.Manifest + + if m.SchemaVersion != 1 { + return nil, fmt.Errorf("unsupported schema version %d for %q", m.SchemaVersion, ref.String()) + } + if len(m.FSLayers) != len(m.History) { + return nil, fmt.Errorf("length of history not equal to number of layers for %q", ref.String()) + } + if len(m.FSLayers) == 0 { + return nil, fmt.Errorf("no FSLayers in manifest for %q", ref.String()) + } + return m, nil +} + +// fixManifestLayers removes repeated layers from the manifest and checks the +// correctness of the parent chain. +func fixManifestLayers(m *schema1.Manifest) error { + imgs := make([]*image.V1Image, len(m.FSLayers)) + for i := range m.FSLayers { + img := &image.V1Image{} + + if err := json.Unmarshal([]byte(m.History[i].V1Compatibility), img); err != nil { + return err + } + + imgs[i] = img + if err := v1.ValidateID(img.ID); err != nil { + return err + } + } + + if imgs[len(imgs)-1].Parent != "" && runtime.GOOS != "windows" { + // Windows base layer can point to a base layer parent that is not in manifest. + return errors.New("Invalid parent ID in the base layer of the image.") + } + + // check general duplicates to error instead of a deadlock + idmap := make(map[string]struct{}) + + var lastID string + for _, img := range imgs { + // skip IDs that appear after each other, we handle those later + if _, exists := idmap[img.ID]; img.ID != lastID && exists { + return fmt.Errorf("ID %+v appears multiple times in manifest", img.ID) + } + lastID = img.ID + idmap[lastID] = struct{}{} + } + + // backwards loop so that we keep the remaining indexes after removing items + for i := len(imgs) - 2; i >= 0; i-- { + if imgs[i].ID == imgs[i+1].ID { // repeated ID. remove and continue + m.FSLayers = append(m.FSLayers[:i], m.FSLayers[i+1:]...) + m.History = append(m.History[:i], m.History[i+1:]...) + } else if imgs[i].Parent != imgs[i+1].ID { + return fmt.Errorf("Invalid parent ID. Expected %v, got %v.", imgs[i+1].ID, imgs[i].Parent) + } + } + + return nil +} + +func createDownloadFile() (*os.File, error) { + return ioutil.TempFile("", "GetImageBlob") +} diff --git a/distribution/pull_v2_test.go b/distribution/pull_v2_test.go new file mode 100644 index 00000000..8555c81e --- /dev/null +++ b/distribution/pull_v2_test.go @@ -0,0 +1,183 @@ +package distribution + +import ( + "encoding/json" + "io/ioutil" + "reflect" + "runtime" + "strings" + "testing" + + "github.com/docker/distribution/digest" + "github.com/docker/distribution/manifest/schema1" + "github.com/docker/docker/reference" +) + +// TestFixManifestLayers checks that fixManifestLayers removes a duplicate +// layer, and that it makes no changes to the manifest when called a second +// time, after the duplicate is removed. +func TestFixManifestLayers(t *testing.T) { + duplicateLayerManifest := schema1.Manifest{ + FSLayers: []schema1.FSLayer{ + {BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")}, + {BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")}, + {BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")}, + }, + History: []schema1.History{ + {V1Compatibility: "{\"id\":\"3b38edc92eb7c074812e217b41a6ade66888531009d6286a6f5f36a06f9841b9\",\"parent\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:11.368300679Z\",\"container\":\"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT [\\\"/go/bin/dnsdock\\\"]\"],\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":null,\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"}, + {V1Compatibility: "{\"id\":\"3b38edc92eb7c074812e217b41a6ade66888531009d6286a6f5f36a06f9841b9\",\"parent\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:11.368300679Z\",\"container\":\"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT [\\\"/go/bin/dnsdock\\\"]\"],\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":null,\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"}, + {V1Compatibility: "{\"id\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:07.568027497Z\",\"container\":\"fe9e5a5264a843c9292d17b736c92dd19bdb49986a8782d7389964ddaff887cc\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"cd /go/src/github.com/tonistiigi/dnsdock \\u0026\\u0026 go get -v github.com/tools/godep \\u0026\\u0026 godep restore \\u0026\\u0026 go install -ldflags \\\"-X main.version `git describe --tags HEAD``if [[ -n $(command git status --porcelain --untracked-files=no 2\\u003e/dev/null) ]]; then echo \\\"-dirty\\\"; fi`\\\" ./...\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/bash\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":118430532}\n"}, + }, + } + + duplicateLayerManifestExpectedOutput := schema1.Manifest{ + FSLayers: []schema1.FSLayer{ + {BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")}, + {BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")}, + }, + History: []schema1.History{ + {V1Compatibility: "{\"id\":\"3b38edc92eb7c074812e217b41a6ade66888531009d6286a6f5f36a06f9841b9\",\"parent\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:11.368300679Z\",\"container\":\"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT [\\\"/go/bin/dnsdock\\\"]\"],\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":null,\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"}, + {V1Compatibility: "{\"id\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:07.568027497Z\",\"container\":\"fe9e5a5264a843c9292d17b736c92dd19bdb49986a8782d7389964ddaff887cc\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"cd /go/src/github.com/tonistiigi/dnsdock \\u0026\\u0026 go get -v github.com/tools/godep \\u0026\\u0026 godep restore \\u0026\\u0026 go install -ldflags \\\"-X main.version `git describe --tags HEAD``if [[ -n $(command git status --porcelain --untracked-files=no 2\\u003e/dev/null) ]]; then echo \\\"-dirty\\\"; fi`\\\" ./...\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/bash\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":118430532}\n"}, + }, + } + + if err := fixManifestLayers(&duplicateLayerManifest); err != nil { + t.Fatalf("unexpected error from fixManifestLayers: %v", err) + } + + if !reflect.DeepEqual(duplicateLayerManifest, duplicateLayerManifestExpectedOutput) { + t.Fatal("incorrect output from fixManifestLayers on duplicate layer manifest") + } + + // Run fixManifestLayers again and confirm that it doesn't change the + // manifest (which no longer has duplicate layers). + if err := fixManifestLayers(&duplicateLayerManifest); err != nil { + t.Fatalf("unexpected error from fixManifestLayers: %v", err) + } + + if !reflect.DeepEqual(duplicateLayerManifest, duplicateLayerManifestExpectedOutput) { + t.Fatal("incorrect output from fixManifestLayers on duplicate layer manifest (second pass)") + } +} + +// TestFixManifestLayersBaseLayerParent makes sure that fixManifestLayers fails +// if the base layer configuration specifies a parent. +func TestFixManifestLayersBaseLayerParent(t *testing.T) { + // TODO Windows: Fix this unit text + if runtime.GOOS == "windows" { + t.Skip("Needs fixing on Windows") + } + duplicateLayerManifest := schema1.Manifest{ + FSLayers: []schema1.FSLayer{ + {BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")}, + {BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")}, + {BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")}, + }, + History: []schema1.History{ + {V1Compatibility: "{\"id\":\"3b38edc92eb7c074812e217b41a6ade66888531009d6286a6f5f36a06f9841b9\",\"parent\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:11.368300679Z\",\"container\":\"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT [\\\"/go/bin/dnsdock\\\"]\"],\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":null,\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"}, + {V1Compatibility: "{\"id\":\"3b38edc92eb7c074812e217b41a6ade66888531009d6286a6f5f36a06f9841b9\",\"parent\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:11.368300679Z\",\"container\":\"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT [\\\"/go/bin/dnsdock\\\"]\"],\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":null,\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"}, + {V1Compatibility: "{\"id\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"parent\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"created\":\"2015-08-19T16:49:07.568027497Z\",\"container\":\"fe9e5a5264a843c9292d17b736c92dd19bdb49986a8782d7389964ddaff887cc\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"cd /go/src/github.com/tonistiigi/dnsdock \\u0026\\u0026 go get -v github.com/tools/godep \\u0026\\u0026 godep restore \\u0026\\u0026 go install -ldflags \\\"-X main.version `git describe --tags HEAD``if [[ -n $(command git status --porcelain --untracked-files=no 2\\u003e/dev/null) ]]; then echo \\\"-dirty\\\"; fi`\\\" ./...\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/bash\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":118430532}\n"}, + }, + } + + if err := fixManifestLayers(&duplicateLayerManifest); err == nil || !strings.Contains(err.Error(), "Invalid parent ID in the base layer of the image.") { + t.Fatalf("expected an invalid parent ID error from fixManifestLayers") + } +} + +// TestFixManifestLayersBadParent makes sure that fixManifestLayers fails +// if an image configuration specifies a parent that doesn't directly follow +// that (deduplicated) image in the image history. +func TestFixManifestLayersBadParent(t *testing.T) { + duplicateLayerManifest := schema1.Manifest{ + FSLayers: []schema1.FSLayer{ + {BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")}, + {BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")}, + {BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")}, + }, + History: []schema1.History{ + {V1Compatibility: "{\"id\":\"3b38edc92eb7c074812e217b41a6ade66888531009d6286a6f5f36a06f9841b9\",\"parent\":\"ac3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:11.368300679Z\",\"container\":\"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT [\\\"/go/bin/dnsdock\\\"]\"],\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":null,\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"}, + {V1Compatibility: "{\"id\":\"3b38edc92eb7c074812e217b41a6ade66888531009d6286a6f5f36a06f9841b9\",\"parent\":\"ac3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:11.368300679Z\",\"container\":\"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT [\\\"/go/bin/dnsdock\\\"]\"],\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":null,\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"}, + {V1Compatibility: "{\"id\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:07.568027497Z\",\"container\":\"fe9e5a5264a843c9292d17b736c92dd19bdb49986a8782d7389964ddaff887cc\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"cd /go/src/github.com/tonistiigi/dnsdock \\u0026\\u0026 go get -v github.com/tools/godep \\u0026\\u0026 godep restore \\u0026\\u0026 go install -ldflags \\\"-X main.version `git describe --tags HEAD``if [[ -n $(command git status --porcelain --untracked-files=no 2\\u003e/dev/null) ]]; then echo \\\"-dirty\\\"; fi`\\\" ./...\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/bash\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":118430532}\n"}, + }, + } + + if err := fixManifestLayers(&duplicateLayerManifest); err == nil || !strings.Contains(err.Error(), "Invalid parent ID.") { + t.Fatalf("expected an invalid parent ID error from fixManifestLayers") + } +} + +// TestValidateManifest verifies the validateManifest function +func TestValidateManifest(t *testing.T) { + // TODO Windows: Fix this unit text + if runtime.GOOS == "windows" { + t.Skip("Needs fixing on Windows") + } + expectedDigest, err := reference.ParseNamed("repo@sha256:02fee8c3220ba806531f606525eceb83f4feb654f62b207191b1c9209188dedd") + if err != nil { + t.Fatal("could not parse reference") + } + expectedFSLayer0 := digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4") + + // Good manifest + + goodManifestBytes, err := ioutil.ReadFile("fixtures/validate_manifest/good_manifest") + if err != nil { + t.Fatal("error reading fixture:", err) + } + + var goodSignedManifest schema1.SignedManifest + err = json.Unmarshal(goodManifestBytes, &goodSignedManifest) + if err != nil { + t.Fatal("error unmarshaling manifest:", err) + } + + verifiedManifest, err := verifySchema1Manifest(&goodSignedManifest, expectedDigest) + if err != nil { + t.Fatal("validateManifest failed:", err) + } + + if verifiedManifest.FSLayers[0].BlobSum != expectedFSLayer0 { + t.Fatal("unexpected FSLayer in good manifest") + } + + // "Extra data" manifest + + extraDataManifestBytes, err := ioutil.ReadFile("fixtures/validate_manifest/extra_data_manifest") + if err != nil { + t.Fatal("error reading fixture:", err) + } + + var extraDataSignedManifest schema1.SignedManifest + err = json.Unmarshal(extraDataManifestBytes, &extraDataSignedManifest) + if err != nil { + t.Fatal("error unmarshaling manifest:", err) + } + + verifiedManifest, err = verifySchema1Manifest(&extraDataSignedManifest, expectedDigest) + if err != nil { + t.Fatal("validateManifest failed:", err) + } + + if verifiedManifest.FSLayers[0].BlobSum != expectedFSLayer0 { + t.Fatal("unexpected FSLayer in extra data manifest") + } + + // Bad manifest + + badManifestBytes, err := ioutil.ReadFile("fixtures/validate_manifest/bad_manifest") + if err != nil { + t.Fatal("error reading fixture:", err) + } + + var badSignedManifest schema1.SignedManifest + err = json.Unmarshal(badManifestBytes, &badSignedManifest) + if err != nil { + t.Fatal("error unmarshaling manifest:", err) + } + + verifiedManifest, err = verifySchema1Manifest(&badSignedManifest, expectedDigest) + if err == nil || !strings.HasPrefix(err.Error(), "image verification failed for digest") { + t.Fatal("expected validateManifest to fail with digest error") + } +} diff --git a/distribution/pull_v2_unix.go b/distribution/pull_v2_unix.go new file mode 100644 index 00000000..9fbb875e --- /dev/null +++ b/distribution/pull_v2_unix.go @@ -0,0 +1,12 @@ +// +build !windows + +package distribution + +import ( + "github.com/docker/distribution/manifest/schema1" + "github.com/docker/docker/image" +) + +func detectBaseLayer(is image.Store, m *schema1.Manifest, rootFS *image.RootFS) error { + return nil +} diff --git a/distribution/pull_v2_windows.go b/distribution/pull_v2_windows.go new file mode 100644 index 00000000..de99fc9d --- /dev/null +++ b/distribution/pull_v2_windows.go @@ -0,0 +1,29 @@ +// +build windows + +package distribution + +import ( + "encoding/json" + "fmt" + + "github.com/docker/distribution/manifest/schema1" + "github.com/docker/docker/image" +) + +func detectBaseLayer(is image.Store, m *schema1.Manifest, rootFS *image.RootFS) error { + v1img := &image.V1Image{} + if err := json.Unmarshal([]byte(m.History[len(m.History)-1].V1Compatibility), v1img); err != nil { + return err + } + if v1img.Parent == "" { + return fmt.Errorf("Last layer %q does not have a base layer reference", v1img.ID) + } + // There must be an image that already references the baselayer. + for _, img := range is.Map() { + if img.RootFS.BaseLayerID() == v1img.Parent { + rootFS.BaseLayer = img.RootFS.BaseLayer + return nil + } + } + return fmt.Errorf("Invalid base layer %q", v1img.Parent) +} diff --git a/distribution/push.go b/distribution/push.go new file mode 100644 index 00000000..52ee8e77 --- /dev/null +++ b/distribution/push.go @@ -0,0 +1,219 @@ +package distribution + +import ( + "bufio" + "compress/gzip" + "fmt" + "io" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/distribution/metadata" + "github.com/docker/docker/distribution/xfer" + "github.com/docker/docker/image" + "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/progress" + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" + "github.com/docker/engine-api/types" + "github.com/docker/libtrust" + "golang.org/x/net/context" +) + +// ImagePushConfig stores push configuration. +type ImagePushConfig struct { + // MetaHeaders store HTTP headers with metadata about the image + MetaHeaders map[string][]string + // AuthConfig holds authentication credentials for authenticating with + // the registry. + AuthConfig *types.AuthConfig + // ProgressOutput is the interface for showing the status of the push + // operation. + ProgressOutput progress.Output + // RegistryService is the registry service to use for TLS configuration + // and endpoint lookup. + RegistryService *registry.Service + // ImageEventLogger notifies events for a given image + ImageEventLogger func(id, name, action string) + // MetadataStore is the storage backend for distribution-specific + // metadata. + MetadataStore metadata.Store + // LayerStore manages layers. + LayerStore layer.Store + // ImageStore manages images. + ImageStore image.Store + // ReferenceStore manages tags. + ReferenceStore reference.Store + // TrustKey is the private key for legacy signatures. This is typically + // an ephemeral key, since these signatures are no longer verified. + TrustKey libtrust.PrivateKey + // UploadManager dispatches uploads. + UploadManager *xfer.LayerUploadManager +} + +// Pusher is an interface that abstracts pushing for different API versions. +type Pusher interface { + // Push tries to push the image configured at the creation of Pusher. + // Push returns an error if any, as well as a boolean that determines whether to retry Push on the next configured endpoint. + // + // TODO(tiborvass): have Push() take a reference to repository + tag, so that the pusher itself is repository-agnostic. + Push(ctx context.Context) error +} + +const compressionBufSize = 32768 + +// NewPusher creates a new Pusher interface that will push to either a v1 or v2 +// registry. The endpoint argument contains a Version field that determines +// whether a v1 or v2 pusher will be created. The other parameters are passed +// through to the underlying pusher implementation for use during the actual +// push operation. +func NewPusher(ref reference.Named, endpoint registry.APIEndpoint, repoInfo *registry.RepositoryInfo, imagePushConfig *ImagePushConfig) (Pusher, error) { + switch endpoint.Version { + case registry.APIVersion2: + return &v2Pusher{ + v2MetadataService: metadata.NewV2MetadataService(imagePushConfig.MetadataStore), + ref: ref, + endpoint: endpoint, + repoInfo: repoInfo, + config: imagePushConfig, + }, nil + case registry.APIVersion1: + return &v1Pusher{ + v1IDService: metadata.NewV1IDService(imagePushConfig.MetadataStore), + ref: ref, + endpoint: endpoint, + repoInfo: repoInfo, + config: imagePushConfig, + }, nil + } + return nil, fmt.Errorf("unknown version %d for registry %s", endpoint.Version, endpoint.URL) +} + +// Push initiates a push operation on the repository named localName. +// ref is the specific variant of the image to be pushed. +// If no tag is provided, all tags will be pushed. +func Push(ctx context.Context, ref reference.Named, imagePushConfig *ImagePushConfig) error { + // FIXME: Allow to interrupt current push when new push of same image is done. + + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := imagePushConfig.RegistryService.ResolveRepository(ref) + if err != nil { + return err + } + + endpoints, err := imagePushConfig.RegistryService.LookupPushEndpoints(repoInfo.Hostname()) + if err != nil { + return err + } + + progress.Messagef(imagePushConfig.ProgressOutput, "", "The push refers to a repository [%s]", repoInfo.FullName()) + + associations := imagePushConfig.ReferenceStore.ReferencesByName(repoInfo) + if len(associations) == 0 { + return fmt.Errorf("Repository does not exist: %s", repoInfo.Name()) + } + + var ( + lastErr error + + // confirmedV2 is set to true if a push attempt managed to + // confirm that it was talking to a v2 registry. This will + // prevent fallback to the v1 protocol. + confirmedV2 bool + + // confirmedTLSRegistries is a map indicating which registries + // are known to be using TLS. There should never be a plaintext + // retry for any of these. + confirmedTLSRegistries = make(map[string]struct{}) + ) + + for _, endpoint := range endpoints { + if confirmedV2 && endpoint.Version == registry.APIVersion1 { + logrus.Debugf("Skipping v1 endpoint %s because v2 registry was detected", endpoint.URL) + continue + } + + if endpoint.URL.Scheme != "https" { + if _, confirmedTLS := confirmedTLSRegistries[endpoint.URL.Host]; confirmedTLS { + logrus.Debugf("Skipping non-TLS endpoint %s for host/port that appears to use TLS", endpoint.URL) + continue + } + } + + logrus.Debugf("Trying to push %s to %s %s", repoInfo.FullName(), endpoint.URL, endpoint.Version) + + pusher, err := NewPusher(ref, endpoint, repoInfo, imagePushConfig) + if err != nil { + lastErr = err + continue + } + if err := pusher.Push(ctx); err != nil { + // Was this push cancelled? If so, don't try to fall + // back. + select { + case <-ctx.Done(): + default: + if fallbackErr, ok := err.(fallbackError); ok { + confirmedV2 = confirmedV2 || fallbackErr.confirmedV2 + if fallbackErr.transportOK && endpoint.URL.Scheme == "https" { + confirmedTLSRegistries[endpoint.URL.Host] = struct{}{} + } + err = fallbackErr.err + lastErr = err + logrus.Errorf("Attempting next endpoint for push after error: %v", err) + continue + } + } + + logrus.Errorf("Not continuing with push after error: %v", err) + return err + } + + imagePushConfig.ImageEventLogger(ref.String(), repoInfo.Name(), "push") + return nil + } + + if lastErr == nil { + lastErr = fmt.Errorf("no endpoints found for %s", repoInfo.FullName()) + } + return lastErr +} + +// compress returns an io.ReadCloser which will supply a compressed version of +// the provided Reader. The caller must close the ReadCloser after reading the +// compressed data. +// +// Note that this function returns a reader instead of taking a writer as an +// argument so that it can be used with httpBlobWriter's ReadFrom method. +// Using httpBlobWriter's Write method would send a PATCH request for every +// Write call. +// +// The second return value is a channel that gets closed when the goroutine +// is finished. This allows the caller to make sure the goroutine finishes +// before it releases any resources connected with the reader that was +// passed in. +func compress(in io.Reader) (io.ReadCloser, chan struct{}) { + compressionDone := make(chan struct{}) + + pipeReader, pipeWriter := io.Pipe() + // Use a bufio.Writer to avoid excessive chunking in HTTP request. + bufWriter := bufio.NewWriterSize(pipeWriter, compressionBufSize) + compressor := gzip.NewWriter(bufWriter) + + go func() { + _, err := io.Copy(compressor, in) + if err == nil { + err = compressor.Close() + } + if err == nil { + err = bufWriter.Flush() + } + if err != nil { + pipeWriter.CloseWithError(err) + } else { + pipeWriter.Close() + } + close(compressionDone) + }() + + return pipeReader, compressionDone +} diff --git a/distribution/push_v1.go b/distribution/push_v1.go new file mode 100644 index 00000000..b6e4a130 --- /dev/null +++ b/distribution/push_v1.go @@ -0,0 +1,454 @@ +package distribution + +import ( + "fmt" + "sync" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/digest" + "github.com/docker/distribution/registry/client/transport" + "github.com/docker/docker/distribution/metadata" + "github.com/docker/docker/dockerversion" + "github.com/docker/docker/image" + "github.com/docker/docker/image/v1" + "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/progress" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" + "golang.org/x/net/context" +) + +type v1Pusher struct { + v1IDService *metadata.V1IDService + endpoint registry.APIEndpoint + ref reference.Named + repoInfo *registry.RepositoryInfo + config *ImagePushConfig + session *registry.Session +} + +func (p *v1Pusher) Push(ctx context.Context) error { + tlsConfig, err := p.config.RegistryService.TLSConfig(p.repoInfo.Index.Name) + if err != nil { + return err + } + // Adds Docker-specific headers as well as user-specified headers (metaHeaders) + tr := transport.NewTransport( + // TODO(tiborvass): was NoTimeout + registry.NewTransport(tlsConfig), + registry.DockerHeaders(dockerversion.DockerUserAgent(ctx), p.config.MetaHeaders)..., + ) + client := registry.HTTPClient(tr) + v1Endpoint, err := p.endpoint.ToV1Endpoint(dockerversion.DockerUserAgent(ctx), p.config.MetaHeaders) + if err != nil { + logrus.Debugf("Could not get v1 endpoint: %v", err) + return fallbackError{err: err} + } + p.session, err = registry.NewSession(client, p.config.AuthConfig, v1Endpoint) + if err != nil { + // TODO(dmcgowan): Check if should fallback + return fallbackError{err: err} + } + if err := p.pushRepository(ctx); err != nil { + // TODO(dmcgowan): Check if should fallback + return err + } + return nil +} + +// v1Image exposes the configuration, filesystem layer ID, and a v1 ID for an +// image being pushed to a v1 registry. +type v1Image interface { + Config() []byte + Layer() layer.Layer + V1ID() string +} + +type v1ImageCommon struct { + layer layer.Layer + config []byte + v1ID string +} + +func (common *v1ImageCommon) Config() []byte { + return common.config +} + +func (common *v1ImageCommon) V1ID() string { + return common.v1ID +} + +func (common *v1ImageCommon) Layer() layer.Layer { + return common.layer +} + +// v1TopImage defines a runnable (top layer) image being pushed to a v1 +// registry. +type v1TopImage struct { + v1ImageCommon + imageID image.ID +} + +func newV1TopImage(imageID image.ID, img *image.Image, l layer.Layer, parent *v1DependencyImage) (*v1TopImage, error) { + v1ID := digest.Digest(imageID).Hex() + parentV1ID := "" + if parent != nil { + parentV1ID = parent.V1ID() + } + + config, err := v1.MakeV1ConfigFromConfig(img, v1ID, parentV1ID, false) + if err != nil { + return nil, err + } + + return &v1TopImage{ + v1ImageCommon: v1ImageCommon{ + v1ID: v1ID, + config: config, + layer: l, + }, + imageID: imageID, + }, nil +} + +// v1DependencyImage defines a dependency layer being pushed to a v1 registry. +type v1DependencyImage struct { + v1ImageCommon +} + +func newV1DependencyImage(l layer.Layer, parent *v1DependencyImage) (*v1DependencyImage, error) { + v1ID := digest.Digest(l.ChainID()).Hex() + + config := "" + if parent != nil { + config = fmt.Sprintf(`{"id":"%s","parent":"%s"}`, v1ID, parent.V1ID()) + } else { + config = fmt.Sprintf(`{"id":"%s"}`, v1ID) + } + return &v1DependencyImage{ + v1ImageCommon: v1ImageCommon{ + v1ID: v1ID, + config: []byte(config), + layer: l, + }, + }, nil +} + +// Retrieve the all the images to be uploaded in the correct order +func (p *v1Pusher) getImageList() (imageList []v1Image, tagsByImage map[image.ID][]string, referencedLayers []layer.Layer, err error) { + tagsByImage = make(map[image.ID][]string) + + // Ignore digest references + if _, isCanonical := p.ref.(reference.Canonical); isCanonical { + return + } + + tagged, isTagged := p.ref.(reference.NamedTagged) + if isTagged { + // Push a specific tag + var imgID image.ID + imgID, err = p.config.ReferenceStore.Get(p.ref) + if err != nil { + return + } + + imageList, err = p.imageListForTag(imgID, nil, &referencedLayers) + if err != nil { + return + } + + tagsByImage[imgID] = []string{tagged.Tag()} + + return + } + + imagesSeen := make(map[image.ID]struct{}) + dependenciesSeen := make(map[layer.ChainID]*v1DependencyImage) + + associations := p.config.ReferenceStore.ReferencesByName(p.ref) + for _, association := range associations { + if tagged, isTagged = association.Ref.(reference.NamedTagged); !isTagged { + // Ignore digest references. + continue + } + + tagsByImage[association.ImageID] = append(tagsByImage[association.ImageID], tagged.Tag()) + + if _, present := imagesSeen[association.ImageID]; present { + // Skip generating image list for already-seen image + continue + } + imagesSeen[association.ImageID] = struct{}{} + + imageListForThisTag, err := p.imageListForTag(association.ImageID, dependenciesSeen, &referencedLayers) + if err != nil { + return nil, nil, nil, err + } + + // append to main image list + imageList = append(imageList, imageListForThisTag...) + } + if len(imageList) == 0 { + return nil, nil, nil, fmt.Errorf("No images found for the requested repository / tag") + } + logrus.Debugf("Image list: %v", imageList) + logrus.Debugf("Tags by image: %v", tagsByImage) + + return +} + +func (p *v1Pusher) imageListForTag(imgID image.ID, dependenciesSeen map[layer.ChainID]*v1DependencyImage, referencedLayers *[]layer.Layer) (imageListForThisTag []v1Image, err error) { + img, err := p.config.ImageStore.Get(imgID) + if err != nil { + return nil, err + } + + topLayerID := img.RootFS.ChainID() + + var l layer.Layer + if topLayerID == "" { + l = layer.EmptyLayer + } else { + l, err = p.config.LayerStore.Get(topLayerID) + *referencedLayers = append(*referencedLayers, l) + if err != nil { + return nil, fmt.Errorf("failed to get top layer from image: %v", err) + } + } + + dependencyImages, parent, err := generateDependencyImages(l.Parent(), dependenciesSeen) + if err != nil { + return nil, err + } + + topImage, err := newV1TopImage(imgID, img, l, parent) + if err != nil { + return nil, err + } + + imageListForThisTag = append(dependencyImages, topImage) + + return +} + +func generateDependencyImages(l layer.Layer, dependenciesSeen map[layer.ChainID]*v1DependencyImage) (imageListForThisTag []v1Image, parent *v1DependencyImage, err error) { + if l == nil { + return nil, nil, nil + } + + imageListForThisTag, parent, err = generateDependencyImages(l.Parent(), dependenciesSeen) + + if dependenciesSeen != nil { + if dependencyImage, present := dependenciesSeen[l.ChainID()]; present { + // This layer is already on the list, we can ignore it + // and all its parents. + return imageListForThisTag, dependencyImage, nil + } + } + + dependencyImage, err := newV1DependencyImage(l, parent) + if err != nil { + return nil, nil, err + } + imageListForThisTag = append(imageListForThisTag, dependencyImage) + + if dependenciesSeen != nil { + dependenciesSeen[l.ChainID()] = dependencyImage + } + + return imageListForThisTag, dependencyImage, nil +} + +// createImageIndex returns an index of an image's layer IDs and tags. +func createImageIndex(images []v1Image, tags map[image.ID][]string) []*registry.ImgData { + var imageIndex []*registry.ImgData + for _, img := range images { + v1ID := img.V1ID() + + if topImage, isTopImage := img.(*v1TopImage); isTopImage { + if tags, hasTags := tags[topImage.imageID]; hasTags { + // If an image has tags you must add an entry in the image index + // for each tag + for _, tag := range tags { + imageIndex = append(imageIndex, ®istry.ImgData{ + ID: v1ID, + Tag: tag, + }) + } + continue + } + } + + // If the image does not have a tag it still needs to be sent to the + // registry with an empty tag so that it is associated with the repository + imageIndex = append(imageIndex, ®istry.ImgData{ + ID: v1ID, + Tag: "", + }) + } + return imageIndex +} + +// lookupImageOnEndpoint checks the specified endpoint to see if an image exists +// and if it is absent then it sends the image id to the channel to be pushed. +func (p *v1Pusher) lookupImageOnEndpoint(wg *sync.WaitGroup, endpoint string, images chan v1Image, imagesToPush chan string) { + defer wg.Done() + for image := range images { + v1ID := image.V1ID() + truncID := stringid.TruncateID(image.Layer().DiffID().String()) + if err := p.session.LookupRemoteImage(v1ID, endpoint); err != nil { + logrus.Errorf("Error in LookupRemoteImage: %s", err) + imagesToPush <- v1ID + progress.Update(p.config.ProgressOutput, truncID, "Waiting") + } else { + progress.Update(p.config.ProgressOutput, truncID, "Already exists") + } + } +} + +func (p *v1Pusher) pushImageToEndpoint(ctx context.Context, endpoint string, imageList []v1Image, tags map[image.ID][]string, repo *registry.RepositoryData) error { + workerCount := len(imageList) + // start a maximum of 5 workers to check if images exist on the specified endpoint. + if workerCount > 5 { + workerCount = 5 + } + var ( + wg = &sync.WaitGroup{} + imageData = make(chan v1Image, workerCount*2) + imagesToPush = make(chan string, workerCount*2) + pushes = make(chan map[string]struct{}, 1) + ) + for i := 0; i < workerCount; i++ { + wg.Add(1) + go p.lookupImageOnEndpoint(wg, endpoint, imageData, imagesToPush) + } + // start a go routine that consumes the images to push + go func() { + shouldPush := make(map[string]struct{}) + for id := range imagesToPush { + shouldPush[id] = struct{}{} + } + pushes <- shouldPush + }() + for _, v1Image := range imageList { + imageData <- v1Image + } + // close the channel to notify the workers that there will be no more images to check. + close(imageData) + wg.Wait() + close(imagesToPush) + // wait for all the images that require pushes to be collected into a consumable map. + shouldPush := <-pushes + // finish by pushing any images and tags to the endpoint. The order that the images are pushed + // is very important that is why we are still iterating over the ordered list of imageIDs. + for _, img := range imageList { + v1ID := img.V1ID() + if _, push := shouldPush[v1ID]; push { + if _, err := p.pushImage(ctx, img, endpoint); err != nil { + // FIXME: Continue on error? + return err + } + } + if topImage, isTopImage := img.(*v1TopImage); isTopImage { + for _, tag := range tags[topImage.imageID] { + progress.Messagef(p.config.ProgressOutput, "", "Pushing tag for rev [%s] on {%s}", stringid.TruncateID(v1ID), endpoint+"repositories/"+p.repoInfo.RemoteName()+"/tags/"+tag) + if err := p.session.PushRegistryTag(p.repoInfo, v1ID, tag, endpoint); err != nil { + return err + } + } + } + } + return nil +} + +// pushRepository pushes layers that do not already exist on the registry. +func (p *v1Pusher) pushRepository(ctx context.Context) error { + imgList, tags, referencedLayers, err := p.getImageList() + defer func() { + for _, l := range referencedLayers { + p.config.LayerStore.Release(l) + } + }() + if err != nil { + return err + } + + imageIndex := createImageIndex(imgList, tags) + for _, data := range imageIndex { + logrus.Debugf("Pushing ID: %s with Tag: %s", data.ID, data.Tag) + } + + // Register all the images in a repository with the registry + // If an image is not in this list it will not be associated with the repository + repoData, err := p.session.PushImageJSONIndex(p.repoInfo, imageIndex, false, nil) + if err != nil { + return err + } + // push the repository to each of the endpoints only if it does not exist. + for _, endpoint := range repoData.Endpoints { + if err := p.pushImageToEndpoint(ctx, endpoint, imgList, tags, repoData); err != nil { + return err + } + } + _, err = p.session.PushImageJSONIndex(p.repoInfo, imageIndex, true, repoData.Endpoints) + return err +} + +func (p *v1Pusher) pushImage(ctx context.Context, v1Image v1Image, ep string) (checksum string, err error) { + l := v1Image.Layer() + v1ID := v1Image.V1ID() + truncID := stringid.TruncateID(l.DiffID().String()) + + jsonRaw := v1Image.Config() + progress.Update(p.config.ProgressOutput, truncID, "Pushing") + + // General rule is to use ID for graph accesses and compatibilityID for + // calls to session.registry() + imgData := ®istry.ImgData{ + ID: v1ID, + } + + // Send the json + if err := p.session.PushImageJSONRegistry(imgData, jsonRaw, ep); err != nil { + if err == registry.ErrAlreadyExists { + progress.Update(p.config.ProgressOutput, truncID, "Image already pushed, skipping") + return "", nil + } + return "", err + } + + arch, err := l.TarStream() + if err != nil { + return "", err + } + defer arch.Close() + + // don't care if this fails; best effort + size, _ := l.DiffSize() + + // Send the layer + logrus.Debugf("rendered layer for %s of [%d] size", v1ID, size) + + reader := progress.NewProgressReader(ioutils.NewCancelReadCloser(ctx, arch), p.config.ProgressOutput, size, truncID, "Pushing") + defer reader.Close() + + checksum, checksumPayload, err := p.session.PushImageLayerRegistry(v1ID, reader, ep, jsonRaw) + if err != nil { + return "", err + } + imgData.Checksum = checksum + imgData.ChecksumPayload = checksumPayload + // Send the checksum + if err := p.session.PushImageChecksumRegistry(imgData, ep); err != nil { + return "", err + } + + if err := p.v1IDService.Set(v1ID, p.repoInfo.Index.Name, l.DiffID()); err != nil { + logrus.Warnf("Could not set v1 ID mapping: %v", err) + } + + progress.Update(p.config.ProgressOutput, truncID, "Image successfully pushed") + return imgData.Checksum, nil +} diff --git a/distribution/push_v2.go b/distribution/push_v2.go new file mode 100644 index 00000000..e86badb4 --- /dev/null +++ b/distribution/push_v2.go @@ -0,0 +1,438 @@ +package distribution + +import ( + "errors" + "fmt" + "io" + "sync" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution" + "github.com/docker/distribution/digest" + "github.com/docker/distribution/manifest/schema1" + "github.com/docker/distribution/manifest/schema2" + distreference "github.com/docker/distribution/reference" + "github.com/docker/distribution/registry/client" + "github.com/docker/docker/distribution/metadata" + "github.com/docker/docker/distribution/xfer" + "github.com/docker/docker/image" + "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/progress" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" + "golang.org/x/net/context" +) + +// PushResult contains the tag, manifest digest, and manifest size from the +// push. It's used to signal this information to the trust code in the client +// so it can sign the manifest if necessary. +type PushResult struct { + Tag string + Digest digest.Digest + Size int +} + +type v2Pusher struct { + v2MetadataService *metadata.V2MetadataService + ref reference.Named + endpoint registry.APIEndpoint + repoInfo *registry.RepositoryInfo + config *ImagePushConfig + repo distribution.Repository + + // pushState is state built by the Upload functions. + pushState pushState +} + +type pushState struct { + sync.Mutex + // remoteLayers is the set of layers known to exist on the remote side. + // This avoids redundant queries when pushing multiple tags that + // involve the same layers. It is also used to fill in digest and size + // information when building the manifest. + remoteLayers map[layer.DiffID]distribution.Descriptor + // confirmedV2 is set to true if we confirm we're talking to a v2 + // registry. This is used to limit fallbacks to the v1 protocol. + confirmedV2 bool +} + +func (p *v2Pusher) Push(ctx context.Context) (err error) { + p.pushState.remoteLayers = make(map[layer.DiffID]distribution.Descriptor) + + p.repo, p.pushState.confirmedV2, err = NewV2Repository(ctx, p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "push", "pull") + if err != nil { + logrus.Debugf("Error getting v2 registry: %v", err) + return err + } + + if err = p.pushV2Repository(ctx); err != nil { + if continueOnError(err) { + return fallbackError{ + err: err, + confirmedV2: p.pushState.confirmedV2, + transportOK: true, + } + } + } + return err +} + +func (p *v2Pusher) pushV2Repository(ctx context.Context) (err error) { + if namedTagged, isNamedTagged := p.ref.(reference.NamedTagged); isNamedTagged { + imageID, err := p.config.ReferenceStore.Get(p.ref) + if err != nil { + return fmt.Errorf("tag does not exist: %s", p.ref.String()) + } + + return p.pushV2Tag(ctx, namedTagged, imageID) + } + + if !reference.IsNameOnly(p.ref) { + return errors.New("cannot push a digest reference") + } + + // Pull all tags + pushed := 0 + for _, association := range p.config.ReferenceStore.ReferencesByName(p.ref) { + if namedTagged, isNamedTagged := association.Ref.(reference.NamedTagged); isNamedTagged { + pushed++ + if err := p.pushV2Tag(ctx, namedTagged, association.ImageID); err != nil { + return err + } + } + } + + if pushed == 0 { + return fmt.Errorf("no tags to push for %s", p.repoInfo.Name()) + } + + return nil +} + +func (p *v2Pusher) pushV2Tag(ctx context.Context, ref reference.NamedTagged, imageID image.ID) error { + logrus.Debugf("Pushing repository: %s", ref.String()) + + img, err := p.config.ImageStore.Get(imageID) + if err != nil { + return fmt.Errorf("could not find image from tag %s: %v", ref.String(), err) + } + + var l layer.Layer + + topLayerID := img.RootFS.ChainID() + if topLayerID == "" { + l = layer.EmptyLayer + } else { + l, err = p.config.LayerStore.Get(topLayerID) + if err != nil { + return fmt.Errorf("failed to get top layer from image: %v", err) + } + defer layer.ReleaseAndLog(p.config.LayerStore, l) + } + + var descriptors []xfer.UploadDescriptor + + descriptorTemplate := v2PushDescriptor{ + v2MetadataService: p.v2MetadataService, + repoInfo: p.repoInfo, + repo: p.repo, + pushState: &p.pushState, + } + + // Loop bounds condition is to avoid pushing the base layer on Windows. + for i := 0; i < len(img.RootFS.DiffIDs); i++ { + descriptor := descriptorTemplate + descriptor.layer = l + descriptors = append(descriptors, &descriptor) + + l = l.Parent() + } + + if err := p.config.UploadManager.Upload(ctx, descriptors, p.config.ProgressOutput); err != nil { + return err + } + + // Try schema2 first + builder := schema2.NewManifestBuilder(p.repo.Blobs(ctx), img.RawJSON()) + manifest, err := manifestFromBuilder(ctx, builder, descriptors) + if err != nil { + return err + } + + manSvc, err := p.repo.Manifests(ctx) + if err != nil { + return err + } + + putOptions := []distribution.ManifestServiceOption{distribution.WithTag(ref.Tag())} + if _, err = manSvc.Put(ctx, manifest, putOptions...); err != nil { + logrus.Warnf("failed to upload schema2 manifest: %v - falling back to schema1", err) + + manifestRef, err := distreference.WithTag(p.repo.Named(), ref.Tag()) + if err != nil { + return err + } + builder = schema1.NewConfigManifestBuilder(p.repo.Blobs(ctx), p.config.TrustKey, manifestRef, img.RawJSON()) + manifest, err = manifestFromBuilder(ctx, builder, descriptors) + if err != nil { + return err + } + + if _, err = manSvc.Put(ctx, manifest, putOptions...); err != nil { + return err + } + } + + var canonicalManifest []byte + + switch v := manifest.(type) { + case *schema1.SignedManifest: + canonicalManifest = v.Canonical + case *schema2.DeserializedManifest: + _, canonicalManifest, err = v.Payload() + if err != nil { + return err + } + } + + manifestDigest := digest.FromBytes(canonicalManifest) + progress.Messagef(p.config.ProgressOutput, "", "%s: digest: %s size: %d", ref.Tag(), manifestDigest, len(canonicalManifest)) + // Signal digest to the trust client so it can sign the + // push, if appropriate. + progress.Aux(p.config.ProgressOutput, PushResult{Tag: ref.Tag(), Digest: manifestDigest, Size: len(canonicalManifest)}) + + return nil +} + +func manifestFromBuilder(ctx context.Context, builder distribution.ManifestBuilder, descriptors []xfer.UploadDescriptor) (distribution.Manifest, error) { + // descriptors is in reverse order; iterate backwards to get references + // appended in the right order. + for i := len(descriptors) - 1; i >= 0; i-- { + if err := builder.AppendReference(descriptors[i].(*v2PushDescriptor)); err != nil { + return nil, err + } + } + + return builder.Build(ctx) +} + +type v2PushDescriptor struct { + layer layer.Layer + v2MetadataService *metadata.V2MetadataService + repoInfo reference.Named + repo distribution.Repository + pushState *pushState + remoteDescriptor distribution.Descriptor +} + +func (pd *v2PushDescriptor) Key() string { + return "v2push:" + pd.repo.Named().Name() + " " + pd.layer.DiffID().String() +} + +func (pd *v2PushDescriptor) ID() string { + return stringid.TruncateID(pd.layer.DiffID().String()) +} + +func (pd *v2PushDescriptor) DiffID() layer.DiffID { + return pd.layer.DiffID() +} + +func (pd *v2PushDescriptor) Upload(ctx context.Context, progressOutput progress.Output) (distribution.Descriptor, error) { + diffID := pd.DiffID() + + pd.pushState.Lock() + if descriptor, ok := pd.pushState.remoteLayers[diffID]; ok { + // it is already known that the push is not needed and + // therefore doing a stat is unnecessary + pd.pushState.Unlock() + progress.Update(progressOutput, pd.ID(), "Layer already exists") + return descriptor, nil + } + pd.pushState.Unlock() + + // Do we have any metadata associated with this layer's DiffID? + v2Metadata, err := pd.v2MetadataService.GetMetadata(diffID) + if err == nil { + descriptor, exists, err := layerAlreadyExists(ctx, v2Metadata, pd.repoInfo, pd.repo, pd.pushState) + if err != nil { + progress.Update(progressOutput, pd.ID(), "Image push failed") + return distribution.Descriptor{}, retryOnError(err) + } + if exists { + progress.Update(progressOutput, pd.ID(), "Layer already exists") + pd.pushState.Lock() + pd.pushState.remoteLayers[diffID] = descriptor + pd.pushState.Unlock() + return descriptor, nil + } + } + + logrus.Debugf("Pushing layer: %s", diffID) + + // if digest was empty or not saved, or if blob does not exist on the remote repository, + // then push the blob. + bs := pd.repo.Blobs(ctx) + + var layerUpload distribution.BlobWriter + mountAttemptsRemaining := 3 + + // Attempt to find another repository in the same registry to mount the layer + // from to avoid an unnecessary upload. + // Note: metadata is stored from oldest to newest, so we iterate through this + // slice in reverse to maximize our chances of the blob still existing in the + // remote repository. + for i := len(v2Metadata) - 1; i >= 0 && mountAttemptsRemaining > 0; i-- { + mountFrom := v2Metadata[i] + + sourceRepo, err := reference.ParseNamed(mountFrom.SourceRepository) + if err != nil { + continue + } + if pd.repoInfo.Hostname() != sourceRepo.Hostname() { + // don't mount blobs from another registry + continue + } + + namedRef, err := reference.WithName(mountFrom.SourceRepository) + if err != nil { + continue + } + + // TODO (brianbland): We need to construct a reference where the Name is + // only the full remote name, so clean this up when distribution has a + // richer reference package + remoteRef, err := distreference.WithName(namedRef.RemoteName()) + if err != nil { + continue + } + + canonicalRef, err := distreference.WithDigest(remoteRef, mountFrom.Digest) + if err != nil { + continue + } + + logrus.Debugf("attempting to mount layer %s (%s) from %s", diffID, mountFrom.Digest, sourceRepo.FullName()) + + layerUpload, err = bs.Create(ctx, client.WithMountFrom(canonicalRef)) + switch err := err.(type) { + case distribution.ErrBlobMounted: + progress.Updatef(progressOutput, pd.ID(), "Mounted from %s", err.From.Name()) + + err.Descriptor.MediaType = schema2.MediaTypeLayer + + pd.pushState.Lock() + pd.pushState.confirmedV2 = true + pd.pushState.remoteLayers[diffID] = err.Descriptor + pd.pushState.Unlock() + + // Cache mapping from this layer's DiffID to the blobsum + if err := pd.v2MetadataService.Add(diffID, metadata.V2Metadata{Digest: mountFrom.Digest, SourceRepository: pd.repoInfo.FullName()}); err != nil { + return distribution.Descriptor{}, xfer.DoNotRetry{Err: err} + } + return err.Descriptor, nil + case nil: + // blob upload session created successfully, so begin the upload + mountAttemptsRemaining = 0 + default: + // unable to mount layer from this repository, so this source mapping is no longer valid + logrus.Debugf("unassociating layer %s (%s) with %s", diffID, mountFrom.Digest, mountFrom.SourceRepository) + pd.v2MetadataService.Remove(mountFrom) + mountAttemptsRemaining-- + } + } + + if layerUpload == nil { + layerUpload, err = bs.Create(ctx) + if err != nil { + return distribution.Descriptor{}, retryOnError(err) + } + } + defer layerUpload.Close() + + arch, err := pd.layer.TarStream() + if err != nil { + return distribution.Descriptor{}, xfer.DoNotRetry{Err: err} + } + + // don't care if this fails; best effort + size, _ := pd.layer.DiffSize() + + reader := progress.NewProgressReader(ioutils.NewCancelReadCloser(ctx, arch), progressOutput, size, pd.ID(), "Pushing") + compressedReader, compressionDone := compress(reader) + defer func() { + reader.Close() + <-compressionDone + }() + + digester := digest.Canonical.New() + tee := io.TeeReader(compressedReader, digester.Hash()) + + nn, err := layerUpload.ReadFrom(tee) + compressedReader.Close() + if err != nil { + return distribution.Descriptor{}, retryOnError(err) + } + + pushDigest := digester.Digest() + if _, err := layerUpload.Commit(ctx, distribution.Descriptor{Digest: pushDigest}); err != nil { + return distribution.Descriptor{}, retryOnError(err) + } + + logrus.Debugf("uploaded layer %s (%s), %d bytes", diffID, pushDigest, nn) + progress.Update(progressOutput, pd.ID(), "Pushed") + + // Cache mapping from this layer's DiffID to the blobsum + if err := pd.v2MetadataService.Add(diffID, metadata.V2Metadata{Digest: pushDigest, SourceRepository: pd.repoInfo.FullName()}); err != nil { + return distribution.Descriptor{}, xfer.DoNotRetry{Err: err} + } + + pd.pushState.Lock() + + // If Commit succeeded, that's an indication that the remote registry + // speaks the v2 protocol. + pd.pushState.confirmedV2 = true + + descriptor := distribution.Descriptor{ + Digest: pushDigest, + MediaType: schema2.MediaTypeLayer, + Size: nn, + } + pd.pushState.remoteLayers[diffID] = descriptor + + pd.pushState.Unlock() + + return descriptor, nil +} + +func (pd *v2PushDescriptor) SetRemoteDescriptor(descriptor distribution.Descriptor) { + pd.remoteDescriptor = descriptor +} + +func (pd *v2PushDescriptor) Descriptor() distribution.Descriptor { + return pd.remoteDescriptor +} + +// layerAlreadyExists checks if the registry already know about any of the +// metadata passed in the "metadata" slice. If it finds one that the registry +// knows about, it returns the known digest and "true". +func layerAlreadyExists(ctx context.Context, metadata []metadata.V2Metadata, repoInfo reference.Named, repo distribution.Repository, pushState *pushState) (distribution.Descriptor, bool, error) { + for _, meta := range metadata { + // Only check blobsums that are known to this repository or have an unknown source + if meta.SourceRepository != "" && meta.SourceRepository != repoInfo.FullName() { + continue + } + descriptor, err := repo.Blobs(ctx).Stat(ctx, meta.Digest) + switch err { + case nil: + descriptor.MediaType = schema2.MediaTypeLayer + return descriptor, true, nil + case distribution.ErrBlobUnknown: + // nop + default: + return distribution.Descriptor{}, false, err + } + } + return distribution.Descriptor{}, false, nil +} diff --git a/distribution/registry.go b/distribution/registry.go new file mode 100644 index 00000000..f768a14d --- /dev/null +++ b/distribution/registry.go @@ -0,0 +1,130 @@ +package distribution + +import ( + "fmt" + "net" + "net/http" + "net/url" + "time" + + "github.com/docker/distribution" + distreference "github.com/docker/distribution/reference" + "github.com/docker/distribution/registry/client" + "github.com/docker/distribution/registry/client/auth" + "github.com/docker/distribution/registry/client/transport" + "github.com/docker/docker/dockerversion" + "github.com/docker/docker/registry" + "github.com/docker/engine-api/types" + "golang.org/x/net/context" +) + +type dumbCredentialStore struct { + auth *types.AuthConfig +} + +func (dcs dumbCredentialStore) Basic(*url.URL) (string, string) { + return dcs.auth.Username, dcs.auth.Password +} + +func (dcs dumbCredentialStore) RefreshToken(*url.URL, string) string { + return dcs.auth.IdentityToken +} + +func (dcs dumbCredentialStore) SetRefreshToken(*url.URL, string, string) { +} + +// NewV2Repository returns a repository (v2 only). It creates a HTTP transport +// providing timeout settings and authentication support, and also verifies the +// remote API version. +func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, endpoint registry.APIEndpoint, metaHeaders http.Header, authConfig *types.AuthConfig, actions ...string) (repo distribution.Repository, foundVersion bool, err error) { + repoName := repoInfo.FullName() + // If endpoint does not support CanonicalName, use the RemoteName instead + if endpoint.TrimHostname { + repoName = repoInfo.RemoteName() + } + + // TODO(dmcgowan): Call close idle connections when complete, use keep alive + base := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + TLSClientConfig: endpoint.TLSConfig, + // TODO(dmcgowan): Call close idle connections when complete and use keep alive + DisableKeepAlives: true, + } + + modifiers := registry.DockerHeaders(dockerversion.DockerUserAgent(ctx), metaHeaders) + authTransport := transport.NewTransport(base, modifiers...) + + challengeManager, foundVersion, err := registry.PingV2Registry(endpoint, authTransport) + if err != nil { + transportOK := false + if responseErr, ok := err.(registry.PingResponseError); ok { + transportOK = true + err = responseErr.Err + } + return nil, foundVersion, fallbackError{ + err: err, + confirmedV2: foundVersion, + transportOK: transportOK, + } + } + + if authConfig.RegistryToken != "" { + passThruTokenHandler := &existingTokenHandler{token: authConfig.RegistryToken} + modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler)) + } else { + creds := dumbCredentialStore{auth: authConfig} + tokenHandlerOptions := auth.TokenHandlerOptions{ + Transport: authTransport, + Credentials: creds, + Scopes: []auth.Scope{ + auth.RepositoryScope{ + Repository: repoName, + Actions: actions, + }, + }, + ClientID: registry.AuthClientID, + } + tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions) + basicHandler := auth.NewBasicHandler(creds) + modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)) + } + tr := transport.NewTransport(base, modifiers...) + + repoNameRef, err := distreference.ParseNamed(repoName) + if err != nil { + return nil, foundVersion, fallbackError{ + err: err, + confirmedV2: foundVersion, + transportOK: true, + } + } + + repo, err = client.NewRepository(ctx, repoNameRef, endpoint.URL.String(), tr) + if err != nil { + err = fallbackError{ + err: err, + confirmedV2: foundVersion, + transportOK: true, + } + } + return +} + +type existingTokenHandler struct { + token string +} + +func (th *existingTokenHandler) Scheme() string { + return "bearer" +} + +func (th *existingTokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", th.token)) + return nil +} diff --git a/distribution/registry_unit_test.go b/distribution/registry_unit_test.go new file mode 100644 index 00000000..b60a465d --- /dev/null +++ b/distribution/registry_unit_test.go @@ -0,0 +1,133 @@ +package distribution + +import ( + "net/http" + "net/http/httptest" + "net/url" + "os" + "strings" + "testing" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" + "github.com/docker/docker/utils" + "github.com/docker/engine-api/types" + registrytypes "github.com/docker/engine-api/types/registry" + "golang.org/x/net/context" +) + +const secretRegistryToken = "mysecrettoken" + +type tokenPassThruHandler struct { + reached bool + gotToken bool + shouldSend401 func(url string) bool +} + +func (h *tokenPassThruHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.reached = true + if strings.Contains(r.Header.Get("Authorization"), secretRegistryToken) { + logrus.Debug("Detected registry token in auth header") + h.gotToken = true + } + if h.shouldSend401 == nil || h.shouldSend401(r.RequestURI) { + w.Header().Set("WWW-Authenticate", `Bearer realm="foorealm"`) + w.WriteHeader(401) + } +} + +func testTokenPassThru(t *testing.T, ts *httptest.Server) { + tmp, err := utils.TestDirectory("") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("could not parse url from test server: %v", err) + } + + endpoint := registry.APIEndpoint{ + Mirror: false, + URL: uri, + Version: 2, + Official: false, + TrimHostname: false, + TLSConfig: nil, + //VersionHeader: "verheader", + } + n, _ := reference.ParseNamed("testremotename") + repoInfo := ®istry.RepositoryInfo{ + Named: n, + Index: ®istrytypes.IndexInfo{ + Name: "testrepo", + Mirrors: nil, + Secure: false, + Official: false, + }, + Official: false, + } + imagePullConfig := &ImagePullConfig{ + MetaHeaders: http.Header{}, + AuthConfig: &types.AuthConfig{ + RegistryToken: secretRegistryToken, + }, + } + puller, err := newPuller(endpoint, repoInfo, imagePullConfig) + if err != nil { + t.Fatal(err) + } + p := puller.(*v2Puller) + ctx := context.Background() + p.repo, _, err = NewV2Repository(ctx, p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "pull") + if err != nil { + t.Fatal(err) + } + + logrus.Debug("About to pull") + // We expect it to fail, since we haven't mock'd the full registry exchange in our handler above + tag, _ := reference.WithTag(n, "tag_goes_here") + _ = p.pullV2Repository(ctx, tag) +} + +func TestTokenPassThru(t *testing.T) { + handler := &tokenPassThruHandler{shouldSend401: func(url string) bool { return url == "/v2/" }} + ts := httptest.NewServer(handler) + defer ts.Close() + + testTokenPassThru(t, ts) + + if !handler.reached { + t.Fatal("Handler not reached") + } + if !handler.gotToken { + t.Fatal("Failed to receive registry token") + } +} + +func TestTokenPassThruDifferentHost(t *testing.T) { + handler := new(tokenPassThruHandler) + ts := httptest.NewServer(handler) + defer ts.Close() + + tsredirect := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.RequestURI == "/v2/" { + w.Header().Set("WWW-Authenticate", `Bearer realm="foorealm"`) + w.WriteHeader(401) + return + } + http.Redirect(w, r, ts.URL+r.URL.Path, http.StatusMovedPermanently) + })) + defer tsredirect.Close() + + testTokenPassThru(t, tsredirect) + + if !handler.reached { + t.Fatal("Handler not reached") + } + if handler.gotToken { + t.Fatal("Redirect should not forward Authorization header to another host") + } +} diff --git a/distribution/xfer/download.go b/distribution/xfer/download.go new file mode 100644 index 00000000..739c427c --- /dev/null +++ b/distribution/xfer/download.go @@ -0,0 +1,430 @@ +package xfer + +import ( + "errors" + "fmt" + "io" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/image" + "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/progress" + "golang.org/x/net/context" +) + +const maxDownloadAttempts = 5 + +// LayerDownloadManager figures out which layers need to be downloaded, then +// registers and downloads those, taking into account dependencies between +// layers. +type LayerDownloadManager struct { + layerStore layer.Store + tm TransferManager +} + +// NewLayerDownloadManager returns a new LayerDownloadManager. +func NewLayerDownloadManager(layerStore layer.Store, concurrencyLimit int) *LayerDownloadManager { + return &LayerDownloadManager{ + layerStore: layerStore, + tm: NewTransferManager(concurrencyLimit), + } +} + +type downloadTransfer struct { + Transfer + + layerStore layer.Store + layer layer.Layer + err error +} + +// result returns the layer resulting from the download, if the download +// and registration were successful. +func (d *downloadTransfer) result() (layer.Layer, error) { + return d.layer, d.err +} + +// A DownloadDescriptor references a layer that may need to be downloaded. +type DownloadDescriptor interface { + // Key returns the key used to deduplicate downloads. + Key() string + // ID returns the ID for display purposes. + ID() string + // DiffID should return the DiffID for this layer, or an error + // if it is unknown (for example, if it has not been downloaded + // before). + DiffID() (layer.DiffID, error) + // Download is called to perform the download. + Download(ctx context.Context, progressOutput progress.Output) (io.ReadCloser, int64, error) + // Close is called when the download manager is finished with this + // descriptor and will not call Download again or read from the reader + // that Download returned. + Close() +} + +// DownloadDescriptorWithRegistered is a DownloadDescriptor that has an +// additional Registered method which gets called after a downloaded layer is +// registered. This allows the user of the download manager to know the DiffID +// of each registered layer. This method is called if a cast to +// DownloadDescriptorWithRegistered is successful. +type DownloadDescriptorWithRegistered interface { + DownloadDescriptor + Registered(diffID layer.DiffID) +} + +// Download is a blocking function which ensures the requested layers are +// present in the layer store. It uses the string returned by the Key method to +// deduplicate downloads. If a given layer is not already known to present in +// the layer store, and the key is not used by an in-progress download, the +// Download method is called to get the layer tar data. Layers are then +// registered in the appropriate order. The caller must call the returned +// release function once it is is done with the returned RootFS object. +func (ldm *LayerDownloadManager) Download(ctx context.Context, initialRootFS image.RootFS, layers []DownloadDescriptor, progressOutput progress.Output) (image.RootFS, func(), error) { + var ( + topLayer layer.Layer + topDownload *downloadTransfer + watcher *Watcher + missingLayer bool + transferKey = "" + downloadsByKey = make(map[string]*downloadTransfer) + ) + + rootFS := initialRootFS + for _, descriptor := range layers { + key := descriptor.Key() + transferKey += key + + if !missingLayer { + missingLayer = true + diffID, err := descriptor.DiffID() + if err == nil { + getRootFS := rootFS + getRootFS.Append(diffID) + l, err := ldm.layerStore.Get(getRootFS.ChainID()) + if err == nil { + // Layer already exists. + logrus.Debugf("Layer already exists: %s", descriptor.ID()) + progress.Update(progressOutput, descriptor.ID(), "Already exists") + if topLayer != nil { + layer.ReleaseAndLog(ldm.layerStore, topLayer) + } + topLayer = l + missingLayer = false + rootFS.Append(diffID) + continue + } + } + } + + // Does this layer have the same data as a previous layer in + // the stack? If so, avoid downloading it more than once. + var topDownloadUncasted Transfer + if existingDownload, ok := downloadsByKey[key]; ok { + xferFunc := ldm.makeDownloadFuncFromDownload(descriptor, existingDownload, topDownload) + defer topDownload.Transfer.Release(watcher) + topDownloadUncasted, watcher = ldm.tm.Transfer(transferKey, xferFunc, progressOutput) + topDownload = topDownloadUncasted.(*downloadTransfer) + continue + } + + // Layer is not known to exist - download and register it. + progress.Update(progressOutput, descriptor.ID(), "Pulling fs layer") + + var xferFunc DoFunc + if topDownload != nil { + xferFunc = ldm.makeDownloadFunc(descriptor, "", topDownload) + defer topDownload.Transfer.Release(watcher) + } else { + xferFunc = ldm.makeDownloadFunc(descriptor, rootFS.ChainID(), nil) + } + topDownloadUncasted, watcher = ldm.tm.Transfer(transferKey, xferFunc, progressOutput) + topDownload = topDownloadUncasted.(*downloadTransfer) + downloadsByKey[key] = topDownload + } + + if topDownload == nil { + return rootFS, func() { + if topLayer != nil { + layer.ReleaseAndLog(ldm.layerStore, topLayer) + } + }, nil + } + + // Won't be using the list built up so far - will generate it + // from downloaded layers instead. + rootFS.DiffIDs = []layer.DiffID{} + + defer func() { + if topLayer != nil { + layer.ReleaseAndLog(ldm.layerStore, topLayer) + } + }() + + select { + case <-ctx.Done(): + topDownload.Transfer.Release(watcher) + return rootFS, func() {}, ctx.Err() + case <-topDownload.Done(): + break + } + + l, err := topDownload.result() + if err != nil { + topDownload.Transfer.Release(watcher) + return rootFS, func() {}, err + } + + // Must do this exactly len(layers) times, so we don't include the + // base layer on Windows. + for range layers { + if l == nil { + topDownload.Transfer.Release(watcher) + return rootFS, func() {}, errors.New("internal error: too few parent layers") + } + rootFS.DiffIDs = append([]layer.DiffID{l.DiffID()}, rootFS.DiffIDs...) + l = l.Parent() + } + return rootFS, func() { topDownload.Transfer.Release(watcher) }, err +} + +// makeDownloadFunc returns a function that performs the layer download and +// registration. If parentDownload is non-nil, it waits for that download to +// complete before the registration step, and registers the downloaded data +// on top of parentDownload's resulting layer. Otherwise, it registers the +// layer on top of the ChainID given by parentLayer. +func (ldm *LayerDownloadManager) makeDownloadFunc(descriptor DownloadDescriptor, parentLayer layer.ChainID, parentDownload *downloadTransfer) DoFunc { + return func(progressChan chan<- progress.Progress, start <-chan struct{}, inactive chan<- struct{}) Transfer { + d := &downloadTransfer{ + Transfer: NewTransfer(), + layerStore: ldm.layerStore, + } + + go func() { + defer func() { + close(progressChan) + }() + + progressOutput := progress.ChanOutput(progressChan) + + select { + case <-start: + default: + progress.Update(progressOutput, descriptor.ID(), "Waiting") + <-start + } + + if parentDownload != nil { + // Did the parent download already fail or get + // cancelled? + select { + case <-parentDownload.Done(): + _, err := parentDownload.result() + if err != nil { + d.err = err + return + } + default: + } + } + + var ( + downloadReader io.ReadCloser + size int64 + err error + retries int + ) + + defer descriptor.Close() + + for { + downloadReader, size, err = descriptor.Download(d.Transfer.Context(), progressOutput) + if err == nil { + break + } + + // If an error was returned because the context + // was cancelled, we shouldn't retry. + select { + case <-d.Transfer.Context().Done(): + d.err = err + return + default: + } + + retries++ + if _, isDNR := err.(DoNotRetry); isDNR || retries == maxDownloadAttempts { + logrus.Errorf("Download failed: %v", err) + d.err = err + return + } + + logrus.Errorf("Download failed, retrying: %v", err) + delay := retries * 5 + ticker := time.NewTicker(time.Second) + + selectLoop: + for { + progress.Updatef(progressOutput, descriptor.ID(), "Retrying in %d second%s", delay, (map[bool]string{true: "s"})[delay != 1]) + select { + case <-ticker.C: + delay-- + if delay == 0 { + ticker.Stop() + break selectLoop + } + case <-d.Transfer.Context().Done(): + ticker.Stop() + d.err = errors.New("download cancelled during retry delay") + return + } + + } + } + + close(inactive) + + if parentDownload != nil { + select { + case <-d.Transfer.Context().Done(): + d.err = errors.New("layer registration cancelled") + downloadReader.Close() + return + case <-parentDownload.Done(): + } + + l, err := parentDownload.result() + if err != nil { + d.err = err + downloadReader.Close() + return + } + parentLayer = l.ChainID() + } + + reader := progress.NewProgressReader(ioutils.NewCancelReadCloser(d.Transfer.Context(), downloadReader), progressOutput, size, descriptor.ID(), "Extracting") + defer reader.Close() + + inflatedLayerData, err := archive.DecompressStream(reader) + if err != nil { + d.err = fmt.Errorf("could not get decompression stream: %v", err) + return + } + + d.layer, err = d.layerStore.Register(inflatedLayerData, parentLayer) + if err != nil { + select { + case <-d.Transfer.Context().Done(): + d.err = errors.New("layer registration cancelled") + default: + d.err = fmt.Errorf("failed to register layer: %v", err) + } + return + } + + progress.Update(progressOutput, descriptor.ID(), "Pull complete") + withRegistered, hasRegistered := descriptor.(DownloadDescriptorWithRegistered) + if hasRegistered { + withRegistered.Registered(d.layer.DiffID()) + } + + // Doesn't actually need to be its own goroutine, but + // done like this so we can defer close(c). + go func() { + <-d.Transfer.Released() + if d.layer != nil { + layer.ReleaseAndLog(d.layerStore, d.layer) + } + }() + }() + + return d + } +} + +// makeDownloadFuncFromDownload returns a function that performs the layer +// registration when the layer data is coming from an existing download. It +// waits for sourceDownload and parentDownload to complete, and then +// reregisters the data from sourceDownload's top layer on top of +// parentDownload. This function does not log progress output because it would +// interfere with the progress reporting for sourceDownload, which has the same +// Key. +func (ldm *LayerDownloadManager) makeDownloadFuncFromDownload(descriptor DownloadDescriptor, sourceDownload *downloadTransfer, parentDownload *downloadTransfer) DoFunc { + return func(progressChan chan<- progress.Progress, start <-chan struct{}, inactive chan<- struct{}) Transfer { + d := &downloadTransfer{ + Transfer: NewTransfer(), + layerStore: ldm.layerStore, + } + + go func() { + defer func() { + close(progressChan) + }() + + <-start + + close(inactive) + + select { + case <-d.Transfer.Context().Done(): + d.err = errors.New("layer registration cancelled") + return + case <-parentDownload.Done(): + } + + l, err := parentDownload.result() + if err != nil { + d.err = err + return + } + parentLayer := l.ChainID() + + // sourceDownload should have already finished if + // parentDownload finished, but wait for it explicitly + // to be sure. + select { + case <-d.Transfer.Context().Done(): + d.err = errors.New("layer registration cancelled") + return + case <-sourceDownload.Done(): + } + + l, err = sourceDownload.result() + if err != nil { + d.err = err + return + } + + layerReader, err := l.TarStream() + if err != nil { + d.err = err + return + } + defer layerReader.Close() + + d.layer, err = d.layerStore.Register(layerReader, parentLayer) + if err != nil { + d.err = fmt.Errorf("failed to register layer: %v", err) + return + } + + withRegistered, hasRegistered := descriptor.(DownloadDescriptorWithRegistered) + if hasRegistered { + withRegistered.Registered(d.layer.DiffID()) + } + + // Doesn't actually need to be its own goroutine, but + // done like this so we can defer close(c). + go func() { + <-d.Transfer.Released() + if d.layer != nil { + layer.ReleaseAndLog(d.layerStore, d.layer) + } + }() + }() + + return d + } +} diff --git a/distribution/xfer/download_test.go b/distribution/xfer/download_test.go new file mode 100644 index 00000000..96f2db17 --- /dev/null +++ b/distribution/xfer/download_test.go @@ -0,0 +1,340 @@ +package xfer + +import ( + "bytes" + "errors" + "io" + "io/ioutil" + "runtime" + "sync/atomic" + "testing" + "time" + + "github.com/docker/distribution/digest" + "github.com/docker/docker/image" + "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/progress" + "golang.org/x/net/context" +) + +const maxDownloadConcurrency = 3 + +type mockLayer struct { + layerData bytes.Buffer + diffID layer.DiffID + chainID layer.ChainID + parent layer.Layer +} + +func (ml *mockLayer) TarStream() (io.ReadCloser, error) { + return ioutil.NopCloser(bytes.NewBuffer(ml.layerData.Bytes())), nil +} + +func (ml *mockLayer) ChainID() layer.ChainID { + return ml.chainID +} + +func (ml *mockLayer) DiffID() layer.DiffID { + return ml.diffID +} + +func (ml *mockLayer) Parent() layer.Layer { + return ml.parent +} + +func (ml *mockLayer) Size() (size int64, err error) { + return 0, nil +} + +func (ml *mockLayer) DiffSize() (size int64, err error) { + return 0, nil +} + +func (ml *mockLayer) Metadata() (map[string]string, error) { + return make(map[string]string), nil +} + +type mockLayerStore struct { + layers map[layer.ChainID]*mockLayer +} + +func createChainIDFromParent(parent layer.ChainID, dgsts ...layer.DiffID) layer.ChainID { + if len(dgsts) == 0 { + return parent + } + if parent == "" { + return createChainIDFromParent(layer.ChainID(dgsts[0]), dgsts[1:]...) + } + // H = "H(n-1) SHA256(n)" + dgst := digest.FromBytes([]byte(string(parent) + " " + string(dgsts[0]))) + return createChainIDFromParent(layer.ChainID(dgst), dgsts[1:]...) +} + +func (ls *mockLayerStore) Register(reader io.Reader, parentID layer.ChainID) (layer.Layer, error) { + var ( + parent layer.Layer + err error + ) + + if parentID != "" { + parent, err = ls.Get(parentID) + if err != nil { + return nil, err + } + } + + l := &mockLayer{parent: parent} + _, err = l.layerData.ReadFrom(reader) + if err != nil { + return nil, err + } + l.diffID = layer.DiffID(digest.FromBytes(l.layerData.Bytes())) + l.chainID = createChainIDFromParent(parentID, l.diffID) + + ls.layers[l.chainID] = l + return l, nil +} + +func (ls *mockLayerStore) Get(chainID layer.ChainID) (layer.Layer, error) { + l, ok := ls.layers[chainID] + if !ok { + return nil, layer.ErrLayerDoesNotExist + } + return l, nil +} + +func (ls *mockLayerStore) Release(l layer.Layer) ([]layer.Metadata, error) { + return []layer.Metadata{}, nil +} +func (ls *mockLayerStore) CreateRWLayer(string, layer.ChainID, string, layer.MountInit) (layer.RWLayer, error) { + return nil, errors.New("not implemented") +} + +func (ls *mockLayerStore) GetRWLayer(string) (layer.RWLayer, error) { + return nil, errors.New("not implemented") +} + +func (ls *mockLayerStore) ReleaseRWLayer(layer.RWLayer) ([]layer.Metadata, error) { + return nil, errors.New("not implemented") +} +func (ls *mockLayerStore) GetMountID(string) (string, error) { + return "", errors.New("not implemented") +} + +func (ls *mockLayerStore) ReinitRWLayer(layer.RWLayer) error { + return errors.New("not implemented") +} + +func (ls *mockLayerStore) Cleanup() error { + return nil +} + +func (ls *mockLayerStore) DriverStatus() [][2]string { + return [][2]string{} +} + +func (ls *mockLayerStore) DriverName() string { + return "mock" +} + +type mockDownloadDescriptor struct { + currentDownloads *int32 + id string + diffID layer.DiffID + registeredDiffID layer.DiffID + expectedDiffID layer.DiffID + simulateRetries int +} + +// Key returns the key used to deduplicate downloads. +func (d *mockDownloadDescriptor) Key() string { + return d.id +} + +// ID returns the ID for display purposes. +func (d *mockDownloadDescriptor) ID() string { + return d.id +} + +// DiffID should return the DiffID for this layer, or an error +// if it is unknown (for example, if it has not been downloaded +// before). +func (d *mockDownloadDescriptor) DiffID() (layer.DiffID, error) { + if d.diffID != "" { + return d.diffID, nil + } + return "", errors.New("no diffID available") +} + +func (d *mockDownloadDescriptor) Registered(diffID layer.DiffID) { + d.registeredDiffID = diffID +} + +func (d *mockDownloadDescriptor) mockTarStream() io.ReadCloser { + // The mock implementation returns the ID repeated 5 times as a tar + // stream instead of actual tar data. The data is ignored except for + // computing IDs. + return ioutil.NopCloser(bytes.NewBuffer([]byte(d.id + d.id + d.id + d.id + d.id))) +} + +// Download is called to perform the download. +func (d *mockDownloadDescriptor) Download(ctx context.Context, progressOutput progress.Output) (io.ReadCloser, int64, error) { + if d.currentDownloads != nil { + defer atomic.AddInt32(d.currentDownloads, -1) + + if atomic.AddInt32(d.currentDownloads, 1) > maxDownloadConcurrency { + return nil, 0, errors.New("concurrency limit exceeded") + } + } + + // Sleep a bit to simulate a time-consuming download. + for i := int64(0); i <= 10; i++ { + select { + case <-ctx.Done(): + return nil, 0, ctx.Err() + case <-time.After(10 * time.Millisecond): + progressOutput.WriteProgress(progress.Progress{ID: d.ID(), Action: "Downloading", Current: i, Total: 10}) + } + } + + if d.simulateRetries != 0 { + d.simulateRetries-- + return nil, 0, errors.New("simulating retry") + } + + return d.mockTarStream(), 0, nil +} + +func (d *mockDownloadDescriptor) Close() { +} + +func downloadDescriptors(currentDownloads *int32) []DownloadDescriptor { + return []DownloadDescriptor{ + &mockDownloadDescriptor{ + currentDownloads: currentDownloads, + id: "id1", + expectedDiffID: layer.DiffID("sha256:68e2c75dc5c78ea9240689c60d7599766c213ae210434c53af18470ae8c53ec1"), + }, + &mockDownloadDescriptor{ + currentDownloads: currentDownloads, + id: "id2", + expectedDiffID: layer.DiffID("sha256:64a636223116aa837973a5d9c2bdd17d9b204e4f95ac423e20e65dfbb3655473"), + }, + &mockDownloadDescriptor{ + currentDownloads: currentDownloads, + id: "id3", + expectedDiffID: layer.DiffID("sha256:58745a8bbd669c25213e9de578c4da5c8ee1c836b3581432c2b50e38a6753300"), + }, + &mockDownloadDescriptor{ + currentDownloads: currentDownloads, + id: "id2", + expectedDiffID: layer.DiffID("sha256:64a636223116aa837973a5d9c2bdd17d9b204e4f95ac423e20e65dfbb3655473"), + }, + &mockDownloadDescriptor{ + currentDownloads: currentDownloads, + id: "id4", + expectedDiffID: layer.DiffID("sha256:0dfb5b9577716cc173e95af7c10289322c29a6453a1718addc00c0c5b1330936"), + simulateRetries: 1, + }, + &mockDownloadDescriptor{ + currentDownloads: currentDownloads, + id: "id5", + expectedDiffID: layer.DiffID("sha256:0a5f25fa1acbc647f6112a6276735d0fa01e4ee2aa7ec33015e337350e1ea23d"), + }, + } +} + +func TestSuccessfulDownload(t *testing.T) { + // TODO Windows: Fix this unit text + if runtime.GOOS == "windows" { + t.Skip("Needs fixing on Windows") + } + layerStore := &mockLayerStore{make(map[layer.ChainID]*mockLayer)} + ldm := NewLayerDownloadManager(layerStore, maxDownloadConcurrency) + + progressChan := make(chan progress.Progress) + progressDone := make(chan struct{}) + receivedProgress := make(map[string]progress.Progress) + + go func() { + for p := range progressChan { + receivedProgress[p.ID] = p + } + close(progressDone) + }() + + var currentDownloads int32 + descriptors := downloadDescriptors(¤tDownloads) + + firstDescriptor := descriptors[0].(*mockDownloadDescriptor) + + // Pre-register the first layer to simulate an already-existing layer + l, err := layerStore.Register(firstDescriptor.mockTarStream(), "") + if err != nil { + t.Fatal(err) + } + firstDescriptor.diffID = l.DiffID() + + rootFS, releaseFunc, err := ldm.Download(context.Background(), *image.NewRootFS(), descriptors, progress.ChanOutput(progressChan)) + if err != nil { + t.Fatalf("download error: %v", err) + } + + releaseFunc() + + close(progressChan) + <-progressDone + + if len(rootFS.DiffIDs) != len(descriptors) { + t.Fatal("got wrong number of diffIDs in rootfs") + } + + for i, d := range descriptors { + descriptor := d.(*mockDownloadDescriptor) + + if descriptor.diffID != "" { + if receivedProgress[d.ID()].Action != "Already exists" { + t.Fatalf("did not get 'Already exists' message for %v", d.ID()) + } + } else if receivedProgress[d.ID()].Action != "Pull complete" { + t.Fatalf("did not get 'Pull complete' message for %v", d.ID()) + } + + if rootFS.DiffIDs[i] != descriptor.expectedDiffID { + t.Fatalf("rootFS item %d has the wrong diffID (expected: %v got: %v)", i, descriptor.expectedDiffID, rootFS.DiffIDs[i]) + } + + if descriptor.diffID == "" && descriptor.registeredDiffID != rootFS.DiffIDs[i] { + t.Fatal("diffID mismatch between rootFS and Registered callback") + } + } +} + +func TestCancelledDownload(t *testing.T) { + ldm := NewLayerDownloadManager(&mockLayerStore{make(map[layer.ChainID]*mockLayer)}, maxDownloadConcurrency) + + progressChan := make(chan progress.Progress) + progressDone := make(chan struct{}) + + go func() { + for range progressChan { + } + close(progressDone) + }() + + ctx, cancel := context.WithCancel(context.Background()) + + go func() { + <-time.After(time.Millisecond) + cancel() + }() + + descriptors := downloadDescriptors(nil) + _, _, err := ldm.Download(ctx, *image.NewRootFS(), descriptors, progress.ChanOutput(progressChan)) + if err != context.Canceled { + t.Fatal("expected download to be cancelled") + } + + close(progressChan) + <-progressDone +} diff --git a/distribution/xfer/transfer.go b/distribution/xfer/transfer.go new file mode 100644 index 00000000..dd83f8b8 --- /dev/null +++ b/distribution/xfer/transfer.go @@ -0,0 +1,392 @@ +package xfer + +import ( + "runtime" + "sync" + + "github.com/docker/docker/pkg/progress" + "golang.org/x/net/context" +) + +// DoNotRetry is an error wrapper indicating that the error cannot be resolved +// with a retry. +type DoNotRetry struct { + Err error +} + +// Error returns the stringified representation of the encapsulated error. +func (e DoNotRetry) Error() string { + return e.Err.Error() +} + +// Watcher is returned by Watch and can be passed to Release to stop watching. +type Watcher struct { + // signalChan is used to signal to the watcher goroutine that + // new progress information is available, or that the transfer + // has finished. + signalChan chan struct{} + // releaseChan signals to the watcher goroutine that the watcher + // should be detached. + releaseChan chan struct{} + // running remains open as long as the watcher is watching the + // transfer. It gets closed if the transfer finishes or the + // watcher is detached. + running chan struct{} +} + +// Transfer represents an in-progress transfer. +type Transfer interface { + Watch(progressOutput progress.Output) *Watcher + Release(*Watcher) + Context() context.Context + Close() + Done() <-chan struct{} + Released() <-chan struct{} + Broadcast(masterProgressChan <-chan progress.Progress) +} + +type transfer struct { + mu sync.Mutex + + ctx context.Context + cancel context.CancelFunc + + // watchers keeps track of the goroutines monitoring progress output, + // indexed by the channels that release them. + watchers map[chan struct{}]*Watcher + + // lastProgress is the most recently received progress event. + lastProgress progress.Progress + // hasLastProgress is true when lastProgress has been set. + hasLastProgress bool + + // running remains open as long as the transfer is in progress. + running chan struct{} + // released stays open until all watchers release the transfer and + // the transfer is no longer tracked by the transfer manager. + released chan struct{} + + // broadcastDone is true if the master progress channel has closed. + broadcastDone bool + // closed is true if Close has been called + closed bool + // broadcastSyncChan allows watchers to "ping" the broadcasting + // goroutine to wait for it for deplete its input channel. This ensures + // a detaching watcher won't miss an event that was sent before it + // started detaching. + broadcastSyncChan chan struct{} +} + +// NewTransfer creates a new transfer. +func NewTransfer() Transfer { + t := &transfer{ + watchers: make(map[chan struct{}]*Watcher), + running: make(chan struct{}), + released: make(chan struct{}), + broadcastSyncChan: make(chan struct{}), + } + + // This uses context.Background instead of a caller-supplied context + // so that a transfer won't be cancelled automatically if the client + // which requested it is ^C'd (there could be other viewers). + t.ctx, t.cancel = context.WithCancel(context.Background()) + + return t +} + +// Broadcast copies the progress and error output to all viewers. +func (t *transfer) Broadcast(masterProgressChan <-chan progress.Progress) { + for { + var ( + p progress.Progress + ok bool + ) + select { + case p, ok = <-masterProgressChan: + default: + // We've depleted the channel, so now we can handle + // reads on broadcastSyncChan to let detaching watchers + // know we're caught up. + select { + case <-t.broadcastSyncChan: + continue + case p, ok = <-masterProgressChan: + } + } + + t.mu.Lock() + if ok { + t.lastProgress = p + t.hasLastProgress = true + for _, w := range t.watchers { + select { + case w.signalChan <- struct{}{}: + default: + } + } + } else { + t.broadcastDone = true + } + t.mu.Unlock() + if !ok { + close(t.running) + return + } + } +} + +// Watch adds a watcher to the transfer. The supplied channel gets progress +// updates and is closed when the transfer finishes. +func (t *transfer) Watch(progressOutput progress.Output) *Watcher { + t.mu.Lock() + defer t.mu.Unlock() + + w := &Watcher{ + releaseChan: make(chan struct{}), + signalChan: make(chan struct{}), + running: make(chan struct{}), + } + + t.watchers[w.releaseChan] = w + + if t.broadcastDone { + close(w.running) + return w + } + + go func() { + defer func() { + close(w.running) + }() + var ( + done bool + lastWritten progress.Progress + hasLastWritten bool + ) + for { + t.mu.Lock() + hasLastProgress := t.hasLastProgress + lastProgress := t.lastProgress + t.mu.Unlock() + + // Make sure we don't write the last progress item + // twice. + if hasLastProgress && (!done || !hasLastWritten || lastProgress != lastWritten) { + progressOutput.WriteProgress(lastProgress) + lastWritten = lastProgress + hasLastWritten = true + } + + if done { + return + } + + select { + case <-w.signalChan: + case <-w.releaseChan: + done = true + // Since the watcher is going to detach, make + // sure the broadcaster is caught up so we + // don't miss anything. + select { + case t.broadcastSyncChan <- struct{}{}: + case <-t.running: + } + case <-t.running: + done = true + } + } + }() + + return w +} + +// Release is the inverse of Watch; indicating that the watcher no longer wants +// to be notified about the progress of the transfer. All calls to Watch must +// be paired with later calls to Release so that the lifecycle of the transfer +// is properly managed. +func (t *transfer) Release(watcher *Watcher) { + t.mu.Lock() + delete(t.watchers, watcher.releaseChan) + + if len(t.watchers) == 0 { + if t.closed { + // released may have been closed already if all + // watchers were released, then another one was added + // while waiting for a previous watcher goroutine to + // finish. + select { + case <-t.released: + default: + close(t.released) + } + } else { + t.cancel() + } + } + t.mu.Unlock() + + close(watcher.releaseChan) + // Block until the watcher goroutine completes + <-watcher.running +} + +// Done returns a channel which is closed if the transfer completes or is +// cancelled. Note that having 0 watchers causes a transfer to be cancelled. +func (t *transfer) Done() <-chan struct{} { + // Note that this doesn't return t.ctx.Done() because that channel will + // be closed the moment Cancel is called, and we need to return a + // channel that blocks until a cancellation is actually acknowledged by + // the transfer function. + return t.running +} + +// Released returns a channel which is closed once all watchers release the +// transfer AND the transfer is no longer tracked by the transfer manager. +func (t *transfer) Released() <-chan struct{} { + return t.released +} + +// Context returns the context associated with the transfer. +func (t *transfer) Context() context.Context { + return t.ctx +} + +// Close is called by the transfer manager when the transfer is no longer +// being tracked. +func (t *transfer) Close() { + t.mu.Lock() + t.closed = true + if len(t.watchers) == 0 { + close(t.released) + } + t.mu.Unlock() +} + +// DoFunc is a function called by the transfer manager to actually perform +// a transfer. It should be non-blocking. It should wait until the start channel +// is closed before transferring any data. If the function closes inactive, that +// signals to the transfer manager that the job is no longer actively moving +// data - for example, it may be waiting for a dependent transfer to finish. +// This prevents it from taking up a slot. +type DoFunc func(progressChan chan<- progress.Progress, start <-chan struct{}, inactive chan<- struct{}) Transfer + +// TransferManager is used by LayerDownloadManager and LayerUploadManager to +// schedule and deduplicate transfers. It is up to the TransferManager +// implementation to make the scheduling and concurrency decisions. +type TransferManager interface { + // Transfer checks if a transfer with the given key is in progress. If + // so, it returns progress and error output from that transfer. + // Otherwise, it will call xferFunc to initiate the transfer. + Transfer(key string, xferFunc DoFunc, progressOutput progress.Output) (Transfer, *Watcher) +} + +type transferManager struct { + mu sync.Mutex + + concurrencyLimit int + activeTransfers int + transfers map[string]Transfer + waitingTransfers []chan struct{} +} + +// NewTransferManager returns a new TransferManager. +func NewTransferManager(concurrencyLimit int) TransferManager { + return &transferManager{ + concurrencyLimit: concurrencyLimit, + transfers: make(map[string]Transfer), + } +} + +// Transfer checks if a transfer matching the given key is in progress. If not, +// it starts one by calling xferFunc. The caller supplies a channel which +// receives progress output from the transfer. +func (tm *transferManager) Transfer(key string, xferFunc DoFunc, progressOutput progress.Output) (Transfer, *Watcher) { + tm.mu.Lock() + defer tm.mu.Unlock() + + for { + xfer, present := tm.transfers[key] + if !present { + break + } + // Transfer is already in progress. + watcher := xfer.Watch(progressOutput) + + select { + case <-xfer.Context().Done(): + // We don't want to watch a transfer that has been cancelled. + // Wait for it to be removed from the map and try again. + xfer.Release(watcher) + tm.mu.Unlock() + // The goroutine that removes this transfer from the + // map is also waiting for xfer.Done(), so yield to it. + // This could be avoided by adding a Closed method + // to Transfer to allow explicitly waiting for it to be + // removed the map, but forcing a scheduling round in + // this very rare case seems better than bloating the + // interface definition. + runtime.Gosched() + <-xfer.Done() + tm.mu.Lock() + default: + return xfer, watcher + } + } + + start := make(chan struct{}) + inactive := make(chan struct{}) + + if tm.activeTransfers < tm.concurrencyLimit { + close(start) + tm.activeTransfers++ + } else { + tm.waitingTransfers = append(tm.waitingTransfers, start) + } + + masterProgressChan := make(chan progress.Progress) + xfer := xferFunc(masterProgressChan, start, inactive) + watcher := xfer.Watch(progressOutput) + go xfer.Broadcast(masterProgressChan) + tm.transfers[key] = xfer + + // When the transfer is finished, remove from the map. + go func() { + for { + select { + case <-inactive: + tm.mu.Lock() + tm.inactivate(start) + tm.mu.Unlock() + inactive = nil + case <-xfer.Done(): + tm.mu.Lock() + if inactive != nil { + tm.inactivate(start) + } + delete(tm.transfers, key) + tm.mu.Unlock() + xfer.Close() + return + } + } + }() + + return xfer, watcher +} + +func (tm *transferManager) inactivate(start chan struct{}) { + // If the transfer was started, remove it from the activeTransfers + // count. + select { + case <-start: + // Start next transfer if any are waiting + if len(tm.waitingTransfers) != 0 { + close(tm.waitingTransfers[0]) + tm.waitingTransfers = tm.waitingTransfers[1:] + } else { + tm.activeTransfers-- + } + default: + } +} diff --git a/distribution/xfer/transfer_test.go b/distribution/xfer/transfer_test.go new file mode 100644 index 00000000..39fb7d08 --- /dev/null +++ b/distribution/xfer/transfer_test.go @@ -0,0 +1,414 @@ +package xfer + +import ( + "sync/atomic" + "testing" + "time" + + "github.com/docker/docker/pkg/progress" +) + +func TestTransfer(t *testing.T) { + makeXferFunc := func(id string) DoFunc { + return func(progressChan chan<- progress.Progress, start <-chan struct{}, inactive chan<- struct{}) Transfer { + select { + case <-start: + default: + t.Fatalf("transfer function not started even though concurrency limit not reached") + } + + xfer := NewTransfer() + go func() { + for i := 0; i <= 10; i++ { + progressChan <- progress.Progress{ID: id, Action: "testing", Current: int64(i), Total: 10} + time.Sleep(10 * time.Millisecond) + } + close(progressChan) + }() + return xfer + } + } + + tm := NewTransferManager(5) + progressChan := make(chan progress.Progress) + progressDone := make(chan struct{}) + receivedProgress := make(map[string]int64) + + go func() { + for p := range progressChan { + val, present := receivedProgress[p.ID] + if !present { + if p.Current != 0 { + t.Fatalf("got unexpected progress value: %d (expected 0)", p.Current) + } + } else if p.Current <= val { + t.Fatalf("got unexpected progress value: %d (expected %d)", p.Current, val+1) + } + receivedProgress[p.ID] = p.Current + } + close(progressDone) + }() + + // Start a few transfers + ids := []string{"id1", "id2", "id3"} + xfers := make([]Transfer, len(ids)) + watchers := make([]*Watcher, len(ids)) + for i, id := range ids { + xfers[i], watchers[i] = tm.Transfer(id, makeXferFunc(id), progress.ChanOutput(progressChan)) + } + + for i, xfer := range xfers { + <-xfer.Done() + xfer.Release(watchers[i]) + } + close(progressChan) + <-progressDone + + for _, id := range ids { + if receivedProgress[id] != 10 { + t.Fatalf("final progress value %d instead of 10", receivedProgress[id]) + } + } +} + +func TestConcurrencyLimit(t *testing.T) { + concurrencyLimit := 3 + var runningJobs int32 + + makeXferFunc := func(id string) DoFunc { + return func(progressChan chan<- progress.Progress, start <-chan struct{}, inactive chan<- struct{}) Transfer { + xfer := NewTransfer() + go func() { + <-start + totalJobs := atomic.AddInt32(&runningJobs, 1) + if int(totalJobs) > concurrencyLimit { + t.Fatalf("too many jobs running") + } + for i := 0; i <= 10; i++ { + progressChan <- progress.Progress{ID: id, Action: "testing", Current: int64(i), Total: 10} + time.Sleep(10 * time.Millisecond) + } + atomic.AddInt32(&runningJobs, -1) + close(progressChan) + }() + return xfer + } + } + + tm := NewTransferManager(concurrencyLimit) + progressChan := make(chan progress.Progress) + progressDone := make(chan struct{}) + receivedProgress := make(map[string]int64) + + go func() { + for p := range progressChan { + receivedProgress[p.ID] = p.Current + } + close(progressDone) + }() + + // Start more transfers than the concurrency limit + ids := []string{"id1", "id2", "id3", "id4", "id5", "id6", "id7", "id8"} + xfers := make([]Transfer, len(ids)) + watchers := make([]*Watcher, len(ids)) + for i, id := range ids { + xfers[i], watchers[i] = tm.Transfer(id, makeXferFunc(id), progress.ChanOutput(progressChan)) + } + + for i, xfer := range xfers { + <-xfer.Done() + xfer.Release(watchers[i]) + } + close(progressChan) + <-progressDone + + for _, id := range ids { + if receivedProgress[id] != 10 { + t.Fatalf("final progress value %d instead of 10", receivedProgress[id]) + } + } +} + +func TestInactiveJobs(t *testing.T) { + concurrencyLimit := 3 + var runningJobs int32 + testDone := make(chan struct{}) + + makeXferFunc := func(id string) DoFunc { + return func(progressChan chan<- progress.Progress, start <-chan struct{}, inactive chan<- struct{}) Transfer { + xfer := NewTransfer() + go func() { + <-start + totalJobs := atomic.AddInt32(&runningJobs, 1) + if int(totalJobs) > concurrencyLimit { + t.Fatalf("too many jobs running") + } + for i := 0; i <= 10; i++ { + progressChan <- progress.Progress{ID: id, Action: "testing", Current: int64(i), Total: 10} + time.Sleep(10 * time.Millisecond) + } + atomic.AddInt32(&runningJobs, -1) + close(inactive) + <-testDone + close(progressChan) + }() + return xfer + } + } + + tm := NewTransferManager(concurrencyLimit) + progressChan := make(chan progress.Progress) + progressDone := make(chan struct{}) + receivedProgress := make(map[string]int64) + + go func() { + for p := range progressChan { + receivedProgress[p.ID] = p.Current + } + close(progressDone) + }() + + // Start more transfers than the concurrency limit + ids := []string{"id1", "id2", "id3", "id4", "id5", "id6", "id7", "id8"} + xfers := make([]Transfer, len(ids)) + watchers := make([]*Watcher, len(ids)) + for i, id := range ids { + xfers[i], watchers[i] = tm.Transfer(id, makeXferFunc(id), progress.ChanOutput(progressChan)) + } + + close(testDone) + for i, xfer := range xfers { + <-xfer.Done() + xfer.Release(watchers[i]) + } + close(progressChan) + <-progressDone + + for _, id := range ids { + if receivedProgress[id] != 10 { + t.Fatalf("final progress value %d instead of 10", receivedProgress[id]) + } + } +} + +func TestWatchRelease(t *testing.T) { + ready := make(chan struct{}) + + makeXferFunc := func(id string) DoFunc { + return func(progressChan chan<- progress.Progress, start <-chan struct{}, inactive chan<- struct{}) Transfer { + xfer := NewTransfer() + go func() { + defer func() { + close(progressChan) + }() + <-ready + for i := int64(0); ; i++ { + select { + case <-time.After(10 * time.Millisecond): + case <-xfer.Context().Done(): + return + } + progressChan <- progress.Progress{ID: id, Action: "testing", Current: i, Total: 10} + } + }() + return xfer + } + } + + tm := NewTransferManager(5) + + type watcherInfo struct { + watcher *Watcher + progressChan chan progress.Progress + progressDone chan struct{} + receivedFirstProgress chan struct{} + } + + progressConsumer := func(w watcherInfo) { + first := true + for range w.progressChan { + if first { + close(w.receivedFirstProgress) + } + first = false + } + close(w.progressDone) + } + + // Start a transfer + watchers := make([]watcherInfo, 5) + var xfer Transfer + watchers[0].progressChan = make(chan progress.Progress) + watchers[0].progressDone = make(chan struct{}) + watchers[0].receivedFirstProgress = make(chan struct{}) + xfer, watchers[0].watcher = tm.Transfer("id1", makeXferFunc("id1"), progress.ChanOutput(watchers[0].progressChan)) + go progressConsumer(watchers[0]) + + // Give it multiple watchers + for i := 1; i != len(watchers); i++ { + watchers[i].progressChan = make(chan progress.Progress) + watchers[i].progressDone = make(chan struct{}) + watchers[i].receivedFirstProgress = make(chan struct{}) + watchers[i].watcher = xfer.Watch(progress.ChanOutput(watchers[i].progressChan)) + go progressConsumer(watchers[i]) + } + + // Now that the watchers are set up, allow the transfer goroutine to + // proceed. + close(ready) + + // Confirm that each watcher gets progress output. + for _, w := range watchers { + <-w.receivedFirstProgress + } + + // Release one watcher every 5ms + for _, w := range watchers { + xfer.Release(w.watcher) + <-time.After(5 * time.Millisecond) + } + + // Now that all watchers have been released, Released() should + // return a closed channel. + <-xfer.Released() + + // Done() should return a closed channel because the xfer func returned + // due to cancellation. + <-xfer.Done() + + for _, w := range watchers { + close(w.progressChan) + <-w.progressDone + } +} + +func TestWatchFinishedTransfer(t *testing.T) { + makeXferFunc := func(id string) DoFunc { + return func(progressChan chan<- progress.Progress, start <-chan struct{}, inactive chan<- struct{}) Transfer { + xfer := NewTransfer() + go func() { + // Finish immediately + close(progressChan) + }() + return xfer + } + } + + tm := NewTransferManager(5) + + // Start a transfer + watchers := make([]*Watcher, 3) + var xfer Transfer + xfer, watchers[0] = tm.Transfer("id1", makeXferFunc("id1"), progress.ChanOutput(make(chan progress.Progress))) + + // Give it a watcher immediately + watchers[1] = xfer.Watch(progress.ChanOutput(make(chan progress.Progress))) + + // Wait for the transfer to complete + <-xfer.Done() + + // Set up another watcher + watchers[2] = xfer.Watch(progress.ChanOutput(make(chan progress.Progress))) + + // Release the watchers + for _, w := range watchers { + xfer.Release(w) + } + + // Now that all watchers have been released, Released() should + // return a closed channel. + <-xfer.Released() +} + +func TestDuplicateTransfer(t *testing.T) { + ready := make(chan struct{}) + + var xferFuncCalls int32 + + makeXferFunc := func(id string) DoFunc { + return func(progressChan chan<- progress.Progress, start <-chan struct{}, inactive chan<- struct{}) Transfer { + atomic.AddInt32(&xferFuncCalls, 1) + xfer := NewTransfer() + go func() { + defer func() { + close(progressChan) + }() + <-ready + for i := int64(0); ; i++ { + select { + case <-time.After(10 * time.Millisecond): + case <-xfer.Context().Done(): + return + } + progressChan <- progress.Progress{ID: id, Action: "testing", Current: i, Total: 10} + } + }() + return xfer + } + } + + tm := NewTransferManager(5) + + type transferInfo struct { + xfer Transfer + watcher *Watcher + progressChan chan progress.Progress + progressDone chan struct{} + receivedFirstProgress chan struct{} + } + + progressConsumer := func(t transferInfo) { + first := true + for range t.progressChan { + if first { + close(t.receivedFirstProgress) + } + first = false + } + close(t.progressDone) + } + + // Try to start multiple transfers with the same ID + transfers := make([]transferInfo, 5) + for i := range transfers { + t := &transfers[i] + t.progressChan = make(chan progress.Progress) + t.progressDone = make(chan struct{}) + t.receivedFirstProgress = make(chan struct{}) + t.xfer, t.watcher = tm.Transfer("id1", makeXferFunc("id1"), progress.ChanOutput(t.progressChan)) + go progressConsumer(*t) + } + + // Allow the transfer goroutine to proceed. + close(ready) + + // Confirm that each watcher gets progress output. + for _, t := range transfers { + <-t.receivedFirstProgress + } + + // Confirm that the transfer function was called exactly once. + if xferFuncCalls != 1 { + t.Fatal("transfer function wasn't called exactly once") + } + + // Release one watcher every 5ms + for _, t := range transfers { + t.xfer.Release(t.watcher) + <-time.After(5 * time.Millisecond) + } + + for _, t := range transfers { + // Now that all watchers have been released, Released() should + // return a closed channel. + <-t.xfer.Released() + // Done() should return a closed channel because the xfer func returned + // due to cancellation. + <-t.xfer.Done() + } + + for _, t := range transfers { + close(t.progressChan) + <-t.progressDone + } +} diff --git a/distribution/xfer/upload.go b/distribution/xfer/upload.go new file mode 100644 index 00000000..563824c1 --- /dev/null +++ b/distribution/xfer/upload.go @@ -0,0 +1,163 @@ +package xfer + +import ( + "errors" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution" + "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/progress" + "golang.org/x/net/context" +) + +const maxUploadAttempts = 5 + +// LayerUploadManager provides task management and progress reporting for +// uploads. +type LayerUploadManager struct { + tm TransferManager +} + +// NewLayerUploadManager returns a new LayerUploadManager. +func NewLayerUploadManager(concurrencyLimit int) *LayerUploadManager { + return &LayerUploadManager{ + tm: NewTransferManager(concurrencyLimit), + } +} + +type uploadTransfer struct { + Transfer + + remoteDescriptor distribution.Descriptor + err error +} + +// An UploadDescriptor references a layer that may need to be uploaded. +type UploadDescriptor interface { + // Key returns the key used to deduplicate uploads. + Key() string + // ID returns the ID for display purposes. + ID() string + // DiffID should return the DiffID for this layer. + DiffID() layer.DiffID + // Upload is called to perform the Upload. + Upload(ctx context.Context, progressOutput progress.Output) (distribution.Descriptor, error) + // SetRemoteDescriptor provides the distribution.Descriptor that was + // returned by Upload. This descriptor is not to be confused with + // the UploadDescriptor interface, which is used for internally + // identifying layers that are being uploaded. + SetRemoteDescriptor(descriptor distribution.Descriptor) +} + +// Upload is a blocking function which ensures the listed layers are present on +// the remote registry. It uses the string returned by the Key method to +// deduplicate uploads. +func (lum *LayerUploadManager) Upload(ctx context.Context, layers []UploadDescriptor, progressOutput progress.Output) error { + var ( + uploads []*uploadTransfer + dedupDescriptors = make(map[string]*uploadTransfer) + ) + + for _, descriptor := range layers { + progress.Update(progressOutput, descriptor.ID(), "Preparing") + + key := descriptor.Key() + if _, present := dedupDescriptors[key]; present { + continue + } + + xferFunc := lum.makeUploadFunc(descriptor) + upload, watcher := lum.tm.Transfer(descriptor.Key(), xferFunc, progressOutput) + defer upload.Release(watcher) + uploads = append(uploads, upload.(*uploadTransfer)) + dedupDescriptors[key] = upload.(*uploadTransfer) + } + + for _, upload := range uploads { + select { + case <-ctx.Done(): + return ctx.Err() + case <-upload.Transfer.Done(): + if upload.err != nil { + return upload.err + } + } + } + for _, l := range layers { + l.SetRemoteDescriptor(dedupDescriptors[l.Key()].remoteDescriptor) + } + + return nil +} + +func (lum *LayerUploadManager) makeUploadFunc(descriptor UploadDescriptor) DoFunc { + return func(progressChan chan<- progress.Progress, start <-chan struct{}, inactive chan<- struct{}) Transfer { + u := &uploadTransfer{ + Transfer: NewTransfer(), + } + + go func() { + defer func() { + close(progressChan) + }() + + progressOutput := progress.ChanOutput(progressChan) + + select { + case <-start: + default: + progress.Update(progressOutput, descriptor.ID(), "Waiting") + <-start + } + + retries := 0 + for { + remoteDescriptor, err := descriptor.Upload(u.Transfer.Context(), progressOutput) + if err == nil { + u.remoteDescriptor = remoteDescriptor + break + } + + // If an error was returned because the context + // was cancelled, we shouldn't retry. + select { + case <-u.Transfer.Context().Done(): + u.err = err + return + default: + } + + retries++ + if _, isDNR := err.(DoNotRetry); isDNR || retries == maxUploadAttempts { + logrus.Errorf("Upload failed: %v", err) + u.err = err + return + } + + logrus.Errorf("Upload failed, retrying: %v", err) + delay := retries * 5 + ticker := time.NewTicker(time.Second) + + selectLoop: + for { + progress.Updatef(progressOutput, descriptor.ID(), "Retrying in %d second%s", delay, (map[bool]string{true: "s"})[delay != 1]) + select { + case <-ticker.C: + delay-- + if delay == 0 { + ticker.Stop() + break selectLoop + } + case <-u.Transfer.Context().Done(): + ticker.Stop() + u.err = errors.New("upload cancelled during retry delay") + return + } + } + } + }() + + return u + } +} diff --git a/distribution/xfer/upload_test.go b/distribution/xfer/upload_test.go new file mode 100644 index 00000000..275d2426 --- /dev/null +++ b/distribution/xfer/upload_test.go @@ -0,0 +1,143 @@ +package xfer + +import ( + "errors" + "sync/atomic" + "testing" + "time" + + "github.com/docker/distribution" + "github.com/docker/distribution/digest" + "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/progress" + "golang.org/x/net/context" +) + +const maxUploadConcurrency = 3 + +type mockUploadDescriptor struct { + currentUploads *int32 + diffID layer.DiffID + simulateRetries int +} + +// Key returns the key used to deduplicate downloads. +func (u *mockUploadDescriptor) Key() string { + return u.diffID.String() +} + +// ID returns the ID for display purposes. +func (u *mockUploadDescriptor) ID() string { + return u.diffID.String() +} + +// DiffID should return the DiffID for this layer. +func (u *mockUploadDescriptor) DiffID() layer.DiffID { + return u.diffID +} + +// SetRemoteDescriptor is not used in the mock. +func (u *mockUploadDescriptor) SetRemoteDescriptor(remoteDescriptor distribution.Descriptor) { +} + +// Upload is called to perform the upload. +func (u *mockUploadDescriptor) Upload(ctx context.Context, progressOutput progress.Output) (distribution.Descriptor, error) { + if u.currentUploads != nil { + defer atomic.AddInt32(u.currentUploads, -1) + + if atomic.AddInt32(u.currentUploads, 1) > maxUploadConcurrency { + return distribution.Descriptor{}, errors.New("concurrency limit exceeded") + } + } + + // Sleep a bit to simulate a time-consuming upload. + for i := int64(0); i <= 10; i++ { + select { + case <-ctx.Done(): + return distribution.Descriptor{}, ctx.Err() + case <-time.After(10 * time.Millisecond): + progressOutput.WriteProgress(progress.Progress{ID: u.ID(), Current: i, Total: 10}) + } + } + + if u.simulateRetries != 0 { + u.simulateRetries-- + return distribution.Descriptor{}, errors.New("simulating retry") + } + + return distribution.Descriptor{}, nil +} + +func uploadDescriptors(currentUploads *int32) []UploadDescriptor { + return []UploadDescriptor{ + &mockUploadDescriptor{currentUploads, layer.DiffID("sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"), 0}, + &mockUploadDescriptor{currentUploads, layer.DiffID("sha256:1515325234325236634634608943609283523908626098235490238423902343"), 0}, + &mockUploadDescriptor{currentUploads, layer.DiffID("sha256:6929356290463485374960346430698374523437683470934634534953453453"), 0}, + &mockUploadDescriptor{currentUploads, layer.DiffID("sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"), 0}, + &mockUploadDescriptor{currentUploads, layer.DiffID("sha256:8159352387436803946235346346368745389534789534897538734598734987"), 1}, + &mockUploadDescriptor{currentUploads, layer.DiffID("sha256:4637863963478346897346987346987346789346789364879364897364987346"), 0}, + } +} + +var expectedDigests = map[layer.DiffID]digest.Digest{ + layer.DiffID("sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"): digest.Digest("sha256:c5095d6cf7ee42b7b064371dcc1dc3fb4af197f04d01a60009d484bd432724fc"), + layer.DiffID("sha256:1515325234325236634634608943609283523908626098235490238423902343"): digest.Digest("sha256:968cbfe2ff5269ea1729b3804767a1f57ffbc442d3bc86f47edbf7e688a4f36e"), + layer.DiffID("sha256:6929356290463485374960346430698374523437683470934634534953453453"): digest.Digest("sha256:8a5e56ab4b477a400470a7d5d4c1ca0c91235fd723ab19cc862636a06f3a735d"), + layer.DiffID("sha256:8159352387436803946235346346368745389534789534897538734598734987"): digest.Digest("sha256:5e733e5cd3688512fc240bd5c178e72671c9915947d17bb8451750d827944cb2"), + layer.DiffID("sha256:4637863963478346897346987346987346789346789364879364897364987346"): digest.Digest("sha256:ec4bb98d15e554a9f66c3ef9296cf46772c0ded3b1592bd8324d96e2f60f460c"), +} + +func TestSuccessfulUpload(t *testing.T) { + lum := NewLayerUploadManager(maxUploadConcurrency) + + progressChan := make(chan progress.Progress) + progressDone := make(chan struct{}) + receivedProgress := make(map[string]int64) + + go func() { + for p := range progressChan { + receivedProgress[p.ID] = p.Current + } + close(progressDone) + }() + + var currentUploads int32 + descriptors := uploadDescriptors(¤tUploads) + + err := lum.Upload(context.Background(), descriptors, progress.ChanOutput(progressChan)) + if err != nil { + t.Fatalf("upload error: %v", err) + } + + close(progressChan) + <-progressDone +} + +func TestCancelledUpload(t *testing.T) { + lum := NewLayerUploadManager(maxUploadConcurrency) + + progressChan := make(chan progress.Progress) + progressDone := make(chan struct{}) + + go func() { + for range progressChan { + } + close(progressDone) + }() + + ctx, cancel := context.WithCancel(context.Background()) + + go func() { + <-time.After(time.Millisecond) + cancel() + }() + + descriptors := uploadDescriptors(nil) + err := lum.Upload(ctx, descriptors, progress.ChanOutput(progressChan)) + if err != context.Canceled { + t.Fatal("expected upload to be cancelled") + } + + close(progressChan) + <-progressDone +} diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 00000000..015bc133 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,3 @@ +docker.go contains Docker's main function. + +This file provides first line CLI argument parsing and environment variable setting. diff --git a/docker/client.go b/docker/client.go new file mode 100644 index 00000000..3b4c4f63 --- /dev/null +++ b/docker/client.go @@ -0,0 +1,33 @@ +package main + +import ( + "path/filepath" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cliconfig" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/utils" +) + +var clientFlags = &cli.ClientFlags{FlagSet: new(flag.FlagSet), Common: commonFlags} + +func init() { + client := clientFlags.FlagSet + client.StringVar(&clientFlags.ConfigDir, []string{"-config"}, cliconfig.ConfigDir(), "Location of client config files") + + clientFlags.PostParse = func() { + clientFlags.Common.PostParse() + + if clientFlags.ConfigDir != "" { + cliconfig.SetConfigDir(clientFlags.ConfigDir) + } + + if clientFlags.Common.TrustKey == "" { + clientFlags.Common.TrustKey = filepath.Join(cliconfig.ConfigDir(), defaultTrustKeyFile) + } + + if clientFlags.Common.Debug { + utils.EnableDebug() + } + } +} diff --git a/docker/client_test.go b/docker/client_test.go new file mode 100644 index 00000000..5708c96c --- /dev/null +++ b/docker/client_test.go @@ -0,0 +1,23 @@ +package main + +import ( + "os" + "testing" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/utils" +) + +func TestClientDebugEnabled(t *testing.T) { + defer utils.DisableDebug() + + clientFlags.Common.FlagSet.Parse([]string{"-D"}) + clientFlags.PostParse() + + if os.Getenv("DEBUG") != "1" { + t.Fatal("expected debug enabled, got false") + } + if logrus.GetLevel() != logrus.DebugLevel { + t.Fatalf("expected logrus debug level, got %v", logrus.GetLevel()) + } +} diff --git a/docker/common.go b/docker/common.go new file mode 100644 index 00000000..6028f79d --- /dev/null +++ b/docker/common.go @@ -0,0 +1,100 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/cli" + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/opts" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/go-connections/tlsconfig" +) + +const ( + defaultTrustKeyFile = "key.json" + defaultCaFile = "ca.pem" + defaultKeyFile = "key.pem" + defaultCertFile = "cert.pem" + tlsVerifyKey = "tlsverify" +) + +var ( + commonFlags = &cli.CommonFlags{FlagSet: new(flag.FlagSet)} + + dockerCertPath = os.Getenv("DOCKER_CERT_PATH") + dockerTLSVerify = os.Getenv("DOCKER_TLS_VERIFY") != "" +) + +func init() { + if dockerCertPath == "" { + dockerCertPath = cliconfig.ConfigDir() + } + + commonFlags.PostParse = postParseCommon + + cmd := commonFlags.FlagSet + + cmd.BoolVar(&commonFlags.Debug, []string{"D", "-debug"}, false, "Enable debug mode") + cmd.StringVar(&commonFlags.LogLevel, []string{"l", "-log-level"}, "info", "Set the logging level") + cmd.BoolVar(&commonFlags.TLS, []string{"-tls"}, false, "Use TLS; implied by --tlsverify") + cmd.BoolVar(&commonFlags.TLSVerify, []string{"-tlsverify"}, dockerTLSVerify, "Use TLS and verify the remote") + + // TODO use flag flag.String([]string{"i", "-identity"}, "", "Path to libtrust key file") + + var tlsOptions tlsconfig.Options + commonFlags.TLSOptions = &tlsOptions + cmd.StringVar(&tlsOptions.CAFile, []string{"-tlscacert"}, filepath.Join(dockerCertPath, defaultCaFile), "Trust certs signed only by this CA") + cmd.StringVar(&tlsOptions.CertFile, []string{"-tlscert"}, filepath.Join(dockerCertPath, defaultCertFile), "Path to TLS certificate file") + cmd.StringVar(&tlsOptions.KeyFile, []string{"-tlskey"}, filepath.Join(dockerCertPath, defaultKeyFile), "Path to TLS key file") + + cmd.Var(opts.NewNamedListOptsRef("hosts", &commonFlags.Hosts, opts.ValidateHost), []string{"H", "-host"}, "Daemon socket(s) to connect to") +} + +func postParseCommon() { + cmd := commonFlags.FlagSet + + setDaemonLogLevel(commonFlags.LogLevel) + + // Regardless of whether the user sets it to true or false, if they + // specify --tlsverify at all then we need to turn on tls + // TLSVerify can be true even if not set due to DOCKER_TLS_VERIFY env var, so we need to check that here as well + if cmd.IsSet("-"+tlsVerifyKey) || commonFlags.TLSVerify { + commonFlags.TLS = true + } + + if !commonFlags.TLS { + commonFlags.TLSOptions = nil + } else { + tlsOptions := commonFlags.TLSOptions + tlsOptions.InsecureSkipVerify = !commonFlags.TLSVerify + + // Reset CertFile and KeyFile to empty string if the user did not specify + // the respective flags and the respective default files were not found. + if !cmd.IsSet("-tlscert") { + if _, err := os.Stat(tlsOptions.CertFile); os.IsNotExist(err) { + tlsOptions.CertFile = "" + } + } + if !cmd.IsSet("-tlskey") { + if _, err := os.Stat(tlsOptions.KeyFile); os.IsNotExist(err) { + tlsOptions.KeyFile = "" + } + } + } +} + +func setDaemonLogLevel(logLevel string) { + if logLevel != "" { + lvl, err := logrus.ParseLevel(logLevel) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to parse logging level: %s\n", logLevel) + os.Exit(1) + } + logrus.SetLevel(lvl) + } else { + logrus.SetLevel(logrus.InfoLevel) + } +} diff --git a/docker/daemon.go b/docker/daemon.go new file mode 100644 index 00000000..bee921c7 --- /dev/null +++ b/docker/daemon.go @@ -0,0 +1,421 @@ +// +build daemon + +package main + +import ( + "crypto/tls" + "fmt" + "io" + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/uuid" + apiserver "github.com/docker/docker/api/server" + "github.com/docker/docker/api/server/router" + "github.com/docker/docker/api/server/router/build" + "github.com/docker/docker/api/server/router/container" + "github.com/docker/docker/api/server/router/image" + "github.com/docker/docker/api/server/router/network" + systemrouter "github.com/docker/docker/api/server/router/system" + "github.com/docker/docker/api/server/router/volume" + "github.com/docker/docker/builder/dockerfile" + "github.com/docker/docker/cli" + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/daemon" + "github.com/docker/docker/daemon/logger" + "github.com/docker/docker/docker/listeners" + "github.com/docker/docker/dockerversion" + "github.com/docker/docker/libcontainerd" + "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/jsonlog" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/pidfile" + "github.com/docker/docker/pkg/signal" + "github.com/docker/docker/pkg/system" + "github.com/docker/docker/registry" + "github.com/docker/docker/utils" + "github.com/docker/go-connections/tlsconfig" +) + +const ( + daemonUsage = " docker daemon [ --help | ... ]\n" + daemonConfigFileFlag = "-config-file" +) + +var ( + daemonCli cli.Handler = NewDaemonCli() +) + +// DaemonCli represents the daemon CLI. +type DaemonCli struct { + *daemon.Config + flags *flag.FlagSet +} + +func presentInHelp(usage string) string { return usage } +func absentFromHelp(string) string { return "" } + +// NewDaemonCli returns a pre-configured daemon CLI +func NewDaemonCli() *DaemonCli { + daemonFlags := cli.Subcmd("daemon", nil, "Enable daemon mode", true) + + // TODO(tiborvass): remove InstallFlags? + daemonConfig := new(daemon.Config) + daemonConfig.LogConfig.Config = make(map[string]string) + daemonConfig.ClusterOpts = make(map[string]string) + + if runtime.GOOS != "linux" { + daemonConfig.V2Only = true + } + + daemonConfig.InstallFlags(daemonFlags, presentInHelp) + daemonConfig.InstallFlags(flag.CommandLine, absentFromHelp) + daemonFlags.Require(flag.Exact, 0) + + return &DaemonCli{ + Config: daemonConfig, + flags: daemonFlags, + } +} + +func migrateKey() (err error) { + // Migrate trust key if exists at ~/.docker/key.json and owned by current user + oldPath := filepath.Join(cliconfig.ConfigDir(), defaultTrustKeyFile) + newPath := filepath.Join(getDaemonConfDir(), defaultTrustKeyFile) + if _, statErr := os.Stat(newPath); os.IsNotExist(statErr) && currentUserIsOwner(oldPath) { + defer func() { + // Ensure old path is removed if no error occurred + if err == nil { + err = os.Remove(oldPath) + } else { + logrus.Warnf("Key migration failed, key file not removed at %s", oldPath) + os.Remove(newPath) + } + }() + + if err := system.MkdirAll(getDaemonConfDir(), os.FileMode(0644)); err != nil { + return fmt.Errorf("Unable to create daemon configuration directory: %s", err) + } + + newFile, err := os.OpenFile(newPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return fmt.Errorf("error creating key file %q: %s", newPath, err) + } + defer newFile.Close() + + oldFile, err := os.Open(oldPath) + if err != nil { + return fmt.Errorf("error opening key file %q: %s", oldPath, err) + } + defer oldFile.Close() + + if _, err := io.Copy(newFile, oldFile); err != nil { + return fmt.Errorf("error copying key: %s", err) + } + + logrus.Infof("Migrated key from %s to %s", oldPath, newPath) + } + + return nil +} + +func getGlobalFlag() (globalFlag *flag.Flag) { + defer func() { + if x := recover(); x != nil { + switch f := x.(type) { + case *flag.Flag: + globalFlag = f + default: + panic(x) + } + } + }() + visitor := func(f *flag.Flag) { panic(f) } + commonFlags.FlagSet.Visit(visitor) + clientFlags.FlagSet.Visit(visitor) + return +} + +// CmdDaemon is the daemon command, called the raw arguments after `docker daemon`. +func (cli *DaemonCli) CmdDaemon(args ...string) error { + // warn from uuid package when running the daemon + uuid.Loggerf = logrus.Warnf + + if !commonFlags.FlagSet.IsEmpty() || !clientFlags.FlagSet.IsEmpty() { + // deny `docker -D daemon` + illegalFlag := getGlobalFlag() + fmt.Fprintf(os.Stderr, "invalid flag '-%s'.\nSee 'docker daemon --help'.\n", illegalFlag.Names[0]) + os.Exit(1) + } else { + // allow new form `docker daemon -D` + flag.Merge(cli.flags, commonFlags.FlagSet) + } + + configFile := cli.flags.String([]string{daemonConfigFileFlag}, defaultDaemonConfigFile, "Daemon configuration file") + + cli.flags.ParseFlags(args, true) + commonFlags.PostParse() + + if commonFlags.TrustKey == "" { + commonFlags.TrustKey = filepath.Join(getDaemonConfDir(), defaultTrustKeyFile) + } + cliConfig, err := loadDaemonCliConfig(cli.Config, cli.flags, commonFlags, *configFile) + if err != nil { + fmt.Fprint(os.Stderr, err) + os.Exit(1) + } + cli.Config = cliConfig + + if cli.Config.Debug { + utils.EnableDebug() + } + + if utils.ExperimentalBuild() { + logrus.Warn("Running experimental build") + } + + logrus.SetFormatter(&logrus.TextFormatter{ + TimestampFormat: jsonlog.RFC3339NanoFixed, + DisableColors: cli.Config.RawLogs, + }) + + if err := setDefaultUmask(); err != nil { + logrus.Fatalf("Failed to set umask: %v", err) + } + + if len(cli.LogConfig.Config) > 0 { + if err := logger.ValidateLogOpts(cli.LogConfig.Type, cli.LogConfig.Config); err != nil { + logrus.Fatalf("Failed to set log opts: %v", err) + } + } + + var pfile *pidfile.PIDFile + if cli.Pidfile != "" { + pf, err := pidfile.New(cli.Pidfile) + if err != nil { + logrus.Fatalf("Error starting daemon: %v", err) + } + pfile = pf + defer func() { + if err := pfile.Remove(); err != nil { + logrus.Error(err) + } + }() + } + + serverConfig := &apiserver.Config{ + AuthorizationPluginNames: cli.Config.AuthorizationPlugins, + Logging: true, + SocketGroup: cli.Config.SocketGroup, + Version: dockerversion.Version, + } + serverConfig = setPlatformServerConfig(serverConfig, cli.Config) + + if cli.Config.TLS { + tlsOptions := tlsconfig.Options{ + CAFile: cli.Config.CommonTLSOptions.CAFile, + CertFile: cli.Config.CommonTLSOptions.CertFile, + KeyFile: cli.Config.CommonTLSOptions.KeyFile, + } + + if cli.Config.TLSVerify { + // server requires and verifies client's certificate + tlsOptions.ClientAuth = tls.RequireAndVerifyClientCert + } + tlsConfig, err := tlsconfig.Server(tlsOptions) + if err != nil { + logrus.Fatal(err) + } + serverConfig.TLSConfig = tlsConfig + } + + if len(cli.Config.Hosts) == 0 { + cli.Config.Hosts = make([]string, 1) + } + + api := apiserver.New(serverConfig) + + for i := 0; i < len(cli.Config.Hosts); i++ { + var err error + if cli.Config.Hosts[i], err = opts.ParseHost(cli.Config.TLS, cli.Config.Hosts[i]); err != nil { + logrus.Fatalf("error parsing -H %s : %v", cli.Config.Hosts[i], err) + } + + protoAddr := cli.Config.Hosts[i] + protoAddrParts := strings.SplitN(protoAddr, "://", 2) + if len(protoAddrParts) != 2 { + logrus.Fatalf("bad format %s, expected PROTO://ADDR", protoAddr) + } + l, err := listeners.Init(protoAddrParts[0], protoAddrParts[1], serverConfig.SocketGroup, serverConfig.TLSConfig) + if err != nil { + logrus.Fatal(err) + } + + logrus.Debugf("Listener created for HTTP on %s (%s)", protoAddrParts[0], protoAddrParts[1]) + api.Accept(protoAddrParts[1], l...) + } + + if err := migrateKey(); err != nil { + logrus.Fatal(err) + } + cli.TrustKeyPath = commonFlags.TrustKey + + registryService := registry.NewService(cli.Config.ServiceOptions) + + containerdRemote, err := libcontainerd.New(filepath.Join(cli.Config.ExecRoot, "libcontainerd"), cli.getPlatformRemoteOptions()...) + if err != nil { + logrus.Fatal(err) + } + + d, err := daemon.NewDaemon(cli.Config, registryService, containerdRemote) + if err != nil { + if pfile != nil { + if err := pfile.Remove(); err != nil { + logrus.Error(err) + } + } + logrus.Fatalf("Error starting daemon: %v", err) + } + + logrus.Info("Daemon has completed initialization") + + logrus.WithFields(logrus.Fields{ + "version": dockerversion.Version, + "commit": dockerversion.GitCommit, + "graphdriver": d.GraphDriverName(), + }).Info("Docker daemon") + + initRouter(api, d) + + reload := func(config *daemon.Config) { + if err := d.Reload(config); err != nil { + logrus.Errorf("Error reconfiguring the daemon: %v", err) + return + } + if config.IsValueSet("debug") { + debugEnabled := utils.IsDebugEnabled() + switch { + case debugEnabled && !config.Debug: // disable debug + utils.DisableDebug() + api.DisableProfiler() + case config.Debug && !debugEnabled: // enable debug + utils.EnableDebug() + api.EnableProfiler() + } + + } + } + + setupConfigReloadTrap(*configFile, cli.flags, reload) + + // The serve API routine never exits unless an error occurs + // We need to start it as a goroutine and wait on it so + // daemon doesn't exit + serveAPIWait := make(chan error) + go api.Wait(serveAPIWait) + + signal.Trap(func() { + api.Close() + <-serveAPIWait + shutdownDaemon(d, 15) + if pfile != nil { + if err := pfile.Remove(); err != nil { + logrus.Error(err) + } + } + }) + + // after the daemon is done setting up we can notify systemd api + notifySystem() + + // Daemon is fully initialized and handling API traffic + // Wait for serve API to complete + errAPI := <-serveAPIWait + shutdownDaemon(d, 15) + containerdRemote.Cleanup() + if errAPI != nil { + if pfile != nil { + if err := pfile.Remove(); err != nil { + logrus.Error(err) + } + } + logrus.Fatalf("Shutting down due to ServeAPI error: %v", errAPI) + } + return nil +} + +// shutdownDaemon just wraps daemon.Shutdown() to handle a timeout in case +// d.Shutdown() is waiting too long to kill container or worst it's +// blocked there +func shutdownDaemon(d *daemon.Daemon, timeout time.Duration) { + ch := make(chan struct{}) + go func() { + d.Shutdown() + close(ch) + }() + select { + case <-ch: + logrus.Debug("Clean shutdown succeeded") + case <-time.After(timeout * time.Second): + logrus.Error("Force shutdown daemon") + } +} + +func loadDaemonCliConfig(config *daemon.Config, daemonFlags *flag.FlagSet, commonConfig *cli.CommonFlags, configFile string) (*daemon.Config, error) { + config.Debug = commonConfig.Debug + config.Hosts = commonConfig.Hosts + config.LogLevel = commonConfig.LogLevel + config.TLS = commonConfig.TLS + config.TLSVerify = commonConfig.TLSVerify + config.CommonTLSOptions = daemon.CommonTLSOptions{} + + if commonConfig.TLSOptions != nil { + config.CommonTLSOptions.CAFile = commonConfig.TLSOptions.CAFile + config.CommonTLSOptions.CertFile = commonConfig.TLSOptions.CertFile + config.CommonTLSOptions.KeyFile = commonConfig.TLSOptions.KeyFile + } + + if configFile != "" { + c, err := daemon.MergeDaemonConfigurations(config, daemonFlags, configFile) + if err != nil { + if daemonFlags.IsSet(daemonConfigFileFlag) || !os.IsNotExist(err) { + return nil, fmt.Errorf("unable to configure the Docker daemon with file %s: %v\n", configFile, err) + } + } + // the merged configuration can be nil if the config file didn't exist. + // leave the current configuration as it is if when that happens. + if c != nil { + config = c + } + } + + // Regardless of whether the user sets it to true or false, if they + // specify TLSVerify at all then we need to turn on TLS + if config.IsValueSet(tlsVerifyKey) { + config.TLS = true + } + + // ensure that the log level is the one set after merging configurations + setDaemonLogLevel(config.LogLevel) + + return config, nil +} + +func initRouter(s *apiserver.Server, d *daemon.Daemon) { + routers := []router.Router{ + container.NewRouter(d), + image.NewRouter(d), + systemrouter.NewRouter(d), + volume.NewRouter(d), + build.NewRouter(dockerfile.NewBuildManager(d)), + } + if d.NetworkControllerEnabled() { + routers = append(routers, network.NewRouter(d)) + } + + s.InitRouter(utils.IsDebugEnabled(), routers...) +} diff --git a/docker/daemon_freebsd.go b/docker/daemon_freebsd.go new file mode 100644 index 00000000..5ac4d4be --- /dev/null +++ b/docker/daemon_freebsd.go @@ -0,0 +1,7 @@ +// +build daemon + +package main + +// notifySystem sends a message to the host when the server is ready to be used +func notifySystem() { +} diff --git a/docker/daemon_linux.go b/docker/daemon_linux.go new file mode 100644 index 00000000..0a02128d --- /dev/null +++ b/docker/daemon_linux.go @@ -0,0 +1,13 @@ +// +build daemon + +package main + +import ( + systemdDaemon "github.com/coreos/go-systemd/daemon" +) + +// notifySystem sends a message to the host when the server is ready to be used +func notifySystem() { + // Tell the init daemon we are accepting requests + go systemdDaemon.SdNotify("READY=1") +} diff --git a/docker/daemon_none.go b/docker/daemon_none.go new file mode 100644 index 00000000..0ea18838 --- /dev/null +++ b/docker/daemon_none.go @@ -0,0 +1,13 @@ +// +build !daemon + +package main + +import "github.com/docker/docker/cli" + +const daemonUsage = "" + +var daemonCli cli.Handler + +// notifySystem sends a message to the host when the server is ready to be used +func notifySystem() { +} diff --git a/docker/daemon_test.go b/docker/daemon_test.go new file mode 100644 index 00000000..c568bdb1 --- /dev/null +++ b/docker/daemon_test.go @@ -0,0 +1,296 @@ +// +build daemon + +package main + +import ( + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/cli" + "github.com/docker/docker/daemon" + "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/mflag" + "github.com/docker/go-connections/tlsconfig" +) + +func TestLoadDaemonCliConfigWithoutOverriding(t *testing.T) { + c := &daemon.Config{} + common := &cli.CommonFlags{ + Debug: true, + } + + flags := mflag.NewFlagSet("test", mflag.ContinueOnError) + loadedConfig, err := loadDaemonCliConfig(c, flags, common, "/tmp/fooobarbaz") + if err != nil { + t.Fatal(err) + } + if loadedConfig == nil { + t.Fatalf("expected configuration %v, got nil", c) + } + if !loadedConfig.Debug { + t.Fatalf("expected debug to be copied from the common flags, got false") + } +} + +func TestLoadDaemonCliConfigWithTLS(t *testing.T) { + c := &daemon.Config{} + common := &cli.CommonFlags{ + TLS: true, + TLSOptions: &tlsconfig.Options{ + CAFile: "/tmp/ca.pem", + }, + } + + flags := mflag.NewFlagSet("test", mflag.ContinueOnError) + loadedConfig, err := loadDaemonCliConfig(c, flags, common, "/tmp/fooobarbaz") + if err != nil { + t.Fatal(err) + } + if loadedConfig == nil { + t.Fatalf("expected configuration %v, got nil", c) + } + if loadedConfig.CommonTLSOptions.CAFile != "/tmp/ca.pem" { + t.Fatalf("expected /tmp/ca.pem, got %s: %q", loadedConfig.CommonTLSOptions.CAFile, loadedConfig) + } +} + +func TestLoadDaemonCliConfigWithConflicts(t *testing.T) { + c := &daemon.Config{} + common := &cli.CommonFlags{} + f, err := ioutil.TempFile("", "docker-config-") + if err != nil { + t.Fatal(err) + } + configFile := f.Name() + defer os.Remove(configFile) + + f.Write([]byte(`{"labels": ["l3=foo"]}`)) + f.Close() + + var labels []string + + flags := mflag.NewFlagSet("test", mflag.ContinueOnError) + flags.String([]string{daemonConfigFileFlag}, "", "") + flags.Var(opts.NewNamedListOptsRef("labels", &labels, opts.ValidateLabel), []string{"-label"}, "") + + flags.Set(daemonConfigFileFlag, configFile) + if err := flags.Set("-label", "l1=bar"); err != nil { + t.Fatal(err) + } + if err := flags.Set("-label", "l2=baz"); err != nil { + t.Fatal(err) + } + + _, err = loadDaemonCliConfig(c, flags, common, configFile) + if err == nil { + t.Fatalf("expected configuration error, got nil") + } + if !strings.Contains(err.Error(), "labels") { + t.Fatalf("expected labels conflict, got %v", err) + } +} + +func TestLoadDaemonCliConfigWithTLSVerify(t *testing.T) { + c := &daemon.Config{} + common := &cli.CommonFlags{ + TLSOptions: &tlsconfig.Options{ + CAFile: "/tmp/ca.pem", + }, + } + + f, err := ioutil.TempFile("", "docker-config-") + if err != nil { + t.Fatal(err) + } + configFile := f.Name() + defer os.Remove(configFile) + + f.Write([]byte(`{"tlsverify": true}`)) + f.Close() + + flags := mflag.NewFlagSet("test", mflag.ContinueOnError) + flags.Bool([]string{"-tlsverify"}, false, "") + loadedConfig, err := loadDaemonCliConfig(c, flags, common, configFile) + if err != nil { + t.Fatal(err) + } + if loadedConfig == nil { + t.Fatalf("expected configuration %v, got nil", c) + } + + if !loadedConfig.TLS { + t.Fatalf("expected TLS enabled, got %q", loadedConfig) + } +} + +func TestLoadDaemonCliConfigWithExplicitTLSVerifyFalse(t *testing.T) { + c := &daemon.Config{} + common := &cli.CommonFlags{ + TLSOptions: &tlsconfig.Options{ + CAFile: "/tmp/ca.pem", + }, + } + + f, err := ioutil.TempFile("", "docker-config-") + if err != nil { + t.Fatal(err) + } + configFile := f.Name() + defer os.Remove(configFile) + + f.Write([]byte(`{"tlsverify": false}`)) + f.Close() + + flags := mflag.NewFlagSet("test", mflag.ContinueOnError) + flags.Bool([]string{"-tlsverify"}, false, "") + loadedConfig, err := loadDaemonCliConfig(c, flags, common, configFile) + if err != nil { + t.Fatal(err) + } + if loadedConfig == nil { + t.Fatalf("expected configuration %v, got nil", c) + } + + if !loadedConfig.TLS { + t.Fatalf("expected TLS enabled, got %q", loadedConfig) + } +} + +func TestLoadDaemonCliConfigWithoutTLSVerify(t *testing.T) { + c := &daemon.Config{} + common := &cli.CommonFlags{ + TLSOptions: &tlsconfig.Options{ + CAFile: "/tmp/ca.pem", + }, + } + + f, err := ioutil.TempFile("", "docker-config-") + if err != nil { + t.Fatal(err) + } + configFile := f.Name() + defer os.Remove(configFile) + + f.Write([]byte(`{}`)) + f.Close() + + flags := mflag.NewFlagSet("test", mflag.ContinueOnError) + loadedConfig, err := loadDaemonCliConfig(c, flags, common, configFile) + if err != nil { + t.Fatal(err) + } + if loadedConfig == nil { + t.Fatalf("expected configuration %v, got nil", c) + } + + if loadedConfig.TLS { + t.Fatalf("expected TLS disabled, got %q", loadedConfig) + } +} + +func TestLoadDaemonCliConfigWithLogLevel(t *testing.T) { + c := &daemon.Config{} + common := &cli.CommonFlags{} + + f, err := ioutil.TempFile("", "docker-config-") + if err != nil { + t.Fatal(err) + } + configFile := f.Name() + defer os.Remove(configFile) + + f.Write([]byte(`{"log-level": "warn"}`)) + f.Close() + + flags := mflag.NewFlagSet("test", mflag.ContinueOnError) + flags.String([]string{"-log-level"}, "", "") + loadedConfig, err := loadDaemonCliConfig(c, flags, common, configFile) + if err != nil { + t.Fatal(err) + } + if loadedConfig == nil { + t.Fatalf("expected configuration %v, got nil", c) + } + if loadedConfig.LogLevel != "warn" { + t.Fatalf("expected warn log level, got %v", loadedConfig.LogLevel) + } + + if logrus.GetLevel() != logrus.WarnLevel { + t.Fatalf("expected warn log level, got %v", logrus.GetLevel()) + } +} + +func TestLoadDaemonConfigWithEmbeddedOptions(t *testing.T) { + c := &daemon.Config{} + common := &cli.CommonFlags{} + + flags := mflag.NewFlagSet("test", mflag.ContinueOnError) + flags.String([]string{"-tlscacert"}, "", "") + flags.String([]string{"-log-driver"}, "", "") + + f, err := ioutil.TempFile("", "docker-config-") + if err != nil { + t.Fatal(err) + } + configFile := f.Name() + defer os.Remove(configFile) + + f.Write([]byte(`{"tlscacert": "/etc/certs/ca.pem", "log-driver": "syslog"}`)) + f.Close() + + loadedConfig, err := loadDaemonCliConfig(c, flags, common, configFile) + if err != nil { + t.Fatal(err) + } + if loadedConfig == nil { + t.Fatal("expected configuration, got nil") + } + if loadedConfig.CommonTLSOptions.CAFile != "/etc/certs/ca.pem" { + t.Fatalf("expected CA file path /etc/certs/ca.pem, got %v", loadedConfig.CommonTLSOptions.CAFile) + } + if loadedConfig.LogConfig.Type != "syslog" { + t.Fatalf("expected LogConfig type syslog, got %v", loadedConfig.LogConfig.Type) + } +} + +func TestLoadDaemonConfigWithRegistryOptions(t *testing.T) { + c := &daemon.Config{} + common := &cli.CommonFlags{} + flags := mflag.NewFlagSet("test", mflag.ContinueOnError) + c.ServiceOptions.InstallCliFlags(flags, absentFromHelp) + + f, err := ioutil.TempFile("", "docker-config-") + if err != nil { + t.Fatal(err) + } + configFile := f.Name() + defer os.Remove(configFile) + + f.Write([]byte(`{"registry-mirrors": ["https://mirrors.docker.com"], "insecure-registries": ["https://insecure.docker.com"], "disable-legacy-registry": true}`)) + f.Close() + + loadedConfig, err := loadDaemonCliConfig(c, flags, common, configFile) + if err != nil { + t.Fatal(err) + } + if loadedConfig == nil { + t.Fatal("expected configuration, got nil") + } + + m := loadedConfig.Mirrors + if len(m) != 1 { + t.Fatalf("expected 1 mirror, got %d", len(m)) + } + + r := loadedConfig.InsecureRegistries + if len(r) != 1 { + t.Fatalf("expected 1 insecure registries, got %d", len(r)) + } + + if !loadedConfig.V2Only { + t.Fatal("expected disable-legacy-registry to be true, got false") + } +} diff --git a/docker/daemon_unix.go b/docker/daemon_unix.go new file mode 100644 index 00000000..b65eb1f0 --- /dev/null +++ b/docker/daemon_unix.go @@ -0,0 +1,82 @@ +// +build daemon,!windows + +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/Sirupsen/logrus" + apiserver "github.com/docker/docker/api/server" + "github.com/docker/docker/daemon" + "github.com/docker/docker/libcontainerd" + "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/system" +) + +const defaultDaemonConfigFile = "/etc/docker/daemon.json" + +func setPlatformServerConfig(serverConfig *apiserver.Config, daemonCfg *daemon.Config) *apiserver.Config { + serverConfig.EnableCors = daemonCfg.EnableCors + serverConfig.CorsHeaders = daemonCfg.CorsHeaders + + return serverConfig +} + +// currentUserIsOwner checks whether the current user is the owner of the given +// file. +func currentUserIsOwner(f string) bool { + if fileInfo, err := system.Stat(f); err == nil && fileInfo != nil { + if int(fileInfo.UID()) == os.Getuid() { + return true + } + } + return false +} + +// setDefaultUmask sets the umask to 0022 to avoid problems +// caused by custom umask +func setDefaultUmask() error { + desiredUmask := 0022 + syscall.Umask(desiredUmask) + if umask := syscall.Umask(desiredUmask); umask != desiredUmask { + return fmt.Errorf("failed to set umask: expected %#o, got %#o", desiredUmask, umask) + } + + return nil +} + +func getDaemonConfDir() string { + return "/etc/docker" +} + +// setupConfigReloadTrap configures the USR2 signal to reload the configuration. +func setupConfigReloadTrap(configFile string, flags *mflag.FlagSet, reload func(*daemon.Config)) { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGHUP) + go func() { + for range c { + if err := daemon.ReloadConfiguration(configFile, flags, reload); err != nil { + logrus.Error(err) + } + } + }() +} + +func (cli *DaemonCli) getPlatformRemoteOptions() []libcontainerd.RemoteOption { + opts := []libcontainerd.RemoteOption{ + libcontainerd.WithDebugLog(cli.Config.Debug), + } + if cli.Config.ContainerdAddr != "" { + opts = append(opts, libcontainerd.WithRemoteAddr(cli.Config.ContainerdAddr)) + } else { + opts = append(opts, libcontainerd.WithStartDaemon(true)) + } + if daemon.UsingSystemd(cli.Config) { + args := []string{"--systemd-cgroup=true"} + opts = append(opts, libcontainerd.WithRuntimeArgs(args)) + } + return opts +} diff --git a/docker/daemon_unix_test.go b/docker/daemon_unix_test.go new file mode 100644 index 00000000..5b17b3af --- /dev/null +++ b/docker/daemon_unix_test.go @@ -0,0 +1,212 @@ +// +build daemon,!windows + +package main + +import ( + "io/ioutil" + "testing" + + "github.com/docker/docker/cli" + "github.com/docker/docker/daemon" + "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/mflag" +) + +func TestLoadDaemonCliConfigWithDaemonFlags(t *testing.T) { + c := &daemon.Config{} + common := &cli.CommonFlags{ + Debug: true, + LogLevel: "info", + } + + f, err := ioutil.TempFile("", "docker-config-") + if err != nil { + t.Fatal(err) + } + + configFile := f.Name() + f.Write([]byte(`{"log-opts": {"max-size": "1k"}}`)) + f.Close() + + flags := mflag.NewFlagSet("test", mflag.ContinueOnError) + flags.String([]string{daemonConfigFileFlag}, "", "") + flags.BoolVar(&c.EnableSelinuxSupport, []string{"-selinux-enabled"}, true, "") + flags.StringVar(&c.LogConfig.Type, []string{"-log-driver"}, "json-file", "") + flags.Var(opts.NewNamedMapOpts("log-opts", c.LogConfig.Config, nil), []string{"-log-opt"}, "") + flags.Set(daemonConfigFileFlag, configFile) + + loadedConfig, err := loadDaemonCliConfig(c, flags, common, configFile) + if err != nil { + t.Fatal(err) + } + if loadedConfig == nil { + t.Fatalf("expected configuration %v, got nil", c) + } + if !loadedConfig.Debug { + t.Fatalf("expected debug mode, got false") + } + if loadedConfig.LogLevel != "info" { + t.Fatalf("expected info log level, got %v", loadedConfig.LogLevel) + } + if !loadedConfig.EnableSelinuxSupport { + t.Fatalf("expected enabled selinux support, got disabled") + } + if loadedConfig.LogConfig.Type != "json-file" { + t.Fatalf("expected LogConfig type json-file, got %v", loadedConfig.LogConfig.Type) + } + if maxSize := loadedConfig.LogConfig.Config["max-size"]; maxSize != "1k" { + t.Fatalf("expected log max-size `1k`, got %s", maxSize) + } +} + +func TestLoadDaemonConfigWithNetwork(t *testing.T) { + c := &daemon.Config{} + common := &cli.CommonFlags{} + flags := mflag.NewFlagSet("test", mflag.ContinueOnError) + flags.String([]string{"-bip"}, "", "") + flags.String([]string{"-ip"}, "", "") + + f, err := ioutil.TempFile("", "docker-config-") + if err != nil { + t.Fatal(err) + } + + configFile := f.Name() + f.Write([]byte(`{"bip": "127.0.0.2", "ip": "127.0.0.1"}`)) + f.Close() + + loadedConfig, err := loadDaemonCliConfig(c, flags, common, configFile) + if err != nil { + t.Fatal(err) + } + if loadedConfig == nil { + t.Fatalf("expected configuration %v, got nil", c) + } + if loadedConfig.IP != "127.0.0.2" { + t.Fatalf("expected IP 127.0.0.2, got %v", loadedConfig.IP) + } + if loadedConfig.DefaultIP.String() != "127.0.0.1" { + t.Fatalf("expected DefaultIP 127.0.0.1, got %s", loadedConfig.DefaultIP) + } +} + +func TestLoadDaemonConfigWithMapOptions(t *testing.T) { + c := &daemon.Config{} + common := &cli.CommonFlags{} + flags := mflag.NewFlagSet("test", mflag.ContinueOnError) + + flags.Var(opts.NewNamedMapOpts("cluster-store-opts", c.ClusterOpts, nil), []string{"-cluster-store-opt"}, "") + flags.Var(opts.NewNamedMapOpts("log-opts", c.LogConfig.Config, nil), []string{"-log-opt"}, "") + + f, err := ioutil.TempFile("", "docker-config-") + if err != nil { + t.Fatal(err) + } + + configFile := f.Name() + f.Write([]byte(`{ + "cluster-store-opts": {"kv.cacertfile": "/var/lib/docker/discovery_certs/ca.pem"}, + "log-opts": {"tag": "test"} +}`)) + f.Close() + + loadedConfig, err := loadDaemonCliConfig(c, flags, common, configFile) + if err != nil { + t.Fatal(err) + } + if loadedConfig == nil { + t.Fatal("expected configuration, got nil") + } + if loadedConfig.ClusterOpts == nil { + t.Fatal("expected cluster options, got nil") + } + + expectedPath := "/var/lib/docker/discovery_certs/ca.pem" + if caPath := loadedConfig.ClusterOpts["kv.cacertfile"]; caPath != expectedPath { + t.Fatalf("expected %s, got %s", expectedPath, caPath) + } + + if loadedConfig.LogConfig.Config == nil { + t.Fatal("expected log config options, got nil") + } + if tag := loadedConfig.LogConfig.Config["tag"]; tag != "test" { + t.Fatalf("expected log tag `test`, got %s", tag) + } +} + +func TestLoadDaemonConfigWithTrueDefaultValues(t *testing.T) { + c := &daemon.Config{} + common := &cli.CommonFlags{} + flags := mflag.NewFlagSet("test", mflag.ContinueOnError) + flags.BoolVar(&c.EnableUserlandProxy, []string{"-userland-proxy"}, true, "") + + f, err := ioutil.TempFile("", "docker-config-") + if err != nil { + t.Fatal(err) + } + + if err := flags.ParseFlags([]string{}, false); err != nil { + t.Fatal(err) + } + + configFile := f.Name() + f.Write([]byte(`{ + "userland-proxy": false +}`)) + f.Close() + + loadedConfig, err := loadDaemonCliConfig(c, flags, common, configFile) + if err != nil { + t.Fatal(err) + } + if loadedConfig == nil { + t.Fatal("expected configuration, got nil") + } + + if loadedConfig.EnableUserlandProxy { + t.Fatal("expected userland proxy to be disabled, got enabled") + } + + // make sure reloading doesn't generate configuration + // conflicts after normalizing boolean values. + err = daemon.ReloadConfiguration(configFile, flags, func(reloadedConfig *daemon.Config) { + if reloadedConfig.EnableUserlandProxy { + t.Fatal("expected userland proxy to be disabled, got enabled") + } + }) + if err != nil { + t.Fatal(err) + } +} + +func TestLoadDaemonConfigWithTrueDefaultValuesLeaveDefaults(t *testing.T) { + c := &daemon.Config{} + common := &cli.CommonFlags{} + flags := mflag.NewFlagSet("test", mflag.ContinueOnError) + flags.BoolVar(&c.EnableUserlandProxy, []string{"-userland-proxy"}, true, "") + + f, err := ioutil.TempFile("", "docker-config-") + if err != nil { + t.Fatal(err) + } + + if err := flags.ParseFlags([]string{}, false); err != nil { + t.Fatal(err) + } + + configFile := f.Name() + f.Write([]byte(`{}`)) + f.Close() + + loadedConfig, err := loadDaemonCliConfig(c, flags, common, configFile) + if err != nil { + t.Fatal(err) + } + if loadedConfig == nil { + t.Fatal("expected configuration, got nil") + } + + if !loadedConfig.EnableUserlandProxy { + t.Fatal("expected userland proxy to be enabled, got disabled") + } +} diff --git a/docker/daemon_windows.go b/docker/daemon_windows.go new file mode 100644 index 00000000..ae8d737d --- /dev/null +++ b/docker/daemon_windows.go @@ -0,0 +1,64 @@ +// +build daemon + +package main + +import ( + "fmt" + "os" + "syscall" + + "github.com/Sirupsen/logrus" + apiserver "github.com/docker/docker/api/server" + "github.com/docker/docker/daemon" + "github.com/docker/docker/libcontainerd" + "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/system" +) + +var defaultDaemonConfigFile = os.Getenv("programdata") + string(os.PathSeparator) + "docker" + string(os.PathSeparator) + "config" + string(os.PathSeparator) + "daemon.json" + +func setPlatformServerConfig(serverConfig *apiserver.Config, daemonCfg *daemon.Config) *apiserver.Config { + return serverConfig +} + +// currentUserIsOwner checks whether the current user is the owner of the given +// file. +func currentUserIsOwner(f string) bool { + return false +} + +// setDefaultUmask doesn't do anything on windows +func setDefaultUmask() error { + return nil +} + +func getDaemonConfDir() string { + return os.Getenv("PROGRAMDATA") + `\docker\config` +} + +// notifySystem sends a message to the host when the server is ready to be used +func notifySystem() { +} + +// setupConfigReloadTrap configures a Win32 event to reload the configuration. +func setupConfigReloadTrap(configFile string, flags *mflag.FlagSet, reload func(*daemon.Config)) { + go func() { + sa := syscall.SecurityAttributes{ + Length: 0, + } + ev := "Global\\docker-daemon-config-" + fmt.Sprint(os.Getpid()) + if h, _ := system.CreateEvent(&sa, false, false, ev); h != 0 { + logrus.Debugf("Config reload - waiting signal at %s", ev) + for { + syscall.WaitForSingleObject(h, syscall.INFINITE) + if err := daemon.ReloadConfiguration(configFile, flags, reload); err != nil { + logrus.Error(err) + } + } + } + }() +} + +func (cli *DaemonCli) getPlatformRemoteOptions() []libcontainerd.RemoteOption { + return nil +} diff --git a/docker/docker.go b/docker/docker.go new file mode 100644 index 00000000..39b527ee --- /dev/null +++ b/docker/docker.go @@ -0,0 +1,82 @@ +package main + +import ( + "fmt" + "os" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/client" + "github.com/docker/docker/cli" + "github.com/docker/docker/dockerversion" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/reexec" + "github.com/docker/docker/pkg/term" + "github.com/docker/docker/utils" +) + +func main() { + if reexec.Init() { + return + } + + // Set terminal emulation based on platform as required. + stdin, stdout, stderr := term.StdStreams() + + logrus.SetOutput(stderr) + + flag.Merge(flag.CommandLine, clientFlags.FlagSet, commonFlags.FlagSet) + + flag.Usage = func() { + fmt.Fprint(stdout, "Usage: docker [OPTIONS] COMMAND [arg...]\n"+daemonUsage+" docker [ --help | -v | --version ]\n\n") + fmt.Fprint(stdout, "A self-sufficient runtime for containers.\n\nOptions:\n") + + flag.CommandLine.SetOutput(stdout) + flag.PrintDefaults() + + help := "\nCommands:\n" + + for _, cmd := range dockerCommands { + help += fmt.Sprintf(" %-10.10s%s\n", cmd.Name, cmd.Description) + } + + help += "\nRun 'docker COMMAND --help' for more information on a command." + fmt.Fprintf(stdout, "%s\n", help) + } + + flag.Parse() + + if *flVersion { + showVersion() + return + } + + if *flHelp { + // if global flag --help is present, regardless of what other options and commands there are, + // just print the usage. + flag.Usage() + return + } + + clientCli := client.NewDockerCli(stdin, stdout, stderr, clientFlags) + + c := cli.New(clientCli, daemonCli) + if err := c.Run(flag.Args()...); err != nil { + if sterr, ok := err.(cli.StatusError); ok { + if sterr.Status != "" { + fmt.Fprintln(stderr, sterr.Status) + os.Exit(1) + } + os.Exit(sterr.StatusCode) + } + fmt.Fprintln(stderr, err) + os.Exit(1) + } +} + +func showVersion() { + if utils.ExperimentalBuild() { + fmt.Printf("Docker version %s, build %s, experimental\n", dockerversion.Version, dockerversion.GitCommit) + } else { + fmt.Printf("Docker version %s, build %s\n", dockerversion.Version, dockerversion.GitCommit) + } +} diff --git a/docker/docker_windows.go b/docker/docker_windows.go new file mode 100644 index 00000000..a31dffc9 --- /dev/null +++ b/docker/docker_windows.go @@ -0,0 +1,5 @@ +package main + +import ( + _ "github.com/docker/docker/autogen/winresources" +) diff --git a/docker/flags.go b/docker/flags.go new file mode 100644 index 00000000..35a81088 --- /dev/null +++ b/docker/flags.go @@ -0,0 +1,30 @@ +package main + +import ( + "sort" + + "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" +) + +var ( + flHelp = flag.Bool([]string{"h", "-help"}, false, "Print usage") + flVersion = flag.Bool([]string{"v", "-version"}, false, "Print version information and quit") +) + +type byName []cli.Command + +func (a byName) Len() int { return len(a) } +func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a byName) Less(i, j int) bool { return a[i].Name < a[j].Name } + +var dockerCommands []cli.Command + +// TODO(tiborvass): do not show 'daemon' on client-only binaries + +func init() { + for _, cmd := range cli.DockerCommands { + dockerCommands = append(dockerCommands, cmd) + } + sort.Sort(byName(dockerCommands)) +} diff --git a/docker/flags_test.go b/docker/flags_test.go new file mode 100644 index 00000000..28021ba4 --- /dev/null +++ b/docker/flags_test.go @@ -0,0 +1,13 @@ +package main + +import ( + "sort" + "testing" +) + +// Tests if the subcommands of docker are sorted +func TestDockerSubcommandsAreSorted(t *testing.T) { + if !sort.IsSorted(byName(dockerCommands)) { + t.Fatal("Docker subcommands are not in sorted order") + } +} diff --git a/docker/listeners/listeners.go b/docker/listeners/listeners.go new file mode 100644 index 00000000..8150ba0c --- /dev/null +++ b/docker/listeners/listeners.go @@ -0,0 +1,22 @@ +package listeners + +import ( + "crypto/tls" + "net" + + "github.com/Sirupsen/logrus" + "github.com/docker/go-connections/sockets" +) + +func initTCPSocket(addr string, tlsConfig *tls.Config) (l net.Listener, err error) { + if tlsConfig == nil || tlsConfig.ClientAuth != tls.RequireAndVerifyClientCert { + logrus.Warn("/!\\ DON'T BIND ON ANY IP ADDRESS WITHOUT setting -tlsverify IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\") + } + if l, err = sockets.NewTCPSocket(addr, tlsConfig); err != nil { + return nil, err + } + if err := allocateDaemonPort(addr); err != nil { + return nil, err + } + return +} diff --git a/docker/listeners/listeners_unix.go b/docker/listeners/listeners_unix.go new file mode 100644 index 00000000..732565a3 --- /dev/null +++ b/docker/listeners/listeners_unix.go @@ -0,0 +1,119 @@ +// +build !windows + +package listeners + +import ( + "crypto/tls" + "fmt" + "net" + "strconv" + + "github.com/Sirupsen/logrus" + "github.com/coreos/go-systemd/activation" + "github.com/docker/go-connections/sockets" + "github.com/docker/libnetwork/portallocator" +) + +// Init creates new listeners for the server. +func Init(proto, addr, socketGroup string, tlsConfig *tls.Config) (ls []net.Listener, err error) { + switch proto { + case "fd": + ls, err = listenFD(addr, tlsConfig) + if err != nil { + return nil, err + } + case "tcp": + l, err := initTCPSocket(addr, tlsConfig) + if err != nil { + return nil, err + } + ls = append(ls, l) + case "unix": + l, err := sockets.NewUnixSocket(addr, socketGroup) + if err != nil { + return nil, fmt.Errorf("can't create unix socket %s: %v", addr, err) + } + ls = append(ls, l) + default: + return nil, fmt.Errorf("Invalid protocol format: %q", proto) + } + + return +} + +// listenFD returns the specified socket activated files as a slice of +// net.Listeners or all of the activated files if "*" is given. +func listenFD(addr string, tlsConfig *tls.Config) ([]net.Listener, error) { + var ( + err error + listeners []net.Listener + ) + // socket activation + if tlsConfig != nil { + listeners, err = activation.TLSListeners(false, tlsConfig) + } else { + listeners, err = activation.Listeners(false) + } + if err != nil { + return nil, err + } + + if len(listeners) == 0 { + return nil, fmt.Errorf("No sockets found. Make sure the docker daemon was started by systemd.") + } + + // default to all fds just like unix:// and tcp:// + if addr == "" || addr == "*" { + return listeners, nil + } + + fdNum, err := strconv.Atoi(addr) + if err != nil { + return nil, fmt.Errorf("failed to parse systemd address, should be number: %v", err) + } + fdOffset := fdNum - 3 + if len(listeners) < int(fdOffset)+1 { + return nil, fmt.Errorf("Too few socket activated files passed in") + } + if listeners[fdOffset] == nil { + return nil, fmt.Errorf("failed to listen on systemd activated file at fd %d", fdOffset+3) + } + for i, ls := range listeners { + if i == fdOffset || ls == nil { + continue + } + if err := ls.Close(); err != nil { + logrus.Errorf("Failed to close systemd activated file at fd %d: %v", fdOffset+3, err) + } + } + return []net.Listener{listeners[fdOffset]}, nil +} + +// allocateDaemonPort ensures that there are no containers +// that try to use any port allocated for the docker server. +func allocateDaemonPort(addr string) error { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return err + } + + intPort, err := strconv.Atoi(port) + if err != nil { + return err + } + + var hostIPs []net.IP + if parsedIP := net.ParseIP(host); parsedIP != nil { + hostIPs = append(hostIPs, parsedIP) + } else if hostIPs, err = net.LookupIP(host); err != nil { + return fmt.Errorf("failed to lookup %s address in host specification", host) + } + + pa := portallocator.Get() + for _, hostIP := range hostIPs { + if _, err := pa.RequestPort(hostIP, "tcp", intPort); err != nil { + return fmt.Errorf("failed to allocate daemon listening port %d (err: %v)", intPort, err) + } + } + return nil +} diff --git a/docker/listeners/listeners_windows.go b/docker/listeners/listeners_windows.go new file mode 100644 index 00000000..ae862fcf --- /dev/null +++ b/docker/listeners/listeners_windows.go @@ -0,0 +1,58 @@ +package listeners + +import ( + "crypto/tls" + "errors" + "fmt" + "net" + "strings" + + "github.com/Microsoft/go-winio" +) + +// Init creates new listeners for the server. +func Init(proto, addr, socketGroup string, tlsConfig *tls.Config) (ls []net.Listener, err error) { + switch proto { + case "tcp": + l, err := initTCPSocket(addr, tlsConfig) + if err != nil { + return nil, err + } + ls = append(ls, l) + + case "npipe": + // allow Administrators and SYSTEM, plus whatever additional users or groups were specified + sddl := "D:P(A;;GA;;;BA)(A;;GA;;;SY)" + if socketGroup != "" { + for _, g := range strings.Split(socketGroup, ",") { + sid, err := winio.LookupSidByName(g) + if err != nil { + return nil, err + } + sddl += fmt.Sprintf("(A;;GRGW;;;%s)", sid) + } + } + c := winio.PipeConfig{ + SecurityDescriptor: sddl, + MessageMode: true, // Use message mode so that CloseWrite() is supported + InputBufferSize: 65536, // Use 64KB buffers to improve performance + OutputBufferSize: 65536, + } + l, err := winio.ListenPipe(addr, &c) + if err != nil { + return nil, err + } + ls = append(ls, l) + + default: + return nil, errors.New("Invalid protocol format. Windows only supports tcp and npipe.") + } + + return +} + +// allocateDaemonPort ensures that there are no containers +// that try to use any port allocated for the docker server. +func allocateDaemonPort(addr string) error { + return nil +} diff --git a/dockerversion/useragent.go b/dockerversion/useragent.go new file mode 100644 index 00000000..d2a891c4 --- /dev/null +++ b/dockerversion/useragent.go @@ -0,0 +1,74 @@ +package dockerversion + +import ( + "fmt" + "runtime" + + "github.com/docker/docker/api/server/httputils" + "github.com/docker/docker/pkg/parsers/kernel" + "github.com/docker/docker/pkg/useragent" + "golang.org/x/net/context" +) + +// DockerUserAgent is the User-Agent the Docker client uses to identify itself. +// In accordance with RFC 7231 (5.5.3) is of the form: +// [docker client's UA] UpstreamClient([upstream client's UA]) +func DockerUserAgent(ctx context.Context) string { + httpVersion := make([]useragent.VersionInfo, 0, 6) + httpVersion = append(httpVersion, useragent.VersionInfo{Name: "docker", Version: Version}) + httpVersion = append(httpVersion, useragent.VersionInfo{Name: "go", Version: runtime.Version()}) + httpVersion = append(httpVersion, useragent.VersionInfo{Name: "git-commit", Version: GitCommit}) + if kernelVersion, err := kernel.GetKernelVersion(); err == nil { + httpVersion = append(httpVersion, useragent.VersionInfo{Name: "kernel", Version: kernelVersion.String()}) + } + httpVersion = append(httpVersion, useragent.VersionInfo{Name: "os", Version: runtime.GOOS}) + httpVersion = append(httpVersion, useragent.VersionInfo{Name: "arch", Version: runtime.GOARCH}) + + dockerUA := useragent.AppendVersions("", httpVersion...) + upstreamUA := getUserAgentFromContext(ctx) + if len(upstreamUA) > 0 { + ret := insertUpstreamUserAgent(upstreamUA, dockerUA) + return ret + } + return dockerUA +} + +// getUserAgentFromContext returns the previously saved user-agent context stored in ctx, if one exists +func getUserAgentFromContext(ctx context.Context) string { + var upstreamUA string + if ctx != nil { + var ki interface{} = ctx.Value(httputils.UAStringKey) + if ki != nil { + upstreamUA = ctx.Value(httputils.UAStringKey).(string) + } + } + return upstreamUA +} + +// escapeStr returns s with every rune in charsToEscape escaped by a backslash +func escapeStr(s string, charsToEscape string) string { + var ret string + for _, currRune := range s { + appended := false + for _, escapeableRune := range charsToEscape { + if currRune == escapeableRune { + ret += `\` + string(currRune) + appended = true + break + } + } + if !appended { + ret += string(currRune) + } + } + return ret +} + +// insertUpstreamUserAgent adds the upstream client useragent to create a user-agent +// string of the form: +// $dockerUA UpstreamClient($upstreamUA) +func insertUpstreamUserAgent(upstreamUA string, dockerUA string) string { + charsToEscape := `();\` + upstreamUAEscaped := escapeStr(upstreamUA, charsToEscape) + return fmt.Sprintf("%s UpstreamClient(%s)", dockerUA, upstreamUAEscaped) +} diff --git a/dockerversion/version_lib.go b/dockerversion/version_lib.go new file mode 100644 index 00000000..6644bce2 --- /dev/null +++ b/dockerversion/version_lib.go @@ -0,0 +1,13 @@ +// +build !autogen + +// Package dockerversion is auto-generated at build-time +package dockerversion + +// Default build-time variable for library-import. +// This file is overridden on build with build-time informations. +const ( + GitCommit string = "library-import" + Version string = "library-import" + BuildTime string = "library-import" + IAmStatic string = "library-import" +) diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..9ad7c6e2 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +# avoid committing the awsconfig file used for releases +awsconfig diff --git a/docs/Dockerfile b/docs/Dockerfile new file mode 100644 index 00000000..3690d157 --- /dev/null +++ b/docs/Dockerfile @@ -0,0 +1,17 @@ +FROM docs/base:latest +MAINTAINER Mary Anthony (@moxiegirl) + +RUN svn checkout https://github.com/docker/compose/trunk/docs /docs/content/compose +RUN svn checkout https://github.com/docker/swarm/trunk/docs /docs/content/swarm +RUN svn checkout https://github.com/docker/machine/trunk/docs /docs/content/machine +RUN svn checkout https://github.com/docker/distribution/trunk/docs /docs/content/registry +RUN svn checkout https://github.com/docker/notary/trunk/docs /docs/content/notary +RUN svn checkout https://github.com/docker/kitematic/trunk/docs /docs/content/kitematic +RUN svn checkout https://github.com/docker/toolbox/trunk/docs /docs/content/toolbox +RUN svn checkout https://github.com/docker/opensource/trunk/docs /docs/content/opensource + +ENV PROJECT=engine +# To get the git info for this repo +COPY . /src + +COPY . /docs/content/$PROJECT/ diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..711462ea --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,54 @@ +.PHONY: all binary build cross default docs docs-build docs-shell shell test test-unit test-integration test-integration-cli test-docker-py validate + +# env vars passed through directly to Docker's build scripts +# to allow things like `make DOCKER_CLIENTONLY=1 binary` easily +# `docs/sources/contributing/devenvironment.md ` and `project/PACKAGERS.md` have some limited documentation of some of these +DOCKER_ENVS := \ + -e BUILDFLAGS \ + -e DOCKER_CLIENTONLY \ + -e DOCKER_GRAPHDRIVER \ + -e TESTDIRS \ + -e TESTFLAGS \ + -e TIMEOUT +# note: we _cannot_ add "-e DOCKER_BUILDTAGS" here because even if it's unset in the shell, that would shadow the "ENV DOCKER_BUILDTAGS" set in our Dockerfile, which is very important for our official builds + +# to allow `make DOCSDIR=docs docs-shell` (to create a bind mount in docs) +DOCS_MOUNT := $(if $(DOCSDIR),-v $(CURDIR)/$(DOCSDIR):/$(DOCSDIR)) + +# to allow `make DOCSPORT=9000 docs` +DOCSPORT := 8000 + +# Get the IP ADDRESS +DOCKER_IP=$(shell python -c "import urlparse ; print urlparse.urlparse('$(DOCKER_HOST)').hostname or ''") +HUGO_BASE_URL=$(shell test -z "$(DOCKER_IP)" && echo localhost || echo "$(DOCKER_IP)") +HUGO_BIND_IP=0.0.0.0 + +GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null) +DOCKER_IMAGE := docker$(if $(GIT_BRANCH),:$(GIT_BRANCH)) +DOCKER_DOCS_IMAGE := docs-base$(if $(GIT_BRANCH),:$(GIT_BRANCH)) + + +DOCKER_RUN_DOCS := docker run --rm -it $(DOCS_MOUNT) -e AWS_S3_BUCKET -e NOCACHE + +# for some docs workarounds (see below in "docs-build" target) +GITCOMMIT := $(shell git rev-parse --short HEAD 2>/dev/null) + +default: docs + +docs: docs-build + $(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" hugo server --port=$(DOCSPORT) --baseUrl=$(HUGO_BASE_URL) --bind=$(HUGO_BIND_IP) + +docs-draft: docs-build + $(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" hugo server --buildDrafts="true" --port=$(DOCSPORT) --baseUrl=$(HUGO_BASE_URL) --bind=$(HUGO_BIND_IP) + + +docs-shell: docs-build + $(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 "$(DOCKER_DOCS_IMAGE)" bash + + +docs-build: +# ( git remote | grep -v upstream ) || git diff --name-status upstream/release..upstream/docs ./ > ./changed-files +# echo "$(GIT_BRANCH)" > GIT_BRANCH +# echo "$(AWS_S3_BUCKET)" > AWS_S3_BUCKET +# echo "$(GITCOMMIT)" > GITCOMMIT + docker build -t "$(DOCKER_DOCS_IMAGE)" . diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..f57f4526 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,288 @@ + + +# Docker Documentation + +The source for Docker documentation is in this directory. Our +documentation uses extended Markdown, as implemented by +[MkDocs](http://mkdocs.org). The current release of the Docker documentation +resides on [https://docs.docker.com](https://docs.docker.com). + +## Understanding the documentation branches and processes + +Docker has two primary branches for documentation: + +| Branch | Description | URL (published via commit-hook) | +|----------|--------------------------------|------------------------------------------------------------------------------| +| `docs` | Official release documentation | [https://docs.docker.com](https://docs.docker.com) | +| `master` | Merged but unreleased development work | | + +Additions and updates to upcoming releases are made in a feature branch off of +the `master` branch. The Docker maintainers also support a `docs` branch that +contains the last release of documentation. + +After a release, documentation updates are continually merged into `master` as +they occur. This work includes new documentation for forthcoming features, bug +fixes, and other updates. + +Periodically, the Docker maintainers update `docs.docker.com` between official +releases of Docker. They do this by cherry-picking commits from `master`, +merging them into `docs`, and then publishing the result. + +In the rare case where a change is not forward-compatible, changes may be made +on other branches by special arrangement with the Docker maintainers. + +### Quickstart for documentation contributors + +If you are a new or beginner contributor, we encourage you to read through the +[our detailed contributors +guide](https://docs.docker.com/opensource/code/). The guide explains in +detail, with examples, how to contribute. If you are an experienced contributor +this quickstart should be enough to get you started. + +The following is the essential workflow for contributing to the documentation: + +1. Fork the `docker/docker` repository. + +2. Clone the repository to your local machine. + +3. Select an issue from `docker/docker` to work on or submit a proposal of your +own. + +4. Create a feature branch from `master` in which to work. + + By basing from `master` your work is automatically included in the next + release. It also allows docs maintainers to easily cherry-pick your changes + into the `docs` release branch. + +4. Modify existing or add new `.md` files to the `docs` directory. + +5. As you work, build the documentation site locally to see your changes. + + The `docker/docker` repository contains a `Dockerfile` and a `Makefile`. + Together, these create a development environment in which you can build and + run a container running the Docker documentation website. To build the + documentation site, enter `make docs` in the `docs` directory of your `docker/docker` fork: + + $ make docs + .... (lots of output) .... + docker run --rm -it -e AWS_S3_BUCKET -p 8000:8000 "docker-docs:master" mkdocs serve + Running at: http://0.0.0.0:8000/ + Live reload enabled. + Hold ctrl+c to quit. + + + The build creates an image containing all the required tools, adds the local + `docs/` directory and generates the HTML files. Then, it runs a Docker + container with this image. + + The container exposes port 8000 on the localhost so that you can connect and + see your changes. If you use Docker Machine, the `docker-machine ip + ` command gives you the address of your server. + +6. Check your writing for style and mechanical errors. + + Use our [documentation style + guide](https://docs.docker.com/opensource/doc-style/) to check style. There are + several [good grammar and spelling online + checkers](http://www.hemingwayapp.com/) that can check your writing + mechanics. + +7. Squash your commits on your branch. + +8. Make a pull request from your fork back to Docker's `master` branch. + +9. Work with the reviewers until your change is approved and merged. + +### Debugging and testing + +If you have any issues you need to debug, you can use `make docs-shell` and then +run `mkdocs serve`. You can use `make docs-test` to generate a report of missing +links that are referenced in the documentation—there should be none. + +## Style guide + +If you have questions about how to write for Docker's documentation, please see +the [style guide](https://docs.docker.com/opensource/doc-style/). The style guide provides +guidance about grammar, syntax, formatting, styling, language, or tone. If +something isn't clear in the guide, please submit an issue to let us know or +submit a pull request to help us improve it. + + +## Publishing documentation (for Docker maintainers) + +To publish Docker's documentation you need to have Docker up and running on your +machine. You'll also need a `docs/awsconfig` file containing the settings you +need to access the AWS bucket you'll be deploying to. + +The process for publishing is to build first to an AWS bucket, verify the build, +and then publish the final release. + +1. Have Docker installed and running on your machine. + +2. Ask the core maintainers for the `awsconfig` file. + +3. Copy the `awsconfig` file to the `docs/` directory. + + The `awsconfig` file contains the profiles of the S3 buckets for our + documentation sites. (If needed, the release script creates an S3 bucket and + pushes the files to it.) Each profile has this format: + + [profile dowideit-docs] + aws_access_key_id = IHOIUAHSIDH234rwf.... + aws_secret_access_key = OIUYSADJHLKUHQWIUHE...... + region = ap-southeast-2 + + The `profile` name must be the same as the name of the bucket you are + deploying to. + +4. Call the `make` from the `docker` directory. + + $ make AWS_S3_BUCKET=dowideit-docs docs-release + + This publishes _only_ to the `http://bucket-url/v1.2/` version of the + documentation. + +5. If you're publishing the current release's documentation, you need to also +update the root docs pages by running + + $ make AWS_S3_BUCKET=dowideit-docs BUILD_ROOT=yes docs-release + +### Errors publishing using a Docker Machine VM + +Sometimes, in a Windows or Mac environment, the publishing procedure returns this +error: + + Post http:///var/run/docker.sock/build?rm=1&t=docker-docs%3Apost-1.2.0-docs_update-2: + dial unix /var/run/docker.sock: no such file or directory. + +If this happens, set the Docker host. Run the following command to get the +variables in your shell: + + docker-machine env + +Then, set your environment accordingly. + +## Cherry-picking documentation changes to update an existing release. + +Whenever the core team makes a release, they publish the documentation based on +the `release` branch. At that time, the `release` branch is copied into the +`docs` branch. The documentation team makes updates between Docker releases by +cherry-picking changes from `master` into any of the documentation branches. +Typically, we cherry-pick into the `docs` branch. + +For example, to update the current release's docs, do the following: + +1. Go to your `docker/docker` fork and get the latest from master. + + $ git fetch upstream + +2. Checkout a new branch based on `upstream/docs`. + + You should give your new branch a descriptive name. + + $ git checkout -b post-1.2.0-docs-update-1 upstream/docs + +3. In a browser window, open [https://github.com/docker/docker/commits/master]. + +4. Locate the merges you want to publish. + + You should only cherry-pick individual commits; do not cherry-pick merge + commits. To minimize merge conflicts, start with the oldest commit and work + your way forward in time. + +5. Copy the commit SHA from GitHub. + +6. Cherry-pick the commit. + + $ git cherry-pick -x fe845c4 + +7. Repeat until you have cherry-picked everything you want to merge. + +8. Push your changes to your fork. + + $ git push origin post-1.2.0-docs-update-1 + +9. Make a pull request to merge into the `docs` branch. + + Do __NOT__ merge into `master`. + +10. Have maintainers review your pull request. + +11. Once the PR has the needed "LGTMs", merge it on GitHub. + +12. Return to your local fork and make sure you are still on the `docs` branch. + + $ git checkout docs + +13. Fetch your merged pull request from `docs`. + + $ git fetch upstream/docs + +14. Ensure your branch is clean and set to the latest. + + $ git reset --hard upstream/docs + +15. Copy the `awsconfig` file into the `docs` directory. + +16. Make the beta documentation + + $ make AWS_S3_BUCKET=beta-docs.docker.io BUILD_ROOT=yes docs-release + +17. Open [the beta +website](http://beta-docs.docker.io.s3-website-us-west-2.amazonaws.com/) site +and make sure what you published is correct. + +19. When you're happy with your content, publish the docs to our live site: + + $ make AWS_S3_BUCKET=docs.docker.com BUILD_ROOT=yes +DISTRIBUTION_ID=C2K6......FL2F docs-release + +20. Test the uncached version of the live docs at [http://docs.docker.com.s3-website-us-east-1.amazonaws.com/] + + +### Caching and the docs + +New docs do not appear live on the site until the cache (a complex, distributed +CDN system) is flushed. The `make docs-release` command flushes the cache _if_ +the `DISTRIBUTION_ID` is set to the Cloudfront distribution ID. The cache flush +can take at least 15 minutes to run and you can check its progress with the CDN +Cloudfront Purge Tool Chrome app. + +## Removing files from the docs.docker.com site + +Sometimes it becomes necessary to remove files from the historical published documentation. +The most reliable way to do this is to do it directly using `aws s3` commands running in a +docs container: + +Start the docs container like `make docs-shell`, but bind mount in your `awsconfig`: + +``` +docker run --rm -it -v $(CURDIR)/docs/awsconfig:/docs/awsconfig docker-docs:master bash +``` + +and then the following example shows deleting 2 documents from s3, and then requesting the +CloudFlare cache to invalidate them: + + +``` +export BUCKET BUCKET=docs.docker.com +export AWS_CONFIG_FILE=$(pwd)/awsconfig +aws s3 --profile $BUCKET ls s3://$BUCKET +aws s3 --profile $BUCKET rm s3://$BUCKET/v1.0/reference/api/docker_io_oauth_api/index.html +aws s3 --profile $BUCKET rm s3://$BUCKET/v1.1/reference/api/docker_io_oauth_api/index.html + +aws configure set preview.cloudfront true +export DISTRIBUTION_ID=YUTIYUTIUTIUYTIUT +aws cloudfront create-invalidation --profile docs.docker.com --distribution-id $DISTRIBUTION_ID --invalidation-batch '{"Paths":{"Quantity":1, "Items":["/v1.0/reference/api/docker_io_oauth_api/"]},"CallerReference":"6Mar2015sventest1"}' +aws cloudfront create-invalidation --profile docs.docker.com --distribution-id $DISTRIBUTION_ID --invalidation-batch '{"Paths":{"Quantity":1, "Items":["/v1.1/reference/api/docker_io_oauth_api/"]},"CallerReference":"6Mar2015sventest1"}' +``` + +### Generate the man pages + +For information on generating man pages (short for manual page), see the README.md +document in [the man page directory](https://github.com/docker/docker/tree/master/docker) +in this project. diff --git a/docs/admin/ambassador_pattern_linking.md b/docs/admin/ambassador_pattern_linking.md new file mode 100644 index 00000000..b5c30411 --- /dev/null +++ b/docs/admin/ambassador_pattern_linking.md @@ -0,0 +1,159 @@ + + +# Link via an ambassador container + +Rather than hardcoding network links between a service consumer and +provider, Docker encourages service portability, for example instead of: + + (consumer) --> (redis) + +Requiring you to restart the `consumer` to attach it to a different +`redis` service, you can add ambassadors: + + (consumer) --> (redis-ambassador) --> (redis) + +Or + + (consumer) --> (redis-ambassador) ---network---> (redis-ambassador) --> (redis) + +When you need to rewire your consumer to talk to a different Redis +server, you can just restart the `redis-ambassador` container that the +consumer is connected to. + +This pattern also allows you to transparently move the Redis server to a +different docker host from the consumer. + +Using the `svendowideit/ambassador` container, the link wiring is +controlled entirely from the `docker run` parameters. + +## Two host example + +Start actual Redis server on one Docker host + + big-server $ docker run -d --name redis crosbymichael/redis + +Then add an ambassador linked to the Redis server, mapping a port to the +outside world + + big-server $ docker run -d --link redis:redis --name redis_ambassador -p 6379:6379 svendowideit/ambassador + +On the other host, you can set up another ambassador setting environment +variables for each remote port we want to proxy to the `big-server` + + client-server $ docker run -d --name redis_ambassador --expose 6379 -e REDIS_PORT_6379_TCP=tcp://192.168.1.52:6379 svendowideit/ambassador + +Then on the `client-server` host, you can use a Redis client container +to talk to the remote Redis server, just by linking to the local Redis +ambassador. + + client-server $ docker run -i -t --rm --link redis_ambassador:redis relateiq/redis-cli + redis 172.17.0.160:6379> ping + PONG + +## How it works + +The following example shows what the `svendowideit/ambassador` container +does automatically (with a tiny amount of `sed`) + +On the Docker host (192.168.1.52) that Redis will run on: + + # start actual redis server + $ docker run -d --name redis crosbymichael/redis + + # get a redis-cli container for connection testing + $ docker pull relateiq/redis-cli + + # test the redis server by talking to it directly + $ docker run -t -i --rm --link redis:redis relateiq/redis-cli + redis 172.17.0.136:6379> ping + PONG + ^D + + # add redis ambassador + $ docker run -t -i --link redis:redis --name redis_ambassador -p 6379:6379 alpine:3.2 sh + +In the `redis_ambassador` container, you can see the linked Redis +containers `env`: + + / # env + REDIS_PORT=tcp://172.17.0.136:6379 + REDIS_PORT_6379_TCP_ADDR=172.17.0.136 + REDIS_NAME=/redis_ambassador/redis + HOSTNAME=19d7adf4705e + SHLVL=1 + HOME=/root + REDIS_PORT_6379_TCP_PORT=6379 + REDIS_PORT_6379_TCP_PROTO=tcp + REDIS_PORT_6379_TCP=tcp://172.17.0.136:6379 + TERM=xterm + PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + PWD=/ + / # exit + +This environment is used by the ambassador `socat` script to expose Redis +to the world (via the `-p 6379:6379` port mapping): + + $ docker rm redis_ambassador + $ CMD="apk update && apk add socat && sh" + $ docker run -t -i --link redis:redis --name redis_ambassador -p 6379:6379 alpine:3.2 sh -c "$CMD" + [...] + / # socat -t 100000000 TCP4-LISTEN:6379,fork,reuseaddr TCP4:172.17.0.136:6379 + +Now ping the Redis server via the ambassador: + +Now go to a different server: + + $ CMD="apk update && apk add socat && sh" + $ docker run -t -i --expose 6379 --name redis_ambassador alpine:3.2 sh -c "$CMD" + [...] + / # socat -t 100000000 TCP4-LISTEN:6379,fork,reuseaddr TCP4:192.168.1.52:6379 + +And get the `redis-cli` image so we can talk over the ambassador bridge. + + $ docker pull relateiq/redis-cli + $ docker run -i -t --rm --link redis_ambassador:redis relateiq/redis-cli + redis 172.17.0.160:6379> ping + PONG + +## The svendowideit/ambassador Dockerfile + +The `svendowideit/ambassador` image is based on the `alpine:3.2` image with +`socat` installed. When you start the container, it uses a small `sed` +script to parse out the (possibly multiple) link environment variables +to set up the port forwarding. On the remote host, you need to set the +variable using the `-e` command line option. + + --expose 1234 -e REDIS_PORT_1234_TCP=tcp://192.168.1.52:6379 + +Will forward the local `1234` port to the remote IP and port, in this +case `192.168.1.52:6379`. + + # + # do + # docker build -t svendowideit/ambassador . + # then to run it (on the host that has the real backend on it) + # docker run -t -i -link redis:redis -name redis_ambassador -p 6379:6379 svendowideit/ambassador + # on the remote host, you can set up another ambassador + # docker run -t -i -name redis_ambassador -expose 6379 -e REDIS_PORT_6379_TCP=tcp://192.168.1.52:6379 svendowideit/ambassador sh + # you can read more about this process at https://docs.docker.com/articles/ambassador_pattern_linking/ + + # use alpine because its a minimal image with a package manager. + # prettymuch all that is needed is a container that has a functioning env and socat (or equivalent) + FROM alpine:3.2 + MAINTAINER SvenDowideit@home.org.au + + RUN apk update && \ + apk add socat && \ + rm -r /var/cache/ + + CMD env | grep _TCP= | (sed 's/.*_PORT_\([0-9]*\)_TCP=tcp:\/\/\(.*\):\(.*\)/socat -t 100000000 TCP4-LISTEN:\1,fork,reuseaddr TCP4:\2:\3 \&/' && echo wait) | sh diff --git a/docs/admin/b2d_volume_images/add_cd.png b/docs/admin/b2d_volume_images/add_cd.png new file mode 100644 index 0000000000000000000000000000000000000000..50d7c4e7ed917161b5e0ebc3294583ec5e26ec44 GIT binary patch literal 27607 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 literal 0 HcmV?d00001 diff --git a/docs/admin/b2d_volume_images/add_volume.png b/docs/admin/b2d_volume_images/add_volume.png new file mode 100644 index 0000000000000000000000000000000000000000..ea2d6f6e79f07aa6d7ae29641ca63f351b0ce556 GIT binary patch literal 30506 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 literal 0 HcmV?d00001 diff --git a/docs/admin/b2d_volume_images/gparted.png b/docs/admin/b2d_volume_images/gparted.png new file mode 100644 index 0000000000000000000000000000000000000000..1a50155ce2a8ee10f1b7ee0c3858e8c4a6e4b316 GIT binary patch literal 77989 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

0_6@73zP^l!DU{u%NrZ13WQN%j%mh7u>? z*|$a<{li$Zvv0{nP7k$(e*znYCyDCU9vVH>`*mK$u-wS7mv*}jQmMvU@oPa3b172Y zet5-L%aY9Ux>xH})b0i41&S-|5M=)2<3%(;v^rsytBg=M*tlE8jPOH9Mi+Ut=U!cM zk9Eh>KBZ^A26=WBfOZ%pqW8QswpXJ~wI(AhLbWb)zV(W7wc`w|j1 z_bJETxId=-ajRN7{V~xGzyoQTm^-f?$V*>-xv&3)RaPN}u-DUqLn8tmnl3f`GAV|A zoqas)A$4VpDr1m_y0#VtpA19paFGD{Cz;&ZQ7v9Pwl`s;d|Ntow?qS`bK#?YeyURRuh8QnlJw80aFF{WsETKO^g?UMbT&I9#fialHnyFF;SmdXV zY{HlC((5t}%+<)NQQc@&08r*qjDoivP_8Hl>aGurS$#^1Pmj;IUJFrW7rYal8k*W> zP&O}8?^-`$C;BklZ1BF71@|;}B)1b6PR7S3 z$*C%G$f@Q*bt@&ErBy3 zH$1C<-X_4$70F{_w)hskD7ExzHt$13WP87=v~vtiNM7ZNw4 z>Xe3_n*S~|;R8c}DM-?a`2+ct`70fV9OoJ~8U*%iJaIi0_sI86<_u@u_Ca$iE?^?Y{nLu@%2w*SXzLTjCaQY6n+jze5H{qGK;K0yYaZZ#GeRTrUib zvvux&T{vZ;*KE{0SKW4-0a-U4dK@0V^?BvJcf55xd1~RU;{`nDKKkWFdE~R{xS4SP z?!C&7s*f6l2aNcd>aIw)4?&VmznX@a2E#)A!*4uqWN}fr#khR9k{=&^B)FCK`M&r| z@!ii(c=mX;pG!Xn7YHdE^VahPkUt@*5nXpF@-E`6vn3aD+^LJ0pbB^%c-%?eA@245 zvim6bG5uq~$1r6Em8fJB;I(a@@_I*a1EbGmdeLX~!yItVjLb?DSH$D4x*yEl!{OzT zU;vy9`m%xn;)>3>EPasrpf#XdO5>fwua7@gpJRCVN4B!IIcvQ(%k}ANH;0-<4Bb38 z_a@wk;Z>V26lq>{(NQX)2Jx-6tAgS_$AMlERfDno<#=t9uAGirAq={Gp&#no9~2KS zJ}9Qp#2Xix=pVo5-QK$2s=F<|-7>La9bm0x6>imSIc*tJ8(X7tqJ8-sJ?)UO=RIe9 zi$J{kD?<;TwcBtsr<4~pSzkuNLUJKKRA`?@b4)d7*)Ixxf+lh^hf}2$ibRQ3g4>a3 z?b9rhJH+T39Mf+mqcv*It2KdbVXb8Q#^J0gCW|#c-pY@~V)8#KmL{qvTC!fXjW)lR zTwU038AuDQ3ri$LToYevDpyNXS6!atLrGe6f3bM82STQh|Jwmenw};*-@y=d^bvYP?;`m*(tLY>6!wb zJ79}v%V85Wl!Wp-@hmntEx`9}7n~EX;%D|p=d0~{t}@aymRedprw*ops(s-={qoSh;Zb-@`PRru!EIzj}W?`zR90WMWY|;0B6=(GOC%a;KVAuq40_=xxan7R-Z*wl$*{$suoHtqA|wfI z2}L6wL>My^W;)oq+6x`0O*tnNo~a~c=?J%-Kip6nOBt)ZYaN_+cjz-*V+2t@Xxrou z#GoUlx63KXB-o9bpS?f!O9L&;DIO%L7<;z)orIny1eHeW?k-TytD+B%u0kI5M563f zX2^w&h0>Y}+%vY%_kf_KLsS>VcY%uKhb?m+ESu9A3p~yji>r&?5P{Q!QeWhS%mD}Z zjhEVq+P2voZMiIofAY2Z%-XD5^Gqf5+GTR>3cS}&rs|sIcfjGJ;A?QMbfJ%MTYwkd zyLC_KCjHp@hTwZx%=cfg_?!9T@w1I%SR2L(5DM7wr}N`O`5-7F1QMM0R~jD5J{0pw zLO891`|u)&v;i4n4dBqLGY5+U>v4Fw>0moAm=KA2%WLnh_3cT`D?{9eI=pmuKg=ZH zj6V6fjGMW}h(CUslt(?zlN<0&tokgRo+%PX%Ln(5TH+6Cjk0rhOCMV>w}wGi#!PVP zYbh#gV$4<=?M4XsG}5iMzchjh?a^ zwWPDFB{d&AFFPj<5TBZwTEx}DN=QRW=D*FcPogw7?(Xk}I57n_qC?Vp|e)sK{=o4Kp)dv{xBC+gq*n!R)Oa2KVa`909z z*PrvW^s@bDBqz83l7%IZ<97`Q7dt1%-+g17iu`^nq-N`7>7XZN>uBlZh8+XQ&Benl z@}CC(zp8(R{8v-`f0}afaQ%;_|El_LQxT5eB>an{Kl$~aw^)V&@kKcPWQ zF1$0Xvt~TcaR0Lf{``CR{M?M-U(Yzmf7gCM|GVl9|LWv_H_2$gk>^`kRb78c@JcJv7}lD*Heh<#Xd!WRmuyqaQyh~X1?)F z?MY7-b~QREdMd_uh?g%BJ1g#_iZ#ud=BOOI{0t%k0I=D)SW}%g?1IhT5&=M0Dkm#n1_6iafl-nF7xNTAMpP8=X?UkRAiJYouF%M>} zxLvemte{n{tl9hh*U{%Zz*K<04rYDgq$g^G68!GG%G@nDd3YzQZV+B zp75BlrUI9_Ax|U>!z#kk{#pDF=nri%lx<7bjzeHKxCdqn<(cmje)ayK%G?z%YKY3q z>-Yw{w|Op14O{t$oEY9lbyeaMke)*uTBT2DQ?~rjA%oEN8NUuD z#1&=o3-DWG#P|XGL0*Gq42VbEy%}J&LDPDflP3oQ~aw%wm|cx4S<| zHDn_dwWb^@OXj#2J9z-wQxvjVl>qg&iV~~xXX-5tsBaP0=?h1vrgbA}D>L(h*LVLW z?+xUeiR?;H#1iyIW`C#_50Fy&RTOh@l6(CT&#WgiQsRmDGCxIYKSKr5SQ2QTIREif0rL~{8#ns%q85957zePQ_FO)jKxa7Qf z;Ir-PHoHBSG>)`c_YttMVZ(d>|4J86fjI|H#7_P0bUA2Dm_op*AMUm{LQzl*M&~>B`Rw)Pa6INCW0_Vka-HHsn%V3U^+kbNVC)*owWH2C2D+HdImDy);ZeUw&JF&U%u>d+doc|ai z^=_?uXj`e-COB=m#AcAfqBBT2N(9)ncghLEwc=^-y{H zY3N$}jurC22Pb4lV4UVe{;{BhW}^$+IH>>DDqFtaL6?Pf_S>K-C8Q*%P+l*5@a1^H?16?I6c_h&&Bh-ILhXFM9O06HBGG zf^5RfPUof{c0))-pH~llU5~2nacp#uxcxVM<)Q9G)M83!4NXn9&eIYtuh@u9A@9a{ zOl*AT%b>Yv8z;MQN&y1MULR9)C|SXX&shl5xwl@UP2a+S4*A^I(k+%&lHz*`$KL4T zV$Yt$ay@NxOETx&! zp9uN#)Me7#aGyKU+iw?Mn9rV?=(-YLJ4bhuO(2M5Qav>S(subvVUI1vhU`p~b3aUZ zy{CYAVkfupXTlR;B6&QT>%0;1m%8`>HG(jjW{)l`ElgPIHQ7a6qhL8mluhMQim|I$ zEFdlmvP|f>>wAIk&tckxDi3LeiJPPY7mN_-yBsZt96q)o-lq83EiI?}9xM?Kqvq)8 z#ZSs<&_>kJwzbLSRe#jX#j8VRe}g!Fn$-@n*tSSHwZKwZst4g;YUsiRy)4W(+xZ;( ziRYGPD%`+3O#%AJ;R9=FH3za#4*PnHWQkQ$f#{X3Dnuzg1dWvx2iU_r<7w>ghN*MA zw{V310X2a*5N`AwQ^-!10bZV}@8*NUzYGSu0!>J0?M*4QYxj!GXN>u?ss2*VM;DL6 z8=!EdCz9LH8B1B6O8pl1xzPLpT`fCfYQieW^*!go8WMcG7CKO#2Cuj6W2&~fi6w26 zxnZ;85{eLYtHn4ka;e6nbXi?AtESp3Ljqv?fqhixYmJeQ0OER5E2IGCuKV(_Jv!5AssiQPSW`zcp`zPpNYiy4SXdxp zFCxCZRBiHu{tuf#%6%lRjuRD$`S2j+SV8eE?J+i<>7mw&Qbn7=d5>KdRIGEA)Vbr#&kVX1<`z35Rp5NMkLluPA%VtblXUSF>TQA*?LF^a6P=<1&`*rD)K z>Bm3_(XU`3e!h_0#}At_fAwITDY#`gAExh&O^)%f#AIbSa2P5wPb`(bcy#I`tq5Od zn}b95TX!Z0rIyoCN3spCPDCy7l6i@*9WBFECg%(php6R2casWKRa4gcqDI=IS3Jlc1<9>NM9i5HQYQU>-0GEnf_IOQEL9*J5iJ#W_CJVzNu-+GYeU&WH(gT zB3BeQ@gV*JMuGf?QPj{hP1D6Jwly9p`H6Y=JW4%X%ZXBt7a5g=y zvzU%)exh%D0`xCTJ!8L@y@&Qi{hSuz+h`iE-8#1gWTvB)geG*h`>I(48U`^~(ex{a zP5TFIE3bJ?n#^$WofIzEQk{4|&2t=>Vs*n8;@HH6+=hpCA0GG-7}rpF97;`C>F*wm z4=5^Z^yC$0Wj%qtxZdi|`!2>%k>>6eb zI@)RaUOaVUB!>A0i(bo`-Q27!Y73S(QU83lYwE;tv$L zudNxQ0F8tF(-Z(+h)T@B-MfwsKNqfd+M4dfy;mcD?CEcW?jIk=wbGt*=x(nh8^>{) zHJH(67E7$uvjla%QnE?@H8V^FgkNSeZ5pD5jaT1l{$VxJvUY&?WAHm4^UORw65741 zfAUL-j_%=RpNp`(RxH3$zsV#4_}kPg%`D%sqSSHg0xbwJ6I8%{ zmvtbql9$1t)+(dTuY8(3b=;$QrnGSK)%U$|H;q?f^xBDl&t=#3eu&hSrJE$v>tod@ z6)J~v&b5b1H{rz#HG%!1!>JF~9_FzR6R~I}yxURGH+{#&^9%y5@skK~l+3A%id^nc zsg{9yG=Y`+FI$rod4e@*yFVC{>8{Rb<7Z zwyM?we>2|}&}Hl9THCak8%n?+nAm6U3Rl!vI6%-y6M1HD&^BkPCSi`s^o>hWU36b} z*+qZSEmd#@S^!DI)cH7~O2Bvj5{6Yhy+j;40*;9Y(voF!r65ehRE3ENI@IH3Xk7DB zCK(50O3Hld4Go(8rUSb>)Hj38mYNA6z#cnj)Td)c9;*>Tnr+C5tR(oeGOzl4zdd9w z-#RPHYQ%&%M%PcDul{TkYna?k!8OZuh3Z9nTO)S(q^~*8DE&@1z+Jh@g9iQ!HM;aP zrgfdA9rAU?n=ELd^}zRW%4Po1EH9G1!$?905cVk)(M%SGaoVexZI*TH2?h!VH33iYPf3Y4hc5Jga`}s! zeedY#kfy@=9K)ug!p`kcCaha20x;i$)`}*#md!77RJGQi;nXQEUln*Oa*iR>rg3xL z5pGobt2SuPK?&*A^cRt`rqHJs$7nQj8>rZq_{LYdPGP`uBO(%H=ja zGHqm(yV8R}%ohz=tIV;O9jRXzWb*|BwIQd>55|wiu>ksteHo8@6NiZ}IhU1=(67Mf z2XhR_=QuJWjVnFS6+7rk64W$;W9fM=AQT@@evp{Xlh`;zIEpZ`rI^_&P_v-@E%+O? z?rTm}74UaLGcg|UG&SIXeThO!agn<71@??648K?9vL8lM8}>TiGxt>L_*z=pIX8>= zXlHP#0n!aFrpu3U5@k;dGY^OcTUiZdh`rGmJ6I6nBWF_3(|;U_2a@;9a2gL!?Mb#z z5z(gG^xPMGt_Rt(>pUf~pKea_pUJx6?;XghquZsoXq0JijImhNolLmJ*Q{GbgQw$tb90f7VU;q{|JU1)r+t*F~jv(PCFOR(3xp#+($u=Tx7C{P-8JG5d5qI6_l^ zpFdSdg#KtJQeEKw!mkee*aEN4F)5m2j@nY|ZuUMR!@ETeT2RJ$rz;PZ@JxmLGvg}@ zZaRb1v@8VdgDEjL#ADkPgj>+`rL7s)Q$`SPM>yw}?8%)5(zxgR@3-de9V^xfl)V|R z60dmA5VK`n*z|JG+Iml-l3>4yJaqSodj-wS%RtU=7_6Xna$oj;_T>Cy@tH`GBXrJi zKY`BE6+KAHYzR%5Fit*h z%{BppQvX3By0IQCYWEgt#v?{AWLf!JE_(_0Mb$-JG?(!6yi>A*-3CAE*8;?si@wtI zeeXH=66*%~oNhUjj0yF^-(-D`?4|OvIR9C_{kf-s($8X7T$UH$t~Vhb%tz(bSR{6I zG>IEbM1KpWZkji3pK|5M zY)aY!IHJnDZS(4at%0PG4n!ATDdEpLHW+uzyH}w+Os3H!? zfd0(?NL$|WN^H@2mq-F~mcajmT|%KiL#o{d*AgUi_iLy@vEe;hF^2+)vEsHbZhDiY z8fw0}pD!u+9X%yy7j}0{R-WdhD(+4@S8b%Bo0TdLO3;jU01fw7ho(?0xm z1r|&o#9Xc2(Fw%Wy^*i;O3yAZ+c8(r$VHgtWO6}FeFSYcZ}>A!G1r^P-s`8i=am$K zR##uR7G%Qbgj@$VAJ?bJOgp^LuZ2hrH9zKK_Nec6c5Sz_NIqRniQodMhx^=ui9o%nJSPr>suWEs#mhNaP8p@P<#az?CiV5i?-}-RF#F4A_%J68#3W7v) z5^m*Cdp(XT5GnN0uQY_r;=gTxH#BsR&Xb3us|xuk75eE3ADWEp#dmKO*t%_ox%;RQ zrc4=2#;zgBtm4~1_A?crw8*_!G^0WnBa2w*`Hv;}G^9Fj)RZwjMZq{`Z>PO2vfRy| zfY}W2pf+=Sh4%D=ZAOzC*2RLaM+^{gAPS+r=N(Ct2=M8&c>M$Qk4%=jekkO6nN)kL z@zGS*Ycc6gzUTbAzg7uT1dJhP7Z)kRFBF7qcZ$4r{o8V;9MdDwUSS)uxL==u?R7v) z?@!;7XTns@5psuyOAXE*yH#UDoiYN16icri+&imwP-aubt9&7=d>)erOkWFTiqpoMP0Z%&HarI z%`Q32R*gLfC-p~-D=Ipy+=aPypLVmHT_74e9T<)f6#TQ?`k17%T(15UuYc#j#bu6UxgFSC_VQPMSacN22 z{fltHR=BjVd6b4|4x0%J;wPhafopIA`*=o=$}iA|FXf}W8;QLA@Y>Htt7(yrwbhX8 zpmEifmif;HN|^ivuCn3#M%GY)+<~zoKFdq^hr8yQ+22V5l z=?b@=y{$vO{f$DAT;OxjgmY`qVcK^)PW_zb=KTf9NtR_TqKRR5kNO2fQJf#z0KcFt zoz<88@0uCeSE;Ajh;229 zm$~O2oA9hK1 zQ=-U6{It9`&F8)_!p%d*f$8Z)DN)FGlgZv!5&Ew^7ef^pCSU#qk}n$|v#UJDWbt_y zILe{uX%&k`m2S-*=EE=6whNkYpH*Dn2L z`|L=w%1P%w6Lj%)7gr+9@{!0hbNilReH3-p(<>!ZVDBd`EL!LOM0uG8X|@p%F0CmG zqWk_f{F6Pm#bc!jSGquVQkuD=;DmCO&eItVF|4LZ_FiY3jB4A`%rMTc7{uudr^KYK z82%jcKEq-;K4m+M2>^(|V~p@g&^jKHgLAe}^)@>WeE-WPdd+swuad^|jsN$pHPN`uXpKI_&kc9Bc1x<({!8n-o*lmH)Rh+B)GW;>LA_U=^?M2Z&$ne4DkL{$bk|{V z+8a3vqGH4aGdl64lm=BtmMqVd;O`NkUi4+tmMD`}ealKulCDMN@efUdTFf?2%XhV} z>1tmYhRHu5^en9>X(nA#ZlRF2tXxc_3hQvES@ArZr93^{V|BB&i!xLCfN`?5oqpCL z?X!HAyUlAJp3136*|?WLHJ?+W?@WXB-rOm2h-bj~7j`8{ag){?!G7UL(eaM6`55`n zJ}P^Msh;wMqgcSzvD#O|;Jja?uO-BJ*tsrZsL)|(z5#Hmv0tdXjE#^*^n}^zEw$#& z`T1bn9(ZhTu@ggJHA4*#ylE{fA3l|Ads){j)m~Wplh9;r-1bgTHF+hkNAfY0&5_F) z;;qtlVl!^swvs&aHkc%#O@r(y!T}DWJwtz3q8EesX<{a8L0M>hk56OAAQfauyRcya==y%y+IC!wA&uBQ8?CqRhVETu zfKL%_nj<~;iie)b6AO0-y>qNGy!}d-IYZ^;+owDAA01BD>{Tuhxa+~)BD*#e6y5^W z&gQht9+VtHxh*`H5}(WmB$3{Q4zl)a-~6msd~OLx*-n>FGdUSL=mQqSmkvg#bsL4{ zUOhP=v7J~Hwh84+fq$N2l4Kt{VII1t<#V{a7h9sYkrZO09d7$U3l$iRn_`kgrOrLKEWWIShTODFTD>p z{B%P2p({y`DAl!1ti*hCtL6f5z1yP5wD(x`hfH%a{kn88cI3?d6UhuQ_RPc*UYI)h zh+|ahV(+JioUUL-v9d8HN4;BEvn_7DXP(bkVDVQ%+x*R_fU@sp9e9+s7x@g>>~gsk z1TNLQ>UINuveTW){LSW$bfU_so-e(AtcXF`{n(hPFj*ZIc&9Vs@rEDcs(FJMjO#S& zIrQBxvYmq#m^sToi$O0&Y^qb{KT$B0?_#o{N=&H3+c3q#Lqe!IEUP|gsBRF)X-2R9 z8Sg?f5Qeby6nJgljy&l0b_DHi^T3-=I;G%__BMpI+Ci+L%4t%6TtsUhJB`x^c4bx< zw%B8_|3yFIdR*NBcaT9}^1g=$zg#&6Af#5zQ*pZrT;mu35*t5i^UsnZguts3<&_q6|G3AoERuXjhz=mmQ6Y@>M3>^MeS=Ld_a@>T%=<`)ioP1p4)sGRJ@;SZF{oEju4J|VdHOYHsWueW(Zy+6_YHO9? z;_by^51;EalJbL>Q4Zm@LAxd=>%k&Fa8dw4zB=WA!$Z6qOXK-Rj2hg(@hf>qnc)ngcHsmLq46Dk?A(mDx&As1lo8n9O^aI1-Hkv$ z)6o#1Jxhw-7wYEe8n~ zcG=L@WuWpYs&%5cAp28_z8%S9TPiq<%pcHU_4b%6!0)0U?oJR^4>yF+WiUN`F}P8a z26fVo$+F@!l`8gr zDth3jt1C;#^3^EOh`rD^yL_S1rFMx?7gA=_CQ~_OZ)|=xyMEeKQy8Z-?h_DXJN%u& zdMi?HtnbdA`c+wv?bHsRPrAde$z3lM3y{{i0`yJP_0O#JC8>gzi(}_dHWB9VjhHXbmJSAf;x@AQ6o& zaV~y4Q>skK!RNkL#3>_Wt!!vr(&meIqwaP4Z`!ovZYm;w+nYMyS)C#vBQ!I}@a)>kcA0%Yixca9YQDmdZZlr}h_4-4|_bo0bz9yiU}$~bX{gZz`lHh-Y2b{5lKi5ax%0CLkV zA_N2l1OYL~BYcyT^eBYMyeuoc)7I4q-$egZy_Pc`Fztu6e}d4#9AcyyF4KuCX#SS7 z1c`!#zr%){ch-YVPY95>mW__Kqen(lrBrj#5Fh*VohI=IWpP>a%M&vLq=b%0aSVZ$ z%1nO&!9WsX(F25`g+pf`jJn!s;HRnSc!B^(+-ayFz@!xGqnKwToj)Exeo0M_D%1Z} z2u@BX8u1IqHvsFMX8*RsQtI)BMSpGSvbWbk#`6v=a$v7CpZvv}Wtq#2g#Q&w z%;d#~9Z>VAMB{f^n+a}SfL#!`v*fwp2xGtR*8Gd8)4>x;Os%UWMc=zV@^A0M)P&gI}f@Y^dXO$*W$cvM;p(1{~I z$OlhqBf-NFH$^AoK>o<0InEp_EPmZwGG9+?ryEsd%va;N(Ob39(>t##JKfG*V%5ip zjG9l9nPw;Fvsx|UA~>huaTXi#Pu2nOln^C*9GRU)3PO(WW*OhaW$^)TBtYi3FNpu5 z5C@w*;tQqRw>eyBZGxx3n=4>U{upaF*^K1pV6}o09Ski)kaj7hoUdH_izaYs?G-c9 z+Hth}>S^pvYiW;55!OZoaJRZCkrr=(ywzO5t{Su`h-<1=x;UZ`?TO}iEnfQ?s& zml=zWv-oaULa-(N8qe(_tSKH|mVnd~u^Uvxnt}7qgB{nmkX*i6`(@YXS=zp%dy_+-D$(PcrZ)|a-#;C6qc8{T2TcD=9*bD{pa{SrQmaky7N;8zxGpRKb`*aBsMieY=0453}06j754AfM;=>miuncE%&ahEW$Lz-Jyj)1d8#U(#o3Ms zI3A)ZX1s>%VsGiMA+@i!L4tTo6eoTjU=1j93EmfoUgW2RJ9+7Ty{F3{fj^xoRiyMpVi}I zvqaiym!?-{N3)d9oL5S6^uQGS#M2c_Wd>7FKkt>A{6M;}D^l1Z)JEOx`=^dpWSbit zF_<~QW3Mt2o+3J7?WvkJCJL6B37Zfy^{SQ>IUK`$lErk9EtsBR&P|lkrh(=%PN;D7 zpng+Ity?X8eRFVV#4y^liW+94e)TKqw34+ChEjkaYtSVm`}Vq^3e%M)7xS4!He-*4 zmfbkfp@_CQcYhEA^x;2~N8(oT+>Duem5~b|WtZniJF!{Jqqw)*9SWkA$y`$~=SKdr?XcQ5SgGW2F09aAHXEDV`S*POA7o|y zJr=frwOU&ybhr8}ClJLyf)Kkpl?F{t`>Fd24eM^t+h7yspffTb-{x^gHbba`L9I{1 zqm@Q8u(_mNbnt&OF@E=Ua{K3yY|gu>ev&u_QN-zj)Ad+XMbmaS1(nxNh;8_Aq#riP z-b2F_ZZ{}zzsfV zXYaUK{ZC5Xe`xV>0DSkG&u(h-9K;^$TR$7Vae~AY`XI`|zuMhoin&6qa_UZ~fD$**l zzADr3Oy2|Gc3#}_)$+y8Q74hGQVSPin~53H755L1KcL@|oxhHsi!NHgnucOGNY!yF zc-DCd&0SmG`CdkTPM39p6!PiDUlm^^&vyX}07euAayjf)cJjMyMjq>mk?vH`{qkH_({x^p= z*Arsb6EMs!41;|U%=+aXBkej8o~hkc2vu)!?jtf@+ek7t(AGv2^hR-jx5NGdn164q zH;_FSMql%*2DTDRp2)ofSJZ|74e04&(vH{4)pFshkn3{_KA$rPtxWjk;SJ^x)P9dzhCmx~?q?R9@)T`8r9ZKWJuUExJYxE~I^K7chLb^eo>xkkEyr85M9%Y#;&8sC zvK=#_T&vP}g`5ZVB&kLV@1|`%)97NpTBItKgFdsXsh_Mvth?nlMbg={0m6tEVy+J%zBktwDk&n{Q@Tk-Y{8qX2>^x+_t;d=EpW)kyQlCHN)+}4*_Ru2 zlCq_9+^M1 zOIW=K)xUPtde7+kvhN}C917DmG&H}3Qoy{%iUV!9!pF;+WB0f~>XB7?M#g@QmoG2A zzHT}D25m&Q&i8gF`LjIWjsA#fDHw9uRP8@au5K;+4RPdv5C&i}3UBc?ny$jFHuc?N^YKxUA6<-V;2r%l4-A5kJ(# zyeLm$WWo{`zSKf~I>{WZZ7fy_UIgv>qXRjNki`V-qc06gNJU&aNDMxlI9SldQG++P4`)?Qpc4W6edj<9+#wO>!x)cQ%2cn30v|2TjF2u$g58xg>T6J@D(~4DoqrhZ@ zT(F$q4F(@ec9D*%vQyX${{J7#1fdJbA5^sn`d=hE;;?^9shhQY8%MMRQ}q3uA=G$Sq=(2ta)B zX!IyrgH>oPgdwvi&=!*KE_d_Hg(jPZj3y-;axxFtc)#^7;X}@`ofOAucKPVbk|zc4 zbjZ^EG5XdS#mssq+9#?q_pxKKpQpPGCtk($js5~LE(cx%S_E~8468c?sGO#R2(ujWtBR^ze)=BA1o|#eOeSHRwL0@8RXu*TV zwfHA5!8vFmJjivC^!a?fW;VyC$Gp1pj?3fh`C&=?*(9v@a%qO``xG0w&5?=@g{L)NEQDjRm^;XUxAKB zj>=8#X^1_UcxNmjL04|U5&t=-69Fx;E!GWIDZh@V)9`=vfQT}O{)NI}w6zAoQ*Bi2 zh#G>K*311X{n)RKVE3;L#{o}OIISlW6as8ociwG>PPQ3&)VX`0)ITfV_O6#3dsfTv zgmWGl#?dV2X}7#MH?LQ2YCS_ab9ZM)MHE4tK(O}z;jLPp$?zL9pW?eII9;eI{1%PU zQw@u|#9_K3#;3^&YW-PiQYK84{fXFHo1}#%@o|!K^gDGa6U)^2FJB9Y3v3enzF`we z=!A+O@j}`TD-F_u&re8Gb+DPVCFV>sQoFi^(ONYsTY=$$(_cx+YsmV3oWn2(jtVrV zkoX)?t@V&vKGTM3(P>lshrZ7L7@g>cAuozkHIUg)G!(dE$n`n>D_kwz*~^XYN#ByE zD+%w89v{UN)rYvg*4k98k%OH-L!8kanlZwuY1n@p8vi8mVa zWWtrUld43r{j(L{KprxkA^b@+eZVJ@+hjFlx~A78U8CA2@(pi}-AP==9~KNi#laoQ zE069jZrpY!t2JkT&x}fB1fN=k4~7POG>Vj`8!MhV@8rFgBb|kDl2)gJu0@UZyg?eDiSY^@+q%{(BdekG3Usm}d*pg>YA*UX z3bgH>`u-%Z&_Yv*KGZ$ziK+Y4IKva>SfO$@<${YVdx*Vz#+L8ORYghgoak76uQZde zY=gX#&yIY4Ved1hyRx68o|Zf$gZ)b#_eQ-8jJ+LS@HOC}=D7l2F)=_l^n*~!;hEl) zz6KXv;EcA6JW48#F-AfuF_T-&>4s7MBSK#K9-@Bys zV530dPa2GqH68H_HTtyuMbEx!V`Ie8vFatkv2yW(X5BXO3LT>jTdwXJY>0V#e=T`P z(T~J_4Si%bSTmSN`+E)B;?T1PPb|HXxps&u33Odbu(s*xZ=*?z#Fi+3*ybrVjDIbN zuIkhpRM@5@FQe3{3DbE;?qNfepq!%vIAQN+ikVXT6qcetEvpMcS|%BUNnHBBl7 zdrbz}VYyZBvBS%!BIsKHRNfl@g^7=-hSb}4H)dBUR0?dq3V zE{My@dHM1WWv*9J?45~|AEOgJVtiF?8G`rWZkxwlQnu<`m@8WWx8kAeW2^`lQjr6; zEd3p|=trGAPtt?R%^ek{TX|n0}uIxMBi-oB!;|qfQAAx|Y=ahLLjXK+2kAeSI283xkSB66``E{!% zyYTsD6U@Hm&vr^O2}`E}b~D@>>qCcNF*vF&CQmtXAR9>?!IF!(w_jT$$?i-q=~#wv zNKlv0R;)lw=2E-0lJAXXehV8ZNzX3&o?S?-awbSjugvCwO(di;vfw#IH;t9;IGf@IIAKpVLAhU8?@SUa9QQu>kv?>hH=)`0bYBNH#S2V%_b>tAaX$@5EklMU0s4`d6~ z7wF?DA%_kb8q!swcg{weU!d!|S!2ofHH5=D{)~wo)8KyuTnA;2OCTnS&8r!ecxBZ@ z#x0?ns#`;k3wq6cvZe1T- zq=i&A&j@@A7q=3_L>|W3QA1m%9YfM0|A(t$G0V4L;-Xh3LKfpksdluSwx$JPws#z_ zX&&zyn{JqkzbF4zW%wqh zmGt!jVb)u+1<9`S$Ge9_9c4|sfY~s!^D+ba)ZA^H_~4wuHivJouGASANX+1-N|6?Q zj69(~?VW_1Ldjqx4AyAi7|W#PrcS~H2`F)HVMxA%o_owkQwVGCNb!>0orkOV8M3NT zhUkX@p&JO({5~clb8qOQOSlqN!p$B@smQ3=bA!;lNo|pb0tIwHOjVM!N~MfZpUs`8>BE{$#vz^K&rPUtD-#bG|Tk+C0yT`hRZa= zBsmiPzL*M3o{HuE}x5+ys`7jRUsPFQ4Y7#c8jeW z7t&~G`?*cTEcP@2{XXb#BRNgOj-XltS=sQJ!-r3LBlHNSjd@!O`|E#pYi z4Epu6B6WsOH}{L1-4I#kUivyLx== z1DfT-Z#}$5jE5En-E1P=z3P#plKrNWhROED7xDPY1(Mz~s=wqIqctyh*~(|

H3( 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 literal 0 HcmV?d00001 diff --git a/docs/examples/couchdb_data_volumes.md b/docs/examples/couchdb_data_volumes.md new file mode 100644 index 00000000..972e78a7 --- /dev/null +++ b/docs/examples/couchdb_data_volumes.md @@ -0,0 +1,49 @@ + + +# Dockerizing a CouchDB service + +> **Note**: +> - **If you don't like sudo** then see [*Giving non-root +> access*](../installation/binaries.md#giving-non-root-access) + +Here's an example of using data volumes to share the same data between +two CouchDB containers. This could be used for hot upgrades, testing +different versions of CouchDB on the same data, etc. + +## Create first database + +Note that we're marking `/var/lib/couchdb` as a data volume. + + $ COUCH1=$(docker run -d -p 5984 -v /var/lib/couchdb shykes/couchdb:2013-05-03) + +## Add data to the first database + +We're assuming your Docker host is reachable at `localhost`. If not, +replace `localhost` with the public IP of your Docker host. + + $ HOST=localhost + $ URL="http://$HOST:$(docker port $COUCH1 5984 | grep -o '[1-9][0-9]*$')/_utils/" + $ echo "Navigate to $URL in your browser, and use the couch interface to add data" + +## Create second database + +This time, we're requesting shared access to `$COUCH1`'s volumes. + + $ COUCH2=$(docker run -d -p 5984 --volumes-from $COUCH1 shykes/couchdb:2013-05-03) + +## Browse data on the second database + + $ HOST=localhost + $ URL="http://$HOST:$(docker port $COUCH2 5984 | grep -o '[1-9][0-9]*$')/_utils/" + $ echo "Navigate to $URL in your browser. You should see the same data as in the first database"'!' + +Congratulations, you are now running two Couchdb containers, completely +isolated from each other *except* for their data. diff --git a/docs/examples/index.md b/docs/examples/index.md new file mode 100644 index 00000000..87dd2b3f --- /dev/null +++ b/docs/examples/index.md @@ -0,0 +1,23 @@ + + +# Dockerize an application + +This section contains the following: + +* [Dockerizing MongoDB](mongodb.md) +* [Dockerizing PostgreSQL](postgresql_service.md) +* [Dockerizing a CouchDB service](couchdb_data_volumes.md) +* [Dockerizing a Node.js web app](nodejs_web_app.md) +* [Dockerizing a Redis service](running_redis_service.md) +* [Dockerizing an apt-cacher-ng service](apt-cacher-ng.md) +* [Dockerizing applications: A 'Hello world'](../userguide/containers/dockerizing.md) diff --git a/docs/examples/mongodb.md b/docs/examples/mongodb.md new file mode 100644 index 00000000..3173aa1b --- /dev/null +++ b/docs/examples/mongodb.md @@ -0,0 +1,177 @@ + + +# Dockerizing MongoDB + +## Introduction + +In this example, we are going to learn how to build a Docker image with +MongoDB pre-installed. We'll also see how to `push` that image to the +[Docker Hub registry](https://hub.docker.com) and share it with others! + +> **Note:** This guide will show the mechanics of building a MongoDB container, but +> you will probably want to use the official image on [Docker Hub]( https://hub.docker.com/_/mongo/) + +Using Docker and containers for deploying [MongoDB](https://www.mongodb.org/) +instances will bring several benefits, such as: + + - Easy to maintain, highly configurable MongoDB instances; + - Ready to run and start working within milliseconds; + - Based on globally accessible and shareable images. + +> **Note:** +> +> If you do **_not_** like `sudo`, you might want to check out: +> [*Giving non-root access*](../installation/binaries.md#giving-non-root-access). + +## Creating a Dockerfile for MongoDB + +Let's create our `Dockerfile` and start building it: + + $ nano Dockerfile + +Although optional, it is handy to have comments at the beginning of a +`Dockerfile` explaining its purpose: + + # Dockerizing MongoDB: Dockerfile for building MongoDB images + # Based on ubuntu:latest, installs MongoDB following the instructions from: + # http://docs.mongodb.org/manual/tutorial/install-mongodb-on-ubuntu/ + +> **Tip:** `Dockerfile`s are flexible. However, they need to follow a certain +> format. The first item to be defined is the name of an image, which becomes +> the *parent* of your *Dockerized MongoDB* image. + +We will build our image using the latest version of Ubuntu from the +[Docker Hub Ubuntu](https://hub.docker.com/_/ubuntu/) repository. + + # Format: FROM repository[:version] + FROM ubuntu:latest + +Continuing, we will declare the `MAINTAINER` of the `Dockerfile`: + + # Format: MAINTAINER Name + MAINTAINER M.Y. Name + +> **Note:** Although Ubuntu systems have MongoDB packages, they are likely to +> be outdated. Therefore in this example, we will use the official MongoDB +> packages. + +We will begin with importing the MongoDB public GPG key. We will also create +a MongoDB repository file for the package manager. + + # Installation: + # Import MongoDB public GPG key AND create a MongoDB list file + RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 + RUN echo "deb http://repo.mongodb.org/apt/ubuntu "$(lsb_release -sc)"/mongodb-org/3.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-3.0.list + +After this initial preparation we can update our packages and install MongoDB. + + # Update apt-get sources AND install MongoDB + RUN apt-get update && apt-get install -y mongodb-org + +> **Tip:** You can install a specific version of MongoDB by using a list +> of required packages with versions, e.g.: +> +> RUN apt-get update && apt-get install -y mongodb-org=3.0.1 mongodb-org-server=3.0.1 mongodb-org-shell=3.0.1 mongodb-org-mongos=3.0.1 mongodb-org-tools=3.0.1 + +MongoDB requires a data directory. Let's create it as the final step of our +installation instructions. + + # Create the MongoDB data directory + RUN mkdir -p /data/db + +Lastly we set the `ENTRYPOINT` which will tell Docker to run `mongod` inside +the containers launched from our MongoDB image. And for ports, we will use +the `EXPOSE` instruction. + + # Expose port 27017 from the container to the host + EXPOSE 27017 + + # Set usr/bin/mongod as the dockerized entry-point application + ENTRYPOINT ["/usr/bin/mongod"] + +Now save the file and let's build our image. + +> **Note:** +> +> The full version of this `Dockerfile` can be found [here](https://github.com/docker/docker/blob/master/docs/examples/mongodb/Dockerfile). + +## Building the MongoDB Docker image + +With our `Dockerfile`, we can now build the MongoDB image using Docker. Unless +experimenting, it is always a good practice to tag Docker images by passing the +`--tag` option to `docker build` command. + + # Format: docker build --tag/-t / . + # Example: + $ docker build --tag my/repo . + +Once this command is issued, Docker will go through the `Dockerfile` and build +the image. The final image will be tagged `my/repo`. + +## Pushing the MongoDB image to Docker Hub + +All Docker image repositories can be hosted and shared on +[Docker Hub](https://hub.docker.com) with the `docker push` command. For this, +you need to be logged-in. + + # Log-in + $ docker login + Username: + .. + + # Push the image + # Format: docker push / + $ docker push my/repo + The push refers to a repository [my/repo] (len: 1) + Sending image list + Pushing repository my/repo (1 tags) + .. + +## Using the MongoDB image + +Using the MongoDB image we created, we can run one or more MongoDB instances +as daemon process(es). + + # Basic way + # Usage: docker run --name -d / + $ docker run -p 27017:27017 --name mongo_instance_001 -d my/repo + + # Dockerized MongoDB, lean and mean! + # Usage: docker run --name -d / --noprealloc --smallfiles + $ docker run -p 27017:27017 --name mongo_instance_001 -d my/repo --smallfiles + + # Checking out the logs of a MongoDB container + # Usage: docker logs + $ docker logs mongo_instance_001 + + # Playing with MongoDB + # Usage: mongo --port + $ mongo --port 27017 + + # If using docker-machine + # Usage: mongo --port --host + $ mongo --port 27017 --host 192.168.59.103 + +> **Tip:** +If you want to run two containers on the same engine, then you will need to map +the exposed port to two different ports on the host + + # Start two containers and map the ports + $ docker run -p 28001:27017 --name mongo_instance_001 -d my/repo + $ docker run -p 28002:27017 --name mongo_instance_002 -d my/repo + + # Now you can connect to each MongoDB instance on the two ports + $ mongo --port 28001 + $ mongo --port 28002 + + - [Linking containers](../userguide/networking/default_network/dockerlinks.md) + - [Cross-host linking containers](../admin/ambassador_pattern_linking.md) + - [Creating an Automated Build](https://docs.docker.com/docker-hub/builds/) diff --git a/docs/examples/mongodb/Dockerfile b/docs/examples/mongodb/Dockerfile new file mode 100644 index 00000000..3513da47 --- /dev/null +++ b/docs/examples/mongodb/Dockerfile @@ -0,0 +1,22 @@ +# Dockerizing MongoDB: Dockerfile for building MongoDB images +# Based on ubuntu:latest, installs MongoDB following the instructions from: +# http://docs.mongodb.org/manual/tutorial/install-mongodb-on-ubuntu/ + +FROM ubuntu:latest +MAINTAINER Docker + +# Installation: +# Import MongoDB public GPG key AND create a MongoDB list file +RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 +RUN echo "deb http://repo.mongodb.org/apt/ubuntu "$(lsb_release -sc)"/mongodb-org/3.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-3.0.list +# Update apt-get sources AND install MongoDB +RUN apt-get update && apt-get install -y mongodb-org + +# Create the MongoDB data directory +RUN mkdir -p /data/db + +# Expose port #27017 from the container to the host +EXPOSE 27017 + +# Set /usr/bin/mongod as the dockerized entry-point application +ENTRYPOINT ["/usr/bin/mongod"] diff --git a/docs/examples/nodejs_web_app.md b/docs/examples/nodejs_web_app.md new file mode 100644 index 00000000..149f5b47 --- /dev/null +++ b/docs/examples/nodejs_web_app.md @@ -0,0 +1,199 @@ + + +# Dockerizing a Node.js web app + +> **Note**: +> - **If you don't like sudo** then see [*Giving non-root +> access*](../installation/binaries.md#giving-non-root-access) + +The goal of this example is to show you how you can build your own +Docker images from a parent image using a `Dockerfile` +. We will do that by making a simple Node.js hello world web +application running on CentOS. You can get the full source code at[https://github.com/enokd/docker-node-hello/](https://github.com/enokd/docker-node-hello/). + +## Create Node.js app + +First, create a directory `src` where all the files +would live. Then create a `package.json` file that +describes your app and its dependencies: + + { + "name": "docker-centos-hello", + "private": true, + "version": "0.0.1", + "description": "Node.js Hello world app on CentOS using docker", + "author": "Daniel Gasienica ", + "dependencies": { + "express": "3.2.4" + } + } + +Then, create an `index.js` file that defines a web +app using the [Express.js](http://expressjs.com/) framework: + + var express = require('express'); + + // Constants + var PORT = 8080; + + // App + var app = express(); + app.get('/', function (req, res) { + res.send('Hello world\n'); + }); + + app.listen(PORT); + console.log('Running on http://localhost:' + PORT); + +In the next steps, we'll look at how you can run this app inside a +CentOS container using Docker. First, you'll need to build a Docker +image of your app. + +## Creating a Dockerfile + +Create an empty file called `Dockerfile`: + + touch Dockerfile + +Open the `Dockerfile` in your favorite text editor + +Define the parent image you want to use to build your own image on +top of. Here, we'll use +[CentOS](https://hub.docker.com/_/centos/) (tag: `centos6`) +available on the [Docker Hub](https://hub.docker.com/): + + FROM centos:centos6 + +Since we're building a Node.js app, you'll have to install Node.js as +well as npm on your CentOS image. Node.js is required to run your app +and npm is required to install your app's dependencies defined in +`package.json`. To install the right package for +CentOS, we'll use the instructions from the [Node.js wiki]( +https://github.com/joyent/node/wiki/Installing-Node.js- +via-package-manager#rhelcentosscientific-linux-6): + + # Enable Extra Packages for Enterprise Linux (EPEL) for CentOS + RUN yum install -y epel-release + # Install Node.js and npm + RUN yum install -y nodejs npm + +Install your app dependencies using the `npm` binary: + + # Install app dependencies + COPY package.json /src/package.json + RUN cd /src; npm install --production + +To bundle your app's source code inside the Docker image, use the `COPY` +instruction: + + # Bundle app source + COPY . /src + +Your app binds to port `8080` so you'll use the `EXPOSE` instruction to have +it mapped by the `docker` daemon: + + EXPOSE 8080 + +Last but not least, define the command to run your app using `CMD` which +defines your runtime, i.e. `node`, and the path to our app, i.e. `src/index.js` +(see the step where we added the source to the container): + + CMD ["node", "/src/index.js"] + +Your `Dockerfile` should now look like this: + + FROM centos:centos6 + + # Enable Extra Packages for Enterprise Linux (EPEL) for CentOS + RUN yum install -y epel-release + # Install Node.js and npm + RUN yum install -y nodejs npm + + # Install app dependencies + COPY package.json /src/package.json + RUN cd /src; npm install --production + + # Bundle app source + COPY . /src + + EXPOSE 8080 + CMD ["node", "/src/index.js"] + +## Building your image + +Go to the directory that has your `Dockerfile` and run the following command +to build a Docker image. The `-t` flag lets you tag your image so it's easier +to find later using the `docker images` command: + + $ docker build -t /centos-node-hello . + +Your image will now be listed by Docker: + + $ docker images + + # Example + REPOSITORY TAG ID CREATED + centos centos6 539c0211cd76 8 weeks ago + /centos-node-hello latest d64d3505b0d2 2 hours ago + +## Run the image + +Running your image with `-d` runs the container in detached mode, leaving the +container running in the background. The `-p` flag redirects a public port to +a private port in the container. Run the image you previously built: + + $ docker run -p 49160:8080 -d /centos-node-hello + +Print the output of your app: + + # Get container ID + $ docker ps + + # Print app output + $ docker logs + + # Example + Running on http://localhost:8080 + +## Test + +To test your app, get the port of your app that Docker mapped: + + $ docker ps + + # Example + ID IMAGE COMMAND ... PORTS + ecce33b30ebf /centos-node-hello:latest node /src/index.js 49160->8080 + +In the example above, Docker mapped the `8080` port of the container to `49160`. + +Now you can call your app using `curl` (install if needed via: +`sudo apt-get install curl`): + + $ curl -i localhost:49160 + + HTTP/1.1 200 OK + X-Powered-By: Express + Content-Type: text/html; charset=utf-8 + Content-Length: 12 + Date: Sun, 02 Jun 2013 03:53:22 GMT + Connection: keep-alive + + Hello world + +If you use Docker Machine on OS X, the port is actually mapped to the Docker +host VM, and you should use the following command: + + $ curl $(docker-machine ip VM_NAME):49160 + +We hope this tutorial helped you get up and running with Node.js and +CentOS on Docker. You can get the full source code at +[https://github.com/enokd/docker-node-hello/](https://github.com/enokd/docker-node-hello/). diff --git a/docs/examples/postgresql_service.Dockerfile b/docs/examples/postgresql_service.Dockerfile new file mode 100644 index 00000000..d5767c93 --- /dev/null +++ b/docs/examples/postgresql_service.Dockerfile @@ -0,0 +1,49 @@ +# +# example Dockerfile for https://docs.docker.com/examples/postgresql_service/ +# + +FROM ubuntu +MAINTAINER SvenDowideit@docker.com + +# Add the PostgreSQL PGP key to verify their Debian packages. +# It should be the same key as https://www.postgresql.org/media/keys/ACCC4CF8.asc +RUN apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8 + +# Add PostgreSQL's repository. It contains the most recent stable release +# of PostgreSQL, ``9.3``. +RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ precise-pgdg main" > /etc/apt/sources.list.d/pgdg.list + +# Install ``python-software-properties``, ``software-properties-common`` and PostgreSQL 9.3 +# There are some warnings (in red) that show up during the build. You can hide +# them by prefixing each apt-get statement with DEBIAN_FRONTEND=noninteractive +RUN apt-get update && apt-get install -y python-software-properties software-properties-common postgresql-9.3 postgresql-client-9.3 postgresql-contrib-9.3 + +# Note: The official Debian and Ubuntu images automatically ``apt-get clean`` +# after each ``apt-get`` + +# Run the rest of the commands as the ``postgres`` user created by the ``postgres-9.3`` package when it was ``apt-get installed`` +USER postgres + +# Create a PostgreSQL role named ``docker`` with ``docker`` as the password and +# then create a database `docker` owned by the ``docker`` role. +# Note: here we use ``&&\`` to run commands one after the other - the ``\`` +# allows the RUN command to span multiple lines. +RUN /etc/init.d/postgresql start &&\ + psql --command "CREATE USER docker WITH SUPERUSER PASSWORD 'docker';" &&\ + createdb -O docker docker + +# Adjust PostgreSQL configuration so that remote connections to the +# database are possible. +RUN echo "host all all 0.0.0.0/0 md5" >> /etc/postgresql/9.3/main/pg_hba.conf + +# And add ``listen_addresses`` to ``/etc/postgresql/9.3/main/postgresql.conf`` +RUN echo "listen_addresses='*'" >> /etc/postgresql/9.3/main/postgresql.conf + +# Expose the PostgreSQL port +EXPOSE 5432 + +# Add VOLUMEs to allow backup of config, logs and databases +VOLUME ["/etc/postgresql", "/var/log/postgresql", "/var/lib/postgresql"] + +# Set the default command to run when starting the container +CMD ["/usr/lib/postgresql/9.3/bin/postgres", "-D", "/var/lib/postgresql/9.3/main", "-c", "config_file=/etc/postgresql/9.3/main/postgresql.conf"] diff --git a/docs/examples/postgresql_service.md b/docs/examples/postgresql_service.md new file mode 100644 index 00000000..8d5f6752 --- /dev/null +++ b/docs/examples/postgresql_service.md @@ -0,0 +1,153 @@ + + +# Dockerizing PostgreSQL + +> **Note**: +> - **If you don't like sudo** then see [*Giving non-root +> access*](../installation/binaries.md#giving-non-root-access) + +## Installing PostgreSQL on Docker + +Assuming there is no Docker image that suits your needs on the [Docker +Hub](http://hub.docker.com), you can create one yourself. + +Start by creating a new `Dockerfile`: + +> **Note**: +> This PostgreSQL setup is for development-only purposes. Refer to the +> PostgreSQL documentation to fine-tune these settings so that it is +> suitably secure. + + # + # example Dockerfile for https://docs.docker.com/examples/postgresql_service/ + # + + FROM ubuntu + MAINTAINER SvenDowideit@docker.com + + # Add the PostgreSQL PGP key to verify their Debian packages. + # It should be the same key as https://www.postgresql.org/media/keys/ACCC4CF8.asc + RUN apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8 + + # Add PostgreSQL's repository. It contains the most recent stable release + # of PostgreSQL, ``9.3``. + RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ precise-pgdg main" > /etc/apt/sources.list.d/pgdg.list + + # Install ``python-software-properties``, ``software-properties-common`` and PostgreSQL 9.3 + # There are some warnings (in red) that show up during the build. You can hide + # them by prefixing each apt-get statement with DEBIAN_FRONTEND=noninteractive + RUN apt-get update && apt-get install -y python-software-properties software-properties-common postgresql-9.3 postgresql-client-9.3 postgresql-contrib-9.3 + + # Note: The official Debian and Ubuntu images automatically ``apt-get clean`` + # after each ``apt-get`` + + # Run the rest of the commands as the ``postgres`` user created by the ``postgres-9.3`` package when it was ``apt-get installed`` + USER postgres + + # Create a PostgreSQL role named ``docker`` with ``docker`` as the password and + # then create a database `docker` owned by the ``docker`` role. + # Note: here we use ``&&\`` to run commands one after the other - the ``\`` + # allows the RUN command to span multiple lines. + RUN /etc/init.d/postgresql start &&\ + psql --command "CREATE USER docker WITH SUPERUSER PASSWORD 'docker';" &&\ + createdb -O docker docker + + # Adjust PostgreSQL configuration so that remote connections to the + # database are possible. + RUN echo "host all all 0.0.0.0/0 md5" >> /etc/postgresql/9.3/main/pg_hba.conf + + # And add ``listen_addresses`` to ``/etc/postgresql/9.3/main/postgresql.conf`` + RUN echo "listen_addresses='*'" >> /etc/postgresql/9.3/main/postgresql.conf + + # Expose the PostgreSQL port + EXPOSE 5432 + + # Add VOLUMEs to allow backup of config, logs and databases + VOLUME ["/etc/postgresql", "/var/log/postgresql", "/var/lib/postgresql"] + + # Set the default command to run when starting the container + CMD ["/usr/lib/postgresql/9.3/bin/postgres", "-D", "/var/lib/postgresql/9.3/main", "-c", "config_file=/etc/postgresql/9.3/main/postgresql.conf"] + +Build an image from the Dockerfile assign it a name. + + $ docker build -t eg_postgresql . + +And run the PostgreSQL server container (in the foreground): + + $ docker run --rm -P --name pg_test eg_postgresql + +There are 2 ways to connect to the PostgreSQL server. We can use [*Link +Containers*](../userguide/networking/default_network/dockerlinks.md), or we can access it from our host +(or the network). + +> **Note**: +> The `--rm` removes the container and its image when +> the container exits successfully. + +### Using container linking + +Containers can be linked to another container's ports directly using +`-link remote_name:local_alias` in the client's +`docker run`. This will set a number of environment +variables that can then be used to connect: + + $ docker run --rm -t -i --link pg_test:pg eg_postgresql bash + + postgres@7ef98b1b7243:/$ psql -h $PG_PORT_5432_TCP_ADDR -p $PG_PORT_5432_TCP_PORT -d docker -U docker --password + +### Connecting from your host system + +Assuming you have the postgresql-client installed, you can use the +host-mapped port to test as well. You need to use `docker ps` +to find out what local host port the container is mapped to +first: + + $ docker ps + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 5e24362f27f6 eg_postgresql:latest /usr/lib/postgresql/ About an hour ago Up About an hour 0.0.0.0:49153->5432/tcp pg_test + $ psql -h localhost -p 49153 -d docker -U docker --password + +### Testing the database + +Once you have authenticated and have a `docker =#` +prompt, you can create a table and populate it. + + psql (9.3.1) + Type "help" for help. + + $ docker=# CREATE TABLE cities ( + docker(# name varchar(80), + docker(# location point + docker(# ); + CREATE TABLE + $ docker=# INSERT INTO cities VALUES ('San Francisco', '(-194.0, 53.0)'); + INSERT 0 1 + $ docker=# select * from cities; + name | location + ---------------+----------- + San Francisco | (-194,53) + (1 row) + +### Using the container volumes + +You can use the defined volumes to inspect the PostgreSQL log files and +to backup your configuration and data: + + $ docker run --rm --volumes-from pg_test -t -i busybox sh + + / # ls + bin etc lib linuxrc mnt proc run sys usr + dev home lib64 media opt root sbin tmp var + / # ls /etc/postgresql/9.3/main/ + environment pg_hba.conf postgresql.conf + pg_ctl.conf pg_ident.conf start.conf + /tmp # ls /var/log + ldconfig postgresql diff --git a/docs/examples/running_redis_service.md b/docs/examples/running_redis_service.md new file mode 100644 index 00000000..82daaa78 --- /dev/null +++ b/docs/examples/running_redis_service.md @@ -0,0 +1,89 @@ + + +# Dockerizing a Redis service + +Very simple, no frills, Redis service attached to a web application +using a link. + +## Create a Docker container for Redis + +Firstly, we create a `Dockerfile` for our new Redis +image. + + FROM ubuntu:14.04 + RUN apt-get update && apt-get install -y redis-server + EXPOSE 6379 + ENTRYPOINT ["/usr/bin/redis-server"] + +Next we build an image from our `Dockerfile`. +Replace `` with your own user name. + + $ docker build -t /redis . + +## Run the service + +Use the image we've just created and name your container `redis`. + +Running the service with `-d` runs the container in detached mode, leaving +the container running in the background. + +Importantly, we're not exposing any ports on our container. Instead +we're going to use a container link to provide access to our Redis +database. + + $ docker run --name redis -d /redis + +## Create your web application container + +Next we can create a container for our application. We're going to use +the `-link` flag to create a link to the `redis` container we've just +created with an alias of `db`. This will create a secure tunnel to the +`redis` container and expose the Redis instance running inside that +container to only this container. + + $ docker run --link redis:db -i -t ubuntu:14.04 /bin/bash + +Once inside our freshly created container we need to install Redis to +get the `redis-cli` binary to test our connection. + + $ sudo apt-get update + $ sudo apt-get install redis-server + $ sudo service redis-server stop + +As we've used the `--link redis:db` option, Docker +has created some environment variables in our web application container. + + $ env | grep DB_ + + # Should return something similar to this with your values + DB_NAME=/violet_wolf/db + DB_PORT_6379_TCP_PORT=6379 + DB_PORT=tcp://172.17.0.33:6379 + DB_PORT_6379_TCP=tcp://172.17.0.33:6379 + DB_PORT_6379_TCP_ADDR=172.17.0.33 + DB_PORT_6379_TCP_PROTO=tcp + +We can see that we've got a small list of environment variables prefixed +with `DB`. The `DB` comes from the link alias specified when we launched +the container. Let's use the `DB_PORT_6379_TCP_ADDR` variable to connect to +our Redis container. + + $ redis-cli -h $DB_PORT_6379_TCP_ADDR + $ redis 172.17.0.33:6379> + $ redis 172.17.0.33:6379> set docker awesome + OK + $ redis 172.17.0.33:6379> get docker + "awesome" + $ redis 172.17.0.33:6379> exit + +We could easily use this or other environment variables in our web +application to make a connection to our `redis` +container. diff --git a/docs/examples/running_riak_service.Dockerfile b/docs/examples/running_riak_service.Dockerfile new file mode 100644 index 00000000..9b82cb02 --- /dev/null +++ b/docs/examples/running_riak_service.Dockerfile @@ -0,0 +1,31 @@ +# Riak +# +# VERSION 0.1.1 + +# Use the Ubuntu base image provided by dotCloud +FROM ubuntu:trusty +MAINTAINER Hector Castro hector@basho.com + +# Install Riak repository before we do apt-get update, so that update happens +# in a single step +RUN apt-get install -q -y curl && \ + curl -fsSL https://packagecloud.io/install/repositories/basho/riak/script.deb | sudo bash + +# Install and setup project dependencies +RUN apt-get update && \ + apt-get install -y supervisor riak=2.0.5-1 + +RUN mkdir -p /var/log/supervisor + +RUN locale-gen en_US en_US.UTF-8 + +COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf + +# Configure Riak to accept connections from any host +RUN sed -i "s|listener.http.internal = 127.0.0.1:8098|listener.http.internal = 0.0.0.0:8098|" /etc/riak/riak.conf +RUN sed -i "s|listener.protobuf.internal = 127.0.0.1:8087|listener.protobuf.internal = 0.0.0.0:8087|" /etc/riak/riak.conf + +# Expose Riak Protocol Buffers and HTTP interfaces +EXPOSE 8087 8098 + +CMD ["/usr/bin/supervisord"] diff --git a/docs/examples/running_riak_service.md b/docs/examples/running_riak_service.md new file mode 100644 index 00000000..f17969fe --- /dev/null +++ b/docs/examples/running_riak_service.md @@ -0,0 +1,108 @@ + + +# Dockerizing a Riak service + +The goal of this example is to show you how to build a Docker image with +Riak pre-installed. + +## Creating a Dockerfile + +Create an empty file called `Dockerfile`: + + $ touch Dockerfile + +Next, define the parent image you want to use to build your image on top +of. We'll use [Ubuntu](https://hub.docker.com/_/ubuntu/) (tag: +`trusty`), which is available on [Docker Hub](https://hub.docker.com): + + # Riak + # + # VERSION 0.1.1 + + # Use the Ubuntu base image provided by dotCloud + FROM ubuntu:trusty + MAINTAINER Hector Castro hector@basho.com + +After that, we install the curl which is used to download the repository setup +script and we download the setup script and run it. + + # Install Riak repository before we do apt-get update, so that update happens + # in a single step + RUN apt-get install -q -y curl && \ + curl -fsSL https://packagecloud.io/install/repositories/basho/riak/script.deb | sudo bash + +Then we install and setup a few dependencies: + + - `supervisor` is used manage the Riak processes + - `riak=2.0.5-1` is the Riak package coded to version 2.0.5 + + + + # Install and setup project dependencies + RUN apt-get update && \ + apt-get install -y supervisor riak=2.0.5-1 + + RUN mkdir -p /var/log/supervisor + + RUN locale-gen en_US en_US.UTF-8 + + COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf + +After that, we modify Riak's configuration: + + # Configure Riak to accept connections from any host + RUN sed -i "s|listener.http.internal = 127.0.0.1:8098|listener.http.internal = 0.0.0.0:8098|" /etc/riak/riak.conf + RUN sed -i "s|listener.protobuf.internal = 127.0.0.1:8087|listener.protobuf.internal = 0.0.0.0:8087|" /etc/riak/riak.conf + +Then, we expose the Riak Protocol Buffers and HTTP interfaces: + + # Expose Riak Protocol Buffers and HTTP interfaces + EXPOSE 8087 8098 + +Finally, run `supervisord` so that Riak is started: + + CMD ["/usr/bin/supervisord"] + +## Create a supervisord configuration file + +Create an empty file called `supervisord.conf`. Make +sure it's at the same directory level as your `Dockerfile`: + + touch supervisord.conf + +Populate it with the following program definitions: + + [supervisord] + nodaemon=true + + [program:riak] + command=bash -c "/usr/sbin/riak console" + numprocs=1 + autostart=true + autorestart=true + user=riak + environment=HOME="/var/lib/riak" + stdout_logfile=/var/log/supervisor/%(program_name)s.log + stderr_logfile=/var/log/supervisor/%(program_name)s.log + +## Build the Docker image for Riak + +Now you should be able to build a Docker image for Riak: + + $ docker build -t "/riak" . + +## Next steps + +Riak is a distributed database. Many production deployments consist of +[at least five nodes]( +http://basho.com/why-your-riak-cluster-should-have-at-least-five-nodes/). +See the [docker-riak](https://github.com/hectcastro/docker-riak) project +details on how to deploy a Riak cluster using Docker and Pipework. diff --git a/docs/examples/running_ssh_service.Dockerfile b/docs/examples/running_ssh_service.Dockerfile new file mode 100644 index 00000000..7aba7f68 --- /dev/null +++ b/docs/examples/running_ssh_service.Dockerfile @@ -0,0 +1,20 @@ +# sshd +# +# VERSION 0.0.2 + +FROM ubuntu:14.04 +MAINTAINER Sven Dowideit + +RUN apt-get update && apt-get install -y openssh-server +RUN mkdir /var/run/sshd +RUN echo 'root:screencast' | chpasswd +RUN sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config + +# SSH login fix. Otherwise user is kicked off after login +RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd + +ENV NOTVISIBLE "in users profile" +RUN echo "export VISIBLE=now" >> /etc/profile + +EXPOSE 22 +CMD ["/usr/sbin/sshd", "-D"] diff --git a/docs/examples/running_ssh_service.md b/docs/examples/running_ssh_service.md new file mode 100644 index 00000000..284a5394 --- /dev/null +++ b/docs/examples/running_ssh_service.md @@ -0,0 +1,84 @@ + + +# Dockerizing an SSH daemon service + +## Build an `eg_sshd` image + +The following `Dockerfile` sets up an SSHd service in a container that you +can use to connect to and inspect other container's volumes, or to get +quick access to a test container. + + # sshd + # + # VERSION 0.0.2 + + FROM ubuntu:14.04 + MAINTAINER Sven Dowideit + + RUN apt-get update && apt-get install -y openssh-server + RUN mkdir /var/run/sshd + RUN echo 'root:screencast' | chpasswd + RUN sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config + + # SSH login fix. Otherwise user is kicked off after login + RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd + + ENV NOTVISIBLE "in users profile" + RUN echo "export VISIBLE=now" >> /etc/profile + + EXPOSE 22 + CMD ["/usr/sbin/sshd", "-D"] + +Build the image using: + + $ docker build -t eg_sshd . + +## Run a `test_sshd` container + +Then run it. You can then use `docker port` to find out what host port +the container's port 22 is mapped to: + + $ docker run -d -P --name test_sshd eg_sshd + $ docker port test_sshd 22 + 0.0.0.0:49154 + +And now you can ssh as `root` on the container's IP address (you can find it +with `docker inspect`) or on port `49154` of the Docker daemon's host IP address +(`ip address` or `ifconfig` can tell you that) or `localhost` if on the +Docker daemon host: + + $ ssh root@192.168.1.2 -p 49154 + # The password is ``screencast``. + $$ + +## Environment variables + +Using the `sshd` daemon to spawn shells makes it complicated to pass environment +variables to the user's shell via the normal Docker mechanisms, as `sshd` scrubs +the environment before it starts the shell. + +If you're setting values in the `Dockerfile` using `ENV`, you'll need to push them +to a shell initialization file like the `/etc/profile` example in the `Dockerfile` +above. + +If you need to pass`docker run -e ENV=value` values, you will need to write a +short script to do the same before you start `sshd -D` and then replace the +`CMD` with that script. + +## Clean up + +Finally, clean up after your test by stopping and removing the +container, and then removing the image. + + $ docker stop test_sshd + $ docker rm test_sshd + $ docker rmi eg_sshd + diff --git a/docs/examples/supervisord.conf b/docs/examples/supervisord.conf new file mode 100644 index 00000000..385fbe7a --- /dev/null +++ b/docs/examples/supervisord.conf @@ -0,0 +1,12 @@ +[supervisord] +nodaemon=true + +[program:riak] +command=bash -c "/usr/sbin/riak console" +numprocs=1 +autostart=true +autorestart=true +user=riak +environment=HOME="/var/lib/riak" +stdout_logfile=/var/log/supervisor/%(program_name)s.log +stderr_logfile=/var/log/supervisor/%(program_name)s.log diff --git a/docs/extend/images/authz_additional_info.png b/docs/extend/images/authz_additional_info.png new file mode 100644 index 0000000000000000000000000000000000000000..1a6a6d01d2048fcb975b7d2b025cdfabd4c4f5f3 GIT binary patch literal 45916 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 Ol5(_nwX52}O8Ou06wbc@ literal 0 HcmV?d00001 diff --git a/docs/extend/images/authz_allow.png b/docs/extend/images/authz_allow.png new file mode 100644 index 0000000000000000000000000000000000000000..f42108040bbbf9facb9fff0c320428323f061e10 GIT binary patch literal 33505 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 AiU0rr literal 0 HcmV?d00001 diff --git a/docs/extend/images/authz_connection_hijack.png b/docs/extend/images/authz_connection_hijack.png new file mode 100644 index 0000000000000000000000000000000000000000..f13a2987b28d70f81bda7096a54ec479f20b2690 GIT binary patch literal 38780 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 literal 0 HcmV?d00001 diff --git a/docs/extend/images/authz_deny.png b/docs/extend/images/authz_deny.png new file mode 100644 index 0000000000000000000000000000000000000000..fa4a48584abb3db280b8226d18888cb0539de89d GIT binary patch literal 27099 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 z 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 ++++ +title = "Extend Engine" +description = "How to extend Docker Engine with plugins" +keywords = ["extend, plugins, docker, documentation, developer"] +[menu.main] +identifier = "engine_extend" +parent = "engine_use" +weight = 6 ++++ + + + +## Extending Docker Engine + +Currently, you can extend Docker Engine by adding a plugin. This section contains the following topics: + +* [Understand Docker plugins](plugins.md) +* [Write a volume plugin](plugins_volume.md) +* [Write a network plugin](plugins_network.md) +* [Write an authorization plugin](plugins_authorization.md) +* [Docker plugin API](plugin_api.md) diff --git a/docs/extend/plugin_api.md b/docs/extend/plugin_api.md new file mode 100644 index 00000000..a799a135 --- /dev/null +++ b/docs/extend/plugin_api.md @@ -0,0 +1,188 @@ + + +# Docker Plugin API + +Docker plugins are out-of-process extensions which add capabilities to the +Docker Engine. + +This page is intended for people who want to develop their own Docker plugin. +If you just want to learn about or use Docker plugins, look +[here](plugins.md). + +## What plugins are + +A plugin is a process running on the same or a different host as the docker daemon, +which registers itself by placing a file on the same docker host in one of the plugin +directories described in [Plugin discovery](#plugin-discovery). + +Plugins have human-readable names, which are short, lowercase strings. For +example, `flocker` or `weave`. + +Plugins can run inside or outside containers. Currently running them outside +containers is recommended. + +## Plugin discovery + +Docker discovers plugins by looking for them in the plugin directory whenever a +user or container tries to use one by name. + +There are three types of files which can be put in the plugin directory. + +* `.sock` files are UNIX domain sockets. +* `.spec` files are text files containing a URL, such as `unix:///other.sock` or `tcp://localhost:8080`. +* `.json` files are text files containing a full json specification for the plugin. + +Plugins with UNIX domain socket files must run on the same docker host, whereas +plugins with spec or json files can run on a different host if a remote URL is specified. + +UNIX domain socket files must be located under `/run/docker/plugins`, whereas +spec files can be located either under `/etc/docker/plugins` or `/usr/lib/docker/plugins`. + +The name of the file (excluding the extension) determines the plugin name. + +For example, the `flocker` plugin might create a UNIX socket at +`/run/docker/plugins/flocker.sock`. + +You can define each plugin into a separated subdirectory if you want to isolate definitions from each other. +For example, you can create the `flocker` socket under `/run/docker/plugins/flocker/flocker.sock` and only +mount `/run/docker/plugins/flocker` inside the `flocker` container. + +Docker always searches for unix sockets in `/run/docker/plugins` first. It checks for spec or json files under +`/etc/docker/plugins` and `/usr/lib/docker/plugins` if the socket doesn't exist. The directory scan stops as +soon as it finds the first plugin definition with the given name. + +### JSON specification + +This is the JSON format for a plugin: + +```json +{ + "Name": "plugin-example", + "Addr": "https://example.com/docker/plugin", + "TLSConfig": { + "InsecureSkipVerify": false, + "CAFile": "/usr/shared/docker/certs/example-ca.pem", + "CertFile": "/usr/shared/docker/certs/example-cert.pem", + "KeyFile": "/usr/shared/docker/certs/example-key.pem", + } +} +``` + +The `TLSConfig` field is optional and TLS will only be verified if this configuration is present. + +## Plugin lifecycle + +Plugins should be started before Docker, and stopped after Docker. For +example, when packaging a plugin for a platform which supports `systemd`, you +might use [`systemd` dependencies]( +http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Before=) to +manage startup and shutdown order. + +When upgrading a plugin, you should first stop the Docker daemon, upgrade the +plugin, then start Docker again. + +## Plugin activation + +When a plugin is first referred to -- either by a user referring to it by name +(e.g. `docker run --volume-driver=foo`) or a container already configured to +use a plugin being started -- Docker looks for the named plugin in the plugin +directory and activates it with a handshake. See Handshake API below. + +Plugins are *not* activated automatically at Docker daemon startup. Rather, +they are activated only lazily, or on-demand, when they are needed. + +## Systemd socket activation + +Plugins may also be socket activated by `systemd`. The official [Plugins helpers](https://github.com/docker/go-plugins-helpers) +natively supports socket activation. In order for a plugin to be socket activated it needs +a `service` file and a `socket` file. + +The `service` file (for example `/lib/systemd/system/your-plugin.service`): + +``` +[Unit] +Description=Your plugin +Before=docker.service +After=network.target your-plugin.socket +Requires=your-plugin.socket docker.service + +[Service] +ExecStart=/usr/lib/docker/your-plugin + +[Install] +WantedBy=multi-user.target +``` +The `socket` file (for example `/lib/systemd/system/your-plugin.socket`): +``` +[Unit] +Description=Your plugin + +[Socket] +ListenStream=/run/docker/plugins/your-plugin.sock + +[Install] +WantedBy=sockets.target +``` + +This will allow plugins to be actually started when the Docker daemon connects to +the sockets they're listening on (for instance the first time the daemon uses them +or if one of the plugin goes down accidentally). + +## API design + +The Plugin API is RPC-style JSON over HTTP, much like webhooks. + +Requests flow *from* the Docker daemon *to* the plugin. So the plugin needs to +implement an HTTP server and bind this to the UNIX socket mentioned in the +"plugin discovery" section. + +All requests are HTTP `POST` requests. + +The API is versioned via an Accept header, which currently is always set to +`application/vnd.docker.plugins.v1+json`. + +## Handshake API + +Plugins are activated via the following "handshake" API call. + +### /Plugin.Activate + +**Request:** empty body + +**Response:** +``` +{ + "Implements": ["VolumeDriver"] +} +``` + +Responds with a list of Docker subsystems which this plugin implements. +After activation, the plugin will then be sent events from this subsystem. + +Possible values are: + +* [`authz`](plugins_authorization.md) +* [`NetworkDriver`](plugins_network.md) +* [`VolumeDriver`](plugins_volume.md) + + +## Plugin retries + +Attempts to call a method on a plugin are retried with an exponential backoff +for up to 30 seconds. This may help when packaging plugins as containers, since +it gives plugin containers a chance to start up before failing any user +containers which depend on them. + +## Plugins helpers + +To ease plugins development, we're providing an `sdk` for each kind of plugins +currently supported by Docker at [docker/go-plugins-helpers](https://github.com/docker/go-plugins-helpers). diff --git a/docs/extend/plugins.md b/docs/extend/plugins.md new file mode 100644 index 00000000..da46a16e --- /dev/null +++ b/docs/extend/plugins.md @@ -0,0 +1,115 @@ + + +# Understand Engine plugins + +You can extend the capabilities of the Docker Engine by loading third-party +plugins. This page explains the types of plugins and provides links to several +volume and network plugins for Docker. + +## Types of plugins + +Plugins extend Docker's functionality. They come in specific types. For +example, a [volume plugin](plugins_volume.md) might enable Docker +volumes to persist across multiple Docker hosts and a +[network plugin](plugins_network.md) might provide network plumbing. + +Currently Docker supports volume and network driver plugins. In the future it +will support additional plugin types. + +## Installing a plugin + +Follow the instructions in the plugin's documentation. + +## Finding a plugin + +The following plugins exist: + +* The [Blockbridge plugin](https://github.com/blockbridge/blockbridge-docker-volume) + is a volume plugin that provides access to an extensible set of + container-based persistent storage options. It supports single and multi-host Docker + environments with features that include tenant isolation, automated + provisioning, encryption, secure deletion, snapshots and QoS. + +* The [Convoy plugin](https://github.com/rancher/convoy) is a volume plugin for a + variety of storage back-ends including device mapper and NFS. It's a simple standalone + executable written in Go and provides the framework to support vendor-specific extensions + such as snapshots, backups and restore. + +* The [Flocker plugin](https://clusterhq.com/docker-plugin/) is a volume plugin + which provides multi-host portable volumes for Docker, enabling you to run + databases and other stateful containers and move them around across a cluster + of machines. + +* The [GlusterFS plugin](https://github.com/calavera/docker-volume-glusterfs) is + another volume plugin that provides multi-host volumes management for Docker + using GlusterFS. + +* The [Horcrux Volume Plugin](https://github.com/muthu-r/horcrux) allows on-demand, + version controlled access to your data. Horcrux is an open-source plugin, + written in Go, and supports SCP, [Minio](https://www.minio.io) and Amazon S3. + +* The [IPFS Volume Plugin](http://github.com/vdemeester/docker-volume-ipfs) + is an open source volume plugin that allows using an + [ipfs](https://ipfs.io/) filesystem as a volume. + +* The [Keywhiz plugin](https://github.com/calavera/docker-volume-keywhiz) is + a plugin that provides credentials and secret management using Keywhiz as + a central repository. + +* The [Netshare plugin](https://github.com/gondor/docker-volume-netshare) is a volume plugin + that provides volume management for NFS 3/4, AWS EFS and CIFS file systems. + +* The [OpenStorage Plugin](https://github.com/libopenstorage/openstorage) is a cluster aware volume plugin that provides volume management for file and block storage solutions. It implements a vendor neutral specification for implementing extensions such as CoS, encryption, and snapshots. It has example drivers based on FUSE, NFS, NBD and EBS to name a few. + +* The [Quobyte Volume Plugin](https://github.com/quobyte/docker-volume) connects Docker to [Quobyte](http://www.quobyte.com/containers)'s data center file system, a general-purpose scalable and fault-tolerant storage platform. + +* The [REX-Ray plugin](https://github.com/emccode/rexray) is a volume plugin + which is written in Go and provides advanced storage functionality for many + platforms including VirtualBox, EC2, Google Compute Engine, OpenStack, and EMC. + +* The [Contiv Volume Plugin](https://github.com/contiv/volplugin) is an open + source volume plugin that provides multi-tenant, persistent, distributed storage + with intent based consumption using ceph underneath. + +* The [Contiv Networking](https://github.com/contiv/netplugin) is an open source + libnetwork plugin to provide infrastructure and security policies for a + multi-tenant micro services deployment, while providing an integration to + physical network for non-container workload. Contiv Networking implements the + remote driver and IPAM APIs available in Docker 1.9 onwards. + +* The [Weave Network Plugin](http://docs.weave.works/weave/latest_release/plugin.html) + creates a virtual network that connects your Docker containers - + across multiple hosts or clouds and enables automatic discovery of + applications. Weave networks are resilient, partition tolerant, + secure and work in partially connected networks, and other adverse + environments - all configured with delightful simplicity. + +* The [Kuryr Network Plugin](https://github.com/openstack/kuryr) is + developed as part of the OpenStack Kuryr project and implements the + Docker networking (libnetwork) remote driver API by utilizing + Neutron, the OpenStack networking service. It includes an IPAM + driver as well. + +* The [Local Persist Plugin](https://github.com/CWSpear/local-persist) + extends the default `local` driver's functionality by allowing you specify + a mountpoint anywhere on the host, which enables the files to *always persist*, + even if the volume is removed via `docker volume rm`. + +## Troubleshooting a plugin + +If you are having problems with Docker after loading a plugin, ask the authors +of the plugin for help. The Docker team may not be able to assist you. + +## Writing a plugin + +If you are interested in writing a plugin for Docker, or seeing how they work +under the hood, see the [docker plugins reference](plugin_api.md). diff --git a/docs/extend/plugins_authorization.md b/docs/extend/plugins_authorization.md new file mode 100644 index 00000000..31a6a072 --- /dev/null +++ b/docs/extend/plugins_authorization.md @@ -0,0 +1,254 @@ + + + +# Create an authorization plugin + +Docker's out-of-the-box authorization model is all or nothing. Any user with +permission to access the Docker daemon can run any Docker client command. The +same is true for callers using Docker's remote API to contact the daemon. If you +require greater access control, you can create authorization plugins and add +them to your Docker daemon configuration. Using an authorization plugin, a +Docker administrator can configure granular access policies for managing access +to Docker daemon. + +Anyone with the appropriate skills can develop an authorization plugin. These +skills, at their most basic, are knowledge of Docker, understanding of REST, and +sound programming knowledge. This document describes the architecture, state, +and methods information available to an authorization plugin developer. + +## Basic principles + +Docker's [plugin infrastructure](plugin_api.md) enables +extending Docker by loading, removing and communicating with +third-party components using a generic API. The access authorization subsystem +was built using this mechanism. + +Using this subsystem, you don't need to rebuild the Docker daemon to add an +authorization plugin. You can add a plugin to an installed Docker daemon. You do +need to restart the Docker daemon to add a new plugin. + +An authorization plugin approves or denies requests to the Docker daemon based +on both the current authentication context and the command context. The +authentication context contains all user details and the authentication method. +The command context contains all the relevant request data. + +Authorization plugins must follow the rules described in [Docker Plugin API](plugin_api.md). +Each plugin must reside within directories described under the +[Plugin discovery](plugin_api.md#plugin-discovery) section. + +**Note**: the abbreviations `AuthZ` and `AuthN` mean authorization and authentication +respectively. + +## Basic architecture + +You are responsible for registering your plugin as part of the Docker daemon +startup. You can install multiple plugins and chain them together. This chain +can be ordered. Each request to the daemon passes in order through the chain. +Only when all the plugins grant access to the resource, is the access granted. + +When an HTTP request is made to the Docker daemon through the CLI or via the +remote API, the authentication subsystem passes the request to the installed +authentication plugin(s). The request contains the user (caller) and command +context. The plugin is responsible for deciding whether to allow or deny the +request. + +The sequence diagrams below depict an allow and deny authorization flow: + +![Authorization Allow flow](images/authz_allow.png) + +![Authorization Deny flow](images/authz_deny.png) + +Each request sent to the plugin includes the authenticated user, the HTTP +headers, and the request/response body. Only the user name and the +authentication method used are passed to the plugin. Most importantly, no user +credentials or tokens are passed. Finally, not all request/response bodies +are sent to the authorization plugin. Only those request/response bodies where +the `Content-Type` is either `text/*` or `application/json` are sent. + +For commands that can potentially hijack the HTTP connection (`HTTP +Upgrade`), such as `exec`, the authorization plugin is only called for the +initial HTTP requests. Once the plugin approves the command, authorization is +not applied to the rest of the flow. Specifically, the streaming data is not +passed to the authorization plugins. For commands that return chunked HTTP +response, such as `logs` and `events`, only the HTTP request is sent to the +authorization plugins. + +During request/response processing, some authorization flows might +need to do additional queries to the Docker daemon. To complete such flows, +plugins can call the daemon API similar to a regular user. To enable these +additional queries, the plugin must provide the means for an administrator to +configure proper authentication and security policies. + +## Docker client flows + +To enable and configure the authorization plugin, the plugin developer must +support the Docker client interactions detailed in this section. + +### Setting up Docker daemon + +Enable the authorization plugin with a dedicated command line flag in the +`--authorization-plugin=PLUGIN_ID` format. The flag supplies a `PLUGIN_ID` +value. This value can be the plugin’s socket or a path to a specification file. + +```bash +$ docker daemon --authorization-plugin=plugin1 --authorization-plugin=plugin2,... +``` + +Docker's authorization subsystem supports multiple `--authorization-plugin` parameters. + +### Calling authorized command (allow) + +```bash +$ docker pull centos +... +f1b10cd84249: Pull complete +... +``` + +### Calling unauthorized command (deny) + +```bash +$ docker pull centos +... +docker: Error response from daemon: authorization denied by plugin PLUGIN_NAME: volumes are not allowed. +``` + +### Error from plugins + +```bash +$ docker pull centos +... +docker: Error response from daemon: plugin PLUGIN_NAME failed with error: AuthZPlugin.AuthZReq: Cannot connect to the Docker daemon. Is the docker daemon running on this host?. +``` + +## API schema and implementation + +In addition to Docker's standard plugin registration method, each plugin +should implement the following two methods: + +* `/AuthzPlugin.AuthZReq` This authorize request method is called before the Docker daemon processes the client request. + +* `/AuthzPlugin.AuthZRes` This authorize response method is called before the response is returned from Docker daemon to the client. + +#### /AuthzPlugin.AuthZReq + +**Request**: + +```json +{ + "User": "The user identification", + "UserAuthNMethod": "The authentication method used", + "RequestMethod": "The HTTP method", + "RequestURI": "The HTTP request URI", + "RequestBody": "Byte array containing the raw HTTP request body", + "RequestHeader": "Byte array containing the raw HTTP request header as a map[string][]string ", + "RequestStatusCode": "Request status code" +} +``` + +**Response**: + +```json +{ + "Allow": "Determined whether the user is allowed or not", + "Msg": "The authorization message", + "Err": "The error message if things go wrong" +} +``` +#### /AuthzPlugin.AuthZRes + +**Request**: + +```json +{ + "User": "The user identification", + "UserAuthNMethod": "The authentication method used", + "RequestMethod": "The HTTP method", + "RequestURI": "The HTTP request URI", + "RequestBody": "Byte array containing the raw HTTP request body", + "RequestHeader": "Byte array containing the raw HTTP request header as a map[string][]string", + "RequestStatusCode": "Request status code", + "ResponseBody": "Byte array containing the raw HTTP response body", + "ResponseHeader": "Byte array containing the raw HTTP response header as a map[string][]string", + "ResponseStatusCode":"Response status code" +} +``` + +**Response**: + +```json +{ + "Allow": "Determined whether the user is allowed or not", + "Msg": "The authorization message", + "Err": "The error message if things go wrong", + "ModifiedBody": "Byte array containing a modified body of the raw HTTP body (or nil if no changes required)", + "ModifiedHeader": "Byte array containing a modified header of the HTTP response (or nil if no changes required)", + "ModifiedStatusCode": "int containing the modified version of the status code (or 0 if not change is required)" +} +``` + +The modified response enables the authorization plugin to manipulate the content +of the HTTP response. In case of more than one plugin, each subsequent plugin +receives a response (optionally) modified by a previous plugin. + +### Request authorization + +Each plugin must support two request authorization messages formats, one from the daemon to the plugin and then from the plugin to the daemon. The tables below detail the content expected in each message. + +#### Daemon -> Plugin + +Name | Type | Description +-----------------------|-------------------|------------------------------------------------------- +User | string | The user identification +Authentication method | string | The authentication method used +Request method | enum | The HTTP method (GET/DELETE/POST) +Request URI | string | The HTTP request URI including API version (e.g., v.1.17/containers/json) +Request headers | map[string]string | Request headers as key value pairs (without the authorization header) +Request body | []byte | Raw request body + + +#### Plugin -> Daemon + +Name | Type | Description +--------|--------|---------------------------------------------------------------------------------- +Allow | bool | Boolean value indicating whether the request is allowed or denied +Msg | string | Authorization message (will be returned to the client in case the access is denied) +Err | string | Error message (will be returned to the client in case the plugin encounter an error. The string value supplied may appear in logs, so should not include confidential information) + +### Response authorization + +The plugin must support two authorization messages formats, one from the daemon to the plugin and then from the plugin to the daemon. The tables below detail the content expected in each message. + +#### Daemon -> Plugin + + +Name | Type | Description +----------------------- |------------------ |---------------------------------------------------- +User | string | The user identification +Authentication method | string | The authentication method used +Request method | string | The HTTP method (GET/DELETE/POST) +Request URI | string | The HTTP request URI including API version (e.g., v.1.17/containers/json) +Request headers | map[string]string | Request headers as key value pairs (without the authorization header) +Request body | []byte | Raw request body +Response status code | int | Status code from the docker daemon +Response headers | map[string]string | Response headers as key value pairs +Response body | []byte | Raw docker daemon response body + + +#### Plugin -> Daemon + +Name | Type | Description +--------|--------|---------------------------------------------------------------------------------- +Allow | bool | Boolean value indicating whether the response is allowed or denied +Msg | string | Authorization message (will be returned to the client in case the access is denied) +Err | string | Error message (will be returned to the client in case the plugin encounter an error. The string value supplied may appear in logs, so should not include confidential information) diff --git a/docs/extend/plugins_network.md b/docs/extend/plugins_network.md new file mode 100644 index 00000000..ac072732 --- /dev/null +++ b/docs/extend/plugins_network.md @@ -0,0 +1,57 @@ + + +# Engine network driver plugins + +Docker Engine network plugins enable Engine deployments to be extended to +support a wide range of networking technologies, such as VXLAN, IPVLAN, MACVLAN +or something completely different. Network driver plugins are supported via the +LibNetwork project. Each plugin is implemented asa "remote driver" for +LibNetwork, which shares plugin infrastructure with Engine. Effectively, network +driver plugins are activated in the same way as other plugins, and use the same +kind of protocol. + +## Using network driver plugins + +The means of installing and running a network driver plugin depend on the +particular plugin. So, be sure to install your plugin according to the +instructions obtained from the plugin developer. + +Once running however, network driver plugins are used just like the built-in +network drivers: by being mentioned as a driver in network-oriented Docker +commands. For example, + + $ docker network create --driver weave mynet + +Some network driver plugins are listed in [plugins](plugins.md) + +The `mynet` network is now owned by `weave`, so subsequent commands +referring to that network will be sent to the plugin, + + $ docker run --net=mynet busybox top + + +## Write a network plugin + +Network plugins implement the [Docker plugin +API](https://docs.docker.com/extend/plugin_api/) and the network plugin protocol + +## Network plugin protocol + +The network driver protocol, in addition to the plugin activation call, is +documented as part of libnetwork: +[https://github.com/docker/libnetwork/blob/master/docs/remote.md](https://github.com/docker/libnetwork/blob/master/docs/remote.md). + +# Related Information + +To interact with the Docker maintainers and other interested users, see the IRC channel `#docker-network`. + +- [Docker networks feature overview](../userguide/networking/index.md) +- The [LibNetwork](https://github.com/docker/libnetwork) project diff --git a/docs/extend/plugins_volume.md b/docs/extend/plugins_volume.md new file mode 100644 index 00000000..cb1bebf5 --- /dev/null +++ b/docs/extend/plugins_volume.md @@ -0,0 +1,216 @@ + + +# Write a volume plugin + +Docker Engine volume plugins enable Engine deployments to be integrated with +external storage systems, such as Amazon EBS, and enable data volumes to persist +beyond the lifetime of a single Engine host. See the [plugin +documentation](plugins.md) for more information. + +## Command-line changes + +A volume plugin makes use of the `-v`and `--volume-driver` flag on the `docker run` command. The `-v` flag accepts a volume name and the `--volume-driver` flag a driver type, for example: + + $ docker run -ti -v volumename:/data --volume-driver=flocker busybox sh + +This command passes the `volumename` through to the volume plugin as a +user-given name for the volume. The `volumename` must not begin with a `/`. + +By having the user specify a `volumename`, a plugin can associate the volume +with an external volume beyond the lifetime of a single container or container +host. This can be used, for example, to move a stateful container from one +server to another. + +By specifying a `volumedriver` in conjunction with a `volumename`, users can use plugins such as [Flocker](https://clusterhq.com/docker-plugin/) to manage volumes external to a single host, such as those on EBS. + + +## Create a VolumeDriver + +The container creation endpoint (`/containers/create`) accepts a `VolumeDriver` +field of type `string` allowing to specify the name of the driver. It's default +value of `"local"` (the default driver for local volumes). + +## Volume plugin protocol + +If a plugin registers itself as a `VolumeDriver` when activated, then it is +expected to provide writeable paths on the host filesystem for the Docker +daemon to provide to containers to consume. + +The Docker daemon handles bind-mounting the provided paths into user +containers. + +> **Note**: Volume plugins should *not* write data to the `/var/lib/docker/` +> directory, including `/var/lib/docker/volumes`. The `/var/lib/docker/` +> directory is reserved for Docker. + +### /VolumeDriver.Create + +**Request**: +```json +{ + "Name": "volume_name", + "Opts": {} +} +``` + +Instruct the plugin that the user wants to create a volume, given a user +specified volume name. The plugin does not need to actually manifest the +volume on the filesystem yet (until Mount is called). +Opts is a map of driver specific options passed through from the user request. + +**Response**: +```json +{ + "Err": "" +} +``` + +Respond with a string error if an error occurred. + +### /VolumeDriver.Remove + +**Request**: +```json +{ + "Name": "volume_name" +} +``` + +Delete the specified volume from disk. This request is issued when a user invokes `docker rm -v` to remove volumes associated with a container. + +**Response**: +```json +{ + "Err": "" +} +``` + +Respond with a string error if an error occurred. + +### /VolumeDriver.Mount + +**Request**: +```json +{ + "Name": "volume_name" +} +``` + +Docker requires the plugin to provide a volume, given a user specified volume +name. This is called once per container start. If the same volume_name is requested +more than once, the plugin may need to keep track of each new mount request and provision +at the first mount request and deprovision at the last corresponding unmount request. + +**Response**: +```json +{ + "Mountpoint": "/path/to/directory/on/host", + "Err": "" +} +``` + +Respond with the path on the host filesystem where the volume has been made +available, and/or a string error if an error occurred. + +### /VolumeDriver.Path + +**Request**: +```json +{ + "Name": "volume_name" +} +``` + +Docker needs reminding of the path to the volume on the host. + +**Response**: +```json +{ + "Mountpoint": "/path/to/directory/on/host", + "Err": "" +} +``` + +Respond with the path on the host filesystem where the volume has been made +available, and/or a string error if an error occurred. + +### /VolumeDriver.Unmount + +**Request**: +```json +{ + "Name": "volume_name" +} +``` + +Indication that Docker no longer is using the named volume. This is called once +per container stop. Plugin may deduce that it is safe to deprovision it at +this point. + +**Response**: +```json +{ + "Err": "" +} +``` + +Respond with a string error if an error occurred. + + +### /VolumeDriver.Get + +**Request**: +```json +{ + "Name": "volume_name" +} +``` + +Get the volume info. + + +**Response**: +```json +{ + "Volume": { + "Name": "volume_name", + "Mountpoint": "/path/to/directory/on/host", + }, + "Err": "" +} +``` + +Respond with a string error if an error occurred. + + +### /VolumeDriver.List + +**Request**: +```json +{} +``` + +Get the list of volumes registered with the plugin. + +**Response**: +```json +{ + "Volumes": [ + { + "Name": "volume_name", + "Mountpoint": "/path/to/directory/on/host" + } + ], + "Err": "" +} +``` + +Respond with a string error if an error occurred. diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 00000000..b017557c --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,294 @@ + + +# Frequently Asked Questions (FAQ) + +If you don't see your question here, feel free to submit new ones to +. Or, you can fork [the +repo](https://github.com/docker/docker) and contribute them yourself by editing +the documentation sources. + + +### How much does Engine cost? + +Docker Engine is 100% free. It is open source, so you can use it without paying. + +### What open source license are you using? + +We are using the Apache License Version 2.0, see it here: +[https://github.com/docker/docker/blob/master/LICENSE]( +https://github.com/docker/docker/blob/master/LICENSE) + +### Does Docker run on Mac OS X or Windows? + +Docker Engine currently runs only on Linux, but you can use VirtualBox to run +Engine in a virtual machine on your box, and get the best of both worlds. Check +out the [*Mac OS X*](installation/mac.md) and [*Microsoft +Windows*](installation/windows.md) installation guides. The small Linux +distribution boot2docker can be set up using the Docker Machine tool to be run +inside virtual machines on these two operating systems. + +>**Note:** if you are using a remote Docker Engine daemon on a VM through Docker +>Machine, then _do not_ type the `sudo` before the `docker` commands shown in +>the documentation's examples. + +### How do containers compare to virtual machines? + +They are complementary. VMs are best used to allocate chunks of hardware +resources. Containers operate at the process level, which makes them very +lightweight and perfect as a unit of software delivery. + +### What does Docker technology add to just plain LXC? + +Docker technology is not a replacement for LXC. "LXC" refers to capabilities of +the Linux kernel (specifically namespaces and control groups) which allow +sandboxing processes from one another, and controlling their resource +allocations. On top of this low-level foundation of kernel features, Docker +offers a high-level tool with several powerful functionalities: + + - *Portable deployment across machines.* Docker defines a format for bundling + an application and all its dependencies into a single object which can be + transferred to any Docker-enabled machine, and executed there with the + guarantee that the execution environment exposed to the application will be the + same. LXC implements process sandboxing, which is an important pre-requisite + for portable deployment, but that alone is not enough for portable deployment. + If you sent me a copy of your application installed in a custom LXC + configuration, it would almost certainly not run on my machine the way it does + on yours, because it is tied to your machine's specific configuration: + networking, storage, logging, distro, etc. Docker defines an abstraction for + these machine-specific settings, so that the exact same Docker container can + run - unchanged - on many different machines, with many different + configurations. + + - *Application-centric.* Docker is optimized for the deployment of + applications, as opposed to machines. This is reflected in its API, user + interface, design philosophy and documentation. By contrast, the `lxc` helper + scripts focus on containers as lightweight machines - basically servers that + boot faster and need less RAM. We think there's more to containers than just + that. + + - *Automatic build.* Docker includes [*a tool for developers to automatically + assemble a container from their source + code*](reference/builder.md), with full control over application + dependencies, build tools, packaging etc. They are free to use `make`, `maven`, + `chef`, `puppet`, `salt,` Debian packages, RPMs, source tarballs, or any + combination of the above, regardless of the configuration of the machines. + + - *Versioning.* Docker includes git-like capabilities for tracking successive + versions of a container, inspecting the diff between versions, committing new + versions, rolling back etc. The history also includes how a container was + assembled and by whom, so you get full traceability from the production server + all the way back to the upstream developer. Docker also implements incremental + uploads and downloads, similar to `git pull`, so new versions of a container + can be transferred by only sending diffs. + + - *Component re-use.* Any container can be used as a [*"base image"*](reference/glossary.md#image) to create more specialized components. This can + be done manually or as part of an automated build. For example you can prepare + the ideal Python environment, and use it as a base for 10 different + applications. Your ideal PostgreSQL setup can be re-used for all your future + projects. And so on. + + - *Sharing.* Docker has access to a public registry [on Docker Hub](https://hub.docker.com/) + where thousands of people have uploaded useful images: anything from Redis, + CouchDB, PostgreSQL to IRC bouncers to Rails app servers to Hadoop to base + images for various Linux distros. The + [*registry*](https://docs.docker.com/registry/) also + includes an official "standard library" of useful containers maintained by the + Docker team. The registry itself is open-source, so anyone can deploy their own + registry to store and transfer private containers, for internal server + deployments for example. + + - *Tool ecosystem.* Docker defines an API for automating and customizing the + creation and deployment of containers. There are a huge number of tools + integrating with Docker to extend its capabilities. PaaS-like deployment + (Dokku, Deis, Flynn), multi-node orchestration (Maestro, Salt, Mesos, Openstack + Nova), management dashboards (docker-ui, Openstack Horizon, Shipyard), + configuration management (Chef, Puppet), continuous integration (Jenkins, + Strider, Travis), etc. Docker is rapidly establishing itself as the standard + for container-based tooling. + +### What is different between a Docker container and a VM? + +There's a great StackOverflow answer [showing the differences]( +http://stackoverflow.com/questions/16047306/how-is-docker-io-different-from-a-normal-virtual-machine). + +### Do I lose my data when the container exits? + +Not at all! Any data that your application writes to disk gets preserved in its +container until you explicitly delete the container. The file system for the +container persists even after the container halts. + +### How far do Docker containers scale? + +Some of the largest server farms in the world today are based on containers. +Large web deployments like Google and Twitter, and platform providers such as +Heroku and dotCloud all run on container technology, at a scale of hundreds of +thousands or even millions of containers running in parallel. + +### How do I connect Docker containers? + +Currently the recommended way to connect containers is via the Docker network feature. You can see details of how to [work with Docker networks here](userguide/networking/work-with-networks.md). + +Also useful for more flexible service portability is the [Ambassador linking +pattern](admin/ambassador_pattern_linking.md). + +### How do I run more than one process in a Docker container? + +Any capable process supervisor such as [http://supervisord.org/]( +http://supervisord.org/), runit, s6, or daemontools can do the trick. Docker +will start up the process management daemon which will then fork to run +additional processes. As long as the processor manager daemon continues to run, +the container will continue to as well. You can see a more substantial example +[that uses supervisord here](admin/using_supervisord.md). + +### What platforms does Docker run on? + +Linux: + + - Ubuntu 12.04, 13.04 et al + - Fedora 19/20+ + - RHEL 6.5+ + - CentOS 6+ + - Gentoo + - ArchLinux + - openSUSE 12.3+ + - CRUX 3.0+ + +Cloud: + + - Amazon EC2 + - Google Compute Engine + - Microsoft Azure + - Rackspace + +### How do I report a security issue with Docker? + +You can learn about the project's security policy +[here](https://www.docker.com/security/) and report security issues to this +[mailbox](mailto:security@docker.com). + +### Why do I need to sign my commits to Docker with the DCO? + +Please read [our blog post]( +http://blog.docker.com/2014/01/docker-code-contributions-require-developer-certificate-of-origin/) on the introduction of the DCO. + +### When building an image, should I prefer system libraries or bundled ones? + +*This is a summary of a discussion on the [docker-dev mailing list]( +https://groups.google.com/forum/#!topic/docker-dev/L2RBSPDu1L0).* + +Virtually all programs depend on third-party libraries. Most frequently, they +will use dynamic linking and some kind of package dependency, so that when +multiple programs need the same library, it is installed only once. + +Some programs, however, will bundle their third-party libraries, because they +rely on very specific versions of those libraries. For instance, Node.js bundles +OpenSSL; MongoDB bundles V8 and Boost (among others). + +When creating a Docker image, is it better to use the bundled libraries, or +should you build those programs so that they use the default system libraries +instead? + +The key point about system libraries is not about saving disk or memory space. +It is about security. All major distributions handle security seriously, by +having dedicated security teams, following up closely with published +vulnerabilities, and disclosing advisories themselves. (Look at the [Debian +Security Information](https://www.debian.org/security/) for an example of those +procedures.) Upstream developers, however, do not always implement similar +practices. + +Before setting up a Docker image to compile a program from source, if you want +to use bundled libraries, you should check if the upstream authors provide a +convenient way to announce security vulnerabilities, and if they update their +bundled libraries in a timely manner. If they don't, you are exposing yourself +(and the users of your image) to security vulnerabilities. + +Likewise, before using packages built by others, you should check if the +channels providing those packages implement similar security best practices. +Downloading and installing an "all-in-one" .deb or .rpm sounds great at first, +except if you have no way to figure out that it contains a copy of the OpenSSL +library vulnerable to the [Heartbleed](http://heartbleed.com/) bug. + +### Why is `DEBIAN_FRONTEND=noninteractive` discouraged in Dockerfiles? + +When building Docker images on Debian and Ubuntu you may have seen errors like: + + unable to initialize frontend: Dialog + +These errors don't stop the image from being built but inform you that the +installation process tried to open a dialog box, but was unable to. Generally, +these errors are safe to ignore. + +Some people circumvent these errors by changing the `DEBIAN_FRONTEND` +environment variable inside the Dockerfile using: + + ENV DEBIAN_FRONTEND=noninteractive + +This prevents the installer from opening dialog boxes during installation which +stops the errors. + +While this may sound like a good idea, it *may* have side effects. The +`DEBIAN_FRONTEND` environment variable will be inherited by all images and +containers built from your image, effectively changing their behavior. People +using those images will run into problems when installing software +interactively, because installers will not show any dialog boxes. + +Because of this, and because setting `DEBIAN_FRONTEND` to `noninteractive` is +mainly a 'cosmetic' change, we *discourage* changing it. + +If you *really* need to change its setting, make sure to change it back to its +[default value](https://www.debian.org/releases/stable/i386/ch05s03.html.en) +afterwards. + +### Why do I get `Connection reset by peer` when making a request to a service running in a container? + +Typically, this message is returned if the service is already bound to your +localhost. As a result, requests coming to the container from outside are +dropped. To correct this problem, change the service's configuration on your +localhost so that the service accepts requests from all IPs. If you aren't sure +how to do this, check the documentation for your OS. + +### Why do I get `Cannot connect to the Docker daemon. Is the docker daemon running on this host?` when using docker-machine? + +This error points out that the docker client cannot connect to the virtual machine. +This means that either the virtual machine that works underneath `docker-machine` +is not running or that the client doesn't correctly point at it. + +To verify that the docker machine is running you can use the `docker-machine ls` +command and start it with `docker-machine start` if needed. + + $ docker-machine ls + NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS + default - virtualbox Stopped Unknown + + $ docker-machine start default + +You have to tell Docker to talk to that machine. You can do this with the +`docker-machine env` command. For example, + + $ eval "$(docker-machine env default)" + $ docker ps + +### Where can I find more answers? + +You can find more answers on: + + +- [Docker user mailinglist](https://groups.google.com/d/forum/docker-user) +- [Docker developer mailinglist](https://groups.google.com/d/forum/docker-dev) +- [IRC, docker on freenode](irc://chat.freenode.net#docker) +- [GitHub](https://github.com/docker/docker) +- [Ask questions on Stackoverflow](http://stackoverflow.com/search?q=docker) +- [Join the conversation on Twitter](http://twitter.com/docker) + +Looking for something else to read? Checkout the [User Guide](userguide/index.md). diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..616793cb --- /dev/null +++ b/docs/index.md @@ -0,0 +1,121 @@ + + +# About Docker Engine + +**Develop, Ship and Run Any Application, Anywhere** + +[**Docker**](https://www.docker.com) is a platform for developers and sysadmins +to develop, ship, and run applications. Docker lets you quickly assemble +applications from components and eliminates the friction that can come when +shipping code. Docker lets you get your code tested and deployed into production +as fast as possible. + +Docker consists of: + +* The Docker Engine - our lightweight and powerful open source containerization + technology combined with a work flow for building and containerizing your + applications. +* [Docker Hub](https://hub.docker.com) - our SaaS service for + sharing and managing your application stacks. + +## Why Docker? + +*Faster delivery of your applications* + +* We want your environment to work better. Docker containers, + and the work flow that comes with them, help your developers, + sysadmins, QA folks, and release engineers work together to get your code + into production and make it useful. We've created a standard + container format that lets developers care about their applications + inside containers while sysadmins and operators can work on running the + container in your deployment. This separation of duties streamlines and + simplifies the management and deployment of code. +* We make it easy to build new containers, enable rapid iteration of + your applications, and increase the visibility of changes. This + helps everyone in your organization understand how an application works + and how it is built. +* Docker containers are lightweight and fast! Containers have + sub-second launch times, reducing the cycle + time of development, testing, and deployment. + +*Deploy and scale more easily* + +* Docker containers run (almost) everywhere. You can deploy + containers on desktops, physical servers, virtual machines, into + data centers, and up to public and private clouds. +* Since Docker runs on so many platforms, it's easy to move your + applications around. You can easily move an application from a + testing environment into the cloud and back whenever you need. +* Docker's lightweight containers also make scaling up and + down fast and easy. You can quickly launch more containers when + needed and then shut them down easily when they're no longer needed. + +*Get higher density and run more workloads* + +* Docker containers don't need a hypervisor, so you can pack more of + them onto your hosts. This means you get more value out of every + server and can potentially reduce what you spend on equipment and + licenses. + +*Faster deployment makes for easier management* + +* As Docker speeds up your work flow, it gets easier to make lots + of small changes instead of huge, big bang updates. Smaller + changes mean reduced risk and more uptime. + +## About this guide + +The [Understanding Docker section](understanding-docker.md) will help you: + + - See how Docker works at a high level + - Understand the architecture of Docker + - Discover Docker's features; + - See how Docker compares to virtual machines + - See some common use cases. + +### Installation guides + +The [installation section](installation/index.md) will show you how to install Docker +on a variety of platforms. + + +### Docker user guide + +To learn about Docker in more detail and to answer questions about usage and +implementation, check out the [Docker User Guide](userguide/index.md). + +## Release notes + +A summary of the changes in each release in the current series can now be found +on the separate [Release Notes page](https://docs.docker.com/release-notes) + +## Feature Deprecation Policy + +As changes are made to Docker there may be times when existing features +will need to be removed or replaced with newer features. Before an existing +feature is removed it will be labeled as "deprecated" within the documentation +and will remain in Docker for, usually, at least 2 releases. After that time +it may be removed. + +Users are expected to take note of the list of deprecated features each +release and plan their migration away from those features, and (if applicable) +towards the replacement features as soon as possible. + +The complete list of deprecated features can be found on the +[Deprecated Features page](deprecated.md). + +## Licensing + +Docker is licensed under the Apache License, Version 2.0. See +[LICENSE](https://github.com/docker/docker/blob/master/LICENSE) for the full +license text. diff --git a/docs/installation/binaries.md b/docs/installation/binaries.md new file mode 100644 index 00000000..452a715a --- /dev/null +++ b/docs/installation/binaries.md @@ -0,0 +1,249 @@ + + +# Installation from binaries + +**This instruction set is meant for hackers who want to try out Docker +on a variety of environments.** + +Before following these directions, you should really check if a packaged +version of Docker is already available for your distribution. We have +packages for many distributions, and more keep showing up all the time! + +## Check runtime dependencies + +To run properly, docker needs the following software to be installed at +runtime: + + - iptables version 1.4 or later + - Git version 1.7 or later + - procps (or similar provider of a "ps" executable) + - XZ Utils 4.9 or later + - a [properly mounted]( + https://github.com/tianon/cgroupfs-mount/blob/master/cgroupfs-mount) + cgroupfs hierarchy (having a single, all-encompassing "cgroup" mount + point [is](https://github.com/docker/docker/issues/2683) + [not](https://github.com/docker/docker/issues/3485) + [sufficient](https://github.com/docker/docker/issues/4568)) + +## Check kernel dependencies + +Docker in daemon mode has specific kernel requirements. For details, +check your distribution in [*Installation*](index.md#on-linux). + +A 3.10 Linux kernel is the minimum requirement for Docker. +Kernels older than 3.10 lack some of the features required to run Docker +containers. These older versions are known to have bugs which cause data loss +and frequently panic under certain conditions. + +The latest minor version (3.x.y) of the 3.10 (or a newer maintained version) +Linux kernel is recommended. Keeping the kernel up to date with the latest +minor version will ensure critical kernel bugs get fixed. + +> **Warning**: +> Installing custom kernels and kernel packages is probably not +> supported by your Linux distribution's vendor. Please make sure to +> ask your vendor about Docker support first before attempting to +> install custom kernels on your distribution. + +> **Warning**: +> Installing a newer kernel might not be enough for some distributions +> which provide packages which are too old or incompatible with +> newer kernels. + +Note that Docker also has a client mode, which can run on virtually any +Linux kernel (it even builds on OS X!). + +## Enable AppArmor and SELinux when possible + +Please use AppArmor or SELinux if your Linux distribution supports +either of the two. This helps improve security and blocks certain +types of exploits. Your distribution's documentation should provide +detailed steps on how to enable the recommended security mechanism. + +Some Linux distributions enable AppArmor or SELinux by default and +they run a kernel which doesn't meet the minimum requirements (3.10 +or newer). Updating the kernel to 3.10 or newer on such a system +might not be enough to start Docker and run containers. +Incompatibilities between the version of AppArmor/SELinux user +space utilities provided by the system and the kernel could prevent +Docker from running, from starting containers or, cause containers to +exhibit unexpected behaviour. + +> **Warning**: +> If either of the security mechanisms is enabled, it should not be +> disabled to make Docker or its containers run. This will reduce +> security in that environment, lose support from the distribution's +> vendor for the system, and might break regulations and security +> policies in heavily regulated environments. + +## Get the Docker Engine binaries + +You can download either the latest release binaries or a specific version. To get +the list of stable release version numbers from GitHub, view the `docker/docker` +[releases page](https://github.com/docker/docker/releases). You can get the MD5 +and SHA256 hashes by appending .md5 and .sha256 to the URLs respectively + + +### Get the Linux binaries + +To download the latest version for Linux, use the +following URLs: + + https://get.docker.com/builds/Linux/i386/docker-latest.tgz + + https://get.docker.com/builds/Linux/x86_64/docker-latest.tgz + +To download a specific version for Linux, use the +following URL patterns: + + https://get.docker.com/builds/Linux/i386/docker-.tgz + + https://get.docker.com/builds/Linux/x86_64/docker-.tgz + +For example: + + https://get.docker.com/builds/Linux/i386/docker-1.11.0.tgz + + https://get.docker.com/builds/Linux/x86_64/docker-1.11.0.tgz + +> **Note** These instructions are for Docker Engine 1.11 and up. Engine 1.10 and +> under consists of a single binary, and instructions for those versions are +> different. To install version 1.10 or below, follow the instructions in the +> 1.10 documentation. + + +#### Install the Linux binaries + +After downloading, you extract the archive, which puts the binaries in a +directory named `docker` in your current location. + +```bash +$ tar -xvzf docker-latest.tgz + +docker/ +docker/docker-containerd-ctr +docker/docker +docker/docker-containerd +docker/docker-runc +docker/docker-containerd-shim +``` + +Engine requires these binaries to be installed in your host's `$PATH`. +For example, to install the binaries in `/usr/bin`: + +```bash +$ mv docker/* /usr/bin/ +``` + +> **Note**: If you already have Engine installed on your host, make sure you +> stop Engine before installing (`killall docker`), and install the binaries +> in the same location. You can find the location of the current installation +> with `dirname $(which docker)`. + +#### Run the Engine daemon on Linux + +You can manually start the Engine in daemon mode using: + +```bash +$ sudo docker daemon & +``` + +The GitHub repository provides samples of init-scripts you can use to control +the daemon through a process manager, such as upstart or systemd. You can find +these scripts in the +contrib directory. + +For additional information about running the Engine in daemon mode, refer to +the [daemon command](../reference/commandline/daemon.md) in the Engine command +line reference. + +### Get the Mac OS X binary + +The Mac OS X binary is only a client. You cannot use it to run the `docker` +daemon. To download the latest version for Mac OS X, use the following URLs: + + https://get.docker.com/builds/Darwin/x86_64/docker-latest.tgz + +To download a specific version for Mac OS X, use the +following URL pattern: + + https://get.docker.com/builds/Darwin/x86_64/docker-.tgz + +For example: + + https://get.docker.com/builds/Darwin/x86_64/docker-1.11.0.tgz + +You can extract the downloaded archive either by double-clicking the downloaded +`.tgz` or on the command line, using `tar -xvzf docker-1.11.0.tgz`. The client +binary can be executed from any location on your filesystem. + + +### Get the Windows binary + +You can only download the Windows binary for version `1.9.1` onwards. +Moreover, the 32-bit (`i386`) binary is only a client, you cannot use it to +run the `docker` daemon. The 64-bit binary (`x86_64`) is both a client and +daemon. + +To download the latest version for Windows, use the following URLs: + + https://get.docker.com/builds/Windows/i386/docker-latest.zip + + https://get.docker.com/builds/Windows/x86_64/docker-latest.zip + +To download a specific version for Windows, use the following URL pattern: + + https://get.docker.com/builds/Windows/i386/docker-.zip + + https://get.docker.com/builds/Windows/x86_64/docker-.zip + +For example: + + https://get.docker.com/builds/Windows/i386/docker-1.11.0.zip + + https://get.docker.com/builds/Windows/x86_64/docker-1.11.0.zip + + +> **Note** These instructions are for Engine 1.11 and up. Instructions for older +> versions are slightly different. To install version 1.10 or below, follow the +> instructions in the 1.10 documentation. + +## Giving non-root access + +The `docker` daemon always runs as the root user, and the `docker` +daemon binds to a Unix socket instead of a TCP port. By default that +Unix socket is owned by the user *root*, and so, by default, you can +access it with `sudo`. + +If you (or your Docker installer) create a Unix group called *docker* +and add users to it, then the `docker` daemon will make the ownership of +the Unix socket read/writable by the *docker* group when the daemon +starts. The `docker` daemon must always run as the root user, but if you +run the `docker` client as a user in the *docker* group then you don't +need to add `sudo` to all the client commands. + +> **Warning**: +> The *docker* group (or the group specified with `-G`) is root-equivalent; +> see [*Docker Daemon Attack Surface*](../security/security.md#docker-daemon-attack-surface) details. + +## Upgrade Docker Engine + +To upgrade your manual installation of Docker Engine on Linux, first kill the docker +daemon: + + $ killall docker + +Then follow the [regular installation steps](#get-the-linux-binaries). + +## Next steps + +Continue with the [User Guide](../userguide/index.md). diff --git a/docs/installation/cloud/cloud-ex-aws.md b/docs/installation/cloud/cloud-ex-aws.md new file mode 100644 index 00000000..0484f248 --- /dev/null +++ b/docs/installation/cloud/cloud-ex-aws.md @@ -0,0 +1,208 @@ + + +# Example: Manual install on cloud provider + +You can install Docker Engine directly to servers you have on cloud providers. This example shows how to create an Amazon Web Services (AWS) EC2 instance, and install Docker Engine on it. + +You can use this same general approach to create Dockerized hosts on other cloud providers. + +### Step 1. Sign up for AWS + +1. If you are not already an AWS user, sign up for AWS to create an account and get root access to EC2 cloud computers. If you have an Amazon account, you can use it as your root user account. + +2. Create an IAM (Identity and Access Management) administrator user, an admin group, and a key pair associated with a region. + + From the AWS menus, select **Services** > **IAM** to get started. + + See the AWS documentation on Setting Up with Amazon EC2. Follow the steps for "Create an IAM User" and "Create a Key Pair". + + If you are just getting started with AWS and EC2, you do not need to create a virtual private cloud (VPC) or specify a subnet. The newer EC2-VPC platform (accounts created after 2013-12-04) comes with a default VPC and subnet in each availability zone. When you launch an instance, it automatically uses the default VPC. + +### Step 2. Configure and start an EC2 instance + +Launch an instance to create a virtual machine (VM) with a specified operating system (OS) as follows. + + 1. Log into AWS with your IAM credentials. + + On the AWS home page, click **EC2** to go to the dashboard, then click **Launch Instance**. + + ![EC2 dashboard](../images/ec2_launch_instance.png) + + AWS EC2 virtual servers are called *instances* in Amazon parlance. Once you set up an account, IAM user and key pair, you are ready to launch an instance. It is at this point that you select the OS for the VM. + + 2. Choose an Amazon Machine Image (AMI) with the OS and applications you want. For this example, we select an Ubuntu server. + + ![Launch Ubuntu](../images/ec2-ubuntu.png) + + 3. Choose an instance type. + + ![Choose a general purpose instance type](../images/ec2_instance_type.png) + + 4. Configure the instance. + + You can select the default network and subnet, which are inherently linked to a region and availability zone. + + ![Configure the instance](../images/ec2_instance_details.png) + + 5. Click **Review and Launch**. + + 6. Select a key pair to use for this instance. + + When you choose to launch, you need to select a key pair to use. Save the `.pem` file to use in the next steps. + +The instance is now up-and-running. The menu path to get back to your EC2 instance on AWS is: **EC2 (Virtual Servers in Cloud)** > **EC2 Dashboard** > **Resources** > **Running instances**. + +To get help with your private key file, instance IP address, and how to log into the instance via SSH, click the **Connect** button at the top of the AWS instance dashboard. + + +### Step 3. Log in from a terminal, configure apt, and get packages + +1. Log in to the EC2 instance from a command line terminal. + + Change directories into the directory containing the SSH key and run this command (or give the path to it as part of the command): + + $ ssh -i "YourKey" ubuntu@xx.xxx.xxx.xxx + + For our example: + + $ cd ~/Desktop/keys/amazon_ec2 + $ ssh -i "my-key-pair.pem" ubuntu@xx.xxx.xxx.xxx + + We'll follow the instructions for installing Docker on Ubuntu at https://docs.docker.com/engine/installation/ubuntulinux/. The next few steps reflect those instructions. + +2. Check the kernel version to make sure it's 3.10 or higher. + + ubuntu@ip-xxx-xx-x-xxx:~$ uname -r + 3.13.0-48-generic + +3. Add the new `gpg` key. + + ubuntu@ip-xxx-xx-x-xxx:~$ sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D + Executing: gpg --ignore-time-conflict --no-options --no-default-keyring --homedir /tmp/tmp.jNZLKNnKte --no-auto-check-trustdb --trust-model always --keyring /etc/apt/trusted.gpg --primary-keyring /etc/apt/trusted.gpg --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D + gpg: requesting key 2C52609D from hkp server p80.pool.sks-keyservers.net + gpg: key 2C52609D: public key "Docker Release Tool (releasedocker) " imported + gpg: Total number processed: 1 + gpg: imported: 1 (RSA: 1) + +4. Create a `docker.list` file, and add an entry for our OS, Ubuntu Trusty 14.04 (LTS). + + ubuntu@ip-xxx-xx-x-xxx:~$ sudo vi /etc/apt/sources.list.d/docker.list + + If we were updating an existing file, we'd delete any existing entries. + +5. Update the `apt` package index. + + ubuntu@ip-xxx-xx-x-xxx:~$ sudo apt-get update + +6. Purge the old repo if it exists. + + In our case the repo doesn't because this is a new VM, but let's run it anyway just to be sure. + + ubuntu@ip-xxx-xx-x-xxx:~$ sudo apt-get purge lxc-docker + Reading package lists... Done + Building dependency tree + Reading state information... Done + Package 'lxc-docker' is not installed, so not removed + 0 upgraded, 0 newly installed, 0 to remove and 139 not upgraded. + +7. Verify that `apt` is pulling from the correct repository. + + ubuntu@ip-172-31-0-151:~$ sudo apt-cache policy docker-engine + docker-engine: + Installed: (none) + Candidate: 1.9.1-0~trusty + Version table: + 1.9.1-0~trusty 0 + 500 https://apt.dockerproject.org/repo/ ubuntu-trusty/main amd64 Packages + 1.9.0-0~trusty 0 + 500 https://apt.dockerproject.org/repo/ ubuntu-trusty/main amd64 Packages + . . . + + From now on when you run `apt-get upgrade`, `apt` pulls from the new repository. + +### Step 4. Install recommended prerequisites for the OS + +For Ubuntu Trusty (and some other versions), it’s recommended to install the `linux-image-extra` kernel package, which allows you use the `aufs` storage driver, so we'll do that now. + + ubuntu@ip-xxx-xx-x-xxx:~$ sudo apt-get update + ubuntu@ip-172-31-0-151:~$ sudo apt-get install linux-image-extra-$(uname -r) + +### Step 5. Install Docker Engine on the remote instance + +1. Update the apt package index. + + ubuntu@ip-xxx-xx-x-xxx:~$ sudo apt-get update + +2. Install Docker Engine. + + ubuntu@ip-xxx-xx-x-xxx:~$ sudo apt-get install docker-engine + Reading package lists... Done + Building dependency tree + Reading state information... Done + The following extra packages will be installed: + aufs-tools cgroup-lite git git-man liberror-perl + Suggested packages: + git-daemon-run git-daemon-sysvinit git-doc git-el git-email git-gui gitk + gitweb git-arch git-bzr git-cvs git-mediawiki git-svn + The following NEW packages will be installed: + aufs-tools cgroup-lite docker-engine git git-man liberror-perl + 0 upgraded, 6 newly installed, 0 to remove and 139 not upgraded. + Need to get 11.0 MB of archives. + After this operation, 60.3 MB of additional disk space will be used. + Do you want to continue? [Y/n] y + Get:1 http://us-west-1.ec2.archive.ubuntu.com/ubuntu/ trusty/universe aufs-tools amd64 1:3.2+20130722-1.1 [92.3 kB] + Get:2 http://us-west-1.ec2.archive.ubuntu.com/ubuntu/ trusty/main liberror-perl all 0.17-1.1 [21.1 kB] + . . . + +3. Start the Docker daemon. + + ubuntu@ip-xxx-xx-x-xxx:~$ sudo service docker start + +4. Verify Docker Engine is installed correctly by running `docker run hello-world`. + + ubuntu@ip-xxx-xx-x-xxx:~$ sudo docker run hello-world + ubuntu@ip-172-31-0-151:~$ sudo docker run hello-world + Unable to find image 'hello-world:latest' locally + latest: Pulling from library/hello-world + b901d36b6f2f: Pull complete + 0a6ba66e537a: Pull complete + Digest: sha256:8be990ef2aeb16dbcb9271ddfe2610fa6658d13f6dfb8bc72074cc1ca36966a7 + Status: Downloaded newer image for hello-world:latest + + Hello from Docker. + This message shows that your installation appears to be working correctly. + + To generate this message, Docker took the following steps: + 1. The Docker client contacted the Docker daemon. + 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. + 3. The Docker daemon created a new container from that image which runs the executable that produces the output you are currently reading. + 4. The Docker daemon streamed that output to the Docker client, which sent it to your terminal. + + To try something more ambitious, you can run an Ubuntu container with: + $ docker run -it ubuntu bash + + Share images, automate workflows, and more with a free Docker Hub account: + https://hub.docker.com + + For more examples and ideas, visit: + https://docs.docker.com/userguide/ + +## Where to go next + +_Looking for a quicker way to do Docker cloud installs and provision multiple hosts?_ You can use [Docker Machine](https://docs.docker.com/machine/overview/) to provision hosts. + + * [Use Docker Machine to provision hosts on cloud providers](https://docs.docker.com/machine/get-started-cloud/) + + * [Docker Machine driver reference](https://docs.docker.com/machine/drivers/) + +* [Install Docker Engine](../index.md) + +* [Docker User Guide](../../userguide/intro.md) diff --git a/docs/installation/cloud/cloud-ex-machine-ocean.md b/docs/installation/cloud/cloud-ex-machine-ocean.md new file mode 100644 index 00000000..ac00a84c --- /dev/null +++ b/docs/installation/cloud/cloud-ex-machine-ocean.md @@ -0,0 +1,201 @@ + + +# Example: Use Docker Machine to provision cloud hosts + +Docker Machine driver plugins are available for many cloud platforms, so you can use Machine to provision cloud hosts. When you use Docker Machine for provisioning, you create cloud hosts with Docker Engine installed on them. + +You'll need to install and run Docker Machine, and create an account with the cloud provider. + +Then you provide account verification, security credentials, and configuration options for the providers as flags to `docker-machine create`. The flags are unique for each cloud-specific driver. For instance, to pass a Digital Ocean access token, you use the `--digitalocean-access-token` flag. + +As an example, let's take a look at how to create a Dockerized Digital Ocean _Droplet_ (cloud server). + +### Step 1. Create a Digital Ocean account and log in + +If you have not done so already, go to Digital Ocean, create an account, and log in. + +### Step 2. Generate a personal access token + +To generate your access token: + + 1. Go to the Digital Ocean administrator console and click **API** in the header. + + ![Click API in Digital Ocean console](../images/ocean_click_api.png) + + 2. Click **Generate New Token** to get to the token generator. + + ![Generate token](../images/ocean_gen_token.png) + + 3. Give the token a clever name (e.g. "machine"), make sure the **Write (Optional)** checkbox is checked, and click **Generate Token**. + + ![Name and generate token](../images/ocean_token_create.png) + + 4. Grab (copy to clipboard) the generated big long hex string and store it somewhere safe. + + ![Copy and save personal access token](../images/ocean_save_token.png) + + This is the personal access token you'll use in the next step to create your cloud server. + +### Step 3. Install Docker Machine + +1. If you have not done so already, install Docker Machine on your local host. + + * How to install Docker Machine on Mac OS X + + * How to install Docker Machine on Windows + + * Install Docker Machine directly (e.g., on Linux) + +2. At a command terminal, use `docker-machine ls` to get a list of Docker Machines and their status. + + $ docker-machine ls + NAME ACTIVE DRIVER STATE URL SWARM + default * virtualbox Running tcp:////xxx.xxx.xx.xxx:xxxx + +6. Run some Docker commands to make sure that Docker Engine is also up-and-running. + + We'll run `docker run hello-world` again, but you could try `docker ps`, `docker run docker/whalesay cowsay boo`, or another command to verify that Docker is running. + + $ docker run hello-world + + Hello from Docker. + This message shows that your installation appears to be working correctly. + ... + +### Step 4. Use Machine to Create the Droplet + +1. Run `docker-machine create` with the `digitalocean` driver and pass your key to the `--digitalocean-access-token` flag, along with a name for the new cloud server. + + For this example, we'll call our new Droplet "docker-sandbox". + + $ docker-machine create --driver digitalocean --digitalocean-access-token xxxxx docker-sandbox + Running pre-create checks... + Creating machine... + (docker-sandbox) OUT | Creating SSH key... + (docker-sandbox) OUT | Creating Digital Ocean droplet... + (docker-sandbox) OUT | Waiting for IP address to be assigned to the Droplet... + Waiting for machine to be running, this may take a few minutes... + Machine is running, waiting for SSH to be available... + Detecting operating system of created instance... + Detecting the provisioner... + Provisioning created instance... + Copying certs to the local machine directory... + Copying certs to the remote machine... + Setting Docker configuration on the remote daemon... + To see how to connect Docker to this machine, run: docker-machine env docker-sandbox + + When the Droplet is created, Docker generates a unique SSH key and stores it on your local system in `~/.docker/machines`. Initially, this is used to provision the host. Later, it's used under the hood to access the Droplet directly with the `docker-machine ssh` command. Docker Engine is installed on the cloud server and the daemon is configured to accept remote connections over TCP using TLS for authentication. + +2. Go to the Digital Ocean console to view the new Droplet. + + ![Droplet in Digital Ocean created with Machine](../images/ocean_droplet.png) + +3. At the command terminal, run `docker-machine ls`. + + $ docker-machine ls + NAME ACTIVE DRIVER STATE URL SWARM + default * virtualbox Running tcp://192.168.99.100:2376 + docker-sandbox - digitalocean Running tcp://45.55.139.48:2376 + + Notice that the new cloud server is running but is not the active host. Our command shell is still connected to the default machine, which is currently the active host as indicated by the asterisk (*). + +4. Run `docker-machine env docker-sandbox` to get the environment commands for the new remote host, then run `eval` as directed to re-configure the shell to connect to `docker-sandbox`. + + $ docker-machine env docker-sandbox + export DOCKER_TLS_VERIFY="1" + export DOCKER_HOST="tcp://45.55.222.72:2376" + export DOCKER_CERT_PATH="/Users/victoriabialas/.docker/machine/machines/docker-sandbox" + export DOCKER_MACHINE_NAME="docker-sandbox" + # Run this command to configure your shell: + # eval "$(docker-machine env docker-sandbox)" + + $ eval "$(docker-machine env docker-sandbox)" + +5. Re-run `docker-machine ls` to verify that our new server is the active machine, as indicated by the asterisk (*) in the ACTIVE column. + + $ docker-machine ls + NAME ACTIVE DRIVER STATE URL SWARM + default - virtualbox Running tcp://192.168.99.100:2376 + docker-sandbox * digitalocean Running tcp://45.55.222.72:2376 + +6. Run some `docker-machine` commands to inspect the remote host. For example, `docker-machine ip ` gets the host IP adddress and `docker-machine inspect ` lists all the details. + + $ docker-machine ip docker-sandbox + 104.131.43.236 + + $ docker-machine inspect docker-sandbox + { + "ConfigVersion": 3, + "Driver": { + "IPAddress": "104.131.43.236", + "MachineName": "docker-sandbox", + "SSHUser": "root", + "SSHPort": 22, + "SSHKeyPath": "/Users/samanthastevens/.docker/machine/machines/docker-sandbox/id_rsa", + "StorePath": "/Users/samanthastevens/.docker/machine", + "SwarmMaster": false, + "SwarmHost": "tcp://0.0.0.0:3376", + "SwarmDiscovery": "", + ... + +7. Verify Docker Engine is installed correctly by running `docker` commands. + + Start with something basic like `docker run hello-world`, or for a more interesting test, run a Dockerized webserver on your new remote machine. + + In this example, the `-p` option is used to expose port 80 from the `nginx` container and make it accessible on port `8000` of the `docker-sandbox` host. + + $ docker run -d -p 8000:80 --name webserver kitematic/hello-world-nginx + Unable to find image 'kitematic/hello-world-nginx:latest' locally + latest: Pulling from kitematic/hello-world-nginx + a285d7f063ea: Pull complete + 2d7baf27389b: Pull complete + ... + Digest: sha256:ec0ca6dcb034916784c988b4f2432716e2e92b995ac606e080c7a54b52b87066 + Status: Downloaded newer image for kitematic/hello-world-nginx:latest + 942dfb4a0eaae75bf26c9785ade4ff47ceb2ec2a152be82b9d7960e8b5777e65 + + In a web browser, go to `http://:8000` to bring up the webserver home page. You got the `` from the output of the `docker-machine ip ` command you ran in a previous step. Use the port you exposed in the `docker run` command. + + ![nginx webserver](../images/nginx-webserver.png) + +#### Understand the defaults and options on the create command + +For convenience, `docker-machine` will use sensible defaults for choosing settings such as the image that the server is based on, but you override the defaults using the respective flags (e.g. `--digitalocean-image`). This is useful if, for example, you want to create a cloud server with a lot of memory and CPUs (by default `docker-machine` creates a small server). For a full list of the flags/settings available and their defaults, see the output of `docker-machine create -h` at the command line. See also Driver options and operating system defaults and information about the create command in the Docker Machine documentation. + + +### Step 5. Use Machine to remove the Droplet + +To remove a host and all of its containers and images, first stop the machine, then use `docker-machine rm`: + + $ docker-machine stop docker-sandbox + $ docker-machine rm docker-sandbox + Do you really want to remove "docker-sandbox"? (y/n): y + Successfully removed docker-sandbox + + $ docker-machine ls + NAME ACTIVE DRIVER STATE URL SWARM + default * virtualbox Running tcp:////xxx.xxx.xx.xxx:xxxx + +If you monitor the Digital Ocean console while you run these commands, you will see it update first to reflect that the Droplet was stopped, and then removed. + +If you create a host with Docker Machine, but remove it through the cloud provider console, Machine will lose track of the server status. So please use the `docker-machine rm` command for hosts you create with `docker-machine --create`. + +## Where to go next + +* [Docker Machine driver reference](https://docs.docker.com/machine/drivers/) + +* [Docker Machine Overview](https://docs.docker.com/machine/overview/) + +* [Use Docker Machine to provision hosts on cloud providers](https://docs.docker.com/machine/get-started-cloud/) + +* [Install Docker Engine](../../installation/index.md) + +* [Docker User Guide](../../userguide/intro.md) diff --git a/docs/installation/cloud/index.md b/docs/installation/cloud/index.md new file mode 100644 index 00000000..c7a83d31 --- /dev/null +++ b/docs/installation/cloud/index.md @@ -0,0 +1,25 @@ + + +# Install Engine in the cloud + +* [Understand cloud install options and choose one](overview.md) +* [Example: Use Machine to provision cloud hosts](cloud-ex-machine-ocean.md) +* [Example: Manual install on a cloud provider](cloud-ex-aws.md) diff --git a/docs/installation/cloud/overview.md b/docs/installation/cloud/overview.md new file mode 100644 index 00000000..e8b3bb7e --- /dev/null +++ b/docs/installation/cloud/overview.md @@ -0,0 +1,56 @@ + + +# Choose how to install + +You can install Docker Engine on any cloud platform that runs an operating system (OS) that Docker supports. This includes many flavors and versions of Linux, along with Mac and Windows. + +You have two options for installing: + +* Manually install on the cloud (create cloud hosts, then install Docker Engine on them) +* Use Docker Machine to provision cloud hosts + +## Manually install Docker Engine on a cloud host + +To install on a cloud provider: + +1. Create an account with the cloud provider, and read cloud provider documentation to understand their process for creating hosts. + +2. Decide which OS you want to run on the cloud host. + +3. Understand the Docker prerequisites and install process for the chosen OS. See [Install Docker Engine](../index.md) for a list of supported systems and links to the install guides. + +4. Create a host with a Docker supported OS, and install Docker per the instructions for that OS. + +[Example (AWS): Manual install on a cloud provider](cloud-ex-aws.md) shows how to create an Amazon Web Services (AWS) EC2 instance, and install Docker Engine on it. + + +## Use Docker Machine to provision cloud hosts + +Docker Machine driver plugins are available for several popular cloud platforms, so you can use Machine to provision one or more Dockerized hosts on those platforms. + +With Docker Machine, you can use the same interface to create cloud hosts with Docker Engine on them, each configured per the options you specify. + +To do this, you use the `docker-machine create` command with the driver for the cloud provider, and provider-specific flags for account verification, security credentials, and other configuration details. + +[Example: Use Docker Machine to provision cloud hosts](cloud-ex-machine-ocean.md) walks you through the steps to set up Docker Machine and provision a Dockerized host on Digital Ocean). + +## Where to go next +* [Example: Manual install on a cloud provider](cloud-ex-aws.md) (AWS EC2) + +* [Example: Use Docker Machine to provision cloud hosts](cloud-ex-machine-ocean.md) (Digital Ocean) + +* For supported platforms, see [Install Docker Engine](../index.md). + +* To get started with Docker post-install, see [Docker User Guide](../../userguide/intro.md). diff --git a/docs/installation/images/bad_host.png b/docs/installation/images/bad_host.png new file mode 100644 index 0000000000000000000000000000000000000000..cdc78defc551464b2e6a9cbe4cca772dca08c0d1 GIT binary patch literal 27367 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 Myt*7z_HEGr0f)>+{Qv*} literal 0 HcmV?d00001 diff --git a/docs/installation/images/ec2-ubuntu.png b/docs/installation/images/ec2-ubuntu.png new file mode 100644 index 0000000000000000000000000000000000000000..04f1d1c50ded44d94923274c0e727bc1d413e733 GIT binary patch literal 238001 zcmZ^~1yEei(mosm0t5)|5F7%*9RfiYcXxMRT!Xv22Zsa+gy0f%k!1<)&a${`V0Zud z-Fx5nz4cXnRa13p&z$Yjj7-SgFo;|~omy_0b_UvWOvu7_tU!(rL z!)^1*;MvO(UwLT>E#K$Ig($v+x_^)>UOw3{Q;D>y=OhZvDBw3pej zE7qHgiI?l@W{np$s?^?ToxeMJg!gW=1IIjrb5}&n*|Amzt^$kN>1_A9&T1;?9Mj#9Vf7N~k5fOi=5{DmX?jBSXBbvb>QLA1H#>EUxPe8In*|I4+-hVC+8&ZD7 zm!E`7E~Pq3FJ+;r1Y;7=G;-D&E!v0uvgBYyQNZa#X&_>H07v}E`tp7!IV5t#+jQ>V z|NckFL~Qb> z`R^ZHJ{XF|9PdVR!!In8KYEX8s($p!V-q*dN=!MCExP|d!xE#C{zqITW^l5L9H5+a zniR{YS+TO?gU%>JVun;1ul8w#$S%Y>IOCXEXQ9O)WyEPr&vdEiV`sa?Lm~MbO&wWlqZPeH^T*Xmn1CLWC zR^pT4NvM~z8&-w&>QsgqxtOGE#g@F2kxAWUzpk!V2{#u5m5D%ooVZx|(2 z7J8rw2?>#4EG;czKtz;P2~?^D^_Y?^o4IROh8`$9drfT%u68WMgIllgt1>rl#MeNc zrlhEXFyLYlbqDeQQT4cSF8oPWI@2hmF+&%oyJ;R`>yw+h^kJnnvkg+kzcUCqw z!u>ah%3J4XboOmG_B-{tr5!y=%O+=Xr_}-x_GR`qFSVJHZ1O0$>K)gbZkVa9?nAS3 zGE)b#9dIYUhK%wPY2zS%Lcn@fzk}P+f|la$7AE?TNQ7XE`ELlCaA}Hz@Q%HwTnBw& zn#<^hS@3XR_NYJv^(DH5WS3vDGaqZke|ZRtGWD-d%0;GW4$?doIx1*#EDB6DT0wbt zGmQCni~QDBR-gJPg_XPeH!hw^#zN#MlMN|CDJF61$2x>sGPfwoGB+2NknW0E}km9ua_lt%!r9LwX8b# ztbUo1XrXJlLA25#k_h*M3yzK*3W>ekQ+a&6Zi`J9prSQZXx3}Zbqik5+0^D{-lJJy zn<=ZycyTrSZ1Bjkqd|~*V8;7y)0gTxdg~Eh^;mow(t&(Hicu16EKnJXv0DU?#D=?6sDfaMVBj1vm9hanOy>M`+X*up396!>1$NW zR(98x;|GUrdgzu{CQ5l6qRGoE!nEcq#`#b0YF>X=VHYkCp-|D9bTie3c~F54h5Eko zTm%GHRvJ{@dSR%J>o=Y$ZXqaMI~WFw{%bfnj>xXzXuXhsCMR*1@DT#`)O`&1@i-rU z*ns4FcpDAw_!e&21i&o)3{UldptgdA&dE$PC_xdFjbwwtQ+kH*$9~ek=Ed||rJGm! zVir35!p1R~FWUD4*sB`T`mCH3$hJY1G)#2p_!t(P4F_6JTgMIC2Z}v$8u0l zdFh`sG6?7sly#wku0v;BX=!P1iHQL_drxjdRLPEu`xh4%aw;k|J2-cGbu-BXdmn^_ zg}+=$!N!A-B3q9+E&+F5UZ%E%rfbOShmyx)8i^bLO%c80>q@q#_rzn#Jb;ln!Q4q=vWnkIRJ?lhiOMqOIut~$g;4>M5AWBF zFG(r%5b{r!bEEfnhw1LVL!*m7u4fjT&wuC%K%@6-F!~mEwQ!krK_#9np>BqNF)~uC z+?yZP7kI*?55c}N$_xXq8W&`&ju7JmmStx?iM z_JzWaGItla+^QJdaM69P>f$bw>7}E}i}}I`WUVkdT6QpAveCJa(b5I-w+SAt^>%9N zu%#gNhKT?}4#nI~SAGs2K3+;nN=*5nbj^=raTfJG{FDqUDh?L(*ruc_HoCHkraH4) z#SBX(+Dxgjno+qFfBKvip`C&s&xC*CDcux*dMB4qo6S;zM|YLEi@5jB;n^E@r#3nj z(*b!Dyf=bI)2&TFG)a1$RgqQvLjJXHs>f7GO*2rUX!5JP#TZh`xAsTH!FywYui=Jl-}n^- z3r5f8FAkHprc=+S4$a*GY(hv)L8k2YWc8=_?0IptzxCe6E%DJcww!ei+u7La3vT0S zz7QjdzRfi2sIj!T6y#OO3y-{lm9^F=4YHzVp73xX)BN`B*`zH3DIA2W%-6#dr!;lr z$`|#t_*I9)hA^jQjAGrFtaF0q18X}kY*w4SQ6S>L_i^0PVqwgL>gwuG;B>a{Bs=(O zMr$#cM5dAXF72e>vW+8!f+pnTg@-%JD=eoA;yLe1g+^3Wcbc{^t}0)pOYSpgJt@20 zBK@{pi8K;!vQDP0J8R$2Co4;2ITc$E7RUGs;q2h!)jevx@fSVH5X_$WCnnn4iG+)! zZ1=@`V0^}FN!!ND$k1bEmu-AW)Q-=l!CahZEmJXvqC>8qhUQ*wZeQj0a|*k8HD7u;0yH3?jghoJl8c5O}oLcR2> znxD>PW`)Ju>7W@23(dg3~#Ta!B$3WEpq zCx9$JlWqhp<9f(uZCT#%;Gi0(U)s`w(D1<2a6+lh>Yar+D)w=oCi(n3CK^0S%;3l% zwA-!8)=3#FX)+z;t^2VM=VyRxetblmd)uE|aZ58Zrun)K4Bd^}XX{HhRO_c3p{*N! zIGyWjNu91HpPg`Lq@$CLBI+PXl;Ygn8gR68b7*-F*GPR%%!b$>?)8#JU-8~HaC~Jq$d@}6M&eZ7so^iBwSJ>W&$R%i(&naMs!w#a7 z-Q>O(yxmr39l?=0uGAL7UY0OKc0#0>Z~`w@b(xe{5oX8%&wLpK4S#7Xc}YBJ8iRsK zWVY2Gi5gGD{%PUfXugn(q*2!gGT7e^8zGi`3F-EI4mrw?0bhGI-zWnNy~M7D)!j}P zSdE5GIf&Uk_7Z^9SDnQrEpXRoQia00YHo>R*5{yIG-5BR9XpU7gEH#&+0Pz z1xG1iL8~gAMcVljgDGmgIJ#aC9)k6}H5*NucHtKBDJ%w*#44URz$z9RQn$s=NSl*B zCi#|9gnzR`jU8R}`={~2Vc9aos+X@TQi4L%_fm@$JfxynexNltic1lIIyi|xaZoJ&dGqJ8 zRQfv{#x5#aKrx{!{pt)q+kZ$n%W?oH;w2fVm!RI{ymQ^Y=zebMdz8Cj7HIMUV)94v z=|mjamsJ&#R1o7a8KA&PkQ2vug=p$U@zZD zTb^58@AygE?|GguQ^~ggyx}1bEejw1Pkh7QGb@Q%Q5Pd}ncld5391SJ=OA)_fBz}Y z5)AnZ3g;w17DZW2H|1JkVYR?5W#Zz!1kl?4hu&UH+JuqXz2Ky%NB2foWK`6pIL)c6 znnhWe+OjaAxIXY@ePTOnEm1E@2QxIsYUQj88_)LkY?_;! zzf`oND~qDu$!rn$*2d!J|DIlsyG_FO^!7$C+_yXX6ktfcY+x|83~u_sojLhIECGsY zw2-jJ80$>CllDix(7}Opd-*a0Da}WNsqu9hYpVSy z5A>%)AmPsTNFB@X1Ui5qSw7i8xs^r_TiB=|YU~7)N|afWA|NXUC}EB}qz0)lnAf}< z&#Cp(x#7-z=a((4YSztw%6b#7Zu@KUeel()6KB>{GUgm@KGeE`d2hd2h})TA!AU&H zN`uq+r0TWoKg+4v%gp3lSOu-m{qcPNm8j2Xwo+fS+m8JG5ZKkyNs$iGt6@s{-)plT`(-Jo#GNDm$STR>4B$cH0q*gi! z9k2s_SY+8=L5`*C`9WV;N}o>4{EbW2Kc`J!wC$_|8odIHJ3$0w2KwzJt^;QjgE zt&eO(AHlLtYb~UPE!{~1Cij?(+O}WXt{l2rywFa25VWa(sZes8BB%HH0CE#Qc8fEL3h3|lO& zjwUT{hJxgh;-m`(gg_J*;`f=Xy4wXn2-oE=Y@d^#3)`3D9J>AX996GW&?|R@DDWvm z)8<+9{Nagh+xGnemen7$x$jWMb_eBfTYpXN zVc9=AkU86>y7tUhI!*P{ABH-t?T+TGt|gwzr|*&!?jPI1xU70r$uqlbYfZ z2nmVQG?XxAPr4}{w8QK(ISrGPCAa{8-t>XNk|x2=6~@Q7dOA>V!0oR&u55RI&P4d} zZQ2Gv^le!ID|htsX)rwugbuEP+bWVo0n4w7q{8A> zR7A`Jy@dlcPo-`b2K8jj9-_l8r|}qaB_)2H*v8HsA$*R*Mlnr>-VqPX*I*%6l428l z2*AbhW{m2by4dVFg$!lVYKcKC6v-94Qs|W(f<7^Tx4AKIq-3v97Te%&*8`wp1zuYY zP8&h54U6=Wn|F!#LQg*WKD0A6?sTvD-s|@E8$RM})tG*e)Uu+5 zn0oLO5eu!ghKS#uF2@p6Au)AK+^ICud?^&+&hFonY>R{V|2#f6`rkOLr?FVvVvGM! zKk;hK(heI{xh^@uC#Q^EY-G3}{cyKAR|j8YDt$6R5{X{_G8PQ+7JwN15g{$4{I%9< zoT#t68g_?`z#+x3L}*L?JuG{9OU_|Lg)*PmHbqn0Nfr**Hl97rg30apM~(P|gD zh|Fq3ZtGhSB*OLD_2~lD_(>Skj;*zf&gz*iAiL8(V8F%lG^+tXliNN+!PF*AH$z`{$;a<=W zcuuZk+`Y~7^~Ui$lcJhK^=da>YslGIl=&v_7l@EP6NpS<^s8~a0-(&%G`uf=y{&+(-r+ugx`EBK_Mav@GwDqB;vX*>a^5^V%X|ZVaD9< zDZPgG)g}3+IGaRThUv?fuUJ4xP7;u#og_Y+ZQu_UjybG;ZSi^mcK#S(GFT$@*a)9k zIG=WOwRnX1G(H@dZf<+sS#JKOS?w-IyGSiz&J9FqiTxx7Tv+oxy*crP4F#x~W`-LU zuSTuGRV@)e$vMwk2kzn8g|BfirOb^*It(4%5nqe9*ItGU1cP|$y8(;bj zY_qgMPsJ_2f$z>1lLSots2qt@WjsWYYMZOU3fzA;P!*^3E>hR73sjlHlZJU5Ve55C zNy#tk(KVN@4BVm#VJvhx0%oHO#3f}V&y^=W{5`kG>U&Y^tbNCZH51daSrL%g7=Qjb zP8?7FzVBF4#h;6)!6>@Dn@^+o^&`ShiM4BPsg2KeD2@=-w$O_xTOH_QIX(zcvACYfFOeD31Q+?WKQ{K%L5Z5eq< ztLFXM8X$qb$-Pvcts!myMKosw=E9zS6QMCRH8(n?yly|ZLZRHpE32lDmSjRjX}-J9 z1n5dIlnZJd))--?CjjSWjO+j4GE^%SS~26_1Ls+~4Ljwg=^G6@w^K%@m&)rO@$FVg zS=HK?Ce+R+04i8=2)L52q4+D#w#b*3ubS;%;c^>w1 z1i+edkGR*nvo$%4jVe_XnrRb)%JkTpc~u(1ON<|*7S(S>gjX1BC+yx{e66`aX9V10 zfPD%4!eqJxf0dtFk<;_<2+`vCgt>b1J5w|@P2hqL$%cng0Yv};Q;hGw9Y+Ei!frDD zsFdt0I%!rQW=7s{4@9>+y>HDGR!-C+zrLU-_f#R0D4cc1u;C#SG7u?h9nJc(NK*52 zaK8uM)lc4MRfUC0V`(AH*(=y)2DxY4wk$;X(QQZHPiyfv?^L0PUP(6LN&B9g!kskp z{v_RX>-;%xT@@8b3P(IHk)t~)JR0>JM9v|pvSQRV+l%w^<;z7x|CzehVnl%He)d$4 z6p1g)uX(rfOUqrL(@G~AhnPE#@Ykjk{ohg1{3nAUsNNO_?Or5=7PF(c^XV3YhL+hJ zrVwj|49YF^!RR?om7C%Q9(9tBjOaYXNQ{8H z=Og8^ApRhpmBs*&W{%IOKgIpy#`z*geG2ZT1zYW(lpvUC)R8Wm zthF^Jkviw(sicbz7g3#FbmWix)VBh@-6w9md9BL@0jKRwhS9eR|Ee?&ME^bWWR)0{ zf9dP0OAQ0557tB2al7OrnwUKCfduJ#04^bpBc^=1oOJ0okxA^K#hsn6#4IHf&AdtI za8|9>t5Vdtt4Q77_m!(n7S#rMmHobeR7BE$|@>`qIkbWljxf|r}d#d>FrJ@Ol; zzt1XRr-TvQZqI@Ec_*Y)V?P*GwN7BZ5vgk$MEQtKJ&bu*E2Hm>#xMFqTQ;iIawb0i zyX69v=uJM<&RG<5@fW9Vc+g8 zxU-O}gofFvC@xcAHHy66G#omf6?;Fvzdc=%vt$yvbd_axl9$=G9mm?}aiZL2eUn*D zu@S|pyB5I!Q<1fi72D%5j?Z5hwcE{Ll#Yy|348SZ?6s#=*jk(I)sD3QS5=1dlp|D( zGQ0${9y{=0JM9fWGCyB!R~fI)%WSonVCed}o7(z-e&D)w7mWLb1TQAT5C!x9u1O@> zB7d~{*yyyJWlc0;AQ;u?2ivqWTV3K7`K}~{cK5?Qq{-ggceZ+$1~Y*so4VHS9>@3Q zZFqn3np^yMH&dy@?kEGyH$$MN<9KYHaXefR-Ds(aRb-7b=MN8r-G3t4VJRQR*Pg?- z($~42Iwpsgy=Hx0>G}GwH9P2I`6QZ*vV??s>#cWw>y|=y;?(%EgXMKo+ixaw{an%! zwX>w=uAYJSO!F;j?Rcc0EH2VaLyw*{J-Q3>Bn?$)*&1Wo=VcJD?t)R;mJ&|rB&`QKoy_02D zc2LlHs7jYt$0!MU0}muqL5e+|Ih!^!*GXi!#^OygJSH1HN#572l|Vdk&?# z*QEv>U*H~Ss>Z`N@x>ly^~7jebjrU+san;My3!Uu@M^LXyVus&5$4uqrLtqF)||m3 zY1<=;AJ&!GIq;dK3t%4#Wg_?d=QSfT@HEG(#`0A03hm52@rfV)jbhtll?=w$`vm6so51806s>2|9rWqf8~aks{BO`Gw@V2;cy3!`-^+;avW`ZVn^Q&Fc$r3!4zL60CY8B)Y?-KB6oz8YeZfgzkkCC2m+$nibROlH?Y&pCbqUdjI@X=n8X@v4QU4PGPLf zjR)n5JSG+!)T>(v##IdOUMNZ+hJ|1+s3I`9($Q62ao=UD(Zz8;OZf_Vo_*t9DV z=CjqQB$Q;8+g#EP*VXDI3<+EOC1nr#<)GHy@4w$BkO<+?l$3B!c0>Xre!pAy?UpT| z=_pxU4+$Pg0oJ`rzd+SuUw!K~s!qSXM7krmO-?J#iCW{z+%HY^V6~=BLtwkdL_h~f zvS0%o2{R@z?Fxu`>dR@rJ95t}|8_ z$d6~^e)_t6l$U<|gMq=)|3-gLC;UbHxQW}7-!^DVX=G(nAp8N1J^_>X%Vq;`?E8 zq3G`xal7`ord_DL7qZCBw?&+$^n=wE;&ddtNOjqEJP7k;s+C3)MBe8fkOr34Hgl|0 zMVxhB|5RqMtR~pXY^ml)G3fqk^|mc(#m%rK)4u}iZV1@WJa`8}Lu`fOoU6AET#9bl zF$_(o*xWSh!bncsQOl5ibUES1tIJhV9iOu7wX8_eEF{%4-QPpL!HYy!Vc!=05=CXa zhZk`138SPGRr?2EAw&x;LFC3m;*zr4c7rk+o2!)R(-p7MxuG0Xk;@R3oV>5j{aT`5 z6THL;ShN|z#msGEWyKyDCO3wf#L)hvd5#3HmJCcfbLkHvWZ#0Q*^W{;BGrdGM3tZ) zzy$_Zj{1At0QwZ~mfgg#QE}Px9P9{>ZY*HHd`JrPc5=QZ+BYwLO9S%QvmGQP@JJdA zRe8D^czCq)&76TxK6?C9(Xd5Q?Vi09au`DUd44-G+swP8qlOsfl#&j`vywAql$iv0 z6Aev%#);&*n6M?49O2f`k(-nwwi@$euVNd^I2x|Vr&D5ah;U;|llS1x&ZQskGBX4j zQtAVWQw)yM97P8iRHcO`eZMRAM2d!sl4o$lv0G=|?0qN`Wrs$*7)BEvKF^X{`H-Pg zavtK6&uz2HSmTmik#Mn=18(I2ka{bXKrEaRSc>t+aE)*sF!E`p!)S~N*o)s?-O#mX z`A#Q_@-W^AV`D6!$&CNP)BW7I^tUdXE1FG0yuuWI^5JI0JfCwRN(pvlU!Nf<^(9`& z!)4$W5GT1a+Hm;%qm%8%gYCn>tD#&C`y_tFf=WFy$r=}1ueso3>Y}?|OCqH?_i(RJ zim@7@Qa*kg0taqY3Hig<`VW_K|Lhna4-3Vqd7G{y_>vR*(SxT2*?lxf5sp2MJ#>C z*C|mJPB?*z4F6u5CFJ7Dam4%cl;au2_(97*JcbF->tHnh^Pm$%z)A=y48G{5PW+e9 zH3SP3&F2+zJqv<-OrD7P*z#-{@}t8ynEk#1$oa?A4MkIXd32HR))|fhH$QqI*e71w zgMHsX4lAtmj!L5oPzoMH$#Beu8i7dUgvhax4{h;tLtMslbL*&o(Ub!3>l4>0;AC9P zOL*PwAcQ(xtnxALWiSVJl!3jvJu^(8uM2fR&@OK#m=t zyJ-EDD6HU440f6#9QUJW6(-JmD-%GxA6OkVmHQOmcSkndR(X_9o3~@=1^o z$4VdM9FF)8t;lI3w!XI&9)i=ktg@vo^ypd5CA__@Co$SCRY=k33cLI>s_hg?F~9$p~T)mUM^TPg8oBin3ul;7A6?yQFZvZgB`D5|7uKmBn(tptm^;^j2 z;gcnhfgYs3kouqELi}?rLxH_sRw&j_<3BdN9Fn+gqW_?ezVfLCR@A5PokZ$zE8JX6|Nhcnx=s_V9| zt+=lw-u|b(McG^VzhCNR7-8>T|GSm`BKY49=}?yc-2L14|JM=oLi2y5O8Dk8bE5pO z5($Ew{}mhd-yQ$0FNEvAwfq&9D!3Y-!k+cORpD8Xxn%!hXNN74HbZN~l)ABnQev++ zN+&C(cCwL3tW06&(ky<{DP>Wc=$WmwGS5q#x_ihxkW|Rcg&Dp5UnEh||FIpB|A@HTvHPr%HbO*?NVBVWle+d{eqmv0ah1+={=SEs7_d6 z`A}CCJrhHwrq1zo_kw6G2-BjTiJ5~(S_RbDgB#})LklsZiCU@$X`J;e9q1KKXRkIt2A{X~IJi)y%w3y?XQ4 z=OJY%m6F1ww&o)PuEAG1zuohwH57*3W`l+rLas!_klUKU7gwg66nbW?`#V1jFm)eZ zve}p2m^YrpYdEHEoZfkt>qP;hoDiy`t+n&7>H3-5|b| zO+62!vHv_)ek2lniG+VtEzo>sRPTK;9`XnMEXa-_I9O>7c6Kq_3l5)&t9|uEVifGZ z{4wVkP9KL6QeDvVWybnTjblzG)X(7Lh8IT(cO|zUij_8QGeSszI-(KkDJ&&F)kX&e zme1&0-Si~ORm~76TAvbJkxnnYAai{`pqZWO~o-=gG4 zc;f^Q0oGWB1@vbp$ruCa0|NeD8{g#$}Zmur$arlaAlKYb0?S1Vg`*`7UHBfSVK&{N^Xt#^C>a^0)YmKBsn9i6 z%*=fqLMw9B4?SfWz3ZPPTChto<^xbZRcNK9#tBX5ipEvatR0tz+!Rb@YFC0P+cLC1 zm8B@Be~`<(%O*t8XMg$SJ-3H`&0wuj77Ib#Gr347ui`G2@R`+-YjcctkJUiy+Kjo* zi;P}s)i_UiRYj)M9dh1JsoySYA@X&X57X31;@UL%^+m8kjV&)F-)vrQP*9+uC1aMx zJy$-RPVn6l{ycX2s=8_G(I4)JzmfPwaILN2y6Lae6Pe1d8H)oJ^IY7W;OW_CQN;eY z>@x)}Y*Ru_at1ojZ*BxR)JkebpxhcXAuFn`6PahAmWWdK-eezrH--UA5;pc>reRXb zVmgLiW3!^;I&Lxk7{q8R?}LACkuj}zZr9{XABOru6qoRX6AxZ5BF3 zyj(E(iO#syd&`PfdHsk>&b1)cxxliqxrNK21=q~f^25OFQ&^`0dp;tVO7qmCu)njU z5!Y@2dTLRE;%s=BTPfIZPxTKJ; ztu2v8f4TH{G$!KS*eU6wOB@b277hMWuouheI=;y-FwOtg%*G+Do(L`KR-T*t6CcMO zYtT2HJ^9E=?Y*I?WqKY%-Der^7s4Ehzj?$qm1?g9ub6z&v@X}k?WGEP_ZP}1aQCmy z_v7y#dJzaqNi>%A%J1BVd&S6u1tq8A6geZF1zT7}P)fz;eA6x@4M$NB2#q>;$c|Z- z9T+=#ot7+)h-jb^XbLWngjA7SlHC9OSO?jAc9^@Gq$xLePF8fWJJl-sm8;PsjRDvZ zU$w!PGp$b> zNEmW>0|}9SditD~j7a4$#uOD~>9f-1S&6pCIFP!YEdJrAy@?}}g;wo@mBLJr3dO4# z52~!PiFHXh-&g;b#E6C(9v|ZEiM<_0?a;j?J##2qI_j@&015D_GWrdFihmhD z5m#4{pIK*Z5_C(mvEME2EpXtj&$$97F)kd87pnXteBoc9hlM350vgFya6(1<8ohMB4D3nRaJeAlZa}OHNk4!`40A@w0Mb9A> zc&T&G{L7x_uZ6D6j2-Tg!STnLe?1J7l(Cu10M zI+h=QYz3hT8@MeNl8_%B6vwKy36<21h7MWvuWs_m1Jn`?Eo1&rrr6 zC}nfQeVP3@Q{{@Q!?FfXR0Z9@kxwi@$ftP1?$M;V^0KLAnfF1mtt&B8+rPei10A1A zRVoM~j%=iyUIW^e))+_I{f@Tp@n z^WY?^WUTR{c8}ET9%93)8AW;;TobB3r;ZZ=G{r5)^~jUwI9NK=eUW}G*z9V4`5Npr zgm|8$MO;6(%NcR5D76d=pGmeb~M7=TEYHC)gd2WKZj$f(Ub>K-zDyOaBgcJAcI{KGx>JgGHC@&v zb74!wwFLSe^4p?a{fl6t$ZHg1<;tq@^u>Fyl)bAZw~`(*5ZRox@nFm*B8e1+@fIAL7x)Wq|z%Qf2OmkS(LU$w%d_C;|qzBx(i!c!z^DA zFOQ4b(~BK?sor}vG8x;a7h|fC&x5lOLAh;+|~;BJmm<@p|P7IQQjf^!eU!=%PyvMR&p%M=t)ZHxGU zYsfL}>#*|$O|w$SODGJzK(x zQ)x(Zp{r?^g}GyLZYp}|7S{2f?CV#pi$!P92Ub=l(#WrPlpV7Ey0rITj*FyP58RbL z7ds2??Fa{vENkiSj1Sr{C%I>b;joSxC`SV-Y-@-cW zNKI@?Cgr+=QfA_TvIb7~vbOdDe7}X-``7pA8ypT}j0n~xNX7}`8FXGy2HtiiA7Gtc zWqA-ye)G>t!dz7!XDADB6`vMEEku6{(%UJV1e_nNDt)u#-OD!`P9xwqp0lWqy{{F zOtq`7RJl@VZPfglR5QT@&AYJmhu2kx1N##u^RO!4>p>+upXkvkF7^i6MH7Af<#u`? zsr7f)jGc6;Rjh{BPRH9nddeBzZg0DhcCSPBhVg{E3(pT2R)A%m*Q(t6HC29YsqhIu zlb&iO56#VzOpPkvj}x`dt}`>+W@ie}Xd@Su5}rGJalNYg`N5iUJW@v~*#0(su>AMQ zB*+p(vX@$O{B8CHP&P1e|6_MX&GeW)5vYTUx2xZ*`SlC=#YvbiQ`Cjj+0!Zao6Mq>AUjPRDsBQk31~ zOtrT{Zk`RLHvJ^z&J?imXL8x(ykb1=xeFsy4bv;@2^$OGO^b*^s92A$1s#FG(6XAU zsay1q9ZIL4H?Lax-N_Bw!FCx9I(EFvG1A%90Vb1w`h*^jc4h?x1+#hAQM47NPpWO5 z-~@@(X+w1leY@wIt`}41+vukVKgeD9Y?l)~!AGmExr9FPhz&b4m%FI(L>h3B`aEM$ zvx>{~nf))mx4UMJsK&-G3%i3BpVA9X*)HZ9kRqWY4B=uTbR{GqUVjrIA!t0L`A}FZ z%AR^$Yv$|bxy$sBK(Kvk+|2K{KP$2KWSyRl$A#-J8N!SwA0B3OIu18rn&ZzH^ce}x z&&NX^;`ot@hyzxRG&Ugr0{D^oDXa142Qc5p#r=V$*q?nxRHp3jv%!PnPlt-N{P?E9 zmn@8T%+_C%!s@#2(vk6|Pp3dJ-!tl9)AL}(`_fK1>2m=@ zssz{{UlW9vSJ_1B1X^E`KVjtwU_nvV)T6lDCNDIv?R}ZQmNa^79pGF*JlWfKcsaG* zqS@f%?n~FJZRTH#r9@>ll|gYL%OgT;t90#a=dsMGmxpdjoa5TUgV)1|zw%|tyl-Jz z@?^)Bt-y3@efCqxKAtk`jO$MxQIVw4k8=8RYJ|D>@+8p*=#yCd2Ji{qc>&HQ|IvJG zqLtsBozD|9aRr>qc;5IYuu<6P9kgy2f0UZxRhh4BY`yn;n9xjs*gT0bVcqcGLYhQ* zS$w2Kix{)JY%H6S4UV3%#GYw-v=#Th^G(@;%&l|3tP4)i5oeK?(Y6*QyKYu~<(>zp zx6YJwitGs|JgqYxlARnE5cleuK~7zExB~oiPZY)q1nGe0_F7-~2VPM>@ck{e*th((nHL5ZB=L=*C5j&!2(JuKq{!~nBIDWnwHFMYs=WMHcl3+Jry z4s^hkKaE-V;Q|Gb4;a;;{coI9y<|Y0z`WJzQ`3lt}N;j7ri7VWmjNij8(Pz+XKCenATy|EIV;} zm#sflJt7VcsAfjw1>K^CB6d1r47RDdWo%`p6pPN}j-79kspR{vy=huZ*JvzwYlW0~ zt6J#X^Tg-&8U?ILU|CuNO4pi9v{~^Yh^slKz=hD+j@NSWlSxgR_Ak4jR)j1<$b0P@ zj5$*v%Obl*Wq9SAB5p&gIO@=kr z@iA?(SHOQ7@u{qA0rk4}J@~hW_HK!WVXKeaKBcm#Q-m5odH>rMMszBPQ?yj0n(#6u zyYP^a_q%sTv*-ALqyAIUoRV2dsBP^9yuXyzdl-&s z+9l76JDT=6&D2s_QYDdbV(ajolKJ@FJ^^p8GKjYp!pGRAb0qfzmm|WZR>@gYFGXqm z3YvmcW`+JwgXcV`nsiX0AE|*jZt+f<0dXd`^KTV~LbgQ|2d>GJf;pj~B_E8rwGVRP z5+jHNbO+14Oi>U;?%1&njyh1%hxiii)Y9XVG2m5{dIi%i>Y6mwW^I3qV@-$lP=4|U zybT$OxD4%O*v98!%QQT-0qPc}XP=uB_q(Su{xZSR6RuJvmnlwei9fU7;JWF9>Rjz{ zQu-k>GrIb<5Bl8POAvkiE<3B7mX0hKElSKB`7tZSq(?x9;D2U+E+_7u@s|z^9kJ24 z4>e#%2QG-7!zgDENyqywe?#E$Cfa>)jpT{_ohh$)E7sETSRt{LZJvX>Yl$Cf$I zMbZA1bytV9Qq97Ssuinq4fnl~$VQ%-ZK|R7yUb0E>J)ym5M3_|EvxBy_C}Ylv_w5x z0QoaVHjy5j-inHS*SQJa8AYH0OV1I<>m1}*f8(rj_d-=IBJb_N?5aR+{&i@|zMivz zANu2r`n;FM07kq*CB$6Mzpod`4r@4{5)t2ez&wKPjgT5lSXUJKUt<40sL@T)Hpr6C}Uynb#Art&kmN>B4GS2vM33-O&2sE(UAhysmZjx zrdb>VE`?ZH>$oh}Jg6+v-v=-(Z)=@g40f_;(5LRX-AKC?pfQyjFu@4D`ttO1JxvIJoY7#p@^c>`+l%OmIqX;jTV zqsN7j!nJ7$WYX`p6=H8RK8*zIs!b)!{95-o<|0`azb(#8+9#Uln?F`~^?&=Oex0Ff zOGAp;gL3{jXfeD6%?6OPfk8Qef@efx1Chj{h)}P;MIdKfWwqgT_KorZu9_Yz%b0>2 zj+k3M%fD8pm&~%l&7IIf{2NLpLg8oJNkWIZwQ)Q>J2a|DsP^F*5Sm-a_R+p{Vqv25 z@EpyerKZ*mU1h>UTNiBD5gCvaGz?6O(NZQt17IyLQiBIyPvsjzLri@kU_n#N*f~pi zjHc&x9~ZcR$^#&3X1y0Ry)V4(V|BMiHga%k9tfqgRdQwL^nE}0e(CD!y~X}9xjFnP z_5KSV8X1l)^OccvG;p##Ub&6fHL;l0wtBria`3&4%9;P zW=wUkkNSB`klA3G8MS5d{;lbSm9(!HX@+=n8d~l9T!biHG=;3)S2MwhEn7Xgojob? zwW9@mYV<3u#?DaWC9rDLAT?SraFc(2^7D8{i7{Vm)Z}z*-r$wsQB`Y(-sFV^KQUlZ ztV2eH70ZBV!!tK%VhWQ7k*-P-oN@of3+Z*VXg8sEk$!>wHU}_W0{yN|zf|Ajmd)ud z1-7^}vXo}zzeynv$_%y%M@|a}7)6s zLJCi1HG_OzMs2f)__S?o2n}N56>iob2~e2vge7#)E%o1Rwpw=4nQ+|28e`U9i0&*f-Yw z(QaBVk1cn>W!zJ9d#n0y6&I|VuTCnt4GgPtt>%-tdoHsz+=V!|?Ld>Id2et3!k@To zMG|(PPuH*IA1g6LD#j?%gnN_q8YPbB&D8^(ddu{WZ?pvsHe>T(Nks>2a$Cm70BF9=jPx^6_}lHiyEYOpy&QI*N$P1o zo5vRMmkqC&DvOY`J5e@KkF3p6!pCeZ(Z{J#*QfW(m$5}hI(ykei}Fh(nG=@w-A8Vl zRw;jeC~z*o%$})-$jbChU8@tMb3Y_c2Z&n$UE%R*K>|z7ng|PMZj%%%Fvj|b<{LA^~l@ll0a=K87_bGJ4xrQ178>%!WRkWyMg;d%*bEoXOrD2hoC# z?f{j`fHj?^?U>>hJ5eWbz-6n1NkdaHDZ+U&N^Kz#n&x4);DO zmn!Tm+7>*3!%ux3U%FTp`S@RnZAOh98PlcR=X2B<2}g-Vg2-swwq#POJltQ0NH=R6 z7bB$i-H)nxn%H?~CT0ZX7@fYB)RBimrM9629xZe#e^oDJLxfiM?a=iV-Hw$oLWkZi zPP6j{1x!XufDzlYOYxZzae`{RaarB1PBZ@e={{fL7Yu3=)Ur055M=Lib|i!zE%=@9 zGc5Cc(4<@9Sj^zoiH>4D&G|Q>jy^}no>In7N-Mnjrnj&!C%r9l~OajL?3J8dOYHav7y^-rFL03I^@RY_o(zZq*okm zRZvT16!R9Gl2t=bNntF#zPswU{IUV zxXN{`2PMhvKJ~=szOQhEbgfckF=EKJ`>0;c;mXmYQA+;oy}*ZfBqshShdoX1l)(#n z!Cd6z<>*mcQx^t>Du_v54RXnJaF03A>g?BZ9rBeRlcF0+85K0IBF7_|mUN8v&~TQc z*{@eyi=<_~=~0+ME^(3QM&<03mbJ_3Z>R(DvMesoT4Gzo)Gn_EEMt1N|DUHrkax{8XecZ?qCB+?B zG_DB$7A2HMl~L<5n(IU-Zy}E&)5M98eOWGJM~COr|=!bXHJ-7e2G#SbKc!!+=uf&CwyO;x4L82O@15}^q~mBf@&#Mq>eT2f-8TEtBw)dJ8s z7xa^&)G-KFSA&{PGuUGzs}`Z=S4R{Knoi4Zo%0td#I&gWZRe9!VgwaJz!i!cMYvN?}Wth*P^@c8@bMh33}LNViIStZHPLfIBiazgm36&eVaNK4%gy zj47)ldU93NOrxaSBTXS*&Vq5^Z>E0%_Oxw#*zhfbS-6k*H#KTX8=O|#+G=kN1fz0^ zP@AovwgpS9QY|*#@sgBMr#GNh6ym;bF{YH_f@xBk>f9Q%$!velY3r8G=I2*ds87cW znF)sS;;BzHZDw#UUe`Xj8fx6;f`_zj3RMR8REDiWnwN=EAwajhV2fyb`<=rVhj>}e z6a_Qk?|t8PeNMH^$EcYq_9AFb+tvz8>2Xgroi%rd&xeQYgsb!7WviRs78j^6?lAh7 zw}NF86bgpS?YGlu0Q9OND|RFo^cCw_3TwZW_I~4)3Ch$Yooh$aoW*M%ysKLpU)KxJ zjf+eobX2BE9@TirhK&FIuKviO<{%jEl)VYW&? z5zmwXFcdyCbA+cndttE51Y&#a*B{CsBRW2~^RtEi@DtIBKNu0x<4`RsT{E!rZl`~v z-e0e-xqdqDgWgoC2{Pl9DvlrE+_|(0lwi4fADAShUx%hNHcfMym}jyJGHdeacxzPz zk%=!U$tb`Xl8b2>-%CNIX_6F8ZXZ=n0;*rQaVV>3o&ojCVF|<9ZL($z;KcbRMtM$8 zA-@-r=Sw42cLu*2u826O2zp7|foS9As7!wW6# z?X*clD-?RgBddomauK>iUs4p}B`>1&UB%dnvl2cfX}8AhW$TYJiZuJ+3l`L` zW!@&)0KjQSOyxLM&Q1Eegl0O;6lb$d&fZTutNP_5WjmvG>t;-z3}h)~%n|MxBbs|@ z`Y^Jh-WqMyCMtr#C0S*eh`O8(g$Q4BO)TDEDL{| zyQ^`p7EoRv#GTi<&+wI?U+vXKl~Un(c|iVe#g0iHFI}YF*h1w6_g)x|=wJV&5yyc4 z2{!`UOhW>*>iJRt&e^wha!;^6d5@A>{24D2_tUIUw?A^?@Yw)!_Bp!MCug1i5?dc& z{tW*w=$0W%(3yXrW>s5V-IE{5lfx6Cl*;#fRg*`q_4py`YUWE|^`h;rNKVCzJW%!E zmO>>wIV^m?CiIX`HvWzy_?!4uT3E|IfjB04B0YP;GW%6WXV}8pQiy-Y2mhg16^$eX zTB`bvpNcApVh>UhSCjttn*V*5c?mL^tCWS81q?_;U(P-imir`I;Adg_*K)~>-Z1DY zC~ZMb_JvS!y7z{lG9WBFEOMV~(jsSFaz$$8l}Y3;&Z? zd-BBoQ1E}@xxmk#6#l>OFdEMPKMA;VrtrCx_sZT|kOI}x0cfjyYB8p^B9|Y%%GMXl z(qr@e#lbk0Mgpdh?k9?e<-m`Gn~vPtK2#n-3iUMp!SoDZ-L zG{;c&fc*%8Y9lK zPEN-wYF&o2mV>XImS%40_;YuVfl;iKaGj2ww|}COaPyKIVQLgB;N=Z$?q)INC=aGV zVsU_d_FrJ|34Z(>tf#TO1doL?b-cI@fNtjO+}+)O{&8_Hocb8{{o|A!Lg2?R%I~sr z?t5=FtG2F5Sl=z`ji0BFFOKgC&-WZP!!!v%F^y-pA~27dIel+amw*y?r2FeJg!yfm zh2!;caF`0?fX0)KW#T7R#A{HEM*z)t9fwx3@ z1nHK-G5;)R=?pDmqt)qYn3|cM`&Tc4X&07ZeOH^I5 zQu@Zm%c|{qTP#z-7%mUmyFaOaH~X7%S6oocq(_Ox0^F)d=yI7T>yaivzG3Q6f8v>h zGxm1B7>Q8yoU@d`rxwusKjx9R+)xast2>4ZUD`VfRnPUHcaLM2ch?AFCONcm+eqk} z1h+9_B(iXCW~GzuzyN|C#+M!{5{JIy711kO3iggtgQ4ecLM&dBXWXOkIQfqWpERjX zdM1xQAz>Wv1=?}fM4W|7$a}Vm(V>%J5(|Da(I7xbC{q@2a%2k$Xu9HU-{@WwxyzgX zvzrAawkfUZC$_On=IOOSKVSF8h?`&rI_k>mJfjK)BAyq=Rn_M0sGL*tQN%bic?Qa$ zIqyW6#TzQ%s0c>NKM>CM9nrj#tJQ_1{&p6!NoP6--rMVt287WxuI#Fx-$=i_t#-$ zeL^miaMnfKt&L^0JWbrl-Vig`^FQp6fv(%Vy=?ao?xp3{e!Q&;gaW2 zVdI}qDmEUP-r26ZwlY?$Bq$0zS0MN-uuo(@^#>XUWsmdwXJvSBP%EnNx-_O+0sp+#I>ZUk{`r;k7j#$i8en25gC;6o>Qa^VcOSrtwzo z4dN9`n>~?V`|!c$W2&6CY3ps*W&0mFUQ!Zev}^pHMLCDP?y*329xD4}BuoH<)!yt*_MQ)iVx=$k2aW2u4UOMC7uI@5bL!=hKP6Wgc*uyr3i)7rLWye5h%gq0?g5shSJ zGYI__If5&rW=Q;I9{%-P{jUae5T8akx^lMxzRk99XP)z8I8MR0^P(SF$BA#mWW;5^*$So{EMH6#THhd-tj6*JSn^FE8&tfdook z>zALpL&rP(&s*v30bk6i<=1`X{rtS*yIw>ekU~>30bef%5`K>;a8At2E5k8sP{iom z`r?1ZZHjaB`-MnLOSSJZu@omVf2VG4Vu2D6k{=cp7ABt`2&n76@G>H+u<|X`-O^_o zj*%|367BsXD7h#x1arYXMI(ZVGBmvq5&Q1&5dPxIqhg>>txk{MjZi#lFqtVRMNKU~ za_~whjLQA+C+koGu8VrOv_TytKZq>;V6qi=#&W zLg=vy2|;{t0TN^F4R&AwgV`Y+Wny1r*dZ5E;rta@?d@;O+b2V%MaMfk5!67=2#L-2 za&JUu&Bq6pNKKTgb=3eDTpXN|?(P^8xd|pdV#e(5aFWpRC61h_-S0bqoXC@E^=|-W zzE$66g)tUiaJ$0H#K6rXQCg~D5&=S4qo0?vLbAV#n1aG+b?GxOgKb2KV**~(Pt6&8 zDmc{LU0f$lRm(5%P*vbfNSgVTmH5GC(4yI~sWzbv@ykf5E&{Hp$3czpfKIEg!9u+g zO1TpaGkneH(68$AT*atf6&j0Vk9Ek=CxHfEk+cQ}i^#tj8i=4v!Lb(FT!aV1ekaFA z@~3U4$?8|N{*PqCg=a3zY=a&a;y4>FWSPsr*~S7H(~O8A$sdQasQ(xRP8$b3>HYgy zfoPVF)qm=;#9YIPhJ5hORtb{d?3)RPwq z^L0;D%z5_l-OiVFA_YR+=MOKY{uOB!cUh|s;dbE2mzfn=VsnXweF{_&j0^oe*+FtCvXqZHL}`mNtC{eQhje6-_zFQ2;&bG%W^+x#wt=i{9Q8n1wcr}wGV{TcavD@X`B~8_s?xBRaYrHaJNm z;VpOAGKrv8hNrDB95db~bGJ<72Io<%$SRe3vz!Z(8YqD?uMOij&;T|1^ReOwDEa< zdc_UX$r2if#9B%agyL9?ISFxwo(p_HP+WAMY;jKc_Y0TQ!S6|X`;7tl7@%!v+FgXQ zUyK)G5qq|K*MySJUm43qTaF5?D?#7Q5_zmx*4jtD6{$SLU?W0s*!I0{II=db<)VUY=S`o1p$lY-^S8ghgXqeKRR=MEqzBBE5l!CA+BW1 zaO-oz@BHtlk~^JJ1{L)INBlU-{g#WEll!rNuYR(>fl^FH`#0?=u2lhmbuQ%|IUj2} zo|sr~z`M{U<4sxHzevdrI%=yO?>kqtkyLUja0Wz2cPFaU_z`<2L>RVZu3Ez_j+&$2 zn|4S*KSjhf=;P^%nQsh-24FzQ7Dvjb-LB*kOHg5>P#J!Y3ucSnyI6gWT)iB_>r+ed zsKHfO(1KuUmD#rRDSXoC(4{=?#ss9fKQfrMNrMJw{=a6RrL_A@oB}?2!ok?Dju3^; z;z;ZGJ8hh12OLu)>Vwfn(%LV-GCejjvay{tb~Q`b1%r<&O$mO=IOx^#ar8h6u7Lmp&^sw3jn`>SFkLn!^(I;N`1zWz-gdFy6+{Qn`7dw)k3oF!ka=r@8OLRZeQ&OH{VR^;rN|D1yngRHjKEF`JqM(y&K=C`!j ziV{ND)7375HLYIXXP`>hjDlh$(ci9=PB8_AVzl<(Iag1g*R^$l6-n4@BhAE7lH%N& z)9rD9d@fg1JVK%0{q^b{76%!oKbmW>>tu{{8F-&J0non679$NB?700tDAM-sL~0jg+_;Db=TI1^^=TDU;}HK8e7^FlXZ5W*F;9xP z^i#Dl(ygIO1t$cN;1<+10#0=F=Ew{()wsI&u3?l@NgkA|EPDd7D~ z9cZYiv0pYTmQ*bN&$#+(fW#8|`*nD|uB&#}2+?EdntU97!?SKb6P;d0L0ZcLRnMj8E z)q}nDS2-gA#9pp9UR8^k<6e%)DooK;`tnNX>|qlGAuW)I3@`ruGAE-nAz0Q)otGsN zuBO0TREcr8&4vXO_Fx0NrW8S%G`Yq9utK!hoIT*=ybfz)*;E7k?&pj*YhQ4+>MH*r zl<9`Mw3FEMY^OJ$(gwEu(l08wHv8nwXWQFN7cDSE=^UIM+=Lw8R$gVlEuP7PF>%8f zY~X(%41Cb!#u)#c!1P;|pvV(@=q0+tahNexpedw7%3b5h9gQ|ZAa!kCMQwRF&?V)J zV_A1NC8NXGk|O$;E~`k$-v%IHHCC>iqdr)o4@sdPie)` zNOA;Tb;~nXv8?c2vnVW1pCGq?3YQho{_UG51ys!rlDXEC9SP<}8}}EqunGtvKt5it zj?oiWR=JdswvLz(@$z;@RmatIz{_|F^-*%6RR-{R9Cg6fGZ6x!?8)*RwZ6@q|c1wd*D4Ef+KDyhRuG0t+BjNv$_i~&P>TS-Dk!7?oR$-jpwXUuC zRI>fJQ29v6*5`>fBzkOc>+kq&JQEh6lcAX(IlWRN)b*ff5iFbX!h{GynNNRv-~ms3 z?syKdbHS2h5hSkm34XKwts9Cs4Dty^zBggl*zrztI<7_DJ(F=2z!S_Z-+80wZxE`{ zt(P%|z3PBW)fn8FX96S0r|Q5|OHEu5+$CHIVq04$9O-zHg2+ZXB3g*{xmnfYKqEsJ{rZ2^BY&vVS+mt` z2ofT~`|jckg=C%d_1vMRVd!tUChruY!D8%47X3%S4F)*PO{_8XTh4X_ByOZT#wmYm z#+&6Y>o5z>Fwc{=D~06~4?M^8Zwo;tZ|?V&JV=iitZk9p%bL%!P3a=E8{RSKFgH&%LYuu-A6cOAzAa$fjL7 zr;~+b^OQZnm;6!8{t37qXhOIR4PEpdgTOc#m@EmslE)|QjySG;z8^?n7|S(a#u-G* zs4*qpR3Bpy=&Mv@JebCL~HTRVg>QTLmiF;z&Q2 zBHy0TU{Vk$w}!d9G!oFcms^}M3?#*x`332=e+Rf@WRR`Sw6juC(Zm^vS=^i~jGRKz zuUIHiVW2VFd%p4|>g+pO+0n&z+dDH7*>RG4ED4QpQtaomrmb(rR(Kj38i``cTVa;| z0|7QOF|0nL>xgk;XSKt4WFK=)-0dnLH8ep`rf^RpbDDAyd8xgeF<~Y2n~Ut4jF&IJns|{LwrAl+0Q)#2Gzd=6bQv!Z z7YIpP!>Z5X&IbAfT%cBrBZ5c~hI-k_GDKWz8-8eBK^l>69ogqCw)>+OdVQx-!Y3AQ zY8u*!J-!koA7tAf9;HH5x=ZNwj7{9J0TbO?DOP#H7sBHqEPGa&v+UO*K@5j{)AwIn^)p)hLzbi)@D&XqrvG5FKO%}4?7*@*@q{3!yxRYT6d~6bc5V#wd!G` z%*KPxFm>tDdn-R7%kQg$*p-rD*HXM>jo<1B0U+6hb#92=c#rFm(L{pv{~4#Yvqmqf z!3Q)vz)QvgF!%SqjyLLzEL@S+t2WaV7)6Lp9(INW;yDv<|NO9o#Mw-~wXWj_)N(cb zF_%uKQkwmeWCPJxKZ^N~kN@*NKdIv#{D*&W?DmV#%kh9jfzGbbOtJE1>VtC_$U-s= zCsAZ1HlkO{ zVC*Fht+9w+Y|0cjGt$Yz87d50=l1pXex{8f6&o9xF64JT#HH(Xddj4J2b?i>9J0*vpyreCh=4!MLSzv*Q8Sog${9Nlm`_(1K%(z{-9E; z@uZ8iYTU9^a}l>s!TXH*HG)bpdQiLtJbJ*HG@4(Ie0zf5b$hg|74ok|Dw1IQHkcf@ z#_bhcGu>XhRCi&?)=%AWQYF-4%ba|!Fu6Hh?U_ulXtE;dTn_kvXlUl>0|hKJoqa4+ z%9QSXxOCFL-RooOo?tGz7a0!bApg+p_Ixp`-nPpuLCi=51}ePu}B)FnEGes;ci|yB~>Y6;Fv6 zC6U)&#g);1$^Kk}0rAT(kY-=GzGz zgqp==p%Ab-KJ=>8+phc?wa6e!UuQy%5XnK+%AU1$>Cm2hyPMUqLw96G0Z@^by>7k& z2BlnBF{da3OFw_vfXM_|QKQ0eZym@@w{HwcMf_A}u8Jy}g**?bWzf?E?08H)CFva=vODnjut`rPa?!zp zlSdasHIa)z-{pBASyJ?sJCED?CNSCn6A=`H8Va&qs?O+<>C;#86G~Pi74bkl`=_q{ zgnq(4W`SP$SS=#LwS=cAsr`EJIJ`cIAOlFJ?5s*UfI(qA09*g1S&#i&CFIlBhan^K z4u-8ifj3*<>z|qf{#cL=h8*&D0QUPi6F<~}L%KIQtk}Ja$;(TF{&yGl-*idi-NcO(*a?7GWsQSuB7!uSKo}C;LYy@65x16nj6;j6&eeRB6qw0_+tU> z8sFSy@e>|d&op;3ZS~Q7^e;Tus!zM5?$PK~PM0*qw5I;WT{Em#z)L6A<(N1CU+3-J zzjK{Q*P}-B-u8f#s(ir;( zymya;ug?{bijpdoYEA1?c`gWS;2C8usw*{5;S+FQq#9W>L&2!d?AVfU7gJNSTgY4* zFPC?F4U*83pE{sM!!pDi0yU9jkK-UZ=G1*}6-g;6EHc+vOb3D+hxJY(2@G1i39aja zCo2mvqLH9NTf+O8uF~T;j&+0nzEvT9pH14y17%Cg6C-EqKo4bkUVD+NS)*e7fpFI; z;AKJC9W7qXAAKvwC*Lm?_0Q}2HHu${tcxi^t7Z46ZzH>K`zPoMQt`NPpU4nFGkuNr@~-Vwqj2*E2^!QtdXf%M6SN0tylTpTAeu~9tjDs!dAfe zx2)uQ!Dww|Z#`vIUUVQ5eYU1*s3{A-BVFp~F>0p8dmb~&e9G?fx$_F#4Hvw^I5mb3 z9sQ!+Rh?hyzuUocpw#-zA@J-=r60v3voy_^V)vILp~x7E87AKrdD zbz(crRJv8UJ;fo~8GNxL;kU&c{1=F5Dgr<)P}-iFltzuU+f29OSB|GzXg6kR0O?J< zJ^S=h-(|~yj^p_f{PT(U|C~^-72r!BYC*h*Khw*}`W)~S{&R?iW}TAAXP_*h<<6{u zyzZIl%qVjbB=hzsIHZun_Hmc*I4F}+qe!Jhj4V3%w$V=`D+gw%3sqaVPX_Q?z(&I) z`v?#ZodueFMTE*9I2qO8+lXqpraqRxGuw?#KjE!Ql@u`d>kzheIF5BD-9Fd&K%XYO z^~*CRe8pvEBK;smFSdj}?KbQzz~Zk|G~GL#9T7G;6dlI*@>&r()s)9Na3~@vg(ipU z^KV~@ES@^lY;U>tDa{kLv6IEl_DJv*%ka=^NRZlup%{enSA%i2#xhqiUw`HDwfyns zV*e4lw#h=;1nUiTTAbG&N1fR+_aIyTXF*^0gwBw1TY3jASk3Doktx@IT~5Y1soUH4 zoVMsX8;34Gmp6gOZd6cL8Yc#XO3D2y#`=>S`+T z0Nu=mUb|iFR8q6Pb(c+`V-%p_c+YA;_)P6Z9<5*^60sJ_^$#vc@sWQ&O2c&=LnSwVllonUraUky9aa|0c} z`Zx-Xd^i~a_|NlM@Lf$T;A*rNIY6Yk$7 z^1%t4Hf2IG>to3SG`op-V*Sfc%t!vK-z@#e{3XYxAC;bV;cGtw?C5DK7(HM3iME+! zf{CzTFTJi8#g9aG^gCCy#_SpG7VHgv^G4P?(<^ntL(S1hgVx#{^d-?LA|&t&hXt9v zN%i|5xB90UPYvrFkh#~ye_5;}NMQ1JcMQOPcjE&ICyJ!LAwK4g$>3{*y4VO5v+wqp z+mQmfWKFERcEpl&@o7nl($aHe+Kr-NoT@t{a4q2Rj|12bsFk$}VF zPtPRql816V^1Ce%+#B(7|5e@z+F!nXlTTp(((((P*KX}6P@l+J+m1c0(6~I{NXD(& zvv5M$gcYv>>WRDCm))kfSm}gAkdbphoSs|8t$TY??~QSUlXV?69AVNeqOiz{x{UYe zkw;9AQiCnHaQiOE^R%W*VW$&4yJ_DVA%ddmf`9{j)qh4ZatpLJZw7$;Zh7m;1tpAZ z5tAGkhayn6?oQVi{3kOr1+D=+kpD6t0S7SDdfU4Xd%fKq+hilMrftE;>y4RhbkVY7 z{;Y>v0&$}F$4m7oXnO9(c&X!wlF}L>7|VR++xr4k*jy9|38X=5*=@}Z*lR93jJvC< zJKKs7GMuB?OsYHS{2yNq#l!^SK}hwLCxa|&=(6`zy2BKZ@brv7t#+Tld&t#IBW&IO zEHrL>&wM!W0lY8OnM|iegL%FyPjg0ZMa($DETsDu=8Q=b2dcrZ*9BwF=n_782$RD5 z38wRVcxpbuPNFdX-&o0i*pfSfCw!d`26f}-4O}Mh)79hEme}XV+oMeQviC9Rno5xm zA^6Xm3Av%j>)p%=NzW}emGDl#o0HYi^AdB@MD^SWODoTm z!S;1tle3e^wR@c7MY@NDhGY?MWsuMd2M!AK2{q_i=XkL(ZljQ=V|k@xcX-j)R@msf z>$tF(WQlokMFydm4Eot3)x-LLC)Z2O7DLXPB7dk(=8a_p~Mna1!arGM)i zbC$hr@iCgs#My}mVMvm0vfZs){}^w3Y5a}TTBa`&2MBFW1O=&*e^JIKjATGk?3MO- z6zjj3KyE!X=i+4A^Hy-1-*Vg~6mmbsZdx74d=9O0#OR$r6XA9-X)x8hh28xopit??jvz;9M%KNS27qt-bl43drZW z;^VWuMe;x)7*y{o##Gf~G?u!jT zHa`siTU3|<4zInSjM+T*x&)6-U+}h26KsE=l$Z<(D$`D1f`WauJXZy-cNGltylR}F zO3tbM;!N8ba{Lw;$42wy90kvmS#%Z7Li~`U!kbF_OO-%uYZWG%L^Da}^E>04uqB)& zOc4Lia1bFyi20kp%GYMgH$c;5_$iS>7L)CwXEa!c1*E7$D)Mbzj_&!|2_r>^&+!*O&l}CR#L|(JeW*JdZ>}`djRLPQbDv?g{QlaHw7{)%g z&sH%}S66$wD1Fr3sZFbGx~UFt&t>9|@(ljY%vixie@vhZTPbUn`QCYnItJ6%+?r{XQ|JaI~yaUHpf2r^^M+7(>@Q_w&c*tOM+8 z7fTYPv9zg)c?J`A2p4Tk6#YcD;bGs&k`u4JSXWK+3m(vbu)jY#E!=+tZ*5UepZwb<_sl)yB*FiwSSH$Huy86y(Wp!17 z#cvusF}7et;rk3^uIZkdp?{^yn8ST2X=yXEa%#$S2wEOOXdh$>we=x6*S;)uXUjw+ zgntSP^+f-*Aj~RmrwzW@~V8Oi*12M|{gSu(GjFSK?p|*Z=TSwZ8MKG7=ia zQBHkkOr|kDKEc6}Wx<1iNKk@20y8w3kO=cy?N-*}Oi$3&Q{7*wbqiSj3m*nfEd3+C zqthxHrjJJ4C+zF%YvRX=V2U!mWzqIM@la!40S#kfRV?rr$~VBm1R*BNt7{UUclTA} z%EbZEgL$4lMypyD7bdi4%eqVg408Y@-p+nj8X`)u{|oLw5x@1*(>Jop+(~CA>FeoZ zR;vD#H<6d_j#g@gUTa1Edxj>2o+Na%)`>c4VSY)$P+J{UZ{IUGI6%jTR@%GznV9&1 zlfg0eozbI1@}lAr~;Up`=; zFzIgL&rxgnS7ePf)>vbWHP%=&m@8;BYE;Y1EQ>>>Qlt5SW6_G&#qs^?RVvhK^(wQ| z{33x}>o$`1eq48*_&F-|O8Y;_O5Z;}rXQl!XmoQ`DwSfsDlvBL58C|6F^JbR>d*5R zZEJrRV~v=<>aWgU`}rIZCvpB?IY;#M^Ko1~zAvw-b^q4>mHDp3_2={PJ} zd`Tg2P$2Be>%Ub+xbZi{$xzWm_E;CC7)VdJ+0Kz%6Th! z{N5P&{@8ywzn{;^iiUJC|Ji!g{v)wn{o3lfSc!=k&*y*3Np?KN_B|H~O)Oz}a*nx) zK60WR*}G*u7j5sdpkCG5>hRufG$h=o#rkv-V<8^lJ zH{|W`B7Z(>tg*%#Ypk)xn!)@R<^9V??Ao}6^!H<|vBnx}{PWS!S)R_dWBb^(_b8Vx zU*qz{Gr|yV>=wlPVFDawFTjWyO-V~sV|SYyp#uCc}%Yy1$cmj3{(vBnyjf7iF&*H~kXHP%=&m}&(bHP0!1 z*U#UJWUP;p!sm=i)#%VFXsRxwq^yx;R#=|xp{SsQ!SN*&k|By-zGY6Xr_^Zs2G-IG$jhgPFxYIF>Rn!k-WcWh`vcdlgUJymZz`56>T zvy4y8@%^d&c`;Loe3g~FW>KNxA7N>3hN+n)&`4>0`-+iC8Gjbag&_(vlPIidVPayG zxus?PBOsd|;YoTLMX#Hvs(Q(YM8Q85t%~XKA(liARST1p6_qo&r1{62Q%v(TJ)fa@ z1%EbLwSvj9VdN^1PpzyI|K6U(GTGf>R-QvRlzMHRp2UoM_1#RB6KlmD(?1g4cwQ1tXU zt>467BORmU`3nZ8@ugM8OgGOSKWA`4@=f1L z`kKpmUe=11Wg2TbwD9IlGb;Wz3!?+PuJ1wjJi5fu5`07?x@Id6bks@@LDIecXNAfe!5qfv$S&Jz+@0xRNi_ zr}(jB`*CtB2Zg9!a_*WFozp9_S{mshIrRmza{h~0nrgw{JD3qA|77HZd)km%GsyQ) z$p;AV@~3Bszl}K8-^G_+1@x5!;OLyi&oJ~R7mxeTSmyWqxiM4E^^lj6%ZyabKSD!6 z9Py8+=lHW|%DjWVnLDqVK9GFJlSl6+`H#T+f`^#f+~##%JB4|v^vp{6 zr=nUQ&fAq&{c^O6ACqDQw*%*gq7 zXQlrB^=N;ShD&571EZZJ8iowP68uR{{2{<_>MOoP zQaRVpZM}mW){9~p1)*+MxFy%}XE7^`}P3B=s> z#L6>_{JboZ?s;?LmId#J7xq>4# zH(FMsnCZgZ!U*5nZrpMTW>y0*gP(&Q`wko=B(?TS)!=7!nVmaNkX_!-{9q}kFWS)l zjlo|ypJV& zMBgrvc}P`iy52s+)5QiGdp8Q-4xp9{k(>}uY|LG(ENqBL$tOQG3Oh>+f*w3YqEONB zB9;61@8RR-h`o~sMb(3Vl3*7zQr``W@i!9g?}fb>qi0AWeUp>q-gRZqp1s%vrZK%F zr~PFfPIflfIr;LcV`62DswJK$`mlS)F5IFYQ&$jyp@BPLLB3d9+mKXJk6NQ;w!e`` zUr+4pZSjpvrMXx%o*lVncOvvv>BT?86kA*?a{2 zh2GIY9z_M}=IrDZO3mPskWvSU$!R={xI=tl4Fip@@o{m)%EpPbvgXyl9oDG$@H!87 z7dxCBT*xY}VHuE(cJnaE8(V8jyg%w>O2)!qJrAOihz$wA*20$f{5l5PUSWFvB&V)e z@VKm&ti%*LXXMBydr1lvy0CF0;$9p{$+-*-ws2ouKPm@MP*sr4+qOa4YF?0;l}Ui7 zn}~BKHP4c8b8yDS!Hvgn>M6^4z}bEKF|ZD!slA!Bc%hR;P%I3S5O!OqJ)f|Lbk8VJ zO?8rxkVtHFFjkhf+)XcGX<7Si;S+6lL#!2DXw}M9IxDK~2Q<7CuLTALirRC)**$=o z&M^RH`sxYxbrx~7(pqK-dm)tZop`7A7hMlr?nln89BZMhQ^MC84A`lpwFtfi45 z9>+)Uu6qW6#lcqYMrY97)5iTf?%3N}3r%O!*Vpv9{;m_7QI48r-aSvp$<`Jdr#n=M z`ALQvh>1$%L9jnIHjZQz7LybaDDrR5!<;uLHGG$4Vfgc7!*CS!WAElC#xf~h8zVa| zn7g4tIM~_acQ=g@sTxUlrC6I{hz|1;?TWF*7qX~O(_WQ|Ls$v}{cR*Av84n*f07@~x4Ba|x zBl4Q}`U5J>GH(h}h>w4OtEdGhzbIP97Fa>c#P8OENx2ry(ioW!0(a5IANcm>X67@bJE>z1zd>;g1D`FdRHwYUeBPv^GSG_yY zT~o+yH#h7=d*Mk1EUBRFWd@N^5%_t!iX6L>TiK3E^!qe1l!&__IEiz;f$B4Psl(cr8whbTphR*56udi%QThe;r{V{;N(e; z$nkt%F=sBF!#_9>XFE|l4+>fM<|Z&$S42$eOQc$WWuB+S@S=5`$*#9TL(W)>Ywo4L zU{0ZBxS@!Yr;Pw8Cb~$^f6nyW953=yKCOen513H$3DqQN(Ft@&RJ0VPla!i>ue&{V z?*6n6&7qJ^ezTrM(mB*gf|m_@51z*>Jcn87JVoE^mtHoGp_O!#C~}l0=Ivl1YBD01 zS%n6be1epSJJ{QbI=K@~+r%PhmT4}_#?{dpTW5dD8%Ov)TITxSVSV)w(M4^dzTaSQ z@eJ9oI(U;4|GRyNgpB)9yy~6?V5p^xte}*RuNq7H#1h)&CCbvHak3Nhw|D1B zRX>WUE*}1sdzD75pt>*v7e@y%ejkeJ`%!CP{6iJ~B97L!PGps|q6HrK+7MaXEaEWF z;|HNwSy|x~nna&u8Bnfh!r7b0RrQo+>+Y#SpCNo0Us#ERa$(gtAHV5EqnIG|fr$CT zSe&ga@d!+2ROIycRHgW0AWVaD2GhVAdBrBqAyjZ=pYXdtcpNTB}<4dcVCL7DxX;pQx3UJ<7n0oQ8r#(u$h^ zSf1|Z!GjDM8mf7uyRRT}`x;+Y2OQmfiN0fwk!>6*wDi=!z}wMY)VQ07Pa7Iww5fvI z?oK$m`f)GF8)NslRR**7MJg9>SP^vF3mZ#E;_|CeEDn+w9!1@-41kf=a^f;ekgHii zGfRkzDPF-|7#TU!FHxgakKw3yov^}wp_NHOY%g>C#08Sx41#tVS0f|t=fA-6);aDz zeUD5gL%KLcVn_%T9aH>>$+l|3y+jRLTM-)nj75cto~nEveq3h(ltQbqne+~IlbV`F zlF(vgW-0P7LzPYQBrS=~DHQ;f=}D9|4Kmf=Oqh?0Zr>gu)^~|Yi(+<&bfJ4ID{F2? zXEH9+i2h#^o196wuN(Pw{r|xX=JFJ`EpHL~ypQ@+Z_HidP-vmXDFBNN(J1 zuHLxKr1-(Nbe6|sXdA-Ncqi6345{dp0CV^|Iua;EYi56kIWKTG{e;D zE=ynMGEY%vIOk4ZCPkQl>4AD|Elh~Yd`fQGeXiYb;cep^Y_1-}=}tUP)1o+c;siGS z(G)y+#FdK%l)fLQx?4h0&Zi zdW8EKk9m?9#5EIl`e&uWu-f7fki@HsVr*_*AgZvEWM5}aoVVrWyG~XjCnd$t0)2-& z6c-ocYj%-4G5zhX~QUng#sm$_`=Ls7|dJWOwLJL)kno<{3p zmR9zj#*#$z%)BVBs388fJw{>0$VO|hHSuS#uLBo@Lm0UPiN2m;W1vsMn{HuH?J)C< zqoOz$TT7ASx=Fqu5r!zx`6lNr{HX8hOkv+f-lr(`*6mdMw*O?ex`ZB*DE?%{vw7w1x!wXz;4W}?a zAImdmFmZn*3cm&CtG9U3HqAg?Iy*KVpr~t}agiqrlK`G%MhfF~o3dBs+_t`n{lj<2 z2ddFOcL04$A08Lx<90)z`)PSR&Af-9i8a+dGkjrwpbk^LYXm1fp(Hm!#QidrJwqh9 zn_yz+%M)?Ei>NVczf}60^Eq_%G(qvX6y`qQ$_0CBn(7E~G2*gqAWfZZcsbe8Fgi|p zfGx&0UKEI2d5OGTvT&oVwFG@r3py46mdLp4O!k}ilx6y}<-kQeLlcDQOb{`&AzRc? ze!Lfkc5XZ`eSy)jlXylwVX&(nS1U)_Cl`4fVUM1PH&35F=8l6Nrngg==&8cs>|Wf3 z;VgcV%ndy=D*I)8K{53K>+2@G9ahl)CXE}HzR}qwTUsWkx$kDmaeaH9l#~$bW65{MP<8sQ#3vF5<&K7SDF;AD!6GPrf^45b4S8uG%pYG_X8vtl0lTC?}O8I8j~ z(vmT;k1NOww!}BN3@z|3ClXVyMAAb|xh`~6{^A)fW;e*LeD}5f%4)k9uFK`RzR-I9 zb5g~a&1`+CeVxsblc$6k$t5c;kYoGL;B!A$=qn1n%jWbfsQDfp#gSY$^`PXHFpvJG zIL16?W~v{TQ@b#AzfY;CHFxt%xIZXkqAr#5C-&kN7Ee)8Hb&R@;c0Yq zlw;)-KxfxmEG}H)UfNThW#8xQMH?ErM6KSj#49q3N)c02!%I9Ye@D2v0jIBdP~AF6 zeXb9lL1m1;&*jQFJ>pXzlb7JnRefWsJBCR2yUh7p4m=Z@^taH*F|mpj6hwPiuUZRo znkFer^uk1##An3?xLcYLl3qziO)}f}p1{#3m;fIajC@m20L?kUSUAMe+E9pzxwSC8 z&xsH9#=t6^;lUcNo}ST2J7>w69yKXR3n{9+y_ z-r*>umtKyBFUZKb@5r{pSGW`Ph~&Fom^u6Mq_~Ix zXB+J9q*3=gmW!7}O+9@iv( zC-IDY!thWlUS?Nt4o%`ob_QnW&U61+8{d-QVMSPRC$H~2WA-JUw=Ro#K4tWS(1wL0 z!%Bc@{2i@$Ufn>f^BJyLdh%R%Khh(hpc_eh0s0s9L>)h-C_Rj;23C~5%H!n8BX~xo z^88sch8J$~s%?@lOtrq?$`uD%r`4z>AFweorKIj1ZbsK}@sFo4HxY{)7YWU&p)v0^ zuKq;;%(YZt>L_wMIW5L=Ubha$2$Qs^`4Wa9oGh)WoK#ZiZ_Fuud-95lx$CNjZ%P4G zc|n{pu>887-Oqi_leh>@95W>SMT0PhX_$VyU$TuBHStc=(Mha)?(tmIshL>63#)s1 z^~j5>#@-Y@FDBZ{1k;dw2HPvKFgGLNaUsd~1Gr}BLHoRt@1jxBP#DAc%NE$%n&TLi z&-=1e48OMzvA)Wqcf$a*yokom=K-p3ez{W`=el|mc*y$5awi# zxx>mH#l}C4nx~=MbO>fa4Fhk}xak^AT|+UJP63QdCb?~X5tr~}N{aHZxOAKgR>>@m zH)DOvlrUkGo+O81ddrHcrh2@MuHqh=NJ+_KEc8xr);Vd_e!j0Fol`px5D=eFaee|f zu3e+Bu37gho>V2kal2i3$Vvd#JQ;W3EuW5A_i|5tc>I2@uS$fhf~ut`yb0- zPS)pg>exZt9^{e|ZprrJ1~iW>Quo9UecyQI|gE`Y$j+tXP&24qq ziR9ZjdGjDHg;(``sI+`#da#YhIXPsc#Ni^$)O8~-=2uFT*^#%Hm^(78P|;ix!GY5^ z2nvZJBFvvNm#z@@C=~}oOPYt5P{?|*y>*Kx?GhA<8KN9bc~&{V(_mxnJS;<|P#}|z z;c8?+?5j2+y{t&B>E?aSOLFqEiN6=d)#IlLDd=Tc-hqXwB^?XP)Fj(-SZc2?hu^%f@Re#5te#nR`lww zFTCx5S9UY+({FS7)MY{=?{YW9mm~WxQ}Aw>FMOzY$eD|maP$i%F+H2M{!t!>S*@Di z9TOuW$_FS5b;d2Z2Dx}W#@2+WvLV!J71_a7MCQNZWlkX1uAL+3X$_^Bq4+#}L4VhK z-JFx-qcJ&q0_(VHWPKIfx@bz%goJ_eNKTwOfnR7O5upL-i+Z_N(#RL;o`svZON6arQ zA)D;xNp3#5nUC;rGUB{2?1Pd;!pu!5>R3cQCJeWU4UN5n+`gsHllK$4G0t~9Mc>$r z7nRSkxN1ws%o0k)47ZJpC=?xjK{8s6sj(fsOBxEpEq<2=6YiC@@W8`@h=Nw+N+nAZ zO*o1=%gsyX)ai40-VP_~Zm2Mrr*MeMCd&3Y4~tq@k}2q|dqs9`E{X9G7+*Sytxuw8 zcM4B2t~bL9mO4Ie$in&Xp#`TK!t@MD#n_h!b~4rd@+ZRc7M|%Me+tp>a0BMoOnEz~ z__3A-CK#yA<(f$VW0T`NaK0tXQbhfwp;%T^9_7I4 z3kKYcxJ&3Aca9vsNmg0}7tHMGlggO?P|oFRuCz{zJTJ6kX==uY`DMPx%s>-anR%oo z$6|i#Jl4VhPK|cqaLs^MJqsvB{s$``pl|8QyRrmfrXA>7SkmR}O=d89UddEfq~mlq zjrR7Jm^t2|Br6zGySut|MIm3}LrV+ObMv&keL;3sHqns*oH})stoCWD?|WktS%^}l zrY_wLH(@XxN7dm>*w*9>U%Q0C`B>bCrpinIH#Aun|hd7yATMEB^-G~x#_j5Ghn3)Fw*5 zwlsjVi2-G;U1UXC;+|d!z+!iS=*x!%^%#y;R+P6l3oTzF?$t0$W3M=O>NGCip+t## zu`xP_ad0MaZid{=e)nmOSm?m)svh^thga03oFmZGnCBgfjJ!+Y>`g~{C1Q?p8P$(` zFtLe6@jHVVT-d=t!|PRRh`oO@18q56G__~wv%&Oru%M{AnkXBC)%T;EX5K&Y#mFOx z#gFT`Y>|%U7LlVoE*Kc`enRxGm?O#Gij-F!d_kj}#M|0{2e10*s}QEnEuOBo1sE86 z(myZN-4|+d{m>T%<3*|uERVwGcSdhpyQK{vcv6! zF)bm?%82x;Azf`RO%Bo0(ML(9KL^hm5f&CjWS|R2&s`z8dXgWZlF#zMvV>q0ebT-eOLNB{Gb3F*%@uRWc!;;%buQ_- z@Zm!_CYNn~c&}2-b>Jl8V`1k`bj%~l-*xk$I+=q<&*C2#N#xx?E?ziA*z+b5f*gs; zDVQF!~6#NR?cKc`C{(%=&L3^yl-NBdV*jJ6AIhs(P;VR{-z#O8CcRjE!AC9pXH0ORU%z)vN?C`Gy&n^ zM1%)%=9oSaer}iD;QEP|n)M^6eY9Jugy3e8ZVt`m}d?8fl7q!c%HXz4oCB%M3&T0^8T0)`nA-dD z=5Yij*5NE_HOxrhO7`x6c*(OQ|ZOvycRTr z*qNJBHmn5gJaMijJgXSsNsuX#*>!wk5ihe_gg#jv zVnfUD9I1geSb9bAw73{s%j?{K+eiP)G%NzLP_u%1yozf#^eG)w0kAkb%)90;(tU1< zIW&BYTePzUcd|w@4D0?to(p{tH8k6kZ0wOu`vo@b{SZnC&+th-0lOXMQg_+J^HsfL{YGoQgL9m+@+0~8Q zalFp$$E^UU=Y+AlW=7SZ;)h!Dh$+y0MAgeeXeA*Q2P0>C=F|-Jw(+L4nEQ8wvDDWi zrnrqhUHxSP@I2IpOXl9f(0tTvI`<>vD9nrClF-qp8Wumi!q_&LK`9{X!p3aX`nihQ ztPu3AJxR`dM&bictfGX$6lTdu--P!QDgfqM(ztGFCrr;np-T@Y`9yQ^eJ(mA@V0!# zV46GL5qj{&+9e(>0BXA1Yv}Fm;GVaoi0K2Kmla`Va-GZ%v%HJ-!T$bB0G+{f7Y4H| z!V$a3mwZA=W|$GaDUXSDH|KH12mn>lp4fbAFf|IgnwwVEf?LLfC#7L}>nfHW@#JS` zlb#gEgRFAi2!miPjf5ZP+ecmwS)i(xH zG0};ey#?-p2^5z-!@x+7=fiT!LLG3rSGMx$kpO*s6EpF+X~L_|b)@Vo#L&oy7gbNb z8q5%PYZ42ei<;J_yn7yi<%J=dTHlcp<}Pyew>97Kp$UhpMl?*Q0g!e*<*JD=b4B;C zv4}wlsON|9G%@E@#~AJPwG=;2Bgn&r8%9>t4$bkd;6BdL-x|y%Qo|j1__7_eb2yn@ zLf`J5E>{_;Nj!LzO{j$l-YJdf&`jZDX+*g&n3?yigdwW}V4^w^6LUWnG+zv66ZYoj zG|gyH4wrHC_<4fwWsoP#PICMMQl6CaK-6lSsH;zmVykDk%3$i&o{#tEekH?iA8=fl zwJ8-kTJvsm%On&TD`a?D6IR&7lY4Hf))0r=DGase3e(`om=;is;BI6hZl*JsdEUaTJ?jLZ z?M*87p{WcG_w)AKdt+=Nqb;v7caC6M1I^FlSMQgsgP5MEM*o%`^P4fV#7bj?`YoajOi~Sd!$jZnjCnt;O$Y|blN%(8jrn+KmAHuR0@_Y@6%=z6u zB)b$3yQ?HOi~&%S;wcPf{5J-34nO@HtH#n?|AFR;bS%s)h)&I?Br8nh-;4J4Vk``Q ze6LbS#;LDIBy!e=?td1@g<&g z&I3>>%wlj#@i&(#gt@Zi>~&K-ynXQSa_7RC6WmFyX1p~A3s+&nrkb!4Ig0SJ!ZtLY zx!w}Ybf1v;zQN3NH^Vn=rEa~=&+(wJm?)w7SFKW*sl)t|(W*U)fxZpxQ%huo*b@?; zil>Pc)x%OIJ3ksVe_9$IbMBS{U*06{DG0^fI+51$G|pc&BSXY2J13h5F>z!@xngAG z&IF%mEso{9_3sU4GDZ%uEc1!VL|bg5bH!Y{aS^$WeH4wEV*t}CEqwF*sfW=yZrOMU z@8N@|vl*vP-K1_vfpnw}TfOU;-?~nG|wq8L*wBbP~; zpPeSg#RSuc628ZHRSY+8crwi=`YThp>KwzfbbrhvGf@EAiFzz=TGP~4j`a;U2IthP zOL%?&cN0shMgg?k_q8Odu#W!jRw|1Ni3|(C{Du(^a|>|MKY>S7qR?eFX-Tofr4{n- zX%IG!>HqPb!IX}Fz~uBr-RFAe=jU}tZC<1>oZe(4xM38Wj1*w012bb&TE1+Mw9Dj# z*`n_l&)nzh=B7HZ&_Bn+=T*8*^Yiuz<~z$cvwIU)EJAh9D`fc@ky6mheE$nhov{)^ z{>EUolwo2UOl3tbH!QuVY-p$EX$(Vc`p$uO1^Jw}R%{5{%Cr!S=x$mK9wXUb{|C zRTmxQ30yp5Nb%cd8q2b=G4!E(PQh0eJ1{l4NS-h^tl25FVG# z6aDxdO;si69pBIG$F+37%;5437aF@qRu*IB1pX%Hu?5LwwO)WT?og=<_5P+uVQCND!2!r{a>mOrQ+fASjT3ZYB3_yWsnO>a$ttU-`R!9o>^-R+kggO)rav}b5j1ynk#pCL zOD+kN<%X@E->Iv-7+9h-*bbM(YCuhlg&x+SkC~RKP)@aDdVW7Aulq2!G=$BGJ)AUj zWprLjvdej#@21n6MM>AR}AoQ z<5Mo5yFl*iM%rr%Fwz%eX=x|W>N55|4{2?0CdS!}8xD7AdYjHU6B`DV08@Ba8&ldj zNJ)AyKB=#C2Gicul)Aot?m8P{>J&>uQ$4qBuW;JLk?xTe^e$f|<4r5|FOxaFdoPcx z+j$!AK~!E90M!q?Ie*iS*3J%60v$Md*_aO#Glbl{LTE-6lfCuW-?E~0bcRRn1{irJ z(%jZe+#MS(TZGZwS|H|rhj}HiJcqxr!Rnj#bgNiM&8Qt!(p(t&yF6SICU%@y7bDS@ zH_bi0lqTQd!WCQIceLSh{R9yiFX(Eo6K2qY%+i;H*&0*!UIN+)oGmWm99_hR_pfod zaRNj8NF)mrxLv(SY(XQ#O)oLA3*kds6HZ2!G))31L|Z1LH4O10&Vy@4c2w3kkrU*| z{<9|34=aACrL)4EDbVmH*@q)X4&!{U5VclC-d$U6$L7)1)y8czeXQcE=o9sKNz`9c z&jjtoQJlYFLuqX*wZeFsxCHUCAeIZJwhXIbp{-1}nbXNw+JU*TDV>W7s`FEMl>f)y zG~e`iM5izi0hU+M^SICOurQd%ci|BInC{+o!ac+}-f0ZJOXuv7Lxd%j(bHLv@68K% zrD^9*I=`H*Cv04j?s+xIMn$G2m*C?uaPn@eUd1b=@R7SaC6<)|! z)RP2zJFTn*H_zb~luE*FGt520Y3u4F_ns#qsYSdkj>XbF3iX#wSy3eW_U^?sG7F_f zOG|MA*YrMaxIa+)JP9W^e>ysa(Gu|;Qo&&L6H>EZqGW~gXiH(jJ!$J~Bk8s=2lemJ z*Wb8$&2lv-xUijqaBK7}y?Eax^!do2J=-twx?_$nNIKqd`LrR=sy|Rwn99jRM@WA^ z%IhdQj-4^#MMDRTm6=?;a9zZu0l$m;Fbznfv!|1|Kx;1BL^IS`gz=3))z?X^w;iq?K{(o7$K|hUUiZt2f$>qIY_DPylqvGuC^TV;f!jm6Uq@kL za__Uj9KiLK886Ua??JFk-;~d zJ+IF*VN~8ciQ>?XW4x$;N4Tx=s{4_9#(Rb<;yHibkeoNIG*@PDyPJt2jp$(pnVEwVQ6Vb_tUn zXv_)I2*xG{2(!7R8_OzGANMH6RE#Ug6^6m@@dp4}UL}coPv%8dDE8hf>)A!uWCoh6 zbdMjDzH27^z8%-U-7j5np})HVy&G3}Iiv(sT@f6>qKE;V!La)m}!;_HT-5;*=vTKlFCj+db zpV8U$3d;)zG53i7<|DFl(mV{f`FSi^_nd{fOXk@lPtIF~($djJT!1CV&RSAkm4~Up zkMC7;T_xNwbD{Wc3r%IITt0M#XD^>)edQGK&tB8n{047ZbIN+gi1D>1x=5JO2X5T3 z3Zki_lV|sDr4d&NU8exnKf8|Kt>j1XpYh2XSTNEGU?Cc5O28)v^6=h4$2{n96o~HpPt~v6$rAyRp0>>{}P}k8+xHwmLzm-qmXMSnb9z|c| zO|lFX(E%omFQi+L1Ixg z9~w%raq{AM`E$%KUnC^q<)?dElI05DxYZc=!eq=<9%U=tCyuOQeT8W9yyD=;S1Yi4izDIO62sN<#Kq6dG1g&+zcJ z1AZ|XG*n~~5&IHA#HE|)z;JqJd`Y4d_luV zQz`z=HaLlSx_brCIlh2WK21ufpKhFXR?a-iuST&vPjr|!1#RO1^t{XG?!zLM)w8;r zzDqzJO|Kr~W@m$&w?B6RZ{y(R&70N+?uDi?Hq=REPz>E;asXb$hLQGWfbQA?(T_8p z9`3lhhfv!+%eM?QKEu)4nxc2_$%+o6zDEK;b;>;+JpX`3xxn*`C~O>EMP8i=PcLV2 zc|{xZL$%!XbH>HpUF6b($M5=(OJ_)3HNUqyoiyg(Beu8=fQG_{7@Ilqu4fXoN;1Oz z@Qp1;i-yO+&V&6RNvCp&=ZV2MI*Om$+i^dwK=(1`v9{OvIf%H5yx7|ZiMUKN+gpiSa3m8d zfH@K(B54?zrr~8eNhM97nHD(;qiu4L!InxQME!Vp`rzkj$whN#MwLnm9|mG=?}o3p zHy-ZZRJ9HArXZ1=%2ohoJF4&!+H?2x;7-78Y;T%S($qm^UMv>EeAG47@*wOky%H^w zfqH!0opE$>z{WL*ijH~YqqPJHW3ZqCR0|{r2k^FE%2!kp-A71}!jrg@$mij7M!P4C|j7L!R|Zx8o_!s(d>Tm-_TrA84>p$0yltq$Mei&&ZeI9!x?<-UO5M;B6V(amoxe#ypdZdcZ=T-5$Vg-i zwUlr_vxNS>Izl7jnN!LrN{+XJWFdk;)c$8XJs_y(@lqZsTENO+a)i1MP2c zu(Bp8{}msqbGV=Q4z*&QyoaG!+c^pCIue>vA?CculekbSo2CG0%8BEyXmIMK%NvFt-y^WKrh_9m^ zX{B|@W||2Jjz+{+XGqQM0+F)9MB7WeZ7gu}_TzS7pwP54C3W@O3x2S&W<3Za<;^g% z=}uz&?QwSV#9ipo+QFCRp~Y`D1(U?y@xsp04Sx|US95bBGoDfu?Z$;$R`~mR;2?Az zCd|X4WE5Y6vsgHL;OFHk>L`Hnj#-u`-xCs;g#tO5ANAQj!DQzfyhS`+-JG#^y-Q>N zG-JG3p5%p>-Ond#{A~P+I2@ZdZ3eYT#3xgX%)6gu>Gwx&O z;EJ1zGk5N$GrKTDjH9vc%ehvNQJi_7;`)97md4wO4RRH65&09>J+JRUzBIw3D1Y6T zwD`E0bNc8x%6n#*>#xPl!479vF9Q6<+(WXFGtGmrFp=YK?uA6sx1bvp1C2%ax;u(m z5jwveOz*S|+58YOKAs|uj(GXq7IX68Y2y?tXsF1H#KPK}_OU5i%QJ9xuoouB2LH$; zp?f9YQkED-`rAI5p8qkPoxQ_k-S-oDcIURQ7tYRs)OU1>*S>Q<654;36eiAf5$8D) z{kWRhkp?^iK7S$sxrmuC-i-sSLaQRp?K&ntk5Fgx_cItVnOIIM6~TH>d=OS#}!DWV`80hC%tNz@6q)lo}j24be>N(LCPQQ(`r6c zWME@!kB7UDs2O)66HC#s!t7up{$Iy(o40*4O!rpf>1>O$m`l(dZ_F&*Ns3D3Ze->U z?^SBWEE#cO*gLw49JmmbRDx6{rCFFwS64^znggM6=`1MKJWUEG_g#VUtGyO_T-iN#GQ3-8K*MNT<*GeOb>}!*KrI;{$jgSL(ry4@ zAm0#rH=cQonyxpQy8EE3hYzmy7I;P$p;pVOdK!HDAnsfv|(H1Ps&gxQ&S6r#-)G@{QrX6L170i}G2IbB;bEXow0`d(N#kb2ZA(hkw2GH1Zb&^UxtgjG9@js_6tSgMErnq??t+BQjKY`4ExG=@O%rB?n5 z)|E}8zxrZS$z0CUaucQ^>fo-P*(!Gt@=^FZxw@{2^w(%CI%7=ruPVV@6o4lVS*mpf z4zWCdQJn7m6E2%Nb&K0@%J2_BufwIpwt`WIeWeOO!X(E+sN-XQKKfhqmgeBevV99} zmHGgmlppmj?*LI}yW--xNsKjkStEg&9fZ+_^Xw8#7vtAL)LRVPg_A#ybh##h%_^*Q8Mc@ifuGeby!Wd z`#kvrUVu9uU3!ttdgD^e^;@8Ehw-8JuLk+*-+$RrOswW9oW4n|C)sCK?1YPp4zC-a z^noCr4yhAa8h5Ka=rhcLL`nR?&c@vr_Oa-J2lw-R_Qt)gX7XkJD&wM?^%GqRhFXA- zrd9T$-vHyk*UDrE{Y($Ue9a1VWxO_GVA*S&40t}R+tXgVE_a^(ufSQFFYgsw{4;b6 zJFaMhT5{s+g?mynCp7hk1ueYO=Kc7{NZ?eAMsa1e16ZL!XZI!o znjD-R!kR+?_^t7D9`swUnc~1+_LxMX351#eyGCDb7IwrQYeJquyt+Usj(Tjj5I%o`URjGO2an ziUio>OdLoRX&v;}pI;k)oa}oXZg--^SNKf5lp$NDb}6D2Rfs2&|J3tjM$5{yJ+&I$ zQHNf%azl!O-yb<2&(!R@D)9A(1D$3dNi%%^2Q|1A?J6$NH+-mb>%h=Q6#t}zNBwOD)14^ zmF5tDiI*Y(ZK=LAwupuM+3q`)e(ascu@UlLK`^%^&)hb7lj0b&lO}sLGIU4no=r!0 z-T+$<1|{a{Bz6b7dNuTP9q_V|wA>kt_rYc?&75~t?7&E0o(-*K!_yujfnfnD3KztZQZPM_e5t!LFGV^8ph^_;Wd-P5PP5B^_NRNUQF5|fNi6wZu3BdH^C-x#Y) zeehSqQv2yF{&Q_vErL;LUGpYrEnWSCg^G%tXih+43e2Li(JC$Q#9`duq;785;TNC? zo2|p~Mu04dj$W(*tc%9bG7r`eePG_hq$urq`yx&Rnf9m(uQ`~lqU*pa^{(UVxP z;m=MQA}AjELVtc`{CCbvBG+j+w5JKRe{AB$s|F9liZ`Jp!iD)hE4SSEcf!V6vC?G_>FKxU zO~-_g7`?3QV2&!G^9+z&&(Y0><{bt%$QU!DAG7!le3AdW(G?2Ogx&@`W-_RtM1jHM z!0`F)hf@b7MCEJPQ1rjb5Q_97{MU6@muwt_bZc8)F2Q%UD;6E}Cxkv^dV@>d9r8ih z;xd53A`8!P4HRwM_Fk=&k-sZU(dpLihA0Yi*R{p^l~~U7XSY+VB^2c=9%D)-k+9xd z!bDB3=DC?3^Z-AVv(dfIX0{q1eQutG&pHrZ!D6LHp^To-lU-aM2YAm}>8COog8jFt zxFWkYaZ@~w;*acr)!j@1^*G?!m>^eg&zz|6xhV)SjOdVO{Wtd7cb{X=vvHBw|Bb!9|H&> z2~p3MYr(?kyZD&g9PiEIg^C3--XhNerIEDrdts#mnHTxGp7PhCZxlBkNMCC?emmO( zSnwFQEmg)~zUL^+kwq*LvM;GP>b9Rrpdc9x3s+Y{rnZjm)+EMHdR;zz$p$C*lok9u z4Jf(wt@(27gG-`0ecyNHbHUGID3mfjH{HlV6d@mN(s$4z>S00st@)*c8Fl()!zV|- z+{$EhH?OEYUmTh&`WL?G=+`v(p|lu}FoMbR=O%Xj!pzQ;Xl${T^GW>fd`D1({}bR=Kc?bUiMMrhDCm!SbFuFWfK%I(J~BQ0T!ZH&7!mnf zdw;(CSx7mX5OdY9Y-)49nyzaSKHR!gqC+QEWz^xi&X?3iXiY~*Ol&dc=+8@lju8<%2sP`siMi!A<`fg^IzN9a z1MEiVv$nSML28?fTi5Cd%BPOcePv62g<}!3Z><)sRF8F@b%cTf>OE0~nW|p$cvzNwoSnXtYm5G&O8;63c_D z$Y$OG-7x;9BB$S%ggDnXr898j@3JdD8DSBaaBg(t;(~K@XfpZwaktt9u%$!t`mnLO z=3ZSWE@4>;WzXW~)tU_~(iyweK=R-71_FdDN%e36pFmnmi4+UET;e=$G3{L&?10^s zfE60num0tUu+OfDfh{G5MZY&RJh9!1@8JUQBL%98|ZS#p#o!72AaNVN#61u!8XqA#&Jn*H8--l?m$)`Aw@#Zhfv(C2G`i7Z#&yM zcumD?w+K72DOI32YTZ#*;As5`#U%iLsE-8ClBUXBTMFTTk;C!rU~W;**KwZFNTcPq zn9(%c+}T47lmVcDDu<7}yy?TVzEWTa`%!pv+))LFqS?qnLBS!nTTO!FkEM~XWHJtM z9}@qBpc>(ni{2$)YO{=FI5uT*B^$MT8#}Z}5bP@Q%sGd31P>$tRnITK+z;yAy>GIiaJTk+^k~lL8Yuoj1%vYn3&nMdgCTo zHjYC;B5rHTZ<5Ji)6|i$kGZzR7HrUy?(klVAXr?&9ZfzmGBiTU{m_df%HlgBW5SFW z2j`N8jJ+cQ6sj&o>#_^|O;#kxK7Q+Jww1xwWAzgytzgujpC35mjk`=? zu6wuEaVe{Cz9;`sf+lZw3jdp39P&hkh_$es_PWHG*w!>x+piJ_FCrk-M}_53L1-S9 zluUTy&-w`7tgJVjJnf>>iv8qUKFr|DJAXUz&Nyb~&6!NkxKhIW4UY+vcTYiGuq z$r_qo8Laf1H>y{x8R*_g=}A5PJA~gQdP?j`B&@y-oo}N}VVg%rW6;|H$e)l==1N~< zSkhpTW64-v@E*#tjPR0C2||!WN9C{Ur=`NAv1=)l5ovc9w9-yMJ-?D|&CT=WRMsRk zHQ$tq=bSGIQPgZJz5g_RK)zz7=*XXtKpEO^rMWH|X?IS3Ag}MZ`Zu!VrzvN9Q`{{2 zT)%a^DoR7&OpdAfu$n4ipB`7UM1=AwH;4O@^;dNNKnwH)Jp$ohLsn4AHvasf^66B6}2&rZF@vwesmwd=vn&h z+bMFsE(lB@?M*~__>sp$1WfEXL$NrJ(Uj!@!*u5mi<-p(9vqTv>2&daZzSM){+(SC z>rYhoT5lndR94y_1!$rBp`$!GFM$#bU@^E791S*sB&HjTt<1av&QD||Jd z1)P3HK6qf`DyN`gv3}#$%F52osbuC%n*QK2omGK4YB6%WDG=KV zu8myK`R>`C)9XpXKQA14NJ#5DihSxh=4vI2(1#b#|C3O5Gf-sLUqenrE%p%L-e}8a z)rC@*_VJWs_PNmA$yG5W|50DwiuZf!szMP{=C-z3xyK%&^sKW}i_bq| z)yw%8Clvj)Uzg`MOfHEjsrE=?C)U|IRMYaT_DfR=S^wdNhdsF`YfyUq@{FqW0J5{kA(W_9?p!xvQAWv11Rh7InC0Z7XJRyus&Y0R9`_*xPOLxe0*Vx|4-^>1Abk%oUsPK z#Q6x<7T*j5Uv(rGVWqQkb}M==d7%I-@TC*qn2W(+Mn%tG5xd=)HL@A6o6FrUq!;E> z8AS(lT7O>b^uWw-kZl^;9IKYrjGrl3hHbE~5wI$(8gr2p7NcYN_cqftB;UwJq9Zs5 zS(=7MnPejuLGjt8zWBCtZVj^4bZ`$MM9Dxy;mpyN&_EAi(7-Z9_3oEIz7V5Xsmj=` z&D^+JNwzUs;XpARCpY#uOBtNh;Oy)!@l*U$GbdM3#E@0e73{7x^xcux0QAz#Xo1My z)ArXT$(G={r(+}5Q&swa%jh%8N~5Qp-d=UF)7RW_5IVrIF@=R^>g{8tDnrX^Z}Q-n zrl|9A96e1Cfk+$TUoG4}lr)L=5A{;jk;M-7am!))po@8!T^c~WnLf56DOQ+a$ZB_q zJHtQ1*U;_zM(Deu73R&t7XJ9AHY+`Af4R3`hOb$1*r6;A%-@9CUbJRXdZo!b*(aq( zhWHk|UMTNmQ;yf`3MeQF)VLZKLaf zi}&QERsF4pXfv7H%MEs{<37I9R}7_^?M@%X!d*K=&0Id>O`sqeo-}~TMZV9knEo+e ze32<*bwGZ>%AH?Y_q(5EM7prx@gt*0ttQ~o5Z?;@`t0F&8)frS3huFu$~vD=R>SugoIjgb6+-wjP{{sawqMe9ifs`?aGTN=~9#H#nwX z^AtWy+QK!`le8=+sD-r;4y)jAF!KAZI(zP0G#F*l6?rlAxwFE7i~Y9gJK_Uot%WQv z1qiN+b}9lHnxx^)YK0vW63`_P8EL-GX@}!;92g8NCCZ-{Q`GH!VKJ^=UXOpF4RzL| z0iUV5QPXF$NXNVS2igi3G&g%%GuhEQ?3b}nvOc`CAbTGQcDCs}MqTN_i65&Gqy!#1 zw8}4fpo+~)p%AIddoZJ>!KxwH?R9UiIfuIVHwO|-vj9XQ+ zK_p>?$*81E#ygO*^o6Rg_Vc#<$r~!_Zq#T+>$6F~6~;Ay`C3vGL*=885HNJYlu8p| zSH`boza-#y#C8FFp!k_xpU$FB6mX+YL!dDB0iUct3rp8DP|)deWSP{12fxlu6^mEd z;+xIQYqxS7+Z-?CszvSvIW4tMDcXB_WUUEqshG3HHU{qRb}_&3XFo&H0jR1&rZZ!j zHwgg(MkP~;e}AXB|hsTvuUY8XtF~5HfB@1f03{?ToJQn0f;X> zS*f6MHvZ8`^Bsx{PaU;L_Qh*1OciJ9pz-&@qA7QSGWiyoMA~{g^-75^gz9iT0&v{Q zyS4j=a+>4z*dgvnxP7cuug0XsTcj5gaCL$75|?b0mPYt{(+o2j>zZ}{q4dAD-dh9| z>^zsR6%kRAzaa4`W0~tk_ z9Q+C`ehq-r)sBgERu>#H3<`kHfzItJN;iz*vE@@YiEO;$UK)&Bs{|SfwnpPoNKNzYZ9yxk11_FhL+MU>s=9lVIOp0QSSc!%t$Is2 zXMMtNm6y{!?R?tEk9kEFH;)F{j{GT2`;jADL2encea#gZ?=7rmcpz z1fY&0T1qymY+w!ZftHzQ=q6TvLHssyz#M8rg^6TWD$fpWh3R}4vdmHybPc?z#Vwpy zUJ#;4AFr|Mkb8HllNhhH>CMQLc!j8q#NWR_c#lfU!$YbNU075a-$v527}%bX-A-Q1 z8}3?mgUY93{Q+=}9F&p2-Ftw2=#oX}GfM0%>{wj4jcT>0qP{j0F%CO>NW~@XEk&7L z8Bn&|ixq7jn=w%igt3F>0|>z_dX^ zfM{eacRnj`(pWi~Z#S45I^*c0It=(9i0n`re;9iG}we2Y%MiNdVK{Rp>W=k2ScoYYtA~DW9)lwW9zS$3iR&LV%X5bxaf%6DD;UX!CxIPf0UaN}Y#h%2MipYBNvc2Jb z;NQl7Q=J!If_H|K45BF4-5!HFq757Ps1&rdd`+RK+LlshG@$`wk=AD!8}YqF>^y+N zAi09)Ls8N9j9wGD){5VnK~M@Emi%cXS0Q9W%_-<`GZM<`)_P*AUzXh;>Ckg=AfRq;Z0DLp`h|U zy~`iLy0laOh#x@wXa zjBBG|qOtjhW(4yWRJUMR5`O)|jvM6w+~$zH$k#XYvTnug5sw=wj%xPiti z*RPX#Kj=}3h+=mfLWOXFV6!iG$ z8Ug+hBN!>YE^UA1H+7S}>oWpm*Fw(tYnqBJ_kN+i%M~+E3lA__XVg32KNv zCTq^O(}*(MlFaK_8}c`{KwRIY;1GkNo700D=qoLbx~PT};%m&M2~)FU)YFnwPD}m% z?fMRCevRWw*w4@r5SgD3NDk8PnE`zV+Z8n*3hqTkF)+3%?JbasHpcc3eWI)QwDG$X zLQah=6x8+H!+s_)o;wtIun8ZqHw~72eaX z5C-{&TuSe?mpQy>oNQjEl~$E?ZD`!J+#QIrnt-GL!>_LE8%@#_l{NW9H=#dMYD&ZjX^a4YN)b0qHxBa_jFu@nl(`BeGAg z52YkZjAgU@E-4A^ZxF|=?gw}sy68>`oFL6AJY6e##H79AQ-;|Sa_OZX`gZnhT_zi| zZaZrer8=*fdF-Rk!m@J_GRzC1&UiSyJnVZdWh5&{GkFwFdkP14pW$b`jd{1+r1xf3 z$-kz~o1zt)QG0{j!bx-$1#|<_!H9LYS}2v*p8M8L?;7UZ+e<>rt*nwwTDy0T@bpAj zmrhsnOG|STuRYoo;;&Ki^F1k3iJgS0Od0I_!X<0I8P2~nJlfM78eIU5dD#LH4KyWw z`@tUZ*l^HzR&kBft46T0tdRgglw#AB73~Xw$<`N7oXTsM54UCKIQyhzpdz0$zOA11 zeY5W_0xfNyA!F-s(_$>xD*vXxzVq6oNo(xKTA1c~PgW~gNm(j1`yu()FEvBMp)8cZ zroc}aPj4I9OmhF6lj^#x^zFNDHz)&+4&ONI!18N9qgU)k4L=_-ZEfueDP?5wLy&K4 z56N{>`aFf~T|wNbc|;_Nj1BtsE=G`*Wn&vmrNJCbvtty6B1=j|eol6qhdY-je(mR%8`-dMd0RmY~t81xywH|92;JSIqZGrcVM@RAB#*g}U zp(6gPhUgZoW1|%0**WXc!v}A#IyYUnpk*)7t^x*T;bUn0-gGuhe0n0lI8$|xpb0ru zv1>1O3Rf@L#flb3|7wZx~|0n@Jf?ZJt_#5{Kvo|GFHlPd$tj2a1GE1UwOT9nHCw z8;nyqZMKj$;bNi-4^8{87B(SnLc|t)P!QNZYyktkCUo&*(l)5gN4p#cP3Ve7y8d4gbvlWbx@D z7T_BR-&5V|xV>~a7B|rpNH;qgSn@|~M1CBGeNYR`w%PO8@@ruI+gJb032!kVl{Bb@ z;P%1qJN}YuRX^P6WwbOwQpxw8?!R0!G>MR}L_HYH5$4{xwu(g0*Gw?&+MOZ3u z>x+P*qo#=kt601!Hy`I6PQDe**2wh2t&X}KpVSXb>JLJM>j$~Qg?a7SvVNBwi$WpY*Hcb8U8yu@%H5vBpHW&L^wT*yVjwv? zuf--KTau&^H#heb4}07&m@i|#UUPZoo6(CzfiV8*{Ca`^ai2pYA-J?vH$wh(M9|N< zxu5{Jc;MN|$u&y16%1B>+&?n%PLU{9hM1W6F;5$z$nH;e#ZPbF%eMH1KW2y&?U%?D zV8>!3i#c3F-(emi}*nRHo>r9RZrKpk2$#uS{Dt@RF;9rWP(hZWV-T^dVLKm zhE!Bk0`9FLXNer1m4y|?6#aJ@Dt~ET-yD9^GsoslY-EX@bzAn$_s%VEQ@AQ~ZP;_6 zr?Y%d@65XiIl@(_GJTuNWb`etG z7$X+mjmak7z1<1_{PSo_w%Y3 zVS#4zH6aPyv=B-Fv=krh_98X zTC_LE*f+KOy~W54kGm<1eCP}_7nr6}OV%D#bl}mo=mfgRfOi7l)QqMK?a%v9O)% z-(L7&xU^m412K%dcGL*{eIm&8frBGsyDGyBiURZ$*dz_%QQ3BoQRKM(>do^Me_Wg5 zKf4P~BbS$dSNR8{(n8YkvNhX0=8S9%=ICnOVN)|gA=&`GIzXu?C2+885QWSP`nkMhWk zw7Q&w=XEq!U3$kYrfaPFfRA^xvmR-tMF$@yIlyiXqR1Ic3-v5XECRkOzQR9L4W4C< zii?VD9UU!AO-=PFr?;WEl0`2lezlaZ&B?Os@-sHomv#%z%+4+wVb=^?zpDe|y(Olb z{ZBreRqDyE{zMv|q8|J;Qmg%UV66q@CsysWpu5tr1qdG>e;tFB??PoSnBSJTl4U+v@Y(!aSJIy0eV z-bQy`Df<}ogc3^UVg+j;G=>lRCu@0KZkXe{Jf3)p>No%Ty+hNj1L`?)d5ldvkyaXQ zy{r1CUy(-Q;^I1;tsET6;RQ9r7^%-{9D;&}>$6|pr4ASRu8sheS43M=R{OL;oi@ey z_gJF7aB2(_C(EX5UbmcTQ`IhkvFw#W8>TH=QqG!A2jDKWKnGV_k^>{Lc@uF=%Zqbr zy{BaIJ;RD{ZRC4{v5oUuxp^;O?ls8*py-8|xB(kCdE`m9GIIcRZ<=S5`)2e{r_@?$*Qm7+>;yco^|*F zhq8%C0XQ#ALZOJVDoGsz-`C+cs-(?JN)~R&9WSD7QRQC{_uATo;xKh83a4$s#s2`G zdTSAjg7+>a3uM1#jqIMI4CUof9v&Sbe%p;GX>Cdp*<1UY5QSt)gOaCj(w$ayY?q?*sVv zkooJGV#j$Mx17N=#N(w6jGWzT)RwZ8vyEtXw>{nh8UpW5gSvTpp;oh2TU(rjwaxrB zEOFGjObZ$K;GVFrL?&OSS5zI(!J$q=dm~}dTC*bEo4fwd2uZk#rLOLLEdt%2h0B8j zyO-YMK6*QNv<9J|2z`-e1j_>`t%4e`((l90ck08+7OZSptr%6gT}JR8=bw8A_AB6XI%{)=XDQj>IOFB(Gb6+=<>fBg|CuIgF zjwKMi`0Doz4f!)aYltJFkGZ_8qOa;5_t{v`R1TPR^?z zD--PQu$&@Ao9>foJ%xF{q}j7JSx5IzM8K)VKNmO>Tq>qs7&x_pQHN`cDmJt=C%r`Z z7E^?2WR{8&?=XvVkVn!MlA2a@vW4v-_jgH_&I**gX6QzKVY?YX;N{t8ZjP9 z6~yB~#cj5ZJ05@M8BKLi`g&VRnDgI=UTv52xfTxvMMe4BTcd71vMsk=Gh^Rf%gI;?H*nkcS<|lB zEflY-hT8*UTHpWM#<;HfqH`{M%?-OYeq&o;*cRQ4B6#du;-?nD)&nxaZrcffr6Q8U z_RZANsCn^6wQu@+lcxG< zvjZn^178%zZO4%ye2y~GFDV7aGo7mcnVvniDjWFjZ*tw?v}xIW#qe`RVo?zt{OG)` zyRZYSD2l=I8o=1-8CT2Is{e3DAMgmJuP|;6$6OBv=2qa0s9gUfs0oi3d&taPkeUm= zXS8u3pZ3{)l_N#kxpOfw?HaqkQD0%u7|ugks?~6i78H<>Qr=T)n{HlJo>@}euPkKP z?gJ<#r-!-VeQ5W>%L#rK$O&rrzEFV}Ls+3#+`2$m^Gj87ive`FK8I-)Xp1`-IT>(2 zsi3m%(;LFny703FF-y9~~L~aaG^y<({=k226l^ zi=EMqvFVC-CKsm0zWqh|PK`aVk}1B_Z+S7k*0XDsV4eoQGQ zV+CR!5oCPdJ;srk?UB^^#q5OP0>(G9E5i*xKRP$4AOem@hl?f8`WC(@jKwiAsBr1V zIn%OQ;SkvkTAm1vA9KE(18QwO*x%Q?0h?=UUq^sGH(kMfzjc}BHZQ$HANvQ)?k9Q~Gi7kDcf z9lUbZb5d*J-Qt@bzG~iWTDz&qW9Y>czNl|~4bVG5Q!FJXpxa)lGm@Z|DIn@Q>9^-K z=p2Xr9nh=&=LJiV2NYtbH9V-PrRK`m>P9ynETiK1TFt@n*=Db6agikHz|( z%=R=mMg~&prIkGyFapehgza&Y;^XiZBMyV!G-6JG^ps=_$??=`zA*b}ArSRo58yr@ zHJWAurNxB6pwY;o3ngV`7MR6E^lN~*`Bou(V9>g5a((>_-i$O-%|<6Sy4O|cbEj@g z49ELtUQyG&F8s4mY?sjyG|?nxHwA-FSnwztth6P6z3V~!XUTuD@Bi=p|FQLvl>YYw z3&Z(ms2~)X^Kl3*#ZB5KCw}p|>)D-|!S!m*GU5N{e`!nwvs8+Ti#dR!8XD{6 zkFfYr>wh4ZZ(qxGW|{>B8Fwen)D8!)Gr#{YhI_H=krqjb@xShgf7|x7|1CUf8a8|3 z|GX71HIbM9PhGULWT?>m7u6dfU-he_0L+dWL1ber^m~ zT7sK9JwVnG$DgXeES0G_je61BAmOzs0L$LWx?mi94=y|Qs$=+UH+aZLNv*3hSo+1A zA>dNOE(X8Y#*em2r|j>3M2{a0-X^@fjNVJ?WPW{VV7pQO%>kMDgk9&y9C4f%TylRD zC6Jb1<}=54$Tt1(o~5JD*Y*TdL*t-YM*!EmD-8C6#@gf_VP%uE54?Rn!F0=y%x!^f z`|nus8SuFzpW3djF)V7B6d{5NXP?DFBnucaqRbRz*Eincr^XZ5?bCzz&rU2nG~y#{ zgjGBWdahuYoECqfR1{p>IOF5RpGH)a**Q46c6N{iS7)}yf7KEd6CIz~*lI-ppi-au zOWp4M9*BT51sBz}gpeW*J>2%lvXLph<=%l?B#VL&rIC?5XtI@8;&Z#%6^ivOgt zm{`0j2iL;o)o|L z1OH*W(dc0pm2w$>i_tecEIgtV^Wsg9%ZKJDSp<0{2f=xSt2;eM(<*HruNIa4toN7$ zaQdusJIW~v^z{QyZ<{dcls5$^=$>%5x04odQ>2Z<7_(Si;mv^{IZq4jjPsHJ#8`Mv zTiw?vccUvzSeF^CSWF2;Wp^Dn}XdyvyaeQJj-rDlM5airb1GAKDjeY9ic*f*Y zS&yG|mb^0=fsOk=+Fy&`Vg{H6hGp{_Zl;0{dS`+QO6KVTaUW$QykDpm>D}na)C#bFUv39wsuWsre%!ga;WJS&!c@2~ZH3qWwBWH_pPw{`l;~+dJy1y0Y6_Gv(DDnOwAGp|{}UezxWWGIEsX z_X_izEgM{mPDnt%Yu;Ntu^t;M>4}N{{pxCMv{z#NYm5V|3FN|8MAyH-k2E1#|E}}6 z{0oBP`-w>&+B+#$Uoch8P1J9HzjRm)_v$#b_w&o)0}fuUUnw%T^p@=GjYEcT>KmVB zb6eBzHSLt)GLU02^?{WO>{JXHGjmf0laQJk4mt>LFIlS;{I_j^gjx_YA)*M0GtWB^ z&@~3JYA#KHW}gK_`kt?G{`#D0tfNB~?1o5ToN7?!Kz#}ubh(=_IXymm+ z-Z_WU{*iKwyCiabhQo1PgDl@$Abc@@SLsBVzZw~nBlTNP@Cc$XPrJf6#j~@0S{`co zP7td1bj7(g5STR@iM??*H^E&q zUb}rGqj~cObCBQ&N8RuK6=U;tfDq8JRvujPRSd&7N`h1245x&%H zBaK4)_^8HBHp6{+Zt&4i0?rmd9U`y?!e_^^Npe$6C8oqyGsrW6!?lOR?dW=b4Yld z<=22}{AXn|^r)yZ8pzzMk0}*YhPXpwx%hk5X&Kql2+XLE^vq0|zh>y4YpJYnPG~Of zy@mLhYJ(*5J}f@$8ZpXHNyqs>FFEMa4L;P44b>9X_(bl~VrkY|Tq1Co6m!sHDZlYd z9Gk64I7m=&UIq04eJRT?eEXOq!;}W+{l?}ZVX(APM)(Mnjge!pbW`ul{~QV; zW|ZinLV#jxv`4u<3J59lsA;M74g~R$;V8a(C(PRW<%ugfC1nvSFz${=FTJ8?y8i>7 zHeQTUAdxg`$ z{EFecy41+a2H8uN@ILcxjkNx7WN{$Z*E9g?8bg_Z z+8TFnJ->~Xwzk?N=gz{Y%Bu{fsR=b58ME?F9E^AGMA@X2N0Yy27_Z5$XDNAT^Zbx& zma~}V;)LDRX{$$5&u6PAL_h{|3J*_E`(W;W7(5-buX_f;GtH(BJM-kKhFf8H@$ zX*|lUD3U&gYVaN&4;MGvGCI<3_|R&YmbDRDTsz(VHndi!vXXv*1P{tvL*5+uscQV@ zlKDj$QziESJ?ux{@GH4Foe!*Ndd5ac9X3c39EyrmcMjg_AcRCb>ms`JYy(T`TG;Q$ zCptspr|@HMF-FK^n4ty?Wq?Ap`iE4lDrxbKywFjlZffsaDf68(%^JPuc3^hE@JfDK z><@gK^yFE#D4pAcgbH>hgNL1EJ1IqnSG9MRfXWJXF`>ShV`1#i!fYLEKXiY;0v*`> zuCm%7fjQW<=M21(#W1P1Ff;psWI~Ui!8*M(IkL%4K&85FW?d2e%Aaehdaby!KKvDd ztVUIPZUHl%tPKS@PvbBWH>Nj0=#vm#8by%!7YX(;9YMX&Y)ivG;nL zFHDcF4~D6cKdX58TUdIYvG74$wp;GX)103@UDzn-ZlEsE)kmwCfaBOF-ro_y%0I6I z;fCuO9SNhC#R*1_DHV^KbC07ICI|FPaP@|{N)$Nk4M3B94_`0sfTnv6iPi|^wEkU9 z6GJ%=xU)shhl|^&sk1EMFpS>6Ha4K@L>9dI!fJ@3Nz#vn2Js0HLG(xG%4~y8@U(Xh zO)+~{lS<#=&~{Hdv$J8>GT=}8ZR7eyMIoJuj300D>nCL6YI#uVrRAP(1`aWuPOgZ2 zG+0)xw{%8>+&am27}j-nf%#QOj^31KVZg;vP24J z4DUa3M!N_Oh?0i2g^)Gec!knhL+$OehZNIqeeXUH6RTM2y*2aa+RrjHZz}vwNWrHR z6XUjmCpFo|tCQeL&z7`b(niyVWM*X@wR}pL5mCG-w_Pap7nfU3P;*@7{tyj%9a|je zk+=l3dHXJgN|cjOF=-quUj*>UhMp=c`ce5#abZ-rZ7Y33jbt=z3yAx(@sV}Cs<65! z^kdFPcP0b?7 zR;-#7JcP@(jOb@Vc1(R3hxh~Q5QG4ld;W1bxxQ(Ur;p$VvV0_F@7NOddrxKT4r%s2 z>TzhpWIx-Js6*ZLF*}E;_c5w5Mmywsg6RVDt{~S|Vz5B`%W!-yNICr~iuc$0%?Hcs zkVr{}o^I_bnWciFvZ%!TPoi$cSXGs-977n=R(Q>(G899yG(M5l4cOxJ&i@|(+(0A0 zzV~#~*6YcrDQOU&dWF{Re!3bSlX3J2rH@{4I@*VAYQ`MDe4pMRGNTrjc@=gqdJHRbFjqN%$(4~lXMKsvXRwgF+!n; zuE%FlR#YIZM|mH956CA!z=s{twR$?`QcbuoTThrG9dxM1bTZBpPIvjNJ8z^G?ES;V^2gVUYGyhI0u{94c#IapDzMbIywR7ig=QLx@4>))B z44(Eoh)TW3CYx;Xe!={spr>p}?&R-cyZGDK?w>SrS#vEIx$S@)0bz+u`mROY`dmRt2ME z#`)u5XGiG1Q?fB8$cpmf>g`r;o{S|ZIFk6d2wYrasp}YFVd1Ct`piwid~+K~=`SKJ z`B_Z;(FOjoNz0njx)EA$rpZJxfP%70 z@=xu@KwXQpTOIVZ)^Y1b5ql#%Wpc0uOW!!!`Wgv#)W?2z93?l(I2h%Lj-e|L`^U&h z@JB_>h{RJ@$Ul1+0~KYWE}qd0#l3j+;Z%H(aO+&t`tf=LveBQuN+4)XkNHY$obT)OiDfTHv`w3O7a zb#TYQ+5%&9Cn}oyHwaM=Q5IWqK2(Yn5DrwLr)@}DMl!b62H1G-rM0Vza|gUoRM8~k zZr{d>${2k;Nd3(`thAKad$oBlt)K5~N&-ut8PVnC&K77&wMd*IJ94nF+gmeKFQG zAnITqtI|bM-8C@@KEwPn--=+QfjvH!=;`TUW#fonP#6bgGSxr1#LGuTc$gZZr)z?Z zqdOtt(PZV`VPRFoxoB@xHatg?)maifZBeuGrD=F+gTVp4n?&13>C^*k9a}}(D&(mC6ij|QrA<3sHzLJH7zCNA_XP8~) z8}!#+KuKX6>6K0N))r$T8!!Iia|EyMVQrv8NYXW$Zl|K9rbf{IG|Gze@vt?)(sM5j zO*Ob%8R8szh@#@l_-|LoCgLP-7FStkZFL2aNFO`#nF-my6>LC&oh)kF>>BCmErCb$Ai04fI;4^{L_z^QHThTvWH2HG5R;o2!mhC6v~~5-RCNs(OI^;~eM-)L7yOPE zGA$6%asL$hI@`JW_!Vm_O9%u4@)P`sI$X&QVv|ia1+#`vlG^#3=ysXlb$o07`UhlV zyT6ER$ME7f<01(&SPk%DzBt=MeMrn}YdME0`1INN@ZilH%`u znX5mCw8b74`;Zp@8q%VlK`N`4r%0Cjp9|(V-x;K0^5UIQP*P!Ub{^TuQK)>XNN`dC zGh>f1)YK#;=f3>Cou9rNHEmbA1Y%y@O+!P?lxq(Lndo`UiPU6b_r&7m=7gr22K%qJ z$$xzIJ}$-CPzytwo!ogb!oY)*XsQ};uB=u5!T0h}9;VvrWZr#2-X34H%^ay86(O2v zC&YLwI}hJN@^!&f)FA1618l4>Kgw2x?L6xrp!jSgD%#F8Pl*su)U!idmFTlKcu|)p z7tEWDgTM-z5xX$9+eN={jp3$3^i)+**D)fmdVn>Fl#NWZR${BGfQh{a1tnM6sjrE; zt}6{=0*0Sn$4pZVzXK=O?P-anqbE+bhPWqOq@jV)kl=Y795h~f{AG89{L(OWEECYk#`U`XD2*;{Bd`-MomqFlMi08 z0r{L0&3ACnR`^!IY^%yeUr~kp<|%kvhpo8*_MTxB6c%#rdLajPJE5Rx%JqkXAerN2 ztR3q5PDDldqNZ#>amxg-#^q3Jl+3)y%G!sv!FGZI+|V*{TBw_cO##qxNw~9tk5>J z;oQ9@F2(zyYUoIf{BZ}EBgu0I>Q+89k1X*eQo8P6##log*T4uI%(QTg%44u0A2V%j zVpA&-FD)_JbQ4=WB@$%|Y`4^Yu7k{@sGoyPlE z7k?Dd!LE`Cn#NWbo?Yfq!#KMxj?gi+3c%cyhy(fE{BGzEx7PPW-sW$jTKRZ?i%c+| zZ3^c9g8Aj3eOzoUVAxfiNGgRX-sq!k?1HKKiS#0TV z2!yj-O$tO?&k#?)KwKTn&@}U*abScymk;BSSU}a)OahZHQC4u2==3X8-#$rjLgtq{ znB|?cJuV_KAqr0qcY-4ldHmwl`d^^^sm!KeRv<0?MOnUwJSDPR`yMnq!$KdD;$KBB zoh9#bU6!ivmS75ot8q5g!g#waL80M)Dc7^ZP}lWf za0x~pWTBy9LGk@oa$=oPH+SXyjfa%wW}&C3N^(gXy$_4=G||J*+J*AQ0hU*#jMwL) zrK-l^{Ko*a-Off^O@pkeHZH~cyc0ff}7?fu(UGy)Dpp@M1tDrXq6> zYAPzIsHqc~Ucmh7S0%jSyzID=3eKMXL_~z+T_)v_rv=NA)7*;3kwW^rUr<%u$jsZF+CNn5<`h+FsAQFoai$zprgrcNi%(bckKuY!XbhH(f(9%{z-zkaV z#Wi4+%VCym)Ai)h(^`V8bx>7P!_p&;fuUzOnCTOdl*^iQiPJ#_=&h%qW>)_Ty!5h;z^(7A5ek^e~D_D&*EQfAQLVDTbw?yXa|X zlU4K-q>G%6w8X^em`vIZvPD6al-%1qsj1;YS`sNI3z!l~-z%6$%ASE#Le;4R^!2nc zHZsK8=OAyFrF4}XL|N6CE06ksCC@p5~!?g;$_!E+;)19+c3_pgJB%G)dN6(eF0Vh z8N68nM12JKxlsO2FazTXS)3lDtL^jg+vx6p1IwdC2RoD7G|uXqYMk9%x!tt@Kx<_- zHs0wp)s$jm9Y^2HDgfn2g9wT}$E)^-Tq(WBtC#%@4|J0n=ERAL7Jx6;^0{D&fR%B= zJa=;RS{ri2lOzP#l6Aj@mg;hDJ#MD2?Y8Y&`tl$DuX?53B5kbs8GU~tY7NQK}0JO8tMkgemD+D zsk_sOS#mRf7}oleu3skEEi&0|-W1IL6TzHnXSeGvGAjFMyqc_GsS4FtJ3kX>|_XLa>7)>aYj=}dCo1MXZo zjAML0BVAANceBUD#*r&koitUP*dUnR zufeQ9c8aVl-&>DdFr_Una>10A{MyFd&F`RKN~k&&!IqCy$;f-i$k-TTqr;RO-Ob0J zs&VvWGU{rYq~%uwQt~r*vsKlF{v~*QFP*L147hs#1(zas@|l7@u}P_fdf1|{O^GO( z&>iwM<>L=NMn%^U7mr;ydd2eO`Ew5Y*`aT`oxreAtj&!vw-2FXYL-)>JJGRlq(=a2 zZ@UTB|4-}=lx`GE>M!m^A#kK3Fu~b*UC+;9;_xqr#q(gRD6Dtzr z2q)VKw^T&kB!ZTK5yr;Gc-iy_S0e@VU1GR*J0IOoKO*$VV~O%{Z70sY5#*Iu5$$P-wt+PsZuYV@ z8E~SwmeqCHYt84g50yDr(E`#HuBOMJqpF78P7j=IO;Oizp!7)(ce6v#(6Og}Xq?8I zr!Z1h!`96mYhx|8?+l@B=rtz;-O)ENWp|Jt+YPnxKT^Q-!~pSL=2*GJFtYk?&c@qp zSNwpOD=mDTskZw#=qeF$sTHQ5VyvY`V8R8~edA$T03Us*N&ek_0A{)#utQIQ5C8KM zBC{*r-LF+H1ex-m3Jx@OjZvEB%LgBPhJVHb)~0GP(f)kQODwHT5pSW0`?2f1>1!px zN+0j2t1PpQgwFdHu`<-g$Yck*JUwxButG!6oFhdKDL=apeGN6tZJqIy9ka18Mc>Sx z@)xf-8|}rGPqtyc!wyG#TMP~KF}8N1sJb7Cl&>NYaVFAL*8W3Ob-ZYuSVFoyPG*QJ zdinRdvrD^&1hoYJmet~}Mv@_+i&$e)>x|!>zLfER} zM4u4EgB7T5RVKBd2BZsQg&3i3n@VqM6<$_qY)mRo~D9;Aoh6>$ZrRI*`wDJ%~Y>TAvUx)E5LB`d-W&yaX-lob+WZ$?T{ zHKLgz;yoP*N;*SX;W>8N*^yoOlBu?0T--c4eYu!R$M<665kbrAMb_zkdKokCR7QmW zYgAo~V~4XJmx{~D%}PX1Q-yQY?K~;X#=#|wlJc7*hx_6mp2hTvly&l>JqS5)oALD?s+m5Mfy>Wb)^}6SIq#{i>iVj_uCA5$6wnU~gZcBx!d%X2;PKga^jO^W6LUU@ zWZy=9IkleG*3`1${0*9~Ymv`^X_|+VO)iFmU^sOeTAldA7Vf?QmX%0@=^}MU7j859 z2fgasoM?U%qt8u6%P?lhPyVJ#9!)6Yowe1xkx|Lty*L#Yz*dQ5?*+jRcG38UU%^PpZz{b`DZY+PLifiVqfVM46i}))_MHt zKfTE5n(G+4CgVlBhpMA%c>aY%+WMUg4>|D|8fRA}^U7=Qu`Mr~mlAiNc?MWL_htV6 z-xFx*byIz81=AKp4d$wM-elK_T6U$(B4KtS>sIG5>E(Z8!?{|9-R`^85GsRd-99to z1XIe4Gnl3@n6V#^l{z5wxiXjmF6`SxTJ|bhx^*neifEvtl=QSTwjbS3M#fSKsyYCR z>I=Kb$lAqF7@TcIq-U+5`u#zAnoC%ennGG;Hp|!KvUpJ@+mDxWc;_mTQ`5v4nPg_B zlajTL=FVZ9H|trGlTKn%5{c={C~D{=WQ8f(vx%jvH_-0~OYddh$|TmGYQW+SUDc;q znx4$U^b9d>RErnqQBrY&)RbJV_7728vX6yXo9GV!fj%~6XL6{xfk4pU^v>1Hoj;#t zD^`-bd>QNVH?e2`Hqx?|P}V+x(Q}Cv>1iz8c>z6WA#wL!Vb#K9@{Uv!w%5oEX&m03 zOZtlSILgb(%}QhU*;cHOoNpl;Pn3ziCe?+zL@qlRwgMFI-9*NcwR8*yz^1qH9GPN1 z`5c*%brR*WTxxG>bk<)WJ2jca1xc*jc$5MCPMp?VOF@~B!T97E-OEv{H9fVAi!M1AFElL%!iR7%>z_r_6 zEYWvgUXG~m#c2}3b>kANGgCNJ)x~JS+I3+@gz2BL8cn~-s+>jaI$VYw%9Vm$q^9Li z*?Jp^<-g6A{N*f|pF>%5Pt2559a>9LRz5d}ymU7glbMo6afb^$?X1XI%${Sl40jdr zx8ld7bC1=a4|VePbAQ9X&)!9dJB0iOHRleIE}m&YVq#>kZ8=zg(;L9A-{$n*d=e9r zSg>FLNvT;JExCk06r}jT7O}3WWTa;ZrL%Zx9;Hp!@CCz+#kGqENl!~+`L^>ILHiq& z-7Rd*&6e*%jJ=psl^q1bK929-K#n{e$E3aLBnwj#NnM!1hK+e-q-9fg{T8*31F^ju z7;GXdeKCdAHvpf=Etl-|1tL#BmoFY8bD?-=$tmRI?xej>XDs^KOGr&giu^uq(C?-H z=4Iyon=qJvJB_+dH$)6(7Qg?8f63hBG_u9}O-j!dhWIw78RAA`F-tQRl9HTE_Og6h zyN0p&V7U9*$zPd6M%E(ockX7(k}THkKS{yCy_{+2LDJh%&W1hb(Lz9Ih;7?8&^QpF zy<{)jkJXI%xtD!A*WtM81S|%x)sVL|ovh4ER^;!ZdpH1ASgh}9mM+dDJ3Ev0d(JWJ z4`T*h6dc`3W>zNY*-JT5-bTmh{YQa@VY8?dn`gTKn)j zyV$*Y1&gw?$Xc?Irkk!2YtndT3;Rx7LZMKMV=xmO{CtuyS+nbSY*rnQj;VSc8$+Yp zAD+>`<|a3{x`s%|Dd)-8FOXO10>aewcrZd1?HzrrJK9D?+Ys-!++capkZmxp;{~+d zKGqy*qon0F?>F^O(l`j=Fsn8>$z9&aJ=1FC{IOie$$8x8 zcxq1FXJRmw!4zY;hWn^4y-3TAel%x4HHGJC?&!v?4Ny~AL0hMsa~Q{t3|^kOh|Xah z&CWrVSFhCLsJ@JI*d_W2X&Qaknkg+Up|Pofin`WF+qFv-qOE};aov5jnev)uF~5_h znsVB%_j2oM3k7FRa{RS>5g6&OX}E;k9*<~uWBB~| zP@O87;XkAyYv7}t1>y}~{G74R!N}Z#u{H>Lqx&&zTCwL3ME8t6W{}g% z#WyJb^)2eVb&T#--uW-_iIod&j3yND+3V*L(lBEASonQ1AJJ~Fg+CaM?Liq6dl!+< z+E{!)!olbq9&aH2<>1xb(RtIN-Zg`AFT~~M!L9r4 zdHu0xbL(EdUy5cZI*&BSx0}nD``Ul=XMgx7B<3APw}YAwrSjW<^dFq6>XdyW>mM{N zJ3ffVo=2{QNb!SXg)uzQb!ZiLn}E-_^Ik^nW0{K~bBX_F+=%=4g|0{UuVu#GuUxCqYajF* zk$pNg3WcJjCu%T%Hc6Pv88ti-r$=Yjj!>*^oPG}^R5RmvFP3F7G~ne-lZ#Hb2|hRT z4z1c$ERa#o&<6 ziDo@=B6@P~75*;o29o}6H?uc2^S!CnA2?^J(fy>JoK+*tc8xHZpRU1F2Gf?)=@c5q z6{$OILn5w4o37oasVg0jErY1+7_$3;;x5fyeyq| z9z%l+4h}}!q+z{_4WarS%yU?WzZEr=hwhwU((uyA6U>7``-Bb%Gg|&H5OktYeA)00 zTw%xd9W>n>=99yRdqne*fs!|5?voVGx-9(t>U zA4@3Wx88B^Q&Wm~WYR_Mom#;IlO6ov)FOUkb}>KlRx$UBz7I}u@aUVxJTRq#`zM$3 zO`lmIwk}{ZBVEUs*Q+}4D`yf=k0IkUpgGtTn&~pUzYY=qF zzRFxG6bgkxp}4yZ=2T%Y->Vd+O&F~yA3~GMM@Y8aGr26L??qJ3J+@>$c3<`#)#rPY z%A$(gkB%uzS-*EuEH2h2CF3TV_dTO>mU>7U%z4saRx$yU!ThwOP8F5G{9KsLz>v@Y zguM`Sf*FY3L!nS86bgkx8O#Tz!F)#=#f!q&UF5sAz89zO#w*q~_Py`CRT`71k0WU$ zBc-kJ<|!K^ZglMUdBs1=*g0Q}oQnR`+%jb_KLdlQqztA)p-?Ck3Weg!U@)f@^JDLp zanGbuzWr95-n^slMCy)6`?ub72u12bqhjp)w(K~eG0JfyW9`0<|2wxV$Bc`gW9*!z z;-4vgKG}BvWCuSz_aeWb45l)eO3Gj=6bgkxp-?C$#+6=SFeexBlkZ&==Bk8my;00J zW0G}SZ;q{Rj?p$cPL3%{d(2oFZ$zbJ`?qXKV<=-}Ickit;^Gv^$)0-x68*m;%15R- z_=$HN{E{-5%3vxfgQ-v`6bgkxp_mwho*;jiRmW2^iujQ^r93s=!4nA%9v6Bnp+uCn zWZSf8`Iso>y%D9nXUmR{^|duBZnVbfH*LiHGM|x(pD$J)JLh=C=lrDT^Yjb{KmLw` z2c{GWiY$Cx)>N3kPoD!G!e;tBlpjj++Zpc3WY+U zP^gnkU(m!GxVz;KS$qJL!R+kpq_3~t{A4W<_a{qi{1~cq)QB`u5>MK2bmh`tbP+oDF0N4a* zE-#~|wHJj#p-?Ck3WZ_eB<+yJAfpBClx2EA2 zxlf$H|FZWNUUDQ^-X{EKE$_aw-*9(V7o(&SE18*@nL){*WM*b&X6B3x#teAAkRK^a z`Ifq?ns%mV;yK4odYGGCyVuOzrC*qbIdel@R8&<_TUSeMZ7tQ+Z)h2qW_EZIv5r3t zqn63eCMLHp_>V=)L~kEk*IItU!0N~#I}$yAO*Tfm=^R_;z173H5t`b2xzTZl=4zFi z>Sm6w|EeeJ9t%J^%d79G>kP)T*o+i|Tik8v%NtEU_b?u zs~Mf&L2DEyI$P7-D6x zGPV8>7g37nY-nQRN(sQ`cptM{A^XTp9837~FtX6s#>l(-lJsDef$>!g046Qt?JcYxiU81w zkLc_eK&nxj>ok#`oIpxS9+Rs_nE3lZe7I(gZww)F zVlg_SiC>LY#@z5A$09Ai2A%who~CNyo?cyDL2b(b=Q7>LO)Djcr5Z43SsW7XyB7X7 zSOlg;U|NzzVCrZ}aN>(EzU22`Kc!*r8Vq6zBfRhr@W<|HclSAX&Pg=`8D=-&bYg|;_MvC=8cTGo;SGKzT(*{ThiZjq0)+Yoe+Y5 zkTdB7`A}qa}@t#uLJp!3KlJcjcXS%16f`Zq0*gxg5V=zU9`BXG?lI0i0 zp^-lc#n~KopZ*&TF_ozF{0AZ>+TVkgEy=y{AFQZi;rf=p7ADOBq0heM2fIKHZu9^Q zWd`t@-#wsv>FRwcM)Upo$A5p!;L;g0ttq5e_i%?%$xu~3bE_i$nw-ydWBbIOoWfGd z%1SB7D`RB&nAUi296V!rQ(jJ1dK}N6TGP39&DF{P_I6Gb)U?u6nvV4gYx;y3enNXX zfsaERmrDL*j9Ll#K0z!?_!BrH$|r||KcXb|rW^U;-~Wm5l5qg~(gO%iY6oCvvX0;V zX zT~Sj}z=_((|7Ykmj5L*#Ur>m@^%HzUk|`*vU}&(1AU|JrH2hiI%yi=%RDk%FpQG3z z)YE}Ct-~k`{3;ql18LjUaygnqWMn%ZB0ZYoO?~6vQeghsW28G9#>>l>L+vk%o9@zV zT6zxnbI{8+@c8BvUIyhM*1`3B8_xEzi1c8TuJiKuzaz1-4-;@aT8F297870NINP{W z)zU_JdJHdK+A(yX;_rcz{(4+uYJVDYqlkn+SNhlXm>Z~JeB;mQM-}Pu@f@g4{A$$4 zWcs)>vMA=)Al;tC`H3~DuS+Q@%)#Hq2JgsL#^H#jblW%^Yuz8TkR}b(?tU;rf5cR`9@G2;UhSDs){-=K-wy1`zXg9umn$GFb z26^#O)O2;@>+pncANVo5xkzViJ`eu!fAE=YB$=&nfS^Y0!}HgnB;`cgc2X%&Qw_sEE_eTje8AS3y{eE!9^)O2(a^!QWUW3ng> zb;ijjm0O@S?intbH7t)e^YV*N`Rw!02+V0lp*QkpAgqWgl?w7B+_6jP6pmG*1JXPl z$SKMrJ|>ZlnOy+1;zLTaQ;AGSV{rKdxX(aQ6fO~oxH-Erye9$RU}=!rmL{^2W63XV zXSAo9#F%KR+orfR8o550Co3(H=(rTRCU;QX98vZU!re0HWPhI?Q(S5)+BbC}Ufsy(ch3JsoLT7vB@pvt6z1$ntJzrpO%V3mX2khm(WyE$;ye0583L>#ojlPm{3J_y?a}h zE3bDbd2Q~4n0QJXhJ{u4<;iZE8cRuzkK#@1C~Eyr6;3Cba0tmoq*T6JZC9ebHBcSq zMoil#LY`boVvqx`ht`>C%H;RfsobbFXjF1~O44W>-{nJeXH&Snct%=QIyM&MCAexE1eHlb>!L*F>5IR?%uCF7%Na z7e#t*<-2d=4a^VJo9!o1Q2rWcrxcEEw0sn!ilS&AqEh2vyrjAygUHweeafkMby6Se0Pmk#7@1mwOgP53H2FAyzDM%tV;WaAD(N0|l?@aZ=8%w*!N9~g1vx3^*bCUXRtc-0on?8%MMaZS zTqlgPNv|-spJ(pdREmo8sp+1>WHPYWS4(_sG~rK zDvrj!ISjl<$Kp^GKYac*k*_PcQr@yU*hFG|Lt>kR2`YIxUfygj9x?k3n-v zy0;_4i(<|<#{bwD3EZp>;NY9Wu|g^QJAJy+hOM&$c_mH4zaIc+8&lMl*O46XoUgw2 zW_)Rj`~YV%iwa4MiRDe_9LD>;Ftl_G7+4gZ#t3bOjxi8OpCy@B#XfO z1y-7q_^1EzfAhr3fgk?$ANb7^e>RU#i2U}S$?lv-dN|9AfBr242Nz5>rt!t&1kTPj zNc42ZCnOB}C*ShT18+`LVoHK+`TbY+^sikYS|8xs|Klr$4m9XAyI8$^NJ{MhA)Zf2 z?A%3nG0b<59#JrGc28iwvI#@Sdo)B@KgTbtpWz%|p1<@#qSeutYR~h)NOJw`aPdjQ z1k{H-z$v2^rSg)*SO3EQ_K&}#W=X_fhJm7JPwZ2B0Wd1hhME!}Ci<`@3naDdg!VzoDRGochcFJY(|c>1e>$ z<~gO~dmIfn@aoA68hiRF2=yQ^z31lwb3@EeXn7Oj$MB_&&PZo$y(8#1w-aRVMElGh zby0x~9BRQNr7+Bywy^^y8&mO&&850L1&@d<&Qu2OFvv&?aVBqckInuP?EDM31$tA0 z@Jmd@D=3T|*)2yi=2*j{NeK3#W9)>fmSmFObaRJZN>PLxEyJ7Sy4ewz*UUgi8UD#7 zoGDCq<1nc@$F|ejP5D8-WbBe(Hb|uDDund7@C;nN)UWT3yXZ||G#Z3hjlfJv5-+X87?~I(CO(OdnK{BeJ()O?0y^^i-N>J~pfk~l zC$2&C4)&8CU`2d+C(C2yeD>K7lr(kHTA7WtO*9*OTg3W15uIB@S7Ry8*7gkSo>3Cy z!q?B7D5!3sB0nA*I}aw-_h@+AA6b=*^w;8Q=gr9C8YQu=gk+ohzN;F$Z$BlZVu3r9 z+xU0|v9LCYm-SN;-?UMk;?L8^)|9sOQx@+*Kx`Q@$qF%1G4xH&3w`dJQp{9mA+}zj zbPV*7=>7!X+{8w*5vdy(DHM^{xc9zn4jDUAqVVyrR-Yu{*khx$nNe8A(tHyo^u<7@9jW6u~> zuVZnIuVSvJkcXdH(LB1sXnhG0iDk^qkK$wRLSgSXkzW7v7@gA*<4^O_0qyB-W&t0< z^6?GtIi9b@Jv4`_lVKiw_l*3iPAXpq@!eBb>N@+#j`JX)W{|a+Mk3OSnVB0W#?O|j zu@xp;U-Qx@o}T_5LR}v5%Bz%>u|_8Py+z2bEXP~VDkB|iBmU{8>4C3Tw2VWO2)+Bn+OGxtcq}6QS-@}c#l2&?~3;6!q z=X6aTF;o#raB?A|qiqDaxX?T}MV!@3(knXYXvim|uoH=vpU__s?c+%O>NWkZLvf3( z$AE@RPX}|~9n+Q^KyG6*!JhuiZEaB);Y#!DA=^_exY*d>9~MVhZ7buGi-@(x53bEg z9PM2xXzr!8JOj^|e3lpbaj*%8>)&q~Q zOir$jh(IE2h7r3U?bHCKi`Sa({8>i|tc? zW_FKh%n#s+Z3KM--2}KhP&#>p1&hG62uw?|2+Uvj?Mt-lQ$By@C;aZLG%Jp;{>Q&l z&^|!WH~&Uq@2aq>^6Vdf%gEt1;|*zi`7D;P-g18T|9nsL=mZJw&-nIB8!Ad7`ImqA z2cG+8Gc+>J*2)-mpZzm|#chnX74zj6Ur;$VMUd-{l+0ej^%&nictrld3GeAob?5Wn ze@|LQ9_h)MRJS+c_39PASpy7Zd6@-fASz%u%ZbOnk(9+b^VJhaItF^M{p^3>mRZGU zYp(E(>4UF-k5yPMS8CH=C@?*+OX&eXCq5+J&4bx16y9 z$@kxVMa{wm?>U}qk_#uv%1;63UzIEwow@;a*O3 zF6^+-TS-cCDhXi@eEVZCXL2Jt^%?1L0r;dhAUfZ{-^z)hJ-NA_hRMjnvoIQ{U3T?vl`0jgKddBcuFzu^a~RKzMB zs|_)H_uVrRQqzd^b>OR)k>o|W5FFJijH^mCkEeYi2O=FGV>CaEFF$)oYE~954}Xhw zU^dEIC9gv~$tx?zBOnu*>X1ku530(u35cygt=DmRcEZ-mENv|f-3aYL1D!C_>kPTD#WlhR7pr`1@q$-eDd`V zL?)(^5b49WU)i#FspjM8sEKvrP3z8`FK&)DOr0pvYOaa(@T8<98=HsU6B?gLVx%8m zef5&Q^&13C5)zyp$nM)W`=Diiup0ZIJbIea`28a*;*!&e332D)kN&K$590nRj3bGj zlcf>T<5NgA_tDeu9+BBRK!lg;{V^Ilr6bChwp|tbUGcbkSD`l;dC&1|1ulWXHySMytyzTRH*(^)owp- zZb6)gw()XuWnE$bqm)!nFNzBj@U)NPN~IB=4k%5Gq_KUPs22~g^$s`tnjnb$#M%KG zQ=@V52qY>tk;<+`K3tf(TJ7h_Z+}NXLMD+OPxgWhEPOUF^}_Tl(4noDz?3-egHc^3)y zhQ-bdyrK%YI$j_rCCNN4(Y*ZOTk87v87=Z7IJQ@K%A>a-kKC$S!kpdcT)jc3)3Vx< zk8M;HI@5iDIk2)pb!H&-qYHGDgp*h~fYxkdC_5bQq;g*S{fL!o5UJ@&*gyOh_q1N# zV>H~DZG^CCj-xr!m%x${)aJU%(?$HO9cid9#la_F&O1! zc(^cfs6?keAl28E>au+7eLwIe#F^aQNvgB_c-=lL%;(lPAA&2Uv0xFH7J+F=7J>Ok zP@K#W==dY~J=?+@RmsoD53zJKtTS9UJAd-yQ4d&WpZ1~&HgIN8||on41ic}8-yC#|bDAfCh3 z+l$({OWrg6CZ6XHf8gok7d-so5h1B5L<>S9tVA>}40H75eknR`2o(_;M<7*qh#LxsD zj~-FhGsDK(0>#-$OzepfFe<1q_lsXbKEuPK4E43){^AKuQ%4LJ`VyHk4j5=I$e^kB z2#^x(=SpoyI{{W6R1YmOH(1WAm$6((<+No*;22fPgoXz6->U)0p`bQFLXE5>}6Ju2|IQhrZJ3Pk7@E~z6FNmrbqbDPX zh}vb2hKhOq+!`lWU)GN=DG7C?{SOJuAO{);Hkln6U~F-P@$NF5o;fhHEkoe;iUfOC zG8#u%Ug*Z((VfYKeM-WeXc{^POyopG&@q3&drrrjun&06jarTRZPIAa8%-Vp{)P0`rju1Q#H~(?TBW4BJQUaDQg-*YSz20RWo-xP@g%O#oS8jQ0R|dU zy$OscBsRvI;$MEdz~Jj4W@GW0MsYrINC&WBGw`k-JsCi@*b1&m=OEtq?9)^JTyvA zQwg@utXVjgGFG3)r@#4x;No#i7->ne=XbyPlG6S)^x_qwd;*w0lAzJ3QCzJP^5Oy2 zBkLS*F0;6}#OCfHTZ>b44NkDUJVTG5#Iv0=lHr<@w)5{FKmKY72SeKOTMoykre}EMz~WpaR|V{N*AFK zxoB^1@PG7x?B-E6Hdc6(0Pa8$zn@A=XDc4BY#2LI1A3~1T?j0l;SR+nUhY0Dt`3;X z1#>6`{dqs0Ze`p8r*oCK1!uA~U5=M)IJ2v3v=&8T9~en$fVN=$H1V7dmq6`1|Fc!Z$<&Nn)6 z@`^wPOm^fD{JNg@{6M19>sVcxCoRN|#M&;3!W_wLUIO$C7lq*Do=I&<1g_z^tgLS` z)>c7oVHdiOj!_7Q5)&AX2=`B5b-fR}CqC@U-{vW3si|ast_x4kU`|DE1-mP2QWGZm zE&*00Knz?kP!CeQa!)$M;P(O(PQwb@wr~xXMst4%RP1 z*gn_XZJ5;YIuQ5pbOwgU86N5($o4TwwR6Z1`+4x2-|{3dABEAxMoSvsef}Ff0y9t; zbTp@VQr5jJe7U7kDyWXM$1S`R0|M-ICt~fGiPnVKy+f*N>S-$s#?Iq4nzwnhm-ducQGpUG=!7I6% z_0?GteO;*S*=MxWhlu1c07hF&DXeKG(%F{ksdE4pYqGG7sOFY;n68QRalR)o+e;$J zX`TRJ;&l|hNhM_Y*$`XQ!N$fqjb*u%cCYdi5+c2V*wz~9iSxoSwG9Ix+Z)5-sXc9- zl~~#Na3D8;QD!b{Lq>BunSL(xPwp_*+j}=gHZMZiJeD94i@7y&$IaRxj_wI;UMWy0 z%t@)@)?mVv9kC5$rn#+ z^7YMmaF@fPWg7@fg{UaLYZQDQXck(%pWU*B*eGM5)iQOLwxi$vVq+z3gd zH*%{{a&vvj<<&KEr3Nq}mq}3H8o_K=EHNMJjJ!vuk|Ppbo9&v@)fE!COxVR%uF?tT zq*4VYp`6ToP9f|pD}HPLS}e3FS1N?}hRPHgjCzgWn?{K3&&5DNgbR)ty#Q#h4@mX$ zVp|8u*6^|oWNG_?mK+~EL*pndNWs(Fht(@30P@pWJf6Q|Y)gzzr!yxlJq13vB~?K>_d17+v~*g>r^)kv zCoqHEY1|M2dTQd`sc2uPJv{*HfD}q{WBBZ=SF{c+kQe7hTJ0b^3*Ef5wPRrO5&)z2 zk|_IU)DG>LW4#q(-Wtf}xqFbgAAJd{n8m0$z~P5aaf&EHYgCgLXh-Yl2@{QpB$VD4 zn6CpJXzW{|Fe;e9*c{69;<10}$?A~`0Mo6QcxO9267nd@ishTnzBl_d#OqKe8b;3n z$O;d+Cos<@TKM7Na}u-j$jQkTR?CNG_Nk9_;DK8N`FVL{rzPTGZ%@Ve9!GP%xITVH zLRKLaWqG*2dPqsn47UaYdc9tFbX0dXkKK2_#WpB`vivk2+WF8nHb-K>E5g$9$xDmk zyRX0Ib@wP0F~PXFMN(RjVU8t)+07#gd_7r`>ev}9#KzW@iP>R1++0Y=E+!>95Wk{c zw$_Hs=Nw7RDjO_8A!Z| zQCgIRmxnu3o7*&Hdf*ZoPg!9mKm7hT1ihKR_lT^Avlk1?eYn_%aVUe^^FCZ%iqQeb z(-k-cX0tb6gY^%N6jziIZ1XLjKJcKku>>zKSF-Z+@p|$tKAAO$L`Nh$IS`qWLvCUO zK0YPfT<#Ma7{K`c4foMe8SOx6>lOmaEu0-(S&``BR!pp;J#Di~q=diizbqks>(FeD z={`-I6*&5*2rHQOE@8-k z^VK%oJVTHJvn{!V=G4-W7lMmh92IX@V!!!!oWiSE9INA{OCULUxwt(4oTpwT>@W4> z_S}K&(lR1lT}jIw`-5Y2APCGuf;`N}vUAcRyl{)mrZhVihmbUOwuW$a^5tA3B_+g> z;N(2A6TJEN&!5pZyC7^x=N6esX>J7n{@Eisr%#w`PQ=+KfZ~#Tg1j8ec~d0&`Vwlc zTa+1&oqaq!(&)lOq7oXzuyMH94xE~{+IUVBr-+xAU z!6*PL{bf9U?nFjjsk#3%uzBXho33@bm6<*pn&qjIQB zi{MAEcq$6Bd1}7*_-6OglV)?dQE{^OT)tegU{(UHo-Z#|e^Ul#=CvK-Kz2rHPq zV{7F5IQ`^{Z6F(G7v{bUpmbmrfR0#i!mB3eFHIo6YK*@xwB|8rqp%>4vf56L#hQD4 zwKvyK@9Y)^?qg6(=%{I7PkR51=6sdL>Ta~a^}#ATr_%qr0@ETerFZs)m-GMp{Lh7p zOpZ({L3DkM=thEEq4=2XpZopxv-EEh!<`S7^fwon7Jdm6hg&PGA6+AS(ka~+^5U5N(<=Z}=C)Qg&-e*FJBxFSj*ba7#zsciI6UECeT{3ak;}~$E)+ThpY}F5 zxm2Mr+ZY_^XJl-Oh2=HQ&d<#;Eh19hVl-&kS)OF?QUZX?)eNdP1J%X4fV-U!DB8_QgX)krRO*g3ww_u7TSx93C}WS0kw4Ve3PV2H(yL-cq3 zY4-VGje&sy#wTZ)n;mCj^??2DbuJ`20QR>wxsWP(k4AdQ+}IFf<3c|&GBU}|p_r?k zB}PX_nV2x=kBu_FvWNQCfbmvq?w>jOdb{cA8(?~N4W;&$4>4(EEYD4t>y9!oILPeU zA)4D;F7}og8t7+iYLSJRQ5JTOQAjSC85yFte~5*RV{Y|&4%b$Y>%pj#u|6|l_Fcin z!8(KHxCTb2IG3m~=ye=zEz;lDZ;o}8srhYmw`z*RJSZR7;60MdeU>(lG5q8mSI1i{ z>|AoI5VJ5fi&!I^MJqdJdS?0E>s3ss-uBJt5Ix;J^bL-(esCqM?tX|)En#VLlEMB# zAuh2>hwOZZk^X*$$EO*coa0!kr7^^voVGE>&HXWA&XcHhto7$$;~B>I^aKNa!|WX2 zU^E($U+ptF+|R(kAgg<4LYs%{%bbW+0K!Hx%jTFD_O8$>ub7({rhlNH#jQhhdIM_7 zIg=y(3=WMlHagDMxr{rEYF1Y^kjbxw)n<(WOq#dH$C$ue_k`v3L-e;Y7RN{EA0B3Q zVV2>6Nv6A7DScB%@4x{0Nx`_MR--W)k(}-_I$&;ZV4VH)w*p+r>f98Uat-$}ak{g@ z@ud<0bGen}H57W2Fu!+}7P(UBkzei$tNs0hV{9FZ_~3SdUL$9HZd~Y_(UEaxH;;uc z_|DCChx_}?F;8+VQVW{_tt@S!HJXr|?Jzz(z|hzX%kxuAF0627_Id2y7-_iNTIE8i z$EdksdTflna}nk~!?`^{~ceVd(P zVr-T5*?tPk>gnwt`FpT3%b?kZ(eXt@3hf^rqg%kFW@BX&wdw8rIAVBc zK)C)ljzq#(EibQQjvCR?IzxiZX~w1(xKZlOzU?tGFu?HW2$Qq(TuQXuY9uU9kI-i> zJH5PtMyE5^-DcG6#$k?e zFsSA1t#6_QG&dLKu{b~vXheIgAKze5UbD2eFXY`SFIe3;13-Rt$m;efGSLwu!~G16 zPclC@#qj(thkF}rpU42Xxj5$NLd>m3%EIIb{e!~%VsZC^pQ65Hdu^3#xx(z<7H2n7 zE|0g_H`fzv>~FGqaLuh&!Q$*V{r&w+EpDUI8Sh;KCmRcl%`PKVX*k)OXV4t`@Yp2h zVwJGjlKkR;@xgv`{b4o_uh1K`=Ga%bk!cYf@7#^i*5NgZlX(IIf?2sV@E)Dq+!w1m zsP){#pwqC}U5cGsIu~jq0Jri>CWpt&v0rnzu_oMiF=^S~T<1ovLwR}lfiIUBj7HA( zHaWRb0C2gp&eoZTiHcZ!3kLZ6!Nhv^Yh2yk=^7p-C*Fs|njy3ZAUmGr)wjPRsJIh7 z_t8rZ3AS~ke_Jek@h?;A0Js%x)7spRTBo5iC6LgvQPdjUe<^`!5tuRsqN6pcN{i^7 z-bba7vN6+3Mf(_+A{lapn2E+ZYFfIvG?!J$MGSN|(Kfch<>3ZZ`RPQ21QHRMKuiBD z*HYO}wr?&OZ7ZduX^g9zcVb5_=XhhD(t-kdX7-TF{&-AE`3*x|tqd*i{bKw#S7)p) zE^~SF@p3Y`l-aH}T1MB9Nu?HnY2j~jSl*MR=k{P$K^88rVD?4lRi)&BJRDbriq zrn_TVIH%KU5&HR~yAqo)yd9@^b3hQlKh+a_GrS!`!KUebxqJO#LO>{IG~Lw| zd@*t@QjR|3Vb@%(^?mVWu{<}Blg#sU3f5LU~ z;e7n?nlgQGEeOXyyG9IVzl8DA>jfWA=9>u!3?eZ-87BvSTE})Uy{n_s-R~pAySVRj zKaM+J?jQd&zlA>eWBsjvH;x~fA3{v`$3vLg!Zo4C^s_O#cfAYO#Jzd*zQ69-{pr{w z+-HpM$4+QV=r^67^_ezY-9kxAOu*m6k&eZ~yL*M;voLS?g)!n@yJp{ou@U^e=abM6 z@8Y`OpThl3h@E@$=JtcJ*WKB@caykx-@bou6XLj6-k`rX#=?26R%$Bsjb+Zh`gpl@)9mHkWf2w%8S z6ylGyqb=bDt=w`Sqx?*G?U;?bKc246xOhi1x-Ug{GD~7wB`b5?SpWWa{Of1dw2p25 zR}z>OfvM6cm}<%6lmGXBU>{t-jYh}%U&8yC>1mBM3|7Z(>?UR=FfDHU@g77NF6*#+t0ZY0%>qEIRk-@Gk*g;Xx%Ms#I9 zzC*N&lM(%#Mx@Qc_aL$jGpycPS|;iJhGt{(iAw z!Fcx{q5o`%&yNwS4Sa}NddbnL2orxF7;e>^AMG+bJIluI*{^)u-omX;&B@*tvoo{o z9A65*O|@X*uYvHztLBzpgZk!#<+UADe}n7xRuI!B{%+8o<7M-ZuUwCid5D+ zD~R*4#xxR&+8AgnB&?tfBcRi2-|a`BKc$ZKjdd!MoUsqcLuZxHA7fdA*8J|V7t5#{AJrAgs9JGo%%=t6PpI9KNfRHh}7o)kf7YzEPx z9;7x;BQ=*z3-u?fdYFTi3F7_Sv9`7*AhDEBH)& z9gQOkG?m8S5}C`{^);3GG1%GL;oud|%E}sHcJ}mc$%WNijn zfkapIF*o#v$8L%2UfeL+oXw-Je!$UPcm)6zEd!ihO9ej#fq8vER)`Z(krCK=M{^|8 zu+m$Cvy&^%FRY1bSYfw69~;*Qh9{?}&JE&)S0vj<`-FY>8|?k!=$)7(E!>@i!U{6{ z-0+XgVQqJnM7I}&rS`8eb_&KOhH{Qh*i1X__6aC~u0&r#tLFYIZSrFgmGK~*$jHXS=5u~;o4}b|L35%X zjuFKaWd)JdJjqOZ5so3b!po8avl|d8lpG&yF*h|qdvh&G5gt5x@RYp9F(eA*yA{lR zGJWl-tnDSx+Mf1>Bi=-~5|-OQl)D2F)eGG0&rx1fL{?@BPPUKu(Ic2Gvt57lgsdIi zLw747Kf;aA%mU}%K7By*+zFDsaqJ%b8!tVAh>3~D;lVdJ1SPV(F@l4W7aJlq^L+(4 z24!;~QVDU^Cb{4f-y*~nSUAA$+yJEod1Ry~;Q0Jo?0qtrXiLZ1E}R3Y0=Y!Q{?-Pw zeZ_q8+1GgcMG_Yo%JUz;#v`X2xm^C=N??BS$tQgE)mI-)Uwr;4zy0lR@6soqekSDq zN$JZk_|#nQ%dgCTfBy-ee(~j>VdFDlrS-?T`X9zV8Ma~3l@wF6bACl-kp~2ZW7!df8c>vBE19M1bbOy7g@*EZYQ=cVo;c1Fe8xft%6zD zJO8gCFf9U8j`VB^zvtiM;1xt%Y%I~C{(SxQk2K7mvD=@=(}z!ZW$#YU!YQK70UYeD zc=7xNWgRPsPnJpW4`XWk3Yk=l=<docbvV-yR4ad&P zjqderLY=ILtY2cbCYdLWzLZrpQ&k$rQ;$%#_O}SMv8QYGN_ZDhZkQ`JJ~2dk+2S7Y zhU3jK93Fl_Y(WJL^>vixXOLUm#Ky(|_D()*OLffm7GN8a%ZXHrR;#8l)(!9ICR&pH z2`=iQCdC&!H-AbSI;qO?$0;C_@#Yk4>>@Z*YK8X{El+jR)|kO3Upyfrzn12v2J+LB zC~KcZDr{zC5tv_m@fA<4yhu$;C;i>;6QjKC`09(#KV0!Mm;1u}SEzIE^Lugm<#&&9 zadqUIuO1N)9E0og@6Bc2mlMS1ovpii_iTO6HxKLx3k)J4Ab?o0D+<^UuHJ zgH>zcpIvF*W%q#Ky*v zoRQ1+xf~P1&l!ZYyU%1lki< zH^#~GAkQB>BBip4crR<5d{fNzdWdszAug+f)5Bq&JbX+^+bpyFrF{CEugR!vWw5If zs~;Ya)3riVtOtQP4J7!#;H7^SgZ-U&S$)fQ&qJ7-YUj!K4@fI%XP~`=m)~1a-_=8q z^>>72lruKaf}g`HO8OTqE12)nr=NX^Ph2O`gIP)nvnePnB01QJM~@!y?Adc3Km3*_ z&tG9}^^ynQePb4NU-J0bOY`Sf=FeaA`Ilyq`1z+ia!X`Kh(D`ce2T@RLX12_o}Lvxxov(G%f!xQ`!v#kVh$Sf8GvrnrEjSP!Ah z^OshHh;J_AN4r7>P`U ziTkMKQbgvMwYSE5ef>Av(8OdEuK8bWUAQm*2mB`X{(blF5DOMeXp{6e!JgWwN(*RTu z1Q40=hOMJ3!B2BSCOW0DAcOvy1C%%W&(c+sftOzrqmz>ijn2|jlgY!ce$OXgJ|MDW2&G0&$D0}k z`^U)kcA~gxkcpvwW~Ro3vF=@5WqWynqDWieaoPtqK6H#sGSD}|^k5Ax9+|97n(elB z(pnfnP)a%LtMkk(@31yeM|gS*eZ}$Aj&2~oIc9unf#s2UoLw{7T%07_)ra<>Npt(d zO!n4d`{)b4`0~#am`2Sxc`5P4C8m&;l1Nl+JPln-XifYa?FCh(rJN}B{HZLomJt;h zLwZ^Yk&zJ;RgWUm82F>8Pbe!W=18IEuf^JQCrxAPd@Q{4l~GC>M$w!2D`8@LwukuG zSP~LqiHdpqH=4|<9)_FBsO#L|KLbY#9TZfxqchyHI={$`*2FI%T5TsMw+$_S8lsgy zt=`#uJ7qOf{4y#gx;t3i74yfC9j}rV6Ha1M3UM)!WR}#iaV+N7Vv-&6Ccl=WYb}2^ z>N84n6NyPkCM6-Bu-G)_cF)jB&!{eW!=coG00t&n${3%&;AVG#sK_W%($a`A|II9| z<5F?o2d!+64D-2x)g#`cS6oq@7)`_E4nN1p>GmY?A>jo0`Qa0u#K7DM0BZ386*(#R zc=_P%6+m`*Ga|JC^dc$?N?5+S<&R<1%4p6>ASOPU)Z_#rW0Ghe+D6Z>!>E(8xU`Cn zKM$?si2A}Lf`UQ`^ba7fX&9Lf(5skeFDJ-9kU(!ALgI6o+r9!|W4xJ;@m2m7NY0ih zt!O~{3mu@mI%R(22%}L>Q)wY{ha&#A{2BLM3l=Q={R>QszrV!FVIOHlMq``z#UPaCLRb(a{mdr)TD45#o2Yg)$Pc(8kS; z*lb@c*cIJ~h4W@R!m&gwyh_9z--YlJ){AqqookU$Pb@b3^wtm2+p;$w_#iT$6LV>< zcY1ne{%dYSEP2=7rEqSdBOm9eJa(@nf=zRrNQC+#;n*Cfkbib|VQ%l*e0+iEUHCP-&g9|9mw+MHB$Zvo52~T}9**ROs%gUF*@h!RwayXY?GuG3^ z>fRN*!%ggq_NYh=m z=zn_Grmza6GUw?&SdGxCm8jGjx@(e&t{TF`&)M#JgHu!$w}PR!Hk4?zdH`-eTzxPZ z?%pwXt9jo(A4Pn=&*a1y6*(b%WfRWe=s0s52ef8-5S2QDTCKYGZKz(S73|&KcTjj2 zo6%?z&g+Dedci+oWlD%!crPYel@hh~_QRU*V%Kv2o7$i9B&Tak%q^i39TVc@M&GUq zKIj9j+1{NGCbJJ(Ez+Y!Mut`}+}XR|uJQJ597=8t_XAO#Ei*D`uJ`UecjoaE{P?hx zR-?M}ZKo?9PoEl0CgTV1^%LgfT`X$l+qN&M&PZio>f+tJ(4bVQF>;@?sWxow1L+^1 zVSKcY{CGbexF>NcGrVj2tv~PQB6sui&X#l;A1i+r4>j+~YZ3A>AoSI(;e+==n&T7R z@oMBA@-5sPZK)X?XL@=MCJV6f2xW3+mQl0c z0XEM`?O5YO)Yl6<{m1{otAuJa1_VwPoB7@U`G1nwGKK#3!*^I3xmBK%AM8j%Q7_v& zo3xk4^3pYmE3J%@P)}?<(wSe{U}bKUV5_G@XAfX9o#E@|LjQ@DKZag*LWI>z3R}jQ zF~`_ek<2qIFP6mbuBUf>YT_q(l4-FI}-eRnB8OZyE;|+1gm8HaC(-jxiid^-R{skzCQuuR*WX+?(G9 z;T^cQC7KfqNLH5U%4f`2XO738rGR@7!evam$`qP zoAd7G{ZH>sHlkJu_s)B(MvAxhH6wpJELgB$5tx65@DS3+@}&3A-#>T%^ZoQQAuFVJ z$FdKOe@*(CpTAI6XhV2>b#u0l>Xu=y6{?@t|J6yT^9wOr1g3vdKho6}yc}Qf>XkX! z1hcq0ipR4rX7UqPpuCuC=?Tr{WwiFqurl7k>e?`#k3Thw%dhzS^DhXgoI`fD$(xc=CRbOe$mw8x zbX*Xa^TRdFo}Lry`IzT^>FjKclax2aLTv`^6HAOX#PhGeeMs%<8LedltPYPcI5^7X z(GsNv70j=!P?_q%C!c-(X9~JEj{H~w2kip_o)hTBcbcy-OK0IB>KG~qicz? z%^@O!{PFbhrJ#NijpT%ivRvXKBFM{6!qvlv4aqG($H%j|1ZdNpFzPjR2F43ZKPOIUCSZ_4^ln<@4 zHPcCEQ3Lgb@qGGk|Bg#S9oLdm3KK%{_VU0tIGoY_Yxbr(NsNspBq)-`{9Ljs`!VQM zjJD+C=j(?@Km-Hx=g5wi$&C#sG}sTn*c?``)x76=xt;W~0c5AkWMn6k7$1bUn+tC` z=P+^0WKRXYKA!lS?G<&-v(z4scVIK3%`OsiThRgXt943q8qu1xbk)4Z%gdMW=m;Df zeHdRABRQNRHaq}Nb3bO5^`X=ndC%#16App7NZyXatq$e+GJ#f)XrDV~d$^WBA8&JC z`H)vRf?B8Jc)6EgKVN+PeaNV2MSQV{k9{DUXKGga%ZZ3C;Y=){vmnLnvk$(Jsmz^9 zSRZIG#}-4NcPKNfSKLFfg{QMKyV_e}Zrm(&@uNc&D@zLmIJz@)q7ojR)o5gtM>tUxfT4@(h%*2YgRuWBEOu)?!gy)|IC~DQzf9MDanO@|M%aK*3eIDMGp1T zdjMQaaijdTTjn^z2;YT2G@CCI}aUvD=O%Q}&%6bv_&5b7U{ucr?U zT|Keq>Q`vnpd>_ci5DcPG(t>f6- zJ43BiGuz+B;?5yOK`%+IokpY63j$0nIUwBjDRrYe!rlf+Z@T#77&uuUB_<$%Ab&ri z(@K%4Z>Y%2Bq}0?fIx4ve}!D=Ovn!ANDVfB4hkX2(^lAd{>H!^(&GgZLIUyj@+KrE zlkKB55?o*MFQ5NNVMC9(4QuQ@0|^Q6#$T`r%ClASVnXrv^ClvzmUEem#`Hvz(xT1& zyhfxl0H6^cQJx$`ba)s6-ku~E)S}R7Ss!oy;GR{3%4lSFp_kwQF9LjhNG|E*;&L4? zS5J17dX{_3h=_g7@$n{=SyA`~2jlG>CS2!w^#yNoqVV(&Ak>_nU08)&r>3|1H35Ea z8HQvORFH3&M#>zZeU?*oi|Y-46p4m)8Eg+$|hZ<i`Na<1 z9m5>#%@ZD4%=*+A$#I#i@9)yrG03jjUXa1Lxc>N%;P@mmQd0=`v*T&t zYmRrv@URQzNMZuRkvTt>L(w))R^O9U-pA454mly-q}O#*7VAe$$s}6sAwkYgOz!=B z{2C%0c;*pCW@aWCi4pv0=StVqEQ#*69cvu*al)%_0$ z%v%O3OQ`OdXJ@e&>&K1^?un`Nu_h?DnS=dhqFr2Q?jIu1#g4k6W%jq0h;aRps6Qky zyUTG;tl?;Lmb`*SwvTto@N&ejXo!jWG+a|!Fln!e@$#U1**q@wr8EsMn%nQe+Uf&`?x{UvGuz|_IFMY?!}jVhuCHEFGjn)PU^)fmaZL6KHQod~7&=J{~OW zp0U(djE!p)vl~0q=Y-)Jo=ZnvCSN{qr+s`8x$2gCD7SI4`H_&s6f!f@337A9JE@7Q z(;fUDekzEHpg?m9^5*+5zNdcRfc@!0zJ2_PjLb|jGZJ|9y)|z}kN6Pj*(7e>zT_u( zlT;bw+lcYiWdqR4CXc*!dr{o8$ogy-kwIR#d%F`JlSEDL68och zIN2p4HFAeRv4+h{C&qU-3G#4Z;1>j@L4HQW%Wv_CNf)m9xKKA<1SGR}xQvgTGv($! z-Cv&})YpZ|mU^rlUJ+Z-&*EqcbuH~|Y>eRU=*7yZh=rCktlg7X+uLWTCXpW=hcePv z%~xMqfAo6rh^yuDV2-@xDBN7V@DBWM=gcg9!ok`5InNzNf5jiNnnWqRstVGO$2T!w2^)8ykk#mWgn4 zCa-Inqumv9GqV|4oFTx~ncTuOJVRp`*|`4BidXqGSrvnPoCrKXwQSMzTnGu zZGZjEcQ|+i5ETttTpTLIBdwCv>1N#gU>)(EkS6CbgaZ%XW+0i`EM}Sul3#VF)>K#0sU0MH_ zz%=O?Yb_@{IFh)SP#!*b#lVh;sz47~wpC{Tq!fg@QdU@igP$MQIs-A}a=X z@1gAGgze)i0QP#yaf++q{BVWjNVDCzSX`fck7wZ+>XT_)UBj85XvZZmg)60!k%mG- z!=s6h4duHB-_y1w=MORWR$zMO-wDj#Y%ju-MuZJqhFeO=tE|EQ;UnyQBS=h$!^7zn zIqmbjXJfnqr;q~S@wB>JZ@#f{BR(OX5MO8fV~Xj_jwZTdOnB^6FFU~di5K}^(rb7+$rs!W2UWwkl+vk0s_gd9Au(B7w^bCv>Fi+j;>6fsA-I{p`>w5@b6Z( zj<2gT1(g|i##f@b)pI_UiKkB?Q_aPE`T0|#;}VDp^~J$AjQNE{^5Y^24iCdOD30;9 zE8cTvuIK7ehG@43XX{wRdLug7n%S3BmWN;C;+la*ZxkMpzBoQ&t|PIGY2nv1_tTv?qOnX6P3=uhe%E*aP$mkd9f2;zZg2pqVdme zqPH%C#F{a5R~vXZ`ZG4wi>H4C3wvkgvKw4nU9hpS%s^`yR(1h2yh+2sHh^O-fC+_6 zhWvbrS60p}pC1tD;lj|_Eq@HX{DiPqPe`lmWT3x~-hm-D&&0@gI&u6lm}_ALQO{6$ z1PLXL^t4wJ78poqkiWT3KR41X!u;b=8cp<;pXyXIEQ`gdT_xv zFpasbP0p`xINaN2exjc!KWh@J+lg|tp?>}n0F6S1N+qQ{+J%CSX~vq?hRg39meZh>wrO%gKSn z+)~1Q-6@|qz4y}u#lZsMZUOA7gt<#Yupfa1157p--`%r*c>I)xfi}Es{5TZmxPhC~ zLoQBMak2THhY!Ezp;szbIur7}8PcL7h>4HK-}y(vGJ2?s^C7%?QFs*mdS?(nzgW7e zV$FSGMO;DxQ9+(OcTHkueuRv;C<23m2?~p4;Yh-N87x?^2uzE>v?R-WFu(rlYrgvW z>-%};&tD72Uz&yBw`S4#t+~wm&)Ueq+x2@+X`3+j(p2 zJHC0}{<|`tef0zW38hR<%`h`HL0OV7-+lG@yE;NWv+ZwxvM1CR+7Rpse*6Uj^H#o& zlcO7J;#=W;P}-X%++AInT^zv8+KY`#HTz=~*jW2H&ovt7vs{>XLm4$!|w3 zza%ZfiI*=QQ#g8v>gs^S^&^b;9+7n=rg0sePS{OV_|2sDVvE36kEt46M|81EX-*2` zQ~d-cWw9#-)El@vxv(MCaapwzS+%mRz~5SQp)A!F?j*D zIHz}mQB6^VGgj74WH(G;Q0@_E^@945Z4Ot4v3vTEmQ69I+l%a+NPaFbJ97BeJ?~Cn zc4xa0o;(J?Xln_DwH;)5+mKp6!nNpvuBr@LXEu1x>To%pN#z)TzKVFP(p;az3+rIEE@g<17w~qprfy_{+OitvcF)-yt>D!I zAF7KJ@rlYqtGy!F+*kcOQhKvJ@k(h#tWdEvUdIb3e=4do3CwB209=e_;^tMt&PXFK z?0i`|y5w-RkG!JSba!-6-7(Jj#UZ(YF9?X~;XP-g8MwHYBHHc3$vO_nEy82j!2!wa zEY@K4%#Wp08S;}=lGAc%DUZT0w2kwrT=kI$0jX+?U&v>?zOyp!0w5V1Z~Pr>UZ z>Z_88su%*3mW}x(&ZOFV0@K~mi6xO*xE6G}TTCWU9WUZ%@6O_t0fXUf(oh}kL}kZ5 z+hh5-hh?JG>4Z93*(Hl}>!|b}u3%1J=Muo7Tt>dvGyd(1=X9+f(^sEj_GJ)E1{zWW zd2Ve(YDq6*(E)+hR@97bqc;0+y+4QjvsbhXO%dT`MMBj8GMSkAj3B}S-Eoe5jZ}Ar zuag6rE=M0S0^%H{!j(@z^TWqUuNuyv7>ty$b&{y^2l0x`bMv@e~!yVrS; z);>#CpaV(eqo{Q%2FoJ&(IX6z0R;3iQk-6p-ZF<$C8M_>0=w86_QspB_Rc_}6cOd^ zc+X~f$2{3V4#XC9b0xl{x2%BLrUAnIU1(XC@FAMhRlJ@)q-pUKtvUWQ7h56sZl)h&F4&XMrFJg ze(`;bmPB9^QOLDS#_*eHp8Li!+MUO9yBM}luGpIGCAqAf@t#^5x+b_h-66&IA*qc+ z9Pe$ge=fs<1q&8|X%U!~^!Fq%|DKXr$bQd@SJqg+e9m`fQTgScZ|l!k!Q3RoFNke9 z2pDci2=t|Sa)pX0AG~}52n+PU-YtO(=>Y+5FLCkkCpaV!H-~VR4y9l+G1HL6m*4wx zrZsXjT7;WV5(<8zDItiA?p^L-)Uwc1fS0!y!2v!*q!)3vzd-!!QugJ5_K?uPa1PZf zY7%^jsG8#ry^M-9Up)NG{)I>3YWs}T=6)(;A{jo{0dlHRBIuqvWUes}Z#Pdu!~A*v z;0rQ4W_ge5Y72i)cf5T4aCG;kXZ?)**#7|B)wS#V5+N{>aJ-H zS2_vsaU&==fPmm|RzDEbd(%zC=hmVE`7QegxNoD+Qv^1cvmkdv<~t92mxV*$n^aQ|BBh*biImyaOgue3&Hi{2 zl~#)AVx36uST@gY5p4|MVs3kMd7b=FU$d=XJe~Zh?_Fi0t$^&NNia%@@v^6K=9D{> zyMzRUu`Si}A?mYrqI^SHzcGTqEzOxhGz}iIKUG0!(i;r#O&pHk)!9P0jEeP1(U@3vD2vm!Fd0 z8^F|wjt?;z)b!Qo|54hNzB-Yze80U~6KRi8ySvVG4`35j?#!|wF^(@Al{S|;^^W| zSa>Ai@zbaTYPJCZZmH3i9Gc=-k6>*q^h|0;(IErfbF5gHbP zrxbi@=PGjO3sH`JV-m`FE9m_d9;~KK+$#|KmUY zBV%J@{6S25Bw`8r4|bQ7OT;L(dQ>uzu=Dc8l?c&|9HYsA(Dy-?9b@3^3^6?rwv-Gr-_BxLa^{m%%N#yW8OI?t{C#ySu&o&OPVe^XmQc zQk7I{W$(4RS9W%~zwZ7T#it(w$q-`V-~>kAYG7EtqyTl#;$7SYpbTOK8k-)0HEdCI zU$By<(B=BqrG+97#-ehkK6{~;%XB+UT(nM-@9w0P5 zXR;}N+UuL*_dEK_>C-uheJPnsucU}7ZjNML!7k<@Gb1LNgz{DthOy493>}yUusJCl z)8Zlg%q%h$i$E;dvi*bhT{IGbRN=k~)gaL#Fl58l<~Z2PgapaTpldQh0DG>TrL{`oiUGSEi@juhQpbo?;vpHCI{6DH)1?^xpGOpXKLc6XDPE4)UG==y)2!idProH< z`%760eKvL(OKs%0q<}gDXVoDU*Z!iiA~uE0-LxSW{>)Pu2~rV@JPZFw69pm9mmCE% zr^?!T8MR$ajAJOvtK2seSgB9peee7aNb+3#D|uC(s0|~bbNIId`xL@Ny4oqH)W_cO zKddh=LV8k8Bc^elaCjmWm%!hzo)YjDLiSEF zcKgtJHr^k@NY9|@I{8GJhr-EWK2qO1TRO$Q|N8JB2QX9>uBA=VjUTc-QlZUcLdEfY zQ5WbsxZ6v?OBHrYP;vgu8fcIhuaFxUIA#tp5gc$xw98JG3VaS@8mh8YpiT^BkmnZQ z7k47txw}b=oQ76p3CB*)Dk5hGs%@AL%Tgvdz*5;(<=06kXm;4~hciwFUAMHOz(?1Y z?Q;&R;C|Erdi?*xy-;-3R#;id;Nj`HNg;i5cJ?L>x?K@J7rxDJC~kAwd#Xu+C18EU z*BSXt5QFse#r_h4#~X^?E(6Ig?KK*(qC}k zWMl=IW-%DW3=g0mlkIYq#ur}cZu`1rv{o9L=>IS|-T(XF;3_dLE zHHh6r|7G5>FZ4R!uOsp~bGUOJcIN^h0oo-V48Y56m1pi*w(rMeS$^kQeqrsm*dNrd zqn(@g?^Q&s`}3IlL8<)|11N-<14q5gMS@}3~?MaQ`*y(ci5W>^AV!@Q{S3}axd+!FUR*3rZ@V^F4s$xcctce+Gb42=@Z$v z24-Bul&4jF790gybnQ+Zm4x9yjOs3aR|qZSJYP^ST)#moD7K5_fJ0e|ySF(MqxV&p z{$DZqe+fVYG5hcA^zz>tga6=~pZrioV)@%!Tfb*#mE`2)zK_Ix6c}Z3_P+h1P1OG< zs`Nj(?EX-ra0koFIEFP(lafhBlD%&=<@%bSB=`Ru0Ww&{jp#>iLd*Z}lz$ttplsj& zO!Cic|LHRD-)HxLc-lcL$HU0{?|Ps&|5QfPkAR;4WYhoGLPj83Df9mw_^<2l|IgA= zv7;FNV6pFqs5QR7#uQV}Ozr;~)kfEopshT^uhjj2B`|it!4lWHt7li#Ssh;5!Q@0; z_Wfp0-f8G;@7GsnS#U7PNs6+gr}21j;-A=}5yeos8TK94RGfEAX42ln`PT0VwXUvl z_~ryNFI_qbE1A0!oumO~tP2dDXM~cwG>n{*5``#TNK%DJsv}IVzr@J&73>R<$f%nV zI~DE+^~x#Q%gG7~o^L}F{uzGWOzP8VzM)r6^f`dtm(B&+vJ`I}5}ZArzk*E#YpTX! zfB>4Am)G2)npu%r zR08Lc_8?upi?R-Ylc*go`bWsVCA_ImX-#C7yF3=seFvYgXZa-#Bw1#)1?mtw70~Je`|5SuC|#0Va()1>|EsmK0aytnT~fnLEIcD&}c{V zocP%hF&i0mHO-*f?+EF-cbeN2-M{t!`%{Owtx716U|fW8@&Wp4LcN+EYU^gP&L)Pe z)A5=-Pz=j8mC$+7m-n(c$&)0FET<6^wzXydX_rx~4NuY3&dH@tPpIb`Op1=lu%6ji z`spi9Mspp;b|=rYpdv3NOv-+awia5IdTN_*YN3N1q9#sP&w*Ii#V@WU&Pjh?*iTU~ z%h77|aP6N|t5}4ENfY_FUq+M_qp~4osY75HqDGw{CAUyiVsUhEcTl{vH$x|t$6wvp zv>jy0gqO_1hKsXH&;54$`OtRh*gpU^8ZQQNyz?QXN9)eQT*q(T|#~Bjg~1j0hMl zze_~HOc4E{%fg&$dt^*N=e%Niy2MWr_(B|RAkqRF4zj%YFOFDl6)SqAxP*)!KYyYaTx;Od-GNCw z1iM{lVrV=&qaPM9)zFEw^N2dgu{&#kKLt|UP%>ub40|;`G!9DwbMItne%W7t$3Ig_ zPVZ}70)x=xp{pAiqoL&G&e)k?wy5iZu96b|p90_;Bpatwv<1A73OSZ1T&v zqwCv+9kU0oTAjnU8y&(=UyrQDa*Xw*+*-*f29wpm-GTDXaVbeZB;|ORpXm`e53X-s zi|c{(3R+phn|oZxHQ;-Rn5ZcDKOavh-j{12$=i#F>4Fp9!0*7s2$WQ&z042gm8>!B zaa7C@o93_vl{RLi$~)M@xC;+l`lwG+0p7I%PkVe)4zm)Im4|N*>8Q8vv4noBw+)w)YvCAKAxV1|Ge74iAq2qP#j4Q6#O4m5Mj~tc}y8@ z7JXZ{dfLdiB+8Vjj=-q&(ZIh%xVgDTTJnUBL&g|>+~n(?V9h9KR>*qOA3gfZgF$lv znd}1cFSs`^Y=-xG0G8!hE3*(}#RWr<(`;;_p?c_QJp3>c zVRsKkdlJAKlWfRGFpoC48qaMpX8>C6w~IXv#CUAN{<#zM>9Av96fhnGLsC^A*)|v{ z2;;ruT?G0T;C6v32T+1~V5qFG*aXv`BSJe#8r#WASq@&@V*^etDu_ysqC2~PBt#PA zCuGOs;rw*}1gQ~P3fm-xJe7rd1ft*Xg@Z$Dhk=nGXC#X)`rxl4 zQiFVMQ(Byt3a!Ihu8Jr%#}l?6)eH5jj8hL!FE$%RBrf|5O?==&J>?0#i!d?T#U1dW zx{xb0b29t~W=Zz@n}KztGosG3t#oXRN@+GSDiNh%o)9^j`V8W9HX+~`bDO9hSv;ujrO8IlGe9L z1x*7Jnv82NU-h$)kBVhDJ9XaN5LF2q2ZwBAJ(|A7KfMroEq|6K)S-upFJxs0AOEaZ zomUA)4|T)&^%=}23TKTv5g{=+W~oh1TAN9>uFPnlfQgAiQdJQwJ_Ifp&vCefK*(Mo zL{E{@lLhTRwjijRLPWewu7PU_SeaU7c{uuT;iW43xbFZew2Y%#JMg@T3}S8$r7N zVseA=!14YPP;VTvgZY{^A4w5DqlmqpNUYC8vBnzKqJ!8P@zd*zKTLF_(Y=hEQWBOi z8I^uJIqzKdOdf(&MxXl9QcfjX$i=ZWc^+K?4;jP@n%6k9%7Y9mtHRK!KirLCe(A*~ zR3Bo`Mbz9q(v%=qzrqmS8NRnV_X5anX`GR+t^yeeC}*_5G0uKuwP{i>66f5&)jEN- z%+Vp5s#RewK1d>#5f~s!0WA~m`XWd z?!tGVA&tuDn~PNP`ue3rXl=55+8NIGPK^5a4s@+(BZrgH?BV3d1Q$Y{{0TWQgWWPS zM;BVqr{~!(+jKk^qC=Eun!25IAg9k?*S;?r9&Ss{SvnGB5XGco4u%i+D~r+Gc# zBwkw2TDaDbipzwdJ{PNGw&POu@6cjXT2JL#DzalxvU6+(b~Sv-y!G!cu)j!n4}z|H z*Z+ntR;l`G@3t>x34oe-$o-BVitN&r_%}#gs}G;(_SKeXYwK+6Hbn1g_0l?(x7o0r zJCmsWCgrH2cY)WhHqQV+G{6p8IE(6s2`uvH8bSjS7vn?#{r$ zx0#KM4yJY0{Hr1aYkG9st8WZktmAvBkrXYzme5EV>Kg>*6%hB5l3gQ77+r`;*(yS= zw<*f}(b*xZjrIHeVS(dHZfOYIp?@Rt4i~pW_xq8n9BpG)&trDc zg)A&AEJMu*}yY>f|gZZjZjn_h?ex6sIQ0 zmy_p*6tC(=<$A@MvUexOT$?D5sXy&(9)(Z<@zN} zB~F9(h%zYEwyauz#xjr!T!uJDbwt6!8B{zNBP}S_{0D(bD#WFQs|%U1 zj5Ae+df!k^E-Gk~;uH~@9Ft=om})q|qRx|teDN@& z^IqEIp|4*L9L?bvQL*sbZ>>m#Ak6^9!k^W)Gh}U@n1)j&Mj4q|4)*S0qPb%9$qoh| zUe4aeQMXb{1u_*N9)0-ebw_V^nl}*#dern=bnUd=W;R~|-Zr89j-NCbBypt*v_@w4 zAgmvySlsq}1S+;NR&we{)*nf+)W6Lx(!)k{QTTKG+x+%Pa(_chhM}vUx|%KK1S5A_ zvseXdO2i4WUY6rqxyo+>Lu;pjz(d`Owk9Y_BTLdUsrTI&r??j$3%T`QxJy2Jp2gz z=#X#h5;7la0is@okhc}gu+G-sjh&GBv|Qt>MTR_>4sXeV=fBk|c;TAn^Zh%20mCra zHN@YZW*HPbzVTh){Q-sGR=fm*Wn0i+sx)-)yWSe=h8>M6oIS0IVXO4+n6=T&Yz&>% zoPDXnOvdpj(qLU8`tkg5=Men5ilf2n-V}evKHC>R9C;<^{!FxF`d`M|{@ug|;@7rQ8Cf7Ji@-$h z3WpkJuP0fHfS`%HLDX@nxhls4N*IN9##v2`YoI9Q?#W%~cqZA8@(t~*>XwEO2`3P{ zFtq8S2_EO-nUxjX5KC!jW`(T5>0A1#qiXU&YNnPxz&kSXT!dlr+xR^aq*h!yU7&{= z=|dAIcSk$(p-x>#=h)wu&gxZbd3#7k?Om?8;aFIEi6B?akI!Z9(-P{%4&Q9I-Mfv$ zidoY~pEdxVlWWLKPH3L~Y-mCPJ>@F2axXD@1T+ROGPx*18c9KF>gi*~HWhq;vrDIC zmFwoBX0~(mq~p*dVfOQ`<|9Hho~5~&Pf=R2nsY_+L-F|B9NUeIMPY;T;|dX{BH4L^ z^vUcz<#~Q5tcHAYy;wzkrA+IC@}FzynNJ+oi^Zvh<-)Yn&l}hxA>vGiKXUs;A6k_{ zR}FbaMs6w0EvOh$P=I^@^>YykG7KbypAgJ}X_5@iy3V?jt1DT&takcafFR3qzjNv3 z=;@F|13*^uE(_~q>8;^W`aJgb{P7Yn-c)A5F`mn^wE zPxs5(Qco;@n*W)c7#XGq-JjcAnT0!banFc!cG&RJvWn{PiL9#F$l>kOTMuuxZ#USY za-&_`vPs7IyrDyeYUa;FA^&I1N^vKZFcTC=M9?gd{Nlh6?(T;l9hc7rF0K0eYt;p^tB`-{m+AiiRtPbd2A?}sEm9Y zUQ|@{{w);ke}xmu&}ZS`C|ViD3JaAmHKmfz7Qp!LRRgcD?TuKLu|C2JLPMVrL0{Rw z1puj36#7Ia_-}Kq~&1hM)mb(oK&4RS^L=-11tGoj&fpc{11V z?FtH{E$RPhI5oo?KKY?3@U691oJ7S7-Q!*>g;oHevsW-krRM%kNo*+b-+Si8I{6ja z?D8n5q9ed26`@cO_CH$$Er$M`PknM|dr@KYN=S;oZc*N7fi9JQ*=O)A*1j%Icj68} z#rpoD*J)yY>0Cm!e%R4X>w?h$*W-orFJ*XJTbJ&Si;vQ@t~f6{C#yC-V|-t-alRwL z?5#AMzUhe*V-w&S+L;Ab#mDCQyg&=Q;`RlI0D*LF1W*Br9lKH=SSTQ2et54EUw+m7 z0eJWRaMa=dMAw-V6yubMe;ILMu|4`k`gI@g?wsLdUHiVf+v!zQ)$-BM@Tp<(Nd@Yn z9@P;b*Go?c+8jtFQJ90oKV@SJ3)`7QA2B6eZzw-(KSP(gUTDAtKVd(gOnnD0eLg)= z(NLc#^YE>WU9}Om4RRsBenmkCF##NDD$45tWJXK@qJzipvE}OtLoxo6d$peA1LgZd z3+{U|c2tkZJ>b&`>sAIl2_iacC9S<3!Bo^K{OqaFpca>VF=QV9D8NBQJh% z&rYwd2)Pfi^FWI|pWqaIw?*n` zO+;!ofE!-PaZQ;eSO(B?BMu3-d~P-G$Qoa%p8xaGw^&ZX#|O;$twMgrBXdZ->E?m^ zXQ}p?NvFyuYmZF3BV}YGAcV{7iRoB8D#o2J`=Ih0L&n%vw_gC-Vl)8>eOXLaE^`*U zHraW%kD}5n7x_A4+v6FlH^T&>Y++sz!SCrx9+!b&l!i})4nT>Udg9P^tq0{V1Vxn? zNh_=Gt4N2XS?qp7S0;b55wFTr5=$z4y9GrBwssqwl@tY2yCM@-hRrogFXto{Df4kZ z6}32*r5=k-R=9Qb9AE6?8#;AwK?R8xpWJ`G-4|GVu)h339)k`1=T>DEdy>iRNcj&f z*Y(@8e9LoNQ}gYF)?ND}Ca2FQW=oe(L{rzRj?Yy9k=@mlL8m!&Fz!_oo9K9V_%nQR zwb8T}(79on>0xheyg1uFj!*!x_J`{Q<;$K7%Z937MX3N?J`&3WWRz@fr#TOG?JvGfIa3_|y{*t`^Uo<88kXP-=2S<23Vdy)xE$clT7#A5(9yd+HLt z4*9qu(JU|69(*_D-+oC9BdLWl#^rTc71MrJD|*lD2@C-5C5U>bJ0?y}ae2M$@XS&Ho}?`F_B$);7EfjjGMM!s^x~4Ig2@b*$GoTHn z4x}IBVjtXXV`sbtELGd1NnSJW>3R1P?fOiEBh1Ng`8<<=(HGLVdA`_RXnx6*$7|M3 z3yF>(^TUHn)z65V1?ZI_!e$jUq(y_FK zVNkm1y4f`}H&;N;Mki{8iswww6Yuwm^s|+erufbQx((l%i9d5CMi#MvKBO)d;J4=cf*<^ya}PH zVI&V1qcqJccj5XT`W9{eCkx+rR#wezZSx!QLpClbK_xvjbc|%qhhn7*>uVMlJJ*?T z9HIXHPh6`w8WcG)1*OM9XUZoMU+t&22Ma1H1-Vp5boC*;983#G~;~{xHO3Aj|g-5+tmdL*!Il z!@&bhYiSEpUbKwy`CSN?xtJJswFjA{Z1{3^N4%qojFsH3@oDf-B{zJ-UqbE+E|-PZ zUW}w{*jan9U8UqDdh4TPd1sqH%d<;?NcS@>lkIY4`e+GkK+i*LWI>jArK8#}0om%-aiC zGM>ioEO$OqBi1q}Olq~*HHPN-$6EpAcO*V2ENs#1ypDvXsVjvNgS_uo(8LcDhjmz0Q<#?)}GWXG$eVaHxb8)#R8 z>E~1=krG?gZV{=B_@(C9Cvb9NGF9e?=kJ{BwbAY)9gCSO?Hy^8k* z;3(XP4v}+YOX`)&>j`g(gF{f*@B&e);ePjznLi72z^JBtBl+enpsCasVrfBj`@?N~ zPJ&eZ^+5H6m;Ew$14Kl3qLH}X`!Z`?E-=-<`E%~dr`bvGM3B3we#2AogYKAQ6p5Xg ztXB0ziIw|s&LvIaT~Kd4-hGy<^nJDAF%^Xo)+FKICa~Y?-M8@2B;{`;O_`k>}=N+l-`%kir$G98pR+`vp~~H={930M!WVvTt4zMD3_wU{CyAqu z_Vt~^khUHH*(Cd1!@W{!Jg~5V{}%Ik>%c6c?NX|;3E`IZ@5CsV#_^$D_k z|JNs1Htar3@ngk+w+1@SGEVv3wUOi=jz>CUP4`H`zz>vyvz6cR9rU-}B1?6)j3g16 zumb8mb$C#2+*R8nV6zU~j$dwt8)sOEP5%Z+xUA8>kwr0)sLxHj;n-`*dnB`kx~Pj!@m_g>!1Z57Dr@2ocLI1x+y;R2 z(UEZ6m+Z&s>FL2vWIbVy45*x{V-|Spj8M5yz&)z3>QAffmw;~|w^@cU1>;R#o1G|7 z<<@8E-JDEv32(L3owFcKo`d7pjZvUqti71{K0@>`O{E1u?8)1Qas6}u#PeuOXSTIt?^1|tE5?01?e;9O_3qUv{XjNf ztAZ$e`E(oiWg`lbz8_i#IrjhZAt6sWY_~5z&t9FnUNHePlKT&OZwaY_udvx~M_>1( zdb!C*_J1d|b@@honK?cXd>?U5Ev@X?nwsL2W~vyUTA2PRFE5WlFYBD~F6=(DI+_G! zQj+dj!+1*m+7y^eCn>u(p=5=S9+GK7Jkv~?A1QCR>@k?O+}!*$=J zEi6Oh!Ps2&uG=+PT4gq+`7DQAHt*y{Bua{zIZC=tpM)08H_XBE@a4(j6#OC>6T>Ah z&}EhaA~We?+Z4WpD$s8J(KpVTGMOTK zhb+aibQ>i`bmJJ|ji7!i1MTO2qGPkUrNLPzuosV=uhkoZ^$Wop>g*8?{+{LrA%YWP zm^AlKqa3=l7mflf?R>2TxrHn+Cs{9ZD|6!RRXA@)*9SuGzm?;7l9N??9v9Ge4NmGP zZ4nveZ0%=2CRc5-_1`zq@Ft`5Nn`ATf|TiFjUg7oflQEia(z5YWq% zYjZ1a{Jz7`NYrNl4sSzsI0tdzSZ;MaFy%8zT8=|mv6-^^uKPStJ?G@^OI)vemTPf; z6RhEW@shPP%tl& zlH}tr*R6iH=jdJWW{>uG8D8J&`Oa_qw`!AQa2||)^(X%LsDWcM=gA3lf8mCH98Gpt z3uwkIx0eJd213n%q$G?1c4sS?`)(r}FixAq9h&>=Z|jj(b}`}Q^hcIv@d-(?zdOFY z$c=I{RKz#;2H~VF#5u9c^V9CTj7UahbNyJjo}CjsHbxzQNxrQc>97mCiHqoenivAO z8#2BjiJkZ zQRD}wX)5*L`|V|fzG)=MKt{Ng{tS#!s`$RKI6O3y27c)2=hGffo~x}CV7j6-|GsZ7 z`*zr$@up??WwcfkhWi;9M05WpZbcAA!4Jw8gPY{Y*W7)Y* z<3#%0l2{0a%wZfkZQX5{^46e8>46pBe8Q7qeJz}G8IEywm3DMt<)SP;Hu-i?XHu7c z1P=CG6KBNDa9}e^?K)v=xM0O=)A(plZ_2sCI%j{42yz&cQc__21DX^xlS1mq5pqil zhcTp}i86=O)v_y=_rZW{nA=7M5(;PFy=rYNra##D9iW&b8p%_52I1Y7IET+~X04>P zLz+TZT^2aLhp)(fbV}ScG^BKWm$+|OD`7VX6sK?~wtQpnS#xjqF9kMjWTB4%$4Ri$ zmzCa>zvUBSx@&b(fJe&~HiSQhGv zTQn=<4~cw0P`zXn9f8~h@p>XgLoh9=h+Bm+7I}ae-FPPAFARzZ9lklQ_bQ1;=6FMa z8IeQh6o7ljY5(V}YbE+Gahvg=+2iLK~$kGVGY762=bS5!h zzsl8_l+wMtKx5;kRv*5paEQI=8wir5%u^XE%w~=*pH3>@@bd8tDyP|E#i0{Q_UuEq z@@h5vo|E?6(lc}19~kNK{9>k27CbHJC`^?$ow1))>NC8y!L`lk`{_yv4{!iih>BM5 zv4US=orhgknNUWbTwQ7$BXst5{9f3h&LE##R_I5JBw~eiVJHJT+@^6^8l`dto;9Z| z3wJOgA27vnlPme{PJ3uWAzmm^zbIE=HL(43Jm-vf*p`6-et{WMn;(DJ>KzI_8EezP?K-+|~N{@rpAFK;~TcqCjwFD#IRl z9m1Rt@snF_s+xZoHJD1g!H6xQdbkNQ8yQesJJw@!}+p zQpx2za&C2zM!f}Gk2N?M&1RuDQc2(2t_ew*V2)~AU+dP!RZk)FWmwr=>x>Mf38XQ6 z{9vj}k#b2O%4LMC>x_GTc}QTpCre(y^--PBUNqKrX9*V~cB|9>Fm&hs#PLpd^|!fQ z?H;*m%i+!D?b7tdD;`q?^H_c5Quzp!ud|iDQSb2;or}@r?cdshsmK&TK#Hv4?C^_$ zz?m9?kVGYuemTO!FKn<3B{}|Ri5P!-dDB?58$KV%M%_|%7;m-dd14m!x)Gl;hxB|hLTC9?WLeHd;~`k(+kB~_`fuN z`FsED({cau$qulOP~faKIt(NI>wvUtP!QJgda4)Oz{yE!w{DOiqtMDKM3F)TS5><_ zl$2_s3xdN(O8LX$tjth6Z@`^YxAe^e4|f{Y%*+DRI!Vhc_r|j75m=%L_K&9Xjf^sL z+R5SJC2L)h=fPvp_>sK7>Lu!L&@K%FHB1yM2G*$g{5oPWee6SM=xGdW6{*4TqHeZq zm>uU$;$j>VVYhjs3&S`jd@O0&y;AFCx6LfEV=}yBL-*yl=tFafC?OpmE#_fOtZ%&+ zo;;CbV*bwK8AEcX=Ge3Z#vf}j&ohx)IPw%?P*_EMqhxwP_Lc&pTx8{B>1H$o;T^^? zSy>aYXWABjs&*M+i}ai@hsb-_*j;nVo$-TLH8|A}i;ob}E)ur$?qTJ6c&X{=^sn(9 zoe17rUdy8$X;93LmUeb`{VIc@cyBMRXq#@)KyH=XL<1$Ig2R5li)s+L;_FjveI*gA ziN)3#p^(c3j)VMx!{auqZVt8?$)Ev8W9(4O7aU1HGczrR>UIo_8`HoiZo{)Pk##U8 z27q3DdVLXYT?0P1a|_PzvJk_m%)-8xfW;OsYJ8oF!wvnH_W`7p4)-#uWiS)LAGB z3-^l7`{lf$FSduwTI~tdI^?VGb^Bt+0B38+5&2n^D=u+M><`y0yV?@+!j22y=S(bW zNa|1vy?*a0FxDfMbH(V1ZI^nhmrR-*KS1!{+LrhP;M6|y6+eVXzj7GK%aJ^nh8 zq;U0=%lx!XLTOn{`>`xHWhkD}o{s}oiLX8`)A6jl?juy2&Hi%HlR@+SaBQ0YyAGj{ z+Bpm zQUd?0_HpC1!>9F-)zJr^wZRh>5y2SrNJw^SMrnM`mn2HCr8}*weTL<@Z9}F*5Y1gn zll0C#pk4Ys=|NLsdH`Yp${nTFnlaKkpDj~QUPuB{X#|+U*OSmVEjs0;h58L^mgY2X zZ`k9kB!QY3rknh&5v%3g>37h7S9y>mbhtV?t7$_ zWxJwyLMXnD&9Rlt$0J1{c_m)h0sfpbz<4KRR#e~ZLUH-BXL2hccOStdw_^o-i{g)8 zABZ1xdvFx4TEj^N&KDbELL(pJAcn$E+Ai4kcX7+CIL?d1bz(_|d|SAlw<6dgatfu} zintn3ZQ07|s><=%Sw=ep2Ht;F(#KhTU^vy{4Aac4spka+Pxczc z(kc`g8T^>6tOjmP=7BEv`*>pcbwf}09I7LlrHl-M{oU%oZ-2=_UWQ!@Q&~4fXa~xY zCA8qIPJ&`uB8PW37FB4p3tq5fD5(swUdTJpQt8gh9iZlSFIm3Eq zL?&jA!KHrDhRVWmmI-u|-JG(~kH64hH=Iv~7o9-Q^WFM&?x6S}@YV&Jf<(?=9#(W5 zlqMYT7iLk#sJn4+PmHFdIvr3S&b6{Uhd94nX00pL^c01s$*-gkbf4?AgA>Msq z8Y>MzNzr7;FKtTp_l8wZ(b_sY2}~_ypMHfq^4yTVxS%$d6&a&1WY0{F(OBqKhE9V| z1=xnw)La{VKg;CS(UsidvsASir{d*?z|&G=Xo~EU_!-R zFf!|&^lw=kRV9COFOM&$%$Q7p`!lp>O9! z+L^PH279}=bHV4iDf*>ANN#Cho6S=^(0Xvp{7oeqa6ge>Ki}0|GyHSXa-J)9@_q;_cfZBLFCGp zSV7vDy!3e^DEb88RZ>HGl%?9=RvEW9tv_(`TfR}f3v7n0Vih!faMjtig$cg>B05C! z-Q5_{?dBc}3>AMr+f*nKTy)&C1wS;B-yQU6J}oV!i{E{`cmsnNr)ArCxj>7G^`bj&c=u0G1>y9CS)KfeH$ z%sxGDacKkuh8>4^Q)(;2)72Io%KJq4FpDs@?a7hqf*>YZiih|vpDm<$CIy(*pQJvR{( z_R{Se{1W~*J|Dr!v!#LjXuiLl=e%6ari`0Uy;!12N+!IsEG))JS1%;~qc#J_*J#pk z!NjU%Q1na)*VqE%KYaC&I^M;eaV51?!*?8)k4}#Vj2edzCok?$<$qO?oVoDY$p3~U z$=0f}q{0qIogKnq!8o;T}IW(RU(wKy(SqccjLA!cIO5u_ zZR-KWUBPYlYON6*_+)Z{0eBJ3G^ZrNUZuAOS6B} z#a2pZ-reEZc|e4U*mp#-?&B^^wS&i|;)#m}*@d;(H^YdFdtIkwn2P8j@qZB`nRCi6 zr+amI?&}BqI>SU;=l5R1m$Q1|%Iao{wbS2QjyV=vWB4jpjP!##hotJ4G7x2eE~{h1 z1{xv2ZU3I&i@kh=>FPNNCZeKRZA79w>(e!o&tzqs9OTC+#@chTUTnt!n5upDDYK{e zg$7FTLG{$^Y-A6cs6JOO9SQ!O{@G=*eDrih3v=q&5}(hSoa>FimATwoF15deg5Uw- z-!6E$%sJl<4x;|Z2+jtaC@zb6HlN znW%*l?%>G~opz%HD6G1}h+ca75}@ z0=uct-EMMM(V1<_tXJ)de8GNx2@CfU`8qy=nUd!%Apuhek=x*M^oyrdpr~kUREI^; zQkY$<*W;V;TjPTZ52K=?A>(WcshCXIMf5DxoMy8XZMIO(up;fH9Vm{(GWJaGkw-e? z4jRGCxhRBje=9-b6NtvhO0&t2oXClqj+R-tJvwlu^s2qU>^kUSo*EXhz9`<{` z7IqxDMLeT&jvmhzmsD036&J&_tuwr0^w!qYC{3t9bgu5Ge{ny<*5JVOZZloG6Nopi z^6->!>4@0t6K6kRPrv*fKJTGr6@Nz?);F* zNj`Cl`ABG9uk%9rh z58G8g;sv0%?o^2A=?{VCYHUcsWu4J#b3w+k$N1%&leJUsVY8tWk2c#sl={RMa>Gu7 z+P5bdjeew%t5}~_*u6I<^G@|*%ZTU$B zHezD~P+D3VAc?jrg>F=un=2F|tF?G~tLS!uYlpF!myjL*r_S8k((-ta$)Mkg8B)Lg z1uir^gFvDO!kv*3q?9Q8w+!^LAz0voiq(+SOqw&u3bKOCpfUBJoaeK)wnA@NSoa(8 zEK9p2MNUo6ghP02a9|J+QeiCYu{&vnm4RBv7PdRWBIrRRFhtSmB)cEpJ>M6K#81*! zYFav)UrL8Fvt%sgV7$HGx$YYon!arp%?F)?>^MX^*a^gF$@U>!-u<8CQg z(x9NAjP$>bFT!3chL_(hP-bm~7H16}J8#2(F8GF7)ZB@xrQ0?<7x-QFvlu~@+0ga_ zb1K5?`gh~ZaW1j`CJ&>-G^$TT$>T9$biRD4CGIqb&$~Oy0u_(<4+tIdQ5G6P6G3o3$!NpcKDk25uXvi& zZnbffcJ&g0`?;P?tJ@bDY*XOknJIx_d-l#+i)=b`qE;$x$t8>NQF$Jfv4Bdktm)vH zE@XO^lr_c_w+Fbabafn2<4N zG(EM6k`l!&{AJ(P!#0+dba5(n*2-{wTha0T<#&a*KS|R<`x}VkW8;*xypf=b)Ili5 zd812SiRm@5_Ih)e<g-j%a*!0?DQRJ*; zU@*9KNuccWfpx^_9D-G2V)eikJ`380y{hXcJ6TLy!SlPPOO)jO$3hdn{E}Sb+IZ}<@JDsyqp-! z?7KqHnJXdRfTIWpcd1$#Cv3oj)B!qxN5~4lbu-A5x$uxdvG&ri=*=6+^bk+eag~^A zqdh)o0zlvisuCRDA3aQU8gm9zTI@Jk)6>@3nfG0w6L{4BY&*nKT(r%(S%yjNbfEQ4 zwx-T#iy1A&ODdK#cAGcVT~9QTmDyDdCwbPcLWvgEZE9+w)F;Qf4zp)=yI7O*_4QSD z4tt)hq8HCsf}Eig+GXi~78$bE8OQqbNlMH?h2)1wpU$U9aG+-BB_7>S;l;$cdlL*i-C%a z)L3A4mSf7+Wjm>=_@%~$h?KQ12I-Iqb2Ut>_JB!(NNYH=Uy!%$c@f@G~em+hih+vbweXsbi<9_YX2Ie&h98(g2;7cgVJTLR1`o0liPc zMx{2K8Dmam-*B$&-*1<{pi~Lny21twai4Be7?_H&ZE|Z`LR(i@`JRuARp;kw>G9IbM!r#IETK3wbE|S&n zF-_O;d93x7;R!@OGuae@*Q)tLX4qSw?G^Pb^-D`st_=z}<~Uw@kzm_!MB38CM>1&#B6DaoGUt=QuFzL3a zK<(-v0NiF|kH*1SC|kn`m=CtePrgaHBbRZcYuHS1H71a`qkkB(OAaw#OHrAmCO6xj zJF zn}gTY)>vs}o$p4Oy7>ssdt zhEgLWXckQ0nI8&4$vt0^H^boj_QmXB&Woo1=1->((P;W;q)J`MsoVDK8a3@q*cYFE zxHcnSzF(=Dmg-(*mP|Ma-Kzh(IbH0#Wq&WesyIfv@+}eW#C8?zRC|T^L0)Cqr%~tl z{iAmbbYYi%ut0Bj0hyq3VPb^!Fe^tIEU+{plopu%?gEB)R;hbDiny@q1CT(P+NLz z^~znBZ6U*8ae~lhvmv^)XpGC&j7B~q2Q(Gx>H-3bxyc|-UaUsL#^1W-Cp3*Nv@$7Ig$dci zj572pvW>*+Lk%l>`IR9uNsD+2{aildQZnbevpZ|>W6u_LxWp5*#RUWZ#NBt<&~>|H zL8ZJQvf4yy3ZFwqlkGHAp^|MW6Qrs)*Mf`NE%`9ortyd{Tdh_T!WMd2^n*_AkQP(I z=*4i2S%=euYciiMr-Dcd*#+!#i5)3!`N@?T-oDHDOSi%q8$xOG`8hP$JjTk z(|YR$-{pGR#?~3QbitRoH<-6-29v}};i0S5iK42eMw#a?>n@KKOt7FDD zBsdwMA0mA%ZDVOk4H4f=qyS{jSLsLxX(UwruLFgA3}XHiHb&oh9CgR*P||h=Q7IZF z%-s|J_fY)vgHI~U5JDUJJ?*dO(NHnc&;EU_U%nn;MGdCHPD`dJQVGYoM@(H+ZwPSI z;+wj2iPA;=H=aQ*bex?y!_ zA#Pg_JuE%NxofTF*5xX9eMXe`vsj7-AM~HskL}hZKpMo5v>g#1X6@mbh;n}hlQ{k* zyC9kUwz18W*5~KZZ%$NOymsibhysqhuh%~@=Ne3TBA`e+IWZyh4UihTblpZ2SI$;D z{YJs>v-|U7xX7+&w>f_I9)d904_^)1NV#)_7Jo9_g(Dlgf|?feWI;%afnh@mgaw8Z zVb-3@^=aq_W2F-I58#rcXOI*cvSHm4Zd9DK?s&BS53a#7DZVpf#W!2F)cwDd*G(e^ zOxviAG?3_t_TP;G=Dq1ZY9hPZuBcbGSGX!RP9yx2flF9%^iH$mAyVUEl(7+Z51cjC&?r~^ zU)WC0(hoS+USSiJB6HD2xA>o0z zjT2ce9tToD0 zOU86un+Xa1O$=-W`}U4@2WJ<`;y~8##^zksi zb_>Q@xYy@GCV(5U{-vn;^RY9=TF=A+76=B0;M}(Q7BsvTmd2jImQ-M1YDJ#NOv$v( z9S$KFvS0FORFb3m@`nsDO1R6o*zw74Q}e;r?J7y3+aJ?K3~=sH;G%XOt-egLzcU!9 zsH2!WXL_Wihbe3knyGp*+V0~X9j9+!>_;XSQ$_*)Bs6sGEMRIxlxJLDKlCr?5)Taz zjZ?EnuqAGpS%jn~V=p8#?BY`ls9zDCt7)a2;BK9q{Hmxa!MMd+SAp}9m$nm)nP(b@ z4u5}YO))+{`<2|r+WI2SNB3K-XY*Srt`QX9NQD&z^V(M+FQ?ZUA#cdTLxpr@JQ?k@1djZhTB zl+wjaU(#u_?v@nLZiP2Wl<#YUY=>NWL??f+RukXs&INqggOClJmECD$-Zu$)S0;Yt z^EMivIlQkEj$b>;6F(<;2TvNq=6Huh9H?%R3wp^t8r^&+wBiC)4PvJHBM}xG!1io#D$S}u65O{ieMOMKc z82~+K(!h9?)Upj1L6?cETsy&Z(E1{Z74<<{9Hz~}&eGV%6?cA=E>u8Vlbf8ijS9qh zX&RdH+CL*GFwy@M@K7-V45yVwJR-0Ct4EZ=Tc?G{jSOf4S#Vrgq@>gD?NXUmAx!GRgh8D2qGN9;qVmLdb|)U>pY zgkU%eqV^nZ`-xp)2k?s5O)f1<%H5 z(b>gidUke#eNl#M-D}iSBUZs>*LerFW|{HWJ9TJ3Joz`i)*e0HJ_5-D)}9NJBX~U{ zV8w7lIRkH0ESn5Y#jp9yC7OM*brccCyK;^3@K}vh;MG|$8m?_RFYDXUj*!5rOKjD&1!b54p2mg3;>Qf>z(iyl* z-WIwsb)3t}y_+wjVV;|jA{>kU=Oe750yO4kc2S|*u3j#aJA`_leww*NT{N0`LJuxf z#d=uE7av`o7vvw#4`ykl;ucl3r1r#WeMrK_)=Xey;tCMXH?*_)nUsVDEm8b&_#XJZ zrZyW9i?@c2xv1b2Ge4g+p?G~OW1c^Xf0Py2_jBNsGuuJRiumKk)2GbaU4SLAjY0YXLP%=HPFaQR~v zC3MYSKSGztd3Vj08TR4>dZ>nIF3?C;j|buh4<-4OS3cqVB-XiEDQgfiuBFOc_*cQEx1^lx9#9efVrInIy=;RfEbdERiaZ}IjpP6B36&%@IK@N%^Y)!s9zVjjXs^}?L|l|MubbQb0IozdE720#Nw{oPtAHe5!P`J%)R*Hhs=#F z4_Q5Y9wv-K44M#yCK|iOptE(Cpw>>I+TFZn4@x8rYkt=Y4kH6A6v{eQuFWcu!iBdD zgrdL_%!)c`HayHa(uNegBb7D5qIZex#7M%T1G9dEN@j*_lY$a&G7P;==3D3U}C}Z0d+D6>6lzPg;$R-M*dW4uA~`S;z}DeBgc-Y z?NLY=8Snb}rwRx)sc5Ow#ilzDTI1u}faOBuy(enZ5Yw_ZyB8>-=Zl8!YVBUGn%^+x zxEGmff!%%c(CDPBR-TlA6I*9v@F0b`5^HdDM6}#pPQy5AtOtiTpJh_Qv+s z0X31N59%*VUj~>Pf9LQDe$+vSf=tTRRDUfpH;LL9B|AGOaEN=cv_u^|t-&275;d^4 zjgCy@?itx{zb&>3Qx7X7f8V4^-HIO+_?+S*sD77B#Go@%!nH}lV)C+h#)s^ zMVxz%muA|`0S36wy zL&k1s>tx*f3u=GQOvRvvufLdwSl=0&X|R>&pXYlOd}f8m7FRF++Tcvq;X|MpkG?zB zapuWFN?2XpIIupAMVil1MC(2BPt#{Aa%3kSVYS<3+?2*nop}5c6QaUvIDwm-IEBuBPzlmt!ml$1bLq~J)-L?kNn49nVpwYQUG{)UiRwcyRNwFqE#~B!|x&FY_ zXzeGM2BF41A^hApd}Qv58=nznls^zB;B^VDp0iTtoRm0*x;rD)(oN}}TnJh#OHCsGnT)jS3^LUfnZes7`C4MOJtd=$Rht*5BpZ8$}&! zG-v{t$Y=$jRE$Lc^-;A~t+>|c@TEcf?ZIY2))N%{eCI#j0j2EiJv(IM?Uzt+M!Mr%9v5gF91C#-E$%nplA-E^8YF+en;L}&TgoF3PVFPXAwsw> zC~*%M$ROUd;zLPTS28;CNPN481h?^7jW<}BzoJ<4UcrNhZd|TgpOZN3W#hhFbY73w zNX9&iO`PBwT3h?qY*@OUyiW0vnp>JBjDupimNX6RECYid>4OgT+6xal@m(t)r$6-Z zV>WZ~Y#W>oXYL8F07QWodF+MYbH2#i-FgpLkJM>hrdtzd&#Z^`&$3?j@fdc|Eg-VU zOxrc&`ktv*qUhcA<~`TU+!5Rr>8xw2j+BWgD}uszd~GI4!z&nQUlP;{_s2@HM0&ZLv?>K9c=PH1(ccm){PYzsej%P?fm zEMs2_KiAL(j_u%Zp-)0n9>DRK$0@^cG@6PH_-buRveh+}yuo17p^j+@i81BIHEI zz_9UI@BVO2%RB5PiK^@Tr@kg;x+T5L+=h3WCcsSW)d`myZ@^l&T($%+4Ju)+o^xM( z`SbZPpcxt~KZ|DjsKnS^50QfG2~OVi!lYBOuXiTUCl&8NyN1&_$HN`-XD7B2S8Fip zp0$FPmLOgXLJ@tZVPU%!EeVAvUR51?RBq_p-n#JPlgB6F0*{y%JL5w+)5UQR^y~y@ zk{gv&rx6YAnt=2r_YS)D#~)e`dKYGi%5jd7R}x=7+#T|F_i}!Xi6zJxTd;q)-t!+4 zU=N3C)>(;5eTLPkf(0nZqu7G5SV6>o{vr@r#GRLQgtrV^kC`_-#aXpmqgfmx#do?- zl!xwCtQQ7&F^!!*aCx>7%|K2f5Daz@23`t&?^u7tj1X>ms*O)rRwt{!jMat&xGd>V zGi?FnnS6JL2l>cx}pY4N4FP@%9zU*PT zxkjB;v9KD%gC7S=9he?S7+)D4G|)Abtu>FqeQQT>Kx=pfpK_So#YzLCL-EH_j@X$D zhjJs96r9uTg-FSE-OTDn;P+Q4x?uUt0YS2a7z1CMijERr_HeBEPmiCzmTT z7KBX7%IFv;l;d!NvNCCxTgX*iWvvcf9bFS5@HcdC=#f9X~{mVY{zP zNdSbu#YT@UkBb61D6lsA*Aejc7 z7q@P3BdcSvH#+r3P8FAH?;uA`!}{Q^_J+fM3w!aD+TL_AAyh(F0%7U+35x!k76FRb zVra~>)|Gyob8cEt0nCO=lNWasWt>rJVW@G`6{zNGXBM`SWm9rbmM}0D*AybNpAHvM zSN+cMHo*HipQ=&?`B#g&N*w#yJ150{U}-9& zk-PqPGN_t^b4$zdqq=yj$-MK7%3}(xkkDMhVQy9N*lds;0BddkNQe;7Zq78;Vq+!o z_hfa;Uo(UA8%G%|sE(D7%C@+U#-DXaA%31s=$IaV+-4_LOYJ7<;YJkU{H?^ws;K0c zJTej-f#9}dVkUgNMevPsboA$Noa4&u8Z%p{nNbmPayWKNw#HCd7M^gy%a<=LDVkg? zzdszjI&k^oG%XQ8zv?*F%urtWpc-D7^=lxM`&NxX+i$}3i+_pHnig{N=AfKNiPK5& zPtE-@QfPbEN!LHY7%_o6PvMU{@&(9=m#$;!x%>kt!|T=&Avu0g$Tfl06od9bn&krJ zu{ojDZ#Bh3Vs>^`JYmKWX&Db=80m=L(HTRx76J`kP&d!*kRJE{ zhj78BH#PQ9%?qTmGpQe05k45gpS9&Oo=L+k=#0Q{PsZ$3_WE(Nl*89 zz9C19q>q@4+%N{B3(aGWW_bH_0Hl?B3(el-diOsloHTQ5KrTU|7je`m|STI==cEMl4y*}zInNIe3dv(|bkLY!IXCj?=OgAps?l%JYV@Z6`}5jd)hNO1ZuAe1JeRD zC>4npGjYeoj_PY%G?QHskx7E|SY&KN8=Jly1jP||g44mK%`=LY3-+9y#(C;HKGI{{ zqr#oQ0XC`~cq?!@#x%OynYU7-$ zXpiMr_Jr_RXO}MFLaoLnVq>$p)$IW6(PgDu*H8w3*RRoV)(7IQ1WlBdfr`=cM2QI!xF>L$L2#xAZ`zd=W+2Iqe6pNx$fXIW{`ymbxW0MYtyJSlNcB7gG{lTyTK^BxS9suQi;O>AKJf zCYuP9WgbC^8uQKCJW6wGLsMhIZ@L5y&Q7E>4A~}3FUiD6$79UFnxs?d`0k!#+tx0yESM7jAaeHzp5km8)nSwIZcbU5ZLA*Ni-T-* zo0e)Zx!VqAhsIvYsS-doA8P^)BT|;=KND8D6L@1vNMJ)`=^UJ#_syI>j?rL_p?8SB zc1i(zmzPtwc)7<%<}d%=jh}J|zwtZ998AnFhc7k176`gmX3oAPq;qlc?Cu;7g)g8& zr)8r8|5AULtuPhsm%2Wn2^bJ=wk4}qANCv6StoLpOtH_N(zjbDvfT|h;s>VA$)-E!HjMY{ z@%`mIdoIda(yFq6_Zgbt@J);nURb<8sNqKDCoF ze?n&Jz~O|t{>_jgkXhodG+Dj^fi{n4qr)>fS2grddjXLm^&hMw>%U=n4%w*8KrMtG z;a?Mj7VjrH1w_7I@|jEJ8nTWan|=R&!Ib+$#@CXCQXa%qoA(i8&5gN#Lhv)AWyZxv z5@&^B#bJt&aQC*0#or+I;2+L*9G;e98zb`g?H+IjnkYQOn; zwF-<=XwAX|`+|y5llIHPd1bnRBF(f&P5gTkr@dOWJ?TVM-9kB)4){E|c+1_cM1(@= zNa5Ti3z-%obg;Hoq4qm~zeTu^{Uq$Z^LH_RO^bz$@nsx`K1CrE73NK3}4{;o$?WzbAB?dLUy>4}k5=E})n*UXK)gt2;W?{MksB5)J%yN*6(B*(;?13kyX zg7f6>l#3EFitA&L8`C^-`N+x2o7L)4M&**l<+_*jOyx+h#p6Spqt#=+a_-w!+SCKk zwzI)w<-Ps7X~*_;rD0C;9^=&dr)yRDWMLy=>YJQbAjbx>ei^>Sp{ecPk%a~ELg8H1 zPfVMMfyafMZZMmA#M&EowzlId4+8CfOipbrRi#Y?tN+f+R9!ha7qq*%8WV9`27mJHkW_|sa$KSYMc1n{NwSU|Zh9QUlEZ+Ay3 z3h!BYn(Yb3G2)Ah8sv-{gMv7w3>Ov_mznbNbw+Zvw2M_EZ}2 z3){qHt-_-&zYs+n)=1LSj=S!I#i;Y&+gGj5+cE#q_e16Dd}jU|!;vjjZl`sSK92~Z zhv>SrxtG%Q1*c*7&2g&AcNEw^K~v*9wz<(QZr>h~2A7(@ObhTKc8f|dOf38%>kr;W zf5qV@uI34v9I4{=>^#}6u{ZE{Q zhJgWYb$^T^F`<_hplW>J(FeFzkm*S|)*L$tMYPxVh!LJ7A@d;ncvJeJjG zyZQe3r&vQ`$OLH*Uam7o4n`&hBu_PM`h+{%-u~73m*1+o%Y}Q4nkX2eTxe8T7X!rR zb5HX~?|7Bp-9C?Q7~y`%ZkANnqV>RayLx;i-O?t|)3bF?PgA*;{`k}E=?ip`_+A3v z-~w3+`IbirIsp0f842P%{He(h4su0;Iqp{Rxojgg%aFeMKY#oe(n`DL&>Enoi3gOC zQKf(ZZg7N6Y+%*0vW$#CGJTUt@JSbQ53)I`!;4)_3=%C|5sTN}x*n<%J0~pUQhzeo zA9CABk>UHw=sqeyomYpt_r}RWD%Ahkkj~lI{2^}d13Dfa9_>tSmz^rZn<$67Bc)^a z1eM|mhn=O^Y@*#^X8A0x9!SqEm-o$++f6i~zKkG4BDc@k(d)f@)>a5p%D;c~l6g8c zIzJW(Yk}iPpKmh$1fXiwu#hd8_h;F*a&1H`ccB{heuhdTGRjGP6N6F$f8r=LUX!Q) z!~xe;U5V~@?&-5$6akNn)rd^}|CG^RABcNbcoVO;KLR>!woFyfmpOHpnhtebFPFg^ z-mlA~gybR=iTvN@xraBJ4quVV2e_+~p$^NXWLqY$-&^@4X}3z3wMs4k1y0Ht9Rz&t zG2WoZYS5z{^+Zo3Z>Dlu<9JrbrqkPuS}8o76evN`DS;K{;oP_>fihHY^P+EsGe8n zG@C;CW`?dosmIg+WJDA{51u{TC|?dBRxugtvAv|7*)dee--6;eB(9s!AGqKYr#5^25WkCGK72 zHKu31c*Ox}aEO0zS(+kx+ZK|UCb2H71iueJd9_`ls0&HSj(1SZ@J8I+xUzizbc};f z!pI%_QpsEzcRAI>Bw=mIaZyy<;3gvXDIS;jcz?UYS-e;ul_+g|Y=%Y}KJ52!6nRNr zdT9oz-+YmURdaMcJ#daZI9`3aM^gcD(TIbuU-UeFb}5<~$& zte+AT>=E^)Wxub8kU8{u@I3o-pd+6GGqd&=tz)+cL-j$?nGvE6MV)qHWqFC0({#Yi z@9*M{LumTv7uSS$_b<@uOEwc8}Tp$Ck-F&t`#S{ZXj5BP|Rh!FCk<7 zg`_FWQnqCxepEb5@NT=TgEF)Vch6Foq=Fn112ch%_ckmo%`-?JF$V8=c*7ee;D4db zki$fF_sWt|iAqjaZ;}IYs&ej(1}5`acxr_$Uw8B;B2KBt32Q#_x3=Q_K0Pn2;)tdd zLOwdT>jxikfm|;SnwK#>DWO}eP8HO=1t;yqZmc{5U>18Ny%&DS7;?x(ilq>5f;&Zm z8=z+yZn*2J+M0>*gk3bS-SX;=%PWgp+v5hY9ZfpUC>gi$Vx5hWWCs zk$ejVeJ7HA0D*X`8JVOv6(i$r3&!QlH7Nnc=Kr@b%qf>XP`6ucw|)3s!6kBnPIowz z@ijxGF&kmUcl3gid z>RN}y1Zuoe-`I(h72b-)aN9r$>1!b$wZ4vcsoPs~qoA^HgXN48zS}va%);vbZ|jo= zL<@MMmnjp-h{Xlqpu5-8a_jixIlOK#vv2c6(XGGo8G7;k)m@u(P7`gbc~m9tl$)bq z`Qy4fKf% zi4Zl5C;q(Y9Hp|qIe_#(A6Kr+*wZ#yD$}yjge(3`-~2bKK)wjAB8fB3>vmyaV-t~> zIKRoGrLAps-xg(UVj`-df-cy9qKWu`1*t}RY}wdHUnaU!U?sj$rE!mrS}9(8Qw>KU z6un9$DM5x{cbmzI=H7zJ6*Z=Z*Wd)9ZcX^Ty2r9ieHgS^s?gI^e__oNL|=+K^75o4 zyfAYpBB=ltdhCV0w~dJetMc}iy-NWj&t-}X;2BLourT?qhZ$casgsY&T%CUc$Ng2? z4*`IfgFDgx^Usg4?U^v)zLb@frIXdy*5-8>-;0dj?->~#4_v*mgtK;twxq_Zv3Adx z2rA6PjrgdCF4$Wo{bn2?=cbSEyyk8#RpzS57g1XMMzf$;A*&o0FKuRqm4&rX&)B;W@p)5_2S#|gU$D-sLH+N*w#@7uN zR@J}}Zgjg%khqErE2Is^CMc)~$#WHO!^aqWcQ?d#ukeX!V_no!8IBxW^%E0MMj8j8 zdW$u`LnfAbY_g|O_bq!@=?=3u4_#7zvdGwkh>gW^d{R;xTwrMswwbhCS*)q0XDW_J zODfmQ&3&bd=c{)}`|=7Zp$C(b3Pivc6H?osb8}N+2YD_^j{V@azO;j%my@=@Z?gzB^Sx!j|k#sjh_ zp(mkb^)GKXI#6f-8BrR?r_54&oNV-4gYq|Zg^5rg*0TfZK@os&M4qy#K5&0P%F>3P zimjSunev^#ThX3XOd;#)P7Jl;e!rl|bmqYd`oH=B|2zZV0f+|*64g`bW8B^%SqZ9f z*^bz(D%PWEX)R{fli+%Soyr!pcr8((UbVG$yKAytq$oh{9>3f{@AFuKTXZh;4AZIp?fX%@alhmrYy@e|K5KlI4lWCci@UfapH}YBwo~U3S$hnsNcEAkK;NKp-mmmvD+eN_{ z3y*kRTKTM~rx6t#4`sQG+svzZn^zJ~8TxpfsRCsk#LY05c-3rj@p*LzKV4`B5i|~E zcSA>u1oLVD2RB>obVYWr^nJ1Kf~98EKG-M;78(79=irDss_)(k>;BnLcamx=tc_fL z`+AQe2>2(0@_zmNsqJ~1qu4O_m6`|;2N}lQQCG(MO0w|;0g7|I3b@SzuAXuQamne~ z7L!r8tph8E^k``6V8bIx&*eXJe0-$xeSxim7sp_)k9S{<9&aCVZ(fPN=L`NI0fQFi zIr7!HmxL@fBjL0err;aQ6qV&D+^-s6slz&@ zZKRRh+QBW3jPdwFja)(faj!=d6C^9o*#8wVfZQGaX#q9CddYIS=oQ;Riw}uL>K~p7 z-zoi`vF@j-MwM#MFnB7B^9MSL?>sqIb;6N%DY8CK`E9L|rEH;kknQ5(fo*a`sZt)* zc30l+K@Q=rLP}P?XLBNvSHU{8V@KFd3Q6O;k>zjeZ!3%GOdw98rg6y{OgaVzMVx&w zc?t0dWcUZ@xGgnCrp?y(v?Mfjb&;;`X5mMu!p--_BD0=1t>bVTzkrG^CKId_gqwb`hr&{!@eR#6gB3?4#P46ZM%g}m@^U*MkUu77Drj<|<)q(aR@8LOI#Zxdy9)4R#| z5v#ItXoi<1!i#|sxmLMm1e?O3e`4m@*oM$r7}s}vHYUZFmQks$ht26M3$DlCfV6(I zsJle(LIQj?pzll(fBPb94bFLI^qqwwc(s`b6ulPkgYM0KYI$~0PEdao=3qLHsAqLF z`mmwzw}$?%ovB}88DqYTG>ZuMtGDV%za)C6Yhyvt`cVI9;$Wp6(K-@kQ>lMr5%eII znuU_WmYw+((dUv_;-o|etf$V&1tqJL^&u71QE<(FM{WG;|! z^iw@vQc|*h6V8DX8qvp%TPAmrR(E^YsJ*=4si>857+_+B$E72kr@zB`N6PsHx z^X!*`I&$qb%S0wu*yTDU-9#q^Uw>o~HG86#d~HAoYVBNS*W)@ZDd8 zP!!aYfL5qy>+}j(ePw*pjJ+LIIxbd+u<3`&9)b;vyG|~1dX_iN^z9N zyS;rOcIN-d!_YgC;1Lp*1oudC`uc~E# zTh7Rpg#9_x-Q9g_-*6~$4lRsmqo;4#pLWCU30n8i!%%>66rik^(f_XF=)R)0b9^de zp@~rVh5L!8*+~p9J3O8k>%fdC_O#Fj zSBoe5V{=&QvOwN0T8etE81r&BxY=m#hsLqD@=1_TU2h+(epVL&)IM3N<>OKI?3B`b zMseb~@~i)v)QFM$$NwAXAsL!*@qA$hjVr7~1zb0gDbj%ne=?Ww3YJ!YEy5#as&FkF zt_per3)s3A8M5%U391#R*L8QC8hOQA&knWBVY3c6n z9D3;PhM|U-dFTB8=bYzx-r8UGm$~*_*Shvvd)@cCLtc%@oPV?&s@Azvw~bZ$GI6-} zxNX(Q0oAHvdnF664JdpM%P&8@J8%Awohc-MW;>G(*R-h(#`rm%|DkAlr*w4ruSeEb ztmjp=KzY}qs5hunFL|2Yg!0|N8gEt>r$>D;$y!uj8qGFK!h&4v3|S=MX>097Rx$sf z|7T{e5Lt4$>=Xu;6IM+v>WaUC@GR6UZ1rw!Q`I29ms!mBz^mO|fJ3W1%04?p7-Ub{ zkA%89e6hYSnAl;f^~L93fRFzhPX7zHfjdz+VT#Vb#sjCETvi~y+h2*J=#~_}P;=kj z&eYn+`_U~m8vz{<`0t$hhmU&>@}02eFPupmefjY4V%k<9?bWQ*|2+0T%=^D-i2s@| z{!K`sZm1`qD82u*J^hbF@_z$9{|L1Ie}C{V>`agIU;O#*AG)SpBdAETSgl9{boS%=y3nRiKxQCTh|6Z>iJTO%RHeL!C$d z2TVQt51aBo-xt)Y_iwJ~|N963vkv{|k)6dwU*rDve<~zl$i8@`F*#Rlq*6tzbq@W% z4U0Q^xZlp=K)Y+n_Np)cC))>?Mv(IZw`wtU^m)KMSQW5hh>jdf@`)24 zp8X5y{jY)KZq79~(q=W6|S}w0*Ls@woY%9*&08foI?w!HrB5X z*UYJD6c%S!+(1RwfFe}A-SGnQHCbZZex->G1s(g~)~PI0a(lfaYhoIBsAuh6&J=85 z8%r~9&KB26NTPU{t&q@FQ#1Egy-gN|AR8o9C`vIc^-qoPoF_|gP1SIE-l$<|YaY|N z{xTsA9PsF1>0%kz*-4YXL!<_~uKMyI>#J3yZJ~1zty^7`Y@1jYtx%hGDxLTF6P#D$ z;lX}n6Elu&<^f3=DGQsL0?i`cy?!vXrZ(81*8Zx{_cs%{H`CjGzPDpkez@v&-IC~r zXhV$p_MhBrBUBFEg`yVRj@8oZb{Rq0H8D!yN69;b_0#{=8JBqAay%Cg2 z<9*GOpM-p!gWve9AkON4K^u`#3h?|B{zz7@;e%QF1SQN{#uxAEp<0Dt=$JQzuD*X$M>azZ_?02I5B0fhX9&KuF zs>5?5U7tC$?A=7@BiGjggEGY3Q-8EzY^7N^`iMF2OtaW5M*)xKTvQDBX=nR<`l5{gU(6UENeN870}m-}wT^7BMCOt(xB6h0h*=1uv8vB7;g_7DV{%ktII) zonqbd`IK;nhGjZA|2N&SQ>G$@dF?3LZynOjrs!1u8#x!8@Em4=Z|KDAET^gWgHWU4 zJytR}&Y)0>NPtWlI?L(V1c8@~q^V(c_MIa)u_}0Nx~TTi_ux{brHo}DE>1v$a0R%2TM0B5!aetIQHt7#JF(zR&%bT-hKr|3Po}75h>tb`cgID5x(BBw zDx^kMrt+g)nQ+9@rIvC&h|%c5Crqe+&|YRpVlV!QA*^Fy?zL7sGg-(E8j7Nbb73Ov z9!{Su zAnj61n2qp{Jb$)*lzcyG{Dr0MIw>Vp?{!kl=xtRTm>}@CyH_Zno^?0AgLBZ_{eo+$ zsm)D=MbF0^3V;hTlO6ds1NYyQ>>iKH<3` zLc)!n$nH^V`gLX8dg*piDyilb}n?I6voq&X88MPdWob~`wtVR+%nQE@<%mO`S{hj3pr-H(DxhcZnc#jiT;tBTB0z? z7xim#2b2Y>Exmy46xN)!iFyTFQ4-W7iZgUUvm_P)a{c4c-jXuiFlS$nBQeE~o|r45 z_qV$JSt~ZUXv0B}Tg%0QIa$D(+OOwezm}lfFe!5==SaSRbj27Pcqs{f?nc+Oxg| zwU^lXw3`xfnj<)tZeVn}qjJs+5PFPLySTpWoF9wf%7ria_q|?4GkV%h&Bp~jQ|%3cXA1DuA1rD*aT1-$~KWn?96R|-UW$$|ZcBg1!< z6&2aOsW+d;u8nh0LczmVECE@;gM$S6je4VyDAWghm=`hGZosQe`M_7Nf`B(6QtkRJ zZKeX&8gv$FKpT^t0M~#5(7^&+`YN~OinrZFkKBK<4mNeyqCJ>@=s1M;!~C@^Kz{_1?`)7B-R5L@k4*cwM!BvN50ly_5VQVdu(8KBQ9N5Q zJFdWnquF;=4GhYUp0{^Xkj5)a`Yo^#6Ji@O&Ya&sOxn4;^+BU!`aQbI3c`FFK*u{Coi6{pgi|RdU{^HI}pj0iJ8zy3C>IuNsf#-k5;jCHhg&? zv|l7lvhQ+U#p7B1H-=51#Vj@VQ1iiOSVi3qBbH5*1w=CBEg|+7b2o-VHP&P5RcrVz z53ohS9V%S$!NZTR{gb*!^xiSfA_=3;T?v%EOdBEFA$Nllywf5(zDCXd!pqq7uo|2I9i=+SD@NZrn%3*K^F zuJ_1$iXMw^l0qsv$U=a2pc%l(Gaw#tpv@=9W4$5Tqcu1T-SiONy%5#^!$ecdaa_j{R_P9sfav1ChF&IF!0i9&%L~V z(q*ezqLK$WXn(N_boC$9Cp7+BV_2QdR`qKa(e{h63GR=Y>#19ug>D-t{nPof@9=gwe5>*8F0+)K8Zr+@Pu-Y z6Le%EMkptNsW(#WTG@!{ws8S@Z}<`;w=@BJ}W11?B;-~zeN(yI^Cot%?~PFJgN zqYM2A&$2pO8XmDdvbcKy%)fsBF223{8KbwPYBlz@?e2n&lT3Hr)T-;`Sg&yltmGi| zRLMmIyghP2HoILm~et>#!(>&F+Ecc%g=OQ=dgcK<_ua#2znRj{GP z!N>2os?`=?Jo!moEv;mHV~;>c{qefv47ZsCrAC4|5I$n4a(M68Z~Ql3lp8wV@10D@ zAF=7N^G>hsw0nFKLktsB30DQyThKmBHj=t8wWQ)dSQekvLT1~wG3{{;uC~)0c|5PW}>3^ zb!R`#$J{yf-VphRFhPb>V~bf@9lHytPZ{%mgEoB=MQ9lPE(Zg674E%4;<~nq{jLWE zyIg%)+V+`KHw?swoYs3OU)7z?g%q{&)#W!G-T5%NeDvq1zq7@)H2uhwZD}QJY2N<} zE9tJdvEk6H|x=Yul&MgK%jIo-;FN%`vC%KKTTP!%esS7GJ)m z^)@~hH2Rc>jukvPTB!#Y$`#S0juP6++dFBvvmSQ$ClR6_d%~du7_L0k*QQZ7YNPI( z#?Dx5(b`y87tZyQCIwOG5(LFK9&X&Nxlc#mq~gzJ2(%6r2g*jceNdzgqk>CKb3cqG98KgDX2+9EJ^CSf4N<7*1y0NN@@Ov4xU7KoPI2PSs$k{91}pEF z%-u{DE)~=Jq|#@333f&mZ?D7xO z_CzS2rR(csJcU4wnNrw|_|P|10Fn${4)CDwFkkIY9voG6E2eukKP%BAVP_oRojN)F zqV3L}Tqyi5c2IM$dAp+kob`@_E0Ir6a`W5FSD*fTZG%i2eJx(@Iv%%rs9BvaF(zbh zeeLLnku#36!?C`5b5B3msefP=(@2ZY!w)X9>qk|Q>v!`eW~T8aYZ$EIE&i{Q^KL(e z(!ne##HR)*bZirDc#7WXbj<>|t( z;)Sa`iObZUyNRQ6r)#*}lrmnNov&@R8N}_^|1POZ2*0m$9plefFY?}E6-ZYro{10= z-@LqzKKGh#Ny=BJO@_q3CFs~kSIj;o-7P(P@Z}(ufYk1N&c97?AakZCY*# zx)YeVNb!2=&3m4`#=JhYxQB{WG`fa_GQ1^=)C~iCZ#{QW+4J`AHy#Qh=Mk@?lt@-G zrl_C7gl{a;340sRa5KN4CjL_AkHDW*V_}|QRsVCufZl|;Y7D>wowEo+FDB5Iw}s$G zQ_?arKPy0o&^g~m*Mr052BYZC^XVsUM}9ibu&8`426y{yNc0f$Q1I6dJ5MP4&y(8oGYFhyEg`5i?zRp-f#{V7t_mWpIu3!SGiKTzE2zWFTi~nx1*}Xn*|1y z%IWO#Q}fPf^@Iv}at8q?n0H^+2zO^q?PzG^1MP3{D5R@M4+l4fA#L<$YDL%7%2zgl zM{||bo{-Nt46ydYaS?I!@7(RqO1#^vF&bhG`(mxiGIt^y2M1xrpr_?l7N`v_ue6OJ zRjSUu{;QX3YtWFh_Qm&$W=1#;=-QghItOgK8-c^quQ=>ZFBwGj-aO<|&l^+a*9DA> zc(_Q+5eo@<`jrRA-1C1VE5|qB!*YP9MiG-7EKCaKO&Atb2XVcu725t@gbop9(xGCO z-5r1m%UFyH2@g~+A}B<*dpk-L^x7Kf(P4_BbKb$_%SF>w3MU*)I0hR!U+vJ|l6EFL zx5fU{FPc6n?XnhAqNJ_yI@NFC!5llT;y0c4wO3KS766AHCouf`CT*1g9n4+b`*s9z8g* zm#%>iQO_MENVgU%0$hDH(e{p0dim?(0hRFI<Jju-(cWKg@PnN08 z;@$=u3k)!aK*1(d2?H%;@1_3Se+;;S%U6Z5z7TAW0CpGz?`qaYH%heb4={nwpVwE1 z{}Np8XS%nyJ^)*Kdxz%bF&w9}z$Jr<$cI|=BNd8diZrs-*;e>c7VD1IR$z>1RJZe0 zcB}FV&AQx4IQdxy!f3~EHNT7Vf~CJ+X579LfW~N}$6h1nA<-X|7FV7)JMdDl%cMwD z=uZoWReGCM{rR*u#0%#~!V;WyuXA!d@|EXFFD3zgjT{2t~VTe=)wgx2w- z0@`Hy`!}7;y0SK#YLttg6e{_eao5Ld3q61T^)E3s$S+2&Q9fho;$@cnTWd6aMVS_! zl-Y?EGH+!mS*&`~vQ1O7q0S&o=bBet^ZUGDhvbEbCdIB+;SZ0?FI8Ek%4Ea+Jbrz$ zG8k!f4sq&mP1&<|zE9D?r&7P-kh{;3K_1-;y+bay7hsa}dF}@{3 zYXZ{F!AJ3;C>Hup9+?J@4fF3Sx%4EFZ|oq(HZ3=eRIWe2t!a}TgiVN45G=kfJ-;*T zBLlnI#&;%gw9$?%MwGYBMc&WI$XZ$y3Thdi^h0MU`aABGIEQjw*v~uqcbXEM?5O+G~v(70=)-A zSdTON6n6)}2z}bYNSAFGqJ?dJ*kCqM284QTlhTU$D8+=249<))FK(L(e2#;gUVSn@|*Er;eYpHl4Fka6=|q79WRJEjenzrUgqa{94xK%^uMnSMn+ zQaq35W*$i;e|%4v5%2Y5nH z|HhM#LX}C?uqN^XWwFyD6d!!Y#}{%Am9-(jdq9Nn=3^Ek(470WS9xL0ZZ$-1+R#7dNX`CeA>MXj_*!mC`nb|+G;LzzQu(j*;fItkWd zffS))4mbnP#AOP)5vUVEKzx4D{^46WpKtrZkVv?3Ux800pfO0b4kh-V$2NN9d+mr+ z_qEFQH_s6EG?0XwGxd3t={LGQA4Y-j;qs^$wJC*!2`b5zCSn3RUkXY}mx}7%SuXW^ zgFp2pN^ZuWHK<=5jHK>bMOauE6{4r8=`C&M^4FYslC~U=bjr4kAxEaR0ij-*qHqE4sw9g~M#8{r+DU{pA3I zMdPYB((@=BQ{cw&B4($fdqw-+U-N#>im*rce7hg)d>z+#t#+nZ5LLN7#gXz{!ql)6 z)0+8LP5cF5XNp5Fbm$aa(`zlKKZ)|*{y-{zNMZA)E=Tb8y2EpVSjl@raVn9X8H z!KA1#{O6jZ*}yI}C}Me;%rKFm@+KVe(w`UYuOX*GAC!FqpLLpEPEWz*%I2i7d#i`@ z+MtGsm6uEl7QVM&WZ`Al8=`bcZi2hV6lP%Jy<`#eirIA`D zazEO}<+fpJEHht zw#rY&AHh$&Y3lH9J9B#HP{&(+LGYJuj)~2maPC)PV+!6(s+k{lyBJKVwJO+?V*_T= zV8mR`>krSzzyOGM=2S-gmH#QVbl5y}$+?Ywrg;F4y<5AjLfQUaT?$vVi;~+l{C5!# z^&?EyT=NPo#+QwR;0sd|Q6m3VA5d*KqAshtm>}65_m`@2$7LK|+j(P2iPD=xGYF|a z6RVGvces5WrDu?${&;I)DO)ICfyx?LKuD9-c?Pt;;17^Uv%g`-_Qy=35`SUQP)pFt zPrg0fC39EDv`cHQB7d8uUAKy}U)fHUNj2O~BS#WmwGl~Smxo777Yu#Z>d3K-wTLXz zXuG>qN{5c?BhroVwDCWNJK4>WmNYEtM>@Pt1`}hMoeUCg$`Z_u=^q-!WruL7$K;PQ zMM{H8wQ!`X+3YQC{(|+M;O%83&8q7rPZ8sUjuy(~Yw$|kzHwrs8_SYl>K)(Jlzq!P zLvJG3CmhnpTVP4a0CDtk)!Y@Y&#EmwB@mfWbu{xr)FcED`P&%{`R?jaI`(uZTV^thj@dEUz0|MIG~w{w%T*zcQ9@cTnPit$QwYnwD?Ujn72Zc@Yd z!$Z!``|d=4h!5$NvoVPbP*U1}jE22#1m`;YG;DB3m=Ci|J$_^Ob^~u$%VoQ9pfxb> z^!Z*=KU8R*>!N&z+hC&h1ReAgww7qH@kM&VQgs0Y z->d zp0&4o)>>=0$(z-4^Y#t(Vd>fF*^ATek?nQpA>ZT^CC$yx89P|~3|UoXoZ*HV1V$e- zjIfVZ*3@KI*?rI6>Cc#IX78=&?(36PsAqi8WI;oBFPbxG?2P!1t5N06Th?r5`;c$o z8v!w5d?%jycAJ@|Oy$6MT-Rz*4Z=t)J~BvBJ+dI5qSMyyTN>& zm&ie7UY%Loher3!V8n=mC{!W64SpMRM-VPcYin6(L$Oif$%9;|_;$M>^8O=ijyBvC z*Ngy2I2%1FDJ%zqyk0-Vm=be&EmFZNBo~;lDk`jKv`CW3F=b7N_4LodEj8|8gJ1RA zRyN$Dwkxd9K#qn`&wM@-Q zHM95{eBi)Hh7wUGB@?A&rkm-6Wox&K{Z1&aqRn)&_SVt!Y&~ecQ$HHrPD+NFS|7IW zhv*h5YyF+Jb9rkAs46$SkM&@2O6|>iTLiH?W+B8nR?GkV^tabkd*W9I@M^Yf-Gxc> zaLLeiVGU31HnjhSvF?qr#ObIoN}uT;E5E609v~%#>NIjqoo@F8v>Bz5_0@4K4(_MU zBpJk+vgOU85N}}yb%h~JzN9}?t$2r-G;fjYq#7W3Qa&oR9vCzT;TBOH_VmS85 z*jQzajq-S5tzBaUcRLTn3HqKfxteq)z0%=iH6NY@Hqt(f53Y6jbhVB37Vx7cq{b|f zZDzGus0PRt57K_w=s^KH^tF52tLuM!D|nhWYQK_P%=>&*4qDCKc4rH)$wME|(pm+} zMJg;Z!S4_z4$8InT4fpz2U!=(k`pD7s`*}G;NpQTEJG+DW;TGS9(&=vph`lw$w(`K zHsU38^-gj;K$4T4!*0{I#*Su{KV-PXdUvN^O<)>3u zwR+m0+1b>NTHqK4pl0nV09Wy&-X< zy~7b%qK9(ZaNk_*A)~)#Y5C!u6(k)fKPkL&3>aM?$+go;FQ2tGD@mN!Fl5v!Jtgnm zX}#|ZFfjGbX>s+BaOz{+=HMtT|7kh3n^p%_Z(iU*l)QLWN@s@w12oLQc9Uu!iav6H zs>%ShCHE$!yCCfoq(E_RwHHyXuC(V3KaF>{Mtf4APvY5zRB+PF6c?xp4xYzuS_N`IQ!JIJKHohQV06?m(&Oo0m8vvwre z-Ku3oa8*;E!Ym!erlPw4M73LeWlKHQ9W+N-!2f|#)Z&a{mBKRf@5zLU9bRzpy=$6` zNU^m$5%MvCZ2>t8keC@+C#F8$2>^;`$J-(eB0ismnj+Sc15p}r@)bk+xBNL9yE#03 zc#w|87Q@ehron%!{CxV8Uq8$+7u+|#-3u<(di{J9o!pKeY7`vaD%5){{o)zu9v=g? z+B|)3MI`W030F-v9}~Qkn}oN!mu;DhKC6Zm4`^+TjB>`tLx%v#3za_N9k(M`;n>AW z?OAKCpE;7_7;QnEvRIr08164}sG%=--9q>wY+ChuV=*xR9m z?XbBWw_ejH{0Oi!y7)4E|6=?MGw8jqr$U`az3N%G$+2V9hayDd>UfJwKR$n<1}p!I z0K!P0N5}MsN2XUbo-7s%{DEsuLa4x>N*&aaSeXXkE#5#P3bCB)rchK+q41i$SR&+!dygwK+f2=Xk zF61)?CQGn6d<<>VfZRU6r!+h}$&KAX`I* z)MHi@gqHhfTbR5BYoXx$ZT~Lw&k82Jpbvqr4}SU)WLyyrT>!HV?ng)!CBZ^ajqCrC zI!i4M@Ph2sd)i$KH;_D~Ye-(i@*a{O6*8DunwP5KsE|DJTdh&>7&*Q3)&7A)KAmf3 zD!)9>=1{(`){jFq9Z^@R;9wvRXpLS~jvw)OjvOocz?<^*zk5^xGJ#`)#HAvacYh}n z&Z>rgB<;UZ&zG~WjZDxg^Ph)Mi~h&q_&_&Bi^zM_Gpm;mwm3Dv%xsC=MAA^M#wRd_ zBK6HO%Ya)ricRfM@af*yFO=Z zjmHC+%Wg`LQgLMpc_}5a-ymWjim<#AtP$yZH633ga`NiN8%R#wb8B>|cxx8_22m=_ zf*dT31AqHJ1>1cn*(q_dEuztZ2ifyC0l>qu6CDu_2LWOPN3D-fk-NT83++()M2^_W z+R6O(sU08cgOFi|1fe^&yKC}m?`pvtcywMHBGm@NMhu8zt;okW-T2aY%G~V1Q36cx5*F#=Tk)3ct5AOIEVX zHg71l_Ng6EMLG8WcR4&(VsOR|>E(%=>xC>flNsVYL*(+E8nwYDTcN;%`~RsW4^+Tq z!x9+20N9tZPFwRoQ2f6N|0f2P0r{DihXj&QJ`eP$n>00<9M z>HGGby%$neha<9i&@^|M_Wxx+FIO$jYLnWruwAL*73y4DW5&WgEKK#27+4!%2c7+U z7+(C#84N?3opNN*$yfO;0}j_W&iILh2o+d)_@u*7J;kZ&ESjjh`iN5K>X)C1f4?p_ z!7O*Kmor<_!Sg&U5?v6Nn@y@oh&b;DJ7n_tFgcO98M!_6d{UA{! zIiFSVh4t-DQcX_$cIFp^?zEO~M6?pH3^D8-!%+?_y4#jd3+EuqLoeYRqiGiMgy0FL zr0SSDnnujDVxPo-?Rj~5$L{*xJfcr6LH1@4qCwIGBdRLg{MA_E5eIiaVpqmq9e%2a z!egnnYql>}qJ7(!9iN4u5sVH9YnA|fkkVxQnfPWly4?F$?S6o3_0%An!}~hW7*coI zo(ob@`cIIW;OdXce+_Ky#x5CO7kOk5(N}B_$^@J^j#WRk&R;rd$O?4RY9B zSn9f@$&xQB0`%nM!@_Lm$O3*l(FM#OqxrP#Q9anoywEG9I+Gzk*%LH-rI=(zBrLgL zY43(7f? zs@NEY{KTnRmtvqVS0)usRNCV3@TBJzCDXH`vu}YlYGe`|*ha>-3T0>0Ji85h0dZ_#VkZ1l^*9)Va~68&5JvY% zin6OgjdS0TJ=XU<`}yNF1>rYTIbY?r8I@^Oc~(=Fd;so(g4yfn5mtXEPOUQ-Z-g;z`5<=0)7W}ZXetsb?M zZiXHOV>UJlTL#qNGwERxjU}`XOf(3mchs=j0>2O1ldMIhUMu;TsWamc;U`S@QFiC? z2#4}o7mu#>PC?zA9tj&b1R|hLN;c#tz5Nntk=SovxwHuduQ%T`++kqb4KB*b$Pd4( zO*L$(cctYQe#XQGR62gt7XDl*sUC&!Rs8b_Wn~;?Bts$9 z#@R9h%&}VkWdy!IT#j~gp$51}xi>MLmS(=h#qFEyB4LYuKg$J&NV_QgZO8rUAWbi} z=;uK>k~sET^9cX&G@`PW%cZh&?|}4;)?EMg;AKC>&o#P>j_L83A48l(q6#iyEE_9e zG#|e2m7arJoOHflI8w;rRd>L*x#Bd7(A2xKW246C5LCR+=Hc{e0Civ?$@=uRz zZ`z4`71iAg4>#VvR`@o6-hI>5gDNz79UfWfbw3LPrw0kBw|pSgBcT`NjQWD$9jX?H zd*I-%fYC_`QYd~wZ+Sx|vUuEk<=Bd(w0F~3C((_{2$6D;O}ba4N|&wl%nKMmr^y;D z0TjTyu#uo6!TDLkhps4|)5tZ7`BPyoAS%1GBCYzK)AXMK5u)f<5N!w)n!I z8r&Oc{m2;6%CR#Y8-n9rq?6yVesNYvs8}!v`YHFO-}AUF0sq)I(9JqkNekVToG4~^ zJ7YLkc)pp}#p?$+{`uD_IciSpdXLe_4}+@Ag(@)G%D?HV%;hPWBvz-lamQB{mIW9w z(@e#}Ed88+Wx~LwZVcX=uelH;$lybA5!S?v zt6q^ljJv{ee7>fZ;j4Yd(Cn-R%)Tn;LnENOjA!wW1xA*|UDgIk6K{ve#!$(8V#^e8 zim7!&@?&kY{wX{LRwH_dn{WR+d@kq34B|o2%q5+9;ay@lDt%qL;s_q zMf698A@v&vE2a0n{3z#vF?p@~6^>HrpQrp#OdPjgMFjQl7Mnk`E~1X;>Fg9@s0a5B zN!OIdnEGrtz3vahvc!2z*Of;7`Js#SUSgB{jM5qV(hC06o+EYn8*AXc4-M|Sq8S?r1~t@OPjRlm zvy=vIjH45R!tIhmd<%<hn zksEP|jif4E5bcZ%Qy0sWtU8_~VoMUr16h5o;qhs#7uItr)H!WsQDqZ4Th!1VH=|~5 zrat1j*9{_;h0;p?K?$YcW4x0wLDN%`uB8MLX&9tD2+t)l!|BolK9n}NyyyzYC5U7Z zeojxfNY>jmlnB=Y2|=|+ZV1l|p)vWXq)(4BNf+8SqN<<2lnu_uIc9kxQ2HDp+@qQE z*?{+*L$>Bp&(RLPvqD-3>CEw={VA>|E{3R|HJjBCsZP!?3d5>Z^K8M|r0{wT3xUuU+oxNkyc_q!k zAt0h4X47cPV63}DIh>~!X0mx8oneP@A;D97C0Ezhb-hlC#-ul_G#sVv7}e3mh+`v1 zccpyX6;*oR6-h$;D*wiVgd#(h<<|g*kM2QXvRj3}U$?((v9z}htEoFuePDP@qq|g< zI9OJ9O0^)eoXsR<*l-9N9+DJk3H^gZci z_DTu*sab-H%i~q9dVYrCNimdPSpIg3?_KANU{YlzizQLL8favAYmS!KRRrj-Zfj>t znxZXLs#eLNXKHDif#E`FK!?rzS;==@?zL3wr1fDq2RLYzXkEmV8u&k%mg!squ8lpY>zpb`PjA-ZfmJ zw*r_c5o5e&$3E0Rh2r=O5Fp zZ?8U6M47^_F_*q?6x!@tngqm-K7Oh5DXu*Ggitc>6V@4*X!@ueCMFu}VVE4jml!k*O!`CosL_m z(#jPWKdEhWv1s|kIt(9QV_Y^Y8okUV@=}QCY?d=@)VKM)1WQ|sXXKyy^5`)YmSuFa zjEt-pQv{h_^!rIOX>P%DOFs`BkQ`ma(b7IyIs?2b06dCmrt~^RJek<>dE#i-wAD(&SpO>(Y z#U41SesU94LzHy^5_hq1Uldw%VM%L2e%v1M2IueS=;-dKm*w(IP+RSC^Ttc_IAdA4 zdOxj)0{)me$i;Xv6E$&;fV&b)uOKWAP}13%z%9b9c#~dQ;;`d#b8vKRxOM1KI~T_RPa$kv!qgsP_0|v+Z){&d%8|b{++jHsh#iq!8s&v zkf=ndG(|$q8Qh)6nPD$6*Q7}^c-2v2Z7**BH@-F!x&=R{I33RMW`g6^9|`tEi<)z6gFFs;AVj=Jd7dx6RA?IfF66S9)U znQIV~p--ijvG&8tj|x3Vnm;MexX`JqC;NNRopl>>BIb5P|E_O01+2GosoP7*Ys1rI zDMe8-pXRb(LqIb{`FL3Y`tB_Ypzvh-9?y-GmNhN9WgPv8Cm~M(eNc*W794$kn~W- z)u29RxU@H5WHnWNSv@E`lZ#ixT4nY{h36k%53*<>z}eV^pr!iDLBl(7As8(m9nqIB zbQiu*ZCgn9?0fLQ79qvusQkcTb~!&jww+U$>n4xuSsu!=6%awqDRrJJ=e==6JlMwo zCSkqq7WnY}!IW$fY=9|rtum3pzGj*q-C95`Fq%2SQT|SEanq}r8k=0m9QwjqgIGp~ z?2V?NtaJoj_6(oj2cSYi(39Bh_}?P*1AX7Z+C*$=DJy{}@jvSe0!m`p9j$@0y5D7H ziD_QMSY$*`tT`15G(}|m@{JMS z>$QIgQIM1deru0sD1{y3B+(m_YlmW_{Eh6Hp^G|OoZ~V~jm=06%nTHH+1pLQQNQWK zb;|P5-};szoex)A*So^ajdrV!WfT?r=taMeza&g$PL#aLkE?09|W8rqt63BGl+Ze9cY>`VWf8o+RXok*$M;e(kIr_mbC3 z;w9-;*11?69{iXde?<$8&SiFTv;T-Qd|h;Q^V?rJ#X9$XL2a?Q)7qas_?jx$>i?qa z9pfVj+J50+lWc6;w!LvSwryvF&BV#Zwry=}XJgw=Cbq42?)y2w9)+8l%NGE#{^NDp&Xiy?zQ|u;7T$! z!yJ!+h$+Rdt&1(GV#SAvHHx@pt7G9VbySvA&j`CRTu;b|_YW_JN*FXfTyw5_Vfjo? z=gC}&*Jhs#;vtl>b#$%=vNk!Dx_JlYY$#306O4{X_udb3uMA)0u~x za0xiSXRSqLZD1(uVTyG8Ixv*3s2Dz&6*k6W_5}oZilxh!K?<+L`g67;`$^dZGIq-&ns#6ir>hG{MgV;rGv{v`}pF*01E5vc>c2 zNu>nb$dPeGcpp;}mtuye1*6^Mu{tzxrsc5T%>_R9ac5_c?G%*XU|RRgEMw9&1;(ji zKp^fy$BQ88{ou$%Zs1m14-E^{?5WkyqZzo>9E<&hDbiF!bUNo!bMFkGJK%Pm3iQtS zC6OdNFnP)Fc$IrI_=MFsd6~aU{mTCSe3Q(SV?YKn=(-MRp3)Ow)2#V&YArAGF+&T zKXn77-7@vdi%^-gn`eF9$O1V0EX^=UBe(Gy&4wt!&YzmJdv!v z#N=1}pEZnkl~bHJio5$RL35x*gleGs+gw&Fk0ya>1aDinNWUV;T2F0#a|O?bD$9xOm??*Rl`ocexoQTuUie3GO{Y?Uf*`JTM&@GFWCB>cwq{n`^oRGs6Vg)9=pB zrKYN*pIWpj4)J=ovLLz?i}xplZY*-0z^|t;6JyKFdNWG{JS{l;Uw}H1r!FCVkn1ld z_5?yB3OZBYQ_x5iRN_lxf6ws|C|28qb7Q zlS)abF00pr;0=^|*!Y~o@+*CX)KhEW#B@wt1@4c@>hrrZBUc^tR2g2Wb$GRXO5nvs zL~uH7ff}nWKBU&lCk2a$SV^D|_-bL4CiD&-#ja4ldcIb8!iv^K-+;`(y(spO1y|IS zciP|7{b4UOEhVw`7R29#^lDP8afS`w{EKLU=}u4{c(j!h-B2ERMM!1^j^ zB(=2cmc0ek-lI`8y&MaF#ay4Z!F(&hOgIs&ZicC2aW;Mc27OcHm*Inkb><|i^TlWr zopLs0rE(B5`{r@;Xs~6t`sz{y#6D+kzP((hUg7E3yF3gC41|C0cOm|{<%^)eVSI!))@MX?NY!1bzfID^lULJ_`60^* zu@PrvDk-uou`KZSXl2_MmS5k_lIHoTsF_<%2BRN|4L zyvQM0R)uR0soU3zgfVP#t{D(uSiq;>r*g$xfWRsrobUZMmK5$oDTq6@5)l#X;ZB!m zVvI2~^h!pAh2_fNFsFhep`{FSb{CkWwlG6REAe%q%C_bxOwEI?xl;S(e!MR9UKhwKN4FN+fYcuQ?)UI!yzhVPmX|jSk6b`VK zr)M$F??UOK-o8D5)9iG=M=W82JtrWTQze?>_WLA{%@I0`H54GD>xs7=q&*1l_I?}0 z?)1KkaP0P#lU7FV0&0D}7=A`?f8E5s-?aDvkqw3OjZdQZp@jgpUu%8B{SRHCgvB5;iqMNBidfo8Q5-fq zgjNUUwQ`Tg3`7O(%5+=bccfT~Wj^5lr0XpYQ&%qs>CaZ@Q`Y-1mN{%ga8STN&v0>x z7)S9-WZ!`;JoAx`5sUm5hz{LN^?v)%&7gK`Qv@U+$G|cIZi3gr7jI1yvI)}#pd5q- z*h?KB(+r%DJP;uXrrd50VbIye&dO_QsAyT!ySt(F0?<~vpby-rLC6=mY?2urtOzM* zT&U3;H3T9skOD@l$_ne$3r^dwvm@}6>JV{C$$ZCZMQ;(#2Oyq`X`QmVlfL>5cg{a3w=~G;(JOAzmWtSd1x5 z>>{sLc7ger;x?%HQZPIVDX-(R>iy^(Bwoyiu)$=CK?;`I#%aR2Ig|jlFC=Y|>xCHA$&sdqDYsv*7sDk)Q=gMa_lpn7(^ ziQZto9ln|g9wLEffj{7abpGzWGjtQ>fATz2Le_2>2DCL`cjqkaH!?%aXkmzmdmxho z_Yo>2pM<6g+2;f&>%#30V*ag63Jd>Tqf+2XPz z5L{Iv`_;hxXVOa$PAVp?`nrEnal&V68OEuZm7L}uc*}xYHTU{V%P)&P=sy_*ew{9K z(r+{@^o3QlNtIxDbSe;K#{~I8>$ydz24Hg#gkg?p-==Py>Pjn{P&-;)DX=xjs%yw8 zg;FxQ7h#}E+`S<=oNxQ>Zo;F7df5}Y5(Oc$SLQ;^%A`wlLH#1d*?v?>-vg%8>$15IVT zFIpPX<*po{#K;(HlG6V);`5PXXMN2wD{M>kV5IgJ~3 z^)B;u+#WxjKTxX5tKGjXk^W-)(;yLcy%@d3!A#I+0(Y;#RNaBp^>t>v1PC z+&xdu)8>DfgES|$!CEhG><1unq!+9V0J(5okdY7M0Gj_)o)g9I6~^Yven%wK4~tBk zVQ(yPvr8wTCluIoE-SAtDrgKkePo?F@D~61y8@odL1bZJK}lI##KnivxVgmjo;mK& zTvYpw3ari`r%{e!PLCiW*^yxKH&mzBt);+xX82QcKdro+qXZvcjX=6UD==ot9oZnw zQ~9OWPXf2t{GoD0<7Z=w#d+bss9Mf^+t;-CLsz>EQJm~-(wSXCW9?*V%9mCz4^}>k zULk>OAKo%Ikh9J^s@s-zBZr33i(V*5!81a2qYOtFvU()Emb|7M*{J}sH~i(kQQfHE zDI(#JO5g!OnmLt(R$*wCExrpL6XF#9bWWm^#ksV-F8eUUJIXL!X;V<9BvsJi)7n+$ z?1s&`uu3(eDVTDe`g>U{HjD2Ir(>lAK?Mf^qpRt|Qk19@e$y&znBLq-%pFO4L1buL z4Zx^mFXkMFBb2=J?MC;Uyur%eIlLhsf0(6X zkbj^}a8*e^l>cYn6Kd1%G)epe46$GRr}syuaKc(U+$lP+vI>`U4^aC6Cp$IxVRX>2NNX-4G- zp0jbA0{=EzdftwWq|AjN6(JUD;r38#2mr450FwDVh!#ss3oQV0rs+PJCtcs#$vq{m zpspAGdYg5Wqfn9vIg5HuB-L?u2mk&iBE8Lm^mC@6jlQV%7$T5i@b__=(!vGe+%euC z^6Fu%Fu!cGcqt%AH&i5pTbT%pk~UTCtEE!`I{o2KOL zR8n>94Q8(xaaV~}UwSK?_O^*e<|T?sqhOmJs5qU?#TCk;WnHKSv#V-mL&itha)yrZ zIU!eJG8Lph%t8%qD6^v}L9V-O-G6stw!Dc2OxEHY-QIzGR(834r$7`p=4p~aRju5>Vq03zQogZO6b7h(_byvtZhI*1} zUx?O!XIyR<`vR=pkFDZZGxIv(#SLL0gX}_L4Y8%ku$+aqVCM>p*NcvRie?_vcue12 zJ;obaF2jm1g3@q-xCWDY03nN~TcQkuv861Go_}hO$=)XR1$bhzijSP$L(&E~o?N{g zEej?EZ;;Ks7nOj4yTR0H>6$pD6wEPIF~y&z8K9yRj=*lTAtraI?11D@ox3-p|LdVY1whcm z21c0%BS2mn&E1?(LD37A`8Oj?g)J%nP|mi%Z}T&>ef;ds_d`IMyTq?*wYanLKq)O! z6(`iI?pH)#+@^hT-o#Zt@E`hdj5)!Ajm4pPrAG>4Ui6`2ri5vYARc>w_yjth97|Wy=xj@nCL!sd_mnde3fuTNh|P zuizt=jiW=O;FxAo47^r>)xGC)9eS1~2vtxi_(d~sHc#WPqx_x6&9HOyA#>_>@@5M^ zk-+EpHH5%9@(y;LulfQVR|yQw*cb^pdthgRPKw*~tkuGK&7_J}oUErOA1{=-Ts0xw zWT((pQGR_`)V*B2^X)N_p+tEK`;3;9hzR(g#2^OdzUmxbUmH-BM5Cl*WRgCwtW2x+ z^_2*kZ~a@mAnQyoaEcSGuuHI#Ajjo*t*E*(7q5glMykf_8h2EYPTyf0TGMS(NeEwv zredq9SAFPQK$h;d1O_&Cf~L}V{kea6HPYijK$&S#M`Cm#KGS1L83SFB?TvrnqOuHp z!|2p#p`{Jv;wT@XSfXC}j~io{`GKH~QxFWOq0AN!C|PV_EH~EoXD6v`mgtZRiDx~h zu!1JF@|*#WFSJdAZZ#Xffaa~ACz*}!>xB^{hCyD$X>Mj#l$f3!T4V_qEHnA=Nrzb6 z;#==9UZkowu6S(iaG*3Lr542zX^pD|@uN_qj%7x&*M1|b6Yg;NNjvM<(#|O~R4LnW zc5b0RonXQ8OidF*SxZgsrxU7&#P3Q6!;MTLxWl*gk&l_7N;w&u_0d1GX43=9Bk2Y?ei`&~4@uB*FdXt@@^^f0^?q2vXaEPh=@C#V`3GvC1MPAU!I+G_$ z446=>X6A?UbNs^}lUGfv;Ls)J-Z3E$bA(Xei4TJ3;RdU|32C7gCMiXI!qJN*{qWfuX5KLFjC-{pE@wZef!b z1R|IX)jH(@cT9mO*lH-(v_pWTh@mbddE-0aEdO2aW)qpIiN=_!T9z#l1qUHfZ4p#PA9flwJhfX>Pu+^qk>}cwhB((4rz$lU z;=x5!{RB8R;eFQ9sI6Wh9nZsAE49iP+0p4qs+QWM^aoX@7AmXL90^FhT>DvCLeX*+ zmRW5nH#e-{gWr8Pm(2Hb9j@R0#oHwS&+Tqy$IHsA3MyL@({FqwvAM2V0@8ceomWY7 z3oV?=!m}pq;AH>Olv^909s75u#JYTX2J&bU_5tT;C#H+FHU{HGqh*|A48?F)wY4#l zf3EPUf;JsL^z_6P+?dRpF3t_--}#$4D(nQ8OuoBtG7L28shN}0*18~V11ddns( z?xa9aQ;(&N9P0HApH6h9*EXmo{jQ<@=@OIo@vYf0yql!RhH3DaoqZ1~T#z#EFH16P3gewE{CD%M(a+ zFQnN`<^esxQzWEnwX$mDxFw9l>|`+yedJ+onAh*YUyTN1^eCH0d;VP}#># z4znpHxh)lptQ}gAr08DmX=~@-Oiz%q*rf%01zI4h1x`jbn zUT@U;UafxKRQef6&T{wZWA3~<5dy@I?u{?kT< zJ@r!S5cK$Eq7QfRUg?;j*z@;lsU=P|e@nmns)p1!Dj5#*6z0~gMdMS2DVWn&m^zgW zn9XpU!}En>I7n!7pr0v}s|#4h-oZY56T&-ymDu%Fd#V)jCd6>|(e(B6@y;piU`_q4 z4n$M=mwdQ;cCMd!IqfmHndP_h>bi^^ElD0Vd*lLg3~g_}dqSy*{d9c9f_16?r=CRL zi1{GYTb;r&>99S06X+pTh_$q4X?0oC0CQCT>;2Nx>nnpiY4c$(C-=?vC7;*j>uUbv zeR~lbxDk2Nrg%_V2X=X&LYqauZJ5_sk*(SWefhri@YT=_>*S}CDMM=bw7d~m7tC3p z=F?2X@l<_M(Q)%KvHaoTvrILpuV*Z8+eS6nk2YON<>m%R$Gwo`YY!s$L0-zIGLU%X zH^AM11eIIJw+Z5CGhT`q^YE|ip{Mkpro$avGNcEi9S*J6c|NAq_|{6yao1p`>HH~g z`ci;!v9JhA@65{o=Ek#cW20iia1tx(;@EG@9&|8lC(gG_7_EziZqWA~04!b~60-a6AfX8}+&uI+vcRhOAx5jLf77Z?8A=R;^IF zX0HZZzkMQiHo%saD@WyC&@+h9^XNDXO|gM2lW5-`wy)P1l4w`EL5~?!$_lE3 zN%u1lK1aagcDC`cRQMwtWH|6?(D=PY{B9eYvB&e16K|pAu_eE2T(n>Cll0E%=>hul zrfq%lYgJ(R+7O3PXDXF-!Q)SkCUnDiT7anGlh^0{Gk=5*70xM^s+yX#a&S=7qK9|XWPG(AWL^@diJgO_swU#& z;jW&yw|<7=P1z&=|J>zjh`ayzKd#M}D!Shv&r*B6@0TlmwydfeTVD51562SU?VmLC z^oJ^Gvg9N7SRIAlQVYE781j62bd~z~Fo3|ij{g5UMiJ{U-d85$2_|M$43pqGN32Yk z^$9sSR(5uI<$B_jt((eo!@Tyd58yasXYLo)U7Tpjvc|~whJz-3GJJS621Kz|FuZ;O zR%b=yy<@rmUB7xzujSm_MuR^h#k97!Z)VVgMB4=X9$UgD)?EzW!(yh^*FA+z92_Eh z&#v5Gxg*ApwtEIQ1~Ay!T^ORaZ_ZEVr`KL2hY@7S^${GVS!aBrN!WrtuY$0Bdb~gH z4dnilg^~E5>)f`HO|~|UrR(K&PN@UYl@O{62m&Ap+2 z&I*7lGX2CD-1?p`60BEdY$_dDEDMVTB=|MRqQ^GfhJyy7JcQ$k{_W5;vc*S!wCG<^+l(2|tK3n+XbB0-{nyvmR8f^;@oF_eHRUpZ&qr@{YkRjy zG3vXMOX2ge&L~J{9u5SV|G$4ozh9R(qhe@r2*m~7CF;J4#@eBQfq)wrQOqW zH2O|NYB_izHs>5+x>@UrY-wbc_Gj0L&xZXP zfw_QgG*{(>BeY}u{tk@QL}kCNQG^!^c4l z4+Ouiin%P;2V+9kpdGKVI8#IVcj%7KDKA#`ob}NL>p85c{8h0h7}IK>^){K5drmT? z*tfl$xl@ZVr1|(6QF`g1s;)=hX?stKp*ehhnUe=zvWAzw(!DQ?*944P3W0X3ejXTo zG-AakKftJp-@ zd61vgeHJ|`yx8~(SRxvMAE7NxJ>ePDO zB^u6-o*XjdfVf}4Oh;C%;)dp)px9{Uxg70N)dFqd)xv6)%s*{{7L*brQ;#P!ii0vo z!L{tx_{_UzfZ&Zv6}PD;Y56h>y){`&TbPdohSTBY@<{|L8nKB>XYAc_dxX>hm0AA& zIVHizoP0Tlap|@y?pey>`udoQZ0%czkBh6T__HeN`>QL|wA+4Iy{&H2*CkC=G0G`V z<{Rd?TTLgv$Pu-H^O69=%)u$O&x2fIRep-Sm>f1@5JWj=j;m+bk8le1?#A>)rC+R3 zykiGX4eFn_CV0=A-`9_aXXw2Ajcw@M1t#AqId!3*l-chYm7J+YmzrT&u~rT=aLxmB zQ>m{hSaa`$9+syQx9JHzVJ4AtNf2gU+yVo>esELG+F%GtNi#i8y?#90@GC2PEIwn@ zT+ny54+wh^Uj-WzK#RXI7*3z*VQpOr37mT+k!?WiDi9J)>4<>bWSCN+ss|({hw^+j}i;dFzGX+ zMh+yWD;jyI?)SrK<`DwDQr4D0T5IIFM+KzjLu&^cQwz6@0xiqkl+_+7qns)Ao2h?s zZIDoO#R(5}TnTU#9O{^)+3Qe7fP(10R|AW;fp2f_KzxG1uyg;}S9c*E z=rd;&k9PEA^~rcR4KH0<+7U{b{hz-iaqXMTpF7&nQ|Wg_i%mT8;=el*clz`l=WPAx zSDXM?Ua$MTP$P)ZE5mwPMv^ofy`N+Pq>lir`sd^fGuX)S!pRrxG}BKg+-ixbxZxxH zi3x|w9C)1T>|Fz|FC=I$%3Ev!&^%grWMk9lS36q3!~`52AyFSHa%LtRKsnFKG~aZd6U_X(@HkOiJXD_5wE)Bt|QDjHuZ4ZaNFIQ9Cvd%me| zaZSF2M@3BNML47XjHN9p$dA70A}?x!8lV>6C3mLI|1mK-Dzpkt^?nk|x4(GLKDYJn zd=^nw1y)Wauj5Ql$eAQQJs$HV)=BZ6qc;Zv%DcS=&noKO;jB; zd{76-hnU0!c779(@D(2ziq}{hTYR9kFCKjk6MzkyGW*=YTUd70v>>~^B@rn9w)%?K z)3}j2cDA#DP<%YzIRep-0e3ux6Oo^`Vk`S6sp_V%q^T8n;t|makE2ulcs}Su!A2xA985jXFbSjE;*p>N z@!!Qj2;l!9y2?QTVIEE{wiRiL>aY9P9(PM_&q386N=s`0J^?HHr z*doWY#c&0*L|d(F;sNb1-fizYOy-FXZGja^dWzuPcuX5$U;5@&0UtT~&t(pBpv;ie zdadUT)V7chl>qD#U2D+h1E%wwYPyy-32k#g+b8O4#~V%xMdG?pFUWl&`9d{oq)5Ez ziA6i-$K(u~%m%-0q>@ADY%2i1 zT%{dsm`qW7cXZPS``R63U1XJHp>hH?FhbE4YihfPw4)o}$(l`w_ksaQqws0M-((c0 zzv@LR<86+g4dhuzSU^DA#Vc~0sXf0zJ2<$5NOehi@5X6t>L5G*OSpvU3YOklN(WU+ zO@@maPl$CfCAhc=o@?wl!jnv`(E%D z=dz-2Wg_P>91~d8JuF-mm6BoC?e!jY?+hEwi;DA%&BDDF6a7aJIE6|#FUA7m#pkzx ze_Z(%H5NzC{nD)Lx?4>^G^ze8WuVGTt%M);XGYf-FaA4RzM^kS8f^+nE#e? z`^bB%e_wkYQB>1`D&@njGt6DC=-S7{lfCuH6~JhCEtH3b2`IComHQ{R4JI`WRv%e` zo1+Chw1_pLCnRIDJ+ZIpo1I9#>_*!1`(Qlug>`LS*s(bo1X0{+r)p;JWQu!w2$^;D zAzV6+C!T-@044-R2bIes$^KK^IVfq)2?z_ z&*}df-6D4~a>JSVNM{J(nb^c2oSci7{7BL%+MA#yKj!ftTZM=ORDP zD0gn#j5X-%78a_GiqOYXHP;Jr9K5r>pHiXc8EzT$Y#ukq^bwNH&r<)hPk~Sct@%nY zK@2Z(*GZ5+iPQc;Bz50kNndws|C$;Y#~k1O@fNCqB8$Y-T(P`RM&@lC)a(QWV_&RZ zgU3$y_iwuoq}wMu_(Za;ek{F0zFNnfy;JOA8}EAcs)cndjVNw*TY`QA^RdX!9ycGd z0%E|?NqQ2}0epRZ_;>LwcTacVfsL3-U0Mc^AmWUlXYF`ssTbZ*mz_WW6666!O--Hp z-A{9?o0W28ITOJA`zA7?G>;{=RSC7mVNc+LzbDYc#dl(B3dzOPXJI)R{n92f+l9?G z(nm|noxOu4Dlicy5FTGm;?>0dEDG=gXHihL47t>a^~TKUB5n+Wn&?a$l>jT`Z!*#0 zl+ywwbfS`=zf5s~xS8y)2L`(2fH?|o8_8uFMdZG|u^9hCu07W3@b=(Xr0^-*n^Cv^N#%OYy~E z;M8={Thmuror=V1ZHLL3B42^uyM68b4W)T}bGSMeol1(kv;2=mnbhgryb^;5DVq+X zj5HlB6dc@94p_^#Vg^brjm~lg1}C{hrkeN727tuLiuTpW+nt=GEDQw9UgYTL-cwe0%9gEX zpNHzIP!|8JrrN-0++h&3^`?w>fBhO(e|YFLOr{TG=E9V8yKXKp&+(hmmgnjE!F z!}cbYUp6(yn;JSqOP`Go3i)RIcnfuTVj_bsQ6xQt0@Nq0s{@RvxIiCXmRC+T8C?>L zZ8e4P7dZ(HXb9G7s0frmg=_;N4`?OPrdi z{n=&5YecyI;mrt5tf|IG)!G)h9i4KZQ5QD$wAED7u$mbK8UbEZ;Hs^wo)`{c*}p!{ zB-Mk^-&E9n%Mk+=cxW3Jw37>RAz_~}^j+G@>EP0#t*&_$8ZWvqr5yO&+>N_Card<| z)86G!mmul^G6JvH{pR;k=G=2nd@1+sgHRq&YF7yS@o#Aj?4+HUiEGkLI4uQlueTq) zJF%b#*?`ZzbQt46M-|}e$bLNs-sd&fGvYjh1B<^D2kC&$_=6~F`TEgIRikEl6!F)< z*j7wl5nRj^lC`~+g{O8%qd>w_`yaaz$zN>0OpMR6UhRa>A?CG`s#rJlBps}|CvU*b zIHDnx&h2HjE(r4SI2I#MKeeIPGSjrRN=mt+ueSMF+bMgfSqVh?FJ`m2fxK;UMd^v>%%IAZHW*iuzfj;%^3YLhn|F}q7DEuQD)kk2TkR|S>N35F*({*`cvpfR} z4)L-&riYc1ptSdE4Ypa+9Ca#tbai#c-i#qbgoBJLF18ek`MrS`y~ml;3I`qT;8i!@Zx!OFB89b$a--&mLjqOdrOm zl?EC;7Y*Zko=|%K^jcPXoxI2b%|5@l?@F88_ui3#5N*whrlMQC5h56qFlQhKAMG3FCtAXY=nWSvX@a>pfdqe?=w4?K)` z|AAisx32WJdS!cEEFVZNb85nH6I)|q?i3?Vd>0EbpSyob1SVqSS-b>WpqfM>iAXU0 zbB43Y?6LbF0mHzUV3sUm*&^Kn9w*@{QD;k(sjtM4RchKk8LLOH#y2xS!CYBd3dP+P zN!V$DQdZxbwt+Uu<~&--ECET6OS&9^jsJBidcoP-QST)jkM`(fbu)GK4SuO} zV{?hXwGJPi;=|?r)j1p60$@|sKUvXwWohaZH8nt?s?Lu>Of57$jXKwZsvz&IL>Mh| zr0CsDxxH0!-Nv71ZTolHhk|?a5@yTp?szyAL%^}D+p66~4zS7|PZUH_WCC%zL0bi| z|77I-SU~I#e&v_zBmQ36_xJO=`HtzQ>5hh2bH%GM;)t6a*4;ahjmfFd7b-fuZKWLI z%Wj*}BkSQj%o(hZF#XpXRLQM-GK(UA6eA%lBK*Ihg`|J`7W@Bt)u0q}x=Wny!44Lw zc9GmxI$m}!Oo9^~QA>w)zoqpIk1}Tu2BQifAfP~Pyw0>xv^bqozvZYfceU7CtV~ zO0ue10$olGO_Ut@$!$wA1#@Gq;BIeob&c{;lZdbiuUg;CSa|26s=C68RfSBwivAG$ zy>TMCgn}_VnO(mLpRsj1jlMh*2n~g_zPuVcT8Wr!vB29)bbj2d+Mknb*#CAyo(6(o z07+r!@9qxg(pLV=eA~jqJVjwVUi=wLj{fL#T^D7S8%81^ec5tc-_Orup%=8S9XPcO zCowM%w5u%~AR$Un%U?$5l>CVrM`c{9zKspo_)KyO4OY47`yH!;2s!X91)$Y_@ACp& zdG)k=iK(=bKQc37B@OP@p-nk>*RbPAg>~t`Fs3mK`$+J6Hzt^lir{thm05Qcj;@ca zw|MpY^JH!~{n_EKLTp`pRQO?=Z%Xsod!f5tUZP^1vp%1kRFAYtMX#uai|{`*JF#QJ zCkZMM_DTvvJ934&+Na&`hx}g}ym)aKfj9HmAXMGn!EQjPBiFoYS1Z5PF5PYa)>24W zJ1Un3bZvcI|LQ!9ZXvO|HJU;x1r#~DDnx)--JIW`E{t$HG@p}qX>4kbaLE_a?MFv30#?tf;aDu9*-4oRD(@LCqY|n2&-%P$f zp$vtZ3);96r3`(dyE=poVz{){hw9oI#48&x?E}Y?LOqO3uOeD9(We}E?Hn8u_8bDE z1NnujQ@)7#0pOTY8D&1+!YzwumzR84QKLl zposUrSCf;$S^W3`MlKm;w9)GHwaexWS&7kd2n7bGRLYrUV|Xi$YR}+^+1kR7IJXQr~g$Ovt5g$2)8)U@q{%?Bx;g*u>+UWSL19X z8D-O#S1+Qbj7!Ln>X9|J1~JwkJ6y z&)VbjYu#rjC8-r;p_zq0dSZfvSxd~5S8}33GTRct5gSpiDs%N#m#4kTvCYBf_ZTW{ z&a}+TR9$5eI_i|!RJd_g)qnH#Z6%vCXr`wrSb3Ar&qdW9E=D>{n~uvFM#zMiQi76P z7ozdZv^9p7B^`9=oPIhk{U+6ZSWv4tsmtE-*HMq|E{rhEk!^k8i2+4W=W&}lX*R9s zB;m9K}#4Jn=EdX3s&x%s&u4e zW=^fFWS~qpv!Gv>*I2`+x4UxnbYtaf5SLd*1F-~2nV8~Xr~wUQSI~T7*p#c~6vY<# zD#`Jgxw2BR!6jp8325jeBbrm@@yD|O)VFJrvyW-NgSCIlpH+=##amD8)Y(BnYGW0d zc2~I!y4GbhD%J7gG0u|-5t3$RYqsO|rc?Ax`Hclcf%%Ct>W~^{2!x1cC6ozCITEU+ zus`cnbXCM7>isMfV097)_}MnAIE#uY60*`|Gq(aH)`KdVF~K5#M(S+(Q#>f;dU?=mVX)ZNS7(Kje4r|0Y47F1XSgrd&QtQu-^ z?JBKg>4a}T?0q?GQ)_^FX0U7@Pl@MeX4=iffebx@p>|)Ft+`Pj<3EmENXW-Kp9DUt zi^W9~%-@=8))iCu+Fbc?6t74EKDZj5MyxZuYU3$Kn`;>Ob4+)pA8%w6`nk5aq1T%b z8G}1zXeEZ?eYXGfJmqTcx7<1OxcNQW*E6if;9nb1jDBtqGOzl1eOXfceEfZtOKb|+ z67c?$es1MZ`V=6)F649e+J#@A(D+t*eX}Nc{YTJl)g!I|ns5k-^JaE6?cmR)`enzm z@6nlXY}eakX(b?R;|m9@BGPoF(HbTcBJg^%JtGbw`9sTG==DQT+R(${r{YwD>j75| zsIYe2|EclXmHRmPc=WUdFwo}S;kPy1V^M&C?A*Kc+ID*Lq8NXyTiNQ`dWd=An&IDf zXM7_VLz-4Z0R=56Rq=g(=CrXl%QsWDbh$3ny2((ApYM$b`c-9gXDr&3pAD?(_X|m> z%4#|>OJ<^n{A99Pm1x4m6zyy4vze9ex>}P|88-Bz%nbSJrauziA`%CBsI}`0tgZ9P z^1yzZ;$!^;iZyXBDXFgqNF@9xPJUBnk?6&Ev4StTW~}{^dCX1vNbQPEG5R`q+M9o( zCnP@Qf8|v8&)~#+F+ew8^*@LI(*lhaL+-x~P#a1WLk?A$1?vB_G2}q!1%2axKKxAt z)cODJ{qGR|->3f155H8ymyrBt4*quFT# zFdGD+q1^^7WXV*%pP7vYl@!8g)Gt29q%oigdr>VM z6mtM@UYa>lEfO_1C7Izl4+i&(2IEx=oxDk0ZQA2smYWvTmDT5^&8?|UkND(I zsI@(580A2!+`D2wsmSrFE-ml7IpY&cC4s_(|Ggyt=lTR!S4gKY>5muIN*f5Yg>R=^ z6s0tQqv^;~>*yo<$b~rYads7}EGv(E?(}ou%7dcj`BI*>?S8?s#%+h^cu7eEdZ0GM zYQ&(;;E+wp-?otD{CF$yhPtN>4P5+DySxU~z}1NC;?kIgUhGj?n;|nca!8VOA>9d! z`*sJ8F(~e;#hb#dRaqnO@B*vwhjRxq30d5UkpO8sk zZWeH7P0rBw7N@&QAE1nGolL@o4k0G%x2vAep_#xzJCS9(`D|fv=V9q7zq%b-%{c{u`P>5c-WB^S`qXH1*Ga3eXB#@?3td`ZIX;^yf1gn!?B@Yki}cMEN*!$;gpA-~>!TW(rS zb7$95WFevMtX6ahU0J9WCIZW8BHu zrAgdF^|;8`XDI3XxI+5=-qJ94hoSvK`wi;cVj0PAN?4KN^cB1$8YJ!!Ck~p~qLS$u zFqRP4Du=d5pDsy_CqLUgVgyA*+&%2}B&~$l{M8lrDIGVCD)%SK>GfLC|3FE*dxxA& zi}DHt0}L zR!JWpz2mlS7*QMC*aZek_f->`ZO3$-uQg@wQOE3@os6arhu=&8PrLvHhmm1Dmf*{= z?}rn){k@4tJgv6u4H_3D4J_!n^87T8%gRJ9nO)S?zbranGz-;S0cGFs*Lx$2c)Yuw zX6)+c{ze+bG6rFA7gSC6`z12h)i~C=h1XxIV|1ic9}e_O7kgjk7qy4^2TFy-MC_e^ zLh4;_j|N)?cM0z9nm}+1?(Xhx!CeO%G`QOUgF|q4cXxMZHqZ0^-}mfYb#~SHaQfraRCiC; z^u5-#?%%am3TrajmhI{8MjqN#cOcJ;1Q2_ikY845S_7*u`BrUGYYG11GOt>?u!S@r zQ!rLS_MHe4OimB=Uhux5Vfoh3_{J?obNCjtW5Ik_%5&Hd92qZKOvm;)JZk5$UWERR zU)n$wFSPCDmz%Twc9$-r9#Yr#0CF=DFKHDly1ihs8C)w5q!h`cfTpnu8 z>xyF2fEk1jAXTYz?d27&B5ohj9oT@p`7jEYz zD%XpN{TssmiBE!C0W{{y2IZ>`3zG-#wvh4A}WT##a07nr0;d zKQQdyJzkMB#<%gI#!T}0u{K7Qmr+8qphk%N+wj&OG{#7@7>+0btAy7z4mJ*h3Hb+SKl0i_pY3e{MatDXQf5wMh_nUdl|3m`%V5gLjf@nxn`}Qd z==K0Et_0N>*Ee1+yIZBKKG2_B>W*(eD5L3#U%$MGa=M=vwJ!E>sU)FF{q7(Z)bbuY zOdRmQ8tL@j=G?B3Yw4yW#P~x*r^D`ar2VSkeHx<`-IJK z9cu(`{ti81Sz@GKe@r{N+d4_Y!y(*f|19C{2U*Q=Ri)|xV+3t*@ zz^DF|V!t$=@;6WX^73NW#e^$)j~z=^^Sgu?df$?_-02q*&MP7!{5Bq()=EKLhY|ZCKI#K=@J$O`W{B=OR zGW*6vEshn=$`nMifrtH)+kt)j4AznSfh4jp#!PJkl~K33 zYx|gL5j+@6*3!Uib0?dDBcrsM2mm%oM>#F=` zU^$&Ziul*e8`jwZkn=0nSunQIBnHUwD20F6T!%z5n_=&B|G+_`gloR%b;D7 z1aeznk{QiF8mV_ohS;qC|CTsDvee@778jRxOI3kQIid+y5BSmsC8S8ns-Oc!g+X}K zj`=`J@yy@fllq^sFT5#WZE# ztrFtBoY`k*48du|1EcSJigqNHnLVyMn+U+I)MC|jYL7MBDYW$?z@{7A#%kjdxNWfJk*HtyJS}6 znpo8p*R(`f^zcyTaGlWA6V+5?ZF=+=BEXnlm=d;150Tp?uvDQCZF+adm?V^0TUl9A zQgHywGqfFRuMZC9rj>Pse~5l!h~_YV7sOW55|T3CI`Ti1kx%O?iMDipOz^jK5m0@O z^P;9>%x%pDh71|=H#{hrB}}UA+v~6Lc1^QN4$~A4TZ@8dpZCn-;u7Ut09b=lvL&ow zB3%5%8A%n{1+INuior@-uGd%SQ(6z3{NL06#~Ogi z;w2(06l=n41i9o*JOzB2iZJ+`_8SioBG}Qqp0`{rS#{ZC=U!L-k$zM>lr{?NM{9i@UyK?{kBANbkZiGVW)b~^4MT^yG zR@e}u{`V1|J%4cP`A^(zx9CiK|ADi#r2lC!86^{H z-ZI3K9~;o#RpK`>(c~^e=e3I?~)$h zaWSU<>(_^D0_uIcyW)pq|1CXRJ>DIk=f&&jnCHGjzf=Dzh}zJ7he~o_K;rJ1H6_6) zHq15QKR4Z(I7-=^MNnjWsh8{eEL1H1flZS^#rnu=v6izaEhx4lpl_s=CF^YN}?(fnco5x=F z6^gZPU!<{EMeH1}Rqz%sS%xla*xm3>&wt81Am*}04u(f=J=V^%gYi%MF?&U+?{lK+ zbJFISgpn@|Gi*l`hLR&1zQI7{E+Iu-F4Qor6pQ^`|IOhI3LNyQ=n`t~32LOtuz&zF7 znNyE3kuWjEW@PZwmZryh6iMnx$hLgoE6du-V9(7%|26A2(Lb{)C8bU8aEV)YNdIE> zUd{{mcud$CQIuhIb=iIVDJBZg(T;`@S5@}FWw&DZ9B^?;TReY>pAN9@(0)tO!}SA6 z51u({z1q)kg;`{MH%xQ0n^5&4PHxrqrOS}j#J4~pap2-I} zpO{4f@qdP5rZ=YZCEY(E#*1;KAeN>w%1Z`xqn<)Nk7bZ!R*G&)w(ysTyrxhj+PP#X>h zrz7@!>_d(Dq$0RnXDkDgs3)b)z?#Vba%4iU)es&{w^w7>4R6^zf|OsmK~aW1YP$V& zTb{}h=rg|s1#FG0Eq`!CJ?5%1*c+kHZcO*5AHF%L$LL)(?Ck38Np>Oo+|^K4t=nHk z2Xh_ySMkkRjmGb#Q2$QsQQI=sMyJCjX87I)BCxLLJKqqrzHGqP|Gj^)^6{B(_#w}~ zipyTmTRY~SZR1X8U1taz4&gx@;;g{z75-1KGj;26nK^?1Xy?xt6$P%{CPH*NQ!#C{ zjsU_FR6^nccW}x)5I5ec$4i!YB-*)HCxfYEHumFc3r6>#>xuEpj_ZV-Fmxm${$Oki zo}B_pKvq%LXZ@phOKa5Lm@QN@Us}@nt}|{Fduds5ad9Ci|98&=xm4M590eKfcqNO; z0|{rw=R9yu)|Fxs$QthN#H^Tx0X!iu+qpApqb*~0%Qyvn6_Jh=OpB^)zsCrMuFBDe zM60B@#MJMux-eL{e}$Fm9oMjsW&bfo@BdQPl`I;Y@#>rNl#(uG`5>S(YFtDG$jFdY z3Ppv|31zJ;)nq4>HQvRXsH~kTYN7P!a4MoU>4z^0*Yn-=i{iv=)qlfLYsX>D;NZ)? zp}QO<&=kbl4=)QFmZ8uewayS9-zup1Geo|E*yS=nB8bbI6grv2Z8;R9A9Gzqzy6pg z*bEk9omPZz@d*hZ3ruaf!x?A>@=V1J$=}D~LpoY8& zhQtDYxZ5(sx{ho)%@-J}99GAdE;6qLmt^e^N=E8OOz+b#_z)xhBJ_%n7irhu?`1wJ zRZJCHk)(MQNv#_JL@I=^Q(+ke`06s zo&TxU5VsO#rK6-f-*J>jxXuG^2_~9&F-{atE1|gnE{ASYyy({Pw{VIQUC#qk29@`2 z?;iCjo48rbZ%B2q!BgU#ZuT7dEFxkul)?p{ngD-HjBxkBAuFB&sy=)?hmz!!1;18{ zKm6g@h2385`)dCUIj=ArO@+g=7HN*ia>k_ClCL_|S-F(f+|nY4n<{fBik}_!Q#}O_ zIXPvX>ey^ig{r8d;YVDJf&&@v;F1==N*%@)8BvYn%hwgN=dY-@5zAb*0KK9Qe+_4T z=_*Spc~RA4(pnxJ(+m!iuxV}l!y~!N!e)^c@fUjwT2e6HTCgIhb(nQy)P;Wk|Y*uTri8R)y z4E4uC8BjT5WoXG64fdFDGC0=0)AluJ(KT5yYUR=qEh;hYPkSWr$THH(zGD6iF`A#m zYiT^VrBI@ZHhst!UDtR|r4*RaA5Xrs__fxi3N2||G4c_Brdi%MSXS?z+k0Pf?BK~_ zR9PRDG06gl9tiDBR=r}B=4c8kk>)oX-;PGzo^N4ewq1NzYb9uEKE+u5e#W(LQ3ORk zdi&*2r?&l*DZ)&bTI3nrQYkl4?>au*n+sHbK8+kU7h%lx@g)-Jaza{UF}H=w@x3nF zt3Fp~*db2b))WE-91iVN=rN=+igf75R_JG$9{e)f8_OJ3SHlj8O&hDxQgEK3Vllfy zVic7BL?Up5##T5&)?S{`-ChP{wc)HSlI!cW;0Bm_CM2gs?B7Uow=`~?aQgq={xFgb zp?K>jg8GHw7Yj6^ysY~Jbh{zYYKhC~7OyqwXCV?NPr%FmK6A`%$s3g~$UT0gp=9rd zKWJV+53}waUD=X(ECoz$Ge(x)r&-=((m$Cw)b@^#GUl-7vgcsKhEvi`>wUY8#63NW zNnch^wt*+nsmLSUqGmH=(tj=OMapgtN}vlWerjwzAnsA2WBvz(rXnEOxeocC4JE+1V2DV#BF^}@I@o4>G~jcgH6O~h)4oN}p0ZNBvnZWwT{2 z&hA#7em{-dgh%9Xi zGJZrTjB@Jl_2I`C;Md^yLF&%T0Cp@zqRFDb7;tH@`87L6$2Ydf_w64rEIG4&vEJqa zC)XMsVMl4X`m4Hdrl-i5w5^LGeRp_ppo|6%zlLF3YcYpD>M@!=^~4Xktf+%HexGQP z$$A)AYq4ZclC9bk3#8q)r<5pELJUS)n;jA`Bj-@7?NAVlMmKNT<`38ltkVP{F_&8} zm+F#(5mh%1Yv9)jrG8|zr8=Qp#YZQd++5^nKQoPccLfSHG6%zhWopJ=+XK!x44Kn<}y~r#>psL1~);^ptKvc&D69m!6*nfvgvMvG4u|` zX;+RqiD7jh0T>Zroq$fnYPTkpD_#Fj){8x&x12aY^^EXHwoY1rgSC7OCny?IQ-QB< zF@ElaeW~U(%mbJUZeEQ7FH1!`Mx&3HKGIc@`i_g!1PxuefwAuxlU_T#r{qL^s1O6B z9|fwomJyRqr4q+GAa_ec9yZ5`3$c)sxU}PvQ+fe)ORf8FMy?}XXDT#{!QRbG*?u?Z zFzEkmxMIkx9zPfBK=3gGleQWk#%nB>4x9bLjwaW|1)d;^0&i*utzbmSssfX9>(Y!w zQr@i&qXF+*1?-XJu6{k5)IeC|{^u9AQWYL|hODULlhGb3A+xT}Typ$ja3s5fGzIL@ z6s3)+nfRug{0<)!7EOMxf6>3#w_TB?Ent2kVp|EReqFIj8`5JHrNPv?*M<4BaqA-X z1#c*GMU0>xi)ypMl}Cv(rr&3Wx^N`5du&_2xO7DS6^W~menD)YjQWemm|5Jl_11Mn z;{h9BJT%G-(ks4$QBdvLWxw~JTL8-ajaezO;C}K=Ue3;VAuc?l6}uA<`RqW*3tg2b zKj`BCo$^gYqtQcX(!!(exMD#PYq^RzUL3r^X~5`U!drdh?zx8FKw}jy8DX+&?UsDz z9-xwNXk`2WZ3^WjA#b5gv~b3b(-- z4a=_W%Y_w8fBl~E3gxl}s7=}_<$z-`f%_H{X*IR%8?~-r#V)rTm^0h#Ze}XALb>eG zR*!2Sn-Kz|oJi`PLkuGT7-r>#d%~XPlv8!7!4vM_G^bPmI8^Ls^-5(+e@y~yZuP_< zq9lH7#z|Q$TWs}46In@)S1QQizN0(~40YhE^}h$uc)fXBbmL8HAXlBmi+HSlJ_OO;hUGt#pntyW&+)x>aYcr)>g+C92f`?&z;Kv zsa(F?(yakW&x^ss`dpRm1EgC!mLW*#J-5{|${v^HX`5qFx4y>xD+X7^HBy};Vwgn5 zty*jz!0C@qmS85l-nv`217)gO{Z{nrxf7}mYQ%F5GTfslicK0A{*JGI?e`}$JgyKr zFwt@NTsUIM@_6`m_H9VZ zsG|$WLfK6HxoCDRb#)f=Fg&sEh+BGzVtb=f*|V3&bsbJqQL_DbqeOBZ7bFFg)shlY zQsjMo1!Z+$Pv|pOxLtog$MYoam^rT52@riyn>4=y3OT`lx^ zds?#Lw~kG(D)sguYuT7IH<$Ogp0#!v9xWenw~q(9=!U|hcm>U;)T*~XLswA7nZQd< zIoKb5JZmk5`p#;>UGFQnUF0_i9;L%&F`$&c*7_xyY20-q${#UG%Jafo+uu~pyMfEI zsm5K|>SR(t@l%Lru*V}^a!A(nur7Q=YqPs4a(|)8f{5;Z^$-0u1>$OGwe__tqb`5% zRWRa$!wC^A-9SQ6K9ky9#W%%wR^cHA!So}9;MbqT#*e<^Ef+ei2qy~de!qOZygslI z*BK=L0XHT`GSL>Ff(=2b8lH7=dKdIvkLEB&@~T^C%3XFId)v6H;q-2`tc`K&zaqBd zL}-^?8$=3TYQ+5&DG{wZoSO;Dj8DQ5LWtd!K1XU?7h~3{?5&N`zOBjAWPdSw-C((I zSr@vZ*#P|3elPd@ll9c9J=eG>M?b?8&c7ad+Cw|}P`K#5S)4Pak#&SF$%a2ey5lfK z_A6e&w@D)Hp@L_d;waa8a@^r(KW zWPxI1`&_mzhz_;Uh<1Zt0cH*FQ98V;0uMiCw<^mc9+Ks zn$GXFUayO(>CVJqGY~Wr0kBLBPo#}%7yWq94u}3jyG4%PCvW(usAt-OYm~{DieFBT3SEx`%OG&#!<1c8Hc>RUyLXy*U zkC%UL#CQw@h4fb^78l7iy^J75N(lN{4f_#%d@H zc3a&`4l)$7Qzc=?+SM4GL5IZX4b|hvHeDZ}Ho@%f1FVr94;S!QJToCPvh5Ui&{VcA{tYBEe27y9W!*=Y_FYp?yPE%GC z=-B0Cj8|An;`f*%`mily6tqR3WLzHT3G)&sTAWLjQn@+_V@0as^NHjb9gJ4`(CZ}8 zIN}dOw=<4mhPUkIPXBsuVeLvYwet)9kD1q*zL?3&7<@hr2jX z6-xVBdl2@F5tYmJ)uyO-tH5Vocj$9u=?h-_$e0-IIh!ldRkBZ~C+qE|i z_cV0^D%NWUKY(K0d2Dc(x0k(M4MOA7Rufjd%s8^@^jJ&7MjGrkxE8bwoT|v3(%4Ig zraF5{ShBhj#_eg9F=A&#Ya*e#y| z;a6IM9j?J~$$+j5jmXAK?Z`o6VJ+10G?U1t*NbnF;<8jVnnqfBNscKchz&|hj34x8 zF>9?iE#En)O+MPxj%F5rznk%0Hvy+dn{j^Mef*vIhF#EwBpuXIuGrl= zb?=$5w;`X!ar^|3d`ZjsA%n4#tQ`Ho(7_%xk{JA}>~wXlg2VAlH~6$;yftn%8Z?$X zw`Us@%rR}52*sQ(o8hC~70b2nn^yw?i!*j~=+LBiIVJ`GVc+Zi{}xt;+ZSiXUeS$x zutRPnka%ilWFAM?AVg02X!qv|IT%u-qa?gzp1DLh7$cohol%f5ghGYUMGP#Bo8~zR?3~P_7&e%rvn3e%sut%n-5!L`nxm95hnN5FGa0Z8un0S8k7F znII8z#21qm=vQVJB&G-){2nXiHl=~N-WVppbJS&-zGdUgh{@F<9IbPxq+bJPr|n_c z5-k>E2+mhw$@oyD??s#R)Ty+N&=<9}MIZCcvmIPdKEP#|_TI#g=;8i!)g@vXuL7;I zrDj|?GP=w5`@AVQt6E#ucBKkhf9?)JHoO|joN>c(ceJMo3>n#Wu%=u*toppx6et~= zFz7=x@->A=3f>0#;VD3)5>A9pevEK);BuSb%R=W&O=VPOjTB9BaK0G--pa0c@oeu& z+J09t(<@Cy$=24{ERtJxU&(X4wvbDh5tUrE*|nm!&Z>Ho2@@^{-V=K}P5e;C=&E0STWkC2mLYy=72%BRKmQrR4XuIalOw#ztN@qaVo5reJ@~D7*6X{O{yKAs0)HT5;9*C!t=yH z*B2a)*H`YhH?YHn%@@GskGTkGhfLd*#Ef#?IGi)#qQ~;NDVsGAZ$V;CfLVA_-TW?% zs2>4(ihIK46Q|PJOMnMW`cxPcEK1hj*=p?>Q3d)e_Mk=l^Snd4)Xpk@A~^;ZqX1U3 zOtdx_r$oNcs?d!ldkm$$hlIi?md9Nk?}jtVocu#8MAHUL<2<}`i*0)c7t@}`MgF0) zpJziRgJA|ydNtWweOOmrW6Yk7YETrA#a zw*JX$`_wilMXY2*bi5a{GZSf?mX_|}yz*)Ml`GNbdZBtT#S;BH{xjH)oPWGVKP!J& zmAOH2io$5dulMii+d$*mxP!Os*BWGrKtp-R2bS0wgWe()Y8Ijz& zz-Qf;eE=4zO+WOX;=c58KC8pw)f%m}oZf@?sbSc({=ClY!$4=JRVhOt&nG)WiwaRE$XAsyADHV8#39zPv*=bBrW_ip6?@xiwa|sJ4Zus$d#7KQX3cnv&UIg*v23mKqkDapEQu)Qe?2)fLD=093@mPu zc5dU5k??tvQWY6Uj9AjGJ0ftbrZhNv!yg4vs?{kfE3-$67$XN2UE=Uv!dWUA8>w-5 zM=}|F)hz1OnbSRERoeJ-F-}-kJXJze-j3dBp$v%scv6#tgM-4>NL(kGN0yqbv@W?4 z9E32^J8NQC*mOqLl&06`VfoR~)y3rOY88Xs`7$Pam``oVx0lYA%VFG#)=$X!15E+m za|dUeVb9k-YkoPvm+xDH?e*T5gF?)PATG;j+~2CZi*iq)@0LP#xM<$!;5;FdnQ{hY ztBREs{`d}J&g_#RSH*0Y?fk^Y))38@M@EC5o_FXdwYt{Q-F_laflM3cY^!e|-WCa8 z5G^)Pt+<3~`!9Gm%HGv5C>`I{cBt*W&)dPR`jS;1?(ts!ND+XQsgj#U)aqm>I71bD zE5Z8eQep($;!l-NbaP+d_J+b6-cskWW3?p{WARA9AKNx{q-D?Eo5647YXUthQdShi zrk=e8JNmj6Mr8&(hjE!aVBxtWfY!p1!2uRK2LT3AOZFMA@TEp}Y^DqPF>UmeZ+EDa zwBt5c>G#ez5~k%}@^crz`}OS^^7p?Aj9tgH`nZ{(*`9VX3EdJj6NndONNnZp%?P)RN@ER%Crdh}UlNnc40-Q3q`cYS$tn##+dS~~ z!y3sNdC({ellALdLe?#og{pY4(G@peXG(2w1Gz41)DK%_PJ8odW1O8{zL z-R*@UspO2#gP#v}+D~-|9vrc_NQXWgSTGok7cv1GT={-{5R}PgPS;}LspH(MwH?z2 zKf`VAH)ZwGp-in*?4BKFHje9#)&RuSTO3)8wEG+$UA~pb$I_ZO(ZGEnG<==%u;#r& zimXq!>UU%HIg$zpxM#JxvW{7c>JetLazmBgXP}ZQ5cwKp*P;Iwp~wueZBPWQVfE;_ z4cOh7q;EU^rS9KhJyT{m9?J*4dJ@g+sVFU)_8A_2E0K~mLs6H(VoxKI5y0Y2*Wy8) z{{RXRaI<9jIH1Z~FMzXtqE_^+HviM$Om_|&&5OGYLhXGY+ZtW)kVE+lua!to^e7Dfa*gFvLFL~~T>ttAhckh=Z{J376^`wP^s5D6xs>t^2_A3^G z>N5NW?=!_lf13n`u>FyuDsYZ@g%{{8@s=Tvgjez9HDD5khli)2Weo58@(F~L$$;6@ z+Yd;~5;p$H9bYrZ?gD(b{|pI}c6EnENRm)dMegh%vBL`Vdf?{oCmO)WG^NYWo3en!k9v(J~fW0qvF(I&5~SfimQSWE&Pj5+9x=tjs%Nu%N>xQwK8!1ef& zu+p(1D?934-Cgdu02z@YR1NSil~gIiWCS{&#N*_h3tmoml{_pc4UnQ*mh{cAeOI?$ zypCdIFKwO;H5nsICf2U@CkvHbKx&3u*;oYG;)1BHCz=LDG}O#q5n6UyWZ&n6SqTNx z1lE3opZx{PdlT_F1qnet7)MLD+^km115zAXK!f5l}=iMhhC{!&Y|Hm{&eAf`33 z+Ve&`V&Dd)u8ruB8W`jR(K=zIWXP7W5@{r}xN^p!VT+neP&0LrN{rEn+JQ;&H6`Ta zyt$adB{52mSZJ2l#KCyfQj*Gd()kKjM>P8tZamQmim4Hot$GsFDuCsH8%PcnQ#iUlwa3sgLqT zu%TI>Tl`f1h>4upS_C1zbymtilXe6n(3vU+H)-=7c4KFwY^u8pLWO|ovAB^9hfDCu zsVKf;5lPBvd{TucFAI+dKw4fmcJHy0diQD>3r5jTDl7a{HioXs1<)`uZm9eu$8@no zK#*pZfTA2iKr}yN{rIPZso+oJ7s)w=#FW^%kFUTMElFwjiWM*D%fUzUs2Dzt68fP( zm7-?EfrTXz3kMRr8m;6UMM!z?5L9$UTyYBO&!6q!wsF=!YJ1!~oFKO^5${n&&`g=# zupVbnI+9EVc%IsW#EXOlHPXyw6uv2{&OLZ2YCeh;>Xw%taKgWYj21xR4F}^S!td80 zO{1X&df@1{9;@3|YcTH6$sVn`w^2&&SZb-;Pf5&mYgh^EG~Vh-()RT+;ipPq|DBYN zhwNDX2^_!wau8^q=kODYPzL^J}}{}fA~}M z=u=kBVx@_T`gwH+dB;8h8mQY+!`ExA{Ss>6Q*_?C!_@P=LSNobSDetBB%8Uln)dVj zm~AJLdm=NJs37ZXt>@dPRkih2^>raR5oA7Wncm|O5hT0w#rW6O_wQ-XLl$`j-9jJF zsACQH8pE}t$38`^v|#kau#HbjFWmYiOPyTrxo57~fLV~Zxxo9}%+GJEHysUr9=2sc z`c{bqK0To(`4`^@%&1Meceuh#xmfp$3x3P;mXW9zJM ztz{B?y}Z8q=GU+1m+8M|^^8ODxVm0)ljQb52eGzKg$wsz=v#yUd85>l*YD}+U)MU? z{RB1Q*RORKvLw&lpCL}0BK?MZ{SES8gStZ20v@^ek^<^!0`(jQs_urX7E(U#V{_%R z>Ci+Fis1q@gpI1FjFJv*Z4LSct-H#c)hkqIM#V-{*~YUlK*uUnf6RP-abi#L#!-Yf zV+DeANSQQxZ~l5CEX$-apvm&Wms#(1^=gY{R(||z4YJ=d_y%#ac?ym7`2Dh%j8tG< zbdA#TUQ_$0sz1`;8Ie$u^^)G!A=mt-HnZ*b{kk%|j!;+9>Mwze2~a()v>5N%r&R0> z#kuPI@?8F_v1Vz+AVXOv>1D-ay($2xB9Zdra}%6S#}V;Rhk2kL=4y{&eEJH5ZY zngRTOS7?r{m)}9QH!@nwiRAq}i?l=;GoJeA9rjsgd%AIgGNl3Q>?cY0cwWhdCfD5_L+ z`!c(5bY8K~(JCx2<{tb)nq}H%HVKZ}Q3>k1t%E}@9ghosqQJn8E7^BX*uSy;xki&Y z$#}|lDey?6Lwfe&d{If@F~uBNJ&HvDjh^zqC_mpU#66~p_bT5VhHowgn7Hck@H+2) zC$vpLkm}x7H{DvRivIN2Z>M?;tAG0`*=jv&+E+W_mHU~;`jK+GUK|)n{x-y?L6chY z-&GSKP>?mNp(^&2QtIzZSp+oZGRHV%-njDrLdsuOut!{KUc&d&uaXAt)u^rl8y??E zp}i8%V`{4K&C?lP?`xP&kKx+;{>~p~XWMFjA}%72*Hcd)(DS5v=Ux7Vw)8}je`I^Te!K1z}-}`Ai z)?J_L394TGp5{>8SG+1q=;0cRe2wHbu;+TN-zlhkkJJB}{eFBMN=JSRFeOLsWE*{cwA)Y4=Ltzq?Jdrdvkyo0Bt|6{@Xa~!YkoD-&}`J5(nE%9qeh2O85 zB5Pgk%BxM9!smEvtx_#Hy`#C#=S+6}^3LZJccbw`?PB|hKsKl@HV<+uXg?C;KyXb= z`}`8_cTn*Qzp!LPXzg0G2n`%ifQXMZx^GrhA#Lg>R zEpva3PE+3cscT+44vBm;0z2t1mHrQS^ardnWjKiPlPI!hgx?pX8%~u9fmC|d_22Kb z=l?_2vP$h_Utid~yIzmq?d1RMGW@>)wfUQ!!A2P!DIb4pX!Vb7e4iRh8;EEdtvDcf zW01*o!AMD(Cc8$BtI@0&V0BF}7ELfQmQ+mk<|1cnKC?v~r4qkIQ1RT@eAU8Ub* zu?dJFzFQJwYq_wNU5rgE4t30GCiG%;#T(Ybdj3W=+t#gmlDE5n&GeIY-;UQ-0~fpc ze{?Mv!F~td{F${pWRdm`o;7|p%bPUzIcH1w>+2m`urBcKzHKoQTDX&e^R@F!Hh$)| zIq%pVeMwIoM-#G2CJH>Y8zj9XPM{x_YCq#as+LU_au=*9B$6?u6S5L`vRz)$dIS%s zQ@{gJ$nVdXJ`UWCt*TJ$9HQ@rBk^AsKfatSi(hnpzawsv5H;e{7`Llg?hj)U0h=R|oxF#Ab>xFj-exoDx5DUM7$<||HPEM4sOK(wFS#u`)%fy&(JdjDV~ ziupw9-*U3IMHuh4w#%!For`+28LLrl5FY!_HU7anhV%>NJ9u8F^eL=Q>xJMh>dLjK z&MrQRDq_ctQvv9c+wGZI`=yrA7q3z|kDkr+$r%+B)H*9JBCUq4O~0^*Rxv2vGBzIk z6SHUhcPik!@(+OlUg5JF!MwoE=g<7U{)t<(n#vB)*CzB{vl_?zaFml6WV%FE~hG2a?6#gFO2gP6-1f-=Kwsr<_i8HagkfGWp!ruG4w< zy>65%XkE|G*4p^!AF7)@o&K(iwXBF{D2iE(^ZPNC_ruj;?|r4leD`UJjs)^2yGFmX z)toXVBwZywMw9s>?qynJnWR)3RbQY45c0YDTVSWU)*{MqD8^cG4@7?8UQ7G=cIv0N z#3$nOgWIp^<Q>` zy2xGpJk)ebl2ZrDQonBr60(KyM_*bmxZJO5FLb=~mhI=iLXV%!hyCka-LG#)mFW`P zxN1+)Q64^)Wnc;`W#6_h{JH`CZg7TfS`h}$cY09LO~(mR7f`U_NqRDvrE*kAWQtgn zG3~`@QAcXBLD8);HSVL2o;Z`~L6#x1GuON$B+M!)J&-4%H+&|m=Mrz+Zom*~dzej% zL=zp&Bj(NB-PH8z=)nzb(x87>x6{J~U*H&6Gr;E@pvn>8`6tcliy~gAX)56%<-#ML zLsQ#6UH-NSv)mSxW+)X9c(89!%|?V8JE4VI?Q2kOqdLq)Rv>(%l4{lw)~6v4kP9*| zLxyr${?v08%W}n5AO05BorQ2dJEt7cbxVX|C06|!Jpk^d9m&4FKDKK(WY6C97AsQ; zOI>;}FgDPZ-L z!SH2OVvMP%s0aX>$6a|Gi{HzUWEJr-CzyBao1FW6=0<1%oFvs|;@Uo;&vkl*uJ*l? zV;D3rRzZFn)$iEV?zzE!z1$T9;R5ka3d&lD;=5&cN_w4LT$$Uz5DlL6)8W@gkEIkA zJCyDH4Jlm}q3c>B{&2Ky)QsTHJCp2N3tyo82I8sE{*~Y;U1XA;cy|ND@0qvkYl#5Q zaMWp}p!7kvyr64MRp?@-$dY2$cy!>ccX|-ZPz5f}Df&0mRT&=A4ie{=(>rkM=N`4B zU@@o;K?a#pT@P#zjLjfB`|Busde5P?FO)9$YhS+6p8_94yJ84i{?;MNv@F6D8vW&i zl37-I@cWu-LJYH)uHWtuY35+!KLcfE>p`yY${CHwwax4_@~P=_RhU|)g{^^i;J#)$V0q#$M<~^ z%=Qmb!aHa^zEYZ0vnL~UadFxKxD!msahYmE`tl6l{1u?#;7G1iCiru58pz|T-nQ{~ zHmh(Ctn)!GJIXH@fS2<4Y#tIhXY8VZ2J4ffnp79%HlOi?yRc}Ws_;uem^Z~v;+<2u z(lzZlJp6k@Ke|?lQIByJ``q*`hZ&LNxs*ZqFa6`=>3{S0i*I2|iHhPA(V|DFfCaK- zA63y`U0}$Ao;Hs#A0_pdVjA2Du)?xeVvCE;SsX%=n&)evP3I8&$Sdmjd%OAKMf7bN z(UN+I-LTb4O`WshN2nZ8W7B4`UvrEnqx0;QSV4~T)@EA7-o3ZWV>e?}@aumykL+?m zMrra3UnCH*l(?Hk?Np)LaWPXzOl2t9LJCa1S8^wdv4454<@|Ajd0)RyMFyt7#LTBP zJ*{Rp9-s~<%zfk}MA@l)v45rXaOp1pqLv#qYH}Z>XSDoH@l0~|f>gB(bNKLXC9MZa zxyloDO6D0u5P`D3HG43b|LrC)>J5k-!+@mbzEyG$T3XK{WoBGc+8?sI*eU^=&;9(4p)2BcyVT!nrG3EO5fvmoY?cy43bDOHb zS|3Q({l$)`hwL6e0tjW!oikj>B4^=;if=$c#SK1)!Wx?xttV9_uWoFYDxD`J!mO_? z6WZ#jdmlA~>D6cR@l2%;L1()SsILE)>C z#TueQ8nO@D4FsQ5R}pwnh_Bn3Q6L*6A3yFsJV+K3X!jYiu&H`+p7|%DROoNdr_MFo zzYYS2)-1lnFdT4EQCFE8c;p86I0@3xRVB{WtLPRMhdpp-8L~+76cD5#w|Zp=>+6%C z0ijD+gX2v;aRQPPs2m=`d@kc*r+F()?Fu3jZ>&j46MLwjuTK4ZBeRcq_>$$vg5nEF z6Wh`9+kN?It&J`6V-iDyamF9ZcPmV;*hnvu1}~C=rZ}5=?JbB1rFoa8$p&q3_Vm5t z#TEPqj1|d$gh!p=9*)Lf3)f#`*@?m{ha1%8^&Ea zG;Ou@8|{_$Wp@Q%QL{#*Qe#@soLgWHxo>Y%73F&nO^?ybXXX1g5{C_litW{ECy-*e zH%pfsWe9P>Y6Ug%#A^y(TG*U3Imh2}6K5)jD|H&}tSOTFDk7wdY2cTTFfxP>e5F}4 zd>{Epw_x2r^Xf==YT=%V^CO=Y$1a{&BdI&%VAj~Q!%))9)Ch!IAQKSV6MLg}zQ}bl zeRqbc2t*Rn!)-F@#+m26a6vXOIS=ytda_XSzHI)jOV`n_{N~cpCtFpN0R?v?XZxb} zzId|NR|PbptNb1{%_CpT*5ca1z%Fz;)Fid<`s9bIt5D?uDBfr`!{SW$mr#SlIAkd2 z6xu7S<&*A@qkHHm5qEZFw!@?1*Z3F1%ZHH}QRG#kLhlblcXrFw`{P+ar zH0vPoa?KO#nIHe~i=xtY4H`Naptk7mY6eul^vP1at>It$9U6!~yP8={P=)%~n08pg z`IBhP*-i`ijI$XzwkK*y(!M#auB}7PHWfS3FUpT}x7Tcjv1){v$^agJ8?Hz%`74dq zCkWMF_dfLat~6$BNt?8G4DprhiWug)&H?)OYm?MZ5VdPvBst80MTR!Hm}5k$`4WWgdg>JmU79dFoo<;4NC%o@~uO#?jC|1!l7zV0$V7b-Y1N_72ywH zAnXv!(6;yYmKmI6suz!a?%~S$7-@hS9?EA^CWeIQ@*o))4*%-{*;=LfDcuRxcR@;*S~G6@-a4 z8bO)gIHk<3^a`WyPv?K84(dO?bcWB#sYG%UM@jo%e7$3I<g zQIG+Yv$Je5GS?3g$-Hx1p~K$sUYlgchSZt&=z2pCBr)EDE|0i)mr%2uO#>p@#Id zgV%GWoTKoB_75ddPAMl~!wtsNMX%q&7vYh=&(gm7&1U1O9h|k69Y`zpU`gj4+P4gE zUmkMZ3TKFWa;kv5`=?l5K)P7M*vbrMwVKd#i!0R3FXVem2VdnGg}^nz)-0oj-u$%y zE6P>4jTY>@qY@cQTdUOay31t;#Qaw3^yBiC!pcnloJi5%Gpi0v?j5vo0*L7b3oK28 z(hejjPGo-8{wI%cd0#b}t=ZbuT-*KC+plWP9G(@O@)BC4_m+m8xcX(Jq=}f5{d9=z zNh#{%FAprxdNm7Il83O73sFd*aQXzU5mo`HZ3*zcThQC&62Py?Lw=n%HZf~<~0*1d`01OQxF|p z6j*qAq?{d5ZdNcS9uEDRWAXUV8{OSeU5+3JHl2qpaEr#8p+cQDk2#Gg?=_C7mB){0#0bYD>I zKpGYKqQB%7@yoZ04o;w?mC)uxde7U*wxsW}q+uhe|I1cjalrQEm|CxNHr0FJH8~X}qr}U$jWJgzB~=9R9HgMc0}ZqW=0379>1BuR$BrjUUtYUW9ZH@S0#_h%+?CklCj>R0$gB9EB3I5q*c zxu>$cu@zWih!3%?i%s(G9o9y&XH^wLeRq+ua@UB1J?{x;N?kbt|FWNe+|%Sd!Yv8PFP5EPA5rg@zfr{as(Tmd)QQm|P)+);o9P2R6UIBN95 z4z;LQEJ1e|?GDd-CY)a~4?B8ji<`Psew{&;Wa+<090o2QN2mz@MgyADMwIj$-1E(? ztP=Py5ayFe7ck77=X>Lp5+G6+aS#w(CS;;#RX*@2%U&1aWid6ms1}Ovc;OU}{oI z$$SjfN4BBSA@!C++oJi=l^8w|i}hl~5(E9i$UeUJhh>iLooB<>sR{b^Jtc-_hNu@y zID4dBXaW&CrRHe#O^opn@8=CZZ}V7$_!MF&6LSDn3xCV-$oO-o-IOhrX#4M1?sZ#% zl|4b=<$|;J;=edF5mLW9IvyNEi9dmtIG6wo=3{rRYK)BTpR&7o@&U)xy#+fV5Ry;8 z2Yw!oIBKaoRG+1J3G8W}H0GMv8!CaIx;TOHJse_0ElU~rgNyP%K3i-P4|7bChK3?y zAv;d@l!dgV$G&)yHhzpkg4k>$c2XYtp9rcNY@WaZosZs#@DT|W80OqDDmF2J*?4M7 zny$2@_b(f|2Xy#3GwPWVD^Xs(@>Hu zdu0X}UyaN$D@qZbL);?MxN9|*tvs?)q#QiJzo1~+z5bfjNR0OX5jgX35aA_ca|i8bz}NZzaoPoyojO}avceLmQYsKJdb3^(chde^h$9;^JBo;epx|YV zt{gIpXEpGJ!;@2SL^R%+=&8HTYXU>>HpKph+?-Q(mSZ1zxCbTwRzT5bXJMWDX%i0W zgkgeWOhRRaKQu-dcJF|E<-P1V&oc_0uZ=r!KP3x8@n|8Z`WNpb+BYd>@WCe=@7>al zp-&d2{{xT#ctmVH%lYX#YHWg>0aoRjN&@3Ib~(hw0!0X!ijYw~LCSW3KTH{xY&wEG z3i}-57|etyUO)E2yjq3TL<=5On{;zS-wRTk>kWSb)nq^B;o7}%6v@q5dcyv!DM06P zUha^{8ow&YtA%h9W0#!cz}=-CDL*wQgg6N5JycyofPXnWz#J1W*vKNM&jU_tfFijr_sWop!&*7LyL@QP~H**Z4a)mk@`Of zVy2B8)%XekOxSq=a0}jH8r8#%X+w_Nj3K&b(_xVOz}p`QT0={L*k=)e4}ECe?IeZP zHc0lvuWZqlGgtIV*Gy40Tf_tvLFt@d+L7NjlT|ty2UcgJ6ek%v2wTj{4wbc2qDT(V z%?zsO!YTz(}>S!MXb=?VrrYbLwn7RkB&wdU{u1>Y32UDOxkV?F|@}h z@AuWB0QD7809Lr-*knC3Eq#&MDPW-JA#{)I@&3Q(^!{hVN26fJn z#P5L88en`}h(!_4rV_8thCSW=2O&Ra}EdA0iF0TI(&274fph65IcQW>u zbNXo9c|sFjOvBsW?&k)mBY(t0*^VW zGH2D%=)d;%Q~C}AGcL-#E;r%xD=Ll~9R0KJa3*tOBLLJO_%@|+Rk=ImOYIxQ+xMbz zS-F2{%Y0zU=$JO>ccbY(fS8__BjR_2VM=-?q{66rE@Gre3X~ETB3O1bX<|}I4UD48 zZ8<^a3Omf_X9Fy}$G##6sHk=QVWi414(LKIM%Z$ZzkK(hX)J~9S&wCtx2fk+2u%w9 zHi<*ayI1mEKss^k5b!JW$-wUQsNT0uSTRztVnFP43j8owLT}MsA^SjHLJs0(lYP#V zA*2W2kccy}quzuAj?zYMVb^ImEVLN~jTp#wK)y#8XKE@wJ!S!~*~U1l`7E+YDoCQ# z!F>7xM1ra}3oIX^Z1orh6!VKze#HB22Ukqdm@xFxo&D+D3Kz)^gH}N7?4>1&Af4jc zFOeGXHD$obfzFbYA4Dae3e=o%H{09EH}F!PHW(DXd<{|3PYmHBA`CbH@bncqV^#T& z=sxBxC+ztmUs>afe2*}|ud8a>Il~#fc_aED_<85@Q0USreE)=T*w4;lEJV%V)qkHw z=uGn~U%5{$zd$l@%&%_`f-d zi5^bEoLOeEBIG?Fu&p0Kj!S^Uw{8#7&Fd*>GIZ31t~g7EVuG^oh)4h*iV<&~(Swn; zH11%6QiyEI#YgL~@k0;;w#Ze}qm8#f0b*xwS4c)o;8hLRHlvrz+Y9L!6yDfO*f{k= zKwKPvX#ZQLuzTqe*3m0(S?>2!!H59tj6X%9NYBBcRa`3o%W}jR12YuKyOlJB7I^C_ zj-&jn`8m~;OybxUMyK#g=KQCD2u`LhKB=o__@EwD@fH;-iy|W&$~#q0Qqbd&f;lMSE1@OnX^KGCchuB0gP5$ z04z!Pd-4`nDfQFph@MU~55jMN?>(SH&*G|e{!sPHlJ*wTO=Lw;pK(Q(gNR0_7Y+vx}XlUv~48ZELGrqSHrWs>-~oA^A8A0`H4=l5K0d7ZKujvBSL~ z`AbEWG(HlE=iOWS`jY|?%)47`4p`s86AzxK?@E%bo|8#iZK7LdgGw7+RNp6lTxoA~ zGLrl#|8pOkJXGL1F+@)=RE`u;;^7%7L3eH@%_@@KgC+R+!>7~NbBSwcPJvs=p3{T{ z+wTTUY(Z;^OT$LEFw#)d+^qP6ddkj@>Z__mS2ncwTPpt^G^(oS6BjRx^}=+2OngY$ zIo^39+F>s|CE`Ad;0B+lNlK%$gHcf#T~6J(l-yHxOz*PJ#|c7{FD_V>90#e}qYy*V z-+gqU+Z%>1O<<>8&OjYc$ni$IYAO5NEn7lbOg~s>3^Y--7Y37ZBC6bd@|R5`)LuWi zk$o5+-x8N;TOjxA!1HVTV*U5NdR`(uFWl{Yt*SrA>kbS$PZ(`1V2FtFC|hoZTr-Nf z$oG~Ts08c|jV&UrdEtkv7tUp9*3{z&{j-a(DN+$D^C@?`2rjRc15oWCt=SUntdX5X zGWLveiGm)egh*B}T6JZMgnK+9@2+!=2s3bynjSf2ir{W835`LVItC$_uDm&Y>VW+P zS)Ln`=_3BYu`}hwMiZ$HhrL*;>-s@CKN&Q^>Q|NtW^ACS^lABY{g+!=hz)0;NIBe) z{QdyEB}$wMvC6c~>?2uV&=Rp&Rvzii&4507XkHoV?y<{QJu2|-X=PfflONCHsRngZ z3O6!e#I4DyzNian^lQ9VTY~ZyaFx$u0-AWolF)1utM{V-^fY#$PBwN9lM#Su zw@@^Hhnz1XY7BNO23wKuBNJeDP5-g8npa&OJW0K)in7slK!x7jI@)iUvD)7_8$34( zp_z};)`GhIxi73Ig;j9URCZgNuo7a973b#R;)jp!+R()$rlvxN8(?dVl;Y1*8Py;1 zBFfxWXq-<1pOA*hB8W^Zx%xY;6!?ftoLa6R<@*Gz;ba_>f=(eT{76GicdHV&?vzs{ z0fm9@5z9=SzTujuTY6;t5Ywcj^qGpV3yz*4gKV+QK5{6JiC)o(4QN|JISpO-A5FR) zgkL!r?Pi@b_{^=gaE2vo1~w4}`SAF-s-bV}Ykj4Hx6UpsuIDKr^(9<#yZHVo)=yO}E$%$cK z$Yj40r`TcZ5AG-RTl&ip4b?txZw>>&^|sQi%D&#=MYk09c#k!y)*UkG6s_kU9y86& zj1N7_6Y#AIBlrY-$M;T)&5Av%<7HZI5dt?1$}3h-`dg9S@YEE?Ixnw{y*;48pDbI; zGhgsiUkR6@DA0A1B}$ss=HirDTcFmwfE-rx3HITay-cRStJ`lq`#)B^9ehUe?$;-4 zX3Pc4gkKz-Y<~D#yxmR876IcEp?dke1OqNarq3Ep)`ps&v1!Agc$!;UR-acss_7eF zy`saQ&M(ftTWV{spP#^K*pyv47Mt6;W|SPLx;@*P z!R8A3_RQrCK|^1;XmzEREL+XyK%xiKwpvVs6Qh#J-E|$eszQBrmhk5JZuLkpJp_|J z>AN*hxVvPKeBn&Hk2m)nQ?~=ux13;mCvByQ zqOslAu#&XQw{+RANUd^pmH@`sQh@J=N6lp9#KLPWEmr2e#d&6C4OxHv2b;Wx5 ze%tJhjj6_eaD_^zQ=2GrzM4R+skSfNUaoIzm@McG(Ce9A8hV+2H(`a{&TlmA_xWwC z+c37abXhBB#(@iu6+00C5&QvmR(AJoY0+wFxrK`(Z_z#Eeq!0@1}H}S?p#%N<{$Cv zz;q{`CMss$am`G%Yp6zT{9%a0D@QL~+WdCSfa~kiAWuDsNHPLk*>nq$5AU*<(V-H4ibK#v9 z`ahYJHJfHe&?9JE^XHxwz#GeR%kt~`xX<%*4-ntafBl2`^aIz}l=VU>^E2qVXHW5U zsfa$(S-+vxJ`j~O^8f-t5VQHSg#GFD=KDzxp!Va#7?d>c4)BahTaC+DVetX@t*0%I z*xRjNy1>?MyBj@cS??&nY>U)l6P4u$30Gb`PM!%GRDWBUz~zHOBx|b+7H)tK_%$V@ zmm`*D+vB_8A9(TU%7?&-LFKDC5PP80t}3p0Y3zmJETD3w`x*aM#l`<+uFy z`C9wQSn}?>_EvcWz5H3RGK+e2^z(X8olhoqZ|Am!&K4Jp$u0(o`hk)|AUgbkxa>~T z^0CYra!+ByWr29Aw4{ZULU+zdxy_afY#NJS9Ek;K5E0xW&Z8u(0#QTN-4Jo22+ zXMLIhjE7@EfIs_xN{%M%yPrd^ zdOklu%Py+xzjK22v+|#15hSj=<)5+$z%bVIoX1H6x4I!w_7WxJC4bU%Pamu^s5~$@ zfhOsN*H-87?|{F~WT}r@Yl0a7*rPfaSRlnKu3hE-fT;|RLwjun#yI~^gqu;f{ZGrb z-|J|1hr`Kdhed}^1S}?l&(kx@w(lc8CZp~LzN-$}7|g>j4xRQV=64cIfsGyq;9TvP zZZoXRzo_xA{pCz!)%phX=V+4&dIU^wju)df!)#ZwXUGIKrIi*Z<1nuh?mI&)ra?`y zpm(osOn+keQHJ-k0;!904xdx=E+G`(PT9FvHTk5~9DsdcZ)Js#XZ8$42tGo3LQL4x z+wsWcyYl`Yr;R#HSFqcE{qA|Yxht>w2K_w)E&s!OfH6O}nOA;P<|&R=2P7AhCfD@V zA<4Y-nZ`3-*5Js51K~Gcezv9M**W&U?$w|RkF?oY2ss;v&Yqv%*0{H)^x&Yefd7`W zZQWLxA3~%rZ4@LyB~-ydiXI)iq$!0;5X6|9!-7_kQ=*7n)x$`$m)nx}WHQmi$W%o%Lf&OEnIed)u%{a{Yy7m|*ie49?D3 zZwcvf2s5W}8qel^eQW1zT1EHjCmmVZ$1#*WuW6SLOG{Z@+!4TgVZ=v`X%R+=7v7PIr_Y` zmj7nDUnmz0H*m`8i^JUq85N&HS-PuDJ&giVo9%{PGCj6u##QXG<@HjSnb#yWZ5dfN zRa%I; z@V*f;akGj_Dzd}gzK4V!vCOTCI}4M>h+6XbEKg4SK!xwRJ)dAl%F+~{&O(diEuFQe zS5TXeG;7O?pD~xYYrZAg`O-gcoZMAy;%610@JQI|aqYGvbDrhAJ{ z$*srZfol1Jby8Dg?v=6~8gojCY?6HcZAKYOTEULfpX_uB(rJs$=KJy7$9c+eFZrEb+=GU{cdvXwhM(T=|cj+x9JW#0%=*QCD7 zG5-A%{f{C8U0skQex0}b)cLB6tUeB$f@4oHP3~bN(m?L7@(LONGanmj*3`!)XX;XFbSdG zLuVqbXiAo6r8Xn>#tNwuT`Fsv-<_OS{+<29trr_iq1|nz@y5cMPADWDFNK zNy3MKYE7&C9hox=;$jI>TpY4_sb%IjCKe91xw!pM)p9&Tr+3ly7w?Np21>9)(h$VJ zPZA^d)|R1>8Kj2stnAHMduuFg0yDWTh=5fhAF`b12;|bkW$u|P7H*b-jW8B9q+=iC z2%1W{)-Bx}Qws;I`&5DzQlUf1!D|{Ytx3FoVPQIZ3;9<+sLdVbfNRC)rg=Ys6ZhBMTN9)qb%JwodC+FbX z7>60{S151LFsutE7N+p6ujvTs*tmu!MuEYo;B-U26k?&+IogA80Wmo#+@YbNnyWL0 z{E9`1FY`@R{r@(g0Ao;vZJ=d~KTTk{jHM9g8%COT+Ve-)RofZbz>EX!?inT(mY#X> z2RU@aYIHOrQ$xd$j5c+`>~wlI9+8SqQmr72j3eIsspQ+`)9k{#qJddMf$ol{4?z{y|I zJ<j-D^}!&zL?pya2eFnM=a z5m|P~cWk>Gi5GRm3@T}H93TuW)3Ta`g_Qsl$BauF5?-GC^y`sCQ4~|gUGXa~sps04 zL-@pRWZ=W#33}Y&io9{#IkcSw8;2GpF<_q_xdPEd)w|1&WRWV(G8|w$+X6)IuC!oL z^uI#Pm+KbPqh2Skpd+QoRCc8l-h`JP520^hO_jXAQt}Xlg3qojh73%?m-}-e6Qhtx zdQQZ&{B_;a-r4kq@*b6xFr0@>jS+Y|09Kt&WCJ>8q+7#qk)b0-_)7LW^M3HF#EkVv z#HVWv$QkLS%GYh&!UcEAmi}wH$;5wSuwis@~G(GqJzjp z$Vt!bQLC{^Xx(2-Oa^w&QGaY^$G)&Z%npA0FA4*Jk~}F;X@K8I99eTZAk>^or|Wv> zDZ);NBo$;(t0mzOHY<%o}PbElK)j^MsYj#XV|LFtiwAPE7e1LmUE~4iUB#t6P z>8z5bw5KJ+!8A$uxaaVb1LJ$r__5l0gp{|uu^V0+dQKJn^!zsOgoi!3U$>+Sy9)AO zOtJA9uFVVn=zC6*j#pGnhR<&Ldffi!@Pl$JKmw^NF~B6Cs7Q9sG&h+|P7&`&T5g95 zBPwI=$gAH zYYp@QnWf)lRhAl)kclbSIuhTDNQch!9n?|ey*+sdF3ZpH@aED%Wn%YV-@{%ZMp0NQ zUo^5Th`0{1*M#Xce_ry@5$-=dh1X=Ftci<-L*EtE_y?R5$+N(`hKyKy|HRw9mZ_L4 z94*HN&leO7&kym^a_&PKM9=5M(LAR4`@xUx%)=Vv2{h@x5^rG(86V{3qqi(BDF|)P zb-XFsypHf18`(#S*mA#?W+?refadrn-TctlDI1RxKOf;)g3RbhCY?18E_J}Rl;*I_ z8eTpr49A<4-0dyzElbR4@1?lTDx>Si$GLHUW=7_C6&n3gLjdY7-~Ov@%)>`05@7gW z@CrNOPZDU!`D!lZ_XdPnuGQNKYiK0&+do~bB&-o67`Xk`f}i8)>VB}cnAiIDj=0f& z6cE;d$|;ci{Bfh1LD=f`&ci9Na$mXpbkct>uU-Rvkd1GD8?11IUNF^gr1zolFc#Ax zgm!#+UrBiS6k$!6>wPeW?$>!Cqjv$;BwFjCJwG*O!R>CZw=*U$=yV{Qi_3`z>I!o( z)I=+^NjL_5(c^f=aY<0K(dK#IXbUp$3@s+Rs849D7<%ARzqOga@#I2!D$UkQXjTwD zt44t>>Yu@~#jA!#tx9GASVLsx zs-#dR0HFCMeENbFxPbsiKC0t=4|kMbwUMpKXawd*DEoOoWPHG{ywvI}DC7Ni^c;`) z%o{r+$mx5K1Ab*LOQ3Akue{{`Y30QGVn8yx703|IGwJFLFb{L7bbj7cT?9I{{ryjzA=X5k9{ z$fEC+V8_dAt@Q8(HDu7D2FPGVuG}JIO<)QiY{}=3wkrHlhng2?8p{$7t_7Yt-dcVM zqLX~HU%FM?n-VCx<@>K0z+UG~J)lGs6EURq-oJwdKNE+LFDamv5mgy{2fJM(lS0IP zzl|Dz@Sg8-i(d4R5LYE0g$7WObr+kZ-mr}zb8ahu=9PpaC?;MPrypVe?6k@NGjOl} z8o`&EI9veEZ$0G36$dJ(UJtV2Q<-M|>d@CW0XO-)`gR?R8bM7>HYAFIlGV50N0MS+ z9}xpByiZd+*}R z96S>5Y3Jtoq9)D1|JJYP&jRDp-T`K>07vflz(N%sY#%M_OiF#J=oY0XQm#_qtWn9O zE6S{9NfjdkQMY48`f|)fjO*W%a|x|cw#;6dEfu%<7rLNp^NW?bS>5btnu2Ilq;#s7 zqtA~sL^M~r`83KS|EHmZ<`dn&A!)q?KCgTDFUyU5z}5lNBH`KvM*_TCs3+^gfJ-7}=SVpym9 zTtcEyQuFuOSe zzJ1%wVVgWCq0JMUsl!FN?SV;yk znTa_G5YfPF8)nJ(CW-8c0Hq?O& ze@rhgTUy4d;oGJ?X_UQLDMbozF3U?9=I<%!gI#!R^5;0{=!s#I;EOi7ugT?iQ&ls1 zXw&Z#9f-r>cw25;YRybD14M?qQ8!amL+vM0Jk?5h21t zk|05*#8hH179YfYV;tm+eX&oioWbJBvWKtVqxE4=ksi4NeU`>pP}mmtCj9#QxE}0X zN0N7-`jwwe)dU$p61ql!KdI_j6%o5LF(@O1Aw$kbll^<-I|rHhoHYRWg;*E)t12G1s|K=$7|NBXe~ptmrT2lIby~OPB$|GW4L)nJ})c4 zsb~K3q19agvN7Cb@uCG;;TDVn}<{#N7T#HWlryQtkbP&{@LkWN3%2m zCBeRuP!^Y*PQ1JspaMAYEK)9g7erBjQes=)$cRK?p?Z?;LwI*Hj@w`rIS)-HyCZM; z#j%n9z7m3tH+ZJE%~o>?uPz1|N`weWLZ@SIauBsJ%wef)78bu3TN0(b-2 ztNDNlfTWHZV**1w9V*3`qi6-|k6r(MJL4Ef2K|a6fL3iH_6mZd61#!wk@ixPo}y|B z<~&`tke?#DFh)(ZQN2;LadlNLN}{Y;*@tnCyDxgO)BwO|9fluO>HBTkP8?9-DWXZy zu$UsZU@K14tv2rSaQ;BNEaF+H`!#D4G>=p`U!!`e(JU$mx5I-VN&GXCgT{@Q$zgia zzfgbi2y^ZLyMwxBJJ7`*8)~84&_}#pR{yPA4PBy)b^1* zMX?L%6icbJ7l=<$TqD~>aYeNMvwAT|$uNHfSB^|n9EnDfnZC}G)gSYlkcB^0S|g-# zEBa#n{%wM@#(>KW>AiH(B8?8om#t8!QL<6CQL$0AQL|C^-}DfHDTTvUV(L*2`SGC3 zCw=usJ)M@eNQ~vw0Kk6fSuV8o)j7&0XUpb__Pgb2lkYloPhft(a>nak@wKA18c zda*q_+3tYvpY_F(FEtqBi#a(Jo%pp}r?Szf<&0TUKv2>JpQRSOY2^0*e>67OQiOFm zAl$Cz0BeLf>NJ%(ANjv1{j8P{L=B-F`Y+D$=1`w;a|2%fVmK8~_ zg|Se=%TUcIpKDnd5gqTwQ<#b8L z>`h_TJVU$jWt~o<&Hnd z8*AU>644xGjx52RF_GDwOd4bUHWvNb zF}HV1ALe_kEY>uhdt}+%4R}Z4^|zmVl^$lm^Fl-s{cr)`G3l(i5p~VZ9Cvao$N!b- z{Sd{2+`vSv_)1;NGD}gl&R#vIHjR+}6HZc23l;$sDLCmbt zyG=S@`1A{E5kJ`$qVm4e13@en9aC5V5W*A>W^2I%9i2#omhwG zfs)16vWwO_H#Eq;)GsP`)yKxuC5_6n)p06mb5`tdQU5H3Lj7;`lq_u~>M!VZ7hb7e z>%>fvS<{xJk^!qv!V!=Ca4zAzW%{xd;xQIZD*st>OT|N98(Cf1@2L`Iw&T)v=UU8A zqRW-jE$m&_*n;pr4gP9S$+h7+|HYaGS46Qb@jRj-0 z|C)86d!|h*UaMB4Yk`>^u62wG58o=;Iv%vE$-&Qc#ne4etjpugar;ntd9-!E^rOMf zpqi9DeXYAKLwNHUyjiVSXC%)&d@$m$YYYEBqSzvfwcjlp`&jc??M-&3ch z?r_T`@7^Mt%#@6iCVMebGd#Q4F|BiTH=K|V?pRo-ClTVwB1xUk^Ucjo>HcQab_{z2 z_q)0bc2qm?Ni!lZW4NOg6f`+z93$R)`q_NBGMpm@FSU)zklXg77M=2N2^ai{?&!}4 zJ|3TLfY@zF=Ppb%$Fa9lm^J5>lJ_g5O(=1HGGT_b7?#f(Yqehpr2eWa@VYL2>tE74yCtbl`@LeJ{$Tr(4Lr_JF_ zvBHqM-Ke8cfhgL}Bp6QK1ACE$HqVodLiaixXU&HdqfODb2ETcF+>Q9_^h>-H$Y z?|^2VM9s0gIJs5T+9RJmzQg+4tDK(6XC~M~sSXjfAw+@pgCqAlm?8A23)X*37f6@F zQKyq)Zx%^UWU|Kf56e55*nj=#wQg1OiKz>hwYc*-t<4*xbl`>+z!|2uS0(W}lI!!> zX^bCMPelgs+HAi_$Z%_(GBhP#LAWJJzRB>LRb?H@6$u<*y~}&owGBm?aePg7s>*#vgSCCF08SR5xJwAu0)?hj8E zRUYsrgaPZG8Xnoo2h|3g@!j6v)GK)YtjQj(qTw{USxMJAwe1T$bWhwVN^xJXMsk!u^F*s09JeLlxHm2`TRD1D9*~VAY2{=KEkOIV8{&myu(Zj zlK=#M|HKqvDq00_W7vW^=_Y-NGE$yTm!;z1H5j&PGJvv)+{KGB7&dD9Uz~)^Qf%O- zufTZ@&^SIhDQIucyfvS4a6b9NOd7g3DnZtdZ)nWt4^#c<;<{Qj4cA^jWyFM~2gH;8 zufj4Ni4S`25>iBS;laPZzt<7W`q}V^dCE+IOX7))5-~R$>Sx^eDoD;#q&~BLjTJR` zvnyNlKZb}HgpwaIna_%AsY;gpQ_r3fxizGsJg~`7oOjrn%@$T=^cmM`j^?(v;|i(X z{`_0E|6@82lr$Y>BFwf-Tsc>t*jHS%y1BjRo==WZjZRP#vLoB{`oYxb2Dwk02uWTb zqb(z>w!V{k+}GRzeS-89?7HLegQq)U~JycWVeKhP5^mt}B(wdiS0)tN$s z_qN4Y;|_iqR`;nWkft>RgAZ*zdMsjS2u%pZ;x&Sc0FbS%bn=8Oy?6n7laposA?3dy zNHQ_R?Q8T+F|uxW9*yruL-w#jC&3+7p(^z0cuij{tTJmTmuh@;423FFFurTq!ga8a z_HLrIJzUBpU;mbJu)xBiecL1hvaD*)bX8wW^AP#fR{A~ZqXcTTb(^dBU!6=ljEEkn z{Y1@wmfQfL+kY7F-CCDkOJDWDMRB~&)$MxCVnV5ey*z~Y4lUm`(mYt3i5nl9s)&T667Qzx7WBQtLKhz$lEyrLf8`(tgcwxwuT}n0I zUO`kYM*hFI=q=Zvx-H6W7PDgiJbKuU`IZNeLSAmVe5&tNs=S9Q>MvF=jnTz)-)btI zGIh=uP0c_ZIrZnVMtnqvWN4S~ZZ@~U|By`eVSqPkD4MKs$}6$%i<@c?KJqRVZ*aFc zYuNJz++ZYiud5*WH)FSo%AB7XG6uDB_+-`*cY>(1GcM;o*fH@|>b$veM5onc{lKZYgQe@JVNzgCG`Ivu3l#V01wj-RiyNcjBmTshm%v8#20J zv-9=qWw42SZK5XU>(4&Fs9LY!E2}a*Qv?)4z@G}DQm9smdW2V5rcWkRwE=4c910fl z(LLuX{{DKusqRXn9?GQ2xGh!4NR_g-rlm-dlq&s)BOrHf#G@<7vSD?AD+!1n0%*jV z46>}qC%^hw@@rFf+S_c1pYE?&Hs;iX>I&BUh3m_$wS1$fI>%?ebl!AQ5s-@BR$Lz1 ztHCO?;Y4xMk+wuZW<*ufD`0`4_}xffiNp(eG~!r~E2*qEJ1bUz8AJECR*hDiHq&7A z1(GD*OKhot|PmfOrFM1Sn znbhINXfpnqys&Wa?Gk5DKaHjBxK0-7kyAtyWtx({9^^5n=BCjjX~o4E)l z->55eO3#nh2fCPL9eITnAe7GWUVVDmRNKHCz6swcAfuXk!cwfKB3A%wvdsHGn?5cpgos-^Q$lbB4%yuKgSy%&BykSj;?D{jxX zB~fj(WP!?P<`)-#$$j7x3IB7JaMt1ygV?ut;P0|R`JOl`$%*tnZmC`C*vR(z?kk)1DNYKpbD z_@liV4N1q+*fh?K`*5Cu^Au|Z-NDb1ppazu8iK9U=!|z;u&Dyw0lt`+_l=INm0#{3 zukO!s75IsV?mB%oAIxxOs?)Pf&R;?El$KUjeFjWi5Gwz1)h3YfK;}y7{N2li_I(rg z#YbI(wQwWr5o^4nZb8jZ0>)8SS}3p= z)kyb9c&U8cz;`j@hq5uJn=Nl?GScImuC-ZsCPy`x=z0wIJh|KG0o+gR{4ME;zSG~s ziFxO_#B2+0Jqk>dUb(`rf3OQ2N7Jp(6mZTUIhauz(kQZ5Xp%PGEh_AOoLCGgr5W`$g4)7-9Cr`#-peZ$WgQtGLrbKd9!vfQ5WyTuApFZ4 zbjyPG?bA{%CP_2m%y_!2Ga8k{Y3G(Tf{gs?lVY%RimXw;$3K1@GdO4LN{DY1etSbF zNDakH(|iios9&Ln5eK)fx$&0I27hwu zRVwb|`97*Mw$8HJ2b(##tg*32W&NDrN~#d zCpop(*RHR05pkQ*9*miS{YyiRaU&I5dQi|;AYvIGrSd-#0m8TRAk>zuahY7%pnO|q zP+e_CMv@Q?Smm{^cdjxUsx9m6+<3N+Eu{J{?BxC`1d1_4Y#NERYz#k3cbphOl43`Q zL@P3@;U3BFiHs>$R95D6jIG)Hueba!8;LvQA>|7tj|l+p$cDz*kr=>2H&%^arS0|0 z=J!s0dGTs%r~;|FPEj@&90__WQtqCV{UAa!yb1dQ8&Ar2ii20u|9Fj5w)7SlGNP>R zC`;{HWQi?bnAN=TgH}PmU-yhgCMQd}x^%nas6pt%{tJ1W%iC)^%m-Ula*v*GR!^+j zUTrs;KmS`EAhnE{DHhDJdyNrz zI3$4e{gEB`d_#QGE*Q3T8(#3ZnP-(*Z~zDG-mY%nu(wKhy`6P=3q{=Wop+H4VjdL~ zb1ODjvxSjTCKy}%9-Zcve|OI^2iWN1ud_a1UpzvIz-qYEHASrYPC-u}lbMNg)bmrU z0h`wz*X^9+_4aDTHr7A`O|vK{__tUT*)Ar!d*4OcIpakRz&RUFBIdvL4}e({tRxV7V+U+-XaC{@i> zP2vlY`1$b8lYn;x2Y!Sd8wtg1%~a12PUOPV%D#&ks1G63xN`19m%9!)4(a*4BKBF} zrvjHSES-!W{rz6Q{^6jOeyoyLO8X0S9STQU)5A#aEWthC(~jd)#`8^Fz-G3`>h_ds zfev<46k$brLr6-7oup<($x?r6wUg_mnm}&l#>j=oth3c7i-q@CzlS-wpzR`d?aYet z%$a$)K7sP?nKACvIrvl~?c+*oMj>wz@~9t0;jwlPo9qHwP{B}zRp(Qd*u7vds5zgY zWsjK%)86>0T=)j5lBVVdQWOOiJOwXo{s5^$Bs$9_qdKuvt$T3Z3NqQq8-V6Yd-;0@ zHx~DwY?pe8n_Bt7oQ%k>;LEA>oP62T{v5UY<^g(V^JH?#&Ybr8ZxV^9zPHzzb-`S} z84$v}k7Y&m$T}VKj8X#HSjZ)lG}mLm@^Nq~IK|dtQ8BKY_ncy)uphtXs6#6O9!#AE zNm>^JM3DU)UK^p=qbv&J%@p;`NYRgHWQKEazY%>U{zzl*B3y-M>z?#u4M$a*015l@ z_NV=nv?V&uD?RfB)?7i0$3GWC^PczEK&={M(zY7L6UTsuRK+{fmnfOD+#bJ~o=$TC z+g9%Ej%atQ45*PK+?et3=`HU;5eo)^j7V1%mnQkwHV*h}W-8M(4D9G7E5@6%3rcK} zi=#8Pb3-i&N^(lH?uMf)quCD-z@uQqUdWg1<>|WUGu?iE*#%iacHnaK3)r=8*q|fa z8cg|fz0<)UQh&@PJ!?lW+K_W}#=43|37kBlA)!fAK)so3Bz+RvkO&}F9ieL4!0I{n z32qb^Es+hk?daH-=pb{?UDGS-KMtG2%sj^Gp5Y5PnQ_Cgo;wyjZ*$EQ@2(l!uX(mM z!+0iSL_Amcw*gb$_qZv3&yhzHxu&QO{G@BQ!jv)yI3RCni8M`gMwT z(zi5i#1N*i9}f-T(-CAxxahr`U?IknEx*G=+OY_a{6ukp+G0~F9s%7O6U3DUE&Y8I z3b(ld0l8A?8rtaMX5`wC!}MI<6dkF4)K#_!h*N7`d>JjX6JqHdqahnqvXP-j;e z4~-h4hc*Xz1dZuGT#I-S)Up#C8j=MbOFBXo4dtY;LSx^MD=rSf*vU?F0AVvrSfJBs z|CF22o&i8ci&ERJF6^7P5Jg2cHLi!%9-GA&rp4Y_->BY}%2^Rld8pXX8w+$Bubwh32_SkKz^vpbH%0Wsd4aT^CWVOFZEWpz5e6P5%CesKXY zO$SWss5`uWDT6@kHwR5lcK1)%OF4id$1BMBjgf!OaL_3NEFdfy-f3B!2{82;Y6;f& z3_BAoa%>-xp@r9fd`bNS(XNN=UKf4zrlXK|(2VRUhC(1ehHhv^W+W`#^%f})!Zr}w z8O+U$>gWD4v=DpXvF{=TeNe~9odQx+g6fymz?x0eyhUz@I!^{#u1U@KgB^W&M_rQ& z8J_K@FJSW4K*CeoQ$clxP+q0-Z}v||n@qH|L$)X2l=HAP` zz4Cm=y~L@NSkE2&d>7||vf%QKy)1}W+?GTTCSeL4Ch_I5rzFF}Ma}?ZU&|n>%>Z|{ zxtyK)%wFYtd{XtwTu}w3H=OcSU@4@Zug2-r!;l4Cl-K#MfW=6C-}JOQ@4gHL9}4#c z`83_I>q(WwaGGp?JTa3|N*gL=oojTp;M^Qg+&GC5(Iq3B)H{R&KVZhdA+*_v{28$M ztdFmlGwGB6c^aQHOBgCCm$=_xq_AQ6s!P@e*+Th6?$}ucl#5m7s@!lil(z`qXud0D znj9fp3BxKzvr`gw8WWuh4!kpbJ)p&DdiL7%Y}9WO|yA=Tdq5}UVVBGdAdR zslnWWBkZA%Gk4V8MKLPH0PaEYo;joC=%_pz25Bx)ETH^!XM*~X@o9ATw5chVymPaH zYtXI&yr3fGE7<+7;7@=T{4Z(?uKlj;NQe`p^dSlS&^?6ff6+mCNn8^9pw}{7h^A~&{(84VjGkU&odL5#{to~E&C?dYE~Q4iH&X;f zgdWOOCgdLApNav6IoTgG^aotv@IvpwrW(X=l($X`MWQCDI(jVCcfj69x(`H(n~YTc zJJ`PmD38IE^VOk63aP*7%m^42mc-1eVeZsKp;{uAD617txkk z{XLzW!@Ap|#-xGt2YXxhx&gN$32)qihb}w>CUBzdR3U7iW7)z^TBtq9X|yhtx~aoF z(gL2C1M^2&R*k%Zq!24y-EGnv+A=ymTD>ihQSqvYubI=ki8~QxYnn#eO{;n02q3us zwsj@}ea!lW+5bW7d}QP&vD^3b!#7C7nRjv>XTi>*WJ4Ym!&YA9z`i_Pe{1U*L5Jt# zs@5JrWvLUg#eej0q47^K7&mVGf=ss?Mj|HPnGp{Cp{SOuoCXp#qYO^+HD)Fn!1OX% zML53Y%^@w+^UYLWbb~yXo3(~mPXf+{Zt>5746@@Cr8xR(nl?SIf@&*g)v$4|j##M4 z*yoVn^QIJyDHZY+d8-JQdzDoc5J)Z#UCJP4E8ZS{BOU(d!IXL{ZoQ*F>pxb^IF~XD{XED zy`X?+oT^=E_wo$Ln*v6hrpp&^a0O4-1D>$BtqJ&6s{Xjzp!#^wW7kelCjipB^MWFL zuNLk{waL5U@%9SIB?kL1GY3oe&0g^G7u{~@kFTF4vrdamo}y=h({KI=BghOSy0xVx z-WQzW2I{1%81HK4?7plm$2>FX33-Ezs!~DAbeeA5KjFaVBKpjd%4xLYV))WPMEuQb zOWZo{Fw4R|jy^$_n6L&;F8h9c0hok*913G(WrS@J0r4kpiT2pFQA9Inw)s`qrm*J~ zO?tXcmS}eWI-a$32%E{(p)i~7?$|U+?&;(%_;9#cVQv%GVmuwP&wg)pR6|pJnZ)fg ztFJ79r>I9%76|?3U6a8p>*2hvT+$Nf{wBt9pve@T=593wHZ>=#8LAFBa!`e@={gTk zzz!;%49HxL=TbCz_KjZ?&I!!3ANggCv=3bTuzir)l47Yty{j~=#BTuplJa+dy}qFG zl3ha2R?g43f3bufs!7%+Lv}fHq_}t;%!pKN20q|Ewj_CQ7~UF{UtFe7xz zN&~4J%UwTy@&v!k0JvI+_+N!otl^5|ng4Y@OF&}TISi2Q7q8(AweHyZFm+_*u(tC4 z!!43c3k2v#3<0sg&rQz3^2WW6e@nZvU|dxzm!kdCK& zD`ms>uDiYh;`>`M0S1O&7Nr2gn%7NBKscEvUST{8oze!;#r=#VV>9APj(Dk8%GMtF ze1<@J|Dsm*)jcC0wAgg3i`ASA{yQb4Ka(H;C!#}$_vtds?nmjUV zs4zqof3No*-FwCCn&87gJ$?C=@tcULK_+`iI7ZRVmexU@1X=sA zw$qTJaY8oYaDEW9A!x`fog?pWDTkw8pPb;;f)^zB-gxdB1gl4?An?v;5lWwmUPTIx z<=ChKAhv|^%jQhHN#j$IgZN15bzp!}bI$JD<_XZkFbU2;tAg6OE}7NAnXL_Nl+|GN z%J5uW*Qm9dyCZk#V>SW7gP&^?{^-kpX<^fy>1$h#I`D%X8DQ~KztLD@XVb;z^3o$2 zapi;2$%)E(*YT9rO^a*&%xN)P2U9A!2@P4X@QFg;MUFzjl5f^V5@pn|k;P?fZo^B*H>dS^iLVTe*!{ zSeR5OYED_boP0V?H;x3CpJuNgMo~SlteqS`Kdm8z+5b@mkzFE|pcpoOxsCGGX>~z- z|GwQ$LOr&!C=$cu>u9!UuGe{+PRl*H2rKcTBOcxc1f7%9V(b*^IZlCpKO)d8Z+OQ+ z$bLra`2iUv6HmLzlbi8ZRkghb=@HTk@iz?)TXxp0f%^}ecFG-+>QW?29^{2{@VYy` zjWZ*8L=(5!1*IMBao+2JG$U5Fw@N>FZKH?{1CwE5EpBG%>bHH+>{(Pf`dQc9T$niy zc=>?ut*2V@$ncARSFc}d;}WW9)P!~|x48!E{#z8iWEf?O$UX*RrPuL-R zP#>wB$0ocLsr(R%heOg=7uKfjlPQEv6j+tpQD6v27+>i&m4ct0P*jUQ7*7npsZ$kL zZ}?>&_G^$ipmWpMbK-!o2qQz^%peUB*qWoct{G*=Ve(kchT*%sW_6X^K4sgM(2*kc z&*I+lD?tpnRi%MV0(CEWC#v@7?={hqFU7@Lmt<5mu-v1P8x71KawW`f$bXou_sdHx zJtI_u-OefeS-vaU-%r#x13E-l$P*04*JS1Xr-X_cP>C6pIQtT_U_UDM@TJ4hIs4l8 zS5szFN=D%`k?r2I%P{@SbJWhk@kALvyv!srRJR@V!}sV6-FF33)a_&EMRax#KX>NJmY;_s4Zu;eAM_p7hSu zTPvS@;M%3KLqqy|?q8?FK+;RpxP83^r;V4%?A|OO+lY3y-v`?D6_I)?0K-@rrhC4zN?USpstr6ML||j-X62r5>kvVeE*xh zDTi;0@$81d1n&}qP{zK=XHL(+{WUs)BIF_;u-RK6oEh$!c@^*C}g z_jhkz)?Qoyr!#jQtTv+>ad*fa*PRta@wy#&ycOu{D3G9!<5wkitDr=q%O+(-P1>WC zSYN-#=@qZulDv?I$3+$gK^yD6*>6te3C{x!Teb~)$in&jo zT$T-HG9SN(O)$_G#Q9%Q0$Gt%xFi!=0Qy90B8uTz_qd$*4i_)6hb_e zk5viDR(|*HgfwF43W~Zp6`qhqT6f+D7`p=c!J%N(6xo zM-+-aDx&fV_n&ULt;y#y!PZJY(yFa5IXULcx+}TF?i`Z(k;-H53`r6xTZV=eHf=h7 z7rZ~a#0Kg{AN>+mKfS!9XJ81;$|8pXDkJkxcHpNAcc^XF-+$^l4h5`Nt02fd^jxTNu4q%YW5gb1}0&IQO|T()mE!zUy3 zPqsfpu1Dq^QC|{V8@0Dp-dIBfqMQcZaRNv*x3EVu|EB=BoneBP*Tky3-LGaS%rY}q zdAct8%PqC(aJh^7<}0&#NvNo(XlQAN1_%EPB{Gf&Wd4;n@bDm<6TPlHWu%s@Cg<7J z8&ql8DDg#Irf$DRzFeH}kveZX0X2$*76zoibXFh`!9q+AUpEO^{4=ztkO%W0|75JI zrd>%^uPsCaP#UcHTiJhP&IcP$^0L9f^eI!jhSBukh6cgBygYJ%f0k?+tr15x0-sKD0VY1mNht z{kyBt4O(3E8D5Fm9l=W-mb@bWeGkB`w>tz)2W*Nc8%w^v-V>HKX7TSI(jZJxYj{2M z4ZrfAg^D~wx(^=KDsKXix~YvR^9(%#R!_p;dxbPcsKgy?oSUCDnOc!Zoi|A-T?YyT zG}+tySQFhyeQ*@Iqsi@vfm2HV6rjRE%<<9FB#P8&uxzvAwT|Nl*{d_MMyv- z+d5XN0Te5B=e+C*bLdQclgJ!UJ-H{j^>E_^s3U+N^|`s-c1{% zWV{$V>V$HQ&LJS*oinnRO%LARWgDxSj=g-_B^%G%ymjkfRp{{!Hv%m^ybwo`jfOy% zTk{UFCpR|~HwvV7%)it({EvCWKVp6xG%yP>MnXgc+s<+P9YNEL@Q=qr#BxI@7Bho= zCEcNXFbVa%8sx>OwPtH|`1>fpvGtswQpe7;l#fP|<)m_@X=MBl*x-iGCc~Uv#cyvj zpvL~smj8Y(&EFW;kBqGtvA!z-jN1Z%6llzzbmCNbU0$a z6BbOb)XGS95jpVdBm5EGG@oltcl0Ovk0SgZ`ZMLQx49k^R(nbMf{Y>tZU|EJeqw1+ zdL;-+cahZVq!3M%Q)X>yxIosPDVohrd<7nN>--Jp?FCl(cJSS%^8P0V!8_Z_3=gCP znj+sVBjuPlj=m=%4+I-xrExKQ$b)cne#7TEY)ZfCe|0C>Hcj&T4-Nb@Qb<%rLmbkj&MA$(Lyzj1S_tm0e41a?i|}D8{;s^&^@5 zICN+KI#$5RfdlkM5}C8B5TK?gvnKB1<8K)upjk~#;5|IkvGvDY>}5L?>zQ%9#Xvr! z*xoH@TdC=T)A6v{eO`tFzuDqOx>1Va_vR(d$JV}5T%Q|w)v$p&o1#4n^+o49v0p&{ z@1FfT^eYCfGBNa~KA42IWFrF)MC$o!pURPNdx*HKyJUVRZXz!wk7+prJ(y~CWC~2S z@(f@F4j1Q*v4T;d{?vBua`T=Nge70RfwKpDL@tSH{8rZX`)`G3fS)YIR=O?7Xi{}5 z@2M{=EhnJzYpH9P+T8S|8>O5uBy|GahGop@q6mfVw9F8R@gcEBA)oYzabo3$WN_R68xEp9im2PZg*;+!`JL~U| zTSZmkUp2cPa%BPq^)lXOSk-mfaoiw1F7L-vF`qpTK?sN8w(QgM?xgv zth-A`yAJ;L*AHt7_8J&SWUuFAKCiHB4^8=B>fH+_+w;14{M_Z|{W2zb1UKGkzEh<- zok3h3KPcE#CGpw7Yt+9AZ~mF7Khz}Y;J_g)ynwH{JTN>$?Jq;15x3{FxOI1nu|3qr zlJUkZROvpvO|-$~u-gsY{qIfrp9-x*>2865D<@=q$=acDoDp#Au}abqxwG>oc-ZZMD9DCQjRIy5#pI$^hYw@| zj0cdtu%_rOx=QCeT$+BYG8ZgX;g2%G-A#i~fLV=HaXi{iZ}vUV(!NP%*RXL$vuh&E zE_xA+14i^Bv zllaw&_=w?h8GJ^(`MtcS+;#E)d(UaSDR;hAL*L5iy;*8F>Ox(HEPU7veH5b-t!t7! z&E-ur&x^V>g^^OW(Dx4x*;9|MIAJ2H)Lwdg)UW%>fmECgpPjW7cf&BR zq_WT;7I=?@^r<#$r9Cuom@)$fb)jiVOO;)psI*@`;67<)8eZ=C6&@pI=fFVyJJU;{ zTf4(w+*VT-e15W>N;tZoWl+9c9aldfIRDFsQzq1ffhMc`&0{0Cdwfc5#zK_VRMA`b zgYG=2eL#m01=P_c5>c%ii+Ilw3I_SjES*^Q#)ip|{6PJ}2<=CpvjX1<>N z>mE?zEzcuE$ey0p^W&b~Qcdo6Yv3uS{=g(fyzgB)(NehgkHh|ww6ji6V@T&$&4-ub2yl7vEJ*Bjv<-~@u~E^^!r>rm(=6(4X=)RLk_&k`irx z7W=Gd!s-4Qwgi{jY$8`-h@iWDHy-8e;w^tPW9VehJ)d|f^wWlJN^oVtue>+q-l!lR zIe<^R_uOIehb3jhSsmq^mflSVar;a9o3Wgp*o!DWLGk?jV%2Qxmh$gDOtqlQ!od$R3@101n-pQ!~cEk{it!g!^@czgMQS+<0n z!@_I)??+wcgq%hSCEX4Gx)}be<}IK>tctgPq$KWK*(QB>7H}y#EvW(qOL7N(|D1HO z@u*hk;>8N#1wLAL_J&q3u&w&%B>DuV6T2b3-&y?hu!HoAZ#on4R2bQT3S%>>6FU-w z#q0Bgu8DNV<=lPBOCvS%g-}0WP}|)VelBN#nAh(AUfAJepK(L6 zF~tpy?j=Gi*1HI;*LV~eJWuaSSSCU)W>{RYNPX+mO0}m`#d1gq5uXsi*cml*&0Bpv z;)!=&tyR*zra-Zq>sexT1@KX+D?pNRX&vC`xclV}_sc<#s;?bO?}alGtWP(P=g2sK zZ!XpIy~@1L4ielt_kC9vm*mE{NsS=)L88h+rWcuktuVC$O=C)?%*n9!c-BJCMTEMr zYiZJOtDo=5q~`|Sb&Yosr>)vq^>+m1>1TS!#}%qg zPHlXv19u4rK{c&A^Z8?{b5U(5nokehJ!QCSh5P2N_tV<%&T%Wpl=7AFuPc*mhRsQ> z`B9^pBI&y;1I)q=F`Y#|T(4Dfm+4sS7?0Qm(*FC`>cwHt=xoY4d)r)P(|G;(=+ z94QPkWC&FrzEC|j5bZ(7f9>`>?_{f3s!dKoDn=}YGu*%G!M!$i+f=0;N1#jZjzua9 z1D-Kg-@@p>FK#8|UAu_WkH0nJ1^4nEkDwMGv@`H!|C3ANDM;h+O+lSMM+z3_yu=~& zded7mTqj+>V~bi0MK7#in{+dJrtflX3$ihN0hap|_-$j|!WXD2mf93q-y}dJPf90F z8XaM{FPh0N%ux!n_~MQpaZ1dx1m0oDQ{d=SQfcyOI?9xZHkrJ7Ok_A&?|5K;7Pk>u zUted-JS9Q7uWA;&@zPBVGCCoJt+aJsFayISJQFsu9?O^0l-1pk6uKMo>%>ehe_~v` zBV{~$9NfKhXs|u zUFj~Ly;-)%$KS9O@)qBy*gH|_JC1ebRD$<5^J=D;MrP_axRo>fpzZ0G%G+ApX%^hJ z9z2O7smeHfD^W+n8iz5cG``0eb{C1*yJLU3PmxTRy4x|OUJ3`pG5DpOy&V?HmiL|% zW);fYvLD%tzt9fFy^`Buy1oO7Z=X6zT*gbdZ2J*#G3jdFI*DV5yx{e4NIsp;x4hry zPjbt!;#`#bWSqc~#$F`-;V8e3J5^Xh;l~^kK9zPq(RqsVlRV~|Sx(@^LyRlQ5hwm= zV|B>rm4T$0bg=q-oKeO4@GP=G4x7}sO#JR<3NLa!qS0n`4P@9hh51Z95MKgsv<0e7 zn*0%XiTH|W9H=sEJ)S+Z{C{h(H5~WXCGJ&! zvg6ts3xXti5aSl|8Go}d13w_HIQpFLp> zLu|Lto&e?QkmX+Oq~hZSnnY&xH&BSuml$3!$P;?*#>dA8rnTZr48+VcE;#IdX%S7T z@W&Lu;LX0XpRL@EFyOF>>H=D;dYoh2?e{aL4IrZf& z>M4(=Adp?1{%9K!uRNW)(t`@^Q|(F`ufeN>1Uit*H-(3biTHvPs*K1n@hQ@%CNM=& zduN-XMkbTH>!WVqUO2X5%nH*CWc+1}?5Q^4=y!KIlRc-xim~_CY!%(uBekG8KuHrf43Pq2$ z+=b3e>1YGE&|EBA{OcrD>}Em0qmRs(_B;J$5gMXspvSP%^?j-U5X&b&6}!4K3@9vI z9v4A3Z;4Y%ud?@39_V}{z!y3o9adSg#+>G(4R+uRoXxL6kfxio?TP^3Vk+z)ewvI( z+$@vQsrLtr0H{W~JBRL9Qvf1saB|Yj1gzrgygWWsP{V& z!;FzYM<#fwQ&%qgcLqqi(D|GhX@_N{t0D#Idk$krT?<_WhC7^*o2c$OMfa%`EIac3 zG#t~Nd5D6xX-H*kW9swq1)}*Sxgps{O;xgM1)h)cBSjtO5-;%pmMY$ashpM|&u2;= z<)V!?GhTL?7ifq?HC8a6;n^Qn!=mi2LI}K03x~=pJ>z7BB00iWiu{55 zty1O7{#o6uZwvyxQvQTjSY5OwB_}!UNg^GjjF}%j0WrB_=nhOcjj8>lUJP(bqy)kh zuK(Bb($Z(Q54EnsB>OUUXq8Er*caMqwp#!(?l!~Ewdjm(SBV-oem^m{q$CC8%`n6e z$$t6u{nAKcKSOgardVoUA=EneWy?w|*eGgNC#5sAz9gu)nDN5q{-o_5#| zvt)x%m%mnSXtyA@4Q=7>!GkNvh%HTb&Y9T;p-FHQY!=*@f=GMvV~M?(LNh1oce#8) z3l?xv98a-4Z7RS0bBbW-KJ06OZs5wbzNAhVo_e-r6UScNzE?pwn03*`|4=si)>MG% z;n9fKI|a+<>hSG&0IE-{me4?RWOjEA%EMBJQju~AHM0v2RV-Tm3{pjrtahy(t%y2a zS55mLPZRbcB99HKCcO6wPjWk)Dy|UYMfSErN4?Pvx1}K=Fy673;qs4zVwJMIf`Xug z&4^JnrTiMm;lgUYO-k%7R*IcG!@;ya;dpl6%)ZSmn}PKBql`n=qKxqk!%tE4%1Ayt zL;l2S`>>%F*MuhZ0bKt-_y<~dpvGP9!0w*1v;(ryS-YT$dpBhWuPh$pz zd*{blsViBwjl&oLQit4b#Pt1s`Akgxcn7b%rEXC$-JZ)Bs=0}??RJF(h9J6C|1{+&$j1NO> z7w4P1LwqSJWKx4UiTDPqBOANoe4n$s9k&T7Qwpoaj8PVD5f7sc|LiuH#mahX(|^iULSeD^97uG8pj1!0E2|`c29;)5YS%k}mqPiC~iZsE`=tTjjshW3ZH;qxBjiKM> zjYd1+-jLL)cZrNH9)q8<{Jv{AyH%0LVL7iUFC~+p&ZhlpU~8>8Y$%khrZPyuGB)0q zE_uU8g*ZB|CN^{Ca{jb8KS2J&69I=sj>qt4fAiPhKdGuI))eQq$9j+F3QJiBg<6W) zH;*jf$$pfB6rL=aa~MK4HBXf&-LBT9_X;j-kirtM;@v9ngZf5;5@@{HzKnIY{ZdI{p;pcs%PU)ptSpEvKSXfg zk5Y(Q(4MQ*lkQigwlZCYJrqF z4R`1|x_INWFI3SIvR+V*o;B1Aoa4$fos^(6UJNUrPmLf@DOVxW5oY{yFqY%NM&Tg` zaKYqtvbUt;-fJlOuP;#jjK)rYtgUiI@E!{$eYWx)wOD8gZ%FW6v<|ns<5>EB(ntsz z5iUM{?`(x0GYboDK#9=6&7hQ828ftytw^8EcE{W+&Ev}#f_QR~c*)L(x0pud8@7|r z;S{z(I>ha&lyE58w~DKpwKsKU;B4>E3TmPx-0+i(t7V)_IZ9{f24Yza`@}#frs~RG zuEP~n6kmpNaca`HFD^E%4X$6?d}l*=&BC!MzQZd)+z@__$Pb|VpA8Cp`1;`Z_|9-L ztIZ(x<7sz?K6*^tUN)fbR-(ABl5nUYu1&&VMOX_mCr#6DQzgaGLIHMJB`-1N(5(%x zuI7RpLG|~9VP!v8$12h_=U6zb4wlq|Q&R_&d)oA}6cskTy~G9DDc#kv2n3^=4D{j_ z;6@_1?cZH-aJa&BzDA`BLeYvL7n^r&3?9mQ{dFX zR+yBLIGUxB#nU2wj%zK5--}`@h5qc%DtjOylFOqiBO;MIPW>i6Qcce08-Wd~KPrDB z5q_2(gT3RiyyDQ2qurn61_ zZ!V@(;ARW{>lT1VT@3LkXj#~^(Wo&UZG74U)6x;PQg1zF0%zad5a-Q)9{=t+><*Mo3NN^R%ZiL4+C!h}Sf1BEP_`W$Yl~-CVBBoU zjaFF-pYNUWMc8V5I9ud-bqnmajhMMOm46B7QOnxeJj{JY+7!jhxM zUXG#~_GB;WVpiycJKCXAESB&|cq{g7%4W&M(h9j#v9}?Kan*voH&oIZ1kxApbluA@ zE&IyJBmsC9KPxmYcjX>J~`nV1d-GW#CwSIR_>q4@pMp-_P%KD zho`S>8OPh$izqPPuBg|yzGt2ctrA_X&-Byng3iWT3ovCFZ1|~4gc$%KjVb++3km@N z0noD>5OjQUV&d&u8@%{UpkmTI0$!EtDZewu9+50)oXZDtVaboj`?e#K<;j(P25&{*d{ROu9|2D((y1&Xh)l#ih8EkWj zVATT2$!YiBI2UBsglxX)Ys}02IS~U&I;sghD7qcZ*-+ZwBj50CBR7vDz}*dZj?U%k z;y)Miev#qqG{WhRFdvV5!Bfjrz4-@4D;IKNSg3lhI$*qC>5M3ckUcQ zzgP4m83ii6oG>0A>-ueCY6k49oH>`oe;lDWHu?QS7MxaYw42FoUYxHIUBby7|8)2YAI}kio3JkpA9%jAY(1JQ2x$XD;E_xCzqYAe zSCJ+@&A*I19A{Wlf-`$0Dc?q>S}c)Kvf|>7X?7q6qapcfKONV0Hcs2N2v+hBuPBs{ zqJU32K;(3+uH2QLf-MnzDrx@OHUtnj6tilGn zB0|yR8;vyjz)CM;y$86>Q?LeV(3sjZl%GOgd)_;)8_JQSm+*dE!%;rTpja+zQ@MQf z9tOqXw9&_)$$<;k8pB}LtQs@d%tP}1OO~uy$4S^Cb8)qYG{fZE2BHmARm_q+9r4D^ z&kywge`%jT`j@*2*{8m?yCaj4^i4zCQs((0L#iD_@ZK6%4PP?2o_`{Q!Ia;x$Ax9Z zo)SqsbT`7tSuBD8q+7o0J|}z?!FUK1YM=+n|HWgow(Szcpsjhpq9#Er)9r%zX$@z>o-68 zN({NgH<=REp1eXu*fLY`pG6G$Dsp_StX2P|FT5>=TnojE8elsLK$Xe`ZCV`MQcM{0x3+%$@1(<@81_Yyab-2cE(c0E~ zP1*?*HI%H#Jdn$Arn-Y$ryWF148{#(cafgfOd13 zsFFt7__ZaDC5_hvM=R=}F~=pzL5Vd)tMbz)m&}&Fh3RDo<4u&xjoqO%ltIE)Mf3ZS z@94~k@aN@@k%*O8KE%E)sKKW4lbk~X->+d0o+-kCPkvS5?Dtjb>)4}FeGNm(24l~@ zTwfm>a-4*>k!A!V2RjVkY1y(1FEqtoXppbr4FBNXYzG^o+6iQW(oI&Hro^&E<&sB} z9vwe(xk=gt0w)Jh)^UwwP0Einr|P%zK)#MPe5dHP4m5u`+%Nn96a!O{;$^dIwk!_j z0WXMQ?siVm6jGRR`Eh3&aiBy)7btYfO9k@j;-bvF{S)O;j1G*iCpVyU0Gq`E4Nuim zM$lW;|H3%WR;%jP+s~kjT-VZx#T}_YwGTAt#@a9K$mhJ44VODI=F|+}WbTZ+c6jVT zp|YPpCxPbX_xEj}X#_y6e^Dg@fAp9Q9(<$cy(m^zYT&qQq-udIFQ|(Ywmd!%uu)<+ z&IEnGN1u?$>#6UPh$bj+)u=ZJD@5ayU^4Gpj)DZ??bs!OEkOQufmCg(nQ= zL>?fn0qSDJbTgv4JlkgF?yr(RRgO1&bzb+!`jD@BWX!^la5q%cvd zdc-{S2P!9YqDih}EV;zplL-Q1k8{P8ATYkVTT{*r$u1(|x6;<)+=lC6PH81fX9HETt6rM(ylf2*zu-L+C^D+6>Rzh4#(*DS8>R>iBQx#o8k^xNThi?AnOf;QVGkTMks|X>|*d1Ps zVVLxIcLyNtqk->x2?Nf-cv& zTd}TMQ~A%9>=j@5d$KKPk(xU*GIy$Ix3vD zIS^w$od#BPqC(*@%nIAP@lQC99-Sy*;u}Af<#ivmI&B-^rUlAGkbH9XxAeuj7d?9}L(A6!;`duTvK1c3j|_UYoO5}+ z4}~n7a{UD4lb}Bw%Ufc`-Qy_x;J)3x*K1Bh3+vpOV|;>wpo4kUW~h?0;{&iE?Y5uE z?V>Hvct5HojALDq%@d^yUyTUPs{i~PL9RPzOQ={Rq&lWocC83BWhxeNmAVIhe^fEZ zyrN5P4EbD^@_2+?jOL@loAOHxo%z(}ktQ$^d|L6gKgCBXS2;|-e?0SuI&~zKlL%@c zB6x`dRF78u7n$Kl5$>Ep8z{^Je6`uHY<%VGNI0H86zt6L(*M%)D3CdYnKVWwBO`-J zB1dxWp>*Hn_Yd=V5z!lq#w`Xuzwl*kgBtgozy(aB1Zkz;PJtwasKq6A&F5DEjDe*j zo`5N>+AffQ(+^^SG6m~I1*gqLJK5Sb&FsVB1GrvDACy{jwSIuFsQ&s9PAB|q2`dz2 z@NV+x_FD<(rteSjEOrmmuO#`V0A`zIPt~f_#{sHED>aqI{ogHWgWn?1O0(PFf=Tk{ z>aUyNIElMK9IEpnTaJ6URw4q#yzVqlKx;pyG%fBs81&|{*5g{-!&2uI&2N2(FNZB7 zPd;W%kuBNW*H8bLd_1Dh^e*9|*Xys&+?3Kmksp?AzVb@jR`SDULa`RU;?!GbCGu}# z^nfQgaYCTrAo6IvNBmoj+pO?6(Su!L-se$v1~KlYSni0vJM%ro!LC(R<$%Z<^14y{ zI)lYvY4lrubQ9z26)hsgIUL6PRZgJX)srTIdNe(EaWC)8Si0g(t1zVtb4nj<9vlSs zeTxJ$c|<4wJ;TzdI}KFu{&&M>*9iNidN2s8Oj0@D>{fP;9JW3oXwYZ7=-Qn>hfC6& zMISY%Iyd>eoIc8^lH;^8IpC4{gKYCcq}y}nX3mdusxA2QCRtQz^UES|q<@M2&zi(P zO+zY1?gaI*q#mTeD7SZ^`H5gxKA=2`%h-Nfp>eYX(mjuSH8Z485E&g+;=7Nn_Yu87HgX6^@@KqjUnb>Q}p3< zg?~pC?jt4m*D0Zlc2aDV#<4(B4Fl_Oj|q>eAikimP~C4a;H_k?YKeuX6@^dsRd zVzmhbM|mt;8ghuTU(9=ITfJA?Dxx`R4VJ>Zn`0;}@bx;+Y{ZV^yL$+IZ!YacPLNL~ z7L<-d*G{R)HHn2>-mVr@A+l1C1IKZHqQ@}&Dce2`2~21H9o%SXcT~4W`KnYxA>8sY zou3nbFi*i={9cKdueZhrwU}iyhv|qIHz;%`PPz9tkmC1KRS+?+>0vlej>yJDEqCkv zq*cG~1LpIo%JXQ)<3Iq9e{|Z)b8uaRthJ3pip&2OwF-@6&0OO+PH)Re4uuo?+iyNf z7*z-U%8>8gdE;Dk$2}M8NMm6{%FGQ$vt*bu+R}rZR~M+mQ5B1O?iXIe~NT1wya|f_CA^RRc(7=jvp19r$ID6eP31(tcp^ zcjc2^ErTJEY58CCD&>MXAxTT$ai8Z0NbC7J=XA+Qt}&~3@j&zC z9|zoWP5-kT`DY0#%d^-V#dcw!=&vK(fXbs&!x9>m%864CAKbASlqo&HQ0?xIthLnX zAn3#;5&c*KS#NMc8A!xZI})@swr~dW$;*;K-qYX!rJ`}JJ)1=QaM1uyoKL46&tU;i zaa6qacYLcp#~s?_B(jRyFKinqiqBPXOi&`GFX#V-{=a0`Kcl*|)kTo!8yS;##&qT& z^imSxa)V!4Qt3_P^3ldq5ejCo2s+o5moA*e*sXLaXU!}rY-v65QFDVr0XgUK)MDwv zh6G_DQ(SPs*g*B$M+8&0aBk}UP-0s44d0PTzy=i{ZPa&QF5pbGW2hrQDAruTml&|~ zx}HK@VL#T*PS^znWJy0FXa_aH`Y$K`PkRZX1J3fPesU$_eMJ^$&(QT{r@ob4#1Ud+XCq6*tTuk>Dca$ZQJbFJGPUKZQJVDwr&4? z&VBFwdtd!}^{Y~~t1{PKW6Uv!_8f|MW$2MM+0>|m887v@A8GwI-t~;d@F#UUJ8L$W zvG+9>ViXaF8Dc=)J|^-mM}bU#Q`=M)e>iB$Z0Dl$7h{oZk04j#?~OIC7C+7d_6NSQ z(rsNI+EmZhpDr;=Ey7PG|@r72J(aog)G+2IVOrAR0^ zo02rdJ3uorIVomfkaz1n{`u((zy}Zm3I%VCB>#CUl0P+w3KfWo=pmxud+z-QDy6*s z-cWSv*Q|AiR`L|$#FvG5eIdx!)hdOI8L+je!OHNt)xwq9ETxf#DaviVpXa-zt}emY z*x2;qVn{166=K5v+#b$>F1!T|>Td{OMNqOkggW{#zGMzJ4@-J`yMvLrPEg5=dVrHa zP<5ru$h)*!L4Jw?9>T58ZUVnz)U7K}kVo>jM4$547j1Dk3=@h1Nd@)W3KIv1+FR@K zogD*maV}LZQ!ZDokbmtVtOw1X8u&MzGx_7)A&CoytegOv@YRxvC59e;|yiBR5b{^ub*C70ZwxO$C^~Wnjt$R-_00=y>78#y6Rg+(waOM!$slG;*_k zqu0Qm-a_XP6eJ`kCoie4HruL;Sy?)FdgLGONzg)|{dYj7Y%AU8M>8`@-q*d5>J8?| z5Ri~FYFg3Jt)!ftB zFffTa2DrGmD=FtHkMwWq7jrB|)c=sI8WK5GI1fq|IlaS*YAe+3{n^2Kr4Bq2lW}Nd z1gZ@ex8^qy>>JCTLtt<;!PO%AP6j#b&C*_h!nFe>NE7*5qRlwRVl(&Kc0m*u-Gr>T zhU=nFUtLEB9V)Uf`#)GN8WJ4dc65y#Pn|| zh}SjTllR>ui5biHC-whiau=L)y?(x3(UO=xp06BVT!^WtpwiILBu*Mz*0a9>@Grl(B5BTsO7lUHfIz%hBp~Lc3<`Ysohj4{QgS z3714oO)aUQVCFE|WSUWbu>yGx`w0CO;xo`?m{WU|ty=;0H)>SI8{{B&%s`b~u?O&*bw!an|kgy6OxCmh0~prsyiWQrlQqC}r2G|NIfA(TW|C zGFvtQ7~D97F_AiMzM>mOgnZl{E>F2}qq))mU&>nRvDx8pSZj%1S8dz~N$!##mlZNF zg@BbT`edj%b)sEqvNmdBU(2+a`bRweA>hJX3^EWkG?-vM1`L1dcXFUO$G)+>kc~R@ zQUYhAe4vFQ5)L)ltkqa9(9o&Z{H0PPD`aL?eCr-{TgT#jaI{-^&eoBY6s41P!4lDiOE4}$G*=Cr-SgTI%KhSbt)CK5x%?=&OSzSdIv7te+U)RnPlem`4l&zFT;ak{qAP=` zW0{GMi_kccQjf!kL7X$ZmTKEsk^kwwNj=rK@rvTTq7=#awsyU6@9WL)$J6EEBhaN& zi?vnhg}Y_5nQNWgzk5|p3D_NW%Gn;aQoC+$ce{Qf^wq!h`ULpxv1KO5>=>lIz#jKw_0D6L#f621R5UaJEM6%&kUH`dhi1h)Srug7iP$kt zW!LZUac2WAT3Kmft!dQ96VejMf*b=q6=aMq2TcyR|Hk?TYcf~7hZbz_o*Na_v3-?hI(X+|P=F6O% zrc)Dw0q1b{CWQtxH{>X8)y5P?&~a`JJ}E8StU?+9&tL9rq!>BI1KY~tY*obysv}n~ zyr0o?_L>4AF$a8HjDHgD4p~|%Tv1{lG_XrX#zch{`-91rbo3-0{}BLN*5)jmo+kZ_ z8XM!2^8XjfZDbGxrj8qlte@NuNA+#(`Li68-I+F@$*Vo`BOV*YG_E@1f#a8 zDf!Cb$AG?v2e;kt{{5$S{I4MkE_%ZLVMl^Y%%f2cCv0tulu;8W{1ygn%Bl<+verM^ zZnlBIPn&gdTiWKdwLct4LjP?$4(CFEq}%cMeD^rOj^TTAyWSQu*~8^gxUI;oYd-}J zp-7Z)tI+Evuv)CxIVc{Z;C_#i)CIh*T2=2Psp%>W5DA_dEmmmA`c7@#*ky*y-7%<3pY^%ywmIO7Ny7ZgGxGl%`c(Y&{=skW9FnVsA;Zg|Xx#T- zMq=wX1uoC6v3yqUm+RbJ)p2IX9&>oUroZcOX{YD_007Gizu$1T1CIRo@O5n_euh2hye3W-s zo?MU&`|r&C51aq}kAKkn$IbfNJ%N(_uZRBr0Qz13xGevA_J1$^#~1$*nE&s0{>PI0 zAyCi#&vQ^<|KR>Fb_PQHkEMSJ;r~4Qe}emuFa96z{NGDpMgIdE2})G|zfSC*P5+-u z|C<*72bunD3F!O%{{w}X z=S7kG5jG(UboJcd-33bP>FIr_1>QeBg+@lgYU}6>%Q@L)d$K*hktTkm4$Ebtma;mL zxVSny2Ze?X2LZBLz(+` ze*OMp_P{&Y-*;BvIq|Dd0Zy^rIXtBJxMIHbv-ThTRyMn_;iFw<(b3VNZ)#c;LH4nA ze}~Q3?J#$9CVFbv5>>3U`Ru_&=vq-+OerfXtDY;EbbGHiykoTX#5`u?6{WT*YIw%{ z{<1&r74?~W8=gY>G4(yG@QXR87KnwyYtFsk-)Okg*;-k_R;rHgQJwfbEf3AlCyE^s zc78E)3;0TXd3pPf!uXNwN7U@$;o&{s4Nw_N)PJ~RsGke{0DX&_h5g6#K^t3^LQiE0 zVPX1n7|xWv@lmiQj2_>;y*!MWO6N==%#a8k-`UAO)yF(6GyX+T6X?;DFbdT84{idg zUk_?WMv242eBnIhAXzpy)3?j2vJ0*g;fW7$%PALmIoVs~-Y^fojtBg{1PYZMg3e!F z=?DBGk3J#)y@lUjQeS(uT(GjVwB=s6iJ!yy5#SUGK_4@3gnq+&=X$YMd$6(?OsGU^ z$tmuR`Y{R*S+TR3kp=6K(u(D14fwLl%LF-r5*O zCXHj+Ail^Ukh!6uq3x@4McuD;U<+N~FIuFfYQ5p7Ex(v{x2xUbnF941{ck9!s2bmE z6+IO-5{Hs`V}%)FEX=A2Sd|<%&J#P%O8~B!j0^tVUnb7K;zQ{h_mE?D9#1F#ipc<+ zrN+ko`rDtpJs5vJEw2WaAnEO1@AP9~WBbR%Ag*2Ynkao*Gy1GCU;M}rbj-Ri7=Kap zaBbp1G-v!O{r}fvE;>O=XvT#cj2PR}VRuUKs_m>SGXe=Mwce(+ zvq9B(tuSt4j1dKp?Pa5n1V1mKWA9$s@BGqTOa*SE8t#rTff3b{3<@2qi(aJC)EYK>56BRbzNVFZyw_Be1mw_ zZgRYhhL<-}sxQ>LV5kK@E3#8wxH6bmSNV^%tw9U%6}rEu{pM*dc6prI@6Y=l%-T0t z1r#szQ?2#LV`@7nv?&`A6w1C6c518r%C=40WgGs)&@;78hsVNlu&^jE z`keXMv6_|uhs*@#!N`ah=Xu=hiiwJe-9R6FI!Y3ytxD*>93~`w(yq5UVY-EH`98sK zT1$p3uA?2I@!@^PNJdL-^2XyG2IHTCF4HJ$PRE88b*bKQL1ml05nTaghLurr zV7Z{NrBSEI(xk< z*i5PL)WgQzx&|-W-RzI53&Pg_ppd*> z99*PabSTWV%wf@4aDs>u%UB(k%e@Pve=*M^P?~H%+`kecmTh>0AQ2#gKpr5fxQU8M z^v+E}YgY2Vmu%#H5_k$U_$P!4_6d)4X#@Ok@$vYPl2Riw*R_9Bi1quNCud27BNlZg z!1m>n>|E|XKi^DxC};HblWw0?t$vL@2{uL6s}c3}2b6p;TtD9CVkutS**n6udAyM_ zQ$y#W84>R|plLaT!662RwnO&y1@2s4hL4OWMz}O;H_GD)>KZ-1ntUkSl$Ya@6c}yq z85S>(X1yxg|KjX;4@5q{8uhvC^7aYnr`qiF{ltYe<~0in66?u%%hVJIr%_@gNA~Tisfi3XG+N4k6TkF)x5uEKTzfQ_EbsM zVpPo3RRCsmduQ`9Y{X_j>#BmBxgCOZ65GC(z4-P*X`LzTr`4j-1uyNiW*UMFiqK{6 zNoun@xN5a#a5SNJ6mPS@aNCmS^-eLTy{|Lwrn?pSriT}FnNxw3x&{$ZBaBRP^-!D3 z+0A3!l2Z1z1jralY2l>1(}QM(PCJg%>zR}&w^8q5L5r^??ra~Pk{}mmxf+9rktx^I zB~AlABrl`MHn!{o;klPh$=nH=gRYud@v3@lTpRJ9e6dKhH=Rz;q^t3Lqsyt`39#1A zJNt(WLi_tpH^sEH@b^Apw8JA3YH9)%47#CAY}Ok01Ir4X4~o;s-I5DcNvf;9F?Oq) z$FadwPPcCu6UV}q;=}-*_UI?osov}O%H<|cj*g3t87?-&a_Wop#&V{4?Z0@JgwMI1 z;3*$%`)8L@^yz&B6*IuRa0}u0RsuowvOYf5ih-VqrpBtFqa$%!8;_!vR?J~Chx2Kh zl3qHD1V(hn?a&HnA-&J>l1py68xnmX_4PG^$nZ8&+v@7HAYar3l^)pOfst_urp*Np z+~Yh+&p0Z#3;ywmwODx-;zd##Sw+L%V(`~8T-2!O)io54rWP~Xa`e2to@h}DHLd4r z`|I?YZX^*WWRNI%epM%?Posu?-AvCAFG8EYK?6ImmZB8_y4SXb#eSofgg8aDs=pl8 zl$d=PTy=5GMfZbK#|~mt*P>ncpx#j{=o7ArCsnz7F2m=;BKEiMu!&Wv&*lv{7T&Q? zNg#M;U_GsXJxzFD zd2r*BvV#{1ZY50GNX{F7H1T-pS1w8*HwxyQxHvxvS?IunbmALW@N*R8|51&ZtZ~pu zyuG*1n`qgv(yK(T$f79d_Csn_x=hT0UvoLfNUrsC!XE-G;o$HdPie;#DxKLD|KZcr z>ZK<`M<-9Shm{u7F+$h288)3w!bWM&S&Ck(RBZ~v?EIC;- zXaIBXKn*jWBjdKVLD*Rc7Ker&5V12jcf?M zZm0~vSegfZny~_go^>cu2e44LYW@2yVWUu%)K5x4B0L4YM@Zb+ME4Ds>I>-Ps%mY5 zA#9_~>5#}y>z%{HE_Q{+3&x^v4epU3Z*X?UU*;n*t_T2oN9UlVR%qcmD`wAQ9b~sjQb@cD&@}t3c+_cvFLc3slY?bN zl|gI%)ANGLtCWCWDSPiOVPQUygJmN7 zy!ci0?HZ^dn>+!TNQ7GYj(IfP?)Q3EA95eQz;T{**7trYpJKqCKRsJtpHHC?G8DyY zH3p!O3vc61O}}c{VJ8xvZ(q&SKVM~1Ijk5=6>LP5g%|yoc^MawAp2{^$=1w+{ z(E}dsmK$<;g7sjL;y-(fg(6gPQiFZ)1wS@{hS3uWQ=r?QS5I3B>=2+}yG@IdF$~OC z!F22HUog5NW^ejy?T@$rSq}a{@}!o1lIdRx*4Z)3i7a@Xj*y(y+5NTkm^aPczTxO| zuh*xqG4?Od1a!JL2!$UwZ;Ml6VJVMUyo=3YZjc0wOr0Vq%qW7y0>P_ZQI}R3H_6J(tE_)~d zLcs`NPz&A6jX_M<5c@H!J-j_9r}&RAPIEU2vezjnT0##Vum&ZAqa)&=U9dlGgLX!7 zF_d=eZD|~Uq7?f{xAW& zn~?Kz=qp;!Z*q^|xolWs?28)jX<+pUEygSU!V6}Y|K*YYVsA)L{51z1nn0pjvB8DC zprQ!-;vs3|52Kif4(VzJZyfImDh5Wf{SAzSj263hmb-pVRNkgInekFVI}1D@8Ar4b z!>FVk=4WUBNDQL?&?!5 zI*ioILCJUy9BeeYqt%+Cn(nW%B3(2&*TBX_e0m>#D)|^8ZO?C-?Vi&*3DCT>D(-{( zxy9AMa0+u66fBXDsYwq&wZ;ja;@d{>;vWV!wcq{HM*hDBQ0^aH3fhZv2JCS)*LbD{ z*`ij@vD{xn@=EIweSCb5Z7Y9uw?jtYO0p$D5#nGAt4Lu4kS0to2nO}$5u-p_*xSR1 z|1{!VUE^V9l6^rd>fKyLSrnx$R&;*2Ct+R3j`L_t<#vKtl|kJ(+>5E&(fvJCn44Fh zVwaM)7r)KRjR~qEFqSj^w^S4A~y#U#!{=)#|I1NR7lPiu5m5iTiyS#4J2`KDXzzv@|%jt0fK&RHzu`` zul-Y`a6*+rsf>;TPvbaxFgv}pz(;3g7_0;7*U4}ycm*EGv|Q7qe0TwtxrCYh1x+DPhfW93h3W)YLVM=&Y6T>~?BhgsO z*nDa~I|=^J#-C`-JQIE|2%DHjb~(#1tf8 z>rhyjxr``^5aPSvLYU1K(GoGybMx!qb_|q`&&!IJMoUWR#eeA$S+05JJ$F~ajB2zV zXK{oMneha9dKfsGa=*U5l~!ei!QTr1@FZR-h6GLI94=jqx6a=h)+kwN%WcpJ^{f+f zb7EhwV1t?}m9Vj(QI51!(n14uiY9MPFl3F~t&JSJ5*AaTg;5sX!AT4QbbY%OLVPV* zZ}?3%-+*8^1hwMw%loG$uoEfHL^>y;N}FX|yq+jX;;KSd<%CQC{ag5{$sTfm=B*Ov z!O!e8fON!)FHcuz7Z`y&K$4+ExY=;Z)>!PVZE=)4ab_19VO175VkFgN7Vq5--%0m2_}ur3Ek_vIP}W#Y&Zz!k`Mv!U74IyhXl3IEU`sJubeo1OMZr1J_oGyEwI zIuMVEic_?}#~+ecpcE;FU;Ynaj}1X~-qcO!xP##d#Wl7Ds3@#1%db1Oc+2&KTASJ- z`1d@X#%Q6gc7w@Tggei*3ClGlkp2dv>YYA}2!sTMJk{ccxv+GjF*vX?U=%3^M?0uo zE{AXe+D|ZbvpBV12{ONx{^(nJAw)$-?Vu+CJ}~9;+4>+caNxE1=9per41OdU8~nL_ za@gKR#^ddPe<=Ngnmil2>>nP19Ii38#6Pc;EtT6Fj%+hd6;!xx<96_GHJc)^%4Z~( z|9v&~B3z`<)e`+w*5!9Ih^cXZOjyj7;1)UW$)*!!EI`2(AN@+RMe38UK$ zPtVT@lhby+=OM%Gda#)HX7SHQi^m&=YqcM&%+`8ThMsS@&svI+@q&T(*TK>9`(wF! z{l{Njua6imu@}$#tSEdLnEgp0l}&E=z(#0*hiX@&C9`G+&n1$U)wNuw6M65iji1hH z3%L&0?CF(W=+z9K@Ws{ZLuSPS1Bn_LQu7-x(C$s z{DSM>7D<)?LCh}Z3#uX3g&HB2gi?#B?2b9(6k+6SPeGS2hvLA2WxK z3JCQt>ylc0k1$N#(0gVXKE*@cOqe_(=4wFUmDe6_G|E7_I10E-3cbF_x-*Y-4M`KX$RYN+h95Ed(#Mg%Gr!@! zF}m0Dvk@q5JH4}**NEwE@lmM%6EdEm{+8|ktp6#^O#h)^RORm`N#aZL83CfHuozen?TH2 zmZet2Aqg;`#^PxC#`IN)oq99Nhw?j(ujS|IP(7iqP%jibw10CbE!}VxGGBi$t9_S} z##n`kxG)`bm4WdqucA(c()4n8-GpMtbpSF*zH)B)cu4dR+{{YS*tvy;1GackbS@J! z6pm%2uNOVrA=G49p%KguL2Ou8wksHq1_@O?a(@;)6fBg`+=z4H5g7>;54wmCeApy| zkeCwBd!XSouMf^F3F!9eDWIf5ST>C(>^z98!5JZ+$BX4XfOLkRFRWoe+u0Q{mx2-D z+YU9>v5in{Ir>DBS55|EdwB#8<`wvXu@)TdpwIp+Ksj^^Rsdx)|BjN_)+G9 z{asuLUpz!2HUTaAKETkJOsuC*wtzEvrl(H;ukDZg{%`WUPB)l{-9V&F-QcQ2&3*Kw zf)ucr?ikEET3n@{v0%}gH;1MeP=N8?>+iHLeUQ*%563oD--US=Y{rOYNdqL!&Cz!b zuyuYV7b!oBAI9n12`ftSwLzNZb}^wtW4BUG$OSYG#T)iSS)&0bF&RF;K^D|{A+cjC zjZ|9+Svdet?{<1gL#oS2D-H=Jn#^A#w5CSgSxKh=g;hx@9aQiO${9gH$k&qu2A600 z`>LqvPgkGAh3Nn*lJs-QrzzF zQHlglaj6V}M||(~ga6(z-i}l>*V7AkN4+7mu3i&HjxJXqHJVqo9Z_7JAGk7!N{z$Y zo4bc9p(_Dj5D1~#gkCEwt?tQCLt*)}_@hmF9bU2g#o+dRG*_(;vNDB)!C^f7a?`1c zx3ec2c&&lCy9?(gVQFw>YpElws{Ih1x8oy|91?v`-2$#T#2pp{ZA8t>fCt@KnW(oW zzbhe6gv+?8;oBqn2=$D?7vvqY(}OGCN(YFP!3~yX2eOP=^}r}8A&FldD1_Qo-)qWR zuE7S=Cs(*xmPUd-KY?QxXQ-bsx4>rix0!FYsbUG5yjz)ox>Z+i?^jeh0?lZ~F0R1O zM;>RmivB@TvYvWC4`3Qk?o)*-$Q|jmUS7L=N+_1XSVJOg7F4S~2KzamZ?*3*U_bIQo!zR(efd^> z6F-Ttw^#U~%aN_fy_ZB?gXC=?so#7A-oM$7$h%{mslkx-a=kNJ1QJpID3sTJ|M>4b zF&xHfL4P}2KR~~itNvN`2bYHQ}L!xZ%!t%fo_Pg&!~ni>t*J zYU*prPx$hh2Bl7o*~gxNxB~iWPFi zRaG&XY&XgxWU@Pr9dUFy|8%u55+-2keRqA**8aGZz|(Anav-?8MuZFKj)8n9L9P9o zO?h_r5zKyz#MkxkLsJ8E!~~ZwZ7-&9PV1$7L**r;TlPdyQ?UK+swp5|y-DHUibU(0 z)o%u=MmkFm!irhl+r>|~GY4!2e6c=kUX=;h7_Qzq_PX0PwF*fdgp}0R6;}&Dm7Tn@ zE4M^M@ct||Hgqsw?ut?HnGw9m48W|V&upu3bf7dsyQiAx#@Tp{eg)3@hLy&1Rft<7zl6;P( z-u2A)oMcS@rgrAzk654%r+mIiAAWs>XjERs@xVsA0^jNVtpmd4LgA;C#i4x zT}>uGDMiEMrFUuLTM-3?WSO;ym(MpkO1XgWQqK#H;wG+wGMm5Vir{i93vlc8(nF#~ zyP)K%#dRETCgkXqW_W|hECZA35qX>+!lL9vM6_^E&v2Pd4%vRkPO~=Ii<@X6(y$8W zy1)vXT`?JE%H8D0QL~)##-->+R4pR>hEUOJ+%&(k6|X60mqpi#ADJvsH6?4;!reYO z4vc8hZD2o@j7=`c0F$sYgSE86ot6n;?CwCna51T2^(4UIHDjCmCBV=V=p)fL_$@Dw z*xsowG_sv>R$4L&09VkIM>zM&qo|@5Bd^1_sx>zK-4(&Hc_=dZ()dXr@poZv!7~^|xA6ZgUrie9@E=Km9g#=CABHR+iixYS;_ zRLrIZ@-FXE`Wy|-eL6;9p*46B%=nsTtJ6P_LZtZgUG0NY+F>U~^-RauRX~kY=yzEp zf2a=j2d!F}5=Lh?QJ^I!<+chS;(XSx#)}nDuTvfs6y_oQ@ufB=F))_vk{P2HF_YkR z-q4|vjWH!I3q$aVQJimbjzv71=46Ws7}b@esSvjjC(;fynpx5Q%PL|R0wooh82R|P zjJ#dEpuQd=kBrYFxjNgLo6U%)E++`~x`x!VbfPxfvZ$-1LI>ZP12d?qBUums^|1&PM_s6Y0d($Y$j>hUR$juCzI zB=|U&=l1r+8>PraM}@h;gj=Cvs};1hBZL!EuD?x(Fpr1{O6P8? zcTo=5!tR1_XJF++{kgYzxkM&(ru!WfkKY>^4>bnB`Lwrj!qegBE?5@-ULsHwApkX& z8O&M$Sz~`GlPc(j0i9?699+D+r_zpmVCe7nTz#nC;tPvF;7zF2Oef;%fcZG{?7$!0 zsdvISAo$*&lk0*&gnSsP)^s^s@n>bZ%Y=7TW#cA4Jml*auAO!%jI2O2RzvjX%O+<> z6xd;V7~*5kt;mss{qUQ5yDk4CA&)e}j#BlfL|iGw2`S_DOW3SV4ID4At(Q$*A3D|<4P#qQf9US}BS+LF;i z`lEl1Sas4mRpZH#)i`!CtI-^2+1H&foVVL4Q5l|* zNP*$$HxdSCkbU7&)b5_S+ozAAuis!`VEV%Vd_LG>#xS(Hmi;St%$0};7RPuG+4&!w+tY<>TyBQ6ni&|ok55~>{Xbb}m@eqOZyq?8Z&&#`T74O-vNrv{ z(Oj)ZU)_uX#Gu@!d*&zB72D@CYBhjapMj5iCeG%#yn3_>E517R=t$fnkQ zPDxv^7CgyD#s2iJ8W_HUOs?Jxn7Z-~cPcoG%Sw+iTDsJ~gD` zj}IznZDbwOY*$p%2p$R%Z)l94uCNeR@Inp9l0v)b>D?rZ_8~2ymh^Yric^K%8X=>?>l8oV>Kg>{V{=WP2u4-UKK%+a+2B)5?NnBrn zJ!ukNv$RT*Gl0YB=i>4p{);e@d2DR}Oa9x;kDPAmXEokU>lkaH&zSK7&-uRb zQ4HVFgRu%tO;rKSGp9sBp2SpUcK@|~kOnr4c)3|gF82O4*1t|xG_LRL#T^}3#XZ-y zO>BZNDKv8pL!|5Mq&gVmBP^9tutA=txsEkq^IV{b{8vJ*W9_qhe}I`v zc%hGJt(IIC+PmBcZ)`(38V1CvtVETqOL9G%=)*u$Zfb1N(qS)%y-?9l2&bm-lFZUD zTh*W&_-{hN-G@elziooyXz4W^Lny`LA?QW)E+=|N!Eva-HRP8aQ~FyNTaWE#ftfPD zkU&zPbyJ(Tl+cZm?<~iRB@%J9hvu1;5PD!+lvrH~2{qu#vfG-U*^0G<@|LY>#^Iue zB{ty6KHiDiYkxC^dm@<~7e}^SyhJuXvGp$~1!pNwpV7?%=dDClzLSYfN(jD>a(EjtPVhpU|^v7n|azep%nCZaP*55nXApY1^hq zZYaFMFU3xZ9md06^{$=2LR9dim|pbzt8}m?9)y3KS+zV5x}58mu6hw?A6lFFrb#Yi$|M zM=09F)~+>0Ad;Aa>M^@|^5m*}0*9ye)`F+9tT6?uOL$DQ(`T*($e0lE6-}9H?T;+z zu4sd+-Kb0%HN=FU@Xz&FG3AFCi4>Z@Q7$1Wuh^>Boc@J@=Gn{9@E76vM)<2XzV2z6 zsQV8NSG?A+i#ut9F(W5l%#N-r)QU`1M6)#_x;00%4BaX*-+t?J&vIB*9F+q;>pj#! zn_>nd4;f@Uy2?0pCREHnm*OTeltod_M0|WOkxh90y_JM}$D1OlYw>VTVcr$qUeqik znb-NXD=jt+O{$kWKdPBgNvuQ{BPy^pc&57BjhAYR(LV5&ztavB#l5p0*cD;b z0W4=Lx6alZ5VPe6D%7+XR^|mw$*4W}V6{||kkv}Mo_r2AoCnlR5+C_5NYJ4O00brgDumvGmFXTa5jX=)oRA>7|_X7cP^5- z(u@&z$#Cpzgj=P=5C5O&oxyZDg}+$Go0tkWH;$9$8d06yugNjl z^Kw;^WElPM(1%&A&k9o}o$PdtK5tdFU_d&PmOv|38D(<{sGg4D2bFO*X~i(~^A4xV)mDSgaP`u_`}IEc$5d3#oh(RQM-}h}deYqL?7Ni{Xnx>& zq7&!bC?$BmO6c@hx0`cbAs>DdAiKo;LR>)&^VQScbpqqhXWa4SnS`1eVR)T23d*A- zgRQWg8>X`lnZTWqBvcws6Crew6h+ergH>;09swJB<9CTZO4cDl27}VE8y+3TbXq3^ zD+7=MDT-?ZIdK&a7&hAh)LI@m^6f-Mxx(KwSq&SJ{M0~Yq7fGdj z_G(p>uC~3$ILF^zEl{QJQ1neL!{Z-q$-q2%R!3psCl4g)OGFVDx|4!Nx3`gi*ZwmS zB#rI$_%;_=C5(TByfp&hWO~{m_2c0T$i>zOOWzAC&5K=+(h3!+8IKXk=*=atf;&&14lp^vLDP^yGV%x#0ZGvH-QE*3YTj|q>35_3=| zBo%)&P21@bjFj3E>$xZj@Q)SA>H3cfE)=$+31kzGotsHmp{|ZzVbA1^R9}79vSFCe z%M^WJ4hiRgijIbt1Y|^wg=lhR;Px}Kf!aKvUq2uS%b5{22OAba?+EBpRh)j83P6lP zU&HFxJ`0&0BDg<&&K}n_7WX6~B8G(jdBTK?E0LOxMeVVuOsCEbQBZ64S0i{ZRq^Xn ztfckHvABmk9$R8gTpTo?$BVTd<4X;GAgKzqFKnCyNGTh4?5-(Gx>dcw!G7e}kS7>q zP;PB$rlH$@uBcirHGXkB?LrqZY$Hb@E3F}9P`4|Cvl!Z`IsvFwrqm^VR54N z-w>dkI_96)uE%2|+i)iFaU~e_lLlHI7z^LKF|cw6jX}fNO{KyH3~ac6vB9h2x91HJorjC>rAgZtk_1O z{A(Mz?r$tnQk$e@(=xEviJQ|v+*X0nGCv)!Hg#huN(dPkVSy#%-;smYFkLp5ux z?|ku=8&SQU2nSbYd{X6njyin`mb={$0lppBV#Zs3wQ$3a{YSNiRG5qc#Zgj8;W0h; z7R!%|aPFyJ*h58pRY!MeQR$F zhBo=%m|kx<_Q81j28glNrZxw5ha^!3h7cPfc>sq`i1m5U2M5U7zbduVh(>1c3^Xda z+>O!NEg+r>d+;zsbs?v@?Sx7@HoUAqFI)aYkvC2AZDQ0XRH=!+u0(^=<) zs}w63Ej3+mNmo}R{OsA}hsSj#9L~PTsPWnokS(2(a2?l<0uc0F#s~~mB{^*y$<iLek#Ix_FsydrpjNrsKl37VFCP(!A#vc4iAaJ_5a=lo6m`a>ZMB$zj${T>= z2#|J-5?Hvp_uCWB@%!{ZAo{e9eUE{ORFnr!S1DM}NJ~x*0k&fd=$&$ggM-_LT?HNA zYdppn+he+)#lnr7lifZ!3LP~Ns$pl{`K25sPnjzvJ~Xy?5w}A; zR#i79$X~I4l4OVDb^|LE)D0P_R>+!eWrKu70LQ6CPwQUjjUcR>Z6IM$KoXGGIDZ}8 zsx2trkNNHLyU{ob+g@Y%*@(a0^yaCXHD`7jpW{9h7GLBjF3H7KH=&1z#~8);a@ERR zKXSMi*(rs?kQA`g42QKB7g;E)T;?>er+Hg%#}6yKSAF7D%~$*TQ_ko6elh!*8p6ak z)UZUzPDkFBi_gzIsE4PZ;8tZnay|Ls1E;QtE4U>E8`iCaF_nkCLhPwz2)_(Ty7Fp; zWK>4G{EsV(#h1~yotqb{mk*1D7v}d*v^|m6oLa{}oS|6>u{EjeT{R_BRXqN&Sj_D>x@w+ylO1rEwPaK-Cq?&4`q0fr=`lnCRK#to~MCvat&5 zo&DT{6m#b5;c=bSBRouUJ>m$>6rMwf1hxkF$aqjU!ZUpP!3%F{=$B znR*^L_$=IVG1{`e0L-h-mygZM{S8$}t0I$6cY9ojnPn{|*lH-hRy8g3Wl5-NYjDIY z4Cu1wd7|N0(X>tn5!d2<1`x;GR44H+O346;_>=8`9EN;!m~FEZKGmrtXQP zAsgz|Po9sOx-QNWG<9MZ7Q5H(Ia3(Dq@E47DKp5WElNJ6KdGesWhs^qSEj5PL>^_v8oH|<}n~C965?gAaNO6X3CYL(e;DQ&k6w_p#^HhB4vR@Um*`Gyme$*o2G&mCS{xA zCW(uBqb8LYcA&SX#ns0+O^Be$uq0|zt()zeSj;?ppssr;*c{2cw1&$J{eDdCMcx(l zjAvLH?Dk7=-KMe?@1O}3Qo=3>+_P7>6@%~Ev%LFhJZPA>0$GE$&eZt7HIkTq*UMqX z7pk1eC*l2GH#zt#;%~las#<=0v>r~oI#6Kg(0%JsHn5$#v84qfzgpqD zqnq8qvSXYSJV}_VujujO4$9u+?E8cgzR&EfGr#Jutk3ee_!wU;E<|~&T!E+k$!dtx z_nyZZ_?8Vs>EXu%9pD%b9$g$SUx`v zJIgZL-JWWhofI+Ml1X+-O`)9M4;jS-Td*wk)q36ZLhVkhD0 z*M>Fad#069g_6X_AKp)B2d?fSrO@$8*Hb2t5JPCkiZ^F7scS|EjY7#N>lo-Xn*{yU zX*{pgK1!@YHtaI}lD_j?oUh zRq9o1a>@#Nr57dvQFNw(a+^IX2^~m@yes^993j7RpJlOzdx3nn|28q!D?PUUDnLr0 zADi~@>q1`sPw|(YBebSeQy*2}b?}OTv)7aI%f)9yelx+{!)d*Ar3tJD9Q^R?10ReX zTq0x`y4F~H?X{i)VzXv2W2Qu|YQw0X$BiPN^G=#?zXb=CqFt7B+%x`|99B$9$!5>m zz#ksQmj5z{>4_UyyEZwhw8>U928|3aF39u`V zS*zbz2JV06IMd&Wk8B2B85M_D&yY0O%AY|}he-?z^v-WpP$$5Lvv9NZ9$_P0MvyX$ zwEqsjZEP}`qEPeW-p9zTs#pH+YkA)ebo&6hrp`sbeXU{iFyW;XP-brMUG z^-4b;kDL=r6i)kr_~cza@WB@uHZs-aF<6rX;?HZV^kg!Y%J;7OoF3eDuaUT-MZAt3HaXkqh)1oz%-=1z1VV95qA z)#fm3J#!7}8v})%q(0Kd8F%0CKE(oJpA=a0sbtPk%XtBiWrh}1YUd2WuhatL(LMDW z-58C@6M&+hpIKFbdDm6Rk->Em=Z97Fl*B~}QUe)=O;~7v#7!ckh7|H6c7if@?07Y) zrz0_4d2O1)wAb`vc{DsFgenNgf|l08P=g0^xn*1TD8oF9I|u5?DzGiE9Y7wyjz@3{ zn`nor8v~;Y4(5*--1toX(tpX$VwqB0QjkzgWOD~;%@$in*>aPv^I1$?w21+CvB%4f z!|AmIWJ7-^9I0P5)9|U%Gr(Te00`l?z7C1UDUgCG+YLSy8Fj)qPyAJb(&pkEV>l`# z`yU)|aibq+^Gv!DteMK@CzwXs`m;EqyY0y;!JRCw+4nrE?W?M)z#_*R`*=}ua5O?# zt)mW&{EKNE-t4rJN^~1{MqRfj%gg3cmJ|V&eBO~9*YYL<3L)(8O=d1!mt&uIrgOLa zxL>~ER_H);ZJ(l7Bj-4PExc zjDbtG`(nSTMY2K6j=|V}`zmkI90?&f_ogtl(5u~{M7tho#}3)y(lroU^#e=c*UV%&Jn+%>RxOI z|4k-zGvQ$GWu_i_sV|X0ufgr@)*m~ACYQ7@eAgAo)Zly1d}csC3~Ih;x*YHGa5_Wi zb8ZvifS)hhuG=V=X{wjnrb3k0i*XrMl?!xQVTn16Liu5FsDz>NHGPnFn0z9_JxC2s XUeOj01s}!yq*)$+AQ6uag%_Kk^E~e#^?h~gR!!AR zP0jR7cMlrBa3uvvBzQb{FfcGAX(=%kFfa%J7}%F)Sm@6lU+!LiurKT3(qh7D9^j{0 zP>Gn5&-YsH?wjI6>%UMXa0d-jd)7zLC=l=|zJLeT!NF0G#+l|DtgQzPLb)UKtS`L3 z_}tyjc`M#!b+SE7uu?cgIQtze)N8nI0$)y=%G%PNkh@cccSjPsUZ+HneWAKF^!?xN zsyZJwybaJ7{^~s3ePBio7}sbrV0~6B{6QPSAGyD#Bo8Um%>%1QW{ly1$&5OkH6}wb z_LaIV*E+=huUGD2DD+kJk-iIWlw^UxGS^v9fN#Zy2@$_$9It^Cy#Pfq6B}E@c>+YMjME^4K zAAUq)a~1!GT+q8Pa$f9|te7DM1ZX&tmPu}tL8tLlawW`fJi7Da{e^Muv9%Fj5?Q{9_H75! zncX00qj2^j9TOIoIE9kwBstGZtdu@^_WU=IPg=LI&yXD=@xqfz{j#J4EVupPmE zB8qucQ#$E2$hfwlzQDNytMQ?%d_8|D&*f zwuZnTC?H&wYd{lGZ(lGYcu0DS>IL^E<~_Q2$T*%Fm^Xfi`XOju7CP96>HTmXE#Uji zrE6t|?)*$?uvt{qW~=z3S8b+4m1Do0EFA<~s%1<7tb8+Bok~9X52fI25re!Yb`y9B z!Q|7Idi?uuv%ZrmH`mvO{|aVr=Z{{STO%O@7Et^JU-VxYA5!09Ji~Vsrn%sPKqFkE zImvwj7IN#UFy_EUn|}*|X_}9Mna;%cI3?*{YhdH6c&oV${>mn<7p!lvzKCF6vD6}q zr2Z|C+P?yN@HILA?JhS>glI0L_kSgMNE(6h$>~4)a~z5E1K+#2n;l^N-ZICr5z_xZ z!=^+J$#2n~;rItA0R)hJoVv9JX?`0{9FiXgySpU3ysJIQw#&&7DFE~ITzQ-2p|nyYH9zWe?=O8pO7=7LExzzQgwsei5P0%s+LpZbIwX^kerkhG8xoZ zV2j1NYECZd@6}6!~kp31+Qa>4B z@XjBzyI6G`3yL&(d^ICl3!xxqL!dq4j z@m&8wEN&sw?|P~Yg>lc5QUFRQsN+2+Np2r!|GgSMBsQ5`s{(By84Ll@JA;mVbquA< zC?cE_;tG<-haK>$l*PowYXk`a>?Y5J$S2?tVr?f_k^6B z*ucOq7+6?;N=je{2F|8C3m^%3T!btwX-7v#mG$*wA|l|`)YW-hPM4R^Nn+LFn5?N%RmSgGWlG=oM2mnm zPW+WsOuy`O&qjpNJh-X0^h2Yd^R<>bQCHUneEyC` zKi%x1+|@Bl=SEi;S0Y&@l|K`fjx{_@;(SAVi@^t3mv|^+)ZkuyPp*|2J1Zwe{0{@Z zzg1jlzPpe#%*sfGODL)W4^PHyEN*xb?&F+Cyb$K`-rSRBiF6k}bb(KHI4u*Sqb~Tz zAb_%~^(0Si<$wY&mS>X*Xhq4ku4AQh)=$D)DkD>46sqODaH8Gr%bd>ip%;vxUkK;8 z=sRQnzi;ntLhfFOu)LR7tEg(y>&2{=pX<%IP}LzcN`A+RMihDeNBJS4LQrB z{~5rD z`sPgSP7W{)@CsUH%Fu3{M2FTvtwGHRMg2823;4)cqHiVl^IZr=X%5-TME$D9a_wfz z&E~SQdylx?*5J2L7v{Xsv!VTUNG<%M0T+Y9X}{QW0CYa7A|SL>l@G6%g2HaS3+Sq> zxKQWICfeQ2dI?}8ZbkM7tF)xB0_*5CXu6I9KB2X#U)7{5)|YP8$$UHIC*}Ch!tu*k zo%HR?z$#F5d1o}P@5j$~vuzq{>|eF*kapyAZ!%z;B_;~;3$^jdHe-sSMdZduGW|1L zit@k;J&AEyS$Sn(AisBNUx2mDR8%Zuk?s?t)NL{L;c6K={I+c^Hh6=Vx&0n+WiBy)v zI*n#d+rY*#e0nWJCbit2-25y+nDM>%ZU}fHuG~UWkQK~R#!WZ?NP2AqDJjNEgP6Id zqDHBO?oY}ZB3}VU{fzu;=82xnCo2F zfij0qB>FXMC2gyOY*&X@l2X&K;R3}C!vI4SiRZvvg)d5t_pP3 z^-3@emqvP#0?wx>S8`HFhkG>lQq{aYm~qu)nXmLaDYQQrG3pWcz4<9QgT-;q!E}(4 zq_~1}f4zB1tEl9!c9E^Xpgun)=XgfAUm>BA_B@O;_IfyVwil5i{}rrVIzQTXGjS;? z8MqF@jMnqg5h*F4glmqGrptk^$MMs_yozvLQwuvP~PDEzzO=@N#sTc@TQt+vsQ-vtumt*I%Q>3MOCSTd?b2*@i7gTXk2g<)W!MKSgmv zF+M^|w%q|nru_zTG-Gp#z#%zSNJY>dB_-v6drC9ysPffO>e64z_lpU0F6Y%0$J?T+ zouHG>kPpt|VjPVdd<-*65?*q+KDu<1qsttV`EQ$jJzc(-#da_7@6M8J?dh>elJGExW@^7Xe1nr*Ryu`KuWlJEj#0K+J%iF;E%}PodR!v+vzBwnGh2Z zQAudvKEc;sS4APt2%Uh~2lWx=T%~%pN04*57y|U0?cw29CCEux(pJgKqJR6GP*YNl zxIk~VR>V2Sr~V8}{1Vk+i>r{s6LF9drFy66vY?o_N9Z#I+AvpfI=jVCP-HLS)Wera zGpMMpX7+0_>)I$W85y;YWl1PD9BoKo@pNKb)N>~sjKS?jOh)UbpvK2eNewX~uj(&w z%vVZP4Mz3v!15=YRcz4U@IoXA753#Jrwjl(uO1}bYzTasKD)gswrQLKtS%^pemS=| zsoI{~OCeti6~HKWdb@|mAen`3g(!Mn&YRZmT!Y062I9y@l3l*BQIWy;EfcX#T@>9* zN*ish)cry|`@SyZb0K28!%V^s31e4Gf?6AN{Tciw}m5+>Y@T)S>X35XZcZ=$TzRM1(NjX5`5 zTQAI9W;7R|{YeH$wPh72^roTr1Z#S~*(x+mDg*|)6I z74^pH3)MH;7q`ek)OO&DWP#<2`=Nf5yXB;NP)P3|b=h36^Z*3Q#x8*D6^_Rsj=lw- zt+AU_&g64kGaATei^m~~84YhTdWd)C>NKH*RzKL@w-D$JN2hjpeN^V-D#Ysu)eE0g z_&{hA&e~x{TspeZxU96${ztW4Y^-vCM0~nNa4iwUbhKdo0F;!X%BliTNLcwysOERA z9}bHRV(Wap#+{6qxw-(4+i~pn+jHI@W@Ow_N0~UZxYYER_OD5Y94=f|Wx)7#j&aLh zb5V^nH1ry?FZr^rl-Ev)O8o8q52gK_bNO1kCrSg)vfawv>^R{{Gd9+fi>hI` z+l^OXu3A$wb+aZo8<_bD{)EiT}l&d<6Gz8 zWxM>|rgCmC09^hLC*=@s)S4GPZsmUy$W0rTv2h<%bZv$aASOv5&Uw~T=J}{0W;ta4 zmbdX+dTzVsv$>tx{n$doiH`DQZH-`2!gh3Q2*SczXdylk%uNP|U=~t-ard9dA9K*E z%wISguRv@fCo`EEkG>$d_9?wFbs5Nqt!2!T3q~D|Z1;e<0uOXoKo4-VBbYPizd?Xs zI*U0gw#UOc6JDNj5=kTf9YR(?9zUi_D7NDr){mZ*w0lRVcSaEI2pNit<91S^bMR$^ zBW+P}-Fm29m=f};jRF`V0{X|CS49){JX%?!1bivhQ(i|C*K&NkK#P5HUl^6ul+!MG zNdn6b(2YBxAmiafRmB#JB&Sx~nc9Bia=GQYuk=SCU!^3W`f#e)a6|dvYrzDC%J%>- z?1$ha9b&fs{<0s~0}&4_`)Bp6DsF}w<)`c{b$x{U;%gWV8r~J2@k1dCJ`oT#yR%sF zB$Wthe^G!S2it?A2bS@;m<`W$fU5e}9BOU8_U> zg?v7g{LS8EWS&314AOF|en7Ls4*#BzCrVLpJ+S!V1t0!;bvwe-eH%2sT60r3Id`k) zaZvb+rM(_@`s%KU(*Fmb>(%LfE4=?w5TlWDw#57rkSZ*=7b0k71N=!*Vp~5B7?{(! z=w)0)61LNk&MzVrV8SX*^ThGo!&s|#K($L1MCRPAXvLpshkn9s!QnTet8BHR2#w8& zrj)fCI+MjR+3!~#R%OM`*=i9aV!Oy>&Nb9dbv4#|%0Vo?@@R?r@T!;{i2byuP9x6+ ziY~wo_PB$pe6LkTz&axWd%)HRHLaB|M*>y%UEmU<+BOhlqIlZ_yPBo$`w=U}pefKr zzl29hMWSCO@T9?`;75x#`)sJG)w%<4&70F}w z9fpw+aRvqt(E$#z4yjC^=cPASXSYypw{RGaj~2jl^4rA4*;u0*I#BUXkiCq(MA5ku z&9%z#7{cBFH6>y%9MBwEf|&?Nbyy^a~_Gr60RcYeCJ>v%2EcS1&Al6sEkIAK1+Km=~6&Y{@Ia|wm1K? zB=9?$l~pBO0B_<#p5_WZ9{0$cdXtTjd8}J%F#Ldl#>m-#_Z%T7!JG+hqJk`X!jduMJ8ae%IwE4<$d4b1|-9!B(i3T%B_#8qV zu5)mm*bJJv_jTAe2a^XQ0+X(aLM}FBWs(!U_r8*jB4)q8o^D3|#b4Y>Dkkop4{9|z ztbST*{(!u<)e5m5`Z~aNVLMm~@^%CU<7zeeJ%H(&2j2-Vp z43gXkLI2;a3-ygSj)xB@d zGm_bVfeVBFNm@Qe;~I_F!T&WNi^T*Qi%IwJm3o}`d>|4Vlx|(QjjWgpGRtDJ`wZkx zbV{M@-F7Xy|3t990K-qiPHCSTaflOmrTD$a+;*0Vivuk=BWdvY>Vrt!laM@7pWx*6 zpVP06VWSlFy*x)EubPdBkL^YEt=e11o`AAVLf13%iP&|KXo49k`l+5QgYC?h71Spx z)Q{|6ls<1p@r$ZNQL=u_@~~vUO)kv3ZPXtX>Khe1bN%CZ?8YFe+GOQ?BdjDti~!%P?#>k0s7@vb5zi~>ruek{IrY{(@mkGJL@`@lye}oI)e2O?D@w7vswa+X z2tyg%>w}*@?;_DUp(Cm#OR%_mmPwYh|Bfy`UL)Z_Az9xWNfw@GS`W! zGghj_*oOua87ZkQSyCoNJOBXk2hL$!5zQ}gY9pU-YS})`a8#v+zP313NUH(v zk8vSD$sYN46kUEBJ~yHl7zLu0XzUyh*z(2m!Dau@m#}$iwI)%`Q)vuS5c&`M(+s zC#I|@#KfWah&?NP-Ho1^Y zBw}4^`SXW#V`2uL4!L){)@oenu78tFw|@^rYg}$NT4)#s()530vmeYnfhHvrK-QRS zhN~+=ln^^%hPMlP*^ND0YjGxQ(X0P~S|pd&OH8}~^napyiunu}qw92Dn!TIz`VMB1 z(9IHxVIGl`wKu**uzeM7jppuYfQ{mP=~YC9%xU+C3Md8Kkg;~x(lXt-a}cGl2#5u$K6tBGDVrUQ?xy)(Neq%Sw?=xp0X>0y9O!32|;aYs=4`&PR(Sf@}3WPj&bC0EClT6&@=ZBH$M)x zJt8kuRha6qL8yE454-N4^AxiII!_~&I8Xj0@9}J;r4U7rx0iS^$1R%ZBciegdXT?t z&4-GuFed?PPf|_!pQa!hbIARYIxh%UMFlIm6S0vUjowDUU{G~i)G?Sml~@0u;HF`3 zNGWZ~uG?1pR@4D;5#zG$B^&s@J+jIZ?HF&MAZO+J&wgGGbLKUSJ&ySB=2~#ik;yQ4 z8UWz+?$-~@c$`tcrxCDh>R)@g$+2=1y#;ORJ7a08T$Nw;AzmJ~r{dA+JOrXm+624J zzCR7y3KFfxK{52qK*h zKNo@%p{U~32Ps&&F(KdD?Vt{xOSq( z(wilwg<~nxvo4mTD#%|9!)HFSq8#1Qm@bUpswUDa5AnR0io5eU-2=Cauis zxmN10hcn4KUZ<{tScv@a9M9&*S1ndZs!^Va9?Q6SBy9RjP&N8&1AqHEo_{$xm38BQ zL4I*3bdL2tI>vk0!F%bg?S=h<^c2Trave+#BWY~P?4%Koqax|CE3Nc30Q&SM)$7tD z%-`RiD1jG5wj-kKNEBUu={KW^S+@3a8+`ZKRQ(n&{H&7FeP@LK5(~ zVePlW-Pf$=zkZ-HA)&F4wO>NAq*^;HFCG3c#r`^Uye=c7a6V*$K4^3pmxtrEY^K>^ zhCJ~GVkgR2(8;>x#> zOl~g)5PIy5{JPu%0ZfMS61d!rkt>NcgAS{i`xpG>`0U^jFXS#GG3EFvN($Q{4IY+$ zh*387)q8KKx^hMPt_lpQBCkbL>+6SzQcrFlP9&w)9+KKDyQ94{b`}qIGj!_#7q*s0 z6l+X(?=&0FRbOwG+z^lS!a0m=QHo4WkLrkb63ad;lLE9{7rFYW%#3br%jZ+m6Fl6G zT;%ktWy04iD-7QF&OHxlPx(AB+627_od@-TzP%{!TvmQ-w%;b>yIXkSsW=iLaJ%Daif4b^Z4V3HmW`I8rLWCw=cn zLkIh3((%UWMZS*N@ihrY(2tKw4kwm0s4ZVL7Pa}TvM|+aj|LH+1q;Ehg^+Wi9lpg9 zs@ZK{WGsUNhecXEmCqX0uFZ^y>0}+V)bY4oY;iqBIoUc`4@ARKq0uok@~r#%nE{4| zYeyiUdFcgTLgd(U9Lc)Pa&(6d^g#ByPeJzl9na?|x5YkjV`dDQmB&Kdq`T>@?2*!5 zj)i;S{wHf&V z@Q0hf^Kk)jLd8%kj{BIiu8A&}0;y>1Upd(utQ5%KxtDNJe9LS|N9OmGoQko&v?B3- ztDSH`P7z3L5~z|+Q7L(~)>?%ZJkDYhV`AaMv^2KRh`ZtOdZ=1Q=H?f>uP-H}M6u7w z){6ZLsMyfg&(bD?n%D;JEGC(e8YqB(cG7@OjyaPFC#Pe-3xd4--J2Jomg1#yFI=?z z)jpgX5x$|;uzqi8**A#0tcomdsZHz5$hL{1`e3<|F0<%lY&bh9)kK}pA<*dYCWQMU zIlwXz`{Joe_eqo;ITG2kiwr&0W^4)vP z+wG#iHC6)@Y7I@UGBVVx^X6ungAOYL^P6v#=O_Ddn{yT@h2L-k!>(^Ok4fA5?h zc^}!7^UPn5hlJ>*+HKpYQ-e^!kYeNEM9CW06R$NkZ=64DFNPy*Uz{a+^S#|1h7(47k#`m9SC63yp4ruR9p; z8>3BdT%WUSejY^~mKhl+xmq;ZS=tCw3EXh*_ZUm(C0>ld4Xk*C6%%uOK@zcqGjF+h z|M%U;?z913=c2lkSP8t9Y(FRrw<%bKo&3G%_bKl% zn)B00nBjp!vTP9QqP1W)xriY^HJw)6r73)vK#$&3S>4(n4%@5mW9$?e1Jm4FL^~w*Qhc*V zaD!FI$_INy@a5`UH=D5Byo$|Oo8xbTxj!tQvG1?43NNXdq5fmz z_2IFPSeulUPQ~{fyvx2cS$VHoo$+#Y^X;p@)doR4AGF;AUoVdqLvm!!<0Is!V;NKu zC70zp5|4d=$@<71C=0#vej!Yn^~w_HwW-sVCDai$JX;j@V*3KUPwsZd1*h_7%1q(F zN2^PvmLe%s6V}Qri$S*Z3fhsHy!)q4{%Gc)=b0WE=ybN5TY|ID&w+m?K#E@m^5Xf? z!Y7PAgB;|dOGagaA*LHuDUhuC+Q5*>+ zsHwN2MC4~^%D=Y_(||l4_lr331lvQ!cr6x(bMs|~8ye`d);#p0%YzHpVK9NwwEU<;H-LfmrSx9t-ImS0eEP8p`so2MkZ5Z!LZFXXN*3YeDry%<8qV z@aioZ7Gw{dwI*pAtPPma1jqo;#}jY|o^%^&SG8jlcPS5BGimY)(WB8m3N?xwsS?Q1lCwxr5dYP)sUroP`YWo{uI4 ztUBhW18)HY3yeL!RQ2Q&8O~|1O2zhOKc`zS?E83Bs7^WskZe_Lb*5KEQ%&<_M;Kz{ zD~4l^mDX}jP-Ubtb< z24CVE2HUt&|0Wd2a9@)F{*KvFe^Hec5DekNCS&Pjh`R7uNa8}}wjZlVq3zNZ89SMZ zt>qbRC|*_l;u+%%_xi(>4FOdf|JnB<^Axi%=FA9IPpc)qlwJRFG%9Ksv6O{nv&i19 z%T%W(dQ@Dy{?y9;?()=%CO0d7?GKR+royS#!JW6d<`u&j0XhPZ$K z=JOk@b&)?(pPD)qJvZX2xi6CJFF#~ao>!tXwsdk*4n)+%xTy!~>t1vU;hlqDxS1UM zXgGD}HbH~-L1v$M=b7YSTj!c&K(zBGh6{%V$Np_1uIcOqTU|x}mgMvc?hr{&8?UO+ zsO~asPuc-BBc(PTKRWQ>;gFxvFsy`PEAY)-#JN(56iJnHi`@QkUhP*8i&Yo?E{0S4otw7k+aJ#dZB!~C zr(ah+kMnb(zFlDJYAxg&0fcLOUu)*%ea)x4^v}0B*31m6%$#d>Sz0I#qvZJ%+Ke#* zc7mqp$Es+$e&u4jo# z3I}a+`U$h7th93pB&CBUW2ih)prLf)4}pyB!@#ke{L*!As?V#FCUnqj;qM2d{~YBgU*bO5OlMiDH2z0`X^pmCdomRa~C)NUQ}=E#4Kp z=gwh}_?u>X+K99Wr-j=S-YB#U{q*hW=o&PM@+|FQGTS(_*_y9Zp^hC32>fFhBRqml zflemuDcaoT+d?5GreWH|#Ja9CCljt^|8Ww&o&^g8zZx4wX;dW{4QKM!b$4BqWJ!j8D#AqW6Y7I1D& z^$SMPCt<)Kg_7CggwOFlo(5YhOu*W6n5d9xOJZZFbOBP$?e4rVy(JkoecW~?r>IvK zcKeLMl)qhJ!)n@XhA6eyEUF(@mOl9K7QY`DDgzOS7SZC1m!4F`%YucFv*T(ch)dEK*e~;Kr|rsKewCDy zOlARaHAv%jvDvnyXmnbs65H$R6Xl)yB52nQ=ss^y=K3OU^49h3>;$)(49QrG+Pb&( zHp|yDT*O$BA)5zEogY|0?DG%S)ld$%ANB2*xeQpcZ|wFPnU|41HAx||2R@%l`MBV{ z`UlRL@c|QDTgkY}K|#hWFOt8vx7hXmjmgaKuH2xdSnv7nO=NLhpZC8I#K{ssV7=w6 z11+2D@f=|4YN>VgzV4IQKloyHZGDAozHo$3HUAgZ_@AzKL2vhMqP?__tM@7{uMtQ0 zkCA6Ly%k$6L!C0Ef@Vw_NNN$jeo@$d{(gS+v@wbV(;b0nY1VUro3R^#Vsx5@H%~9O zNl*QhD1irO;6taKfDqV-yA)Zx6a`wi*k`pZ{(o;5T>fu=r>cs_|4g7jqiz51$^YMW zIEMaz5&frv2mEJ&|9_h-1pD8G{~a2F{r@1=AD8oUGIrNM8l{^!m>Dg6_d`Q0P=GiP zWq&hNj11-W85T z3;0UDuIUrv(#4TKk? zQ@D!pYen+zL3pk}@1!sz4*c}Z;Lr=v*^{Y8WDNLs*6`*mHwJb5ca#qjmaPG+x%@@k z8eaZaSx7#GF)Uu9Vu+0O7(t5jWgf1->X~-L9|Xo+a+(-V)@HhE6#0%&Io&x24UP;3 z(oQ+q_W3G`&RxzKRj@d;W$6P9*MVX3#UQ`V--3H39&F#Gr{hpjrjHs92?gG@;dHcF zbH2tWR_)lmX^r4(^_|KR=ebTy2r9yg&r@hYK);tp)#2ml zlD>PvR!lIUB$e)hGL?D0eeLz(t(q8~_n~~LQ#Na zKCl}T>u;U+Gxnb2+l5&fS6u1tovv0jc?J8P-R5ZY!U{&+a&_o zM~OpR=)Lqxy*`D1z~^~2UbLUNElA0cxFgxTy*U#p;T>AeDao~82-`3$_*0Ri3N@>_ zi1jG`%3Oy>{v8Az6+;5{r!T}uMshY0^_Lyu=HHc{ ze+m8W)1q*#<2q17vp0o(=@^Fdj$fEr#+z90g_jkLWT3wC%bAHz6KbXi-|18@Ae18+ zKKQa&*wpt}337>%^)uTbcgv9DtcyKnRWE>9`)(nkZZGs$QT{j}9iulQzV(5=dE092 zjltqb7vhB$?!E|!#?w4nvjy^f9j6wahxhB|K~9%hQVSw>unQ7K$e1(|MFMwQ<}fUT z=O`97OmVe)(C!G(5UaN79pqB;K~4u6dP8HG_neZD9R_BJPgx6hT^s8npqQrsq07Hl zmqG^cW0;$?<`leWG&>yO#nP{^JVJIxZ`KI+)u~P72m+t#ESlW_;VjYN)x&~WhPkY~ zq4Z5Nao@2}a(?#&v$RX5xkVS0YmAiHQ1=TePxZ+uA~{7zcS-1{#J826wI>TiK1_y+ zp!U^=Zf^Y8zbm-Xz}3HGWh*YW5>b@?PE4F!32wNU{zK&iH#-9anlq9kYwNXRwKCPM z)D^jKVBu%)%X@LEmw?~|?q*spMw0 z#o$k+w~KjXBFZ2A$YHCgv+FrV5bd5^ipma^CRZ>B>!_dQ9oUw`OkgXQE7f4}^zjiL z9$#7Em?f8ey>+5ayZ*MH(p1xuiL(%`7JZ~-WdjNIdDs5pTPmHT>D3uBx59jv%w z3d~$EmJ|1qB5LDrprBweTm9wY)4$cGU)3=;Z{QuyD4tqA<(p7)D)qs=s|_ct%Lx;T z3;B-V)+Hp?Eo`nXIFzGMS5Q~eS=-Z5V29S$`G{M3cSFMB$45<#4lQ)}w3lqFrqH7*=!0_K@m)uew3zO6J!gdyS*GBsc(2*JG zXxtY?NKPyogT$G)9<6OcCK9 z&)~NsPgJO;DR~VzBBW%2cA5r|>nMCr$a&jj?8E{09+xk92RLeafkm@ZE|>(}{k}B( zU$YkT_C=Sbgg%gdbij{WkEc-x7DqXuFtjY{JfIavRykBF!b0>*Y_Snbmr>s z^AnpAaNXnUlocHz#n+0Ew99}rbhP>IHfbo;JTsCeI@HmV#(s`L@1YDa#QbAN z^dq-?01_sa6mk6K`q{apB&E0Zfc+u~FG-7OD+$Y#ak4MZuHWt$oyM>TYwy)VF+1kI zxmFujv(8;*?))*?C=_YIb_1frFY{)=DdCX~~+nmyn5|$H`oR7Gp<;r<0YX zVJbKGnIEfd&(=8**q5>45M|Rwq^B#BiJ)a-%6Io5^5^Ogt4M2%u{$oKEAiRrY2}7| zOgpyDFK7-v4z+XB-LZQ~fKbE0R#jddZJY&WqrKKI#5mD=$C*5_#k)Q^gP&PGfkCa~ z{YHC{Qq~DTA)JT*N;T}MM0U{CwKh1tBA;n(O%R>pSgbTqc&PABWry>Ey*(iN-4-C| zS@Oro5c91OZu~dV`WhmPeZy5GBvd*h%=j7Ih9b_Eoxb43P!w}{lS(3;iGeP1>@im9 zsHy{rZK}88^V8M{yIoV{uf3p5IgL0a=HZ?aB2#B`o)yXF)W2I^3II;JWabvAdx@}( zYa>5bjfwa_j=y;p9KHH{5}IF%U8vaXHx7#|m*q8K^YOKI`d)n3$_b1%`-oOj2pBAr zJpi3KFqD0nvBwl;Znzp@8rzie6KCp3jIN^Xp5?!GjWJ(#ypD9-nU^YeK&M9Mm_Sc} zKjJ_);xDEq2WFjUbDMa2TGn&55^hugXrwx3zL5LgcVS@t52}FlA*ugd*ks;;W_pzD z0Qs8V5-{ujd-5RG6SMhGoe-I?lyo@LPfPT&(V$l*ub+=Iv@uC?Q%#1#yBUbU*;gk0 zo<`nanm`)Z(~4s)&SqS7Ifp|dL3e~*ABWG^+m-WVoxI)AV6bn-=GuH`?SG637zc@YJd=9-0+f4uOH4 z+z@7GJG2r5p}ez z#@_71GLp>?KIpKdRruSEXJoDePF{=r40PsCB+3f63Bm$n_$I;~6Qz()^yNsy@Na!4;}@Hh7UKL zjIy$JevJe%$XDJ(!UPRBC{#Q%ZiJnY6+5RROheEtW~CxQD^R;#d$`2R*60z?ri*;5 zk!w@wJ6yu>k5Zob)+KIs$J`Z;FMfdOVQKCWoHG>7+|tk&p0Q3rGxjs4{0Ks=G3NJN zb@+fO8I!!U=se1S)#CEY<(J_0E$8k-Um5{837Acc&?3<({s|4pS3@M4`n)R(W;R47HkVgCE9cyEn5lR~Swd!R$YU3pCopvhqem*W zPMCMVuaxq(Z8JLJW2>6x6m*lLf3We_I#ZB%U~7j1V)D<^ z?y30sTHDAGdQMz&hGI{?W(2a00viHGlfn%6*t$J?Fa{(kbmU_=h z0$v#vBmSFOG^R{$(Yf_iT^{$}8E{Wy6XU;~ZV*#0sDA&x{Rwjw;<9Mwu)ky^(1IiG zC<^d=sB{K=i=6wl__(V}fMC7K*Wn${mQv|-v2Q`}@kk&(of8F9- zu-jBu%PMg?ek7I0T;AZL6&OCJfmBJCWZWzI6jRSqp ztS>Y2-~9)z=Stsvk^HFw zXA+XrhG*l?7whdkg;iEFvo~Evy&fZXlpJdEVo_NwI~bTcHN-JHB~qPkD|h7-hGLtw z0E0Fec_2YP9wBWhYs~&63k%D{h@`5G<{IMHvSzKC-XFgra*E2PSJw|U7A=1e9{4D5 z5@Ss=*H#ZMr+y`Bv_Unum(z&7;>2DVxk#m^EzU8pklTJteqKw(*@h`45B3m8482^g z;0rsaGFOTnKi_fm1YjR?D$mnBN*_Z~wPkeffWOCe{VcT44C0>ku#wo1JvwOd%D!=g zpjpUTLd1#@Z;0opjb_l1z6r#Kz2>w0$(w~IBm4Zs5MeE-SL} zD|b$V4GKm$Gd?f|@HaHUm=iO3ldZdxvt7kha$g1DA!Um#LOegl?G1ZdhS>7gFPluK z)u`_ISqA2aj)zywUG4LO)9^^nY^pvNF;T5>jN@nI?S$pV6^i5b=-*-eU?f$&JOnE9 zncH$g3*~oIOm)lj4Q-5)*xbFt>{9ooWRU+4UGEs3Nzk>8#YN+nLyy zWMbQP?l=?Ov6GYcdA|4jIp673t5>b=YSpUVd)HOh-n)sC-3-_6y>k^~%D<;z{}3Su z?1JYTqtd@oOCkBX15g?sgbcYyQxSF@Hl52b=Zmv*j?bn@-o&6R%-oe6f-ZsMT^-FB zql3)L`PRken90tVS2#t0&MVn_Q9nVqX%Xd}EWWcoQa_;Tu}f%t(e(CkjR6*Dkxr^T z1@xw8u*=gE%ColL3!YxNQ)>@fbL`$(1M-EXN7E1}bMUqO3#v$Z%_6zrPv(&29h+|X z;FysZTMB~1=^O06)f%bjlZqw%5qvSScZy*ZRMjq6J&Zdg{~KZjAHa(&6ja<&5bD89 z*#kS5iR>p=(FJaeo5<=L@ZDoh9}0Y4`Ii1v2thBR@CIhBhe+Sss0okPClY5iG#+Oy zH|%O~4k%?A2_6CJo!^&@WXnG12Hl0w&hb(z58W9|sRIRKAn{valEGpiI=uOVi z4UhR&^hIvDjV#cJ^6u^Fb^Xf|cD?WfPrgm`vY-zq@X?@ujsrEw&GS<((W0-pH)v;Y z?g42@4>}{DenC*s03^mNyQSSHm)}2kp?}!{mKi>Y1m&reK&Z2e30)2zFP&2$*Z-Ou zae{|o6R)YBfadxyds}hPt_kreo-Zua^+PI}uc1vR zA8Q`zp5AXTtek_HwVNF2$13wnvQ8^{J8ye*|H3$l40wjSFWBze2=|rpNm?rsr=(bQ zyVGX=PKQ@j=Vy;#ZD_l^JP+PsEH5Rep$tb52Tarq&gKJixN#OIK7BD0qA}mgxxRnX z)0ZD6-E^NPlX@4k>bDWk_WrIuiQ~%>Gry6F;3Gje{vZKWmf1qQWb+YpK!EM)JgID< zBBmL$lS4yL+SSC(+oKEna1mEeBs#4NYn7$;BhfByF`=iV>ilFV#T{znAroG3#?V%q zK(RGBm$G0N+!6w&pTHPR%E2be9skl1Qn2EXy985=1*^u*ZPh% zbmPcrH*_qiKw%bfjiTh#Do)X_QXrZnd#kW(75^8Uir1c$dm=@b@IRQwlVg=8g zE9mIzz-0HgU$QJz1yL&)D3QQ9d8Hfr`39Uv%m{ZXYhRR93B#sdGKIJPN20@)RxG;hbD`WkQy*9LvH^<0)GVG2b zF-e^W*rV7AR2%07=B+@Z%=AoZ>m$gNAVBGxROa)CipjKbpNQBWWc2`LdYX) zf&%i;o^3NMcdtRmqt*-CXW5!-aKgAxoDWz_;Q?-XGItb27sV$bYn7eqBLG$&GlCS0-f3z%yrYYQJJ#t`Q2l zLZNp^fa_N46>|FrUQ;8u)R;zlIH&4LUmbO0P@*5BkQlH3DB1m?oC$o%2k})*wtt+j z(mHbY7D7K}DLip@7OD;&v%G;B-u8xh8~5FsU2Ma2X7i@ia=%MYu|ILo(nv3=&bT4vMm^ezlELXk?hXn(`fGMjp>Ud2VzgIe&NL zk=+yyj4Wg}oae?9G^pEztIqlM3dBTGC?I~<9N^SKkTnuJV<@r1Inp0t;`&7Ql2G!> ztMWDc{(QRO}H{MU5xKP zH1so%L$<%4uhcr8C?Sm{MYhW7_Pv&c#T?YO(5b+aqH3)Mfv|SoVF$iS9wiW~{K~>i&6o4M6AAsN1a37o$M|7Qt z!kGB_{~Py4UwR4N}+-e zQEs;AkQ*nqK@0pLYY%-Le&^Cl)_LJn*Lja)7P!}(B0j8saQpY>SuFAbErf5VKwxB@ zFJa9YLgb#TUg>0D7y^nqhx>KL3M0lFm_^&+5<3g$Z6(t^(S4!XMRsqdf9xv?TJ2(& z+u;~jMGC)}*P$EfEi6}`>vJMR_U$^xpEI=kx9{T+F8nZzpcj~_yp~XP&PrS$V`k@- z4J{>9WML2AYZntcdF{djfECcw`-ydY)9#KDaGC=!`~AVXdM#c8JD}}CbyB}uWFsPF zV1n7|_KQNw;9$Vw#`1>-ZKq#oL@hhckI{CQBQPr^dN2L-k++C?7QtTGtF%eew0m+i zlDBtSRk%AD>2|5557pqljWrUx%i5*Y-@yJPnAmRT(=asqI=r>woWwje( z_Gj z0m)wnio`LJ&^aF^@5Cw}wi{bCY1KFcBKmM0wOgKZGkM{dU4lY0ac=&8C2=((j9E^E zTh|Nw9-3n_|H!KRVxqh$Pw?A7lz$-tC$}iO1{Vp&cM+MdDIVr)Rvc+%C68)w4<_io zR)>EyjkHhiu~5nG^A+lW)4;EcmXX zeCf0Vm)Su>=>AMabgc7DIv#c2@nMUbKN8)kqI%7W78>?QE@ufG9E)2L{A=DKzWcAl zdpXL5_!IRyX0CLd0Cy^jhAX&XdtESU5AXVKYv&BYGV3kg|7q zAcdP!jtNyfDN%j2Ug*5&w?it|H3D1nfw5Tg*h{jLEoe}qB<0t4ppYiMKQK`57^V*@ zy`|*rDkEjbZm~@8WfEc-3LB0yGdI0@Hb)`Jkm{SGUYD})tDWwNz9*%?g?#p4-%04c z{UHd8$rDshO2g*lH~Id0)l`Bh6=0`0r2st>M0YrNM+%IvrA3yX7I~(hVD@?VvsiYp zUljP3e^2c8xk-gv>>qTD3pv8QyaBIRdu+_s!FM{b;esaU4wde}X>)RFjs6~aOlUP| zT0(YI6j1Tw>`@+-mOvCNP?QjTHu%?Zf_(D7!f`~XcDoq8GCv#9QCJb=oocPnUA>Vg z=rtQ6*3kW-TR!2c0w6D;3(bfxxD897vf)XA%OUym+!Zf~Ete4+e~he&7)<7zF~9;{ zFz9w5Q;0`W3Lo7pUpZX`z#U*XT9xm3Te@HsJo-zae_j(>q4#~qJVDy=iF7?9N1%#$^J9eMCY)i~>2rnlTU4kqTguYhK zVM9mLl2I)dmJxQH&ovNdBMV>pH2ei^Gqdy#c-ja*NP6m_>)K5Bjb?_JQvM9+yC*}v z>`3KIF`=03oNoZ?x62<2cJG<{v$vAO1H7ZqslUvStME2Ay z2Mf}a+MF9bEycb)aHEz%L(I{xUpd<4>J$d*?0Tsg_JVT4g9|-CM zIh$oYeY{bN;~Mx|Fl|x8L2Xw^_PD#+^e2Z(KK5keK}P19W)a&lH*tV>!PWv85;3o+ z{x5imvsE6?Q?RNRGd5;jl1@!n2x%d&%m_#`4h%ELe~0D#jI`8`uardQ>TIRffT@Ur zL=Dy1=Gcg>E`z{+Y~y-}V6dyeKpUCKv|=9qeiG+lZb$*X(DkD={o}z{)RkN?)Z##I z!)$m7m)OVHJEJKQ=w|4bGYd9ByG<%Xb|6VffskqSvsp{5fkpfQ#>@UjYZm5CYjWyIZhAH$ru9KNq+Y@X>mv9NOw^I6Fvl_EPgtA{L8H*i5vr8QqD z;q3{JkQDH%G#foMHgU|B@FFS()i-T&r*lX@{l!MfI?X%0q~N(es^%u9KQJ8O$g#oC z9Va@>>q7nXY)%_YqkkUkLJ?JG888{93Tvg)Hop{(Y|18u^KR2jRRW462Zwlx5lpBy zUm>yR^G#egE#JE;+!~-Ms%DY z1D=|IKVM(x{E?IkzCP%iSz>IyIjIlZd6|ldd>i2k*&ByXqEkn0+Uj(a1hyQXpdsGU zj3+2dU}&(y;y{V)SZ0jc%pB_Oo-{ZPele>IbdQdf&`B(dce-*t1Ie_-1#I7P`l7?@ z(i(vEhAh4ej;-2G-N(8xC=3_Xh6~r~{gFf>!AU0z`*j*Dq??cmO zw~5USO#9?**>CJui_4$qY)zk@On*b)yZh})Oegm91hQq>39{)RKptBP!K6FySC+Tr zH?QAwsBrBxOsRJ{_oQ`Z$hUXmn0z*pNaBC)?B7wK*`Rq|Tyx!Y61zDTSf<{}@bu9X z%$fLA*{pzf$-CwyR6ensN|`Vjap-3st~RkPtH^imuP|@yho!CcsA&B?C4dke@cX*I z!Z6Az6{UXLF<$OTN<>J`Bo3MAJl4mAq%i+FQaOYd3d4XSaJ>ruES6|UUgBi{kiK;w z{y~w3B!TOMht=X+_ClMI_~*R%-co^uAfaOQ3q2?Ur*J4x?>ivgF)57A&=DFSB&Wy? zChm<^H^$BpD4_M!eQndjzo%oi@xfGvE^nL7oEz+J0I=a+w5N!MH=)mL8Jh`<#6tii z5)gneN(g%(Oz4M*tv+Zd3zbD+;usNZ$SMdCp{HACLNE*|m#3bu*c{{|BTHS78Zs&9 z04Pv95(RvZRpfctLvPb?7pgY`rvHx*@;R0#AxSh!Ly#JLJR{I65~$PP2x<=w0p3Lx zybD=G@(sHqqoCdg?smVO?tqT|c+MpO1BLdSLb<(s9Bpjgl*+(;M*>1e!SME<_^?t- zHdytWfsrB@<8)Cq*m!KBLG%FV?RX3Y!RfAoC*{QN5{!Hyba@@|NinDf9KheGxLr|T z8Y~7#(<3&dmEP%-j^y*G`F`Da@D%9_~-fw62qXLkKlbj2?*JrXc8oR2c=O6w|@V9>Os!7D(? z(G@oF!{lNrI8fAQR=-4mw1O=)H%-dAiHsdpIQ3YeQJ6Kc+C+-fT(zJbo8Fr*;b1FB zZ8UPdQ{`@#5z)qT)1zCF;-g`z9f3QQ%>MT0F;}z@+fkM`my|61*gpw@fFQoRrAkg& zq_t6<15)JO*AO^6FUZJFm|2U--7SJ`Kw|}>O!J)m^gsZ(T2)TZQZO>c-QSPK)^5T) z*S2V2B<4Rxwk*m2T|N&rx`qY&p#%fcD-<|M4NRs9W$6G5^`gXpF?NC`PoS!Xt{sk< zIUdF8V2T|)GugZ_+&muMgCp}V^Myb_V$PAo|K`NHE5Y1Ip0bk%_bu>?kL9+_c@uOBe~E^oJ{uiw11(2Ey?zY`deSW4(!&+(PYnc)iF23iuM7nP=ZN zWzT>_?45(}K!VeOM1tWvZ=o`D^iA(|Dr|lMLJdY5f#wWGtQ>9$C=HPLNDZCeG@Cdz z*gXJ{LP-fm_Sb5Yn9(7>IZeaAX9oWE-wbO^;>Yn1j%S{Ob_-$FLLg)+~mHB*P; zAaqv*pjPt*&K~?O)5NVTf9#?t3kU z(nlq?XkEDwws91<=Awxt33uY|PP)^-2BM=3F>kEDcI9C|96Y^{`^Z{6896}#;lUK2 zCfNmIw=so5AOcWAwi6c5#CQZIXNm9G?BH$?0oYHRn)7(uYq|ae`6QphD9wCBbsC(7 zt#3exfxyVsON+t7p^JMHM<|q)hj&^Tnd{dR&J%B~FCRLf@uj}E7Jd**eWVe8-57!2 z4R6!6sL^{5$fh~9b_{a4lIM*ROw7kGKd-HzF(OPc(y1Q~hczrby&NZsLNmfRp(v8& z1&Lz@Y*F_@7cr5=k%zl|Li`D637~%+`u8zH*Ev5B+8Ez^Wnf;Nc=EXO6u0z=jaXxM z4~EcTZrntAxo!m_27@LqDH(dEUO;t|o;)^Df(mEjR!<_ZXSel*j7TFVFGq;cV4?lm zw~0hS;VG@S+C4u=y|id^1$^R0VEqn(PMz*=N6sB&&<>g?!1k?B2viVOhP{_}K7-3y zcrg16_?52ShNz=E-ICB*8v7IB@oI z}&CFI2xqI$E-vb{&Gxg;W^_~O!c1oB{-k~X?7!_?X$O~rl zS*3-*SE`FMq!^f~66&E1~OYEH3rH~(Ss%io9r?u$r_OQ=+Rj)jC2rhsZ7hUEQY z9PR!oaGCeD);@V4;73+nx_16?X;OB!qLmk8hUd~w|u|H|EZI8{l5p_;|62qEq(7EixSdlbP{UR>50iF zn@$&Z4|#LssRU({NNiwBr{CE#Dx}zp_7R9nXyCxX!Hs7Ejs;jbIEJX#(gW5{B2O!a zjZyFJG}1EhOa7pCqkY~D#MptkdUz}@F5casnVl!C+a8-x$e{VX`siA0W`rlG);PWd z3}k(Q3B6m%EsYn@TMU=?fJDB0LOzA|8}6aoP7a@pWjrjIvPHen{)_3g8Ebm~{86ki z5TNg=f-&sQ9{GCo3Rx@_kfMqari5o38uTjU67{bh+T~BXLGOY9si4$n&XOFli>i;(RzhC$N*Puu6f1e`m$?DFjnwX(00vRG9y7!E1MmYBm2I z-|tZaa&lH|2ZO#&i{7kO0Pmh&$1wbi*#@J2aejfNitfQRj}uC`!GA7_`$e#OTZ3pl zbi<;b-6qdxoR<9Hk=gt)l_{eKwJs9Zs1xf;7SeDA;zX9m#{0hacp5D|q~Zacan)Q7 zPA?ZC>C^Gz)#@dW1rtT3WEB3yBad|Z*nTaW;%xdMgkX;B&Rw+3BRJMBi4-@84q$&^O&D*IHs0re3P7B zPgNC^Re7$>`dhumyGRJF0OM0{vADke;KF%x0KjbL<3VZvhma=*XHdNNs?P65*EK-p zB3Kq`FD68l0(&qABCw5^8?O^u-Apdx^5eF6aUg=G>w0pd_Ce{ipKkO4x1MD|~dV|3eW2CIifkVXQS;f-T@^)m;t{k_vU~X%iV6Remmq_kOVx01R5W&6|)drT<~P(1`Sw(b9OLf?}-^6 zLjBV|@9?$7J~uWmcAjUmIKy-I^~g-UwYSB9A4aZkWG4-JZr9vLL*sV7K_ZmQTCG}S zZn*4JE48q2JO=8c?e9k>#85k}`d?u#-O^qzpM`R-13FG=7(dkY&+Vqn+N#N!l5RNe zwL##mxMrIfNs}gJvNWGzOek4ZKG*nO98bN8#IT~~F~WKC8#qKqxw6Ok8Ej8D#l&dK zAFqw%Le26~LRJV1X3HJh2#@6&v``+RcxuxGk-^CDPl4nxF$RMTEZ z6t=iji@(~o!mqcR>Wo)WUc8F|m>ES$$xar~i zPn!u2M2x?~BWq}=mSPCWq0>x`Y*j5L31{WuLeS@t;MiRqbN&0X7zJ0{x6#_J4hOo8 zr@AJG7CTSYIvDex+cj1hg7n@JQVizDX;$os^uO}$bkHK>VkiZFBoWt@v(bDEq-1CS zhGnKJgWY#5@GcInqFTB6_)t0Am@ggN&G*Mc=qc{ClBLp^#v?M^=J<#A@?5b%2s*lA%K_g%*Ge{$P^%B+d`&5t7JTpNL?tHDps^QGS>sQ7_J7S4+!MTeO~8=;N3R zRFr=zDiz==pVoNx{h0??>TLrNVMzHIARIgxy{}1r5cWp(gRu;KF1yTCKFG&M5A4n+ zBsRc^g88a+R%Nxh5@PyEDqd(t75t&T(q`mEcHkH2*6RG=4u8Y|<9B7y55U+|IU*&} z-H=Lrs($c=K};Z_n8RlNVxf`@FZ?ByxM@D3oiVtj%oR*#VwvhW`O^WvnaL6iIa|d3 zuT)_Imx>xzgYS-~?)K02v+|^0OOkRodd1XXMNdEqiu>hU<;+F^Kq$m((P9-Y zmmOjm<0XGr11_?M0A4LrUMF>rzsQu1KW@YA>9;4-r@rNupY0nmJnVqjNx0G)#f&*8 zm~uuJxPb>@KR_iE5*h^uO;%hJhBpq+4k)j2MjXu=T<26kFLd$vX}=Xs69DB0L-w4=uEF z1@pR(mO`jj2M&{_-M?V$wT?B!Av6Un-Z!QQPbd9j{v;-Q6I+<2+R3^+ zxZ)d&^4niS9qGCWWI$P5?w*;*)<%>HI7R#v?P;FG5UDd zCbAw<9G%=vZ-ZO6d~SnRep_GVZpxT8>^vUbYDx+iwuE7)bqf#)id=Mi=KNi8r~4MB zEL+f0b}+l!RfVlR$+14GStn)*v$^7`F8Rt@y93I1cJB|`Shakyn3mODqZG^n_B}}# zf?t8Jzuol(yQQYK8Y{N8<}As^n!_?|k3-whxUaf_#x=$-xg;Dx2T}uaZ2kUMZM*lL z5sHStw*2k4W`9?=#;%P6Dg?T;IITMqvBo@75Bsb3%Li#KGBVVqhqc!Yy;O9P8+~5_l zB7;EksIUh!PyCK;?6prJi19 z_c=h?b3Lj+Kdd>2nthcJAR_^OiO9V6M-UGini^|}$W%J$@wQX`ac=(*eLS7rqtf2C zq=!GJ7QVQl*~M^Ug)k@4avrT+8J4vQE#3WMdOzG`F2lBDJI@ehl6(z*O+od}$ z(RS}_c0zEGr<>LM#`_B~Zb==>h1(vfD&RdN@mIzf7y17fq64d>12MLos#?kYfDgXN zd_kf(?GVLssq3pChdFNJ73#!X1MonG$t82*ksZas%FY+*jd`6Qa6*n-RPNz|IYkn7_;+v}-U^gn0Bcsm&kSynWkQ8XUd zx||1NZ-*%vbm_B*l5fZ3a%`la-nmPl7rpsn4!a;!&0$rWoi|QyMkCSPu5r4=5^W2+ zY9m!Q-no9}$!aSfF90(MmPaOg+g{bNzfwjVJTe=tIv&B z&wjj6jAO}R@f#<)0xsF5OLQOZ;O>}joMN+iCknx@g9KU_snfNPNp+7v-;ll!wd;`hOzZ9DwQ@GB%P>@eIL$EfI zM4W8*VN@#Ak%4po*y~IEg{=}8#HFOK=tt7!>S8~M(c|Q5_LvU;vkgxfBdY&O@+x0w z@THU*rxxx%D1a~L{&m-Pck~Ad;+sP7t}SlEvg28_|3If!DRJ>rdIibST`XX z3myORr0pBo>dOv0aQtZ%mC2TK>UFoXl$&*+!nXShy|y}Ft9U|L#!%`t3dVf z%A|Wipy|k#VPK5=udJmaa=%!Y=|U$eV+FGpn03g7|E&A#5BiOJtI5bjtt-9nQesg^ zTiXw)SaUk|_&r+(sryd~C{xL^fUJ=z5KPbm_4%MIv1$uIDbitQCt?@9-xJ#wfm_XH z3RGZaYcAsF?)Nl=zncm~G7KBu(K$%SoyllxruIZHx+p5-&d*6z!q>?Y_TG6o;FV6_ zRm7!mF8S#%&OdxjmFAx!Ygh~NI-L9Jl2BE#s+ zd>Z=8CCQVs<$>ruoka`Lr+gM`M3{{+kIMlPrrR{mv^ofe9HonIORNr-TyvOFw78NE zIkPOhm*XAS=D`mX0UxZSB9ZpWC2>Z7LVfo=!2Uq-v=l3Cmr;$}!0dL`&E$dA4>^LE z#}P%$2_>bS^JcR7)kQ_0a!|2F_LWfXB2^z&yGQzK-*;)YlR`)GH8xLREAgzqU)9dYh|znN!`1o9IkMb7(`qeg-A3k$UOE<<~Z$ zZ9Bv+PxZc!oD&&>v)T4JVclPNznOc1XFz8j{KT;?FAxU}OnYmk5h(>~?{z$+{7y;0 z9o%ay)_*XNXWnlp!!0SxJ7Z}}6be%r16L-QB`5A4*Smt&voF|5u#LFYfj=2u*zGYW z33!WvuZCESExET}WwIa3BQ7P?TiM}Kk|pGU#jbH#Tq?1D76rN1p}42$&t5`nx+54{ zk_QftY<@@DJ)L%VBv8`ogQA>B2`kgYwxl-O=!V|tq2Bx?f>}__<0kaU%jauRb8MC{ z6d|Ll!-Z*3@3b}MD`Pfeq~odOxKAVoSE6}-tBF!aYN$IU?U@zVOqB@!l=H>e#eJ&T z%ZCVZkOOqSDghh~=^Bi*E9|Qn!I#|saVDbzk;*VYVrIP#%K%ZrCd;rbULV3oM$6a- z6qAwIHeJfS@U1RjNm@*2j$Tq98KCF~JN}HLtoL&D?C^44ck~>$fy8+A$$7b1vQKB! zADXH}2R=DTodYBs$2exTXb0eE(xbTmW#hqd>vL0C&o80RZ+pc2@f~;A4gBVqF(>lF z+skPcVGe^aoiXf_`-+Bu+Wrkg092Z?FerFm08v*i#f6yh`s_FDTq1;3mN{a*{&5svCGI~V2$*j<>LTaIblz<$yxCD*az=%>71Lyad6DL zyYzjM)<)aKQQSTdPW;{BE9qF-IXQOA3#tk;2d>UBbgXiTex{(0NZN}0G?D;4Dx!S7 zgp?3;R*QmLQb-LGr&UPjS0mJttd0PCa`#6?+aC3aljOhoY}8}$(5=Kmr23Pn0Vc zFw+UOF`=+zMI#J zr>Gl{x|DxTE$cc3uW=ZZYsSo3kg|YrbfF-zW)u>bD(mT(e*6dc+cGKM9&fbp2B3U= zE+3R@L3UT{*_x_6)cbv9&U6{|jV3T|Sm&b?p9N0%fqJDqw6t*ao~ensQEHu4V*L6+ zdUFQ6^%H^qnwU!PNng{SC2Tr`5YS`H-C!Yt)?lu~{2ABt2d{c;QkJ~4G?^3@L+xb~ zNM>|*@%i*znG2KMl)ss<^H}~m5BWW*jnclA15m=lhCPnR%1+vxUY-?oS7uXZA@c-K zBt`Yqopy~RsXGY&52zTrn+LM~sFb{o5q!&6d9JMFWN#k&N z(mIpPzas8wTz|V~Yy?ky%XZ19I}%H3d*lEd;zD9Vi!qMp>`xb5oyYpHFzli$mV`i z%}Nhqy2TxMjw5w^=@mL|ETJOI=H_(pOr=Yh8>#IC8u)5}!0gnbN&4{&mU}^vg%=Rt z=1)IJkuM||kq|?u53k5Fw>T44tJdIhZ+cQID$iC3GIqEtFnIW#V~b!mEFl7WQ*9vS z#cnJ)^jDKHE8@2&$wb~Fthi^#-s#0mW5n#YG0`bBWa4l=mb@)w`0FopBoxS z1jNQ@%NbRJV&GN}2RC-cZp-QG86axN^8Od=c%mMFN0a#Dj$Ya-b$K=!9Oonb_b&1N zp5yA%HLCrH-OX6>}>MSSS{CrTKZItGA13D(2r|D5m`wr z!-_aA) zh@DL*(YjpeT7nKb^fq8|);o=VDAs>-&JED@?c=;}7dC(P4Zs*&{HmYvqn7cwa`vA6 z1L0kzQn@u}MLuRv0MZe%_HxkpM~&TLT~U>dU8cu1wxfu5b910;kh&LN-&Ulse+oak zENwfcxWPl6)k}+?>#8&<$-SW4#^S)-VfMMK(~6a@0Ew9iU7FSw+68|!9om<-3*(-| zL%`%n+=C#uJG0kZ=t*rsTB&pLGZfSETc+X@O65Rg>az`Q650tuQmmQ%M_WArVl-b4a)6 zla%#_@~|hcFt6ZxoY8jQrV~~%)i>22);Nl#Vo81`*BBd{AUS(iF^@QN{0Zq&xre>; zTD90_77`>xLg7b;kpIE4)9bMxo7%OD>526w zeX-t8`%s0rvWBLl&y5^n1m^SlL$a*D9qo2(NW1N9PWt>@{@!B;_ZTw-JF*okt(C^snP1BA~Cl3GI26uxd6%tX0U(Ve!Sph~%ik z3ROFbABiV}?tdi4rdsHlE!RkM*yHi@^E<8D%s(esyp2~c-D`r333h>4=&@9D4YM(h ztv*V!Z|+{too03LCly!yFbzXiHKkvsOwbgt0Mm|xdeSgyu{)ocGRqmKtrt@Tsmayv z1c~@E8QpgqWxZ2r^J`Pbbg{g1=6@J;(7FTS5T$G=OqRabbUYR&`j?~`!HMOi>GHJa zD=RT-e^ftgt5mm!%4-e8)i}#1gK6?K3*w~-2AXKqcG26dqUm08)@)|HQgzily+D6f z&-TDFAE!$_cb;puU(p;VIjy9debZtP?zQtS@y_^tHJb$UGtWRPmYQj-@F7g;79E;M z1_-%YXI9ALW@2zBxtdIGHM>H{y-eYm{{US)$mpo@k+M8!Tvr54c8`xd&pA%F1Z+>r zmo2mYm?LY2#f=rb!oPufk9aClH+Nwl5-E-<_Ra7FZ~gY)`FAjKLmAts;l%a_6NCQ9 zYP=>FFFZ#(=536X)7s-V!@9V&3OZZh*66xj%v?vM-N_FV1+)*s|mZl6tF50_! z{Fxgsf7ig8^DUMgdA~LrzO(}GbS#U((guL1WU*X4fiEHAtXc=+Rtr#L@NE0PhzqsH4DxBoa zgWijBS^a5wKc!zFZ{Wk}lOa`vdzEnMe}j}Q{yrm473zcdM18{_9`D+<#W6J0{mIAt z2j=-iNYjH89yaT-%GjMD&pHR%+v5qqQ?!DL!4ok_Tc%0zUxVL!Xmh0IH)(b?y=U@W zW~b2h{zzQTyDM{CZuzX`zl%XK+OFTySlU$vw|Cq$-jBKJgXbE1cvcP-WJ1&_EvXm_ zi^OW0+Hpp6kqG1fGa~o8gO0S%;EmgFVYMiuAVc|N#rjYSMWOXF{HNHBLtQPkKu%kG zV)h|(s~GK{j!(k`TfN)Lva+tSxSJmzT*2=({pBrLs~dFH>s`PoK398Is`$i#ZB-60 zOIFzvS0DGB#Behi8*zDZa<;7Gl^UJ34y)EKu9z4mCFQIYe#0%3m>TD>1mzMZK1d>? z*pdK!VM|>emtEVVingk#n;t!!A@0ATzps>-zn1x#8OfNr680^at!fxZ1t$=pXWf<_ zG9@1^N*ix+d9o7hx~IqJ3xBt!r2mtZEy!53ftuRpr>Fg0TnBmv9-w4VSRQvG;Z<7J z%iHUEn~$-uHJ>m{cc(Ie@@rO(##djaKn99}H@ljKj;?%`;VxZV!-g|VVz}iYOX@g2 zs_m~VT>c@Dn=!5B9^v*brqb@r+v18A!JSh@RaMl;iIw@^Uy-CUyXtao=7`bC%2L6Z z;zKXWZ9gbFS!Wp;nG>&H?CC}J`Iy*hdb{BRI!%=^7L*cQ6?LOEhU8ibEh{|~%+wJ} zOBkw)Y&5jBg&uPp);cj(k*^b~x6W)bjnhx^vfs=^%NC#$S{`HlOn2n|{=UjDZFLbF z3ueg_+^~6y=BTz;y4yQv+b4BxZ(hhU%r+l60B6`|UAV?ar0As7eTS~sn z_#K`1t9K=h|8fDxM0HM*)Ul3&A0NnZzr8FxT*Wm!^t$bUDVxy`od#L7iP4ob3JNWvZeP>y=x0YKO&jUG^yfhS z5pF4Gu5#ROOKM|>D%B@vXXHWfLm?1x%yQrcSWf(#bU{=l~Kbp!5Duv z=d)XgxW?fmm;ZK`Ld&ap`|aed@vF|)nkWJ9w&Fo>efDYg8>m*Y z-Dd2+$y0dK&H9H2KMTUEO^yH3IbfI#b`QXtpUV$4uS~gaUP3?9(bQ+)hSwDa4|(LG z6r>FYCtf_ymMy_Yo$jmzk_R0AmjEH|Svmfux?)MJ#*XTo$O zObX@Gg;DoUo)lynPAyw6>Hm@#01|A-^NS{ZwrMY-!3?Zx4u#dtDUXltIuAInjnZuY zUxvv*0b`f6IeJKlSf<&hT}eRf#}R}`ViJa+s0iR39YP}c(@Y;Oc%9?hTV539Q=UR* z-$5=E8{I6+W32#ZG}~dd@ng>FwtC}*6GIId_s^{J^C4~W-L03hZdBNAhUveOx?4&= zs)k1or>}iR>&@_I)yuTAEyYQ(*CzDew_cP2;8;C)vqQmh6+NB zkIar4o8a-DKWKiYoj3_}qE!z#DB&Kyr1+$Axf!y#D&G}D!LzLG2;V+Lv>^RYy4JfG zS5;>r2Ybxj?$MBBq@GZ>ikyuJSt!PL0Ph4FPmcw10-QeC-RAkV?V*Gi7szGVQ~MhY zH2gQW$T6V!iS&RnDn0mgs(7v@KrG*Jj)2$7Tc2$M_g|Jae)s(BfVBPz`tT{a+Vr$D| z_Ol6Xi-!8#meHO|*4qY6_2<$_n7F+p+&U-WI}wE#c*0fbqEW-`b&6FzZF%G7n1>ZhG%Q;-2H3nSHTVDg@n* zd8MT}xjW7IR_qoj+Uw&PZ2WR&GpSy>(tl?Mx8&>$hTAnCiP zorTKdn3vT*1FQcK8y>++KMXM%C`yWP8B8mqm;V?Avh;pVqIG^GxBM&+gDGHQrhn?VE4? z-1jgAz9o5inqiZk*BPe15PoO2xCUb~7cBXGh(zDBt-xhyU3>EHOo!v1mY7gur8TZb znmcfanZEsne6?BU8SMkJV~A;2*orHlfiymVFCpw^l(y3P@CnFfg>SQO53cOY6n;vX zgR{qM+z>v^J_;x%9St5!Yc*fj)!7`dtQ_?l_$ zDZPvo8$N05*Antn33U3X4-FF&Io}F#l(ej>7c;b!jcEnJ%h#8+xU??oU_D>ORqml= zy~T+dasQ4p#!gSE*-7%^bM^zXTB&6D?aLlkU>O(Tl6!n(54WPeKVcd!Y3T2k!o^FK zbhIdGnjw0Qv@pMEWK3l>jH1K&*#yUQDpq#BsO!ZD&sWL)qcY(u^!}w+S`eQY^3Z1* z)M$#*)cfL&zeiFlJAabYSR=hFGp2H7>242D;RRrns!g6`6YPDGirbY-Rbi_VO@9kI zur8rYUJHE@TGSgtohWf@`N`QfFgs^qTqjZpR=5$&BqXW*l(cfkP>o95JCto>4zwXe zuMMLqMKgt?#3Xd+SUB%ksGwCu>aDK#u*8#t#@;HiXd%NJgcdTouGxRcKyUGZFAeu0 z$B4f9+z_;$Hb^$-J=R|al%$fUkI7Wy3Hwx*Fmh$>$3ucJJ!w>qGaEWx=k6pkY*3$! zCpdPz9eUP3gZ+r_{gak{d_iF?sSY9>pr~L@5La@ZYuiK3q#35kKuJ-TJ4?NKii|BC zsd#d*0}1l)42f-Wbj={Yu;zDu)fRWx!QFZ%6o~y}C0#{fNiAAH7_k~XG(-0}NuOG@ za%pkPlK}6Ydh_>#m&-x(w7J3!l)=hbp$M!UjG&Y;+=sd;p> z3tN8YPi$%WqQ$K9@fCHmpe9L2(y@dg<74X*PbKB6iAq?}p(P~(59wYvZD5k4fItKb zM@7UfpjUAoNES}fPZ^m}2(G?&$NvYVKw7^CB_(BKM0z6@$w;pqVrO%b5P>CsduT;u zN-djv>l7phB9Vwu2E;M3diXC&mG1a6DN1W>f~zPFDr zUeS^rM^*plZ_nPOJUxd+^%mtBMbuW6;3ttFQbsefZ~*ApS{NbPSBgX?Cp@Epy^BkR z>JoThBqBB{7|GYRts6$C=LgOhX{n{Oyqs{A9EmE3mcc%#bNPRvP zeV@1mxZa^Kxq$WKP0Era6g1Wlu5jSzr!VkNX=itJhVr~ZR*%o=D2yXKHV!{88FEz! zU0>S_7^w?J5|RPjGBePCpF)aI>_zp^Coc8;iSeo|>OZWrFw{;-Svl$9D#RiMnGGY{ zaKrwW55)Vc?#4zyLLO`CB{ID|`2E+Xh`ln{ytriJLpPyba-<>&0Z~P$&vkr$(|OlN zsgYrbJS4>AbZ~K_=l14=?y59o5)m?0C=J6)-00zabDFH^KsApGwRF!KHo9HfAX(ETX(98M#DERCX189i{jyBzXG8FtWS{=-FL(O|-8k9%3o!RehXX z-SAgd`qHqqkP{s)!85gyGd;jHCDESz&;RZJh_xt&Z?8IbW_qyx<$vMl$4;zXYblBl zW8_!&FUpH%V}Z)5D(Wk9P=zP6etOQE#yliqA#(31`lfdP*k7L_HBg06sv@hbnxc#> zK5XvLn4LoH$Pxf(m&i*jU_%GzJF{d)1|akh5uRDYw&wco{Zp9}iA*X+stV=x{4O6m z3ow8DGnUpOT0gAwog2+A0Z$%~P%y~3_G|p0;Y~w2!MT0tZ_cPMOd~EPl0bzFi7K4V zw_gCzoovyN5p`F7g--;7AGg?D89`$7l)pW)BPF*L9rw(46jRv!fx|B|40eB9vR%Q$fXs=M*6@=s-9SHsoG z3MI+ul;$PlBNL(wOl9dn3&8S7Gk$mLn1bZuZccQ!aC1Uec`8z&5J^BB<4gPK^_NU^ zRuUkSAdxA_YaK!N!!4Gw5HB(2 z@Biyl!qdx9UmP=7nTkpwLn=~{-}H_fu1F5_AhP)*a7A}{Dj{*z9HD3XQ$OJ<86FZD z*>$hcT;Fn!9?kwTnQ;NgzQ!G@Kt>nU=mGUYKOx>qq)KnHGZS#KR?*kfK|)d)>MH;} zi^Fx4HI8s{vQA}U2nx9jsVtPviFNcGQ=gteRZSToDlsB?7=w%Jj5Ot8@!(e;nJDO) z_{`DjBuRnZ$mJ5egAu2e*YOUdG*Y6Wnp9V zn25w!yu}^_Csnec1<*0oQ;td`MCchv%jn`?#92q2pBr&sh5AZIe?=y8u?UGwMP2_a zm)ZmBGU9lxLQ8N$hVaF znQjbBg^aCT0WjE@idS|UGs6Y^^y_n6L`upUUelHrhLcAKwN2HecngrnRC1)d1%TTl zTJw`I{`CdH$xW=UeIoGb|Ad7gfX0?4!W7mh3tyq$nI}wUPGDv!jWtC$+q+WUKgTWi zeCka=7Lvo~fozPQJ;W;}gVwqd94&0g99X74$_F!tAYKi05$)}YIHaD*?re;nKg26J zpHFLRM628o1!piY(1nkaHOlf2Y>l^K@!)5aff=+mRUoi7Cg;sM>l2OGTUd}#T1`V) zD#kC2=>GVbw%ib$g`s!#P4;p^5m(L09~Y6Xm}tz!(eNQE|1_pQFOcKsf~`1|^4cmw zBvuIga#3sV`}1Js4UP}ZNGxlkr6LI%2PI>3n`8)V2uvCT;QY&b?hyOD+Zl0u;cX7@)w4d>GGw0C@TLL4qm>R6d)y|EC zoHA+);}JSJ)BIjdPmV7Z4l;@wTFH;~l2Ox5eV#wgN`ESvn(hKD7iA7BOQU$0J;2>BlA5oXL~{cS1U{5B z)Dj}K#lxq8ll5^VuI@x-Rnt(JhpV|2ZSy;fwPx_(7h^)Rs%WZ8!`jT2SD$9c_LK7H znH%-(uW71@?=tOhFzrZb9A3Cu&dPZ1ODE2Sy=$qX_&n-(Mby%2L z5|v#+bwMhY24+-^eP*~Z5d&ojr`G^ygeY7o7~B3+a5qw$gr%hm6}4@YrugyF%$M2u z_r$rsBp{=P{;oRYwuaP=ePX&lnV%jS5SCd*eN_t926pt%>~PN|jp23#$BlC|Q;M0D zD+P7kw3S6;;Us0^@RT^Y09oM#0GG!zxVp)B{dobUs}UwH{**N~6DhYt=u^$<$r36z zOQeD6G`H0tw>QMEppTK3RQ~_}e*?mDYiO!W!P3-@H#=wCb3{$1jNi?iDXgd?HCV#q zhh~IlRZ(3Si-oZZi~C#Dq`Ki%HpC6U#Vk@c7iM-=sYsSmP*+E0v=>hdt;nezWBcPU zGABXt$SyWY^KxS%cihh}L)WOm)Ta_lJf=X4g<(o`991J!A7bylaWZz*s;@T@yu# zo)}n5`LMJ?Mu07nh;*8os*t+6k=;7MK)woV8v$*-oumdzk$C5z{o#1*_^V>fUp_^Y zSjlL03IdBqm^z3^s_3JyJOxt|Co)Q_$xjKu(%g~$B{i*4?mT;DOMXQy*?~eHKYvMN zP6>^rNtl?q(6e;kf7>5NiIW)d@}&zM@0OUV3gzL$2SnzU(^8Rzg}F1`3&$+9r()&o zLuEq~*-`$udibz(dULO^^R2ksz9cZeu$MV<(NGcica+gZG}TrhvNR*J;{&(+fc~8P zPm>O~5qrne*i=ENn+0J7uXx*&$74Hh>bkqhk5J$mS59wR zJreWZ5z3+%Ti*NeOe0qbFU70KZ;%xx9)O-~Uiu;P({l&1doTyxKGdN3|=9c+Dijl;vAaaYCD z(o&0utr791q7O~myh!rusU05PlX^bMKkiPyd0-cP> zY98kNdr{-MZ~qeVYn&- zXKN?gCcdEAoxw-!j$dLaP1U8io12r@x4<9Vp3{;p$H6m!uD(7J{T)%|cCdf6L5Q;f zvZ#ETn`;Shwd8M4RE&-HU~A$?&+G|sN~ymC87<@VWJs|VhtM@JNT$CtV*eyA*~7=y zj2E^lDw>-}^K(WV7{~BfH^OI6F?UPklsFMuGQ=(4xniI=98*gt(u=CePYA}$+>xf? zIdVfB`TbV`l}!U2-vEF{y-I$d9Zw#bQ`$DlLU$G>PySaVVac>Nl;UD(MO^1PyQAee z2&80`Hd2}qgTT&({#EUt<;CBf#8)5r_$nW3V{4L1N-50_#mLx+j`3+q{VYjtev3}K zOQ_L*k9lMr=Q=Gd$wHzl2bmwK!rD?qQB5tSiGH{Wz4^GljGwJ3h9=eohi9;OaPbd$ z`Ip!k%fjAH$jUW;$;|4|o$gW+Xi03@>pPLroF9=Q6X01i#JjExez%Eb^;m;Wdqjk% z139CstoPSpCrf8fcgbK&3W|(YW`^=HcJXB4SaX+|8K{imnIw--GhIA0^k8K53V@-O zG?bYwObw>~L`FUZd6{@RJ;N%zmeVW#VCGdEqQEp3d(*MD zbY$Z244qa(O`r=wDTB18d-BA@k)-4-TAJG!o89J0w}jHomfqzZu5V5l?d_m?dpQa5Z>{ z$S;E8ih8>GhEN|LlkRAWU)%rynw3dZHedVVh%|Qrv9ET}t~cW3D&fn8mRmi4aJtlr zm92pO-CL%MLvRWzV`;P*a}zPs8>eWs7pULYVd*TQXRIHwu^~fiCjhLpgd!A|vocnX z*{|jdFRD4y=+MI-SwX3b0bT{89G{-kmn_HNsVS-X1r+3`WBvFkLE(w0l(r;w%%aii zI6vLM&(w&p%uxoC6*#))aRYF3Lb8)1i33OMbOs`l+&vf5Uo9A2`6}nD$lXjamVX^v z`D8@Ocx2{GacvV4v!}E!>~WzzCPM0nZ}~ekS}mVyeUV6u80*O8!Ov!-W_>MJ5)LNM z5Jk80x+RInPO0piU!c3#BT!;b*@t~rIx4VJ6`;{xGEf!Ib7LoBld@@SYGZtM^IyHk zp}*GBUlhbIzda_RsP8U|jQ))JEIGo|Mo!N!xX_%@6z7J_r;PsQM4rftzsbuvp>hFv zBbz_OS!-k{j7e^J$Awmh=5&+6-aa;WHW}$`qpPczlDt?PO$?|WnqjIdf~U?td_KEC zd$CDDh$F?l(|?wiu&?qm%K63zS{plf-PJ^5v^NIUu8bU<5-oE?nD-8VtMgB|x=0yZ zo=5ItNJ7`bT|e%OW#Xm?V|=gy3qv{cduMm^W$pa{Lz4^i)yMP9Ig|anw%x&3XiwS1 zF83T#k?f2|RvVfNEwgQf7+6QM{k7c3(}&= zuqM5rfc%^^%zk}9KvFZe+*9r2L14{0hU(*a?wZZv`PVkx#z*K#-J4I8MmzD;QATo3 zIV~++ENvgMKQn~%YZ-QQT%T++-0`)KdML_?!QRAx`Zu$Tw}$Y{)t|My|F=eNupX zbHhECx@~;KPLxicaXeOtz2PJ3r?xoLsEKrU!Y6l_^9wDSvqMTk97(P3p*~E6voPoG z`#X~bmsI@h2*+r;0}k>-!5eCPJ+lK17F2*s6XuWtBM?v0hell5~Ax{G6S#Ew*U zjFK!DU})`0enBDGX+He+s|gLm_v2gpr3Xig$2fRLQdHGQd&dy_nlmPcx_Q;xM?+O1 z3JYWGBih)V8^ZQyOS-1jXtle9$V`Z79Opu-WgsySL0Bzw<4yejH$x&4vM4OfLSp}d z=gzsD|1fFK)~9iM`Zq%Jt9jj0hQpJ;6WQ_+fPzQ~j$zd_WvK`)8sPL|1F4HET_de1 zMD8qolNZB(MP82P2C(`01@&VqoSq*L<73A|Cm-?(3dl|J!@$^x@U#j9MvnB(s?qAS z9DQoR&dQjE=_R66rufxP0KonLA`cIS#s_hF_;~5#DB!_S>5vHaVge7*Nz1bl|ZbeG#-8#eh z$!CHE0wT*=339fksCN#{#RX?4i}={v65F%Mcxn)iVRdMASNzCevL7xnt?bP7Vg0iO zy)$ZbIxTH6N(70mO!O4-@@tvn(y47|W#s*5G#7^ydD~Lfu)yWDhKX10baeJpS)75p zwIOM>gPfjDVerJ3mZ`nFHXChDM44L0U}Y!{!cgwkajk~Mi9TkQKM>+%gPDW(-Pp@Y z@#Mindul3*5!gJ(*)N8YnnpT0huOQhZ=;(l9Z5kVA{ySKyE-Jy(+dBuF_q>5&Djx! zzP89iyZDa&_J*zgI0Tk4oN6!G`!r5dOAkG*^`wN0F*0#sL$4!0Op1k-4NreJrsUNe zy6amOURUtj-;9Y(&ZVF*9S@u57>i5L>F*O)^P@dJ;bH5-$m$s@@A_$KAE2qKhybxQ&Wd<0Q6sal z!oO(puD`eETCjKZWJRl^Na9RT{Sw!==e+4_r>%dOs={={Rt9*-5AeP`3`@rd)TbAB zu~1ur8*X0dtWB0IcB3~Mflb2QyLhWN6>C!~#?)tb^=^w0;vd&WWvr4%PX*)^6jG2M zj^U%{_-BuEkB-;?4-%SQQxR%QWN|-t{ds;&n$iVnc0X?#QgIBaVrgj%7sF>5y2LR* zJ4cwkBaKr#)CO2$Y~e<3egQe@u^9aA5tXmnkl5LxO7G|7k5%}h}lIv+nMbBrx(;P0xIlYIso#{OC$U&_G&=Kw9M*7ein?nuQ$}>2= z-gjYOEM$E17J&ZRD5P1fO!TE8P(^aC2c3Ez#nYdV z`G%8}kU&Cg41qyO%##9{r>&6DZQyVY%eC_=WXyxH?O3Qxzd9DXuQA1V+}O zyIw`*Vb9R|DO~N69vyIZ)Fmyy0HvKZ;=)PPZ|X67ENA8D5`fNdC5rS`>eJl`s~x(R zjMF1l<|mOq{Rz3>eSL$1l34iBz%^e;7unD~y2t)v8CGt#O@=c1*3062$ICa36 zo^ot`iqYJ{!R88GO{D~>q&Pb}Ax~=N?2><#)7g3qZS3h>y#xS`DNl4JvF#n-nXU9e zEXtv;F$qJ@GA{H0$M{O@$sXI{4~~~ebT*}ad=miBb9}VT&iY3({atbQjUb~i3#Ee* z)kCvPbw*$$4`cTh;OH*MFX*27tGo~rH_p~n9TJg*K>v91a-%SCaOch888H$^JPW4) zxIG-l##zFhyto;W_f8GK@q7*L3V-@rD=@K7;#B`V>3_)a{vN%xalBBLb8!Q3N`O>A z{`k&c}p}yt^cJLNEQT|EC>6;7$rk-qQ0Cw?rbs%G8 zoA10XkP=tXLVcJ756@}<_CJ>4B=qJ4K&vL$&goC`0x(mdKrF2X;A5E=j_yI60NiS* z^mC-LzMn9$0~W46Bqb)0kPt;+P%J|qw}JcdGWBMd(zGaq9`3k%DCwMBrXnjEccGZ1 z>>L7J?Xik%V{i5~Hov>_X6YO_Ak5Q_^5IzkM$&?C39Di1btMlUSr8ii^?7l`MFta= z-onKXfyGRB2EYCC8+J}ExVZ^1d;SZC&dKPwrZ7^1S!g?--?U*PPN1v?|805Md*6eleJC?(J+McJ(gFjKA9w3kd>rA?(d6WmU}5aTC-wI<^CYe=)-+5n z5~(sJsBsE_o7Fai0uTCzUSajy@A!qq-i?F!*hs>oa#+~fWU!-#m_QW*fdlRlrEI7V zsPMC)viS=?qB~orFuj<~a~&6&a}IVk87_^$(pbRS$;Mscq-Jsz0JtSbF2pCT6dxM_ z19Qg!z%}V&0lqCOyvqo}De_+U@8!k+PF{9r`mp-Voyjlf01T%D;27S(v3irfj%uO< zRXBewb7VPN`+Jo8*izlT1{_ixF2YR}Ok!R>fo{g6){Ss>I*Fm7fSyGy0294=NRsO4 z$n-|!S%Cg~3AM)@9nT`SGRMl@kGruKA4NcL0%Pyz`7kj;QA!vd?rwO9RdmcAbI&D- zAwr_t=YU;&#U>QJ`OJ5QQrwVu*7GCk!5Eya61fIu+H>%fc;FSCMp2p%Ru&#?aY=rd zgeN9Wc-Wianc9pNnC>g&w?`I)$KH?CsH{$|{z!yr=qwD!&CQFXJE( zQmo|)2F{*|oZj5>Dkl(ksfvj70#bsVa8smkg&LW? zEuoDo032)#V(a3`iVljU&V;wE!|g74!Cpv}zNF_D5$I@yZ_)rm>Hb)F7W19SQYG#_ z87vQFWAWlCehEn=BqR_M5k`DQ5$enD9kY(N(3tXxHNMlI=!_zuhy6G4INKy~q32I* z_eJ7to5nS;)02$7g)_@q02s~?5g6J)c9@iB=E84lOblVc;S{&d{6$`bBsBDq?`=s* z*D~K}i}u7OzK^5DS2#QSk&}^#PjETGex9V~r6Y0mVnusPf!G)`J1HqiU*8vT1O-IU z`?eRkyBp;*r~DiILtgZk8+Vrs9b{RosZZecijDW(*ccj;{puYxF}4W93)nf)aJc;u ziH#A#brZa8|0*w8--1saF+Q%O4Xv?0SdWb~gQIWqBF|`LVWN@eFD-dByUWSZ26;ht zJXRF4JU5KhQ#-on&p1C>B~D?4G_Q+yz3F)P#BvGTo~;sX|AMfrZq!FdEWc~SE4-MU z<7>WizCMk_*p7~m$LRLvkpFH(%jgyWt9>>V8Nwk`~78xMBQzo2xrpmAms&F3N9OpK{{ zx6J;|GKntMxTO!XHQIp5Lj|7?E&*r{_e7G>MNf$@hLTuTPIPDwm++D}liA)+jKfPJ zvU}M(I$~-3Ykf=D`{7swXDg(*8&cW1z`^<`w$Du{>77TTJ>yenE-# zM0zDKv%16n#s?zYo{`rvLW#FEiij3;S2{*&V))5W!uz#DQr!i__3WWvZ@|XUh3UOh z(DNrgy~@J;=_8CB!dO1L<#47O%O@t(yjeo0J7cUV4nr$BlS}WA*qBf^y~EY%HmPn_ z*vraT?61aFk;{SZpOY6mhQ1sD@V?9+Lqio4OIsYNzfhPOO-a)zQ8Fu}sf`@#ZrESx z!{)aagyy_uszimYAdwT@73=TndGg$X?7N+p0K}3C7W%5NRpxPc1>Eit>|u^PqlwMK zW7a-)6B(AkyLWvEjg1-I&;aKIDxAq0-T+{!(HF6_jOl@59$P3Gncd-Jf0L@jFp5VO z7;Z@5nXKrWyqpnoColVa=_#z2sQT6e|vTn|v61u3YYvwB5Ieb2+gi$FBNqnd)oiZ5CB(ymoZczkwTELQ4Q1yB;P9Kg zO#Ve)Yza#qqa#X=tw$Ey`$v2nEa$nEGyOXna%7IUC-kAa(J@vZ!%JH?1{dd%yBd)< zzWYsHs&SS3Gxeqoi{CBjoZaE}Mnh|w7~kkBy6fY4=~>SCw}3zJtGvAVi@YQX@GN-^ zz(QLwc0R?N-2$39#4e6}+}ouwSA|njBigGAK6Yng^2mfwdz*jA%iL5gCIa8Pou=>d zGP%A_oZU;L3C)~b-mo|O3Zd~6S|(O_(-eVcWG=&HL718bGPS(T!OjxdiQyEsEyG{s zrTQJOYh!t-s`w^i$N0-!DeQemZF)GZuNOHy*rg@KACrJu*53~yG!-zmcFN*-4G)c6 z8Clw6V`Yp$cMFP!rWtDp#6%Iv-YvjBB?%(RdWOh$H^IrbfD_#{2lHL{xVrGFuNiA| zGcvkAaD1}QU_&OEHC?o3Cy`#=$L`?)AA9n7Ve88D2|uunO6)}0M=hssl5sW*My=5U z$E0~WVCz@H?unL@jgJIaKOw#24Rt}HKjdX^rWAq5mlJ@CU4rc#`K!FVFA?JGo^^M> zV5ZayC%5nNa!#ec4du-f6a~9t>0f*|XLc6f5f>i8yUlz1KbdYOJTjf>&uR{~W(c&m zLz!HQznu}8?e93;UnAes8FRndyJJXJzq&HEcn0hfs&Jrkcn*N!v>*iGb*#Ma;Dxap zHN&4dIa1S}8%u1}@E@X~zuYI;!;ILHUiS9)+27x1^=%ne7RIzKsVR$+V(8z@<;fcU zVhjG)Uw^}|cn1B+FdoiMEMI6SjF4dLU&oF9hLyz`CYE;4bB}uJ6?Pu6%pL-qljiS; zeNZ+ldq-?7j}jjoM9;_|ZqE!zZ+p+x^(E7tX&4zkqh)cE1V4L(S+CGtX&5T@#q^~e zlb^u{Sp+t!0o<=jR0Ev~i-< zvHr0OYa?TF-tIG+;*YhefCJ4X`hP}Vc0TlC`HLG9pU=L1KLDrjYQ{PWNGfS(d+&gy zp>j-qcIEx-DkUmovMZ-JQorTdOB)*AuCceXM3R>+5tV%$9Zg_pAfRVK1Hik!93+W# z3^&B{!pxPATc=#U}ob4?U=ky#GRXXaWPV9_c+|9|8qdkVIvWbhY z%w zESW;D+aW}3%TK?(Bx87+z9cUmJopJ|cr7ll^J;bzJ@-s^Cm{6CW@R)N z2itpj=}PnxHZJ zbBx>qXzi@S=Bbp=`&R(8he?qYj&Z#Hi6DDpT;)oX?v9APqS-t?W@)4vGi!U4clC9| zzi<%k757{n(-=t7+iJ03sqVSaAXlqGq{nr=Fea$o{E5t}W5V*(*P|2{j z7BIG~W}!13vlqtr2Kpngvf`zcocG(uq_{c}+r0zl(F3bsMkN@WAH0wgY*1zoG#U1WNS~~<{um2 z9IbsK%+(ktu>ysM9nR7K-m5P-Ssuf|$O4Jd2c@Sw4vXr-{j?h(nL8$z3+d_fY1=;L`cdg zOWpYxKQ%`c;Ezl!#NffNgcY?h-I0&sOIv&cd~tQN2*5&X5jI{$ zf5?kaAYkf1$I6>J42>-C^!CNm!<;A2-B>u-q&z`N>A)-pYj5$ec!q~x1?%qz5Zg+a z+&Cc1{&yZcHpe$00HMGd=a547POliP4?`T1i{@~VNN;z{oE7-0JTR6-F}i%pk5qYk z;9ot-o4RByJj?%(moSMFwG+#XREA*X;6^}zFHZJ$#5K>L+5b$Wy9M^bP$p-G5j&dT zD)Yh5TZoydA$FondTSyul7@eimpzK&WmFE$vNh9*yOTXar7t2^dpshm**`g_FESIQF zUDW-~%gs3zK~9vmedb_s5HD97MBafYh4x6Ji`lv0541ZUNb$ADUZ^D4SAv>{ z_$qO5mQXXXfgS)jT57}8%!!dz4FG&w4--7BQN`6#8mGi6qWNy#b!B<+_-9K-_pae+ z1b2Z4i&`C(5ejVl8$f?dT!1|mq0RiTgrlD5#?~!{xdT0*XY)fZJ`P3*RemU3%#nsA zqCP!kpgImiGY5SAd~g@p^4oI*8do$-HYV`G*b0@84^nqyo*Rnze16JcRXm24PWT0Q zWA5NWa?=O4raQ5-wMF9PkIY?wP?g8t;W>l(KD;!rybF@`Kir?2>d3;})Dl;@4?%vO znA(kr;&o`EDk1W~Q1XM3@J4s{v0n1 zjI1dh7$rbzgPn(pAYU;yHYPl?2xEGB7&}8}`o77_WKT9;$(8IMZxZJ##9koB$4iW* zO%R{fj@kX#iR|m#@D9Y&-5ym^Eo+~rNcOVDLF7Y_j|fvoPdes~x#yOyf>55kup+zs z9h)Bp@OCi3RpEum-WXv(JoEcHe&l?y1DBUiP$uP4UYdfTiPc@7%4IJ6^58kC<%48} zi->BR=JxuWmQ+uSojqCD-lHmBj+LW`0AB?Tu5y}Zwz==FV~Tv;F}L%$TVq`8t$Ai( z!^rX~EvY`(IXL1I;E#*>3m!gG@M-e`ZZ9YZap&Pf0W(K8z&Qmm5}sSR6X-9;)y>J1EL`|;q39qofHL@VvFa#!Ln_r%rSh=jeC4)JugEL;cxKLB z)6b=ITUEQ>z4u$)DkV!Iy?8s>37g2RJi&-9$kOLi=;BVTKfK@?en#>Sf1>E*ExH>o zi?z}j^`OvnHkaakA8?v_*k6=Ha?Vb67U)RZewd!H9{D)7GlQcq2CzhpHoOy zaxz(kht|GSN}};>2|s{q!B@KF&JRQ3H9TXR&6TR&$GYdwF zGE#V~9bW$TkVAgvOrx|mH}m1c2U_&~EI7RY$Wj2)*bp5pEwr_EGBf8u0^G}U42@a{ zNB|P1iBYUhpYl5^qmyQWfPKltxOoxR!UT1fF7xVB3x=*XYWC%mc<4SZSz>8!9KF7s zZejePkzuB+Hk@_~#tAbrtAqj`Mu&Q6X>Fygvrkzk3=wC+Y^2lBi{0s9YG{B(yB`4G z@;t`LSwgamcX^iXjy76bTj(Dd!xxlLQF1aVzs2EqSs57_$0ws;GmkTAb`tP880qa6 zb!fw&@58d>K}Kd_#zU8w?*U zhgh^s(zz0^wS$4tDFPvhkY||zLnj@bdisY(m7HAx3G?WP$i)wGkeT6LI{S?{d~(>| zOpElMXP0;PSIn1S0Bca0gTf&y!Ls9$G`GD z_OeIR&8U1*yk<`9a}#ukzO}4qpAh{C$wGry^jY+xMXc4+$IXgF%Bnwr0bPX~J$7#2S8c^Db!74zC? z@9bu7*+a{5%MlDI4pYWM@9ID`RNflT7?GTp)vEKl9wFvF+SWwyYN)s zz#s#IgNlYPk%E3Prk@tEuiN!28hl7n0L$c{=v5~@0|ShX4hmn6vNUgEU`%)hfFM@W zIMy(oGK*72rpyjPK0AZG-EcDIXqOcD^+`+D@EFP#jE{tk{9aRr11;}Els zwaA_YMvYShk@48(=n-?py^Y>s6TXm)bHT#UxD|=D+K#Q%){hDj3ziAS=j;G@mgX3q z5WN79+={Q90SNi+4EJ`@F0>2`4KO$~jyLF6@|v@|k%K;_M|y>?rtmoJjESB(9Sc;) z#Zi8>nO;LXEuDSL%sY@}a4bwQHZ==K*yhIR(6=d`7@D*yUj35!2~np7m+*7nLO|SJR zz}(C@^LDRtZ#Rz(iXOMp+R=|mtStk6_aZ}G9kjNzia5if7ePWkH{%06qUS5V>=OAo z30LnII!Ca&B|?r_Mn{Avq%WQ!6T_?fRBvEH^gxnfd1e&j`24CWx0Mmm-_?Eum=b9aG?Wnjd-fWu~D^tbUy%ukJBwfT`HKbG-+p>riCq0{O>LPd$aWEsQIDg4u> zr*C8$Uoga?s7be|o4%t5qj7}pZUg4UB}NBEad<+2&@^sT>IPspPcb|-4~idW=`*w{ z_vBs?&l6B}i=NHWFZ`^8P?1HiM?~&s=FF3fnw9|Y2(JwFjp3JppabLR7#;~0rzaSh zS_J$w94{o}$W#0hDgoy*!^RmrK@{YmpV_ei+JzpyzJn?2G7>U=kBxyoy>hP{8fhjk zErDPAoCJig29^CMYAZZ5G&F%vW^KGdxo3Cuj=pW3 zlzz zI7|0R4ognv&7&B^Yu!C7{U4^iWf;4|uJqq7ngHNjwlHa)M@GhBofiISQ(}ty4nb-4 z-fAA}qfK0E(RX8-cM%E&STGsUulTLKlm0OizI6{a@1lj??ruiMm(<=V@2P~I$vHdg zZWGG$Vtk-i_-X>1eL;ApOVK_&W~9f^#mI=2S>flIwO;s^W*F@6!!FkK+RXHIuIla| zox!{A+s5p3Gjtm|Y47e~#Au|eqm2cZ7oYG*Z%6Cu^K4*9{AU7}Z*{KCG$Hyj!D?D7 z_u?rfhM~)VQE1Y)c41v`GA-gwFM0t8`s||4qV_ zK+v^vk1`{%zP4`0`bBMp#~k)~j3Y+Y?1bGi!NiOm5tK#KnDAx`Ep43`XBG)7BQoa6 zQA{?Ea(}jnJoL?C{Z>6wv&%>_3hu>8h6aa~y(W7W85|yD(Wk!oz&bOc?6a2EPDU+D zge1T6n_i1vSv$P^ZzD?tg8}@0KfzEa{3YeJpzmm*tkg=$XH}E@?Rb1yTRA-6;J5Qtv?^<& z>LQ@jQTpb1MVFepI!{?UBuR=!wU@uOP{~29r-HTfIYpUKWLBQW;tgL<3Mutf=BTWz ziS+dzt0+DQ7mwPvFN!3u)hHy^Y&e{ye))hTh36!zz5Uv{YMij1hV|RbAiM z7iv$Hx~P5*u3J;_3g1BS&^PyOP|<*jT7!^UgRe}L!uFK9kCb^zuf%I&?)sc$)gRy7 zBWnFaiiWW3lIpefyYwo5guc8tEBp4_dwrdD_1?C=vFl=}ta|G!o|M9B z5MHMMZ*JG|xo4VizESVMSNbZ2)}C=@%Ca)48*l>h`u(^5*&6OQ>5E>XUR9rSD&fy7 zWnI-zV*TA)GkBsm5( z@^DW#QhWXyb{P!~4Gj$q4ejs}!f9W|F8(uTHZiwoSAP`D<8%=W1z4J&$2vPo-Lv=D zLI4u>MJqEVGs{jN0L!EAdGcJ3Bn5Cdz4*K?%qA1oMF%Q!0Gri}d2SxJ$Bo0~L+0BA zd`@OX470`h#Sbk>ELms8+7|3?AHU&RUcfYCVsY7t49H7EJbqHovW$Y;z9e23bDSRB z4hMly5SQJJ-{-|D@}9NY2=W!=mHb!MGqGg%uIlo7@Opi?Y*tLBS?s>A&cwO$`pgX0 z1v`?g21M2G@qe+5n>mZfb#9R_!v#HdEGuF0u|nX1F@V%EIcAGa^S zZ}?rSV;Ai%v91eWKms7(Ud?4*TvrJBaM(TgJPvW)jLqd!))ToY>j%E5nedyEo6{Qv zz~^${^7(Nt3m>jr_sL2PdhVB#nsH9}z(EKVq<~k<4T#=3lwXSm@Y_TEwK{g+EhjDWqVSP}XO%gm zq+Y>hn_tUI0tjDs;VYgDEb!vVLriWjKBcEV7UwLheGJL!o?Z~w6fYGLN^fn16kGaN z?-io|rWJ45TmY&!=I2)S!vc!~zoN@3!Xlv}=3-ao+A*(q$LT`~_^^sNbMv-vjY}*0 z)WWjEhe9ZT!|fv|>O4QcL{J9Vuk0yN^JPN(e$mj-(9qD(4lk006FYOrD$FN4Q%8J! zCik0q2}m|BRPEtR?NM~mSv+fc!NKZt%mah3?@*viB3-8=z2pcTW6Lad-lXc(E4qFe_w33O zuj#VcT2VzsSrvwvX=tD6V2?*+XCB z1NQE&qP#d8U19$aQJHP~)>2IVYH(kutk&u)|L-!1!pr8Ji7f9753$4lIZWH>g zcKp6st{g2VIYnqpN#)@6M(jTD&G&I)R}qN`3B;$C@KSHY@15t)=_=AvMBKOp4&8Xp zvd6{8N9V~F*HTjwDXBig)Upp1kCnS;tJqz=o9t}S6J0KsE?r73p!&WR@M| z;`tNgh}uL)=TO%)j12TQ+@UBni3BmGX#aI4YymoM9HFfGF#EP;iR)2Ro_oUBXeULH zf8g)`_TMN!aTA+_%AEc-6$egpq(XST`U#^Quh^ZJNpf-`I-#ebeS{Dm4yGh?$Loshw`=gp`UMP{h^K3o3vANl)#j;7|uJM88mYAcF}OGqLyK7*@IKH`#5 zSvKm)kN7u5+n)1xxwp4~U;Yq9aY-?WLVra~9g6{w-6npm*+oL^%DS0cuW!M}7x*VQ zRCSc`r8(Z-uBCeKL5i}n#lB7E;I&W6hX!4*YRS~;NJ)w(Z~JM+jUDXNZRYR){SRbT z-e%T1#bx0)U1}OB@$u|D^ANjF;`7}j?5sG*_N}=j#l}#6t{ztaoHK14+L1%bO3bue zetqAAALxC0lS0v}c+r=VL%(8N^z$vuqTc0Msib9P2ybO`@7**$jJf%aCdityEC{XSa1srgS)%CySuv#I>6vE!1BD$_r1Gk&+ggnzv}eqs_CBY zs_MFaci&e{da<4w@tOe@EKb%5z)eQQk5PH~u&~qHM-`Kx)4N{$4lD4Co|Sg}5HL(z z!sTL!I3(q`Ya=+CuuQ&t6t{?#(esqg@>z2Kk*C@fNN83<=N-q{W}z~x~` zRCTCEP*Bk1)2zH3``IeJuM7ye`?vp2-ldW1_YW;rhwiRvuw1#S_?Zd{N`Q@%2y)#o z6+|iR_AGlOnp-mBVswT&56mxiI34d6I8m+uj?MH5*-x&r?Sisq{gkOx{M;!ovDrHf z2Zv`{+W?Zox$7jajhM(JWS4V=%#0{XaQ2Y<)uAZM5<6~>0Cw$;)IcKq+cH|Ly z1<%&c8&2BqV%pO=q7oCJg{J;QeJN6d<(c-CK;D9zs{JcuU1TPyk#D^w4GzqL$=f*# zheyYIr&Gedt5`BQj(^j*Q~uBjNa-jDPFRIY$(ET(iBobdNd00APeHXsxo(Kd!0OAp z`N*T6zDM>#3DT+HIpA#l%)s$m!Us>)TfJPvw5H*P1}?xSRWN3xexw3KYWtOiHtsT7 zU}|Hf|2fX2_{?wfI*&w@%62cHJtRtlQ!+Gk-{<9h0{}m!`4Wo+0S^0+kSvqz!;wZOuD6w=gAmr;8$BF%_C zen!q>%*o%alEU#aS}rqjaAfiItl&&)oHw+6r8A*e7O9xAnte@F$c6$xqirD5-pJ39 zwWXZ>{7Cep;7d$rkDIMa-6}Ps{y=0sjsDy&MpK^ZtO-g-#PD4WocHaM+h5X-@q+o= z-B5in$wPv?2(_Cvpz2HUpXWo5D6<2uC6lbDBY|wT*Es9G&Vk@#3W&YQ~QlTvK~zA{SBV@f=jkZ{+VNb1W_newiQ z1ua%WkDJ`65qzs9HH-l=Um8>I{@yh>!d|Mw#rDU~8fTo$mU{WuV7u&`Y#Q3K+?!?o zWdu2Q+3Q@>1ZEqU?0CM#I+1M{=|r#0*LY=jHrDR(R8(Sb7D=l0>6uXDpJt*(DR!&< z{J!JnL18OyTI_sYKlIJ`q`1XY$pi;3-4O6j*i_FZF+p*CH+msWD!0iXQzqJS!f_M7 zfuengbh%t~RMv_YQG+Ja=cCvvOI~a|YU42QaVocye&P^x5#-S`7X1Cyv>LY?@KA-nhgvi5PZmu7`?I%s|R}3NXk}d&2;=oINvRr5FbzY-loj?DsH(hh}gl+Q%j;p?qp**0;FV^z#$xoFnAIQkpI9jxqa>Z+J9tj>SYZwW|Tk-cj>jr7+ly zY%od+JWPFg^wec*?ldYokwe@5vc-GxxBZ0j8Ymd2?fAFvv%!~M2MKI+($$@l&>j*ioP6&;GUtZFNy0q|?weS=HgEQROta`{W+DHEE zKN`f%zs}lZF9dy6I&1&id*#uYKs+Sq;y&Np&ABk}$9fhbarR9VIZxm_*_VF^uamAX zqEyk~(bGnY_f;j{ zvBk;FQ>iaWd;p@FTg6HRdv@u(~a3Cs*+aS|4qHTx$1Zn3ec=;hi*d^SIWKiz2SS<_kI za=gIOdlI>FafRle(yAq_NXd`pmm3HFdKM7qDBM~6#h)uN;FRtA4R#)P4YyCEzF<+x zEBzDV4DdYk;KWr@oN#c@Bk&c-XY858j<4x39#Aw9tI$Bw}$pzDHR8 z#E69r90$=Yxci=cip3PG{q#5#Aq6%oL4C8K%gOEcCo`)kG=d>^_q3%kaYQY48k8jeec)rHZ9001rjQ{d0Da~@CI`Z)bu*s39 z(oSQ>y2T*go;vNH+FC`Wfu%+$3@&n6qr0u!Fg{UJ+p@U^aHz*N>f`i`!<}`1=UbLi ze_(i{4;i7NJVwHLUEKx@@tCf~i_wVE9tyj?_m9YlIBIqjuF6?Wu-aBqh8dnZ>VM#fkEim4@XF*fHS(y*Er|~Um80P zZM$_=ZXI580otij#ntU#t{1s=@90>hT)$(!GDH*$T5xwh`_Np>BV5X=-w{o*7UdcO zT*wV;f;jtj3VF0gh1=r2K{&G~@5qhXYT#UobBZxl|EMh(pBg(6f2%MQwcH;8*Ew{0 z_1*|6T9Mky_jbG@74N&>I>*=<&cD(Nq$1MP{^6sZDXRD}J(=C<&Oip6<@cM|_=Mq_ zk1xwxdFe?ftPhCEf0obT5Gz{E0e~2|CC6QjRpV^v=5j`BZOrvZbStUed&B0R_@HKO zDz6Oe`<{tZk7aR#W8`U$XWkUn3JjN&Cf8@?3Pi9_Usq1DYW4^tn`!!1-=5R+oqbj7 z<7!t{pyc?18?${{em!4osWUpaK}^?;aRW$8bN6~L4fa=WXmgN`mGzQ^+q`ISRo3&+ z6hXp^TQ9MjjHCA);Qp6HtHYtzk@(HTE_R&j($5g!kee&)0==r0EH&%Jn14zT7F&#H z%Pz=0bi1^#c}2^{t(m=JvP00Z#d;n4i0-%D?F^^VlcsvWf!T747l5D?;P(`D$+W^8 z?N-D4gb=m}32ZV#u;cb)C+3@Nc${pWjxsv%mQ@vg z$7F8OIh3_48r8}Qm+4oZaZ(+>2-cRmyT_N`@EGVm;o7fHbjZEIy$%VNoGc2#zX@+b z+*o23WFn`{_OE9_7P=n^=s{CfQ6G>&BkxOTr#|a64W_1kbzuV@P!BG{|y@HHq)sT-7UlTsRB_%LVVm^Y7se8{i z07O}^rKYV;8n>i#HRYx-)m2Aldc=z7x^RrDN9MibHl-$epqD*3rWs_~ovNfhIgbM- z{`u({*D|0o)Uqn+0P*w{$EpzIP$I$aOK3pA1NU=Pkq2+5kP$HRZ4EFKwH^}g43&S_ zMn-4lw+A1BjGoU2M3aN@Ef?`c?DpWV&z$rc@T)xIeTD^KBiHC{bF!jSs>BZkG&Y7TAoK^;&qB|n&m<6 z+*~uNb|&4dt&RJ3D(1!5VdB&pZEVVuJ)^+Fa~|=k@M#ibVQkMqg*kH7FRJ_!tLh7O z_w93hr-ueo@S7YS*;qU1{*{4idt~HiseB{Gs5qHIaP*MO({DH9%?g*>BO6rSz5x$x zRFz$Q&i@f}^AJuUN*NoUA|yTkrP+LS)Q?ZX^4`b0-k0{W`i`spz5kvM11~_3Vz=fJ zdSf}wrY^9yA?4dGZsc6CU4^Dtc~8a#o4?EZmVXsJC@uAwyu=?8v@(@PrRDU6l>jDCWrT~qD+Z38qC z%@W$0QR(2W!HAzi{q{V0y|V4#yx%Cc6 z&DGTMr?-|~+0{~2&$1W$ucK^3O$SBf?5OUvsJ?jyU|TbJ^P-(NZrW*Z1PgkrXJpgW zpJLr2B`-xS$b_x8Whd|ns03wlc>KZG@agfbg=M4U^_cj(ABGRudGH}1kj~~1h(8Pr z+cv648f(r~eI+ls%sFxTG9UcsA1+fp&(lN8Jui%MhK89wSGXevS;i83ni-BSc1@)P z<(losYAF8bMaOJq8QrNrC}NZnE7*;bhg>qQI+vLw*yIg;eQP&vOSk1cL;R-A-nxj( z8%_#s3VMgtjN4&q{_*7`Z)W$0pAV;w+qyn)D_4_ZbL4Rv+V6a`iyZQA+>W@`7zv>Z z)HG%8l{}=NA@G2WTqMPJa+O(zgvYU*_ofX_T+MeLLV@g#MLY&Y&g$AHL} z<1_5BNifHTtQ9J?5V$olWSqp&XrL^q?JsZ5zj?P>!{9uobiVQ5(oKw?;d-7{kmwVq zu2f}jFwUoex7%CiTvg&1TYUA-2wG%h4jaa29IVN_PJO6y^_dDLZ1V2=S(H@*JF2IL z2G@j+cUjl_l+bYdW8>UnPj4HBB119T=qNf&^4ye&?GJV#bJ74ZgOVbjh;XlAOVZjJ z6K^@O`l#Ac8<%W1P5~6GzrySr6_k{;u_q@etkpN+?OZG!++$Qq@e5Rnv90W>bkr=A zau&3&={;i@4tbkyt{BwB;9a!%-$cxj<(y1f?`Eb1UnWpc6~vLxuWHkKx9xmWCrt3q z3z$yzZr*?L565n4aH_B-eX~>Zs-k(KRFn0LjP&+@JTuA1jMt~R2_o6(^#b6eb{-+U z-E4?dX`$9AyMLyus6s|9Xh25S;Fkd32K+C3`A8r&lXpIO&=0BHId?xtr2Nspe_foR zKjQSEVj=`ZvdjMhw^2q$#=X^-f^av+pq2EEmNGb~SL_briLoL{SAa&gr7}SQ9{N&@ z%^9yYgVIHbmhji1!Z?{JHt(~3R^Og>Ki#%Rs{F}wrshiY0K~|{gYb5l$H~I9l&EDa zSY`C%ho{nmYoxX;c*eQZ?S~vQ`WmYB=jISOD7Wmr2~2?L*QEVN1dV~5YIQht(rDzC z6*~RfO&6{Y2yoKn)`gDqPam>a?X^JrxR;5zWx^eWY1ko?XG#k?bK5RUqWYxXzc7%b z88j}myTPiD!iJ`$&LGnknW-;uS)Ie4Xe=A$y{JHP@V8HtZ-j+j@T~)TKlz$ysk67N zZ6(8XB`iN+OwTnv8(P!}+Ho>wb?sc!@taC{+Mqyjzc=$RFxHa>tUg~1>8{)BGZ=w_lB^F|KSQt-=%ffWZ|o2y zvyqVrGynaCTQ)%3{1HnAz}fyI=6W#8e{dzzpf#92yovPWYkJR~x>YTyT8d!i?D8${ z)6Zj`_WDz45+6yrC#1W-0_7c~NO#c2_C@mQIA-6z5#QWN?X$#=gS1HszTA0YimV<9 z$%ip=(o_y201$Cb!pgwL%3w8LVb@%3()wlD7sI-l_)gRpEd)-1`P;Uqn!OZV`xX4p zu?f47NYfCJCAW3l=<o6~nZ zVeC(j&&Q{L6IY*Ab#X%kvQ@p`?*p9P4>JJp=S$1Ne(G`VqTUGS5f4UFeY_iBARpJHmw;mFyJf(xZl&0GCS zVfejdq1?0x$f;pqYNU3@`-PZ)&Lpum`*R?M@E--CZe+&h6&}ZL-bS?YDWSp1NYW#8 zhX-{znMz7H2?|3Hr|BtW1uaXOYlgH3ts4cAZ+6scwsL=g2}*0T!98W4pVPl(96Zs# zHum8oQ^v32V$Y}Tt39heYHHsHXNG@vf1zmo@Z~>+z(D-}Da4$A{ddoQ3m91X|L(!a zi2VO`m~eOaKaIau`fowf9aGpFp{=dW1ptti@|k8$8T;pee_HlS3=2cj*492aIEaaR zpqN`;me$fD{HOlwkM(Ad=<71#7Z<-)igRjN`wIk;zqSj5LuL~GP%~H^E@E^g1?d*cQ#G_^o z99$B4>_}Hv7Z?ERk5LAE_}F*>1x0J29g(eT+9C;U2MVsS4s{woU{;Vr zP6nMSJidc#PF%*72(KhuLHICdSy5s*#R?@@Jr(91HY6*N^6>5TZ1j3l zT$?0_!AN#SvGWG2P} zIAaM)jVQa_pUJ-zq$J&|Kt~|5Ge0$UC16>BcqyTbsXYTaLA+l+8t)8|6 zabg&styUk*-g<{CcDcVp&R7{9NH%+^(j*5V=1w19$f7|(mD@Sqq@YS?>^V@q;bjfRzvq4ZV9#U6UOnsJUuNZ_BUw108C<)ZJ+2XL?FGffNn`-nq|PE z1v{EeNL{>4g?`(#-}A?R9x{qn-6huWieNPJn{CKnmy*VmbQaSS3m71^$Hp!%Gc!g# z2TswVh(5b98?B{BR!fPc&h;8+&nz}ER-t7`*w&F$N~-AZf@`J}-)Z0ZeS7OTEd@VA zLTVh(YHQBoH#U>F)W5>*?L<9cF*CDn*HC>057yX%@i=|k?pb9SB`I!H)H#z}s(90HJ=vMo6%1b(zK8DC z+8k*!Y#vcQ+hFWajTzqVsq%P2evp|4X;Dy9#wVL*=B*$s7SYq^SC^+Ni=atKS*mil zT&7pmCBM43us#&nAJzL>*cYGJD^M{se8^EFB!JQm4Sl7d2|f9ZB@6JakBh@HGB&Ow z+5KTL1$KLRe)`1$sSaAGFt8?PeszNQ+b3vLa)#B9LNt(;izPt$Wvg#}rIRlMM;}M$ zj>TGsb4ts0)q6!=PljwWyDB>ztj_zX@72byThhRQ%r#-{ik&EQ++rso3pqta%>1~= z&|+U)ofp2&taGSN<}hMK&p)d&AVrHrwLyn)RL)YH+rw1cb2+k>;}Z?T(dJ_W;Aq)) zv({lBsskot{93g5=UrjG{yMnMk+USS8IP_5X!Yg7{nJY~=Pw$>utLtm4k-yA&hK?? zg%+u`YHl;LR=ew=HsU9P)OE9eh&pFw)b)utYgUS0ok9m_vyt!Tk}1NpNv@VxRSApl z$_(Byg^gL1qUwp7*F*U_u;Z6P$CqWwZ(vsq=D?dxgovU(Z`@S5=qXugZ%PP18qbl} zfdxyPoyEQ{0FnR9&%kv?ae5pb1ld))R-7>i5Ld&&uOQ34RA^}3Qv*qz0h{vD;I=-( zXh;3W?DE?45p^13Ih)d47IB#gJ+9Ra6ttJiEJhi`FG@xmr(P}b05e7|Mi6Et&1tcy ztTbK_WVY@GI&ejH8XvMo3M_S24hl>-Q$a~%-xYl(kpHs0;L^sYaGc)ZZ@vhj{NBe6T= zjvmxEOT(7LY>5>9bmMdn=I5hEJvJC0d`|9btCh7QS??;fj8Eb=M%swX7%e8jFFGzZ zyQ~%)z0~YK8QOVV@ZqXaC(CS}leb=C<0ws?IEW=DxLkYBr+${#I4;?<@>qJEI*>F) zPRMVE^ej<6V^WXdV~ET~EpPsCbr_MsnviCGIt>9AOrw?8D&az8H-0!w0pga?E_a9G zYwPMzYpsQZiP9Xoh%+c#T^9bvf$oEIblMs}rA!o1QM8U;OKojMcZXNyM``h5YkDxF zPLx4q`Asqhmc@G4O>S20g=WlcK6BBBG!JWouN8QGy;yyJ4n^o?_;0jPIox76u>@53 zU_)2p2V*i-?e=QV&$X#uuBW-ggYqi*DALaW`;)th_nlpHBZRu{OTyj94c@qadxzJG z+=Hl?*=IXfR*l~M-oBGqhrTDH>zy|>y3H~PMx@xTV~AzxvJ?es-m&Odeix~yg4Sn( zik_=L~+~gxSbS{FFvtj&Y+WZgFST-F6&!i+FV!eHgyL0{V72nD0aOB(1!Ndjvo#yl9cwy~45Tbb;VKq%G>Z1y%jU^F;Uwm-r z8VfD<$9(&-6iKFwoH^ZZD?=WT6ZzJMilaP|lCKH7Aq-8eZ+&Vk9%gsK;5xb#JJ>jx z`|O(=ucF&?(l!*a5!YKi_R+hFKjjXAt`EJM&~4HmwR3NGbxL$R|61{qFXP2ajeZ1B zQ0b||_u?djU~lDl?^6pY;V7P7UM^mn7uUTyce=kYWz=si!_D_OFZ%Fzno@M7kHiFnec96gq*M$}M0J@TaYpE1 z4z29+Mmzz&=_ssz{to@FgCDusvPWH1LT@y1m4!mWAL;`nRa8`u4nuP`{j@$4!sfH| z<${*3smd9ZTwG!Ae9il!eWNdsW@gEaEk(a1{PrlF)U11UDOKV0Ge4_8W={wC?g<^z zv>mJCt_26Aqd&Zm0p)3|!RABT>kyUl-hQpCtr5w|?!*xGm++(|R>;ks5>dUv$a+z$ zx`vIb)fc5_A2HTICt@R_N>;o2S7560DXHd=eK5muhh7)RzsZ};gD^TCm4cslWLJu< zF&$&VZemGRyETd$O1LUepT2R>$V&aY@aDd6f`AnQk#)M-db4vTZiRRMD-2OVE^ zCh8_{$58omP4QVRbvRJ-0f1AvpFL}M?0ClfbO5lTfqKuRtz&72R!G%=^u?NNyMXQw zIVZvwD?L7R8Luk9t>dFGBb^=4=}a8&r9=UbuuNUHP7MWJYST0*5}la1EoOX5LQYh0 zV)JD!P6SW~@9aajaX;YHRv8~L`|e#daAx%FUl@=@7C+vMw>5oyg0-i&c|oC9Zw z-!4#II|ts_3%}ron&niJg;79}L_K|N!dO%hOGHG404q+sH%FU3O23>~cB=#~i2YX6 zvZBm*5!DOt|41lKtwHm1$7v}wWr_%^!P0#V|up$?9mDVj_Wywwu|2hlE^dKU%2 zl4@c2r{g(Gx-W5sTI%>^Rgd41{$i8L|3`%b$v&g!~sGu~VGR-Vx#(pr{^%Wu#nn9XkpD_d5r>@FOs zt*zoh0J~q#^tQ-m8T%`U&lSylz4pAS*<2!NcEgt?FCYT@t!x@gv^lOFP9D}-DE1P8 zZ$k5`Mg6@e>t4%LM6y71nFOPX>Wa+F$OkI9l%A$Wbw!3;zQK@n zj}cgfK|wsfP*-vsXlZdj4%|UFW&SA0n?bG|-R=2BqjkC7*9(_1r#b+*T@P{W#FJRt_ZUvI)rEO33^ZhURTd}Cde%3gl+1!X9I zX?Fbd9Ow6GE15@;Ii#HUXtJkZRLg9{)AUmVEVkfk;ZiG*c1rC`liaZEk8E1^Pi#%ck||D){0tJ3VbbHewA;OTF3)@A zy2&HO_W-Z26LiFdTun8cky@NqN*mXgu109ZlQhbwi&7lX+C%{{LNp{t#h z{Ux6(jyYU}dWDDYDHQ`dhL!!L)bs?`MBZPqCudv346h2MPMRz-`JV$x*~eQgpV!V= zAtLS|0k5}Jk^ag<#i7_Akvb$T?Y`nGWb9db^SLRU8SXY1FA_ulE-%K*rgEfwV`z1u zV75IGRk!03b$ZarZlz+j+A>3_>$6E>4eeaRSM;9_?0MQUrGbJ7-^-FsjJon#*7`S% z{kWUA<&*OmjVszoYN3kJps}Rr6QDBM_gy#H!|>Vpv{1gWH+H;#az)$|L$jW%@yTy~ zU+?1LqLySk^BjwiFcb(}$UHY<1>M26^7errnO@(;n|=lP7PT@S@4#_F&Dri>^H*YK zY>1iu#M}Xx%orVROS&VaoYrL@GUKr1cC=-c1}pJmHyRiU%_u7l{@H2TU1iucc*vhQ zQJR{T2Eb>AclywDVCzTeN$%h6P?1U$upE2i`kcCL?fu}>K#trc z3S0EfLa?2mnot1|XbDxw!K;Rmf}sjNYzHpInJPte-^_Ild6bxlnqpSF(T5U7(WEIh zO-Z85O;}$X;O+bqwW(~v_qFO0wsjEHMOELRm8Z#=k8UaJW_o#1hcn%RjrREwZ!2=y z?hQY3{7_R(l@F>x-1*?CHCTVn-b%)v?X218P!|z0h0u;BPw_P?C^#4`8<1bMCx(9` zh@sEXW+AjN-?#gGBVj)RYWouU*k{HLp-h0~?`1pF_DNO)fYdL%Ad`BXLougV|rA?I!V*&{&#$tu8v}DXnUpS{@WW4aslfd?&K+v zselHQ=x?H8IT%mOX}CbEN*Y)a$8?!}xF50pJ2<$HA3hM^=+{Cj@$_qDJcC~Kf?kEW zm0|b|mI!ED#1#B^I#Ru-+`hq6_;XC<4wjslTM|FugRvQXw^sswPWv4$n-4%m9~$=2 zmfWL~y*(X9g^licDLXxlM*Tys80~@%M%AyCUo#+O4T_Rmk z;~sB=#u&7|(Qd2p0oi=4cYoKJ=SX^y{U~GGiyMzOplNLGZB31whh&JUk2`+d7&EZcWgKb=3@ zu@ebTWWJ$woQpi3u)>Jl$IX05F0c*3) z-_n^QUM)u9Ak0HH2_m<9@9eH{pSTq+!pzQlS7c>vJv2Bt2}pdIKc8M&l6rYmS+Vr( zKc61E{&TJC9vg{~!P?pwfhwzwH&U%~zv`zuIL3=$Ck}M-F#r1Ul`9~y)JmRI4(w-u zIdNHyrY~xGkupha6hER>!=L^_&J`~HR72tDov{&~&%M!Eal1dQrazGsSw+ufAg%2E zk({$18&(*WfMKQuWm7bOLwqcLF?sQhtX91IAdkrhybGPWjxYN!*P5y)}H zOd~F2z`{2CX}GSky4W(Ed0+MUm`sL)-C<`On_v=P{N;d=7N8BUKxb}bPyJ6*%(x)3 zpUedVT!$90`ziO+&FkrQ+(ztq)Fli*!o%Xff8-ms$rq@LtKjIxa%oH2M@R8KCK_n= zx-M)jP{uas@6dV-7NbV#HJgJzrRhGO%|S&?U1PkNLi|pEqh-0;cDm!6$F}xTQH(}` z6GZZTYS>SoGb&n*?;cbsNrot4p2m3j!D#(=6c{R2+q3NiundkIYlum$0$+u6GdDhH z`24(q&t0v%9`X@ZK&LEaR|%~W7optY=&hj`{;|5kbGklyc=kf{>^i8EEKN&T%^!-_(2wNj6pTmgJMUX ztF;OI#>TJS%E3I+MvPck)sdC9S%L52t6k3s!?Yh1af6yu_XC3tL=FQ_!?Sbeey_~- zMy@^UhBI~cMHjhf6ga!PeM{j$#IANlSEwm*D4GOa#;kdvpI29=M7t8DUpJ6Z#In9H1v`|X>xeA2x^fEk;$;>Jo@HcNZhW1e zon^<+t>y8BanY%l5`19NewC(=dF5Kxo(X$on53qF9`PCh|8%ZzH>6P+F6iojkUOosI+QeGMe7 z%1GoC^hi?9GvQNKnslFXuZPSpJk55C_?d|;w zeSIb(R*`Nx?1a#H!Xp1X2DMsNW?dyM`Uqrr;1@f7V+_L{)_PrV84Nftc~v0f^o_kK zh@{&`ZqzUW8|x+1x$DssG&hFsEL>a*1h3J5g(+hO1MtWD)#uYk$j^ls7#om9bh(}7 zEsozw@&Af-pcS+ z3=Vl5*ZIJOuRL`xvw7wHnGRHuSrUD zHV?~s(aR6QhQK{)GZW(I!=9d?mzPAZvz3fb9p-t){e;&)^Ht!A<-n;6`ZD*DEl29v zvMw5L{qrM73tY=uaL=Kz8tzl)!lAOjXwQ}?ylP+2nAw8cQP@b4T)|u?9}aFLM_M~^ z*_yy)`F8D!Lo==~lUf(lSD%BIaj$25(2lIJf=DxX?U#(u*~sSJ;zn5xPi2($5(%R! zLcT`>Ba&T#v0)S7vBgyhhZyd13dm_eqDS@=HM1wq=I%k3E~4g$&PT>!tG9|Is39cu z77B&J8B7(;MG|o?w)?bj46{1aGf`2QZGJwx48nJ4NqsmL{i8sr|D$h9@!(OZX$|Sd ztpOqrsVDMT$thkWMoN5k;<_GCR8xC7RG&JywJpD0AxjVlA(HW1N=5o)yN>qcnD#87 zF-vx<(Y2?34GDkBu_P(~wTE)0kkg1;y6kjf=kq}3%!8wg6i`H~Uhq4+@S$0Ay`p43 zzujv!%g`B(-q=v@>KUEmH7AQo%Esok%TctY59uOl%N0l4mB7K1vHf_668(I#z`OiJR zYY}jt8Q-|TRvR)BF1F29jV1h!i;?O+XdAtkY#g&THC7nr@4ln*HE~Jn}(bmkw^2chXdIc z$2d~Jf6&}ri=h7uAZ*$6XHiknGLf1fcG4^Ksyjq<3(I~3+uqt!Vbu7brbgKcLsiDb zZCQ)#1NU1kNLczRg>ih{RetNM?mt!EvdqEBz#!Mc&-^tfO}Dtb+_!@tNBAEeXu&I1 zvs9XKNJmFEQ_|i}8s2xY1V~yI%EL3PO{@PqTTnPWEdAvC-rHCBAZJB9CpJ0x6U4R1 z7Va9;F{I}UIpcp?f&F5e($<_hsyU;F09x?Ub8`hOEi2f*nDElS$^rlHEYol2zwqG$&W=D*wGK*>bIRDe?>jlk~8P+Tz4BNRu z@pFo~N;yvC_a;&!k*zaOfyEb1G>pJ!KAH|ggXZSuTF!4(RaLF-P~nQkMlL63=j+dx z=%Finy=HYrWbSR;_s4tLC1pX)%~YNwM+NRIKi!J|hwa{lYG1H^xV$vo$v6BR5b$BS zF|=(!L1lJtcVcc~y_2xxIyLJ1=B~nmdPcxm^K~o7--{9F*!!!u8g^~{D76%6)b7Xf zr)I8~GT)e(_OFi@aPaV^XJ)Wb2#nj$Lr0HZ;<>8t}P^edspp! z`+7mvT;3*3M11%x-UpJxQERX1|EDHScyZrwg40Q5+c;F`iW*i(7-iSe2pAaI!RctP zS!oVgwI(%FWES}z3^2XPf0d_~KMyP)I@7i;SfAKiTV|ED!KKK+rwJ=)5HUPK($>t_ z!Q)s_5G3-V#Cob|6h(c1e$ld=(BgLGz{F>rDnvsri$rC2 zyV?Z9IV(`3UPlrzaM<2!ld;E_r z*A{li!f%P^c^FUEIzi^4Nu`YJcsVz#R|+%BK|1iSpJFYnhL2h07G?dKm&c|(qj9Nc zpPF0@96BPNJp=cPYx6`jdJN1-c*($_j@fFSAZ4We6Y#Jl#-1l=wDpGx+NSKk_H)3B z(QqnP$=&AT&idRpZ|`?QRw_c4h7(8MPxwjXGFU?1kQtznhMGXKL3bG=ndN>00^tR0 zo>vy&iuJ+xRD=iZ;U$4<8N5e(6G*pK#JwFW?^m)J>$4ojTkhu)Fx$^u632$ z!}=ta)~3#jXG?0i)X|6_qQ(quISCPtUo!I-UmKE75FgJkP))2mY5V;+gPTU+hDsr` z=%O{G=_c#5P}!vIuKXB9^)i&M$@Kw5J8jyy4_aqkuUHJkYR(NjHQg4^`@Drl$G=cx zxuZ=L{i%<5!q7;0MAE}0P^l;6T0sycx|5tjp#AqozZ~B%+s@Iqoue+pd^v)?!qA!} z0E`j0jFE?}yvp}UTe!jA4BT5Kn_L%oT(PmeUEtiL+v;%{{5sGxBm$jcR>e06-f5U%qL&#TIrIIhz z8=QZt=$h}H`-6cuuG5(cPuE187K~je<0ljYM3muc`?!cXUOvgd$9xsz$&$JWH=+0@ z)y(c>sXvk!yB@6jwZ7bVy&InXd{xrL6I=%no?uD@RyP@2~FC$E}elaEZ>Y$3n_`cK!C&I@F_{dFV6 zt0aSe?8P@b&c05?ei=>r9U2SdOT$-1r^Q00A zc)7z45bG;IgOwm&8oDv#+tDv5DSIPjA%Yn79v?TnTLNLrwB=`7y;t9V_EJ(huzK@v zpry!ouj>FN2!|)3zI-0{VT1fc=gZD1)!9JVM~I|d-+sLNddL@%fhx9b+Kmjs?m#9# znZ*E;VGZzt zM>Xl-8u`Q2Znl)D((3Mn8qJNJRK@JiCBoVszW)jhuuduVVqSU#sK$~e@&b8I$)G=H z8>LRTAN>*W0W40t7Vi}JGGTKcsUnGos_V8y1zY=nEA?nr(?oD^3Tin!vpt=0^YHT4 zO-2419vs{pil=sQak03qJI_W01qQxx*r@$;Wou-$pb6SNyboy5Kt&+m0x z-OvHBMK*k1#qIs;7UN)sh|Eb$S&|u1%GsdRZh?9`!Bg>S9f{Am;u(jv4!6|FQ8;hIR_7}#m4;u|Z;q_~AA z;ylSI%=;~asiW0!#!5LyQs_OY9@~{@I+L`sHIyG?d&2LVklin!IvV&2B{-SGR^yPI z4D^g|A}Cjf3#qAS|8mO+hfdH?3GyeSI-5D4UszulDO4`bq%3Aj+b|-y09VayHyMkE zl{LRxse2i{!bh7(= z2(jV6hpW5(&L>z06yrUe(v20_476Xuy!Ib1Mk*)%Xwm}$s_yccJnN#LQs5}-rT4T) zj&NUa1%*h!w3@bV17&m7kJS-p2sa1kHa#;r>jJ>hnGH zj0~RTv%0Mlzha&K>^*^Awc8j^Z*n%Xi9I>CAE?K#(D$Q?7x4^r#LqFXWHSSRHlFkMvcLxRQ7c!Iwunz=hnb5ebRLsI>Yr!>1lQ_Y>Us zf|NgYPC-wfy4Es(wV^+Bnic}R)|Hb7ija*tYK3bF_)+dp^ZpUs?XlV*`t~NuE~-p> z;?R7^Mq4nNuq5Ddzb0hTe@|AYIcfymXmO_{3AbdS=4nz&p90@d1=r}5jPlBJI=bDz zD{{;#E2k-{yH%ESO(1+>``+E=17%rLT2EIAcgl3`OVoC~gMd2=BDn!(l_@_WDR6sU zB0=|6l!|@LICf~~KF+g&!Gn_!?V7Ou6VlJGnm}1)_sloVd-A}8x4soxP`ll!CMp%%l~^5@4)R8G7_pXAK-NGCF=rj)HeyG&Osx$wW0y9u9( zAYl#P{VgnC4(gBCYZnzw+xfBhDHt_HM%syMFe`0G=6u_D>dZV9l<2- zNVooDyQmJufWSXXu5V3W`k>dKW=|a}hz5B7(6GaGyZXkCS7F@VcyV$mpj$?!&pQ9< z$Qq4r@W+hKO>ka(yXnmB(pmS2==!YIr0CgNXQ7mHlcmMxDxN)&>h*_GFA*c__iShG z@cFuDJff329Co9UuDAU@+g8K5YMkUA(Q4`pzdTIqC4rjCb)p~W*4=rP?!dw+e*55I zxcncSy>(DrP4_K~4DJ$Kg9dkZf&_O6?(RCcOK=VD?hxE9xVy_h2=4B8p69*4_ufBl z)mQcHs+pRp({sA(OrJix*IIk;V=lCd?SsBIQXcPf#B!J>H4PjrLf_y4Bb~G!L*8#O zf}QW=LBuU8%8wDnfrL`xK)sdN0@~Ir)G9$w;MC1uF>M&Qma2UE&fOzb=n=VAdSnm`v0+Md0uEIy;7Y zEy+Jgh(h%Ld-o*c{reTv(|6FX1J=!+bQP6}bQAIhagx9A{_>!p2tyt4PsP#SVzGMV zaQ{k~cT}U$Z_}STagX@tHk5XWIlH$d3Qa{>U*s^vUwo$t$Z<5S$a;K&-#IEx13Yy| z|LYObx^iG^z?th1K20IS9pxB@zLX0PCGp6T?zh7;`q0#k`T9>o90d}vFNOrG$7Oy&XYvtQADz<`IgiVX zb-p(m;ZTCEH(Ox9#q~U1dxg9;u!>arUBHZJ9v4>Spw5}rk%~epn&Na!{MR{=>0G0y zu0q*9=)EbLgfq-8yf8ulyZ_Q*xbAAiWXyoX0Z8S;0tU67cd~Koj(C&TkltF3-nVYNq7CqZmaZ2)ZGv}TgSCU zUK?r%a&E3ppB}orD7UfOT^+TH&e-isSs25PuEt*Cygt@V+wx;9535}7{NC_} zH#m9L>FbmzzwVB4^ZF&Qk}FYbNm@sOfF5W1kGT<&gI|Q<)n92DLVo-m7Uzo)VV5o_ z;9YZC^Sz}0Hec^)8$D2WYoxw+pWzl^&rXjW#(}B9@ENnloFmE6ot^!s-L9twuf&h- zJzj5WkjHi1!iGs7n_|v^+MY9*YwNaf>$R^K!_;E;fLy&87)Q%QK!U=wW$fzos=G*Vd-%vO{X@#Pdwc2J1HP7#S)-0XHUF)P!PjhwP*?WqCDNn z3kY^Z;Wyxdn!G+mr7{`4_frWlSJ80uMHb)u-Dz{?MZ#s+w{<0{ZaS*U`4ul^wQs7w zKGca~lw1On-ivsQ~|ghXI;N@w7D#lWuP(Z)Iz)R$!#eQlSe&4tq@7JDlgu4wSwP zw8vf?Fj*^ z<-1%<&PE^90-AO+>n*w7GL;gLE>v*7KkIBytSUONw@%8v#+C$2t!&3EI{eAPPzf`} zEQVKnIdEJo@H>{O&9dW!g(@|icKX>*nP|eeHMP#$5|fx^N4ou?YWMXY0e0p$KMgf% zb*49qC{b&-Ma;jWfw>{LkQ~pz0eh;WV{#2|a4PQaP18BVry31g>)BzIDZbwTH`>OUJYF}IGoyE`L~FFhBcwo5|o{2ldL)wfpj zyt-Z;O&{F@M}Gwe&wj626qHsXH7th7DvPSu=`gP7oyV2b7nIR4g%c;0!07h8o^9Q+Q$%krYQ7l_KIT_#_+_OnDU#$f@7D6_hqWH?%!$@|95v#{OA1P^9iEM~SKr-_oaNYKa`GekfNos2M++S>7xc zE;)t^F*l*n+r+VTr}3(M*|X}kIvFtc$h&T^Ec~)Y=(3|T*vRL7&mbfu1a=E#WFmz& z55VZA?6+?}xKxVZ(%aklx*FHPp&rjBdU|@nAK!{M2ZuLM3Aw_tV&?4&mlxz)+R!@Y z??4Vq%TgXK=|ZY-FR5NmWZie0+uJg=tI><*1LjR2H4%rF@2qYfC$-;8WgQj{HZ9nU zI5LG_tFdu4Y_>0+4yx!-?MU=CT^rqwYZZNk(pGSGCRd{+t9-lC7;sLZFQ7kleRNTK zdEGrA(h*b(ubm7B7O;uvG{3jcyx0%|<-E~Wi0wrTgyl9RCrAZJBW3+btu9tQYUSy1 zOh~yhQ5sf5=Ff(V8bBHgfk#cns^whpc{Dq@@E2wZ*CS zc58v%NZ_X`818*+C`V;~a%w@?Je?jZu3W|R3VH1J+IYpLGvlpYA@+(uER5;FSqp68 zDo9LE=Vf_T(bksvcv0-x{+DW3VLt2q^k@rNQeOTIi7~jMzFzr5Ahx37b&w|}Ya3@G zWkNwHPG8|-YxE?NN&YUagdb#yY{)Bf3t{Wd`P2k%x_zsuWoZo8ecx3SC3{pY~{2R{Gr zkil>Nh1vi6;UBu;U(os$|NjE}Q=6N)Dk>_;}ll?zXd2-Nx!@na%|CgNjx6l7)vwyF{zmEHV+JHUySFmpX-!K0i z?LWUPd!4LXKLf`-C8ec}+#F2bfd^C2-cArc&?o$#_TY(879F=g7HUJeznIsMIpXP= zP4AIdzw+rF-9-wEcU1z<&i*}7O_N)AqkHJ2hy(jwAUiwz_wewEQE@>*(EYu8S0hUj zIHIV9Fh=s798)Y}X-NYa7W!=%@6Z1J?9vjL;8fpti2V1;)($}9-RwK0?c&RrbzoI7 zW^MDt*3z8kjUMixjs9rs|E)~&$Nk|7*NoZXXoVGU{-b{|x2B^bGnLsuMMXsnjP3jN zba&_cL+!Z#6TmPZ_v1&ODchB-3tzUj;~bh$MrJ0MNO>QsXsG@H_tbUzLvuB^?6B!V zudBzO7_46VJw6@<#!}dQ9v!I_vLaeqxJwmtms*?#2M51ONEkJ& zK4$*=eE%x;6=E;kcTiJ2y#B%(@Q6wtg z*sj(YYZ7Qc5`^_D$^_S;&Se}D(R)M78kuPO(?sz>u?Ta^G>ORt=;FwE?VF!t?A>^3 z-GmK_g@45GDO7PobeT9fkoNKmjo7l&VW0d@`81i!z!R+(?bb%_5P2hIwWhl()$)pp zm?}%qS0tvjSp8a^7W;aeWhO=z%ZEE+pO2S>#zu}a55L)+0+{lW66z|AN_R(cDylnf z4b+a9`6DKCT}`dWw{UGoi_5n`k5554m1?dP!k&$0<3AariFgxPO@>%3rigGDwDR5- z&QHz=x$S=s3+)WO9WTMYg+QWgbm5H2d32#@L=66jiI${Epd`4eKK?RL^ipcT~&F?)zIw zfZNtMY1*1Pj&yu0(^KnM^}DbK_COEebd>~s+vPJPed&a$-He~Yxz_k^)`p1o>pzSY-hXWW6CFs^l#zj^1ATCOLyz7bK8bCN{-*9EA-ivD46oTlREYlKJzye+-!xt zyLnjN8O{zq*)-|7aA2RY@CE0h({WU@2$A6)rv|zc_woD;-Pq~7)AP9-T%Pz28sDP$wBtUYeOJ-oe2@Lt{bK zYAM__2QIF5u~nVjMs%CTbzKk>^r}KYNC>Al269}ff7ouhzcV`GG~vQ$y?348T>Pdf z1@|HBZ2?KDnbPyNyt%n)wplkA*}=esge{|InO`hHr$nnQWA&wRVVxsq!jui)!y|X} z@%JV{?!nue{dL%k8Sc{I`%~^g!}P)06}#R!x0F3|=H_~Mw-DC=-m+DEepJ6|mT3F= znW&-#9N+r-$k}0m*S)Lk6(w`*w$uSq$%!X($R1FSSkIEVsW6#2UyXLwlW+~(P9k~j znl^a_20Iew&yA-kJANazs`T&Oh+*T1VJzt;^Z3$M;%vRc5{{0J_SxC3Z%0k}@$?1p zjPW8uWS;O!pBJ{f7NjvN;Wvz^5Phk3x2}vut-wvC2$r*<*Qt=o8B=AzhPQ9?^OQ*% zqv4hERPn1;s{uxrQpuRftF{kfHu1K2lZHYVOsxz^692lU-ifb+|j&DHa<*_sSDTi)| znFMo$>F1zvf}~+h_p>#E<4-2NXHHOYw4n?7vOSA&sX(~3EZn^&(CLW}s{%^=aZ_Eg z7pu=^*cp#;ub_NGIAEQ-@+zYp_eM<921;1VF8E9Kzz{L@Mjdy^KnawP-8ayCwbzCd zZ49XIrtJB5*oiyORbUN@`p?h$p2H!P`p)EYoS{)<;*Go)+L zRGPYBCqH$6oLsEN09S(Lx%DDX&V4YMZP%h1neF>rPBcM9RFHAkCc283yBC>|V-!G( zJr&)aKhzs;!-6Yq7BuHQ7xpp_B)a~4bRleCe44Ltbn9PDkW)no1~R7w17AosBh|)! zq&zV8t;L(q9WB2F2i|-ryx);lDz`hXh~3L0z^BI<6AeeXpv^s0M_( zw+ad&5=z-nkMIBWfk*$PCZxb0s}&DHhF$2wCs_awBYyex>gd_&*e;N$XHV+$j{e!H zLh)x{5M*%xho}N2QORX;OWZiRC`+;kWrir*_o7!mgywbx1-|&g=id}g0B9q&MH(5y z6dCp^M{i|bD0nXccqQVXWPeJ$f=lHf7Wa^2BFW@q;vY(_0k|YB(C(Qq8c{Ir;Ynk- zU!&LPhtWj?h6BdlQe+xcx-j9D1>d@9J9FT95qpHgSQh!kJVVM+RbHN6|3JL4)*Xev(jl8qv!Jz1H_ zzbW+na?OExuf5R4(Ruv`8y8)G7Fzr<%odACyT?H}Tg^vRw3)iuEA`dkY9KV6Nl?+`3CexJ0CwXWh647(aa+jBdN1`)X6#$aw}y!)f`UdRa8Yc{4Br?7OtG% zKkqUz4vB*#+#xjIZuxvZbLab7!UiU-TS0ylxBfY%$#edC`Ml?`SCMNF>1|M+hY%)`^fE9`}@G?RH^v^{(2nfN^JTZvMQU|*-L`F;_??dmbuMTBFiM%F-P%175#pqZei53N@rqG8Y=B~l1m))dC-?pKZoNeVL59HmVch{^9wsU`54pZxH1esLo{MwxA~|k$k1ROyvI_=oE^Hj2ohm$1DYh6 zo9aicgHs{sB<$u_7lT2wta6y0mq_POc@RCbV*bFhCMZoGCQLfc?!n*wMc(|=-G!( zWe)YdX>7C)hH9%ripr)*ie&L@CJvRW3cVU;62DI&Fj%_ctuI+#`s=$DYOF?XddmZk z9btX-e6>0TBHV`eEH#Q9_hltJ?HKYv(1i8^Glj-j$pM4(gZDTcVFv1b%&w6a+lr2O(A4O zDFqz#6kD!NawXGe+f8hzSHpdXarp@>X7=UwW;C7+liV|12s2&A((N40w z?M`p6`wMX5K|Ij=8~axd_3LgJ{Z#|u#W1-nsrhp?@V12BX+B9O{xmbp@5A}^ltvR& zNR6f?C?VV|ygA`LHQ2(iYNmTBj3ix-+rtY2nL7ol?aYxJ3qe(#c9vtu8$1j}#Erob1x830Nh^bS z8V7-;j+#=7Wxg_D-fUXl{Pp;+Pq1AvyZ-h`vqA?S{_<80G0AZn<)Ob442yn>6xAS% zg;6(->;q`;;>RnMnQO(WJ7JrC@hxyI6qc0Ws2@}~p4(DQsjC%KwBy7$!!*KT5djR| z$pUfXs({dr{9OQedmaQHT{YaeY>_C#aU+jj+j=oYmb^v6r~NGX0~{@UXLz5YBhz}k zSAFKdko4m+@hBO%l(5~yEC8k-50Mif7@yLZ*Lq$fsF18L71|52a2_35Ih&YkY+m6> zl|Ung09FFaRZUpWg*}>p(bjeQs7{q13%;EZfQ4*l9fZ!CVai!nDHNVjA+xZ&C|+)x zFg7_u^dqFq;8jhkA}a7WPpz_79Z%V4i7p95axp?#RAd^$hBxqRtgai@$rD*sNjT00!q;tfvY4wdVN)BE8kDZ#QF z2NQ#8wArBDX>~lvF)M1p6CB7hY*)t^ACko5z!n$4$s=MJS1gC^|DAJQiW zn`%1``dh6rGEWTjk0ZWFd%l8+c`)9O0(^YknoCNte!IiK#Jv1W>59QIw@587L69C+bUguB(^i$< z?_kj@Ia#%8#MQdoO6icOS1x@XKWWbDf*Mr)rfLQ~9h&tkmPvElsFhfco&#(%>B8kPJEvRNqlETtw>!M6Bmt66rjDjl~fkF3aF-oLb zLY|DGPZq-6o5;_AU{;ABK_rW85UycLQpqAD%ytF?IUNtvZo%9uEm_9WEJlf&Lqr~; zP_x{KGZW;_Up|psI}KYJwYE|4Su0U=ODQyCj2&QOp{%2aBB6>Em>)QH@s(_cO2nnl zo)yEj0$%q|S#_1v;E{F~pMDIM6ur|mUejW^%T2uIkK;8uwPnr1idbEpn6V53XU2+h z0Te`-j72~ppxC;?CQ3}=iMy~yAvQxhh)l*HU+r6=dY=*?)R+(!Hk?I>^+!5BBr~pR z$8E2_Kk4JU-_Dp~piL~q_9mQ9xX(UK?_&5t&Koo0O|ON)+lLLQ&m|;N*R4vHkS{^c zw}Ywd0g5<*pq4E!Bm=32Vg{yl+urnE7NKSASaqP&%Ph8nhE82~H%e9gSAdGj<^P_v@9j{ zb_{r(>fkE##qAw;Ccf7`I7bPQ1wN;cC3}&+FM3hj6J72mL6e+*%)~-}`r_KyWO5^m zO2l*68#cg7Bw#(TM8U-1IT^T=a2?td^4^N2*?A$gXQ98*K@;??&a3XojGZg%OWq7u zkggLzuWK*u3p9Qy>4$d%Qd=LsPmQ1L%E@nSJGB4(^uvBT_VMVZX70|}u0Nz7NbvV< zpA%FF*X?E@-mxjUxsx*Z2ju)>+F2876x#F4;QRN{s2qIVq6@Od#zyHv_M#{_W{TiB zZt`E%0A>=nDp=0ONJ-ig1l@}1{Q7o8lqNUJG8!@~H8Ii40uAOMTzv`*a# zbWlWXLH%?yGi8+115sHZiShV;{Q$ht$Z3#7wJ8?zh)!OaLr_(F!iI=hd3a|SL`eyz z8>fY`x(jTIYgJWskqIYVrFEHa%dl(8aeAbCJ# zVRI!Vgn-L0*iuEc*#ybbDfBuub*vC{ys_iim9Kf99YpNJi6P}s`pBDT?!4tqC zP`X!N^5mhXIUzsNbKblWT^@!XWv+sJ-b;{btw5Yg2Gs9+6G@6fKgOARh{DlGhn7`5 z;V-~ZgFRiRQdM@?X&EA{+P!hhHkZv6F@E71wi=DWoSR!Ql^Rx8HO*K_CWY|U@CvAp zj=%aNbcCgiftJ|#Qn1^4Mj>rHh%NjfW*0Q9EG&`76tEl3!m-Uc%5tvmJ57RIS}rCA zA*p6wox*6$!JyAMKu-8B?lAf#of$l$-am)5(egszZQt+HTZUL-mR$#-nm!{<;h-sW zzsEF!RNwesWIuJ^HVxf2?I?f6fZHF1q={o3*J78~GUD80W_7q6TgyM-G|Ir6PKNg9 z=^G*gJe{CNZkt=BmRM0~>E5XL_!&}H1OF{`IdFouk3I{-9Y%@OPU?R^{uaX`n>mTl z)cRSY+Xwe=6iQd|Xe5~kICe;`02YDgxs8>8ztk|=$<{$QIeg|}tA7IKkHlH$|fO*A)$+bw2n!b!iSo0-4<(RTFJ|8v7eIU<0iD5D+?KvK1fbg zwhIs~4$hZ=7VL{CNgtbIpy08k`rXCZ-kbj-t zR0slWQTBquk^)9f6w%2r>b^Q-*NB{n4O>R`hk@LIMhBeC8n{^ka|1RJ0z!{j3Rdme zlX>A1wZSpVwXGY;nt*aw#8sb+tfCwE&&tf5`h-pOBwmb760+=Vp=%0o9h0y<+u_nc z`?a*;{X8xX5&CKv6|t=qxjgY2in=NqyaXqHx^jmFfQ2@m9G{R%M|ob}eKMyq4@?t% zajlx_woTJQ_{N8%RadmauLRqGS;G>(HjI5Mlj3_%VERsV67#Ocs90ul@AkF?#0}2n z#TG1A3q-ejuR4_FCB(x-odACYoVIlDS>41vmdz1C29v{Q{L&4zXLFskM z8(Q`cjDr*F)|r?^Z{){!B(9B@8mv@7i(YoA`9s1?F^MLaY3?R*6;H%THsSXdFN$-o z@xXEJjLY_Jq%Tm>vdfrRr|vu5b^tK|k7CYbA^-PY&Pt9}P!|hq$6Ea^j9`t=VysRx z{#BF*1#Rq<;VhAMXPrje1H+r)T{xoFn>7SJ6r4#YoTdXCC`Zs;w`&%{6Np|F!5-~@ zRR*~>YIa*LSRCe!*SX30($AeH8j@>$ zQ7w0q7VOePCh#!OmzXTA8~Qw(LVUm89Fqj?bDHnkx)Os=Lqp#nW%!Ar$;1q?QA04U zVK9m;AZ4bd>UNsfcN%pJY1U6W>oz2%_*X?AvH(O(bfuYNNBf*ib7F6<)D(;Sn;Czt z0@S8Bw3EJ@>bQ(pl)binj(qxzXu4IxBMwT!cCe>mqEjWKl*dhShnJnUv&qUb=p{i_ z_M~tc3!k%RRMNLg)`Tmb4-~6GX_L$Gcq^z|;L;_nAz)1ioP7{KUK%f>@`x@P7oHcs-fvD3;e1_&@v&4E{f*8$t6wRzebaH} zv@XMLA)E^VnNhO4dBA>O{!`WhnterK+*;(^t;+{yJnt!HWBMpgq=Spnx(bN`ZWH}o z=`vybX}(L#I5ZlswB3Tt+5A=?sS)vz=eyEwe~U%;nm-VsS{cIsN`2>|3-y)wTG$U7 zSb?fV1NXkj-@EOH^4QT4*du22v-9K5L)GVg)uOmM{-XVa$#$g<(xLMT795Na&VSLF z)E6eK^rgDzxmt~B?{$Y(;L85c*RHM^)zH)#rz$Xz)fHLML_aM(O{{SR&Zd#DuOFV; z96rFJB!LOFe`zgBYaKo@R0zO7Elb)V*Mc&jD!{zYy26fZQGnZQnwxC?6vXE09-Ze+ zVrf29Iy1`|`$Z@>OQa~}TJDx?&dJZH@AdRIC1xmV)&cFjizmu!7+Ub%gUTxH(GZ&`B;zs_W#ty#rQJ>SV7W|ug7Fp&w zV+k~e@mVxCvc5UGY1>2T{Syb93RmszPu^5rhezozQ?ikHefv{N?Sh9#=Y@O^`MD=E z!~DG{6aEfiCv);w2V-i9-OTf&S6>vSU+-tK5AzO^;ryk-u8)d+XzjPj21$!tSD=xs z+*AbD$~XTWFMqiUG(ARBD2*AD{d@aJNRKp+6draUz#?sSR@&*^PAimBF6}mLMrC=6 z?;26s;NmO0e_Uju!wQ|WCB-5h|8o>G{wiUOZv3h&dql#z7V}_rR??}BX@gYaD3fhf zH+IsF-@$$zs^vs1_Y)G83{718Vzd(NkdB2aA*!)yvPVXSq4fLO`+%%vI`SjZI=rEK z-BH%7=BU3NuAakz&9%aHRGxBd?Z=13>xKPtjo#xRvtJ)+Q8LaI^Y>q0+qb%X`4v-B z`rnv#kNVh6pSN+<-qn}dSctX@PuNHgrg67F#7KPJis?SWd=7427g=zVL6vInV}u4h zdjD}e5qwVuNn;8zVEkA=CYkY&^{|p?{nrf>cbtr!VYfdk3&;46%F5$o?agmXe_R<9 zd-{&&{tWrKgpp?Zx#yPfRp7FgJC*$5!@Y2GOCj?DT$Knsl>nWu zeUy}xzY8H>cl%WN1xVuR>MmO*KzUe-#D_yYI;!B8;#Ti4QYW|+k__HX{~!COgF~G> z=eFn{KM;^nP>dWMm9(@Tb^gwRM8B@ld4~ z-5juTzPM5BC`t;nC&*D7aS-Gy(Nbp0QG-cJIR%AX;|K#=#L%GRkBNy1WA+uFw$c(7 zPR>y$9D%`y6$6s+T;KA8oaqBkQ7xI&HCv*+J*1y&j!u5KCCn+|{f=7C**@~qrHqSa z%1=1Q+~mc)^(#pG&(|CWW6y(J8Ctq=PG2UG)*ZHTmqppK{mHZ2EO4J~mOY*LnSeoZ zW*LUeGB&oh(E{6&x_>3?8e0X39LOh;p3S$Sp2PQ~r4&xMKe$Gs{F4lx2=`xlX6FsA zI^H?o6SqH-7aUHJPF_6y8X6jKwF!=1|EdpJ2<`MWGyn_CrxX)OD{}_-7Jc+fyj}Iw zhu%splt0n!?b^VuT4K;BJUQ-Q2_ILTf&Gi8u5}}0Ak@LI#ihkJ!6VdGW8btlb~TUx zaB}QPBS6>3+hC8nv*OgchrPm?c9%4!y=A)8+nvLNJxfCT;fu;q5Gnv#)D`*NK7Mw6 z9jQy7xle{JMa;va#WV~*xUcttD*mx#u#ky@g5p)I=`wMO`Hx8<1s=(}55elp3|Uxs z*odp(O!!s~ldHR1URMM0nO^qc@sX&5f$;MS91DwM{*j*nX$$0H8=RirseXHjMesoK zG_9nRBrBW1XL&us!;4ZdG1~xbSN!?*q+dy#+@|u!q!0?>@JnMMh;@$snUGt({?e*w8&dW$x+Krk!Lb0`N5bT z3CO#BqgIiYc9T`vRCQ69TINjoxr-fXmu!x^FYk6%c??-z)3#izo zGm>Yk-;CNw;SPD*a(5kb@9z>{indXQ!S`2oFcU28^%Lv6Z|t|8bR(~Bmj2$JX%GgY zV(;J%Cz=6KuHW9RZCV$LVB-A2_EgZ9X(6_c`}Vf!71ynS{oTfcdA!|i7YA;ya%QhD za-?^}g9o>?dgaK-KFF2;P9&Ban7lSTzEh^X8dWm^=bGqndh;to7-(61S(|a-4#Q^Q z3tGYj-}j;Bzii-erv`W%ajol41dnqh!#7dknr-9X!7rZ=N#172k`Yf3%H&vw%&}Uth}w!17FW=oCfXU#B)vuYm?fp zD0FWcGC^mgeHWDq1Aec&?@~^4YjN8euf~OHbQn>CIQnV8#=@p1H_#wI9Qlq(N=k(xka^7xI?gRgGUW#w{N`VA)uSUzm8VFFr1%+4q2M@^F-n%o zoTtQaJ9+vR&`RCjcke9eG!J>pD~ z?8iYM#zXX#67wfSQA!?V(thd@QP1xNDy`8XUV%P@P$bybORr7Gwroe9v$|EgI<0CJ z9XoK~J{Me7Vs;(>#w8FMKC~cWI7zKmqo_RV$dq0i%T>NNDS8+l(O8${xTVOnJ65R; z%8%JcAFY$58P_kD6eRsMs6b$`i&9iJY%ae&L@WXz9uZpTBmYE61wn^_=Z3lCny?-P z`0Of7@;98cwy))=R@Oa4cj`Z%Y2~d-f0h&zH3m4kcW zPw!8C^cZkD^3%x#eDei}A+sX40O$c|;s63@09qGO+-{@lOVecFC&(=#9>!h-aYj)B zXww6t=;jueHKZPSd(F|dEWoG$=Gs_KF>?C38LwUB_JHGn;Xtp#j`oC^QroaUWFd=uKLD(Bx;MvHlza$-`*U<<7$) zP=j)X3E=LpLfx7VjHRpRh^gy`AzrDg9d;CFXtGI-er=Xa0S8OqmMbc^-P~U6GIad9 z_odX&f8CS(vUk1L!(q*x$vqU>kV1838HA<$m`s_4Me6?w=RgCdJcyrO^I(D zN&6>i#LPunmU$52w~6vs3i2RyW&wM+ECs@*IB5GgBz;(z+|<+HIEHL*uo@+bPc(AU zIB2viT%j~fyYQ<^6ck%1(H$H9G2y;tJZGVDO)>)o_ZRJ%xq{+L>ak19RngGwDhikcO@q9v}3>t$9Dkv9A}N}g)2RmApnVBrDT&MAy5 z2tO!o${s-jm|T`5QucKw#con%l`5d%{s;P>p#)09iBIqWYkwg_cPPhM5(|Xl{WMt- zr9cBlN83f)7pZ>YLKgx7*B24b5>P@x%9>;W5Cb+`;ZVg=6b=hg_@;?~2EgJEcwPe0 zEaD$i!R91q09)u_3Ov%>-y8$7ZPC}C4Ul~eVQTo0Vev?e5_*e;V6!fvK}<97$b$SD zL+ZId>u(vDD(1k5_e~_BpotknnLu!N4QtRcWR=%?fPceS0KL9z|FN|3d@@X;Yj1J{J{Kd?xw&`bju(itJpkdRTq>yK*T#xw*pbeYIq#*GOEr z0ry^bE!<2G9t+>g)eI8BYc3&xOK+TYygZEY63#xAWR0u_^{_hH?$8AL$r)d)0dtcK5 zs%f1aT6r@2=)LA@YTJ+Bad74Osofx{6_Q@TO1D<)!cTVq6@SzJXi}_i)OT_S{bT8C z^n(m)d?3Sx_>p3$qOXp~M}+jk#~n6er*3*pr-Jq$nEP%@H!o?0`x0fD1~%hr`^I&a zcAItVdzo|jLyX5FIni{ZPV7D5b(*^(;2b*eJwC3#1Ehv$JN>cF+uxXMx1RX)b>HF7 zJ$>*|m#r`Vj(Izsp>Tupi*<}5ar`QejQO$^GOind8IhazQsDIQUS3|08IWNgm=mhw z-Ltc)wp=f~ozZbdra%b2Y^uf*<4bS7I-QBSjA+NS$;*|hOY|xpMoBz3f-<#9CE8C) zW!iaj$9ddohFn)s0_m|hSfYTyO#RYO$?7nb@*uJNu!8S!O>2k^p^zcuRDnQ_SZNly zfSvH^y=>A!M~rwDxirHjJVRDOLux`QmRT9*1G=WjvA)avuTj77WhAhY<()aWImMW2 zX|&2k)z5gdggkI);PK*eYkEi7)p>lIy{26K!gQ6DgENl3I2Oid7FI&cJ=xU|Ke^H| zPIytAs`=wCZlC<3*fEK?>b;<7b#(ScQBx#FR-?|AgwFoC>*(#-6>%xc2x-7`7`|>_ z*m%o=uAAltD>ExkV2MVQ0GZWtuS*y!BdYPt-_P}V=cRPdRk=f0h3`ZKi@(g zp!i297n!b1#$sqcuLDbaYa3xA=`2<;Nc-zkc zpZ9}hgBS@z>w9h4Zra(8wu>bx-nb5`R75xDf^dE%PNisjN&1?CW;y5#*d{4t@PlN7 z&K(2RCPEWukF>I+E&TR@^NZGdTD?y^UZ9i))ROhCgKDaCEi36;a^e z16z$k?&Ht~nNq{l+DWX{wHQK08fL^@q_nK^m3l20E7$W<*x=IG+MMVGZSB%XmARx; z@^V6-g2^|j->buLDdaMj`_O5P{S^k-X6K8v#dd4#>o5ESNC-d=^*HrB4Jv+iVkzjhrNDl%J438L}=Pu;)P!YM~3csA?{73%Nks zxg+K}RW+?Q&P7P=`IKIn$`|p`+7}S!h=%HCJPav;R#G~S{x2SIE0qf+7#kuR8n?X} zD|O!SNmI|0s-|t<$VbgVVty8-^)^iN?+%sC!1YVbK(i`SP^vh9Syl;TKk)mJ7S+}d zvqi8G8!-yT=As8y(bgduhkCiY9kFHm0}x&PrL%QcU)O*94frxou~L7IpzE|MzjBp; zMf~jBdN=iQ^`je%g`4^4T`jQGE_XmM`Iz2}j8A3rd;T%IVkhwg_23y`1M!80Bl83G z8$UHfg!$B^qs%mN6c&F&QD4FamS4>}Z>!#!2d5jt}iW8E(+7O%W7|SX(^Xrx6 zl|<}6_mU}D72TJoE@Cw*dCz;g6(XzOIjb#xfj3sORKd@0X{GHV>obP;BSCO^9@U4U z3Dcmz1t>78X@nMpKFbC^he<9hEGlO@3nx}uE4$T@sDDCnwWZ|FYyXua?<@a=H6d-d zXdIGwt;%6jo~5>xVIGu?10_$81pu z(nREn0c*nuNlVD`4X~5H%sCS9f89z(ad1wN!jtcyDfy%VX0@BbdTT=0!uz?tz@PuI zwEV-SLb$k;U{kKkSysHQfI@8Z=%NsyNbv}CE5)zWZ^>KR z2!?UH2QA%Mt5Hhz9di#mE^^VF-oyLd7m=KJq}4}V>-CHWHH8}=%KvnHFsD%%>1@3r z=05jd4jN!ow)I|xF}T1M(!|fMS2%rI_z}Kz>^Mo!)MM1yAp z{E>~Ylm2Fv%vxGC5N>l1XIM+-xW1E(CLCJ9owcH#-9Pt${J2Gp5gyuH){muGBKo>nAJcleWOtYYGj@i` zK*O?1*N3D?*a5v1i%@sGtsBKp;)NL_iL@jHBwq6W3nTk*uf6ehnWXe2xN`Y>vi1J2 zcb(sfMrW%Vp+@am1X^5Xg7U=xqlaL?wBI-<^R0tIMn*pceH6%SyW1CdTwV(7dxh$p zi9m`Qxm2r(f?_@xmSi2dR2d<|yzMn!__i|#_MlVM2nt~+}&LafJMYsdUp1|_Ty`BxiJnqYO1_$AsurUbbWYc5FbY`$!ASs)(h-Zp*NvlwGCrLj&lP+K`pDMAL^!ax zG;zq%hPX&C82J>6xi{^^j=W$2dklbu?{Jhl3ZE$`Q7K~i7=Yj^Fn7zAlmj7G8NO;5 zHgBC;5-S1aXLYPhhml&68cGSQ@2i?0_{}^3iRPp`YCiLsGXWew%+xQ%be@%30T`1{@a>1|sjM3F;Kge(NZhLRT;S0er0)DP3+l1SJOCs;KiK5HL=DAJx zLK`C^Ygp)r#rSx&Q*1!_`+QE8jFq zyJwyPT4-r0Rt~R!lRV6)Rp(bTI=45F zShH-PSkLIN-%~ct1hfex(<&Qy@pyFDDPfkERok+<18Pn zgABBp2}A-g=z46!-P0@(pJ(1iKI`wU0K?MukP8WhH9bqnP`m*rve2;&!N7=+>YK6c zmz^N!uI`!8+Egs*d-R)aQ<6p)!UH3mswYNphTgG941F`?#P&?r zPX5ToE3BLSSXlQ~U6HaSAb!Csmwc`3xv@*pz}2q@rH^*NMz!68dVGu3<;G-CB#dBe8v(QS@TMIw z26m=@vs!cbady3qgrI)fytp&@>hs;97R9#09kF@67M;2sXE90A+WW?2zQtpDu>pr; z)1HRiXFBBJLjkmnxK!dT{t$G1zV1i38E2WI{VyB7Bjky=XRHF8K?uz2nDPKA8hnRT zeY#vlo(6TF-2Ax2LiOjuz+4%$rLq0+fsDB#<;-kekgPiuNAGX)X?^G&3nOlC2^B$! zo2Tl{i3#4qVQBx}ufZ(7`h-E|0T^{+h<{&p0Bz8Q8MAuh@B9dY9YdtY@wf$6$^DS= zfAr=HXE6NY-R(2MS?t>m?HlEFPtnO(Eyu)%t^@DU4PkXlor;(uvvEcIAkVLq#_DFg~F=y<4QWpa+5^}~Y z$)4=F3md7BUVh*bpD8!HXy=b1BL~~xWEFX)z6HSz)F>(ghf0IxqboO*al#dmXPnnx9?Yl54 zH69+^w7h!w+>d5~2;b~A{(u;YkQxs0YvE@^)@3aH2z1FcXt+T&3+M=CI&)RUban^5 zvJ!#@`9@4qn(yRku5=2W8LM&H)j9l%xe~d7em#;ad1B$CUm8b+zW}Y`JFY!Qz<)~= zpnNYdBsw+IW$4clbt?LZ3J?;Xski2qu}R2#2otw4DzrawgOjUI`;saKP?kqIPNjo) z^@5FfYY#(aRrI)7#lu_B7|eGzM$;m*$(x~lr%yi|@3v@#wdXH3qf<8HyHD(wyNDAf z>Q`ElBx}I9r8GU}7WKl(;chvfmxHue&BnX}{n^yopdGJbORVWFL_Hpr7D^OSazSHDM2&rzR1<@$0$^KGFR z&O8hu=QQBQmX6VYSrK~!HgbzeAw14^Qs)k{zQ{E^aRfTuyV-CnH7Wox^EcIMeqqG@ z0lO*0Ci$$TD@UWP)-Orc%=*=}!9U;+%w*r@fQ{2G<2hT1z1{ALp~-37HzII*J-RgB zO(RjA_XfOD2d@XUtS|Xk>n);@7*UY57fvi6k4D%Xl^3FaBM3hCbJ5@Is}Tr15Bq$I zZ*A-E3EdW>KbuBFW8GZ2?#dvv(s*Jy$nEB+>9PdSdLLY5S}cNp*zCn~%9y<3FX=1= z3Do^JvJB@edcBxrR=a;Y1~_0_+CEa#{zI{NC0J95VBt~NfPXkjV>`&#ycu-%y9JUV z)1iOE6ME@w->l{G;S4ZV$+4G4bJYWc^Nu(?cYWBjGT$GBOFFz@Yq`Tf=$%#(omuc* z=x#ycSa;?5-D2=PYrT%(=29Yp)Y$cAfA^Zuf+cH31xtBtC+h8gs1zyADO>sM2HoWzA+Zd@0$4J_YCajYF1mMa+n zxS;%+CbzlPm2V`yp`q67X(P%U{z&L`iKmAJ?$3N(&)pvsF=Gvq?b3<28Wuaw{OO{q zeo-wA*ZhbnT3_&?A(0{n3m{&pK;+Ko+eu?&U+`9Mq|H`ZkrNa7jIG)-@kz7#1l2ef zETfj#RTfleq%VUpId5(=-XXU)(_X=2LQZ$`c;6#WOHqbOD&)&uqvQjbV)=afGD`YFW z%Z_Hh#$RkBA=Sf)epBz96lYiZluu8$hDGDe4BNGH)m<9MK^9}qDqe=aT-xy&_Z_mTGtN>Pr3Em>oSP>}8bi-HO{23$$@h)ZmN5dl7`P@pWvjN)ujSu7<3#UI zo6Lc1T$98eVW+V6N+nC3DdeGs02wUy%)=Mn4dyV@B&oXu`<8eC&e;CP=jTWC)i)36 zp05`g| zeJz5hV}~d4#K7^T;LsE1AStgBMl^tYa`3`dD2x<-(Hkat8zf*z*^iew;&z-=F=6qS zU#8q&vVb2XDTBlcTNc`Q!?2QW0Bx%GRA$E6nP?axbSO<``6mymsn!${n|JdX7sC*S z;Na=w%b#mKu=MlBS6|ZL$UF-scTx($@ZOaur?SMR*Mh)Samy(6pcc3``Tq1;6IfQ@ znX@0<#F9rR{S+WCK_m&}XKi!;wLmsOOmO_G!A7u5jrr%j;=}@tA}$0Jmd#ahTk|g@ ztUdy*|8j&}jrDVWNLYNWg4(e|#p~=(p#wW8WojTh@g@vgJYPp|$MZMh6j%>$_`4B_ z+V=->_l9<;$m}bO-e}>>rVD<>%q9E&ty_dFfPLem>_tM;aZS(!%;~kWIv{DD*x?|SC9`xbmo$r%sqQCpbHTC_ZKcK_h z$NYD>F|}5D!vT}eUO|9jZVyA;9eoa^j#Snsx&)LDPGECkVpH+O$!Xr1Y}=OXO`%OZ zRX2RwN_-VK_B+MYgD|6@oQl_$r}?ze_JZUl{XEwpcX2XBXG^Kz4M!Fi#^EN zfcM7DFB35**24}R&$dnAovQPTG%cT;w)Pm0IL+UWge)XaVotD}>yOEaK6?9kF=K89`*z0|p3(}wyY6`YvQNThipIfp4&9d*N+15o=8HF%pkvOS z3*Jws#dpIGyb`9}6g*tAagxn0h$ZW5f^A0jogrVly&17N&8l zpv&Z1aYrV$euJ@;BD1^40?v42c!3GvZJ!qm2Z8jc5j}1u{jt;?^`6>Xc9s-@9y+AM zk%?!k>EWKzO1HXMsi!jj!!9$!{L$DQwbTyAl9e-{6MZ%G_mBT%h`tml>ptlUKKIdu zK5quzjbc9K>v_T6Bxs;y%6|A@g+)6mQ;QFLb9JXFro zi@gcH%7l#}hYuD+UwO3#6b()7tu=7TxtWF)GwWQdT~cLGxPoLhPBpOpjG*6^-)(yL z#&*IzrB8j$J|+TDivAv<{bT82y1J?{RjQ6@HQp-IebDf0tww_62PC%qcFt@yVJ+U0 zk3ei6E=*YOFI+fuZ*|p$FT0pbLVH96J)jWM2-*YKc1A&<_?IZ z^aWOqYexZIcZv9DuvbO~xq&oZB16CjA46)^J-L<%AHvZ0B}qeFy`{Sv=Vl#RJ5-`2 zPVcviB}{1)R)lQAzXp>2V>_9DMKzzf^fNnjCVEl&T47rm7#R3xrJ6Ailxg%IO!8mF z|M~m>pkGaAGr*Bn$TyiYX$KW#CEz%zm;rXB*?4omktPC>SS{=h=K&ezw!iZB-rnAs zTxBFP_yD*I_H?)YN>6#@xr(FHsOOMH9=S;>+r!1#yGpla!3cX5YmhM&p`6S9C<%0tb9N=Iu73R?lYPo$1?FyQTzdr~U+zlIlm z^}E3I=4p3{cpPVlGyjJ^=K9P+Nko);E05luycGzAzvUan%(!qe^kRar*V-JlJ5I3% zSj%VlnTZ|nInU1vSeoP1)Cum?${L!byCUR+2QPoziS@`D+BwBOS9c; zD04?(z@OU{G#(wD+D=ZF(>H%v17n~lPm3WS=!-f^+9@f<(JQm(knL*5MtgwnD#47nK(JFZZoyESn9~uf!=K?^C)8-A~jk?OMFdFL6#h*8?oJ`Cit@O7WeXEj!*F zUEN3?O;tlUD*V#2GJBrcP;$I2s6d-aSKBe>^OEc7cdEZ;^RC;;Q-X;+(Nk;@8#qY@ z{boe^WoT!r>(MB^xvh~uD5JU!FfO&aQSj!T{M>ilE8HyM;Jq8L#@IU}tC&eAr0<2C z|Kid5ICC|1ec*3;+ORZbvm2N?W8<%Uuh4D9#cA$}^0K|`xz#}%CI{nm^A&Z4@BEYO z>=l+3$}|k$C2^D8n|9>Vmx?3k=Kk&13YE2bC-M}%p8xw1Y)X!M6PH$v)g2_QUKcLU zgIXRZD_JB+HSz+OW|q}kKh+6iQxm{!ia4*bGq^xgV|YN_;iDQFTdF~e+^B*6>S8-M ziZ)j3zJYF@XCuCXQ)8xg@D{Sfs$G{~G7CAQce~dUbyQem#eNw?wHkTriZAwvFJ|D5 z$-_<${H{k(H1(Kczh$M~Xb+)Ur5ntWhBm*o>Vo8e^$q=$4HkXENcN6yU+8 z!+ywbMT5HfmUdC`Wva>(zPM3g{J--IG}8{`)Id`gYl5l5(;Wf#!@WYI9c|-_=B7rl z$(fuXb_FvlL1phE-_>?wq}9!>0gbv6A3Lw}nvv^Se*N;xItl0Q<+pO)tll(W_K>&G z62?xTD9j>3rhoGN*cI?nk_EYxQ}7o8eAg{0zQ+e2?6`jHC?l@hSF0uh;I#g?v1I;8 zhX?$Vu0}(s%QhFPNp|-9muno??IN1m`o%vfd${Io{o?4c1$oTi!7e>Or8 zTHp?uVhalDZ@*q{<1lDS2khTRy8dl+C*_TF-QHY)Zj#C6jqB|Kr5KN54vg3!=$|xs z)o@$8T3z(J-el%uW{MlEjLOv9BN!~*fy^P!gmNws@jZ=H$$7cK<8K#4$$qiBE@1v@b14JaR<(CPPsBz6 zmH$F3@SMs3aVQyaW7f*_M5cSlIERZ!7i{G8vJHjEm z#;Zxdk%vrwd%ogD?DJ^V0I{&l)r)#A^fG~g`*|=q0om*zDBf`gE>#&kTo#mbl)L6f|~Kh^k#7mj6H`oIJ9iHNor2 zJMhxWw6&c2!r61TiJ^WINW;7k;lYbxZyrbr-iO!QJW2|{Ta#c{^>liwnhr;hTE(9tDbEyP2H-vbdVY89Vr10q4d)klQ z+|Umyz}T;fxlSufzhnX%k^^?Bl>mhm892$4`PWZeMVB0$h~U2v!I2pJ2;ZRGy_t~I zI5_4&|#35n2sUj>KB`r-aNb{$#l46=la*HqomZQeZ1XcVIYWRgjp}Y zgY&zDK7Vi%TiFD5*~`##9ye7|!%(tMcyh41y@_j}umLsQPDg7%-#PB8H*SCEe`f*` z_>REzdU|n#dYVO(dP#d+M0K?QT=W*xh6mc#PqI!29`z+LE{eOtoE?w`GOV$-h0&a; zndz{f?N9KjBxfZ_1u=y`8)Y*}C6bh0dhs5a~V%Afgq-^KS1@qn83QjPaMD9Zz2sJhn zc4Ta*pAF>&JRl^?U;ew?P+vtOzkuz0TR5e zZ`KK~ks`+PygL9BV#UPRkw%?|#i7!1s{NH$h<~%-*T9$$=`X~LsJZIT2Oq{6KW^^u zdnG8@SD3;RCfCtCad}8Y zikb`26C*CxDZ8M^3+A7AEW-t}7N(ZIZX+#RVKmI2XF$3xd-apn*pTQC_WJxza}uFg zG4pBiVZgm(VU1Bp<^>nb9?5t3NWtRJgNSsDTY@w!Ec2gO*@L&=ed8Bu`R6yyLL{bh zlY-)O*M~-5NS^_gf_;Y5gJsZ(Kt^63DJKB6S%(lKwiCD%sJm~&4{uV6`Go<(SfCyt zC+iIk^x+<)+LMN&| z0a_lQ{X&vIUHu3-qntI=HT%E%qO48AWDEG{@9$BDpFwec#igj2lmS7Hj0B>^&mU~M>SkoLl;EC&uvs@mg;bbEs4N9;De38oidx~Onb?(Mli~y= z-Pwb7nMcd8&26o6Dmq}w;lrxdN~qalG7`i!-O(dOY1*o06!j!bC1FNJuuzki@=myu z7Ts@In8hU|$*N0m^Hsof@@r>*idqtnoUACbHRYe3*3yhjN|TUrMfS~FR-{dyt)`z1 zPmPoE@dd}#+1uB#lv*_Av8pC1YN`fD<;`j8$vT^&YiMvKs3pXurb!tYu^gBjG_0_e zjY(NBS}d=oGwqwif-J285?IfzEBi~VbLv{M3IHOFlaYc#6Ztu7K?{qLys|F1X^Zk| zQZ}yC{e4#Ivh?YH;-Uo|&_v#1$1my0Y^*Y3K1MM`7Uq`MT1{tdAB`?mE|sUAKiCen znp`e`=|()wR+jCO&1+uwZsxY_mG1Z(dcoEF&6mxer^_HU=FHbcF4fQ5Fpl5(^xf{> z$H!@#7j7N=>YulbK4`vbXVqC#XMdad^#19cQvDxeFrtJ1BPsg7g?~-@SBEGd0ptH& zh`IeAYqTdjaku{wNB!Tz|JSP?2vGz~JK@ET2rDU}fb6-DL-)a?tQ<&A4#@p}5LJkO zt{SVJHKc&91!_}BHaRHy7v$LkGK{(VUvN1$lYj73)JUd0XqS@bFr7TF;UIL~ab^ z-us}T2#?IvmDo}jwxSJ{v$oZtc^>Y)YKwJ=h39{+HKXC;(a`&|&v)Qrq^JF*93XLJheVns&ldXwdoj;zFLYsx`oLwH+r>B-^DyE6=?sF-)X!i3njB<>&!2VZc+k@v zwJ{p(ux%?hcoJVeJAlf~Ps+++iNfJWV2Aq;<@7`TQfD-ks5^ zq+N4n1$=6xU}9Z<;y*n*b9*?)N=;2=Wo0cHvuyJ3=-_Px*}&t0T=AY>U(4@gU6aGY z!sc_0I!Y_KBQM$+=A?^Ngg;%ntcNSbrLF(fYvV^E7ek%wf4JnDyd; z9Ns{;f0v5N=#L_Vv%#KRuoD}OKR||X1(1n{y?#8;R}Cj$i0-j0SKKyG#+uU*EY9%H z;NFTA4|E|f9IPD|J1yqq(z!#x_Bq(-sf=f$s1X2L%%z>l=#UnDT+eSR9#c9ka%h$n z-p0Y`p<-oRuO$mO6R1vyJ_}}B|GW2_>3G`63s^B#37cZ_2~hOWzW|NR$cC=>XxOw! zE_3psznHVjmW4w|iE`xH3>ej~eaqcf$2FJQ?anjv+<5FcqMp^8G(*5LdO>|~FEhV{ zTI0Qg8WfQ#^3xrC6N&@CMM*$5_z2VfNI+3c9PA`D#N=MbgWtZ$;`Q=PRvRII@EZ~S zm>n)h;&h+orR(L+aVsKgPAzHX9M2My(;=*?E&oqtRW$Buiz%Z6Pc!YJ-$iYeA#8YO zvhj;?8_Uv0IuIP}QAc|pbI=l{&-@hq<*8^YvdVxHZpinS`!nmUu1t`;uaib1Cbu_! zXjqtHvs?1PL1l0Rn&{X^vFt8?iI?>2~vt~T$~Ly z6)bVNse+DLylD#?^Ipi~wA4dYlvcHey3YX%W|r9zhSHxi)~c(TX;i~wc|Gz|7}%q? z5>iw76BTo&u0y96DAy7ty+w0{t%?^~?2$IM=UTrYxMNW4|0qOlVoHv-?3@ma6yLBW zm|at{`>V#I8A-7`>$$DdGCTYzt+W_0o_P|qk^JJ3#Bem3==5fJ%-&~|7a9>l%Fh31 zh@FL8iG~d8WFJi}FZXyJ(_VaPIBx;wVnq$BaVtEb!jbt-YR0$iI2c!JTM|p(s+@v` zF0xLPr0JBD{c256J{O6|JH#kf-vwSpe;ziRnx1z&fJP!l6{7yCKSqk%d6T=|mM{QO zQFowuS_XV$Q=)s5Uc22V{^TLMZuSg|J%{wrft>sA08807^P`kkVscsa2-|Iyp=kWx znBIIlfm5MlK3j6m$R3jyMfO$(6a7zviq%V@`oPNyHtjXO?@-ArxZD8b=D>I?PFL#C zL6LPH?ri?|pt{##rlyLNiDij)3CSqq-kcBE%>AL-eyCIyZ&I!5z!+TnuqW{6a1)hEondoZ6(AF*n+Cns0tdoUWV zNaz+7#3Swj*QsP99CakMEy2ggVfAlXT@#LiRd>(1{ipr3+3E14w!R9f*I2TQnMLG& z)&MeG(|$eGs4%Il%iE4|%n(hY_4lHQ3Cnl@h z>0sVP9}M=aLeO(Y=?S~Kb4%13oTqbyOo@(D5O*JV;L)R_q87j-ouxS`ghoXj&g6^D z&(D{PmGzXwA1>sYTsb}zf|6P*4+@FSzVp4%x>k1Z?hW{r8e@LbX!azpHB|nZ->_$a~9n{b26Wn+;eH7l)`TEt?Gv?Xd40Cga|JArWxk1(ah#o=WF!# zmi!>?lIFX2e8VflB%g&e@*pyn%ICvPB33cPP#D^_4`uC?-aj8Yx6&pMQnSYMG}ryc zzWM-%_|ud%%pjs?p=o!17sY9zk@T???Ln^lc=b0>D zVd;AI)JK^>y{U5q0I*x*L%zTy$5m@iJt)JLv09g?F;`UMxT6X?A(}7udqJni@=!4Y0T z0qHJNB}XS7hy1i@KJ#Mo2pIpt-|}1gSucL0p~|8~OORF|!B*|>Cc7CgBLCp%L5_Nm zMHh-LO@>{{u4q_p#2pZ!&g&Jv>1sLDN0!X3c8YV)DfJ9vvc_}9`21Wul!11;2}tYy zfZvy9!?V&n=ASONOF0^DsM@^=n^ohR+5Wf-cmikQIxr5zJBa6>gO2hHeO)$?)@(`) zdvHE=@Jn61GGgC^CgVCs&GzPw)9PJmXim0socV&gw2?D1-`Qd9a4MwtYQV|O&1_r) z_I?gob7cxUF<48Bebgg)l}z~yDtYH`^dM>SeK>HN6F^aDbJ2j_)iYv%cm}V#v+~_# zNIE47d#}rtc20FYm$Wxp0zSKT*A?-i$hj`huk~&^`W$j?DJj_ruld4?!*?c178cK3 zuG-sQ-SHS7&a{ig$y^--EUEz%q|n&z>WqN!Ys;Up0W8rCT&ka+8r^oX# z*SOV#bKep8diDMSb|R_;Fl?dZ)9`z&i{+M`SyEo{aj>R6SS{#sl!LR4eBX;*`CLbj znzLhLGe)h04-&v8G!Ouo*y1DcLbO>fF=>mcqr_F)yq~nc9&MSdj*+n=Yc8BI zN6B3RwWG@iAh&jtyB)WGT_0Jmb3{^zpm*s*Zw?YX;`=Ateo4LrmrpvJq6OlgccArc zfL(GfN0LyUvctb$&HZL}m}(SQLZN0l&^DZ1DG%W7n5Zz2vS4ijY|Z&&(e!b#y3u0f zg_~QI8;Na2w>;FrqE<#~Z8~T;w+F@fPP|P_9ob)8+EnSZ_w5}UMg%@$Ho0O9R#uW` zuZ%NY;S3CJ3?8{5%gI(B?Rj4&pz-BB8oDr_X<5+y$Zft;?en~{cVDsY%evT0?pE&$ zZ^7iv`1mtYPQtXlIyDpC%yjeqy_N>k+Z&aiACADSuUgJ|U)jvtNf9l5^6f&wu5*|J z<=$m?a+XV|<*I@51X-s5Eju9v4Qp{xZcUH6&5QoGwAO#~BY2erGma2d@e&u=ki!B$vsaxK%k* zuk}QBcHRr6DJ`aE$f^fAv-pe}54A)XjYc$*q9(`he};6XCo!!!@$L3kiwV33r^eS%>x8MWF<3?Ptn;Sw}!tfmk;cc=4idV^X-47_PZ;-4AG$A*Cco{j$mRMU_qlBa) z3(dyp*v%d=mCY)N+n}qwqXY|sjU*rbz`sjtk2c45P56fJ# zd!%QEfpJ|R1UPa=>Fxy!53)4JJdrJ7? zoT)9T4yS`1uH=S@IzGo^mcr>N2<+N0xL?lFhL<6qyu#n4sE)zC3hyi@7@gC2Z_PFe z@H>;{_Q3mmMNnsRBN(m!k(|N>%x>%lqG+%C+0S$lEbWc{tZlS=Z(#ZTM+P&CuHfov ztz=1I&~lE+M9-w-2u|})2hHnshh3lp&r!|WF$*zR)vVDrI(673)62G7nta&_hbvO_ zl}^9IincDVdVh5hE*TeF?9pR1V3Q*|?(FrN(`64T5re1+ypI4cpqmr6{;IbV_V@y# zoAbL`T+al;$1UUA$S=E_-Sq#QCugFjiX{Hx@CM=Go3*$;Gn*k?=#R_u6jgVN{ys<1 zz7R0&?ZO!l5ir_JnBkOjR5}PGu{vV*d=kzgHM2MSlst!mHJM8MqNw{)Z=AxHfB3s7 zN21@Db#`&FNKgm>X@%!KkzkWNJTkGn-evOl;z&~a_g5GAakCj&hS_?SR!>JbJ`ZlY z+dB_hm(0PHU;|@vsGY-Gs&_LyqNh-(?1xmz=`m0G>a-pTFl|o2GridZZGl{MmUU~X zsjjRuY6cZJg^GV+(2sJ_j>ZmaymzLk^xrf3bs)=#X;!?#PRfYZy>mN_?O%@CsOhHd z%%1DkD$>g>#<2dPa);io-+Eo(4Q)BTJmw>~0u1pcs}qx{#%z_;5QpZGyzDgWGg<<` zW$Dv_CACV|G|XLv9oC8mfozsuVLT>_m#=@lO61U`q@~{^-M9d|;2=%^3sv2DOt!l` z*+ZYL0}+nodZ&5uqUyWt+k^0m6uw9kkKeKP&+I{y&PTg}4&(JM101XY{aRu*x7Tj6 zHk!by!S<4TrKl?i(+SPd!MjcAY|b>AXoBrS^Qk{~$t$NX7;o+`An_EpYm97I!>iV@ z^Hi322%u-^b?4MKU?rs-eU`y_2PJVcgUQM)o_2?8h6c)FtX{%ERAm+IL&$*nFQV5Z z9RH(=qg@Ew!&ROeQq}+=Ezz3OVK>>|+GANJuS}T4Cyy=;uIQ;E!|ZBB=bh5p%UtBy zoqhFdF6ze}aD$KQSVI%h#LnF~^^4Z~ZwLKvFGoJ(kptA+E7zUQ0a(A85(26ZqrLfp zl`y9E++JAuU9uIPZkeNi^fR!=ddovIN@iTlmYCu2y4&Zq_iwudN(>`GP-N+UtAX z-Y~Ug{D2yEd=PJU+-CY8$6wx>HyCe8_2m8o=18fKq6Dw909Np+#}6mFx6^2rI~~7) z;eyT#{Hd9D8MS9~(FT)~oUph9%MzE$c{@Jd2w<ti-W^Tc1 z&q?Hu8Uog|Ql%l55+k==3+59dv8R$NQond=3(C-g>q8akZ1(sGo+cJ2I|O!Z1R|=x z-~^w0DV8-LQ>zVg(Tcm=k#*3<1N*~DyJ_QP^qI@u!wG04SLoBtzl`8w(9r)=v~7}z zS0EWqYG}GxPCzsozt8s%d_+oZq`DAFu2|9@r*|5*|C%5MIc>XN71sAIhlCk_JWSRC zqmFkMmIgfiRf_(wS#@lST?hO&uD#hvTVpD!R(^ zJBF4O)J#|H9lo+!7U++Y?)K-)-|4q*i+hcK)w3gcKNC~Px!W^vLWECaOT+PYyMC3MVi7Jl$Ungrk;v7)4!xviNGlAzmcp3t0tk?* zT;NoqjjSuaI`5#DqE#Ndvk)~i-0{--FQ!>nt-A>e{zeBt1S(^~4g?hCB%qf7D~VZN zAk?O^hF%xhG6Y^c@W~VJ0^`*6;43sm6rAAI=rjA*)^M&Ij#`|rm`oA_15A#LAkrL^ zh9?+^8R!)GTz>Pr_sz88#yd3U zThu4))%x(b^h-Sv1*l&_`Lw`&?BPEl;62X?)d;}K0{gZy1AMBRt^$2(;UIB$&i(@z z<))`o4o?+V)Iq+FRnDs>#U<;B3$-V+A{|RQCF}>R?k}4lo0aqCC-m)-~RY9GBuUIX|80;NkuhEPR3Ta_X1m% z7IEAbRa={O-oOg-_8A$b={28WY+i_oH_I8EA9~LmOXYOn%7z+1en0=_&wueJ*cP_3 zQZJ(R8~Z8E52_OLpK~X9S~AM$`}!r8!h15@6Kp00g+(!GsWJ)%M1eubtK;O$c&nC@RLrO@QB-t&@~Y8>^$mu@Q)E`C|4v|dwJry67(RcnErR+aA}?6C(>%EldZI%Gw!)R_!9 zfTFUhzlPXQo_H2Ab4Aw)F1!P%%zKr+p03LMoqWA=zf`4Jj4C;^QMEeyNToaG_FWBT z(4{#mzrjYkiRNl@=35}oBoGX7i{3Fy-$rohBXHb*Z0hgc1pD+Rc>8#W8sp(k;@L67 zG04N%lK#)*OlOHl(8W?~Nfb|JZvLRE<2Gq*wY+xgWvGqBwzIqHOt-k)2G7l*A`WM# zfA-lSh0_4H3Wb8n?43waN~>+i(MMMA?(Lbo`l0yM;p#!N?a)b;fVk&uuq52d{V>cY~v6t}9XyMBo3 z9O@{E$a)DoSX!2az3^j)3g7-AqagWyWaRW_js0dVD|XnWSvLM*zoz%QruHBz@nZ0X)OaZ&=ypRY;B+#r1Z z|AyGV+5N-o|NjE^XV=%IWn>~f!EWV9kwb-vW5q%L+2Vpv#MIE8Tk{Lh>J z`Rw$)H6gh=|LjGM;(u!k!{dMsC;!iO{NExC2Ri@1BL-9qin{*|I)E^&|0(<%v;SW1 ze+&O&4azDir38u~oE$__B4T2S1sVK-e)#6X0;E1zM9shV-_OJqQ?Wr`1y$Z{pP`C~ zB;sTbbkC3S+Y~glN6n25O=vjw%x%kyxt?YuHjFQ|D2^}8>`S2R>+2JjWxwt>-xm`@ ziH{3cI1)2r#Xfm*BOxUGiH2_P;GhCxNNo>>V_4##hX=Y$l>M?K1K;oOmv)UIiWTQ? zcp~j>P2S4*f!CVy#buUTDq?Wx(U3W{q`JDeu1=ATDShG}eh=#3^cp9N4eNVx8;YLg zFN+a4qY+mqcX)i@W_qaOY^dw`#5VuCon6MzN)AjyQXK1gCf>1tIi-V|cK;AUPNTiy zE_6}LRsMWmwBWm!eK<_zL|`fV)DY z9+Sxse6CpTXuH>cU|>MMe#P9}T)uuny;7B*@;@<~(Xqf*q1x=0pnYZ>eX_SprgMP{ ziL0#6)W%D~MpIP7tc>2B`mDd_3)zhx^hRIn`X?D{K+0AtaQZK!mj{KltnpfEmx#&O z(Ni~j0cNTOEDP8BX&fGp=Yw~nU@%tFzOgJUkUMSe7-;M@Y)RW5BTA9IN53#5O}mwg?kH zZ|`Cm7%uA2kT6(9bXGAPY69y>jH=Fb&L;V>qwtodTFM_qGjiEuHDXi9sgs|#xoopL-) zPz;!aB{=Qti~)I^(Hj2^F^IroiBb@PWCFfZirmv>r1N1eEqG8`lQ zxk1P8vw6M<>@5u=qkL}Vh_Emi6-DJg3Pg;Ihy(-#W##4Vk}?Vkv8%FLS~%S{bfzuw zLDaP8J~PApu#s`(B`lKv?uvUnF0}kTcO|2t&dKrf;f8iDjNWkA?g}C^5&?>JcC7Z#SYifYWX-DYw`d~7d!39#KzTP-y zRhf5q)P*UYBDEXjKoIIo-Ab4+&aGSs8+u)km;a@$4Ux8aZ>Sh=MaWy4h=aZlHXoUAnlyfO1?N?qxfq9`2pf2On;9m7c?c4MoY-N}l-Ota8%M7!*j-0RK_ z=>+6$)DNnS~bK3^3E^A`xk81rrsX?Siz{MleX1tTOlX>$0z{$K+uIwb$Ah(+n}R|;!@pI8J!q>b$hjp2I@nLM1=B3I3(t{XSun(b z!70Pjk$h`5)lsSczKEKZyZ-7-y5+{wLrhffq{UO`$dh+n_N&ZM`~0G?0|`8H4E2GH$-NR>UJZ# z-_8u4e^jh=*kd$C?z|Npw|~WrB&Fj6G1nz(n{ydnjKa)xGGi!0V=kpZ{|Cs+z*7yh z4LFr&WCNSKsHp`x_LX-FJEzB5SXwF`4VLuv+iGPZO?C-SwAoRJ?8yv;39qx;kmchC z{%_!rM;sc!Ejm$l%=voEsk@NshCmD#qYght(0?3dBfI1%s=bKiC2=Ikt1aQ~0UFYS z(L|cR=PU7PFs89-rdzGF zk)(Jbz+u+G3Ij3jb`{V)zLn*6#!@`8q@1S)naN|Rro*d`gj4B1uCSVV6e6Xl2e)>G zRf>@ucQU~!@dxHJ&04E{&s8(dE`LTfm~SI5-`>UsY&Z7(UCMPQ1MpR0TSWctD#CL zP48cR&&iNwvtCCXziNG{rzl91#U62qdycM}IzEYA>&=E(5eIU^C0owyqhypTm!MyrLviX!Q;{1=Py6-!oHBa9yp51>&?5((dK0UY zlE3nCJ=Vb#a$oWaoxju@KB}BzpF-bgOcnD5+4B%1RjaWu)JAt=vf4pg!Ech$u_XeR z)Zf-!xskXivTe?%U5#~raD4iY zlrtF4<}&q08{G{lg77Js{eog8<&^+KHZ8_xwz!qr%9gw^23BR%xU5*Y_8|}Q08aGp z&PVbPL7~l#D%!3oD;n~%W%>_ZnD!FD9lyzt@XUjQ)1?Fq71hORJ-KS#PG}gIT^o?e z>O2K?85U+^K|_ObB7Rq7C1G^*hX40*+aoD%x_cXCj)uE)0PLrO0d%*0_`umO_TuWH zSt}t{;3WBW2YUqA2im+Tj-4)UALJFPY*yG|@|a<0$e8iiBBDDjXwg}~g*}02se(7^ zR6{;{5Sw+;V-AEv@5*Urt#wc#^r`3e+t>^pU&S04T^nrRXa>BggBv||@As^@emJXe z@=Fx1^J9)Y-HH4}(PD?3Gg3nq#Y6r*IQ#jNSv8YhVku6ShT%f{wB@3}3NlR3c-Gd2 z)@f1gz(k8Po&@A^x5!D^@s?C~7NBS}|7b8~qNl3jQ%!Dm?N>m~B-KB<>CQ^vrm>F#^S@!gn@{-m>j zqZW}=QFM>RWZ>mdN5E#^NxhyOciA@)4mCT+3*Pfr?rm-WEj-w{osLsR$0@n2^hNmI ztWCl^dY#vb;|m<_WJ75cEbg}NYp4~gxu@DGMiV!xbYi?M9m6SN-So8ly@c^qO$RFo z>SI0RD_k;nQ5`3kc#cujRP^Iy@<9%qXv}cT9#MC~pRV9qm#Z$~z5R}(+QwvA@xzFG zf~d45C{9{0i9v5P0l>oPcnE*dAtxB(zjHlfaqI+{qJNBrX@@T>+$-vHd{J3` zhUE-TPwMIA#*`{etJ9{Oh{4wW)_63~k$RQ)Y{H)18W_7YiNo46bd)W2E3i0#qGvRu zEI-o2_4V9SQ(9XP4H(tAKczYtm!dKr2?eolx`h11o7}C3{L9ge<~zgO^0AKZjQK&B z3$ze|88Vihh_L=w0y_6UE{Y^Nn(69HQ`wOW)J&;1`vYcsZG4F(Ey^EDVbC4A3CI|F zy+bHOusn%!1Mi&fxU&74YJFnA|GXLMumTbzwYd=|-o(&))W)P}^y!$U`}x49rm?Y6 zn4bsJ^gVaAd^HzsqW7Qfr8AlWwzZaJjeb$}F98AXBDnCjR1BZCPK0y6lc!DVZmz{i zv)u~jtm_Pz(>NW8Q#4#8ha;p6dTJf9=k88wsYBwYXhLO5c3Z?a2`*%c7W!N${*jcd z_l0?n+?2mr0GN0^@PC?GoFy-@tDs?FNovrTPYeh{ zAR(uRPbROHg+t$XNdJzEmE|r9LhgIx2RV<&w8~_KYX(O3W>X-1i279K=Mpd!r1uzi5bi(cKt1P) zby4RRLZ~-cnI#eODa)_rlM)27NmeL6;W|K+c#NuZyfDz@Y0_D3{83#l$5pqjbl$v9 zK6HgG}i^$~ZS8pQm8^mXjn4mGc&&5o-3ftb!vimUBMFlK~A|opTc+^5#H^P>3^}QR( z6Y$#mR_a(X2DSQH3AC0fJ81O~2VzUy6NVa)O>Ja$MC_11Z6p|_oCE3|#4yjAf@*IB z(IK7#$5(FS%$iYB9e88CC`eX58B@P|??h<2=cC$qT05?Hul}l+}eM z=?(iej{I=436#c;uveuh=}5v7vBD+8<$+sb&}y*+1wU#85x|`RXkeFdhSprlvdmZT zYzF>w4zuR4=0K8Dky*v-;&QgGs$6tNN}mteXjm@=`nVIV+roN)uwQBK%?2z_e~yQ2 zoZhV@1InwSww`IJsQ!TW{+5{HnTxsM@Y%j^Eot64g@xb(0ENgHDwLOH%n;`t?QF)6 zMvM2vt)>H$0FsW77{k%4M<_wf$emOsbSoRGJm@O2lPAM=hIR24-wP@%hDQhEl`2gi z(#W}Hrb_&auhmmS&!j<1sIb0HpWn8b#yVl{S$;QRy=;@|NPXlKd>HM^T;z4@9k8Wh z41W@=a6R9k(`ml2nlw?OJSf9SixGH|^hQD43~NYYPY$C%cbDP@B#}SZ@?DA2G&NDRB-u$Rdm3Ng4GE ztFL4~B)K49MZ@fP5XX#+nBtC>HM{X7C4Z_|A02%$h!*n!<6CRF9zmMmE`0(iNe@yR z_Hi<(Li=kAX7%__=Yhm8RW1G*uqDJSY_Xk=kPpwVCH2Qn@w>C&pdMu|J12{k_U9kq zA3hV?buH;6B-%rnT8(i+Vy%xh)bZ<)dCcVg;^@c6yn-AZd8aeM)Ftd z=%lMrM*}8J8Q=`qmA1dtLqkhalfi|woilkjbfR9R5|W=zB)n$M8NwSHehzfKu)h6R z5<}$9iNxa$6E$P3bKereB!H!yG((YSugqOrv4=PxedFWf#i4A{)#33>hyv`#E_8d6 z1=)EbzuUW-`9D6^Oc-5pAygLm`RaiX$A}30-aCj+n-w-^WdU&E7xmDN zE1p~5jO65G9z%U^n$)MRm~-(WDw@z34QLhzeG*Edd{%E6s;-p`A$2vpMcd$-munVu zI-3(-yy16kw1HU95?M{6-saKApohV0l+oTLPx{2YHjziy*_Y*g4PhS#27WB4Ef@au zxH0@E*K)=SdBgi*s;7!*@|uM;@DDqap!=;FtgthNRN*9xRhsTjq& z%RHUD!#XVA#GeS6v=S;aYte@AJtI_*!$a?h$PkLB9KHzyk32<;VAz)MPuT721v!1Fw;)ZMbJ6`^q4{D5Fkio1qI{EXC z^FG-|)5j9dRr6s5a^5D00xI^*wO-uv(>%*EhOk4JOzV7s`**#HZ`^v|t6hL1VjV zt%b{hWH|8P7-=P|IC4^sw8Ioi^Twf*p2P{EaJWwOhccTjBPsC0OLD?sw$xP9oy=Kx zuyn66P+sI+IFEJpxdVE$R-OrrB6jM#EU<;^Q>s#Dn3csO8JHEj%El9d!v5 zW{;-a<+NE9c!8X&ic{=SwJ*{PQ0NXTn?lCwXzEC5R!6))v}2G{@Nh2?sJ9Y)KWOCA zT39b0ivJb;4k1SJz=#uY6pn<4^B9j=HgwkEB>&jFj)Q=K{LY0nD_;@Id9Y;7qw9oJ zHhyP0AF^x>w61a1EwXqjQ>!>`St>VWM^2vr@7Xmkuzecg-s~t}ge81zd0&qCFV63K z&TY?|+w)l8J$Mm)oR^j|>#OJ)bwQo|Kw#CcOEV-CIAo-EOzl(a`$SD z9w;?buQ@!>O~0)=iJvblv)8od(XTGCAy8Ueo>M+v*tU$y%`3h&Qlh4H!w)6f@;2a<20m|w*p4 znI<9p>`#V;ntyH_7BLidl=!V_09b*Ip@z+rKHkm<9$IWfXiH1Wzq~0Gg$W#(5DA#* z$HQekciGJXmiy`)9kJA%IseG^&<1QkW(Fz$D~I+`9ZwwS4+{Q_Ng!db#tNULE**h5 zw}A7=tf{R{P0glgTYtH_u%e@2=<`K9J?dPnNl`^z*4>@73r@xgVc-!OH>z>-2~onu zg+o|aSWH~Jlt>kWgoI>{10EjUjY!o(sgA+&J?Jx0CBh{Spz2t*Bd;uO=1b;qRqn#P zOCqxXPpGK#KTpd;?2_ai|LBWFrL0}%&to?%9SZtC3AQ{*rJwg@Ge?=LG&{>0)0|uw zRwaczKG~N3L@=GkHmf94yId_m!#n7M0)^#+3!utJeml_<%-;KP{~(tz5K`muB>SUi z@p3iSlucDhiM*h=SP}yo7Z+Dm)&8rXAd#=HFF1y|;T}fC!0@(`N+hFAZ!sK3MsBKj zUAks*VBY2UO#*fcqlon=eBuGGwBJ0krLc}u3?#XWwdw?1KftfM@sIu|R3V5g1DCEY z+warw<`rNB+v*!_(f7>aaj8DTlD)lq(7#XGX`J7WMu;OLRq=%>HaShaG6%k(qbB38 zYzh}I4577mY7*M6AM^p5UEPY3lsRN*iK~}L$57`z%S}8nM>E|@(g#P%O$M`k}8he33z8MoxM~XxIJm{@L7D zbli!_N{0PKy9WCRr|9_<;5kRwK^+d)k2)q10l3{dm!A9L)Z5oPOjB7Dk0I&oXpf|g7SqbQ2TANw+`^5^TX+rGitlSrF&2TwS#Hu<6p zqPF6wcG-qnyVv;!37Bpq16|^2_1^5_*Vaihws^`UgE$h-&)45QV2GdO z4|&2(q#sehldht@&*Jop-{lU{F01{Q<0OlumrragBtc8|ypVe&5o-ufwe=vHlM3@{ zvVz&f+WJ~+XS~uxd2oPZX#Ay4TagtvkC^in*O@Y@A;CKFVLU!xsr_a<=)R@FcG(rW5>0^u@bSK5MLW z97fT*u!SwEZ1Aqm;P4|R9k1k%C>-K`g}zwW19#{r_rB7iUy2`Oi25QuFKQ88p#9g~ zGxf5Vc5weLf8SnyhDJSRn%s#|h9Ke&O>ITXX4L!yGy%+MyrV~9Jqzhp#Jo&tU_dB+ z8_r0>A#_RH(FTtTtV@0wQ+-}@o-D%5E%{>W#{Fh@_+5{YH#e~R+&8`UdrHc)vlrak z@?D)SI&ySdd-1bN_eXtqW&XPbLDg;}RN|9*GKxQmW_Y9bnSyBs6MIIl(_vqN&ztnnMJ2 zyQRez8B#nFv>jpWQjswtxk!$_kX&ohgRKL~BxBzG5)XZ|ZREre!>=(_cFyGWFBy z)OhYnmzs{w%gTLv1%8=xBddoYe)-Tn?BYSzGL2?=_MT|TZXvmMw@ns@11)<}^m4g6 z;Tx}QET&hwzwc@`!XznohZ%dkdU>9fDC{L#H7EMp7L=C@G_;;FEB!?Y)(sbAA4A_% z^vz8Gn0Qbp{}blk9-Q7#mGJ15sdaFKb8|{1NMB@epM&s-(v$y#K%^tiTQj`FomTnf zj63uVZQ++r!mz`jQu&$-G7{BKo?*(;b~5#8Ma-cCa1??tOUCDe>Y@BzgvBcAAT3H_HHq0{Of}%@db!r^<3#*Iwbk**44n+SES|*EHkYkA zSJ)WVYc~fAjBM`mfvHkM`Y_YjGJe|j?F}l%qw1P3Vu|d|8}C^K&9bS{_Z6P9_79Q5 zq&lhW&U_O9@huh`;+P$@rD`X9wEe7s+(Yy(u5Q@D%M1htVoK(q9nYp}w+TcbGLG2$ z7qphodZVMGU_%j$zju~d3M(*6u8FlI(tRl3-}?et_D}Ar-P71@{zh@ma@taZ1N2Uj zZO&K5q+q~lYRx4fMFfT}h<9{kgoK&7#tQh2UQIreXCUAc0$6LMyU5PLfl5FS)nnjx zzBiz$E9ByiME;n{cd;B2f6W#Bq>kEYjqU1aMa}2q7x-KWs9&kBr(i0Np^RbBHGAww z(zNCnE9kLa<&OLXAM{!9!)oB4NtC`#fP9si@lk}~csY{aDy<}GhKy5p zm`vL|r}9N-PNQxPScCuc)8LK<*sBJUb0r>aON!qg42Q@FQyorLF}2&;R<@7r3^dSt z&CG@samxuIP;!i!R$q(t9@F!!b_MjfWKXxr90w)eO(o;+pWsFx#YKG_!k{^Ovbf6k;7*oHX6dPtE9Z52XfsY~0@Ib93|Dyv8FS zCHSOhCC%1z?PYZwJ`phS-5CU38#4$*nLJ;G5geDf6XFiPW=7!fdC`FatZLL?wzjz< zg#%$&mki;V8z@FdiJ0sSY$rm=w(D%@Ix-gG7NP3VYkx$e0g7xXyS^rU+riiLCiXv< z$vlrsNXQSDYPxdR?uXl-$Xu;xXJco7H6Do|zW6FyWLr=?A2L4!Z0TT4spm|cN)){e zr_Wh=hXv?x8ZlX56M}Pn{_?`p{azlwS z3hTg?yM0tS7jgI`+ll16{o&n{kt<95?}qy)QD{ZMuT$EhsU{6yph7Be&SDmOZwdw! z+$QPeo@^KxTGvC0O};ziB;fd6T3Lzu*|d48=$#3ZUUx{0($i-ez$bi}IF}rP3qP;bpctRKt#h$W$ zPcr%pSBK4NHn`vfKCLi&AHqqOvMe=sr}hBMYF$R5O%?1+Yo4W!qJrtN+2KK2SkwL_ zo8DZAM5D`-eaj7NpW2AyxsSX<$m5}}!!k(+v(W=Ldbd919bOJs+3Re{(JG?*U=40` zZF?C}GiGO+NKSUjrquD}GaL6NH?5TbFL%%}t!e^k0g|C5hEXa4#{h znUAi#21CsM?wo55*pdnzOefVBDjWf)X${uv&VC2w-|Ji5Tu#>doV(Z(PCy&hTu#xw z(0JdN?=?Z4Nw;_$_Co;1AL%i_l;`|@lz^@s37ac6yC45}l{$(pw^^`Ttu@(A?NB|t z*pPp1BV0EnpEzgH<|rQ+ACDq{sf^QDkIrqvaow31O&ZQN0jT<1^-cnG$3Z^ z#%t1DEanVUPYzw|sc?hYy&})d2r{$QYm26DYpUK@&y2#mL(|X+Fvo$NW??UA+Bk>RL7wzxI%K?Aw-(TPQ*ZIj=4H3 zMcpR?)yD%|TaI+|^HwBH`u&Mh4g%dKLzWVI3R%;-R>ATrXv-s}21Y`ab=xM+*1|=? z(cB*~r}L-Q3(=#68avK%{BIf?OO_n86wY0`lgFd4vki=QC8QLr90Wpxxo3k$p6tL< zJ(z|n^Tx$Hm393^tE+*|20(*_?!66117Nnb3zNn)qG9#eMk&z8Z*EQ%tkxn8Sf9qLhH}Nx#k}h>8y;r&zd!+t z_^kvBTMc#2&@eVO_6R01I-4JS6jfZEi;O6=Xh%63kK(xpJJY{;H~WEsKzCd$?&N~i zeB>Gcu-n7w@Y|95=oeDcv3hTv_0sN7g@66?G?bPuqT}_(W@dTHIy&VO5lV|4_uh5O z)_tIZ9sfU#|Muno$BzG>1~{s* z!T*2qhW}jsPvdX!ns8K0Rcr>OY(pF4_?`12?1Tw_4MN1x<+wnYFHomfXm$TkV$9{bY6dQk(X zl9F1C9EdWnrZl`2oYd~FAvWzu-N%b(JnD9iqp2(KJv&NOO&w5w@S@min%CMMu1*W? zbAJ2F8LK*my+JAM4yC-&RsOJS7GG*uC7NdE2n_9JObJ=^cm%NK$P#`X=W?wp);(fD z(R7D*%H95vP5WLaHr=ZM;hTCy056JOvI?Ss!}s<~78VzSYwPR#A0D{CJMo*lJ9uJHws46m3mco5 zyE`{ngz!|DkeHbBcqT*9=MFGGJ}}XR4nI`qi)^rOe~aFVIh|v7zL07~If=vE4#>*ELj4pF zd?}pv)@U>Nc~!czriD$aIUR$i)nyJ@zB&)$uHdGrNl9aKY^aF+bE)Ye5x$XyP2xs0 zg}50A`{dBfLtezz3JvMBL$awU_2f7|r!%ai07+=;d2UrW zvoOlo1kYc(=fQyU*4dejy@{o%6Cs@bJN)dl;1#6(Kw9ID=P!EeV$8+zFXKepl9Y7p9ACGL%dHr;`T1*mqql2(Gd4$j@f1K? z;Up4w@jcGr>uODL(d!PEv`|Zjh>tqmieSc$762w9U0MhzX3N>@79xR@D-zMTVCg&H9!l*oT-8tIsgm(zU%p0GaVt4qX^okgFsUW!sI{aY)a98p7 za1D;$=P514OHEmYnnQH$=v|vh<5lyP48{{1H}=I!RGYt17|YuqCFfL-03fLmrKNmd z`*IdPqsm!c!7$6qH!QIE9sgZY*O|P*`E<+&>HInJwn0TpYjkRtExwNwmWZ0;;Mja_ zPGfe1wO8P`PWB&fIjlBMUazz?W5XY5Pb-#3xNS=b)E}1OC#ElQ(vDUr$Yj!oT=$2& z4g=%$p*qlHis!rl_O&rbG~+30JSqB{qJ9~d8!j|7We0>w)hJ&PEu!y$mO+mU){sma zn%}MujzG^Q{z4kw~=Eu5Y+;l<3-Ed;=ISj<)r{? z3xwqb5|cwXor`99m3wRl11rOD;0jG?*)ewJI+SX{o-R306EZpM+z8k&NVH>cCaver zm<})0FsUd3nK<7NNpJnJx}mlEWt)Z-&{5yyZ7gyX)^aE;psF#;%RE@ zUeV}zqhTjXOV!;lHT8;Yd8A&!EiJJ*F?yBpQypb{Wm8tul2QS|w!!1XWqG`e(@FWP z4D}xI5U8oiR?>c0(#iNdu^ifpjLC8LK{E^2#L_3WGn$9S3O7Y(N%$}OHgbDh4@uTs z@3LECPi?)^+bUYs&E%)ErcFI&7!*E7RCnP3XXHe$$SZ>FR35uKqa@V1+*@1D=62Of z&Q{1GXFG&Lh(q?0jd2j!OhIU+Lg()vkW2=wtYq*x z`lsxAEvR}mO5DzJA-}r8+Ej>UG{yYc>Bl}y4xHk3kLz0*oT`KSqxFh78s%Ee#N55S z9(7$5^CkWry{+y^lULLJ?cqyw#@qny6p>@5iaC)$K!Ft>>x{DidVh{>>e!N|p#1`f zxuIs@qTIASw4uhOZKB;Nb$^=-(aCo|vQoe-I=g$A(tlwC{=|1-f9i`({X`mC_AlZ5 z7&t@s*RT3yl%)8XDuYuqGyC;fpQM^fm5NP&B2@QvFX1}&v6>UiYzJsV-Oje*FA>wO zpy+y&%0}IAIUIa8V`x@U`2s4@pz!BG-+-)Ro3aG`=`bVEN~=c z@DxTZ#K!jvx&e{9Ho)5F>QWYd{#Wv5SpPq_$BX$zMF=@tGOq^*#nn953T`msny;xD z885CHK5Wh+#nWCI_(91a>XeY(>)Xv_yi6S?XOg=5u%NGu8JXowOb&V104zAetQ2kD z?PH^u8b`K*C~rRb3vsYG8FEkt(P!m%!v$(1vdhC$E5Km;oAC1Ip}{a@yMd8~C?S0q zx>r=U?=R61j^o^dUU3JJA35f{*w<46lRLQIzSK0Q;h~}X#?a5r@k#dp$DrPSDQ2*F zFY!E2x3Ez)gl9RK3dp?_gMWr3pAd`Rl8#1U^6GG`D6%b}CpK zaOg;Vi*a4oLFSgaO^7X3oXpBG@r7>0Bw=5l^hk#0;PBQVxIOBTPAz|wFeCc-%E!!F*Ecwr`sWu!d1Msq zq^(4!fS9ZhUDP_-+Ff1u2r#&M%HU+&srjy5zPS_Ek+j;`j+BPxqay)8Z*Jj&jXrud zG}^$LQ2ts-kTdWj715@dYc8~)8SM%Gt6g=gpJTr>MZUv^?)AXjO4y(72sdtAt0P+` z#+`d9@Ql)bK@yV86`1{S_`oeIAjn~AWF)Qkj*DPp%QXK)3d}*lKCtZ{l_w(DF(8ZR zBYRjyOH!AW&_+6I3^_=FW)|4+kE!7h#CufYB#ZfvQsQ3ah;x+m%hsICwR*+Q_cK^V4+(1T z-9i0m2^N+qnS=>_E+Ya_6@AY8IKGEBPtq02BDd6|Kov1e-OLJOz02vje0HCA4S=(+m1Jc&h1NNTn&k{H72oKCq z*Gc#tHGi!oCRg?N_*V&Ji|id!LsrNr_XB+XAYC_H-2$96LElLn0OysA)uH+9jpKu7 z#x@ZX*ObHpxmo9;fWl$-XT~%A{R0$``#Sf8<}q*is982kquyB15C_?4hexC?%0aZGN7?N zA22_HJV?_u^_CJ5FUI4%;yrXHjE=abPg>wma{PTL%r~u7AS*-0hNasxL7$OYK>T8J zGwiiF+Uk_qD)Hq&t4&#uoS2*bsLcA zOdV)@2G~mx@dUsXMdK=79|nGzbWn;Cc|rBNEXiRqcx1Cp!e97>6FD2PwlXI0SJ z$E3P}f?tMZvD7j5^E=KYT5IIxrjcIa&s5&N!Xq3Tw1U{Kbu)HNt7%hhpWSgl3U0!T zkclzVvk18wwUJKFS*x83OP*v`HASdxj)e(qPAqJ5GNsAfSZLQJR4v2yfs!7A6(qU` zJ+m7bivU`u{mlUirif3646GGZaA942)y8RC3q~j76*wCk+q!S;Bq+TROv%GfEba9C z7TEe`Ci%E91hfv)tL)xByJ<0|bkmP>iT#(!bsGt6C@jlrfv>TSYu>BlD@RbVgssi` zz7r!)xVET)HIY6O6UvNO^zDyREWOThR8i8NJy%COnS1w4NYZB3-$vqPu!gH}jV--W zB$9O`N8HCpM);VMT=+`fgRE_1v3aqiLqWq47yUrah}1o`tx&nG{U^TCqg|5#yrcy~ zSvqk3b+wp^0@C?7U7zf~p?O5c?U9HLeQrxM@wYmSUWN9EY%JDx3P9&666T68MS&N5 zqDS@(6Xy+*cgmV(UJhC40feA6{WzEV6%|%_FAmXh_v!;|^x-YlyruEZK*qCH@2s>$T`LBoz&b$KZGHjL_s_KI!k<(;GC;m8B5@k)s+i*Em+%ET|!%$G+a(%+TybtDPQFY7| zd{s(b97ltFRmI48-*Gke&6tfOj^FW}FS62|P!(mys(N{HDfS>2!d?8?BK{60l5q!PFHFKhOduYvKx<}BdmNgVD5h+M; zn7_c@CzfKvD6-gh@UM5xRxG`e6-IxHL+r}RtU0>wH!(Pn^Mv8qQB-yeCj&>wQfSeylYRfez3{5l zv-8thMhzBa{E#9L+MJh_Nqtm%+o71)w=L_km1^QIkapRZ+ZGY6Vc`!|(}eHqV63^nFuu61O;oO#%(Bil zGt6oxSNGx74-Pl`A|Qt4qO6e;02=2!Df<$cOu6&rm!!>}BSXdO-i$ ztHBg}hJaoao>7aeTu{nb2Gk!oZi2A#q}9DB1bd6Li8k(t$=5^%;eorGJDKlv0skDYqUPwjH_u9 zz0;maDVWc_EKiQ3^Iv}sx-iO*3btin5mNSvN0X5MlKuGfwLP+N%YBQ9hd~2uL@F$- z_bby-p*q+6(~WF_l{w9NV*p(&b+yw!J=?$TtDxe@s=!)l_iD(TIZ$$Nglku3iRo8R z0`K(ZlrlK){WbaOVcT6!GR)LnL45Gp6MsG^d9XsiR|Z|gjr&<5fKUT+Nf?EJRm z@PuUILL%~Gd~u2G_H;$W%R?SzJ+Jxvvrv2P-p7lrRH`TUgQd8`r3bkBh;B z!?mTL57@Tt;6eb7V{WsMLdVD2=IFBX)d&lzGU06EFA-x}8$TYw$S2$k=HftdL{v9l*)fedFW?)Gd=<95i2O z4!;UR9VZmP7Y8|5M$m0!i%JK?Nvy?ndSApvr;MvBXPUv4oST%4OyBhMW%@;JP0gh~ zMZ1@qJ;CdrqLY*JI!1JE8un`5$gXtA3f6Rp1fcsiwpTU*lv9UvKh4*y!zd-4_4Z=@ z4+A%NHx<@yn!H2Z-4Jr~1;(H*GTt==(C@(^2*sJW?6K=6$jHvH{!!K#+tXi!`$3{> z{70T4IHj_Kv#9bN9t6iAZZ+@D9d~pLY*o9AP<n81FV5u6Gy+CX`EY-4S1 zeE74;y)qHCOlcX+C&^u5WZ6)ULr&QXE&e_79qYTC*m)Wv-f76zdcZv zi^N%!(seI9n1L-ta0s_O2knG#t{Y5xvA|&!vzEsn(0;*}_ZbgeT_E9aC_{^GpY1Yy zXOY2cxVZKqz0bq@TX$fg@CM(-?j~|5%i5;gLCrhDd8ir$TbCTD$d_02n-%XDkk4*) z36bl0p<$J0oS>D6aNinoUBx-m!(o^GLjWG9^7+n>{NTZLhC6|VZ8Q4-K&vU#6U=YV zU06&8d)dn9tsOgLg4O5Z55wtY`25b%_4jr7OBD1=pDMEJnOrY>X9tq9-yBjd=0K6{ zT|Jj7ASuS|Z|2;qGW7$@(V47d7e-&7%oOZ^T;U}Zx@nv1J4Nqz5;p{p?s~ck^=%(0 z5pCci5eFtTQ6_(}VJ?!1d8S_O*z@ajbTILiM)_C7T+HsyF=2H+&+1o)N#^nJ`lFdS zwWzR)?!3bK2wah>Gh{V$r(VD2T$2fuo8{7lkrp`(6{o=5Nisjf=}uCa?!H*6sHlMb z!muCqe9`sP)zzogvU@$5y6!KOuWx8t1yGjbt}E7s`A#b=Uh4g;bu|PiLV!W*ZqoC| z&)A3QcHDLF!49arI|xvP9R@agXw(da&B2s2OB+Jc)i_^DmLnT5jjYY3*ZMf;Iyxe9 zYB26gF&}T6aN@vN@-i+ zG=eJJF3Q7`N=4e`{4KlWK2+K zl|?aC|MCSC1v2wkB_issd6ebKPeFw)tx+`T$Z1`OA8fgqfmbIQN$iK2y%<0ILY_DDSjuV@rmmK*uVPz6s8_ae(z zY=KKMArvD~6#}7oSOQ#orJ*PF1`EOM>7V(L5*2Egxq@wWb@vw*S}biOrxGb;#}!ab zYzAq^*Pg;rr#PMe7)pI2cg>w{F;LKY|2U8n&GjhoNvMHcAte!s5ey9e<&gYwG9W5R zL}D_wjOyy@s=cS3@9IEaDY$FU>mS_T`FxZS{bey)>v~&K`|KYBMSh02iMbh**-ITd zp&(x~aR*E(j&Jsfz`?_lADd9ZC&q=J$fBa1*X62{rW5>8H$ub;4ECY6z!xvs$5W}W z4MXUXl)Ul=YTVLqg&u2LBhU-i{GT?hyenrC4kYWHMwB^cXzPC_pJd96UqG)IyomhxE~wB zTY7jZ#Xo()MwgTnLyZcIDE1#;7FK4I`25*Bs)>kGlc0p0m?HAeuZSRvm4?Xj-{Rvd zZmd!@UwUg0kvc?92enkY#jdT4df9Aj_h;0v<3=^&sgCs}sP#d6d1XQ7EBc7=q^E}5 zRu1H}1dKnY$M#+aorUDxJ)nl~BQ!Ox%4}?HRrK^2pdg8xy%;!cH~Kd=-elaqjP&;h zU3A3_8?(H*f(vP(eT@8G@iy?8gN3CRi&U;aXR^{zVYunetT{V%VCZNAG)rAnc168q zxRr(~eAUO2beHHCxl%-}gD7R9THCYl1k=P2jIg*>ke}}46_=|{ZSIpUrzr*%`(@f# zz{*HFN4%47C~8RY2cP@nc*`%RE(#8c@RS3pGyYZy%?p#PWG7bMh2G5F(SG27BPIPu z042(bMw9#dAByQOv0~(Fe+<>(FD+ax@D8@$DnOD25ecBR-}^h15XN!cy+5f4F{@7EhOv8m|HY)06?nf{cR9;d~3GHC<+?ia>1O!`&hkZ;W@PcX6%7Jg6F)>FP z#y$#EoU_cmL}Jaa7k_xXVrr^dGrgE)s2;8xbS)-G`DD;s{_EyN-1s><-I>o-8=E zlP0#d3T`fX70R_$kFq(+kiUPOTL~oRLGM04i?mg(TD(=NmayHX37)@JoWNy|VwCzW zVK#)nPQIR4tk5J_zGZzc5u%8caa)5I}a0UOvx4+yiyd?y(qp zLL?Sen9BB=(G=(uYm^1IkZP1dC?ePx!@g4QZcZqu1eq2Gb6WzVbSgi;wO4U1>0#GZ zuM5!J3VWvNJQj>M0Whr{mnT2)0~j&y?osw15Kh@ad+9y`DDQuo;ZHx@ZZ@d+@;Ac_ z>Zc?>tpX|!wW(j+^_M1-uPN)_y=?I7lL_h{S8IMD?7Yc6Jv|kdlo&cY-&!dSkB^gs zGr)|#b5OMB>nkY{Lk0PMG|U130KL!LY3D(<;f2v#TZWB|ji0~jRQ1|F!SvpK)xpEB zPjs}i#_-BPkA4I-_O`rwz4~@EB3G&yY6qionbSV+>v%bO!-J$KbovnbC4%FAJq7sZ z$X80V8SePT%iSiw1zSmAU-$4=Ld^BD=e(Ss^XCvIgxi9yPg4@!-Let9VHcyP#Fs#z zJN@%d?3-uLf7-rb7wMF|dC-~kf@&FKj$ixuNl(s+Ccij5CVPs}uMwr$C?Q; z+Hh#8R!9)Z_Uv8Sg;^W}9ZTOmW_YKrE?(1kQH%*6*uJL&avt_^Uqk*HI{vOGr?Wbj zX}ur}$@!O2B`WmZHac+jBd|FTmRP)QhPZvTwzih!r4o)oL{gC^@uhBP&ta=0J#cWf zvonJ9rSGnb;uC;ij|>Xt&(^-Q!J3T+nesmmCu$gYQ~N~TD;+A5v#X!**?{dn`$AI_ z03c2Cl-HaG=hYEXp)7AMR;76Zp_K?$NuQma5td_z|NG{?2uZT1Zz5dFYisE0-$Oz} zgTWCQ`gSZ*XsmyeGIjA9xqS%}vuyYdOPfaWqV+dIM-qFW?}x4TUtbUY^FP~QI`$u* ziX>Uce|^l~fBnxkYTxL)%YVP_|MNZI&aSMlo0yu0)YLHNj7?0`c6G)4YnYi9{M(D% z*Ku;PIBK%lx|89AuZnezETP7h;fIfEyOutzj|-^r@!3m>)bBfh_{y)bl1oc!7hQqh zD@@(oQowZ)aPjb_*BnJG%*{U^QBY7kR2f@2A`S26+aY!(9JY|7zhFw|(>j))pIqMG zaTL&1Sm&r;m(k0|C_cW?CpO5@N$P*kmz2oMBN;4S#29z&|`fP4|R1_Dmn6ugrOI#;a|s z0Rjs%-skTYurzhn_XR~-+9pnN(b@l{4-t|#2!&;{_VsN2Q{7ojnP{-atU6(LV)_zT zlMhb&caP(R-_Hy!zQ`_4;Xcm@Z@2dr6YBhFN~w|?9A@d);3`i5f$a;25fBT5%0dRQYA`*?0TJ@4{n}2hg-s9^XN&BNOM;0tyr@5s}R1nxw%kP4@N%YU19U3x|WWtcGZmo z!_zoeW|a6Rx>UznLLHnV1$GJ^SWOR;%cRuI;pG~MVt6gHJk^NZ`;zpXE`N< z-(WE-+aI8t%X4LVhht03cx@V=VYgcuz6zCP)vL0O4s?l=Yhj;ZX(X+slZqfsv|{uIE#=GjN{#UEhjnjYY|hN5Uo0z%3rzcXWLj13CWqw*~{2j z8_v(^M`ULXb0KAj`Ha`XG-rqo(lIG}zMk(;{}@Y!S`7E&BI()~t#EBSQg^L!0x-lQ z?K_X=nleOn3yY}!Cf9KpYkiXo>xz=F_`dFU%Ei=dd=mpF_~HJqQO>tF$dkW6`a8KK z6t?nsgaT9W@eIsmuhKOlpkZK2N=s1#Mv|wLH8cjVL6cnzN_at*^53N%{mU;1~(5^o3a4Od!%IabbfW%;|7V@J`G~%~3v~mvvKK>}^;5aKgH5WF^ zKU|7WH}Pq9VV2%{mACb0nlGCmw~+HUj8kt?K7?Bet~LaSy#*EmXLi*^cB}m=vCa-L zEsu{_e9GoWgc5Lc@<AKSwNk(>zM5jB2TnK*$NB{jN z6xBsGQmv(@*3%Tz)vZpjAjAqgP3NP5dwbXs2b?1see-f!tfK1V%MsykquQ$Yqf$#A zkHKFbiW^h0GsRm4B&=jm>f(t?iVqEJJ!4~QaQ4ajRMpijRul*$Jr$&F9lROui)eKONLa<$KieDH4vO`ud5}v4Ptg9m}FO^kL@CC1u zU)^4x*bfg6Ma&`4(>LX0*Q>-BboC-Z*`uAoD2QF&V*y*1kA(t ztV5GiQxb}F`1$QD+Nt97ssscnKmpC==DuZQ8=`H8S=LsDUi65Txafx}#351*Hsxz9 z96tSus)=TPGt7Z{;L0sB>%Q*crg7M@m-Ptz&OrWd%9R{nX>jfnTS=cnt? z=a0nDHx${^wc_+;XypIJ*IPx!6>M9e1OfyP?hpv>?iMt-ySuwJ9^4^7aCdj7afikw zxVyXC>zs4%eedi2)#&cArF!k!tJa)r&M4RRp`~Gn>fa8pZyiiCv*dLKUz*4ZhF%QJ zF2a+`zn_0%W^-NGwlA985_#6?RP7a#ClnZ@c$5&|+A#{Z-b6Qf0b#7g?79wd1rkCp z!jjdsbjkiOfw*^zd2G=(KJ^HX$d%;?mA-Om=pEz_Ga#mvVc%(qQxA@Z`d(daWMdVm zuK|&Tysxq3wRvYmVwR6n5!6I5+@BOvxLSX>5_bD5{hcm0HR!(3AG& zwPbuqAa)TSG143^O3Al#*T*GC%sob34{`5TJ* zY6g9)0lJ9o(|6&W^UKl$Tq3t5004-JwHqFn`aM=F`3JMaext%zv~EkuQfy^3^SM9g z;{W^1{Qjdi)UANRxgmrYll2W)o7pZMlHm~fZSMJV(L0<>waliiOMvs=PPW@9rF*V4 z;&;!ms5&<*JFm+HVwYjMiu^Yp0nd#$|B?W$Ho?{1SV1j?Sq>@pK4stcat=%Ay|Hnb zxIM2dVtG%+d45q{MG77*!LKQ*veKGZp&yUth?S5YnhVnIdIW+eX_qy=L1PM($ixeK za+;j79&iLRJfJImsLHlq>;hbU>x`bKIK)#O1*MMXt^8sjCEm0&*+ z1whcLb7l>EzGfV%?}vF%^3sx~Z@5{;Mo{wewu@W_TQ_KfSWsV|ufx055tuYGXpTcX zy`!7_xzr`=vpg?DqHjSnUAlC$K9c~y*vksy*loV%hDOn05~W;t>sY0vW|mdY_)N-e zno^2VXa z%PJ!P#PLWbXzFGfJa9#kQ8_kJW^u4YuO8rZq6m44xP5RWYV3{xr)DncwLZK&$di8; zoN|&$YHxus5~@>i8n2mlxD}C3d#9V9fGGnRO{Nii9-H5cXm!7?RaWj(YGBemwEc6do=Fy8Oj3^nHcg>tIcL=H6bl4fEjd zSLb!+i)bXKE3%QPUAeq-1bxC;H$`6^rJQoYT|~7`v;P@3$+LEE-?KbA#C{V*{V3MhX!pYD#GgTBeE8#|6!gS%2-VA6(ibjw8iA@<+U;C}v&3`&xq1^|5NhkYT z|5{8%o4ieQ$KZ-mw)O>&rmk*LGYf(bf*{Xx*;UPK9}it}X)3vZEB_!p_$9jUeTpRK z(Cqme=?ETrYvPe?o_WOq#Tp8s40b>Gq2~Ovrr`Tg#B!U3bz5FJ<`90h)($;;UyXPNCFzQUNw&rQp{z^}ZJsPGsH7B5Sp9 zC(EFz*^lVo#jdw=Mc3xCfYK99Bipw48^4)-%eSv9(#D^OKE5O47M_;PQMrd)InMPB zUNA>rTsWLoOAKFRNaU(8-|gcGo}|RDF-fVFpum5l7kpZ<>)0_%p>3mR zLOd%AdZ1B~i7`g|`k~z$c}XOxuD-B(h`B1>v>cH0FISEh+b#GdGq7;J6YU zsox8_&nfEQ#bn0>;PK4z)k6L9^nRoNim-8N*Qrn)>##PS5$^`~YidE&o|6=z&B8lwH2k~y<@MFT&=9IL zEkd51k3kD5O6YJg7&0cx^zyu*sGU}PT*P;pAPqn35OcBzC@(HD24N3vVpv5mILk@E zdf5wlclgw_8i)d<&^phv3e;u2b)P()*Afdd;QtWd45JM7e2+k4rz+6p;`W?r%!6^G zj%Xsk82?J1?0wZXA0&m8Qv|#wBI1lF8R-ZrH)m7I^$iX9DdD6q%koS^K)iw74WHXN zvHNgQyYvEqed8ctTMnUMBi&+g)B}1}g6ycEI@gz|3>oMI8(=W7PRB=pIkS&^;9DZ-j94 zr{+=`9wSBPw(tj;0VQXICI$w-9tNQKs*l7<_gOKKxZg7x5zIYxd$0qmcbME9^34sS z3>d*z={bD6uw%Ce@ufiL%9)g|IB6T$sWWB|VPIZ?#_4QbmVR<6Xy5(fUpVQXtNtkY z-dE+SP#wQ%I9i906&Ca#x@RuSj>3sexsDPV4&?D{dVjO@XvE#-114PE=VYN#&f8)`r_}I zTGLfI!FN)0Cx&;BSv9@H`;DO5o%;bW&pmVqBp@)#_V)~3XokFu8qH~RPw^jpS=1h= z1p-CSk+K6XotRULuq8E?feP%8%|3U^Z*{-(k{QEqoXEF*R~9bg(Jh3O?DhnMuq7TW zI3Q9R(7mwj1*)Ju>g#0`{X>=v!BW}A+uG211?OD=H9o6(ipx1kO z<@JdU{dh-aIxIeOJ(Md=YpRV3N~m+yKQWlM@Xj>IVsHuA4*Ti@sLgY?CH~df4guM> zF_Y7mfJTFG)0>Ext7lUe*4~FeB^Y&zE1m9M0)66(OrFgpH zo+*3smEcqGcf%0!+nYfwsW5A46^Bb%8?}~y(MAWW_sBTbG;hvrT`;4_Jj!mFT1m~i zOGkcHc$e5RH8q7Ef1_@Pz41a}bPc8^_*?h=#WL>lQTu(?d;M?2v~p%>&xnv2XCeef zu-CR%S4OjYJJUVzhe#i+oft63r$6I4*D=Bl==f;P8&P%)Nbm_ncz*sPf5D{pjB~O% zB!k?DYT}ahC?UqSko5JqOkVB{!V38(IepMdW*^PBxQIbDnyj^+3DMA;7uR9aKESf} zY-5X*t|H5G+damgq=ba5U;bI_X}N%*E84fVY(lrxRlEqFxs&u$J*B6ZnZs}b-!a1<(aq$d$(iYe-;0j00R_o;GerNw9H{#Xe`Y`64rVq}|6#QQ%T{hNY{M)QA@(v)?nLJh9D> zv-|O$>f}>@TxMyg^V;}Sv+!!i76&4&Nqa%i-SYM7u@;9X*___W9_lT@=1{J{qI4PE zV;{r0G<8$HGuvOYIJ7&!<3Df}g#9ZEAu{Iq;nHT3{{y&egd((_<64Gsh_L_Wi_3ag zk&QgSYpD0E&oH4Vlfoe)k}`3Hv^TF&saQSh7o(h8uasoX&|t212&tV2t)Pj}NO_iSw+ZqPhf6H}XTMd9Hu; z;tns1<6bx4*tSyQu(ndDbVDa}PYt;KVfM1@0pO48-{n=A>l)hliGTA8XFu`PYGq{w z+{=j2BlLwK8#JEIS=-$Ffu~8;+5LHs8SC+_(!ujagR={r5l5XtV?hKL?Y|~<@<&KpLMqJ#MD7k=%iP*&R zPeRUWenZ_Nm>lX02I%qG?oR{vr1Exp-|5Jr7Ogp3vjB+!T5u(QT#^q^2{GkF0bI@3 zv`Z@fMm_;^<&Fg#ljbHRM>nluJ8I8_XjMApta#KrF)y8dz(s3=djmuJKdw7{tBP(vwgCH8Fha5 zyMKpli>6z9=$h>KcnJlKJuqc+o0k-mtPz?u^o>b|q7VImC3H#2=r=TTn{q^qH`O*X zOG-5XHEEPRcSaCU%DGRvOtwSu^G||Zxg}}NmK&|r=^tCm=pZ5QbL6Z0W`2uY##Gv@ z)uncK8?cluD~>;9+}PAtjKc|)lY}TIi!iHEI^QqCjjrfIsPwOIzplBgCSlbXWjQ%K zFu1`G9t{8Wpwnu@MgTWnlC_5k;*_lfuh|V1LQt(g$Q9#5B=^Pi9Tg-*%o+}v)q9M_ z%oeu$z;vHG_V>K4+ME~s@M25Ranf$W5|wB?R=5Ih3H(cvM)AxAccjT_Ki_u`c|gDPU> zrEF&8%w^lDJg3cZ5v(KGw{=;y<3Qb_D|d>_>vrLuIfO0bvlcu1b`BQ#mA~N5zh+k{ ztp8{}TV0!=hlS77K^|JU8RM;bSh$NhTYFR`u%93Y_HEI-Kim{+cjat5EdvKX);%oi zdE1Q-tuEYn+Ce&_z?*R((o6bTXR1iscK9VO}X=hL$0so#_9cpfX>A|(7MC!B?qw5&Pz$q0*r24VSYJoTfes-+;iJ@ zX#-Z8Kiuz}ZM@qtVBD3j+-CX7VV52I+IyI1!~eZz;;7P>9p2HnM(bT{x1jq(Y4lGF zZA&a{13#i8@iqmUV5K>_Ti2`ghQ>dH1GbAU8MNnwoyUaPn=&0bm)ec6Ijc?d^6D*alhw1Dd4&B4pdi zs9v*Ht8$T$LFIoAfRfCa0al0hs&2r*!tyMyt3#__qU-sOi}?mWc**<39EAnun$vNqtM1$qK&m$2kty4<*r^p)adhh!7qXT1C)z8+U^?E+z7ccNjMaS8<3=lsauT+N zU)vdIo==u3Zd}PJHs3LqA-7qzJ|!NzK=Do-)I91CU1?GX_54;B)2Z+ua(7B~qezC8 zj0P<6gaOiq*e2c7^hS$w{=$?cLSZ)&NZV}r$c@HJX*f}&Lnk8))7e3(6Dq$NrT9_A zA|*R%de#SxisM5J3Pb(C29u0KPvTTHel24_jYz7l`o<4MyP!|BdCKWMG;r$+i2~r3 zORVT1{&Hjqj+)<7Pqv|EnbQ19?Z*zLySZ(?^F-Z4LS8?}V=3$fULZ{DUiSiH?Pw5a zS|4hDC#X9;ZQC#?9*IM&%2L>^Gj6WuLne7W^If+DApdF-hp}Dw8mHJZYYxMcK?6~V ziL~mi6SGuE`rn8W_Lgr2xKV_o7E3Yhsm*$c(G{M-x6teGC3GKHZ*#d(2TuEIy#s1v zK+jk#*(O&@H1sQzW;H(QbbdUXX{qkk3S|*EwUk7e90|Dy`8Y(sgE$J2*OpZNu2mNn z=7i+FPmc-w?(OXJ^sI>ZUePaVQ$-rHny-ZVn&xd_Hiye+Wttq}$dDj(S(&s^=?jhNt$rq&Ag`Q67FFWk8lJxVY? zs6jI*Bjqnx;`)&V)?+6;rFLcEFHyu#6kc+I38L*3N#$`*6&rYdu-1=;t9#5{*|aCk z9T~{th^)7%8kfRihc_9`Bx>2XGL3uSvbe+d9BYx>C6V}(f@ZC)Ne>tcrf2m7FYe%V z2*(lEF-00WQzZ5Kzv@{+5oGxN?hZgOZNUKl0bk%4g~KjNIlhZDSuMWj#oUuZ@k8Zt zqge+W!#qDqI8v5Fh0e-~7@`Vlp$^RtDlNwDsyA)Sws3an&G zg@Sskpd42vt%lCj!uXPUlTHS7TB^-AYlW9>>`w2IPVDRPhqcIFTdc zMtF?@Y16&ZxPPJwo@1*t#L0r zJ&1V0$h|E*Qqp+$o#2?(ddZP7JHF1$#Sv3R8+9_3>^pOD{L_o8BnBH-NzBA{GHjRQ~!sQ6_nTHz_mP(^$UMSH}T7mGka@XUI8SLn2PaE zW>OxE-0E;~UQOyxSrS1FY?0i72oApJE3X@iMxY}X9RcrGiEP$FD$_>q<6@GgW&hbN z3PGAcc_?M7$`bR#Dh(%E?L~L0h@3+#rnJiqjPcf0j{i>ihO+P%Xb~x9KYrGX?5BY# z_sS$cR?YyT&}$~lg;|iibk$OHGVoJZqo?;|maW^Y}9qt@bawejD2&o-_M`12SVvx=x46CGLzk5&)5^#oa4S15$ZLKgGl z7^ogk0e!roRfj$Ye5c12QHSIvpFjCC zSMbFBko5h2Ky1n)C?vAM?pV>L|06Q6d6@?9QZVh;WDMY*C~Rl))n%QQk+D~4>7#@w zQO@H*`OvH8tGfqMcC~atPZ6ZSMTyCNlS-Y$RZCu;dZMc8h$rVC4Nvf96 z6PEeZ&;`esj(h|cU12uc^i!>)q^Phqvww=fI{;pRFsV8OJ2`R9nhlX#A2K2U*cpuE zg$9V%?H^;mBA8Ew0K1|Z_xS6e5u05;;G;`c+2nVWWnubW5y-IQp-A>zm|;R-ZXLZE z*g{gfB0DU?A^)A~tG82?WERw`+$mZQYb_02ee`Bn8>QTGB$0P*Y?yCGtsdE){5z1K zb5M{FsBTz#i!%1DzA13O*U9s9zk)#{8e!Jp9Mn_8%Gzd=gReZnTWoUPpM`1T!jKXg z{LLahhDa0+N@YE@10MsyzRwZ0afY9!AMIwCcBF7=HXC>?ENXXD{=N6Xi^K+4mZ~X& zW^gim+wKQ!8T{ypQ)BHkBYd_pu1;Ih7_9WpqPt)^<_Y`jk_{5J%Xk4lsOW(ODc#Pr z-mN(1Lx4LYoU_bP9piGRn~cZ7;nabSxFY<((u{vvB0LOaYWb+CoLH6;_`%m!r!wXE{{t^u1!Ks=WxZ-Qj`_orm(?O~RNx`U_?kt^|a zMakHS?Tch94QKk~Jul-&18o>`cFZw-ygo}=1PEwNZn~93F*6f}Y=%gqK=ap6utxkZ zm=iU$!F+-#{WIl>5G}lE8Xc5LJ^1KLzAXM-Is}rFP)jZv^g4nV^4WKe1YW=U8XDs& zd^N;J1L>Ztr@jJclgD9QZkJM}G$YgH?35a~UyGr&+}0+{`rdKghsJ)o@iS5~^8|iy zv7>#krk(8p^&q#}&=Zc`MIs>fG>MG3rn z9f{D;!$|p**k5AedzdY1S&#ltSEd*A5c0Km)rgd)7A`?H7kK^3qqm#t(A1nCUQSBZ zo-tM6X@rhPW#y35VpBjl|Mj{oJ!)!(ClU-c%8|Px?K) z5KCco4uj2sBQNUrPpELQvldmRzqsKbrY$819g_n#rIr0|m6cvwSV?sr?@!PzM~SS3D4yN6>UWoUg~fp3{EZx3u{|)?qba1J}Dz3dUJjs z{kesF`HF2tt?#Cq<7A225DpC-akn(t619ywQPq39)bu_vyIV?E6ViXGB7TQC*-^3^ z*tl=GY5kNe+pL)kqDY5lz-OE}VO~~+*q)%gaa6#fk; zIi)#@^0{M_VDco&i)NP3Ru8lMl@xs8;3;tD^*FjZ zbgAthI+;GX%Tg!g(S?i_XYIPEP@^3g83Ai5dqPP&I@UJN%*+7pjuYKeXOCVpgG!T< zusSz_zG5q>4z4St`l_2Q69$*M^K49B&dsf?Kdj?9>zYagf#>|+whg=XPo3!iTL?1# zMA()H4u=aB*cH!cI7e}HdhhaO%nrOCt0N;Lo0^;3MM4-KA76hE&!5Ks=E6gGb#>Ks z2Vu;FRN@G<6~d(b+Wr;j=eZst z3Dx1(++`gVp<4F*4)^G{A1{l*4>Cq{cm;)@zHYHE@_hi$3mUJ0dxyJlTPF;Lkqf1@nOyF>vDysdvFl_l##g&N&5R&&7L1{`700> zMM+=(;_VcUc^x7lF0RL3SxjtT0fs`_7H^hP#@v2bY~mEb;4uZdpQEwPFpStzmHw3{ zMX8tTZa@_A)hJOOWyW}zDj-1K;rfxp4C!|7S5?3UhO{%0{;jSEYZsBJx;whl9uYDR z`yPMM~CcM8|D#tfYH$#(8=M|lIToYMavpIjT+IZ_Gtgr zP6~%D-i`#4vE~%D6)%kjyCq_uc&z#IpVBCT#q>|;e-l*ANKdCA#ba(wr>{+Aa5>|9 z^3~%r)CYpWwmxj$4aM1|k1+&7*`JiBP1Kc?(MBqm^Kp+@Lu!pBm@T@wssw;TtIQ&) zxDLNzU`+%Ue_muI0Mb-OnoU_!+nXc6bY2gU2+D2%Ee%Zs*k*5baq*F34iFIJ-a25k z)m+%#{%YMPt?fv~?^_qNiw0F`TWKQR@vXipIH$)4m&cu`FEr}=t{z9zy_<_URO%*g z+K6&`r;OAJToiS64rfa8LGE}V~)R16#eJ0DV)Y)nsg=BBdEMr&Bl8}=2=>`(0E zrJ(y;wF|CdnUx<(als4|`%-fDX+t<0%ydh4VIjwmW^c2xIJG8lB7AS6`%Q+{GzVjC z;E5s{m|@4ZWLuin<&&QpY}5guN&L;Hr6b$}jorZx+c_HBjT+DXsg+6Lj$}_5bh>>t z)^V{KLD?hrz5NuKJX!RSXKRvv%7)9L!Rv~aFA;5e_Eg-UdD7x@O^Ofcc1^|+xzfXn z0~R!0;jlIE{7BSkzxvfb1`|c@_!-tUE8CLU9nK?OdaR>93g-yap9=a}Zc)U?84zgh zR`YF>C#23L;4fpZ`6I!(gn9mqq%G~i$Sm)!I$nzpU6AbxHgm~_k`eqev#M#W$32Gk zkgU$NcX5_fq z$aJ(cu7DArzn8snL&``burF$4CzXxSig3>JVuX2tM$39pV<0}}wfg$HPdphE~^hgfX z*o~IlDE;>E8F5x-?1f9k>Qsvv+UYg!!I(bt@aZ83y9K_AcdKdxUZ%Q8e~X+Nqz&3dGnZks0$P4 z`xhTuP%Wvf&tI46bTvu?qGSEjl>rH_{uQD8d z%;DVS2}eqAY$DGt zJT#>)H@l42lTK8Uta0y0CIRt(hSu_xaoVu4Nz&^Sxt4XEFh=z>i z9&|$pPulLyoelMi!QL=_I1$uZ&gi8Wr zV->J)Z~SSW?%4wsc%v zDNi>CUG8EDiOHir=EBw)59 znMCoyqLN=#aF@|Z8t&L?y7cQYs*ZfAn5xv#%LtjAuGcWQBWbDGSdc{Gfz>qx2Gb(K zW2|aRxx_a;8IiBuF$>|K8a-KAKDn$I7Kh24J1)GG>^Q6bm@*KMl-A>aR@{O;58osc zAXl|1Y4(X0rm8?v(Q@2RID0+U<%mjZYZ%SXE-19#V;->xwrG05kY;P55GsSOA<}eq zA#a+z-Xu&gy830rt2f7xv1DGrXy>nSP*>a7AKV*{uZm;Qno!|3Zbl3(ld*m*2}5yl zt6i@Cvl0(=hT|ke{MHaEEHO4a3YSm>E>@$+h58#{3#i>tdc&T4#5|EnGyq%BB`q+O z#g8ryI+zZ>A+IXYgss8bbG{CeK+ot%=wXd| zz3aT%d1%M(A@&Zp3-ccdPDa*o{n)&;MZx`0IstU4{v9uZ<<4YtaDwfnlz3a1?MPJf9$Ew_Fh=xF%o{t zc(EaruL~7MQeXDw;(RiWc_Usl$A3lindQ>G6R{S{Z|>%+^R;>>trZg+Fx}u&K;~~v zBKK|(`D;FG84$ega=rE`eu)a$li#|C>DDI-x;l7N1-q(%bNsCVt1m;v8$wtWJi=;n z9JQ17b&l#xOQxlCgzWLEyc4+)!OMYJmn`VA-UJ|TSSDi+h@+?Apb)y99fA6(!er+w zSAb?mq$hORsNVJt93v*o@jg}*wuvxoMdhads(T*=*q;k^@_>-GI)k9s+ z$#aOojWG7x{<9v;!!>zX$uC9$eHnU#QIDUolf+Zxg4dA7UiaiY+|jnaP`I71&~`j~ z{GG#9{FIUr9s?Q7f!v;Zkj^iUiU#toQN>Dg<}#P3nsf zD7^6m?n|5W%v;|h@&zbxSEci?F@q;Z;D`XqTOBPu9kdo$|JWn{#$4_O3FA9q9>}4r zjW2S6@*dIHT;A;vn?0^6B`S<3ktAm{aDTHZ;N>5FbNefC7GKa@>+-vjQI@00DCA6( zi&Yr;@`i)!;RR-CRY$ktZ4&*NE>374T`S{zyEh9jbx z{8Gc~v;+p|dr-}#QDT0PoKF!NonMnf=lVrswxTwh6#MRPLg(jJxOGEcy4R!Q!}n! z=Y3ansU7WBGYYHWwkU;cT7R9$q=hS23mPDzmx8vXY{`u&zU*4q{!1#GBm5_SYYpHB z#ubT|WTnMt6|3upu@vUiaCCD=%J$F6vtya{z?8!y=TRHKm{V>k{4NdaXDH+~uaKo^ zb=70nrd3d$zya96Rn}lT=xS`OCVM6XZ^A}Eg4Uo)&S)oBw2dn*k{fl3d*V)ep~mdM zk>{|$WvJ(uzassKyCrvgGVOv)8^8k$6$=Olc)eoNQAfZq`!g>1%f6VyX(>*?wWZ)9{&GSyGO#9TPKMvWos%0A=P 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

U869fYC{7rJ?HsaN<-=7n1{t{f>t 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 literal 0 HcmV?d00001 diff --git a/docs/installation/images/ec2_instance_type.png b/docs/installation/images/ec2_instance_type.png new file mode 100644 index 0000000000000000000000000000000000000000..5327a5e21eec8bddf5f0abc943fbd52218fe3ae0 GIT binary patch literal 145409 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

H(J!TWcP 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 literal 0 HcmV?d00001 diff --git a/docs/installation/images/ec2_launch_instance.png b/docs/installation/images/ec2_launch_instance.png new file mode 100644 index 0000000000000000000000000000000000000000..2463f3441df0f9c3f24f2bc80a21f677eafecac1 GIT binary patch literal 234843 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

x3VA0Ks5);V7Mi2t1_#w>ZH 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/installation/images/good_host.png b/docs/installation/images/good_host.png new file mode 100644 index 0000000000000000000000000000000000000000..2a6e7c47eb579839535a26e5e952eea242c39347 GIT binary patch literal 30853 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%0Dr2?*5My2Th;hoE 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 literal 0 HcmV?d00001 diff --git a/docs/installation/images/kitematic.png b/docs/installation/images/kitematic.png new file mode 100644 index 0000000000000000000000000000000000000000..5bb221ccf7d6849b941dad105dc6a887430bed16 GIT binary patch literal 14191 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 literal 0 HcmV?d00001 diff --git a/docs/installation/images/linux_docker_host.svg b/docs/installation/images/linux_docker_host.svg new file mode 100644 index 00000000..0ad7240b --- /dev/null +++ b/docs/installation/images/linux_docker_host.svg @@ -0,0 +1,1195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/installation/images/mac-page-finished.png b/docs/installation/images/mac-page-finished.png new file mode 100644 index 0000000000000000000000000000000000000000..7055a92fdfb07e90aa5e89c8b79e3409fed5d8d2 GIT binary patch literal 158534 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

vGlqRN(p5NLO;-) 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

YZ#mcRx9@aZ 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

DlCtinjS; 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 literal 0 HcmV?d00001 diff --git a/docs/installation/images/mac-page-two.png b/docs/installation/images/mac-page-two.png new file mode 100644 index 0000000000000000000000000000000000000000..bd08ca15221c61d647e01505a59d78761bc3efd0 GIT binary patch literal 140504 zcmY(pV}K?wv#8m&ZBE;^ZFAbTZ5z|J?Vh&nw{6?DvGaZBp548FlS)#lq>@VNiBy!A zfQP|>0RjSomy#4!1_A=H0RjSUfr9u~GO)US2LuF%ZYd(7C?z67r08UCW@%#z1SA=m zq5-L?GLDh^TNw(P4hfj5fJ4YDWt(TdUtAI-1Vx0Xd^JG~d?hFfDk`crqPz`55Ca{# zlVtTHC?cwX9>b=*OA6%lq5Et5MZPEF<7AS{{M-3@^2-e9Ppc_3SVlPrkk+S;VR0`e z3qNs!)H`baEd{7eAXjt_>1|8|6z~t_jXkjFXO|s)+|!KPj~W`+CLTMGB-qe+Hl7>C zkOWZQ(i(^%Jy3{z`ASw24bp)-5)CY|5Kx2(lYEL1Jd=E?kxZnVOEJd~KhU2F+Ehk# zATCEAGXfs;kpxshoAHy|S0wxJ)!3YoeZfumyW;BVH6(xvF}wUXvHW@Ouq;9RxKT-* zu_ZkU@yD$bSVpzyk2%+OtBGmU$nk7wY$iEe1lkGQMGJ+oTRoP@wIr+n7cHIo_!HLIUCNM+*L)PFubQsn$ifIJ15wFBwHW|vh&}R!Q1`b9qGzwUA ziMSDohc>PrC1cK5Tr6fBXK(=)8#s-U+fNYUFdBHi#5@dqilfMulRKk9qSW9v779{o zkHv2#zfAYl^jJkSs^ei>iqc#pAW9tYn1G>ciWBBH~=Y4psp&1_g*NhxI zvvM+zk@YypL@B+`ToIt2Gu91ode2oqX}BmwLfw3OIu1e8_)t%&p=jmEQk_iFe6VGV7$8alb=R<~$uideR%RJR*Hk0a8Q& zKmiZj>2olW%4Gz5vT@xl6jT$|UYEvCcxtKrUDUJJDf~V^ zDlw`?x_Uw|T$#PFR2AKI<;&w7T!Gp|r*{;FxmYNfDHkwqR6-QKm5cJl#GWp8ey- z#D0&%cIk|m2M|xx3Sf+<+`bLy>uPV_sGjh(8-tUndwpL zef{8bWO_#S$bJ6Qzp>_B;9iAzd~l<3xv*Dtho9Vm4m8-*zEz*0+OMmGbIiVS_l85P`VJ7Ks6Iy2xM3t zqCb`)S_1yvsOtV=L`8~9p18IceFQJ2SbQdWGsn4alg4A_=?BvYQ(=0G=G*Go-{!xB z`#$^92OS3#qYN><$@#f`G8WRm$VSMoAI2+xG_U=T+qVm8w)|JG@1vyc2qxrXo>$8NUX5Xwt!}JRymX4;?XLVxDt7f9;ypMg#mTZIH{=~Mo zanN5vq}kAQO@4p6D@Vb7_?r7A>%hLbbICpJ@>_eb?ra;iy{vn=pvA3jWSh;^BB#98 zf9KIR=fn4z1D{c#z9?5o-w6p14d3U^d;X>s0i4i^=hEY~4>_3hcienD1_u%R0}cf4 z0#0WZN@hnKIKH*Dn@ASjw_(Scz={(hfllXt(BMiYxo`g z`+(d0*Mfn;1__+_${*IZ#-5Fy507|-nWIsq_?wpoZ+-r8&-;?PTdFI|HI^&$=9!wj z*2j-0_n!3U!kd!aqg%l_{V0E1Pt(uyu^j?`eS#RmOK&myb;$#%VBicC8F!oF#@uZ~oY`{cpptowA%*9 zEbbE-EIZvS&es_%&U8RQZ;dfHY?U`^8oEywN#t@~+H~rjqX&kxDzeig$l`cO@MZh4 z(Lg(SX2}mF-n(S^=g<)>lCJ>f0X+8kmxowx*)A!9B6|IlZKIy9Ko>fUXbyj0)=5Wk zT?o{`74fcTHV_=(*Z^@s(pz_tK3@Ihh3CHZ6&k(QV|^9BpHJH;9)tQ^?w6`jjaAtd zJoW{3(~APjTdU~{L_$Ff#+AJjs(_Z^!ROz+NL)i#jw>eS<_vkSXG1^y&#T7Kl6e(^ zr!fp>I$IA-xif3{&m(F}YSIQwP}Do40Z7Q^#gRaVh%#LyWet+TIjrp%6mOx9KLW^b zXNzou_cQrtFLF;fj+KhO{>21@P{M$Z9n8I|QCX2tcs+Ri`;1qZVx=-|{mz)@3Rcou zd2!zYSFjf$pzyqwn^@?QyMBE2QgQ|3?(5KvFH2}xZ@_>W_s@gj@2+AkNmm8iHtRNsU3e0zFAG`tXMbG7S+BLG3y?Eox= zcCPnzyU>cB@6{+VG4WsY6m&hf5Kez#LFBtowL;`)H4D|k`Iw0#VGGI`!dvmzZx|Jl ze=iX%bDb=fw+r)PN0y64^JyK0Riutz<4`huemLU4;qu;!35JvEe2<-E%UyfSDxgSh zUYpxpBuVrC&y@Tbz+&3xZ%dwz9s07X`tF7^OhN4bt_O){PWb;3_~y#>ge#beV6nJ7 z`ABAk&?4=UELXB-E2s=5EjRHe97@mSi6ehU%_R?<_-{h6a6 z)U{r_y$BnY+t11-&SY~GH)BDizB~|xmT!Ko(Dd)<4^bdJrrF1AiLt&bAyC5vfQ7+( zK_HGaC$oeft>4HZ1`wM_Wg@em-=CxfRKyS?;Rq&C9U*9LyP$rWPC9Pyzt@THB8v5W zWNW{9XuqwFi<~Pz#-2PP*2tnR{CCBAA%S;3EhmejLXasiA+x-FpDMVr38tCI2;9R-mi1>`J)HH&-E0>MiE*a! zDfCOFR-lh*67yN@BG2B_76Df$GH0bB>06S?RfIW`?dm3)XGu zSJUKMNSw}x#Tz(l5x)w)_#1Z*I2%d15)4lWYJcQFiuWk}s-GUT_dCBw_L871Dd&be zm^?4`a=l{DCE;;MK`MTY1G-%xpFPthIuF+xXJ9_3s)S2;$@p1zz`$yKrU;`tM|)cn z)-c&{Ik`Pa@tUn$NLUC7Id|CQ3QVS}%v&u~D)TdGA%rAE{nIgr1kCJ6EK{J}bm1UQ z;7&|{7qDTkvl_^BcfGoOaFG+~5@Ew* zKQmMdq?1Gl*Ihj-W?H=Y4WG*$!S@^;C(;YI9_cL)GUuj5@*5nmYzXnsr+2*-{Q5|j zcYT*E4{c_6zGL}ek#hK)d_^`%tm#c|ceB#*h)ngT4`{jRwT-AB2VsFvbnEy=7nClZ zWL1uQGOL34l0a=>i6;stGpyy&Cl0572wD+_toQ|=3X&2hzF|Ll{aPqN5m$n$T8EO+ zS;Tqrh9R`QkkGVDUEC0$YovK`Z*MmxBPlYf5@r@7J}D^Y$srB=Oif+gX~3DZ$FU1# z`&?F?oEsu~Al#|B{FNS*#Aof?Lwt=p;a&LOY>*5D95GJ%)L&r-NICkHlHv@0li6f< zm3$<(zh!UGLN#mLc9TPyib+wnx|HpXV3yB{&u4t5Nb8q7s_(7n`-#~kVMt0@mcEAw z4BEp<3Z#7Z>i;0*8<*s#re(zCShjN(cw_b)rWkI$Pg!jIg)JNms(505PkuDxaI@P8 znd~xp_Jm21ZM+YyvJ@uKF8L+$7)@XC;?9ljP7R2z_d8&3y4eIzAQmcp_oq<8K!vNU zn)vHB#o*wSonVbSO4vU(e5dqjMyEVWUJ?rPlbR{IT0=?I7dFK+TVSmbqN!7Dj*p_E zoPb35PzJNz8)~1UL7Hz8F4AW(x%X`YnJ$1x4TYZ{lXu(?GIERhN8-*kpedkQ+U{-? z{N5?wdj6lAX~b)7n9V-aiN5!t;eC#}zh)#$%T9W6T48`Q0K&hs6i5(aVdgFCkUT&ZNiJNMtHFDS?=hYs#(kz};2ykB)w`y-oYNlK`>0k9BxB0_&M9gn&^Gq&<}q-#EV zYAzUEm!*r{3W>DTBWC!2;5fE(Wdm!Za{MF*p=f`uacOs&5a}{08psjBtCiPn4~qcf zWqXMQn8fVN=_1jsgx51!@$kX8aVk-m$7%j3^fVL0<|+b%HB%}`YqaW13bk5=Zme8L zx}H#xvjunm!Gs+&T@bnEJ2k%Ycc{Tka57?H(vBeXq#Yw78h(PsUmURwG$wMlDR zz5-nyo5goINO!Lad)w2!PA{mMcNVC0mmS0><(nRUJDZLpChMFI~;&<0;`23lAxC)bk)XL$kGU|zZX zEhtG~9~AlBF|2Ce${GY%u-G|%n7TdP7LV1~Q``LzMD9M}y8$o*6~Cp7Tkr zoZB|GCoJ@R&AwNOuytglrIXF%LfGbVv15}*WHh&(#AuN5@M1PUb$T+$Jsw^yus^Xs zrQ6@L6Y6@JxE-5k7bBN3{~fGB0a?Dp#g@2-rlWm{Y2fB^#Icw-ja3m9KxyMa<->QX z`2h?hM{XR2##kq(eLYC9~^a)C>$NAZk!kNO(H$ z$)-HbzbelSr5cP?+^Y2aimUfRYB!FAI_W9)mvQTkWc1;|7o;pkD^8;r^aFH2ju+h1ie@%kKkfN!{F&Nzd~byGh`{M#s(1 zA4ZL2*2$#+NnJ^4`s1`P0KfFM0=fFk#3xaxK8Cl`(|+*HNHz|=#Vj`i3i4qBCMfBO zf&M5;*a9mEpB?5_PkWopq9yNTK&pT@=E{Aw{nwN?g>k{(%j)Z1Uf3=wl+?efJh6fz z!h-KQ-SUNqPi+MG7DRj*FH_7S2JeBIe|%hDQT=3HE6MbBniJ_j=D$2+nZ@;zm?w4t zmo)HMM%pp2ulmSo9i}d(6fHlWsA)n}P>zDeGtW*(q}NA(vK?j{9Fd|{Wv4Xd*#+sc zwAl^q9IY4)u(A8_Jq3t(ixbcGlp|wQ^q70zIh7sjpZhTIf^PRyD5-lxwArn$e6;z7 zt9qM#Kufl?DZ$0}RyG;^yWz$Hew}2fs;ddR4RV`Et?WRIQ>6hTZ(Dq6klvP&5-Av& zj*Mh`QgVk&20|l z^>i9%d!b?cuHmetsx8lp$5}C|g~ho`yUHs+`I)UfoKJ~%aw#U>gW4L0Rh#N+x{5+^ zl}Nlu^T`;LBBMEt7)Y+tB+BolKbUc|M37F6*Qnc|Q z2}8h-NwG16tJcLc~I6xp>1 z;Cw;q^?A<`RB8Cdufe9}|NMz|e2>IW`0LEV|MuwY!q+G;&Hwd_><>-YBVlY`UO*r6 zRQvo5f`~;`9fHWn9mm|tzW`nGA&sTv?coFDo3|7!lEngR?rHdK7@oyfU-P}4^N6MV z{O--|X9Jy7wEmt&NN8d(aG@U_!l?0B7ecKhBry%slSakxT}K^yAGer?12I*T-uRsy zj?7?Ok{oUpVo{kixFqqPkqnDjL$FF%UFIa8^1$lL90pB+UBz&2vq&$%F4b z$C;p(-;G>qaqtlMisi!ct-~^|5ro1IfD%@{#D9VI$cKCJRf#kzW3~C)J`|y23B2kK>_g26E&6C^;;r{!oh?tZ+h%$>ETf9f?_Mp+_%&n0%aZVyTU9+gmr zGTGtIo0w7m>u!}TYXLoF%ro1q4p2}?M4^jgN6oI=-?G_^1WZisMa~xR{& z4-8#UH7G{6TdmHw1(g@_F6Ybe0^jve3*-vIMhAQ8Y_7~s($%N)^lH%7%`bx%3I9GE z;L(7e+5A_M>h~rM-7uYhs;Bk=LoSWH1t#bMO7_=Z6OC}N*pbbTv7Dvl%%#?1D-@to zLaw%BRAvM|hGbcc)u)oaF5=lMVNKe`-6Z?v1uNgD;#1r@Xd$f^|m7*$ie3cMx8rU#? zInjT}YZmWyYKL3&qsK++r3J3~WVjHbW=qzSxI9bk) zkd1x^Gl+Jg$`|f;A)_25{`EQ|Q59nqdAZI}8ZK!?pvaAy?#{{w1@9R6v>9Txi($38 zNiLmKEmLY;<8VmzvyDru=hUio0b5XyXK<8X->Z4^1kY@_imIyZ0x=X~cd=N(84hjN z{CiJfm9cJc*1&c;cfkwC`9ZrhbAT(h_faB1^_zL!ABhst@6!w-l};}(*Bo&;B@JPB znFbMqRzD2J>g(h2EHwQVDceVz!Dg!gn#S)PyUewSJZ8PJl#a*fwf^@kC5a}T>oQ-Q zWLwP!1U5%>DF|WaT@vydI z&1Q2r%CIYGNJYespwtvOK5hW!dN;w;Puu6YF*R9DP8q=i(S|27l?`Q6X+(7ZAeG@F z;Bl{SZ&f0d@Iql|(p9rs+Im*rU7;C`cjm07^T6kX-SQwL!p49)042c^fTxpak(3Cu{hw-j2 z4gXRGU@DeboPi#65lJc3W|`U8pcq!>mq&nrPqDP^5$6GF$14^u!L^bjgVVWL6>Q?v za90ZJ>yglH+%Fbe92_-l*h2r@jzD91osn(gDGeIZh6`I@V=(+=fDpOIs?SZw%Jg7V z;fA8g(@?`O?gh7|73mtJH1JvP6wr|j4mS;WkF}+}toH_T0%Sv1T|d$k~#iRiY2GL zfqeSSJM9p91fP;vqrJHJlY-pEh76^9&=FwTWB^YI010o82-$T*cbbKp{#Raj1h%2dFfd|w`3Un>*=11rRp2JLQsYzR9eDa&h#^zP1s zI6a}vHokIyS%Utm@m_;m2FBiA7@#V&S0Wc~cj~BZZ^t;+ml}J)=>3eRiC^$KI()*2 ziT@E{!%j&<+xKzP9Ul>*3L_4hke=RmG_t1I9$E_LcP>u%@c|Jp9)%!=KE57TrRFkyDP;_kit9pd?Y!3XZ;DQXP<6JvXyGCDX3zNFg}Zv1s(l^W z_4K)hqv(kw%jju+E-(vg-mQk<@eooL89}xeegl}V0DF-!8KvjNEE_Il3% z4E`QaQ!r;|kbCJ~<$=V=+9B|cU>L^_b41$f?o=OfzGLQN#>UqR^Dda^o21sE)(OI9 zS&Mrz`LESAYl|VB)oQMq{5N(6& zU6P@c0!G)iZMpv@)k#qOQB)GOlkH9lFwy!QrHmb zf1cSHh<4eUh=MV3#86vxtSk2%9f7|tTo;a$8eRSc(WY0Dg{{06>?2q3IOS@ zvzBzPno9!c#7+>d7Q=#a&e|6xn*~+N@pq;(0$qQ*RcsYiZlM+W$#|PoN5w>m!Pt)X ze3>D%+>`>1F*%G6)crNJXIHk!TIq7N?qSnV65&b%%%S7Gz%W@q+i+G;B^_v3m6KK1 zBn|$ug~0T0Mf&YtQswBl)IcanlQ!z}3pr2o7f19!tNzSDTz7_a(F^I-QOkXK+A`g5 z=Xh!C(mTx*X$ds(ZAS|3)~LZn1pzTHyT3St))#MIGx4Pbe|ZjB402^+>-S^?*apJv zV~j_dU&XV>H5vlj<1-&tB+|~LOY^%5ee$xAHGlkEj|iO4$p zwDPZ*c-Y1jbYmeYAh{aK562CPjp}+sN71){-MFj%)*-8>II4SaC*VL)P8nCwnK7G#uB=!QRM->1yQHE2y?|IxAlz!s43_ zi1w*a^BPD>?OsmK&Me%O*vL0MQ>CyA3bqZjJ{ z2Eh2KuTf~-iLvR*4UVhfJ4!YRL$3irZ2aLhssBo<9R^51*=E3%)Kw0zR5?$%8$1bt zJ*&%Ft{tG+?530{Oi5AGiHjW_*3b*~GtV6}7SHB@3y?baYtoOGoQ6h1S%^%3F|6CTcng3nAY?$@f%wte*&q*X#Br%$ zK+UD^#5)-Ek8w2C6Dm4&Ffl*ckNF05gt0vcYXAMzP!N2#mgaoB|18JKTT#$bgA>fq zmOY?B%j{;F$#^hCf*8<%lLkfjLPF+(|j-hsJmT$ngB#Z!W?k9 zwR2kc5v{+t_7!sJ8@#RSwpkA8T3`@{KQQ1#Y3O-_s_^GU?s4e!GbPIL8El-lsOtaA zuRsN^*WTd!z!@ETM6%4Zqp<4;^50OJ{hH?}!!^8v_cGH~o=h{Fttj$*NZ}YM=WZ&w zw0A8wUo@U&W=NoRN*q0n7XRvMvDs`>(w#7%l7AC7lS&|j1aY@R5XSV2F2DMXMACvm z66<6@zxP|$vX0-uwWyvPEHomSgEPT8PsLF<{@FS=%7);P3xBS*8LB0fIq?}AO6V^Q zvgu-V`@TFGU526+c=fx5G#g3NxFfz3c~*?MQ&=)1$w0Nxk|C$Ic{Z z=tb(R22Nm6P=H0a^=k}677uu5k1r;Z5CH)MensxZycPeB65VXEvwaia87*rOUE0s& z0Fd2cV6FE85(aAcNGwT0k{CSPiyXV{1gVE)ZFP;10yZ@H&R(n%V5oo<|FeP` zTXAh1+fgHAT!4MIvIA{lc|h^xH^IaFKp%z zhlr7|O0#ogrke{k_L^iq#MB7cY`6CU)zC;mg!uz{N-`-eGfa(k%dZ=wEE!x(OMx`~ zm-|!nw|ij|Rnp~@A*T6Qmt}peD`GtzdeiQE8&mV^gE`418*5nlw+RSHe-#%&Qin$* zMgjA{Jk3s&9Z|h*ad<7go{@9L~ z243f>8<4kTYyE7LO)L!V)TfJ;s?%T?o5YNq-$P6?1o2l$xX?Y0+8*19SpXyqEvv(P zGgSk|J#*E63?Po>cD&XCLJ@U1JAtR{Nht^}n*s3@5h`#Q5&}Uu(z1`sFiXHgF33#7 zCk1H1YIP(-Y-(PD!UW!IhJy)k9qc2brY402*taHFvAwp1K(pH!h)wPjM$jq;tsGC` z(fHR~<-+)9v3*Qe&TX$olA8Y&>-vd_UHNx+c=JU6{5C(it7NuCqN>21F5rP7zUPpo zX<>WNa`;n_)eH~vb>P>tnXt%vwNI?z6Bqqil(J$E8MQV=!kNHE5!OgD=8WV4fP2)zFO>G>GOc6o-qhia#=NT*W0{F3{u$ylZDRj<+`Fi z@j{5;ecTK*4{;+}o#J-G;I1`GrO=09(il0h?X8P6$C`XBS%u1~k_%FWU00xES6!M4h-<>|R#IcfjpsM6dOl9FDuFTh?AIG@BBdDnt z0cVaNldW%OfG@s!`{nAv;&nt7{AZ0LH?H6>9 z=Njux<-g8U{G4YQ+_7AIzx|$Mh=WV_KAtSII7? zeehJ%WPzj?n0v?xBK(ND`>c!fuB`uqx%`LQ+4Ybha;Syt zQolk>h6br4TPQe;3I@WatXl*GD{XU(elUPGS&Db3!Gd}U)Qe~ zF6d6_0IWsX_nx-)(=3rq|HESZ9~>wj`PCO{5xGNh0I3BbeBCewJ+MZJ)Q?)4=fB(S zf58--K#1so_f(7EBYBq6|K)G~2Ph&MowO?Zzl&!7AkTvQ7U|*1HyZ%x7=s{$4ue>YyNLEjMYb$|F^sUjGiAznh)Lr;TO#6{r_6yKgu=rmFE4AcK<65 zj|WUw&vuJjy!L+^51joA?!PF~|B74eLRkdX1nv+`<^KPb2ks2}4|yfrXVpkLW*5A1 zpfuH@Nqg&|Q(jxkWrP)y${8R3jEu@l2@D$)6uj%mwO8#($a+$M;t#tMls9N6=BmcV zi$N0o_KiP1)N1EB(9^-VERPzl_XeX;YG*N`1c)J z>K9C+N2$|g^&dP#*P+#Kstu=q&Uw&kHKN+CH;a@9x8XOHzN40W&u`+tdQ8CmYO;(U zGx!iX^We2pRaaMnX&A72g1LKA;rFP98s*|qH=KXGNtiDH6&6F`iD*>$qt|UgZ}wA< zjn7j;-7nVw@ZD%JA}*c!A{f8oA)Z$c_GBqDnC=qO>@s0WrPT{c5%`I6YDJfln%dj# zfX#|ggFP`A=^{iJ7Fz4Xfayn+}k&ZC=ddf>mTjd9Mq6A@HTza=rGFna?tbB6>*7Dre9=YU{W+P zJ{r_Yy|W|eci6#d&ynkWtaUj?Um&RW5%bE#wk}Lu#qAGSOXx*)Tr(pIbrZY86u-MW zJV1B>dl{d!bSyY>Z0fPEw67~;P-T=t;KU8u)}*}oR79f!Z`1XM->2%-AfFvBD(4PrkqQ{ZB`VXmS9M~5qzGmDXU8_Rskc_lztfv-1anlLU1JE`&?*dQ1 zAOH8W{S2wuWI+H?y6SM_t6*qP-%R45q++(*DX8SP4EYQ#56-wxX}c_d`)r zIdb=pq9z~P*u3d#myL;uSwYi8=Pz|yZs%3P}fdVrfN zo~Mh3hQUmWEtG;qU}81g;R+|#nYuPTazn{lR@&rnL#(CFb?SYWLavqU*Qd;wu)75< z(^J7|{_L5XI!W{kH4Q~PuND=Q=LKu3H|&EGnm~$<6TGrapggOcC$pIk6TWVe(c)K) z+p?mwTC}AWI$M=ZmbQ9zdEa(ymZGv=K`yLiJng~ytId&#ISC6*8Zk{8HaILIAu#<# zVrUQ^!*&W-Av+yZK>;$<^MNQHUo$VPZ6mA^UqQkBzM)f>{j&k&fyiPb^{WFPE@&-w z+#77~Xjk2d;oghD{?*CNz^B4?pNKJk{_RCyiu+D5O9wd-^IuZCPCFzV$Z(}L_PHgY zf49=Ef|#ItuK>E8?%H2LL2^KjHHN|Uye`(;sWIovVk#vAgNUaVHp%a%6R;=dFx~BV z99-B`d0$A4op+)rt4ryJ;0fNCu1BMo??VZPB}5fHFSxJw$KkcFY4IKYZl=q<)-wuC^r}eLcR~8H!{Mn_X5PP1cjdXh5_15)gKUN?ci=**30nzpY zQCDwh7Qu=My^!vkK6Gy>A2NP3{E&Hmq?s#zFz$P)GV~*SFI%y| zU)^0VgboB4)$4&xODs%>^SJd@g=j@~ACLrC55#ze39GGM$hDn5NbiZYn=9XGdhs{Q z*FubzX=ZlAwk!Gt^Qzq^+0hlS>Ym_sFvE5l$ho+VsMGt-zc}H$zU8X{bY*`uer|0I zKZ2Q*rT86eQj>ks| z+1gLn?18H3J(*_Z|8vn`KrQYZ#Di0GOhbeRSfs@02OD2=Fq#xy&E&@Z-o0n-U*qe{ z*S?JQsg2s3F3XFb`|$;u!VN9V&P=PpZHWYtfR@5I7k;k&9lynOG`8~>1kr)39x%I? z&g{wz-tL!v|33I?bA|!ee(a{>IgK4*y^Qg~|H;`wF}nMec0I^);}`f~--Af#|Hw~& zni4hVz7>R6R(C9l6Vg@9m(K4CQaIC&kr(qr$jweYd^$=HS~e``^LQqKY)c9;P5~G0R{4Q`iH;U@uc%vHQf39UST`M?UI_z;9+Ujth;=bEh zk;M2J#BzH_^I|wX;uGWZ>i2lLiih8k7c6npjqKR3hV}9&>C;>bq!9Z?NbdKF(J`F7 zo5CR=Sdql81(}Rz&4n_R)yN9^vgt&3ObZw!9lG3F(HB%c=HB#qVziND0AEVg>~P^P ztr=5#>SBKfHlhE4x#{{suSU!LjGM_{FdmI4+v#sj%99vPnjZRmdvIJi;!_lhEeY2a z8gl7t#>|U=V({P@yjhJ+m?;@fTTOaJ1G=48cK0y#PMmKsl&`?sP6Uen=#oGGK|njS zA9(TR^SF+6A3uPy`7k5l&$;JoHY3mW3DBZtSH-aF|M>9+wePwk-}ZTs>*9CAGoxDr zLIn43L?vE6-DxPuhcP^LwxZyq<{)M%L+Y-@3wzmlXUH6G?E@U_44axa9hrrZ?qnMu z3;j1r9hAtoL*)N-;y!VV3xGSclcu{z4HUerA4=QNpZQCN)+IwC+tX=OFr=?~%!_vl=o@cvjVGUyV0 z$H?*>=JTZ;a<0}AEK~Jrq&TC9as$sR(Iv_6LIv{RNa=-yeKAT@0+|2%UcC6|xQOaT z7d?yCCrv#s()wF2&}kQ4(k3YcRoiX2>1C9d@*PZvu^5pW=HB6%&Ao8Nwu1BGP=0K? z6W!2py#9E>;D?ceJtAkJRzJ<)x7rb0D@r0w|o#o?v5mfRPUyU8M?wiuM+hWjERCvoIJUqo(4;KmEN_2)qu zCfLp<;|V{=<&UT=`fC6vscTDtU|SU zBexM?^)k!NIgU!RK3w_Poqm5N%HOz1i^B7FNoJ!t$#)lNkS`D+GQunnN44=Q(pDvf zD*%8Rqh3=K`Bji7vM2PP=kx9z3 zF{KU%WE$diw4U$u@DKT95pjzyG?Ki$MT3k|TrMRI-OzI;QMQx1!fFnBd9iL1-7kzS z1wP=nlC@2_iSboi5hr2q>+dQd@VjQG0h{$Erh#_3y28T5x50KeY=i%Ysdo(0qzSZj zr`^-GZQGo-ZQHhO+t#!-t+#F4wr%V7ckbTj-uhjUl~IvVk-46=R>c|LpzbWs>~zUc z(|vPlZxpDHj;3Hz>~L6or>vB)4@G+-Xz=?)NP#~MW%xkseaf{CjFJj+UiFa%wwkNE zI|mQqMX#h|ct6}3Sw>u?CVcmPsAQS9yM>Q#4aB7Y3o6jrObI61A4%W>9<*a4?;Key zG9ss9?TxR;f}%P?m^ziROto`sl4ZF1TH~?bvF%4v2lUjmXNNl+nSWgjE#KZhCwOAardw@H}6@uE#1A0#zLR z*cq{q*{4YLZNLj zV)L30s4g>cIFa$zz-^MSUhNINK*`Ii@Qw>vNvi~x;nUiO`{&goJG0nak!`}+;Fb5m(5oXyH23jy{0Y(^D~ar9@viTE^h`;#H(Vw zFSVz0`GDhfdXQzfJuJ_Kr%z_SOKTNq|_2 znD_{%yJ=vzP7t*qj$om~D{Gp5e_-wHnBOoF%c*nrJadX!lLveJ3U2QT;0AfaaeJQQ zHZsBex%2sUQ7x~??HJT0S4-R+L$H;ni=8$MTLK{9|2qDUeRq@`f4Cxt6LRlf-}N60 z4h@@gm-rz>9o?{ncvszsaI%_-p_(tFV0ef%Njc4!2%n2h!K^mcwCmpA_T1q;lW_*0 zxiF3fB_kf6Ye*XHmbffD;dS-xp;-Shm?I3ET{z8FwL8PT#b_cxbxIGwI6}1r(Q7sio&@HF!RX90V_)6MRn*-S0JSinBEtL2DpZj!F zf{JoTEO}mpQk#n?-?UB^iw%|}rHYEY69&Wa^6oGI!A9NjIM*CoDF{8!69xO}NWpv~ z>^RpKynV9(W22q<76Dd4Cl-W+J!pf_r*~Fjy#mjNUYL5_w1d@!YpV0Zl7tniK%mon zsLZIOCnc*n1)GD&L3LX9WQ7VS%2cM8oxfDUOF6piMYZuTst3Ea)Bl>iWFDLzA3xR? zXS(Bst%7#rK>;59=)E*pb{jUqy*_E9!3AQjm1Og(xM4P$9(p>)6Knlnq#iW{%<&lrvb_}$)*q%Uh%Lo9r$?F+EKkHW$+ri7y|Hu+jB9a1eZ76FS~YM%&nz_ z*EUyL=|MU}H~bmpHsQ5YFx%ib@H@F=2S%)>hNBvZo#uT@J(9$g=j=OO;_mI|UrYB+ zwQ766b7cff9N-XAA{kp?j#|xC%rocE7CEnX450wmli5-2qH(>MSz%7cn;zy8US-aB zV2)cZ8#12#2v9VRj=bodiVEiZ*1;-Ah57PD)^scn(|u`PjJI_P-#(8U362%3zj_%NQ)!1WSBG?MgsQqXh4 z*}u4fqxgIgS+z4`Y_h|vRPJx?Rz?9li@l`EP+5GT#$g5Dedy%aKt#w~`WM*KJ5TLj zQD2>2gj6FTRf6zepDWlL&%tObU!51S#$FntFKRYB`0Pz4NPODlHV5Sx&UC`$#e02( zKJ{;(s2E(u?CjV#7pz_s=k*nbqp^*7=NoTM8}6f=<_QQQTCE5q4x@f!UUK^z1t2#M z=`HsClf?FjZm(p)S_%EQ1<$(LPodVCA-{;zEsP#_4WD<7Bnfa8KxTlw;lb zb)q~I9B4(?t+9ePN*M1i;vHXW#DM*PwAtq>Fvv&c!h9waxmaIh@+L{)6PXsnr5kRB zVmo>gh4=J&a(FXcZ|JF$1)Pj1%aLvgePWKP*uL~r)2yC5gg7jqU@W$V1`be&)Z2O* z*9>T~?x3#*vN2Eo5Qlu3;VJj0Pjvgwo}|Sznu$Xf_5Y5CPmt=4PEIlP&nr$voaP9S z;dNwdO^uB#*}iY5UnO<-Q{2<^jS{)tsWYM`i8Knc0I$z!exvs;=NVO8nZ)(I#-w(_ z=wWM)#e?fV4oA9Gy4}B1vQN0(xDit$ag@Q>MbCpdu!hr&aNVFyZ=xIZ>4-~QFCx)y zd9tFgh&ApuT79VRA}L5*&Vm7D_PML6VT)EvchYp^MEjY$1GYMd#=FLHA9K$a!X@0m z)mp42@p}RWM|{Te?J1dSSZuO@MQy>2f4m^oYb8WH=}`?|xnra0n)R31=31#UNY(pA zb#BLS%0x)jN_!@3rG&neY)bt}8_m3iM1AgOew4l%ouPtC<$LWurM4)-MHI>RPQnW$ zwyVu4$UZ1-&X&W>=aF@1$BPsPa6>P}swWE-zg21ULIB0F;ueO3Y!D5OEwg{N0uPpOXIp)kv=aD2aZtHIBx2>($ zb^I6LY+64q|MG-p1xfab!@Fqy}IL*&jl#`nXWvbe;ilAFo1tog=4zBU@C~(e(Uue#!u#mUnLur{iMCyU*=%#$ zW%>(NBEq>+^I|tis!r!}$GJLaHqlSIlUxki6l|+c z5!XkS{XP@ST6qe0?d8Z7WI_GMW9F@;yjBLsRN|8;9V=NWXoE^yUk;mu+tB!4fSZ_% zo;{$hx0iyv#@9~DiK1pcW}N0^QT4xF0F+5h3)EOFaW0PSs4=gUT)jJ^*_Ky@X;qAq3 zP&3jqGa$nZ^3A@G{av9PYmtM63Bj+&M^Q;KhcFR}T*1NG7OL)ie!R6J$6b}*8QeN- zAMPu3c&XkYmu<@OPS1mPiLYajB7ebt)~QkP_*;HF@F4AZ9daYqK}wvnk{$6!OXv$E zhX*1VebI-d)Wad(OycEv4gqM|ehxl)$^IK+a-`7bzf%sze-zEg8wAVn5dwK7c)Po8 zW(k3Dndkpvx*dH;cfDu0RH0G}P+$ZG>$R^jY((J9V*3AEN-*;S+JLo!x_DvJIB6SM zH@u2C{|B@M?8E<{H(bBx2RW+*4f7=2B3*96{<$MLBO$fo|D}!r`*{D|^xX0?f<^IU z9g4ip>aaI{i+q<<#5X`nMpl9RL^`D{N_xis|Kof(h`?_+&BTG%*GlTajV;^0*!`if zLFP-QlbIpK#UwQQrx*40^&iEGN~DdfBnH)lw|CDTiq2C1J84(%?^#)BYd@C4VM1Ob zwUcR{?u(~&J2l|-^V;q!{v;XD=CU%1?^f$!EMUZix%v4N0?J`)^Z$!V1NcFJ5El-- zrbn~Fv5IB0LV3&oqr&DLEjtnHT z@gwYpCMTmj@+!PO?TjQ4t9+z08YSnG*&&?usN3=|1oNcqVtgU;eT}eZ8Lgn`H2n|6 z{0KpaZ#_1j2+`Dkhvo(KBdaW=gocG3OeB?x&(5y8Sj@a_k6LfG>5nHA`p|4n$9Sbd zmU}!l4GO)|o}&6`iy$N>7NX5CEjBAG4%{w%%b7Z?IuM(Oeh~@Ume_GasidJ7aFzAl zOynuymx!yxTGd$ns2I4$cwaG#y1Q2xl}o^d zm+j*AVW^4?V`B?u`+={78zJB$LFN-g#KeoHKW|=`0hyb%UTwY2rShL1GNH|IEUq7> zjOy$bD~9W53Wo2x_{aP9_YiH%hWPG1llRx;-L?zFT{G=)f03T(?RDC#uTv*=_Ztd2 zd>4YAZ}+NRGmRT`D11Bmdc*jq1J{3u7zY!$khHv}hL0d2?>KgN#|dJ8e>IHV_d@p_ zWGm}|HQ9am8adAM%(rU8ML*!iAC(IYI1I1eEwohiKccU9lXnRyu$H&n zskul*u|(vRbjpX4+IXk9T>dF_WPamYtJ<)$;MA+~;oIrK)`IDZEGSWXsIW`zB z_+pb}jcwmC^(J&IroPf(&SfC{M%P^*GSCVphvE#et#0?QHQ5oTgy+d*Q)NW*lRCtV zCcolr%Qx|24^pdJy~u?Vx-3sx(>uJ8#$G0`aILzAjpDlfnY0kbB*z&J^4#{O&O>`t zRQ=QPe4wCx*FeJ-gASfP89g61D<%WMo;3;V&gB5#{Brs0hu?-7(Dn9r*zTu6t&pzj z3P&*gIq>{Jgq=zW4WDK_uLH;{374XEI3Y;bpnwp z?v^aFcKCV_z_0S3r^hgV!&v{vix4bz);y9c;}IyJIf2h=dL zRD%+QPA7jnOsmO(KHbR(q#&a-LoyKYtm}>yBioMhqe*jJwY|_tXK_S~=h#Z;F@Ptb zmF(^+(C?R+;fbt`QDSg=aB_F#^kssq`)UMe*>=GzY=ZCorKbDiF@+Y5b&pwtaSJ)r zc?&*ejU7Cy)MdOW8|`!Fdm!dISZOq@ufARLVtS;VtlTP}(~axK`$g2I%?E7P_2|dZ zE<};*ek6iYzI>~x%zK3;Ow!yhG`$g(GOycPW#Wa1v$u(3STduX(24nt`^;I>dj4C* zZ8{Zgi8lz8W~JU0wM$PaP%O^}j2-qQ7+%*0`VwL@a9I~KrtCrQ=JiAGml~L^=bivZ zR!;D@*S*Y!&uRw7{YlbFvWDh(Fq{SG*Ae7IE+g3N=6m6EdFlQFoD8GOn|6q}$rKyU z+ewi)&nHu~$0Mq?n=E<9!-&rf%XL5&=U-|Tt{4=`UrK3Oknu)KRI z*UJyiq&6EQ`>|ZWE$i}v5LQq3y~}L|K&tLW+D4%3dg8URQ0W5fdD*Pj;jG?MDQKs9 z_HY;7&w{*W3UrX2PZ(hICSQ>1RuftsDDRlZ5fqanYP&~)1^eP#pUf@_bTpTbcEK0s zAeGfr^FJek7(V`Cy6<+&6^v`S^~=iStzQb-@2k2;xE!M%==TP)t_WlI=TDVabkLF@ zE$g~)x4E782WNJ@Z1>@4vo-SXy{h`{mE%(2+`G1M6!WG#Hx5u-B-pf5^%h0X%~%Uu=c5$XP|VT45lS zgGzPoOFZBJplOjZ3#xRe1lf;@yoOOM{T#~*TAXm24CnVa#m}66i|V@706*|Qe?sto z8G-t4s)19p{xQ>WMy2C^XGw8&Z9me!S|7BRbE*DbAVNr3LEPv2_=WHN!bj%!K)2<| z3GYBV-2+|ACzx#m`7eBo2J)GQBhnWWuk}_BaI|%7uTc90c>D)3TRuGqIQUSSt+oNx z_5SwxxX(i`QgQBAY*?IXYr0(e33dq}zKs9p*17@cH?a;X)po>Q(HbdNI5N%6|<=4(jX+%X>rGEK|vA zDuQ6A35UU7M-OS!txh|5XkKW4c^n6&yA85pu(}e}R@XTJQ%Jw3NRdqwP}I}D?TxEjb9lVbau7_c zV*Ec_&yO}|wB|$b0xu14zEa%UemrQaTQ2WTbKEJ({Lflqte@f7d-v1@3Ziw;WU(?l z0VUdRHLYqt_yG~`3^xvRlRyj-=OWr<ZxCA;p2P4zF!DMIoZ*~36(%~{K~_6VmFV$z5YH*>IF!b*)%&BT9Y+%`OxbzYWL zK7e?4Uf0=^Q3xvaJWE-ZxwA82r9GTRwI?HP|z7NnIbny$O^0 zl$s$LQL5C&suMZ>eqZN&2Tp$RDTSIc9$@S&5d=Ffwovx8T-njp(4_{aL(Zai?-Nd? z#ON{Q8gaX#g>BoWY>(Osr^V6=5SDyZP!nhz7r;H7R}@$=W~YS1zFptw-quDDlBNc&SUWY*6GN{{ccvw z{u-IyaAX--A0AdnI!Uzd#)g6ERe8hH2A{`g7!_k2uve%E^H)a78t|%YT-sF@)*{;jQEm86yI$`n_4~E=R*v> zAfP`o6UAU~a4z1m9r|tyKq3aL%!iOUlMME5|SP02=o0+!BXlo=r}u;@mAiUyy!9eQ;)p;UU-ff zMOE*lhK7`sWSK@9BmA~#^11>rA+O+NE1ugT@mYoo9%2)#98g!DBeU*b zy0q5}1PrW#DHI5ll^-BtdkmWArkA;5r=S=cso7$3l{5<~Ea_fsBmrp&<0vL!8@wOo zn9nxTHd*rbC8kRUOpTc!K<9QmvSr_&XaSn#$OlJrZO#)C)FR7$aV^hFft z?#8mKm;*q%mKT*#{?&Lln&%X{h8~$-^jMHC04jh0Nh@3964guT=E-EyiX=yoxVMiI zv(d2f_GF;VdK zEe^T9vFFL_DwJ;iN92v~alOcJ-J67{9bV^8+YOC^Fgm~Ucxh_nB@>r+!unEw(Ssqf#hw@MXq5)rr90Nn1Pan{^h1Y82TI@=ulNPuD0^!MTB z&WNhiJ5aP-OZpK}hmnt?Ap_0`4s}!9RQ!kBg?Y!ekIh z_g+&jz8@9LYb=fh8i1_8XoziVry_vmH|gqFA1~l*ZJhC@BLIWNJpN8>;~wFNUdQI` z*W|U8``6UB*)>&95m(z2w%c#ITUR@lA!{^k@vwAI)1i{Zyx_ujtf z&f7LhFk>|9*7IvZw*$OZ(2zfoAG$?-YXvcgQGZ9iYP%l)mgs8bSlU7%o!gfxH;lx0 zN>Rn+QwGKTbBE=O>V6}D(Le^!6M8*{!y?p((b^sUh%#6}F?NAs(eA}xB~ z^g8Z-rpv~u@oEP3+!ZfX5EyMC1Yrmzhs~5C(<%0n*`3Cg?~#rI8|f22-(_*me9nO9 zj#pN=t2MwgMY3qK&LsL5G|Ag$e+#Qrk#NKwg|4=@WPxbsQF^y8I?{ZD+Zou>orhcn zM)S0koQlP_0u%!{b746+YP9B8asp{sjsTi!%aW*z80Je_P&&}z{nY84jmD|Ojpr@Y zpk>ogIAT8|z|Zbv@WoI+?<&3JIIVl2v4nJRf6z#dgv@VCh4^C9$N72@UibGZ!&4T7}`G(ocS<9yf072a?ks}?^N9vRaB z1iCD4&qD_DE!D}U73p5>`DpipbTaI?v`kMDL`$9dnlKKJ8i+Sd(v05(6iT@|^kNs|U~<7`o#%I0 zR^P!4`^nm0Qn;-f!E-w;!i9Sr&6ZPtGXZ?t_!=mrVJlH5E!udHA)0IdBzj<8tT(3um7`vrnT)9ft2(mSYPb0tY%iH~uFV+# z@<@=4gwg?7yA16M_d1jOX> zrm6fq@XrIzKDr}-4WljYb*8^x7Fa=p+u#9q@f19!{-SN4?xjsx(Tlu zK1J3TllJbf7;N~wp`ks|QCJwk#RzWa`= z)SZDRs&HqkR3MV=RR+)={XEdaYzrtETzySq=d%S7?$hS%IX?G$26zlSG_ei(v@a|MKpmEgEKxb9m#5pTx-DXQD zmfw2jydKh;?{0|Qsp z1adN;WhfJKtrS&11xnYwN$yvN5u0ICy<20je?8J!6+FR-jP?||q){U@kj=)HD&QVD z{DcRNN+JMcbF-i86oi*yv-3y0TlaqkAiDc*uGC+37wCo3wa{9-`$ZtgDkEL=?%fxbqjyN6lq934KCSU-JI>-0pc+cZ@f z8`zg`(tV$5cJ}(il8xrua8kZpw6lhXdl}~I|G?g&Gb6m{1rd->B9>s!4F_YD+}|Hi z+2}LFWp(}r_4$JGbB39^yy|{>f31wV(mX$xb`awQIJPm@y*@@r{ZI1nZwiHOMaDT383No((S8%Io)GuB!|Dn@hO2Z|Gn>ZCR|T zI?x*wR_}0#cCpETPw&SI7f0UZ?ta8U=QUdYc>Jl}=|K8)u@y~ZdUg{hzL%~L)??M- z;E4_%mmTK{a%?!b5y3DLIe7QVE#Ey3bFmHpZ+E|LamlW?IL0fkI86kK36q%B0lB3Ol*0k{)4wyp_>d<~bh3m(lTAQP=QgU~6o23HKBGOG2FacqN*I=Vb2p54`;u z|IhfD>q^JFndEEZiYjm82^VJjQg!c^j+T6;OU8Z`vz`c;ky05SZjK@y`YYulz5$fcy0Nt+gIK{c>2NtGWlt&YC zAt|vx)Ibu(kn@U(ghXcHa*B!Sbc^{?O?C{V`Or((C2!6cHCLMf& z>#F^d#E1oqlruZvlRba(9?uj4swBTnpvuav1huxIvfw`gKi{ACpCH1V(jmqYFFIO7~rn8p+F{KYSJ{rpSr>{#{GH&VTX6>?eMQQ z+Mu{&2&%T-J}OCkn%-jKKaSwITu%I#bRhO*D!{2yUXOyh0H}0O9{K2KG%pd@b>=u4 zo!Oe9`cq=g+w=q_WTi!}IZJh_(EYdfQ0v|OUd{cjp4`fgEFypBlA`x@#K=LQhh4== zBvW3>qyKJpi4{&jrbsyqf3cU)v$;V=B>s6v#dnFc_-+}%usze@Xf`Ef!|kSJWG&>8 zh>5S%F2NC!QbVt8&2PHq`Stai(#NaQ*$Aa@&YpQdU0(Zi+nOfFKVPoUSUn(C9p2_RqIG8e-JTGL>=7rl z?7%BjQtETWZY-2eEBVLb3oqHpilXiyU_nVNgcNY*92#k>{FqtRri9GYfS49V19SbG zFWz^(^~B7c96M7i>Bj^2<4?53Oy|I+0K*M{iiQ8$po}^3eiVAIuYI*D;$c*#q)K{g z@^C6Hx5DBoj37cvDw02`(=jm#88T|63dk!v$zaH?jV9Aoo=#XQZDx^3R5Rf1hSmF1 zkfU57q|z=p_w%j4PyRW`G|C| z+2a~q+Z!uraoyGExF)aO(~&=$s}bcm5A=XmwJedPe5s^OJ%eeq=l77b7%wdL5v@!-6K~M(BZzt>D_PG;-4@V<{i+?}i?t1Bdz7htVt{xxzhFr%_LoSvSVuUipRq{%Zl(3za+>xZ?evlh@-sMZ7nF}n#$ zg;BZKGU$ed7D*hVZE#466i9EHQs@nc7%~Y8dL#DMKkrB9QV0;gGO=dv!Q$#%ZTa1Y z#qvbXeerJpE4Gi$<3gQ+_1t}X#0Gd=GGU=A6t%e%CH}i>o^O?uvQB4$8eWPM47odG zG#dYn(%cvvJzbnnLc9qJ)mkLgkhs$DO_A8&;jpt|<$Sq{%2lj9^*}7-*+w=ry#)#n z51-BH1-D$2EsFXs4Rgv5+gSIar3lw@gIS3@5gA^Bij3T!MvfdB8p^=ToRprL1Ug=GK)BrYo<5L(YST_RLd zGl&bo3Jel7ko+B5&_&0jh>VDXC%Z3CONYS5P}oL68w8&eB%d{*WUUNZXoO^;wL`F) ziE(-=i2&CpOG(NZ6C@Ib#55>KwMMd^NFh997=Q$Rny_mSE+-k1P|is+DML6eZN@XM zEG#NYL?91ASx(vLrP%)r?MkFH4tiQ{(UI0HX53duB#4+54=jZ2q^^}tNX;yy$XZfb z@<&D}4O$%wSco|PZwUmqmQ~q2v%+8Cut^V-uA2mmjy`E>9t{wqI8h@lc&TKkncnaq z1MxthXh#o{1DJk8Um3a;+KG6V}m{tUSq1-e+(LK$BcUn-~@LOf{>GHUCN4<%&jirW8 zW!~dJ{DS)+;Mh)Zwh)KZ{2G}(4QIGMg7JD<__gzIeLZb7?Bs0uxA4MgJWZ3BPSSMd z5ln`4T?B!?E7E#d)kAiOKq_9GQn$81h9&H-;Ul+e%own8_qQih*==X+@n53wzaUp= zoO2H=>B`2Z85Bbh!-A%oRgD3PkxHM$i3O7}?R4y5|B_jRhn#pDl%vV?1P_f*sfXe) zV9|y+4|O+NP%eM8vJnfXy3iw#FL*hFzB`CH&5FrhL~04IhEY*w=z7tJH03a(>6ICn zB=o9)%5ts)ODyr|2>-tY=V~pha4E>3|0XF0RXRO zI^Xw8b{@Yk9C$pQp7~Na!ai1ShI&82$2}4L1|K{w=aaCl>K{?*66kEXGHNO*l>l+= zW`zy<)3oM~a4~xudu>oZJCB3yvY_)RBdAfRt0->U_LeUu_;#tQ5NbsLloQ%OG1)13ved_(iy78XW}JM#NqINl{E_fHt-GnQYx=Ib+M3PuY?+wGH@ zQJ|}OdNX_h9~Csk{`xo%BlD2+#2~yjFQlgl7Eyg24Tvm`la)}YNj3go2opL25>tYG zAAyL7h;Z9(wRL9miqbj_5Hanp!U|s0$@F@#=x}zugJ!qL-+Ru_1SYf_;1L~0jWLoO_Ahj+ zIGtg)o3DFB#4m&12f@X{o(NSoq&2Rg0Vy zP>xtQC^32UiSK60gp&C`uLM(D?ldl?(UrKsw%R4avvD@k!;8pbH)LF>q?^_W8Au@@ z{MY@+wSSHl1s|Dg0nRtSt~|tuNcn!sIk-S8|C#6;RYVEaT$QO80O!eoCDZCeXED22 zeCMpARok8>y1Tz&__j(>imGx=U1-J4?xW2V5anUy{}HT|^@T;Tccy|Pf5iA;u;S_v zDtrYt+kK4@Qm=mpj$|wQ&FdWV#47@dq9VuN zB+576ND}5MXEkus>`H){J2*Ff*5oj;udApiUbTP;gVB>x!$8k)RQd7}$IxT5L2=!1Ppy}=#sYA%&Jtn(F% zLU#DqxL}~XMgDUznib=9MDZ~kS)bu zD4^fJ_<7p=z_OkSjxkD zYb8ZD)OyHC$Mk}XMdA;abMRlDC3U95Sn8ui3`TPpk<8u0k5PEy9r4+xz*#;HFuJ+MIAb??UHW|>zoALUC@l)sd z%4nwWwWBnchkHAvJ-qp?*bbsfcmLLsqS50)pkw2HNIx6#H<_=|nCox<(@4MHH-O3M zg6zyx9Y`_aX0X{MkzC8JbC$Lu864huYeF_>cV~;OvFaOiMgB{x7twyXUZf!c5oxIc z-IwQW!vD(F>#agr9O@=_m_GwarhM!nTE6R`jwVexdw5Pj$~d3|V5~H+1xL<-FBPRa z=luCyU8zZuy<-^PTm*VyxmCF9=djVmNWffZhs-si`voUZ(Ytd%PB~nm5%R3x1CFv= zgMZt2%S+d?3k}{f{`$ov$JXAA)rN&I*z8F~LoJx={_sKH6!Ajf(QUZ`AN zRKm8L=(wp_L6L+PEv^-f_8LrEGVIw$FN$u7j2#VB)X-dv)Xn+sPwMdi1qU+-Jg0kj z#Fuu(=n&I)=h#c5p(HS-v%4(uPcGY|8aG1iCW$*CGmo zON~ElmZ`zZN=cdGE(wjy0|M4Tm|aCn?~%o4aWgboPhC9olwEeLk`*~I9Tn)HrRxx& ztGd9bAgbDnEq@z9vU?iWA80WPu7=G!4#h}3?>bqt#;rng{fl9X6#+F8Jp||&7ur&9 zE(lM5=Z=?y#TST<-#4lz#;SL12)I0(S6aFivA`xcb(LMc)fDdy9xH4Q;)sx$6VLRD zb+%li+K<%-+A_PX00fOjGrX7hZ-L;d(ae~1KEWW+c=Kd1VQIEl+)4S@r@3`qO<(~o zm7(&sYdT{Ab2B}Ykg-0D=ahQ%(BVH`XD5|>(U_dJRJ8KRR`GtUP41?F^6jU5ui>Ls zN0VubGcFd~(Z0T0-a3cWt^wuJnVQ9NW!x%FPUO`k>ej1wrZ7w8#X}R~Z$aKbgL{&i zh`hW!WA@Z`id!VB1dc^>HKRXT__yEt5;;hb#xP|n5M3tkduvk?D;!PiM>rl5Lx)(} zv`vWD&yYXy2{AoM1hV;@ud6MU6Gj*E*zNp)(ipTQx$7_qZoHj?5-D@VB}6q>dXauU z=(@)VmxqesL4T0_$EMD*a3;JaC~j$PJ&ft#AAAkr&xDR8DNf8T?E!0O%PQ(*lTb!A!RD$Sq~{+zScDX zx$-D2HF&*C5xDhdOoS*kx_=0bWP}kHYAth)Fj`hYf-+S^s1~~`l7)b7ZC~HsB(p#Y zRKK+;xin%~TbLcj<>(kqUdK`G8sn={npKKyvcNNhH^G5DY{Aq>2|tDa8z*6*g*9*)#+R*?%bb|cA_I2SX)55we-z3O*E2#f{OKT zC7gK|p5Jx+rS?4YjICh0#}~+~MW3p>4@gp0-sS7Er))$x$7 zLMJW?2JpCGY_jx`N))wc@!J;`1p@Y2)OLq1aX>pWGBmexK8qNGGLg85e;+pB!A9h8<7 zvxvb5+@nWOOF+R48AYEVRpResB9ZcHiI9K@RvHRa zG|IkfTHSXDA(`P{q~oc_WOx0X5@Ro@-h;v5drd9$%eJ%?WSo8(X4CY)XOq1{dZ%W= zOhqGoyC2_Rdl4NN>K%nwk?6plWOFm1?79j1u2Di6Woi{i7v8|d3d6H18||Xwph|?} z{7-708r5VsMfohYkhxlLK-pn3t7DWCr?~221}h;n)Jv~)&Ezu# z{Xhylv$_u&%u6YXR(sdV@Hslwg>&+Gd7j91SF>aq!#I?+*59F%u`jAnMLKt#KCLd0urPGIC}VTnF-}cd zFcA97NM7C>id-%;6#*OScM6 zJ=6;k44DFoxjFyS@<~@(Mk=kj$Y_s_6_FNMxvxIt68UFjo{3^ecv6MkboD!AW3d zd^ubyxi=E0fz7R3z~D#4L& zS|XY>rLQ7juIHjC60m{ta&f1?&D{>%DbN7PVkz;gSpfH`yzCmm zt8hmVXf^u(@bnJsnKjYYZtSFE+h)hMZL?$B>DWofwr$&bV%xTzoV$aL>pmPTy7!CYO^JiARf=FUfe9vSNX0dCO`r8XjF6*hcE>L@>T@EW)VJF9?x%{c- zF9?BG$(PDJI0<=bz0pv_?&W$*Jd6q|X(|N`4NYrD2Ms1w`0HB%b(qV*vWA=nLK)oJ z!OX_{r>u|`88H%+MfwbtHdlL}5(RKzdvkw87X@jf+jvpo2lHBDnj&wz^Py5y4l{x5 zx$fU;t#}jnF6T>_M2B3Zp>h^@m}^8qUkD!8nv`4)>rz`L@`fZ)Z9<<6V$7u~I#XMX zza1s6@n*!dVf*Z(g$p!+)GOEIbR#6+M3~|SkB8{PVa231&tXF+B9K8U<${}~2$1H_EpcBV9}%tA^>p%#jOA1(~DF#9R8EEBzbN*?61 z)NZWPsmmyH^Z!PG{8wMO`MuwuFy3k1^$~F>(2m%iC{MrO z@lNI}EwUKz#~G9{6^{`n!dhYj4dv?yX9LH^D3|2U*|pgX*3b_EC}7c_K=1wIpzcahw6#aiKumP1Av zNNqJukjKDpvo~JR+@KldXX*P8$2F?MpkMiJ^%RTyAz}c?cz9p|O_cIRuk|&N-D4*L zglA}fpo>*VcY|`NL{$j`#ViUq7_LwxH%QLVql!2f0>B$k%j_b~J5%CW;^b$f+S#>{ z%l?rbT}iucpIYaEOeKwG9G_nstRzme$`5@MAbTE_gq`*L@0#CyM}p`_7`|6h&u+wu z(5Cm>Ue=I?-;UD@^bsD?%QCWpb&e;Ad_A+y@zG*@?Y!#{TaSuokYSQ})*s2WO(6Op zXIHl=+j{Zz75+KsCQ=szM@e?7F&Dog^=L)r|JfFd1Ml@-Yp=mKOSqmrs0g26O7u+k zK5#fiz{u{FJmPn1;KLNv1|;Ei$IWjz^h0>sRQ>+nXI4Rdafb0`3b~xm$Q>`s?_C!3 zN72*Na7d_Rd1c(COXm*nk@8(<$wXibvcyspmauX<9`~UwoQn7sDleD%-;(~R?QdHc zsJ=*I=&+ieo+_BE!)^VVaxpaUZfY2)%0ZSIi|A27YI|gGdYh}*fB(=~rvti(6o(fN zTOZ^?UVb0$Or2UkJ1D$EP7N}h73E_P&>Nz_kr8ILxa1Z93yOEv0o(iw|ZY5(hm9o-#))k6BsA37q{R5y3!LZx<9G%Gb6RLT}Hoz(1(k3PEe804|^_hNtQ4p~Jt={2VS zXc7-embKJi?v4VbE^!X{v1eQ_@%=(*e5L-OXgB>xez z&(@kMUTAXSL^k7o>#p^*JK#EDn$65-T+$Mbu^`tP0H7{<4#GoB`dNwb4OVg$-LamI zPve!Nar?S!?BgAf%$Nb$ej@f$WQ()gQvM4E96a~!L!w;I50a1Ow+b;)k9#iq-b$!@kSt zG=AN0g44_i0W&q)+_>GJ=y>g}Vgb{#jaJ*>@*n(#;Zbpho%-E_X!(3Po(CxBd)@qeX}9l3JhYVu^gyWuh)rTm-Gncn%5HNqhJk|xwE#4q^39bK)42V64vL1@$% z#Fvj;c5Q9`Dr}o^yicf#Ag-(~6@h6rnS(g_fS))kIU_P z3Hv@5JyQtLm2pq%Nr~Yaaviwtj-MG;{}l1X+vbYypN36oUG}c`n1P7`7pu%OoqM_V zR$Qu80bQO*N1H@W-W5q9zguR9*a#0$XmD^+p*<6TXQ_k365Z>YtZ;t~ix;%mLjG;C~Sv436y4N+he-8_o$&Ij=@MW*-494t@=W~{Fj zk8Iwqw|UJ8h8vN~7TI0Ps&ij*ho+ezWx{QU^)k4~jj1(aJpG_z9fpidsrC;qB5CB$ z-$(F!$4uc7X0ltRYNWg#J=VapNpBorSk4~kQbu6zs=qi`A=K%9ML(2U+lv|zO{{*W zD^IB(x|}d`IKGwcCT%ZO=)gZyqZN&PQOVqD0WZ1k6wKF2!9NARz)l3O@a>@lAHzQI<_QcO& zHHS<6m&@04q9587G7jl>hJq`Id2BDB_V~udo8Jz@^oMbrA3(e>hjY#D43gwMr)rSw z@t42a>sBXr<>v@Mzd}FJy2WW#ISWfcf;M@8+D>9PO*>^#!LQ9h?s!rSkyQV)A#i=u z3i$W8IdkF^igGHx`Yg&wDb%>#CxKY0n_icD?=0!u{jFdB?c2?}Ozdl0@Q*6qHx^77 zeB%uO4S}`ai!+I64%_KN&26qXAGS^+ktz2^9Q|=aWE7AKMXzqTT}A_UNR*h3h;V%P z!=%okbnsU9@o(VZV8{>siv#mGqDzDJ$KRNSnCu7KGO2BdUwz%JLXSH;NDW9-DrHfyt(Ip$ zp?Iw!&bK44T~@QkEAB5Nhw8t~h^dnk+{&b`{beT>N@1eG{Dw_szjCh|Wd>vKO$AFF z-MqKEkxVQ+ax(5hUPc9&^XMrL%QX;LCo!!SH-Uk-z$--e`?n0%4sK88{RmxiIs-(c zM0uO1MSJ?@Z!bilw)k3Oh(vl7qx5d( z*efN3AdCoK6H;*@K;kaRorfdfif04&aX1-}q_bOvjMy1}Ss?o~enm_|?xskqJ6O*f zY0Tv*l-;TvL)bnv2ESAb;|+Dyn}`fiFbOGEGkUKUX{D40!!9%A=Vd?#cy`8HPgy@BX6s4D&3u>9qV{wnw-wF3MXw zdTO^jNxG`DDCOn4mJd4$unBWRzTM6-i~P;Nlux6ygRs)x=PIi3CJ)qPIC2b4(3KiW^d^!ySctr_{(*;HSOYA(&M6L;Y@Rk{gZh{2cR% zNPH0Bxm+6$Q$vF=%2hO=O~qGbm&qBY1B@k< z%{Wg}gWrxBBUAui-9IL~S|tuDtwieQSRD4OGSUm7{p&M84Gk|2>o|0Hk=kH!s(_NM z6$Jg;9oVTwXYfF8kw`vVq#`|u141-s0W(G+1gedwTK5$#DU!4I_|_xQd;u+T`3p+i z$Fr>F8(%oti=_q*2ZK&HK-P(;%>@oF9DbU{OR+X%3~byA(-w}W-s*0a8(fGi%K1xitkG_>gz`dPYR@6GO9nzPh8x;##_aW>OROQGLU9Tgtyd1L>k@Vmpshj7%T3R>us zbttT`l$xVNACr_YS~!kj791`1Hf)CJnNy`15%F(0F*Z5M>#M}9VV`Z17vTcu0p&4ZQU zdzmy#!ZvOw?tBz0j+y*A70Ke7%vQu{97%n#+Rc(9hI*NQl|bsezl;jUWfh8zM8jt# z!(c<*Jeh@fmU35z-~knK!;H8VN2Xxu#V{#wBC*{*Fs4g|@r$DeCW^tUFcWG|eO)bv z5?vR-1;mGn1*yEQ&?{KXLg~Y{C$o7}Q=g4+(m5V|Iq}uM)6fRa%Skq-QO-=SO4a=V z=@RY9sSO?p&X@v{{@ub@D)gttttO_x@L9hL3yY2vdB@t;7u_f$rO0%h4yDDph#ZMQ zwg|~c5NL|wMyQ#&iI?o4;GqLM^_lGGLu3ueEG?C75(iTM)Z_2gSLCUXNfv=ZC)xsX zTCl3f8G>!4P*u0aB%r_p28;~ld(SrlRFLxJbSI(GP14cAF(VUY&Q8uqZdN_uu(@*@ zXGAQ^;XsEO#NNx(XISb4YVe+RU59Cd!Of1ObL!Z4&2KQGnMf^ zyHF9yAweow+~Nz*yhFjZ$p|y7b*%F6(^%tv%Vy4W`LfdboCUjm4NLqkh-ma6J55i| zCQDCo9a*(bEpZlWwpe!x`&lwL`XZ}5w+RDUtWBI20(auxXZVLo92OlT^sNx_T*(r8 zrF58?Irqj{>TFl2K;FMK<*_1p1?S_ntZ2iLyh_h@MHv}vxQDc zv348|)Bi;Fckus|#AogD@{Cf93I^PaUY-vlzugT~*oGFqMRS=jB;kEA1DMoX!{ zm~(#AwtAn?Z+@VPs&Lv{$0cti`Mu&)gyhCzUz^%;lss_Rdl2bfBA5JLQ z2e287R{RcvUndXb8>ZLyzj3m<9RNs{x?fPNd0Px9$f9gN5kz{DU6zg~k?^u+&k2%X zqOi*`;)i(tc)dfJjx7{(oTA>;%GrUn>o zwbaeYUX4#$>6G}v7)X`n{lnggFva5VbvR?38|v-z@g-#u)lfx})eYz)^o!`SM=NBA za}uK|5?=|`6D}ee>2#SKra)4y{z%OV8VFf0FTC6hjgXQM5<~xoW1V1nlKweO3!`B8(Hkihyz>1hV^ZIPC=`VX zKzY}C{*aXr5y#BwNK>(U@9n$S?C~O!^a3c4huZ7in79zugKis+jpuKuZv5{gBN>WO&Yb&bW@pn@HpDyD~*@IPk)kkC(r-JI;W==YigaWRIsbL zWdwViHz3+Qz^^S(`DbMR4~4~fjJ2Fkh80|@;i^eCQzHAvyXTYM$eb##1~I^|Fniib z>o(atyI!T}r-_|Y^w+@hfujdzF&J?bvhc(Z5GMh+%{6qWCCAFn9Mh(4lRAkFfFd>{ z%H#8gf`<)$wqp2b^d%~;+Ke>a0^-hreum73D=#l-==_VvPjga zbEkUQY>)ah!*`^}im{?_Gki#0g_6%z31>rJTSG6^Zy^$s`38P7bYrv|<2%vdDByy( zuhI1Yfn46B)ZGBbc(NO*vx?0VMk}oydta$mYt)x;NsP~GzD(a=s?m1f9R#{eSHcz{ zw5S^mlOUaKyp~vE6ic~o&-G>!=jm`jU~({Q#EKLrdM=*{_l%tEi20`o2%7$k#AI@Z zv+XHx>i06G3)Om7+AEABmkv-U+so z>IRpBU7SE0Dhhmec$dYDH|oFHmxnY|t){(@p<;UU2>J=Fn`a|~jyX)P%LbM5IN< z`-@oxD^9oA*h|$RMr73askX$)fSl-^8;K3BR}$BG6@gMvQRID@AQBbw916GGLj~l( zkS8GrNMRjb2QcU{T;+p_^u?M8mi6Raz-5U<$;f_kv=wF6FS1z5zy>eQR?Nbk@y8ZQ z?KX%A47TApg5up|w^8+Hz9R9SfftK;#q$p67l*6onF4ricXtjW^AU2T(jJN*iC}Dt z|7~p(Q?E;Gw1(RW_8bR0blJx0cIS?c2GOO;oa${w>M|InZ=RH_!WK1bBu2GZ;8mC5 zZ@xgoYC4EgbIiM4O-rjF1*=B zC-&48eux5T_zr_Id?72+GmD~FQp!o(2_~0`t(1^ps8R}Ns>I7-^5vL$IwN4TBL<%n zwKO>;D4qZa_<=wWEP!=M1yee%e`%-RM>)3vQ3aGMUMf`~ZA2#vb$AP-Hjy@@5T=}= zmIn*{!I|B^1bTBb7o+k=0062Ch+sz^RL|`lhfrlQe#7xs>IYO12=+7| z#@#*8E>J%1L8wVJ4hG_52^8IjV6T_RSg!H-=E!Hg4ra5!zCDpKnavq6cRQJP_JEv@ z&8pzJiR`xzCAD;a+br2IR&U`%=JBo%s7S}4!k3#ZKN(->?aXc_q%t`{E+th4_M>I( z?EJf29qFjTMA2jzgQI6#QbLi#-x8E>(pboYv@>*Kk0Rj4Oi*VKo`}qdL&T44=M63J z4eq}_pY+M?OOZ%XVkO&bin?HO+2x4JsfDr8Lf#@Hflj7ZW$N}DdG;uL_Xv!qZ<|oY z`{w-te5gaCwU2k-nIaXc<}6ZiPKkJa1Cjj%PyGk@h(Q%kjLyRM*k%OIofi}Hap97k zKMuC?RplC22ii2sUb4F*Q>*j938#~Apfv@CWF|l)0B^&}9!mFpFRuRzdffuvDD?sL z5tFNHsuffl<%QGgh7=#38d#*bo@ubq)zHWwN+|br@)VyKH8)R3A3AG_oN?PhP2^}q zObhy`qgVd5skM-N_S)hX?xw?864K=Z%*BSts;cDs4|HbxeO z&5OMj!^%`>qb`Hx8}6jdZJj=jc`%Dqy3j~K?!9~$NMB)LKK~%ZI9}M*&f4x~8>}q< zoG(6N8qsO}7n>DDDStmUJ1_I8xU0^g8Gl_Mv|$0YHQDBG6OGvy-I>B{3dRAcTXPVkbpKAqLinFewEAq@A03lqi4+?I>fBzRg(`$fzJ-3$_mmV~eO1iSH zC5=RCl874;64bXb1)Q!)^|OTp>JoXCA`Q@!Cclm{hD)GuzFy>IR2jvTDROUTY=$BLw4W8wVe1)$(_ z9u$IrvLMzxxDg2?*M(hdG}4ZH@egbZ&R(wb1iVEEdGtF4K~#5!kD4jfyWt+{pV8D^ zr}}n4nOYm%usij;3c8Ak$SR8p&#|D9I2;aeai!D($&MkFjY$ASDAU|2)f^4B$ix93 zM9VBk@2U6RBp+S7c(@2QBrJ)vdmwfU^5z_ zsv>Bmcq1f4)IN_IM)tV?;Dp^x8U|>Hq23i8CBq2EI||N`Tzv&fT5DW`!U{G-At;jT zOwt3C-(S)g_?mObX-=o|xZ%B*`M9E*KR10VNr(bX74?rK(n8$5f+&`9F$;f2sVS3= zq+jnfi8;r5O9fSQ=vy*pdkv)Pc)OCxf-7Mg`Hwu$2X7=GbFds?&Jn@iTF4x%5xLKP zpJN|Q$=*~UvW-&aG{mhEPQHop?%Yn#>XkLg5RGNy*_~)@D%=0V4}1rMq%_M5Z{s1k zyZJQq7t^Va{1LghR%ybmu1Fdj6dcqjD7ilZZK=Jy`QvPJm-m}vxvG4iChWfnA&tBA zXDmv{kBaV;qs|zT$wcThuh?Lw(HTxL;O)f}-m?F%_OT37^t(I-cI+^6f|&F#u|(TZ z3(~j!;?2^TR#0k;A{w@|9R8%%fD#mZLMAf2VX@(v`~~8(OOy*v2b`?8hu}c0w0<2n z7{79aLs^wFb0s~;*B8C~8T~>?CCrD(D#Zdrrx5-#FdWdw?JDVnsLg`&68mX(wC!16 zsSM<_4M1ppy8J8JTnvX0#tBaS^efyM|KV2+S#0f}1|~pP_=}cSwPneFgU=5^I` z_23=}sphvfviW2hN0S5Ik~IfZjd~5%(>|H49%po47gxSb?DZ`KcpsSmGWDE-+vphg-xoH@pA?t{ZbJ3nHdH*5>QgXn{RZs$SV(uILBDwzlgem>rK@ z=4$hn6J@#8xknxSHPj*eZAP3dujt_lLag%Ta#-a@Qa}bHT%b6eIQ1%LpFtSuc)}ae zS~&+9liDx4tdX_Lj*~8NnxYHXfDnSq1_R{#CT(41$Z-GInK~D7p1T;MobRIdzZbq0 zzW+O+^=A+TZn=A8sx+gSDg~!7hGdT-L%gUn9&tE1hBsNN&$ES@s^+T5@AZQ5zR2>- z*`3A>1Lu#MOFlUKOGk}(le)-hFEYhZ38>>q@M zgp}#KxmN-jO0)=m#i@qT`BfleV{?%R1D>R$^FT44jF=Lf5J`TnnKcBr6^-Ph1dElV zBRTo?{~ehc)b&{+4yWW#+qY%IU=jkqt0fDZRoBbrZU zZ2NWSLhP}FBl);jG-kt8OC71AOtwhZ1cO6FXC!nmnqB*^Bm*zygoT7a zKtPR8&{Bg`JjV{rGQR;{h3(@1;W7VjvW|-SqA#lNC&6B)00{=RbG6yw{&a!W+1Yt> zvn$NwaVKIFlo(&iPfN)is1v;+VqpPRB~9W~;6JPjX`nOR-s*IV>P*WmS(Kb z;i^mZAXc>oA5Q6$mM-Tb!UC}_;90q7w}LsD&L$S!9$kC8<1@Oqou}QQ)_On?v2SIT zyZ?7KjKaJBr4H2L=?nJ6>W=|Jpu;mh2O?cMs}M`bL#cKJd9pQ^gqUb(Jk`#{Fj=X3 z2YNc2O^6u$S)N74lRG|YG)hK^@lIzG6s%Y9l&sbM=%zYchZ9a9WOU!ejY@v=`QGZ} zy!m=9J~o?e#BelrzT!O_$Kwi4h!}*s>le^k;}^P5_xbp_ta}%*R?E4+8yN2bYITGM z^5wOONep!E34sKoa(flaQahIbv%gcRGnIaTHQ>khYDAnb(-lBD@oL(t!L>(J8ydp9ot z+r|q%kXgw-%=r?!;%Y8wQ*}(g^7#LQasG$O@kW3-I0pJsN(<0LQDW^YgocnVLq?O) zGB>Bx>q0Kw|2EDy5w}n$E1|I=$4W#_vLfc?g07RKO6;B-_{fnn0(7#rdf(!PG}8uv zqlE6iMFQ#Y+>!8P4;idp?YDYAQDUPm4d#==7YSQRz!_E6Hek$H3|fCUUj61$cN<=N#Mu?Ei6i}nOy#iR>$cd!z&~c#ok<1{ z^Yg{#z4>!d_d5+DJ|M?n258v#H)V(o9ExMm z?WoNB2cvT@R3|hAsFYnIh5QwfjA^*f$h5&oMA?m(Jve_>7ag!~cS*L6m^-wxGclih z@4j%UW*aisBW@6xwEC`t07(18NqE1AaV)(1U*Agk3KiZ=9`TLR$HDKmW)zeZLI_!rWf9VtkVPnwO+y_*3R?02rU2a;eQf`TLQ24KP{f`!5 zSj?Nrmlz^NyOsj&_s5(P^HF8BC-7NJ>4c)|oBt)%3OBXU-0rdFA(yTJvY1CZU2!-e zuT;Uq(@^QnLU4%_&u@fONgkjA-f68(x>di@I3IHuYp;bf67wo|HD+Pussq zV?E==1^$$B*gbGCZ*AXS=lX+$f)eP+ij&{CX`0FF2ttM;yOUwy&kCE8>ncgZJVjRL zG%dLCBo~kuPG_lBE?^GslUS_*PqtxQkF@R7@^sFRemxkDk;X9%i7mY|Zk^EcjE4e9 z2iBj&4qlw8s)PuvKAyhit2p}~hVcL2+`hv>akQ@kG?<8?21(@U&aPxsQ3?Y#IzdYeA>t4 zM`SG}0Z*jgSor>QL+x2IP|2Ucw9Z&zOT!SvwzzJF{zC}~P_gB4hquv9aI%fayN%<< zXOvo|aoN!NkLhO`aolbFTWl=M3`D<^o@ia-VNYP?)nk&9^1_-}5N|3I9Y~JPBS}X~ z9+8uLKkNk!0G-j6T33q){$z9i2idoh)lZ67^8S&+A&JTLF-h zS%!Ff!e#laK${^cDEE5=_#T|t*@mOO<#)UoYwe&SCyD~gNzW4vWnhr(}Hs`WpX48PJ#3f_Zf~tVfmq79rHq}GE!zlTOi5tcUlF5ZeK1zuvWShC>uiPVQypcNlJ+h_x zSKP(<(ry3hN&^a6C(z2lZFvusL9bH4-p$P|bnlAZ!Od;@duo$52(eV~eV*aiBYv~X z?P|1*#cT@qkV6FHG<=+Cg4Yt<>17+ZFEY!e1Ur2n5G#))Q=Y(t)%3gk8^xq`FDuwL zsotvsYz@Q57gw?*#vS>`0|oIrI3h)~V@h|X&{nlOs|O)2+l|TjO%9-2hbbDx z--GZi1-mtEYly#iS>ufvqzorHkBIjM|KlAji&^(wv+(l$p0*y!w(psWPmXZ;(t#KK z6RL;BL+E03;DJE_vHAfvce{S;!{L3#URA$a?56)Y#F?K?RBGsAlx<*GQauGK4w4U&TOcyixEof)n z!PRIrLqcmhiIln@F?n#SER#M9bvmy>8|@bHsMuuf_g17#d0!PsFE&*ht@#m$Rp0jx zj>aVJ+Ct?ny?W`yaL2(Zg>(8MGh8krZU^23bNt=Dppqm3(OCrMhQna3eBdj5Iq_KkGo z^Zhp$a+?k!@;wBt4ScsP6aYC$G0ZH`MSpPtD;P>$3p%0euuKA^=m0JALL+BDJgg0t z&F>QMD zwG^g~S=7LI*72bQr$$XFR|b{h+0r*KaO|}4^X|RE@}g+zyD=ON6HLjKSAiuS#tFHH z;oQgJ4SB=q*A8}Z3{EJGbMg$j!-adljC^d-=bjT-kXRh>hu!6#kU6F->j{M~!C=-X zr&&QpcoE&A;wrUty0nmVcKWaB^t3oI20H?kmpq(enY5_e0`XpEZfp|Yi(o8mx!Jm- z8ewdbATO3T&-&5mM>L|7OFZXmUc@h{=9eT9Cl)6wS1T2WiPSgCNq<=Vx+1@xa>DBt zj%GWS;>olT?|i|Y?YsxDR=KnY5)3;=8gPA2zMDOO-kRrB-m571?<#F%FLgk`c`-^` zsVv_wA4e)2ie`qw=o2oE#Ol~~)r;Cy7vdv3uCJCO0L3J#&I>Vwi0wA})vr-^zs})3 zVvyb4-RLMD=eNitpUaG>wae?vzKt$u{f;BQF1}a{_FC@&q?7dYf%sWyfNV5X=Bsn8 zr2LI=PE1wZaXJo{>or7DX^Q!BF?@Bm$G!bY3U7Ov%;#%i_N<5ZS~?15^E5%FY8pKlkg6cygsbk@HcVI)s--*t zn_Y;Y{atl_1J0g)O2K50;dR62(_;QfU;+M0hUwUxc+&kMBnbR>4;{|mnhS>Y_J1JW(82?v@hS{%BZ5_vFWx71c3W;U4n?-kD3u^5Tjhw9%e~|$ikq%x_OQv-NIWC#$V3P8W z?NLk>2nS>(!X8HI_DfR5$dXHVU_H;91&O#)p~|ZN8PUif>+!bxzcX~r7TQ2+D0#jo zj*a_uy#57~h{T_lRj7y{8CVM12NY5Kc+WxoNNFa$bNO>6C-;=N=lF|9*LpmlnQ3W^ z*WZR1r{gfWlI=s3gutq|TVoDlqSNMCaANKB`;)X|Nh|+T0 zO@?1~pJ4q~Q0Zfbo3d`#R}-Kjr2fi=N0`r75;>>tKg?E5gpJP#QjS~`jQ`eVlQZ9J z#)=vm3qxdx{%lMX8vaYMQCy|P(o(_`9XqnW-fI6dR86BFUCV>hYO$1|O7=#2@9qv& zKCU5QW5xMXvGix}f;vZvrD5Lw>V_?|^)iGs4V|p4$1j_=&6o(^eYkH<;hW`cUIv%b z1$zkFd0<15gzWUMnhfDlCP!dSXk+ktSB1h{gRs&J3eS`Bu!2}m>@Vk$*#t8r^dy<^ zHae|F8(3IgvP?cFczCa`pm6@jfA)FENPo%HZ4LHnU1vT&tEHwJ+wtaH+dOO^uW$4G(1?$R+vf{1|NceBt4Bof=P9K+&W$K6$}Dq~*Q3eWHwHJq^DY=DSxw zzek?!k1bH2c)A1C{s5R-l}S3ORY6j0x5YN+F8Mvw5~$t*Eeq@$;mJ4>_$Wo zQxlV*urR1GV8K|)1aOj+oCCUV|38XKMKO}vPX?HmMX0X>jg5~6wuXJJhT{P8f8b;vo=tlwpw@|N0)3*^17L zj^i5QA#F*GbE%lvi-MCtBhZ`0(61xK)0xBUinJzoP!J_3AYD+Y)o@KSOK+ja%r~8Q z&YI*y#nhOY6B`*#-n4$7b72cc4HQp#$I8jY7(_^{mHo*x?r|txt>yg)5Guo15UAU0 z?3$nb#}mK`h%m${qRT@vA(o-VjHUOp5Wf2Ii`^d5`6sPyu`Q5+Mx<+XMJcSx{ReIX zCAg5*hWW?c-<2R%ZRPDSWa*OI5HIV`$AGaR@+-2g7kp5F!aMG>`2}5iRQ(64=A=$ht!Nh$;fq*@Co+?#_y{q60X}``%vO< zqK1|NovJs5NS_$Ib+0pq;brgDuJXJuHz{;^NK~f^6%X8N;%o_2^QqtlZDt! z)^oa_VUZk8S`Tv(e`e=tetJqYlm?`WR@v{)uw86?Bg^Mli-0XQ$oic?E#|vW2`~;U zY-a4BqXt(i$?d?=W~{&`xONKMo4O!*zS6Q;uZGWK)Z`g475HIE`!&B>le_zYUZ*&e zsy}lX`|l<>vqD@f3YGQL~8yZ40jQ_SU_1Kf8bDGaJcFY1s(}+yankPbe0nofZulSM8O_mX8yFkY zsZDkBf3wiR4p^BRPX}`FK&$%uck3WOn1xpU83m1oDC(`7<#6-_H`ovO^jhunC) zpURVccHnq4VZ`l}+f6Rvddgghu183ueD4`7Y@_T>4f0YQZ4dgfu+u=e0lWd-0q1lsuGU1RYseq+u+zVj<604+$=SRqn9~ z?TSG@Dt76AGdG@jYXOxjauA{0w=&q0KJAmx5}s zEyS)fmABUYVp%W1xDGIUahXJauvCLB#wI}VW`&4DCzM>~)3u%By=CAfikdqjGv_lP zK1;6aW3E;|{7|Y#@*~D{HD$x|b`)42OecPKCuM$!gIJH<*>zx0Wn9d(t~0yRXe~h> zzD?*bG%F0)QJ-Y8TJ#MXEXq^N!1dyiGJmqu334gPF?^eI7&Rs@GcVq`*YE!;BME-C zmb~J4!jqodAZFUh?^~({JmH2b7A}ZZXFq@j*mT6&Z&%F=mqOKADwR+z&uwAGZ>~ow zvJT5omn#rbgni-Z3WK5Z++QxDF({B^cvKZ;at>$5o2{>P{W2n5Z|>d12{i;*ydS;s zE##I3Ba{7&!+&Np8&Yk(Y72t&*6#=C+prfH3Y(y!4s7@2LYFxShG#Lik~3w9_rG=lT$(47 zZjYe3_)bjQ_1A_YOr((@zxMzK5WSEPt|3P$`4XDp-(82+c8rg{2qcMeAcITj`=Id3mPKk)GfSx#RWod_)dNuS?6Np4`k z-9WZjqFEyIistVblE%SnW~3=Y=qCC6|`*_y~CK$xW<=K3b+8~Ah69uI}`jtz;k~$rcClx$G8)n!~ocWB$JT~ zB6Q4FAc-zEvqf^Mt=;ah@_wMC*lD?v`*(A^=9qbsr+XX;4a?zyUguZB|2@T?@-jf~ z(dYKO*0xoA(e!T)MI!*)q1>dOh8a0}%`M@_E4n#I+9#1tNn~waNazz$(rfh(3YXy_ z*se-((9MlVA^%g~g~*-YOONeZPmQ=x05RfV!PAv4$nmfvC$9FFACh`{CA z)?<&+9m9md$p)?&Ta6Hra<=z9aNRE}O^p8!yFf(0N7rU$u$5Z1!L?0r@S-Qi@96}! zx0|H*DSu1|ic~Op(RP|+m5?qaMg?H{PkkAoViN9L-vOin7 zI3b1%k^;zVg~zAEN`tw1<7scm^k?tu&rHJMW2aEO+7GZ~-?G_{3CmU>^xSgHp8hi` z9or6LHFfAP-d{Urz-Um~O0vUDO^QWA3Pa{$3F#bCwU(aB02n(NRuRsll7 zm`bw=1!N{i;ozxDsNSd*WUsGb;PA1qpk3)9dLRh-Hxg8965J+O2Q{l#~ z!F8vvV}#i&wxXCZBZ<-=J32HFO4<&Jr6#KXE+9QF5a&*vN39k;$*{@0Y**M4vdd8) z3me3(%oHq|{3B+rzl9k~He*Duj-)|f8m7?5si1w&URc_B8fLA`K!RRzw5;uf8!tW< z0%d0_!H#iC9UUEWDs)887L_n;%L7Dn$!h&oa`L6_<+&%HC3!9u{4Bsg0R}$CfG`ge zQ5b}(T}V)ZQnWDoa0I5E48y}<+BN1%#U&(`G_!mkl=$PC$W&3P8RY4UprAmwzj=X8 z%V$%4xDU;HbVgANV^pkE0~Zf&#Fmh{NS$o3ZlHEwrQ8wxzWL zsuwZDx`lJ#9UQ?}fB~4la0OJ&%c7XM9wiQpQT0j1AFkwxcJ1IHk7d~sB(Tp3=sPhtD9%M8q%jGO27VBe)Ts9B%(nb!`WedmGjOVdUZrcCSC zp&9e%l4+zjcSWZ%cy!|ulng9Tx@0jtzq}8tw_Kp==!=y6;~vwo=HiiUIDYjZHwjgY z|8*TsAKZtH8`fjpk~y%~)yLRh7olZ!W;Tfn!L}`1@sJJ{v{yila>Z~_7@50=B94v% zYqlJOWySI^Wd2|g&rtlyL52q9%i_`TLpXiw4efLjadi7~OqsS4i3wx~m(7EIwU$^k zjkatm>6Dm+Q9>G@4{ks1dcm)+hXK1nrBOsR7BeRQj5od^Gzm$;rUc8QmwOvrWR7I4~O2qt`Q{f*RiW^5(V&~cWWa{P2 zpID0@<^$mS;y%=@OQ3hZPAFuojPnQg(Le4-ErsUS*+Y~7bcGNK+BMS1pWlY z`bYXx&`1FW{$FB1NY<*`%HR>nz_euC^mH`g7E1%;$V})n2dt1%2@t}fWFE|_7M4LT zp8#bd!kz}L#CI*9%(QBnv>7A=sB1)H_h}e0xSvE!TlN}(7jFac^T2K}(N#kND;zrU zS5z%z2qlMB7&Y)VmQNmvtxJVhbTS5xorGFN4H5n(TXO74W$(ZD`uf)M~mf>`# zh8Qtz4n__8nckQe=nYNgPTwB$N4DSw6AvwJFge|q4&dEDV@v zg-Xu0Xi62=^+#hdq(@uggSgSO#_YMhC69E0do{)6VTE#ND0qP-e|kO3NcqwsBvK?b94;hHBVChm(GT z#-nQGqogASDC%SbrEIx5j-Cp0RZGr8@)6W%KNzNa`lE8G;+$hjl)zKX8hQ_dmh8e& z#{C=g+zWl0kzp_~K(>Y!OwDY0Amm|DzbGRJEJWY_Q&IY`D>70N$&@rij}{-)>)#4T zU%Ornb7!nszZ65p%s}~zd+FJp2)jy6uxjQ|X$`qOyNa>nM#IRd3GAX@Aw@X@Yo_+b zhN;3aOdN*Pv2gPdbp4_1M_rQ%T#F1vGU8sz4Ba}n!l{?L(7pW+QaM2bggJu*${Gw7 zu7pJs`(VRN4cesAF_IpA#VR(!s%6u_U=05V$mq$bINtpu4Hh&~fPrs~0dXqw@$o_F z(xqwC{q^M3G0E9j0>i^+002M$Nkl?TI;j0ZSvs4HtgZrE zgHKm+HXUMuLqn-9lskDa(lCsSO&R<&hcLWHP6m8Dy?8=Tg%KT8Y$$ol4S_zM5MIH$ zMrJ5vYa!jrq^d78JPs!2W>f>PZK@Xo0)rS+&lslq%$7?jjhC-KT@F-WU8pFGC^8iN z2@4H}o{1SZgfE}pqPa2o_y$m=sstlTD|$p*=I#OHfWosj}B;So-|FdZ5wJJ`rQ#l*wno`_~*vbAAlszUEYElgwBB%dPPaGIf(B7*}Nlv)?Y1|Q~URFEIM zgTkOj8!o59cJO)o2Fk{kC~U=b!9&fvH}4R~xNx?G3V~vGXp#xk*OiNWVuJnQ6&L~; z`{2l+=-OXi;{O=_{z^m9w9E7I50#=i6l3Txq45wPq>Die5^qMwI_1m+DSf1{P5Z#u zD8t;!hPeKi#%43jR$y=#3`t|ONx0L<)CPsez}CiE%4CyBGlr=6EZEoxJf}$-k(4yr zu1idyFl$kTwXFjL&Cd(5v808bo^Wt*$Rnl8Tk_BSE@0UH9KH%le+L-&Rt%dMx*!6P ziP=cup)4;uChmzW2*x~a09sPl)OC@55bf&Xt9_-)G+ z^xYN?&+8j<9BV%GFe@PxU8c@R?Y^ZUoZx0JAb z|JW%QK>-E|Fi?Pj0t|dh416mITP~~GZ`u8Vx(hH+fPn%G6kwnL0|gi;z(4^8{wf9v z7`DHPxq{~kFi?Pj0t^&jpa25}7%0HNKZJn-8@7K4(FM)?cQ7EFgczbHUl1PoxxDi8 zEmLr@00RXWD8N7g1`06nXE9K~u>Dz#6_o#<#Q;6Jar9LNF1Tld8J#{3>p`ptMoxH+ zyV%Rny__<1MOO3zD8N7g1`04xfPn%Gd<6{rzh>A}Rn@pDGJ(33P+i22{~N%1ltl1O z?y4e9&hG-ttEtKZy<{*FayEafir;)zL`MFMfaOEApR|`#AG?sjI8lmNE*~EFd*4Km zKUGx~dM4$^B*@3CNYwuKTKeC8`sm9{EWaZ%3dmDb<;8@Ej4gtFxG}V0LMkw(mIw%& zx0J{=$QVI6v3$f`l|K`4|I_MnU6DsgR~Ga9TL5E`MMp&=o#WKjGhjNaoIpsw*A~Z` z$-m$0m&gDiLX0VkD8>Ieu+Jh^-e>xu%rseC;$IcA8DlMr^XK>Q^rc2cK3|de>+jU@ zrTYKZ?}?n!BA2pCp13`VJjyC69|A)E8@LjApk)k7E#)@-H#+bSv?-BJuDw6B{6rLC z*=OhpE6U)@lre3eaz3vui@40<4HdD_bIN|8+gxL$+xgM=Z;68cp9~ujEK7^V#`W6~ z7#@%MZF`_rS&`IM9u4|WBZLUgWS+Ii*f=PwYr;UEIryZv{+XIY`x&v=w0rZG{L>3c4B!{;{utZ#97k$ClT#3xV$O9Lg}xrL$ov+-pT~gU#m>2BNdXl_thKx# z-en1sYzDBDAvi&XYd%@%UpbrUhdw4NDbYUY*?$1kNl4Cyo z2th9{W8m!lShIOG?Dam+70Nr?eRLx=+7H8e41yc;J~BtUbZ{imkb>5#XzO9=#+@i__2=9BsF6HB z4!cgp`B+n|oj>L;&w~?-XX3X+b1a)NtRVYA9{hfdpYQG;NB7a2apdeV*z14TK2NV0 zhB^C_aF$8HjXpoczeX26Uq^Bny0+|sdIMcCq;uuZm-~+IM7!U^ut^)RVEZQ6{7!hK zJI7XH^PLQAavh!Th~s_lqjifeNU>>x(Z7Py{@1i|J z4!On1X+d_jGJ|?sqFINom^kuB7-)W&3;8zol`ej>Rs@k#aG&CoDtMF6omM{`3*|&v z$=E^y|Jo}O`ekaNUd7_l<|gNG$=ymUF;BsHq!mJnCjP`WnJG@}{8M4tn6PITxhal- zUWM8?uxmT?WhrtyBr;dZIVTdq*kp;khh%!PQ)01p?NYe=r=n`J-Y`;rjb)3MAla@W zx^(LWa~+O{&x#Z~;xwapR!GiN)HD$M=q%dz7!8vu4X}UbHfUueNaJz$%SN?kUEu6s zA~9@2f+)tq52#s|E_vZc#FOC7}j zWZ1-ekO`)gZ3?M|_#*lta4R2APCI!RQt6(Meh^P`yyx50fAXS0Q%p+!k~JZ5veV7P z(w3T7dpiZk-YFxInGl2uH7{glB*WJ)0QI_zLW`)R2C-JAMJ?3~Vi> zTgm&}ANie;oCtq6FC>eozF!gi9$!I^-a}zkwIvp=oXvDksF-;19Q|YUy6ybzf1v=VxkZn949|Zi3R*eK$eQ_izmZn+`hlWuZSv-;37=8W3>vko}!6jAyFTa0*u7s#?gv5Lt z(fyC0v7pQ!!$2yd;k$c1MnalcV;{Cpk@>*eC-$?pt$(cczgbF689{zNh-S*R{~E+c z_~Y7*o7lWyCT@mM0P}?b`n$it^-C8p^Vd00tu*4ZC9~3#5#-~8cdz&2(!&YpSiR8u zvKMyj!Q*#6D534ntSsE}(vsl*))y_tu0Zn|MUc)66EAKZ!ajHRW=aCgvepxky82`GYdZgRbcuq_e=r9L|ua^hYA!RY;j{}U@h7jc{Ad~4x zOiR>ej=N3FHLZpl=k{X5uCuT&T?v!Nk4733;ObcxMPhss^FWHHWJpbnXUlsl23K?n|sfU<@z@ovXdMlvbIlA<5~ z_0Rlx2qCDI@m-Cm!G!S9X*m5V8EWdp=@&qW>alx~OkMk=&6vxkAL!BmVn4ZZ9&P)@g!wp41OsE(8e+ofiaWFKuLW{QT;b5lE?z8_~ z>+@0#KVtKNaKU2g+1xL1Rg?A+9@3GymRcVKsRh;1U zv~1lLwygi&^-F+`EmA|Drw=+J>F#^f~d?cHlQcm1BkIG9-4qE(wVurk(tk3*^B zdH$uuhU4_%V|d4OJTg@cRH{)A4Qo_DdO`%wpT7=cQ!U)T^B9R4N+?sKK69p4k)(7l zZe2krX-|BhCtiAn!ob27E!(!^JNuA6DNj@X*`H!Y<@I#g&%L)`A+ z?4wXZOoIg7jV; zo|%Zldv@XGqu0>THAB13y->Nd?fX7|PhVc&IEF<__j51JgqFsa=xZj9?AU|@7w;j9 z?@3er%i@0x18rDPQo48NNiu9BC6r_(24U;D>oBvjV%}xB@JF0TWimC=gr;cPxTsXF zRjU>_d-eieyOW0eL+}~p?q6?RFl%>%fpyPiIN~nDDK8nezmj418-V$k-%H*8Obg1Y z>WFl^fwpyPph@!#Il5rsvO~G!ua@@1uT|+PL~66!Ct~@MGuJs9C!%erVDJb?eqaqo%EJ@rjpY z5B9k}3W-*P+anC<+!D3e55=>Mo3+M`x50RIVISJ`nF{i&@Oye2bt~0l^5@r>JL_ls zIPfPt_lkg)rUqW$zl5&bphf%j>eWS!8g(&w;bt<6wB1VzL645j;3CG`u^n7$)4Z%GdzdnQD8|epL&pTc zvh*kn@6-w|E_HDB{u^wX+KX-1LzfOMQLlC_)URJ3H5<3U*5gm1s;Pg zw{_~(N1ZygNHhlGZD5o%p+ASG|KfEaBNCEGfpeh-6^w3EM;R*HDr5e|SW09BLoa8T zzBK+!+Ck=qmTfygPbnM$5pf88bsJr~^v1E9&q;`*u$xK82M(Q#SlY#<$NFM)|87{b z{W4PGQgQw8R`l%iBmCo2xLJsHnOvUl?BS*T=-zu6Ji`;Egygr5EJMGaroufk2?1VD zF?>KToVezOtdua$%W3F0U;y@^=^c_N_yytk9$LgZPfsTSBP4~s#gP8pvG>YTzMn*FSw0QprmO(drQ!@3 z#~}mzVe9>v_j6sh!pxhd`C&CsqlGu52Jq?frq|vkR{UGW8ydr95w@P0SSomevR=1`eNCEYf^QO zP9DI3Zhf$R|8=DDot@jZ65R)mgRhv71@I9DxHZ7z{sr{w)*54GuVk*#WZXKtlY2x1 zTzwY8vgw#Vsw;YqT7ZzGWZXEj3mtm*!peOYq|N*FqA0j}J*fnP^{Me@zHAxCOEwoVCaPxctA8#LcdAj2S zX^T&I2Ab5VkZ<+h;^J48unA_(lU&g$PZ`olyeRmRKx}faB4p4~$wtTGe`OfL@gSKJ zvJeFMhoDU52I$$<83*?r$D?bT@!S2b=v7f43s$U!cdQ1c&07K+r65fGWiDK2&qAqn zbI^xUfR#HALBq-({dx^VQJ#3W%^1MRejgQ^bY>32&e*nME{f0TccJIf#r+2X8z-8>7)`Mc%%=dj13`A;bCYc4XvB)=FVF;VT`BtuCRfI0tk4qO$ zB7h=XrB@Gd`N=b+nN~pgQibtqeE>p2d=X7?e~(@x5aWIwhcDd4q;ca>tHv2b-QI$c z(-uS9wiHHA9}WNO=XioSjL{R#ad^jkXvot3gyi&nm%qy|Lj766G7BSIR59aXsw5d5 z>Y|NZ_hRuhOcjRue@z{wX<$tIN^Qi)MC0h`l~A^?jeWaUgZa=A(BUFlcO8vWPlm$5 z>lE(zYGMD$T_|m*jfB9im^yD2LZeeqSV-8EGjZ|IdTd_11AWFW#@GQ(keL{QB@6c8 zhwjs`U`#8Dxzq5=sJ_^;XAf%q+JlP9L|9ksz_~Y$o~<8~gcLFv5oDt5>?}WdUn)H5 zYLcA~C1avwqsG&$#(UMMkT_^+YDwk-^6whlNR)8xzyUl7H^HG(yHMOx12MjxFn!^A zNs_2S=~_~9B$m$@kKGsEV8zz`Xi&BglEUBNnR^6k4;g_OgF7KP;2AdUyH82434UHQ zAE8~FqT)~U(5ad&Jnx>S?a44qoH1OIh;Pz}!%4pgsqY#*ZAWJCG`)T|s+W*U8bbX2 zkzvyu<9ZV>e-KV=-2#ue;yAQ>6YO|8fAK>lbQ!h)uLFBSm(uX)EH&)gv

=TO*Zy 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 X_~U703gOQJ|4>uWdRYCy^2L7v%PYCH literal 0 HcmV?d00001 diff --git a/docs/installation/images/mac-welcome-page.png b/docs/installation/images/mac-welcome-page.png new file mode 100644 index 0000000000000000000000000000000000000000..6f4501fc9e83ef43b0683280675bec41d6fd7fd0 GIT binary patch literal 155430 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ෆ#V*6|}@uNhU241QM;M)Y3jY9CSs zuM)f%Zdf98d}t6D1x3*tH!3uk#v1c^ZBOEO4S%mJLnSIcoqiqf4yMzF7a>;7d3W5- z*x6b%L7%TQEBf)hZ+Rb-Tn!fe<1*U7rPFSA_i@}NT}o@9c7F^nWA;M>*ByKE^3%T8 z-F(BzA@^?Kr#zZ;^JC~;`mov)>@fJ_|P7oMT_;Q!{=HRkxg}!mes<5u9ss z7})*IcR;Wi;1hz0!`DIG<+(2$o%;rUMexj0toVg)?l@J(!&Kc0}%RI=|X-lM=?{I(FQ0vVrAsy0y+^ zb!84G!C_iLIJnW8gzB`__;NNLH^cE4hLQgjLJtwzWbA}wJ1GJZBHCQABa}TKUw%C>^0kS&na57#`yl>c5M{#aU7GLlj4iG7aaNy_o#fIBxo@*sFEoC~GP>*72;qD& zkR<{#?AcBvqkv=U+nZ%*)06Z_@f*R#cRlQe1Y01(QE)4#6X98q6&2v~MZ~j$wAaKt zv733Yur-f{2}o3W&y z(|IcM&v_wJ_3I1c`p~S+Sm+#|{x%M+@KeRdJK)9-AN|cniye$iM}F1)!nK^Ai@L*3b+g%tHTd)96GEx9S7zOx2o{|11M6|!fzc|gPn9_#*aA5b zsLy;+>DzLH;q`cXkHgo4O&=tDVyr<}kAG%eb`Cu}E{b3(Mg~-oHzVwdls)zfq|-ij zZNkU4l-)lhZTCi-3?vlKdwVboT{!I1&xM67C8C;W^|UDV+AEMD$?1Kz127sC}0UU03^? zy?3`Y{?#dAaR3ecueB{#t(sGqA)gm=`_{JHVzY=`jWDRkOyx9R5LT*B|1QnA{_*{* z;D{2;c6a+2aQx{Zch=&)sf0~S+iS7RE%I9^><%7MD$@$xK=#q|o4yw(OHAz0v^xlI z{%c4ETZM`oPV5LXW{<0*cj2sEFIpaS{;sG)1=5{=fV$)BP zxBCeo2*ke3)|q#=Z|>V9#p943$Mn{SPOXQRB~d+p$R|i@S~=;(qgmURh8$+5w_(F`~?bf z270b8*(dCAU8s+?ZeT)IJ6kNIsDHn~@_+Fs8Y7=ui7R>FH0xYSt-R6tZe&NKQcvfl zToc%bWz5h~;cP+~L^x<(4aiR>-QI5zblg6kw_K4mp*I>ZJ1M6%n-c%@m{V#}t$(T> zme=$RD1xQ0aUMm;S`CDjmT|s)jBd4Da)+tHUBlQr~tsrUVy?=i() zOXRS(S%cAgCREsR5RjIRFl@;^-!EU8Y`vPlt5L#T0K4qwN6S79?e*3ZOmjlnYsTsB zxd`MmT*~sW@Y-~7zdsm8l}EDfCf0R$+LrhJ0Lk(>9GO=7B_GPLc~Hae#&$nISL02* zq%)L}SV+e17}uKY*=b-;i&<)zcYH2cAEQo#WUYS3`=A=@y(9AG5J!Sh*!4cWa_oGl ze}-x@SRaF8cj{&XxA)h-Zh4<7UI-hZnM z6OWy$QXC8J3|Om?Te}}n-hd|G>%l*-XxuFk;5R7FRsy7FyNW0sdq3c%d4x6dVbx<< z2=6^D!8WgY7|ls#65;Ibo{}a>+!hxv$U+5Mxk_QL-VlzC?HPS}my6<*Xe}1aH4xS_ zY?Nhi!0vA`ah2@KhBbPnQE_$bcgGxHB}*zPFBO&$en%cBTb^IZ?T80 zeqU>&Zi*Oii~tuH1@#Q=jTo7(Ottc>F6NDdz zwh^Lqex%cyO^TRJP;#ab}Xk0StR^I`9KIK%-LzIMi(1FgDi79xo& zrGp%n;SWowCPb5>5-fWV1O;n&a*Dro`d->*n$LusLENx0H{_G9R(m(%UyZy&jxDrR z;e&b(vdCK++J*V|rCU^-yEp|-3bCS9Q8F>d8bnaSuvl-@+7?3MGadq@$ zk6-h-%*zagEe2Uw%91X4$iVr03BsRji;VFn7L;cCv5%#oof3~!W+-=Gz-*x^z4xnb z$dVx9O-6_h3(}*u(hMC1V?p$j8qrLUt1XnN_^;{jLi~33oDuM5n&Ym%j%U*RSfdMy?jws*@66fOg4F;9#qwE<)MNjp*JMd3-fJ!?tzasA)zxUNtXqe>^x~;oU5M56YaVNCF+udKT z(+P>?OndR{FhwzCSZ=rdclU=Sf76Tbmv18IS@Z;4eX6IctR0A3D|!*`K@Q;TKcFrocTJ*Sr7 zFd1@_lQmu~!g(&sI3m%&&#;k{b*oo!)<&s>e)zv$EfmEp-|F%k$tom_$=0mYxMwZl z#Vp*3&PP^4f522#TUeVGq9*5Lhm2_n!y*=exv@XD{1|^f~hhVt7G6GQxKoLLv>{>1${2`=VSq{`2krtp~-Bu99#Tv zHL&sT_3etGAaqpRp85oL#h%eGWWRZ2Hr&Wby=`IP!xlw5{DhMud1=2XO8NR$2ZM6= z`@ZxJnMR+CK-&a;mC)Ca&ua_$Bm8qH-F?N|QuVr;ia11#~F;pd3_ zYM`#AD_ay|2`Z6LzPdn`Jd>=o*rFi3zaJR*SO`deCafHuMpB4PLg}7&dxEFq!e~#8 z?b2DmsQ<5JedbKNI`cjaabGhe4Ab!jvcDmKtJ~g-A%~p^27c0Fwh|7#`R!{)cG@CTC=m3+kBuDMDAd&pKL@VvgvZ5P@)RIj9_(vcWCqbvvAM&;Z1_W zT{TCj8bNO7CsapE87lfp!EXFlOqPCQd}D*@Lk*T&ddg%N^oEd?c2}kguVYPD$VhQI1^+<_EI93{50digH1e%6(ETG_@_O@p5Hm@G#0r<7$s$6HsC{p6IalcL zrPY0>%HFUFOH{9YFO|g1csO2`&3+dAS+d#SeB-nKu#g*5+Ek10z8SC_N`pgJ`#`b~ z3%)k(?z?NUf9-s-Qx$pQhoc07&a)j)Irgzrc)MBjO5-z!`%dfrez#%L!D&qgjd&4! zJo8Q8KN>tcYa#pTMrI{7BQzn)q~rDta-WNQwo_N;4sV6v`PFaW)?QZF*OCg^PAdp! z)19P_Xj<}>+NI8OijwbRl!5PR0VeJGpF5!@E0RLjHB!}s0!UBbIqix^K`=S7B@^2_ z=yqBVh2D)aUv$|Ca8gWxCiAaaQNE@&JNpeNCC3}EzrcWrb4XsZ5Hz*%nup;ba5pY8 zq2G!xZjmDYX)Ru4p!zBQbXO^?j++9#&}s_;oUkge?3wrgiPrvz zS%ui)kl8}Os7L1vB}ag|hRHnivB2Hs9AYvafd>kG%gz!{*StzKYUfbep3uP7(tJX3SB_^bsn)hau; z|FNnuAiptDlGW3G$%iG!YuA2Ol@Xm~pXa=-UKV?Ak-_$4DPJdcdClPpP+$90sckKKQD><(WT3=Ax# zC-F3fUY%@2+Z{`jAsrLWP0wCcFUP0A!kX_$@ zf%&H;U_KHJyj&f`6}%Qk#P?>Z_B)2NS$D&@FlZlOxBAl2FJnfmQ|j33YY#E3aGIrfl}4yeh?d}+L|UgpF0@0pf2KDk^8 zOC~8$oICLJQNrQ=9A|w9lK<>RkLCLW zo3g`%bLu2uu{)RV82z}qZ49R!6^NJ_(!HL|oVMgJf82az$v`BG_NoDe@-+H*oj-WE z8}*5DU9K|$8|~>=pH)H88Ne1sSL$%Fx?I4 z`}o}Ch?S{#hi8XX6P`(nUzol2U<>>{72Gb9bop$*rV`15S641 z$`UmHoM{FS;)rdUQaMLB)q0GpZ62vgk(DvEIiJF2hJ%v?*CiE(DjmX$Hc$gF4RuCj zWF)?`V=-!T%vZ0L(T?^DtiG|?E+#zasM(o& zVX})Wf1eS$+CrCC*D%tW4s%G}W;>DJTEqd?;)PDBOl3qvxe(Kb#UQ$Jz_9RLlrdrye?P)dfv~J33r$&>YcH+Y-p?I!(0F#Fb8?BHoY{UhIAA;wqh zu)-}h2LvHOye`80520^aY!AbbY<6?lF2*8Zf+x2;F`)JbvNpS)Y}F(OA1ra6aq@Jp zFCP?i7Q*dKidN&uAVFAdcf!+QNsIP;Q}Na)1&K&CTht-Y(oLU@jUn&&3hbepeP~R! zVyN~R`LASb6T>ZkNR58unJ!i3q=Qn!`4l4B?=+v2Yk5*c1WUH-G1OJ<1a&#e zrKnHLq+k8$PZ9bG9#6EmwWR|Qo#%hldwk3|GdEGivD*|l9%IwUqejfDG1BnFNsK1J zdq(JamIQZOq*&SvAZkyE@rMc~hhM$*wd3OX!!w3i`xwGSmV+FEw za!fxK$IPtIP&ZoIWdwNHNrsi!VRLgF?_c0i9MNjhO~djiLowtVx5Inrw0jEfQ4Pi3 zc)M)({=1;Rx;~z|IdpD3 zCtUR5sI{aEv&xP?R$Xv39=jkFixFLP_p|j#j%jQ0HN~$Zrg&*8#4$K!6Na)o3gWSS zd&f?&SMTa;MIF7@zfy||n)}6-9ihql|G5P_?$>7*w}+G}|0@RMKZXGHt9nrK;{V?vf(9fI!Re@F;g zf4Terorv;(=>a<-m2F0v)YFB(g}S+S z=E)V1&4LJ0;-y2Rq=DiWjMfWfXnw(xz9>rE|3w_$_+gWhl1LdDK?=;Wa5jECMG*#a zLPBiT`T6-@W&Hj*cFC-y^^&kJyC-VG1PI2Mae~Kv-nc}R)|4&!^5y^x98aq|PeDD9E zE37F0^A+)v1!=$kJ3~_*+Vvr&2cCi!1KiMqt2fqbWh7{RTW4$M`kIha9Vyo83SRzhvXwgdr;~TOOwfxYcUx zCU2ePSbMWGWde zCyIP2f>|V1w6=5K^U;03M__24=&h$`h*d7{GV|aYNviREc`;X7Le2XMa1=kR;cEqJ z+)h7W$DB=%=+sL*E--{G3(0*YIS5;?H-X32A5LHyV2BmAtQtI!b{@WLE4q*#u8b9 zD2H}@i&h@HTb2**Zlsd9sD{ZK9JSg||H+Mx? z?uQ`)n~$FgIpSMB5d`+_Fek0Wdd)T7+uYv#*HV|STOhjW)yN#snn7Z&_m=Ci?h4N* z5#$rgexLF(^3bjR`#y$Ng}lN!e?7d5=;!F{Nq@p)F(nPsw4gWd=KHSK(`eBq+irN- z8sVb=H=06pYg#T;*OX>XBxkM{>U1|ZoCj4`Kow_kdca!k`1M5)&dk@lf&JAr9|5+n zzf;I2VmsMatB9cwqXqd#5b$cKpW?cwqM^euUv9cDBw4UfE>LT;_OrgUwiZ<~hEVwZ z<(?hvWxB86@P1#g!Ym?2WZR9@;DaJq?_Tab+ES3=tzcMdlKqc#G;~KBKdH55Y^i6ggn=`igMTaT7B!E$k3qGad=cdQ zWfFXmjxoCt<4k=|)A9_Ja1GCvFxTrV^ng$0+!t7-1>xO9^^2h|^YixfWw{+vj5j*| z0-c{|&t<>I#bLBHgsg|9O6Z>^9&V`X_k3jiu-BN2F?HYD^VoDtWJIj#`shMm>Ii#t z$uX83&{t0>s6>783l*~QCTO%Jf+_81Oh`_}G?<>UrKPq zy4}C>;@Qm=V70k6DdRfWw~YMcrUM(LYr`K5YGYi@1E}Bm2*N0xL#XkwFk zRp0y9LTx-$;kGkCFhIS2y?&e4V1lPcGPZbAa`}pdA($0|LeehvP<}l%&rHhN4Zr=d zydJ78>3n-rz~@Kwb^Coit5r?bhvG}1fHbZx*so2vzjT= z9EU8|cM=)QIJRPdLvf$1bYGJGxGiI^)j}iLLj9WFm&)ygm$IdH5{9B&495?!C9Q#A z|6+xwU{an9N}Lq=Ei1E$?7}~N+5nec}nDr6{}$3E4hfXXvThC7YD*%^5F%}wc$L0hpw z`_3wH%e5o4(aUWv_&8$klUJ`@8BZv zvWpdXdq}`)d`-=%YR*L6!3UQ5@Cx6g(VI=6Mp`%10Kh|yGT(e7jNS5xPYslji0Lb< zVCu`0sTP**(&lZpdEorz_aMK3#9hb1#aB(F=Y1iC*!chkS9U9-$A3pSvjE_&RU<{|W{`S=&*^R0` z+EJ2^c<14~FInaf#tXe(8i0GUT2dGp8x_{%Zu&iegGCe#CMs!k$M2T!HU8pxgCXFT z@xVdsbD-P;sGaxUUKvWeTU56oxWtByvC$4R*Ym`SRIe0Pv-5Ap!opWWsEgLxds({} z`fA3z(}A@;hV#Ief95IFDNZsu1hz=>a=u= zjF&n0eNb9BkK-i@h|SFZL3%3rjIf%V#(Ch=@o*Tr;=}a?%O*r=0z5X_TxJU&2i@QP z4bK#NBpV1mCSJSWfc~<01l4NUdKye6p`Z~k%mWc}`8+u{Zx~9Lnq7|2+>GaBjLUUp zi3Zc%pFUVuI$poUsN5v^56eA2LD^`(u}&Sk;pq{(WtOg`8b9D%8ttx;7~6|vEGba7 zCWGi!?$5pGa@<`Bc{u*g1rhHw{|i%cyHZSXa z4k6D+^6w}I)p$l{76z@Z$l4)Ruh+*LTLuYG)_Dvv$KgZHd6(;o8QVz7mZjuWu#xC@ zae|S^=O5DX!-x+SJ17|-z`;q%>imrAxWHd`9Cul8d~QlEg#6k4_RZesi3*vz=mw%Z z$|4hu7%v=I*K_yK>!Kg)$mQ`y*>)$V=ULw`WIy72hgY=cm+thc87UB`b`6YqI!=tO zd(HY-NjY6g0BLr@$}AK2hUv*EX{t4eTOx-KwFbVt?5Cz8zccqnr=s*n9cVY9G-}m> z(b7?53?s)F-#oNiu~ip`TqL%3TLA_w+ph+UQ3~#^;P=mHkYxScpVE(JT4(D>aE4J; z!2ICBKi>Bh$jZaMmb5U!;<958@jxwFhL1B5L@i=;xx%Oy8E=kN5&KUDx03J&oL%~2 zsqj2=rFNoQj)uRzf>)?j4K3K4?akd*$afwPZv>n%w?Ycq@a!fk=)vDOT|c+)UEjCd zUT*}DumG(KjpuM7mKdM$mp86Sf57w0u8?1?&R-TpOKQBje5gdV?Z^{OGW{?U<;Drj zszl^-J!01A-a##?XL=#b&2WZocLmMO;b>YJ-XS5dy4Ags3JW<*NHuGJ1q16j%ViK# zXy*+(al@UlS`EOy0gJ5=6cn`6QUm)s*xh%ln^vUHsS}S#FVD?}kC@j(SQX?%Jd(ks z$4|@hnh%cAJ~|_l>HYfMwx%Vze-a#C{9NgBV)jkD z#yb^<^xCDrx}I;@u0ARuE`)PbNwR$Neq7YnsIy`;^Akkqm>5OzXb+@{y%!NaH7rQIdClu*$04L+r! zkzwBo`my*d)X)v>DrBAuZPq?b(~%`-2l_XkiFX~ znw-A{X_x~)fBr_}c!ht^Mecpp%`{*l;hQTLseE#v6!_eMRoJe;W}>#!*K#D6&*ch$ zv2HegUhpp;o!(r=+?%LkA=49GCY+~^^$XEZHu)-NhSu35^$c&lO-TY8>3|+b*b22$ z`&Lj4!dhuDSo#xzy;*oyw~lJh&{R)0py@OT>dQCZL}1SlwREo}F8G>mFV<&>ZfP{wX9nzuG zqMw8<&ITFLE}11$U$tm8t`x2F#=3207f~*S0W(@WXw146({pDK5=5l-e-vZa9WJ&J zdAgp#9%M}!eoo)koi4wNV4Z#sfe6p%gfxmaEvx>EE|xChukPHhnrgHoscemA?Ma<- zJljsf(|E7^S3E+{Fa0aAHy3M{m~bs77H5%qN$a^~@Q-h&p|vH${nR z=qjSUpcZNgB)<kVq5f(mBuA~D?fDUNy@;|U%+IvOP?_0Wq za=Q|F#%T|4^MKSw-opIB=?F)?t}`%d913`NqdM1k=9XQM@(4#Ufvxw1xETv~Cqx;{X@G$}?D8&{L`R2Q}cd+Oau6jX=eA#jpW2E8?J zbOtUqPz!0#5`x#C4d=$iN$zUz)_co==#pgyad+6P|0w`vFP~=O_kf;$5<_99pZ~V7IzFwfIBwUY9{qGc7r@(+bgw>>=%0bNb|W0@73>O4 z6w(BvpkW%q`>nBc_fKq&d3yMzrW*~IV3iS))^f_J)8>F28k5tLtSmXRI4b{*#M2Jv zA=`tNPta#?W&&gW$U8imfU`hhSxte4^NE4#rih~T*&x537N; zp}xB+bV_-(?H{bnhrO}lNTBBd{1D1nHEqz?ghdvgi|*ISl(ZFKsuoj;BvfZ(Jl|mu zpLMWuHUu|GOAE7x15R2!&Mt6y3o@nQ6}6IrUsI=z77M0XcdL9YWWE#V>G`I$TQK(R zS~>YyW7%vQ4kU@=u1{6DBC`|8zIdZ~0zbv&oo#7%=Vc6pC|C5rEsfC7Abub&P#dMm z#N8B_ob!6f2*t@5aFP=r_K9>wwgBwH7mHB#EsFLhv-#>%^)ah+cw=n_%SFT%f~X>A z*=HbDi_90-TOhFgI3PAR^{%e3{aZ>IZ={lx>1PTkl821Pf*u`ykVh*dl${vuX4TYs ze!!ZlS<*4(2r}_$H=l=5$nA?e}9CDBPXK0IG z23crcDG5C!ahU|}y6QEmC*m2yu!x4@S7sypzzd0bzYa?hDd1t2Vmj;-^4tBovxhaj z+GK7Jbxd=ulT}E1Mb}W6U6;s3z(gqUg$oKwD%Ro9EC`i^%>gsTj|V%X4e?P(`LbE# zvtK$bPP~X=0TtjZMv9;IW7us_!Ls6`edkoKs#0F@2yzq z6NkTDtg6OINh7FPo$q3_tA)pYBhR+mmD@dDf+(sWqLoVZ2-a?Mn>q0RX21M<8YrzWiUX1pwhRa&n5SX zbhd92&}}Sr&ON*0`s6Fi1e1~Zk*THSOTSlUO|S*=cSP1;Pl!3Z#%R=tEL0p?AM5q^L~ukP zH#In*r{QWY#9-upP~a*OA$-Qe4@!$6zG<^4O4*ctxX_8{jK{&uX0fU(cTSa z{7;L)d2MczAe&$AdJPLt>7I)NO|w|l8U9a{bcLp3S-SaOWW&2mn(LvB|%eUQeV=J@X{3gpaO zy>pO&l!$ifYGPwX3U^6d+Hbpc>V3a^&g3wvae46>rz_FN2Wh5a20f(C*T%syLHQd5 z{9ooLI~a~RHn))6@UGa5kGii&0Z}T-LKNhqHALVUCp%)msBc|3*nuJi;Ig?>WemSx zvAsqy^OH1rxu+lpRZs8fiDeg}50b>BJ<~v53ODRfc^x>~nk|5kM1|V%}uJ zb3V{_T~A1qkpbdKp(>5+3>_$L{=RqoMVKo!Jc8S$Cl3T_hofj(F7d)?J>rYuprSd3 z-_=(eE`)KSqTNG6wFZYpE0hOAh*!bY?TlawIKcVD>RR7#t+K;fd5MtlY{N``(rdOFAPT$`mQ(~;xLVxA>qdJ>f zpi>Po406p{yax8|YbMBxrm$~zS+94>SqG)ACSix3B$F2-Y)c+GO*c#7XFU0hyH`y# zzYUE+#W_eLE2E7X5iz&mLnDB^)+$-`wr(*Q?#XC$nXAhsK0(p2 znxs5g`!(T>FpDpL;Zo_3@`qY}6oYCmat1z!g-Q8Bt0&KUGJhhO2$bd>T|h<-obzb% z@O^`q?S3j_8(jTk$;3_qn=Plswe6lsHUb320Y~MFyYFUf`bH(ZodR zdyftE9A6teWkMS6hFwfIUs4%AHN%^ZDmf6@F3xZU215W0c8#91DytNbgax|l2DdPB zrAAK|2Zrz_J9W_j;bEIA*059oXMjrCuMk3xZ`B+_T=b8rv~1eG$~n0a>6g*H_L2&P zViMW{*$3%I-(OOKNoPxaR}o_C3F=uEYckvKRU|%?hG!6=Q{wuzlWnVP$mNZ_1|9+ zH+o!}lpK}ecvIF*3;EY@ND&!@orBCB9?KFdRt3|{fC)QNdK)%fLkuxJv44{ra-{zFw#HsBev%Begw5!?tRc=z?7F$Oh4e#VcOOFrIg-xF zIt~GjgH=_Pzn$x2<7MEc1{-4ThLDh#!O6Z?GzB+LTga)PoIG7R4Yg=r6!cFqykG*; zv=Kj==6fNJS?e2m{gMerae0rl=DvrU^E%b@eZm1xi#6j1t^lO*i~9{)B{5b8x~p6U zIWSrfE#n4!`rGm!nd~DIbh7-SkQ}=EG4fygC1g1L*DT{$TX_k`Cf8KX>#?VvatQ=C zJ0)a$4_CPi_w7u90jG}NEB3hpI%B)hk24u*6*bonOl3x>WIzvb3f#8jK+T2&Q2fp1 z)8Dc3ucY7fu@RnC3|A}Wn2IoZ&BK88wH4qk7j_h=is6;>;_HWT0O#h?sH&wIkkG+E zrtSlLZQl!e3@ob-kh(9-%PQZZ0Df1Q;e2)r;5k9!aIy(OKruCDq@lu; zoZcK*=mnjE(GTWtJ^n4S>?tkg)J_!#{o8Ri_GX7w2Hy+Vup%ip%d!e?tm|{{u zBxFSn^?iY-p!SaV{Tvq~C$H|md<9CmOxY3p6%{vH{g=QB{`Ux@3@eG-+oS!}*_A); zmNMN7&W}jW8Yqa*Y)FDC_k?nAo3&;HOKvLI3w~Vxi|A4KsxBz;kj$}&A4w^yBAMRY zCv%XWIHenHXcKDsJPn{_eK+!UF(Ugo<$h|85NDyU-EVZ<{)QO0;$vbZ%uArFoSdBr zM<)69Uu5;yIgwfHpN5hJhTY3wkPKW+8w*tt4;Q|6X@=~%B?LofG+6{bRAmS;^M(=* zTT#@=N|f=#bBmI<88!{*VR?)e1s3Y;pA)3_O$Z@)nn^2(yI5ixDbMW~ZpoJI-Q?@n zy@c||p}I*)ztwxtcT40|RK-ngm023k(ICwJs|L4M&?TK8ED(1YeSf5`uFFF8vK4T9(q@WE% zfHTOic+T=#ua6*7^DGdPmza9he_!u%!NXnDQwsW=l=BUcjF)~NDA|vc(+{rG%E^t2 zHJ_BQGXzckNkYV5^5#L}=Xs(S5$8^^0{cWjF>{S}?}tl=OpW1c^MS%$D!~q_45AGZ zR6_qzStXnBCVjZ6#Xd^fOlJSN;{DAk8EU0<$9mxQT*U>_W6XIjr#iK@I}W?l=o64` zM+9-Q%lQrF@jVDT<_DoEe^*pJ562EP_5c!w2vp9%PrH%?;24!^fF+4f47fbfgq&2a zn_y4U4y9War-h-xrTo*$h^i*?9F=a185Xqjt)%guIm~*0qIMnViV7^F`7Z@e?T7M{ z6X?gAgTw>B&^+ct6;FP}dU1r}AJ`RSU7o%O%BJg^_ksBWE7Q$Yx6%BgT9ZYybgtZl zg+|%hi_&-6)-L4B>DTz_3DpDR>j{rzY-d<`3FdM^#dhNBlN!T6W}sz^G~(|VFtsu> zxjsAPzqlJpAPKs4gQ<$#ef9thfLxPx4k#SCY-*vyL2cG=O)iaq`vEoQi*>k$Ws-mt z=KW(MPM0TjX&EeA}-0Gx7P z@=;01$S&r=N$Rdi)=$$;Sym!bJoit~$MM1uzgS6XpeDg)w+p^ZD&t^^tvT5&W%VCI z#2V+ZmUuW7mB4xdIAz(Y7LxL>>1ua`e?S!52kO?!ipId;;LgXSj0WGkPn3RbSBlZg zh+&~&q09>{huv3JJ*A4%*#y|z(-G88x`)xHN3YR(Ja4}V905ET zcQm=Wx?A8m*~#s@+BC5SN6!vaW##DJtcG0c8yn9R#crHIrP$x+Klj{sGUi87<@GEIjdZ=Nma*SF9&%aGU5mImF*!>h09=vUEhKr za73|U<5I8V z6!({+5@ASv?2iOjRkL=yYyI8-y;*;s-`0`FAd?IYX6xmK7RH`(d)50s;+rv7=#DeT z?!sF1tVR6r-i(otC%t>umm^pU>MuJ8|jqJe+LliMx`jP`pc$1UzEg z)HFlFiOx)w$Fx|h7-W%IREJn833_pKHpgLTaN!)~;ZD?Kee&5F+kf{l>nbHMnyJr8 zjeM-kp)Q9@LSh=}jBUxtrDT+w?@gy9{ym4D-c^c25>c=eCl@=b8S#vba>l18vrouH zxnJ9PXwsqY?#0LHM|30@-2kgXFCd2Z6vsDNsLm)9Gt+hCGP&$MxYI`_rb0`q!8{yW zf}45dR2wUj5~eF6#=cveofejJkyxg4&lLNv{L>JTUT@_&%=Nrtkl+mRr^r0onX4Xr zn^lP-WoD#AIXM8|3SvGz7KF@|B^lb1VMAhZy`g;b^}wIj-5G1RzHWy=b##Fb;LF&4 z3x{}e+LAhWWo;Y;!UaCp`}@51Px_MeDIf3UJ3Pc59H(Xmk-fh=L?`oBHwxH(E-AXh zN7ChHe^TVzgR?IPtfEb8U&F;b;Ke$=K&Rs#q!veBZ}#L|0oZ!A`z4J)%;vqg3d!Ps zabL~c*3)_La>P`!R=OeDQL?aLf2Wd?tjs>=e44pnouwK6_adDr~cj=f4lS_2}!$>WaC41A{9oZz} z^46NV&Lhll*6JJC*?8*kJ5VrG!-al43xdNK?iKqT#VDpTd`O4$xM`0VnuurnVGs#t zygAtFVeN5+)e6AnF5?|i_R``>+v&;e-g{~#5Lx8ezxLgqVe0#NOV_fUy3yBQdn?c7 z?fz51vw=c><7+MPGRj9#a`*jUAHXNro7t(*b`=*xqBEKvf%9rmrXj~bsWS@Q~{`Uvb)l&>r8zlk-Tn`%Sh27o7pIK>Fe)azXzY2L+k|R!+TFf zfBu!365SJW>5Dyvmyo#D`<}GUNw)Ksz{e5T>2P6#Nc;&^MO}{yu%l^n;|gsCt@Zrt zh0b>qb+~=l(6U1CY|eSIx`5P!9FxBH^&-sb+6V`NLyJeqcyUr(VfVr!OE^i=ns%BBC9~Gv4a|T|ldRD+dk5yqZ<>lfBf z4-{XV&c;Xg9F_aH^dBXnF1FR)09i6Xrz8Ytm)*C(RRF(FCpU#C#MJs+hV_=$cbv@l zX_@6&ncW&-Y*Q_*|ne9TeJ%}skx z&Q1U+YOd?svkgE=Ry-YHVX)L3)G;5f0vvN&%dryzd_az#Ef<25Pk`+ZNPu8UTz9ko znTs2s8ydBXULVQz1L+7m_F|Nqo7>5$Td*fNH5Kq!O?U*|_^|VJ7vT1R5Uo}+M!AS& zUu%7m#A(e_i{?T?DoHEo)ztXsh~UdTMfB!Fw7fnU?dA1G&eRSJ8XuQEQ1j&ow_Y7d znO|N$lAT~|ZPpESJQ5*x!ZM{=za;41&&uF&B7#QR;nN}Fh7c8<8rt;FZ1Sc^P1GMb z=-7RHS$BKTSJ78f!+zZEh44f&MF40VH=30;`qRCBh-4>x8olx)0I*s ztCulHphHbd_nocT=}cHO!nm(;E4;^Pgc3wB8^N%hjwcpPPO-xiFET*hmY_3-F5r^l>B zet6xZf>ZM$w96reLw@yO)vBi!?d^KzskGgE6rgD{^_$EA(f3_v+YG1fY~U;6ifH%5 zUX;EAV)Py9tPpz|cl?}Ui`nu4i*>{hxjYqsf$LW=` zN3F1){T>xHG)DNnpW=?QvySF@{R}}xO*@NFL&C;7O@XuMn+$r9c|RW|&L=m=leSF= zjmNrVN&5rGjL_u%f@2~H_wh(lE1xvzb?|m)Y4D9qpdiZF@pU3urfy3iDe=%e2 zS_1?Ozz;b)Ps84T62oogij6BRmLRH2!f;{-sVT49zupDtX~Rl^8cA_!B|L&?^EC23 z`Pz@$V!p`|vAAJxMJx%AvAzD8e~-89zeU}LV~FU)+3{}$37+_v6w~YgOei&<_T#n~ zdYX~p7}Nk=gs5Y%SYpX=d86H zC8mur1{W)Yx80jDs<$Dq?Pe*qPy0|$zLQ4|Wy8kpl4r;(P4-_;yGqEiRoMw96v7Q@ zEE2qvhRGKjFv5Ei9fOpEmCFi;;}>K6A+0By18kd5PrCKY6}pJ!mwT%hyhIwrM3im$Ci zeKDQFej5D_O(udclg!y$es6e;y0be8<)*_OC%d^U!4pV1v1uH08(*R zx$8M}~_SBg28>j!Mu(Zfm z-pc24J>`PyW^4)8Q7pO0nja4C z`YI)YLd#4WS4ATk9b>sYBTN_W4ln9=_)h|ka(sY`m726$d3_y(@u4}Ahn@LrA2h#D zhPFHWKTn86=K@|>diX6f)q56c&@TN_JYF+e?cfN*&I4mUhG|VZ-$JxX$lQ-jG~Atg zM)zR0?W%(Hmu56QmlIelc1zt`ve~`gwI~>du~Y4GB)yETKWpunsHgN|k$uc|uPzi= zx^%72edD60Ip4yA6Jf|`UcT`4NifVP*3;fPR?^m1%%gdwzxfsFP3=1k+wMp=a`CV! zp3!Hk)A6Zw=-Ak5LA&SmJF|=df~D2>=WR!)z~<($${X}*i&E){~I#I6Juwj~sp_JiZ z2U9@6CWnCYzHvT2BY9#l!Th2yUCAiq<4$_HA%l++@5WnvxU4%0(%b~<%0+k8EgWJt^8F>A2&~HpWDLa4|@#G;O z9G^>!EbWvkqPn9mWzO_Sb`)o`*9&gUIlPsV#yVC;XYT-yzo$);`P3Ajb00v^c1OTn z8G8KmgPx(WpfJ>XPt4>>*}kBx^{+E}qLcBLO5p-@Rwhe{Oj)opjU>F#_rq(Wyo68x zSThH4@!BE44`$Bn>mIZ7Y*MVErA$ir6GB9EkvpO*9)X*nxC0;RQ4yb^p)H7BDnj31`ap;@4!=WFlTp z*rBN7Lh33UgJMVM3_?7lP7&UYxK3!dq~5?qZfP3p-GA%nH*i)+q>jp=759kieU#Yq zlB4EB2q~t`59CgKUL$jJDiPK|)GK?8^v%IU<<)ias!3sOMa=nsS+ddfZ3Q8VkR6rI zvbH}7zjcEak+L#5AvqT>H+sCEkug4l1;&SMo!lJBHv+ah0m^%c@%66x;t@P!vNQYr z-H4}x1SvRHyL~Q5uQ$7;R1-pGNm0^Kt3PL3bl@{qHAfk{hj!)zwYlBx@m_9rgp{QT zDJ8N+W@dqHX}HHET&dif@8#c8E^7RVzLx1ZbEu-HUt?k;$jHV`X6KiK3YCT zWv32)nbOHL21YNC9l9?mXTN5dG_{#g!>-eZSV??8vysTnm;*%%R;8=8Y_>%pXK zoS&`n)`qqCBse9l|C~G2|IY6#%c^o&!y2A*lgi?vZ}zf5o3mx6Wkw_3N0!S1)u#uM z*j1nLj~AybhlU-T1QYuMsWMQ=ibcfJ-6w&pETUgyqJ7=6qtkmGsiL%ZqZA;GkRMx1bkAkbJXaEqF zLzmYLZft=R{|lKdgrFP!mj%x1_$2H;easv#c?LYv$BwK8A3ftiJn5YspAy(Czw<6BaH+BG}( zS}gm|;;yJD!=-0@Ub)bY0#y3Yfyg~yb{K{k3n^h&O`o&N*470+A$enZHjs`>G9d`E zr&;z#SHGUzNuAXq`V;a&<2i;_p3zIs=achV??aWDb!Z-}&g+HAo;dTraitpRDN4lp}5#g*0H0bU}+v1@-^vd`gbaaY_-z&lV zbu8jd2W0}`!UTv53!@xF?FbZ8LduL*L7=}w_^yeJ)RplDp~2(ygfgOI6W|;GIqpP; z{6qfX00=;YpuY#rGe*0~j;rVpdqUWAVCh3^1z;*Z$OXx>=rCr-!s9`0mW z-(Ob5D9VFQ{(KHio1@I#BsIcW!x{w1J@Q^*VLj6IagU~3dAJ}WlZ zYITFBv1Yp|3;^L#^>%nJ6NpWUvy;eQKv?%Se41Zd485mJf(k&BxuuvZ0}6vb^hTuO!{sHH=0| zlG8Xpq>NY8jXtYt8QOfL43RSSXy$!pNw+eCHvES2EB6i|g=M*ikpCg(J8H9{KjSoR?iYh%1i&FB0Cd&HZ@;EBFb{%@Xs zi{}Rrx=XPTC=^Z^dWuT?XP%sXA+VrQQi2 z?*1GlTuz)|!cVcp*?AX7;Mbs96+6JqCQal%XPx5bKN@Ue0;dm*C7L}86QIVja zp{!V#sIdK58G1>PwoXbII42ZiZ5#5tsHwe+6)cjDi(*MNsY3-Sr9}Okf-te2iOJ0& zpil$*6rot)Gi;23xb18)N(eYql(2vWN!3-hfe@KUX_3i5q%*lR%+skjwv43|A_YF# zUpaP)=6ZZ4%rt zF?=qz+($a`faRr_q<WA`>czveAdm>XG25z4)5)&R{Pw z?vZ(!@>d+8+Fcd_x)|m!!LLZ=&y=uF`O~alzLL9?;FTzf&hdUb|DmjssY785F5#ekJiM<%6H`?m&8_B!jTNN>EX8ipOQ-yJ-Ca za{}eWHxHeFvjPZ1+{ohd4#~1gz-j9==fK|Z%9pMl9k1c}E5s^mzKUn*(w0J;&sv%ih zI@ft!Md7&1g0=J#cGV9qo%O=2>rh|cfPt!kz-~?G|M-l~2>!*vtgVVWPr$SMvEDW_ zQs-E1QA0?waYvaIM*pvs{onin0@YsKx`}|$eNYLoixavFb6Jw-$^aYfpNd?H zXMqL>#lBI_E(8MMKayf{3R+QBO~e{)&L|}_+Fz*!L`zC-YLL=0TH3fksZwd88srN5 zhjEY1C~1O%$Z&EC5RsApo^zT9LvK<{T!nC==|y%SCi-J|`3s6vZ4b z6$MTI(NS{bRJFU?*DS|r)EN(@KS@87>*dxjN*IT6mX_EBLPGZ6`!Y$(=6%D6-e0(s9HO%j{2;c)jFN#}lg7fs#Ceh7;vk$Mz7&MWAg5~LMn0#>?1ValyxC?m9rO7bBrR4^hV zrSi)Q!GwT(;7vwaa}bn%f5MWJq9d}voMNH#YYE}7s0`^))#g<~9|5;kNWzGs#>{r7 zQba?S*;;~8Y3Zm009rOCiHiXV5t&;d`bDi%7xStrp=4lMj?PSo!E9lnV<3qs$m}wY z&yo>jB@twV(+s0ru2u7ddx%X{ik@CYwIRF%Yvj=A%WVpZs;j7_5cSrf=ka1-VIXWD z%7?Eis*!2m#YuBh5_Hx1D&#gll}#<0S&|qn*1M{yB<0tY0VD29V=8vCL@bPLONoep zjE{qtK5D5{j||2y;s5_1NmDM(|{NzFeTkUp#McG8+ z;x}@dNR1h8_ zC*T&Vu=6f=?e@FBbWR`p9>(qNg!FkF2onw7-llhP|36^g&ZEocGMP=M(&==>2CAs2 zkdxk#Kt;rx#WAy3gHRFC0QW+MkU_En1_M*ldb}lOL}cMSHn%oXjnX&b^eLkj zwYH`%C^Y(Y9+GIIBcRQ|{XGH^P?G{Wl2-QkCS;z%F0Tfjng3HCH~3eJAVA5;>#*n_ zQK5L`aSiRLOl04`3%K3Y0kHl60Q$u;LT%o_j(8Yp7D?x*_B9QB21@8L z4-|W=Ic!o+#3Hl9y!KYU43-w6Zhp%RKJ>_NFAD>MekWJGDhx$#$o2rScm{E4X?{mX zNJK(v6sGyK$7-k8xJCFS6?Xf8@gdv`m z;!j7_lLyGJf6f*z9D+4lz@>JeHkeK;b|L+n!x<-w-y-sK+1rvsiIglUJT}k`gHcgdTQDA zdoS@xN|07i3o9xh_w$JG_vAUwHa36Jzv1uF|JBtqdgZ@UO5WRFEbE|atOlX9G-{}A&PA*Ej~QSU4Ui3y7QwmGhql4jk+f8 z#qK6U7)UV;w9^X##r3lE3THbomc+wG0-8FJX|KxY>1%xh>R2MhM|CY$!g!7#`-lq? z0J-dn#3_@L;z9}_y9*^e9Ze5mH6^6f)zxQeIG#;^Hngu^51HuFR~?2eLEhB%5Ovf3=)foH}y)7X77ij+*@j5Ier zc+mk!$LJfFl>Vw!gf0gJm|L5HSb>o>LEI9g+mb4V^C!&?l$7Md>GAQcYNieLuVI*m zzLV3C6Vv8a*+lkYmx8kW^0KbiP-v%P@XKk1UI#nbeeT(1vpV>(yC{tnd_YVr%{NC; zRgV72udsv$RZ}P?CPL^*m&(G%w53tp)%*8Q&=iz#&)Jw8Kr!ja6p@nPlVsbgm&5$9 z0x)}|4=(p(TyZs#4NM_&a)u(0*pu%?pgJK9a*p^HZ2ctnYJ;@YWQ9Q>ks0j2zWiG9 zp+UPJ$Vg`}hQx6>?r|Q+d7+^V?B`hcsxF#DyW5c_+*9UCR8kQV-6M*2@g;_DRdgg^ z#U`rme*DYbZ53+8npD%t3MjXO>A${U7>()yM~!7xurU+TPLWIP{EJOkox1sH~Ww7E(-I_H~ZoL?t9u{hkVIa+dX z0iMu3-NzjaJd`v(Gs2D+oIo##w+fY>M;{4ODipb-S4Z+5(Lk8Y~J- z4fVNrt^JQ7HD3-#+~oFpbha|EZJbVUnBK3{8rb%FwmZ|8XLq+r^hu58gCl@1xK!^W zEs+rPCfY1|!C0MW`iiKs%F4+Fhv+_XqU#7@DXep0V`T&Yb!qIV2{a}M(g_)i#U=%z z{o!HXo?Akp*|t-$a9F=THa0r!tWII+=^RHS-PGe#z8AKQ!0*E)!haPwkBkU4Kx_uV zhwlflwV{20DI#E+Sy<#H#!VZ18Qi&zac!%86`g-I1Bg3B=^18)U?QM7C~NJ^p>=Fr z5W7|3$)cLp#aCo=t7xpmQ)44kHU4soRNn#HYalQ--0MpSTmJPi%E7N8rxM|I=&|MV zjuzW{u3IJq%xFuFyWTR)J~D`>#60W7i1*E&2M;{36pz+{p+InHttp%CwBM#})}xHcc71*acoDF?8%zDg#jw-a?F@w6tkMnyXnX@B@cVvmLDCz%tU#Zh5FECPh?T zc^{*vJodlNuGc-C|JC#H?3s%ax~x zW-vx%Q!U5`k^&(M*)}6Nnk$%+mUSg^I3T3!N6*z~MgSogPf`@ zFSInZdon~15T)NRt!9*?3y-gERgs-Q=CBZLOc>>PE{hF5N&pe8t)(TWrTElP|8~lV zk)}60>vtW75b0Dfk_3f1Iqj&3t^fqI`D7X<)izbk2qnf71wpo!tO=5y_FthlsdAqr zJziMF16+dxIX?Z@u*SWc%NQLoxWb6!`~r$m>HH}(5f6Yo@zZv= zuGZh6t+dzwiqd7e#}P;BhAn2X00!4-!@MF@L~0(-7Rn<7$T ziX}0nSSTMH2^=g|-OBBZ$DIZm77!6qlR$-(tlB!5_ckNz*dY@Y7khI7?WY z&QctJU8J$;z3wm%RV()HmcCvsZb;f*L5=3Q>}K5x;mo9dyJl2!xNxMNIhJL2egMdo zI6yJDqCt?`9bVEX(tfiIV>9dWjW|L$RF0yW4iRFC$y%m5J`bM@?5^iEfx$gX zIr`XUXZRTfZ(>iC;Ibdc_&B9+XdZRDs9~t*%re93{yxeaJH3FO2|`4XhndJ?`N#9` zR8egmnlwXq5B`Dvmxy5usm`%lG@EwbSYa1ER-BQdzOl75UOobhI>h8vM*fa}!0AdY z-uu%P#Et5;`MGfr;k*1*77xAtZ06}oCs@X6#_ru0m-sXVyz^O_fnc|d>EfPJfChgF znaG~2IdvA#=WL~#-Q)Ef1nK*bMB<##J)(4go~aF`KfXQYp=(g#SY8^qBuhpUMwcUD z17(F}UZD)$pMHIJPhI`18hmZ-c6(5SyEAW$lHOt7OVu2j2>eA-B7a9l?Ac;Y=eZUq zxOJE(F~1H@;O0=TC{u2LJbdzWlPX#bn$wKcSy;P2??|*CFE>Gd@Z|QlIRtW}St=s< zsVnt2g;_M)7UK~g;L&Qr@~Sk~e)&qNB3BRZ4>P4w`qIH&&B-X%=r769Oph@%1{M|) z4$JgKFB98KGg5s1xpZfYo?iJU{&De2+CiVeAFsO$B%`E+*ZTo@yB8Nhu-Tpc6AN&a zzH1Y5*A-tA5fJ&GQV|Q9AuhQwVyOay2-v$|et}rCJ!rQ7Oc#VzLvM~_gW}@wI$SRP z^zPQMdvGA`(5A;m)fcn(SfHkSJ0grVZQS5qMm*yL&8_c%>AXjadaDxfwHWVYR++li z%KA`_Mh}mEKgPVi_it2^yho=8MSnsrCKVYfmp;3nWcS=Y-uK6y;FhyujJ?q2$&LK6 zjH4?@cNUDZ)uHzq%Plg655S)f>w?c2I+>B^isSd}LCg-(-jDXaU4ulPHzhjS=6Hm* zg|MT%*=b>NpiC6Eo*u=-zuX6KhOvzB_g}uXcN-zwu_XcoARmwJ4+q%V!`p7rQM$x{kDHKxC;Y6f8m8ZGP>O>ufDyr4I3#BVefV#`sCr6YqZnL7}eTugy}RF z0Id0}M;8DZCWvEoqSk&hLI?zv1(tXTkghiFdPiC_^~c#9D;x?z<5A~4kB|+}?(rPh zZ$Ri*J@Z#w?378nTvD++?L}A59l@6O?zkw(-C`fbvZNJZ+l+oY4n`{Xx1<@o))|eOGElx$f&H@^n z$gt?lpwp2TdYet{3?j+9?b(2E>6yXhr-$9^y%^Xn&U}>#)wGXh=pSoWq+@o>)GifX zgk!$yZ(thk(~bC(AHxIRL*mfAgS!WL9nI*MFu38EzZ>eeb0gy9W&TO;o#y-S?yV3- z&&~nAqBrawM1eQsp&ClFe)@XOMeVUZwc9;94z$Dqb258uOuycxc{fyLQgF_;JD}a5 zaYELnu*cIGk)8Ets|}8*zMCY2`SRkv2eOE@#!sn7V_g~%Kb z`AxLW`F2F;*_>_#c8x5&-;PkL@Rh`%gJgxCiO3;~Hkz#^cjr+SQ7r|bL<3nvy}@AZf#1=P;B&kYHuH%%UGhAF zi^O_!JH}3AV!A?dbhrv^I-eVSUz_{0j{I(S;ti7xGd)qwJq1MPJ(_H;fcVa9{rKTg zzr3U6q$1iPaCJE0u`zf7!Kb@aIxQOYB0-RL@))in+O2;0H>;7rOYB;7M*6|!C=VwZ z_#6n#=pVD;`kUsE2ITbb+&cS}X-i-(VwY_KXN#rCfah8n4982!^E_Uk+q0Q%eP%1| z?3(UghPz_C$`5~h%W^wB$9gQRtf=@zQP895XNd!&5OnD&$q)h0h}Bu8jUvMD)Sz zABbi*U4#Yk=wDYBDm3feiN1Z6f7OixUk<8K7~4~oM~HTc__1$es`+L9)5B(Xw8pueI`XhC;{Lv&)X zogpj~lFzS-IyJZE@pnMuVEJIj72D@6B;4ylq|--gB8RV`y&btL&-;f zb92Hg&1UrX_wDL`;$lYQ*yE-;YZJbYWW0Nh&Ahwb&E^H#{bgH@)a$H4LZx4kZL(tK z&aTqa*hlfOdNlo=W$?1P?33GE`3S#{X+J%V7#1Ut$}m^ala$en&W7gNqUa4WN ze?a%d*z~s(^dh)QV_(Bw>>st+QLEojg*G}M{#8mk-R^x9A`~tIcEaKGF9KZIUtf9@ znVA-Llmt4v8@@VBL)K-}{747OnrI*p<`5S|56?wA+z?f4LUFy|4P55y#G_|QW}?v~ zp$zS9^%v@2av`78>JFY`+!$U$;UDcAEdq zm$^`Nw|7OW2`#?A^ZRZGY6p$Uk9Wbl>0v{;67>#eDwQ*sdu6LPJojRECj($HY5$#x zJ)o)6$e;g&o>!7a$kD()koInHht=@=)iN1>GC$In-La%CaKhSq0UEhm8WE~D+i>I- z9|IFZ-Eo`75sS^oV*P1AZFQ^ee{>0>H~Xq(cX>wlY~H2!Ty+;NJtm}KfaMUjz741p zRP=_1l;kK1$?oIAn-oZ8$lV>?r z-?ODI;w30Xd{9;k0-ss)N;Ce81sb7uGp4&-Z3q;O4otVMAjB|QTZf^iiWw}J>FwuYTG?Muu6*79nqI9l=1D{S(ES6QEQI zTM(lHyG^oB+M2*##$mzkd5Q@t_YE25_5&zk3hw*;P%e_GY{{F+49d1pwCb}<{%rSw za3*XolirJfH~k7t6CU)my;)1&R+?(*6yh}up#+SDW;b8nLIP`vaqq-f@4XDh?sEqF zI{+Q#a*;Bq-WdG;!0_FL9BRZ@Gc;~fsxM%|oBl;mPoQ-Bl-g*|LYV%7il0AqTW@1^ z*WV;EdNf&Qd<2tLqO|{?*2! z>e!5YWz>t(v}T$gI$TMJ3@^a#F)qz5PCgvm9#wxDvr$T2%^J@BA;qzjwHuq!T2z%x?W-m6k z)dch4i1tb8XaroW(21r|XPXldY-A7%k={j9m5iV(K#Z60`hltHO9K=5n2XdI{xDR| zh!WcRLYTjwhX9C~Yl$ClNT(IaO79-8Zr31Qlq0(gca=TqEPxxY52^*Faz#i05{BX~ zI_pbK0mb>S3G>D-RCmHz2+t()8}U*?oMx9|HkW7BO%9fZk_NvPgqc~Gg!F6c+n$!C z+0!g3^vDL;V?a`JX|z;`1y6YmQHNnuu6%bc_v3=e9LnbK8KO%tX;`@oYFPg$rF;qQ zD?%|hu#wK;YG`w{CA2O5QR?}G-HISWb9jJ6kVG)PEf5imS%)I5%+gRdBjX!xVRD<> z+gc-^k`RU{1?x?5W%oM(j+A_lFK^`KeLn|_#@WtBK@enhfSMLFI~Y-tnpnp+f8sseVIlU1XEyiGzvU)~qoQ}}Be-6#2Rx7o-KOB|iJLSIC}XdF$irA- zuG>4g9kHd`;#QCL8gME&gsRsW58;7J~W6uT>|(}5-*aFtKdnMU`m4`Pz0iH>ARPiKsY?{M>S2XyFMWj zpQa3rFNTFQvNgC!(F>Dokr=D#U*mEXUXNug6>LF07!&y>*(Z96yBua))eGRP1=k|9#lCr zIGJ-gWwIq5ezBSX(rhy4g>~`3i$fIW@jq^y9vm?`2F6@L4t2E*k{ZMK?B17C@Fb?y zg&HBtq|2)=r4s4fk4m>Q7!QS_f478;u!or&ngTj*qVToF_c3I7mj0)9;_zBu9>(vB{(cFx2o3L-3XRItU>VI|2RD5F#Md9jUJk_NmH%-m z#cZZw@Z{E;7iUKV+=*4_lk33V{)5OVlVrobwH`ZMti+cP7P3Bc6t#cSOQUBK-CMAH z1`n0%u&_$p4-UW@&}haQ(5tS$iAe|_$Tb+oL6`_e3c5%#z#qlV33&hlV5lJf1R`SKe766=)nkae={HSLrmLBv*{cIH5MwPg0k?dv@{s6+(Ckln`H zoQYUM>kpNe8#K;xq%;DVx{X#a9Qsh3sY#Y!Q&|p6kf{($WLk3nrg-k%Ds;IoI^|mP znG)oPxD`76P^67TrLYzT*^4W>@yVn}f;3HXN2utq;$@p>&)sr#IqT?fOotcf8ayH{ z3`oaI1V0g(E5X~ZcY%-6h3e_TP6jST#hulC5Zf;LRCJ8Gio_?@-sKsTm6w{SVA<}p za!4(Kjh1D3sLL~?e>Rte5LdA1^AXl!P@*yfCAyxhmROuCh-nj?AumY5{&uWOgkJis zFbQu-Q~K-)t<;dg0z-rFO}Ic=iA`i+)Zn>rO;xpy%C^(K!s&z0GCZ4l zFic)bJdUdMYILfuPUzA^ioSv^1YpP0ub83_ELo^iyM{tSEb6qaOsjeC=!={L7ZH}M zcc$+|r2svk_#orLUWTyi?un+!9*!F5pow*g`yT@R3xBx4O7ib(rssLkxUZudd)LTn6* zV=#020UGjORH|>tlI9~f+MYGYbuUW?!zcS@Tfl1n>P9Fud|Oq_F>VKPFIMj`qO2I< z*R0qtQG&8+4norDf02YVpG_7}zUqyDpvTMZM>>p2L|$zD0LUe==sX2*sx~{ZL;~yL z6ih>EaL>bQa(f1s?eI0=>{*`bWs>w6*zZNinqRz8!tH#|hVQwMbA za#@38pS7FSfBihj$Z2A7*MZi9F%$bsLTGHTC+2v|x|QWV8R%OASuoN$*N?z0EtoO( zB}kFF=mLXNRh0)rO29~Em>S~iNiNy#B1;_lSd(LnhbTCvIm1HeCX1ZgQ*bYE7lBZv>apxv zfWehw377TwEtD*MaWp$S3+r3L9~jeR_VZ)u@pFRuq`LH6u(dcyk^yqI)kgHKt*547 zR&aQ+>j~VPY#4N5uCRuwh_^CANpG!Byir0)Mhdn@?Pan?g1_q4m#-~DP{Axu*c{5~ zc`#1O1()UuakA@I2O^utL0C&Fz|QA8vKJe|S+g_^byrGwToQfMhRPzqiKaC(ROU&= zgCh|ovGXt$yh|EO9}gc?eHKD6PHvY@avic6hw=h|_Ku0rAC)0oQ)Rboqco6WC@EDe zM^vXRG0g`vr|p%m6&YpE7<%=Hgw!CBifDJMaKW0XV_f$q>}ZKOqN}wAMKjKk?Ohu7 zrIm?z=*rJ3&!+%d~&?B7nKGB{jZTu9Zg*cOw19pJL) z_k|fqYOy@|Au$pR+^S7gMLZ`w_?NIwlKa@(+vlw>Bn2TtL7Z^(ymD&)sZN_7pPrUY zD64O`95BV9ku3mhiO&Zh1L$XG@TbwqDjtjUxmY0A_>#0t@L*zKNJ!&hc!L<8U0%+4 zmSsg;D3B7;_X?GnJp!g!5C79+8ceg)(yPf# zi`s9Q*$j$_NDai8q!y3BpAiXuPi5bDXt+2&7D2o8oKRmQoJx9F{LRj=#uy#UbH{?8 zNWu^loWBdJipdwZq$rJqX}O-5vb>C{9j^QuIUT|jAn&nymy=D?Eo2c?6iQ^jLc&Bd zTtx?+o%ybM@Miah!mNmbQdoeS3Z6rK>AU>h1V>la(6JuV z3%nv8QPu(es+j~@niD?I?{xI)F(I2J)$YGC{55laeTy|WLZ7(l9hR|Rv`+C(nlzC7~pAk6%sWS}w-y1(8CP5At@VzCy|L*%=L&d29lviMGze#oSawEeDn zlko`=KIy+JA6)*FXrYMB(Ol`gpXHVT9y zl;yacsxY@z^a|@ZqPIl?OZbMntKlj0UG~ux2Lo2o&uKe*{CYsXYu@1O4d0P{3WP() z_WI5-!K$ej7S|6=;oOP=&`{a6{&y&{5Gh1Y*|`dj2z!wVzWz4wF68&ntcRyp*?-z1 zZ-)#OXA$`}N|yb6Shcu8MTH&k_4Slkf&k=>7rx}^E`9QGPFhT7HP3VR_^H!mCkaaS z(eqcOcz&p8QJA|UFSST4h@8wYl%ss1*&lN9N&;f}dVa|np79>d2OZ%VhL@`dH0dzh zjD*qC_P*iV@c=sY{1%ye&2FlMebeY#y+7bPN->tw1i|Fr1VnXSe$1-9t$(_`=ALU( zZ&bvv8+H}5H{OiwP5=#qR(D#7(o5(>&dkcLxh1fvch62l`U3#hG{Wl1I3uJT8CdN$ z$pc;-9eNBR_fDcG54TpGxM4&v%<8|$uOM8g+vmuz=ZUx;;0fV!#c+h|zJ^~!knQFR z^O><`dT%3#sv@j*sQrL8;z*(fGtusFnP$7G4iXFKkuqDZ4?rtmwODOLd9~ORB3Nu8 zRS2*0ZDEgky4IU=JGt6+yba$jd?KEIdH)w%<4kvP`r3YS^c?4oM!kj%N;@_g!mD-@ z^9@tYb+tBB>)KwuAjk|UsHNu}D>@y=k12JUi71%^8=M3D-|9xWdOzV%3_ztBDhPEobKQwy?85Kk-0C~16^ccyU6L} z#pOV5{*>iem?<5+0<*`(8IyueeEVIRsW*Qxic-zwgb)Xpx#8zgy)3v+rCmD91A2zw zZ+`ZatRK7}ud<-a1HWJ@of+$oPHqt7P^!n`diAaQghTTbu2x0-1}0WE%W-l%%g|7% z)B0DfXMonoZpX1I>3^&zgN9m@be_ZC`7?93Qg^Kf6VTUp4dMo#W17=$XMrcv7}0xcKEXp;sx91c{@8E|Cl%BD zLt-*h%GQi(?Ol)7Och}BcJnlb#FUb{o%^^sC?5G>`Zi)g16|sn>nVlof!o)1Pgk9n zsujqeA!kPanZ-1zFTGnT z%`Mn9K1;f-j+P^JVh7g4*jyhKye33r-r#!KCjtzlm+Q<*kjozEl9yFo)EA7zGgL6X1p6*pFxNKta^L(IXt#B z8jK~nC8idjW59)g%TPnt>L@5-1lsZY;&7#WQ1`=dd~ms)cvgRGU=fkgh2{1B0peyE zihB}w3!x~9#gl;BUSXJ8gV))DzI~UfO^Ah>i-msRTt+yMN1Kyl5u$IV4t3Q?u-br04LzNDNY406M2{$iR7}3h$m~>rY z1tBntATC+M*iUJVCVFVAo%;TeBVO%*8TI{3kj}O*c;a z>)vqId--c4$W7nfP1GQgRt0T&$lN-5=Vxc@j(lMS0+lR-J!n&E*UtSeWllSr>bL?; z8fZ*L=t1)!R6L{zABDM5YRjR5)>I>V`wy!$=)J#+C5s}$cBTZbuI~I9)-5=xHSl3|wcd%KT02T>*p;Eq;!J{t za3L>1lC~GtYg@}`#s~EP4gCSfp$9e{N5qbj@|Tn6OLL^Nn(d(rb?WlP;W=}6!Y!RC z&FABx`fzm?9l{VkiKPC)jhIBwar)W5RnQne{ z6aqeMUNrn_$xsB7YHP$ot1VebOcYHiAOrjY5tfW#Bhqf3I_p%c)$7~DLl{)s#NYd^ zd;!mP^4t$@&m??!&BHFZp%8a__j=ilW+1XbgonqMRFeJgE>Yri-Hw>&*0ZmasoQSD zt*@`{dXyb6jCgn#lhKkIqmVt?!;uubT(m+}9lyZLOyWpo1fIMZGv##R8@eVEp7|8# zJg%ZjNzrx>)5eV2!-Li4&w~Deq?{d?l6r^7g4MDXX0E)OhP(Sh&g@NwTVq!nd<%2r zxiEy8gau5;oezB}rMmdYcp65D)eZnU13TaoNO=u ztF17q5SO%WcL?nmjGB@;&_d9c5Y%F>)cM9Uo>eSP<_mDxtHHL6?zwH{CY;LPS6__3nwhxrMafOrUHO(<*nvYwc1x4UQfnty=PLvj2@%j z9D_~ix$;n0$6;tW1aZ0>lw9vXemE?4&xMQSg%nbTrP)#@?YP^p+cPpedtC-Wg1aWC zhk{glJQDo{tZq*IWvPkV$5mfMpvuKYes-Q@o-CT$ z{^p|y`(2~BQ=!%DjdrQl;4c?oBAoEM$hMr6mYP#GU(A%=MsQT+h>wNM(Fs)~m8F

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

;#SU@d%jRKw&%sO#w>f+3FN0qL|m5oNfnkE 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/installation/images/my-docker-vm.png b/docs/installation/images/my-docker-vm.png new file mode 100644 index 0000000000000000000000000000000000000000..f7073eebfe85f932b9fd82af0b3bedb80902894a GIT binary patch literal 136985 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 literal 0 HcmV?d00001 diff --git a/docs/installation/images/newsite_view.png b/docs/installation/images/newsite_view.png new file mode 100644 index 0000000000000000000000000000000000000000..27b6b1a4dd63330e4f57883d75c7719ae6f3acf0 GIT binary patch literal 21403 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 zℜ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 a=tf^iKJiFReE_aL0h(%hs#VGsA^!uE&_J^Q literal 0 HcmV?d00001 diff --git a/docs/installation/images/nginx-webserver.png b/docs/installation/images/nginx-webserver.png new file mode 100644 index 0000000000000000000000000000000000000000..941fdaaf639ed2fcc38ef9314e44ff2eaa38f57f GIT binary patch literal 82642 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

1+Kds?I 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 W8FZ^wT`=IX+w5&FTVv1Pc<>(!6x!JU literal 0 HcmV?d00001 diff --git a/docs/installation/images/ocean_droplet.png b/docs/installation/images/ocean_droplet.png new file mode 100644 index 0000000000000000000000000000000000000000..32edd5ce3538af7e21dd832ad27a62b0a9dc3982 GIT binary patch literal 26453 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 literal 0 HcmV?d00001 diff --git a/docs/installation/images/ocean_droplet_ubuntu.png b/docs/installation/images/ocean_droplet_ubuntu.png new file mode 100644 index 0000000000000000000000000000000000000000..668de5b5558459d48025aa61960534ff7f517dc3 GIT binary patch literal 26811 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 literal 0 HcmV?d00001 diff --git a/docs/installation/images/ocean_gen_token.png b/docs/installation/images/ocean_gen_token.png new file mode 100644 index 0000000000000000000000000000000000000000..044c26acc02f5849a51efe366ec6c8725875acb7 GIT binary patch literal 42803 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%0AIHh8wk2y|1t;@garT(QOl!>I><@=$1hPawd{snJ z>&r3Izp+B?QHGcS`23|HOo@a{yNUgCY?(P!XnRZ@FIUy6>1ei_?QvY_8ERx~rd zkyw-c>wJLPeQ4Z$Pt2I#A$xTF)0 zM{ApF68C>}Av{5|%)W!umdHW$1Ob!4Pn!p#k?`!X7c)LkvlSnJXg&xfuO4M+qRXQ@ zlZ5FiJ3Y+AQ{#N3Z^Az;;6Hx1H+ zi3luAz$+*{$IE8C0U}@l1}D_C3Tn*<>Bp|eJ*c>&e{eA*f+oT~NYGKy-m5*UlN&s% zzbC1SLRWylK!V_!<+C3rLZr#U;c8^QxQt4*cy2RbK1kAEq$MMaeBfvagP5e{bkjda z5wN{Wbw)6o;2mrTD<`C5ONqs0pQ)aR-rCC- z{Ej}N#a0L8#$=eou`+VFmL`9EYB*;+%gFt72HL6q8xO9r7)G zP9Sp)d9i@wmp%LyNYwoyTzo!PJu3$;fWOl3k`z5`HXq!1VH*n z^QEu-u|zmyYB7*ta|Y-eJidSM#O|g{z7HGIcbCd*ZO8u@l%Z-!zV^RZPDj%cjtv zaXfW1tEilQGMo(pi1a(a2*-sB3a6U}cDO!$+j2T|gK~@2Iu|a4!Lr;<5L4LHkPgRG z^eyrXHqnTbH?9u>{N_^t0sjV}Z6Te6cd%@ks-6c4JV{ua6s3P04B;9UCou&m2DCem zr;1KA;73P138NZ0#`EvOz#eV(3RMD#gogy@)Mm-RUzKu~p1W`4+sBw=0M z%t_mINyJg}x?!-t#3m1}?kqsbsxQ?WhTw$)1&yElL%Q(BG#pFR9A^>ZkB9X{6h&4n z?@JKWm0zvPKFL#Y+X1>az4cXZPg+a8POj$>V*aozD~K86az~>2YBHqOp`I^@k>oz- zwj21RPFq%(rB(uFe@lGsZVILPSv88>&gb@OFDt|Adh4PbK&GzOxp@chI#W`w1(^l- z+Szl$pPr@3%!dQ=wGyhUlNQdM-~zBz;0Lru=~fx-P(IZwUxm_t24cMkv(%V`vI}Ac z=EL)ZdNPS%H2#%~M9n{Sb- z?JGhwQ~)SEYP*WeiaIrc`B-$^_0$Onr)5Hss4_vETyDsiYTLTQC)!>*-Wxcdo3JRM z937=PKt@TfEgWAwsuFEvNA=vF<>g}8hTqHgsWYdk1 zGuU;ZC4->*Wb zKN}N?x1B{Vxln!3|7Cd`3b$5eX}j0_vP$Mu&S$BhX&@T&hB{Gv-9gQ7rvOMe{PKL^ z$TPKa*G6kp%A3510D~fpVk$Zg`KXxE*XzPGE&$dF$MUZ<=|Ebe-Wtyg$WRx5IGxp8 zy{>VKQEYsyjYLtS_M-^L=cIM(=U8lzmEHk83KT{6epF!iFUU8B#`+(L=?W;>Pz%=M z>0S+*+2TmyMouQOgjxO)Xh)tn*4~L-eRrf3&1q*2yb(DCjnMcflgzwZXOvsk@i%MR z*{eHF4;Qat7LTJ8H;%Q_%8rYR6CBboA`xHp_`rNWnThoSGdAMN4`cb4O&5BuT(t+K z`IY7z`PZKS9B=Se2=yf{`zVe&RuK z@Ne^NNQT0_z{o8}T`1C{4V{#+!9KLX-XPoi*oA9loVjxZo)__LjTZ9we%9pd&)`lf z=`1(Lr^;s2>;rrz_8*P@u&uMLyREl@Og5v%)qZ6u?=u_iY*8%`@3$-v9q@jgW();p zzuP~EHnOy}et2b9m8kM+*cOkIo;3mZ9^t40GA48OQKAWy5m6!K2%m-hN7;Iw9dps@ z3*^D@kt(0JD(2Yh9;fBKgaf&f&~pD_LF^mYe;OVWrhgIrKpWc7tx5XShcYTh%*0}` zn>}Bd>6n7#e&fYyff#NHhC))thcGLfz4DAr2d|qBU&(jzFA12`L8Gd~ts>lGyE+FG zDk&tRXjpMvP$K^N{hDUyPH0n%>cbvG&U6Vxi&EBwgW$|253OJhc)^ zg*HU5Kim+_&;eb*T9n(R)UfbIfpiXbKmFG-=3mQptuTCT2s*2ICB;ff2Pc7}`rxP@ zxJ|RpBQ#~LL!&GY0@z@KR`BM2-3#^JDdqT5{Wq44?RwCjuIrkcJAY@rHT_z@gr$UKPgYdwPyI@kls~oRG2evF&PTDIe&8-+-Aglde&Pg%p>Wt zds7_|t95^GWKZU~_{!++qLz*uM805-qjFwDylZ-HWOAoNmsHTAQ+HcZ3CD<n8Q-J{z51)oORB)S@pF8V{$Orp}t-g+N+giQJ>?`O-6NgI?bMR+Mb z;~sJ{{dGbSgq%qHoeKFol^$xFv&M^U+q_pxk=n5(3rB1J41EIMzTu%K<5U5+6X)*A z-QagbOX_^1v)I5yAfr$mQtveI2H{I|)rPf6Oqd38T#vQXie9i4t<8_qliR)O+c#C( zoXI|(@zm(u1V{iCm>Sv-^7k0S)XS+?GG^MnWnS)|eG)Bdk;}WRl<`eCn%+lAl z%GAkc6jD;sQY~h079Kio_z^ToEO<#QpBKJ*Y4#TK--iu~^_j_dOrp+k!o!mi?(~*F zjX=))>SV})7DRL;^Xlv~Rud9lH*}RHz(MADh;*^AVN1lQ2f)pXEx)o;df5|=;aJ{y=tZuh90Dim47+#)C zZaoea=;-U?zO;Af%1d)e$3y;&41S~f-McQIv6J9bae1$@!gQR;SF4K(|F!NU;->%h3ebAHZM$5UyJ z2p-GkffR(|pScP@e(FmCTcOdbc__K)@_-N!G$w{4S+^4Lf|Uj@Q7!!XE7a3##8s^deuwhcoBH~aM{`1@ z(;UQJ>P%&f$fu0=E<`F%M88G&AtX7Vn`Ip=?4fsl5ihI~x$_%qh-01ONZw*lm&}2q z)~tt9XT^&Q49r}yP>d&F(KxmkQzadXob9^BDu~HOs&~H5z7d>RzR2J&n%`Rw zcY6I-gzn!({({gwBeZfApwlNiSsQi$LkDK3X{fbrlNr*Oqy+D zb`3$^9w zCM)7Nmp_^K&DKoyD(3cjPKA3Ks&u+|E5E@G>F6EuF~2XTT>!)Q$JplW#n9}{a0O1d)tjEl>51Fr^V zQ|wL%?|ch>GUDQ)Trz~3379hQ4fpW&8rxoZ=4;WGY^IRwy9*DpgKs2%ZBop4FQ}lQ zCI~u1AyQ>_N3}`5_xJ*tX8tBU(PJy!&%gUPhnXMEK_dZmLV8WAgCmgK+=(R3ey*5C zP-Ay53_bRZPAOXB$QA9XP^w31r)t&<#}xrdK_1RYs8C~7lS3BjHGHI|lUDQv{@ZZm zBB;eCcf{3JG~eDDrp0Zy|M&Y$F<+9q5V(wD(bawETuE|4)&Ji-DLot=Im2&=d)J(; zBYH5fQHGasi;$qXy+;+U+E8>N3hK)Xs6oiq3rZO|Wj2Ev@Odec_ool~9LbQn&^oXu@Bd>wh^;D7h`Re-tarsEtte76VD zwt($w9bD*D3^lgrl{Aa#%}9hx#sf8GLr&0hXS$@3p`~)Lc_`Figa3{7UD1@g!LPK3 zQ-4VwY*85L)AZdU718NkUD8)_c{9agy2n>YrfdV+1z3<-JEkOnQR-BIeKQ*5k zMw6uF4#T7zgz^hHZS*2EI6&Vxe-{t=H=+{K{(F&{eW zdB^|Pqz#N6yf}W`5u4M(<#N5Rktyhc*yOMRRc1ZR5w!ob_&O7uYEsx!?AX0am7#i+ z!+qr@FB}7b#wFu{|8M+h;%P^q&t38r)q{P&;Gy;P9)YL?Im|dZ50auzs^|;eWB1Cx z7ELj1lpYjFm=uM)(7oq_3)TNjozpJnCCZ~enM_5s9PE@(7^NyGH8hVLxtmAgenWBp zG)N$6{w%zaesMkpjvW=8w*o;<1n)}xkCX^r!PWOwwPuYUZtEz^S@bu^Tn7RLkN|LY zg_MU^ogC$Yv+AHs2wJ4CBp%J`Fotpg*}?4udaeLZadDyUG&WvufytJ}epaoWtC2KM zHFXsQ)Xc;ghjJ20(OXEE_Gno|Jj!c{CV(aK*wGp}0!`I^uof@|+pJ2VGx8R6x{T!* zN~6C&B#FoqqDXOWF$ai5Db4sBOO>+P!-$|4$ony9Mpt#y=LB$KsZTaC$^7lG((z-# zvR5)-cV8F$=3c)AO;TKELdBuj^M95lU(Js}cq~x6o7=rQ4SF96UaO3yaJZ>C5kXyd zue~NuK4A9^+2yH#J70Y-J= z!<4nVfps3%wQi$-dqk)K%T-uxPJ{-rdk5tIYmOqv;Q2gsdxsvh{$DXpP7gWuexnw? z?D9m4)g3T09r%9A!<`06Z~tY?(H`3+mUwQ?fZVtr@xf9d=Fj;-ytjd8iew6T8cvUM zYGPb1Nyu<7)(!@Jbx^eXc-}*6Z8C(zQBV6DMdWjE&9uG-x*X{km`q(-8jPG%k# zl?)AhED4E{s1$4H#=T4#Pg+R7L`&7r(@p_YY$i`cKxOXwY5~6PoU*17Gx3XiGPR#PlRX9&-3zx#NN>^ z$4RM4#Pp|!mj8HU)6871#hdt3n#gJVz$RHw{q^JwvprxUi2N4>9u~%3YZrP)O*1>$ z6wk>>NqWNnS`EChPlXN!Lmjb%+mB$|q@M|3Er{V-&*gay!q!_F_?H9kc)1M_tnpog zEIFZ;YOeOaik4H|hL!tJC?yjiM6AkOZZy{5NE2=D7oqb&Ax~TJ1mTd9&-eVQy!}XJ z^(+>e?J=j4#fR8W?~_9{lA5!Ph@=aE@g{AS9aL}DrEvFrb}03=xp?~|Rp_u-moTUY*bn%C!VS4Sa- zr6h_PMEvIRzH|p}!+&QX6`Tt#lw?;UQh-m02wb>83?)eqc(6vTDJxNYskNOH;>@?^ z&1(X@;e0ZTy!QANLlD|m!xq26IQ%%MwSYrHH@9M|@0Rw?> zXiTFjbvW9JC4bz`8^^8jg_pPEc$|OjTS;1$xIe|%*jWDBT)hVI|2Fx_6=Xj=(bwJ{ z;+mmWU;#{V&t_*&I^Iu~NPaeIN+O{btv7t9tSj(<-;yfx%X^{b2XPd9;WvmqObe&u zHVrIy6GA7?F57p^z$^JA;&a|A3P~-3&nDyObB=(3MXegDQB>P%7ZxCTy=Se-F*CPR zf2FcBUKs}$+%LyGFQ&@k(!Z3b5n{Amn8u+glf9X-<>X7Fr?DWG%O;HfTnr^J)Wry7 zH%hgQZq2VIO<6OSA;q?ro!MA@$-IhyB+$xFO-~0|@kBy_S*_D!0(Yhtd7n;`w?LP@U| zt6+Gt!f;ULT~No(_k6~gj7&YKlVL1+GzZbCaS+t&-ELq>F!41+GOMQMTrqq4@)enc zYz@&2c>FHXfRxtF*fwcRR)jh->~QStI1JoGlC~xzvxH?YW5DgYF8HiJbfje7F9a@- zkE0>OFtf3F_v6P8o8NDpNziZ*oM17&dQZYV_p3s+#d<6{MvbP%L%H4WxHf65Lg)Tg zIKB4RKUejwkDS}}M0xpvx=AfW?(au>?>mY?#*%CuvCpsBy`n2v7Ghsl97_JAXLcek zSsOSWR=y|btV^_>7Iz)msNrZ&V|+g&;5yuF0kz3)}0sfZr-}O^azvg)mia%>G#G! zh-N#VA*uX8;2mRyny(VxNPRl3jbtQKU0@gy?eV7z3k(H`5<=5f1CNibi;ag{E^O?#&WNQW>lr6Ny4#ljMpI@MEywpbvz+yyw^uA2*J$QG zW4A5mZEuWE#s@}LS(%QVqN#{QL{;H;Yb}#<&qwZqMA4vsi*cL(N)dx|prgD+6 z2%LAry-7JG_HfO`5cdYVRP~lb7ID7<$V?2(A+cHfY$-nfhlEv7ML7oWS2~=JYVo@j zZod6<(aR??ArE!7TS9#fcX#(T%%OJYop;c?-u?=KecyA#OHM-s*4PQHJGcn_i1G86c zEgowFlauXcWHbHmvSS|{aqcf4wKp3CRArKnV);})j5l|UeLEB`gYaC;jcH$e<_Kpm z{?!BIK5Nxa2Y19)?R9C);qPVbAN8g2cZS00-ZrhVwY*6;SQxHghaDMDw(U8|Pn;3l zQ8hKZ_#bD5xZo?!47_!|AkE_6?T4;o-L2!`kZlH{zxRZ9|1SM}QNx+3G90yb_S{Ng z=vkFotCm+>0U~7JX1iNm6)lZ4t(a?c{%ga+%v5vgG@sq*rqk*ZmBl_vIlpOUzI-#Dp|1aH6a+bH zIOaQ;rKh~UZs}@jbhkcrJkj-LoE&XDTX}TcZ*?shIE^CWc6>DbS?av^gI(`lHV&!k zdO`Sn6uZ(JlR^*p?f2&@T~cYiM~kzcOKv)PdKs=PZaRc_OKrWgJ{Ps3*7kRkgB5|0 zqtNir3R_}tZF586uLv4{TdSDgJ1m9eg)=H-3H>fwYpZ>#c8G0#x`+F_|86^5&?(2& zM1ND)t<~%eEvDIzhGhGqjt|waIox+MAoXKzxa@PE=q=8x3FUd`>On8V{QS^dSkIT) z<2j|ubNAN|dgz>TBI{=Ynxo6QNt?F}FHVzh(H&%(nWCP%$NMj`%h>*31~y94P5dyo z<&P!E_P@?bpqUwq?*{Bjpk3DaF9lr_YA#PqkL)cJUmd#hsIPuM>bfyitC3$6>Wi!a zJylT6JzDxYzCU685~{k5xuivR>3RVccKyv>EL$~$L;7Sgtv7Wp*vW`kt&#I*R0;G3 z6LBHKKuz5<`^(T4db{xK^0M8A)BHxe;u~kg+nhOrl_csgTEKTT1CTjLw=Y4CWqQQg zO*J;=zzS)q#^#1Z*r{~%9cq-Fyl%^2rZbh@!X{x3zp+PJPSOZUifN zEx9m@ouU-%c7GuB*2%l8*pChb?23S47-nL%_2U>M9Tx3h&V?lnKUbWcuPPf~t5ePv z?GZ92^#kucu#kF8*uu)o+4LtZ_qGM6C=+UKPnR4KWMv!Q)~Uyahii+c5HM>GkpA>! zzNT~bxt*-Csgkv$ zt#tbHsx*Pv+H8e8&iA@+c&Jf5yILw{3~faRo%mZ|@eo(|^F7+Ue7B z;#Q-<%3B({M_$S}IFI{-&oLp#jVqKESCFL3=ztYJG9hS@RAkco#|@c{xyILs@a2zc zR0?@pufIf}=EX%YM?(6o?YG=YYexqLdPZ&ZE7D7U+O881BbxIlv8rq2%txkFWW~6# zP`o9R>nh9Nxny6MiE2IL+pvYr7=u1Hz8d7uy95soK&vK?=G`xq`8bPwg_WX5e<3_R zNb<8Q;5>ykq$U4ycQdZBNzr~#W$Lhq8hqtxG8{L^rew(I#3RqT)7 z{k^yp=sSzLl|bpbQ;c-gp-iT?;Cf#7HM0O4nRX5Ot`{i$=#&cVIaH+M@v zM`g>&=(0E{+_z>KDioEKJPGfPj*jXS=r-}e?7?NmHu905Kx%lV(Wp!=G1JN!OUd4b`407d}3Zd59G4z{}Z&gum%wiJ5+-AQut0s9ZVcxs_Ll2P014Cb!|FY>BDYPQhY9hPSYZQXHZPE zn$0mb(l(4meKh1l8NmH#ZR_C55GeCQl~G4(l4Rf`XEH3zfQs9$0rHbTZdk39B(>Xz zRB}OG!gIXt&j|-%nDJ0j$}L4k#^qSF>+9PEi@$=BK#dUrU-m!+^f<3JxY-BCi()o? zglAjcDn;+>%D)6`Mw_ZPL-maxOD1(&A9E9g218cVOR3=Xo6Z8@g(TYaC43ZQVg{Sz zaUZ+++1J`#+{C{gM*9d1EK`xiYx>376o_o|bRxn*$w6FlrafWq%MV6BTxa|tiH-Px zhl7h{vctt*k3^GtkMYTVV!0*W=1f=%R{=AasF#!DJW+6%zk#p($(qKDmxk;fTlhxd zmk1@41*AOW_``hEV*}~jH5$v(`{GbYNqm`tf@`8(_ zSUHVp9m)9=C0JC%=6WGN#ty{mv8F@ZP8!H$sGBU6H&qp>x@o}e%I4t6!Hbd3bXcnC zQ?lI#h#YUN7;i7vC(NKIvR1K zA%{~jI;O(p?vO?4{{D-klFDx{h~oUidS4aGesA7UHRfdpHU|k@t|h*WEP~hHA)|XI zHQTqmDIm$jxB<<6ie^4Vd1}_(yGQZNGT-bM_Z!W|=4UjVU@`?#Vi~!O*zyuy<+`x&1`#Cwq*Ii*`}kd4OgE-$70$-K?=IKMk?f|B zm@ZNLnujIfWCdh_+&oeG0c6_l6q_El;)qLiA=7bDo3O)>adszbVV>cUKSc)sl?34u zrQei%*ty4XFg{zgNNx<|@ku9a+^zxYf(o5fEer3K#g`!8+d4!nGp8gRG+K zF+HBR5}_4$|0cXZ(4Q)g*xh8fk<-HJJiG*&>#~~HiaCcxj~ND4CKljjIo!LTWZIt( z;UCqjrAdyCeak*pYiY){iIlA5nw+a@YVm@vu)X?6%7Z#q28yHB`h*mFVVB1A?0hR+3m+#8* zXymZiF$m_H{2b!d@W>YTZr3G}>72yLL2^4^YHiPM!Qa*ly~B3V~gue<^B>bW>q z=T|qJa9CAP%upK9lXgGzbQi1eLGoJJVmj&>7Z$Xi;5ol0mpB$7tW&DN?1suDO{e@;b8j%s=&zX{0bJvdx`QY$@R!8v%Cp7 zs*ntrdAf1rZj~Sj$lN0p_4EkZIv!VFdf_0z#&M-(h-6vT3o|$vQDSnEo$ga~GM(xL zwW8{ZX;#+n7UxIRtkEBLL0cQ9cbN5hGKU8SRclBMn_|^jwf&69NuO5OomhlSq)jJ$@JFp>CJgx`FpQ z0q$>L$8b`*7@o4miT8K{W$>axCzO&|>v(HR+}~c;Mm`jTE7jQeS^>UjN!dw|rV6Fy z-nX&p`&^Qvn^v3dDDAi#aNM2&o;2m^W_9xYtFssbWiRb@Nu_n_Lamvq`m38MX^KKMcW2<6*> zTQ*FJfq1YD_J9YMc;%gS*gvv?`fU2-!OefBHrRk=Zi{iU_N_3wDR zr)n*{cS_hl_WrP{WeX}p9(vzAxNw`0E~veUx%#?1_xyLFTzl|me&-r}gwi)as6Tez z4y7iWw{d69us;8#K~Ld(P*Zoh4B_<&Jv-04V>D(Odm_YU1rJL1SyA^$(Ml@W?U3CqiIxT@>YqqYVk_;Bn^U?*BeT}|u=1`cD z@GAQ<%w2Tiw3rEW^1O0GeSw2OxeEG%Eh6{rKkCg|X}OyIDnyY?6^KEa$%B}iTj)ip z&Rb_f80qPSO?+gTL`x?4MkstsHB<*kNGNUs;%0=j=jhfIRoN*CC3vM)4$!&)*Gerd z46wi7eK}{wV>~K6&%2H)z3dx~S!Pw#*_5t_%L@ov?DqA(@1U zR>XgP*n?scZUb4F90pzA5UefYbz_z)zV(p4myL4Mxx4R%?auU~8z!UU&W7-0b?wiH zYbI9fXDiV5(>;sWxz@tS5IS>C`-x3p^{BX?o@ zfu)5iFO)Uz>(LAJ<`2bjTG!v$rWmZPycFo;|yJ ze&_e!%e*`H-kHpsm(0w)A1&U)tYgdj&8(j?O7-+r;KWaFS*&xvX*tX13Que(4c+Kj z$GPT-w(9nZ+wZo*4^GBjhPiDqHnccowxRbEW;mIXZnHhGQH@mc+j=j)?e^q}x08K# zcD8zOuLwxzN)=g@2z0%rXiwn7vffn~FLCEwl0^i;W_ZDS;i0TI{ zE&1-k8>HCJ>+g%Cg{@0&kiB}3L3qA{!6kc?b6%raHTX@1;O${S zI6A1srv0Paub4ZTc?$38f88hgkzFymaPLhjL}V}hC56O8b`7>*y4mca@LHM8FvGNb zxmjeYly{}GL}0HXODqnfiXZ5x`65FmdQYe|D?faW*IBxRy@|d5CL*!gGo|Bflbo}5 zgDTc|xi{Tq8yehNou`iur%BXr6t!xjs?xNan;Gu2j;E&2dUi3sQK0m>I?wNgOZkr& zIC9SnD&#vRxd%2We1bCazqZ?_`~>D(r0~dTpGW9f(z*oKt&HQ&aqfmcV<`U`|D0hx z#9Y_;@~^YUi2biqGg9%WKog%T(47bw0n9KZEN6Ng`t##tqIIjcg`@A+Q;#lF|)sGjnf$FMvto|)yA&ADw& zo{zl=oJgc^wjgAzfWJ)7k)DM0l)p zK1#8*OFi&fc2166*w&_6Da)t52*S5iJ-65;0o(K`0((eI7SyM3nX7SqD*A^~?{s6i zL}A?~4{n1R&Xn%DfDit4k5JO*d@!-b-`p72Po^Dw+^H^|QuD-W)@!Mvm@4$lc>@lauk?O?3_VJhv z{ybaM`)#&$B?(HUiEEQEs7M`rW3++(Sfyn4+tcC=JWq-RramisBEA{((Ca9!-bnP# z@VYJIyC^Z&4ymT1Gc;;2#jpTLn{rPztYlW};EMEgZ2kadM+t$uBpo2HrpxgzR`gPv zPHUNjLZqgY_~9cDg-JZo;NL^$sgnKt^AWQY%-taL30}=>W;z6M*C4 z5DpL(-0YbDK_47F4$a-xV2!bVn_nPP{cCFf7jOEWP^gyvg`fI~@48Lh?rgB#Gd8>b za&p+Kf_T9zX;hIVZGhF_sZ{giXRsNN(PaD`7vkp3VEay`p|uHXsxX)<|I1?N(t)k5 z?Zz)VfRY7}&ls6n?{x#OK?WwUqT-6M%uU$vuzJ9?>^q|B6*_idoX&&aweWL^T-af0 zsMOXHCa=(~4O=*o0KM}ny~0cOz|adh$06$)sv({E*rvdT8fL_c3c%q`9^1LolnZ86TWE;<+RglQW#&)wf6@}9Mx&`v`@wp}fyQ&|P$SHRj zd=mz2S|@UZ&WDJ|p_Odqj7(VU?$Q>>N!ld6&~1#o4JMb$XhXe-Y>_pn{;3}^+>(7@ zX4M~-yhDc2%<5Ozxhp+0kedNh{MEy8>CuUfwJT*mBkq)`^lYt+KSIUiH zXSmRgi;u#})5{F40aThRd3iz84W}g_xJ+Li4aAU`=_lg4%`(vXf804z>x}tx7{Gkc zzw-QNL)X#Zq|2jymNgP>IYl4N0sm^>@TA3{T`%Q+5|j}@Pghfc3%KljnyC4`YUQQ!sX+RC#-Jr7dPt)tcxK)^U^o(we}bXhXo z^G2VHhLC20P%Rr)cXimHs;n)7zxsA$&z)^9_Kz*b^G$cl@N_FFvM(=AT7_W zVk+Bx`~p5dU8A@8TO4}HJYYan!S=2z=Zz}n-yzKGVKJp%9#5YZ_43;%^ z1$sV+Hf{q>TuqsAuP^7$?X%LD?bddjsO0`x`FpgXoOVs&sjlW@nKI99o#w|8To2M; zwb?Q8zL?1h<`Jixkf68#quu4YdzK?X>-lXCMn#8L3uRI~3Ng|<93O~IltEu-tEA4* zBeE98&RZR*w?^Qv{EuHt=erBKv{fAHcAUZnU^$jI@QJ2GOD z{{H@hd-g!AsHnKOHUgpMgPV@k4yT)I38ewT%u!uw{+$T_-F4*a6g=KNEIy&c@6~JI z@%?uQ+Vphx>`cBCRmf%@TH)vVF~nZ%j+d zd$U`-^od@+sM6&%g~WAfi&&E4$CcOXcYeAv@wM=0iAE4aMoZ@BGX(Gg={m2T%`B(4 zW@2jDFj&4C_RTATSGiZmNLnU9D6Z(uTTV{-UQ@+izG&aNMYFp(B|c~4(3_r6*7;T0 z=V14Vs{i(xVrU~Qk7LF(a=WagWD7@t{+HQD3;*;dHyg9H!^3hnZuiCwdUSS=l{iJ`sa#U~u_j+~MM`_@ zQ6lslx2K;x+=;JdR7|J{ey@2VNxQ z_RWn(4k>!9U!vnxij!(oZ@D9%Usi?F0Ks>88GeR;cydWQwx=-9%@yY4MAnFvYe2#M zq}rcJEYDh`CwY2aDEc8+E5FxN|IEk|GSkvmzZd$oQrxap+EndsTh3-)?v}IFPhq^nW|jr;F`}2-;DeSocXjn=s2(jN1%ieyFK=pDCHM z6^%BqCGAcby(^&YTvQUu5IW*J!&O3k{Pe+t3-rlZU|3VC(_?_Apz<&-j;sgT~LS#HSO5t4Y_l`~gL<4Vz z-dF5VgbYCn5JEn!PFL)D|4@AefUD`R=`rT=+M-gMQ%QPL9Z7mPt5`r=!SfoS7*5AA zAXRo#DoITm02Khrscfl0pg*#w#bm&zCcymZP3b?@{}nnd1_b^ei$S(IN7AsW>S|kS zYk||%PcJW7AYaJ5a$bR0a!Lx}%+NmbVoQsbMWrfPUvDpJana68IU^@1OgA%|Kp>E6 zl9vxxSDlV)NgOHFl^TI35QI7(56|KRw-TFzfLBO@mH`lRwj8N@mNsoGH-%byO& zX>5dS&NM=)dV=~0HHZVBX=|(KnQmZNv@3SryRxBy56F3pS0Noi{I&#&%++CiS&%;N zO;li2!0q9n-Ge>cLfN)|vV!`;!opIRbdUlN03sRh&Aqr66cE5Ob3nw~)~?ol4Uj|- zw*<#GSBG-#X)At?jXj*KhbuHB^^K{bwo4nfKz%a97|h<%=uQ9QMt=gV4!&2UnzHTE z75rqDTVa#@bIO#^#Q-7PY%}NN&9Zzz;?E+pI^;kVVlG5Cb?ds z#kmFI5l)7twW3wjyr$i@uDPZ7Q{&=!`}}YGA=+Zzu|srK3(@HHOgd`Jl)s4u=6h1` z3mcb)TpBDzY!)Pl%1@-fq(LRsftvAsa62yO1Wq|vfEVVV>XG!{W`Sx%p)v>L9b zZioVU+6c%94n6rr2bsx93QEIq1-b4YDx$u;;PkI6h zUaK8~&)ryXlTVz>ek>=CDBorx=<6wgJC*U$)WmN&5N;C2`&fwIt!!|H>#!}?9e-_x zn%+M$NL`%H^`U3BAN=AkE&-J^-8JF9?FSNtKZfc&#AfjEid&jWRq~D464qjUueOxJ zhUz^$D#ZdH9W#+cL>v;`<-vtZMuf$APH9v;VoXp$MPzi0qX9UD(6GJ{*HF4`#dkTx zqOP0dABPkV>aZYQh>q>+yCBGoz)?6-t{>&t9A4ys3@xW+*ym0F#9d-!Y+BCam zAbP1W9a~DO;TZ7u=lu`iC&!3NYDigO55FDBE?#N9Jaf9;NLt~^cn|t1Onw7~5;k#_ zla;T`(Jj4ye^S%*4v0L|MDW3#$cvtXS0SBc^R_onfXCte&xKy^Uad#&wSa8+JrG^jMr=aIUxL2r3$36 zZtc=^)HZP#&?W_ns}XECQ?$}<6ks4myq)y@qbyx-rM2OFjb>^z!{_ zx0^VE%-m>GCD<}~x{`u#%X;dn>IsEZ69?WJsD7=@gVd8UrWVVuHzd9Ye)z3}`}j0V zGcDoD9W-V)Y;F7c(|!s8X6}F$eJRopllRDCizJb9lqL-F{+s zyx+2r`rBNTTp_U+lOL1N2^mT3vajf} zH^}Ph5js9mKQm#|#_7t`H}B|r8XL3fzSZ^a`z z0_DGljg-^5`Nxfh8r8O7$Iubc{(8&Ok1|9KrFOU5td^sJL%+l52q8<73D4I;tCBt0 zEY)K+xH9KVg_8pKJ94Zd4QVVx0uZAt)*AZm#uhBJ+&UljR-b;kn3v~`Q3N$n2{fC7 z>M&2qUHwG;>XRuuK-xf3a-x{oj$jIuE{*f#Y0=B&1v&vvUaj+aI5`!@BbJ{|DN3%l z+|D=EVeh2elFnpPA)e*s$NcE0D60%RJS;XGMGtdQN%hvWsS?c!O~Ns z4n7{zSnJ9Jy$0(=i-1Xi+je01sHiW+^BY1*yoag0FK$(lSINyDinVX0`5u$1*X9{O zy;(M}^j`J5aOcZS6XOwvhr#rg>yYDesdegFr>v1YfxzSmInBHrcKX95<~uDYE_hk_ zEy>7E)%G?fXq)g!rX8d|L4BL*%$E(i>Tk`hy_I$TgV+ADmdFdhqLkAuE zqPb!lTl!nh_!}qA^|gy!+4lSKR-^U3Bp%yCp+2HEui5et^5{^bny6wL-#fR?EadN; zg{4@&n%TkG7B4sMA~_Z^;+)uJT)T`O|My$w1a;}&c}6}?COB)@Z2F4q9OZRJI#*L! zOJhZ{J5{27L`ne`#n1$aH5V9&>l&Uyi{%-FwJ*9SMrL0$uDCp%<`eyXd_`Q_w0!kE4YVZn*79;>u| zxXdi$HI-SxKRP=71^EG7>mhf|6rJ(N%T^>2Zqw*A^c3a;E(f0GsJS_*SLBjWcYcJ# zyf6%wGP-^Reg+<0bPL9-N^0pt~tBHNY{of@1TVl@USmByI-wh6M7Z;qC1Uo8JUA&=cY+TMo4t1|Btz5RRaprj zqCv<$vN;gJHD-ye|6xOEdA+uzje@#Y`vf0OY8VMgLt2e_PO17&;TjjAK4+ZC4i+OS z4;_42QkV34IKPuHqX4;MF8zGRLVP?=-YNqI{Ak61T}Mh)1Yb`i(5R7;0}gHnIDk1n zhg&T8Hf$yJdQ1hb`NBTh41KY^3u+w2RFpKEIZ$V8t*Z(-1mHR=h~1?jgcU#a6K{3d z_IBcPx`QhtY7d`C9+3)7&meGNQFW_McMbW})qACUF4hi1zpvB&Gf$HspQq3}PY*wB z22wlWM)cew-Nd}ccOT<#;)b(sgB#Oy|p#-;ZI;P#r2DXLi92rS_D0idhj$eBjS z=UA-+Iw?LDW(WD+9J9V4?d8axv6>fAMgiR?%R_-!g3mT`s$5%*EyjlOqFC00FY+o& zP^d)Jsia{EP>u5uFk2lx%1Lj^$$fv3dT+A(vz%wGZ>bj`7elZdQdaAq6fAjGTq(7J zvjcIk`sgQ;$JmQKdKZ#q@L4*(&v&A`n{FyGq>6l@x+O8z$ItvVwHk|dm1Ugmq$j=} z;@7Onfj|d~>%{g}ZTJEMgxnrI8oId2h&(j(RV`eA!iCdTXN;AD?``&><|7<}c3x~W zjP2f!0Z?(0tQm@(DSMjU!e^ZiSh>z}&@^BLBu*d#Rl_1!_0r zeg`zu@5a>MZ`e~dx7~i#7rS(_ouqCfZiE3xT|g}vx?3W8sId`@LFJE`W~v*~6h`_Z z>sC*09MT_)zNCu1I^W#e3JZGzRzI?e3|RfC>?!Bs>J=*$E28H)!he5OudQ!~m4g49 znRsy*H^C($Z6?cRGvM_EK7CnwV&ikKtAkZwZnpdW7_L`P>2B{_h+CuSLp}jQ#u#d8 za%U>X5O@{$pmUD0U`0(R;l}4@@7rOCPZo)j&z^3>D11bRQe6~EZ{4ly@&vv0aylN~ z*lkb|CIE{Sli~rzTymtTld`d?3Wo5^x6O)1u-!%M^ z+_?i&uzS%bV{pgttRd*Um#WX2=X$}_qM?nd+AZ9BB*wItf5CJ^3ailbqc?o*8*f&1 z?1|bkJ8$4q9N%k+&UsO~_)}Qm1j@=;oOOM(!1?O?C$?mjp`{KU$II8JqvSpM_ZuRH zo^vd)D?m-CY;@5AuwyVtY2ZdbD1uW4ph78rTqm?KX`mxGPq}q}1OS>?Oez_`wi1A~ z(VjA?WQ1U+jH}$!h-d&|;(v95{!w>&^ArjQsM7>){;Td3 zboJjX{-f?xam};=+sxhSrEwj9CAXu+qa5Au8A`VM0R0WODG{8~m?tg`Tl`Pdpy@P)-F8YF#y8Rv9W)S!q z+Cky3RX>}#j|bDRNf5uP=m0GjvzxXABI3Z_qaJ%e#>Qem{O0%JJ&`_uJ=c+rq;|#( z(`*)~DFq!L#w!DTs{Ltyv8O_xvM0moEr7nUO7-FVfgT+lJx9_E{O}R`^7-IT?`xj$ zqw(ydq$Heo(+3LBtQ~e~T?i1oQ<#oS;wluM$CBal}rFs1GT*Cd8=MjDo~2A9iT4?0-#Js9!(>=v)be68})ps>@MiksitBrYUm-JiLY=q(lpk%IKj#UcI}#aZg1(1!>=2J%nVFJ=*B)?eTUxO6XmxWi{X2NLT1JB>#me zE}nV8{TF5yCSjORb;EXt-@Eo2ph`b_k>bQDiC0^Aqs$hK4)Dpz=)XU@*y->oc$=!0 z(tKJ%;cN#x#z4@X6r1{|MHe2fMak-#+X7SAa<1Ah0eco1QjCG*RTse_g>Jl4DzB}u zN`s}x$ZePxZ0a8>4K63WI!3ui0b(!$x+4lJF?xI+0B- zKa;mLr!Y`E_phO&G?)5P+on(Y=52N=Royd#Vx|ROBVfS82+-uN-_|M8vVQp=0(col literal 0 HcmV?d00001 diff --git a/docs/installation/images/ocean_save_token.png b/docs/installation/images/ocean_save_token.png new file mode 100644 index 0000000000000000000000000000000000000000..a479058d02283f031a905a084d54092fad297516 GIT binary patch literal 51520 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 literal 0 HcmV?d00001 diff --git a/docs/installation/images/ocean_token_create.png b/docs/installation/images/ocean_token_create.png new file mode 100644 index 0000000000000000000000000000000000000000..95ce3e880a8dd836f598badc658e5fb41f16fe07 GIT binary patch literal 57138 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 aa>)AN2kw!J-c?N!fb_KQYgK62q5cI|r}umS literal 0 HcmV?d00001 diff --git a/docs/installation/images/virtualization.png b/docs/installation/images/virtualization.png new file mode 100644 index 0000000000000000000000000000000000000000..cd2ec08b42dba1525520b01b197307136fb0ac51 GIT binary patch literal 59249 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 Awg3PC literal 0 HcmV?d00001 diff --git a/docs/installation/images/win-page-6.png b/docs/installation/images/win-page-6.png new file mode 100644 index 0000000000000000000000000000000000000000..2420b497abb1788abdf60955928c65300832ec6b GIT binary patch literal 88115 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�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

J1Qrc=Qpg$&zVMXs4CO3L4maNaDxkIq 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%0AwRhnc0pWDCq2)lx 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 G3H*NnU$5B! literal 0 HcmV?d00001 diff --git a/docs/installation/images/win_docker_host.svg b/docs/installation/images/win_docker_host.svg new file mode 100644 index 00000000..eef284e7 --- /dev/null +++ b/docs/installation/images/win_docker_host.svg @@ -0,0 +1,1259 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/installation/images/win_ver.png b/docs/installation/images/win_ver.png new file mode 100644 index 0000000000000000000000000000000000000000..2a8fd9035b4028c7c1ea51cbcacd347d1112ce31 GIT binary patch literal 50312 zcmZsD1ys~q`?Uf}gLF62NOwxBG)i~Z(9$6iLx+HXl(b4Cox%V^ch>+z*U%l`aJ~2a zzt{I$>$jF`2`8Tud+%o-!c~>!G0{lT9zA-5`AR`X{m~=D@JEjxbD=zi|IY~1hB^G7 z$FAz~Qjbap$+sRol6>?^=H*+@$GaJ*zVFsD?y~IJOpu>EnIU>kTOIKZmA(3%oTT2Y zt>tSG-D{h+Sv70cF`N6EeD*QSl5aekoBZx9&^*oWbn3$_+Ck_j-yVk`wbSZ-C=Y&y zpmPUv-S=}G6ch3oP;z;dc@8~ggPr-bZaBGS9bY%YdbFFp-yCCtL>UBslnKOoB}rQ$ZRn1v`)8kSP!;H}Tt{^N><_$=^a&-A)owoPfBz^DOKGXH z??t5D4XowbL4R%bO?I%%$}$M(bTOPgJWXb7SYdrvgw^m<;g;2#ozMP&c>&pXTXh?^tA(ZsZOP)rY^Ar)=pU~B{+uS4G$~g`pu@i1j1f)Vu~ZUZLcF7PfcAN_q%8BkluASjAtENx~iqQ z=17}a^23xMVXWJVyxylvkKAh+kEVx?Rs|q9I8VC*Xah(J)I*NpDgjGV6bYUK&+*iAHGi_M42*a5^llO=Ye zO>2V~ed%+DP;jnLlx~`9LSXf1_~(@=^5chUp`AoVNM#}#O;b|muUx+sl|FPkT?DCE zHLr93c9Qs|VthB_>7Uz5+#S>-{P z4C*$iKi4x$;gVhPfi(jI?t3^6PR9D8#7MfkyCv_BZJoC^e|kchHJX=^7-AQqgp4+0 zVB&9qZK-;kUtc^cc&EjvVrr)^b4r6yBKyI_;e>k8eGb4C5-vbKLGjeOSvcT&4nppj zEzpr*0c$!|E;eW+#A8r&l0pGcL}-L=)`L!RPLt61eYLxuy(RnfTII*-qD~`ibmtkY zhD%p)Cyqv1_1(KQ6j{CGx#J-Ts_N=$CJqjdEUdkR)HE)h>B8zHLl(Q%JxJ_N0OyzH zV(|3`z#RWfOIpJ9)inQto`+72$)0(tXX+@{oW#!YON%`G$gC0~z&smt&`w1*@i04V31bndB*(;J0m2SuVy&q5LkT8UPdTE$O*MB zM~>JOsaY&DuFClf`N6RO*{Sc>N=PjXMyh0zd{oXy|?n!z#UYT`%Z7YyUz9(=2nqm=Pl=A z^YEZ3U#vz8EQVdfS|OJ&2RNI+ls^49mc@N$X_*0cUN_rXt!N=zopZ@0S>5iN!`aG* zD=Vg=cNfr@j4I&PhQxjS&+7X~Uc&XUEn`&a+OI^`qpkSPhyJ$+W zSrT&4#^&>)Qht+NHC&J`AVcOVV$?i=4ei(7TrDWV#5>qNiyZw4>XQfpb9?Gh?c!+i zR!sDKk3`zu2#2!AS_bdm;c2q*xUMV8Z{C6fZh+92u932{ul@0Z73YtpW3uh0N@D~* zd1)DXSy+pTx)*xx<$tSHEKo`deT?`tShPQ$xc=Jag~s9=hk_;!hvrjF4=o!M;oRx$|=mX)Z?^35FU@RX7R_`$vdp{U>N%e&GcrVt;XR-PHRCr;THK2a( zc1tAy%|C@Wre|1^b=x!MWB>Xb_x$W?P2|R|Hq-d)H*XGShWsZ9F#*i{x450iMCHpD zOl)`!qwQn6Ve$AntUP9<$6sNpsH?d5J4Eu=EaKY(e}w3cRS6D_38X zEgws%Ol87oMo!;1&Tj`}_EAi;_Nl@TH!61K$?A#NxAv3iJzk`X@0~dwDP`_+OT6wl z8rJ;l3jKPD(t#iDput28#HI?J;6NrUSmu%l)G|nSitv`rZ)`b(BZENz0zT=?`eQXV z8jlw-pJ)J^!r zE~#^bdir7kvSb2fUV3>7Q{%Iy$8%6WQ}DNvhod@7atAMr#GyCD)_!~DCy{dcw~h)Z zcp$waSJ3w&4An~5TyuUdsIqc_LDa6$D?%?6Fjr%?GpX*G#XIL-1qU^P0;t1?3Xa|P zH$(R_8)3u7kuyap(5elMNrC}H2&6s(x*7`jA)zEb zdrK8Q>7q67M{mWmOI|;0YH9?4GiK*Qjo+QR6_VoRlqX3(XR{1_S!%er6v`{<7cyLPQ$-kYbT6A=SqOj)i?1L8knQdo#(<`+n-y-@^f=lo-~|uXqtP= zS&dFnp%N1hjY%|p*pRKhHJ6imjv?({dmudT*8vAXR5IMpx4q7*joaYy;~!mw;fW{% zegjS2WJ&nJRqJSEWr(p+Rfm`L$zf67UQ=){w2$LH&TDj$f>o>3&40YU&*tH>@xxh) zRUe!(GYv~Kxa>ZJVn{7tcREYaLS%hO*5;sk#6bI1D{ zCJ9gg2U6oGlLYY)P{H0;z)U3ZAd?M42DGpk+%MokoH z2~1O=`y1SE1c*ljA@>dFN8epsO~KFzbbeY^|5U+#91zFpT#5_HSK!F`^n1}Cqa0kH zX8Pd~3`+8%vffw)=5S?^l+=h6ji3fv)k7?)N+N>$=!6 z7J|&{zW-fGAI{hApD@KJd3tp=RqDzZB-1{FOnBiuzOt|@+=s$3FR3l-Y{0+1 zszp3(H*Z|yuoob5v*)3!-1^SLUtgkzoOao2T>|8(;~&cHl_q!-K9oCCcqr#XUe!ha zu*CoM4JSSVDXqP-@0GRslh1qw;l?TKmM0q`68*_I3g8?ijeCUT-{j#j&Sv=odD+dM z`t>Jy`O5q_yrV$TP3}*YAj}r{4fcwSnR%<+nAhpg3a!hKJTD8S#l!6U_a_OarH-GjD6N%a{rBLh5!B!WNv<eFY6`4A#wQ+8~P6-w8*ZH+!b ze2O8dtgO7L&*DK!O6s)UPcl9=1^hD5-!D_BoDmlvk0U%~=%MbHP^G0zn*7f!NYXuy zsQ7$`8DG3)XVXZ^Y^{PcD2C8;*BHS+N{APZZ(Y`Ogf>~M6Er`sf3h_O#sT2l;X47b zDB6Qj@eW2}{qeM=Dpz1R?z!|DTZ8V1w|E2YS!f1TpGWy+2?%!nJJfp6AROy zlkyUQofg+)cRH{z%b%1;-|6uKU|3yS<9pl^aPN;#K(O3~Ow|Ezl%j1qPZfbtIbARi zUY+nq*NcPY&G~xQgxRE|q;KE8{ep8)=Mh<=2Y8~?TQ!9T#7crL1tMlI^L>4MQ`VAA zdD6t~Y$s7)zrf|bJFV*VCx3LqKwu^c&(DX0zHmmT+tVpJ2?+_>PIQi@XAhSq9d$B? zQA|UFu)g2v1WuoPpP>JZAQtgsndPZm`PeYoR4}$!G=3$1!ikG}uuR~^WN37D_Gn|T zii3mJ&jBe-&4Np(v=D#4YvYYU?*HYll3|S~&{lCV1E^2&+Y=wpr0l923oe!Bu6d`W1;xv{2<8z;bd|3kas|Y1>R4LuiNKoh zCdG!Z$ptyh>0m^TspV*|C}p-Kde(C^bEh(!E|;gLJ$&Abc-EXiN8D?D1tAw*MnxTwRumy{y|^tB3nd-@u6m zY;?P=MRQ&E&^qnR;PI*@$=zJ*t=>HJjX8Pzl}cXpnf7OmjZE)t=9{DBRyXnajry9` z%y{9A9x!)JE{l}!0&1H#a4_j2uCLw2p{DI}Is)f3t@aRCMwLvrj;K=%Z*=FtbT}p# zq4Fkc4tHfhg)|@is8zJ1(hIb{&evAdWOB*RO*Zl;A>+5_9K$9{KNxL8{4w%;jtMj- z7iTfPH2iX@%T7fY<3DUWTUIRmW?AtJay3~iUfg}EezEjHIBYhUYX6v=j&F`OUy!w{a$#a&M;DGW}qKq&(Mm$m=lRLTP|05EVc5Ky@mL8tcOk%;#fD$*^=WqMX zywRDfKEE*7X45ujJ&-?nKV@KrJ?7#AS6)QURbRdOpr%Kfis9ouuvJ}q>@@Ub{k!@6 zVpo|Db&>~g7JglaB|oCtEY+>)!eM5EJ4sVjJ?S6~K?5yCM&JGG7aOd{s(ruSv_ zM9GmRWD-!jKv)xJ)-X+kIs8t`VC_cEfi)g?pyurJ`RE%CA>}YNQ^5j(>g`-Zy%Yw_}4H3QbmH~8&jtbWX4-Gm zNMIQ|`bUq|=$EYj%4l&P3oy%7g9 zJig7pIC|bH!aNty3y^jNiQ!qOtebZfs`Jm38}+4Z)DImPl_Kq8q29sn?_dn+I*X2q zpAk1m=j?jiy_(s0_OJ8TUEo9?@nt2d;ILr_x!-u8QJ|s1L+j! zVpl5nCn{Ka68pP6i{pl{Ku>U)3;Z%3L6y0R33C(owsWHGy6~H9_VK{S#}{k|=Iqi@ zU80De*AI36Sj@Qug<`rAa_Kj^0}}iwict~vk%_qR3T<6+ED@q<^9b=9o}jrn zjq@|OvRCy^iZ)?|LvzLUn=i6yt;2$ptQXzvf%^GpiFx}YG}(x58K|FE!gapF#%-^h z@CIR6O%y~ZjR-LJUs^?k^OLa%P?vPd%!i_T6M>p0t&e2^g~Tewq#;L$I@2Cs@y^Jx znw|#PLzTksDwa)OPNuEx1x-cVOI|z&Uq@tT8ePv$NtwjorA9WMuYqcG=_R0vaolDR^!E>2?^)yXw+>BI2n8na~UW zyF;pboPhglNQQUo-Dz1He_8^r$q?}QjmJv|d@+0`3|5nA4(K9aMSQ6FY}Srg*zKDc zEBJB<&L0n4V)z+cj0JJ2s4G+&Q4+3aD_R7egC_g{LdyO`o(Yf(9fnb2rpXI$%2}nf z%Vp={)fU**8DFGKYDY;L#r?3{O6F#f*DO@*S!Bf#90IN|F5Si)P=X=2<<3w!xL~`) zlXZ!)+qWlV(0!MplvnaLc?}kEz27)=*}O%d8vs{t$>Tepy}qL`taZp36Qi0wBo85! zme_+}`QKxc@^(9;s3P633$!{nePHX0^{Xc9t0;L8vFhNEfFc&V^NYYC#lj?LYryRC zfslVp18DRgc~s63qA9JjAIVGT*T}g>P9zeedYm6 zM@dA$^S8^x`(xdr{`f{W?aIXvr$=d#JE{%0_%#kCQWjnzbkw*w;k$J462f}54)0q8 zwTIH`p5e5{6&J?S#An^CGD@RkaKXqR7oi%_7M}gW{^4HkG4{pox7)|$0S99uJpont zc{eLH5-ZG=o|!a5XMO4KX=fD3vlJIal{ZEin7F6g4$yqjkX zX?y%2vPDGUK2^f;N3(Nc&ur3#IS^>$$RA%*nAG-E;~vmgz93qy+%luOPFZ+=S5S^E z-t8DEL(_%ahp&v!=%&hvkwk^|G6D=2G}(A>!!AX^M{H(z_nOnl?UQ)j!*3i-g$CKp zTR~BUao~M-doN)oT6T5NQ8D;R@#eY{xK%^3uG2MgWo+=d+8u10ympHlQee>uLBAoh ze&WDu=+8g;)ZgC6;mqI9W7XC74I&;2?7H% zYdxE|!>5OFEiQwy(3@0?8}OP#-EoM;n{RG5Bw&LvbweBhiyQks!$WC=6_2x`JZBn< zTttiI%V^3FpX8p}9zmGzhs!Z!D=~5M1>)Mf($s|t%F5RzgdyG0thHf;t?p_++L;gGEpha;c# zm5?13U-+qj9cjAasM$D-Df_HYzDg#CD5mxK=J%@Bc)gsgx79tZ_xdM5j0I*LL;u~| zrjq>@;aTx&6p5LcnL$&&eOXa2v0dqd$@Nd%DzsWfCoeFhv#rID@gwzrh2{+3I%B7p zmImD4#ay^%ozE3==BJztX8hy`ubs0-h~GPsi^jj&oGLrluQaM!bk|);iL&1wD=b4T zD(T=l&TY+E`Gy)VYPudk*71DJCVjmRI=VAB(r&pS1JSgMpxQ$DTrCY|nfI2^4}@i<2C z-YJ-sWAipmMM8*thfSzN5ARNPhUQu!&b<5nYqx{dwLZTbql6jZyF>Ib%p>?*F#dCi z(?R6NW!U%1N^Z`oN_O0P!a10!_ul5|iU*%8+Wq>>GYva3F@1bT{IC;3m)S(4O4=bk zNLevNsAlM$(w1zl690(ito>eYnE8s=J~p~HVMfBM8;*&+uD(9Nk7^nn_x+mL9dStc zFFq>I0h0CsWYlqi8>g&HXSlF6i=n>qDsLqH+G3&mM&1T5gf@iqNzN&YE=MlASuI;a zH@XfWC2Nk=Y;NTnRhAi{so-i^72w8vZl=nx!|+##~Kv$*PYG8L+GW2 zT2JyhLe^PyeNwX&8*5rT`U-{NX9EVtdvhj>;?&MM_E_xBDJ`_e%0)tq@{C{!pTX zR0^^XKA6$<*C6|8M^Olk|L#rQ&jC*jpC;rLipE|zk?Tw0yZyAu&18N7{e7r27t!6i`O0-I^I*y*2{c{2WhgD&OWzt8`Rm|1ec9K| z{o;9qXsCWAKrhP`5f8`treY{gbJn=oT7P_()#x&UPQAo$(z)%K_FYT4#qF-CIKY^R z5q$hz#gCEHvX(Wi_@)}UjmRqZHvxhw z4jbL~cwL{`4xm-Mi`gjsv5oIZ#c8hf-3)i>M-H6Z_X*)VozE;)6?Rr()SDgSo79$B z(x+Lv!^G*a3~lFf2{Mhsb|Ik!qfnPx{6cQ3Ftj32p;~N7_znd~gK6U&kS;!??a-=H z`@=-LaZAkC?9Dk_kt`d zOw!yBO?1+Vm@ex}T|yRT)#b_S!#C`d*l{JUr2qkkdGpUjFXRD$3#6l!6_1SUgYc|V zmcuuhQ%bal`i;HcE0RsV-2_22onhZeI_sxi|22leW>)4%*{{%NOO11aeBGh5JXHy0 zl4iWI=kKg3fDv*m2;q2NCG%ozN+lJWHZE-Q-qLf|q#^pjg^%^dhAW6x*&%N^X zX-4o)C(8UM!j(3VEWMxA>*n|nXml+-P(3%-qh0g z-Ky$o#7|$Upl(pRWVypd_Zdq&Rs@Pqk7TnA4)UcDtaD!$;+?xy=10E(HvGpEaqvu` zUcvlYza!8UU8pT-83p5czDUwaRD1OPy8=`^h9#r>QzMO)=FQ2Hw4bz$AO#YsS3|VT zQ8g;ccloOMg)*irxo4TKs_#tFsp8O+Y;7-f=O!2g- zboM#u>kJI|&MtW`UL)pBS}&FKTiyLffqBp*CCw4o8E^r~_>uU4o+iV#n)uDne(KNM z->P*J#Qc}3gjx+@>xzulVDEKeS#RX8da19SvMconInC2HMgb#+UAfNxQ8?Y~5v~AZ z6=kh*5Pl?5Eq9x=yNRNIq`&{PElG+Kr=XY{Nu%*UBP)Sxpf#ris`vk=N&Xgzkzz!qX6O3id`*8o4-sYSflEs&zI$558Ca*+qKg zrRvJpHw~NAcMWUS+G}LTS4%~gh)#P4lqMT`$8awVpG^_RzlR)%rLu(F=OKbH^0Dru zUH@eJOUQ~}A^C{HvbIyFm$qAPg3-#-_o)irIbFr68rtnyE1TNIYfAhvH56YSNh_8}(xXKDLcXgZLTqis z(&F$%PCL(Xky2u&Ol2b0VT|YX*q@h)xc;1^aMGER@J9`_ak>cje*0DK^2Ua=LwiEH zW>QMALNoZW{9ZfL$NqY(&-NzE6h$}aaiQ6H+wAQE+ zuED4^G}#n$PmZqUh$oJWdmu>vT2)`4pknNgk#NlWF~?|4eRL(nQ*wA&XFZam`u_ZK zxJS?(-q?pc+W%=x9ch=PKtQx|=8&FYE|wt7Jnm(T#sKULX+!$Flz~%!V6p^DNP84cf5^%EAnTbpM^XEy}p&9A}U!1w@{7nw^bI8 z%`!Jui@Xi^^H+z}P;Z>Bo>^_~pHtnz^nhG-DutYmv`=!_=VpuParM&bt{!qk|MhJS zBQf0xPQ^*Z!(OhY%cOa>l!%5+hVl}HUmt9q)Ti7l56>U+99|uf5Sx@ZPV5QyQ|O0r z*%7Xkaw2;A(|iuKCyBUJ>J8>jtH3L{S8%_~a12HL!Se6@7l(_0L8ru@=yrX!_iB#Q zWwkry{g{XHN{mGcx95u*zuaeQJ|DB>KeVb*SDA&Y%_wC4YgsDyl)E}nvVl=ZL;g%I59c)uJ}C-$3$NU zxlB;Xa4bR3OasswJ)bg?v91ScJFi|(DzlE3_FXv4t_DjMYLH}o)( zbI~4cDEBE95~Z1T*>d$s48lC>Up;%(U06|vKIZ~ z(L~B-yNk9TXgV<@=VaelKJFYyx zNjCTF-v@nQj1091JF>_MT?*G?SPN0K2kfeOMyq0c*P4j%9@SW@OA09?fiS^ z=V(ga{0rgB6kPk7pA-a@^xS*IN4gW~Vp9^F>P=2{J9 zZZ0;9z`asPsGVVTi8Oz0n&B8!^MZ}(rz@q7NbQtl8-pZoqBp%0U9sgcq$myfXt}MM zg#rRhqJ*LjF?c8T_NSwj`0XcCtw?}4FUd@SDP1F<*+2bot_}uF$^p`3jXQ6icVD#2 zfv*>fLsx$I;$!=npdL|4kZNt2qw!gZzM5KS#<|49kUvhnea)>|tCW!kPE&W)yUSGQ zn!gHtTFkR@aDVG8^0|1P>>YLGtu&);h5xO${Xzo)+&a<)7f^~#Z0Vn!3Y<Sq?rB;rQ%qKHDwHk}T?7C~iM&JqNYYXh`YyuRc&>^Ky0!+G ztbtY!48l%6PdU4y9DwhuJ{74RbpW|~3uzds1iqTwIc4c*ypED2u?%ph=(=?wj1d2< zt(2rJfq?H$=KgkNYYNN6NLFe7sb#uQC_J%3E=KD4Xf*!~(aHV-nU0Q*b8Ys=zt{CJ zC5m~mVRHkGgapKc?fLWP#3Js+j+x5vEN~TPpw?Yzlp|$lPN8F4I?ZBB)VI_F1Y?}kv+er(2O+j39Y}o2%4`>;ZWRbssOZHt>~zWrLTKzfCV2~Qebq9 zAR8E2euW75S?_GW_3N7f@sqtdB4}4M2%aT%f~O{F7#YEYTjg`s{UU7m@B|zDN}P@R z6^cprY2R{$;f%s%b-POC2uWnxg9@CK5F*u!Benj+hzc!tV^q17 zOHHCMim=vDZ)F_M#$w!|CV!MhtPJJ({VQGbJ)M~T1Q9hm!V)346%(FQ3HfO?#4^w8 z{N=~rixdk54(Ng+ztL1_A%M7G`z>9rRe1Sw^!%aeq-6sudnumhB`Kuj4Mv;ak5}*G zBaFjaXkzn>bXtcsTU`uCm;H!G8txKk(w00oCQSg})T3@9$)xT4uH8k8=A=v7-nAUk zFO~E`pMV@Yp%-LwdX%LN`&`AhySV-N7g#Z%jExg#4NuRq1eKx1U9_k>1_1i_6X_5o zCa0c*TGDHbNleF|6&!`PVf_f14fT3Dy(fHaY~}>y(yz)nJf@vJsR|;%``U`+dQJC{ z)_R_82Q6Yy9$C=wKse_d0(72RWL|oIoVjr{So7SyY1#pc$nX@$cMB?3ac{h`Qhs;h z+;rND?ip}vJ3f&*p$s$(E;AY<$`J98b8ry56NEAEX7OOR-pN3T$pEOqULs)e^AESR zGumUjvh~1RSDU6Xyv9OOu_lxA)*VV@Tq_5dm_K94+`@$z$oaa$hV#DWZ=61wCHdur ze!|_7c{WPb(?GSU=#Gw(xeDLfqS^%!wYq8!w9-iO?N>NU!Ou&xLZ15j&mg->TniL) z4k+GsC@j@e4Elea;AZm~+zmO!6BO9Ni{B6_^;=b`qsYRja=h}qE7(g}*A*>1Swq8X zA=!nj4{@Mly}WIwP5)e8wyClVNaj%{4no|Tj)Q7!k#=#{{ET~w5}cs z_jld4bd$*;BG_#2z3|z4Wu+Gy(uF9vle}M_x(?jAQCI`9AY--LV|rSvxv(ACmSah$ zh#TZ!7Yt>6;SxV5q;!dN_qMnp#O79{!VW>j21lh?_bSX22~Q!96%vnZRMN5VWSQ^@ zC50`Za)a4oy8!Z`sa~k3Hr*9ZNP{r8IVJ6~&+sCeHXNLKaOtl+}ZBF4!DK0v8I z7Jq0=r~s5)iF{GyW;bq;*y?Jbax5JSZTdi!mr%e^v4DoE%TJ&L#5-&4e<7#aHo=_s z@iN&^PaadiaJqzmiHm|a^ia6@$=wDg@5e#bPkvotkxB~XMYh%+Zm{widF(rAQ6cotX5BNqQ&d4y{qoW%s7{C zrT1*oeb1j1FK9%h$y?GwFJ6($aQK>AV|lynMTq-dE!`Xsfxo`gsk^D?Chw~!-pJ63 zE$-jFSWMWgFI8d-cgJRQ8i-pJBRFk9x-a3c5Sl~lbK9+8L<*Kytu|87Ju(|UjPt6VCiNqRqLrWK) z!m=Cw961fC(D?Ym=X-zS(|6uXZzhqSl<-g$Rh*1pGMKyg8 zPxsHp$jr^uKqdJyMTLmRd@{HY4i%+PZ0&wH?;bSP9dz_f-^`0+WO3uBrjm3I@&{a1 z3g-@m5Sc38-?bf#2FfvM=2~sMHb339T7RAyrb5kNuI9r^e(O`v9IXj`KwJ-SD^u`G zOoy}?K_D-^k%G}}0u^A0X`3S73lo*p?~=o2+y11WPr z=)d_PvZ33%F9?YAR-rf6tEks<|EvXF-Z)zVhgeJ-PSLHz-Exy|+s1iWQ(#Hl-k0wW zOmxrtAi=QrVp*?9hfHEA>x@^aqju*}*L0}b;a1FWY`;zEC6}dJ6|3djesnH}x4Fpi zHFX-VQVlSA{rJ25^D2Jo<~89j+NqVj89oUHll1HpGbOVBh5Vcxp2aDM3X~=k?=B&2 zUg|a4A`*^*h%C5BkQ4BFQQ0mnjkCWz68`yd`6jwl$g?>fTptrT5RtAC`UFIam|fBt z3E_6d&+YfBnrdgu;kqX9F`m7T(diiv{w54_JGuM7dz|c0%Nki3A(VMrw*#)lYp|R< zvWJq!7Am`9)17Wnnz>#dOvZNhCwc&Tyo*G@>=qv6HA1)Yjf^XmCD;tUM^bLyKFIzT z<|G$`pkVb|9dk;5^o-Yc=re0}Tqig7mN%x8-)&g;dpz9@(v z;Fp6d((fDqRoaE;`YCVk7@FLVgO~JYf3*UuW9_Z=S9?4`h0gJXG8%am#}MeyPBiI(h#Ro8(F1ulJaPYbUeAwC-TDG%osW zF_WX*nVNG2%uK)IXN#gj2%u04Pawf*-D1-Q=e2dkA!SQ=e}gTfVj}vWId=22sifWZ zzKpIF`6&`-YfW|vUh6oXd#9t-o(uD3msLOC?>cR~!&ETl^V2m`u}IA)USkNZvB~p( z__=$u&1xlRLs7i$cWLAV%XjW`xQn``@)f0T^r0v9=Yj>B5T5Xv@1wsF?v^yQ+3EHK z=H=yO3t>`XqVeu@MT?j4KL*5s=jHd-n0K%Nkf+~{GcWzLb`TCw2V6tFGfdXaSypY_ znoqKz$YpbhnAB2F>uvcznGU?Qo1i$^B z3epI0Ym&k&i*m(p?0RP4jV&uH8^_3H)AZxF)h~`M@W{;SV7Eb9`?>`Oh*|^^HYZ;& zm3j@~%#<#5Cw&HlgY`{YP1dz0=~7K$&JEtoA5AFtX9KT@CyJ62rnovxDz#)!|5lHv zB_(OqJU;#<1V|R)2>PyI+&M0oG&QUdZ{y(uF;Cxji=Jgdh5C!dwx$(_XgW=PxlZ#? zV<~BG{R&(-oNsLne{t?hnAN(3pg>+T`&?80_YCfRfJ=`ebz!7`Rh=giNS9<);d^V+ zRC&p?y^Z&?COdmE3{=uBej?00l?>^BNEnTNUlT!$!U4A`n%3}UR zXB;AwhZvm1(4~?8x9~+x2p2uIgA;!%n{m8>xs1_*zB`g zt_sfGowkA-sw=~;TyFn3JYT`R!HT%)KYrKEi!12WfW<06*~W7G>MeO?QdBRuIhB57 z{}+&QuK&vLpitFHT&8q5sLSr_KU$sw0esT_k<^ACqo)7(?i#$@8%-eS$KOU^gHaZh z&=_aaVKTYp0;KGuk9g*M9J1_WFn#ev~i-0wK(#=^tAQ=zHy z5cOdPl(%Hg7wvNd%GNfgspcKa4(84;zicC7;gt#QPL)v=3+8Q7c`U6Kxz-hDDMXLaI_KG?8N;$a> za8dHJ7lbB3k6d>MzChs(IBA4~KG$d3zWZBn`$Q85H#Rxfc^z9OCMD5ua141ZbIHQZ z-p|p{(Yah!6{5%l22ojGcPUK#)_<&Z5Z<3+YG2gMT3#xnZ9}fP&rn^2!>lSPo zzzH>U-aI2$C+F^eyTa+*|2H$HMksmtRfjgd-3jhl74$lyXHfbQ+{e*MemwaWN~{dM z6E~6_GHi~PY#PFeRuR9G@iAjxJNV40&>j768L=cXs_ zN_jzha8gTPKb{=@t6`umi=4-Mnp(QD{rY94FkFC7AZ)PX9~qA!cTwUvrJT0K>EYM zgdB>&O2KLT;W$&;8%sza!z3RU2ym*H$p3rQ(!Y9$x8D)6d=jdFtTI(o%Q=Slv>&bz z_(y!|yXf6w1M6>%+mMBZ5D%uLjS|v;D$+YySS)u^CMS) zcx3Yh=aq_4D8*Msm(0VvnM_0r#P*-5ca251m+pi{-Nmfagy8l#=?oa-GcN??mv zok%*{GI?vZ7bVB7n2gH?2+6!ug7yEJ06|Op)Q?bff$^{)qpm{&_IosXx#NcST zF!hC-;=15kS-Na*C^pqTo8nltotC+k74S~kP4SAf`tOjX`yhRJj=M*7KfC>UoLo5M zDrimc{TaTk(zIHTj!^TuCxbF&&#&x^>TW@@g(`}F{uV&m+KuWaTp>hbB6+;p&&4Kl zNqKxJ=5%kmRO+bMK1!e&^|(m0DIyh;5_n(}h9Blyv8xe@@LuUwc4~~im7lCruWm%k zL^n{cyd~VC8yvk%VbY8E^7^cwhf*%|+pMO$$z6?IP>o7EL-Nu|ne^OeTG)Bu8Krc* zU5h1e<8QIwf4_pl&Ib!en@a@or&b-NVc-nOrRTTt_YR%*=H;qe(%t9l^%1g*r#3t; z%bLB;Gxz}gkt5xnQ8(E7f&t@UE~Cd%_WfMY0#KRJ+yOL`&^_Nu`T*f|0J*vj;vhx-N`Oqw2WcRBEZ6W@%f!+p1taJPN5=ShmA9xJC| z)6A6VIt4%ddy33}0ET_9UxX+Q9>g!t2kGYOLZuT@mCyX(9z}$6UugK$8O8tvoisSG1VqY{jj-GHJ52ZOCRB761k(#$@7mz1eu3poC9C+fGo$f*k9N9x# zS6<#s1lPXcF+HhZFdYvDEj~6g%dUuD>_hfTLlx;MACcJ+#Jrf%GY*BH39PY>yX6WS44d`D(3`9YEt4JJ=fYT}`AuAXkf=LdJl?*`LBfNSOF^D5FM zsq+5jh}^OEQlY3%oD1+$rgWSJX~cpI;isv8x@-65vv5$bbJ1@U4)=8M1@^CNd%*w3 z!`I?s3?V%K5WF?34?p2>#{O5D^(3$4)O_jYY$Ii}$8N{RR~h9VFhfRq=0kBnorNlX z3~{h%ldY6uT4+#A9SxP|8MEk3F;3!sh=qM&TYa?Bww%qFCME=ncex~p>pmv`SE2am z+o*@M2^1(Ov9^pde=yr3AHO|Z z;Wfp}6ahbsE_R(zNqk$>nqmqdSB#uni1inJMz&KLya>{Cfpl1m+WU51#N+T|s#!vD z7cI)2S*NLkVJ#0M-hto-;U{knRsFvjcr27-wa;czkjs}x$CWsw*&EF(gK(j*GDK!i zmQSpXKZ*rcFIbD19-{CZ%!@K{s3A#52DJ}w8*-#BC}l+Umgeahi~f^4zd_-BL(knGR@SzJ-45bIbo$YN zg?U=v6^U{TqCrID_#t1b@osbKQt*YCM4#7eKf%Gun?_DZaKckU59W>(KKFfUJ7i6f zPK0A!j7HCB1LCg3gXW0*Wc|60KH|-(u*S9H{&t?|_lS`TGB!M)+NK~MFQH1CKeC7| zGUHvQhFcmv=p23XE7yE0Q=zc+!dG4Jpzv<$-1GjA%n^B|MQ=ILV1+h$! zvg4!1;JhnJr*m3vxz3d2bN?5Un<)RDux98*#rv3VfY!CN*QICU$JMR;6EKNf-cJ}TAK|?^;4YtepUG?IkCbJ zT5&6dpNMqj;7O^y6gG9>4+2znDRr7^m=H3U;_L|6h&Ex??gbLk9+vOb4%WcapelE_ zlD)!n{!93j+BN7BUbc*jJcb>?>ITtXNu4|JeXt9%oET+VIp}3A3nl>GUY~m`1~ECO zJOdE?gV0PUCZ_VqwFiA;%c^FgF|p9xJz(%4HAMwaCd^cYY6VaJwR}G zceepTa2ebO_d(ty_q+1mzqi(`nSXG)d!O#TYgbjrQ`TU3gv^=y^zsJ+9uPq?wzE8Y zc42<4_#saF;H6&oRr8MFgrhX)1>kJU8`OSPe3T)s&~*-}8bRNQp!~MuF3&qq#CN}2BwJ_Kokn$8ttN}N3aWY_4BG}#v@f6b1$4&v|@kp&+1cD(+atda_#IV|Dfly z2`EN7Yync5Y>$>T6A1vksA_+L!eIvFa4*l!sN4@|WffPQimR{O-xxh{}2 z=)G|oy>0S8iqasLM6r4)$^fBN&+Qn`+a=43Ql)1^a5EumzfC}I;B#Ti$t9rWco~^E z$n$EFS5TA-pdf!}e)GOw89-YfufX`)rND|vGMMp$Pd7m`TAPzNB3xxpomH!acsxqi zNhBZp(YqbVk0h9R5H79RvrVN8S;a&sV~97rr(vXrlCuzfmuRyVuRglDE~0;GWNx1b zFU&_YrpM;^8bVZTY(bKVE;w6K@byiJ7}iWSSw8smdv1=uFDfrP-QvZt!u7xmetk7D zxGu6nODc0`VPSq_qKj9h-Ue0KLjWgo;^CW~Yba|+G0CcP5aHW}v9V8C{4UZiXqnT$ zk^V`lKQco}gMO-6H+|eO8j2-3j*gIU8cQN_T2E8jM&Cmiiom9;v7URcOuo!<$aU{B zoYG?>xYupUyeGlEuIaNVWVeC~C6g61)X<7jj}mK(vrnVQxz0z`g5{^$(`Hj_ycx+K z8_4Kq6@+bnTnJFp|v-1XnkW1tzVcwGBn{G;|RT&v9 z!bGXKM9Mqbir8DqpL|XY>b=Dm;zCzj%Nerb8T4TxNO>`-;@xb1HE6*`MXcay zJCo!AxVF84X`AEAY4?AAvATnW=oVxnDC1plG%K`%Ag^jcR~Kn80=m0| zyq7^KKWzTDJf33hIbj4u0xP8A;wwQI7Lzv)w$cfSvF6hy>p+v^d`2y|1kEo+~yMOPay4&@6EZD zq3R#X1aP5DfE^BLbr1)gdA!0=1TE%@F(3pMw=E5%_ZbLh*1Ja1|t>d9NmB< zfboOF(E|K917VM4LzUp}4lhIpdqHDZ3zO2kF1&Pcr!*BJEl|?>r_g8}rxDywYmllt z%zN0_Buo9@4o=Hd|B|Ec>}TSN`>D#xMhFFp=(&FSQ!a1ODEptFbTzvQ0)F!(d*RP@ zggSj_6J!iSd&e<=>40)2WObxW-1YKuK|Q#MXGZ3a{jM=+(3T#JL5$%Izf=z++JiUp zTbZ|WlKV14BzSe^{i9Tm!mwg9 zF6s{Wl3Oii76vAg{&Ci&upwv(ArD1u4=uGkRlcp@-m2*;EsC};@VfBZtG$bN8wu~P z532dhh{o_{ZOaj&R0nKv&AZS$5s*|m|8xHRD1Z?8POLQbwjoqbD{km&TanN^QN(ZF zk8U~(=5Iv!GJ8u>Vw8^(&ztOWXAKNiWY8yWs)neUaF9HGr zLZG7bZAH;}NPP(bc<>E8^Qh1ePzc^c^rxkMfcwvEp+H4QuEGDQ{ps^zzCcj;=tio-^6eknGx^V_HbV1yTKlmPY1z$tX2Szj( z7nf}QFs!f(c8$Mf_uqMvh7desm>T}VV3V}x0~s(S6;(Fwg0|$_lIhx&*W6|~B0CNH zkn{8L2Dx)Tp7EzMz!;)BqefGcFpT+!oGLa<15rJD?u=ko=(S?u;^G22-T|UoANDe5 zAR_NrIV~#4IdH%R%ag3Wg?hCG-Ybe8(}%!WBwF{OTk&KpAN$YLs`<>cN(YQxO3SchA9AnwtHg(lVUF9)^QrK8l2G*nq%Cr$ z7g}-Trzq&Z6>nS!0r7y9^iGRRJ7r9N;T_0&ZhZLv0*S8b4j>xZZ3v)q!!A|)Cq6b- zY0kT)5S%K?nzUg?Mkb(~y~(#-L$~=GT+j~xd@-P{K>#A_|0T?o{tqey)Yf?}N?sZ% zt?HXN9@+>nTnQ8J9Am$a{NNd|P5haJz-sIn_GYis07WK-8Ee7#bR$ITi12U%h;K4; zQ-uw3^BCrkPuKhQ?hh3Z6XX^T@d9XIg+2>V4&fdgR^=#Zd(ouYH!HpB99z)TgAnNn zgghOwr}74jKRK3T{u}OrB7>gP!R~Bo!t3CvX=a3!;&P24zh@lY?7VPH1AWGboTc2J9}r#z#p1rIyW*=Chb1Y zk{G}r8Y>c_!)4moeOx^#emg9B}3y2C+#~*$SePX1j*Jd%*+DGif-Ctw>yQ^ zOMn5Zo>(>s=`Mu0g1lP;FPbWD4McT6A3{k1ALO(E1oH8r?AP|iO_M+1d!g3xs z16$z9kRB?R>ru#Clt0C()7AhIoAn1-NUP2dsU>8CQzod@Cs6h*b78Rq&UFRx+K=d7 zQuc9~b^K2}_sIRwcEPHzuP1?^fBF#_I`tcUhwrllaft@W=LieB4qc%ZVC;soyh<)J@(1_m{(hV#ao(pP1rsann;xv(#U53%Kjl#m6)Ai6) z2u;E=ej*Q;16Ac%e-GCjL^EDXHk=4wnXUUPfJ{@m+agUf!t?_ZfVi4nG@3(Yp;5H$ z`Y1B=6k<`>@(U?@i{liDhH8Vf-GTI!D=%NI;tePCS=>TQ(-*jIZ{1x5KK#)=s*9A8~^*zuCN2omI+yah=!PO&;-Zwfu>1Gx5LGefi) zNGGM9+ct7VyrGVcjvP-G*&+N){7fFZJ^+lM)6s8+eW7;Sz=)C&wi#UCPN^cy7@Sy( z6^vB}HZP&!?P#W}ACsfVS>#tdx^4EG?_q&^O^@=qi?x4 zh@TMMTUNwiAXRL*I60@Z|8Lu>7EOpZS{71wUxf3OozMKY?>VTR*qoD}2di53XBs_( zz0VdS;1UJ+l?EV~VqJ;dqxmnnl;(}zp2z|!cnb9DE&sbGiSAD`0Js&;mq8;6YWxeY1gl(B{D3cIq{>lv7dR`q#@;MNKVnwd3?Nx(+Bpj1WQs zrr5ywN<+((6gBwd8ekOrgFF2SX4n>O0`uvJWh2ko>n$_UUz4mJEUT1{J3{YRN_x(V z|8IzCEi^ozzig3w`vUah{%C~aK?$sQ zLnPmIyC`ob$P}b9s}d+dFq|h_7)w5pDFVNp!qop3;nTlCu%DJJWa#|n!C>_CX~2ew z`a@9ko3USE@nizs%#MH;VDfNVJq$@uUbQ<}(cMs&ywtoC#~~aFQ|xY)O{Vmsr-sqe zckQ%Ud!BB5Nhi+xJ?TLw@{IdpyB%XjB`l#pmXp=@$I_2@5gPxS6b{d$@jr zsNtE5GBBFTDuKL8(@F)J&8BHJPrL+LesA@U(2ZvBSqC6(zUV*hkpR`xrm7|e>J6r@ zdZIfXq<#fsGV7x(x@kVv8N~W0yEIKM1e*G*TDu&iStcZaj4Lg(YRl3?-d|KEf^LBK*B=K$b+4lid}TE}wvXPr{qi+f9sco7p^x zlQH)pQ5IDb-dg2jlX@LbgZk@{Cgdc0##pTB6~dI`xCpxG@&mJZUt0n3A|oRwKKEJB zc6A>Q5jhOhPl-e+`5nnDcDuccGz25VpRf*^r+N(OFBk+SGiq+_WjdsY+0q_c&1pXG ztJ-oPFJ(~$eVqx-@4%zR$Vj!q`=-NT506)0Ewr7+dpdY%5t`3QHq-nXp~b~|P?Raq zKXIlTXg4&@hl}!{_iZ>>yI^lMOxpz?R>yHq2+-%(D-@j(47QjYKiIJ6+ONh}j>L>U=hj|bGCvrht zcU=dnkgX&J-mMPy+##}wI707tnw2`ch?2UrxX-1iSR3sa`z9}fDe(BCNL44+N?#bE z4WPxHnkcIY>Mi)NW+d(K-XN!%wr)=ptXhZce2ZMSmoOYQl5>LEW6rTpI+92t*!(j8 zIivuSeOOy4EN3t}x=}$WBuh>9te}yPIuO(+RLO?20w<+Mxh~8|tPPBmSxK3^vS&bB zLIA5`K}_7l&Mt4eb??tXAI2W7jGO6YgNj5hBFY=yHeu3j31Qs-_Kl1&Hbd>( zcPVTY`GrEp7(z}o8|0Wfc2E1kI+`KuF>&^W-+*FQ^^XPmZ-v3NTXE3wWB`X9y5SCo z6f%J9PS`#*#_R#**k5TO8!SQB$qQm6Vgo)j!v(btfu8MV(~5n4*fNB~|C0pbUKWvN z)8Q5qmF8@&Ou)}e72$!XIJmWMneKsW!MEj@ug&b1asdbbeUwq#Y!8JHB9>yKD7UCG zp?^$z?^Ja`1iu;@D^s5~Od}5baZD0!F_f2nYvmW(^e{-jWU(h^K`=Nzz@G>x7do-7 z44QrXm#R_B8)MOOFZE^1s3%3~4!Y_%oz#6a@>VX;B@Ug!p&;hGSHDk z!E1h6cRO6OguKuoN==yX?^;#N2HWZo&io{woq0`(%YHu8$~=Gf0>Oc9N(YxU{Y~Uv zc%|$ajFfszCr@!FkQn}62h*d@bk9ci{t?r|TG8;-ec2i4Bf50aRH$$ycutr_mazaF z&{=Bf>2Tewr4Y+aYMa#Af7@`~$Ng)n(uHI$>fir45RofNEEZ{eI0EsfO$eCK z?*&ov{s;B=mD1A;|1EQfto0^HDyg4yi_iPjbyi}uNrtsp)A1!3i9sDJXrB5!`r$5q zf>P@`P>w#-kRf8gkyPLx9m*dHL<5au<8b1~56uhImZxN~_X~nX>oVz9TOF(pTUo4` zv^Q@;UF&3mKFpA)$)sKUxisb93ygDG3%b)4}M-d0jgs+;OZ|jmj+Ole@ zZvO#E{(cTHU{_t9`Har=dOXP`ukdJRb39nKQvo*CZ~<9{2`kq!KC|BisGuV-&od3j z5wK=RCC@2_Xw`TvYPeBQVfL7Lm9GKV2fKdmIqM~Q6(m_9iOF9a;8|)WbhXLc5Lb}I za_{|L;Kjf?TJ1o)l$rhXYvy*W3Tp`IuIw(ESsF>m3nM(*supi}P3B$fC1? zLQhF9DBE^Z4p3Rh&bCUBt@9XrwxVXPN~ z;*3)46p&k;Z&|%AO}@S#J_K=N!tw*(4&GmEI~^8U6qWpkmi4E0Q-ysQrNZiNN55Ch zDi84%4zL{u7i9b$DJW#q$UbTVn@@*G3!{LLJ#&%Ha*7;JAWDCFA-pyP68tx9Uo^q{%(=nib5}VC8>sI zQ0gPQTMcFa1$&Lhn*1+v#JrNM^B?a|DKT^#t+0shdZ zywjJ>Q$RD=Afe~#RMT+LdG)ez_jDkcu5&94dPX<4n%QXUzG`jR^@YlASLhR?Mr+Mf zGpp5mF__Bx*IbFi7nnDLO}w>s9$*~a?hg9e+fF;xgZ0|_!*&`wenGtULS<&I?qf=Q zprA*XPf9M2xv1$tfY71S)q9+-m);^nbhxgq`t$h7W?hab*Gk)W>twvCk8!iHllDN4 zsF7(R00Nu_xvKJI9!#}DLl_=@yFqhb7emu_r}Vm1>b3t=&)j$eK6|>IogK3`#M_X8 z^}27azukC`F4Sl|0sgl}u2k{rRYHObBTKsg0zS%WM24H;hlMKrY_{KNekup6P*f<$ zDuAgB3VJ_w3udJtw=_XdQqr|e{%rTK$;Mf=fB}>W2&A9po~r#f=lBzcZ;_PsTJbZD zXRm(`e0qYi4@jzKS~1e)`8KByY$pg8oH}(7Fu#M=ZNit+3S9_43qPwSy0%vfI?&9N zIJ+{x=pGA{u=rftEp}~_`-JrcxCZ|)wFVQtbi$goYQ6@>DJ2g0iQhF#bfTyP>D_f4 z4ukvUyyN;^h~nJ6*QXBk-BKmeD8DxiC&p$T_Y5Gw z(c7Ex$p^=490C2b#xA8}0^6IU)qQ2pQNXY+*OSR1<)ayb-jAZOZ<&;I2-Ro8!OS$n z@h%mF>DqH6fMxSb*5T=yVrAr1GQkvcL3h*HdkO;c@pqdp3pcl7y8qpNFoFby*K<6- z1>MczpYn7_^oNQ`Q-N;!d|3hU3s!dCSM)q0Dp>w0>q20pDJ0^Huvf#cd71tXx5S-= zI2`M9+UZKYHsFm81rHN2@Ttd-_ErRraiD}mYSPFb_ z7?sTDf41l~aaD`;!GG-#51;C-!&_sAt(FslPWd%+bObo6sT($H^Y&T>jr`)dY|p9l z^@t=sY44)7!@`+}kjA#-rsijhh=J&6l=J=u{{^Rx=Hj8!=q z_hqr}AKdqQ)}&S5aq0&!TXfZ5zc%0T^f;+&wjC_=QuCx!-wwd9d}DK`MnF!dF#Gsb zG8A8QoBW~AihRaz_vg7AM*=|!Is1=vZ*K39^qJ1)b!-%@R*avz-;Q;$E%Ucz@=V;= zy8PM1o%?d1!{Z}#w48{?hcImEpfb;A9V-0*r|Xt?{KZp;WUVxnpzl-sU`tSAcc-p^ zVRhre5USs`x-zZ8W*6$D6)=7D%_5CkKlyDxgCMxW;#>)=*i_|aN`z)Kj+^9hT(3yg zq}=8P5IG|~QEsiGvA1lB1}_2^^2};GZb*e)Ut2+SH(7PJy4s;DamA441)sNbx607r zQ6q3}w%L%y<@g3Ol!aFNd9BGA6BZUqTnqyP7B(nSs>heXlBNIy27G{=sJF;cA#ahk z2us7VEiyR(hL+2oe+QdjxCLy7J>X{r_fMS|d?~g$)TfW8uYD=^3r9&La1Rl#pey$vpm%Pm`tp+lZ5MoAdfw4O204NNOJY}(@!QU7^E*oSU!J1&tsjot z_L3_jPTt$l(w5o!am%t+BMrsvuTzi6>vU7gG@ES{(5QQ6@H*sBMx5n539tKum^)sN zR61_Zv{6UP(uy43tczSgJ#984b=;PFBzW$lsC&Hlj(=Hf2FuO3)F3FtHMakZV`LtV zY1WIi8@1$QcBjrA8q&?Eao+#1=qw(#o$=|kRQR~X4}oXP*VZd?RsF0jX&Hk-)#pYJ ze;#49oBzX0xcoZm>Qq%Fx++;_rW1_4)6UG*dR3U7MEc=!C;PH+Bj!Qdilc*YV68e+ z@SUnNa7Fp*#|rNp$;w1kjnlQ+*TvXg8}G~L+28CXhWcUGWZg`Cd~SZN06f_5l>mI9 z$EX}h8xB`1i|0p!ZQg)7b-ya@Si0Q8(mD+@8l@l2A}iGasj>~1sY)*)X$O_XFGqQw z(+;FMyU^#lqC>o&ZaUeNkq)P$#3`NIF%ouJGHA2r2*=Ys4A4(~XFrXthoK16bz$y? zxN_G=?k#&-&Y$3vfr@=&`$l9?Y&9ij8v#=xb4@}SZI8(hhh|L-J2{3s>1G`d@Ahfd zW3$GlI$i;B%1#&=KdyzBqb$F}Rt-}J%c7VX)47lC`lDfbH6W$yFZlZhtvcJOj{e*M zilTm=Ew2@(wV!%s|FY?{@?4udLEF=W%RA;3AU<#D11u9JU4b4oHTL6oN(Y&Yh0R~_ zfd;nsd}-r|JEZB>l}5nWUN5Ib;1V#S7i*ycPPf~e`*TwAi;kwtBNE;(Ekxpr=OSF@eO4A?Nr}`CwrC!E)R^5;(w{H)Z_&>>LLHKfm%a;Dp zx`r>79nZHbT6Jc!k~scVqMAZ%JImzv4)RM0ipt%Kt^mPT6Bvo zVK)`%rkcUXn~01>lZS~9BJ)}5 ze4f03w$6vH+N-375TMpjA0es(uOI0wFpIUv+q zDp68|>LPzoPOMge{k0;_=q3N*T0u0TF!v5GEBGO5O2&I>$X z)~>|zLnA|Rj{mdlx}LaEeskta+0U7U9@gIFGR-&;Q7EqQAy4HuLLBcydz)$WxspqKAP5NgTAO+5z#{bUieAWx&Y5*#jjqjY?GW3=UP$5I6k8OjG4ez zr}%0|VoljekKwfbJ64A>$&V05V?7vjNH%%+%jjzthNKIEK!)GmvnvD5v!Wn0ac^Cg zsJGbe301Cbb3=R4Vw}sOQT49qY^8IIpWy?>Vro$dU%VE`Gf|`shbj8lO3xlv970B|6N$y&sSF3lzWnlsx4rDbW}F$0UtPc7W?aycbEwK8DAIr#1MW~;~QkWuDB3zKuFcNX!wUP zG~-Fax|w~s22nPmWY@Q%UMFu@4BV=$8!yMaH)_Kir%Xq5z<*Wn8m7nJlWl!%A&jD7MNuY4 zWW|PR2(7t=i19S`6%kAhwui?O3X$PibhlIh#!sWazY++B>Vt^a~CS*&>SVKu*kQEDvr%_}K!Ll_y0QWAX#4EBU3ZxGl94dcl+|KcaF=dIbD;f(N8!9TJeT3&6EUtLr?ZoSWkipePC?9R09Cyt-*5jKB$mT%g^bt} z)?K|}wy`;p$d1RNw8Zylj8VhW()pu&rpqTR?hDubetpKfAHL z&cUY|?WS}4liGvBErn56*405@cu;59$(7k=ZQ`N-nE46M!QJJAR}pUH@uipl?nhoJ zH;JgcUxH<43kU#EJW^i7Gl2#LT7tsWrPZ z#a6C%saduw-6(Urc0qiakYhJ_D+h@Z0e-8q#N9+<>U89(-;}$uNH2A`AJe}7VE%D8>$-%W?{8=M9hjc`~bO_I{h0L8P?v z>jZpN-gAI8@JIcW%G?5!-Q^*+mcNMg5XtfGlJ%7Fw05i_(6nJe<;Djt*RhD*IAcAWTwq!zN-4$dRC= z4OTGwf?_I?X|_E(1&L|4=m~=JI}9Iulp)>r6&v5>Uo_Hp-*XbVdzU zyS20JC&g0C?J`BVpYk*Obz#^@nRXZ{oi#M!EtcV+44D}YF)>1v_{1XrA2sCMSb$RCff0V znr6hpHsO$?CGsOD86p<<$0|T}Rc5^d*w?iAM)Y~gyBm_)dVkyfP9{|~pO>WR&);n5 zX%|ItH_STCQ+nmHZF;A+Vm|ATTaZ00AnC-wWOm|o%YSKn>RvbHZ1&aWEWJMXc9G*L zCpGIZH8ayPx6=xNc+74ycbk{DX%5f?n<+V+GjZbsGzu6XZU9t_>kle-{arEiF%fU+rsIm(N+<3&WWYBqSwIkqzL0~FJ*6#gYB3_V%HvC0$s5a1b?q@g#97m&G zkcOff*a73?)hn_g- zDpuI?&3E=-%o@XAcA8?OgzZ8ZqfLD2=1oLpc1YW8x9h{str^aSPPdU#rQonTtukI* z2}jU#mO%s{G1q)78v1B~mg!7XdpPGQnyJ&Gj=7f>lk0ftSRC8Qg_(WxlFGd)WM&bm^wO6uoh{H5461#$SZF1^-k#0Kq-@cNO*Z*z>Q}OUja+>aVBo7 zth}nvG)_1aJ>W6d@m`W7)T1ld1x?9lGWQwJBfzedGTiXxGPTz&O7W!Zp^m7g$jX>f z3?oerak{HFLwr~#iPA@*(Vcsu%@|Fhy8?0%iTLWzTaFCa8F$vkH6;Zk{yS7Zf?qpK z#nY418Z^%3C%;$+#NFf)l+0|>5KlC0Ro5}Kd4FRe?$`c)mokGDJ`tJr#_MHB=_LOn zaD_i8O!c5NiZ%ZpPmwJ@;nq5YAG^#NO)-h$!{D7V77|=#3?9Djg3fpmNQ_^^%>`%Q zue)H3l7D6;WX9sEp0Lm_R%|p)3UP5*k&V5^aKKLW&fMlK4=D^3YTaJyy!I+S{5Gvr z`8bWiwC$8`)WnPyjZ&@Z`WD>!y&!&kRYMU_JYr%~qzUBC%@UKm^|T!)Yi3)3Sy+%9 zGmvWL_a4RHuPux%sH^(Xt=-`;9T(`_*%Gn4$WnBoL1<3KsH^0m z_D!4fVt=?vBy-ngx6A7Lh*+gTC!waHUn6BC8UO2h-?l@a``qS=0if?K)nct_*!yjP zL7Ka0G55LO2_K`d=_}!a_`}51brEVJZg~ocC3a3^Pd>P@f|jUA7dI^<)L5KFPD{t$ ze{ZynuVuBVz52kD&UH)ho#v;g;WYl+z*+66TWOlbMy>~>BB9i845?j#@89dfO|Xv; zVis2U_t%Z&k1VHC4sngq)*#;Ww~eP&vs z7)!SIynm-{2XnSHa)(H(ypvWokZyh9Uw5EwN`ZllRZvm!HipW@N|)@9+7#E@B)?>b9U<;|9)KLh)D| z9g&*aEjGXZEF@?1L*z@4$%JpjooThF1gE=1f2g9hrJ#0Cti3h^NSc)+f*P$q_o^*Y zC-3t`9yz(T25DgJtfw`Ll>eF*|CUDsmdGm4$?V_WGUcnoV+fEV2l1+bG1e~1JM}s= z6I6$8zE^5L(g^+Zk7enI`{l-deVkQM+!5fI6rS%aZpIXz=AfKCd>AHJ^#PP-dLdsT z9DYh*MIE$Ve`9Vt6Kh>KOkjQN8dBpl5MMnQqUU9(pFK=Dl)SXt0H}Xuk(A7#{FQo3 z$iZ(p!8;Q>oFc@WWr7R?Y{aA;>5a%IG~dry_*R`2*yg{L?AA&4+$d1Sruo*|@L=&- zAf@AzOrYT}P^w-c!99szFYARdgUmWvT_dO@%{JowkU&Z@lSiRgPZQQkif9uPhsx}z ztIBSpnGU5LW1XFL1Naxm!De+?wZAcAUwc5+=7`YF`3%0C^b zZ(BY*UHX+^GUx(OL*cxU>P#z=^Hr@C?Bsp%YIwY1sH4Y1Hg6yLyuZQeE5=W^jxc2J zFK{8}({=U6&P$%~fr(%7^@h!;OVY(Lo<1ylWwHBWzqQwXaq>**RkWDwIY4jq^2T)A zI9B2A-Hr8~`LRyqMOvHW-1C#6XT$~>>nN6&`_1puba}*pk5`to?Qy)Ki7^IaDWbIQ z0_o0fIlyG*c;BjmrTHq4@UNqb8n*4Rzn-YTFOFdnnR(YaVW%xg6@3K- zg}k<9QLMr9bH|Gyo5){^gvftJO%56S?6)@vVq-0fifXyPV#wL_5f@Jj_G)38xJS^q zwu%dwr&jl^#u=$ON(^(08PE6`@8v!VzjaGwY9Kkji8O}V*u^g zTBEPlY7bB$2*q7mjUpWWI+0@WV)9kiJl_1G&Jgd+d+agw{R4=OIr$`=oGz}uSDxVO z0sX%gfgJIkVnaB!CgC<04Yggx__<6jhny`K(`Z4YyMQq*KFw#uGu%H&`XmoLy6qR+ zbo{>g&x5T2T}8mz+nR_@^>==cF}-IWW9dOpW=`waX1F|wBqP$PONOuqV%$Ojd;Y4T z9x6FLoLl>GdcnzS^KnK;FTqtAOm)ezvPJ2OaPzTk)+yu9*OcVckcCcJ{|g$@V?iz3 zq~K}JT9FI*IA*_5ASRGGTZ^98u||UCM1=x-!lDV7bi3BunE!s(X%brBx617$a8Hsh z+}ihu&5cpRS)26B6~B3Bli7i;Ey-F^neBiOv+2BYRJ^ii2y2DBF<$J7D#J?q&408( z(webuDP`pzQ(jct-)r>@UBh#feqq*ZzFI*!N7kw}){p6AIJ zKQ*i$MG3it5ASy~1KmlE+sVVX=Ai@GGnXVfaJ1q&E71an?^SiD@C8JB$wo^NvCd7Q z^iQn){h9a(YkQ1|4YMN5K61IkYotrsUMcb^EG2U^I{M3==3}SA>Y`2xM}_UfwPl^T zG$LyGD;eHiin}CrS{n^^$E)PdFZtJwRo}7BW+WlqAcX}}u-qH9uYNf2x(bs=x2wjmmZ2qUDSb4d0Yn~9|2y&^yMUPd zp9kaRl@-@JMjyXg;Vqgok(-xqY`Vv6cEf>68P)0on zi?l;s$&!MD9z&Egm7vCGh4};mfdCf$5Nbf-QcCUHf4`hxdn%-Rnn=@wnFj<++Aq^D zZEdo&H4g9OSMLwdIBU(%`+bPNZ|_%)m{mR--4Y;Vrlr-LfE_RnYxMQfN1kY;u2BT2 z;@yn$)wqvnz)U`8M^8R-UeUCpyxnZ!b$ESy>rGtC+vZz0WOoRsTO8l6d%)n}gi6TewSuL(XmWoy2r&>3!xfh6VVw;DIZP3p{Li?p zK^zerqu6mWxeTM?{;mS@K9?un^74SfE5rh7S`I-CsPc%UH2X67?^^KKfbOPM)urWw z)FIYrQk4{{_+{_<33?j}tJ4B_(Jg;BAZ^NOH*nDt-8>IWy!3u-UWazwV$0vvP-tNs zFd>q8b1i)QZg)aSKY+(jP68{Pxa<0tTv}F^q=Evf$Ynwt{r7t?@Rbw>2Il^Eu}H}B zvX1Z5wn=;TWR&a>?SIEpj1ScmSTi`*G+m?dP$BA{aU~~+O^~>F;C-7Tl`X1PCBMqo zM@y$*kwv#3pujRA6O~{=h1S}BQ`_uO6o#S(?nkT6B_@bBUwFHqeQM&3XnCwX`VYJUDDdm6PM?`32 z#QWBuOF6#8e2MS5E5@H~R4JYnEQu6wK*GV|0&GwL+f&<^+-i8()d6(KKS4*<^B3)w z9dy>)cjXZ|42jI{Dhpq8b*L=R5J|&9j%&Gid5Pi?f!-cZMwxxBdUB~zPiDrpA?c`7EwH}=U7vfhu+MAu$bWCgb(-Hdc|MmhH zy!MIu6{m^4A2Ix38o53;9O{UpX(w#{)sH3t-F>E^($n40$#u8(@*&zcXV*aBvA4nX z%fiQ|Wp)Ne*Hcfkj@MH=DsrW)#tq&(mEjR_V*)vm`Mw5f4hAstBg5e0)1JzHEyM$~ z>){|)^9x8=rm7QR)K07Y#yOtYz0{mi7<)DXo2!QM*YDprW#tih;GdhD#sg1_UWFZy zD}u(p-WTT8pG$Ep7}i{G6Zky)b@*J8_?}nLe9U^Fsd#uwyq{wY2q~C)VHWXzxJx1` z)BH4ojTtRy*hNgl9tgfQ)G>x-lRT@#SI2s2aI4MH9^@{YfOg1_r;u zjx1rlRAFOG?w}W7{}QdrDt{)ia>eH*O89!6g+KqHsbacI3py@YcK@|?BH^yTV)6Sr zJJS0kL|o20As&UYf-UW_p!(C)tlg(1)l{L0z$=b4n1@qIF|sv>fx<@7d(Nl$$JUpQp*3nuK$gBWfW+s zgaJuPsF%&_)gbIek8_2NfO}YNEZ`slKq!RQzY(EZ@QG_(=+<}w9B;aBq56(lVUsQn zNNHkvG+$7!s`j_{ zo39*5sYY8}kh$Twl$SUf`ECAgN&3|?*8|=CfF+t#D36w%z9nzF?_#29R+TKJf>h3J0qIcjlX)*{@MY9ZzIq* z{HV`|MZ2N~1T9P2uJ&b6MjKw=q2!a^Rh#_~38P(&$vts!)>n+M)vHD2Mn^Lm76`lc z4YIcDbzb!x-{lj0VDg^l_aX53+ct#nG0|A(7V{y}CQZb3Htv*duff_RbQqcm=2csy zr;N%Z>x-SwtRIa-l_L9NIhrw zLFvLeGX{U##ADJMFoPw{u7|3BxIcH#hc^VRm%58S&!QU-DFFj6m9K%zB%;zc+oF9L zuQ&AbB3GuXkC&p`pl0CEcN#fLx5JhUf5imBw;a*sBfGBSomDpukS>{i112xF_)bGz zWBM&~+m|)-MZTJq#*;rywfY+z2Gx0qrh)WBUt5a45(uMGT)qY_D&iC~tQBZMyf~}MI5b(+<8WDxihFd#fhHwbdW zD5G?#D3b3M`2c1k+HXhi6EIVJz8+nONkX7<++jP?&6SaOeVR3vhh3;T`Q8c6cAD5> zUqmc_c=zE9vpq03F0YZhWTN^-ew1?vm*=IX&KZT<$d9J`PDtl2gM{$$*w!8_94W0* z0}$b8Jkj=_^ZDV)iYfDcr&C^@oc~~)jMa%!PDF|QhL4g`h7AXKhsh9wLkJR|e9VYA z=CWqrM=Yrg8a$Dtdc5$jrXCT`^E}Rnv2n^6E}%M^H*69`S3DoL|978(GEdb`!-4R5@8btLq9w7yH!hxo;0C(iR~ zGZh!6Zyx&%(Wy@Sqb4>53E>~5KH0P34}-4W@O+m_V$SXI2G8Fd56Y0$rQsz#J(@)^ zd|H$t9(=lX>Lw4Tl0L$Me(LiEvvUD2v{BmkF=@J+$r#?3y@d@o)IcxTizRHp^$Jxn z!iU3+HvkHUrgT3uJ+iP!iiO159CD!C9Rd?^%SXMkN+S846|@>-XU1TLKU$EUOKRJC@C60*bBJ+D zYBFlY5gn+u4CLLYMG^2JAnOy3%T8^CK7HlBHMo}Dm(tQk%x=nit)}MHJi1b(%*q*f zrXm-WWQ{}d)>>MQ$#u(#tJNfa(C*CcOtbK7uhK3$Kc5{|V|WQJp8CN)jDHNIoXT>h ziW7NJrRK#6Qab?UN`QdgqXzxF8u=6r`ZV|~VhU223!jEQxkkl_^*rjdy>aX2Pp<7z zI^hpproH6jm#>{y7YG4eljvVrQrcICSEn_ci;Kw&?pQ_2lk%HT>ryO1KagsZyO|(U z=VIgU6LYU=RQ;ajDX~=@MOWgy*35`VB!w9OW~hDx>pR!@T)zVnkVeD?S6;O zh`4|QSQb5JcmUrv3X@J!fTh40RmVLc$hxfftG?pYFwe3BqF!VU0_$+oq#{NJk=5Cj zw0=yr{AoCsUHe8|MWMr*W=%-R4*sI>^-8z&q?ASQ51-}jmQEOMeX|+Y;AOrbo08Y! z5*aEW5_zHw>l*4Aoa?S%-Z*)k8GAbjdcK|CXEn@WO>SmU-UpR4$mZ+y49QRj{PjC7 z3tjv(m)~6*dbm4FjIpT>Y^lr+BIt}x`T?F&UP z>+ig@@E-S#*?VG-)xq8SnT!&Urt?0ueYtJPHd;>-L>LYSZ5n zdY=AdN@lz ze)P^wA*5OkJ9f!?3RaaaQ;Z|RcY7+kzad@hGVe&9T=YSFE&qDq)-dPm!xdV(_(Rwg zHPW9_OBoY!dh_=153Bvo($+TmYeFcUzzF*@>+2=!A%xahm`n6zEpYQ{kS+2!fj^tu z@tk_d`t+Q0{pzmE(dluve!b=a%-(DJ-iPV=bk>z&??c%E$fEb#ow-iVJx~z+Kiz%x zS6o@sE)WRr?ry=|0zn!N5?q731Sd!n+}+(>8h1;AySpYd+DPzV;dW-`o9~^u@BIfZ zzp&2gbXYZqxc8-dzaJxnDlt%Yd z>qn(pG@BTDu>od{#`f*}$x@yT^lA~T*zRkjs=8%!i+WQNZgf?CnKfRI9VM_xLCjm6 zPs1ptPvkH?pD;zY7X_RzW>$E{ShiC>9mqN_Xlj3}AEM|Bd-J3s=v#=*8>Ful4o z(fQD*p!Fu%Dg@L9M~|aByup?4g<#Uu zx2r~rcU}!`=B%DFosw?Ic117ZnZ~lWhzh~d({USEpL(|J5_qIZ9V15wl?(1jE~-D? z5Vf-nI;%9Ddz|w2v@e>uvh^>#u~rs*Uf(@%s#(=UA8UY2Q^-y? zCX)B$`VLu+jB(IO|CNfR4XVW1jDk{XJYzPkT=!(GQDEwgFZi7 zwYQ|#yexQ)WVB^%|G*o>&n*?=wQ5T_&63lZKt`YN32N_I9#vc!pKEDwvVc6 zLqY0+r8vx~8JIda;OLo)t9^-##(aAiW$QXkJhX9R z?x;$>TiTYlj<;`iED$w)j!t78r$jC7%Iz<`Od`lZZTk~H#eCdvht^cFY;tgJOBUl$ zU?)j&GI30~AOwlFta57FJ$zmDlhgyY{+`Md7HthPBazy6M>d6$l?8_fOir&yuj{uj zJ=bF72r_Scw%}rW5no>;a=>R|U0k0%k5h-ec_V>O|EGC?MZ$BCpEonc`Q0qC>*x2V zNLI1?gCZhI${~TCb9Cx(lH$)SXFxEO=*CKSc!0Z=UBI9?B1;3!(Bz_t3ZyMOnK$5u zVD;g_fZhOS#p0BjwZsqfD6!gDj)NTn$|FssYOIA$DO*=Coy?>m%za^Abw(G*-0^T% z85KRWMn}pop9Qb`-ortW^pYdo# zzjM26*bL)<+R2ye8Z)u)F06F=1QZeh{$^bLLL-<(WY+st7@G8C>9kec?uSKj=J+w6b+1AP=n~oDp)NE$Hp5%dtHbkA__e}4yQ zoZ_t-?*0QrE<+DU)WK=K@qPLIddaAka`!jvgCFYe!ezJz|7xakfzL2p%;Jk)aF0mp z95yirwFF1cE1)0!toe!U{i7eR+IFVLt!`_K=fcHEiY6ajtB9~xpI!4jj{AmzpQM%a$DVhaRB5Bp*xe$6!A+$zUX?R*|d&3rt9O`)4rr7dZRpKV?ifJglbj*s{`6 zRl}OcY8NmXCT5ZOX{pdGz0`j|xqsheJV*@%r3aNVm{|Ysm{$A_3oB7a%wD?Tf5iAR zBLCxx2LqNmg2!3EA`bCy4G;@h6xRJ0 z-R(*lzrO&wwY}cg5vqCd!F>F+0}|aT>?LXW6SEmrKCRw27d9;Y5$jJw+D(_ zB=#?TihMv6@BiTuMl^e=ayyfp97aEx(w^!(34qdD<#=y8Gh2E6?_LOcQJ;~sJQ9oe_l`St=SQ`tDl@tI(D|6Q(pP|S>jBk>c07)~ zi4J0nt40ys;gfi&>UDT$HEN`xfrP z7CT~3tetmIGc{awd+;t2HWKZhnoINO+!^~`k41U4@l*UvX zmgB3LP63BwST2v=sU=4fwD7`OxrA$6OWw8*Tw4%hhnsP1D-&s;ud7wa6)=;F*WRHf zeZ3xIo^>3d(Kh<&KUdGVDg3K+x{h*O=7-VMG^ev3AOJ^b%86?MR|+sIV3c;5GGfU< zgwdHIW}Yg1JHLe9`YEAuQ-xq$o4j>)d~kUED3KN^nG%*Jq|c_Q{yMHYpeB+jF{IuZ zYozBTifK~+wl${ky%$G2nu}D8453U`t1y(*idME}KsbZFba7CY zZj`Js_i3S4$#L^?>P=o2v(SHP6A7*!`{%LM8`4fCAyTNYbfz~XC(>O~alYs$xEjSy zT%Z!sfZHuaAN=NC=Cql;FZsS~a8=JZc6l9yw=R9Ii^dZ|8HvX+L*yxJspD$yd3uyC z>Lon*85~euj7GwoU>?^jez9e!0~*7-Pbd>*RiZahd#Vd`$Or7zyq|loxyGqhKEz;g z)5u$`7oyid?Q!i9A63zsqsen-R`SF>R#m6*pSDjND?Au#gaG($p*K|b=wIBCCzw%2 zr!?Gd1ub*0(Gf?SnYX^%F$>11)OuOi=|W*dg>dn{SJBS?#B2>AH>9?vvRZwUtMDWe zUYU>zV9;Jhq=9NkX{rowgAa2nBjG}i24*`*_<=0NI;B=5q3Rz-^s|91)j2pxr4#~z z0G;JZ)WD|9{5(cZbm~}w6G(LWxJ35fjiK$-un+$L-Trn##(LO-zcbfQzcbgF(J%dM zK7J&^_l6J6>qumTx<6ugrg$Yn#A@uo^Zv(YR_l4nbhC|Y$QL6q9(-E zPbbKgRahO${KG;UJ5s3@_~7in{K>5#K_v!vnm}|;s(|_ra{wzrg#%|Q9}SuubAm)ivDZ6BmWTDlGv+pKB*TDpD4(}72cNJ|X zCdyX)OfWO`^$xYp9=6W(moKZn+!Dg1yVzcpaBk>@Djr;l5>5noWu`so`*Wl882%guy$ViD2g$IN#7^bM*QVd^f|PueNE*EI>(Bk1 zP7`cx#GX5$jHOOykc&V>z06Q^iN_WxET6ZMa^_7Bz{h86&Lb_DF7S1m4`iJv%alJPJDP&ShU zL-Jm~eK!6qsEA>Z7*7@U%oRKRf{{W>x20lbi$zoPw1_c# zPSL6DMF3Aj6%VkZ2>|SsLAMXsnezA&R5Vce^o(lct6DlwT`qELal5aZ7_Rl2maO5f zu?Z?Z_3H9zP+oTr8=i8xWkEM0GE>#ZF6$NN@d^lOTfI*BzIQ%#60d5_fwm$%}A zgTIznFU^leEcSRHr)g>A>g%1VISM_K$Mp>;n3sQ3dzFX6rpTuB4fHkCMstYHzGHSx*C-9-V0d(WM` zTWg#SlI4=MuxRk=jVLRvbT}Gk2SJ&-!<7Fe!BbAbCoC)BbQsbB)F#(A?3i(E2ktuQfdJ>ve zWem>7n{V=u`-Ehs){Cokfq`TFZfB3bdJYTh-xBHW3(w{ZeKyO;%~cAv90Z0W_FFlt zEWtMHHCwH}=@p+MT0Z%(k;PuUQs^}^N3=@3_-*&6_EJYhb+!<}L|scqH6C{pYdZ?u zJ1i%I_j>v(*Hcfa*}eD^ZKbR1TCgC+T>kqtkKPVHui~<@?v=6<%FbWO^9Zj{WYidm z0+5+v+X_%Lo6!B!6(-Yc_{ek2@sIh3DQW^h@2=c}yPBNCG)boomf|Fgp6gye&3do$ zE=KgAf5~)}=iSKWOTYHJ*6&*sw)hrcmx)@_&rxLFe7Kk=;u`Z1ZDHz*=gzl4Os!dE z%f+24sp?m1TAcNIStb#?8ON;RlTmQTMqHHSL{)ygrXsR9BS=hpUt=r z@T`s6-!Y~E+71TvZDi`{X=#mgTkkj}7ueu7f%j2Pg(2;JD>fj3fY*lG22ZH99kTZ6 z@zjbw#wzS1{~3y(1khq=YK#OIWUwrIR| zX>Gjs^NI+qTeZ=u8{lPkeiVgoY$Ac@#Z;V@IMidPosEx?dQ}% zT=lR@Y%f=M*6=FZnQ6<6^vbg2s$xG2yf0d6>p%d$)G2OP?h})hx!rSkX0kQV?k!mL zuve?*oz!`2BE24F3Q5U&=KD&y$bZAw3qa5V{3$#Gk#sGHLU5r z>`4*Be9CZy^HY)e`LrY^18lV8YVV5u*yA*URF-9I$U_%AwEZ~CPbz%t&Npo&Vt(Xv zlb#77{XzLAibixeyf@|A{V@Suz^#bYi4``FYpa*My-$!$2)k~qn)XRrwUDckzj8Hn zqc`aoV>7TIUq~1|XblL)8r zWLkGwPM&`=aE%4De{6oqs^6@}+4b9)IQ5g!hK5;wq;0l_Eto{!La=UM;n##kya$u8 z8H`5ZVQ~YFQ338gY@yL<DBQA^!q&dygZ9G%b7Pi zF=#A$idHiFp$s%H?1`g=Qqjbv*Mm(U;S+v~uAkBX$AUo;e5EGbW`TxcF{49&gCPk8fRuR*ml$hr--4{Tq^96 z+y`4z@7p!6u6=R+MS5I+Oqcq{CHys)_w=?czd*1?7cC9Vq_R9Hu2AN?PLh|0W82A0 zXG4Z4;s!7@8~>vt%^3F)SHab5tKEuDaiK53kGo#GYr=?!hbbn9ba8>=bkIfgwR4M3 zt#LTzU`@p$f|rz6^>ZA*_l$-1B6d@}dqZtNkiU-CJ;biDVton*KHK;et@$UprmUk` zG0A&|b+aM$#U7<)lddBW2S7vhPOTTHFbUg5-8zj5Dle(2K8JHnTFY9 zeHPOrOWrtORiImwv^9Tu;KR?@lB9J-#Jem~M6w|^P*6+u-M;L>cnwZyP3~O&7X3nb zy@3BKIE^R<9}FI%x`lGms?fgo(M1Q~N~2kK%Y2!8$CC=}#f_K05~Jloq|?O#WYs

g5x9WXk}s0J!gce&f$p|)7B!#fe_wWzKn1$ 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 TRsx#{1O1biRtA71jRXG=3IsDK literal 0 HcmV?d00001 diff --git a/docs/installation/images/windows-boot2docker-cmd.png b/docs/installation/images/windows-boot2docker-cmd.png new file mode 100644 index 0000000000000000000000000000000000000000..09e3206ef97ecef230a24e13e9b25e106f7b576e GIT binary patch literal 37436 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 literal 0 HcmV?d00001 diff --git a/docs/installation/images/windows-boot2docker-powershell.png b/docs/installation/images/windows-boot2docker-powershell.png new file mode 100644 index 0000000000000000000000000000000000000000..b1ef89672619b3dd7f4c1ee7d269f221bec3395d GIT binary patch literal 37945 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

ARw{c%q1S3=%OeW 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 literal 0 HcmV?d00001 diff --git a/docs/installation/index.md b/docs/installation/index.md new file mode 100644 index 00000000..f4c530a1 --- /dev/null +++ b/docs/installation/index.md @@ -0,0 +1,48 @@ + + +# Install Docker Engine + +Docker Engine is supported on Linux, Cloud, Windows, and OS X. Installation instructions are available for the following: + +## On Linux +* [Arch Linux](linux/archlinux.md) +* [CentOS](linux/centos.md) +* [CRUX Linux](linux/cruxlinux.md) +* [Debian](linux/debian.md) +* [Fedora](linux/fedora.md) +* [FrugalWare](linux/frugalware.md) +* [Gentoo](linux/gentoolinux.md) +* [Oracle Linux](linux/oracle.md) +* [Red Hat Enterprise Linux](linux/rhel.md) +* [openSUSE and SUSE Linux Enterprise](linux/SUSE.md) +* [Ubuntu](linux/ubuntulinux.md) + +If your linux distribution is not listed above, don't give up yet. To try out Docker on a distribution that is not listed above, go here: [Installation from binaries](binaries.md). + +## On Cloud +* [Choose how to Install](cloud/overview.md) +* [Example: Manual install on a cloud provider](cloud/cloud-ex-aws.md) +* [Example: Use Docker Machine to provision cloud hosts](cloud/cloud-ex-machine-ocean.md) + +## On OSX and Windows +* [Mac OS X](mac.md) +* [Windows](windows.md) + +## The Docker Archives +Instructions for installing prior releases of Docker can be found in the following docker archives: +[Docker v1.7](http://docs.docker.com/v1.7/), [Docker v1.6](http://docs.docker.com/v1.6/), [Docker v1.5](http://docs.docker.com/v1.5/), and [Docker v1.4](http://docs.docker.com/v1.4/). + +## Where to go after installing +* [About Docker Engine](../index.md) +* [Support](https://www.docker.com/support/) +* [Training](https://training.docker.com//) diff --git a/docs/installation/linux/SUSE.md b/docs/installation/linux/SUSE.md new file mode 100644 index 00000000..797a329e --- /dev/null +++ b/docs/installation/linux/SUSE.md @@ -0,0 +1,117 @@ + + +# openSUSE and SUSE Linux Enterprise + +This page provides instructions for installing and configuring the latest +Docker Engine software on openSUSE and SUSE systems. + +>**Note:** You can also find bleeding edge Docker versions inside of the repositories maintained by the [Virtualization:containers project](https://build.opensuse.org/project/show/Virtualization:containers) on the [Open Build Service](https://build.opensuse.org/). This project delivers also other packages that are related with the Docker ecosystem (for example, Docker Compose). + +## Prerequisites + +You must be running a 64 bit architecture. + +## openSUSE + +Docker is part of the official openSUSE repositories starting from 13.2. No +additional repository is required on your system. + +## SUSE Linux Enterprise + +Docker is officially supported on SUSE Linux Enterprise 12 and later. You can find the latest supported Docker packages inside the `Container` module. To enable this module, do the following: + +1. Start YaST, and select *Software > Software Repositories*. +2. Click *Add* to open the add-on dialog. +3. Select *Extensions and Module from Registration Server* and click *Next*. +4. From the list of available extensions and modules, select *Container Module* and click *Next*. + The containers module and its repositories are added to your system. +5. If you use Subscription Management Tool, update the list of repositories at the SMT server. + +Otherwise execute the following command: + + $ sudo SUSEConnect -p sle-module-containers/12/x86_64 -r '' + + >**Note:** currently the `-r ''` flag is required to avoid a known limitation of `SUSEConnect`. + +The [Virtualization:containers project](https://build.opensuse.org/project/show/Virtualization:containers) +on the [Open Build Service](https://build.opensuse.org/) contains also bleeding +edge Docker packages for SUSE Linux Enterprise. However these packages are +**not supported** by SUSE. + +### Install Docker + +1. Install the Docker package: + + $ sudo zypper in docker + +2. Start the Docker daemon. + + $ sudo systemctl start docker + +3. Test the Docker installation. + + $ sudo docker run hello-world + +## Configure Docker boot options + +You can use these steps on openSUSE or SUSE Linux Enterprise. To start the `docker daemon` at boot, set the following: + + $ sudo systemctl enable docker + +The `docker` package creates a new group named `docker`. Users, other than +`root` user, must be part of this group to interact with the +Docker daemon. You can add users with this command syntax: + + sudo /usr/sbin/usermod -a -G docker + +Once you add a user, make sure they relog to pick up these new permissions. + +## Enable external network access + +If you want your containers to be able to access the external network, you must +enable the `net.ipv4.ip_forward` rule. To do this, use YaST. + +For openSUSE Tumbleweed and later, browse to the **System -> Network Settings -> Routing** menu. For SUSE Linux Enterprise 12 and previous openSUSE versions, browse to **Network Devices -> Network Settings -> Routing** menu (f) and check the *Enable IPv4 Forwarding* box. + +When networking is handled by the Network Manager, instead of YaST you must edit +the `/etc/sysconfig/SuSEfirewall2` file needs by hand to ensure the `FW_ROUTE` +flag is set to `yes` like so: + + FW_ROUTE="yes" + +## Custom daemon options + +If you need to add an HTTP Proxy, set a different directory or partition for the +Docker runtime files, or make other customizations, read the systemd article to +learn how to [customize your systemd Docker daemon options](../../admin/systemd.md). + +## Uninstallation + +To uninstall the Docker package: + + $ sudo zypper rm docker + +The above command does not remove images, containers, volumes, or user created +configuration files on your host. If you wish to delete all images, containers, +and volumes run the following command: + + $ rm -rf /var/lib/docker + +You must delete the user created configuration files manually. + +## Where to go from here + +You can find more details about Docker on openSUSE or SUSE Linux Enterprise in the +[Docker quick start guide](https://www.suse.com/documentation/sles-12/dockerquick/data/dockerquick.html) +on the SUSE website. The document targets SUSE Linux Enterprise, but its contents apply also to openSUSE. + +Continue to the [User Guide](../../userguide/index.md). diff --git a/docs/installation/linux/archlinux.md b/docs/installation/linux/archlinux.md new file mode 100644 index 00000000..b62b21c6 --- /dev/null +++ b/docs/installation/linux/archlinux.md @@ -0,0 +1,105 @@ + + +# Arch Linux + +Installing on Arch Linux can be handled via the package in community: + + - [docker](https://www.archlinux.org/packages/community/x86_64/docker/) + +or the following AUR package: + + - [docker-git](https://aur.archlinux.org/packages/docker-git/) + +The docker package will install the latest tagged version of docker. The +docker-git package will build from the current master branch. + +## Dependencies + +Docker depends on several packages which are specified as dependencies +in the packages. The core dependencies are: + + - bridge-utils + - device-mapper + - iproute2 + - sqlite + +## Installation + +For the normal package a simple + + $ sudo pacman -S docker + +is all that is needed. + +For the AUR package execute: + + $ yaourt -S docker-git + +The instructions here assume **yaourt** is installed. See [Arch User +Repository](https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages) +for information on building and installing packages from the AUR if you +have not done so before. + +## Starting Docker + +There is a systemd service unit created for docker. To start the docker +service: + + $ sudo systemctl start docker + +To start on system boot: + + $ sudo systemctl enable docker + +## Custom daemon options + +If you need to add an HTTP Proxy, set a different directory or partition for the +Docker runtime files, or make other customizations, read our systemd article to +learn how to [customize your systemd Docker daemon options](../../admin/systemd.md). + +## Running Docker with a manually-defined network + +If you manually configure your network using `systemd-network` version 220 or +higher, containers you start with Docker may be unable to access your network. +Beginning with version 220, the forwarding setting for a given network +(`net.ipv4.conf..forwarding`) defaults to *off*. This setting +prevents IP forwarding. It also conflicts with Docker which enables the +`net.ipv4.conf.all.forwarding` setting within a container. + +To work around this, edit the `.network` file in +`/etc/systemd/network/` on your Docker host add the following block: + +``` +[Network] +... +IPForward=kernel +... +``` + +This configuration allows IP forwarding from the container as expected. +## Uninstallation + +To uninstall the Docker package: + + $ sudo pacman -R docker + +To uninstall the Docker package and dependencies that are no longer needed: + + $ sudo pacman -Rns docker + +The above commands will not remove images, containers, volumes, or user created +configuration files on your host. If you wish to delete all images, containers, +and volumes run the following command: + + $ rm -rf /var/lib/docker + +You must delete the user created configuration files manually. diff --git a/docs/installation/linux/centos.md b/docs/installation/linux/centos.md new file mode 100644 index 00000000..1647f766 --- /dev/null +++ b/docs/installation/linux/centos.md @@ -0,0 +1,192 @@ + + +# CentOS + +Docker runs on CentOS 7.X. An installation on other binary compatible EL7 +distributions such as Scientific Linux might succeed, but Docker does not test +or support Docker on these distributions. + +This page instructs you to install using Docker-managed release packages and +installation mechanisms. Using these packages ensures you get the latest release +of Docker. If you wish to install using CentOS-managed packages, consult your +CentOS documentation. + +## Prerequisites + +Docker requires a 64-bit installation regardless of your CentOS version. Also, +your kernel must be 3.10 at minimum, which CentOS 7 runs. + +To check your current kernel version, open a terminal and use `uname -r` to +display your kernel version: + + $ uname -r + 3.10.0-229.el7.x86_64 + +Finally, it is recommended that you fully update your system. Please keep in +mind that your system should be fully patched to fix any potential kernel bugs. +Any reported kernel bugs may have already been fixed on the latest kernel +packages. + +## Install + +There are two ways to install Docker Engine. You can install using the `yum` +package manager. Or you can use `curl` with the `get.docker.com` site. This +second method runs an installation script which also installs via the `yum` +package manager. + +### Install with yum + +1. Log into your machine as a user with `sudo` or `root` privileges. + +2. Make sure your existing yum packages are up-to-date. + + $ sudo yum update + +3. Add the yum repo. + + $ sudo tee /etc/yum.repos.d/docker.repo <<-'EOF' + [dockerrepo] + name=Docker Repository + baseurl=https://yum.dockerproject.org/repo/main/centos/$releasever/ + enabled=1 + gpgcheck=1 + gpgkey=https://yum.dockerproject.org/gpg + EOF + +4. Install the Docker package. + + $ sudo yum install docker-engine + +5. Start the Docker daemon. + + $ sudo service docker start + +6. Verify `docker` is installed correctly by running a test image in a container. + + $ sudo docker run hello-world + Unable to find image 'hello-world:latest' locally + latest: Pulling from hello-world + a8219747be10: Pull complete + 91c95931e552: Already exists + hello-world:latest: The image you are pulling has been verified. Important: image verification is a tech preview feature and should not be relied on to provide security. + Digest: sha256:aa03e5d0d5553b4c3473e89c8619cf79df368babd1.7.1cf5daeb82aab55838d + Status: Downloaded newer image for hello-world:latest + Hello from Docker. + This message shows that your installation appears to be working correctly. + + To generate this message, Docker took the following steps: + 1. The Docker client contacted the Docker daemon. + 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. + (Assuming it was not already locally available.) + 3. The Docker daemon created a new container from that image which runs the + executable that produces the output you are currently reading. + 4. The Docker daemon streamed that output to the Docker client, which sent it + to your terminal. + + To try something more ambitious, you can run an Ubuntu container with: + $ docker run -it ubuntu bash + + For more examples and ideas, visit: + http://docs.docker.com/userguide/ + +### Install with the script + + +1. Log into your machine as a user with `sudo` or `root` privileges. + +2. Make sure your existing yum packages are up-to-date. + + $ sudo yum update + +3. Run the Docker installation script. + + $ curl -fsSL https://get.docker.com/ | sh + + This script adds the `docker.repo` repository and installs Docker. + +4. Start the Docker daemon. + + $ sudo service docker start + +5. Verify `docker` is installed correctly by running a test image in a container. + + $ sudo docker run hello-world + + +## Create a docker group + +The `docker` daemon binds to a Unix socket instead of a TCP port. By default +that Unix socket is owned by the user `root` and other users can access it with +`sudo`. For this reason, `docker` daemon always runs as the `root` user. + +To avoid having to use `sudo` when you use the `docker` command, create a Unix +group called `docker` and add users to it. When the `docker` daemon starts, it +makes the ownership of the Unix socket read/writable by the `docker` group. + +>**Warning**: The `docker` group is equivalent to the `root` user; For details +>on how this impacts security in your system, see [*Docker Daemon Attack +>Surface*](../../security/security.md#docker-daemon-attack-surface) for details. + +To create the `docker` group and add your user: + +1. Log into Centos as a user with `sudo` privileges. + +2. Create the `docker` group. + + `sudo groupadd docker` + +3. Add your user to `docker` group. + + `sudo usermod -aG docker your_username` + +4. Log out and log back in. + + This ensures your user is running with the correct permissions. + +5. Verify your work by running `docker` without `sudo`. + + $ docker run hello-world + +## Start the docker daemon at boot + +To ensure Docker starts when you boot your system, do the following: + + $ sudo chkconfig docker on + +If you need to add an HTTP Proxy, set a different directory or partition for the +Docker runtime files, or make other customizations, read our Systemd article to +learn how to [customize your Systemd Docker daemon options](../../admin/systemd.md). + + +## Uninstall + +You can uninstall the Docker software with `yum`. + +1. List the package you have installed. + + $ yum list installed | grep docker + yum list installed | grep docker + docker-engine.x86_64 1.7.1-1.el7 @/docker-engine-1.7.1-1.el7.x86_64.rpm + +2. Remove the package. + + $ sudo yum -y remove docker-engine.x86_64 + + This command does not remove images, containers, volumes, or user-created + configuration files on your host. + +3. To delete all images, containers, and volumes, run the following command: + + $ rm -rf /var/lib/docker + +4. Locate and delete any user-created configuration files. diff --git a/docs/installation/linux/cruxlinux.md b/docs/installation/linux/cruxlinux.md new file mode 100644 index 00000000..6c95110b --- /dev/null +++ b/docs/installation/linux/cruxlinux.md @@ -0,0 +1,93 @@ + + +# CRUX Linux + +Installing on CRUX Linux can be handled via the contrib ports from +[James Mills](http://prologic.shortcircuit.net.au/) and are included in the +official [contrib](http://crux.nu/portdb/?a=repo&q=contrib) ports: + +- docker + +The `docker` port will build and install the latest tagged version of Docker. + + +## Installation + +Assuming you have contrib enabled, update your ports tree and install docker: + + $ sudo prt-get depinst docker + + +## Kernel requirements + +To have a working **CRUX+Docker** Host you must ensure your Kernel has +the necessary modules enabled for the Docker Daemon to function correctly. + +Please read the `README`: + + $ sudo prt-get readme docker + +The `docker` port installs the `contrib/check-config.sh` script +provided by the Docker contributors for checking your kernel +configuration as a suitable Docker host. + +To check your Kernel configuration run: + + $ /usr/share/docker/check-config.sh + +## Starting Docker + +There is a rc script created for Docker. To start the Docker service: + + $ sudo /etc/rc.d/docker start + +To start on system boot: + + - Edit `/etc/rc.conf` + - Put `docker` into the `SERVICES=(...)` array after `net`. + +## Images + +There is a CRUX image maintained by [James Mills](http://prologic.shortcircuit.net.au/) +as part of the Docker "Official Library" of images. To use this image simply pull it +or use it as part of your `FROM` line in your `Dockerfile(s)`. + + $ docker pull crux + $ docker run -i -t crux + +There are also user contributed [CRUX based image(s)](https://hub.docker.com/_/crux/) on the Docker Hub. + + +## Uninstallation + +To uninstall the Docker package: + + $ sudo prt-get remove docker + +The above command will not remove images, containers, volumes, or user created +configuration files on your host. If you wish to delete all images, containers, +and volumes run the following command: + + $ rm -rf /var/lib/docker + +You must delete the user created configuration files manually. + +## Issues + +If you have any issues please file a bug with the +[CRUX Bug Tracker](http://crux.nu/bugs/). + +## Support + +For support contact the [CRUX Mailing List](http://crux.nu/Main/MailingLists) +or join CRUX's [IRC Channels](http://crux.nu/Main/IrcChannels). on the +[FreeNode](http://freenode.net/) IRC Network. diff --git a/docs/installation/linux/debian.md b/docs/installation/linux/debian.md new file mode 100644 index 00000000..c4adab8f --- /dev/null +++ b/docs/installation/linux/debian.md @@ -0,0 +1,184 @@ + + +# Debian + +Docker is supported on the following versions of Debian: + + - [*Debian testing stretch (64-bit)*](#debian-wheezy-stable-7-x-64-bit) + - [*Debian 8.0 Jessie (64-bit)*](#debian-jessie-80-64-bit) + - [*Debian 7.7 Wheezy (64-bit)*](#debian-wheezy-stable-7-x-64-bit) + + >**Note**: If you previously installed Docker using `APT`, make sure you update + your `APT` sources to the new `APT` repository. + +## Prerequisites + + Docker requires a 64-bit installation regardless of your Debian version. + Additionally, your kernel must be 3.10 at minimum. The latest 3.10 minor + version or a newer maintained version are also acceptable. + + Kernels older than 3.10 lack some of the features required to run Docker + containers. These older versions are known to have bugs which cause data loss + and frequently panic under certain conditions. + + To check your current kernel version, open a terminal and use `uname -r` to + display your kernel version: + + $ uname -r + +### Update your apt repository + +Docker's `APT` repository contains Docker 1.7.1 and higher. To set `APT` to use +from the new repository: + + 1. If you haven't already done so, log into your machine as a user with `sudo` or `root` privileges. + + 2. Open a terminal window. + + 3. Purge any older repositories. + + $ apt-get purge lxc-docker* + $ apt-get purge docker.io* + + 4. Update package information, ensure that APT works with the `https` method, and that CA certificates are installed. + + $ apt-get update + $ apt-get install apt-transport-https ca-certificates + + 5. Add the new `GPG` key. + + $ apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D + + 6. Open the `/etc/apt/sources.list.d/docker.list` file in your favorite editor. + + If the file doesn't exist, create it. + + 7. Remove any existing entries. + + 8. Add an entry for your Debian operating system. + + The possible entries are: + + - On Debian Wheezy + + deb https://apt.dockerproject.org/repo debian-wheezy main + + - On Debian Jessie + + deb https://apt.dockerproject.org/repo debian-jessie main + + - On Debian Stretch/Sid + + deb https://apt.dockerproject.org/repo debian-stretch main + + > **Note**: Docker does not provide packages for all architectures. To install docker on + > a multi-architecture system, add an `[arch=...]` clause to the entry. Refer to the + > [Debian Multiarch wiki](https://wiki.debian.org/Multiarch/HOWTO#Setting_up_apt_sources) + > for details. + + 9. Save and close the file. + + 10. Update the `APT` package index. + + $ apt-get update + + 11. Verify that `APT` is pulling from the right repository. + + $ apt-cache policy docker-engine + + From now on when you run `apt-get upgrade`, `APT` pulls from the new apt repository. + +## Install Docker + +Before installing Docker, make sure you have set your `APT` repository correctly as described in the prerequisites. + +1. Update the `APT` package index. + + $ sudo apt-get update + +2. Install Docker. + + $ sudo apt-get install docker-engine + +5. Start the `docker` daemon. + + $ sudo service docker start + +6. Verify `docker` is installed correctly. + + $ sudo docker run hello-world + + This command downloads a test image and runs it in a container. When the + container runs, it prints an informational message. Then, it exits. + + +## Giving non-root access + +The `docker` daemon always runs as the `root` user and the `docker` +daemon binds to a Unix socket instead of a TCP port. By default that +Unix socket is owned by the user `root`, and so, by default, you can +access it with `sudo`. + +If you (or your Docker installer) create a Unix group called `docker` +and add users to it, then the `docker` daemon will make the ownership of +the Unix socket read/writable by the `docker` group when the daemon +starts. The `docker` daemon must always run as the root user, but if you +run the `docker` client as a user in the `docker` group then you don't +need to add `sudo` to all the client commands. From Docker 0.9.0 you can +use the `-G` flag to specify an alternative group. + +> **Warning**: +> The `docker` group (or the group specified with the `-G` flag) is +> `root`-equivalent; see [*Docker Daemon Attack Surface*](../../security/security.md#docker-daemon-attack-surface) details. + +**Example:** + + # Add the docker group if it doesn't already exist. + $ sudo groupadd docker + + # Add the connected user "${USER}" to the docker group. + # Change the user name to match your preferred user. + # You may have to logout and log back in again for + # this to take effect. + $ sudo gpasswd -a ${USER} docker + + # Restart the Docker daemon. + $ sudo service docker restart + +## Upgrade Docker + +To install the latest version of Docker with `apt-get`: + + $ apt-get upgrade docker-engine + +## Uninstall + +To uninstall the Docker package: + + $ sudo apt-get purge docker-engine + +To uninstall the Docker package and dependencies that are no longer needed: + + $ sudo apt-get autoremove --purge docker-engine + +The above commands will not remove images, containers, volumes, or user created +configuration files on your host. If you wish to delete all images, containers, +and volumes run the following command: + + $ rm -rf /var/lib/docker + +You must delete the user created configuration files manually. + +## What next? + +Continue with the [User Guide](../../userguide/index.md). diff --git a/docs/installation/linux/fedora.md b/docs/installation/linux/fedora.md new file mode 100644 index 00000000..782adc67 --- /dev/null +++ b/docs/installation/linux/fedora.md @@ -0,0 +1,203 @@ + + +# Fedora + +Docker is supported on Fedora version 22 and 23. This page instructs you to install +using Docker-managed release packages and installation mechanisms. Using these +packages ensures you get the latest release of Docker. If you wish to install +using Fedora-managed packages, consult your Fedora release documentation for +information on Fedora's Docker support. + +## Prerequisites + +Docker requires a 64-bit installation regardless of your Fedora version. Also, your kernel must be 3.10 at minimum. To check your current kernel +version, open a terminal and use `uname -r` to display your kernel version: + + $ uname -r + 3.19.5-100.fc21.x86_64 + +If your kernel is at a older version, you must update it. + +Finally, is it recommended that you fully update your system. Please keep in +mind that your system should be fully patched to fix any potential kernel bugs. Any +reported kernel bugs may have already been fixed on the latest kernel packages + + +## Install + +There are two ways to install Docker Engine. You can install with the `dnf` package manager. Or you can use `curl` with the `get.docker.com` site. This second method runs an installation script which also installs via the `dnf` package manager. + +### Install with DNF + +1. Log into your machine as a user with `sudo` or `root` privileges. + +2. Make sure your existing dnf packages are up-to-date. + + $ sudo dnf update + +3. Add the yum repo yourself. + + $ sudo tee /etc/yum.repos.d/docker.repo <<-'EOF' + [dockerrepo] + name=Docker Repository + baseurl=https://yum.dockerproject.org/repo/main/fedora/$releasever/ + enabled=1 + gpgcheck=1 + gpgkey=https://yum.dockerproject.org/gpg + EOF + +4. Install the Docker package. + + $ sudo dnf install docker-engine + +5. Start the Docker daemon. + + $ sudo systemctl start docker + +6. Verify `docker` is installed correctly by running a test image in a container. + + + $ sudo docker run hello-world + Unable to find image 'hello-world:latest' locally + latest: Pulling from hello-world + a8219747be10: Pull complete + 91c95931e552: Already exists + hello-world:latest: The image you are pulling has been verified. Important: image verification is a tech preview feature and should not be relied on to provide security. + Digest: sha256:aa03e5d0d5553b4c3473e89c8619cf79df368babd1.7.1cf5daeb82aab55838d + Status: Downloaded newer image for hello-world:latest + Hello from Docker. + This message shows that your installation appears to be working correctly. + + To generate this message, Docker took the following steps: + 1. The Docker client contacted the Docker daemon. + 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. + (Assuming it was not already locally available.) + 3. The Docker daemon created a new container from that image which runs the + executable that produces the output you are currently reading. + 4. The Docker daemon streamed that output to the Docker client, which sent it + to your terminal. + + To try something more ambitious, you can run an Ubuntu container with: + $ docker run -it ubuntu bash + + For more examples and ideas, visit: + http://docs.docker.com/userguide/ + + +### Install with the script + + +1. Log into your machine as a user with `sudo` or `root` privileges. + +2. Make sure your existing dnf packages are up-to-date. + + $ sudo dnf update + +3. Run the Docker installation script. + + $ curl -fsSL https://get.docker.com/ | sh + + This script adds the `docker.repo` repository and installs Docker. + +4. Start the Docker daemon. + + $ sudo systemctl start docker + +5. Verify `docker` is installed correctly by running a test image in a container. + + $ sudo docker run hello-world + +## Create a docker group + +The `docker` daemon binds to a Unix socket instead of a TCP port. By default +that Unix socket is owned by the user `root` and other users can access it with +`sudo`. For this reason, `docker` daemon always runs as the `root` user. + +To avoid having to use `sudo` when you use the `docker` command, create a Unix +group called `docker` and add users to it. When the `docker` daemon starts, it +makes the ownership of the Unix socket read/writable by the `docker` group. + +>**Warning**: The `docker` group is equivalent to the `root` user; For details +>on how this impacts security in your system, see [*Docker Daemon Attack +>Surface*](../../security/security.md#docker-daemon-attack-surface) for details. + +To create the `docker` group and add your user: + +1. Log into your system as a user with `sudo` privileges. + +2. Create the `docker` group. + + `sudo groupadd docker` + +3. Add your user to `docker` group. + + `sudo usermod -aG docker your_username` + +4. Log out and log back in. + + This ensures your user is running with the correct permissions. + +5. Verify your work by running `docker` without `sudo`. + + $ docker run hello-world + +## Start the docker daemon at boot + +To ensure Docker starts when you boot your system, do the following: + + $ sudo systemctl enable docker + +If you need to add an HTTP Proxy, set a different directory or partition for the +Docker runtime files, or make other customizations, read our Systemd article to +learn how to [customize your Systemd Docker daemon options](../../admin/systemd.md). + +## Running Docker with a manually-defined network + +If you manually configure your network using `systemd-network` with `systemd` version 219 or higher, containers you start with Docker may be unable to access your network. +Beginning with version 220, the forwarding setting for a given network (`net.ipv4.conf..forwarding`) defaults to *off*. This setting prevents IP forwarding. It also conflicts with Docker which enables the `net.ipv4.conf.all.forwarding` setting within a container. + +To work around this, edit the `.network` file in +`/usr/lib/systemd/network/` on your Docker host (ex: `/usr/lib/systemd/network/80-container-host0.network`) add the following block: + +``` +[Network] +... +IPForward=kernel +# OR +IPForward=true +... +``` + +This configuration allows IP forwarding from the container as expected. + +## Uninstall + +You can uninstall the Docker software with `dnf`. + +1. List the package you have installed. + + $ dnf list installed | grep docker dnf list installed | grep docker + docker-engine.x86_64 1.7.1-0.1.fc21 @/docker-engine-1.7.1-0.1.fc21.el7.x86_64 + +2. Remove the package. + + $ sudo dnf -y remove docker-engine.x86_64 + + This command does not remove images, containers, volumes, or user-created + configuration files on your host. + +3. To delete all images, containers, and volumes, run the following command: + + $ rm -rf /var/lib/docker + +4. Locate and delete any user-created configuration files. diff --git a/docs/installation/linux/frugalware.md b/docs/installation/linux/frugalware.md new file mode 100644 index 00000000..40368302 --- /dev/null +++ b/docs/installation/linux/frugalware.md @@ -0,0 +1,75 @@ + + +# FrugalWare + +Installing on FrugalWare is handled via the official packages: + + - [lxc-docker i686](http://www.frugalware.org/packages/200141) + - [lxc-docker x86_64](http://www.frugalware.org/packages/200130) + +The lxc-docker package will install the latest tagged version of Docker. + +## Dependencies + +Docker depends on several packages which are specified as dependencies +in the packages. The core dependencies are: + + - systemd + - lvm2 + - sqlite3 + - libguestfs + - lxc + - iproute2 + - bridge-utils + +## Installation + +A simple + + $ sudo pacman -S lxc-docker + +is all that is needed. + +## Starting Docker + +There is a systemd service unit created for Docker. To start Docker as +service: + + $ sudo systemctl start lxc-docker + +To start on system boot: + + $ sudo systemctl enable lxc-docker + +## Custom daemon options + +If you need to add an HTTP Proxy, set a different directory or partition for the +Docker runtime files, or make other customizations, read our systemd article to +learn how to [customize your systemd Docker daemon options](../../admin/systemd.md). + +## Uninstallation + +To uninstall the Docker package: + + $ sudo pacman -R lxc-docker + +To uninstall the Docker package and dependencies that are no longer needed: + + $ sudo pacman -Rns lxc-docker + +The above commands will not remove images, containers, volumes, or user created +configuration files on your host. If you wish to delete all images, containers, +and volumes run the following command: + + $ rm -rf /var/lib/docker + +You must delete the user created configuration files manually. diff --git a/docs/installation/linux/gentoolinux.md b/docs/installation/linux/gentoolinux.md new file mode 100644 index 00000000..3b33ee45 --- /dev/null +++ b/docs/installation/linux/gentoolinux.md @@ -0,0 +1,122 @@ + + +# Gentoo + +Installing Docker on Gentoo Linux can be accomplished using one of two ways: the **official** way and the `docker-overlay` way. + +Official project page of [Gentoo Docker](https://wiki.gentoo.org/wiki/Project:Docker) team. + +## Official way +The first and recommended way if you are looking for a stable +experience is to use the official `app-emulation/docker` package directly +from the tree. + +If any issues arise from this ebuild including, missing kernel +configuration flags or dependencies, open a bug +on the Gentoo [Bugzilla](https://bugs.gentoo.org) assigned to `docker AT gentoo DOT org` +or join and ask in the official +[IRC](http://webchat.freenode.net?channels=%23gentoo-containers&uio=d4) channel on the Freenode network. + +## docker-overlay way + +If you're looking for a `-bin` ebuild, a live ebuild, or a bleeding edge +ebuild, use the provided overlay, [docker-overlay](https://github.com/tianon/docker-overlay) +which can be added using `app-portage/layman`. The most accurate and +up-to-date documentation for properly installing and using the overlay +can be found in the [overlay](https://github.com/tianon/docker-overlay/blob/master/README.md#using-this-overlay). + +If any issues arise from this ebuild or the resulting binary, including +and especially missing kernel configuration flags or dependencies, +open an [issue](https://github.com/tianon/docker-overlay/issues) on +the `docker-overlay` repository or ping `tianon` directly in the `#docker` +IRC channel on the Freenode network. + +## Installation + +### Available USE flags + +| USE Flag | Default | Description | +| ------------- |:-------:|:------------| +| aufs | |Enables dependencies for the "aufs" graph driver, including necessary kernel flags.| +| btrfs | |Enables dependencies for the "btrfs" graph driver, including necessary kernel flags.| +| contrib | Yes |Install additional contributed scripts and components.| +| device-mapper | Yes |Enables dependencies for the "devicemapper" graph driver, including necessary kernel flags.| +| doc | |Add extra documentation (API, Javadoc, etc). It is recommended to enable per package instead of globally.| +| vim-syntax | |Pulls in related vim syntax scripts.| +| zsh-completion| |Enable zsh completion support.| + +USE flags are described in detail on [tianon's +blog](https://tianon.github.io/post/2014/05/17/docker-on-gentoo.html). + +The package should properly pull in all the necessary dependencies and +prompt for all necessary kernel options. + + $ sudo emerge -av app-emulation/docker + +>Note: Sometimes there is a disparity between the latest versions +>in the official **Gentoo tree** and the **docker-overlay**. +>Please be patient, and the latest version should propagate shortly. + +## Starting Docker + +Ensure that you are running a kernel that includes all the necessary +modules and configuration (and optionally for device-mapper +and AUFS or Btrfs, depending on the storage driver you've decided to use). + +To use Docker, the `docker` daemon must be running as **root**. +To use Docker as a **non-root** user, add yourself to the **docker** +group by running the following command: + + $ sudo groupadd docker + $ sudo usermod -a -G docker user + +### OpenRC + +To start the `docker` daemon: + + $ sudo /etc/init.d/docker start + +To start on system boot: + + $ sudo rc-update add docker default + +### systemd + +To start the `docker` daemon: + + $ sudo systemctl start docker + +To start on system boot: + + $ sudo systemctl enable docker + +If you need to add an HTTP Proxy, set a different directory or partition for the +Docker runtime files, or make other customizations, read our systemd article to +learn how to [customize your systemd Docker daemon options](../../admin/systemd.md). + +## Uninstallation + +To uninstall the Docker package: + + $ sudo emerge -cav app-emulation/docker + +To uninstall the Docker package and dependencies that are no longer needed: + + $ sudo emerge -C app-emulation/docker + +The above commands will not remove images, containers, volumes, or user created +configuration files on your host. If you wish to delete all images, containers, +and volumes run the following command: + + $ rm -rf /var/lib/docker + +You must delete the user created configuration files manually. diff --git a/docs/installation/linux/index.md b/docs/installation/linux/index.md new file mode 100644 index 00000000..2fb81ae2 --- /dev/null +++ b/docs/installation/linux/index.md @@ -0,0 +1,29 @@ + + +# Install Docker Engine on Linux + +Docker Engine is supported on several Linux distributions. Installation instructions are available for the following: + +* [Arch Linux](archlinux.md) +* [CentOS](centos.md) +* [CRUX Linux](cruxlinux.md) +* [Debian](debian.md) +* [Fedora](fedora.md) +* [FrugalWare](frugalware.md) +* [Gentoo](gentoolinux.md) +* [Oracle Linux](oracle.md) +* [Red Hat Enterprise Linux](rhel.md) +* [openSUSE and SUSE Linux Enterprise](SUSE.md) +* [Ubuntu](ubuntulinux.md) + +If your linux distribution is not listed above, don't give up yet. To try out Docker on a distribution that is not listed above, go here: [Installation from binaries](../binaries.md). diff --git a/docs/installation/linux/oracle.md b/docs/installation/linux/oracle.md new file mode 100644 index 00000000..9513f8b8 --- /dev/null +++ b/docs/installation/linux/oracle.md @@ -0,0 +1,214 @@ + + +# Oracle Linux + +Docker is supported Oracle Linux 6 and 7. You do not require an Oracle Linux +Support subscription to install Docker on Oracle Linux. + +## Prerequisites + +Due to current Docker limitations, Docker is only able to run only on the x86_64 +architecture. Docker requires the use of the Unbreakable Enterprise Kernel +Release 4 (4.1.12) or higher on Oracle Linux. This kernel supports the Docker +btrfs storage engine on both Oracle Linux 6 and 7. + +## Install + + +> **Note**: The procedure below installs binaries built by Docker. These binaries +> are not covered by Oracle Linux support. To ensure Oracle Linux support, please +> follow the installation instructions provided in the +> [Oracle Linux documentation](https://docs.oracle.com/en/operating-systems/?tab=2). +> +> The installation instructions for Oracle Linux 6 can be found in [Chapter 10 of +> the Administrator's +> Solutions Guide](https://docs.oracle.com/cd/E37670_01/E37355/html/ol_docker.html) +> +> The installation instructions for Oracle Linux 7 can be found in [Chapter 29 of +> the Administrator's +> Guide](https://docs.oracle.com/cd/E52668_01/E54669/html/ol7-docker.html) + + +1. Log into your machine as a user with `sudo` or `root` privileges. + +2. Make sure your existing yum packages are up-to-date. + + $ sudo yum update + +3. Add the yum repo yourself. + + For version 6: + + $ sudo tee /etc/yum.repos.d/docker.repo <<-EOF + [dockerrepo] + name=Docker Repository + baseurl=https://yum.dockerproject.org/repo/main/oraclelinux/6 + enabled=1 + gpgcheck=1 + gpgkey=https://yum.dockerproject.org/gpg + EOF + + For version 7: + + $ cat >/etc/yum.repos.d/docker.repo <<-EOF + [dockerrepo] + name=Docker Repository + baseurl=https://yum.dockerproject.org/repo/main/oraclelinux/7 + enabled=1 + gpgcheck=1 + gpgkey=https://yum.dockerproject.org/gpg + EOF + +4. Install the Docker package. + + $ sudo yum install docker-engine + +5. Start the Docker daemon. + + On Oracle Linux 6: + + $ sudo service docker start + + On Oracle Linux 7: + + $ sudo systemctl start docker.service + +6. Verify `docker` is installed correctly by running a test image in a container. + + $ sudo docker run hello-world + +## Optional configurations + +This section contains optional procedures for configuring your Oracle Linux to work +better with Docker. + +* [Create a docker group](#create-a-docker-group) +* [Configure Docker to start on boot](#configure-docker-to-start-on-boot) +* [Use the btrfs storage engine](#use-the-btrfs-storage-engine) + +### Create a Docker group + +The `docker` daemon binds to a Unix socket instead of a TCP port. By default +that Unix socket is owned by the user `root` and other users can access it with +`sudo`. For this reason, `docker` daemon always runs as the `root` user. + +To avoid having to use `sudo` when you use the `docker` command, create a Unix +group called `docker` and add users to it. When the `docker` daemon starts, it +makes the ownership of the Unix socket read/writable by the `docker` group. + +>**Warning**: The `docker` group is equivalent to the `root` user; For details +>on how this impacts security in your system, see [*Docker Daemon Attack +>Surface*](../../security/security.md#docker-daemon-attack-surface) for details. + +To create the `docker` group and add your user: + +1. Log into Oracle Linux as a user with `sudo` privileges. + +2. Create the `docker` group. + + sudo groupadd docker + +3. Add your user to `docker` group. + + sudo usermod -aG docker username + +4. Log out and log back in. + + This ensures your user is running with the correct permissions. + +5. Verify your work by running `docker` without `sudo`. + + $ docker run hello-world + + If this fails with a message similar to this: + + Cannot connect to the Docker daemon. Is 'docker daemon' running on this host? + + Check that the `DOCKER_HOST` environment variable is not set for your shell. + If it is, unset it. + +### Configure Docker to start on boot + +You can configure the Docker daemon to start automatically at boot. + +On Oracle Linux 6: + +``` +$ sudo chkconfig docker on +``` + +On Oracle Linux 7: + +``` +$ sudo systemctl enable docker.service +``` + +If you need to add an HTTP Proxy, set a different directory or partition for the +Docker runtime files, or make other customizations, read our systemd article to +learn how to [customize your systemd Docker daemon options](../../admin/systemd.md). + +### Use the btrfs storage engine + +Docker on Oracle Linux 6 and 7 supports the use of the btrfs storage engine. +Before enabling btrfs support, ensure that `/var/lib/docker` is stored on a +btrfs-based filesystem. Review [Chapter +5](http://docs.oracle.com/cd/E37670_01/E37355/html/ol_btrfs.html) of the [Oracle +Linux Administrator's Solution +Guide](http://docs.oracle.com/cd/E37670_01/E37355/html/index.html) for details +on how to create and mount btrfs filesystems. + +To enable btrfs support on Oracle Linux: + +1. Ensure that `/var/lib/docker` is on a btrfs filesystem. + +2. Edit `/etc/sysconfig/docker` and add `-s btrfs` to the `OTHER_ARGS` field. + +3. Restart the Docker daemon: + +## Uninstallation + +To uninstall the Docker package: + + $ sudo yum -y remove docker-engine + +The above command will not remove images, containers, volumes, or user created +configuration files on your host. If you wish to delete all images, containers, +and volumes run the following command: + + $ rm -rf /var/lib/docker + +You must delete the user created configuration files manually. + +## Known issues + +### Docker unmounts btrfs filesystem on shutdown +If you're running Docker using the btrfs storage engine and you stop the Docker +service, it will unmount the btrfs filesystem during the shutdown process. You +should ensure the filesystem is mounted properly prior to restarting the Docker +service. + +On Oracle Linux 7, you can use a `systemd.mount` definition and modify the +Docker `systemd.service` to depend on the btrfs mount defined in systemd. + +### SElinux support on Oracle Linux 7 +SElinux must be set to `Permissive` or `Disabled` in `/etc/sysconfig/selinux` to +use the btrfs storage engine on Oracle Linux 7. + +## Further issues? + +If you have a current Basic or Premier Support Subscription for Oracle Linux, +you can report any issues you have with the installation of Docker via a Service +Request at [My Oracle Support](http://support.oracle.com). + +If you do not have an Oracle Linux Support Subscription, you can use the [Oracle +Linux +Forum](https://community.oracle.com/community/server_%26_storage_systems/linux/oracle_linux) for community-based support. diff --git a/docs/installation/linux/rhel.md b/docs/installation/linux/rhel.md new file mode 100644 index 00000000..abf7b30b --- /dev/null +++ b/docs/installation/linux/rhel.md @@ -0,0 +1,184 @@ + + +# Red Hat Enterprise Linux + +Docker is supported on Red Hat Enterprise Linux 7. This page instructs you to +install using Docker-managed release packages and installation mechanisms. Using +these packages ensures you get the latest release of Docker. If you wish to +install using Red Hat-managed packages, consult your Red Hat release +documentation for information on Red Hat's Docker support. + +## Prerequisites + +Docker requires a 64-bit installation regardless of your Red Hat version. Docker +requires that your kernel must be 3.10 at minimum, which Red Hat 7 runs. + +To check your current kernel version, open a terminal and use `uname -r` to +display your kernel version: + + $ uname -r + 3.10.0-229.el7.x86_64 + +Finally, is it recommended that you fully update your system. Please keep in +mind that your system should be fully patched to fix any potential kernel bugs. +Any reported kernel bugs may have already been fixed on the latest kernel +packages. + +## Install Docker Engine + +There are two ways to install Docker Engine. You can install with the `yum` package manager directly yourself. Or you can use `curl` with the `get.docker.com` site. This second method runs an installation script which installs via the `yum` package manager. + +### Install with yum + +1. Log into your machine as a user with `sudo` or `root` privileges. + +2. Make sure your existing yum packages are up-to-date. + + $ sudo yum update + +3. Add the yum repo yourself. + + $ sudo tee /etc/yum.repos.d/docker.repo <<-EOF + [dockerrepo] + name=Docker Repository + baseurl=https://yum.dockerproject.org/repo/main/centos/7 + enabled=1 + gpgcheck=1 + gpgkey=https://yum.dockerproject.org/gpg + EOF + +4. Install the Docker package. + + $ sudo yum install docker-engine + +5. Start the Docker daemon. + + $ sudo service docker start + +6. Verify `docker` is installed correctly by running a test image in a container. + + $ sudo docker run hello-world + Unable to find image 'hello-world:latest' locally + latest: Pulling from hello-world + a8219747be10: Pull complete + 91c95931e552: Already exists + hello-world:latest: The image you are pulling has been verified. Important: image verification is a tech preview feature and should not be relied on to provide security. + Digest: sha256:aa03e5d0d5553b4c3473e89c8619cf79df368babd1.7.1cf5daeb82aab55838d + Status: Downloaded newer image for hello-world:latest + Hello from Docker. + This message shows that your installation appears to be working correctly. + + To generate this message, Docker took the following steps: + 1. The Docker client contacted the Docker daemon. + 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. + (Assuming it was not already locally available.) + 3. The Docker daemon created a new container from that image which runs the + executable that produces the output you are currently reading. + 4. The Docker daemon streamed that output to the Docker client, which sent it + to your terminal. + + To try something more ambitious, you can run an Ubuntu container with: + $ docker run -it ubuntu bash + + For more examples and ideas, visit: + http://docs.docker.com/userguide/ + +### Install with the script + +You use the same installation procedure for all versions of CentOS. + +1. Log into your machine as a user with `sudo` or `root` privileges. + +2. Make sure your existing yum packages are up-to-date. + + $ sudo yum update + +3. Run the Docker installation script. + + $ curl -fsSL https://get.docker.com/ | sh + +4. Start the Docker daemon. + + $ sudo service docker start + +5. Verify `docker` is installed correctly by running a test image in a container. + + $ sudo docker run hello-world + +## Create a docker group + +The `docker` daemon binds to a Unix socket instead of a TCP port. By default +that Unix socket is owned by the user `root` and other users can access it with +`sudo`. For this reason, `docker` daemon always runs as the `root` user. + +To avoid having to use `sudo` when you use the `docker` command, create a Unix +group called `docker` and add users to it. When the `docker` daemon starts, it +makes the ownership of the Unix socket read/writable by the `docker` group. + +>**Warning**: The `docker` group is equivalent to the `root` user; For details +>on how this impacts security in your system, see [*Docker Daemon Attack +>Surface*](../../security/security.md#docker-daemon-attack-surface) for details. + +To create the `docker` group and add your user: + +1. Log into your machine as a user with `sudo` or `root` privileges. + +2. Create the `docker` group. + + `sudo groupadd docker` + +3. Add your user to `docker` group. + + `sudo usermod -aG docker your_username` + +4. Log out and log back in. + + This ensures your user is running with the correct permissions. + +5. Verify your work by running `docker` without `sudo`. + + $ docker run hello-world + +## Start the docker daemon at boot + +To ensure Docker starts when you boot your system, do the following: + + $ sudo chkconfig docker on + +If you need to add an HTTP Proxy, set a different directory or partition for the +Docker runtime files, or make other customizations, read our Systemd article to +learn how to [customize your Systemd Docker daemon options](../../admin/systemd.md). + + +## Uninstall + +You can uninstall the Docker software with `yum`. + +1. List the package you have installed. + + $ yum list installed | grep docker + yum list installed | grep docker + docker-engine.x86_64 1.7.1-0.1.el7@/docker-engine-1.7.1-0.1.el7.x86_64 + +2. Remove the package. + + $ sudo yum -y remove docker-engine.x86_64 + + This command does not remove images, containers, volumes, or user created + configuration files on your host. + +3. To delete all images, containers, and volumes run the following command: + + $ rm -rf /var/lib/docker + +4. Locate and delete any user-created configuration files. diff --git a/docs/installation/linux/ubuntulinux.md b/docs/installation/linux/ubuntulinux.md new file mode 100644 index 00000000..1807874c --- /dev/null +++ b/docs/installation/linux/ubuntulinux.md @@ -0,0 +1,452 @@ + + +# Ubuntu + +Docker is supported on these Ubuntu operating systems: + +- Ubuntu Xenial 16.04 (LTS) +- Ubuntu Wily 15.10 +- Ubuntu Trusty 14.04 (LTS) +- Ubuntu Precise 12.04 (LTS) + +This page instructs you to install using Docker-managed release packages and +installation mechanisms. Using these packages ensures you get the latest release +of Docker. If you wish to install using Ubuntu-managed packages, consult your +Ubuntu documentation. + +>**Note**: Ubuntu Utopic 14.10 and 15.04 exist in Docker's `APT` repository but +> are no longer officially supported. + +## Prerequisites + +Docker requires a 64-bit installation regardless of your Ubuntu version. +Additionally, your kernel must be 3.10 at minimum. The latest 3.10 minor version +or a newer maintained version are also acceptable. + +Kernels older than 3.10 lack some of the features required to run Docker +containers. These older versions are known to have bugs which cause data loss +and frequently panic under certain conditions. + +To check your current kernel version, open a terminal and use `uname -r` to +display your kernel version: + + $ uname -r + 3.11.0-15-generic + +>**Note**: If you previously installed Docker using `APT`, make sure you update +your `APT` sources to the new Docker repository. + +### Update your apt sources + +Docker's `APT` repository contains Docker 1.7.1 and higher. To set `APT` to use +packages from the new repository: + +1. Log into your machine as a user with `sudo` or `root` privileges. + +2. Open a terminal window. + +3. Update package information, ensure that APT works with the `https` method, and that CA certificates are installed. + + $ sudo apt-get update + $ sudo apt-get install apt-transport-https ca-certificates + +4. Add the new `GPG` key. + + $ sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D + +5. Open the `/etc/apt/sources.list.d/docker.list` file in your favorite editor. + + If the file doesn't exist, create it. + +6. Remove any existing entries. + +7. Add an entry for your Ubuntu operating system. + + The possible entries are: + + - On Ubuntu Precise 12.04 (LTS) + + deb https://apt.dockerproject.org/repo ubuntu-precise main + + - On Ubuntu Trusty 14.04 (LTS) + + deb https://apt.dockerproject.org/repo ubuntu-trusty main + + - Ubuntu Wily 15.10 + + deb https://apt.dockerproject.org/repo ubuntu-wily main + + - Ubuntu Xenial 16.04 (LTS) + + deb https://apt.dockerproject.org/repo ubuntu-xenial main + + > **Note**: Docker does not provide packages for all architectures. You can find + > nightly built binaries in https://master.dockerproject.org. To install docker on + > a multi-architecture system, add an `[arch=...]` clause to the entry. Refer to the + > [Debian Multiarch wiki](https://wiki.debian.org/Multiarch/HOWTO#Setting_up_apt_sources) + > for details. + +8. Save and close the `/etc/apt/sources.list.d/docker.list` file. + +9. Update the `APT` package index. + + $ sudo apt-get update + +10. Purge the old repo if it exists. + + $ sudo apt-get purge lxc-docker + +11. Verify that `APT` is pulling from the right repository. + + $ apt-cache policy docker-engine + + From now on when you run `apt-get upgrade`, `APT` pulls from the new repository. + +### Prerequisites by Ubuntu Version + +- Ubuntu Xenial 16.04 (LTS) +- Ubuntu Wily 15.10 +- Ubuntu Trusty 14.04 (LTS) + +For Ubuntu Trusty, Wily, and Xenial, it's recommended to install the +`linux-image-extra` kernel package. The `linux-image-extra` package +allows you use the `aufs` storage driver. + +To install the `linux-image-extra` package for your kernel version: + +1. Open a terminal on your Ubuntu host. + +2. Update your package manager. + + $ sudo apt-get update + +3. Install the recommended package. + + $ sudo apt-get install linux-image-extra-$(uname -r) + +4. Go ahead and install Docker. + +If you are installing on Ubuntu 14.04 or 12.04, `apparmor` is required. You can install it using: `apt-get install apparmor` + +#### Ubuntu Precise 12.04 (LTS) + +For Ubuntu Precise, Docker requires the 3.13 kernel version. If your kernel +version is older than 3.13, you must upgrade it. Refer to this table to see +which packages are required for your environment: + + + +
linux-image-generic-lts-trusty Generic +Linux kernel image. This kernel has AUFS built in. This is required to run +Docker.
linux-headers-generic-lts-trustyAllows packages such as ZFS and VirtualBox guest additions +which depend on them. If you didn't install the headers for your existing +kernel, then you can skip these headers for the"trusty" kernel. If you're +unsure, you should include this package for safety.
xserver-xorg-lts-trusty Optional in non-graphical environments without Unity/Xorg. +Required when running Docker on machine with a graphical environment. +
+
To learn more about the reasons for these packages, read the installation +instructions for backported kernels, specifically the LTS +Enablement Stack — refer to note 5 under each version. +
libgl1-mesa-glx-lts-trusty
  + +To upgrade your kernel and install the additional packages, do the following: + +1. Open a terminal on your Ubuntu host. + +2. Update your package manager. + + $ sudo apt-get update + +3. Install both the required and optional packages. + + $ sudo apt-get install linux-image-generic-lts-trusty + + Depending on your environment, you may install more as described in the preceding table. + +4. Reboot your host. + + $ sudo reboot + +5. After your system reboots, go ahead and install Docker. + +## Install + +Make sure you have installed the prerequisites for your Ubuntu version. + +Then, +install Docker using the following: + +1. Log into your Ubuntu installation as a user with `sudo` privileges. + +2. Update your `APT` package index. + + $ sudo apt-get update + +3. Install Docker. + + $ sudo apt-get install docker-engine + +4. Start the `docker` daemon. + + $ sudo service docker start + +5. Verify `docker` is installed correctly. + + $ sudo docker run hello-world + + This command downloads a test image and runs it in a container. When the + container runs, it prints an informational message. Then, it exits. + +## Optional configurations + +This section contains optional procedures for configuring your Ubuntu to work +better with Docker. + +* [Create a docker group](#create-a-docker-group) +* [Adjust memory and swap accounting](#adjust-memory-and-swap-accounting) +* [Enable UFW forwarding](#enable-ufw-forwarding) +* [Configure a DNS server for use by Docker](#configure-a-dns-server-for-use-by-docker) +* [Configure Docker to start on boot](#configure-docker-to-start-on-boot) + +### Create a Docker group + +The `docker` daemon binds to a Unix socket instead of a TCP port. By default +that Unix socket is owned by the user `root` and other users can access it with +`sudo`. For this reason, `docker` daemon always runs as the `root` user. + +To avoid having to use `sudo` when you use the `docker` command, create a Unix +group called `docker` and add users to it. When the `docker` daemon starts, it +makes the ownership of the Unix socket read/writable by the `docker` group. + +>**Warning**: The `docker` group is equivalent to the `root` user; For details +>on how this impacts security in your system, see [*Docker Daemon Attack +>Surface*](../../security/security.md#docker-daemon-attack-surface) for details. + +To create the `docker` group and add your user: + +1. Log into Ubuntu as a user with `sudo` privileges. + + This procedure assumes you log in as the `ubuntu` user. + +2. Create the `docker` group. + + $ sudo groupadd docker + +3. Add your user to `docker` group. + + $ sudo usermod -aG docker ubuntu + +4. Log out and log back in. + + This ensures your user is running with the correct permissions. + +5. Verify your work by running `docker` without `sudo`. + + $ docker run hello-world + + If this fails with a message similar to this: + + Cannot connect to the Docker daemon. Is 'docker daemon' running on this host? + + Check that the `DOCKER_HOST` environment variable is not set for your shell. + If it is, unset it. + +### Adjust memory and swap accounting + +When users run Docker, they may see these messages when working with an image: + + WARNING: Your kernel does not support cgroup swap limit. WARNING: Your + kernel does not support swap limit capabilities. Limitation discarded. + +To prevent these messages, enable memory and swap accounting on your +system. Enabling memory and swap accounting does induce both a memory +overhead and a performance degradation even when Docker is not in +use. The memory overhead is about 1% of the total available +memory. The performance degradation is roughly 10%. + +To enable memory and swap on system using GNU GRUB (GNU GRand Unified +Bootloader), do the following: + +1. Log into Ubuntu as a user with `sudo` privileges. + +2. Edit the `/etc/default/grub` file. + +3. Set the `GRUB_CMDLINE_LINUX` value as follows: + + GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1" + +4. Save and close the file. + +5. Update GRUB. + + $ sudo update-grub + +6. Reboot your system. + + +### Enable UFW forwarding + +If you use [UFW (Uncomplicated Firewall)](https://help.ubuntu.com/community/UFW) +on the same host as you run Docker, you'll need to do additional configuration. +Docker uses a bridge to manage container networking. By default, UFW drops all +forwarding traffic. As a result, for Docker to run when UFW is +enabled, you must set UFW's forwarding policy appropriately. + +Also, UFW's default set of rules denies all incoming traffic. If you want to +reach your containers from another host allow incoming connections on the Docker +port. The Docker port defaults to `2376` if TLS is enabled or `2375` when it is +not. If TLS is not enabled, communication is unencrypted. By default, Docker +runs without TLS enabled. + +To configure UFW and allow incoming connections on the Docker port: + +1. Log into Ubuntu as a user with `sudo` privileges. + +2. Verify that UFW is installed and enabled. + + $ sudo ufw status + +3. Open the `/etc/default/ufw` file for editing. + + $ sudo nano /etc/default/ufw + +4. Set the `DEFAULT_FORWARD_POLICY` policy to: + + DEFAULT_FORWARD_POLICY="ACCEPT" + +5. Save and close the file. + +6. Reload UFW to use the new setting. + + $ sudo ufw reload + +7. Allow incoming connections on the Docker port. + + $ sudo ufw allow 2375/tcp + +### Configure a DNS server for use by Docker + +Systems that run Ubuntu or an Ubuntu derivative on the desktop typically use +`127.0.0.1` as the default `nameserver` in `/etc/resolv.conf` file. The +NetworkManager also sets up `dnsmasq` to use the real DNS servers of the +connection and sets up `nameserver 127.0.0.1` in /`etc/resolv.conf`. + +When starting containers on desktop machines with these configurations, Docker +users see this warning: + + WARNING: Local (127.0.0.1) DNS resolver found in resolv.conf and containers + can't use it. Using default external servers : [8.8.8.8 8.8.4.4] + +The warning occurs because Docker containers can't use the local DNS nameserver. +Instead, Docker defaults to using an external nameserver. + +To avoid this warning, you can specify a DNS server for use by Docker +containers. Or, you can disable `dnsmasq` in NetworkManager. Though, disabling +`dnsmasq` might make DNS resolution slower on some networks. + +The instructions below describe how to configure the Docker daemon +running on Ubuntu 14.10 or below. Ubuntu 15.04 and above use `systemd` +as the boot and service manager. Refer to [control and configure Docker +with systemd](../../admin/systemd.md#custom-docker-daemon-options) to +configure a daemon controlled by `systemd`. + +To specify a DNS server for use by Docker: + +1. Log into Ubuntu as a user with `sudo` privileges. + +2. Open the `/etc/default/docker` file for editing. + + $ sudo nano /etc/default/docker + +3. Add a setting for Docker. + + DOCKER_OPTS="--dns 8.8.8.8" + + Replace `8.8.8.8` with a local DNS server such as `192.168.1.1`. You can also + specify multiple DNS servers. Separated them with spaces, for example: + + --dns 8.8.8.8 --dns 192.168.1.1 + + >**Warning**: If you're doing this on a laptop which connects to various + >networks, make sure to choose a public DNS server. + +4. Save and close the file. + +5. Restart the Docker daemon. + + $ sudo restart docker + + +  +  + +**Or, as an alternative to the previous procedure,** disable `dnsmasq` in +NetworkManager (this might slow your network). + +1. Open the `/etc/NetworkManager/NetworkManager.conf` file for editing. + + $ sudo nano /etc/NetworkManager/NetworkManager.conf + +2. Comment out the `dns=dnsmasq` line: + + dns=dnsmasq + +3. Save and close the file. + +4. Restart both the NetworkManager and Docker. + + $ sudo restart network-manager + $ sudo restart docker + +### Configure Docker to start on boot + +Ubuntu uses `systemd` as its boot and service manager `15.04` onwards and `upstart` +for versions `14.10` and below. + +For `15.04` and up, to configure the `docker` daemon to start on boot, run + + $ sudo systemctl enable docker + +For `14.10` and below the above installation method automatically configures `upstart` +to start the docker daemon on boot + +## Upgrade Docker + +To install the latest version of Docker with `apt-get`: + + $ sudo apt-get upgrade docker-engine + +## Uninstallation + +To uninstall the Docker package: + + $ sudo apt-get purge docker-engine + +To uninstall the Docker package and dependencies that are no longer needed: + + $ sudo apt-get autoremove --purge docker-engine + +The above commands will not remove images, containers, volumes, or user created +configuration files on your host. If you wish to delete all images, containers, +and volumes run the following command: + + $ rm -rf /var/lib/docker + +You must delete the user created configuration files manually. diff --git a/docs/installation/mac.md b/docs/installation/mac.md new file mode 100644 index 00000000..3efb2f2a --- /dev/null +++ b/docs/installation/mac.md @@ -0,0 +1,428 @@ + + +# Mac OS X + +You install Docker using Docker Toolbox. Docker Toolbox includes the following Docker tools: + +* Docker Machine for running the `docker-machine` binary +* Docker Engine for running the `docker` binary +* Docker Compose for running the `docker-compose` binary +* Kitematic, the Docker GUI +* a shell preconfigured for a Docker command-line environment +* Oracle VM VirtualBox + +Because the Docker daemon uses Linux-specific kernel features, you can't run +Docker natively in OS X. Instead, you must use `docker-machine` to create and +attach to a virtual machine (VM). This machine is a Linux VM that hosts Docker +for you on your Mac. + +**Requirements** + +Your Mac must be running OS X 10.8 "Mountain Lion" or newer to install the +Docker Toolbox. + +### Learn the key concepts before installing + +In a Docker installation on Linux, your physical machine is both the localhost +and the Docker host. In networking, localhost means your computer. The Docker +host is the computer on which the containers run. + +On a typical Linux installation, the Docker client, the Docker daemon, and any +containers run directly on your localhost. This means you can address ports on a +Docker container using standard localhost addressing such as `localhost:8000` or +`0.0.0.0:8376`. + +![Linux Architecture Diagram](images/linux_docker_host.svg) + +In an OS X installation, the `docker` daemon is running inside a Linux VM called +`default`. The `default` is a lightweight Linux VM made specifically to run +the Docker daemon on Mac OS X. The VM runs completely from RAM, is a small ~24MB +download, and boots in approximately 5s. + +![OSX Architecture Diagram](images/mac_docker_host.svg) + +In OS X, the Docker host address is the address of the Linux VM. When you start +the VM with `docker-machine` it is assigned an IP address. When you start a +container, the ports on a container map to ports on the VM. To see this in +practice, work through the exercises on this page. + + +### Installation + +If you have VirtualBox running, you must shut it down before running the +installer. + +1. Go to the [Docker Toolbox](https://www.docker.com/toolbox) page. + +2. Click the Download link. + +3. Install Docker Toolbox by double-clicking the package or by right-clicking +and choosing "Open" from the pop-up menu. + + The installer launches the "Install Docker Toolbox" dialog. + + ![Install Docker Toolbox](images/mac-welcome-page.png) + +4. Press "Continue" to install the toolbox. + + The installer presents you with options to customize the standard + installation. + + ![Standard install](images/mac-page-two.png) + + By default, the standard Docker Toolbox installation: + + * installs binaries for the Docker tools in `/usr/local/bin` + * makes these binaries available to all users + * installs VirtualBox; or updates any existing installation + + To change these defaults, press "Customize" or "Change + Install Location." + +5. Press "Install" to perform the standard installation. + + The system prompts you for your password. + + ![Password prompt](images/mac-password-prompt.png) + +6. Provide your password to continue with the installation. + + When it completes, the installer provides you with some information you can + use to complete some common tasks. + + ![All finished](images/mac-page-finished.png) + +7. Press "Close" to exit. + +## Running a Docker Container + +To run a Docker container, you: + +* Create a new (or start an existing) virtual machine +* Switch your environment to your new VM +* Use the `docker` client to create, load, and manage containers + +You can reuse this virtual machine as often as you like. Like any +VirtualBox VM, it maintains its configuration between uses. + +There are two ways to use the installed tools, from the Docker Quickstart Terminal or +[from your shell](#from-your-shell). + +### From the Docker Quickstart Terminal + +1. Open the "Applications" folder or the "Launchpad". + +2. Find the Docker Quickstart Terminal and double-click to launch it. + + The application: + + * Opens a terminal window + * Creates a `default` VM if it doesn't exists, and starts the VM after + * Points the terminal environment to this VM + + Once the launch completes, the Docker Quickstart Terminal reports: + + ![All finished](images/mac-success.png) + + Now, you can run `docker` commands. + +3. Verify your setup succeeded by running the `hello-world` container. + + $ docker run hello-world + Unable to find image 'hello-world:latest' locally + 511136ea3c5a: Pull complete + 31cbccb51277: Pull complete + e45a5af57b00: Pull complete + hello-world:latest: The image you are pulling has been verified. + Important: image verification is a tech preview feature and should not be + relied on to provide security. + Status: Downloaded newer image for hello-world:latest + Hello from Docker. + This message shows that your installation appears to be working correctly. + + To generate this message, Docker took the following steps: + 1. The Docker client contacted the Docker daemon. + 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. + (Assuming it was not already locally available.) + 3. The Docker daemon created a new container from that image which runs the + executable that produces the output you are currently reading. + 4. The Docker daemon streamed that output to the Docker client, which sent it + to your terminal. + + To try something more ambitious, you can run an Ubuntu container with: + $ docker run -it ubuntu bash + + For more examples and ideas, visit: + http://docs.docker.com/userguide/ + + +A more typical way to interact with the Docker tools is from your regular shell command line. + +### From your shell + +This section assumes you are running a Bash shell. You may be running a +different shell such as C Shell but the commands are the same. + +1. Create a new Docker VM. + + $ docker-machine create --driver virtualbox default + Creating VirtualBox VM... + Creating SSH key... + Starting VirtualBox VM... + Starting VM... + To see how to connect Docker to this machine, run: docker-machine env default + + This creates a new `default` VM in VirtualBox. + + The command also creates a machine configuration in the + `~/.docker/machine/machines/default` directory. You only need to run the + `create` command once. Then, you can use `docker-machine` to start, stop, + query, and otherwise manage the VM from the command line. + +2. List your available machines. + + $ docker-machine ls + NAME ACTIVE DRIVER STATE URL SWARM + default * virtualbox Running tcp://192.168.99.101:2376 + + If you have previously installed the deprecated Boot2Docker application or + run the Docker Quickstart Terminal, you may have a `dev` VM as well. When you + created `default` VM, the `docker-machine` command provided instructions + for learning how to connect the VM. + +3. Get the environment commands for your new VM. + + $ docker-machine env default + export DOCKER_TLS_VERIFY="1" + export DOCKER_HOST="tcp://192.168.99.101:2376" + export DOCKER_CERT_PATH="/Users/mary/.docker/machine/machines/default" + export DOCKER_MACHINE_NAME="default" + # Run this command to configure your shell: + # eval "$(docker-machine env default)" + +4. Connect your shell to the `default` machine. + + $ eval "$(docker-machine env default)" + +5. Run the `hello-world` container to verify your setup. + + $ docker run hello-world + +## Learn about your Toolbox installation + +Toolbox installs the Docker Engine binary, the Docker binary on your system. When you +use the Docker Quickstart Terminal or create a `default` VM manually, Docker +Machine updates the `~/.docker/machine/machines/default` folder to your +system. This folder contains the configuration for the VM. + +You can create multiple VMs on your system with Docker Machine. Therefore, you +may end up with multiple VM folders if you have more than one VM. To remove a +VM, use the `docker-machine rm ` command. + +## Migrate from Boot2Docker + +If you were using Boot2Docker previously, you have a pre-existing Docker +`boot2docker-vm` VM on your local system. To allow Docker Machine to manage +this older VM, you can migrate it. + +1. Open a terminal or the Docker CLI on your system. + +2. Type the following command. + + $ docker-machine create -d virtualbox --virtualbox-import-boot2docker-vm boot2docker-vm docker-vm + +3. Use the `docker-machine` command to interact with the migrated VM. + +The `docker-machine` subcommands are slightly different than the `boot2docker` +subcommands. The table below lists the equivalent `docker-machine` subcommand +and what it does: + +| `boot2docker` | `docker-machine` | `docker-machine` description | +|----------------|------------------|----------------------------------------------------------| +| init | create | Creates a new docker host. | +| up | start | Starts a stopped machine. | +| ssh | ssh | Runs a command or interactive ssh session on the machine.| +| save | - | Not applicable. | +| down | stop | Stops a running machine. | +| poweroff | stop | Stops a running machine. | +| reset | restart | Restarts a running machine. | +| config | inspect | Prints machine configuration details. | +| status | ls | Lists all machines and their status. | +| info | inspect | Displays a machine's details. | +| ip | ip | Displays the machine's ip address. | +| shellinit | env | Displays shell commands needed to configure your shell to interact with a machine | +| delete | rm | Removes a machine. | +| download | - | Not applicable. | +| upgrade | upgrade | Upgrades a machine's Docker client to the latest stable release. | + + +## Examples on Mac OS X + +Work through this section to try some practical container tasks on a VM. At this +point, you should have a VM running and be connected to it through your shell. +To verify this, run the following commands: + + $ docker-machine ls + NAME ACTIVE DRIVER STATE URL SWARM + default * virtualbox Running tcp://192.168.99.100:2376 + +The `ACTIVE` machine, in this case `default`, is the one your environment is pointing to. + +### Access container ports + +1. Start an NGINX container on the DOCKER_HOST. + + $ docker run -d -P --name web nginx + + Normally, the `docker run` commands starts a container, runs it, and then + exits. The `-d` flag keeps the container running in the background + after the `docker run` command completes. The `-P` flag publishes exposed ports from the + container to your local host; this lets you access them from your Mac. + +2. Display your running container with `docker ps` command + + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 5fb65ff765e9 nginx:latest "nginx -g 'daemon of 3 minutes ago Up 3 minutes 0.0.0.0:49156->443/tcp, 0.0.0.0:49157->80/tcp web + + At this point, you can see `nginx` is running as a daemon. + +3. View just the container's ports. + + $ docker port web + 443/tcp -> 0.0.0.0:49156 + 80/tcp -> 0.0.0.0:49157 + + This tells you that the `web` container's port `80` is mapped to port + `49157` on your Docker host. + +4. Enter the `http://localhost:49157` address (`localhost` is `0.0.0.0`) in your browser: + + ![Bad Address](images/bad_host.png) + + This didn't work. The reason it doesn't work is your `DOCKER_HOST` address is + not the localhost address (0.0.0.0) but is instead the address of your Docker VM. + +5. Get the address of the `default` VM. + + $ docker-machine ip default + 192.168.59.103 + +6. Enter the `http://192.168.59.103:49157` address in your browser: + + ![Correct Addressing](images/good_host.png) + + Success! + +7. To stop and then remove your running `nginx` container, do the following: + + $ docker stop web + $ docker rm web + +### Mount a volume on the container + +When you start a container it automatically shares your `/Users/username` directory +with the VM. You can use this share point to mount directories onto your container. +The next exercise demonstrates how to do this. + +1. Change to your user `$HOME` directory. + + $ cd $HOME + +2. Make a new `site` directory. + + $ mkdir site + +3. Change into the `site` directory. + + $ cd site + +4. Create a new `index.html` file. + + $ echo "my new site" > index.html + +5. Start a new `nginx` container and replace the `html` folder with your `site` directory. + + $ docker run -d -P -v $HOME/site:/usr/share/nginx/html \ + --name mysite nginx + +6. View the `mysite` container's port. + + $ docker port mysite + 80/tcp -> 0.0.0.0:49166 + 443/tcp -> 0.0.0.0:49165 + +7. Open the site in a browser: + + ![My site page](images/newsite_view.png) + +8. Add a page to your `$HOME/site` in real time. + + $ echo "This is cool" > cool.html + +9. Open the new page in the browser. + + ![Cool page](images/cool_view.png) + +10. Stop and then remove your running `mysite` container. + + $ docker stop mysite + $ docker rm mysite + +> **Note**: There is a [known +> issue](https://docs.docker.com/machine/drivers/virtualbox/#known-issues) that +> may cause files shared with your nginx container to not update correctly as you +> modify them on your host. + +## Upgrade Docker Toolbox + +To upgrade Docker Toolbox, download and re-run the [Docker Toolbox +installer](https://docker.com/toolbox/). + + +## Uninstall Docker Toolbox + +To uninstall, do the following: + +1. List your machines. + + $ docker-machine ls + NAME ACTIVE DRIVER STATE URL SWARM + dev * virtualbox Running tcp://192.168.99.100:2376 + my-docker-machine virtualbox Stopped + default virtualbox Stopped + +2. Remove each machine. + + $ docker-machine rm dev + Successfully removed dev + + Removing a machine deletes its VM from VirtualBox and from the + `~/.docker/machine/machines` directory. + +3. Remove the Docker Quickstart Terminal and Kitematic from your "Applications" folder. + +4. Remove the `docker`, `docker-compose`, and `docker-machine` commands from the `/usr/local/bin` folder. + + $ rm /usr/local/bin/docker + +5. Delete the `~/.docker` folder from your system. + + +## Learning more + +Use `docker-machine help` to list the full command line reference for Docker Machine. For more +information about using SSH or SCP to access a VM, see the [Docker Machine +documentation](https://docs.docker.com/machine/). + +You can continue with the [Docker Engine User Guide](../userguide/index.md). If you are +interested in using the Kitematic GUI, see the [Kitematic user +guide](https://docs.docker.com/kitematic/userguide/). diff --git a/docs/installation/windows.md b/docs/installation/windows.md new file mode 100644 index 00000000..696ee339 --- /dev/null +++ b/docs/installation/windows.md @@ -0,0 +1,379 @@ + + +# Windows + +> **Note**: This release of Docker deprecates the Boot2Docker command line in +> favor of Docker Machine. Use the Docker Toolbox to install Docker Machine as +> well as the other Docker tools. + +You install Docker using Docker Toolbox. Docker Toolbox includes the following Docker tools: + +* Docker Machine for running the `docker-machine` binary +* Docker Engine for running the `docker` binary +* Kitematic, the Docker GUI +* a shell preconfigured for a Docker command-line environment +* Oracle VM VirtualBox + +Because the Docker daemon uses Linux-specific kernel features, you can't run +Docker natively in Windows. Instead, you must use `docker-machine` to create and attach to a Docker VM on your machine. This VM hosts Docker for you on your Windows system. + +The virtual machine runs a lightweight Linux distribution made specifically to +run the Docker daemon. The VirtualBox VM runs completely from RAM, is a small +~24MB download, and boots in approximately 5s. + +## Requirements + +To run Docker, your machine must have a 64-bit operating system running Windows 7 or higher. Additionally, you must make sure that virtualization is enabled on your machine. +To verify your machine meets these requirements, do the following: + +1. Right click the Windows Start Menu and choose **System**. + + ![Which version](images/win_ver.png) + + If you are using an unsupported version of Windows, you should consider + upgrading your operating system in order to try out Docker. + +2. Make sure your CPU supports [virtualization technology](https://en.wikipedia.org/wiki/X86_virtualization) +and virtualization support is enabled in BIOS and recognized by Windows. + + #### For Windows 8, 8.1 or 10 + + Choose **Start > Task Manager**. On Windows 10, click more details. Navigate to the **Performance** tab. + Under **CPU** you should see the following: + + ![Release page](images/virtualization.png) + + If virtualization is not enabled on your system, follow the manufacturer's instructions for enabling it. + + #### For Windows 7 + + Run the
Microsoft® Hardware-Assisted Virtualization Detection + Tool and follow the on-screen instructions. + +3. Verify your Windows OS is 64-bit (x64) + + How you do this verification depends on your Windows version. For details, see the Windows + article [How to determine whether a computer is running a 32-bit version or 64-bit version + of the Windows operating system](https://support.microsoft.com/en-us/kb/827218). + +> **Note**: If you have Docker hosts running and you don't wish to do a Docker Toolbox +installation, you can install the `docker.exe` using the *unofficial* Windows package +manager Chocolatey. For information on how to do this, see [Docker package on +Chocolatey](http://chocolatey.org/packages/docker). + +### Learn the key concepts before installing + +In a Docker installation on Linux, your machine is both the localhost and the +Docker host. In networking, localhost means your computer. The Docker host is +the machine on which the containers run. + +On a typical Linux installation, the Docker client, the Docker daemon, and any +containers run directly on your localhost. This means you can address ports on a +Docker container using standard localhost addressing such as `localhost:8000` or +`0.0.0.0:8376`. + +![Linux Architecture Diagram](images/linux_docker_host.svg) + +In an Windows installation, the `docker` daemon is running inside a Linux virtual +machine. You use the Windows Docker client to talk to the Docker host VM. Your +Docker containers run inside this host. + +![Windows Architecture Diagram](images/win_docker_host.svg) + +In Windows, the Docker host address is the address of the Linux VM. When you +start the VM with `docker-machine` it is assigned an IP address. When you start +a container, the ports on a container map to ports on the VM. To see this in +practice, work through the exercises on this page. + + +### Installation + +If you have VirtualBox running, you must shut it down before running the +installer. + +1. Go to the [Docker Toolbox](https://www.docker.com/toolbox) page. + +2. Click the installer link to download. + +3. Install Docker Toolbox by double-clicking the installer. + + The installer launches the "Setup - Docker Toolbox" dialog. + + ![Install Docker Toolbox](images/win-welcome.png) + +4. Press "Next" to install the toolbox. + + The installer presents you with options to customize the standard + installation. By default, the standard Docker Toolbox installation: + + * installs executables for the Docker tools in `C:\Program Files\Docker Toolbox` + * install VirtualBox; or updates any existing installation + * adds a Docker Inc. folder to your program shortcuts + * updates your `PATH` environment variable + * adds desktop icons for the Docker Quickstart Terminal and Kitematic + + This installation assumes the defaults are acceptable. + +5. Press "Next" until you reach the "Ready to Install" page. + + The system prompts you for your password. + + ![Install](images/win-page-6.png) + +6. Press "Install" to continue with the installation. + + When it completes, the installer provides you with some information you can + use to complete some common tasks. + + ![All finished](images/windows-finish.png) + +7. Press "Finish" to exit. + +## Running a Docker Container + +To run a Docker container, you: + +* Create a new (or start an existing) Docker virtual machine +* Switch your environment to your new VM +* Use the `docker` client to create, load, and manage containers + +Once you create a machine, you can reuse it as often as you like. Like any +VirtualBox VM, it maintains its configuration between uses. + +There are several ways to use the installed tools, from the Docker Quickstart Terminal or +[from your shell](#from-your-shell). + +### Using the Docker Quickstart Terminal + +1. Find the Docker Quickstart Terminal icon on your Desktop and double-click to launch it. + + The application: + + * Opens a terminal window + * Creates a `default` VM if it doesn't exist, and starts the VM after + * Points the terminal environment to this VM + + Once the launch completes, you can run `docker` commands. + +3. Verify your setup succeeded by running the `hello-world` container. + + $ docker run hello-world + Unable to find image 'hello-world:latest' locally + 511136ea3c5a: Pull complete + 31cbccb51277: Pull complete + e45a5af57b00: Pull complete + hello-world:latest: The image you are pulling has been verified. + Important: image verification is a tech preview feature and should not be + relied on to provide security. + Status: Downloaded newer image for hello-world:latest + Hello from Docker. + This message shows that your installation appears to be working correctly. + + To generate this message, Docker took the following steps: + 1. The Docker client contacted the Docker daemon. + 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. + (Assuming it was not already locally available.) + 3. The Docker daemon created a new container from that image which runs the + executable that produces the output you are currently reading. + 4. The Docker daemon streamed that output to the Docker client, which sent it + to your terminal. + + To try something more ambitious, you can run an Ubuntu container with: + $ docker run -it ubuntu bash + + For more examples and ideas, visit: + http://docs.docker.com/userguide/ + + +### Using Docker from Windows Command Prompt (cmd.exe) + +1. Launch a Windows Command Prompt (cmd.exe). + + The `docker-machine` command requires `ssh.exe` in your `PATH` environment + variable. This `.exe` is in the MsysGit `bin` folder. + +2. Add this to the `%PATH%` environment variable by running: + + set PATH=%PATH%;"c:\Program Files (x86)\Git\bin" + +3. Create a new Docker VM. + + docker-machine create --driver virtualbox my-default + Creating VirtualBox VM... + Creating SSH key... + Starting VirtualBox VM... + Starting VM... + To see how to connect Docker to this machine, run: docker-machine env my-default + + The command also creates a machine configuration in the + `C:\USERS\USERNAME\.docker\machine\machines` directory. You only need to run the `create` + command once. Then, you can use `docker-machine` to start, stop, query, and + otherwise manage the VM from the command line. + +4. List your available machines. + + C:\Users\mary> docker-machine ls + NAME ACTIVE DRIVER STATE URL SWARM + my-default * virtualbox Running tcp://192.168.99.101:2376 + + If you have previously installed the deprecated Boot2Docker application or + run the Docker Quickstart Terminal, you may have a `dev` VM as well. + +5. Get the environment commands for your new VM. + + C:\Users\mary> docker-machine env --shell cmd my-default + +6. Connect your shell to the `my-default` machine. + + C:\Users\mary> eval "$(docker-machine env my-default)" + +7. Run the `hello-world` container to verify your setup. + + C:\Users\mary> docker run hello-world + +### Using Docker from PowerShell + +1. Launch a Windows PowerShell window. + +2. Add `ssh.exe` to your PATH: + + PS C:\Users\mary> $Env:Path = "${Env:Path};c:\Program Files (x86)\Git\bin" + +3. Create a new Docker VM. + + PS C:\Users\mary> docker-machine create --driver virtualbox my-default + +4. List your available machines. + + C:\Users\mary> docker-machine ls + NAME ACTIVE DRIVER STATE URL SWARM + my-default * virtualbox Running tcp://192.168.99.101:2376 + +5. Get the environment commands for your new VM. + + C:\Users\mary> docker-machine env --shell powershell my-default + +6. Connect your shell to the `my-default` machine. + + C:\Users\mary> eval "$(docker-machine env my-default)" + +7. Run the `hello-world` container to verify your setup. + + C:\Users\mary> docker run hello-world + + +## Learn about your Toolbox installation + +Toolbox installs the Docker Engine binary in the `C:\Program Files\Docker +Toolbox` directory. When you use the Docker Quickstart Terminal or create a +`default` VM manually, Docker Machine updates the +`C:\USERS\USERNAME\.docker\machine\machines\default` folder to your +system. This folder contains the configuration for the VM. + +You can create multiple VMs on your system with Docker Machine. Therefore, you +may end up with multiple VM folders if you have created more than one VM. To +remove a VM, use the `docker-machine rm ` command. + +## Migrate from Boot2Docker + +If you were using Boot2Docker previously, you have a pre-existing Docker +`boot2docker-vm` VM on your local system. To allow Docker Machine to manage +this older VM, you can migrate it. + +1. Open a terminal or the Docker CLI on your system. + +2. Type the following command. + + $ docker-machine create -d virtualbox --virtualbox-import-boot2docker-vm boot2docker-vm docker-vm + +3. Use the `docker-machine` command to interact with the migrated VM. + +The `docker-machine` subcommands are slightly different than the `boot2docker` +subcommands. The table below lists the equivalent `docker-machine` subcommand +and what it does: + +| `boot2docker` | `docker-machine` | `docker-machine` description | +|----------------|------------------|----------------------------------------------------------| +| init | create | Creates a new docker host. | +| up | start | Starts a stopped machine. | +| ssh | ssh | Runs a command or interactive ssh session on the machine.| +| save | - | Not applicable. | +| down | stop | Stops a running machine. | +| poweroff | stop | Stops a running machine. | +| reset | restart | Restarts a running machine. | +| config | inspect | Prints machine configuration details. | +| status | ls | Lists all machines and their status. | +| info | inspect | Displays a machine's details. | +| ip | ip | Displays the machine's ip address. | +| shellinit | env | Displays shell commands needed to configure your shell to interact with a machine | +| delete | rm | Removes a machine. | +| download | - | Not applicable. | +| upgrade | upgrade | Upgrades a machine's Docker client to the latest stable release. | + + +## Upgrade Docker Toolbox + +To upgrade Docker Toolbox, download and re-run [the Docker Toolbox +installer](https://www.docker.com/toolbox). + +## Container port redirection + +If you are curious, the username for the Docker default VM is `docker` and the +password is `tcuser`. The latest version of `docker-machine` sets up a host only +network adaptor which provides access to the container's ports. + +If you run a container with a published port: + + $ docker run --rm -i -t -p 80:80 nginx + +Then you should be able to access that nginx server using the IP address +reported to you using: + + $ docker-machine ip + +Typically, the IP is 192.168.59.103, but it could get changed by VirtualBox's +DHCP implementation. + +> **Note**: There is a [known +> issue](https://docs.docker.com/machine/drivers/virtualbox/#known-issues) that +> may cause files shared with your nginx container to not update correctly as you +> modify them on your host. + +## Login with PUTTY instead of using the CMD + +Docker Machine generates and uses the public/private key pair in your +`%USERPROFILE%\.docker\machine\machines\` directory. To +log in you need to use the private key from this same directory. The private key +needs to be converted into the format PuTTY uses. You can do this with +[puttygen](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html): + +1. Open `puttygen.exe` and load ("File"->"Load" menu) the private key from (you may need to change to the `All Files (*.*)` filter) + + %USERPROFILE%\.docker\machine\machines\\id_rsa + +2. Click "Save Private Key". + +3. Use the saved file to login with PuTTY using `docker@127.0.0.1:2022`. + +## Uninstallation + +You can uninstall Docker Toolbox using Window's standard process for removing +programs. This process does not remove the `docker-install.exe` file. You must +delete that file yourself. + +## Learn more + +You can continue with the [Docker Engine User Guide](../userguide/index.md). If you are +interested in using the Kitematic GUI, see the [Kitematic user +guide](https://docs.docker.com/kitematic/userguide/). diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 00000000..8ff5c70f --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,84 @@ + + +# Migrate to Engine 1.10 + +Starting from version 1.10 of Docker Engine, we completely change the way image +data is addressed on disk. Previously, every image and layer used a randomly +assigned UUID. In 1.10 we implemented a content addressable method using an ID, +based on a secure hash of the image and layer data. + +The new method gives users more security, provides a built-in way to avoid ID +collisions and guarantee data integrity after pull, push, load, or save. It also +brings better sharing of layers by allowing many images to freely share their +layers even if they didn’t come from the same build. + +Addressing images by their content also lets us more easily detect if something +has already been downloaded. Because we have separated images and layers, you +don’t have to pull the configurations for every image that was part of the +original build chain. We also don’t need to create layers for the build +instructions that didn’t modify the filesystem. + +Content addressability is the foundation for the new distribution features. The +image pull and push code has been reworked to use a download/upload manager +concept that makes pushing and pulling images much more stable and mitigate any +parallel request issues. The download manager also brings retries on failed +downloads and better prioritization for concurrent downloads. + +We are also introducing a new manifest format that is built on top of the +content addressable base. It directly references the content addressable image +configuration and layer checksums. The new manifest format also makes it +possible for a manifest list to be used for targeting multiple +architectures/platforms. Moving to the new manifest format will be completely +transparent. + +## Preparing for upgrade + +To make your current images accessible to the new model we have to migrate them +to content addressable storage. This means calculating the secure checksums for +your current data. + +All your current images, tags and containers are automatically migrated to the +new foundation the first time you start Docker Engine 1.10. Before loading your +container, the daemon will calculate all needed checksums for your current data, +and after it has completed, all your images and tags will have brand new secure +IDs. + +**While this is simple operation, calculating SHA256 checksums for your files +can take time if you have lots of image data.** On average you should assume +that migrator can process data at a speed of 100MB/s. During this time your +Docker daemon won’t be ready to respond to requests. + +## Minimizing migration time + +If you can accept this one time hit, then upgrading Docker Engine and restarting +the daemon will transparently migrate your images. However, if you want to +minimize the daemon’s downtime, a migration utility can be run while your old +daemon is still running. + +This tool will find all your current images and calculate the checksums for +them. After you upgrade and restart the daemon, the checksum data of the +migrated images will already exist, freeing the daemon from that computation +work. If new images appeared between the migration and the upgrade, those will +be processed at time of upgrade to 1.10. + +[You can download the migration tool +here.](https://github.com/docker/v1.10-migrator/releases) + +The migration tool can also be run as a Docker image. While running the migrator +image you need to expose your Docker data directory to the container. If you use +the default path then you would run: + + $ docker run --rm -v /var/lib/docker:/var/lib/docker docker/v1.10-migrator + +If you use the +devicemapper storage driver, you also need to pass the flag `--privileged` to +give the tool access to your storage devices. diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 00000000..4f282b75 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,205 @@ + + +# Docker Engine Quickstart + +This quickstart assumes you have a working installation of Docker Engine. To verify Engine is installed and configured, use the following command: + + # Check that you have a working install + $ docker info + +If you have a successful install, the system information appears. If you get `docker: command not found` or something like +`/var/lib/docker/repositories: permission denied` you may have an +incomplete Docker installation or insufficient privileges to access +Engine on your machine. With the default installation of Engine `docker` +commands need to be run by a user that is in the `docker` group or by the +`root` user. + +Depending on your Engine system configuration, you may be required +to preface each `docker` command with `sudo`. If you want to run without using +`sudo` with the `docker` commands, then create a Unix group called `docker` and +add the user to the 'docker' group. + +For more information about installing Docker Engine or `sudo` configuration, refer to +the [installation](installation/index.md) instructions for your operating system. + + +## Download a pre-built image + +To pull an `ubuntu` image, run: + + # Download an ubuntu image + $ docker pull ubuntu + +This downloads the `ubuntu` image by name from [Docker Hub](https://hub.docker.com) to a local +image cache. To search for an image, run `docker search`. For more information, go to: +[Searching images](userguide/containers/dockerrepos.md#searching-for-images) + + +> **Note**: +> When the image is successfully downloaded, you see a 12 character +> hash `539c0211cd76: Download complete` which is the +> short form of the Image ID. These short Image IDs are the first 12 +> characters of the full Image ID. To view this information, run +> `docker inspect` or `docker images --no-trunc=true`. + +To display a list of downloaded images, run `docker images`. + +## Running an interactive shell + +To run an interactive shell in the Ubuntu image: + + $ docker run -i -t ubuntu /bin/bash + +The `-i` flag starts an interactive container. +The `-t` flag creates a pseudo-TTY that attaches `stdin` and `stdout`. +The image is `ubuntu`. +The command `/bin/bash` starts a shell you can log in. + +To detach the `tty` without exiting the shell, use the escape sequence +`Ctrl-p` + `Ctrl-q`. The container continues to exist in a stopped state +once exited. To list all running containers, run `docker ps`. To view stopped and running containers, +run `docker ps -a`. + +## Bind Docker to another host/port or a Unix socket + +> **Warning**: +> Changing the default `docker` daemon binding to a +> TCP port or Unix *docker* user group will increase your security risks +> by allowing non-root users to gain *root* access on the host. Make sure +> you control access to `docker`. If you are binding +> to a TCP port, anyone with access to that port has full Docker access; +> so it is not advisable on an open network. + +With `-H` it is possible to make the Docker daemon to listen on a +specific IP and port. By default, it will listen on +`unix:///var/run/docker.sock` to allow only local connections by the +*root* user. You *could* set it to `0.0.0.0:2375` or a specific host IP +to give access to everybody, but that is **not recommended** because +then it is trivial for someone to gain root access to the host where the +daemon is running. + +Similarly, the Docker client can use `-H` to connect to a custom port. +The Docker client will default to connecting to `unix:///var/run/docker.sock` +on Linux, and `tcp://127.0.0.1:2376` on Windows. + +`-H` accepts host and port assignment in the following format: + + tcp://[host]:[port][path] or unix://path + +For example: + +- `tcp://` -> TCP connection to `127.0.0.1` on either port `2376` when TLS encryption + is on, or port `2375` when communication is in plain text. +- `tcp://host:2375` -> TCP connection on + host:2375 +- `tcp://host:2375/path` -> TCP connection on + host:2375 and prepend path to all requests +- `unix://path/to/socket` -> Unix socket located + at `path/to/socket` + +`-H`, when empty, will default to the same value as +when no `-H` was passed in. + +`-H` also accepts short form for TCP bindings: + + `host:` or `host:port` or `:port` + +Run Docker in daemon mode: + + $ sudo /docker daemon -H 0.0.0.0:5555 & + +Download an `ubuntu` image: + + $ docker -H :5555 pull ubuntu + +You can use multiple `-H`, for example, if you want to listen on both +TCP and a Unix socket + + # Run docker in daemon mode + $ sudo /docker daemon -H tcp://127.0.0.1:2375 -H unix:///var/run/docker.sock & + # Download an ubuntu image, use default Unix socket + $ docker pull ubuntu + # OR use the TCP port + $ docker -H tcp://127.0.0.1:2375 pull ubuntu + +## Starting a long-running worker process + + # Start a very useful long-running process + $ JOB=$(docker run -d ubuntu /bin/sh -c "while true; do echo Hello world; sleep 1; done") + + # Collect the output of the job so far + $ docker logs $JOB + + # Kill the job + $ docker kill $JOB + +## Listing containers + + $ docker ps # Lists only running containers + $ docker ps -a # Lists all containers + +## Controlling containers + + # Start a new container + $ JOB=$(docker run -d ubuntu /bin/sh -c "while true; do echo Hello world; sleep 1; done") + + # Stop the container + $ docker stop $JOB + + # Start the container + $ docker start $JOB + + # Restart the container + $ docker restart $JOB + + # SIGKILL a container + $ docker kill $JOB + + # Remove a container + $ docker stop $JOB # Container must be stopped to remove it + $ docker rm $JOB + +## Bind a service on a TCP port + + # Bind port 4444 of this container, and tell netcat to listen on it + $ JOB=$(docker run -d -p 4444 ubuntu:12.10 /bin/nc -l 4444) + + # Which public port is NATed to my container? + $ PORT=$(docker port $JOB 4444 | awk -F: '{ print $2 }') + + # Connect to the public port + $ echo hello world | nc 127.0.0.1 $PORT + + # Verify that the network connection worked + $ echo "Daemon received: $(docker logs $JOB)" + +## Committing (saving) a container state + +To save the current state of a container as an image: + + $ docker commit + +When you commit your container, Docker Engine only stores the diff (difference) between +the source image and the current state of the container's image. To list images +you already have, run: + + # List your images + $ docker images + +You now have an image state from which you can create new instances. + +## Where to go next + +* Work your way through the [Docker Engine User Guide](userguide/index.md) +* Read more about [Store Images on Docker Hub](userguide/containers/dockerrepos.md) +* Review [Command Line](reference/commandline/cli.md) diff --git a/docs/reference/api/README.md b/docs/reference/api/README.md new file mode 100644 index 00000000..36485223 --- /dev/null +++ b/docs/reference/api/README.md @@ -0,0 +1,15 @@ + + +This directory holds the authoritative specifications of APIs defined and implemented by Docker. Currently this includes: + + * The remote API by which a docker node can be queried over HTTP + * The registry API by which a docker node can download and upload + images for storage and sharing + * The index search API by which a docker node can search the public + index for images to download + * The docker.io OAuth and accounts API which 3rd party services can + use to access account information diff --git a/docs/reference/api/_static/io_oauth_authorization_page.png b/docs/reference/api/_static/io_oauth_authorization_page.png new file mode 100644 index 0000000000000000000000000000000000000000..455d631e178921fd513e05b5418fd9f588c6e6a6 GIT binary patch literal 39458 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/reference/api/docker-io_api.md b/docs/reference/api/docker-io_api.md new file mode 100644 index 00000000..5e3c6844 --- /dev/null +++ b/docs/reference/api/docker-io_api.md @@ -0,0 +1,16 @@ + + +# Docker Hub API + +This API is deprecated as of 1.7. To view the old version, see the [Docker Hub +API](https://docs.docker.com/v1.7/docker/reference/api/docker-io_api/) in the 1.7 documentation. diff --git a/docs/reference/api/docker_io_accounts_api.md b/docs/reference/api/docker_io_accounts_api.md new file mode 100644 index 00000000..dfee194b --- /dev/null +++ b/docs/reference/api/docker_io_accounts_api.md @@ -0,0 +1,277 @@ + + +# docker.io accounts API + +## Get a single user + +`GET /api/v1.1/users/:username/` + +Get profile info for the specified user. + +Parameters: + +- **username** – username of the user whose profile info is being + requested. + +Request Headers: + +- **Authorization** – required authentication credentials of + either type HTTP Basic or OAuth Bearer Token. + +Status Codes: + +- **200** – success, user data returned. +- **401** – authentication error. +- **403** – permission error, authenticated user must be the user + whose data is being requested, OAuth access tokens must have + `profile_read` scope. +- **404** – the specified username does not exist. + +**Example request**: + + GET /api/v1.1/users/janedoe/ HTTP/1.1 + Host: www.docker.io + Accept: application/json + Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ= + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "id": 2, + "username": "janedoe", + "url": "https://www.docker.io/api/v1.1/users/janedoe/", + "date_joined": "2014-02-12T17:58:01.431312Z", + "type": "User", + "full_name": "Jane Doe", + "location": "San Francisco, CA", + "company": "Success, Inc.", + "profile_url": "https://docker.io/", + "gravatar_url": "https://secure.gravatar.com/avatar/0212b397124be4acd4e7dea9aa357.jpg?s=80&r=g&d=mm" + "email": "jane.doe@example.com", + "is_active": true + } + +## Update a single user + +`PATCH /api/v1.1/users/:username/` + +Update profile info for the specified user. + +Parameters: + +- **username** – username of the user whose profile info is being + updated. + +Json Parameters: + +- **full_name** (*string*) – (optional) the new name of the user. +- **location** (*string*) – (optional) the new location. +- **company** (*string*) – (optional) the new company of the user. +- **profile_url** (*string*) – (optional) the new profile url. +- **gravatar_email** (*string*) – (optional) the new Gravatar + email address. + +Request Headers: + +- **Authorization** – required authentication credentials of + either type HTTP Basic or OAuth Bearer Token. +- **Content-Type** – MIME Type of post data. JSON, url-encoded + form data, etc. + +Status Codes: + +- **200** – success, user data updated. +- **400** – post data validation error. +- **401** – authentication error. +- **403** – permission error, authenticated user must be the user + whose data is being updated, OAuth access tokens must have + `profile_write` scope. +- **404** – the specified username does not exist. + +**Example request**: + + PATCH /api/v1.1/users/janedoe/ HTTP/1.1 + Host: www.docker.io + Accept: application/json + Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ= + + { + "location": "Private Island", + "profile_url": "http://janedoe.com/", + "company": "Retired", + } + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "id": 2, + "username": "janedoe", + "url": "https://www.docker.io/api/v1.1/users/janedoe/", + "date_joined": "2014-02-12T17:58:01.431312Z", + "type": "User", + "full_name": "Jane Doe", + "location": "Private Island", + "company": "Retired", + "profile_url": "http://janedoe.com/", + "gravatar_url": "https://secure.gravatar.com/avatar/0212b397124be4acd4e7dea9aa357.jpg?s=80&r=g&d=mm" + "email": "jane.doe@example.com", + "is_active": true + } + +## List email addresses for a user + +`GET /api/v1.1/users/:username/emails/` + +List email info for the specified user. + +Parameters: + +- **username** – username of the user whose profile info is being + updated. + +Request Headers: + +- **Authorization** – required authentication credentials of + either type HTTP Basic or OAuth Bearer Token + +Status Codes: + +- **200** – success, user data updated. +- **401** – authentication error. +- **403** – permission error, authenticated user must be the user + whose data is being requested, OAuth access tokens must have + `email_read` scope. +- **404** – the specified username does not exist. + +**Example request**: + + GET /api/v1.1/users/janedoe/emails/ HTTP/1.1 + Host: www.docker.io + Accept: application/json + Authorization: Bearer zAy0BxC1wDv2EuF3tGs4HrI6qJp6KoL7nM + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "email": "jane.doe@example.com", + "verified": true, + "primary": true + } + ] + +## Add email address for a user + +`POST /api/v1.1/users/:username/emails/` + +Add a new email address to the specified user's account. The email +address must be verified separately, a confirmation email is not +automatically sent. + +Json Parameters: + +- **email** (*string*) – email address to be added. + +Request Headers: + +- **Authorization** – required authentication credentials of + either type HTTP Basic or OAuth Bearer Token. +- **Content-Type** – MIME Type of post data. JSON, url-encoded + form data, etc. + +Status Codes: + +- **201** – success, new email added. +- **400** – data validation error. +- **401** – authentication error. +- **403** – permission error, authenticated user must be the user + whose data is being requested, OAuth access tokens must have + `email_write` scope. +- **404** – the specified username does not exist. + +**Example request**: + + POST /api/v1.1/users/janedoe/emails/ HTTP/1.1 + Host: www.docker.io + Accept: application/json + Content-Type: application/json + Authorization: Bearer zAy0BxC1wDv2EuF3tGs4HrI6qJp6KoL7nM + + { + "email": "jane.doe+other@example.com" + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "email": "jane.doe+other@example.com", + "verified": false, + "primary": false + } + +## Delete email address for a user + +`DELETE /api/v1.1/users/:username/emails/` + +Delete an email address from the specified user's account. You +cannot delete a user's primary email address. + +Json Parameters: + +- **email** (*string*) – email address to be deleted. + +Request Headers: + +- **Authorization** – required authentication credentials of + either type HTTP Basic or OAuth Bearer Token. +- **Content-Type** – MIME Type of post data. JSON, url-encoded + form data, etc. + +Status Codes: + +- **204** – success, email address removed. +- **400** – validation error. +- **401** – authentication error. +- **403** – permission error, authenticated user must be the user + whose data is being requested, OAuth access tokens must have + `email_write` scope. +- **404** – the specified username or email address does not + exist. + +**Example request**: + + DELETE /api/v1.1/users/janedoe/emails/ HTTP/1.1 + Host: www.docker.io + Accept: application/json + Content-Type: application/json + Authorization: Bearer zAy0BxC1wDv2EuF3tGs4HrI6qJp6KoL7nM + + { + "email": "jane.doe+other@example.com" + } + +**Example response**: + + HTTP/1.1 204 NO CONTENT + Content-Length: 0 diff --git a/docs/reference/api/docker_remote_api.md b/docs/reference/api/docker_remote_api.md new file mode 100644 index 00000000..34914c4e --- /dev/null +++ b/docs/reference/api/docker_remote_api.md @@ -0,0 +1,291 @@ + + +# Docker Remote API + +Docker's Remote API uses an open schema model. In this model, unknown +properties in incoming messages are ignored. Client applications need to take +this behavior into account to ensure they do not break when talking to newer +Docker daemons. + +The API tends to be REST, but for some complex commands, like attach or pull, +the HTTP connection is hijacked to transport STDOUT, STDIN, and STDERR. + +By default the Docker daemon listens on `unix:///var/run/docker.sock` and the +client must have `root` access to interact with the daemon. If a group named +`docker` exists on your system, `docker` applies ownership of the socket to the +group. + +To connect to the Docker daemon with cURL you need to use cURL 7.40 or +later, as these versions have the `--unix-socket` flag available. To +run `curl` against the daemon on the default socket, use the +following: + + curl --unix-socket /var/run/docker.sock http:/containers/json + +If you have bound the Docker daemon to a different socket path or TCP +port, you would reference that in your cURL rather than the +default. + +The current version of the API is v1.23 which means calling `/info` is the same +as calling `/v1.23/info`. To call an older version of the API use +`/v1.22/info`. + +Use the table below to find the API version for a Docker version: + +Docker version | API version | Changes +----------------|------------------------------------|------------------------------------------------------ +1.11.x | [1.23](docker_remote_api_v1.23.md) | [API changes](docker_remote_api.md#v1-23-api-changes) +1.10.x | [1.22](docker_remote_api_v1.22.md) | [API changes](docker_remote_api.md#v1-22-api-changes) +1.9.x | [1.21](docker_remote_api_v1.21.md) | [API changes](docker_remote_api.md#v1-21-api-changes) +1.8.x | [1.20](docker_remote_api_v1.20.md) | [API changes](docker_remote_api.md#v1-20-api-changes) +1.7.x | [1.19](docker_remote_api_v1.19.md) | [API changes](docker_remote_api.md#v1-19-api-changes) +1.6.x | [1.18](docker_remote_api_v1.18.md) | [API changes](docker_remote_api.md#v1-18-api-changes) +1.5.x | [1.17](docker_remote_api_v1.17.md) | [API changes](docker_remote_api.md#v1-17-api-changes) +1.4.x | [1.16](docker_remote_api_v1.16.md) | [API changes](docker_remote_api.md#v1-16-api-changes) +1.3.x | [1.15](docker_remote_api_v1.15.md) | [API changes](docker_remote_api.md#v1-15-api-changes) +1.2.x | [1.14](docker_remote_api_v1.14.md) | [API changes](docker_remote_api.md#v1-14-api-changes) + +Refer to the [GitHub repository]( +https://github.com/docker/docker/tree/master/docs/reference/api) for +older releases. + +## Authentication + +Since API version 1.2, the auth configuration is now handled client side, so the +client has to send the `authConfig` as a `POST` in `/images/(name)/push`. The +`authConfig`, set as the `X-Registry-Auth` header, is currently a Base64 encoded +(JSON) string with the following structure: + +``` +{"username": "string", "password": "string", "email": "string", + "serveraddress" : "string", "auth": ""} +``` + +Callers should leave the `auth` empty. The `serveraddress` is a domain/ip +without protocol. Throughout this structure, double quotes are required. + +## Using Docker Machine with the API + +If you are using `docker-machine`, the Docker daemon is on a host that +uses an encrypted TCP socket using TLS. This means, for Docker Machine users, +you need to add extra parameters to `curl` or `wget` when making test +API requests, for example: + +``` +curl --insecure \ + --cert $DOCKER_CERT_PATH/cert.pem \ + --key $DOCKER_CERT_PATH/key.pem \ + https://YOUR_VM_IP:2376/images/json + +wget --no-check-certificate --certificate=$DOCKER_CERT_PATH/cert.pem \ + --private-key=$DOCKER_CERT_PATH/key.pem \ + https://YOUR_VM_IP:2376/images/json -O - -q +``` + +## Docker Events + +The following diagram depicts the container states accessible through the API. + +![States](images/event_state.png) + +Some container-related events are not affected by container state, so they are not included in this diagram. These events are: + +* **export** emitted by `docker export` +* **exec_create** emitted by `docker exec` +* **exec_start** emitted by `docker exec` after **exec_create** + +Running `docker rmi` emits an **untag** event when removing an image name. The `rmi` command may also emit **delete** events when images are deleted by ID directly or by deleting the last tag referring to the image. + +> **Acknowledgment**: This diagram and the accompanying text were used with the permission of Matt Good and Gilder Labs. See Matt's original blog post [Docker Events Explained](https://gliderlabs.com/blog/2015/04/14/docker-events-explained/). + +## Version history + +This section lists each version from latest to oldest. Each listing includes a link to the full documentation set and the changes relevant in that release. + +### v1.23 API changes + +[Docker Remote API v1.23](docker_remote_api_v1.23.md) documentation + +* `GET /containers/json` returns the state of the container, one of `created`, `restarting`, `running`, `paused`, `exited` or `dead`. +* `GET /containers/json` returns the mount points for the container. +* `GET /networks/(name)` now returns an `Internal` field showing whether the network is internal or not. +* `GET /networks/(name)` now returns an `EnableIPv6` field showing whether the network has ipv6 enabled or not. +* `POST /containers/(name)/update` now supports updating container's restart policy. +* `POST /networks/create` now supports enabling ipv6 on the network by setting the `EnableIPv6` field (doing this with a label will no longer work). +* `GET /info` now returns `CgroupDriver` field showing what cgroup driver the daemon is using; `cgroupfs` or `systemd`. +* `GET /info` now returns `KernelMemory` field, showing if "kernel memory limit" is supported. +* `POST /containers/create` now takes `PidsLimit` field, if the kernel is >= 4.3 and the pids cgroup is supported. +* `GET /containers/(id or name)/stats` now returns `pids_stats`, if the kernel is >= 4.3 and the pids cgroup is supported. +* `POST /containers/create` now allows you to override usernamespaces remapping and use privileged options for the container. +* `POST /containers/create` now allows specifying `nocopy` for named volumes, which disables automatic copying from the container path to the volume. +* `POST /auth` now returns an `IdentityToken` when supported by a registry. +* `POST /containers/create` with both `Hostname` and `Domainname` fields specified will result in the container's hostname being set to `Hostname`, rather than `Hostname.Domainname`. + +### v1.22 API changes + +[Docker Remote API v1.22](docker_remote_api_v1.22.md) documentation + +* `POST /container/(name)/update` updates the resources of a container. +* `GET /containers/json` supports filter `isolation` on Windows. +* `GET /containers/json` now returns the list of networks of containers. +* `GET /info` Now returns `Architecture` and `OSType` fields, providing information + about the host architecture and operating system type that the daemon runs on. +* `GET /networks/(name)` now returns a `Name` field for each container attached to the network. +* `GET /version` now returns the `BuildTime` field in RFC3339Nano format to make it + consistent with other date/time values returned by the API. +* `AuthConfig` now supports a `registrytoken` for token based authentication +* `POST /containers/create` now has a 4M minimum value limit for `HostConfig.KernelMemory` +* Pushes initiated with `POST /images/(name)/push` and pulls initiated with `POST /images/create` + will be cancelled if the HTTP connection making the API request is closed before + the push or pull completes. +* `POST /containers/create` now allows you to set a read/write rate limit for a + device (in bytes per second or IO per second). +* `GET /networks` now supports filtering by `name`, `id` and `type`. +* `POST /containers/create` now allows you to set the static IPv4 and/or IPv6 address for the container. +* `POST /networks/(id)/connect` now allows you to set the static IPv4 and/or IPv6 address for the container. +* `GET /info` now includes the number of containers running, stopped, and paused. +* `POST /networks/create` now supports restricting external access to the network by setting the `Internal` field. +* `POST /networks/(id)/disconnect` now includes a `Force` option to forcefully disconnect a container from network +* `GET /containers/(id)/json` now returns the `NetworkID` of containers. +* `POST /networks/create` Now supports an options field in the IPAM config that provides options + for custom IPAM plugins. +* `GET /networks/{network-id}` Now returns IPAM config options for custom IPAM plugins if any + are available. +* `GET /networks/` now returns subnets info for user-defined networks. +* `GET /info` can now return a `SystemStatus` field useful for returning additional information about applications + that are built on top of engine. + +### v1.21 API changes + +[Docker Remote API v1.21](docker_remote_api_v1.21.md) documentation + +* `GET /volumes` lists volumes from all volume drivers. +* `POST /volumes/create` to create a volume. +* `GET /volumes/(name)` get low-level information about a volume. +* `DELETE /volumes/(name)` remove a volume with the specified name. +* `VolumeDriver` was moved from `config` to `HostConfig` to make the configuration portable. +* `GET /images/(name)/json` now returns information about an image's `RepoTags` and `RepoDigests`. +* The `config` option now accepts the field `StopSignal`, which specifies the signal to use to kill a container. +* `GET /containers/(id)/stats` will return networking information respectively for each interface. +* The `HostConfig` option now includes the `DnsOptions` field to configure the container's DNS options. +* `POST /build` now optionally takes a serialized map of build-time variables. +* `GET /events` now includes a `timenano` field, in addition to the existing `time` field. +* `GET /events` now supports filtering by image and container labels. +* `GET /info` now lists engine version information and return the information of `CPUShares` and `Cpuset`. +* `GET /containers/json` will return `ImageID` of the image used by container. +* `POST /exec/(name)/start` will now return an HTTP 409 when the container is either stopped or paused. +* `GET /containers/(name)/json` now accepts a `size` parameter. Setting this parameter to '1' returns container size information in the `SizeRw` and `SizeRootFs` fields. +* `GET /containers/(name)/json` now returns a `NetworkSettings.Networks` field, + detailing network settings per network. This field deprecates the + `NetworkSettings.Gateway`, `NetworkSettings.IPAddress`, + `NetworkSettings.IPPrefixLen`, and `NetworkSettings.MacAddress` fields, which + are still returned for backward-compatibility, but will be removed in a future version. +* `GET /exec/(id)/json` now returns a `NetworkSettings.Networks` field, + detailing networksettings per network. This field deprecates the + `NetworkSettings.Gateway`, `NetworkSettings.IPAddress`, + `NetworkSettings.IPPrefixLen`, and `NetworkSettings.MacAddress` fields, which + are still returned for backward-compatibility, but will be removed in a future version. +* The `HostConfig` option now includes the `OomScoreAdj` field for adjusting the + badness heuristic. This heuristic selects which processes the OOM killer kills + under out-of-memory conditions. + +### v1.20 API changes + +[Docker Remote API v1.20](docker_remote_api_v1.20.md) documentation + +* `GET /containers/(id)/archive` get an archive of filesystem content from a container. +* `PUT /containers/(id)/archive` upload an archive of content to be extracted to +an existing directory inside a container's filesystem. +* `POST /containers/(id)/copy` is deprecated in favor of the above `archive` +endpoint which can be used to download files and directories from a container. +* The `hostConfig` option now accepts the field `GroupAdd`, which specifies a +list of additional groups that the container process will run as. + +### v1.19 API changes + +[Docker Remote API v1.19](docker_remote_api_v1.19.md) documentation + +* When the daemon detects a version mismatch with the client, usually when +the client is newer than the daemon, an HTTP 400 is now returned instead +of a 404. +* `GET /containers/(id)/stats` now accepts `stream` bool to get only one set of stats and disconnect. +* `GET /containers/(id)/logs` now accepts a `since` timestamp parameter. +* `GET /info` The fields `Debug`, `IPv4Forwarding`, `MemoryLimit`, and +`SwapLimit` are now returned as boolean instead of as an int. In addition, the +end point now returns the new boolean fields `CpuCfsPeriod`, `CpuCfsQuota`, and +`OomKillDisable`. +* The `hostConfig` option now accepts the fields `CpuPeriod` and `CpuQuota` +* `POST /build` accepts `cpuperiod` and `cpuquota` options + +### v1.18 API changes + +[Docker Remote API v1.18](docker_remote_api_v1.18.md) documentation + +* `GET /version` now returns `Os`, `Arch` and `KernelVersion`. +* `POST /containers/create` and `POST /containers/(id)/start`allow you to set ulimit settings for use in the container. +* `GET /info` now returns `SystemTime`, `HttpProxy`,`HttpsProxy` and `NoProxy`. +* `GET /images/json` added a `RepoDigests` field to include image digest information. +* `POST /build` can now set resource constraints for all containers created for the build. +* `CgroupParent` can be passed in the host config to setup container cgroups under a specific cgroup. +* `POST /build` closing the HTTP request cancels the build +* `POST /containers/(id)/exec` includes `Warnings` field to response. + +### v1.17 API changes + +[Docker Remote API v1.17](docker_remote_api_v1.17.md) documentation + +* The build supports `LABEL` command. Use this to add metadata to an image. For +example you could add data describing the content of an image. `LABEL +"com.example.vendor"="ACME Incorporated"` +* `POST /containers/(id)/attach` and `POST /exec/(id)/start` +* The Docker client now hints potential proxies about connection hijacking using HTTP Upgrade headers. +* `POST /containers/create` sets labels on container create describing the container. +* `GET /containers/json` returns the labels associated with the containers (`Labels`). +* `GET /containers/(id)/json` returns the list current execs associated with the +container (`ExecIDs`). This endpoint now returns the container labels +(`Config.Labels`). +* `POST /containers/(id)/rename` renames a container `id` to a new name.* +* `POST /containers/create` and `POST /containers/(id)/start` callers can pass +`ReadonlyRootfs` in the host config to mount the container's root filesystem as +read only. +* `GET /containers/(id)/stats` returns a live stream of a container's resource usage statistics. +* `GET /images/json` returns the labels associated with each image (`Labels`). + + +### v1.16 API changes + +[Docker Remote API v1.16](docker_remote_api_v1.16.md) + +* `GET /info` returns the number of CPUs available on the machine (`NCPU`), +total memory available (`MemTotal`), a user-friendly name describing the running Docker daemon (`Name`), a unique ID identifying the daemon (`ID`), and +a list of daemon labels (`Labels`). +* `POST /containers/create` callers can set the new container's MAC address explicitly. +* Volumes are now initialized when the container is created. +* `POST /containers/(id)/copy` copies data which is contained in a volume. + +### v1.15 API changes + +[Docker Remote API v1.15](docker_remote_api_v1.15.md) documentation + +`POST /containers/create` you can set a container's `HostConfig` when creating a +container. Previously this was only available when starting a container. + +### v1.14 API changes + +[Docker Remote API v1.14](docker_remote_api_v1.14.md) documentation + +* `DELETE /containers/(id)` when using `force`, the container will be immediately killed with SIGKILL. +* `POST /containers/(id)/start` the `HostConfig` option accepts the field `CapAdd`, which specifies a list of capabilities +to add, and the field `CapDrop`, which specifies a list of capabilities to drop. +* `POST /images/create` th `fromImage` and `repo` parameters support the +`repo:tag` format. Consequently, the `tag` parameter is now obsolete. Using the +new format and the `tag` parameter at the same time will return an error. diff --git a/docs/reference/api/docker_remote_api_v1.14.md b/docs/reference/api/docker_remote_api_v1.14.md new file mode 100644 index 00000000..e7c8fde6 --- /dev/null +++ b/docs/reference/api/docker_remote_api_v1.14.md @@ -0,0 +1,1470 @@ + + +# Docker Remote API v1.14 + +## 1. Brief introduction + + - The Remote API has replaced `rcli`. + - The daemon listens on `unix:///var/run/docker.sock` but you can + [Bind Docker to another host/port or a Unix socket](../../quickstart.md#bind-docker-to-another-host-port-or-a-unix-socket). + - The API tends to be REST, but for some complex commands, like `attach` + or `pull`, the HTTP connection is hijacked to transport `STDOUT`, + `STDIN` and `STDERR`. + +# 2. Endpoints + +## 2.1 Containers + +### List containers + +`GET /containers/json` + +List containers + +**Example request**: + + GET /containers/json?all=1&before=8dfafdbc3a40&size=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "8dfafdbc3a40", + "Image": "ubuntu:latest", + "Command": "echo 1", + "Created": 1367854155, + "Status": "Exit 0", + "Ports": [{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "SizeRw": 12288, + "SizeRootFs": 0 + }, + { + "Id": "9cd87474be90", + "Image": "ubuntu:latest", + "Command": "echo 222222", + "Created": 1367854155, + "Status": "Exit 0", + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 + }, + { + "Id": "3176a2479c92", + "Image": "ubuntu:latest", + "Command": "echo 3333333333333333", + "Created": 1367854154, + "Status": "Exit 0", + "Ports":[], + "SizeRw":12288, + "SizeRootFs":0 + }, + { + "Id": "4cb07b47f9fb", + "Image": "ubuntu:latest", + "Command": "echo 444444444444444444444444444444444", + "Created": 1367854152, + "Status": "Exit 0", + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 + } + ] + +Query Parameters: + +- **all** – 1/True/true or 0/False/false, Show all containers. + Only running containers are shown by default (i.e., this defaults to false) +- **limit** – Show `limit` last created containers, include non-running ones. +- **since** – Show only containers created since Id, include non-running ones. +- **before** – Show only containers created before Id, include non-running ones. +- **size** – 1/True/true or 0/False/false, Show the containers sizes +- **filters** - a json encoded value of the filters (a map[string][]string) to process on the containers list. Available filters: + - exited=<int> -- containers with exit code of <int> + - status=(restarting|running|paused|exited) + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **500** – server error + +### Create a container + +`POST /containers/create` + +Create a container + +**Example request**: + + POST /containers/create HTTP/1.1 + Content-Type: application/json + + { + "Hostname":"", + "Domainname": "", + "User":"", + "Memory":0, + "MemorySwap":0, + "CpuShares": 512, + "Cpuset": "0,1", + "AttachStdin":false, + "AttachStdout":true, + "AttachStderr":true, + "PortSpecs":null, + "Tty":false, + "OpenStdin":false, + "StdinOnce":false, + "Env": [ + "FOO=bar", + "BAZ=quux" + ], + "Cmd":[ + "date" + ], + "Image":"ubuntu", + "Volumes":{ + "/tmp": {} + }, + "WorkingDir":"", + "NetworkDisabled": false, + "ExposedPorts":{ + "22/tcp": {} + }, + "RestartPolicy": { "Name": "always" } + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "Id":"e90e34656806" + "Warnings":[] + } + +Json Parameters: + +- **RestartPolicy** – The behavior to apply when the container exits. The + value is an object with a `Name` property of either `"always"` to + always restart or `"on-failure"` to restart only when the container + exit code is non-zero. If `on-failure` is used, `MaximumRetryCount` + controls the number of times to retry before giving up. + The default is not to restart. (optional) + An ever increasing delay (double the previous delay, starting at 100mS) + is added before each restart to prevent flooding the server. +- **config** – the container's configuration + +Query Parameters: + +- **name** – Assign the specified name to the container. Must match `/?[a-zA-Z0-9_-]+`. + +Status Codes: + +- **201** – no error +- **404** – no such container +- **406** – impossible to attach (container not running) +- **500** – server error + +### Inspect a container + +`GET /containers/(id or name)/json` + +Return low-level information on the container `id` + + +**Example request**: + + GET /containers/4fa6e0f0c678/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Id": "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2", + "Created": "2013-05-07T14:51:42.041847+02:00", + "Path": "date", + "Args": [], + "Config": { + "Hostname": "4fa6e0f0c678", + "User": "", + "Memory": 0, + "MemorySwap": 0, + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "PortSpecs": null, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "date" + ], + "Dns": null, + "Image": "ubuntu", + "Volumes": {}, + "VolumesFrom": "", + "WorkingDir": "" + }, + "State": { + "Running": false, + "Pid": 0, + "ExitCode": 0, + "StartedAt": "2013-05-07T14:51:42.087658+02:01360", + "Ghost": false + }, + "Image": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "NetworkSettings": { + "IpAddress": "", + "IpPrefixLen": 0, + "Gateway": "", + "Bridge": "", + "PortMapping": null + }, + "SysInitPath": "/home/kitty/go/src/github.com/docker/docker/bin/docker", + "ResolvConfPath": "/etc/resolv.conf", + "Volumes": {}, + "HostConfig": { + "Binds": null, + "ContainerIDFile": "", + "LxcConf": [], + "Privileged": false, + "PortBindings": { + "80/tcp": [ + { + "HostIp": "0.0.0.0", + "HostPort": "49153" + } + ] + }, + "Links": ["/name:alias"], + "PublishAllPorts": false, + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"] + } + } + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### List processes running inside a container + +`GET /containers/(id or name)/top` + +List processes running inside the container `id`. On Unix systems this +is done by running the `ps` command. This endpoint is not +supported on Windows. + +**Example request**: + + GET /containers/4fa6e0f0c678/top HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Titles" : [ + "UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD" + ], + "Processes" : [ + [ + "root", "13642", "882", "0", "17:03", "pts/0", "00:00:00", "/bin/bash" + ], + [ + "root", "13735", "13642", "0", "17:06", "pts/0", "00:00:00", "sleep 10" + ] + ] + } + +**Example request**: + + GET /containers/4fa6e0f0c678/top?ps_args=aux HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Titles" : [ + "USER","PID","%CPU","%MEM","VSZ","RSS","TTY","STAT","START","TIME","COMMAND" + ] + "Processes" : [ + [ + "root","13642","0.0","0.1","18172","3184","pts/0","Ss","17:03","0:00","/bin/bash" + ], + [ + "root","13895","0.0","0.0","4348","692","pts/0","S+","17:15","0:00","sleep 10" + ] + ], + } + +Query Parameters: + +- **ps_args** – `ps` arguments to use (e.g., `aux`), defaults to `-ef` + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Get container logs + +`GET /containers/(id or name)/logs` + +Get stdout and stderr logs from the container ``id`` + +**Example request**: + + GET /containers/4fa6e0f0c678/logs?stderr=1&stdout=1×tamps=1&follow=1&tail=10 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + +Query Parameters: + +- **follow** – 1/True/true or 0/False/false, return stream. Default false +- **stdout** – 1/True/true or 0/False/false, show stdout log. Default false +- **stderr** – 1/True/true or 0/False/false, show stderr log. Default false +- **timestamps** – 1/True/true or 0/False/false, print timestamps for every + log line. Default false +- **tail** – Output specified number of lines at the end of logs: `all` or + ``. Default all + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Inspect changes on a container's filesystem + +`GET /containers/(id or name)/changes` + +Inspect changes on container `id`'s filesystem + +**Example request**: + + GET /containers/4fa6e0f0c678/changes HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Path": "/dev", + "Kind": 0 + }, + { + "Path": "/dev/kmsg", + "Kind": 1 + }, + { + "Path": "/test", + "Kind": 1 + } + ] + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Export a container + +`GET /containers/(id or name)/export` + +Export the contents of container `id` + +**Example request**: + + GET /containers/4fa6e0f0c678/export HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/octet-stream + + {{ TAR STREAM }} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Start a container + +`POST /containers/(id or name)/start` + +Start the container `id` + +**Example request**: + + POST /containers/e90e34656806/start HTTP/1.1 + Content-Type: application/json + + { + "Binds":["/tmp:/tmp"], + "Links":["redis3:redis"], + "LxcConf":[{"Key":"lxc.utsname","Value":"docker"}], + "PortBindings":{ "22/tcp": [{ "HostPort": "11022" }] }, + "PublishAllPorts":false, + "Privileged":false, + "Dns": ["8.8.8.8"], + "VolumesFrom": ["parent", "other:ro"], + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"] + } + +**Example response**: + + HTTP/1.1 204 No Content + +Json Parameters: + +- **hostConfig** – the container's host configuration (optional) + +Status Codes: + +- **204** – no error +- **304** – container already started +- **404** – no such container +- **500** – server error + +### Stop a container + +`POST /containers/(id or name)/stop` + +Stop the container `id` + +**Example request**: + + POST /containers/e90e34656806/stop?t=5 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **t** – number of seconds to wait before killing the container + +Status Codes: + +- **204** – no error +- **304** – container already stopped +- **404** – no such container +- **500** – server error + +### Restart a container + +`POST /containers/(id or name)/restart` + +Restart the container `id` + +**Example request**: + + POST /containers/e90e34656806/restart?t=5 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **t** – number of seconds to wait before killing the container + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Kill a container + +`POST /containers/(id or name)/kill` + +Kill the container `id` + +**Example request**: + + POST /containers/e90e34656806/kill HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters + +- **signal** - Signal to send to the container: integer or string like "SIGINT". + When not set, SIGKILL is assumed and the call will wait for the container to exit. + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Pause a container + +`POST /containers/(id or name)/pause` + +Pause the container `id` + +**Example request**: + + POST /containers/e90e34656806/pause HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Unpause a container + +`POST /containers/(id or name)/unpause` + +Unpause the container `id` + +**Example request**: + + POST /containers/e90e34656806/unpause HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Attach to a container + +`POST /containers/(id or name)/attach` + +Attach to the container `id` + +**Example request**: + + POST /containers/16253994b7c4/attach?logs=1&stream=0&stdout=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach to stdin. + Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + + **Stream details**: + + When using the TTY setting is enabled in + [`POST /containers/create`](#create-a-container), + the stream is the raw data from the process PTY and client's stdin. + When the TTY is disabled, then the stream is multiplexed to separate + stdout and stderr. + + The format is a **Header** and a **Payload** (frame). + + **HEADER** + + The header will contain the information on which stream write the + stream (stdout or stderr). It also contain the size of the + associated frame encoded on the last 4 bytes (uint32). + + It is encoded on the first 8 bytes like this: + + header := [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4} + + `STREAM_TYPE` can be: + +- 0: stdin (will be written on stdout) +- 1: stdout +- 2: stderr + + `SIZE1, SIZE2, SIZE3, SIZE4` are the 4 bytes of + the uint32 size encoded as big endian. + + **PAYLOAD** + + The payload is the raw stream. + + **IMPLEMENTATION** + + The simplest way to implement the Attach protocol is the following: + + 1. Read 8 bytes + 2. chose stdout or stderr depending on the first byte + 3. Extract the frame size from the last 4 bytes + 4. Read the extracted size and output it on the correct output + 5. Goto 1 + +### Attach to a container (websocket) + +`GET /containers/(id or name)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + +### Wait a container + +`POST /containers/(id or name)/wait` + +Block until container `id` stops, then returns the exit code + +**Example request**: + + POST /containers/16253994b7c4/wait HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"StatusCode": 0} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Remove a container + +`DELETE /containers/(id or name)` + +Remove the container `id` from the filesystem + +**Example request**: + + DELETE /containers/16253994b7c4?v=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **v** – 1/True/true or 0/False/false, Remove the volumes + associated to the container. Default false +- **force** - 1/True/true or 0/False/false, Kill then remove the container. + Default false + +Status Codes: + +- **204** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + +### Copy files or folders from a container + +`POST /containers/(id or name)/copy` + +Copy files or folders of container `id` + +**Example request**: + + POST /containers/4fa6e0f0c678/copy HTTP/1.1 + Content-Type: application/json + + { + "Resource": "test.txt" + } + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/octet-stream + + {{ TAR STREAM }} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +## 2.2 Images + +### List Images + +`GET /images/json` + +**Example request**: + + GET /images/json?all=0 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "RepoTags": [ + "ubuntu:12.04", + "ubuntu:precise", + "ubuntu:latest" + ], + "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", + "Created": 1365714795, + "Size": 131506275, + "VirtualSize": 131506275 + }, + { + "RepoTags": [ + "ubuntu:12.10", + "ubuntu:quantal" + ], + "ParentId": "27cf784147099545", + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Created": 1364102658, + "Size": 24653, + "VirtualSize": 180116135 + } + ] + + +Query Parameters: + +- **all** – 1/True/true or 0/False/false, default false +- **filters** – a json encoded value of the filters (a map[string][]string) to process on the images list. Available filters: + - dangling=true +- **filter** - only return images with the specified name + +### Create an image + +`POST /images/create` + +Create an image, either by pulling it from the registry or by importing it + +**Example request**: + + POST /images/create?fromImage=ubuntu HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "Pulling..."} + {"status": "Pulling", "progress": "1 B/ 100 B", "progressDetail": {"current": 1, "total": 100}} + {"error": "Invalid..."} + ... + + When using this endpoint to pull an image from the registry, the + `X-Registry-Auth` header can be used to include + a base64-encoded AuthConfig object. + +Query Parameters: + +- **fromImage** – name of the image to pull +- **fromSrc** – source to import, - means stdin +- **repo** – repository +- **tag** – tag +- **registry** – the registry to pull from + +Request Headers: + +- **X-Registry-Auth** – base64-encoded AuthConfig object + +Status Codes: + +- **200** – no error +- **500** – server error + + + +### Inspect an image + +`GET /images/(name)/json` + +Return low-level information on the image `name` + +**Example request**: + + GET /images/ubuntu/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Created": "2013-03-23T22:24:18.818426-07:00", + "Container": "3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", + "ContainerConfig": + { + "Hostname": "", + "User": "", + "Memory": 0, + "MemorySwap": 0, + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "PortSpecs": null, + "Tty": true, + "OpenStdin": true, + "StdinOnce": false, + "Env": null, + "Cmd": ["/bin/bash"], + "Dns": null, + "Image": "ubuntu", + "Volumes": null, + "VolumesFrom": "", + "WorkingDir": "" + }, + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Parent": "27cf784147099545", + "Size": 6824592 + } + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Get the history of an image + +`GET /images/(name)/history` + +Return the history of the image `name` + +**Example request**: + + GET /images/ubuntu/history HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "b750fe79269d", + "Created": 1364102658, + "CreatedBy": "/bin/bash" + }, + { + "Id": "27cf78414709", + "Created": 1364068391, + "CreatedBy": "" + } + ] + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Push an image on the registry + +`POST /images/(name)/push` + +Push the image `name` on the registry + +**Example request**: + + POST /images/test/push HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "Pushing..."} + {"status": "Pushing", "progress": "1/? (n/a)", "progressDetail": {"current": 1}}} + {"error": "Invalid..."} + ... + + If you wish to push an image on to a private registry, that image must already have been tagged + into a repository which references that registry host name and port. This repository name should + then be used in the URL. This mirrors the flow of the CLI. + +**Example request**: + + POST /images/registry.acme.com:5000/test/push HTTP/1.1 + + +Query Parameters: + +- **tag** – the tag to associate with the image on the registry, optional + +Request Headers: + +- **X-Registry-Auth** – include a base64-encoded AuthConfig object. + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Tag an image into a repository + +`POST /images/(name)/tag` + +Tag the image `name` into a repository + +**Example request**: + + POST /images/test/tag?repo=myrepo&force=0&tag=v42 HTTP/1.1 + +**Example response**: + + HTTP/1.1 201 Created + +Query Parameters: + +- **repo** – The repository to tag in +- **force** – 1/True/true or 0/False/false, default false +- **tag** - The new tag name + +Status Codes: + +- **201** – no error +- **400** – bad parameter +- **404** – no such image +- **409** – conflict +- **500** – server error + +### Remove an image + +`DELETE /images/(name)` + +Remove the image `name` from the filesystem + +**Example request**: + + DELETE /images/test HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-type: application/json + + [ + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} + ] + +Query Parameters: + +- **force** – 1/True/true or 0/False/false, default false +- **noprune** – 1/True/true or 0/False/false, default false + +Status Codes: + +- **200** – no error +- **404** – no such image +- **409** – conflict +- **500** – server error + +### Search images + +`GET /images/search` + +Search for an image on [Docker Hub](https://hub.docker.com). + +> **Note**: +> The response keys have changed from API v1.6 to reflect the JSON +> sent by the registry server to the docker daemon's request. + +**Example request**: + + GET /images/search?term=sshd HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "wma55/u1210sshd", + "star_count": 0 + }, + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "jdswinbank/sshd", + "star_count": 0 + }, + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "vgauthier/sshd", + "star_count": 0 + } + ... + ] + +Query Parameters: + +- **term** – term to search + +Status Codes: + +- **200** – no error +- **500** – server error + +## 2.3 Misc + +### Build an image from Dockerfile via stdin + +`POST /build` + +Build an image from Dockerfile via stdin + +**Example request**: + + POST /build HTTP/1.1 + + {{ TAR STREAM }} + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"stream": "Step 1..."} + {"stream": "..."} + {"error": "Error...", "errorDetail": {"code": 123, "message": "Error..."}} + + The stream must be a tar archive compressed with one of the + following algorithms: identity (no compression), gzip, bzip2, xz. + + The archive must include a file called `Dockerfile` + at its root. It may include any number of other files, + which will be accessible in the build context (See the [*ADD build + command*](../../reference/builder.md#dockerbuilder)). + +Query Parameters: + +- **t** – repository name (and optionally a tag) to be applied to + the resulting image in case of success +- **remote** – git or HTTP/HTTPS URI build source +- **q** – suppress verbose build output +- **nocache** – do not use the cache when building the image +- **rm** - remove intermediate containers after a successful build (default behavior) +- **forcerm** - always remove intermediate containers (includes rm) + + Request Headers: + +- **Content-type** – should be set to `"application/tar"`. +- **X-Registry-Config** – base64-encoded ConfigFile object + +Status Codes: + +- **200** – no error +- **500** – server error + +### Check auth configuration + +`POST /auth` + +Get the default username and email + +**Example request**: + + POST /auth HTTP/1.1 + Content-Type: application/json + + { + "username":" hannibal", + "password: "xxxx", + "email": "hannibal@a-team.com", + "serveraddress": "https://index.docker.io/v1/" + } + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – no error +- **204** – no error +- **500** – server error + +### Display system-wide information + +`GET /info` + +Display system-wide information + +**Example request**: + + GET /info HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Containers": 11, + "Images": 16, + "Driver": "btrfs", + "ExecutionDriver": "native-0.1", + "KernelVersion": "3.12.0-1-amd64" + "Debug": false, + "NFd": 11, + "NGoroutines": 21, + "NEventsListener": 0, + "InitPath": "/usr/bin/docker", + "IndexServerAddress": ["https://index.docker.io/v1/"], + "MemoryLimit": true, + "SwapLimit": false, + "IPv4Forwarding": true + } + +Status Codes: + +- **200** – no error +- **500** – server error + +### Show the docker version information + +`GET /version` + +Show the docker version information + +**Example request**: + + GET /version HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "ApiVersion": "1.12", + "Version": "0.2.2", + "GitCommit": "5a2a5cc+CHANGES", + "GoVersion": "go1.0.3" + } + +Status Codes: + +- **200** – no error +- **500** – server error + +### Ping the docker server + +`GET /_ping` + +Ping the docker server + +**Example request**: + + GET /_ping HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: text/plain + + OK + +Status Codes: + +- **200** - no error +- **500** - server error + +### Create a new image from a container's changes + +`POST /commit` + +Create a new image from a container's changes + +**Example request**: + + POST /commit?container=44c004db4b17&comment=message&repo=myrepo HTTP/1.1 + Content-Type: application/json + + { + "Hostname": "", + "Domainname": "", + "User": "", + "Memory": 0, + "MemorySwap": 0, + "CpuShares": 512, + "Cpuset": "0,1", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "PortSpecs": null, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "date" + ], + "Volumes": { + "/tmp": {} + }, + "WorkingDir": "", + "NetworkDisabled": false, + "ExposedPorts": { + "22/tcp": {} + } + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + {"Id": "596069db4bf5"} + +Json Parameters: + +- **config** - the container's configuration + +Query Parameters: + +- **container** – source container +- **repo** – repository +- **tag** – tag +- **comment** – commit message +- **author** – author (e.g., "John Hannibal Smith + <[hannibal@a-team.com](mailto:hannibal%40a-team.com)>") + +Status Codes: + +- **201** – no error +- **404** – no such container +- **500** – server error + +### Monitor Docker's events + +`GET /events` + +Get container events from docker, either in real time via streaming, or via +polling (using since). + +Docker containers will report the following events: + + create, destroy, die, export, kill, pause, restart, start, stop, unpause + +and Docker images will report: + + untag, delete + +**Example request**: + + GET /events?since=1374067924 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "create", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067924} + {"status": "start", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067924} + {"status": "stop", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067966} + {"status": "destroy", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067970} + +Query Parameters: + +- **since** – timestamp used for polling +- **until** – timestamp used for polling + +Status Codes: + +- **200** – no error +- **500** – server error + +### Get a tarball containing all images and tags in a repository + +`GET /images/(name)/get` + +Get a tarball containing all images and metadata for the repository +specified by `name`. + +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + GET /images/ubuntu/get + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + Binary data stream + +Status Codes: + +- **200** – no error +- **500** – server error + +### Load a tarball with a set of images and tags into docker + +`POST /images/load` + +Load a set of images and tags into the docker repository. +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + POST /images/load + + Tarball in body + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – no error +- **500** – server error + +### Image tarball format + +An image tarball contains one directory per image layer (named using its long ID), +each containing three files: + +1. `VERSION`: currently `1.0` - the file format version +2. `json`: detailed layer information, similar to `docker inspect layer_id` +3. `layer.tar`: A tarfile containing the filesystem changes in this layer + +The `layer.tar` file will contain `aufs` style `.wh..wh.aufs` files and directories +for storing attribute changes and deletions. + +If the tarball defines a repository, there will also be a `repositories` file at +the root that contains a list of repository and tag names mapped to layer IDs. + +``` +{"hello-world": + {"latest": "565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} +} +``` + +# 3. Going further + +## 3.1 Inside `docker run` + +As an example, the `docker run` command line makes the following API calls: + +- Create the container + +- If the status code is 404, it means the image doesn't exist: + - Try to pull it + - Then retry to create the container + +- Start the container + +- If you are not in detached mode: + - Attach to the container, using logs=1 (to have stdout and + stderr from the container's start) and stream=1 + +- If in detached mode or only stdin is attached: + - Display the container's id + +## 3.2 Hijacking + +In this version of the API, /attach, uses hijacking to transport stdin, +stdout and stderr on the same socket. This might change in the future. + +## 3.3 CORS Requests + +To enable cross origin requests to the remote api add the flag +"--api-enable-cors" when running docker in daemon mode. + + $ docker -d -H="192.168.1.9:2375" --api-enable-cors diff --git a/docs/reference/api/docker_remote_api_v1.15.md b/docs/reference/api/docker_remote_api_v1.15.md new file mode 100644 index 00000000..f6a860a4 --- /dev/null +++ b/docs/reference/api/docker_remote_api_v1.15.md @@ -0,0 +1,1764 @@ + + +# Docker Remote API v1.15 + +## 1. Brief introduction + + - The Remote API has replaced `rcli`. + - The daemon listens on `unix:///var/run/docker.sock` but you can + [Bind Docker to another host/port or a Unix socket](../../quickstart.md#bind-docker-to-another-host-port-or-a-unix-socket). + - The API tends to be REST, but for some complex commands, like `attach` + or `pull`, the HTTP connection is hijacked to transport `STDOUT`, + `STDIN` and `STDERR`. + +# 2. Endpoints + +## 2.1 Containers + +### List containers + +`GET /containers/json` + +List containers + +**Example request**: + + GET /containers/json?all=1&before=8dfafdbc3a40&size=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "8dfafdbc3a40", + "Names":["/boring_feynman"], + "Image": "ubuntu:latest", + "Command": "echo 1", + "Created": 1367854155, + "Status": "Exit 0", + "Ports": [{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "SizeRw": 12288, + "SizeRootFs": 0 + }, + { + "Id": "9cd87474be90", + "Names":["/coolName"], + "Image": "ubuntu:latest", + "Command": "echo 222222", + "Created": 1367854155, + "Status": "Exit 0", + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 + }, + { + "Id": "3176a2479c92", + "Names":["/sleepy_dog"], + "Image": "ubuntu:latest", + "Command": "echo 3333333333333333", + "Created": 1367854154, + "Status": "Exit 0", + "Ports":[], + "SizeRw":12288, + "SizeRootFs":0 + }, + { + "Id": "4cb07b47f9fb", + "Names":["/running_cat"], + "Image": "ubuntu:latest", + "Command": "echo 444444444444444444444444444444444", + "Created": 1367854152, + "Status": "Exit 0", + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 + } + ] + +Query Parameters: + +- **all** – 1/True/true or 0/False/false, Show all containers. + Only running containers are shown by default (i.e., this defaults to false) +- **limit** – Show `limit` last created + containers, include non-running ones. +- **since** – Show only containers created since Id, include + non-running ones. +- **before** – Show only containers created before Id, include + non-running ones. +- **size** – 1/True/true or 0/False/false, Show the containers + sizes +- **filters** - a json encoded value of the filters (a map[string][]string) to process on the containers list. Available filters: + - exited=<int> -- containers with exit code of <int> + - status=(restarting|running|paused|exited) + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **500** – server error + +### Create a container + +`POST /containers/create` + +Create a container + +**Example request**: + + POST /containers/create HTTP/1.1 + Content-Type: application/json + + { + "Hostname": "", + "Domainname": "", + "User": "", + "Memory": 0, + "MemorySwap": 0, + "CpuShares": 512, + "Cpuset": "0,1", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "FOO=bar", + "BAZ=quux" + ], + "Cmd": [ + "date" + ], + "Entrypoint": "", + "Image": "ubuntu", + "Volumes": { + "/tmp": {} + }, + "WorkingDir": "", + "NetworkDisabled": false, + "MacAddress": "12:34:56:78:9a:bc", + "ExposedPorts": { + "22/tcp": {} + }, + "SecurityOpts": [], + "HostConfig": { + "Binds": ["/tmp:/tmp"], + "Links": ["redis3:redis"], + "LxcConf": {"lxc.utsname":"docker"}, + "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] }, + "PublishAllPorts": false, + "Privileged": false, + "Dns": ["8.8.8.8"], + "DnsSearch": [""], + "ExtraHosts": null, + "VolumesFrom": ["parent", "other:ro"], + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"], + "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 }, + "NetworkMode": "bridge", + "Devices": [] + } + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "Id": "f91ddc4b01e079c4481a8340bbbeca4dbd33d6e4a10662e499f8eacbb5bf252b" + "Warnings": [] + } + +Json Parameters: + +- **Hostname** - A string value containing the desired hostname to use for the + container. +- **Domainname** - A string value containing the desired domain name to use + for the container. +- **User** - A string value containing the user to use inside the container. +- **Memory** - Memory limit in bytes. +- **MemorySwap** - Total memory limit (memory + swap); set `-1` to enable unlimited swap. +- **CpuShares** - An integer value containing the CPU Shares for container + (ie. the relative weight vs other containers). + **CpuSet** - String value containing the cgroups Cpuset to use. +- **AttachStdin** - Boolean value, attaches to stdin. +- **AttachStdout** - Boolean value, attaches to stdout. +- **AttachStderr** - Boolean value, attaches to stderr. +- **Tty** - Boolean value, Attach standard streams to a tty, including stdin if it is not closed. +- **OpenStdin** - Boolean value, opens stdin, +- **StdinOnce** - Boolean value, close stdin after the 1 attached client disconnects. +- **Env** - A list of environment variables in the form of `["VAR=value"[,"VAR2=value2"]]` +- **Cmd** - Command to run specified as a string or an array of strings. +- **Entrypoint** - Set the entrypoint for the container a string or an array + of strings +- **Image** - String value containing the image name to use for the container +- **Volumes** – An object mapping mountpoint paths (strings) inside the + container to empty objects. +- **WorkingDir** - A string value containing the working dir for commands to + run in. +- **NetworkDisabled** - Boolean value, when true disables networking for the + container +- **ExposedPorts** - An object mapping ports to an empty object in the form of: + `"ExposedPorts": { "/: {}" }` +- **SecurityOpts**: A list of string values to customize labels for MLS + systems, such as SELinux. +- **HostConfig** + - **Binds** – A list of volume bindings for this container. Each volume + binding is a string of the form `container_path` (to create a new + volume for the container), `host_path:container_path` (to bind-mount + a host path into the container), or `host_path:container_path:ro` + (to make the bind-mount read-only inside the container). + - **Links** - A list of links for the container. Each link entry should be + in the form of "container_name:alias". + - **LxcConf** - LXC specific configurations. These configurations will only + work when using the `lxc` execution driver. + - **PortBindings** - A map of exposed container ports and the host port they + should map to. It should be specified in the form + `{ /: [{ "HostPort": "" }] }` + Take note that `port` is specified as a string and not an integer value. + - **PublishAllPorts** - Allocates a random host port for all of a container's + exposed ports. Specified as a boolean value. + - **Privileged** - Gives the container full access to the host. Specified as + a boolean value. + - **Dns** - A list of dns servers for the container to use. + - **DnsSearch** - A list of DNS search domains + - **ExtraHosts** - A list of hostnames/IP mappings to be added to the + container's `/etc/hosts` file. Specified in the form `["hostname:IP"]`. + - **VolumesFrom** - A list of volumes to inherit from another container. + Specified in the form `[:]` + - **CapAdd** - A list of kernel capabilities to add to the container. + - **Capdrop** - A list of kernel capabilities to drop from the container. + - **RestartPolicy** – The behavior to apply when the container exits. The + value is an object with a `Name` property of either `"always"` to + always restart or `"on-failure"` to restart only when the container + exit code is non-zero. If `on-failure` is used, `MaximumRetryCount` + controls the number of times to retry before giving up. + The default is not to restart. (optional) + An ever increasing delay (double the previous delay, starting at 100mS) + is added before each restart to prevent flooding the server. + - **NetworkMode** - Sets the networking mode for the container. Supported + values are: `bridge`, `host`, `none`, and `container:` + - **Devices** - A list of devices to add to the container specified in the + form + `{ "PathOnHost": "/dev/deviceName", "PathInContainer": "/dev/deviceName", "CgroupPermissions": "mrw"}` + +Query Parameters: + +- **name** – Assign the specified name to the container. Must + match `/?[a-zA-Z0-9_-]+`. + +Status Codes: + +- **201** – no error +- **404** – no such container +- **406** – impossible to attach (container not running) +- **500** – server error + +### Inspect a container + +`GET /containers/(id or name)/json` + +Return low-level information on the container `id` + + +**Example request**: + + GET /containers/4fa6e0f0c678/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Id": "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2", + "Created": "2013-05-07T14:51:42.041847+02:00", + "Path": "date", + "Args": [], + "Config": { + "Hostname": "4fa6e0f0c678", + "User": "", + "Memory": 0, + "MemorySwap": 0, + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "PortSpecs": null, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "date" + ], + "Dns": null, + "Image": "ubuntu", + "Volumes": {}, + "VolumesFrom": "", + "WorkingDir": "" + }, + "State": { + "Running": false, + "Pid": 0, + "ExitCode": 0, + "StartedAt": "2013-05-07T14:51:42.087658+02:01360", + "Ghost": false + }, + "Image": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "NetworkSettings": { + "IpAddress": "", + "IpPrefixLen": 0, + "Gateway": "", + "Bridge": "", + "PortMapping": null + }, + "SysInitPath": "/home/kitty/go/src/github.com/docker/docker/bin/docker", + "ResolvConfPath": "/etc/resolv.conf", + "Volumes": {}, + "HostConfig": { + "Binds": null, + "ContainerIDFile": "", + "LxcConf": [], + "Privileged": false, + "PortBindings": { + "80/tcp": [ + { + "HostIp": "0.0.0.0", + "HostPort": "49153" + } + ] + }, + "Links": ["/name:alias"], + "PublishAllPorts": false, + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"] + } + } + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### List processes running inside a container + +`GET /containers/(id or name)/top` + +List processes running inside the container `id`. On Unix systems this +is done by running the `ps` command. This endpoint is not +supported on Windows. + +**Example request**: + + GET /containers/4fa6e0f0c678/top HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Titles" : [ + "UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD" + ], + "Processes" : [ + [ + "root", "13642", "882", "0", "17:03", "pts/0", "00:00:00", "/bin/bash" + ], + [ + "root", "13735", "13642", "0", "17:06", "pts/0", "00:00:00", "sleep 10" + ] + ] + } + +**Example request**: + + GET /containers/4fa6e0f0c678/top?ps_args=aux HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Titles" : [ + "USER","PID","%CPU","%MEM","VSZ","RSS","TTY","STAT","START","TIME","COMMAND" + ] + "Processes" : [ + [ + "root","13642","0.0","0.1","18172","3184","pts/0","Ss","17:03","0:00","/bin/bash" + ], + [ + "root","13895","0.0","0.0","4348","692","pts/0","S+","17:15","0:00","sleep 10" + ] + ], + } + +Query Parameters: + +- **ps_args** – `ps` arguments to use (e.g., `aux`), defaults to `-ef` + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Get container logs + +`GET /containers/(id or name)/logs` + +Get stdout and stderr logs from the container ``id`` + +**Example request**: + + GET /containers/4fa6e0f0c678/logs?stderr=1&stdout=1×tamps=1&follow=1&tail=10 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + +Query Parameters: + +- **follow** – 1/True/true or 0/False/false, return stream. Default false +- **stdout** – 1/True/true or 0/False/false, show stdout log. Default false +- **stderr** – 1/True/true or 0/False/false, show stderr log. Default false +- **timestamps** – 1/True/true or 0/False/false, print timestamps for + every log line. Default false +- **tail** – Output specified number of lines at the end of logs: `all` or ``. Default all + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Inspect changes on a container's filesystem + +`GET /containers/(id or name)/changes` + +Inspect changes on container `id`'s filesystem + +**Example request**: + + GET /containers/4fa6e0f0c678/changes HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Path": "/dev", + "Kind": 0 + }, + { + "Path": "/dev/kmsg", + "Kind": 1 + }, + { + "Path": "/test", + "Kind": 1 + } + ] + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Export a container + +`GET /containers/(id or name)/export` + +Export the contents of container `id` + +**Example request**: + + GET /containers/4fa6e0f0c678/export HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/octet-stream + + {{ TAR STREAM }} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Resize a container TTY + +`GET /containers/(id or name)/resize?h=&w=` + +Resize the TTY of container `id` + +**Example request**: + + GET /containers/4fa6e0f0c678/resize?h=40&w=80 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Length: 0 + Content-Type: text/plain; charset=utf-8 + +Status Codes: + +- **200** – no error +- **404** – No such container +- **500** – bad file descriptor + +### Start a container + +`POST /containers/(id or name)/start` + +Start the container `id` + +**Example request**: + + POST /containers/e90e34656806/start HTTP/1.1 + Content-Type: application/json + + { + "Binds": ["/tmp:/tmp"], + "Links": ["redis3:redis"], + "LxcConf": {"lxc.utsname":"docker"}, + "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] }, + "PublishAllPorts": false, + "Privileged": false, + "Dns": ["8.8.8.8"], + "DnsSearch": [""], + "VolumesFrom": ["parent", "other:ro"], + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"], + "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 }, + "NetworkMode": "bridge", + "Devices": [] + } + +**Example response**: + + HTTP/1.1 204 No Content + +Json Parameters: + +- **Binds** – A list of volume bindings for this container. Each volume + binding is a string of the form `container_path` (to create a new + volume for the container), `host_path:container_path` (to bind-mount + a host path into the container), or `host_path:container_path:ro` + (to make the bind-mount read-only inside the container). +- **Links** - A list of links for the container. Each link entry should be of + of the form "container_name:alias". +- **LxcConf** - LXC specific configurations. These configurations will only + work when using the `lxc` execution driver. +- **PortBindings** - A map of exposed container ports and the host port they + should map to. It should be specified in the form + `{ /: [{ "HostPort": "" }] }` + Take note that `port` is specified as a string and not an integer value. +- **PublishAllPorts** - Allocates a random host port for all of a container's + exposed ports. Specified as a boolean value. +- **Privileged** - Gives the container full access to the host. Specified as + a boolean value. +- **Dns** - A list of dns servers for the container to use. +- **DnsSearch** - A list of DNS search domains +- **VolumesFrom** - A list of volumes to inherit from another container. + Specified in the form `[:]` +- **CapAdd** - A list of kernel capabilities to add to the container. +- **Capdrop** - A list of kernel capabilities to drop from the container. +- **RestartPolicy** – The behavior to apply when the container exits. The + value is an object with a `Name` property of either `"always"` to + always restart or `"on-failure"` to restart only when the container + exit code is non-zero. If `on-failure` is used, `MaximumRetryCount` + controls the number of times to retry before giving up. + The default is not to restart. (optional) + An ever increasing delay (double the previous delay, starting at 100mS) + is added before each restart to prevent flooding the server. +- **NetworkMode** - Sets the networking mode for the container. Supported + values are: `bridge`, `host`, `none`, and `container:` +- **Devices** - A list of devices to add to the container specified in the + form + `{ "PathOnHost": "/dev/deviceName", "PathInContainer": "/dev/deviceName", "CgroupPermissions": "mrw"}` + +Status Codes: + +- **204** – no error +- **304** – container already started +- **404** – no such container +- **500** – server error + +### Stop a container + +`POST /containers/(id or name)/stop` + +Stop the container `id` + +**Example request**: + + POST /containers/e90e34656806/stop?t=5 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **t** – number of seconds to wait before killing the container + +Status Codes: + +- **204** – no error +- **304** – container already stopped +- **404** – no such container +- **500** – server error + +### Restart a container + +`POST /containers/(id or name)/restart` + +Restart the container `id` + +**Example request**: + + POST /containers/e90e34656806/restart?t=5 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **t** – number of seconds to wait before killing the container + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Kill a container + +`POST /containers/(id or name)/kill` + +Kill the container `id` + +**Example request**: + + POST /containers/e90e34656806/kill HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters + +- **signal** - Signal to send to the container: integer or string like "SIGINT". + When not set, SIGKILL is assumed and the call will waits for the container to exit. + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Pause a container + +`POST /containers/(id or name)/pause` + +Pause the container `id` + +**Example request**: + + POST /containers/e90e34656806/pause HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Unpause a container + +`POST /containers/(id or name)/unpause` + +Unpause the container `id` + +**Example request**: + + POST /containers/e90e34656806/unpause HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Attach to a container + +`POST /containers/(id or name)/attach` + +Attach to the container `id` + +**Example request**: + + POST /containers/16253994b7c4/attach?logs=1&stream=0&stdout=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + + **Stream details**: + + When using the TTY setting is enabled in + [`POST /containers/create`](#create-a-container), + the stream is the raw data from the process PTY and client's stdin. + When the TTY is disabled, then the stream is multiplexed to separate + stdout and stderr. + + The format is a **Header** and a **Payload** (frame). + + **HEADER** + + The header will contain the information on which stream write the + stream (stdout or stderr). It also contain the size of the + associated frame encoded on the last 4 bytes (uint32). + + It is encoded on the first 8 bytes like this: + + header := [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4} + + `STREAM_TYPE` can be: + +- 0: stdin (will be written on stdout) +- 1: stdout +- 2: stderr + + `SIZE1, SIZE2, SIZE3, SIZE4` are the 4 bytes of + the uint32 size encoded as big endian. + + **PAYLOAD** + + The payload is the raw stream. + + **IMPLEMENTATION** + + The simplest way to implement the Attach protocol is the following: + + 1. Read 8 bytes + 2. chose stdout or stderr depending on the first byte + 3. Extract the frame size from the last 4 bytes + 4. Read the extracted size and output it on the correct output + 5. Goto 1 + +### Attach to a container (websocket) + +`GET /containers/(id or name)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + +### Wait a container + +`POST /containers/(id or name)/wait` + +Block until container `id` stops, then returns the exit code + +**Example request**: + + POST /containers/16253994b7c4/wait HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"StatusCode": 0} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Remove a container + +`DELETE /containers/(id or name)` + +Remove the container `id` from the filesystem + +**Example request**: + + DELETE /containers/16253994b7c4?v=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **v** – 1/True/true or 0/False/false, Remove the volumes + associated to the container. Default false +- **force** - 1/True/true or 0/False/false, Kill then remove the container. + Default false + +Status Codes: + +- **204** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + +### Copy files or folders from a container + +`POST /containers/(id or name)/copy` + +Copy files or folders of container `id` + +**Example request**: + + POST /containers/4fa6e0f0c678/copy HTTP/1.1 + Content-Type: application/json + + { + "Resource": "test.txt" + } + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + {{ TAR STREAM }} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +## 2.2 Images + +### List Images + +`GET /images/json` + +**Example request**: + + GET /images/json?all=0 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "RepoTags": [ + "ubuntu:12.04", + "ubuntu:precise", + "ubuntu:latest" + ], + "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", + "Created": 1365714795, + "Size": 131506275, + "VirtualSize": 131506275 + }, + { + "RepoTags": [ + "ubuntu:12.10", + "ubuntu:quantal" + ], + "ParentId": "27cf784147099545", + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Created": 1364102658, + "Size": 24653, + "VirtualSize": 180116135 + } + ] + + +Query Parameters: + +- **all** – 1/True/true or 0/False/false, default false +- **filters** – a json encoded value of the filters (a map[string][]string) to process on the images list. Available filters: + - dangling=true +- **filter** - only return images with the specified name + +### Create an image + +`POST /images/create` + +Create an image, either by pulling it from the registry or by importing it + +**Example request**: + + POST /images/create?fromImage=ubuntu HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "Pulling..."} + {"status": "Pulling", "progress": "1 B/ 100 B", "progressDetail": {"current": 1, "total": 100}} + {"error": "Invalid..."} + ... + + When using this endpoint to pull an image from the registry, the + `X-Registry-Auth` header can be used to include + a base64-encoded AuthConfig object. + +Query Parameters: + +- **fromImage** – name of the image to pull +- **fromSrc** – source to import. The value may be a URL from which the image + can be retrieved or `-` to read the image from the request body. +- **repo** – repository +- **tag** – tag +- **registry** – the registry to pull from + + Request Headers: + +- **X-Registry-Auth** – base64-encoded AuthConfig object + +Status Codes: + +- **200** – no error +- **500** – server error + + + +### Inspect an image + +`GET /images/(name)/json` + +Return low-level information on the image `name` + +**Example request**: + + GET /images/ubuntu/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Created": "2013-03-23T22:24:18.818426-07:00", + "Container": "3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", + "ContainerConfig": + { + "Hostname": "", + "User": "", + "Memory": 0, + "MemorySwap": 0, + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "PortSpecs": null, + "Tty": true, + "OpenStdin": true, + "StdinOnce": false, + "Env": null, + "Cmd": ["/bin/bash"], + "Dns": null, + "Image": "ubuntu", + "Volumes": null, + "VolumesFrom": "", + "WorkingDir": "" + }, + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Parent": "27cf784147099545", + "Size": 6824592 + } + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Get the history of an image + +`GET /images/(name)/history` + +Return the history of the image `name` + +**Example request**: + + GET /images/ubuntu/history HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "b750fe79269d", + "Created": 1364102658, + "CreatedBy": "/bin/bash" + }, + { + "Id": "27cf78414709", + "Created": 1364068391, + "CreatedBy": "" + } + ] + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Push an image on the registry + +`POST /images/(name)/push` + +Push the image `name` on the registry + +**Example request**: + + POST /images/test/push HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "Pushing..."} + {"status": "Pushing", "progress": "1/? (n/a)", "progressDetail": {"current": 1}}} + {"error": "Invalid..."} + ... + + If you wish to push an image on to a private registry, that image must already have been tagged + into a repository which references that registry host name and port. This repository name should + then be used in the URL. This mirrors the flow of the CLI. + +**Example request**: + + POST /images/registry.acme.com:5000/test/push HTTP/1.1 + + +Query Parameters: + +- **tag** – the tag to associate with the image on the registry, optional + +Request Headers: + +- **X-Registry-Auth** – include a base64-encoded AuthConfig + object. + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Tag an image into a repository + +`POST /images/(name)/tag` + +Tag the image `name` into a repository + +**Example request**: + + POST /images/test/tag?repo=myrepo&force=0&tag=v42 HTTP/1.1 + +**Example response**: + + HTTP/1.1 201 Created + +Query Parameters: + +- **repo** – The repository to tag in +- **force** – 1/True/true or 0/False/false, default false +- **tag** - The new tag name + +Status Codes: + +- **201** – no error +- **400** – bad parameter +- **404** – no such image +- **409** – conflict +- **500** – server error + +### Remove an image + +`DELETE /images/(name)` + +Remove the image `name` from the filesystem + +**Example request**: + + DELETE /images/test HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-type: application/json + + [ + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} + ] + +Query Parameters: + +- **force** – 1/True/true or 0/False/false, default false +- **noprune** – 1/True/true or 0/False/false, default false + +Status Codes: + +- **200** – no error +- **404** – no such image +- **409** – conflict +- **500** – server error + +### Search images + +`GET /images/search` + +Search for an image on [Docker Hub](https://hub.docker.com). + +> **Note**: +> The response keys have changed from API v1.6 to reflect the JSON +> sent by the registry server to the docker daemon's request. + +**Example request**: + + GET /images/search?term=sshd HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "wma55/u1210sshd", + "star_count": 0 + }, + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "jdswinbank/sshd", + "star_count": 0 + }, + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "vgauthier/sshd", + "star_count": 0 + } + ... + ] + +Query Parameters: + +- **term** – term to search + +Status Codes: + +- **200** – no error +- **500** – server error + +## 2.3 Misc + +### Build an image from Dockerfile via stdin + +`POST /build` + +Build an image from Dockerfile via stdin + +**Example request**: + + POST /build HTTP/1.1 + + {{ TAR STREAM }} + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"stream": "Step 1..."} + {"stream": "..."} + {"error": "Error...", "errorDetail": {"code": 123, "message": "Error..."}} + + The stream must be a tar archive compressed with one of the + following algorithms: identity (no compression), gzip, bzip2, xz. + + The archive must include a file called `Dockerfile` + at its root. It may include any number of other files, + which will be accessible in the build context (See the [*ADD build + command*](../../reference/builder.md#dockerbuilder)). + +Query Parameters: + +- **t** – repository name (and optionally a tag) to be applied to + the resulting image in case of success +- **remote** – git or HTTP/HTTPS URI build source +- **q** – suppress verbose build output +- **nocache** – do not use the cache when building the image +- **rm** - remove intermediate containers after a successful build (default behavior) +- **forcerm** - always remove intermediate containers (includes rm) + + Request Headers: + +- **Content-type** – should be set to `"application/tar"`. +- **X-Registry-Config** – base64-encoded ConfigFile object + +Status Codes: + +- **200** – no error +- **500** – server error + +### Check auth configuration + +`POST /auth` + +Get the default username and email + +**Example request**: + + POST /auth HTTP/1.1 + Content-Type: application/json + + { + "username":" hannibal", + "password: "xxxx", + "email": "hannibal@a-team.com", + "serveraddress": "https://index.docker.io/v1/" + } + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – no error +- **204** – no error +- **500** – server error + +### Display system-wide information + +`GET /info` + +Display system-wide information + +**Example request**: + + GET /info HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Containers": 11, + "Images": 16, + "Driver": "btrfs", + "ExecutionDriver": "native-0.1", + "KernelVersion": "3.12.0-1-amd64" + "Debug": false, + "NFd": 11, + "NGoroutines": 21, + "NEventsListener": 0, + "InitPath": "/usr/bin/docker", + "IndexServerAddress": ["https://index.docker.io/v1/"], + "MemoryLimit": true, + "SwapLimit": false, + "IPv4Forwarding": true + } + +Status Codes: + +- **200** – no error +- **500** – server error + +### Show the docker version information + +`GET /version` + +Show the docker version information + +**Example request**: + + GET /version HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "ApiVersion": "1.12", + "Version": "0.2.2", + "GitCommit": "5a2a5cc+CHANGES", + "GoVersion": "go1.0.3" + } + +Status Codes: + +- **200** – no error +- **500** – server error + +### Ping the docker server + +`GET /_ping` + +Ping the docker server + +**Example request**: + + GET /_ping HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: text/plain + + OK + +Status Codes: + +- **200** - no error +- **500** - server error + +### Create a new image from a container's changes + +`POST /commit` + +Create a new image from a container's changes + +**Example request**: + + POST /commit?container=44c004db4b17&comment=message&repo=myrepo HTTP/1.1 + Content-Type: application/json + + { + "Hostname": "", + "Domainname": "", + "User": "", + "Memory": 0, + "MemorySwap": 0, + "CpuShares": 512, + "Cpuset": "0,1", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "PortSpecs": null, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "date" + ], + "Volumes": { + "/tmp": {} + }, + "WorkingDir": "", + "NetworkDisabled": false, + "ExposedPorts": { + "22/tcp": {} + } + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + {"Id": "596069db4bf5"} + +Json Parameters: + +- **config** - the container's configuration + +Query Parameters: + +- **container** – source container +- **repo** – repository +- **tag** – tag +- **comment** – commit message +- **author** – author (e.g., "John Hannibal Smith + <[hannibal@a-team.com](mailto:hannibal%40a-team.com)>") + +Status Codes: + +- **201** – no error +- **404** – no such container +- **500** – server error + +### Monitor Docker's events + +`GET /events` + +Get container events from docker, either in real time via streaming, or via +polling (using since). + +Docker containers will report the following events: + + create, destroy, die, export, kill, pause, restart, start, stop, unpause + +and Docker images will report: + + untag, delete + +**Example request**: + + GET /events?since=1374067924 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "create", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067924} + {"status": "start", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067924} + {"status": "stop", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067966} + {"status": "destroy", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067970} + +Query Parameters: + +- **since** – timestamp used for polling +- **until** – timestamp used for polling + +Status Codes: + +- **200** – no error +- **500** – server error + +### Get a tarball containing all images in a repository + +`GET /images/(name)/get` + +Get a tarball containing all images and metadata for the repository specified +by `name`. + +If `name` is a specific name and tag (e.g. ubuntu:latest), then only that image +(and its parents) are returned. If `name` is an image ID, similarly only that +image (and its parents) are returned, but with the exclusion of the +'repositories' file in the tarball, as there were no image names referenced. + +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + GET /images/ubuntu/get + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + Binary data stream + +Status Codes: + +- **200** – no error +- **500** – server error + +### Get a tarball containing all images. + +`GET /images/get` + +Get a tarball containing all images and metadata for one or more repositories. + +For each value of the `names` parameter: if it is a specific name and tag (e.g. +ubuntu:latest), then only that image (and its parents) are returned; if it is +an image ID, similarly only that image (and its parents) are returned and there +would be no names referenced in the 'repositories' file for this image ID. + +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + GET /images/get?names=myname%2Fmyapp%3Alatest&names=busybox + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + Binary data stream + +Status Codes: + +- **200** – no error +- **500** – server error + +### Load a tarball with a set of images and tags into docker + +`POST /images/load` + +Load a set of images and tags into the docker repository. +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + POST /images/load + + Tarball in body + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – no error +- **500** – server error + +### Image tarball format + +An image tarball contains one directory per image layer (named using its long ID), +each containing three files: + +1. `VERSION`: currently `1.0` - the file format version +2. `json`: detailed layer information, similar to `docker inspect layer_id` +3. `layer.tar`: A tarfile containing the filesystem changes in this layer + +The `layer.tar` file will contain `aufs` style `.wh..wh.aufs` files and directories +for storing attribute changes and deletions. + +If the tarball defines a repository, there will also be a `repositories` file at +the root that contains a list of repository and tag names mapped to layer IDs. + +``` +{"hello-world": + {"latest": "565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} +} +``` + +### Exec Create + +`POST /containers/(id or name)/exec` + +Sets up an exec instance in a running container `id` + +**Example request**: + + POST /containers/e90e34656806/exec HTTP/1.1 + Content-Type: application/json + + { + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "Cmd": [ + "date" + ], + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "Id": "f90e34656806" + } + +Json Parameters: + +- **AttachStdin** - Boolean value, attaches to stdin of the exec command. +- **AttachStdout** - Boolean value, attaches to stdout of the exec command. +- **AttachStderr** - Boolean value, attaches to stderr of the exec command. +- **Tty** - Boolean value to allocate a pseudo-TTY +- **Cmd** - Command to run specified as a string or an array of strings. + + +Status Codes: + +- **201** – no error +- **404** – no such container + +### Exec Start + +`POST /exec/(id)/start` + +Starts a previously set up exec instance `id`. If `detach` is true, this API +returns after starting the `exec` command. Otherwise, this API sets up an +interactive session with the `exec` command. + +**Example request**: + + POST /exec/e90e34656806/start HTTP/1.1 + Content-Type: application/json + + { + "Detach": false, + "Tty": false, + } + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + +Json Parameters: + +- **Detach** - Detach from the exec command +- **Tty** - Boolean value to allocate a pseudo-TTY + +Status Codes: + +- **200** – no error +- **404** – no such exec instance + + **Stream details**: + Similar to the stream behavior of `POST /containers/(id or name)/attach` API + +### Exec Resize + +`POST /exec/(id)/resize` + +Resizes the tty session used by the exec command `id`. +This API is valid only if `tty` was specified as part of creating and starting the exec command. + +**Example request**: + + POST /exec/e90e34656806/resize HTTP/1.1 + Content-Type: plain/text + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: plain/text + +Query Parameters: + +- **h** – height of tty session +- **w** – width + +Status Codes: + +- **201** – no error +- **404** – no such exec instance + +# 3. Going further + +## 3.1 Inside `docker run` + +As an example, the `docker run` command line makes the following API calls: + +- Create the container + +- If the status code is 404, it means the image doesn't exist: + - Try to pull it + - Then retry to create the container + +- Start the container + +- If you are not in detached mode: +- Attach to the container, using logs=1 (to have stdout and + stderr from the container's start) and stream=1 + +- If in detached mode or only stdin is attached: +- Display the container's id + +## 3.2 Hijacking + +In this version of the API, /attach, uses hijacking to transport stdin, +stdout and stderr on the same socket. This might change in the future. + +## 3.3 CORS Requests + +To enable cross origin requests to the remote api add the flag +"--api-enable-cors" when running docker in daemon mode. + + $ docker -d -H="192.168.1.9:2375" --api-enable-cors diff --git a/docs/reference/api/docker_remote_api_v1.16.md b/docs/reference/api/docker_remote_api_v1.16.md new file mode 100644 index 00000000..630da701 --- /dev/null +++ b/docs/reference/api/docker_remote_api_v1.16.md @@ -0,0 +1,1834 @@ + + +# Docker Remote API v1.16 + +## 1. Brief introduction + + - The Remote API has replaced `rcli`. + - The daemon listens on `unix:///var/run/docker.sock` but you can + [Bind Docker to another host/port or a Unix socket](../../quickstart.md#bind-docker-to-another-host-port-or-a-unix-socket). + - The API tends to be REST, but for some complex commands, like `attach` + or `pull`, the HTTP connection is hijacked to transport `STDOUT`, + `STDIN` and `STDERR`. + +# 2. Endpoints + +## 2.1 Containers + +### List containers + +`GET /containers/json` + +List containers + +**Example request**: + + GET /containers/json?all=1&before=8dfafdbc3a40&size=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "8dfafdbc3a40", + "Names":["/boring_feynman"], + "Image": "ubuntu:latest", + "Command": "echo 1", + "Created": 1367854155, + "Status": "Exit 0", + "Ports": [{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "SizeRw": 12288, + "SizeRootFs": 0 + }, + { + "Id": "9cd87474be90", + "Names":["/coolName"], + "Image": "ubuntu:latest", + "Command": "echo 222222", + "Created": 1367854155, + "Status": "Exit 0", + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 + }, + { + "Id": "3176a2479c92", + "Names":["/sleep_dog"], + "Image": "ubuntu:latest", + "Command": "echo 3333333333333333", + "Created": 1367854154, + "Status": "Exit 0", + "Ports":[], + "SizeRw":12288, + "SizeRootFs":0 + }, + { + "Id": "4cb07b47f9fb", + "Names":["/running_cat"], + "Image": "ubuntu:latest", + "Command": "echo 444444444444444444444444444444444", + "Created": 1367854152, + "Status": "Exit 0", + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 + } + ] + +Query Parameters: + +- **all** – 1/True/true or 0/False/false, Show all containers. + Only running containers are shown by default (i.e., this defaults to false) +- **limit** – Show `limit` last created + containers, include non-running ones. +- **since** – Show only containers created since Id, include + non-running ones. +- **before** – Show only containers created before Id, include + non-running ones. +- **size** – 1/True/true or 0/False/false, Show the containers + sizes +- **filters** - a json encoded value of the filters (a map[string][]string) to process on the containers list. Available filters: + - exited=<int> -- containers with exit code of <int> + - status=(restarting|running|paused|exited) + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **500** – server error + +### Create a container + +`POST /containers/create` + +Create a container + +**Example request**: + + POST /containers/create HTTP/1.1 + Content-Type: application/json + + { + "Hostname": "", + "Domainname": "", + "User": "", + "Memory": 0, + "MemorySwap": 0, + "CpuShares": 512, + "Cpuset": "0,1", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "FOO=bar", + "BAZ=quux" + ], + "Cmd": [ + "date" + ], + "Entrypoint": "", + "Image": "ubuntu", + "Volumes": { + "/tmp": {} + }, + "WorkingDir": "", + "NetworkDisabled": false, + "MacAddress": "12:34:56:78:9a:bc", + "ExposedPorts": { + "22/tcp": {} + }, + "SecurityOpts": [], + "HostConfig": { + "Binds": ["/tmp:/tmp"], + "Links": ["redis3:redis"], + "LxcConf": {"lxc.utsname":"docker"}, + "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] }, + "PublishAllPorts": false, + "Privileged": false, + "Dns": ["8.8.8.8"], + "DnsSearch": [""], + "ExtraHosts": null, + "VolumesFrom": ["parent", "other:ro"], + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"], + "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 }, + "NetworkMode": "bridge", + "Devices": [] + } + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "Id":"e90e34656806" + "Warnings":[] + } + +Json Parameters: + +- **Hostname** - A string value containing the desired hostname to use for the + container. +- **Domainname** - A string value containing the desired domain name to use + for the container. +- **User** - A string value containing the user to use inside the container. +- **Memory** - Memory limit in bytes. +- **MemorySwap** - Total memory limit (memory + swap); set `-1` to enable unlimited swap. +- **CpuShares** - An integer value containing the CPU Shares for container + (ie. the relative weight vs other containers). + **CpuSet** - String value containing the cgroups Cpuset to use. +- **AttachStdin** - Boolean value, attaches to stdin. +- **AttachStdout** - Boolean value, attaches to stdout. +- **AttachStderr** - Boolean value, attaches to stderr. +- **Tty** - Boolean value, Attach standard streams to a tty, including stdin if it is not closed. +- **OpenStdin** - Boolean value, opens stdin, +- **StdinOnce** - Boolean value, close stdin after the 1 attached client disconnects. +- **Env** - A list of environment variables in the form of `["VAR=value"[,"VAR2=value2"]]` +- **Cmd** - Command to run specified as a string or an array of strings. +- **Entrypoint** - Set the entrypoint for the container a string or an array + of strings +- **Image** - String value containing the image name to use for the container +- **Volumes** – An object mapping mountpoint paths (strings) inside the + container to empty objects. +- **WorkingDir** - A string value containing the working dir for commands to + run in. +- **NetworkDisabled** - Boolean value, when true disables networking for the + container +- **ExposedPorts** - An object mapping ports to an empty object in the form of: + `"ExposedPorts": { "/: {}" }` +- **SecurityOpts**: A list of string values to customize labels for MLS + systems, such as SELinux. +- **HostConfig** + - **Binds** – A list of volume bindings for this container. Each volume + binding is a string of the form `container_path` (to create a new + volume for the container), `host_path:container_path` (to bind-mount + a host path into the container), or `host_path:container_path:ro` + (to make the bind-mount read-only inside the container). + - **Links** - A list of links for the container. Each link entry should be + in the form of "container_name:alias". + - **LxcConf** - LXC specific configurations. These configurations will only + work when using the `lxc` execution driver. + - **PortBindings** - A map of exposed container ports and the host port they + should map to. It should be specified in the form + `{ /: [{ "HostPort": "" }] }` + Take note that `port` is specified as a string and not an integer value. + - **PublishAllPorts** - Allocates a random host port for all of a container's + exposed ports. Specified as a boolean value. + - **Privileged** - Gives the container full access to the host. Specified as + a boolean value. + - **Dns** - A list of dns servers for the container to use. + - **DnsSearch** - A list of DNS search domains + - **ExtraHosts** - A list of hostnames/IP mappings to be added to the + container's `/etc/hosts` file. Specified in the form `["hostname:IP"]`. + - **VolumesFrom** - A list of volumes to inherit from another container. + Specified in the form `[:]` + - **CapAdd** - A list of kernel capabilities to add to the container. + - **Capdrop** - A list of kernel capabilities to drop from the container. + - **RestartPolicy** – The behavior to apply when the container exits. The + value is an object with a `Name` property of either `"always"` to + always restart or `"on-failure"` to restart only when the container + exit code is non-zero. If `on-failure` is used, `MaximumRetryCount` + controls the number of times to retry before giving up. + The default is not to restart. (optional) + An ever increasing delay (double the previous delay, starting at 100mS) + is added before each restart to prevent flooding the server. + - **NetworkMode** - Sets the networking mode for the container. Supported + values are: `bridge`, `host`, `none`, and `container:` + - **Devices** - A list of devices to add to the container specified in the + form + `{ "PathOnHost": "/dev/deviceName", "PathInContainer": "/dev/deviceName", "CgroupPermissions": "mrw"}` + +Query Parameters: + +- **name** – Assign the specified name to the container. Must + match `/?[a-zA-Z0-9_-]+`. + +Status Codes: + +- **201** – no error +- **404** – no such container +- **406** – impossible to attach (container not running) +- **500** – server error + +### Inspect a container + +`GET /containers/(id or name)/json` + +Return low-level information on the container `id` + + +**Example request**: + + GET /containers/4fa6e0f0c678/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Id": "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2", + "Created": "2013-05-07T14:51:42.041847+02:00", + "Path": "date", + "Args": [], + "Config": { + "Hostname": "4fa6e0f0c678", + "User": "", + "Memory": 0, + "MemorySwap": 0, + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "PortSpecs": null, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "date" + ], + "Dns": null, + "Image": "ubuntu", + "Volumes": {}, + "VolumesFrom": "", + "WorkingDir": "" + }, + "State": { + "Running": false, + "Pid": 0, + "ExitCode": 0, + "StartedAt": "2013-05-07T14:51:42.087658+02:01360", + "Ghost": false + }, + "Image": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "NetworkSettings": { + "IpAddress": "", + "IpPrefixLen": 0, + "Gateway": "", + "Bridge": "", + "PortMapping": null + }, + "SysInitPath": "/home/kitty/go/src/github.com/docker/docker/bin/docker", + "ResolvConfPath": "/etc/resolv.conf", + "Volumes": {}, + "HostConfig": { + "Binds": null, + "ContainerIDFile": "", + "LxcConf": [], + "Privileged": false, + "PortBindings": { + "80/tcp": [ + { + "HostIp": "0.0.0.0", + "HostPort": "49153" + } + ] + }, + "Links": ["/name:alias"], + "PublishAllPorts": false, + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"] + } + } + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### List processes running inside a container + +`GET /containers/(id or name)/top` + +List processes running inside the container `id`. On Unix systems this +is done by running the `ps` command. This endpoint is not +supported on Windows. + +**Example request**: + + GET /containers/4fa6e0f0c678/top HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Titles" : [ + "UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD" + ], + "Processes" : [ + [ + "root", "13642", "882", "0", "17:03", "pts/0", "00:00:00", "/bin/bash" + ], + [ + "root", "13735", "13642", "0", "17:06", "pts/0", "00:00:00", "sleep 10" + ] + ] + } + +**Example request**: + + GET /containers/4fa6e0f0c678/top?ps_args=aux HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Titles" : [ + "USER","PID","%CPU","%MEM","VSZ","RSS","TTY","STAT","START","TIME","COMMAND" + ] + "Processes" : [ + [ + "root","13642","0.0","0.1","18172","3184","pts/0","Ss","17:03","0:00","/bin/bash" + ], + [ + "root","13895","0.0","0.0","4348","692","pts/0","S+","17:15","0:00","sleep 10" + ] + ], + } + +Query Parameters: + +- **ps_args** – `ps` arguments to use (e.g., `aux`), defaults to `-ef` + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Get container logs + +`GET /containers/(id or name)/logs` + +Get stdout and stderr logs from the container ``id`` + +**Example request**: + + GET /containers/4fa6e0f0c678/logs?stderr=1&stdout=1×tamps=1&follow=1&tail=10 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + +Query Parameters: + +- **follow** – 1/True/true or 0/False/false, return stream. Default false +- **stdout** – 1/True/true or 0/False/false, show stdout log. Default false +- **stderr** – 1/True/true or 0/False/false, show stderr log. Default false +- **timestamps** – 1/True/true or 0/False/false, print timestamps for + every log line. Default false +- **tail** – Output specified number of lines at the end of logs: `all` or ``. Default all + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Inspect changes on a container's filesystem + +`GET /containers/(id or name)/changes` + +Inspect changes on container `id`'s filesystem + +**Example request**: + + GET /containers/4fa6e0f0c678/changes HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Path": "/dev", + "Kind": 0 + }, + { + "Path": "/dev/kmsg", + "Kind": 1 + }, + { + "Path": "/test", + "Kind": 1 + } + ] + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Export a container + +`GET /containers/(id or name)/export` + +Export the contents of container `id` + +**Example request**: + + GET /containers/4fa6e0f0c678/export HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/octet-stream + + {{ TAR STREAM }} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Resize a container TTY + +`POST /containers/(id or name)/resize?h=&w=` + +Resize the TTY for container with `id`. The container must be restarted for the resize to take effect. + +**Example request**: + + POST /containers/4fa6e0f0c678/resize?h=40&w=80 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Length: 0 + Content-Type: text/plain; charset=utf-8 + +Status Codes: + +- **200** – no error +- **404** – No such container +- **500** – Cannot resize container + +### Start a container + +`POST /containers/(id or name)/start` + +Start the container `id` + +> **Note**: +> For backwards compatibility, this endpoint accepts a `HostConfig` as JSON-encoded request body. +> See [create a container](#create-a-container) for details. + +**Example request**: + + POST /containers/e90e34656806/start HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **304** – container already started +- **404** – no such container +- **500** – server error + +### Stop a container + +`POST /containers/(id or name)/stop` + +Stop the container `id` + +**Example request**: + + POST /containers/e90e34656806/stop?t=5 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **t** – number of seconds to wait before killing the container + +Status Codes: + +- **204** – no error +- **304** – container already stopped +- **404** – no such container +- **500** – server error + +### Restart a container + +`POST /containers/(id or name)/restart` + +Restart the container `id` + +**Example request**: + + POST /containers/e90e34656806/restart?t=5 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **t** – number of seconds to wait before killing the container + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Kill a container + +`POST /containers/(id or name)/kill` + +Kill the container `id` + +**Example request**: + + POST /containers/e90e34656806/kill HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters + +- **signal** - Signal to send to the container: integer or string like "SIGINT". + When not set, SIGKILL is assumed and the call will waits for the container to exit. + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Pause a container + +`POST /containers/(id or name)/pause` + +Pause the container `id` + +**Example request**: + + POST /containers/e90e34656806/pause HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Unpause a container + +`POST /containers/(id or name)/unpause` + +Unpause the container `id` + +**Example request**: + + POST /containers/e90e34656806/unpause HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Attach to a container + +`POST /containers/(id or name)/attach` + +Attach to the container `id` + +**Example request**: + + POST /containers/16253994b7c4/attach?logs=1&stream=0&stdout=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + + **Stream details**: + + When using the TTY setting is enabled in + [`POST /containers/create` + ](#create-a-container), + the stream is the raw data from the process PTY and client's stdin. + When the TTY is disabled, then the stream is multiplexed to separate + stdout and stderr. + + The format is a **Header** and a **Payload** (frame). + + **HEADER** + + The header will contain the information on which stream write the + stream (stdout or stderr). It also contain the size of the + associated frame encoded on the last 4 bytes (uint32). + + It is encoded on the first 8 bytes like this: + + header := [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4} + + `STREAM_TYPE` can be: + +- 0: stdin (will be written on stdout) +- 1: stdout +- 2: stderr + + `SIZE1, SIZE2, SIZE3, SIZE4` are the 4 bytes of + the uint32 size encoded as big endian. + + **PAYLOAD** + + The payload is the raw stream. + + **IMPLEMENTATION** + + The simplest way to implement the Attach protocol is the following: + + 1. Read 8 bytes + 2. chose stdout or stderr depending on the first byte + 3. Extract the frame size from the last 4 bytes + 4. Read the extracted size and output it on the correct output + 5. Goto 1 + +### Attach to a container (websocket) + +`GET /containers/(id or name)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + +### Wait a container + +`POST /containers/(id or name)/wait` + +Block until container `id` stops, then returns the exit code + +**Example request**: + + POST /containers/16253994b7c4/wait HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"StatusCode": 0} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Remove a container + +`DELETE /containers/(id or name)` + +Remove the container `id` from the filesystem + +**Example request**: + + DELETE /containers/16253994b7c4?v=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **v** – 1/True/true or 0/False/false, Remove the volumes + associated to the container. Default false +- **force** - 1/True/true or 0/False/false, Kill then remove the container. + Default false + +Status Codes: + +- **204** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + +### Copy files or folders from a container + +`POST /containers/(id or name)/copy` + +Copy files or folders of container `id` + +**Example request**: + + POST /containers/4fa6e0f0c678/copy HTTP/1.1 + Content-Type: application/json + + { + "Resource": "test.txt" + } + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + {{ TAR STREAM }} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +## 2.2 Images + +### List Images + +`GET /images/json` + +**Example request**: + + GET /images/json?all=0 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "RepoTags": [ + "ubuntu:12.04", + "ubuntu:precise", + "ubuntu:latest" + ], + "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", + "Created": 1365714795, + "Size": 131506275, + "VirtualSize": 131506275 + }, + { + "RepoTags": [ + "ubuntu:12.10", + "ubuntu:quantal" + ], + "ParentId": "27cf784147099545", + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Created": 1364102658, + "Size": 24653, + "VirtualSize": 180116135 + } + ] + + +Query Parameters: + +- **all** – 1/True/true or 0/False/false, default false +- **filters** – a json encoded value of the filters (a map[string][]string) to process on the images list. Available filters: + - dangling=true +- **filter** - only return images with the specified name + +### Create an image + +`POST /images/create` + +Create an image, either by pulling it from the registry or by importing it + +**Example request**: + + POST /images/create?fromImage=ubuntu HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "Pulling..."} + {"status": "Pulling", "progress": "1 B/ 100 B", "progressDetail": {"current": 1, "total": 100}} + {"error": "Invalid..."} + ... + + When using this endpoint to pull an image from the registry, the + `X-Registry-Auth` header can be used to include + a base64-encoded AuthConfig object. + +Query Parameters: + +- **fromImage** – name of the image to pull +- **fromSrc** – source to import. The value may be a URL from which the image + can be retrieved or `-` to read the image from the request body. +- **repo** – repository +- **tag** – tag +- **registry** – the registry to pull from + + Request Headers: + +- **X-Registry-Auth** – base64-encoded AuthConfig object + +Status Codes: + +- **200** – no error +- **500** – server error + + + +### Inspect an image + +`GET /images/(name)/json` + +Return low-level information on the image `name` + +**Example request**: + + GET /images/ubuntu/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Created": "2013-03-23T22:24:18.818426-07:00", + "Container": "3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", + "ContainerConfig": + { + "Hostname": "", + "User": "", + "Memory": 0, + "MemorySwap": 0, + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "PortSpecs": null, + "Tty": true, + "OpenStdin": true, + "StdinOnce": false, + "Env": null, + "Cmd": ["/bin/bash"], + "Dns": null, + "Image": "ubuntu", + "Volumes": null, + "VolumesFrom": "", + "WorkingDir": "" + }, + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Parent": "27cf784147099545", + "Size": 6824592 + } + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Get the history of an image + +`GET /images/(name)/history` + +Return the history of the image `name` + +**Example request**: + + GET /images/ubuntu/history HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "b750fe79269d", + "Created": 1364102658, + "CreatedBy": "/bin/bash" + }, + { + "Id": "27cf78414709", + "Created": 1364068391, + "CreatedBy": "" + } + ] + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Push an image on the registry + +`POST /images/(name)/push` + +Push the image `name` on the registry + +**Example request**: + + POST /images/test/push HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "Pushing..."} + {"status": "Pushing", "progress": "1/? (n/a)", "progressDetail": {"current": 1}}} + {"error": "Invalid..."} + ... + + If you wish to push an image on to a private registry, that image must already have been tagged + into a repository which references that registry host name and port. This repository name should + then be used in the URL. This mirrors the flow of the CLI. + +**Example request**: + + POST /images/registry.acme.com:5000/test/push HTTP/1.1 + + +Query Parameters: + +- **tag** – the tag to associate with the image on the registry, optional + +Request Headers: + +- **X-Registry-Auth** – include a base64-encoded AuthConfig + object. + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Tag an image into a repository + +`POST /images/(name)/tag` + +Tag the image `name` into a repository + +**Example request**: + + POST /images/test/tag?repo=myrepo&force=0&tag=v42 HTTP/1.1 + +**Example response**: + + HTTP/1.1 201 Created + +Query Parameters: + +- **repo** – The repository to tag in +- **force** – 1/True/true or 0/False/false, default false +- **tag** - The new tag name + +Status Codes: + +- **201** – no error +- **400** – bad parameter +- **404** – no such image +- **409** – conflict +- **500** – server error + +### Remove an image + +`DELETE /images/(name)` + +Remove the image `name` from the filesystem + +**Example request**: + + DELETE /images/test HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-type: application/json + + [ + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} + ] + +Query Parameters: + +- **force** – 1/True/true or 0/False/false, default false +- **noprune** – 1/True/true or 0/False/false, default false + +Status Codes: + +- **200** – no error +- **404** – no such image +- **409** – conflict +- **500** – server error + +### Search images + +`GET /images/search` + +Search for an image on [Docker Hub](https://hub.docker.com). + +> **Note**: +> The response keys have changed from API v1.6 to reflect the JSON +> sent by the registry server to the docker daemon's request. + +**Example request**: + + GET /images/search?term=sshd HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "wma55/u1210sshd", + "star_count": 0 + }, + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "jdswinbank/sshd", + "star_count": 0 + }, + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "vgauthier/sshd", + "star_count": 0 + } + ... + ] + +Query Parameters: + +- **term** – term to search + +Status Codes: + +- **200** – no error +- **500** – server error + +## 2.3 Misc + +### Build an image from Dockerfile via stdin + +`POST /build` + +Build an image from Dockerfile via stdin + +**Example request**: + + POST /build HTTP/1.1 + + {{ TAR STREAM }} + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"stream": "Step 1..."} + {"stream": "..."} + {"error": "Error...", "errorDetail": {"code": 123, "message": "Error..."}} + + The stream must be a tar archive compressed with one of the + following algorithms: identity (no compression), gzip, bzip2, xz. + + The archive must include a file called `Dockerfile` + at its root. It may include any number of other files, + which will be accessible in the build context (See the [*ADD build + command*](../../reference/builder.md#dockerbuilder)). + +Query Parameters: + +- **t** – repository name (and optionally a tag) to be applied to + the resulting image in case of success +- **remote** – git or HTTP/HTTPS URI build source +- **q** – suppress verbose build output +- **nocache** – do not use the cache when building the image +- **pull** - attempt to pull the image even if an older image exists locally +- **rm** - remove intermediate containers after a successful build (default behavior) +- **forcerm** - always remove intermediate containers (includes rm) + + Request Headers: + +- **Content-type** – should be set to `"application/tar"`. +- **X-Registry-Config** – base64-encoded ConfigFile object + +Status Codes: + +- **200** – no error +- **500** – server error + +### Check auth configuration + +`POST /auth` + +Get the default username and email + +**Example request**: + + POST /auth HTTP/1.1 + Content-Type: application/json + + { + "username":" hannibal", + "password: "xxxx", + "email": "hannibal@a-team.com", + "serveraddress": "https://index.docker.io/v1/" + } + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – no error +- **204** – no error +- **500** – server error + +### Display system-wide information + +`GET /info` + +Display system-wide information + +**Example request**: + + GET /info HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Containers":11, + "Images":16, + "Driver":"btrfs", + "DriverStatus": [[""]], + "ExecutionDriver":"native-0.1", + "KernelVersion":"3.12.0-1-amd64" + "NCPU":1, + "MemTotal":2099236864, + "Name":"prod-server-42", + "ID":"7TRN:IPZB:QYBB:VPBQ:UMPP:KARE:6ZNR:XE6T:7EWV:PKF4:ZOJD:TPYS", + "Debug":false, + "NFd": 11, + "NGoroutines":21, + "NEventsListener":0, + "InitPath":"/usr/bin/docker", + "InitSha1":"", + "IndexServerAddress":["https://index.docker.io/v1/"], + "MemoryLimit":true, + "SwapLimit":false, + "IPv4Forwarding":true, + "Labels":["storage=ssd"], + "DockerRootDir": "/var/lib/docker", + "OperatingSystem": "Boot2Docker", + } + +Status Codes: + +- **200** – no error +- **500** – server error + +### Show the docker version information + +`GET /version` + +Show the docker version information + +**Example request**: + + GET /version HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "ApiVersion": "1.12", + "Version": "0.2.2", + "GitCommit": "5a2a5cc+CHANGES", + "GoVersion": "go1.0.3" + } + +Status Codes: + +- **200** – no error +- **500** – server error + +### Ping the docker server + +`GET /_ping` + +Ping the docker server + +**Example request**: + + GET /_ping HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: text/plain + + OK + +Status Codes: + +- **200** - no error +- **500** - server error + +### Create a new image from a container's changes + +`POST /commit` + +Create a new image from a container's changes + +**Example request**: + + POST /commit?container=44c004db4b17&comment=message&repo=myrepo HTTP/1.1 + Content-Type: application/json + + { + "Hostname": "", + "Domainname": "", + "User": "", + "Memory": 0, + "MemorySwap": 0, + "CpuShares": 512, + "Cpuset": "0,1", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "PortSpecs": null, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "date" + ], + "Volumes": { + "/tmp": {} + }, + "WorkingDir": "", + "NetworkDisabled": false, + "ExposedPorts": { + "22/tcp": {} + } + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + {"Id": "596069db4bf5"} + +Json Parameters: + +- **config** - the container's configuration + +Query Parameters: + +- **container** – source container +- **repo** – repository +- **tag** – tag +- **comment** – commit message +- **author** – author (e.g., "John Hannibal Smith + <[hannibal@a-team.com](mailto:hannibal%40a-team.com)>") + +Status Codes: + +- **201** – no error +- **404** – no such container +- **500** – server error + +### Monitor Docker's events + +`GET /events` + +Get container events from docker, either in real time via streaming, or via +polling (using since). + +Docker containers will report the following events: + + create, destroy, die, export, kill, pause, restart, start, stop, unpause + +and Docker images will report: + + untag, delete + +**Example request**: + + GET /events?since=1374067924 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "create", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067924} + {"status": "start", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067924} + {"status": "stop", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067966} + {"status": "destroy", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067970} + +Query Parameters: + +- **since** – timestamp used for polling +- **until** – timestamp used for polling +- **filters** – a json encoded value of the filters (a map[string][]string) to process on the event list. Available filters: + - event=<string> -- event to filter + - image=<string> -- image to filter + - container=<string> -- container to filter + +Status Codes: + +- **200** – no error +- **500** – server error + +### Get a tarball containing all images in a repository + +`GET /images/(name)/get` + +Get a tarball containing all images and metadata for the repository specified +by `name`. + +If `name` is a specific name and tag (e.g. ubuntu:latest), then only that image +(and its parents) are returned. If `name` is an image ID, similarly only that +image (and its parents) are returned, but with the exclusion of the +'repositories' file in the tarball, as there were no image names referenced. + +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + GET /images/ubuntu/get + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + Binary data stream + +Status Codes: + +- **200** – no error +- **500** – server error + +### Get a tarball containing all images. + +`GET /images/get` + +Get a tarball containing all images and metadata for one or more repositories. + +For each value of the `names` parameter: if it is a specific name and tag (e.g. +ubuntu:latest), then only that image (and its parents) are returned; if it is +an image ID, similarly only that image (and its parents) are returned and there +would be no names referenced in the 'repositories' file for this image ID. + +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + GET /images/get?names=myname%2Fmyapp%3Alatest&names=busybox + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + Binary data stream + +Status Codes: + +- **200** – no error +- **500** – server error + +### Load a tarball with a set of images and tags into docker + +`POST /images/load` + +Load a set of images and tags into the docker repository. +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + POST /images/load + + Tarball in body + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – no error +- **500** – server error + +### Image tarball format + +An image tarball contains one directory per image layer (named using its long ID), +each containing three files: + +1. `VERSION`: currently `1.0` - the file format version +2. `json`: detailed layer information, similar to `docker inspect layer_id` +3. `layer.tar`: A tarfile containing the filesystem changes in this layer + +The `layer.tar` file will contain `aufs` style `.wh..wh.aufs` files and directories +for storing attribute changes and deletions. + +If the tarball defines a repository, there will also be a `repositories` file at +the root that contains a list of repository and tag names mapped to layer IDs. + +``` +{"hello-world": + {"latest": "565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} +} +``` + +### Exec Create + +`POST /containers/(id or name)/exec` + +Sets up an exec instance in a running container `id` + +**Example request**: + + POST /containers/e90e34656806/exec HTTP/1.1 + Content-Type: application/json + + { + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "Cmd": [ + "date" + ], + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "Id": "f90e34656806" + } + +Json Parameters: + +- **AttachStdin** - Boolean value, attaches to stdin of the exec command. +- **AttachStdout** - Boolean value, attaches to stdout of the exec command. +- **AttachStderr** - Boolean value, attaches to stderr of the exec command. +- **Tty** - Boolean value to allocate a pseudo-TTY +- **Cmd** - Command to run specified as a string or an array of strings. + + +Status Codes: + +- **201** – no error +- **404** – no such container + +### Exec Start + +`POST /exec/(id)/start` + +Starts a previously set up exec instance `id`. If `detach` is true, this API +returns after starting the `exec` command. Otherwise, this API sets up an +interactive session with the `exec` command. + +**Example request**: + + POST /exec/e90e34656806/start HTTP/1.1 + Content-Type: application/json + + { + "Detach": false, + "Tty": false, + } + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + +Json Parameters: + +- **Detach** - Detach from the exec command +- **Tty** - Boolean value to allocate a pseudo-TTY + +Status Codes: + +- **200** – no error +- **404** – no such exec instance + + **Stream details**: + Similar to the stream behavior of `POST /containers/(id or name)/attach` API + +### Exec Resize + +`POST /exec/(id)/resize` + +Resizes the tty session used by the exec command `id`. +This API is valid only if `tty` was specified as part of creating and starting the exec command. + +**Example request**: + + POST /exec/e90e34656806/resize HTTP/1.1 + Content-Type: plain/text + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: plain/text + +Query Parameters: + +- **h** – height of tty session +- **w** – width + +Status Codes: + +- **201** – no error +- **404** – no such exec instance + +### Exec Inspect + +`GET /exec/(id)/json` + +Return low-level information about the exec command `id`. + +**Example request**: + + GET /exec/11fb006128e8ceb3942e7c58d77750f24210e35f879dd204ac975c184b820b39/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: plain/text + + { + "ID" : "11fb006128e8ceb3942e7c58d77750f24210e35f879dd204ac975c184b820b39", + "Running" : false, + "ExitCode" : 2, + "ProcessConfig" : { + "privileged" : false, + "user" : "", + "tty" : false, + "entrypoint" : "sh", + "arguments" : [ + "-c", + "exit 2" + ] + }, + "OpenStdin" : false, + "OpenStderr" : false, + "OpenStdout" : false, + "Container" : { + "State" : { + "Running" : true, + "Paused" : false, + "Restarting" : false, + "OOMKilled" : false, + "Pid" : 3650, + "ExitCode" : 0, + "Error" : "", + "StartedAt" : "2014-11-17T22:26:03.717657531Z", + "FinishedAt" : "0001-01-01T00:00:00Z" + }, + "ID" : "8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c", + "Created" : "2014-11-17T22:26:03.626304998Z", + "Path" : "date", + "Args" : [], + "Config" : { + "Hostname" : "8f177a186b97", + "Domainname" : "", + "User" : "", + "Memory" : 0, + "MemorySwap" : 0, + "CpuShares" : 0, + "Cpuset" : "", + "AttachStdin" : false, + "AttachStdout" : false, + "AttachStderr" : false, + "PortSpecs" : null, + "ExposedPorts" : null, + "Tty" : false, + "OpenStdin" : false, + "StdinOnce" : false, + "Env" : [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ], + "Cmd" : [ + "date" + ], + "Image" : "ubuntu", + "Volumes" : null, + "WorkingDir" : "", + "Entrypoint" : null, + "NetworkDisabled" : false, + "MacAddress" : "", + "OnBuild" : null, + "SecurityOpt" : null + }, + "Image" : "5506de2b643be1e6febbf3b8a240760c6843244c41e12aa2f60ccbb7153d17f5", + "NetworkSettings" : { + "IPAddress" : "172.17.0.2", + "IPPrefixLen" : 16, + "MacAddress" : "02:42:ac:11:00:02", + "Gateway" : "172.17.42.1", + "Bridge" : "docker0", + "PortMapping" : null, + "Ports" : {} + }, + "ResolvConfPath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/resolv.conf", + "HostnamePath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/hostname", + "HostsPath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/hosts", + "Name" : "/test", + "Driver" : "aufs", + "ExecDriver" : "native-0.2", + "MountLabel" : "", + "ProcessLabel" : "", + "AppArmorProfile" : "", + "RestartCount" : 0, + "Volumes" : {}, + "VolumesRW" : {} + } + } + +Status Codes: + +- **200** – no error +- **404** – no such exec instance +- **500** - server error + +# 3. Going further + +## 3.1 Inside `docker run` + +As an example, the `docker run` command line makes the following API calls: + +- Create the container + +- If the status code is 404, it means the image doesn't exist: + - Try to pull it + - Then retry to create the container + +- Start the container + +- If you are not in detached mode: +- Attach to the container, using logs=1 (to have stdout and + stderr from the container's start) and stream=1 + +- If in detached mode or only stdin is attached: +- Display the container's id + +## 3.2 Hijacking + +In this version of the API, /attach, uses hijacking to transport stdin, +stdout and stderr on the same socket. This might change in the future. + +## 3.3 CORS Requests + +To enable cross origin requests to the remote api add the flag +"--api-enable-cors" when running docker in daemon mode. + + $ docker -d -H="192.168.1.9:2375" --api-enable-cors diff --git a/docs/reference/api/docker_remote_api_v1.17.md b/docs/reference/api/docker_remote_api_v1.17.md new file mode 100644 index 00000000..9baa758c --- /dev/null +++ b/docs/reference/api/docker_remote_api_v1.17.md @@ -0,0 +1,2008 @@ + + +# Docker Remote API v1.17 + +## 1. Brief introduction + + - The Remote API has replaced `rcli`. + - The daemon listens on `unix:///var/run/docker.sock` but you can + [Bind Docker to another host/port or a Unix socket](../../quickstart.md#bind-docker-to-another-host-port-or-a-unix-socket). + - The API tends to be REST, but for some complex commands, like `attach` + or `pull`, the HTTP connection is hijacked to transport `STDOUT`, + `STDIN` and `STDERR`. + +# 2. Endpoints + +## 2.1 Containers + +### List containers + +`GET /containers/json` + +List containers + +**Example request**: + + GET /containers/json?all=1&before=8dfafdbc3a40&size=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "8dfafdbc3a40", + "Names":["/boring_feynman"], + "Image": "ubuntu:latest", + "Command": "echo 1", + "Created": 1367854155, + "Status": "Exit 0", + "Ports": [{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "SizeRw": 12288, + "SizeRootFs": 0 + }, + { + "Id": "9cd87474be90", + "Names":["/coolName"], + "Image": "ubuntu:latest", + "Command": "echo 222222", + "Created": 1367854155, + "Status": "Exit 0", + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 + }, + { + "Id": "3176a2479c92", + "Names":["/sleepy_dog"], + "Image": "ubuntu:latest", + "Command": "echo 3333333333333333", + "Created": 1367854154, + "Status": "Exit 0", + "Ports":[], + "SizeRw":12288, + "SizeRootFs":0 + }, + { + "Id": "4cb07b47f9fb", + "Names":["/running_cat"], + "Image": "ubuntu:latest", + "Command": "echo 444444444444444444444444444444444", + "Created": 1367854152, + "Status": "Exit 0", + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 + } + ] + +Query Parameters: + +- **all** – 1/True/true or 0/False/false, Show all containers. + Only running containers are shown by default (i.e., this defaults to false) +- **limit** – Show `limit` last created + containers, include non-running ones. +- **since** – Show only containers created since Id, include + non-running ones. +- **before** – Show only containers created before Id, include + non-running ones. +- **size** – 1/True/true or 0/False/false, Show the containers + sizes +- **filters** - a json encoded value of the filters (a map[string][]string) to process on the containers list. Available filters: + - exited=<int> -- containers with exit code of <int> + - status=(restarting|running|paused|exited) + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **500** – server error + +### Create a container + +`POST /containers/create` + +Create a container + +**Example request**: + + POST /containers/create HTTP/1.1 + Content-Type: application/json + + { + "Hostname": "", + "Domainname": "", + "User": "", + "Memory": 0, + "MemorySwap": 0, + "CpuShares": 512, + "Cpuset": "0,1", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "FOO=bar", + "BAZ=quux" + ], + "Cmd": [ + "date" + ], + "Entrypoint": "", + "Image": "ubuntu", + "Volumes": { + "/tmp": {} + }, + "WorkingDir": "", + "NetworkDisabled": false, + "MacAddress": "12:34:56:78:9a:bc", + "ExposedPorts": { + "22/tcp": {} + }, + "HostConfig": { + "Binds": ["/tmp:/tmp"], + "Links": ["redis3:redis"], + "LxcConf": {"lxc.utsname":"docker"}, + "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] }, + "PublishAllPorts": false, + "Privileged": false, + "ReadonlyRootfs": false, + "Dns": ["8.8.8.8"], + "DnsSearch": [""], + "ExtraHosts": null, + "VolumesFrom": ["parent", "other:ro"], + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"], + "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 }, + "NetworkMode": "bridge", + "Devices": [], + "SecurityOpt": [] + } + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "Id":"e90e34656806", + "Warnings":[] + } + +Json Parameters: + +- **Hostname** - A string value containing the desired hostname to use for the + container. +- **Domainname** - A string value containing the desired domain name to use + for the container. +- **User** - A string value containing the user to use inside the container. +- **Memory** - Memory limit in bytes. +- **MemorySwap** - Total memory limit (memory + swap); set `-1` to enable unlimited swap. + You must use this with `memory` and make the swap value larger than `memory`. +- **CpuShares** - An integer value containing the CPU Shares for container + (ie. the relative weight vs other containers). + **CpuSet** - String value containing the cgroups Cpuset to use. +- **AttachStdin** - Boolean value, attaches to stdin. +- **AttachStdout** - Boolean value, attaches to stdout. +- **AttachStderr** - Boolean value, attaches to stderr. +- **Tty** - Boolean value, Attach standard streams to a tty, including stdin if it is not closed. +- **OpenStdin** - Boolean value, opens stdin, +- **StdinOnce** - Boolean value, close stdin after the 1 attached client disconnects. +- **Env** - A list of environment variables in the form of `["VAR=value"[,"VAR2=value2"]]` +- **Cmd** - Command to run specified as a string or an array of strings. +- **Entrypoint** - Set the entrypoint for the container a string or an array + of strings +- **Image** - String value containing the image name to use for the container +- **Volumes** – An object mapping mountpoint paths (strings) inside the + container to empty objects. +- **WorkingDir** - A string value containing the working dir for commands to + run in. +- **NetworkDisabled** - Boolean value, when true disables networking for the + container +- **ExposedPorts** - An object mapping ports to an empty object in the form of: + `"ExposedPorts": { "/: {}" }` +- **HostConfig** + - **Binds** – A list of volume bindings for this container. Each volume + binding is a string of the form `container_path` (to create a new + volume for the container), `host_path:container_path` (to bind-mount + a host path into the container), or `host_path:container_path:ro` + (to make the bind-mount read-only inside the container). + - **Links** - A list of links for the container. Each link entry should be + in the form of "container_name:alias". + - **LxcConf** - LXC specific configurations. These configurations will only + work when using the `lxc` execution driver. + - **PortBindings** - A map of exposed container ports and the host port they + should map to. It should be specified in the form + `{ /: [{ "HostPort": "" }] }` + Take note that `port` is specified as a string and not an integer value. + - **PublishAllPorts** - Allocates a random host port for all of a container's + exposed ports. Specified as a boolean value. + - **Privileged** - Gives the container full access to the host. Specified as + a boolean value. + - **ReadonlyRootfs** - Mount the container's root filesystem as read only. + Specified as a boolean value. + - **Dns** - A list of dns servers for the container to use. + - **DnsSearch** - A list of DNS search domains + - **ExtraHosts** - A list of hostnames/IP mappings to be added to the + container's `/etc/hosts` file. Specified in the form `["hostname:IP"]`. + - **VolumesFrom** - A list of volumes to inherit from another container. + Specified in the form `[:]` + - **CapAdd** - A list of kernel capabilities to add to the container. + - **Capdrop** - A list of kernel capabilities to drop from the container. + - **RestartPolicy** – The behavior to apply when the container exits. The + value is an object with a `Name` property of either `"always"` to + always restart or `"on-failure"` to restart only when the container + exit code is non-zero. If `on-failure` is used, `MaximumRetryCount` + controls the number of times to retry before giving up. + The default is not to restart. (optional) + An ever increasing delay (double the previous delay, starting at 100mS) + is added before each restart to prevent flooding the server. + - **NetworkMode** - Sets the networking mode for the container. Supported + values are: `bridge`, `host`, `none`, and `container:` + - **Devices** - A list of devices to add to the container specified in the + form + `{ "PathOnHost": "/dev/deviceName", "PathInContainer": "/dev/deviceName", "CgroupPermissions": "mrw"}` + - **SecurityOpt**: A list of string values to customize labels for MLS + systems, such as SELinux. + +Query Parameters: + +- **name** – Assign the specified name to the container. Must + match `/?[a-zA-Z0-9_-]+`. + +Status Codes: + +- **201** – no error +- **404** – no such container +- **406** – impossible to attach (container not running) +- **500** – server error + +### Inspect a container + +`GET /containers/(id or name)/json` + +Return low-level information on the container `id` + + +**Example request**: + + GET /containers/4fa6e0f0c678/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "AppArmorProfile": "", + "Args": [ + "-c", + "exit 9" + ], + "Config": { + "AttachStderr": true, + "AttachStdin": false, + "AttachStdout": true, + "Cmd": [ + "/bin/sh", + "-c", + "exit 9" + ], + "CpuShares": 0, + "Cpuset": "", + "Domainname": "", + "Entrypoint": null, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "ExposedPorts": null, + "Hostname": "ba033ac44011", + "Image": "ubuntu", + "MacAddress": "", + "Memory": 0, + "MemorySwap": 0, + "NetworkDisabled": false, + "OnBuild": null, + "OpenStdin": false, + "PortSpecs": null, + "StdinOnce": false, + "Tty": false, + "User": "", + "Volumes": null, + "WorkingDir": "" + }, + "Created": "2015-01-06T15:47:31.485331387Z", + "Driver": "devicemapper", + "ExecDriver": "native-0.2", + "ExecIDs": null, + "HostConfig": { + "Binds": null, + "CapAdd": null, + "CapDrop": null, + "ContainerIDFile": "", + "Devices": [], + "Dns": null, + "DnsSearch": null, + "ExtraHosts": null, + "IpcMode": "", + "Links": null, + "LxcConf": [], + "NetworkMode": "bridge", + "PortBindings": {}, + "Privileged": false, + "ReadonlyRootfs": false, + "PublishAllPorts": false, + "RestartPolicy": { + "MaximumRetryCount": 2, + "Name": "on-failure" + }, + "SecurityOpt": null, + "VolumesFrom": null + }, + "HostnamePath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/hostname", + "HostsPath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/hosts", + "Id": "ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39", + "Image": "04c5d3b7b0656168630d3ba35d8889bd0e9caafcaeb3004d2bfbc47e7c5d35d2", + "MountLabel": "", + "Name": "/boring_euclid", + "NetworkSettings": { + "Bridge": "", + "Gateway": "", + "IPAddress": "", + "IPPrefixLen": 0, + "MacAddress": "", + "PortMapping": null, + "Ports": null + }, + "Path": "/bin/sh", + "ProcessLabel": "", + "ResolvConfPath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/resolv.conf", + "RestartCount": 1, + "State": { + "Error": "", + "ExitCode": 9, + "FinishedAt": "2015-01-06T15:47:32.080254511Z", + "OOMKilled": false, + "Paused": false, + "Pid": 0, + "Restarting": false, + "Running": false, + "StartedAt": "2015-01-06T15:47:32.072697474Z" + }, + "Volumes": {}, + "VolumesRW": {} + } + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### List processes running inside a container + +`GET /containers/(id or name)/top` + +List processes running inside the container `id`. On Unix systems this +is done by running the `ps` command. This endpoint is not +supported on Windows. + +**Example request**: + + GET /containers/4fa6e0f0c678/top HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Titles" : [ + "UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD" + ], + "Processes" : [ + [ + "root", "13642", "882", "0", "17:03", "pts/0", "00:00:00", "/bin/bash" + ], + [ + "root", "13735", "13642", "0", "17:06", "pts/0", "00:00:00", "sleep 10" + ] + ] + } + +**Example request**: + + GET /containers/4fa6e0f0c678/top?ps_args=aux HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Titles" : [ + "USER","PID","%CPU","%MEM","VSZ","RSS","TTY","STAT","START","TIME","COMMAND" + ] + "Processes" : [ + [ + "root","13642","0.0","0.1","18172","3184","pts/0","Ss","17:03","0:00","/bin/bash" + ], + [ + "root","13895","0.0","0.0","4348","692","pts/0","S+","17:15","0:00","sleep 10" + ] + ], + } + +Query Parameters: + +- **ps_args** – `ps` arguments to use (e.g., `aux`), defaults to `-ef` + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Get container logs + +`GET /containers/(id or name)/logs` + +Get stdout and stderr logs from the container ``id`` + +**Example request**: + + GET /containers/4fa6e0f0c678/logs?stderr=1&stdout=1×tamps=1&follow=1&tail=10 HTTP/1.1 + +**Example response**: + + HTTP/1.1 101 UPGRADED + Content-Type: application/vnd.docker.raw-stream + Connection: Upgrade + Upgrade: tcp + + {{ STREAM }} + +Query Parameters: + +- **follow** – 1/True/true or 0/False/false, return stream. Default false +- **stdout** – 1/True/true or 0/False/false, show stdout log. Default false +- **stderr** – 1/True/true or 0/False/false, show stderr log. Default false +- **timestamps** – 1/True/true or 0/False/false, print timestamps for + every log line. Default false +- **tail** – Output specified number of lines at the end of logs: `all` or ``. Default all + +Status Codes: + +- **101** – no error, hints proxy about hijacking +- **200** – no error, no upgrade header found +- **404** – no such container +- **500** – server error + +### Inspect changes on a container's filesystem + +`GET /containers/(id or name)/changes` + +Inspect changes on container `id`'s filesystem + +**Example request**: + + GET /containers/4fa6e0f0c678/changes HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Path": "/dev", + "Kind": 0 + }, + { + "Path": "/dev/kmsg", + "Kind": 1 + }, + { + "Path": "/test", + "Kind": 1 + } + ] + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Export a container + +`GET /containers/(id or name)/export` + +Export the contents of container `id` + +**Example request**: + + GET /containers/4fa6e0f0c678/export HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/octet-stream + + {{ TAR STREAM }} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Get container stats based on resource usage + +`GET /containers/(id or name)/stats` + +This endpoint returns a live stream of a container's resource usage statistics. + +**Example request**: + + GET /containers/redis1/stats HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "read" : "2015-01-08T22:57:31.547920715Z", + "network" : { + "rx_dropped" : 0, + "rx_bytes" : 648, + "rx_errors" : 0, + "tx_packets" : 8, + "tx_dropped" : 0, + "rx_packets" : 8, + "tx_errors" : 0, + "tx_bytes" : 648 + }, + "memory_stats" : { + "stats" : { + "total_pgmajfault" : 0, + "cache" : 0, + "mapped_file" : 0, + "total_inactive_file" : 0, + "pgpgout" : 414, + "rss" : 6537216, + "total_mapped_file" : 0, + "writeback" : 0, + "unevictable" : 0, + "pgpgin" : 477, + "total_unevictable" : 0, + "pgmajfault" : 0, + "total_rss" : 6537216, + "total_rss_huge" : 6291456, + "total_writeback" : 0, + "total_inactive_anon" : 0, + "rss_huge" : 6291456, + "hierarchical_memory_limit" : 67108864, + "total_pgfault" : 964, + "total_active_file" : 0, + "active_anon" : 6537216, + "total_active_anon" : 6537216, + "total_pgpgout" : 414, + "total_cache" : 0, + "inactive_anon" : 0, + "active_file" : 0, + "pgfault" : 964, + "inactive_file" : 0, + "total_pgpgin" : 477 + }, + "max_usage" : 6651904, + "usage" : 6537216, + "failcnt" : 0, + "limit" : 67108864 + }, + "blkio_stats" : {}, + "cpu_stats" : { + "cpu_usage" : { + "percpu_usage" : [ + 16970827, + 1839451, + 7107380, + 10571290 + ], + "usage_in_usermode" : 10000000, + "total_usage" : 36488948, + "usage_in_kernelmode" : 20000000 + }, + "system_cpu_usage" : 20091722000000000, + "throttling_data" : {} + } + } + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Resize a container TTY + +`POST /containers/(id or name)/resize?h=&w=` + +Resize the TTY for container with `id`. The container must be restarted for the resize to take effect. + +**Example request**: + + POST /containers/4fa6e0f0c678/resize?h=40&w=80 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Length: 0 + Content-Type: text/plain; charset=utf-8 + +Status Codes: + +- **200** – no error +- **404** – No such container +- **500** – Cannot resize container + +### Start a container + +`POST /containers/(id or name)/start` + +Start the container `id` + +> **Note**: +> For backwards compatibility, this endpoint accepts a `HostConfig` as JSON-encoded request body. +> See [create a container](#create-a-container) for details. + +**Example request**: + + POST /containers/e90e34656806/start HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **304** – container already started +- **404** – no such container +- **500** – server error + +### Stop a container + +`POST /containers/(id or name)/stop` + +Stop the container `id` + +**Example request**: + + POST /containers/e90e34656806/stop?t=5 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **t** – number of seconds to wait before killing the container + +Status Codes: + +- **204** – no error +- **304** – container already stopped +- **404** – no such container +- **500** – server error + +### Restart a container + +`POST /containers/(id or name)/restart` + +Restart the container `id` + +**Example request**: + + POST /containers/e90e34656806/restart?t=5 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **t** – number of seconds to wait before killing the container + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Kill a container + +`POST /containers/(id or name)/kill` + +Kill the container `id` + +**Example request**: + + POST /containers/e90e34656806/kill HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters + +- **signal** - Signal to send to the container: integer or string like "SIGINT". + When not set, SIGKILL is assumed and the call will waits for the container to exit. + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Rename a container + +`POST /containers/(id or name)/rename` + +Rename the container `id` to a `new_name` + +**Example request**: + + POST /containers/e90e34656806/rename?name=new_name HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **name** – new name for the container + +Status Codes: + +- **204** – no error +- **404** – no such container +- **409** - conflict name already assigned +- **500** – server error + +### Pause a container + +`POST /containers/(id or name)/pause` + +Pause the container `id` + +**Example request**: + + POST /containers/e90e34656806/pause HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Unpause a container + +`POST /containers/(id or name)/unpause` + +Unpause the container `id` + +**Example request**: + + POST /containers/e90e34656806/unpause HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Attach to a container + +`POST /containers/(id or name)/attach` + +Attach to the container `id` + +**Example request**: + + POST /containers/16253994b7c4/attach?logs=1&stream=0&stdout=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 101 UPGRADED + Content-Type: application/vnd.docker.raw-stream + Connection: Upgrade + Upgrade: tcp + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **101** – no error, hints proxy about hijacking +- **200** – no error, no upgrade header found +- **400** – bad parameter +- **404** – no such container +- **500** – server error + + **Stream details**: + + When using the TTY setting is enabled in + [`POST /containers/create` + ](#create-a-container), + the stream is the raw data from the process PTY and client's stdin. + When the TTY is disabled, then the stream is multiplexed to separate + stdout and stderr. + + The format is a **Header** and a **Payload** (frame). + + **HEADER** + + The header will contain the information on which stream write the + stream (stdout or stderr). It also contain the size of the + associated frame encoded on the last 4 bytes (uint32). + + It is encoded on the first 8 bytes like this: + + header := [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4} + + `STREAM_TYPE` can be: + +- 0: stdin (will be written on stdout) +- 1: stdout +- 2: stderr + + `SIZE1, SIZE2, SIZE3, SIZE4` are the 4 bytes of + the uint32 size encoded as big endian. + + **PAYLOAD** + + The payload is the raw stream. + + **IMPLEMENTATION** + + The simplest way to implement the Attach protocol is the following: + + 1. Read 8 bytes + 2. chose stdout or stderr depending on the first byte + 3. Extract the frame size from the last 4 bytes + 4. Read the extracted size and output it on the correct output + 5. Goto 1 + +### Attach to a container (websocket) + +`GET /containers/(id or name)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + +### Wait a container + +`POST /containers/(id or name)/wait` + +Block until container `id` stops, then returns the exit code + +**Example request**: + + POST /containers/16253994b7c4/wait HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"StatusCode": 0} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Remove a container + +`DELETE /containers/(id or name)` + +Remove the container `id` from the filesystem + +**Example request**: + + DELETE /containers/16253994b7c4?v=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **v** – 1/True/true or 0/False/false, Remove the volumes + associated to the container. Default false +- **force** - 1/True/true or 0/False/false, Kill then remove the container. + Default false + +Status Codes: + +- **204** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + +### Copy files or folders from a container + +`POST /containers/(id or name)/copy` + +Copy files or folders of container `id` + +**Example request**: + + POST /containers/4fa6e0f0c678/copy HTTP/1.1 + Content-Type: application/json + + { + "Resource": "test.txt" + } + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + {{ TAR STREAM }} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +## 2.2 Images + +### List Images + +`GET /images/json` + +**Example request**: + + GET /images/json?all=0 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "RepoTags": [ + "ubuntu:12.04", + "ubuntu:precise", + "ubuntu:latest" + ], + "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", + "Created": 1365714795, + "Size": 131506275, + "VirtualSize": 131506275 + }, + { + "RepoTags": [ + "ubuntu:12.10", + "ubuntu:quantal" + ], + "ParentId": "27cf784147099545", + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Created": 1364102658, + "Size": 24653, + "VirtualSize": 180116135 + } + ] + + +Query Parameters: + +- **all** – 1/True/true or 0/False/false, default false +- **filters** – a json encoded value of the filters (a map[string][]string) to process on the images list. Available filters: + - dangling=true +- **filter** - only return images with the specified name + +### Build image from a Dockerfile + +`POST /build` + +Build an image from a Dockerfile + +**Example request**: + + POST /build HTTP/1.1 + + {{ TAR STREAM }} + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"stream": "Step 1..."} + {"stream": "..."} + {"error": "Error...", "errorDetail": {"code": 123, "message": "Error..."}} + +The input stream must be a tar archive compressed with one of the +following algorithms: identity (no compression), gzip, bzip2, xz. + +The archive must include a build instructions file, typically called +`Dockerfile` at the root of the archive. The `dockerfile` parameter may be +used to specify a different build instructions file by having its value be +the path to the alternate build instructions file to use. + +The archive may include any number of other files, +which will be accessible in the build context (See the [*ADD build +command*](../../reference/builder.md#dockerbuilder)). + +Query Parameters: + +- **dockerfile** - path within the build context to the Dockerfile +- **t** – repository name (and optionally a tag) to be applied to + the resulting image in case of success +- **remote** – git or HTTP/HTTPS URI build source +- **q** – suppress verbose build output +- **nocache** – do not use the cache when building the image +- **pull** - attempt to pull the image even if an older image exists locally +- **rm** - remove intermediate containers after a successful build (default behavior) +- **forcerm** - always remove intermediate containers (includes rm) + + Request Headers: + +- **Content-type** – should be set to `"application/tar"`. +- **X-Registry-Config** – base64-encoded ConfigFile object + +Status Codes: + +- **200** – no error +- **500** – server error + +### Create an image + +`POST /images/create` + +Create an image, either by pulling it from the registry or by importing it + +**Example request**: + + POST /images/create?fromImage=ubuntu HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "Pulling..."} + {"status": "Pulling", "progress": "1 B/ 100 B", "progressDetail": {"current": 1, "total": 100}} + {"error": "Invalid..."} + ... + + When using this endpoint to pull an image from the registry, the + `X-Registry-Auth` header can be used to include + a base64-encoded AuthConfig object. + +Query Parameters: + +- **fromImage** – name of the image to pull +- **fromSrc** – source to import. The value may be a URL from which the image + can be retrieved or `-` to read the image from the request body. +- **repo** – repository +- **tag** – tag +- **registry** – the registry to pull from + + Request Headers: + +- **X-Registry-Auth** – base64-encoded AuthConfig object + +Status Codes: + +- **200** – no error +- **500** – server error + + + +### Inspect an image + +`GET /images/(name)/json` + +Return low-level information on the image `name` + +**Example request**: + + GET /images/ubuntu/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Created": "2013-03-23T22:24:18.818426-07:00", + "Container": "3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", + "ContainerConfig": + { + "Hostname": "", + "User": "", + "Memory": 0, + "MemorySwap": 0, + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "PortSpecs": null, + "Tty": true, + "OpenStdin": true, + "StdinOnce": false, + "Env": null, + "Cmd": ["/bin/bash"], + "Dns": null, + "Image": "ubuntu", + "Volumes": null, + "VolumesFrom": "", + "WorkingDir": "" + }, + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Parent": "27cf784147099545", + "Size": 6824592 + } + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Get the history of an image + +`GET /images/(name)/history` + +Return the history of the image `name` + +**Example request**: + + GET /images/ubuntu/history HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "b750fe79269d", + "Created": 1364102658, + "CreatedBy": "/bin/bash" + }, + { + "Id": "27cf78414709", + "Created": 1364068391, + "CreatedBy": "" + } + ] + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Push an image on the registry + +`POST /images/(name)/push` + +Push the image `name` on the registry + +**Example request**: + + POST /images/test/push HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "Pushing..."} + {"status": "Pushing", "progress": "1/? (n/a)", "progressDetail": {"current": 1}}} + {"error": "Invalid..."} + ... + + If you wish to push an image on to a private registry, that image must already have been tagged + into a repository which references that registry host name and port. This repository name should + then be used in the URL. This mirrors the flow of the CLI. + +**Example request**: + + POST /images/registry.acme.com:5000/test/push HTTP/1.1 + + +Query Parameters: + +- **tag** – the tag to associate with the image on the registry, optional + +Request Headers: + +- **X-Registry-Auth** – include a base64-encoded AuthConfig + object. + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Tag an image into a repository + +`POST /images/(name)/tag` + +Tag the image `name` into a repository + +**Example request**: + + POST /images/test/tag?repo=myrepo&force=0&tag=v42 HTTP/1.1 + +**Example response**: + + HTTP/1.1 201 Created + +Query Parameters: + +- **repo** – The repository to tag in +- **force** – 1/True/true or 0/False/false, default false +- **tag** - The new tag name + +Status Codes: + +- **201** – no error +- **400** – bad parameter +- **404** – no such image +- **409** – conflict +- **500** – server error + +### Remove an image + +`DELETE /images/(name)` + +Remove the image `name` from the filesystem + +**Example request**: + + DELETE /images/test HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-type: application/json + + [ + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} + ] + +Query Parameters: + +- **force** – 1/True/true or 0/False/false, default false +- **noprune** – 1/True/true or 0/False/false, default false + +Status Codes: + +- **200** – no error +- **404** – no such image +- **409** – conflict +- **500** – server error + +### Search images + +`GET /images/search` + +Search for an image on [Docker Hub](https://hub.docker.com). + +> **Note**: +> The response keys have changed from API v1.6 to reflect the JSON +> sent by the registry server to the docker daemon's request. + +**Example request**: + + GET /images/search?term=sshd HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "wma55/u1210sshd", + "star_count": 0 + }, + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "jdswinbank/sshd", + "star_count": 0 + }, + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "vgauthier/sshd", + "star_count": 0 + } + ... + ] + +Query Parameters: + +- **term** – term to search + +Status Codes: + +- **200** – no error +- **500** – server error + +## 2.3 Misc + +### Check auth configuration + +`POST /auth` + +Get the default username and email + +**Example request**: + + POST /auth HTTP/1.1 + Content-Type: application/json + + { + "username":" hannibal", + "password: "xxxx", + "email": "hannibal@a-team.com", + "serveraddress": "https://index.docker.io/v1/" + } + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – no error +- **204** – no error +- **500** – server error + +### Display system-wide information + +`GET /info` + +Display system-wide information + +**Example request**: + + GET /info HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Containers":11, + "Images":16, + "Driver":"btrfs", + "DriverStatus": [[""]], + "ExecutionDriver":"native-0.1", + "KernelVersion":"3.12.0-1-amd64" + "NCPU":1, + "MemTotal":2099236864, + "Name":"prod-server-42", + "ID":"7TRN:IPZB:QYBB:VPBQ:UMPP:KARE:6ZNR:XE6T:7EWV:PKF4:ZOJD:TPYS", + "Debug":false, + "NFd": 11, + "NGoroutines":21, + "NEventsListener":0, + "InitPath":"/usr/bin/docker", + "InitSha1":"", + "IndexServerAddress":["https://index.docker.io/v1/"], + "MemoryLimit":true, + "SwapLimit":false, + "IPv4Forwarding":true, + "Labels":["storage=ssd"], + "DockerRootDir": "/var/lib/docker", + "OperatingSystem": "Boot2Docker", + } + +Status Codes: + +- **200** – no error +- **500** – server error + +### Show the docker version information + +`GET /version` + +Show the docker version information + +**Example request**: + + GET /version HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "ApiVersion": "1.12", + "Version": "0.2.2", + "GitCommit": "5a2a5cc+CHANGES", + "GoVersion": "go1.0.3" + } + +Status Codes: + +- **200** – no error +- **500** – server error + +### Ping the docker server + +`GET /_ping` + +Ping the docker server + +**Example request**: + + GET /_ping HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: text/plain + + OK + +Status Codes: + +- **200** - no error +- **500** - server error + +### Create a new image from a container's changes + +`POST /commit` + +Create a new image from a container's changes + +**Example request**: + + POST /commit?container=44c004db4b17&comment=message&repo=myrepo HTTP/1.1 + Content-Type: application/json + + { + "Hostname": "", + "Domainname": "", + "User": "", + "Memory": 0, + "MemorySwap": 0, + "CpuShares": 512, + "Cpuset": "0,1", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "PortSpecs": null, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "date" + ], + "Volumes": { + "/tmp": {} + }, + "WorkingDir": "", + "NetworkDisabled": false, + "ExposedPorts": { + "22/tcp": {} + } + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + {"Id": "596069db4bf5"} + +Json Parameters: + +- **config** - the container's configuration + +Query Parameters: + +- **container** – source container +- **repo** – repository +- **tag** – tag +- **comment** – commit message +- **author** – author (e.g., "John Hannibal Smith + <[hannibal@a-team.com](mailto:hannibal%40a-team.com)>") + +Status Codes: + +- **201** – no error +- **404** – no such container +- **500** – server error + +### Monitor Docker's events + +`GET /events` + +Get container events from docker, either in real time via streaming, or via +polling (using since). + +Docker containers will report the following events: + + create, destroy, die, exec_create, exec_start, export, kill, oom, pause, restart, start, stop, unpause + +and Docker images will report: + + untag, delete + +**Example request**: + + GET /events?since=1374067924 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "create", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067924} + {"status": "start", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067924} + {"status": "stop", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067966} + {"status": "destroy", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067970} + +Query Parameters: + +- **since** – timestamp used for polling +- **until** – timestamp used for polling +- **filters** – a json encoded value of the filters (a map[string][]string) to process on the event list. Available filters: + - event=<string> -- event to filter + - image=<string> -- image to filter + - container=<string> -- container to filter + +Status Codes: + +- **200** – no error +- **500** – server error + +### Get a tarball containing all images in a repository + +`GET /images/(name)/get` + +Get a tarball containing all images and metadata for the repository specified +by `name`. + +If `name` is a specific name and tag (e.g. ubuntu:latest), then only that image +(and its parents) are returned. If `name` is an image ID, similarly only that +image (and its parents) are returned, but with the exclusion of the +'repositories' file in the tarball, as there were no image names referenced. + +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + GET /images/ubuntu/get + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + Binary data stream + +Status Codes: + +- **200** – no error +- **500** – server error + +### Get a tarball containing all images. + +`GET /images/get` + +Get a tarball containing all images and metadata for one or more repositories. + +For each value of the `names` parameter: if it is a specific name and tag (e.g. +ubuntu:latest), then only that image (and its parents) are returned; if it is +an image ID, similarly only that image (and its parents) are returned and there +would be no names referenced in the 'repositories' file for this image ID. + +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + GET /images/get?names=myname%2Fmyapp%3Alatest&names=busybox + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + Binary data stream + +Status Codes: + +- **200** – no error +- **500** – server error + +### Load a tarball with a set of images and tags into docker + +`POST /images/load` + +Load a set of images and tags into the docker repository. +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + POST /images/load + + Tarball in body + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – no error +- **500** – server error + +### Image tarball format + +An image tarball contains one directory per image layer (named using its long ID), +each containing three files: + +1. `VERSION`: currently `1.0` - the file format version +2. `json`: detailed layer information, similar to `docker inspect layer_id` +3. `layer.tar`: A tarfile containing the filesystem changes in this layer + +The `layer.tar` file will contain `aufs` style `.wh..wh.aufs` files and directories +for storing attribute changes and deletions. + +If the tarball defines a repository, there will also be a `repositories` file at +the root that contains a list of repository and tag names mapped to layer IDs. + +``` +{"hello-world": + {"latest": "565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} +} +``` + +### Exec Create + +`POST /containers/(id or name)/exec` + +Sets up an exec instance in a running container `id` + +**Example request**: + + POST /containers/e90e34656806/exec HTTP/1.1 + Content-Type: application/json + + { + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "Cmd": [ + "date" + ], + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "Id": "f90e34656806" + } + +Json Parameters: + +- **AttachStdin** - Boolean value, attaches to stdin of the exec command. +- **AttachStdout** - Boolean value, attaches to stdout of the exec command. +- **AttachStderr** - Boolean value, attaches to stderr of the exec command. +- **Tty** - Boolean value to allocate a pseudo-TTY +- **Cmd** - Command to run specified as a string or an array of strings. + + +Status Codes: + +- **201** – no error +- **404** – no such container + +### Exec Start + +`POST /exec/(id)/start` + +Starts a previously set up exec instance `id`. If `detach` is true, this API +returns after starting the `exec` command. Otherwise, this API sets up an +interactive session with the `exec` command. + +**Example request**: + + POST /exec/e90e34656806/start HTTP/1.1 + Content-Type: application/json + + { + "Detach": false, + "Tty": false, + } + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + +Json Parameters: + +- **Detach** - Detach from the exec command +- **Tty** - Boolean value to allocate a pseudo-TTY + +Status Codes: + +- **200** – no error +- **404** – no such exec instance + + **Stream details**: + Similar to the stream behavior of `POST /containers/(id or name)/attach` API + +### Exec Resize + +`POST /exec/(id)/resize` + +Resizes the tty session used by the exec command `id`. +This API is valid only if `tty` was specified as part of creating and starting the exec command. + +**Example request**: + + POST /exec/e90e34656806/resize HTTP/1.1 + Content-Type: text/plain + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: text/plain + +Query Parameters: + +- **h** – height of tty session +- **w** – width + +Status Codes: + +- **201** – no error +- **404** – no such exec instance + +### Exec Inspect + +`GET /exec/(id)/json` + +Return low-level information about the exec command `id`. + +**Example request**: + + GET /exec/11fb006128e8ceb3942e7c58d77750f24210e35f879dd204ac975c184b820b39/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: plain/text + + { + "ID" : "11fb006128e8ceb3942e7c58d77750f24210e35f879dd204ac975c184b820b39", + "Running" : false, + "ExitCode" : 2, + "ProcessConfig" : { + "privileged" : false, + "user" : "", + "tty" : false, + "entrypoint" : "sh", + "arguments" : [ + "-c", + "exit 2" + ] + }, + "OpenStdin" : false, + "OpenStderr" : false, + "OpenStdout" : false, + "Container" : { + "State" : { + "Running" : true, + "Paused" : false, + "Restarting" : false, + "OOMKilled" : false, + "Pid" : 3650, + "ExitCode" : 0, + "Error" : "", + "StartedAt" : "2014-11-17T22:26:03.717657531Z", + "FinishedAt" : "0001-01-01T00:00:00Z" + }, + "ID" : "8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c", + "Created" : "2014-11-17T22:26:03.626304998Z", + "Path" : "date", + "Args" : [], + "Config" : { + "Hostname" : "8f177a186b97", + "Domainname" : "", + "User" : "", + "Memory" : 0, + "MemorySwap" : 0, + "CpuShares" : 0, + "Cpuset" : "", + "AttachStdin" : false, + "AttachStdout" : false, + "AttachStderr" : false, + "PortSpecs" : null, + "ExposedPorts" : null, + "Tty" : false, + "OpenStdin" : false, + "StdinOnce" : false, + "Env" : [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ], + "Cmd" : [ + "date" + ], + "Image" : "ubuntu", + "Volumes" : null, + "WorkingDir" : "", + "Entrypoint" : null, + "NetworkDisabled" : false, + "MacAddress" : "", + "OnBuild" : null, + "SecurityOpt" : null + }, + "Image" : "5506de2b643be1e6febbf3b8a240760c6843244c41e12aa2f60ccbb7153d17f5", + "NetworkSettings" : { + "IPAddress" : "172.17.0.2", + "IPPrefixLen" : 16, + "MacAddress" : "02:42:ac:11:00:02", + "Gateway" : "172.17.42.1", + "Bridge" : "docker0", + "PortMapping" : null, + "Ports" : {} + }, + "ResolvConfPath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/resolv.conf", + "HostnamePath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/hostname", + "HostsPath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/hosts", + "Name" : "/test", + "Driver" : "aufs", + "ExecDriver" : "native-0.2", + "MountLabel" : "", + "ProcessLabel" : "", + "AppArmorProfile" : "", + "RestartCount" : 0, + "Volumes" : {}, + "VolumesRW" : {} + } + } + +Status Codes: + +- **200** – no error +- **404** – no such exec instance +- **500** - server error + +# 3. Going further + +## 3.1 Inside `docker run` + +As an example, the `docker run` command line makes the following API calls: + +- Create the container + +- If the status code is 404, it means the image doesn't exist: + - Try to pull it + - Then retry to create the container + +- Start the container + +- If you are not in detached mode: +- Attach to the container, using logs=1 (to have stdout and + stderr from the container's start) and stream=1 + +- If in detached mode or only stdin is attached: +- Display the container's id + +## 3.2 Hijacking + +In this version of the API, /attach, uses hijacking to transport stdin, +stdout and stderr on the same socket. + +To hint potential proxies about connection hijacking, Docker client sends +connection upgrade headers similarly to websocket. + + Upgrade: tcp + Connection: Upgrade + +When Docker daemon detects the `Upgrade` header, it will switch its status code +from **200 OK** to **101 UPGRADED** and resend the same headers. + +This might change in the future. + +## 3.3 CORS Requests + +To set cross origin requests to the remote api, please add flag "--api-enable-cors" +when running docker in daemon mode. + + $ docker -d -H="192.168.1.9:2375" --api-enable-cors diff --git a/docs/reference/api/docker_remote_api_v1.18.md b/docs/reference/api/docker_remote_api_v1.18.md new file mode 100644 index 00000000..9de2cc6c --- /dev/null +++ b/docs/reference/api/docker_remote_api_v1.18.md @@ -0,0 +1,2125 @@ + + +# Docker Remote API v1.18 + +## 1. Brief introduction + + - The Remote API has replaced `rcli`. + - The daemon listens on `unix:///var/run/docker.sock` but you can + [Bind Docker to another host/port or a Unix socket](../../quickstart.md#bind-docker-to-another-host-port-or-a-unix-socket). + - The API tends to be REST, but for some complex commands, like `attach` + or `pull`, the HTTP connection is hijacked to transport `STDOUT`, + `STDIN` and `STDERR`. + +# 2. Endpoints + +## 2.1 Containers + +### List containers + +`GET /containers/json` + +List containers + +**Example request**: + + GET /containers/json?all=1&before=8dfafdbc3a40&size=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "8dfafdbc3a40", + "Names":["/boring_feynman"], + "Image": "ubuntu:latest", + "Command": "echo 1", + "Created": 1367854155, + "Status": "Exit 0", + "Ports": [{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "SizeRw": 12288, + "SizeRootFs": 0 + }, + { + "Id": "9cd87474be90", + "Names":["/coolName"], + "Image": "ubuntu:latest", + "Command": "echo 222222", + "Created": 1367854155, + "Status": "Exit 0", + "Ports": [], + "Labels": {}, + "SizeRw": 12288, + "SizeRootFs": 0 + }, + { + "Id": "3176a2479c92", + "Names":["/sleepy_dog"], + "Image": "ubuntu:latest", + "Command": "echo 3333333333333333", + "Created": 1367854154, + "Status": "Exit 0", + "Ports":[], + "Labels": {}, + "SizeRw":12288, + "SizeRootFs":0 + }, + { + "Id": "4cb07b47f9fb", + "Names":["/running_cat"], + "Image": "ubuntu:latest", + "Command": "echo 444444444444444444444444444444444", + "Created": 1367854152, + "Status": "Exit 0", + "Ports": [], + "Labels": {}, + "SizeRw": 12288, + "SizeRootFs": 0 + } + ] + +Query Parameters: + +- **all** – 1/True/true or 0/False/false, Show all containers. + Only running containers are shown by default (i.e., this defaults to false) +- **limit** – Show `limit` last created + containers, include non-running ones. +- **since** – Show only containers created since Id, include + non-running ones. +- **before** – Show only containers created before Id, include + non-running ones. +- **size** – 1/True/true or 0/False/false, Show the containers + sizes +- **filters** - a json encoded value of the filters (a map[string][]string) to process on the containers list. Available filters: + - exited=<int> -- containers with exit code of <int> + - status=(restarting|running|paused|exited) + - label=`key` or `label="key=value"` of a container label + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **500** – server error + +### Create a container + +`POST /containers/create` + +Create a container + +**Example request**: + + POST /containers/create HTTP/1.1 + Content-Type: application/json + + { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "FOO=bar", + "BAZ=quux" + ], + "Cmd": [ + "date" + ], + "Entrypoint": "", + "Image": "ubuntu", + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "Volumes": { + "/tmp": {} + }, + "WorkingDir": "", + "NetworkDisabled": false, + "MacAddress": "12:34:56:78:9a:bc", + "ExposedPorts": { + "22/tcp": {} + }, + "HostConfig": { + "Binds": ["/tmp:/tmp"], + "Links": ["redis3:redis"], + "LxcConf": {"lxc.utsname":"docker"}, + "Memory": 0, + "MemorySwap": 0, + "CpuShares": 512, + "CpusetCpus": "0,1", + "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] }, + "PublishAllPorts": false, + "Privileged": false, + "ReadonlyRootfs": false, + "Dns": ["8.8.8.8"], + "DnsSearch": [""], + "ExtraHosts": null, + "VolumesFrom": ["parent", "other:ro"], + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"], + "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 }, + "NetworkMode": "bridge", + "Devices": [], + "Ulimits": [{}], + "LogConfig": { "Type": "json-file", Config: {} }, + "SecurityOpt": [], + "CgroupParent": "" + } + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "Id":"e90e34656806", + "Warnings":[] + } + +Json Parameters: + +- **Hostname** - A string value containing the desired hostname to use for the + container. +- **Domainname** - A string value containing the desired domain name to use + for the container. +- **User** - A string value containing the user to use inside the container. +- **Memory** - Memory limit in bytes. +- **MemorySwap** - Total memory limit (memory + swap); set `-1` to enable unlimited swap. + You must use this with `memory` and make the swap value larger than `memory`. +- **CpuShares** - An integer value containing the CPU Shares for container + (ie. the relative weight vs other containers). +- **Cpuset** - The same as CpusetCpus, but deprecated, please don't use. +- **CpusetCpus** - String value containing the cgroups CpusetCpus to use. +- **AttachStdin** - Boolean value, attaches to stdin. +- **AttachStdout** - Boolean value, attaches to stdout. +- **AttachStderr** - Boolean value, attaches to stderr. +- **Tty** - Boolean value, Attach standard streams to a tty, including stdin if it is not closed. +- **OpenStdin** - Boolean value, opens stdin, +- **StdinOnce** - Boolean value, close stdin after the 1 attached client disconnects. +- **Env** - A list of environment variables in the form of `["VAR=value"[,"VAR2=value2"]]` +- **Labels** - Adds a map of labels that to a container. To specify a map: `{"key":"value"[,"key2":"value2"]}` +- **Cmd** - Command to run specified as a string or an array of strings. +- **Entrypoint** - Set the entrypoint for the container a string or an array + of strings +- **Image** - String value containing the image name to use for the container +- **Volumes** – An object mapping mountpoint paths (strings) inside the + container to empty objects. +- **WorkingDir** - A string value containing the working dir for commands to + run in. +- **NetworkDisabled** - Boolean value, when true disables networking for the + container +- **ExposedPorts** - An object mapping ports to an empty object in the form of: + `"ExposedPorts": { "/: {}" }` +- **HostConfig** + - **Binds** – A list of volume bindings for this container. Each volume + binding is a string of the form `container_path` (to create a new + volume for the container), `host_path:container_path` (to bind-mount + a host path into the container), or `host_path:container_path:ro` + (to make the bind-mount read-only inside the container). + - **Links** - A list of links for the container. Each link entry should be + in the form of `container_name:alias`. + - **LxcConf** - LXC specific configurations. These configurations will only + work when using the `lxc` execution driver. + - **PortBindings** - A map of exposed container ports and the host port they + should map to. It should be specified in the form + `{ /: [{ "HostPort": "" }] }` + Take note that `port` is specified as a string and not an integer value. + - **PublishAllPorts** - Allocates a random host port for all of a container's + exposed ports. Specified as a boolean value. + - **Privileged** - Gives the container full access to the host. Specified as + a boolean value. + - **ReadonlyRootfs** - Mount the container's root filesystem as read only. + Specified as a boolean value. + - **Dns** - A list of dns servers for the container to use. + - **DnsSearch** - A list of DNS search domains + - **ExtraHosts** - A list of hostnames/IP mappings to be added to the + container's `/etc/hosts` file. Specified in the form `["hostname:IP"]`. + - **VolumesFrom** - A list of volumes to inherit from another container. + Specified in the form `[:]` + - **CapAdd** - A list of kernel capabilities to add to the container. + - **Capdrop** - A list of kernel capabilities to drop from the container. + - **RestartPolicy** – The behavior to apply when the container exits. The + value is an object with a `Name` property of either `"always"` to + always restart or `"on-failure"` to restart only when the container + exit code is non-zero. If `on-failure` is used, `MaximumRetryCount` + controls the number of times to retry before giving up. + The default is not to restart. (optional) + An ever increasing delay (double the previous delay, starting at 100mS) + is added before each restart to prevent flooding the server. + - **NetworkMode** - Sets the networking mode for the container. Supported + values are: `bridge`, `host`, `none`, and `container:` + - **Devices** - A list of devices to add to the container specified in the + form + `{ "PathOnHost": "/dev/deviceName", "PathInContainer": "/dev/deviceName", "CgroupPermissions": "mrw"}` + - **Ulimits** - A list of ulimits to be set in the container, specified as + `{ "Name": , "Soft": , "Hard": }`, for example: + `Ulimits: { "Name": "nofile", "Soft": 1024, "Hard": 2048 }` + - **SecurityOpt**: A list of string values to customize labels for MLS + systems, such as SELinux. + - **LogConfig** - Log configuration for the container, specified as + `{ "Type": "", "Config": {"key1": "val1"}}`. + Available types: `json-file`, `syslog`, `journald`, `none`. + `json-file` logging driver. + - **CgroupParent** - Path to cgroups under which the cgroup for the container will be created. If the path is not absolute, the path is considered to be relative to the cgroups path of the init process. Cgroups will be created if they do not already exist. + +Query Parameters: + +- **name** – Assign the specified name to the container. Must + match `/?[a-zA-Z0-9_-]+`. + +Status Codes: + +- **201** – no error +- **404** – no such container +- **406** – impossible to attach (container not running) +- **500** – server error + +### Inspect a container + +`GET /containers/(id or name)/json` + +Return low-level information on the container `id` + + +**Example request**: + + GET /containers/4fa6e0f0c678/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "AppArmorProfile": "", + "Args": [ + "-c", + "exit 9" + ], + "Config": { + "AttachStderr": true, + "AttachStdin": false, + "AttachStdout": true, + "Cmd": [ + "/bin/sh", + "-c", + "exit 9" + ], + "Domainname": "", + "Entrypoint": null, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "ExposedPorts": null, + "Hostname": "ba033ac44011", + "Image": "ubuntu", + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "MacAddress": "", + "NetworkDisabled": false, + "OnBuild": null, + "OpenStdin": false, + "PortSpecs": null, + "StdinOnce": false, + "Tty": false, + "User": "", + "Volumes": null, + "WorkingDir": "" + }, + "Created": "2015-01-06T15:47:31.485331387Z", + "Driver": "devicemapper", + "ExecDriver": "native-0.2", + "ExecIDs": null, + "HostConfig": { + "Binds": null, + "CapAdd": null, + "CapDrop": null, + "ContainerIDFile": "", + "CpusetCpus": "", + "CpuShares": 0, + "Devices": [], + "Dns": null, + "DnsSearch": null, + "ExtraHosts": null, + "IpcMode": "", + "Links": null, + "LxcConf": [], + "Memory": 0, + "MemorySwap": 0, + "NetworkMode": "bridge", + "PortBindings": {}, + "Privileged": false, + "ReadonlyRootfs": false, + "PublishAllPorts": false, + "RestartPolicy": { + "MaximumRetryCount": 2, + "Name": "on-failure" + }, + "LogConfig": { + "Config": null, + "Type": "json-file" + }, + "SecurityOpt": null, + "VolumesFrom": null, + "Ulimits": [{}] + }, + "HostnamePath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/hostname", + "HostsPath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/hosts", + "LogPath": "/var/lib/docker/containers/1eb5fabf5a03807136561b3c00adcd2992b535d624d5e18b6cdc6a6844d9767b/1eb5fabf5a03807136561b3c00adcd2992b535d624d5e18b6cdc6a6844d9767b-json.log", + "Id": "ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39", + "Image": "04c5d3b7b0656168630d3ba35d8889bd0e9caafcaeb3004d2bfbc47e7c5d35d2", + "MountLabel": "", + "Name": "/boring_euclid", + "NetworkSettings": { + "Bridge": "", + "Gateway": "", + "IPAddress": "", + "IPPrefixLen": 0, + "MacAddress": "", + "PortMapping": null, + "Ports": null + }, + "Path": "/bin/sh", + "ProcessLabel": "", + "ResolvConfPath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/resolv.conf", + "RestartCount": 1, + "State": { + "Error": "", + "ExitCode": 9, + "FinishedAt": "2015-01-06T15:47:32.080254511Z", + "OOMKilled": false, + "Paused": false, + "Pid": 0, + "Restarting": false, + "Running": false, + "StartedAt": "2015-01-06T15:47:32.072697474Z" + }, + "Volumes": {}, + "VolumesRW": {} + } + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### List processes running inside a container + +`GET /containers/(id or name)/top` + +List processes running inside the container `id`. On Unix systems this +is done by running the `ps` command. This endpoint is not +supported on Windows. + +**Example request**: + + GET /containers/4fa6e0f0c678/top HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Titles" : [ + "UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD" + ], + "Processes" : [ + [ + "root", "13642", "882", "0", "17:03", "pts/0", "00:00:00", "/bin/bash" + ], + [ + "root", "13735", "13642", "0", "17:06", "pts/0", "00:00:00", "sleep 10" + ] + ] + } + +**Example request**: + + GET /containers/4fa6e0f0c678/top?ps_args=aux HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Titles" : [ + "USER","PID","%CPU","%MEM","VSZ","RSS","TTY","STAT","START","TIME","COMMAND" + ] + "Processes" : [ + [ + "root","13642","0.0","0.1","18172","3184","pts/0","Ss","17:03","0:00","/bin/bash" + ], + [ + "root","13895","0.0","0.0","4348","692","pts/0","S+","17:15","0:00","sleep 10" + ] + ], + } + +Query Parameters: + +- **ps_args** – `ps` arguments to use (e.g., `aux`), defaults to `-ef` + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Get container logs + +`GET /containers/(id or name)/logs` + +Get stdout and stderr logs from the container ``id`` + +> **Note**: +> This endpoint works only for containers with the `json-file` or `journald` logging drivers. + +**Example request**: + + GET /containers/4fa6e0f0c678/logs?stderr=1&stdout=1×tamps=1&follow=1&tail=10 HTTP/1.1 + +**Example response**: + + HTTP/1.1 101 UPGRADED + Content-Type: application/vnd.docker.raw-stream + Connection: Upgrade + Upgrade: tcp + + {{ STREAM }} + +Query Parameters: + +- **follow** – 1/True/true or 0/False/false, return stream. Default false +- **stdout** – 1/True/true or 0/False/false, show stdout log. Default false +- **stderr** – 1/True/true or 0/False/false, show stderr log. Default false +- **timestamps** – 1/True/true or 0/False/false, print timestamps for + every log line. Default false +- **tail** – Output specified number of lines at the end of logs: `all` or ``. Default all + +Status Codes: + +- **101** – no error, hints proxy about hijacking +- **200** – no error, no upgrade header found +- **404** – no such container +- **500** – server error + +### Inspect changes on a container's filesystem + +`GET /containers/(id or name)/changes` + +Inspect changes on container `id`'s filesystem + +**Example request**: + + GET /containers/4fa6e0f0c678/changes HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Path": "/dev", + "Kind": 0 + }, + { + "Path": "/dev/kmsg", + "Kind": 1 + }, + { + "Path": "/test", + "Kind": 1 + } + ] + +Values for `Kind`: + +- `0`: Modify +- `1`: Add +- `2`: Delete + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Export a container + +`GET /containers/(id or name)/export` + +Export the contents of container `id` + +**Example request**: + + GET /containers/4fa6e0f0c678/export HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/octet-stream + + {{ TAR STREAM }} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Get container stats based on resource usage + +`GET /containers/(id or name)/stats` + +This endpoint returns a live stream of a container's resource usage statistics. + +**Example request**: + + GET /containers/redis1/stats HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "read" : "2015-01-08T22:57:31.547920715Z", + "network" : { + "rx_dropped" : 0, + "rx_bytes" : 648, + "rx_errors" : 0, + "tx_packets" : 8, + "tx_dropped" : 0, + "rx_packets" : 8, + "tx_errors" : 0, + "tx_bytes" : 648 + }, + "memory_stats" : { + "stats" : { + "total_pgmajfault" : 0, + "cache" : 0, + "mapped_file" : 0, + "total_inactive_file" : 0, + "pgpgout" : 414, + "rss" : 6537216, + "total_mapped_file" : 0, + "writeback" : 0, + "unevictable" : 0, + "pgpgin" : 477, + "total_unevictable" : 0, + "pgmajfault" : 0, + "total_rss" : 6537216, + "total_rss_huge" : 6291456, + "total_writeback" : 0, + "total_inactive_anon" : 0, + "rss_huge" : 6291456, + "hierarchical_memory_limit" : 67108864, + "total_pgfault" : 964, + "total_active_file" : 0, + "active_anon" : 6537216, + "total_active_anon" : 6537216, + "total_pgpgout" : 414, + "total_cache" : 0, + "inactive_anon" : 0, + "active_file" : 0, + "pgfault" : 964, + "inactive_file" : 0, + "total_pgpgin" : 477 + }, + "max_usage" : 6651904, + "usage" : 6537216, + "failcnt" : 0, + "limit" : 67108864 + }, + "blkio_stats" : {}, + "cpu_stats" : { + "cpu_usage" : { + "percpu_usage" : [ + 16970827, + 1839451, + 7107380, + 10571290 + ], + "usage_in_usermode" : 10000000, + "total_usage" : 36488948, + "usage_in_kernelmode" : 20000000 + }, + "system_cpu_usage" : 20091722000000000, + "throttling_data" : {} + } + } + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Resize a container TTY + +`POST /containers/(id or name)/resize?h=&w=` + +Resize the TTY for container with `id`. The container must be restarted for the resize to take effect. + +**Example request**: + + POST /containers/4fa6e0f0c678/resize?h=40&w=80 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Length: 0 + Content-Type: text/plain; charset=utf-8 + +Status Codes: + +- **200** – no error +- **404** – No such container +- **500** – Cannot resize container + +### Start a container + +`POST /containers/(id or name)/start` + +Start the container `id` + +> **Note**: +> For backwards compatibility, this endpoint accepts a `HostConfig` as JSON-encoded request body. +> See [create a container](#create-a-container) for details. + +**Example request**: + + POST /containers/e90e34656806/start HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **304** – container already started +- **404** – no such container +- **500** – server error + +### Stop a container + +`POST /containers/(id or name)/stop` + +Stop the container `id` + +**Example request**: + + POST /containers/e90e34656806/stop?t=5 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **t** – number of seconds to wait before killing the container + +Status Codes: + +- **204** – no error +- **304** – container already stopped +- **404** – no such container +- **500** – server error + +### Restart a container + +`POST /containers/(id or name)/restart` + +Restart the container `id` + +**Example request**: + + POST /containers/e90e34656806/restart?t=5 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **t** – number of seconds to wait before killing the container + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Kill a container + +`POST /containers/(id or name)/kill` + +Kill the container `id` + +**Example request**: + + POST /containers/e90e34656806/kill HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters + +- **signal** - Signal to send to the container: integer or string like "SIGINT". + When not set, SIGKILL is assumed and the call will waits for the container to exit. + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Rename a container + +`POST /containers/(id or name)/rename` + +Rename the container `id` to a `new_name` + +**Example request**: + + POST /containers/e90e34656806/rename?name=new_name HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **name** – new name for the container + +Status Codes: + +- **204** – no error +- **404** – no such container +- **409** - conflict name already assigned +- **500** – server error + +### Pause a container + +`POST /containers/(id or name)/pause` + +Pause the container `id` + +**Example request**: + + POST /containers/e90e34656806/pause HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Unpause a container + +`POST /containers/(id or name)/unpause` + +Unpause the container `id` + +**Example request**: + + POST /containers/e90e34656806/unpause HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Attach to a container + +`POST /containers/(id or name)/attach` + +Attach to the container `id` + +**Example request**: + + POST /containers/16253994b7c4/attach?logs=1&stream=0&stdout=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 101 UPGRADED + Content-Type: application/vnd.docker.raw-stream + Connection: Upgrade + Upgrade: tcp + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **101** – no error, hints proxy about hijacking +- **200** – no error, no upgrade header found +- **400** – bad parameter +- **404** – no such container +- **500** – server error + + **Stream details**: + + When using the TTY setting is enabled in + [`POST /containers/create` + ](#create-a-container), + the stream is the raw data from the process PTY and client's stdin. + When the TTY is disabled, then the stream is multiplexed to separate + stdout and stderr. + + The format is a **Header** and a **Payload** (frame). + + **HEADER** + + The header will contain the information on which stream write the + stream (stdout or stderr). It also contain the size of the + associated frame encoded on the last 4 bytes (uint32). + + It is encoded on the first 8 bytes like this: + + header := [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4} + + `STREAM_TYPE` can be: + +- 0: stdin (will be written on stdout) +- 1: stdout +- 2: stderr + + `SIZE1, SIZE2, SIZE3, SIZE4` are the 4 bytes of + the uint32 size encoded as big endian. + + **PAYLOAD** + + The payload is the raw stream. + + **IMPLEMENTATION** + + The simplest way to implement the Attach protocol is the following: + + 1. Read 8 bytes + 2. chose stdout or stderr depending on the first byte + 3. Extract the frame size from the last 4 bytes + 4. Read the extracted size and output it on the correct output + 5. Goto 1 + +### Attach to a container (websocket) + +`GET /containers/(id or name)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + +### Wait a container + +`POST /containers/(id or name)/wait` + +Block until container `id` stops, then returns the exit code + +**Example request**: + + POST /containers/16253994b7c4/wait HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"StatusCode": 0} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Remove a container + +`DELETE /containers/(id or name)` + +Remove the container `id` from the filesystem + +**Example request**: + + DELETE /containers/16253994b7c4?v=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **v** – 1/True/true or 0/False/false, Remove the volumes + associated to the container. Default false +- **force** - 1/True/true or 0/False/false, Kill then remove the container. + Default false + +Status Codes: + +- **204** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + +### Copy files or folders from a container + +`POST /containers/(id or name)/copy` + +Copy files or folders of container `id` + +**Example request**: + + POST /containers/4fa6e0f0c678/copy HTTP/1.1 + Content-Type: application/json + + { + "Resource": "test.txt" + } + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + {{ TAR STREAM }} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +## 2.2 Images + +### List Images + +`GET /images/json` + +**Example request**: + + GET /images/json?all=0 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "RepoTags": [ + "ubuntu:12.04", + "ubuntu:precise", + "ubuntu:latest" + ], + "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", + "Created": 1365714795, + "Size": 131506275, + "VirtualSize": 131506275 + }, + { + "RepoTags": [ + "ubuntu:12.10", + "ubuntu:quantal" + ], + "ParentId": "27cf784147099545", + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Created": 1364102658, + "Size": 24653, + "VirtualSize": 180116135 + } + ] + +**Example request, with digest information**: + + GET /images/json?digests=1 HTTP/1.1 + +**Example response, with digest information**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Created": 1420064636, + "Id": "4986bf8c15363d1c5d15512d5266f8777bfba4974ac56e3270e7760f6f0a8125", + "ParentId": "ea13149945cb6b1e746bf28032f02e9b5a793523481a0a18645fc77ad53c4ea2", + "RepoDigests": [ + "localhost:5000/test/busybox@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf" + ], + "RepoTags": [ + "localhost:5000/test/busybox:latest", + "playdate:latest" + ], + "Size": 0, + "VirtualSize": 2429728 + } + ] + +The response shows a single image `Id` associated with two repositories +(`RepoTags`): `localhost:5000/test/busybox`: and `playdate`. A caller can use +either of the `RepoTags` values `localhost:5000/test/busybox:latest` or +`playdate:latest` to reference the image. + +You can also use `RepoDigests` values to reference an image. In this response, +the array has only one reference and that is to the +`localhost:5000/test/busybox` repository; the `playdate` repository has no +digest. You can reference this digest using the value: +`localhost:5000/test/busybox@sha256:cbbf2f9a99b47fc460d...` + +See the `docker run` and `docker build` commands for examples of digest and tag +references on the command line. + +Query Parameters: + +- **all** – 1/True/true or 0/False/false, default false +- **filters** – a json encoded value of the filters (a map[string][]string) to process on the images list. Available filters: + - dangling=true + - label=`key` or `label="key=value"` of an image label +- **filter** - only return images with the specified name + +### Build image from a Dockerfile + +`POST /build` + +Build an image from a Dockerfile + +**Example request**: + + POST /build HTTP/1.1 + + {{ TAR STREAM }} + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"stream": "Step 1..."} + {"stream": "..."} + {"error": "Error...", "errorDetail": {"code": 123, "message": "Error..."}} + +The input stream must be a tar archive compressed with one of the +following algorithms: identity (no compression), gzip, bzip2, xz. + +The archive must include a build instructions file, typically called +`Dockerfile` at the root of the archive. The `dockerfile` parameter may be +used to specify a different build instructions file by having its value be +the path to the alternate build instructions file to use. + +The archive may include any number of other files, +which will be accessible in the build context (See the [*ADD build +command*](../../reference/builder.md#dockerbuilder)). + +The build will also be canceled if the client drops the connection by quitting +or being killed. + +Query Parameters: + +- **dockerfile** - path within the build context to the Dockerfile. This is + ignored if `remote` is specified and points to an individual filename. +- **t** – repository name (and optionally a tag) to be applied to + the resulting image in case of success +- **remote** – A Git repository URI or HTTP/HTTPS URI build source. If the + URI specifies a filename, the file's contents are placed into a file + called `Dockerfile`. +- **q** – suppress verbose build output +- **nocache** – do not use the cache when building the image +- **pull** - attempt to pull the image even if an older image exists locally +- **rm** - remove intermediate containers after a successful build (default behavior) +- **forcerm** - always remove intermediate containers (includes rm) +- **memory** - set memory limit for build +- **memswap** - Total memory (memory + swap), `-1` to enable unlimited swap. +- **cpushares** - CPU shares (relative weight) +- **cpusetcpus** - CPUs in which to allow execution, e.g., `0-3`, `0,1` + + Request Headers: + +- **Content-type** – should be set to `"application/tar"`. +- **X-Registry-Config** – base64-encoded ConfigFile object + +Status Codes: + +- **200** – no error +- **500** – server error + +### Create an image + +`POST /images/create` + +Create an image, either by pulling it from the registry or by importing it + +**Example request**: + + POST /images/create?fromImage=ubuntu HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "Pulling..."} + {"status": "Pulling", "progress": "1 B/ 100 B", "progressDetail": {"current": 1, "total": 100}} + {"error": "Invalid..."} + ... + + When using this endpoint to pull an image from the registry, the + `X-Registry-Auth` header can be used to include + a base64-encoded AuthConfig object. + +Query Parameters: + +- **fromImage** – name of the image to pull +- **fromSrc** – source to import. The value may be a URL from which the image + can be retrieved or `-` to read the image from the request body. +- **repo** – repository +- **tag** – tag +- **registry** – the registry to pull from + + Request Headers: + +- **X-Registry-Auth** – base64-encoded AuthConfig object + +Status Codes: + +- **200** – no error +- **500** – server error + + + +### Inspect an image + +`GET /images/(name)/json` + +Return low-level information on the image `name` + +**Example request**: + + GET /images/ubuntu/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Created": "2013-03-23T22:24:18.818426-07:00", + "Container": "3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", + "ContainerConfig": + { + "Hostname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "PortSpecs": null, + "Tty": true, + "OpenStdin": true, + "StdinOnce": false, + "Env": null, + "Cmd": ["/bin/bash"], + "Dns": null, + "Image": "ubuntu", + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "Volumes": null, + "VolumesFrom": "", + "WorkingDir": "" + }, + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Parent": "27cf784147099545", + "Size": 6824592 + } + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Get the history of an image + +`GET /images/(name)/history` + +Return the history of the image `name` + +**Example request**: + + GET /images/ubuntu/history HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "b750fe79269d", + "Created": 1364102658, + "CreatedBy": "/bin/bash" + }, + { + "Id": "27cf78414709", + "Created": 1364068391, + "CreatedBy": "" + } + ] + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Push an image on the registry + +`POST /images/(name)/push` + +Push the image `name` on the registry + +**Example request**: + + POST /images/test/push HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "Pushing..."} + {"status": "Pushing", "progress": "1/? (n/a)", "progressDetail": {"current": 1}}} + {"error": "Invalid..."} + ... + + If you wish to push an image on to a private registry, that image must already have been tagged + into a repository which references that registry host name and port. This repository name should + then be used in the URL. This mirrors the flow of the CLI. + +**Example request**: + + POST /images/registry.acme.com:5000/test/push HTTP/1.1 + + +Query Parameters: + +- **tag** – the tag to associate with the image on the registry, optional + +Request Headers: + +- **X-Registry-Auth** – include a base64-encoded AuthConfig + object. + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Tag an image into a repository + +`POST /images/(name)/tag` + +Tag the image `name` into a repository + +**Example request**: + + POST /images/test/tag?repo=myrepo&force=0&tag=v42 HTTP/1.1 + +**Example response**: + + HTTP/1.1 201 Created + +Query Parameters: + +- **repo** – The repository to tag in +- **force** – 1/True/true or 0/False/false, default false +- **tag** - The new tag name + +Status Codes: + +- **201** – no error +- **400** – bad parameter +- **404** – no such image +- **409** – conflict +- **500** – server error + +### Remove an image + +`DELETE /images/(name)` + +Remove the image `name` from the filesystem + +**Example request**: + + DELETE /images/test HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-type: application/json + + [ + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} + ] + +Query Parameters: + +- **force** – 1/True/true or 0/False/false, default false +- **noprune** – 1/True/true or 0/False/false, default false + +Status Codes: + +- **200** – no error +- **404** – no such image +- **409** – conflict +- **500** – server error + +### Search images + +`GET /images/search` + +Search for an image on [Docker Hub](https://hub.docker.com). + +> **Note**: +> The response keys have changed from API v1.6 to reflect the JSON +> sent by the registry server to the docker daemon's request. + +**Example request**: + + GET /images/search?term=sshd HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "wma55/u1210sshd", + "star_count": 0 + }, + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "jdswinbank/sshd", + "star_count": 0 + }, + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "vgauthier/sshd", + "star_count": 0 + } + ... + ] + +Query Parameters: + +- **term** – term to search + +Status Codes: + +- **200** – no error +- **500** – server error + +## 2.3 Misc + +### Check auth configuration + +`POST /auth` + +Get the default username and email + +**Example request**: + + POST /auth HTTP/1.1 + Content-Type: application/json + + { + "username":" hannibal", + "password: "xxxx", + "email": "hannibal@a-team.com", + "serveraddress": "https://index.docker.io/v1/" + } + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – no error +- **204** – no error +- **500** – server error + +### Display system-wide information + +`GET /info` + +Display system-wide information + +**Example request**: + + GET /info HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Containers": 11, + "Debug": 0, + "DockerRootDir": "/var/lib/docker", + "Driver": "btrfs", + "DriverStatus": [[""]], + "ExecutionDriver": "native-0.1", + "HttpProxy": "http://test:test@localhost:8080", + "HttpsProxy": "https://test:test@localhost:8080", + "ID": "7TRN:IPZB:QYBB:VPBQ:UMPP:KARE:6ZNR:XE6T:7EWV:PKF4:ZOJD:TPYS", + "IPv4Forwarding": 1, + "Images": 16, + "IndexServerAddress": "https://index.docker.io/v1/", + "InitPath": "/usr/bin/docker", + "InitSha1": "", + "KernelVersion": "3.12.0-1-amd64", + "Labels": [ + "storage=ssd" + ], + "MemTotal": 2099236864, + "MemoryLimit": 1, + "NCPU": 1, + "NEventsListener": 0, + "NFd": 11, + "NGoroutines": 21, + "Name": "prod-server-42", + "NoProxy": "9.81.1.160", + "OperatingSystem": "Boot2Docker", + "RegistryConfig": { + "IndexConfigs": { + "docker.io": { + "Mirrors": null, + "Name": "docker.io", + "Official": true, + "Secure": true + } + }, + "InsecureRegistryCIDRs": [ + "127.0.0.0/8" + ] + }, + "SwapLimit": 0, + "SystemTime": "2015-03-10T11:11:23.730591467-07:00" + } + +Status Codes: + +- **200** – no error +- **500** – server error + +### Show the docker version information + +`GET /version` + +Show the docker version information + +**Example request**: + + GET /version HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Version": "1.5.0", + "Os": "linux", + "KernelVersion": "3.18.5-tinycore64", + "GoVersion": "go1.4.1", + "GitCommit": "a8a31ef", + "Arch": "amd64", + "ApiVersion": "1.18" + } + +Status Codes: + +- **200** – no error +- **500** – server error + +### Ping the docker server + +`GET /_ping` + +Ping the docker server + +**Example request**: + + GET /_ping HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: text/plain + + OK + +Status Codes: + +- **200** - no error +- **500** - server error + +### Create a new image from a container's changes + +`POST /commit` + +Create a new image from a container's changes + +**Example request**: + + POST /commit?container=44c004db4b17&comment=message&repo=myrepo HTTP/1.1 + Content-Type: application/json + + { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "PortSpecs": null, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "date" + ], + "Volumes": { + "/tmp": {} + }, + "WorkingDir": "", + "NetworkDisabled": false, + "ExposedPorts": { + "22/tcp": {} + } + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + {"Id": "596069db4bf5"} + +Json Parameters: + +- **config** - the container's configuration + +Query Parameters: + +- **container** – source container +- **repo** – repository +- **tag** – tag +- **comment** – commit message +- **author** – author (e.g., "John Hannibal Smith + <[hannibal@a-team.com](mailto:hannibal%40a-team.com)>") + +Status Codes: + +- **201** – no error +- **404** – no such container +- **500** – server error + +### Monitor Docker's events + +`GET /events` + +Get container events from docker, either in real time via streaming, or via +polling (using since). + +Docker containers will report the following events: + + create, destroy, die, exec_create, exec_start, export, kill, oom, pause, restart, start, stop, unpause + +and Docker images will report: + + untag, delete + +**Example request**: + + GET /events?since=1374067924 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "create", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067924} + {"status": "start", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067924} + {"status": "stop", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067966} + {"status": "destroy", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067970} + +Query Parameters: + +- **since** – timestamp used for polling +- **until** – timestamp used for polling +- **filters** – a json encoded value of the filters (a map[string][]string) to process on the event list. Available filters: + - event=<string> -- event to filter + - image=<string> -- image to filter + - container=<string> -- container to filter + +Status Codes: + +- **200** – no error +- **500** – server error + +### Get a tarball containing all images in a repository + +`GET /images/(name)/get` + +Get a tarball containing all images and metadata for the repository specified +by `name`. + +If `name` is a specific name and tag (e.g. ubuntu:latest), then only that image +(and its parents) are returned. If `name` is an image ID, similarly only that +image (and its parents) are returned, but with the exclusion of the +'repositories' file in the tarball, as there were no image names referenced. + +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + GET /images/ubuntu/get + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + Binary data stream + +Status Codes: + +- **200** – no error +- **500** – server error + +### Get a tarball containing all images. + +`GET /images/get` + +Get a tarball containing all images and metadata for one or more repositories. + +For each value of the `names` parameter: if it is a specific name and tag (e.g. +ubuntu:latest), then only that image (and its parents) are returned; if it is +an image ID, similarly only that image (and its parents) are returned and there +would be no names referenced in the 'repositories' file for this image ID. + +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + GET /images/get?names=myname%2Fmyapp%3Alatest&names=busybox + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + Binary data stream + +Status Codes: + +- **200** – no error +- **500** – server error + +### Load a tarball with a set of images and tags into docker + +`POST /images/load` + +Load a set of images and tags into the docker repository. +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + POST /images/load + + Tarball in body + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – no error +- **500** – server error + +### Image tarball format + +An image tarball contains one directory per image layer (named using its long ID), +each containing three files: + +1. `VERSION`: currently `1.0` - the file format version +2. `json`: detailed layer information, similar to `docker inspect layer_id` +3. `layer.tar`: A tarfile containing the filesystem changes in this layer + +The `layer.tar` file will contain `aufs` style `.wh..wh.aufs` files and directories +for storing attribute changes and deletions. + +If the tarball defines a repository, there will also be a `repositories` file at +the root that contains a list of repository and tag names mapped to layer IDs. + +``` +{"hello-world": + {"latest": "565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} +} +``` + +### Exec Create + +`POST /containers/(id or name)/exec` + +Sets up an exec instance in a running container `id` + +**Example request**: + + POST /containers/e90e34656806/exec HTTP/1.1 + Content-Type: application/json + + { + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "Cmd": [ + "date" + ], + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "Id": "f90e34656806", + "Warnings":[] + } + +Json Parameters: + +- **AttachStdin** - Boolean value, attaches to stdin of the exec command. +- **AttachStdout** - Boolean value, attaches to stdout of the exec command. +- **AttachStderr** - Boolean value, attaches to stderr of the exec command. +- **Tty** - Boolean value to allocate a pseudo-TTY +- **Cmd** - Command to run specified as a string or an array of strings. + + +Status Codes: + +- **201** – no error +- **404** – no such container + +### Exec Start + +`POST /exec/(id)/start` + +Starts a previously set up exec instance `id`. If `detach` is true, this API +returns after starting the `exec` command. Otherwise, this API sets up an +interactive session with the `exec` command. + +**Example request**: + + POST /exec/e90e34656806/start HTTP/1.1 + Content-Type: application/json + + { + "Detach": false, + "Tty": false, + } + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + +Json Parameters: + +- **Detach** - Detach from the exec command +- **Tty** - Boolean value to allocate a pseudo-TTY + +Status Codes: + +- **200** – no error +- **404** – no such exec instance + + **Stream details**: + Similar to the stream behavior of `POST /containers/(id or name)/attach` API + +### Exec Resize + +`POST /exec/(id)/resize` + +Resizes the tty session used by the exec command `id`. +This API is valid only if `tty` was specified as part of creating and starting the exec command. + +**Example request**: + + POST /exec/e90e34656806/resize HTTP/1.1 + Content-Type: text/plain + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: text/plain + +Query Parameters: + +- **h** – height of tty session +- **w** – width + +Status Codes: + +- **201** – no error +- **404** – no such exec instance + +### Exec Inspect + +`GET /exec/(id)/json` + +Return low-level information about the exec command `id`. + +**Example request**: + + GET /exec/11fb006128e8ceb3942e7c58d77750f24210e35f879dd204ac975c184b820b39/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: plain/text + + { + "ID" : "11fb006128e8ceb3942e7c58d77750f24210e35f879dd204ac975c184b820b39", + "Running" : false, + "ExitCode" : 2, + "ProcessConfig" : { + "privileged" : false, + "user" : "", + "tty" : false, + "entrypoint" : "sh", + "arguments" : [ + "-c", + "exit 2" + ] + }, + "OpenStdin" : false, + "OpenStderr" : false, + "OpenStdout" : false, + "Container" : { + "State" : { + "Running" : true, + "Paused" : false, + "Restarting" : false, + "OOMKilled" : false, + "Pid" : 3650, + "ExitCode" : 0, + "Error" : "", + "StartedAt" : "2014-11-17T22:26:03.717657531Z", + "FinishedAt" : "0001-01-01T00:00:00Z" + }, + "ID" : "8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c", + "Created" : "2014-11-17T22:26:03.626304998Z", + "Path" : "date", + "Args" : [], + "Config" : { + "Hostname" : "8f177a186b97", + "Domainname" : "", + "User" : "", + "AttachStdin" : false, + "AttachStdout" : false, + "AttachStderr" : false, + "PortSpecs" : null, + "ExposedPorts" : null, + "Tty" : false, + "OpenStdin" : false, + "StdinOnce" : false, + "Env" : [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ], + "Cmd" : [ + "date" + ], + "Image" : "ubuntu", + "Volumes" : null, + "WorkingDir" : "", + "Entrypoint" : null, + "NetworkDisabled" : false, + "MacAddress" : "", + "OnBuild" : null, + "SecurityOpt" : null + }, + "Image" : "5506de2b643be1e6febbf3b8a240760c6843244c41e12aa2f60ccbb7153d17f5", + "NetworkSettings" : { + "IPAddress" : "172.17.0.2", + "IPPrefixLen" : 16, + "MacAddress" : "02:42:ac:11:00:02", + "Gateway" : "172.17.42.1", + "Bridge" : "docker0", + "PortMapping" : null, + "Ports" : {} + }, + "ResolvConfPath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/resolv.conf", + "HostnamePath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/hostname", + "HostsPath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/hosts", + "LogPath": "/var/lib/docker/containers/1eb5fabf5a03807136561b3c00adcd2992b535d624d5e18b6cdc6a6844d9767b/1eb5fabf5a03807136561b3c00adcd2992b535d624d5e18b6cdc6a6844d9767b-json.log", + "Name" : "/test", + "Driver" : "aufs", + "ExecDriver" : "native-0.2", + "MountLabel" : "", + "ProcessLabel" : "", + "AppArmorProfile" : "", + "RestartCount" : 0, + "Volumes" : {}, + "VolumesRW" : {} + } + } + +Status Codes: + +- **200** – no error +- **404** – no such exec instance +- **500** - server error + +# 3. Going further + +## 3.1 Inside `docker run` + +As an example, the `docker run` command line makes the following API calls: + +- Create the container + +- If the status code is 404, it means the image doesn't exist: + - Try to pull it + - Then retry to create the container + +- Start the container + +- If you are not in detached mode: +- Attach to the container, using logs=1 (to have stdout and + stderr from the container's start) and stream=1 + +- If in detached mode or only stdin is attached: +- Display the container's id + +## 3.2 Hijacking + +In this version of the API, /attach, uses hijacking to transport stdin, +stdout and stderr on the same socket. + +To hint potential proxies about connection hijacking, Docker client sends +connection upgrade headers similarly to websocket. + + Upgrade: tcp + Connection: Upgrade + +When Docker daemon detects the `Upgrade` header, it will switch its status code +from **200 OK** to **101 UPGRADED** and resend the same headers. + +This might change in the future. + +## 3.3 CORS Requests + +To set cross origin requests to the remote api please give values to +"--api-cors-header" when running docker in daemon mode. Set * will allow all, +default or blank means CORS disabled + + $ docker -d -H="192.168.1.9:2375" --api-cors-header="http://foo.bar" diff --git a/docs/reference/api/docker_remote_api_v1.19.md b/docs/reference/api/docker_remote_api_v1.19.md new file mode 100644 index 00000000..e049dbe7 --- /dev/null +++ b/docs/reference/api/docker_remote_api_v1.19.md @@ -0,0 +1,2206 @@ + + +# Docker Remote API v1.19 + +## 1. Brief introduction + + - The Remote API has replaced `rcli`. + - The daemon listens on `unix:///var/run/docker.sock` but you can + [Bind Docker to another host/port or a Unix socket](../../quickstart.md#bind-docker-to-another-host-port-or-a-unix-socket). + - The API tends to be REST. However, for some complex commands, like `attach` + or `pull`, the HTTP connection is hijacked to transport `stdout`, + `stdin` and `stderr`. + - When the client API version is newer than the daemon's, these calls return an HTTP + `400 Bad Request` error message. + +# 2. Endpoints + +## 2.1 Containers + +### List containers + +`GET /containers/json` + +List containers + +**Example request**: + + GET /containers/json?all=1&before=8dfafdbc3a40&size=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "8dfafdbc3a40", + "Names":["/boring_feynman"], + "Image": "ubuntu:latest", + "Command": "echo 1", + "Created": 1367854155, + "Status": "Exit 0", + "Ports": [{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "SizeRw": 12288, + "SizeRootFs": 0 + }, + { + "Id": "9cd87474be90", + "Names":["/coolName"], + "Image": "ubuntu:latest", + "Command": "echo 222222", + "Created": 1367854155, + "Status": "Exit 0", + "Ports": [], + "Labels": {}, + "SizeRw": 12288, + "SizeRootFs": 0 + }, + { + "Id": "3176a2479c92", + "Names":["/sleepy_dog"], + "Image": "ubuntu:latest", + "Command": "echo 3333333333333333", + "Created": 1367854154, + "Status": "Exit 0", + "Ports":[], + "Labels": {}, + "SizeRw":12288, + "SizeRootFs":0 + }, + { + "Id": "4cb07b47f9fb", + "Names":["/running_cat"], + "Image": "ubuntu:latest", + "Command": "echo 444444444444444444444444444444444", + "Created": 1367854152, + "Status": "Exit 0", + "Ports": [], + "Labels": {}, + "SizeRw": 12288, + "SizeRootFs": 0 + } + ] + +Query Parameters: + +- **all** – 1/True/true or 0/False/false, Show all containers. + Only running containers are shown by default (i.e., this defaults to false) +- **limit** – Show `limit` last created + containers, include non-running ones. +- **since** – Show only containers created since Id, include + non-running ones. +- **before** – Show only containers created before Id, include + non-running ones. +- **size** – 1/True/true or 0/False/false, Show the containers + sizes +- **filters** - a JSON encoded value of the filters (a `map[string][]string`) to process on the containers list. Available filters: + - `exited=`; -- containers with exit code of `` ; + - `status=`(`restarting`|`running`|`paused`|`exited`) + - `label=key` or `label="key=value"` of a container label + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **500** – server error + +### Create a container + +`POST /containers/create` + +Create a container + +**Example request**: + + POST /containers/create HTTP/1.1 + Content-Type: application/json + + { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "FOO=bar", + "BAZ=quux" + ], + "Cmd": [ + "date" + ], + "Entrypoint": "", + "Image": "ubuntu", + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "Volumes": { + "/tmp": {} + }, + "WorkingDir": "", + "NetworkDisabled": false, + "MacAddress": "12:34:56:78:9a:bc", + "ExposedPorts": { + "22/tcp": {} + }, + "HostConfig": { + "Binds": ["/tmp:/tmp"], + "Links": ["redis3:redis"], + "LxcConf": {"lxc.utsname":"docker"}, + "Memory": 0, + "MemorySwap": 0, + "CpuShares": 512, + "CpuPeriod": 100000, + "CpuQuota": 50000, + "CpusetCpus": "0,1", + "CpusetMems": "0,1", + "BlkioWeight": 300, + "OomKillDisable": false, + "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] }, + "PublishAllPorts": false, + "Privileged": false, + "ReadonlyRootfs": false, + "Dns": ["8.8.8.8"], + "DnsSearch": [""], + "ExtraHosts": null, + "VolumesFrom": ["parent", "other:ro"], + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"], + "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 }, + "NetworkMode": "bridge", + "Devices": [], + "Ulimits": [{}], + "LogConfig": { "Type": "json-file", "Config": {} }, + "SecurityOpt": [], + "CgroupParent": "" + } + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "Id":"e90e34656806", + "Warnings":[] + } + +Json Parameters: + +- **Hostname** - A string value containing the hostname to use for the + container. +- **Domainname** - A string value containing the domain name to use + for the container. +- **User** - A string value specifying the user inside the container. +- **Memory** - Memory limit in bytes. +- **MemorySwap** - Total memory limit (memory + swap); set `-1` to enable unlimited swap. + You must use this with `memory` and make the swap value larger than `memory`. +- **CpuShares** - An integer value containing the container's CPU Shares + (ie. the relative weight vs other containers). +- **CpuPeriod** - The length of a CPU period in microseconds. +- **CpuQuota** - Microseconds of CPU time that the container can get in a CPU period. +- **Cpuset** - Deprecated please don't use. Use `CpusetCpus` instead. +- **CpusetCpus** - String value containing the `cgroups CpusetCpus` to use. +- **CpusetMems** - Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems. +- **BlkioWeight** - Block IO weight (relative weight) accepts a weight value between 10 and 1000. +- **OomKillDisable** - Boolean value, whether to disable OOM Killer for the container or not. +- **AttachStdin** - Boolean value, attaches to `stdin`. +- **AttachStdout** - Boolean value, attaches to `stdout`. +- **AttachStderr** - Boolean value, attaches to `stderr`. +- **Tty** - Boolean value, Attach standard streams to a `tty`, including `stdin` if it is not closed. +- **OpenStdin** - Boolean value, opens stdin, +- **StdinOnce** - Boolean value, close `stdin` after the 1 attached client disconnects. +- **Env** - A list of environment variables in the form of `["VAR=value"[,"VAR2=value2"]]` +- **Labels** - Adds a map of labels to a container. To specify a map: `{"key":"value"[,"key2":"value2"]}` +- **Cmd** - Command to run specified as a string or an array of strings. +- **Entrypoint** - Set the entry point for the container as a string or an array + of strings. +- **Image** - A string specifying the image name to use for the container. +- **Volumes** – An object mapping mount point paths (strings) inside the + container to empty objects. +- **WorkingDir** - A string specifying the working directory for commands to + run in. +- **NetworkDisabled** - Boolean value, when true disables networking for the + container +- **ExposedPorts** - An object mapping ports to an empty object in the form of: + `"ExposedPorts": { "/: {}" }` +- **HostConfig** + - **Binds** – A list of volume bindings for this container. Each volume binding is a string in one of these forms: + + `container_path` to create a new volume for the container + + `host_path:container_path` to bind-mount a host path into the container + + `host_path:container_path:ro` to make the bind-mount read-only inside the container. + - **Links** - A list of links for the container. Each link entry should be + in the form of `container_name:alias`. + - **LxcConf** - LXC specific configurations. These configurations only + work when using the `lxc` execution driver. + - **PortBindings** - A map of exposed container ports and the host port they + should map to. A JSON object in the form + `{ /: [{ "HostPort": "" }] }` + Take note that `port` is specified as a string and not an integer value. + - **PublishAllPorts** - Allocates a random host port for all of a container's + exposed ports. Specified as a boolean value. + - **Privileged** - Gives the container full access to the host. Specified as + a boolean value. + - **ReadonlyRootfs** - Mount the container's root filesystem as read only. + Specified as a boolean value. + - **Dns** - A list of DNS servers for the container to use. + - **DnsSearch** - A list of DNS search domains + - **ExtraHosts** - A list of hostnames/IP mappings to add to the + container's `/etc/hosts` file. Specified in the form `["hostname:IP"]`. + - **VolumesFrom** - A list of volumes to inherit from another container. + Specified in the form `[:]` + - **CapAdd** - A list of kernel capabilities to add to the container. + - **Capdrop** - A list of kernel capabilities to drop from the container. + - **RestartPolicy** – The behavior to apply when the container exits. The + value is an object with a `Name` property of either `"always"` to + always restart or `"on-failure"` to restart only when the container + exit code is non-zero. If `on-failure` is used, `MaximumRetryCount` + controls the number of times to retry before giving up. + The default is not to restart. (optional) + An ever increasing delay (double the previous delay, starting at 100mS) + is added before each restart to prevent flooding the server. + - **NetworkMode** - Sets the networking mode for the container. Supported + values are: `bridge`, `host`, `none`, and `container:` + - **Devices** - A list of devices to add to the container specified as a JSON object in the + form + `{ "PathOnHost": "/dev/deviceName", "PathInContainer": "/dev/deviceName", "CgroupPermissions": "mrw"}` + - **Ulimits** - A list of ulimits to set in the container, specified as + `{ "Name": , "Soft": , "Hard": }`, for example: + `Ulimits: { "Name": "nofile", "Soft": 1024, "Hard": 2048 }` + - **SecurityOpt**: A list of string values to customize labels for MLS + systems, such as SELinux. + - **LogConfig** - Log configuration for the container, specified as a JSON object in the form + `{ "Type": "", "Config": {"key1": "val1"}}`. + Available types: `json-file`, `syslog`, `journald`, `none`. + `syslog` available options are: `address`. + - **CgroupParent** - Path to cgroups under which the cgroup for the container will be created. If the path is not absolute, the path is considered to be relative to the cgroups path of the init process. Cgroups will be created if they do not already exist. + +Query Parameters: + +- **name** – Assign the specified name to the container. Must + match `/?[a-zA-Z0-9_-]+`. + +Status Codes: + +- **201** – no error +- **404** – no such container +- **406** – impossible to attach (container not running) +- **500** – server error + +### Inspect a container + +`GET /containers/(id or name)/json` + +Return low-level information on the container `id` + + +**Example request**: + + GET /containers/4fa6e0f0c678/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "AppArmorProfile": "", + "Args": [ + "-c", + "exit 9" + ], + "Config": { + "AttachStderr": true, + "AttachStdin": false, + "AttachStdout": true, + "Cmd": [ + "/bin/sh", + "-c", + "exit 9" + ], + "Domainname": "", + "Entrypoint": null, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "ExposedPorts": null, + "Hostname": "ba033ac44011", + "Image": "ubuntu", + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "MacAddress": "", + "NetworkDisabled": false, + "OnBuild": null, + "OpenStdin": false, + "PortSpecs": null, + "StdinOnce": false, + "Tty": false, + "User": "", + "Volumes": null, + "WorkingDir": "" + }, + "Created": "2015-01-06T15:47:31.485331387Z", + "Driver": "devicemapper", + "ExecDriver": "native-0.2", + "ExecIDs": null, + "HostConfig": { + "Binds": null, + "BlkioWeight": 0, + "CapAdd": null, + "CapDrop": null, + "ContainerIDFile": "", + "CpusetCpus": "", + "CpusetMems": "", + "CpuShares": 0, + "CpuPeriod": 100000, + "Devices": [], + "Dns": null, + "DnsSearch": null, + "ExtraHosts": null, + "IpcMode": "", + "Links": null, + "LxcConf": [], + "Memory": 0, + "MemorySwap": 0, + "OomKillDisable": false, + "NetworkMode": "bridge", + "PortBindings": {}, + "Privileged": false, + "ReadonlyRootfs": false, + "PublishAllPorts": false, + "RestartPolicy": { + "MaximumRetryCount": 2, + "Name": "on-failure" + }, + "LogConfig": { + "Config": null, + "Type": "json-file" + }, + "SecurityOpt": null, + "VolumesFrom": null, + "Ulimits": [{}] + }, + "HostnamePath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/hostname", + "HostsPath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/hosts", + "LogPath": "/var/lib/docker/containers/1eb5fabf5a03807136561b3c00adcd2992b535d624d5e18b6cdc6a6844d9767b/1eb5fabf5a03807136561b3c00adcd2992b535d624d5e18b6cdc6a6844d9767b-json.log", + "Id": "ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39", + "Image": "04c5d3b7b0656168630d3ba35d8889bd0e9caafcaeb3004d2bfbc47e7c5d35d2", + "MountLabel": "", + "Name": "/boring_euclid", + "NetworkSettings": { + "Bridge": "", + "Gateway": "", + "IPAddress": "", + "IPPrefixLen": 0, + "MacAddress": "", + "PortMapping": null, + "Ports": null + }, + "Path": "/bin/sh", + "ProcessLabel": "", + "ResolvConfPath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/resolv.conf", + "RestartCount": 1, + "State": { + "Error": "", + "ExitCode": 9, + "FinishedAt": "2015-01-06T15:47:32.080254511Z", + "OOMKilled": false, + "Paused": false, + "Pid": 0, + "Restarting": false, + "Running": false, + "StartedAt": "2015-01-06T15:47:32.072697474Z" + }, + "Volumes": {}, + "VolumesRW": {} + } + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### List processes running inside a container + +`GET /containers/(id or name)/top` + +List processes running inside the container `id`. On Unix systems this +is done by running the `ps` command. This endpoint is not +supported on Windows. + +**Example request**: + + GET /containers/4fa6e0f0c678/top HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Titles" : [ + "UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD" + ], + "Processes" : [ + [ + "root", "13642", "882", "0", "17:03", "pts/0", "00:00:00", "/bin/bash" + ], + [ + "root", "13735", "13642", "0", "17:06", "pts/0", "00:00:00", "sleep 10" + ] + ] + } + +**Example request**: + + GET /containers/4fa6e0f0c678/top?ps_args=aux HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Titles" : [ + "USER","PID","%CPU","%MEM","VSZ","RSS","TTY","STAT","START","TIME","COMMAND" + ] + "Processes" : [ + [ + "root","13642","0.0","0.1","18172","3184","pts/0","Ss","17:03","0:00","/bin/bash" + ], + [ + "root","13895","0.0","0.0","4348","692","pts/0","S+","17:15","0:00","sleep 10" + ] + ], + } + +Query Parameters: + +- **ps_args** – `ps` arguments to use (e.g., `aux`), defaults to `-ef` + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Get container logs + +`GET /containers/(id or name)/logs` + +Get `stdout` and `stderr` logs from the container ``id`` + +> **Note**: +> This endpoint works only for containers with the `json-file` or `journald` logging drivers. + +**Example request**: + + GET /containers/4fa6e0f0c678/logs?stderr=1&stdout=1×tamps=1&follow=1&tail=10&since=1428990821 HTTP/1.1 + +**Example response**: + + HTTP/1.1 101 UPGRADED + Content-Type: application/vnd.docker.raw-stream + Connection: Upgrade + Upgrade: tcp + + {{ STREAM }} + +Query Parameters: + +- **follow** – 1/True/true or 0/False/false, return stream. Default `false`. +- **stdout** – 1/True/true or 0/False/false, show `stdout` log. Default `false`. +- **stderr** – 1/True/true or 0/False/false, show `stderr` log. Default `false`. +- **since** – UNIX timestamp (integer) to filter logs. Specifying a timestamp + will only output log-entries since that timestamp. Default: 0 (unfiltered) +- **timestamps** – 1/True/true or 0/False/false, print timestamps for + every log line. Default `false`. +- **tail** – Output specified number of lines at the end of logs: `all` or ``. Default all. + +Status Codes: + +- **101** – no error, hints proxy about hijacking +- **200** – no error, no upgrade header found +- **404** – no such container +- **500** – server error + +### Inspect changes on a container's filesystem + +`GET /containers/(id or name)/changes` + +Inspect changes on container `id`'s filesystem + +**Example request**: + + GET /containers/4fa6e0f0c678/changes HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Path": "/dev", + "Kind": 0 + }, + { + "Path": "/dev/kmsg", + "Kind": 1 + }, + { + "Path": "/test", + "Kind": 1 + } + ] + +Values for `Kind`: + +- `0`: Modify +- `1`: Add +- `2`: Delete + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Export a container + +`GET /containers/(id or name)/export` + +Export the contents of container `id` + +**Example request**: + + GET /containers/4fa6e0f0c678/export HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/octet-stream + + {{ TAR STREAM }} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Get container stats based on resource usage + +`GET /containers/(id or name)/stats` + +This endpoint returns a live stream of a container's resource usage statistics. + +**Example request**: + + GET /containers/redis1/stats HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "read" : "2015-01-08T22:57:31.547920715Z", + "network" : { + "rx_dropped" : 0, + "rx_bytes" : 648, + "rx_errors" : 0, + "tx_packets" : 8, + "tx_dropped" : 0, + "rx_packets" : 8, + "tx_errors" : 0, + "tx_bytes" : 648 + }, + "memory_stats" : { + "stats" : { + "total_pgmajfault" : 0, + "cache" : 0, + "mapped_file" : 0, + "total_inactive_file" : 0, + "pgpgout" : 414, + "rss" : 6537216, + "total_mapped_file" : 0, + "writeback" : 0, + "unevictable" : 0, + "pgpgin" : 477, + "total_unevictable" : 0, + "pgmajfault" : 0, + "total_rss" : 6537216, + "total_rss_huge" : 6291456, + "total_writeback" : 0, + "total_inactive_anon" : 0, + "rss_huge" : 6291456, + "hierarchical_memory_limit" : 67108864, + "total_pgfault" : 964, + "total_active_file" : 0, + "active_anon" : 6537216, + "total_active_anon" : 6537216, + "total_pgpgout" : 414, + "total_cache" : 0, + "inactive_anon" : 0, + "active_file" : 0, + "pgfault" : 964, + "inactive_file" : 0, + "total_pgpgin" : 477 + }, + "max_usage" : 6651904, + "usage" : 6537216, + "failcnt" : 0, + "limit" : 67108864 + }, + "blkio_stats" : {}, + "cpu_stats" : { + "cpu_usage" : { + "percpu_usage" : [ + 8646879, + 24472255, + 36438778, + 30657443 + ], + "usage_in_usermode" : 50000000, + "total_usage" : 100215355, + "usage_in_kernelmode" : 30000000 + }, + "system_cpu_usage" : 739306590000000, + "throttling_data" : {"periods":0,"throttled_periods":0,"throttled_time":0} + }, + "precpu_stats" : { + "cpu_usage" : { + "percpu_usage" : [ + 8646879, + 24350896, + 36438778, + 30657443 + ], + "usage_in_usermode" : 50000000, + "total_usage" : 100093996, + "usage_in_kernelmode" : 30000000 + }, + "system_cpu_usage" : 9492140000000, + "throttling_data" : {"periods":0,"throttled_periods":0,"throttled_time":0} + } + } + +The precpu_stats is the cpu statistic of last read, which is used for calculating the cpu usage percent. It is not the exact copy of the “cpu_stats” field. + +Query Parameters: + +- **stream** – 1/True/true or 0/False/false, pull stats once then disconnect. Default `true`. + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Resize a container TTY + +`POST /containers/(id or name)/resize?h=&w=` + +Resize the TTY for container with `id`. You must restart the container for the resize to take effect. + +**Example request**: + + POST /containers/4fa6e0f0c678/resize?h=40&w=80 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Length: 0 + Content-Type: text/plain; charset=utf-8 + +Status Codes: + +- **200** – no error +- **404** – No such container +- **500** – Cannot resize container + +### Start a container + +`POST /containers/(id or name)/start` + +Start the container `id` + +> **Note**: +> For backwards compatibility, this endpoint accepts a `HostConfig` as JSON-encoded request body. +> See [create a container](#create-a-container) for details. + +**Example request**: + + POST /containers/e90e34656806/start HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **304** – container already started +- **404** – no such container +- **500** – server error + +### Stop a container + +`POST /containers/(id or name)/stop` + +Stop the container `id` + +**Example request**: + + POST /containers/e90e34656806/stop?t=5 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **t** – number of seconds to wait before killing the container + +Status Codes: + +- **204** – no error +- **304** – container already stopped +- **404** – no such container +- **500** – server error + +### Restart a container + +`POST /containers/(id or name)/restart` + +Restart the container `id` + +**Example request**: + + POST /containers/e90e34656806/restart?t=5 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **t** – number of seconds to wait before killing the container + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Kill a container + +`POST /containers/(id or name)/kill` + +Kill the container `id` + +**Example request**: + + POST /containers/e90e34656806/kill HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters + +- **signal** - Signal to send to the container: integer or string like `SIGINT`. + When not set, `SIGKILL` is assumed and the call waits for the container to exit. + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Rename a container + +`POST /containers/(id or name)/rename` + +Rename the container `id` to a `new_name` + +**Example request**: + + POST /containers/e90e34656806/rename?name=new_name HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **name** – new name for the container + +Status Codes: + +- **204** – no error +- **404** – no such container +- **409** - conflict name already assigned +- **500** – server error + +### Pause a container + +`POST /containers/(id or name)/pause` + +Pause the container `id` + +**Example request**: + + POST /containers/e90e34656806/pause HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Unpause a container + +`POST /containers/(id or name)/unpause` + +Unpause the container `id` + +**Example request**: + + POST /containers/e90e34656806/unpause HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Attach to a container + +`POST /containers/(id or name)/attach` + +Attach to the container `id` + +**Example request**: + + POST /containers/16253994b7c4/attach?logs=1&stream=0&stdout=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 101 UPGRADED + Content-Type: application/vnd.docker.raw-stream + Connection: Upgrade + Upgrade: tcp + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default `false`. +- **stream** – 1/True/true or 0/False/false, return stream. + Default `false`. +- **stdin** – 1/True/true or 0/False/false, if `stream=true`, attach + to `stdin`. Default `false`. +- **stdout** – 1/True/true or 0/False/false, if `logs=true`, return + `stdout` log, if `stream=true`, attach to `stdout`. Default `false`. +- **stderr** – 1/True/true or 0/False/false, if `logs=true`, return + `stderr` log, if `stream=true`, attach to `stderr`. Default `false`. + +Status Codes: + +- **101** – no error, hints proxy about hijacking +- **200** – no error, no upgrade header found +- **400** – bad parameter +- **404** – no such container +- **500** – server error + + **Stream details**: + + When using the TTY setting is enabled in + [`POST /containers/create` + ](#create-a-container), + the stream is the raw data from the process PTY and client's `stdin`. + When the TTY is disabled, then the stream is multiplexed to separate + `stdout` and `stderr`. + + The format is a **Header** and a **Payload** (frame). + + **HEADER** + + The header contains the information which the stream writes (`stdout` or + `stderr`). It also contains the size of the associated frame encoded in the + last four bytes (`uint32`). + + It is encoded on the first eight bytes like this: + + header := [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4} + + `STREAM_TYPE` can be: + +- 0: `stdin` (is written on `stdout`) +- 1: `stdout` +- 2: `stderr` + + `SIZE1, SIZE2, SIZE3, SIZE4` are the four bytes of + the `uint32` size encoded as big endian. + + **PAYLOAD** + + The payload is the raw stream. + + **IMPLEMENTATION** + + The simplest way to implement the Attach protocol is the following: + + 1. Read eight bytes. + 2. Choose `stdout` or `stderr` depending on the first byte. + 3. Extract the frame size from the last four bytes. + 4. Read the extracted size and output it on the correct output. + 5. Goto 1. + +### Attach to a container (websocket) + +`GET /containers/(id or name)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default `false`. +- **stream** – 1/True/true or 0/False/false, return stream. + Default `false`. +- **stdin** – 1/True/true or 0/False/false, if `stream=true`, attach + to `stdin`. Default `false`. +- **stdout** – 1/True/true or 0/False/false, if `logs=true`, return + `stdout` log, if `stream=true`, attach to `stdout`. Default `false`. +- **stderr** – 1/True/true or 0/False/false, if `logs=true`, return + `stderr` log, if `stream=true`, attach to `stderr`. Default `false`. + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + +### Wait a container + +`POST /containers/(id or name)/wait` + +Block until container `id` stops, then returns the exit code + +**Example request**: + + POST /containers/16253994b7c4/wait HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"StatusCode": 0} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Remove a container + +`DELETE /containers/(id or name)` + +Remove the container `id` from the filesystem + +**Example request**: + + DELETE /containers/16253994b7c4?v=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **v** – 1/True/true or 0/False/false, Remove the volumes + associated to the container. Default `false`. +- **force** - 1/True/true or 0/False/false, Kill then remove the container. + Default `false`. + +Status Codes: + +- **204** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + +### Copy files or folders from a container + +`POST /containers/(id or name)/copy` + +Copy files or folders of container `id` + +**Example request**: + + POST /containers/4fa6e0f0c678/copy HTTP/1.1 + Content-Type: application/json + + { + "Resource": "test.txt" + } + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + {{ TAR STREAM }} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +## 2.2 Images + +### List Images + +`GET /images/json` + +**Example request**: + + GET /images/json?all=0 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "RepoTags": [ + "ubuntu:12.04", + "ubuntu:precise", + "ubuntu:latest" + ], + "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", + "Created": 1365714795, + "Size": 131506275, + "VirtualSize": 131506275, + "Labels": {} + }, + { + "RepoTags": [ + "ubuntu:12.10", + "ubuntu:quantal" + ], + "ParentId": "27cf784147099545", + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Created": 1364102658, + "Size": 24653, + "VirtualSize": 180116135, + "Labels": { + "com.example.version": "v1" + } + } + ] + +**Example request, with digest information**: + + GET /images/json?digests=1 HTTP/1.1 + +**Example response, with digest information**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Created": 1420064636, + "Id": "4986bf8c15363d1c5d15512d5266f8777bfba4974ac56e3270e7760f6f0a8125", + "ParentId": "ea13149945cb6b1e746bf28032f02e9b5a793523481a0a18645fc77ad53c4ea2", + "RepoDigests": [ + "localhost:5000/test/busybox@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf" + ], + "RepoTags": [ + "localhost:5000/test/busybox:latest", + "playdate:latest" + ], + "Size": 0, + "VirtualSize": 2429728, + "Labels": {} + } + ] + +The response shows a single image `Id` associated with two repositories +(`RepoTags`): `localhost:5000/test/busybox`: and `playdate`. A caller can use +either of the `RepoTags` values `localhost:5000/test/busybox:latest` or +`playdate:latest` to reference the image. + +You can also use `RepoDigests` values to reference an image. In this response, +the array has only one reference and that is to the +`localhost:5000/test/busybox` repository; the `playdate` repository has no +digest. You can reference this digest using the value: +`localhost:5000/test/busybox@sha256:cbbf2f9a99b47fc460d...` + +See the `docker run` and `docker build` commands for examples of digest and tag +references on the command line. + +Query Parameters: + +- **all** – 1/True/true or 0/False/false, default false +- **filters** – a JSON encoded value of the filters (a map[string][]string) to process on the images list. Available filters: + - `dangling=true` + - `label=key` or `label="key=value"` of an image label +- **filter** - only return images with the specified name + +### Build image from a Dockerfile + +`POST /build` + +Build an image from a Dockerfile + +**Example request**: + + POST /build HTTP/1.1 + + {{ TAR STREAM }} + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"stream": "Step 1..."} + {"stream": "..."} + {"error": "Error...", "errorDetail": {"code": 123, "message": "Error..."}} + +The input stream must be a `tar` archive compressed with one of the +following algorithms: `identity` (no compression), `gzip`, `bzip2`, `xz`. + +The archive must include a build instructions file, typically called +`Dockerfile` at the archive's root. The `dockerfile` parameter may be +used to specify a different build instructions file. To do this, its value must be +the path to the alternate build instructions file to use. + +The archive may include any number of other files, +which are accessible in the build context (See the [*ADD build +command*](../../reference/builder.md#dockerbuilder)). + +The build is canceled if the client drops the connection by quitting +or being killed. + +Query Parameters: + +- **dockerfile** - Path within the build context to the `Dockerfile`. This is + ignored if `remote` is specified and points to an external `Dockerfile`. +- **t** – Repository name (and optionally a tag) to be applied to + the resulting image in case of success. +- **remote** – A Git repository URI or HTTP/HTTPS context URI. If the + URI points to a single text file, the file's contents are placed into + a file called `Dockerfile` and the image is built from that file. If + the URI points to a tarball, the file is downloaded by the daemon and + the contents therein used as the context for the build. If the URI + points to a tarball and the `dockerfile` parameter is also specified, + there must be a file with the corresponding path inside the tarball. +- **q** – Suppress verbose build output. +- **nocache** – Do not use the cache when building the image. +- **pull** - Attempt to pull the image even if an older image exists locally. +- **rm** - Remove intermediate containers after a successful build (default behavior). +- **forcerm** - Always remove intermediate containers (includes `rm`). +- **memory** - Set memory limit for build. +- **memswap** - Total memory (memory + swap), `-1` to enable unlimited swap. +- **cpushares** - CPU shares (relative weight). +- **cpusetcpus** - CPUs in which to allow execution (e.g., `0-3`, `0,1`). +- **cpuperiod** - The length of a CPU period in microseconds. +- **cpuquota** - Microseconds of CPU time that the container can get in a CPU period. + + Request Headers: + +- **Content-type** – Set to `"application/tar"`. +- **X-Registry-Config** – base64-encoded ConfigFile object + +Status Codes: + +- **200** – no error +- **500** – server error + +### Create an image + +`POST /images/create` + +Create an image either by pulling it from the registry or by importing it + +**Example request**: + + POST /images/create?fromImage=ubuntu HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "Pulling..."} + {"status": "Pulling", "progress": "1 B/ 100 B", "progressDetail": {"current": 1, "total": 100}} + {"error": "Invalid..."} + ... + +When using this endpoint to pull an image from the registry, the +`X-Registry-Auth` header can be used to include +a base64-encoded AuthConfig object. + +Query Parameters: + +- **fromImage** – Name of the image to pull. +- **fromSrc** – Source to import. The value may be a URL from which the image + can be retrieved or `-` to read the image from the request body. +- **repo** – Repository name. +- **tag** – Tag. +- **registry** – The registry to pull from. + + Request Headers: + +- **X-Registry-Auth** – base64-encoded AuthConfig object + +Status Codes: + +- **200** – no error +- **500** – server error + + + +### Inspect an image + +`GET /images/(name)/json` + +Return low-level information on the image `name` + +**Example request**: + + GET /images/ubuntu/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Created": "2013-03-23T22:24:18.818426-07:00", + "Container": "3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", + "ContainerConfig": + { + "Hostname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "PortSpecs": null, + "Tty": true, + "OpenStdin": true, + "StdinOnce": false, + "Env": null, + "Cmd": ["/bin/bash"], + "Dns": null, + "Image": "ubuntu", + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "Volumes": null, + "VolumesFrom": "", + "WorkingDir": "" + }, + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Parent": "27cf784147099545", + "Size": 6824592 + } + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Get the history of an image + +`GET /images/(name)/history` + +Return the history of the image `name` + +**Example request**: + + GET /images/ubuntu/history HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "3db9c44f45209632d6050b35958829c3a2aa256d81b9a7be45b362ff85c54710", + "Created": 1398108230, + "CreatedBy": "/bin/sh -c #(nop) ADD file:eb15dbd63394e063b805a3c32ca7bf0266ef64676d5a6fab4801f2e81e2a5148 in /", + "Tags": [ + "ubuntu:lucid", + "ubuntu:10.04" + ], + "Size": 182964289, + "Comment": "" + }, + { + "Id": "6cfa4d1f33fb861d4d114f43b25abd0ac737509268065cdfd69d544a59c85ab8", + "Created": 1398108222, + "CreatedBy": "/bin/sh -c #(nop) MAINTAINER Tianon Gravi - mkimage-debootstrap.sh -i iproute,iputils-ping,ubuntu-minimal -t lucid.tar.xz lucid http://archive.ubuntu.com/ubuntu/", + "Tags": null, + "Size": 0, + "Comment": "" + }, + { + "Id": "511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158", + "Created": 1371157430, + "CreatedBy": "", + "Tags": [ + "scratch12:latest", + "scratch:latest" + ], + "Size": 0, + "Comment": "Imported from -" + } + ] + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Push an image on the registry + +`POST /images/(name)/push` + +Push the image `name` on the registry + +**Example request**: + + POST /images/test/push HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "Pushing..."} + {"status": "Pushing", "progress": "1/? (n/a)", "progressDetail": {"current": 1}}} + {"error": "Invalid..."} + ... + +If you wish to push an image on to a private registry, that image must already have a tag +into a repository which references that registry `hostname` and `port`. This repository name should +then be used in the URL. This duplicates the command line's flow. + +**Example request**: + + POST /images/registry.acme.com:5000/test/push HTTP/1.1 + + +Query Parameters: + +- **tag** – The tag to associate with the image on the registry. This is optional. + +Request Headers: + +- **X-Registry-Auth** – Include a base64-encoded AuthConfig. + object. + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Tag an image into a repository + +`POST /images/(name)/tag` + +Tag the image `name` into a repository + +**Example request**: + + POST /images/test/tag?repo=myrepo&force=0&tag=v42 HTTP/1.1 + +**Example response**: + + HTTP/1.1 201 Created + +Query Parameters: + +- **repo** – The repository to tag in +- **force** – 1/True/true or 0/False/false, default false +- **tag** - The new tag name + +Status Codes: + +- **201** – no error +- **400** – bad parameter +- **404** – no such image +- **409** – conflict +- **500** – server error + +### Remove an image + +`DELETE /images/(name)` + +Remove the image `name` from the filesystem + +**Example request**: + + DELETE /images/test HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-type: application/json + + [ + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} + ] + +Query Parameters: + +- **force** – 1/True/true or 0/False/false, default false +- **noprune** – 1/True/true or 0/False/false, default false + +Status Codes: + +- **200** – no error +- **404** – no such image +- **409** – conflict +- **500** – server error + +### Search images + +`GET /images/search` + +Search for an image on [Docker Hub](https://hub.docker.com). This API +returns both `is_trusted` and `is_automated` images. Currently, they +are considered identical. In the future, the `is_trusted` property will +be deprecated and replaced by the `is_automated` property. + +> **Note**: +> The response keys have changed from API v1.6 to reflect the JSON +> sent by the registry server to the docker daemon's request. + +**Example request**: + + GET /images/search?term=sshd HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "star_count": 12, + "is_official": false, + "name": "wma55/u1210sshd", + "is_trusted": false, + "is_automated": false, + "description": "", + }, + { + "star_count": 10, + "is_official": false, + "name": "jdswinbank/sshd", + "is_trusted": false, + "is_automated": false, + "description": "", + }, + { + "star_count": 18, + "is_official": false, + "name": "vgauthier/sshd", + "is_trusted": false, + "is_automated": false, + "description": "", + } + ... + ] + +Query Parameters: + +- **term** – term to search + +Status Codes: + +- **200** – no error +- **500** – server error + +## 2.3 Misc + +### Check auth configuration + +`POST /auth` + +Get the default username and email + +**Example request**: + + POST /auth HTTP/1.1 + Content-Type: application/json + + { + "username":" hannibal", + "password: "xxxx", + "email": "hannibal@a-team.com", + "serveraddress": "https://index.docker.io/v1/" + } + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – no error +- **204** – no error +- **500** – server error + +### Display system-wide information + +`GET /info` + +Display system-wide information + +**Example request**: + + GET /info HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Containers": 11, + "CpuCfsPeriod": true, + "CpuCfsQuota": true, + "Debug": false, + "DockerRootDir": "/var/lib/docker", + "Driver": "btrfs", + "DriverStatus": [[""]], + "ExecutionDriver": "native-0.1", + "ExperimentalBuild": false, + "HttpProxy": "http://test:test@localhost:8080", + "HttpsProxy": "https://test:test@localhost:8080", + "ID": "7TRN:IPZB:QYBB:VPBQ:UMPP:KARE:6ZNR:XE6T:7EWV:PKF4:ZOJD:TPYS", + "IPv4Forwarding": true, + "Images": 16, + "IndexServerAddress": "https://index.docker.io/v1/", + "InitPath": "/usr/bin/docker", + "InitSha1": "", + "KernelVersion": "3.12.0-1-amd64", + "Labels": [ + "storage=ssd" + ], + "MemTotal": 2099236864, + "MemoryLimit": true, + "NCPU": 1, + "NEventsListener": 0, + "NFd": 11, + "NGoroutines": 21, + "Name": "prod-server-42", + "NoProxy": "9.81.1.160", + "OomKillDisable": true, + "OperatingSystem": "Boot2Docker", + "RegistryConfig": { + "IndexConfigs": { + "docker.io": { + "Mirrors": null, + "Name": "docker.io", + "Official": true, + "Secure": true + } + }, + "InsecureRegistryCIDRs": [ + "127.0.0.0/8" + ] + }, + "SwapLimit": false, + "SystemTime": "2015-03-10T11:11:23.730591467-07:00" + } + +Status Codes: + +- **200** – no error +- **500** – server error + +### Show the docker version information + +`GET /version` + +Show the docker version information + +**Example request**: + + GET /version HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Version": "1.5.0", + "Os": "linux", + "KernelVersion": "3.18.5-tinycore64", + "GoVersion": "go1.4.1", + "GitCommit": "a8a31ef", + "Arch": "amd64", + "ApiVersion": "1.19" + } + +Status Codes: + +- **200** – no error +- **500** – server error + +### Ping the docker server + +`GET /_ping` + +Ping the docker server + +**Example request**: + + GET /_ping HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: text/plain + + OK + +Status Codes: + +- **200** - no error +- **500** - server error + +### Create a new image from a container's changes + +`POST /commit` + +Create a new image from a container's changes + +**Example request**: + + POST /commit?container=44c004db4b17&comment=message&repo=myrepo HTTP/1.1 + Content-Type: application/json + + { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "PortSpecs": null, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "date" + ], + "Volumes": { + "/tmp": {} + }, + "Labels": { + "key1": "value1", + "key2": "value2" + }, + "WorkingDir": "", + "NetworkDisabled": false, + "ExposedPorts": { + "22/tcp": {} + } + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + {"Id": "596069db4bf5"} + +Json Parameters: + +- **config** - the container's configuration + +Query Parameters: + +- **container** – source container +- **repo** – repository +- **tag** – tag +- **comment** – commit message +- **author** – author (e.g., "John Hannibal Smith + <[hannibal@a-team.com](mailto:hannibal%40a-team.com)>") + +Status Codes: + +- **201** – no error +- **404** – no such container +- **500** – server error + +### Monitor Docker's events + +`GET /events` + +Get container events from docker, either in real time via streaming, or via +polling (using since). + +Docker containers report the following events: + + attach, commit, copy, create, destroy, die, exec_create, exec_start, export, kill, oom, pause, rename, resize, restart, start, stop, top, unpause + +and Docker images report: + + untag, delete + +**Example request**: + + GET /events?since=1374067924 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "create", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067924} + {"status": "start", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067924} + {"status": "stop", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067966} + {"status": "destroy", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067970} + +Query Parameters: + +- **since** – Timestamp used for polling +- **until** – Timestamp used for polling +- **filters** – A json encoded value of the filters (a map[string][]string) to process on the event list. Available filters: + - `event=`; -- event to filter + - `image=`; -- image to filter + - `container=`; -- container to filter + +Status Codes: + +- **200** – no error +- **500** – server error + +### Get a tarball containing all images in a repository + +`GET /images/(name)/get` + +Get a tarball containing all images and metadata for the repository specified +by `name`. + +If `name` is a specific name and tag (e.g. ubuntu:latest), then only that image +(and its parents) are returned. If `name` is an image ID, similarly only that +image (and its parents) are returned, but with the exclusion of the +'repositories' file in the tarball, as there were no image names referenced. + +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + GET /images/ubuntu/get + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + Binary data stream + +Status Codes: + +- **200** – no error +- **500** – server error + +### Get a tarball containing all images. + +`GET /images/get` + +Get a tarball containing all images and metadata for one or more repositories. + +For each value of the `names` parameter: if it is a specific name and tag (e.g. +`ubuntu:latest`), then only that image (and its parents) are returned; if it is +an image ID, similarly only that image (and its parents) are returned and there +would be no names referenced in the 'repositories' file for this image ID. + +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + GET /images/get?names=myname%2Fmyapp%3Alatest&names=busybox + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + Binary data stream + +Status Codes: + +- **200** – no error +- **500** – server error + +### Load a tarball with a set of images and tags into docker + +`POST /images/load` + +Load a set of images and tags into a Docker repository. +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + POST /images/load + + Tarball in body + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – no error +- **500** – server error + +### Image tarball format + +An image tarball contains one directory per image layer (named using its long ID), +each containing these files: + +- `VERSION`: currently `1.0` - the file format version +- `json`: detailed layer information, similar to `docker inspect layer_id` +- `layer.tar`: A tarfile containing the filesystem changes in this layer + +The `layer.tar` file contains `aufs` style `.wh..wh.aufs` files and directories +for storing attribute changes and deletions. + +If the tarball defines a repository, the tarball should also include a `repositories` file at +the root that contains a list of repository and tag names mapped to layer IDs. + +``` +{"hello-world": + {"latest": "565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} +} +``` + +### Exec Create + +`POST /containers/(id or name)/exec` + +Sets up an exec instance in a running container `id` + +**Example request**: + + POST /containers/e90e34656806/exec HTTP/1.1 + Content-Type: application/json + + { + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "Cmd": [ + "date" + ], + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "Id": "f90e34656806", + "Warnings":[] + } + +Json Parameters: + +- **AttachStdin** - Boolean value, attaches to `stdin` of the `exec` command. +- **AttachStdout** - Boolean value, attaches to `stdout` of the `exec` command. +- **AttachStderr** - Boolean value, attaches to `stderr` of the `exec` command. +- **Tty** - Boolean value to allocate a pseudo-TTY. +- **Cmd** - Command to run specified as a string or an array of strings. + + +Status Codes: + +- **201** – no error +- **404** – no such container + +### Exec Start + +`POST /exec/(id)/start` + +Starts a previously set up `exec` instance `id`. If `detach` is true, this API +returns after starting the `exec` command. Otherwise, this API sets up an +interactive session with the `exec` command. + +**Example request**: + + POST /exec/e90e34656806/start HTTP/1.1 + Content-Type: application/json + + { + "Detach": false, + "Tty": false, + } + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + +Json Parameters: + +- **Detach** - Detach from the `exec` command. +- **Tty** - Boolean value to allocate a pseudo-TTY. + +Status Codes: + +- **200** – no error +- **404** – no such exec instance + + **Stream details**: + Similar to the stream behavior of `POST /containers/(id or name)/attach` API + +### Exec Resize + +`POST /exec/(id)/resize` + +Resizes the `tty` session used by the `exec` command `id`. +This API is valid only if `tty` was specified as part of creating and starting the `exec` command. + +**Example request**: + + POST /exec/e90e34656806/resize HTTP/1.1 + Content-Type: text/plain + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: text/plain + +Query Parameters: + +- **h** – height of `tty` session +- **w** – width + +Status Codes: + +- **201** – no error +- **404** – no such exec instance + +### Exec Inspect + +`GET /exec/(id)/json` + +Return low-level information about the `exec` command `id`. + +**Example request**: + + GET /exec/11fb006128e8ceb3942e7c58d77750f24210e35f879dd204ac975c184b820b39/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: plain/text + + { + "ID" : "11fb006128e8ceb3942e7c58d77750f24210e35f879dd204ac975c184b820b39", + "Running" : false, + "ExitCode" : 2, + "ProcessConfig" : { + "privileged" : false, + "user" : "", + "tty" : false, + "entrypoint" : "sh", + "arguments" : [ + "-c", + "exit 2" + ] + }, + "OpenStdin" : false, + "OpenStderr" : false, + "OpenStdout" : false, + "Container" : { + "State" : { + "Running" : true, + "Paused" : false, + "Restarting" : false, + "OOMKilled" : false, + "Pid" : 3650, + "ExitCode" : 0, + "Error" : "", + "StartedAt" : "2014-11-17T22:26:03.717657531Z", + "FinishedAt" : "0001-01-01T00:00:00Z" + }, + "ID" : "8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c", + "Created" : "2014-11-17T22:26:03.626304998Z", + "Path" : "date", + "Args" : [], + "Config" : { + "Hostname" : "8f177a186b97", + "Domainname" : "", + "User" : "", + "AttachStdin" : false, + "AttachStdout" : false, + "AttachStderr" : false, + "PortSpecs": null, + "ExposedPorts" : null, + "Tty" : false, + "OpenStdin" : false, + "StdinOnce" : false, + "Env" : [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ], + "Cmd" : [ + "date" + ], + "Image" : "ubuntu", + "Volumes" : null, + "WorkingDir" : "", + "Entrypoint" : null, + "NetworkDisabled" : false, + "MacAddress" : "", + "OnBuild" : null, + "SecurityOpt" : null + }, + "Image" : "5506de2b643be1e6febbf3b8a240760c6843244c41e12aa2f60ccbb7153d17f5", + "NetworkSettings" : { + "IPAddress" : "172.17.0.2", + "IPPrefixLen" : 16, + "MacAddress" : "02:42:ac:11:00:02", + "Gateway" : "172.17.42.1", + "Bridge" : "docker0", + "PortMapping" : null, + "Ports" : {} + }, + "ResolvConfPath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/resolv.conf", + "HostnamePath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/hostname", + "HostsPath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/hosts", + "LogPath": "/var/lib/docker/containers/1eb5fabf5a03807136561b3c00adcd2992b535d624d5e18b6cdc6a6844d9767b/1eb5fabf5a03807136561b3c00adcd2992b535d624d5e18b6cdc6a6844d9767b-json.log", + "Name" : "/test", + "Driver" : "aufs", + "ExecDriver" : "native-0.2", + "MountLabel" : "", + "ProcessLabel" : "", + "AppArmorProfile" : "", + "RestartCount" : 0, + "Volumes" : {}, + "VolumesRW" : {} + } + } + +Status Codes: + +- **200** – no error +- **404** – no such exec instance +- **500** - server error + +# 3. Going further + +## 3.1 Inside `docker run` + +As an example, the `docker run` command line makes the following API calls: + +- Create the container + +- If the status code is 404, it means the image doesn't exist: + - Try to pull it. + - Then, retry to create the container. + +- Start the container. + +- If you are not in detached mode: +- Attach to the container, using `logs=1` (to have `stdout` and + `stderr` from the container's start) and `stream=1` + +- If in detached mode or only `stdin` is attached, display the container's id. + +## 3.2 Hijacking + +In this version of the API, `/attach`, uses hijacking to transport `stdin`, +`stdout`, and `stderr` on the same socket. + +To hint potential proxies about connection hijacking, Docker client sends +connection upgrade headers similarly to websocket. + + Upgrade: tcp + Connection: Upgrade + +When Docker daemon detects the `Upgrade` header, it switches its status code +from **200 OK** to **101 UPGRADED** and resends the same headers. + + +## 3.3 CORS Requests + +To set cross origin requests to the remote api please give values to +`--api-cors-header` when running Docker in daemon mode. Set * (asterisk) allows all, +default or blank means CORS disabled + + $ docker -d -H="192.168.1.9:2375" --api-cors-header="http://foo.bar" diff --git a/docs/reference/api/docker_remote_api_v1.20.md b/docs/reference/api/docker_remote_api_v1.20.md new file mode 100644 index 00000000..dd47ff7f --- /dev/null +++ b/docs/reference/api/docker_remote_api_v1.20.md @@ -0,0 +1,2346 @@ + + +# Docker Remote API v1.20 + +## 1. Brief introduction + + - The Remote API has replaced `rcli`. + - The daemon listens on `unix:///var/run/docker.sock` but you can + [Bind Docker to another host/port or a Unix socket](../../quickstart.md#bind-docker-to-another-host-port-or-a-unix-socket). + - The API tends to be REST. However, for some complex commands, like `attach` + or `pull`, the HTTP connection is hijacked to transport `stdout`, + `stdin` and `stderr`. + - When the client API version is newer than the daemon's, these calls return an HTTP + `400 Bad Request` error message. + +# 2. Endpoints + +## 2.1 Containers + +### List containers + +`GET /containers/json` + +List containers + +**Example request**: + + GET /containers/json?all=1&before=8dfafdbc3a40&size=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "8dfafdbc3a40", + "Names":["/boring_feynman"], + "Image": "ubuntu:latest", + "Command": "echo 1", + "Created": 1367854155, + "Status": "Exit 0", + "Ports": [{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "SizeRw": 12288, + "SizeRootFs": 0 + }, + { + "Id": "9cd87474be90", + "Names":["/coolName"], + "Image": "ubuntu:latest", + "Command": "echo 222222", + "Created": 1367854155, + "Status": "Exit 0", + "Ports": [], + "Labels": {}, + "SizeRw": 12288, + "SizeRootFs": 0 + }, + { + "Id": "3176a2479c92", + "Names":["/sleepy_dog"], + "Image": "ubuntu:latest", + "Command": "echo 3333333333333333", + "Created": 1367854154, + "Status": "Exit 0", + "Ports":[], + "Labels": {}, + "SizeRw":12288, + "SizeRootFs":0 + }, + { + "Id": "4cb07b47f9fb", + "Names":["/running_cat"], + "Image": "ubuntu:latest", + "Command": "echo 444444444444444444444444444444444", + "Created": 1367854152, + "Status": "Exit 0", + "Ports": [], + "Labels": {}, + "SizeRw": 12288, + "SizeRootFs": 0 + } + ] + +Query Parameters: + +- **all** – 1/True/true or 0/False/false, Show all containers. + Only running containers are shown by default (i.e., this defaults to false) +- **limit** – Show `limit` last created + containers, include non-running ones. +- **since** – Show only containers created since Id, include + non-running ones. +- **before** – Show only containers created before Id, include + non-running ones. +- **size** – 1/True/true or 0/False/false, Show the containers + sizes +- **filters** - a JSON encoded value of the filters (a `map[string][]string`) to process on the containers list. Available filters: + - `exited=`; -- containers with exit code of `` ; + - `status=`(`created`|`restarting`|`running`|`paused`|`exited`) + - `label=key` or `label="key=value"` of a container label + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **500** – server error + +### Create a container + +`POST /containers/create` + +Create a container + +**Example request**: + + POST /containers/create HTTP/1.1 + Content-Type: application/json + + { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "FOO=bar", + "BAZ=quux" + ], + "Cmd": [ + "date" + ], + "Entrypoint": "", + "Image": "ubuntu", + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "Volumes": { + "/volumes/data": {} + }, + "WorkingDir": "", + "NetworkDisabled": false, + "MacAddress": "12:34:56:78:9a:bc", + "ExposedPorts": { + "22/tcp": {} + }, + "HostConfig": { + "Binds": ["/tmp:/tmp"], + "Links": ["redis3:redis"], + "LxcConf": {"lxc.utsname":"docker"}, + "Memory": 0, + "MemorySwap": 0, + "CpuShares": 512, + "CpuPeriod": 100000, + "CpuQuota": 50000, + "CpusetCpus": "0,1", + "CpusetMems": "0,1", + "BlkioWeight": 300, + "MemorySwappiness": 60, + "OomKillDisable": false, + "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] }, + "PublishAllPorts": false, + "Privileged": false, + "ReadonlyRootfs": false, + "Dns": ["8.8.8.8"], + "DnsSearch": [""], + "ExtraHosts": null, + "VolumesFrom": ["parent", "other:ro"], + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"], + "GroupAdd": ["newgroup"], + "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 }, + "NetworkMode": "bridge", + "Devices": [], + "Ulimits": [{}], + "LogConfig": { "Type": "json-file", "Config": {} }, + "SecurityOpt": [], + "CgroupParent": "" + } + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "Id":"e90e34656806", + "Warnings":[] + } + +Json Parameters: + +- **Hostname** - A string value containing the hostname to use for the + container. +- **Domainname** - A string value containing the domain name to use + for the container. +- **User** - A string value specifying the user inside the container. +- **Memory** - Memory limit in bytes. +- **MemorySwap** - Total memory limit (memory + swap); set `-1` to enable unlimited swap. + You must use this with `memory` and make the swap value larger than `memory`. +- **CpuShares** - An integer value containing the container's CPU Shares + (ie. the relative weight vs other containers). +- **CpuPeriod** - The length of a CPU period in microseconds. +- **CpuQuota** - Microseconds of CPU time that the container can get in a CPU period. +- **Cpuset** - Deprecated please don't use. Use `CpusetCpus` instead. +- **CpusetCpus** - String value containing the `cgroups CpusetCpus` to use. +- **CpusetMems** - Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems. +- **BlkioWeight** - Block IO weight (relative weight) accepts a weight value between 10 and 1000. +- **MemorySwappiness** - Tune a container's memory swappiness behavior. Accepts an integer between 0 and 100. +- **OomKillDisable** - Boolean value, whether to disable OOM Killer for the container or not. +- **AttachStdin** - Boolean value, attaches to `stdin`. +- **AttachStdout** - Boolean value, attaches to `stdout`. +- **AttachStderr** - Boolean value, attaches to `stderr`. +- **Tty** - Boolean value, Attach standard streams to a `tty`, including `stdin` if it is not closed. +- **OpenStdin** - Boolean value, opens stdin, +- **StdinOnce** - Boolean value, close `stdin` after the 1 attached client disconnects. +- **Env** - A list of environment variables in the form of `["VAR=value"[,"VAR2=value2"]]` +- **Labels** - Adds a map of labels to a container. To specify a map: `{"key":"value"[,"key2":"value2"]}` +- **Cmd** - Command to run specified as a string or an array of strings. +- **Entrypoint** - Set the entry point for the container as a string or an array + of strings. +- **Image** - A string specifying the image name to use for the container. +- **Volumes** - An object mapping mount point paths (strings) inside the + container to empty objects. +- **WorkingDir** - A string specifying the working directory for commands to + run in. +- **NetworkDisabled** - Boolean value, when true disables networking for the + container +- **ExposedPorts** - An object mapping ports to an empty object in the form of: + `"ExposedPorts": { "/: {}" }` +- **HostConfig** + - **Binds** – A list of volume bindings for this container. Each volume binding is a string in one of these forms: + + `container_path` to create a new volume for the container + + `host_path:container_path` to bind-mount a host path into the container + + `host_path:container_path:ro` to make the bind-mount read-only inside the container. + - **Links** - A list of links for the container. Each link entry should be + in the form of `container_name:alias`. + - **LxcConf** - LXC specific configurations. These configurations only + work when using the `lxc` execution driver. + - **PortBindings** - A map of exposed container ports and the host port they + should map to. A JSON object in the form + `{ /: [{ "HostPort": "" }] }` + Take note that `port` is specified as a string and not an integer value. + - **PublishAllPorts** - Allocates a random host port for all of a container's + exposed ports. Specified as a boolean value. + - **Privileged** - Gives the container full access to the host. Specified as + a boolean value. + - **ReadonlyRootfs** - Mount the container's root filesystem as read only. + Specified as a boolean value. + - **Dns** - A list of DNS servers for the container to use. + - **DnsSearch** - A list of DNS search domains + - **ExtraHosts** - A list of hostnames/IP mappings to add to the + container's `/etc/hosts` file. Specified in the form `["hostname:IP"]`. + - **VolumesFrom** - A list of volumes to inherit from another container. + Specified in the form `[:]` + - **CapAdd** - A list of kernel capabilities to add to the container. + - **Capdrop** - A list of kernel capabilities to drop from the container. + - **GroupAdd** - A list of additional groups that the container process will run as + - **RestartPolicy** – The behavior to apply when the container exits. The + value is an object with a `Name` property of either `"always"` to + always restart or `"on-failure"` to restart only when the container + exit code is non-zero. If `on-failure` is used, `MaximumRetryCount` + controls the number of times to retry before giving up. + The default is not to restart. (optional) + An ever increasing delay (double the previous delay, starting at 100mS) + is added before each restart to prevent flooding the server. + - **NetworkMode** - Sets the networking mode for the container. Supported + values are: `bridge`, `host`, `none`, and `container:` + - **Devices** - A list of devices to add to the container specified as a JSON object in the + form + `{ "PathOnHost": "/dev/deviceName", "PathInContainer": "/dev/deviceName", "CgroupPermissions": "mrw"}` + - **Ulimits** - A list of ulimits to set in the container, specified as + `{ "Name": , "Soft": , "Hard": }`, for example: + `Ulimits: { "Name": "nofile", "Soft": 1024, "Hard": 2048 }` + - **SecurityOpt**: A list of string values to customize labels for MLS + systems, such as SELinux. + - **LogConfig** - Log configuration for the container, specified as a JSON object in the form + `{ "Type": "", "Config": {"key1": "val1"}}`. + Available types: `json-file`, `syslog`, `journald`, `gelf`, `none`. + `json-file` logging driver. + - **CgroupParent** - Path to `cgroups` under which the container's `cgroup` is created. If the path is not absolute, the path is considered to be relative to the `cgroups` path of the init process. Cgroups are created if they do not already exist. + +Query Parameters: + +- **name** – Assign the specified name to the container. Must + match `/?[a-zA-Z0-9_-]+`. + +Status Codes: + +- **201** – no error +- **404** – no such container +- **406** – impossible to attach (container not running) +- **500** – server error + +### Inspect a container + +`GET /containers/(id or name)/json` + +Return low-level information on the container `id` + + +**Example request**: + + GET /containers/4fa6e0f0c678/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "AppArmorProfile": "", + "Args": [ + "-c", + "exit 9" + ], + "Config": { + "AttachStderr": true, + "AttachStdin": false, + "AttachStdout": true, + "Cmd": [ + "/bin/sh", + "-c", + "exit 9" + ], + "Domainname": "", + "Entrypoint": null, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "ExposedPorts": null, + "Hostname": "ba033ac44011", + "Image": "ubuntu", + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "MacAddress": "", + "NetworkDisabled": false, + "OnBuild": null, + "OpenStdin": false, + "StdinOnce": false, + "Tty": false, + "User": "", + "Volumes": null, + "WorkingDir": "" + }, + "Created": "2015-01-06T15:47:31.485331387Z", + "Driver": "devicemapper", + "ExecDriver": "native-0.2", + "ExecIDs": null, + "HostConfig": { + "Binds": null, + "BlkioWeight": 0, + "CapAdd": null, + "CapDrop": null, + "ContainerIDFile": "", + "CpusetCpus": "", + "CpusetMems": "", + "CpuShares": 0, + "CpuPeriod": 100000, + "Devices": [], + "Dns": null, + "DnsSearch": null, + "ExtraHosts": null, + "IpcMode": "", + "Links": null, + "LxcConf": [], + "Memory": 0, + "MemorySwap": 0, + "OomKillDisable": false, + "NetworkMode": "bridge", + "PortBindings": {}, + "Privileged": false, + "ReadonlyRootfs": false, + "PublishAllPorts": false, + "RestartPolicy": { + "MaximumRetryCount": 2, + "Name": "on-failure" + }, + "LogConfig": { + "Config": null, + "Type": "json-file" + }, + "SecurityOpt": null, + "VolumesFrom": null, + "Ulimits": [{}] + }, + "HostnamePath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/hostname", + "HostsPath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/hosts", + "LogPath": "/var/lib/docker/containers/1eb5fabf5a03807136561b3c00adcd2992b535d624d5e18b6cdc6a6844d9767b/1eb5fabf5a03807136561b3c00adcd2992b535d624d5e18b6cdc6a6844d9767b-json.log", + "Id": "ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39", + "Image": "04c5d3b7b0656168630d3ba35d8889bd0e9caafcaeb3004d2bfbc47e7c5d35d2", + "MountLabel": "", + "Name": "/boring_euclid", + "NetworkSettings": { + "Bridge": "", + "Gateway": "", + "IPAddress": "", + "IPPrefixLen": 0, + "MacAddress": "", + "PortMapping": null, + "Ports": null + }, + "Path": "/bin/sh", + "ProcessLabel": "", + "ResolvConfPath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/resolv.conf", + "RestartCount": 1, + "State": { + "Error": "", + "ExitCode": 9, + "FinishedAt": "2015-01-06T15:47:32.080254511Z", + "OOMKilled": false, + "Paused": false, + "Pid": 0, + "Restarting": false, + "Running": false, + "StartedAt": "2015-01-06T15:47:32.072697474Z" + }, + "Mounts": [ + { + "Source": "/data", + "Destination": "/data", + "Mode": "ro,Z", + "RW": false + } + ] + } + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### List processes running inside a container + +`GET /containers/(id or name)/top` + +List processes running inside the container `id`. On Unix systems this +is done by running the `ps` command. This endpoint is not +supported on Windows. + +**Example request**: + + GET /containers/4fa6e0f0c678/top HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Titles" : [ + "UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD" + ], + "Processes" : [ + [ + "root", "13642", "882", "0", "17:03", "pts/0", "00:00:00", "/bin/bash" + ], + [ + "root", "13735", "13642", "0", "17:06", "pts/0", "00:00:00", "sleep 10" + ] + ] + } + +**Example request**: + + GET /containers/4fa6e0f0c678/top?ps_args=aux HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Titles" : [ + "USER","PID","%CPU","%MEM","VSZ","RSS","TTY","STAT","START","TIME","COMMAND" + ] + "Processes" : [ + [ + "root","13642","0.0","0.1","18172","3184","pts/0","Ss","17:03","0:00","/bin/bash" + ], + [ + "root","13895","0.0","0.0","4348","692","pts/0","S+","17:15","0:00","sleep 10" + ] + ], + } + +Query Parameters: + +- **ps_args** – `ps` arguments to use (e.g., `aux`), defaults to `-ef` + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Get container logs + +`GET /containers/(id or name)/logs` + +Get `stdout` and `stderr` logs from the container ``id`` + +> **Note**: +> This endpoint works only for containers with the `json-file` or `journald` logging drivers. + +**Example request**: + + GET /containers/4fa6e0f0c678/logs?stderr=1&stdout=1×tamps=1&follow=1&tail=10&since=1428990821 HTTP/1.1 + +**Example response**: + + HTTP/1.1 101 UPGRADED + Content-Type: application/vnd.docker.raw-stream + Connection: Upgrade + Upgrade: tcp + + {{ STREAM }} + +Query Parameters: + +- **follow** – 1/True/true or 0/False/false, return stream. Default `false`. +- **stdout** – 1/True/true or 0/False/false, show `stdout` log. Default `false`. +- **stderr** – 1/True/true or 0/False/false, show `stderr` log. Default `false`. +- **since** – UNIX timestamp (integer) to filter logs. Specifying a timestamp + will only output log-entries since that timestamp. Default: 0 (unfiltered) +- **timestamps** – 1/True/true or 0/False/false, print timestamps for + every log line. Default `false`. +- **tail** – Output specified number of lines at the end of logs: `all` or ``. Default all. + +Status Codes: + +- **101** – no error, hints proxy about hijacking +- **200** – no error, no upgrade header found +- **404** – no such container +- **500** – server error + +### Inspect changes on a container's filesystem + +`GET /containers/(id or name)/changes` + +Inspect changes on container `id`'s filesystem + +**Example request**: + + GET /containers/4fa6e0f0c678/changes HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Path": "/dev", + "Kind": 0 + }, + { + "Path": "/dev/kmsg", + "Kind": 1 + }, + { + "Path": "/test", + "Kind": 1 + } + ] + +Values for `Kind`: + +- `0`: Modify +- `1`: Add +- `2`: Delete + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Export a container + +`GET /containers/(id or name)/export` + +Export the contents of container `id` + +**Example request**: + + GET /containers/4fa6e0f0c678/export HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/octet-stream + + {{ TAR STREAM }} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Get container stats based on resource usage + +`GET /containers/(id or name)/stats` + +This endpoint returns a live stream of a container's resource usage statistics. + +**Example request**: + + GET /containers/redis1/stats HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "read" : "2015-01-08T22:57:31.547920715Z", + "network" : { + "rx_dropped" : 0, + "rx_bytes" : 648, + "rx_errors" : 0, + "tx_packets" : 8, + "tx_dropped" : 0, + "rx_packets" : 8, + "tx_errors" : 0, + "tx_bytes" : 648 + }, + "memory_stats" : { + "stats" : { + "total_pgmajfault" : 0, + "cache" : 0, + "mapped_file" : 0, + "total_inactive_file" : 0, + "pgpgout" : 414, + "rss" : 6537216, + "total_mapped_file" : 0, + "writeback" : 0, + "unevictable" : 0, + "pgpgin" : 477, + "total_unevictable" : 0, + "pgmajfault" : 0, + "total_rss" : 6537216, + "total_rss_huge" : 6291456, + "total_writeback" : 0, + "total_inactive_anon" : 0, + "rss_huge" : 6291456, + "hierarchical_memory_limit" : 67108864, + "total_pgfault" : 964, + "total_active_file" : 0, + "active_anon" : 6537216, + "total_active_anon" : 6537216, + "total_pgpgout" : 414, + "total_cache" : 0, + "inactive_anon" : 0, + "active_file" : 0, + "pgfault" : 964, + "inactive_file" : 0, + "total_pgpgin" : 477 + }, + "max_usage" : 6651904, + "usage" : 6537216, + "failcnt" : 0, + "limit" : 67108864 + }, + "blkio_stats" : {}, + "cpu_stats" : { + "cpu_usage" : { + "percpu_usage" : [ + 8646879, + 24472255, + 36438778, + 30657443 + ], + "usage_in_usermode" : 50000000, + "total_usage" : 100215355, + "usage_in_kernelmode" : 30000000 + }, + "system_cpu_usage" : 739306590000000, + "throttling_data" : {"periods":0,"throttled_periods":0,"throttled_time":0} + }, + "precpu_stats" : { + "cpu_usage" : { + "percpu_usage" : [ + 8646879, + 24350896, + 36438778, + 30657443 + ], + "usage_in_usermode" : 50000000, + "total_usage" : 100093996, + "usage_in_kernelmode" : 30000000 + }, + "system_cpu_usage" : 9492140000000, + "throttling_data" : {"periods":0,"throttled_periods":0,"throttled_time":0} + } + } + +The precpu_stats is the cpu statistic of last read, which is used for calculating the cpu usage percent. It is not the exact copy of the “cpu_stats” field. + +Query Parameters: + +- **stream** – 1/True/true or 0/False/false, pull stats once then disconnect. Default `true`. + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Resize a container TTY + +`POST /containers/(id or name)/resize?h=&w=` + +Resize the TTY for container with `id`. You must restart the container for the resize to take effect. + +**Example request**: + + POST /containers/4fa6e0f0c678/resize?h=40&w=80 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Length: 0 + Content-Type: text/plain; charset=utf-8 + +Status Codes: + +- **200** – no error +- **404** – No such container +- **500** – Cannot resize container + +### Start a container + +`POST /containers/(id or name)/start` + +Start the container `id` + +> **Note**: +> For backwards compatibility, this endpoint accepts a `HostConfig` as JSON-encoded request body. +> See [create a container](#create-a-container) for details. + +**Example request**: + + POST /containers/e90e34656806/start HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **304** – container already started +- **404** – no such container +- **500** – server error + +### Stop a container + +`POST /containers/(id or name)/stop` + +Stop the container `id` + +**Example request**: + + POST /containers/e90e34656806/stop?t=5 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **t** – number of seconds to wait before killing the container + +Status Codes: + +- **204** – no error +- **304** – container already stopped +- **404** – no such container +- **500** – server error + +### Restart a container + +`POST /containers/(id or name)/restart` + +Restart the container `id` + +**Example request**: + + POST /containers/e90e34656806/restart?t=5 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **t** – number of seconds to wait before killing the container + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Kill a container + +`POST /containers/(id or name)/kill` + +Kill the container `id` + +**Example request**: + + POST /containers/e90e34656806/kill HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters + +- **signal** - Signal to send to the container: integer or string like `SIGINT`. + When not set, `SIGKILL` is assumed and the call waits for the container to exit. + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Rename a container + +`POST /containers/(id or name)/rename` + +Rename the container `id` to a `new_name` + +**Example request**: + + POST /containers/e90e34656806/rename?name=new_name HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **name** – new name for the container + +Status Codes: + +- **204** – no error +- **404** – no such container +- **409** - conflict name already assigned +- **500** – server error + +### Pause a container + +`POST /containers/(id or name)/pause` + +Pause the container `id` + +**Example request**: + + POST /containers/e90e34656806/pause HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Unpause a container + +`POST /containers/(id or name)/unpause` + +Unpause the container `id` + +**Example request**: + + POST /containers/e90e34656806/unpause HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Attach to a container + +`POST /containers/(id or name)/attach` + +Attach to the container `id` + +**Example request**: + + POST /containers/16253994b7c4/attach?logs=1&stream=0&stdout=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 101 UPGRADED + Content-Type: application/vnd.docker.raw-stream + Connection: Upgrade + Upgrade: tcp + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default `false`. +- **stream** – 1/True/true or 0/False/false, return stream. + Default `false`. +- **stdin** – 1/True/true or 0/False/false, if `stream=true`, attach + to `stdin`. Default `false`. +- **stdout** – 1/True/true or 0/False/false, if `logs=true`, return + `stdout` log, if `stream=true`, attach to `stdout`. Default `false`. +- **stderr** – 1/True/true or 0/False/false, if `logs=true`, return + `stderr` log, if `stream=true`, attach to `stderr`. Default `false`. + +Status Codes: + +- **101** – no error, hints proxy about hijacking +- **200** – no error, no upgrade header found +- **400** – bad parameter +- **404** – no such container +- **500** – server error + + **Stream details**: + + When using the TTY setting is enabled in + [`POST /containers/create` + ](#create-a-container), + the stream is the raw data from the process PTY and client's `stdin`. + When the TTY is disabled, then the stream is multiplexed to separate + `stdout` and `stderr`. + + The format is a **Header** and a **Payload** (frame). + + **HEADER** + + The header contains the information which the stream writes (`stdout` or + `stderr`). It also contains the size of the associated frame encoded in the + last four bytes (`uint32`). + + It is encoded on the first eight bytes like this: + + header := [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4} + + `STREAM_TYPE` can be: + +- 0: `stdin` (is written on `stdout`) +- 1: `stdout` +- 2: `stderr` + + `SIZE1, SIZE2, SIZE3, SIZE4` are the four bytes of + the `uint32` size encoded as big endian. + + **PAYLOAD** + + The payload is the raw stream. + + **IMPLEMENTATION** + + The simplest way to implement the Attach protocol is the following: + + 1. Read eight bytes. + 2. Choose `stdout` or `stderr` depending on the first byte. + 3. Extract the frame size from the last four bytes. + 4. Read the extracted size and output it on the correct output. + 5. Goto 1. + +### Attach to a container (websocket) + +`GET /containers/(id or name)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default `false`. +- **stream** – 1/True/true or 0/False/false, return stream. + Default `false`. +- **stdin** – 1/True/true or 0/False/false, if `stream=true`, attach + to `stdin`. Default `false`. +- **stdout** – 1/True/true or 0/False/false, if `logs=true`, return + `stdout` log, if `stream=true`, attach to `stdout`. Default `false`. +- **stderr** – 1/True/true or 0/False/false, if `logs=true`, return + `stderr` log, if `stream=true`, attach to `stderr`. Default `false`. + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + +### Wait a container + +`POST /containers/(id or name)/wait` + +Block until container `id` stops, then returns the exit code + +**Example request**: + + POST /containers/16253994b7c4/wait HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"StatusCode": 0} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Remove a container + +`DELETE /containers/(id or name)` + +Remove the container `id` from the filesystem + +**Example request**: + + DELETE /containers/16253994b7c4?v=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **v** – 1/True/true or 0/False/false, Remove the volumes + associated to the container. Default `false`. +- **force** - 1/True/true or 0/False/false, Kill then remove the container. + Default `false`. + +Status Codes: + +- **204** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + +### Copy files or folders from a container + +`POST /containers/(id or name)/copy` + +Copy files or folders of container `id` + +**Deprecated** in favor of the `archive` endpoint below. + +**Example request**: + + POST /containers/4fa6e0f0c678/copy HTTP/1.1 + Content-Type: application/json + + { + "Resource": "test.txt" + } + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + {{ TAR STREAM }} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Retrieving information about files and folders in a container + +`HEAD /containers/(id or name)/archive` + +See the description of the `X-Docker-Container-Path-Stat` header in the +following section. + +### Get an archive of a filesystem resource in a container + +`GET /containers/(id or name)/archive` + +Get an tar archive of a resource in the filesystem of container `id`. + +Query Parameters: + +- **path** - resource in the container's filesystem to archive. Required. + + If not an absolute path, it is relative to the container's root directory. + The resource specified by **path** must exist. To assert that the resource + is expected to be a directory, **path** should end in `/` or `/.` + (assuming a path separator of `/`). If **path** ends in `/.` then this + indicates that only the contents of the **path** directory should be + copied. A symlink is always resolved to its target. + + **Note**: It is not possible to copy certain system files such as resources + under `/proc`, `/sys`, `/dev`, and mounts created by the user in the + container. + +**Example request**: + + GET /containers/8cce319429b2/archive?path=/root HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + X-Docker-Container-Path-Stat: eyJuYW1lIjoicm9vdCIsInNpemUiOjQwOTYsIm1vZGUiOjIxNDc0ODQwOTYsIm10aW1lIjoiMjAxNC0wMi0yN1QyMDo1MToyM1oiLCJsaW5rVGFyZ2V0IjoiIn0= + + {{ TAR STREAM }} + +On success, a response header `X-Docker-Container-Path-Stat` will be set to a +base64-encoded JSON object containing some filesystem header information about +the archived resource. The above example value would decode to the following +JSON object (whitespace added for readability): + + { + "name": "root", + "size": 4096, + "mode": 2147484096, + "mtime": "2014-02-27T20:51:23Z", + "linkTarget": "" + } + +A `HEAD` request can also be made to this endpoint if only this information is +desired. + +Status Codes: + +- **200** - success, returns archive of copied resource +- **400** - client error, bad parameter, details in JSON response body, one of: + - must specify path parameter (**path** cannot be empty) + - not a directory (**path** was asserted to be a directory but exists as a + file) +- **404** - client error, resource not found, one of: + – no such container (container `id` does not exist) + - no such file or directory (**path** does not exist) +- **500** - server error + +### Extract an archive of files or folders to a directory in a container + +`PUT /containers/(id or name)/archive` + +Upload a tar archive to be extracted to a path in the filesystem of container +`id`. + +Query Parameters: + +- **path** - path to a directory in the container + to extract the archive's contents into. Required. + + If not an absolute path, it is relative to the container's root directory. + The **path** resource must exist. +- **noOverwriteDirNonDir** - If "1", "true", or "True" then it will be an error + if unpacking the given content would cause an existing directory to be + replaced with a non-directory and vice versa. + +**Example request**: + + PUT /containers/8cce319429b2/archive?path=/vol1 HTTP/1.1 + Content-Type: application/x-tar + + {{ TAR STREAM }} + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – the content was extracted successfully +- **400** - client error, bad parameter, details in JSON response body, one of: + - must specify path parameter (**path** cannot be empty) + - not a directory (**path** should be a directory but exists as a file) + - unable to overwrite existing directory with non-directory + (if **noOverwriteDirNonDir**) + - unable to overwrite existing non-directory with directory + (if **noOverwriteDirNonDir**) +- **403** - client error, permission denied, the volume + or container rootfs is marked as read-only. +- **404** - client error, resource not found, one of: + – no such container (container `id` does not exist) + - no such file or directory (**path** resource does not exist) +- **500** – server error + +## 2.2 Images + +### List Images + +`GET /images/json` + +**Example request**: + + GET /images/json?all=0 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "RepoTags": [ + "ubuntu:12.04", + "ubuntu:precise", + "ubuntu:latest" + ], + "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", + "Created": 1365714795, + "Size": 131506275, + "VirtualSize": 131506275, + "Labels": {} + }, + { + "RepoTags": [ + "ubuntu:12.10", + "ubuntu:quantal" + ], + "ParentId": "27cf784147099545", + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Created": 1364102658, + "Size": 24653, + "VirtualSize": 180116135, + "Labels": { + "com.example.version": "v1" + } + } + ] + +**Example request, with digest information**: + + GET /images/json?digests=1 HTTP/1.1 + +**Example response, with digest information**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Created": 1420064636, + "Id": "4986bf8c15363d1c5d15512d5266f8777bfba4974ac56e3270e7760f6f0a8125", + "ParentId": "ea13149945cb6b1e746bf28032f02e9b5a793523481a0a18645fc77ad53c4ea2", + "RepoDigests": [ + "localhost:5000/test/busybox@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf" + ], + "RepoTags": [ + "localhost:5000/test/busybox:latest", + "playdate:latest" + ], + "Size": 0, + "VirtualSize": 2429728, + "Labels": {} + } + ] + +The response shows a single image `Id` associated with two repositories +(`RepoTags`): `localhost:5000/test/busybox`: and `playdate`. A caller can use +either of the `RepoTags` values `localhost:5000/test/busybox:latest` or +`playdate:latest` to reference the image. + +You can also use `RepoDigests` values to reference an image. In this response, +the array has only one reference and that is to the +`localhost:5000/test/busybox` repository; the `playdate` repository has no +digest. You can reference this digest using the value: +`localhost:5000/test/busybox@sha256:cbbf2f9a99b47fc460d...` + +See the `docker run` and `docker build` commands for examples of digest and tag +references on the command line. + +Query Parameters: + +- **all** – 1/True/true or 0/False/false, default false +- **filters** – a JSON encoded value of the filters (a map[string][]string) to process on the images list. Available filters: + - `dangling=true` + - `label=key` or `label="key=value"` of an image label +- **filter** - only return images with the specified name + +### Build image from a Dockerfile + +`POST /build` + +Build an image from a Dockerfile + +**Example request**: + + POST /build HTTP/1.1 + + {{ TAR STREAM }} + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"stream": "Step 1..."} + {"stream": "..."} + {"error": "Error...", "errorDetail": {"code": 123, "message": "Error..."}} + +The input stream must be a `tar` archive compressed with one of the +following algorithms: `identity` (no compression), `gzip`, `bzip2`, `xz`. + +The archive must include a build instructions file, typically called +`Dockerfile` at the archive's root. The `dockerfile` parameter may be +used to specify a different build instructions file. To do this, its value must be +the path to the alternate build instructions file to use. + +The archive may include any number of other files, +which are accessible in the build context (See the [*ADD build +command*](../../reference/builder.md#dockerbuilder)). + +The build is canceled if the client drops the connection by quitting +or being killed. + +Query Parameters: + +- **dockerfile** - Path within the build context to the Dockerfile. This is + ignored if `remote` is specified and points to an individual filename. +- **t** – A repository name (and optionally a tag) to apply to + the resulting image in case of success. +- **remote** – A Git repository URI or HTTP/HTTPS URI build source. If the + URI specifies a filename, the file's contents are placed into a file + called `Dockerfile`. +- **q** – Suppress verbose build output. +- **nocache** – Do not use the cache when building the image. +- **pull** - Attempt to pull the image even if an older image exists locally. +- **rm** - Remove intermediate containers after a successful build (default behavior). +- **forcerm** - Always remove intermediate containers (includes `rm`). +- **memory** - Set memory limit for build. +- **memswap** - Total memory (memory + swap), `-1` to enable unlimited swap. +- **cpushares** - CPU shares (relative weight). +- **cpusetcpus** - CPUs in which to allow execution (e.g., `0-3`, `0,1`). +- **cpuperiod** - The length of a CPU period in microseconds. +- **cpuquota** - Microseconds of CPU time that the container can get in a CPU period. + + Request Headers: + +- **Content-type** – Set to `"application/tar"`. +- **X-Registry-Config** – A base64-url-safe-encoded Registry Auth Config JSON + object with the following structure: + + { + "docker.example.com": { + "username": "janedoe", + "password": "hunter2" + }, + "https://index.docker.io/v1/": { + "username": "mobydock", + "password": "conta1n3rize14" + } + } + + This object maps the hostname of a registry to an object containing the + "username" and "password" for that registry. Multiple registries may + be specified as the build may be based on an image requiring + authentication to pull from any arbitrary registry. Only the registry + domain name (and port if not the default "443") are required. However + (for legacy reasons) the "official" Docker, Inc. hosted registry must + be specified with both a "https://" prefix and a "/v1/" suffix even + though Docker will prefer to use the v2 registry API. + +Status Codes: + +- **200** – no error +- **500** – server error + +### Create an image + +`POST /images/create` + +Create an image either by pulling it from the registry or by importing it + +**Example request**: + + POST /images/create?fromImage=ubuntu HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "Pulling..."} + {"status": "Pulling", "progress": "1 B/ 100 B", "progressDetail": {"current": 1, "total": 100}} + {"error": "Invalid..."} + ... + +When using this endpoint to pull an image from the registry, the +`X-Registry-Auth` header can be used to include +a base64-encoded AuthConfig object. + +Query Parameters: + +- **fromImage** – Name of the image to pull. +- **fromSrc** – Source to import. The value may be a URL from which the image + can be retrieved or `-` to read the image from the request body. +- **repo** – Repository name. +- **tag** – Tag. +- **registry** – The registry to pull from. + + Request Headers: + +- **X-Registry-Auth** – base64-encoded AuthConfig object + +Status Codes: + +- **200** – no error +- **500** – server error + + + +### Inspect an image + +`GET /images/(name)/json` + +Return low-level information on the image `name` + +**Example request**: + + GET /images/ubuntu/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Created": "2013-03-23T22:24:18.818426-07:00", + "Container": "3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", + "ContainerConfig": + { + "Hostname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": true, + "OpenStdin": true, + "StdinOnce": false, + "Env": null, + "Cmd": ["/bin/bash"], + "Dns": null, + "Image": "ubuntu", + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "Volumes": null, + "VolumesFrom": "", + "WorkingDir": "" + }, + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Parent": "27cf784147099545", + "Size": 6824592 + } + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Get the history of an image + +`GET /images/(name)/history` + +Return the history of the image `name` + +**Example request**: + + GET /images/ubuntu/history HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "3db9c44f45209632d6050b35958829c3a2aa256d81b9a7be45b362ff85c54710", + "Created": 1398108230, + "CreatedBy": "/bin/sh -c #(nop) ADD file:eb15dbd63394e063b805a3c32ca7bf0266ef64676d5a6fab4801f2e81e2a5148 in /", + "Tags": [ + "ubuntu:lucid", + "ubuntu:10.04" + ], + "Size": 182964289, + "Comment": "" + }, + { + "Id": "6cfa4d1f33fb861d4d114f43b25abd0ac737509268065cdfd69d544a59c85ab8", + "Created": 1398108222, + "CreatedBy": "/bin/sh -c #(nop) MAINTAINER Tianon Gravi - mkimage-debootstrap.sh -i iproute,iputils-ping,ubuntu-minimal -t lucid.tar.xz lucid http://archive.ubuntu.com/ubuntu/", + "Tags": null, + "Size": 0, + "Comment": "" + }, + { + "Id": "511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158", + "Created": 1371157430, + "CreatedBy": "", + "Tags": [ + "scratch12:latest", + "scratch:latest" + ], + "Size": 0, + "Comment": "Imported from -" + } + ] + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Push an image on the registry + +`POST /images/(name)/push` + +Push the image `name` on the registry + +**Example request**: + + POST /images/test/push HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "Pushing..."} + {"status": "Pushing", "progress": "1/? (n/a)", "progressDetail": {"current": 1}}} + {"error": "Invalid..."} + ... + +If you wish to push an image on to a private registry, that image must already have a tag +into a repository which references that registry `hostname` and `port`. This repository name should +then be used in the URL. This duplicates the command line's flow. + +**Example request**: + + POST /images/registry.acme.com:5000/test/push HTTP/1.1 + + +Query Parameters: + +- **tag** – The tag to associate with the image on the registry. This is optional. + +Request Headers: + +- **X-Registry-Auth** – Include a base64-encoded AuthConfig. + object. + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Tag an image into a repository + +`POST /images/(name)/tag` + +Tag the image `name` into a repository + +**Example request**: + + POST /images/test/tag?repo=myrepo&force=0&tag=v42 HTTP/1.1 + +**Example response**: + + HTTP/1.1 201 Created + +Query Parameters: + +- **repo** – The repository to tag in +- **force** – 1/True/true or 0/False/false, default false +- **tag** - The new tag name + +Status Codes: + +- **201** – no error +- **400** – bad parameter +- **404** – no such image +- **409** – conflict +- **500** – server error + +### Remove an image + +`DELETE /images/(name)` + +Remove the image `name` from the filesystem + +**Example request**: + + DELETE /images/test HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-type: application/json + + [ + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} + ] + +Query Parameters: + +- **force** – 1/True/true or 0/False/false, default false +- **noprune** – 1/True/true or 0/False/false, default false + +Status Codes: + +- **200** – no error +- **404** – no such image +- **409** – conflict +- **500** – server error + +### Search images + +`GET /images/search` + +Search for an image on [Docker Hub](https://hub.docker.com). + +> **Note**: +> The response keys have changed from API v1.6 to reflect the JSON +> sent by the registry server to the docker daemon's request. + +**Example request**: + + GET /images/search?term=sshd HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "wma55/u1210sshd", + "star_count": 0 + }, + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "jdswinbank/sshd", + "star_count": 0 + }, + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "vgauthier/sshd", + "star_count": 0 + } + ... + ] + +Query Parameters: + +- **term** – term to search + +Status Codes: + +- **200** – no error +- **500** – server error + +## 2.3 Misc + +### Check auth configuration + +`POST /auth` + +Get the default username and email + +**Example request**: + + POST /auth HTTP/1.1 + Content-Type: application/json + + { + "username":" hannibal", + "password: "xxxx", + "email": "hannibal@a-team.com", + "serveraddress": "https://index.docker.io/v1/" + } + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – no error +- **204** – no error +- **500** – server error + +### Display system-wide information + +`GET /info` + +Display system-wide information + +**Example request**: + + GET /info HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Containers": 11, + "CpuCfsPeriod": true, + "CpuCfsQuota": true, + "Debug": false, + "DockerRootDir": "/var/lib/docker", + "Driver": "btrfs", + "DriverStatus": [[""]], + "ExecutionDriver": "native-0.1", + "ExperimentalBuild": false, + "HttpProxy": "http://test:test@localhost:8080", + "HttpsProxy": "https://test:test@localhost:8080", + "ID": "7TRN:IPZB:QYBB:VPBQ:UMPP:KARE:6ZNR:XE6T:7EWV:PKF4:ZOJD:TPYS", + "IPv4Forwarding": true, + "Images": 16, + "IndexServerAddress": "https://index.docker.io/v1/", + "InitPath": "/usr/bin/docker", + "InitSha1": "", + "KernelVersion": "3.12.0-1-amd64", + "Labels": [ + "storage=ssd" + ], + "MemTotal": 2099236864, + "MemoryLimit": true, + "NCPU": 1, + "NEventsListener": 0, + "NFd": 11, + "NGoroutines": 21, + "Name": "prod-server-42", + "NoProxy": "9.81.1.160", + "OomKillDisable": true, + "OperatingSystem": "Boot2Docker", + "RegistryConfig": { + "IndexConfigs": { + "docker.io": { + "Mirrors": null, + "Name": "docker.io", + "Official": true, + "Secure": true + } + }, + "InsecureRegistryCIDRs": [ + "127.0.0.0/8" + ] + }, + "SwapLimit": false, + "SystemTime": "2015-03-10T11:11:23.730591467-07:00" + } + +Status Codes: + +- **200** – no error +- **500** – server error + +### Show the docker version information + +`GET /version` + +Show the docker version information + +**Example request**: + + GET /version HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Version": "1.5.0", + "Os": "linux", + "KernelVersion": "3.18.5-tinycore64", + "GoVersion": "go1.4.1", + "GitCommit": "a8a31ef", + "Arch": "amd64", + "ApiVersion": "1.20", + "Experimental": false + } + +Status Codes: + +- **200** – no error +- **500** – server error + +### Ping the docker server + +`GET /_ping` + +Ping the docker server + +**Example request**: + + GET /_ping HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: text/plain + + OK + +Status Codes: + +- **200** - no error +- **500** - server error + +### Create a new image from a container's changes + +`POST /commit` + +Create a new image from a container's changes + +**Example request**: + + POST /commit?container=44c004db4b17&comment=message&repo=myrepo HTTP/1.1 + Content-Type: application/json + + { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "date" + ], + "Mounts": [ + { + "Source": "/data", + "Destination": "/data", + "Mode": "ro,Z", + "RW": false + } + ], + "Labels": { + "key1": "value1", + "key2": "value2" + }, + "WorkingDir": "", + "NetworkDisabled": false, + "ExposedPorts": { + "22/tcp": {} + } + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + {"Id": "596069db4bf5"} + +Json Parameters: + +- **config** - the container's configuration + +Query Parameters: + +- **container** – source container +- **repo** – repository +- **tag** – tag +- **comment** – commit message +- **author** – author (e.g., "John Hannibal Smith + <[hannibal@a-team.com](mailto:hannibal%40a-team.com)>") +- **pause** – 1/True/true or 0/False/false, whether to pause the container before committing +- **changes** – Dockerfile instructions to apply while committing + +Status Codes: + +- **201** – no error +- **404** – no such container +- **500** – server error + +### Monitor Docker's events + +`GET /events` + +Get container events from docker, either in real time via streaming, or via +polling (using since). + +Docker containers report the following events: + + attach, commit, copy, create, destroy, die, exec_create, exec_start, export, kill, oom, pause, rename, resize, restart, start, stop, top, unpause + +and Docker images report: + + delete, import, pull, push, tag, untag + +**Example request**: + + GET /events?since=1374067924 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "create", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067924} + {"status": "start", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067924} + {"status": "stop", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067966} + {"status": "destroy", "id": "dfdf82bd3881","from": "ubuntu:latest", "time":1374067970} + +Query Parameters: + +- **since** – Timestamp used for polling +- **until** – Timestamp used for polling +- **filters** – A json encoded value of the filters (a map[string][]string) to process on the event list. Available filters: + - `event=`; -- event to filter + - `image=`; -- image to filter + - `container=`; -- container to filter + +Status Codes: + +- **200** – no error +- **500** – server error + +### Get a tarball containing all images in a repository + +`GET /images/(name)/get` + +Get a tarball containing all images and metadata for the repository specified +by `name`. + +If `name` is a specific name and tag (e.g. ubuntu:latest), then only that image +(and its parents) are returned. If `name` is an image ID, similarly only that +image (and its parents) are returned, but with the exclusion of the +'repositories' file in the tarball, as there were no image names referenced. + +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + GET /images/ubuntu/get + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + Binary data stream + +Status Codes: + +- **200** – no error +- **500** – server error + +### Get a tarball containing all images. + +`GET /images/get` + +Get a tarball containing all images and metadata for one or more repositories. + +For each value of the `names` parameter: if it is a specific name and tag (e.g. +`ubuntu:latest`), then only that image (and its parents) are returned; if it is +an image ID, similarly only that image (and its parents) are returned and there +would be no names referenced in the 'repositories' file for this image ID. + +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + GET /images/get?names=myname%2Fmyapp%3Alatest&names=busybox + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + Binary data stream + +Status Codes: + +- **200** – no error +- **500** – server error + +### Load a tarball with a set of images and tags into docker + +`POST /images/load` + +Load a set of images and tags into a Docker repository. +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + POST /images/load + + Tarball in body + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – no error +- **500** – server error + +### Image tarball format + +An image tarball contains one directory per image layer (named using its long ID), +each containing these files: + +- `VERSION`: currently `1.0` - the file format version +- `json`: detailed layer information, similar to `docker inspect layer_id` +- `layer.tar`: A tarfile containing the filesystem changes in this layer + +The `layer.tar` file contains `aufs` style `.wh..wh.aufs` files and directories +for storing attribute changes and deletions. + +If the tarball defines a repository, the tarball should also include a `repositories` file at +the root that contains a list of repository and tag names mapped to layer IDs. + +``` +{"hello-world": + {"latest": "565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} +} +``` + +### Exec Create + +`POST /containers/(id or name)/exec` + +Sets up an exec instance in a running container `id` + +**Example request**: + + POST /containers/e90e34656806/exec HTTP/1.1 + Content-Type: application/json + + { + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "Cmd": [ + "date" + ] + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "Id": "f90e34656806", + "Warnings":[] + } + +Json Parameters: + +- **AttachStdin** - Boolean value, attaches to `stdin` of the `exec` command. +- **AttachStdout** - Boolean value, attaches to `stdout` of the `exec` command. +- **AttachStderr** - Boolean value, attaches to `stderr` of the `exec` command. +- **Tty** - Boolean value to allocate a pseudo-TTY. +- **Cmd** - Command to run specified as a string or an array of strings. + + +Status Codes: + +- **201** – no error +- **404** – no such container + +### Exec Start + +`POST /exec/(id)/start` + +Starts a previously set up `exec` instance `id`. If `detach` is true, this API +returns after starting the `exec` command. Otherwise, this API sets up an +interactive session with the `exec` command. + +**Example request**: + + POST /exec/e90e34656806/start HTTP/1.1 + Content-Type: application/json + + { + "Detach": false, + "Tty": false + } + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + +Json Parameters: + +- **Detach** - Detach from the `exec` command. +- **Tty** - Boolean value to allocate a pseudo-TTY. + +Status Codes: + +- **200** – no error +- **404** – no such exec instance + + **Stream details**: + Similar to the stream behavior of `POST /containers/(id or name)/attach` API + +### Exec Resize + +`POST /exec/(id)/resize` + +Resizes the `tty` session used by the `exec` command `id`. The unit is number of characters. +This API is valid only if `tty` was specified as part of creating and starting the `exec` command. + +**Example request**: + + POST /exec/e90e34656806/resize?h=40&w=80 HTTP/1.1 + Content-Type: text/plain + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: text/plain + +Query Parameters: + +- **h** – height of `tty` session +- **w** – width + +Status Codes: + +- **201** – no error +- **404** – no such exec instance + +### Exec Inspect + +`GET /exec/(id)/json` + +Return low-level information about the `exec` command `id`. + +**Example request**: + + GET /exec/11fb006128e8ceb3942e7c58d77750f24210e35f879dd204ac975c184b820b39/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: plain/text + + { + "ID" : "11fb006128e8ceb3942e7c58d77750f24210e35f879dd204ac975c184b820b39", + "Running" : false, + "ExitCode" : 2, + "ProcessConfig" : { + "privileged" : false, + "user" : "", + "tty" : false, + "entrypoint" : "sh", + "arguments" : [ + "-c", + "exit 2" + ] + }, + "OpenStdin" : false, + "OpenStderr" : false, + "OpenStdout" : false, + "Container" : { + "State" : { + "Running" : true, + "Paused" : false, + "Restarting" : false, + "OOMKilled" : false, + "Pid" : 3650, + "ExitCode" : 0, + "Error" : "", + "StartedAt" : "2014-11-17T22:26:03.717657531Z", + "FinishedAt" : "0001-01-01T00:00:00Z" + }, + "ID" : "8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c", + "Created" : "2014-11-17T22:26:03.626304998Z", + "Path" : "date", + "Args" : [], + "Config" : { + "Hostname" : "8f177a186b97", + "Domainname" : "", + "User" : "", + "AttachStdin" : false, + "AttachStdout" : false, + "AttachStderr" : false, + "ExposedPorts" : null, + "Tty" : false, + "OpenStdin" : false, + "StdinOnce" : false, + "Env" : [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ], + "Cmd" : [ + "date" + ], + "Image" : "ubuntu", + "Volumes" : null, + "WorkingDir" : "", + "Entrypoint" : null, + "NetworkDisabled" : false, + "MacAddress" : "", + "OnBuild" : null, + "SecurityOpt" : null + }, + "Image" : "5506de2b643be1e6febbf3b8a240760c6843244c41e12aa2f60ccbb7153d17f5", + "NetworkSettings" : { + "IPAddress" : "172.17.0.2", + "IPPrefixLen" : 16, + "MacAddress" : "02:42:ac:11:00:02", + "Gateway" : "172.17.42.1", + "Bridge" : "docker0", + "PortMapping" : null, + "Ports" : {} + }, + "ResolvConfPath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/resolv.conf", + "HostnamePath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/hostname", + "HostsPath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/hosts", + "LogPath": "/var/lib/docker/containers/1eb5fabf5a03807136561b3c00adcd2992b535d624d5e18b6cdc6a6844d9767b/1eb5fabf5a03807136561b3c00adcd2992b535d624d5e18b6cdc6a6844d9767b-json.log", + "Name" : "/test", + "Driver" : "aufs", + "ExecDriver" : "native-0.2", + "MountLabel" : "", + "ProcessLabel" : "", + "AppArmorProfile" : "", + "RestartCount" : 0, + "Mounts" : [] + } + } + +Status Codes: + +- **200** – no error +- **404** – no such exec instance +- **500** - server error + +# 3. Going further + +## 3.1 Inside `docker run` + +As an example, the `docker run` command line makes the following API calls: + +- Create the container + +- If the status code is 404, it means the image doesn't exist: + - Try to pull it. + - Then, retry to create the container. + +- Start the container. + +- If you are not in detached mode: +- Attach to the container, using `logs=1` (to have `stdout` and + `stderr` from the container's start) and `stream=1` + +- If in detached mode or only `stdin` is attached, display the container's id. + +## 3.2 Hijacking + +In this version of the API, `/attach`, uses hijacking to transport `stdin`, +`stdout`, and `stderr` on the same socket. + +To hint potential proxies about connection hijacking, Docker client sends +connection upgrade headers similarly to websocket. + + Upgrade: tcp + Connection: Upgrade + +When Docker daemon detects the `Upgrade` header, it switches its status code +from **200 OK** to **101 UPGRADED** and resends the same headers. + + +## 3.3 CORS Requests + +To set cross origin requests to the remote api please give values to +`--api-cors-header` when running Docker in daemon mode. Set * (asterisk) allows all, +default or blank means CORS disabled + + $ docker daemon -H="192.168.1.9:2375" --api-cors-header="http://foo.bar" diff --git a/docs/reference/api/docker_remote_api_v1.21.md b/docs/reference/api/docker_remote_api_v1.21.md new file mode 100644 index 00000000..7cdfd0f3 --- /dev/null +++ b/docs/reference/api/docker_remote_api_v1.21.md @@ -0,0 +1,2914 @@ + + +# Docker Remote API v1.21 + +## 1. Brief introduction + + - The Remote API has replaced `rcli`. + - The daemon listens on `unix:///var/run/docker.sock` but you can + [Bind Docker to another host/port or a Unix socket](../../quickstart.md#bind-docker-to-another-host-port-or-a-unix-socket). + - The API tends to be REST. However, for some complex commands, like `attach` + or `pull`, the HTTP connection is hijacked to transport `stdout`, + `stdin` and `stderr`. + - When the client API version is newer than the daemon's, these calls return an HTTP + `400 Bad Request` error message. + +# 2. Endpoints + +## 2.1 Containers + +### List containers + +`GET /containers/json` + +List containers + +**Example request**: + + GET /containers/json?all=1&before=8dfafdbc3a40&size=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "8dfafdbc3a40", + "Names":["/boring_feynman"], + "Image": "ubuntu:latest", + "ImageID": "d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82", + "Command": "echo 1", + "Created": 1367854155, + "Status": "Exit 0", + "Ports": [{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "SizeRw": 12288, + "SizeRootFs": 0 + }, + { + "Id": "9cd87474be90", + "Names":["/coolName"], + "Image": "ubuntu:latest", + "ImageID": "d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82", + "Command": "echo 222222", + "Created": 1367854155, + "Status": "Exit 0", + "Ports": [], + "Labels": {}, + "SizeRw": 12288, + "SizeRootFs": 0 + }, + { + "Id": "3176a2479c92", + "Names":["/sleepy_dog"], + "Image": "ubuntu:latest", + "ImageID": "d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82", + "Command": "echo 3333333333333333", + "Created": 1367854154, + "Status": "Exit 0", + "Ports":[], + "Labels": {}, + "SizeRw":12288, + "SizeRootFs":0 + }, + { + "Id": "4cb07b47f9fb", + "Names":["/running_cat"], + "Image": "ubuntu:latest", + "ImageID": "d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82", + "Command": "echo 444444444444444444444444444444444", + "Created": 1367854152, + "Status": "Exit 0", + "Ports": [], + "Labels": {}, + "SizeRw": 12288, + "SizeRootFs": 0 + } + ] + +Query Parameters: + +- **all** – 1/True/true or 0/False/false, Show all containers. + Only running containers are shown by default (i.e., this defaults to false) +- **limit** – Show `limit` last created + containers, include non-running ones. +- **since** – Show only containers created since Id, include + non-running ones. +- **before** – Show only containers created before Id, include + non-running ones. +- **size** – 1/True/true or 0/False/false, Show the containers + sizes +- **filters** - a JSON encoded value of the filters (a `map[string][]string`) to process on the containers list. Available filters: + - `exited=`; -- containers with exit code of `` ; + - `status=`(`created`|`restarting`|`running`|`paused`|`exited`) + - `label=key` or `label="key=value"` of a container label + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **500** – server error + +### Create a container + +`POST /containers/create` + +Create a container + +**Example request**: + + POST /containers/create HTTP/1.1 + Content-Type: application/json + + { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "FOO=bar", + "BAZ=quux" + ], + "Cmd": [ + "date" + ], + "Entrypoint": "", + "Image": "ubuntu", + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "Volumes": { + "/volumes/data": {} + }, + "WorkingDir": "", + "NetworkDisabled": false, + "MacAddress": "12:34:56:78:9a:bc", + "ExposedPorts": { + "22/tcp": {} + }, + "StopSignal": "SIGTERM", + "HostConfig": { + "Binds": ["/tmp:/tmp"], + "Links": ["redis3:redis"], + "LxcConf": {"lxc.utsname":"docker"}, + "Memory": 0, + "MemorySwap": 0, + "MemoryReservation": 0, + "KernelMemory": 0, + "CpuShares": 512, + "CpuPeriod": 100000, + "CpuQuota": 50000, + "CpusetCpus": "0,1", + "CpusetMems": "0,1", + "BlkioWeight": 300, + "MemorySwappiness": 60, + "OomKillDisable": false, + "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] }, + "PublishAllPorts": false, + "Privileged": false, + "ReadonlyRootfs": false, + "Dns": ["8.8.8.8"], + "DnsOptions": [""], + "DnsSearch": [""], + "ExtraHosts": null, + "VolumesFrom": ["parent", "other:ro"], + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"], + "GroupAdd": ["newgroup"], + "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 }, + "NetworkMode": "bridge", + "Devices": [], + "Ulimits": [{}], + "LogConfig": { "Type": "json-file", "Config": {} }, + "SecurityOpt": [], + "CgroupParent": "", + "VolumeDriver": "" + } + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "Id":"e90e34656806", + "Warnings":[] + } + +Json Parameters: + +- **Hostname** - A string value containing the hostname to use for the + container. +- **Domainname** - A string value containing the domain name to use + for the container. +- **User** - A string value specifying the user inside the container. +- **Memory** - Memory limit in bytes. +- **MemorySwap** - Total memory limit (memory + swap); set `-1` to enable unlimited swap. + You must use this with `memory` and make the swap value larger than `memory`. +- **MemoryReservation** - Memory soft limit in bytes. +- **KernelMemory** - Kernel memory limit in bytes. +- **CpuShares** - An integer value containing the container's CPU Shares + (ie. the relative weight vs other containers). +- **CpuPeriod** - The length of a CPU period in microseconds. +- **CpuQuota** - Microseconds of CPU time that the container can get in a CPU period. +- **Cpuset** - Deprecated please don't use. Use `CpusetCpus` instead. +- **CpusetCpus** - String value containing the `cgroups CpusetCpus` to use. +- **CpusetMems** - Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems. +- **BlkioWeight** - Block IO weight (relative weight) accepts a weight value between 10 and 1000. +- **MemorySwappiness** - Tune a container's memory swappiness behavior. Accepts an integer between 0 and 100. +- **OomKillDisable** - Boolean value, whether to disable OOM Killer for the container or not. +- **AttachStdin** - Boolean value, attaches to `stdin`. +- **AttachStdout** - Boolean value, attaches to `stdout`. +- **AttachStderr** - Boolean value, attaches to `stderr`. +- **Tty** - Boolean value, Attach standard streams to a `tty`, including `stdin` if it is not closed. +- **OpenStdin** - Boolean value, opens stdin, +- **StdinOnce** - Boolean value, close `stdin` after the 1 attached client disconnects. +- **Env** - A list of environment variables in the form of `["VAR=value"[,"VAR2=value2"]]` +- **Labels** - Adds a map of labels to a container. To specify a map: `{"key":"value"[,"key2":"value2"]}` +- **Cmd** - Command to run specified as a string or an array of strings. +- **Entrypoint** - Set the entry point for the container as a string or an array + of strings. +- **Image** - A string specifying the image name to use for the container. +- **Volumes** - An object mapping mount point paths (strings) inside the + container to empty objects. +- **WorkingDir** - A string specifying the working directory for commands to + run in. +- **NetworkDisabled** - Boolean value, when true disables networking for the + container +- **ExposedPorts** - An object mapping ports to an empty object in the form of: + `"ExposedPorts": { "/: {}" }` +- **StopSignal** - Signal to stop a container as a string or unsigned integer. `SIGTERM` by default. +- **HostConfig** + - **Binds** – A list of volume bindings for this container. Each volume binding is a string in one of these forms: + + `container_path` to create a new volume for the container + + `host_path:container_path` to bind-mount a host path into the container + + `host_path:container_path:ro` to make the bind-mount read-only inside the container. + + `volume_name:container_path` to bind-mount a volume managed by a volume plugin into the container. + + `volume_name:container_path:ro` to make the bind mount read-only inside the container. + - **Links** - A list of links for the container. Each link entry should be + in the form of `container_name:alias`. + - **LxcConf** - LXC specific configurations. These configurations only + work when using the `lxc` execution driver. + - **PortBindings** - A map of exposed container ports and the host port they + should map to. A JSON object in the form + `{ /: [{ "HostPort": "" }] }` + Take note that `port` is specified as a string and not an integer value. + - **PublishAllPorts** - Allocates a random host port for all of a container's + exposed ports. Specified as a boolean value. + - **Privileged** - Gives the container full access to the host. Specified as + a boolean value. + - **ReadonlyRootfs** - Mount the container's root filesystem as read only. + Specified as a boolean value. + - **Dns** - A list of DNS servers for the container to use. + - **DnsOptions** - A list of DNS options + - **DnsSearch** - A list of DNS search domains + - **ExtraHosts** - A list of hostnames/IP mappings to add to the + container's `/etc/hosts` file. Specified in the form `["hostname:IP"]`. + - **VolumesFrom** - A list of volumes to inherit from another container. + Specified in the form `[:]` + - **CapAdd** - A list of kernel capabilities to add to the container. + - **Capdrop** - A list of kernel capabilities to drop from the container. + - **GroupAdd** - A list of additional groups that the container process will run as + - **RestartPolicy** – The behavior to apply when the container exits. The + value is an object with a `Name` property of either `"always"` to + always restart, `"unless-stopped"` to restart always except when + user has manually stopped the container or `"on-failure"` to restart only when the container + exit code is non-zero. If `on-failure` is used, `MaximumRetryCount` + controls the number of times to retry before giving up. + The default is not to restart. (optional) + An ever increasing delay (double the previous delay, starting at 100mS) + is added before each restart to prevent flooding the server. + - **NetworkMode** - Sets the networking mode for the container. Supported + standard values are: `bridge`, `host`, `none`, and `container:`. Any other value is taken + as a custom network's name to which this container should connect to. + - **Devices** - A list of devices to add to the container specified as a JSON object in the + form + `{ "PathOnHost": "/dev/deviceName", "PathInContainer": "/dev/deviceName", "CgroupPermissions": "mrw"}` + - **Ulimits** - A list of ulimits to set in the container, specified as + `{ "Name": , "Soft": , "Hard": }`, for example: + `Ulimits: { "Name": "nofile", "Soft": 1024, "Hard": 2048 }` + - **SecurityOpt**: A list of string values to customize labels for MLS + systems, such as SELinux. + - **LogConfig** - Log configuration for the container, specified as a JSON object in the form + `{ "Type": "", "Config": {"key1": "val1"}}`. + Available types: `json-file`, `syslog`, `journald`, `gelf`, `awslogs`, `none`. + `json-file` logging driver. + - **CgroupParent** - Path to `cgroups` under which the container's `cgroup` is created. If the path is not absolute, the path is considered to be relative to the `cgroups` path of the init process. Cgroups are created if they do not already exist. + - **VolumeDriver** - Driver that this container users to mount volumes. + +Query Parameters: + +- **name** – Assign the specified name to the container. Must + match `/?[a-zA-Z0-9_-]+`. + +Status Codes: + +- **201** – no error +- **404** – no such container +- **406** – impossible to attach (container not running) +- **500** – server error + +### Inspect a container + +`GET /containers/(id or name)/json` + +Return low-level information on the container `id` + + +**Example request**: + + GET /containers/4fa6e0f0c678/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "AppArmorProfile": "", + "Args": [ + "-c", + "exit 9" + ], + "Config": { + "AttachStderr": true, + "AttachStdin": false, + "AttachStdout": true, + "Cmd": [ + "/bin/sh", + "-c", + "exit 9" + ], + "Domainname": "", + "Entrypoint": null, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "ExposedPorts": null, + "Hostname": "ba033ac44011", + "Image": "ubuntu", + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "MacAddress": "", + "NetworkDisabled": false, + "OnBuild": null, + "OpenStdin": false, + "StdinOnce": false, + "Tty": false, + "User": "", + "Volumes": null, + "WorkingDir": "", + "StopSignal": "SIGTERM" + }, + "Created": "2015-01-06T15:47:31.485331387Z", + "Driver": "devicemapper", + "ExecDriver": "native-0.2", + "ExecIDs": null, + "HostConfig": { + "Binds": null, + "BlkioWeight": 0, + "CapAdd": null, + "CapDrop": null, + "ContainerIDFile": "", + "CpusetCpus": "", + "CpusetMems": "", + "CpuShares": 0, + "CpuPeriod": 100000, + "Devices": [], + "Dns": null, + "DnsOptions": null, + "DnsSearch": null, + "ExtraHosts": null, + "IpcMode": "", + "Links": null, + "LxcConf": [], + "Memory": 0, + "MemorySwap": 0, + "MemoryReservation": 0, + "KernelMemory": 0, + "OomKillDisable": false, + "NetworkMode": "bridge", + "PortBindings": {}, + "Privileged": false, + "ReadonlyRootfs": false, + "PublishAllPorts": false, + "RestartPolicy": { + "MaximumRetryCount": 2, + "Name": "on-failure" + }, + "LogConfig": { + "Config": null, + "Type": "json-file" + }, + "SecurityOpt": null, + "VolumesFrom": null, + "Ulimits": [{}], + "VolumeDriver": "" + }, + "HostnamePath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/hostname", + "HostsPath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/hosts", + "LogPath": "/var/lib/docker/containers/1eb5fabf5a03807136561b3c00adcd2992b535d624d5e18b6cdc6a6844d9767b/1eb5fabf5a03807136561b3c00adcd2992b535d624d5e18b6cdc6a6844d9767b-json.log", + "Id": "ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39", + "Image": "04c5d3b7b0656168630d3ba35d8889bd0e9caafcaeb3004d2bfbc47e7c5d35d2", + "MountLabel": "", + "Name": "/boring_euclid", + "NetworkSettings": { + "Bridge": "", + "SandboxID": "", + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "Ports": null, + "SandboxKey": "", + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "", + "Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "", + "IPPrefixLen": 0, + "IPv6Gateway": "", + "MacAddress": "", + "Networks": { + "bridge": { + "EndpointID": "", + "Gateway": "", + "IPAddress": "", + "IPPrefixLen": 0, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "" + } + } + }, + "Path": "/bin/sh", + "ProcessLabel": "", + "ResolvConfPath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/resolv.conf", + "RestartCount": 1, + "State": { + "Error": "", + "ExitCode": 9, + "FinishedAt": "2015-01-06T15:47:32.080254511Z", + "OOMKilled": false, + "Paused": false, + "Pid": 0, + "Restarting": false, + "Running": true, + "StartedAt": "2015-01-06T15:47:32.072697474Z", + "Status": "running" + }, + "Mounts": [ + { + "Source": "/data", + "Destination": "/data", + "Mode": "ro,Z", + "RW": false + } + ] + } + +**Example request, with size information**: + + GET /containers/4fa6e0f0c678/json?size=1 HTTP/1.1 + +**Example response, with size information**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + .... + "SizeRw": 0, + "SizeRootFs": 972, + .... + } + +Query Parameters: + +- **size** – 1/True/true or 0/False/false, return container size information. Default is `false`. + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### List processes running inside a container + +`GET /containers/(id or name)/top` + +List processes running inside the container `id`. On Unix systems this +is done by running the `ps` command. This endpoint is not +supported on Windows. + +**Example request**: + + GET /containers/4fa6e0f0c678/top HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Titles" : [ + "UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD" + ], + "Processes" : [ + [ + "root", "13642", "882", "0", "17:03", "pts/0", "00:00:00", "/bin/bash" + ], + [ + "root", "13735", "13642", "0", "17:06", "pts/0", "00:00:00", "sleep 10" + ] + ] + } + +**Example request**: + + GET /containers/4fa6e0f0c678/top?ps_args=aux HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Titles" : [ + "USER","PID","%CPU","%MEM","VSZ","RSS","TTY","STAT","START","TIME","COMMAND" + ] + "Processes" : [ + [ + "root","13642","0.0","0.1","18172","3184","pts/0","Ss","17:03","0:00","/bin/bash" + ], + [ + "root","13895","0.0","0.0","4348","692","pts/0","S+","17:15","0:00","sleep 10" + ] + ], + } + +Query Parameters: + +- **ps_args** – `ps` arguments to use (e.g., `aux`), defaults to `-ef` + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Get container logs + +`GET /containers/(id or name)/logs` + +Get `stdout` and `stderr` logs from the container ``id`` + +> **Note**: +> This endpoint works only for containers with the `json-file` or `journald` logging drivers. + +**Example request**: + + GET /containers/4fa6e0f0c678/logs?stderr=1&stdout=1×tamps=1&follow=1&tail=10&since=1428990821 HTTP/1.1 + +**Example response**: + + HTTP/1.1 101 UPGRADED + Content-Type: application/vnd.docker.raw-stream + Connection: Upgrade + Upgrade: tcp + + {{ STREAM }} + +Query Parameters: + +- **follow** – 1/True/true or 0/False/false, return stream. Default `false`. +- **stdout** – 1/True/true or 0/False/false, show `stdout` log. Default `false`. +- **stderr** – 1/True/true or 0/False/false, show `stderr` log. Default `false`. +- **since** – UNIX timestamp (integer) to filter logs. Specifying a timestamp + will only output log-entries since that timestamp. Default: 0 (unfiltered) +- **timestamps** – 1/True/true or 0/False/false, print timestamps for + every log line. Default `false`. +- **tail** – Output specified number of lines at the end of logs: `all` or ``. Default all. + +Status Codes: + +- **101** – no error, hints proxy about hijacking +- **200** – no error, no upgrade header found +- **404** – no such container +- **500** – server error + +### Inspect changes on a container's filesystem + +`GET /containers/(id or name)/changes` + +Inspect changes on container `id`'s filesystem + +**Example request**: + + GET /containers/4fa6e0f0c678/changes HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Path": "/dev", + "Kind": 0 + }, + { + "Path": "/dev/kmsg", + "Kind": 1 + }, + { + "Path": "/test", + "Kind": 1 + } + ] + +Values for `Kind`: + +- `0`: Modify +- `1`: Add +- `2`: Delete + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Export a container + +`GET /containers/(id or name)/export` + +Export the contents of container `id` + +**Example request**: + + GET /containers/4fa6e0f0c678/export HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/octet-stream + + {{ TAR STREAM }} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Get container stats based on resource usage + +`GET /containers/(id or name)/stats` + +This endpoint returns a live stream of a container's resource usage statistics. + +**Example request**: + + GET /containers/redis1/stats HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "read" : "2015-01-08T22:57:31.547920715Z", + "networks": { + "eth0": { + "rx_bytes": 5338, + "rx_dropped": 0, + "rx_errors": 0, + "rx_packets": 36, + "tx_bytes": 648, + "tx_dropped": 0, + "tx_errors": 0, + "tx_packets": 8 + }, + "eth5": { + "rx_bytes": 4641, + "rx_dropped": 0, + "rx_errors": 0, + "rx_packets": 26, + "tx_bytes": 690, + "tx_dropped": 0, + "tx_errors": 0, + "tx_packets": 9 + } + }, + "memory_stats" : { + "stats" : { + "total_pgmajfault" : 0, + "cache" : 0, + "mapped_file" : 0, + "total_inactive_file" : 0, + "pgpgout" : 414, + "rss" : 6537216, + "total_mapped_file" : 0, + "writeback" : 0, + "unevictable" : 0, + "pgpgin" : 477, + "total_unevictable" : 0, + "pgmajfault" : 0, + "total_rss" : 6537216, + "total_rss_huge" : 6291456, + "total_writeback" : 0, + "total_inactive_anon" : 0, + "rss_huge" : 6291456, + "hierarchical_memory_limit" : 67108864, + "total_pgfault" : 964, + "total_active_file" : 0, + "active_anon" : 6537216, + "total_active_anon" : 6537216, + "total_pgpgout" : 414, + "total_cache" : 0, + "inactive_anon" : 0, + "active_file" : 0, + "pgfault" : 964, + "inactive_file" : 0, + "total_pgpgin" : 477 + }, + "max_usage" : 6651904, + "usage" : 6537216, + "failcnt" : 0, + "limit" : 67108864 + }, + "blkio_stats" : {}, + "cpu_stats" : { + "cpu_usage" : { + "percpu_usage" : [ + 8646879, + 24472255, + 36438778, + 30657443 + ], + "usage_in_usermode" : 50000000, + "total_usage" : 100215355, + "usage_in_kernelmode" : 30000000 + }, + "system_cpu_usage" : 739306590000000, + "throttling_data" : {"periods":0,"throttled_periods":0,"throttled_time":0} + }, + "precpu_stats" : { + "cpu_usage" : { + "percpu_usage" : [ + 8646879, + 24350896, + 36438778, + 30657443 + ], + "usage_in_usermode" : 50000000, + "total_usage" : 100093996, + "usage_in_kernelmode" : 30000000 + }, + "system_cpu_usage" : 9492140000000, + "throttling_data" : {"periods":0,"throttled_periods":0,"throttled_time":0} + } + } + +The precpu_stats is the cpu statistic of last read, which is used for calculating the cpu usage percent. It is not the exact copy of the “cpu_stats” field. + +Query Parameters: + +- **stream** – 1/True/true or 0/False/false, pull stats once then disconnect. Default `true`. + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Resize a container TTY + +`POST /containers/(id or name)/resize` + +Resize the TTY for container with `id`. The unit is number of characters. You must restart the container for the resize to take effect. + +**Example request**: + + POST /containers/4fa6e0f0c678/resize?h=40&w=80 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Length: 0 + Content-Type: text/plain; charset=utf-8 + +Query Parameters: + +- **h** – height of `tty` session +- **w** – width + +Status Codes: + +- **200** – no error +- **404** – No such container +- **500** – Cannot resize container + +### Start a container + +`POST /containers/(id or name)/start` + +Start the container `id` + +> **Note**: +> For backwards compatibility, this endpoint accepts a `HostConfig` as JSON-encoded request body. +> See [create a container](#create-a-container) for details. + +**Example request**: + + POST /containers/e90e34656806/start HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **304** – container already started +- **404** – no such container +- **500** – server error + +### Stop a container + +`POST /containers/(id or name)/stop` + +Stop the container `id` + +**Example request**: + + POST /containers/e90e34656806/stop?t=5 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **t** – number of seconds to wait before killing the container + +Status Codes: + +- **204** – no error +- **304** – container already stopped +- **404** – no such container +- **500** – server error + +### Restart a container + +`POST /containers/(id or name)/restart` + +Restart the container `id` + +**Example request**: + + POST /containers/e90e34656806/restart?t=5 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **t** – number of seconds to wait before killing the container + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Kill a container + +`POST /containers/(id or name)/kill` + +Kill the container `id` + +**Example request**: + + POST /containers/e90e34656806/kill HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters + +- **signal** - Signal to send to the container: integer or string like `SIGINT`. + When not set, `SIGKILL` is assumed and the call waits for the container to exit. + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Rename a container + +`POST /containers/(id or name)/rename` + +Rename the container `id` to a `new_name` + +**Example request**: + + POST /containers/e90e34656806/rename?name=new_name HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **name** – new name for the container + +Status Codes: + +- **204** – no error +- **404** – no such container +- **409** - conflict name already assigned +- **500** – server error + +### Pause a container + +`POST /containers/(id or name)/pause` + +Pause the container `id` + +**Example request**: + + POST /containers/e90e34656806/pause HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Unpause a container + +`POST /containers/(id or name)/unpause` + +Unpause the container `id` + +**Example request**: + + POST /containers/e90e34656806/unpause HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Attach to a container + +`POST /containers/(id or name)/attach` + +Attach to the container `id` + +**Example request**: + + POST /containers/16253994b7c4/attach?logs=1&stream=0&stdout=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 101 UPGRADED + Content-Type: application/vnd.docker.raw-stream + Connection: Upgrade + Upgrade: tcp + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default `false`. +- **stream** – 1/True/true or 0/False/false, return stream. + Default `false`. +- **stdin** – 1/True/true or 0/False/false, if `stream=true`, attach + to `stdin`. Default `false`. +- **stdout** – 1/True/true or 0/False/false, if `logs=true`, return + `stdout` log, if `stream=true`, attach to `stdout`. Default `false`. +- **stderr** – 1/True/true or 0/False/false, if `logs=true`, return + `stderr` log, if `stream=true`, attach to `stderr`. Default `false`. + +Status Codes: + +- **101** – no error, hints proxy about hijacking +- **200** – no error, no upgrade header found +- **400** – bad parameter +- **404** – no such container +- **500** – server error + + **Stream details**: + + When using the TTY setting is enabled in + [`POST /containers/create` + ](#create-a-container), + the stream is the raw data from the process PTY and client's `stdin`. + When the TTY is disabled, then the stream is multiplexed to separate + `stdout` and `stderr`. + + The format is a **Header** and a **Payload** (frame). + + **HEADER** + + The header contains the information which the stream writes (`stdout` or + `stderr`). It also contains the size of the associated frame encoded in the + last four bytes (`uint32`). + + It is encoded on the first eight bytes like this: + + header := [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4} + + `STREAM_TYPE` can be: + +- 0: `stdin` (is written on `stdout`) +- 1: `stdout` +- 2: `stderr` + + `SIZE1, SIZE2, SIZE3, SIZE4` are the four bytes of + the `uint32` size encoded as big endian. + + **PAYLOAD** + + The payload is the raw stream. + + **IMPLEMENTATION** + + The simplest way to implement the Attach protocol is the following: + + 1. Read eight bytes. + 2. Choose `stdout` or `stderr` depending on the first byte. + 3. Extract the frame size from the last four bytes. + 4. Read the extracted size and output it on the correct output. + 5. Goto 1. + +### Attach to a container (websocket) + +`GET /containers/(id or name)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default `false`. +- **stream** – 1/True/true or 0/False/false, return stream. + Default `false`. +- **stdin** – 1/True/true or 0/False/false, if `stream=true`, attach + to `stdin`. Default `false`. +- **stdout** – 1/True/true or 0/False/false, if `logs=true`, return + `stdout` log, if `stream=true`, attach to `stdout`. Default `false`. +- **stderr** – 1/True/true or 0/False/false, if `logs=true`, return + `stderr` log, if `stream=true`, attach to `stderr`. Default `false`. + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + +### Wait a container + +`POST /containers/(id or name)/wait` + +Block until container `id` stops, then returns the exit code + +**Example request**: + + POST /containers/16253994b7c4/wait HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"StatusCode": 0} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Remove a container + +`DELETE /containers/(id or name)` + +Remove the container `id` from the filesystem + +**Example request**: + + DELETE /containers/16253994b7c4?v=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **v** – 1/True/true or 0/False/false, Remove the volumes + associated to the container. Default `false`. +- **force** - 1/True/true or 0/False/false, Kill then remove the container. + Default `false`. + +Status Codes: + +- **204** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + +### Copy files or folders from a container + +`POST /containers/(id or name)/copy` + +Copy files or folders of container `id` + +**Deprecated** in favor of the `archive` endpoint below. + +**Example request**: + + POST /containers/4fa6e0f0c678/copy HTTP/1.1 + Content-Type: application/json + + { + "Resource": "test.txt" + } + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + {{ TAR STREAM }} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Retrieving information about files and folders in a container + +`HEAD /containers/(id or name)/archive` + +See the description of the `X-Docker-Container-Path-Stat` header in the +following section. + +### Get an archive of a filesystem resource in a container + +`GET /containers/(id or name)/archive` + +Get an tar archive of a resource in the filesystem of container `id`. + +Query Parameters: + +- **path** - resource in the container's filesystem to archive. Required. + + If not an absolute path, it is relative to the container's root directory. + The resource specified by **path** must exist. To assert that the resource + is expected to be a directory, **path** should end in `/` or `/.` + (assuming a path separator of `/`). If **path** ends in `/.` then this + indicates that only the contents of the **path** directory should be + copied. A symlink is always resolved to its target. + + **Note**: It is not possible to copy certain system files such as resources + under `/proc`, `/sys`, `/dev`, and mounts created by the user in the + container. + +**Example request**: + + GET /containers/8cce319429b2/archive?path=/root HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + X-Docker-Container-Path-Stat: eyJuYW1lIjoicm9vdCIsInNpemUiOjQwOTYsIm1vZGUiOjIxNDc0ODQwOTYsIm10aW1lIjoiMjAxNC0wMi0yN1QyMDo1MToyM1oiLCJsaW5rVGFyZ2V0IjoiIn0= + + {{ TAR STREAM }} + +On success, a response header `X-Docker-Container-Path-Stat` will be set to a +base64-encoded JSON object containing some filesystem header information about +the archived resource. The above example value would decode to the following +JSON object (whitespace added for readability): + + { + "name": "root", + "size": 4096, + "mode": 2147484096, + "mtime": "2014-02-27T20:51:23Z", + "linkTarget": "" + } + +A `HEAD` request can also be made to this endpoint if only this information is +desired. + +Status Codes: + +- **200** - success, returns archive of copied resource +- **400** - client error, bad parameter, details in JSON response body, one of: + - must specify path parameter (**path** cannot be empty) + - not a directory (**path** was asserted to be a directory but exists as a + file) +- **404** - client error, resource not found, one of: + – no such container (container `id` does not exist) + - no such file or directory (**path** does not exist) +- **500** - server error + +### Extract an archive of files or folders to a directory in a container + +`PUT /containers/(id or name)/archive` + +Upload a tar archive to be extracted to a path in the filesystem of container +`id`. + +Query Parameters: + +- **path** - path to a directory in the container + to extract the archive's contents into. Required. + + If not an absolute path, it is relative to the container's root directory. + The **path** resource must exist. +- **noOverwriteDirNonDir** - If "1", "true", or "True" then it will be an error + if unpacking the given content would cause an existing directory to be + replaced with a non-directory and vice versa. + +**Example request**: + + PUT /containers/8cce319429b2/archive?path=/vol1 HTTP/1.1 + Content-Type: application/x-tar + + {{ TAR STREAM }} + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – the content was extracted successfully +- **400** - client error, bad parameter, details in JSON response body, one of: + - must specify path parameter (**path** cannot be empty) + - not a directory (**path** should be a directory but exists as a file) + - unable to overwrite existing directory with non-directory + (if **noOverwriteDirNonDir**) + - unable to overwrite existing non-directory with directory + (if **noOverwriteDirNonDir**) +- **403** - client error, permission denied, the volume + or container rootfs is marked as read-only. +- **404** - client error, resource not found, one of: + – no such container (container `id` does not exist) + - no such file or directory (**path** resource does not exist) +- **500** – server error + +## 2.2 Images + +### List Images + +`GET /images/json` + +**Example request**: + + GET /images/json?all=0 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "RepoTags": [ + "ubuntu:12.04", + "ubuntu:precise", + "ubuntu:latest" + ], + "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", + "Created": 1365714795, + "Size": 131506275, + "VirtualSize": 131506275, + "Labels": {} + }, + { + "RepoTags": [ + "ubuntu:12.10", + "ubuntu:quantal" + ], + "ParentId": "27cf784147099545", + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Created": 1364102658, + "Size": 24653, + "VirtualSize": 180116135, + "Labels": { + "com.example.version": "v1" + } + } + ] + +**Example request, with digest information**: + + GET /images/json?digests=1 HTTP/1.1 + +**Example response, with digest information**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Created": 1420064636, + "Id": "4986bf8c15363d1c5d15512d5266f8777bfba4974ac56e3270e7760f6f0a8125", + "ParentId": "ea13149945cb6b1e746bf28032f02e9b5a793523481a0a18645fc77ad53c4ea2", + "RepoDigests": [ + "localhost:5000/test/busybox@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf" + ], + "RepoTags": [ + "localhost:5000/test/busybox:latest", + "playdate:latest" + ], + "Size": 0, + "VirtualSize": 2429728, + "Labels": {} + } + ] + +The response shows a single image `Id` associated with two repositories +(`RepoTags`): `localhost:5000/test/busybox`: and `playdate`. A caller can use +either of the `RepoTags` values `localhost:5000/test/busybox:latest` or +`playdate:latest` to reference the image. + +You can also use `RepoDigests` values to reference an image. In this response, +the array has only one reference and that is to the +`localhost:5000/test/busybox` repository; the `playdate` repository has no +digest. You can reference this digest using the value: +`localhost:5000/test/busybox@sha256:cbbf2f9a99b47fc460d...` + +See the `docker run` and `docker build` commands for examples of digest and tag +references on the command line. + +Query Parameters: + +- **all** – 1/True/true or 0/False/false, default false +- **filters** – a JSON encoded value of the filters (a map[string][]string) to process on the images list. Available filters: + - `dangling=true` + - `label=key` or `label="key=value"` of an image label +- **filter** - only return images with the specified name + +### Build image from a Dockerfile + +`POST /build` + +Build an image from a Dockerfile + +**Example request**: + + POST /build HTTP/1.1 + + {{ TAR STREAM }} + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"stream": "Step 1..."} + {"stream": "..."} + {"error": "Error...", "errorDetail": {"code": 123, "message": "Error..."}} + +The input stream must be a `tar` archive compressed with one of the +following algorithms: `identity` (no compression), `gzip`, `bzip2`, `xz`. + +The archive must include a build instructions file, typically called +`Dockerfile` at the archive's root. The `dockerfile` parameter may be +used to specify a different build instructions file. To do this, its value must be +the path to the alternate build instructions file to use. + +The archive may include any number of other files, +which are accessible in the build context (See the [*ADD build +command*](../../reference/builder.md#dockerbuilder)). + +The build is canceled if the client drops the connection by quitting +or being killed. + +Query Parameters: + +- **dockerfile** - Path within the build context to the Dockerfile. This is + ignored if `remote` is specified and points to an individual filename. +- **t** – A name and optional tag to apply to the image in the `name:tag` format. + If you omit the `tag` the default `latest` value is assumed. + You can provide one or more `t` parameters. +- **remote** – A Git repository URI or HTTP/HTTPS URI build source. If the + URI specifies a filename, the file's contents are placed into a file + called `Dockerfile`. +- **q** – Suppress verbose build output. +- **nocache** – Do not use the cache when building the image. +- **pull** - Attempt to pull the image even if an older image exists locally. +- **rm** - Remove intermediate containers after a successful build (default behavior). +- **forcerm** - Always remove intermediate containers (includes `rm`). +- **memory** - Set memory limit for build. +- **memswap** - Total memory (memory + swap), `-1` to enable unlimited swap. +- **cpushares** - CPU shares (relative weight). +- **cpusetcpus** - CPUs in which to allow execution (e.g., `0-3`, `0,1`). +- **cpuperiod** - The length of a CPU period in microseconds. +- **cpuquota** - Microseconds of CPU time that the container can get in a CPU period. +- **buildargs** – JSON map of string pairs for build-time variables. Users pass + these values at build-time. Docker uses the `buildargs` as the environment + context for command(s) run via the Dockerfile's `RUN` instruction or for + variable expansion in other Dockerfile instructions. This is not meant for + passing secret values. [Read more about the buildargs instruction](../../reference/builder.md#arg) + + Request Headers: + +- **Content-type** – Set to `"application/tar"`. +- **X-Registry-Config** – A base64-url-safe-encoded Registry Auth Config JSON + object with the following structure: + + { + "docker.example.com": { + "username": "janedoe", + "password": "hunter2" + }, + "https://index.docker.io/v1/": { + "username": "mobydock", + "password": "conta1n3rize14" + } + } + + This object maps the hostname of a registry to an object containing the + "username" and "password" for that registry. Multiple registries may + be specified as the build may be based on an image requiring + authentication to pull from any arbitrary registry. Only the registry + domain name (and port if not the default "443") are required. However + (for legacy reasons) the "official" Docker, Inc. hosted registry must + be specified with both a "https://" prefix and a "/v1/" suffix even + though Docker will prefer to use the v2 registry API. + +Status Codes: + +- **200** – no error +- **500** – server error + +### Create an image + +`POST /images/create` + +Create an image either by pulling it from the registry or by importing it + +**Example request**: + + POST /images/create?fromImage=ubuntu HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "Pulling..."} + {"status": "Pulling", "progress": "1 B/ 100 B", "progressDetail": {"current": 1, "total": 100}} + {"error": "Invalid..."} + ... + +When using this endpoint to pull an image from the registry, the +`X-Registry-Auth` header can be used to include +a base64-encoded AuthConfig object. + +Query Parameters: + +- **fromImage** – Name of the image to pull. The name may include a tag or + digest. This parameter may only be used when pulling an image. +- **fromSrc** – Source to import. The value may be a URL from which the image + can be retrieved or `-` to read the image from the request body. + This parameter may only be used when importing an image. +- **repo** – Repository name given to an image when it is imported. + The repo may include a tag. This parameter may only be used when importing + an image. +- **tag** – Tag or digest. + + Request Headers: + +- **X-Registry-Auth** – base64-encoded AuthConfig object + +Status Codes: + +- **200** – no error +- **500** – server error + + + +### Inspect an image + +`GET /images/(name)/json` + +Return low-level information on the image `name` + +**Example request**: + + GET /images/example/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Id" : "85f05633ddc1c50679be2b16a0479ab6f7637f8884e0cfe0f4d20e1ebb3d6e7c", + "Container" : "cb91e48a60d01f1e27028b4fc6819f4f290b3cf12496c8176ec714d0d390984a", + "Comment" : "", + "Os" : "linux", + "Architecture" : "amd64", + "Parent" : "91e54dfb11794fad694460162bf0cb0a4fa710cfa3f60979c177d920813e267c", + "ContainerConfig" : { + "Tty" : false, + "Hostname" : "e611e15f9c9d", + "Volumes" : null, + "Domainname" : "", + "AttachStdout" : false, + "PublishService" : "", + "AttachStdin" : false, + "OpenStdin" : false, + "StdinOnce" : false, + "NetworkDisabled" : false, + "OnBuild" : [], + "Image" : "91e54dfb11794fad694460162bf0cb0a4fa710cfa3f60979c177d920813e267c", + "User" : "", + "WorkingDir" : "", + "Entrypoint" : null, + "MacAddress" : "", + "AttachStderr" : false, + "Labels" : { + "com.example.license" : "GPL", + "com.example.version" : "1.0", + "com.example.vendor" : "Acme" + }, + "Env" : [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "ExposedPorts" : null, + "Cmd" : [ + "/bin/sh", + "-c", + "#(nop) LABEL com.example.vendor=Acme com.example.license=GPL com.example.version=1.0" + ] + }, + "DockerVersion" : "1.9.0-dev", + "VirtualSize" : 188359297, + "Size" : 0, + "Author" : "", + "Created" : "2015-09-10T08:30:53.26995814Z", + "GraphDriver" : { + "Name" : "aufs", + "Data" : null + }, + "RepoDigests" : [ + "localhost:5000/test/busybox/example@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf" + ], + "RepoTags" : [ + "example:1.0", + "example:latest", + "example:stable" + ], + "Config" : { + "Image" : "91e54dfb11794fad694460162bf0cb0a4fa710cfa3f60979c177d920813e267c", + "NetworkDisabled" : false, + "OnBuild" : [], + "StdinOnce" : false, + "PublishService" : "", + "AttachStdin" : false, + "OpenStdin" : false, + "Domainname" : "", + "AttachStdout" : false, + "Tty" : false, + "Hostname" : "e611e15f9c9d", + "Volumes" : null, + "Cmd" : [ + "/bin/bash" + ], + "ExposedPorts" : null, + "Env" : [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Labels" : { + "com.example.vendor" : "Acme", + "com.example.version" : "1.0", + "com.example.license" : "GPL" + }, + "Entrypoint" : null, + "MacAddress" : "", + "AttachStderr" : false, + "WorkingDir" : "", + "User" : "" + } + } + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Get the history of an image + +`GET /images/(name)/history` + +Return the history of the image `name` + +**Example request**: + + GET /images/ubuntu/history HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "3db9c44f45209632d6050b35958829c3a2aa256d81b9a7be45b362ff85c54710", + "Created": 1398108230, + "CreatedBy": "/bin/sh -c #(nop) ADD file:eb15dbd63394e063b805a3c32ca7bf0266ef64676d5a6fab4801f2e81e2a5148 in /", + "Tags": [ + "ubuntu:lucid", + "ubuntu:10.04" + ], + "Size": 182964289, + "Comment": "" + }, + { + "Id": "6cfa4d1f33fb861d4d114f43b25abd0ac737509268065cdfd69d544a59c85ab8", + "Created": 1398108222, + "CreatedBy": "/bin/sh -c #(nop) MAINTAINER Tianon Gravi - mkimage-debootstrap.sh -i iproute,iputils-ping,ubuntu-minimal -t lucid.tar.xz lucid http://archive.ubuntu.com/ubuntu/", + "Tags": null, + "Size": 0, + "Comment": "" + }, + { + "Id": "511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158", + "Created": 1371157430, + "CreatedBy": "", + "Tags": [ + "scratch12:latest", + "scratch:latest" + ], + "Size": 0, + "Comment": "Imported from -" + } + ] + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Push an image on the registry + +`POST /images/(name)/push` + +Push the image `name` on the registry + +**Example request**: + + POST /images/test/push HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "Pushing..."} + {"status": "Pushing", "progress": "1/? (n/a)", "progressDetail": {"current": 1}}} + {"error": "Invalid..."} + ... + +If you wish to push an image on to a private registry, that image must already have a tag +into a repository which references that registry `hostname` and `port`. This repository name should +then be used in the URL. This duplicates the command line's flow. + +**Example request**: + + POST /images/registry.acme.com:5000/test/push HTTP/1.1 + + +Query Parameters: + +- **tag** – The tag to associate with the image on the registry. This is optional. + +Request Headers: + +- **X-Registry-Auth** – Include a base64-encoded AuthConfig. + object. + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Tag an image into a repository + +`POST /images/(name)/tag` + +Tag the image `name` into a repository + +**Example request**: + + POST /images/test/tag?repo=myrepo&force=0&tag=v42 HTTP/1.1 + +**Example response**: + + HTTP/1.1 201 Created + +Query Parameters: + +- **repo** – The repository to tag in +- **force** – 1/True/true or 0/False/false, default false +- **tag** - The new tag name + +Status Codes: + +- **201** – no error +- **400** – bad parameter +- **404** – no such image +- **409** – conflict +- **500** – server error + +### Remove an image + +`DELETE /images/(name)` + +Remove the image `name` from the filesystem + +**Example request**: + + DELETE /images/test HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-type: application/json + + [ + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} + ] + +Query Parameters: + +- **force** – 1/True/true or 0/False/false, default false +- **noprune** – 1/True/true or 0/False/false, default false + +Status Codes: + +- **200** – no error +- **404** – no such image +- **409** – conflict +- **500** – server error + +### Search images + +`GET /images/search` + +Search for an image on [Docker Hub](https://hub.docker.com). + +> **Note**: +> The response keys have changed from API v1.6 to reflect the JSON +> sent by the registry server to the docker daemon's request. + +**Example request**: + + GET /images/search?term=sshd HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "wma55/u1210sshd", + "star_count": 0 + }, + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "jdswinbank/sshd", + "star_count": 0 + }, + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "vgauthier/sshd", + "star_count": 0 + } + ... + ] + +Query Parameters: + +- **term** – term to search + +Status Codes: + +- **200** – no error +- **500** – server error + +## 2.3 Misc + +### Check auth configuration + +`POST /auth` + +Get the default username and email + +**Example request**: + + POST /auth HTTP/1.1 + Content-Type: application/json + + { + "username":" hannibal", + "password: "xxxx", + "email": "hannibal@a-team.com", + "serveraddress": "https://index.docker.io/v1/" + } + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – no error +- **204** – no error +- **500** – server error + +### Display system-wide information + +`GET /info` + +Display system-wide information + +**Example request**: + + GET /info HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "ClusterStore": "etcd://localhost:2379", + "Containers": 11, + "CpuCfsPeriod": true, + "CpuCfsQuota": true, + "Debug": false, + "DockerRootDir": "/var/lib/docker", + "Driver": "btrfs", + "DriverStatus": [[""]], + "ExecutionDriver": "native-0.1", + "ExperimentalBuild": false, + "HttpProxy": "http://test:test@localhost:8080", + "HttpsProxy": "https://test:test@localhost:8080", + "ID": "7TRN:IPZB:QYBB:VPBQ:UMPP:KARE:6ZNR:XE6T:7EWV:PKF4:ZOJD:TPYS", + "IPv4Forwarding": true, + "Images": 16, + "IndexServerAddress": "https://index.docker.io/v1/", + "InitPath": "/usr/bin/docker", + "InitSha1": "", + "KernelVersion": "3.12.0-1-amd64", + "Labels": [ + "storage=ssd" + ], + "MemTotal": 2099236864, + "MemoryLimit": true, + "NCPU": 1, + "NEventsListener": 0, + "NFd": 11, + "NGoroutines": 21, + "Name": "prod-server-42", + "NoProxy": "9.81.1.160", + "OomKillDisable": true, + "OperatingSystem": "Boot2Docker", + "RegistryConfig": { + "IndexConfigs": { + "docker.io": { + "Mirrors": null, + "Name": "docker.io", + "Official": true, + "Secure": true + } + }, + "InsecureRegistryCIDRs": [ + "127.0.0.0/8" + ] + }, + "ServerVersion": "1.9.0", + "SwapLimit": false, + "SystemTime": "2015-03-10T11:11:23.730591467-07:00" + } + +Status Codes: + +- **200** – no error +- **500** – server error + +### Show the docker version information + +`GET /version` + +Show the docker version information + +**Example request**: + + GET /version HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Version": "1.5.0", + "Os": "linux", + "KernelVersion": "3.18.5-tinycore64", + "GoVersion": "go1.4.1", + "GitCommit": "a8a31ef", + "Arch": "amd64", + "ApiVersion": "1.20", + "Experimental": false + } + +Status Codes: + +- **200** – no error +- **500** – server error + +### Ping the docker server + +`GET /_ping` + +Ping the docker server + +**Example request**: + + GET /_ping HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: text/plain + + OK + +Status Codes: + +- **200** - no error +- **500** - server error + +### Create a new image from a container's changes + +`POST /commit` + +Create a new image from a container's changes + +**Example request**: + + POST /commit?container=44c004db4b17&comment=message&repo=myrepo HTTP/1.1 + Content-Type: application/json + + { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "date" + ], + "Mounts": [ + { + "Source": "/data", + "Destination": "/data", + "Mode": "ro,Z", + "RW": false + } + ], + "Labels": { + "key1": "value1", + "key2": "value2" + }, + "WorkingDir": "", + "NetworkDisabled": false, + "ExposedPorts": { + "22/tcp": {} + } + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + {"Id": "596069db4bf5"} + +Json Parameters: + +- **config** - the container's configuration + +Query Parameters: + +- **container** – source container +- **repo** – repository +- **tag** – tag +- **comment** – commit message +- **author** – author (e.g., "John Hannibal Smith + <[hannibal@a-team.com](mailto:hannibal%40a-team.com)>") +- **pause** – 1/True/true or 0/False/false, whether to pause the container before committing +- **changes** – Dockerfile instructions to apply while committing + +Status Codes: + +- **201** – no error +- **404** – no such container +- **500** – server error + +### Monitor Docker's events + +`GET /events` + +Get container events from docker, either in real time via streaming, or via +polling (using since). + +Docker containers report the following events: + + attach, commit, copy, create, destroy, die, exec_create, exec_start, export, kill, oom, pause, rename, resize, restart, start, stop, top, unpause + +and Docker images report: + + delete, import, pull, push, tag, untag + +**Example request**: + + GET /events?since=1374067924 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status":"pull","id":"busybox:latest","time":1442421700,"timeNano":1442421700598988358} + {"status":"create","id":"5745704abe9caa5","from":"busybox","time":1442421716,"timeNano":1442421716853979870} + {"status":"attach","id":"5745704abe9caa5","from":"busybox","time":1442421716,"timeNano":1442421716894759198} + {"status":"start","id":"5745704abe9caa5","from":"busybox","time":1442421716,"timeNano":1442421716983607193} + +Query Parameters: + +- **since** – Timestamp used for polling +- **until** – Timestamp used for polling +- **filters** – A json encoded value of the filters (a map[string][]string) to process on the event list. Available filters: + - `container=`; -- container to filter + - `event=`; -- event to filter + - `image=`; -- image to filter + - `label=`; -- image and container label to filter + +Status Codes: + +- **200** – no error +- **500** – server error + +### Get a tarball containing all images in a repository + +`GET /images/(name)/get` + +Get a tarball containing all images and metadata for the repository specified +by `name`. + +If `name` is a specific name and tag (e.g. ubuntu:latest), then only that image +(and its parents) are returned. If `name` is an image ID, similarly only that +image (and its parents) are returned, but with the exclusion of the +'repositories' file in the tarball, as there were no image names referenced. + +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + GET /images/ubuntu/get + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + Binary data stream + +Status Codes: + +- **200** – no error +- **500** – server error + +### Get a tarball containing all images. + +`GET /images/get` + +Get a tarball containing all images and metadata for one or more repositories. + +For each value of the `names` parameter: if it is a specific name and tag (e.g. +`ubuntu:latest`), then only that image (and its parents) are returned; if it is +an image ID, similarly only that image (and its parents) are returned and there +would be no names referenced in the 'repositories' file for this image ID. + +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + GET /images/get?names=myname%2Fmyapp%3Alatest&names=busybox + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + Binary data stream + +Status Codes: + +- **200** – no error +- **500** – server error + +### Load a tarball with a set of images and tags into docker + +`POST /images/load` + +Load a set of images and tags into a Docker repository. +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + POST /images/load + + Tarball in body + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – no error +- **500** – server error + +### Image tarball format + +An image tarball contains one directory per image layer (named using its long ID), +each containing these files: + +- `VERSION`: currently `1.0` - the file format version +- `json`: detailed layer information, similar to `docker inspect layer_id` +- `layer.tar`: A tarfile containing the filesystem changes in this layer + +The `layer.tar` file contains `aufs` style `.wh..wh.aufs` files and directories +for storing attribute changes and deletions. + +If the tarball defines a repository, the tarball should also include a `repositories` file at +the root that contains a list of repository and tag names mapped to layer IDs. + +``` +{"hello-world": + {"latest": "565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} +} +``` + +### Exec Create + +`POST /containers/(id or name)/exec` + +Sets up an exec instance in a running container `id` + +**Example request**: + + POST /containers/e90e34656806/exec HTTP/1.1 + Content-Type: application/json + + { + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "Cmd": [ + "date" + ] + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "Id": "f90e34656806", + "Warnings":[] + } + +Json Parameters: + +- **AttachStdin** - Boolean value, attaches to `stdin` of the `exec` command. +- **AttachStdout** - Boolean value, attaches to `stdout` of the `exec` command. +- **AttachStderr** - Boolean value, attaches to `stderr` of the `exec` command. +- **Tty** - Boolean value to allocate a pseudo-TTY. +- **Cmd** - Command to run specified as a string or an array of strings. + + +Status Codes: + +- **201** – no error +- **404** – no such container +- **409** - container is paused +- **500** - server error + +### Exec Start + +`POST /exec/(id)/start` + +Starts a previously set up `exec` instance `id`. If `detach` is true, this API +returns after starting the `exec` command. Otherwise, this API sets up an +interactive session with the `exec` command. + +**Example request**: + + POST /exec/e90e34656806/start HTTP/1.1 + Content-Type: application/json + + { + "Detach": false, + "Tty": false + } + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + +Json Parameters: + +- **Detach** - Detach from the `exec` command. +- **Tty** - Boolean value to allocate a pseudo-TTY. + +Status Codes: + +- **200** – no error +- **404** – no such exec instance +- **409** - container is paused + + **Stream details**: + Similar to the stream behavior of `POST /containers/(id or name)/attach` API + +### Exec Resize + +`POST /exec/(id)/resize` + +Resizes the `tty` session used by the `exec` command `id`. The unit is number of characters. +This API is valid only if `tty` was specified as part of creating and starting the `exec` command. + +**Example request**: + + POST /exec/e90e34656806/resize?h=40&w=80 HTTP/1.1 + Content-Type: text/plain + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: text/plain + +Query Parameters: + +- **h** – height of `tty` session +- **w** – width + +Status Codes: + +- **201** – no error +- **404** – no such exec instance + +### Exec Inspect + +`GET /exec/(id)/json` + +Return low-level information about the `exec` command `id`. + +**Example request**: + + GET /exec/11fb006128e8ceb3942e7c58d77750f24210e35f879dd204ac975c184b820b39/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: plain/text + + { + "ID" : "11fb006128e8ceb3942e7c58d77750f24210e35f879dd204ac975c184b820b39", + "Running" : false, + "ExitCode" : 2, + "ProcessConfig" : { + "privileged" : false, + "user" : "", + "tty" : false, + "entrypoint" : "sh", + "arguments" : [ + "-c", + "exit 2" + ] + }, + "OpenStdin" : false, + "OpenStderr" : false, + "OpenStdout" : false, + "Container" : { + "State" : { + "Status" : "running", + "Running" : true, + "Paused" : false, + "Restarting" : false, + "OOMKilled" : false, + "Pid" : 3650, + "ExitCode" : 0, + "Error" : "", + "StartedAt" : "2014-11-17T22:26:03.717657531Z", + "FinishedAt" : "0001-01-01T00:00:00Z" + }, + "ID" : "8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c", + "Created" : "2014-11-17T22:26:03.626304998Z", + "Path" : "date", + "Args" : [], + "Config" : { + "Hostname" : "8f177a186b97", + "Domainname" : "", + "User" : "", + "AttachStdin" : false, + "AttachStdout" : false, + "AttachStderr" : false, + "ExposedPorts" : null, + "Tty" : false, + "OpenStdin" : false, + "StdinOnce" : false, + "Env" : [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ], + "Cmd" : [ + "date" + ], + "Image" : "ubuntu", + "Volumes" : null, + "WorkingDir" : "", + "Entrypoint" : null, + "NetworkDisabled" : false, + "MacAddress" : "", + "OnBuild" : null, + "SecurityOpt" : null + }, + "Image" : "5506de2b643be1e6febbf3b8a240760c6843244c41e12aa2f60ccbb7153d17f5", + "NetworkSettings": { + "Bridge": "", + "SandboxID": "", + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "Ports": null, + "SandboxKey": "", + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "", + "Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "", + "IPPrefixLen": 0, + "IPv6Gateway": "", + "MacAddress": "", + "Networks": { + "bridge": { + "EndpointID": "", + "Gateway": "", + "IPAddress": "", + "IPPrefixLen": 0, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "" + } + } + }, + "ResolvConfPath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/resolv.conf", + "HostnamePath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/hostname", + "HostsPath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/hosts", + "LogPath": "/var/lib/docker/containers/1eb5fabf5a03807136561b3c00adcd2992b535d624d5e18b6cdc6a6844d9767b/1eb5fabf5a03807136561b3c00adcd2992b535d624d5e18b6cdc6a6844d9767b-json.log", + "Name" : "/test", + "Driver" : "aufs", + "ExecDriver" : "native-0.2", + "MountLabel" : "", + "ProcessLabel" : "", + "AppArmorProfile" : "", + "RestartCount" : 0, + "Mounts" : [] + } + } + +Status Codes: + +- **200** – no error +- **404** – no such exec instance +- **500** - server error + +## 2.4 Volumes + +### List volumes + +`GET /volumes` + +**Example request**: + + GET /volumes HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Volumes": [ + { + "Name": "tardis", + "Driver": "local", + "Mountpoint": "/var/lib/docker/volumes/tardis" + } + ] + } + +Query Parameters: + +- **filters** - JSON encoded value of the filters (a `map[string][]string`) to process on the volumes list. There is one available filter: `dangling=true` + +Status Codes: + +- **200** - no error +- **500** - server error + +### Create a volume + +`POST /volumes/create` + +Create a volume + +**Example request**: + + POST /volumes/create HTTP/1.1 + Content-Type: application/json + + { + "Name": "tardis" + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "Name": "tardis", + "Driver": "local", + "Mountpoint": "/var/lib/docker/volumes/tardis" + } + +Status Codes: + +- **201** - no error +- **500** - server error + +JSON Parameters: + +- **Name** - The new volume's name. If not specified, Docker generates a name. +- **Driver** - Name of the volume driver to use. Defaults to `local` for the name. +- **DriverOpts** - A mapping of driver options and values. These options are + passed directly to the driver and are driver specific. + +### Inspect a volume + +`GET /volumes/(name)` + +Return low-level information on the volume `name` + +**Example request**: + + GET /volumes/tardis + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Name": "tardis", + "Driver": "local", + "Mountpoint": "/var/lib/docker/volumes/tardis" + } + +Status Codes: + +- **200** - no error +- **404** - no such volume +- **500** - server error + +### Remove a volume + +`DELETE /volumes/(name)` + +Instruct the driver to remove the volume (`name`). + +**Example request**: + + DELETE /volumes/tardis HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes + +- **204** - no error +- **404** - no such volume or volume driver +- **409** - volume is in use and cannot be removed +- **500** - server error + +## 2.5 Networks + +### List networks + +`GET /networks` + +**Example request**: + + GET /networks HTTP/1.1 + +**Example response**: + +``` +HTTP/1.1 200 OK +Content-Type: application/json + +[ + { + "Name": "bridge", + "Id": "f2de39df4171b0dc801e8002d1d999b77256983dfc63041c0f34030aa3977566", + "Scope": "local", + "Driver": "bridge", + "IPAM": { + "Driver": "default", + "Config": [ + { + "Subnet": "172.17.0.0/16" + } + ] + }, + "Containers": { + "39b69226f9d79f5634485fb236a23b2fe4e96a0a94128390a7fbbcc167065867": { + "EndpointID": "ed2419a97c1d9954d05b46e462e7002ea552f216e9b136b80a7db8d98b442eda", + "MacAddress": "02:42:ac:11:00:02", + "IPv4Address": "172.17.0.2/16", + "IPv6Address": "" + } + }, + "Options": { + "com.docker.network.bridge.default_bridge": "true", + "com.docker.network.bridge.enable_icc": "true", + "com.docker.network.bridge.enable_ip_masquerade": "true", + "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", + "com.docker.network.bridge.name": "docker0", + "com.docker.network.driver.mtu": "1500" + } + }, + { + "Name": "none", + "Id": "e086a3893b05ab69242d3c44e49483a3bbbd3a26b46baa8f61ab797c1088d794", + "Scope": "local", + "Driver": "null", + "IPAM": { + "Driver": "default", + "Config": [] + }, + "Containers": {}, + "Options": {} + }, + { + "Name": "host", + "Id": "13e871235c677f196c4e1ecebb9dc733b9b2d2ab589e30c539efeda84a24215e", + "Scope": "local", + "Driver": "host", + "IPAM": { + "Driver": "default", + "Config": [] + }, + "Containers": {}, + "Options": {} + } +] +``` + + + +Query Parameters: + +- **filters** - JSON encoded value of the filters (a `map[string][]string`) to process on the networks list. Available filters: `name=[network-names]` , `id=[network-ids]` + +Status Codes: + +- **200** - no error +- **500** - server error + +### Inspect network + +`GET /networks/` + +**Example request**: + + GET /networks/f2de39df4171b0dc801e8002d1d999b77256983dfc63041c0f34030aa3977566 HTTP/1.1 + +**Example response**: + +``` +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "Name": "bridge", + "Id": "f2de39df4171b0dc801e8002d1d999b77256983dfc63041c0f34030aa3977566", + "Scope": "local", + "Driver": "bridge", + "IPAM": { + "Driver": "default", + "Config": [ + { + "Subnet": "172.17.0.0/16" + } + ] + }, + "Containers": { + "39b69226f9d79f5634485fb236a23b2fe4e96a0a94128390a7fbbcc167065867": { + "EndpointID": "ed2419a97c1d9954d05b46e462e7002ea552f216e9b136b80a7db8d98b442eda", + "MacAddress": "02:42:ac:11:00:02", + "IPv4Address": "172.17.0.2/16", + "IPv6Address": "" + } + }, + "Options": { + "com.docker.network.bridge.default_bridge": "true", + "com.docker.network.bridge.enable_icc": "true", + "com.docker.network.bridge.enable_ip_masquerade": "true", + "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", + "com.docker.network.bridge.name": "docker0", + "com.docker.network.driver.mtu": "1500" + } +} +``` + +Status Codes: + +- **200** - no error +- **404** - network not found + +### Create a network + +`POST /networks/create` + +Create a network + +**Example request**: + +``` +POST /networks/create HTTP/1.1 +Content-Type: application/json + +{ + "Name":"isolated_nw", + "Driver":"bridge" + "IPAM":{ + "Config":[{ + "Subnet":"172.20.0.0/16", + "IPRange":"172.20.10.0/24", + "Gateway":"172.20.10.11" + }] +} +``` + +**Example response**: + +``` +HTTP/1.1 201 Created +Content-Type: application/json + +{ + "Id": "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30", + "Warning": "" +} +``` + +Status Codes: + +- **201** - no error +- **404** - plugin not found +- **500** - server error + +JSON Parameters: + +- **Name** - The new network's name. this is a mandatory field +- **Driver** - Name of the network driver plugin to use. Defaults to `bridge` driver +- **IPAM** - Optional custom IP scheme for the network +- **Options** - Network specific options to be used by the drivers +- **CheckDuplicate** - Requests daemon to check for networks with same name + +### Connect a container to a network + +`POST /networks/(id)/connect` + +Connect a container to a network + +**Example request**: + +``` +POST /networks/22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30/connect HTTP/1.1 +Content-Type: application/json + +{ + "Container":"3613f73ba0e4" +} +``` + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** - no error +- **404** - network or container is not found +- **500** - Internal Server Error + +JSON Parameters: + +- **container** - container-id/name to be connected to the network + +### Disconnect a container from a network + +`POST /networks/(id)/disconnect` + +Disconnect a container from a network + +**Example request**: + +``` +POST /networks/22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30/disconnect HTTP/1.1 +Content-Type: application/json + +{ + "Container":"3613f73ba0e4" +} +``` + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** - no error +- **404** - network or container not found +- **500** - Internal Server Error + +JSON Parameters: + +- **Container** - container-id/name to be disconnected from a network + +### Remove a network + +`DELETE /networks/(id)` + +Instruct the driver to remove the network (`id`). + +**Example request**: + + DELETE /networks/22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes + +- **200** - no error +- **404** - no such network +- **500** - server error + +# 3. Going further + +## 3.1 Inside `docker run` + +As an example, the `docker run` command line makes the following API calls: + +- Create the container + +- If the status code is 404, it means the image doesn't exist: + - Try to pull it. + - Then, retry to create the container. + +- Start the container. + +- If you are not in detached mode: +- Attach to the container, using `logs=1` (to have `stdout` and + `stderr` from the container's start) and `stream=1` + +- If in detached mode or only `stdin` is attached, display the container's id. + +## 3.2 Hijacking + +In this version of the API, `/attach`, uses hijacking to transport `stdin`, +`stdout`, and `stderr` on the same socket. + +To hint potential proxies about connection hijacking, Docker client sends +connection upgrade headers similarly to websocket. + + Upgrade: tcp + Connection: Upgrade + +When Docker daemon detects the `Upgrade` header, it switches its status code +from **200 OK** to **101 UPGRADED** and resends the same headers. + + +## 3.3 CORS Requests + +To set cross origin requests to the remote api please give values to +`--api-cors-header` when running Docker in daemon mode. Set * (asterisk) allows all, +default or blank means CORS disabled + + $ docker daemon -H="192.168.1.9:2375" --api-cors-header="http://foo.bar" diff --git a/docs/reference/api/docker_remote_api_v1.22.md b/docs/reference/api/docker_remote_api_v1.22.md new file mode 100644 index 00000000..5d3e1166 --- /dev/null +++ b/docs/reference/api/docker_remote_api_v1.22.md @@ -0,0 +1,3142 @@ + + +# Docker Remote API v1.22 + +## 1. Brief introduction + + - The Remote API has replaced `rcli`. + - The daemon listens on `unix:///var/run/docker.sock` but you can + [Bind Docker to another host/port or a Unix socket](../../quickstart.md#bind-docker-to-another-host-port-or-a-unix-socket). + - The API tends to be REST. However, for some complex commands, like `attach` + or `pull`, the HTTP connection is hijacked to transport `stdout`, + `stdin` and `stderr`. + - When the client API version is newer than the daemon's, these calls return an HTTP + `400 Bad Request` error message. + +# 2. Endpoints + +## 2.1 Containers + +### List containers + +`GET /containers/json` + +List containers + +**Example request**: + + GET /containers/json?all=1&before=8dfafdbc3a40&size=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "8dfafdbc3a40", + "Names":["/boring_feynman"], + "Image": "ubuntu:latest", + "ImageID": "d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82", + "Command": "echo 1", + "Created": 1367854155, + "Status": "Exit 0", + "Ports": [{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "SizeRw": 12288, + "SizeRootFs": 0, + "HostConfig": { + "NetworkMode": "default" + }, + "NetworkSettings": { + "Networks": { + "bridge": { + "IPAMConfig": null, + "Links": null, + "Aliases": null, + "NetworkID": "7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812", + "EndpointID": "2cdc4edb1ded3631c81f57966563e5c8525b81121bb3706a9a9a3ae102711f3f", + "Gateway": "172.17.0.1", + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "02:42:ac:11:00:02" + } + } + } + }, + { + "Id": "9cd87474be90", + "Names":["/coolName"], + "Image": "ubuntu:latest", + "ImageID": "d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82", + "Command": "echo 222222", + "Created": 1367854155, + "Status": "Exit 0", + "Ports": [], + "Labels": {}, + "SizeRw": 12288, + "SizeRootFs": 0, + "HostConfig": { + "NetworkMode": "default" + }, + "NetworkSettings": { + "Networks": { + "bridge": { + "IPAMConfig": null, + "Links": null, + "Aliases": null, + "NetworkID": "7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812", + "EndpointID": "88eaed7b37b38c2a3f0c4bc796494fdf51b270c2d22656412a2ca5d559a64d7a", + "Gateway": "172.17.0.1", + "IPAddress": "172.17.0.8", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "02:42:ac:11:00:08" + } + } + } + + }, + { + "Id": "3176a2479c92", + "Names":["/sleepy_dog"], + "Image": "ubuntu:latest", + "ImageID": "d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82", + "Command": "echo 3333333333333333", + "Created": 1367854154, + "Status": "Exit 0", + "Ports":[], + "Labels": {}, + "SizeRw":12288, + "SizeRootFs":0, + "HostConfig": { + "NetworkMode": "default" + }, + "NetworkSettings": { + "Networks": { + "bridge": { + "IPAMConfig": null, + "Links": null, + "Aliases": null, + "NetworkID": "7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812", + "EndpointID": "8b27c041c30326d59cd6e6f510d4f8d1d570a228466f956edf7815508f78e30d", + "Gateway": "172.17.0.1", + "IPAddress": "172.17.0.6", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "02:42:ac:11:00:06" + } + } + } + + }, + { + "Id": "4cb07b47f9fb", + "Names":["/running_cat"], + "Image": "ubuntu:latest", + "ImageID": "d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82", + "Command": "echo 444444444444444444444444444444444", + "Created": 1367854152, + "Status": "Exit 0", + "Ports": [], + "Labels": {}, + "SizeRw": 12288, + "SizeRootFs": 0, + "HostConfig": { + "NetworkMode": "default" + }, + "NetworkSettings": { + "Networks": { + "bridge": { + "IPAMConfig": null, + "Links": null, + "Aliases": null, + "NetworkID": "7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812", + "EndpointID": "d91c7b2f0644403d7ef3095985ea0e2370325cd2332ff3a3225c4247328e66e9", + "Gateway": "172.17.0.1", + "IPAddress": "172.17.0.5", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "02:42:ac:11:00:05" + } + } + } + + } + ] + +Query Parameters: + +- **all** – 1/True/true or 0/False/false, Show all containers. + Only running containers are shown by default (i.e., this defaults to false) +- **limit** – Show `limit` last created + containers, include non-running ones. +- **since** – Show only containers created since Id, include + non-running ones. +- **before** – Show only containers created before Id, include + non-running ones. +- **size** – 1/True/true or 0/False/false, Show the containers + sizes +- **filters** - a JSON encoded value of the filters (a `map[string][]string`) to process on the containers list. Available filters: + - `exited=`; -- containers with exit code of `` ; + - `status=`(`created`|`restarting`|`running`|`paused`|`exited`|`dead`) + - `label=key` or `label="key=value"` of a container label + - `isolation=`(`default`|`process`|`hyperv`) (Windows daemon only) + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **500** – server error + +### Create a container + +`POST /containers/create` + +Create a container + +**Example request**: + + POST /containers/create HTTP/1.1 + Content-Type: application/json + + { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "FOO=bar", + "BAZ=quux" + ], + "Cmd": [ + "date" + ], + "Entrypoint": "", + "Image": "ubuntu", + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "Volumes": { + "/volumes/data": {} + }, + "WorkingDir": "", + "NetworkDisabled": false, + "MacAddress": "12:34:56:78:9a:bc", + "ExposedPorts": { + "22/tcp": {} + }, + "StopSignal": "SIGTERM", + "HostConfig": { + "Binds": ["/tmp:/tmp"], + "Links": ["redis3:redis"], + "Memory": 0, + "MemorySwap": 0, + "MemoryReservation": 0, + "KernelMemory": 0, + "CpuShares": 512, + "CpuPeriod": 100000, + "CpuQuota": 50000, + "CpusetCpus": "0,1", + "CpusetMems": "0,1", + "BlkioWeight": 300, + "BlkioWeightDevice": [{}], + "BlkioDeviceReadBps": [{}], + "BlkioDeviceReadIOps": [{}], + "BlkioDeviceWriteBps": [{}], + "BlkioDeviceWriteIOps": [{}], + "MemorySwappiness": 60, + "OomKillDisable": false, + "OomScoreAdj": 500, + "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] }, + "PublishAllPorts": false, + "Privileged": false, + "ReadonlyRootfs": false, + "Dns": ["8.8.8.8"], + "DnsOptions": [""], + "DnsSearch": [""], + "ExtraHosts": null, + "VolumesFrom": ["parent", "other:ro"], + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"], + "GroupAdd": ["newgroup"], + "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 }, + "NetworkMode": "bridge", + "Devices": [], + "Ulimits": [{}], + "LogConfig": { "Type": "json-file", "Config": {} }, + "SecurityOpt": [], + "CgroupParent": "", + "VolumeDriver": "", + "ShmSize": 67108864 + }, + "NetworkingConfig": { + "EndpointsConfig": { + "isolated_nw" : { + "IPAMConfig": { + "IPv4Address":"172.20.30.33", + "IPv6Address":"2001:db8:abcd::3033" + }, + "Links":["container_1", "container_2"], + "Aliases":["server_x", "server_y"] + } + } + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "Id":"e90e34656806", + "Warnings":[] + } + +Json Parameters: + +- **Hostname** - A string value containing the hostname to use for the + container. +- **Domainname** - A string value containing the domain name to use + for the container. +- **User** - A string value specifying the user inside the container. +- **Memory** - Memory limit in bytes. +- **MemorySwap** - Total memory limit (memory + swap); set `-1` to enable unlimited swap. + You must use this with `memory` and make the swap value larger than `memory`. +- **MemoryReservation** - Memory soft limit in bytes. +- **KernelMemory** - Kernel memory limit in bytes. +- **CpuShares** - An integer value containing the container's CPU Shares + (ie. the relative weight vs other containers). +- **CpuPeriod** - The length of a CPU period in microseconds. +- **CpuQuota** - Microseconds of CPU time that the container can get in a CPU period. +- **Cpuset** - Deprecated please don't use. Use `CpusetCpus` instead. +- **CpusetCpus** - String value containing the `cgroups CpusetCpus` to use. +- **CpusetMems** - Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems. +- **BlkioWeight** - Block IO weight (relative weight) accepts a weight value between 10 and 1000. +- **BlkioWeightDevice** - Block IO weight (relative device weight) in the form of: `"BlkioWeightDevice": [{"Path": "device_path", "Weight": weight}]` +- **BlkioDeviceReadBps** - Limit read rate (bytes per second) from a device in the form of: `"BlkioDeviceReadBps": [{"Path": "device_path", "Rate": rate}]`, for example: + `"BlkioDeviceReadBps": [{"Path": "/dev/sda", "Rate": "1024"}]"` +- **BlkioDeviceWriteBps** - Limit write rate (bytes per second) to a device in the form of: `"BlkioDeviceWriteBps": [{"Path": "device_path", "Rate": rate}]`, for example: + `"BlkioDeviceWriteBps": [{"Path": "/dev/sda", "Rate": "1024"}]"` +- **BlkioDeviceReadIOps** - Limit read rate (IO per second) from a device in the form of: `"BlkioDeviceReadIOps": [{"Path": "device_path", "Rate": rate}]`, for example: + `"BlkioDeviceReadIOps": [{"Path": "/dev/sda", "Rate": "1000"}]` +- **BlkioDeviceWiiteIOps** - Limit write rate (IO per second) to a device in the form of: `"BlkioDeviceWriteIOps": [{"Path": "device_path", "Rate": rate}]`, for example: + `"BlkioDeviceWriteIOps": [{"Path": "/dev/sda", "Rate": "1000"}]` +- **MemorySwappiness** - Tune a container's memory swappiness behavior. Accepts an integer between 0 and 100. +- **OomKillDisable** - Boolean value, whether to disable OOM Killer for the container or not. +- **OomScoreAdj** - An integer value containing the score given to the container in order to tune OOM killer preferences. +- **AttachStdin** - Boolean value, attaches to `stdin`. +- **AttachStdout** - Boolean value, attaches to `stdout`. +- **AttachStderr** - Boolean value, attaches to `stderr`. +- **Tty** - Boolean value, Attach standard streams to a `tty`, including `stdin` if it is not closed. +- **OpenStdin** - Boolean value, opens stdin, +- **StdinOnce** - Boolean value, close `stdin` after the 1 attached client disconnects. +- **Env** - A list of environment variables in the form of `["VAR=value"[,"VAR2=value2"]]` +- **Labels** - Adds a map of labels to a container. To specify a map: `{"key":"value"[,"key2":"value2"]}` +- **Cmd** - Command to run specified as a string or an array of strings. +- **Entrypoint** - Set the entry point for the container as a string or an array + of strings. +- **Image** - A string specifying the image name to use for the container. +- **Volumes** - An object mapping mount point paths (strings) inside the + container to empty objects. +- **WorkingDir** - A string specifying the working directory for commands to + run in. +- **NetworkDisabled** - Boolean value, when true disables networking for the + container +- **ExposedPorts** - An object mapping ports to an empty object in the form of: + `"ExposedPorts": { "/: {}" }` +- **StopSignal** - Signal to stop a container as a string or unsigned integer. `SIGTERM` by default. +- **HostConfig** + - **Binds** – A list of volume bindings for this container. Each volume binding is a string in one of these forms: + + `host_path:container_path` to bind-mount a host path into the container + + `host_path:container_path:ro` to make the bind-mount read-only inside the container. + + `volume_name:container_path` to bind-mount a volume managed by a volume plugin into the container. + + `volume_name:container_path:ro` to make the bind mount read-only inside the container. + - **Links** - A list of links for the container. Each link entry should be + in the form of `container_name:alias`. + - **PortBindings** - A map of exposed container ports and the host port they + should map to. A JSON object in the form + `{ /: [{ "HostPort": "" }] }` + Take note that `port` is specified as a string and not an integer value. + - **PublishAllPorts** - Allocates a random host port for all of a container's + exposed ports. Specified as a boolean value. + - **Privileged** - Gives the container full access to the host. Specified as + a boolean value. + - **ReadonlyRootfs** - Mount the container's root filesystem as read only. + Specified as a boolean value. + - **Dns** - A list of DNS servers for the container to use. + - **DnsOptions** - A list of DNS options + - **DnsSearch** - A list of DNS search domains + - **ExtraHosts** - A list of hostnames/IP mappings to add to the + container's `/etc/hosts` file. Specified in the form `["hostname:IP"]`. + - **VolumesFrom** - A list of volumes to inherit from another container. + Specified in the form `[:]` + - **CapAdd** - A list of kernel capabilities to add to the container. + - **Capdrop** - A list of kernel capabilities to drop from the container. + - **GroupAdd** - A list of additional groups that the container process will run as + - **RestartPolicy** – The behavior to apply when the container exits. The + value is an object with a `Name` property of either `"always"` to + always restart, `"unless-stopped"` to restart always except when + user has manually stopped the container or `"on-failure"` to restart only when the container + exit code is non-zero. If `on-failure` is used, `MaximumRetryCount` + controls the number of times to retry before giving up. + The default is not to restart. (optional) + An ever increasing delay (double the previous delay, starting at 100mS) + is added before each restart to prevent flooding the server. + - **NetworkMode** - Sets the networking mode for the container. Supported + standard values are: `bridge`, `host`, `none`, and `container:`. Any other value is taken + as a custom network's name to which this container should connect to. + - **Devices** - A list of devices to add to the container specified as a JSON object in the + form + `{ "PathOnHost": "/dev/deviceName", "PathInContainer": "/dev/deviceName", "CgroupPermissions": "mrw"}` + - **Ulimits** - A list of ulimits to set in the container, specified as + `{ "Name": , "Soft": , "Hard": }`, for example: + `Ulimits: { "Name": "nofile", "Soft": 1024, "Hard": 2048 }` + - **SecurityOpt**: A list of string values to customize labels for MLS + systems, such as SELinux. + - **LogConfig** - Log configuration for the container, specified as a JSON object in the form + `{ "Type": "", "Config": {"key1": "val1"}}`. + Available types: `json-file`, `syslog`, `journald`, `gelf`, `awslogs`, `splunk`, `none`. + `json-file` logging driver. + - **CgroupParent** - Path to `cgroups` under which the container's `cgroup` is created. If the path is not absolute, the path is considered to be relative to the `cgroups` path of the init process. Cgroups are created if they do not already exist. + - **VolumeDriver** - Driver that this container users to mount volumes. + - **ShmSize** - Size of `/dev/shm` in bytes. The size must be greater than 0. If omitted the system uses 64MB. + +Query Parameters: + +- **name** – Assign the specified name to the container. Must + match `/?[a-zA-Z0-9_-]+`. + +Status Codes: + +- **201** – no error +- **404** – no such container +- **406** – impossible to attach (container not running) +- **500** – server error + +### Inspect a container + +`GET /containers/(id or name)/json` + +Return low-level information on the container `id` + + +**Example request**: + + GET /containers/4fa6e0f0c678/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "AppArmorProfile": "", + "Args": [ + "-c", + "exit 9" + ], + "Config": { + "AttachStderr": true, + "AttachStdin": false, + "AttachStdout": true, + "Cmd": [ + "/bin/sh", + "-c", + "exit 9" + ], + "Domainname": "", + "Entrypoint": null, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "ExposedPorts": null, + "Hostname": "ba033ac44011", + "Image": "ubuntu", + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "MacAddress": "", + "NetworkDisabled": false, + "OnBuild": null, + "OpenStdin": false, + "StdinOnce": false, + "Tty": false, + "User": "", + "Volumes": { + "/volumes/data": {} + }, + "WorkingDir": "", + "StopSignal": "SIGTERM" + }, + "Created": "2015-01-06T15:47:31.485331387Z", + "Driver": "devicemapper", + "ExecDriver": "native-0.2", + "ExecIDs": null, + "HostConfig": { + "Binds": null, + "BlkioWeight": 0, + "BlkioWeightDevice": [{}], + "BlkioDeviceReadBps": [{}], + "BlkioDeviceWriteBps": [{}], + "BlkioDeviceReadIOps": [{}], + "BlkioDeviceWriteIOps": [{}], + "CapAdd": null, + "CapDrop": null, + "ContainerIDFile": "", + "CpusetCpus": "", + "CpusetMems": "", + "CpuShares": 0, + "CpuPeriod": 100000, + "Devices": [], + "Dns": null, + "DnsOptions": null, + "DnsSearch": null, + "ExtraHosts": null, + "IpcMode": "", + "Links": null, + "LxcConf": [], + "Memory": 0, + "MemorySwap": 0, + "MemoryReservation": 0, + "KernelMemory": 0, + "OomKillDisable": false, + "OomScoreAdj": 500, + "NetworkMode": "bridge", + "PortBindings": {}, + "Privileged": false, + "ReadonlyRootfs": false, + "PublishAllPorts": false, + "RestartPolicy": { + "MaximumRetryCount": 2, + "Name": "on-failure" + }, + "LogConfig": { + "Config": null, + "Type": "json-file" + }, + "SecurityOpt": null, + "VolumesFrom": null, + "Ulimits": [{}], + "VolumeDriver": "", + "ShmSize": 67108864 + }, + "HostnamePath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/hostname", + "HostsPath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/hosts", + "LogPath": "/var/lib/docker/containers/1eb5fabf5a03807136561b3c00adcd2992b535d624d5e18b6cdc6a6844d9767b/1eb5fabf5a03807136561b3c00adcd2992b535d624d5e18b6cdc6a6844d9767b-json.log", + "Id": "ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39", + "Image": "04c5d3b7b0656168630d3ba35d8889bd0e9caafcaeb3004d2bfbc47e7c5d35d2", + "MountLabel": "", + "Name": "/boring_euclid", + "NetworkSettings": { + "Bridge": "", + "SandboxID": "", + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "Ports": null, + "SandboxKey": "", + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "", + "Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "", + "IPPrefixLen": 0, + "IPv6Gateway": "", + "MacAddress": "", + "Networks": { + "bridge": { + "NetworkID": "7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812", + "EndpointID": "7587b82f0dada3656fda26588aee72630c6fab1536d36e394b2bfbcf898c971d", + "Gateway": "172.17.0.1", + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "02:42:ac:12:00:02" + } + } + }, + "Path": "/bin/sh", + "ProcessLabel": "", + "ResolvConfPath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/resolv.conf", + "RestartCount": 1, + "State": { + "Error": "", + "ExitCode": 9, + "FinishedAt": "2015-01-06T15:47:32.080254511Z", + "OOMKilled": false, + "Dead": false, + "Paused": false, + "Pid": 0, + "Restarting": false, + "Running": true, + "StartedAt": "2015-01-06T15:47:32.072697474Z", + "Status": "running" + }, + "Mounts": [ + { + "Name": "fac362...80535", + "Source": "/data", + "Destination": "/data", + "Driver": "local", + "Mode": "ro,Z", + "RW": false, + "Propagation": "" + } + ] + } + +**Example request, with size information**: + + GET /containers/4fa6e0f0c678/json?size=1 HTTP/1.1 + +**Example response, with size information**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + .... + "SizeRw": 0, + "SizeRootFs": 972, + .... + } + +Query Parameters: + +- **size** – 1/True/true or 0/False/false, return container size information. Default is `false`. + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### List processes running inside a container + +`GET /containers/(id or name)/top` + +List processes running inside the container `id`. On Unix systems this +is done by running the `ps` command. This endpoint is not +supported on Windows. + +**Example request**: + + GET /containers/4fa6e0f0c678/top HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Titles" : [ + "UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD" + ], + "Processes" : [ + [ + "root", "13642", "882", "0", "17:03", "pts/0", "00:00:00", "/bin/bash" + ], + [ + "root", "13735", "13642", "0", "17:06", "pts/0", "00:00:00", "sleep 10" + ] + ] + } + +**Example request**: + + GET /containers/4fa6e0f0c678/top?ps_args=aux HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Titles" : [ + "USER","PID","%CPU","%MEM","VSZ","RSS","TTY","STAT","START","TIME","COMMAND" + ] + "Processes" : [ + [ + "root","13642","0.0","0.1","18172","3184","pts/0","Ss","17:03","0:00","/bin/bash" + ], + [ + "root","13895","0.0","0.0","4348","692","pts/0","S+","17:15","0:00","sleep 10" + ] + ], + } + +Query Parameters: + +- **ps_args** – `ps` arguments to use (e.g., `aux`), defaults to `-ef` + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Get container logs + +`GET /containers/(id or name)/logs` + +Get `stdout` and `stderr` logs from the container ``id`` + +> **Note**: +> This endpoint works only for containers with the `json-file` or `journald` logging drivers. + +**Example request**: + + GET /containers/4fa6e0f0c678/logs?stderr=1&stdout=1×tamps=1&follow=1&tail=10&since=1428990821 HTTP/1.1 + +**Example response**: + + HTTP/1.1 101 UPGRADED + Content-Type: application/vnd.docker.raw-stream + Connection: Upgrade + Upgrade: tcp + + {{ STREAM }} + +Query Parameters: + +- **follow** – 1/True/true or 0/False/false, return stream. Default `false`. +- **stdout** – 1/True/true or 0/False/false, show `stdout` log. Default `false`. +- **stderr** – 1/True/true or 0/False/false, show `stderr` log. Default `false`. +- **since** – UNIX timestamp (integer) to filter logs. Specifying a timestamp + will only output log-entries since that timestamp. Default: 0 (unfiltered) +- **timestamps** – 1/True/true or 0/False/false, print timestamps for + every log line. Default `false`. +- **tail** – Output specified number of lines at the end of logs: `all` or ``. Default all. + +Status Codes: + +- **101** – no error, hints proxy about hijacking +- **200** – no error, no upgrade header found +- **404** – no such container +- **500** – server error + +### Inspect changes on a container's filesystem + +`GET /containers/(id or name)/changes` + +Inspect changes on container `id`'s filesystem + +**Example request**: + + GET /containers/4fa6e0f0c678/changes HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Path": "/dev", + "Kind": 0 + }, + { + "Path": "/dev/kmsg", + "Kind": 1 + }, + { + "Path": "/test", + "Kind": 1 + } + ] + +Values for `Kind`: + +- `0`: Modify +- `1`: Add +- `2`: Delete + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Export a container + +`GET /containers/(id or name)/export` + +Export the contents of container `id` + +**Example request**: + + GET /containers/4fa6e0f0c678/export HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/octet-stream + + {{ TAR STREAM }} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Get container stats based on resource usage + +`GET /containers/(id or name)/stats` + +This endpoint returns a live stream of a container's resource usage statistics. + +**Example request**: + + GET /containers/redis1/stats HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "read" : "2015-01-08T22:57:31.547920715Z", + "networks": { + "eth0": { + "rx_bytes": 5338, + "rx_dropped": 0, + "rx_errors": 0, + "rx_packets": 36, + "tx_bytes": 648, + "tx_dropped": 0, + "tx_errors": 0, + "tx_packets": 8 + }, + "eth5": { + "rx_bytes": 4641, + "rx_dropped": 0, + "rx_errors": 0, + "rx_packets": 26, + "tx_bytes": 690, + "tx_dropped": 0, + "tx_errors": 0, + "tx_packets": 9 + } + }, + "memory_stats" : { + "stats" : { + "total_pgmajfault" : 0, + "cache" : 0, + "mapped_file" : 0, + "total_inactive_file" : 0, + "pgpgout" : 414, + "rss" : 6537216, + "total_mapped_file" : 0, + "writeback" : 0, + "unevictable" : 0, + "pgpgin" : 477, + "total_unevictable" : 0, + "pgmajfault" : 0, + "total_rss" : 6537216, + "total_rss_huge" : 6291456, + "total_writeback" : 0, + "total_inactive_anon" : 0, + "rss_huge" : 6291456, + "hierarchical_memory_limit" : 67108864, + "total_pgfault" : 964, + "total_active_file" : 0, + "active_anon" : 6537216, + "total_active_anon" : 6537216, + "total_pgpgout" : 414, + "total_cache" : 0, + "inactive_anon" : 0, + "active_file" : 0, + "pgfault" : 964, + "inactive_file" : 0, + "total_pgpgin" : 477 + }, + "max_usage" : 6651904, + "usage" : 6537216, + "failcnt" : 0, + "limit" : 67108864 + }, + "blkio_stats" : {}, + "cpu_stats" : { + "cpu_usage" : { + "percpu_usage" : [ + 8646879, + 24472255, + 36438778, + 30657443 + ], + "usage_in_usermode" : 50000000, + "total_usage" : 100215355, + "usage_in_kernelmode" : 30000000 + }, + "system_cpu_usage" : 739306590000000, + "throttling_data" : {"periods":0,"throttled_periods":0,"throttled_time":0} + }, + "precpu_stats" : { + "cpu_usage" : { + "percpu_usage" : [ + 8646879, + 24350896, + 36438778, + 30657443 + ], + "usage_in_usermode" : 50000000, + "total_usage" : 100093996, + "usage_in_kernelmode" : 30000000 + }, + "system_cpu_usage" : 9492140000000, + "throttling_data" : {"periods":0,"throttled_periods":0,"throttled_time":0} + } + } + +The precpu_stats is the cpu statistic of last read, which is used for calculating the cpu usage percent. It is not the exact copy of the “cpu_stats” field. + +Query Parameters: + +- **stream** – 1/True/true or 0/False/false, pull stats once then disconnect. Default `true`. + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Resize a container TTY + +`POST /containers/(id or name)/resize` + +Resize the TTY for container with `id`. The unit is number of characters. You must restart the container for the resize to take effect. + +**Example request**: + + POST /containers/4fa6e0f0c678/resize?h=40&w=80 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Length: 0 + Content-Type: text/plain; charset=utf-8 + +Query Parameters: + +- **h** – height of `tty` session +- **w** – width + +Status Codes: + +- **200** – no error +- **404** – No such container +- **500** – Cannot resize container + +### Start a container + +`POST /containers/(id or name)/start` + +Start the container `id` + +> **Note**: +> For backwards compatibility, this endpoint accepts a `HostConfig` as JSON-encoded request body. +> See [create a container](#create-a-container) for details. + +**Example request**: + + POST /containers/e90e34656806/start HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **detachKeys** – Override the key sequence for detaching a + container. Format is a single character `[a-Z]` or `ctrl-` + where `` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`. + +Status Codes: + +- **204** – no error +- **304** – container already started +- **404** – no such container +- **500** – server error + +### Stop a container + +`POST /containers/(id or name)/stop` + +Stop the container `id` + +**Example request**: + + POST /containers/e90e34656806/stop?t=5 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **t** – number of seconds to wait before killing the container + +Status Codes: + +- **204** – no error +- **304** – container already stopped +- **404** – no such container +- **500** – server error + +### Restart a container + +`POST /containers/(id or name)/restart` + +Restart the container `id` + +**Example request**: + + POST /containers/e90e34656806/restart?t=5 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **t** – number of seconds to wait before killing the container + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Kill a container + +`POST /containers/(id or name)/kill` + +Kill the container `id` + +**Example request**: + + POST /containers/e90e34656806/kill HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters + +- **signal** - Signal to send to the container: integer or string like `SIGINT`. + When not set, `SIGKILL` is assumed and the call waits for the container to exit. + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Update a container + +`POST /containers/(id or name)/update` + +Update resource configs of one or more containers. + +**Example request**: + + POST /containers/e90e34656806/update HTTP/1.1 + Content-Type: application/json + + { + "BlkioWeight": 300, + "CpuShares": 512, + "CpuPeriod": 100000, + "CpuQuota": 50000, + "CpusetCpus": "0,1", + "CpusetMems": "0", + "Memory": 314572800, + "MemorySwap": 514288000, + "MemoryReservation": 209715200, + "KernelMemory": 52428800, + } + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Warnings": [] + } + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + +### Rename a container + +`POST /containers/(id or name)/rename` + +Rename the container `id` to a `new_name` + +**Example request**: + + POST /containers/e90e34656806/rename?name=new_name HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **name** – new name for the container + +Status Codes: + +- **204** – no error +- **404** – no such container +- **409** - conflict name already assigned +- **500** – server error + +### Pause a container + +`POST /containers/(id or name)/pause` + +Pause the container `id` + +**Example request**: + + POST /containers/e90e34656806/pause HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Unpause a container + +`POST /containers/(id or name)/unpause` + +Unpause the container `id` + +**Example request**: + + POST /containers/e90e34656806/unpause HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Attach to a container + +`POST /containers/(id or name)/attach` + +Attach to the container `id` + +**Example request**: + + POST /containers/16253994b7c4/attach?logs=1&stream=0&stdout=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 101 UPGRADED + Content-Type: application/vnd.docker.raw-stream + Connection: Upgrade + Upgrade: tcp + + {{ STREAM }} + +Query Parameters: + +- **detachKeys** – Override the key sequence for detaching a + container. Format is a single character `[a-Z]` or `ctrl-` + where `` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`. +- **logs** – 1/True/true or 0/False/false, return logs. Default `false`. +- **stream** – 1/True/true or 0/False/false, return stream. + Default `false`. +- **stdin** – 1/True/true or 0/False/false, if `stream=true`, attach + to `stdin`. Default `false`. +- **stdout** – 1/True/true or 0/False/false, if `logs=true`, return + `stdout` log, if `stream=true`, attach to `stdout`. Default `false`. +- **stderr** – 1/True/true or 0/False/false, if `logs=true`, return + `stderr` log, if `stream=true`, attach to `stderr`. Default `false`. + +Status Codes: + +- **101** – no error, hints proxy about hijacking +- **200** – no error, no upgrade header found +- **400** – bad parameter +- **404** – no such container +- **500** – server error + + **Stream details**: + + When using the TTY setting is enabled in + [`POST /containers/create` + ](#create-a-container), + the stream is the raw data from the process PTY and client's `stdin`. + When the TTY is disabled, then the stream is multiplexed to separate + `stdout` and `stderr`. + + The format is a **Header** and a **Payload** (frame). + + **HEADER** + + The header contains the information which the stream writes (`stdout` or + `stderr`). It also contains the size of the associated frame encoded in the + last four bytes (`uint32`). + + It is encoded on the first eight bytes like this: + + header := [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4} + + `STREAM_TYPE` can be: + +- 0: `stdin` (is written on `stdout`) +- 1: `stdout` +- 2: `stderr` + + `SIZE1, SIZE2, SIZE3, SIZE4` are the four bytes of + the `uint32` size encoded as big endian. + + **PAYLOAD** + + The payload is the raw stream. + + **IMPLEMENTATION** + + The simplest way to implement the Attach protocol is the following: + + 1. Read eight bytes. + 2. Choose `stdout` or `stderr` depending on the first byte. + 3. Extract the frame size from the last four bytes. + 4. Read the extracted size and output it on the correct output. + 5. Goto 1. + +### Attach to a container (websocket) + +`GET /containers/(id or name)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **detachKeys** – Override the key sequence for detaching a + container. Format is a single character `[a-Z]` or `ctrl-` + where `` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`. +- **logs** – 1/True/true or 0/False/false, return logs. Default `false`. +- **stream** – 1/True/true or 0/False/false, return stream. + Default `false`. +- **stdin** – 1/True/true or 0/False/false, if `stream=true`, attach + to `stdin`. Default `false`. +- **stdout** – 1/True/true or 0/False/false, if `logs=true`, return + `stdout` log, if `stream=true`, attach to `stdout`. Default `false`. +- **stderr** – 1/True/true or 0/False/false, if `logs=true`, return + `stderr` log, if `stream=true`, attach to `stderr`. Default `false`. + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + +### Wait a container + +`POST /containers/(id or name)/wait` + +Block until container `id` stops, then returns the exit code + +**Example request**: + + POST /containers/16253994b7c4/wait HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"StatusCode": 0} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Remove a container + +`DELETE /containers/(id or name)` + +Remove the container `id` from the filesystem + +**Example request**: + + DELETE /containers/16253994b7c4?v=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **v** – 1/True/true or 0/False/false, Remove the volumes + associated to the container. Default `false`. +- **force** - 1/True/true or 0/False/false, Kill then remove the container. + Default `false`. + +Status Codes: + +- **204** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + +### Copy files or folders from a container + +`POST /containers/(id or name)/copy` + +Copy files or folders of container `id` + +**Deprecated** in favor of the `archive` endpoint below. + +**Example request**: + + POST /containers/4fa6e0f0c678/copy HTTP/1.1 + Content-Type: application/json + + { + "Resource": "test.txt" + } + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + {{ TAR STREAM }} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Retrieving information about files and folders in a container + +`HEAD /containers/(id or name)/archive` + +See the description of the `X-Docker-Container-Path-Stat` header in the +following section. + +### Get an archive of a filesystem resource in a container + +`GET /containers/(id or name)/archive` + +Get an tar archive of a resource in the filesystem of container `id`. + +Query Parameters: + +- **path** - resource in the container's filesystem to archive. Required. + + If not an absolute path, it is relative to the container's root directory. + The resource specified by **path** must exist. To assert that the resource + is expected to be a directory, **path** should end in `/` or `/.` + (assuming a path separator of `/`). If **path** ends in `/.` then this + indicates that only the contents of the **path** directory should be + copied. A symlink is always resolved to its target. + + **Note**: It is not possible to copy certain system files such as resources + under `/proc`, `/sys`, `/dev`, and mounts created by the user in the + container. + +**Example request**: + + GET /containers/8cce319429b2/archive?path=/root HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + X-Docker-Container-Path-Stat: eyJuYW1lIjoicm9vdCIsInNpemUiOjQwOTYsIm1vZGUiOjIxNDc0ODQwOTYsIm10aW1lIjoiMjAxNC0wMi0yN1QyMDo1MToyM1oiLCJsaW5rVGFyZ2V0IjoiIn0= + + {{ TAR STREAM }} + +On success, a response header `X-Docker-Container-Path-Stat` will be set to a +base64-encoded JSON object containing some filesystem header information about +the archived resource. The above example value would decode to the following +JSON object (whitespace added for readability): + + { + "name": "root", + "size": 4096, + "mode": 2147484096, + "mtime": "2014-02-27T20:51:23Z", + "linkTarget": "" + } + +A `HEAD` request can also be made to this endpoint if only this information is +desired. + +Status Codes: + +- **200** - success, returns archive of copied resource +- **400** - client error, bad parameter, details in JSON response body, one of: + - must specify path parameter (**path** cannot be empty) + - not a directory (**path** was asserted to be a directory but exists as a + file) +- **404** - client error, resource not found, one of: + – no such container (container `id` does not exist) + - no such file or directory (**path** does not exist) +- **500** - server error + +### Extract an archive of files or folders to a directory in a container + +`PUT /containers/(id or name)/archive` + +Upload a tar archive to be extracted to a path in the filesystem of container +`id`. + +Query Parameters: + +- **path** - path to a directory in the container + to extract the archive's contents into. Required. + + If not an absolute path, it is relative to the container's root directory. + The **path** resource must exist. +- **noOverwriteDirNonDir** - If "1", "true", or "True" then it will be an error + if unpacking the given content would cause an existing directory to be + replaced with a non-directory and vice versa. + +**Example request**: + + PUT /containers/8cce319429b2/archive?path=/vol1 HTTP/1.1 + Content-Type: application/x-tar + + {{ TAR STREAM }} + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – the content was extracted successfully +- **400** - client error, bad parameter, details in JSON response body, one of: + - must specify path parameter (**path** cannot be empty) + - not a directory (**path** should be a directory but exists as a file) + - unable to overwrite existing directory with non-directory + (if **noOverwriteDirNonDir**) + - unable to overwrite existing non-directory with directory + (if **noOverwriteDirNonDir**) +- **403** - client error, permission denied, the volume + or container rootfs is marked as read-only. +- **404** - client error, resource not found, one of: + – no such container (container `id` does not exist) + - no such file or directory (**path** resource does not exist) +- **500** – server error + +## 2.2 Images + +### List Images + +`GET /images/json` + +**Example request**: + + GET /images/json?all=0 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "RepoTags": [ + "ubuntu:12.04", + "ubuntu:precise", + "ubuntu:latest" + ], + "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", + "Created": 1365714795, + "Size": 131506275, + "VirtualSize": 131506275, + "Labels": {} + }, + { + "RepoTags": [ + "ubuntu:12.10", + "ubuntu:quantal" + ], + "ParentId": "27cf784147099545", + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Created": 1364102658, + "Size": 24653, + "VirtualSize": 180116135, + "Labels": { + "com.example.version": "v1" + } + } + ] + +**Example request, with digest information**: + + GET /images/json?digests=1 HTTP/1.1 + +**Example response, with digest information**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Created": 1420064636, + "Id": "4986bf8c15363d1c5d15512d5266f8777bfba4974ac56e3270e7760f6f0a8125", + "ParentId": "ea13149945cb6b1e746bf28032f02e9b5a793523481a0a18645fc77ad53c4ea2", + "RepoDigests": [ + "localhost:5000/test/busybox@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf" + ], + "RepoTags": [ + "localhost:5000/test/busybox:latest", + "playdate:latest" + ], + "Size": 0, + "VirtualSize": 2429728, + "Labels": {} + } + ] + +The response shows a single image `Id` associated with two repositories +(`RepoTags`): `localhost:5000/test/busybox`: and `playdate`. A caller can use +either of the `RepoTags` values `localhost:5000/test/busybox:latest` or +`playdate:latest` to reference the image. + +You can also use `RepoDigests` values to reference an image. In this response, +the array has only one reference and that is to the +`localhost:5000/test/busybox` repository; the `playdate` repository has no +digest. You can reference this digest using the value: +`localhost:5000/test/busybox@sha256:cbbf2f9a99b47fc460d...` + +See the `docker run` and `docker build` commands for examples of digest and tag +references on the command line. + +Query Parameters: + +- **all** – 1/True/true or 0/False/false, default false +- **filters** – a JSON encoded value of the filters (a map[string][]string) to process on the images list. Available filters: + - `dangling=true` + - `label=key` or `label="key=value"` of an image label +- **filter** - only return images with the specified name + +### Build image from a Dockerfile + +`POST /build` + +Build an image from a Dockerfile + +**Example request**: + + POST /build HTTP/1.1 + + {{ TAR STREAM }} + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"stream": "Step 1..."} + {"stream": "..."} + {"error": "Error...", "errorDetail": {"code": 123, "message": "Error..."}} + +The input stream must be a `tar` archive compressed with one of the +following algorithms: `identity` (no compression), `gzip`, `bzip2`, `xz`. + +The archive must include a build instructions file, typically called +`Dockerfile` at the archive's root. The `dockerfile` parameter may be +used to specify a different build instructions file. To do this, its value must be +the path to the alternate build instructions file to use. + +The archive may include any number of other files, +which are accessible in the build context (See the [*ADD build +command*](../../reference/builder.md#dockerbuilder)). + +The build is canceled if the client drops the connection by quitting +or being killed. + +Query Parameters: + +- **dockerfile** - Path within the build context to the Dockerfile. This is + ignored if `remote` is specified and points to an individual filename. +- **t** – A name and optional tag to apply to the image in the `name:tag` format. + If you omit the `tag` the default `latest` value is assumed. + You can provide one or more `t` parameters. +- **remote** – A Git repository URI or HTTP/HTTPS URI build source. If the + URI specifies a filename, the file's contents are placed into a file + called `Dockerfile`. +- **q** – Suppress verbose build output. +- **nocache** – Do not use the cache when building the image. +- **pull** - Attempt to pull the image even if an older image exists locally. +- **rm** - Remove intermediate containers after a successful build (default behavior). +- **forcerm** - Always remove intermediate containers (includes `rm`). +- **memory** - Set memory limit for build. +- **memswap** - Total memory (memory + swap), `-1` to enable unlimited swap. +- **cpushares** - CPU shares (relative weight). +- **cpusetcpus** - CPUs in which to allow execution (e.g., `0-3`, `0,1`). +- **cpuperiod** - The length of a CPU period in microseconds. +- **cpuquota** - Microseconds of CPU time that the container can get in a CPU period. +- **buildargs** – JSON map of string pairs for build-time variables. Users pass + these values at build-time. Docker uses the `buildargs` as the environment + context for command(s) run via the Dockerfile's `RUN` instruction or for + variable expansion in other Dockerfile instructions. This is not meant for + passing secret values. [Read more about the buildargs instruction](../../reference/builder.md#arg) +- **shmsize** - Size of `/dev/shm` in bytes. The size must be greater than 0. If omitted the system uses 64MB. + + Request Headers: + +- **Content-type** – Set to `"application/tar"`. +- **X-Registry-Config** – A base64-url-safe-encoded Registry Auth Config JSON + object with the following structure: + + { + "docker.example.com": { + "username": "janedoe", + "password": "hunter2" + }, + "https://index.docker.io/v1/": { + "username": "mobydock", + "password": "conta1n3rize14" + } + } + + This object maps the hostname of a registry to an object containing the + "username" and "password" for that registry. Multiple registries may + be specified as the build may be based on an image requiring + authentication to pull from any arbitrary registry. Only the registry + domain name (and port if not the default "443") are required. However + (for legacy reasons) the "official" Docker, Inc. hosted registry must + be specified with both a "https://" prefix and a "/v1/" suffix even + though Docker will prefer to use the v2 registry API. + +Status Codes: + +- **200** – no error +- **500** – server error + +### Create an image + +`POST /images/create` + +Create an image either by pulling it from the registry or by importing it + +**Example request**: + + POST /images/create?fromImage=ubuntu HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "Pulling..."} + {"status": "Pulling", "progress": "1 B/ 100 B", "progressDetail": {"current": 1, "total": 100}} + {"error": "Invalid..."} + ... + +When using this endpoint to pull an image from the registry, the +`X-Registry-Auth` header can be used to include +a base64-encoded AuthConfig object. + +Query Parameters: + +- **fromImage** – Name of the image to pull. The name may include a tag or + digest. This parameter may only be used when pulling an image. + The pull is cancelled if the HTTP connection is closed. +- **fromSrc** – Source to import. The value may be a URL from which the image + can be retrieved or `-` to read the image from the request body. + This parameter may only be used when importing an image. +- **repo** – Repository name given to an image when it is imported. + The repo may include a tag. This parameter may only be used when importing + an image. +- **tag** – Tag or digest. + + Request Headers: + +- **X-Registry-Auth** – base64-encoded AuthConfig object, containing either login information, or a token + - Credential based login: + + ``` + { + "username": "jdoe", + "password": "secret", + "email": "jdoe@acme.com", + } + ``` + + - Token based login: + + ``` + { + "registrytoken": "9cbaf023786cd7..." + } + ``` + +Status Codes: + +- **200** – no error +- **500** – server error + + + +### Inspect an image + +`GET /images/(name)/json` + +Return low-level information on the image `name` + +**Example request**: + + GET /images/example/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Id" : "85f05633ddc1c50679be2b16a0479ab6f7637f8884e0cfe0f4d20e1ebb3d6e7c", + "Container" : "cb91e48a60d01f1e27028b4fc6819f4f290b3cf12496c8176ec714d0d390984a", + "Comment" : "", + "Os" : "linux", + "Architecture" : "amd64", + "Parent" : "91e54dfb11794fad694460162bf0cb0a4fa710cfa3f60979c177d920813e267c", + "ContainerConfig" : { + "Tty" : false, + "Hostname" : "e611e15f9c9d", + "Volumes" : null, + "Domainname" : "", + "AttachStdout" : false, + "PublishService" : "", + "AttachStdin" : false, + "OpenStdin" : false, + "StdinOnce" : false, + "NetworkDisabled" : false, + "OnBuild" : [], + "Image" : "91e54dfb11794fad694460162bf0cb0a4fa710cfa3f60979c177d920813e267c", + "User" : "", + "WorkingDir" : "", + "Entrypoint" : null, + "MacAddress" : "", + "AttachStderr" : false, + "Labels" : { + "com.example.license" : "GPL", + "com.example.version" : "1.0", + "com.example.vendor" : "Acme" + }, + "Env" : [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "ExposedPorts" : null, + "Cmd" : [ + "/bin/sh", + "-c", + "#(nop) LABEL com.example.vendor=Acme com.example.license=GPL com.example.version=1.0" + ] + }, + "DockerVersion" : "1.9.0-dev", + "VirtualSize" : 188359297, + "Size" : 0, + "Author" : "", + "Created" : "2015-09-10T08:30:53.26995814Z", + "GraphDriver" : { + "Name" : "aufs", + "Data" : null + }, + "RepoDigests" : [ + "localhost:5000/test/busybox/example@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf" + ], + "RepoTags" : [ + "example:1.0", + "example:latest", + "example:stable" + ], + "Config" : { + "Image" : "91e54dfb11794fad694460162bf0cb0a4fa710cfa3f60979c177d920813e267c", + "NetworkDisabled" : false, + "OnBuild" : [], + "StdinOnce" : false, + "PublishService" : "", + "AttachStdin" : false, + "OpenStdin" : false, + "Domainname" : "", + "AttachStdout" : false, + "Tty" : false, + "Hostname" : "e611e15f9c9d", + "Volumes" : null, + "Cmd" : [ + "/bin/bash" + ], + "ExposedPorts" : null, + "Env" : [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Labels" : { + "com.example.vendor" : "Acme", + "com.example.version" : "1.0", + "com.example.license" : "GPL" + }, + "Entrypoint" : null, + "MacAddress" : "", + "AttachStderr" : false, + "WorkingDir" : "", + "User" : "" + } + } + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Get the history of an image + +`GET /images/(name)/history` + +Return the history of the image `name` + +**Example request**: + + GET /images/ubuntu/history HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "3db9c44f45209632d6050b35958829c3a2aa256d81b9a7be45b362ff85c54710", + "Created": 1398108230, + "CreatedBy": "/bin/sh -c #(nop) ADD file:eb15dbd63394e063b805a3c32ca7bf0266ef64676d5a6fab4801f2e81e2a5148 in /", + "Tags": [ + "ubuntu:lucid", + "ubuntu:10.04" + ], + "Size": 182964289, + "Comment": "" + }, + { + "Id": "6cfa4d1f33fb861d4d114f43b25abd0ac737509268065cdfd69d544a59c85ab8", + "Created": 1398108222, + "CreatedBy": "/bin/sh -c #(nop) MAINTAINER Tianon Gravi - mkimage-debootstrap.sh -i iproute,iputils-ping,ubuntu-minimal -t lucid.tar.xz lucid http://archive.ubuntu.com/ubuntu/", + "Tags": null, + "Size": 0, + "Comment": "" + }, + { + "Id": "511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158", + "Created": 1371157430, + "CreatedBy": "", + "Tags": [ + "scratch12:latest", + "scratch:latest" + ], + "Size": 0, + "Comment": "Imported from -" + } + ] + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Push an image on the registry + +`POST /images/(name)/push` + +Push the image `name` on the registry + +**Example request**: + + POST /images/test/push HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "Pushing..."} + {"status": "Pushing", "progress": "1/? (n/a)", "progressDetail": {"current": 1}}} + {"error": "Invalid..."} + ... + +If you wish to push an image on to a private registry, that image must already have a tag +into a repository which references that registry `hostname` and `port`. This repository name should +then be used in the URL. This duplicates the command line's flow. + +The push is cancelled if the HTTP connection is closed. + +**Example request**: + + POST /images/registry.acme.com:5000/test/push HTTP/1.1 + + +Query Parameters: + +- **tag** – The tag to associate with the image on the registry. This is optional. + +Request Headers: + +- **X-Registry-Auth** – base64-encoded AuthConfig object, containing either login information, or a token + - Credential based login: + + ``` + { + "username": "jdoe", + "password": "secret", + "email": "jdoe@acme.com", + } + ``` + + - Token based login: + + ``` + { + "registrytoken": "9cbaf023786cd7..." + } + ``` + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Tag an image into a repository + +`POST /images/(name)/tag` + +Tag the image `name` into a repository + +**Example request**: + + POST /images/test/tag?repo=myrepo&force=0&tag=v42 HTTP/1.1 + +**Example response**: + + HTTP/1.1 201 Created + +Query Parameters: + +- **repo** – The repository to tag in +- **force** – 1/True/true or 0/False/false, default false +- **tag** - The new tag name + +Status Codes: + +- **201** – no error +- **400** – bad parameter +- **404** – no such image +- **409** – conflict +- **500** – server error + +### Remove an image + +`DELETE /images/(name)` + +Remove the image `name` from the filesystem + +**Example request**: + + DELETE /images/test HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-type: application/json + + [ + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} + ] + +Query Parameters: + +- **force** – 1/True/true or 0/False/false, default false +- **noprune** – 1/True/true or 0/False/false, default false + +Status Codes: + +- **200** – no error +- **404** – no such image +- **409** – conflict +- **500** – server error + +### Search images + +`GET /images/search` + +Search for an image on [Docker Hub](https://hub.docker.com). + +> **Note**: +> The response keys have changed from API v1.6 to reflect the JSON +> sent by the registry server to the docker daemon's request. + +**Example request**: + + GET /images/search?term=sshd HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "wma55/u1210sshd", + "star_count": 0 + }, + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "jdswinbank/sshd", + "star_count": 0 + }, + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "vgauthier/sshd", + "star_count": 0 + } + ... + ] + +Query Parameters: + +- **term** – term to search + +Status Codes: + +- **200** – no error +- **500** – server error + +## 2.3 Misc + +### Check auth configuration + +`POST /auth` + +Get the default username and email + +**Example request**: + + POST /auth HTTP/1.1 + Content-Type: application/json + + { + "username":" hannibal", + "password: "xxxx", + "email": "hannibal@a-team.com", + "serveraddress": "https://index.docker.io/v1/" + } + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – no error +- **204** – no error +- **500** – server error + +### Display system-wide information + +`GET /info` + +Display system-wide information + +**Example request**: + + GET /info HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Architecture": "x86_64", + "ClusterStore": "etcd://localhost:2379", + "Containers": 11, + "ContainersRunning": 7, + "ContainersStopped": 3, + "ContainersPaused": 1, + "CpuCfsPeriod": true, + "CpuCfsQuota": true, + "Debug": false, + "DockerRootDir": "/var/lib/docker", + "Driver": "btrfs", + "DriverStatus": [[""]], + "ExecutionDriver": "native-0.1", + "ExperimentalBuild": false, + "HttpProxy": "http://test:test@localhost:8080", + "HttpsProxy": "https://test:test@localhost:8080", + "ID": "7TRN:IPZB:QYBB:VPBQ:UMPP:KARE:6ZNR:XE6T:7EWV:PKF4:ZOJD:TPYS", + "IPv4Forwarding": true, + "Images": 16, + "IndexServerAddress": "https://index.docker.io/v1/", + "InitPath": "/usr/bin/docker", + "InitSha1": "", + "KernelVersion": "3.12.0-1-amd64", + "Labels": [ + "storage=ssd" + ], + "MemTotal": 2099236864, + "MemoryLimit": true, + "NCPU": 1, + "NEventsListener": 0, + "NFd": 11, + "NGoroutines": 21, + "Name": "prod-server-42", + "NoProxy": "9.81.1.160", + "OomKillDisable": true, + "OSType": "linux", + "OperatingSystem": "Boot2Docker", + "Plugins": { + "Volume": [ + "local" + ], + "Network": [ + "null", + "host", + "bridge" + ] + }, + "RegistryConfig": { + "IndexConfigs": { + "docker.io": { + "Mirrors": null, + "Name": "docker.io", + "Official": true, + "Secure": true + } + }, + "InsecureRegistryCIDRs": [ + "127.0.0.0/8" + ] + }, + "ServerVersion": "1.9.0", + "SwapLimit": false, + "SystemStatus": [["State", "Healthy"]], + "SystemTime": "2015-03-10T11:11:23.730591467-07:00" + } + +Status Codes: + +- **200** – no error +- **500** – server error + +### Show the docker version information + +`GET /version` + +Show the docker version information + +**Example request**: + + GET /version HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Version": "1.10.0-dev", + "Os": "linux", + "KernelVersion": "3.19.0-23-generic", + "GoVersion": "go1.4.2", + "GitCommit": "e75da4b", + "Arch": "amd64", + "ApiVersion": "1.22", + "BuildTime": "2015-12-01T07:09:13.444803460+00:00", + "Experimental": true + } + +Status Codes: + +- **200** – no error +- **500** – server error + +### Ping the docker server + +`GET /_ping` + +Ping the docker server + +**Example request**: + + GET /_ping HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: text/plain + + OK + +Status Codes: + +- **200** - no error +- **500** - server error + +### Create a new image from a container's changes + +`POST /commit` + +Create a new image from a container's changes + +**Example request**: + + POST /commit?container=44c004db4b17&comment=message&repo=myrepo HTTP/1.1 + Content-Type: application/json + + { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "date" + ], + "Mounts": [ + { + "Source": "/data", + "Destination": "/data", + "Mode": "ro,Z", + "RW": false + } + ], + "Labels": { + "key1": "value1", + "key2": "value2" + }, + "WorkingDir": "", + "NetworkDisabled": false, + "ExposedPorts": { + "22/tcp": {} + } + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + {"Id": "596069db4bf5"} + +Json Parameters: + +- **config** - the container's configuration + +Query Parameters: + +- **container** – source container +- **repo** – repository +- **tag** – tag +- **comment** – commit message +- **author** – author (e.g., "John Hannibal Smith + <[hannibal@a-team.com](mailto:hannibal%40a-team.com)>") +- **pause** – 1/True/true or 0/False/false, whether to pause the container before committing +- **changes** – Dockerfile instructions to apply while committing + +Status Codes: + +- **201** – no error +- **404** – no such container +- **500** – server error + +### Monitor Docker's events + +`GET /events` + +Get container events from docker, either in real time via streaming, or via polling (using since). + +Docker containers report the following events: + + attach, commit, copy, create, destroy, die, exec_create, exec_start, export, kill, oom, pause, rename, resize, restart, start, stop, top, unpause, update + +Docker images report the following events: + + delete, import, pull, push, tag, untag + +Docker volumes report the following events: + + create, mount, unmount, destroy + +Docker networks report the following events: + + create, connect, disconnect, destroy + +**Example request**: + + GET /events?since=1374067924 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "action": "pull", + "type": "image", + "actor": { + "id": "busybox:latest", + "attributes": {} + } + "time": 1442421700, + "timeNano": 1442421700598988358 + }, + { + "action": "create", + "type": "container", + "actor": { + "id": "5745704abe9caa5", + "attributes": {"image": "busybox"} + } + "time": 1442421716, + "timeNano": 1442421716853979870 + }, + { + "action": "attach", + "type": "container", + "actor": { + "id": "5745704abe9caa5", + "attributes": {"image": "busybox"} + } + "time": 1442421716, + "timeNano": 1442421716894759198 + }, + { + "action": "start", + "type": "container", + "actor": { + "id": "5745704abe9caa5", + "attributes": {"image": "busybox"} + } + "time": 1442421716, + "timeNano": 1442421716983607193 + } + ] + +Query Parameters: + +- **since** – Timestamp used for polling +- **until** – Timestamp used for polling +- **filters** – A json encoded value of the filters (a map[string][]string) to process on the event list. Available filters: + - `container=`; -- container to filter + - `event=`; -- event to filter + - `image=`; -- image to filter + - `label=`; -- image and container label to filter + - `type=`; -- either `container` or `image` or `volume` or `network` + - `volume=`; -- volume to filter + - `network=`; -- network to filter + +Status Codes: + +- **200** – no error +- **500** – server error + +### Get a tarball containing all images in a repository + +`GET /images/(name)/get` + +Get a tarball containing all images and metadata for the repository specified +by `name`. + +If `name` is a specific name and tag (e.g. ubuntu:latest), then only that image +(and its parents) are returned. If `name` is an image ID, similarly only that +image (and its parents) are returned, but with the exclusion of the +'repositories' file in the tarball, as there were no image names referenced. + +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + GET /images/ubuntu/get + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + Binary data stream + +Status Codes: + +- **200** – no error +- **500** – server error + +### Get a tarball containing all images. + +`GET /images/get` + +Get a tarball containing all images and metadata for one or more repositories. + +For each value of the `names` parameter: if it is a specific name and tag (e.g. +`ubuntu:latest`), then only that image (and its parents) are returned; if it is +an image ID, similarly only that image (and its parents) are returned and there +would be no names referenced in the 'repositories' file for this image ID. + +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + GET /images/get?names=myname%2Fmyapp%3Alatest&names=busybox + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + Binary data stream + +Status Codes: + +- **200** – no error +- **500** – server error + +### Load a tarball with a set of images and tags into docker + +`POST /images/load` + +Load a set of images and tags into a Docker repository. +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + POST /images/load + + Tarball in body + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – no error +- **500** – server error + +### Image tarball format + +An image tarball contains one directory per image layer (named using its long ID), +each containing these files: + +- `VERSION`: currently `1.0` - the file format version +- `json`: detailed layer information, similar to `docker inspect layer_id` +- `layer.tar`: A tarfile containing the filesystem changes in this layer + +The `layer.tar` file contains `aufs` style `.wh..wh.aufs` files and directories +for storing attribute changes and deletions. + +If the tarball defines a repository, the tarball should also include a `repositories` file at +the root that contains a list of repository and tag names mapped to layer IDs. + +``` +{"hello-world": + {"latest": "565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} +} +``` + +### Exec Create + +`POST /containers/(id or name)/exec` + +Sets up an exec instance in a running container `id` + +**Example request**: + + POST /containers/e90e34656806/exec HTTP/1.1 + Content-Type: application/json + + { + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "DetachKeys": "ctrl-p,ctrl-q", + "Tty": false, + "Cmd": [ + "date" + ] + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "Id": "f90e34656806", + "Warnings":[] + } + +Json Parameters: + +- **AttachStdin** - Boolean value, attaches to `stdin` of the `exec` command. +- **AttachStdout** - Boolean value, attaches to `stdout` of the `exec` command. +- **AttachStderr** - Boolean value, attaches to `stderr` of the `exec` command. +- **DetachKeys** – Override the key sequence for detaching a + container. Format is a single character `[a-Z]` or `ctrl-` + where `` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`. +- **Tty** - Boolean value to allocate a pseudo-TTY. +- **Cmd** - Command to run specified as a string or an array of strings. + + +Status Codes: + +- **201** – no error +- **404** – no such container +- **409** - container is paused +- **500** - server error + +### Exec Start + +`POST /exec/(id)/start` + +Starts a previously set up `exec` instance `id`. If `detach` is true, this API +returns after starting the `exec` command. Otherwise, this API sets up an +interactive session with the `exec` command. + +**Example request**: + + POST /exec/e90e34656806/start HTTP/1.1 + Content-Type: application/json + + { + "Detach": false, + "Tty": false + } + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + +Json Parameters: + +- **Detach** - Detach from the `exec` command. +- **Tty** - Boolean value to allocate a pseudo-TTY. + +Status Codes: + +- **200** – no error +- **404** – no such exec instance +- **409** - container is paused + + **Stream details**: + Similar to the stream behavior of `POST /containers/(id or name)/attach` API + +### Exec Resize + +`POST /exec/(id)/resize` + +Resizes the `tty` session used by the `exec` command `id`. The unit is number of characters. +This API is valid only if `tty` was specified as part of creating and starting the `exec` command. + +**Example request**: + + POST /exec/e90e34656806/resize?h=40&w=80 HTTP/1.1 + Content-Type: text/plain + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: text/plain + +Query Parameters: + +- **h** – height of `tty` session +- **w** – width + +Status Codes: + +- **201** – no error +- **404** – no such exec instance + +### Exec Inspect + +`GET /exec/(id)/json` + +Return low-level information about the `exec` command `id`. + +**Example request**: + + GET /exec/11fb006128e8ceb3942e7c58d77750f24210e35f879dd204ac975c184b820b39/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "CanRemove": false, + "ContainerID": "b53ee82b53a40c7dca428523e34f741f3abc51d9f297a14ff874bf761b995126", + "DetachKeys": "", + "ExitCode": 2, + "ID": "f33bbfb39f5b142420f4759b2348913bd4a8d1a6d7fd56499cb41a1bb91d7b3b", + "OpenStderr": true, + "OpenStdin": true, + "OpenStdout": true, + "ProcessConfig": { + "arguments": [ + "-c", + "exit 2" + ], + "entrypoint": "sh", + "privileged": false, + "tty": true, + "user": "1000" + }, + "Running": false + } + +Status Codes: + +- **200** – no error +- **404** – no such exec instance +- **500** - server error + +## 2.4 Volumes + +### List volumes + +`GET /volumes` + +**Example request**: + + GET /volumes HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Volumes": [ + { + "Name": "tardis", + "Driver": "local", + "Mountpoint": "/var/lib/docker/volumes/tardis" + } + ], + "Warnings": [] + } + +Query Parameters: + +- **filters** - JSON encoded value of the filters (a `map[string][]string`) to process on the volumes list. There is one available filter: `dangling=true` + +Status Codes: + +- **200** - no error +- **500** - server error + +### Create a volume + +`POST /volumes/create` + +Create a volume + +**Example request**: + + POST /volumes/create HTTP/1.1 + Content-Type: application/json + + { + "Name": "tardis" + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "Name": "tardis", + "Driver": "local", + "Mountpoint": "/var/lib/docker/volumes/tardis" + } + +Status Codes: + +- **201** - no error +- **500** - server error + +JSON Parameters: + +- **Name** - The new volume's name. If not specified, Docker generates a name. +- **Driver** - Name of the volume driver to use. Defaults to `local` for the name. +- **DriverOpts** - A mapping of driver options and values. These options are + passed directly to the driver and are driver specific. + +### Inspect a volume + +`GET /volumes/(name)` + +Return low-level information on the volume `name` + +**Example request**: + + GET /volumes/tardis + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Name": "tardis", + "Driver": "local", + "Mountpoint": "/var/lib/docker/volumes/tardis" + } + +Status Codes: + +- **200** - no error +- **404** - no such volume +- **500** - server error + +### Remove a volume + +`DELETE /volumes/(name)` + +Instruct the driver to remove the volume (`name`). + +**Example request**: + + DELETE /volumes/tardis HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes + +- **204** - no error +- **404** - no such volume or volume driver +- **409** - volume is in use and cannot be removed +- **500** - server error + +## 2.5 Networks + +### List networks + +`GET /networks` + +**Example request**: + + GET /networks?filters={"type":{"custom":true}} HTTP/1.1 + +**Example response**: + +``` +HTTP/1.1 200 OK +Content-Type: application/json + +[ + { + "Name": "bridge", + "Id": "f2de39df4171b0dc801e8002d1d999b77256983dfc63041c0f34030aa3977566", + "Scope": "local", + "Driver": "bridge", + "IPAM": { + "Driver": "default", + "Config": [ + { + "Subnet": "172.17.0.0/16" + } + ] + }, + "Containers": { + "39b69226f9d79f5634485fb236a23b2fe4e96a0a94128390a7fbbcc167065867": { + "EndpointID": "ed2419a97c1d9954d05b46e462e7002ea552f216e9b136b80a7db8d98b442eda", + "MacAddress": "02:42:ac:11:00:02", + "IPv4Address": "172.17.0.2/16", + "IPv6Address": "" + } + }, + "Options": { + "com.docker.network.bridge.default_bridge": "true", + "com.docker.network.bridge.enable_icc": "true", + "com.docker.network.bridge.enable_ip_masquerade": "true", + "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", + "com.docker.network.bridge.name": "docker0", + "com.docker.network.driver.mtu": "1500" + } + }, + { + "Name": "none", + "Id": "e086a3893b05ab69242d3c44e49483a3bbbd3a26b46baa8f61ab797c1088d794", + "Scope": "local", + "Driver": "null", + "IPAM": { + "Driver": "default", + "Config": [] + }, + "Containers": {}, + "Options": {} + }, + { + "Name": "host", + "Id": "13e871235c677f196c4e1ecebb9dc733b9b2d2ab589e30c539efeda84a24215e", + "Scope": "local", + "Driver": "host", + "IPAM": { + "Driver": "default", + "Config": [] + }, + "Containers": {}, + "Options": {} + } +] +``` + +Query Parameters: + +- **filters** - JSON encoded network list filter. The filter value is one of: + - `name=` Matches all or part of a network name. + - `id=` Matches all or part of a network id. + - `type=["custom"|"builtin"]` Filters networks by type. The `custom` keyword returns all user-defined networks. + +Status Codes: + +- **200** - no error +- **500** - server error + +### Inspect network + +`GET /networks/` + +**Example request**: + + GET /networks/7d86d31b1478e7cca9ebed7e73aa0fdeec46c5ca29497431d3007d2d9e15ed99 HTTP/1.1 + +**Example response**: + +``` +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "Name": "net01", + "Id": "7d86d31b1478e7cca9ebed7e73aa0fdeec46c5ca29497431d3007d2d9e15ed99", + "Scope": "local", + "Driver": "bridge", + "IPAM": { + "Driver": "default", + "Config": [ + { + "Subnet": "172.19.0.0/16", + "Gateway": "172.19.0.1/16" + } + ], + "Options": { + "foo": "bar" + } + }, + "Containers": { + "19a4d5d687db25203351ed79d478946f861258f018fe384f229f2efa4b23513c": { + "Name": "test", + "EndpointID": "628cadb8bcb92de107b2a1e516cbffe463e321f548feb37697cce00ad694f21a", + "MacAddress": "02:42:ac:13:00:02", + "IPv4Address": "172.19.0.2/16", + "IPv6Address": "" + } + }, + "Options": { + "com.docker.network.bridge.default_bridge": "true", + "com.docker.network.bridge.enable_icc": "true", + "com.docker.network.bridge.enable_ip_masquerade": "true", + "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", + "com.docker.network.bridge.name": "docker0", + "com.docker.network.driver.mtu": "1500" + } +} +``` + +Status Codes: + +- **200** - no error +- **404** - network not found + +### Create a network + +`POST /networks/create` + +Create a network + +**Example request**: + +``` +POST /networks/create HTTP/1.1 +Content-Type: application/json + +{ + "Name":"isolated_nw", + "Driver":"bridge", + "IPAM":{ + "Config":[ + { + "Subnet":"172.20.0.0/16", + "IPRange":"172.20.10.0/24", + "Gateway":"172.20.10.11" + }, + { + "Subnet":"2001:db8:abcd::/64", + "Gateway":"2001:db8:abcd::1011" + } + ], + "Options": { + "foo": "bar" + } + }, + "Internal":true +} +``` + +**Example response**: + +``` +HTTP/1.1 201 Created +Content-Type: application/json + +{ + "Id": "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30", + "Warning": "" +} +``` + +Status Codes: + +- **201** - no error +- **404** - plugin not found +- **500** - server error + +JSON Parameters: + +- **Name** - The new network's name. this is a mandatory field +- **Driver** - Name of the network driver plugin to use. Defaults to `bridge` driver +- **IPAM** - Optional custom IP scheme for the network +- **Options** - Network specific options to be used by the drivers +- **CheckDuplicate** - Requests daemon to check for networks with same name + +### Connect a container to a network + +`POST /networks/(id)/connect` + +Connect a container to a network + +**Example request**: + +``` +POST /networks/22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30/connect HTTP/1.1 +Content-Type: application/json + +{ + "Container":"3613f73ba0e4", + "EndpointConfig": { + "IPAMConfig": { + "IPv4Address":"172.24.56.89", + "IPv6Address":"2001:db8::5689" + } + } +} +``` + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** - no error +- **404** - network or container is not found +- **500** - Internal Server Error + +JSON Parameters: + +- **container** - container-id/name to be connected to the network + +### Disconnect a container from a network + +`POST /networks/(id)/disconnect` + +Disconnect a container from a network + +**Example request**: + +``` +POST /networks/22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30/disconnect HTTP/1.1 +Content-Type: application/json + +{ + "Container":"3613f73ba0e4", + "Force":false +} +``` + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** - no error +- **404** - network or container not found +- **500** - Internal Server Error + +JSON Parameters: + +- **Container** - container-id/name to be disconnected from a network +- **Force** - Force the container to disconnect from a network + +### Remove a network + +`DELETE /networks/(id)` + +Instruct the driver to remove the network (`id`). + +**Example request**: + + DELETE /networks/22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes + +- **200** - no error +- **404** - no such network +- **500** - server error + +# 3. Going further + +## 3.1 Inside `docker run` + +As an example, the `docker run` command line makes the following API calls: + +- Create the container + +- If the status code is 404, it means the image doesn't exist: + - Try to pull it. + - Then, retry to create the container. + +- Start the container. + +- If you are not in detached mode: +- Attach to the container, using `logs=1` (to have `stdout` and + `stderr` from the container's start) and `stream=1` + +- If in detached mode or only `stdin` is attached, display the container's id. + +## 3.2 Hijacking + +In this version of the API, `/attach`, uses hijacking to transport `stdin`, +`stdout`, and `stderr` on the same socket. + +To hint potential proxies about connection hijacking, Docker client sends +connection upgrade headers similarly to websocket. + + Upgrade: tcp + Connection: Upgrade + +When Docker daemon detects the `Upgrade` header, it switches its status code +from **200 OK** to **101 UPGRADED** and resends the same headers. + + +## 3.3 CORS Requests + +To set cross origin requests to the remote api please give values to +`--api-cors-header` when running Docker in daemon mode. Set * (asterisk) allows all, +default or blank means CORS disabled + + $ docker daemon -H="192.168.1.9:2375" --api-cors-header="http://foo.bar" diff --git a/docs/reference/api/docker_remote_api_v1.23.md b/docs/reference/api/docker_remote_api_v1.23.md new file mode 100644 index 00000000..30f55625 --- /dev/null +++ b/docs/reference/api/docker_remote_api_v1.23.md @@ -0,0 +1,3228 @@ + + +# Docker Remote API v1.23 + +## 1. Brief introduction + + - The Remote API has replaced `rcli`. + - The daemon listens on `unix:///var/run/docker.sock` but you can + [Bind Docker to another host/port or a Unix socket](../../quickstart.md#bind-docker-to-another-host-port-or-a-unix-socket). + - The API tends to be REST. However, for some complex commands, like `attach` + or `pull`, the HTTP connection is hijacked to transport `stdout`, + `stdin` and `stderr`. + - When the client API version is newer than the daemon's, these calls return an HTTP + `400 Bad Request` error message. + +# 2. Endpoints + +## 2.1 Containers + +### List containers + +`GET /containers/json` + +List containers + +**Example request**: + + GET /containers/json?all=1&before=8dfafdbc3a40&size=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "8dfafdbc3a40", + "Names":["/boring_feynman"], + "Image": "ubuntu:latest", + "ImageID": "d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82", + "Command": "echo 1", + "Created": 1367854155, + "State": "Exited", + "Status": "Exit 0", + "Ports": [{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "SizeRw": 12288, + "SizeRootFs": 0, + "HostConfig": { + "NetworkMode": "default" + }, + "NetworkSettings": { + "Networks": { + "bridge": { + "IPAMConfig": null, + "Links": null, + "Aliases": null, + "NetworkID": "7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812", + "EndpointID": "2cdc4edb1ded3631c81f57966563e5c8525b81121bb3706a9a9a3ae102711f3f", + "Gateway": "172.17.0.1", + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "02:42:ac:11:00:02" + } + } + }, + "Mounts": [ + { + "Name": "fac362...80535", + "Source": "/data", + "Destination": "/data", + "Driver": "local", + "Mode": "ro,Z", + "RW": false, + "Propagation": "" + } + ] + }, + { + "Id": "9cd87474be90", + "Names":["/coolName"], + "Image": "ubuntu:latest", + "ImageID": "d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82", + "Command": "echo 222222", + "Created": 1367854155, + "State": "Exited", + "Status": "Exit 0", + "Ports": [], + "Labels": {}, + "SizeRw": 12288, + "SizeRootFs": 0, + "HostConfig": { + "NetworkMode": "default" + }, + "NetworkSettings": { + "Networks": { + "bridge": { + "IPAMConfig": null, + "Links": null, + "Aliases": null, + "NetworkID": "7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812", + "EndpointID": "88eaed7b37b38c2a3f0c4bc796494fdf51b270c2d22656412a2ca5d559a64d7a", + "Gateway": "172.17.0.1", + "IPAddress": "172.17.0.8", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "02:42:ac:11:00:08" + } + } + }, + "Mounts": [] + }, + { + "Id": "3176a2479c92", + "Names":["/sleepy_dog"], + "Image": "ubuntu:latest", + "ImageID": "d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82", + "Command": "echo 3333333333333333", + "Created": 1367854154, + "State": "Exited", + "Status": "Exit 0", + "Ports":[], + "Labels": {}, + "SizeRw":12288, + "SizeRootFs":0, + "HostConfig": { + "NetworkMode": "default" + }, + "NetworkSettings": { + "Networks": { + "bridge": { + "IPAMConfig": null, + "Links": null, + "Aliases": null, + "NetworkID": "7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812", + "EndpointID": "8b27c041c30326d59cd6e6f510d4f8d1d570a228466f956edf7815508f78e30d", + "Gateway": "172.17.0.1", + "IPAddress": "172.17.0.6", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "02:42:ac:11:00:06" + } + } + }, + "Mounts": [] + }, + { + "Id": "4cb07b47f9fb", + "Names":["/running_cat"], + "Image": "ubuntu:latest", + "ImageID": "d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82", + "Command": "echo 444444444444444444444444444444444", + "Created": 1367854152, + "State": "Exited", + "Status": "Exit 0", + "Ports": [], + "Labels": {}, + "SizeRw": 12288, + "SizeRootFs": 0, + "HostConfig": { + "NetworkMode": "default" + }, + "NetworkSettings": { + "Networks": { + "bridge": { + "IPAMConfig": null, + "Links": null, + "Aliases": null, + "NetworkID": "7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812", + "EndpointID": "d91c7b2f0644403d7ef3095985ea0e2370325cd2332ff3a3225c4247328e66e9", + "Gateway": "172.17.0.1", + "IPAddress": "172.17.0.5", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "02:42:ac:11:00:05" + } + } + }, + "Mounts": [] + } + ] + +Query Parameters: + +- **all** – 1/True/true or 0/False/false, Show all containers. + Only running containers are shown by default (i.e., this defaults to false) +- **limit** – Show `limit` last created + containers, include non-running ones. +- **since** – Show only containers created since Id, include + non-running ones. +- **before** – Show only containers created before Id, include + non-running ones. +- **size** – 1/True/true or 0/False/false, Show the containers + sizes +- **filters** - a JSON encoded value of the filters (a `map[string][]string`) to process on the containers list. Available filters: + - `exited=`; -- containers with exit code of `` ; + - `status=`(`created`|`restarting`|`running`|`paused`|`exited`|`dead`) + - `label=key` or `label="key=value"` of a container label + - `isolation=`(`default`|`process`|`hyperv`) (Windows daemon only) + - `ancestor`=(`[:]`, `` or ``) + - `before`=(`` or ``) + - `since`=(`` or ``) + - `volume`=(`` or ``) + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **500** – server error + +### Create a container + +`POST /containers/create` + +Create a container + +**Example request**: + + POST /containers/create HTTP/1.1 + Content-Type: application/json + + { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "FOO=bar", + "BAZ=quux" + ], + "Cmd": [ + "date" + ], + "Entrypoint": "", + "Image": "ubuntu", + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "Volumes": { + "/volumes/data": {} + }, + "WorkingDir": "", + "NetworkDisabled": false, + "MacAddress": "12:34:56:78:9a:bc", + "ExposedPorts": { + "22/tcp": {} + }, + "StopSignal": "SIGTERM", + "HostConfig": { + "Binds": ["/tmp:/tmp"], + "Links": ["redis3:redis"], + "Memory": 0, + "MemorySwap": 0, + "MemoryReservation": 0, + "KernelMemory": 0, + "CpuShares": 512, + "CpuPeriod": 100000, + "CpuQuota": 50000, + "CpusetCpus": "0,1", + "CpusetMems": "0,1", + "BlkioWeight": 300, + "BlkioWeightDevice": [{}], + "BlkioDeviceReadBps": [{}], + "BlkioDeviceReadIOps": [{}], + "BlkioDeviceWriteBps": [{}], + "BlkioDeviceWriteIOps": [{}], + "MemorySwappiness": 60, + "OomKillDisable": false, + "OomScoreAdj": 500, + "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] }, + "PublishAllPorts": false, + "Privileged": false, + "ReadonlyRootfs": false, + "Dns": ["8.8.8.8"], + "DnsOptions": [""], + "DnsSearch": [""], + "ExtraHosts": null, + "VolumesFrom": ["parent", "other:ro"], + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"], + "GroupAdd": ["newgroup"], + "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 }, + "NetworkMode": "bridge", + "Devices": [], + "Ulimits": [{}], + "LogConfig": { "Type": "json-file", "Config": {} }, + "SecurityOpt": [], + "CgroupParent": "", + "VolumeDriver": "", + "ShmSize": 67108864 + }, + "NetworkingConfig": { + "EndpointsConfig": { + "isolated_nw" : { + "IPAMConfig": { + "IPv4Address":"172.20.30.33", + "IPv6Address":"2001:db8:abcd::3033" + }, + "Links":["container_1", "container_2"], + "Aliases":["server_x", "server_y"] + } + } + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "Id":"e90e34656806", + "Warnings":[] + } + +Json Parameters: + +- **Hostname** - A string value containing the hostname to use for the + container. +- **Domainname** - A string value containing the domain name to use + for the container. +- **User** - A string value specifying the user inside the container. +- **Memory** - Memory limit in bytes. +- **MemorySwap** - Total memory limit (memory + swap); set `-1` to enable unlimited swap. + You must use this with `memory` and make the swap value larger than `memory`. +- **MemoryReservation** - Memory soft limit in bytes. +- **KernelMemory** - Kernel memory limit in bytes. +- **CpuShares** - An integer value containing the container's CPU Shares + (ie. the relative weight vs other containers). +- **CpuPeriod** - The length of a CPU period in microseconds. +- **CpuQuota** - Microseconds of CPU time that the container can get in a CPU period. +- **Cpuset** - Deprecated please don't use. Use `CpusetCpus` instead. +- **CpusetCpus** - String value containing the `cgroups CpusetCpus` to use. +- **CpusetMems** - Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems. +- **BlkioWeight** - Block IO weight (relative weight) accepts a weight value between 10 and 1000. +- **BlkioWeightDevice** - Block IO weight (relative device weight) in the form of: `"BlkioWeightDevice": [{"Path": "device_path", "Weight": weight}]` +- **BlkioDeviceReadBps** - Limit read rate (bytes per second) from a device in the form of: `"BlkioDeviceReadBps": [{"Path": "device_path", "Rate": rate}]`, for example: + `"BlkioDeviceReadBps": [{"Path": "/dev/sda", "Rate": "1024"}]"` +- **BlkioDeviceWriteBps** - Limit write rate (bytes per second) to a device in the form of: `"BlkioDeviceWriteBps": [{"Path": "device_path", "Rate": rate}]`, for example: + `"BlkioDeviceWriteBps": [{"Path": "/dev/sda", "Rate": "1024"}]"` +- **BlkioDeviceReadIOps** - Limit read rate (IO per second) from a device in the form of: `"BlkioDeviceReadIOps": [{"Path": "device_path", "Rate": rate}]`, for example: + `"BlkioDeviceReadIOps": [{"Path": "/dev/sda", "Rate": "1000"}]` +- **BlkioDeviceWiiteIOps** - Limit write rate (IO per second) to a device in the form of: `"BlkioDeviceWriteIOps": [{"Path": "device_path", "Rate": rate}]`, for example: + `"BlkioDeviceWriteIOps": [{"Path": "/dev/sda", "Rate": "1000"}]` +- **MemorySwappiness** - Tune a container's memory swappiness behavior. Accepts an integer between 0 and 100. +- **OomKillDisable** - Boolean value, whether to disable OOM Killer for the container or not. +- **OomScoreAdj** - An integer value containing the score given to the container in order to tune OOM killer preferences. +- **PidsLimit** - Tune a container's pids limit. Set -1 for unlimited. +- **AttachStdin** - Boolean value, attaches to `stdin`. +- **AttachStdout** - Boolean value, attaches to `stdout`. +- **AttachStderr** - Boolean value, attaches to `stderr`. +- **Tty** - Boolean value, Attach standard streams to a `tty`, including `stdin` if it is not closed. +- **OpenStdin** - Boolean value, opens stdin, +- **StdinOnce** - Boolean value, close `stdin` after the 1 attached client disconnects. +- **Env** - A list of environment variables in the form of `["VAR=value"[,"VAR2=value2"]]` +- **Labels** - Adds a map of labels to a container. To specify a map: `{"key":"value"[,"key2":"value2"]}` +- **Cmd** - Command to run specified as a string or an array of strings. +- **Entrypoint** - Set the entry point for the container as a string or an array + of strings. +- **Image** - A string specifying the image name to use for the container. +- **Volumes** - An object mapping mount point paths (strings) inside the + container to empty objects. +- **WorkingDir** - A string specifying the working directory for commands to + run in. +- **NetworkDisabled** - Boolean value, when true disables networking for the + container +- **ExposedPorts** - An object mapping ports to an empty object in the form of: + `"ExposedPorts": { "/: {}" }` +- **StopSignal** - Signal to stop a container as a string or unsigned integer. `SIGTERM` by default. +- **HostConfig** + - **Binds** – A list of volume bindings for this container. Each volume binding is a string in one of these forms: + + `host_path:container_path` to bind-mount a host path into the container + + `host_path:container_path:ro` to make the bind-mount read-only inside the container. + + `volume_name:container_path` to bind-mount a volume managed by a volume plugin into the container. + + `volume_name:container_path:ro` to make the bind mount read-only inside the container. + - **Links** - A list of links for the container. Each link entry should be + in the form of `container_name:alias`. + - **PortBindings** - A map of exposed container ports and the host port they + should map to. A JSON object in the form + `{ /: [{ "HostPort": "" }] }` + Take note that `port` is specified as a string and not an integer value. + - **PublishAllPorts** - Allocates a random host port for all of a container's + exposed ports. Specified as a boolean value. + - **Privileged** - Gives the container full access to the host. Specified as + a boolean value. + - **ReadonlyRootfs** - Mount the container's root filesystem as read only. + Specified as a boolean value. + - **Dns** - A list of DNS servers for the container to use. + - **DnsOptions** - A list of DNS options + - **DnsSearch** - A list of DNS search domains + - **ExtraHosts** - A list of hostnames/IP mappings to add to the + container's `/etc/hosts` file. Specified in the form `["hostname:IP"]`. + - **VolumesFrom** - A list of volumes to inherit from another container. + Specified in the form `[:]` + - **CapAdd** - A list of kernel capabilities to add to the container. + - **Capdrop** - A list of kernel capabilities to drop from the container. + - **GroupAdd** - A list of additional groups that the container process will run as + - **RestartPolicy** – The behavior to apply when the container exits. The + value is an object with a `Name` property of either `"always"` to + always restart, `"unless-stopped"` to restart always except when + user has manually stopped the container or `"on-failure"` to restart only when the container + exit code is non-zero. If `on-failure` is used, `MaximumRetryCount` + controls the number of times to retry before giving up. + The default is not to restart. (optional) + An ever increasing delay (double the previous delay, starting at 100mS) + is added before each restart to prevent flooding the server. + - **UsernsMode** - Sets the usernamespace mode for the container when usernamespace remapping option is enabled. + supported values are: `host`. + - **NetworkMode** - Sets the networking mode for the container. Supported + standard values are: `bridge`, `host`, `none`, and `container:`. Any other value is taken + as a custom network's name to which this container should connect to. + - **Devices** - A list of devices to add to the container specified as a JSON object in the + form + `{ "PathOnHost": "/dev/deviceName", "PathInContainer": "/dev/deviceName", "CgroupPermissions": "mrw"}` + - **Ulimits** - A list of ulimits to set in the container, specified as + `{ "Name": , "Soft": , "Hard": }`, for example: + `Ulimits: { "Name": "nofile", "Soft": 1024, "Hard": 2048 }` + - **SecurityOpt**: A list of string values to customize labels for MLS + systems, such as SELinux. + - **LogConfig** - Log configuration for the container, specified as a JSON object in the form + `{ "Type": "", "Config": {"key1": "val1"}}`. + Available types: `json-file`, `syslog`, `journald`, `gelf`, `fluentd`, `awslogs`, `splunk`, `etwlogs`, `none`. + `json-file` logging driver. + - **CgroupParent** - Path to `cgroups` under which the container's `cgroup` is created. If the path is not absolute, the path is considered to be relative to the `cgroups` path of the init process. Cgroups are created if they do not already exist. + - **VolumeDriver** - Driver that this container users to mount volumes. + - **ShmSize** - Size of `/dev/shm` in bytes. The size must be greater than 0. If omitted the system uses 64MB. + +Query Parameters: + +- **name** – Assign the specified name to the container. Must + match `/?[a-zA-Z0-9_-]+`. + +Status Codes: + +- **201** – no error +- **404** – no such container +- **406** – impossible to attach (container not running) +- **500** – server error + +### Inspect a container + +`GET /containers/(id or name)/json` + +Return low-level information on the container `id` + + +**Example request**: + + GET /containers/4fa6e0f0c678/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "AppArmorProfile": "", + "Args": [ + "-c", + "exit 9" + ], + "Config": { + "AttachStderr": true, + "AttachStdin": false, + "AttachStdout": true, + "Cmd": [ + "/bin/sh", + "-c", + "exit 9" + ], + "Domainname": "", + "Entrypoint": null, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "ExposedPorts": null, + "Hostname": "ba033ac44011", + "Image": "ubuntu", + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "MacAddress": "", + "NetworkDisabled": false, + "OnBuild": null, + "OpenStdin": false, + "StdinOnce": false, + "Tty": false, + "User": "", + "Volumes": { + "/volumes/data": {} + }, + "WorkingDir": "", + "StopSignal": "SIGTERM" + }, + "Created": "2015-01-06T15:47:31.485331387Z", + "Driver": "devicemapper", + "ExecDriver": "native-0.2", + "ExecIDs": null, + "HostConfig": { + "Binds": null, + "BlkioWeight": 0, + "BlkioWeightDevice": [{}], + "BlkioDeviceReadBps": [{}], + "BlkioDeviceWriteBps": [{}], + "BlkioDeviceReadIOps": [{}], + "BlkioDeviceWriteIOps": [{}], + "CapAdd": null, + "CapDrop": null, + "ContainerIDFile": "", + "CpusetCpus": "", + "CpusetMems": "", + "CpuShares": 0, + "CpuPeriod": 100000, + "Devices": [], + "Dns": null, + "DnsOptions": null, + "DnsSearch": null, + "ExtraHosts": null, + "IpcMode": "", + "Links": null, + "LxcConf": [], + "Memory": 0, + "MemorySwap": 0, + "MemoryReservation": 0, + "KernelMemory": 0, + "OomKillDisable": false, + "OomScoreAdj": 500, + "NetworkMode": "bridge", + "PortBindings": {}, + "Privileged": false, + "ReadonlyRootfs": false, + "PublishAllPorts": false, + "RestartPolicy": { + "MaximumRetryCount": 2, + "Name": "on-failure" + }, + "LogConfig": { + "Config": null, + "Type": "json-file" + }, + "SecurityOpt": null, + "VolumesFrom": null, + "Ulimits": [{}], + "VolumeDriver": "", + "ShmSize": 67108864 + }, + "HostnamePath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/hostname", + "HostsPath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/hosts", + "LogPath": "/var/lib/docker/containers/1eb5fabf5a03807136561b3c00adcd2992b535d624d5e18b6cdc6a6844d9767b/1eb5fabf5a03807136561b3c00adcd2992b535d624d5e18b6cdc6a6844d9767b-json.log", + "Id": "ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39", + "Image": "04c5d3b7b0656168630d3ba35d8889bd0e9caafcaeb3004d2bfbc47e7c5d35d2", + "MountLabel": "", + "Name": "/boring_euclid", + "NetworkSettings": { + "Bridge": "", + "SandboxID": "", + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "Ports": null, + "SandboxKey": "", + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "", + "Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "", + "IPPrefixLen": 0, + "IPv6Gateway": "", + "MacAddress": "", + "Networks": { + "bridge": { + "NetworkID": "7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812", + "EndpointID": "7587b82f0dada3656fda26588aee72630c6fab1536d36e394b2bfbcf898c971d", + "Gateway": "172.17.0.1", + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "02:42:ac:12:00:02" + } + } + }, + "Path": "/bin/sh", + "ProcessLabel": "", + "ResolvConfPath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/resolv.conf", + "RestartCount": 1, + "State": { + "Error": "", + "ExitCode": 9, + "FinishedAt": "2015-01-06T15:47:32.080254511Z", + "OOMKilled": false, + "Dead": false, + "Paused": false, + "Pid": 0, + "Restarting": false, + "Running": true, + "StartedAt": "2015-01-06T15:47:32.072697474Z", + "Status": "running" + }, + "Mounts": [ + { + "Name": "fac362...80535", + "Source": "/data", + "Destination": "/data", + "Driver": "local", + "Mode": "ro,Z", + "RW": false, + "Propagation": "" + } + ] + } + +**Example request, with size information**: + + GET /containers/4fa6e0f0c678/json?size=1 HTTP/1.1 + +**Example response, with size information**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + .... + "SizeRw": 0, + "SizeRootFs": 972, + .... + } + +Query Parameters: + +- **size** – 1/True/true or 0/False/false, return container size information. Default is `false`. + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### List processes running inside a container + +`GET /containers/(id or name)/top` + +List processes running inside the container `id`. On Unix systems this +is done by running the `ps` command. This endpoint is not +supported on Windows. + +**Example request**: + + GET /containers/4fa6e0f0c678/top HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Titles" : [ + "UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD" + ], + "Processes" : [ + [ + "root", "13642", "882", "0", "17:03", "pts/0", "00:00:00", "/bin/bash" + ], + [ + "root", "13735", "13642", "0", "17:06", "pts/0", "00:00:00", "sleep 10" + ] + ] + } + +**Example request**: + + GET /containers/4fa6e0f0c678/top?ps_args=aux HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Titles" : [ + "USER","PID","%CPU","%MEM","VSZ","RSS","TTY","STAT","START","TIME","COMMAND" + ] + "Processes" : [ + [ + "root","13642","0.0","0.1","18172","3184","pts/0","Ss","17:03","0:00","/bin/bash" + ], + [ + "root","13895","0.0","0.0","4348","692","pts/0","S+","17:15","0:00","sleep 10" + ] + ], + } + +Query Parameters: + +- **ps_args** – `ps` arguments to use (e.g., `aux`), defaults to `-ef` + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Get container logs + +`GET /containers/(id or name)/logs` + +Get `stdout` and `stderr` logs from the container ``id`` + +> **Note**: +> This endpoint works only for containers with the `json-file` or `journald` logging drivers. + +**Example request**: + + GET /containers/4fa6e0f0c678/logs?stderr=1&stdout=1×tamps=1&follow=1&tail=10&since=1428990821 HTTP/1.1 + +**Example response**: + + HTTP/1.1 101 UPGRADED + Content-Type: application/vnd.docker.raw-stream + Connection: Upgrade + Upgrade: tcp + + {{ STREAM }} + +Query Parameters: + +- **follow** – 1/True/true or 0/False/false, return stream. Default `false`. +- **stdout** – 1/True/true or 0/False/false, show `stdout` log. Default `false`. +- **stderr** – 1/True/true or 0/False/false, show `stderr` log. Default `false`. +- **since** – UNIX timestamp (integer) to filter logs. Specifying a timestamp + will only output log-entries since that timestamp. Default: 0 (unfiltered) +- **timestamps** – 1/True/true or 0/False/false, print timestamps for + every log line. Default `false`. +- **tail** – Output specified number of lines at the end of logs: `all` or ``. Default all. + +Status Codes: + +- **101** – no error, hints proxy about hijacking +- **200** – no error, no upgrade header found +- **404** – no such container +- **500** – server error + +### Inspect changes on a container's filesystem + +`GET /containers/(id or name)/changes` + +Inspect changes on container `id`'s filesystem + +**Example request**: + + GET /containers/4fa6e0f0c678/changes HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Path": "/dev", + "Kind": 0 + }, + { + "Path": "/dev/kmsg", + "Kind": 1 + }, + { + "Path": "/test", + "Kind": 1 + } + ] + +Values for `Kind`: + +- `0`: Modify +- `1`: Add +- `2`: Delete + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Export a container + +`GET /containers/(id or name)/export` + +Export the contents of container `id` + +**Example request**: + + GET /containers/4fa6e0f0c678/export HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/octet-stream + + {{ TAR STREAM }} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Get container stats based on resource usage + +`GET /containers/(id or name)/stats` + +This endpoint returns a live stream of a container's resource usage statistics. + +**Example request**: + + GET /containers/redis1/stats HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "read" : "2015-01-08T22:57:31.547920715Z", + "pids_stats": { + "current": 3 + }, + "networks": { + "eth0": { + "rx_bytes": 5338, + "rx_dropped": 0, + "rx_errors": 0, + "rx_packets": 36, + "tx_bytes": 648, + "tx_dropped": 0, + "tx_errors": 0, + "tx_packets": 8 + }, + "eth5": { + "rx_bytes": 4641, + "rx_dropped": 0, + "rx_errors": 0, + "rx_packets": 26, + "tx_bytes": 690, + "tx_dropped": 0, + "tx_errors": 0, + "tx_packets": 9 + } + }, + "memory_stats" : { + "stats" : { + "total_pgmajfault" : 0, + "cache" : 0, + "mapped_file" : 0, + "total_inactive_file" : 0, + "pgpgout" : 414, + "rss" : 6537216, + "total_mapped_file" : 0, + "writeback" : 0, + "unevictable" : 0, + "pgpgin" : 477, + "total_unevictable" : 0, + "pgmajfault" : 0, + "total_rss" : 6537216, + "total_rss_huge" : 6291456, + "total_writeback" : 0, + "total_inactive_anon" : 0, + "rss_huge" : 6291456, + "hierarchical_memory_limit" : 67108864, + "total_pgfault" : 964, + "total_active_file" : 0, + "active_anon" : 6537216, + "total_active_anon" : 6537216, + "total_pgpgout" : 414, + "total_cache" : 0, + "inactive_anon" : 0, + "active_file" : 0, + "pgfault" : 964, + "inactive_file" : 0, + "total_pgpgin" : 477 + }, + "max_usage" : 6651904, + "usage" : 6537216, + "failcnt" : 0, + "limit" : 67108864 + }, + "blkio_stats" : {}, + "cpu_stats" : { + "cpu_usage" : { + "percpu_usage" : [ + 8646879, + 24472255, + 36438778, + 30657443 + ], + "usage_in_usermode" : 50000000, + "total_usage" : 100215355, + "usage_in_kernelmode" : 30000000 + }, + "system_cpu_usage" : 739306590000000, + "throttling_data" : {"periods":0,"throttled_periods":0,"throttled_time":0} + }, + "precpu_stats" : { + "cpu_usage" : { + "percpu_usage" : [ + 8646879, + 24350896, + 36438778, + 30657443 + ], + "usage_in_usermode" : 50000000, + "total_usage" : 100093996, + "usage_in_kernelmode" : 30000000 + }, + "system_cpu_usage" : 9492140000000, + "throttling_data" : {"periods":0,"throttled_periods":0,"throttled_time":0} + } + } + +The precpu_stats is the cpu statistic of last read, which is used for calculating the cpu usage percent. It is not the exact copy of the “cpu_stats” field. + +Query Parameters: + +- **stream** – 1/True/true or 0/False/false, pull stats once then disconnect. Default `true`. + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Resize a container TTY + +`POST /containers/(id or name)/resize` + +Resize the TTY for container with `id`. The unit is number of characters. You must restart the container for the resize to take effect. + +**Example request**: + + POST /containers/4fa6e0f0c678/resize?h=40&w=80 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Length: 0 + Content-Type: text/plain; charset=utf-8 + +Query Parameters: + +- **h** – height of `tty` session +- **w** – width + +Status Codes: + +- **200** – no error +- **404** – No such container +- **500** – Cannot resize container + +### Start a container + +`POST /containers/(id or name)/start` + +Start the container `id` + +> **Note**: +> For backwards compatibility, this endpoint accepts a `HostConfig` as JSON-encoded request body. +> See [create a container](#create-a-container) for details. + +**Example request**: + + POST /containers/e90e34656806/start HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **detachKeys** – Override the key sequence for detaching a + container. Format is a single character `[a-Z]` or `ctrl-` + where `` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`. + +Status Codes: + +- **204** – no error +- **304** – container already started +- **404** – no such container +- **500** – server error + +### Stop a container + +`POST /containers/(id or name)/stop` + +Stop the container `id` + +**Example request**: + + POST /containers/e90e34656806/stop?t=5 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **t** – number of seconds to wait before killing the container + +Status Codes: + +- **204** – no error +- **304** – container already stopped +- **404** – no such container +- **500** – server error + +### Restart a container + +`POST /containers/(id or name)/restart` + +Restart the container `id` + +**Example request**: + + POST /containers/e90e34656806/restart?t=5 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **t** – number of seconds to wait before killing the container + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Kill a container + +`POST /containers/(id or name)/kill` + +Kill the container `id` + +**Example request**: + + POST /containers/e90e34656806/kill HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters + +- **signal** - Signal to send to the container: integer or string like `SIGINT`. + When not set, `SIGKILL` is assumed and the call waits for the container to exit. + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Update a container + +`POST /containers/(id or name)/update` + +Update configuration of one or more containers. + +**Example request**: + + POST /containers/e90e34656806/update HTTP/1.1 + Content-Type: application/json + + { + "BlkioWeight": 300, + "CpuShares": 512, + "CpuPeriod": 100000, + "CpuQuota": 50000, + "CpusetCpus": "0,1", + "CpusetMems": "0", + "Memory": 314572800, + "MemorySwap": 514288000, + "MemoryReservation": 209715200, + "KernelMemory": 52428800, + "RestartPolicy": { + "MaximumRetryCount": 4, + "Name": "on-failure" + }, + } + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Warnings": [] + } + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + +### Rename a container + +`POST /containers/(id or name)/rename` + +Rename the container `id` to a `new_name` + +**Example request**: + + POST /containers/e90e34656806/rename?name=new_name HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **name** – new name for the container + +Status Codes: + +- **204** – no error +- **404** – no such container +- **409** - conflict name already assigned +- **500** – server error + +### Pause a container + +`POST /containers/(id or name)/pause` + +Pause the container `id` + +**Example request**: + + POST /containers/e90e34656806/pause HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Unpause a container + +`POST /containers/(id or name)/unpause` + +Unpause the container `id` + +**Example request**: + + POST /containers/e90e34656806/unpause HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Attach to a container + +`POST /containers/(id or name)/attach` + +Attach to the container `id` + +**Example request**: + + POST /containers/16253994b7c4/attach?logs=1&stream=0&stdout=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 101 UPGRADED + Content-Type: application/vnd.docker.raw-stream + Connection: Upgrade + Upgrade: tcp + + {{ STREAM }} + +Query Parameters: + +- **detachKeys** – Override the key sequence for detaching a + container. Format is a single character `[a-Z]` or `ctrl-` + where `` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`. +- **logs** – 1/True/true or 0/False/false, return logs. Default `false`. +- **stream** – 1/True/true or 0/False/false, return stream. + Default `false`. +- **stdin** – 1/True/true or 0/False/false, if `stream=true`, attach + to `stdin`. Default `false`. +- **stdout** – 1/True/true or 0/False/false, if `logs=true`, return + `stdout` log, if `stream=true`, attach to `stdout`. Default `false`. +- **stderr** – 1/True/true or 0/False/false, if `logs=true`, return + `stderr` log, if `stream=true`, attach to `stderr`. Default `false`. + +Status Codes: + +- **101** – no error, hints proxy about hijacking +- **200** – no error, no upgrade header found +- **400** – bad parameter +- **404** – no such container +- **500** – server error + + **Stream details**: + + When using the TTY setting is enabled in + [`POST /containers/create` + ](#create-a-container), + the stream is the raw data from the process PTY and client's `stdin`. + When the TTY is disabled, then the stream is multiplexed to separate + `stdout` and `stderr`. + + The format is a **Header** and a **Payload** (frame). + + **HEADER** + + The header contains the information which the stream writes (`stdout` or + `stderr`). It also contains the size of the associated frame encoded in the + last four bytes (`uint32`). + + It is encoded on the first eight bytes like this: + + header := [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4} + + `STREAM_TYPE` can be: + +- 0: `stdin` (is written on `stdout`) +- 1: `stdout` +- 2: `stderr` + + `SIZE1, SIZE2, SIZE3, SIZE4` are the four bytes of + the `uint32` size encoded as big endian. + + **PAYLOAD** + + The payload is the raw stream. + + **IMPLEMENTATION** + + The simplest way to implement the Attach protocol is the following: + + 1. Read eight bytes. + 2. Choose `stdout` or `stderr` depending on the first byte. + 3. Extract the frame size from the last four bytes. + 4. Read the extracted size and output it on the correct output. + 5. Goto 1. + +### Attach to a container (websocket) + +`GET /containers/(id or name)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **detachKeys** – Override the key sequence for detaching a + container. Format is a single character `[a-Z]` or `ctrl-` + where `` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`. +- **logs** – 1/True/true or 0/False/false, return logs. Default `false`. +- **stream** – 1/True/true or 0/False/false, return stream. + Default `false`. +- **stdin** – 1/True/true or 0/False/false, if `stream=true`, attach + to `stdin`. Default `false`. +- **stdout** – 1/True/true or 0/False/false, if `logs=true`, return + `stdout` log, if `stream=true`, attach to `stdout`. Default `false`. +- **stderr** – 1/True/true or 0/False/false, if `logs=true`, return + `stderr` log, if `stream=true`, attach to `stderr`. Default `false`. + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + +### Wait a container + +`POST /containers/(id or name)/wait` + +Block until container `id` stops, then returns the exit code + +**Example request**: + + POST /containers/16253994b7c4/wait HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"StatusCode": 0} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Remove a container + +`DELETE /containers/(id or name)` + +Remove the container `id` from the filesystem + +**Example request**: + + DELETE /containers/16253994b7c4?v=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **v** – 1/True/true or 0/False/false, Remove the volumes + associated to the container. Default `false`. +- **force** - 1/True/true or 0/False/false, Kill then remove the container. + Default `false`. + +Status Codes: + +- **204** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + +### Copy files or folders from a container + +`POST /containers/(id or name)/copy` + +Copy files or folders of container `id` + +**Deprecated** in favor of the `archive` endpoint below. + +**Example request**: + + POST /containers/4fa6e0f0c678/copy HTTP/1.1 + Content-Type: application/json + + { + "Resource": "test.txt" + } + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + {{ TAR STREAM }} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Retrieving information about files and folders in a container + +`HEAD /containers/(id or name)/archive` + +See the description of the `X-Docker-Container-Path-Stat` header in the +following section. + +### Get an archive of a filesystem resource in a container + +`GET /containers/(id or name)/archive` + +Get an tar archive of a resource in the filesystem of container `id`. + +Query Parameters: + +- **path** - resource in the container's filesystem to archive. Required. + + If not an absolute path, it is relative to the container's root directory. + The resource specified by **path** must exist. To assert that the resource + is expected to be a directory, **path** should end in `/` or `/.` + (assuming a path separator of `/`). If **path** ends in `/.` then this + indicates that only the contents of the **path** directory should be + copied. A symlink is always resolved to its target. + + **Note**: It is not possible to copy certain system files such as resources + under `/proc`, `/sys`, `/dev`, and mounts created by the user in the + container. + +**Example request**: + + GET /containers/8cce319429b2/archive?path=/root HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + X-Docker-Container-Path-Stat: eyJuYW1lIjoicm9vdCIsInNpemUiOjQwOTYsIm1vZGUiOjIxNDc0ODQwOTYsIm10aW1lIjoiMjAxNC0wMi0yN1QyMDo1MToyM1oiLCJsaW5rVGFyZ2V0IjoiIn0= + + {{ TAR STREAM }} + +On success, a response header `X-Docker-Container-Path-Stat` will be set to a +base64-encoded JSON object containing some filesystem header information about +the archived resource. The above example value would decode to the following +JSON object (whitespace added for readability): + + { + "name": "root", + "size": 4096, + "mode": 2147484096, + "mtime": "2014-02-27T20:51:23Z", + "linkTarget": "" + } + +A `HEAD` request can also be made to this endpoint if only this information is +desired. + +Status Codes: + +- **200** - success, returns archive of copied resource +- **400** - client error, bad parameter, details in JSON response body, one of: + - must specify path parameter (**path** cannot be empty) + - not a directory (**path** was asserted to be a directory but exists as a + file) +- **404** - client error, resource not found, one of: + – no such container (container `id` does not exist) + - no such file or directory (**path** does not exist) +- **500** - server error + +### Extract an archive of files or folders to a directory in a container + +`PUT /containers/(id or name)/archive` + +Upload a tar archive to be extracted to a path in the filesystem of container +`id`. + +Query Parameters: + +- **path** - path to a directory in the container + to extract the archive's contents into. Required. + + If not an absolute path, it is relative to the container's root directory. + The **path** resource must exist. +- **noOverwriteDirNonDir** - If "1", "true", or "True" then it will be an error + if unpacking the given content would cause an existing directory to be + replaced with a non-directory and vice versa. + +**Example request**: + + PUT /containers/8cce319429b2/archive?path=/vol1 HTTP/1.1 + Content-Type: application/x-tar + + {{ TAR STREAM }} + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – the content was extracted successfully +- **400** - client error, bad parameter, details in JSON response body, one of: + - must specify path parameter (**path** cannot be empty) + - not a directory (**path** should be a directory but exists as a file) + - unable to overwrite existing directory with non-directory + (if **noOverwriteDirNonDir**) + - unable to overwrite existing non-directory with directory + (if **noOverwriteDirNonDir**) +- **403** - client error, permission denied, the volume + or container rootfs is marked as read-only. +- **404** - client error, resource not found, one of: + – no such container (container `id` does not exist) + - no such file or directory (**path** resource does not exist) +- **500** – server error + +## 2.2 Images + +### List Images + +`GET /images/json` + +**Example request**: + + GET /images/json?all=0 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "RepoTags": [ + "ubuntu:12.04", + "ubuntu:precise", + "ubuntu:latest" + ], + "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", + "Created": 1365714795, + "Size": 131506275, + "VirtualSize": 131506275, + "Labels": {} + }, + { + "RepoTags": [ + "ubuntu:12.10", + "ubuntu:quantal" + ], + "ParentId": "27cf784147099545", + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Created": 1364102658, + "Size": 24653, + "VirtualSize": 180116135, + "Labels": { + "com.example.version": "v1" + } + } + ] + +**Example request, with digest information**: + + GET /images/json?digests=1 HTTP/1.1 + +**Example response, with digest information**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Created": 1420064636, + "Id": "4986bf8c15363d1c5d15512d5266f8777bfba4974ac56e3270e7760f6f0a8125", + "ParentId": "ea13149945cb6b1e746bf28032f02e9b5a793523481a0a18645fc77ad53c4ea2", + "RepoDigests": [ + "localhost:5000/test/busybox@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf" + ], + "RepoTags": [ + "localhost:5000/test/busybox:latest", + "playdate:latest" + ], + "Size": 0, + "VirtualSize": 2429728, + "Labels": {} + } + ] + +The response shows a single image `Id` associated with two repositories +(`RepoTags`): `localhost:5000/test/busybox`: and `playdate`. A caller can use +either of the `RepoTags` values `localhost:5000/test/busybox:latest` or +`playdate:latest` to reference the image. + +You can also use `RepoDigests` values to reference an image. In this response, +the array has only one reference and that is to the +`localhost:5000/test/busybox` repository; the `playdate` repository has no +digest. You can reference this digest using the value: +`localhost:5000/test/busybox@sha256:cbbf2f9a99b47fc460d...` + +See the `docker run` and `docker build` commands for examples of digest and tag +references on the command line. + +Query Parameters: + +- **all** – 1/True/true or 0/False/false, default false +- **filters** – a JSON encoded value of the filters (a map[string][]string) to process on the images list. Available filters: + - `dangling=true` + - `label=key` or `label="key=value"` of an image label +- **filter** - only return images with the specified name + +### Build image from a Dockerfile + +`POST /build` + +Build an image from a Dockerfile + +**Example request**: + + POST /build HTTP/1.1 + + {{ TAR STREAM }} + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"stream": "Step 1..."} + {"stream": "..."} + {"error": "Error...", "errorDetail": {"code": 123, "message": "Error..."}} + +The input stream must be a `tar` archive compressed with one of the +following algorithms: `identity` (no compression), `gzip`, `bzip2`, `xz`. + +The archive must include a build instructions file, typically called +`Dockerfile` at the archive's root. The `dockerfile` parameter may be +used to specify a different build instructions file. To do this, its value must be +the path to the alternate build instructions file to use. + +The archive may include any number of other files, +which are accessible in the build context (See the [*ADD build +command*](../../reference/builder.md#dockerbuilder)). + +The build is canceled if the client drops the connection by quitting +or being killed. + +Query Parameters: + +- **dockerfile** - Path within the build context to the Dockerfile. This is + ignored if `remote` is specified and points to an individual filename. +- **t** – A name and optional tag to apply to the image in the `name:tag` format. + If you omit the `tag` the default `latest` value is assumed. + You can provide one or more `t` parameters. +- **remote** – A Git repository URI or HTTP/HTTPS URI build source. If the + URI specifies a filename, the file's contents are placed into a file + called `Dockerfile`. +- **q** – Suppress verbose build output. +- **nocache** – Do not use the cache when building the image. +- **pull** - Attempt to pull the image even if an older image exists locally. +- **rm** - Remove intermediate containers after a successful build (default behavior). +- **forcerm** - Always remove intermediate containers (includes `rm`). +- **memory** - Set memory limit for build. +- **memswap** - Total memory (memory + swap), `-1` to enable unlimited swap. +- **cpushares** - CPU shares (relative weight). +- **cpusetcpus** - CPUs in which to allow execution (e.g., `0-3`, `0,1`). +- **cpuperiod** - The length of a CPU period in microseconds. +- **cpuquota** - Microseconds of CPU time that the container can get in a CPU period. +- **buildargs** – JSON map of string pairs for build-time variables. Users pass + these values at build-time. Docker uses the `buildargs` as the environment + context for command(s) run via the Dockerfile's `RUN` instruction or for + variable expansion in other Dockerfile instructions. This is not meant for + passing secret values. [Read more about the buildargs instruction](../../reference/builder.md#arg) +- **shmsize** - Size of `/dev/shm` in bytes. The size must be greater than 0. If omitted the system uses 64MB. +- **labels** – JSON map of string pairs for labels to set on the image. + + Request Headers: + +- **Content-type** – Set to `"application/tar"`. +- **X-Registry-Config** – A base64-url-safe-encoded Registry Auth Config JSON + object with the following structure: + + { + "docker.example.com": { + "username": "janedoe", + "password": "hunter2" + }, + "https://index.docker.io/v1/": { + "username": "mobydock", + "password": "conta1n3rize14" + } + } + + This object maps the hostname of a registry to an object containing the + "username" and "password" for that registry. Multiple registries may + be specified as the build may be based on an image requiring + authentication to pull from any arbitrary registry. Only the registry + domain name (and port if not the default "443") are required. However + (for legacy reasons) the "official" Docker, Inc. hosted registry must + be specified with both a "https://" prefix and a "/v1/" suffix even + though Docker will prefer to use the v2 registry API. + +Status Codes: + +- **200** – no error +- **500** – server error + +### Create an image + +`POST /images/create` + +Create an image either by pulling it from the registry or by importing it + +**Example request**: + + POST /images/create?fromImage=ubuntu HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "Pulling..."} + {"status": "Pulling", "progress": "1 B/ 100 B", "progressDetail": {"current": 1, "total": 100}} + {"error": "Invalid..."} + ... + +When using this endpoint to pull an image from the registry, the +`X-Registry-Auth` header can be used to include +a base64-encoded AuthConfig object. + +Query Parameters: + +- **fromImage** – Name of the image to pull. The name may include a tag or + digest. This parameter may only be used when pulling an image. + The pull is cancelled if the HTTP connection is closed. +- **fromSrc** – Source to import. The value may be a URL from which the image + can be retrieved or `-` to read the image from the request body. + This parameter may only be used when importing an image. +- **repo** – Repository name given to an image when it is imported. + The repo may include a tag. This parameter may only be used when importing + an image. +- **tag** – Tag or digest. + + Request Headers: + +- **X-Registry-Auth** – base64-encoded AuthConfig object, containing either login information, or a token + - Credential based login: + + ``` + { + "username": "jdoe", + "password": "secret", + "email": "jdoe@acme.com", + } + ``` + + - Token based login: + + ``` + { + "registrytoken": "9cbaf023786cd7..." + } + ``` + +Status Codes: + +- **200** – no error +- **500** – server error + + + +### Inspect an image + +`GET /images/(name)/json` + +Return low-level information on the image `name` + +**Example request**: + + GET /images/example/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Id" : "sha256:85f05633ddc1c50679be2b16a0479ab6f7637f8884e0cfe0f4d20e1ebb3d6e7c", + "Container" : "cb91e48a60d01f1e27028b4fc6819f4f290b3cf12496c8176ec714d0d390984a", + "Comment" : "", + "Os" : "linux", + "Architecture" : "amd64", + "Parent" : "sha256:91e54dfb11794fad694460162bf0cb0a4fa710cfa3f60979c177d920813e267c", + "ContainerConfig" : { + "Tty" : false, + "Hostname" : "e611e15f9c9d", + "Volumes" : null, + "Domainname" : "", + "AttachStdout" : false, + "PublishService" : "", + "AttachStdin" : false, + "OpenStdin" : false, + "StdinOnce" : false, + "NetworkDisabled" : false, + "OnBuild" : [], + "Image" : "91e54dfb11794fad694460162bf0cb0a4fa710cfa3f60979c177d920813e267c", + "User" : "", + "WorkingDir" : "", + "Entrypoint" : null, + "MacAddress" : "", + "AttachStderr" : false, + "Labels" : { + "com.example.license" : "GPL", + "com.example.version" : "1.0", + "com.example.vendor" : "Acme" + }, + "Env" : [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "ExposedPorts" : null, + "Cmd" : [ + "/bin/sh", + "-c", + "#(nop) LABEL com.example.vendor=Acme com.example.license=GPL com.example.version=1.0" + ] + }, + "DockerVersion" : "1.9.0-dev", + "VirtualSize" : 188359297, + "Size" : 0, + "Author" : "", + "Created" : "2015-09-10T08:30:53.26995814Z", + "GraphDriver" : { + "Name" : "aufs", + "Data" : null + }, + "RepoDigests" : [ + "localhost:5000/test/busybox/example@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf" + ], + "RepoTags" : [ + "example:1.0", + "example:latest", + "example:stable" + ], + "Config" : { + "Image" : "91e54dfb11794fad694460162bf0cb0a4fa710cfa3f60979c177d920813e267c", + "NetworkDisabled" : false, + "OnBuild" : [], + "StdinOnce" : false, + "PublishService" : "", + "AttachStdin" : false, + "OpenStdin" : false, + "Domainname" : "", + "AttachStdout" : false, + "Tty" : false, + "Hostname" : "e611e15f9c9d", + "Volumes" : null, + "Cmd" : [ + "/bin/bash" + ], + "ExposedPorts" : null, + "Env" : [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Labels" : { + "com.example.vendor" : "Acme", + "com.example.version" : "1.0", + "com.example.license" : "GPL" + }, + "Entrypoint" : null, + "MacAddress" : "", + "AttachStderr" : false, + "WorkingDir" : "", + "User" : "" + }, + "RootFS": { + "Type": "layers", + "Layers": [ + "sha256:1834950e52ce4d5a88a1bbd131c537f4d0e56d10ff0dd69e66be3b7dfa9df7e6", + "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + ] + } + } + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Get the history of an image + +`GET /images/(name)/history` + +Return the history of the image `name` + +**Example request**: + + GET /images/ubuntu/history HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "3db9c44f45209632d6050b35958829c3a2aa256d81b9a7be45b362ff85c54710", + "Created": 1398108230, + "CreatedBy": "/bin/sh -c #(nop) ADD file:eb15dbd63394e063b805a3c32ca7bf0266ef64676d5a6fab4801f2e81e2a5148 in /", + "Tags": [ + "ubuntu:lucid", + "ubuntu:10.04" + ], + "Size": 182964289, + "Comment": "" + }, + { + "Id": "6cfa4d1f33fb861d4d114f43b25abd0ac737509268065cdfd69d544a59c85ab8", + "Created": 1398108222, + "CreatedBy": "/bin/sh -c #(nop) MAINTAINER Tianon Gravi - mkimage-debootstrap.sh -i iproute,iputils-ping,ubuntu-minimal -t lucid.tar.xz lucid http://archive.ubuntu.com/ubuntu/", + "Tags": null, + "Size": 0, + "Comment": "" + }, + { + "Id": "511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158", + "Created": 1371157430, + "CreatedBy": "", + "Tags": [ + "scratch12:latest", + "scratch:latest" + ], + "Size": 0, + "Comment": "Imported from -" + } + ] + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Push an image on the registry + +`POST /images/(name)/push` + +Push the image `name` on the registry + +**Example request**: + + POST /images/test/push HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status": "Pushing..."} + {"status": "Pushing", "progress": "1/? (n/a)", "progressDetail": {"current": 1}}} + {"error": "Invalid..."} + ... + +If you wish to push an image on to a private registry, that image must already have a tag +into a repository which references that registry `hostname` and `port`. This repository name should +then be used in the URL. This duplicates the command line's flow. + +The push is cancelled if the HTTP connection is closed. + +**Example request**: + + POST /images/registry.acme.com:5000/test/push HTTP/1.1 + + +Query Parameters: + +- **tag** – The tag to associate with the image on the registry. This is optional. + +Request Headers: + +- **X-Registry-Auth** – base64-encoded AuthConfig object, containing either login information, or a token + - Credential based login: + + ``` + { + "username": "jdoe", + "password": "secret", + "email": "jdoe@acme.com", + } + ``` + + - Identity token based login: + + ``` + { + "identitytoken": "9cbaf023786cd7..." + } + ``` + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Tag an image into a repository + +`POST /images/(name)/tag` + +Tag the image `name` into a repository + +**Example request**: + + POST /images/test/tag?repo=myrepo&force=0&tag=v42 HTTP/1.1 + +**Example response**: + + HTTP/1.1 201 Created + +Query Parameters: + +- **repo** – The repository to tag in +- **force** – 1/True/true or 0/False/false, default false +- **tag** - The new tag name + +Status Codes: + +- **201** – no error +- **400** – bad parameter +- **404** – no such image +- **409** – conflict +- **500** – server error + +### Remove an image + +`DELETE /images/(name)` + +Remove the image `name` from the filesystem + +**Example request**: + + DELETE /images/test HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-type: application/json + + [ + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} + ] + +Query Parameters: + +- **force** – 1/True/true or 0/False/false, default false +- **noprune** – 1/True/true or 0/False/false, default false + +Status Codes: + +- **200** – no error +- **404** – no such image +- **409** – conflict +- **500** – server error + +### Search images + +`GET /images/search` + +Search for an image on [Docker Hub](https://hub.docker.com). + +> **Note**: +> The response keys have changed from API v1.6 to reflect the JSON +> sent by the registry server to the docker daemon's request. + +**Example request**: + + GET /images/search?term=sshd HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "wma55/u1210sshd", + "star_count": 0 + }, + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "jdswinbank/sshd", + "star_count": 0 + }, + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "vgauthier/sshd", + "star_count": 0 + } + ... + ] + +Query Parameters: + +- **term** – term to search + +Status Codes: + +- **200** – no error +- **500** – server error + +## 2.3 Misc + +### Check auth configuration + +`POST /auth` + +Validate credentials for a registry and get identity token, +if available, for accessing the registry without password. + +**Example request**: + + POST /auth HTTP/1.1 + Content-Type: application/json + + { + "username": "hannibal", + "password": "xxxx", + "serveraddress": "https://index.docker.io/v1/" + } + +**Example response**: + + HTTP/1.1 200 OK + + { + "Status": "Login Succeeded", + "IdentityToken": "9cbaf023786cd7..." + } + +Status Codes: + +- **200** – no error +- **204** – no error +- **500** – server error + +### Display system-wide information + +`GET /info` + +Display system-wide information + +**Example request**: + + GET /info HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Architecture": "x86_64", + "ClusterStore": "etcd://localhost:2379", + "CgroupDriver": "cgroupfs", + "Containers": 11, + "ContainersRunning": 7, + "ContainersStopped": 3, + "ContainersPaused": 1, + "CpuCfsPeriod": true, + "CpuCfsQuota": true, + "Debug": false, + "DockerRootDir": "/var/lib/docker", + "Driver": "btrfs", + "DriverStatus": [[""]], + "ExecutionDriver": "native-0.1", + "ExperimentalBuild": false, + "HttpProxy": "http://test:test@localhost:8080", + "HttpsProxy": "https://test:test@localhost:8080", + "ID": "7TRN:IPZB:QYBB:VPBQ:UMPP:KARE:6ZNR:XE6T:7EWV:PKF4:ZOJD:TPYS", + "IPv4Forwarding": true, + "Images": 16, + "IndexServerAddress": "https://index.docker.io/v1/", + "InitPath": "/usr/bin/docker", + "InitSha1": "", + "KernelMemory": true, + "KernelVersion": "3.12.0-1-amd64", + "Labels": [ + "storage=ssd" + ], + "MemTotal": 2099236864, + "MemoryLimit": true, + "NCPU": 1, + "NEventsListener": 0, + "NFd": 11, + "NGoroutines": 21, + "Name": "prod-server-42", + "NoProxy": "9.81.1.160", + "OomKillDisable": true, + "OSType": "linux", + "OperatingSystem": "Boot2Docker", + "Plugins": { + "Volume": [ + "local" + ], + "Network": [ + "null", + "host", + "bridge" + ] + }, + "RegistryConfig": { + "IndexConfigs": { + "docker.io": { + "Mirrors": null, + "Name": "docker.io", + "Official": true, + "Secure": true + } + }, + "InsecureRegistryCIDRs": [ + "127.0.0.0/8" + ] + }, + "ServerVersion": "1.9.0", + "SwapLimit": false, + "SystemStatus": [["State", "Healthy"]], + "SystemTime": "2015-03-10T11:11:23.730591467-07:00" + } + +Status Codes: + +- **200** – no error +- **500** – server error + +### Show the docker version information + +`GET /version` + +Show the docker version information + +**Example request**: + + GET /version HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Version": "1.10.0-dev", + "Os": "linux", + "KernelVersion": "3.19.0-23-generic", + "GoVersion": "go1.4.2", + "GitCommit": "e75da4b", + "Arch": "amd64", + "ApiVersion": "1.23", + "BuildTime": "2015-12-01T07:09:13.444803460+00:00", + "Experimental": true + } + +Status Codes: + +- **200** – no error +- **500** – server error + +### Ping the docker server + +`GET /_ping` + +Ping the docker server + +**Example request**: + + GET /_ping HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: text/plain + + OK + +Status Codes: + +- **200** - no error +- **500** - server error + +### Create a new image from a container's changes + +`POST /commit` + +Create a new image from a container's changes + +**Example request**: + + POST /commit?container=44c004db4b17&comment=message&repo=myrepo HTTP/1.1 + Content-Type: application/json + + { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "date" + ], + "Mounts": [ + { + "Source": "/data", + "Destination": "/data", + "Mode": "ro,Z", + "RW": false + } + ], + "Labels": { + "key1": "value1", + "key2": "value2" + }, + "WorkingDir": "", + "NetworkDisabled": false, + "ExposedPorts": { + "22/tcp": {} + } + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + {"Id": "596069db4bf5"} + +Json Parameters: + +- **config** - the container's configuration + +Query Parameters: + +- **container** – source container +- **repo** – repository +- **tag** – tag +- **comment** – commit message +- **author** – author (e.g., "John Hannibal Smith + <[hannibal@a-team.com](mailto:hannibal%40a-team.com)>") +- **pause** – 1/True/true or 0/False/false, whether to pause the container before committing +- **changes** – Dockerfile instructions to apply while committing + +Status Codes: + +- **201** – no error +- **404** – no such container +- **500** – server error + +### Monitor Docker's events + +`GET /events` + +Get container events from docker, either in real time via streaming, or via polling (using since). + +Docker containers report the following events: + + attach, commit, copy, create, destroy, die, exec_create, exec_start, export, kill, oom, pause, rename, resize, restart, start, stop, top, unpause, update + +Docker images report the following events: + + delete, import, pull, push, tag, untag + +Docker volumes report the following events: + + create, mount, unmount, destroy + +Docker networks report the following events: + + create, connect, disconnect, destroy + +**Example request**: + + GET /events?since=1374067924 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "action": "pull", + "type": "image", + "actor": { + "id": "busybox:latest", + "attributes": {} + } + "time": 1442421700, + "timeNano": 1442421700598988358 + }, + { + "action": "create", + "type": "container", + "actor": { + "id": "5745704abe9caa5", + "attributes": {"image": "busybox"} + } + "time": 1442421716, + "timeNano": 1442421716853979870 + }, + { + "action": "attach", + "type": "container", + "actor": { + "id": "5745704abe9caa5", + "attributes": {"image": "busybox"} + } + "time": 1442421716, + "timeNano": 1442421716894759198 + }, + { + "action": "start", + "type": "container", + "actor": { + "id": "5745704abe9caa5", + "attributes": {"image": "busybox"} + } + "time": 1442421716, + "timeNano": 1442421716983607193 + } + ] + +Query Parameters: + +- **since** – Timestamp used for polling +- **until** – Timestamp used for polling +- **filters** – A json encoded value of the filters (a map[string][]string) to process on the event list. Available filters: + - `container=`; -- container to filter + - `event=`; -- event to filter + - `image=`; -- image to filter + - `label=`; -- image and container label to filter + - `type=`; -- either `container` or `image` or `volume` or `network` + - `volume=`; -- volume to filter + - `network=`; -- network to filter + +Status Codes: + +- **200** – no error +- **500** – server error + +### Get a tarball containing all images in a repository + +`GET /images/(name)/get` + +Get a tarball containing all images and metadata for the repository specified +by `name`. + +If `name` is a specific name and tag (e.g. ubuntu:latest), then only that image +(and its parents) are returned. If `name` is an image ID, similarly only that +image (and its parents) are returned, but with the exclusion of the +'repositories' file in the tarball, as there were no image names referenced. + +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + GET /images/ubuntu/get + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + Binary data stream + +Status Codes: + +- **200** – no error +- **500** – server error + +### Get a tarball containing all images. + +`GET /images/get` + +Get a tarball containing all images and metadata for one or more repositories. + +For each value of the `names` parameter: if it is a specific name and tag (e.g. +`ubuntu:latest`), then only that image (and its parents) are returned; if it is +an image ID, similarly only that image (and its parents) are returned and there +would be no names referenced in the 'repositories' file for this image ID. + +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + GET /images/get?names=myname%2Fmyapp%3Alatest&names=busybox + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + Binary data stream + +Status Codes: + +- **200** – no error +- **500** – server error + +### Load a tarball with a set of images and tags into docker + +`POST /images/load` + +Load a set of images and tags into a Docker repository. +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + POST /images/load + + Tarball in body + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – no error +- **500** – server error + +### Image tarball format + +An image tarball contains one directory per image layer (named using its long ID), +each containing these files: + +- `VERSION`: currently `1.0` - the file format version +- `json`: detailed layer information, similar to `docker inspect layer_id` +- `layer.tar`: A tarfile containing the filesystem changes in this layer + +The `layer.tar` file contains `aufs` style `.wh..wh.aufs` files and directories +for storing attribute changes and deletions. + +If the tarball defines a repository, the tarball should also include a `repositories` file at +the root that contains a list of repository and tag names mapped to layer IDs. + +``` +{"hello-world": + {"latest": "565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} +} +``` + +### Exec Create + +`POST /containers/(id or name)/exec` + +Sets up an exec instance in a running container `id` + +**Example request**: + + POST /containers/e90e34656806/exec HTTP/1.1 + Content-Type: application/json + + { + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "DetachKeys": "ctrl-p,ctrl-q", + "Tty": false, + "Cmd": [ + "date" + ] + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "Id": "f90e34656806", + "Warnings":[] + } + +Json Parameters: + +- **AttachStdin** - Boolean value, attaches to `stdin` of the `exec` command. +- **AttachStdout** - Boolean value, attaches to `stdout` of the `exec` command. +- **AttachStderr** - Boolean value, attaches to `stderr` of the `exec` command. +- **DetachKeys** – Override the key sequence for detaching a + container. Format is a single character `[a-Z]` or `ctrl-` + where `` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`. +- **Tty** - Boolean value to allocate a pseudo-TTY. +- **Cmd** - Command to run specified as a string or an array of strings. + + +Status Codes: + +- **201** – no error +- **404** – no such container +- **409** - container is paused +- **500** - server error + +### Exec Start + +`POST /exec/(id)/start` + +Starts a previously set up `exec` instance `id`. If `detach` is true, this API +returns after starting the `exec` command. Otherwise, this API sets up an +interactive session with the `exec` command. + +**Example request**: + + POST /exec/e90e34656806/start HTTP/1.1 + Content-Type: application/json + + { + "Detach": false, + "Tty": false + } + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + +Json Parameters: + +- **Detach** - Detach from the `exec` command. +- **Tty** - Boolean value to allocate a pseudo-TTY. + +Status Codes: + +- **200** – no error +- **404** – no such exec instance +- **409** - container is paused + + **Stream details**: + Similar to the stream behavior of `POST /containers/(id or name)/attach` API + +### Exec Resize + +`POST /exec/(id)/resize` + +Resizes the `tty` session used by the `exec` command `id`. The unit is number of characters. +This API is valid only if `tty` was specified as part of creating and starting the `exec` command. + +**Example request**: + + POST /exec/e90e34656806/resize?h=40&w=80 HTTP/1.1 + Content-Type: text/plain + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: text/plain + +Query Parameters: + +- **h** – height of `tty` session +- **w** – width + +Status Codes: + +- **201** – no error +- **404** – no such exec instance + +### Exec Inspect + +`GET /exec/(id)/json` + +Return low-level information about the `exec` command `id`. + +**Example request**: + + GET /exec/11fb006128e8ceb3942e7c58d77750f24210e35f879dd204ac975c184b820b39/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "CanRemove": false, + "ContainerID": "b53ee82b53a40c7dca428523e34f741f3abc51d9f297a14ff874bf761b995126", + "DetachKeys": "", + "ExitCode": 2, + "ID": "f33bbfb39f5b142420f4759b2348913bd4a8d1a6d7fd56499cb41a1bb91d7b3b", + "OpenStderr": true, + "OpenStdin": true, + "OpenStdout": true, + "ProcessConfig": { + "arguments": [ + "-c", + "exit 2" + ], + "entrypoint": "sh", + "privileged": false, + "tty": true, + "user": "1000" + }, + "Running": false + } + +Status Codes: + +- **200** – no error +- **404** – no such exec instance +- **500** - server error + +## 2.4 Volumes + +### List volumes + +`GET /volumes` + +**Example request**: + + GET /volumes HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Volumes": [ + { + "Name": "tardis", + "Driver": "local", + "Mountpoint": "/var/lib/docker/volumes/tardis" + } + ], + "Warnings": [] + } + +Query Parameters: + +- **filters** - JSON encoded value of the filters (a `map[string][]string`) to process on the volumes list. There is one available filter: `dangling=true` + +Status Codes: + +- **200** - no error +- **500** - server error + +### Create a volume + +`POST /volumes/create` + +Create a volume + +**Example request**: + + POST /volumes/create HTTP/1.1 + Content-Type: application/json + + { + "Name": "tardis", + "Labels": { + "com.example.some-label": "some-value", + "com.example.some-other-label": "some-other-value" + }, + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "Name": "tardis", + "Driver": "local", + "Mountpoint": "/var/lib/docker/volumes/tardis", + "Labels": { + "com.example.some-label": "some-value", + "com.example.some-other-label": "some-other-value" + }, + } + +Status Codes: + +- **201** - no error +- **500** - server error + +JSON Parameters: + +- **Name** - The new volume's name. If not specified, Docker generates a name. +- **Driver** - Name of the volume driver to use. Defaults to `local` for the name. +- **DriverOpts** - A mapping of driver options and values. These options are + passed directly to the driver and are driver specific. +- **Labels** - Labels to set on the volume, specified as a map: `{"key":"value" [,"key2":"value2"]}` + +### Inspect a volume + +`GET /volumes/(name)` + +Return low-level information on the volume `name` + +**Example request**: + + GET /volumes/tardis + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Name": "tardis", + "Driver": "local", + "Mountpoint": "/var/lib/docker/volumes/tardis/_data", + "Labels": { + "com.example.some-label": "some-value", + "com.example.some-other-label": "some-other-value" + } + } + +Status Codes: + +- **200** - no error +- **404** - no such volume +- **500** - server error + +### Remove a volume + +`DELETE /volumes/(name)` + +Instruct the driver to remove the volume (`name`). + +**Example request**: + + DELETE /volumes/tardis HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes + +- **204** - no error +- **404** - no such volume or volume driver +- **409** - volume is in use and cannot be removed +- **500** - server error + +## 2.5 Networks + +### List networks + +`GET /networks` + +**Example request**: + + GET /networks?filters={"type":{"custom":true}} HTTP/1.1 + +**Example response**: + +``` +HTTP/1.1 200 OK +Content-Type: application/json + +[ + { + "Name": "bridge", + "Id": "f2de39df4171b0dc801e8002d1d999b77256983dfc63041c0f34030aa3977566", + "Scope": "local", + "Driver": "bridge", + "EnableIPv6": false, + "Internal": false, + "IPAM": { + "Driver": "default", + "Config": [ + { + "Subnet": "172.17.0.0/16" + } + ] + }, + "Containers": { + "39b69226f9d79f5634485fb236a23b2fe4e96a0a94128390a7fbbcc167065867": { + "EndpointID": "ed2419a97c1d9954d05b46e462e7002ea552f216e9b136b80a7db8d98b442eda", + "MacAddress": "02:42:ac:11:00:02", + "IPv4Address": "172.17.0.2/16", + "IPv6Address": "" + } + }, + "Options": { + "com.docker.network.bridge.default_bridge": "true", + "com.docker.network.bridge.enable_icc": "true", + "com.docker.network.bridge.enable_ip_masquerade": "true", + "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", + "com.docker.network.bridge.name": "docker0", + "com.docker.network.driver.mtu": "1500" + } + }, + { + "Name": "none", + "Id": "e086a3893b05ab69242d3c44e49483a3bbbd3a26b46baa8f61ab797c1088d794", + "Scope": "local", + "Driver": "null", + "EnableIPv6": false, + "Internal": false, + "IPAM": { + "Driver": "default", + "Config": [] + }, + "Containers": {}, + "Options": {} + }, + { + "Name": "host", + "Id": "13e871235c677f196c4e1ecebb9dc733b9b2d2ab589e30c539efeda84a24215e", + "Scope": "local", + "Driver": "host", + "EnableIPv6": false, + "Internal": false, + "IPAM": { + "Driver": "default", + "Config": [] + }, + "Containers": {}, + "Options": {} + } +] +``` + +Query Parameters: + +- **filters** - JSON encoded network list filter. The filter value is one of: + - `name=` Matches all or part of a network name. + - `id=` Matches all or part of a network id. + - `type=["custom"|"builtin"]` Filters networks by type. The `custom` keyword returns all user-defined networks. + +Status Codes: + +- **200** - no error +- **500** - server error + +### Inspect network + +`GET /networks/` + +**Example request**: + + GET /networks/7d86d31b1478e7cca9ebed7e73aa0fdeec46c5ca29497431d3007d2d9e15ed99 HTTP/1.1 + +**Example response**: + +``` +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "Name": "net01", + "Id": "7d86d31b1478e7cca9ebed7e73aa0fdeec46c5ca29497431d3007d2d9e15ed99", + "Scope": "local", + "Driver": "bridge", + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Config": [ + { + "Subnet": "172.19.0.0/16", + "Gateway": "172.19.0.1/16" + } + ], + "Options": { + "foo": "bar" + } + }, + "Internal": false, + "Containers": { + "19a4d5d687db25203351ed79d478946f861258f018fe384f229f2efa4b23513c": { + "Name": "test", + "EndpointID": "628cadb8bcb92de107b2a1e516cbffe463e321f548feb37697cce00ad694f21a", + "MacAddress": "02:42:ac:13:00:02", + "IPv4Address": "172.19.0.2/16", + "IPv6Address": "" + } + }, + "Options": { + "com.docker.network.bridge.default_bridge": "true", + "com.docker.network.bridge.enable_icc": "true", + "com.docker.network.bridge.enable_ip_masquerade": "true", + "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", + "com.docker.network.bridge.name": "docker0", + "com.docker.network.driver.mtu": "1500" + }, + "Labels": { + "com.example.some-label": "some-value", + "com.example.some-other-label": "some-other-value" + } +} +``` + +Status Codes: + +- **200** - no error +- **404** - network not found + +### Create a network + +`POST /networks/create` + +Create a network + +**Example request**: + +``` +POST /networks/create HTTP/1.1 +Content-Type: application/json + +{ + "Name":"isolated_nw", + "CheckDuplicate":false, + "Driver":"bridge", + "EnableIPv6": true, + "IPAM":{ + "Config":[ + { + "Subnet":"172.20.0.0/16", + "IPRange":"172.20.10.0/24", + "Gateway":"172.20.10.11" + }, + { + "Subnet":"2001:db8:abcd::/64", + "Gateway":"2001:db8:abcd::1011" + } + ], + "Options": { + "foo": "bar" + } + }, + "Internal":true, + "Options": { + "com.docker.network.bridge.default_bridge": "true", + "com.docker.network.bridge.enable_icc": "true", + "com.docker.network.bridge.enable_ip_masquerade": "true", + "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", + "com.docker.network.bridge.name": "docker0", + "com.docker.network.driver.mtu": "1500" + }, + "Labels": { + "com.example.some-label": "some-value", + "com.example.some-other-label": "some-other-value" + } +} +``` + +**Example response**: + +``` +HTTP/1.1 201 Created +Content-Type: application/json + +{ + "Id": "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30", + "Warning": "" +} +``` + +Status Codes: + +- **201** - no error +- **404** - plugin not found +- **500** - server error + +JSON Parameters: + +- **Name** - The new network's name. this is a mandatory field +- **CheckDuplicate** - Requests daemon to check for networks with same name +- **Driver** - Name of the network driver plugin to use. Defaults to `bridge` driver +- **Internal** - Restrict external access to the network +- **IPAM** - Optional custom IP scheme for the network +- **EnableIPv6** - Enable IPv6 on the network +- **Options** - Network specific options to be used by the drivers +- **Labels** - Labels to set on the network, specified as a map: `{"key":"value" [,"key2":"value2"]}` + +### Connect a container to a network + +`POST /networks/(id)/connect` + +Connect a container to a network + +**Example request**: + +``` +POST /networks/22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30/connect HTTP/1.1 +Content-Type: application/json + +{ + "Container":"3613f73ba0e4", + "EndpointConfig": { + "IPAMConfig": { + "IPv4Address":"172.24.56.89", + "IPv6Address":"2001:db8::5689" + } + } +} +``` + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** - no error +- **404** - network or container is not found +- **500** - Internal Server Error + +JSON Parameters: + +- **container** - container-id/name to be connected to the network + +### Disconnect a container from a network + +`POST /networks/(id)/disconnect` + +Disconnect a container from a network + +**Example request**: + +``` +POST /networks/22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30/disconnect HTTP/1.1 +Content-Type: application/json + +{ + "Container":"3613f73ba0e4", + "Force":false +} +``` + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** - no error +- **404** - network or container not found +- **500** - Internal Server Error + +JSON Parameters: + +- **Container** - container-id/name to be disconnected from a network +- **Force** - Force the container to disconnect from a network + +### Remove a network + +`DELETE /networks/(id)` + +Instruct the driver to remove the network (`id`). + +**Example request**: + + DELETE /networks/22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes + +- **204** - no error +- **404** - no such network +- **500** - server error + +# 3. Going further + +## 3.1 Inside `docker run` + +As an example, the `docker run` command line makes the following API calls: + +- Create the container + +- If the status code is 404, it means the image doesn't exist: + - Try to pull it. + - Then, retry to create the container. + +- Start the container. + +- If you are not in detached mode: +- Attach to the container, using `logs=1` (to have `stdout` and + `stderr` from the container's start) and `stream=1` + +- If in detached mode or only `stdin` is attached, display the container's id. + +## 3.2 Hijacking + +In this version of the API, `/attach`, uses hijacking to transport `stdin`, +`stdout`, and `stderr` on the same socket. + +To hint potential proxies about connection hijacking, Docker client sends +connection upgrade headers similarly to websocket. + + Upgrade: tcp + Connection: Upgrade + +When Docker daemon detects the `Upgrade` header, it switches its status code +from **200 OK** to **101 UPGRADED** and resends the same headers. + + +## 3.3 CORS Requests + +To set cross origin requests to the remote api please give values to +`--api-cors-header` when running Docker in daemon mode. Set * (asterisk) allows all, +default or blank means CORS disabled + + $ docker daemon -H="192.168.1.9:2375" --api-cors-header="http://foo.bar" diff --git a/docs/reference/api/hub_registry_spec.md b/docs/reference/api/hub_registry_spec.md new file mode 100644 index 00000000..f2517c23 --- /dev/null +++ b/docs/reference/api/hub_registry_spec.md @@ -0,0 +1,18 @@ + + +# The Docker Hub and the Registry v1 + +This API is deprecated as of 1.7. To view the old version, see the [go +here](hub_registry_spec.md) in +the 1.7 documentation. If you want an overview of the current features in +Docker Hub or other image management features see the [image management +overview](../../userguide/eng-image/image_management.md) in the current documentation set. diff --git a/docs/reference/api/images/event_state.gliffy b/docs/reference/api/images/event_state.gliffy new file mode 100644 index 00000000..fcf080da --- /dev/null +++ b/docs/reference/api/images/event_state.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#FFFFFF","width":1193,"height":556,"nodeIndex":370,"autoFit":true,"exportBorder":false,"gridOn":true,"snapToGrid":true,"drawingGuidesOn":true,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"maxWidth":5000,"maxHeight":5000,"themeData":null,"viewportType":"default","fitBB":{"min":{"x":26.46762966848334,"y":100},"max":{"x":1192.861928406027,"y":555.2340187157677}},"printModel":{"pageSize":"Letter","portrait":true,"fitToOnePage":false,"displayPageBreaks":false},"objects":[{"x":373.99998474121094,"y":389.93402099609375,"rotation":0.0,"id":355,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":0,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":191,"py":0.7071067811865475,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":-0.663724900050094,"endArrowRotation":-0.6637248993502937,"interpolationType":"quadratic","cornerRadius":null,"controlPath":[[22.0,-17.0],[94.00000762939453,-17.0],[94.00000762939453,-61.64974974863185],[166.00001525878906,-61.64974974863185]],"lockSegments":{},"ortho":true}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":359,"width":75.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":0.5,"linePerpValue":0.0,"cardinalityType":null,"html":"

docker start

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":275.99998474121094,"y":323.93402099609375,"rotation":0.0,"id":344,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":127,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":335,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":193,"py":0.0,"px":0.7071067811865476}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":105.08369488824782,"endArrowRotation":91.96866662391399,"interpolationType":"quadratic","cornerRadius":null,"controlPath":[[22.531977827253513,30.06597900390625],[22.531977827253513,51.06597900390625],[-52.96697615221987,51.06597900390625],[-52.96697615221987,106.06597900390625]],"lockSegments":{"1":true},"ortho":true}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":347,"width":64.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

docker rm

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":279.99998474121094,"y":249.93402099609375,"rotation":0.0,"id":342,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":126,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":188,"py":0.5,"px":1.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":191,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[-74.99998474121094,0.06597900390625],[297.50001525878906,0.06597900390625],[297.50001525878906,50.06597900390625]],"lockSegments":{},"ortho":true}},"linkMap":[],"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":313.99998474121094,"y":290.93402099609375,"rotation":0.0,"id":341,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":123,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":335,"py":0.5,"px":1.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":191,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[19.531977827253513,28.06597900390625],[88.35546419381131,28.06597900390625],[157.17895056036912,28.06597900390625],[226.00243692692698,28.06597900390625]],"lockSegments":{},"ortho":true}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":353,"width":75.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

docker start

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":214.99998474121094,"y":322.93402099609375,"rotation":0.0,"id":340,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":122,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":228,"py":0.5733505249023437,"px":1.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":335,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[-7.637919363960094,-3.93402099609375],[11.085379699777775,-3.93402099609375],[29.808678763515644,-3.93402099609375],[48.53197782725351,-3.93402099609375]],"lockSegments":{},"ortho":true}},"linkMap":[],"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":83.0,"y":251.0,"rotation":0.0,"id":328,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":116,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":188,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-52.03237033151666,-0.9999999999999716],[47.0,-1.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":332,"width":67.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":0.5233416311379174,"linePerpValue":null,"cardinalityType":null,"html":"

docker run

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":74.0,"y":318.0,"rotation":0.0,"id":327,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":113,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":228,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-42.0,1.0],[58.5,2.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":333,"width":85.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":0.5689443767164591,"linePerpValue":null,"cardinalityType":null,"html":"

docker create

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":191.0,"y":409.0,"rotation":0.0,"id":325,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":112,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":193,"py":0.5,"px":0.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":215,"py":0.5,"px":1.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-21.0,41.0],[-61.0,41.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":331.0,"y":346.0,"rotation":0.0,"id":320,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":109,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":209,"py":0.5,"px":0.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":193,"py":0.5,"px":1.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[2.5319625684644507,49.0],[-41.734018715767775,49.0],[-41.734018715767775,104.0],[-86.0,104.0]],"lockSegments":{},"ortho":true}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":324,"width":64.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

docker rm

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":872.0,"y":503.0,"rotation":0.0,"id":310,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":108,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":205,"py":0.0,"px":0.2928932188134524}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-60.03300858899104,-53.0],[-148.0,-151.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":735.0,"y":341.0,"rotation":0.0,"id":307,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":105,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":203,"py":0.2928932188134525,"px":1.1102230246251563E-16}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[0.0,0.0],[137.5,60.7157287525381]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":309,"width":83.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":0.37922003257116654,"linePerpValue":null,"cardinalityType":null,"html":"

docker pause

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":1023.0,"y":446.0,"rotation":0.0,"id":298,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":102,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":213,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":205,"py":0.5,"px":1.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[39.5,-1.0],[39.5,24.0],[-158.0,24.0]],"lockSegments":{},"ortho":true}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":313,"width":100.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":0.37286693198126664,"linePerpValue":null,"cardinalityType":null,"html":"

 docker unpause

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":904.0,"y":434.0,"rotation":0.0,"id":295,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":101,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":203,"py":0.5,"px":1.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":213,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[43.5,-24.0],[123.5,-24.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":411.0,"y":419.0,"rotation":0.0,"id":291,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":98,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":217,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[7.2659812842321685,51.0],[-14.0,51.0],[-14.0,-3.0]],"lockSegments":{},"ortho":true}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":292,"width":21.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":0.5714437496124175,"linePerpValue":0.0,"cardinalityType":null,"html":"

No

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":415.0,"y":419.0,"rotation":0.0,"id":289,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":95,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":217,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":191,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[53.26598128423217,1.0],[53.26598128423217,-32.5],[162.5,-32.5],[162.5,-79.0]],"lockSegments":{"1":true},"ortho":true}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":290,"width":26.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":0.46753493572435184,"linePerpValue":null,"cardinalityType":null,"html":"

Yes

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":521.0,"y":209.0,"rotation":0.0,"id":287,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":94,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":195,"py":0.5,"px":0.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":209,"py":0.5,"px":1.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[-11.0,-19.0],[-97.23401871576777,-19.0],[-97.23401871576777,186.0],[-117.46803743153555,186.0]],"lockSegments":{"1":true},"ortho":true}},"linkMap":[],"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":988.0,"y":232.0,"rotation":0.0,"id":282,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":93,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":201,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[39.5,18.0],[-150.0,18.0],[-150.0,68.0],[-250.0,68.0]],"lockSegments":{"1":true},"ortho":true}},"linkMap":[],"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":664.0,"y":493.0,"rotation":0.0,"id":276,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":92,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":207,"py":0.5,"px":0.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":236,"py":0.7071067811865475,"px":0.9999999999999998}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[8.5,42.23401871576766],[-20.25,42.23401871576766],[-20.25,-44.7157287525381],[-49.0,-44.7157287525381]],"lockSegments":{},"ortho":true}},"linkMap":[],"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":678.0,"y":344.0,"rotation":0.0,"id":273,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":89,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":236,"py":0.29289321881345237,"px":1.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":91.17113025781374,"endArrowRotation":176.63803454243802,"interpolationType":"quadratic","cornerRadius":null,"controlPath":[[2.0,-4.0],[2.0,87.7157287525381],[-63.0,87.7157287525381]],"lockSegments":{},"ortho":true}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":275,"width":59.0,"height":42.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":0.5,"linePerpValue":0.0,"cardinalityType":null,"html":"

container 

process

exited

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":566.0,"y":431.0,"rotation":0.0,"id":272,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":88,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":236,"py":0.5,"px":0.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":217,"py":0.5,"px":1.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[-26.0,9.0],[-36.867009357883944,9.0],[-36.867009357883944,39.0],[-47.73401871576789,39.0]],"lockSegments":{},"ortho":true}},"linkMap":[],"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":785.0,"y":119.0,"rotation":0.0,"id":270,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":87,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":199,"py":0.5,"px":0.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":209,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[5.0,1.0],[-416.46803743153555,1.0],[-416.46803743153555,241.0]],"lockSegments":{},"ortho":true}},"linkMap":[],"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":829.0,"y":172.0,"rotation":0.0,"id":269,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":86,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":248,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":199,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-1.5,-2.0],[-1.5,-32.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":661.0,"y":189.0,"rotation":0.0,"id":267,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":85,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":195,"py":0.5,"px":1.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[7.0,2.284271247461902],[-76.0,1.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":946.0,"y":319.0,"rotation":0.0,"id":263,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":83,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":197,"py":0.5,"px":1.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":233,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.5,1.0],[81.5,1.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":708.0,"y":286.0,"rotation":0.0,"id":256,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":80,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":211,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":254,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-0.5,-2.0],[-0.5,-76.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":258,"width":64.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":0.3108108108108108,"linePerpValue":null,"cardinalityType":null,"html":"

docker kill

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":710.0,"y":359.0,"rotation":0.0,"id":245,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":68,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":211,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":207,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-2.5,-5.0],[0.0,156.23401871576766]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":247,"width":84.0,"height":28.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

 killed by

out-of-memory

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":761.0,"y":318.0,"rotation":0.0,"id":238,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":65,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":211,"py":0.5,"px":1.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":197,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-18.5,1.0],[111.5,2.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":240,"width":87.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":0.4363456059259962,"linePerpValue":null,"cardinalityType":null,"html":"

docker restart

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":608.0,"y":319.0,"rotation":0.0,"id":232,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":58,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":191,"py":0.5,"px":1.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":211,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[7.0,1.0],[64.5,0.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":333.53196256846445,"y":360.0,"rotation":0.0,"id":209,"width":70.0,"height":70.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.connector","order":33,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ellipse.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#e6b8af","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":1.5555555555555554,"y":0.0,"rotation":0.0,"id":210,"width":66.88888888888889,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

stopped

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":540.0,"y":300.0,"rotation":0.0,"id":191,"width":75.0,"height":40.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","order":6,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":1.5,"y":0.0,"rotation":0.0,"id":192,"width":72.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

start

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":510.0,"y":170.0,"rotation":0.0,"id":195,"width":75.0,"height":40.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","order":12,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":1.5,"y":0.0,"rotation":0.0,"id":196,"width":72.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

kill

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":872.5,"y":300.0,"rotation":0.0,"id":197,"width":75.0,"height":40.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","order":15,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":1.5,"y":0.0,"rotation":0.0,"id":198,"width":72.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

die

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":790.0,"y":100.0,"rotation":0.0,"id":199,"width":75.0,"height":40.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","order":18,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":1.5,"y":0.0,"rotation":0.0,"id":200,"width":72.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

stop

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":790.0,"y":450.0,"rotation":0.0,"id":205,"width":75.0,"height":40.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","order":27,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":1.5,"y":0.0,"rotation":0.0,"id":206,"width":72.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

unpause

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":672.5,"y":515.2340187157677,"rotation":0.0,"id":207,"width":75.0,"height":40.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","order":30,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":1.5,"y":0.0,"rotation":0.0,"id":208,"width":72.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

OOM

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":672.5,"y":284.0,"rotation":0.0,"id":211,"width":70.0,"height":70.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.connector","order":36,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ellipse.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#b6d7a8","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":1.5555555555555556,"y":0.0,"rotation":0.0,"id":212,"width":66.88888888888889,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

running

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":403.5319625684644,"y":420.0,"rotation":0.0,"id":227,"width":130.46803743153555,"height":116.23401871576777,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":54,"lockAspectRatio":false,"lockShape":false,"children":[{"x":-6.765981284232225,"y":76.0,"rotation":45.0,"id":223,"width":80.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":53,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Restart 

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":57.234018715767775,"y":75.0,"rotation":315.0,"id":219,"width":80.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":51,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Policy

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":14.734018715767775,"y":0.0,"rotation":0.0,"id":217,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.decision","order":46,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.diamond.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":218,"width":96.0,"height":28.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Should restart?

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":1027.5,"y":375.0,"rotation":0.0,"id":213,"width":70.0,"height":70.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.connector","order":39,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ellipse.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#fce5cd","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":1.5555555555555556,"y":0.0,"rotation":0.0,"id":214,"width":66.88888888888889,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

paused

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":872.5,"y":390.0,"rotation":0.0,"id":203,"width":75.0,"height":40.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","order":24,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":1.5,"y":0.0,"rotation":0.0,"id":204,"width":72.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

pause

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":540.0,"y":420.0,"rotation":0.0,"id":236,"width":75.0,"height":40.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","order":62,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":1.5,"y":0.0,"rotation":0.0,"id":237,"width":72.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

die

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":790.0,"y":170.0,"rotation":0.0,"id":248,"width":75.0,"height":40.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","order":71,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":1.5,"y":0.0,"rotation":0.0,"id":249,"width":72.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

die

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":670.0,"y":170.0,"rotation":0.0,"id":254,"width":75.0,"height":40.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","order":77,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":1.5,"y":0.0,"rotation":0.0,"id":255,"width":72.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

die

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":740.0,"y":323.0,"rotation":0.0,"id":250,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":74,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":248,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-10.0,-33.0],[87.5,-113.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":253,"width":73.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

docker stop

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":1027.5,"y":300.0,"rotation":0.0,"id":233,"width":75.0,"height":40.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","order":59,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":1.5,"y":0.0,"rotation":0.0,"id":234,"width":72.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

start

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":1027.5,"y":230.0,"rotation":0.0,"id":201,"width":75.0,"height":40.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","order":21,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":1.5,"y":0.0,"rotation":0.0,"id":202,"width":72.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

restart

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":1066.5,"y":298.0,"rotation":0.0,"id":264,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":84,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":233,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":201,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-1.5,2.0],[-1.5,-28.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":132.5,"y":300.0,"rotation":0.0,"id":228,"width":75.0,"height":40.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","order":55,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":1.5,"y":0.0,"rotation":0.0,"id":229,"width":72.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

create

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":130.0,"y":230.0,"rotation":0.0,"id":188,"width":75.0,"height":40.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","order":3,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":1.5,"y":0.0,"rotation":0.0,"id":190,"width":72.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

create

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":263.53196256846445,"y":284.0,"rotation":0.0,"id":335,"width":70.0,"height":70.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.connector","order":119,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ellipse.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#a4c2f4","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":1.5555555555555554,"y":0.0,"rotation":0.0,"id":336,"width":66.88888888888889,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

created

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":60.0,"y":415.0,"rotation":0.0,"id":215,"width":70.0,"height":70.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.connector","order":42,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ellipse.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#b7b7b7","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":1.5555555555555556,"y":0.0,"rotation":0.0,"id":216,"width":66.88888888888889,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

deleted

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":170.0,"y":430.0,"rotation":0.0,"id":193,"width":75.0,"height":40.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","order":9,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":1.5,"y":0.0,"rotation":0.0,"id":194,"width":72.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

destroy

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":1133.0,"y":570.0,"rotation":0.0,"id":362,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":130,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":213,"py":0.5,"px":1.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":0.9595103354441726,"endArrowRotation":177.33110321368451,"interpolationType":"quadratic","cornerRadius":null,"controlPath":[[-55.0,-192.0],[-3.5,-192.0],[-3.5,-160.0],[-35.5,-160.0]],"lockSegments":{"1":true},"ortho":true}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":363,"width":87.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":0.5835366104291947,"linePerpValue":-20.0,"cardinalityType":null,"html":"

docker update

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":281.0,"y":596.0,"rotation":0.0,"id":364,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":133,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":335,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":-88.08561222234982,"endArrowRotation":85.23919045962671,"interpolationType":"quadratic","cornerRadius":null,"controlPath":[[-7.0,-301.0],[-7.0,-334.0],[17.53196256846445,-334.0],[17.53196256846445,-312.0]],"lockSegments":{"1":true},"ortho":true}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":365,"width":87.0,"height":14.0,"uid":null,"order":135,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":0.524371533874117,"linePerpValue":0.0,"cardinalityType":null,"html":"

docker update

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":305.0,"y":604.0,"rotation":0.0,"id":366,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":136,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":209,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":92.55340974719384,"endArrowRotation":-91.2277874986563,"interpolationType":"quadratic","cornerRadius":null,"controlPath":[[63.53196256846445,-174.0],[63.53196256846445,-144.0],[37.0,-144.0],[37.0,-186.0]],"lockSegments":{"1":true},"ortho":true}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":367,"width":87.0,"height":14.0,"uid":null,"order":138,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":0.5749848592663713,"linePerpValue":-20.0,"cardinalityType":null,"html":"

docker update

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"},{"x":516.0,"y":570.0,"rotation":0.0,"id":368,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":139,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":1,"startArrowRotation":183.34296440226473,"endArrowRotation":-0.7310374013608921,"interpolationType":"quadratic","cornerRadius":null,"controlPath":[[158.0,-263.0],[134.0,-263.0],[134.0,-284.0],[182.0,-284.0]],"lockSegments":{"1":true},"ortho":true}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":369,"width":87.0,"height":14.0,"uid":null,"order":141,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":0.4230219192816351,"linePerpValue":-20.0,"cardinalityType":null,"html":"

docker update

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"gmMmie3VnJbh"}],"hidden":false,"layerId":"gmMmie3VnJbh"}],"layers":[{"guid":"gmMmie3VnJbh","order":0,"name":"Layer 0","active":true,"locked":false,"visible":true,"nodeIndex":142}],"shapeStyles":{"com.gliffy.shape.uml.uml_v2.state_machine":{"fill":"#e2e2e2","stroke":"#000000","strokeWidth":2},"com.gliffy.shape.flowchart.flowchart_v1.default":{"fill":"#a4c2f4","stroke":"#333333","strokeWidth":2}},"lineStyles":{"global":{"endArrow":1,"orthoMode":2}},"textStyles":{"global":{"color":"#000000"}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":["com.gliffy.libraries.flowchart.flowchart_v1.default"],"autosaveDisabled":false,"lastSerialized":1451304727693,"analyticsProduct":"Online"},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/docs/reference/api/images/event_state.png b/docs/reference/api/images/event_state.png new file mode 100644 index 0000000000000000000000000000000000000000..28d09ba192d05bf5b55dbf4c85a879842a7f4c46 GIT binary patch literal 78354 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 literal 0 HcmV?d00001 diff --git a/docs/reference/api/index.md b/docs/reference/api/index.md new file mode 100644 index 00000000..05f3d126 --- /dev/null +++ b/docs/reference/api/index.md @@ -0,0 +1,16 @@ + + + +# API Reference + +* [Docker Remote API](docker_remote_api.md) +* [Docker Remote API client libraries](remote_api_client_libraries.md) diff --git a/docs/reference/api/remote_api_client_libraries.md b/docs/reference/api/remote_api_client_libraries.md new file mode 100644 index 00000000..fd4b3e39 --- /dev/null +++ b/docs/reference/api/remote_api_client_libraries.md @@ -0,0 +1,248 @@ + + +# Docker Remote API client libraries + +These libraries have not been tested by the Docker maintainers for +compatibility. Please file issues with the library owners. If you find +more library implementations, please list them in Docker doc bugs and we +will add the libraries here. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Language/FrameworkNameRepositoryStatus
C#Docker.DotNethttps://github.com/ahmetalpbalkan/Docker.DotNetActive
C++lasote/docker_clienthttp://www.biicode.com/lasote/docker_client (Biicode C++ dependency manager)Active
Erlangerldockerhttps://github.com/proger/erldockerActive
Dartbwu_dockerhttps://github.com/bwu-dart/bwu_dockerActive
Goengine-apihttps://github.com/docker/engine-apiActive
Gogo-dockerclienthttps://github.com/fsouza/go-dockerclientActive
Godockerclienthttps://github.com/samalba/dockerclientActive
Gradlegradle-docker-pluginhttps://github.com/gesellix/gradle-docker-pluginActive
Groovydocker-clienthttps://github.com/gesellix/docker-clientActive
Haskelldocker-hshttps://github.com/denibertovic/docker-hsActive
HTML (Web Components)docker-elementshttps://github.com/kapalhq/docker-elementsActive
Javadocker-javahttps://github.com/docker-java/docker-javaActive
Javadocker-clienthttps://github.com/spotify/docker-clientActive
Javajclouds-dockerhttps://github.com/jclouds/jclouds-labs/tree/master/dockerActive
Javarx-docker-clienthttps://github.com/shekhargulati/rx-docker-clientActive
JavaScript (NodeJS)dockerizerhttps://github.com/kesarion/dockerizerActive
JavaScript (NodeJS)dockerodehttps://github.com/apocas/dockerode + Install via NPM: npm install dockerodeActive
JavaScript (NodeJS)docker.iohttps://github.com/appersonlabs/docker.io + Install via NPM: npm install docker.ioActive
JavaScriptdocker-jshttps://github.com/dgoujard/docker-jsOutdated
JavaScript (Angular) WebUIAlbatroshttps://github.com/dcylabs/albatrosActive
JavaScript (Angular) WebUIdocker-cphttps://github.com/13W/docker-cpActive
JavaScript (Angular) WebUIdockeruihttps://github.com/crosbymichael/dockeruiActive
JavaScript (Angular) WebUIdockeryhttps://github.com/lexandro/dockeryActive
PerlNet::Dockerhttps://metacpan.org/pod/Net::DockerActive
PerlEixo::Dockerhttps://github.com/alambike/eixo-dockerActive
PHPAlvinehttp://pear.alvine.io/ (alpha)Active
PHPDocker-PHPhttps://github.com/docker-php/docker-phpActive
PHPDocker-PHP-Clienthttps://github.com/jarkt/docker-php-clientActive
Pythondocker-pyhttps://github.com/docker/docker-pyActive
Rubydocker-apihttps://github.com/swipely/docker-apiActive
Rubydocker-clienthttps://github.com/geku/docker-clientOutdated
Rustdocker-rusthttps://github.com/abh1nav/docker-rustActive
Rustshiplifthttps://github.com/softprops/shipliftActive
Scalatugboathttps://github.com/softprops/tugboatActive
Scalareactive-dockerhttps://github.com/almoehi/reactive-dockerActive
diff --git a/docs/reference/builder.md b/docs/reference/builder.md new file mode 100644 index 00000000..a932276b --- /dev/null +++ b/docs/reference/builder.md @@ -0,0 +1,1344 @@ + + +# Dockerfile reference + +Docker can build images automatically by reading the instructions from a +`Dockerfile`. A `Dockerfile` is a text document that contains all the commands a +user could call on the command line to assemble an image. Using `docker build` +users can create an automated build that executes several command-line +instructions in succession. + +This page describes the commands you can use in a `Dockerfile`. When you are +done reading this page, refer to the [`Dockerfile` Best +Practices](../userguide/eng-image/dockerfile_best-practices.md) for a tip-oriented guide. + +## Usage + +The [`docker build`](commandline/build.md) command builds an image from +a `Dockerfile` and a *context*. The build's context is the files at a specified +location `PATH` or `URL`. The `PATH` is a directory on your local filesystem. +The `URL` is a the location of a Git repository. + +A context is processed recursively. So, a `PATH` includes any subdirectories and +the `URL` includes the repository and its submodules. A simple build command +that uses the current directory as context: + + $ docker build . + Sending build context to Docker daemon 6.51 MB + ... + +The build is run by the Docker daemon, not by the CLI. The first thing a build +process does is send the entire context (recursively) to the daemon. In most +cases, it's best to start with an empty directory as context and keep your +Dockerfile in that directory. Add only the files needed for building the +Dockerfile. + +>**Warning**: Do not use your root directory, `/`, as the `PATH` as it causes +>the build to transfer the entire contents of your hard drive to the Docker +>daemon. + +To use a file in the build context, the `Dockerfile` refers to the file specified +in an instruction, for example, a `COPY` instruction. To increase the build's +performance, exclude files and directories by adding a `.dockerignore` file to +the context directory. For information about how to [create a `.dockerignore` +file](#dockerignore-file) see the documentation on this page. + +Traditionally, the `Dockerfile` is called `Dockerfile` and located in the root +of the context. You use the `-f` flag with `docker build` to point to a Dockerfile +anywhere in your file system. + + $ docker build -f /path/to/a/Dockerfile . + +You can specify a repository and tag at which to save the new image if +the build succeeds: + + $ docker build -t shykes/myapp . + +To tag the image into multiple repositories after the build, +add multiple `-t` parameters when you run the `build` command: + + $ docker build -t shykes/myapp:1.0.2 -t shykes/myapp:latest . + +The Docker daemon runs the instructions in the `Dockerfile` one-by-one, +committing the result of each instruction +to a new image if necessary, before finally outputting the ID of your +new image. The Docker daemon will automatically clean up the context you +sent. + +Note that each instruction is run independently, and causes a new image +to be created - so `RUN cd /tmp` will not have any effect on the next +instructions. + +Whenever possible, Docker will re-use the intermediate images (cache), +to accelerate the `docker build` process significantly. This is indicated by +the `Using cache` message in the console output. +(For more information, see the [Build cache section](../userguide/eng-image/dockerfile_best-practices.md#build-cache)) in the +`Dockerfile` best practices guide: + + $ docker build -t svendowideit/ambassador . + Sending build context to Docker daemon 15.36 kB + Step 0 : FROM alpine:3.2 + ---> 31f630c65071 + Step 1 : MAINTAINER SvenDowideit@home.org.au + ---> Using cache + ---> 2a1c91448f5f + Step 2 : RUN apk update && apk add socat && rm -r /var/cache/ + ---> Using cache + ---> 21ed6e7fbb73 + Step 3 : CMD env | grep _TCP= | (sed 's/.*_PORT_\([0-9]*\)_TCP=tcp:\/\/\(.*\):\(.*\)/socat -t 100000000 TCP4-LISTEN:\1,fork,reuseaddr TCP4:\2:\3 \&/' && echo wait) | sh + ---> Using cache + ---> 7ea8aef582cc + Successfully built 7ea8aef582cc + +When you're done with your build, you're ready to look into [*Pushing a +repository to its registry*](../userguide/containers/dockerrepos.md#contributing-to-docker-hub). + +## Format + +Here is the format of the `Dockerfile`: + + # Comment + INSTRUCTION arguments + +The instruction is not case-sensitive, however convention is for them to +be UPPERCASE in order to distinguish them from arguments more easily. + +Docker runs the instructions in a `Dockerfile` in order. **The +first instruction must be \`FROM\`** in order to specify the [*Base +Image*](glossary.md#base-image) from which you are building. + +Docker will treat lines that *begin* with `#` as a +comment. A `#` marker anywhere else in the line will +be treated as an argument. This allows statements like: + + # Comment + RUN echo 'we are running some # of cool things' + +Here is the set of instructions you can use in a `Dockerfile` for building +images. + +### Environment replacement + +Environment variables (declared with [the `ENV` statement](#env)) can also be +used in certain instructions as variables to be interpreted by the +`Dockerfile`. Escapes are also handled for including variable-like syntax +into a statement literally. + +Environment variables are notated in the `Dockerfile` either with +`$variable_name` or `${variable_name}`. They are treated equivalently and the +brace syntax is typically used to address issues with variable names with no +whitespace, like `${foo}_bar`. + +The `${variable_name}` syntax also supports a few of the standard `bash` +modifiers as specified below: + +* `${variable:-word}` indicates that if `variable` is set then the result + will be that value. If `variable` is not set then `word` will be the result. +* `${variable:+word}` indicates that if `variable` is set then `word` will be + the result, otherwise the result is the empty string. + +In all cases, `word` can be any string, including additional environment +variables. + +Escaping is possible by adding a `\` before the variable: `\$foo` or `\${foo}`, +for example, will translate to `$foo` and `${foo}` literals respectively. + +Example (parsed representation is displayed after the `#`): + + FROM busybox + ENV foo /bar + WORKDIR ${foo} # WORKDIR /bar + ADD . $foo # ADD . /bar + COPY \$foo /quux # COPY $foo /quux + +Environment variables are supported by the following list of instructions in +the `Dockerfile`: + +* `ADD` +* `COPY` +* `ENV` +* `EXPOSE` +* `LABEL` +* `USER` +* `WORKDIR` +* `VOLUME` +* `STOPSIGNAL` + +as well as: + +* `ONBUILD` (when combined with one of the supported instructions above) + +> **Note**: +> prior to 1.4, `ONBUILD` instructions did **NOT** support environment +> variable, even when combined with any of the instructions listed above. + +Environment variable substitution will use the same value for each variable +throughout the entire command. In other words, in this example: + + ENV abc=hello + ENV abc=bye def=$abc + ENV ghi=$abc + +will result in `def` having a value of `hello`, not `bye`. However, +`ghi` will have a value of `bye` because it is not part of the same command +that set `abc` to `bye`. + +### .dockerignore file + +Before the docker CLI sends the context to the docker daemon, it looks +for a file named `.dockerignore` in the root directory of the context. +If this file exists, the CLI modifies the context to exclude files and +directories that match patterns in it. This helps to avoid +unnecessarily sending large or sensitive files and directories to the +daemon and potentially adding them to images using `ADD` or `COPY`. + +The CLI interprets the `.dockerignore` file as a newline-separated +list of patterns similar to the file globs of Unix shells. For the +purposes of matching, the root of the context is considered to be both +the working and the root directory. For example, the patterns +`/foo/bar` and `foo/bar` both exclude a file or directory named `bar` +in the `foo` subdirectory of `PATH` or in the root of the git +repository located at `URL`. Neither excludes anything else. + +Here is an example `.dockerignore` file: + +``` + */temp* + */*/temp* + temp? +``` + +This file causes the following build behavior: + +| Rule | Behavior | +|----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `*/temp*` | Exclude files and directories whose names start with `temp` in any immediate subdirectory of the root. For example, the plain file `/somedir/temporary.txt` is excluded, as is the directory `/somedir/temp`. | +| `*/*/temp*` | Exclude files and directories starting with `temp` from any subdirectory that is two levels below the root. For example, `/somedir/subdir/temporary.txt` is excluded. | +| `temp?` | Exclude files and directories in the root directory whose names are a one-character extension of `temp`. For example, `/tempa` and `/tempb` are excluded. + + +Matching is done using Go's +[filepath.Match](http://golang.org/pkg/path/filepath#Match) rules. A +preprocessing step removes leading and trailing whitespace and +eliminates `.` and `..` elements using Go's +[filepath.Clean](http://golang.org/pkg/path/filepath/#Clean). Lines +that are blank after preprocessing are ignored. + +Beyond Go's filepath.Match rules, Docker also supports a special +wildcard string `**` that matches any number of directories (including +zero). For example, `**/*.go` will exclude all files that end with `.go` +that are found in all directories, including the root of the build context. + +Lines starting with `!` (exclamation mark) can be used to make exceptions +to exclusions. The following is an example `.dockerignore` file that +uses this mechanism: + +``` + *.md + !README.md +``` + +All markdown files *except* `README.md` are excluded from the context. + +The placement of `!` exception rules influences the behavior: the last +line of the `.dockerignore` that matches a particular file determines +whether it is included or excluded. Consider the following example: + +``` + *.md + !README*.md + README-secret.md +``` + +No markdown files are included in the context except README files other than +`README-secret.md`. + +Now consider this example: + +``` + *.md + README-secret.md + !README*.md +``` + +All of the README files are included. The middle line has no effect because +`!README*.md` matches `README-secret.md` and comes last. + +You can even use the `.dockerignore` file to exclude the `Dockerfile` +and `.dockerignore` files. These files are still sent to the daemon +because it needs them to do its job. But the `ADD` and `COPY` commands +do not copy them to the image. + +Finally, you may want to specify which files to include in the +context, rather than which to exclude. To achieve this, specify `*` as +the first pattern, followed by one or more `!` exception patterns. + +**Note**: For historical reasons, the pattern `.` is ignored. + +## FROM + + FROM + +Or + + FROM : + +Or + + FROM @ + +The `FROM` instruction sets the [*Base Image*](glossary.md#base-image) +for subsequent instructions. As such, a valid `Dockerfile` must have `FROM` as +its first instruction. The image can be any valid image – it is especially easy +to start by **pulling an image** from the [*Public Repositories*](../userguide/containers/dockerrepos.md). + +- `FROM` must be the first non-comment instruction in the `Dockerfile`. + +- `FROM` can appear multiple times within a single `Dockerfile` in order to create +multiple images. Simply make a note of the last image ID output by the commit +before each new `FROM` command. + +- The `tag` or `digest` values are optional. If you omit either of them, the builder +assumes a `latest` by default. The builder returns an error if it cannot match +the `tag` value. + +## MAINTAINER + + MAINTAINER + +The `MAINTAINER` instruction allows you to set the *Author* field of the +generated images. + +## RUN + +RUN has 2 forms: + +- `RUN ` (*shell* form, the command is run in a shell - `/bin/sh -c`) +- `RUN ["executable", "param1", "param2"]` (*exec* form) + +The `RUN` instruction will execute any commands in a new layer on top of the +current image and commit the results. The resulting committed image will be +used for the next step in the `Dockerfile`. + +Layering `RUN` instructions and generating commits conforms to the core +concepts of Docker where commits are cheap and containers can be created from +any point in an image's history, much like source control. + +The *exec* form makes it possible to avoid shell string munging, and to `RUN` +commands using a base image that does not contain `/bin/sh`. + +In the *shell* form you can use a `\` (backslash) to continue a single +RUN instruction onto the next line. For example, consider these two lines: +``` +RUN /bin/bash -c 'source $HOME/.bashrc ;\ +echo $HOME' +``` +Together they are equivalent to this single line: +``` +RUN /bin/bash -c 'source $HOME/.bashrc ; echo $HOME' +``` + +> **Note**: +> To use a different shell, other than '/bin/sh', use the *exec* form +> passing in the desired shell. For example, +> `RUN ["/bin/bash", "-c", "echo hello"]` + +> **Note**: +> The *exec* form is parsed as a JSON array, which means that +> you must use double-quotes (") around words not single-quotes ('). + +> **Note**: +> Unlike the *shell* form, the *exec* form does not invoke a command shell. +> This means that normal shell processing does not happen. For example, +> `RUN [ "echo", "$HOME" ]` will not do variable substitution on `$HOME`. +> If you want shell processing then either use the *shell* form or execute +> a shell directly, for example: `RUN [ "sh", "-c", "echo", "$HOME" ]`. + +The cache for `RUN` instructions isn't invalidated automatically during +the next build. The cache for an instruction like +`RUN apt-get dist-upgrade -y` will be reused during the next build. The +cache for `RUN` instructions can be invalidated by using the `--no-cache` +flag, for example `docker build --no-cache`. + +See the [`Dockerfile` Best Practices +guide](../userguide/eng-image/dockerfile_best-practices.md#build-cache) for more information. + +The cache for `RUN` instructions can be invalidated by `ADD` instructions. See +[below](#add) for details. + +### Known issues (RUN) + +- [Issue 783](https://github.com/docker/docker/issues/783) is about file + permissions problems that can occur when using the AUFS file system. You + might notice it during an attempt to `rm` a file, for example. + + For systems that have recent aufs version (i.e., `dirperm1` mount option can + be set), docker will attempt to fix the issue automatically by mounting + the layers with `dirperm1` option. More details on `dirperm1` option can be + found at [`aufs` man page](http://aufs.sourceforge.net/aufs3/man.html) + + If your system doesn't have support for `dirperm1`, the issue describes a workaround. + +## CMD + +The `CMD` instruction has three forms: + +- `CMD ["executable","param1","param2"]` (*exec* form, this is the preferred form) +- `CMD ["param1","param2"]` (as *default parameters to ENTRYPOINT*) +- `CMD command param1 param2` (*shell* form) + +There can only be one `CMD` instruction in a `Dockerfile`. If you list more than one `CMD` +then only the last `CMD` will take effect. + +**The main purpose of a `CMD` is to provide defaults for an executing +container.** These defaults can include an executable, or they can omit +the executable, in which case you must specify an `ENTRYPOINT` +instruction as well. + +> **Note**: +> If `CMD` is used to provide default arguments for the `ENTRYPOINT` +> instruction, both the `CMD` and `ENTRYPOINT` instructions should be specified +> with the JSON array format. + +> **Note**: +> The *exec* form is parsed as a JSON array, which means that +> you must use double-quotes (") around words not single-quotes ('). + +> **Note**: +> Unlike the *shell* form, the *exec* form does not invoke a command shell. +> This means that normal shell processing does not happen. For example, +> `CMD [ "echo", "$HOME" ]` will not do variable substitution on `$HOME`. +> If you want shell processing then either use the *shell* form or execute +> a shell directly, for example: `CMD [ "sh", "-c", "echo", "$HOME" ]`. + +When used in the shell or exec formats, the `CMD` instruction sets the command +to be executed when running the image. + +If you use the *shell* form of the `CMD`, then the `` will execute in +`/bin/sh -c`: + + FROM ubuntu + CMD echo "This is a test." | wc - + +If you want to **run your** `` **without a shell** then you must +express the command as a JSON array and give the full path to the executable. +**This array form is the preferred format of `CMD`.** Any additional parameters +must be individually expressed as strings in the array: + + FROM ubuntu + CMD ["/usr/bin/wc","--help"] + +If you would like your container to run the same executable every time, then +you should consider using `ENTRYPOINT` in combination with `CMD`. See +[*ENTRYPOINT*](#entrypoint). + +If the user specifies arguments to `docker run` then they will override the +default specified in `CMD`. + +> **Note**: +> don't confuse `RUN` with `CMD`. `RUN` actually runs a command and commits +> the result; `CMD` does not execute anything at build time, but specifies +> the intended command for the image. + +## LABEL + + LABEL = = = ... + +The `LABEL` instruction adds metadata to an image. A `LABEL` is a +key-value pair. To include spaces within a `LABEL` value, use quotes and +backslashes as you would in command-line parsing. A few usage examples: + + LABEL "com.example.vendor"="ACME Incorporated" + LABEL com.example.label-with-value="foo" + LABEL version="1.0" + LABEL description="This text illustrates \ + that label-values can span multiple lines." + +An image can have more than one label. To specify multiple labels, +Docker recommends combining labels into a single `LABEL` instruction where +possible. Each `LABEL` instruction produces a new layer which can result in an +inefficient image if you use many labels. This example results in a single image +layer. + + LABEL multi.label1="value1" multi.label2="value2" other="value3" + +The above can also be written as: + + LABEL multi.label1="value1" \ + multi.label2="value2" \ + other="value3" + +Labels are additive including `LABEL`s in `FROM` images. If Docker +encounters a label/key that already exists, the new value overrides any previous +labels with identical keys. + +To view an image's labels, use the `docker inspect` command. + + "Labels": { + "com.example.vendor": "ACME Incorporated" + "com.example.label-with-value": "foo", + "version": "1.0", + "description": "This text illustrates that label-values can span multiple lines.", + "multi.label1": "value1", + "multi.label2": "value2", + "other": "value3" + }, + +## EXPOSE + + EXPOSE [...] + +The `EXPOSE` instruction informs Docker that the container listens on the +specified network ports at runtime. `EXPOSE` does not make the ports of the +container accessible to the host. To do that, you must use either the `-p` flag +to publish a range of ports or the `-P` flag to publish all of the exposed +ports. You can expose one port number and publish it externally under another +number. + +To set up port redirection on the host system, see [using the -P +flag](run.md#expose-incoming-ports). The Docker network feature supports +creating networks without the need to expose ports within the network, for +detailed information see the [overview of this +feature](../userguide/networking/index.md)). + +## ENV + + ENV + ENV = ... + +The `ENV` instruction sets the environment variable `` to the value +``. This value will be in the environment of all "descendant" +`Dockerfile` commands and can be [replaced inline](#environment-replacement) in +many as well. + +The `ENV` instruction has two forms. The first form, `ENV `, +will set a single variable to a value. The entire string after the first +space will be treated as the `` - including characters such as +spaces and quotes. + +The second form, `ENV = ...`, allows for multiple variables to +be set at one time. Notice that the second form uses the equals sign (=) +in the syntax, while the first form does not. Like command line parsing, +quotes and backslashes can be used to include spaces within values. + +For example: + + ENV myName="John Doe" myDog=Rex\ The\ Dog \ + myCat=fluffy + +and + + ENV myName John Doe + ENV myDog Rex The Dog + ENV myCat fluffy + +will yield the same net results in the final container, but the first form +is preferred because it produces a single cache layer. + +The environment variables set using `ENV` will persist when a container is run +from the resulting image. You can view the values using `docker inspect`, and +change them using `docker run --env =`. + +> **Note**: +> Environment persistence can cause unexpected side effects. For example, +> setting `ENV DEBIAN_FRONTEND noninteractive` may confuse apt-get +> users on a Debian-based image. To set a value for a single command, use +> `RUN = `. + +## ADD + +ADD has two forms: + +- `ADD ... ` +- `ADD ["",... ""]` (this form is required for paths containing +whitespace) + +The `ADD` instruction copies new files, directories or remote file URLs from `` +and adds them to the filesystem of the container at the path ``. + +Multiple `` resource may be specified but if they are files or +directories then they must be relative to the source directory that is +being built (the context of the build). + +Each `` may contain wildcards and matching will be done using Go's +[filepath.Match](http://golang.org/pkg/path/filepath#Match) rules. For example: + + ADD hom* /mydir/ # adds all files starting with "hom" + ADD hom?.txt /mydir/ # ? is replaced with any single character, e.g., "home.txt" + +The `` is an absolute path, or a path relative to `WORKDIR`, into which +the source will be copied inside the destination container. + + ADD test relativeDir/ # adds "test" to `WORKDIR`/relativeDir/ + ADD test /absoluteDir/ # adds "test" to /absoluteDir/ + +All new files and directories are created with a UID and GID of 0. + +In the case where `` is a remote file URL, the destination will +have permissions of 600. If the remote file being retrieved has an HTTP +`Last-Modified` header, the timestamp from that header will be used +to set the `mtime` on the destination file. However, like any other file +processed during an `ADD`, `mtime` will not be included in the determination +of whether or not the file has changed and the cache should be updated. + +> **Note**: +> If you build by passing a `Dockerfile` through STDIN (`docker +> build - < somefile`), there is no build context, so the `Dockerfile` +> can only contain a URL based `ADD` instruction. You can also pass a +> compressed archive through STDIN: (`docker build - < archive.tar.gz`), +> the `Dockerfile` at the root of the archive and the rest of the +> archive will get used at the context of the build. + +> **Note**: +> If your URL files are protected using authentication, you +> will need to use `RUN wget`, `RUN curl` or use another tool from +> within the container as the `ADD` instruction does not support +> authentication. + +> **Note**: +> The first encountered `ADD` instruction will invalidate the cache for all +> following instructions from the Dockerfile if the contents of `` have +> changed. This includes invalidating the cache for `RUN` instructions. +> See the [`Dockerfile` Best Practices +guide](../userguide/eng-image/dockerfile_best-practices.md#build-cache) for more information. + + +`ADD` obeys the following rules: + +- The `` path must be inside the *context* of the build; + you cannot `ADD ../something /something`, because the first step of a + `docker build` is to send the context directory (and subdirectories) to the + docker daemon. + +- If `` is a URL and `` does not end with a trailing slash, then a + file is downloaded from the URL and copied to ``. + +- If `` is a URL and `` does end with a trailing slash, then the + filename is inferred from the URL and the file is downloaded to + `/`. For instance, `ADD http://example.com/foobar /` would + create the file `/foobar`. The URL must have a nontrivial path so that an + appropriate filename can be discovered in this case (`http://example.com` + will not work). + +- If `` is a directory, the entire contents of the directory are copied, + including filesystem metadata. + +> **Note**: +> The directory itself is not copied, just its contents. + +- If `` is a *local* tar archive in a recognized compression format + (identity, gzip, bzip2 or xz) then it is unpacked as a directory. Resources + from *remote* URLs are **not** decompressed. When a directory is copied or + unpacked, it has the same behavior as `tar -x`: the result is the union of: + + 1. Whatever existed at the destination path and + 2. The contents of the source tree, with conflicts resolved in favor + of "2." on a file-by-file basis. + + > **Note**: + > Whether a file is identified as a recognized compression format or not + > is done solely based on the contents of the file, not the name of the file. + > For example, if an empty file happens to end with `.tar.gz` this will not + > be recognized as a compressed file and **will not** generate any kind of + > decompression error message, rather the file will simply be copied to the + > destination. + +- If `` is any other kind of file, it is copied individually along with + its metadata. In this case, if `` ends with a trailing slash `/`, it + will be considered a directory and the contents of `` will be written + at `/base()`. + +- If multiple `` resources are specified, either directly or due to the + use of a wildcard, then `` must be a directory, and it must end with + a slash `/`. + +- If `` does not end with a trailing slash, it will be considered a + regular file and the contents of `` will be written at ``. + +- If `` doesn't exist, it is created along with all missing directories + in its path. + +## COPY + +COPY has two forms: + +- `COPY ... ` +- `COPY ["",... ""]` (this form is required for paths containing +whitespace) + +The `COPY` instruction copies new files or directories from `` +and adds them to the filesystem of the container at the path ``. + +Multiple `` resource may be specified but they must be relative +to the source directory that is being built (the context of the build). + +Each `` may contain wildcards and matching will be done using Go's +[filepath.Match](http://golang.org/pkg/path/filepath#Match) rules. For example: + + COPY hom* /mydir/ # adds all files starting with "hom" + COPY hom?.txt /mydir/ # ? is replaced with any single character, e.g., "home.txt" + +The `` is an absolute path, or a path relative to `WORKDIR`, into which +the source will be copied inside the destination container. + + COPY test relativeDir/ # adds "test" to `WORKDIR`/relativeDir/ + COPY test /absoluteDir/ # adds "test" to /absoluteDir/ + +All new files and directories are created with a UID and GID of 0. + +> **Note**: +> If you build using STDIN (`docker build - < somefile`), there is no +> build context, so `COPY` can't be used. + +`COPY` obeys the following rules: + +- The `` path must be inside the *context* of the build; + you cannot `COPY ../something /something`, because the first step of a + `docker build` is to send the context directory (and subdirectories) to the + docker daemon. + +- If `` is a directory, the entire contents of the directory are copied, + including filesystem metadata. + +> **Note**: +> The directory itself is not copied, just its contents. + +- If `` is any other kind of file, it is copied individually along with + its metadata. In this case, if `` ends with a trailing slash `/`, it + will be considered a directory and the contents of `` will be written + at `/base()`. + +- If multiple `` resources are specified, either directly or due to the + use of a wildcard, then `` must be a directory, and it must end with + a slash `/`. + +- If `` does not end with a trailing slash, it will be considered a + regular file and the contents of `` will be written at ``. + +- If `` doesn't exist, it is created along with all missing directories + in its path. + +## ENTRYPOINT + +ENTRYPOINT has two forms: + +- `ENTRYPOINT ["executable", "param1", "param2"]` + (*exec* form, preferred) +- `ENTRYPOINT command param1 param2` + (*shell* form) + +An `ENTRYPOINT` allows you to configure a container that will run as an executable. + +For example, the following will start nginx with its default content, listening +on port 80: + + docker run -i -t --rm -p 80:80 nginx + +Command line arguments to `docker run ` will be appended after all +elements in an *exec* form `ENTRYPOINT`, and will override all elements specified +using `CMD`. +This allows arguments to be passed to the entry point, i.e., `docker run -d` +will pass the `-d` argument to the entry point. +You can override the `ENTRYPOINT` instruction using the `docker run --entrypoint` +flag. + +The *shell* form prevents any `CMD` or `run` command line arguments from being +used, but has the disadvantage that your `ENTRYPOINT` will be started as a +subcommand of `/bin/sh -c`, which does not pass signals. +This means that the executable will not be the container's `PID 1` - and +will _not_ receive Unix signals - so your executable will not receive a +`SIGTERM` from `docker stop `. + +Only the last `ENTRYPOINT` instruction in the `Dockerfile` will have an effect. + +### Exec form ENTRYPOINT example + +You can use the *exec* form of `ENTRYPOINT` to set fairly stable default commands +and arguments and then use either form of `CMD` to set additional defaults that +are more likely to be changed. + + FROM ubuntu + ENTRYPOINT ["top", "-b"] + CMD ["-c"] + +When you run the container, you can see that `top` is the only process: + + $ docker run -it --rm --name test top -H + top - 08:25:00 up 7:27, 0 users, load average: 0.00, 0.01, 0.05 + Threads: 1 total, 1 running, 0 sleeping, 0 stopped, 0 zombie + %Cpu(s): 0.1 us, 0.1 sy, 0.0 ni, 99.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st + KiB Mem: 2056668 total, 1616832 used, 439836 free, 99352 buffers + KiB Swap: 1441840 total, 0 used, 1441840 free. 1324440 cached Mem + + PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND + 1 root 20 0 19744 2336 2080 R 0.0 0.1 0:00.04 top + +To examine the result further, you can use `docker exec`: + + $ docker exec -it test ps aux + USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND + root 1 2.6 0.1 19752 2352 ? Ss+ 08:24 0:00 top -b -H + root 7 0.0 0.1 15572 2164 ? R+ 08:25 0:00 ps aux + +And you can gracefully request `top` to shut down using `docker stop test`. + +The following `Dockerfile` shows using the `ENTRYPOINT` to run Apache in the +foreground (i.e., as `PID 1`): + +``` +FROM debian:stable +RUN apt-get update && apt-get install -y --force-yes apache2 +EXPOSE 80 443 +VOLUME ["/var/www", "/var/log/apache2", "/etc/apache2"] +ENTRYPOINT ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"] +``` + +If you need to write a starter script for a single executable, you can ensure that +the final executable receives the Unix signals by using `exec` and `gosu` +commands: + +```bash +#!/bin/bash +set -e + +if [ "$1" = 'postgres' ]; then + chown -R postgres "$PGDATA" + + if [ -z "$(ls -A "$PGDATA")" ]; then + gosu postgres initdb + fi + + exec gosu postgres "$@" +fi + +exec "$@" +``` + +Lastly, if you need to do some extra cleanup (or communicate with other containers) +on shutdown, or are co-ordinating more than one executable, you may need to ensure +that the `ENTRYPOINT` script receives the Unix signals, passes them on, and then +does some more work: + +``` +#!/bin/sh +# Note: I've written this using sh so it works in the busybox container too + +# USE the trap if you need to also do manual cleanup after the service is stopped, +# or need to start multiple services in the one container +trap "echo TRAPed signal" HUP INT QUIT KILL TERM + +# start service in background here +/usr/sbin/apachectl start + +echo "[hit enter key to exit] or run 'docker stop '" +read + +# stop service and clean up here +echo "stopping apache" +/usr/sbin/apachectl stop + +echo "exited $0" +``` + +If you run this image with `docker run -it --rm -p 80:80 --name test apache`, +you can then examine the container's processes with `docker exec`, or `docker top`, +and then ask the script to stop Apache: + +```bash +$ docker exec -it test ps aux +USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND +root 1 0.1 0.0 4448 692 ? Ss+ 00:42 0:00 /bin/sh /run.sh 123 cmd cmd2 +root 19 0.0 0.2 71304 4440 ? Ss 00:42 0:00 /usr/sbin/apache2 -k start +www-data 20 0.2 0.2 360468 6004 ? Sl 00:42 0:00 /usr/sbin/apache2 -k start +www-data 21 0.2 0.2 360468 6000 ? Sl 00:42 0:00 /usr/sbin/apache2 -k start +root 81 0.0 0.1 15572 2140 ? R+ 00:44 0:00 ps aux +$ docker top test +PID USER COMMAND +10035 root {run.sh} /bin/sh /run.sh 123 cmd cmd2 +10054 root /usr/sbin/apache2 -k start +10055 33 /usr/sbin/apache2 -k start +10056 33 /usr/sbin/apache2 -k start +$ /usr/bin/time docker stop test +test +real 0m 0.27s +user 0m 0.03s +sys 0m 0.03s +``` + +> **Note:** you can over ride the `ENTRYPOINT` setting using `--entrypoint`, +> but this can only set the binary to *exec* (no `sh -c` will be used). + +> **Note**: +> The *exec* form is parsed as a JSON array, which means that +> you must use double-quotes (") around words not single-quotes ('). + +> **Note**: +> Unlike the *shell* form, the *exec* form does not invoke a command shell. +> This means that normal shell processing does not happen. For example, +> `ENTRYPOINT [ "echo", "$HOME" ]` will not do variable substitution on `$HOME`. +> If you want shell processing then either use the *shell* form or execute +> a shell directly, for example: `ENTRYPOINT [ "sh", "-c", "echo", "$HOME" ]`. +> Variables that are defined in the `Dockerfile`using `ENV`, will be substituted by +> the `Dockerfile` parser. + +### Shell form ENTRYPOINT example + +You can specify a plain string for the `ENTRYPOINT` and it will execute in `/bin/sh -c`. +This form will use shell processing to substitute shell environment variables, +and will ignore any `CMD` or `docker run` command line arguments. +To ensure that `docker stop` will signal any long running `ENTRYPOINT` executable +correctly, you need to remember to start it with `exec`: + + FROM ubuntu + ENTRYPOINT exec top -b + +When you run this image, you'll see the single `PID 1` process: + + $ docker run -it --rm --name test top + Mem: 1704520K used, 352148K free, 0K shrd, 0K buff, 140368121167873K cached + CPU: 5% usr 0% sys 0% nic 94% idle 0% io 0% irq 0% sirq + Load average: 0.08 0.03 0.05 2/98 6 + PID PPID USER STAT VSZ %VSZ %CPU COMMAND + 1 0 root R 3164 0% 0% top -b + +Which will exit cleanly on `docker stop`: + + $ /usr/bin/time docker stop test + test + real 0m 0.20s + user 0m 0.02s + sys 0m 0.04s + +If you forget to add `exec` to the beginning of your `ENTRYPOINT`: + + FROM ubuntu + ENTRYPOINT top -b + CMD --ignored-param1 + +You can then run it (giving it a name for the next step): + + $ docker run -it --name test top --ignored-param2 + Mem: 1704184K used, 352484K free, 0K shrd, 0K buff, 140621524238337K cached + CPU: 9% usr 2% sys 0% nic 88% idle 0% io 0% irq 0% sirq + Load average: 0.01 0.02 0.05 2/101 7 + PID PPID USER STAT VSZ %VSZ %CPU COMMAND + 1 0 root S 3168 0% 0% /bin/sh -c top -b cmd cmd2 + 7 1 root R 3164 0% 0% top -b + +You can see from the output of `top` that the specified `ENTRYPOINT` is not `PID 1`. + +If you then run `docker stop test`, the container will not exit cleanly - the +`stop` command will be forced to send a `SIGKILL` after the timeout: + + $ docker exec -it test ps aux + PID USER COMMAND + 1 root /bin/sh -c top -b cmd cmd2 + 7 root top -b + 8 root ps aux + $ /usr/bin/time docker stop test + test + real 0m 10.19s + user 0m 0.04s + sys 0m 0.03s + +### Understand how CMD and ENTRYPOINT interact + +Both `CMD` and `ENTRYPOINT` instructions define what command gets executed when running a container. +There are few rules that describe their co-operation. + +1. Dockerfile should specify at least one of `CMD` or `ENTRYPOINT` commands. + +2. `ENTRYPOINT` should be defined when using the container as an executable. + +3. `CMD` should be used as a way of defining default arguments for an `ENTRYPOINT` command +or for executing an ad-hoc command in a container. + +4. `CMD` will be overridden when running the container with alternative arguments. + +The table below shows what command is executed for different `ENTRYPOINT` / `CMD` combinations: + +| | No ENTRYPOINT | ENTRYPOINT exec_entry p1_entry | ENTRYPOINT ["exec_entry", "p1_entry"] | +|--------------------------------|----------------------------|-----------------------------------------------------------|------------------------------------------------| +| **No CMD** | *error, not allowed* | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry | +| **CMD ["exec_cmd", "p1_cmd"]** | exec_cmd p1_cmd | /bin/sh -c exec_entry p1_entry exec_cmd p1_cmd | exec_entry p1_entry exec_cmd p1_cmd | +| **CMD ["p1_cmd", "p2_cmd"]** | p1_cmd p2_cmd | /bin/sh -c exec_entry p1_entry p1_cmd p2_cmd | exec_entry p1_entry p1_cmd p2_cmd | +| **CMD exec_cmd p1_cmd** | /bin/sh -c exec_cmd p1_cmd | /bin/sh -c exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd | exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd | + +## VOLUME + + VOLUME ["/data"] + +The `VOLUME` instruction creates a mount point with the specified name +and marks it as holding externally mounted volumes from native host or other +containers. The value can be a JSON array, `VOLUME ["/var/log/"]`, or a plain +string with multiple arguments, such as `VOLUME /var/log` or `VOLUME /var/log +/var/db`. For more information/examples and mounting instructions via the +Docker client, refer to +[*Share Directories via Volumes*](../userguide/containers/dockervolumes.md#mount-a-host-directory-as-a-data-volume) +documentation. + +The `docker run` command initializes the newly created volume with any data +that exists at the specified location within the base image. For example, +consider the following Dockerfile snippet: + + FROM ubuntu + RUN mkdir /myvol + RUN echo "hello world" > /myvol/greeting + VOLUME /myvol + +This Dockerfile results in an image that causes `docker run`, to +create a new mount point at `/myvol` and copy the `greeting` file +into the newly created volume. + +> **Note**: +> If any build steps change the data within the volume after it has been +> declared, those changes will be discarded. + +> **Note**: +> The list is parsed as a JSON array, which means that +> you must use double-quotes (") around words not single-quotes ('). + +## USER + + USER daemon + +The `USER` instruction sets the user name or UID to use when running the image +and for any `RUN`, `CMD` and `ENTRYPOINT` instructions that follow it in the +`Dockerfile`. + +## WORKDIR + + WORKDIR /path/to/workdir + +The `WORKDIR` instruction sets the working directory for any `RUN`, `CMD`, +`ENTRYPOINT`, `COPY` and `ADD` instructions that follow it in the `Dockerfile`. +If the `WORKDIR` doesn't exist, it will be created even if its not used in any +subsequent `Dockerfile` instruction. + +It can be used multiple times in the one `Dockerfile`. If a relative path +is provided, it will be relative to the path of the previous `WORKDIR` +instruction. For example: + + WORKDIR /a + WORKDIR b + WORKDIR c + RUN pwd + +The output of the final `pwd` command in this `Dockerfile` would be +`/a/b/c`. + +The `WORKDIR` instruction can resolve environment variables previously set using +`ENV`. You can only use environment variables explicitly set in the `Dockerfile`. +For example: + + ENV DIRPATH /path + WORKDIR $DIRPATH/$DIRNAME + RUN pwd + +The output of the final `pwd` command in this `Dockerfile` would be +`/path/$DIRNAME` + +## ARG + + ARG [=] + +The `ARG` instruction defines a variable that users can pass at build-time to +the builder with the `docker build` command using the `--build-arg +=` flag. If a user specifies a build argument that was not +defined in the Dockerfile, the build outputs an error. + +``` +One or more build-args were not consumed, failing build. +``` + +The Dockerfile author can define a single variable by specifying `ARG` once or many +variables by specifying `ARG` more than once. For example, a valid Dockerfile: + +``` +FROM busybox +ARG user1 +ARG buildno +... +``` + +A Dockerfile author may optionally specify a default value for an `ARG` instruction: + +``` +FROM busybox +ARG user1=someuser +ARG buildno=1 +... +``` + +If an `ARG` value has a default and if there is no value passed at build-time, the +builder uses the default. + +An `ARG` variable definition comes into effect from the line on which it is +defined in the `Dockerfile` not from the argument's use on the command-line or +elsewhere. For example, consider this Dockerfile: + +``` +1 FROM busybox +2 USER ${user:-some_user} +3 ARG user +4 USER $user +... +``` +A user builds this file by calling: + +``` +$ docker build --build-arg user=what_user Dockerfile +``` + +The `USER` at line 2 evaluates to `some_user` as the `user` variable is defined on the +subsequent line 3. The `USER` at line 4 evaluates to `what_user` as `user` is +defined and the `what_user` value was passed on the command line. Prior to its definition by an +`ARG` instruction, any use of a variable results in an empty string. + +> **Note:** It is not recommended to use build-time variables for +> passing secrets like github keys, user credentials etc. + +You can use an `ARG` or an `ENV` instruction to specify variables that are +available to the `RUN` instruction. Environment variables defined using the +`ENV` instruction always override an `ARG` instruction of the same name. Consider +this Dockerfile with an `ENV` and `ARG` instruction. + +``` +1 FROM ubuntu +2 ARG CONT_IMG_VER +3 ENV CONT_IMG_VER v1.0.0 +4 RUN echo $CONT_IMG_VER +``` +Then, assume this image is built with this command: + +``` +$ docker build --build-arg CONT_IMG_VER=v2.0.1 Dockerfile +``` + +In this case, the `RUN` instruction uses `v1.0.0` instead of the `ARG` setting +passed by the user:`v2.0.1` This behavior is similar to a shell +script where a locally scoped variable overrides the variables passed as +arguments or inherited from environment, from its point of definition. + +Using the example above but a different `ENV` specification you can create more +useful interactions between `ARG` and `ENV` instructions: + +``` +1 FROM ubuntu +2 ARG CONT_IMG_VER +3 ENV CONT_IMG_VER ${CONT_IMG_VER:-v1.0.0} +4 RUN echo $CONT_IMG_VER +``` + +Unlike an `ARG` instruction, `ENV` values are always persisted in the built +image. Consider a docker build without the --build-arg flag: + +``` +$ docker build Dockerfile +``` + +Using this Dockerfile example, `CONT_IMG_VER` is still persisted in the image but +its value would be `v1.0.0` as it is the default set in line 3 by the `ENV` instruction. + +The variable expansion technique in this example allows you to pass arguments +from the command line and persist them in the final image by leveraging the +`ENV` instruction. Variable expansion is only supported for [a limited set of +Dockerfile instructions.](#environment-replacement) + +Docker has a set of predefined `ARG` variables that you can use without a +corresponding `ARG` instruction in the Dockerfile. + +* `HTTP_PROXY` +* `http_proxy` +* `HTTPS_PROXY` +* `https_proxy` +* `FTP_PROXY` +* `ftp_proxy` +* `NO_PROXY` +* `no_proxy` + +To use these, simply pass them on the command line using the `--build-arg +=` flag. + +### Impact on build caching + +`ARG` variables are not persisted into the built image as `ENV` variables are. +However, `ARG` variables do impact the build cache in similar ways. If a +Dockerfile defines an `ARG` variable whose value is different from a previous +build, then a "cache miss" occurs upon first use of the `ARG` variable. The +declaration of the `ARG` variable does not count as a use. + +For example, consider these two Dockerfile: + +``` +1 FROM ubuntu +2 ARG CONT_IMG_VER +3 RUN echo $CONT_IMG_VER +``` + +``` +1 FROM ubuntu +2 ARG CONT_IMG_VER +3 RUN echo hello +``` + +If you specify `--build-arg CONT_IMG_VER=` on the command line, in both +cases, the specification on line 2 does not cause a cache miss; line 3 does +cause a cache miss.`ARG CONT_IMG_VER` causes the RUN line to be identified +as the same as running `CONT_IMG_VER=` echo hello, so if the `` +changes, we get a cache miss. + +Consider another example under the same command line: + +``` +1 FROM ubuntu +2 ARG CONT_IMG_VER +3 ENV CONT_IMG_VER $CONT_IMG_VER +4 RUN echo $CONT_IMG_VER +``` +In this example, the cache miss occurs on line 3. The miss happens because +the variable's value in the `ENV` references the `ARG` variable and that +variable is changed through the command line. In this example, the `ENV` +command causes the image to include the value. + +If an `ENV` instruction overrides an `ARG` instruction of the same name, like +this Dockerfile: + +``` +1 FROM ubuntu +2 ARG CONT_IMG_VER +3 ENV CONT_IMG_VER hello +4 RUN echo $CONT_IMG_VER +``` + +Line 3 does not cause a cache miss because the value of `CONT_IMG_VER` is a +constant (`hello`). As a result, the environment variables and values used on +the `RUN` (line 4) doesn't change between builds. + +## ONBUILD + + ONBUILD [INSTRUCTION] + +The `ONBUILD` instruction adds to the image a *trigger* instruction to +be executed at a later time, when the image is used as the base for +another build. The trigger will be executed in the context of the +downstream build, as if it had been inserted immediately after the +`FROM` instruction in the downstream `Dockerfile`. + +Any build instruction can be registered as a trigger. + +This is useful if you are building an image which will be used as a base +to build other images, for example an application build environment or a +daemon which may be customized with user-specific configuration. + +For example, if your image is a reusable Python application builder, it +will require application source code to be added in a particular +directory, and it might require a build script to be called *after* +that. You can't just call `ADD` and `RUN` now, because you don't yet +have access to the application source code, and it will be different for +each application build. You could simply provide application developers +with a boilerplate `Dockerfile` to copy-paste into their application, but +that is inefficient, error-prone and difficult to update because it +mixes with application-specific code. + +The solution is to use `ONBUILD` to register advance instructions to +run later, during the next build stage. + +Here's how it works: + +1. When it encounters an `ONBUILD` instruction, the builder adds a + trigger to the metadata of the image being built. The instruction + does not otherwise affect the current build. +2. At the end of the build, a list of all triggers is stored in the + image manifest, under the key `OnBuild`. They can be inspected with + the `docker inspect` command. +3. Later the image may be used as a base for a new build, using the + `FROM` instruction. As part of processing the `FROM` instruction, + the downstream builder looks for `ONBUILD` triggers, and executes + them in the same order they were registered. If any of the triggers + fail, the `FROM` instruction is aborted which in turn causes the + build to fail. If all triggers succeed, the `FROM` instruction + completes and the build continues as usual. +4. Triggers are cleared from the final image after being executed. In + other words they are not inherited by "grand-children" builds. + +For example you might add something like this: + + [...] + ONBUILD ADD . /app/src + ONBUILD RUN /usr/local/bin/python-build --dir /app/src + [...] + +> **Warning**: Chaining `ONBUILD` instructions using `ONBUILD ONBUILD` isn't allowed. + +> **Warning**: The `ONBUILD` instruction may not trigger `FROM` or `MAINTAINER` instructions. + +## STOPSIGNAL + + STOPSIGNAL signal + +The `STOPSIGNAL` instruction sets the system call signal that will be sent to the container to exit. +This signal can be a valid unsigned number that matches a position in the kernel's syscall table, for instance 9, +or a signal name in the format SIGNAME, for instance SIGKILL. + +## Dockerfile examples + +Below you can see some examples of Dockerfile syntax. If you're interested in +something more realistic, take a look at the list of [Dockerization examples](../examples/index.md). + +``` +# Nginx +# +# VERSION 0.0.1 + +FROM ubuntu +MAINTAINER Victor Vieux + +LABEL Description="This image is used to start the foobar executable" Vendor="ACME Products" Version="1.0" +RUN apt-get update && apt-get install -y inotify-tools nginx apache2 openssh-server +``` + +``` +# Firefox over VNC +# +# VERSION 0.3 + +FROM ubuntu + +# Install vnc, xvfb in order to create a 'fake' display and firefox +RUN apt-get update && apt-get install -y x11vnc xvfb firefox +RUN mkdir ~/.vnc +# Setup a password +RUN x11vnc -storepasswd 1234 ~/.vnc/passwd +# Autostart firefox (might not be the best way, but it does the trick) +RUN bash -c 'echo "firefox" >> /.bashrc' + +EXPOSE 5900 +CMD ["x11vnc", "-forever", "-usepw", "-create"] +``` + +``` +# Multiple images example +# +# VERSION 0.1 + +FROM ubuntu +RUN echo foo > bar +# Will output something like ===> 907ad6c2736f + +FROM ubuntu +RUN echo moo > oink +# Will output something like ===> 695d7793cbe4 + +# You᾿ll now have two images, 907ad6c2736f with /bar, and 695d7793cbe4 with +# /oink. +``` diff --git a/docs/reference/commandline/attach.md b/docs/reference/commandline/attach.md new file mode 100644 index 00000000..dfe2908c --- /dev/null +++ b/docs/reference/commandline/attach.md @@ -0,0 +1,115 @@ + + +# attach + + Usage: docker attach [OPTIONS] CONTAINER + + Attach to a running container + + --detach-keys="" Set up escape key sequence + --help Print usage + --no-stdin Do not attach STDIN + --sig-proxy=true Proxy all received signals to the process + +The `docker attach` command allows you to attach to a running container using +the container's ID or name, either to view its ongoing output or to control it +interactively. You can attach to the same contained process multiple times +simultaneously, screen sharing style, or quickly view the progress of your +detached process. + +To stop a container, use `CTRL-c`. This key sequence sends `SIGKILL` to the +container. If `--sig-proxy` is true (the default),`CTRL-c` sends a `SIGINT` to +the container. You can detach from a container and leave it running using the +using `CTRL-p CTRL-q` key sequence. + +> **Note:** +> A process running as PID 1 inside a container is treated specially by +> Linux: it ignores any signal with the default action. So, the process +> will not terminate on `SIGINT` or `SIGTERM` unless it is coded to do +> so. + +It is forbidden to redirect the standard input of a `docker attach` command +while attaching to a tty-enabled container (i.e.: launched with `-t`). + + +## Override the detach sequence + +If you want, you can configure a override the Docker key sequence for detach. +This is is useful if the Docker default sequence conflicts with key squence you +use for other applications. There are two ways to defines a your own detach key +sequence, as a per-container override or as a configuration property on your +entire configuration. + +To override the sequence for an individual container, use the +`--detach-keys=""` flag with the `docker attach` command. The format of +the `` is either a letter [a-Z], or the `ctrl-` combined with any of +the following: + +* `a-z` (a single lowercase alpha character ) +* `@` (at sign) +* `[` (left bracket) +* `\\` (two backward slashes) +* `_` (underscore) +* `^` (caret) + +These `a`, `ctrl-a`, `X`, or `ctrl-\\` values are all examples of valid key +sequences. To configure a different configuration default key sequence for all +containers, see [**Configuration file** section](cli.md#configuration-files). + +#### Examples + + $ docker run -d --name topdemo ubuntu /usr/bin/top -b + $ docker attach topdemo + top - 02:05:52 up 3:05, 0 users, load average: 0.01, 0.02, 0.05 + Tasks: 1 total, 1 running, 0 sleeping, 0 stopped, 0 zombie + Cpu(s): 0.1%us, 0.2%sy, 0.0%ni, 99.7%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st + Mem: 373572k total, 355560k used, 18012k free, 27872k buffers + Swap: 786428k total, 0k used, 786428k free, 221740k cached + + PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND + 1 root 20 0 17200 1116 912 R 0 0.3 0:00.03 top + + top - 02:05:55 up 3:05, 0 users, load average: 0.01, 0.02, 0.05 + Tasks: 1 total, 1 running, 0 sleeping, 0 stopped, 0 zombie + Cpu(s): 0.0%us, 0.2%sy, 0.0%ni, 99.8%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st + Mem: 373572k total, 355244k used, 18328k free, 27872k buffers + Swap: 786428k total, 0k used, 786428k free, 221776k cached + + PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND + 1 root 20 0 17208 1144 932 R 0 0.3 0:00.03 top + + + top - 02:05:58 up 3:06, 0 users, load average: 0.01, 0.02, 0.05 + Tasks: 1 total, 1 running, 0 sleeping, 0 stopped, 0 zombie + Cpu(s): 0.2%us, 0.3%sy, 0.0%ni, 99.5%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st + Mem: 373572k total, 355780k used, 17792k free, 27880k buffers + Swap: 786428k total, 0k used, 786428k free, 221776k cached + + PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND + 1 root 20 0 17208 1144 932 R 0 0.3 0:00.03 top + ^C$ + $ echo $? + 0 + $ docker ps -a | grep topdemo + 7998ac8581f9 ubuntu:14.04 "/usr/bin/top -b" 38 seconds ago Exited (0) 21 seconds ago topdemo + +And in this second example, you can see the exit code returned by the `bash` +process is returned by the `docker attach` command to its caller too: + + $ docker run --name test -d -it debian + 275c44472aebd77c926d4527885bb09f2f6db21d878c75f0a1c212c03d3bcfab + $ docker attach test + $$ exit 13 + exit + $ echo $? + 13 + $ docker ps -a | grep test + 275c44472aeb debian:7 "/bin/bash" 26 seconds ago Exited (13) 17 seconds ago test diff --git a/docs/reference/commandline/build.md b/docs/reference/commandline/build.md new file mode 100644 index 00000000..4530f77b --- /dev/null +++ b/docs/reference/commandline/build.md @@ -0,0 +1,318 @@ + + +# build + + Usage: docker build [OPTIONS] PATH | URL | - + + Build a new image from the source code at PATH + + --build-arg=[] Set build-time variables + --cpu-shares CPU Shares (relative weight) + --cgroup-parent="" Optional parent cgroup for the container + --cpu-period=0 Limit the CPU CFS (Completely Fair Scheduler) period + --cpu-quota=0 Limit the CPU CFS (Completely Fair Scheduler) quota + --cpuset-cpus="" CPUs in which to allow execution, e.g. `0-3`, `0,1` + --cpuset-mems="" MEMs in which to allow execution, e.g. `0-3`, `0,1` + --disable-content-trust=true Skip image verification + -f, --file="" Name of the Dockerfile (Default is 'PATH/Dockerfile') + --force-rm Always remove intermediate containers + --help Print usage + --isolation="" Container isolation technology + --label=[] Set metadata for an image + -m, --memory="" Memory limit for all build containers + --memory-swap="" A positive integer equal to memory plus swap. Specify -1 to enable unlimited swap. + --no-cache Do not use cache when building the image + --pull Always attempt to pull a newer version of the image + -q, --quiet Suppress the build output and print image ID on success + --rm=true Remove intermediate containers after a successful build + --shm-size=[] Size of `/dev/shm`. The format is ``. `number` must be greater than `0`. Unit is optional and can be `b` (bytes), `k` (kilobytes), `m` (megabytes), or `g` (gigabytes). If you omit the unit, the system uses bytes. If you omit the size entirely, the system uses `64m`. + -t, --tag=[] Name and optionally a tag in the 'name:tag' format + --ulimit=[] Ulimit options + +Builds Docker images from a Dockerfile and a "context". A build's context is +the files located in the specified `PATH` or `URL`. The build process can refer +to any of the files in the context. For example, your build can use an +[*ADD*](../builder.md#add) instruction to reference a file in the +context. + +The `URL` parameter can specify the location of a Git repository; the repository +acts as the build context. The system recursively clones the repository and its +submodules using a `git clone --depth 1 --recursive` command. This command runs +in a temporary directory on your local host. After the command succeeds, the +directory is sent to the Docker daemon as the context. Local clones give you the +ability to access private repositories using local user credentials, VPNs, and +so forth. + +Git URLs accept context configuration in their fragment section, separated by a +colon `:`. The first part represents the reference that Git will check out, +this can be either a branch, a tag, or a commit SHA. The second part represents +a subdirectory inside the repository that will be used as a build context. + +For example, run this command to use a directory called `docker` in the branch +`container`: + + $ docker build https://github.com/docker/rootfs.git#container:docker + +The following table represents all the valid suffixes with their build +contexts: + +Build Syntax Suffix | Commit Used | Build Context Used +--------------------|-------------|------------------- +`myrepo.git` | `refs/heads/master` | `/` +`myrepo.git#mytag` | `refs/tags/mytag` | `/` +`myrepo.git#mybranch` | `refs/heads/mybranch` | `/` +`myrepo.git#abcdef` | `sha1 = abcdef` | `/` +`myrepo.git#:myfolder` | `refs/heads/master` | `/myfolder` +`myrepo.git#master:myfolder` | `refs/heads/master` | `/myfolder` +`myrepo.git#mytag:myfolder` | `refs/tags/mytag` | `/myfolder` +`myrepo.git#mybranch:myfolder` | `refs/heads/mybranch` | `/myfolder` +`myrepo.git#abcdef:myfolder` | `sha1 = abcdef` | `/myfolder` + +Instead of specifying a context, you can pass a single Dockerfile in the `URL` +or pipe the file in via `STDIN`. To pipe a Dockerfile from `STDIN`: + + docker build - < Dockerfile + +If you use STDIN or specify a `URL`, the system places the contents into a file +called `Dockerfile`, and any `-f`, `--file` option is ignored. In this +scenario, there is no context. + +By default the `docker build` command will look for a `Dockerfile` at the root +of the build context. The `-f`, `--file`, option lets you specify the path to +an alternative file to use instead. This is useful in cases where the same set +of files are used for multiple builds. The path must be to a file within the +build context. If a relative path is specified then it must to be relative to +the current directory. + +In most cases, it's best to put each Dockerfile in an empty directory. Then, +add to that directory only the files needed for building the Dockerfile. To +increase the build's performance, you can exclude files and directories by +adding a `.dockerignore` file to that directory as well. For information on +creating one, see the [.dockerignore file](../builder.md#dockerignore-file). + +If the Docker client loses connection to the daemon, the build is canceled. +This happens if you interrupt the Docker client with `CTRL-c` or if the Docker +client is killed for any reason. If the build initiated a pull which is still +running at the time the build is cancelled, the pull is cancelled as well. + +## Return code + +On a successful build, a return code of success `0` will be returned. When the +build fails, a non-zero failure code will be returned. + +There should be informational output of the reason for failure output to +`STDERR`: + + $ docker build -t fail . + Sending build context to Docker daemon 2.048 kB + Sending build context to Docker daemon + Step 1 : FROM busybox + ---> 4986bf8c1536 + Step 2 : RUN exit 13 + ---> Running in e26670ec7a0a + INFO[0000] The command [/bin/sh -c exit 13] returned a non-zero code: 13 + $ echo $? + 1 + +See also: + +[*Dockerfile Reference*](../builder.md). + +## Examples + +### Build with PATH + + $ docker build . + Uploading context 10240 bytes + Step 1 : FROM busybox + Pulling repository busybox + ---> e9aa60c60128MB/2.284 MB (100%) endpoint: https://cdn-registry-1.docker.io/v1/ + Step 2 : RUN ls -lh / + ---> Running in 9c9e81692ae9 + total 24 + drwxr-xr-x 2 root root 4.0K Mar 12 2013 bin + drwxr-xr-x 5 root root 4.0K Oct 19 00:19 dev + drwxr-xr-x 2 root root 4.0K Oct 19 00:19 etc + drwxr-xr-x 2 root root 4.0K Nov 15 23:34 lib + lrwxrwxrwx 1 root root 3 Mar 12 2013 lib64 -> lib + dr-xr-xr-x 116 root root 0 Nov 15 23:34 proc + lrwxrwxrwx 1 root root 3 Mar 12 2013 sbin -> bin + dr-xr-xr-x 13 root root 0 Nov 15 23:34 sys + drwxr-xr-x 2 root root 4.0K Mar 12 2013 tmp + drwxr-xr-x 2 root root 4.0K Nov 15 23:34 usr + ---> b35f4035db3f + Step 3 : CMD echo Hello world + ---> Running in 02071fceb21b + ---> f52f38b7823e + Successfully built f52f38b7823e + Removing intermediate container 9c9e81692ae9 + Removing intermediate container 02071fceb21b + +This example specifies that the `PATH` is `.`, and so all the files in the +local directory get `tar`d and sent to the Docker daemon. The `PATH` specifies +where to find the files for the "context" of the build on the Docker daemon. +Remember that the daemon could be running on a remote machine and that no +parsing of the Dockerfile happens at the client side (where you're running +`docker build`). That means that *all* the files at `PATH` get sent, not just +the ones listed to [*ADD*](../builder.md#add) in the Dockerfile. + +The transfer of context from the local machine to the Docker daemon is what the +`docker` client means when you see the "Sending build context" message. + +If you wish to keep the intermediate containers after the build is complete, +you must use `--rm=false`. This does not affect the build cache. + +### Build with URL + + $ docker build github.com/creack/docker-firefox + +This will clone the GitHub repository and use the cloned repository as context. +The Dockerfile at the root of the repository is used as Dockerfile. Note that +you can specify an arbitrary Git repository by using the `git://` or `git@` +schema. + +### Build with - + + $ docker build - < Dockerfile + +This will read a Dockerfile from `STDIN` without context. Due to the lack of a +context, no contents of any local directory will be sent to the Docker daemon. +Since there is no context, a Dockerfile `ADD` only works if it refers to a +remote URL. + + $ docker build - < context.tar.gz + +This will build an image for a compressed context read from `STDIN`. Supported +formats are: bzip2, gzip and xz. + +### Usage of .dockerignore + + $ docker build . + Uploading context 18.829 MB + Uploading context + Step 1 : FROM busybox + ---> 769b9341d937 + Step 2 : CMD echo Hello world + ---> Using cache + ---> 99cc1ad10469 + Successfully built 99cc1ad10469 + $ echo ".git" > .dockerignore + $ docker build . + Uploading context 6.76 MB + Uploading context + Step 1 : FROM busybox + ---> 769b9341d937 + Step 2 : CMD echo Hello world + ---> Using cache + ---> 99cc1ad10469 + Successfully built 99cc1ad10469 + +This example shows the use of the `.dockerignore` file to exclude the `.git` +directory from the context. Its effect can be seen in the changed size of the +uploaded context. The builder reference contains detailed information on +[creating a .dockerignore file](../builder.md#dockerignore-file) + +### Tag image (-t) + + $ docker build -t vieux/apache:2.0 . + +This will build like the previous example, but it will then tag the resulting +image. The repository name will be `vieux/apache` and the tag will be `2.0` + +You can apply multiple tags to an image. For example, you can apply the `latest` +tag to a newly built image and add another tag that references a specific +version. +For example, to tag an image both as `whenry/fedora-jboss:latest` and +`whenry/fedora-jboss:v2.1`, use the following: + + $ docker build -t whenry/fedora-jboss:latest -t whenry/fedora-jboss:v2.1 . + +### Specify Dockerfile (-f) + + $ docker build -f Dockerfile.debug . + +This will use a file called `Dockerfile.debug` for the build instructions +instead of `Dockerfile`. + + $ docker build -f dockerfiles/Dockerfile.debug -t myapp_debug . + $ docker build -f dockerfiles/Dockerfile.prod -t myapp_prod . + +The above commands will build the current build context (as specified by the +`.`) twice, once using a debug version of a `Dockerfile` and once using a +production version. + + $ cd /home/me/myapp/some/dir/really/deep + $ docker build -f /home/me/myapp/dockerfiles/debug /home/me/myapp + $ docker build -f ../../../../dockerfiles/debug /home/me/myapp + +These two `docker build` commands do the exact same thing. They both use the +contents of the `debug` file instead of looking for a `Dockerfile` and will use +`/home/me/myapp` as the root of the build context. Note that `debug` is in the +directory structure of the build context, regardless of how you refer to it on +the command line. + +> **Note:** +> `docker build` will return a `no such file or directory` error if the +> file or directory does not exist in the uploaded context. This may +> happen if there is no context, or if you specify a file that is +> elsewhere on the Host system. The context is limited to the current +> directory (and its children) for security reasons, and to ensure +> repeatable builds on remote Docker hosts. This is also the reason why +> `ADD ../file` will not work. + +### Optional parent cgroup (--cgroup-parent) + +When `docker build` is run with the `--cgroup-parent` option the containers +used in the build will be run with the [corresponding `docker run` +flag](../run.md#specifying-custom-cgroups). + +### Set ulimits in container (--ulimit) + +Using the `--ulimit` option with `docker build` will cause each build step's +container to be started using those [`--ulimit` +flag values](./run.md#set-ulimits-in-container-ulimit). + +### Set build-time variables (--build-arg) + +You can use `ENV` instructions in a Dockerfile to define variable +values. These values persist in the built image. However, often +persistence is not what you want. Users want to specify variables differently +depending on which host they build an image on. + +A good example is `http_proxy` or source versions for pulling intermediate +files. The `ARG` instruction lets Dockerfile authors define values that users +can set at build-time using the `--build-arg` flag: + + $ docker build --build-arg HTTP_PROXY=http://10.20.30.2:1234 . + +This flag allows you to pass the build-time variables that are +accessed like regular environment variables in the `RUN` instruction of the +Dockerfile. Also, these values don't persist in the intermediate or final images +like `ENV` values do. + +For detailed information on using `ARG` and `ENV` instructions, see the +[Dockerfile reference](../builder.md). + +### Specify isolation technology for container (--isolation) + +This option is useful in situations where you are running Docker containers on +Windows. The `--isolation=` option sets a container's isolation +technology. On Linux, the only supported is the `default` option which uses +Linux namespaces. On Microsoft Windows, you can specify these values: + + +| Value | Description | +|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `default` | Use the value specified by the Docker daemon's `--exec-opt` . If the `daemon` does not specify an isolation technology, Microsoft Windows uses `process` as its default value. | +| `process` | Namespace isolation only. | +| `hyperv` | Hyper-V hypervisor partition-based isolation. | + +Specifying the `--isolation` flag without a value is the same as setting `--isolation="default"`. diff --git a/docs/reference/commandline/cli.md b/docs/reference/commandline/cli.md new file mode 100644 index 00000000..ac5f62d4 --- /dev/null +++ b/docs/reference/commandline/cli.md @@ -0,0 +1,209 @@ + + +# Use the Docker command line + +To list available commands, either run `docker` with no parameters +or execute `docker help`: + + $ docker + Usage: docker [OPTIONS] COMMAND [arg...] + docker daemon [ --help | ... ] + docker [ --help | -v | --version ] + + -H, --host=[]: The socket(s) to talk to the Docker daemon in the format of tcp://host:port/path, unix:///path/to/socket, fd://* or fd://socketfd. + + A self-sufficient runtime for Linux containers. + + ... + +Depending on your Docker system configuration, you may be required to preface +each `docker` command with `sudo`. To avoid having to use `sudo` with the +`docker` command, your system administrator can create a Unix group called +`docker` and add users to it. + +For more information about installing Docker or `sudo` configuration, refer to +the [installation](../../installation/index.md) instructions for your operating system. + +## Environment variables + +For easy reference, the following list of environment variables are supported +by the `docker` command line: + +* `DOCKER_API_VERSION` The API version to use (e.g. `1.19`) +* `DOCKER_CONFIG` The location of your client configuration files. +* `DOCKER_CERT_PATH` The location of your authentication keys. +* `DOCKER_DRIVER` The graph driver to use. +* `DOCKER_HOST` Daemon socket to connect to. +* `DOCKER_NOWARN_KERNEL_VERSION` Prevent warnings that your Linux kernel is + unsuitable for Docker. +* `DOCKER_RAMDISK` If set this will disable 'pivot_root'. +* `DOCKER_TLS_VERIFY` When set Docker uses TLS and verifies the remote. +* `DOCKER_CONTENT_TRUST` When set Docker uses notary to sign and verify images. + Equates to `--disable-content-trust=false` for build, create, pull, push, run. +* `DOCKER_CONTENT_TRUST_SERVER` The URL of the Notary server to use. This defaults + to the same URL as the registry. +* `DOCKER_TMPDIR` Location for temporary Docker files. + +Because Docker is developed using 'Go', you can also use any environment +variables used by the 'Go' runtime. In particular, you may find these useful: + +* `HTTP_PROXY` +* `HTTPS_PROXY` +* `NO_PROXY` + +These Go environment variables are case-insensitive. See the +[Go specification](http://golang.org/pkg/net/http/) for details on these +variables. + +## Configuration files + +By default, the Docker command line stores its configuration files in a +directory called `.docker` within your `$HOME` directory. However, you can +specify a different location via the `DOCKER_CONFIG` environment variable +or the `--config` command line option. If both are specified, then the +`--config` option overrides the `DOCKER_CONFIG` environment variable. +For example: + + docker --config ~/testconfigs/ ps + +Instructs Docker to use the configuration files in your `~/testconfigs/` +directory when running the `ps` command. + +Docker manages most of the files in the configuration directory +and you should not modify them. However, you *can modify* the +`config.json` file to control certain aspects of how the `docker` +command behaves. + +Currently, you can modify the `docker` command behavior using environment +variables or command-line options. You can also use options within +`config.json` to modify some of the same behavior. When using these +mechanisms, you must keep in mind the order of precedence among them. Command +line options override environment variables and environment variables override +properties you specify in a `config.json` file. + +The `config.json` file stores a JSON encoding of several properties: + +The property `HttpHeaders` specifies a set of headers to include in all messages +sent from the Docker client to the daemon. Docker does not try to interpret or +understand these header; it simply puts them into the messages. Docker does +not allow these headers to change any headers it sets for itself. + +The property `psFormat` specifies the default format for `docker ps` output. +When the `--format` flag is not provided with the `docker ps` command, +Docker's client uses this property. If this property is not set, the client +falls back to the default table format. For a list of supported formatting +directives, see the +[**Formatting** section in the `docker ps` documentation](ps.md) + +Once attached to a container, users detach from it and leave it running using +the using `CTRL-p CTRL-q` key sequence. This detach key sequence is customizable +using the `detachKeys` property. Specify a `` value for the +property. The format of the `` is a comma-separated list of either +a letter [a-Z], or the `ctrl-` combined with any of the following: + +* `a-z` (a single lowercase alpha character ) +* `@` (at sign) +* `[` (left bracket) +* `\\` (two backward slashes) +* `_` (underscore) +* `^` (caret) + +Your customization applies to all containers started in with your Docker client. +Users can override your custom or the default key sequence on a per-container +basis. To do this, the user specifies the `--detach-keys` flag with the `docker +attach`, `docker exec`, `docker run` or `docker start` command. + +The property `imagesFormat` specifies the default format for `docker images` output. +When the `--format` flag is not provided with the `docker images` command, +Docker's client uses this property. If this property is not set, the client +falls back to the default table format. For a list of supported formatting +directives, see the [**Formatting** section in the `docker images` documentation](images.md) + +Following is a sample `config.json` file: + + { + "HttpHeaders": { + "MyHeader": "MyValue" + }, + "psFormat": "table {{.ID}}\\t{{.Image}}\\t{{.Command}}\\t{{.Labels}}", + "imagesFormat": "table {{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}", + "detachKeys": "ctrl-e,e" + } + +### Notary + +If using your own notary server and a self-signed certificate or an internal +Certificate Authority, you need to place the certificate at +`tls//ca.crt` in your docker config directory. + +Alternatively you can trust the certificate globally by adding it to your system's +list of root Certificate Authorities. + +## Help + +To list the help on any command just execute the command, followed by the +`--help` option. + + $ docker run --help + + Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...] + + Run a command in a new container + + -a, --attach=[] Attach to STDIN, STDOUT or STDERR + --cpu-shares=0 CPU shares (relative weight) + ... + +## Option types + +Single character command line options can be combined, so rather than +typing `docker run -i -t --name test busybox sh`, +you can write `docker run -it --name test busybox sh`. + +### Boolean + +Boolean options take the form `-d=false`. The value you see in the help text is +the default value which is set if you do **not** specify that flag. If you +specify a Boolean flag without a value, this will set the flag to `true`, +irrespective of the default value. + +For example, running `docker run -d` will set the value to `true`, so your +container **will** run in "detached" mode, in the background. + +Options which default to `true` (e.g., `docker build --rm=true`) can only be +set to the non-default value by explicitly setting them to `false`: + + $ docker build --rm=false . + +### Multi + +You can specify options like `-a=[]` multiple times in a single command line, +for example in these commands: + + $ docker run -a stdin -a stdout -i -t ubuntu /bin/bash + $ docker run -a stdin -a stdout -a stderr ubuntu /bin/ls + +Sometimes, multiple options can call for a more complex value string as for +`-v`: + + $ docker run -v /host:/container example/mysql + +> **Note:** +> Do not use the `-t` and `-a stderr` options together due to +> limitations in the `pty` implementation. All `stderr` in `pty` mode +> simply goes to `stdout`. + +### Strings and Integers + +Options like `--name=""` expect a string, and they +can only be specified once. Options like `-c=0` +expect an integer, and they can only be specified once. diff --git a/docs/reference/commandline/commit.md b/docs/reference/commandline/commit.md new file mode 100644 index 00000000..df64e957 --- /dev/null +++ b/docs/reference/commandline/commit.md @@ -0,0 +1,82 @@ + + +# commit + + Usage: docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]] + + Create a new image from a container's changes + + -a, --author="" Author (e.g., "John Hannibal Smith ") + -c, --change=[] Apply specified Dockerfile instructions while committing the image + --help Print usage + -m, --message="" Commit message + -p, --pause=true Pause container during commit + +It can be useful to commit a container's file changes or settings into a new +image. This allows you debug a container by running an interactive shell, or to +export a working dataset to another server. Generally, it is better to use +Dockerfiles to manage your images in a documented and maintainable way. + +The commit operation will not include any data contained in +volumes mounted inside the container. + +By default, the container being committed and its processes will be paused +while the image is committed. This reduces the likelihood of encountering data +corruption during the process of creating the commit. If this behavior is +undesired, set the `--pause` option to false. + +The `--change` option will apply `Dockerfile` instructions to the image that is +created. Supported `Dockerfile` instructions: +`CMD`|`ENTRYPOINT`|`ENV`|`EXPOSE`|`LABEL`|`ONBUILD`|`USER`|`VOLUME`|`WORKDIR` + +## Commit a container + + $ docker ps + ID IMAGE COMMAND CREATED STATUS PORTS + c3f279d17e0a ubuntu:12.04 /bin/bash 7 days ago Up 25 hours + 197387f1b436 ubuntu:12.04 /bin/bash 7 days ago Up 25 hours + $ docker commit c3f279d17e0a svendowideit/testimage:version3 + f5283438590d + $ docker images + REPOSITORY TAG ID CREATED SIZE + svendowideit/testimage version3 f5283438590d 16 seconds ago 335.7 MB + +## Commit a container with new configurations + + $ docker ps + ID IMAGE COMMAND CREATED STATUS PORTS + c3f279d17e0a ubuntu:12.04 /bin/bash 7 days ago Up 25 hours + 197387f1b436 ubuntu:12.04 /bin/bash 7 days ago Up 25 hours + $ docker inspect -f "{{ .Config.Env }}" c3f279d17e0a + [HOME=/ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin] + $ docker commit --change "ENV DEBUG true" c3f279d17e0a svendowideit/testimage:version3 + f5283438590d + $ docker inspect -f "{{ .Config.Env }}" f5283438590d + [HOME=/ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin DEBUG=true] + +## Commit a container with new `CMD` and `EXPOSE` instructions + + $ docker ps + ID IMAGE COMMAND CREATED STATUS PORTS + c3f279d17e0a ubuntu:12.04 /bin/bash 7 days ago Up 25 hours + 197387f1b436 ubuntu:12.04 /bin/bash 7 days ago Up 25 hours + + $ docker commit --change='CMD ["apachectl", "-DFOREGROUND"]' -c "EXPOSE 80" c3f279d17e0a svendowideit/testimage:version4 + f5283438590d + + $ docker run -d svendowideit/testimage:version4 + 89373736e2e7f00bc149bd783073ac43d0507da250e999f3f1036e0db60817c0 + + $ docker ps + ID IMAGE COMMAND CREATED STATUS PORTS + 89373736e2e7 testimage:version4 "apachectl -DFOREGROU" 3 seconds ago Up 2 seconds 80/tcp + c3f279d17e0a ubuntu:12.04 /bin/bash 7 days ago Up 25 hours + 197387f1b436 ubuntu:12.04 /bin/bash 7 days ago Up 25 hours diff --git a/docs/reference/commandline/cp.md b/docs/reference/commandline/cp.md new file mode 100644 index 00000000..841aeb36 --- /dev/null +++ b/docs/reference/commandline/cp.md @@ -0,0 +1,89 @@ + + +# cp + + Usage: docker cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH | - + docker cp [OPTIONS] SRC_PATH | - CONTAINER:DEST_PATH + + Copy files/folders between a container and the local filesystem + + -L, --follow-link Always follow symbol link in SRC_PATH + --help Print usage + +The `docker cp` utility copies the contents of `SRC_PATH` to the `DEST_PATH`. +You can copy from the container's file system to the local machine or the +reverse, from the local filesystem to the container. If `-` is specified for +either the `SRC_PATH` or `DEST_PATH`, you can also stream a tar archive from +`STDIN` or to `STDOUT`. The `CONTAINER` can be a running or stopped container. +The `SRC_PATH` or `DEST_PATH` can be a file or directory. + +The `docker cp` command assumes container paths are relative to the container's +`/` (root) directory. This means supplying the initial forward slash is optional; +The command sees `compassionate_darwin:/tmp/foo/myfile.txt` and +`compassionate_darwin:tmp/foo/myfile.txt` as identical. Local machine paths can +be an absolute or relative value. The command interprets a local machine's +relative paths as relative to the current working directory where `docker cp` is +run. + +The `cp` command behaves like the Unix `cp -a` command in that directories are +copied recursively with permissions preserved if possible. Ownership is set to +the user and primary group at the destination. For example, files copied to a +container are created with `UID:GID` of the root user. Files copied to the local +machine are created with the `UID:GID` of the user which invoked the `docker cp` +command. If you specify the `-L` option, `docker cp` follows any symbolic link +in the `SRC_PATH`. `docker cp` does *not* create parent directories for +`DEST_PATH` if they do not exist. + +Assuming a path separator of `/`, a first argument of `SRC_PATH` and second +argument of `DEST_PATH`, the behavior is as follows: + +- `SRC_PATH` specifies a file + - `DEST_PATH` does not exist + - the file is saved to a file created at `DEST_PATH` + - `DEST_PATH` does not exist and ends with `/` + - Error condition: the destination directory must exist. + - `DEST_PATH` exists and is a file + - the destination is overwritten with the source file's contents + - `DEST_PATH` exists and is a directory + - the file is copied into this directory using the basename from + `SRC_PATH` +- `SRC_PATH` specifies a directory + - `DEST_PATH` does not exist + - `DEST_PATH` is created as a directory and the *contents* of the source + directory are copied into this directory + - `DEST_PATH` exists and is a file + - Error condition: cannot copy a directory to a file + - `DEST_PATH` exists and is a directory + - `SRC_PATH` does not end with `/.` + - the source directory is copied into this directory + - `SRC_PATH` does end with `/.` + - the *content* of the source directory is copied into this + directory + +The command requires `SRC_PATH` and `DEST_PATH` to exist according to the above +rules. If `SRC_PATH` is local and is a symbolic link, the symbolic link, not +the target, is copied by default. To copy the link target and not the link, specify +the `-L` option. + +A colon (`:`) is used as a delimiter between `CONTAINER` and its path. You can +also use `:` when specifying paths to a `SRC_PATH` or `DEST_PATH` on a local +machine, for example `file:name.txt`. If you use a `:` in a local machine path, +you must be explicit with a relative or absolute path, for example: + + `/path/to/file:name.txt` or `./file:name.txt` + +It is not possible to copy certain system files such as resources under +`/proc`, `/sys`, `/dev`, and mounts created by the user in the container. + +Using `-` as the `SRC_PATH` streams the contents of `STDIN` as a tar archive. +The command extracts the content of the tar to the `DEST_PATH` in container's +filesystem. In this case, `DEST_PATH` must specify a directory. Using `-` as +the `DEST_PATH` streams the contents of the resource as a tar archive to `STDOUT`. diff --git a/docs/reference/commandline/create.md b/docs/reference/commandline/create.md new file mode 100644 index 00000000..70c0e4c3 --- /dev/null +++ b/docs/reference/commandline/create.md @@ -0,0 +1,162 @@ + + +# create + +Creates a new container. + + Usage: docker create [OPTIONS] IMAGE [COMMAND] [ARG...] + + Create a new container + + -a, --attach=[] Attach to STDIN, STDOUT or STDERR + --add-host=[] Add a custom host-to-IP mapping (host:ip) + --blkio-weight=0 Block IO weight (relative weight) + --blkio-weight-device=[] Block IO weight (relative device weight, format: `DEVICE_NAME:WEIGHT`) + --cpu-shares=0 CPU shares (relative weight) + --cap-add=[] Add Linux capabilities + --cap-drop=[] Drop Linux capabilities + --cgroup-parent="" Optional parent cgroup for the container + --cidfile="" Write the container ID to the file + --cpu-period=0 Limit CPU CFS (Completely Fair Scheduler) period + --cpu-quota=0 Limit CPU CFS (Completely Fair Scheduler) quota + --cpuset-cpus="" CPUs in which to allow execution (0-3, 0,1) + --cpuset-mems="" Memory nodes (MEMs) in which to allow execution (0-3, 0,1) + --device=[] Add a host device to the container + --device-read-bps=[] Limit read rate (bytes per second) from a device (e.g., --device-read-bps=/dev/sda:1mb) + --device-read-iops=[] Limit read rate (IO per second) from a device (e.g., --device-read-iops=/dev/sda:1000) + --device-write-bps=[] Limit write rate (bytes per second) to a device (e.g., --device-write-bps=/dev/sda:1mb) + --device-write-iops=[] Limit write rate (IO per second) to a device (e.g., --device-write-iops=/dev/sda:1000) + --disable-content-trust=true Skip image verification + --dns=[] Set custom DNS servers + --dns-opt=[] Set custom DNS options + --dns-search=[] Set custom DNS search domains + -e, --env=[] Set environment variables + --entrypoint="" Overwrite the default ENTRYPOINT of the image + --env-file=[] Read in a file of environment variables + --expose=[] Expose a port or a range of ports + --group-add=[] Add additional groups to join + -h, --hostname="" Container host name + --help Print usage + -i, --interactive Keep STDIN open even if not attached + --ip="" Container IPv4 address (e.g. 172.30.100.104) + --ip6="" Container IPv6 address (e.g. 2001:db8::33) + --ipc="" IPC namespace to use + --isolation="" Container isolation technology + --kernel-memory="" Kernel memory limit + -l, --label=[] Set metadata on the container (e.g., --label=com.example.key=value) + --label-file=[] Read in a line delimited file of labels + --link=[] Add link to another container + --log-driver="" Logging driver for container + --log-opt=[] Log driver specific options + -m, --memory="" Memory limit + --mac-address="" Container MAC address (e.g. 92:d0:c6:0a:29:33) + --memory-reservation="" Memory soft limit + --memory-swap="" A positive integer equal to memory plus swap. Specify -1 to enable unlimited swap. + --memory-swappiness="" Tune a container's memory swappiness behavior. Accepts an integer between 0 and 100. + --name="" Assign a name to the container + --net="bridge" Connect a container to a network + 'bridge': create a network stack on the default Docker bridge + 'none': no networking + 'container:': reuse another container's network stack + 'host': use the Docker host network stack + '|': connect to a user-defined network + --net-alias=[] Add network-scoped alias for the container + --oom-kill-disable Whether to disable OOM Killer for the container or not + --oom-score-adj=0 Tune the host's OOM preferences for containers (accepts -1000 to 1000) + -P, --publish-all Publish all exposed ports to random ports + -p, --publish=[] Publish a container's port(s) to the host + --pid="" PID namespace to use + --pids-limit=-1 Tune container pids limit (set -1 for unlimited), kernel >= 4.3 + --privileged Give extended privileges to this container + --read-only Mount the container's root filesystem as read only + --restart="no" Restart policy (no, on-failure[:max-retry], always, unless-stopped) + --security-opt=[] Security options + --stop-signal="SIGTERM" Signal to stop a container + --shm-size=[] Size of `/dev/shm`. The format is ``. `number` must be greater than `0`. Unit is optional and can be `b` (bytes), `k` (kilobytes), `m` (megabytes), or `g` (gigabytes). If you omit the unit, the system uses bytes. If you omit the size entirely, the system uses `64m`. + -t, --tty Allocate a pseudo-TTY + -u, --user="" Username or UID + --userns="" Container user namespace + 'host': Use the Docker host user namespace + '': Use the Docker daemon user namespace specified by `--userns-remap` option. + --ulimit=[] Ulimit options + --uts="" UTS namespace to use + -v, --volume=[host-src:]container-dest[:] + Bind mount a volume. The comma-delimited + `options` are [rw|ro], [z|Z], + [[r]shared|[r]slave|[r]private], and + [nocopy]. The 'host-src' is an absolute path + or a name value. + --volume-driver="" Container's volume driver + --volumes-from=[] Mount volumes from the specified container(s) + -w, --workdir="" Working directory inside the container + +The `docker create` command creates a writeable container layer over the +specified image and prepares it for running the specified command. The +container ID is then printed to `STDOUT`. This is similar to `docker run -d` +except the container is never started. You can then use the +`docker start ` command to start the container at any point. + +This is useful when you want to set up a container configuration ahead of time +so that it is ready to start when you need it. The initial status of the +new container is `created`. + +Please see the [run command](run.md) section and the [Docker run reference](../run.md) for more details. + +## Examples + + $ docker create -t -i fedora bash + 6d8af538ec541dd581ebc2a24153a28329acb5268abe5ef868c1f1a261221752 + $ docker start -a -i 6d8af538ec5 + bash-4.2# + +As of v1.4.0 container volumes are initialized during the `docker create` phase +(i.e., `docker run` too). For example, this allows you to `create` the `data` +volume container, and then use it from another container: + + $ docker create -v /data --name data ubuntu + 240633dfbb98128fa77473d3d9018f6123b99c454b3251427ae190a7d951ad57 + $ docker run --rm --volumes-from data ubuntu ls -la /data + total 8 + drwxr-xr-x 2 root root 4096 Dec 5 04:10 . + drwxr-xr-x 48 root root 4096 Dec 5 04:11 .. + +Similarly, `create` a host directory bind mounted volume container, which can +then be used from the subsequent container: + + $ docker create -v /home/docker:/docker --name docker ubuntu + 9aa88c08f319cd1e4515c3c46b0de7cc9aa75e878357b1e96f91e2c773029f03 + $ docker run --rm --volumes-from docker ubuntu ls -la /docker + total 20 + drwxr-sr-x 5 1000 staff 180 Dec 5 04:00 . + drwxr-xr-x 48 root root 4096 Dec 5 04:13 .. + -rw-rw-r-- 1 1000 staff 3833 Dec 5 04:01 .ash_history + -rw-r--r-- 1 1000 staff 446 Nov 28 11:51 .ashrc + -rw-r--r-- 1 1000 staff 25 Dec 5 04:00 .gitconfig + drwxr-sr-x 3 1000 staff 60 Dec 1 03:28 .local + -rw-r--r-- 1 1000 staff 920 Nov 28 11:51 .profile + drwx--S--- 2 1000 staff 460 Dec 5 00:51 .ssh + drwxr-xr-x 32 1000 staff 1140 Dec 5 04:01 docker + +### Specify isolation technology for container (--isolation) + +This option is useful in situations where you are running Docker containers on +Windows. The `--isolation=` option sets a container's isolation +technology. On Linux, the only supported is the `default` option which uses +Linux namespaces. On Microsoft Windows, you can specify these values: + + +| Value | Description | +|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `default` | Use the value specified by the Docker daemon's `--exec-opt` . If the `daemon` does not specify an isolation technology, Microsoft Windows uses `process` as its default value. | +| `process` | Namespace isolation only. | +| `hyperv` | Hyper-V hypervisor partition-based isolation. | + +Specifying the `--isolation` flag without a value is the same as setting `--isolation="default"`. diff --git a/docs/reference/commandline/daemon.md b/docs/reference/commandline/daemon.md new file mode 100644 index 00000000..796a04c3 --- /dev/null +++ b/docs/reference/commandline/daemon.md @@ -0,0 +1,958 @@ + + +# daemon + + Usage: docker daemon [OPTIONS] + + A self-sufficient runtime for linux containers. + + Options: + --api-cors-header="" Set CORS headers in the remote API + --authorization-plugin=[] Set authorization plugins to load + -b, --bridge="" Attach containers to a network bridge + --bip="" Specify network bridge IP + --cgroup-parent= Set parent cgroup for all containers + --cluster-store="" URL of the distributed storage backend + --cluster-advertise="" Address of the daemon instance on the cluster + --cluster-store-opt=map[] Set cluster options + --config-file=/etc/docker/daemon.json Daemon configuration file + --containerd Path to containerd socket + -D, --debug Enable debug mode + --default-gateway="" Container default gateway IPv4 address + --default-gateway-v6="" Container default gateway IPv6 address + --dns=[] DNS server to use + --dns-opt=[] DNS options to use + --dns-search=[] DNS search domains to use + --default-ulimit=[] Set default ulimit settings for containers + --exec-opt=[] Set runtime execution options + --exec-root="/var/run/docker" Root directory for execution state files + --fixed-cidr="" IPv4 subnet for fixed IPs + --fixed-cidr-v6="" IPv6 subnet for fixed IPs + -G, --group="docker" Group for the unix socket + -g, --graph="/var/lib/docker" Root of the Docker runtime + -H, --host=[] Daemon socket(s) to connect to + --help Print usage + --icc=true Enable inter-container communication + --insecure-registry=[] Enable insecure registry communication + --ip=0.0.0.0 Default IP when binding container ports + --ip-forward=true Enable net.ipv4.ip_forward + --ip-masq=true Enable IP masquerading + --iptables=true Enable addition of iptables rules + --ipv6 Enable IPv6 networking + -l, --log-level="info" Set the logging level + --label=[] Set key=value labels to the daemon + --log-driver="json-file" Default driver for container logs + --log-opt=[] Log driver specific options + --mtu=0 Set the containers network MTU + --disable-legacy-registry Do not contact legacy registries + -p, --pidfile="/var/run/docker.pid" Path to use for daemon PID file + --raw-logs Full timestamps without ANSI coloring + --registry-mirror=[] Preferred Docker registry mirror + -s, --storage-driver="" Storage driver to use + --selinux-enabled Enable selinux support + --storage-opt=[] Set storage driver options + --tls Use TLS; implied by --tlsverify + --tlscacert="~/.docker/ca.pem" Trust certs signed only by this CA + --tlscert="~/.docker/cert.pem" Path to TLS certificate file + --tlskey="~/.docker/key.pem" Path to TLS key file + --tlsverify Use TLS and verify the remote + --userns-remap="default" Enable user namespace remapping + --userland-proxy=true Use userland proxy for loopback traffic + +Options with [] may be specified multiple times. + +The Docker daemon is the persistent process that manages containers. Docker +uses the same binary for both the daemon and client. To run the daemon you +type `docker daemon`. + +To run the daemon with debug output, use `docker daemon -D`. + +## Daemon socket option + +The Docker daemon can listen for [Docker Remote API](../api/docker_remote_api.md) +requests via three different types of Socket: `unix`, `tcp`, and `fd`. + +By default, a `unix` domain socket (or IPC socket) is created at +`/var/run/docker.sock`, requiring either `root` permission, or `docker` group +membership. + +If you need to access the Docker daemon remotely, you need to enable the `tcp` +Socket. Beware that the default setup provides un-encrypted and +un-authenticated direct access to the Docker daemon - and should be secured +either using the [built in HTTPS encrypted socket](../../security/https/), or by +putting a secure web proxy in front of it. You can listen on port `2375` on all +network interfaces with `-H tcp://0.0.0.0:2375`, or on a particular network +interface using its IP address: `-H tcp://192.168.59.103:2375`. It is +conventional to use port `2375` for un-encrypted, and port `2376` for encrypted +communication with the daemon. + +> **Note:** +> If you're using an HTTPS encrypted socket, keep in mind that only +> TLS1.0 and greater are supported. Protocols SSLv3 and under are not +> supported anymore for security reasons. + +On Systemd based systems, you can communicate with the daemon via +[Systemd socket activation](http://0pointer.de/blog/projects/socket-activation.html), +use `docker daemon -H fd://`. Using `fd://` will work perfectly for most setups but +you can also specify individual sockets: `docker daemon -H fd://3`. If the +specified socket activated files aren't found, then Docker will exit. You can +find examples of using Systemd socket activation with Docker and Systemd in the +[Docker source tree](https://github.com/docker/docker/tree/master/contrib/init/systemd/). + +You can configure the Docker daemon to listen to multiple sockets at the same +time using multiple `-H` options: + + # listen using the default unix socket, and on 2 specific IP addresses on this host. + docker daemon -H unix:///var/run/docker.sock -H tcp://192.168.59.106 -H tcp://10.10.10.2 + +The Docker client will honor the `DOCKER_HOST` environment variable to set the +`-H` flag for the client. + + $ docker -H tcp://0.0.0.0:2375 ps + # or + $ export DOCKER_HOST="tcp://0.0.0.0:2375" + $ docker ps + # both are equal + +Setting the `DOCKER_TLS_VERIFY` environment variable to any value other than +the empty string is equivalent to setting the `--tlsverify` flag. The following +are equivalent: + + $ docker --tlsverify ps + # or + $ export DOCKER_TLS_VERIFY=1 + $ docker ps + +The Docker client will honor the `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` +environment variables (or the lowercase versions thereof). `HTTPS_PROXY` takes +precedence over `HTTP_PROXY`. + +### Daemon storage-driver option + +The Docker daemon has support for several different image layer storage +drivers: `aufs`, `devicemapper`, `btrfs`, `zfs` and `overlay`. + +The `aufs` driver is the oldest, but is based on a Linux kernel patch-set that +is unlikely to be merged into the main kernel. These are also known to cause +some serious kernel crashes. However, `aufs` is also the only storage driver +that allows containers to share executable and shared library memory, so is a +useful choice when running thousands of containers with the same program or +libraries. + +The `devicemapper` driver uses thin provisioning and Copy on Write (CoW) +snapshots. For each devicemapper graph location – typically +`/var/lib/docker/devicemapper` – a thin pool is created based on two block +devices, one for data and one for metadata. By default, these block devices +are created automatically by using loopback mounts of automatically created +sparse files. Refer to [Storage driver options](#storage-driver-options) below +for a way how to customize this setup. +[~jpetazzo/Resizing Docker containers with the Device Mapper plugin](http://jpetazzo.github.io/2014/01/29/docker-device-mapper-resize/) +article explains how to tune your existing setup without the use of options. + +The `btrfs` driver is very fast for `docker build` - but like `devicemapper` +does not share executable memory between devices. Use +`docker daemon -s btrfs -g /mnt/btrfs_partition`. + +The `zfs` driver is probably not as fast as `btrfs` but has a longer track record +on stability. Thanks to `Single Copy ARC` shared blocks between clones will be +cached only once. Use `docker daemon -s zfs`. To select a different zfs filesystem +set `zfs.fsname` option as described in [Storage driver options](#storage-driver-options). + +The `overlay` is a very fast union filesystem. It is now merged in the main +Linux kernel as of [3.18.0](https://lkml.org/lkml/2014/10/26/137). Call +`docker daemon -s overlay` to use it. + +> **Note:** +> As promising as `overlay` is, the feature is still quite young and should not +> be used in production. Most notably, using `overlay` can cause excessive +> inode consumption (especially as the number of images grows), as well as +> being incompatible with the use of RPMs. + +> **Note:** +> It is currently unsupported on `btrfs` or any Copy on Write filesystem +> and should only be used over `ext4` partitions. + +### Storage driver options + +Particular storage-driver can be configured with options specified with +`--storage-opt` flags. Options for `devicemapper` are prefixed with `dm` and +options for `zfs` start with `zfs`. + +* `dm.thinpooldev` + + Specifies a custom block storage device to use for the thin pool. + + If using a block device for device mapper storage, it is best to use `lvm` + to create and manage the thin-pool volume. This volume is then handed to Docker + to exclusively create snapshot volumes needed for images and containers. + + Managing the thin-pool outside of Engine makes for the most feature-rich + method of having Docker utilize device mapper thin provisioning as the + backing storage for Docker containers. The highlights of the lvm-based + thin-pool management feature include: automatic or interactive thin-pool + resize support, dynamically changing thin-pool features, automatic thinp + metadata checking when lvm activates the thin-pool, etc. + + As a fallback if no thin pool is provided, loopback files are + created. Loopback is very slow, but can be used without any + pre-configuration of storage. It is strongly recommended that you do + not use loopback in production. Ensure your Engine daemon has a + `--storage-opt dm.thinpooldev` argument provided. + + Example use: + + $ docker daemon \ + --storage-opt dm.thinpooldev=/dev/mapper/thin-pool + +* `dm.basesize` + + Specifies the size to use when creating the base device, which limits the + size of images and containers. The default value is 10G. Note, thin devices + are inherently "sparse", so a 10G device which is mostly empty doesn't use + 10 GB of space on the pool. However, the filesystem will use more space for + the empty case the larger the device is. + + The base device size can be increased at daemon restart which will allow + all future images and containers (based on those new images) to be of the + new base device size. + + Example use: + + $ docker daemon --storage-opt dm.basesize=50G + + This will increase the base device size to 50G. The Docker daemon will throw an + error if existing base device size is larger than 50G. A user can use + this option to expand the base device size however shrinking is not permitted. + + This value affects the system-wide "base" empty filesystem + that may already be initialized and inherited by pulled images. Typically, + a change to this value requires additional steps to take effect: + + $ sudo service docker stop + $ sudo rm -rf /var/lib/docker + $ sudo service docker start + + Example use: + + $ docker daemon --storage-opt dm.basesize=20G + +* `dm.loopdatasize` + + > **Note**: + > This option configures devicemapper loopback, which should not + > be used in production. + + Specifies the size to use when creating the loopback file for the + "data" device which is used for the thin pool. The default size is + 100G. The file is sparse, so it will not initially take up this + much space. + + Example use: + + $ docker daemon --storage-opt dm.loopdatasize=200G + +* `dm.loopmetadatasize` + + > **Note**: + > This option configures devicemapper loopback, which should not + > be used in production. + + Specifies the size to use when creating the loopback file for the + "metadata" device which is used for the thin pool. The default size + is 2G. The file is sparse, so it will not initially take up + this much space. + + Example use: + + $ docker daemon --storage-opt dm.loopmetadatasize=4G + +* `dm.fs` + + Specifies the filesystem type to use for the base device. The supported + options are "ext4" and "xfs". The default is "xfs" + + Example use: + + $ docker daemon --storage-opt dm.fs=ext4 + +* `dm.mkfsarg` + + Specifies extra mkfs arguments to be used when creating the base device. + + Example use: + + $ docker daemon --storage-opt "dm.mkfsarg=-O ^has_journal" + +* `dm.mountopt` + + Specifies extra mount options used when mounting the thin devices. + + Example use: + + $ docker daemon --storage-opt dm.mountopt=nodiscard + +* `dm.datadev` + + (Deprecated, use `dm.thinpooldev`) + + Specifies a custom blockdevice to use for data for the thin pool. + + If using a block device for device mapper storage, ideally both datadev and + metadatadev should be specified to completely avoid using the loopback + device. + + Example use: + + $ docker daemon \ + --storage-opt dm.datadev=/dev/sdb1 \ + --storage-opt dm.metadatadev=/dev/sdc1 + +* `dm.metadatadev` + + (Deprecated, use `dm.thinpooldev`) + + Specifies a custom blockdevice to use for metadata for the thin pool. + + For best performance the metadata should be on a different spindle than the + data, or even better on an SSD. + + If setting up a new metadata pool it is required to be valid. This can be + achieved by zeroing the first 4k to indicate empty metadata, like this: + + $ dd if=/dev/zero of=$metadata_dev bs=4096 count=1 + + Example use: + + $ docker daemon \ + --storage-opt dm.datadev=/dev/sdb1 \ + --storage-opt dm.metadatadev=/dev/sdc1 + +* `dm.blocksize` + + Specifies a custom blocksize to use for the thin pool. The default + blocksize is 64K. + + Example use: + + $ docker daemon --storage-opt dm.blocksize=512K + +* `dm.blkdiscard` + + Enables or disables the use of blkdiscard when removing devicemapper + devices. This is enabled by default (only) if using loopback devices and is + required to resparsify the loopback file on image/container removal. + + Disabling this on loopback can lead to *much* faster container removal + times, but will make the space used in `/var/lib/docker` directory not be + returned to the system for other use when containers are removed. + + Example use: + + $ docker daemon --storage-opt dm.blkdiscard=false + +* `dm.override_udev_sync_check` + + Overrides the `udev` synchronization checks between `devicemapper` and `udev`. + `udev` is the device manager for the Linux kernel. + + To view the `udev` sync support of a Docker daemon that is using the + `devicemapper` driver, run: + + $ docker info + [...] + Udev Sync Supported: true + [...] + + When `udev` sync support is `true`, then `devicemapper` and udev can + coordinate the activation and deactivation of devices for containers. + + When `udev` sync support is `false`, a race condition occurs between + the`devicemapper` and `udev` during create and cleanup. The race condition + results in errors and failures. (For information on these failures, see + [docker#4036](https://github.com/docker/docker/issues/4036)) + + To allow the `docker` daemon to start, regardless of `udev` sync not being + supported, set `dm.override_udev_sync_check` to true: + + $ docker daemon --storage-opt dm.override_udev_sync_check=true + + When this value is `true`, the `devicemapper` continues and simply warns + you the errors are happening. + + > **Note:** + > The ideal is to pursue a `docker` daemon and environment that does + > support synchronizing with `udev`. For further discussion on this + > topic, see [docker#4036](https://github.com/docker/docker/issues/4036). + > Otherwise, set this flag for migrating existing Docker daemons to + > a daemon with a supported environment. + +* `dm.use_deferred_removal` + + Enables use of deferred device removal if `libdm` and the kernel driver + support the mechanism. + + Deferred device removal means that if device is busy when devices are + being removed/deactivated, then a deferred removal is scheduled on + device. And devices automatically go away when last user of the device + exits. + + For example, when a container exits, its associated thin device is removed. + If that device has leaked into some other mount namespace and can't be + removed, the container exit still succeeds and this option causes the + system to schedule the device for deferred removal. It does not wait in a + loop trying to remove a busy device. + + Example use: + + $ docker daemon --storage-opt dm.use_deferred_removal=true + +* `dm.use_deferred_deletion` + + Enables use of deferred device deletion for thin pool devices. By default, + thin pool device deletion is synchronous. Before a container is deleted, + the Docker daemon removes any associated devices. If the storage driver + can not remove a device, the container deletion fails and daemon returns. + + Error deleting container: Error response from daemon: Cannot destroy container + + To avoid this failure, enable both deferred device deletion and deferred + device removal on the daemon. + + $ docker daemon \ + --storage-opt dm.use_deferred_deletion=true \ + --storage-opt dm.use_deferred_removal=true + + With these two options enabled, if a device is busy when the driver is + deleting a container, the driver marks the device as deleted. Later, when + the device isn't in use, the driver deletes it. + + In general it should be safe to enable this option by default. It will help + when unintentional leaking of mount point happens across multiple mount + namespaces. + +* `dm.min_free_space` + + Specifies the min free space percent in a thin pool require for new device + creation to succeed. This check applies to both free data space as well + as free metadata space. Valid values are from 0% - 99%. Value 0% disables + free space checking logic. If user does not specify a value for this option, + the Engine uses a default value of 10%. + + Whenever a new a thin pool device is created (during `docker pull` or during + container creation), the Engine checks if the minimum free space is + available. If sufficient space is unavailable, then device creation fails + and any relevant `docker` operation fails. + + To recover from this error, you must create more free space in the thin pool + to recover from the error. You can create free space by deleting some images + and containers from the thin pool. You can also add more storage to the thin + pool. + + To add more space to a LVM (logical volume management) thin pool, just add + more storage to the volume group container thin pool; this should automatically + resolve any errors. If your configuration uses loop devices, then stop the + Engine daemon, grow the size of loop files and restart the daemon to resolve + the issue. + + Example use: + + ```bash + $ docker daemon --storage-opt dm.min_free_space=10% + ``` + +Currently supported options of `zfs`: + +* `zfs.fsname` + + Set zfs filesystem under which docker will create its own datasets. + By default docker will pick up the zfs filesystem where docker graph + (`/var/lib/docker`) is located. + + Example use: + + $ docker daemon -s zfs --storage-opt zfs.fsname=zroot/docker + +## Docker runtime execution options + +The Docker daemon relies on a +[OCI](https://github.com/opencontainers/specs) compliant runtime +(invoked via the `containerd` daemon) as its interface to the Linux +kernel `namespaces`, `cgroups`, and `SELinux`. + +## Options for the runtime + +You can configure the runtime using options specified +with the `--exec-opt` flag. All the flag's options have the `native` prefix. A +single `native.cgroupdriver` option is available. + +The `native.cgroupdriver` option specifies the management of the container's +cgroups. You can specify only specify `cgroupfs` or `systemd`. If you specify +`systemd` and it is not available, the system errors out. If you omit the +`native.cgroupdriver` option,` cgroupfs` is used. + +This example sets the `cgroupdriver` to `systemd`: + + $ sudo docker daemon --exec-opt native.cgroupdriver=systemd + +Setting this option applies to all containers the daemon launches. + +Also Windows Container makes use of `--exec-opt` for special purpose. Docker user +can specify default container isolation technology with this, for example: + + $ docker daemon --exec-opt isolation=hyperv + +Will make `hyperv` the default isolation technology on Windows, without specifying +isolation value on daemon start, Windows isolation technology will default to `process`. + +## Daemon DNS options + +To set the DNS server for all Docker containers, use +`docker daemon --dns 8.8.8.8`. + +To set the DNS search domain for all Docker containers, use +`docker daemon --dns-search example.com`. + +## Insecure registries + +Docker considers a private registry either secure or insecure. In the rest of +this section, *registry* is used for *private registry*, and `myregistry:5000` +is a placeholder example for a private registry. + +A secure registry uses TLS and a copy of its CA certificate is placed on the +Docker host at `/etc/docker/certs.d/myregistry:5000/ca.crt`. An insecure +registry is either not using TLS (i.e., listening on plain text HTTP), or is +using TLS with a CA certificate not known by the Docker daemon. The latter can +happen when the certificate was not found under +`/etc/docker/certs.d/myregistry:5000/`, or if the certificate verification +failed (i.e., wrong CA). + +By default, Docker assumes all, but local (see local registries below), +registries are secure. Communicating with an insecure registry is not possible +if Docker assumes that registry is secure. In order to communicate with an +insecure registry, the Docker daemon requires `--insecure-registry` in one of +the following two forms: + +* `--insecure-registry myregistry:5000` tells the Docker daemon that + myregistry:5000 should be considered insecure. +* `--insecure-registry 10.1.0.0/16` tells the Docker daemon that all registries + whose domain resolve to an IP address is part of the subnet described by the + CIDR syntax, should be considered insecure. + +The flag can be used multiple times to allow multiple registries to be marked +as insecure. + +If an insecure registry is not marked as insecure, `docker pull`, +`docker push`, and `docker search` will result in an error message prompting +the user to either secure or pass the `--insecure-registry` flag to the Docker +daemon as described above. + +Local registries, whose IP address falls in the 127.0.0.0/8 range, are +automatically marked as insecure as of Docker 1.3.2. It is not recommended to +rely on this, as it may change in the future. + +Enabling `--insecure-registry`, i.e., allowing un-encrypted and/or untrusted +communication, can be useful when running a local registry. However, +because its use creates security vulnerabilities it should ONLY be enabled for +testing purposes. For increased security, users should add their CA to their +system's list of trusted CAs instead of enabling `--insecure-registry`. + +## Legacy Registries + +Enabling `--disable-legacy-registry` forces a docker daemon to only interact with registries which support the V2 protocol. Specifically, the daemon will not attempt `push`, `pull` and `login` to v1 registries. The exception to this is `search` which can still be performed on v1 registries. + +## Running a Docker daemon behind a HTTPS_PROXY + +When running inside a LAN that uses a `HTTPS` proxy, the Docker Hub +certificates will be replaced by the proxy's certificates. These certificates +need to be added to your Docker host's configuration: + +1. Install the `ca-certificates` package for your distribution +2. Ask your network admin for the proxy's CA certificate and append them to + `/etc/pki/tls/certs/ca-bundle.crt` +3. Then start your Docker daemon with `HTTPS_PROXY=http://username:password@proxy:port/ docker daemon`. + The `username:` and `password@` are optional - and are only needed if your + proxy is set up to require authentication. + +This will only add the proxy and authentication to the Docker daemon's requests - +your `docker build`s and running containers will need extra configuration to +use the proxy + +## Default Ulimits + +`--default-ulimit` allows you to set the default `ulimit` options to use for +all containers. It takes the same options as `--ulimit` for `docker run`. If +these defaults are not set, `ulimit` settings will be inherited, if not set on +`docker run`, from the Docker daemon. Any `--ulimit` options passed to +`docker run` will overwrite these defaults. + +Be careful setting `nproc` with the `ulimit` flag as `nproc` is designed by Linux to +set the maximum number of processes available to a user, not to a container. For details +please check the [run](run.md) reference. + +## Nodes discovery + +The `--cluster-advertise` option specifies the `host:port` or `interface:port` +combination that this particular daemon instance should use when advertising +itself to the cluster. The daemon is reached by remote hosts through this value. +If you specify an interface, make sure it includes the IP address of the actual +Docker host. For Engine installation created through `docker-machine`, the +interface is typically `eth1`. + +The daemon uses [libkv](https://github.com/docker/libkv/) to advertise +the node within the cluster. Some key-value backends support mutual +TLS. To configure the client TLS settings used by the daemon can be configured +using the `--cluster-store-opt` flag, specifying the paths to PEM encoded +files. For example: + +```bash +docker daemon \ + --cluster-advertise 192.168.1.2:2376 \ + --cluster-store etcd://192.168.1.2:2379 \ + --cluster-store-opt kv.cacertfile=/path/to/ca.pem \ + --cluster-store-opt kv.certfile=/path/to/cert.pem \ + --cluster-store-opt kv.keyfile=/path/to/key.pem +``` + +The currently supported cluster store options are: + +* `discovery.heartbeat` + + Specifies the heartbeat timer in seconds which is used by the daemon as a + keepalive mechanism to make sure discovery module treats the node as alive + in the cluster. If not configured, the default value is 20 seconds. + +* `discovery.ttl` + + Specifies the ttl (time-to-live) in seconds which is used by the discovery + module to timeout a node if a valid heartbeat is not received within the + configured ttl value. If not configured, the default value is 60 seconds. + +* `kv.cacertfile` + + Specifies the path to a local file with PEM encoded CA certificates to trust + +* `kv.certfile` + + Specifies the path to a local file with a PEM encoded certificate. This + certificate is used as the client cert for communication with the + Key/Value store. + +* `kv.keyfile` + + Specifies the path to a local file with a PEM encoded private key. This + private key is used as the client key for communication with the + Key/Value store. + +* `kv.path` + + Specifies the path in the Key/Value store. If not configured, the default value is 'docker/nodes'. + +## Access authorization + +Docker's access authorization can be extended by authorization plugins that your +organization can purchase or build themselves. You can install one or more +authorization plugins when you start the Docker `daemon` using the +`--authorization-plugin=PLUGIN_ID` option. + +```bash +docker daemon --authorization-plugin=plugin1 --authorization-plugin=plugin2,... +``` + +The `PLUGIN_ID` value is either the plugin's name or a path to its specification +file. The plugin's implementation determines whether you can specify a name or +path. Consult with your Docker administrator to get information about the +plugins available to you. + +Once a plugin is installed, requests made to the `daemon` through the command +line or Docker's remote API are allowed or denied by the plugin. If you have +multiple plugins installed, at least one must allow the request for it to +complete. + +For information about how to create an authorization plugin, see [authorization +plugin](../../extend/plugins_authorization.md) section in the Docker extend section of this documentation. + + +## Daemon user namespace options + +The Linux kernel [user namespace support](http://man7.org/linux/man-pages/man7/user_namespaces.7.html) provides additional security by enabling +a process, and therefore a container, to have a unique range of user and +group IDs which are outside the traditional user and group range utilized by +the host system. Potentially the most important security improvement is that, +by default, container processes running as the `root` user will have expected +administrative privilege (with some restrictions) inside the container but will +effectively be mapped to an unprivileged `uid` on the host. + +When user namespace support is enabled, Docker creates a single daemon-wide mapping +for all containers running on the same engine instance. The mappings will +utilize the existing subordinate user and group ID feature available on all modern +Linux distributions. +The [`/etc/subuid`](http://man7.org/linux/man-pages/man5/subuid.5.html) and +[`/etc/subgid`](http://man7.org/linux/man-pages/man5/subgid.5.html) files will be +read for the user, and optional group, specified to the `--userns-remap` +parameter. If you do not wish to specify your own user and/or group, you can +provide `default` as the value to this flag, and a user will be created on your behalf +and provided subordinate uid and gid ranges. This default user will be named +`dockremap`, and entries will be created for it in `/etc/passwd` and +`/etc/group` using your distro's standard user and group creation tools. + +> **Note**: The single mapping per-daemon restriction is in place for now +> because Docker shares image layers from its local cache across all +> containers running on the engine instance. Since file ownership must be +> the same for all containers sharing the same layer content, the decision +> was made to map the file ownership on `docker pull` to the daemon's user and +> group mappings so that there is no delay for running containers once the +> content is downloaded. This design preserves the same performance for `docker +> pull`, `docker push`, and container startup as users expect with +> user namespaces disabled. + +### Starting the daemon with user namespaces enabled + +To enable user namespace support, start the daemon with the +`--userns-remap` flag, which accepts values in the following formats: + + - uid + - uid:gid + - username + - username:groupname + +If numeric IDs are provided, translation back to valid user or group names +will occur so that the subordinate uid and gid information can be read, given +these resources are name-based, not id-based. If the numeric ID information +provided does not exist as entries in `/etc/passwd` or `/etc/group`, daemon +startup will fail with an error message. + +> **Note:** On Fedora 22, you have to `touch` the `/etc/subuid` and `/etc/subgid` +> files to have ranges assigned when users are created. This must be done +> *before* the `--userns-remap` option is enabled. Once these files exist, the +> daemon can be (re)started and range assignment on user creation works properly. + +*Example: starting with default Docker user management:* + +```bash +$ docker daemon --userns-remap=default +``` + +When `default` is provided, Docker will create - or find the existing - user and group +named `dockremap`. If the user is created, and the Linux distribution has +appropriate support, the `/etc/subuid` and `/etc/subgid` files will be populated +with a contiguous 65536 length range of subordinate user and group IDs, starting +at an offset based on prior entries in those files. For example, Ubuntu will +create the following range, based on an existing user named `user1` already owning +the first 65536 range: + +```bash +$ cat /etc/subuid +user1:100000:65536 +dockremap:165536:65536 +``` + +If you have a preferred/self-managed user with subordinate ID mappings already +configured, you can provide that username or uid to the `--userns-remap` flag. +If you have a group that doesn't match the username, you may provide the `gid` +or group name as well; otherwise the username will be used as the group name +when querying the system for the subordinate group ID range. + +### Detailed information on `subuid`/`subgid` ranges + +Given potential advanced use of the subordinate ID ranges by power users, the +following paragraphs define how the Docker daemon currently uses the range entries +found within the subordinate range files. + +The simplest case is that only one contiguous range is defined for the +provided user or group. In this case, Docker will use that entire contiguous +range for the mapping of host uids and gids to the container process. This +means that the first ID in the range will be the remapped root user, and the +IDs above that initial ID will map host ID 1 through the end of the range. + +From the example `/etc/subuid` content shown above, the remapped root +user would be uid 165536. + +If the system administrator has set up multiple ranges for a single user or +group, the Docker daemon will read all the available ranges and use the +following algorithm to create the mapping ranges: + +1. The range segments found for the particular user will be sorted by *start ID* ascending. +2. Map segments will be created from each range in increasing value with a length matching the length of each segment. Therefore the range segment with the lowest numeric starting value will be equal to the remapped root, and continue up through host uid/gid equal to the range segment length. As an example, if the lowest segment starts at ID 1000 and has a length of 100, then a map of 1000 -> 0 (the remapped root) up through 1100 -> 100 will be created from this segment. If the next segment starts at ID 10000, then the next map will start with mapping 10000 -> 101 up to the length of this second segment. This will continue until no more segments are found in the subordinate files for this user. +3. If more than five range segments exist for a single user, only the first five will be utilized, matching the kernel's limitation of only five entries in `/proc/self/uid_map` and `proc/self/gid_map`. + +### Disable user namespace for a container + +If you enable user namespaces on the daemon, all containers are started +with user namespaces enabled. In some situations you might want to disable +this feature for a container, for example, to start a privileged container (see +[user namespace known restrictions](#user-namespace-known-restrictions)). +To enable those advanced features for a specific container use `--userns=host` +in the `run/exec/create` command. +This option will completely disable user namespace mapping for the container's user. + +### User namespace known restrictions + +The following standard Docker features are currently incompatible when +running a Docker daemon with user namespaces enabled: + + - sharing PID or NET namespaces with the host (`--pid=host` or `--net=host`) + - A `--readonly` container filesystem (this is a Linux kernel restriction against remounting with modified flags of a currently mounted filesystem when inside a user namespace) + - external (volume or graph) drivers which are unaware/incapable of using daemon user mappings + - Using `--privileged` mode flag on `docker run` (unless also specifying `--userns=host`) + +In general, user namespaces are an advanced feature and will require +coordination with other capabilities. For example, if volumes are mounted from +the host, file ownership will have to be pre-arranged if the user or +administrator wishes the containers to have expected access to the volume +contents. + +Finally, while the `root` user inside a user namespaced container process has +many of the expected admin privileges that go along with being the superuser, the +Linux kernel has restrictions based on internal knowledge that this is a user namespaced +process. The most notable restriction that we are aware of at this time is the +inability to use `mknod`. Permission will be denied for device creation even as +container `root` inside a user namespace. + +## Miscellaneous options + +IP masquerading uses address translation to allow containers without a public +IP to talk to other machines on the Internet. This may interfere with some +network topologies and can be disabled with `--ip-masq=false`. + +Docker supports softlinks for the Docker data directory (`/var/lib/docker`) and +for `/var/lib/docker/tmp`. The `DOCKER_TMPDIR` and the data directory can be +set like this: + + DOCKER_TMPDIR=/mnt/disk2/tmp /usr/local/bin/docker daemon -D -g /var/lib/docker -H unix:// > /var/lib/docker-machine/docker.log 2>&1 + # or + export DOCKER_TMPDIR=/mnt/disk2/tmp + /usr/local/bin/docker daemon -D -g /var/lib/docker -H unix:// > /var/lib/docker-machine/docker.log 2>&1 + + +## Default cgroup parent + +The `--cgroup-parent` option allows you to set the default cgroup parent +to use for containers. If this option is not set, it defaults to `/docker` for +fs cgroup driver and `system.slice` for systemd cgroup driver. + +If the cgroup has a leading forward slash (`/`), the cgroup is created +under the root cgroup, otherwise the cgroup is created under the daemon +cgroup. + +Assuming the daemon is running in cgroup `daemoncgroup`, +`--cgroup-parent=/foobar` creates a cgroup in +`/sys/fs/cgroup/memory/foobar`, whereas using `--cgroup-parent=foobar` +creates the cgroup in `/sys/fs/cgroup/memory/daemoncgroup/foobar` + +The systemd cgroup driver has different rules for `--cgroup-parent`. Systemd +represents hierarchy by slice and the name of the slice encodes the location in +the tree. So `--cgroup-parent` for systemd cgroups should be a slice name. A +name can consist of a dash-separated series of names, which describes the path +to the slice from the root slice. For example, `--cgroup-parent=user-a-b.slice` +means the memory cgroup for the container is created in +`/sys/fs/cgroup/memory/user.slice/user-a.slice/user-a-b.slice/docker-.scope`. + +This setting can also be set per container, using the `--cgroup-parent` +option on `docker create` and `docker run`, and takes precedence over +the `--cgroup-parent` option on the daemon. + +## Daemon configuration file + +The `--config-file` option allows you to set any configuration option +for the daemon in a JSON format. This file uses the same flag names as keys, +except for flags that allow several entries, where it uses the plural +of the flag name, e.g., `labels` for the `label` flag. By default, +docker tries to load a configuration file from `/etc/docker/daemon.json` +on Linux and `%programdata%\docker\config\daemon.json` on Windows. + +The options set in the configuration file must not conflict with options set +via flags. The docker daemon fails to start if an option is duplicated between +the file and the flags, regardless their value. We do this to avoid +silently ignore changes introduced in configuration reloads. +For example, the daemon fails to start if you set daemon labels +in the configuration file and also set daemon labels via the `--label` flag. + +Options that are not present in the file are ignored when the daemon starts. +This is a full example of the allowed configuration options in the file: + +```json +{ + "authorization-plugins": [], + "dns": [], + "dns-opts": [], + "dns-search": [], + "exec-opts": [], + "exec-root": "", + "storage-driver": "", + "storage-opts": "", + "labels": [], + "log-driver": "", + "log-opts": [], + "mtu": 0, + "pidfile": "", + "graph": "", + "cluster-store": "", + "cluster-store-opts": [], + "cluster-advertise": "", + "debug": true, + "hosts": [], + "log-level": "", + "tls": true, + "tlsverify": true, + "tlscacert": "", + "tlscert": "", + "tlskey": "", + "api-cors-headers": "", + "selinux-enabled": false, + "userns-remap": "", + "group": "", + "cgroup-parent": "", + "default-ulimits": {}, + "ipv6": false, + "iptables": false, + "ip-forward": false, + "ip-mask": false, + "userland-proxy": false, + "ip": "0.0.0.0", + "bridge": "", + "bip": "", + "fixed-cidr": "", + "fixed-cidr-v6": "", + "default-gateway": "", + "default-gateway-v6": "", + "icc": false, + "raw-logs": false, + "registry-mirrors": [], + "insecure-registries": [], + "disable-legacy-registry": false +} +``` + +### Configuration reloading + +Some options can be reconfigured when the daemon is running without requiring +to restart the process. We use the `SIGHUP` signal in Linux to reload, and a global event +in Windows with the key `Global\docker-daemon-config-$PID`. The options can +be modified in the configuration file but still will check for conflicts with +the provided flags. The daemon fails to reconfigure itself +if there are conflicts, but it won't stop execution. + +The list of currently supported options that can be reconfigured is this: + +- `debug`: it changes the daemon to debug mode when set to true. +- `cluster-store`: it reloads the discovery store with the new address. +- `cluster-store-opts`: it uses the new options to reload the discovery store. +- `cluster-advertise`: it modifies the address advertised after reloading. +- `labels`: it replaces the daemon labels with a new set of labels. + +Updating and reloading the cluster configurations such as `--cluster-store`, +`--cluster-advertise` and `--cluster-store-opts` will take effect only if +these configurations were not previously configured. If `--cluster-store` +has been provided in flags and `cluster-advertise` not, `cluster-advertise` +can be added in the configuration file without accompanied by `--cluster-store` +Configuration reload will log a warning message if it detects a change in +previously configured cluster configurations. diff --git a/docs/reference/commandline/diff.md b/docs/reference/commandline/diff.md new file mode 100644 index 00000000..bda74ead --- /dev/null +++ b/docs/reference/commandline/diff.md @@ -0,0 +1,40 @@ + + +# diff + + Usage: docker diff [OPTIONS] CONTAINER + + Inspect changes on a container's filesystem + + --help Print usage + +List the changed files and directories in a container᾿s filesystem + There are 3 events that are listed in the `diff`: + +1. `A` - Add +2. `D` - Delete +3. `C` - Change + +For example: + + $ docker diff 7bb0e258aefe + + C /dev + A /dev/kmsg + C /etc + A /etc/mtab + A /go + A /go/src + A /go/src/github.com + A /go/src/github.com/docker + A /go/src/github.com/docker/docker + A /go/src/github.com/docker/docker/.git + .... diff --git a/docs/reference/commandline/docker_images.gif b/docs/reference/commandline/docker_images.gif new file mode 100644 index 0000000000000000000000000000000000000000..5894ca270e002758b8f332141e00356e42868880 GIT binary patch literal 35785 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 ++++ +title = "events" +description = "The events command description and usage" +keywords = ["events, container, report"] +[menu.main] +parent = "smn_cli" ++++ + + +# events + + Usage: docker events [OPTIONS] + + Get real time events from the server + + -f, --filter=[] Filter output based on conditions provided + --help Print usage + --since="" Show all events created since timestamp + --until="" Stream events until this timestamp + +Docker containers report the following events: + + attach, commit, copy, create, destroy, die, exec_create, exec_start, export, kill, oom, pause, rename, resize, restart, start, stop, top, unpause, update + +Docker images report the following events: + + delete, import, pull, push, tag, untag + +Docker volumes report the following events: + + create, mount, unmount, destroy + +Docker networks report the following events: + + create, connect, disconnect, destroy + +The `--since` and `--until` parameters can be Unix timestamps, date formatted +timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed +relative to the client machine’s time. If you do not provide the `--since` option, +the command returns only new and/or live events. Supported formats for date +formatted time stamps include RFC3339Nano, RFC3339, `2006-01-02T15:04:05`, +`2006-01-02T15:04:05.999999999`, `2006-01-02Z07:00`, and `2006-01-02`. The local +timezone on the client will be used if you do not provide either a `Z` or a +`+-00:00` timezone offset at the end of the timestamp. When providing Unix +timestamps enter seconds[.nanoseconds], where seconds is the number of seconds +that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap +seconds (aka Unix epoch or Unix time), and the optional .nanoseconds field is a +fraction of a second no more than nine digits long. + +## Filtering + +The filtering flag (`-f` or `--filter`) format is of "key=value". If you would +like to use multiple filters, pass multiple flags (e.g., +`--filter "foo=bar" --filter "bif=baz"`) + +Using the same filter multiple times will be handled as a *OR*; for example +`--filter container=588a23dac085 --filter container=a8f7720b8c22` will display +events for container 588a23dac085 *OR* container a8f7720b8c22 + +Using multiple filters will be handled as a *AND*; for example +`--filter container=588a23dac085 --filter event=start` will display events for +container container 588a23dac085 *AND* the event type is *start* + +The currently supported filters are: + +* container (`container=`) +* event (`event=`) +* image (`image=`) +* label (`label=` or `label==`) +* type (`type=`) +* volume (`volume=`) +* network (`network=`) + +## Examples + +You'll need two shells for this example. + +**Shell 1: Listening for events:** + + $ docker events + +**Shell 2: Start and Stop containers:** + + $ docker start 4386fb97867d + $ docker stop 4386fb97867d + $ docker stop 7805c1d35632 + +**Shell 1: (Again .. now showing events):** + + 2015-05-12T11:51:30.999999999Z07:00 container start 4386fb97867d (image=ubuntu-1:14.04) + 2015-05-12T11:51:30.999999999Z07:00 container die 4386fb97867d (image=ubuntu-1:14.04) + 2015-05-12T15:52:12.999999999Z07:00 container stop 4386fb97867d (image=ubuntu-1:14.04) + 2015-05-12T15:53:45.999999999Z07:00 container die 7805c1d35632 (image=redis:2.8) + 2015-05-12T15:54:03.999999999Z07:00 container stop 7805c1d35632 (image=redis:2.8) + +**Show events in the past from a specified time:** + + $ docker events --since 1378216169 + 2015-05-12T11:51:30.999999999Z07:00 container die 4386fb97867d (image=ubuntu-1:14.04) + 2015-05-12T15:52:12.999999999Z07:00 container stop 4386fb97867d (image=ubuntu-1:14.04) + 2015-05-12T15:53:45.999999999Z07:00 container die 7805c1d35632 (image=redis:2.8) + 2015-05-12T15:54:03.999999999Z07:00 container stop 7805c1d35632 (image=redis:2.8) + + $ docker events --since '2013-09-03' + 2015-05-12T11:51:30.999999999Z07:00 container start 4386fb97867d (image=ubuntu-1:14.04) + 2015-05-12T11:51:30.999999999Z07:00 container die 4386fb97867d (image=ubuntu-1:14.04) + 2015-05-12T15:52:12.999999999Z07:00 container stop 4386fb97867d (image=ubuntu-1:14.04) + 2015-05-12T15:53:45.999999999Z07:00 container die 7805c1d35632 (image=redis:2.8) + 2015-05-12T15:54:03.999999999Z07:00 container stop 7805c1d35632 (image=redis:2.8) + + $ docker events --since '2013-09-03T15:49:29' + 2015-05-12T11:51:30.999999999Z07:00 container die 4386fb97867d (image=ubuntu-1:14.04) + 2015-05-12T15:52:12.999999999Z07:00 container stop 4386fb97867d (image=ubuntu-1:14.04) + 2015-05-12T15:53:45.999999999Z07:00 container die 7805c1d35632 (image=redis:2.8) + 2015-05-12T15:54:03.999999999Z07:00 container stop 7805c1d35632 (image=redis:2.8) + +This example outputs all events that were generated in the last 3 minutes, +relative to the current time on the client machine: + + $ docker events --since '3m' + 2015-05-12T11:51:30.999999999Z07:00 container die 4386fb97867d (image=ubuntu-1:14.04) + 2015-05-12T15:52:12.999999999Z07:00 container stop 4386fb97867d (image=ubuntu-1:14.04) + 2015-05-12T15:53:45.999999999Z07:00 container die 7805c1d35632 (image=redis:2.8) + 2015-05-12T15:54:03.999999999Z07:00 container stop 7805c1d35632 (image=redis:2.8) + +**Filter events:** + + $ docker events --filter 'event=stop' + 2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu-1:14.04) + 2014-09-03T17:42:14.999999999Z07:00 container stop 7805c1d35632 (image=redis:2.8) + + $ docker events --filter 'image=ubuntu-1:14.04' + 2014-05-10T17:42:14.999999999Z07:00 container start 4386fb97867d (image=ubuntu-1:14.04) + 2014-05-10T17:42:14.999999999Z07:00 container die 4386fb97867d (image=ubuntu-1:14.04) + 2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu-1:14.04) + + $ docker events --filter 'container=7805c1d35632' + 2014-05-10T17:42:14.999999999Z07:00 container die 7805c1d35632 (image=redis:2.8) + 2014-09-03T15:49:29.999999999Z07:00 container stop 7805c1d35632 (image= redis:2.8) + + $ docker events --filter 'container=7805c1d35632' --filter 'container=4386fb97867d' + 2014-09-03T15:49:29.999999999Z07:00 container die 4386fb97867d (image=ubuntu-1:14.04) + 2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu-1:14.04) + 2014-05-10T17:42:14.999999999Z07:00 container die 7805c1d35632 (image=redis:2.8) + 2014-09-03T15:49:29.999999999Z07:00 container stop 7805c1d35632 (image=redis:2.8) + + $ docker events --filter 'container=7805c1d35632' --filter 'event=stop' + 2014-09-03T15:49:29.999999999Z07:00 container stop 7805c1d35632 (image=redis:2.8) + + $ docker events --filter 'container=container_1' --filter 'container=container_2' + 2014-09-03T15:49:29.999999999Z07:00 container die 4386fb97867d (image=ubuntu-1:14.04) + 2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu-1:14.04) + 2014-05-10T17:42:14.999999999Z07:00 container die 7805c1d35632 (imager=redis:2.8) + 2014-09-03T15:49:29.999999999Z07:00 container stop 7805c1d35632 (image=redis:2.8) + + $ docker events --filter 'type=volume' + 2015-12-23T21:05:28.136212689Z volume create test-event-volume-local (driver=local) + 2015-12-23T21:05:28.383462717Z volume mount test-event-volume-local (read/write=true, container=562fe10671e9273da25eed36cdce26159085ac7ee6707105fd534866340a5025, destination=/foo, driver=local, propagation=rprivate) + 2015-12-23T21:05:28.650314265Z volume unmount test-event-volume-local (container=562fe10671e9273da25eed36cdce26159085ac7ee6707105fd534866340a5025, driver=local) + 2015-12-23T21:05:28.716218405Z volume destroy test-event-volume-local (driver=local) + + $ docker events --filter 'type=network' + 2015-12-23T21:38:24.705709133Z network create 8b111217944ba0ba844a65b13efcd57dc494932ee2527577758f939315ba2c5b (name=test-event-network-local, type=bridge) + 2015-12-23T21:38:25.119625123Z network connect 8b111217944ba0ba844a65b13efcd57dc494932ee2527577758f939315ba2c5b (name=test-event-network-local, container=b4be644031a3d90b400f88ab3d4bdf4dc23adb250e696b6328b85441abe2c54e, type=bridge) diff --git a/docs/reference/commandline/exec.md b/docs/reference/commandline/exec.md new file mode 100644 index 00000000..80796a59 --- /dev/null +++ b/docs/reference/commandline/exec.md @@ -0,0 +1,56 @@ + + +# exec + + Usage: docker exec [OPTIONS] CONTAINER COMMAND [ARG...] + + Run a command in a running container + + -d, --detach Detached mode: run command in the background + --detach-keys Specify the escape key sequence used to detach a container + --help Print usage + -i, --interactive Keep STDIN open even if not attached + --privileged Give extended Linux capabilities to the command + -t, --tty Allocate a pseudo-TTY + -u, --user= Username or UID (format: [:]) + +The `docker exec` command runs a new command in a running container. + +The command started using `docker exec` only runs while the container's primary +process (`PID 1`) is running, and it is not restarted if the container is +restarted. + +If the container is paused, then the `docker exec` command will fail with an error: + + $ docker pause test + test + $ docker ps + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 1ae3b36715d2 ubuntu:latest "bash" 17 seconds ago Up 16 seconds (Paused) test + $ docker exec test ls + FATA[0000] Error response from daemon: Container test is paused, unpause the container before exec + $ echo $? + 1 + +## Examples + + $ docker run --name ubuntu_bash --rm -i -t ubuntu bash + +This will create a container named `ubuntu_bash` and start a Bash session. + + $ docker exec -d ubuntu_bash touch /tmp/execWorks + +This will create a new file `/tmp/execWorks` inside the running container +`ubuntu_bash`, in the background. + + $ docker exec -it ubuntu_bash bash + +This will create a new Bash session in the container `ubuntu_bash`. diff --git a/docs/reference/commandline/export.md b/docs/reference/commandline/export.md new file mode 100644 index 00000000..604ceab1 --- /dev/null +++ b/docs/reference/commandline/export.md @@ -0,0 +1,35 @@ + + +# export + + Usage: docker export [OPTIONS] CONTAINER + + Export the contents of a container's filesystem as a tar archive + + --help Print usage + -o, --output="" Write to a file, instead of STDOUT + +The `docker export` command does not export the contents of volumes associated +with the container. If a volume is mounted on top of an existing directory in +the container, `docker export` will export the contents of the *underlying* +directory, not the contents of the volume. + +Refer to [Backup, restore, or migrate data +volumes](../../userguide/containers/dockervolumes.md#backup-restore-or-migrate-data-volumes) in +the user guide for examples on exporting data in a volume. + +## Examples + + $ docker export red_panda > latest.tar + +Or + + $ docker export --output="latest.tar" red_panda diff --git a/docs/reference/commandline/history.md b/docs/reference/commandline/history.md new file mode 100644 index 00000000..d8750d83 --- /dev/null +++ b/docs/reference/commandline/history.md @@ -0,0 +1,40 @@ + + +# history + + Usage: docker history [OPTIONS] IMAGE + + Show the history of an image + + -H, --human=true Print sizes and dates in human readable format + --help Print usage + --no-trunc Don't truncate output + -q, --quiet Only show numeric IDs + +To see how the `docker:latest` image was built: + + $ docker history docker + IMAGE CREATED CREATED BY SIZE COMMENT + 3e23a5875458 8 days ago /bin/sh -c #(nop) ENV LC_ALL=C.UTF-8 0 B + 8578938dd170 8 days ago /bin/sh -c dpkg-reconfigure locales && loc 1.245 MB + be51b77efb42 8 days ago /bin/sh -c apt-get update && apt-get install 338.3 MB + 4b137612be55 6 weeks ago /bin/sh -c #(nop) ADD jessie.tar.xz in / 121 MB + 750d58736b4b 6 weeks ago /bin/sh -c #(nop) MAINTAINER Tianon Gravi ++++ +title = "images" +description = "The images command description and usage" +keywords = ["list, docker, images"] +[menu.main] +parent = "smn_cli" ++++ + + +# images + + Usage: docker images [OPTIONS] [REPOSITORY[:TAG]] + + List images + + -a, --all Show all images (default hides intermediate images) + --digests Show digests + -f, --filter=[] Filter output based on conditions provided + --help Print usage + --no-trunc Don't truncate output + -q, --quiet Only show numeric IDs + +The default `docker images` will show all top level +images, their repository and tags, and their size. + +Docker images have intermediate layers that increase reusability, +decrease disk usage, and speed up `docker build` by +allowing each step to be cached. These intermediate layers are not shown +by default. + +The `SIZE` is the cumulative space taken up by the image and all +its parent images. This is also the disk space used by the contents of the +Tar file created when you `docker save` an image. + +An image will be listed more than once if it has multiple repository names +or tags. This single image (identifiable by its matching `IMAGE ID`) +uses up the `SIZE` listed only once. + +### Listing the most recently created images + + $ docker images + REPOSITORY TAG IMAGE ID CREATED SIZE + 77af4d6b9913 19 hours ago 1.089 GB + committ latest b6fa739cedf5 19 hours ago 1.089 GB + 78a85c484f71 19 hours ago 1.089 GB + docker latest 30557a29d5ab 20 hours ago 1.089 GB + 5ed6274db6ce 24 hours ago 1.089 GB + postgres 9 746b819f315e 4 days ago 213.4 MB + postgres 9.3 746b819f315e 4 days ago 213.4 MB + postgres 9.3.5 746b819f315e 4 days ago 213.4 MB + postgres latest 746b819f315e 4 days ago 213.4 MB + +### Listing images by name and tag + +The `docker images` command takes an optional `[REPOSITORY[:TAG]]` argument +that restricts the list to images that match the argument. If you specify +`REPOSITORY`but no `TAG`, the `docker images` command lists all images in the +given repository. + +For example, to list all images in the "java" repository, run this command : + + $ docker images java + REPOSITORY TAG IMAGE ID CREATED SIZE + java 8 308e519aac60 6 days ago 824.5 MB + java 7 493d82594c15 3 months ago 656.3 MB + java latest 2711b1d6f3aa 5 months ago 603.9 MB + +The `[REPOSITORY[:TAG]]` value must be an "exact match". This means that, for example, +`docker images jav` does not match the image `java`. + +If both `REPOSITORY` and `TAG` are provided, only images matching that +repository and tag are listed. To find all local images in the "java" +repository with tag "8" you can use: + + $ docker images java:8 + REPOSITORY TAG IMAGE ID CREATED SIZE + java 8 308e519aac60 6 days ago 824.5 MB + +If nothing matches `REPOSITORY[:TAG]`, the list is empty. + + $ docker images java:0 + REPOSITORY TAG IMAGE ID CREATED SIZE + +## Listing the full length image IDs + + $ docker images --no-trunc + REPOSITORY TAG IMAGE ID CREATED SIZE + sha256:77af4d6b9913e693e8d0b4b294fa62ade6054e6b2f1ffb617ac955dd63fb0182 19 hours ago 1.089 GB + committest latest sha256:b6fa739cedf5ea12a620a439402b6004d057da800f91c7524b5086a5e4749c9f 19 hours ago 1.089 GB + sha256:78a85c484f71509adeaace20e72e941f6bdd2b25b4c75da8693efd9f61a37921 19 hours ago 1.089 GB + docker latest sha256:30557a29d5abc51e5f1d5b472e79b7e296f595abcf19fe6b9199dbbc809c6ff4 20 hours ago 1.089 GB + sha256:0124422dd9f9cf7ef15c0617cda3931ee68346455441d66ab8bdc5b05e9fdce5 20 hours ago 1.089 GB + sha256:18ad6fad340262ac2a636efd98a6d1f0ea775ae3d45240d3418466495a19a81b 22 hours ago 1.082 GB + sha256:f9f1e26352f0a3ba6a0ff68167559f64f3e21ff7ada60366e2d44a04befd1d3a 23 hours ago 1.089 GB + tryout latest sha256:2629d1fa0b81b222fca63371ca16cbf6a0772d07759ff80e8d1369b926940074 23 hours ago 131.5 MB + sha256:5ed6274db6ceb2397844896966ea239290555e74ef307030ebb01ff91b1914df 24 hours ago 1.089 GB + +## Listing image digests + +Images that use the v2 or later format have a content-addressable identifier +called a `digest`. As long as the input used to generate the image is +unchanged, the digest value is predictable. To list image digest values, use +the `--digests` flag: + + $ docker images --digests + REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE + localhost:5000/test/busybox sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf 4986bf8c1536 9 weeks ago 2.43 MB + +When pushing or pulling to a 2.0 registry, the `push` or `pull` command +output includes the image digest. You can `pull` using a digest value. You can +also reference by digest in `create`, `run`, and `rmi` commands, as well as the +`FROM` image reference in a Dockerfile. + +## Filtering + +The filtering flag (`-f` or `--filter`) format is of "key=value". If there is more +than one filter, then pass multiple flags (e.g., `--filter "foo=bar" --filter "bif=baz"`) + +The currently supported filters are: + +* dangling (boolean - true or false) +* label (`label=` or `label==`) + +##### Untagged images (dangling) + + $ docker images --filter "dangling=true" + + REPOSITORY TAG IMAGE ID CREATED SIZE + 8abc22fbb042 4 weeks ago 0 B + 48e5f45168b9 4 weeks ago 2.489 MB + bf747efa0e2f 4 weeks ago 0 B + 980fe10e5736 12 weeks ago 101.4 MB + dea752e4e117 12 weeks ago 101.4 MB + 511136ea3c5a 8 months ago 0 B + +This will display untagged images, that are the leaves of the images tree (not +intermediary layers). These images occur when a new build of an image takes the +`repo:tag` away from the image ID, leaving it as `:` or untagged. +A warning will be issued if trying to remove an image when a container is presently +using it. By having this flag it allows for batch cleanup. + +Ready for use by `docker rmi ...`, like: + + $ docker rmi $(docker images -f "dangling=true" -q) + + 8abc22fbb042 + 48e5f45168b9 + bf747efa0e2f + 980fe10e5736 + dea752e4e117 + 511136ea3c5a + +NOTE: Docker will warn you if any containers exist that are using these untagged images. + + +##### Labeled images + +The `label` filter matches images based on the presence of a `label` alone or a `label` and a +value. + +The following filter matches images with the `com.example.version` label regardless of its value. + + $ docker images --filter "label=com.example.version" + + REPOSITORY TAG IMAGE ID CREATED SIZE + match-me-1 latest eeae25ada2aa About a minute ago 188.3 MB + match-me-2 latest eeae25ada2aa About a minute ago 188.3 MB + +The following filter matches images with the `com.example.version` label with the `1.0` value. + + $ docker images --filter "label=com.example.version=1.0" + REPOSITORY TAG IMAGE ID CREATED SIZE + match-me latest eeae25ada2aa About a minute ago 188.3 MB + +In this example, with the `0.1` value, it returns an empty set because no matches were found. + + $ docker images --filter "label=com.example.version=0.1" + REPOSITORY TAG IMAGE ID CREATED SIZE + +## Formatting + +The formatting option (`--format`) will pretty print container output +using a Go template. + +Valid placeholders for the Go template are listed below: + +Placeholder | Description +---- | ---- +`.ID` | Image ID +`.Repository` | Image repository +`.Tag` | Image tag +`.Digest` | Image digest +`.CreatedSince` | Elapsed time since the image was created. +`.CreatedAt` | Time when the image was created. +`.Size` | Image disk size. + +When using the `--format` option, the `image` command will either +output the data exactly as the template declares or, when using the +`table` directive, will include column headers as well. + +The following example uses a template without headers and outputs the +`ID` and `Repository` entries separated by a colon for all images: + + $ docker images --format "{{.ID}}: {{.Repository}}" + 77af4d6b9913: + b6fa739cedf5: committ + 78a85c484f71: + 30557a29d5ab: docker + 5ed6274db6ce: + 746b819f315e: postgres + 746b819f315e: postgres + 746b819f315e: postgres + 746b819f315e: postgres + +To list all images with their repository and tag in a table format you +can use: + + $ docker images --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}" + IMAGE ID REPOSITORY TAG + 77af4d6b9913 + b6fa739cedf5 committ latest + 78a85c484f71 + 30557a29d5ab docker latest + 5ed6274db6ce + 746b819f315e postgres 9 + 746b819f315e postgres 9.3 + 746b819f315e postgres 9.3.5 + 746b819f315e postgres latest diff --git a/docs/reference/commandline/import.md b/docs/reference/commandline/import.md new file mode 100644 index 00000000..d4ca8d57 --- /dev/null +++ b/docs/reference/commandline/import.md @@ -0,0 +1,69 @@ + + +# import + + Usage: docker import file|URL|- [REPOSITORY[:TAG]] + + Create an empty filesystem image and import the contents of the + tarball (.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz) into it, then + optionally tag it. + + -c, --change=[] Apply specified Dockerfile instructions while importing the image + --help Print usage + -m, --message= Set commit message for imported image + +You can specify a `URL` or `-` (dash) to take data directly from `STDIN`. The +`URL` can point to an archive (.tar, .tar.gz, .tgz, .bzip, .tar.xz, or .txz) +containing a filesystem or to an individual file on the Docker host. If you +specify an archive, Docker untars it in the container relative to the `/` +(root). If you specify an individual file, you must specify the full path within +the host. To import from a remote location, specify a `URI` that begins with the +`http://` or `https://` protocol. + +The `--change` option will apply `Dockerfile` instructions to the image +that is created. +Supported `Dockerfile` instructions: +`CMD`|`ENTRYPOINT`|`ENV`|`EXPOSE`|`ONBUILD`|`USER`|`VOLUME`|`WORKDIR` + +## Examples + +**Import from a remote location:** + +This will create a new untagged image. + + $ docker import http://example.com/exampleimage.tgz + +**Import from a local file:** + +Import to docker via pipe and `STDIN`. + + $ cat exampleimage.tgz | docker import - exampleimagelocal:new + +Import with a commit message. + + $ cat exampleimage.tgz | docker import --message "New image imported from tarball" - exampleimagelocal:new + +Import to docker from a local archive. + + $ docker import /path/to/exampleimage.tgz + +**Import from a local directory:** + + $ sudo tar -c . | docker import - exampleimagedir + +**Import from a local directory with new configurations:** + + $ sudo tar -c . | docker import --change "ENV DEBUG true" - exampleimagedir + +Note the `sudo` in this example – you must preserve +the ownership of the files (especially root ownership) during the +archiving with tar. If you are not root (or the sudo command) when you +tar, then the ownerships might not get preserved. diff --git a/docs/reference/commandline/index.md b/docs/reference/commandline/index.md new file mode 100644 index 00000000..a5704da0 --- /dev/null +++ b/docs/reference/commandline/index.md @@ -0,0 +1,88 @@ + + + + +# The Docker commands + +This section contains reference information on using Docker's command line client. Each command has a reference page along with samples. If you are unfamiliar with the command line, you should start by reading about how to [Use the Docker command line](cli.md). + +You start the Docker daemon with the command line. How you start the daemon affects your Docker containers. For that reason you should also make sure to read the [`daemon`](daemon.md) reference page. + +### Docker management commands + +* [daemon](daemon.md) +* [info](info.md) +* [inspect](inspect.md) +* [version](version.md) + +### Image commands + +* [build](build.md) +* [commit](commit.md) +* [export](export.md) +* [history](history.md) +* [images](images.md) +* [import](import.md) +* [load](load.md) +* [rmi](rmi.md) +* [save](save.md) +* [tag](tag.md) + +### Container commands + +* [attach](attach.md) +* [cp](cp.md) +* [create](create.md) +* [diff](diff.md) +* [events](events.md) +* [exec](exec.md) +* [kill](kill.md) +* [logs](logs.md) +* [pause](pause.md) +* [port](port.md) +* [ps](ps.md) +* [rename](rename.md) +* [restart](restart.md) +* [rm](rm.md) +* [run](run.md) +* [start](start.md) +* [stats](stats.md) +* [stop](stop.md) +* [top](top.md) +* [unpause](unpause.md) +* [update](update.md) +* [wait](wait.md) + +### Hub and registry commands + +* [login](login.md) +* [logout](logout.md) +* [pull](pull.md) +* [push](push.md) +* [search](search.md) + +### Network and connectivity commands + +* [network_connect](network_connect.md) +* [network_create](network_create.md) +* [network_disconnect](network_disconnect.md) +* [network_inspect](network_inspect.md) +* [network_ls](network_ls.md) +* [network_rm](network_rm.md) + +### Shared data volume commands + +* [volume_create](volume_create.md) +* [volume_inspect](volume_inspect.md) +* [volume_ls](volume_ls.md) +* [volume_rm](volume_rm.md) diff --git a/docs/reference/commandline/info.md b/docs/reference/commandline/info.md new file mode 100644 index 00000000..06f7848d --- /dev/null +++ b/docs/reference/commandline/info.md @@ -0,0 +1,69 @@ + + +# info + + + Usage: docker info [OPTIONS] + + Display system-wide information + + --help Print usage + +For example: + + $ docker -D info + Containers: 14 + Running: 3 + Paused: 1 + Stopped: 10 + Images: 52 + Server Version: 1.9.0 + Storage Driver: aufs + Root Dir: /var/lib/docker/aufs + Backing Filesystem: extfs + Dirs: 545 + Dirperm1 Supported: true + Execution Driver: native-0.2 + Logging Driver: json-file + Cgroup Driver: cgroupfs + Plugins: + Volume: local + Network: bridge null host + Kernel Version: 3.19.0-22-generic + OSType: linux + Architecture: x86_64 + Operating System: Ubuntu 15.04 + CPUs: 24 + Total Memory: 62.86 GiB + Name: docker + ID: I54V:OLXT:HVMM:TPKO:JPHQ:CQCD:JNLC:O3BZ:4ZVJ:43XJ:PFHZ:6N2S + Docker Root Dir: /var/lib/docker + Debug mode (client): true + Debug mode (server): true + File Descriptors: 59 + Goroutines: 159 + System Time: 2015-09-23T14:04:20.699842089+08:00 + EventsListeners: 0 + Init SHA1: + Init Path: /usr/bin/docker + Docker Root Dir: /var/lib/docker + Http Proxy: http://test:test@localhost:8080 + Https Proxy: https://test:test@localhost:8080 + WARNING: No swap limit support + Username: svendowideit + Registry: [https://index.docker.io/v1/] + Labels: + storage=ssd + +The global `-D` option tells all `docker` commands to output debug information. + +When sending issue reports, please use `docker version` and `docker -D info` to +ensure we know how your setup is configured. diff --git a/docs/reference/commandline/inspect.md b/docs/reference/commandline/inspect.md new file mode 100644 index 00000000..38d4098c --- /dev/null +++ b/docs/reference/commandline/inspect.md @@ -0,0 +1,76 @@ + + +# inspect + + Usage: docker inspect [OPTIONS] CONTAINER|IMAGE [CONTAINER|IMAGE...] + + Return low-level information on a container or image + + -f, --format="" Format the output using the given go template + --help Print usage + --type=container|image Return JSON for specified type, permissible + values are "image" or "container" + -s, --size Display total file sizes if the type is container + +By default, this will render all results in a JSON array. If the container and +image have the same name, this will return container JSON for unspecified type. +If a format is specified, the given template will be executed for each result. + +Go's [text/template](http://golang.org/pkg/text/template/) package +describes all the details of the format. + +## Examples + +**Get an instance's IP address:** + +For the most part, you can pick out any field from the JSON in a fairly +straightforward manner. + + $ docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $INSTANCE_ID + +**Get an instance's MAC Address:** + +For the most part, you can pick out any field from the JSON in a fairly +straightforward manner. + + $ docker inspect --format='{{range .NetworkSettings.Networks}}{{.MacAddress}}{{end}}' $INSTANCE_ID + +**Get an instance's log path:** + + $ docker inspect --format='{{.LogPath}}' $INSTANCE_ID + +**List All Port Bindings:** + +One can loop over arrays and maps in the results to produce simple text +output: + + $ docker inspect --format='{{range $p, $conf := .NetworkSettings.Ports}} {{$p}} -> {{(index $conf 0).HostPort}} {{end}}' $INSTANCE_ID + +**Find a Specific Port Mapping:** + +The `.Field` syntax doesn't work when the field name begins with a +number, but the template language's `index` function does. The +`.NetworkSettings.Ports` section contains a map of the internal port +mappings to a list of external address/port objects. To grab just the +numeric public port, you use `index` to find the specific port map, and +then `index` 0 contains the first object inside of that. Then we ask for +the `HostPort` field to get the public address. + + $ docker inspect --format='{{(index (index .NetworkSettings.Ports "8787/tcp") 0).HostPort}}' $INSTANCE_ID + +**Get a subsection in JSON format:** + +If you request a field which is itself a structure containing other +fields, by default you get a Go-style dump of the inner values. +Docker adds a template function, `json`, which can be applied to get +results in JSON format. + + $ docker inspect --format='{{json .Config}}' $INSTANCE_ID diff --git a/docs/reference/commandline/kill.md b/docs/reference/commandline/kill.md new file mode 100644 index 00000000..6f202587 --- /dev/null +++ b/docs/reference/commandline/kill.md @@ -0,0 +1,26 @@ + + +# kill + + Usage: docker kill [OPTIONS] CONTAINER [CONTAINER...] + + Kill a running container using SIGKILL or a specified signal + + --help Print usage + -s, --signal="KILL" Signal to send to the container + +The main process inside the container will be sent `SIGKILL`, or any +signal specified with option `--signal`. + +> **Note:** +> `ENTRYPOINT` and `CMD` in the *shell* form run as a subcommand of `/bin/sh -c`, +> which does not pass signals. This means that the executable is not the container’s PID 1 +> and does not receive Unix signals. diff --git a/docs/reference/commandline/load.md b/docs/reference/commandline/load.md new file mode 100644 index 00000000..0b40fde2 --- /dev/null +++ b/docs/reference/commandline/load.md @@ -0,0 +1,37 @@ + + +# load + + Usage: docker load [OPTIONS] + + Load an image from a tar archive or STDIN + + --help Print usage + -i, --input="" Read from a tar archive file, instead of STDIN. The tarball may be compressed with gzip, bzip, or xz + -q, --quiet Suppress the load output. Without this option, a progress bar is displayed. + +Loads a tarred repository from a file or the standard input stream. +Restores both images and tags. + + $ docker images + REPOSITORY TAG IMAGE ID CREATED SIZE + $ docker load < busybox.tar.gz + $ docker images + REPOSITORY TAG IMAGE ID CREATED SIZE + busybox latest 769b9341d937 7 weeks ago 2.489 MB + $ docker load --input fedora.tar + $ docker images + REPOSITORY TAG IMAGE ID CREATED SIZE + busybox latest 769b9341d937 7 weeks ago 2.489 MB + fedora rawhide 0d20aec6529d 7 weeks ago 387 MB + fedora 20 58394af37342 7 weeks ago 385.5 MB + fedora heisenbug 58394af37342 7 weeks ago 385.5 MB + fedora latest 58394af37342 7 weeks ago 385.5 MB diff --git a/docs/reference/commandline/login.md b/docs/reference/commandline/login.md new file mode 100644 index 00000000..baff45f8 --- /dev/null +++ b/docs/reference/commandline/login.md @@ -0,0 +1,117 @@ + + +# login + + Usage: docker login [OPTIONS] [SERVER] + + Log in to a Docker registry server, if no server is + specified "https://index.docker.io/v1/" is the default. + + --help Print usage + -p, --password="" Password + -u, --username="" Username + +If you want to login to a self-hosted registry you can specify this by +adding the server name. + + example: + $ docker login localhost:8080 + + +`docker login` requires user to use `sudo` or be `root`, except when: + +1. connecting to a remote daemon, such as a `docker-machine` provisioned `docker engine`. +2. user is added to the `docker` group. This will impact the security of your system; the `docker` group is `root` equivalent. See [Docker Daemon Attack Surface](https://docs.docker.com/security/security/#docker-daemon-attack-surface) for details. + +You can log into any public or private repository for which you have +credentials. When you log in, the command stores encoded credentials in +`$HOME/.docker/config.json` on Linux or `%USERPROFILE%/.docker/config.json` on Windows. + +> **Note**: When running `sudo docker login` credentials are saved in `/root/.docker/config.json`. +> + +## Credentials store + +The Docker Engine can keep user credentials in an external credentials store, +such as the native keychain of the operating system. Using an external store +is more secure than storing credentials in the Docker configuration file. + +To use a credentials store, you need an external helper program to interact +with a specific keychain or external store. Docker requires the helper +program to be in the client's host `$PATH`. + +This is the list of currently available credentials helpers and where +you can download them from: + +- D-Bus Secret Service: https://github.com/docker/docker-credential-helpers/releases +- Apple OS X keychain: https://github.com/docker/docker-credential-helpers/releases +- Microsoft Windows Credential Manager: https://github.com/docker/docker-credential-helpers/releases + +### Usage + +You need to speficy the credentials store in `$HOME/.docker/config.json` +to tell the docker engine to use it: + +```json +{ + "credsStore": "osxkeychain" +} +``` + +If you are currently logged in, run `docker logout` to remove +the credentials from the file and run `docker login` again. + +### Protocol + +Credential helpers can be any program or script that follows a very simple protocol. +This protocol is heavily inspired by Git, but it differs in the information shared. + +The helpers always use the first argument in the command to identify the action. +There are only three possible values for that argument: `store`, `get`, and `erase`. + +The `store` command takes a JSON payload from the standard input. That payload carries +the server address, to identify the credential, the user name, and either a password +or an identity token. + +```json +{ + "ServerURL": "https://index.docker.io/v1", + "Username": "david", + "Secret": "passw0rd1" +} +``` + +If the secret being stored is an identity token, the Username should be set to +``. + +The `store` command can write error messages to `STDOUT` that the docker engine +will show if there was an issue. + +The `get` command takes a string payload from the standard input. That payload carries +the server address that the docker engine needs credentials for. This is +an example of that payload: `https://index.docker.io/v1`. + +The `get` command writes a JSON payload to `STDOUT`. Docker reads the user name +and password from this payload: + +```json +{ + "Username": "david", + "Secret": "passw0rd1" +} +``` + +The `erase` command takes a string payload from `STDIN`. That payload carries +the server address that the docker engine wants to remove credentials for. This is +an example of that payload: `https://index.docker.io/v1`. + +The `erase` command can write error messages to `STDOUT` that the docker engine +will show if there was an issue. diff --git a/docs/reference/commandline/logout.md b/docs/reference/commandline/logout.md new file mode 100644 index 00000000..a3bb40c6 --- /dev/null +++ b/docs/reference/commandline/logout.md @@ -0,0 +1,22 @@ + + +# logout + + Usage: docker logout [SERVER] + + Log out from a Docker registry, if no server is + specified "https://index.docker.io/v1/" is the default. + + --help Print usage + +For example: + + $ docker logout localhost:8080 diff --git a/docs/reference/commandline/logs.md b/docs/reference/commandline/logs.md new file mode 100644 index 00000000..91558ffa --- /dev/null +++ b/docs/reference/commandline/logs.md @@ -0,0 +1,50 @@ + + +# logs + + Usage: docker logs [OPTIONS] CONTAINER + + Fetch the logs of a container + + -f, --follow Follow log output + --help Print usage + --since="" Show logs since timestamp + -t, --timestamps Show timestamps + --tail="all" Number of lines to show from the end of the logs + +> **Note**: this command is available only for containers with `json-file` and +> `journald` logging drivers. + +The `docker logs` command batch-retrieves logs present at the time of execution. + +The `docker logs --follow` command will continue streaming the new output from +the container's `STDOUT` and `STDERR`. + +Passing a negative number or a non-integer to `--tail` is invalid and the +value is set to `all` in that case. + +The `docker logs --timestamps` command will add an [RFC3339Nano timestamp](https://golang.org/pkg/time/#pkg-constants) +, for example `2014-09-16T06:17:46.000000000Z`, to each +log entry. To ensure that the timestamps are aligned the +nano-second part of the timestamp will be padded with zero when necessary. + +The `--since` option shows only the container logs generated after +a given date. You can specify the date as an RFC 3339 date, a UNIX +timestamp, or a Go duration string (e.g. `1m30s`, `3h`). Besides RFC3339 date +format you may also use RFC3339Nano, `2006-01-02T15:04:05`, +`2006-01-02T15:04:05.999999999`, `2006-01-02Z07:00`, and `2006-01-02`. The local +timezone on the client will be used if you do not provide either a `Z` or a +`+-00:00` timezone offset at the end of the timestamp. When providing Unix +timestamps enter seconds[.nanoseconds], where seconds is the number of seconds +that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap +seconds (aka Unix epoch or Unix time), and the optional .nanoseconds field is a +fraction of a second no more than nine digits long. You can combine the +`--since` option with either or both of the `--follow` or `--tail` options. diff --git a/docs/reference/commandline/network_connect.md b/docs/reference/commandline/network_connect.md new file mode 100644 index 00000000..a815ca38 --- /dev/null +++ b/docs/reference/commandline/network_connect.md @@ -0,0 +1,93 @@ + + +# network connect + + Usage: docker network connect [OPTIONS] NETWORK CONTAINER + + Connects a container to a network + + --alias=[] Add network-scoped alias for the container + --help Print usage + --ip IPv4 Address + --ip6 IPv6 Address + --link=[] Add a link to another container + +Connects a container to a network. You can connect a container by name +or by ID. Once connected, the container can communicate with other containers in +the same network. + +```bash +$ docker network connect multi-host-network container1 +``` + +You can also use the `docker run --net=` option to start a container and immediately connect it to a network. + +```bash +$ docker run -itd --net=multi-host-network busybox +``` + +You can specify the IP address you want to be assigned to the container's interface. + +```bash +$ docker network connect --ip 10.10.36.122 multi-host-network container2 +``` + +You can use `--link` option to link another container with a preferred alias + +```bash +$ docker network connect --link container1:c1 multi-host-network container2 +``` + +`--alias` option can be used to resolve the container by another name in the network +being connected to. + +```bash +$ docker network connect --alias db --alias mysql multi-host-network container2 +``` + +You can pause, restart, and stop containers that are connected to a network. +Paused containers remain connected and can be revealed by a `network inspect`. +When the container is stopped, it does not appear on the network until you restart +it. + +If specified, the container's IP address(es) is reapplied when a stopped +container is restarted. If the IP address is no longer available, the container +fails to start. One way to guarantee that the IP address is available is +to specify an `--ip-range` when creating the network, and choose the static IP +address(es) from outside that range. This ensures that the IP address is not +given to another container while this container is not on the network. + +```bash +$ docker network create --subnet 172.20.0.0/16 --ip-range 172.20.240.0/20 multi-host-network +``` + +```bash +$ docker network connect --ip 172.20.128.2 multi-host-network container2 +``` + +To verify the container is connected, use the `docker network inspect` command. Use `docker network disconnect` to remove a container from the network. + +Once connected in network, containers can communicate using only another +container's IP address or name. For `overlay` networks or custom plugins that +support multi-host connectivity, containers connected to the same multi-host +network but launched from different Engines can also communicate in this way. + +You can connect a container to one or more networks. The networks need not be the same type. For example, you can connect a single container bridge and overlay networks. + +## Related information + +* [network inspect](network_inspect.md) +* [network create](network_create.md) +* [network disconnect](network_disconnect.md) +* [network ls](network_ls.md) +* [network rm](network_rm.md) +* [Understand Docker container networks](../../userguide/networking/dockernetworks.md) +* [Work with networks](../../userguide/networking/work-with-networks.md) diff --git a/docs/reference/commandline/network_create.md b/docs/reference/commandline/network_create.md new file mode 100644 index 00000000..f44c5eff --- /dev/null +++ b/docs/reference/commandline/network_create.md @@ -0,0 +1,169 @@ + + +# network create + + Usage: docker network create [OPTIONS] NETWORK-NAME + + Creates a new network with a name specified by the user + + --aux-address=map[] Auxiliary ipv4 or ipv6 addresses used by network driver + -d --driver=DRIVER Driver to manage the Network bridge or overlay. The default is bridge. + --gateway=[] ipv4 or ipv6 Gateway for the master subnet + --help Print usage + --internal Restricts external access to the network + --ip-range=[] Allocate container ip from a sub-range + --ipam-driver=default IP Address Management Driver + --ipam-opt=map[] Set custom IPAM driver specific options + --ipv6 Enable IPv6 networking + --label=[] Set metadata on a network + -o --opt=map[] Set custom driver specific options + --subnet=[] Subnet in CIDR format that represents a network segment + +Creates a new network. The `DRIVER` accepts `bridge` or `overlay` which are the +built-in network drivers. If you have installed a third party or your own custom +network driver you can specify that `DRIVER` here also. If you don't specify the +`--driver` option, the command automatically creates a `bridge` network for you. +When you install Docker Engine it creates a `bridge` network automatically. This +network corresponds to the `docker0` bridge that Engine has traditionally relied +on. When launch a new container with `docker run` it automatically connects to +this bridge network. You cannot remove this default bridge network but you can +create new ones using the `network create` command. + +```bash +$ docker network create -d bridge my-bridge-network +``` + +Bridge networks are isolated networks on a single Engine installation. If you +want to create a network that spans multiple Docker hosts each running an +Engine, you must create an `overlay` network. Unlike `bridge` networks overlay +networks require some pre-existing conditions before you can create one. These +conditions are: + +* Access to a key-value store. Engine supports Consul, Etcd, and ZooKeeper (Distributed store) key-value stores. +* A cluster of hosts with connectivity to the key-value store. +* A properly configured Engine `daemon` on each host in the cluster. + +The `docker daemon` options that support the `overlay` network are: + +* `--cluster-store` +* `--cluster-store-opt` +* `--cluster-advertise` + +To read more about these options and how to configure them, see ["*Get started +with multi-host network*"](../../userguide/networking/get-started-overlay.md). + +It is also a good idea, though not required, that you install Docker Swarm on to +manage the cluster that makes up your network. Swarm provides sophisticated +discovery and server management that can assist your implementation. + +Once you have prepared the `overlay` network prerequisites you simply choose a +Docker host in the cluster and issue the following to create the network: + +```bash +$ docker network create -d overlay my-multihost-network +``` + +Network names must be unique. The Docker daemon attempts to identify naming +conflicts but this is not guaranteed. It is the user's responsibility to avoid +name conflicts. + +## Connect containers + +When you start a container use the `--net` flag to connect it to a network. +This adds the `busybox` container to the `mynet` network. + +```bash +$ docker run -itd --net=mynet busybox +``` + +If you want to add a container to a network after the container is already +running use the `docker network connect` subcommand. + +You can connect multiple containers to the same network. Once connected, the +containers can communicate using only another container's IP address or name. +For `overlay` networks or custom plugins that support multi-host connectivity, +containers connected to the same multi-host network but launched from different +Engines can also communicate in this way. + +You can disconnect a container from a network using the `docker network +disconnect` command. + +## Specifying advanced options + +When you create a network, Engine creates a non-overlapping subnetwork for the network by default. This subnetwork is not a subdivision of an existing network. It is purely for ip-addressing purposes. You can override this default and specify subnetwork values directly using the `--subnet` option. On a `bridge` network you can only create a single subnet: + +```bash +docker network create -d --subnet=192.168.0.0/16 +``` +Additionally, you also specify the `--gateway` `--ip-range` and `--aux-address` options. + +```bash +network create --driver=bridge --subnet=172.28.0.0/16 --ip-range=172.28.5.0/24 --gateway=172.28.5.254 br0 +``` + +If you omit the `--gateway` flag the Engine selects one for you from inside a +preferred pool. For `overlay` networks and for network driver plugins that +support it you can create multiple subnetworks. + +```bash +docker network create -d overlay + --subnet=192.168.0.0/16 --subnet=192.170.0.0/16 + --gateway=192.168.0.100 --gateway=192.170.0.100 + --ip-range=192.168.1.0/24 + --aux-address a=192.168.1.5 --aux-address b=192.168.1.6 + --aux-address a=192.170.1.5 --aux-address b=192.170.1.6 + my-multihost-network +``` +Be sure that your subnetworks do not overlap. If they do, the network create fails and Engine returns an error. + +# Bridge driver options + +When creating a custom network, the default network driver (i.e. `bridge`) has additional options that can be passed. +The following are those options and the equivalent docker daemon flags used for docker0 bridge: + +| Option | Equivalent | Description | +|--------------------------------------------------|-------------|-------------------------------------------------------| +| `com.docker.network.bridge.name` | - | bridge name to be used when creating the Linux bridge | +| `com.docker.network.bridge.enable_ip_masquerade` | `--ip-masq` | Enable IP masquerading | +| `com.docker.network.bridge.enable_icc` | `--icc` | Enable or Disable Inter Container Connectivity | +| `com.docker.network.bridge.host_binding_ipv4` | `--ip` | Default IP when binding container ports | +| `com.docker.network.mtu` | `--mtu` | Set the containers network MTU | + +The following arguments can be passed to `docker network create` for any network driver, again with their approximate +equivalents to `docker daemon`. + +| Argument | Equivalent | Description | +|--------------|----------------|--------------------------------------------| +| `--gateway` | - | ipv4 or ipv6 Gateway for the master subnet | +| `--ip-range` | `--fixed-cidr` | Allocate IPs from a range | +| `--internal` | - | Restricts external access to the network | +| `--ipv6` | `--ipv6` | Enable IPv6 networking | +| `--subnet` | `--bip` | Subnet for network | + +For example, let's use `-o` or `--opt` options to specify an IP address binding when publishing ports: + +```bash +docker network create -o "com.docker.network.bridge.host_binding_ipv4"="172.19.0.1" simple-network +``` + +### Network internal mode + +By default, when you connect a container to an `overlay` network, Docker also connects a bridge network to it to provide external connectivity. +If you want to create an externally isolated `overlay` network, you can specify the `--internal` option. + +## Related information + +* [network inspect](network_inspect.md) +* [network connect](network_connect.md) +* [network disconnect](network_disconnect.md) +* [network ls](network_ls.md) +* [network rm](network_rm.md) +* [Understand Docker container networks](../../userguide/networking/dockernetworks.md) diff --git a/docs/reference/commandline/network_disconnect.md b/docs/reference/commandline/network_disconnect.md new file mode 100644 index 00000000..10c4f16e --- /dev/null +++ b/docs/reference/commandline/network_disconnect.md @@ -0,0 +1,35 @@ + + +# network disconnect + + Usage: docker network disconnect [OPTIONS] NETWORK CONTAINER + + + Disconnects a container from a network + + -f, --force Force the container to disconnect from a network + --help Print usage + +Disconnects a container from a network. The container must be running to disconnect it from the network. + +```bash + $ docker network disconnect multi-host-network container1 +``` + + +## Related information + +* [network inspect](network_inspect.md) +* [network connect](network_connect.md) +* [network create](network_create.md) +* [network ls](network_ls.md) +* [network rm](network_rm.md) +* [Understand Docker container networks](../../userguide/networking/dockernetworks.md) diff --git a/docs/reference/commandline/network_inspect.md b/docs/reference/commandline/network_inspect.md new file mode 100644 index 00000000..251407e5 --- /dev/null +++ b/docs/reference/commandline/network_inspect.md @@ -0,0 +1,119 @@ + + +# network inspect + + Usage: docker network inspect [OPTIONS] NETWORK [NETWORK..] + + Displays detailed information on a network + + -f, --format= Format the output using the given go template. + --help Print usage + +Returns information about one or more networks. By default, this command renders all results in a JSON object. For example, if you connect two containers to the default `bridge` network: + +```bash +$ sudo docker run -itd --name=container1 busybox +f2870c98fd504370fb86e59f32cd0753b1ac9b69b7d80566ffc7192a82b3ed27 + +$ sudo docker run -itd --name=container2 busybox +bda12f8922785d1f160be70736f26c1e331ab8aaf8ed8d56728508f2e2fd4727 +``` + +The `network inspect` command shows the containers, by id, in its +results. For networks backed by multi-host network driver, such as Overlay, +this command also shows the container endpoints in other hosts in the +cluster. These endpoints are represented as "ep-{endpoint-id}" in the output. +You can specify an alternate format to execute a given +template for each result. Go's +[text/template](http://golang.org/pkg/text/template/) package describes all the +details of the format. + +```bash +$ sudo docker network inspect bridge +[ + { + "Name": "bridge", + "Id": "b2b1a2cba717161d984383fd68218cf70bbbd17d328496885f7c921333228b0f", + "Scope": "local", + "Driver": "bridge", + "IPAM": { + "Driver": "default", + "Config": [ + { + "Subnet": "172.17.42.1/16", + "Gateway": "172.17.42.1" + } + ] + }, + "Internal": false, + "Containers": { + "bda12f8922785d1f160be70736f26c1e331ab8aaf8ed8d56728508f2e2fd4727": { + "Name": "container2", + "EndpointID": "0aebb8fcd2b282abe1365979536f21ee4ceaf3ed56177c628eae9f706e00e019", + "MacAddress": "02:42:ac:11:00:02", + "IPv4Address": "172.17.0.2/16", + "IPv6Address": "" + }, + "f2870c98fd504370fb86e59f32cd0753b1ac9b69b7d80566ffc7192a82b3ed27": { + "Name": "container1", + "EndpointID": "a00676d9c91a96bbe5bcfb34f705387a33d7cc365bac1a29e4e9728df92d10ad", + "MacAddress": "02:42:ac:11:00:01", + "IPv4Address": "172.17.0.1/16", + "IPv6Address": "" + } + }, + "Options": { + "com.docker.network.bridge.default_bridge": "true", + "com.docker.network.bridge.enable_icc": "true", + "com.docker.network.bridge.enable_ip_masquerade": "true", + "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", + "com.docker.network.bridge.name": "docker0", + "com.docker.network.driver.mtu": "1500" + } + } +] +``` + +Returns the information about the user-defined network: + +```bash +$ docker network create simple-network +69568e6336d8c96bbf57869030919f7c69524f71183b44d80948bd3927c87f6a +$ docker network inspect simple-network +[ + { + "Name": "simple-network", + "Id": "69568e6336d8c96bbf57869030919f7c69524f71183b44d80948bd3927c87f6a", + "Scope": "local", + "Driver": "bridge", + "IPAM": { + "Driver": "default", + "Config": [ + { + "Subnet": "172.22.0.0/16", + "Gateway": "172.22.0.1/16" + } + ] + }, + "Containers": {}, + "Options": {} + } +] +``` + +## Related information + +* [network disconnect ](network_disconnect.md) +* [network connect](network_connect.md) +* [network create](network_create.md) +* [network ls](network_ls.md) +* [network rm](network_rm.md) +* [Understand Docker container networks](../../userguide/networking/dockernetworks.md) diff --git a/docs/reference/commandline/network_ls.md b/docs/reference/commandline/network_ls.md new file mode 100644 index 00000000..b12957a3 --- /dev/null +++ b/docs/reference/commandline/network_ls.md @@ -0,0 +1,135 @@ + + +# docker network ls + + Usage: docker network ls [OPTIONS] + + Lists all the networks created by the user + -f, --filter=[] Filter output based on conditions provided + --help Print usage + --no-trunc Do not truncate the output + -q, --quiet Only display numeric IDs + +Lists all the networks the Engine `daemon` knows about. This includes the +networks that span across multiple hosts in a cluster, for example: + +```bash + $ sudo docker network ls + NETWORK ID NAME DRIVER + 7fca4eb8c647 bridge bridge + 9f904ee27bf5 none null + cf03ee007fb4 host host + 78b03ee04fc4 multi-host overlay +``` + +Use the `--no-trunc` option to display the full network id: + +```bash +docker network ls --no-trunc +NETWORK ID NAME DRIVER +18a2866682b85619a026c81b98a5e375bd33e1b0936a26cc497c283d27bae9b3 none null +c288470c46f6c8949c5f7e5099b5b7947b07eabe8d9a27d79a9cbf111adcbf47 host host +7b369448dccbf865d397c8d2be0cda7cf7edc6b0945f77d2529912ae917a0185 bridge bridge +95e74588f40db048e86320c6526440c504650a1ff3e9f7d60a497c4d2163e5bd foo bridge +63d1ff1f77b07ca51070a8c227e962238358bd310bde1529cf62e6c307ade161 dev bridge +``` + +## Filtering + +The filtering flag (`-f` or `--filter`) format is a `key=value` pair. If there +is more than one filter, then pass multiple flags (e.g. `--filter "foo=bar" --filter "bif=baz"`). +Multiple filter flags are combined as an `OR` filter. For example, +`-f type=custom -f type=builtin` returns both `custom` and `builtin` networks. + +The currently supported filters are: + +* id (network's id) +* name (network's name) +* type (custom|builtin) + +#### Type + +The `type` filter supports two values; `builtin` displays predefined networks +(`bridge`, `none`, `host`), whereas `custom` displays user defined networks. + +The following filter matches all user defined networks: + +```bash +$ docker network ls --filter type=custom +NETWORK ID NAME DRIVER +95e74588f40d foo bridge +63d1ff1f77b0 dev bridge +``` + +By having this flag it allows for batch cleanup. For example, use this filter +to delete all user defined networks: + +```bash +$ docker network rm `docker network ls --filter type=custom -q` +``` + +A warning will be issued when trying to remove a network that has containers +attached. + +#### Name + +The `name` filter matches on all or part of a network's name. + +The following filter matches all networks with a name containing the `foobar` string. + +```bash +$ docker network ls --filter name=foobar +NETWORK ID NAME DRIVER +06e7eef0a170 foobar bridge +``` + +You can also filter for a substring in a name as this shows: + +```bash +$ docker network ls --filter name=foo +NETWORK ID NAME DRIVER +95e74588f40d foo bridge +06e7eef0a170 foobar bridge +``` + +#### ID + +The `id` filter matches on all or part of a network's ID. + +The following filter matches all networks with an ID containing the +`63d1ff1f77b0...` string. + +```bash +$ docker network ls --filter id=63d1ff1f77b07ca51070a8c227e962238358bd310bde1529cf62e6c307ade161 +NETWORK ID NAME DRIVER +63d1ff1f77b0 dev bridge +``` + +You can also filter for a substring in an ID as this shows: + +```bash +$ docker network ls --filter id=95e74588f40d +NETWORK ID NAME DRIVER +95e74588f40d foo bridge + +$ docker network ls --filter id=95e +NETWORK ID NAME DRIVER +95e74588f40d foo bridge +``` + +## Related information + +* [network disconnect ](network_disconnect.md) +* [network connect](network_connect.md) +* [network create](network_create.md) +* [network inspect](network_inspect.md) +* [network rm](network_rm.md) +* [Understand Docker container networks](../../userguide/networking/dockernetworks.md) diff --git a/docs/reference/commandline/network_rm.md b/docs/reference/commandline/network_rm.md new file mode 100644 index 00000000..0653458f --- /dev/null +++ b/docs/reference/commandline/network_rm.md @@ -0,0 +1,47 @@ + + +# network rm + + Usage: docker network rm [OPTIONS] NETWORK [NETWORK...] + + Deletes one or more networks + + --help Print usage + +Removes one or more networks by name or identifier. To remove a network, +you must first disconnect any containers connected to it. +To remove the network named 'my-network': + +```bash + $ docker network rm my-network +``` + +To delete multiple networks in a single `docker network rm` command, provide +multiple network names or ids. The following example deletes a network with id +`3695c422697f` and a network named `my-network`: + +```bash + $ docker network rm 3695c422697f my-network +``` + +When you specify multiple networks, the command attempts to delete each in turn. +If the deletion of one network fails, the command continues to the next on the +list and tries to delete that. The command reports success or failure for each +deletion. + +## Related information + +* [network disconnect ](network_disconnect.md) +* [network connect](network_connect.md) +* [network create](network_create.md) +* [network ls](network_ls.md) +* [network inspect](network_inspect.md) +* [Understand Docker container networks](../../userguide/networking/dockernetworks.md) diff --git a/docs/reference/commandline/pause.md b/docs/reference/commandline/pause.md new file mode 100644 index 00000000..36d5416f --- /dev/null +++ b/docs/reference/commandline/pause.md @@ -0,0 +1,27 @@ + + +# pause + + Usage: docker pause [OPTIONS] CONTAINER [CONTAINER...] + + Pause all processes within a container + + --help Print usage + +The `docker pause` command uses the cgroups freezer to suspend all processes in +a container. Traditionally, when suspending a process the `SIGSTOP` signal is +used, which is observable by the process being suspended. With the cgroups freezer +the process is unaware, and unable to capture, that it is being suspended, +and subsequently resumed. + +See the +[cgroups freezer documentation](https://www.kernel.org/doc/Documentation/cgroups/freezer-subsystem.txt) +for further details. diff --git a/docs/reference/commandline/port.md b/docs/reference/commandline/port.md new file mode 100644 index 00000000..dbfae610 --- /dev/null +++ b/docs/reference/commandline/port.md @@ -0,0 +1,34 @@ + + +# port + + Usage: docker port [OPTIONS] CONTAINER [PRIVATE_PORT[/PROTO]] + + List port mappings for the CONTAINER, or lookup the public-facing port that is + NAT-ed to the PRIVATE_PORT + + --help Print usage + +You can find out all the ports mapped by not specifying a `PRIVATE_PORT`, or +just a specific mapping: + + $ docker ps + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + b650456536c7 busybox:latest top 54 minutes ago Up 54 minutes 0.0.0.0:1234->9876/tcp, 0.0.0.0:4321->7890/tcp test + $ docker port test + 7890/tcp -> 0.0.0.0:4321 + 9876/tcp -> 0.0.0.0:1234 + $ docker port test 7890/tcp + 0.0.0.0:4321 + $ docker port test 7890/udp + 2014/06/24 11:53:36 Error: No public port '7890/udp' published for test + $ docker port test 7890 + 0.0.0.0:4321 diff --git a/docs/reference/commandline/ps.md b/docs/reference/commandline/ps.md new file mode 100644 index 00000000..1dda728a --- /dev/null +++ b/docs/reference/commandline/ps.md @@ -0,0 +1,251 @@ + + +# ps + + Usage: docker ps [OPTIONS] + + List containers + + -a, --all Show all containers (default shows just running) + -f, --filter=[] Filter output based on these conditions: + - exited= an exit code of + - label= or label== + - status=(created|restarting|running|paused|exited) + - name= a container's name + - id= a container's ID + - before=(|) + - since=(|) + - ancestor=([:tag]||) - containers created from an image or a descendant. + - volume=(|) + --format=[] Pretty-print containers using a Go template + --help Print usage + -l, --latest Show the latest created container (includes all states) + -n=-1 Show n last created containers (includes all states) + --no-trunc Don't truncate output + -q, --quiet Only display numeric IDs + -s, --size Display total file sizes + +Running `docker ps --no-trunc` showing 2 linked containers. + + $ docker ps + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 4c01db0b339c ubuntu:12.04 bash 17 seconds ago Up 16 seconds 3300-3310/tcp webapp + d7886598dbe2 crosbymichael/redis:latest /redis-server --dir 33 minutes ago Up 33 minutes 6379/tcp redis,webapp/db + +`docker ps` will show only running containers by default. To see all containers: +`docker ps -a` + +`docker ps` will group exposed ports into a single range if possible. E.g., a container that exposes TCP ports `100, 101, 102` will display `100-102/tcp` in the `PORTS` column. + +## Filtering + +The filtering flag (`-f` or `--filter`) format is a `key=value` pair. If there is more +than one filter, then pass multiple flags (e.g. `--filter "foo=bar" --filter "bif=baz"`) + +The currently supported filters are: + +* id (container's id) +* label (`label=` or `label==`) +* name (container's name) +* exited (int - the code of exited containers. Only useful with `--all`) +* status (created|restarting|running|paused|exited|dead) +* ancestor (`[:]`, `` or ``) - filters containers that were created from the given image or a descendant. +* before (container's id or name) - filters containers created before given id or name +* since (container's id or name) - filters containers created since given id or name +* isolation (default|process|hyperv) (Windows daemon only) +* volume (volume name or mount point) - filters containers that mount volumes. + + +#### Label + +The `label` filter matches containers based on the presence of a `label` alone or a `label` and a +value. + +The following filter matches containers with the `color` label regardless of its value. + + $ docker ps --filter "label=color" + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 673394ef1d4c busybox "top" 47 seconds ago Up 45 seconds nostalgic_shockley + d85756f57265 busybox "top" 52 seconds ago Up 51 seconds high_albattani + +The following filter matches containers with the `color` label with the `blue` value. + + $ docker ps --filter "label=color=blue" + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + d85756f57265 busybox "top" About a minute ago Up About a minute high_albattani + +#### Name + +The `name` filter matches on all or part of a container's name. + +The following filter matches all containers with a name containing the `nostalgic_stallman` string. + + $ docker ps --filter "name=nostalgic_stallman" + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 9b6247364a03 busybox "top" 2 minutes ago Up 2 minutes nostalgic_stallman + +You can also filter for a substring in a name as this shows: + + $ docker ps --filter "name=nostalgic" + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 715ebfcee040 busybox "top" 3 seconds ago Up 1 seconds i_am_nostalgic + 9b6247364a03 busybox "top" 7 minutes ago Up 7 minutes nostalgic_stallman + 673394ef1d4c busybox "top" 38 minutes ago Up 38 minutes nostalgic_shockley + +#### Exited + +The `exited` filter matches containers by exist status code. For example, to filter for containers +that have exited successfully: + + $ docker ps -a --filter 'exited=0' + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + ea09c3c82f6e registry:latest /srv/run.sh 2 weeks ago Exited (0) 2 weeks ago 127.0.0.1:5000->5000/tcp desperate_leakey + 106ea823fe4e fedora:latest /bin/sh -c 'bash -l' 2 weeks ago Exited (0) 2 weeks ago determined_albattani + 48ee228c9464 fedora:20 bash 2 weeks ago Exited (0) 2 weeks ago tender_torvalds + +#### Status + +The `status` filter matches containers by status. You can filter using `created`, `restarting`, `running`, `paused`, `exited` and `dead`. For example, to filter for `running` containers: + + $ docker ps --filter status=running + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 715ebfcee040 busybox "top" 16 minutes ago Up 16 minutes i_am_nostalgic + d5c976d3c462 busybox "top" 23 minutes ago Up 23 minutes top + 9b6247364a03 busybox "top" 24 minutes ago Up 24 minutes nostalgic_stallman + +To filter for `paused` containers: + + $ docker ps --filter status=paused + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 673394ef1d4c busybox "top" About an hour ago Up About an hour (Paused) nostalgic_shockley + +#### Ancestor + +The `ancestor` filter matches containers based on its image or a descendant of it. The filter supports the +following image representation: + +- image +- image:tag +- image:tag@digest +- short-id +- full-id + +If you don't specify a `tag`, the `latest` tag is used. For example, to filter for containers that use the +latest `ubuntu` image: + + $ docker ps --filter ancestor=ubuntu + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 919e1179bdb8 ubuntu-c1 "top" About a minute ago Up About a minute admiring_lovelace + 5d1e4a540723 ubuntu-c2 "top" About a minute ago Up About a minute admiring_sammet + 82a598284012 ubuntu "top" 3 minutes ago Up 3 minutes sleepy_bose + bab2a34ba363 ubuntu "top" 3 minutes ago Up 3 minutes focused_yonath + +Match containers based on the `ubuntu-c1` image which, in this case, is a child of `ubuntu`: + + $ docker ps --filter ancestor=ubuntu-c1 + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 919e1179bdb8 ubuntu-c1 "top" About a minute ago Up About a minute admiring_lovelace + +Match containers based on the `ubuntu` version `12.04.5` image: + + $ docker ps --filter ancestor=ubuntu:12.04.5 + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 82a598284012 ubuntu:12.04.5 "top" 3 minutes ago Up 3 minutes sleepy_bose + +The following matches containers based on the layer `d0e008c6cf02` or an image that have this layer +in it's layer stack. + + $ docker ps --filter ancestor=d0e008c6cf02 + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 82a598284012 ubuntu:12.04.5 "top" 3 minutes ago Up 3 minutes sleepy_bose + +#### Before + +The `before` filter shows only containers created before the container with given id or name. For example, +having these containers created: + + $ docker ps + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 9c3527ed70ce busybox "top" 14 seconds ago Up 15 seconds desperate_dubinsky + 4aace5031105 busybox "top" 48 seconds ago Up 49 seconds focused_hamilton + 6e63f6ff38b0 busybox "top" About a minute ago Up About a minute distracted_fermat + +Filtering with `before` would give: + + $ docker ps -f before=9c3527ed70ce + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 4aace5031105 busybox "top" About a minute ago Up About a minute focused_hamilton + 6e63f6ff38b0 busybox "top" About a minute ago Up About a minute distracted_fermat + +#### Since + +The `since` filter shows only containers created since the container with given id or name. For example, +with the same containers as in `before` filter: + + $ docker ps -f since=6e63f6ff38b0 + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 9c3527ed70ce busybox "top" 10 minutes ago Up 10 minutes desperate_dubinsky + 4aace5031105 busybox "top" 10 minutes ago Up 10 minutes focused_hamilton + +#### Volume + +The `volume` filter shows only containers that mount a specific volume or have a volume mounted in a specific path: + + $ docker ps --filter volume=remote-volume --format "table {{.ID}}\t{{.Mounts}}" + CONTAINER ID MOUNTS + 9c3527ed70ce remote-volume + + $ docker ps --filter volume=/data --format "table {{.ID}}\t{{.Mounts}}" + CONTAINER ID MOUNTS + 9c3527ed70ce remote-volume + + +## Formatting + +The formatting option (`--format`) will pretty-print container output using a Go template. + +Valid placeholders for the Go template are listed below: + +Placeholder | Description +---- | ---- +`.ID` | Container ID +`.Image` | Image ID +`.Command` | Quoted command +`.CreatedAt` | Time when the container was created. +`.RunningFor` | Elapsed time since the container was started. +`.Ports` | Exposed ports. +`.Status` | Container status. +`.Size` | Container disk size. +`.Names` | Container names. +`.Labels` | All labels assigned to the container. +`.Label` | Value of a specific label for this container. For example `{{.Label "com.docker.swarm.cpu"}}` +`.Mounts` | Names of the volumes mounted in this container. + +When using the `--format` option, the `ps` command will either output the data exactly as the template +declares or, when using the `table` directive, will include column headers as well. + +The following example uses a template without headers and outputs the `ID` and `Command` +entries separated by a colon for all running containers: + + $ docker ps --format "{{.ID}}: {{.Command}}" + a87ecb4f327c: /bin/sh -c #(nop) MA + 01946d9d34d8: /bin/sh -c #(nop) MA + c1d3b0166030: /bin/sh -c yum -y up + 41d50ecd2f57: /bin/sh -c #(nop) MA + +To list all running containers with their labels in a table format you can use: + + $ docker ps --format "table {{.ID}}\t{{.Labels}}" + CONTAINER ID LABELS + a87ecb4f327c com.docker.swarm.node=ubuntu,com.docker.swarm.storage=ssd + 01946d9d34d8 + c1d3b0166030 com.docker.swarm.node=debian,com.docker.swarm.cpu=6 + 41d50ecd2f57 com.docker.swarm.node=fedora,com.docker.swarm.cpu=3,com.docker.swarm.storage=ssd diff --git a/docs/reference/commandline/pull.md b/docs/reference/commandline/pull.md new file mode 100644 index 00000000..01ec88e8 --- /dev/null +++ b/docs/reference/commandline/pull.md @@ -0,0 +1,228 @@ + + +# pull + + Usage: docker pull [OPTIONS] NAME[:TAG] | [REGISTRY_HOST[:REGISTRY_PORT]/]NAME[:TAG] + + Pull an image or a repository from the registry + + -a, --all-tags Download all tagged images in the repository + --disable-content-trust=true Skip image verification + --help Print usage + +Most of your images will be created on top of a base image from the +[Docker Hub](https://hub.docker.com) registry. + +[Docker Hub](https://hub.docker.com) contains many pre-built images that you +can `pull` and try without needing to define and configure your own. + +To download a particular image, or set of images (i.e., a repository), +use `docker pull`. + +## Examples + +### Pull an image from Docker Hub + +To download a particular image, or set of images (i.e., a repository), use +`docker pull`. If no tag is provided, Docker Engine uses the `:latest` tag as a +default. This command pulls the `debian:latest` image: + +```bash +$ docker pull debian + +Using default tag: latest +latest: Pulling from library/debian +fdd5d7827f33: Pull complete +a3ed95caeb02: Pull complete +Digest: sha256:e7d38b3517548a1c71e41bffe9c8ae6d6d29546ce46bf62159837aad072c90aa +Status: Downloaded newer image for debian:latest +``` + +Docker images can consist of multiple layers. In the example above, the image +consists of two layers; `fdd5d7827f33` and `a3ed95caeb02`. + +Layers can be reused by images. For example, the `debian:jessie` image shares +both layers with `debian:latest`. Pulling the `debian:jessie` image therefore +only pulls its metadata, but not its layers, because all layers are already +present locally: + +```bash +$ docker pull debian:jessie + +jessie: Pulling from library/debian +fdd5d7827f33: Already exists +a3ed95caeb02: Already exists +Digest: sha256:a9c958be96d7d40df920e7041608f2f017af81800ca5ad23e327bc402626b58e +Status: Downloaded newer image for debian:jessie +``` + +To see which images are present locally, use the [`docker images`](images.md) +command: + +```bash +$ docker images + +REPOSITORY TAG IMAGE ID CREATED SIZE +debian jessie f50f9524513f 5 days ago 125.1 MB +debian latest f50f9524513f 5 days ago 125.1 MB +``` + +Docker uses a content-addressable image store, and the image ID is a SHA256 +digest covering the image's configuration and layers. In the example above, +`debian:jessie` and `debian:latest` have the same image ID because they are +actually the *same* image tagged with different names. Because they are the +same image, their layers are stored only once and do not consume extra disk +space. + +For more information about images, layers, and the content-addressable store, +refer to [understand images, containers, and storage drivers](../../userguide/storagedriver/imagesandcontainers.md). + + +## Pull an image by digest (immutable identifier) + +So far, you've pulled images by their name (and "tag"). Using names and tags is +a convenient way to work with images. When using tags, you can `docker pull` an +image again to make sure you have the most up-to-date version of that image. +For example, `docker pull ubuntu:14.04` pulls the latest version of the Ubuntu +14.04 image. + +In some cases you don't want images to be updated to newer versions, but prefer +to use a fixed version of an image. Docker enables you to pull an image by its +*digest*. When pulling an image by digest, you specify *exactly* which version +of an image to pull. Doing so, allows you to "pin" an image to that version, +and guarantee that the image you're using is always the same. + +To know the digest of an image, pull the image first. Let's pull the latest +`ubuntu:14.04` image from Docker Hub: + +```bash +$ docker pull ubuntu:14.04 + +14.04: Pulling from library/ubuntu +5a132a7e7af1: Pull complete +fd2731e4c50c: Pull complete +28a2f68d1120: Pull complete +a3ed95caeb02: Pull complete +Digest: sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2 +Status: Downloaded newer image for ubuntu:14.04 +``` + +Docker prints the digest of the image after the pull has finished. In the example +above, the digest of the image is: + + sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2 + +Docker also prints the digest of an image when *pushing* to a registry. This +may be useful if you want to pin to a version of the image you just pushed. + +A digest takes the place of the tag when pulling an image, for example, to +pull the above image by digest, run the following command: + +```bash +$ docker pull ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2 + +sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2: Pulling from library/ubuntu +5a132a7e7af1: Already exists +fd2731e4c50c: Already exists +28a2f68d1120: Already exists +a3ed95caeb02: Already exists +Digest: sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2 +Status: Downloaded newer image for ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2 +``` + +Digest can also be used in the `FROM` of a Dockerfile, for example: + +```Dockerfile +FROM ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2 +MAINTAINER some maintainer +``` + +> **Note**: Using this feature "pins" an image to a specific version in time. +> Docker will therefore not pull updated versions of an image, which may include +> security updates. If you want to pull an updated image, you need to change the +> digest accordingly. + + +## Pulling from a different registry + +By default, `docker pull` pulls images from Docker Hub. It is also possible to +manually specify the path of a registry to pull from. For example, if you have +set up a local registry, you can specify its path to pull from it. A registry +path is similar to a URL, but does not contain a protocol specifier (`https://`). + +The following command pulls the `testing/test-image` image from a local registry +listening on port 5000 (`myregistry.local:5000`): + +```bash +$ docker pull myregistry.local:5000/testing/test-image +``` + +Registry credentials are managed by [docker login](login.md). + +Docker uses the `https://` protocol to communicate with a registry, unless the +registry is allowed to be accessed over an insecure connection. Refer to the +[insecure registries](daemon.md#insecure-registries) section for more information. + + +## Pull a repository with multiple images + +By default, `docker pull` pulls a *single* image from the registry. A repository +can contain multiple images. To pull all images from a repository, provide the +`-a` (or `--all-tags`) option when using `docker pull`. + +This command pulls all images from the `fedora` repository: + +```bash +$ docker pull --all-tags fedora + +Pulling repository fedora +ad57ef8d78d7: Download complete +105182bb5e8b: Download complete +511136ea3c5a: Download complete +73bd853d2ea5: Download complete +.... + +Status: Downloaded newer image for fedora +``` + +After the pull has completed use the `docker images` command to see the +images that were pulled. The example below shows all the `fedora` images +that are present locally: + +```bash +$ docker images fedora + +REPOSITORY TAG IMAGE ID CREATED SIZE +fedora rawhide ad57ef8d78d7 5 days ago 359.3 MB +fedora 20 105182bb5e8b 5 days ago 372.7 MB +fedora heisenbug 105182bb5e8b 5 days ago 372.7 MB +fedora latest 105182bb5e8b 5 days ago 372.7 MB +``` + +## Canceling a pull + +Killing the `docker pull` process, for example by pressing `CTRL-c` while it is +running in a terminal, will terminate the pull operation. + +```bash +$ docker pull fedora + +Using default tag: latest +latest: Pulling from library/fedora +a3ed95caeb02: Pulling fs layer +236608c7b546: Pulling fs layer +^C +``` + +> **Note**: Technically, the Engine terminates a pull operation when the +> connection between the Docker Engine daemon and the Docker Engine client +> initiating the pull is lost. If the connection with the Engine daemon is +> lost for other reasons than a manual interaction, the pull is also aborted. diff --git a/docs/reference/commandline/push.md b/docs/reference/commandline/push.md new file mode 100644 index 00000000..81091b14 --- /dev/null +++ b/docs/reference/commandline/push.md @@ -0,0 +1,26 @@ + + +# push + + Usage: docker push [OPTIONS] NAME[:TAG] + + Push an image or a repository to the registry + + --disable-content-trust=true Skip image signing + --help Print usage + +Use `docker push` to share your images to the [Docker Hub](https://hub.docker.com) +registry or to a self-hosted one. + +Killing the `docker push` process, for example by pressing `CTRL-c` while it is +running in a terminal, will terminate the push operation. + +Registry credentials are managed by [docker login](login.md). diff --git a/docs/reference/commandline/rename.md b/docs/reference/commandline/rename.md new file mode 100644 index 00000000..3e2b3703 --- /dev/null +++ b/docs/reference/commandline/rename.md @@ -0,0 +1,19 @@ + + +# rename + + Usage: docker rename [OPTIONS] OLD_NAME NEW_NAME + + Rename a container + + --help Print usage + +The `docker rename` command allows the container to be renamed to a different name. diff --git a/docs/reference/commandline/restart.md b/docs/reference/commandline/restart.md new file mode 100644 index 00000000..5e6633c8 --- /dev/null +++ b/docs/reference/commandline/restart.md @@ -0,0 +1,18 @@ + + +# restart + + Usage: docker restart [OPTIONS] CONTAINER [CONTAINER...] + + Restart a container + + --help Print usage + -t, --time=10 Seconds to wait for stop before killing the container diff --git a/docs/reference/commandline/rm.md b/docs/reference/commandline/rm.md new file mode 100644 index 00000000..bf615b55 --- /dev/null +++ b/docs/reference/commandline/rm.md @@ -0,0 +1,61 @@ + + +# rm + + Usage: docker rm [OPTIONS] CONTAINER [CONTAINER...] + + Remove one or more containers + + -f, --force Force the removal of a running container (uses SIGKILL) + --help Print usage + -l, --link Remove the specified link + -v, --volumes Remove the volumes associated with the container + +## Examples + + $ docker rm /redis + /redis + +This will remove the container referenced under the link +`/redis`. + + $ docker rm --link /webapp/redis + /webapp/redis + +This will remove the underlying link between `/webapp` and the `/redis` +containers removing all network communication. + + $ docker rm --force redis + redis + +The main process inside the container referenced under the link `/redis` will receive +`SIGKILL`, then the container will be removed. + + $ docker rm $(docker ps -a -q) + +This command will delete all stopped containers. The command +`docker ps -a -q` will return all existing container IDs and pass them to +the `rm` command which will delete them. Any running containers will not be +deleted. + + $ docker rm -v redis + redis + +This command will remove the container and any volumes associated with it. +Note that if a volume was specified with a name, it will not be removed. + + $ docker create -v awesome:/foo -v /bar --name hello redis + hello + $ docker rm -v hello + +In this example, the volume for `/foo` will remain intact, but the volume for +`/bar` will be removed. The same behavior holds for volumes inherited with +`--volumes-from`. diff --git a/docs/reference/commandline/rmi.md b/docs/reference/commandline/rmi.md new file mode 100644 index 00000000..f02734e8 --- /dev/null +++ b/docs/reference/commandline/rmi.md @@ -0,0 +1,75 @@ + + +# rmi + + Usage: docker rmi [OPTIONS] IMAGE [IMAGE...] + + Remove one or more images + + -f, --force Force removal of the image + --help Print usage + --no-prune Do not delete untagged parents + +You can remove an image using its short or long ID, its tag, or its digest. If +an image has one or more tag referencing it, you must remove all of them before +the image is removed. Digest references are removed automatically when an image +is removed by tag. + + $ docker images + REPOSITORY TAG IMAGE ID CREATED SIZE + test1 latest fd484f19954f 23 seconds ago 7 B (virtual 4.964 MB) + test latest fd484f19954f 23 seconds ago 7 B (virtual 4.964 MB) + test2 latest fd484f19954f 23 seconds ago 7 B (virtual 4.964 MB) + + $ docker rmi fd484f19954f + Error: Conflict, cannot delete image fd484f19954f because it is tagged in multiple repositories, use -f to force + 2013/12/11 05:47:16 Error: failed to remove one or more images + + $ docker rmi test1 + Untagged: test1:latest + $ docker rmi test2 + Untagged: test2:latest + + $ docker images + REPOSITORY TAG IMAGE ID CREATED SIZE + test latest fd484f19954f 23 seconds ago 7 B (virtual 4.964 MB) + $ docker rmi test + Untagged: test:latest + Deleted: fd484f19954f4920da7ff372b5067f5b7ddb2fd3830cecd17b96ea9e286ba5b8 + +If you use the `-f` flag and specify the image's short or long ID, then this +command untags and removes all images that match the specified ID. + + $ docker images + REPOSITORY TAG IMAGE ID CREATED SIZE + test1 latest fd484f19954f 23 seconds ago 7 B (virtual 4.964 MB) + test latest fd484f19954f 23 seconds ago 7 B (virtual 4.964 MB) + test2 latest fd484f19954f 23 seconds ago 7 B (virtual 4.964 MB) + + $ docker rmi -f fd484f19954f + Untagged: test1:latest + Untagged: test:latest + Untagged: test2:latest + Deleted: fd484f19954f4920da7ff372b5067f5b7ddb2fd3830cecd17b96ea9e286ba5b8 + +An image pulled by digest has no tag associated with it: + + $ docker images --digests + REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE + localhost:5000/test/busybox sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf 4986bf8c1536 9 weeks ago 2.43 MB + +To remove an image using its digest: + + $ docker rmi localhost:5000/test/busybox@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf + Untagged: localhost:5000/test/busybox@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf + Deleted: 4986bf8c15363d1c5d15512d5266f8777bfba4974ac56e3270e7760f6f0a8125 + Deleted: ea13149945cb6b1e746bf28032f02e9b5a793523481a0a18645fc77ad53c4ea2 + Deleted: df7546f9f060a2268024c8a230d8639878585defcc1bc6f79d2728a13957871b diff --git a/docs/reference/commandline/run.md b/docs/reference/commandline/run.md new file mode 100644 index 00000000..97553a67 --- /dev/null +++ b/docs/reference/commandline/run.md @@ -0,0 +1,614 @@ + + +# run + + Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...] + + Run a command in a new container + + -a, --attach=[] Attach to STDIN, STDOUT or STDERR + --add-host=[] Add a custom host-to-IP mapping (host:ip) + --blkio-weight=0 Block IO weight (relative weight) + --blkio-weight-device=[] Block IO weight (relative device weight, format: `DEVICE_NAME:WEIGHT`) + --cpu-shares=0 CPU shares (relative weight) + --cap-add=[] Add Linux capabilities + --cap-drop=[] Drop Linux capabilities + --cgroup-parent="" Optional parent cgroup for the container + --cidfile="" Write the container ID to the file + --cpu-period=0 Limit CPU CFS (Completely Fair Scheduler) period + --cpu-quota=0 Limit CPU CFS (Completely Fair Scheduler) quota + --cpuset-cpus="" CPUs in which to allow execution (0-3, 0,1) + --cpuset-mems="" Memory nodes (MEMs) in which to allow execution (0-3, 0,1) + -d, --detach Run container in background and print container ID + --detach-keys Specify the escape key sequence used to detach a container + --device=[] Add a host device to the container + --device-read-bps=[] Limit read rate (bytes per second) from a device (e.g., --device-read-bps=/dev/sda:1mb) + --device-read-iops=[] Limit read rate (IO per second) from a device (e.g., --device-read-iops=/dev/sda:1000) + --device-write-bps=[] Limit write rate (bytes per second) to a device (e.g., --device-write-bps=/dev/sda:1mb) + --device-write-iops=[] Limit write rate (IO per second) to a device (e.g., --device-write-bps=/dev/sda:1000) + --disable-content-trust=true Skip image verification + --dns=[] Set custom DNS servers + --dns-opt=[] Set custom DNS options + --dns-search=[] Set custom DNS search domains + -e, --env=[] Set environment variables + --entrypoint="" Overwrite the default ENTRYPOINT of the image + --env-file=[] Read in a file of environment variables + --expose=[] Expose a port or a range of ports + --group-add=[] Add additional groups to run as + -h, --hostname="" Container host name + --help Print usage + -i, --interactive Keep STDIN open even if not attached + --ip="" Container IPv4 address (e.g. 172.30.100.104) + --ip6="" Container IPv6 address (e.g. 2001:db8::33) + --ipc="" IPC namespace to use + --isolation="" Container isolation technology + --kernel-memory="" Kernel memory limit + -l, --label=[] Set metadata on the container (e.g., --label=com.example.key=value) + --label-file=[] Read in a file of labels (EOL delimited) + --link=[] Add link to another container + --log-driver="" Logging driver for container + --log-opt=[] Log driver specific options + -m, --memory="" Memory limit + --mac-address="" Container MAC address (e.g. 92:d0:c6:0a:29:33) + --memory-reservation="" Memory soft limit + --memory-swap="" A positive integer equal to memory plus swap. Specify -1 to enable unlimited swap. + --memory-swappiness="" Tune a container's memory swappiness behavior. Accepts an integer between 0 and 100. + --name="" Assign a name to the container + --net="bridge" Connect a container to a network + 'bridge': create a network stack on the default Docker bridge + 'none': no networking + 'container:': reuse another container's network stack + 'host': use the Docker host network stack + '|': connect to a user-defined network + --net-alias=[] Add network-scoped alias for the container + --oom-kill-disable Whether to disable OOM Killer for the container or not + --oom-score-adj=0 Tune the host's OOM preferences for containers (accepts -1000 to 1000) + -P, --publish-all Publish all exposed ports to random ports + -p, --publish=[] Publish a container's port(s) to the host + --pid="" PID namespace to use + --pids-limit=-1 Tune container pids limit (set -1 for unlimited), kernel >= 4.3 + --privileged Give extended privileges to this container + --read-only Mount the container's root filesystem as read only + --restart="no" Restart policy (no, on-failure[:max-retry], always, unless-stopped) + --rm Automatically remove the container when it exits + --shm-size=[] Size of `/dev/shm`. The format is ``. `number` must be greater than `0`. Unit is optional and can be `b` (bytes), `k` (kilobytes), `m` (megabytes), or `g` (gigabytes). If you omit the unit, the system uses bytes. If you omit the size entirely, the system uses `64m`. + --security-opt=[] Security Options + --sig-proxy=true Proxy received signals to the process + --stop-signal="SIGTERM" Signal to stop a container + -t, --tty Allocate a pseudo-TTY + -u, --user="" Username or UID (format: [:]) + --userns="" Container user namespace + 'host': Use the Docker host user namespace + '': Use the Docker daemon user namespace specified by `--userns-remap` option. + --ulimit=[] Ulimit options + --uts="" UTS namespace to use + -v, --volume=[host-src:]container-dest[:] + Bind mount a volume. The comma-delimited + `options` are [rw|ro], [z|Z], + [[r]shared|[r]slave|[r]private], and + [nocopy]. The 'host-src' is an absolute path + or a name value. + --volume-driver="" Container's volume driver + --volumes-from=[] Mount volumes from the specified container(s) + -w, --workdir="" Working directory inside the container + +The `docker run` command first `creates` a writeable container layer over the +specified image, and then `starts` it using the specified command. That is, +`docker run` is equivalent to the API `/containers/create` then +`/containers/(id)/start`. A stopped container can be restarted with all its +previous changes intact using `docker start`. See `docker ps -a` to view a list +of all containers. + +The `docker run` command can be used in combination with `docker commit` to +[*change the command that a container runs*](commit.md). There is additional detailed information about `docker run` in the [Docker run reference](../run.md). + +For information on connecting a container to a network, see the ["*Docker network overview*"](../../userguide/networking/index.md). + +## Examples + +### Assign name and allocate pseudo-TTY (--name, -it) + + $ docker run --name test -it debian + root@d6c0fe130dba:/# exit 13 + $ echo $? + 13 + $ docker ps -a | grep test + d6c0fe130dba debian:7 "/bin/bash" 26 seconds ago Exited (13) 17 seconds ago test + +This example runs a container named `test` using the `debian:latest` +image. The `-it` instructs Docker to allocate a pseudo-TTY connected to +the container's stdin; creating an interactive `bash` shell in the container. +In the example, the `bash` shell is quit by entering +`exit 13`. This exit code is passed on to the caller of +`docker run`, and is recorded in the `test` container's metadata. + +### Capture container ID (--cidfile) + + $ docker run --cidfile /tmp/docker_test.cid ubuntu echo "test" + +This will create a container and print `test` to the console. The `cidfile` +flag makes Docker attempt to create a new file and write the container ID to it. +If the file exists already, Docker will return an error. Docker will close this +file when `docker run` exits. + +### Full container capabilities (--privileged) + + $ docker run -t -i --rm ubuntu bash + root@bc338942ef20:/# mount -t tmpfs none /mnt + mount: permission denied + +This will *not* work, because by default, most potentially dangerous kernel +capabilities are dropped; including `cap_sys_admin` (which is required to mount +filesystems). However, the `--privileged` flag will allow it to run: + + $ docker run -t -i --privileged ubuntu bash + root@50e3f57e16e6:/# mount -t tmpfs none /mnt + root@50e3f57e16e6:/# df -h + Filesystem Size Used Avail Use% Mounted on + none 1.9G 0 1.9G 0% /mnt + +The `--privileged` flag gives *all* capabilities to the container, and it also +lifts all the limitations enforced by the `device` cgroup controller. In other +words, the container can then do almost everything that the host can do. This +flag exists to allow special use-cases, like running Docker within Docker. + +### Set working directory (-w) + + $ docker run -w /path/to/dir/ -i -t ubuntu pwd + +The `-w` lets the command being executed inside directory given, here +`/path/to/dir/`. If the path does not exists it is created inside the container. + +### Mount tmpfs (--tmpfs) + + $ docker run -d --tmpfs /run:rw,noexec,nosuid,size=65536k my_image + +The `--tmpfs` flag mounts an empty tmpfs into the container with the `rw`, +`noexec`, `nosuid`, `size=65536k` options. + +### Mount volume (-v, --read-only) + + $ docker run -v `pwd`:`pwd` -w `pwd` -i -t ubuntu pwd + +The `-v` flag mounts the current working directory into the container. The `-w` +lets the command being executed inside the current working directory, by +changing into the directory to the value returned by `pwd`. So this +combination executes the command using the container, but inside the +current working directory. + + $ docker run -v /doesnt/exist:/foo -w /foo -i -t ubuntu bash + +When the host directory of a bind-mounted volume doesn't exist, Docker +will automatically create this directory on the host for you. In the +example above, Docker will create the `/doesnt/exist` +folder before starting your container. + + $ docker run --read-only -v /icanwrite busybox touch /icanwrite here + +Volumes can be used in combination with `--read-only` to control where +a container writes files. The `--read-only` flag mounts the container's root +filesystem as read only prohibiting writes to locations other than the +specified volumes for the container. + + $ docker run -t -i -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/static-docker-binary:/usr/bin/docker busybox sh + +By bind-mounting the docker unix socket and statically linked docker +binary (refer to [get the linux binary]( +../../installation/binaries.md#get-the-linux-binary)), +you give the container the full access to create and manipulate the host's +Docker daemon. + +### Publish or expose port (-p, --expose) + + $ docker run -p 127.0.0.1:80:8080 ubuntu bash + +This binds port `8080` of the container to port `80` on `127.0.0.1` of the host +machine. The [Docker User +Guide](../../userguide/networking/default_network/dockerlinks.md) +explains in detail how to manipulate ports in Docker. + + $ docker run --expose 80 ubuntu bash + +This exposes port `80` of the container without publishing the port to the host +system's interfaces. + +### Set environment variables (-e, --env, --env-file) + + $ docker run -e MYVAR1 --env MYVAR2=foo --env-file ./env.list ubuntu bash + +This sets simple (non-array) environmental variables in the container. For +illustration all three +flags are shown here. Where `-e`, `--env` take an environment variable and +value, or if no `=` is provided, then that variable's current value is passed +through (i.e. `$MYVAR1` from the host is set to `$MYVAR1` in the container). +When no `=` is provided and that variable is not defined in the client's +environment then that variable will be removed from the container's list of +environment variables. +All three flags, `-e`, `--env` and `--env-file` can be repeated. + +Regardless of the order of these three flags, the `--env-file` are processed +first, and then `-e`, `--env` flags. This way, the `-e` or `--env` will +override variables as needed. + + $ cat ./env.list + TEST_FOO=BAR + $ docker run --env TEST_FOO="This is a test" --env-file ./env.list busybox env | grep TEST_FOO + TEST_FOO=This is a test + +The `--env-file` flag takes a filename as an argument and expects each line +to be in the `VAR=VAL` format, mimicking the argument passed to `--env`. Comment +lines need only be prefixed with `#` + +An example of a file passed with `--env-file` + + $ cat ./env.list + TEST_FOO=BAR + + # this is a comment + TEST_APP_DEST_HOST=10.10.0.127 + TEST_APP_DEST_PORT=8888 + _TEST_BAR=FOO + TEST_APP_42=magic + helloWorld=true + 123qwe=bar + org.spring.config=something + + # pass through this variable from the caller + TEST_PASSTHROUGH + $ TEST_PASSTHROUGH=howdy docker run --env-file ./env.list busybox env + PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + HOSTNAME=5198e0745561 + TEST_FOO=BAR + TEST_APP_DEST_HOST=10.10.0.127 + TEST_APP_DEST_PORT=8888 + _TEST_BAR=FOO + TEST_APP_42=magic + helloWorld=true + TEST_PASSTHROUGH=howdy + HOME=/root + 123qwe=bar + org.spring.config=something + + $ docker run --env-file ./env.list busybox env + PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + HOSTNAME=5198e0745561 + TEST_FOO=BAR + TEST_APP_DEST_HOST=10.10.0.127 + TEST_APP_DEST_PORT=8888 + _TEST_BAR=FOO + TEST_APP_42=magic + helloWorld=true + TEST_PASSTHROUGH= + HOME=/root + 123qwe=bar + org.spring.config=something + +### Set metadata on container (-l, --label, --label-file) + +A label is a `key=value` pair that applies metadata to a container. To label a container with two labels: + + $ docker run -l my-label --label com.example.foo=bar ubuntu bash + +The `my-label` key doesn't specify a value so the label defaults to an empty +string(`""`). To add multiple labels, repeat the label flag (`-l` or `--label`). + +The `key=value` must be unique to avoid overwriting the label value. If you +specify labels with identical keys but different values, each subsequent value +overwrites the previous. Docker uses the last `key=value` you supply. + +Use the `--label-file` flag to load multiple labels from a file. Delimit each +label in the file with an EOL mark. The example below loads labels from a +labels file in the current directory: + + $ docker run --label-file ./labels ubuntu bash + +The label-file format is similar to the format for loading environment +variables. (Unlike environment variables, labels are not visible to processes +running inside a container.) The following example illustrates a label-file +format: + + com.example.label1="a label" + + # this is a comment + com.example.label2=another\ label + com.example.label3 + +You can load multiple label-files by supplying multiple `--label-file` flags. + +For additional information on working with labels, see [*Labels - custom +metadata in Docker*](../../userguide/labels-custom-metadata.md) in the Docker User +Guide. + +### Connect a container to a network (--net) + +When you start a container use the `--net` flag to connect it to a network. +This adds the `busybox` container to the `my-net` network. + +```bash +$ docker run -itd --net=my-net busybox +``` + +You can also choose the IP addresses for the container with `--ip` and `--ip6` +flags when you start the container on a user-defined network. + +```bash +$ docker run -itd --net=my-net --ip=10.10.9.75 busybox +``` + +If you want to add a running container to a network use the `docker network connect` subcommand. + +You can connect multiple containers to the same network. Once connected, the +containers can communicate easily need only another container's IP address +or name. For `overlay` networks or custom plugins that support multi-host +connectivity, containers connected to the same multi-host network but launched +from different Engines can also communicate in this way. + +**Note**: Service discovery is unavailable on the default bridge network. +Containers can communicate via their IP addresses by default. To communicate +by name, they must be linked. + +You can disconnect a container from a network using the `docker network +disconnect` command. + +### Mount volumes from container (--volumes-from) + + $ docker run --volumes-from 777f7dc92da7 --volumes-from ba8c0c54f0f2:ro -i -t ubuntu pwd + +The `--volumes-from` flag mounts all the defined volumes from the referenced +containers. Containers can be specified by repetitions of the `--volumes-from` +argument. The container ID may be optionally suffixed with `:ro` or `:rw` to +mount the volumes in read-only or read-write mode, respectively. By default, +the volumes are mounted in the same mode (read write or read only) as +the reference container. + +Labeling systems like SELinux require that proper labels are placed on volume +content mounted into a container. Without a label, the security system might +prevent the processes running inside the container from using the content. By +default, Docker does not change the labels set by the OS. + +To change the label in the container context, you can add either of two suffixes +`:z` or `:Z` to the volume mount. These suffixes tell Docker to relabel file +objects on the shared volumes. The `z` option tells Docker that two containers +share the volume content. As a result, Docker labels the content with a shared +content label. Shared volume labels allow all containers to read/write content. +The `Z` option tells Docker to label the content with a private unshared label. +Only the current container can use a private volume. + +### Attach to STDIN/STDOUT/STDERR (-a) + +The `-a` flag tells `docker run` to bind to the container's `STDIN`, `STDOUT` +or `STDERR`. This makes it possible to manipulate the output and input as +needed. + + $ echo "test" | docker run -i -a stdin ubuntu cat - + +This pipes data into a container and prints the container's ID by attaching +only to the container's `STDIN`. + + $ docker run -a stderr ubuntu echo test + +This isn't going to print anything unless there's an error because we've +only attached to the `STDERR` of the container. The container's logs +still store what's been written to `STDERR` and `STDOUT`. + + $ cat somefile | docker run -i -a stdin mybuilder dobuild + +This is how piping a file into a container could be done for a build. +The container's ID will be printed after the build is done and the build +logs could be retrieved using `docker logs`. This is +useful if you need to pipe a file or something else into a container and +retrieve the container's ID once the container has finished running. + +### Add host device to container (--device) + + $ docker run --device=/dev/sdc:/dev/xvdc --device=/dev/sdd --device=/dev/zero:/dev/nulo -i -t ubuntu ls -l /dev/{xvdc,sdd,nulo} + brw-rw---- 1 root disk 8, 2 Feb 9 16:05 /dev/xvdc + brw-rw---- 1 root disk 8, 3 Feb 9 16:05 /dev/sdd + crw-rw-rw- 1 root root 1, 5 Feb 9 16:05 /dev/nulo + +It is often necessary to directly expose devices to a container. The `--device` +option enables that. For example, a specific block storage device or loop +device or audio device can be added to an otherwise unprivileged container +(without the `--privileged` flag) and have the application directly access it. + +By default, the container will be able to `read`, `write` and `mknod` these devices. +This can be overridden using a third `:rwm` set of options to each `--device` +flag: + + + $ docker run --device=/dev/sda:/dev/xvdc --rm -it ubuntu fdisk /dev/xvdc + + Command (m for help): q + $ docker run --device=/dev/sda:/dev/xvdc:r --rm -it ubuntu fdisk /dev/xvdc + You will not be able to write the partition table. + + Command (m for help): q + + $ docker run --device=/dev/sda:/dev/xvdc:rw --rm -it ubuntu fdisk /dev/xvdc + + Command (m for help): q + + $ docker run --device=/dev/sda:/dev/xvdc:m --rm -it ubuntu fdisk /dev/xvdc + fdisk: unable to open /dev/xvdc: Operation not permitted + +> **Note:** +> `--device` cannot be safely used with ephemeral devices. Block devices +> that may be removed should not be added to untrusted containers with +> `--device`. + +### Restart policies (--restart) + +Use Docker's `--restart` to specify a container's *restart policy*. A restart +policy controls whether the Docker daemon restarts a container after exit. +Docker supports the following restart policies: + + + + + + + + + + + + + + + + + + + + + + + + + + +
PolicyResult
no + Do not automatically restart the container when it exits. This is the + default. +
+ + on-failure[:max-retries] + + + Restart only if the container exits with a non-zero exit status. + Optionally, limit the number of restart retries the Docker + daemon attempts. +
always + Always restart the container regardless of the exit status. + When you specify always, the Docker daemon will try to restart + the container indefinitely. The container will also always start + on daemon startup, regardless of the current state of the container. +
unless-stopped + Always restart the container regardless of the exit status, but + do not start it on daemon startup if the container has been put + to a stopped state before. +
+ + $ docker run --restart=always redis + +This will run the `redis` container with a restart policy of **always** +so that if the container exits, Docker will restart it. + +More detailed information on restart policies can be found in the +[Restart Policies (--restart)](../run.md#restart-policies-restart) +section of the Docker run reference page. + +### Add entries to container hosts file (--add-host) + +You can add other hosts into a container's `/etc/hosts` file by using one or +more `--add-host` flags. This example adds a static address for a host named +`docker`: + + $ docker run --add-host=docker:10.180.0.1 --rm -it debian + $$ ping docker + PING docker (10.180.0.1): 48 data bytes + 56 bytes from 10.180.0.1: icmp_seq=0 ttl=254 time=7.600 ms + 56 bytes from 10.180.0.1: icmp_seq=1 ttl=254 time=30.705 ms + ^C--- docker ping statistics --- + 2 packets transmitted, 2 packets received, 0% packet loss + round-trip min/avg/max/stddev = 7.600/19.152/30.705/11.553 ms + +Sometimes you need to connect to the Docker host from within your +container. To enable this, pass the Docker host's IP address to +the container using the `--add-host` flag. To find the host's address, +use the `ip addr show` command. + +The flags you pass to `ip addr show` depend on whether you are +using IPv4 or IPv6 networking in your containers. Use the following +flags for IPv4 address retrieval for a network device named `eth0`: + + $ HOSTIP=`ip -4 addr show scope global dev eth0 | grep inet | awk '{print \$2}' | cut -d / -f 1` + $ docker run --add-host=docker:${HOSTIP} --rm -it debian + +For IPv6 use the `-6` flag instead of the `-4` flag. For other network +devices, replace `eth0` with the correct device name (for example `docker0` +for the bridge device). + +### Set ulimits in container (--ulimit) + +Since setting `ulimit` settings in a container requires extra privileges not +available in the default container, you can set these using the `--ulimit` flag. +`--ulimit` is specified with a soft and hard limit as such: +`=[:]`, for example: + + $ docker run --ulimit nofile=1024:1024 --rm debian sh -c "ulimit -n" + 1024 + +> **Note:** +> If you do not provide a `hard limit`, the `soft limit` will be used +> for both values. If no `ulimits` are set, they will be inherited from +> the default `ulimits` set on the daemon. `as` option is disabled now. +> In other words, the following script is not supported: +> `$ docker run -it --ulimit as=1024 fedora /bin/bash` + +The values are sent to the appropriate `syscall` as they are set. +Docker doesn't perform any byte conversion. Take this into account when setting the values. + +#### For `nproc` usage + +Be careful setting `nproc` with the `ulimit` flag as `nproc` is designed by Linux to set the +maximum number of processes available to a user, not to a container. For example, start four +containers with `daemon` user: + + docker run -d -u daemon --ulimit nproc=3 busybox top + docker run -d -u daemon --ulimit nproc=3 busybox top + docker run -d -u daemon --ulimit nproc=3 busybox top + docker run -d -u daemon --ulimit nproc=3 busybox top + +The 4th container fails and reports "[8] System error: resource temporarily unavailable" error. +This fails because the caller set `nproc=3` resulting in the first three containers using up +the three processes quota set for the `daemon` user. + +### Stop container with signal (--stop-signal) + +The `--stop-signal` flag sets the system call signal that will be sent to the container to exit. +This signal can be a valid unsigned number that matches a position in the kernel's syscall table, for instance 9, +or a signal name in the format SIGNAME, for instance SIGKILL. + +### Specify isolation technology for container (--isolation) + +This option is useful in situations where you are running Docker containers on +Microsoft Windows. The `--isolation ` option sets a container's isolation +technology. On Linux, the only supported is the `default` option which uses +Linux namespaces. These two commands are equivalent on Linux: + +``` +$ docker run -d busybox top +$ docker run -d --isolation default busybox top +``` + +On Microsoft Windows, can take any of these values: + + +| Value | Description | +|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `default` | Use the value specified by the Docker daemon's `--exec-opt` . If the `daemon` does not specify an isolation technology, Microsoft Windows uses `process` as its default value. | +| `process` | Namespace isolation only. | +| `hyperv` | Hyper-V hypervisor partition-based isolation. | + +In practice, when running on Microsoft Windows without a `daemon` option set, these two commands are equivalent: + +``` +$ docker run -d --isolation default busybox top +$ docker run -d --isolation process busybox top +``` + +If you have set the `--exec-opt isolation=hyperv` option on the Docker `daemon`, any of these commands also result in `hyperv` isolation: + +``` +$ docker run -d --isolation default busybox top +$ docker run -d --isolation hyperv busybox top +``` diff --git a/docs/reference/commandline/save.md b/docs/reference/commandline/save.md new file mode 100644 index 00000000..23fc6712 --- /dev/null +++ b/docs/reference/commandline/save.md @@ -0,0 +1,37 @@ + + +# save + + Usage: docker save [OPTIONS] IMAGE [IMAGE...] + + Save one or more images to a tar archive (streamed to STDOUT by default) + + --help Print usage + -o, --output="" Write to a file, instead of STDOUT + +Produces a tarred repository to the standard output stream. +Contains all parent layers, and all tags + versions, or specified `repo:tag`, for +each argument provided. + +It is used to create a backup that can then be used with `docker load` + + $ docker save busybox > busybox.tar + $ ls -sh busybox.tar + 2.7M busybox.tar + $ docker save --output busybox.tar busybox + $ ls -sh busybox.tar + 2.7M busybox.tar + $ docker save -o fedora-all.tar fedora + $ docker save -o fedora-latest.tar fedora:latest + +It is even useful to cherry-pick particular tags of an image repository + + $ docker save -o ubuntu.tar ubuntu:lucid ubuntu:saucy diff --git a/docs/reference/commandline/search.md b/docs/reference/commandline/search.md new file mode 100644 index 00000000..893895e2 --- /dev/null +++ b/docs/reference/commandline/search.md @@ -0,0 +1,97 @@ + + +# search + + Usage: docker search [OPTIONS] TERM + + Search the Docker Hub for images + + --automated Only show automated builds + --help Print usage + --no-trunc Don't truncate output + -s, --stars=0 Only displays with at least x stars + +Search [Docker Hub](https://hub.docker.com) for images + +See [*Find Public Images on Docker Hub*](../../userguide/containers/dockerrepos.md#searching-for-images) for +more details on finding shared images from the command line. + +> **Note:** +> Search queries will only return up to 25 results + +## Examples + +### Search images by name + +This example displays images with a name containing 'busybox': + + $ docker search busybox + NAME DESCRIPTION STARS OFFICIAL AUTOMATED + busybox Busybox base image. 316 [OK] + progrium/busybox 50 [OK] + radial/busyboxplus Full-chain, Internet enabled, busybox made... 8 [OK] + odise/busybox-python 2 [OK] + azukiapp/busybox This image is meant to be used as the base... 2 [OK] + ofayau/busybox-jvm Prepare busybox to install a 32 bits JVM. 1 [OK] + shingonoide/archlinux-busybox Arch Linux, a lightweight and flexible Lin... 1 [OK] + odise/busybox-curl 1 [OK] + ofayau/busybox-libc32 Busybox with 32 bits (and 64 bits) libs 1 [OK] + peelsky/zulu-openjdk-busybox 1 [OK] + skomma/busybox-data Docker image suitable for data volume cont... 1 [OK] + elektritter/busybox-teamspeak Leightweight teamspeak3 container based on... 1 [OK] + socketplane/busybox 1 [OK] + oveits/docker-nginx-busybox This is a tiny NginX docker image based on... 0 [OK] + ggtools/busybox-ubuntu Busybox ubuntu version with extra goodies 0 [OK] + nikfoundas/busybox-confd Minimal busybox based distribution of confd 0 [OK] + openshift/busybox-http-app 0 [OK] + jllopis/busybox 0 [OK] + swyckoff/busybox 0 [OK] + powellquiring/busybox 0 [OK] + williamyeh/busybox-sh Docker image for BusyBox's sh 0 [OK] + simplexsys/busybox-cli-powered Docker busybox images, with a few often us... 0 [OK] + fhisamoto/busybox-java Busybox java 0 [OK] + scottabernethy/busybox 0 [OK] + marclop/busybox-solr + +### Search images by name and number of stars (-s, --stars) + +This example displays images with a name containing 'busybox' and at +least 3 stars: + + $ docker search --stars=3 busybox + NAME DESCRIPTION STARS OFFICIAL AUTOMATED + busybox Busybox base image. 325 [OK] + progrium/busybox 50 [OK] + radial/busyboxplus Full-chain, Internet enabled, busybox made... 8 [OK] + + +### Search automated images (--automated) + +This example displays images with a name containing 'busybox', at +least 3 stars and are automated builds: + + $ docker search --stars=3 --automated busybox + NAME DESCRIPTION STARS OFFICIAL AUTOMATED + progrium/busybox 50 [OK] + radial/busyboxplus Full-chain, Internet enabled, busybox made... 8 [OK] + + +### Display non-truncated description (--no-trunc) + +This example displays images with a name containing 'busybox', +at least 3 stars and the description isn't truncated in the output: + + $ docker search --stars=3 --no-trunc busybox + NAME DESCRIPTION STARS OFFICIAL AUTOMATED + busybox Busybox base image. 325 [OK] + progrium/busybox 50 [OK] + radial/busyboxplus Full-chain, Internet enabled, busybox made from scratch. Comes in git and cURL flavors. 8 [OK] + diff --git a/docs/reference/commandline/start.md b/docs/reference/commandline/start.md new file mode 100644 index 00000000..156a2aae --- /dev/null +++ b/docs/reference/commandline/start.md @@ -0,0 +1,20 @@ + + +# start + + Usage: docker start [OPTIONS] CONTAINER [CONTAINER...] + + Start one or more containers + + -a, --attach Attach STDOUT/STDERR and forward signals + --detach-keys Specify the escape key sequence used to detach a container + --help Print usage + -i, --interactive Attach container's STDIN diff --git a/docs/reference/commandline/stats.md b/docs/reference/commandline/stats.md new file mode 100644 index 00000000..8ef7d6e1 --- /dev/null +++ b/docs/reference/commandline/stats.md @@ -0,0 +1,40 @@ + + +# stats + + Usage: docker stats [OPTIONS] [CONTAINER...] + + Display a live stream of one or more containers' resource usage statistics + + -a, --all Show all containers (default shows just running) + --help Print usage + --no-stream Disable streaming stats and only pull the first result + +The `docker stats` command returns a live data stream for running containers. To limit data to one or more specific containers, specify a list of container names or ids separated by a space. You can specify a stopped container but stopped containers do not return any data. + +If you want more detailed information about a container's resource usage, use the `/containers/(id)/stats` API endpoint. + +## Examples + +Running `docker stats` on all running containers + + $ docker stats + CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O + 1285939c1fd3 0.07% 796 KB / 64 MB 1.21% 788 B / 648 B 3.568 MB / 512 KB + 9c76f7834ae2 0.07% 2.746 MB / 64 MB 4.29% 1.266 KB / 648 B 12.4 MB / 0 B + d1ea048f04e4 0.03% 4.583 MB / 64 MB 6.30% 2.854 KB / 648 B 27.7 MB / 0 B + +Running `docker stats` on multiple containers by name and id. + + $ docker stats fervent_panini 5acfcb1b4fd1 + CONTAINER CPU % MEM USAGE/LIMIT MEM % NET I/O + 5acfcb1b4fd1 0.00% 115.2 MB/1.045 GB 11.03% 1.422 kB/648 B + fervent_panini 0.02% 11.08 MB/1.045 GB 1.06% 648 B/648 B diff --git a/docs/reference/commandline/stop.md b/docs/reference/commandline/stop.md new file mode 100644 index 00000000..1fb376ea --- /dev/null +++ b/docs/reference/commandline/stop.md @@ -0,0 +1,22 @@ + + +# stop + + Usage: docker stop [OPTIONS] CONTAINER [CONTAINER...] + + Stop a container by sending SIGTERM and then SIGKILL after a + grace period + + --help Print usage + -t, --time=10 Seconds to wait for stop before killing it + +The main process inside the container will receive `SIGTERM`, and after a grace +period, `SIGKILL`. diff --git a/docs/reference/commandline/tag.md b/docs/reference/commandline/tag.md new file mode 100644 index 00000000..cd104e8c --- /dev/null +++ b/docs/reference/commandline/tag.md @@ -0,0 +1,20 @@ + + +# tag + + Usage: docker tag [OPTIONS] IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG] + + Tag an image into a repository + + --help Print usage + +You can group your images together using names and tags, and then upload them +to [*Share Images via Repositories*](../../userguide/containers/dockerrepos.md#contributing-to-docker-hub). diff --git a/docs/reference/commandline/top.md b/docs/reference/commandline/top.md new file mode 100644 index 00000000..2bff2fdb --- /dev/null +++ b/docs/reference/commandline/top.md @@ -0,0 +1,17 @@ + + +# top + + Usage: docker top [OPTIONS] CONTAINER [ps OPTIONS] + + Display the running processes of a container + + --help Print usage diff --git a/docs/reference/commandline/unpause.md b/docs/reference/commandline/unpause.md new file mode 100644 index 00000000..641e3771 --- /dev/null +++ b/docs/reference/commandline/unpause.md @@ -0,0 +1,24 @@ + + +# unpause + + Usage: docker unpause [OPTIONS] CONTAINER [CONTAINER...] + + Unpause all processes within a container + + --help Print usage + +The `docker unpause` command uses the cgroups freezer to un-suspend all +processes in a container. + +See the +[cgroups freezer documentation](https://www.kernel.org/doc/Documentation/cgroups/freezer-subsystem.txt) +for further details. diff --git a/docs/reference/commandline/update.md b/docs/reference/commandline/update.md new file mode 100644 index 00000000..24fb1f29 --- /dev/null +++ b/docs/reference/commandline/update.md @@ -0,0 +1,73 @@ + + +## update + + Usage: docker update [OPTIONS] CONTAINER [CONTAINER...] + + Update configuration of one or more containers + + --help=false Print usage + --blkio-weight=0 Block IO (relative weight), between 10 and 1000 + --cpu-shares=0 CPU shares (relative weight) + --cpu-period=0 Limit the CPU CFS (Completely Fair Scheduler) period + --cpu-quota=0 Limit the CPU CFS (Completely Fair Scheduler) quota + --cpuset-cpus="" CPUs in which to allow execution (0-3, 0,1) + --cpuset-mems="" Memory nodes (MEMs) in which to allow execution (0-3, 0,1) + -m, --memory="" Memory limit + --memory-reservation="" Memory soft limit + --memory-swap="" A positive integer equal to memory plus swap. Specify -1 to enable unlimited swap + --kernel-memory="" Kernel memory limit: container must be stopped + --restart Restart policy to apply when a container exits + +The `docker update` command dynamically updates container configuration. +You can use this command to prevent containers from consuming too many resources +from their Docker host. With a single command, you can place limits on +a single container or on many. To specify more than one container, provide +space-separated list of container names or IDs. + +With the exception of the `--kernel-memory` value, you can specify these +options on a running or a stopped container. You can only update +`--kernel-memory` on a stopped container. When you run `docker update` on +stopped container, the next time you restart it, the container uses those +values. + +Another configuration you can change with this command is restart policy, +new restart policy will take effect instantly after you run `docker update` +on a container. + +## EXAMPLES + +The following sections illustrate ways to use this command. + +### Update a container with cpu-shares=512 + +To limit a container's cpu-shares to 512, first identify the container +name or ID. You can use **docker ps** to find these values. You can also +use the ID returned from the **docker run** command. Then, do the following: + +```bash +$ docker update --cpu-shares 512 abebf7571666 +``` + +### Update a container with cpu-shares and memory + +To update multiple resource configurations for multiple containers: + +```bash +$ docker update --cpu-shares 512 -m 300M abebf7571666 hopeful_morse +``` + +### Update a container's restart policy + +To update restart policy for one or more containers: +```bash +$ docker update --restart=on-failure:3 abebf7571666 hopeful_morse +``` diff --git a/docs/reference/commandline/version.md b/docs/reference/commandline/version.md new file mode 100644 index 00000000..14fac17e --- /dev/null +++ b/docs/reference/commandline/version.md @@ -0,0 +1,55 @@ + + +# version + + Usage: docker version [OPTIONS] + + Show the Docker version information. + + -f, --format="" Format the output using the given go template + --help Print usage + +By default, this will render all version information in an easy to read +layout. If a format is specified, the given template will be executed instead. + +Go's [text/template](http://golang.org/pkg/text/template/) package +describes all the details of the format. + +## Examples + +**Default output:** + + $ docker version + Client: + Version: 1.8.0 + API version: 1.20 + Go version: go1.4.2 + Git commit: f5bae0a + Built: Tue Jun 23 17:56:00 UTC 2015 + OS/Arch: linux/amd64 + + Server: + Version: 1.8.0 + API version: 1.20 + Go version: go1.4.2 + Git commit: f5bae0a + Built: Tue Jun 23 17:56:00 UTC 2015 + OS/Arch: linux/amd64 + +**Get server version:** + + $ docker version --format '{{.Server.Version}}' + 1.8.0 + +**Dump raw data:** + + $ docker version --format '{{json .}}' + {"Client":{"Version":"1.8.0","ApiVersion":"1.20","GitCommit":"f5bae0a","GoVersion":"go1.4.2","Os":"linux","Arch":"amd64","BuildTime":"Tue Jun 23 17:56:00 UTC 2015"},"ServerOK":true,"Server":{"Version":"1.8.0","ApiVersion":"1.20","GitCommit":"f5bae0a","GoVersion":"go1.4.2","Os":"linux","Arch":"amd64","KernelVersion":"3.13.2-gentoo","BuildTime":"Tue Jun 23 17:56:00 UTC 2015"}} diff --git a/docs/reference/commandline/volume_create.md b/docs/reference/commandline/volume_create.md new file mode 100644 index 00000000..112c260a --- /dev/null +++ b/docs/reference/commandline/volume_create.md @@ -0,0 +1,76 @@ + + +# volume create + + Usage: docker volume create [OPTIONS] + + Create a volume + + -d, --driver=local Specify volume driver name + --help Print usage + --label=[] Set metadata for a volume + --name= Specify volume name + -o, --opt=map[] Set driver specific options + +Creates a new volume that containers can consume and store data in. If a name is not specified, Docker generates a random name. You create a volume and then configure the container to use it, for example: + +```bash +$ docker volume create --name hello +hello + +$ docker run -d -v hello:/world busybox ls /world +``` + +The mount is created inside the container's `/world` directory. Docker does not support relative paths for mount points inside the container. + +Multiple containers can use the same volume in the same time period. This is useful if two containers need access to shared data. For example, if one container writes and the other reads the data. + +Volume names must be unique among drivers. This means you cannot use the same volume name with two different drivers. If you attempt this `docker` returns an error: + +``` +A volume named "hello" already exists with the "some-other" driver. Choose a different volume name. +``` + +If you specify a volume name already in use on the current driver, Docker assumes you want to re-use the existing volume and does not return an error. + +## Driver specific options + +Some volume drivers may take options to customize the volume creation. Use the `-o` or `--opt` flags to pass driver options: + +```bash +$ docker volume create --driver fake --opt tardis=blue --opt timey=wimey +``` + +These options are passed directly to the volume driver. Options for +different volume drivers may do different things (or nothing at all). + +The built-in `local` driver on Windows does not support any options. + +The built-in `local` driver on Linux accepts options similar to the linux `mount` +command: + +```bash +$ docker volume create --driver local --opt type=tmpfs --opt device=tmpfs --opt o=size=100m,uid=1000 +``` + +Another example: + +```bash +$ docker volume create --driver local --opt type=btrfs --opt device=/dev/sda2 +``` + + +## Related information + +* [volume inspect](volume_inspect.md) +* [volume ls](volume_ls.md) +* [volume rm](volume_rm.md) +* [Understand Data Volumes](../../userguide/containers/dockervolumes.md) diff --git a/docs/reference/commandline/volume_inspect.md b/docs/reference/commandline/volume_inspect.md new file mode 100644 index 00000000..8fdd34d9 --- /dev/null +++ b/docs/reference/commandline/volume_inspect.md @@ -0,0 +1,47 @@ + + +# volume inspect + + Usage: docker volume inspect [OPTIONS] VOLUME [VOLUME...] + + Return low-level information on a volume + + -f, --format= Format the output using the given go template. + --help Print usage + +Returns information about a volume. By default, this command renders all results +in a JSON array. You can specify an alternate format to execute a +given template for each result. Go's +[text/template](http://golang.org/pkg/text/template/) package describes all the +details of the format. + +Example output: + + $ docker volume create + 85bffb0677236974f93955d8ecc4df55ef5070117b0e53333cc1b443777be24d + $ docker volume inspect 85bffb0677236974f93955d8ecc4df55ef5070117b0e53333cc1b443777be24d + [ + { + "Name": "85bffb0677236974f93955d8ecc4df55ef5070117b0e53333cc1b443777be24d", + "Driver": "local", + "Mountpoint": "/var/lib/docker/volumes/85bffb0677236974f93955d8ecc4df55ef5070117b0e53333cc1b443777be24d/_data" + } + ] + + $ docker volume inspect --format '{{ .Mountpoint }}' 85bffb0677236974f93955d8ecc4df55ef5070117b0e53333cc1b443777be24d + /var/lib/docker/volumes/85bffb0677236974f93955d8ecc4df55ef5070117b0e53333cc1b443777be24d/_data + +## Related information + +* [volume create](volume_create.md) +* [volume ls](volume_ls.md) +* [volume rm](volume_rm.md) +* [Understand Data Volumes](../../userguide/containers/dockervolumes.md) \ No newline at end of file diff --git a/docs/reference/commandline/volume_ls.md b/docs/reference/commandline/volume_ls.md new file mode 100644 index 00000000..0388e8ae --- /dev/null +++ b/docs/reference/commandline/volume_ls.md @@ -0,0 +1,41 @@ + + +# volume ls + + Usage: docker volume ls [OPTIONS] + + List volumes + + -f, --filter=[] Provide filter values (i.e. 'dangling=true') + --help Print usage + -q, --quiet Only display volume names + +Lists all the volumes Docker knows about. You can filter using the `-f` or `--filter` flag. The filtering format is a `key=value` pair. To specify more than one filter, pass multiple flags (for example, `--filter "foo=bar" --filter "bif=baz"`) + +There is a single supported filter `dangling=value` which takes a boolean of `true` or `false`. + +Example output: + + $ docker volume create --name rose + rose + $docker volume create --name tyler + tyler + $ docker volume ls + DRIVER VOLUME NAME + local rose + local tyler + +## Related information + +* [volume create](volume_create.md) +* [volume inspect](volume_inspect.md) +* [volume rm](volume_rm.md) +* [Understand Data Volumes](../../userguide/containers/dockervolumes.md) \ No newline at end of file diff --git a/docs/reference/commandline/volume_rm.md b/docs/reference/commandline/volume_rm.md new file mode 100644 index 00000000..ff5ce24a --- /dev/null +++ b/docs/reference/commandline/volume_rm.md @@ -0,0 +1,29 @@ + + +# volume rm + + Usage: docker volume rm [OPTIONS] VOLUME [VOLUME...] + + Remove a volume + + --help Print usage + +Removes one or more volumes. You cannot remove a volume that is in use by a container. + + $ docker volume rm hello + hello + +## Related information + +* [volume create](volume_create.md) +* [volume inspect](volume_inspect.md) +* [volume ls](volume_ls.md) +* [Understand Data Volumes](../../userguide/containers/dockervolumes.md) \ No newline at end of file diff --git a/docs/reference/commandline/wait.md b/docs/reference/commandline/wait.md new file mode 100644 index 00000000..5cd9a7b9 --- /dev/null +++ b/docs/reference/commandline/wait.md @@ -0,0 +1,17 @@ + + +# wait + + Usage: docker wait [OPTIONS] CONTAINER [CONTAINER...] + + Block until a container stops, then print its exit code. + + --help Print usage diff --git a/docs/reference/glossary.md b/docs/reference/glossary.md new file mode 100644 index 00000000..22c2d36d --- /dev/null +++ b/docs/reference/glossary.md @@ -0,0 +1,221 @@ + + +# Glossary + +A list of terms used around the Docker project. + +## aufs + +aufs (advanced multi layered unification filesystem) is a Linux [filesystem](#filesystem) that +Docker supports as a storage backend. It implements the +[union mount](http://en.wikipedia.org/wiki/Union_mount) for Linux file systems. + +## Base image + +An image that has no parent is a **base image**. + +## boot2docker + +[boot2docker](http://boot2docker.io/) is a lightweight Linux distribution made +specifically to run Docker containers. The boot2docker management tool for Mac and Windows was deprecated and replaced by [`docker-machine`](#machine) which you can install with the Docker Toolbox. + +## btrfs + +btrfs (B-tree file system) is a Linux [filesystem](#filesystem) that Docker +supports as a storage backend. It is a [copy-on-write](http://en.wikipedia.org/wiki/Copy-on-write) +filesystem. + +## build + +build is the process of building Docker images using a [Dockerfile](#dockerfile). +The build uses a Dockerfile and a "context". The context is the set of files in the +directory in which the image is built. + +## cgroups + +cgroups is a Linux kernel feature that limits, accounts for, and isolates +the resource usage (CPU, memory, disk I/O, network, etc.) of a collection +of processes. Docker relies on cgroups to control and isolate resource limits. + +*Also known as : control groups* + +## Compose + +[Compose](https://github.com/docker/compose) is a tool for defining and +running complex applications with Docker. With compose, you define a +multi-container application in a single file, then spin your +application up in a single command which does everything that needs to +be done to get it running. + +*Also known as : docker-compose, fig* + +## container + +A container is a runtime instance of a [docker image](#image). + +A Docker container consists of + +- A Docker image +- Execution environment +- A standard set of instructions + +The concept is borrowed from Shipping Containers, which define a standard to ship +goods globally. Docker defines a standard to ship software. + +## data volume + +A data volume is a specially-designated directory within one or more containers +that bypasses the Union File System. Data volumes are designed to persist data, +independent of the container's life cycle. Docker therefore never automatically +delete volumes when you remove a container, nor will it "garbage collect" +volumes that are no longer referenced by a container. + + +## Docker + +The term Docker can refer to + +- The Docker project as a whole, which is a platform for developers and sysadmins to +develop, ship, and run applications +- The docker daemon process running on the host which manages images and containers + + +## Docker Hub + +The [Docker Hub](https://hub.docker.com/) is a centralized resource for working with +Docker and its components. It provides the following services: + +- Docker image hosting +- User authentication +- Automated image builds and work-flow tools such as build triggers and web hooks +- Integration with GitHub and Bitbucket + + +## Dockerfile + +A Dockerfile is a text document that contains all the commands you would +normally execute manually in order to build a Docker image. Docker can +build images automatically by reading the instructions from a Dockerfile. + +## filesystem + +A file system is the method an operating system uses to name files +and assign them locations for efficient storage and retrieval. + +Examples : + +- Linux : ext4, aufs, btrfs, zfs +- Windows : NTFS +- OS X : HFS+ + +## image + +Docker images are the basis of [containers](#container). An Image is an +ordered collection of root filesystem changes and the corresponding +execution parameters for use within a container runtime. An image typically +contains a union of layered filesystems stacked on top of each other. An image +does not have state and it never changes. + +## libcontainer + +libcontainer provides a native Go implementation for creating containers with +namespaces, cgroups, capabilities, and filesystem access controls. It allows +you to manage the lifecycle of the container performing additional operations +after the container is created. + +## libnetwork + +libnetwork provides a native Go implementation for creating and managing container +network namespaces and other network resources. It manage the networking lifecycle +of the container performing additional operations after the container is created. + +## link + +links provide a legacy interface to connect Docker containers running on the +same host to each other without exposing the hosts' network ports. Use the +Docker networks feature instead. + +## Machine + +[Machine](https://github.com/docker/machine) is a Docker tool which +makes it really easy to create Docker hosts on your computer, on +cloud providers and inside your own data center. It creates servers, +installs Docker on them, then configures the Docker client to talk to them. + +*Also known as : docker-machine* + +## overlay network driver + +Overlay network driver provides out of the box multi-host network connectivity +for docker containers in a cluster. + +## overlay storage driver + +OverlayFS is a [filesystem](#filesystem) service for Linux which implements a +[union mount](http://en.wikipedia.org/wiki/Union_mount) for other file systems. +It is supported by the Docker daemon as a storage driver. + +## registry + +A Registry is a hosted service containing [repositories](#repository) of [images](#image) +which responds to the Registry API. + +The default registry can be accessed using a browser at [Docker Hub](#docker-hub) +or using the `docker search` command. + +## repository + +A repository is a set of Docker images. A repository can be shared by pushing it +to a [registry](#registry) server. The different images in the repository can be +labeled using [tags](#tag). + +Here is an example of the shared [nginx repository](https://hub.docker.com/_/nginx/) +and its [tags](https://hub.docker.com/r/library/nginx/tags/) + +## Swarm + +[Swarm](https://github.com/docker/swarm) is a native clustering tool for Docker. +Swarm pools together several Docker hosts and exposes them as a single virtual +Docker host. It serves the standard Docker API, so any tool that already works +with Docker can now transparently scale up to multiple hosts. + +*Also known as : docker-swarm* + +## tag + +A tag is a label applied to a Docker image in a [repository](#repository). +tags are how various images in a repository are distinguished from each other. + +*Note : This label is not related to the key=value labels set for docker daemon* + +## Toolbox + +Docker Toolbox is the installer for Mac and Windows users. + + +## Union file system + +Union file systems, or UnionFS, are file systems that operate by creating layers, making them +very lightweight and fast. Docker uses union file systems to provide the building +blocks for containers. + + +## Virtual Machine + +A Virtual Machine is a program that emulates a complete computer and imitates dedicated hardware. +It shares physical hardware resources with other users but isolates the operating system. The +end user has the same experience on a Virtual Machine as they would have on dedicated hardware. + +Compared to to containers, a Virtual Machine is heavier to run, provides more isolation, +gets its own set of resources and does minimal sharing. + +*Also known as : VM* diff --git a/docs/reference/index.md b/docs/reference/index.md new file mode 100644 index 00000000..a207ee7d --- /dev/null +++ b/docs/reference/index.md @@ -0,0 +1,18 @@ + + +# Engine reference + +* [Dockerfile reference](builder.md) +* [Docker run reference](run.md) +* [Command line reference](commandline/index.md) +* [API Reference](api/index.md) diff --git a/docs/reference/run.md b/docs/reference/run.md new file mode 100644 index 00000000..b37dd5ae --- /dev/null +++ b/docs/reference/run.md @@ -0,0 +1,1468 @@ + + + + +# Docker run reference + +Docker runs processes in isolated containers. A container is a process +which runs on a host. The host may be local or remote. When an operator +executes `docker run`, the container process that runs is isolated in +that it has its own file system, its own networking, and its own +isolated process tree separate from the host. + +This page details how to use the `docker run` command to define the +container's resources at runtime. + +## General form + +The basic `docker run` command takes this form: + + $ docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...] + +The `docker run` command must specify an [*IMAGE*](glossary.md#image) +to derive the container from. An image developer can define image +defaults related to: + + * detached or foreground running + * container identification + * network settings + * runtime constraints on CPU and memory + +With the `docker run [OPTIONS]` an operator can add to or override the +image defaults set by a developer. And, additionally, operators can +override nearly all the defaults set by the Docker runtime itself. The +operator's ability to override image and Docker runtime defaults is why +[*run*](commandline/run.md) has more options than any +other `docker` command. + +To learn how to interpret the types of `[OPTIONS]`, see [*Option +types*](commandline/cli.md#option-types). + +> **Note**: Depending on your Docker system configuration, you may be +> required to preface the `docker run` command with `sudo`. To avoid +> having to use `sudo` with the `docker` command, your system +> administrator can create a Unix group called `docker` and add users to +> it. For more information about this configuration, refer to the Docker +> installation documentation for your operating system. + + +## Operator exclusive options + +Only the operator (the person executing `docker run`) can set the +following options. + + - [Detached vs foreground](#detached-vs-foreground) + - [Detached (-d)](#detached-d) + - [Foreground](#foreground) + - [Container identification](#container-identification) + - [Name (--name)](#name-name) + - [PID equivalent](#pid-equivalent) + - [IPC settings (--ipc)](#ipc-settings-ipc) + - [Network settings](#network-settings) + - [Restart policies (--restart)](#restart-policies-restart) + - [Clean up (--rm)](#clean-up-rm) + - [Runtime constraints on resources](#runtime-constraints-on-resources) + - [Runtime privilege and Linux capabilities](#runtime-privilege-and-linux-capabilities) + +## Detached vs foreground + +When starting a Docker container, you must first decide if you want to +run the container in the background in a "detached" mode or in the +default foreground mode: + + -d=false: Detached mode: Run container in the background, print new container id + +### Detached (-d) + +To start a container in detached mode, you use `-d=true` or just `-d` option. By +design, containers started in detached mode exit when the root process used to +run the container exits. A container in detached mode cannot be automatically +removed when it stops, this means you cannot use the `--rm` option with `-d` option. + +Do not pass a `service x start` command to a detached container. For example, this +command attempts to start the `nginx` service. + + $ docker run -d -p 80:80 my_image service nginx start + +This succeeds in starting the `nginx` service inside the container. However, it +fails the detached container paradigm in that, the root process (`service nginx +start`) returns and the detached container stops as designed. As a result, the +`nginx` service is started but could not be used. Instead, to start a process +such as the `nginx` web server do the following: + + $ docker run -d -p 80:80 my_image nginx -g 'daemon off;' + +To do input/output with a detached container use network connections or shared +volumes. These are required because the container is no longer listening to the +command line where `docker run` was run. + +To reattach to a detached container, use `docker` +[*attach*](commandline/attach.md) command. + +### Foreground + +In foreground mode (the default when `-d` is not specified), `docker +run` can start the process in the container and attach the console to +the process's standard input, output, and standard error. It can even +pretend to be a TTY (this is what most command line executables expect) +and pass along signals. All of that is configurable: + + -a=[] : Attach to `STDIN`, `STDOUT` and/or `STDERR` + -t : Allocate a pseudo-tty + --sig-proxy=true: Proxy all received signals to the process (non-TTY mode only) + -i : Keep STDIN open even if not attached + +If you do not specify `-a` then Docker will [attach all standard +streams]( https://github.com/docker/docker/blob/75a7f4d90cde0295bcfb7213004abce8d4779b75/commands.go#L1797). +You can specify to which of the three standard streams (`STDIN`, `STDOUT`, +`STDERR`) you'd like to connect instead, as in: + + $ docker run -a stdin -a stdout -i -t ubuntu /bin/bash + +For interactive processes (like a shell), you must use `-i -t` together in +order to allocate a tty for the container process. `-i -t` is often written `-it` +as you'll see in later examples. Specifying `-t` is forbidden when the client +standard output is redirected or piped, such as in: + + $ echo test | docker run -i busybox cat + +>**Note**: A process running as PID 1 inside a container is treated +>specially by Linux: it ignores any signal with the default action. +>So, the process will not terminate on `SIGINT` or `SIGTERM` unless it is +>coded to do so. + +## Container identification + +### Name (--name) + +The operator can identify a container in three ways: + +| Identifier type | Example value | +| --------------------- | ------------------------------------------------------------------ | +| UUID long identifier | "f78375b1c487e03c9438c729345e54db9d20cfa2ac1fc3494b6eb60872e74778" | +| UUID short identifier | "f78375b1c487" | +| Name | "evil_ptolemy" | + +The UUID identifiers come from the Docker daemon. If you do not assign a +container name with the `--name` option, then the daemon generates a random +string name for you. Defining a `name` can be a handy way to add meaning to a +container. If you specify a `name`, you can use it when referencing the +container within a Docker network. This works for both background and foreground +Docker containers. + +> **Note**: Containers on the default bridge network must be linked to +> communicate by name. + +### PID equivalent + +Finally, to help with automation, you can have Docker write the +container ID out to a file of your choosing. This is similar to how some +programs might write out their process ID to a file (you've seen them as +PID files): + + --cidfile="": Write the container ID to the file + +### Image[:tag] + +While not strictly a means of identifying a container, you can specify a version of an +image you'd like to run the container with by adding `image[:tag]` to the command. For +example, `docker run ubuntu:14.04`. + +### Image[@digest] + +Images using the v2 or later image format have a content-addressable identifier +called a digest. As long as the input used to generate the image is unchanged, +the digest value is predictable and referenceable. + +## PID settings (--pid) + + --pid="" : Set the PID (Process) Namespace mode for the container, + 'host': use the host's PID namespace inside the container + +By default, all containers have the PID namespace enabled. + +PID namespace provides separation of processes. The PID Namespace removes the +view of the system processes, and allows process ids to be reused including +pid 1. + +In certain cases you want your container to share the host's process namespace, +basically allowing processes within the container to see all of the processes +on the system. For example, you could build a container with debugging tools +like `strace` or `gdb`, but want to use these tools when debugging processes +within the container. + +### Example: run htop inside a container + +Create this Dockerfile: + +``` +FROM alpine:latest +RUN apk add --update htop && rm -rf /var/cache/apk/* +CMD ["htop"] +``` + +Build the Dockerfile and tag the image as `myhtop`: + +```bash +$ docker build -t myhtop . +``` + +Use the following command to run `htop` inside a container: + +``` +$ docker run -it --rm --pid=host myhtop +``` + +## UTS settings (--uts) + + --uts="" : Set the UTS namespace mode for the container, + 'host': use the host's UTS namespace inside the container + +The UTS namespace is for setting the hostname and the domain that is visible +to running processes in that namespace. By default, all containers, including +those with `--net=host`, have their own UTS namespace. The `host` setting will +result in the container using the same UTS namespace as the host. Note that +`--hostname` is invalid in `host` UTS mode. + +You may wish to share the UTS namespace with the host if you would like the +hostname of the container to change as the hostname of the host changes. A +more advanced use case would be changing the host's hostname from a container. + +## IPC settings (--ipc) + + --ipc="" : Set the IPC mode for the container, + 'container:': reuses another container's IPC namespace + 'host': use the host's IPC namespace inside the container + +By default, all containers have the IPC namespace enabled. + +IPC (POSIX/SysV IPC) namespace provides separation of named shared memory +segments, semaphores and message queues. + +Shared memory segments are used to accelerate inter-process communication at +memory speed, rather than through pipes or through the network stack. Shared +memory is commonly used by databases and custom-built (typically C/OpenMPI, +C++/using boost libraries) high performance applications for scientific +computing and financial services industries. If these types of applications +are broken into multiple containers, you might need to share the IPC mechanisms +of the containers. + +## Network settings + + --dns=[] : Set custom dns servers for the container + --net="bridge" : Connect a container to a network + 'bridge': create a network stack on the default Docker bridge + 'none': no networking + 'container:': reuse another container's network stack + 'host': use the Docker host network stack + '|': connect to a user-defined network + --net-alias=[] : Add network-scoped alias for the container + --add-host="" : Add a line to /etc/hosts (host:IP) + --mac-address="" : Sets the container's Ethernet device's MAC address + --ip="" : Sets the container's Ethernet device's IPv4 address + --ip6="" : Sets the container's Ethernet device's IPv6 address + +By default, all containers have networking enabled and they can make any +outgoing connections. The operator can completely disable networking +with `docker run --net none` which disables all incoming and outgoing +networking. In cases like this, you would perform I/O through files or +`STDIN` and `STDOUT` only. + +Publishing ports and linking to other containers only works with the default (bridge). The linking feature is a legacy feature. You should always prefer using Docker network drivers over linking. + +Your container will use the same DNS servers as the host by default, but +you can override this with `--dns`. + +By default, the MAC address is generated using the IP address allocated to the +container. You can set the container's MAC address explicitly by providing a +MAC address via the `--mac-address` parameter (format:`12:34:56:78:9a:bc`). + +Supported networks : + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NetworkDescription
none + No networking in the container. +
bridge (default) + Connect the container to the bridge via veth interfaces. +
host + Use the host's network stack inside the container. +
container:<name|id> + Use the network stack of another container, specified via + its *name* or *id*. +
NETWORK + Connects the container to a user created network (using `docker network create` command) +
+ +#### Network: none + +With the network is `none` a container will not have +access to any external routes. The container will still have a +`loopback` interface enabled in the container but it does not have any +routes to external traffic. + +#### Network: bridge + +With the network set to `bridge` a container will use docker's +default networking setup. A bridge is setup on the host, commonly named +`docker0`, and a pair of `veth` interfaces will be created for the +container. One side of the `veth` pair will remain on the host attached +to the bridge while the other side of the pair will be placed inside the +container's namespaces in addition to the `loopback` interface. An IP +address will be allocated for containers on the bridge's network and +traffic will be routed though this bridge to the container. + +Containers can communicate via their IP addresses by default. To communicate by +name, they must be linked. + +#### Network: host + +With the network set to `host` a container will share the host's +network stack and all interfaces from the host will be available to the +container. The container's hostname will match the hostname on the host +system. Note that `--add-host` `--dns` `--dns-search` +`--dns-opt` and `--mac-address` are invalid in `host` netmode. Even in `host` +network mode a container has its own UTS namespace by default. As such +`--hostname` is allowed in `host` network mode and will only change the +hostname inside the container. + +Compared to the default `bridge` mode, the `host` mode gives *significantly* +better networking performance since it uses the host's native networking stack +whereas the bridge has to go through one level of virtualization through the +docker daemon. It is recommended to run containers in this mode when their +networking performance is critical, for example, a production Load Balancer +or a High Performance Web Server. + +> **Note**: `--net="host"` gives the container full access to local system +> services such as D-bus and is therefore considered insecure. + +#### Network: container + +With the network set to `container` a container will share the +network stack of another container. The other container's name must be +provided in the format of `--net container:`. Note that `--add-host` +`--hostname` `--dns` `--dns-search` `--dns-opt` and `--mac-address` are +invalid in `container` netmode, and `--publish` `--publish-all` `--expose` are +also invalid in `container` netmode. + +Example running a Redis container with Redis binding to `localhost` then +running the `redis-cli` command and connecting to the Redis server over the +`localhost` interface. + + $ docker run -d --name redis example/redis --bind 127.0.0.1 + $ # use the redis container's network stack to access localhost + $ docker run --rm -it --net container:redis example/redis-cli -h 127.0.0.1 + +#### User-defined network + +You can create a network using a Docker network driver or an external network +driver plugin. You can connect multiple containers to the same network. Once +connected to a user-defined network, the containers can communicate easily using +only another container's IP address or name. + +For `overlay` networks or custom plugins that support multi-host connectivity, +containers connected to the same multi-host network but launched from different +Engines can also communicate in this way. + +The following example creates a network using the built-in `bridge` network +driver and running a container in the created network + +``` +$ docker network create -d bridge my-net +$ docker run --net=my-net -itd --name=container3 busybox +``` + +### Managing /etc/hosts + +Your container will have lines in `/etc/hosts` which define the hostname of the +container itself as well as `localhost` and a few other common things. The +`--add-host` flag can be used to add additional lines to `/etc/hosts`. + + $ docker run -it --add-host db-static:86.75.30.9 ubuntu cat /etc/hosts + 172.17.0.22 09d03f76bf2c + fe00::0 ip6-localnet + ff00::0 ip6-mcastprefix + ff02::1 ip6-allnodes + ff02::2 ip6-allrouters + 127.0.0.1 localhost + ::1 localhost ip6-localhost ip6-loopback + 86.75.30.9 db-static + +If a container is connected to the default bridge network and `linked` +with other containers, then the container's `/etc/hosts` file is updated +with the linked container's name. + +If the container is connected to user-defined network, the container's +`/etc/hosts` file is updated with names of all other containers in that +user-defined network. + +> **Note** Since Docker may live update the container’s `/etc/hosts` file, there +may be situations when processes inside the container can end up reading an +empty or incomplete `/etc/hosts` file. In most cases, retrying the read again +should fix the problem. + +## Restart policies (--restart) + +Using the `--restart` flag on Docker run you can specify a restart policy for +how a container should or should not be restarted on exit. + +When a restart policy is active on a container, it will be shown as either `Up` +or `Restarting` in [`docker ps`](commandline/ps.md). It can also be +useful to use [`docker events`](commandline/events.md) to see the +restart policy in effect. + +Docker supports the following restart policies: + + + + + + + + + + + + + + + + + + + + + + + + + + +
PolicyResult
no + Do not automatically restart the container when it exits. This is the + default. +
+ + on-failure[:max-retries] + + + Restart only if the container exits with a non-zero exit status. + Optionally, limit the number of restart retries the Docker + daemon attempts. +
always + Always restart the container regardless of the exit status. + When you specify always, the Docker daemon will try to restart + the container indefinitely. The container will also always start + on daemon startup, regardless of the current state of the container. +
unless-stopped + Always restart the container regardless of the exit status, but + do not start it on daemon startup if the container has been put + to a stopped state before. +
+ +An ever increasing delay (double the previous delay, starting at 100 +milliseconds) is added before each restart to prevent flooding the server. +This means the daemon will wait for 100 ms, then 200 ms, 400, 800, 1600, +and so on until either the `on-failure` limit is hit, or when you `docker stop` +or `docker rm -f` the container. + +If a container is successfully restarted (the container is started and runs +for at least 10 seconds), the delay is reset to its default value of 100 ms. + +You can specify the maximum amount of times Docker will try to restart the +container when using the **on-failure** policy. The default is that Docker +will try forever to restart the container. The number of (attempted) restarts +for a container can be obtained via [`docker inspect`](commandline/inspect.md). For example, to get the number of restarts +for container "my-container"; + + $ docker inspect -f "{{ .RestartCount }}" my-container + # 2 + +Or, to get the last time the container was (re)started; + + $ docker inspect -f "{{ .State.StartedAt }}" my-container + # 2015-03-04T23:47:07.691840179Z + + +Combining `--restart` (restart policy) with the `--rm` (clean up) flag results +in an error. On container restart, attached clients are disconnected. See the +examples on using the [`--rm` (clean up)](#clean-up-rm) flag later in this page. + +### Examples + + $ docker run --restart=always redis + +This will run the `redis` container with a restart policy of **always** +so that if the container exits, Docker will restart it. + + $ docker run --restart=on-failure:10 redis + +This will run the `redis` container with a restart policy of **on-failure** +and a maximum restart count of 10. If the `redis` container exits with a +non-zero exit status more than 10 times in a row Docker will abort trying to +restart the container. Providing a maximum restart limit is only valid for the +**on-failure** policy. + +## Exit Status + +The exit code from `docker run` gives information about why the container +failed to run or why it exited. When `docker run` exits with a non-zero code, +the exit codes follow the `chroot` standard, see below: + +**_125_** if the error is with Docker daemon **_itself_** + + $ docker run --foo busybox; echo $? + # flag provided but not defined: --foo + See 'docker run --help'. + 125 + +**_126_** if the **_contained command_** cannot be invoked + + $ docker run busybox /etc; echo $? + # exec: "/etc": permission denied + docker: Error response from daemon: Contained command could not be invoked + 126 + +**_127_** if the **_contained command_** cannot be found + + $ docker run busybox foo; echo $? + # exec: "foo": executable file not found in $PATH + docker: Error response from daemon: Contained command not found or does not exist + 127 + +**_Exit code_** of **_contained command_** otherwise + + $ docker run busybox /bin/sh -c 'exit 3' + # 3 + +## Clean up (--rm) + +By default a container's file system persists even after the container +exits. This makes debugging a lot easier (since you can inspect the +final state) and you retain all your data by default. But if you are +running short-term **foreground** processes, these container file +systems can really pile up. If instead you'd like Docker to +**automatically clean up the container and remove the file system when +the container exits**, you can add the `--rm` flag: + + --rm=false: Automatically remove the container when it exits (incompatible with -d) + +> **Note**: When you set the `--rm` flag, Docker also removes the volumes +associated with the container when the container is removed. This is similar +to running `docker rm -v my-container`. Only volumes that are specified without a +name are removed. For example, with +`docker run --rm -v /foo -v awesome:/bar busybox top`, the volume for `/foo` will be removed, +but the volume for `/bar` will not. Volumes inheritted via `--volumes-from` will be removed +with the same logic -- if the original volume was specified with a name it will **not** be removed. + +## Security configuration + --security-opt="label=user:USER" : Set the label user for the container + --security-opt="label=role:ROLE" : Set the label role for the container + --security-opt="label=type:TYPE" : Set the label type for the container + --security-opt="label=level:LEVEL" : Set the label level for the container + --security-opt="label=disable" : Turn off label confinement for the container + --security-opt="apparmor=PROFILE" : Set the apparmor profile to be applied + to the container + --security-opt="no-new-privileges" : Disable container processes from gaining + new privileges + --security-opt="seccomp=unconfined": Turn off seccomp confinement for the container + --security-opt="seccomp=profile.json: White listed syscalls seccomp Json file to be used as a seccomp filter + + +You can override the default labeling scheme for each container by specifying +the `--security-opt` flag. For example, you can specify the MCS/MLS level, a +requirement for MLS systems. Specifying the level in the following command +allows you to share the same content between containers. + + $ docker run --security-opt label=level:s0:c100,c200 -it fedora bash + +An MLS example might be: + + $ docker run --security-opt label=level:TopSecret -it rhel7 bash + +To disable the security labeling for this container versus running with the +`--permissive` flag, use the following command: + + $ docker run --security-opt label=disable -it fedora bash + +If you want a tighter security policy on the processes within a container, +you can specify an alternate type for the container. You could run a container +that is only allowed to listen on Apache ports by executing the following +command: + + $ docker run --security-opt label=type:svirt_apache_t -it centos bash + +> **Note**: You would have to write policy defining a `svirt_apache_t` type. + +If you want to prevent your container processes from gaining additional +privileges, you can execute the following command: + + $ docker run --security-opt no-new-privileges -it centos bash + +For more details, see [kernel documentation](https://www.kernel.org/doc/Documentation/prctl/no_new_privs.txt). + +## Specifying custom cgroups + +Using the `--cgroup-parent` flag, you can pass a specific cgroup to run a +container in. This allows you to create and manage cgroups on their own. You can +define custom resources for those cgroups and put containers under a common +parent group. + +## Runtime constraints on resources + +The operator can also adjust the performance parameters of the +container: + +| Option | Description | +| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| `-m`, `--memory=""` | Memory limit (format: `[]`). Number is a positive integer. Unit can be one of `b`, `k`, `m`, or `g`. Minimum is 4M. | +| `--memory-swap=""` | Total memory limit (memory + swap, format: `[]`). Number is a positive integer. Unit can be one of `b`, `k`, `m`, or `g`. | +| `--memory-reservation=""` | Memory soft limit (format: `[]`). Number is a positive integer. Unit can be one of `b`, `k`, `m`, or `g`. | +| `--kernel-memory=""` | Kernel memory limit (format: `[]`). Number is a positive integer. Unit can be one of `b`, `k`, `m`, or `g`. Minimum is 4M. | +| `-c`, `--cpu-shares=0` | CPU shares (relative weight) | +| `--cpu-period=0` | Limit the CPU CFS (Completely Fair Scheduler) period | +| `--cpuset-cpus=""` | CPUs in which to allow execution (0-3, 0,1) | +| `--cpuset-mems=""` | Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems. | +| `--cpu-quota=0` | Limit the CPU CFS (Completely Fair Scheduler) quota | +| `--blkio-weight=0` | Block IO weight (relative weight) accepts a weight value between 10 and 1000. | +| `--blkio-weight-device=""` | Block IO weight (relative device weight, format: `DEVICE_NAME:WEIGHT`) | +| `--device-read-bps=""` | Limit read rate from a device (format: `:[]`). Number is a positive integer. Unit can be one of `kb`, `mb`, or `gb`. | +| `--device-write-bps=""` | Limit write rate to a device (format: `:[]`). Number is a positive integer. Unit can be one of `kb`, `mb`, or `gb`. | +| `--device-read-iops="" ` | Limit read rate (IO per second) from a device (format: `:`). Number is a positive integer. | +| `--device-write-iops="" ` | Limit write rate (IO per second) to a device (format: `:`). Number is a positive integer. | +| `--oom-kill-disable=false` | Whether to disable OOM Killer for the container or not. | +| `--memory-swappiness=""` | Tune a container's memory swappiness behavior. Accepts an integer between 0 and 100. | +| `--shm-size=""` | Size of `/dev/shm`. The format is ``. `number` must be greater than `0`. Unit is optional and can be `b` (bytes), `k` (kilobytes), `m` (megabytes), or `g` (gigabytes). If you omit the unit, the system uses bytes. If you omit the size entirely, the system uses `64m`. | + +### User memory constraints + +We have four ways to set user memory usage: + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionResult
+ memory=inf, memory-swap=inf (default) + + There is no memory limit for the container. The container can use + as much memory as needed. +
memory=L<inf, memory-swap=inf + (specify memory and set memory-swap as -1) The container is + not allowed to use more than L bytes of memory, but can use as much swap + as is needed (if the host supports swap memory). +
memory=L<inf, memory-swap=2*L + (specify memory without memory-swap) The container is not allowed to + use more than L bytes of memory, swap *plus* memory usage is double + of that. +
+ memory=L<inf, memory-swap=S<inf, L<=S + + (specify both memory and memory-swap) The container is not allowed to + use more than L bytes of memory, swap *plus* memory usage is limited + by S. +
+ +Examples: + + $ docker run -it ubuntu:14.04 /bin/bash + +We set nothing about memory, this means the processes in the container can use +as much memory and swap memory as they need. + + $ docker run -it -m 300M --memory-swap -1 ubuntu:14.04 /bin/bash + +We set memory limit and disabled swap memory limit, this means the processes in +the container can use 300M memory and as much swap memory as they need (if the +host supports swap memory). + + $ docker run -it -m 300M ubuntu:14.04 /bin/bash + +We set memory limit only, this means the processes in the container can use +300M memory and 300M swap memory, by default, the total virtual memory size +(--memory-swap) will be set as double of memory, in this case, memory + swap +would be 2*300M, so processes can use 300M swap memory as well. + + $ docker run -it -m 300M --memory-swap 1G ubuntu:14.04 /bin/bash + +We set both memory and swap memory, so the processes in the container can use +300M memory and 700M swap memory. + +Memory reservation is a kind of memory soft limit that allows for greater +sharing of memory. Under normal circumstances, containers can use as much of +the memory as needed and are constrained only by the hard limits set with the +`-m`/`--memory` option. When memory reservation is set, Docker detects memory +contention or low memory and forces containers to restrict their consumption to +a reservation limit. + +Always set the memory reservation value below the hard limit, otherwise the hard +limit takes precedence. A reservation of 0 is the same as setting no +reservation. By default (without reservation set), memory reservation is the +same as the hard memory limit. + +Memory reservation is a soft-limit feature and does not guarantee the limit +won't be exceeded. Instead, the feature attempts to ensure that, when memory is +heavily contended for, memory is allocated based on the reservation hints/setup. + +The following example limits the memory (`-m`) to 500M and sets the memory +reservation to 200M. + +```bash +$ docker run -it -m 500M --memory-reservation 200M ubuntu:14.04 /bin/bash +``` + +Under this configuration, when the container consumes memory more than 200M and +less than 500M, the next system memory reclaim attempts to shrink container +memory below 200M. + +The following example set memory reservation to 1G without a hard memory limit. + +```bash +$ docker run -it --memory-reservation 1G ubuntu:14.04 /bin/bash +``` + +The container can use as much memory as it needs. The memory reservation setting +ensures the container doesn't consume too much memory for long time, because +every memory reclaim shrinks the container's consumption to the reservation. + +By default, kernel kills processes in a container if an out-of-memory (OOM) +error occurs. To change this behaviour, use the `--oom-kill-disable` option. +Only disable the OOM killer on containers where you have also set the +`-m/--memory` option. If the `-m` flag is not set, this can result in the host +running out of memory and require killing the host's system processes to free +memory. + +The following example limits the memory to 100M and disables the OOM killer for +this container: + + $ docker run -it -m 100M --oom-kill-disable ubuntu:14.04 /bin/bash + +The following example, illustrates a dangerous way to use the flag: + + $ docker run -it --oom-kill-disable ubuntu:14.04 /bin/bash + +The container has unlimited memory which can cause the host to run out memory +and require killing system processes to free memory. + +### Kernel memory constraints + +Kernel memory is fundamentally different than user memory as kernel memory can't +be swapped out. The inability to swap makes it possible for the container to +block system services by consuming too much kernel memory. Kernel memory includes: + + - stack pages + - slab pages + - sockets memory pressure + - tcp memory pressure + +You can setup kernel memory limit to constrain these kinds of memory. For example, +every process consumes some stack pages. By limiting kernel memory, you can +prevent new processes from being created when the kernel memory usage is too high. + +Kernel memory is never completely independent of user memory. Instead, you limit +kernel memory in the context of the user memory limit. Assume "U" is the user memory +limit and "K" the kernel limit. There are three possible ways to set limits: + + + + + + + + + + + + + + + + + + + + + + +
OptionResult
U != 0, K = inf (default) + This is the standard memory limitation mechanism already present before using + kernel memory. Kernel memory is completely ignored. +
U != 0, K < U + Kernel memory is a subset of the user memory. This setup is useful in + deployments where the total amount of memory per-cgroup is overcommitted. + Overcommitting kernel memory limits is definitely not recommended, since the + box can still run out of non-reclaimable memory. + In this case, the you can configure K so that the sum of all groups is + never greater than the total memory. Then, freely set U at the expense of + the system's service quality. +
U != 0, K > U + Since kernel memory charges are also fed to the user counter and reclamation + is triggered for the container for both kinds of memory. This configuration + gives the admin a unified view of memory. It is also useful for people + who just want to track kernel memory usage. +
+ +Examples: + + $ docker run -it -m 500M --kernel-memory 50M ubuntu:14.04 /bin/bash + +We set memory and kernel memory, so the processes in the container can use +500M memory in total, in this 500M memory, it can be 50M kernel memory tops. + + $ docker run -it --kernel-memory 50M ubuntu:14.04 /bin/bash + +We set kernel memory without **-m**, so the processes in the container can +use as much memory as they want, but they can only use 50M kernel memory. + +### Swappiness constraint + +By default, a container's kernel can swap out a percentage of anonymous pages. +To set this percentage for a container, specify a `--memory-swappiness` value +between 0 and 100. A value of 0 turns off anonymous page swapping. A value of +100 sets all anonymous pages as swappable. By default, if you are not using +`--memory-swappiness`, memory swappiness value will be inherited from the parent. + +For example, you can set: + + $ docker run -it --memory-swappiness=0 ubuntu:14.04 /bin/bash + +Setting the `--memory-swappiness` option is helpful when you want to retain the +container's working set and to avoid swapping performance penalties. + +### CPU share constraint + +By default, all containers get the same proportion of CPU cycles. This proportion +can be modified by changing the container's CPU share weighting relative +to the weighting of all other running containers. + +To modify the proportion from the default of 1024, use the `-c` or `--cpu-shares` +flag to set the weighting to 2 or higher. If 0 is set, the system will ignore the +value and use the default of 1024. + +The proportion will only apply when CPU-intensive processes are running. +When tasks in one container are idle, other containers can use the +left-over CPU time. The actual amount of CPU time will vary depending on +the number of containers running on the system. + +For example, consider three containers, one has a cpu-share of 1024 and +two others have a cpu-share setting of 512. When processes in all three +containers attempt to use 100% of CPU, the first container would receive +50% of the total CPU time. If you add a fourth container with a cpu-share +of 1024, the first container only gets 33% of the CPU. The remaining containers +receive 16.5%, 16.5% and 33% of the CPU. + +On a multi-core system, the shares of CPU time are distributed over all CPU +cores. Even if a container is limited to less than 100% of CPU time, it can +use 100% of each individual CPU core. + +For example, consider a system with more than three cores. If you start one +container `{C0}` with `-c=512` running one process, and another container +`{C1}` with `-c=1024` running two processes, this can result in the following +division of CPU shares: + + PID container CPU CPU share + 100 {C0} 0 100% of CPU0 + 101 {C1} 1 100% of CPU1 + 102 {C1} 2 100% of CPU2 + +### CPU period constraint + +The default CPU CFS (Completely Fair Scheduler) period is 100ms. We can use +`--cpu-period` to set the period of CPUs to limit the container's CPU usage. +And usually `--cpu-period` should work with `--cpu-quota`. + +Examples: + + $ docker run -it --cpu-period=50000 --cpu-quota=25000 ubuntu:14.04 /bin/bash + +If there is 1 CPU, this means the container can get 50% CPU worth of run-time every 50ms. + +For more information, see the [CFS documentation on bandwidth limiting](https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt). + +### Cpuset constraint + +We can set cpus in which to allow execution for containers. + +Examples: + + $ docker run -it --cpuset-cpus="1,3" ubuntu:14.04 /bin/bash + +This means processes in container can be executed on cpu 1 and cpu 3. + + $ docker run -it --cpuset-cpus="0-2" ubuntu:14.04 /bin/bash + +This means processes in container can be executed on cpu 0, cpu 1 and cpu 2. + +We can set mems in which to allow execution for containers. Only effective +on NUMA systems. + +Examples: + + $ docker run -it --cpuset-mems="1,3" ubuntu:14.04 /bin/bash + +This example restricts the processes in the container to only use memory from +memory nodes 1 and 3. + + $ docker run -it --cpuset-mems="0-2" ubuntu:14.04 /bin/bash + +This example restricts the processes in the container to only use memory from +memory nodes 0, 1 and 2. + +### CPU quota constraint + +The `--cpu-quota` flag limits the container's CPU usage. The default 0 value +allows the container to take 100% of a CPU resource (1 CPU). The CFS (Completely Fair +Scheduler) handles resource allocation for executing processes and is default +Linux Scheduler used by the kernel. Set this value to 50000 to limit the container +to 50% of a CPU resource. For multiple CPUs, adjust the `--cpu-quota` as necessary. +For more information, see the [CFS documentation on bandwidth limiting](https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt). + +### Block IO bandwidth (Blkio) constraint + +By default, all containers get the same proportion of block IO bandwidth +(blkio). This proportion is 500. To modify this proportion, change the +container's blkio weight relative to the weighting of all other running +containers using the `--blkio-weight` flag. + +> **Note:** The blkio weight setting is only available for direct IO. Buffered IO +> is not currently supported. + +The `--blkio-weight` flag can set the weighting to a value between 10 to 1000. +For example, the commands below create two containers with different blkio +weight: + + $ docker run -it --name c1 --blkio-weight 300 ubuntu:14.04 /bin/bash + $ docker run -it --name c2 --blkio-weight 600 ubuntu:14.04 /bin/bash + +If you do block IO in the two containers at the same time, by, for example: + + $ time dd if=/mnt/zerofile of=test.out bs=1M count=1024 oflag=direct + +You'll find that the proportion of time is the same as the proportion of blkio +weights of the two containers. + +The `--blkio-weight-device="DEVICE_NAME:WEIGHT"` flag sets a specific device weight. +The `DEVICE_NAME:WEIGHT` is a string containing a colon-separated device name and weight. +For example, to set `/dev/sda` device weight to `200`: + + $ docker run -it \ + --blkio-weight-device "/dev/sda:200" \ + ubuntu + +If you specify both the `--blkio-weight` and `--blkio-weight-device`, Docker +uses the `--blkio-weight` as the default weight and uses `--blkio-weight-device` +to override this default with a new value on a specific device. +The following example uses a default weight of `300` and overrides this default +on `/dev/sda` setting that weight to `200`: + + $ docker run -it \ + --blkio-weight 300 \ + --blkio-weight-device "/dev/sda:200" \ + ubuntu + +The `--device-read-bps` flag limits the read rate (bytes per second) from a device. +For example, this command creates a container and limits the read rate to `1mb` +per second from `/dev/sda`: + + $ docker run -it --device-read-bps /dev/sda:1mb ubuntu + +The `--device-write-bps` flag limits the write rate (bytes per second)to a device. +For example, this command creates a container and limits the write rate to `1mb` +per second for `/dev/sda`: + + $ docker run -it --device-write-bps /dev/sda:1mb ubuntu + +Both flags take limits in the `:[unit]` format. Both read +and write rates must be a positive integer. You can specify the rate in `kb` +(kilobytes), `mb` (megabytes), or `gb` (gigabytes). + +The `--device-read-iops` flag limits read rate (IO per second) from a device. +For example, this command creates a container and limits the read rate to +`1000` IO per second from `/dev/sda`: + + $ docker run -ti --device-read-iops /dev/sda:1000 ubuntu + +The `--device-write-iops` flag limits write rate (IO per second) to a device. +For example, this command creates a container and limits the write rate to +`1000` IO per second to `/dev/sda`: + + $ docker run -ti --device-write-iops /dev/sda:1000 ubuntu + +Both flags take limits in the `:` format. Both read and +write rates must be a positive integer. + +## Additional groups + --group-add: Add additional groups to run as + +By default, the docker container process runs with the supplementary groups looked +up for the specified user. If one wants to add more to that list of groups, then +one can use this flag: + + $ docker run --rm --group-add audio --group-add nogroup --group-add 777 busybox id + uid=0(root) gid=0(root) groups=10(wheel),29(audio),99(nogroup),777 + +## Runtime privilege and Linux capabilities + + --cap-add: Add Linux capabilities + --cap-drop: Drop Linux capabilities + --privileged=false: Give extended privileges to this container + --device=[]: Allows you to run devices inside the container without the --privileged flag. + +> **Note:** +> With Docker 1.10 and greater, the default seccomp profile will also block +> syscalls, regardless of `--cap-add` passed to the container. We recommend in +> these cases to create your own custom seccomp profile based off our +> [default](https://github.com/docker/docker/blob/master/profiles/seccomp/default.json). +> Or if you don't want to run with the default seccomp profile, you can pass +> `--security-opt=seccomp=unconfined` on run. + +By default, Docker containers are "unprivileged" and cannot, for +example, run a Docker daemon inside a Docker container. This is because +by default a container is not allowed to access any devices, but a +"privileged" container is given access to all devices (see +the documentation on [cgroups devices](https://www.kernel.org/doc/Documentation/cgroups/devices.txt)). + +When the operator executes `docker run --privileged`, Docker will enable +to access to all devices on the host as well as set some configuration +in AppArmor or SELinux to allow the container nearly all the same access to the +host as processes running outside containers on the host. Additional +information about running with `--privileged` is available on the +[Docker Blog](http://blog.docker.com/2013/09/docker-can-now-run-within-docker/). + +If you want to limit access to a specific device or devices you can use +the `--device` flag. It allows you to specify one or more devices that +will be accessible within the container. + + $ docker run --device=/dev/snd:/dev/snd ... + +By default, the container will be able to `read`, `write`, and `mknod` these devices. +This can be overridden using a third `:rwm` set of options to each `--device` flag: + + $ docker run --device=/dev/sda:/dev/xvdc --rm -it ubuntu fdisk /dev/xvdc + + Command (m for help): q + $ docker run --device=/dev/sda:/dev/xvdc:r --rm -it ubuntu fdisk /dev/xvdc + You will not be able to write the partition table. + + Command (m for help): q + + $ docker run --device=/dev/sda:/dev/xvdc:w --rm -it ubuntu fdisk /dev/xvdc + crash.... + + $ docker run --device=/dev/sda:/dev/xvdc:m --rm -it ubuntu fdisk /dev/xvdc + fdisk: unable to open /dev/xvdc: Operation not permitted + +In addition to `--privileged`, the operator can have fine grain control over the +capabilities using `--cap-add` and `--cap-drop`. By default, Docker has a default +list of capabilities that are kept. The following table lists the Linux capability options which can be added or dropped. + +| Capability Key | Capability Description | +| ---------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| SETPCAP | Modify process capabilities. | +| SYS_MODULE | Load and unload kernel modules. | +| SYS_RAWIO | Perform I/O port operations (iopl(2) and ioperm(2)). | +| SYS_PACCT | Use acct(2), switch process accounting on or off. | +| SYS_ADMIN | Perform a range of system administration operations. | +| SYS_NICE | Raise process nice value (nice(2), setpriority(2)) and change the nice value for arbitrary processes. | +| SYS_RESOURCE | Override resource Limits. | +| SYS_TIME | Set system clock (settimeofday(2), stime(2), adjtimex(2)); set real-time (hardware) clock. | +| SYS_TTY_CONFIG | Use vhangup(2); employ various privileged ioctl(2) operations on virtual terminals. | +| MKNOD | Create special files using mknod(2). | +| AUDIT_WRITE | Write records to kernel auditing log. | +| AUDIT_CONTROL | Enable and disable kernel auditing; change auditing filter rules; retrieve auditing status and filtering rules. | +| MAC_OVERRIDE | Allow MAC configuration or state changes. Implemented for the Smack LSM. | +| MAC_ADMIN | Override Mandatory Access Control (MAC). Implemented for the Smack Linux Security Module (LSM). | +| NET_ADMIN | Perform various network-related operations. | +| SYSLOG | Perform privileged syslog(2) operations. | +| CHOWN | Make arbitrary changes to file UIDs and GIDs (see chown(2)). | +| NET_RAW | Use RAW and PACKET sockets. | +| DAC_OVERRIDE | Bypass file read, write, and execute permission checks. | +| FOWNER | Bypass permission checks on operations that normally require the file system UID of the process to match the UID of the file. | +| DAC_READ_SEARCH | Bypass file read permission checks and directory read and execute permission checks. | +| FSETID | Don't clear set-user-ID and set-group-ID permission bits when a file is modified. | +| KILL | Bypass permission checks for sending signals. | +| SETGID | Make arbitrary manipulations of process GIDs and supplementary GID list. | +| SETUID | Make arbitrary manipulations of process UIDs. | +| LINUX_IMMUTABLE | Set the FS_APPEND_FL and FS_IMMUTABLE_FL i-node flags. | +| NET_BIND_SERVICE | Bind a socket to internet domain privileged ports (port numbers less than 1024). | +| NET_BROADCAST | Make socket broadcasts, and listen to multicasts. | +| IPC_LOCK | Lock memory (mlock(2), mlockall(2), mmap(2), shmctl(2)). | +| IPC_OWNER | Bypass permission checks for operations on System V IPC objects. | +| SYS_CHROOT | Use chroot(2), change root directory. | +| SYS_PTRACE | Trace arbitrary processes using ptrace(2). | +| SYS_BOOT | Use reboot(2) and kexec_load(2), reboot and load a new kernel for later execution. | +| LEASE | Establish leases on arbitrary files (see fcntl(2)). | +| SETFCAP | Set file capabilities. | +| WAKE_ALARM | Trigger something that will wake up the system. | +| BLOCK_SUSPEND | Employ features that can block system suspend. + +Further reference information is available on the [capabilities(7) - Linux man page](http://linux.die.net/man/7/capabilities) + +Both flags support the value `ALL`, so if the +operator wants to have all capabilities but `MKNOD` they could use: + + $ docker run --cap-add=ALL --cap-drop=MKNOD ... + +For interacting with the network stack, instead of using `--privileged` they +should use `--cap-add=NET_ADMIN` to modify the network interfaces. + + $ docker run -it --rm ubuntu:14.04 ip link add dummy0 type dummy + RTNETLINK answers: Operation not permitted + $ docker run -it --rm --cap-add=NET_ADMIN ubuntu:14.04 ip link add dummy0 type dummy + +To mount a FUSE based filesystem, you need to combine both `--cap-add` and +`--device`: + + $ docker run --rm -it --cap-add SYS_ADMIN sshfs sshfs sven@10.10.10.20:/home/sven /mnt + fuse: failed to open /dev/fuse: Operation not permitted + $ docker run --rm -it --device /dev/fuse sshfs sshfs sven@10.10.10.20:/home/sven /mnt + fusermount: mount failed: Operation not permitted + $ docker run --rm -it --cap-add SYS_ADMIN --device /dev/fuse sshfs + # sshfs sven@10.10.10.20:/home/sven /mnt + The authenticity of host '10.10.10.20 (10.10.10.20)' can't be established. + ECDSA key fingerprint is 25:34:85:75:25:b0:17:46:05:19:04:93:b5:dd:5f:c6. + Are you sure you want to continue connecting (yes/no)? yes + sven@10.10.10.20's password: + root@30aa0cfaf1b5:/# ls -la /mnt/src/docker + total 1516 + drwxrwxr-x 1 1000 1000 4096 Dec 4 06:08 . + drwxrwxr-x 1 1000 1000 4096 Dec 4 11:46 .. + -rw-rw-r-- 1 1000 1000 16 Oct 8 00:09 .dockerignore + -rwxrwxr-x 1 1000 1000 464 Oct 8 00:09 .drone.yml + drwxrwxr-x 1 1000 1000 4096 Dec 4 06:11 .git + -rw-rw-r-- 1 1000 1000 461 Dec 4 06:08 .gitignore + .... + + +## Logging drivers (--log-driver) + +The container can have a different logging driver than the Docker daemon. Use +the `--log-driver=VALUE` with the `docker run` command to configure the +container's logging driver. The following options are supported: + +| Driver | Description | +| ----------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `none` | Disables any logging for the container. `docker logs` won't be available with this driver. | +| `json-file` | Default logging driver for Docker. Writes JSON messages to file. No logging options are supported for this driver. | +| `syslog` | Syslog logging driver for Docker. Writes log messages to syslog. | +| `journald` | Journald logging driver for Docker. Writes log messages to `journald`. | +| `gelf` | Graylog Extended Log Format (GELF) logging driver for Docker. Writes log messages to a GELF endpoint likeGraylog or Logstash. | +| `fluentd` | Fluentd logging driver for Docker. Writes log messages to `fluentd` (forward input). | +| `awslogs` | Amazon CloudWatch Logs logging driver for Docker. Writes log messages to Amazon CloudWatch Logs | +| `splunk` | Splunk logging driver for Docker. Writes log messages to `splunk` using Event Http Collector. | + +The `docker logs` command is available only for the `json-file` and `journald` +logging drivers. For detailed information on working with logging drivers, see +[Configure a logging driver](../admin/logging/overview.md). + + +## Overriding Dockerfile image defaults + +When a developer builds an image from a [*Dockerfile*](builder.md) +or when she commits it, the developer can set a number of default parameters +that take effect when the image starts up as a container. + +Four of the Dockerfile commands cannot be overridden at runtime: `FROM`, +`MAINTAINER`, `RUN`, and `ADD`. Everything else has a corresponding override +in `docker run`. We'll go through what the developer might have set in each +Dockerfile instruction and how the operator can override that setting. + + - [CMD (Default Command or Options)](#cmd-default-command-or-options) + - [ENTRYPOINT (Default Command to Execute at Runtime)]( + #entrypoint-default-command-to-execute-at-runtime) + - [EXPOSE (Incoming Ports)](#expose-incoming-ports) + - [ENV (Environment Variables)](#env-environment-variables) + - [VOLUME (Shared Filesystems)](#volume-shared-filesystems) + - [USER](#user) + - [WORKDIR](#workdir) + +### CMD (default command or options) + +Recall the optional `COMMAND` in the Docker +commandline: + + $ docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...] + +This command is optional because the person who created the `IMAGE` may +have already provided a default `COMMAND` using the Dockerfile `CMD` +instruction. As the operator (the person running a container from the +image), you can override that `CMD` instruction just by specifying a new +`COMMAND`. + +If the image also specifies an `ENTRYPOINT` then the `CMD` or `COMMAND` +get appended as arguments to the `ENTRYPOINT`. + +### ENTRYPOINT (default command to execute at runtime) + + --entrypoint="": Overwrite the default entrypoint set by the image + +The `ENTRYPOINT` of an image is similar to a `COMMAND` because it +specifies what executable to run when the container starts, but it is +(purposely) more difficult to override. The `ENTRYPOINT` gives a +container its default nature or behavior, so that when you set an +`ENTRYPOINT` you can run the container *as if it were that binary*, +complete with default options, and you can pass in more options via the +`COMMAND`. But, sometimes an operator may want to run something else +inside the container, so you can override the default `ENTRYPOINT` at +runtime by using a string to specify the new `ENTRYPOINT`. Here is an +example of how to run a shell in a container that has been set up to +automatically run something else (like `/usr/bin/redis-server`): + + $ docker run -it --entrypoint /bin/bash example/redis + +or two examples of how to pass more parameters to that ENTRYPOINT: + + $ docker run -it --entrypoint /bin/bash example/redis -c ls -l + $ docker run -it --entrypoint /usr/bin/redis-cli example/redis --help + +### EXPOSE (incoming ports) + +The following `run` command options work with container networking: + + --expose=[]: Expose a port or a range of ports inside the container. + These are additional to those exposed by the `EXPOSE` instruction + -P : Publish all exposed ports to the host interfaces + -p=[] : Publish a container᾿s port or a range of ports to the host + format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort + Both hostPort and containerPort can be specified as a + range of ports. When specifying ranges for both, the + number of container ports in the range must match the + number of host ports in the range, for example: + -p 1234-1236:1234-1236/tcp + + When specifying a range for hostPort only, the + containerPort must not be a range. In this case the + container port is published somewhere within the + specified hostPort range. (e.g., `-p 1234-1236:1234/tcp`) + + (use 'docker port' to see the actual mapping) + + --link="" : Add link to another container (:alias or ) + +With the exception of the `EXPOSE` directive, an image developer hasn't +got much control over networking. The `EXPOSE` instruction defines the +initial incoming ports that provide services. These ports are available +to processes inside the container. An operator can use the `--expose` +option to add to the exposed ports. + +To expose a container's internal port, an operator can start the +container with the `-P` or `-p` flag. The exposed port is accessible on +the host and the ports are available to any client that can reach the +host. + +The `-P` option publishes all the ports to the host interfaces. Docker +binds each exposed port to a random port on the host. The range of +ports are within an *ephemeral port range* defined by +`/proc/sys/net/ipv4/ip_local_port_range`. Use the `-p` flag to +explicitly map a single port or range of ports. + +The port number inside the container (where the service listens) does +not need to match the port number exposed on the outside of the +container (where clients connect). For example, inside the container an +HTTP service is listening on port 80 (and so the image developer +specifies `EXPOSE 80` in the Dockerfile). At runtime, the port might be +bound to 42800 on the host. To find the mapping between the host ports +and the exposed ports, use `docker port`. + +If the operator uses `--link` when starting a new client container in the +default bridge network, then the client container can access the exposed +port via a private networking interface. +If `--link` is used when starting a container in a user-defined network as +described in [*Docker network overview*""](../userguide/networking/index.md)), +it will provide a named alias for the container being linked to. + +### ENV (environment variables) + +When a new container is created, Docker will set the following environment +variables automatically: + + + + + + + + + + + + + + + + + + + + + +
VariableValue
HOME + Set based on the value of USER +
HOSTNAME + The hostname associated with the container +
PATH + Includes popular directories, such as :
+ /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +
TERMxterm if the container is allocated a pseudo-TTY
+ +Additionally, the operator can **set any environment variable** in the +container by using one or more `-e` flags, even overriding those mentioned +above, or already defined by the developer with a Dockerfile `ENV`: + + $ docker run -e "deep=purple" --rm ubuntu /bin/bash -c export + declare -x HOME="/" + declare -x HOSTNAME="85bc26a0e200" + declare -x OLDPWD + declare -x PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + declare -x PWD="/" + declare -x SHLVL="1" + declare -x deep="purple" + +Similarly the operator can set the **hostname** with `-h`. + +### TMPFS (mount tmpfs filesystems) + +```bash +--tmpfs=[]: Create a tmpfs mount with: container-dir[:], + where the options are identical to the Linux + 'mount -t tmpfs -o' command. +``` + +The example below mounts an empty tmpfs into the container with the `rw`, +`noexec`, `nosuid`, and `size=65536k` options. + + $ docker run -d --tmpfs /run:rw,noexec,nosuid,size=65536k my_image + +### VOLUME (shared filesystems) + + -v, --volume=[host-src:]container-dest[:]: Bind mount a volume. + The comma-delimited `options` are [rw|ro], [z|Z], + [[r]shared|[r]slave|[r]private], and [nocopy]. + The 'host-src' is an absolute path or a name value. + + If neither 'rw' or 'ro' is specified then the volume is mounted in + read-write mode. + + The `nocopy` modes is used to disable automatic copying requested volume + path in the container to the volume storage location. + For named volumes, `copy` is the default mode. Copy modes are not supported + for bind-mounted volumes. + + --volumes-from="": Mount all volumes from the given container(s) + +> **Note**: +> When using systemd to manage the Docker daemon's start and stop, in the systemd +> unit file there is an option to control mount propagation for the Docker daemon +> itself, called `MountFlags`. The value of this setting may cause Docker to not +> see mount propagation changes made on the mount point. For example, if this value +> is `slave`, you may not be able to use the `shared` or `rshared` propagation on +> a volume. + +The volumes commands are complex enough to have their own documentation +in section [*Managing data in +containers*](../userguide/containers/dockervolumes.md). A developer can define +one or more `VOLUME`'s associated with an image, but only the operator +can give access from one container to another (or from a container to a +volume mounted on the host). + +The `container-dest` must always be an absolute path such as `/src/docs`. +The `host-src` can either be an absolute path or a `name` value. If you +supply an absolute path for the `host-dir`, Docker bind-mounts to the path +you specify. If you supply a `name`, Docker creates a named volume by that `name`. + +A `name` value must start with start with an alphanumeric character, +followed by `a-z0-9`, `_` (underscore), `.` (period) or `-` (hyphen). +An absolute path starts with a `/` (forward slash). + +For example, you can specify either `/foo` or `foo` for a `host-src` value. +If you supply the `/foo` value, Docker creates a bind-mount. If you supply +the `foo` specification, Docker creates a named volume. + +### USER + +`root` (id = 0) is the default user within a container. The image developer can +create additional users. Those users are accessible by name. When passing a numeric +ID, the user does not have to exist in the container. + +The developer can set a default user to run the first process with the +Dockerfile `USER` instruction. When starting a container, the operator can override +the `USER` instruction by passing the `-u` option. + + -u="", --user="": Sets the username or UID used and optionally the groupname or GID for the specified command. + + The followings examples are all valid: + --user=[ user | user:group | uid | uid:gid | user:gid | uid:group ] + +> **Note:** if you pass a numeric uid, it must be in the range of 0-2147483647. + +### WORKDIR + +The default working directory for running binaries within a container is the +root directory (`/`), but the developer can set a different default with the +Dockerfile `WORKDIR` command. The operator can override this with: + + -w="": Working directory inside the container diff --git a/docs/security/apparmor.md b/docs/security/apparmor.md new file mode 100644 index 00000000..b0998f84 --- /dev/null +++ b/docs/security/apparmor.md @@ -0,0 +1,183 @@ + + +# AppArmor security profiles for Docker + +AppArmor (Application Armor) is a Linux security module that protects an +operating system and its applications from security threats. To use it, a system +administrator associates an AppArmor security profile with each program. Docker +expects to find an AppArmor policy loaded and enforced. + +Docker automatically loads container profiles. The Docker binary installs +a `docker-default` profile in the `/etc/apparmor.d/docker` file. This profile +is used on containers, _not_ on the Docker Daemon. + +A profile for the Docker Engine Daemon exists but it is not currently installed +with the deb packages. If you are interested in the source for the Daemon +profile, it is located in +[contrib/apparmor](https://github.com/docker/docker/tree/master/contrib/apparmor) +in the Docker Engine source repository. + +## Understand the policies + +The `docker-default` profile is the default for running containers. It is +moderately protective while providing wide application compatibility. The +profile is the following: + +``` +#include + + +profile docker-default flags=(attach_disconnected,mediate_deleted) { + + #include + + + network, + capability, + file, + umount, + + deny @{PROC}/{*,**^[0-9*],sys/kernel/shm*} wkx, + deny @{PROC}/sysrq-trigger rwklx, + deny @{PROC}/mem rwklx, + deny @{PROC}/kmem rwklx, + deny @{PROC}/kcore rwklx, + + deny mount, + + deny /sys/[^f]*/** wklx, + deny /sys/f[^s]*/** wklx, + deny /sys/fs/[^c]*/** wklx, + deny /sys/fs/c[^g]*/** wklx, + deny /sys/fs/cg[^r]*/** wklx, + deny /sys/firmware/efi/efivars/** rwklx, + deny /sys/kernel/security/** rwklx, +} +``` + +When you run a container, it uses the `docker-default` policy unless you +override it with the `security-opt` option. For example, the following +explicitly specifies the default policy: + +```bash +$ docker run --rm -it --security-opt apparmor=docker-default hello-world +``` + +## Loading and Unloading Profiles + +To load a new profile into AppArmor, for use with containers: + +``` +$ apparmor_parser -r -W /path/to/your_profile +``` + +Then you can run the custom profile with `--security-opt` like so: + +```bash +$ docker run --rm -it --security-opt apparmor=your_profile hello-world +``` + +To unload a profile from AppArmor: + +```bash +# stop apparmor +$ /etc/init.d/apparmor stop +# unload the profile +$ apparmor_parser -R /path/to/profile +# start apparmor +$ /etc/init.d/apparmor start +``` + +## Debugging AppArmor + +### Using `dmesg` + +Here are some helpful tips for debugging any problems you might be facing with +regard to AppArmor. + +AppArmor sends quite verbose messaging to `dmesg`. Usually an AppArmor line +will look like the following: + +``` +[ 5442.864673] audit: type=1400 audit(1453830992.845:37): apparmor="ALLOWED" operation="open" profile="/usr/bin/docker" name="/home/jessie/docker/man/man1/docker-attach.1" pid=10923 comm="docker" requested_mask="r" denied_mask="r" fsuid=1000 ouid=0 +``` + +In the above example, the you can see `profile=/usr/bin/docker`. This means the +user has the `docker-engine` (Docker Engine Daemon) profile loaded. + +> **Note:** On version of Ubuntu > 14.04 this is all fine and well, but Trusty +> users might run into some issues when trying to `docker exec`. + +Let's look at another log line: + +``` +[ 3256.689120] type=1400 audit(1405454041.341:73): apparmor="DENIED" operation="ptrace" profile="docker-default" pid=17651 comm="docker" requested_mask="receive" denied_mask="receive" +``` + +This time the profile is `docker-default`, which is run on containers by +default unless in `privileged` mode. It is telling us, that apparmor has denied +`ptrace` in the container. This is great. + +### Using `aa-status` + +If you need to check which profiles are loaded you can use `aa-status`. The +output looks like: + +```bash +$ sudo aa-status +apparmor module is loaded. +14 profiles are loaded. +1 profiles are in enforce mode. + docker-default +13 profiles are in complain mode. + /usr/bin/docker + /usr/bin/docker///bin/cat + /usr/bin/docker///bin/ps + /usr/bin/docker///sbin/apparmor_parser + /usr/bin/docker///sbin/auplink + /usr/bin/docker///sbin/blkid + /usr/bin/docker///sbin/iptables + /usr/bin/docker///sbin/mke2fs + /usr/bin/docker///sbin/modprobe + /usr/bin/docker///sbin/tune2fs + /usr/bin/docker///sbin/xtables-multi + /usr/bin/docker///sbin/zfs + /usr/bin/docker///usr/bin/xz +38 processes have profiles defined. +37 processes are in enforce mode. + docker-default (6044) + ... + docker-default (31899) +1 processes are in complain mode. + /usr/bin/docker (29756) +0 processes are unconfined but have a profile defined. +``` + +In the above output you can tell that the `docker-default` profile running on +various container PIDs is in `enforce` mode. This means AppArmor will actively +block and audit in `dmesg` anything outside the bounds of the `docker-default` +profile. + +The output above also shows the `/usr/bin/docker` (Docker Engine Daemon) +profile is running in `complain` mode. This means AppArmor will _only_ log to +`dmesg` activity outside the bounds of the profile. (Except in the case of +Ubuntu Trusty, where we have seen some interesting behaviors being enforced.) + +## Contributing to AppArmor code in Docker + +Advanced users and package managers can find a profile for `/usr/bin/docker` +(Docker Engine Daemon) underneath +[contrib/apparmor](https://github.com/docker/docker/tree/master/contrib/apparmor) +in the Docker Engine source repository. + +The `docker-default` profile for containers lives in +[profiles/apparmor](https://github.com/docker/docker/tree/master/profiles/apparmor). diff --git a/docs/security/certificates.md b/docs/security/certificates.md new file mode 100644 index 00000000..5684e331 --- /dev/null +++ b/docs/security/certificates.md @@ -0,0 +1,85 @@ + + +# Using certificates for repository client verification + +In [Running Docker with HTTPS](https.md), you learned that, by default, +Docker runs via a non-networked Unix socket and TLS must be enabled in order +to have the Docker client and the daemon communicate securely over HTTPS. TLS ensures authenticity of the registry endpoint and that traffic to/from registry is encrypted. + +This article demonstrates how to ensure the traffic between the Docker registry (i.e., *a server*) and the Docker daemon (i.e., *a client*) traffic is encrypted and a properly authenticated using *certificate-based client-server authentication*. + +We will show you how to install a Certificate Authority (CA) root certificate +for the registry and how to set the client TLS certificate for verification. + +## Understanding the configuration + +A custom certificate is configured by creating a directory under +`/etc/docker/certs.d` using the same name as the registry's hostname (e.g., +`localhost`). All `*.crt` files are added to this directory as CA roots. + +> **Note:** +> In the absence of any root certificate authorities, Docker +> will use the system default (i.e., host's root CA set). + +The presence of one or more `.key/cert` pairs indicates to Docker +that there are custom certificates required for access to the desired +repository. + +> **Note:** +> If there are multiple certificates, each will be tried in alphabetical +> order. If there is an authentication error (e.g., 403, 404, 5xx, etc.), Docker +> will continue to try with the next certificate. + +The following illustrates a configuration with multiple certs: + +``` + /etc/docker/certs.d/ <-- Certificate directory + └── localhost <-- Hostname + ├── client.cert <-- Client certificate + ├── client.key <-- Client key + └── localhost.crt <-- Certificate authority that signed + the registry certificate +``` + +The preceding example is operating-system specific and is for illustrative +purposes only. You should consult your operating system documentation for +creating an os-provided bundled certificate chain. + + +## Creating the client certificates + +You will use OpenSSL's `genrsa` and `req` commands to first generate an RSA +key and then use the key to create the certificate. + + $ openssl genrsa -out client.key 4096 + $ openssl req -new -x509 -text -key client.key -out client.cert + +> **Note:** +> These TLS commands will only generate a working set of certificates on Linux. +> The version of OpenSSL in Mac OS X is incompatible with the type of +> certificate Docker requires. + +## Troubleshooting tips + +The Docker daemon interprets ``.crt` files as CA certificates and `.cert` files +as client certificates. If a CA certificate is accidentally given the extension +`.cert` instead of the correct `.crt` extension, the Docker daemon logs the +following error message: + +``` +Missing key KEY_NAME for client certificate CERT_NAME. Note that CA certificates should use the extension .crt. +``` + +## Related Information + +* [Use trusted images](index.md) +* [Protect the Docker daemon socket](https.md) diff --git a/docs/security/https.md b/docs/security/https.md new file mode 100644 index 00000000..1b2619cb --- /dev/null +++ b/docs/security/https.md @@ -0,0 +1,216 @@ + + +# Protect the Docker daemon socket + +By default, Docker runs via a non-networked Unix socket. It can also +optionally communicate using a HTTP socket. + +If you need Docker to be reachable via the network in a safe manner, you can +enable TLS by specifying the `tlsverify` flag and pointing Docker's +`tlscacert` flag to a trusted CA certificate. + +In the daemon mode, it will only allow connections from clients +authenticated by a certificate signed by that CA. In the client mode, +it will only connect to servers with a certificate signed by that CA. + +> **Warning**: +> Using TLS and managing a CA is an advanced topic. Please familiarize yourself +> with OpenSSL, x509 and TLS before using it in production. + +> **Warning**: +> These TLS commands will only generate a working set of certificates on Linux. +> Mac OS X comes with a version of OpenSSL that is incompatible with the +> certificates that Docker requires. + +## Create a CA, server and client keys with OpenSSL + +> **Note**: replace all instances of `$HOST` in the following example with the +> DNS name of your Docker daemon's host. + +First generate CA private and public keys: + + $ openssl genrsa -aes256 -out ca-key.pem 4096 + Generating RSA private key, 4096 bit long modulus + ............................................................................................................................................................................................++ + ........++ + e is 65537 (0x10001) + Enter pass phrase for ca-key.pem: + Verifying - Enter pass phrase for ca-key.pem: + $ openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem + Enter pass phrase for ca-key.pem: + You are about to be asked to enter information that will be incorporated + into your certificate request. + What you are about to enter is what is called a Distinguished Name or a DN. + There are quite a few fields but you can leave some blank + For some fields there will be a default value, + If you enter '.', the field will be left blank. + ----- + Country Name (2 letter code) [AU]: + State or Province Name (full name) [Some-State]:Queensland + Locality Name (eg, city) []:Brisbane + Organization Name (eg, company) [Internet Widgits Pty Ltd]:Docker Inc + Organizational Unit Name (eg, section) []:Sales + Common Name (e.g. server FQDN or YOUR name) []:$HOST + Email Address []:Sven@home.org.au + +Now that we have a CA, you can create a server key and certificate +signing request (CSR). Make sure that "Common Name" (i.e., server FQDN or YOUR +name) matches the hostname you will use to connect to Docker: + +> **Note**: replace all instances of `$HOST` in the following example with the +> DNS name of your Docker daemon's host. + + $ openssl genrsa -out server-key.pem 4096 + Generating RSA private key, 4096 bit long modulus + .....................................................................++ + .................................................................................................++ + e is 65537 (0x10001) + $ openssl req -subj "/CN=$HOST" -sha256 -new -key server-key.pem -out server.csr + +Next, we're going to sign the public key with our CA: + +Since TLS connections can be made via IP address as well as DNS name, they need +to be specified when creating the certificate. For example, to allow connections +using `10.10.10.20` and `127.0.0.1`: + + $ echo subjectAltName = IP:10.10.10.20,IP:127.0.0.1 > extfile.cnf + + $ openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem \ + -CAcreateserial -out server-cert.pem -extfile extfile.cnf + Signature ok + subject=/CN=your.host.com + Getting CA Private Key + Enter pass phrase for ca-key.pem: + +For client authentication, create a client key and certificate signing +request: + + $ openssl genrsa -out key.pem 4096 + Generating RSA private key, 4096 bit long modulus + .........................................................++ + ................++ + e is 65537 (0x10001) + $ openssl req -subj '/CN=client' -new -key key.pem -out client.csr + +To make the key suitable for client authentication, create an extensions +config file: + + $ echo extendedKeyUsage = clientAuth > extfile.cnf + +Now sign the public key: + + $ openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem \ + -CAcreateserial -out cert.pem -extfile extfile.cnf + Signature ok + subject=/CN=client + Getting CA Private Key + Enter pass phrase for ca-key.pem: + +After generating `cert.pem` and `server-cert.pem` you can safely remove the +two certificate signing requests: + + $ rm -v client.csr server.csr + +With a default `umask` of 022, your secret keys will be *world-readable* and +writable for you and your group. + +In order to protect your keys from accidental damage, you will want to remove their +write permissions. To make them only readable by you, change file modes as follows: + + $ chmod -v 0400 ca-key.pem key.pem server-key.pem + +Certificates can be world-readable, but you might want to remove write access to +prevent accidental damage: + + $ chmod -v 0444 ca.pem server-cert.pem cert.pem + +Now you can make the Docker daemon only accept connections from clients +providing a certificate trusted by our CA: + + $ docker daemon --tlsverify --tlscacert=ca.pem --tlscert=server-cert.pem --tlskey=server-key.pem \ + -H=0.0.0.0:2376 + +To be able to connect to Docker and validate its certificate, you now +need to provide your client keys, certificates and trusted CA: + +> **Note**: replace all instances of `$HOST` in the following example with the +> DNS name of your Docker daemon's host. + + $ docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem \ + -H=$HOST:2376 version + +> **Note**: +> Docker over TLS should run on TCP port 2376. + +> **Warning**: +> As shown in the example above, you don't have to run the `docker` client +> with `sudo` or the `docker` group when you use certificate authentication. +> That means anyone with the keys can give any instructions to your Docker +> daemon, giving them root access to the machine hosting the daemon. Guard +> these keys as you would a root password! + +## Secure by default + +If you want to secure your Docker client connections by default, you can move +the files to the `.docker` directory in your home directory -- and set the +`DOCKER_HOST` and `DOCKER_TLS_VERIFY` variables as well (instead of passing +`-H=tcp://$HOST:2376` and `--tlsverify` on every call). + + $ mkdir -pv ~/.docker + $ cp -v {ca,cert,key}.pem ~/.docker + $ export DOCKER_HOST=tcp://$HOST:2376 DOCKER_TLS_VERIFY=1 + +Docker will now connect securely by default: + + $ docker ps + +## Other modes + +If you don't want to have complete two-way authentication, you can run +Docker in various other modes by mixing the flags. + +### Daemon modes + + - `tlsverify`, `tlscacert`, `tlscert`, `tlskey` set: Authenticate clients + - `tls`, `tlscert`, `tlskey`: Do not authenticate clients + +### Client modes + + - `tls`: Authenticate server based on public/default CA pool + - `tlsverify`, `tlscacert`: Authenticate server based on given CA + - `tls`, `tlscert`, `tlskey`: Authenticate with client certificate, do not + authenticate server based on given CA + - `tlsverify`, `tlscacert`, `tlscert`, `tlskey`: Authenticate with client + certificate and authenticate server based on given CA + +If found, the client will send its client certificate, so you just need +to drop your keys into `~/.docker/{ca,cert,key}.pem`. Alternatively, +if you want to store your keys in another location, you can specify that +location using the environment variable `DOCKER_CERT_PATH`. + + $ export DOCKER_CERT_PATH=~/.docker/zone1/ + $ docker --tlsverify ps + +### Connecting to the secure Docker port using `curl` + +To use `curl` to make test API requests, you need to use three extra command line +flags: + + $ curl https://$HOST:2376/images/json \ + --cert ~/.docker/cert.pem \ + --key ~/.docker/key.pem \ + --cacert ~/.docker/ca.pem + +## Related information + +* [Using certificates for repository client verification](certificates.md) +* [Use trusted images](trust/index.md) diff --git a/docs/security/https/Dockerfile b/docs/security/https/Dockerfile new file mode 100644 index 00000000..a3cc132c --- /dev/null +++ b/docs/security/https/Dockerfile @@ -0,0 +1,10 @@ +FROM debian + +RUN apt-get update && apt-get install -yq openssl + +ADD make_certs.sh / + + +WORKDIR /data +VOLUME ["/data"] +CMD /make_certs.sh diff --git a/docs/security/https/Makefile b/docs/security/https/Makefile new file mode 100644 index 00000000..3c846974 --- /dev/null +++ b/docs/security/https/Makefile @@ -0,0 +1,24 @@ + +HOST:=boot2docker + +makescript: + ./parsedocs.sh > make_certs.sh + +build: clean makescript + docker build -t makecerts . + +cert: build + docker run --rm -it -v $(CURDIR):/data -e HOST=$(HOST) -e YOUR_PUBLIC_IP=$(shell ip a | grep "inet " | sed "s/.*inet \([0-9.]*\)\/.*/\1/" | xargs echo | sed "s/ /,IP:/g") makecerts + +certs: cert + +run: + sudo docker daemon -D --tlsverify --tlscacert=ca.pem --tlscert=server-cert.pem --tlskey=server-key.pem -H=0.0.0.0:6666 --pidfile=$(pwd)/docker.pid --graph=$(pwd)/graph + +client: + sudo docker --tls --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem -H=$(HOST):6666 version + sudo docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem -H=$(HOST):6666 info + sudo curl https://$(HOST):6666/images/json --cert ./cert.pem --key ./key.pem --cacert ./ca.pem + +clean: + rm -f ca-key.pem ca.pem ca.srl cert.pem client.csr extfile.cnf key.pem server-cert.pem server-key.pem server.csr extfile.cnf diff --git a/docs/security/https/README.md b/docs/security/https/README.md new file mode 100644 index 00000000..ff553891 --- /dev/null +++ b/docs/security/https/README.md @@ -0,0 +1,33 @@ + + + + +This is an initial attempt to make it easier to test the examples in the https.md +doc. + +At this point, it has to be a manual thing, and I've been running it in boot2docker. + +My process is as following: + + $ boot2docker ssh + $$ git clone https://github.com/docker/docker + $$ cd docker/docs/articles/https + $$ make cert + +lots of things to see and manually answer, as openssl wants to be interactive + +**NOTE:** make sure you enter the hostname (`boot2docker` in my case) when prompted for `Computer Name`) + + $$ sudo make run + +Start another terminal: + + $ boot2docker ssh + $$ cd docker/docs/articles/https + $$ make client + +The last will connect first with `--tls` and then with `--tlsverify`, both should succeed. diff --git a/docs/security/https/make_certs.sh b/docs/security/https/make_certs.sh new file mode 100755 index 00000000..39001fdb --- /dev/null +++ b/docs/security/https/make_certs.sh @@ -0,0 +1,23 @@ +#!/bin/sh +openssl genrsa -aes256 -out ca-key.pem 2048 +openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem +openssl genrsa -out server-key.pem 2048 +openssl req -subj "/CN=$HOST" -new -key server-key.pem -out server.csr +echo subjectAltName = IP:$YOUR_PUBLIC_IP > extfile.cnf +openssl x509 -req -days 365 -in server.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -extfile extfile.cnf +openssl genrsa -out key.pem 2048 +openssl req -subj '/CN=client' -new -key key.pem -out client.csr +echo extendedKeyUsage = clientAuth > extfile.cnf +openssl x509 -req -days 365 -in client.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out cert.pem -extfile extfile.cnf +rm -v client.csr server.csr +chmod -v 0400 ca-key.pem key.pem server-key.pem +chmod -v 0444 ca.pem server-cert.pem cert.pem +# docker -d --tlsverify --tlscacert=ca.pem --tlscert=server-cert.pem --tlskey=server-key.pem -H=0.0.0.0:7778 +# docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem -H=$HOST:7778 version +mkdir -pv ~/.docker +cp -v {ca,cert,key}.pem ~/.docker +export DOCKER_HOST=tcp://$HOST:7778 DOCKER_TLS_VERIFY=1 +# docker ps +export DOCKER_CERT_PATH=~/.docker/zone1/ +# docker --tlsverify ps +# curl https://$HOST:7778/images/json --cert ~/.docker/cert.pem --key ~/.docker/key.pem --cacert ~/.docker/ca.pem diff --git a/docs/security/https/parsedocs.sh b/docs/security/https/parsedocs.sh new file mode 100755 index 00000000..f9df33c3 --- /dev/null +++ b/docs/security/https/parsedocs.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +echo "#!/bin/sh" +cat ../https.md | awk '{if (sub(/\\$/,"")) printf "%s", $0; else print $0}' \ + | grep ' $ ' \ + | sed 's/ $ //g' \ + | sed 's/2375/7777/g' \ + | sed 's/2376/7778/g' \ + | sed 's/^docker/# docker/g' \ + | sed 's/^curl/# curl/g' diff --git a/docs/security/index.md b/docs/security/index.md new file mode 100644 index 00000000..9524a93e --- /dev/null +++ b/docs/security/index.md @@ -0,0 +1,24 @@ + + +# Secure Engine + +This section discusses the security features you can configure and use within your Docker Engine installation. + +* You can configure Docker's trust features so that your users can push and pull trusted images. To learn how to do this, see [Use trusted images](trust/index.md) in this section. + +* You can protect the Docker daemon socket and ensure only trusted Docker client connections. For more information, [Protect the Docker daemon socket](https.md) + +* You can use certificate-based client-server authentication to verify a Docker daemon has the rights to access images on a registry. For more information, see [Using certificates for repository client verification](certificates.md). + +* You can configure secure computing mode (Seccomp) policies to secure system calls in a container. For more information, see [Seccomp security profiles for Docker](seccomp.md). + +* An AppArmor profile for Docker is installed with the official *.deb* packages. For information about this profile and overriding it, see [AppArmor security profiles for Docker](apparmor.md). diff --git a/docs/security/seccomp.md b/docs/security/seccomp.md new file mode 100644 index 00000000..bf567906 --- /dev/null +++ b/docs/security/seccomp.md @@ -0,0 +1,143 @@ + + +# Seccomp security profiles for Docker + +Secure computing mode (Seccomp) is a Linux kernel feature. You can use it to +restrict the actions available within the container. The `seccomp()` system +call operates on the seccomp state of the calling process. You can use this +feature to restrict your application's access. + +This feature is available only if Docker has been built with seccomp and the +kernel is configured with `CONFIG_SECCOMP` enabled. To check if your kernel +supports seccomp: + +```bash +$ cat /boot/config-`uname -r` | grep CONFIG_SECCOMP= +CONFIG_SECCOMP=y +``` + +> **Note**: seccomp profiles require seccomp 2.2.1 and are only +> available starting with Debian 9 "Stretch", Ubuntu 15.10 "Wily", and +> Fedora 22. To use this feature on Ubuntu 14.04, Debian Wheezy, or +> Debian Jessie, you must download the [latest static Docker Linux binary](../installation/binaries.md). +> This feature is currently *not* available on other distributions. + +## Passing a profile for a container + +The default seccomp profile provides a sane default for running containers with +seccomp and disables around 44 system calls out of 300+. It is moderately protective while providing wide application +compatibility. The default Docker profile (found [here](https://github.com/docker/docker/blob/master/profiles/seccomp/default.json) has a JSON layout in the following form: + +```json +{ + "defaultAction": "SCMP_ACT_ERRNO", + "architectures": [ + "SCMP_ARCH_X86_64", + "SCMP_ARCH_X86", + "SCMP_ARCH_X32" + ], + "syscalls": [ + { + "name": "accept", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "accept4", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + ... + ] +} +``` + +When you run a container, it uses the default profile unless you override +it with the `security-opt` option. For example, the following explicitly +specifies the default policy: + +``` +$ docker run --rm -it --security-opt seccomp=/path/to/seccomp/profile.json hello-world +``` + +### Significant syscalls blocked by the default profile + +Docker's default seccomp profile is a whitelist which specifies the calls that +are allowed. The table below lists the significant (but not all) syscalls that +are effectively blocked because they are not on the whitelist. The table includes +the reason each syscall is blocked rather than white-listed. + +| Syscall | Description | +|---------------------|---------------------------------------------------------------------------------------------------------------------------------------| +| `acct` | Accounting syscall which could let containers disable their own resource limits or process accounting. Also gated by `CAP_SYS_PACCT`. | +| `add_key` | Prevent containers from using the kernel keyring, which is not namespaced. | +| `adjtimex` | Similar to `clock_settime` and `settimeofday`, time/date is not namespaced. | +| `bpf` | Deny loading potentially persistent bpf programs into kernel, already gated by `CAP_SYS_ADMIN`. | +| `clock_adjtime` | Time/date is not namespaced. | +| `clock_settime` | Time/date is not namespaced. | +| `clone` | Deny cloning new namespaces. Also gated by `CAP_SYS_ADMIN` for CLONE_* flags, except `CLONE_USERNS`. | +| `create_module` | Deny manipulation and functions on kernel modules. | +| `delete_module` | Deny manipulation and functions on kernel modules. Also gated by `CAP_SYS_MODULE`. | +| `finit_module` | Deny manipulation and functions on kernel modules. Also gated by `CAP_SYS_MODULE`. | +| `get_kernel_syms` | Deny retrieval of exported kernel and module symbols. | +| `get_mempolicy` | Syscall that modifies kernel memory and NUMA settings. Already gated by `CAP_SYS_NICE`. | +| `init_module` | Deny manipulation and functions on kernel modules. Also gated by `CAP_SYS_MODULE`. | +| `ioperm` | Prevent containers from modifying kernel I/O privilege levels. Already gated by `CAP_SYS_RAWIO`. | +| `iopl` | Prevent containers from modifying kernel I/O privilege levels. Already gated by `CAP_SYS_RAWIO`. | +| `kcmp` | Restrict process inspection capabilities, already blocked by dropping `CAP_PTRACE`. | +| `kexec_file_load` | Sister syscall of `kexec_load` that does the same thing, slightly different arguments. | +| `kexec_load` | Deny loading a new kernel for later execution. | +| `keyctl` | Prevent containers from using the kernel keyring, which is not namespaced. | +| `lookup_dcookie` | Tracing/profiling syscall, which could leak a lot of information on the host. | +| `mbind` | Syscall that modifies kernel memory and NUMA settings. Already gated by `CAP_SYS_NICE`. | +| `modify_ldt` | Old syscall only used in 16-bit code and a potential information leak. | +| `mount` | Deny mounting, already gated by `CAP_SYS_ADMIN`. | +| `move_pages` | Syscall that modifies kernel memory and NUMA settings. | +| `name_to_handle_at` | Sister syscall to `open_by_handle_at`. Already gated by `CAP_SYS_NICE`. | +| `nfsservctl` | Deny interaction with the kernel nfs daemon. | +| `open_by_handle_at` | Cause of an old container breakout. Also gated by `CAP_DAC_READ_SEARCH`. | +| `perf_event_open` | Tracing/profiling syscall, which could leak a lot of information on the host. | +| `personality` | Prevent container from enabling BSD emulation. Not inherently dangerous, but poorly tested, potential for a lot of kernel vulns. | +| `pivot_root` | Deny `pivot_root`, should be privileged operation. | +| `process_vm_readv` | Restrict process inspection capabilities, already blocked by dropping `CAP_PTRACE`. | +| `process_vm_writev` | Restrict process inspection capabilities, already blocked by dropping `CAP_PTRACE`. | +| `ptrace` | Tracing/profiling syscall, which could leak a lot of information on the host. Already blocked by dropping `CAP_PTRACE`. | +| `query_module` | Deny manipulation and functions on kernel modules. | +| `quotactl` | Quota syscall which could let containers disable their own resource limits or process accounting. Also gated by `CAP_SYS_ADMIN`. | +| `reboot` | Don't let containers reboot the host. Also gated by `CAP_SYS_BOOT`. | +| `request_key` | Prevent containers from using the kernel keyring, which is not namespaced. | +| `set_mempolicy` | Syscall that modifies kernel memory and NUMA settings. Already gated by `CAP_SYS_NICE`. | +| `setns` | Deny associating a thread with a namespace. Also gated by `CAP_SYS_ADMIN`. | +| `settimeofday` | Time/date is not namespaced. Also gated by `CAP_SYS_TIME`. | +| `stime` | Time/date is not namespaced. Also gated by `CAP_SYS_TIME`. | +| `swapon` | Deny start/stop swapping to file/device. Also gated by `CAP_SYS_ADMIN`. | +| `swapoff` | Deny start/stop swapping to file/device. Also gated by `CAP_SYS_ADMIN`. | +| `sysfs` | Obsolete syscall. | +| `_sysctl` | Obsolete, replaced by /proc/sys. | +| `umount` | Should be a privileged operation. Also gated by `CAP_SYS_ADMIN`. | +| `umount2` | Should be a privileged operation. | +| `unshare` | Deny cloning new namespaces for processes. Also gated by `CAP_SYS_ADMIN`, with the exception of `unshare --user`. | +| `uselib` | Older syscall related to shared libraries, unused for a long time. | +| `userfaultfd` | Userspace page fault handling, largely needed for process migration. | +| `ustat` | Obsolete syscall. | +| `vm86` | In kernel x86 real mode virtual machine. Also gated by `CAP_SYS_ADMIN`. | +| `vm86old` | In kernel x86 real mode virtual machine. Also gated by `CAP_SYS_ADMIN`. | + +## Run without the default seccomp profile + +You can pass `unconfined` to run a container without the default seccomp +profile. + +``` +$ docker run --rm -it --security-opt seccomp=unconfined debian:jessie \ + unshare --map-root-user --user sh -c whoami +``` diff --git a/docs/security/security.md b/docs/security/security.md new file mode 100644 index 00000000..b9738a3c --- /dev/null +++ b/docs/security/security.md @@ -0,0 +1,276 @@ + + +# Docker security + +There are three major areas to consider when reviewing Docker security: + + - the intrinsic security of the kernel and its support for + namespaces and cgroups; + - the attack surface of the Docker daemon itself; + - loopholes in the container configuration profile, either by default, + or when customized by users. + - the "hardening" security features of the kernel and how they + interact with containers. + +## Kernel namespaces + +Docker containers are very similar to LXC containers, and they have +similar security features. When you start a container with +`docker run`, behind the scenes Docker creates a set of namespaces and control +groups for the container. + +**Namespaces provide the first and most straightforward form of +isolation**: processes running within a container cannot see, and even +less affect, processes running in another container, or in the host +system. + +**Each container also gets its own network stack**, meaning that a +container doesn't get privileged access to the sockets or interfaces +of another container. Of course, if the host system is setup +accordingly, containers can interact with each other through their +respective network interfaces — just like they can interact with +external hosts. When you specify public ports for your containers or use +[*links*](../userguide/networking/default_network/dockerlinks.md) +then IP traffic is allowed between containers. They can ping each other, +send/receive UDP packets, and establish TCP connections, but that can be +restricted if necessary. From a network architecture point of view, all +containers on a given Docker host are sitting on bridge interfaces. This +means that they are just like physical machines connected through a +common Ethernet switch; no more, no less. + +How mature is the code providing kernel namespaces and private +networking? Kernel namespaces were introduced [between kernel version +2.6.15 and +2.6.26](http://lxc.sourceforge.net/index.php/about/kernel-namespaces/). +This means that since July 2008 (date of the 2.6.26 release, now 7 years +ago), namespace code has been exercised and scrutinized on a large +number of production systems. And there is more: the design and +inspiration for the namespaces code are even older. Namespaces are +actually an effort to reimplement the features of [OpenVZ]( +http://en.wikipedia.org/wiki/OpenVZ) in such a way that they could be +merged within the mainstream kernel. And OpenVZ was initially released +in 2005, so both the design and the implementation are pretty mature. + +## Control groups + +Control Groups are another key component of Linux Containers. They +implement resource accounting and limiting. They provide many +useful metrics, but they also help ensure that each container gets +its fair share of memory, CPU, disk I/O; and, more importantly, that a +single container cannot bring the system down by exhausting one of those +resources. + +So while they do not play a role in preventing one container from +accessing or affecting the data and processes of another container, they +are essential to fend off some denial-of-service attacks. They are +particularly important on multi-tenant platforms, like public and +private PaaS, to guarantee a consistent uptime (and performance) even +when some applications start to misbehave. + +Control Groups have been around for a while as well: the code was +started in 2006, and initially merged in kernel 2.6.24. + +## Docker daemon attack surface + +Running containers (and applications) with Docker implies running the +Docker daemon. This daemon currently requires `root` privileges, and you +should therefore be aware of some important details. + +First of all, **only trusted users should be allowed to control your +Docker daemon**. This is a direct consequence of some powerful Docker +features. Specifically, Docker allows you to share a directory between +the Docker host and a guest container; and it allows you to do so +without limiting the access rights of the container. This means that you +can start a container where the `/host` directory will be the `/` directory +on your host; and the container will be able to alter your host filesystem +without any restriction. This is similar to how virtualization systems +allow filesystem resource sharing. Nothing prevents you from sharing your +root filesystem (or even your root block device) with a virtual machine. + +This has a strong security implication: for example, if you instrument Docker +from a web server to provision containers through an API, you should be +even more careful than usual with parameter checking, to make sure that +a malicious user cannot pass crafted parameters causing Docker to create +arbitrary containers. + +For this reason, the REST API endpoint (used by the Docker CLI to +communicate with the Docker daemon) changed in Docker 0.5.2, and now +uses a UNIX socket instead of a TCP socket bound on 127.0.0.1 (the +latter being prone to cross-site-scripting attacks if you happen to run +Docker directly on your local machine, outside of a VM). You can then +use traditional UNIX permission checks to limit access to the control +socket. + +You can also expose the REST API over HTTP if you explicitly decide to do so. +However, if you do that, being aware of the above mentioned security +implication, you should ensure that it will be reachable only from a +trusted network or VPN; or protected with e.g., `stunnel` and client SSL +certificates. You can also secure them with [HTTPS and +certificates](https.md). + +The daemon is also potentially vulnerable to other inputs, such as image +loading from either disk with 'docker load', or from the network with +'docker pull'. This has been a focus of improvement in the community, +especially for 'pull' security. While these overlap, it should be noted +that 'docker load' is a mechanism for backup and restore and is not +currently considered a secure mechanism for loading images. As of +Docker 1.3.2, images are now extracted in a chrooted subprocess on +Linux/Unix platforms, being the first-step in a wider effort toward +privilege separation. + +Eventually, it is expected that the Docker daemon will run restricted +privileges, delegating operations well-audited sub-processes, +each with its own (very limited) scope of Linux capabilities, +virtual network setup, filesystem management, etc. That is, most likely, +pieces of the Docker engine itself will run inside of containers. + +Finally, if you run Docker on a server, it is recommended to run +exclusively Docker in the server, and move all other services within +containers controlled by Docker. Of course, it is fine to keep your +favorite admin tools (probably at least an SSH server), as well as +existing monitoring/supervision processes (e.g., NRPE, collectd, etc). + +## Linux kernel capabilities + +By default, Docker starts containers with a restricted set of +capabilities. What does that mean? + +Capabilities turn the binary "root/non-root" dichotomy into a +fine-grained access control system. Processes (like web servers) that +just need to bind on a port below 1024 do not have to run as root: they +can just be granted the `net_bind_service` capability instead. And there +are many other capabilities, for almost all the specific areas where root +privileges are usually needed. + +This means a lot for container security; let's see why! + +Your average server (bare metal or virtual machine) needs to run a bunch +of processes as root. Those typically include SSH, cron, syslogd; +hardware management tools (e.g., load modules), network configuration +tools (e.g., to handle DHCP, WPA, or VPNs), and much more. A container is +very different, because almost all of those tasks are handled by the +infrastructure around the container: + + - SSH access will typically be managed by a single server running on + the Docker host; + - `cron`, when necessary, should run as a user + process, dedicated and tailored for the app that needs its + scheduling service, rather than as a platform-wide facility; + - log management will also typically be handed to Docker, or by + third-party services like Loggly or Splunk; + - hardware management is irrelevant, meaning that you never need to + run `udevd` or equivalent daemons within + containers; + - network management happens outside of the containers, enforcing + separation of concerns as much as possible, meaning that a container + should never need to perform `ifconfig`, + `route`, or ip commands (except when a container + is specifically engineered to behave like a router or firewall, of + course). + +This means that in most cases, containers will not need "real" root +privileges *at all*. And therefore, containers can run with a reduced +capability set; meaning that "root" within a container has much less +privileges than the real "root". For instance, it is possible to: + + - deny all "mount" operations; + - deny access to raw sockets (to prevent packet spoofing); + - deny access to some filesystem operations, like creating new device + nodes, changing the owner of files, or altering attributes (including + the immutable flag); + - deny module loading; + - and many others. + +This means that even if an intruder manages to escalate to root within a +container, it will be much harder to do serious damage, or to escalate +to the host. + +This won't affect regular web apps; but malicious users will find that +the arsenal at their disposal has shrunk considerably! By default Docker +drops all capabilities except [those +needed](https://github.com/docker/docker/blob/master/oci/defaults_linux.go#L64-L79), +a whitelist instead of a blacklist approach. You can see a full list of +available capabilities in [Linux +manpages](http://man7.org/linux/man-pages/man7/capabilities.7.html). + +One primary risk with running Docker containers is that the default set +of capabilities and mounts given to a container may provide incomplete +isolation, either independently, or when used in combination with +kernel vulnerabilities. + +Docker supports the addition and removal of capabilities, allowing use +of a non-default profile. This may make Docker more secure through +capability removal, or less secure through the addition of capabilities. +The best practice for users would be to remove all capabilities except +those explicitly required for their processes. + +## Other kernel security features + +Capabilities are just one of the many security features provided by +modern Linux kernels. It is also possible to leverage existing, +well-known systems like TOMOYO, AppArmor, SELinux, GRSEC, etc. with +Docker. + +While Docker currently only enables capabilities, it doesn't interfere +with the other systems. This means that there are many different ways to +harden a Docker host. Here are a few examples. + + - You can run a kernel with GRSEC and PAX. This will add many safety + checks, both at compile-time and run-time; it will also defeat many + exploits, thanks to techniques like address randomization. It doesn't + require Docker-specific configuration, since those security features + apply system-wide, independent of containers. + - If your distribution comes with security model templates for + Docker containers, you can use them out of the box. For instance, we + ship a template that works with AppArmor and Red Hat comes with SELinux + policies for Docker. These templates provide an extra safety net (even + though it overlaps greatly with capabilities). + - You can define your own policies using your favorite access control + mechanism. + +Just like there are many third-party tools to augment Docker containers +with e.g., special network topologies or shared filesystems, you can +expect to see tools to harden existing Docker containers without +affecting Docker's core. + +As of Docker 1.10 User Namespaces are supported directly by the docker +daemon. This feature allows for the root user in a container to be mapped +to a non uid-0 user outside the container, which can help to mitigate the +risks of container breakout. This facility is available but not enabled +by default. + +Refer to the [daemon command](../reference/commandline/daemon.md#daemon-user-namespace-options) +in the command line reference for more information on this feature. +Additional information on the implementation of User Namespaces in Docker +can be found in this blog post. + +## Conclusions + +Docker containers are, by default, quite secure; especially if you take +care of running your processes inside the containers as non-privileged +users (i.e., non-`root`). + +You can add an extra layer of safety by enabling AppArmor, SELinux, +GRSEC, or your favorite hardening solution. + +Last but not least, if you see interesting security features in other +containerization systems, these are simply kernels features that may +be implemented in Docker as well. We welcome users to submit issues, +pull requests, and communicate via the mailing list. + +## Related Information + +* [Use trusted images](../security/trust/index.md) +* [Seccomp security profiles for Docker](../security/seccomp.md) +* [AppArmor security profiles for Docker](../security/apparmor.md) +* [On the Security of Containers (2014)](https://medium.com/@ewindisch/on-the-security-of-containers-2c60ffe25a9e) diff --git a/docs/security/trust/content_trust.md b/docs/security/trust/content_trust.md new file mode 100644 index 00000000..f11e4e6e --- /dev/null +++ b/docs/security/trust/content_trust.md @@ -0,0 +1,300 @@ + + +# Content trust in Docker + +When transferring data among networked systems, *trust* is a central concern. In +particular, when communicating over an untrusted medium such as the internet, it +is critical to ensure the integrity and publisher of all the data a system +operates on. You use Docker to push and pull images (data) to a registry. Content trust +gives you the ability to both verify the integrity and the publisher of all the +data received from a registry over any channel. + +Content trust is currently only available for users of the public Docker Hub. It +is currently not available for the Docker Trusted Registry or for private +registries. + +## Understand trust in Docker + +Content trust allows operations with a remote Docker registry to enforce +client-side signing and verification of image tags. Content trust provides the +ability to use digital signatures for data sent to and received from remote +Docker registries. These signatures allow client-side verification of the +integrity and publisher of specific image tags. + +Currently, content trust is disabled by default. You must enable it by setting +the `DOCKER_CONTENT_TRUST` environment variable. Refer to the +[environment variables](../../reference/commandline/cli.md#environment-variables) +and [Notary](../../reference/commandline/cli.md#notary) configuration +for the docker client for more options. + +Once content trust is enabled, image publishers can sign their images. Image consumers can +ensure that the images they use are signed. publishers and consumers can be +individuals alone or in organizations. Docker's content trust supports users and +automated processes such as builds. + +### Image tags and content trust + +An individual image record has the following identifier: + +``` +[REGISTRY_HOST[:REGISTRY_PORT]/]REPOSITORY[:TAG] +``` + +A particular image `REPOSITORY` can have multiple tags. For example, `latest` and + `3.1.2` are both tags on the `mongo` image. An image publisher can build an image + and tag combination many times changing the image with each build. + +Content trust is associated with the `TAG` portion of an image. Each image +repository has a set of keys that image publishers use to sign an image tag. +Image publishers have discretion on which tags they sign. + +An image repository can contain an image with one tag that is signed and another +tag that is not. For example, consider [the Mongo image +repository](https://hub.docker.com/r/library/mongo/tags/). The `latest` +tag could be unsigned while the `3.1.6` tag could be signed. It is the +responsibility of the image publisher to decide if an image tag is signed or +not. In this representation, some image tags are signed, others are not: + +![Signed tags](images/tag_signing.png) + +Publishers can choose to sign a specific tag or not. As a result, the content of +an unsigned tag and that of a signed tag with the same name may not match. For +example, a publisher can push a tagged image `someimage:latest` and sign it. +Later, the same publisher can push an unsigned `someimage:latest` image. This second +push replaces the last unsigned tag `latest` but does not affect the signed `latest` version. +The ability to choose which tags they can sign, allows publishers to iterate over +the unsigned version of an image before officially signing it. + +Image consumers can enable content trust to ensure that images they use were +signed. If a consumer enables content trust, they can only pull, run, or build +with trusted images. Enabling content trust is like wearing a pair of +rose-colored glasses. Consumers "see" only signed images tags and the less +desirable, unsigned image tags are "invisible" to them. + +![Trust view](images/trust_view.png) + +To the consumer who does not enabled content trust, nothing about how they +work with Docker images changes. Every image is visible regardless of whether it +is signed or not. + + +### Content trust operations and keys + +When content trust is enabled, `docker` CLI commands that operate on tagged images must +either have content signatures or explicit content hashes. The commands that +operate with content trust are: + +* `push` +* `build` +* `create` +* `pull` +* `run` + +For example, with content trust enabled a `docker pull someimage:latest` only +succeeds if `someimage:latest` is signed. However, an operation with an explicit +content hash always succeeds as long as the hash exists: + +```bash +$ docker pull someimage@sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a +``` + +Trust for an image tag is managed through the use of signing keys. A key set is +created when an operation using content trust is first invoked. A key set consists +of the following classes of keys: + +- an offline key that is the root of content trust for a image tag +- repository or tagging keys that sign tags +- server-managed keys such as the timestamp key, which provides freshness + security guarantees for your repository + +The following image depicts the various signing keys and their relationships: + +![Content trust components](images/trust_components.png) + +>**WARNING**: Loss of the root key is **very difficult** to recover from. +>Correcting this loss requires intervention from [Docker +>Support](https://support.docker.com) to reset the repository state. This loss +>also requires **manual intervention** from every consumer that used a signed +>tag from this repository prior to the loss. + +You should backup the root key somewhere safe. Given that it is only required +to create new repositories, it is a good idea to store it offline. +For details on securing, and backing up your keys, make sure you +read how to [manage keys for content trust](trust_key_mng.md). + +## Survey of typical content trust operations + +This section surveys the typical trusted operations users perform with Docker +images. + +### Enable and disable content trust per-shell or per-invocation + +In a shell, you can enable content trust by setting the `DOCKER_CONTENT_TRUST` +environment variable. Enabling per-shell is useful because you can have one +shell configured for trusted operations and another terminal shell for untrusted +operations. You can also add this declaration to your shell profile to have it +turned on always by default. + +To enable content trust in a `bash` shell enter the following command: + +```bash +export DOCKER_CONTENT_TRUST=1 +``` + +Once set, each of the "tag" operations requires a key for a trusted tag. + +In an environment where `DOCKER_CONTENT_TRUST` is set, you can use the +`--disable-content-trust` flag to run individual operations on tagged images +without content trust on an as-needed basis. + +```bash +$ docker pull --disable-content-trust docker/trusttest:untrusted +``` + +To invoke a command with content trust enabled regardless of whether or how the `DOCKER_CONTENT_TRUST` variable is set: + +```bash +$ docker build --disable-content-trust=false -t docker/trusttest:testing . +``` + +All of the trusted operations support the `--disable-content-trust` flag. + + +### Push trusted content + +To create signed content for a specific image tag, simply enable content trust +and push a tagged image. If this is the first time you have pushed an image +using content trust on your system, the session looks like this: + +```bash +$ docker push docker/trusttest:latest +The push refers to a repository [docker.io/docker/trusttest] (len: 1) +9a61b6b1315e: Image already exists +902b87aaaec9: Image already exists +latest: digest: sha256:d02adacee0ac7a5be140adb94fa1dae64f4e71a68696e7f8e7cbf9db8dd49418 size: 3220 +Signing and pushing trust metadata +You are about to create a new root signing key passphrase. This passphrase +will be used to protect the most sensitive key in your signing system. Please +choose a long, complex passphrase and be careful to keep the password and the +key file itself secure and backed up. It is highly recommended that you use a +password manager to generate the passphrase and keep it safe. There will be no +way to recover this key. You can find the key in your config directory. +Enter passphrase for new root key with id a1d96fb: +Repeat passphrase for new root key with id a1d96fb: +Enter passphrase for new repository key with id docker.io/docker/trusttest (3a932f1): +Repeat passphrase for new repository key with id docker.io/docker/trusttest (3a932f1): +Finished initializing "docker.io/docker/trusttest" +``` +When you push your first tagged image with content trust enabled, the `docker` +client recognizes this is your first push and: + + - alerts you that it will create a new root key + - requests a passphrase for the key + - generates a root key in the `~/.docker/trust` directory + - generates a repository key for in the `~/.docker/trust` directory + +The passphrase you chose for both the root key and your content key-pair +should be randomly generated and stored in a *password manager*. + +> **NOTE**: If you omit the `latest` tag, content trust is skipped. This is true +even if content trust is enabled and even if this is your first push. + +```bash +$ docker push docker/trusttest +The push refers to a repository [docker.io/docker/trusttest] (len: 1) +9a61b6b1315e: Image successfully pushed +902b87aaaec9: Image successfully pushed +latest: digest: sha256:a9a9c4402604b703bed1c847f6d85faac97686e48c579bd9c3b0fa6694a398fc size: 3220 +No tag specified, skipping trust metadata push +``` + +It is skipped because as the message states, you did not supply an image `TAG` +value. In Docker content trust, signatures are associated with tags. + +Once you have a root key on your system, subsequent images repositories +you create can use that same root key: + +```bash +$ docker push docker.io/docker/seaside:latest +The push refers to a repository [docker.io/docker/seaside] (len: 1) +a9539b34a6ab: Image successfully pushed +b3dbab3810fc: Image successfully pushed +latest: digest: sha256:d2ba1e603661a59940bfad7072eba698b79a8b20ccbb4e3bfb6f9e367ea43939 size: 3346 +Signing and pushing trust metadata +Enter key passphrase for root key with id a1d96fb: +Enter passphrase for new repository key with id docker.io/docker/seaside (bb045e3): +Repeat passphrase for new repository key with id docker.io/docker/seaside (bb045e3): +Finished initializing "docker.io/docker/seaside" +``` + +The new image has its own repository key and timestamp key. The `latest` tag is signed with both of +these. + + +### Pull image content + +A common way to consume an image is to `pull` it. With content trust enabled, the Docker +client only allows `docker pull` to retrieve signed images. + +``` +$ docker pull docker/seaside +Using default tag: latest +Pull (1 of 1): docker/trusttest:latest@sha256:d149ab53f871 +... +Tagging docker/trusttest@sha256:d149ab53f871 as docker/trusttest:latest +``` + +The `seaside:latest` image is signed. In the following example, the command does not specify a tag, so the system uses +the `latest` tag by default again and the `docker/cliffs:latest` tag is not signed. + +```bash +$ docker pull docker/cliffs +Using default tag: latest +no trust data available +``` + +Because the tag `docker/cliffs:latest` is not trusted, the `pull` fails. + + +### Disable content trust for specific operations + +A user that wants to disable content trust for a particular operation can use the +`--disable-content-trust` flag. **Warning: this flag disables content trust for +this operation**. With this flag, Docker will ignore content-trust and allow all +operations to be done without verifying any signatures. If we wanted the +previous untrusted build to succeed we could do: + +``` +$ cat Dockerfile +FROM docker/trusttest:notrust +RUN echo +$ docker build --disable-content-trust -t docker/trusttest:testing . +Sending build context to Docker daemon 42.84 MB +... +Successfully built f21b872447dc +``` + +The same is true for all the other commands, such as `pull` and `push`: + +``` +$ docker pull --disable-content-trust docker/trusttest:untrusted +... +$ docker push --disable-content-trust docker/trusttest:untrusted +... +``` + +## Related information + +* [Manage keys for content trust](trust_key_mng.md) +* [Automation with content trust](trust_automation.md) +* [Delegations for content trust](trust_delegation.md) +* [Play in a content trust sandbox](trust_sandbox.md) diff --git a/docs/security/trust/deploying_notary.md b/docs/security/trust/deploying_notary.md new file mode 100644 index 00000000..68e57fb7 --- /dev/null +++ b/docs/security/trust/deploying_notary.md @@ -0,0 +1,34 @@ + + +# Deploying Notary Server with Compose + +The easiest way to deploy Notary Server is by using Docker Compose. To follow the procedure on this page, you must have already [installed Docker Compose](/compose/install.md). + +1. Clone the Notary repository + + git clone git@github.com:docker/notary.git + +2. Build and start Notary Server with the sample certificates. + + docker-compose up -d + + + For more detailed documentation about how to deploy Notary Server see https://github.com/docker/notary. +3. Make sure that your Docker or Notary client trusts Notary Server's certificate before you try to interact with the Notary server. + +See the instructions for [Docker](../../reference/commandline/cli.md#notary) or +for [Notary](https://github.com/docker/notary#using-notary) depending on which one you are using. + +## If you want to use Notary in production + +Please check back here for instructions after Notary Server has an official +stable release. To get a head start on deploying Notary in production see +https://github.com/docker/notary. diff --git a/docs/security/trust/images/tag_signing.png b/docs/security/trust/images/tag_signing.png new file mode 100644 index 0000000000000000000000000000000000000000..9a1f9062b4dc6d30942ab7c4f47dca5f5df2be31 GIT binary patch literal 74416 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 Key

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":311.0,"y":147.0,"rotation":0.0,"id":268,"width":18.0,"height":53.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":178,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":152,"py":0.5,"px":1.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":264,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":17,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[-3.417721518987321,-4.214000000000027],[9.708860759493689,-4.214000000000027],[9.708860759493689,50.74999999999994],[22.8354430379747,50.74999999999994]],"lockSegments":{},"ortho":true}},"linkMap":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":415.0,"y":313.0,"rotation":0.0,"id":250,"width":7.0,"height":413.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":172,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":79,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":"1.0,1.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[3.5,-3.0],[9.5,497.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":290.0,"y":340.0,"rotation":0.0,"id":11,"width":63.0,"height":82.0,"uid":"com.gliffy.shape.network.network_v4.business.user","order":12,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.user","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":12,"width":48.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Account

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":479.0,"y":330.0,"rotation":0.0,"id":2,"width":120.0,"height":80.0,"uid":"com.gliffy.shape.network.network_v4.business.user_group","order":9,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.user_group","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":3,"width":73.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Organization

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":159.0,"y":310.0,"rotation":0.0,"id":79,"width":531.0,"height":500.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":0,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#ffffff","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":159.00000000000003,"y":320.0,"rotation":0.0,"id":82,"width":108.99999999999999,"height":20.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":58,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Registry

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":730.0,"y":340.0,"rotation":0.0,"id":86,"width":61.0,"height":79.0,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":59,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#ff0000","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":87,"width":62.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Offline key

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":730.0,"y":455.0,"rotation":0.0,"id":88,"width":61.0,"height":79.0,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":62,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":89,"width":70.0,"height":14.0,"uid":null,"order":64,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Tagging key

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":360.4891500904159,"y":650.0,"rotation":0.0,"id":227,"width":23.16455696202532,"height":30.000000000000007,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":158,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":228,"width":16.0,"height":18.0,"uid":null,"order":160,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

X

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":185.1428571428571,"y":587.0,"rotation":0.0,"id":109,"width":187.85714285714286,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":81,"lockAspectRatio":false,"lockShape":false,"children":[{"x":7.142857142857139,"y":50.0,"rotation":0.0,"id":98,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_right","order":74,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":99,"width":71.42857142857143,"height":50.0,"uid":null,"order":77,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":98}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":98}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_right","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":-7.142857142857139,"y":0.0,"rotation":0.0,"id":100,"width":50.0,"height":18.0,"uid":null,"order":80,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":98,"px":-0.1,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

working

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":7.571428571428527,"y":0.0,"rotation":0.0,"id":95,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_right","order":66,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":96,"width":71.42857142857143,"height":50.0,"uid":null,"order":69,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":95}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":95}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_right","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":-7.142857142857139,"y":0.0,"rotation":0.0,"id":97,"width":38.0,"height":18.0,"uid":null,"order":72,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":95,"px":-0.1,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

latest

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":77.85714285714286,"y":8.0,"rotation":0.0,"id":30,"width":110.00000000000001,"height":80.0,"uid":"com.gliffy.shape.sitemap.sitemap_v2.photo","order":24,"lockAspectRatio":false,"lockShape":false,"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":31,"width":110.00000000000001,"height":25.0,"uid":null,"order":27,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":32}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.rounded_top","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":32,"width":110.00000000000001,"height":25.0,"uid":null,"order":31,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":6,"paddingRight":2,"paddingBottom":6,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Repository

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":25.0,"rotation":0.0,"id":33,"width":110.00000000000001,"height":55.0,"uid":null,"order":34,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":30},{"magnitude":-1,"id":32}],"minHeight":0.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":32,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.sitemap.sitemap_v2.photo","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":184.21428571428567,"y":450.0,"rotation":0.0,"id":253,"width":187.85714285714286,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":173,"lockAspectRatio":false,"lockShape":false,"children":[{"x":77.85714285714286,"y":8.0,"rotation":0.0,"id":125,"width":110.00000000000001,"height":80.0,"uid":"com.gliffy.shape.sitemap.sitemap_v2.photo","order":83,"lockAspectRatio":false,"lockShape":false,"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":126,"width":110.00000000000001,"height":25.0,"uid":null,"order":86,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":127}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.rounded_top","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":127,"width":110.00000000000001,"height":25.0,"uid":null,"order":90,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":6,"paddingRight":2,"paddingBottom":6,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Repository

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":25.0,"rotation":0.0,"id":128,"width":110.00000000000001,"height":55.0,"uid":null,"order":93,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":125},{"magnitude":-1,"id":127}],"minHeight":0.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":127,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.sitemap.sitemap_v2.photo","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":7.571428571428527,"y":0.0,"rotation":0.0,"id":122,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_right","order":95,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":123,"width":71.42857142857143,"height":50.0,"uid":null,"order":98,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":122}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":122}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_right","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":-7.142857142857139,"y":0.0,"rotation":0.0,"id":124,"width":38.0,"height":18.0,"uid":null,"order":101,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":122,"px":-0.1,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

latest

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":7.142857142857139,"y":50.0,"rotation":0.0,"id":119,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_right","order":103,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":120,"width":71.42857142857143,"height":50.0,"uid":null,"order":106,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":119}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":119}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_right","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":-7.142857142857139,"y":0.0,"rotation":0.0,"id":121,"width":26.0,"height":18.0,"uid":null,"order":109,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":119,"px":-0.1,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

2.0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":479.0,"y":120.74999999999994,"rotation":0.0,"id":261,"width":155.08307142857143,"height":168.072,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":174,"lockAspectRatio":false,"lockShape":false,"children":[{"x":85.65449999999998,"y":38.0,"rotation":0.0,"id":245,"width":28.0,"height":43.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":171,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":193,"py":0.5,"px":1.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":204,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":17,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[2.5108499095841808,-13.999999999999972],[16.0465641952984,-13.999999999999972],[16.0465641952984,39.0],[29.582278481012622,39.0]],"lockSegments":{},"ortho":true}},"linkMap":[],"hidden":false,"layerId":null},{"x":89.65449999999998,"y":25.0,"rotation":0.0,"id":244,"width":24.0,"height":1.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":169,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":193,"py":0.5,"px":1.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":192,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":17,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[-1.4891500904158192,-0.9999999999999716],[7.534659433393699,-0.9999999999999716],[16.558468957203104,-0.9999999999999716],[25.582278481012622,-0.9999999999999716]],"lockSegments":{},"ortho":true}},"linkMap":[],"hidden":false,"layerId":null},{"x":115.2367784810126,"y":62.0,"rotation":0.0,"id":204,"width":23.16455696202532,"height":30.000000000000007,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":151,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":205,"width":15.0,"height":16.0,"uid":null,"order":154,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

C

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":null},{"x":115.2367784810126,"y":9.000000000000028,"rotation":0.0,"id":192,"width":23.16455696202532,"height":30.000000000000007,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":148,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":201,"width":15.0,"height":16.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

A

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":null},{"x":65.0007929475588,"y":9.000000000000028,"rotation":0.0,"id":193,"width":23.16455696202532,"height":30.000000000000007,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":141,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#ff0000","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":194,"width":14.0,"height":18.0,"uid":null,"order":144,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

2

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":null},{"x":55.08307142857143,"y":0.0,"rotation":0.0,"id":195,"width":100.0,"height":133.0,"uid":"com.gliffy.shape.ui.ui_v3.containers_content.speech_bubble_right","order":129,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"MinWidthConstraint","MinWidthConstraint":{"width":100}},{"type":"HeightConstraint","HeightConstraint":{"isMin":true,"heightInfo":[{"magnitude":1,"id":197},{"magnitude":1,"id":198}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":196,"width":100.0,"height":118.0,"uid":null,"order":132,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":195,"px":0.0,"py":0.0,"xOffset":0.0,"yOffset":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":195},{"magnitude":-1,"id":198}],"minHeight":0.0,"growParent":false,"padding":0.0}},{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":195}],"minWidth":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.containers_content.speech_bubble","strokeWidth":2.0,"strokeColor":"#BBBBBB","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":197,"width":100.0,"height":29.0,"uid":null,"order":136,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":195}],"minWidth":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

 

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":36.0,"y":117.0,"rotation":0.0,"id":198,"width":24.0,"height":15.0,"uid":null,"order":139,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"ConstWidthConstraint","ConstWidthConstraint":{"width":24}},{"type":"ConstHeightConstraint","ConstHeightConstraint":{"height":15}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":196,"px":1.0,"py":1.0,"xOffset":-64.0,"yOffset":-1.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.containers_content.speech_bubble_right","strokeWidth":2.0,"strokeColor":"#BBBBBB","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":67.0,"rotation":0.0,"id":180,"width":67.309,"height":101.072,"uid":"com.gliffy.shape.cisco.cisco_v1.buildings.generic_building","order":126,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.buildings.generic_building","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":182,"width":56.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Company

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":231.1785714285715,"y":204.78599999999997,"rotation":0.0,"id":0,"width":63.0,"height":82.0,"uid":"com.gliffy.shape.network.network_v4.business.female_user","order":6,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.female_user","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":1,"width":43.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Person

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":272.07142857142856,"y":120.286,"rotation":0.0,"id":171,"width":100.0,"height":132.0,"uid":"com.gliffy.shape.ui.ui_v3.containers_content.speech_bubble_right","order":112,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"MinWidthConstraint","MinWidthConstraint":{"width":100}},{"type":"HeightConstraint","HeightConstraint":{"isMin":true,"heightInfo":[{"magnitude":1,"id":173},{"magnitude":1,"id":174}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":172,"width":100.0,"height":117.0,"uid":null,"order":114,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":171,"px":0.0,"py":0.0,"xOffset":0.0,"yOffset":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":171},{"magnitude":-1,"id":174}],"minHeight":0.0,"growParent":false,"padding":0.0}},{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":171}],"minWidth":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.containers_content.speech_bubble","strokeWidth":2.0,"strokeColor":"#BBBBBB","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":173,"width":100.0,"height":29.0,"uid":null,"order":117,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":171}],"minWidth":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

 

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":36.0,"y":116.0,"rotation":0.0,"id":174,"width":24.0,"height":15.0,"uid":null,"order":119,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"ConstWidthConstraint","ConstWidthConstraint":{"width":24}},{"type":"ConstHeightConstraint","ConstHeightConstraint":{"height":15}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":172,"px":1.0,"py":1.0,"xOffset":-64.0,"yOffset":-1.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.containers_content.speech_bubble_right","strokeWidth":2.0,"strokeColor":"#BBBBBB","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":310.5,"y":146.78599999999997,"rotation":0.0,"id":239,"width":20.0,"height":1.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":167,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":152,"py":0.5,"px":1.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":237,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":17,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[-2.917721518987321,-4.0],[6.078661844484657,-4.0],[15.075045207956578,-4.0],[24.071428571428555,-4.0]],"lockSegments":{},"ortho":true}},"linkMap":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":333.8354430379747,"y":182.74999999999994,"rotation":0.0,"id":264,"width":23.16455696202532,"height":30.000000000000007,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":175,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":265,"width":21.0,"height":18.0,"uid":null,"order":177,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

 N

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":284.4177215189874,"y":127.78599999999997,"rotation":0.0,"id":152,"width":23.16455696202532,"height":30.000000000000007,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":120,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#ff0000","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":153,"width":14.0,"height":18.0,"uid":null,"order":122,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

1

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":334.57142857142856,"y":127.78599999999997,"rotation":0.0,"id":237,"width":23.16455696202532,"height":30.000000000000007,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":164,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":238,"width":16.0,"height":18.0,"uid":null,"order":166,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

X

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":565.0,"y":500.0,"rotation":0.0,"id":40,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left","order":1,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":41,"width":71.42857142857143,"height":50.0,"uid":null,"order":3,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":40}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":40}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_left","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":10.714285714285666,"y":0.0,"rotation":0.0,"id":42,"width":26.0,"height":18.0,"uid":null,"order":5,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":40,"px":0.15,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

1.0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":454.99999999999994,"y":461.0,"rotation":0.0,"id":16,"width":110.00000000000001,"height":80.0,"uid":"com.gliffy.shape.sitemap.sitemap_v2.photo","order":15,"lockAspectRatio":false,"lockShape":false,"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":17,"width":110.00000000000001,"height":25.0,"uid":null,"order":17,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":18}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.rounded_top","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":18,"width":110.00000000000001,"height":25.0,"uid":null,"order":20,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":6,"paddingRight":2,"paddingBottom":6,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Repository

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":25.0,"rotation":0.0,"id":19,"width":110.00000000000001,"height":55.0,"uid":null,"order":22,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":16},{"magnitude":-1,"id":18}],"minHeight":0.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":18,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.sitemap.sitemap_v2.photo","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":565.0,"y":450.0,"rotation":0.0,"id":37,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left","order":35,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":38,"width":71.42857142857143,"height":50.0,"uid":null,"order":37,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":37}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":37}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_left","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":10.714285714285666,"y":0.0,"rotation":0.0,"id":39,"width":38.0,"height":18.0,"uid":null,"order":39,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":37,"px":0.15,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

latest

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":443.4177215189873,"y":513.0,"rotation":0.0,"id":229,"width":23.16455696202532,"height":30.000000000000007,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":161,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":230,"width":15.0,"height":16.0,"uid":null,"order":163,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

A

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":565.0,"y":630.0,"rotation":0.0,"id":63,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left","order":40,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":64,"width":71.42857142857143,"height":50.0,"uid":null,"order":42,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":63}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":63}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_left","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":10.714285714285666,"y":0.0,"rotation":0.0,"id":65,"width":68.0,"height":18.0,"uid":null,"order":44,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":63,"px":0.15,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

producttion

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":454.99999999999994,"y":591.0,"rotation":0.0,"id":58,"width":110.00000000000001,"height":80.0,"uid":"com.gliffy.shape.sitemap.sitemap_v2.photo","order":45,"lockAspectRatio":false,"lockShape":false,"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":59,"width":110.00000000000001,"height":25.0,"uid":null,"order":47,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":60}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.rounded_top","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":60,"width":110.00000000000001,"height":25.0,"uid":null,"order":50,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":6,"paddingRight":2,"paddingBottom":6,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Repository

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":25.0,"rotation":0.0,"id":61,"width":110.00000000000001,"height":55.0,"uid":null,"order":52,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":58},{"magnitude":-1,"id":60}],"minHeight":0.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":60,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.sitemap.sitemap_v2.photo","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":565.0,"y":580.0,"rotation":0.0,"id":55,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left","order":53,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":56,"width":71.42857142857143,"height":50.0,"uid":null,"order":55,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":55}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":55}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_left","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":10.714285714285666,"y":0.0,"rotation":0.0,"id":57,"width":28.0,"height":18.0,"uid":null,"order":57,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":55,"px":0.15,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

test

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":443.4177215189873,"y":646.0,"rotation":0.0,"id":221,"width":23.16455696202532,"height":30.000000000000007,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":155,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":222,"width":15.0,"height":16.0,"uid":null,"order":157,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

C

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":565.0,"y":745.0,"rotation":0.0,"id":281,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left","order":179,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":282,"width":71.42857142857143,"height":50.0,"uid":null,"order":181,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":281}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":281}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_left","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":10.714285714285666,"y":0.0,"rotation":0.0,"id":283,"width":48.0,"height":18.0,"uid":null,"order":183,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":281,"px":0.15,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

release

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":454.99999999999994,"y":706.0,"rotation":0.0,"id":277,"width":110.00000000000001,"height":80.0,"uid":"com.gliffy.shape.sitemap.sitemap_v2.photo","order":184,"lockAspectRatio":false,"lockShape":false,"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":278,"width":110.00000000000001,"height":25.0,"uid":null,"order":186,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":279}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.rounded_top","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":279,"width":110.00000000000001,"height":25.0,"uid":null,"order":189,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":6,"paddingRight":2,"paddingBottom":6,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Repository

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":25.0,"rotation":0.0,"id":280,"width":110.00000000000001,"height":55.0,"uid":null,"order":191,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":277},{"magnitude":-1,"id":279}],"minHeight":0.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":279,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.sitemap.sitemap_v2.photo","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":565.0,"y":695.0,"rotation":0.0,"id":274,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left","order":192,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":275,"width":71.42857142857143,"height":50.0,"uid":null,"order":194,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":274}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":274}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_left","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":10.714285714285666,"y":0.0,"rotation":0.0,"id":276,"width":26.0,"height":18.0,"uid":null,"order":196,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":274,"px":0.15,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

7.5

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":360.4891500904159,"y":510.0,"rotation":0.0,"id":289,"width":23.16455696202532,"height":30.000000000000007,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":197,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":290,"width":21.0,"height":18.0,"uid":null,"order":199,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

 N

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":332.57142857142856,"y":532.0,"rotation":0.0,"id":301,"width":30.0,"height":30.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.events.timer_intermediate","order":205,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.timer_intermediate.bpmn_v1","strokeWidth":2.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":330.4177215189874,"y":670.0,"rotation":0.0,"id":302,"width":30.0,"height":30.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.events.timer_intermediate","order":206,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.timer_intermediate.bpmn_v1","strokeWidth":2.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":466.5822784810126,"y":667.0,"rotation":0.0,"id":303,"width":30.0,"height":30.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.events.timer_intermediate","order":207,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.timer_intermediate.bpmn_v1","strokeWidth":2.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":621.401335443038,"y":508.0,"rotation":0.0,"id":306,"width":20.0,"height":12.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":209,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":621.401335443038,"y":459.0,"rotation":0.0,"id":307,"width":20.0,"height":12.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":210,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":621.401335443038,"y":589.0,"rotation":0.0,"id":308,"width":20.0,"height":12.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":211,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":186.21428571428567,"y":594.0,"rotation":0.0,"id":309,"width":20.0,"height":12.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":212,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":189.21428571428567,"y":644.0,"rotation":0.0,"id":310,"width":20.0,"height":12.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":213,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":810.0,"y":358.5,"rotation":0.0,"id":164,"width":217.0,"height":70.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":110,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

A offline key is used to create repository keys. Offline keys belong to a person or an organization. Resides client-side. You should store these in a safe place and back them up. 

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":810.0,"y":487.5,"rotation":0.0,"id":170,"width":217.0,"height":56.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":111,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

A tagging key is associated with an image repository. publishers with this key can push or pull any tag in this repository. This resides on client-side.

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":810.0,"y":587.0,"rotation":0.0,"id":298,"width":217.0,"height":42.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":203,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

A timestamp key is associated with an image repository. This is created by Docker and resides on the server.

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":743.3333333333334,"y":681.0,"rotation":0.0,"id":314,"width":283.66666666666663,"height":20.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":215,"lockAspectRatio":false,"lockShape":false,"children":[{"x":66.66666666666663,"y":4.0,"rotation":0.0,"id":312,"width":217.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":214,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Signed tag.

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":null},{"x":0.0,"y":0.0,"rotation":0.0,"id":304,"width":33.333333333333336,"height":20.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":208,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"}],"layers":[{"guid":"dockVlz9GmcW","order":0,"name":"Layer 0","active":true,"locked":false,"visible":true,"nodeIndex":216}],"shapeStyles":{},"lineStyles":{"global":{"strokeWidth":1,"endArrow":17}},"textStyles":{"global":{"size":"16px","color":"#000000"}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":["com.gliffy.libraries.cisco.cisco_v1.buildings","com.gliffy.libraries.sitemap.sitemap_v2","com.gliffy.libraries.sitemap.sitemap_v1.default","com.gliffy.libraries.ui.ui_v3.containers_content","com.gliffy.libraries.table.table_v2.default","com.gliffy.libraries.ui.ui_v3.navigation","com.gliffy.libraries.ui.ui_v3.forms_controls","com.gliffy.libraries.ui.ui_v3.icon_symbols","com.gliffy.libraries.ui.ui_v2.forms_components","com.gliffy.libraries.ui.ui_v2.content","com.gliffy.libraries.ui.ui_v2.miscellaneous","com.gliffy.libraries.network.network_v4.business","com.gliffy.libraries.flowchart.flowchart_v1.default","com.gliffy.libraries.basic.basic_v1.default","com.gliffy.libraries.bpmn.bpmn_v1.events","com.gliffy.libraries.bpmn.bpmn_v1.activities","com.gliffy.libraries.bpmn.bpmn_v1.data_artifacts","com.gliffy.libraries.bpmn.bpmn_v1.gateways","com.gliffy.libraries.bpmn.bpmn_v1.connectors","com.gliffy.libraries.swimlanes.swimlanes_v1.default","com.gliffy.libraries.images"],"lastSerialized":1439068390533},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/docs/security/trust/images/trust_components.gliffy b/docs/security/trust/images/trust_components.gliffy new file mode 100644 index 00000000..07c859bb --- /dev/null +++ b/docs/security/trust/images/trust_components.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#FFFFFF","width":881,"height":704,"nodeIndex":316,"autoFit":true,"exportBorder":false,"gridOn":true,"snapToGrid":true,"drawingGuidesOn":true,"pageBreaksOn":false,"printGridOn":false,"printPaper":null,"printShrinkToFit":false,"printPortrait":false,"maxWidth":5000,"maxHeight":5000,"themeData":null,"viewportType":"default","fitBB":{"min":{"x":10,"y":10},"max":{"x":880.0000000000001,"y":703.7139999999999}},"printModel":{"pageSize":"a4","portrait":false,"fitToOnePage":false,"displayPageBreaks":false},"objects":[{"x":10.0,"y":199.714,"rotation":0.0,"id":79,"width":531.0,"height":500.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":0,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#ffffff","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":416.0,"y":389.714,"rotation":0.0,"id":40,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left","order":1,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":41,"width":71.42857142857143,"height":50.0,"uid":null,"order":3,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":40}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":40}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_left","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":10.714285714285722,"y":0.0,"rotation":0.0,"id":42,"width":26.0,"height":18.0,"uid":null,"order":5,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":40,"px":0.15,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

1.0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":82.1785714285715,"y":94.49999999999997,"rotation":0.0,"id":0,"width":63.0,"height":82.0,"uid":"com.gliffy.shape.network.network_v4.business.female_user","order":6,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.female_user","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":1,"width":43.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Person

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":330.0,"y":219.714,"rotation":0.0,"id":2,"width":120.0,"height":80.0,"uid":"com.gliffy.shape.network.network_v4.business.user_group","order":9,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.user_group","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":3,"width":73.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Organization

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":141.0,"y":229.714,"rotation":0.0,"id":11,"width":63.0,"height":82.0,"uid":"com.gliffy.shape.network.network_v4.business.user","order":12,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.user","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":12,"width":48.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Account

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":305.99999999999994,"y":350.714,"rotation":0.0,"id":16,"width":110.00000000000001,"height":80.0,"uid":"com.gliffy.shape.sitemap.sitemap_v2.photo","order":15,"lockAspectRatio":false,"lockShape":false,"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":17,"width":110.00000000000001,"height":25.0,"uid":null,"order":17,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":18}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.rounded_top","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":18,"width":110.00000000000001,"height":25.0,"uid":null,"order":20,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":6,"paddingRight":2,"paddingBottom":6,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Repository

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":25.0,"rotation":0.0,"id":19,"width":110.00000000000001,"height":55.0,"uid":null,"order":22,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":16},{"magnitude":-1,"id":18}],"minHeight":0.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":18,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.sitemap.sitemap_v2.photo","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":416.0,"y":339.714,"rotation":0.0,"id":37,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left","order":35,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":38,"width":71.42857142857143,"height":50.0,"uid":null,"order":37,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":37}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":37}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_left","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":10.714285714285722,"y":0.0,"rotation":0.0,"id":39,"width":38.0,"height":18.0,"uid":null,"order":39,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":37,"px":0.15,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

latest

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":416.0,"y":519.7139999999999,"rotation":0.0,"id":63,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left","order":40,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":64,"width":71.42857142857143,"height":50.0,"uid":null,"order":42,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":63}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":63}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_left","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":10.714285714285722,"y":0.0,"rotation":0.0,"id":65,"width":68.0,"height":18.0,"uid":null,"order":44,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":63,"px":0.15,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

producttion

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":305.99999999999994,"y":480.71399999999994,"rotation":0.0,"id":58,"width":110.00000000000001,"height":80.0,"uid":"com.gliffy.shape.sitemap.sitemap_v2.photo","order":45,"lockAspectRatio":false,"lockShape":false,"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":59,"width":110.00000000000001,"height":25.0,"uid":null,"order":47,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":60}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.rounded_top","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":60,"width":110.00000000000001,"height":25.0,"uid":null,"order":50,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":6,"paddingRight":2,"paddingBottom":6,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Repository

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":25.0,"rotation":0.0,"id":61,"width":110.00000000000001,"height":55.0,"uid":null,"order":52,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":58},{"magnitude":-1,"id":60}],"minHeight":0.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":60,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.sitemap.sitemap_v2.photo","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":416.0,"y":469.714,"rotation":0.0,"id":55,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left","order":53,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":56,"width":71.42857142857143,"height":50.0,"uid":null,"order":55,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":55}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":55}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_left","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":10.714285714285722,"y":0.0,"rotation":0.0,"id":57,"width":28.0,"height":18.0,"uid":null,"order":57,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":55,"px":0.15,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

test

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":10.000000000000036,"y":209.714,"rotation":0.0,"id":82,"width":108.99999999999999,"height":20.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":58,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Registry

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":581.0,"y":229.714,"rotation":0.0,"id":86,"width":61.0,"height":79.0,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":59,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#ff0000","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":87,"width":62.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Offline key

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":581.0,"y":344.714,"rotation":0.0,"id":88,"width":61.0,"height":79.0,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":62,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":89,"width":70.0,"height":14.0,"uid":null,"order":64,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Tagging key

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":36.142857142857125,"y":476.71399999999994,"rotation":0.0,"id":109,"width":187.85714285714286,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":81,"lockAspectRatio":false,"lockShape":false,"children":[{"x":7.142857142857139,"y":50.0,"rotation":0.0,"id":98,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_right","order":74,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":99,"width":71.42857142857143,"height":50.0,"uid":null,"order":77,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":98}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":98}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_right","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":-7.142857142857139,"y":0.0,"rotation":0.0,"id":100,"width":50.0,"height":18.0,"uid":null,"order":80,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":98,"px":-0.1,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

working

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":7.571428571428527,"y":0.0,"rotation":0.0,"id":95,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_right","order":66,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":96,"width":71.42857142857143,"height":50.0,"uid":null,"order":69,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":95}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":95}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_right","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":-7.142857142857139,"y":0.0,"rotation":0.0,"id":97,"width":38.0,"height":18.0,"uid":null,"order":72,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":95,"px":-0.1,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

latest

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":77.85714285714286,"y":8.0,"rotation":0.0,"id":30,"width":110.00000000000001,"height":80.0,"uid":"com.gliffy.shape.sitemap.sitemap_v2.photo","order":24,"lockAspectRatio":false,"lockShape":false,"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":31,"width":110.00000000000001,"height":25.0,"uid":null,"order":27,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":32}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.rounded_top","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":32,"width":110.00000000000001,"height":25.0,"uid":null,"order":31,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":6,"paddingRight":2,"paddingBottom":6,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Repository

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":25.0,"rotation":0.0,"id":33,"width":110.00000000000001,"height":55.0,"uid":null,"order":34,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":30},{"magnitude":-1,"id":32}],"minHeight":0.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":32,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.sitemap.sitemap_v2.photo","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":661.0,"y":248.214,"rotation":0.0,"id":164,"width":217.0,"height":70.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":110,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

A offline key is used to create tagging keys. Offline keys belong to a person or an organization. Resides client-side. You should store these in a safe place and back them up. 

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":661.0,"y":377.214,"rotation":0.0,"id":170,"width":217.0,"height":56.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":111,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

A tagging key is associated with an image repository. Creators with this key can push or pull any tag in this repository. This resides on client-side.

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":123.07142857142856,"y":10.0,"rotation":0.0,"id":171,"width":100.0,"height":132.0,"uid":"com.gliffy.shape.ui.ui_v3.containers_content.speech_bubble_right","order":112,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"MinWidthConstraint","MinWidthConstraint":{"width":100}},{"type":"HeightConstraint","HeightConstraint":{"isMin":true,"heightInfo":[{"magnitude":1,"id":173},{"magnitude":1,"id":174}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":172,"width":100.0,"height":117.0,"uid":null,"order":114,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":171,"px":0.0,"py":0.0,"xOffset":0.0,"yOffset":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":171},{"magnitude":-1,"id":174}],"minHeight":0.0,"growParent":false,"padding":0.0}},{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":171}],"minWidth":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.containers_content.speech_bubble","strokeWidth":2.0,"strokeColor":"#BBBBBB","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":173,"width":100.0,"height":29.0,"uid":null,"order":117,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":171}],"minWidth":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

 

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":36.0,"y":116.0,"rotation":0.0,"id":174,"width":24.0,"height":15.0,"uid":null,"order":119,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"ConstWidthConstraint","ConstWidthConstraint":{"width":24}},{"type":"ConstHeightConstraint","ConstHeightConstraint":{"height":15}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":172,"px":1.0,"py":1.0,"xOffset":-64.0,"yOffset":-1.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.containers_content.speech_bubble_right","strokeWidth":2.0,"strokeColor":"#BBBBBB","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":135.41772151898738,"y":17.499999999999968,"rotation":0.0,"id":152,"width":23.16455696202532,"height":30.000000000000007,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":120,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#ff0000","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":153,"width":14.0,"height":18.0,"uid":null,"order":122,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

1

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":294.4177215189873,"y":535.7139999999999,"rotation":0.0,"id":221,"width":23.16455696202532,"height":30.000000000000007,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":155,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":222,"width":15.0,"height":16.0,"uid":null,"order":157,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

C

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":211.48915009041588,"y":539.7139999999999,"rotation":0.0,"id":227,"width":23.16455696202532,"height":30.000000000000007,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":158,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":228,"width":16.0,"height":18.0,"uid":null,"order":160,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

X

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":294.4177215189873,"y":402.714,"rotation":0.0,"id":229,"width":23.16455696202532,"height":30.000000000000007,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":161,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":230,"width":15.0,"height":16.0,"uid":null,"order":163,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

A

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":185.57142857142856,"y":17.499999999999968,"rotation":0.0,"id":237,"width":23.16455696202532,"height":30.000000000000007,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":164,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":238,"width":16.0,"height":18.0,"uid":null,"order":166,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

X

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":161.5,"y":36.49999999999997,"rotation":0.0,"id":239,"width":20.0,"height":1.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":167,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":152,"py":0.5,"px":1.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":237,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":17,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[-2.9177215189872925,-4.0],[6.078661844484657,-4.0],[15.075045207956606,-4.0],[24.071428571428555,-4.0]],"lockSegments":{},"ortho":true}},"linkMap":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":266.0,"y":202.714,"rotation":0.0,"id":250,"width":7.0,"height":413.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":172,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":79,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":"1.0,1.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[3.5,-3.0],[9.5,496.99999999999994]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":35.21428571428568,"y":339.714,"rotation":0.0,"id":253,"width":187.85714285714286,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":173,"lockAspectRatio":false,"lockShape":false,"children":[{"x":77.85714285714286,"y":8.0,"rotation":0.0,"id":125,"width":110.00000000000001,"height":80.0,"uid":"com.gliffy.shape.sitemap.sitemap_v2.photo","order":83,"lockAspectRatio":false,"lockShape":false,"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":126,"width":110.00000000000001,"height":25.0,"uid":null,"order":86,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":127}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.rounded_top","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":127,"width":110.00000000000001,"height":25.0,"uid":null,"order":90,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":6,"paddingRight":2,"paddingBottom":6,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Repository

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":25.0,"rotation":0.0,"id":128,"width":110.00000000000001,"height":55.0,"uid":null,"order":93,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":125},{"magnitude":-1,"id":127}],"minHeight":0.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":127,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.sitemap.sitemap_v2.photo","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":7.571428571428527,"y":0.0,"rotation":0.0,"id":122,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_right","order":95,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":123,"width":71.42857142857143,"height":50.0,"uid":null,"order":98,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":122}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":122}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_right","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":-7.142857142857139,"y":0.0,"rotation":0.0,"id":124,"width":38.0,"height":18.0,"uid":null,"order":101,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":122,"px":-0.1,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

latest

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":7.142857142857139,"y":50.0,"rotation":0.0,"id":119,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_right","order":103,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":120,"width":71.42857142857143,"height":50.0,"uid":null,"order":106,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":119}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":119}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_right","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":-7.142857142857139,"y":0.0,"rotation":0.0,"id":121,"width":26.0,"height":18.0,"uid":null,"order":109,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":119,"px":-0.1,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

2.0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":330.0,"y":10.463999999999942,"rotation":0.0,"id":261,"width":155.08307142857143,"height":168.072,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":174,"lockAspectRatio":false,"lockShape":false,"children":[{"x":85.65449999999998,"y":38.0,"rotation":0.0,"id":245,"width":28.0,"height":43.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":171,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":193,"py":0.5,"px":1.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":204,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":17,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[2.510849909584124,-13.999999999999972],[16.0465641952984,-13.999999999999972],[16.0465641952984,39.0],[29.582278481012622,39.0]],"lockSegments":{},"ortho":true}},"linkMap":[],"hidden":false,"layerId":null},{"x":89.65449999999998,"y":25.0,"rotation":0.0,"id":244,"width":24.0,"height":1.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":169,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":193,"py":0.5,"px":1.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":192,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":17,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[-1.489150090415876,-0.9999999999999716],[7.534659433393642,-0.9999999999999716],[16.558468957203104,-0.9999999999999716],[25.582278481012622,-0.9999999999999716]],"lockSegments":{},"ortho":true}},"linkMap":[],"hidden":false,"layerId":null},{"x":115.2367784810126,"y":62.0,"rotation":0.0,"id":204,"width":23.16455696202532,"height":30.000000000000007,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":151,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":205,"width":15.0,"height":16.0,"uid":null,"order":154,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

C

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":null},{"x":115.2367784810126,"y":9.000000000000028,"rotation":0.0,"id":192,"width":23.16455696202532,"height":30.000000000000007,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":148,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":201,"width":15.0,"height":16.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

A

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":null},{"x":65.0007929475588,"y":9.000000000000028,"rotation":0.0,"id":193,"width":23.16455696202532,"height":30.000000000000007,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":141,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#ff0000","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":194,"width":14.0,"height":18.0,"uid":null,"order":144,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

2

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":null},{"x":55.08307142857143,"y":0.0,"rotation":0.0,"id":195,"width":100.0,"height":133.0,"uid":"com.gliffy.shape.ui.ui_v3.containers_content.speech_bubble_right","order":129,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"MinWidthConstraint","MinWidthConstraint":{"width":100}},{"type":"HeightConstraint","HeightConstraint":{"isMin":true,"heightInfo":[{"magnitude":1,"id":197},{"magnitude":1,"id":198}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":196,"width":100.0,"height":118.0,"uid":null,"order":132,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":195,"px":0.0,"py":0.0,"xOffset":0.0,"yOffset":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":195},{"magnitude":-1,"id":198}],"minHeight":0.0,"growParent":false,"padding":0.0}},{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":195}],"minWidth":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.containers_content.speech_bubble","strokeWidth":2.0,"strokeColor":"#BBBBBB","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":197,"width":100.0,"height":29.0,"uid":null,"order":136,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":195}],"minWidth":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

 

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":36.0,"y":117.0,"rotation":0.0,"id":198,"width":24.0,"height":15.0,"uid":null,"order":139,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"ConstWidthConstraint","ConstWidthConstraint":{"width":24}},{"type":"ConstHeightConstraint","ConstHeightConstraint":{"height":15}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":196,"px":1.0,"py":1.0,"xOffset":-64.0,"yOffset":-1.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.containers_content.speech_bubble_right","strokeWidth":2.0,"strokeColor":"#BBBBBB","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":67.0,"rotation":0.0,"id":180,"width":67.309,"height":101.072,"uid":"com.gliffy.shape.cisco.cisco_v1.buildings.generic_building","order":126,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.buildings.generic_building","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":182,"width":56.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Company

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":184.8354430379747,"y":72.46399999999994,"rotation":0.0,"id":264,"width":23.16455696202532,"height":30.000000000000007,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":175,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":265,"width":21.0,"height":18.0,"uid":null,"order":177,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

 N

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":162.0,"y":36.714,"rotation":0.0,"id":268,"width":18.0,"height":53.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":178,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":152,"py":0.5,"px":1.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":264,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":17,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[-3.4177215189872925,-4.214000000000027],[9.708860759493689,-4.214000000000027],[9.708860759493689,50.74999999999994],[22.8354430379747,50.74999999999994]],"lockSegments":{},"ortho":true}},"linkMap":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":416.0,"y":634.7139999999999,"rotation":0.0,"id":281,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left","order":179,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":282,"width":71.42857142857143,"height":50.0,"uid":null,"order":181,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":281}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":281}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_left","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":10.714285714285722,"y":0.0,"rotation":0.0,"id":283,"width":48.0,"height":18.0,"uid":null,"order":183,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":281,"px":0.15,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

release

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":305.99999999999994,"y":595.7139999999999,"rotation":0.0,"id":277,"width":110.00000000000001,"height":80.0,"uid":"com.gliffy.shape.sitemap.sitemap_v2.photo","order":184,"lockAspectRatio":false,"lockShape":false,"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":278,"width":110.00000000000001,"height":25.0,"uid":null,"order":186,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":279}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.rounded_top","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":279,"width":110.00000000000001,"height":25.0,"uid":null,"order":189,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":6,"paddingRight":2,"paddingBottom":6,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Repository

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":25.0,"rotation":0.0,"id":280,"width":110.00000000000001,"height":55.0,"uid":null,"order":191,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":277},{"magnitude":-1,"id":279}],"minHeight":0.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":279,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.sitemap.sitemap_v2.photo","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":416.0,"y":584.7139999999999,"rotation":0.0,"id":274,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left","order":192,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":275,"width":71.42857142857143,"height":50.0,"uid":null,"order":194,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":274}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":274}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_left","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":10.714285714285722,"y":0.0,"rotation":0.0,"id":276,"width":26.0,"height":18.0,"uid":null,"order":196,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":274,"px":0.15,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

7.5

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":211.48915009041588,"y":399.714,"rotation":0.0,"id":289,"width":23.16455696202532,"height":30.000000000000007,"uid":"com.gliffy.shape.network.network_v4.business.encrypted","order":197,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.encrypted","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":290,"width":21.0,"height":18.0,"uid":null,"order":199,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

 N

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":584.0,"y":467.714,"rotation":0.0,"id":294,"width":54.0,"height":54.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.events.timer_intermediate","order":200,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.timer_intermediate.bpmn_v1","strokeWidth":2.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":297,"width":88.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Timestamp Key

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":661.0,"y":476.714,"rotation":0.0,"id":298,"width":217.0,"height":42.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":203,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

A timestamp key is associated with an image repository. This is created by Docker and resides on the server.

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":316.5822784810126,"y":420.714,"rotation":0.0,"id":299,"width":30.0,"height":30.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.events.timer_intermediate","order":204,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.timer_intermediate.bpmn_v1","strokeWidth":2.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":183.57142857142856,"y":421.714,"rotation":0.0,"id":301,"width":30.0,"height":30.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.events.timer_intermediate","order":205,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.timer_intermediate.bpmn_v1","strokeWidth":2.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":181.41772151898738,"y":559.7139999999999,"rotation":0.0,"id":302,"width":30.0,"height":30.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.events.timer_intermediate","order":206,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.timer_intermediate.bpmn_v1","strokeWidth":2.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":317.5822784810126,"y":556.7139999999999,"rotation":0.0,"id":303,"width":30.0,"height":30.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.events.timer_intermediate","order":207,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.timer_intermediate.bpmn_v1","strokeWidth":2.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":472.40133544303796,"y":397.714,"rotation":0.0,"id":306,"width":20.0,"height":12.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":209,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":472.40133544303796,"y":348.714,"rotation":0.0,"id":307,"width":20.0,"height":12.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":210,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":472.40133544303796,"y":478.714,"rotation":0.0,"id":308,"width":20.0,"height":12.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":211,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":37.214285714285666,"y":483.714,"rotation":0.0,"id":309,"width":20.0,"height":12.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":212,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":40.214285714285666,"y":533.7139999999999,"rotation":0.0,"id":310,"width":20.0,"height":12.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":213,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":594.3333333333335,"y":570.7139999999999,"rotation":0.0,"id":314,"width":283.66666666666663,"height":20.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":215,"lockAspectRatio":false,"lockShape":false,"children":[{"x":66.66666666666663,"y":4.0,"rotation":0.0,"id":312,"width":217.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":214,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Signed tag.

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":null},{"x":0.0,"y":0.0,"rotation":0.0,"id":304,"width":33.333333333333336,"height":20.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":208,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"}],"layers":[{"guid":"dockVlz9GmcW","order":0,"name":"Layer 0","active":true,"locked":false,"visible":true,"nodeIndex":216}],"shapeStyles":{},"lineStyles":{"global":{"strokeWidth":1,"endArrow":17}},"textStyles":{"global":{"size":"16px","color":"#000000"}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":["com.gliffy.libraries.cisco.cisco_v1.buildings","com.gliffy.libraries.sitemap.sitemap_v2","com.gliffy.libraries.sitemap.sitemap_v1.default","com.gliffy.libraries.ui.ui_v3.containers_content","com.gliffy.libraries.table.table_v2.default","com.gliffy.libraries.ui.ui_v3.navigation","com.gliffy.libraries.ui.ui_v3.forms_controls","com.gliffy.libraries.ui.ui_v3.icon_symbols","com.gliffy.libraries.ui.ui_v2.forms_components","com.gliffy.libraries.ui.ui_v2.content","com.gliffy.libraries.ui.ui_v2.miscellaneous","com.gliffy.libraries.network.network_v4.business","com.gliffy.libraries.flowchart.flowchart_v1.default","com.gliffy.libraries.basic.basic_v1.default","com.gliffy.libraries.bpmn.bpmn_v1.events","com.gliffy.libraries.bpmn.bpmn_v1.activities","com.gliffy.libraries.bpmn.bpmn_v1.data_artifacts","com.gliffy.libraries.bpmn.bpmn_v1.gateways","com.gliffy.libraries.bpmn.bpmn_v1.connectors","com.gliffy.libraries.swimlanes.swimlanes_v1.default","com.gliffy.libraries.images"],"lastSerialized":1439174260766},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/docs/security/trust/images/trust_components.png b/docs/security/trust/images/trust_components.png new file mode 100644 index 0000000000000000000000000000000000000000..039dfc8cf323cf1933ebb385a5b0edd2e6be37ab GIT binary patch literal 124071 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⁢TU`eop|-D&mDE^r8- z?Uwc-=c6_qMC9HoR#Bn7(d|6lona)k?#0A*vDG>FDf_ni z^uW6|na&X8f8+eU z8^0nP&>0U(KKvfQP^ZjHyp@6guE3WUhl3m~yMi6)Ok8zoPD1M06?qf-XO!3+=Cy0p z!_oXC)>C2{7?$3QTep+w)GRxABJkAWwkIhcL$o7$Q7b|f$sXrN@+dGZ>!MwU0(2}T z7nQ1bpqJT@_k=R@K%>&ZL;_DK?3HPS=q(c7Hg2L?w)D7|4)^6jj%Aco*IAPA%9duW zAiSO{@25Ys-0-!0%d2p?cW2m_SGjRdcm2O=8ody!YiFT<1aMM zntT)?V^}y|yT*^wQlWC?meqeLhNeiEtz!Q)RqY_G$t<)e$;=aXM87?m?*3}HS2Mja zx(bK7nun>PK7$L;dev?5DlEP~#4piqsx|-Du+y-w^RJ zj2vqXKHS|!^qtg{HDzKO8m_ECu>D9dEh2oDJPJ3*euBK_&5m>;H<|J;3WPlouX111 zc^l@150ChThKc*v2;hA9JYnn=woR?zG6@(>YZrtwP;+0=k3Bc`yILR9vCO*o4+*H@ zFQF@nq?N4~Q(RtqR}wtO37<6{@)_%Y`@ZxJ5q&tN<8iu1V%BP`8Ye#AofX>>LtG|J zmZ&KSveEQM^E^8+$~U+ItNH+F4qr#-z%;sKPuZc>4HHHozKOy7nM!}MRc>}demj#M z;o17aF!^p<9J{G!=W9Lo+OLXXx`2@}Yb5lX3Q;%JXos0hBS*96k4yb16g+#KZTE%` z^ig9{wvYu<_Ghw6+{gq|En_;Z?jK4jZLYW~0f&ip0}<;9?NH(fmDe`gNZ6DTDa5&W z)TzCr<-SuL>`0<|{VQpX`n?gyrccxELW^NZ^UkC_O}4+VghA$*&R3mE2B~zB7I}BC z{j-Y#$D8Qm>>{e056m8s@Z#5-1 z(cq1q{`F;xtrMxrvM0G=a^Qu`Z-e>g{wdvu`Pr(xHzU zX7H=*nfE7yy7kQPZ}~pK3P*g@n)(nL&}Nc82eL_&68qp|0vLT4$vyd6EQ|j5)nAhS zDd_%elcYYX!^k}2&Bb1lWTTG)6I~L|tAfBrB+J z(2A6QalZv4#&6F8DeohRIGK$|wMx}B2XwLg$zlPjrvA&-)m6)wSzug+J_x$v(&5lg ztAEl#P46}+QxY2o+xj`~>K1b>gf@$e_Km^5mxI~3IVxlNkkEE<`<;9Py6NAcZKYuY zS{NeA$Nv|ee#c9%*)G$~mbOPdp&fQSOX>F=gv-f!+xSjNf8alA=42f`nvfFue5vb` z@7056@msb$+tnP=vOOxq#sOm9`BF6Q5~gb8OgFM9Lm&a?rjaFFN&y3Ph-EL=ma)5o zODjDmw?H^tSJYkq<`T5Xy?ct!_O1^_{Bo);z@B6wF%ymCR=1RVROP+1B0gPyNJ&~6 zJTFhr_mK%9R23&yj~s#eUM^K9boI)+H1xLjLl3m}wY(Gt$710(B7IZnnJAgt87$^(gF42asEa<3g~{qa_J5uv<2 zp45o(fgY>s9;g|ymqQQHSvcXw%64s^CB!EcxP8ueM}ye2P3PIvs$jC}pf zmtrJw{}UIIn+BSx3J$ib{eL&YT=*pk$c(zwc-$AeGkNi&9oO5XEL$S}bki93(G80q zN_{{;VN*(Et@B=K!X3J{Zt;tzH3r6yn(FWcQUW(`AiF9Rdy|vDA)H+QMxWZRm(vmg zLza--45sg?W^0!lL`+}+IvR6)XZGr0$Gc7nX7$=CLiRL9!v-g9lX=jVYlQm1*-tFN zn=yL6TfE7oF>$)yH&pnxL2T!8vliUA@gI|8S4;~fHG9Q*{r|*12gQ+^Cd=TH|}UE_Ve-aK-h;KvLSgFNlx#s+W%0N-)#-?F)~O{}0w9I>m&-*0dZ# z?mBqr1<--t1$&3f-RsdJ`)VgAAf`zUQB4Hk(0~Cg99L|7oL$csBU?W0;HUlJ7;y7Y;xPiv^&= zzw@MvE+^5o&MAEC!nnVZ3joV#I^7(TY1L`xHfPGw#8B$2}+Y4^t` z$dg7vKfN|GnbxFhF`)ospoLY7HsSP}2wL<(>qvI!&!(Dirk2$9&JLEuN3NfPTzifw zr{#@xP688K$dh;wAig9!D(Ms+nj{nJG9#s6o+b3%g=RVTzX@u%-Np8A#q~+rFF3H) zreLQ(6`2;6mi@BSzWGi$4Ngl8BpxjWU;l`9GC$X-$}sfI7OrGU!;<0 z0ahd;74`pN?7YLNj{m=3h+`jnbB?|Dh|FWJY*~?!k&zY2=Gg1lGn#6GF*O zl9_Dx`}F<(?)&=vcmLIOx$^P(y!ZR{dcGdd*P8EZFJi?YSSTO0%HtUWol5-GP)wXi z3+(j`*QwH&08rEB2_$2ivajnypeH+XI!@3`KJ#)2tQ6ms3is=7AEw{Tzkcq?E(iW8 zU>p^2UUq4}N~*AI`><e+Q48yg2KVF#&o9+G-!VdEzk$Pk z3lhr1aDEAL`|5Sqkx#}y$O`hjD<5AimRLHEy(1@~?=AV~>-s&!K<9&hwc=|!ax3J~ z_h-lwj3*Yp$>nEr7w}v1`1&~UKY01IGp450h2a!8NZh)*luDL+@tvDdj47SilQjR+ zTCnK-UMUc8aU)gtd?IuCwzB@r{j^`p(|OjdrD9PoEeVbRr}sW;Nwhtk&-GgWpQF5` zZ#6}+WDhHLw?%c<(aHfxe4k?LP>bkmQ1DfVzFd;`B)de5ZgU?K@y$wWUov2fU`Igh zbHnj*NG}y(eF2d)!<%wG17$bOmfS`l&Fow4wZ7qb07dkrS1okMlPab9Tn8QwneBtn zIi6e44$k^U_1iIh_CO2j28ZHg)7IzpAEG~Tzq0O${KkY>l!gNDGCf%QY^w^`v+7xQqEQfXC?gS*vZIs zy~N>eUD8;njYpUfp~*x)5rY(`Hhn23BAW8Eo3t?o31KzoQU#4S(xI+%qk}6!hp3AU|nmwuB z!Q|nvKtN|XcG&)P?D?M20UKP4jnA4F6t3Hx|9q9;d9X(Ixd4Xb`)Qn_uWT?q!?0dJAz3!vn-n_fyW@~eN*E67Sb zjY!%Vw#oNbD~-+C5V@=8@$_Fh3=$b7nb8HZ3wc6U>+ZUxY9uYdQ}&1eH+Zls9EayE z1=ec+f;C`4h-sb~c*g7lJtQaJVLoP5*~OpttbE!D(?=`bfd{0BuLXwFl;YL;7W;55 zBO#=qk%RbNb7MAZ)N_7<{v@VaerwD-Q(YTXvg=-_Q6@!3N*4yjO1OU*;(m14euz%Ijd zl`v&=k=M>dFOiMrJt!hN?(6f30H+x2X|?^(iAP_HN8Q9sZLD^_wSD|IR~K}C5=Jyp z;P=_R^#98FEMypXqU63}n%xqE6MVMX6gTmo{o?`jj|0sfYlh~alG$j%e%83+4+VaZ z@o4>AJ1)97L5QLeS^wk?Ce`G=PUdj<-pJ9y!l7X>_pTYg%z|Mi;tufV-C}%3*DpQj zH2gnWfTEX+YHpE&i*y&tWB;tQB3wmzAkT;TH8Z&2c)_j*zzM(lt zCXo-2eNqnX`0!tU!~R0x>9mi=8UC3)R-+kpGt=ZMgkrZ^I5 zC>e34dtI$Dqp=AQO^S8do<`2YEmYozGatW4j6u8ftn79fmL-1k7x67MuND51d4Biz zhZIwVD)y51Hcued!VgSV7l`6bV*0i~{dNinJ5}BPUUG+~H=Znk9!Y>tI3gxi-;E@D zD<6PC!m|<|;dvlQEb!S;~%#C<`gGaz=}0DgYP_Xlx269*pbB&mg_5I$r< zmN4!z|8c(FtwdXTK}~>s*x-DcW z+QN1C{P1zF*Co2lu$H?FwZyDn;tP;=yE|d#$@WTSyWR2(4Nez-b2RjUSo+7BSHfn* zp6=xdoaI;7soN1ld@yxzPXN?Fp)v51$!o&zXp>8u0hw||c?lzMOqy6PRPKF%ik`ci zkwpejO|x&*-Rj<2tl`I~#ClQ*gPeXMxfRvce*dM^&ju*jXS7lJStgDKx*x5`5F@1Z z#$ij3JnSU$<-t!l$jeXfME2mwskkeL$C~m61)2^^yU;`v?q`F%_l9xvO&F`s;EgYO%jEOGnj%TKNJUOy)ot5H4$%$?Aklp*^nXc zWOqXBcJJ9A?NGrzEdUOIt7F^$vFxdS@Z~{&+4FFra*$ks3n4cvLU@;Nv!0h33_1Qa zsGUCjl=$z$rO)5?aL+{+f#Hoe8fx46Y@fSA%^wjFtaPV%#SLJOtng&j)!RhImq@d# zR5!_a#%dnD74V^DT%MKi{iK4X1E+88HnneQCY^9dN@e^~R5koA)bU@)#QKY5Xrw0H zcW)SkW17&UXCFu9h4=BN*aqQ`c|O3*h$$>s+?e8Y!#NC#i`5ZVp%vDAdg?V;&iDNE z-Yh*oPU|%D(8W*5NT=}EVG6L>_?itgoltC-Ax@oo9B}qW> z>4_ho0ri5{;>*H64^uod4OrGAleBqHNU#Ed#+BS2DdLN0@6uT zJBZ>n1e|$p9FOp=ZtwDE$ON;A`Cx?WZ#(5r5&RUg9tzFIw=Rk4``IWDx%v7xK;5^K zN#w6)(6WNxdF888*J;|JV=Or}qVTLX(oR^Kpyi0puK*rzNjCx9w9Z}jTwdq39j4i# zhDH%(yjPNh$+bHl-x!GaokYH?NeDewf-1^6;v2}I;-95}oWQf2urk8$UYBjn1PHMH zb#~AjAQ;{;xni2}iP8dliiQQ-u)w}gtgI1N8!;UD5ZW*~!>`6Eg<>!i-g)#pOigSp zu3dec3HI`w$#Nt2)yJQ-h^w1JZ24gH3-#|-I>)Io1hycSkv>G)j|$zlvmK3P86Z3qrsMk1T?UfAI_bezq zoRG6b1sq!6e#x#e6YgL!KlKc?qYAT%YJAW0RLr`JFJKqsJ4QSI3T>txhv!xLI5Dz= z5NSYBiS}#gjz4uxUMhRz%0leAm&~$Bhr5tVf;fY^-%-3mXA+_{6DMYGi8+9&+~Md>2PLvxc)HS;2lL35kkRx*P&T|B`Rc7>Q7K z0xu=eC@D^y-hL08+4^15g3P_3el5$VCn(=-U?NWzfHA0p6`G`^hR~60 z3|ztD!=QCig9Rx&D&PH1qb`eK36WYe94cBbp$R0utN``?Dj%2|gR-6Fm!1Cc6e*>;ZiT6$@ELirp8SlCmE?9+u zFAm^!q9`tKHRwi5>F_Z?DcEfpQ(2a*C{j`ObTYh)$9g+Kq!Zz&9sn0=i6!sTc43Y3 z8zOU~cJK`AMUssu2{C;DDXkek;pC6r!UoQ!&eYD=_xY91BYz-2p7O}!wdJ)MCgFPN z^(CrnH^XFB1hrI?O{Ab!K#_27y07@isoqk{}ht>yRkcvDqAmC_I6$lad~) zHfsLg!h9*D>_uccboCJW-@^PuwVTsK8)A60Mx7{eR@_3F(Lk;9RQw|Ne5kr4qJl$A zFINT>8ip;M(jHJ^pro{DzqB%e;xVU*!cbwRPkStRg}dQtcSIs)qJoPJQ=Ww6+9K3S z?l5O_-FCoGjw)q8q(eYlwIa9;D+(d-&FFwlDqxo>8YJeZFVuD%jv~T9K)&?zW@%z* zS?+pj+)2dY^Wucxx*f*FhlOhDq@@QSghSg6UzTr*|3wzyu#3oU^lyIKK~C_yb^^6) z+EW-S;YxSXck6)QBZlY=+)!1RFIH|>CX@?BYCCJ@Wx>=numQP2)fs+cZ1hSi>cZsu6Wr z!rKvP;uDTow#q^t8+TD&f*kssU6#y*5K>oSI5qwG&ECzmj!GhZrPi}X>`Ai+g^?)K zI+Qj5TWA+<5>ue1Ld9zWTk=-4a|YgOd@cZHCiXT#RQfKvB~*bq-m{M71{xH4AK%P& zlQftmfMReu+BB2oDa$P(yCnA~wINuDe`WibV#vucvgIoaP{{-`js>Z?x*su+=e#LC zfU{ghm}(49p%EOX#yX_JIE#e-bb8#=pO>aBcQH}F9FB_xEsz&KK5h()#5(`=6*k0D zeh(@jDw=h$;=R`>AFpQB85;8amg9&ZCLTFRZQCV#V`afeHc|qfVL2S6MxrqB7bFMn zKVr6iCG_rf5}micrDKc96HY9Px{Zl__uE6!QNf6Nx)>zI=$mSaq0#arb0HNYDD~0gcMY(UqkdM4ngNJd|Wq?~m zC5$)%@6Ox+6pA785Tgf*^#hC)V_9*>6!`i9UU|={0jVLjoPNNAmU!540BiZs!MmqJ zACqqBX*A(u)L^oYyLe~4q6M~o$D2-QwJq$sHWnz~-)zmYlnl8Q?B#P552RG3G zlr2ABiLa8rZP`g}pjYfyd=`;M`juE@RQC`a`*_2k5Xq^tt|yvsn$_ogj>mY4N2(U4 z7KHQ>Fs{y$-tWZJOBk1SfvOpS*V6i&Ew+XRPG81@n)0B0nd!fEknhttej%*icNFg5 z4LnVHVB538WTl|SfOG`qS1I}V`5b=rP2t9=dXN>@_g~8!W3wpFfl|r>)JaoOWU)%BTF=aTY(a!_ z!*FFYd(_ycf=;A8RizO-vIVmb3|}5l^1nHkJozGm{@-7R1L27D= z{=*Qm$*cXjUhlLt(q!z+6~FFJitDwlhZ@iH+<}>1AGrtlkx?V#Ow_i(72FZCHx(Zv zWtvV1yiOwgktouhjT$7uuy60d0YT#LX7BNUHP!_C%3?ogDg@EZI=c%1R?YAg86kEQ z53PxA(4tNfEuja!4@jnG9@%JPRf{2|WtPz0e*qAjFA0jzQcu7ZxI z6APL*2uIoz{^$K_nZf%#aOgW87Q%Y>bh)O{9gB2jU#DG)=!X6q+MlT>lF%Th+#OvD zsf^8~lYW+)VmJ|1!aSD|Oo;_eB{)aR_hIEF&P0^Q) zZu|sZEP`e203lpva&>na_ox9c(@tdtpf@sFND{K`t8i?biAkz3GEqgN`@C+n(msQ5 zOqJT}Q)F1bLqTAayf9f-VB3UkFk`XLyeV*`o1dz7FnW zLf{1V7Nkdv;NHf30_-_vbX4Fi&*PRkoHzyjJ}r@_5O;Cl{klVCQP>xGLZ;1Z-{pve zBO8@3l*l&1Lg>?gy(e@e+SI@?;G@rO4n$3h4xQ@-ZChJD>^%#%{Q(XL5D)3FFAx|Q zf;fSiBQjcdaHXJh1*+I6=@1{5IqyX(v-RkzWOsaB)XsDT%)Iiy2b36 z8qHZbt-8*U2X{cN;!$v`si>$RBJZ2wGRNA?;(GAtm#VpaR86GUWABo-6&oa+6B2De zYe6UVCy#|Qv4-HFQnhtw_Kg9p;x#c1pI$LCmLH$QZnXO0M`JYT07!%>8>1aLcc&7O zJ5gs#FccHIUFAj^XiCtFlmvQ_T8z)W?u)q9a`X)xu^+ES`7uS^80g4|jjt5k5=CJR zFs-8;OC38Wco9UBmx@X-C8bxtCW;g-0uP<7AqP5gSEY?_plovZPMpoWuypLrS5U>- zJ0y?PmAeAo@(a=cX=NcEES!=wr9z!cAp$5y8rF*Y&S|KoeZxbhK?D8}EiRY+w`fA; zz@+a>Dyr;qfVgLE?I(SKM+JidRbqY2GsWqim4fj0L`L-&kgy8(9~{w;jk}j6Y(uqS z;_<;TGtdtSVpQ0m?pklHdyD)HOM3ywGsdzm2lLzq8Cio4 z1=tsK-~FNy+@(WL>d-Y307k8j4T$UPj==W<393WM%RN9>gpk|#o-V(CTEHB}+BE+3 zI?N{yIfEy6eOfYAN?Og4j>V+VSfG^jwb3cnjLH$ml*NMFuu@+ch~P>ZLxbiL%coki z>z^|L>_wZLYIzhI?|iKY%IGg8w(NUvLw@wgZ5HNAJQa_+fjod*hi8qh%0*`grr$xR z?ZX?xvPk|dKQ`#!n0FT`LoYw!A-U*QWB*X@X2o1ceB+3Oz-$1`5~BucM8;>zE6G4u z^S}QL`gi`Kzwg4!?u8?CpA`ek5>90uGy#3#iJ|q107F)(6N+HEpR?V7o_mMrmN_(V z!FGhYNGNr^*2s@Hc!Z2qmx-Czb--$jmWp`WS%J3#;E=Ofy2*r93iAwQ~>p71@W z$uq1r5n}7J!?p&$(i{iYrr)d1PtZ3b0*rcq$Ft_G$QDNbezjSX#>R>>I3Q~*TK9q} zB?v*HzttU*Jq`f=ps`lLdkY?9ux;^;Y z%YFVcity2+C@hByy^}z;r6>g~d4W-6>h%44qp<&e16U!gO`=CI|MLwi=x=Zz&<@8A zospM!h76~1v7jNf2@2VI=vS^mFQ~8gbB6x&N?PE*QMDN|5oHsBw!9%cf%zJX|eP%Ljg1dXhCz!KtKyf$kJ!1(`X{l zvymIT_XoV%%B6qp(2E3*5bCnWLn|-998U#kdNptx+!zrV7Y+H(bL-G=LdkrDK5;yP zs@(+-V5j$-E>wVtK__Ut!PC_NFWJCW*Pa~xrLWQL#NGNIsyYlxhHfXUTrTuM-etZ{ z0lX6ju&PW07-U-cekX)Z#P&L(xKhExy+gcU$6Q)67Ui z+tT$Fj&P!X>j8SC89mqjN5NHqv4cm%YHyC4PHYnH-P|h%~-^L+s+aEO;P7A@E4XCf{PI-gqxUJy+h#=m9fiU} z4A7lLo>I;jE-$P22uMSOfnhVS^GnGC49+u`WBcZ_JE2#Ss&>IAB!jZvN)5zEM~?YL zuPFFoP7!z-FU%Cw1@v{N@Tf)vCVEB33^|a{Rw}CiJ*RJRbe>86B)x}@@ zN{;wQLH1KEI5A6|^9@|`WUr21HYM{`I2z1<{2zuqv1Ia=kA88w82PT}aW3{c7i zU+hXKe{YP`b;f1+{9A#)$p>(uEB_+N?hmj_UCVo*~tpHn;9|#siFfAp zZM1?A!-K^gFzQV^g4)P=jJ3chr|=Cd#|W1f1|G9+OMbcyCk&o%{GzS@G$@y8eSk-T zGF7-YP<%d6uySiXP@xfg6fy-tEMbJZ0%Dyb6N2&~p!$7Yxc7X&@Vqh*5`HID!5K2x z`wIM(P+^;12wGCDl_l7PmG803Cr&|!rB4T@oPmGP=nt?A!BOeyY-oGh_;`Yr{cik+ z&i#fiP@dn9k@Jy`Cv!!UWM`n`Qb0% zU_zn3&eR?X#Ji*|zI&E_*JoQsLIcb<-iG>u;kU5Q38sjyV`on$$!&6;{AW#8LLVfVA;0zq}T6pI*hI+sb%HuipuRa#SqhVA+6+ zeTF<4|99sPQ>C5VX#()z-d)i->_|r%pbRL*cxD>$!YK)632cq67;PXPrIw9pr=*AX zg5rus49V)pw84oBP0|Jniq+BM7tYXq0u1E+0^lMHrM&{AqC>3?EmZ=HE+|a@$9Z)) z23+f%Jf4UKxw?-8>9-(mC_>a1DiBm$)Z>diW8Qh+~gc+`bKp(mQQFYWo| zapvH-+zl@wt!Q1~C0JX@1Ktxe$yNtoH>q>4r!F6`Da8%!%{U$Qum||%ViRJtE5krg z8{9;>+W&T1FS@TTt^X8Ul`3t1>agIw1p6+C@#V`GZUU)m0W2Ls8|c0;MvORd0#;mX zSuE#r_a$o(2Xesyico1dlNXNu!dZaU+dDzg;Z3w;xaYnQYpwfAcNmv!rc0=>{RlNW z@tyvK%Z`{37V6U2QjtzxoLtJ}$QNGr4zdXUxAdcGp##21C`MM3fNHXQP?E9Wk>p7& zN}f95-$m-9?_LI%LAe?*#4ZC^q%#U^5w|23l-ALNTH)Z+90aH=gERw|eO$q`bT$?P1Bsi{@=$Uj^ol6fr3cxTJKGvi2T0+W)~ ztCa7)+V5cYr3oPfX1Md<9#U#pxxaITwnPV{Gi)}bk25tX9v7BDG*kETsq8El_bEd` zx7}coVpJ&*r|G|Ud+4{8AdyD%=4AjNs(AuGKpp}hhT?i)=~otTbM;rhLKi;w!OeBL zNd<%ZdLz@S)OYXOPhmJUW&JUmUe>3n_npF%$6^_hW^FH;Vo&M#`a-X!T%Rt!<10VU z>*P*;28?pFdUxyKkueqx<$Sd;dBfJhRE~cHazM>j-<|!tbB)4L9ZRhZTF9CS%IafL z{0@L48`*RV8q*6D%skKygLSf{hI@@EFE5- z64^@=`8kX0*V4TDcdyK&;zIxIf3yGz<@G+FL^c5EuW9)^sKqi`_0yMjWRiIq4)vNc zi3kx|_aZEZ6Br5D7fDgA+`RQ=n9!qeJ=VZ=I=;_2Ri>dQpA!qwFi?oqpeidl=GI7M zmg6+SQh-9Y)%NsDHTR$|97CZnc*0@9J&%mc#}Z!--`Q`{#}BqU`P%z#ja}gtuyAc; zWyKZ)_!a*jjx?prwGZwqVQT5t7SCI&ac4IjZysmm z`Eh(}>oX|oD`MSIhrA5Uz7I8NZGTroBJhN|aL_EiiTcS!d(f4Xl+Ukdz7NR0v}~h$>AgjZCxYZR*eX(Nn_v9l{@}eG2FT<_@o|+|V78ou1r+F+0^YVK1g2ZOv?5*w~Go!_W^Op^E!8R&nkK6mTM!#f*kSZE@iAR~* z?EVI4#_G)80&2VQd!4lovE8HdhN zqqLp4HIz}@@WH~iE_3$#dC}lf@Cbz3#Q6#iOiH&a2;_SX?%kvK1P)oBtaG0yUkcKS z;fB}a^2I=}CbmYhSq4Z(x&3Z!!P^`si^EWpSg5t-j$k>*q5i;LDZ?d4dySjn?dex> zw`74jwDVxCKcZNX8ByFRW`i_VdBWyrEhzehBm5M#FH*}t$f3Rl4yZ^P{;$kt5>-QCL`5Z_aXySN|{i3Ke2n(SeCs zP=hLe#~G-kBHN0M7$toSyx5bImAT)SZ?*z|!qh)!FP~ODTzZ4qt+&jV_S32nQuhJJ>v~|sHz~VV9@CHk_59YF_x*XZ3hSi> zPW#h&Ib2$SfFDVk4P#d<7h~x>nXq+MB#^0`Dk zGvbhuGB3;%crrFPoF9E zb&epuo^H+96Fi*9OtN9F^FOA*fAMTjRXQ0uHF5x8$`r8##0e=T2k#21q765GDdhjW zeMR_qM%Q@}4;PmN4>EoGrHsF^AxCJsZTZhG#Y&N3peyc$=i&IPl(EC%dcuhxG&~I& z?9>%b9EG`!M8SYkN?}MLQZZv2WDZ^R4`+X8n5TBPlD%Fw<_j=w!Af5LI}Iu{dD1=t z6-EhgOT}0N_D-?q4LVf_uze{2b+W>g;@d~u7OcsQVU^&@m}2|=^pw7TOj7sG+F$1B zM8^&$b)Pf(8xksYYRLy+ru);2Y$H*kC(Jt&(sN6Bqn|m1 z#3I-zM6cJfPOme(I1Y+e`2x@8Ikq&VOEomSw7k~7()KI|i@ACtBM8$4ymC$D!?%x&;?Mcml)o8CSFA1Dc)a88_ z(HDh!B`U_bO;U$fJMWN!r?aLbOQMXeGe#Kv6-y}L>b^9s>Gs()#0E9QHR}?qs$@p{3c!T zR$vWbBfpVhmsK9d8<6;X(n0S;{Zq=OKTxNOiz;+H=hkAiHWW{##Z1>#_iU!R`wEm3 zq9I&}^t1@25N=BRn8{=oRV33jD7_jTTLH&DG9|R>NwM}=mkCfAcfZmp$jKA=tY+$n zaB>^l{5g#^L!QApvG6R}%OQunsuw&=y-u{xR!oA(l6gR^nAX89A^T(MwppoZon(VD zMTT9@p3`qd%Whqx^|CP}E?Mv6I~Oqc%*iO&=s$}*;#Y5Lt*FB?_|4)Q~*# zQTNi3n>u9NQZDA{rf*;RVwV}eV^(;_QBytW#4UtqxwQH-M>PiO9jQfAj#FnG>W6?Y!ctD{Hc^J_ud$oXoyd>e( zO8TOX$dZNe$W|OvSPmM{Nc0HU9CdxU>+$U4CoH{0(3djCpFW7IRZ~}{a+3#79j8Q8 z6|p`NO<>_FdSUBaWG26=Ajjj3Pn>DUkwqF_s`DD#v(@w0<>1^)J%D=IN3AET=}H!p z_QE8j)eO2)m4T0F1PY0Iu2vQ`1e^9!L#U-kU7Hvxske%|F0kgX(mXa7J+fUi^slI! z6m0^+X~!=OY=A(>JAhqKa;{`E_-ws7n&WDCUV!*;Av%4tPaZMwr{pOu%hR`3Da2K! z3oQ+~BA@aFQk$+XsW+;v+X+(EFnD>3J5I>2&#)e>MufI_sOk5K+1$G4bqck;PTicf zz$BGjD-=FV33CAIgoa0e+21{rh6n3caAB*QJJFVI^zWpHYY`Z%1(rgJA_@?$95X$Y zs1kMAtNYt8etdfbxeOFp8yQs_+|Jm=)sAMngV7Xemi-Vn2q+Tg91Y+E*10l0XnmK;@Wu@EsyrM#-OjN{|W=sgu zZXf}tCOX+1p#H459i^hL#g<&&CjH~+UMwEP6*eXY2GWY!NHxU|?6&C>d#EQ54Gq%< z{;26Iq{@@JzBzPoBbZ7`3`|`TDI`c&OR>VXOpWLONwrKHnR%wUNMw|30_Z?tO#|bc zr74_g!$#vb#PGlqtx;1=h$Jzoo`F}nz^lr0Lt*W!mstiarBZxGdf2$D3kF})f?qNB zdrXaF{^ZA{I}ny2B9&-SF_Y6-6Wctmx3ZK)A*Xecem2(=!{2`hw}Jl!2__2Zin9$m9JY&-22Y9hBAYOWl$d z#Uez6g%|-R;)n2$XWR4oh=G8xV`2`j<1}GsTP8d8T%12VpH%OP@;lV*eqvgTT%DB} z4)_Q<{r8XoocUl)E~ja*k(AYpRqVwn(GZQ^s7;#QdHomjyT*wxacn4awDq~x<=QtL z-|BHI9nY0ymka5Y7tm;%# zDtzq7n^E2bom;d<+X)SKo^&ruVCGB5d}@FDTF`grk#&kl)3Ca#jibRLfp|}$h#}!F z-%oiLfv}o4Pq#zon@R%eea~SNd!bCtt)HG5dGuWiSaH$?U3{R_FV_k+O zy_gsCE@&TRo8J3-;Oux4=ad5tGGADJUimT@fsf1l6r zPSb`4(Ts}wi5-(0|9%-_oB2Ll0v?E4VYtp0|MZ?}ZBa`#gAN|IB8GCA!uhJYm)@8{ zRm6+i>ILH5^iv%p`96MvW$Ld-oH)0-#GMv6x2$!T5-aG)$XL>Z&GFwDSNsCa=h1$b z+l4ni_6^^HtUBY_2l3O`GTW8g1w%}@#JDv5k2HlOaBA2_VcTo=mCA`NlFt3<^WYex zL!gdVaF?CdH7$RcJ?vg7SZQ1DmdhMz~1?mRGx2=sQr}xRWq+1fwF3SL2u*o@|DQR zjp|eK?x$a!af82SHFHE3m8&xZ7rlLSLDQHE-RoE#5w7o9bToK-*WTrB;=8?!&Y?D{ z{V(&+#^}So2C}`okm#LAwMC0>7YB1ry>%KtupPXL!UGCkmCjULxHgxGUAILX z$D6hAvH6?5^zkUUi&^FU1oyOyC+*h9%Ry$iZL-`+!$rip4byv4V?&4KL*xT^Rt1J8 za>p#Z3x2a~y)%cC?Wd)cS9^n1g>Oux15e2Rvbfo74YEN9JL2o2UvpZ0e`d&aLZq?5 zhtna7`}C{pZvW>$(lO8}0u=nG)46^1yTRV$7vBdV3GuhME+$H}Q;grxsx585RlebI zI3xKfM6BZ!KE$dGVMun&izQQ+(YD$<322dsJDBW3S{7PnCC5 zni;Mk%3-V85aNfBPZ!JlWcK-M9nTFa0s?O2{IOl+f(V~QRm;yF2bsuL$XbpT7|q1Q z$Mg_uUVoY|yD1J{n>#QHZ%}*5eW${>;TzN9`1j$M+>v+vQV(}ByIOWuI3Ebo&0hQK zH3kYWW2e91hX(4BT(xKtR?mAWG;R#owBE=(q?o~ft#$iScK`Eh$@fnEsgNR8Jwxor zA8=JqiG}NMC}d=Ci|U!+gK+hTjHM5w<+_-95Kb(M#1M1RZc4dX#3HTL%I?%Nt6S0U zE6kJL^zAFRkuW%$dW-A8k$Kg8T-2>L?hnBNYir4` z1#2EY3plgn0waast@z7c=ZFSe=674igf5aVC@^yjJHJLvW;GpP{cO!(`gCO9ox$?J zL*$Y~Ht=nQ>%Oq z>q6+CUSt@Y9=|CLD-~z@NEE!=7E*?}v+`B=Oz`n#_o!Uz5c#8tI;WC(S1$75Nc>gsI zeEqq0g87*G_`yAoi;5a@47}So@b4K&_x08GAAeQ^OYc9HBHb#P=P>cLBi?kO4KZJd zQF8FdZoPSdBU&*ts<1CvW9qDLcy~>{I-r97{Vckr)yBIyOZx6#CMxn;4}+PR0?pCi zQ{T@?unZ4Muu}bk@nwH>>0wx_OnmI8P$oGN4ePS};NmUS4J;D6_m?p2@vnUB9_L%e z1NoCy(6Zx61BS=(6I7GquDzIVUmO77B2;rI1}Y?DbBJ zf9s@b(zNa#>iMC+XT*E9l?SU{RF%NiR@ZJ)?Vf8iBjNq}jFGSZ4b|Es_Z)>$ZkvsE z{8O7%_PmR-CTidML@#>A(#VVD^s4V`{T|O>ds|j7`%>Nxb$avd-N~%PToE(USRkT? zg0(1RQj!f`+TG`k@q)ApT#na3&6O7(JdMQQ4}AyMcZO|)S38J%%|f>H=n8Co=N}t9 zPoA8{rwv9o^+im|WuJt`%r+$(YA1crF--x|1T z-;({-sI*}b@f!4GU&?ql7pb@<(TW-#lt`ERY4`*Age|vwu3scj;Q8rD@)!<*bgGHR zW~j-T?)Ht(u^mE<39*T1lgPNXKX%UY_=B>;?;G=#r2XC2uLEhbRC+>55uU1^@1EQ! zua8jQ;@<9yV9z&tA)xe1s_c&6j`TD9O_vy*h_`1KDxBspo?@E<2)#PJ;%^md%M$gi zEjicTAG1cuA_kXR)G-U+L?k91*SRs_dF?BA7d~@`Quv2WdJIwKhxRY0A80KNj`p%HsLeY~kb zKOkOZ3O)TpRH|8Ud+R=fY6rio`^u72zG|7j4#+s z{n)dtsO`5nJ+tB-5b5|xn%lv_g=7sEC3$>~tqzgA1|-55rDFn3xo1BOKW64iUYwuo z;;V>?@)LZnjt+hpMVziK*7_kYTS;Q(`wv>-?PX!U#lh5nNw5#rvVD2*G2l|dxRidU3o zBU8&0hU}wa1{d2+&$AOu!8^(^0olsBROKNMf9eEArVKI->#a)jh(S(V>-SFRe~0S_ z`Jic%x8{17lV!r?hP+<_oVp?8YPytEHq=FvGj5c7^=j^LZNp`I!g`J1wT-g_f zWDS039vpAI7EEGavhw$LvHcl>S{h8I)+>H{t;^e84)=={|K2_=SM?t;X{P-9`b7G9subyWCyIauV>4$5n4zlCj?o7bAjI9j%OCH| z5x;&=OZof+M8TD|*7pE7j6!{bMri|hW>VVOuk|IszC7Nizj#J3o?#ueq}gh7bx(Km(iQ<$}|QPQ~ZgVooy?c9CFAAMYt9rLMfYq>82 za-E`800`gyTJ?7a+@l{0VzOJDM~h8Qml@%3I5s^t0X6K9E@j=Ttx`Vw$?)@PdRw6S zAn01G7$H&J6wu;T#>OQLH1JdCENG~+B2{L0-viDbooB#EhC(5FJ?9|s$rIUyJ-=OA z^F!&2<&VAsXb2a-NY*q;{iixF-$S~0)^17ea!v;urz5-gEd}$80vcIs6L*tkJZx59 z)UYgTF5&dXoC#%17-CPq^_Hm)3cuG9)=$zs->>6w82Pw8{C?k5(O0%|N~u&RHxu_& z<`-}_JAxi+My{lAn4EV@(+_& zqB%wav8fNbb&L9$u$d5NjK#%sb)MGrZRD}T@0p4fK4FA~{3*C1ei3A7dD*_E6b;ce z5VoR>H}$;LL;34d>{Hr8m2GK+Q>iH@?#eGdj>!fUp-cbkcl(%^MXq9CQ~f~8{?m~H zalgWP1CjN~A`ZauzM%YZiz}r(AGX}|Ft0&L8Fx}jk3`Qdo9*w6m^VGlig&YUBQPY~ zs#)MCs_<2Oy4Lf4sCcjL#`-*|M9_0eD~lCV#@8ojLqNHT@>rexifa$$L)8-5nx(e4~t}QB_ndPY+RJrAHtxe@k76 zEWvq@R6sxyn_Cvxgi&E-*ASKeOhz4;rhL4bMX$r$DWz2|>o@^Z_?5Mu zhZTiPZ;pzLTuqSp23=34f{@}Q#(BL2+5uSk=0eKVaQ*q|DFkC6=q|c)J!bDKNMf>o zc$P)*@;Qmb?nY>D7;+zsV^Tck=?c`X6nT%)=#Q0WDCwc%hqjve916p^61BJGQhZlO zhHq)kMXnFeFc#Gc93-DMv`PCNW?5K~%CMJN0Zqk^%$*)hS6c+9$Exh31xia%!Esgr z$*k^A3xC{uFD%BzWfGxNjXzdjI?4#`SG?1zj2M%K@_-TcK8}N|d%YZ*-@(Wq)-r%# zOWMpf{E(Aq77ot4MV|E*k#p+T-)`I_O?0r+Cx64ZW0;7naIs%}kuE0DSP#s1_`UiD z=lRop;ns&z5U|fiITXWXt(Wk#rq@Jo7(Z{>5&Vc2STS_@LVcN^&GUULt}Gr6WsVZ(8omqcD#! z{ZTG9Jo!;Ej_O1HTN`{I7t5$^vcBlKZ*MM0UIgC5qY<{((6y=+;$cFxp&H*XWG}tM z(PqTD|L*!a+j+%sYgIf&wLXTvh7PuVqI9J{Jz zAF`EdWMia0U{Ig%K6-D}HT-3I?RPk*)^rYc(uF?Tt zpb+-@6D=|KpIaFN9fK`~Y@=wtlX#TF42mU9g5!U;tTBA>;*`@TDZ_GzN zuN7IP+$E(V<*AA_Ou@jwAXv)?eznc6EnG>jH)w0UDC){qpYE!6o9$Zk!9yTuz#wRJ ztqqz%dUqESb(vzJ< zZTkrJ3qZHW|80{Ad=9#ulro`CZ5RV;(Lx;$Z@XuG-rW>)F7bs&v+8)+}Mm1qXY({t>F0t%yj6DO=mhOhLdEaxc>-+vUX6D(?-uu4q-&zawOXR?vzv^&JwshO_vpmB~u8T2N_kcHW#`Oru!g(mr!)7 zRGSWG9bAHBo#fChBjoUl=qbC5*|j1=C@?Sw2wX>=1{1yHOLo_akWi?TjXQrMOCKo` z4&YD;0N;-QiyD&qJ0EaMHT&`mAF<4onJUVWy~#S!4Dd_FdSoyj;=@%w1Q!2F{U{oq zp7w6)SIm0-Zb#}`(>28+71$XP+CKe~4^R>@w|aTOtL~2cNDqrUPti5lCA*Cc;hIXm z^#OnV?73v>{sXSJvDI7AD}MFo4}jCcx{ZwXdo{X8O&1(pzYr?G$enLcaB*0 z5x?hi)V>QhkqKS!)WJyGYGxia7{4?t`l0?3E(KZL8mxpYJ+PO&tEe=&W$o9mANLy46UF*9PLy zlTq}R6HB_!2dbCkkWnTfxlutcy}AyZE#?1W#}A}`q!}FLj)Ny;Or&CU4=qr>`F3ln z0jUo83|^2gQmuT<$=CXpBiR8orreQmyzJznV_0rI-Bspv*74IEepbywP(uc1a>LDa z5QUUKfZbL9WTW?8%l2OkQ@qixwgVRZNKt6pR;&}d3tegRSO zGS_h;$W(&vJ~!;ZACS`g86zKpo!9)2n8S}3j29{NJJyCWs3WOGi5ZIF-PkV1diZq6 znNv{RlCeB&jv^gV9v%1ihE!?nN)S>`-DqT77joHuTNQXbkYi8?Hwi8q06>M}E?$bA zT3+1wA*W9W*Grl3Gc-@KdM^#D`iu<)GAb(T$)Kn$DSJ`NQTRcmaXxOrfg9hY@ao2C za|%=qwmp$mPlLR>ZpK?U+5S}7n8kEp<%&&lK^GFwO6T{;LLy?J!D}bLc6pP_gs@g= z5S!n6r=42Bn_%Itir`q=UNRb~0!rSKISm&M%Yf+vA36ns*u+ zS{U&Va=C@&s+zHqA@Z<+$+?NhsFle4Y&DFyjs)+-;;fuA-^sZMq)LfwG8Y-WZ;nl- zVww1n0{-(9nNgcC1~=?c#9Pvg=O1km90L`?bGvev)cEZ#nuG zx)prEY~2vliR~Tn0p0~Ea(~3{IAe-{0wZLKbkPhX*dgR%9Ma&oooFyGW^7CBn3-!?oY`*|Cr)AbjK%x|QUQ`u>r@oND#?8nh68*+vy zv;}H?orqRqciu*YeO;WUE)k*o^*qSm#<1g>5{C{s876h1qU}!{HSYVXVKViKyVb=~ zT09naKBOTVOm#5FPqxNJZ}zaORa&*Dr($ZQN$HuvjoS5m#W14}&3nGldB_$zjxTxj zN;hLhqtAuL45k|Y-R4|y0-wrcvvn=Zhx2ss(@NjM!h&sYPt_Q9thiF0_qkwj=wwIa znxDoZDXO%)Z|wMM#R#FI0bs?X_KIai;oFWE{k9rJ`IA zKv8QpVqPxjm%K%}NAPwaNm01Oqfn8~0?bkLuPIeC9x=@C9(2up9#i5&5u7j+LYdqK zerW;a`kyngEu(nK&XPm%wUlX*6l?>mrq2<#<0;|t_MHkGT5mq}Hp>(6rF3z~=fW$( zUdd_3$_J+Ub6r0=@wywH3d57Vr%3UlW9l);Ccc21zne4tOpYa;9En_EiN|s^K`cWY z{F<9A53@LA3%WmfB1??4o$vc_Zak?GM$OSv=|5k3A<{MQ+NayDDlls4>h@yRr0&4A zq$bPaU^MA;S@!H2ZzR9)$!P#+M*wwo`mQ zxSljNpcx?P`;^?`OV4$%qGcN*Mm9qr<%gW8tXB9)x)<3-z^yB1^*_G}DKrbYM}ZbT zbXTQN$qsjvHLl||(G=}s+18RBayZ4`=NR^WDv!78F||)`E8CBsXqd}=d2g}zrd~23 zW1zrer6&BHV&D_+dKEl&Zhp~EsYLcdMLXr;Vg-t8F^Hed2DQ>KyV_*gfGC_D<{e~X zS%i65_vFo9nU!I_k4Deu-mB4jxZ#Hr5Z7Ms{+CJ3MxWQag69lft-B~4IkkDpngznXU0`qKsGEn(dQFNYad6?!rz za5Wy8sv?K8cNS}Kr`>3rf-pE7x#+%dr0A&K{u4o7F?VN*DHO@HUf811cnJfY*g^}C zer7X3pXHU~tfwI5aL#z{;jE`zZaF;${5rGi^WcHU2C>Yrhqdf&L5eziU$=Uk-$e&}*QF(+xdEB#kTC z?oI6oLmTa#!eXQgcA8v|$WvF+gsJN9oWm^tY?o_#JJG0=DSL2>2%n0!k|1_FU#G_O zL>LNwE3Re8netl;Mn)k+T96tuq+$wB48btmsHe3aEGj<@G;Nc??+e~{lwp}nM@$0m z!Z_RMZ}v?LZ<~OR9!89nLKc}hkXt;y`1{`S2j0hiDYWX^iVhDl%87z9qaF5sTb=;&>pO8a+OSl!)zv+EB1RPqOr^QAg~Ar3yVO`qgu`w?7gueYt>p zInZB|;4&h!Aa0|`v$TnfS}tgi{ut%u2V$g2ifIPe=(XXnIIn=3oquNEaoA#GWN~BU zrN~hWGTht(Jt^t~hZ+GUby{|1{RCER$-mD7cg+jEr3)D3s%tEc=JRBiL_}&j()vp6 zj2zyrn_mlgPVkwpv2Z_+_&b)s!BwfZ)}Jh;BrE-AB`MiLpCPk0@1EGkE8fS_gQ)bXG7l4Q;YlgtnI^cDDzp^TY37}lO(=0hmDC6l6z*fGS1iRUhhQP@P+*~uERXpdKK3MuYV<3 zWNcy)sG_BT&zO`uP-=^V_&hv(d|G>A95PN+cw6v^rPb})43AX)%d&_5Tkt^NIdzbE zL2yBvthEn7-nakI=;L!O30BdS_Xo&s_m{;wc^@9O5mHgq=4~m4C1ZU7p$6HOF(*ap z1tH%3?<{}n+nu`iOy9V|Vc4Nz1P15xdNFEfFYyqo606#R2sUbFt-%xnmFsokU+trqk?i|+b4R3?t45uR%T!883S9t@oZQj=U9(v@SMZ4(=evKO zgOZW5x}N%0o9AHjJIsgAir{i3y0y>2N~eP~bflihPRg~P-!AId4JekW7i$z@>xRh- z_117uHM!I{rcIN^4sj*IyX7@An9Tq*&1x;ts8#y=F8#bo1|M-CT{H52oPf9&Uvrr7 zzK4-s*)Y_-vGK5qYZ!pk#gOriYuToqk5XW>CMuj=0ONnrUK;bcSn*iGi5DW zD7r2RDcjD7LF-8=$9^7(IzrS*0fS!@s^x{yAhG4Uj!9EV&d`qd@mXSafZ~MI1C8IH zSOET}^r!cA+MO(%_?ZrBv4#d+GVea;blU0dawjM8xrZERTXEJ_vUk!?rOAF}b0e@!Wunc#le(|2vLSGkzD@XS| zeK%R^7bh>pT_sZFk&@C{?^L}K>h3Q&FfO3-Y-c#j|20d5L8z}&Iyc!f?Jv3FkJlVD zw8@NpPWU1L{b~f5VuT%cwUVB*_9ykk=Wq1e{6m`Zwpbms)J*eq22bl-;1vlq0Ot;8 zxjKH0*!U2=T8B}piUypTS7)U&TGH%jCe3r#beDFX*f6^4eg>8RGS22-JOK%a()c?~ zz~N4&L+E>aNN+lJbIAi^u^Pwc;ldel3@`2W*M`6ssz6G@AfL zrjfJ~LB_@KeLpXzQ}obhh$nNV*5$zH-(F4;K4Z*-6t);4@VZAM4ZM z)9~2-3KluWhj|vgWS;AJ!3F%;9Rf2g1L3jE%>G{h`L7q|@30hrB3DfBsaC|zR226h(zeEE*XuvTI8zeYjVN0U|CMSY)bknAle<->2d` zQ*mMIZK0AW4r=k)t~56*uT5yk2W3Pg!ir9zx+!fh-v3fLC$hgi-pcta84yZpGxJUE zn*^s=jJShLN~{+UxiQeiss;bKj80Nhg-2)YQB^U(#wWQLkQ%(xY*;zIF&?Ck;yEH} zEUi;DMr7X?VXA%4);0zWl}?Xpau+v@FNonqO;qjxb$@~i5hzE2TM zH&Ws2&oE$`DiIUo0+tiHDQld2wH#FZ_A8ZEY12c%W?7QZ(MV45!*7;y4>tZ1n@HfS z)RWdS_u&n@uBNyYu8^ff9-ER2!<~?9NEVAqZL6}@j<+*J;KwmO zHtq5bp47`7d>a)AKO3_zcXwRz!yH9>YwmZ(`(zi`!7xpx7U_@3A+oFEXlTqo9E%|} z!>I9Vaqs=%K{r*8i!evELM7uWO&_u@nq zROj8fr(QAR?0gu#tVDDd&M|t-A=s=E1ky1ie+HXE;jrT&);A5@8(2Miv@tj-KS$jH zw%~H{cI-qk5Ao$|IuAHdn0qO2a9yX1$_nTu^hhESF)2K8H(9#9!aO127;%&4s)9=B zZCM$V|6!7%R1Q~%1;;SOQe|eBe}8FXgoQO_p-zmXTsEK(N5>Oy9B%5$b=0+to&!VW z{$kCN6g|cmQSEcD%oK|-8DQ01s$(u~xk{B~z~ITg>1|>CSaFAlIi7ntFFTFRK;)|R zc&-U8k^y;)Yp8^9=6d+WaUfZp9YBf-)Vx=z0?^sVSqkK~6TYv9dPhYv%${-gE|Mg( zgv{KPi~YV8dDDFN##EX-4))jrFGer47K*6%f?L3u;C^uH<7X!9xaecU#4%_Wk)%r$D+Euh?J*}mnt^Y0ZE-PucObS&uy{cfkJf+qly}W2I{VmqK)V| z%OmK043(sZN!ILx2?44ImYRaa#T}oKpB|{J9B~h=?sYt`N_Byc zJ342!(qO$Ll#`vN>b5C`T6vqd;p59{=N1(uIHvs76=dUAx||)!Rn>>Ev*XN~4^t{! zean9%ZNT$9f6}+u)QGg_g$qnPTPe>OTwiG0QftxyLf9+jm_I2eNQKX@?qWNazn1(< zKs`S9a8T@p$3oTXnZTxTnipNZE=>nVvpEt&dRv@5rCibY z-W9s@TTbzfBv0458pn$lub7`S(2qDid&B79@>)l<>f>-uc<$u%nEsgyLbgq~(=l#MTm$W- zh4R%SBW3s5&d8}ZI31^%-Tv4EQWhaF3D?=N^k*L@Zfntarl#x)Zj|2FO;x68zIEi!4G6_U!UtG!$Bd>@LjR>U4dQAJ%N zWgsYr{U%zcea5wLod^&;XHsn}5ZF>PKJm_ERwt#aU@1<=?iISidy`#9ni?YUwfET- zXgBCdZtX;!6IXY0C$`r$2dGrio%KeSPAAEeY&<%xx+nn-rj;d>y3CEsw~QPnx9wM| z${otOSZsHrSJ9AsbWxBr72#8hkKmUm4$wePzE153M~(2NOEzA_PN_Yh#-O~p3G=+M z;FQmJ5hI3vwAcy+To`I{V=DNH_%BnEzEG>HKAG@R|F^)Mq+R3z5W@;LWq63Y)iZP) z&-6zRQ!qF_`Q(I@;C4i$B-Yk$MLaTk$6?N0`RUUqlQQTFvT97;b|KzB9KDPE74Nhg zkDWyKcCQR#B)keFJ4J|gG1f3e*EdlX*JBY-F&VG<*~d+xPxhS<7K+&X1~L4Xm(kmD zF^s_(MXOPgsxdM#(kUS^8l&Gd@We@Mj3$~9ZCH5w_O43GarxQJ6cQv`! zTsTsYGV!0D=^xJ?R?*9HT=NoKajLRQ2sUQdx!wKz=-4@T^4Fm|iNTG>)J`z{%N;$; z#BuZ&qcR@B8R6kf^t=y9_w-TjbK#U#OPcdUjh(sAf0L)Mke8Q=G>LFv>>WdoTXKN= zuxg-dfAck!rIACCV=m%j9vjYeN^*Mx%Y;^yl%&18^zPpu8E5UbuhM3rWy~r~5)4H7xtG#Y3Mq_)=S`(ib_T zAOzXOHZRC+1o}dy--Rd_CnDvOc*-KjS{{~?Wt3#Rv_`6g^7g_o(y^J0<>}-8W@59_ zVdR!fy*#Oasz6+LQS{{?^(KbVb`Xrn>dT*V>8ttEDk|vI1&?rTPwwHQ>6&*z$@RPFFRoSvs+aZ6fuRu!TPbXjC z)@{B@!|m~^L=Pzs`?kH4m9F;hl2($U(S{;|nfxCjMRJWSnhicV0d=3xQimn_iahN` z`F#Uh`;^Kl=wX#99nal3umO-FJqEIA?Np&(^CIC}?&L7N8bMNbrG>U?LEMA$UUk&V zi^cf4WR*rek+{JODH|7k3+3c6gdWc5l+bC_$6H@F)(9?IY07|7D=qND zsX%^3h<%|&>Lj_)eEQ@?i-UeJvfNLNZNg{QI*!elKDv|L<2vK}>vX4B`Q=BN$wx9Z z_Q1vw&U(V}7jO`Q$uQD=D!X(RT zt3MIVPV6&{;n8=SoD9N|AWwqdY%ukr{cs;;3&=||d#}LUIt&;R(VEJ4uZHSXTgCTG z^ra#PnMZ3sADP@rBmhBPFjE1gfMZ(}Od^$sa3T^s`NaniIPk)*9tk8^f3P&owfOXo zj$*;rhx=*tVO}D3ecqh-WA5q#p-r)EoW0-`51Pp*-bGNefs~T~)Sqr=rscxQ(|1R; zY2U{u4h&ZZ;tAQBk?{*&K908{IR0$)yWZxXgLJ<5IPzxN7}_&~53W_AFQup$_6d0n zh%Q4Qq5>%vspt1l*TKdJoj5j&M&V*5{io1S2~Y&ufdIh*X(JKu+a}pZaEv7|t5Ioi zw8Vb$0vp5`66QA+-#73u9|Vhip+jP`6A)j#o`cTfE2WA*a4Auk7=9B{K=J7}TEz-z z!=c_j!lmi}rVc7C9)jIIrz^C&Iw(qFMY7Ot|b>#-lB}}T)F zxpAva!ec(GZ73p`4j7*rlLrC87=6#OO#Asyw$@Bw{eIt%kdI^XUHTD}11{`-+yFMY zhn>347Q7=xJTD*dCsmhhIDr6Li-i_pfF}6> zkuMhYI~|SfnQDdcWz(6hiq;75q_v|o^0PPoRRAw0H9Bt6+LPGpRll3+`er#6#b(fg zI{OkI!5nZMW=0KSR1rX_@OUWsGZw)@>tP=Y$|GXt&<<0O#wi;wFV$bY;#roi3UMJv z0S$FGcRo91e&|o$pu?;Xnf50hkiEHj|H?{s6d<+y{u_Z zHDj8ms}AsV@ZWR;3-wMu zhu-c~u!QuvYK-@$Kg$_$f6*k;c!n3Z;!qC{g$;U@}cY9T#vI|CyY}wDp6yr%uOAP=xqPh+e8S3m;F`2c>S^G6>Z5Ezu zh{VWBzne^PNqO>64x~u_8p~4%>gkcq64{+^78K7l$46{Vz5E!(P;%YGfMK$fIxrBh zW51mWV7q!$*Edy00ZBp3_xrgs?II|>rp*xWKv%8{TK4c|im`zY;$Zo(P!}UvHXb8Y zn339FOz_Oq%bCEzOmgRdBU;86$0XSz^0dlm|Iz-LU8;5hF9E_CliYa6-PaldTHU(3 z1Iy{oGKf0cCB(gN6O&O(OA8HIqt}hVJz2M(7t9pj2r`&*KtGu6tD&?L(hEN45bC)M>VkAc~g<9)Oxsv#Fs~D4TXqlpntad%P^+ zg9T>Bafv+7wNH=C0(%QMz|Qaxmzn#SZEw+&@nrjg^;!SlY``|M+S?g!!i$ zBw18RYTU`9Z`#Nhov!o}WR)H`RQ;zt%-f%)gRMAl6VX@c)mo>i5EmVe3`O0J1w!^+6wqnYT74@Z zNr@mH+T0_)_&^zuCy0mGlmM)VTr30n!~g{>*RAI#8-62*0Mlg(2K6KU1EYlJZA_30Mhg(q|95mQBIFS_meuafhAR`F@6}*Iv=Bk%mCqH@EOa4-P+8Gc^ zl44}c(Wwf5$A{Pm!g9jwh0O<2=uu|&@gr9z=JaH(EJyn?d9l+a944r^PRy>-&a4)SxyiZ64ZZ!jmdn{Q}E0Ck%8@aC*;uu`M znXdj*rRbN8l7TaRdnUFB4j0`?+{3EFtV0n*4m@5G8t@mau>~0Z(hHomZ$C%YO6CXV=tWk0-v?-cz7K%M4mJQ<84iM zqUgL5W2Z%X=2D_@e)fxc62qFV~=qibAv^Q6jQQBC12$iRoaXf*W7>lU5E=V z>>78ITjYU-Mjme;aUq8H0=CN`ocL<6ynU|B-`Bit{5j)qqnD&|5iPVDI6KeMr6D_M z6{Q(-o`uKlBk4|qsr>%_k8ndaoWdm6H#m)!M6w`W`Xz8TkJpp$otG1++bf<#EheFe-PumT!KJxycl0|^{2D8IhozfCHoD=x&bPEp=Sg*k?1eZpI zm*Vd*6{#EU1GZRxmy|~w&RJkmZVxDtq=EX+xD(a7xV9_T&r(|{7!JQzee?0cR zwW?UFaszSXC=V$p_-w_H;^v&jGDIoCFDRSQW3crb34C|CYL-{x8(sF968?z(jq@g( zRYqox|I}u0dGm2)5FhL-LA9mZ>_@5!z(m?-bWgdwo*3oSTDDm);-g7wncsZ@ zs2s;gdmTtn{nI2jG3avs9yDMrwjooue zCs)?+V!fin^=j=SlU5cd%2>Yt=^b0TsK5TsXXBATVcBTTyw0CL_fS&8-E_g+ZT6MM zfDCy5_R&%2(xDN0b0p0o6oKPK=~t6_7@^XySB!l{9ai<4J`y^-c-&UDAG$WRLbiXq zMLC?Q##?K^TO7bmVzfx65WFb1+lb^b3o561F>uouss!hH82s4 ztMFCT8IIh|#MEzfLNoB5C&Ze(HRgeNT=U`B#1Dp^Tn*BOS5z$BJwzF!<@^JCo-P_1 zW7?-%-4p`VK}&SIzj3O7tH`!k+YZLkfcsPwo$)TJx)3q`1!Ci%GH9yGY5`6m(7J?H zJ^cUa)*SF}b&84Ifb(92h=%i?jN$%}E=Rt@y(&Pb$6qlGSxd8IV%rmwk^lL2?eW2+b9 zg4hfM z2}oE!eX1EcQ&tvx=;^7#r-fe2MJ9ZDv`z#2_ARO|oP7MDR7hfbMQm60;b$HWi*e;1 zvN#ftouIu-ns=wEznaX@)~>6GfNJS`t+3!?d;QpXr6rc*<`pHUc`&ZeH}h^{?CnXV z>$_P1G!O24-Z=S9+JAc?b4)Fw1pN_Jl6WvXeh|12Nh#oC_v3PT-&fe;-142{7BAt~ zf{52M;!LM zAaQx{=G~|8P_e=O0M()m8AzWFYLVLO{5pOrr!6;G<~pB7pu_kpak5P9zHfhe?6(n( z?L^*)bKzjx*1bm=RnRNzcGphgm11e$-~I=~~p&M`jgeag^c^G4Gd83S_2nmXcCM$xznZ<$l7H+_Z#&pJ{q2`W90bXcdq89)&&lXX&pglw9`od(Z>@_`y-aU@nH z>#uIo^$(R)AvYBv_f<7^@=2G+cUPt&hihz~TGDi2$}QI$lip01A1#elFxk*Dq%xGs zI`(R9^t-R(L&#>X%C%%vYqd*GH)bx4v7g*wjpi$`#Yx|azLc%dZ=v~n$A04MasEm> z);N0Edh`&>V>F*`O(}z^XEWiXr1TZb&Kwm^ML8tv2kaH)e{G-=ynQ|dupjXs4zhct zJU+(4p5-~;-?FH@{AUo!UVo_~CDolZrTm0r{qto{0>?jhSH^7Cze6Rjq=)G2$QkqR z?3P4QTO75b5DAfwo_DUKSF7?VdxW4lvnH|`bOD<-&}~utXb6EfS$AilaAK=@T zG|1y;_i*pI=kE}y*PJ%_--q@m!=b1d!U33?^BvthEN|zFciK%fZbiEpw`#5Kh+dTI zSlP2$j+x4`TMt$_2MyUC{=x%rQ!|IA_rFbA8Zz2NJHXAE6yh^X8Oc9$)IubsdKPX7 z)?M{zif~FlD-v(WkW&1)8r$^Myun=_)Tj(C5OIioyIliYmbL2S; z1BgAGXV|LZs|$IEy7%ATUw?U9twOOEINu%;;2^F(w05#Uh=MT4!}Jz+sUI=eBp zXN4nY?79isiTUidE7c%rw-G9o|K7WNSAu+i8u5n~9YuqD6MeqHC%X+hU5Ow;`%1O@ zUuL7Hes-*e%cD>h!iAoX*I|8e`t_H`K7PwB+;Esk{gF{h_+_pg(&<1MK0-Jvoy4LFxDLLu`1pYb~<;yD90&@m#nq1?H)c+G{2)bPYB< z>ZQqPK+UZ16%y+=5rh}k6|i9|TtZ{&n$_1IvZf0X9GNNNn`KW`=yN1F$55mTc#&pI z76oc6@MgOdalXFneve0Ewwd5ChTY(9 zC?e)N7+qj-o`ZDBzYRAoBUSh6@fene3NSp7OG_osUu5|`rfD?%2u z*xvYv8+aR9+zzDdEqjmMwUs_ymi&i2L4LxD=4A@EAM{Yr@MxwLRnEi^K4OXWiq2ex zDs1D9D4{Hx(_rQY8$(~=DV9nEj;n35jl>1`# z-$|2D2$Wog)bsG1!_DjBslXXkSbL{DW^BgNO~X=mGL}?sZFor{>~KlK<5wwt1oiFI z`#PHsNr$Sj-al|9E<^r7QH`WM_)rn?C!w~>{juc6KKe_Y#dW&{MhNcwR^tfMwLC^u z7iXBOq9UQ&W2$h8E9b1e;k)mmZB?7k&B_JCUhENm>=ik3L~KB)i$O1&TG$OWB^(_U zm4->yATJoXe;!9HyLTt_2;>*(F%&sZtOhqPpXFaBKoNt9%uDf&LlcOYJj0s?qZ#Uw z4s-4S&RFKEDh`*!bBycj8v~c|xpyO~2Z@m_UTl-Wp`lWWj^AVv70=%P>}C`Z%|E>h z&*0!C-f_tDLupI>HvdW^PSr+Nm52I%X7&WSZ-MARBNfcesLL~b>(lAr-TCQ(!Sc|@ zMpW_ZFWPTB4m1r``r~Dgw7f)ab9{=fBls0YbNjbR*H@h1Y+m6`+ppk@Fmz@82>;># zZXkFg1EHQHLa9qDv|SGSlSaqe-~Ul&crA@KV!QqQY+uk{DT^2^n2FjY@Fxt(J+^5r zvq4n`FxH`ylTJYbm_O4L5jW&qw9IFY9OhU=kOUXb-_&IbgFoTh@K^ocn<_lQ2ka!? zXWJcdRX^BohIlr{>f>dl`yuu=6U9Ne#ff+(@7EJf%p@W|W1bCEuA1ssz}ti0<+O}+ z``72ryeKxMapmNsB$ATOoNaMUgUNos;9^88ih~VPR+0VwZlZCaB5a^7O_J8QR=BJ1 zrobMCZKWx7=CtzxG@JqFL|Yk_e(w!8;@DbK3anfnOtgQ6o!Oqwx%z7cUJdl$;-Q(! z`j=V?bJ<)|eu~{JSOdif-fUVZ+k@hvJ>Rxw>Qed~00OcsxgA(MLSQx89D5th)iCKHd$-JqEe(zPO6FoFRGTnJ#k1$AVsWv#Ulg z@InP=g;|ONU}r{cvg7G`qgQ!|Th{p!oW8RN4{Fy#ouVq1f7>!Ph_+=A*I`0Yj_myP z4Lvx3s=15`QsMWPENr z@3!?UjztUS`}gnd%7?qZ8~MC%g+LfZ0GNn|6cz(Hc_$cGSAzVkxmIuLkt*f=MLR3P zlGg;AoF-pPQhIljcNirqPuu8K=&$RK1TAh)SsIMm_8|J?ZYerFLzjvVPPJn;FmwIM z)se&EZN+B)b(|>gZ@VjcFE8+Be0|F`$xe>Xmmazt9zVq-diER=@GJlx^$1nLIux|y zaFz;XB5ltXJnLLe`Dpio=+wK85sybx9v?v0{gX-ho@hWRrjLQtr6V9G|tz&AUa6oz72WFJ}jTYmDc=v;IV2Aqz*mdAZ z)`BPi+L*tfei;5@svs#PwFDec`JA>%K?0)>SOET3o7qyKQP1B9u+F+-78o#xB%*L` zg#`2ROcw2ykdEaS|HyX~{aZy7n4U@O=1xPlL(78LsC-sI{_0<1$2@a*s2i1%n7;_Q zQiK+KpFJ=Orf@l2;Q#@~Sit=oA7qhz2Ndf5?bROLhna7I9Q%7`yT5+`ieq_8)YT6_ z4w#4MAlhWP`D~t`LP}Ed?Nv0^r=RLXEtc_Wg>O1WFTQ4b1zZ=tqLn?X=CO`4VTTK? zp|v^$Ilq^BgiWIkn#EFy-mkc~Y9hgvv_>HZJ^y~i8>0%v03VUtm2ep+q&l+NBQ~EXeKTVFdn=2cCH8`QAONtVn$6SxVD?t=OJ1#&I z-nAQqiDD5k!>`*nc%@A@7Qg%dYWP^90^Ji0lT6UN*jGx!U0Ob(y->17?t_8!Yhbdk z()o9?tYpjUk5u=c2PgXm6eNu^N>>@vC(WN;dvKbC!=mo_W83Af$Ku;=#~Z$;KNB$S z#9aCJfpCUlBQa#7Cz_4}Xh#m5@ms;bKFfS4otB!;z-BD}@qJj@Z3YQ;v)c)Nw4A0L zB?>onvpp|)mGa!rTK4*A@!fyGb>&P%Mfkrh%;8=#vtrWpl*7&lPx9GxgF|h)G#H2k z2%O)(W6N=~>t1Nm<$Uh!x;8lQj<3d;CuC&+6F&)fpJTPlv{nGFAjhOPrYA#G6v+Db z0iux&$QnNi`GqR5VspTU1E2vaQ25Y#VA&wM|4i_1(b2HK@v~GiXcE<4g_-yer6|Qv zfY7#q+Qy8{2yp7FqJm~D)M&c%8y%=qwF3xM^p4> z(jv>faSQSOYm}%%Tv%bp#PRG!kn9BO+5h!af95GPEp*1pOwpO1+V)5@1Y@ifh+Pap zR7ED|F)+@+j8`ehM4&eu%izEire4@4KLzt>@NCNE#J5y z428{p-`)@?K35D)jAe|Oa3~#y;l|#F&daNdi{R^EcmTKO&K&;%Aq4ED#HyP0B>nLq zRtZA=13+wV0sQmluD;?}>1P(>WN* zKEO0y*q(A*6Yb?ox9(tQ>6|m33|pWZoWTt@g&|#e@B{I!A@_s#ATtJ_OY|t2LT<;h zNulJ7>g&GVfI~UI+^Xob-H?#Ys07>aa-`@jbjJg*I;5grLZs7PSM@>r&yI!IyZm)= z+5h~kJx<4LNp5*e;5Lq^r{#CFnN3G8%EhC@Hz_W4OEAlsdfVN*^n`?)hlIHg8$MR^LHDb=DNRa?=amuY+aWab*_a=W-H)}E zT{Lu^Xcw8e)EdzCY1!++oGo^%uf+9!5$}fi$Pge<6cDmT8*=nA0@QXim6KY-v1=PR z#(A#m_ZIVe20*fKa5N-}g)d7V>Se4*`Bo2yg3pIeCgRgSuzbt`v}r2;pfFMnMAT26 zYOAFI-erd&O>5C75;K}Xtu}v(5Uk(3??lymK|(>+?D^xQ@vk+rc!**H0ArBIKMDRa zzq*kpSVwKCJFlv4EJq!Mo@X%V2h43k2i{QFlu57 z%4|<`K&EY@~tyjk^Yl0>3%c z>Z}aiuVw@IJ4xS-5fTyv!u!f8+;ZnXRiA+$I)@#Dg45&^jT9WY;Hm#uoAZ3@g*bkD zg_ol;qUS9u@YQXlB&c&oWLa*+Jyn;aA-lvGb#!b;V(@vKv-c%D`@BJDL8ZVGHzE_u z3^35{d-(3&v<1=gje-co6BVbp{YHcAyCdaxD8;emP|4U?`BzB4nQRimzM;~eg#A|v zKS!ZbhG1xE%&DigQm*bl(br83l+V;a`|v?x;wz3X5JtW^M~-Mj@~>Jj%hM)mEcs@v zFjS?wc?5#}ADj^0eqN3?O$X>!Q{_7<2tIcePr|`v{ioPT+5RHQii!&$2OxLxW9U@T z;m)rriSv*az=I)rxe6dpa9u#afswLiS&HMW(3<&*=>s;*+nX~3ysAk)t0LWhve6C2 zd^)0t8P9Ukf0??r4_zO7!TpNF5rusYs6b8CDE@ElS4WMaFAn*gOvvdXrh=YfV#Bb~ zU#9HW93M)UO#{Ge5)6!6B^K0`8V@eof{A?t;9Od#I?2db6Z3#GHnv7sf`BW0c`1 z610YiLGw)X+CvT4G7WiZD$->_vS!Hss|atP088Gm&W#c`24i&9M-NH1FMxNu-(PIp zMZorTgDqZaT!m^lmiAc!)wvvdBd`Qel-g8~MRYh4anlPs^I+rV7=8Tgsv}BWtB$_9 zlH}yqlA@t$NwwLJ_;_z3OzRBCP1f)OuTjPd^ge9@;C4$#vII+jMGBsaO(0~*y4%Q! zjG2>pAL<*rQ3AjTY(xi#eMRA)X$ZS~k)!J3x`+K4#y2M(a6=>JIBEKCEeWCMOF`(L zHj$FAs%0RExVsLX@K#Y39!6hBQ^>|PcPNTge>iAU&VGv_{#HZY(q2{#5nlv2v+}g2 zSa(Javw30W+=RFJQ~RWw;H^oi^SpBMW2m-Dt#7I8I^N9rpVryN%& zL|9-ksH$o{`;GUn1A*t$ypdAziD)pV=w{fi2(oV_r)U@*BJL?={o^Kk*axn{b%Ter z$H^+bIIoGDbK|5Rgn{O*w`fG)$buNh*gF&#D2irgkTvEs z-vB9!PO$+o#=g4@?2n&3EBUtctQ1eD=4L z2Czj#i20GneQCiFJoP6#`KmcUf*3c}lvn{*R zBa{;)(h5Ts<}d#WGuDqd4vSmJ3VAIClCy6jGs1myaPMow$`o}2pIcnqo>sq?2KT!i zx6O5CICFypRek{rxIJL@V)k>}>g?uj&koD;y2g!APz(5jFXN3Y3uVeX2qy92oo`5= zpQKJFt%om9!5LToxH{6EV+9wuww=JcP(fK^82f`~Li~xbJWi zZYUugNK!YcN-Reshb4#U2-&Ay1QE{mAj|CeeKj4Yo44**iW%td)k8)%x(-RJA)n|P zixO@N25`rNDw0CmKls)kj2Sp>jDN9AcNyfaU%=x1@qkNF$ymQYg~p?!#8mJD9b@VK z4$$=G-hUdy>hth5)k*8i2MzGLL9G_lCzFSaSeD-2tqwe>-IL0&DesI6 zPS7lFWd~~);=2r%E>s-8JgV}cgjYw`>;-bMn}d<`c$m^Cjyobs{BIH*_0~rnk+Mac z^!~lEEX9hRpnObjQWJ;{9mbN{8)@-yS)O+rj$ZOzHsWr^C1IwxQYnt_UE$k>v{3uS zlXZfyWGe1<&oFQu-v}wr|>|wOyZ2diArb`zS)?G5Pb`aC-qv z7QD_+BsfL}I+dyeQ$=nr=ic2i_W}QvPi0YfS�F@G@g&NL2I{H@a(~7fKAG%LonU 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 literal 0 HcmV?d00001 diff --git a/docs/security/trust/images/trust_signing.gliffy b/docs/security/trust/images/trust_signing.gliffy new file mode 100644 index 00000000..b21fa366 --- /dev/null +++ b/docs/security/trust/images/trust_signing.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#FFFFFF","width":881,"height":627,"nodeIndex":322,"autoFit":true,"exportBorder":false,"gridOn":true,"snapToGrid":true,"drawingGuidesOn":true,"pageBreaksOn":false,"printGridOn":false,"printPaper":null,"printShrinkToFit":false,"printPortrait":false,"maxWidth":5000,"maxHeight":5000,"themeData":null,"viewportType":"default","fitBB":{"min":{"x":10,"y":0},"max":{"x":880.0000000000001,"y":626.25}},"printModel":{"pageSize":"a4","portrait":false,"fitToOnePage":false,"displayPageBreaks":false},"objects":[{"x":10.0,"y":122.25000000000006,"rotation":0.0,"id":79,"width":531.0,"height":500.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":0,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#ffffff","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":416.0,"y":312.25000000000006,"rotation":0.0,"id":40,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left","order":1,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":41,"width":71.42857142857143,"height":50.0,"uid":null,"order":3,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":40}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":40}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_left","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":10.714285714285722,"y":0.0,"rotation":0.0,"id":42,"width":26.0,"height":18.0,"uid":null,"order":5,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":40,"px":0.15,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

1.0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":82.1785714285715,"y":17.03600000000003,"rotation":0.0,"id":0,"width":63.0,"height":82.0,"uid":"com.gliffy.shape.network.network_v4.business.female_user","order":6,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.female_user","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":1,"width":43.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Person

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":330.0,"y":142.25000000000006,"rotation":0.0,"id":2,"width":120.0,"height":80.0,"uid":"com.gliffy.shape.network.network_v4.business.user_group","order":9,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.user_group","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":3,"width":73.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Organization

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":141.0,"y":152.25000000000006,"rotation":0.0,"id":11,"width":63.0,"height":82.0,"uid":"com.gliffy.shape.network.network_v4.business.user","order":12,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.user","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":12,"width":48.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Account

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":305.99999999999994,"y":273.25000000000006,"rotation":0.0,"id":16,"width":110.00000000000001,"height":80.0,"uid":"com.gliffy.shape.sitemap.sitemap_v2.photo","order":15,"lockAspectRatio":false,"lockShape":false,"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":17,"width":110.00000000000001,"height":25.0,"uid":null,"order":17,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":18}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.rounded_top","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":18,"width":110.00000000000001,"height":25.0,"uid":null,"order":20,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":6,"paddingRight":2,"paddingBottom":6,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Repository

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":25.0,"rotation":0.0,"id":19,"width":110.00000000000001,"height":55.0,"uid":null,"order":22,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":16},{"magnitude":-1,"id":18}],"minHeight":0.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":18,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.sitemap.sitemap_v2.photo","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":416.0,"y":262.25000000000006,"rotation":0.0,"id":37,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left","order":35,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":38,"width":71.42857142857143,"height":50.0,"uid":null,"order":37,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":37}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":37}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_left","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":10.714285714285722,"y":0.0,"rotation":0.0,"id":39,"width":38.0,"height":18.0,"uid":null,"order":39,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":37,"px":0.15,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

latest

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":416.0,"y":442.25000000000006,"rotation":0.0,"id":63,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left","order":40,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":64,"width":71.42857142857143,"height":50.0,"uid":null,"order":42,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":63}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":63}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_left","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":10.714285714285722,"y":0.0,"rotation":0.0,"id":65,"width":68.0,"height":18.0,"uid":null,"order":44,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":63,"px":0.15,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

producttion

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":305.99999999999994,"y":403.25000000000006,"rotation":0.0,"id":58,"width":110.00000000000001,"height":80.0,"uid":"com.gliffy.shape.sitemap.sitemap_v2.photo","order":45,"lockAspectRatio":false,"lockShape":false,"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":59,"width":110.00000000000001,"height":25.0,"uid":null,"order":47,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":60}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.rounded_top","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":60,"width":110.00000000000001,"height":25.0,"uid":null,"order":50,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":6,"paddingRight":2,"paddingBottom":6,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Repository

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":25.0,"rotation":0.0,"id":61,"width":110.00000000000001,"height":55.0,"uid":null,"order":52,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":58},{"magnitude":-1,"id":60}],"minHeight":0.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":60,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.sitemap.sitemap_v2.photo","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":416.0,"y":392.25000000000006,"rotation":0.0,"id":55,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left","order":53,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":56,"width":71.42857142857143,"height":50.0,"uid":null,"order":55,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":55}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":55}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_left","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":10.714285714285722,"y":0.0,"rotation":0.0,"id":57,"width":28.0,"height":18.0,"uid":null,"order":57,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":55,"px":0.15,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

test

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":10.000000000000036,"y":132.25000000000006,"rotation":0.0,"id":82,"width":108.99999999999999,"height":20.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":58,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Registry

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":36.142857142857125,"y":399.25000000000006,"rotation":0.0,"id":109,"width":187.85714285714286,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":81,"lockAspectRatio":false,"lockShape":false,"children":[{"x":7.142857142857139,"y":50.0,"rotation":0.0,"id":98,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_right","order":74,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":99,"width":71.42857142857143,"height":50.0,"uid":null,"order":77,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":98}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":98}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_right","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":-7.142857142857139,"y":0.0,"rotation":0.0,"id":100,"width":50.0,"height":18.0,"uid":null,"order":80,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":98,"px":-0.1,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

working

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":7.571428571428527,"y":0.0,"rotation":0.0,"id":95,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_right","order":66,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":96,"width":71.42857142857143,"height":50.0,"uid":null,"order":69,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":95}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":95}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_right","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":-7.142857142857139,"y":0.0,"rotation":0.0,"id":97,"width":38.0,"height":18.0,"uid":null,"order":72,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":95,"px":-0.1,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

latest

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":77.85714285714286,"y":8.0,"rotation":0.0,"id":30,"width":110.00000000000001,"height":80.0,"uid":"com.gliffy.shape.sitemap.sitemap_v2.photo","order":24,"lockAspectRatio":false,"lockShape":false,"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":31,"width":110.00000000000001,"height":25.0,"uid":null,"order":27,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":32}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.rounded_top","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":32,"width":110.00000000000001,"height":25.0,"uid":null,"order":31,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":6,"paddingRight":2,"paddingBottom":6,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Repository

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":25.0,"rotation":0.0,"id":33,"width":110.00000000000001,"height":55.0,"uid":null,"order":34,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":30},{"magnitude":-1,"id":32}],"minHeight":0.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":32,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.sitemap.sitemap_v2.photo","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":330.0,"y":0.0,"rotation":0.0,"id":180,"width":67.309,"height":101.072,"uid":"com.gliffy.shape.cisco.cisco_v1.buildings.generic_building","order":126,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.buildings.generic_building","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":182,"width":56.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Company

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":266.0,"y":125.25000000000006,"rotation":0.0,"id":250,"width":7.0,"height":413.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":172,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":79,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":"1.0,1.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[3.5,-3.0],[9.5,496.99999999999994]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":35.21428571428568,"y":262.25000000000006,"rotation":0.0,"id":253,"width":187.85714285714286,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":173,"lockAspectRatio":false,"lockShape":false,"children":[{"x":77.85714285714286,"y":8.0,"rotation":0.0,"id":125,"width":110.00000000000001,"height":80.0,"uid":"com.gliffy.shape.sitemap.sitemap_v2.photo","order":83,"lockAspectRatio":false,"lockShape":false,"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":126,"width":110.00000000000001,"height":25.0,"uid":null,"order":86,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":127}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.rounded_top","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":127,"width":110.00000000000001,"height":25.0,"uid":null,"order":90,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":6,"paddingRight":2,"paddingBottom":6,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Repository

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":25.0,"rotation":0.0,"id":128,"width":110.00000000000001,"height":55.0,"uid":null,"order":93,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":125},{"magnitude":-1,"id":127}],"minHeight":0.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":127,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.sitemap.sitemap_v2.photo","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":7.571428571428527,"y":0.0,"rotation":0.0,"id":122,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_right","order":95,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":123,"width":71.42857142857143,"height":50.0,"uid":null,"order":98,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":122}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":122}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_right","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":-7.142857142857139,"y":0.0,"rotation":0.0,"id":124,"width":38.0,"height":18.0,"uid":null,"order":101,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":122,"px":-0.1,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

latest

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":7.142857142857139,"y":50.0,"rotation":0.0,"id":119,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_right","order":103,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":120,"width":71.42857142857143,"height":50.0,"uid":null,"order":106,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":119}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":119}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_right","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":-7.142857142857139,"y":0.0,"rotation":0.0,"id":121,"width":26.0,"height":18.0,"uid":null,"order":109,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":119,"px":-0.1,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

2.0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":416.0,"y":557.25,"rotation":0.0,"id":281,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left","order":179,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":282,"width":71.42857142857143,"height":50.0,"uid":null,"order":181,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":281}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":281}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_left","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":10.714285714285722,"y":0.0,"rotation":0.0,"id":283,"width":48.0,"height":18.0,"uid":null,"order":183,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":281,"px":0.15,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

release

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":305.99999999999994,"y":518.25,"rotation":0.0,"id":277,"width":110.00000000000001,"height":80.0,"uid":"com.gliffy.shape.sitemap.sitemap_v2.photo","order":184,"lockAspectRatio":false,"lockShape":false,"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":278,"width":110.00000000000001,"height":25.0,"uid":null,"order":186,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":279}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.rounded_top","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":279,"width":110.00000000000001,"height":25.0,"uid":null,"order":189,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":6,"paddingRight":2,"paddingBottom":6,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Repository

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":25.0,"rotation":0.0,"id":280,"width":110.00000000000001,"height":55.0,"uid":null,"order":191,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":277},{"magnitude":-1,"id":279}],"minHeight":0.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":279,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.sitemap.sitemap_v2.photo","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":416.0,"y":507.25,"rotation":0.0,"id":274,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left","order":192,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":275,"width":71.42857142857143,"height":50.0,"uid":null,"order":194,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":274}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":274}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_left","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":10.714285714285722,"y":0.0,"rotation":0.0,"id":276,"width":26.0,"height":18.0,"uid":null,"order":196,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":274,"px":0.15,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

7.5

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":472.40133544303796,"y":320.25000000000006,"rotation":0.0,"id":306,"width":20.0,"height":12.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":209,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":472.40133544303796,"y":271.25000000000006,"rotation":0.0,"id":307,"width":20.0,"height":12.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":210,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":472.40133544303796,"y":401.25000000000006,"rotation":0.0,"id":308,"width":20.0,"height":12.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":211,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":37.214285714285666,"y":406.25000000000006,"rotation":0.0,"id":309,"width":20.0,"height":12.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":212,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":40.214285714285666,"y":456.25000000000006,"rotation":0.0,"id":310,"width":20.0,"height":12.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":213,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":594.3333333333335,"y":493.25000000000006,"rotation":0.0,"id":314,"width":283.66666666666663,"height":20.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":215,"lockAspectRatio":false,"lockShape":false,"children":[{"x":66.66666666666663,"y":4.0,"rotation":0.0,"id":312,"width":217.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":214,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Signed tag.

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":null},{"x":0.0,"y":0.0,"rotation":0.0,"id":304,"width":33.333333333333336,"height":20.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":208,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"}],"layers":[{"guid":"dockVlz9GmcW","order":0,"name":"Layer 0","active":true,"locked":false,"visible":true,"nodeIndex":216}],"shapeStyles":{},"lineStyles":{"global":{"strokeWidth":1,"endArrow":17}},"textStyles":{"global":{"size":"16px","color":"#000000"}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":["com.gliffy.libraries.cisco.cisco_v1.buildings","com.gliffy.libraries.sitemap.sitemap_v2","com.gliffy.libraries.sitemap.sitemap_v1.default","com.gliffy.libraries.ui.ui_v3.containers_content","com.gliffy.libraries.table.table_v2.default","com.gliffy.libraries.ui.ui_v3.navigation","com.gliffy.libraries.ui.ui_v3.forms_controls","com.gliffy.libraries.ui.ui_v3.icon_symbols","com.gliffy.libraries.ui.ui_v2.forms_components","com.gliffy.libraries.ui.ui_v2.content","com.gliffy.libraries.ui.ui_v2.miscellaneous","com.gliffy.libraries.network.network_v4.business","com.gliffy.libraries.flowchart.flowchart_v1.default","com.gliffy.libraries.basic.basic_v1.default","com.gliffy.libraries.bpmn.bpmn_v1.events","com.gliffy.libraries.bpmn.bpmn_v1.activities","com.gliffy.libraries.bpmn.bpmn_v1.data_artifacts","com.gliffy.libraries.bpmn.bpmn_v1.gateways","com.gliffy.libraries.bpmn.bpmn_v1.connectors","com.gliffy.libraries.swimlanes.swimlanes_v1.default","com.gliffy.libraries.images"],"lastSerialized":1439068922785},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/docs/security/trust/images/trust_signing.png b/docs/security/trust/images/trust_signing.png new file mode 100644 index 0000000000000000000000000000000000000000..4a941be19d7af808c74be2f67e75fb13de6de411 GIT binary patch literal 71621 zcmZ_$1yt4D7c~k45{Ht5bVzqA-5t{1h$2V`N_Tg6OCu@LEhQm{N+Z(U(%p9-|MTqxm0^#5gf+5J@Cyx%az&8|d z&!ogPU7!ESL^aeJKkZGyc^Hevlv7+n@nJF5BF{29RZ_)4&ZD59!qYN9<$c{3^*cw>J&5nRn>=Kg=?YVVGooc@@QzqzG!)TH==Krn=(9_I$CW9n$|O}46?Dvm+c4U z-uRp~Ir@~({bI|GG;nzPygw)+{pr%w50}b)%V$ zj^hgl`QH~|fEW=(;*~XS3nDy}gB%_|fSh$s0iEZumY3b92VR@x*txY==Wgkog&(($ zULwFA{rBq?)R%(mtH>7x@NELN+_y#l{Zds&fCkdihFpokA&TV$BG%#uPjg7QDP884i&$3l(-As%c_YYuxU8 zy$qLj-|dpjsjNu;$PPNzu_IGLw0dJuVNZv>!xoVOg4tkZO9=iihggdnu;YKTxo1QM zjr)?Y{&dGgqQ5vUi|icj{;aled?_@upS*_IQv-nodESUnTJ$E@+jI!~Nhl=uy-27o z&;2Rkio^LsV^Cn<6^l`DULkw6ZxY_v@jVL~{uJVwQ_hR@tAvdz;2=l}xj+y~sp%!i z0sqeh!odOO)}s&j!$Amx#3cI7rb_(?P07x#v5xu^c=2QbmCy~JF&AN--iHnh!-9r;kAd=0z!_10e%(fCErg&JPj)GK9l{V zUW4$tz-Zj8IO_I_>JVsrm5xOnre zhRBhdKE-NcKW~a#sOQb?QhO^#vp^#z!dH4#RVW?Ku+jIF1m4Jxl6X8nn z>MJa65RkX|VR-kU?TZH%7a#;>fqS*2Zs}tfe%{}lOphG#6hH8j#C3^NrKppYT1-o+ zE3ON=_d^7V94I_Dbs!7#_r7Zu7PG`2iX!(cxq%N!YCR1fV%b}*l3Mx}7P}*;f|4-( zRdYigbp89BXb@{#7My>Ys@a(Qe(i)4P*v9c5)=w`Z@|Juz(xj7W`lnQwWq1fHRcv@ z6B&IqtAAu z_QLH0uT`M#+GS$wXK%!N6F4ojH2pOqlDZdiM+q!X=7gR=#5Zsy3^DG*CYTaVym<7J z2_>J;3A}(0<6T$Yj8;WI*;ui%(?NVi4E@EGtGa|evf28Im#$D^toBAf@x)QCPDr3Q z;g;jxVlqEO&YvSEnBc-gr%HdUj-(+p-=*G}@8A*A7)$Dwm%!>%?&AvW1$Xb+EQ;f@ zL>1}P*De1f!axeHUWYKPe}}{hjFBmf!va_2-7xxAs?y5>jvMPR zf6cEAmoOGsr8?li(vgocD$gV#NVk?dnlPG#G+1+FNo;AILKOj_+)9u#>`T7+=w{07 zfHblmMs;lDGr4^3;Ue@hV#a)0SEw!eeKc=mfG~*WvA9hSpkiX+jYd4(!&MaSB#zHk zdHy9eUKb&oa?07?yWt|XcOA_y+fD)X8y~l0XQgwxYtL{e z)?Gu_mM0}uP9QoV|Lb|fJ8_KqW}Mv2ct%_n&jPrg7?u)QW)|HuUBVq<2s9f zUl$`t<)V|LDcTStl%h$zmkDHn@FO(Yezz;SII+;mZfRv29@(+$0UOX|p)&kmsRf9H z8f+ks)!;L8Rw3JO%QP>sA}H}}dy+!SOSSX0jGx+Zok$6nxkHf(&JPmOgM5X>87Az4 z3L@L?#=2@wN)mxYnLkEtwyYrJT|Cx(^Md=|8qz{a%H6sVnHl2ht6!Q718N9krc zDn4_vRhwU zNiz*3e_7q~OJP1&j#q5zn~5q1?dYRz&%1*U*SJn?aM+#kJM6mvG5oDqH2|zQeo=1Y zq8=GBk45kEd|LQ1FG)(FndYt16X&m&*H}f8ol?0(ga9raA<~FjRKR<^%(v^hj3< zZ|yfxT=G$1KB!r0H~In&g5^l?PzoxDgb?z5jKCqt0LLs|WU~>p4W7HSk1&&oZo9Z} zwc7A|rEx=+DK|>6Q!j<8qxhI1FNg!2K`*MDzpZWWSI1KmZ9kE~072wYPzo8?_`9rz zcv0$dX{VRaYBLum+6`U$-jYkW1@3tkge@|HB5iq{3L6K)ez%OaqP@)s!$BDMK!z8;JeZf4q#3D4Q{alEo z?*w16LXIw>Wcl5-AGKKyiAVQxnX9R|Mv&~q2}u`}POS8c+w4az@O&a1t@c!u*i!emOJZ#bZfBT1W5P%FH zb*hoYw$)E}Jiwy!+;DA~;InE`@=0dX;|YIX1Dx11N*0514BCu#m%=@^;a|T^heP)J z(i9To?e-Msjqpi?38DtU)K z<-L-@m(9S7pX14M@JD)EQxhW5;H_BYdcPJ5uo5zVzdaTL%*{!W$J0saC|gD2QZFHE zpTcvKrjh|Q|_0Kf_mk#r}Xee=`JuDAs8i#6W$^{YdAR^!FmeiN^h zjTnxdWRVsEQ9-Z3h%mg z;SS1Rcv@C81kYHeiud)qLP=E1PNUjvj{fp5D#B0(rKI5?gXmFXOP#XhMGLjGw7U(d z$%%Jr_U_Na1*RKFqKEQ$$@0DEH5jP{@2r<*oI+I1G_kPIJ^x`}0+_4AwZ%;-{a%V~ z$aei8EqrMjX_xn&*qXHv&QAiBj|GzOG_-#Oi&~3FIMX?;zt4N2n8Mm--P8V$aSFrn zEisbkY<4WNc&It(1O9dfGq9rFkJa(5W;6K|^Ldi+#(IfLu~2?>7d?&n$Zw+Uo}A}e zLZQ@L!;;*TxfVTvfv4nbETN~)vB z9b2)XM|QDRwELEZG^M=1{W+#6r@#1H7AFhJzi3nd(O8QZ(UhJ4=80X&QmN1BJEa>z zq>n=Rhh2lZwIMt*+4v7)wx<1MPTrJU(-2}KD$0n>g;c8TSUK)KpC)>~HZ%V)LL_j) z;hoy$_;zwq`)3aMW6G810Xh}yH`u{D1?&w&^(x4t2Q{0C9#_%h+x7lXEg7X#ikJNN znK0)m2)iWskeKH$(){w@c#HhH>C?=59=GTdqx@kr!a1T!AL(lN4{9qe5kY{jZyLyh zL-U`*yS^)ejunqNk=gbp>U{&cSF5TxfNkTGQkq0gt|YdU_paFp1JQU59zKR>tz6XV zKRP60k#ezFxrL|gAI9ITI|O=W{ly@B@J6+nIoB)dZ^ZkwtPiKu7BDEYOzy+c_8mYp zOuKj2<0vpx=0@J!Njwtxaz`^mX*RcaAoN3^M-RJ*cNMymm7eh|>JtG7VTRZMU9^N-wb%xCj6%{$8G1nNB4ViMEGbGK~W&{IVQ#RH!501OuVAtOC@yoZT zC%A0?O(mckICnrkyAP0@S`Z)#3YDQ(vWj|2hGvO7buY0r&MJ|a1`2nFjzO0NLEd=yoKNZFpD{ub&#G&JJipW~E^;|& zVHma8t2EAjezW`gE!XRTH4~&oYAj^c3wyIqYg2B=q)rz4MR3EcAp;Un;%okZt9!$# z>Vrcmpw;cW(VMUjyTX!xJ$4ijf2UUUjq751XK8whyT#(7){iN!tCHfyLxg(&{TE9} zAKUyTprBiLMsP&ZCrAU?o1tW)eq(5%VK%1cE=S~)OjwR`UyvNILF;TO+VGf9@|{I5=R54eAbJr=zD^H2gCGdk`HR1}9G#r{N1;tNzy7k9f4EldXP;K@-APXBt zaYV}2`>e^zij^rXAl8z-x^Fvu7u>J+wPti8@`JDxlg-7Hsx9&TG7at6M` zu$E-bU)o%0Qe-x{sf|nq6_7 z@LFg-t=#ZCo^iZavnRRNzNTRRkAZ@+ZC=(29~>eDa-kEB%+HBRF1uPNtK^5<(7>-g z+Liz4AX{2k6mEPc8TnrH;^psr8%Zm&jgKQg3P_TJ$O&TOn8NXS^!HBvxHc!F3*KY6 z1o(bIU72Z=E>dS7t@7x}|FAlvGZ~;kRQ zS|OQT5g(Oge0vr^z8U?D?t-VQw=6JjDRxD%6q=1sM-WIZG@kWqM-VJ%w=R?7k!4;@ z)}Ernug7`EA17GZ4z}fp#!W&v{<13ov2CP)Y>n(umTzOP@tU+-hKjI*9*+n zsw}I+20;cUo^OX#4c{yb2YbzKUzk9LnEiyFxQ5Od5o#4j*o&WA)HFM?3BO-5l{P#7 zI`9)=qtAE~DZC}}&jAJNEVis3|AjdOq(X&3M;nRxw?ZZ^%5H%}dT&GjuTW@yAO;I7n1~K+nqP%n( zEJ5(+sDQtLZ+4|^|BW$!b#as_OFnZtv<`%Xw~5Tq-kMnM*g z-XFRu2hWJ`*=rTa>r&{#O`foMQgGCj!$>{*SxP{;by5e6AdnISK0%@iEWiu25X{}~ zg$w`^=r6Y31vRkQRpC#VM7h}Il*W#xjoO!SDRY)7$&$@8_Z)2O?wkm6WW@>tK(94X ze7|}aD$Xduw-Wavz1frefB^XcJSViT)$M&Wo!L-%{yOP% z<33FG;Ua1B>!zEF84}H)1FCi3vuw6!Z9hkEznxyWt+jGNnC)xL zee+-8^7X|8uJc&<(UA^0bRBKAP}3v4Hq!d? zr5B5I51j#C?aM{}&B=#jC1zfa<9wMZa>x-%(k3h}!$rgHoW|05i=SzIbR9Z>SFhGy zAB8qk@4i%sbV4boJ)W(`?VIAjQ-Oz+?BOM3;Nue5V+lqa5&!7JyEkd z3$DH56Tk~L*OjTf^J?fCHjsD96p}b^w-fHsO{qDej#+M-lP3{zmnd6oF55wDk4_v` z&+YwrA22UmK~)i$>2y*<<$AOEd`WYpGm<*QdoF85Z!!SkL8$@TEBL;zCY6=QN7oK- z8J}26q0}Z1E#7v~dSP><11?817fwtWkNuAmAohgc?IYitx)#>raUV#mE%#BNf3ZOs zz-2^Ca!$z)m@)B3?|0QYP<3~WL_r7!NLpg|!MTzt3+?v-gu|uA$KBKOJ2`K74@ufS zKb6>EvHicB27*0$^e^vp$Z2jTvg}GNYC80B2^Uzse_)H1L$^Y|?X6lYE(W3}z8EDn z5P66$$;S5J73e`=Z_*z1Qpzmu*?j-+3NkNAE*7gqL0O{1{l=nvD$_oxbCC46)CVsW* z_yKnt)A7NMZ#oVChb7?X>xV{cjkqPfkP zeC{9fbpeuh^NW~?-SDCkbZ7k3H0c*HmBC|FeL6T5x}-8abiut*Vhy zyWv0se(ov7;MIOXnAvc=%OaUJIbpf0FuT!}?fhL`=DVuyD`N#UB4%;dhDd1~dH?-9 zCF71IlF^#29|Xyfs?RmQ>V5dXbC*?1wg&zm7AW3|cfbRIAgJt_p&6;&^;Qbu$3*pL zeD7<();#{R$JD^V4r$B)^s%VdF1Gw<B(b=ei;J7*M|ES}JZS<3ka4STwtvz2#| z(opaHu`XPqBZ5qUY@LE*w%LN(AEfCgp62Ndw;Tn=t=@ZWPZQJAl;&?f(%*QTz&AS! z-zl1OUMOC^^+fclwTQusS$;<96iDHCA&G$Uj|J+0^)gdJ<%9yxuW2%c-k-roM#6Zzs#)f7KjM14lc=sl6LQ!L_OpBKSEy zg^db^$YX5z8^NGhomGF@C2rf2KE_A!oR(`#!rH@~VblVu+4~eapMx*N#T*bo;??H^ z`H_|3)x-Ooz(Ef(5ay{Q$Bj7x6*xb@@k>kUi_tY$O5Wb(a(%Gpw!z;i#qTeGq2Hp3Pi0#pU7$uLcFe2D_1FGK%do3)fJkl+$!Ni_d)MDmQM~3_Z!uOxy!dG->3r*ANteo zN$SS(A?8kIOO#>V=e;pN9~L7_-`Fu8k3`CS@5_$dEdBWF5v|c7Ne%C4t_Qh0xusf+ zs}Tp3LlC}+f)q+7W%rFJnRbsqjw;Yt&X+f{)ph*v^4p4{dvR4ZG;oH^WTNUQk>=)l z*XWX(C(WCXTwHW7oM0VtREBY5cg}KixVv2II*r<##e71^BKhVtD^F`rM0s9{EP0U) z28nylpnXMhO8o6~VK z8}5L6mo7{xu}EYnII}k##^p8qb`dd~a)kkAA;p|pX|viF55$X+%zpf%QP*g|k?*%X z^fc$sY)_<>@v`~dH6~p4;xeUE!%Ac(CybfJ@-2D$?;wG?vWh0GR48IC)j}hMkP3|e z-lK0b#B{izS`UEl{`XH>s<%{nh&w4SWuKQTEHGrn{zOYN#GvY-%n4vTEV|aaj+aS=;NgQ%`;Zn#jnbh$?32N% z?Zry3MBOXBF$J=-Y}_(?Ug~ln=5`wRE#6xI4p9?EkW!3+kke2!EG0a+iHlLa8iE7Q zo=`n>2t=nl$^UxddAv%|0-g=Y>9-K+>m{=i`#!gu57oo?N9|JNpnyDtwkvVIfs{P# zoh1<4!la{W>*$3^`%e9C9u<2`;J2~y!%h;YYA6^6Bj!rPJPL-tZ$XU!QIzrNGWMSq zK-1P>#eb*_$&AJjfby+a4tFrbKze4v03E^tjs}sq5sKRXmjM&$!TO;Z)4|8D2?Ein zUxl^*R<+d34S{rR#Q*&yHgZKiq<@Vdtl5RE+&1F6TgrSO{5ja0aBi5HhUq`_<8wsa zH&B5kpi7VcdDv?hm>JsbN6JkzP?l?4@wb(0pyPTgXAmT3LI@oKL}usp4dFOvVw-Dj zO&bs7|D?7=Fe!dnvcEe2XTTrY3c+&^<%nbhPhYc9u=VA=Im?bwcTEszo}vG5WfZDU z!2!}PYcG^1n2%b5T97=^#3(F__R|88ZjT6(p2-bBzjqNyaPB_+MFX}$gI2PwWZbgP z;AD;VB-91@J}IY0!Ms?IX-~4R3zQ|MwjZV;k47xM<_RadSD-fNm|^MfzZESt0>sYu zZhf&1iH=z$%5I7|jDNuJ?O!|Y0VH$`>JM&qA_4!aWdu%u1XNvX;0{ktHEnrpLN0{` z;&}cDOS3>$dF-T6ws6QkeBw;SMC77b81UcsYZh7h@d8B(ncH)T1Mhxi7Jvm)eK|Md zZSo`XL_1G4orJf$UY*9j%K+3ZsP&L;VX#_H2W%T%)v_J4Y?YlvtM8@;> z<;d8DG=8KABpjQ~hlJcauNWR!Yv~JG{pwAIpT=I5zOnHty$Esn!G7@L-{yNGJiSU| z+hdAe$4QBxGh{vx!p)YMZi4Exnh4eO8>6II{R4-0f>>!Vz00Orw3DacM-YyxrS8B_ve>rySy;a~HM z27*GX5?_=-7Hc_vBy3u%Qg_8?QaS4aXLWicrEbnYYK6A1wr7%{ z>IC@mqY_F(cSXFj@Za+99@8D)Psifg*@o8#U1Us$Ed3kd@3UK^60ly;RWDUs`wG4T zRe{u;y`zfv++JHhS?GCED9u}YSp-_Vz*&RwBHp_ONgUHVuPXE;87R-^b~@o-h46Jg zaF%)L)`WEbg8nh^;j!V@9eALI++fRnZ;=7Q7T*!Jb-q7_x|QVPrTS^Vb`%`XvqlWh z4P#I?&nF$j1-$1rUkU(nmM6-Kk*{NV)5Gq;y?6Qg9)S2F_Q9(9$F=;Up`i@e;G7@5 z>{9vY-m6ONZe`0&*|=7-I+O2M(o*XC?&Sd{A5m9FLyJa_ezkdUzG z3`C`J=d=FJA>e+@5Jk!#FLL#V^YGvxx4T=4nD;H|c!@?xGP|*|nwpw~RNM98cN5a+ zr_p4>>HW#93La;>1?yQJ15dx!T8{P&4yr^R%z5mgl0Wy@;oh36WB!@W9g)ucW(|Qj z^$}vEitk-<&5JJx2i^&PqIl<^dz>mND$DFp=Kqe812&!~u>M2?aryI`Qi{wcH8LV1 zpUtRw=xpAHJy3sVs%~?;q8)#@vqYmv-P)SPX0AmqwCQUU$!G+7n;e$#F%$_Aap-7J zRK@4|0L7@$)^Doe9eZF-LqkJjbnnKmPb5m|Tw$j>)3UPBq*akmUjHC;Uro}nSo@LY zczf-pUuTsBjfatu>yVa{Uk1m^F`^^kc+ADm66g|>lVv1B9@6^w_~WqvBSH3L;kdn-wU94x1 z*4s>P%rrSr-6Te>uY^(!etUJiGp!T(h5Y*bdMQUd07Sz+%M1BsoP{&m2qyeMJSNS` z&lJzN@bRk5)%dK&7~D@M&3niLewFD_-=>ZhsgOV6xw8umqAv<*zd4=e^15`i!@OH8 z()y^4OGLydMOzg0ZoVzE-g;7^NF^Kp?Du!%g+zw|(pYaS&*Nd)C70n4a&L=kr7vjz z!5C6dt2a29R@qqoVB~T=D9Z9W`pJ8Iwbvez!dH+eN^xj>#Qwa6L4?aboT13YzI1(O zxo$B8Ft|H%Q#V?YKp`dc=6(yITM;Q>W)1fZ)3b9 zdsd@mx$dkQMBR-8;p@4*xmH21>w{h@YTp`|<$T2A^KLWZ`|-)hlqbrKaC|V9AXInJ zd`?@Mxs!04o105#$mV4GX{NU~9k)Tj!Oin@CO!YM%GbYHnfYCO zqt6bl<-m_S45dKKMJGd0v?N9#ZQY(};z!yxVI*U!Rng+sp0brmK@}`Zd!k9Zep%yO+hUdZU+I5X}kXh9yR= zAL1kY%ZsWkAjO(sGx~_z`Nx{CEa7^V$3%&mf+tiQZ{){>Z})r>a}jw;d$F&GhM&*V}Abjly^z)aW;vm z-G)VX8;3Z9*26Vm^Qutl0MF;=H)rz%4h~%LKE^^vKR6}0?9EK@KT4ZD*x$osljz#9 z;cKmZ*%cf-kIwQx;Ui4@$PcR0z(MCY%r8`Z;qHc4LGFYO^L(?uf(j5g>zFy`HoB9$37Ra>xj`A0{gD^`iE9;-xCP02u3=Z{Wi0ul$Ak1oV(dpo)??`^d! z3x>!yr=-k4ou?WGN8q|-!k#hmP0QEj6Bo9JGLqL~5P_7Hm9}dFDEPY zc?PwP-0D#P>9fH9POXs~WR|mOhccK*(-)BsGf|pE2S=MKo*u8F)S#hUjwp$s`w0bE z<3G6>g{lPD8ZBbTG%Rez6c9W`VwuzI;R$IYslzku`^_P5Cdxl`#}fW)sbG%{9^tKSC^90D?E3oSa70>1zEll%YVAcE!6GMHf8H?9Ats8N7l zJa|iN_@7?!->+yPU_?#AV=aeC%%y6g4EwMbst7Z8(d{iQE!_lv`RxqK*wj=xkfxm1 z@D2Yra00W|5TuuBPt7d8C$lnr4p;=U#bZTKdXs0xq z5XaZo4Pw~lnfCPQ(_&DWR(5u5vVvaHf<31LEzcMt=l=j#5#U4wg3!Te*8_(S011%& zV$ykR7UmG6$wif6xC4dE)px4iW_ymK5~lqq0t*(1!r=Wo zY}f*C){>1x73&Z}`~X?>I-8Z{wi@FY?5unQUaABZYvoUr@b{$}uw``yMTmE_qIUQ6 z39>ww!A7 z66Ci29ZTl6f(`c<0RLwX7z6}z4=TvK>oVr$pPIoxY?W?DYh#}bc-^;3iZtJOvR*uK z+)%wd=p`?)o~%}uYCP84s4g?vzfZt&a_cPIZ-gkunx#4;kJ!2>2w!-zg+G(H~sWSVH z&!khHp&_|5StAS4QO;k!wVs=sQ=W|hCa^#RDV=?)*@=19=k8{#$-z9o3Cjn#w;^!v zp&k<7P1qP6IB*K2Q|Gx`(Ctz79mJ*WVvQssw*TkPf&ztDh3wgwic%M z4R*^i0xr9xTA%cjF3s4;+B3#spy$@sWzcs3Op$>0W10=m z&1!S6{Z3gJgnD9@p{YD}oA=FSSTRk68h+aF@UZiqc^nK*9B|_MvYf8`%9R9dwqD~D zG5Yi!=zES~5Q5xp(;Gj^T8KR2Sv^4WBZ7M^`9F7G;X`fX;qDHoSXq@?=x?sCGg?Se zKoB+x0#|eKL8$Zw77>1ATS8$wEYqb4Ti%W(U@TOsDKbSa5G0TUkj z@*oVyTa5|%GPXA1D zfzImVlM7N~HSA7>6gD*ri(;)rbzlg@Jf1PeDr?#AE$i?DqC{tj1iWwYkKj9ncwpF}K_; z#m-SJHQkj`0y{HXwroCNA&;B!cL6H&-S@Z zL&hRATyeG|W|W<$5$~J712ElI9%>&L6Y+Y#6$Ii-zYE>-gEX0vyL4R2T6zfjwoM;- zag!|uNl~@ED6ca4>XV10d@nADvXr1h)PA7H#}VyHV$yn)2h|pW$A2=7l~GWqcHnqu z-8@SI)cr64Vu{1|%dyfb@`JP8w?J*fxMs>^BWxQC)C7{(t-@~}|6%_vJW@^8T?bD_ z3?PH`BSRqTv=u~P!wam}l3F$2Jing(5`aS0;4rV`0abmp%LLwdr|RnjQ+e>4HY-*=%t(EQ5CA62};i~3X2ofHVyM^YCJ^N z`y`UC$or$HV_Bp!ZM+tpRao%88o?mpe$&+)_ zhQedh3A(q87<%Wdlb(^u)&ly)5SB>Vh_0|fXI~D-?$&#f#OamNX`Zi-X}*>GLRS!3?YdeMWw;SMEn9gPgeuV%L*n7U zrSbNfI9n&s&o>KorqNFFdN?rKiE-qUxsTBz33|HT_dLtl@^gtIER20RK(cL&H7WM@ zSCL zF{p1m$9D;?EPg#*FKuk?72l8<+g-LpB;gzxRMA4rl&xSY{=M@NGJ-o@$GSBr3#7A4C{IS;r;=!1SXA(@zkF zUBp(>f%K{60ND%l)l^fx!(2@iD@Kd{PnW+&;gD!*enh$D&psg$^={#dpidi1%_8|x z)3JJQaQ-f?$y_tAW}82?eDDGC3QcxH$zpouzwAyS^G~! zN_%XmN_cNmt#NyJB3AJQZU%tVAaV--{4WH|@Kpecl}Q9;z3)GM;EwZu)GVRupI7*b z#Gd7`%WroZLrpj%m1Ur|-z2=N*QX`QU;0HDWp#)8&qbG-5L=bc)u|13Q^MdY4`ErQ zb02#a{0Neiy;g_y=whyZbDYWZvJYd~m0Z@g?2uw53Q(f2?Rt89LoQEuKAt)T6J?|K zMy6IzQ6cl!**V^6E29!$nD8HRzZ>^9v13TnqYkQ%6UX<EW`aj$y?~$SzpgVgeOZ>=UHFsPb8dt$$_lNfpJ)k=M+8j1Az5^7ebS#y|EDkeuOX z@V?7Z2yd`;k`Z~|mn6?$m&6LdZliH7wZzD`)}B?096op{2s+g&gg_R#fJ+TO08ARK zczcE?F&E`1eV(0yX^EQcSC+3quNwEGO-b=qDn^Kl7TWmDYou}Q17oR!UPo3YCnxiA}z$NiCL%o1AUfj=Waj$$k}?fk3LfzevIrSGXV5#c`5eO^)@62 zlTw5bLcfmH?_rO^4ue`GhF=W$oH3NO>i2d8xL49w5`j1Nr z`hO?FQ|w=*w~Cis6}Tu6UxrmK(*0dXk}o4tZfQ41qXJ|fWiL@dPut;YV*jc1hi+Oi z1-{Mkk2=!0S@}DjY!kCK{VsxFI^f@tlU%%5Ws`4omkAX&wD?;5wUtN@DqrZRsWq?~ zHTj-3=ry?S_2Jo|GZzz&#yO_AMYv5j6=)e0LH{shN@5kfT6rE3rhqs6KqSS?pmW|i z%2On?{rrJ_5&ns-vRqHd0)V>R<4zYpm~~K{UC_x}JN$jA4O!5}pSo=o4+ljV$}wj$ zS$)5I;hD~-()U%Z6Eho2<4pud`2)~NJ@yS$fOTwQuRtqA{Git{2xmbKowsnUVuY}0 z?0~?>BA8LZ_v79z;0E(L_W-w-_;}_;{((dP&ZFOr!`FXMu}jOv$Y{%qx+2b5a@9Vq zx8v(ERhJSyPCU0o(k*GHE}A~vLrHxD0913Z;AKrCM!qMkP0>2yQDJmpwmU(cQ8#1Azgu>0GJj`{yArzYLIllf? zxvs-n&p`|q1}R?+iKdm7*2Ez?OsjBQ*uSdz0&^=Gb@8)$qZNil8tYFtbeCx3Ox#%x zO#HUbB{@^Jc@NW=gJM(M3E%EigVLZ@Yzcs60oh>R16w2oWt-O;f<~H4G%6(AR_JJM zTH1A^(zI`9%CZI@Ro9`N<^$yc(Zw)l?H)8Rcus^53or<RHAs_U0^I z4r^tV=V3aml^2g&H0Dxc zu0!(!J%b^br3%4mB%^>vO%)OnvH;@}T9R%Wmv89(W!WW|=x!uIl(XqWjg&T+vKm=d zAGO~<^iXw=GPZVen`prfUVe=qLJ9d5>WwinfESy>S!NsSNsWMgv)FULg$_7L#cS;#c>7NPhc%g`|V}Kx#TRM z0r#%q=$iA_8>RE6Sf|hSPN|LGsf_E;S0sml3qs2xPHYKZ6^+^7?pG{IGh@tDcO7VL zAIXD{+(mBzx3m6A<9A!{(#yR2{WJ6WB21S<>hrT~NUWYV?9h;$W)-GIoasJaqvA=B zakLF%m&r&(TEr5)ra)Ze929!hP0O%WBXn|buVoL9GWsoM$e6(Vx|*miJFCU#70@cE zwWkj#)lFskp#Xj=KCZr3J;o!RZ;pBraz6HLR17T{EiQET&y5lfDZ4v(0O{Ao!iN z+|Ep6x6YDl**m{}TlKTVN1`bwLYr*#BLZ+&ZvEt&!aps5V3-rY0I4vkQJS-c`I|+} zVR||?`rDLGDz4C0q2egEmTS*%&%0Yl=Z^;yj0DeGll1=IK3b3Ob`W~$ zJ#Mi}5)&t3L@VDqGV{K{^0=rS+o#x^(+c%YJ%Hg0Tqtx+G69{08~uZ=W9(puov3g< z(0+xARuYc?`k$r`?d--=$a3IKtLFAH1AF(YvHmidS#u9>NmwXU;e(O`yMM>^U3ts; zas6-tmW;5(jERIw{YeMWW&56sp5ap~w5(r>faQ^!@m>OBNBkZ7naP#TVE$q)Lm4)! z7AB9OV7)GkC?iv>mYNy$L|N7+HYAU^nqzs63+Elk{D&|;hjPP?6DAUvbk?Nbj^oW< z6L~fBPBG?mEyZAC;hW|nab95bPAo3v3RC{|;=rsD87;V3iZ896D1_51ZQ7U`X=<2z z_U)|_W{ipmka7`=R#c;Km#-^D zZC@7Kf7O0ol0==&f<`i4QV#k2ZGJZg;OpC&3?)>T&?t2i%Q*s z8Cj3R?oQ~oE9lWzDqAB>gdK3pu^ibj1Kd@~?$(KL55MRrS)~@yx4~|a50#kiqo_bH z$?MrEcNPty7>nk9{r3u{EUZ$=qfPKDie<40lo~4#&#c0S>^b!W&6f+yDeykV(Kvfc zE|-v{Mc2F>UT5)SeW@pc5sWQ@A3l~RBkK>vVZGM&czEG3=wMfcTb}ccaddzXJw0gq zmE~$ZPV%%a!_O>eu5MsN5Cwr(JQJMYGCBGPxW^KH7XIif_AeMt%8TtK9byd3+Qr*) z+_C{F)-D2-F)#eA_L1O+KZM}VJ{)#iz>3L#bUHJ_%qzvxxo=6*BJ@R5FX4^G5POPV6 zJzI~ws?SWTp@vN-hzD*8O^b$=x84cj(5-hW3)nEk4X_81YNBsTbMcv1lEv`KZFqFW z*B35f#q-+=o8T^kiK`|FYx-fe*>?td`Vc@nD1yoDrfa3+Cxt?P9A6d$TnGLe_l;j3#1?U-OyG-my zY9334GbT*&?{y$}PD(nwvJdj4+4;e^#EfxO6Amn_sb~uJfpiMuZRp1SECZEGI z44BL++_OXLyN*tb2LMmaUROK-JjDRQb=}Ivky{k7X5N4L(u8Cm?>#1y0%CjU@OC`$ zRhNk>TPok>SCgUdGNyRkvf2YmlxQ~jsRhCw-%VnhK1GGIs}X+`&er?zRIotZK%t-P zOb2OKm5ALe4~uKO=~rxxM+UP5vp1~_peDl zHq=Q2=Rf240az*d^k?+X!9cd%3TNc?mA`>%u^Gz;dXZ^(pA|Lq%7kSJr~oPkjE(g5 zMFz*m?fXYr1E}Hld(($Cgvq$N5APaIV^<{^5nm1)&`CqU{9FNaGQ8clrJFS9#NN9$ zQ5vMvydRm>AM1Hmg$g*ab;a9%W6aAy`}XyY2mKgtxcUCC4}c4M_40!&+q$k*431oQ z@kj&>-LV75_WB#v%HmE`OuB@E(fIb$5M_F^SBpKL9+Tpgv1syJF<#L9>=Lo3%)%G< z_D3Z-idDB-ojIS9acW`p<}|x zSX*}^#c$%#JHg$mQD5aR!MfiD!5vNBGm$Y#D1NKiN9|8}pp8?#yS*u~8ZQn0ZCsbJ zg21}%++%<_59I8^IN16tzyWSv+MT~Lwe~Y%G+a$UFT^!JJj;I+Cq8{)O&t{7=3+hV zxZlW<;ZbWN2mT-eenXjw&j3}@$J$%6Obe&v1oTB)8l1dJ-M~^KV0&VHjK2duS^y1n zO&lAC$>1+ghMuShNyWHWTSHXacln+VV3%e4JW z{He|iip|-i(iJn{p^X_iITOfyuI);n&o-MBey*&F!m!tT14xWgwrmZqi#(N-{qwjyusfNe$@Ehz6NPimC&D*0E%{&!M zJq2!9XC6)Tmu2Gv@Vf`Qb1;*Nf*>&9^Z&!wTZd)YwC|#V#6veow={xumq>Samvjiy zNOyOKNS8<`DV@^NAdPfN!=A_A_wBWgwT`{tzjzO1?zyj-x#o)VJa2BS32y2adC&D4 z`D*xtz@8LH&CZtRaa?g*gcR8KDzfzCf2D3LtSt@CZY8IW62lL9M_AJMMcaa@GL0_J z%ExrwMXo2Ymx7>4WnHj{dVifGgM=j+_1IOal))dA9M0s~sh|!p8OZOL*dVR@F~RDy z#8QQR$cdR;8)^CQpo_mv98ti5seHI3tqOI+1)g1o!1Kr&c9*<>z z;_VYskDJRs!{Q0#TxN{nDuR??i+gb;!$qVa(%9u9rpU>zae_y9YyeAckhe`UVx9e` zOMO+euDVx6z1fCWKe!?I`;fGtrI3JB=z4uReCZkjh_X6=zAz|_`)XXU5LFuv7yn4o z)b?57atzKoP#;XaS#<1}03Frcx_+`EC@#qkGiYVz+$c)@)#N&T=6~W0=WkWDYZs$X zs&!tOhJ4>IB~NEUJ^#%Kv4$}Y{C*|Y&sem~v8iVu2vE;ucneht_&5pRu*@uCOGW|D zu3kb*0W}>$g4z5{l{L1qdKlRSPj`N4Cv_UNdAm~xOxc~F$MgpPtkhfF?aQ_63K#Pe z^GG)zJxFBSiwDOJ>uTo#O+QND>?(g@2eDY@TjCCJ{rOL_ zzEsNQEqs27gyB*w;K0QLSD=fvK=dX-2e(o{kxMlUkPo393mIa(c>*eY{35O;aW zPR+fzTmCM|)EKJzgaHdZ4!ckw{NvvqTr#w$U9Cu^AxvAsUmIdXr`j9_)wVwz!)zw{ zsJnPRMe#RE67x9FC=wq|6op9>zelWppzgoTka%BbcjL)>b6x=+c_mqK9vy3^SaTY6 z8b%={%HRVIUY9643G_+yqM#)V6aJ!*;$uV0y3Jl-&N?FAP+Bp364|Ula(q!xwu{I4 zFZ;C;`{K6;6~3^l!SPd)zeV$cz?DSL9{V8^v$B=I>Zx}7U+#Fj z(ix|`rwZZCjhL{<;LfW{W1p3TF`@OgnnMTBJ|8Bo|F@y3fyGG+f^B8rh>{V8ThE3+ zUYajr#}xfi$a^&}m*A}!v4%zJmVE{~VBuL)9{ax(I7>Vblk$S5k|@moo=K;vQ)=aY zF4L%a<2j?|`AW%i#-#0yW=57i=FphY_wMt9Aue&u?}@_O|G*P>;AyAkrEf*aG}dIA zGsy{@#)F(?{1^fUA1U~EudAKJGP1KtRFwM+sDix&5$O)og~Y5E)BS$5JPg#U!uRvc z4>Hx)tbW2GuWDROH>?S)tvyduWF<`#_#f$mW)f(a&I!eSz?ggFiW_Dm++n%MAsQVs zvP!y8o)g7nwC6TIQ4p7-@;8x!_FO@|)tG7;t3j!ukT1k%NAvgnjEh)l8gkJjBZ7jy z36|*@S*B8JeYPsAMa1y5a0}hB*sf7`l%_nf^~XvrvF3luBnpP{{Wm4gJ$NNRiH=aW z86P@{uTWAWnpIN{?Y=H6THcq1vmczF{LpSc$P<(wsI1V+ySO+`=(#qIM{uO|+~2QV z-pM??DHcmNgPWY3++Ap}-Q3@o((~D2HB!Y9XlNLtV&~45&s+X)Jb)DnN$+>ZYE~IPnw0rqq$c_N+LatI%c_!G#vLY7Zy9vZ1eihm{m(0ZP zx!zgA=^ZiX+42n|!;-8(=4oOYqeuxL|F~u42iwJ0RMD&w;s8`rESs7dCoh0{G0ylm!32E}vM@d%s zd6V`~;;As63c1lBA1<6QebJYDPOlFm6~2UP{YCW0Wjs{-v-I%4Fi`pMVA@bRM52`T zi~i?Vv-wTANOb-&jOd?1cF-?!P`Kg0C5bSPiAO&lGn(X8?WB~u4(d;XhSjcuX4$rE1~%Kl6><26RFaJD zBv7XVmG_2heAfbYq1=BojBzM6-M}HrGv^ClNb98Vni%JOou&F{ePJO=2cU(uKONSF zD-NNV*1DyG5}>q>j*fc0rNY^n%jy8Kh!#6w=Zmn;xV&w2Gbe4yk~H1{?NqhlTl8Xq zML7=^dq;84F|of38DHh91BUDc8_9T!%6QGm^$_n=DvbS41rWE1V8j2=pp2lp8MKvy|l$@BN4cDNhD}y8?ucd#p(EsTld-A)jNPiS{8lZ zYx>z;0iQEO3O33s_*&=+R75I+nM#@;T}5q6eu}v8?=Pj?RjIPgHz^q&sIFh()${wa z%U$=`e7o;;KeG#+QqjgA)*nq*Fr8uS$qU@S&38&x^y>$zZ|2m${}D6>pxLOi6B5kh zvxf-}f$%$+p(=U8!Xta4$?gJ}>FtjT>UDu;Ayy7ctqOOsc_qtrgBN%0Q*Kw)mtUG2 zdRoTZd&0!w-HEZp;VHfwL;S{KGV0m`L@i9_>g}oDk*NhDAPhVus=Sb$mA>xg0X(l-7Dn%cN_pBN&x@{7aQ9Y zwPdZBqO;KHU_^R7yg&#?(!d-~Z42JPu4ZR|R_l-CFutJv{{h5cN&+a1i4D(e>RMkg?(BTTcT@h43Wd>h@?QC|GMcHO~$^%IFx>jnNlI<^!4!3s> zWtJRchWiA}Jh%2Mly&v%#&#fO6oNCl=z8722iC!G@~HwB=4j!k$btK1^B1<^y<2l&B^Y%1}O>Ao)L^ zD*~dN+eMryPZ~)&UZLqO?m&s_j&3cpe?` z14m(f2P<4cV6Vucm5PBxPz$*!-j81)JT?%#qS zv|HkY0yK@iz&t;%n$EJu%bokzF@G0z3zXDB zaR=!A=m5ZR7ceLQz)=C9SePCW^a~hZyiMa*U6rIb>mv)9{@_ZX+{nH5Bd<#5$2_K^ zrOYyChKMKG(WF9Qn~c!cTTt|64i$-S2lPC-SKNOi)gM#ys53L9Q=gqn28b_K&ir)y7ukSkT#*9 z0I0+8oqt@J3xKax_V+fmjynJg&Nx6WyJdBM@kQD@0ZsThyo&ZUI;D7NT1itrd3K@D z`yYzK7+GRC`~2v+1v;vwH4gziP`f~R-=`$Wna}(or0fNvRBi5%;*8jfu zS)H+qDhIJ(DhS}(R-gTbw`j?^GRcSW=*t|%U$@i+hY<)y2PoAS{z;E2*Q(P_2z z*wZ&BzCwi#iwffQ{}Gx41w_;xEPu?DOrw_98VU?fjw37bVc2IxZcXTaOP&U0ir|Q014(rc2Qbz%*#B1OlBLed3zznz z$Ivr>Y?c)@*nXHevw1#R%PjfJX}t$&q23B3Bsf^P#eI9uQL?kyDgI;vg}MG|1Z|B; z%c+3JQ_a;mLkuTP?|u&Cz{9?Q&#S?%-@O$IK!h%eF`)Nhj=hOMI++(^HK!!ZYuZRw z;YdFW(*bpX3zZj8Hx97>a!@y}5<|ol(9;WNw;BeBw(sktZl`!mB*cE=0)XSq2+0PBbk0-K)sTb zlvDu_m@?nHzqF(XR}61Na~qch$J2ncn?^x`oJjP&AnRkPs#mV%3z+|_8Pa`u0U8u? z7AhRsq+ccJ93?&?EZ(?%f)u2s5zJKTsCCn!tIdOAXZTK+G}tLIU-eG#z*TAhuJrH2 zicGDFH(|<(I9qNi97Ii1zo|Iv!ksn`oy5gq50}0r!9IUp1}H_AOYRa?gMZt2cNz>g zbp|i3Akb`weQI{&!K}1I#@C7xGGi4@_%A)yfu_gU-Isv@typatk%My%HG=kT>Z|yi zpfAwm5BufImt9c(4~7&7lZ8~;e(Ra=8iak``Ul`@(lm*-z!^(tpycabC-{lEx8dpB zK!6!sy#g}=I?&Hx;X$8ay^J2~<9~JFyr2FDETLcMeBn3sUjp_^*`1xkUw@`bZ0`OZ zPS=`9cx@&~U9^WdtgS8>oN@y1Rng$GnLCCH?z5t8v4(+Jy|zH5lAjinWt;LqzeJW9 zp*Zv+QNnt89Y3x>A`0j?KyX<9bvhKl4Bka93MLJXVYv9$0qoPZPPQFw;|_#xXzeKV zf56NXfPYr5QIXoRF+O^$gX>qv`90u8lEA=(gd!c@(~zzjC&n3zJ89VZ6!WYT5tly@ zH4;R&76=C{S1%y4bNc~bZq}C-RG0GF`ecE(i-sm}yxzscN|*ZEhG^1flQ%w^Uz~-m z9Qyn~lj0Vz$x1$&aZy^S=C~S@+UUtwG;H!ZIB%TLb()B`oOMw zN%DD{i~!`n{laD`z^r}1OIcSwOolX_QQ?!=qys`bfnV8H5A;0KLy(6tL97%5(!Wwb zEZN-G^D&pH^8L{)320L`ATC|^SIQk#)}R0iz|KOB6BYFu;UZq{>ub!(p!!etPc32` zO1XGAj5rdSbj|SQ`dyHg$u$G;!V9gQ95Vm`{oQ>=X-t6Y?~1&c%LN-Ng8_3yNTE5& z=}yghCCEY6Ukz$s*TK(Tis9Dsez^V?0v#9u>W}`{zzhH3udkJ?5SdiKU<8h5mg7+qro0~FV6@ZCN2Qw;=;lUpUOUcFwrX=B*XPfPjmWqSD{GYPb~Zg;M(!~ zkh8i)cY*-JW;|akH3|{5fdC4X53m(^xkVXj_}vfE)1LrAR6L&F7QpDR9vCV#D&7{8 z4$pscZhHiqdCx!YH&DY1xC1>zw+^9KmyzFidvT7Lo77o0Ws8pE4ocr`?5heG1j0^A8D)5f*a)`#Ma2Ce6xxL~@^aR-<-#*PWlm z0Nb4kKsib(15)-a07*?M;+G9&S6rxrg7GIJu6LLOWKf1iiY0RiT|ZhVD3Mr)+Xq4g zvK*kuqET-BvMm>!10XGgA|A8%qD4jsi@GDb4*XPy!^f!-@EO z0ifhr*0N7?W^oLc(%&92rm*Oxf_^9CwoZ$C)w1)d*FMYb zW|C5H4RBILm6gowzcvSxdauTWu1H9QywsNj04$o_L8ZOwO)Xb%D{?K&wsS0@ie4ki~(nUSJ{UMDgi|m=+Z%naBc&Is^X@Wff6e{^K-S( zXHiX1if$|kpE&3tH4!C;tP&Xch(T*kfK`B!@OM4x)uaT5&>JWhf!lsz9N_0}yMA*2abOr(0A?l-{|aa#?)?_4cNc#oR{fvaq=_vGhRndzqQ%9~!J?b{I+$l(2RB_*xR$Ko$ZRhtRU!ZjkU)=2T^y?({MDvS5p>0^;(w$l3r^Iwn@ht&@+ZyyKt8Qz)p*AYMY?=u zj>7*sj6vdenvq`ts7j-#STY4`E7#>Oezm~?1^Y;Tw_S=!{Em-mRe5<-S{j;gj^O*%1H=;FM#t4$ zkPS+1^v4nW$+!BMT_?aw(2azKRt%`q39r4*Hf7j*YfgP6Q=1!^C z<(}q#!W{qwjno&HsDSS3SK-;qW$W;V9ExVruUOxew~ zHOfLsHVeqXYKPIMP!~#kOC}zC@AK_ZoCzU(*EX+9>(Y6LaO8tJGKgHCS;J0#;l}OR%=IlX8bK!(#{~ z1ujHPQ5rT&JgYuxZ;0vS(<$`VTuVuo&7YdDA@p#o_4i@LCF!)9Gph0U`jm!Bl7d!mHb#AdD~1jL)~6JhoToyjcr8 z*gErS%W*qX--eq)yW_>~#JVtk@EJQYPVjN-d93u6x$|IIk=;EIyf}yVAIbK-;ImbSjM(W^dpg!TqNHEV zn6hUh=(dEWUBAOycf4_A+2-yh(U*D6$CtsVUYf7N1jpgzYL2F_klc$^qUC@&wpUSbW*+W(cH$ACz&k)|`272i9YIO9WZe zSs}$7!TNTglJqDYnpL_HU)5Gz@sj~FpG%XmC-!mVhjj**trCluYx-s)s zTx@r!yiSu0S=@G%z*`QM<~K1MJ;c{6SAkD9Of>mtgmGm$? zyKm7x1h=V}K_ZYWV9K-df%^1AMknY8Isqzm+AJd(pJV{vI;Lg=rfeS?F*iNBIk*x^ zcvG-;*j+@Fm}6Xe)F**=Di3Gld&h|Xyg|gX90@rILAB_aV zG~JMKh={y$cT|;B2E#sP?%^qYT@Pt|m*}4+5N=|Ua*X1=K#=Ipka`LC(C(NB!fc~ptiYA+m{4?fGUPn~` z7B_gI&SI2C6pXqD~Qzc!WjzY5( zhfb0HYLP}B76&Q*8B((>u&;*dh!iZC^(|ID#dKVQQsV`<^9$5U6r~judV$Ko2|xm; zxz2sDh{D!V5Tsdg9TWPn5yOLthZmChXb{`8?A%W}*W&T2=X|~6@$wa?!_o`DxKeAS z^8-x%NcgkeiJ}ktRZYoHadC0|K=vTTcz*q@KR~_9D%(dazq9~Ja}cm4!_j?)J~c(G z7>!>xFB;%*Mcam?M?qihS?_5=D@r=lNC9vo5@5=Lt|l$H=H}+~KZYf1Bd8N?YuYri zT-S+=nB|?Fozq`iAXk6iW0d?Y!_0;B$zZtyU^}OV#z=3BKCHn@4n&~%Ms&aI>RHoJ zfprylG^$KgJp%Fx%7U-j&wmQYE^#<-_;;i67~fv}8LCtHhS9%C1QWqUC$87iG=lg4 zX}7V4uOCiL!$Xwc=$V)lV5i?6Hm&x>Q%c~XEQ4qyg<_V9d2{#kCeF?au7?)wV!!J( z#4Qw8Ma5XtA-!_ zNH=I-Y+76O(ufy*_7{1-mkds8VvqIyOa%dG{b^QEBvUMc1XZKwvo;LvZbNuKV8j(z zjpfdP%pxwTT;CKOrNHh$x6!Wb3uk@OGoN+`o`vfy;}+0yZjvZ$7xF&I4HRg z)^9s>SOocFzxyq?BSO$Ma8Vw2kTsBSNxxRLC%7GzBBd;gNstL8#pGS=Lo3Em2N=Y zqwn@|A5Sii*AJ`U8b}lr0owV_L(9E>Y&QQ{R61@68`Yz#sw(d3H-}|@upG|xSGdLY zSiv)-bSYk57rO=FswgSN(lVwkgy&e>YSA20s@s;*thAxkEU|6g(2}#AK|1$(ZE09`kJ2h z&t*{pmxW=~C=T05MPc~1cd_&XA)aHA{67zh4E~`OJv#=}P3=O*GFq{VfwL16ZHzq&OMnSC!o)O=Vt2*_<1T;` zGM&q=2PQ}X(*Ry;2HOm}ShVN`+K`R)^`Q-UY!v7td4W6KjmGo`M+|sI=wIdW>CwOp zxn}6oS`|>w|o%n@h~u3;p};2=rc;8?;nbP zp7;!j9uDZ+1huvbMiX%%Q&LiX0_GG2Z0&psHzb_@S%hjZ+xNYrebSv|aDeAr5S)`A zGER0rs1hGw2!3@*HR=xake$#ySY2&J@obp4hu{A_iD{=fCr_O zA9(|P#CNbur}21*sbRb^c|id!GAe4K)zkI*?rzacnFJmz{x%q#OKJoGI=CnoSe2Op zZSbS<y8(Df8zW(LBQKteA8F`5TJ2!M9BQ90>t6IMUM6qADs_ zKh)~2L|+<)Ag}KncpBm1KJsk)g6ys zoOW~Lxj~zyKCuC<#Nf|TJVH2Nc2O1Igm;|f#&={q(tvno1{^6-*llWKZ!vmtHin{; zRx{z1bY#zYo-%T2lU=m*%5GIS(0dQ*>Jrz5uyENP*eSrhfqXW{qoi#aD8dMxnR!Ky zl{Jl_+Czy0#?$!kczR;={l3_fCsXYdGW7sruKmRv3Ai^l^8w5^ zQc_vPdIyL|rBwOFn3W4yd~b<}2{+GY(di)?EGm}&Ol$y|Q^kygfJV~EZQ9C$<(s3Z zH(`kRGH}Ogf4;cPhZn$M^Wfkf13HF#{8idJHU=&FT>H zX{-+)BO!}hF$IeNzd!-=fHbFw*;lf~mo?c%Bbzk*!c$5own9){&lvnFHsO8 z#@nQq3J3i1vBRNon-0cZ^RNolSj~}gD;QP8@OPVK*uYO{A)WtoRDN)F)R3)RC2_Bh z1*Wy@Un8hL&LP|`zq-GEgu>%yMJN)IusA0cN_M)dEwvr`JwAu~WmYj5We!PK!!%Ih<;)AzXr-e4eQ#W5kSte zGu#%g`s74XMH+J>%|G0$6R>F{&d5iwWfYcqbg#P zUD_%x5pI@a1ePvoUQ*)_)Z*ga>%#$A=D9PN#Ghnk`d`tVoEIsZZwp(-+ble`6iw{^ z*eMcGbr()^Iu5ns27k`RcaH=!7IF%9^hAmf1TkH{85Hq!p2`pVz{ z-zrgxyB0*2!EO`E=!gh0KwV!mYd!`Zx$YLD`|byNv!ZB)+0>aKW(j*PLaK)^4cE^7 z<+!R(!=B_O!S-}|nV2@}+Y4XhSpzpx)1tkZbjOv{8n)IkUPrg#aejITTJKn&3~+K_ z6Qtq?d*isec@Q2|y{GAyl6(O?%Vp`%Ndmd2dTvMzt+9q!v*jpJPk+BzX8=riR1^np zAw&!-IFTkmlvKjIyDtZ;lvm0nPBXZGCviTzZy@~*s>9M<+teC7V(3~qO8;5`B?cwc zfse|EmmGu`@?;_NN>%T5)8;vpF*AKb=S0BPFrqQm5wk8XD&%5euEQU~QstA~b10;6 zOUvdP($TxlP~Myf+ZU)ediI4B>T)CcH&u)f|Hm6E5+N^KEq!Nr-6jWn zdUfIFq3c)8&Pus3o&@1jFR+3w<=?>HK}^)=UxjlP()rAg#SJXsDMt9y54YVjcp zM3B6r2mj)0?Of<1Nh^b9sz~baxg#Lz4W$=zs8{a@f?ko6qKb5UKe@V7`?|eJU^04? z^uaEYy+1vd(3Aa$bdnU?eM8=otdUGw+&!1iH2NEVM6O+^fxSeO4+8_kz~G>mn;U0W zSJzkTt}Xt{4+#24s2t@le;;wgr>a@v;i_2wx+tL#3~Nqj4Y!}eDkG$GqHxyJNweMc zny*_p#}HM+%zXcO{uT7V)^fwhA$Kh!@qJ!_`|yQkI<$1$j6?ALi~IFi-H%9*>A=Fo zvo(15YlbHw+hxBcA)64oZug1<#4Cdcf$F2^GLgd{IYHlz*HWpQFAs|yW-QbxJ$Sm` zUd~P&g`yEVU7wif``v6=FE&;cEKh6@vbhCtJL(^D7^f;@SIb99U>dsraEd;-@(4zl z(IOck;d>yqtntMwnTq?Ss^AF{e~sq!3}{p&-6;v1iWBU@xx1U;Ll9=QY*+nnsy_BL zsn>sxXE&To`B}X-_gzX~ExRz}lB}TgtG%AM{8)qfak)r=Gw}IC-9gG)di`SnW!H4l z$BJcC@5xl2;aomf1OVg*i3S!(klS<`_P$?yKv4IHgx7zWzfCAT$f(+dnlTSt`tK=C z;kfAwoMy+}u*esc>91YKjBkf_PVP?g4BSKBi0z(zF?|cQCCw6iu&fQBv-y$d12#W=foLouK4ay=fwqQf1pL_8ej#^EkeVGTp*q+Wh9)`CW+4 z`C(Xuf}aZR+^89bpfOf+XcHHAZFSYLf6=vRV_qfgcY=u!)^8&M)2d{b)yl=V4&U0s zzS~bD5J>h!yNWz@Y1Y#b|xu9kEE_Z|!Sj5|Y?bH$Nv zKa(>vBSBpsL{yld5>6tZ5a;mc6!m28t2@V^1H#&~oKrl3cJ(fq=`ua{*7on}%E?g% zQkolgF;8#bD!Mdyia+l@>Dp~_wrJXK$b#g&`%E#7#F<6;pAAO~*nq+xAQ)>>{b2n2WNFXVvT@7{2=EB z2%SVL$a4AncnJ_mg)(+ks+~(;Jl&BT2qwbT2_8K<>CdnB|IGIYd#Qc1h$L_L1tnqa zNAp)TH9-M;A;WK%ua{gEc;t1Pad1Kn!;g7L)ZzBO^GJ!sCTeQsLE721+&Y(H!q^^j zEM!G`_(oC8?u^%dQ8(`d;qhM28>RReIX$_J=1lW0qGr9Xa-9DHe|L2(l_%^wUS}pH z(Nea<(P=idlDC(c3xmW=o|owu`Rsg!{PKn?S6(PUL!iXO@jg_xtJbmPW3%`8#xCw6 z8rA2@TzW{AaL|DWi2q5Ev}G}GhaP?`;OlDG7UX8FP5t@y!DjpAn;De1hGKQ5-*B88 zchicji3kz+G_|+}vBrk;GGh>{_p8I35JhxC{!|Rb3RT_DDgZkYd4|x^v#{XAB9+Z= zi6l|yc(iqpI6gLzXTDAG{DNE1XyK3T;N`3GjUxMOifl=!8O3kT=f#zOEApK%Va;Y$ zb-QO4y4m#B;pyCTL!^dUB8lAc`6;jDJ>Wp0^yrMXDul5eiC6@x44Z3+?b_mtlV$xn zVkkW#to@=fuJ>u_l>e}*v7s_7P+9JKWfs&qE>RmZh?QC{0mUAY zRmbI_;^tBhqu|*JpS4wu?A%n3hMdS7MqQLk++d9=YAS@@jN#Kwd1*)_D+@8Bq5%!u z8(mFdvW`=#Ue)X0bQLMoMN|;-+*Vz_*Niq-QX80yY^WuZF5}H&^j?CTYFNSOC??kN z9WHdrd#cP)U{wctt0ovf3+LYZkn;DvWqi*Mx8|`w%p5H6ayfE4rmLb#sb_!JkCYKf z{LgS#L^oT$9?F2wU~0_%#zMq-opavbA3woO3}gvI+q|#VLHFq`z>O~J^cgZ0{o4eF zS&e6rjKa^owBf7H93UXmX-UUCe$LbU7Fj%qE?^WCb>c<2;Ov+naiqclCFISd^{IBH zDwy~L1SXSB6SEle7esvjQ4A%9DD^-*?w084Tb=BMj6K;7*VzT<) zyd{wt>g*H2PPy}O**s4e`}O;A|GSypQ|ugZUg%1kznWjzcwGU0)Jk~!Vcotqax5Lt zl*{9eeCF-GfFQ`;ns)Xi@|2s;Tl#0=3)af5AMqofEsEa<=NAa>js@C8TiT_NE}M$K zruUsFwGYZ)Trf^nJq%UMeD4%LF<3C_LM%k7ZyUP_ux;xcNJ*r5^#-%bQ=NcqrY0xj zGcqDdOX;Tpg}xux(RoG<^K9;GljY%<7s*&aCWc?`MHk~S0D$B*=+vFLb={A1Ma{wmZ@_vGvSZ|rLEv`yQKm+zhaj&t zYTLPr?YuwF`F2`HkYf#hS$i z(dT+wgfXRtd_ok>yJk~Cez>oreLjFREU{J9=zR(=+**O!pGXb@DQvl1k}*^aq@Hvh z{Ea~@f`ViC(IJ1>>3^BjyO6_V1YMyB1rvJOy(4-Dl+FLLhRZ``Mkc zoVC%`fhKzLWJ)HBfR*!*n`_|k-p6_Stss9T9nlQNk9GTgaDj--Xy+|)WwU+l_Y73t z!;7-m@4Ljq`22M3Rc|TCq-m!ib`4rWrenE6&AWwB;|;c22HG>z;kkkF{h!-JW*AnN zYW5ApDq4wYUfTD+v_Nk?NVcZ@I%Vg=43b`VN|5wk@3rKaj5PS3o<`qTFRsCk3+_D6^FhbKcH?tQ6)rAERr z5f`4%KMe!x%dFTQ`#z@EIibNm4;mUCu2U27_wWAI>zY^7_kH;^wBguVo7tPCaG#Q? zsrJ4)|9-^T{GyJ*c5O!s@zL)+G<{>dyaLHvfyM^pljZhWQf~|6gM4frDBL5SGU%i~ zoPT`R&yY)zb}VRk1o_-}TV7$jTC#zKM^>qgNa{h@fbdR3>;oWQg7P%Q>(|&=SXjDd z4t=cj`nw#p>KR%T9i?fU8lc@kKQ*uZC%~K1g)#S5y|~p=R@RPQ(m(P{_C3?N-kPL z9`xOakbMuc)g#gm8zFQjw_)ImgK={4SxF%SrEht|T(`EC`CRtqr&Cf=TfA$0+g5N* zYa35_nS+(?JxE`^nm>#w@UB8XeeE^Yu$=52?Zubm?6y)K=mb2;T!SqEYwL;lR$PLN zlGp(BQu*^Klt1HoUrKUBLh%|8g;241~cZsEOBqKEk8@?dNGRX{Fv$ZHNa!BG_ zVt+_qJ!GKFu4lax{?Ss?rYvmb|IG`slEIJCD;e^_chn;NvHP9faxH?=GS5+~XU;ck z7Jm1m*umqrScaq(RX?_;K!^Wg0fcvN*FKyT^NdrxE^lqc>X+P#9gHVv4rj65xREM%`gz^17vrMkOk^53&3Y*bIHQO?t(5F@sGh^;>+{w?{KW2aR5-j}Nr{SoOcdt6F$3 zY9b~qD=UjRUyFr)v>23@mm2^en1p5yPgzuu;?`Vz@I6^(*1_JgXtFF5VU`7~+H$BS zFpvAe+(*#&g_h+PU|b3;>xH1f z$eZR?Ybhmkze^-tQ}PV_wv1cAzi_NgOg^r&tE;N0P$Hk5r%>mg(bk({mvANqN-(y-)@ZL({p4ey zIqKgaLXdc@&aC`G_$x4qbnk091ujO>x{bOirYy5KHBKH3^&jsQcl)t`U%$lvwCSv( zVJ90Z2G6lfh+8)iZhZjuIuPx22~<{K78j6@udnG@Le=w51(zR?{|htnyMZUrcYV_Vb*3-sJq6EKW7ak zs2XL|u5(>NB^K5od*7KdS}TQSt*WEz zD@0H-`_eUs0Zs2f$;<^1x|(slafV|Z7P$E8ku~Wcbjv)Q)x36m)nOtAMbX1EqqXxC z05w4lfHY#DkvIXw!X;YzUTLF9qDInxH7g7mkF6hK2bDkVoPaI3f@cIx zo4TI)94_Jp4bXx8{cOJ`7+5VbBI%!A*vX-EffVA?=g)E1sfX-M(th8^j)D>=fprDUYTFg3@@B9;iCP8o* zI%yYRT}}Yl1JFXB#la;@E2g-3QBZ)_ObsC=Z7gaSU-tM|kW^aej4X~b^Sil3JUlFMWwLNQEdZEODn65kqiz928I*>RdLd6wZ033dZV4nk>qZ@{?CWz zEV%o|JD&^UI}S7HA&30+Bm^KfPeIbw#N5V9G4)S;e~dkZV$=>NHH&g${lLrJE%+Ld%o&hEqp9>tolSpN8+mF{6i7elo7KIlKv`scQ<-rJOqihrgV&ZMj3;bY8Dl(x3 zYu{xt16H2q_e+>zWN|`AaW9yj{nDAlp43DF|NN&LXZV@e5NL15b@S@4rS`E3A9JvY ziAi{D?5~y^*~3#pHr$TI7~50S@CG)9`tdKv@DW1RS8v3uuL!rVfek99J0}B`4If?H z)GI?YrB}WnPx70F%vZO?>Dq-}esTf4i%PvFbO5~<2Q2nWyW7@AXhrVWeM49HxyzWK zFc*0U?atCsUM~FB@_3zdxRx;X-n^s70;q_E5e)->7a!0F$IJqOj|msU3ZX(D&F%?l z)`oBM@J_X>53t2DLqgCa>c+w3ZVb{IL6q@H7d86?c zH`vlB=71aw_X@kVm4dT{9`&Lks5$gUyjh_x4YD{(={uv6Us)zqQkdlAaX_Dpf|V8h zIl>F#=Skpn)>kP!cnLi}n_yeBgXwu^-M5Kyg}~%e0-A!1=6S#lJ~_~Oue_?3wM!XBI~mpAw2Zz=ZBuP?jE-PZh62=zEJGCN688(@gj)vzGgEzm4$kUwUk8 z>{^#C*`B_>_4119Gxy8#y$z0hWyq{)|G*LWmqhN+bxrnDb3ICuc`x24)?jU+^76t=jzrgz`W(n2XtTrMd zLc86E4+vUEiUlEf7Rfg4v)vLpwQL9*j-!qRtn^DaxUgCl7lU0`Ja1|Qt!jn@*uxjd z=ac<}E9|;Y0XcCv!|u;Q4lfSD&ElJZpBx05+OAE*@uStk$BzyJE#TlqB@q|cms2TWk=)L_;&P`Jq# zl3*mh`a$Tq@{NZ|%8yF?9u+SzwvMok(A{pY^1f3Uvpi3TkM%p~dTcfCa?R=&wZpzj z1+Nfa0ID3fNL*1AZWsC}CiGVWMf>o(UNNE%^1FJ0|LKZ?CDNz-jT1NyBYs3+xtT)W zHBL&f>9QmhY^<&wz-0Fxe6GQ=xtzl1I+VQM|{S`8@0yO$n@zYd zgc-v0S8WBuFR?tA6A9%k$bXqX5`qg;YLQO4OeYhBzyK6Kh8Uq~Tc-r9FdbyL zC-0s?)9~bH>qrD$bA0D_2-1mKWss%!e3m_jyx8snj!nv_lR{BJ$cfTx5J@d$)?^MJ zj0J4Gkug-650lVZ#XI{MFJkGf)yyNe@=8){*Y|T}0@RTU(gJ%cAB7G~%m%Ms! zB|01%b1RWOm`z_yz1k_j+F-lDfjx)lpWk>GF$wdH^P9h6W&AYaV;n|DliyqJ)Tcm-wRQi9W8 zmL4J`rLV0W4xl}hY;2g|(=X?(;u!DP7CDTAn^l7x`xDGh$m}gl+fS;M>)(DIkrOMW zHTb0f4Fis_YO5%cy~(hnYHP2vz0LKRExxjGE?PE!rY`DFHrU!~NM-?zGEV%Y<7~dwL+ux}xLv#iz0BEoI{89o>`NYmBFVhP|1at&XI*mf7p$Q+6Ouv9wlAd&6D-eS>^JqiwWtigH)c9Q+lzM zyPXxXC6wUQ*jY6$LB>2KE4kHevj6tS_cprL8Ar!y39((Y$jTt-R&1369$(q;EG+R{ zy+8Vl_2-v`o|bGM_|$+NUAlVvPzH!WmNN%Qnrl_b%nls0_>d&By3z-wg%#=aZ%;xu88_vOEiA%_|=NO?bsupSmff64d-}U3&F%~|4 zD1bB#Hi+3K4}-S{t2cV=_oJ{%Do6GDprFE8EQ-G5W@+?y6gXGO!y2$yA#Xngb0F3p zj$@N3EPcE$)bHW8i!&sx?uV}v8gG;QsuTgPJ!(ygMQGCeaREu&5HnK)RkMc`Tz)U! zVWkb(Wts4&I3H@o&cGLRi|_92gyFO3Hgj9YxMUm>mdaqR+^r!W#3;GSu7$AsIw{no zDxQDK`jG}#(1VRjnQoxG&dLh3lTz_Fny8uTjE51W`Mp267`zXNT15YG*>>EQ??52n zE0R4bBVnHIPL3={R2tcZ8_a0T4jex~t-OK8IqiRsL~?9*bHucEWO|JC)#GQ)R5R>3 z^!1FrLUXQN8$j^4y|rcXGn)^Bg!H3SrAXfwm2=Rl(F%Yx8f<>u5=sxzzmauMFwqn% zIN2{KzJ50oHSlVr3`;hs_QewSZ^GzSJNiJ_ckaxbj>B3nR;;pZofNX80>^ujx~riV zDKiX+e2c8k-e0}k`y-pMQACCM{9)fqH$`;?Y}^I70<+E)RQV3`J9e7STfZ;nNjPIs zyCv;S$BXRUs_g!#_njAOsj3D7;2?;UI%h6>sEbl&b2I~$D?hGtzEzBSk@KI1rUM*e zsEPJ*$9nq@987Fncf&`HjKL4aXY?a}@2aN0kK}#?<$?eAb_b#5HyoWl4JIht;o>ybUkNP6-A{%w<28D0mKVH2Ru`QRV7_Ck9t=9Gs6N71g z_^Ss84{tJ(&hEvfXD?vsjj*fU*Rb{b(5ll;(JVNyB5+*n;FUp5cUw@N=zibfFy%JB z`Nj?5mD$wChPivo*8kg3$l^?-cvbcm5mOl;%p-$mcQ_^!cq%_QXKzW1t zE0v~kG25zI&9qfR0r;vBQJwk8NmbAQE&{1JXaN7$sdm=lR4aVu0j*OCdFfe^&-wKU z<#@rWMn$(Vr~Yk@ees6W3E2t3$J{=#8Z87&3ZR?n7MlG(zzA%oAZQ1)87(cX!yu9F zW8`Qf;V*!}c;WNmjEF=q;2gyYWJMde>n25qNpo6Q%+NsTW<8L z0Up06VKcprIsqdpmm7S&7~87PaS^f!%#yu;;)U$-_OtRYWV5MB#z@la775Z>Z9M5n z>clziU|oT|P@8S@tq;if)H^68%BYBr{fr)yoM3mRJ#Mz>! zIEWn53irWNmIhsaPmD`dRWd->9*6lKk`$~DvEa%FJ{@}`Ia&JJIuse~bm>T9Q_UnY zGCe;6s|xV2e9z=5)A{^j*@qMj_A5=wm!}K%Hfa&EHxy%t2-4Y%gYDm))Xv!aYrL{* zK)EN%A^8~yrgIH-gZT9&Wx&w0Ah3f;g?v(~QvF)uNmBTi=EK4;1`eAho5^~{kCXVJ zrSlcxOf#h7LNp+3J4t>Jw!QkU21~fApD=E1z2iA5tiT2Pq3&lgY-JjSx8ItsX>zPV zte^FZ2E$Lbq43wy9rD`c&nl6exz%d0-as2=kB01l*z!ger~(Dbh6A4WpP`!5LB_Ou zi}<;$O+;xY5I(V^$XnwS*T;Qj^dP!zik9q|oF6MaXl}P+oDst5tMyqwL4xk*{`c8J zERYH|cM%&nOxxk|Q#QN{wT7x%em4RgSOl`f#qtvA46i#4uk%fE4&E9yN{bEu&d`)G z73QdJb^}@)({{HE00c55cgK+K3seR3!dgcS84DLhv_Gu(nCtQhcV++!Neh92`4pt( zI@R!^#nT(TIfWi0NH$D5z2j_MxUhYdZQLK34XGgk)S?R9bX1o#xdZv{z58 zL7dhFDM|*cIt#2oiY+0{@~Y`3pJ?NeT;}}``ZsbFV5V3zt#Yq)g zUwV<^;J%d@NvQ>~20DH92hE`k=D%efuG9?opc=oqvftf7##0HG_b9n>>u2$2@EBJ}@@Yu0`dAO_*L^m1Ke@w3r zef}U(T8ct>qzrv7nr;n?5sK`>GlDeeT^6@)r0ty>PKM8g&6m9VtiGU@gDsEDY_%Em zjaa^VTMUx64b!wp;b6mDo2~_eOAN7&c2oluoRT2Z8C3pbb_*T>ZAc3j0*Be4#Ov2c zHZ7;>7nft6(!G{w<4Unp8Ghm5<$mi`{^4t3w0x=-G^LB4$Kwh+bma?M-F?Zl+Si^Y za)F;1yGCNw-ky$Snf^*Ht5zRjr<^n_&>ypI6f=K)(@X#siGP2dNOEy=ojF`;Y*u_> zCnR*gY3*5O#Nh9L?|HfY&dhKU2XFauVxgxiQO^%!!JUAby32Lx^mVYdIMIrOCW>*7 zy!zd*sTOK8-Pv2Wv6aN-<{Y~(hdB+khpIKB_&)}JFaeFxoTRNQZT+q8=WG(IH~e(&mTAkRbJ9i-bsASKJ zY<*X;el-N9!1WZ2vtU?f2l|@8XA@CRXU1)xYF&dQRTA4;z%gTBpHEr{Y1{qNeYx~J zo_<=!pFNwF%pE7B>ZvkNGnGvDes9;JvF!bPf28Bu`E@120y*zr?C#s$ zrQ_+59AJfvOyA=Zb*g%E;Qsa+mPp&8^2%`<_Y(U zwui0MTeg4hZA(>y?g@B4FY~(PFcmhI0~U*E7iCjAcAKh=YZ;uF0T^i8VRw2)PWw&7 z@Jl;WRk4|wF~4)ADS3D_U4o;Z4w#b1vX(bNTukg1PPb^polQS4{|t>|pZK znPs;H`mfSp;ek9xV%w>18!)`HIOpp5@Vm}t6_yf_?j0ikex@Zoj zib?H2RDH}F0k;S4bDP#XZIo>I5?ikxGns{`$>H}+FL(7obFyd2vA?q0hkR5pDQ?k^ zy0}#aVSWs6EX!d=!yt=iAi*%YGWVT7aNIk8(gW5f(2sy+aij+|UgA_59A# zzul*_8J;S-Zj5cPc}%^yxV&Z(=k#=Na!O#0Rwtq5E3%EoJ zCYrzj?=!(WEm%Xy9&h`3A!kY73~^F!JJr%BGwPuLNHA?;dkH^_2dGJu)Y1KPxCeSE&CN=w{I9v1oCGM2A@9aFYam1(bK5^@W1f{%7lG& zH(Ts)*2dol%DExi1|d}g<#&Q_fHdV9jgqeJ&M~go>8hp}r2Yss>4&!Fh}HXBn~zy1 z_Ew`dVYp&V1GGKja8z#*;S#U{wSzKPw^tiT31{&-0qY;_&EI5mj8+;Lx&8rwGnV{e^f2woVQpS2 zKUa+|oc(Oc$^H8jjx0fR3V$>K1SBn*>taF$IqBeaHF6>3rqcRjwe1_8Vnwyg2_9BC;iDaSMx^RPUhHp9Q!u$c9F>iI*P z)<{VroA?=1pmUm9Gz^>Af@#oDB4jJL*(SDTi+0BuABrU}|CYb5HjuS9^0KP$(goF`5W^PSbl;?~r)-micTHfTZ z^t|TZlMI6HR+>q$-S#El;6Mtra}~D}rnX(%RX)nI z-HMGakJ`l}XOcWpBlNaCyw22L75KwM%&SNmTFyzb35-8LZ5NoT`=!O`&vH8@==0$D z++ab>!06yzbEfJd7v-KbWvKhfWH~GB#bhq_{gc0vKOU*f;0S!^e7yVB(a=x2?+E9L z(xx`qf1I?DPBTQEc8TTWLcpnmul^){X>-1phdi%zV`D?eX$u<}8F{!^iv!%k+_ckY z*61K}QfB6&wh%D_-}K@jBjTOem!p5@?8JfMpqmaN2>)~WVPZT6vPrR}w@wU(@^%Pw z$tx?rwCc}0aC(0$zc_vd6RplS9wW;}h-No~a3~cG%}R=8si<6}T~9p!f-vzz624#Q zWfU^!J=|Liy8~6eJ;cEpWIqTGnjWtb_!j;@B;)nB^@0S zaM%+Aa(C1KGTfuSjI=J}k>CN}#$e#(sgC@!;;+1IjAUPu`xgBN^15ciWtC&5XF0Eg zu$`@lmY=~yD|oo9Rj?C>?;tGy5v2A;DjQsn`d{mx*=r~@hqA}|>OrVpyr2*kHoy!Z zC-2tg>SVt=*$_UStd_)0B z+wYmFWb`^rZCK0KEmPZxsF(=?!>sJBPCz>04N+ebIoN~=Ota5~HodI)$_lWN%uwQ< zQJc!Oey}a4a6uO1C>1+{t+ELuSky=v`x_eT{x(YNN*&2#Ugng{H;W>i+B@ zzp+0H@*DlU2-))tnY#}ne+Rf)ASTA5^*MI`mlhx>Blyz?A#0$4?|!D);?Aj%ArQt; z){Hff%FW#>zVjG~hn&oamAvIUww;xiGU^-X5$3~I?|@YvPlnxjgrli{Aw|U(#pcg9 z^D_K9xZcF3rbdA59_7opC6dS##M+_^Un0_Y<1hAUs{2$Eh^vEH%`Bd5zr{8qss`C$ z;lXs@U66rHOl)>d8re1(TFEvwSl`LSg$KHk5J0Nu`rO}{PgUmO%9kr(4adPU5^i79 zq@_6ZhKnDP+_O|~WkomNW}o)y2WE9XtM(u>e2Gm2$F=+EPpKGkC`(`3Y^%nA&DbVnhh#vJURQUND=<~{gDO%XZQ2*-tnSVJ2 zBFI&=v+}n|)bLZr7OMX(e(a-f14PwRAd&IUbhi9*lvK9(?*5@LX?HRAPZH4ZK}pRRzxQv87>{GadLs`NK+k*m>$h1Za!S zq-H1}uRsf7ye+UZ2m-~r{y|>-k6#VUWW+t09QX41}7EzFJwpwx;2!> z^vRbA0^aI~z(qp@6kz)eiVo^Th6RAG8cqNWuJzR7f0)WJT$tE`5u9_fW=XLbvR(0U z6#t1BMuPw25WC@p{oke{Kn#7>tzuz_;e*s7Oq8|)k_;Q!?*w2V%mObQU;{Cn2|jh# zKShBw>F?V~7N;2qmY)$HjO~Mwh|Eb-4_mliauz>$#9VBZC{{V5Uw^+GOr`*40!*eL__RwIG|(KN+DarC`=y?;&b3RThVD~pL_)&?@UaV zfX<*mql^SN#=MIWwZjIFPk}*z*enbWOjaFOB#l8)LEx9emLUL)V9RiGrjEt$Vcx2M z>7szi_T-`06hNaMZs9{EhehWsg)k98Y z6ihkQo`?ppK<=a$0ly74Ws{@wbTzj_|=z73Kw*x1~-{2Uf>olhJ zfA0uC0EkjiQ+5LVbUx>p7eUux5jhje8SqASW zN&S<7QBq&|eaRDyf2SoWD%?TluJ!QdLmc>7<6wfCms}dzzW)9ao@%ud)Ygd0I+@UQ zhfs8mN&N{|r>R&}LzjPnce{Y4rpX;g5&O+fC zMZqfv6&V>EFj!-K)sRA=a=+-sWSlWq+gg(0Znv{u-;zlZu|EbMMS!iR|MXEZPEMQ% zJQk^rU^KfqrwOf`p&cqDRVoXZ^04#cD? zaBFR_cRoj(fow>*uO@(RA0fFWKSEf#_wD_i{Y`&$Q(?BRC~9mfT*T0j90)mpmUgUo z+J*IqUx=fm})=qzXKI%5VEaMSGUV=?CPJD4aEBLASmgEBVLeGbYl43wR#~2 zVqhXc{30V!88yov0mUjlmlb~YN8k5I?^barg-X6}qZI5|EHdNfXmG%c55@|@!NCm!q}b9irR9VU4og-k zFe|o49Kp3c`N^cSIa2hCJrQqZW+TE{=`!?*T&I=2P~@V0l?3ptOm}!*O|W<(pa)2z zw5fhyBE{sn5GatlUOy@c#??C9$)A9{Gcz-uZ*~LQPS3$oM~iw{6VC082ORFLJ$6%c zhFu+U!ib;9`FIrT$*1x9oQT@!&{daKTtli+GI}PaZd++}z@A`ybUb^tmh+Fq-Wc9o z&>mA0gA1~yteL|fC;@I>zMIN=9@)#a>T(q@xr7v8hpeHp4sfBxHhdM49V4tP^zvqT z*(i<24h|H+1;BSuHf7sCc|`H+@7-gV^}D?a7bic8Mse)P>pUM}e`QnK4{TBz-PZCJ ztj0DdamTz2;^iElBV&9@c_kFI&D@Ayp?qr5&;8G&-o$l*3&^>;KQr@y&F{rDn0f`I zt6#C4Y?>E1c*Z*b?T#|-GI(R|D3H<6Ac8!Mq^2gmR3yIP8+ z$bR7tRQ^i;|A0hUQT)sVj4(D6)ldAvSiYJ>zrn)PLyC)wgJJKWDgX?Hv`U&#NDDdj zP$U=^B6Q9aX&W3UKfxJ_A&eMF3Us_RM&=`I0BJg9#Z2^x#n@~`F#2a}Y8(GKO;`0o z{@wxTn-nqmUx8m9058V?k(G(CI~WzZsrN!Z>%0#i&SI^${(~g$J|D;>&qqePcyo<= z6m$XLT)JrFU)rCpgDTMc9r>kt`BleV{ZAXk^Ejh!Zal%85b?*4AE5Sm-1xwlrq9LC z7*xwtG!F$BT{>4P;k^N{$^Wn5A{~OEz9I9v^&RrP-ldTJaTS9oI$CK6Lz8|)Q<*oa zJjMNqmF8!Bfp}-gK+ZG)mogxS*NEvm@+WtLYZ}GZoI_M`fmC*Tyd)xlN#`5mZp#jc zdKkZ#kl#ZV<3*rQ7gZFHvJ{*bMp)wQ>~a*f>iD-rUP{7o)!3IxsJXS$&rxU(K%q;V48z*>AFoHW2bA{1ZyNSf>Pm%CrOE<#jaAc(3_&_Iks1&F% zV07oqOqfeSv%KoeRW;}@>$0ITJ-87cP_bO*?G>AG_vW0Zn+JAaaQ3-O2jDt02HORQ zv;HqkVxa|4EB(jp9Ua0FF)3;nDUAy+v6%S>o($r{r+l^b&kK3;Hr8Sk=R@xMNkh)> z&)Ui=6f8%D0&wSFxrNAFyN!H9u${~P^UbGbOh;Qg9Net}aK$J$XTHjF`m<6OL0%_c z@u3_Co>a#VQF7kG?-5>QQ#@tJ@1NyJYCYn;@xC!j)FTH`5OzYw2Zbw4!6b_!)|2&A zpNB-8xfO4TJa%oRzhAa11ZA%)EUx9^UAw+YiLb~#U+-Kyh(wj0MBrkTy?t4X#Yt$? z!KWqPf#tlMYTK&Zt}yF)JP_<8A9K1;q=(w->BxF>v(>(^ugKt|;Eg%HBNWz?kH2zz zm>i3@GEY8tc+N8MZ>*|*tf9k_LPu$_4=i)Wo>aMGce2liOKj|+5%Hpom9Vc?v8!@7 zlUEWC3p-e?Y{wyGXomnKy#GC;t^*NE=4r_M-L!(X`Fq_W?H5njK6i6osOW3cXMxc{ zFZ@}VdmX-VxLbvK9WD)9_Lo|Po^C~?IDE(M9~mTZ$eZHnNyC>(?MQULJ)E zlJcK>rmhkn9UI|P8);Xp$Wo_Cq&ISUq%qU>c9l{V9o}~PP*Tz~OdAG24PFZRZM)KI zGogbio+qRN)%Hf_eFQdqLM+x6vYp?J+$oaXORtU|Cq~E8J5c%kq}Lti21Yfd?j3(4 zNeGPyOT9)=e@z8Xz9cUBgNYf)VcM?*zhqrEXR674s(C+gW)Wt4?=6TP7`w8ei$d`=(Wfoj(=kQENW52htb1?2jr&ThO(Z%TjVV9`A2r{dD-ub$lvjm zG%6Tx#|Ga|61HW3V%?c}Pqs&L!o%0)tdhZ51j0K-LuA(=O#4BYh$kP)>%O7-w`^ z`WgRjT8z3aw8x|LZ}{qou45<@|9tDpE9Btjmx{dWmwSRf%Eg{Xv758JZ|5>T*Way? zLWwc`FIi@NQFNpkOze@v5BB%#R(2a^4tEM<4}O+sKn}`KD%52fY{7^x2x$Ve>dksK zq5I-up`-37*HhVAQnf9iR6SqW%%r?(a@^mFG^m{zsWG%p?~}jj+^g3jyGN*fzVb82 z!rUy`l2gGz$bO&iPYeSt{p{iLu>)5Y+R%wp^nKeFPM*Tuz|qT3WOoW5Y^C*72ph)q zjhG{d(gwsvy>)c;-IDhOBR!vO@77Q_Xl^2UF{WXnUSYS=T}V<_hrC&FN9@na#)Rn7 z+D0T#As)XMvo%4&ePR<}9im{oRVjF}dFS_&iMIV_AjHnDsk=T)5<9_0I6AhBrJdj9 zyl0_wH~0^*xataTsjt_Ir0C?jj}298Qdn&G-4dG;AH$MxAHB_N6E$e3ta&2 zqrI^h6Pjhu%B6T?>)}C%YsKr|Foe;SMZ&~pxBjA;|6apZUajU-Rrg*>3t^%}H$1}q zT++3rH`+j#xc=)}k)^Z{R&4~5T@6aYFKJQpw$n zy71NDt8U?qW?yT>^o97aFsracTRo#fIyJ%^u^2SAeWmwxfyImvuK_VyQtNLoFkOuo zbR*5A^!zidjSiM0qrzSLdKo%v&u!EhgA#=fnKJObV<|f4lkXW`3{HMgJ?{BhmuvT~ zfTEv|Ge+XRZRp1!B^|VHWe=k#{?=;;)!oJ1KfW#f`zm!4lz=2smY=h$YGiyRUWcVa zaKf?UOAtSs;bzcEwYP5BqU)wU|L4ML=*YSz4ZQnMqVqvXvo`l9v^)*UGdv)WLVz) zp{Q^}-jWG>ge#rKgF+`TATFIM$l##ja=3*SXvVWNHbut4(vAP_-? zkp+coR9gklH$M(%E%%r6b@n?$Z0}Z@kLDMtNuTm_(N#&z&;h9A>(r_y=+zyu?{T2^ z%`8xo0ZW<4Pk$)V1RS6!Tx~IwVKaR}h95*|`RbbCCiHF|84=dFsHYeDFsJ%Y{v`W3 zv7GWL1Ga>Z?(8cnVHi+XUx6|@%Zcv2T6!-`a?afInS-31wz1l(fX68o3VpA2>3Y*0 zfi1ffkXfuA9T`prWKR}rKMTIhkw%GIb+D$%^og!K&E7m5mXn0?F$QLf-u0%76v|dG zM(+lYPTk{m@b}oqf5Qq7+UMJgIy}Y%eFWnnM zf3|P~O*G(zs0d$hqwg<7T$6a9vj};!{8tE(KK}gpT!DxDuS*Q>(qN-)Lne67+4N2t zg7+LL8Nfm6`NB@51ai#LxOcNWSu}TdL-%icJ!}_GF@&WjND|4oX;5O7qduT-fX>5ZNDi^7 zFi|(;r$!dzA9&FNVnuK9VEU>l1ZtkRG&8)rZ{6gNcLBySRE4|MK)y~)ucos8#*_g? z3kQlM<((quIE@P$4eG9423eeYm(f(ComW@DU*XpIoj+HnFFzOi zTz;pic&btvE>NQ2blL9j8QHqdE_ZKOW6$W=wS4Ki)q0#xC4X;-u=ON)0UVJSGO^|)DxxK=V8B^jH@{~E_&kA)_7t5+cy4N zE4a6skMY12Cc9m{J1mtE{=D)P;PIlKGPtJqo>|k(VaH$B_+t|J{R>N4x9BI&|GG*9 zAu^1M_UK7*8oSWgcAc`2-mJ>_X3&Tb`41bC>k91a^rv#7eQsk$!J{A*@V?*G7>zLZ z9WDo@O{~7mvUy(qMXF!QPi0#_Yoh~q!+GMTWAWK!C%CW2AHt?oi;*zc>$#%ip)|(I z+Ee-)zcD>ZO<#P?}xK}m$Yb?t(UTu`z% zs&rp~Eok%EyL!jwJD<6UtyCO+49+0ZNk}5ixTSJsCb|4y`Hv|CNa>Rh`#yf#d?l zkf1>oFBYHL7Y5b|^Ks-2kQfEB=35^rPSoDHDwCCgPMiZoqgIY+iCJur! z)YUBA01`$_zi(VyNp`c>wErZG$DASqf zTq<6_XoVCUdJ#kP3j*8lZRLE_?I-G2_+?4n3u=Dnnth{IHG-_S#x?+v}!$ESnCiCyq`zW?b`~*3e#WFZtiR?OB&~6~J{qhAK z$cMt8pi_QQgU`maDjqhq-ZoIQ1P~e^EJtI*!;zIgW|r}!6mE)g3zsqWD}Q*k_7SSq z=h3V0|7T)dZ+he^ie6FF9NVq-;R?w448)iIMWV-3idggO`c92}ecLl0m2|XgL?l#R zHf!SgPXv9Vf$-jHJZ7@Nnj^z!)kaS*x=62n$gB1FwsTJr)B5R6vv?kQVLZYT;QM1g ze-6N;lp+J-zi~SH=1{V}rS=nB_U|Zd$9Br2sl!^WB);a!zZWP7Mb$i_biuen& zf5F_|$LMOOj=EFOA}tK}bwRsjc$lXjOe^ETc9)XE22i7Hlh)?aQXj#RASsn_$f5MM z)LD}-z`ag-jjmw>s3iC}6B@{YSK7sG;OXyT;E|oyZcs|%?~*?#W~Xe}(05QmIIg;F z?IJ&S@>A=_3*2nJqrX5oYLy*xOe;acNn02 z<9Ro`vK}Hrw#B=A`b9tmJ5J*j2T?p-b7}f78jKZ~B4Q=woVSO#y@^I3$VJ}^oz#mH zqhz{T`lh18^)xvIh_iuROW#3FAD+gxO|x1PI&@;yUPFc@ab*xUfru{L8X8(q{^*1F zPGmxZPpPxGRsQcZ0k=0uatk0Pse_o${mh~1v@8A8*32?HJtpE0plH7)uKa^p??V!+ zF{J4%?B6{X@&^EV=Z;A#EHsWs(m*JUvD@RqP@7YIqTA5-;SiVG`R5v#8V3K)M;E2` z9ZusW@jMTWKo-mL(W53r$gUxkcH!zYB{=j)K!K&@01`t^LDBxv#_pETf6g+>z4Go~ zN)7+)pkd1xbcP2db{QtXaO2Q?;hCMOfdg~v&EUIPpFT7m0v|cLvqhC>hif$Zi%mFf zS#~3nGRz$+pFR&daC;w`XpqtH%W5R9j72H767`viipi_W%|e?hpZBNsd`7GIoy-0? z=))hcCz0Jh&eWi;7bBc9Hi{E%K)rFq-|8@;^mD-(XxyFCwW5I&@Auw+6UJ2Z@77II zguImu|8*zi?UklcCStQxskDO%0gK?e3&U;>EQ^;>VDYv5OMPTFD*bt1q2oKWgrJaHup{cQsw zvKMJgzM5zQNuEpdZzcBtxv~{6o;==$|iy}W#0h4 zQie7WX7318k$w3V#i{@7hx02JO_VNs^MqAPBc*y^j%IRNK?>VL4rm_e;r4kI-h>9q zH#0d7JurIuwnuXxtpc}Gy;$=xW)RE~kpemD6?UMM(s{Ey<#=1DXhT3og38Y2BZ1-{ zcwJ-&*&z|ZU9dd02qnym&mMjgM0KUitINip!2s&B<6I{cDXY*H!F-s7P;j{tcf z5x7hOg}u9#rkeNv?$RU}m ze9O1EWGLvkACWxK)~wij7gumMOiRHkFsX>?ceEfE50Yf|k`MXEOUR@Y6j_@iILG`S z`83XSfE&mkTBtW9b9a`ayU#T`@p#|xT%GU7fWUa8c7M@jr2c)h+sRgzV=rfeE6f2wQIjpMvDJvF8p2HfB3L6 z;UZ9ql=SpvTwsdH+mm2Td4uG0ML6{-=meM9uV24l&Vis11#k{TP1etyXah)@+%(w5 z#=Z9XTkk-EC5W}GKms#hP~cPV{tZA)!_Sp2#SJxYKjn~(-YOdhJe-W;U@DwnUlpWdITC@uFw3=^;;9)dPzy+L0$P*uUeoM~O zTF;gJ4ESWKa}N+h`FsMQP;)x?qBmd#+q-v85Dx%#!kH&4a6bT|jn_b@q{LuIt1g1+ zZ-nzw2Ex~E3cff4Wm3nQpdyUBBY0B98$@$N!TAH|9Rh)51}GJE)&^?t^t5C{o^gEG zvO*7!kRc|u?OAJuvHY21D3`UsOjdw*nIiD%d%~*0354vMc|{(b ztiOerX0p1)LBQ;G+q>nvR@(_|BuV0?p28YYK)L7ulv*Ub%&Q-0n8hlVE8e)h4?-9V zGDzq9$WL|z$32auIWnsKUcA`G&EoUS1b65BmlWWp2Q(%RZhGK6W;(_^VG@7ZMk9oC z?o(emZg={365e)UY+GN-miME^8iDUhSs)pFyTmWWxDqz2X^63!#OE>eK2Ej1- z;PrG*|(7rU+asaVbq!2oIb`_kIFQ6LvKtRx)V33}x7}T#@Vo3u zz)Vg+L5Ncy@rJj*8Mib2%U9oBx!=k<|6T2eTeA_(#pC_VY=*q61kOdQeJ6lF|851- zcT&5_iD_pHkwPT!$IA+LO-=?;1{g6q1;q1qpH$$sMFLfs9VluhAtgm2AfOw;%(B%1 zzFkMEM(Z96E$$23-V3x5lS4iydLQvs5}hJESSo`RW+}-^2~+4^N@&(1{c6BuiqJoo zJM^1u(W)Iu;649YZ{X3Fz%<$F#ShB)_=5V-jkY2|xTae7HzJ*N{=??4jc4_yKXo-q zrjvENU~!npJlax$ev|z4U{5Y02#eet6(0C~A7xJ@HeFlj&cWQ%f=u zzCLt))y#PCYX4yD=sY$(Rms{ z-HNQAK2}c_yi6s}8U7aJKm3hDwhcsZf$Q{v|A#-H?hTKQ&Kcg#vR+}$&Tn(R47WkK zG*;2SbpNGNU$>Cj*q>-c9$)gM^*=rzHTTndRrW}keC3A*Xj`#693Ae|ZIiW^=3qS| zi2N0E+Vl!LMGu;?M2}><(WoX@ZZZT|M)xMZ$qZOfew5~|tElUKNI?0!@`2It}7N)uVfy;-fq^8|v4Hkzzi zoPcPd41(V{Px!f+_x?os52GuKZg7&5lGKJH9llG(Z$(4MUiupP1iXjK$sdCoW^%{m zbjk0UuBgr7Y7B*R-r%XWw!%nv~kwZLI2hV)tIbSy+5+w^2E_Wfl-H znBxV?Pn4CFEs`8(f~UsY(k}TwCI#OjeZ|C8B_1%QpqGQA6rrNHn2oD3$-pZ{O-iFo zT_H&qULq+ay~HClgM{f|8nuqr3HAH5#q0}b*H7i$@vn| z3Ah;2M^ZqDDL=u(VKJSY7i}H7v&}rI=!w4b5pT-OK1#?;*#)Z62yj-(0J=#)>Bm!Ef`16CVm*D7~l8sOSfJmJ?VlyWaaMAKUE{FE+cSqI+rAa z?=;dhvtQEZ0>7AxW?l{>uxdCHN}6WYi=BgI(1Ir0xM?>@(IH2piU z0I#?q5#fv-sI6rBaLHLDdE9~0-n2j(Dr>C0{MXPn%f03}Gog1*rAgu4%Bv%NIju9R zLuVxoU2c%&tw2;O3!f|ySu5i2V)h@_1N^jk5QpksF(T9U9x%Luc{G6nVRmcr8WvMd zb_P=V_m!r?X>AB7++Ba$yBkzqGgFd&#OD<&FjHl{I}mL?S`|-vX^iEJ%EU-!Pinl`Fts!A6HFVaUXe4U)gXKak*0S z2zAVjUi~L~3J77q+(dvnv&LfV&svG`=;{7nMEq8NSKEd0`Z8O~4rH;GT>qY!u9d@U z+pZJIwk9SB3JS__l^&z_Tsg*LKfkVpkicMa2975o7(3#r=;^a5+Q?#cA1c^k=bMkd zc<`t!5Fg!Z9GhKlA0)i<7p93k8lRFKK)7w7J4Zf~yl!z>HYLw4JtfM{wyo}WfinZk zhxZes=?wIdS0cJ8xI;?uR7MD*t_O>vFk2~5)kd<;X0F;Jfamnx=qZcx56M*Y?e;Il z3aJwj-zR?%qtZY=`{1vPYl&jo*b3a+>P~-SYQ5UD*7dR`6}a!d4lN`_aoa6P%oKA> z$7eL)!Bbf;>HHg~;`%oX(a!vy1hwImS>WiTFZ7$5j%c;_0I~r0+2gfq{Rxcr!Y>k$ z9+&c-*Ha#@rtSRUta3c=@C%+-Hittq_w#`uy!BLvtE6Y3ZXuvzt+&Es(hDENU7ReuO{%-|BI2uP(8PbL zjv-YfCCFNSEM8zx)Zo7QvXUxsO185+2W2b_X z&E<60h}-D~*I_)?HAjnTrd-Pf;x*iiT)Pm~i*)H^tD=MD&Y0mE4@J+Lai4O_83UQQi~4~21{brN{fK?cnN=)- z+hN+5R-^F7nU@&d!uGayUs(4Rg#=EAY1cNsbgtlkFzGY$V2L5jttRHRcuCT-f!lnw zhX*ObER}e^<}oGae)5w%?^Y+%JA_=2g~fp}+AgO4nzVj*o-K**uefAd>4lTu%x{^x zw_8fd{z{0OJD*^$E_(8}e4VD4OXx<|_|KSydq$$PW+**^u3wLimm4_PJf>Hd9fD~TsTLPo=QgKzwP*i)WyojJ zEU_YR@?=?Ys3zsSVq?@hP)RmMDpIcMtnccv%iaIJN325!iMlaeo&>-5)prVbIU2}{ zibHIQnmOjpUW2lJ^{_ccXLN{cM)o&X?-DXdRb?fpgBJMlBXqRX%bPG`yvuys!18`< zQ&CYgiFezOS~+Q!lXjznfng9%zSePlV{N(CPw?PRDrv6iJ8Y6xONc4U0_5ITMDjaz zrhs459j`jucEL3;gJtk%72K`HdLKtYu@m;&WpO|;KaK$+sk=ATfDJ!uYEtV??z;?=dua~C1JOYO#gV? zp8MFLSK2e+u{VCV%N{X3wOHQnxo%KWnWH!HBKwrA5!mt79>-5pAG3lgXa8$_s&Y$X z8^uwu-UI@v_w{Eid51x4dd&6#pVBTATTe|!NZwwjVyhbb(saD+!4;wbUD<1{;jg5v zh_;?5oEe@6rOU-x(f2MAkPW$eTm!j#$uORLs?5$=0u^!{!Tsh#ZE7)~MR$^f_MYca z2KrcEwDebmk~xw9pOHq(sBzf%fhqd}OxgN1^L)h(RUzsG2l70X7>^XbS&aavXxyz^ zksa4^hl_90)jx;_rTE3EF|n!=K#@!k7smo}$O4tG(abiREq7}H7!3k<-w$8v-JIvE zU5o0z|ibhELMdv{ix`LVwsoQ=!AE>25V?3QmN;e}BLmI_0KTo&( z;XInD-s>%f=k3UwYKxi2!nX${pUBp~;-uQG9(b>DhHdHDzn5*kn{E4;oDoc;pAz~* zaC)gotBwAiMTc8lOzdzlal{T}+}38ZJLYNbQ+?~7Tpvs$erBt@S93iai1}S9k2uR! zo_f@BOwgXVa0Tyz+L+&8LHo<<&PV54qz?SjMaW)zw6#vXiRyOHue*oY9Sqd>vhs}% zaE~{d3Iybd_xd>3LUG4iRygtqTLLxTi~i(LEgfjYsf{GzcSZ$u*ChazH(aE_GC`!# zkTCDBWa46FTv~a`0wlDCf6cQ&2T(c0%4NnX|Guzar+E2(TnzI~-agii(>x>f>qzXv z>%&pr`eG-KwG9LBl;{o8dug^~%WKr{_b+aJ%Y@yeUyD$#^^&w;LZMj@vCGtQ&)))) zR5G!0Ihl(02yCZ>z|)kZV`smun_HNv<3?*FZoe(PN_VPz3;%N(Iin?GUJsLSo6sF* zvCcI}F<8%II>x~!!3pJCdQsY$Fm$Rl>=Al;#?EK=&BPeOmlP>=R=HM}$*kUt^Qi*j zzfNjT5_y02{`~$i*2TCdVPGx)+F3#6uDfSpu|y+79SZ`pOwF@1j#G5=I43Rsf=(*1 z%&hFnmbhg;j#tO)bi2pSf_! zGSovYQXB$iCWsS3U}E|fc|L;&+1Vdz`4kSZ0whye=VcJKV*oVn-?77(u1NT@3MJ(C3&VZoIQ-&ezv8ntma+%;YkGF2K0%qtv zk=v8I!^x4*!U?6QvjrxV#Vuh-pg=_)UqJ?L6n+OYr$mVTbHjM3;F5 zstuKQMZZ;y_oTEfWbC^64VaUjAQjesasJ5CL_5JXq~sPn7msK(#ArFO1;vO zvTnn`&UBa0c`FQ4Ot0Kyu8@CwfmhqSdiPpCfVRU|mt(ZHbzey0q$^3U=C$Z1J>B?& z#nvt)NgryX--esnd`ti0wjQb|C?L}ismcEHbcY+ltv4cyX>6%+qiwTwSoz0GcKQfi zuT=F<7{8g|6txV|NaV?qsJHnWhGAdIV{Z`BpXd1Uk4kW6HT8V#T;T0;oe-U8)f^uI~ z9BQ_@lGr@Aqw!95@*by4uFe^+6Y-LP579x=LN`h1Gc$>=Ab>57ikO{warZzsXQ5s_cGHZ#gcI z{r2j8X5pJ807KiEs0zJMobYioLt)teEx;)ObCEU5CaYd9{zC(98CB0%0S!8tR~4DT z?}Dxv-5}fWQ4@VX#_I})8b#MahJF6d^LZ&Dt7b!5ee>vwN#1qBbru!d_cjvzFy^_^ zE^$Qx12Jgp)d^*n)+<>9?VZ^es)C05ggc=yXz$dg$)ArduGQ9_RJ>jY$`FDQ{+gSe z)%{tk+~6T!8?~iO3pvf&^U9nrzVBs^br2d;J1d0ulK^N0yAean-FRl11p_-DbZWb)~?07;9F)(6e4v3gQU3+7gz zb0>W7T469@`D?iu>$_o|X*DjVkB<1!61r68dfO!tF$5StPkuk9ouuNXl|dh-uS2Jq zAQP)SmeKh0#!#uTZt9fV@1@|eC7my($3_n+JMjX_Y=W7s%1ptZQ>tnUnQ3+-wGLU| zEUTdG1nV5dQ-<9R>t|{SllI4}^sl$GKXukp@86rgtjleDO=7}nLBoo|-?UASv~}F!?Aub!t<+T)zx260S?wM;e@x;la+h zOv2Q9yfRB9{-b3JnKVWt*9XVlWF8%ZZS^vH_1jB)RfoQOlPII&iAvD%+}-BI$H{At z@Vo`2-L#aHFv$HbfDHH7>mSa0PTgbUI&B=2@n~y#QyP!o)zmEYPNn`3`pBPCq4&YUBJd z!^y?DRm=oRsg<VLHxtWw9eGCk2fp5hVG1gc=yPH1H#N? z8}3;VRo^u4+E%;$HOVngg+Nx;X42l<((u)WDB08Zpo6P1_;zB=;D@jt1&StlcP`i; zf=7ayMGS#dY@$Q)R<`Kw;cgoZ_pbVse-zjqS^q6Uf~jkx_`=grVUv5rcRFm$v01Qz zzb$e;_o(rbM3(mzA)y{zJq>j)3Gsz!P?>S^#Lk@bt>(5iJ`I+)NE>In%a^}L5A2ib) zLG;M1RlO@E=A=fNM`@jBCKm#HROxC=M$IK7$$Ol7tnsJ%^d8@&aJ&tmhd~a?rvQ3I&?JHcVjqmijS{B6?7cU&LaX9fPltf9oSIQrmwMxNDxcR8L z>YJ|+p4xZp*-lK?=^z!Hf|kLzt`8lG%^zd=FFE>$b*7GkA+{|*`sj1#K2%P>6w`;Wu~HF3|eBJ8g@y62m6uBlaG&aEw5meu;RDpTc7KLtJv4aGp}J`gFK%!xPKvEj{$unsyOQvzRAA!4Tp)| zY}qJQ^+&nRUrHcMD?aN=zc(RCT-DMt8_?^zI9V;y0X(ydT4q9%U43Ld5HzuVnEgW{>v8aCFkP#fxT>S| z*{$2Yr%l_v^qL$_IEn>`nLK%Af~3xB`sacUn)e5yzYXg&m15VcbdkJBw&Z;<3Qc|@ z^lBoYURv7(*c0!iCd%O{oYvAqyNl%`SLlU2LdZ~JBz#!D`i7;o~PUzvV8KN-dDI#X2i zz`Oo|7_vWLF>a2R^u}-Ed6RCvmB#0h6hc0NR6XPS@t-+;t4};yB zqd9(c%{^x5^K(;(AmyUF|0C;m4xdbNJm!woMEK~Dix%Un<{80CX1IZkhz*C7?hDo| zAN$+!Bhz~PROg6x^hrPL9SUZ>Zk&+X8dsb|a>tt*m&)tWp-5-p!hv=@v~q1_r~h?- z4$r&ptSzogXgc#1{*eRsVc$LHMQKO#dYS;A=Coh|X=$^%bWQ<*e!I8VU~;8khFJRc z`eY46&QoQn226!+h9z(f-U%lkh`d?PIh;|4hMQ8pKkcZY0Mqwq;u3bWou!m$J?)HZ z!0EI3+N_=~y_AJC!w}F*%68liD;%Q!sH{_`nUNl~7<1uEp z>u|2BCnW5@L;?jM$wA(z%=9T*fBvQQd+e=+Pp7A*rkd!mVQ-nj|7J=|8{j3OKq`QR z^-+j>SdG1_xZ=1n;}l*v$PV?eAf>i5HC*M8Vc`%}e$!jIE_o)AXsT^7)@JR+iY9%5 zDgui1LmEH>%<8En+;TU>l~$kouha}NVQY5+oB;7SL!CiBCd#BaV;%$AdxvXpF9Y4x7}fxU&v87mxJ26!1Ta{y)JrGnUlyD$;@HXV*uIt(AO|? z-A@HSwqMr|QHzuf zL~ea2&k(!S3|124(oiNXe11wOcepI<0g>bw+4*(z_a5SRiH;;h5VJ0t}q50bj+x1@QHF82}l3Uj5tXyz1SVlmCplvFHQMWFm_N4) zF&}zv5|YegY%|g1Z}&*oFU||zA2@KMjnY&Pz+HsYO$B{k)Y;?H{u%o4wsLq`oVoOl zGFZ>!yJYT_mCMAljM0fs9B!s0ZcRLDWKhFLC`lH;R0UF)A8y9V9Tc-H&o0+xGLu7$ zu-;DqxwW6MHm|-}D%S^|uSWPUDYwZ$Qi~+9Du=!>l|8ZBqs4S5L9V9NjzNg?30wy~ z7dUg{{ZmRmW!78CsIr)oh=fL}oeCPVv$G%V?fIDdZ-0qnlPEQ=_yD0#<<(rSejxZa z>#Kf#lKcI*N}aTlJO~`9fC@wfUS|%Jl9zjUvzW2A@xLh&*A@@q5}ozKiDPyAto16j8Wtw!D0imtZ*ll&zQ6LzfXiMtDfq$(TvMfb6KR7;|Xp5J1P~^PTbhL4q*Q{2{ ziS^SZl8}V=1}sq?3dTD5IVA|mG0ua$AwOEF-EsOCzh-S8E z7AX{ltssjda4a!!7{5DR^s)wu>A`47aB%nPxpojFPAX#?QDOv1&i9oay-H~lMNX+Nc8~G)g2%ephnF2Lks}ei zb4{4WcJJ4?GFd9YK#lZGhnKc` zl1}aYl-sN$YIZ!uEj!@P-VYu!Kjxq57jftJUZES2&w3TDX|VntkI{f7(fz`e+9;`2 ztxSRI!3jYIn4%k(`EL3pJg$a!&qLjWA zGptb6yl(h2ok%qO8;7j&cAtCdNRpx8H+kya?R#|I=7+h)UVdM$B~(Z35#V!W#JEr; zN9V-KY(ylW)B;kT=RbYrlOoy#GI#cTj$L$0)6S({zD`4`AKOcn9NS>Zki^Jdr{(76 z7I~|DJi5Xe6@;z zn<6l|?mWY6cn7h3hj9ua3Jko}o4$}8Gi-3b5nG$uG2qCO3?ib`ddKLS$|`+vWMxOf z`&>x<_@i)FA6(I3=Qk7Mr}*ebE`ya(sW}nz?y*Me>Rg7a*t>;O1+jg^aT4eOa2gW% zP5v25`_w>}rTA!f@iC9Y4K6-H{Ir-r^!wRpY&HicalX4z_2S1bT;35OBD3i~t?PpY?6iZi(W+4g zx9%L)9IKS#((_A+I}C>^w?on9hkRR+k5eY2KZg`|&)N}wEhFg;FN6byE{(N4X>Su?eb=$TVKlHzMi766mxzSt zD`VW@G?c*ww+ltY>(B0$a73?rrFoHq&Bb^*ZS9E}(KhfyOZt*1HQpJaC@)$@&(7Z1 zTAN_}^l7fw!=W^?DyYgmH5aWY`zqWhY#3o(RlcNnKZIZ?3cm1i{Vwbf%Vy1m4Tw;< z`SN)(vJkLj9P=})62>AFWB5?pQsi;|v~+o4w$ z{ByvDX^YE}mII8}B|x{~e)q0t6!Al1c&DrK6T7YJI4tQ#E6RFgvm&`;UpUn+)6SIZ zekn0hjFM9PwOh3#3zu%S{LI&rOE&=vXiL_1M*%zKLM0d}MsO|<6veU-)bRhd$_%Ng ze{^$&aq2u435vmZpLOr-@c7B?l7JDp$2L0IWZ| zKO>3R!pNH5T)wl+W_=191}m*EY}=8+3If$^l3XEneq{dToGb_VOjgg_WPyC)T)nh;i3} zmJ>L9sZK(JsqE(`Q{K2^3Mhc{sx(gCT!-OGKfPjF=9NHrkh@fIE}t$lYhm>W}dsod|XKiT{pa`Smc6Rq0y2*Dc^J`q*_PelSHh>-0F!WV9dVnbws2D)nL} zykT@3%jlMT*aPi@p2R%gG5aIFEW!H<(lWn$Ofxn6-iP@|c64+7`d*%bYfggkiXe~@ z-rX$?HWK{)Hnlnp{1W*%t4_kx++yMQeH*SpYHcog5cCq$(E2TmH%h~fl`qN2hnngy zO`pnmupMH4HqKbjVA)n-d-J1wVa0e9p4_#YF+ zuo>^nj6U>s#xUkIVd3dM$(ymApgP?9ykcxK+OH##koicA$iJv=?eq2U!cXX}-Ij`D znum{X?k=)uHQs5EQRJlRYA>2iUk$$JdQIXLenGP|1F+S?*Rt~v@>C*unh)pt=tp749pJOvvF@eysNW@;}%2Xi_<%dy2|aBoozqOwLDxIczo~m23PuwiB2vE>1B&M^l#lSKnZ*GvVi`^0{#X*V_h zcI&=NNlU$WdyGYf%|KA%-o_C9o{yI6jd_a6`E9&Ubtd%HF4U228L^Wo$NcZ4iOfSD z?6^i%K_0`BU44I@$t~50vWdIcJhk*~RjhAV*jcHpE{;V8Wo6_Vy096!$UbbXb{P%0 zMcCFmmFVCG{GDRUGHxl^KaDK7Yjx!mJ9L)~4-(LG9E7i!xp^zieeob{cP5S{|5rUl z{T<&v%7>R4H&@!ycITOownmw@R{GHwbq-Ij{W(h29ePv6G$_oyH}W~u=je|aGPZ4P zr&kE)H=g+xuk5olu-4yTmqB{Q=+B!|iQ_rfjvnxtzG@>RlUMpuLe-mkpRnhIy>!1` z=pQyA)G2cMrfgVdB-h>s$nLC7#QyE~IW+3c$BH3snb2oT zci_9Fsh*uQS8K4m;c6!7KfSEqg~lkm)$!rtSv2(L@)aDXVLnPSX0&!VWD zle5G4XDVgPcoPXYhOu{?u#FY4%i%TRkxae>%BCcZ$-!s(9ne=nuS-lxSt!ZQhcorD z+ND8cg-9g~hlQg;FLE?e!mM&#z=lPMY53A*s^RJ3m5($&j@Htu4YEgz1X?C9%%0UA zZC`)g;3VR{5#_eCd~Yf(F2Up|-DR=i`1jK^A6@TOmFzw#E~S;e#z^X&37j^BXG-sr zYjv~R;sLR(oJr6aj64SUy2dl*&)N9An%f&neam%4o9iW?XFG>C*R*T%mOS%E=eeUh zr@Vi!HCS)iY5ch!J=IIFI#zdfm*_1+RjR8}R@wpkXkTAf+pW{SlV7~$)yEFbl8}=J zGck_7r7>vaHXZ!($5Pj-x8|EYrhf?I1Wugb1_9Q53v$A>P=&a$0Q!Nu44uD4y%!J_ z`9vHFo-*-f8K|}9vfY(TNIF*+obOq{$^Iuo5n=H!2CpXi)ac7o-y1x#ecBnk)PNH#v z>Th&gp)zr=sjr;U-Y%YZu3}lg&`P8a{iolbNrUx12pzY*Ul^`sp3S^bASK#=+?Mm= z_U@gW-lB-n#}`MZ2joUYEe)_m$1onRbE6&J^}PKohgiQ(s#e zTPgW`Ku_zO^W@oQ#d{`T#eO-^7>0%KLY%i9$goNO&Ef4Ua%TtR6G6navqi+|H<1s zlQ%<`ud;Q!>5XJ<8~e`}_{2v#Gyp?BDC@MX@VtXQ$ zJ8ZVzx;>;9DbiW1M}nCs{}x*B`>J*Bv__}oyH~4ASIQg5)b*3adP$V-EX)z z`NElE`LcusOGe4XdBHp1Lo_i0Zx=Ez*54W4=`G6;+{vCwtNM9y5M6QaO8rdx^m>1m z_*9FleL+Br+O@sSSRe1^O=MoklJ8tMs%uAK$MF5@t8ohqIVQPB#_FU(T7ja*xi3>T zA@C{MWxRm~0n5OTvA@KS2O=V`5;I6-7qZ>GLmoN%ai_ZyGs24Uaaa5*HVar|Gtwhfb~iuHwKxKJHb1 z#*|-ms4x7m;L$}bv+#C2t4F4y!If_MbgRA(H0pL{pHH!dNSU*t+sajVGX|gK-4O8K zKip35ehM@XDPNtsn*x#r3W;(&KW9V4Et`j{RDF(y@%dv^YvazNmoVYsH+3!(DlRd@lCIZn(*dBw>^V$7UFGAmep0d zQNo$yO))9UPi5WAE> zU1KH}q|QkX7nTf_+k7R{lHlZ_^0pKiP!C4SFqS0?heplyDirb5vla&MdaZp69>Lpr zmy~0itD51px1P}_XxqtV*X5{S9p`NO=*+sZ%Z*&;ooJf~Gqo5YGCDk@ZZU`T*6{`D zm?*H+&=xJb)3=N#SI5hK^>gr4&aSbRi+(Qt+V#pRG90hQxcu$HZnLi3Tj%ECvX}gY zsY9y74+RE=OnpRFiC1a97q#vywx{%q%l*ME8}V+gyE`v1AMU#Tb5ET@oQ-ht2_seh z^_jwwUdoGw#uOYeh(9(A0Tcp*Lukxnx=U&7B`g{Sxv{A;D0~mn)7o_rUVTN9n zy*5}1JIav~DD}jbe!s37-2W+_;ggbKmd8Mvp>Q+-7?O)qoi065L*hdJCmPtzkBEqC zT_+y#nbqbZwzB$d|El~gNHCxC;tN7cu(5~%Tewj3+&chK5+^J9A`f%!IVeUw zB|a12e@h8e!DH9#Kg%CMYz2o-3Ma| z;+V3?(vchv2qpTYRBBJ<*-gWW`PuZqF$Q@w2jQL<oT%$RSrY|uts$8*PpR? z2Vi!OO8cxG)V01PQSM!;YBKN zDhT#!h^Wm4>2btEsRxe%0b@o3gb{JqGKJy-br^*ZqLyG1dh#aqM$A}d zaR0Z>mQV07d`pC$L3TdNGBMB@1rEXC^2bMHsh0P-%3n_td|7_u91+}ac=ba`Q7ses z8k=caT6QepDz=hK&5pCZigmvIKS4h7Q&{U5POQ+s+3G~!5<0OEmbIlS>i5GRjgaLaY(vLffWMztPGD5;>x{TIk|epsKF)?d`! z51t4wHfY4#O^W*1a#;}b(o88&0vH&fzu*@7@uBI_cL{n~raTzfoE=+j!taLkB9rph z1dLxNtly)D}? z+}a|>oU5=qEOuc{UU_0lrNPANfXiN5Ob?H-SElEI7OYMW_#Cwy=0~xpGdfH>JcKZ7 zfiOYEmG3p8jC?1Z?V_Hp;)0vP(yb@J(TeNHO}?02;Q|2>Mrsi!yx6~;%zGI$-d#mK zVvv0ZsCyom6D$l@70orBbmqzDVwlivB36!j!3w%dn z7_pEc&i(e-GLUAXURG7z*_hQpY=prBJ$+Lkm}&HZ%vlr3x%xAWIx1)zmJrb#;YC1} z`==T%Vy+{co^z#-I^ur55fHN}f|JUmI7#F{ywX>iuovX${q83WwPkg>0SFNRz!jhhb&G(kF>SU$?Pg2@Vb>CL=4@|F%tu zId{DPs(nvt+Syp(GhX?azGNTGi_U87xF}P~mhR@{OVY6E9zIJdVUu$Z87Ypi>v#%wrUfq zQDIn6>`H0+dQT$WcM2tFeg%bBiRO;pzWViRJHb%nZ^ktLJlPmwHmP)$sfv)aQYZ*Q zts^*ZV16C(KKPZp6X`i>VueTg`Pcg+KK67y;_P;gliWdb{72j2mHX&bd|Y?so`B$s#4|F z^A54k2P9avx*`y3I)0d3a{}NI!f~7kZLF`K*kn()MO`U1;sgB8>aK1NCz>c*B4nnh zO7EH;O5>-yhL!|N3%2NWbwwYBDwng`T7Aj^d9JGryskn^Uw zpKVlGz?4*fdPaY9wCTpU{)h6~sshd@Odg(|l_y#EjtJf&{dGAK#jC*tejP~U4-DM| z9s8Mj{6(Ue=zf0^T7IZomejZT$}(t4TC}7(yNF_$4gG^(ktF#3ZK5)l?BxptTxcFM z5N`F_sJ2X;+UB2&J>QA z9v1XtgAe~GSiAm(tCs$}t*`$Da@)Un+^K{sE(T?&V`5}s2HPYLwuy;Jny|w;h&UJU zSkSZb)GxOHJ=>kYAszEHHBnYfg2)AC?L%ptFP6(*-=s7Zi+4)$WZ|hG{gn+n&F&ee zAe4(X;$gDBz8+2d=a~E2=3Ip4P)It}z{T%$;bhNDB<@KOda@|>zi_(c@tW<;sNl85 zGkO0%F;<}Orpfw>!4@Xq@=c&e{YD`=FWv3m;?b)AE*{nDkz%AEzvB256tOCKhJ-kI zh7KtKP)VeL4|(2Whge$g`cXDR#G6o_T8D1WFQi! zmh*c(*EFI5QK$dNI!WjM2U+Kj{NSe?RVNdXw-RrbK0i6%K#G(1o+F|K%D75oXk(@$ zopk|g8=WcZO9>hsxVZ9HjeFs>JORSHg_*}B7J`+Vp&ey3IC=E>!M!(|Cn^X`+b}*a zwa!e01^55bZ^Z(}!M8X#@3S0P{E30-D7I(aFrxu}60o%l2C6UDc2hDZ2|vIi9jKI_ z5@`-DDY&Pl5~^X13RXeoWW22H-V9DwOt^~?ZYSa$mvR;Z(6jrpLI3Txk`Xm7^h#~Q zA_N2m(!d)t{d$Uo8mWLl5E7pXxNaO?BXD{g<*vfRMB>TVJ92eySf1q8X({m!ziy{0 z_5P8h(x4N*x5$49(?2jx#}3OgDe4E1C$+ubHx%79cw^ym;}nbXbMGs%IC-W#n18g_ zaLHdhVMi{|u+YKp-3cBuHP^Sly?SWU6p3{#-;e9Sy`gAXO*v^kVi zqPw3ug7xz6Gxkmdhh|KR%wK(@3_?Z?`)E4mK^|`E2YXDG_coZd(qCNKw{AhVogjU* z*FEo?1Y_v^^KK3tLKW&{m3(`>*(=>x`=cDI0DRIC-@RUXTe`irGD5q%ee2dWMMN3- zwPQo_!b`{WpgU|ZpnRmTiZ24nM6C(=qnr0(M?7g9n?)LLm8xP2;Y2{Xa& zSyHuLo;(*uhWRKTz5dbUJpwI!?b2m_qjM(ZVrCTArV)#@@aY-GI;!xUwfAvmYt1u# zjbXpip<0-Fjkm^2i8=IMesU8C_G8^TG{KX@;3hQ^QhOR)fO(wy@3vjdysc z*)w^W>`B5HQ`b6SXG}j{nxUZ6Uvj&r53QcY&_T%H`=RRgf0T6thOz>$)ukIG)>%E4 zzED{O>`|Uu9Sgl!cpQu=P=AOswU~5cYjJQn|GsYH2Bq4$g)rU1Khc-@Z$;ARUzl1u zyCksK9=hi{!|1d60`J$4XjO?UcLHys#h7QC#(B3JaqYe*$d&Hz8~*eT56esbDrklS zJy#EPJJ_m0tQzQe2tjjO5|1d zeXHZ@GcT)0yx(BgNe=HV^-ZjQQFrB;Klilfe4l*KaexTTEh!I0FC$zQmYdhxDmJ@T zsuY()>zTXU8%)1&JDN@0_a?bYT=J&dDH?fa8NlvdOrS=~jO9xP{3J#_0|U&+?RxEd z-@YQp?z~uU>c7Z^7Iu)0&aawJs>e+fCKzH0Zy##>aVN7b7R`(fSM41&9@~7zbLFnsZoJO< zp>Yeh_IFL*sGM2N(zUz_E$)KvS96(>=a(r+6Lt&tNThMnR#;_Dx@+JSO}uj=QTswB zWT_jLft&uNG_rzmZQh<|tu)nhMaABdd`S zr7FUEQJEag1zlNye@x`rU}~OZftG} z+O(n}ukog)<{CPWEjGSuhKNpgV9-@micD~CIikGNliW0QWA)zvjpf z|7&TOiyZ_sQTAtAGaq7Zr5dQ=_|=MB`P8K26Bjq$w(y%tEV!<(hqM9A0t zMNel{{LFK?=-Cur`TYJityuFyy!hm`qkU<1#()h~zs*Hyz$ICmoiQ`=y0XFSgaKDA zm%f%V0(^IwJ;dprzmeEtLt2xN(qA>yB?Tm(=|sL;DR{!J0)+#TB&nbfSje{Zh}ik( z^H8Z$E#ilz&Z>l2FDS@oSucuHG3Q;UxS+sYVQFNKi+wtzvzwIrqx};UZgK;E^B1|S z_Tm3ejEC$mHrkE)CqhyM;jwcJj_R3-62x zx2J|kr2^`P8Q}IH>IM=9hVo}?(SLo(ATh=$ic-^~(ojoaw)B54oI;2Z!aNe12sG86 zi-oSD_J3ihM@+3MDO$U(#4>bhT-b-PMKJJ7VG@%%Q!m$OMj>a^p+b9}CJ@@#j?+p-$1X}zPDz1T7zYhLtSVs^Y42H$KB?>)0MaTRxb~q#?A2NA%KRz$~(0!Kfz)xqW%Pc&V?~xY&HWy7v8on?x__ zk6hA|qkjuL!k?Yv6s|z8bJcZRlq!02%ZfVbMBMRyqN3zt_xa8XIC*)6At5IuaY)f( zrox6jWC%scXNm!q$r);aQ($yqIdXUCUF~aqALk9)=VP9uV};KvP6w@){haDKyq{|^ zV=A{~ksnIRl@yWpDJr?*F=tOUknKz3XON1J74G3*DuiTwq|<{%1YVTLBI^8VA?|&F zhlu5zN%Um}6>FaeubXV3_y97DVt$w!FQU}w&I<02D2=lPSg{g;73(Wb9q3|n6GLx| zpOBEyUtuLbSl+{z!Oip(KN(*FjzUsI^F;`vJ4(pV7#-oJX?e$(h{Qd%%)F<$m0=61-Vvbuh(|ecRp-?)R5(rGt z+SUh}Q(J(YE}!skaRpxF8Pomg{mx4L{|Ov&ML83*eDRjIuMCA>|NQcHL!uVq=xWD>gH&HH)8r(T?*FW zB*K4PqxlREEjV&%Ll=E3zgitzjE%`y2Lji)jCFt?0*!L9&L%5fV=}GvCQe^*g*&zg*DtWwTbs28a&< z9RcDap`~?J#`bs&a6bhcX}O-wiP6JfDR>RT=kc#4ykmTDbi(q5h|Xq6B(%|BTqK7T zwhJHF=x~V;zKAVX3{IjZ@-qXpJPLqi!XILK`WR@q&y(hM!Ecnq&F}~%T{=0GTKJze zKYGS5y-Ps-Ioi}k==H|tcW@vg?e_oX!`}hHG7cEUrZvtmgQOK>Q%HrU#2m`51p@Z3 zCtJWRWH8s2(TKDWF5j#1eRq}>Q~53W!Nm6R!KEU&D-Fn0S612dDCB6c~l>hm+-7XV@4r-viCGQ^cGCKJw zB}H5cfcxxVxVv!TgqM~O*cGf75P6dRdNKrx3lvA~O}!M~`j&z`YJ(h;FPRM9-UQ0T zzdq*gfAk|mg6yqf@srZ+um61V_W~gPdg^=2>BtBC=g$_r3W_=WcOWAZo}B;qkwSt5 zN#yEJwygjB9$C0ZKQ(gv|MFl!uG`-S`3P=}C#DY}y#MDnTmtb2Vkqq}@t+@ma{rK> woPMh6=6`-X^6&q98vo^1z_t26xf_3ozVs>nHng;9It70;RCQF!m8^sRACZ8KLI3~& literal 0 HcmV?d00001 diff --git a/docs/security/trust/images/trust_view.gliffy b/docs/security/trust/images/trust_view.gliffy new file mode 100644 index 00000000..b635e657 --- /dev/null +++ b/docs/security/trust/images/trust_view.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#FFFFFF","width":866,"height":537,"nodeIndex":323,"autoFit":true,"exportBorder":false,"gridOn":true,"snapToGrid":true,"drawingGuidesOn":true,"pageBreaksOn":false,"printGridOn":false,"printPaper":null,"printShrinkToFit":false,"printPortrait":false,"maxWidth":5000,"maxHeight":5000,"themeData":null,"viewportType":"default","fitBB":{"min":{"x":10,"y":0},"max":{"x":865.6666666666666,"y":536.25}},"printModel":{"pageSize":"a4","portrait":false,"fitToOnePage":false,"displayPageBreaks":false},"objects":[{"x":10.0,"y":122.25000000000006,"rotation":0.0,"id":79,"width":531.0,"height":409.99999999999994,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":0,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#ffffff","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":416.0,"y":312.25000000000006,"rotation":0.0,"id":40,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left","order":1,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":41,"width":71.42857142857143,"height":50.0,"uid":null,"order":3,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":40}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":40}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_left","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":10.714285714285722,"y":0.0,"rotation":0.0,"id":42,"width":26.0,"height":18.0,"uid":null,"order":5,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":40,"px":0.15,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

1.0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":82.1785714285715,"y":17.03600000000003,"rotation":0.0,"id":0,"width":63.0,"height":82.0,"uid":"com.gliffy.shape.network.network_v4.business.female_user","order":6,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.female_user","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":1,"width":43.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Person

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":330.0,"y":142.25000000000006,"rotation":0.0,"id":2,"width":120.0,"height":80.0,"uid":"com.gliffy.shape.network.network_v4.business.user_group","order":9,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.user_group","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":3,"width":73.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Organization

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":141.0,"y":152.25000000000006,"rotation":0.0,"id":11,"width":63.0,"height":82.0,"uid":"com.gliffy.shape.network.network_v4.business.user","order":12,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.user","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":12,"width":48.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Account

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":305.99999999999994,"y":273.25000000000006,"rotation":0.0,"id":16,"width":110.00000000000001,"height":80.0,"uid":"com.gliffy.shape.sitemap.sitemap_v2.photo","order":15,"lockAspectRatio":false,"lockShape":false,"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":17,"width":110.00000000000001,"height":25.0,"uid":null,"order":17,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":18}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.rounded_top","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":18,"width":110.00000000000001,"height":25.0,"uid":null,"order":20,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":6,"paddingRight":2,"paddingBottom":6,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Repository

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":25.0,"rotation":0.0,"id":19,"width":110.00000000000001,"height":55.0,"uid":null,"order":22,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":16},{"magnitude":-1,"id":18}],"minHeight":0.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":18,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.sitemap.sitemap_v2.photo","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":416.0,"y":262.25000000000006,"rotation":0.0,"id":37,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left","order":35,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":38,"width":71.42857142857143,"height":50.0,"uid":null,"order":37,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":37}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":37}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_left","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":10.714285714285722,"y":0.0,"rotation":0.0,"id":39,"width":38.0,"height":18.0,"uid":null,"order":39,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":37,"px":0.15,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

latest

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":416.0,"y":442.25000000000006,"rotation":0.0,"id":63,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left","order":40,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":64,"width":71.42857142857143,"height":50.0,"uid":null,"order":42,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":63}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":63}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_left","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":10.714285714285722,"y":0.0,"rotation":0.0,"id":65,"width":68.0,"height":18.0,"uid":null,"order":44,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":63,"px":0.15,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

producttion

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":305.99999999999994,"y":403.25000000000006,"rotation":0.0,"id":58,"width":110.00000000000001,"height":80.0,"uid":"com.gliffy.shape.sitemap.sitemap_v2.photo","order":45,"lockAspectRatio":false,"lockShape":false,"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":59,"width":110.00000000000001,"height":25.0,"uid":null,"order":47,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":60}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.rounded_top","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":60,"width":110.00000000000001,"height":25.0,"uid":null,"order":50,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":6,"paddingRight":2,"paddingBottom":6,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Repository

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":25.0,"rotation":0.0,"id":61,"width":110.00000000000001,"height":55.0,"uid":null,"order":52,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":58},{"magnitude":-1,"id":60}],"minHeight":0.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":60,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.sitemap.sitemap_v2.photo","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":416.0,"y":392.25000000000006,"rotation":0.0,"id":55,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left","order":53,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":56,"width":71.42857142857143,"height":50.0,"uid":null,"order":55,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":55}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":55}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_left","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":10.714285714285722,"y":0.0,"rotation":0.0,"id":57,"width":28.0,"height":18.0,"uid":null,"order":57,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":55,"px":0.15,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

test

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":10.000000000000036,"y":132.25000000000006,"rotation":0.0,"id":82,"width":108.99999999999999,"height":20.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":58,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Registry

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":36.142857142857125,"y":399.25000000000006,"rotation":0.0,"id":109,"width":187.85714285714286,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":81,"lockAspectRatio":false,"lockShape":false,"children":[{"x":7.142857142857139,"y":50.0,"rotation":0.0,"id":98,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_right","order":74,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":99,"width":71.42857142857143,"height":50.0,"uid":null,"order":77,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":98}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":98}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_right","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":-7.142857142857139,"y":0.0,"rotation":0.0,"id":100,"width":50.0,"height":18.0,"uid":null,"order":80,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":98,"px":-0.1,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

working

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":7.571428571428527,"y":0.0,"rotation":0.0,"id":95,"width":71.42857142857142,"height":50.0,"uid":"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_right","order":66,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"MinHeightConstraint","MinHeightConstraint":{"height":28}},{"type":"MinWidthConstraint","MinWidthConstraint":{"width":40}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":96,"width":71.42857142857143,"height":50.0,"uid":null,"order":69,"lockAspectRatio":true,"lockShape":false,"constraints":{"constraints":[{"type":"WidthConstraint","WidthConstraint":{"isMin":false,"widthInfo":[{"magnitude":1,"id":95}],"minWidth":0.0,"growParent":false,"padding":0.0}},{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":95}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ui.ui_v3.icon_symbols.annotate_right","strokeWidth":1.0,"strokeColor":"#EA6624","fillColor":"#cfe2f3","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"hidden":false,"layerId":null},{"x":-7.142857142857139,"y":0.0,"rotation":0.0,"id":97,"width":38.0,"height":18.0,"uid":null,"order":72,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"PositionConstraint","PositionConstraint":{"nodeId":95,"px":-0.1,"py":0.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

latest

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":77.85714285714286,"y":8.0,"rotation":0.0,"id":30,"width":110.00000000000001,"height":80.0,"uid":"com.gliffy.shape.sitemap.sitemap_v2.photo","order":24,"lockAspectRatio":false,"lockShape":false,"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":31,"width":110.00000000000001,"height":25.0,"uid":null,"order":27,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":32}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.rounded_top","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":32,"width":110.00000000000001,"height":25.0,"uid":null,"order":31,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":6,"paddingRight":2,"paddingBottom":6,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Repository

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":25.0,"rotation":0.0,"id":33,"width":110.00000000000001,"height":55.0,"uid":null,"order":34,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":30},{"magnitude":-1,"id":32}],"minHeight":0.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":32,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.sitemap.sitemap_v2.photo","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"hidden":false,"layerId":null}],"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":330.0,"y":0.0,"rotation":0.0,"id":180,"width":67.309,"height":101.072,"uid":"com.gliffy.shape.cisco.cisco_v1.buildings.generic_building","order":126,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.buildings.generic_building","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#0b5394","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":182,"width":56.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Company

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"dockVlz9GmcW"}],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":266.0,"y":125.25000000000006,"rotation":0.0,"id":250,"width":7.0,"height":413.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":172,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":79,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":"1.0,1.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[3.5,-3.0],[9.5,406.99999999999994]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":472.40133544303796,"y":320.25000000000006,"rotation":0.0,"id":306,"width":20.0,"height":12.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":209,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":472.40133544303796,"y":271.25000000000006,"rotation":0.0,"id":307,"width":20.0,"height":12.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":210,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":472.40133544303796,"y":401.25000000000006,"rotation":0.0,"id":308,"width":20.0,"height":12.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":211,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":37.214285714285666,"y":406.25000000000006,"rotation":0.0,"id":309,"width":20.0,"height":12.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":212,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":40.214285714285666,"y":456.25000000000006,"rotation":0.0,"id":310,"width":20.0,"height":12.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":213,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"dockVlz9GmcW"},{"x":580.0,"y":418.25000000000006,"rotation":0.0,"id":314,"width":283.66666666666663,"height":20.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":215,"lockAspectRatio":false,"lockShape":false,"children":[{"x":66.66666666666663,"y":4.0,"rotation":0.0,"id":312,"width":217.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":214,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Signed tag.

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":null},{"x":0.0,"y":0.0,"rotation":0.0,"id":304,"width":33.333333333333336,"height":20.0,"uid":"com.gliffy.shape.bpmn.bpmn_v1.activities.ad_hoc","order":208,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ad_hoc.bpmn_v1","strokeWidth":0.0,"strokeColor":"#38761d","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"dockVlz9GmcW"}],"layers":[{"guid":"dockVlz9GmcW","order":0,"name":"Layer 0","active":true,"locked":false,"visible":true,"nodeIndex":216}],"shapeStyles":{},"lineStyles":{"global":{"strokeWidth":1,"endArrow":17}},"textStyles":{"global":{"size":"16px","color":"#000000"}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":["com.gliffy.libraries.cisco.cisco_v1.buildings","com.gliffy.libraries.sitemap.sitemap_v2","com.gliffy.libraries.sitemap.sitemap_v1.default","com.gliffy.libraries.ui.ui_v3.containers_content","com.gliffy.libraries.table.table_v2.default","com.gliffy.libraries.ui.ui_v3.navigation","com.gliffy.libraries.ui.ui_v3.forms_controls","com.gliffy.libraries.ui.ui_v3.icon_symbols","com.gliffy.libraries.ui.ui_v2.forms_components","com.gliffy.libraries.ui.ui_v2.content","com.gliffy.libraries.ui.ui_v2.miscellaneous","com.gliffy.libraries.network.network_v4.business","com.gliffy.libraries.flowchart.flowchart_v1.default","com.gliffy.libraries.basic.basic_v1.default","com.gliffy.libraries.bpmn.bpmn_v1.events","com.gliffy.libraries.bpmn.bpmn_v1.activities","com.gliffy.libraries.bpmn.bpmn_v1.data_artifacts","com.gliffy.libraries.bpmn.bpmn_v1.gateways","com.gliffy.libraries.bpmn.bpmn_v1.connectors","com.gliffy.libraries.swimlanes.swimlanes_v1.default","com.gliffy.libraries.images"],"lastSerialized":1439069097667},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/docs/security/trust/images/trust_view.png b/docs/security/trust/images/trust_view.png new file mode 100644 index 0000000000000000000000000000000000000000..71eb26ce3163d519403bd6f6e72b3347c48c347f GIT binary patch literal 59533 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 NYXR5Ot->`V^ ++++ +title = "Use trusted images" +description = "Use trusted images" +keywords = ["trust, security, docker, index"] +[menu.main] +identifier="smn_content_trust" +parent= "smn_secure_docker" +weight=4 ++++ + + +# Use trusted images + +The following topics are available: + +* [Content trust in Docker](content_trust.md) +* [Manage keys for content trust](trust_key_mng.md) +* [Automation with content trust](trust_automation.md) +* [Delegations for content trust](trust_delegation.md) +* [Play in a content trust sandbox](trust_sandbox.md) diff --git a/docs/security/trust/trust_automation.md b/docs/security/trust/trust_automation.md new file mode 100644 index 00000000..1b3d4564 --- /dev/null +++ b/docs/security/trust/trust_automation.md @@ -0,0 +1,80 @@ + + +# Automation with content trust + +Your automation systems that pull or build images can also work with trust. Any automation environment must set `DOCKER_TRUST_ENABLED` either manually or in in a scripted fashion before processing images. + +## Bypass requests for passphrases + +To allow tools to wrap docker and push trusted content, there are two +environment variables that allow you to provide the passphrases without an +expect script, or typing them in: + + - `DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE` + - `DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE` + +Docker attempts to use the contents of these environment variables as passphrase +for the keys. For example, an image publisher can export the repository `target` +and `snapshot` passphrases: + +```bash +$ export DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE="u7pEQcGoebUHm6LHe6" +$ export DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE="l7pEQcTKJjUHm6Lpe4" +``` + +Then, when pushing a new tag the Docker client does not request these values but signs automatically: + +```bash +$ docker push docker/trusttest:latest +The push refers to a repository [docker.io/docker/trusttest] (len: 1) +a9539b34a6ab: Image already exists +b3dbab3810fc: Image already exists +latest: digest: sha256:d149ab53f871 size: 3355 +Signing and pushing trust metadata +``` + +## Building with content trust + +You can also build with content trust. Before running the `docker build` command, you should set the environment variable `DOCKER_CONTENT_TRUST` either manually or in in a scripted fashion. Consider the simple Dockerfile below. + +```Dockerfile +FROM docker/trusttest:latest +RUN echo +``` + +The `FROM` tag is pulling a signed image. You cannot build an image that has a +`FROM` that is not either present locally or signed. Given that content trust +data exists for the tag `latest`, the following build should succeed: + +```bash +$ docker build -t docker/trusttest:testing . +Using default tag: latest +latest: Pulling from docker/trusttest + +b3dbab3810fc: Pull complete +a9539b34a6ab: Pull complete +Digest: sha256:d149ab53f871 +``` + +If content trust is enabled, building from a Dockerfile that relies on tag without trust data, causes the build command to fail: + +```bash +$ docker build -t docker/trusttest:testing . +unable to process Dockerfile: No trust data for notrust +``` + +## Related information + +* [Content trust in Docker](content_trust.md) +* [Manage keys for content trust](trust_key_mng.md) +* [Delegations for content trust](trust_delegation.md) +* [Play in a content trust sandbox](trust_sandbox.md) + diff --git a/docs/security/trust/trust_delegation.md b/docs/security/trust/trust_delegation.md new file mode 100644 index 00000000..ac803bc9 --- /dev/null +++ b/docs/security/trust/trust_delegation.md @@ -0,0 +1,226 @@ + + +# Delegations for content trust + +Docker Engine supports the usage of the `targets/releases` delegation as the +canonical source of a trusted image tag. + +Using this delegation allows you to collaborate with other publishers without +sharing your repository key (a combination of your targets and snapshot keys - +please see "[Manage keys for content trust](trust_key_mng.md)" for more information). +A collaborator can keep their own delegation key private. + +The `targest/releases` delegation is currently an optional feature - in order +to set up delegations, you must use the Notary CLI: + +1. [Download the client](https://github.com/docker/notary/releases) and ensure that it is +available on your path + +2. Create a configuration file at `~/.notary/config.json` with the following content: + + ``` + { + "trust_dir" : "~/.docker/trust", + "remote_server": { + "url": "https://notary.docker.io" + } + } + ``` + + This tells Notary where the Docker Content Trust data is stored, and to use the + Notary server used for images in Docker Hub. + +For more detailed information about how to use Notary outside of the default +Docker Content Trust use cases, please refer to the +[the Notary CLI documentation](https://docs.docker.com/notary/getting_started/). + +Note that when publishing and listing delegation changes using the Notary client, +your Docker Hub credentials are required. + +## Generating delegation keys + +Your collaborator needs to generate a private key (either RSA or ECDSA) +and give you the public key so that you can add it to the `targets/releases` +delegation. + +The easiest way to for them to generate these keys is with OpenSSL. +Here is an example of how to generate a 2048-bit RSA portion key (all RSA keys +must be at least 2048 bits): + +``` +$ opensl genrsa -out delegation.key 2048 +Generating RSA private key, 2048 bit long modulus +....................................................+++ +............+++ +e is 65537 (0x10001) + +``` + +They should keep `delegation.key` private - this is what they will use to sign +tags. + +Then they need to generate a x509 certificate containing the public key, which is +what they will give to you. Here is the command to generate a CSR (certificate +signing request): + +``` +$ openssl req -new -sha256 -key delegation.key -out delegation.csr +``` + +Then they can send it to whichever CA you trust to sign certificates, or they +can self-sign the certificate (in this example, creating a certificate that is +valid for 1 year): + +``` +$ openssl x509 -req -days 365 -in delegation.csr -signkey delegation.key -out delegation.crt +``` + +Then they need to give you `delegation.crt`, whether it is self-signed or signed +by a CA. + +## Adding a delegation key to an existing repository + +If your repository was created using a version of Docker Engine prior to 1.11, +then before adding any delegations, you should rotate the snapshot key to the server +so that collaborators will not require your snapshot key to sign and publish tags: + +``` +$ notary key rotate docker.io// snapshot -r +``` + +This tells Notary to rotate a key for your particular image repository - note that +you must include the `docker.io/` prefix. `snapshot -r` specifies that you want +to rotate the snapshot key specifically, and you want the server to manage it (`-r` +stands for "remote"). + +When adding a delegation, your must acquire +[the PEM-encoded x509 certificate with the public key](#generating-delegation-keys) +of the collaborator you wish to delegate to. + +Assuming you have the certificate `delegation.crt`, you can add a delegation +for this user and then publish the delegation change: + +``` +$ notary delegation add docker.io// targets/releases delegation.crt --all-paths +$ notary publish docker.io// +``` + +The preceding example illustrates a request to add the delegation +`targets/releases` to the image repository, if it doesn't exist. Be sure to use +`targets/releases` - Notary supports multiple delegation roles, so if you mistype +the delegation name, the Notary CLI will not error. However, Docker Engine +supports reading only from `targets/releases`. + +It also adds the collaborator's public key to the delegation, enabling them to sign +the `targets/releases` delegation so long as they have the private key corresponding +to this public key. The `--all-paths` flags tells Notary not to restrict the tag +names that can be signed into `targets/releases`, which we highly recommend for +`targets/releases`. + +Publishing the changes tells the server about the changes to the `targets/releases` +delegation. + +After publishing, view the delegation information to ensure that you correctly added +the keys to `targets/releases`: + +``` +$ notary delegation list docker.io// + + ROLE PATHS KEY IDS THRESHOLD +--------------------------------------------------------------------------------------------------------------- + targets/releases "" 729c7094a8210fd1e780e7b17b7bb55c9a28a48b871b07f65d97baf93898523a 1 +``` + +You can see the `targets/releases` with its paths and the key ID you just added. + +Notary currently does not map collaborators names to keys, so we recommend +that you add and list delegation keys one at a time, and keep a mapping of the key +IDs to collaborators yourself should you need to remove a collaborator. + +## Removing a delegation key from an existing repository + +To revoke a collaborator's permission to sign tags for your image repository, you must +know the IDs of their keys, because you need to remove their keys from the +`targets/releases` delegation. + +``` +$ notary delegation remove docker.io// targets/releases 729c7094a8210fd1e780e7b17b7bb55c9a28a48b871b07f65d97baf93898523a + +Removal of delegation role targets/releases with keys [729c7094a8210fd1e780e7b17b7bb55c9a28a48b871b07f65d97baf93898523a], to repository "docker.io//" staged for next publish. +``` + +The revocation will take effect as soon as you publish: + +``` +$ notary publish docker.io// +``` + +Note that by removing all the keys from the `targets/releases` delegation, the +delegation (and any tags that are signed into it) is removed. That means that +these tags will all be deleted, and you may end up with older, legacy tags that +were signed directly by the targets key. + +## Removing the `targets/releases` delegation entirely from a repository + +If you've decided that delegations aren't for you, you can delete the +`targets/releases` delegation entirely. This also removes all the tags that +are currently in `targets/releases`, however, and you may end up with older, +legacy tags that were signed directly by the targets key. + +To delete the `targets/releases` delegation: + +``` +$ notary delegation remove docker.io// targets/releases + +Are you sure you want to remove all data for this delegation? (yes/no) +yes + +Forced removal (including all keys and paths) of delegation role targets/releases to repository "docker.io//" staged for next publish. + +$ notary publish docker.io// +``` + +## Pushing trusted data as a collaborator + +As a collaborator with a private key that has been added to a repository's +`targets/releases` delegation, you need to import the private key that you +generated into Content Trust. + +To do so, you can run: + +``` +$ notary key import delegation.key --role user +``` + +where `delegation.key` is the file containing your PEM-encoded private key. + +After you have done so, running `docker push` on any repository that +includes your key in the `targets/releases` delegation will automatically sign +tags using this imported key. + +## `docker push` behavior + +When running `docker push` with Docker Content Trust, Docker Engine +will attempt to sign and push with the `targets/releases` delegation if it exists. +If it does not, the targets key will be used to sign the tag, if the key is available. + +## `docker pull` and `docker build` behavior + +When running `docker pull` or `docker build` with Docker Content Trust, Docker +Engine will pull tags only signed by the `targets/releases` delegation role or +the legacy tags that were signed directly with the `targets` key. + +## Related information + +* [Content trust in Docker](content_trust.md) +* [Manage keys for content trust](trust_key_mng.md) +* [Automation with content trust](trust_automation.md) +* [Play in a content trust sandbox](trust_sandbox.md) diff --git a/docs/security/trust/trust_key_mng.md b/docs/security/trust/trust_key_mng.md new file mode 100644 index 00000000..c15b84ba --- /dev/null +++ b/docs/security/trust/trust_key_mng.md @@ -0,0 +1,90 @@ + + +# Manage keys for content trust + +Trust for an image tag is managed through the use of keys. Docker's content +trust makes use of five different types of keys: + +| Key | Description | +|---------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| root key | Root of content trust for a image tag. When content trust is enabled, you create the root key once. Also known as the offline key, because it should be kept offline. | +| targets | This key allows you to sign image tags, to manage delegations including delegated keys or permitted delegation paths. Also known as the repository key, since this key determines what tags can be signed into an image repository. | +| snapshot | This key signs the current collection of image tags, preventing mix and match attacks. +| timestamp | This key allows Docker image repositories to have freshness security guarantees without requiring periodic content refreshes on the client's side. | +| delegation | Delegation keys are optional tagging keys and allow you to delegate signing image tags to other publishers without having to share your targets key. | + +When doing a `docker push` with Content Trust enabled for the first time, the +root, targets, snapshot, and timestamp keys are generated automatically for +the image repository: + +- The root and targets key are generated and stored locally client-side. + +- The timestamp and snapshot keys are safely generated and stored in a signing server + that is deployed alongside the Docker registry. These keys are generated in a backend + service that isn't directly exposed to the internet and are encrypted at rest. + +Delegation keys are optional, and not generated as part of the normal `docker` +workflow. They need to be +[manually generated and added to the repository](trust_delegation.md#generating-delegation-keys). + +Note: Prior to Docker Engine 1.11, the snapshot key was also generated and stored +locally client-side. [Use the Notary CLI to manage your snapshot key locally +again](https://docs.docker.com/notary/advanced_usage/#rotate-keys) for +repositories created with newer versions of Docker. + +## Choosing a passphrase + +The passphrases you chose for both the root key and your repository key should +be randomly generated and stored in a password manager. Having the repository key +allow users to sign image tags on a repository. Passphrases are used to encrypt +your keys at rest and ensures that a lost laptop or an unintended backup doesn't +put the private key material at risk. + +## Back up your keys + +All the Docker trust keys are stored encrypted using the passphrase you provide +on creation. Even so, you should still take care of the location where you back them up. +Good practice is to create two encrypted USB keys. + +It is very important that you backup your keys to a safe, secure location. Loss +of the repository key is recoverable; loss of the root key is not. + +The Docker client stores the keys in the `~/.docker/trust/private` directory. +Before backing them up, you should `tar` them into an archive: + +```bash +$ umask 077; tar -zcvf private_keys_backup.tar.gz ~/.docker/trust/private; umask 022 +``` + +## Lost keys + +If a publisher loses keys it means losing the ability to sign trusted content for +your repositories. If you lose a key, contact [Docker +Support](https://support.docker.com) (support@docker.com) to reset the repository +state. + +This loss also requires **manual intervention** from every consumer that pulled +the tagged image prior to the loss. Image consumers would get an error for +content that they already downloaded: + +``` +could not validate the path to a trusted root: failed to validate data with current trusted certificates +``` + +To correct this, they need to download a new image tag with that is signed with +the new key. + +## Related information + +* [Content trust in Docker](content_trust.md) +* [Automation with content trust](trust_automation.md) +* [Delegations for content trust](trust_delegation.md) +* [Play in a content trust sandbox](trust_sandbox.md) diff --git a/docs/security/trust/trust_sandbox.md b/docs/security/trust/trust_sandbox.md new file mode 100644 index 00000000..af056728 --- /dev/null +++ b/docs/security/trust/trust_sandbox.md @@ -0,0 +1,331 @@ + + +# Play in a content trust sandbox + +This page explains how to set up and use a sandbox for experimenting with trust. +The sandbox allows you to configure and try trust operations locally without +impacting your production images. + +Before working through this sandbox, you should have read through the [trust +overview](content_trust.md). + +### Prerequisites + +These instructions assume you are running in Linux or Mac OS X. You can run +this sandbox on a local machine or on a virtual machine. You will need to +have `sudo` privileges on your local machine or in the VM. + +This sandbox requires you to install two Docker tools: Docker Engine and Docker +Compose. To install the Docker Engine, choose from the [list of supported +platforms](../../installation/index.md). To install Docker Compose, see the +[detailed instructions here](https://docs.docker.com/compose/install/). + +Finally, you'll need to have `git` installed on your local system or VM. + +## What is in the sandbox? + +If you are just using trust out-of-the-box you only need your Docker Engine +client and access to the Docker hub. The sandbox mimics a +production trust environment, and requires these additional components: + +| Container | Description | +|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------| +| notarysandbox | A container with the latest version of Docker Engine and with some preconfigured certifications. This is your sandbox where you can use the `docker` client to test trust operations. | +| Registry server | A local registry service. | +| Notary server | The service that does all the heavy-lifting of managing trust | +| Notary signer | A service that ensures that your keys are secure. | +| MySQL | The database where all of the trust information will be stored | + +The sandbox uses the Docker daemon on your local system. Within the `notarysandbox` +you interact with a local registry rather than the Docker Hub. This means +your everyday image repositories are not used. They are protected while you play. + +When you play in the sandbox, you'll also create root and repository keys. The +sandbox is configured to store all the keys and files inside the `notarysandbox` +container. Since the keys you create in the sandbox are for play only, +destroying the container destroys them as well. + + +## Build the sandbox + +In this section, you build the Docker components for your trust sandbox. If you +work exclusively with the Docker Hub, you would not need with these components. +They are built into the Docker Hub for you. For the sandbox, however, you must +build your own entire, mock production environment and registry. + +### Configure /etc/hosts + +The sandbox' `notaryserver` and `sandboxregistry` run on your local server. The +client inside the `notarysandbox` container connects to them over your network. +So, you'll need an entry for both the servers in your local `/etc/hosts` file. + +1. Add an entry for the `notaryserver` to `/etc/hosts`. + + $ sudo sh -c 'echo "127.0.0.1 notaryserver" >> /etc/hosts' + +2. Add an entry for the `sandboxregistry` to `/etc/hosts`. + + $ sudo sh -c 'echo "127.0.0.1 sandboxregistry" >> /etc/hosts' + + +### Build the notarytest image + +1. Create a `notarytest` directory on your system. + + $ mkdir notarysandbox + +2. Change into your `notarysandbox` directory. + + $ cd notarysandbox + +3. Create a `notarytest` directory then change into that. + + $ mkdir notarytest + $ cd notarytest + +4. Create a filed called `Dockerfile` with your favorite editor. + +5. Add the following to the new file. + + FROM debian:jessie + + ADD https://master.dockerproject.org/linux/amd64/docker /usr/bin/docker + RUN chmod +x /usr/bin/docker \ + && apt-get update \ + && apt-get install -y \ + tree \ + vim \ + git \ + ca-certificates \ + --no-install-recommends + + WORKDIR /root + RUN git clone -b trust-sandbox https://github.com/docker/notary.git + RUN cp /root/notary/fixtures/root-ca.crt /usr/local/share/ca-certificates/root-ca.crt + RUN update-ca-certificates + + ENTRYPOINT ["bash"] + +6. Save and close the file. + +7. Build the testing container. + + $ docker build -t notarysandbox . + Sending build context to Docker daemon 2.048 kB + Step 1 : FROM debian:jessie + ... + Successfully built 5683f17e9d72 + + +### Build and start up the trust servers + +In this step, you get the source code for your notary and registry services. +Then, you'll use Docker Compose to build and start them on your local system. + +1. Change to back to the root of your `notarysandbox` directory. + + $ cd notarysandbox + +2. Clone the `notary` project. + + $ git clone -b trust-sandbox https://github.com/docker/notary.git + +3. Clone the `distribution` project. + + $ git clone https://github.com/docker/distribution.git + +4. Change to the `notary` project directory. + + $ cd notary + + The directory contains a `docker-compose` file that you'll use to run a + notary server together with a notary signer and the corresponding MySQL + databases. The databases store the trust information for an image. + +5. Build the server images. + + $ docker-compose build + + The first time you run this, the build takes some time. + +6. Run the server containers on your local system. + + $ docker-compose up -d + + Once the trust services are up, you'll setup a local version of the Docker + Registry v2. + +7. Change to the `notarysandbox/distribution` directory. + +8. Build the `sandboxregistry` server. + + $ docker build -t sandboxregistry . + +9. Start the `sandboxregistry` server running. + + $ docker run -p 5000:5000 --name sandboxregistry sandboxregistry & + +## Playing in the sandbox + +Now that everything is setup, you can go into your `notarysandbox` container and +start testing Docker content trust. + + +### Start the notarysandbox container + +In this procedure, you start the `notarysandbox` and link it to the running +`notary_notaryserver_1` and `sandboxregistry` containers. The links allow +communication among the containers. + +``` +$ docker run -it -v /var/run/docker.sock:/var/run/docker.sock --link notary_notaryserver_1:notaryserver --link sandboxregistry:sandboxregistry notarysandbox +root@0710762bb59a:/# +``` + +Mounting the `docker.sock` gives the `notarysandbox` access to the `docker` +daemon on your host, while storing all the keys and files inside the sandbox +container. When you destroy the container, you destroy the "play" keys. + +### Test some trust operations + +Now, you'll pull some images. + +1. Download a `docker` image to test with. + + # docker pull docker/trusttest + docker pull docker/trusttest + Using default tag: latest + latest: Pulling from docker/trusttest + + b3dbab3810fc: Pull complete + a9539b34a6ab: Pull complete + Digest: sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a + Status: Downloaded newer image for docker/trusttest:latest + +2. Tag it to be pushed to our sandbox registry: + + # docker tag docker/trusttest sandboxregistry:5000/test/trusttest:latest + +3. Enable content trust. + + # export DOCKER_CONTENT_TRUST=1 + +4. Identify the trust server. + + # export DOCKER_CONTENT_TRUST_SERVER=https://notaryserver:4443 + + This step is only necessary because the sandbox is using its own server. + Normally, if you are using the Docker Public Hub this step isn't necessary. + +5. Pull the test image. + + # docker pull sandboxregistry:5000/test/trusttest + Using default tag: latest + no trust data available + + You see an error, because this content doesn't exist on the `sandboxregistry` yet. + +6. Push the trusted image. + + # docker push sandboxregistry:5000/test/trusttest:latest + The push refers to a repository [sandboxregistry:5000/test/trusttest] (len: 1) + a9539b34a6ab: Image successfully pushed + b3dbab3810fc: Image successfully pushed + latest: digest: sha256:1d871dcb16805f0604f10d31260e79c22070b35abc71a3d1e7ee54f1042c8c7c size: 3348 + Signing and pushing trust metadata + You are about to create a new root signing key passphrase. This passphrase + will be used to protect the most sensitive key in your signing system. Please + choose a long, complex passphrase and be careful to keep the password and the + key file itself secure and backed up. It is highly recommended that you use a + password manager to generate the passphrase and keep it safe. There will be no + way to recover this key. You can find the key in your config directory. + Enter passphrase for new root key with id 8c69e04: + Repeat passphrase for new root key with id 8c69e04: + Enter passphrase for new repository key with id sandboxregistry:5000/test/trusttest (93c362a): + Repeat passphrase for new repository key with id sandboxregistry:5000/test/trusttest (93c362a): + Finished initializing "sandboxregistry:5000/test/trusttest" + latest: digest: sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a size: 3355 + Signing and pushing trust metadata + +7. Try pulling the image you just pushed: + + # docker pull sandboxregistry:5000/test/trusttest + Using default tag: latest + Pull (1 of 1): sandboxregistry:5000/test/trusttest:latest@sha256:1d871dcb16805f0604f10d31260e79c22070b35abc71a3d1e7ee54f1042c8c7c + sha256:1d871dcb16805f0604f10d31260e79c22070b35abc71a3d1e7ee54f1042c8c7c: Pulling from test/trusttest + b3dbab3810fc: Already exists + a9539b34a6ab: Already exists + Digest: sha256:1d871dcb16805f0604f10d31260e79c22070b35abc71a3d1e7ee54f1042c8c7c + Status: Downloaded newer image for sandboxregistry:5000/test/trusttest@sha256:1d871dcb16805f0604f10d31260e79c22070b35abc71a3d1e7ee54f1042c8c7c + Tagging sandboxregistry:5000/test/trusttest@sha256:1d871dcb16805f0604f10d31260e79c22070b35abc71a3d1e7ee54f1042c8c7c as sandboxregistry:5000/test/trusttest:latest + + +### Test with malicious images + +What happens when data is corrupted and you try to pull it when trust is +enabled? In this section, you go into the `sandboxregistry` and tamper with some +data. Then, you try and pull it. + +1. Leave the sandbox container running. + +2. Open a new bash terminal from your host into the `sandboxregistry`. + + $ docker exec -it sandboxregistry bash + 296db6068327# + +3. Change into the registry storage. + + You'll need to provide the `sha` you received when you pushed the image. + + # cd /var/lib/registry/docker/registry/v2/blobs/sha256/aa/aac0c133338db2b18ff054943cee3267fe50c75cdee969aed88b1992539ed042 + +4. Add malicious data to one of the trusttest layers: + + # echo "Malicious data" > data + +5. Got back to your sandbox terminal. + +6. List the trusttest image. + + # docker images | grep trusttest + docker/trusttest latest a9539b34a6ab 7 weeks ago 5.025 MB + sandboxregistry:5000/test/trusttest latest a9539b34a6ab 7 weeks ago 5.025 MB + sandboxregistry:5000/test/trusttest a9539b34a6ab 7 weeks ago 5.025 MB + +7. Remove the `trusttest:latest` image. + + # docker rmi -f a9539b34a6ab + Untagged: docker/trusttest:latest + Untagged: sandboxregistry:5000/test/trusttest:latest + Untagged: sandboxregistry:5000/test/trusttest@sha256:1d871dcb16805f0604f10d31260e79c22070b35abc71a3d1e7ee54f1042c8c7c + Deleted: a9539b34a6aba01d3942605dfe09ab821cd66abf3cf07755b0681f25ad81f675 + Deleted: b3dbab3810fc299c21f0894d39a7952b363f14520c2f3d13443c669b63b6aa20 + +8. Pull the image again. + + # docker pull sandboxregistry:5000/test/trusttest + Using default tag: latest + ... + b3dbab3810fc: Verifying Checksum + a9539b34a6ab: Pulling fs layer + filesystem layer verification failed for digest sha256:aac0c133338db2b18ff054943cee3267fe50c75cdee969aed88b1992539ed042 + + You'll see the pull did not complete because the trust system was + unable to verify the image. + +## More play in the sandbox + +Now, that you have a full Docker content trust sandbox on your local system, +feel free to play with it and see how it behaves. If you find any security +issues with Docker, feel free to send us an email at . + + +  diff --git a/docs/static_files/README.md b/docs/static_files/README.md new file mode 100644 index 00000000..0b93167b --- /dev/null +++ b/docs/static_files/README.md @@ -0,0 +1,17 @@ + + +Static files dir +================ + +Files you put in /static_files/ will be copied to the web visible /_static/ + +Be careful not to override pre-existing static files from the template. + +Generally, layout related files should go in the /theme directory. + +If you want to add images to your particular documentation page. Just put them next to +your .rst source file and reference them relatively. diff --git a/docs/static_files/contributors.png b/docs/static_files/contributors.png new file mode 100644 index 0000000000000000000000000000000000000000..63c0a0c09b58bce2e1ade867760a937612934202 GIT binary patch literal 23100 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 literal 0 HcmV?d00001 diff --git a/docs/static_files/docker-logo-compressed.png b/docs/static_files/docker-logo-compressed.png new file mode 100644 index 0000000000000000000000000000000000000000..717d09d773cc46ff8297d06f66d9aa855453f35a GIT binary patch literal 4972 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 IT4fCOKM4~1IsgCw literal 0 HcmV?d00001 diff --git a/docs/static_files/docker_pull_chart.png b/docs/static_files/docker_pull_chart.png new file mode 100644 index 0000000000000000000000000000000000000000..57d3f68dbd07967b70a19f8d486bca2e04cfc5b6 GIT binary patch literal 7188 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 oN!z;P&tc~N%j5dLEMPet^SNh-$ZevJUtu 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 literal 0 HcmV?d00001 diff --git a/docs/static_files/dockerlogo-v.png b/docs/static_files/dockerlogo-v.png new file mode 100644 index 0000000000000000000000000000000000000000..69ae6851721e2bcb603b73da49c0c53ffefa70aa GIT binary patch literal 9670 zcmaKS1yq~Cwl1y#f`KiTAbik99k$8ibE+@ z++KR`IcL3l?mKT~*7|3Evvu~KnScJ3=$9IbL?K3io_@Shg>w2PPwo7_FRzeBeJHK29T*w)kBl{PaMLumnVOxeYyKRO zkdjqg<{I>tI53Yhr=LGOpRMkv5Y!DLt(V)%`$^?jh*unaV4A3|iIA2NR^%t*Lu;1$@3{HB=n3tZ8GX#ndxV*7Sf!(sk>$AIRWxo0a9}E7NC|pk2X6H;@yAvC zniY(cZXq3G=~pJXEgetHJqR;;Fy8lIR4!o^Pixz`0P}}AQybBfI{6H38SO$Jd&Xg; zBGKQqVWju+nK}oiWYRb8G*2&3Min{redmF?mrX52<`fO?{I2dFQK}du^UYxNj%Us6 zuAg5ai>sxs-waC5F!u_`|F%`yJrGw~MTDpu{JEtDweyIMm(@3Io|t8;ns$5}4a+I& zTitl#5S%=-QN6OO)jHq0{;TKPvVgt?AffI3$A+5W35xJSvbuHJ#_f@f-O0V*6T1hD zO}mzUp>mBgBDGT^>)Z4tqwp_lnRQLfZJRzFQ#4_D!rHIFc}NaxU-SAg!kQmb`-iU_ z-Tj7F>_Z~BK729l`le!LlQs4YC$+10WqoLUo6R9)Y-^u5sDRcl1uMD%yXw2Jv2Fa| zJaYM`h_2~VOZS;yM*@ac-QU;v%$$G`WyNjXz}hv>*hE%$7)`@gLRr=L_Ae$^_!A2+ z(EASR;4FHVD6!&xGXE@^H}8__+Xxa{1oSO+YevG6vlO2;oRV{B-e&Py`{0$%GW#R| z!%O&We3d_r5eMa)c=%mO>)N3T7u{1^L#K;Y)H?tTjrohRoV2bl`mdQf)Cd|zZRc$q zrV`Y{fPfAn1ps39q5ZGNe}I9Q|4;7!2=}4=KXm_9@gHEw|Muj+44R`{$L|FL;abL5 zU}naebK!5ju{JGkv`Xq2(>F2e+t(swATm%=cf$YpP2Oc5N>GykOdvnJ9Na+|n1C>J zcsH7?c_z^ls0go{h)SL-*ic$V~x2pm#FVk8=@?t`2$5%iXU5fJH;A&hLn z#w$`ud_xKi6-5+T;)hI5S0M82*A^^k;u#dCm}3l{BhjFs!AP=`22aLy?2iX82Z|!0 zY4&l+qf7uPkP!2(01yXx$(k49<$KAVw;kz9Ok%cqnn(Beq*wD2N-;2+oZP>Voc!@> z?E)9qjQ^qc;>i>Kt2GCZ>xrvy?)7o+BxflM3-pd)| zAcx3cT4j?kclv8pdZ-k<*DZR#5w(rb!O$~^dJ7T;4XO}a$j~Y{RKaKESfGj-8R`Vr zDwxc!3m9>8Mlk_;^Iv8yzlKMH8Iz$cjgUK%L`1>IIAwg7ZeCibR1>nCoEog* ziMb*LN~i`u>ht>A0ryl@1SM2}jn{Z_wzbL;U`8G2OS>T~41#AUlN4|bCidy#gjg`6 zEL2ziVx=yT6BlZQiv&k;gA2UlMMXTIXvm}VbVa5of~X$2IdbhHirbbKl^aZU_hAkj z9#l`gNjgiWqzZuO3aP-=A zz=P0IDmwy|7^5UObU0#7r~EB=76zWqyV@$<7o?G22CL1m#iZK|m+vv?f$%V)V_&Hv zc7C`0oK+*^O3RKpfueLyF@CXp?b|1$!soo1`oSUcZ=$7*6}bR@soKtrh-J8{CJAfy zH$T(ZU-yKY(DrH#UlM^7Wnc8%n**Cp#*UXsL{S?1HzOy9Nb>bf7*0j5VUI6KK2ACB&!osMX=`%xbaJ3<}?vEG;XA z5#peBjtg#;j<$t<{RP z>v&#aMC=8_Uo+t{9^UZ_lJ@wwQYubZaeo&wTR5EOw+?UEg=b}=4jM`*i=PHP_DW54 zbx*rML>r|x>TAu;_^c$Ecfl;*B#Oq<-Mx=sG83;NWGTZ!c_ybua0VxNoUmucbqoQZ z09M_qfRRhSV*<_^Cl&xUBcQ@if&flp$mAui{gDvG(6=Y&^N|L>wWU?7_Af2xnZ_ZY zDfV)S4ZBSWgeNN{V&88#&37$A;QhA-Lv>0iXQ0Ti#bew;CD}MW)V2pK2k}*B-VC>b z;-hqI59X9yeaymo$aN78`&u$A(+9uXGfg+k)|Q++Ef0UDKKDjK|~2;@E- zE3$Yko|r^ktT*gEn?q%&hjO2Y8A^3RzOsHv{AIR$BPASs6bN`Nq85u~IHc)ocER6T ztr`P4%hpDjFW)(#bnbX@*Lh znl)dB3NL)ayIzR3=2pXj@pcm;%Ks=?~w`D*y19XC?o;1w3c z)3(W0ifIoOSx^ux(S~~QnY)XI7G5}TAiVwFeddcjF)A6~SWIn$$(;>qgkcZ`5U2N4 zys+>~I^-l!!QaHL`m4d;gzfxP@gID1#so+RKG{$@`s07bgvbN29$2U`30_P|gC2#$ zOzc?L*e`$zjZxs~i}$4^o;|hnuwz!wg;&gghi`c=CnHwhYQ6teXsig*3?J!V?!9~~ z{>j@l{SCEr^1>$~W>;b33Sc=8xQF*&B?sLR zoj>WzsY`hp&_K~}@w9rOm8Q(Cr(ALkNeY9K>Ccx?lM*29G%o&#q~|?*s=R&sczJL9 zTtMvwb88@f?w`~)b6vHc#<1gtoAnf@XfRCYN3g9~Nca+V;gDzdVf6%q_321s(aLmG;Hl{xbtWg zYRytG^C#|g=d{oR4kSByGb+hmPQ7|p4f3`!{fvKEFV$WbOxC`dUa_FzEV}0}$1Tym z@M?KT-G50ewk$w{8(FLBsjvK_l?LO2zHKfSo9yFvtlN5ge9ugtMDH@i3qqTNrxMj7 zzfGrGPkr?3NPcIKO>hPv1%L{5Y8P&f@>A!}>W&vt=jMp3W~QlB7M5Vh#Uw$N<}-%c z$jjd|7c4Pr&VL$9_D>I}5h@*O7qy_pCxvC@`!zDN8;{M2z%z9si(lG3TBvq2`Mzx+op0+DqX^ub^QA zuzFgtK_db3Yo$Z@566BHo5v7f2J>c2y>h5UToAu zJ+J)=q~gatQI;8+bUrDWOuBL5u{gJCv2RL$e$_~-;IEb5cNgCg5Ma-3vMw4-YI=g8 zK4Fecm#q>KF=b<=PzGbMQlGOknMTFr>sB)Nzp5S!jePvUz`jzPqE&Um$>#a5UFbQT z&3U)eSAZa3Od?O@#-{$_UehM7LL!cvo!byMxiL8JVQutA?9e|hR1`R{B5oc+oTv7Q`j0vmxA=s6XO+mAB7$A0uc7b&nH(#Ks7G*G(x{xDB57-(j!NFcX zPou4k(T<4Bm)kB2yrNidHrTwfzBeSD^!hI1GE$lj8%ie`I1K8WH<9NXL7iQ_LaGW3 zO~1L=FBJ%&qMpM;gw-z1^wgMz>>+QC;wX?iKly3+%(~icDQYi|RkqbQuXqm~j=lZuT$Dopwm%D?y%i& z#DR@PRHKtc`9`>Y@qr=6F*nW45`^W47H2q|Kn$ke{x@|=YN7>RZo~BJ&LXY+Fql6~p&*d$7kjv!&H7W9h zTF2vrcVRHgcGt!%=|!#*6Ce-iI>CF-ltb-6{9=@WwA$?1o`8a2AzuUsm8)%J&)eI+ z`Ng}Nf=9jeUfR{JLwQw?zfsJw?2*T|+8ir@(5G(=qJ$(Njn&@LZ8bxMwCWJ~y05U; zxx~x>)2OgP@lwG1NNDmIDh2Y|e#U=Lx6wk2!nfI~dUPU@Nse@cyfqYRS}-Xmx!p zaSRhi0Hz|&CNXz!3dxW7h3IkB&x=@wZqzjaTCw2EAtgNMx4Bs_&MxlH$w~2$CQrtcTP(nczMb99;SsDl>j$&r~= z@ksyZysb=9Qq|}%#9~IZ=P@+XR0idsXL{k=a`WqA8z=2>(YxDK&K?)j23zgMHnsvkukjwq_!9xk1q&$NB{P#%-j)#f-FTOJH8G{`)9mhJX8I+9~b#ir%ous^6P8J3GpY_;^O*y{!-T@a4Knh~z9$%Yr0PKy`Nx_m z`n@QS<$scM{@ncv-_V4YR~7F)jGj6W$Hr=p?y*=pFtYG~l?~cR6*kS->0Yc?>`n7q z>3OK@L~y80b_!RaV%JR=1+&CO91rB?8%U`5(no-a7y7Mtl-F%PeJW#LktRCFtL#&- z%X>3Mh_}vY7BxC)xld>~MV^eq(V5ZcyMTZn<6PtqTk2KuXQzC;!Y#m``9vg#lX;fR{?}``POPt)T-K>`ss=R)QPb#5u`Y!` z96Eh9<@N7vRQiyN!thp}`|L>Yk$N~A|E%9Luay<9lrO~m1~fPg4O2@Jo&Ker^6RP2 zdN1)RBZ#2gb>I$XJCAXlNV7}{js8^MkLD`iRqWSDw2*Wagh~1K*UOjb- zP>ul&?TQe_{1s-=)w4I`q%+AnoYrtD#){pP#)59CUkBH>53=_)c41 z1t?OFSG0!pyw|gQBm7%sucJ^|VyxUx2Ozob`)RNC;dcbEFyXHT0?%H&xM^NnTl28y zFDD??lXx5-U+iZkLxESci)|!pRptiNVnl4Mp5pd4Jw0X#@Tpo-0AQN?rgVpeo6}jC zc^!8=s0!o9d~NnDx2Aa0c}OIB%P8tTVL(70AsymEx8I<^oRGN zk$BR`w1^sZEb}Gt(D|4$uvhr=T&!{>)l>uq6*K@*L`LSjb9gk&CdMuEeK7|BN7IZbH41}XDw*P1f zG9c7-=pPM)5B+<`{_keV3S$ENvmw_0B>`0UPQx%jCei_zU(dW+kuVYXIQ8HN3L5RG zKAXAD>ZmuIrszi=k-fzxe9L%=K6TSIoKds;Zs*s%>1MxWsZLLQ^({}|X*YWO9x?3< z+4@5YRom$`cg-L>tHNUaj9JAO7wI9}{Xbpq^GZ>5cW<0oZsR`(JH+MFu`m7#cJHh0 z^F_?Y;>_l{#MW7VL@yyR?&oQ;P7j&>ac!fMhP5BvS&Y8e&juIc4G4;EhH3-W|Db&y7d9nXU2gyl zKir9iO=GNjkTYOUlgXw}3SpE>M^DvK84z6!?6;0blb%s~dyV{-*GvUrpOA$|cBP26 zh(cr*u!K3@?VO1!GWMaF+LdX5^+?v86X>y1K}p4hobRNf>-5yM;+X9;bS|Z0NzYn9 z2l<+*7N^&UhQ0*L1)gA9Y8ESX%ZlB%to0Uh;(P zyW??dGuB`^gvrFu8B6Wg3W##aN!X3HV!<~EKg24^h(Oj<1zj*{AwHJ$7+vm0V5GR+ zKKfXIzoEx;D61!+CpPRwx!6=%;+IDYIfp(0Vf{nbkA;AyQ6C|-)N6^Ih~^FsW#!tw zuTM*#J34=_I;hczfeMw<%`fsAW#tT{jm5^( zez+6>GZ((Pp@fIO93;! zrT`?NXFyl!XMZY8Vv_UyVnYj%?>GJ^S&OgVR5KzKrbc{wVx|2_Z=JqxC5}P5GvssG z;8}k#vY>J@@|&OIR?D&aSKD7w>F~yyY9sRaNi~T{ji~0)^%A#DV-m+tc zL-OrtK_fzPRn(lE+1O!+uq*UlR1YYXg>Ue6%3J#JyP36SXbXMUJJT@ZgSgXoHq6H)Kx_sjuvNjdw&djcy+iSQktwb!sQN$G9OEx z(Wm3C9siN8#0wd@GiHTC-gYxmyYo?QdJw)ij5zVIRC}!Ujcaccwa&vb!Dz%W(5)u` z5D+^uMX$N;)HJ<7w3W-m}6fFPqo~l7SuStS`1ayIIkmW|V)Twajsq3&cHenK85JDNS}Fm~Qb1bxF+Hw| z!`pGJssL_JI+pDHuMCtdT=o!w`h>8ctKd}7u_5F{GiEKh7KHwahz}CntUE#+O`2z7 z$!ZV56U2kaNG~<0A{HidO#%mPsejyMQT>p>k4fZODK)XUby;e>`gX8*eYe!uPUCIY zHs8Hhei2jGcO)a*O4`X;64_UqWC64ZH4|9UGO1u2s08}wOG_G&y^X=Xz}-`?&K0tx z<-p&yd$G4{QMI?Iz7{qNYbr_wxmqTJJZb5;+xmV=ATNCa z&-l1;pm6aZ#g9FD;i`y>uFvx-V&7WL2A|4?q1*aZ8=~T!HsqsRE3-=jeJoxv>&B71 zg=;)H&cL|UG(kB8h`)IB+)SvY&v(vt;OCve$tQk%k4I0`?z7BVw#aAF&K+KGePA1S z{mrF|ton*1F;-#ZOMD8-t1i7yOJ+88_fLxQ(4+F;=<8F$2W0Ex>7Tjbl0(7S zxJI8VmVTv~f1q7=FGi)k-JA&(9&DHv17n64Mi+UE!(w*Kuy& z$mNr0kZH#@Z6wL2b|q#O6O%%LBoY_R+1R2zeZ)-UC;qMi4EuXU|HaW*?~ki5*)*?6 zR;6|MAuAHrI~osUt`{~EJvPC+zJleKH%VIcntx8TF4#pJ5)khWH3L)tF zE*Ppa#c&Bqz-Gt|&@=1zO4wM-f;jWs8we)BS9t^u<_AzU)SclDAt0-|@6+guQT>zE zO+-xd;cVb;x~e(svS>VC@>P>{{0pn-<026Fph%T+f7@XD+~JMvyx$#u>|seDGHf*( zTlYBZ(sHbO9R0y;|FN?-FT@n>(U$RT0z9;3PJ%<=_)8W>fs9>beHz(AiRrcfp96t= zU%@1Xd*5JBUsq`KF+n!YE1_reQn~QyGqHtnJpuZ_gwHUWisoHWTMucEhNC_Y}CtBoSjV$Ed1i3@yoo6dd=~J#I?&T+p*gNTh{*j-b4;V z%ZKU5hrg&U=@K=eX`uMTF$S^i9}I4tq~~|>rB?RIM8Y9S?3!EZuXrH$GZOT8XlQ7R zXp4FNp`?6~|N9RfzH#Ulp&&+_sDFF>JN-{4w10N~r_g_ZLqY$O?O)WtI{$9@-|2t& c0W_!7EgM}F9dVdO#e=3SuOU|>WB&I402zAIRR910 literal 0 HcmV?d00001 diff --git a/docs/touch-up.sh b/docs/touch-up.sh new file mode 100755 index 00000000..1dd0b1dc --- /dev/null +++ b/docs/touch-up.sh @@ -0,0 +1,20 @@ +#!/bin/bash -e + + +# Sed to process GitHub Markdown +# 1-2 Remove comment code from metadata block +# +for i in ls -l /docs/content/* + do # Line breaks are important + if [ -d $i ] # Spaces are important + then + y=${i##*/} + find $i -type f -name "*.md" -exec sed -i.old \ + -e '/^/g' \ + -e '/^/g' {} \; + fi +done + + + + diff --git a/docs/understanding-docker.md b/docs/understanding-docker.md new file mode 100644 index 00000000..1dba248e --- /dev/null +++ b/docs/understanding-docker.md @@ -0,0 +1,278 @@ + + +# Understand the architecture + +Docker is an open platform for developing, shipping, and running applications. +Docker is designed to deliver your applications faster. With Docker you can +separate your applications from your infrastructure and treat your +infrastructure like a managed application. Docker helps you ship code faster, +test faster, deploy faster, and shorten the cycle between writing code and +running code. + +Docker does this by combining kernel containerization features with workflows +and tooling that help you manage and deploy your applications. + +At its core, Docker provides a way to run almost any application securely +isolated in a container. The isolation and security allow you to run many +containers simultaneously on your host. The lightweight nature of containers, +which run without the extra load of a hypervisor, means you can get more out of +your hardware. + +Surrounding the container is tooling and a platform which can help you in +several ways: + +* Get your applications (and supporting components) into Docker containers +* Distribute and ship those containers to your teams for further development +and testing +* Deploy those applications to your production environment, + whether it is in a local data center or the Cloud + +## What can I use Docker for? + +*Faster delivery of your applications* + +Docker is perfect for helping you with the development lifecycle. Docker +allows your developers to develop on local containers that contain your +applications and services. It can then integrate into a continuous integration and +deployment workflow. + +For example, your developers write code locally and share their development stack via +Docker with their colleagues. When they are ready, they push their code and the +stack they are developing onto a test environment and execute any required +tests. From the testing environment, you can then push the Docker images into +production and deploy your code. + +*Deploying and scaling more easily* + +Docker's container-based platform allows for highly portable workloads. Docker +containers can run on a developer's local host, on physical or virtual machines +in a data center, or in the Cloud. + +Docker's portability and lightweight nature also make dynamically managing +workloads easy. You can use Docker to quickly scale up or tear down applications +and services. Docker's speed means that scaling can be near real time. + +*Achieving higher density and running more workloads* + +Docker is lightweight and fast. It provides a viable, cost-effective alternative +to hypervisor-based virtual machines. This is especially useful in high density +environments: for example, building your own Cloud or Platform-as-a-Service. But +it is also useful for small and medium deployments where you want to get more +out of the resources you have. + +## What are the major Docker components? +Docker has two major components: + + +* Docker Engine: the open source containerization platform. +* [Docker Hub](https://hub.docker.com): our Software-as-a-Service + platform for sharing and managing Docker containers. + + +> **Note:** Docker is licensed under the open source Apache 2.0 license. + +## What is Docker's architecture? +Docker uses a client-server architecture. The Docker *client* talks to the +Docker *daemon*, which does the heavy lifting of building, running, and +distributing your Docker containers. Both the Docker client and the daemon *can* +run on the same system, or you can connect a Docker client to a remote Docker +daemon. The Docker client and daemon communicate via sockets or through a +RESTful API. + +![Docker Architecture Diagram](article-img/architecture.svg) + +### The Docker daemon +As shown in the diagram above, the Docker daemon runs on a host machine. The +user does not directly interact with the daemon, but instead through the Docker +client. + +### The Docker client +The Docker client, in the form of the `docker` binary, is the primary user +interface to Docker. It accepts commands from the user and communicates back and +forth with a Docker daemon. + +### Inside Docker +To understand Docker's internals, you need to know about three resources: + +* Docker images +* Docker registries +* Docker containers + +#### Docker images + +A Docker image is a read-only template. For example, an image could contain an Ubuntu +operating system with Apache and your web application installed. Images are used to create +Docker containers. Docker provides a simple way to build new images or update existing +images, or you can download Docker images that other people have already created. +Docker images are the **build** component of Docker. + +#### Docker registries +Docker registries hold images. These are public or private stores from which you +upload or download images. The public Docker registry is provided with the +[Docker Hub](http://hub.docker.com). It serves a huge collection of existing +images for your use. These can be images you create yourself or you can use +images that others have previously created. Docker registries are the +**distribution** component of Docker. +For more information, go to [Docker Registry](https://docs.docker.com/registry/overview/) and +[Docker Trusted Registry](https://docs.docker.com/docker-trusted-registry/overview/). + +#### Docker containers +Docker containers are similar to a directory. A Docker container holds everything that +is needed for an application to run. Each container is created from a Docker +image. Docker containers can be run, started, stopped, moved, and deleted. Each +container is an isolated and secure application platform. Docker containers are the + **run** component of Docker. + +### How does a Docker image work? +We've already seen that Docker images are read-only templates from which Docker +containers are launched. Each image consists of a series of layers. Docker +makes use of [union file systems](http://en.wikipedia.org/wiki/UnionFS) to +combine these layers into a single image. Union file systems allow files and +directories of separate file systems, known as branches, to be transparently +overlaid, forming a single coherent file system. + +One of the reasons Docker is so lightweight is because of these layers. When you +change a Docker image—for example, update an application to a new version— a new layer +gets built. Thus, rather than replacing the whole image or entirely +rebuilding, as you may do with a virtual machine, only that layer is added or +updated. Now you don't need to distribute a whole new image, just the update, +making distributing Docker images faster and simpler. + +Every image starts from a base image, for example `ubuntu`, a base Ubuntu image, +or `fedora`, a base Fedora image. You can also use images of your own as the +basis for a new image, for example if you have a base Apache image you could use +this as the base of all your web application images. + +> **Note:** [Docker Hub](https://hub.docker.com) is a public registry and stores +images. + +Docker images are then built from these base images using a simple, descriptive +set of steps we call *instructions*. Each instruction creates a new layer in our +image. Instructions include actions like: + +* Run a command +* Add a file or directory +* Create an environment variable +* What process to run when launching a container from this image + +These instructions are stored in a file called a `Dockerfile`. A `Dockerfile` is +a text based script that contains instructions and commands for building the image +from the base image. Docker reads this `Dockerfile` when you request a build of +an image, executes the instructions, and returns a final image. + +### How does a Docker registry work? +The Docker registry is the store for your Docker images. Once you build a Docker +image you can *push* it to a public registry such as [Docker Hub](https://hub.docker.com) +or to your own registry running behind your firewall. + +Using the Docker client, you can search for already published images and then +pull them down to your Docker host to build containers from them. + +[Docker Hub](https://hub.docker.com) provides both public and private storage +for images. Public storage is searchable and can be downloaded by anyone. +Private storage is excluded from search results and only you and your users can +pull images down and use them to build containers. You can [sign up for a storage plan +here](https://hub.docker.com/plans). + +### How does a container work? +A container consists of an operating system, user-added files, and meta-data. As +we've seen, each container is built from an image. That image tells Docker +what the container holds, what process to run when the container is launched, and +a variety of other configuration data. The Docker image is read-only. When +Docker runs a container from an image, it adds a read-write layer on top of the +image (using a union file system as we saw earlier) in which your application can +then run. + +### What happens when you run a container? +Either by using the `docker` binary or via the API, the Docker client tells the Docker +daemon to run a container. + + $ docker run -i -t ubuntu /bin/bash + +The Docker Engine client is launched using the `docker` binary with the `run` option +running a new container. The bare minimum the Docker client needs to tell the +Docker daemon to run the container is: + +* What Docker image to build the container from, for example, `ubuntu` +* The command you want to run inside the container when it is launched, +for example,`/bin/bash` + +So what happens under the hood when we run this command? + +In order, Docker Engine does the following: + +- **Pulls the `ubuntu` image:** Docker Engine checks for the presence of the `ubuntu` +image. If the image already exists, then Docker Engine uses it for the new container. +If it doesn't exist locally on the host, then Docker Engine pulls it from +[Docker Hub](https://hub.docker.com). If the image already exists, then Docker Engine +uses it for the new container. +- **Creates a new container:** Once Docker Engine has the image, it uses it to create a +container. +- **Allocates a filesystem and mounts a read-write _layer_:** The container is created in +the file system and a read-write layer is added to the image. +- **Allocates a network / bridge interface:** Creates a network interface that allows the +Docker container to talk to the local host. +- **Sets up an IP address:** Finds and attaches an available IP address from a pool. +- **Executes a process that you specify:** Runs your application, and; +- **Captures and provides application output:** Connects and logs standard input, outputs +and errors for you to see how your application is running. + +You now have a running container! Now you can manage your container, interact with +your application and then, when finished, stop and remove your container. + +## The underlying technology +Docker is written in Go and makes use of several kernel features to +deliver the functionality we've seen. + +### Namespaces +Docker takes advantage of a technology called `namespaces` to provide the +isolated workspace we call the *container*. When you run a container, Docker +creates a set of *namespaces* for that container. + +This provides a layer of isolation: each aspect of a container runs in its own +namespace and does not have access outside it. + +Some of the namespaces that Docker Engine uses on Linux are: + + - **The `pid` namespace:** Process isolation (PID: Process ID). + - **The `net` namespace:** Managing network interfaces (NET: + Networking). + - **The `ipc` namespace:** Managing access to IPC + resources (IPC: InterProcess Communication). + - **The `mnt` namespace:** Managing mount-points (MNT: Mount). + - **The `uts` namespace:** Isolating kernel and version identifiers. (UTS: Unix +Timesharing System). + +### Control groups +Docker Engine on Linux also makes use of another technology called `cgroups` or control groups. +A key to running applications in isolation is to have them only use the +resources you want. This ensures containers are good multi-tenant citizens on a +host. Control groups allow Docker Engine to share available hardware resources to +containers and, if required, set up limits and constraints. For example, +limiting the memory available to a specific container. + +### Union file systems +Union file systems, or UnionFS, are file systems that operate by creating layers, +making them very lightweight and fast. Docker Engine uses union file systems to provide +the building blocks for containers. Docker Engine can make use of several union file system variants +including: AUFS, btrfs, vfs, and DeviceMapper. + +### Container format +Docker Engine combines these components into a wrapper we call a container format. The +default container format is called `libcontainer`. In the future, Docker may +support other container formats, for example, by integrating with BSD Jails +or Solaris Zones. + +## Next steps +Read about [Installing Docker Engine](installation/index.md#installation). +Learn about the [Docker Engine User Guide](userguide/index.md). diff --git a/docs/userguide/containers/dockerimages.md b/docs/userguide/containers/dockerimages.md new file mode 100644 index 00000000..7a8b9656 --- /dev/null +++ b/docs/userguide/containers/dockerimages.md @@ -0,0 +1,554 @@ + + +# Build your own images + +Docker images are the basis of containers. Each time you've used `docker run` +you told it which image you wanted. In the previous sections of the guide you +used Docker images that already exist, for example the `ubuntu` image and the +`training/webapp` image. + +You also discovered that Docker stores downloaded images on the Docker host. If +an image isn't already present on the host then it'll be downloaded from a +registry: by default the [Docker Hub Registry](https://hub.docker.com). + +In this section you're going to explore Docker images a bit more +including: + +* Managing and working with images locally on your Docker host. +* Creating basic images. +* Uploading images to [Docker Hub Registry](https://hub.docker.com). + +## Listing images on the host + +Let's start with listing the images you have locally on our host. You can +do this using the `docker images` command like so: + + $ docker images + REPOSITORY TAG IMAGE ID CREATED SIZE + ubuntu 14.04 1d073211c498 3 days ago 187.9 MB + busybox latest 2c5ac3f849df 5 days ago 1.113 MB + training/webapp latest 54bb4e8718e8 5 months ago 348.7 MB + +You can see the images you've previously used in the user guide. +Each has been downloaded from [Docker Hub](https://hub.docker.com) when you +launched a container using that image. When you list images, you get three crucial pieces of information in the listing. + +* What repository they came from, for example `ubuntu`. +* The tags for each image, for example `14.04`. +* The image ID of each image. + +> **Tip:** +> You can use [a third-party dockviz tool](https://github.com/justone/dockviz) +> or the [Image layers site](https://imagelayers.io/) to display +> visualizations of image data. + +A repository potentially holds multiple variants of an image. In the case of +our `ubuntu` image you can see multiple variants covering Ubuntu 10.04, 12.04, +12.10, 13.04, 13.10 and 14.04. Each variant is identified by a tag and you can +refer to a tagged image like so: + + ubuntu:14.04 + +So when you run a container you refer to a tagged image like so: + + $ docker run -t -i ubuntu:14.04 /bin/bash + +If instead you wanted to run an Ubuntu 12.04 image you'd use: + + $ docker run -t -i ubuntu:12.04 /bin/bash + +If you don't specify a variant, for example you just use `ubuntu`, then Docker +will default to using the `ubuntu:latest` image. + +> **Tip:** +> You should always specify an image tag, for example `ubuntu:14.04`. +> That way, you always know exactly what variant of an image you are using. +> This is useful for troubleshooting and debugging. + +## Getting a new image + +So how do you get new images? Well Docker will automatically download any image +you use that isn't already present on the Docker host. But this can potentially +add some time to the launch of a container. If you want to pre-load an image you +can download it using the `docker pull` command. Suppose you'd like to +download the `centos` image. + + $ docker pull centos + Pulling repository centos + b7de3133ff98: Pulling dependent layers + 5cc9e91966f7: Pulling fs layer + 511136ea3c5a: Download complete + ef52fb1fe610: Download complete + . . . + + Status: Downloaded newer image for centos + +You can see that each layer of the image has been pulled down and now you +can run a container from this image and you won't have to wait to +download the image. + + $ docker run -t -i centos /bin/bash + bash-4.1# + +## Finding images + +One of the features of Docker is that a lot of people have created Docker +images for a variety of purposes. Many of these have been uploaded to +[Docker Hub](https://hub.docker.com). You can search these images on the +[Docker Hub](https://hub.docker.com) website. + +![indexsearch](search.png) + +You can also search for images on the command line using the `docker search` +command. Suppose your team wants an image with Ruby and Sinatra installed on +which to do our web application development. You can search for a suitable image +by using the `docker search` command to find all the images that contain the +term `sinatra`. + + $ docker search sinatra + NAME DESCRIPTION STARS OFFICIAL AUTOMATED + training/sinatra Sinatra training image 0 [OK] + marceldegraaf/sinatra Sinatra test app 0 + mattwarren/docker-sinatra-demo 0 [OK] + luisbebop/docker-sinatra-hello-world 0 [OK] + bmorearty/handson-sinatra handson-ruby + Sinatra for Hands on with D... 0 + subwiz/sinatra 0 + bmorearty/sinatra 0 + . . . + +You can see the command returns a lot of images that use the term `sinatra`. +You've received a list of image names, descriptions, Stars (which measure the +social popularity of images - if a user likes an image then they can "star" it), +and the Official and Automated build statuses. [Official +Repositories](https://docs.docker.com/docker-hub/official_repos) are a carefully +curated set of Docker repositories supported by Docker, Inc. Automated +repositories are [Automated Builds](dockerrepos.md#automated-builds) that allow +you to validate the source and content of an image. + +You've reviewed the images available to use and you decided to use the +`training/sinatra` image. So far you've seen two types of images repositories, +images like `ubuntu`, which are called base or root images. These base images +are provided by Docker Inc and are built, validated and supported. These can be +identified by their single word names. + +You've also seen user images, for example the `training/sinatra` image you've +chosen. A user image belongs to a member of the Docker community and is built +and maintained by them. You can identify user images as they are always +prefixed with the user name, here `training`, of the user that created them. + +## Pulling our image + +You've identified a suitable image, `training/sinatra`, and now you can download it using the `docker pull` command. + + $ docker pull training/sinatra + +The team can now use this image by running their own containers. + + $ docker run -t -i training/sinatra /bin/bash + root@a8cb6ce02d85:/# + +## Creating our own images + +The team has found the `training/sinatra` image pretty useful but it's not quite +what they need and you need to make some changes to it. There are two ways you +can update and create images. + +1. You can update a container created from an image and commit the results to an image. +2. You can use a `Dockerfile` to specify instructions to create an image. + + +### Updating and committing an image + +To update an image you first need to create a container from the image +you'd like to update. + + $ docker run -t -i training/sinatra /bin/bash + root@0b2616b0e5a8:/# + +> **Note:** +> Take note of the container ID that has been created, `0b2616b0e5a8`, as you'll +> need it in a moment. + +Inside our running container let's add the `json` gem. + + root@0b2616b0e5a8:/# gem install json + +Once this has completed let's exit our container using the `exit` +command. + +Now you have a container with the change you want to make. You can then +commit a copy of this container to an image using the `docker commit` +command. + + $ docker commit -m "Added json gem" -a "Kate Smith" \ + 0b2616b0e5a8 ouruser/sinatra:v2 + 4f177bd27a9ff0f6dc2a830403925b5360bfe0b93d476f7fc3231110e7f71b1c + +Here you've used the `docker commit` command. You've specified two flags: `-m` +and `-a`. The `-m` flag allows us to specify a commit message, much like you +would with a commit on a version control system. The `-a` flag allows us to +specify an author for our update. + +You've also specified the container you want to create this new image from, +`0b2616b0e5a8` (the ID you recorded earlier) and you've specified a target for +the image: + + ouruser/sinatra:v2 + +Break this target down. It consists of a new user, `ouruser`, that you're +writing this image to. You've also specified the name of the image, here you're +keeping the original image name `sinatra`. Finally you're specifying a tag for +the image: `v2`. + +You can then look at our new `ouruser/sinatra` image using the `docker images` +command. + + $ docker images + REPOSITORY TAG IMAGE ID CREATED SIZE + training/sinatra latest 5bc342fa0b91 10 hours ago 446.7 MB + ouruser/sinatra v2 3c59e02ddd1a 10 hours ago 446.7 MB + ouruser/sinatra latest 5db5f8471261 10 hours ago 446.7 MB + +To use our new image to create a container you can then: + + $ docker run -t -i ouruser/sinatra:v2 /bin/bash + root@78e82f680994:/# + +### Building an image from a `Dockerfile` + +Using the `docker commit` command is a pretty simple way of extending an image +but it's a bit cumbersome and it's not easy to share a development process for +images amongst a team. Instead you can use a new command, `docker build`, to +build new images from scratch. + +To do this you create a `Dockerfile` that contains a set of instructions that +tell Docker how to build our image. + +First, create a directory and a `Dockerfile`. + + $ mkdir sinatra + $ cd sinatra + $ touch Dockerfile + +If you are using Docker Machine on Windows, you may access your host +directory by `cd` to `/c/Users/your_user_name`. + +Each instruction creates a new layer of the image. Try a simple example now for +building your own Sinatra image for your fictitious development team. + + # This is a comment + FROM ubuntu:14.04 + MAINTAINER Kate Smith + RUN apt-get update && apt-get install -y ruby ruby-dev + RUN gem install sinatra + +Examine what your `Dockerfile` does. Each instruction prefixes a statement and +is capitalized. + + INSTRUCTION statement + +> **Note:** You use `#` to indicate a comment + +The first instruction `FROM` tells Docker what the source of our image is, in +this case you're basing our new image on an Ubuntu 14.04 image. The instruction uses the `MAINTAINER` instruction to specify who maintains the new image. + +Lastly, you've specified two `RUN` instructions. A `RUN` instruction executes +a command inside the image, for example installing a package. Here you're +updating our APT cache, installing Ruby and RubyGems and then installing the +Sinatra gem. + + + +Now let's take our `Dockerfile` and use the `docker build` command to build an image. + + $ docker build -t ouruser/sinatra:v2 . + Sending build context to Docker daemon 2.048 kB + Sending build context to Docker daemon + Step 1 : FROM ubuntu:14.04 + ---> e54ca5efa2e9 + Step 2 : MAINTAINER Kate Smith + ---> Using cache + ---> 851baf55332b + Step 3 : RUN apt-get update && apt-get install -y ruby ruby-dev + ---> Running in 3a2558904e9b + Selecting previously unselected package libasan0:amd64. + (Reading database ... 11518 files and directories currently installed.) + Preparing to unpack .../libasan0_4.8.2-19ubuntu1_amd64.deb ... + Unpacking libasan0:amd64 (4.8.2-19ubuntu1) ... + Selecting previously unselected package libatomic1:amd64. + Preparing to unpack .../libatomic1_4.8.2-19ubuntu1_amd64.deb ... + Unpacking libatomic1:amd64 (4.8.2-19ubuntu1) ... + Selecting previously unselected package libgmp10:amd64. + Preparing to unpack .../libgmp10_2%3a5.1.3+dfsg-1ubuntu1_amd64.deb ... + Unpacking libgmp10:amd64 (2:5.1.3+dfsg-1ubuntu1) ... + Selecting previously unselected package libisl10:amd64. + Preparing to unpack .../libisl10_0.12.2-1_amd64.deb ... + Unpacking libisl10:amd64 (0.12.2-1) ... + Selecting previously unselected package libcloog-isl4:amd64. + Preparing to unpack .../libcloog-isl4_0.18.2-1_amd64.deb ... + Unpacking libcloog-isl4:amd64 (0.18.2-1) ... + Selecting previously unselected package libgomp1:amd64. + Preparing to unpack .../libgomp1_4.8.2-19ubuntu1_amd64.deb ... + Unpacking libgomp1:amd64 (4.8.2-19ubuntu1) ... + Selecting previously unselected package libitm1:amd64. + Preparing to unpack .../libitm1_4.8.2-19ubuntu1_amd64.deb ... + Unpacking libitm1:amd64 (4.8.2-19ubuntu1) ... + Selecting previously unselected package libmpfr4:amd64. + Preparing to unpack .../libmpfr4_3.1.2-1_amd64.deb ... + Unpacking libmpfr4:amd64 (3.1.2-1) ... + Selecting previously unselected package libquadmath0:amd64. + Preparing to unpack .../libquadmath0_4.8.2-19ubuntu1_amd64.deb ... + Unpacking libquadmath0:amd64 (4.8.2-19ubuntu1) ... + Selecting previously unselected package libtsan0:amd64. + Preparing to unpack .../libtsan0_4.8.2-19ubuntu1_amd64.deb ... + Unpacking libtsan0:amd64 (4.8.2-19ubuntu1) ... + Selecting previously unselected package libyaml-0-2:amd64. + Preparing to unpack .../libyaml-0-2_0.1.4-3ubuntu3_amd64.deb ... + Unpacking libyaml-0-2:amd64 (0.1.4-3ubuntu3) ... + Selecting previously unselected package libmpc3:amd64. + Preparing to unpack .../libmpc3_1.0.1-1ubuntu1_amd64.deb ... + Unpacking libmpc3:amd64 (1.0.1-1ubuntu1) ... + Selecting previously unselected package openssl. + Preparing to unpack .../openssl_1.0.1f-1ubuntu2.4_amd64.deb ... + Unpacking openssl (1.0.1f-1ubuntu2.4) ... + Selecting previously unselected package ca-certificates. + Preparing to unpack .../ca-certificates_20130906ubuntu2_all.deb ... + Unpacking ca-certificates (20130906ubuntu2) ... + Selecting previously unselected package manpages. + Preparing to unpack .../manpages_3.54-1ubuntu1_all.deb ... + Unpacking manpages (3.54-1ubuntu1) ... + Selecting previously unselected package binutils. + Preparing to unpack .../binutils_2.24-5ubuntu3_amd64.deb ... + Unpacking binutils (2.24-5ubuntu3) ... + Selecting previously unselected package cpp-4.8. + Preparing to unpack .../cpp-4.8_4.8.2-19ubuntu1_amd64.deb ... + Unpacking cpp-4.8 (4.8.2-19ubuntu1) ... + Selecting previously unselected package cpp. + Preparing to unpack .../cpp_4%3a4.8.2-1ubuntu6_amd64.deb ... + Unpacking cpp (4:4.8.2-1ubuntu6) ... + Selecting previously unselected package libgcc-4.8-dev:amd64. + Preparing to unpack .../libgcc-4.8-dev_4.8.2-19ubuntu1_amd64.deb ... + Unpacking libgcc-4.8-dev:amd64 (4.8.2-19ubuntu1) ... + Selecting previously unselected package gcc-4.8. + Preparing to unpack .../gcc-4.8_4.8.2-19ubuntu1_amd64.deb ... + Unpacking gcc-4.8 (4.8.2-19ubuntu1) ... + Selecting previously unselected package gcc. + Preparing to unpack .../gcc_4%3a4.8.2-1ubuntu6_amd64.deb ... + Unpacking gcc (4:4.8.2-1ubuntu6) ... + Selecting previously unselected package libc-dev-bin. + Preparing to unpack .../libc-dev-bin_2.19-0ubuntu6_amd64.deb ... + Unpacking libc-dev-bin (2.19-0ubuntu6) ... + Selecting previously unselected package linux-libc-dev:amd64. + Preparing to unpack .../linux-libc-dev_3.13.0-30.55_amd64.deb ... + Unpacking linux-libc-dev:amd64 (3.13.0-30.55) ... + Selecting previously unselected package libc6-dev:amd64. + Preparing to unpack .../libc6-dev_2.19-0ubuntu6_amd64.deb ... + Unpacking libc6-dev:amd64 (2.19-0ubuntu6) ... + Selecting previously unselected package ruby. + Preparing to unpack .../ruby_1%3a1.9.3.4_all.deb ... + Unpacking ruby (1:1.9.3.4) ... + Selecting previously unselected package ruby1.9.1. + Preparing to unpack .../ruby1.9.1_1.9.3.484-2ubuntu1_amd64.deb ... + Unpacking ruby1.9.1 (1.9.3.484-2ubuntu1) ... + Selecting previously unselected package libruby1.9.1. + Preparing to unpack .../libruby1.9.1_1.9.3.484-2ubuntu1_amd64.deb ... + Unpacking libruby1.9.1 (1.9.3.484-2ubuntu1) ... + Selecting previously unselected package manpages-dev. + Preparing to unpack .../manpages-dev_3.54-1ubuntu1_all.deb ... + Unpacking manpages-dev (3.54-1ubuntu1) ... + Selecting previously unselected package ruby1.9.1-dev. + Preparing to unpack .../ruby1.9.1-dev_1.9.3.484-2ubuntu1_amd64.deb ... + Unpacking ruby1.9.1-dev (1.9.3.484-2ubuntu1) ... + Selecting previously unselected package ruby-dev. + Preparing to unpack .../ruby-dev_1%3a1.9.3.4_all.deb ... + Unpacking ruby-dev (1:1.9.3.4) ... + Setting up libasan0:amd64 (4.8.2-19ubuntu1) ... + Setting up libatomic1:amd64 (4.8.2-19ubuntu1) ... + Setting up libgmp10:amd64 (2:5.1.3+dfsg-1ubuntu1) ... + Setting up libisl10:amd64 (0.12.2-1) ... + Setting up libcloog-isl4:amd64 (0.18.2-1) ... + Setting up libgomp1:amd64 (4.8.2-19ubuntu1) ... + Setting up libitm1:amd64 (4.8.2-19ubuntu1) ... + Setting up libmpfr4:amd64 (3.1.2-1) ... + Setting up libquadmath0:amd64 (4.8.2-19ubuntu1) ... + Setting up libtsan0:amd64 (4.8.2-19ubuntu1) ... + Setting up libyaml-0-2:amd64 (0.1.4-3ubuntu3) ... + Setting up libmpc3:amd64 (1.0.1-1ubuntu1) ... + Setting up openssl (1.0.1f-1ubuntu2.4) ... + Setting up ca-certificates (20130906ubuntu2) ... + debconf: unable to initialize frontend: Dialog + debconf: (TERM is not set, so the dialog frontend is not usable.) + debconf: falling back to frontend: Readline + debconf: unable to initialize frontend: Readline + debconf: (This frontend requires a controlling tty.) + debconf: falling back to frontend: Teletype + Setting up manpages (3.54-1ubuntu1) ... + Setting up binutils (2.24-5ubuntu3) ... + Setting up cpp-4.8 (4.8.2-19ubuntu1) ... + Setting up cpp (4:4.8.2-1ubuntu6) ... + Setting up libgcc-4.8-dev:amd64 (4.8.2-19ubuntu1) ... + Setting up gcc-4.8 (4.8.2-19ubuntu1) ... + Setting up gcc (4:4.8.2-1ubuntu6) ... + Setting up libc-dev-bin (2.19-0ubuntu6) ... + Setting up linux-libc-dev:amd64 (3.13.0-30.55) ... + Setting up libc6-dev:amd64 (2.19-0ubuntu6) ... + Setting up manpages-dev (3.54-1ubuntu1) ... + Setting up libruby1.9.1 (1.9.3.484-2ubuntu1) ... + Setting up ruby1.9.1-dev (1.9.3.484-2ubuntu1) ... + Setting up ruby-dev (1:1.9.3.4) ... + Setting up ruby (1:1.9.3.4) ... + Setting up ruby1.9.1 (1.9.3.484-2ubuntu1) ... + Processing triggers for libc-bin (2.19-0ubuntu6) ... + Processing triggers for ca-certificates (20130906ubuntu2) ... + Updating certificates in /etc/ssl/certs... 164 added, 0 removed; done. + Running hooks in /etc/ca-certificates/update.d....done. + ---> c55c31703134 + Removing intermediate container 3a2558904e9b + Step 4 : RUN gem install sinatra + ---> Running in 6b81cb6313e5 + unable to convert "\xC3" to UTF-8 in conversion from ASCII-8BIT to UTF-8 to US-ASCII for README.rdoc, skipping + unable to convert "\xC3" to UTF-8 in conversion from ASCII-8BIT to UTF-8 to US-ASCII for README.rdoc, skipping + Successfully installed rack-1.5.2 + Successfully installed tilt-1.4.1 + Successfully installed rack-protection-1.5.3 + Successfully installed sinatra-1.4.5 + 4 gems installed + Installing ri documentation for rack-1.5.2... + Installing ri documentation for tilt-1.4.1... + Installing ri documentation for rack-protection-1.5.3... + Installing ri documentation for sinatra-1.4.5... + Installing RDoc documentation for rack-1.5.2... + Installing RDoc documentation for tilt-1.4.1... + Installing RDoc documentation for rack-protection-1.5.3... + Installing RDoc documentation for sinatra-1.4.5... + ---> 97feabe5d2ed + Removing intermediate container 6b81cb6313e5 + Successfully built 97feabe5d2ed + +You've specified our `docker build` command and used the `-t` flag to identify +our new image as belonging to the user `ouruser`, the repository name `sinatra` +and given it the tag `v2`. + +You've also specified the location of our `Dockerfile` using the `.` to +indicate a `Dockerfile` in the current directory. + +> **Note:** +> You can also specify a path to a `Dockerfile`. + +Now you can see the build process at work. The first thing Docker does is +upload the build context: basically the contents of the directory you're +building in. This is done because the Docker daemon does the actual +build of the image and it needs the local context to do it. + +Next you can see each instruction in the `Dockerfile` being executed +step-by-step. You can see that each step creates a new container, runs +the instruction inside that container and then commits that change - +just like the `docker commit` work flow you saw earlier. When all the +instructions have executed you're left with the `97feabe5d2ed` image +(also helpfuly tagged as `ouruser/sinatra:v2`) and all intermediate +containers will get removed to clean things up. + +> **Note:** +> An image can't have more than 127 layers regardless of the storage driver. +> This limitation is set globally to encourage optimization of the overall +> size of images. + +You can then create a container from our new image. + + $ docker run -t -i ouruser/sinatra:v2 /bin/bash + root@8196968dac35:/# + +> **Note:** +> This is just a brief introduction to creating images. We've +> skipped a whole bunch of other instructions that you can use. We'll see more of +> those instructions in later sections of the Guide or you can refer to the +> [`Dockerfile`](../../reference/builder.md) reference for a +> detailed description and examples of every instruction. +> To help you write a clear, readable, maintainable `Dockerfile`, we've also +> written a [`Dockerfile` Best Practices guide](../eng-image/dockerfile_best-practices.md). + + +## Setting tags on an image + +You can also add a tag to an existing image after you commit or build it. We +can do this using the `docker tag` command. Now, add a new tag to your +`ouruser/sinatra` image. + + $ docker tag 5db5f8471261 ouruser/sinatra:devel + +The `docker tag` command takes the ID of the image, here `5db5f8471261`, and our +user name, the repository name and the new tag. + +Now, see your new tag using the `docker images` command. + + $ docker images ouruser/sinatra + REPOSITORY TAG IMAGE ID CREATED SIZE + ouruser/sinatra latest 5db5f8471261 11 hours ago 446.7 MB + ouruser/sinatra devel 5db5f8471261 11 hours ago 446.7 MB + ouruser/sinatra v2 5db5f8471261 11 hours ago 446.7 MB + +## Image Digests + +Images that use the v2 or later format have a content-addressable identifier +called a `digest`. As long as the input used to generate the image is +unchanged, the digest value is predictable. To list image digest values, use +the `--digests` flag: + + $ docker images --digests | head + REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE + ouruser/sinatra latest sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf 5db5f8471261 11 hours ago 446.7 MB + +When pushing or pulling to a 2.0 registry, the `push` or `pull` command +output includes the image digest. You can `pull` using a digest value. + + $ docker pull ouruser/sinatra@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf + +You can also reference by digest in `create`, `run`, and `rmi` commands, as well as the +`FROM` image reference in a Dockerfile. + +## Push an image to Docker Hub + +Once you've built or created a new image you can push it to [Docker +Hub](https://hub.docker.com) using the `docker push` command. This +allows you to share it with others, either publicly, or push it into [a +private repository](https://hub.docker.com/account/billing-plans/). + + $ docker push ouruser/sinatra + The push refers to a repository [ouruser/sinatra] (len: 1) + Sending image list + Pushing repository ouruser/sinatra (3 tags) + . . . + +## Remove an image from the host + +You can also remove images on your Docker host in a way [similar to +containers](usingdocker.md) using the `docker rmi` command. + +Delete the `training/sinatra` image as you don't need it anymore. + + $ docker rmi training/sinatra + Untagged: training/sinatra:latest + Deleted: 5bc342fa0b91cabf65246837015197eecfa24b2213ed6a51a8974ae250fedd8d + Deleted: ed0fffdcdae5eb2c3a55549857a8be7fc8bc4241fb19ad714364cbfd7a56b22f + Deleted: 5c58979d73ae448df5af1d8142436d81116187a7633082650549c52c3a2418f0 + +> **Note:** To remove an image from the host, please make sure +> that there are no containers actively based on it. + +# Next steps + +Until now you've seen how to build individual applications inside Docker +containers. Now learn how to build whole application stacks with Docker +by networking together multiple Docker containers. + +Go to [Network containers](networkingcontainers.md). diff --git a/docs/userguide/containers/dockerizing.md b/docs/userguide/containers/dockerizing.md new file mode 100644 index 00000000..92b02608 --- /dev/null +++ b/docs/userguide/containers/dockerizing.md @@ -0,0 +1,194 @@ + + +# Hello world in a container + +*So what's this Docker thing all about?* + +Docker allows you to run applications, worlds you create, inside containers. +Running an application inside a container takes a single command: `docker run`. + +>**Note**: Depending on your Docker system configuration, you may be required to +>preface each `docker` command on this page with `sudo`. To avoid this behavior, +>your system administrator can create a Unix group called `docker` and add users +>to it. + +## Run a Hello world + +Let's run a hello world container. + + $ docker run ubuntu /bin/echo 'Hello world' + Hello world + +You just launched your first container! + +In this example: + +* `docker run` runs a container. + +* `ubuntu` is the image you run, for example the Ubuntu operating system image. + When you specify an image, Docker looks first for the image on your + Docker host. If the image does not exist locally, then the image is pulled from the public + image registry [Docker Hub](https://hub.docker.com). + +* `/bin/echo` is the command to run inside the new container. + +The container launches. Docker creates a new Ubuntu +environment and executes the `/bin/echo` command inside it and then prints out: + + Hello world + +So what happened to the container after that? Well, Docker containers +only run as long as the command you specify is active. Therefore, in the above example, +the container stops once the command is executed. + +## Run an interactive container + +Let's specify a new command to run in the container. + + $ docker run -t -i ubuntu /bin/bash + root@af8bae53bdd3:/# + +In this example: + +* `docker run` runs a container. +* `ubuntu` is the image you would like to run. +* `-t` flag assigns a pseudo-tty or terminal inside the new container. +* `-i` flag allows you to make an interactive connection by +grabbing the standard in (`STDIN`) of the container. +* `/bin/bash` launches a Bash shell inside our container. + +The container launches. We can see there is a +command prompt inside it: + + root@af8bae53bdd3:/# + +Let's try running some commands inside the container: + + root@af8bae53bdd3:/# pwd + / + root@af8bae53bdd3:/# ls + bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var + +In this example: + +* `pwd` displays the current directory, the `/` root directory. +* `ls` displays the directory listing of the root directory of a typical Linux file system. + +Now, you can play around inside this container. When completed, run the `exit` command or enter Ctrl-D +to exit the interactive shell. + + root@af8bae53bdd3:/# exit + +>**Note:** As with our previous container, once the Bash shell process has +finished, the container stops. + +## Start a daemonized Hello world + +Let's create a container that runs as a daemon. + + $ docker run -d ubuntu /bin/sh -c "while true; do echo hello world; sleep 1; done" + 1e5535038e285177d5214659a068137486f96ee5c2e85a4ac52dc83f2ebe4147 + +In this example: + +* `docker run` runs the container. +* `-d` flag runs the container in the background (to daemonize it). +* `ubuntu` is the image you would like to run. + +Finally, we specify a command to run: + + /bin/sh -c "while true; do echo hello world; sleep 1; done" + + +In the output, we do not see `hello world` but a long string: + + 1e5535038e285177d5214659a068137486f96ee5c2e85a4ac52dc83f2ebe4147 + +This long string is called a *container ID*. It uniquely +identifies a container so we can work with it. + +> **Note:** +> The container ID is a bit long and unwieldy. Later, we will cover the short +> ID and ways to name our containers to make +> working with them easier. + +We can use this container ID to see what's happening with our `hello world` daemon. + +First, let's make sure our container is running. Run the `docker ps` command. +The `docker ps` command queries the Docker daemon for information about all the containers it knows +about. + + $ docker ps + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 1e5535038e28 ubuntu /bin/sh -c 'while tr 2 minutes ago Up 1 minute insane_babbage + +In this example, we can see our daemonized container. The `docker ps` returns some useful +information: + +* `1e5535038e28` is the shorter variant of the container ID. +* `ubuntu` is the used image. +* the command, status, and assigned name `insane_babbage`. + + +> **Note:** +> Docker automatically generates names for any containers started. +> We'll see how to specify your own names a bit later. + +Now, we know the container is running. But is it doing what we asked it to do? To +see this we're going to look inside the container using the `docker logs` +command. + +Let's use the container name `insane_babbage`. + + $ docker logs insane_babbage + hello world + hello world + hello world + . . . + +In this example: + +* `docker logs` looks inside the container and returns `hello world`. + +Awesome! The daemon is working and you have just created your first +Dockerized application! + +Next, run the `docker stop` command to stop our detached container. + + $ docker stop insane_babbage + insane_babbage + +The `docker stop` command tells Docker to politely stop the running +container and returns the name of the container it stopped. + +Let's check it worked with the `docker ps` command. + + $ docker ps + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + +Excellent. Our container is stopped. + +# Next steps + +So far, you launched your first containers using the `docker run` command. You +ran an *interactive container* that ran in the foreground. You also ran a +*detached container* that ran in the background. In the process you learned +about several Docker commands: + +* `docker ps` - Lists containers. +* `docker logs` - Shows us the standard output of a container. +* `docker stop` - Stops running containers. + +Now, you have the basis learn more about Docker and how to do some more advanced +tasks. Go to ["*Run a simple application*"](usingdocker.md) to actually build a +web application with the Docker client. diff --git a/docs/userguide/containers/dockerrepos.md b/docs/userguide/containers/dockerrepos.md new file mode 100644 index 00000000..8274adb8 --- /dev/null +++ b/docs/userguide/containers/dockerrepos.md @@ -0,0 +1,187 @@ + + +# Store images on Docker Hub + +So far you've learned how to use the command line to run Docker on your local +host. You've learned how to [pull down images](usingdocker.md) to build +containers from existing images and you've learned how to [create your own +images](dockerimages.md). + +Next, you're going to learn how to use the [Docker Hub](https://hub.docker.com) +to simplify and enhance your Docker workflows. + +The [Docker Hub](https://hub.docker.com) is a public registry maintained by +Docker, Inc. It contains images you can download and use to build +containers. It also provides authentication, work group structure, workflow +tools like webhooks and build triggers, and privacy tools like private +repositories for storing images you don't want to share publicly. + +## Docker commands and Docker Hub + +Docker itself provides access to Docker Hub services via the `docker search`, +`pull`, `login`, and `push` commands. This page will show you how these commands work. + +### Account creation and login +Before you try an Engine CLI command, if you haven't already, create a Docker +ID. You can do this through [Docker Hub](https://hub.docker.com/). Once you have +a Docker ID, log into your account from the Engine CLI: + +```bash +$ docker login +``` + +The `login` command stores your Docker ID authentication credentials in the +`$HOME/.docker/config.json` (Bash notation). For Windows `cmd` users the +notation for this file is `%HOME%\.docker\config.json` ; for PowerShell users +the notation is `$env:Home\.docker\config.json`. + +Once you have logged in from the command line, you can `commit` and `push` +Engine subcommands to interact with your repos on Docker Hub. + +## Searching for images + +You can search the [Docker Hub](https://hub.docker.com) registry via its search +interface or by using the command line interface. Searching can find images by image +name, user name, or description: + + $ docker search centos + NAME DESCRIPTION STARS OFFICIAL AUTOMATED + centos The official build of CentOS 1223 [OK] + tianon/centos CentOS 5 and 6, created using rinse instea... 33 + ... + +There you can see two example results: `centos` and `tianon/centos`. The second +result shows that it comes from the public repository of a user, named +`tianon/`, while the first result, `centos`, doesn't explicitly list a +repository which means that it comes from the trusted top-level namespace for +[Official Repositories](https://docs.docker.com/docker-hub/official_repos/). The `/` character separates +a user's repository from the image name. + +Once you've found the image you want, you can download it with `docker pull `: + + $ docker pull centos + Using default tag: latest + latest: Pulling from library/centos + f1b10cd84249: Pull complete + c852f6d61e65: Pull complete + 7322fbe74aa5: Pull complete + Digest: sha256:90305c9112250c7e3746425477f1c4ef112b03b4abe78c612e092037bfecc3b7 + Status: Downloaded newer image for centos:latest + +You now have an image from which you can run containers. + +### Specific Versions or Latest +Using `docker pull centos` is equivalent to using `docker pull centos:latest`. +To pull an image that is not the default latest image you can be more precise +with the image that you want. + +For example, to pull version 5 of `centos` use `docker pull centos:centos5`. +In this example, `centos5` is the tag labeling an image in the `centos` +repository for a version of `centos`. + +To find a list of tags pointing to currently available versions of a repository +see the [Docker Hub](https://hub.docker.com) registry. + +## Contributing to Docker Hub + +Anyone can pull public images from the [Docker Hub](https://hub.docker.com) +registry, but if you would like to share your own images, then you must +[register first](https://docs.docker.com/docker-hub/accounts). + +## Pushing a repository to Docker Hub + +In order to push a repository to its registry, you need to have named an image +or committed your container to a named image as we saw +[here](dockerimages.md). + +Now you can push this repository to the registry designated by its name or tag. + + $ docker push yourname/newimage + +The image will then be uploaded and available for use by your team-mates and/or the +community. + +## Features of Docker Hub + +Let's take a closer look at some of the features of Docker Hub. You can find more +information [here](https://docs.docker.com/docker-hub/). + +* Private repositories +* Organizations and teams +* Automated Builds +* Webhooks + +### Private repositories + +Sometimes you have images you don't want to make public and share with +everyone. So Docker Hub allows you to have private repositories. You can +sign up for a plan [here](https://hub.docker.com/account/billing-plans/). + +### Organizations and teams + +One of the useful aspects of private repositories is that you can share +them only with members of your organization or team. Docker Hub lets you +create organizations where you can collaborate with your colleagues and +manage private repositories. You can learn how to create and manage an organization +[here](https://hub.docker.com/organizations/). + +### Automated Builds + +Automated Builds automate the building and updating of images from +[GitHub](https://www.github.com) or [Bitbucket](http://bitbucket.com), directly on Docker +Hub. It works by adding a commit hook to your selected GitHub or Bitbucket repository, +triggering a build and update when you push a commit. + +#### To setup an Automated Build + +1. Create a [Docker Hub account](https://hub.docker.com/) and login. +2. Link your GitHub or Bitbucket account on the ["Linked Accounts & Services"](https://hub.docker.com/account/authorized-services/) page. +3. Select "Create Automated Build" from the "Create" dropdown menu +4. Pick a GitHub or Bitbucket project that has a `Dockerfile` that you want to build. +5. Pick the branch you want to build (the default is the `master` branch). +6. Give the Automated Build a name. +7. Assign an optional Docker tag to the Build. +8. Specify where the `Dockerfile` is located. The default is `/`. + +Once the Automated Build is configured it will automatically trigger a +build and, in a few minutes, you should see your new Automated Build on the [Docker Hub](https://hub.docker.com) +Registry. It will stay in sync with your GitHub and Bitbucket repository until you +deactivate the Automated Build. + +To check the output and status of your Automated Build repositories, click on a repository name within the ["Your Repositories" page](https://registry.hub.docker.com/repos/). Automated Builds are indicated by a check-mark icon next to the repository name. Within the repository details page, you may click on the "Build Details" tab to view the status and output of all builds triggered by the Docker Hub. + +Once you've created an Automated Build you can deactivate or delete it. You +cannot, however, push to an Automated Build with the `docker push` command. +You can only manage it by committing code to your GitHub or Bitbucket +repository. + +You can create multiple Automated Builds per repository and configure them +to point to specific `Dockerfile`'s or Git branches. + +#### Build triggers + +Automated Builds can also be triggered via a URL on Docker Hub. This +allows you to rebuild an Automated build image on demand. + +### Webhooks + +Webhooks are attached to your repositories and allow you to trigger an +event when an image or updated image is pushed to the repository. With +a webhook you can specify a target URL and a JSON payload that will be +delivered when the image is pushed. + +See the Docker Hub documentation for [more information on +webhooks](https://docs.docker.com/docker-hub/repos/#webhooks) + +## Next steps + +Go and use Docker! diff --git a/docs/userguide/containers/dockervolumes.md b/docs/userguide/containers/dockervolumes.md new file mode 100644 index 00000000..9c960d1f --- /dev/null +++ b/docs/userguide/containers/dockervolumes.md @@ -0,0 +1,372 @@ + + +# Manage data in containers + +So far you've been introduced to some [basic Docker +concepts](../containers/usingdocker.md), seen how to work with [Docker +images](../containers/dockerimages.md) as well as learned about [networking and +links between containers](../networking/default_network/dockerlinks.md). In this +section you're going to learn how you can manage data inside and between your +Docker containers. + +You're going to look at the two primary ways you can manage data with +Docker Engine. + +* Data volumes +* Data volume containers + +## Data volumes + +A *data volume* is a specially-designated directory within one or more +containers that bypasses the [*Union File System*](../../reference/glossary.md#union-file-system). Data volumes provide several useful features for persistent or shared data: + +- Volumes are initialized when a container is created. If the container's + base image contains data at the specified mount point, that existing data is + copied into the new volume upon volume initialization. (Note that this does + not apply when [mounting a host directory](#mount-a-host-directory-as-a-data-volume).) +- Data volumes can be shared and reused among containers. +- Changes to a data volume are made directly. +- Changes to a data volume will not be included when you update an image. +- Data volumes persist even if the container itself is deleted. + +Data volumes are designed to persist data, independent of the container's life +cycle. Docker therefore *never* automatically deletes volumes when you remove +a container, nor will it "garbage collect" volumes that are no longer +referenced by a container. + +### Adding a data volume + +You can add a data volume to a container using the `-v` flag with the +`docker create` and `docker run` command. You can use the `-v` multiple times +to mount multiple data volumes. Now, mount a single volume in your web +application container. + +```bash +$ docker run -d -P --name web -v /webapp training/webapp python app.py +``` + +This will create a new volume inside a container at `/webapp`. + +> **Note:** +> You can also use the `VOLUME` instruction in a `Dockerfile` to add one or +> more new volumes to any container created from that image. + +### Locating a volume + +You can locate the volume on the host by utilizing the `docker inspect` command. + +```bash +$ docker inspect web +``` + +The output will provide details on the container configurations including the +volumes. The output should look something similar to the following: + +```json +... +"Mounts": [ + { + "Name": "fac362...80535", + "Source": "/var/lib/docker/volumes/fac362...80535/_data", + "Destination": "/webapp", + "Driver": "local", + "Mode": "", + "RW": true, + "Propagation": "" + } +] +... +``` + +You will notice in the above `Source` is specifying the location on the host and +`Destination` is specifying the volume location inside the container. `RW` shows +if the volume is read/write. + +### Mount a host directory as a data volume + +In addition to creating a volume using the `-v` flag you can also mount a +directory from your Engine daemon's host into a container. + +```bash +$ docker run -d -P --name web -v /src/webapp:/opt/webapp training/webapp python app.py +``` + +This command mounts the host directory, `/src/webapp`, into the container at +`/opt/webapp`. If the path `/opt/webapp` already exists inside the container's +image, the `/src/webapp` mount overlays but does not remove the pre-existing +content. Once the mount is removed, the content is accessible again. This is +consistent with the expected behavior of the `mount` command. + +The `container-dir` must always be an absolute path such as `/src/docs`. +The `host-dir` can either be an absolute path or a `name` value. If you +supply an absolute path for the `host-dir`, Docker bind-mounts to the path +you specify. If you supply a `name`, Docker creates a named volume by that `name`. + +A `name` value must start with an alphanumeric character, +followed by `a-z0-9`, `_` (underscore), `.` (period) or `-` (hyphen). +An absolute path starts with a `/` (forward slash). + +For example, you can specify either `/foo` or `foo` for a `host-dir` value. +If you supply the `/foo` value, Engine creates a bind-mount. If you supply +the `foo` specification, Engine creates a named volume. + +If you are using Docker Machine on Mac or Windows, your Engine daemon has only +limited access to your OS X or Windows filesystem. Docker Machine tries to +auto-share your `/Users` (OS X) or `C:\Users` (Windows) directory. So, you can +mount files or directories on OS X using. + +```bash +docker run -v /Users/:/ ... +``` + +On Windows, mount directories using: + +```bash +docker run -v /c/Users/:/ ...` +``` + +All other paths come from your virtual machine's filesystem, so if you want +to make some other host folder available for sharing, you need to do +additional work. In the case of VirtualBox you need to make the host folder +available as a shared folder in VirtualBox. Then, you can mount it using the +Docker `-v` flag. + +Mounting a host directory can be useful for testing. For example, you can mount +source code inside a container. Then, change the source code and see its effect +on the application in real time. The directory on the host must be specified as +an absolute path and if the directory doesn't exist the Engine daemon automatically +creates it for you. + +Docker volumes default to mount in read-write mode, but you can also set it to +be mounted read-only. + +```bash +$ docker run -d -P --name web -v /src/webapp:/opt/webapp:ro training/webapp python app.py +``` + +Here you've mounted the same `/src/webapp` directory but you've added the `ro` +option to specify that the mount should be read-only. + +Because of [limitations in the `mount` +function](http://lists.linuxfoundation.org/pipermail/containers/2015-April/035788.html), +moving subdirectories within the host's source directory can give +access from the container to the host's file system. This requires a malicious +user with access to host and its mounted directory. + +>**Note**: The host directory is, by its nature, host-dependent. For this +>reason, you can't mount a host directory from `Dockerfile` because built images +>should be portable. A host directory wouldn't be available on all potential +>hosts. + +### Mount a shared-storage volume as a data volume + +In addition to mounting a host directory in your container, some Docker +[volume plugins](../../extend/plugins_volume.md) allow you to +provision and mount shared storage, such as iSCSI, NFS, or FC. + +A benefit of using shared volumes is that they are host-independent. This +means that a volume can be made available on any host that a container is +started on as long as it has access to the shared storage backend, and has +the plugin installed. + +One way to use volume drivers is through the `docker run` command. +Volume drivers create volumes by name, instead of by path like in +the other examples. + +The following command creates a named volume, called `my-named-volume`, +using the `flocker` volume driver, and makes it available within the container +at `/opt/webapp`: + +```bash +$ docker run -d -P \ + --volume-driver=flocker \ + -v my-named-volume:/opt/webapp \ + --name web training/webapp python app.py +``` + +You may also use the `docker volume create` command, to create a volume before +using it in a container. + +The following example also creates the `my-named-volume` volume, this time +using the `docker volume create` command. + +```bash +$ docker volume create -d flocker --name my-named-volume -o size=20GB +$ docker run -d -P \ + -v my-named-volume:/opt/webapp \ + --name web training/webapp python app.py +``` + +A list of available plugins, including volume plugins, is available +[here](../../extend/plugins.md). + +### Volume labels + +Labeling systems like SELinux require that proper labels are placed on volume +content mounted into a container. Without a label, the security system might +prevent the processes running inside the container from using the content. By +default, Docker does not change the labels set by the OS. + +To change a label in the container context, you can add either of two suffixes +`:z` or `:Z` to the volume mount. These suffixes tell Docker to relabel file +objects on the shared volumes. The `z` option tells Docker that two containers +share the volume content. As a result, Docker labels the content with a shared +content label. Shared volume labels allow all containers to read/write content. +The `Z` option tells Docker to label the content with a private unshared label. +Only the current container can use a private volume. + +### Mount a host file as a data volume + +The `-v` flag can also be used to mount a single file - instead of *just* +directories - from the host machine. + +```bash +$ docker run --rm -it -v ~/.bash_history:/root/.bash_history ubuntu /bin/bash +``` + +This will drop you into a bash shell in a new container, you will have your bash +history from the host and when you exit the container, the host will have the +history of the commands typed while in the container. + +> **Note:** +> Many tools used to edit files including `vi` and `sed --in-place` may result +> in an inode change. Since Docker v1.1.0, this will produce an error such as +> "*sed: cannot rename ./sedKdJ9Dy: Device or resource busy*". In the case where +> you want to edit the mounted file, it is often easiest to instead mount the +> parent directory. + +## Creating and mounting a data volume container + +If you have some persistent data that you want to share between +containers, or want to use from non-persistent containers, it's best to +create a named Data Volume Container, and then to mount the data from +it. + +Let's create a new named container with a volume to share. +While this container doesn't run an application, it reuses the `training/postgres` +image so that all containers are using layers in common, saving disk space. + +```bash +$ docker create -v /dbdata --name dbstore training/postgres /bin/true +``` + +You can then use the `--volumes-from` flag to mount the `/dbdata` volume in another container. + +```bash +$ docker run -d --volumes-from dbstore --name db1 training/postgres +``` + +And another: + +```bash +$ docker run -d --volumes-from dbstore --name db2 training/postgres +``` + +In this case, if the `postgres` image contained a directory called `/dbdata` +then mounting the volumes from the `dbstore` container hides the +`/dbdata` files from the `postgres` image. The result is only the files +from the `dbstore` container are visible. + +You can use multiple `--volumes-from` parameters to combine data volumes from +several containers. To find detailed information about `--volumes-from` see the +[Mount volumes from container](../../reference/commandline/run.md#mount-volumes-from-container-volumes-from) +in the `run` command reference. + +You can also extend the chain by mounting the volume that came from the +`dbstore` container in yet another container via the `db1` or `db2` containers. + +```bash +$ docker run -d --name db3 --volumes-from db1 training/postgres +``` + +If you remove containers that mount volumes, including the initial `dbstore` +container, or the subsequent containers `db1` and `db2`, the volumes will not +be deleted. To delete the volume from disk, you must explicitly call +`docker rm -v` against the last container with a reference to the volume. This +allows you to upgrade, or effectively migrate data volumes between containers. + +> **Note:** Docker will not warn you when removing a container *without* +> providing the `-v` option to delete its volumes. If you remove containers +> without using the `-v` option, you may end up with "dangling" volumes; +> volumes that are no longer referenced by a container. +> You can use `docker volume ls -f dangling=true` to find dangling volumes, +> and use `docker volume rm ` to remove a volume that's +> no longer needed. + +## Backup, restore, or migrate data volumes + +Another useful function we can perform with volumes is use them for +backups, restores or migrations. You do this by using the +`--volumes-from` flag to create a new container that mounts that volume, +like so: + +```bash +$ docker run --rm --volumes-from dbstore -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /dbdata +``` + +Here you've launched a new container and mounted the volume from the +`dbstore` container. You've then mounted a local host directory as +`/backup`. Finally, you've passed a command that uses `tar` to backup the +contents of the `dbdata` volume to a `backup.tar` file inside our +`/backup` directory. When the command completes and the container stops +we'll be left with a backup of our `dbdata` volume. + +You could then restore it to the same container, or another that you've made +elsewhere. Create a new container. + +```bash +$ docker run -v /dbdata --name dbstore2 ubuntu /bin/bash +``` + +Then un-tar the backup file in the new container`s data volume. + +```bash +$ docker run --rm --volumes-from dbstore2 -v $(pwd):/backup ubuntu bash -c "cd /dbdata && tar xvf /backup/backup.tar --strip 1" +``` + +You can use the techniques above to automate backup, migration and +restore testing using your preferred tools. + +## Removing volumes + +A Docker data volume persists after a container is deleted. You can create named +or anonymous volumes. Named volumes have a specific source form outside the +container, for example `awesome:/bar`. Anonymous volumes have no specific +source. When the container is deleted, you should instruction the Engine daemon +to clean up anonymous volumes. To do this, use the `--rm` option, for example: + +```bash +$ docker run --rm -v /foo -v awesome:/bar busybox top, +``` + +This command creates an anonymous `/foo` volume. When the container is removed, +Engine removes the `/foo` volume but not the `awesome` volume. + +## Important tips on using shared volumes + +Multiple containers can also share one or more data volumes. However, multiple +containers writing to a single shared volume can cause data corruption. Make +sure your applications are designed to write to shared data stores. + +Data volumes are directly accessible from the Docker host. This means you can +read and write to them with normal Linux tools. In most cases you should not do +this as it can cause data corruption if your containers and applications are +unaware of your direct access. + +# Next steps + +Now you've learned a bit more about how to use Docker we're going to see how to +combine Docker with the services available on +[Docker Hub](https://hub.docker.com) including Automated Builds and private +repositories. + +Go to [Working with Docker Hub](../containers/dockerrepos.md). diff --git a/docs/userguide/containers/index.md b/docs/userguide/containers/index.md new file mode 100644 index 00000000..2de0dcce --- /dev/null +++ b/docs/userguide/containers/index.md @@ -0,0 +1,19 @@ + + +# Learn by example + +* [Hello world in a container](dockerizing.md) +* [Run a simple application](usingdocker.md) +* [Build your own images](dockerimages.md) +* [Network containers](networkingcontainers.md) +* [Manage data in containers](dockervolumes.md) +* [Store images on Docker Hub](dockerrepos.md) diff --git a/docs/userguide/containers/networkingcontainers.md b/docs/userguide/containers/networkingcontainers.md new file mode 100644 index 00000000..9355e756 --- /dev/null +++ b/docs/userguide/containers/networkingcontainers.md @@ -0,0 +1,247 @@ + + + +# Network containers + +If you are working your way through the user guide, you just built and ran a +simple application. You've also built in your own images. This section teaches +you how to network your containers. + +## Name a container + +You've already seen that each container you create has an automatically +created name; indeed you've become familiar with our old friend +`nostalgic_morse` during this guide. You can also name containers +yourself. This naming provides two useful functions: + +* You can name containers that do specific functions in a way + that makes it easier for you to remember them, for example naming a + container containing a web application `web`. + +* Names provide Docker with a reference point that allows it to refer to other + containers. There are several commands that support this and you'll use one in an exercise later. + +You name your container by using the `--name` flag, for example launch a new container called web: + + $ docker run -d -P --name web training/webapp python app.py + +Use the `docker ps` command to check the name: + + $ docker ps -l + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + aed84ee21bde training/webapp:latest python app.py 12 hours ago Up 2 seconds 0.0.0.0:49154->5000/tcp web + +You can also use `docker inspect` with the container's name. + + $ docker inspect web + [ + { + "Id": "3ce51710b34f5d6da95e0a340d32aa2e6cf64857fb8cdb2a6c38f7c56f448143", + "Created": "2015-10-25T22:44:17.854367116Z", + "Path": "python", + "Args": [ + "app.py" + ], + "State": { + "Status": "running", + "Running": true, + "Paused": false, + "Restarting": false, + "OOMKilled": false, + ... + +Container names must be unique. That means you can only call one container +`web`. If you want to re-use a container name you must delete the old container +(with `docker rm`) before you can reuse the name with a new container. Go ahead and stop and remove your old `web` container. + + $ docker stop web + web + $ docker rm web + web + + +## Launch a container on the default network + +Docker includes support for networking containers through the use of **network +drivers**. By default, Docker provides two network drivers for you, the +`bridge` and the `overlay` drivers. You can also write a network driver plugin so +that you can create your own drivers but that is an advanced task. + +Every installation of the Docker Engine automatically includes three default networks. You can list them: + + $ docker network ls + NETWORK ID NAME DRIVER + 18a2866682b8 none null + c288470c46f6 host host + 7b369448dccb bridge bridge + +The network named `bridge` is a special network. Unless you tell it otherwise, Docker always launches your containers in this network. Try this now: + + $ docker run -itd --name=networktest ubuntu + 74695c9cea6d9810718fddadc01a727a5dd3ce6a69d09752239736c030599741 + +Inspecting the network is an easy way to find out the container's IP address. + +```bash +$ docker network inspect bridge +[ + { + "Name": "bridge", + "Id": "f7ab26d71dbd6f557852c7156ae0574bbf62c42f539b50c8ebde0f728a253b6f", + "Scope": "local", + "Driver": "bridge", + "IPAM": { + "Driver": "default", + "Config": [ + { + "Subnet": "172.17.0.1/16", + "Gateway": "172.17.0.1" + } + ] + }, + "Containers": { + "3386a527aa08b37ea9232cbcace2d2458d49f44bb05a6b775fba7ddd40d8f92c": { + "EndpointID": "647c12443e91faf0fd508b6edfe59c30b642abb60dfab890b4bdccee38750bc1", + "MacAddress": "02:42:ac:11:00:02", + "IPv4Address": "172.17.0.2/16", + "IPv6Address": "" + }, + "94447ca479852d29aeddca75c28f7104df3c3196d7b6d83061879e339946805c": { + "EndpointID": "b047d090f446ac49747d3c37d63e4307be745876db7f0ceef7b311cbba615f48", + "MacAddress": "02:42:ac:11:00:03", + "IPv4Address": "172.17.0.3/16", + "IPv6Address": "" + } + }, + "Options": { + "com.docker.network.bridge.default_bridge": "true", + "com.docker.network.bridge.enable_icc": "true", + "com.docker.network.bridge.enable_ip_masquerade": "true", + "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", + "com.docker.network.bridge.name": "docker0", + "com.docker.network.driver.mtu": "9001" + } + } +] +``` + +You can remove a container from a network by disconnecting the container. To do this, you supply both the network name and the container name. You can also use the container id. In this example, though, the name is faster. + + $ docker network disconnect bridge networktest + +While you can disconnect a container from a network, you cannot remove the builtin `bridge` network named `bridge`. Networks are natural ways to isolate containers from other containers or other networks. So, as you get more experienced with Docker, you'll want to create your own networks. + +## Create your own bridge network + +Docker Engine natively supports both bridge networks and overlay networks. A bridge network is limited to a single host running Docker Engine. An overlay network can include multiple hosts and is a more advanced topic. For this example, you'll create a bridge network: + + $ docker network create -d bridge my-bridge-network + +The `-d` flag tells Docker to use the `bridge` driver for the new network. You could have left this flag off as `bridge` is the default value for this flag. Go ahead and list the networks on your machine: + + $ docker network ls + NETWORK ID NAME DRIVER + 7b369448dccb bridge bridge + 615d565d498c my-bridge-network bridge + 18a2866682b8 none null + c288470c46f6 host host + +If you inspect the network, you'll find that it has nothing in it. + + $ docker network inspect my-bridge-network + [ + { + "Name": "my-bridge-network", + "Id": "5a8afc6364bccb199540e133e63adb76a557906dd9ff82b94183fc48c40857ac", + "Scope": "local", + "Driver": "bridge", + "IPAM": { + "Driver": "default", + "Config": [ + { + "Subnet": "172.18.0.0/16", + "Gateway": "172.18.0.1/16" + } + ] + }, + "Containers": {}, + "Options": {} + } + ] + +## Add containers to a network + +To build web applications that act in concert but do so securely, create a +network. Networks, by definition, provide complete isolation for containers. You +can add containers to a network when you first run a container. + +Launch a container running a PostgreSQL database and pass it the `--net=my-bridge-network` flag to connect it to your new network: + + $ docker run -d --net=my-bridge-network --name db training/postgres + +If you inspect your `my-bridge-network` you'll see it has a container attached. +You can also inspect your container to see where it is connected: + + $ docker inspect --format='{{json .NetworkSettings.Networks}}' db + {"my-bridge-network":{"NetworkID":"7d86d31b1478e7cca9ebed7e73aa0fdeec46c5ca29497431d3007d2d9e15ed99", + "EndpointID":"508b170d56b2ac9e4ef86694b0a76a22dd3df1983404f7321da5649645bf7043","Gateway":"172.18.0.1","IPAddress":"172.18.0.2","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"MacAddress":"02:42:ac:11:00:02"}} + +Now, go ahead and start your by now familiar web application. This time leave off the `-P` flag and also don't specify a network. + + $ docker run -d --name web training/webapp python app.py + +Which network is your `web` application running under? Inspect the application and you'll find it is running in the default `bridge` network. + + $ docker inspect --format='{{json .NetworkSettings.Networks}}' web + {"bridge":{"NetworkID":"7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812", + "EndpointID":"508b170d56b2ac9e4ef86694b0a76a22dd3df1983404f7321da5649645bf7043","Gateway":"172.17.0.1","IPAddress":"172.17.0.2","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"MacAddress":"02:42:ac:11:00:02"}} + +Then, get the IP address of your `web` + + $ docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' web + 172.17.0.2 + +Now, open a shell to your running `db` container: + + $ docker exec -it db bash + root@a205f0dd33b2:/# ping 172.17.0.2 + ping 172.17.0.2 + PING 172.17.0.2 (172.17.0.2) 56(84) bytes of data. + ^C + --- 172.17.0.2 ping statistics --- + 44 packets transmitted, 0 received, 100% packet loss, time 43185ms + +After a bit, use `CTRL-C` to end the `ping` and you'll find the ping failed. That is because the two containers are running on different networks. You can fix that. Then, use the `exit` command to close the container. + +Docker networking allows you to attach a container to as many networks as you like. You can also attach an already running container. Go ahead and attach your running `web` app to the `my-bridge-network`. + + $ docker network connect my-bridge-network web + +Open a shell into the `db` application again and try the ping command. This time just use the container name `web` rather than the IP Address. + + $ docker exec -it db bash + root@a205f0dd33b2:/# ping web + PING web (172.18.0.3) 56(84) bytes of data. + 64 bytes from web (172.18.0.3): icmp_seq=1 ttl=64 time=0.095 ms + 64 bytes from web (172.18.0.3): icmp_seq=2 ttl=64 time=0.060 ms + 64 bytes from web (172.18.0.3): icmp_seq=3 ttl=64 time=0.066 ms + ^C + --- web ping statistics --- + 3 packets transmitted, 3 received, 0% packet loss, time 2000ms + rtt min/avg/max/mdev = 0.060/0.073/0.095/0.018 ms + +The `ping` shows it is contacting a different IP address, the address on the `my-bridge-network` which is different from its address on the `bridge` network. + +## Next steps + +Now that you know how to network containers, see [how to manage data in containers](dockervolumes.md). diff --git a/docs/userguide/containers/search.png b/docs/userguide/containers/search.png new file mode 100644 index 0000000000000000000000000000000000000000..187b8d9ed89e453f34eb40d46d6939e1a45774e2 GIT binary patch literal 17923 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 z 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 qrLZ45jvh5|4t}awselwA1>^~#v ++++ +title = "Run a simple application" +description = "Learn how to manage and operate Docker containers." +keywords = ["docker, the docker guide, documentation, docker.io, monitoring containers, docker top, docker inspect, docker port, ports, docker logs, log, Logs"] +[menu.main] +parent="engine_learn" +weight=-5 ++++ + + +# Run a simple application + +In the ["*Hello world in a container*"](dockerizing.md) you launched your +first containers using the `docker run` command. You ran an *interactive container* that ran in the foreground. You also ran a *detached container* that ran in the background. In the process you learned about several Docker commands: + +* `docker ps` - Lists containers. +* `docker logs` - Shows us the standard output of a container. +* `docker stop` - Stops running containers. + +## Learn about the Docker client + +If you didn't realize it yet, you've been using the Docker client each time you +typed `docker` in your Bash terminal. The client is a simple command line client +also known as a command-line interface (CLI). Each action you can take with +the client is a command and each command can take a series of flags and arguments. + + # Usage: [sudo] docker [subcommand] [flags] [arguments] .. + # Example: + $ docker run -i -t ubuntu /bin/bash + +You can see this in action by using the `docker version` command to return +version information on the currently installed Docker client and daemon. + + $ docker version + +This command will not only provide you the version of Docker client and +daemon you are using, but also the version of Go (the programming +language powering Docker). + + Client: + Version: 1.8.1 + API version: 1.20 + Go version: go1.4.2 + Git commit: d12ea79 + Built: Thu Aug 13 02:35:49 UTC 2015 + OS/Arch: linux/amd64 + + Server: + Version: 1.8.1 + API version: 1.20 + Go version: go1.4.2 + Git commit: d12ea79 + Built: Thu Aug 13 02:35:49 UTC 2015 + OS/Arch: linux/amd64 + +## Get Docker command help + +You can display the help for specific Docker commands. The help details the +options and their usage. To see a list of all the possible commands, use the +following: + + $ docker --help + +To see usage for a specific command, specify the command with the `--help` flag: + + $ docker attach --help + + Usage: docker attach [OPTIONS] CONTAINER + + Attach to a running container + + --help Print usage + --no-stdin Do not attach stdin + --sig-proxy=true Proxy all received signals to the process + +> **Note:** +> For further details and examples of each command, see the +> [command reference](../../reference/commandline/cli.md) in this guide. + +## Running a web application in Docker + +So now you've learned a bit more about the `docker` client you can move onto +the important stuff: running more containers. So far none of the +containers you've run did anything particularly useful, so you can +change that by running an example web application in Docker. + +For our web application we're going to run a Python Flask application. +Start with a `docker run` command. + + $ docker run -d -P training/webapp python app.py + +Review what the command did. You've specified two flags: `-d` and +`-P`. You've already seen the `-d` flag which tells Docker to run the +container in the background. The `-P` flag is new and tells Docker to +map any required network ports inside our container to our host. This +lets us view our web application. + +You've specified an image: `training/webapp`. This image is a +pre-built image you've created that contains a simple Python Flask web +application. + +Lastly, you've specified a command for our container to run: `python app.py`. This launches our web application. + +> **Note:** +> You can see more detail on the `docker run` command in the [command +> reference](../../reference/commandline/run.md) and the [Docker Run +> Reference](../../reference/run.md). + +## Viewing our web application container + +Now you can see your running container using the `docker ps` command. + + $ docker ps -l + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + bc533791f3f5 training/webapp:latest python app.py 5 seconds ago Up 2 seconds 0.0.0.0:49155->5000/tcp nostalgic_morse + +You can see you've specified a new flag, `-l`, for the `docker ps` +command. This tells the `docker ps` command to return the details of the +*last* container started. + +> **Note:** +> By default, the `docker ps` command only shows information about running +> containers. If you want to see stopped containers too use the `-a` flag. + +We can see the same details we saw [when we first Dockerized a +container](dockerizing.md) with one important addition in the `PORTS` +column. + + PORTS + 0.0.0.0:49155->5000/tcp + +When we passed the `-P` flag to the `docker run` command Docker mapped any +ports exposed in our image to our host. + +> **Note:** +> We'll learn more about how to expose ports in Docker images when +> [we learn how to build images](dockerimages.md). + +In this case Docker has exposed port 5000 (the default Python Flask +port) on port 49155. + +Network port bindings are very configurable in Docker. In our last example the +`-P` flag is a shortcut for `-p 5000` that maps port 5000 inside the container +to a high port (from *ephemeral port range* which typically ranges from 32768 +to 61000) on the local Docker host. We can also bind Docker containers to +specific ports using the `-p` flag, for example: + + $ docker run -d -p 80:5000 training/webapp python app.py + +This would map port 5000 inside our container to port 80 on our local +host. You might be asking about now: why wouldn't we just want to always +use 1:1 port mappings in Docker containers rather than mapping to high +ports? Well 1:1 mappings have the constraint of only being able to map +one of each port on your local host. + +Suppose you want to test two Python applications: both bound to port 5000 inside +their own containers. Without Docker's port mapping you could only access one at +a time on the Docker host. + +So you can now browse to port 49155 in a web browser to +see the application. + +![Viewing the web application](webapp1.png). + +Our Python application is live! + +> **Note:** +> If you have been using a virtual machine on OS X, Windows or Linux, +> you'll need to get the IP of the virtual host instead of using localhost. +> You can do this by running the `docker-machine ip your_vm_name` from your command line or terminal application, for example: +> +> $ docker-machine ip my-docker-vm +> 192.168.99.100 +> +> In this case you'd browse to `http://192.168.99.100:49155` for the above example. + +## A network port shortcut + +Using the `docker ps` command to return the mapped port is a bit clumsy so +Docker has a useful shortcut we can use: `docker port`. To use `docker port` we +specify the ID or name of our container and then the port for which we need the +corresponding public-facing port. + + $ docker port nostalgic_morse 5000 + 0.0.0.0:49155 + +In this case you've looked up what port is mapped externally to port 5000 inside +the container. + +## Viewing the web application's logs + +You can also find out a bit more about what's happening with our application and +use another of the commands you've learned, `docker logs`. + + $ docker logs -f nostalgic_morse + * Running on http://0.0.0.0:5000/ + 10.0.2.2 - - [23/May/2014 20:16:31] "GET / HTTP/1.1" 200 - + 10.0.2.2 - - [23/May/2014 20:16:31] "GET /favicon.ico HTTP/1.1" 404 - + +This time though you've added a new flag, `-f`. This causes the `docker +logs` command to act like the `tail -f` command and watch the +container's standard out. We can see here the logs from Flask showing +the application running on port 5000 and the access log entries for it. + +## Looking at our web application container's processes + +In addition to the container's logs we can also examine the processes +running inside it using the `docker top` command. + + $ docker top nostalgic_morse + PID USER COMMAND + 854 root python app.py + +Here we can see our `python app.py` command is the only process running inside +the container. + +## Inspecting our web application container + +Lastly, we can take a low-level dive into our Docker container using the +`docker inspect` command. It returns a JSON document containing useful +configuration and status information for the specified container. + + $ docker inspect nostalgic_morse + +You can see a sample of that JSON output. + + [{ + "ID": "bc533791f3f500b280a9626688bc79e342e3ea0d528efe3a86a51ecb28ea20", + "Created": "2014-05-26T05:52:40.808952951Z", + "Path": "python", + "Args": [ + "app.py" + ], + "Config": { + "Hostname": "bc533791f3f5", + "Domainname": "", + "User": "", + . . . + +We can also narrow down the information we want to return by requesting a +specific element, for example to return the container's IP address we would: + + $ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' nostalgic_morse + 172.17.0.5 + +## Stopping our web application container + +Okay you've seen web application working. Now you can stop it using the +`docker stop` command and the name of our container: `nostalgic_morse`. + + $ docker stop nostalgic_morse + nostalgic_morse + +We can now use the `docker ps` command to check if the container has +been stopped. + + $ docker ps -l + +## Restarting our web application container + +Oops! Just after you stopped the container you get a call to say another +developer needs the container back. From here you have two choices: you +can create a new container or restart the old one. Look at +starting your previous container back up. + + $ docker start nostalgic_morse + nostalgic_morse + +Now quickly run `docker ps -l` again to see the running container is +back up or browse to the container's URL to see if the application +responds. + +> **Note:** +> Also available is the `docker restart` command that runs a stop and +> then start on the container. + +## Removing our web application container + +Your colleague has let you know that they've now finished with the container +and won't need it again. Now, you can remove it using the `docker rm` command. + + $ docker rm nostalgic_morse + Error: Impossible to remove a running container, please stop it first or use -f + 2014/05/24 08:12:56 Error: failed to remove one or more containers + +What happened? We can't actually remove a running container. This protects +you from accidentally removing a running container you might need. You can try +this again by stopping the container first. + + $ docker stop nostalgic_morse + nostalgic_morse + $ docker rm nostalgic_morse + nostalgic_morse + +And now our container is stopped and deleted. + +> **Note:** +> Always remember that removing a container is final! + +# Next steps + +Until now you've only used images that you've downloaded from Docker Hub. Next, +you can get introduced to building and sharing our own images. + +Go to [Working with Docker Images](dockerimages.md). diff --git a/docs/userguide/containers/webapp1.png b/docs/userguide/containers/webapp1.png new file mode 100644 index 0000000000000000000000000000000000000000..b92cc87d65d45c4cb5b255a089cdb54bbe9b23e4 GIT binary patch literal 13345 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
6iD7ba#KuH&E;^@5JH<}G67wL~FC+LN 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 ++++ +aliases = ["/engine/articles/baseimages/"] +title = "Create a base image" +description = "How to create base images" +keywords = ["Examples, Usage, base image, docker, documentation, examples"] +[menu.main] +parent = "engine_images" ++++ + + +# Create a base image + +So you want to create your own [*Base Image*](../../reference/glossary.md#base-image)? Great! + +The specific process will depend heavily on the Linux distribution you +want to package. We have some examples below, and you are encouraged to +submit pull requests to contribute new ones. + +## Create a full image using tar + +In general, you'll want to start with a working machine that is running +the distribution you'd like to package as a base image, though that is +not required for some tools like Debian's +[Debootstrap](https://wiki.debian.org/Debootstrap), which you can also +use to build Ubuntu images. + +It can be as simple as this to create an Ubuntu base image: + + $ sudo debootstrap raring raring > /dev/null + $ sudo tar -C raring -c . | docker import - raring + a29c15f1bf7a + $ docker run raring cat /etc/lsb-release + DISTRIB_ID=Ubuntu + DISTRIB_RELEASE=13.04 + DISTRIB_CODENAME=raring + DISTRIB_DESCRIPTION="Ubuntu 13.04" + +There are more example scripts for creating base images in the Docker +GitHub Repo: + + - [BusyBox](https://github.com/docker/docker/blob/master/contrib/mkimage-busybox.sh) + - CentOS / Scientific Linux CERN (SLC) [on Debian/Ubuntu]( + https://github.com/docker/docker/blob/master/contrib/mkimage-rinse.sh) or + [on CentOS/RHEL/SLC/etc.]( + https://github.com/docker/docker/blob/master/contrib/mkimage-yum.sh) + - [Debian / Ubuntu]( + https://github.com/docker/docker/blob/master/contrib/mkimage-debootstrap.sh) + +## Creating a simple base image using scratch + +You can use Docker's reserved, minimal image, `scratch`, as a starting point for building containers. Using the `scratch` "image" signals to the build process that you want the next command in the `Dockerfile` to be the first filesystem layer in your image. + +While `scratch` appears in Docker's repository on the hub, you can't pull it, run it, or tag any image with the name `scratch`. Instead, you can refer to it in your `Dockerfile`. For example, to create a minimal container using `scratch`: + + FROM scratch + ADD hello / + CMD ["/hello"] + +This example creates the hello-world image used in the tutorials. +If you want to test it out, you can clone [the image repo](https://github.com/docker-library/hello-world) + + +## More resources + +There are lots more resources available to help you write your 'Dockerfile`. + +* There's a [complete guide to all the instructions](../../reference/builder.md) available for use in a `Dockerfile` in the reference section. +* To help you write a clear, readable, maintainable `Dockerfile`, we've also +written a [`Dockerfile` Best Practices guide](dockerfile_best-practices.md). +* If your goal is to create a new Official Repository, be sure to read up on Docker's [Official Repositories](https://docs.docker.com/docker-hub/official_repos/). diff --git a/docs/userguide/eng-image/dockerfile_best-practices.md b/docs/userguide/eng-image/dockerfile_best-practices.md new file mode 100644 index 00000000..b7463a07 --- /dev/null +++ b/docs/userguide/eng-image/dockerfile_best-practices.md @@ -0,0 +1,495 @@ + + +# Best practices for writing Dockerfiles + +Docker can build images automatically by reading the instructions from a +`Dockerfile`, a text file that contains all the commands, in order, needed to +build a given image. `Dockerfile`s adhere to a specific format and use a +specific set of instructions. You can learn the basics on the +[Dockerfile Reference](../../reference/builder.md) page. If +you’re new to writing `Dockerfile`s, you should start there. + +This document covers the best practices and methods recommended by Docker, +Inc. and the Docker community for creating easy-to-use, effective +`Dockerfile`s. We strongly suggest you follow these recommendations (in fact, +if you’re creating an Official Image, you *must* adhere to these practices). + +You can see many of these practices and recommendations in action in the [buildpack-deps `Dockerfile`](https://github.com/docker-library/buildpack-deps/blob/master/jessie/Dockerfile). + +> Note: for more detailed explanations of any of the Dockerfile commands +>mentioned here, visit the [Dockerfile Reference](../../reference/builder.md) page. + +## General guidelines and recommendations + +### Containers should be ephemeral + +The container produced by the image your `Dockerfile` defines should be as +ephemeral as possible. By “ephemeral,” we mean that it can be stopped and +destroyed and a new one built and put in place with an absolute minimum of +set-up and configuration. + +### Use a .dockerignore file + +In most cases, it's best to put each Dockerfile in an empty directory. Then, +add to that directory only the files needed for building the Dockerfile. To +increase the build's performance, you can exclude files and directories by +adding a `.dockerignore` file to that directory as well. This file supports +exclusion patterns similar to `.gitignore` files. For information on creating one, +see the [.dockerignore file](../../reference/builder.md#dockerignore-file). + +### Avoid installing unnecessary packages + +In order to reduce complexity, dependencies, file sizes, and build times, you +should avoid installing extra or unnecessary packages just because they +might be “nice to have.” For example, you don’t need to include a text editor +in a database image. + +### Run only one process per container + +In almost all cases, you should only run a single process in a single +container. Decoupling applications into multiple containers makes it much +easier to scale horizontally and reuse containers. If that service depends on +another service, make use of [container linking](../../userguide/networking/default_network/dockerlinks.md). + +### Minimize the number of layers + +You need to find the balance between readability (and thus long-term +maintainability) of the `Dockerfile` and minimizing the number of layers it +uses. Be strategic and cautious about the number of layers you use. + +### Sort multi-line arguments + +Whenever possible, ease later changes by sorting multi-line arguments +alphanumerically. This will help you avoid duplication of packages and make the +list much easier to update. This also makes PRs a lot easier to read and +review. Adding a space before a backslash (`\`) helps as well. + +Here’s an example from the [`buildpack-deps` image](https://github.com/docker-library/buildpack-deps): + + RUN apt-get update && apt-get install -y \ + bzr \ + cvs \ + git \ + mercurial \ + subversion + +### Build cache + +During the process of building an image Docker will step through the +instructions in your `Dockerfile` executing each in the order specified. +As each instruction is examined Docker will look for an existing image in its +cache that it can reuse, rather than creating a new (duplicate) image. +If you do not want to use the cache at all you can use the ` --no-cache=true` +option on the `docker build` command. + +However, if you do let Docker use its cache then it is very important to +understand when it will, and will not, find a matching image. The basic rules +that Docker will follow are outlined below: + +* Starting with a base image that is already in the cache, the next +instruction is compared against all child images derived from that base +image to see if one of them was built using the exact same instruction. If +not, the cache is invalidated. + +* In most cases simply comparing the instruction in the `Dockerfile` with one +of the child images is sufficient. However, certain instructions require +a little more examination and explanation. + +* For the `ADD` and `COPY` instructions, the contents of the file(s) +in the image are examined and a checksum is calculated for each file. +The last-modified and last-accessed times of the file(s) are not considered in +these checksums. During the cache lookup, the checksum is compared against the +checksum in the existing images. If anything has changed in the file(s), such +as the contents and metadata, then the cache is invalidated. + +* Aside from the `ADD` and `COPY` commands, cache checking will not look at the +files in the container to determine a cache match. For example, when processing +a `RUN apt-get -y update` command the files updated in the container +will not be examined to determine if a cache hit exists. In that case just +the command string itself will be used to find a match. + +Once the cache is invalidated, all subsequent `Dockerfile` commands will +generate new images and the cache will not be used. + +## The Dockerfile instructions + +Below you'll find recommendations for the best way to write the +various instructions available for use in a `Dockerfile`. + +### FROM + +[Dockerfile reference for the FROM instruction](../../reference/builder.md#from) + +Whenever possible, use current Official Repositories as the basis for your +image. We recommend the [Debian image](https://hub.docker.com/_/debian/) +since it’s very tightly controlled and kept minimal (currently under 150 mb), +while still being a full distribution. + +### RUN + +[Dockerfile reference for the RUN instruction](../../reference/builder.md#run) + +As always, to make your `Dockerfile` more readable, understandable, and +maintainable, split long or complex `RUN` statements on multiple lines separated +with backslashes. + +### apt-get + +Probably the most common use-case for `RUN` is an application of `apt-get`. The +`RUN apt-get` command, because it installs packages, has several gotchas to look +out for. + +You should avoid `RUN apt-get upgrade` or `dist-upgrade`, as many of the +“essential” packages from the base images won't upgrade inside an unprivileged +container. If a package contained in the base image is out-of-date, you should +contact its maintainers. +If you know there’s a particular package, `foo`, that needs to be updated, use +`apt-get install -y foo` to update automatically. + +Always combine `RUN apt-get update` with `apt-get install` in the same `RUN` +statement, for example: + + RUN apt-get update && apt-get install -y \ + package-bar \ + package-baz \ + package-foo + + +Using `apt-get update` alone in a `RUN` statement causes caching issues and +subsequent `apt-get install` instructions fail. +For example, say you have a Dockerfile: + + FROM ubuntu:14.04 + RUN apt-get update + RUN apt-get install -y curl + +After building the image, all layers are in the Docker cache. Suppose you later +modify `apt-get install` by adding extra package: + + FROM ubuntu:14.04 + RUN apt-get update + RUN apt-get install -y curl nginx + +Docker sees the initial and modified instructions as identical and reuses the +cache from previous steps. As a result the `apt-get update` is *NOT* executed +because the build uses the cached version. Because the `apt-get update` is not +run, your build can potentially get an outdated version of the `curl` and `nginx` +packages. + +Using `RUN apt-get update && apt-get install -y` ensures your Dockerfile +installs the latest package versions with no further coding or manual +intervention. This technique is known as "cache busting". You can also achieve +cache-busting by specifying a package version. This is known as version pinning, +for example: + + RUN apt-get update && apt-get install -y \ + package-bar \ + package-baz \ + package-foo=1.3.* + +Version pinning forces the build to retrieve a particular version regardless of +what’s in the cache. This technique can also reduce failures due to unanticipated changes +in required packages. + +Below is a well-formed `RUN` instruction that demonstrates all the `apt-get` +recommendations. + + RUN apt-get update && apt-get install -y \ + aufs-tools \ + automake \ + build-essential \ + curl \ + dpkg-sig \ + libcap-dev \ + libsqlite3-dev \ + mercurial \ + reprepro \ + ruby1.9.1 \ + ruby1.9.1-dev \ + s3cmd=1.1.* \ + && rm -rf /var/lib/apt/lists/* + +The `s3cmd` instructions specifies a version `1.1.0*`. If the image previously +used an older version, specifying the new one causes a cache bust of `apt-get +update` and ensure the installation of the new version. Listing packages on +each line can also prevent mistakes in package duplication. + +In addition, cleaning up the apt cache and removing `/var/lib/apt/lists` helps +keep the image size down. Since the `RUN` statement starts with +`apt-get update`, the package cache will always be refreshed prior to +`apt-get install`. + +> **Note**: The official Debian and Ubuntu images [automatically run `apt-get clean`](https://github.com/docker/docker/blob/03e2923e42446dbb830c654d0eec323a0b4ef02a/contrib/mkimage/debootstrap#L82-L105), +> so explicit invocation is not required. + +### CMD + +[Dockerfile reference for the CMD instruction](../../reference/builder.md#cmd) + +The `CMD` instruction should be used to run the software contained by your +image, along with any arguments. `CMD` should almost always be used in the +form of `CMD [“executable”, “param1”, “param2”…]`. Thus, if the image is for a +service (Apache, Rails, etc.), you would run something like +`CMD ["apache2","-DFOREGROUND"]`. Indeed, this form of the instruction is +recommended for any service-based image. + +In most other cases, `CMD` should be given an interactive shell (bash, python, +perl, etc), for example, `CMD ["perl", "-de0"]`, `CMD ["python"]`, or +`CMD [“php”, “-a”]`. Using this form means that when you execute something like +`docker run -it python`, you’ll get dropped into a usable shell, ready to go. +`CMD` should rarely be used in the manner of `CMD [“param”, “param”]` in +conjunction with [`ENTRYPOINT`](../../reference/builder.md#entrypoint), unless +you and your expected users are already quite familiar with how `ENTRYPOINT` +works. + +### EXPOSE + +[Dockerfile reference for the EXPOSE instruction](../../reference/builder.md#expose) + +The `EXPOSE` instruction indicates the ports on which a container will listen +for connections. Consequently, you should use the common, traditional port for +your application. For example, an image containing the Apache web server would +use `EXPOSE 80`, while an image containing MongoDB would use `EXPOSE 27017` and +so on. + +For external access, your users can execute `docker run` with a flag indicating +how to map the specified port to the port of their choice. +For container linking, Docker provides environment variables for the path from +the recipient container back to the source (ie, `MYSQL_PORT_3306_TCP`). + +### ENV + +[Dockerfile reference for the ENV instruction](../../reference/builder.md#env) + +In order to make new software easier to run, you can use `ENV` to update the +`PATH` environment variable for the software your container installs. For +example, `ENV PATH /usr/local/nginx/bin:$PATH` will ensure that `CMD [“nginx”]` +just works. + +The `ENV` instruction is also useful for providing required environment +variables specific to services you wish to containerize, such as Postgres’s +`PGDATA`. + +Lastly, `ENV` can also be used to set commonly used version numbers so that +version bumps are easier to maintain, as seen in the following example: + + ENV PG_MAJOR 9.3 + ENV PG_VERSION 9.3.4 + RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && … + ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH + +Similar to having constant variables in a program (as opposed to hard-coding +values), this approach lets you change a single `ENV` instruction to +auto-magically bump the version of the software in your container. + +### ADD or COPY + +[Dockerfile reference for the ADD instruction](../../reference/builder.md#add)
+[Dockerfile reference for the COPY instruction](../../reference/builder.md#copy) + +Although `ADD` and `COPY` are functionally similar, generally speaking, `COPY` +is preferred. That’s because it’s more transparent than `ADD`. `COPY` only +supports the basic copying of local files into the container, while `ADD` has +some features (like local-only tar extraction and remote URL support) that are +not immediately obvious. Consequently, the best use for `ADD` is local tar file +auto-extraction into the image, as in `ADD rootfs.tar.xz /`. + +If you have multiple `Dockerfile` steps that use different files from your +context, `COPY` them individually, rather than all at once. This will ensure that +each step's build cache is only invalidated (forcing the step to be re-run) if the +specifically required files change. + +For example: + + COPY requirements.txt /tmp/ + RUN pip install --requirement /tmp/requirements.txt + COPY . /tmp/ + +Results in fewer cache invalidations for the `RUN` step, than if you put the +`COPY . /tmp/` before it. + +Because image size matters, using `ADD` to fetch packages from remote URLs is +strongly discouraged; you should use `curl` or `wget` instead. That way you can +delete the files you no longer need after they've been extracted and you won't +have to add another layer in your image. For example, you should avoid doing +things like: + + ADD http://example.com/big.tar.xz /usr/src/things/ + RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things + RUN make -C /usr/src/things all + +And instead, do something like: + + RUN mkdir -p /usr/src/things \ + && curl -SL http://example.com/big.tar.xz \ + | tar -xJC /usr/src/things \ + && make -C /usr/src/things all + +For other items (files, directories) that do not require `ADD`’s tar +auto-extraction capability, you should always use `COPY`. + +### ENTRYPOINT + +[Dockerfile reference for the ENTRYPOINT instruction](../../reference/builder.md#entrypoint) + +The best use for `ENTRYPOINT` is to set the image's main command, allowing that +image to be run as though it was that command (and then use `CMD` as the +default flags). + +Let's start with an example of an image for the command line tool `s3cmd`: + + ENTRYPOINT ["s3cmd"] + CMD ["--help"] + +Now the image can be run like this to show the command's help: + + $ docker run s3cmd + +Or using the right parameters to execute a command: + + $ docker run s3cmd ls s3://mybucket + +This is useful because the image name can double as a reference to the binary as +shown in the command above. + +The `ENTRYPOINT` instruction can also be used in combination with a helper +script, allowing it to function in a similar way to the command above, even +when starting the tool may require more than one step. + +For example, the [Postgres Official Image](https://hub.docker.com/_/postgres/) +uses the following script as its `ENTRYPOINT`: + +```bash +#!/bin/bash +set -e + +if [ "$1" = 'postgres' ]; then + chown -R postgres "$PGDATA" + + if [ -z "$(ls -A "$PGDATA")" ]; then + gosu postgres initdb + fi + + exec gosu postgres "$@" +fi + +exec "$@" +``` + +> **Note**: +> This script uses [the `exec` Bash command](http://wiki.bash-hackers.org/commands/builtin/exec) +> so that the final running application becomes the container's PID 1. This allows +> the application to receive any Unix signals sent to the container. +> See the [`ENTRYPOINT`](../../reference/builder.md#entrypoint) +> help for more details. + + +The helper script is copied into the container and run via `ENTRYPOINT` on +container start: + + COPY ./docker-entrypoint.sh / + ENTRYPOINT ["/docker-entrypoint.sh"] + +This script allows the user to interact with Postgres in several ways. + +It can simply start Postgres: + + $ docker run postgres + +Or, it can be used to run Postgres and pass parameters to the server: + + $ docker run postgres postgres --help + +Lastly, it could also be used to start a totally different tool, such as Bash: + + $ docker run --rm -it postgres bash + +### VOLUME + +[Dockerfile reference for the VOLUME instruction](../../reference/builder.md#volume) + +The `VOLUME` instruction should be used to expose any database storage area, +configuration storage, or files/folders created by your docker container. You +are strongly encouraged to use `VOLUME` for any mutable and/or user-serviceable +parts of your image. + +### USER + +[Dockerfile reference for the USER instruction](../../reference/builder.md#user) + +If a service can run without privileges, use `USER` to change to a non-root +user. Start by creating the user and group in the `Dockerfile` with something +like `RUN groupadd -r postgres && useradd -r -g postgres postgres`. + +> **Note:** Users and groups in an image get a non-deterministic +> UID/GID in that the “next” UID/GID gets assigned regardless of image +> rebuilds. So, if it’s critical, you should assign an explicit UID/GID. + +You should avoid installing or using `sudo` since it has unpredictable TTY and +signal-forwarding behavior that can cause more problems than it solves. If +you absolutely need functionality similar to `sudo` (e.g., initializing the +daemon as root but running it as non-root), you may be able to use +[“gosu”](https://github.com/tianon/gosu). + +Lastly, to reduce layers and complexity, avoid switching `USER` back +and forth frequently. + +### WORKDIR + +[Dockerfile reference for the WORKDIR instruction](../../reference/builder.md#workdir) + +For clarity and reliability, you should always use absolute paths for your +`WORKDIR`. Also, you should use `WORKDIR` instead of proliferating +instructions like `RUN cd … && do-something`, which are hard to read, +troubleshoot, and maintain. + +### ONBUILD + +[Dockerfile reference for the ONBUILD instruction](../../reference/builder.md#onbuild) + +An `ONBUILD` command executes after the current `Dockerfile` build completes. +`ONBUILD` executes in any child image derived `FROM` the current image. Think +of the `ONBUILD` command as an instruction the parent `Dockerfile` gives +to the child `Dockerfile`. + +A Docker build executes `ONBUILD` commands before any command in a child +`Dockerfile`. + +`ONBUILD` is useful for images that are going to be built `FROM` a given +image. For example, you would use `ONBUILD` for a language stack image that +builds arbitrary user software written in that language within the +`Dockerfile`, as you can see in [Ruby’s `ONBUILD` variants](https://github.com/docker-library/ruby/blob/master/2.1/onbuild/Dockerfile). + +Images built from `ONBUILD` should get a separate tag, for example: +`ruby:1.9-onbuild` or `ruby:2.0-onbuild`. + +Be careful when putting `ADD` or `COPY` in `ONBUILD`. The “onbuild” image will +fail catastrophically if the new build's context is missing the resource being +added. Adding a separate tag, as recommended above, will help mitigate this by +allowing the `Dockerfile` author to make a choice. + +## Examples for Official Repositories + +These Official Repositories have exemplary `Dockerfile`s: + +* [Go](https://hub.docker.com/_/golang/) +* [Perl](https://hub.docker.com/_/perl/) +* [Hy](https://hub.docker.com/_/hylang/) +* [Rails](https://hub.docker.com/_/rails) + +## Additional resources: + +* [Dockerfile Reference](../../reference/builder.md) +* [More about Base Images](baseimages.md) +* [More about Automated Builds](https://docs.docker.com/docker-hub/builds/) +* [Guidelines for Creating Official +Repositories](https://docs.docker.com/docker-hub/official_repos/) diff --git a/docs/userguide/eng-image/image_management.md b/docs/userguide/eng-image/image_management.md new file mode 100644 index 00000000..035b6b68 --- /dev/null +++ b/docs/userguide/eng-image/image_management.md @@ -0,0 +1,53 @@ + + +# Image management + +The Docker Engine provides a client which you can use to create images on the command line or through a build process. You can run these images in a container or publish them for others to use. Storing the images you create, searching for images you might want, or publishing images others might use are all elements of image management. + +This section provides an overview of the major features and products Docker provides for image management. + + +## Docker Hub + +The [Docker Hub](https://docs.docker.com/docker-hub/) is responsible for centralizing information about user accounts, images, and public name spaces. It has different components: + + - Web UI + - Meta-data store (comments, stars, list public repositories) + - Authentication service + - Tokenization + +There is only one instance of the Docker Hub, run and managed by Docker Inc. This public Hub is useful for most individuals and smaller companies. + +## Docker Registry and the Docker Trusted Registry + +The Docker Registry is a component of Docker's ecosystem. A registry is a +storage and content delivery system, holding named Docker images, available in +different tagged versions. For example, the image `distribution/registry`, with +tags `2.0` and `latest`. Users interact with a registry by using docker push and +pull commands such as, `docker pull myregistry.com/stevvooe/batman:voice`. + +The Docker Hub has its own registry which, like the Hub itself, is run and managed by Docker. However, there are other ways to obtain a registry. You can purchase the [Docker Trusted Registry](https://docs.docker.com/docker-trusted-registry) product to run on your company's network. Alternatively, you can use the Docker Registry component to build a private registry. For information about using a registry, see overview for the [Docker Registry](https://docs.docker.com/registry). + + +## Content Trust + +When transferring data among networked systems, *trust* is a central concern. In +particular, when communicating over an untrusted medium such as the internet, it +is critical to ensure the integrity and publisher of all of the data a system +operates on. You use Docker to push and pull images (data) to a registry. +Content trust gives you the ability to both verify the integrity and the +publisher of all the data received from a registry over any channel. + +[Content trust](../../security/trust/index.md) is currently only available for users of the +public Docker Hub. It is currently not available for the Docker Trusted Registry +or for private registries. diff --git a/docs/userguide/eng-image/index.md b/docs/userguide/eng-image/index.md new file mode 100644 index 00000000..c46eec6b --- /dev/null +++ b/docs/userguide/eng-image/index.md @@ -0,0 +1,16 @@ + + +# Work with images + +* [Create a base image](baseimages.md) +* [Best practices for writing Dockerfiles](dockerfile_best-practices.md) +* [Image management](image_management.md) diff --git a/docs/userguide/index.md b/docs/userguide/index.md new file mode 100644 index 00000000..25099975 --- /dev/null +++ b/docs/userguide/index.md @@ -0,0 +1,63 @@ + + +# Docker Engine user guide + +This guide helps users learn how to use Docker Engine. + +- [Introduction to Engine user guide](intro.md) + +## Learn by example + +- [Hello world in a container](containers/dockerizing.md) +- [Build your own images](containers/dockerimages.md) +- [Network containers](containers/networkingcontainers.md) +- [Run a simple application](containers/usingdocker.md) +- [Manage data in containers](containers/dockervolumes.md) +- [Store images on Docker Hub](containers/dockerrepos.md) + +## Work with images + +- [Best practices for writing Dockerfiles](eng-image/dockerfile_best-practices.md) +- [Create a base image](eng-image/baseimages.md) +- [Image management](eng-image/image_management.md) + +## Manage storage drivers + +- [Understand images, containers, and storage drivers](storagedriver/imagesandcontainers.md) +- [Select a storage driver](storagedriver/selectadriver.md) +- [AUFS storage in practice](storagedriver/aufs-driver.md) +- [Btrfs storage in practice](storagedriver/btrfs-driver.md) +- [Device Mapper storage in practice](storagedriver/device-mapper-driver.md) +- [OverlayFS storage in practice](storagedriver/overlayfs-driver.md) +- [ZFS storage in practice](storagedriver/zfs-driver.md) + +## Configure networks + +- [Understand Docker container networks](networking/dockernetworks.md) +- [Embedded DNS server in user-defined networks](networking/configure-dns.md) +- [Get started with multi-host networking](networking/get-started-overlay.md) +- [Work with network commands](networking/work-with-networks.md) + +### Work with the default network + +- [Understand container communication](networking/default_network/container-communication.md) +- [Legacy container links](networking/default_network/dockerlinks.md) +- [Binding container ports to the host](networking/default_network/binding.md) +- [Build your own bridge](networking/default_network/build-bridges.md) +- [Configure container DNS](networking/default_network/configure-dns.md) +- [Customize the docker0 bridge](networking/default_network/custom-docker0.md) +- [IPv6 with Docker](networking/default_network/ipv6.md) + +## Misc + +- [Apply custom metadata](labels-custom-metadata.md) diff --git a/docs/userguide/intro.md b/docs/userguide/intro.md new file mode 100644 index 00000000..18b688ce --- /dev/null +++ b/docs/userguide/intro.md @@ -0,0 +1,118 @@ + + +# Engine user guide + +This guide takes you through the fundamentals of using Docker Engine and +integrating it into your environment. You'll learn how to use Engine to: + +* Dockerize your applications. +* Run your own containers. +* Build Docker images. +* Share your Docker images with others. +* And a whole lot more! + +This guide is broken into major sections that take you through learning the basics of Docker Engine and the other Docker products that support it. + +## Dockerizing applications: A "Hello world" + +*How do I run applications inside containers?* + +Docker Engine offers a containerization platform to power your applications. To +learn how to Dockerize applications and run them: + +Go to [Dockerizing Applications](containers/dockerizing.md). + + +## Working with containers + +*How do I manage my containers?* + +Once you get a grip on running your applications in Docker containers, you'll learn how to manage those containers. To find out +about how to inspect, monitor and manage containers: + +Go to [Working with Containers](containers/usingdocker.md). + +## Working with Docker images + +*How can I access, share and build my own images?* + +Once you've learnt how to use Docker it's time to take the next step and +learn how to build your own application images with Docker. + +Go to [Working with Docker Images](containers/dockerimages.md). + +## Networking containers + +Until now we've seen how to build individual applications inside Docker +containers. Now learn how to build whole application stacks with Docker +networking. + +Go to [Networking Containers](containers/networkingcontainers.md). + +## Managing data in containers + +Now we know how to link Docker containers together the next step is +learning how to manage data, volumes and mounts inside our containers. + +Go to [Managing Data in Containers](containers/dockervolumes.md). + +## Docker products that complement Engine + +Often, one powerful technology spawns many other inventions that make that easier to get to, easier to use, and more powerful. These spawned things share one common characteristic: they augment the central technology. The following Docker products expand on the core Docker Engine functions. + +### Docker Hub + +Docker Hub is the central hub for Docker. It hosts public Docker images +and provides services to help you build and manage your Docker +environment. To learn more: + +Go to [Using Docker Hub](https://docs.docker.com/docker-hub). + +### Docker Machine + +Docker Machine helps you get Docker Engines up and running quickly. Machine +can set up hosts for Docker Engines on your computer, on cloud providers, +and/or in your data center, and then configure your Docker client to securely +talk to them. + +Go to [Docker Machine user guide](https://docs.docker.com/machine/). + +### Docker Compose + +Docker Compose allows you to define a application's components -- their containers, +configuration, links and volumes -- in a single file. Then a single command +will set everything up and start your application running. + +Go to [Docker Compose user guide](https://docs.docker.com/compose/). + + +### Docker Swarm + +Docker Swarm pools several Docker Engines together and exposes them as a single +virtual Docker Engine. It serves the standard Docker API, so any tool that already +works with Docker can now transparently scale up to multiple hosts. + +Go to [Docker Swarm user guide](https://docs.docker.com/swarm/). + +## Getting help + +* [Docker homepage](https://www.docker.com/) +* [Docker Hub](https://hub.docker.com) +* [Docker blog](https://blog.docker.com/) +* [Docker documentation](https://docs.docker.com/) +* [Docker Getting Started Guide](https://docs.docker.com/mac/started/) +* [Docker code on GitHub](https://github.com/docker/docker) +* [Docker mailing + list](https://groups.google.com/forum/#!forum/docker-user) +* Docker on IRC: irc.freenode.net and channel #docker +* [Docker on Twitter](https://twitter.com/docker) +* Get [Docker help](https://stackoverflow.com/search?q=docker) on + StackOverflow diff --git a/docs/userguide/labels-custom-metadata.md b/docs/userguide/labels-custom-metadata.md new file mode 100644 index 00000000..4c9a1a11 --- /dev/null +++ b/docs/userguide/labels-custom-metadata.md @@ -0,0 +1,229 @@ + + +# Apply custom metadata + +You can apply metadata to your images, containers, or daemons via +labels. Labels serve a wide range of uses, such as adding notes or licensing +information to an image, or to identify a host. + +A label is a `` / `` pair. Docker stores the label values as +*strings*. You can specify multiple labels but each `` must be +unique or the value will be overwritten. If you specify the same `key` several +times but with different values, newer labels overwrite previous labels. Docker +uses the last `key=value` you supply. + +>**Note:** Support for daemon-labels was added in Docker 1.4.1. Labels on +>containers and images are new in Docker 1.6.0 + +## Label keys (namespaces) + +Docker puts no hard restrictions on the `key` used for a label. However, using +simple keys can easily lead to conflicts. For example, you have chosen to +categorize your images by CPU architecture using "architecture" labels in +your Dockerfiles: + + LABEL architecture="amd64" + + LABEL architecture="ARMv7" + +Another user may apply the same label based on a building's "architecture": + + LABEL architecture="Art Nouveau" + +To prevent naming conflicts, Docker recommends using namespaces to label keys +using reverse domain notation. Use the following guidelines to name your keys: + +- All (third-party) tools should prefix their keys with the + reverse DNS notation of a domain controlled by the author. For + example, `com.example.some-label`. + +- The `com.docker.*`, `io.docker.*` and `org.dockerproject.*` namespaces are + reserved for Docker's internal use. + +- Keys should only consist of lower-cased alphanumeric characters, + dots and dashes (for example, `[a-z0-9-.]`). + +- Keys should start *and* end with an alpha numeric character. + +- Keys may not contain consecutive dots or dashes. + +- Keys *without* namespace (dots) are reserved for CLI use. This allows end- + users to add metadata to their containers and images without having to type + cumbersome namespaces on the command-line. + + +These are simply guidelines and Docker does not *enforce* them. However, for +the benefit of the community, you *should* use namespaces for your label keys. + + +## Store structured data in labels + +Label values can contain any data type as long as it can be represented as a +string. For example, consider this JSON document: + + + { + "Description": "A containerized foobar", + "Usage": "docker run --rm example/foobar [args]", + "License": "GPL", + "Version": "0.0.1-beta", + "aBoolean": true, + "aNumber" : 0.01234, + "aNestedArray": ["a", "b", "c"] + } + +You can store this struct in a label by serializing it to a string first: + + LABEL com.example.image-specs="{\"Description\":\"A containerized foobar\",\"Usage\":\"docker run --rm example\\/foobar [args]\",\"License\":\"GPL\",\"Version\":\"0.0.1-beta\",\"aBoolean\":true,\"aNumber\":0.01234,\"aNestedArray\":[\"a\",\"b\",\"c\"]}" + +While it is *possible* to store structured data in label values, Docker treats +this data as a 'regular' string. This means that Docker doesn't offer ways to +query (filter) based on nested properties. If your tool needs to filter on +nested properties, the tool itself needs to implement this functionality. + + +## Add labels to images + +To add labels to an image, use the `LABEL` instruction in your Dockerfile: + + + LABEL [.]= ... + +The `LABEL` instruction adds a label to your image. A `LABEL` consists of a `` +and a ``. +Use an empty string for labels that don't have a ``, +Use surrounding quotes or backslashes for labels that contain +white space characters in the ``: + + LABEL vendor=ACME\ Incorporated + LABEL com.example.version.is-beta= + LABEL com.example.version.is-production="" + LABEL com.example.version="0.0.1-beta" + LABEL com.example.release-date="2015-02-12" + +The `LABEL` instruction also supports setting multiple `` / `` pairs +in a single instruction: + + LABEL com.example.version="0.0.1-beta" com.example.release-date="2015-02-12" + +Long lines can be split up by using a backslash (`\`) as continuation marker: + + LABEL vendor=ACME\ Incorporated \ + com.example.is-beta= \ + com.example.is-production="" \ + com.example.version="0.0.1-beta" \ + com.example.release-date="2015-02-12" + +Docker recommends you add multiple labels in a single `LABEL` instruction. Using +individual instructions for each label can result in an inefficient image. This +is because each `LABEL` instruction in a Dockerfile produces a new IMAGE layer. + +You can view the labels via the `docker inspect` command: + + $ docker inspect 4fa6e0f0c678 + + ... + "Labels": { + "vendor": "ACME Incorporated", + "com.example.is-beta": "", + "com.example.is-production": "", + "com.example.version": "0.0.1-beta", + "com.example.release-date": "2015-02-12" + } + ... + + # Inspect labels on container + $ docker inspect -f "{{json .Config.Labels }}" 4fa6e0f0c678 + + {"Vendor":"ACME Incorporated","com.example.is-beta":"", "com.example.is-production":"", "com.example.version":"0.0.1-beta","com.example.release-date":"2015-02-12"} + + # Inspect labels on images + $ docker inspect -f "{{json .ContainerConfig.Labels }}" myimage + + +## Query labels + +Besides storing metadata, you can filter images and containers by label. To list all +running containers that have the `com.example.is-beta` label: + + # List all running containers that have a `com.example.is-beta` label + $ docker ps --filter "label=com.example.is-beta" + +List all running containers with the label `color` that have a value `blue`: + + $ docker ps --filter "label=color=blue" + +List all images with the label `vendor` that have the value `ACME`: + + $ docker images --filter "label=vendor=ACME" + + +## Container labels + + docker run \ + -d \ + --label com.example.group="webservers" \ + --label com.example.environment="production" \ + busybox \ + top + +Please refer to the [Query labels](#query-labels) section above for information +on how to query labels set on a container. + + +## Daemon labels + + docker daemon \ + --dns 8.8.8.8 \ + --dns 8.8.4.4 \ + -H unix:///var/run/docker.sock \ + --label com.example.environment="production" \ + --label com.example.storage="ssd" + +These labels appear as part of the `docker info` output for the daemon: + + $ docker -D info + Containers: 12 + Running: 5 + Paused: 2 + Stopped: 5 + Images: 672 + Server Version: 1.9.0 + Storage Driver: aufs + Root Dir: /var/lib/docker/aufs + Backing Filesystem: extfs + Dirs: 697 + Dirperm1 Supported: true + Execution Driver: native-0.2 + Logging Driver: json-file + Kernel Version: 3.19.0-22-generic + Operating System: Ubuntu 15.04 + CPUs: 24 + Total Memory: 62.86 GiB + Name: docker + ID: I54V:OLXT:HVMM:TPKO:JPHQ:CQCD:JNLC:O3BZ:4ZVJ:43XJ:PFHZ:6N2S + Debug mode (server): true + File Descriptors: 59 + Goroutines: 159 + System Time: 2015-09-23T14:04:20.699842089+08:00 + EventsListeners: 0 + Init SHA1: + Init Path: /usr/bin/docker + Docker Root Dir: /var/lib/docker + Http Proxy: http://test:test@localhost:8080 + Https Proxy: https://test:test@localhost:8080 + WARNING: No swap limit support + Username: svendowideit + Registry: [https://index.docker.io/v1/] + Labels: + com.example.environment=production + com.example.storage=ssd diff --git a/docs/userguide/networking/configure-dns.md b/docs/userguide/networking/configure-dns.md new file mode 100644 index 00000000..d248f429 --- /dev/null +++ b/docs/userguide/networking/configure-dns.md @@ -0,0 +1,138 @@ + + +# Embedded DNS server in user-defined networks + +The information in this section covers the embedded DNS server operation for +containers in user-defined networks. DNS lookup for containers connected to +user-defined networks works differently compared to the containers connected +to `default bridge` network. + +> **Note**: In order to maintain backward compatibility, the DNS configuration +> in `default bridge` network is retained with no behavioral change. +> Please refer to the [DNS in default bridge network](default_network/configure-dns.md) +> for more information on DNS configuration in the `default bridge` network. + +As of Docker 1.10, the docker daemon implements an embedded DNS server which +provides built-in service discovery for any container created with a valid +`name` or `net-alias` or aliased by `link`. The exact details of how Docker +manages the DNS configurations inside the container can change from one Docker +version to the next. So you should not assume the way the files such as +`/etc/hosts`, `/etc/resolv.conf` are managed inside the containers and leave +the files alone and use the following Docker options instead. + +Various container options that affect container domain name services. + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ --name=CONTAINER-NAME +

+
+

+ Container name configured using --name is used to discover a container within + an user-defined docker network. The embedded DNS server maintains the mapping between + the container name and its IP address (on the network the container is connected to). +

+
+

+ --net-alias=ALIAS +

+
+

+ In addition to --name as described above, a container is discovered by one or more + of its configured --net-alias (or --alias in docker network connect command) + within the user-defined network. The embedded DNS server maintains the mapping between + all of the container aliases and its IP address on a specific user-defined network. + A container can have different aliases in different networks by using the --alias + option in docker network connect command. +

+
+

+ --link=CONTAINER_NAME:ALIAS +

+
+

+ Using this option as you run a container gives the embedded DNS + an extra entry named ALIAS that points to the IP address + of the container identified by CONTAINER_NAME. When using --link + the embedded DNS will guarantee that localized lookup result only on that + container where the --link is used. This lets processes inside the new container + connect to container without having to know its name or IP. +

+

+ --dns=[IP_ADDRESS...] +

+ The IP addresses passed via the --dns option is used by the embedded DNS + server to forward the DNS query if embedded DNS server is unable to resolve a name + resolution request from the containers. + These --dns IP addresses are managed by the embedded DNS server and + will not be updated in the container's /etc/resolv.conf file. +

+ --dns-search=DOMAIN... +

+ Sets the domain names that are searched when a bare unqualified hostname is + used inside of the container. These --dns-search options are managed by the + embedded DNS server and will not be updated in the container's /etc/resolv.conf file. + When a container process attempts to access host and the search + domain example.com is set, for instance, the DNS logic will not only + look up host but also host.example.com. +

+

+ --dns-opt=OPTION... +

+ Sets the options used by DNS resolvers. These options are managed by the embedded + DNS server and will not be updated in the container's /etc/resolv.conf file. +

+

+ See documentation for resolv.conf for a list of valid options +

+ + +In the absence of the `--dns=IP_ADDRESS...`, `--dns-search=DOMAIN...`, or +`--dns-opt=OPTION...` options, Docker uses the `/etc/resolv.conf` of the +host machine (where the `docker` daemon runs). While doing so the daemon +filters out all localhost IP address `nameserver` entries from the host's +original file. + +Filtering is necessary because all localhost addresses on the host are +unreachable from the container's network. After this filtering, if there are +no more `nameserver` entries left in the container's `/etc/resolv.conf` file, +the daemon adds public Google DNS nameservers (8.8.8.8 and 8.8.4.4) to the +container's DNS configuration. If IPv6 is enabled on the daemon, the public +IPv6 Google DNS nameservers will also be added (2001:4860:4860::8888 and +2001:4860:4860::8844). + +> **Note**: If you need access to a host's localhost resolver, you must modify +> your DNS service on the host to listen on a non-localhost address that is +> reachable from within the container. diff --git a/docs/userguide/networking/default_network/binding.md b/docs/userguide/networking/default_network/binding.md new file mode 100644 index 00000000..d8799f4f --- /dev/null +++ b/docs/userguide/networking/default_network/binding.md @@ -0,0 +1,103 @@ + + +# Bind container ports to the host + +The information in this section explains binding container ports within the Docker default bridge. This is a `bridge` network named `bridge` created automatically when you install Docker. + +> **Note**: The [Docker networks feature](../dockernetworks.md) allows you to +create user-defined networks in addition to the default bridge network. + +By default Docker containers can make connections to the outside world, but the +outside world cannot connect to containers. Each outgoing connection will +appear to originate from one of the host machine's own IP addresses thanks to an +`iptables` masquerading rule on the host machine that the Docker server creates +when it starts: + +``` +$ sudo iptables -t nat -L -n +... +Chain POSTROUTING (policy ACCEPT) +target prot opt source destination +MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0 +... +``` +The Docker server creates a masquerade rule that let containers connect to IP +addresses in the outside world. + +If you want containers to accept incoming connections, you will need to provide +special options when invoking `docker run`. There are two approaches. + +First, you can supply `-P` or `--publish-all=true|false` to `docker run` which +is a blanket operation that identifies every port with an `EXPOSE` line in the +image's `Dockerfile` or `--expose ` commandline flag and maps it to a host +port somewhere within an _ephemeral port range_. The `docker port` command then +needs to be used to inspect created mapping. The _ephemeral port range_ is +configured by `/proc/sys/net/ipv4/ip_local_port_range` kernel parameter, +typically ranging from 32768 to 61000. + +Mapping can be specified explicitly using `-p SPEC` or `--publish=SPEC` option. +It allows you to particularize which port on docker server - which can be any +port at all, not just one within the _ephemeral port range_ -- you want mapped +to which port in the container. + +Either way, you should be able to peek at what Docker has accomplished in your +network stack by examining your NAT tables. + +``` +# What your NAT rules might look like when Docker +# is finished setting up a -P forward: + +$ iptables -t nat -L -n +... +Chain DOCKER (2 references) +target prot opt source destination +DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:49153 to:172.17.0.2:80 + +# What your NAT rules might look like when Docker +# is finished setting up a -p 80:80 forward: + +Chain DOCKER (2 references) +target prot opt source destination +DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:172.17.0.2:80 +``` + +You can see that Docker has exposed these container ports on `0.0.0.0`, the +wildcard IP address that will match any possible incoming port on the host +machine. If you want to be more restrictive and only allow container services to +be contacted through a specific external interface on the host machine, you have +two choices. When you invoke `docker run` you can use either `-p +IP:host_port:container_port` or `-p IP::port` to specify the external interface +for one particular binding. + +Or if you always want Docker port forwards to bind to one specific IP address, +you can edit your system-wide Docker server settings and add the option +`--ip=IP_ADDRESS`. Remember to restart your Docker server after editing this +setting. + +> **Note**: With hairpin NAT enabled (`--userland-proxy=false`), containers port +exposure is achieved purely through iptables rules, and no attempt to bind the +exposed port is ever made. This means that nothing prevents shadowing a +previously listening service outside of Docker through exposing the same port +for a container. In such conflicting situation, Docker created iptables rules +will take precedence and route to the container. + +The `--userland-proxy` parameter, true by default, provides a userland +implementation for inter-container and outside-to-container communication. When +disabled, Docker uses both an additional `MASQUERADE` iptable rule and the +`net.ipv4.route_localnet` kernel parameter which allow the host machine to +connect to a local container exposed port through the commonly used loopback +address: this alternative is preferred for performance reasons. + +## Related information + +- [Understand Docker container networks](../dockernetworks.md) +- [Work with network commands](../work-with-networks.md) +- [Legacy container links](dockerlinks.md) diff --git a/docs/userguide/networking/default_network/build-bridges.md b/docs/userguide/networking/default_network/build-bridges.md new file mode 100644 index 00000000..73f35e35 --- /dev/null +++ b/docs/userguide/networking/default_network/build-bridges.md @@ -0,0 +1,78 @@ + + +# Build your own bridge + +This section explains how to build your own bridge to replace the Docker default +bridge. This is a `bridge` network named `bridge` created automatically when you +install Docker. + +> **Note**: The [Docker networks feature](../dockernetworks.md) allows you to +create user-defined networks in addition to the default bridge network. + +You can set up your own bridge before starting Docker and use `-b BRIDGE` or +`--bridge=BRIDGE` to tell Docker to use your bridge instead. If you already +have Docker up and running with its default `docker0` still configured, +you can directly create your bridge and restart Docker with it or want to begin by +stopping the service and removing the interface: + +``` +# Stopping Docker and removing docker0 + +$ sudo service docker stop +$ sudo ip link set dev docker0 down +$ sudo brctl delbr docker0 +$ sudo iptables -t nat -F POSTROUTING +``` + +Then, before starting the Docker service, create your own bridge and give it +whatever configuration you want. Here we will create a simple enough bridge +that we really could just have used the options in the previous section to +customize `docker0`, but it will be enough to illustrate the technique. + +``` +# Create our own bridge + +$ sudo brctl addbr bridge0 +$ sudo ip addr add 192.168.5.1/24 dev bridge0 +$ sudo ip link set dev bridge0 up + +# Confirming that our bridge is up and running + +$ ip addr show bridge0 +4: bridge0: mtu 1500 qdisc noop state UP group default + link/ether 66:38:d0:0d:76:18 brd ff:ff:ff:ff:ff:ff + inet 192.168.5.1/24 scope global bridge0 + valid_lft forever preferred_lft forever + +# Tell Docker about it and restart (on Ubuntu) + +$ echo 'DOCKER_OPTS="-b=bridge0"' >> /etc/default/docker +$ sudo service docker start + +# Confirming new outgoing NAT masquerade is set up + +$ sudo iptables -t nat -L -n +... +Chain POSTROUTING (policy ACCEPT) +target prot opt source destination +MASQUERADE all -- 192.168.5.0/24 0.0.0.0/0 +``` + +The result should be that the Docker server starts successfully and is now +prepared to bind containers to the new bridge. After pausing to verify the +bridge's configuration, try creating a container -- you will see that its IP +address is in your new IP address range, which Docker will have auto-detected. + +You can use the `brctl show` command to see Docker add and remove interfaces +from the bridge as you start and stop containers, and can run `ip addr` and `ip +route` inside a container to see that it has been given an address in the +bridge's IP address range and has been told to use the Docker host's IP address +on the bridge as its default gateway to the rest of the Internet. diff --git a/docs/userguide/networking/default_network/configure-dns.md b/docs/userguide/networking/default_network/configure-dns.md new file mode 100644 index 00000000..2703aca1 --- /dev/null +++ b/docs/userguide/networking/default_network/configure-dns.md @@ -0,0 +1,132 @@ + + +# Configure container DNS + +The information in this section explains configuring container DNS within +the Docker default bridge. This is a `bridge` network named `bridge` created +automatically when you install Docker. + +> **Note**: The [Docker networks feature](../dockernetworks.md) allows you to create user-defined networks in addition to the default bridge network. Please refer to the [Docker Embedded DNS](../configure-dns.md) section for more information on DNS configurations in user-defined networks. + +How can Docker supply each container with a hostname and DNS configuration, without having to build a custom image with the hostname written inside? Its trick is to overlay three crucial `/etc` files inside the container with virtual files where it can write fresh information. You can see this by running `mount` inside a container: + +``` +$$ mount +... +/dev/disk/by-uuid/1fec...ebdf on /etc/hostname type ext4 ... +/dev/disk/by-uuid/1fec...ebdf on /etc/hosts type ext4 ... +/dev/disk/by-uuid/1fec...ebdf on /etc/resolv.conf type ext4 ... +... +``` + +This arrangement allows Docker to do clever things like keep `resolv.conf` up to date across all containers when the host machine receives new configuration over DHCP later. The exact details of how Docker maintains these files inside the container can change from one Docker version to the next, so you should leave the files themselves alone and use the following Docker options instead. + +Four different options affect container domain name services. + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ -h HOSTNAME or --hostname=HOSTNAME +

+
+

+ Sets the hostname by which the container knows itself. This is written + into /etc/hostname, into /etc/hosts as the name + of the container's host-facing IP address, and is the name that + /bin/bash inside the container will display inside its + prompt. But the hostname is not easy to see from outside the container. + It will not appear in docker ps nor in the + /etc/hosts file of any other container. +

+
+

+ --link=CONTAINER_NAME or ID:ALIAS +

+
+

+ Using this option as you run a container gives the new + container's /etc/hosts an extra entry named + ALIAS that points to the IP address of the container + identified by CONTAINER_NAME_or_ID. This lets processes + inside the new container connect to the hostname ALIAS + without having to know its IP. The --link= option is + discussed in more detail below. Because Docker may assign a different IP + address to the linked containers on restart, Docker updates the + ALIAS entry in the /etc/hosts file of the + recipient containers. +

+

+ --dns=IP_ADDRESS... +

+ Sets the IP addresses added as server lines to the container's + /etc/resolv.conf file. Processes in the container, when + confronted with a hostname not in /etc/hosts, will connect to + these IP addresses on port 53 looking for name resolution services.

+ --dns-search=DOMAIN... +

+ Sets the domain names that are searched when a bare unqualified hostname is + used inside of the container, by writing search lines into the + container's /etc/resolv.conf. When a container process attempts + to access host and the search domain example.com + is set, for instance, the DNS logic will not only look up host + but also host.example.com. +

+

+ Use --dns-search=. if you don't wish to set the search domain. +

+

+ --dns-opt=OPTION... +

+ Sets the options used by DNS resolvers by writing an options + line into the container's /etc/resolv.conf. +

+

+ See documentation for resolv.conf for a list of valid options +

+ + +Regarding DNS settings, in the absence of the `--dns=IP_ADDRESS...`, `--dns-search=DOMAIN...`, or `--dns-opt=OPTION...` options, Docker makes each container's `/etc/resolv.conf` look like the `/etc/resolv.conf` of the host machine (where the `docker` daemon runs). When creating the container's `/etc/resolv.conf`, the daemon filters out all localhost IP address `nameserver` entries from the host's original file. + +Filtering is necessary because all localhost addresses on the host are unreachable from the container's network. After this filtering, if there are no more `nameserver` entries left in the container's `/etc/resolv.conf` file, the daemon adds public Google DNS nameservers (8.8.8.8 and 8.8.4.4) to the container's DNS configuration. If IPv6 is enabled on the daemon, the public IPv6 Google DNS nameservers will also be added (2001:4860:4860::8888 and 2001:4860:4860::8844). + +> **Note**: If you need access to a host's localhost resolver, you must modify your DNS service on the host to listen on a non-localhost address that is reachable from within the container. + +You might wonder what happens when the host machine's `/etc/resolv.conf` file changes. The `docker` daemon has a file change notifier active which will watch for changes to the host DNS configuration. + +> **Note**: The file change notifier relies on the Linux kernel's inotify feature. Because this feature is currently incompatible with the overlay filesystem driver, a Docker daemon using "overlay" will not be able to take advantage of the `/etc/resolv.conf` auto-update feature. + +When the host file changes, all stopped containers which have a matching `resolv.conf` to the host will be updated immediately to this newest host configuration. Containers which are running when the host configuration changes will need to stop and start to pick up the host changes due to lack of a facility to ensure atomic writes of the `resolv.conf` file while the container is running. If the container's `resolv.conf` has been edited since it was started with the default configuration, no replacement will be attempted as it would overwrite the changes performed by the container. If the options (`--dns`, `--dns-search`, or `--dns-opt`) have been used to modify the default host configuration, then the replacement with an updated host's `/etc/resolv.conf` will not happen as well. + +> **Note**: For containers which were created prior to the implementation of the `/etc/resolv.conf` update feature in Docker 1.5.0: those containers will **not** receive updates when the host `resolv.conf` file changes. Only containers created with Docker 1.5.0 and above will utilize this auto-update feature. diff --git a/docs/userguide/networking/default_network/container-communication.md b/docs/userguide/networking/default_network/container-communication.md new file mode 100644 index 00000000..6b93681a --- /dev/null +++ b/docs/userguide/networking/default_network/container-communication.md @@ -0,0 +1,125 @@ + + +# Understand container communication + +The information in this section explains container communication within the +Docker default bridge. This is a `bridge` network named `bridge` created +automatically when you install Docker. + +**Note**: The [Docker networks feature](../dockernetworks.md) allows you to create user-defined networks in addition to the default bridge network. + +## Communicating to the outside world + +Whether a container can talk to the world is governed by two factors. The first +factor is whether the host machine is forwarding its IP packets. The second is +whether the host's `iptables` allow this particular connection. + +IP packet forwarding is governed by the `ip_forward` system parameter. Packets +can only pass between containers if this parameter is `1`. Usually you will +simply leave the Docker server at its default setting `--ip-forward=true` and +Docker will go set `ip_forward` to `1` for you when the server starts up. If you +set `--ip-forward=false` and your system's kernel has it enabled, the +`--ip-forward=false` option has no effect. To check the setting on your kernel +or to turn it on manually: +``` + $ sysctl net.ipv4.conf.all.forwarding + net.ipv4.conf.all.forwarding = 0 + $ sysctl net.ipv4.conf.all.forwarding=1 + $ sysctl net.ipv4.conf.all.forwarding + net.ipv4.conf.all.forwarding = 1 +``` + +Many using Docker will want `ip_forward` to be on, to at least make +communication _possible_ between containers and the wider world. May also be +needed for inter-container communication if you are in a multiple bridge setup. + +Docker will never make changes to your system `iptables` rules if you set +`--iptables=false` when the daemon starts. Otherwise the Docker server will +append forwarding rules to the `DOCKER` filter chain. + +Docker will not delete or modify any pre-existing rules from the `DOCKER` filter +chain. This allows the user to create in advance any rules required to further +restrict access to the containers. + +Docker's forward rules permit all external source IPs by default. To allow only +a specific IP or network to access the containers, insert a negated rule at the +top of the `DOCKER` filter chain. For example, to restrict external access such +that _only_ source IP 8.8.8.8 can access the containers, the following rule +could be added: + +``` +$ iptables -I DOCKER -i ext_if ! -s 8.8.8.8 -j DROP +``` + +where *ext_if* is the name of the interface providing external connectivity to the host. + +## Communication between containers + +Whether two containers can communicate is governed, at the operating system level, by two factors. + +- Does the network topology even connect the containers' network interfaces? By default Docker will attach all containers to a single `docker0` bridge, providing a path for packets to travel between them. See the later sections of this document for other possible topologies. + +- Do your `iptables` allow this particular connection? Docker will never make changes to your system `iptables` rules if you set `--iptables=false` when the daemon starts. Otherwise the Docker server will add a default rule to the `FORWARD` chain with a blanket `ACCEPT` policy if you retain the default `--icc=true`, or else will set the policy to `DROP` if `--icc=false`. + +It is a strategic question whether to leave `--icc=true` or change it to +`--icc=false` so that `iptables` will protect other containers -- and the main +host -- from having arbitrary ports probed or accessed by a container that gets +compromised. + +If you choose the most secure setting of `--icc=false`, then how can containers +communicate in those cases where you _want_ them to provide each other services? +The answer is the `--link=CONTAINER_NAME_or_ID:ALIAS` option, which was +mentioned in the previous section because of its effect upon name services. If +the Docker daemon is running with both `--icc=false` and `--iptables=true` +then, when it sees `docker run` invoked with the `--link=` option, the Docker +server will insert a pair of `iptables` `ACCEPT` rules so that the new +container can connect to the ports exposed by the other container -- the ports +that it mentioned in the `EXPOSE` lines of its `Dockerfile`. + +> **Note**: The value `CONTAINER_NAME` in `--link=` must either be an +auto-assigned Docker name like `stupefied_pare` or else the name you assigned +with `--name=` when you ran `docker run`. It cannot be a hostname, which Docker +will not recognize in the context of the `--link=` option. + +You can run the `iptables` command on your Docker host to see whether the `FORWARD` chain has a default policy of `ACCEPT` or `DROP`: + +``` +# When --icc=false, you should see a DROP rule: + +$ sudo iptables -L -n +... +Chain FORWARD (policy ACCEPT) +target prot opt source destination +DOCKER all -- 0.0.0.0/0 0.0.0.0/0 +DROP all -- 0.0.0.0/0 0.0.0.0/0 +... + +# When a --link= has been created under --icc=false, +# you should see port-specific ACCEPT rules overriding +# the subsequent DROP policy for all other packets: + +$ sudo iptables -L -n +... +Chain FORWARD (policy ACCEPT) +target prot opt source destination +DOCKER all -- 0.0.0.0/0 0.0.0.0/0 +DROP all -- 0.0.0.0/0 0.0.0.0/0 + +Chain DOCKER (1 references) +target prot opt source destination +ACCEPT tcp -- 172.17.0.2 172.17.0.3 tcp spt:80 +ACCEPT tcp -- 172.17.0.3 172.17.0.2 tcp dpt:80 +``` + +> **Note**: Docker is careful that its host-wide `iptables` rules fully expose +containers to each other's raw IP addresses, so connections from one container +to another should always appear to be originating from the first container's own +IP address. diff --git a/docs/userguide/networking/default_network/custom-docker0.md b/docs/userguide/networking/default_network/custom-docker0.md new file mode 100644 index 00000000..6178b06a --- /dev/null +++ b/docs/userguide/networking/default_network/custom-docker0.md @@ -0,0 +1,62 @@ + + +# Customize the docker0 bridge + +The information in this section explains how to customize the Docker default bridge. This is a `bridge` network named `bridge` created automatically when you install Docker. + +**Note**: The [Docker networks feature](../dockernetworks.md) allows you to create user-defined networks in addition to the default bridge network. + +By default, the Docker server creates and configures the host system's `docker0` interface as an _Ethernet bridge_ inside the Linux kernel that can pass packets back and forth between other physical or virtual network interfaces so that they behave as a single Ethernet network. + +Docker configures `docker0` with an IP address, netmask and IP allocation range. The host machine can both receive and send packets to containers connected to the bridge, and gives it an MTU -- the _maximum transmission unit_ or largest packet length that the interface will allow -- of 1,500 bytes. These options are configurable at server startup: + +- `--bip=CIDR` -- supply a specific IP address and netmask for the `docker0` bridge, using standard CIDR notation like `192.168.1.5/24`. + +- `--fixed-cidr=CIDR` -- restrict the IP range from the `docker0` subnet, using the standard CIDR notation like `172.167.1.0/28`. This range must be an IPv4 range for fixed IPs (ex: 10.20.0.0/16) and must be a subset of the bridge IP range (`docker0` or set using `--bridge`). For example with `--fixed-cidr=192.168.1.0/25`, IPs for your containers will be chosen from the first half of `192.168.1.0/24` subnet. + +- `--mtu=BYTES` -- override the maximum packet length on `docker0`. + +Once you have one or more containers up and running, you can confirm that Docker has properly connected them to the `docker0` bridge by running the `brctl` command on the host machine and looking at the `interfaces` column of the output. Here is a host with two different containers connected: + +``` +# Display bridge info + +$ sudo brctl show +bridge name bridge id STP enabled interfaces +docker0 8000.3a1d7362b4ee no veth65f9 + vethdda6 +``` + +If the `brctl` command is not installed on your Docker host, then on Ubuntu you should be able to run `sudo apt-get install bridge-utils` to install it. + +Finally, the `docker0` Ethernet bridge settings are used every time you create a new container. Docker selects a free IP address from the range available on the bridge each time you `docker run` a new container, and configures the container's `eth0` interface with that IP address and the bridge's netmask. The Docker host's own IP address on the bridge is used as the default gateway by which each container reaches the rest of the Internet. + +``` +# The network, as seen from a container + +$ docker run -i -t --rm base /bin/bash + +$$ ip addr show eth0 +24: eth0: mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 + link/ether 32:6f:e0:35:57:91 brd ff:ff:ff:ff:ff:ff + inet 172.17.0.3/16 scope global eth0 + valid_lft forever preferred_lft forever + inet6 fe80::306f:e0ff:fe35:5791/64 scope link + valid_lft forever preferred_lft forever + +$$ ip route +default via 172.17.42.1 dev eth0 +172.17.0.0/16 dev eth0 proto kernel scope link src 172.17.0.3 + +$$ exit +``` + +Remember that the Docker host will not be willing to forward container packets out on to the Internet unless its `ip_forward` system setting is `1` -- see the section on [Communicating to the outside world](container-communication.md#communicating-to-the-outside-world) for details. diff --git a/docs/userguide/networking/default_network/dockerlinks.md b/docs/userguide/networking/default_network/dockerlinks.md new file mode 100644 index 00000000..cee84cbd --- /dev/null +++ b/docs/userguide/networking/default_network/dockerlinks.md @@ -0,0 +1,358 @@ + + +# Legacy container links + +The information in this section explains legacy container links within the Docker default bridge. This is a `bridge` network named `bridge` created automatically when you install Docker. + +Before the [Docker networks feature](../dockernetworks.md), you could use the +Docker link feature to allow containers to discover each other and securely +transfer information about one container to another container. With the +introduction of the Docker networks feature, you can still create links but they +behave differently between default `bridge` network and +[user defined networks](../work-with-networks.md#linking-containers-in-user-defined-networks) + +This section briefly discusses connecting via a network port and then goes into +detail on container linking in default `bridge` network. + +## Connect using network port mapping + +In [the Using Docker section](../../containers/usingdocker.md), you created a +container that ran a Python Flask application: + + $ docker run -d -P training/webapp python app.py + +> **Note:** +> Containers have an internal network and an IP address +> (as we saw when we used the `docker inspect` command to show the container's +> IP address in the [Using Docker](../../containers/usingdocker.md) section). +> Docker can have a variety of network configurations. You can see more +> information on Docker networking [here](../index.md). + +When that container was created, the `-P` flag was used to automatically map +any network port inside it to a random high port within an *ephemeral port +range* on your Docker host. Next, when `docker ps` was run, you saw that port +5000 in the container was bound to port 49155 on the host. + + $ docker ps nostalgic_morse + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + bc533791f3f5 training/webapp:latest python app.py 5 seconds ago Up 2 seconds 0.0.0.0:49155->5000/tcp nostalgic_morse + +You also saw how you can bind a container's ports to a specific port using +the `-p` flag. Here port 80 of the host is mapped to port 5000 of the +container: + + $ docker run -d -p 80:5000 training/webapp python app.py + +And you saw why this isn't such a great idea because it constrains you to +only one container on that specific port. + +Instead, you may specify a range of host ports to bind a container port to +that is different than the default *ephemeral port range*: + + $ docker run -d -p 8000-9000:5000 training/webapp python app.py + +This would bind port 5000 in the container to a randomly available port +between 8000 and 9000 on the host. + +There are also a few other ways you can configure the `-p` flag. By +default the `-p` flag will bind the specified port to all interfaces on +the host machine. But you can also specify a binding to a specific +interface, for example only to the `localhost`. + + $ docker run -d -p 127.0.0.1:80:5000 training/webapp python app.py + +This would bind port 5000 inside the container to port 80 on the +`localhost` or `127.0.0.1` interface on the host machine. + +Or, to bind port 5000 of the container to a dynamic port but only on the +`localhost`, you could use: + + $ docker run -d -p 127.0.0.1::5000 training/webapp python app.py + +You can also bind UDP ports by adding a trailing `/udp`. For example: + + $ docker run -d -p 127.0.0.1:80:5000/udp training/webapp python app.py + +You also learned about the useful `docker port` shortcut which showed us the +current port bindings. This is also useful for showing you specific port +configurations. For example, if you've bound the container port to the +`localhost` on the host machine, then the `docker port` output will reflect that. + + $ docker port nostalgic_morse 5000 + 127.0.0.1:49155 + +> **Note:** +> The `-p` flag can be used multiple times to configure multiple ports. + +## Connect with the linking system + +> **Note**: +> This section covers the legacy link feature in the default `bridge` network. +> Please refer to [linking containers in user-defined networks] +> (../work-with-networks.md#linking-containers-in-user-defined-networks) +> for more information on links in user-defined networks. + +Network port mappings are not the only way Docker containers can connect to one +another. Docker also has a linking system that allows you to link multiple +containers together and send connection information from one to another. When +containers are linked, information about a source container can be sent to a +recipient container. This allows the recipient to see selected data describing +aspects of the source container. + +### The importance of naming + +To establish links, Docker relies on the names of your containers. +You've already seen that each container you create has an automatically +created name; indeed you've become familiar with our old friend +`nostalgic_morse` during this guide. You can also name containers +yourself. This naming provides two useful functions: + +1. It can be useful to name containers that do specific functions in a way + that makes it easier for you to remember them, for example naming a + container containing a web application `web`. + +2. It provides Docker with a reference point that allows it to refer to other + containers, for example, you can specify to link the container `web` to container `db`. + +You can name your container by using the `--name` flag, for example: + + $ docker run -d -P --name web training/webapp python app.py + +This launches a new container and uses the `--name` flag to +name the container `web`. You can see the container's name using the +`docker ps` command. + + $ docker ps -l + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + aed84ee21bde training/webapp:latest python app.py 12 hours ago Up 2 seconds 0.0.0.0:49154->5000/tcp web + +You can also use `docker inspect` to return the container's name. + + +> **Note:** +> Container names have to be unique. That means you can only call +> one container `web`. If you want to re-use a container name you must delete +> the old container (with `docker rm`) before you can create a new +> container with the same name. As an alternative you can use the `--rm` +> flag with the `docker run` command. This will delete the container +> immediately after it is stopped. + +## Communication across links + +Links allow containers to discover each other and securely transfer information +about one container to another container. When you set up a link, you create a +conduit between a source container and a recipient container. The recipient can +then access select data about the source. To create a link, you use the `--link` +flag. First, create a new container, this time one containing a database. + + $ docker run -d --name db training/postgres + +This creates a new container called `db` from the `training/postgres` +image, which contains a PostgreSQL database. + +Now, you need to delete the `web` container you created previously so you can replace it +with a linked one: + + $ docker rm -f web + +Now, create a new `web` container and link it with your `db` container. + + $ docker run -d -P --name web --link db:db training/webapp python app.py + +This will link the new `web` container with the `db` container you created +earlier. The `--link` flag takes the form: + + --link :alias + +Where `name` is the name of the container we're linking to and `alias` is an +alias for the link name. You'll see how that alias gets used shortly. +The `--link` flag also takes the form: + + --link + +In which case the alias will match the name. You could have written the previous +example as: + + $ docker run -d -P --name web --link db training/webapp python app.py + +Next, inspect your linked containers with `docker inspect`: + + $ docker inspect -f "{{ .HostConfig.Links }}" web + [/db:/web/db] + +You can see that the `web` container is now linked to the `db` container +`web/db`. Which allows it to access information about the `db` container. + +So what does linking the containers actually do? You've learned that a link allows a +source container to provide information about itself to a recipient container. In +our example, the recipient, `web`, can access information about the source `db`. To do +this, Docker creates a secure tunnel between the containers that doesn't need to +expose any ports externally on the container; you'll note when we started the +`db` container we did not use either the `-P` or `-p` flags. That's a big benefit of +linking: we don't need to expose the source container, here the PostgreSQL database, to +the network. + +Docker exposes connectivity information for the source container to the +recipient container in two ways: + +* Environment variables, +* Updating the `/etc/hosts` file. + +### Environment variables + +Docker creates several environment variables when you link containers. Docker +automatically creates environment variables in the target container based on +the `--link` parameters. It will also expose all environment variables +originating from Docker from the source container. These include variables from: + +* the `ENV` commands in the source container's Dockerfile +* the `-e`, `--env` and `--env-file` options on the `docker run` +command when the source container is started + +These environment variables enable programmatic discovery from within the +target container of information related to the source container. + +> **Warning**: +> It is important to understand that *all* environment variables originating +> from Docker within a container are made available to *any* container +> that links to it. This could have serious security implications if sensitive +> data is stored in them. + +Docker sets an `_NAME` environment variable for each target container +listed in the `--link` parameter. For example, if a new container called +`web` is linked to a database container called `db` via `--link db:webdb`, +then Docker creates a `WEBDB_NAME=/web/webdb` variable in the `web` container. + +Docker also defines a set of environment variables for each port exposed by the +source container. Each variable has a unique prefix in the form: + +`_PORT__` + +The components in this prefix are: + +* the alias `` specified in the `--link` parameter (for example, `webdb`) +* the `` number exposed +* a `` which is either TCP or UDP + +Docker uses this prefix format to define three distinct environment variables: + +* The `prefix_ADDR` variable contains the IP Address from the URL, for +example `WEBDB_PORT_5432_TCP_ADDR=172.17.0.82`. +* The `prefix_PORT` variable contains just the port number from the URL for +example `WEBDB_PORT_5432_TCP_PORT=5432`. +* The `prefix_PROTO` variable contains just the protocol from the URL for +example `WEBDB_PORT_5432_TCP_PROTO=tcp`. + +If the container exposes multiple ports, an environment variable set is +defined for each one. This means, for example, if a container exposes 4 ports +that Docker creates 12 environment variables, 3 for each port. + +Additionally, Docker creates an environment variable called `_PORT`. +This variable contains the URL of the source container's first exposed port. +The 'first' port is defined as the exposed port with the lowest number. +For example, consider the `WEBDB_PORT=tcp://172.17.0.82:5432` variable. If +that port is used for both tcp and udp, then the tcp one is specified. + +Finally, Docker also exposes each Docker originated environment variable +from the source container as an environment variable in the target. For each +variable Docker creates an `_ENV_` variable in the target +container. The variable's value is set to the value Docker used when it +started the source container. + +Returning back to our database example, you can run the `env` +command to list the specified container's environment variables. + +``` + $ docker run --rm --name web2 --link db:db training/webapp env + . . . + DB_NAME=/web2/db + DB_PORT=tcp://172.17.0.5:5432 + DB_PORT_5432_TCP=tcp://172.17.0.5:5432 + DB_PORT_5432_TCP_PROTO=tcp + DB_PORT_5432_TCP_PORT=5432 + DB_PORT_5432_TCP_ADDR=172.17.0.5 + . . . +``` + +You can see that Docker has created a series of environment variables with +useful information about the source `db` container. Each variable is prefixed +with +`DB_`, which is populated from the `alias` you specified above. If the `alias` +were `db1`, the variables would be prefixed with `DB1_`. You can use these +environment variables to configure your applications to connect to the database +on the `db` container. The connection will be secure and private; only the +linked `web` container will be able to talk to the `db` container. + +### Important notes on Docker environment variables + +Unlike host entries in the [`/etc/hosts` file](#updating-the-etchosts-file), +IP addresses stored in the environment variables are not automatically updated +if the source container is restarted. We recommend using the host entries in +`/etc/hosts` to resolve the IP address of linked containers. + +These environment variables are only set for the first process in the +container. Some daemons, such as `sshd`, will scrub them when spawning shells +for connection. + +### Updating the `/etc/hosts` file + +In addition to the environment variables, Docker adds a host entry for the +source container to the `/etc/hosts` file. Here's an entry for the `web` +container: + + $ docker run -t -i --rm --link db:webdb training/webapp /bin/bash + root@aed84ee21bde:/opt/webapp# cat /etc/hosts + 172.17.0.7 aed84ee21bde + . . . + 172.17.0.5 webdb 6e5cdeb2d300 db + +You can see two relevant host entries. The first is an entry for the `web` +container that uses the Container ID as a host name. The second entry uses the +link alias to reference the IP address of the `db` container. In addition to +the alias you provide, the linked container's name--if unique from the alias +provided to the `--link` parameter--and the linked container's hostname will +also be added in `/etc/hosts` for the linked container's IP address. You can ping +that host now via any of these entries: + + root@aed84ee21bde:/opt/webapp# apt-get install -yqq inetutils-ping + root@aed84ee21bde:/opt/webapp# ping webdb + PING webdb (172.17.0.5): 48 data bytes + 56 bytes from 172.17.0.5: icmp_seq=0 ttl=64 time=0.267 ms + 56 bytes from 172.17.0.5: icmp_seq=1 ttl=64 time=0.250 ms + 56 bytes from 172.17.0.5: icmp_seq=2 ttl=64 time=0.256 ms + +> **Note:** +> In the example, you'll note you had to install `ping` because it was not included +> in the container initially. + +Here, you used the `ping` command to ping the `db` container using its host entry, +which resolves to `172.17.0.5`. You can use this host entry to configure an application +to make use of your `db` container. + +> **Note:** +> You can link multiple recipient containers to a single source. For +> example, you could have multiple (differently named) web containers attached to your +>`db` container. + +If you restart the source container, the linked containers `/etc/hosts` files +will be automatically updated with the source container's new IP address, +allowing linked communication to continue. + + $ docker restart db + db + $ docker run -t -i --rm --link db:db training/webapp /bin/bash + root@aed84ee21bde:/opt/webapp# cat /etc/hosts + 172.17.0.7 aed84ee21bde + . . . + 172.17.0.9 db + +# Related information diff --git a/docs/userguide/networking/default_network/images/ipv6_basic_host_config.gliffy b/docs/userguide/networking/default_network/images/ipv6_basic_host_config.gliffy new file mode 100644 index 00000000..8d0450fc --- /dev/null +++ b/docs/userguide/networking/default_network/images/ipv6_basic_host_config.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#FFFFFF","width":414,"height":127,"nodeIndex":173,"autoFit":true,"exportBorder":false,"gridOn":false,"snapToGrid":false,"drawingGuidesOn":false,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"maxWidth":5000,"maxHeight":5000,"themeData":null,"viewportType":"default","fitBB":{"min":{"x":8.5,"y":0.5},"max":{"x":413.75,"y":126.5}},"objects":[{"x":6.5,"y":106.0,"rotation":0.0,"id":9,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":20,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

docker0 fe80::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":19.5,"y":9.0,"rotation":0.0,"id":7,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":19,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":31.5,"y":23.5,"rotation":0.0,"id":4,"width":100.0,"height":75.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":16,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#a4c2f4","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":5,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":11.75,"y":0.5,"rotation":0.0,"id":60,"width":402.0,"height":126.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":2,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]},{"x":146.5,"y":83.0,"rotation":0.0,"id":164,"width":249.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":44,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

ip -6 route add 2001:db8:1::/64 dev docker0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":146.5,"y":27.5,"rotation":0.0,"id":73,"width":249.0,"height":16.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":35,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

ip -6 route add default via fe80::1 dev eth0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]}],"shapeStyles":{"com.gliffy.shape.basic.basic_v1.default":{"fill":"#fff2cc","stroke":"#333333","strokeWidth":2,"dashStyle":"2.0,2.0","gradient":true,"shadow":true}},"lineStyles":{"global":{"stroke":"#d9d9d9"}},"textStyles":{"global":{"size":"12px","color":"#b7b7b7"}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":["com.gliffy.libraries.basic.basic_v1.default","com.gliffy.libraries.flowchart.flowchart_v1.default","com.gliffy.libraries.swimlanes.swimlanes_v1.default","com.gliffy.libraries.uml.uml_v2.class","com.gliffy.libraries.uml.uml_v2.sequence","com.gliffy.libraries.uml.uml_v2.activity","com.gliffy.libraries.erd.erd_v1.default","com.gliffy.libraries.ui.ui_v3.containers_content","com.gliffy.libraries.ui.ui_v3.forms_controls","com.gliffy.libraries.images"],"autosaveDisabled":false},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/docs/userguide/networking/default_network/images/ipv6_basic_host_config.svg b/docs/userguide/networking/default_network/images/ipv6_basic_host_config.svg new file mode 100644 index 00000000..0095b8bd --- /dev/null +++ b/docs/userguide/networking/default_network/images/ipv6_basic_host_config.svg @@ -0,0 +1 @@ +Host2eth02001:db8::1/64docker0fe80::1/64ip -6routeadddefaultviafe80::1deveth0ip -6routeadd2001:db8:1::/64devdocker0 \ No newline at end of file diff --git a/docs/userguide/networking/default_network/images/ipv6_ndp_proxying.gliffy b/docs/userguide/networking/default_network/images/ipv6_ndp_proxying.gliffy new file mode 100644 index 00000000..698723e1 --- /dev/null +++ b/docs/userguide/networking/default_network/images/ipv6_ndp_proxying.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#FFFFFF","width":616,"height":438,"nodeIndex":207,"autoFit":true,"exportBorder":false,"gridOn":false,"snapToGrid":false,"drawingGuidesOn":true,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"maxWidth":5000,"maxHeight":5000,"themeData":null,"viewportType":"default","fitBB":{"min":{"x":3,"y":-7.75},"max":{"x":615.5,"y":437.5}},"objects":[{"x":173.0,"y":117.0,"rotation":0.0,"id":190,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":30,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":0,"py":1.0,"px":0.7071067811865476}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":186,"py":0.0,"px":0.2928932188134524}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":"4.0,4.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[120.21067811865476,-7.0],[335.78932188134524,57.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":195.0,"y":117.0,"rotation":0.0,"id":83,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":23,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":222.5,"y":35.0,"rotation":0.0,"id":0,"width":100.0,"height":75.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":7,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#fff2cc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":1,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Router

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":26.0,"y":109.0,"rotation":0.0,"id":33,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":6,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":0,"py":0.9999999999999998,"px":0.29289321881345254}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":2,"py":0.0,"px":0.7071067811865476}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[225.78932188134524,0.9999999999999858],[57.710678118654755,65.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":20.289321881345245,"y":150.0,"rotation":0.0,"id":32,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":5,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":4,"py":0.0,"px":0.2928932188134524}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":0,"py":1.0,"px":0.7071067811865476}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[333.5,24.5],[272.9213562373095,-40.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":271.0,"y":37.0,"rotation":0.0,"id":89,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":1,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":0,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#d9d9d9","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[1.5,-2.0],[1.5,-21.125],[1.5,-21.125],[1.5,-40.25]],"lockSegments":{},"ortho":true}},"linkMap":[],"children":[]},{"x":151.0,"y":115.0,"rotation":0.0,"id":183,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":0,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":0,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":179,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[121.5,-5.0],[62.5,59.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":455.5,"y":257.0,"rotation":0.0,"id":200,"width":150.0,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":200,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":5,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

expected Container location

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":467.5,"y":156.0,"rotation":0.0,"id":185,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":29,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8::c00y/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":479.5,"y":174.5,"rotation":0.0,"id":186,"width":100.0,"height":75.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":27,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#e2e2e2","gradient":false,"dashStyle":"2,2","dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":187,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container x

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":151.5,"y":156.0,"rotation":0.0,"id":178,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":26,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8::b001/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":163.5,"y":174.5,"rotation":0.0,"id":179,"width":100.0,"height":75.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":24,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#a4c2f4","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":180,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":299.5,"y":257.0,"rotation":0.0,"id":9,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":15,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

docker0 fe80::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":317.5,"y":156.0,"rotation":0.0,"id":7,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":14,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8::c001/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":1.0,"y":156.0,"rotation":0.0,"id":6,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":13,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8::a001/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":324.5,"y":174.5,"rotation":0.0,"id":4,"width":100.0,"height":75.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":11,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#a4c2f4","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":5,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host3

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":13.0,"y":174.5,"rotation":0.0,"id":2,"width":100.0,"height":75.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":9,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#a4c2f4","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":3,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host1

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":-142.5,"y":118.5,"rotation":0.0,"id":31,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":4,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":4,"py":1.0,"px":0.7071067811865476}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":25,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[537.7106781186548,131.0],[602.0,204.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":-181.5,"y":122.5,"rotation":0.0,"id":30,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":3,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":4,"py":0.9999999999999998,"px":0.29289321881345254}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":27,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[535.2893218813452,127.0],[473.0,200.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":386.0,"y":306.0,"rotation":0.0,"id":78,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":22,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8::c00a/125

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":218.0,"y":306.0,"rotation":0.0,"id":77,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":21,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8::c009/125

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":409.5,"y":323.0,"rotation":0.0,"id":25,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.square","order":18,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ead1dc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":26,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":241.5,"y":323.0,"rotation":0.0,"id":27,"width":99.99999999999999,"height":99.99999999999999,"uid":"com.gliffy.shape.basic.basic_v1.default.square","order":16,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ead1dc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":28,"width":95.99999999999999,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container1

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":207.75,"y":297.5,"rotation":0.0,"id":58,"width":339.75,"height":140.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":2,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]}],"shapeStyles":{"com.gliffy.shape.basic.basic_v1.default":{"fill":"#e2e2e2","stroke":"#333333","strokeWidth":2,"dashStyle":"2.0,2.0","gradient":false,"shadow":true}},"lineStyles":{"global":{"stroke":"#cccccc","strokeWidth":2,"dashStyle":"4.0,4.0"}},"textStyles":{"global":{"size":"12px","italic":true}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":["com.gliffy.libraries.basic.basic_v1.default","com.gliffy.libraries.flowchart.flowchart_v1.default","com.gliffy.libraries.swimlanes.swimlanes_v1.default","com.gliffy.libraries.uml.uml_v2.class","com.gliffy.libraries.uml.uml_v2.sequence","com.gliffy.libraries.uml.uml_v2.activity","com.gliffy.libraries.erd.erd_v1.default","com.gliffy.libraries.ui.ui_v3.containers_content","com.gliffy.libraries.ui.ui_v3.forms_controls","com.gliffy.libraries.images"],"autosaveDisabled":false},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/docs/userguide/networking/default_network/images/ipv6_ndp_proxying.svg b/docs/userguide/networking/default_network/images/ipv6_ndp_proxying.svg new file mode 100644 index 00000000..49b2da9f --- /dev/null +++ b/docs/userguide/networking/default_network/images/ipv6_ndp_proxying.svg @@ -0,0 +1 @@ +RouterHost1Host3eth02001:db8::a001/64eth02001:db8::c001/64docker0fe80::1/64Container1Container2eth02001:db8::c009/125eth02001:db8::c00a/125eth02001:db8::1/64Host2eth02001:db8::b001/64Containerxeth02001:db8::c00y/64expectedContainerlocation \ No newline at end of file diff --git a/docs/userguide/networking/default_network/images/ipv6_routed_network_example.gliffy b/docs/userguide/networking/default_network/images/ipv6_routed_network_example.gliffy new file mode 100644 index 00000000..544fd52d --- /dev/null +++ b/docs/userguide/networking/default_network/images/ipv6_routed_network_example.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#FFFFFF","width":893,"height":447,"nodeIndex":185,"autoFit":true,"exportBorder":false,"gridOn":false,"snapToGrid":false,"drawingGuidesOn":true,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"maxWidth":5000,"maxHeight":5000,"themeData":null,"viewportType":"default","fitBB":{"min":{"x":-17.000680271168676,"y":7},"max":{"x":892.767693574114,"y":447}},"objects":[{"x":17.5,"y":205.5,"rotation":0.0,"id":167,"width":238.5,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":38,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

ip -6 route add 2001:db8:1::/64 dev docker0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":231.28932188134524,"y":95.0,"rotation":0.0,"id":120,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":6,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":161,"py":0.0,"px":0.2928932188134524}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":131,"py":1.0,"px":0.7071067811865476}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[267.5,47.5],[217.9213562373095,-13.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":187.0,"y":206.5,"rotation":0.0,"id":121,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":9,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":140,"py":0.9999999999999998,"px":0.29289321881345254}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":148,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[130.28932188134524,11.0],[-79.0,91.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":174.0,"y":217.5,"rotation":0.0,"id":122,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":8,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":140,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":146,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[164.0,0.0],[120.0,81.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":33.50000000000003,"y":409.0,"rotation":0.0,"id":123,"width":346.49999999999994,"height":16.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":31,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

ip -6 route add default via fe80::1 dev eth0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":3.5000000000000284,"y":268.5,"rotation":0.0,"id":124,"width":411.00000000000006,"height":163.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":3,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]},{"x":237.0,"y":54.0,"rotation":0.0,"id":125,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":7,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":131,"py":0.9999999999999998,"px":0.29289321881345254}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":140,"py":0.0,"px":0.7071067811865476}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[170.78932188134524,27.999999999999986],[121.71067811865476,88.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":378.5,"y":7.0,"rotation":0.0,"id":131,"width":100.0,"height":75.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":10,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#e2e2e2","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":132,"width":96.0,"height":13.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Layer 2 Switch

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":785.0,"y":195.0,"rotation":0.0,"id":136,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":32,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":143,"py":0.6187943262411347,"px":1.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":"8.0,8.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[78.75000000000011,-0.25],[-798.0006802711687,-3.410605131648481E-13]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":262.0,"y":224.0,"rotation":0.0,"id":138,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":19,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

docker0 fe80::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":278.0,"y":126.0,"rotation":0.0,"id":139,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":16,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:0::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":288.0,"y":142.5,"rotation":0.0,"id":140,"width":100.0,"height":75.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":12,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#a4c2f4","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":141,"width":96.0,"height":13.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host1

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":3.4999999999999716,"y":107.5,"rotation":0.0,"id":142,"width":411.0,"height":141.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":1,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]},{"x":221.0,"y":283.0,"rotation":0.0,"id":144,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":34,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:1::2/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":34.000000000000014,"y":283.0,"rotation":0.0,"id":145,"width":149.99999999999997,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":24,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:1::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":244.0,"y":299.0,"rotation":0.0,"id":146,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.square","order":22,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ead1dc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":147,"width":96.0,"height":13.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container1-2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":58.0,"y":298.0,"rotation":0.0,"id":148,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.square","order":20,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ead1dc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":149,"width":96.0,"height":13.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container1-1

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":317.0,"y":436.5,"rotation":0.0,"id":158,"width":223.00000000000003,"height":11.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":37,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

containers' link-local addresses are not displayed

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":17.5,"y":148.0,"rotation":0.0,"id":137,"width":291.0,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":29,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

ip -6 route add 2001:db8:0::/64 dev eth0

ip -6 route add 2001:db8:2::/64 via 2001:db8:0::2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":901.7500000000001,"y":195.0,"rotation":0.0,"id":172,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":43,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-12.982306425886122,0.0],[-41.25,0.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":670.0,"y":284.0,"rotation":0.0,"id":155,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":36,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:2::2/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":479.0,"y":284.0,"rotation":0.0,"id":150,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":35,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:2::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":488.75,"y":408.0,"rotation":0.0,"id":152,"width":339.75,"height":16.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":30,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

ip -6 route add default via fe80::1 dev eth0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":694.5,"y":298.0,"rotation":0.0,"id":156,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.square","order":27,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ead1dc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":157,"width":96.0,"height":13.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container2-2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":501.5,"y":298.0,"rotation":0.0,"id":153,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.square","order":25,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ead1dc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":154,"width":96.0,"height":13.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container2-1

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":444.5,"y":223.0,"rotation":0.0,"id":160,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":18,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

docker0 fe80::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":460.5,"y":128.0,"rotation":0.0,"id":159,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":17,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:0::2/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":469.5,"y":142.5,"rotation":0.0,"id":161,"width":100.0,"height":75.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":14,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#a4c2f4","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":162,"width":96.0,"height":13.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":139.5,"y":86.5,"rotation":0.0,"id":126,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":5,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":161,"py":1.0,"px":0.7071067811865476}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":156,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[400.71067811865476,131.0],[605.0,211.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":100.5,"y":90.5,"rotation":0.0,"id":127,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":4,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":161,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":153,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[419.0,127.0],[451.0,207.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":447.75,"y":268.5,"rotation":0.0,"id":151,"width":416.0000000000001,"height":163.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":2,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]},{"x":447.75,"y":107.5,"rotation":0.0,"id":143,"width":416.0000000000001,"height":141.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":0,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]},{"x":795.7500000000001,"y":307.5,"rotation":270.0,"id":173,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":41,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

managed by Docker

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":879.7500000000001,"y":417.0,"rotation":0.0,"id":174,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":40,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":2,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[0.0,14.008510484195028],[0.0,-221.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":898.7500000000001,"y":432.0,"rotation":0.0,"id":171,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":42,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-13.981657549458532,0.0],[-41.25,0.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":582.5,"y":151.0,"rotation":0.0,"id":135,"width":285.25000000000017,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":33,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

ip -6 route add 2001:db8:0::/64 dev eth0

ip -6 route add 2001:db8:1::/64 via 2001:db8:0::1 

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":583.0,"y":204.0,"rotation":0.0,"id":168,"width":272.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":39,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

ip -6 route add 2001:db8:2::/64 dev docker0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]}],"shapeStyles":{"com.gliffy.shape.basic.basic_v1.default":{"fill":"#e2e2e2","stroke":"#333333","strokeWidth":2,"dashStyle":"2.0,2.0","gradient":true,"shadow":true}},"lineStyles":{"global":{"stroke":"#000000","strokeWidth":1,"dashStyle":"8.0,8.0"}},"textStyles":{}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":["com.gliffy.libraries.basic.basic_v1.default","com.gliffy.libraries.flowchart.flowchart_v1.default","com.gliffy.libraries.swimlanes.swimlanes_v1.default","com.gliffy.libraries.uml.uml_v2.class","com.gliffy.libraries.uml.uml_v2.sequence","com.gliffy.libraries.uml.uml_v2.activity","com.gliffy.libraries.erd.erd_v1.default","com.gliffy.libraries.ui.ui_v3.containers_content","com.gliffy.libraries.ui.ui_v3.forms_controls","com.gliffy.libraries.images"],"autosaveDisabled":false},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/docs/userguide/networking/default_network/images/ipv6_routed_network_example.svg b/docs/userguide/networking/default_network/images/ipv6_routed_network_example.svg new file mode 100644 index 00000000..c97b02c2 --- /dev/null +++ b/docs/userguide/networking/default_network/images/ipv6_routed_network_example.svg @@ -0,0 +1 @@ +Layer 2 SwitchHost1Host2eth0 2001:db8:0::1/64eth0 2001:db8:0::2/64docker0 fe80::1/64docker0 fe80::1/64Container1-1Container1-2eth0 2001:db8:1::1/64Container2-1Container2-2ip -6 route add 2001:db8:0::/64 dev eth0ip -6 route add 2001:db8:2::/64 via 2001:db8:0::2ip -6 route add default via fe80::1 dev eth0ip -6 route add default via fe80::1 dev eth0ip -6 route add 2001:db8:0::/64 dev eth0ip -6 route add 2001:db8:1::/64 via 2001:db8:0::1 eth0 2001:db8:1::2/64eth0 2001:db8:2::1/64eth0 2001:db8:2::2/64containers' link-local addresses are not displayedip -6 route add 2001:db8:1::/64 dev docker0ip -6 route add 2001:db8:2::/64 dev docker0managed by Docker \ No newline at end of file diff --git a/docs/userguide/networking/default_network/images/ipv6_slash64_subnet_config.gliffy b/docs/userguide/networking/default_network/images/ipv6_slash64_subnet_config.gliffy new file mode 100644 index 00000000..6914fd07 --- /dev/null +++ b/docs/userguide/networking/default_network/images/ipv6_slash64_subnet_config.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#FFFFFF","width":550,"height":341,"nodeIndex":88,"autoFit":true,"exportBorder":false,"gridOn":false,"snapToGrid":false,"drawingGuidesOn":false,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"maxWidth":5000,"maxHeight":5000,"themeData":null,"viewportType":"default","fitBB":{"min":{"x":2.5,"y":2.5},"max":{"x":550,"y":341}},"objects":[{"x":10.5,"y":53.5,"rotation":0.0,"id":74,"width":150.0,"height":16.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":26,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

fe80::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":37.0,"y":2.5,"rotation":0.0,"id":72,"width":100.0,"height":46.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":24,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#d9d9d9","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":73,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Router

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":89.5,"y":83.5,"rotation":0.0,"id":59,"width":150.0,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":17,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Routed Network:
2001:db8:23:42::/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":313.0,"y":314.0,"rotation":0.0,"id":39,"width":235.0,"height":16.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":16,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

ip -6 route add default via fe80::1 dev eth0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":352.0,"y":185.5,"rotation":0.0,"id":36,"width":169.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":15,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:23:42:1::2/80

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":351.0,"y":49.5,"rotation":0.0,"id":29,"width":171.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":14,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:23:42:1::1/80

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":382.1250000000001,"y":202.5,"rotation":0.0,"id":30,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.square","order":12,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ead1dc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":31,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

container1-2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":382.0,"y":65.5,"rotation":0.0,"id":32,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.square","order":10,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ead1dc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":33,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

container1-1

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":15.125000000000057,"y":264.0,"rotation":0.0,"id":20,"width":273.0,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":9,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

ip -6 route add default via fe80::1 dev eth0

ip -6 route add 2001:db8:23:42:1::/80 dev docker0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":120.0,"y":178.5,"rotation":0.0,"id":21,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":8,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

docker0 fe80::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":13.0,"y":132.5,"rotation":0.0,"id":22,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":7,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:23:42::1/80

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":38.0,"y":149.0,"rotation":0.0,"id":23,"width":100.0,"height":75.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":5,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#a4c2f4","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":24,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

host1

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":-118.0,"y":123.0,"rotation":0.0,"id":44,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":4,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":23,"py":0.7071067811865475,"px":0.9999999999999998}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":30,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[255.99999999999997,79.03300858899107],[500.1250000000001,129.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":-138.0,"y":129.0,"rotation":0.0,"id":43,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":3,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":23,"py":0.29289321881345237,"px":1.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":32,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[276.0,41.966991411008934],[520.0,-13.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":313.0,"y":40.0,"rotation":0.0,"id":34,"width":237.00000000000003,"height":301.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":2,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]},{"x":87.0,"y":150.0,"rotation":0.0,"id":58,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":1,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":23,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":72,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.0,-1.0],[0.0,-101.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":2.5,"y":118.50000000000001,"rotation":0.0,"id":25,"width":292.0,"height":178.99999999999997,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":0,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]}],"shapeStyles":{},"lineStyles":{"global":{"stroke":"#cccccc"}},"textStyles":{"global":{"bold":true,"italic":true}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":["com.gliffy.libraries.basic.basic_v1.default","com.gliffy.libraries.flowchart.flowchart_v1.default","com.gliffy.libraries.swimlanes.swimlanes_v1.default","com.gliffy.libraries.uml.uml_v1.default","com.gliffy.libraries.erd.erd_v1.default","com.gliffy.libraries.ui.ui_v2.forms_components","com.gliffy.libraries.network.network_v3.home","com.gliffy.libraries.images"],"autosaveDisabled":false},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/docs/userguide/networking/default_network/images/ipv6_slash64_subnet_config.svg b/docs/userguide/networking/default_network/images/ipv6_slash64_subnet_config.svg new file mode 100644 index 00000000..70b140e2 --- /dev/null +++ b/docs/userguide/networking/default_network/images/ipv6_slash64_subnet_config.svg @@ -0,0 +1 @@ +host1eth02001:db8:23:42::1/80docker0fe80::1/64ip -6routeadddefaultviafe80::1deveth0ip-6routeadd2001:db8:23:42:1::/80devdocker0container1-1container1-2eth02001:db8:23:42:1::1/80eth02001:db8:23:42:1::2/80ip-6routeadddefaultviafe80::1deveth0RoutedNetwork:2001:db8:23:42::/64Routerfe80::1/64 \ No newline at end of file diff --git a/docs/userguide/networking/default_network/images/ipv6_switched_network_example.gliffy b/docs/userguide/networking/default_network/images/ipv6_switched_network_example.gliffy new file mode 100644 index 00000000..75cbfcaa --- /dev/null +++ b/docs/userguide/networking/default_network/images/ipv6_switched_network_example.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#FFFFFF","width":893,"height":448,"nodeIndex":185,"autoFit":true,"exportBorder":false,"gridOn":false,"snapToGrid":false,"drawingGuidesOn":true,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"maxWidth":5000,"maxHeight":5000,"themeData":null,"viewportType":"default","fitBB":{"min":{"x":-17.000680271168676,"y":7},"max":{"x":892.767693574114,"y":447.5}},"objects":[{"x":17.5,"y":205.5,"rotation":0.0,"id":167,"width":238.5,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":38,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

ip -6 route add 2001:db8:1::/64 dev docker0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":231.28932188134524,"y":95.0,"rotation":0.0,"id":120,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":6,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":161,"py":0.0,"px":0.2928932188134524}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":131,"py":1.0,"px":0.7071067811865476}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[267.5,47.5],[217.9213562373095,-13.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":187.0,"y":206.5,"rotation":0.0,"id":121,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":9,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":140,"py":0.9999999999999998,"px":0.29289321881345254}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":148,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[130.28932188134524,11.0],[-79.0,91.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":174.0,"y":217.5,"rotation":0.0,"id":122,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":8,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":140,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":146,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[164.0,0.0],[120.0,81.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":33.50000000000003,"y":409.0,"rotation":0.0,"id":123,"width":346.49999999999994,"height":16.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":31,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

ip -6 route add default via fe80::1 dev eth0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":3.5000000000000284,"y":268.5,"rotation":0.0,"id":124,"width":411.00000000000006,"height":163.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":3,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]},{"x":237.0,"y":54.0,"rotation":0.0,"id":125,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":7,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":131,"py":0.9999999999999998,"px":0.29289321881345254}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":140,"py":0.0,"px":0.7071067811865476}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[170.78932188134524,27.999999999999986],[121.71067811865476,88.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":378.5,"y":7.0,"rotation":0.0,"id":131,"width":100.0,"height":75.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":10,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#e2e2e2","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":132,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Level 2 Switch

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":785.0,"y":195.0,"rotation":0.0,"id":136,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":32,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":143,"py":0.6187943262411347,"px":1.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":"8.0,8.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[78.75000000000011,-0.25],[-798.0006802711687,-3.410605131648481E-13]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":262.0,"y":224.0,"rotation":0.0,"id":138,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":19,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

docker0 fe80::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":278.0,"y":126.0,"rotation":0.0,"id":139,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":16,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:0::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":288.0,"y":142.5,"rotation":0.0,"id":140,"width":100.0,"height":75.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":12,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#a4c2f4","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":141,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host1

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":3.4999999999999716,"y":107.5,"rotation":0.0,"id":142,"width":411.0,"height":141.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":1,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]},{"x":221.0,"y":283.0,"rotation":0.0,"id":144,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":34,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:1::2/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":34.000000000000014,"y":283.0,"rotation":0.0,"id":145,"width":149.99999999999997,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":24,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:1::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":244.0,"y":299.0,"rotation":0.0,"id":146,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.square","order":22,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ead1dc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":147,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container1-2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":58.0,"y":298.0,"rotation":0.0,"id":148,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.square","order":20,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ead1dc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":149,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container1-1

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":317.0,"y":436.5,"rotation":0.0,"id":158,"width":223.00000000000003,"height":11.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":37,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

containers' link-local addresses are not displayed

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":17.5,"y":148.0,"rotation":0.0,"id":137,"width":291.0,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":29,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

ip -6 route add 2001:db8:0::/64 dev eth0

ip -6 route add 2001:db8:2::/64 via 2001:db8:0::2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":901.7500000000001,"y":195.0,"rotation":0.0,"id":172,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":43,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-12.982306425886122,0.0],[-41.25,0.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":670.0,"y":284.0,"rotation":0.0,"id":155,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":36,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:2::2/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":479.0,"y":284.0,"rotation":0.0,"id":150,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":35,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:2::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":488.75,"y":408.0,"rotation":0.0,"id":152,"width":339.75,"height":16.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":30,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

ip -6 route add default via fe80::1 dev eth0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":694.5,"y":298.0,"rotation":0.0,"id":156,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.square","order":27,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ead1dc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":157,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container2-2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":501.5,"y":298.0,"rotation":0.0,"id":153,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.square","order":25,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ead1dc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":154,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container2-1

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":444.5,"y":223.0,"rotation":0.0,"id":160,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":18,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

docker0 fe80::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":460.5,"y":128.0,"rotation":0.0,"id":159,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":17,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:0::2/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":469.5,"y":142.5,"rotation":0.0,"id":161,"width":100.0,"height":75.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":14,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#a4c2f4","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":162,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":139.5,"y":86.5,"rotation":0.0,"id":126,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":5,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":161,"py":1.0,"px":0.7071067811865476}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":156,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[400.71067811865476,131.0],[605.0,211.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":100.5,"y":90.5,"rotation":0.0,"id":127,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":4,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":161,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":153,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[419.0,127.0],[451.0,207.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":447.75,"y":268.5,"rotation":0.0,"id":151,"width":416.0000000000001,"height":163.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":2,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]},{"x":447.75,"y":107.5,"rotation":0.0,"id":143,"width":416.0000000000001,"height":141.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":0,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]},{"x":795.7500000000001,"y":307.5,"rotation":270.0,"id":173,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":41,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

managed by Docker

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":879.7500000000001,"y":417.0,"rotation":0.0,"id":174,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":40,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":2,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[0.0,14.008510484195028],[0.0,-221.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":898.7500000000001,"y":432.0,"rotation":0.0,"id":171,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":42,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-13.981657549458532,0.0],[-41.25,0.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":582.5,"y":151.0,"rotation":0.0,"id":135,"width":285.25000000000017,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":33,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

ip -6 route add 2001:db8:0::/64 dev eth0

ip -6 route add 2001:db8:1::/64 via 2001:db8:0::1 

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":583.0,"y":204.0,"rotation":0.0,"id":168,"width":272.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":39,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

ip -6 route add 2001:db8:2::/64 dev docker0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]}],"shapeStyles":{"com.gliffy.shape.basic.basic_v1.default":{"fill":"#e2e2e2","stroke":"#333333","strokeWidth":2,"dashStyle":"2.0,2.0","gradient":true,"shadow":true}},"lineStyles":{"global":{"stroke":"#000000","strokeWidth":1,"dashStyle":"8.0,8.0"}},"textStyles":{}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":["com.gliffy.libraries.basic.basic_v1.default","com.gliffy.libraries.flowchart.flowchart_v1.default","com.gliffy.libraries.swimlanes.swimlanes_v1.default","com.gliffy.libraries.uml.uml_v2.class","com.gliffy.libraries.uml.uml_v2.sequence","com.gliffy.libraries.uml.uml_v2.activity","com.gliffy.libraries.erd.erd_v1.default","com.gliffy.libraries.ui.ui_v3.containers_content","com.gliffy.libraries.ui.ui_v3.forms_controls","com.gliffy.libraries.images"],"autosaveDisabled":false},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/docs/userguide/networking/default_network/images/ipv6_switched_network_example.svg b/docs/userguide/networking/default_network/images/ipv6_switched_network_example.svg new file mode 100644 index 00000000..4cbf709b --- /dev/null +++ b/docs/userguide/networking/default_network/images/ipv6_switched_network_example.svg @@ -0,0 +1 @@ +Level2SwitchHost1Host2eth02001:db8:0::1/64eth02001:db8:0::2/64docker0fe80::1/64docker0fe80::1/64Container1-1Container1-2eth02001:db8:1::1/64Container2-1Container2-2ip -6routeadd2001:db8:0::/64deveth0ip -6routeadd2001:db8:2::/64via2001:db8:0::2ip-6routeadddefaultviafe80::1deveth0ip-6routeadddefaultviafe80::1deveth0ip -6routeadd2001:db8:0::/64deveth0ip -6routeadd2001:db8:1::/64via2001:db8:0::1eth02001:db8:1::2/64eth02001:db8:2::1/64eth02001:db8:2::2/64containers'link-localaddressesarenotdisplayedip -6routeadd2001:db8:1::/64devdocker0ip -6routeadd2001:db8:2::/64devdocker0managedbyDocker \ No newline at end of file diff --git a/docs/userguide/networking/default_network/index.md b/docs/userguide/networking/default_network/index.md new file mode 100644 index 00000000..b72d1b49 --- /dev/null +++ b/docs/userguide/networking/default_network/index.md @@ -0,0 +1,25 @@ + + +# Docker default bridge network + +With the introduction of the Docker networks feature, you can create your own +user-defined networks. The Docker default bridge is created when you install +Docker Engine. It is a `bridge` network and is also named `bridge`. The topics +in this section are related to interacting with that default bridge network. + +- [Understand container communication](container-communication.md) +- [Legacy container links](dockerlinks.md) +- [Binding container ports to the host](binding.md) +- [Build your own bridge](build-bridges.md) +- [Configure container DNS](configure-dns.md) +- [Customize the docker0 bridge](custom-docker0.md) +- [IPv6 with Docker](ipv6.md) diff --git a/docs/userguide/networking/default_network/ipv6.md b/docs/userguide/networking/default_network/ipv6.md new file mode 100644 index 00000000..ec487190 --- /dev/null +++ b/docs/userguide/networking/default_network/ipv6.md @@ -0,0 +1,259 @@ + + +# IPv6 with Docker + +The information in this section explains IPv6 with the Docker default bridge. +This is a `bridge` network named `bridge` created automatically when you install +Docker. + +As we are [running out of IPv4 +addresses](http://en.wikipedia.org/wiki/IPv4_address_exhaustion) the IETF has +standardized an IPv4 successor, [Internet Protocol Version +6](http://en.wikipedia.org/wiki/IPv6) , in [RFC +2460](https://www.ietf.org/rfc/rfc2460.txt). Both protocols, IPv4 and IPv6, +reside on layer 3 of the [OSI model](http://en.wikipedia.org/wiki/OSI_model). + +## How IPv6 works on Docker + +By default, the Docker server configures the container network for IPv4 only. +You can enable IPv4/IPv6 dualstack support by running the Docker daemon with the +`--ipv6` flag. Docker will set up the bridge `docker0` with the IPv6 [link-local +address](http://en.wikipedia.org/wiki/Link-local_address) `fe80::1`. + +By default, containers that are created will only get a link-local IPv6 address. +To assign globally routable IPv6 addresses to your containers you have to +specify an IPv6 subnet to pick the addresses from. Set the IPv6 subnet via the +`--fixed-cidr-v6` parameter when starting Docker daemon: + +``` +docker daemon --ipv6 --fixed-cidr-v6="2001:db8:1::/64" +``` + +The subnet for Docker containers should at least have a size of `/80`. This way +an IPv6 address can end with the container's MAC address and you prevent NDP +neighbor cache invalidation issues in the Docker layer. + +With the `--fixed-cidr-v6` parameter set Docker will add a new route to the +routing table. Further IPv6 routing will be enabled (you may prevent this by +starting Docker daemon with `--ip-forward=false`): + +``` +$ ip -6 route add 2001:db8:1::/64 dev docker0 +$ sysctl net.ipv6.conf.default.forwarding=1 +$ sysctl net.ipv6.conf.all.forwarding=1 +``` + +All traffic to the subnet `2001:db8:1::/64` will now be routed via the `docker0` interface. + +Be aware that IPv6 forwarding may interfere with your existing IPv6 +configuration: If you are using Router Advertisements to get IPv6 settings for +your host's interfaces you should set `accept_ra` to `2`. Otherwise IPv6 enabled +forwarding will result in rejecting Router Advertisements. E.g., if you want to +configure `eth0` via Router Advertisements you should set: + +``` +$ sysctl net.ipv6.conf.eth0.accept_ra=2 +``` + +![](images/ipv6_basic_host_config.svg) + +Every new container will get an IPv6 address from the defined subnet. Further a +default route will be added on `eth0` in the container via the address specified +by the daemon option `--default-gateway-v6` if present, otherwise via `fe80::1`: +``` +docker run -it ubuntu bash -c "ip -6 addr show dev eth0; ip -6 route show" + +15: eth0: mtu 1500 + inet6 2001:db8:1:0:0:242:ac11:3/64 scope global + valid_lft forever preferred_lft forever + inet6 fe80::42:acff:fe11:3/64 scope link + valid_lft forever preferred_lft forever + +2001:db8:1::/64 dev eth0 proto kernel metric 256 +fe80::/64 dev eth0 proto kernel metric 256 +default via fe80::1 dev eth0 metric 1024 +``` + +In this example the Docker container is assigned a link-local address with the +network suffix `/64` (here: `fe80::42:acff:fe11:3/64`) and a globally routable +IPv6 address (here: `2001:db8:1:0:0:242:ac11:3/64`). The container will create +connections to addresses outside of the `2001:db8:1::/64` network via the +link-local gateway at `fe80::1` on `eth0`. + +Often servers or virtual machines get a `/64` IPv6 subnet assigned (e.g. +`2001:db8:23:42::/64`). In this case you can split it up further and provide +Docker a `/80` subnet while using a separate `/80` subnet for other applications +on the host: + +![](images/ipv6_slash64_subnet_config.svg) + +In this setup the subnet `2001:db8:23:42::/80` with a range from +`2001:db8:23:42:0:0:0:0` to `2001:db8:23:42:0:ffff:ffff:ffff` is attached to +`eth0`, with the host listening at `2001:db8:23:42::1`. The subnet +`2001:db8:23:42:1::/80` with an address range from `2001:db8:23:42:1:0:0:0` to +`2001:db8:23:42:1:ffff:ffff:ffff` is attached to `docker0` and will be used by +containers. + +### Using NDP proxying + +If your Docker host is only part of an IPv6 subnet but has not got an IPv6 +subnet assigned you can use NDP proxying to connect your containers via IPv6 to +the internet. For example your host has the IPv6 address `2001:db8::c001`, is +part of the subnet `2001:db8::/64` and your IaaS provider allows you to +configure the IPv6 addresses `2001:db8::c000` to `2001:db8::c00f`: + +``` +$ ip -6 addr show +1: lo: mtu 65536 + inet6 ::1/128 scope host + valid_lft forever preferred_lft forever +2: eth0: mtu 1500 qlen 1000 + inet6 2001:db8::c001/64 scope global + valid_lft forever preferred_lft forever + inet6 fe80::601:3fff:fea1:9c01/64 scope link + valid_lft forever preferred_lft forever +``` + +Let's split up the configurable address range into two subnets +`2001:db8::c000/125` and `2001:db8::c008/125`. The first one can be used by the +host itself, the latter by Docker: + +``` +docker daemon --ipv6 --fixed-cidr-v6 2001:db8::c008/125 +``` + +You notice the Docker subnet is within the subnet managed by your router that is +connected to `eth0`. This means all devices (containers) with the addresses from +the Docker subnet are expected to be found within the router subnet. Therefore +the router thinks it can talk to these containers directly. + +![](images/ipv6_ndp_proxying.svg) + +As soon as the router wants to send an IPv6 packet to the first container it +will transmit a neighbor solicitation request, asking, who has `2001:db8::c009`? +But it will get no answer because no one on this subnet has this address. The +container with this address is hidden behind the Docker host. The Docker host +has to listen to neighbor solicitation requests for the container address and +send a response that itself is the device that is responsible for the address. +This is done by a Kernel feature called `NDP Proxy`. You can enable it by +executing + +``` +$ sysctl net.ipv6.conf.eth0.proxy_ndp=1 +``` + +Now you can add the container's IPv6 address to the NDP proxy table: + +``` +$ ip -6 neigh add proxy 2001:db8::c009 dev eth0 +``` + +This command tells the Kernel to answer to incoming neighbor solicitation +requests regarding the IPv6 address `2001:db8::c009` on the device `eth0`. As a +consequence of this all traffic to this IPv6 address will go into the Docker +host and it will forward it according to its routing table via the `docker0` +device to the container network: + +``` +$ ip -6 route show +2001:db8::c008/125 dev docker0 metric 1 +2001:db8::/64 dev eth0 proto kernel metric 256 +``` + +You have to execute the `ip -6 neigh add proxy ...` command for every IPv6 +address in your Docker subnet. Unfortunately there is no functionality for +adding a whole subnet by executing one command. An alternative approach would be +to use an NDP proxy daemon such as +[ndppd](https://github.com/DanielAdolfsson/ndppd). + +## Docker IPv6 cluster + +### Switched network environment +Using routable IPv6 addresses allows you to realize communication between +containers on different hosts. Let's have a look at a simple Docker IPv6 cluster +example: + +![](images/ipv6_switched_network_example.svg) + +The Docker hosts are in the `2001:db8:0::/64` subnet. Host1 is configured to +provide addresses from the `2001:db8:1::/64` subnet to its containers. It has +three routes configured: + +- Route all traffic to `2001:db8:0::/64` via `eth0` +- Route all traffic to `2001:db8:1::/64` via `docker0` +- Route all traffic to `2001:db8:2::/64` via Host2 with IP `2001:db8::2` + +Host1 also acts as a router on OSI layer 3. When one of the network clients +tries to contact a target that is specified in Host1's routing table Host1 will +forward the traffic accordingly. It acts as a router for all networks it knows: +`2001:db8::/64`, `2001:db8:1::/64` and `2001:db8:2::/64`. + +On Host2 we have nearly the same configuration. Host2's containers will get IPv6 +addresses from `2001:db8:2::/64`. Host2 has three routes configured: + +- Route all traffic to `2001:db8:0::/64` via `eth0` +- Route all traffic to `2001:db8:2::/64` via `docker0` +- Route all traffic to `2001:db8:1::/64` via Host1 with IP `2001:db8:0::1` + +The difference to Host1 is that the network `2001:db8:2::/64` is directly +attached to the host via its `docker0` interface whereas it reaches +`2001:db8:1::/64` via Host1's IPv6 address `2001:db8::1`. + +This way every container is able to contact every other container. The +containers `Container1-*` share the same subnet and contact each other directly. +The traffic between `Container1-*` and `Container2-*` will be routed via Host1 +and Host2 because those containers do not share the same subnet. + +In a switched environment every host has to know all routes to every subnet. +You always have to update the hosts' routing tables once you add or remove a +host to the cluster. + +Every configuration in the diagram that is shown below the dashed line is +handled by Docker: The `docker0` bridge IP address configuration, the route to +the Docker subnet on the host, the container IP addresses and the routes on the +containers. The configuration above the line is up to the user and can be +adapted to the individual environment. + +### Routed network environment +In a routed network environment you replace the layer 2 switch with a layer 3 +router. Now the hosts just have to know their default gateway (the router) and +the route to their own containers (managed by Docker). The router holds all +routing information about the Docker subnets. When you add or remove a host to +this environment you just have to update the routing table in the router - not +on every host. + +![](images/ipv6_routed_network_example.svg) + +In this scenario containers of the same host can communicate directly with each +other. The traffic between containers on different hosts will be routed via +their hosts and the router. For example packet from `Container1-1` to +`Container2-1` will be routed through `Host1`, `Router` and `Host2` until it +arrives at `Container2-1`. + +To keep the IPv6 addresses short in this example a `/48` network is assigned to +every host. The hosts use a `/64` subnet of this for its own services and one +for Docker. When adding a third host you would add a route for the subnet +`2001:db8:3::/48` in the router and configure Docker on Host3 with +`--fixed-cidr-v6=2001:db8:3:1::/64`. + +Remember the subnet for Docker containers should at least have a size of `/80`. +This way an IPv6 address can end with the container's MAC address and you +prevent NDP neighbor cache invalidation issues in the Docker layer. So if you +have a `/64` for your whole environment use `/78` subnets for the hosts and +`/80` for the containers. This way you can use 4096 hosts with 16 `/80` subnets +each. + +Every configuration in the diagram that is visualized below the dashed line is +handled by Docker: The `docker0` bridge IP address configuration, the route to +the Docker subnet on the host, the container IP addresses and the routes on the +containers. The configuration above the line is up to the user and can be +adapted to the individual environment. diff --git a/docs/userguide/networking/dockernetworks.md b/docs/userguide/networking/dockernetworks.md new file mode 100644 index 00000000..1848e7a7 --- /dev/null +++ b/docs/userguide/networking/dockernetworks.md @@ -0,0 +1,523 @@ + + +# Understand Docker container networks + +To build web applications that act in concert but do so securely, use the Docker +networks feature. Networks, by definition, provide complete isolation for +containers. So, it is important to have control over the networks your +applications run on. Docker container networks give you that control. + +This section provides an overview of the default networking behavior that Docker +Engine delivers natively. It describes the type of networks created by default +and how to create your own, user--defined networks. It also describes the +resources required to create networks on a single host or across a cluster of +hosts. + +## Default Networks + +When you install Docker, it creates three networks automatically. You can list +these networks using the `docker network ls` command: + +``` +$ docker network ls +NETWORK ID NAME DRIVER +7fca4eb8c647 bridge bridge +9f904ee27bf5 none null +cf03ee007fb4 host host +``` + +Historically, these three networks are part of Docker's implementation. When +you run a container you can use the `--net` flag to specify which network you +want to run a container on. These three networks are still available to you. + +The `bridge` network represents the `docker0` network present in all Docker +installations. Unless you specify otherwise with the `docker run +--net=` option, the Docker daemon connects containers to this network +by default. You can see this bridge as part of a host's network stack by using +the `ifconfig` command on the host. + +``` +$ ifconfig +docker0 Link encap:Ethernet HWaddr 02:42:47:bc:3a:eb + inet addr:172.17.0.1 Bcast:0.0.0.0 Mask:255.255.0.0 + inet6 addr: fe80::42:47ff:febc:3aeb/64 Scope:Link + UP BROADCAST RUNNING MULTICAST MTU:9001 Metric:1 + RX packets:17 errors:0 dropped:0 overruns:0 frame:0 + TX packets:8 errors:0 dropped:0 overruns:0 carrier:0 + collisions:0 txqueuelen:0 + RX bytes:1100 (1.1 KB) TX bytes:648 (648.0 B) +``` + +The `none` network adds a container to a container-specific network stack. That container lacks a network interface. Attaching to such a container and looking at it's stack you see this: + +``` +$ docker attach nonenetcontainer + +root@0cb243cd1293:/# cat /etc/hosts +127.0.0.1 localhost +::1 localhost ip6-localhost ip6-loopback +fe00::0 ip6-localnet +ff00::0 ip6-mcastprefix +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters +root@0cb243cd1293:/# ifconfig +lo Link encap:Local Loopback + inet addr:127.0.0.1 Mask:255.0.0.0 + inet6 addr: ::1/128 Scope:Host + UP LOOPBACK RUNNING MTU:65536 Metric:1 + RX packets:0 errors:0 dropped:0 overruns:0 frame:0 + TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 + collisions:0 txqueuelen:0 + RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) + +root@0cb243cd1293:/# +``` +>**Note**: You can detach from the container and leave it running with `CTRL-p CTRL-q`. + +The `host` network adds a container on the hosts network stack. You'll find the +network configuration inside the container is identical to the host. + +With the exception of the `bridge` network, you really don't need to +interact with these default networks. While you can list and inspect them, you +cannot remove them. They are required by your Docker installation. However, you +can add your own user-defined networks and these you can remove when you no +longer need them. Before you learn more about creating your own networks, it is +worth looking at the default `bridge` network a bit. + + +### The default bridge network in detail +The default `bridge` network is present on all Docker hosts. The `docker network inspect` +command returns information about a network: + +``` +$ docker network inspect bridge +[ + { + "Name": "bridge", + "Id": "f7ab26d71dbd6f557852c7156ae0574bbf62c42f539b50c8ebde0f728a253b6f", + "Scope": "local", + "Driver": "bridge", + "IPAM": { + "Driver": "default", + "Config": [ + { + "Subnet": "172.17.0.1/16", + "Gateway": "172.17.0.1" + } + ] + }, + "Containers": {}, + "Options": { + "com.docker.network.bridge.default_bridge": "true", + "com.docker.network.bridge.enable_icc": "true", + "com.docker.network.bridge.enable_ip_masquerade": "true", + "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", + "com.docker.network.bridge.name": "docker0", + "com.docker.network.driver.mtu": "9001" + } + } +] +``` +The Engine automatically creates a `Subnet` and `Gateway` to the network. +The `docker run` command automatically adds new containers to this network. + +``` +$ docker run -itd --name=container1 busybox +3386a527aa08b37ea9232cbcace2d2458d49f44bb05a6b775fba7ddd40d8f92c + +$ docker run -itd --name=container2 busybox +94447ca479852d29aeddca75c28f7104df3c3196d7b6d83061879e339946805c +``` + +Inspecting the `bridge` network again after starting two containers shows both newly launched containers in the network. Their ids show up in the container + +``` +$ docker network inspect bridge +{[ + { + "Name": "bridge", + "Id": "f7ab26d71dbd6f557852c7156ae0574bbf62c42f539b50c8ebde0f728a253b6f", + "Scope": "local", + "Driver": "bridge", + "IPAM": { + "Driver": "default", + "Config": [ + { + "Subnet": "172.17.0.1/16", + "Gateway": "172.17.0.1" + } + ] + }, + "Containers": { + "3386a527aa08b37ea9232cbcace2d2458d49f44bb05a6b775fba7ddd40d8f92c": { + "EndpointID": "647c12443e91faf0fd508b6edfe59c30b642abb60dfab890b4bdccee38750bc1", + "MacAddress": "02:42:ac:11:00:02", + "IPv4Address": "172.17.0.2/16", + "IPv6Address": "" + }, + "94447ca479852d29aeddca75c28f7104df3c3196d7b6d83061879e339946805c": { + "EndpointID": "b047d090f446ac49747d3c37d63e4307be745876db7f0ceef7b311cbba615f48", + "MacAddress": "02:42:ac:11:00:03", + "IPv4Address": "172.17.0.3/16", + "IPv6Address": "" + } + }, + "Options": { + "com.docker.network.bridge.default_bridge": "true", + "com.docker.network.bridge.enable_icc": "true", + "com.docker.network.bridge.enable_ip_masquerade": "true", + "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", + "com.docker.network.bridge.name": "docker0", + "com.docker.network.driver.mtu": "9001" + } + } +] +``` + +The `docker network inspect` command above shows all the connected containers and their network resources on a given network. Containers in this default network are able to communicate with each other using IP addresses. Docker does not support automatic service discovery on the default bridge network. If you want to communicate with container names in this default bridge network, you must connect the containers via the legacy `docker run --link` option. + +You can `attach` to a running `container` and investigate its configuration: + +``` +$ docker attach container1 + +root@0cb243cd1293:/# ifconfig +ifconfig +eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:02 + inet addr:172.17.0.2 Bcast:0.0.0.0 Mask:255.255.0.0 + inet6 addr: fe80::42:acff:fe11:2/64 Scope:Link + UP BROADCAST RUNNING MULTICAST MTU:9001 Metric:1 + RX packets:16 errors:0 dropped:0 overruns:0 frame:0 + TX packets:8 errors:0 dropped:0 overruns:0 carrier:0 + collisions:0 txqueuelen:0 + RX bytes:1296 (1.2 KiB) TX bytes:648 (648.0 B) + +lo Link encap:Local Loopback + inet addr:127.0.0.1 Mask:255.0.0.0 + inet6 addr: ::1/128 Scope:Host + UP LOOPBACK RUNNING MTU:65536 Metric:1 + RX packets:0 errors:0 dropped:0 overruns:0 frame:0 + TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 + collisions:0 txqueuelen:0 + RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) +``` + +Then use `ping` for about 3 seconds to test the connectivity of the containers on this `bridge` network. + +``` +root@0cb243cd1293:/# ping -w3 172.17.0.3 +PING 172.17.0.3 (172.17.0.3): 56 data bytes +64 bytes from 172.17.0.3: seq=0 ttl=64 time=0.096 ms +64 bytes from 172.17.0.3: seq=1 ttl=64 time=0.080 ms +64 bytes from 172.17.0.3: seq=2 ttl=64 time=0.074 ms + +--- 172.17.0.3 ping statistics --- +3 packets transmitted, 3 packets received, 0% packet loss +round-trip min/avg/max = 0.074/0.083/0.096 ms +``` + +Finally, use the `cat` command to check the `container1` network configuration: + +``` +root@0cb243cd1293:/# cat /etc/hosts +172.17.0.2 3386a527aa08 +127.0.0.1 localhost +::1 localhost ip6-localhost ip6-loopback +fe00::0 ip6-localnet +ff00::0 ip6-mcastprefix +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters +``` +To detach from a `container1` and leave it running use `CTRL-p CTRL-q`.Then, attach to `container2` and repeat these three commands. + +``` +$ docker attach container2 + +root@0cb243cd1293:/# ifconfig +eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:03 + inet addr:172.17.0.3 Bcast:0.0.0.0 Mask:255.255.0.0 + inet6 addr: fe80::42:acff:fe11:3/64 Scope:Link + UP BROADCAST RUNNING MULTICAST MTU:9001 Metric:1 + RX packets:15 errors:0 dropped:0 overruns:0 frame:0 + TX packets:13 errors:0 dropped:0 overruns:0 carrier:0 + collisions:0 txqueuelen:0 + RX bytes:1166 (1.1 KiB) TX bytes:1026 (1.0 KiB) + +lo Link encap:Local Loopback + inet addr:127.0.0.1 Mask:255.0.0.0 + inet6 addr: ::1/128 Scope:Host + UP LOOPBACK RUNNING MTU:65536 Metric:1 + RX packets:0 errors:0 dropped:0 overruns:0 frame:0 + TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 + collisions:0 txqueuelen:0 + RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) + +root@0cb243cd1293:/# ping -w3 172.17.0.2 +PING 172.17.0.2 (172.17.0.2): 56 data bytes +64 bytes from 172.17.0.2: seq=0 ttl=64 time=0.067 ms +64 bytes from 172.17.0.2: seq=1 ttl=64 time=0.075 ms +64 bytes from 172.17.0.2: seq=2 ttl=64 time=0.072 ms + +--- 172.17.0.2 ping statistics --- +3 packets transmitted, 3 packets received, 0% packet loss +round-trip min/avg/max = 0.067/0.071/0.075 ms +/ # cat /etc/hosts +172.17.0.3 94447ca47985 +127.0.0.1 localhost +::1 localhost ip6-localhost ip6-loopback +fe00::0 ip6-localnet +ff00::0 ip6-mcastprefix +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters +``` + +The default `docker0` bridge network supports the use of port mapping and `docker run --link` to allow communications between containers in the `docker0` network. These techniques are cumbersome to set up and prone to error. While they are still available to you as techniques, it is better to avoid them and define your own bridge networks instead. + +## User-defined networks + +You can create your own user-defined networks that better isolate containers. +Docker provides some default **network drivers** for creating these +networks. You can create a new **bridge network** or **overlay network**. You +can also create a **network plugin** or **remote network** written to your own +specifications. + +You can create multiple networks. You can add containers to more than one +network. Containers can only communicate within networks but not across +networks. A container attached to two networks can communicate with member +containers in either network. + +The next few sections describe each of Docker's built-in network drivers in +greater detail. + +### A bridge network + +The easiest user-defined network to create is a `bridge` network. This network +is similar to the historical, default `docker0` network. There are some added +features and some old features that aren't available. + +``` +$ docker network create --driver bridge isolated_nw +1196a4c5af43a21ae38ef34515b6af19236a3fc48122cf585e3f3054d509679b + +$ docker network inspect isolated_nw +[ + { + "Name": "isolated_nw", + "Id": "1196a4c5af43a21ae38ef34515b6af19236a3fc48122cf585e3f3054d509679b", + "Scope": "local", + "Driver": "bridge", + "IPAM": { + "Driver": "default", + "Config": [ + { + "Subnet": "172.21.0.0/16", + "Gateway": "172.21.0.1/16" + } + ] + }, + "Containers": {}, + "Options": {} + } +] + +$ docker network ls +NETWORK ID NAME DRIVER +9f904ee27bf5 none null +cf03ee007fb4 host host +7fca4eb8c647 bridge bridge +c5ee82f76de3 isolated_nw bridge + +``` + +After you create the network, you can launch containers on it using the `docker run --net=` option. + +``` +$ docker run --net=isolated_nw -itd --name=container3 busybox +8c1a0a5be480921d669a073393ade66a3fc49933f08bcc5515b37b8144f6d47c + +$ docker network inspect isolated_nw +[ + { + "Name": "isolated_nw", + "Id": "1196a4c5af43a21ae38ef34515b6af19236a3fc48122cf585e3f3054d509679b", + "Scope": "local", + "Driver": "bridge", + "IPAM": { + "Driver": "default", + "Config": [ + {} + ] + }, + "Containers": { + "8c1a0a5be480921d669a073393ade66a3fc49933f08bcc5515b37b8144f6d47c": { + "EndpointID": "93b2db4a9b9a997beb912d28bcfc117f7b0eb924ff91d48cfa251d473e6a9b08", + "MacAddress": "02:42:ac:15:00:02", + "IPv4Address": "172.21.0.2/16", + "IPv6Address": "" + } + }, + "Options": {} + } +] +``` + +The containers you launch into this network must reside on the same Docker host. +Each container in the network can immediately communicate with other containers +in the network. Though, the network itself isolates the containers from external +networks. + +![An isolated network](images/bridge_network.png) + +Within a user-defined bridge network, linking is not supported. You can +expose and publish container ports on containers in this network. This is useful +if you want to make a portion of the `bridge` network available to an outside +network. + +![Bridge network](images/network_access.png) + +A bridge network is useful in cases where you want to run a relatively small +network on a single host. You can, however, create significantly larger networks +by creating an `overlay` network. + + +### An overlay network + +Docker's `overlay` network driver supports multi-host networking natively +out-of-the-box. This support is accomplished with the help of `libnetwork`, a +built-in VXLAN-based overlay network driver, and Docker's `libkv` library. + +The `overlay` network requires a valid key-value store service. Currently, +Docker's `libkv` supports Consul, Etcd, and ZooKeeper (Distributed store). Before +creating a network you must install and configure your chosen key-value store +service. The Docker hosts that you intend to network and the service must be +able to communicate. + +![Key-value store](images/key_value.png) + +Each host in the network must run a Docker Engine instance. The easiest way to +provision the hosts are with Docker Machine. + +![Engine on each host](images/engine_on_net.png) + +You should open the following ports between each of your hosts. + +| Protocol | Port | Description | +|----------|------|-----------------------| +| udp | 4789 | Data plane (VXLAN) | +| tcp/udp | 7946 | Control plane | + +Your key-value store service may require additional ports. +Check your vendor's documentation and open any required ports. + +Once you have several machines provisioned, you can use Docker Swarm to quickly +form them into a swarm which includes a discovery service as well. + +To create an overlay network, you configure options on the `daemon` on each +Docker Engine for use with `overlay` network. There are three options to set: + + + + + + + + + + + + + + + + + + + + + + +
OptionDescription
--cluster-store=PROVIDER://URL
Describes the location of the KV service.
--cluster-advertise=HOST_IP|HOST_IFACE:PORT
The IP address or interface of the HOST used for clustering.
--cluster-store-opt=KEY-VALUE OPTIONS
Options such as TLS certificate or tuning discovery Timers
+ +Create an `overlay` network on one of the machines in the Swarm. + + $ docker network create --driver overlay my-multi-host-network + +This results in a single network spanning multiple hosts. An `overlay` network +provides complete isolation for the containers. + +![An overlay network](images/overlay_network.png) + +Then, on each host, launch containers making sure to specify the network name. + + $ docker run -itd --net=my-multi-host-network busybox + +Once connected, each container has access to all the containers in the network +regardless of which Docker host the container was launched on. + +![Published port](images/overlay-network-final.png) + +If you would like to try this for yourself, see the [Getting started for +overlay](get-started-overlay.md). + +### Custom network plugin + +If you like, you can write your own network driver plugin. A network +driver plugin makes use of Docker's plugin infrastructure. In this +infrastructure, a plugin is a process running on the same Docker host as the +Docker `daemon`. + +Network plugins follow the same restrictions and installation rules as other +plugins. All plugins make use of the plugin API. They have a lifecycle that +encompasses installation, starting, stopping and activation. + +Once you have created and installed a custom network driver, you use it like the +built-in network drivers. For example: + + $ docker network create --driver weave mynet + +You can inspect it, add containers to and from it, and so forth. Of course, +different plugins may make use of different technologies or frameworks. Custom +networks can include features not present in Docker's default networks. For more +information on writing plugins, see [Extending Docker](../../extend/index.md) and +[Writing a network driver plugin](../../extend/plugins_network.md). + +### Docker embedded DNS server + +Docker daemon runs an embedded DNS server to provide automatic service discovery +for containers connected to user defined networks. Name resolution requests from +the containers are handled first by the embedded DNS server. If the embedded DNS +server is unable to resolve the request it will be forwarded to any external DNS +servers configured for the container. To facilitate this when the container is +created, only the embedded DNS server reachable at `127.0.0.11` will be listed +in the container's `resolv.conf` file. More information on embedded DNS server on +user-defined networks can be found in the [embedded DNS server in user-defined networks] +(configure-dns.md) + +## Links + +Before the Docker network feature, you could use the Docker link feature to +allow containers to discover each other. With the introduction of Docker networks, +containers can be discovered by its name automatically. But you can still create +links but they behave differently when used in the default `docker0` bridge network +compared to user-defined networks. For more information, please refer to +[Legacy Links](default_network/dockerlinks.md) for link feature in default `bridge` network +and the [linking containers in user-defined networks](work-with-networks.md#linking-containers-in-user-defined-networks) for links +functionality in user-defined networks. + +## Related information + +- [Work with network commands](work-with-networks.md) +- [Get started with multi-host networking](get-started-overlay.md) +- [Managing Data in Containers](../containers/dockervolumes.md) +- [Docker Machine overview](https://docs.docker.com/machine) +- [Docker Swarm overview](https://docs.docker.com/swarm) +- [Investigate the LibNetwork project](https://github.com/docker/libnetwork) diff --git a/docs/userguide/networking/get-started-overlay.md b/docs/userguide/networking/get-started-overlay.md new file mode 100644 index 00000000..89d5b2ca --- /dev/null +++ b/docs/userguide/networking/get-started-overlay.md @@ -0,0 +1,326 @@ + + +# Get started with multi-host networking + +This article uses an example to explain the basics of creating a multi-host +network. Docker Engine supports multi-host networking out-of-the-box through the +`overlay` network driver. Unlike `bridge` networks, overlay networks require +some pre-existing conditions before you can create one. These conditions are: + +* Access to a key-value store. Docker supports Consul, Etcd, and ZooKeeper (Distributed store) key-value stores. +* A cluster of hosts with connectivity to the key-value store. +* A properly configured Engine `daemon` on each host in the cluster. +* Hosts within the cluster must have unique hostnames because the key-value store uses the hostnames to identify cluster members. + +Though Docker Machine and Docker Swarm are not mandatory to experience Docker +multi-host networking, this example uses them to illustrate how they are +integrated. You'll use Machine to create both the key-value store +server and the host cluster. This example creates a Swarm cluster. + +## Prerequisites + +Before you begin, make sure you have a system on your network with the latest +version of Docker Engine and Docker Machine installed. The example also relies +on VirtualBox. If you installed on a Mac or Windows with Docker Toolbox, you +have all of these installed already. + +If you have not already done so, make sure you upgrade Docker Engine and Docker +Machine to the latest versions. + + +## Step 1: Set up a key-value store + +An overlay network requires a key-value store. The key-value store holds +information about the network state which includes discovery, networks, +endpoints, IP addresses, and more. Docker supports Consul, Etcd, and ZooKeeper +key-value stores. This example uses Consul. + +1. Log into a system prepared with the prerequisite Docker Engine, Docker Machine, and VirtualBox software. + +2. Provision a VirtualBox machine called `mh-keystore`. + + $ docker-machine create -d virtualbox mh-keystore + + When you provision a new machine, the process adds Docker Engine to the + host. This means rather than installing Consul manually, you can create an + instance using the [consul image from Docker + Hub](https://hub.docker.com/r/progrium/consul/). You'll do this in the next step. + +3. Set your local environment to the `mh-keystore` machine. + + $ eval "$(docker-machine env mh-keystore)" + +4. Start a `progrium/consul` container running on the `mh-keystore` machine. + + $ docker run -d \ + -p "8500:8500" \ + -h "consul" \ + progrium/consul -server -bootstrap + + The client starts a `progrium/consul` image running in the + `mh-keystore` machine. The server is called `consul` and is + listening on port `8500`. + +5. Run the `docker ps` command to see the `consul` container. + + $ docker ps + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 4d51392253b3 progrium/consul "/bin/start -server -" 25 minutes ago Up 25 minutes 53/tcp, 53/udp, 8300-8302/tcp, 0.0.0.0:8500->8500/tcp, 8400/tcp, 8301-8302/udp admiring_panini + +Keep your terminal open and move onto the next step. + + +## Step 2: Create a Swarm cluster + +In this step, you use `docker-machine` to provision the hosts for your network. +At this point, you won't actually create the network. You'll create several +machines in VirtualBox. One of the machines will act as the Swarm master; +you'll create that first. As you create each host, you'll pass the Engine on +that machine options that are needed by the `overlay` network driver. + +1. Create a Swarm master. + + $ docker-machine create \ + -d virtualbox \ + --swarm --swarm-master \ + --swarm-discovery="consul://$(docker-machine ip mh-keystore):8500" \ + --engine-opt="cluster-store=consul://$(docker-machine ip mh-keystore):8500" \ + --engine-opt="cluster-advertise=eth1:2376" \ + mhs-demo0 + + At creation time, you supply the Engine `daemon` with the ` --cluster-store` option. This option tells the Engine the location of the key-value store for the `overlay` network. The bash expansion `$(docker-machine ip mh-keystore)` resolves to the IP address of the Consul server you created in "STEP 1". The `--cluster-advertise` option advertises the machine on the network. + +2. Create another host and add it to the Swarm cluster. + + $ docker-machine create -d virtualbox \ + --swarm \ + --swarm-discovery="consul://$(docker-machine ip mh-keystore):8500" \ + --engine-opt="cluster-store=consul://$(docker-machine ip mh-keystore):8500" \ + --engine-opt="cluster-advertise=eth1:2376" \ + mhs-demo1 + +3. List your machines to confirm they are all up and running. + + $ docker-machine ls + NAME ACTIVE DRIVER STATE URL SWARM + default - virtualbox Running tcp://192.168.99.100:2376 + mh-keystore * virtualbox Running tcp://192.168.99.103:2376 + mhs-demo0 - virtualbox Running tcp://192.168.99.104:2376 mhs-demo0 (master) + mhs-demo1 - virtualbox Running tcp://192.168.99.105:2376 mhs-demo0 + +At this point you have a set of hosts running on your network. You are ready to create a multi-host network for containers using these hosts. + +Leave your terminal open and go onto the next step. + +## Step 3: Create the overlay Network + +To create an overlay network + +1. Set your docker environment to the Swarm master. + + $ eval $(docker-machine env --swarm mhs-demo0) + + Using the `--swarm` flag with `docker-machine` restricts the `docker` commands to Swarm information alone. + +2. Use the `docker info` command to view the Swarm. + + $ docker info + Containers: 3 + Images: 2 + Role: primary + Strategy: spread + Filters: affinity, health, constraint, port, dependency + Nodes: 2 + mhs-demo0: 192.168.99.104:2376 + └ Containers: 2 + └ Reserved CPUs: 0 / 1 + └ Reserved Memory: 0 B / 1.021 GiB + └ Labels: executiondriver=native-0.2, kernelversion=4.1.10-boot2docker, operatingsystem=Boot2Docker 1.9.0-rc1 (TCL 6.4); master : 4187d2c - Wed Oct 14 14:00:28 UTC 2015, provider=virtualbox, storagedriver=aufs + mhs-demo1: 192.168.99.105:2376 + └ Containers: 1 + └ Reserved CPUs: 0 / 1 + └ Reserved Memory: 0 B / 1.021 GiB + └ Labels: executiondriver=native-0.2, kernelversion=4.1.10-boot2docker, operatingsystem=Boot2Docker 1.9.0-rc1 (TCL 6.4); master : 4187d2c - Wed Oct 14 14:00:28 UTC 2015, provider=virtualbox, storagedriver=aufs + CPUs: 2 + Total Memory: 2.043 GiB + Name: 30438ece0915 + + From this information, you can see that you are running three containers and two images on the Master. + +3. Create your `overlay` network. + + $ docker network create --driver overlay --subnet=10.0.9.0/24 my-net + + You only need to create the network on a single host in the cluster. In this case, you used the Swarm master but you could easily have run it on any host in the cluster. + +> **Note** : It is highly recommended to use the `--subnet` option when creating +> a network. If the `--subnet` is not specified, the docker daemon automatically +> chooses and assigns a subnet for the network and it could overlap with another subnet +> in your infrastructure that is not managed by docker. Such overlaps can cause +> connectivity issues or failures when containers are connected to that network. + +4. Check that the network is running: + + $ docker network ls + NETWORK ID NAME DRIVER + 412c2496d0eb mhs-demo1/host host + dd51763e6dd2 mhs-demo0/bridge bridge + 6b07d0be843f my-net overlay + b4234109bd9b mhs-demo0/none null + 1aeead6dd890 mhs-demo0/host host + d0bb78cbe7bd mhs-demo1/bridge bridge + 1c0eb8f69ebb mhs-demo1/none null + + As you are in the Swarm master environment, you see all the networks on all + the Swarm agents: the default networks on each engine and the single overlay + network. Notice that each `NETWORK ID` is unique. + +5. Switch to each Swarm agent in turn and list the networks. + + $ eval $(docker-machine env mhs-demo0) + $ docker network ls + NETWORK ID NAME DRIVER + 6b07d0be843f my-net overlay + dd51763e6dd2 bridge bridge + b4234109bd9b none null + 1aeead6dd890 host host + $ eval $(docker-machine env mhs-demo1) + $ docker network ls + NETWORK ID NAME DRIVER + d0bb78cbe7bd bridge bridge + 1c0eb8f69ebb none null + 412c2496d0eb host host + 6b07d0be843f my-net overlay + + Both agents report they have the `my-net` network with the `6b07d0be843f` ID. + You now have a multi-host container network running! + +## Step 4: Run an application on your Network + +Once your network is created, you can start a container on any of the hosts and it automatically is part of the network. + +1. Point your environment to the Swarm master. + + $ eval $(docker-machine env --swarm mhs-demo0) + +2. Start an Nginx web server on the `mhs-demo0` instance. + + $ docker run -itd --name=web --net=my-net --env="constraint:node==mhs-demo0" nginx + +4. Run a BusyBox instance on the `mhs-demo1` instance and get the contents of the Nginx server's home page. + + $ docker run -it --rm --net=my-net --env="constraint:node==mhs-demo1" busybox wget -O- http://web + Unable to find image 'busybox:latest' locally + latest: Pulling from library/busybox + ab2b8a86ca6c: Pull complete + 2c5ac3f849df: Pull complete + Digest: sha256:5551dbdfc48d66734d0f01cafee0952cb6e8eeecd1e2492240bf2fd9640c2279 + Status: Downloaded newer image for busybox:latest + Connecting to web (10.0.0.2:80) + + + + Welcome to nginx! + + + +

Welcome to nginx!

+

If you see this page, the nginx web server is successfully installed and + working. Further configuration is required.

+ +

For online documentation and support please refer to + nginx.org.
+ Commercial support is available at + nginx.com.

+ +

Thank you for using nginx.

+ + + - 100% |*******************************| 612 0:00:00 ETA + +## Step 5: Check external connectivity + +As you've seen, Docker's built-in overlay network driver provides out-of-the-box +connectivity between the containers on multiple hosts within the same network. +Additionally, containers connected to the multi-host network are automatically +connected to the `docker_gwbridge` network. This network allows the containers +to have external connectivity outside of their cluster. + +1. Change your environment to the Swarm agent. + + $ eval $(docker-machine env mhs-demo1) + +2. View the `docker_gwbridge` network, by listing the networks. + + $ docker network ls + NETWORK ID NAME DRIVER + 6b07d0be843f my-net overlay + dd51763e6dd2 bridge bridge + b4234109bd9b none null + 1aeead6dd890 host host + e1dbd5dff8be docker_gwbridge bridge + +3. Repeat steps 1 and 2 on the Swarm master. + + $ eval $(docker-machine env mhs-demo0) + $ docker network ls + NETWORK ID NAME DRIVER + 6b07d0be843f my-net overlay + d0bb78cbe7bd bridge bridge + 1c0eb8f69ebb none null + 412c2496d0eb host host + 97102a22e8d2 docker_gwbridge bridge + +2. Check the Nginx container's network interfaces. + + $ docker exec web ip addr + 1: lo: mtu 65536 qdisc noqueue state UNKNOWN group default + link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 + inet 127.0.0.1/8 scope host lo + valid_lft forever preferred_lft forever + inet6 ::1/128 scope host + valid_lft forever preferred_lft forever + 22: eth0: mtu 1450 qdisc noqueue state UP group default + link/ether 02:42:0a:00:09:03 brd ff:ff:ff:ff:ff:ff + inet 10.0.9.3/24 scope global eth0 + valid_lft forever preferred_lft forever + inet6 fe80::42:aff:fe00:903/64 scope link + valid_lft forever preferred_lft forever + 24: eth1: mtu 1500 qdisc noqueue state UP group default + link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff + inet 172.18.0.2/16 scope global eth1 + valid_lft forever preferred_lft forever + inet6 fe80::42:acff:fe12:2/64 scope link + valid_lft forever preferred_lft forever + + The `eth0` interface represents the container interface that is connected to + the `my-net` overlay network. While the `eth1` interface represents the + container interface that is connected to the `docker_gwbridge` network. + +## Step 6: Extra Credit with Docker Compose + +Please refer to the Networking feature introduced in [Compose V2 format] +(https://docs.docker.com/compose/networking/) and execute the +multi-host networking scenario in the Swarm cluster used above. + +## Related information + +* [Understand Docker container networks](dockernetworks.md) +* [Work with network commands](work-with-networks.md) +* [Docker Swarm overview](https://docs.docker.com/swarm) +* [Docker Machine overview](https://docs.docker.com/machine) diff --git a/docs/userguide/networking/images/bridge_network.gliffy b/docs/userguide/networking/images/bridge_network.gliffy new file mode 100644 index 00000000..d113f4fb --- /dev/null +++ b/docs/userguide/networking/images/bridge_network.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#ffffff","width":378,"height":236,"nodeIndex":146,"autoFit":true,"exportBorder":false,"gridOn":true,"snapToGrid":false,"drawingGuidesOn":false,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"maxWidth":5000,"maxHeight":5000,"themeData":null,"viewportType":"default","fitBB":{"min":{"x":7,"y":5.1999969482421875},"max":{"x":378,"y":235.1428540910994}},"printModel":{"pageSize":"a4","portrait":false,"fitToOnePage":false,"displayPageBreaks":false},"objects":[{"x":196.0,"y":100.69999694824219,"rotation":0.0,"id":140,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":61,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"


","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":149.0,"y":154.96785409109907,"rotation":0.0,"id":114,"width":150.0,"height":54.732142857143145,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":16,"lockAspectRatio":false,"lockShape":false,"children":[{"x":44.0,"y":2.7321428571431454,"rotation":0.0,"id":95,"width":62.0,"height":33.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":5,"lockAspectRatio":false,"lockShape":false,"children":[{"x":29.139999999999997,"y":2.94642857142857,"rotation":0.0,"id":96,"width":3.719999999999998,"height":27.107142857142843,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":13,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":99,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":99,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.8600000000000136,-1.1785714285714448],[1.8600000000000136,28.285714285714278]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":null},{"x":51.46,"y":2.94642857142857,"rotation":0.0,"id":97,"width":1.2156862745098034,"height":28.285714285714267,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":10,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-1.4193795664340882,-1.178571428571729],[-1.4193795664340882,28.28571428571442]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":null},{"x":9.919999999999993,"y":1.3749999999999987,"rotation":0.0,"id":98,"width":1.239999999999999,"height":28.285714285714267,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":7,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[2.0393795664339223,0.3928571428572809],[2.0393795664339223,29.85714285714272]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":null},{"x":0.0,"y":1.7678571428571417,"rotation":0.0,"id":99,"width":62.0,"height":29.46428571428572,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":4,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#6fa8dc","fillColor":"#3d85c6","gradient":true,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":40.732142857143145,"rotation":0.0,"id":112,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":15,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

container2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":226.0,"y":155.96785409109907,"rotation":0.0,"id":115,"width":150.0,"height":54.732142857143145,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":34,"lockAspectRatio":false,"lockShape":false,"children":[{"x":44.0,"y":2.7321428571431454,"rotation":0.0,"id":116,"width":62.0,"height":33.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":22,"lockAspectRatio":false,"lockShape":false,"children":[{"x":29.139999999999997,"y":2.94642857142857,"rotation":0.0,"id":117,"width":3.719999999999998,"height":27.107142857142843,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":31,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":120,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":120,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.8600000000000136,-1.1785714285714448],[1.8600000000000136,28.285714285714278]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":null},{"x":51.46,"y":2.94642857142857,"rotation":0.0,"id":118,"width":1.2156862745098034,"height":28.285714285714267,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":28,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-1.4193795664340882,-1.178571428571729],[-1.4193795664340882,28.28571428571442]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":null},{"x":9.919999999999993,"y":1.3749999999999987,"rotation":0.0,"id":119,"width":1.239999999999999,"height":28.285714285714267,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":25,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[2.0393795664339223,0.3928571428572809],[2.0393795664339223,29.85714285714272]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":null},{"x":0.0,"y":1.7678571428571417,"rotation":0.0,"id":120,"width":62.0,"height":29.46428571428572,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":20,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#6fa8dc","fillColor":"#3d85c6","gradient":true,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":40.732142857143145,"rotation":0.0,"id":121,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":33,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

container3

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":72.0,"y":154.96785409109907,"rotation":0.0,"id":122,"width":150.0,"height":54.732142857143145,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":51,"lockAspectRatio":false,"lockShape":false,"children":[{"x":44.0,"y":2.7321428571431454,"rotation":0.0,"id":123,"width":62.0,"height":33.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":39,"lockAspectRatio":false,"lockShape":false,"children":[{"x":29.139999999999997,"y":2.94642857142857,"rotation":0.0,"id":124,"width":3.719999999999998,"height":27.107142857142843,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":48,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":127,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":127,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.8600000000000136,-1.1785714285714448],[1.8600000000000136,28.285714285714278]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":null},{"x":51.46,"y":2.94642857142857,"rotation":0.0,"id":125,"width":1.2156862745098034,"height":28.285714285714267,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":45,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-1.4193795664340882,-1.178571428571729],[-1.4193795664340882,28.28571428571442]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":null},{"x":9.919999999999993,"y":1.3749999999999987,"rotation":0.0,"id":126,"width":1.239999999999999,"height":28.285714285714267,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":42,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[2.0393795664339223,0.3928571428572809],[2.0393795664339223,29.85714285714272]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":null},{"x":0.0,"y":1.7678571428571417,"rotation":0.0,"id":127,"width":62.0,"height":29.46428571428572,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":37,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#6fa8dc","fillColor":"#3d85c6","gradient":true,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":40.732142857143145,"rotation":0.0,"id":128,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":50,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

container1

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":81.38636363636368,"y":79.1428540910994,"rotation":0.0,"id":129,"width":291.1363636363638,"height":156.0,"uid":"com.gliffy.shape.iphone.iphone_ios7.icons_glyphs.glyph_cloud","order":51,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.iphone.iphone_ios7.icons_glyphs.glyph_cloud","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#929292","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":157.0,"y":124.19999694824219,"rotation":0.0,"id":130,"width":150.0,"height":27.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":52,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

isolated_nw

 

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":15.0,"y":5.1999969482421875,"rotation":0.0,"id":134,"width":73.116,"height":102.32,"uid":"com.gliffy.shape.cisco.cisco_v1.servers.standard_host","order":56,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.servers.standard_host","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#3d85c6","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":53.0,"y":57.19999694824219,"rotation":0.0,"id":136,"width":119.0,"height":45.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":57,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":134,"py":0.5,"px":1.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":6.0,"strokeColor":"#999999","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[35.116,-0.8400000000000034],[89.0,-0.8400000000000034],[89.0,57.0]],"lockSegments":{},"ortho":true}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":5.0,"y":116.19999694824219,"rotation":0.0,"id":142,"width":78.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":63,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Docker Host

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"layers":[{"guid":"9wom3rMkTrb3","order":0,"name":"Layer 0","active":true,"locked":false,"visible":true,"nodeIndex":66}],"shapeStyles":{},"lineStyles":{"global":{"stroke":"#999999","strokeWidth":6}},"textStyles":{"global":{"bold":true}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":[],"lastSerialized":1445538566750},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/docs/userguide/networking/images/bridge_network.png b/docs/userguide/networking/images/bridge_network.png new file mode 100644 index 0000000000000000000000000000000000000000..314c9f7963ee006a5775a1109d8e99b172c0da91 GIT binary patch literal 15878 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 z)+O(c)p>4yK0tIb-k|gVy#o3nfrcsa#4^j5K=)QA<}J`6aVRfcK?zv8Mi2_container2container3container1isolated_nwDockerHost \ No newline at end of file diff --git a/docs/userguide/networking/images/engine_on_net.gliffy b/docs/userguide/networking/images/engine_on_net.gliffy new file mode 100644 index 00000000..2fe97eca --- /dev/null +++ b/docs/userguide/networking/images/engine_on_net.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#ffffff","width":277,"height":209,"nodeIndex":174,"autoFit":true,"exportBorder":false,"gridOn":true,"snapToGrid":false,"drawingGuidesOn":false,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"maxWidth":5000,"maxHeight":5000,"themeData":null,"viewportType":"default","fitBB":{"min":{"x":3,"y":3.1889969482422202},"max":{"x":277,"y":208.1999969482422}},"printModel":{"pageSize":"a4","portrait":false,"fitToOnePage":false,"displayPageBreaks":false},"objects":[{"x":223.0,"y":117.3854006442422,"rotation":0.0,"id":171,"width":26.70555282692303,"height":19.0,"uid":"com.gliffy.shape.basic.basic_v1.default.svg","order":21,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Svg","Svg":{"embeddedResourceId":0,"strokeWidth":2.0,"strokeColor":"#000000","dropShadow":true,"shadowX":5.0,"shadowY":5.0}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":1.0,"y":93.51999694824218,"rotation":0.0,"id":152,"width":78.0,"height":77.68,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":4,"lockAspectRatio":false,"lockShape":false,"children":[{"x":0.0,"y":63.68000000000001,"rotation":0.0,"id":142,"width":78.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":3,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":null},{"x":23.0,"y":0.0,"rotation":0.0,"id":134,"width":42.8749022673964,"height":60.0,"uid":"com.gliffy.shape.cisco.cisco_v1.servers.standard_host","order":1,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.servers.standard_host","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#3d85c6","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":96.0,"y":130.51999694824218,"rotation":0.0,"id":153,"width":78.0,"height":77.68,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":7,"lockAspectRatio":false,"lockShape":false,"children":[{"x":0.0,"y":63.68000000000001,"rotation":0.0,"id":154,"width":78.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":9,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":null},{"x":23.0,"y":0.0,"rotation":0.0,"id":155,"width":42.8749022673964,"height":60.0,"uid":"com.gliffy.shape.cisco.cisco_v1.servers.standard_host","order":6,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.servers.standard_host","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#3d85c6","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":197.0,"y":99.35999694824216,"rotation":0.0,"id":156,"width":78.0,"height":77.68,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":12,"lockAspectRatio":false,"lockShape":false,"children":[{"x":0.0,"y":63.68000000000001,"rotation":0.0,"id":157,"width":78.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":14,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":null},{"x":23.0,"y":0.0,"rotation":0.0,"id":158,"width":42.8749022673964,"height":60.0,"uid":"com.gliffy.shape.cisco.cisco_v1.servers.standard_host","order":11,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.servers.standard_host","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#3d85c6","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":114.0,"y":3.1889969482422202,"rotation":0.0,"id":160,"width":48.773475410240856,"height":39.0,"uid":"com.gliffy.shape.cisco.cisco_v1.storage.relational_database","order":15,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.storage.relational_database","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#02709F","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":163,"width":88.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Key-value store

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":171.0,"y":25.199996948242188,"rotation":0.0,"id":165,"width":72.0,"height":73.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":18,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":160,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":158,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#999999","fillColor":"none","dashStyle":"1.0,1.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-32.613262294879576,16.989000000000033],[70.4374511336982,74.15999999999997]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":141.0,"y":37.19999694824219,"rotation":0.0,"id":168,"width":4.0,"height":91.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":19,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":160,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":155,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#999999","fillColor":"none","dashStyle":"1.0,1.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-2.6132622948795756,4.989000000000033],[-0.5625488663017961,93.32]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":136.0,"y":42.19999694824219,"rotation":0.0,"id":169,"width":86.0,"height":50.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":20,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":160,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":134,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#999999","fillColor":"none","dashStyle":"1.0,1.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[2.3867377051204244,-0.010999999999967258],[-90.5625488663018,51.31999999999999]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":122.0,"y":150.3854006442422,"rotation":0.0,"id":172,"width":26.70555282692303,"height":19.0,"uid":"com.gliffy.shape.basic.basic_v1.default.svg","order":22,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Svg","Svg":{"embeddedResourceId":0,"strokeWidth":2.0,"strokeColor":"#000000","dropShadow":true,"shadowX":5.0,"shadowY":5.0}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":27.0,"y":113.3854006442422,"rotation":0.0,"id":173,"width":26.70555282692303,"height":19.0,"uid":"com.gliffy.shape.basic.basic_v1.default.svg","order":23,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Svg","Svg":{"embeddedResourceId":0,"strokeWidth":2.0,"strokeColor":"#000000","dropShadow":true,"shadowX":5.0,"shadowY":5.0}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"layers":[{"guid":"9wom3rMkTrb3","order":0,"name":"Layer 0","active":true,"locked":false,"visible":true,"nodeIndex":24}],"shapeStyles":{},"lineStyles":{"global":{"stroke":"#999999","strokeWidth":2,"dashStyle":"1.0,1.0"}},"textStyles":{"global":{"bold":true}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":["com.gliffy.custom.confluence.c20f4a380e3cee362007f9e62694d34d947f28ed4263c0702b3dd72d9801532a"],"lastSerialized":1445555725710},"embeddedResources":{"index":1,"resources":[{"id":0,"mimeType":"image/svg+xml","data":"\n\n \n logo copy\n Created with Sketch.\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n","width":59.29392246992643,"height":42.185403696,"x":0.4429050300735753,"y":0.7077644040000006}]}} \ No newline at end of file diff --git a/docs/userguide/networking/images/engine_on_net.png b/docs/userguide/networking/images/engine_on_net.png new file mode 100644 index 0000000000000000000000000000000000000000..a79aa44c8e8963fc39ca10750793c3ac26861352 GIT binary patch literal 14032 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 Uw<+&|H`S0Ja;mab((i-+8=gxczW@LL literal 0 HcmV?d00001 diff --git a/docs/userguide/networking/images/engine_on_net.svg b/docs/userguide/networking/images/engine_on_net.svg new file mode 100644 index 00000000..a7463709 --- /dev/null +++ b/docs/userguide/networking/images/engine_on_net.svg @@ -0,0 +1 @@ +HostHostHostKey-valuestore \ No newline at end of file diff --git a/docs/userguide/networking/images/key_value.gliffy b/docs/userguide/networking/images/key_value.gliffy new file mode 100644 index 00000000..4e632ef3 --- /dev/null +++ b/docs/userguide/networking/images/key_value.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#ffffff","width":277,"height":209,"nodeIndex":171,"autoFit":true,"exportBorder":false,"gridOn":true,"snapToGrid":false,"drawingGuidesOn":false,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"maxWidth":5000,"maxHeight":5000,"themeData":null,"viewportType":"default","fitBB":{"min":{"x":3,"y":3.1889969482422202},"max":{"x":277,"y":208.1999969482422}},"printModel":{"pageSize":"a4","portrait":false,"fitToOnePage":false,"displayPageBreaks":false},"objects":[{"x":1.0,"y":93.51999694824218,"rotation":0.0,"id":152,"width":78.0,"height":77.68,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":4,"lockAspectRatio":false,"lockShape":false,"children":[{"x":0.0,"y":63.68000000000001,"rotation":0.0,"id":142,"width":78.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":3,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":null},{"x":23.0,"y":0.0,"rotation":0.0,"id":134,"width":42.8749022673964,"height":60.0,"uid":"com.gliffy.shape.cisco.cisco_v1.servers.standard_host","order":1,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.servers.standard_host","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#3d85c6","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":96.0,"y":130.51999694824218,"rotation":0.0,"id":153,"width":78.0,"height":77.68,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":5,"lockAspectRatio":false,"lockShape":false,"children":[{"x":0.0,"y":63.68000000000001,"rotation":0.0,"id":154,"width":78.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":9,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":null},{"x":23.0,"y":0.0,"rotation":0.0,"id":155,"width":42.8749022673964,"height":60.0,"uid":"com.gliffy.shape.cisco.cisco_v1.servers.standard_host","order":7,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.servers.standard_host","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#3d85c6","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":197.0,"y":99.35999694824216,"rotation":0.0,"id":156,"width":78.0,"height":77.68,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":10,"lockAspectRatio":false,"lockShape":false,"children":[{"x":0.0,"y":63.68000000000001,"rotation":0.0,"id":157,"width":78.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":14,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":null},{"x":23.0,"y":0.0,"rotation":0.0,"id":158,"width":42.8749022673964,"height":60.0,"uid":"com.gliffy.shape.cisco.cisco_v1.servers.standard_host","order":12,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.servers.standard_host","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#3d85c6","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":114.0,"y":3.1889969482422202,"rotation":0.0,"id":160,"width":48.773475410240856,"height":39.0,"uid":"com.gliffy.shape.cisco.cisco_v1.storage.relational_database","order":16,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.storage.relational_database","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#02709F","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":163,"width":88.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Key-value store

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":171.0,"y":25.199996948242188,"rotation":0.0,"id":165,"width":72.0,"height":73.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":17,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":160,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":158,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#999999","fillColor":"none","dashStyle":"1.0,1.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-32.613262294879576,16.989000000000033],[70.4374511336982,74.15999999999997]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":141.0,"y":37.19999694824219,"rotation":0.0,"id":168,"width":4.0,"height":91.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":20,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":160,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":155,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#999999","fillColor":"none","dashStyle":"1.0,1.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-2.6132622948795756,4.989000000000033],[-0.5625488663017961,93.32]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":136.0,"y":42.19999694824219,"rotation":0.0,"id":169,"width":86.0,"height":50.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":21,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":160,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":134,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#999999","fillColor":"none","dashStyle":"1.0,1.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[2.3867377051204244,-0.010999999999967258],[-90.5625488663018,51.31999999999999]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"layers":[{"guid":"9wom3rMkTrb3","order":0,"name":"Layer 0","active":true,"locked":false,"visible":true,"nodeIndex":22}],"shapeStyles":{},"lineStyles":{"global":{"stroke":"#999999","strokeWidth":2,"dashStyle":"1.0,1.0"}},"textStyles":{"global":{"bold":true}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":[],"lastSerialized":1445552948967},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/docs/userguide/networking/images/key_value.png b/docs/userguide/networking/images/key_value.png new file mode 100644 index 0000000000000000000000000000000000000000..c4056f1145730f5afb1989e659ffe29ec7824d5c GIT binary patch literal 12898 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 \ No newline at end of file diff --git a/docs/userguide/networking/images/network_access.gliffy b/docs/userguide/networking/images/network_access.gliffy new file mode 100644 index 00000000..b1a1910d --- /dev/null +++ b/docs/userguide/networking/images/network_access.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#ffffff","width":437,"height":368,"nodeIndex":178,"autoFit":true,"exportBorder":false,"gridOn":true,"snapToGrid":false,"drawingGuidesOn":false,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"maxWidth":5000,"maxHeight":5000,"themeData":null,"viewportType":"default","fitBB":{"min":{"x":5,"y":1.1999969482421875},"max":{"x":437,"y":367.5199969482422}},"printModel":{"pageSize":"a4","portrait":false,"fitToOnePage":false,"displayPageBreaks":false},"objects":[{"x":126.38636363636371,"y":74.1428540910994,"rotation":0.0,"id":129,"width":291.1363636363638,"height":149.0,"uid":"com.gliffy.shape.iphone.iphone_ios7.icons_glyphs.glyph_cloud","order":0,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.iphone.iphone_ios7.icons_glyphs.glyph_cloud","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#929292","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":199.0,"y":150.96785409109907,"rotation":0.0,"id":114,"width":150.0,"height":54.732142857143145,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":17,"lockAspectRatio":false,"lockShape":false,"children":[{"x":44.0,"y":2.7321428571431454,"rotation":0.0,"id":95,"width":62.0,"height":33.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":5,"lockAspectRatio":false,"lockShape":false,"children":[{"x":29.139999999999997,"y":2.94642857142857,"rotation":0.0,"id":96,"width":3.719999999999998,"height":27.107142857142843,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":14,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":99,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":99,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.8600000000000136,-1.1785714285714448],[1.8600000000000136,28.285714285714278]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":51.46,"y":2.94642857142857,"rotation":0.0,"id":97,"width":1.2156862745098034,"height":28.285714285714267,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":11,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-1.4193795664340882,-1.178571428571729],[-1.4193795664340882,28.28571428571442]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":9.919999999999993,"y":1.3749999999999987,"rotation":0.0,"id":98,"width":1.239999999999999,"height":28.285714285714267,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":8,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[2.0393795664339223,0.3928571428572809],[2.0393795664339223,29.85714285714272]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":0.0,"y":1.7678571428571417,"rotation":0.0,"id":99,"width":62.0,"height":29.46428571428572,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":3,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#6fa8dc","fillColor":"#3d85c6","gradient":true,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":40.732142857143145,"rotation":0.0,"id":112,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":16,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

container2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":209.0,"y":284.96785409109907,"rotation":0.0,"id":115,"width":150.0,"height":54.732142857143145,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":34,"lockAspectRatio":false,"lockShape":false,"children":[{"x":44.0,"y":2.7321428571431454,"rotation":0.0,"id":116,"width":62.0,"height":33.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":22,"lockAspectRatio":false,"lockShape":false,"children":[{"x":29.139999999999997,"y":2.94642857142857,"rotation":0.0,"id":117,"width":3.719999999999998,"height":27.107142857142843,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":31,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":120,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":120,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.8600000000000136,-1.178571428571388],[1.8600000000000136,28.285714285714334]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":51.46,"y":2.94642857142857,"rotation":0.0,"id":118,"width":1.2156862745098034,"height":28.285714285714267,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":28,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-1.4193795664340882,-1.178571428571729],[-1.4193795664340882,28.28571428571442]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":9.919999999999993,"y":1.3749999999999987,"rotation":0.0,"id":119,"width":1.239999999999999,"height":28.285714285714267,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":25,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[2.0393795664339223,0.3928571428572809],[2.0393795664339223,29.85714285714272]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":0.0,"y":1.7678571428571417,"rotation":0.0,"id":120,"width":62.0,"height":29.46428571428572,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":20,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#6fa8dc","fillColor":"#3d85c6","gradient":true,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":40.732142857143145,"rotation":0.0,"id":121,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":33,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

external_container

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":122.0,"y":150.96785409109907,"rotation":0.0,"id":122,"width":150.0,"height":54.732142857143145,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":51,"lockAspectRatio":false,"lockShape":false,"children":[{"x":44.0,"y":2.7321428571431454,"rotation":0.0,"id":123,"width":62.0,"height":33.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":39,"lockAspectRatio":false,"lockShape":false,"children":[{"x":29.139999999999997,"y":2.94642857142857,"rotation":0.0,"id":124,"width":3.719999999999998,"height":27.107142857142843,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":48,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":127,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":127,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.8600000000000136,-1.1785714285714448],[1.8600000000000136,28.285714285714278]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":51.46,"y":2.94642857142857,"rotation":0.0,"id":125,"width":1.2156862745098034,"height":28.285714285714267,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":45,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-1.4193795664340882,-1.178571428571729],[-1.4193795664340882,28.28571428571442]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":9.919999999999993,"y":1.3749999999999987,"rotation":0.0,"id":126,"width":1.239999999999999,"height":28.285714285714267,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":42,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[2.0393795664339223,0.3928571428572809],[2.0393795664339223,29.85714285714272]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":0.0,"y":1.7678571428571417,"rotation":0.0,"id":127,"width":62.0,"height":29.46428571428572,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":37,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#6fa8dc","fillColor":"#3d85c6","gradient":true,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":40.732142857143145,"rotation":0.0,"id":128,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":50,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

container1

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":192.0,"y":120.19999694824219,"rotation":0.0,"id":130,"width":150.0,"height":27.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":52,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

isolated_nw

 

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":65.0,"y":1.1999969482421875,"rotation":0.0,"id":134,"width":73.116,"height":102.32,"uid":"com.gliffy.shape.cisco.cisco_v1.servers.standard_host","order":53,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.servers.standard_host","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#3d85c6","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":103.0,"y":53.19999694824219,"rotation":0.0,"id":136,"width":119.0,"height":45.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":54,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":134,"py":0.5,"px":1.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":6.0,"strokeColor":"#999999","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[35.115999999999985,-0.8400000000000034],[87.0,-0.8400000000000034],[87.0,55.0]],"lockSegments":{},"ortho":true}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":20.0,"y":16.699996948242188,"rotation":0.0,"id":140,"width":150.0,"height":1.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":55,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"


","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":55.0,"y":112.19999694824219,"rotation":0.0,"id":142,"width":78.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":56,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Docker Host

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":160.0,"y":179.0,"rotation":0.0,"id":145,"width":10.0,"height":10.0,"uid":"com.gliffy.shape.basic.basic_v1.default.ellipse","order":57,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ellipse.basic_v1","strokeWidth":1.0,"strokeColor":"#00ffff","fillColor":"#00ffff","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":31.999999999999993,"y":189.1999969482422,"rotation":0.0,"id":147,"width":73.116,"height":102.32,"uid":"com.gliffy.shape.cisco.cisco_v1.servers.standard_host","order":58,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.servers.standard_host","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#3d85c6","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":346.0,"y":265.1999969482422,"rotation":0.0,"id":149,"width":73.116,"height":102.32,"uid":"com.gliffy.shape.cisco.cisco_v1.servers.standard_host","order":59,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.servers.standard_host","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#3d85c6","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":378.0,"y":276.1999969482422,"rotation":0.0,"id":150,"width":56.0,"height":26.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":60,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":149,"py":0.5,"px":0.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":120,"py":0.5,"px":1.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#999999","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[-32.0,40.15999999999997],[-47.5,40.15999999999997],[-47.5,28.0],[-63.0,28.0]],"lockSegments":{},"ortho":true}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":250.0,"y":282.1999969482422,"rotation":0.0,"id":152,"width":84.0,"height":96.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":61,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":120,"py":0.0,"px":0.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":145,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#666666","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[3.0,7.267857142857167],[3.0,-42.96606990269251],[-85.0,-42.96606990269251],[-85.0,-93.19999694824219]],"lockSegments":{},"ortho":true}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":103.0,"y":242.1999969482422,"rotation":0.0,"id":153,"width":54.0,"height":53.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":62,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":147,"py":0.5,"px":1.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":145,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#999999","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[2.1159999999999854,-1.8400000000000034],[29.557999999999993,-1.8400000000000034],[29.557999999999993,-58.19999694824219],[57.0,-58.19999694824219]],"lockSegments":{},"ortho":true}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":250.0,"y":286.0,"rotation":0.0,"id":154,"width":10.0,"height":10.0,"uid":"com.gliffy.shape.basic.basic_v1.default.ellipse","order":63,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ellipse.basic_v1","strokeWidth":1.0,"strokeColor":"#00ffff","fillColor":"#00ffff","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":278.0,"y":149.96785409109907,"rotation":0.0,"id":155,"width":150.0,"height":54.732142857143145,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":64,"lockAspectRatio":false,"lockShape":false,"children":[{"x":44.0,"y":2.7321428571431454,"rotation":0.0,"id":156,"width":62.0,"height":33.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":69,"lockAspectRatio":false,"lockShape":false,"children":[{"x":29.139999999999997,"y":2.94642857142857,"rotation":0.0,"id":157,"width":3.719999999999998,"height":27.107142857142843,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":78,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":160,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":160,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.8600000000000136,-1.1785714285714448],[1.8600000000000136,28.285714285714278]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":51.46,"y":2.94642857142857,"rotation":0.0,"id":158,"width":1.2156862745098034,"height":28.285714285714267,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":75,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-1.4193795664340882,-1.178571428571729],[-1.4193795664340882,28.28571428571442]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":9.919999999999993,"y":1.3749999999999987,"rotation":0.0,"id":159,"width":1.239999999999999,"height":28.285714285714267,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":72,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[2.0393795664339223,0.3928571428572809],[2.0393795664339223,29.85714285714272]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":0.0,"y":1.7678571428571417,"rotation":0.0,"id":160,"width":62.0,"height":29.46428571428572,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":67,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#6fa8dc","fillColor":"#3d85c6","gradient":true,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":40.732142857143145,"rotation":0.0,"id":161,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":80,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

container3

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":3.0,"y":296.1999969482422,"rotation":0.0,"id":162,"width":133.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":81,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":5,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

external host

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":337.0,"y":21.199996948242188,"rotation":0.0,"id":176,"width":98.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":84,"lockAspectRatio":false,"lockShape":false,"children":[{"x":13.0,"y":0.0,"rotation":0.0,"id":174,"width":85.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":83,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":5,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

published port

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":null},{"x":0.0,"y":3.8000030517578125,"rotation":0.0,"id":173,"width":10.0,"height":10.0,"uid":"com.gliffy.shape.basic.basic_v1.default.ellipse","order":82,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.ellipse.basic_v1","strokeWidth":1.0,"strokeColor":"#00ffff","fillColor":"#00ffff","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"9wom3rMkTrb3"}],"layers":[{"guid":"9wom3rMkTrb3","order":0,"name":"Layer 0","active":true,"locked":false,"visible":true,"nodeIndex":85}],"shapeStyles":{},"lineStyles":{"global":{"stroke":"#666666","strokeWidth":2}},"textStyles":{"global":{"bold":true}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":[],"lastSerialized":1445536836098},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/docs/userguide/networking/images/network_access.png b/docs/userguide/networking/images/network_access.png new file mode 100644 index 0000000000000000000000000000000000000000..d8bf6319dd97e81192a0ec0da138caa837df2f06 GIT binary patch literal 30649 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 literal 0 HcmV?d00001 diff --git a/docs/userguide/networking/images/network_access.svg b/docs/userguide/networking/images/network_access.svg new file mode 100644 index 00000000..33c84465 --- /dev/null +++ b/docs/userguide/networking/images/network_access.svg @@ -0,0 +1 @@ +container2external_containercontainer1isolated_nwDockerHostcontainer3externalhostpublishedport \ No newline at end of file diff --git a/docs/userguide/networking/images/overlay-network-final.gliffy b/docs/userguide/networking/images/overlay-network-final.gliffy new file mode 100644 index 00000000..75b878de --- /dev/null +++ b/docs/userguide/networking/images/overlay-network-final.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#ffffff","width":361,"height":263,"nodeIndex":249,"autoFit":true,"exportBorder":false,"gridOn":true,"snapToGrid":false,"drawingGuidesOn":false,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"maxWidth":5000,"maxHeight":5000,"themeData":null,"viewportType":"default","fitBB":{"min":{"x":23.000000000000057,"y":8.18899694824222},"max":{"x":360.00000000000006,"y":262.0000000000038}},"printModel":{"pageSize":"a4","portrait":false,"fitToOnePage":false,"displayPageBreaks":false},"objects":[{"x":140.0,"y":162.1999969482422,"rotation":0.0,"id":247,"width":33.0,"height":11.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":107,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":238,"py":0.9999999999999998,"px":0.29289321881345254}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":134,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#999999","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[-3.2842712474617883,-3.971422467905427],[-3.2842712474617883,16.319999999999993],[-43.562548866301796,16.319999999999993],[-43.562548866301796,-3.680000000000007]],"lockSegments":{"1":true},"ortho":true}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":187.0,"y":134.1999969482422,"rotation":0.0,"id":246,"width":18.0,"height":17.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":106,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":134,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":223,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#999999","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[-90.5625488663018,-35.68000000000001],[-90.5625488663018,-60.68000000000001],[-43.0,-60.68000000000001],[-43.0,-25.428571428571402]],"lockSegments":{"1":true},"ortho":true}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":166.0,"y":169.1999969482422,"rotation":0.0,"id":245,"width":22.0,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":105,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":172,"py":0.7071067811865475,"px":0.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":228,"py":0.5,"px":1.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#999999","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[7.000000000000028,-0.3795674614555935],[-6.5,-0.3795674614555935],[-6.5,29.50000000000003],[-20.0,29.50000000000003]],"lockSegments":{},"ortho":true}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":189.0,"y":197.1999969482422,"rotation":0.0,"id":244,"width":15.0,"height":36.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":104,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":155,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":233,"py":0.5,"px":1.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#999999","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[2.437451133698204,-1.6800000000000068],[2.437451133698204,37.5],[-19.0,37.5]],"lockSegments":{},"ortho":true}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":292.0,"y":163.1999969482422,"rotation":0.0,"id":242,"width":51.0,"height":8.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":102,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":158,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":218,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#999999","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[0.43745113369817545,1.1599999999999682],[0.43745113369817545,21.428571428571473],[-52.0,21.428571428571473],[-52.0,1.4285714285714732]],"lockSegments":{"1":true},"ortho":true}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":289.0,"y":102.19999694824219,"rotation":0.0,"id":240,"width":51.0,"height":4.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":100,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":158,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":200,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#999999","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[3.4374511336981755,2.159999999999968],[3.4374511336981755,-7.840000000000032],[-51.0,-7.840000000000032],[-51.0,8.571428571428598]],"lockSegments":{"1":true},"ortho":true}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":23.000000000000057,"y":81.00000000000378,"rotation":180.0,"id":175,"width":337.0,"height":181.0,"uid":"com.gliffy.shape.iphone.iphone_ios7.icons_glyphs.glyph_cloud","order":41,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.iphone.iphone_ios7.icons_glyphs.glyph_cloud","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#929292","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":52.0,"y":8.18899694824222,"rotation":0.0,"id":178,"width":274.0,"height":205.01099999999997,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":42,"lockAspectRatio":false,"lockShape":false,"children":[{"x":25.999999999999996,"y":110.19640369599998,"rotation":0.0,"id":173,"width":20.88802989941042,"height":19.0,"uid":"com.gliffy.shape.basic.basic_v1.default.svg","order":40,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Svg","Svg":{"embeddedResourceId":0,"strokeWidth":2.0,"strokeColor":"#000000","dropShadow":true,"shadowX":5.0,"shadowY":5.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":121.00000000000003,"y":147.19640369599998,"rotation":0.0,"id":172,"width":20.88802989941042,"height":19.0,"uid":"com.gliffy.shape.basic.basic_v1.default.svg","order":38,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Svg","Svg":{"embeddedResourceId":0,"strokeWidth":2.0,"strokeColor":"#000000","dropShadow":true,"shadowX":5.0,"shadowY":5.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":222.0,"y":114.19640369599998,"rotation":0.0,"id":171,"width":20.88802989941042,"height":19.0,"uid":"com.gliffy.shape.basic.basic_v1.default.svg","order":36,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Svg","Svg":{"embeddedResourceId":0,"strokeWidth":2.0,"strokeColor":"#000000","dropShadow":true,"shadowX":5.0,"shadowY":5.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":135.0,"y":39.01099999999997,"rotation":0.0,"id":169,"width":86.0,"height":50.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":34,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":160,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":134,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#999999","fillColor":"none","dashStyle":"1.0,1.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[2.3867377051204244,-0.010999999999967258],[-90.5625488663018,51.31999999999999]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":140.0,"y":34.01099999999997,"rotation":0.0,"id":168,"width":4.0,"height":91.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":32,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":160,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":155,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#999999","fillColor":"none","dashStyle":"1.0,1.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-2.6132622948795756,4.989000000000033],[-0.5625488663017961,93.32]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":170.0,"y":22.010999999999967,"rotation":0.0,"id":165,"width":72.0,"height":73.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":30,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":160,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":158,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#999999","fillColor":"none","dashStyle":"1.0,1.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-32.613262294879576,16.989000000000033],[70.43745113369818,74.15999999999997]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":113.0,"y":0.0,"rotation":0.0,"id":160,"width":48.773475410240856,"height":39.0,"uid":"com.gliffy.shape.cisco.cisco_v1.storage.relational_database","order":27,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.storage.relational_database","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#02709F","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":163,"width":88.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Key-value store

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":null},{"x":196.0,"y":96.17099999999994,"rotation":0.0,"id":156,"width":78.0,"height":77.68,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":20,"lockAspectRatio":false,"lockShape":false,"children":[{"x":0.0,"y":63.68000000000001,"rotation":0.0,"id":157,"width":78.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":23,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":23.0,"y":0.0,"rotation":0.0,"id":158,"width":42.8749022673964,"height":60.000000000000014,"uid":"com.gliffy.shape.cisco.cisco_v1.servers.standard_host","order":18,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.servers.standard_host","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#3d85c6","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":95.0,"y":127.33099999999996,"rotation":0.0,"id":153,"width":78.0,"height":77.68,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":12,"lockAspectRatio":false,"lockShape":false,"children":[{"x":0.0,"y":63.68000000000001,"rotation":0.0,"id":154,"width":78.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":15,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":23.0,"y":0.0,"rotation":0.0,"id":155,"width":42.8749022673964,"height":60.000000000000014,"uid":"com.gliffy.shape.cisco.cisco_v1.servers.standard_host","order":10,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.servers.standard_host","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#3d85c6","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":90.33099999999996,"rotation":0.0,"id":152,"width":78.0,"height":77.68,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":7,"lockAspectRatio":false,"lockShape":false,"children":[{"x":0.0,"y":63.68000000000001,"rotation":0.0,"id":142,"width":78.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":5,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":23.0,"y":0.0,"rotation":0.0,"id":134,"width":42.8749022673964,"height":60.000000000000014,"uid":"com.gliffy.shape.cisco.cisco_v1.servers.standard_host","order":2,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.servers.standard_host","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#3d85c6","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":null}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":218.0,"y":109.69999694824222,"rotation":0.0,"id":196,"width":40.0,"height":20.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":47,"lockAspectRatio":false,"lockShape":false,"children":[{"x":18.8,"y":1.7857142857142847,"rotation":0.0,"id":197,"width":2.399999999999999,"height":16.428571428571416,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":53,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":200,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":200,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.1999999999999886,-0.7142857142857082],[1.1999999999999886,17.14285714285714]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":33.2,"y":1.7857142857142847,"rotation":0.0,"id":198,"width":1.3333333333333333,"height":17.14285714285713,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":51,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-0.9157287525381217,-0.7142857142858963],[-0.9157287525381217,17.142857142857224]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":6.399999999999995,"y":0.8333333333333324,"rotation":0.0,"id":199,"width":1.3333333333333333,"height":17.14285714285713,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":49,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.3157287525380146,0.23809523809532174],[1.3157287525380146,18.09523809523801]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":0.0,"y":1.0714285714285707,"rotation":0.0,"id":200,"width":40.0,"height":17.857142857142858,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":46,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#6fa8dc","fillColor":"#3d85c6","gradient":true,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":220.0,"y":145.69999694824222,"rotation":0.0,"id":214,"width":40.0,"height":20.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":57,"lockAspectRatio":false,"lockShape":false,"children":[{"x":18.8,"y":1.7857142857142847,"rotation":0.0,"id":215,"width":2.399999999999999,"height":16.428571428571416,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":62,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":218,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":218,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.1999999999999886,-0.714285714285694],[1.1999999999999886,17.142857142857167]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":33.2,"y":1.7857142857142847,"rotation":0.0,"id":216,"width":1.3333333333333333,"height":17.14285714285713,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":60,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-0.9157287525381217,-0.7142857142858963],[-0.9157287525381217,17.142857142857224]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":6.399999999999995,"y":0.8333333333333324,"rotation":0.0,"id":217,"width":1.3333333333333333,"height":17.14285714285713,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":58,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.3157287525380146,0.23809523809532174],[1.3157287525380146,18.09523809523801]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":0.0,"y":1.0714285714285707,"rotation":0.0,"id":218,"width":40.0,"height":17.857142857142858,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":56,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#6fa8dc","fillColor":"#3d85c6","gradient":true,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":124.0,"y":107.69999694824222,"rotation":0.0,"id":219,"width":40.0,"height":20.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":66,"lockAspectRatio":false,"lockShape":false,"children":[{"x":18.8,"y":1.7857142857142847,"rotation":0.0,"id":220,"width":2.399999999999999,"height":16.428571428571416,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":71,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":223,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":223,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.1999999999999886,-0.7142857142857082],[1.1999999999999886,17.142857142857153]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":33.2,"y":1.7857142857142847,"rotation":0.0,"id":221,"width":1.3333333333333333,"height":17.14285714285713,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":69,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-0.9157287525381217,-0.7142857142858963],[-0.9157287525381217,17.142857142857224]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":6.399999999999995,"y":0.8333333333333324,"rotation":0.0,"id":222,"width":1.3333333333333333,"height":17.14285714285713,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":67,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.3157287525380146,0.23809523809532174],[1.3157287525380146,18.09523809523801]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":0.0,"y":1.0714285714285707,"rotation":0.0,"id":223,"width":40.0,"height":17.857142857142858,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":65,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#6fa8dc","fillColor":"#3d85c6","gradient":true,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":106.0,"y":188.69999694824222,"rotation":0.0,"id":224,"width":40.0,"height":20.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":75,"lockAspectRatio":false,"lockShape":false,"children":[{"x":18.8,"y":1.7857142857142847,"rotation":0.0,"id":225,"width":2.399999999999999,"height":16.428571428571416,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":80,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":228,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":228,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.2000000000000028,-0.714285714285694],[1.2000000000000028,17.142857142857167]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":33.2,"y":1.7857142857142847,"rotation":0.0,"id":226,"width":1.3333333333333333,"height":17.14285714285713,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":78,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-0.9157287525381217,-0.7142857142858963],[-0.9157287525381217,17.142857142857224]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":6.399999999999995,"y":0.8333333333333324,"rotation":0.0,"id":227,"width":1.3333333333333333,"height":17.14285714285713,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":76,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.3157287525380146,0.23809523809532174],[1.3157287525380146,18.09523809523801]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":0.0,"y":1.0714285714285707,"rotation":0.0,"id":228,"width":40.0,"height":17.857142857142858,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":74,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#6fa8dc","fillColor":"#3d85c6","gradient":true,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":130.0,"y":224.6999969482422,"rotation":0.0,"id":229,"width":40.0,"height":20.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":84,"lockAspectRatio":false,"lockShape":false,"children":[{"x":18.8,"y":1.7857142857142847,"rotation":0.0,"id":230,"width":2.399999999999999,"height":16.428571428571416,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":89,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":233,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":233,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.1999999999999886,-0.714285714285694],[1.1999999999999886,17.142857142857167]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":33.2,"y":1.7857142857142847,"rotation":0.0,"id":231,"width":1.3333333333333333,"height":17.14285714285713,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":87,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-0.9157287525381217,-0.7142857142858963],[-0.9157287525381217,17.142857142857224]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":6.399999999999995,"y":0.8333333333333324,"rotation":0.0,"id":232,"width":1.3333333333333333,"height":17.14285714285713,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":85,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.3157287525380146,0.23809523809532174],[1.3157287525380146,18.09523809523801]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":0.0,"y":1.0714285714285707,"rotation":0.0,"id":233,"width":40.0,"height":17.857142857142858,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":83,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#6fa8dc","fillColor":"#3d85c6","gradient":true,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":125.00000000000011,"y":139.30000305176532,"rotation":0.0,"id":234,"width":40.0,"height":20.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":93,"lockAspectRatio":false,"lockShape":false,"children":[{"x":18.8,"y":1.7857142857142847,"rotation":0.0,"id":235,"width":2.399999999999999,"height":16.428571428571416,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":98,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":238,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":238,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.1999999999999886,-0.714285714285694],[1.1999999999999886,17.142857142857167]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":33.2,"y":1.7857142857142847,"rotation":0.0,"id":236,"width":1.3333333333333333,"height":17.14285714285713,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":96,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-0.9157287525381217,-0.7142857142858963],[-0.9157287525381217,17.142857142857224]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":6.399999999999995,"y":0.8333333333333324,"rotation":0.0,"id":237,"width":1.3333333333333333,"height":17.14285714285713,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":94,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.3157287525380146,0.23809523809532174],[1.3157287525380146,18.09523809523801]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":0.0,"y":1.0714285714285707,"rotation":0.0,"id":238,"width":40.0,"height":17.857142857142858,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":92,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#6fa8dc","fillColor":"#3d85c6","gradient":true,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":"9wom3rMkTrb3"}],"layers":[{"guid":"9wom3rMkTrb3","order":0,"name":"Layer 0","active":true,"locked":false,"visible":true,"nodeIndex":109}],"shapeStyles":{},"lineStyles":{"global":{"stroke":"#999999","strokeWidth":2}},"textStyles":{"global":{"bold":true,"face":"Courier"}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":["com.gliffy.custom.confluence.c20f4a380e3cee362007f9e62694d34d947f28ed4263c0702b3dd72d9801532a"],"lastSerialized":1445556943068},"embeddedResources":{"index":1,"resources":[{"id":0,"mimeType":"image/svg+xml","data":"\n\n \n logo copy\n Created with Sketch.\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n","width":59.29392246992643,"height":42.185403696,"x":0.4429050300735753,"y":0.7077644040000006}]}} \ No newline at end of file diff --git a/docs/userguide/networking/images/overlay-network-final.png b/docs/userguide/networking/images/overlay-network-final.png new file mode 100644 index 0000000000000000000000000000000000000000..3031839348ec7663b7116c54377467a0de1258ad GIT binary patch literal 28072 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 literal 0 HcmV?d00001 diff --git a/docs/userguide/networking/images/overlay-network-final.svg b/docs/userguide/networking/images/overlay-network-final.svg new file mode 100644 index 00000000..8ff3b3e5 --- /dev/null +++ b/docs/userguide/networking/images/overlay-network-final.svg @@ -0,0 +1 @@ +HostHostHostKey-valuestore \ No newline at end of file diff --git a/docs/userguide/networking/images/overlay_network.gliffy b/docs/userguide/networking/images/overlay_network.gliffy new file mode 100644 index 00000000..e4a6ed95 --- /dev/null +++ b/docs/userguide/networking/images/overlay_network.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#ffffff","width":361,"height":291,"nodeIndex":195,"autoFit":true,"exportBorder":false,"gridOn":true,"snapToGrid":false,"drawingGuidesOn":false,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"maxWidth":5000,"maxHeight":5000,"themeData":null,"viewportType":"default","fitBB":{"min":{"x":23.000000000000057,"y":8.18899694824222},"max":{"x":360.00000000000006,"y":290.6999969482422}},"printModel":{"pageSize":"a4","portrait":false,"fitToOnePage":false,"displayPageBreaks":false},"objects":[{"x":194.0,"y":200.1999969482422,"rotation":0.0,"id":193,"width":47.0,"height":77.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":41,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#999999","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[10.0,-6.0],[47.0,77.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":64.0,"y":272.6999969482422,"rotation":0.0,"id":179,"width":247.0,"height":24.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":27,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":5,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

docker network create -d overlay

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":23.000000000000057,"y":81.00000000000378,"rotation":180.0,"id":175,"width":337.0,"height":181.0,"uid":"com.gliffy.shape.iphone.iphone_ios7.icons_glyphs.glyph_cloud","order":25,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.iphone.iphone_ios7.icons_glyphs.glyph_cloud","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#929292","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":52.0,"y":8.18899694824222,"rotation":0.0,"id":178,"width":274.0,"height":205.01099999999997,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":26,"lockAspectRatio":false,"lockShape":false,"children":[{"x":25.999999999999996,"y":110.19640369599998,"rotation":0.0,"id":173,"width":20.88802989941042,"height":19.0,"uid":"com.gliffy.shape.basic.basic_v1.default.svg","order":23,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Svg","Svg":{"embeddedResourceId":0,"strokeWidth":2.0,"strokeColor":"#000000","dropShadow":true,"shadowX":5.0,"shadowY":5.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":121.00000000000003,"y":147.19640369599998,"rotation":0.0,"id":172,"width":20.88802989941042,"height":19.0,"uid":"com.gliffy.shape.basic.basic_v1.default.svg","order":22,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Svg","Svg":{"embeddedResourceId":0,"strokeWidth":2.0,"strokeColor":"#000000","dropShadow":true,"shadowX":5.0,"shadowY":5.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":222.0,"y":114.19640369599998,"rotation":0.0,"id":171,"width":20.88802989941042,"height":19.0,"uid":"com.gliffy.shape.basic.basic_v1.default.svg","order":21,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Svg","Svg":{"embeddedResourceId":0,"strokeWidth":2.0,"strokeColor":"#000000","dropShadow":true,"shadowX":5.0,"shadowY":5.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":135.0,"y":39.01099999999997,"rotation":0.0,"id":169,"width":86.0,"height":50.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":20,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":160,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":134,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#999999","fillColor":"none","dashStyle":"1.0,1.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[2.3867377051204244,-0.010999999999967258],[-90.5625488663018,51.31999999999999]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":140.0,"y":34.01099999999997,"rotation":0.0,"id":168,"width":4.0,"height":91.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":19,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":160,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":155,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#999999","fillColor":"none","dashStyle":"1.0,1.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-2.6132622948795756,4.989000000000033],[-0.5625488663017961,93.32]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":170.0,"y":22.010999999999967,"rotation":0.0,"id":165,"width":72.0,"height":73.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":18,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":160,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":158,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#999999","fillColor":"none","dashStyle":"1.0,1.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-32.613262294879576,16.989000000000033],[70.43745113369818,74.15999999999997]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":113.0,"y":0.0,"rotation":0.0,"id":160,"width":48.773475410240856,"height":39.0,"uid":"com.gliffy.shape.cisco.cisco_v1.storage.relational_database","order":15,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.storage.relational_database","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#02709F","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":163,"width":88.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Key-value store

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":null},{"x":196.0,"y":96.17099999999994,"rotation":0.0,"id":156,"width":78.0,"height":77.68,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":12,"lockAspectRatio":false,"lockShape":false,"children":[{"x":0.0,"y":63.68000000000001,"rotation":0.0,"id":157,"width":78.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":14,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":23.0,"y":0.0,"rotation":0.0,"id":158,"width":42.8749022673964,"height":60.000000000000014,"uid":"com.gliffy.shape.cisco.cisco_v1.servers.standard_host","order":11,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.servers.standard_host","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#3d85c6","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":95.0,"y":127.33099999999996,"rotation":0.0,"id":153,"width":78.0,"height":77.68,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":7,"lockAspectRatio":false,"lockShape":false,"children":[{"x":0.0,"y":63.68000000000001,"rotation":0.0,"id":154,"width":78.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":9,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":23.0,"y":0.0,"rotation":0.0,"id":155,"width":42.8749022673964,"height":60.000000000000014,"uid":"com.gliffy.shape.cisco.cisco_v1.servers.standard_host","order":6,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.servers.standard_host","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#3d85c6","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":null},{"x":0.0,"y":90.33099999999996,"rotation":0.0,"id":152,"width":78.0,"height":77.68,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":4,"lockAspectRatio":false,"lockShape":false,"children":[{"x":0.0,"y":63.68000000000001,"rotation":0.0,"id":142,"width":78.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":3,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[],"hidden":false,"layerId":null},{"x":23.0,"y":0.0,"rotation":0.0,"id":134,"width":42.8749022673964,"height":60.000000000000014,"uid":"com.gliffy.shape.cisco.cisco_v1.servers.standard_host","order":1,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.servers.standard_host","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#3d85c6","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":null}],"hidden":false,"layerId":null}],"hidden":false,"layerId":"9wom3rMkTrb3"}],"layers":[{"guid":"9wom3rMkTrb3","order":0,"name":"Layer 0","active":true,"locked":false,"visible":true,"nodeIndex":43}],"shapeStyles":{},"lineStyles":{"global":{"stroke":"#999999","strokeWidth":2,"endArrow":2}},"textStyles":{"global":{"bold":true,"face":"Courier"}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":["com.gliffy.custom.confluence.c20f4a380e3cee362007f9e62694d34d947f28ed4263c0702b3dd72d9801532a"],"lastSerialized":1445556181238},"embeddedResources":{"index":1,"resources":[{"id":0,"mimeType":"image/svg+xml","data":"\n\n \n logo copy\n Created with Sketch.\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n","width":59.29392246992643,"height":42.185403696,"x":0.4429050300735753,"y":0.7077644040000006}]}} \ No newline at end of file diff --git a/docs/userguide/networking/images/overlay_network.png b/docs/userguide/networking/images/overlay_network.png new file mode 100644 index 0000000000000000000000000000000000000000..9d728b818273b43d9a0e36c7845e0f53bd757519 GIT binary patch literal 23276 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 z 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 R6%F_^xnyDRQqS?>e*lpyD-8eu literal 0 HcmV?d00001 diff --git a/docs/userguide/networking/images/overlay_network.svg b/docs/userguide/networking/images/overlay_network.svg new file mode 100644 index 00000000..f53de124 --- /dev/null +++ b/docs/userguide/networking/images/overlay_network.svg @@ -0,0 +1 @@ +HostHostHostKey-valuestoredockernetworkcreate-doverlay \ No newline at end of file diff --git a/docs/userguide/networking/images/working.gliffy b/docs/userguide/networking/images/working.gliffy new file mode 100644 index 00000000..5831a460 --- /dev/null +++ b/docs/userguide/networking/images/working.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#ffffff","width":376,"height":241,"nodeIndex":152,"autoFit":true,"exportBorder":false,"gridOn":true,"snapToGrid":false,"drawingGuidesOn":false,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"maxWidth":5000,"maxHeight":5000,"themeData":null,"viewportType":"default","fitBB":{"min":{"x":1,"y":5.1999969482421875},"max":{"x":375.38636363636374,"y":240.14285409109937}},"printModel":{"pageSize":"a4","portrait":false,"fitToOnePage":false,"displayPageBreaks":false},"objects":[{"x":85.0,"y":50.0,"rotation":0.0,"id":150,"width":211.0,"height":31.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":60,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":134,"py":0.5,"px":1.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":6.0,"strokeColor":"#999999","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[3.1159999999999997,6.359996948242184],[180.558,6.359996948242184],[180.558,67.0],[180.0,67.0]],"lockSegments":{"1":true},"ortho":true}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":196.0,"y":100.69999694824219,"rotation":0.0,"id":140,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":56,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"


","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":15.0,"y":5.1999969482421875,"rotation":0.0,"id":134,"width":73.116,"height":102.32,"uid":"com.gliffy.shape.cisco.cisco_v1.servers.standard_host","order":54,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.servers.standard_host","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#3d85c6","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":53.0,"y":57.19999694824219,"rotation":0.0,"id":136,"width":119.0,"height":45.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":55,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":134,"py":0.5,"px":1.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":6.0,"strokeColor":"#999999","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[35.116,-0.8400000000000034],[89.0,-0.8400000000000034],[89.0,57.0]],"lockSegments":{},"ortho":true}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":5.0,"y":116.19999694824219,"rotation":0.0,"id":142,"width":78.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":57,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Docker Host

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":113.38636363636374,"y":116.14285409109937,"rotation":0.0,"id":129,"width":262.0,"height":124.0,"uid":"com.gliffy.shape.iphone.iphone_ios7.icons_glyphs.glyph_cloud","order":0,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.iphone.iphone_ios7.icons_glyphs.glyph_cloud","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#929292","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":15.386363636363683,"y":113.14285409109937,"rotation":0.0,"id":146,"width":233.0,"height":127.0,"uid":"com.gliffy.shape.iphone.iphone_ios7.icons_glyphs.glyph_cloud","order":1,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.iphone.iphone_ios7.icons_glyphs.glyph_cloud","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#929292","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":106.0,"y":175.96785409109907,"rotation":0.0,"id":114,"width":150.0,"height":54.732142857143145,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":18,"lockAspectRatio":false,"lockShape":false,"children":[{"x":44.0,"y":2.7321428571431454,"rotation":0.0,"id":95,"width":62.0,"height":33.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":6,"lockAspectRatio":false,"lockShape":false,"children":[{"x":29.139999999999997,"y":2.94642857142857,"rotation":0.0,"id":96,"width":3.719999999999998,"height":27.107142857142843,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":15,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":99,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":99,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.8600000000000136,-1.1785714285714448],[1.8600000000000136,28.285714285714278]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":51.46,"y":2.94642857142857,"rotation":0.0,"id":97,"width":1.2156862745098034,"height":28.285714285714267,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":12,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-1.4193795664340882,-1.178571428571729],[-1.4193795664340882,28.28571428571442]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":9.919999999999993,"y":1.3749999999999987,"rotation":0.0,"id":98,"width":1.239999999999999,"height":28.285714285714267,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":9,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[2.0393795664339223,0.3928571428572809],[2.0393795664339223,29.85714285714272]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":1.7678571428571417,"rotation":0.0,"id":99,"width":62.0,"height":29.46428571428572,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":4,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#6fa8dc","fillColor":"#3d85c6","gradient":true,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":40.732142857143145,"rotation":0.0,"id":112,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":17,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

container2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":217.0,"y":177.96785409109907,"rotation":0.0,"id":115,"width":150.0,"height":54.732142857143145,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":35,"lockAspectRatio":false,"lockShape":false,"children":[{"x":44.0,"y":2.7321428571431454,"rotation":0.0,"id":116,"width":62.0,"height":33.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":23,"lockAspectRatio":false,"lockShape":false,"children":[{"x":29.139999999999997,"y":2.94642857142857,"rotation":0.0,"id":117,"width":3.719999999999998,"height":27.107142857142843,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":32,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":120,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":120,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.8600000000000136,-1.1785714285714448],[1.8600000000000136,28.285714285714278]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":51.46,"y":2.94642857142857,"rotation":0.0,"id":118,"width":1.2156862745098034,"height":28.285714285714267,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":29,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-1.4193795664340882,-1.178571428571729],[-1.4193795664340882,28.28571428571442]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":9.919999999999993,"y":1.3749999999999987,"rotation":0.0,"id":119,"width":1.239999999999999,"height":28.285714285714267,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":26,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[2.0393795664339223,0.3928571428572809],[2.0393795664339223,29.85714285714272]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":1.7678571428571417,"rotation":0.0,"id":120,"width":62.0,"height":29.46428571428572,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":21,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#6fa8dc","fillColor":"#3d85c6","gradient":true,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":40.732142857143145,"rotation":0.0,"id":121,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":34,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

container3

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":-1.0,"y":175.96785409109907,"rotation":0.0,"id":122,"width":150.0,"height":54.732142857143145,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":52,"lockAspectRatio":false,"lockShape":false,"children":[{"x":44.0,"y":2.7321428571431454,"rotation":0.0,"id":123,"width":62.0,"height":33.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":40,"lockAspectRatio":false,"lockShape":false,"children":[{"x":29.139999999999997,"y":2.94642857142857,"rotation":0.0,"id":124,"width":3.719999999999998,"height":27.107142857142843,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":49,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":127,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":127,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.8599999999999994,-1.1785714285714448],[1.8599999999999994,28.285714285714278]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":51.46,"y":2.94642857142857,"rotation":0.0,"id":125,"width":1.2156862745098034,"height":28.285714285714267,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":46,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-1.4193795664340882,-1.178571428571729],[-1.4193795664340882,28.28571428571442]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":9.919999999999993,"y":1.3749999999999987,"rotation":0.0,"id":126,"width":1.239999999999999,"height":28.285714285714267,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":43,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[2.0393795664339223,0.3928571428572809],[2.0393795664339223,29.85714285714272]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":1.7678571428571417,"rotation":0.0,"id":127,"width":62.0,"height":29.46428571428572,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":38,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#6fa8dc","fillColor":"#3d85c6","gradient":true,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":40.732142857143145,"rotation":0.0,"id":128,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":51,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

container1

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":185.0,"y":143.1999969482422,"rotation":0.0,"id":130,"width":150.0,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":53,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

isolated_nw

 

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":55.0,"y":139.1999969482422,"rotation":0.0,"id":147,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":58,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

bridge 

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"layers":[{"guid":"9wom3rMkTrb3","order":0,"name":"Layer 0","active":true,"locked":false,"visible":true,"nodeIndex":62}],"shapeStyles":{},"lineStyles":{"global":{"stroke":"#999999","strokeWidth":6}},"textStyles":{"global":{"bold":true}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":[],"lastSerialized":1446315118663,"analyticsProduct":"Confluence"},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/docs/userguide/networking/images/working.png b/docs/userguide/networking/images/working.png new file mode 100644 index 0000000000000000000000000000000000000000..8539913aff6aa0597c3600d7c0c01cef7efa0222 GIT binary patch literal 18319 zcmbrmXEdB&_%^EdE`}&kqmEu8MD#v-iP6jGL5LQ;ccL4j6J0QRXO!rj2+>IpJw%Ce z9>4$lzGtoT<(#$72Om6T@BQ3uU-x}qd!jYf6$$aE@la4u2$hxOv{6t{V}U;m94z3< zcPy$16ch#&WjSeGZ_`5)Y;U@O#@ESjw^p4tSih%R4^v#K5WuG(vFIX+C?Clz8Kibm zWp;&0Wb6vqKB5$4{v|+#2+j-9Tyh#Ew}XcfJI<;;Ymy`I-@kpmY`knQkB%hzN;MS3 z#djp@|4ecSrf9u+^F}e73RNM9)Et8f6(2(Y4)*rrLyJRU?!hS|F-K@J_D??jpW9Kx zA=s$EW#DcQS+G=+yc3<4O$1^*JNUnM6?$wj(BwgX^xw>0a=q|Hldca%*queG7am;x zaw=}rYkyVF);=#SWMPi2G4fO9zd@BK(1ICbzjYZY607n4#fZE;usI*QMwkCi`AR$^ zy=v{JvP7tx;f$>9-#^7~c~T$e3D#vw)JBw*R~N1LI_@PjXUzUm<&x;NP`cDVU$42r z3u)QAc-}C<951tgKEB)bhH^IYTf%v9#^ z$bZ7n>UR#eMVt_L^1uNpXa@zkz=A2f1{NeNwN09OX|M%PKDo&e-TAmv-h6xGxLdBF zDiP`qsS%MWx^nx@+IBxbfIZg-+jx3z%m>J!7zK50XfyV6n)|l~rpYn&9wHkyc;2&r zWnrc_Hr2N8o-MU=uXs2FzZ-ctZG4XV+X9$5;D66t3PWY{)VINsxV7N#yg3xpX!ly? z^_|@aKipmQirLM1X`MSS{3V}z#Z?8>zo|IcIXer^uK6-0+?HuQ1W-EzCXTlPh)?`* zNrwxmWSjB!w91kDlTiLL>tEKeFP3S?dN@2{C<164d!pl+;8*1TM`nTugF07nld~Gk z+^n3o27558%iFg0gKNxlt8%Ymcey-I_uu`i=ch=#wjDJOcZw8wJO#lw!+^kZ8P{>4 z-mNr7Oj?WOFC{)4p`3RAdLxliutbkIB^(6Fjs^rR z6B5LaAhD=>c08C4w{+>dE4?hhbgbAb`Ia#=xWAIy>#zIp_q;UaZ13)xW^bZ7!|_`m ztbmZh76w=<34?hUc-#3cq{K900Bw@_UsLI)z$6X9SdrI_^9=xoj<BsEKB3XO=SI^IDv z!^eNM@|LYm%pYdkb~R-DEv1>~Z|HlYnYDnGNe_Prs4eRKYasgIprb)RuG*FeoW9x} z5x~iU)^PZse9$Y2pDq90X9Vs;K#a6aKVZP)Cp4(6_^kNdUunO61@y}(*MkG4onuJJ zdf_aU8G*ESee?J4%EhJiSMe5_7h}bscp9oupWk1omrmE(G}-@wU`(Nvk~CPg%GvS6v=rQqvfA&R~Gf8&0yn)wn!(fJsOL<6!4O<&@)(F5NC6%(V@mm zrpUpc&5bPh*(>LtY=5<};?qj3R&b=qixIdVF#{o>MK)+SWEi=Ev)TOUliWb8H%BPN z22bPwri<$JpYU5~q;EwY!v*Gl!nfz2YE1#6-#XEM!uMj1TC)M>ZjkC>hZ>(2{+og( zM5x5NetjGvIFZpj4$LE7A%5q&PsYiywn2rK4GQrGG5a(%5#$V*{K$|v=7CUptenUC zIV`cqUP49Fsk(;%0qlhYks_$lzD3--yYX^XFG>qH9_2fJ>nIQv*&bs{K3;$AV*J8T zt0~99yZr1~^`mu|;DOjL;j zYPA`j1tzX19#!w|-XF#*Tj>p``)vFchJXf1c0wNAAO?qu5g!G;K7j)fHBUumQt7bo zZqy)dDsC~(6w{aeuOC>TGbx_}$eAYjz`rYWnG4ox{NL_*PHK*b>WdM^zw7OL`6yEV z{}E~4^n?VUB2>Um0ecpG_Ue}DQKZEGiF8kJskQV`q*VWj^lNCO*`x5zr5}~UxgZ%hR-|KafPF3Bx9%NtMD-QolX$es!Ik)D<4xu1Pn@Gw#qVbJCG z(*N$_TZ2{PAFJhA2O&3Gofs4lcC}$+VRkm{{`Y#eM!RVlFi4aiP3rNRLb@>&)=@C> zot`{-qM7&FKkO$eVVI83e;=5%YKC(8ngeZ0DBqgk*(>-`lhde8xez_qqvzY-B0HOL`?Eid>+m6c3&rdFPqv_m9G#2jyEO&-~M!$;%k@|Xgc!aH8 zDW|SC2fcJythHcT4A${sBEduS2v;bsUB>g034D-yS(v}R{iUMf?DHWjXKz@Y<2RnA zPTyvne*@i)4h}@@)UfFSS?vAQ&UOYu^$j@`fk8_wX{xleG?}C5Z9*t9{p;*D&qL~o z1h+~P;6pYFY&HzVIC%2g+Oj~rrV!N(p3hFpO>|RP33TEyxW&N#9L^kXjzOpgm`k-5 z=;L{kc?(t0_dTl&U>rPreEYdFttz!PO!4AlLBL`Rh)nFL<>lp>FsLf07Q^7T0e3gk ziHx!`Vz;f|PjRGZawUA55?_w#)Rh}GJ0rbNZC{k0|L5E?FZcO_$FhV~Aq;i6_jpwz zU*n$kRjNIY*%;O|Hinq_onktzb@_KIG`p;sUU8E!4shK4XR}k(7ku*LaQ>~Jc~&Ot z!2~)k&j+^{EVL67bJtqSftWk7xhklj*mk09S>VI{^e+1)67-)r8=_;8f6Xup_|W6* z4rf*P_)M+ZsCj`iQV^3KRkZ!rM2_nFpJU%^>f&i$l%S$xMfp=^&k+A-`PEw84)dH@ zFW>(}{bkH3h6S50QKPAL6V*8(MS=EA6Ryd>4M}NtS?fX$kN@ARj7sgzO}hB~?KneH zT>O(l9F=sAn8yq-UpRX3bJcX5dMiI9DMKWkr-7(gZ?#YiN(yF5_(BjTFK;P*z*Vq{ zipt+n;?iGJ1+oJ?X6n;`*D;A9gyzW~_m=9qepmsf4h#tLatf(~Vo1OR3xwftaD#aa zgq1aRC~C$)5ePA!qoFuoVemGX%v4tl2fHi0b3fyQ$|>}z&O0J8XVCQQ&yi5Ak*e5o2w0tNHTui#r3`1ys#EDM`q`>L z-3#HNvHVIiON1usFswj^-EcA!T=-mrypvx6L>WK%nak8ziNa__ZCxi}sG#oJiF*&fB{xUa*Ws^Z`ydZPpcqGa zPck78P!j3qx`fAAYDdV*iq8#aN{Jq35-1c}ApK8mz5id-mW&xWanF)5=}hGWQIdXC z2afhh4Iu<{Z$X!y6_+$X@%234ca@`XpU#)=(*NHPHInGCx3wC4358HkM?3rYa~yiq zO&0q6GQGK*mMF6Txs1$`Ql*3wsZ0;|!AR+#S^LqzF1A`EydqKFbHV!3os&y2w4&*_ zVEE$%nSY!l4IV6nFvvOKCUITZAyFFt+uP5od1kf58n2I-Pp2#qd>6yvMYOE3?}mma z^{E;0X<0;pe8LJF_o%%5^TXH*JP~{*Ch|GJ#E_~F=Mzr1fgzwTsnlWQVj{l0Gz3&Y zl2Q{dBm**S5;d(qLGYxGX92P`{aDABIte4i;<#Q`V2u^+$OOZ|w|5w!P?{8ZzMbR| zef~$m#!HIG+W)^vn;ACUVo}o4fRhAaomnsuZt$@6$8qkMEL1K6JIogcM?1(p#VJ^6 zsj<~Rs4-|c^ap1dXvUU5n?LXWlU9#)&cK2XF z@#joSdlxA1>ofeY$)oQ^ioOmj-}IWSDNW9J>kI0~XCnIYCiM+Y*l5cV`XLK z>93E!S-*q`f6#drwV5RNPwSFM4QHD&tsh28p}c<)IxIt6>ONu}2j(j5PXq zPu0q&v5qB{9Lsw4dUDLQ6ZfPf7zF@Q;Td8wi2}d@PF_W~lR4sq@|bUK5VnyBo{46MgOMj3 zpPZOe!lAEX+ZRQVllzK`jn6*&KdI#u7;&+p6U9(snY* zsl;lwUr2msIaEWR$34>fUGp>x)6zG$@mU2L#@K96wf+xkCF;$Kjiu?&vP`oR!S5t5ll+&TqFW!9MU6B^06z8IP=}3iV4oG3a$i7?cqHaq=%z3}YmCnxViMWxB0xgH z55BI4iS@9`krhO)0q}(5}cH@~5 zJ#twVbK`FdxqgoDQOsL^kuT>3g5ib0Nj)Y66i`T)&_;)M6v+05F{Ye~;?v>(6w2_+xjmKmYF{ zU+t4fDgfFZp};;ggU6)d%(Y~WQ4^>O$aU@da=KEwodfTAu zI0pb?n5?HFOEN$xwHmPJnM{r`C)`$Op4%dJP#=L+TVkuJ9l0EoE_ zj3lBv01aFdGS>nq&alm$!)dWrdgyaQL+V0}nFcnUJITfIT3!SZokNQP4-Wx=q{F0D zUN^grX9;hvOWBcLyLYANYq=z)<^X%&BAWC*3;(vQm1O$@F0#Sq`+|4$o$J#>HA#z_G;^+ zOe~IS$Z&=2WG>Qkf|bj^Tw15>%NGzL;^6bK@_is8dA-zbDjzji)WOj){1~l!OQ!=$ zy2POLOtR|f{wT-LOQ)29i5zkJ?V%(gucIISchmwf9Ep=2boWJz$d~)S&vr)lI{r)) zX-MAb5~wueI)zoddjAs=g05fkV$Cll*Z(gR>B4pa`6QwydTH3`u+tRH;Y**@S12Fb zudztZS|}Zgg?qM+yT@i9Ld9U4h^{0pkE6fEkPeo5;(K}2DDjphNokg8JEGeaj}oXP z6qO)3sb38zucYl}NH>25>6wFUI2i%wJOQ#RM*VCzOo>H(f$G$Q1 zPkZKjz<;w_^zrStdfUlo4&T1yTwNhWr=*G20{&Ik%70REn`dW3*K37$!0zB??PsQXO3R^-2b|6ZuL=J$ut#A9BM349!a&y(+a z)Eddj$+T*iIl#t=;LdZ3{#(SZ{lqM>yamI z8xb_StG>q+=rqTJ4&eK!kpOPK7h{=%k>(C=Tg6x$J=^5}mitwnImHOauCF;OV-)jS z_D2n+VBO*yzmLsx zjYwRs&pwCP*%0&IAdSnDl;0tk_;xF89;qoQ^~X<@7$qh1zI2?awCxtg?si_Tk{_El zo+9yiqoRv-O4XSjT))yOdaZsaWhnY6d2f)2xjNvBvlD6q#Dn+2#6xI-=uedqc8j%) zy>S@Wt9O@c8km@vxXUw|A$D24ZMawobp&YfMU9W-|yZx`t-wEBoDF z|4LAjQDE$Sn;2uqLV{KM)ddovuKh%r*#RR7@@0lM?Mqf+%IVQp@xdXqfXF(luQP|u zYdoyhRhtD!rf2k(r@*=t-=2Q}Z0+wgtN9FSw7Gn+*JI_4Be8-3DGCKS zEY|D7LZ%IvY(UkQ%{SM{U=s3#D8fZ3X&-Ss8f(LF3LCjlft(Y@o{8J=bL1G-8UcjO zU{7jG>cc5@8odlc2b=XxV%W+&D-Iet7)HUOli=PTD$3xM*qMWohb2Tvo|Ty6qqZae zz&}{<`4f>`RJ=`$mz>oY8xkC#Q!+<1ZPsjP!88VAp+%!ca?0vhG0yK$G2;{1iMG_; zD7Mf+)aoj4AMVy3C>Xu@gzv5=16OO-7^v-VmEcw8V!`PrOO7?u#mXr}Jvihd*<)Xy z`BpKc1JUO=gDvwZ-p>lfXCLJ3Xs6uB8c9<|tD9FJ@~>skB*6#>kt5NEtNiN7iN;MCn+&{v&)w>!My^jnnOSD5kilDW?9hawg+G69efC4-eB(<*`5 z5wVBcKjaFU<+MM%kLxF2t1c?QP1c9{M)2;*&VL=$PnN9R9eIU-L&~7$+QGa^FA6>; zv$(I}8oqv+xil>z1>$4G%B13-mPz*<;xiRV9zo!!0pTKZxF9E7p1Snm}J3 z^e!1FBfPSpk=C5@<3latJ<)-{`J@|&oumUpHtTCE)z-IUmW#Hf!k%If`Q7fq%lJyn zO$nBGU@uru-;+UFEn>hi@#_fLtGZd{8+@pM{go_r!VptH9;Of~KlG^apd2`@jP#Z9>Ql^1AwB`-xa1@F4q=i0bZw?=IBs7HQONybt#Ws|N) zb1hjSI!a|>kv31GWBp?&@O_9JBOAWL*JWWtfEX+jWDU*zDgxd0#G&V<9jz;Hgx%b+uogb2f7 z+6DF&w9*NlY>LW64DIZ!Pp43f53)8x5GYZF##3yQLrfJxpAlwPi6nX@e@Ge8a6DIZ z$&D&b&=h7%H7KXw>T^&`xWElJGNxAkF*QcHFG`Yeaf$cyu7$p`#xSlJG}ZlWjlhY9 zk!qnSc8~B!%?sRutCCaTar_)sZu-yds7z}XJgnK5@sfM`GbW0O!BW&yLhg6k-jQlL z6{Ic?K;OfE%r6iYuhEod=Kab-+fDvF zaeOLPR`G`qTE?#eC88@duTPOUW#}-nP8X1;5<5$&{)Ic9OPfS<#iS(lH4^(QN7U6) zCFJSBB^d6)2gT6g*_74H%;oFtk?gJ1^py#_GjcwM{4Fs;PL-p3$a00TqLNdw+2ZtM zpsuA(yrz-#)0yxgy={hr(b$>k3g3xmV%9Eqi749&c!|{oMY+pZW*&@3|CqqiGPx`8F-SFe6gj@b((WOIvN=_sY>2<Ye0$Se8Sc!s4Vo)S3L~uA+jk8UzHzno8nW`@5n08|M zL>CJyfkZ1Gba!L>0}ouT7O9r*U3jXG!AXxb2AaYldrteVJS+8z9nWPaqs)DUS?_9j zCWR0vYN((7I{lhQj5!LpW@QDRyKnacHC32YcKiekxp( zpreABnIz(s!JJfD5*Wzc)T_>Bd_TCx9$ibYE12>yA6G6&h_t(Gr zWXz$P^I46xX1=M+FgW{d(x(bgdKw<%#d}tKCeokm$G%OnM4aCItv!ChxXe#o%MNz* zwFJ*1q<_=}Z!PKJUSgf^&&NLVKi6uz+Kjy%O%RtNIsV9H?JDQn#ya$n_i$^g9rwD4UfLfYP!aYC*Hs>Tgx)JbSBL8r!;nhO5@LiJnwx*!s}ZDQ9ZVl!c^sPH z6nJ|#(;`h2sOs5S7SkhLQSk{ET7>iSaay~NRm1e`-3`q6?Y~Cg&(6+ptP`;;ktrq% z=znHk{>rx6D@}w2zb|;FL+yOs{)mJ2D{D&TE8omM;xo29KY>mFK5eFVJJxij-(P)| zjHz@y4Q;CdK#FV99Cz~%naj1n?v@PNFw%bn%?z8&eg4~}IiA!2=8xrRKM-&w&1RR%l^N)0IMH1 zpB9EmDM6vo3g=}X9v6*1mu?tVYY<=1I1s1vJBXRPWND#Rs{I_padlxhB9-mxx`zHL z)VZ4~vmsz}M32PA@R)?YKq4yvm#wfq2fLBSbi$V^4`x9Ys5#=R#INNhCvs|xBc;f6 zO-)Uy?wk3VT|l_x&7e&pcM=>NRENLY!zuRrbLB`ZNte}Er_Kk0#uoV6%I5@ccAmW- z;}7F{Cr!n-cdXG3u8nTAj0=?P{H77JNH0J~j)>P)SPwp;}I2`H|G!6J*kdp^T&0JpYg<8ii50 zCpURw@tE~FcU{}K_Ao;9PlpdXw|j^Xcriaq#Z4{$v}PIu3-eqeNFH{@N)&$-;N*719#Q0+shG@t-&;dD238QR86k^+VO6+9y zG1b_fTJH@l2NLCkuJ<}Wd%YDs{j4@GM@1dpCTIM(;gC8>WPB&N5k|fXfa20+m+qXA zkrBBTp8I$u5f-pQzVc(s|J9ESFvodKn*_gncs;tP*5+K!mvo76|^B(@xNR_ zc4Io7rRu1Y9s}TO2lq}>zpENXRTv*tMI-F+q}rriUk=WQgMLwzcj9XCBRxHR=DTek z_gGofZeJn#p;Q?C(oYXPb&q!X!w!Z&`CzBX}yN-_iSC&|UWsoIT+UTjc zXBE6>U8;LChD%PDOsS7&da2V!H-L)4>*+L0;PUrc_VEa9eD?~}!6rqDxcr+k3!jwx z;?+*y*wLyltv{)e-4^P|V@!O|)ZfEp`}5stq#uSLlbNitgHONh!z@S|GK;Owm z-`-=ghL4jdG19t~AdVA^uVg3A`bRE+9dbVGXQZE{da&4U5cYupl{D(UubUpoJ%K_( zIRiEc1?X9x-&wRBWv+DJ!CZL?p2)XJy@HuCsW-D;EASvnl9!07=P|m*B1KuWhhp>F z*wOP#e{}fh!D`O(cQe-Mu6!mM)tJ2r^S5}-nJgd!0Qcrg&kxit2@22G` zaZ4ql%9T@CLzJZcmJx5UCeiQzJMPxlU!Gya-_wYFF59|}@kq&OS~WGGNm^vEzNf;bz=1e7jLr{FB);bU#k-nqHCDZuodB5Om8 zPn~B=)Gi4%AjT)b>#vUi9Gc2!LF=x z|FWDecbyF`sjY%FmSM;ssP~KScXIao$L(t;A zbYHL}`cKyXatR&w239}p5~kno+u%QCL}#$W5Ox7rLCo8rKjglYkWYLE1VdLHbsN6r z3!aT!f)P#Quq72{Y>i}v32N_#lop-isl7&3J!=-zjQ>VJtTBNEhYyN z{89)F{t$#IgaN0J1~Fj6?fJ@?U{6cfFqmH5p7dFwsXysQi6`lzgV&tUP~-c(I>`dcobZ2Z#;whO11&+$7VH9h4%O3$J)A`6HQao139!aT2Y4BBfIUXj62XR&B) zhee$C4kG^gnvdMDSeOg%{oG#CZ8R%2P4k02mG0_2Q{*_gJXtk#LhSl%vn{Ue4z+TUlF-47;^DT6~R z4I=Fh`jL_5G|0W%TGWqY(qmrqYsMDX?bRt#5>=4;)--y8d-kan>seVttoz5f7!0UE z_z>3cIZ(On+a|Rx)HET-Hx4O*K2J`gu;_V$QJ*=CaY*!!I!o9w{oQ9mZgh<47FVlL zhw4Gf{hk*gX&q_woJlUDFtbqYbd~n2Lzlo$O>Z#6a=|BzQk6>S?g`^KBU15};_hff zNG4_o+<=2krLFy_h{+0`Kr-=(-69ZUf>_Z1HkN*|T%DX~)ZhZGaw;V}O+V?#p@Eq^p%i8&_Otgcci~G8ng+H=UK_XzwWg1}IHc^~{I4uuqZ2ulcB)*|M;>l0(f$9dR z+@(1)bLl(gd;$5-mD#eYAboOegE!S1_x%A5ohp?$?X6h8)MU?BTPV^nuE%sM5$t|;!$_{WX~Ms6)i)HS8rPLe&9OMfr;@|d$T>~uG^MZP zW_wp1DPr>yQj|yrbM{2an67^~BZXE>B}eeNp)5M&q3(mp^k@Wzbowj_IE@@SB^cGi zP0Lmkq>t>V38}99v-ZzLDdG*Vq%r603D+s&!#Rg!cCr7u>!WgQ3b zY#F~)#*xWl|9ZdJ{rsKr)}mMYPboi~P%J_f8m1Tg(JV%p0`?eNzcmlnJ!e$P)hyNV zi_GS|5Sjy^mzSa1Ik&Ety)zM}cDjkxvbl2H3Z zTp>In)gdWp-w)vum&9rjj-UR#3tF3Zx6 zqn!PD;$CkRxwC{_jTaUU#qHIgnDg@sTE7mO<`7VwL&URJ+#2F8h&VAh3#3aw!i|TE z1nY0B!E+KUKi>mMw>#zHVw4X`6-U=zQv$3Ic;Xpmh%Kyu`rK1Z!wrpdNdFkk)UYk7 zB(OP*+QxcDiy6O6vD7mGd*Ez-=X}FUwf~D)IejB7qSvs%zgqK}lx-MgMk)xm zfiNd^UzSsX5kD;t87*j}E~R*BU(oO2JdNdZQ6D~6Ce5lQ)ETJ#Fgkz)ne|JLVFE-Mhhh#kR_(r^#Qeg46 z&{k&r^@^fQUumi;inffLv!aC2{Cd&E8u+dHVbiQA)I70IBk7=!3I1|Ibc>+<@P};# z^;&!D*~$klk*PUekXBI@M9kXQjPso|2tW443~(t%Oygw(?}djnNL4>=c5V_hbChK$ zwlnowD8Dod{Mxi_o&s7HwJfBS|LWyNZB?qQUZ(VC(h$CPX_ZD!nj&G1G7rNtmNkgM z;w-+nl<;}z!@KMd_UwbA#87WzQ^6fZWuiC$1JrB`}=)tI^{C-Pkzit0II zoxYvE=g2T;a5}%!l92VbE~gMd5v(99`{0Jd(ls^I+uf@%^n-=#)6R=|%K~V&yB+<= z@n$v{l#xEWY&M@Vplo%cX_4?gWdR{fasHPwHq6HL3@)?3+*#xX_BZM17};%kv*;`}^L{ zC8@>6jP5?z@Q}h>k-(rkA>j1X^x1~YSj+^rIL=};N?4tr8m8JmhdPE>azzSFdP2n2 z7xz+Tgl3M@mcpnGA!mBp3-Sr^;|{Am0%6y$FCD{Ihtw~|`z5h0%!BjY0zMhC>^4kN zLCYHOn$e*|o6;t8)0(lWA~Wr4bBeNRR3=g%a&jC&s&CR}1G~yUMu<7XP%k_;9@q_y zq>lqk=m~`_V5k%h@P+_mD@kgV=lJKCS{kNwx?d4zHb|{%QH#|9OdV$-`Gxh)q?`#@ zHv31FqVpT01g8{!L~Tq9zpGg_ZT}syb8)}F@}7dv@v;v>)Ki!7;$Zs~t@HPNH=l>) z&xI3K3g;z$lm+!%3wkrYf|U5cGjgaDc3~3}W@drLK{)u2HN-Nya%#{LociMSxv#Py@6E~d%IM2sRM|_mh z<%eS|Sl1A9OygJf`-IB*QWX?1mkS$hWq9WDF8cyrXIjqwvhrVJkBxLA!EW{TxWw>z zdBzekMr>EpcCdAKdNpV1#{dHb!g_(-p^eV+f4vjJv4wysof41Ah1llZ(Yiic&!We2JSiubEk1wy%*1wX7kE&pE+kU7Dq5LrfZ$z3WV}gPo zZhuK~7Zv}&AlZwPVTM`kEH=F_j?@d?VVH2rDL;5Kz3+D8?b&JL+CR0L>idpnM|@bI z%D_lEZ^Dr08U5>G$_sZu)6nkd*VhH6^;D9gd1pcp!{>U9O+!ePzCW^+QS)d}{e;Mz zGx_=Z%(eT!%?S;P3wWW*ASx2h%=URsz0e69sDYZMI<}Kv-wwl3)oPC#*Fx%S9DOVX zG>&E_)(w%9B7fSZlEvSi3nrrZ5!=?zXP@T0D3>>G$TPHAEB#@vK30st>oD+mbovxp z#*}{>lWh*Y7Thq9SA1z=jleqoc%V!E9`UOp-q-Y~`<)s{^JA{x*>IS@;KpJ)<|t4= zwZF4R=-cls;obPDlRgsS@(7RQSH#5FVv_-dUjMj}B?uJZ`{-tpI8cF5JHI~IX=rXr z2=G*Tb}==jJ<{}zKC@C5#HmQ-dE;}45jzhy(630^KWa_8rgV$wIAmQ|4+ZK;KiO-# z-hYExG-i2NiK~@B8=Q+cB0X{~O+=j=Nwb~7@kyh~^WU6yKW`~QzzT$8 zMLcxbDFYRRpX`2i_>5c50rUJs7B|{hQH*CBu?$)2KIEoWx>7q|YkU;Z@Tb-Po_w7-TR!;)@#-6wZq_? z+^PuuS#gneJ|2>WBoMZg(%Ap8`Vv~TS{vPE(xN^0ve1Qie?J7gkp5=6`Hw!1;M`Dv zMA@f^bVN~80SE||=@%-(jZFCIX>Mh06Xw=fo@CKOh8ChjG8dh`b$biKKz0yn)L_TB zd9C*RWfaXq`Ei3uXF+n8kuG@1(qQe`+%P7ziep)Bjh7R&LbXsT?%}%eVDQFNevQ|d zW607w?`Tfgh;F8xttf!JP^G^1>MeiCh6V(t77O%*r|jL1T_IEa*n)1VEGp z47kZoVki@wzrjv{4l3J0f;~+uZNIKg;BB~`VSPce4L24PHnZL(vAgbBty{z5-{lEN z)l|0zN^mqADh60|bW723{s{ShDQfISTPC2tJE_zYKjgp)@BllmA!Eq)k5E=(rDjL$Y9Tp2MU-C_0Bb0?$Gd5blxxhfSQAYdmY&Ew%N z4f;J=X?Ks%*0O!Lr4?HYSFWsb6C5|vmJ#;w5Mq#w6!XxGJQuOo4%BF-LGW?or=b|= z->Zv?^5XgCkUek5$6Ua>0|KHE!siQ~1AilnwdGr zE14nIU|;U@eD4n-#V7lXGoSH7mm1>{Pm7tQ7JGbd1AQ6c#>TlC$|yxO5O>x$v%gOi z5a!UA&peyVgsh2ds`*~Qf~7Z0<$>Yr?BCxB7wMN*6~=%-!C-+hjT&QzwWXI(DH;P~ zKuJ%2?zGHkk5o2I749&^itWcDmdcA7mKrL$6&47gi^B3{CwdYY|KoqHzcgN*$|2u} z9^6zKa1l^Kul6d>L5P;PA3?7Z&d8}`(B=}@rP{TnVfSx~uvY#o4w<_#iXDG@pd^Xe(M-79W(GMI%| zVg*SWtggwgo|DTW#7w^<22e)%n@Q)nrnJFeZDp0}#RmD*@YJ^U|HoOsH2; zrCVn&Sj}#tmihGQoM1Ceqbm@G^o;D}l{;;w1)I4VJqkX*T{2eVQqk9PGNHQ|+fzAQ zDl2MmtN*R0XTkEHzfzBy8`l4?)entUR&CK7Wr1&3drcaev~dHh=WRGm8}$?AFSB{( z+CBqFO=lj&Y1*kj_r256)?q6IT+yVfN!Sw+5>sQ zo*l4tr_Dkb1j0saEgJdzf$_70K-JogAU(ZL@*pF-i!8uA9Wy58 z+S2|ERdCPGH#p2wZ> L#eam^PGLM!uKOObh8Nu5NEcp>y~USE*G%@M_}`LQPi&y z8{#OKIzLF#j;?3hSb=`9L`Wgz>q=^EZ8N?8`a|lWB{QAK)m=;3{oHqH|FZ^>ndgpM z5pJ|}+H%5;!a^;4AH{6HITDH~e}F2h{hu6b|E^~R4sbBWKL`$TaXs}{;*omcnuiyuBHCCk} z8kACH8W@9~SnF|LTmMRYxH+*f()*? zI$F|!6JKRV;lktZoKX^P+VxA=OD_c!>FW6|Cpmlij~GKN1{qhDoGIo|z#Rs~G>rH{ z6P#h`ZyN3it@U(M^AemNd~&*kp_%!M7_=*{1ve9F|G0vR0f=cSCfWv6Nw|#ESDEq+ zUA;XdYnnSZXUG}NpF7UJ&_iH^grb07iY7nnCV$Qg(Nq{l3x`&_b!f(hFn5_Z%I)mh z{E@5O-f0llce8O?<*4Wl)cCCD{oL0o3zI=V+%5CRG@V~k$(L{gSLAi0uh-eXk?(Rz zV%P98=x1p}-W0}2FBiI7Ah11x8)M#2yo@xL=F5po#-z4wT;S` zh*+f)UyL$k1KY{#Y`6Ez4wY8^noVyE!(9cR;OSO-hrC706mg>7D9qQdPNqMJ+)dHZ zkVyLpoWcP5h|bpCYyUn<{G|fw7SqQ8m*UJjhT#hZXzh_>Pwi6+WVD}G0tN8dOxs*# zYinyEzpHmXN3FlgYHO!7YtCb|!jWA)9#|J@)^C?+?D)VCck>N&Z=2w?g==ua-KVSc6 z8Q)DZFfYLA)H+`6+yh!CrT2ILW`VY@g4de=i^qd9z!4-%IhzU@-HCAq-a%detzW~W zJy(ZzqGRhM@eF0V+X?G0Mr3AnoqaB&V~}dRzk-j z$OmpG9Ccorx`GFRp0b9S|3#Yjn=cIXfw!)Yyh`BMQ_MSBp^@&LbR)9#aCPZFD5Iky zKY+y-I@$H@WSFVvhy*v_^c(J>0^mFg(0hZm06j0`4YGd-2jP&801|~Kuy6^m z0mK*}m!WL}$6N5v*f}@~M4aX>%RiA+I54Nb`RhRLlwn=0!gULf26y3B38dqq03&Lk zd9CBaUE_eXx&cFvMsT>)fPp2fg3ve0=u5wzsDM8;}S1U&ZjnKY#x00Yov1 z?wvK4j#CBxYhHs|Daf36#PBs@l&1*$nRgJ-#8GtVx-K!G3(2(5?}?Q3J~8)sxVtO| zSezJtNc2;Hei*KW$A?GGW@Sy6$|IL=6s^~Ne@?k!qh%|h2*@LKQXhb><*{R*Jf(@8 zY$OqdwZ-Q+F@6RT78Eq0Ox z7E2?N#ORLmDOHPt2r0}8y=oey_eMBnKWao? zzzqj*{4Iup9tSlaQ2Xflf{}|BKqv=JVyW5TzawaLUdehq*)*mE&(7dAQ!mYS!yC_) z_~hW=5N5y8e%wV)U}|WnVl^07aJbwYOBgw&uK7kwE9nFp1m^2y<;15(7oteT>>i9D zq`|0LUs+mOx?3UTqAQj2r0!XPtQcTX*LuT z?AMRK3$WFygA5W&P`(E^cz*&Ay=&h9uCfbq0Hqi2*(Zbbme)X=7^7UsZc54y7k;Hh zba2C`F`oT0rDsl4tETm5mcMnL_hz&VdJ^n(gsbL}ZkJjMuEJnd%X065qkhOId}IrH z*SKyC{@XcC2D@WFW2L)Fg(xV?tLaa%o5zHdc=wYk!n0_S(~AtVeZ>8p&l~ zNP->BO?(&*TWW0CQyj@A!T@_vnPLVX-?h{;XMYuhi&pZy*~877HlWcOo=4EdQ$v31 zV(^_!^~9Btb|tlH^LKQXe$+9^8rwF`!*dw!s9a)lg5a`Cp_G3YK=I?KWjQ$2eu^>W+}P@3V4qlQE(<8+hGus(9WfMzs!huRZHumlI|7d#b0W_ph;615qnW zHS|afD{yL6(@LUXrIzcpcq{eS;Lf4|?a%XrHCFPjr;Fc$bM(%9-doQ#$V5MfOr4k^ z@q#gJG|7I_8d&KLtI}H`+WZ;up{*)?Qm^~(t@JA2Cp;OGIC;q+%@u6;z0=oggI$4z zO#5pBU3dgar0@`&$p8g5=EgQW=;ELcHw`1j*~CFMkA|z=eM1tD3@Afmh@IXIkKD-m4Gu7 zps8->mUVWeU{V*j`HV&&J&AO18VJ=_8Bh*!|F7+ywASq1$ytsf3cM3`NMHJ}O#Pvr zsJ7RHmaJ4xdDeZLF6(|yU9>=yNqfeOL|%s`H;d$BJon4*A3w$8!n0_@Eqle!)o*8C zlJwRDHaU148cj;nG$*i{@T&33PP}z~@$29&RbHPk1zy?Wg@47Co|m4XVUW0@M%Y1R zs<2zg7N?%l8I#ubefe1R<)$L%7S_*l-!3evYMf@YiGkz8ufsY^x%j)UnrztgwK(+Y z*8`kEKLc7f?z*a2a5Il5@kCa`#YGqSlBciM8hFv}A|#|hi})D&NP zv?^Oi{txmJ+Z0%q^Spwchr4~-pF$;@I(0*)vN`uRsye7C@M%sCS>M#85$_|U9llP) z{lE*o2^ac9EKQm_5@*~}dD_c+*eJqn``wx(wB9?vc7Cz!RNK5Y`zLUwG~SO8UA}rJ*N=znvn}5Wv|Lg1c~{IP4iy#wC_ z!za8=8?^Jka8I24!Sm+L;%DM=jljJ*i7PTMn5?!Vh(3 z9kye+(J45$jl=klv{{9U%j}05(>ADku2G1xEVOtjT+rChrxxya(2*gkT!+!&-Vf;~ zy+2R&Rch`FSKvG$qaqlx)}|mu(UB#w!Q##Yw}Ljw?JdAv))I}(f)i8(+2+S5oKy6^q$slThVsv1lW|>>%ToBGcsA&S1TMx2%{_8Ft5%*| z^>?9S_YFUfM-68unDYH|0PO?s;5cxk<$$!OP@+mlM2xei*hblgX3iyyOb7EVW=4GG zc(nX*ZW7C)Zic3;lpi~Kn0Tx$k9cc;Ntu}QW4PuLm|9O?Pm%OTOJ3YT3 zT1rT7&Y_>NpYH$oZ?Npf3cbp{8GoWBejm>D>uKm&#lm^Q!#jijrI46h;oJwVvN8YtXe3O~%e-urV`$TXcYXm|}#$Dkr)EH|%U$^t2Cb%wV(d bpM2V^?Rx*%sy_n{+F|f?^>bP0l+XkK?n-Gh literal 0 HcmV?d00001 diff --git a/docs/userguide/networking/images/working.svg b/docs/userguide/networking/images/working.svg new file mode 100644 index 00000000..f39955e1 --- /dev/null +++ b/docs/userguide/networking/images/working.svg @@ -0,0 +1 @@ +container2container3container1isolated_nwDockerHostbridge \ No newline at end of file diff --git a/docs/userguide/networking/index.md b/docs/userguide/networking/index.md new file mode 100644 index 00000000..eb613c6d --- /dev/null +++ b/docs/userguide/networking/index.md @@ -0,0 +1,21 @@ + + +# Docker networks feature overview + +This sections explains how to use the Docker networks feature. This feature allows users to define their own networks and connect containers to them. Using this feature you can create a network on a single host or a network that spans across multiple hosts. + +- [Understand Docker container networks](dockernetworks.md) +- [Work with network commands](work-with-networks.md) +- [Get started with multi-host networking](get-started-overlay.md) + +If you are already familiar with Docker's default bridge network, `docker0` that network continues to be supported. It is created automatically in every installation. The default bridge network is also named `bridge`. To see a list of topics related to that network, read the articles listed in the [Docker default bridge network](default_network/index.md). diff --git a/docs/userguide/networking/work-with-networks.md b/docs/userguide/networking/work-with-networks.md new file mode 100644 index 00000000..6cd66948 --- /dev/null +++ b/docs/userguide/networking/work-with-networks.md @@ -0,0 +1,863 @@ + + +# Work with network commands + +This article provides examples of the network subcommands you can use to interact with Docker networks and the containers in them. The commands are available through the Docker Engine CLI. These commands are: + +* `docker network create` +* `docker network connect` +* `docker network ls` +* `docker network rm` +* `docker network disconnect` +* `docker network inspect` + +While not required, it is a good idea to read [Understanding Docker +network](dockernetworks.md) before trying the examples in this section. The +examples for the rely on a `bridge` network so that you can try them +immediately. If you would prefer to experiment with an `overlay` network see +the [Getting started with multi-host networks](get-started-overlay.md) instead. + +## Create networks + +Docker Engine creates a `bridge` network automatically when you install Engine. +This network corresponds to the `docker0` bridge that Engine has traditionally +relied on. In addition to this network, you can create your own `bridge` or `overlay` network. + +A `bridge` network resides on a single host running an instance of Docker Engine. An `overlay` network can span multiple hosts running their own engines. If you run `docker network create` and supply only a network name, it creates a bridge network for you. + +```bash +$ docker network create simple-network +69568e6336d8c96bbf57869030919f7c69524f71183b44d80948bd3927c87f6a +$ docker network inspect simple-network +[ + { + "Name": "simple-network", + "Id": "69568e6336d8c96bbf57869030919f7c69524f71183b44d80948bd3927c87f6a", + "Scope": "local", + "Driver": "bridge", + "IPAM": { + "Driver": "default", + "Config": [ + { + "Subnet": "172.22.0.0/16", + "Gateway": "172.22.0.1/16" + } + ] + }, + "Containers": {}, + "Options": {} + } +] +``` + +Unlike `bridge` networks, `overlay` networks require some pre-existing conditions +before you can create one. These conditions are: + +* Access to a key-value store. Engine supports Consul, Etcd, and ZooKeeper (Distributed store) key-value stores. +* A cluster of hosts with connectivity to the key-value store. +* A properly configured Engine `daemon` on each host in the swarm. + +The `docker daemon` options that support the `overlay` network are: + +* `--cluster-store` +* `--cluster-store-opt` +* `--cluster-advertise` + +It is also a good idea, though not required, that you install Docker Swarm +to manage the cluster. Swarm provides sophisticated discovery and server +management that can assist your implementation. + +When you create a network, Engine creates a non-overlapping subnetwork for the +network by default. You can override this default and specify a subnetwork +directly using the `--subnet` option. On a `bridge` network you can only +specify a single subnet. An `overlay` network supports multiple subnets. + +> **Note** : It is highly recommended to use the `--subnet` option while creating +> a network. If the `--subnet` is not specified, the docker daemon automatically +> chooses and assigns a subnet for the network and it could overlap with another subnet +> in your infrastructure that is not managed by docker. Such overlaps can cause +> connectivity issues or failures when containers are connected to that network. + +In addition to the `--subnet` option, you also specify the `--gateway` `--ip-range` and `--aux-address` options. + +```bash +$ docker network create -d overlay + --subnet=192.168.0.0/16 --subnet=192.170.0.0/16 + --gateway=192.168.0.100 --gateway=192.170.0.100 + --ip-range=192.168.1.0/24 + --aux-address a=192.168.1.5 --aux-address b=192.168.1.6 + --aux-address a=192.170.1.5 --aux-address b=192.170.1.6 + my-multihost-network +``` + +Be sure that your subnetworks do not overlap. If they do, the network create fails and Engine returns an error. + +When creating a custom network, the default network driver (i.e. `bridge`) has additional options that can be passed. +The following are those options and the equivalent docker daemon flags used for docker0 bridge: + +| Option | Equivalent | Description | +|--------------------------------------------------|-------------|-------------------------------------------------------| +| `com.docker.network.bridge.name` | - | bridge name to be used when creating the Linux bridge | +| `com.docker.network.bridge.enable_ip_masquerade` | `--ip-masq` | Enable IP masquerading | +| `com.docker.network.bridge.enable_icc` | `--icc` | Enable or Disable Inter Container Connectivity | +| `com.docker.network.bridge.host_binding_ipv4` | `--ip` | Default IP when binding container ports | +| `com.docker.network.mtu` | `--mtu` | Set the containers network MTU | + +The following arguments can be passed to `docker network create` for any network driver. + +| Argument | Equivalent | Description | +|--------------|------------|------------------------------------------| +| `--internal` | - | Restricts external access to the network | +| `--ipv6` | `--ipv6` | Enable IPv6 networking | + +For example, now let's use `-o` or `--opt` options to specify an IP address binding when publishing ports: + +```bash +$ docker network create -o "com.docker.network.bridge.host_binding_ipv4"="172.23.0.1" my-network +b1a086897963e6a2e7fc6868962e55e746bee8ad0c97b54a5831054b5f62672a +$ docker network inspect my-network +[ + { + "Name": "my-network", + "Id": "b1a086897963e6a2e7fc6868962e55e746bee8ad0c97b54a5831054b5f62672a", + "Scope": "local", + "Driver": "bridge", + "IPAM": { + "Driver": "default", + "Options": {}, + "Config": [ + { + "Subnet": "172.23.0.0/16", + "Gateway": "172.23.0.1/16" + } + ] + }, + "Containers": {}, + "Options": { + "com.docker.network.bridge.host_binding_ipv4": "172.23.0.1" + } + } +] +$ docker run -d -P --name redis --net my-network redis +bafb0c808c53104b2c90346f284bda33a69beadcab4fc83ab8f2c5a4410cd129 +$ docker ps +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +bafb0c808c53 redis "/entrypoint.sh redis" 4 seconds ago Up 3 seconds 172.23.0.1:32770->6379/tcp redis +``` + +## Connect containers + +You can connect containers dynamically to one or more networks. These networks +can be backed the same or different network drivers. Once connected, the +containers can communicate using another container's IP address or name. + +For `overlay` networks or custom plugins that support multi-host +connectivity, containers connected to the same multi-host network but launched +from different hosts can also communicate in this way. + +Create two containers for this example: + +```bash +$ docker run -itd --name=container1 busybox +18c062ef45ac0c026ee48a83afa39d25635ee5f02b58de4abc8f467bcaa28731 + +$ docker run -itd --name=container2 busybox +498eaaaf328e1018042c04b2de04036fc04719a6e39a097a4f4866043a2c2152 +``` + +Then create an isolated, `bridge` network to test with. + +```bash +$ docker network create -d bridge --subnet 172.25.0.0/16 isolated_nw +06a62f1c73c4e3107c0f555b7a5f163309827bfbbf999840166065a8f35455a8 +``` + +Connect `container2` to the network and then `inspect` the network to verify the connection: + +``` +$ docker network connect isolated_nw container2 +$ docker network inspect isolated_nw +[ + { + "Name": "isolated_nw", + "Id": "06a62f1c73c4e3107c0f555b7a5f163309827bfbbf999840166065a8f35455a8", + "Scope": "local", + "Driver": "bridge", + "IPAM": { + "Driver": "default", + "Config": [ + { + "Subnet": "172.21.0.0/16", + "Gateway": "172.21.0.1/16" + } + ] + }, + "Containers": { + "90e1f3ec71caf82ae776a827e0712a68a110a3f175954e5bd4222fd142ac9428": { + "Name": "container2", + "EndpointID": "11cedac1810e864d6b1589d92da12af66203879ab89f4ccd8c8fdaa9b1c48b1d", + "MacAddress": "02:42:ac:19:00:02", + "IPv4Address": "172.25.0.2/16", + "IPv6Address": "" + } + }, + "Options": {} + } +] +``` + +You can see that the Engine automatically assigns an IP address to `container2`. +Given we specified a `--subnet` when creating the network, Engine picked +an address from that same subnet. Now, start a third container and connect it to +the network on launch using the `docker run` command's `--net` option: + +```bash +$ docker run --net=isolated_nw --ip=172.25.3.3 -itd --name=container3 busybox +467a7863c3f0277ef8e661b38427737f28099b61fa55622d6c30fb288d88c551 +``` + +As you can see you were able to specify the ip address for your container. +As long as the network to which the container is connecting was created with +a user specified subnet, you will be able to select the IPv4 and/or IPv6 address(es) +for your container when executing `docker run` and `docker network connect` commands. +The selected IP address is part of the container networking configuration and will be +preserved across container reload. The feature is only available on user defined networks, +because they guarantee their subnets configuration does not change across daemon reload. + +Now, inspect the network resources used by `container3`. + +```bash +$ docker inspect --format='{{json .NetworkSettings.Networks}}' container3 +{"isolated_nw":{"IPAMConfig":{"IPv4Address":"172.25.3.3"},"NetworkID":"1196a4c5af43a21ae38ef34515b6af19236a3fc48122cf585e3f3054d509679b", +"EndpointID":"dffc7ec2915af58cc827d995e6ebdc897342be0420123277103c40ae35579103","Gateway":"172.25.0.1","IPAddress":"172.25.3.3","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"MacAddress":"02:42:ac:19:03:03"}} +``` +Repeat this command for `container2`. If you have Python installed, you can pretty print the output. + +```bash +$ docker inspect --format='{{json .NetworkSettings.Networks}}' container2 | python -m json.tool +{ + "bridge": { + "NetworkID":"7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812", + "EndpointID": "0099f9efb5a3727f6a554f176b1e96fca34cae773da68b3b6a26d046c12cb365", + "Gateway": "172.17.0.1", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAMConfig": null, + "IPAddress": "172.17.0.3", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "MacAddress": "02:42:ac:11:00:03" + }, + "isolated_nw": { + "NetworkID":"1196a4c5af43a21ae38ef34515b6af19236a3fc48122cf585e3f3054d509679b", + "EndpointID": "11cedac1810e864d6b1589d92da12af66203879ab89f4ccd8c8fdaa9b1c48b1d", + "Gateway": "172.25.0.1", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAMConfig": null, + "IPAddress": "172.25.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "MacAddress": "02:42:ac:19:00:02" + } +} +``` + +You should find `container2` belongs to two networks. The `bridge` network +which it joined by default when you launched it and the `isolated_nw` which you +later connected it to. + +![](images/working.png) + +In the case of `container3`, you connected it through `docker run` to the +`isolated_nw` so that container is not connected to `bridge`. + +Use the `docker attach` command to connect to the running `container2` and +examine its networking stack: + +```bash +$ docker attach container2 +``` + +If you look a the container's network stack you should see two Ethernet interfaces, one for the default bridge network and one for the `isolated_nw` network. + +```bash +/ # ifconfig +eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:03 + inet addr:172.17.0.3 Bcast:0.0.0.0 Mask:255.255.0.0 + inet6 addr: fe80::42:acff:fe11:3/64 Scope:Link + UP BROADCAST RUNNING MULTICAST MTU:9001 Metric:1 + RX packets:8 errors:0 dropped:0 overruns:0 frame:0 + TX packets:8 errors:0 dropped:0 overruns:0 carrier:0 + collisions:0 txqueuelen:0 + RX bytes:648 (648.0 B) TX bytes:648 (648.0 B) + +eth1 Link encap:Ethernet HWaddr 02:42:AC:15:00:02 + inet addr:172.25.0.2 Bcast:0.0.0.0 Mask:255.255.0.0 + inet6 addr: fe80::42:acff:fe19:2/64 Scope:Link + UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 + RX packets:8 errors:0 dropped:0 overruns:0 frame:0 + TX packets:8 errors:0 dropped:0 overruns:0 carrier:0 + collisions:0 txqueuelen:0 + RX bytes:648 (648.0 B) TX bytes:648 (648.0 B) + +lo Link encap:Local Loopback + inet addr:127.0.0.1 Mask:255.0.0.0 + inet6 addr: ::1/128 Scope:Host + UP LOOPBACK RUNNING MTU:65536 Metric:1 + RX packets:0 errors:0 dropped:0 overruns:0 frame:0 + TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 + collisions:0 txqueuelen:0 + RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) +``` + +On the `isolated_nw` which was user defined, the Docker embedded DNS server enables name resolution for other containers in the network. Inside of `container2` it is possible to ping `container3` by name. + +```bash +/ # ping -w 4 container3 +PING container3 (172.25.3.3): 56 data bytes +64 bytes from 172.25.3.3: seq=0 ttl=64 time=0.070 ms +64 bytes from 172.25.3.3: seq=1 ttl=64 time=0.080 ms +64 bytes from 172.25.3.3: seq=2 ttl=64 time=0.080 ms +64 bytes from 172.25.3.3: seq=3 ttl=64 time=0.097 ms + +--- container3 ping statistics --- +4 packets transmitted, 4 packets received, 0% packet loss +round-trip min/avg/max = 0.070/0.081/0.097 ms +``` + +This isn't the case for the default `bridge` network. Both `container2` and `container1` are connected to the default bridge network. Docker does not support automatic service discovery on this network. For this reason, pinging `container1` by name fails as you would expect based on the `/etc/hosts` file: + +```bash +/ # ping -w 4 container1 +ping: bad address 'container1' +``` + +A ping using the `container1` IP address does succeed though: + +```bash +/ # ping -w 4 172.17.0.2 +PING 172.17.0.2 (172.17.0.2): 56 data bytes +64 bytes from 172.17.0.2: seq=0 ttl=64 time=0.095 ms +64 bytes from 172.17.0.2: seq=1 ttl=64 time=0.075 ms +64 bytes from 172.17.0.2: seq=2 ttl=64 time=0.072 ms +64 bytes from 172.17.0.2: seq=3 ttl=64 time=0.101 ms + +--- 172.17.0.2 ping statistics --- +4 packets transmitted, 4 packets received, 0% packet loss +round-trip min/avg/max = 0.072/0.085/0.101 ms +``` + +If you wanted you could connect `container1` to `container2` with the `docker +run --link` command and that would enable the two containers to interact by name +as well as IP. + +Detach from a `container2` and leave it running using `CTRL-p CTRL-q`. + +In this example, `container2` is attached to both networks and so can talk to +`container1` and `container3`. But `container3` and `container1` are not in the +same network and cannot communicate. Test, this now by attaching to +`container3` and attempting to ping `container1` by IP address. + +```bash +$ docker attach container3 +/ # ping 172.17.0.2 +PING 172.17.0.2 (172.17.0.2): 56 data bytes +^C +--- 172.17.0.2 ping statistics --- +10 packets transmitted, 0 packets received, 100% packet loss + +``` + +You can connect both running and non-running containers to a network. However, +`docker network inspect` only displays information on running containers. + +### Linking containers in user-defined networks + +In the above example, `container2` was able to resolve `container3`'s name automatically +in the user defined network `isolated_nw`, but the name resolution did not succeed +automatically in the default `bridge` network. This is expected in order to maintain +backward compatibility with [legacy link](default_network/dockerlinks.md). + +The `legacy link` provided 4 major functionalities to the default `bridge` network. + +* name resolution +* name alias for the linked container using `--link=CONTAINER-NAME:ALIAS` +* secured container connectivity (in isolation via `--icc=false`) +* environment variable injection + +Comparing the above 4 functionalities with the non-default user-defined networks such as +`isolated_nw` in this example, without any additional config, `docker network` provides + +* automatic name resolution using DNS +* automatic secured isolated environment for the containers in a network +* ability to dynamically attach and detach to multiple networks +* supports the `--link` option to provide name alias for the linked container + +Continuing with the above example, create another container `container4` in `isolated_nw` +with `--link` to provide additional name resolution using alias for other containers in +the same network. + +```bash +$ docker run --net=isolated_nw -itd --name=container4 --link container5:c5 busybox +01b5df970834b77a9eadbaff39051f237957bd35c4c56f11193e0594cfd5117c +``` + +With the help of `--link` `container4` will be able to reach `container5` using the +aliased name `c5` as well. + +Please note that while creating `container4`, we linked to a container named `container5` +which is not created yet. That is one of the differences in behavior between the +*legacy link* in default `bridge` network and the new *link* functionality in user defined +networks. The *legacy link* is static in nature and it hard-binds the container with the +alias and it doesn't tolerate linked container restarts. While the new *link* functionality +in user defined networks are dynamic in nature and supports linked container restarts +including tolerating ip-address changes on the linked container. + +Now let us launch another container named `container5` linking `container4` to c4. + +```bash +$ docker run --net=isolated_nw -itd --name=container5 --link container4:c4 busybox +72eccf2208336f31e9e33ba327734125af00d1e1d2657878e2ee8154fbb23c7a +``` + +As expected, `container4` will be able to reach `container5` by both its container name and +its alias c5 and `container5` will be able to reach `container4` by its container name and +its alias c4. + +```bash +$ docker attach container4 +/ # ping -w 4 c5 +PING c5 (172.25.0.5): 56 data bytes +64 bytes from 172.25.0.5: seq=0 ttl=64 time=0.070 ms +64 bytes from 172.25.0.5: seq=1 ttl=64 time=0.080 ms +64 bytes from 172.25.0.5: seq=2 ttl=64 time=0.080 ms +64 bytes from 172.25.0.5: seq=3 ttl=64 time=0.097 ms + +--- c5 ping statistics --- +4 packets transmitted, 4 packets received, 0% packet loss +round-trip min/avg/max = 0.070/0.081/0.097 ms + +/ # ping -w 4 container5 +PING container5 (172.25.0.5): 56 data bytes +64 bytes from 172.25.0.5: seq=0 ttl=64 time=0.070 ms +64 bytes from 172.25.0.5: seq=1 ttl=64 time=0.080 ms +64 bytes from 172.25.0.5: seq=2 ttl=64 time=0.080 ms +64 bytes from 172.25.0.5: seq=3 ttl=64 time=0.097 ms + +--- container5 ping statistics --- +4 packets transmitted, 4 packets received, 0% packet loss +round-trip min/avg/max = 0.070/0.081/0.097 ms +``` + +```bash +$ docker attach container5 +/ # ping -w 4 c4 +PING c4 (172.25.0.4): 56 data bytes +64 bytes from 172.25.0.4: seq=0 ttl=64 time=0.065 ms +64 bytes from 172.25.0.4: seq=1 ttl=64 time=0.070 ms +64 bytes from 172.25.0.4: seq=2 ttl=64 time=0.067 ms +64 bytes from 172.25.0.4: seq=3 ttl=64 time=0.082 ms + +--- c4 ping statistics --- +4 packets transmitted, 4 packets received, 0% packet loss +round-trip min/avg/max = 0.065/0.070/0.082 ms + +/ # ping -w 4 container4 +PING container4 (172.25.0.4): 56 data bytes +64 bytes from 172.25.0.4: seq=0 ttl=64 time=0.065 ms +64 bytes from 172.25.0.4: seq=1 ttl=64 time=0.070 ms +64 bytes from 172.25.0.4: seq=2 ttl=64 time=0.067 ms +64 bytes from 172.25.0.4: seq=3 ttl=64 time=0.082 ms + +--- container4 ping statistics --- +4 packets transmitted, 4 packets received, 0% packet loss +round-trip min/avg/max = 0.065/0.070/0.082 ms +``` + +Similar to the legacy link functionality the new link alias is localized to a container +and the aliased name has no meaning outside of the container using the `--link`. + +Also, it is important to note that if a container belongs to multiple networks, the +linked alias is scoped within a given network. Hence the containers can be linked to +different aliases in different networks. + +Extending the example, let us create another network named `local_alias` + +```bash +$ docker network create -d bridge --subnet 172.26.0.0/24 local_alias +76b7dc932e037589e6553f59f76008e5b76fa069638cd39776b890607f567aaa +``` + +let us connect `container4` and `container5` to the new network `local_alias` + +``` +$ docker network connect --link container5:foo local_alias container4 +$ docker network connect --link container4:bar local_alias container5 +``` + +```bash +$ docker attach container4 + +/ # ping -w 4 foo +PING foo (172.26.0.3): 56 data bytes +64 bytes from 172.26.0.3: seq=0 ttl=64 time=0.070 ms +64 bytes from 172.26.0.3: seq=1 ttl=64 time=0.080 ms +64 bytes from 172.26.0.3: seq=2 ttl=64 time=0.080 ms +64 bytes from 172.26.0.3: seq=3 ttl=64 time=0.097 ms + +--- foo ping statistics --- +4 packets transmitted, 4 packets received, 0% packet loss +round-trip min/avg/max = 0.070/0.081/0.097 ms + +/ # ping -w 4 c5 +PING c5 (172.25.0.5): 56 data bytes +64 bytes from 172.25.0.5: seq=0 ttl=64 time=0.070 ms +64 bytes from 172.25.0.5: seq=1 ttl=64 time=0.080 ms +64 bytes from 172.25.0.5: seq=2 ttl=64 time=0.080 ms +64 bytes from 172.25.0.5: seq=3 ttl=64 time=0.097 ms + +--- c5 ping statistics --- +4 packets transmitted, 4 packets received, 0% packet loss +round-trip min/avg/max = 0.070/0.081/0.097 ms +``` + +Note that the ping succeeds for both the aliases but on different networks. +Let us conclude this section by disconnecting `container5` from the `isolated_nw` +and observe the results + +``` +$ docker network disconnect isolated_nw container5 + +$ docker attach container4 + +/ # ping -w 4 c5 +ping: bad address 'c5' + +/ # ping -w 4 foo +PING foo (172.26.0.3): 56 data bytes +64 bytes from 172.26.0.3: seq=0 ttl=64 time=0.070 ms +64 bytes from 172.26.0.3: seq=1 ttl=64 time=0.080 ms +64 bytes from 172.26.0.3: seq=2 ttl=64 time=0.080 ms +64 bytes from 172.26.0.3: seq=3 ttl=64 time=0.097 ms + +--- foo ping statistics --- +4 packets transmitted, 4 packets received, 0% packet loss +round-trip min/avg/max = 0.070/0.081/0.097 ms + +``` + +In conclusion, the new link functionality in user defined networks provides all the +benefits of legacy links while avoiding most of the well-known issues with *legacy links*. + +One notable missing functionality compared to *legacy links* is the injection of +environment variables. Though very useful, environment variable injection is static +in nature and must be injected when the container is started. One cannot inject +environment variables into a running container without significant effort and hence +it is not compatible with `docker network` which provides a dynamic way to connect/ +disconnect containers to/from a network. + +### Network-scoped alias + +While *link*s provide private name resolution that is localized within a container, +the network-scoped alias provides a way for a container to be discovered by an +alternate name by any other container within the scope of a particular network. +Unlike the *link* alias, which is defined by the consumer of a service, the +network-scoped alias is defined by the container that is offering the service +to the network. + +Continuing with the above example, create another container in `isolated_nw` with a +network alias. + +```bash +$ docker run --net=isolated_nw -itd --name=container6 --net-alias app busybox +8ebe6767c1e0361f27433090060b33200aac054a68476c3be87ef4005eb1df17 +``` + +```bash +$ docker attach container4 +/ # ping -w 4 app +PING app (172.25.0.6): 56 data bytes +64 bytes from 172.25.0.6: seq=0 ttl=64 time=0.070 ms +64 bytes from 172.25.0.6: seq=1 ttl=64 time=0.080 ms +64 bytes from 172.25.0.6: seq=2 ttl=64 time=0.080 ms +64 bytes from 172.25.0.6: seq=3 ttl=64 time=0.097 ms + +--- app ping statistics --- +4 packets transmitted, 4 packets received, 0% packet loss +round-trip min/avg/max = 0.070/0.081/0.097 ms + +/ # ping -w 4 container6 +PING container5 (172.25.0.6): 56 data bytes +64 bytes from 172.25.0.6: seq=0 ttl=64 time=0.070 ms +64 bytes from 172.25.0.6: seq=1 ttl=64 time=0.080 ms +64 bytes from 172.25.0.6: seq=2 ttl=64 time=0.080 ms +64 bytes from 172.25.0.6: seq=3 ttl=64 time=0.097 ms + +--- container6 ping statistics --- +4 packets transmitted, 4 packets received, 0% packet loss +round-trip min/avg/max = 0.070/0.081/0.097 ms +``` + +Now let us connect `container6` to the `local_alias` network with a different network-scoped +alias. + +``` +$ docker network connect --alias scoped-app local_alias container6 +``` + +`container6` in this example now is aliased as `app` in network `isolated_nw` and +as `scoped-app` in network `local_alias`. + +Let's try to reach these aliases from `container4` (which is connected to both these networks) +and `container5` (which is connected only to `isolated_nw`). + +```bash +$ docker attach container4 + +/ # ping -w 4 scoped-app +PING foo (172.26.0.5): 56 data bytes +64 bytes from 172.26.0.5: seq=0 ttl=64 time=0.070 ms +64 bytes from 172.26.0.5: seq=1 ttl=64 time=0.080 ms +64 bytes from 172.26.0.5: seq=2 ttl=64 time=0.080 ms +64 bytes from 172.26.0.5: seq=3 ttl=64 time=0.097 ms + +--- foo ping statistics --- +4 packets transmitted, 4 packets received, 0% packet loss +round-trip min/avg/max = 0.070/0.081/0.097 ms + +$ docker attach container5 + +/ # ping -w 4 scoped-app +ping: bad address 'scoped-app' + +``` + +As you can see, the alias is scoped to the network it is defined on and hence only +those containers that are connected to that network can access the alias. + +In addition to the above features, multiple containers can share the same network-scoped +alias within the same network. For example, let's launch `container7` in `isolated_nw` with +the same alias as `container6` + +```bash +$ docker run --net=isolated_nw -itd --name=container7 --net-alias app busybox +3138c678c123b8799f4c7cc6a0cecc595acbdfa8bf81f621834103cd4f504554 +``` + +When multiple containers share the same alias, name resolution to that alias will happen +to one of the containers (typically the first container that is aliased). When the container +that backs the alias goes down or disconnected from the network, the next container that +backs the alias will be resolved. + +Let us ping the alias `app` from `container4` and bring down `container6` to verify that +`container7` is resolving the `app` alias. + +```bash +$ docker attach container4 +/ # ping -w 4 app +PING app (172.25.0.6): 56 data bytes +64 bytes from 172.25.0.6: seq=0 ttl=64 time=0.070 ms +64 bytes from 172.25.0.6: seq=1 ttl=64 time=0.080 ms +64 bytes from 172.25.0.6: seq=2 ttl=64 time=0.080 ms +64 bytes from 172.25.0.6: seq=3 ttl=64 time=0.097 ms + +--- app ping statistics --- +4 packets transmitted, 4 packets received, 0% packet loss +round-trip min/avg/max = 0.070/0.081/0.097 ms + +$ docker stop container6 + +$ docker attach container4 +/ # ping -w 4 app +PING app (172.25.0.7): 56 data bytes +64 bytes from 172.25.0.7: seq=0 ttl=64 time=0.095 ms +64 bytes from 172.25.0.7: seq=1 ttl=64 time=0.075 ms +64 bytes from 172.25.0.7: seq=2 ttl=64 time=0.072 ms +64 bytes from 172.25.0.7: seq=3 ttl=64 time=0.101 ms + +--- app ping statistics --- +4 packets transmitted, 4 packets received, 0% packet loss +round-trip min/avg/max = 0.072/0.085/0.101 ms + +``` + +## Disconnecting containers + +You can disconnect a container from a network using the `docker network +disconnect` command. + +``` +$ docker network disconnect isolated_nw container2 + +docker inspect --format='{{json .NetworkSettings.Networks}}' container2 | python -m json.tool +{ + "bridge": { + "NetworkID":"7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812", + "EndpointID": "9e4575f7f61c0f9d69317b7a4b92eefc133347836dd83ef65deffa16b9985dc0", + "Gateway": "172.17.0.1", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "172.17.0.3", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "MacAddress": "02:42:ac:11:00:03" + } +} + + +$ docker network inspect isolated_nw +[ + { + "Name": "isolated_nw", + "Id": "06a62f1c73c4e3107c0f555b7a5f163309827bfbbf999840166065a8f35455a8", + "Scope": "local", + "Driver": "bridge", + "IPAM": { + "Driver": "default", + "Config": [ + { + "Subnet": "172.21.0.0/16", + "Gateway": "172.21.0.1/16" + } + ] + }, + "Containers": { + "467a7863c3f0277ef8e661b38427737f28099b61fa55622d6c30fb288d88c551": { + "Name": "container3", + "EndpointID": "dffc7ec2915af58cc827d995e6ebdc897342be0420123277103c40ae35579103", + "MacAddress": "02:42:ac:19:03:03", + "IPv4Address": "172.25.3.3/16", + "IPv6Address": "" + } + }, + "Options": {} + } +] +``` + +Once a container is disconnected from a network, it cannot communicate with +other containers connected to that network. In this example, `container2` can no longer talk to `container3` on the `isolated_nw` network. + +``` +$ docker attach container2 + +/ # ifconfig +eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:03 + inet addr:172.17.0.3 Bcast:0.0.0.0 Mask:255.255.0.0 + inet6 addr: fe80::42:acff:fe11:3/64 Scope:Link + UP BROADCAST RUNNING MULTICAST MTU:9001 Metric:1 + RX packets:8 errors:0 dropped:0 overruns:0 frame:0 + TX packets:8 errors:0 dropped:0 overruns:0 carrier:0 + collisions:0 txqueuelen:0 + RX bytes:648 (648.0 B) TX bytes:648 (648.0 B) + +lo Link encap:Local Loopback + inet addr:127.0.0.1 Mask:255.0.0.0 + inet6 addr: ::1/128 Scope:Host + UP LOOPBACK RUNNING MTU:65536 Metric:1 + RX packets:0 errors:0 dropped:0 overruns:0 frame:0 + TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 + collisions:0 txqueuelen:0 + RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) + +/ # ping container3 +PING container3 (172.25.3.3): 56 data bytes +^C +--- container3 ping statistics --- +2 packets transmitted, 0 packets received, 100% packet loss +``` + +The `container2` still has full connectivity to the bridge network + +```bash +/ # ping container1 +PING container1 (172.17.0.2): 56 data bytes +64 bytes from 172.17.0.2: seq=0 ttl=64 time=0.119 ms +64 bytes from 172.17.0.2: seq=1 ttl=64 time=0.174 ms +^C +--- container1 ping statistics --- +2 packets transmitted, 2 packets received, 0% packet loss +round-trip min/avg/max = 0.119/0.146/0.174 ms +/ # +``` + +There are certain scenarios such as ungraceful docker daemon restarts in multi-host network, +where the daemon is unable to cleanup stale connectivity endpoints. Such stale endpoints +may cause an error `container already connected to network` when a new container is +connected to that network with the same name as the stale endpoint. In order to cleanup +these stale endpoints, first remove the container and force disconnect +(`docker network disconnect -f`) the endpoint from the network. Once the endpoint is +cleaned up, the container can be connected to the network. + +``` +$ docker run -d --name redis_db --net multihost redis +ERROR: Cannot start container bc0b19c089978f7845633027aa3435624ca3d12dd4f4f764b61eac4c0610f32e: container already connected to network multihost + +$ docker rm -f redis_db +$ docker network disconnect -f multihost redis_db + +$ docker run -d --name redis_db --net multihost redis +7d986da974aeea5e9f7aca7e510bdb216d58682faa83a9040c2f2adc0544795a +``` + +## Remove a network + +When all the containers in a network are stopped or disconnected, you can remove a network. + +```bash +$ docker network disconnect isolated_nw container3 +``` + +```bash +docker network inspect isolated_nw +[ + { + "Name": "isolated_nw", + "Id": "06a62f1c73c4e3107c0f555b7a5f163309827bfbbf999840166065a8f35455a8", + "Scope": "local", + "Driver": "bridge", + "IPAM": { + "Driver": "default", + "Config": [ + { + "Subnet": "172.21.0.0/16", + "Gateway": "172.21.0.1/16" + } + ] + }, + "Containers": {}, + "Options": {} + } +] + +$ docker network rm isolated_nw +``` + +List all your networks to verify the `isolated_nw` was removed: + +``` +$ docker network ls +NETWORK ID NAME DRIVER +72314fa53006 host host +f7ab26d71dbd bridge bridge +0f32e83e61ac none null +``` + +## Related information + +* [network create](../../reference/commandline/network_create.md) +* [network inspect](../../reference/commandline/network_inspect.md) +* [network connect](../../reference/commandline/network_connect.md) +* [network disconnect](../../reference/commandline/network_disconnect.md) +* [network ls](../../reference/commandline/network_ls.md) +* [network rm](../../reference/commandline/network_rm.md) diff --git a/docs/userguide/storagedriver/aufs-driver.md b/docs/userguide/storagedriver/aufs-driver.md new file mode 100644 index 00000000..4651e66e --- /dev/null +++ b/docs/userguide/storagedriver/aufs-driver.md @@ -0,0 +1,216 @@ + + +# Docker and AUFS in practice + +AUFS was the first storage driver in use with Docker. As a result, it has a +long and close history with Docker, is very stable, has a lot of real-world +deployments, and has strong community support. AUFS has several features that +make it a good choice for Docker. These features enable: + +- Fast container startup times. +- Efficient use of storage. +- Efficient use of memory. + +Despite its capabilities and long history with Docker, some Linux distributions + do not support AUFS. This is usually because AUFS is not included in the +mainline (upstream) Linux kernel. + +The following sections examine some AUFS features and how they relate to +Docker. + +## Image layering and sharing with AUFS + +AUFS is a *unification filesystem*. This means that it takes multiple +directories on a single Linux host, stacks them on top of each other, and +provides a single unified view. To achieve this, AUFS uses a *union mount*. + +AUFS stacks multiple directories and exposes them as a unified view through a +single mount point. All of the directories in the stack, as well as the union +mount point, must all exist on the same Linux host. AUFS refers to each +directory that it stacks as a *branch*. + +Within Docker, AUFS union mounts enable image layering. The AUFS storage driver + implements Docker image layers using this union mount system. AUFS branches +correspond to Docker image layers. The diagram below shows a Docker container +based on the `ubuntu:latest` image. + +![](images/aufs_layers.jpg) + +This diagram shows that each image layer, and the container layer, is +represented in the Docker hosts filesystem as a directory under +`/var/lib/docker/`. The union mount point provides the unified view of all +layers. As of Docker 1.10, image layer IDs do not correspond to the names of +the directories that contain their data. + +AUFS also supports the copy-on-write technology (CoW). Not all storage drivers +do. + +## Container reads and writes with AUFS + +Docker leverages AUFS CoW technology to enable image sharing and minimize the +use of disk space. AUFS works at the file level. This means that all AUFS CoW +operations copy entire files - even if only a small part of the file is being +modified. This behavior can have a noticeable impact on container performance, + especially if the files being copied are large, below a lot of image layers, +or the CoW operation must search a deep directory tree. + +Consider, for example, an application running in a container needs to add a +single new value to a large key-value store (file). If this is the first time +the file is modified, it does not yet exist in the container's top writable +layer. So, the CoW must *copy up* the file from the underlying image. The AUFS +storage driver searches each image layer for the file. The search order is from + top to bottom. When it is found, the entire file is *copied up* to the +container's top writable layer. From there, it can be opened and modified. + +Larger files obviously take longer to *copy up* than smaller files, and files +that exist in lower image layers take longer to locate than those in higher +layers. However, a *copy up* operation only occurs once per file on any given +container. Subsequent reads and writes happen against the file's copy already +*copied-up* to the container's top layer. + +## Deleting files with the AUFS storage driver + +The AUFS storage driver deletes a file from a container by placing a *whiteout +file* in the container's top layer. The whiteout file effectively obscures the +existence of the file in the read-only image layers below. The simplified +diagram below shows a container based on an image with three image layers. + +![](images/aufs_delete.jpg) + +The `file3` was deleted from the container. So, the AUFS storage driver placed +a whiteout file in the container's top layer. This whiteout file effectively +"deletes" `file3` from the container by obscuring any of the original file's +existence in the image's read-only layers. This works the same no matter which +of the image's read-only layers the file exists in. + +## Configure Docker with AUFS + +You can only use the AUFS storage driver on Linux systems with AUFS installed. +Use the following command to determine if your system supports AUFS. + + $ grep aufs /proc/filesystems + nodev aufs + +This output indicates the system supports AUFS. Once you've verified your +system supports AUFS, you can must instruct the Docker daemon to use it. You do +this from the command line with the `docker daemon` command: + + $ sudo docker daemon --storage-driver=aufs & + + +Alternatively, you can edit the Docker config file and add the +`--storage-driver=aufs` option to the `DOCKER_OPTS` line. + + # Use DOCKER_OPTS to modify the daemon startup options. + DOCKER_OPTS="--storage-driver=aufs" + +Once your daemon is running, verify the storage driver with the `docker info` +command. + + $ sudo docker info + Containers: 1 + Images: 4 + Storage Driver: aufs + Root Dir: /var/lib/docker/aufs + Backing Filesystem: extfs + Dirs: 6 + Dirperm1 Supported: false + Execution Driver: native-0.2 + ...output truncated... + +The output above shows that the Docker daemon is running the AUFS storage +driver on top of an existing `ext4` backing filesystem. + +## Local storage and AUFS + +As the `docker daemon` runs with the AUFS driver, the driver stores images and +containers within the Docker host's local storage area under +`/var/lib/docker/aufs/`. + +### Images + +Image layers and their contents are stored under +`/var/lib/docker/aufs/diff/`. With Docker 1.10 and higher, image layer IDs do +not correspond to directory names. + +The `/var/lib/docker/aufs/layers/` directory contains metadata about how image +layers are stacked. This directory contains one file for every image or +container layer on the Docker host (though file names no longer match image +layer IDs). Inside each file are the names of the directories that exist below +it in the stack + +The command below shows the contents of a metadata file in +`/var/lib/docker/aufs/layers/` that lists the three directories that are +stacked below it in the union mount. Remember, these directory names do no map +to image layer IDs with Docker 1.10 and higher. + + $ cat /var/lib/docker/aufs/layers/91e54dfb11794fad694460162bf0cb0a4fa710cfa3f60979c177d920813e267c + d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82 + c22013c8472965aa5b62559f2b540cd440716ef149756e7b958a1b2aba421e87 + d3a1f33e8a5a513092f01bb7eb1c2abf4d711e5105390a3fe1ae2248cfde1391 + +The base layer in an image has no image layers below it, so its file is empty. + +### Containers + +Running containers are mounted below `/var/lib/docker/aufs/mnt/`. + This is where the AUFS union mount point that exposes the container and all +underlying image layers as a single unified view exists. If a container is not +running, it still has a directory here but it is empty. This is because AUFS +only mounts a container when it is running. With Docker 1.10 and higher, + container IDs no longer correspond to directory names under +`/var/lib/docker/aufs/mnt/`. + +Container metadata and various config files that are placed into the running +container are stored in `/var/lib/docker/containers/`. Files in +this directory exist for all containers on the system, including ones that are +stopped. However, when a container is running the container's log files are +also in this directory. + +A container's thin writable layer is stored in a directory under +`/var/lib/docker/aufs/diff/`. With Docker 1.10 and higher, container IDs no +longer correspond to directory names. However, the containers thin writable +layer still exists under here and is stacked by AUFS as the top writable layer +and is where all changes to the container are stored. The directory exists even + if the container is stopped. This means that restarting a container will not +lose changes made to it. Once a container is deleted, it's thin writable layer +in this directory is deleted. + +## AUFS and Docker performance + +To summarize some of the performance related aspects already mentioned: + +- The AUFS storage driver is a good choice for PaaS and other similar use-cases + where container density is important. This is because AUFS efficiently shares +images between multiple running containers, enabling fast container start times + and minimal use of disk space. + +- The underlying mechanics of how AUFS shares files between image layers and +containers uses the systems page cache very efficiently. + +- The AUFS storage driver can introduce significant latencies into container +write performance. This is because the first time a container writes to any +file, the file has be located and copied into the containers top writable +layer. These latencies increase and are compounded when these files exist below + many image layers and the files themselves are large. + +One final point. Data volumes provide the best and most predictable +performance. This is because they bypass the storage driver and do not incur +any of the potential overheads introduced by thin provisioning and +copy-on-write. For this reason, you may want to place heavy write workloads on +data volumes. + +## Related information + +* [Understand images, containers, and storage drivers](imagesandcontainers.md) +* [Select a storage driver](selectadriver.md) +* [Btrfs storage driver in practice](btrfs-driver.md) +* [Device Mapper storage driver in practice](device-mapper-driver.md) diff --git a/docs/userguide/storagedriver/btrfs-driver.md b/docs/userguide/storagedriver/btrfs-driver.md new file mode 100644 index 00000000..cc329e73 --- /dev/null +++ b/docs/userguide/storagedriver/btrfs-driver.md @@ -0,0 +1,315 @@ + + +# Docker and Btrfs in practice + +Btrfs is a next generation copy-on-write filesystem that supports many advanced +storage technologies that make it a good fit for Docker. Btrfs is included in +the mainline Linux kernel and its on-disk-format is now considered stable. +However, many of its features are still under heavy development and users +should consider it a fast-moving target. + +Docker's `btrfs` storage driver leverages many Btrfs features for image and +container management. Among these features are thin provisioning, +copy-on-write, and snapshotting. + +This article refers to Docker's Btrfs storage driver as `btrfs` and the overall + Btrfs Filesystem as Btrfs. + +>**Note**: The [Commercially Supported Docker Engine (CS-Engine)](https://www.docker.com/compatibility-maintenance) does not currently support the `btrfs` storage driver. + +## The future of Btrfs + +Btrfs has been long hailed as the future of Linux filesystems. With full +support in the mainline Linux kernel, a stable on-disk-format, and active +development with a focus on stability, this is now becoming more of a reality. + +As far as Docker on the Linux platform goes, many people see the `btrfs` +storage driver as a potential long-term replacement for the `devicemapper` +storage driver. However, at the time of writing, the `devicemapper` storage +driver should be considered safer, more stable, and more *production ready*. +You should only consider the `btrfs` driver for production deployments if you +understand it well and have existing experience with Btrfs. + +## Image layering and sharing with Btrfs + +Docker leverages Btrfs *subvolumes* and *snapshots* for managing the on-disk +components of image and container layers. Btrfs subvolumes look and feel like +a normal Unix filesystem. As such, they can have their own internal directory +structure that hooks into the wider Unix filesystem. + +Subvolumes are natively copy-on-write and have space allocated to them +on-demand from an underlying storage pool. They can also be nested and snapped. + The diagram blow shows 4 subvolumes. 'Subvolume 2' and 'Subvolume 3' are +nested, whereas 'Subvolume 4' shows its own internal directory tree. + +![](images/btfs_subvolume.jpg) + +Snapshots are a point-in-time read-write copy of an entire subvolume. They +exist directly below the subvolume they were created from. You can create +snapshots of snapshots as shown in the diagram below. + +![](images/btfs_snapshots.jpg) + +Btfs allocates space to subvolumes and snapshots on demand from an underlying +pool of storage. The unit of allocation is referred to as a *chunk*, and +*chunks* are normally ~1GB in size. + +Snapshots are first-class citizens in a Btrfs filesystem. This means that they +look, feel, and operate just like regular subvolumes. The technology required +to create them is built directly into the Btrfs filesystem thanks to its +native copy-on-write design. This means that Btrfs snapshots are space +efficient with little or no performance overhead. The diagram below shows a +subvolume and its snapshot sharing the same data. + +![](images/btfs_pool.jpg) + +Docker's `btrfs` storage driver stores every image layer and container in its +own Btrfs subvolume or snapshot. The base layer of an image is stored as a +subvolume whereas child image layers and containers are stored as snapshots. +This is shown in the diagram below. + +![](images/btfs_container_layer.jpg) + +The high level process for creating images and containers on Docker hosts +running the `btrfs` driver is as follows: + +1. The image's base layer is stored in a Btrfs *subvolume* under +`/var/lib/docker/btrfs/subvolumes`. + +2. Subsequent image layers are stored as a Btrfs *snapshot* of the parent +layer's subvolume or snapshot. + + The diagram below shows a three-layer image. The base layer is a subvolume. + Layer 1 is a snapshot of the base layer's subvolume. Layer 2 is a snapshot of +Layer 1's snapshot. + + ![](images/btfs_constructs.jpg) + +As of Docker 1.10, image layer IDs no longer correspond to directory names +under `/var/lib/docker/`. + +## Image and container on-disk constructs + +Image layers and containers are visible in the Docker host's filesystem at +`/var/lib/docker/btrfs/subvolumes/`. However, as previously stated, directory +names no longer correspond to image layer IDs. That said, directories for +containers are present even for containers with a stopped status. This is +because the `btrfs` storage driver mounts a default, top-level subvolume at +`/var/lib/docker/subvolumes`. All other subvolumes and snapshots exist below +that as Btrfs filesystem objects and not as individual mounts. + +Because Btrfs works at the filesystem level and not the block level, each image +and container layer can be browsed in the filesystem using normal Unix +commands. The example below shows a truncated output of an `ls -l` command an +image layer: + + $ ls -l /var/lib/docker/btrfs/subvolumes/0a17decee4139b0de68478f149cc16346f5e711c5ae3bb969895f22dd6723751/ + total 0 + drwxr-xr-x 1 root root 1372 Oct 9 08:39 bin + drwxr-xr-x 1 root root 0 Apr 10 2014 boot + drwxr-xr-x 1 root root 882 Oct 9 08:38 dev + drwxr-xr-x 1 root root 2040 Oct 12 17:27 etc + drwxr-xr-x 1 root root 0 Apr 10 2014 home + ...output truncated... + +## Container reads and writes with Btrfs + +A container is a space-efficient snapshot of an image. Metadata in the snapshot +points to the actual data blocks in the storage pool. This is the same as with +a subvolume. Therefore, reads performed against a snapshot are essentially the +same as reads performed against a subvolume. As a result, no performance +overhead is incurred from the Btrfs driver. + +Writing a new file to a container invokes an allocate-on-demand operation to +allocate new data block to the container's snapshot. The file is then written to +this new space. The allocate-on-demand operation is native to all writes with +Btrfs and is the same as writing new data to a subvolume. As a result, writing +new files to a container's snapshot operate at native Btrfs speeds. + +Updating an existing file in a container causes a copy-on-write operation +(technically *redirect-on-write*). The driver leaves the original data and +allocates new space to the snapshot. The updated data is written to this new +space. Then, the driver updates the filesystem metadata in the snapshot to +point to this new data. The original data is preserved in-place for subvolumes +and snapshots further up the tree. This behavior is native to copy-on-write +filesystems like Btrfs and incurs very little overhead. + +With Btfs, writing and updating lots of small files can result in slow +performance. More on this later. + +## Configuring Docker with Btrfs + +The `btrfs` storage driver only operates on a Docker host where +`/var/lib/docker` is mounted as a Btrfs filesystem. The following procedure +shows how to configure Btrfs on Ubuntu 14.04 LTS. + +### Prerequisites + +If you have already used the Docker daemon on your Docker host and have images +you want to keep, `push` them to Docker Hub or your private Docker Trusted +Registry before attempting this procedure. + +Stop the Docker daemon. Then, ensure that you have a spare block device at +`/dev/xvdb`. The device identifier may be different in your environment and you + should substitute your own values throughout the procedure. + +The procedure also assumes your kernel has the appropriate Btrfs modules +loaded. To verify this, use the following command: + + $ cat /proc/filesystems | grep btrfs + +### Configure Btrfs on Ubuntu 14.04 LTS + +Assuming your system meets the prerequisites, do the following: + +1. Install the "btrfs-tools" package. + + $ sudo apt-get install btrfs-tools + Reading package lists... Done + Building dependency tree + + +2. Create the Btrfs storage pool. + + Btrfs storage pools are created with the `mkfs.btrfs` command. Passing +multiple devices to the `mkfs.btrfs` command creates a pool across all of those + devices. Here you create a pool with a single device at `/dev/xvdb`. + + $ sudo mkfs.btrfs -f /dev/xvdb + WARNING! - Btrfs v3.12 IS EXPERIMENTAL + WARNING! - see http://btrfs.wiki.kernel.org before using + + Turning ON incompat feature 'extref': increased hardlink limit per file to 65536 + fs created label (null) on /dev/xvdb + nodesize 16384 leafsize 16384 sectorsize 4096 size 4.00GiB + Btrfs v3.12 + + Be sure to substitute `/dev/xvdb` with the appropriate device(s) on your + system. + + > **Warning**: Take note of the warning about Btrfs being experimental. As + noted earlier, Btrfs is not currently recommended for production deployments + unless you already have extensive experience. + +3. If it does not already exist, create a directory for the Docker host's local + storage area at `/var/lib/docker`. + + $ sudo mkdir /var/lib/docker + +4. Configure the system to automatically mount the Btrfs filesystem each time the system boots. + + a. Obtain the Btrfs filesystem's UUID. + + $ sudo blkid /dev/xvdb + /dev/xvdb: UUID="a0ed851e-158b-4120-8416-c9b072c8cf47" UUID_SUB="c3927a64-4454-4eef-95c2-a7d44ac0cf27" TYPE="btrfs" + + b. Create an `/etc/fstab` entry to automatically mount `/var/lib/docker` +each time the system boots. Either of the following lines will work, just +remember to substitute the UUID value with the value obtained from the previous + command. + + /dev/xvdb /var/lib/docker btrfs defaults 0 0 + UUID="a0ed851e-158b-4120-8416-c9b072c8cf47" /var/lib/docker btrfs defaults 0 0 + +5. Mount the new filesystem and verify the operation. + + $ sudo mount -a + $ mount + /dev/xvda1 on / type ext4 (rw,discard) + + /dev/xvdb on /var/lib/docker type btrfs (rw) + + The last line in the output above shows the `/dev/xvdb` mounted at +`/var/lib/docker` as Btrfs. + +Now that you have a Btrfs filesystem mounted at `/var/lib/docker`, the daemon +should automatically load with the `btrfs` storage driver. + +1. Start the Docker daemon. + + $ sudo service docker start + docker start/running, process 2315 + + The procedure for starting the Docker daemon may differ depending on the + Linux distribution you are using. + + You can force the Docker daemon to start with the `btrfs` storage +driver by either passing the `--storage-driver=btrfs` flag to the `docker +daemon` at startup, or adding it to the `DOCKER_OPTS` line to the Docker config + file. + +2. Verify the storage driver with the `docker info` command. + + $ sudo docker info + Containers: 0 + Images: 0 + Storage Driver: btrfs + [...] + +Your Docker host is now configured to use the `btrfs` storage driver. + +## Btrfs and Docker performance + +There are several factors that influence Docker's performance under the `btrfs` + storage driver. + +- **Page caching**. Btrfs does not support page cache sharing. This means that +*n* containers accessing the same file require *n* copies to be cached. As a +result, the `btrfs` driver may not be the best choice for PaaS and other high +density container use cases. + +- **Small writes**. Containers performing lots of small writes (including +Docker hosts that start and stop many containers) can lead to poor use of Btrfs + chunks. This can ultimately lead to out-of-space conditions on your Docker +host and stop it working. This is currently a major drawback to using current +versions of Btrfs. + + If you use the `btrfs` storage driver, closely monitor the free space on +your Btrfs filesystem using the `btrfs filesys show` command. Do not trust the +output of normal Unix commands such as `df`; always use the Btrfs native +commands. + +- **Sequential writes**. Btrfs writes data to disk via journaling technique. +This can impact sequential writes, where performance can be up to half. + +- **Fragmentation**. Fragmentation is a natural byproduct of copy-on-write +filesystems like Btrfs. Many small random writes can compound this issue. It +can manifest as CPU spikes on Docker hosts using SSD media and head thrashing +on Docker hosts using spinning media. Both of these result in poor performance. + + Recent versions of Btrfs allow you to specify `autodefrag` as a mount +option. This mode attempts to detect random writes and defragment them. You +should perform your own tests before enabling this option on your Docker hosts. + Some tests have shown this option has a negative performance impact on Docker +hosts performing lots of small writes (including systems that start and stop +many containers). + +- **Solid State Devices (SSD)**. Btrfs has native optimizations for SSD media. +To enable these, mount with the `-o ssd` mount option. These optimizations +include enhanced SSD write performance by avoiding things like *seek +optimizations* that have no use on SSD media. + + Btfs also supports the TRIM/Discard primitives. However, mounting with the +`-o discard` mount option can cause performance issues. Therefore, it is +recommended you perform your own tests before using this option. + +- **Use Data Volumes**. Data volumes provide the best and most predictable +performance. This is because they bypass the storage driver and do not incur +any of the potential overheads introduced by thin provisioning and +copy-on-write. For this reason, you should place heavy write workloads on data +volumes. + +## Related Information + +* [Understand images, containers, and storage drivers](imagesandcontainers.md) +* [Select a storage driver](selectadriver.md) +* [AUFS storage driver in practice](aufs-driver.md) +* [Device Mapper storage driver in practice](device-mapper-driver.md) diff --git a/docs/userguide/storagedriver/device-mapper-driver.md b/docs/userguide/storagedriver/device-mapper-driver.md new file mode 100644 index 00000000..4b7b664d --- /dev/null +++ b/docs/userguide/storagedriver/device-mapper-driver.md @@ -0,0 +1,467 @@ + + +# Docker and the Device Mapper storage driver + +Device Mapper is a kernel-based framework that underpins many advanced +volume management technologies on Linux. Docker's `devicemapper` storage driver +leverages the thin provisioning and snapshotting capabilities of this framework +for image and container management. This article refers to the Device Mapper +storage driver as `devicemapper`, and the kernel framework as `Device Mapper`. + + +>**Note**: The [Commercially Supported Docker Engine (CS-Engine) running on RHEL +and CentOS Linux](https://www.docker.com/compatibility-maintenance) requires +that you use the `devicemapper` storage driver. + + +## An alternative to AUFS + +Docker originally ran on Ubuntu and Debian Linux and used AUFS for its storage +backend. As Docker became popular, many of the companies that wanted to use it +were using Red Hat Enterprise Linux (RHEL). Unfortunately, because the upstream +mainline Linux kernel did not include AUFS, RHEL did not use AUFS either. + +To correct this Red Hat developers investigated getting AUFS into the mainline +kernel. Ultimately, though, they decided a better idea was to develop a new +storage backend. Moreover, they would base this new storage backend on existing +`Device Mapper` technology. + +Red Hat collaborated with Docker Inc. to contribute this new driver. As a result +of this collaboration, Docker's Engine was re-engineered to make the storage +backend pluggable. So it was that the `devicemapper` became the second storage +driver Docker supported. + +Device Mapper has been included in the mainline Linux kernel since version +2.6.9. It is a core part of RHEL family of Linux distributions. This means that +the `devicemapper` storage driver is based on stable code that has a lot of +real-world production deployments and strong community support. + + +## Image layering and sharing + +The `devicemapper` driver stores every image and container on its own virtual +device. These devices are thin-provisioned copy-on-write snapshot devices. +Device Mapper technology works at the block level rather than the file level. +This means that `devicemapper` storage driver's thin provisioning and +copy-on-write operations work with blocks rather than entire files. + +>**Note**: Snapshots are also referred to as *thin devices* or *virtual +>devices*. They all mean the same thing in the context of the `devicemapper` +>storage driver. + +With `devicemapper` the high level process for creating images is as follows: + +1. The `devicemapper` storage driver creates a thin pool. + + The pool is created from block devices or loop mounted sparse files (more +on this later). + +2. Next it creates a *base device*. + + A base device is a thin device with a filesystem. You can see which +filesystem is in use by running the `docker info` command and checking the +`Backing filesystem` value. + +3. Each new image (and image layer) is a snapshot of this base device. + + These are thin provisioned copy-on-write snapshots. This means that they +are initially empty and only consume space from the pool when data is written +to them. + +With `devicemapper`, container layers are snapshots of the image they are +created from. Just as with images, container snapshots are thin provisioned +copy-on-write snapshots. The container snapshot stores all updates to the +container. The `devicemapper` allocates space to them on-demand from the pool +as and when data is written to the container. + +The high level diagram below shows a thin pool with a base device and two +images. + +![](images/base_device.jpg) + +If you look closely at the diagram you'll see that it's snapshots all the way +down. Each image layer is a snapshot of the layer below it. The lowest layer of + each image is a snapshot of the base device that exists in the pool. This +base device is a `Device Mapper` artifact and not a Docker image layer. + +A container is a snapshot of the image it is created from. The diagram below +shows two containers - one based on the Ubuntu image and the other based on the + Busybox image. + +![](images/two_dm_container.jpg) + + +## Reads with the devicemapper + +Let's look at how reads and writes occur using the `devicemapper` storage +driver. The diagram below shows the high level process for reading a single +block (`0x44f`) in an example container. + +![](images/dm_container.jpg) + +1. An application makes a read request for block `0x44f` in the container. + + Because the container is a thin snapshot of an image it does not have the +data. Instead, it has a pointer (PTR) to where the data is stored in the image +snapshot lower down in the image stack. + +2. The storage driver follows the pointer to block `0xf33` in the snapshot +relating to image layer `a005...`. + +3. The `devicemapper` copies the contents of block `0xf33` from the image +snapshot to memory in the container. + +4. The storage driver returns the data to the requesting application. + +### Write examples + +With the `devicemapper` driver, writing new data to a container is accomplished + by an *allocate-on-demand* operation. Updating existing data uses a +copy-on-write operation. Because Device Mapper is a block-based technology +these operations occur at the block level. + +For example, when making a small change to a large file in a container, the +`devicemapper` storage driver does not copy the entire file. It only copies the + blocks to be modified. Each block is 64KB. + +#### Writing new data + +To write 56KB of new data to a container: + +1. An application makes a request to write 56KB of new data to the container. + +2. The allocate-on-demand operation allocates a single new 64KB block to the +container's snapshot. + + If the write operation is larger than 64KB, multiple new blocks are +allocated to the container's snapshot. + +3. The data is written to the newly allocated block. + +#### Overwriting existing data + +To modify existing data for the first time: + +1. An application makes a request to modify some data in the container. + +2. A copy-on-write operation locates the blocks that need updating. + +3. The operation allocates new empty blocks to the container snapshot and +copies the data into those blocks. + +4. The modified data is written into the newly allocated blocks. + +The application in the container is unaware of any of these +allocate-on-demand and copy-on-write operations. However, they may add latency +to the application's read and write operations. + +## Configuring Docker with Device Mapper + +The `devicemapper` is the default Docker storage driver on some Linux +distributions. This includes RHEL and most of its forks. Currently, the +following distributions support the driver: + +* RHEL/CentOS/Fedora +* Ubuntu 12.04 +* Ubuntu 14.04 +* Debian + +Docker hosts running the `devicemapper` storage driver default to a +configuration mode known as `loop-lvm`. This mode uses sparse files to build +the thin pool used by image and container snapshots. The mode is designed to +work out-of-the-box with no additional configuration. However, production +deployments should not run under `loop-lvm` mode. + +You can detect the mode by viewing the `docker info` command: + + $ sudo docker info + Containers: 0 + Images: 0 + Storage Driver: devicemapper + Pool Name: docker-202:2-25220302-pool + Pool Blocksize: 65.54 kB + Backing Filesystem: xfs + ... + Data loop file: /var/lib/docker/devicemapper/devicemapper/data + Metadata loop file: /var/lib/docker/devicemapper/devicemapper/metadata + Library Version: 1.02.93-RHEL7 (2015-01-28) + ... + +The output above shows a Docker host running with the `devicemapper` storage +driver operating in `loop-lvm` mode. This is indicated by the fact that the +`Data loop file` and a `Metadata loop file` are on files under +`/var/lib/docker/devicemapper/devicemapper`. These are loopback mounted sparse +files. + +### Configure direct-lvm mode for production + +The preferred configuration for production deployments is `direct lvm`. This +mode uses block devices to create the thin pool. The following procedure shows +you how to configure a Docker host to use the `devicemapper` storage driver in +a `direct-lvm` configuration. + +> **Caution:** If you have already run the Engine daemon on your Docker host +> and have images you want to keep, `push` them Docker Hub or your private +> Docker Trusted Registry before attempting this procedure. + +The procedure below will create a 90GB data volume and 4GB metadata volume to +use as backing for the storage pool. It assumes that you have a spare block +device at `/dev/sdd` with enough free space to complete the task. The device +identifier and volume sizes may be be different in your environment and you +should substitute your own values throughout the procedure. + +The procedure also assumes that the Engine daemon is in the `stopped` state. +Any existing images or data are lost by this process. + +1. Log in to the Docker host you want to configure. +2. If it is running, stop the Engine daemon. +3. Install the logical volume management version 2. + + ```bash + $ yum install lvm2 + ``` +4. Create a physical volume replacing `/dev/sdd` with your block device. + + ```bash + $ pvcreate /dev/sdd + ``` + +5. Create a 'docker' volume group. + + ```bash + $ vgcreate docker /dev/sdd + ``` + +6. Create a thin pool named `thinpool`. + + In this example, the data logical is 95% of the 'docker' volume group size. + Leaving this free space allows for auto expanding of either the data or + metadata if space runs low as a temporary stopgap. + + ```bash + $ lvcreate --wipesignatures y -n thinpool docker -l 95%VG + $ lvcreate --wipesignatures y -n thinpoolmeta docker -l 1%VG + ``` + +7. Convert the pool to a thin pool. + + ```bash + $ lvconvert -y --zero n -c 512K --thinpool docker/thinpool --poolmetadata docker/thinpoolmeta + ``` + +8. Configure autoextension of thin pools via an `lvm` profile. + + ```bash + $ vi /etc/lvm/profile/docker-thinpool.profile + ``` + +9. Specify 'thin_pool_autoextend_threshold' value. + + The value should be the percentage of space used before `lvm` attempts + to autoextend the available space (100 = disabled). + + ``` + thin_pool_autoextend_threshold = 80 + ``` + +10. Modify the `thin_pool_autoextend_percent` for when thin pool autoextension occurs. + + The value's setting is the perentage of space to increase the thin pool (100 = + disabled) + + ``` + thin_pool_autoextend_percent = 20 + ``` + +11. Check your work, your `docker-thinpool.profile` file should appear similar to the following: + + An example `/etc/lvm/profile/docker-thinpool.profile` file: + + ``` + activation { + thin_pool_autoextend_threshold=80 + thin_pool_autoextend_percent=20 + } + ``` + +12. Apply your new lvm profile + + ```bash + $ lvchange --metadataprofile docker-thinpool docker/thinpool + ``` + +13. Verify the `lv` is monitored. + + ```bash + $ lvs -o+seg_monitor + ``` + +14. If Engine was previously started, clear your graph driver directory. + + Clearing your graph driver removes any images and containers in your Docker + installation. + + ```bash + $ rm -rf /var/lib/docker/* + ``` + +14. Configure the Engine daemon with specific devicemapper options. + + There are two ways to do this. You can set options on the commmand line if you start the daemon there: + + ```bash + --storage-driver=devicemapper --storage-opt=dm.thinpooldev=/dev/mapper/docker-thinpool --storage-opt dm.use_deferred_removal=true + ``` + + You can also set them for startup in the `daemon.json` configuration, for example: + + ```json + { + "storage-driver": "devicemapper", + "storage-opts": [ + "dm.thinpooldev=/dev/mapper/docker-thinpool", + "dm.use_deferred_removal=true" + ] + } + ``` +15. Start the Engine daemon. + + ```bash + $ systemctl start docker + ``` + +After you start the Engine daemon, ensure you monitor your thin pool and volume +group free space. While the volume group will auto-extend, it can still fill +up. To monitor logical volumes, use `lvs` without options or `lvs -a` to see tha +data and metadata sizes. To monitor volume group free space, use the `vgs` command. + +Logs can show the auto-extension of the thin pool when it hits the threshold, to +view the logs use: + +```bash +journalctl -fu dm-event.service +``` + +If you run into repeated problems with thin pool, you can use the +`dm.min_free_space` option to tune the Engine behavior. This value ensures that +operations fail with a warning when the free space is at or near the minimum. +For information, see the storage driver options in the Engine daemon reference. + + +### Examine devicemapper structures on the host + +You can use the `lsblk` command to see the device files created above and the +`pool` that the `devicemapper` storage driver creates on top of them. + + $ sudo lsblk + NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT + xvda 202:0 0 8G 0 disk + └─xvda1 202:1 0 8G 0 part / + xvdf 202:80 0 10G 0 disk + ├─vg--docker-data 253:0 0 90G 0 lvm + │ └─docker-202:1-1032-pool 253:2 0 10G 0 dm + └─vg--docker-metadata 253:1 0 4G 0 lvm + └─docker-202:1-1032-pool 253:2 0 10G 0 dm + +The diagram below shows the image from prior examples updated with the detail +from the `lsblk` command above. + +![](http://farm1.staticflickr.com/703/22116692899_0471e5e160_b.jpg) + +In the diagram, the pool is named `Docker-202:1-1032-pool` and spans the `data` + and `metadata` devices created earlier. The `devicemapper` constructs the pool + name as follows: + +``` +Docker-MAJ:MIN-INO-pool +``` + +`MAJ`, `MIN` and `INO` refer to the major and minor device numbers and inode. + +Because Device Mapper operates at the block level it is more difficult to see +diffs between image layers and containers. Docker 1.10 and later no longer +matches image layer IDs with directory names in `/var/lib/docker`. However, +there are two key directories. The `/var/lib/docker/devicemapper/mnt` directory + contains the mount points for image and container layers. The +`/var/lib/docker/devicemapper/metadata`directory contains one file for every +image layer and container snapshot. The files contain metadata about each +snapshot in JSON format. + +## Device Mapper and Docker performance + +It is important to understand the impact that allocate-on-demand and +copy-on-write operations can have on overall container performance. + +### Allocate-on-demand performance impact + +The `devicemapper` storage driver allocates new blocks to a container via an +allocate-on-demand operation. This means that each time an app writes to +somewhere new inside a container, one or more empty blocks has to be located +from the pool and mapped into the container. + +All blocks are 64KB. A write that uses less than 64KB still results in a single + 64KB block being allocated. Writing more than 64KB of data uses multiple 64KB +blocks. This can impact container performance, especially in containers that +perform lots of small writes. However, once a block is allocated to a container + subsequent reads and writes can operate directly on that block. + +### Copy-on-write performance impact + +Each time a container updates existing data for the first time, the +`devicemapper` storage driver has to perform a copy-on-write operation. This +copies the data from the image snapshot to the container's snapshot. This +process can have a noticeable impact on container performance. + +All copy-on-write operations have a 64KB granularity. As a results, updating +32KB of a 1GB file causes the driver to copy a single 64KB block into the +container's snapshot. This has obvious performance advantages over file-level +copy-on-write operations which would require copying the entire 1GB file into +the container layer. + +In practice, however, containers that perform lots of small block writes +(<64KB) can perform worse with `devicemapper` than with AUFS. + +### Other device mapper performance considerations + +There are several other things that impact the performance of the +`devicemapper` storage driver. + +- **The mode.** The default mode for Docker running the `devicemapper` storage +driver is `loop-lvm`. This mode uses sparse files and suffers from poor +performance. It is **not recommended for production**. The recommended mode for + production environments is `direct-lvm` where the storage driver writes +directly to raw block devices. + +- **High speed storage.** For best performance you should place the `Data file` + and `Metadata file` on high speed storage such as SSD. This can be direct +attached storage or from a SAN or NAS array. + +- **Memory usage.** `devicemapper` is not the most memory efficient Docker +storage driver. Launching *n* copies of the same container loads *n* copies of +its files into memory. This can have a memory impact on your Docker host. As a +result, the `devicemapper` storage driver may not be the best choice for PaaS +and other high density use cases. + +One final point, data volumes provide the best and most predictable +performance. This is because they bypass the storage driver and do not incur +any of the potential overheads introduced by thin provisioning and +copy-on-write. For this reason, you should to place heavy write workloads on +data volumes. + +## Related Information + +* [Understand images, containers, and storage drivers](imagesandcontainers.md) +* [Select a storage driver](selectadriver.md) +* [AUFS storage driver in practice](aufs-driver.md) +* [Btrfs storage driver in practice](btrfs-driver.md) +* [daemon reference](../../reference/commandline/daemon#storage-driver-options) diff --git a/docs/userguide/storagedriver/images/aufs_delete.jpg b/docs/userguide/storagedriver/images/aufs_delete.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fa0f0d2e9fbb1b9ff96b14a88e834ec4de133c78 GIT binary patch literal 39059 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

^f0LmM{H#w!&&H1stopGU^V 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 YBP1+ruJf5~^G=~Ain#tyvi|=~0AAkRssI20 literal 0 HcmV?d00001 diff --git a/docs/userguide/storagedriver/images/base_device.jpg b/docs/userguide/storagedriver/images/base_device.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3db490d83dfa72ae2b67a6c3310959527e6d9893 GIT binary patch literal 46684 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%0A(;!$!xI=}@q@tKVB2WJ z8JulB#Y5eBoZO9#i`uRKko}5O^K?x{WK-v`(1;U1^VOPOMJC-IX^T>LHvAVj!m8@e>6Lhn7GL#EONgmT)1zVqHm@| zrt3vUjwyHiVGu2!nMhq7URU18Tqa?qFt*9U*yt^bl?#1-UNvS5?Ms|DvuYiTN9 zIsBB@SAWtv{xF1a;v&&`1~i-LTJYw@FYzKTGE%g=BwIuDS3aLEFDP-#${^vVz?~+X zLHYjP3-JCByw!j5v3b-Uc27k>tVYkYnRIcLa74d!siL^Ks2M(!B8{zd|3!%En2{p1 zd;9kM>B|*DN?O$!lpFZH#?BpuA*IUL=DewlBZk|3ml?pkycHj#zR5?N~+UuDNOsz9iA+yYvzxR&TsQ`#%c)mDU|y?@O8+g)7KSTpZNk*|!E z!q>t^k#vEEb{?70mg2cpE*(cL*T(!Cxw2j#%^F!3f+4Zwl2Q_(t7H^aRU?tZ`4@y! zn%1gtGR!n|w9@24ZRo`ip=4Dnm|8Z+5r<^_tRvwlS^^4!$!>-MNrZa35@*phb}Yl% z2hdxc24yklp=^2B+dKrkMA>uU)w)tr7IfaJ;DUuA%bgn~EhKI_ed?hSj_ez#OHXam zP$H2{Kd2%`vB8H$Gch&}g=OFp-6x?i6BoyEdA|X{JUCZqG}v;J+5*)u_O=MD{6RbR zid`Nn$Gir5S6S8!I|J)U4`%G{)gK5c>0bN9lsH}~gh>T^BC>^cB#&RnE{26oJVPba z-a4D}y)~2Pq@i=F||*dpqP; z>Yg6lgP051%CU`MbI!0ccP+WqO;}&ZZ!Wv*0;)U_>qwg|S{_bd!L+%VaC{6HPmD}2 zGVC~l`y3>2JW6q_UE%8VW0(xae@z=RTbZ8pK1r&Jd+kZ0w-|6WW?)gFP8>V#U(IR} zl_b8wvpIm|VhQggjzl0RFWpm1siVDsS3|{XF8!32$wXQ*rxL#esaISl)|w$%wdq*= zS^<0F_aSwE1-TsN`gl~FcO0x>YyC?awrh#Z?8abxyPvq~XQv!Aq?uDktfhWhi9nj| zdbofkb$IbW1MQ|}!%e5}-Vc8DMp4%@U5M{n6;NreUR5k>3?~xQY}#K17-`j83?3T7iaivu>_|S<6d$4^=fkWaMNt9r0l3*t9Of8m5GNl#O!C`FTLj&N4M!NhIXyo^< zE#DTZ@u$Jb5ac_a+^ZDC;`SGWNR+cp@z=UWbtxv$Dk_Wi1sm?S-sit2oFcBKZOM=K zA&`fvOh&6s^&*#xC9NOk;4%)caW#n#`W}y@CG@KI&1{c~65EH~vdAgZk3;JJ*l1OC zlt_Nxt_jL$AwyN!@Dq(k_uH%-d7$XT)md4~Xh^@Yow2&Sww3!~;pUfEGZdX66F$D) z`r}KNus z1(2$qklk!2!r_et?%7M965GXj#Swpyy|!g8^;ZP~9vo+T_7AmoyBFS{?f;x-H3nkI z0+fP-)WiOR z9WeqWh6~_V$c&U7tz3wG!ysa7zdMtp_O-*dh5q&9)2p>8Kh7H?yPi?l_j)_DJ zn@b^MUvVy7F)sO=)B09LRkh@kkPr-w>*@3%H57RPfjjE z8Av!ohqg;mh)|C1dSmuK4@d&slvF*qk*;U(Obc%Bn^?_HS)n#Pr$SM7#Bkq=@7>c9 z0Yu+WMA>~Mbo9ozKsZ75OTUqs4<(dBFjSSLVv}23OlJ9UZwYjec6cWn53FZb19Qd#;hS&`2Z$ewVmUooWH)|_#H@Erld{Y7xJ9>rxWiegOGf>+23{3cC@EGT9 zvJ5~=k&H9qPiJ2l9J3>*sqrmL*Ku1e;IEW|m3-e7=x?fte?#$s_*&^rt&?kFsQSFy z;Nl*-vMuXfW;@YDbyoaR%otK_QaoP6jBIdPBypQhU`eY(iwk$DnGL!{UBYB96}9fr z2dbU15tcQ_s?lC~5~-nOzKN;9<5wx-k<8vB4i4HS@>`O5QJgN|5!jf$VVQI~Hav3< z&%jl1v5#HxKi1)2v^)=G$IrlemsJDF%&{0Zc{uWL<*}T%_y<{|-UcD8Vv*|eK_|gWP&Uy~aiSws>&#N$dnq%F9tt&z$3N-? zuZUW+Y%})hNe;(vm_@Mv8mlY^!AE#|pf&Vx>%WdaC1vEoI-{irI?2pl*4C;@iJ_V8 zvuaE|^aWo`phBXV%6gIVv4`@uS!lNBMDcl&*2x_8`^m>O>|%x_%B@n-6b;qKdG=%? z#Sj&@6aN6VUT_JgM1KKW^>xIK9uQz_gxmcB*jhvX1F&`c7hpT_8?d#M4E?tLzW}x} z7$CqFkF5zy+Qr%=^3Y0p{WK_6+*+tTEG(zvUwG}GQuqt=Vqq$85=qDK0op5g;NV&g zWeV~G3$DP3W#dWSK<$zWi;+li1d$H%N8*AHa5LYMjhH0FI!QTkVi?Cmt4#3O$M!z8 zCUJ^L)g71Kp%_fj4Frrr)MeTRj1l7>-ZB|Wfs?St>%S)$A2Es=9BCCT2+-bcP`t!x zbhO;Y>MBJA-YO@9_vXlQFHMwlQ}eUaL6h6SeiDEX#A28uZ0dlc9DLWuwYqe(Wx@l? zUCY?7h*|mT&WHA&=OPW8OzQ4e*YFh*2(4`u!-xMje3#4JM(78C2014Y}LD8-f`|$y_B(Pxz&nO%9Pxj#B*134NsaOR_WCG z?BZtW@{2R?8q0kxC)rj)(&(bel1+i*=r3@uHyN>2XEZo1jhF3LHt|o)qU|aZ{vBGB zFN3t=+4`Q5irblGHN2gUFTcrjytc5Fg39EDP+mjCB1z-KrO0)0hNs;#Kx2GR-jIHV zp|WQD^}kOHgcuZ4@|jv~%=yh8*~~-=M;xSYzHLUuz<1_*)@IWK(wCQvWKMqU{ztdM zU@970!Kj+bRzcrepI}}2f;PSI7kxE}hNnr_IPwX;Z6StXc!lg3{lsUA?@?>8Q{@;K zMJx1tll1g{y*IWrmPC0DQ6FW%Ef62#llVLwXMWXw@dFMu^<=qTGp-ZuO_kqb9LWmdGYoa!Ua$5gX3uuVxJ9p$=a9%M4ze%2CGC>E zJL;GrwINwx73wcmBJkmgrgUEA9lTn5duiBk?#*r9*gAoj3>g^EKf2hzaz8nd`MEL$ zfs%H8Jkd@B)1Imc6>Zq368CgJnPed=?$FjKF9t#LK#QR$*m8Jt-C`=~3>(XRPHC9Z zN1sdc^Nuy|1k$WQdAwR?S^uA>7#`mPk9vMSUS)V?1PxhoDZu+o%5WU}rU#qp>m+E6 zv?gqcG#WzV)L?hgT%KP}h6oJl%zKgZwX%e$MmVP`K#HLW>}_bP+we=!m<&Ul$3z=Z zY9h8PXW&&EiWmFW8c5YtB-%vCi{)^NWolHz=YU5GzO%Tk%>!4`Ou&wAU5z%1TpEKq z+>I!yaL5;~tDzsR+*uoOAk|`nf>lS~h;#Tgn}zZr`k)NF2dgi&DZYRKs#F0icHn~? z3k8A((YQ5qa_k7L$z0!{h8|HMMomFNvyA|TO~s1;E7349U5rOBDi*;8bx23*VwTW} ze-{4ZQIl^~m{E}0>FJQ6^~iS) zqWjsW?g@8qUv7^EUAJ|;j~SBR7(kjW&`IwIj?;iQV2A2=U&agytI7VdI!L6FTCmd6 z4L?%q9F9uRADd`_Wh=;T(&$x?KAh?GHKBExH0*#x^REBkQBup@YC%2GT-;qq!k79 z%$g5_)qP+LJYu1fhf*2U7V&Hw#|?wY+norsUCi6K<)GAUgoO}hR~1@lUdOZ ze=vMClphkS-)iJ69Y1_;%4Yn2su-7%*{XUY^~dJ;2VC=L@)_;=^FrLgxqh7FWk8-@ zlI9M&2t`g0OX(2P(@+b)&gK=-`8I2F`Nny1J<2VffIq7n@}oKwb_{Y%npFS|NK-KovYq8#I-fUZOFxsm(OGD=$QIp#_NOVN~?) z^F-#5L9B0{;Na7z-)5JdgdF$rE`Egg;nXH%MK zyjN}(+pze6cX#hQxN^-u!+`{P-il9cB-nx0D|UWS*}LBoIh6pG=T5uEJVI6m`(>m5 z!ille2L^2%9?xwqJVc`+>hCyidPl(WxXD(dGeTRf}rn}olP zynk`T?gA8^*5lufSlYfePEdg~KR0yeYEkYAjI2QBahnVM?Z_*FC_QYAO{bO0zay2p z$-u8JdioSZfoC=GGE(O^v?tV>A{$g6{{7fX0D66c9mgcn*cNU(J}1+fTC;cK;e&-= z6F;7Lu_bS9`n;ax@}`pJ!WB7WL>D*66fT0PB(SrK1*S+%p>d;OgPySnVO_d^)i@J; zF)R7XuRWVp>*&i>?ap$hZKq%O(vO>kzgztlmGx+m?+KiF_K`%T|M!*u2j%}s)BvcfHgv)B47u%w3_{rjbM5xjDuC?K<6&F7$f*gk& z+1BVCJ+Vi&I^yYVzs`3=;}=6sJi15E@!)suHF0B?^pEz4Uz@lxmJ@}|?$m%Mm_o%| zdr0>>mx&|HaRpkZumlvwV(NzomFi;Uf!;otbeq3%KQ?sPjc!=oqFqdkQ#L?9-2N!P zcM83g`c}6ZYu9N}BRGn>!aK3Tn3-4nXwHFmL+Mh7)J)s^R(~_TtYXmrevT;SJyjNF z2k%suwbL(~XW$t)Oq^)=FwQcjlVw7UK^XM8O_8&nyI)h!gD%DXkrbO+C%nNYZ1tU7sj~YO2pQFlT93 zq+r|SvuLR5BfN1fvS7W$lkFQ+UYo3*!9Lu}3Ru;3iG~Xs`lFdCnu{}{n zPflt$(5F*X1XU_CQd?<|j7NvC`pP`jI*zLm)DM1An<|NNPpT9hw2tzO{=uV(ts-(V zx`*lolLmhZLT)wsYvN4lQVT70ip#SQpI%|FQ zlNFV%2@*&7@WHlv*a9^yuE7~_EzBTOu8^$@D$4$F2^59%!MvmyRRQOfZCZs=rXA(5 zgC$Wi-u#xj0Cdyq=>u&uvxdnDJ8L^4!5X&p^s$(eKJ>v;#}DdDv%-nY%p|OFl)7nMlJ3Qni{FjgvC>M}X*pF16g2bEfZee}7Yo}-J*~I{uY0Vs2-=`z z*0l(QSL&CpiVuRnX#F&7{wAz#zS(869PP}xXzyOL>U`yhLALH^NI*fpP2sLHrKHS> zwBFmnib_w-laMm)WSO7k)&hl)EyRd2viLmeNYr6-I_34cvo<@?u!6&Y@;cns z1b6pVSC1G{+~L|C6O2#sn4>dD^~AY}QR$<6>kd|%c0wb6gEh*G3%`_07rpK*I3=^` z%9FhH9UCzmf~++8?T>pXpOmed$`LG683jURleJ5uhgXn~;b^(*ixQFA8RL4ZQ4CKy zBIO++0R)g9GiS#i;D_AEZd!dQndcPpjBoZ6soX_rKT;3URIX$7k(Nfj6_407)OZDz zhu5UDD18u*(e~Gjthi|e41%<(<1{TQH)d(XDHPt#ATAZESLDMBEVLuFZSAbFCUGi# zaeJ3Um-w-Sjj0#&8w(R}wfgtou=lCEF+G^z>9Vbrq`iVU30QS&J6nisFDJAK;S5{Y zZLr*Tr_hTxx@8vSks4hRj>}kDQ==R_Ipwxo3#c*jBiLHJ zp4rm<%7}hHB9u5cONzvprtJ`wCSG~)Zy%k*=EMp^8O&#GygUSJiHasm1!D#S;fYGa ziBxWk8Wbu~YFb@nKVyn$bI-Q0sN$wI5zi{yt5TP!I?Dg%CdpHj`vL76C*39VwP=iJ z59`{Xl}WT2rfR$UDS>TK3)G1i3@2}lZxAE>7jTn5w;@Ttj=?SuY*(c)v;|6{-2Cj4 zf3SF37>tV9h!2QJk3)(7F^t-&bQyyZ{bLZdR_)T+eDw4fhq}A~*3(z-tv3h@wUfeL z7c3=)!{qvackj&#fCX*oLoWInN57s@akejO9}q=pfFBGqAG&WYj6BQz(u>`Qx`h{? z4U2QRB#UNo=Z49FTHX~kK&dU{HC!=8e zuTY%8l>;xh(`U|2*t@GG*)RcpdfE zboE*lAEv;=8Av`3h9rKy?>DtRbGQj8@k*fmXlS28H#B;zCfLb4-09 ze^u7^qC5kgw0bVkF({oSu_$X%7t=;V2Arv2Y$Nw6yl$x42U|{ATc2Rn*{mSh8=t6V z2JYm|KOqD}*^BP6^gt2vh{sslH%?qA`6E-JcT;lt;k+2${}01s0zrmUuF7o|L(S# zDDNb}d=Nv$4hF(y*V=MV$EZ6F$|bWn%@F%sV!Prb*(GH&cG+LGMF#Z$L@)}%|Ky{l zhIWsOHnONxrDVx{2Yvcj4HwG=j>@&`7igz19o#oBO4OQ-AfsVGPsz^99Ot*rXpB*e zSm6ZwPN<27P~sKNO0@^JBZo<2qTwkndSRazv@C;!(0gM@6zKi<9i55x${ZwaG-;t% zG0%WYrUq(1CBCeYNppCl4z3U^+5&R|%V@NW8L#Y^WEQgc?pkJeBfFu05g9tGjLcDk zec`xQ0<9zV$L;deIugm<5rlrwihr)*^U1>ejU`x3aG4k3g4NhtXx~}J?HV$B5*KqJ zpF{qprX16^9;~iiO{~G@>Sug~!KUKnjbR0Q8}xfIVFc{cMz?O0-cN?4L9c6%o9b;t z#6WLl^lgvnr@CK6;UU#k6yDI!t)jL9+ilv!70GBTDwxO zf9)hCpg%RE2fd`3Os6NJ2N7OkWb`x7q!sjrRsOai?qts_qDYD^mDZO-rg*VB&rjR~ z(?d*{XA3s;aO_@$q?G*HB_PTwImRqCDGDfdPg}tj^SG<9daXmz4 z_)1qzvzr!_EIE@TEqZB;%}k~(!h+KsQ+xt>e6W68Wh0Q_l#JPLld6*_lStBGhM9h@ zu+tR9&OgjV+(aw-Ia>2LPCS5OS+!2VuoyuqVZ=D?;8GOBA-aRk6kFeNwIL2e%#%}l zB`ScIhn1H415E5GTcjo$Wtkb1pXC7 z{~`lK{bZ&DV z{{4+Z-aEh7j+W8|YK6u)H;yDMN7xv-opjR0OLW2(;Bw!P8%%SMhHkwCJ$W^m4Cw?b z5ZW{0skLkGz>Hgx1f%Z!P||z|CGnb>8!7%v=TUTVcXyY?-Q67)cXxLU?(Qx@7bmy|f;$Aa06~I#f+PgU+mHO-`+eU( zx9YPRa^oH^Y+J?FGc&-4W5Da`ZM_*CRR9qKLHqL=U$pdk*QP>cTRN0&m; z?W}EIrm5$zZb=#cP^3yt6`4pPhkgbq@dbi%sT%H~$)}+aW(`2$#Z6aU|yII^M zPj(Gn+-d;mx17W6WvD?k+^PAvkK>9mQ0wR1Sk3o{-6UT?Wir;6pEDUtHZ&iZDaR|) z@e*IS4(+7X#^bYe1=0~(^XDuH8!M_3;GnN z(wj<;c9?HWtOx?(M0Lx;o&9=Ub)@>fC}{Z4vudUNuJ|-ajuiF}Z?ukE&i!lD4*MEO z?4C4V3l^;#P}M!y4l67qI%ZZb5vWowHGwX>_*7n0W@Jn;t%ns!0!81Fy z^d)$RgHPpHzZ&snNW5)~fMCcZjHdYt|4ETdr;@*vQ>e#8^=#U;oZf<9%|P@e!Kdbr zuMcEjLZ3C3kCkjI>QAxi2D0bv*@t)>v^J09=U$(*4!Zb`yivn^K+Lq|9-mB&8mxX} zi#B?C0qQ{qTG3->zA`7hksx&nkX*cl@7b|1%6B z7a$pKL|im{eD*o2f6Ouy4E-tcsZG6jz>U?cz5LhAD*|av+k3RJXv7#2vf-x53%m86 zT0Sbt+2{I`dYnpwDAb(^;OY1UW1Kz_)c*U}LbNZ%uh3?j)$?EZb3SZ=-W|O~#y(K| z0TA2WZ|}Mp7PiCXePv)~`!Touoxf6Yac#If$!P{UY|Z76&^KNw&2l6gSzZtN>t(!^ zysBY2+*qx=K%=~fuMT?cmW#5+oQT0O$)HK^AHeDt^*;c!Q|z}nG2dSwJ3^KcPuTBC ziH-A2RhKJexT9=UW#p~#aqJYFj;DYtA;e!?-pvH~JZ6V^=d~X4_D-z=JN;7EXZO#C zSZT9@oU2;%+o@F}$-WCwylBb`&z3)!%0?hL%rQg>2 z&wz-jew_865*!d6)PM>Hn4b*3hUPvx{OFqde{r7K@dD{g;}kQQp)x>Vx%ul0x@#ZW zAHd-8#s9hfUy=`JKi?!CoPuaacORcaf9gTD&?`W;(8EB(z#zgQ!@|PDKvLyFa^_*d zV8c>z;z)wvFsZpLJkl2>!pmCr@hE9HxTQ2SBZhD-mzK56Ju{@OA}en1c-%9~|J_87 zBMz0P_W}v0!-Wo+-Tr^P!WeVtRK4L4RVtMN#$oD- zsR+p%q7j%9JQ_hYQLS1#0oN6qmG`e~N%UHJ0-JKuTdK>^&+q*hEYGnLYbM$ev_Eq% zF?I4Rf?>#i%PM8Aq7f7AWR#`yGHQXZ@`E|enDqkf!E%So8SXkWsnj`JY zUS|((wvr<)8pFuA(HwU5^eQ)W8Y7+ge(x~e!d#=KeljKZz+_kfm`c-XOM zH{CSzN&ow#@$c#xOoDOJ1%e4M zc~OEDd8LA7u-a^gKa+cggE^F(%&8Q67@Bd%q4Lx!kr=xeuT?9d)WOJB02Ij7I5ZA0 z9JmtBv|Z7|mj=twOsHaxt;Azg;3bKaH3(c80yc)_tHAuM*^XbM5&HwE@iJE~Af;Ie zDDJ=JSiBH9R7;OnTY2N(pM3?2Bo!ajeNSv*~J!P?bTCQ;R(v|NR3Le^)Cq|>=`*@E1MSHBe<)BqE&C~;`NqdF30_lOk6AK&F zN~9wy2i5UzX8~y+K7L<~FoAv=hGHe5{XD|4Jnr7f5_>h(yu%rOh9Oz6KwXCkXtzsX z>fuMohw<^FU-M5vJtedPWMlBJHE&jRG(r{&qzLa|r``20e~|JrF3pAC^pxIFiDp>a z2dxBm$?fLW(;mO3EfM>m*xkSLzENGQ$P2=#srYnfKJ2Xn<<`%aPletU5riV20A9NB z43%lj%oLKDCk^eGJLKUg9HVcjiHsk2@`O6NG+#jK(ph)$bo>D`GV>B&wV9M>LxP@tR{?}$e0U;5o@qX@-<&xEl%A~<0Q_6gkG z{Zq=jgNJ~=S2;G8@K^QHLYIA~-o?(+ih0Jw~1v-bKUKijDMvA;5w=W&wB z3#kJtbE7jSuNKq*S)=&D(g}>=hyge9k$Dtn_r0;^X7l;JsHHgDy(w)8*{P6n(6T8K z*}hl}!jRw|fggt2?Xi8PJvOLO^$%c_<4tqP{1Gp%>uh7AC_IOBTvoSMb?63-tz)Q( zt_Ql%roF+N0E+AG<9I^0k_)bp7GItR+@@Vd6{ypq)>)Eo+h=8KRd%l0g|yezYZ7e@ zPfPVf>BSZrH@%R=xBcCs+aD^oIo|?4>6I>x^P@YGb?F*&ACkqdT;Xx-P;TAac(wVy zY6(R)(TDu7VW3Pl0PFmyesy?0*3H{7&!j zlp%Y)5ne(M_(x6nspTfE_MmU>30zoqygwLa_O75fEK|09&)>xp8=xmq9scoDOndC# zfK%k*y`g_=6vu&3bE7|~#?+wW4f1Z?{!Zpe<2hmeVYD>~Ewe`4o-1{lS4J|#@Y&^) z_XKZfo5$YicJt5bm67Edq=F1X)}mJX?dmG8t%W7{x4JjJM#Gt^3P0Ob&XEr&(7gz=>8@J0;v;GcWg|oUfdQzM< z;ptiJ38bntAKP!y!^L0<_+bjDMWHU?urj(l%NNiDv1q)@`T1oQu_L{hwj-#F)^0%M zX!9T&fs3yWwJzUGzFep|r=M*Li@8&z8ltC<3FTq!6{=v0=;S}$%4M2^TkXaK)5#Gv zT}?;V6d$X#V73cYu&!kDD-?Tv;}~!s6iueW0{NiZ#A(mXF)Up;AkEW)6Rp3}uBrF` z7c33Tpf@jnNgdc|6$v$SwmY?1Jk=Ja3fsRlp!Yu-uw;ta6FkXOnx`_!47UsX6;azs zr|q1@qhgJxp`bkqdwidix4v)~Gw>GJK4}|tBotoVB+(_ZHW-5mn?^CQQYiNVX7ja# zeX3aU+IeYwU8oK!k&J^m==ANrj#xEJC&Q#o2yd z8=CV|A2MU8;AoSch{R|#Jgep}$P?*EY-xrVb1gJT6BA3UQR%n{2O%_+@+Mm)kbH(3 z?j#Aha(PF)15nIKe`i~%uJdG;`pWVSE|>qLP8S@}YKbt>Mr|vK zOidUT2xg@zOb#3W6d;UB;sDrMU_ZYuo+RCr?fu!~LJY?ZUR-(y-57_;Q+MMXi`t)% ztC7at)f!Uj{%hNg{cBJ5RY&@nyNjXaqJQj%nZ|%9S zzuvE)-<<}Ju!mM+4RHP1_N2p%2ilt*3(xSSInTE_oW$t$Mks~E(x{u<#&SbtN7D1 zQyX=U2rP;1sB|-rjc7?*uk{-wYe=76tRlB~Y^&)9jmxHm<*a}OZd{DpF0;_6u(KAZ zWll$pEQ{+7=p|!e6U1f=E1bSdNw1V%afjQ@-H{4>r6bVfR*m3Twz24@bM&z_Ho$#> zWK7ath|P;upIqV3~!4+pxkFBJ*(I;#{HUP`{~Yc=T1IYwnYgF!+wG!e?46E{NI3*6i&&ACGz2C>ail-Mg0u z3X3q*z6{(?`0Ld7*1x)X(EbQ1JNt;&Z+9bhZ5NM@;34_RQ9$54Za&W&0qQFhXWHKu zj$DZ|dhi@VJkn(*jr3qPh09`>xUS;dZ>mb3bNqRt3AOtjV!_6*f)sdI3=0@9(%nD3 z1Qgr_F*V$_+P-Iww1h^pwola40*}d2w*`pyiq%y4wiX=@3+*Cmrj`{CFC^@=c_iLz zdU;k6p{SK12P3Gp@`FzWV|OWj%Lm+tE!q5c(}*8X8*$@VyZBV5I2x4cC5CjxO1?5q z0DGAl!LyCfqeT#uEn#${3O>M`+eNx$B?l(6Ju%!XZ3<6uR|Y3kC{1Bu z6fJKViq@G|&7285A57jr*?L&eS{8~@s zsMCOrMGXR1+QC7|6eZI6ZMG6zg&<8=#8{Zqfjk1?XO<2W-0ic(-msy=J8k{1)srZB z^g^;aP9{2)zWmhO?=?qST4G!u^L9hoEyos6ZEQ2iaL+MM(&mcP!1&pelk zJlAo(ldsF1@OfVGC3!HFg>mg%b~KBhH{P2bEM1v!UP|NYL)J}ESTpI&F$?nuGAB8A zn~eL_b<9a?V_|X7(CH4WU7*ICa;cBy#*BoY#!Xtkk7sug#57(TA80L1V6kc|$+Y6? zR(xM`{cG{lAAs>yt%h^Vh0Jj95=}iNAxEk{QU*d`=r*nF&Ut;2s3&?|+G+bu{+eH% zr*iRykS~)Sj&{=E>^BBa7o|vRenWbk>vT6=2@VULZtNcgJW)ld1&aR5cf%r1{$PFa9VVMuY%!?ga(lT*_7=-AIn@ zPO8z>miCyw%M5pf)O#QdBP{CTahlNdJ0XXEWz!S|AX&KB4>Xf-Xvc zHhs{<^q+;X``i=x|E@axL1(Np_E*vHIL*uU(8K%Bf@|!T_y5#U$i+wPuTi7z{Cbrx z1mtpv7){X3Ufp=abgcypu5^#>R?3)sIYoEd!e)E#wj$X4wbErS@)MigDQ%c&rC0cR zF4XUG%D&}b)#H$?5;--o4zp~s1XoWuM_zh}VDA;3A&U3W?YSQzW#X--Cr_U$MIW0s zu4GysU*g%d*7&%Y2aCD(TZ?m4YvhZj{)ASZyKmarjO^5Q>A4yG3I|z71&uxF3A&)G zzKB*$%nG4y+LO`RwV*ExIn{=r6?4xZc_yMakDa}EKq&rWZxxPaR;*y9siC61Z@EC- zYO&5q|2*w`gX34zNJxwrA_&2!Q)Ajk{FaQC)N;G=?<@F&HyprDMLM#|5fhU*}ft1+=lHs?$|%)BblV+CufN02N5l!hwTaP(Q8Fa*sbo@ zE)pBGeE3WFI|k8CE~J-H$$z<$PfjMJ#10AFVKO2zWc7Vku~m!w#*7Q+`4nU;dQmw|# zbrxRtODXq(=_i;&`wu{oK~$+(y{i#-GjI#m4 z)5<(SCjDnuuT*n5z42wM(c`};RjF;uUqd&<`)erWafj5(gaBue)!b33ZI-yhUQoaW zDCY=>`MDW&3|%mYKg%rR&zh!Y4yh)#fRAIg#wkFhQh`q9P(p*&ounRuLbebL%UXkC zw#F+L!JlQ|z+D4$S{Tv{IYVHnnjR6l_M(c5fAr;wn~#xIazB=Ofj5EZ6u7nf)`jh5Nx{w9{7ojm%#2 zXB)`^FYp8!6>pQf__R{>TD90GOprXXvgk{wg#G~LF zAUC&@KV30R-@*{3zY<_Gn7-3$Hj$}v@O>R1IhW#7XZCh_f?QOa~l_y%1hfnD01&>Xe*Lb`2^m6oMtIo3) zeB#$-Yc-}hQIK4l^Q6!Ex%n^HSuX+L=>R*7=AHNEDo1vr5C_p$>Ya|M7Eh7?y|3$2 zoxl&nSKC0eYrHXTKe!kIg6eT>&sQ1&z~k z@g?||J2%<^66A2dES{B4(UG;&Pl&khQO%VUEk{T{@4`% zRac>QKr&iy{uvq%Sr6H36#TmHZd&$lJLsK|^PqO^w&vvB)mrdowmN4i64-@qZf2uj z`W9nF@RPy|{B^vDRY9@TrW%)$jV5%_k-(IA7{q&imj6ZZfbQtAm_}vzmDlOrYU2;x zuDSm$FBhHsQ@(yTg9P=MF(E;`zs}`?f&2IT3n%~<6{jS~!s8Y*d?>xFg_1);(1=$+j`Uc%Y>a?$kO15-5eD_ny ztrvZ24Z{N(+|a|R%sbl+V_J*q>{;*g^}!LWQa!44f?D@k1)0osT~aj4r>JM*kfU?f z$CQ|o@1W{L!ObZrmjPnpO}C^JH4SPoK*{DWOB78syL~dq55}rq6YZsN-G(bESz>_f zjgD}~y?KS8GC9p>meL|%e41E9Q7xDEUZ?h@gBuhxvFwT^m83f0t4zx>mfYJyuU#b9 zsjlMluWDt-`&ye@xWKJ6E@C94(6lnF93SFHqcXn~fki2}+FJhzl^`uQ$|4#z_WvTL z_PxG4nT!wh){KLggW1Pm@5k##6gsY+VvBizuAU}Jzh}F%8*Np``CM`7eKfIF1xC}U5xYG7!Ul@B$UFMIfv#PB%jmIj3OB-n#4&|%>s*ae^M%jktq;J*^ zdHAZxN^$P_`*nD0pop;9sm8vQ_~1VPtEszJzp%YGv&_Ti8>k^veXJDKg0;Bq7fpG| z1KW-QiBr4?kMRxiwv>bFz@9>3IJE~a!NY=zHS?na6KGwxu4K`tqFVJ>?Db-D{v5W+ccZHOwr(S$SfQ2={D)7LaKaZ^mB?YjwNCqVU56*^iGtHL|z;WtH*kYB*S5ZxeYn>O+{ zu#R66bsRagJRJ@V$gVwR3y6JYai z%x80O)v9Ec-xO(c8ah~=pwLS*JixMFZByC=^{RUOS8ee;7xy{>jwbn3z1VAu;X(I2 zr1#3tAj5&M^Adxo^@Yn;Sh#`}4`QsaK{U8ko3r2w)97qTpD+W}g{HLla@Z7>jr(|z zf>R>(?P5g(4zPc9^+b%1zLlKhUIjjmYx<<@fa*f}5!#)YR7me%0ukqU3?t0i&KRA$ z@r1%8Y3E}MLLP_xUNn!Rv}(-RcC2d*r3Y=b`=qLpnm|STIb1zbmO)bc4&(^f(@z0R z@R_A*UlX)H5pCyzMuY}sVarQ$y+aobjm{>x7og{PhEB{>vy<=QkfMZc9sos%Y(Hr!moCSn*1ZgC#p# zQ_JHBpHJOpLMk;oATLGe-%Estu4|8t!+zEskXy5aPH2Uwozkd|wIGKm+?#GGc#9p^GfOI4lo#{TGFFJ4#%$S{8qpll#p$gd=Zu(fp>b2H6k}x zc?BFCLe@r%?wHhutL#GcOd*87%!tn7T6^GUQ)AWYU&1OBpkqRiyCs0?&{yWVUxz_> zXIdNVymIsW@}6SdA01<9tOISnMzdX&<5lbXJ%COgU2?_dDz#t>tJ&FT5;S_-3 z(CKNdw;xTkWT5_F9i7;>A*2>cvuY&+QUc0jJrBm{RM+20s-Pi+LBl~w7tHFHh#{Mf zKYuWq+ZGt_E^7%Xl0#v8Q8UjEX}Dm&`2H@`WKV!Ha=!%_NVw)av8&g?O&YP3yoqe{ z+_7coG4QO?5&LZs)b58U4vNnq<$6Wn75s`a^w3E4dQMSE0BWLNp{SW1@ee`6oqqB8 zu9f_>1f(l9>dlf!8ffl6i?b7|VL#=P$;4t5=FPCT;q|(=rFqFBSL!hkr0{`mvS@3( z7Hb$%0_0tT5gB+TlCDuz^L|f0f-M1t8hunM8}COSNSw~T{rpQ`$Qsz20#VGmYNa%3 zi5VAk+di70)ZM<(Hv+9rabod5Z&0xa>#V(VEw;jVMy+;2!G9hO{$_>Pk#5s#x2%^3 zc)c$eGp6uTtoI3j;-P|rzXVFxCShowqEo2p-2 zDN`J!HMY-zP8W!Z#b~H}?a!Yqpn#$U>sP1$j1!gdqKp1=3G4DQIXy{>EoCJvPaqx8 zHpwP;UV!J_&o($0v>W16rsLvtK1@YccnoPdq=R-?ZRxP)%^Orvu zAgOyLeg*}Y#4ciTE3>WCp=_pv#w3+Y>)29Tz64v1qC=Ekf*1nUXxURTe1qeTq@yVA znsAFb2WRdYbLUN`VxQMoM1AA|jH9c6IVX)b%16b>yPW4&2nPwupP))A9pCW`{H!lX zA%^alJ0{DuuDjFL9-i62sQs0?avZ;Kb^#*d6BX8cYx9A`IeE6-#)h>D$0QToB}mue z!BkAAJ6ZHrj-e!laftP)M+V+Yc zTK@Uo;4M*n(e9b$f2ywa=eR1xAjIAP-??g-B3Y|{L?iLGX!_KPxW2 zCUf%eOHA~gYwEd0&bjgk<{64+ z){*=Ph#OlLy`y-r(V)N;WeffJ*|ddruXM`DO?qJVxgLVCWD;<&UJf{9q`cjp=%^r} ziNI{TRKN6M)#|tj#iG{G5rZETY1--J_m{XdBdK_ftFOVj7_6@l>de9!DsO>70-}HG z665Iy4X&oQsA&$~L#>3BwWd4dFjhHADhwW=ZtmjSLj!Vjw_%PV)7n=2zK%Te)$Jf z!pb=BR`E^%u7{yEVJoI44|e+*`UNJ(Q@2-@F~hFXy91(>5vQ@Esr9 z-{EEE8!?fdrGy$h1#~xStgcT|xpHqmTpYEV&(XKA3&3$btR_eE%{{k#W9HjNnW*+N zJD1uMmjT70;d=t&Iud%Rr{3HTuQE_oP)_y9&u<<5KE(S|o1BQ+fI*D)Z$BUIdc|k{ zWs{8D!5A)wlfI)|IGLdhp>F>r-0#`P#)5vB+zuJp9PSEpamBvm*k7UcyZYf&<9QdS zNia!UA`{{O#gavO0!{Wp8jkF7nd9k^_L=f$Sk5gJS0LaJ|5-T;iTHG#&w~S!7W#%H z!%!``{MD97GuhO^^Rdsmc_RNZ(1D}>^`$>}>S%+bRQ)r9@^pXUgvV*_3G zk>9ITP{89673V(Q|HFH_=+zC4oy&*X46N-)Iy({^G!bvIjQU`B>t6@MR%yPbw}r6R zB2CJuMu9o;dMvFvN+4eeYzv^DecD2oaTo#z#GT-_#VDd}OGEs+u@SnKYri`Yc=1&4iJ)$Chf)4!GeoUvORM zGL6J3pfRwgCDZdp}&R8pFLto0^FvcPM$(!+87h;C@OSz|Pc)e%NMkZZ5YqWqdb zL_v4(`&nfE6GW6^;=`I~*DtP`>7zWVgtd!N_eY#apt?fiW38$acFd`of2B*_m)8xC zFhpj7Z590 zU-xFsV+#te%R|J+hTkg%(&u{Kl0%Kbow7D*^iqyJLT3}fA#CKz*P$p83$-lPmQb(I zM|=6%yL8Jb=({5erTj)}pmtuh1nBLMr>Yp0Yc%v9$`C4$_tZMI5jvie6xeoZ^4jNZ zSc_YTg&WS<-hcd?cUYQL*@bAPH_CL*NM%^PuSm)~9F3AUJh{)l6ao%g zg96h9R}5vrhqm`dg2`^4NoUvnuOffRC^>HTNe9GWDJ`%s-KS1A5}E24_@N`m=t}0W z9x0ne&2t25JJq9&`O%ig=;0-D_@LG)4NVkxTg~5=Ro(O7mQsk;s2Q{9jZvZ-n4@Z@ z3DWB9q}gF##BeTu_DC|-W=cQdEdQg9h{Yb-b>?lgDh0~uQ{RUX3YGj6YL8d zGPx#elHh=4e8H6rD+S7ODM?2j@{8bOnL%njS{dHRLAFwp$Zp0@+;qgjn?=Sl;p_C`9+u~j4 z+kwG#npI<5PdzD~iIa!RQ+F4K-sb!AM#?h%kc#+dtR0s2X(+*I{X-y_d z$cY@6o?ew+L{``31gUwy8B6T8_ditUum2>6cr}HY(FCrJ417y9NAsrbcfbR!MkW8O z3k{W(aM#98M-yAPg&f5vCna#L3{d-XHLrnm!*^o8gy#E2+lPO((fyA3@@K4u(XtwT zG?#~JLWpBD-2yz&JT=C9(8Q`)+b_cX!MkL_p4Pr>9Jz~Ysdf6YtYMUO$tpoAtQ)EP z8e(U*2XR(b_XX~WGPMMSBvC)-4>=aYjVBYzxR!LIK%(O=WUU3*h_;A&6*VeyBW`XE zYVe4fRTlUe8nh^iu7pQWmi*sxc*{zLO0w)x+j@k;4 z%aDzGw-~F!r7m%y8;IuJrza5`Is*L|x1@#=mQm0eJ6fdjRG7nH5c}g&ZeceFW9q5p zB}tDEI_j=xd>TcAFWx?Jf4CJvKsakQ!!_xtq0I|psWW99QF+!Hr0h-eA@cn)bRccM z@zI4tGZ}6HK)$SSU|6A72uHNUj+60iEU_;Rk{_CP&`MI4v5HhKkQ(_y-RvMdQ-Ske zf6RWD4cMRjVA@sK?X%+}?R>JxxI`4t#3MRsrjCfrwOIXckC#{pOsXiOB6v+&L?=gesDm!rU%zlSSd<~>aMD=?8;9q!*cnJE=Ngi{y|8xfmK>IT=~pUa z@a<*3Qh7TItUW^9!IbyNucLT`u{IIcN!%joT)}b{8mjo=51TLtWucX9y`wRS8m5!8 zU;DNCp!43EFJehlTu(qJh)_ZqwI234Dtov;g{^X%;V zU|#bYvRK8M!@nBp4o7Z#)2Lvs%Cw(0%k>VyO^2fyq9C#o!SKRa-2CSM$&2FQS9R?3 zh7h(FKRDuE6U=^dwopkO_PfPPjX%rYxlE_VRWDyHp%KOPs6sJQoQ)RN@I*5K(Lakm zWYIxmj%{hUxlxD3UY-?ox;8U-&5{gg&LWGs`XQQfEUgSJqpVE<3x3tZ#p#{uxJZgH zDc_tPBjx=)2~WL8=?==34%AKobux3@G87w^c3lpY2ShdNRI|kuA}>}|hElX_NGL2k z{%fUZCLzF@2M^tA~+%q z?Ej5ryn%HvAyGvVO|VD-2rA#ItSO&hb&rEdS8I8^4QBz#MrXHIqhv#u0j^Yx4AY^+ z=-VB&SaXvknw!x%qM@f7K1#&;uTCFUgI1V`HEDb-7PzwPS#>A9@x(3LZ|OSfk&A;p+dvlK0QEiukpxnY)ZrtyhrU41=W`RO&5(e|P(t6JlFG0!Q5 zfZ5y8D5OlAA=!ZJ#Uw^aBZIc3=4WOvl|Z4yDqtk;UBCH_&e#@xG4XZbU*|#@``H(u zPu#tXr3sG01RHTwN3PdyMvqvi-s#{Jaa7%Dj~L89)3x2fVnhIr zJ7n~(paef|Z$ggtn8G7Tv;%FeK12+{n4g9|XbFhRh{Yab9}R*IYI{m3$CwL8m-`p> zc;PuDnT>ZX9DHRz;K?fWKV&>-KP0Td|2^f<(&I5r4>m&RD=m4RCZ4h{2-4&rpcg-DamDV*A3U*I(S3Wf84CS7pmvms^S?^A>UjSXY z4)k65%?ELh?@)aQqpEq!*~U#TU0>w#aj8t1;yUxWm9Nx~2^)&dHx_^9TVO-YNqHT^ z94Q-m%MYg(EKr}U7=@}sloSz^Q`(C<;;#~{A0jlz{_?Z8srkkSgc2quc2Vy>|7ZO^ z1^2Mnf3J3WQTr_SvF$%A|Avt#pb>G8VQkEy|G6Ck@xMKpHF|LdAiez`frdb->NS^w z3DNX1CHwC<{~=LbUh3v&7n`)PF7YlE`u<7baQFLWHU&5*xOVU*_nD0>ed_2A;O6|x zaz|`o+TQt$8r}%0Ja07K?LTRp5@ZqG0a+TFe{BED>lpNd_6MY!4(rIXs53L^s#`d^5!A>TaPIIYL^M8$nzp|0DmYi-Do zZa^(vlF$iv8(vkVXYRqhit1b&s)lPKF>Ul9lYA z2ns(AVg^kM+ldumMy-iFFGe_IAe0Rwf3J}G19(>lzxn~|ClFP}(i~9M8;I}`l@n>0 zIvyLcP9Dqhyqm{(9c+)x#(!Uq8ln_Q#+!`d)BOPyv9`vnziVhR3rQ}G?YMi1??5)7>?1fepz_M>q>_A67P&Z?Oz2}g9)gl0-|zSl5z;UK46$=8V? zf2_s@xboAOsX505`1&EF*mzO+J{9}{$wV#3m}3913OFlqwno{Oa;KUxwaDpGxK$60 z4|7>7h%4=8ayEQHDjoKQ1k$&G9;H1k}4(?ndyvWw251&g7N4|s|vXCh@%V^TX5K=D6X*gHd_7&r_Pj1mJfnC`<@xHhK+FJBhb1CA>m5N zG$oKN<>Y6rlI6$EVoBXbo9)nxHi9VO##4Rec&3Oxr_v~j3o2=fm++k(=R6l9r<0LG zXm!vVTO5~ncxX6|VZC(#spTh04I0PcXAzZ6D3a)$=(1pi95E$TY5|(|=68iI{BSMx zy5)8W&<`=m2gDAN20@vei&3mDTP(9~7AiA%gIOs`Np1@-h$2uVeuDSb^Dh*GI+zZe zQ?twIwvkqUQOmkh^=g6)q1Fm+un=1tjes}el?XaS<_*9)S=GiNWrM)a)Q@O{iJYjo zG^)@fjNI55m~w(ao)l?EFx}3Y3ez^~wBd0MeO?3AfI01G7)GL$cshIiu4FYCMe2E6 zifX|EN19ar<4|u<3Aj7ON|N{^=7Xj*8Y3M$zWh{0osX{zzFmVe6%v!?o{YRg^r`xy zbhE2)(KDBIGYKF&8!bi6oaWvfe!>tg5Oth(<6)uZ+ZrY*4j(e?3^3`L3)VmNHxz`_ zaXWPbk0YH^>B=}AxL9Q@A&&SxR1VgfYxld;=?i;CW@fY+(&aN_!qZ214%}wOr`oQJ z-)R{`rCbzcA`=H3;DykH8V_zuVsS;24k_e=B@tH5h`uQO{0_Gl2@b>9I~|eO14^nG zoCkW5Yy+_}%z8t-pp^yU;cGmV9~AMGSFo3DI1*6_K~bwzaH2~NNJhe8vydAN7El}5 zakrX-72EV*ovx+SC@BVEe^A6BNU?ob)HtUf(`hv0O$W`JnE8WjA1D=0*J1F_0MKs}rv;#A#2{q)L`tL4pK9gTyhk8Nccr72tZ z?4JJ%TB|qnhFu)M4dGyL^~oMReTW3^Wt4R7=x6UR>UVJ!+_NF{T~SrSP))sosJOBT z3@o6H9haAWXmR{WK{FAR7Ds8 zvXip}5XZYCw7}a0F>h9qk3s+!u%!%E`)K?Ca-GA`soD&afGm!iMIcXDRtZxC<$1dDGx+{FK%^cq~=JQpI--D4-G3Fi;`ejCH#vNB&ECp>V=96C% z@fmQrd%ySsww1*C=E;QM~qv&-t%9E>9g$t*gIjd4{9nP27D^Qp7aDP zU8F8f`;I~rhc7Ij9PE8`jn~zWHi^yUnl%N#ETtG2dIyiY{6Ox_nVNC|7IgMx7nI>h zZDnfdPLgcbLQXtYiQlM!BM=>F7R*3X+D@t8<)Yh)$X^_;m2E}Z6J~vHP|=W zG>48vZDw{hn_Ruz+E;WoYu@@Zw@t%cP zeQ<)>3P2CPN*M7+${Z1h|Jnrft+gc^skV(W=>o2N==!oQ1bLasz&Z67z z{rOA@oma~W&u>fy(X8sq2}EU%Np6Nez?VKrywp+B1*>cdjiVgU9d35^FRGnwy5?TR zc$Ls~Xek$(McFg3t1!U^7C!r+#sg#T4UL3S1C(rg!9n_BN%*8IX<3uwI_~gaa&%nxoM+`?i4w?Ei1v&cQI&99Y$vL51cL<@|>h4F|sTwP4b^ovGzB?+4X4{vX zBqtFh4FgCPWKaY_$w4v$LkF6Q)pLt4)W=lh0&LgYVqa;&ybdVvan@ls%{C+Bp%Y3cp6&Kn_+MIO;S+_=FR799BZs~>J7Jl&y1Fv6T?oOHQ zFd?I8QC^zT=mh4veNSeEQT;tR9)Y-&OD1-bT>L)uN&Eqb^g4s6MD{u7#qEAR(!1OQ zj8=)GV{Y`6?~_`GCPYq&HMwGNU)ssmcNe-2$bmLR{qjk$^EppPc78(21YX#-mPUi)u*}wmj*J0f^;fs9IBG@Q(s+{ zx)S*f|EEYM@`i2y`#tCNdzD4RUX9E2Ja;ugkWDs3$MxTlAAKX>Nn|uqoSU9JGS)tzPI%Rq#}J zJmpsTka{zY!d1OqnrHK>ot%X5;%)Iu{3G%72LWTAwBS9JXzaUtbx0(UJ!x|ny!UQ3 zGZte=jJeNwdqC(Oe|xtW876uq$tXI>ib;F$xirlacteXv;D%3g@gA?@Vj_Iy{IQ4l z_t9^5cG>rbHl7COw-aqw(_YS610n1rP?o+cua6(^c%~J`L|VEM`61vY(axg^m zpRlAaddi_3v4&k5f^L(Nwa0p(j)V<)@M99v5q!Rnwkm~2hn5f@yVOj)jm^5$o+Q)( z#dtqjGNrxIx zw|Feh(&AnlD_3}$Q@bFJjj}GS&uZh3)5)gqEbfRI&`+#?&RBDU(CWcUsv!0n^{#Qr zR*wNU9a>aE-MYLP@FLa?L^rlocek3B`_uS)8rs;>8nfa%hNq=t*)AiX^cyhboV^ROgBN4* zbLu_SgPWLsV$n2lCmtcoY-0^O9))r0pogU!D&al*cr>LhvwZYveIHk}vT)rr5!$x} zY4S^|LjCO6%dsYIa&_1!#hWlk&0mR}pvh>RH`roY=997HeN%&-M3#MqN2oDUoGE8$2j_Za;F^=$3%B7rWQoNQE~d4%lyS2nDG7X`!FA( zN%K>N+b8K~9lXwqX@~95n=o;FkBfbh5oWCNs#8eRjMXH;+yzU_;;oPvD+#kPPkYa0 z187J1_y3p1%O;&3M`KegaOH1=Z1ste5y1AlMZ}4lJ4%@!O-4yFDy+1Z3+YV!WY$Fs z(2Om>8k368E)fYM=IUfa8)A{3`VocO_9D#Po6PI-!yBlY9X~wBv$0)?eO8jsY}&XA6Im1ISs#(uU~&>TPbZk5|H4b;g~CfX{I)di2!9c6j3e&>;-ttOlUt zH&kp$205q+R-<*^Tz*0LA*_3495{NT$=`{q zv8|3@lFiQpU(*QZUffD9J;b&W(_#!{d_EqsKb;<-gSa8#~CT+PZtmaBh zYObXXbX*&DZ**VB?IAzjC;J&Tvb1-U@gtO_?9eos2x6*5JbZ#~=tQfOl2ZZZoy-P` z_6TWo#gf&tp;lR!No~afs@*Bkg6QTJ{1+Q+83f!C7-jY|xcQ5F^d(x4?Z6m%uu~jv z4CD2s1!^lzhHCI$muj}O!Tr||@wkTDq!K|Z;k~WlWS*N!;ivR94R^>W2F)5x)!r3) zu!LVc;4#6Eji=dzc)qgTT_kG2kTwCo!@v&Mo$x5gXwK|60T=y2G1ta~nv)M-uHnpO zGy^-)@Rl8r=(8MM-?r-(wOJJ!99lvlYJY>?HP!E8*E8a>Ls7e*RgWyVD*9d7>M5jz+@I-N zdac`8{Lm2-)eaLvR>v_9s%B@Mk(7X)uDRXYAj8wXfEYW&BkZ{i-=8(P#VwTc zJsKU_kpJxB-|1N}Ha!dYH4EbGl1mZp&xsVa$x@-W9{g{~FpOV#VB76y_Rw0F%BJx4 zTSW-#{kLSKa`P%P+Wq!_VKguLCLH-Qp7)+Hlsd0EI3T!FuSUz;34(fV@(mY;ot`pV z)?QOyg|MmSr^aY(DqW7PEE>W2`I($6%iMDObmRrzCTH< z5gQ~V^{RizX@2*w(J-MT#?|Dz&`>D~3WCr#tQ6#LKp+YxWDy^mxR7)&2@?suYpaIl z8zY=?`e?9qYYMFtt-X$wLgX)uIr-PO^7EO9J;dpD;QRxqk2~;o2?fMA6 zP;HdKmO$^#uL94%GCrc6f~L418=GfNB0mvGk-uU6$0=xJw!`io`xbY2>$__H;N_QW z$;?6$mfdGRx^N!S(?{h5tv&2>8;cK7BCWN#fx}Bn0AmFX(>JU@&|u`H`~E^T`|)hY zVBzu2m58at3PU|v0kK_ZHJ@i095Mpzq1U=Psmk}SS1l=qp@PhdVFkLF1Tr!p5cwOK z>vm9(2X%96M+WH=W%5Za)KQ$rl+bvxTk+5n=oS+9OC{2^8VX6T#P_EMD-3mabY14w zwKWMDFY8AI*El%qQf2OR7y1ylZ0H1L3duU6<8i%iuv+ofS;E0W6;SD0tZ(0S5?cgA z2d>@(E5I4+L?u^l+wY3Se)VjV#XdFeQCuunrZ_%6H(H4v4sP%wDT z9Ui~p9kJiKr92t4Rqw~&JH9_Y*l%6=wSTr*o10IRU?x4CsRsHvefBI!Hgj41|Nf}g zsgu5c&96N?Sh7vt(jvq34?M|-eePIKR#h~z;y&5nMqBVs!wR3WDMj$MnlMI2p5YXP zn7#ERhS$OA_83Dl*jdL@OjnJwa@VD#OBqSu z0T<(`sNU&aH^1KW@97b;t>2@r=r4n0J9q|t5`b5K`4Vt6Y^M3p-zoR+!t<|h_&2Hg zbAwmpKNbB`?*22YzF9@bxmm@Nl&7)&cuLD^A02|H@!oH&FKcm5hV(>RAs183XfIHsfnz@8*}J`>OQ-OQAo5>tT~j7h_-w*&flG-pCd5&aIio1B}wt* zN91-9h1yz>%#-^EQpLH~JbMyjN^d&jIG!d8S^Rq!$=KgUFyFNijERkjkBjk7BN!V8 z1DAr5hMbBObXQP_n&sLE25jIzCNMzt+XUX!Z0T3}M5Xzq(s6>*$3bP93GHo@17 zZ9{y5vv>%*=)XvKRKoJ~HG(fQ-FqZ122~mXgf4u*A2F`abfzA(5H>F)FHFJb3}RPI z_9U?T^h&`j%;McP1b#?DEtlR&#|OQ&C~HkGqQRb<^%mtWPQiNv>^krTVGatQ&{tg8 z`fPC^{1VT~adn;ROZerx*b*#G^i|v~rwk>wX4CsO6&_ECdTR(eM~m}`r{0qbqr{En zh;>-vnrC=WkMMiDp=8F&#Tf3XAQB+O}r*u($lg}%7Tq5@-(@DL4a_~yas|qo1 zC^^CATyEpKA-8wSSH+lcQC1KXuTFOOtb&-qJuDaMZ#A@*H%W3f5ivU0O1{AF{14wS{VL$o?jAg>6RXLe*Xl@jRGfRLvEGn-n1HII^HojfdR% zZajXa{ByRhij&Oh1?fNkQ8}oTxQ_6W#6~>#@)Imj9>0Nb2NQN~?>yO!SG$U8P+Vek%zE-EZq=db+yk{+;#x3`pP(*^@?yydWQF1-I9obZ(g_de{j zga`B}G<+84j)iGY<@rJOk_GeXZ1#wef-k+3Z@T;6W7p1k@sppK+WScgCMARCo?G!O@3XVO0 zSuNPHk9!F5{1GLrBd@F8b(ufRR%D3Cv(0wX=J|zTpjFCClp{QBkE*sU51S$}Zf8Wh zDnH&b(J7wkM8Zs7-OFu@Up;Hb&v!N;Zcsv)rZ%)iWzz= ztkJ&zgD$EAk;SuRbs~S^$R7ZTFXLfc$nr}lo|C%yrRj%xubx;=tT2)xk0?Ga{7Z}$ zpI}(5@S=1tzMJk_R|^{~VIXF+LZo`3-$AQBp=uYd$Ktrsi2CR3bSkRd1L4Ib8^2Ax zrqRh_EY*ggh&Y+Dxj=_^U_pZ6C2LT)oT7e8!5h;;hGYA@er_ZJ1-o%R%N>@4_?nl~%7=1?VcW>0If$BgM(#*b^3 zWERvUh(zIr2C#-=zwC^`CW&T(MGFm%`-u3|}>=wAUdXTczs z!%(wo$7s5XJ@({3fZA`!TcRbjB)sGdV>a*A!9QiU)@oFtZ`snX;jMXk7dn+R^Vwaj zWHd-Olp2X$>D5>NnjJ>9FoZ-;rR>BnXr{S}A%_CKZ^VCkQgTV+aNJ&aBQmL32eYH> z;f6b?M%-gzpa|h~XzCxU6NLAzj74WzVg%~w9>%;FBbPyCOe+>gAHQG*nUI4`&`q`` z2y2vFic6gIXOSd>A|?26p1MEZ^w@0;WETqwzkkYWT}NcsX$Ht8<1nm_w{s z7CZDBiFW~51&0ft91FTEoh>rf2&Zf^eDgK3!UkJAQ|AXs)TKe;Wr9pOWdyMmY{W)G zX82ow)J_XfyFLW4X(w7&2Z+U6d^L{~8T>v$x@+bvrAZYoO^HW@?RDnsO)>tybIo?A zhu!~BssNo|zus$g2SW$|%M0wa|1Y3^Rgpt7zFvc9Z|GcF8SVT7P}w0wy&Ds0P%*Q5 zm;kSe(qA;1msSi>2wh+F_K3zc=efvdsA?OJdRMVpxi>{aP4=JKfF}aA8N(nCvjGU# zxrT7bQd?K;&EQMZ8Z?Bvz>O?-SQ@Q-nDqQ%n$b~vy&m2YgdVjq5f2q(MisUv@PEEW zd7`|n#Pxq-f0j#1q>dQ=P=4N6s|%sXjWhiwN1_UAW>bc9ii&!a40E1J-L#&l?(CPm z;|9bm7W!yz3>p%^5{(_CIytYk9iTe7Li?kO;@0=B#@br?hU3l(Amp@dkktz#(1(d+s18R(koth)575vHi>T_ zNO_TFI%lvKydUHT3ao&!QU$weWlVASH61XqS6 zQ^2A?x8RjDSS9+hAG&p#kfpquO&?wEWk>Z2jCdY%nX$zfOXkUHqPtv;3~@?t`&lnk zQK-{Cf2#Hgf#6X-w(ZU=Z^u)1wqOFSw3LillD&Is=4XB%>M%CprvxqZd<}F#T+oDx z4sqGuFdqAzxlNn#5AVyN+V@KD(K^y(kH?TREzvHe?$ zc*|*qd=fMF>VWeu{iDbW6S?<3T5|6%Qh|O68=!Ei5?{Qg01Dql-zr8;j z`&)`|`{IQ7`Kg!i2lv+gmD%ny;bwp>+cPQ}?vMAI;%{x7iUe9NkfkKSm8ImT{xboT zZ0J{|6kX4(si-MfeHEw_)s0+J=9a0+IHsc%|C^q=K)63L!^*YCKm;h zerQX6MO@?@x~jF*XDMGrFclR7kFh8&=)Zo1jxbEwDk+K5$ZuenSwCKhENVe2OOeXb({qlHEY3ehP?+za( zMa%yuvhVJ>Bge0voB)pI$V~Q7&rb0d)aII1Te`l9B2~y1HzRLUYKZd@2a8;u{lbX% zmh6zE^vA20NVvfF5+0ZMs1bN)xL`7ym1wt?zSzE;DY?-0E=27de1+@(tu|wkIZX1k zz71%Q@kc^SDdXZ0vdc2o6$)qefVRjSD}b4I$dG0Javt@u>( z$>i+v_ku)2LT@Utat^rDTgaknP9a&q+(?jfD6y17Ax6l!JL9RMYeQlwyJNSfTS3rR z9YI}MsMNUgsb@4(4=kM~Lr68@M(P#r+snkMxClonU2g5uh?-558d~6#W~Dj=CH0fj zZ=8wS6`s%UnfP7RQKybExRWw$`a@NjhMf^@snC^>#h(l|G8Lw)o9+`eO(rdM25FA*nT zzSLzSYZGn`#cHpgu`7}e#eafsv)it7Gl_l82;CO1TU=99Vpn`P8@g?lBT0)YSF20R zxs|C_b7FcwqN$S7GX=-78!=S)>Hi8HR)%ZqWzi!agVGjQm}nrk*O%k%Q&CDr#R>itTeA0IUI8|CgZqp zL$x^W*sg|yLP6Z2NSF{ocqYN042U3{B{Y-E7>6cC`w6*>28}-@D49C62$1f|xgAi( z$2@W)@`7;VH(EYH0W?0LIovE&tooG7G1Qm10W^K#7MdVbVrV=N$H%SYvyJO$pWEoG zxW-Yjc1C0;%3Zo@x4!mLq`pd061&!?yn3|9V*`a8-gGZ`uqx(<1v4>zBvQR=00eXp z^QtFb>vU}ZL{jW} zfPlG07!~_QCy)eLguQG5c%H_9VD@fm^nsXCH@N0&*q0go=-MbdwPR-wN4+>bOGClFYM;9X zj7}#NN7ZULl{y=Qi{ydvz^)^1L%wHTi8xGGIT@<{jlIR?3j$EKZ1Yj^#V*}w2G zV5ZK%m>Ke;qz>gb>b3rH^8`QW(97GwrOfVfK`f#EjOHzzu1)a2c4{B5`L7eV39gUV zUoiVy{lZ|4%;fXARYN@b&T(UQ@xJfoqZ_?@GZqsrt>7>0GjB?sEkO7>EyN;h5NnQm zB$ufdL4(`=IH_zi5z2gIL!W2tT-avbmRMh^BiACYp46B6zpJ_LrdagUtyB1GG{xjK c%P)+^<0BrH#{pP41myqb#UBV+`)mCF0PLw;HUIzs literal 0 HcmV?d00001 diff --git a/docs/userguide/storagedriver/images/btfs_constructs.jpg b/docs/userguide/storagedriver/images/btfs_constructs.jpg new file mode 100644 index 0000000000000000000000000000000000000000..45852bceead92b139a686eaa9c42ec3089f77fda GIT binary patch literal 63773 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ž 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⪻o!3vkq%V?()cR5 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 literal 0 HcmV?d00001 diff --git a/docs/userguide/storagedriver/images/btfs_snapshots.jpg b/docs/userguide/storagedriver/images/btfs_snapshots.jpg new file mode 100644 index 0000000000000000000000000000000000000000..94c7797aca9747e785cb701daee1c052fbf7657c GIT binary patch literal 19902 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 E4dWrNPAwhaaKt(}`*iaE7HBv)I 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 literal 0 HcmV?d00001 diff --git a/docs/userguide/storagedriver/images/dm_container.jpg b/docs/userguide/storagedriver/images/dm_container.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2a5cd58ccfe54fae696e723c35f337d46dbe5b37 GIT binary patch literal 51563 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 literal 0 HcmV?d00001 diff --git a/docs/userguide/storagedriver/images/driver-pros-cons.png b/docs/userguide/storagedriver/images/driver-pros-cons.png new file mode 100644 index 0000000000000000000000000000000000000000..6a13f3343cfaa3361b46da2df241af568c6fd175 GIT binary patch literal 105762 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 (Io>hJRE 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 literal 0 HcmV?d00001 diff --git a/docs/userguide/storagedriver/images/image-layers.jpg b/docs/userguide/storagedriver/images/image-layers.jpg new file mode 100644 index 0000000000000000000000000000000000000000..378e36b84991376ffb23cec090b0a9944ac27d9f GIT binary patch literal 26599 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 YBP1+ruJf5~^G=~Ain#tyvi|=~0AAkRssI20 literal 0 HcmV?d00001 diff --git a/docs/userguide/storagedriver/images/overlay_constructs.jpg b/docs/userguide/storagedriver/images/overlay_constructs.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fffe07f5601263564cd0ed181991e30bf2c8ff20 GIT binary patch literal 49536 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 J;3rnb{s(n+DNO(X literal 0 HcmV?d00001 diff --git a/docs/userguide/storagedriver/images/overlay_constructs2.jpg b/docs/userguide/storagedriver/images/overlay_constructs2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bbcd6e0057728f878d46cb97cb1d77edf7133356 GIT binary patch literal 84972 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 literal 0 HcmV?d00001 diff --git a/docs/userguide/storagedriver/images/saving-space.jpg b/docs/userguide/storagedriver/images/saving-space.jpg new file mode 100644 index 0000000000000000000000000000000000000000..125d56ccd86ec96b7bda77b6944318832bbbe1e0 GIT binary patch literal 57009 zcmeFZ2{@E*+c16$Q9}0JB&4z>Tb4``lAol7vP~ju$WDeSLfNJWA*L*;gk%{pOpK+n zWy#pbShEZ>h`}u1-Sa%(^S(d7?>OG){U67BeE76M3k%>l005l8UKTZA4|uj8Y}TwS06X}}0=Ap1 ztbhCbeGXV1{QLRu3oAZ=4g3I4d2m+z|8f5NK2!hOw|xTKeFMSYV_yP5Ig&X77y)}& zSbqQg<6vcH{eA3ZV`F9K*vrB3yK(O0+Q-ScpOb@QKllFqTnE5|V;>JM_W_>Y=f6Ao z{rK;vz`p~W9Gt)J`0pLe&j24Mi^ZN%R+huS9zGUUJ{D#(00ALo2kG;hME^Kg_OPJ1UvbKwEpHUM z+(VXAzvUmbZ@;jJsF=9?5rw12j%#RYX`j;3H9BW(VtU@}!j-GntZlB}uyuC1?ds<4 z;TaGZ6dV#7b}#y2Ol;hv$MGqt&(hK}o@Zv|iIY>)GqZC)=NBleYwN!@HmO_Nzwu%LSpSaJKQY^d7axe%9&irW zIDX^BvL^&=tbA81v=g>gV4j^fs z8La`x=y4?`@IiBq@#x-G--a*!nSmHJbpt^*{@#==9{_6bCRSVfOF`3Ca zaw%pnd0#_`PE-UVIt$9!Ys%0-G~vih;D2}P)rKG@HV83+9&Iv`v44sQ?1%3D0@zTc zUXO$@ftRyLCh*`X6G(!PKhU5|z_1himQ|Jsh*JnuXVnct(H% zn<%@f^lzg4n^XROuzX&lm%PY@2Aen}rz-vrP&%-VX>{J|vqP`FUoKgZ;qVA`;Xi7S zx%a(IH}*qWti}pEHEwEeYQgp4`P$pa#Qk=|BdxOWLOVilo8C+uNI{5i@4vtQXx{&L z(?KOcR!!kIn7~c1Zu-Gr6eiF{zqNK2l($wt@xt0R!XaF*+~3JZ8A;r?s5x}-LuV+8RUH z@)0Pco$v^;@G7C=(drU<2>wSU!AhzoY6q&A>6m)c>yEc3^B$Z8!&Okw{5q$ zNQIvvA_MpEv2a;G;unln$qj;8?WS4cdCKWGw~906(u8D3Uoy7*4X#{;BX(ppyIZl3 z#3Q5qc#uRG=Yrp0Lj9TUyH&4%ua$R(@C6-N#_9(V90Rzg4gyr(y8 zJ5TZO_7Z+|Ab6vt!!U~|S+SgAs&e;6fkHAcAA}vLyG#j5(02-YHy>g(pQ}_`joC3k zzLPq>@a??N4>dnQ1cu$^ZsYg#tuS`ufZ>lj*lX0~d%1nAiX~MIL!~M$^pRQ8TXgM7EVWc2wA#OPu*&d%yg{f%i)#vTXJrzKZSnG{&)!+Kacq z^uY~@c1V5?uTtc}`SQ`lROQZ`w)FC}l!(IDCRYawXy_vVIWG3%%f+zc(ULY!M9A=@&Aw7-8N# ztQt2*?reAybnLL@M>D~mV<*@TeM?YtX!&q8VwBcYB%Za(1Z=~$GJD>}`W!E;TwNO) z@^Y#D<$D?X0K+%o3xTs!Vqex;*B5#+czZ}I**8#^Zzz}f%H_!Js6LTpF^ctn?mw8A z+Ukd#w2b;d6VGa0Y{qZzEeZ0unQ@uiS)H#Wp2*iJy?%gU{W&;IiSNbhGlv2ahTGau z2NsyXo=PlxS#%JW>QarZW##Z@w9*}PkbS{c=fHA+;?g{1#LoY%(<}E zbj)kAc%_2d{A$9LCv8D9CbF*Q_fKj)Ile6a!i68`yc)JrLP9LQ%`v+)ZS9pQ-&6Rb zUfHWexc0P9AQKSKhmxn78hELQ_ZGy{0T*g0{9~Rr_gr*WP;%*9&q>)GDXL?cw{P$S zqsB^2mDK9rF}fZ1Ih^fohBysW(uE11OQyIjah-4CYU3>{!w#NRP>Xu2^0VNpMWaM= z9)<~6(O)XTEqBfiQ@icSC8foAgQEFWWJ>KZZ*B}9CuxFExqNPaK~#-Dk7(0jO zJ|BZ4UgE1%w@bBXiYvkHC@% z6s@E?dfwkg3hKw(CwuM?EFH9TR*;UY-vhZ78XIKDquS}Zu*AxudMy!cs7Ls|yqfg! ztNf(P3x7C0kd+PQ@pBAuK(?&JFH%%2_y^j|9R!TXo%j1H+xhO|xdmNK)5c~FiG-w> z+`7wg2G~=x@UtjL%2J!}sBRL@+hE6w8Y5_R_11qSx9`O*{UWb4Ar4kT#EyIz?$4Ud zAkvMl_T{5pk_TjcIi5>f>$u6+Z~~%i>H7QUp|@8a4D+@^_bm{H9i@Lb>T%f(EeExG zd!E=(7TkOOVKB>q)Z5jutpPQwHJV>&tkm|GG>C=YFIBE_uT9$llwYC7Q;?mUua^`d zU7~%TZAB74s{QnwYeFCmB&d2Nc8kNXCLt2oMUe;OZ*(#;AzHuv> z=HYVv!Z?z8OU13q&Zc9d{ua=UGOCdiIiz)1xfJCbQCM1%XXhC(IO@RF@-fSzF+Z7j z=Ef6m4*xm!_~|sw2V}R&oH88rpqI9Iddl`pa(#h=he&YG82#xtOLTxzHiXTaF~4{j z?nJHQZ7zoLc+>_b*Gk6_%|*)lN^Iv#XZNpSdNoo$ICMC48(#t=fW?->u%cgd@3oV2 z`M(@tXhbd}2g7i zz0_4~IxH2cAkD)&u@K`bobSnf*Px9V2=w4R)S31^t5`Jt0;g?_x z8r@YC1v*#lCWC7h^cq0SW4tCUw?%?hXd<0Vpt+|bddRY3C(%~XYxj6GMHoNgdtDh~ zqQRGOB-us!TLfzKR*xNQHyTi)Ej5)9GZxMS*w+vgjetWNSC$x4!tuWyjz5p~7bG(| zKBC+6+&9VUDem*?X2t3IiK8PJ1E)Pxr$+bsJ?-na5q6UQ6$Zy@gul?fXs-5pc!dcR zRLqwRt}rg}wwCQkb~By~<7;hV7;T8vOOcw0!CAz5>_B~QgA4uA(Y$!zPpuR-IoAs1 zg>#di56~>a;8t$=UFe5h zgG(lA&+^RzB)ujA2_U9;W&qIK2>JEt2f}(a~gK8fQK|HN( z-|NFZo>6RP8sc^0a>JdAuhKr8kv){m+39hKL!FY(IKPqrUE&U70(sJykO@??vINn0 z2{927&@BE;{9>9Lk#x!;NGrJ2R`iD{@FM-tT|Im`{LczICy;J#-HUKS#-+Mv-LVBV z=a9aB)UY`V{Iyof;Q4VYj^$UbW5^7NnpV&GwyX9dWHxK8Wl;Rle5hW=hxu#_==NCwwqb|Ju*f^-lz|4$nhJbH8l*$DLJN7W@ za#e$NEJBa8O>&pT9HD4pA`jQ6h`uaW>zqB`cf9h9RCeU;AlY2{yV9f`TR4;=od52| zAob(GY{mxAK+#^p=iVs&>^ucW&G- z;o<3k6Z;c$X_NWASWg157%9_VnQbX#+x^y^JZc%fr%h`yl+>uMwxzaubB=9&N|Q8x z)@Q8TUsv+dp)&#H{=ZQj+dv>_EUS~M6xB~8FtdhmK<@iM|Ii@0kSo+5)Q%9n(Oxir z630FBB|aj3Ma}6T^ho5bl0??tB-4#1YCr+i#saSbcXH!NeQ%~=ZFA(<#;At;xtH+F zEprur4%hIatMLc{TG>>s| zlSaO^H}~=iyoGY@0Zl#*h9Yirs*}OO~xxxZohFwT2wmzoqc)!G_m+s z#O-d98<+ycZNJWN4xend3or2*=c*GO)~N^(WC9EJPP))L6Wf#Tha>xPIU6Xy2r;if zKF^gY51Xnf=j)t3_gzurWlnbFH4_C6v{;$@36t|%BFP+5M^`Utk&5f^g5fzRaIFA3 ze*YU+Y-!s=cWgBThvIYBt$yn~#if@7HA+6_J9oWk#53ho64!0@H67N(-tL{7G@WX4 z1p1L2B_``9I>Y}uE*L~LHLtjqH&tiLS~D@_0Jo{+P+}!o|-pJd9djX zF4)TV`bjSxgRG-$%SwA}DrT2b-U?ij)P1e3bP@8K&%WlzzE zDSa~_f^~mK@SnpyV%+Y_-=de*p)-lPyd(s9v3Vn_avPEGe)-jRk_Bl;?#krA!G)NS zV|@Wfrxdb8HX5n^(9(RKx-)ErLNF*O_k@{FBB@aWj!`JRi${f5IQ7aw-9 zD)QV7E!l_g@MUkL9D#CRA1mXV`z)ktJ!W+L9XZBsHKKYTK`DLwG5z5jjB?%X-Zc>?bay49&)GX z%ypiVVXtw&xwM8H(e_A|5|iy-jrMR-9*mE8tXHht8u`ccyYl&DMQ}luoyX_$whyHK z6+_d1VyH_171$guOE~o{A+)J$80Qdg_Wp=_(MaIaPf0^}Gl`9oQ-!cv`fHlNZOUGJ z(?@F7IF!#3!`FDrIlJTO>uc`!^|WZ;7EDj?*i?$j9*QJ|&abZLe?2==t+HZobuZ!{ zD-qIB8cj5|v2nop=AZqaqGU>@ACr7GU_?`jpO|5(1+?lD$vg z&6zlY6trtASwOjIRmbX}yMJpOgFpVEaq8bUiUDC>zooh9lbDL~fW>%K(B}o$Jco+x zR)*zA2EuEZG#i?nwqqU2Wl!NLEEo=bHxOZN8b296D0~cUS6Fkz6?(+rOwlQYBC!(H zScnuxfl{sbRbREtcG0WonH}<&!ykHUXqkTL;N0A_#V9roJrRX6DX%Yq&6IP9O3UQd z0+oy=?Cm~iIqTAD$d*67$ko8NzCy2VN|3ZS2MeXLtyw4Lz3#K9%8ST`d5rkB+_}Bm zrNO9ffUZeOG^Lo)rVhG6T2;$wPy2UA(LxcV0l$eA7R2CA9+-MqvuRL_{LbK|N)9Ma zuJGUZY4MgrD{m@Hovn3pPjuhi#l@=#pYAOII0qQ|T25dB`JHg^fA%6K@PvTFZf|12 zROUo!qOy?nyN5_^t=8^+F=?T4!__;Uy#~)gw){o@H6)*vU0281HB5;icJYao{ zQ7>ZUF(`iGX)$^A$fYa?0+C_b!URgMmrWz57;cMrn!doq3SIED0)vVI*(;wRy^kEi zh?h=f0&q((HeoZZEQoOF1wBdLRo-9iLbRteLpX0H-$#R_R)KmbPALS@WGx`TG3O@>uAs~P`r}OswssDsQ+5r4V4uL z4jUDm2xDF_nA4yDMpYD7h2xjn<7X+Xa{DE;^Ncu|3N8S~$jlOhXpa6}M!mOdxX` z%$w~7>$lUnYj%XTxQrn~8g*D4Et>A%f{;T%)}W5qU4TLUOen03d;ZRmj1Vf~3F6DI;$OBtDy)5;!K zlAMXHB^qVn?9FJth}_}u9j`$3WpvZU%*V~2V-u6rb$Kg}mUTj-P~=Mv;Txu8-Zpm1 zWw>EOzXnX|=RJ`axWv@RylT}Lzq0kwyjZWgk-!>tTl13+7jumqbmc5Wo=Pft(*iZA{Rk7p1URjeWT_mh>BrML>FL4*uWhEAFHV~NKR@#zM>*j~uej-7`S zlH_&Mhl}5P(!{=1^l(N0I6x6;;~mc4@S}#jztKjiYq`f81vj6vF&3bBZeoVuJ5q`B41j;B;d#B*eX=giBJx(27pCh+jr`G0ht?> zShj)*DI4|Je0*EJqx#Z_-g^N|#)@*`_mEW1*Zk&M6x(}fnqcl44pve~@5gbCIZj?cwd;ZQw?1cy``OcoQc=q1TLtvWRw1B3|^eY9o27}%J| z5g&2PcM5?DAqul^N;L*r)s;$)S4~6-XqJdfBAOOS{+n0(gN~L}DMPM)#C(V;yEA@0 zQ+XhC<%MX}^QZY|9~DUc!jjGDcFnqNj}0_5(MkF@rdFaiB9Fx4KHGLb0v-2CUzHB6 zH(8nSKku&%C5+7rlsi42+269~JI6raf3uiEh{%xsF7tt29C^f(t9X7hz*G31?pG%P z=&+KO(TTm6&e$9H*O?J7`du|R1u~#{gD|5T%D+NWm^7nZ%4*FjuWahZp)>A2ILu*h zCVAU##_5GLuh2xT4NXX3!ZC+JXw5wwwK6F(thp0mpwpZy`soDnu#Qmh@uKG#weIJD z!t%6hW%S34qJ`vpJwA6m?l6IkA=#g>-NE6x%_Y#kWWP)`oq&qcENlB!^zCIR9FH3a zZZn~-+tz9kDdW%=N^Z_RCR=tKqNlm$cD;>MuY1 zwg)n)V8IpC08G;~nS$3-S6TkwaPN5J7;5zhm6y>rCc*?vv$jMST%cVggN;M{T0wxB zsYVAk5?Cb0ewAuUQNTI!qx(R@xb*4Uz4i>)8W(MN$m#T~f}|{ZVDM*t>Yz;*mV5;} zlJFT?Dg1U~NMBp~k(|SEiOQNC*MtDKC=#DT!>uGj;QGA7^L~$%d4BL4F_{iVv%z&v&eR6#c~40PlGtgWw+xM z5n^k>-)AL0R-fo4dpzZE%v8vyry<2c>?ZpYr|~^X=On`>mysPa>65b8tlmoBI31*a zzBabiBtqRBc7gFVE3LGI=hK8<(ygwI?Qmu5K1=dUYs~%;&x);+g6Za?8jlvP<@ldc zd$)2a^b2akm#h$Lz~^COyZN|eoe3QDauhX?L8n_>6S(uf^t&Aw^e85txJOLCC)PQp zX*JxX@Y}a!*fGP4+(GrEeCxgtLJ4LmlvAvLr?C7f>JNN>EffdaM^gY7atkldaAWBx zHLHy{?C7{4U~e*TB^z1Z(JpDoY9i8c&01yT!w_qHFZDqw)n?MN5zPE)4?9*7JB^xZ zrIIs4h+~Hdre1d4yzLv04y$2a%UiozXI|e2>40kAdA!|Ps|+P#JNkCCzOI=$c8>IJ zE>xq<7w{IAPX~+xRzIR%71<0pp6VsitCmSVaP7(VpWwVDECps(<`|u;uFy3dIglV= zB&P(Qxn?S1AN_rEsw2=hjJk8OPP#79KzF_gkf-b%A{rU25Puuh}xH%8L zRgOhEOSSAtO^r`-0fU~gw8Vv*;*pEB;RbY)*HY-1=w!q52KrOD6fq~fB`b#U-VV2Y zx*D`X<_J(T#3Hxdw@PxZyk%W* zS9$ffc^HU1Ol~H8JWdTaCVv}T=v!#e$MyytTMnf9$n|?qXeDrG$5{`HDfoY@X;)hp zf;DATBDf3=(JocgR`u9`xt^GnssyoD*%u#VTi2v^nwy`WxqTAzZba+?db#)HC{M+& zgyI&29Repp&u&*S`_^5W?Enj3N?$3;TrCk)-G8(+y8T}&bv?6SCnmPuY33{5=_jri z=H4Ncz4J2Hk_fYg+ovef?WT&NRFpJE5L`59Z$1h~X>%6zF8V6e`QA$Ro1l(Iug%z} zWct>UqE7#e2g0s!JLfuFyDA}#>8vY7dQ-%-?tE2kk6F z0$oFyw#M_fy)u3$L1oWVx2p+QH3`#Usa@Bc#8#?4oF*%@`HFh*-x!*oZL|55JMG>C%*w`gb3 zt>mxQ#k6gJn>bQUs9?#|tuZ9y(S`Met-ke&%)gp&p5i8%zfHJo`#zr_2j~(*@`^7z ze(1#)*DAhhdP&nKYgxz%HWxR|^`chfmTUvq?>{z&e8XSw*H9g_A&#|X7TPY_+`{9& zDElZGjEI?}4>`~<(pF|S2~AhEJ$sQ9k++?R3^qE%e4W(49nv6Ss&mbX zeLShSZ@u&EkHL4NBb^eaN-j~H2NH3IqB}!wWalb1{i!Wo94qFhbZyA)lSbJ8VZ^D@N9I&#ba;!9-yufU!#ch@D><< zP?C}HKwq=1<)dYNQ`L>xr}xS=WJq(D^5oU@pH@0)ooqicqxZPXKkvQ);yZNWMJ_KH ziZd_cqQY8Wf*1v_H(r|t@(uO97`q_pS8p8c^9pg6cUy7|WiHM6DM_YZeq9F6&MaMTL-Ydb_OS#jjJ|ov zi^3R!m7(9M&S4@CZE#a9jsO^TR5g?qszB^p+|>OeL-)+x$<~l0w7E3dUd`a9TQCn- z`H>j#nE`4L5u0d>NGwsoxTd%>6ly8ysC+G~p8J4pRewNRP+)p9Nm+H+^_J)dZdoC& zPZjIjf-I8|F2qd)Cr!e9gqy>jLPE!=D3_9yF-nX>Sr!83@zdJR)5+RnR`u1g>Plf- zLT7*g!anRo8g!q5z#u(tiE)%-lIlzJpa~VtH^}3cgX~U2&exnX&o;k4<2<9(F6s28 z-P`!u@%u-&&&C5=6`%zXbpx)~8wsJJ4O8u@X>U3wS9r=E$`7d0Y=Q!cJ+e-gJ2`pW zZLZ|wzqoW8m9#V{x(*qN-niTc77khRel7UPP^7fh7nD@h=cJ|Ay_NsbkapnsHQ8$8 zBsTNP`)8&&>(v|!|NFJDpa~~Q5vrdfIZ$+)q>glTEwpxN$U_Ui@c%GsM&!gm#iavIQ|a)%bmxS>fkLlN<-If@=Ud9TK7$rB`DGMH;6kp zpp276s+0G56VqNk{;DeJe^N~LteM`m`_tQ5({J)qPRDPzz-A-R;lX+ds9~Eno7{cA zJm!<;b3M6VJU-ezLvB{%ByNR_)c?4Nolvt)qCy~ zd$gplE73{LNxXE^ss|ANWJILujlPK1t&DGx*MVZrN)R4g z277-&X?v(I&lBTZa;VpPCOx!dSxLDx%IsJNGSk_0wC!z|S9Bh#B4JF%RN#iU_GY<5 zLY>+|+mV#`FN0U3{V=M|$88Y%9&=tru)fzKbBPuHC3eH43`|J0>b zHp!#G-u317L4q%?jMntOxMJlEL#UTi!zeo~g#CdxDoGsH!dh4C6Ivga9IDxK_tm+0 z7n9?SXTO6;7#Ri)^j(;M#=jVSSu2`% z+nvqlXc`x$XeT0^u&hH==oO?HWsm#z>CVbkdoy1N&WHT^cPLSf4%ePdNXE8=Z~j>U zgNAaPv_^k}bR=$Ea3C;=XR$de2N@eU<^9@jc}-<3?{RR_ySAjEQylj_-x4zI0whfp z3*}G0U-4#`hWkyS;=wH4v@b)qipY)Hl3q=_->D+Zbc5Ke4G|deu zbFCtueqbxt5pl=%5C0K}^G1YrY>G)=$_{kqdz#bYZ`jK6t~R)k5{cmSw9%FZ$Qbcv z^|RI1=98Cu#!N;q10MRwC$B|VIfAFYw20>xQlfbGdxQu#0Ob1C^1EvZi`tZ9=Se~r zwv1g?dFT`6vaCKF_5-73UaIwo%r=s2{TzW9nNCG;Z!cMRPjx4@HKLQWkmSM8`wr)n zoy~W;%c~j&*0|6anDY{@-}iDVC=`n0k&)`EaT>9Gm5A7q;*;j|*JzQ=1fL}ArlZ;Q z&4X7~d|0{r;#05IQr~jKuaAUVvQhn~Omp>|!$TQ|)Y?6lM7pmbzv?x+e7N!y%lg_Q za0C^9CA8hDs+MdMce6*vF4Ya5dq`FD>!tBC?wF?)9Wq8zZLU*z$*h&zpk?34Z6NKj zF$Bt5-l=lewKkX#2XZN$U`Cb*wXebsPp54BgdIJObsx+-{V@O`$Z z`gAS5{dRCtovDJp;^mY&~kCEd$1R^cZm+ND#_ryoMOjk)nwc{Y_y57abA zebCp&yUvg9-GOGkKK)Vv9oc(VJ&|%_`v}OUi?~`0ejH@e*M+{4rWdP`&9Bd|TyK!$ zVVqgrDC*z=6_b?vCy>lV(aI`$WZsx*yj)$1UV^~ncjn&XPtdC11=5 z!l;{5C&=kp1E7!WinsNM*)~UnDqE{&d{sMv^mATYJ^)BM>-PnXtRw_yh@YKju#!8c zLxi7veN&k$t0r-55Oq>R+H6h8gwsdc?PYQGD*O+`;|$@GU?JW-@ml(|glppS+Wd#C z)ykv4O;oV6`REz`lgxd@S(G=bgaJw0ICrltnW0izUs#GNfS>kOds(fUY8eq+QBVYN z=VOuX&CiK>q>wmS^pS#ZF`8lwHJ8pW2}><@)_6I1R{0-2HjW+E|Qm zf^QZ4W1{T1eKq~_eSnp1&1!Kwu4P0oLaK6wCG^&YdG@(fs~hn1w}h{xgF<1I_xY={ zIioJ+qEnhLKf=wSg4vb^yr7t-9gYk$EJpaCDxX(RLx*Ukg*7ACBAtAPr3NtBClgBQ zG)Qp6+tHv$HXJT&uOJOh$S8*V=uh z;9%4&Ce=UHBN8lAczDiS`{{AVF@*cG;qi;ptF#X-)jqhJ2c{H+QeS#PMaq;kWBcK( z#013zFPQqpWL5sm^o@uGeYC-uRlJkySoiV0I-RUmR1BOSZwd+w#@No?N7oTe7vQPh zGjm;QaAZApW(ZxRL;44m_xp{nt&kgX3?Mjv2QH9}M~nt|3iD~27G^gS=e1QGC!_~q<0`9=9;$i&H)4zEyvAK^Mt_i-h9((XrR?@YyAa&A~b}6|L z4k2!kvoAi{MsLiED2iRW(Sl9;%Kc<7<4l*a_}_#DCU_QQT(MZTN~hPR1+7TKUC#9| zfsde4~bDKvwsM95&zv%k_LwUVqQ|JGbRvJBmIqiCorEF zy=vZiW|pe$V|8K@<%yf`b2u(hKZR-{NxUpn*Y2f(6tkkRNV${r>bu|g73iN=e>^K! z?mDrT$KwMy4cUz$B9}qAy;v$gY0UkIa_q!cAFG55w^vWyY_xG5H&oESm25Kv%8AB{ zAQNvyKuP0RrA0iL&@luf318}_zT(Wgw-R_q@qnCySu*`}CU!=sk^TUGl~ z&xRnJY1$x1B#;Kx=nZR22$$9E<<-rt1~<{xvHG=3i?tc8TozpCvn|OSF)Nx}k6gS> z#x41ur+hP%;e5j|Tw^^V`r?2<^1XqWeu98A^{JP$NHiR(5G$7=gu0?jv3vA1?T`0| zRvT_5UT3NC+0QGM@7N9o$)iM)NBc;>X$;mq1sz{xfNLr%@=0Jlgow$o@XEPlPZ(G7 z@T;$R_}-;5TiteLl!y+9?L!ZYI z!v`GWA}F3Ub=yQWu1sYwIYhiGnW^&u>Yd>Q_8zvX#Ngq-0ef5TZ62+wK%U{R~~Xk+=S{fntY*K85cxNoLfh_DTHuMf-en0kgQy$0|e$ql%)sEcgb@#XeVAEa_LZQ2$#bp&zU(7)iE+)+Gv zC?$-eV-%}4;jJGqP)ZnuUrK1R)=Fn^k(b6H*-HekehHYOX2Z-CgF!edyInqthKu|j z+P$|vV;tLI-^1{rcbC!Y`p650rPQ^llRvX``T`NLWp65Zf{Z_5G1o7{PAnZvOH6mj zHdA65%l+}{x4{Pm4Zf)abFwN;X7)OK%XW2f=bUL3y!Y+Q*6nDYk@QBnX4CMP6E&iGXzAy9-5+=Gu;&4$7{**5V{DPSuIvuV+s~V}*o|}~ zc}B+0Ecv;2*@{^1lan03aEn{iKkU2@t_{58Bme-i*RMg9}~)Z3p@Hg*N}h_mb>Zw zc`>W%odf)8bc?NbeNhx#$FIOrs=U4qaq#(cRWR4wySC|^$&=TQeIY#P)3^Z&4POxC zt)fi>?8b^E?yYpi6d|#l*eO#avvA?r5N9! zOMRn{aH-z5yg$-3Q^LT@WuVV)8D85lY2jOv)1qg}9#Zk^wC*d7JZ3SaY%(y{=qMF! z6ysx&vl@CIgO@i7W64`J={^y|y(X27v=6vq2FkaxEVcvwH|7A#Sa@MAOZGjO5K358{Z&tKQ(Q}*_4xFB3Hz^+ zKh_S=U;J?(lrwbvYKorgt8!3sI1{A|OF8Y?lZ>TT%}^Y$Z@W7kXa92%~?Z!qn$Q%)P!B1yiWXjOz$ZsBkA zI+v4GV@u!rD3b`6axM8p$Gfb-s*OhtW@~$b?<9>!B^dnH*eam)qrsTNGsfnrLEJW= zGRbJFwY%}#CbF}2MeUd!0hRF^ZC-_MhflXD=8W8LY*n|SihRzRMNu+Z7>Yk;%SR@? zq>js1SEr46&!S@n?)b3Is4xLl|GgJ^g@U_$ANSG>F%{X34blm?r&XV;`!gJs^@-OK z$39$(KH9wo+HJ0tinRIDRMnY%&w62YbQ2#ZNkE3gA=ZP~hDJ^sLiOM6Qawi{t2HK4n>Y@0h2)T(<*q14V`6nQ(mds`tG zOV#oHO`$)7-HBYn@~-jT#u2)}`cdE2Q`p@dK*)2b$FOyAvM+<(9zn?>L5b^|sP&6r z-KRf+=8D`!;B3fHBxF{)Jr0`#?f@iA`yqBc4<&**V(d@N2?A|@4M-==VFYe|1^P2* z7$o)#mf8vVGr(HVk2Gk<=?q>3Sd;Y?`(+Z`rSs<=G!VGlNO{C}uSP6Y2+SC@dV%{F zH+Z-EL+>v1-PE$^l2BscRb;v5#-}j z;1uIM{yN9IK$je3w1YPEi4FVHG|h@Yjw&Uik}ymGH=q6otN)u%|3mu293#6>tU;DTeH#WCa%n|?bI_X`!wc$P<@42z9_zOO!q*gl zM6%)>;?FXxEU#e?qldI}C9*?&Gah2*AT*%2bEMi1sUNv4xy)U4Z)HCDM|)yuk>zH) z`IdBpx#YW3$c$T(t- zu24;F|B5`H-AHT8wHwdZ**+$RKeaFW3>i@+T9T~Zcn+X=A`gYaS~F|~{mkDO6%gcc zLj{G+cZe6SswI6^D8M7^d!k}zg@RGYI==fGUOd1ZB6N5I1~ z9VQM)78A22#q0s>9b}BOG}X9(y1h235KSaIHcTq3-_R{E>9Y!`n~uTVGL?WF|0L_| z!d(PNrx}N3JjUL^#xw|g#|XV03mq}e#Z789jfA>-iHUCoRUhJaT4h}}W)n_bJ|r8U ze1ptE@+M;^%bH7!8pp*cpwm}H5c0TnC7^I{!OtQ{^l_8Yy;wkTC%^c}*k{qUYs)qfgM?Od3E#u2Jcg0vWdpvE^ z8j_0a9UU#XOG@5*YxG?p%Ek!tcXWgTfSJNeK^9G+E}8-rO++RWcr~!XRCUK*j1@7* zQ!5>Jwa-U*a)2U~$t4$m{-)Soka+6l>W%e42`z7p5r3cD{orw)kjy=>ShDQ#-i5{!ma zAQHXd0NngYhWLE3OKrBo$e|tEp$c!2BLu_?89C&O*P{mmlHL8y{hcrPV~_icY)NVg zjfO^V1W*vM`XXBO;jeL0X@wY(46BlL-XJ%&!PUa7G5dCLgBje(2Ww?-N))??j9NBy z^M^kEM7URdjAsIuxZm5EWSupuXtyg0xf*LaUT2kjG4$u%`hxJR2S;PHj%Fp^Yj$Oq_ba{zIihiekBTBTe41QT)|?@) z#JR*P?%)m|Z(cq5?hRXPK)OANLn7Aisr~1h7f-6spG^rDIC??L-)x+*dhIu#{kp!%hvJ0fpGbZ2H(0zM@istl@J*A2 zj(i^B%goPG0&#O`adIVITuwI@Sv1n=8+3Og{g&5GxQFJxr$+kL^RmT%Oq`rPq@cuc znceIaqtj7?0*QvAO9ErOw4%bF-wLmht*eba@VFp;?1lN^BLkyf9&(rb^awv+GE{K) zhiz@b4(eAw@o4Gr+6ICmoI_q7X%T@8R&P7Zyli+NKj6*3>~8*yr+U^>=i@ir!)}f$ zr=L*XAfy1*?dpKio#ELwz<^B2ccA!bLf$n5|8Qts_#PPSV$zJgTP zC9>sX)yfFlUN0$2iyTsE$&6^I%azzG>juwE{O%pm>3Pc@)>1edFSPx)`VY|9=Awwu zBRx^u7t^+|umT9#f_Li#mJflSeGBe*nRkrzpg}U%?d2I@KFSCCeLMVLQLb~AeHY#S zq9hmYStCIuIn?75)Opgu?Lq?1)~x1Z^3UPQDXj^XLu{p@(ckApS6~-*Z$GxV@6Sjs>2Yx^dU=VH9M)Su2lP?vFyqr0EYyYWIX?&pP(vSOl*P zy-r_qQdK+>hZR{za>_U79{Zs$P(19xHFX#MYn9*JRY&QPx?ME*?rMmN@eD3Qih^t6^<(ySClGqT)lg-(oLSfZemxdJ{@v8~X9@3A)t5{#aE`@o2TF=ewgOOw>O z*slCyT$Frv+}OjT_WESH`E6l|Wx2G&!4Cx#~3bRXBi8=2*nO!F09z5$HZs)PyUVp*Q zSL0(OBm5C8K>u7!dm_sWLmI(Pbvr{1naswDg$57}N~WL*j+zu?3`1@~S8tf=Y}Pl6 zP8NAIziHWbSy|)5zM{wR#oykW@u8Pa^pRT7snbT&$>+lN_D}~^iWPRSX z*Fc)?|1b95Gpxz2TNlQPii#o~At+5iiu4{4rHF{2f)F4oQe!}**DR&?rl3@jCIUuk z=#dU0A~ldu6X^sJN(dxmod@5&_u0$!erNCf{W;(DUGES6AU?BedQ6m;ZGTAnt`jWyYbG8>&1i6XLxKtnl3)Hm`t{Lqr7QU;;b28#} zJEIMBMzQY{U7HH_ow29{@~n$~%+dBF*Op?#kSdJU+40`PC>B_;)n{XM-`LA6;}IuK zZ!7Hpl`70D4zKE85v@M7WO(e}wXKfhvAGeam}K14d{t^obKww~HnZADhEZIp$uu2n zN9ajt+^0zj!>%G^<@Fa`PlQWUCw6@Oz@F`mQDiZW zMjR{&5F_ceO|c=Ij1T%GL&`vB=8QQloy}9!p5xx;S46=Ui7n~#`uHr7H#=7vzUZCx zWZsn3eQ$&_cjFd2pLktX&n4aVM(p$3><^!8y<_cK8;d8{%_Y)AgT4oW$W8eK@E>Sq zPk#T7&#<~7-Udr>rZ)$DeX+VU5GKi|&!1nx>w45gRb|~%|wpYim7l6D3%zrru298Z(;Ajw-!zB9Js;$ zKl)*Q5#YKPq39ZeU_&D*wqP6e|kUXOoD3Vhu!}oF1@1UXyn9fs| z78s&5p&q-^PQE2(pmucLhz8Wmyph(`x^KB-^D=&}Fx->R#cz<3D_D*$VG##ygV`RPp(h%nt#eA9OyEzV`Si z0a;pL`+x+jF6nl4y_wJ(Qt*+Az8~fbV*jOsxBR93X2V@ZrAEMRUeUC~ZNB0lNBS5E z)fyekY*o_$6i*~RN%$NK*>l`V*amA;F#}VlJQS#9G-&|jP5EB`WyJ~g_M+k*&fE{p zkIS*$u%A00H{^7V`f-ZzQ+MMOzhew~z-J`HFg z!RL3zR`EO2zA!{dfpxuPHpBEHPguH&%~JEz6}<0|m1ihc`7+nsLp|ohte_tn+A|0y z>INRwCd1n2aH=WY-M!F9>4%C%0IoBUZAJ(zs}HAlFms>mkE@!_vyXAVdtNQ7-2Sz@ zxKWYf$NDLU39q%e0WBoef#3=t)lr_2u#0|L>7O8FptTQzij z^&{H)D_xuKaGlB+A#Jy*Ys$}f72%QWjtmae@F$I944%(JQX1D$TxkU~yXqIGVq7L6 zB|dWUp-g%|u5he(^C%n$)*Bu{W0pWzQv=DTCDqjEE)g@A~Ga)>71Ax*_r z*cFU9P4W*{vG`ld-w!`7T<)8nAg?XhbuSSrr!0K15Kn*DUwJmh^nu-80J?|4U28w! z3fDxm&$?C(YgW}#bQ8(SGi%{!Cn;TBvV9KhlTWZsf6R6QL?_VJ!HFA{WUVP}rr-bA zV)(I~#Ow9ap_4Din$z#b2FT!VP+@qP6QAkMm%iM480gGSAi zD#p^guDBcuA|eLPw(ev(1sr`WpCvg@zjO4I$`1pe$Z;h5r`=sy_i&-u%Iers!z}gkB3?<#uZoyl~AnF>a&ul2@3|*`$-IPS^$Z_Hy$qt$#C0KkP z+2&FUDc7}=l;qEUAP<(P-HAYEFCM(aIJc*b7wkU`;il4w=Enmdl+W5w`OY)tnvS=M@Zp z=abu(qQwJ1ZXPdOQc}~co4jy#i7VmU^9!Msn+f$_*Kc0E{O(9V_0_}l^h!UIIQPkN zACEO}^ww@>Vy_4Xdur?AtV@4@Fp8Jj;Q50hBijE;^Nhc{^r zakcH<&UN%GHM>MqRCg6!WDMocN2KfwK2H%b{A|~SENdVsFZT%=Xi><9 zto(PqXDi=AQjkMM3v_LYvb@Jyh<@C+qu?5brbvwf1%V;R+sG?_ufK2d*i-mDH1%T< z+ffSm&Lq-a(~A!U0G!H9spJX<9(4J-a9~FAJm;OacXXf2XLuvu|23!;4a9@o^655v zQFNi1MrYN$fi2gQi{$U0zdlYr$eqFQsO!S3^R)Yeuk+GZZ6ks+#kE}qt?RrO0(mno z&JUzCfUNh@f#Cfa$l_T;JUU`z`8`%DLl@V!EnB%%@{RX#;dYW;g!Fg<6VXy=I`aed z-9G&HaZsGrxPvmtmNAu0Qx@?e(v-a5o$78e8^$U8KpMn%86mNVSz8<=Yi31}0uNDK z2I;B}#m!t%3?VD&3TLa`Z%obTFstgNhlmH&vNA?fA_G_s%iftuWTa9upf&0cY* zBje_!JExkwI0T=>JWMk7Q++f=0#%mMiwmLXLgO|{lw^wsXsn~PwbOjP;XauYK?D-# zvozz#Pxk8^qgNcCBfJH1e=K($elhZvbp=z{Jlzv}(x(Ei0~xmwS+-J+k~YGY3klvx zS5J}SP6~c`RrAAaIHHwttTEs9Cs3_+XwngR6oasD1Vev##A+FH2Iz-L@rqxFf*{zf z+7)KTYvol`6JMwtsb%>?rFa{9sMLjV5lemBU_i)ks7yM}*{`9kHmVwV|DIx#m+ZrL zpM~axlZX0yP6Mbyd-<-j)@pluYC&RECN^B}gT7PLICG1w_3+sd?pkGV&5|NFJS5Wg zg&nPzEjeNW5t{m%Xkr=E3m{hnuX)@jJG6zNnrfpKS3&JuQl5Oe+(!)DYwxV8zk1dt z?Pv094F8)`UHipK%nkiEr-~W9enC|;rDBH3?b&P>!u8G_D+q4qx-xD&*R(O$b$U_O z$9l@+nFJ9?A^L0TbWnIVe9}7;CxB#+{TLh+;llPEdf}eT+hgibuOCowK#ExD`Oj-q zW`3RPY6xyD=Ug zcM&UNKu-bNP^BlZmPn~BgGVU_v*a5+6W1SKzNaUyo_L#3bDHmv=B0ngWPdSrTeLcX zjT=5n`837F-NI0XUVgd-`J#$KVVeGY^!{SL*-1PwyriDTQVC~Xed#t*vV>;3yoETo zm{yr3MyB6&GQr(AFH4QXtNVr=xc1hT`Dti%?9V-V##ZkB5n-adMkjhKZ*MMAvu1Ai zq-2vg`;5pbIiF-A8v;c-@=%f@4NVto{Pi11%?&k?*idh_H?)w0YWFq8D6qjz1j|z0 zt)R=)#LH^UAw3F{mui#ntm4tE64$)-lAhs$NM975x1whNgQ5v&n6=;Dgc{?QaQuPw zY)_u~+z$a{!!~7HlQJt7-_9lJDXGuSk#xsfEjHIVi%Tl5N~wyb@nAWv`n9iERL}(- z(RjEkoJ|{V81X*y5v^1rIFcV7o1U`vkHe^gHDlb^-rHk!f8Su%H*Hs%+Q!$%CKDOp zh4Dr{TXSXzW|r8>OG+LNR%e%9zjt6gAQ zeN3^f!7Rydx`j@fugNe+?H@;0C&-Su4chxQ`o-Ek^I)r>-n78ZV7P}ozAuu+73M91 zAD4+-H6xA<-zzrE?B&KD)A=HtIFkJ-=)0F19jnuWM@A$7fE<=L9L}J~ir+4J#w77R zWL*I9qW7TNY!J)2_JgFwHBL^Z) zVh$eQmsHo@O9R{x)`i{1SNjst@Efkf8H)5pN!zC!F*lz8)-nFN+3shNP|h#OAE4cw z`ns=Qo^)2<_YFLN*7s@|AL-0Nv{;Y@A$oq8mT8}?d1C0-!=v`!2?9s2d7MffNwwKa zFVWC}=iQJ2Z0OZGsrO@T@CIXdmfJaFs9X0K>$l2|;F6g5f43`_kldidM7{Py_&%zE zh8u;TT0;X-fccKq;!v(*6%{V3xEP1D??1ryL~v`hU&9q2BoLinHY*q~t0ilG>pPfg zK5^Xk&6LE^+4;k9VQ~+<>CAoqTNLc(!ARhE6~0Wr21M#B@ZhnmO=x;B-aosV`FO>{ zboGXY^DB$>S(U&wTVbD#=ahG;!c5yylI^op5twq@0{vcq0pW!&d$&pjFP(5(g#FUdDfJTxa=Nkx zVvQz0&<=bc)=cPEvU!^Yx*9HAkxJAOtxfMTf7qVWy*D20atJBN#%#H-PeApAmLG;{ z{%ZhdvN5|Qkk}((vwRu=LlzRuIP+S^iQ-0i1f$GdG?w=Y=-uJ-BAlPEG6@FG-!#dt zBTU1Be4%vJm2K-2Uptp5k>_l}#oo*wo|6XJ&4L2M$Z6!uI`E6CdLvVwtyrCV6@z^G zkcpcN!=6Z~n7P1DYdvR$&Y$bz=9fsle78dWm2+=rnz%(F@G`w_&-e{_^Pj=gM2`L? zkP7sf128rJRaAaAnzHgIQ=;-01Vw8By{j>dz_L;xql@N1L&837Hz_kw}SA-p4FD0hAv z(aid<(f&gw|JuoaY03Ya^H#hW%-Cnz5aq(l?9zw9o0*dNe>=9D=wtK~r0tb85|<5W zE$mTUxDiciwx8?V+dMf9F}KTziSz#EqU!v&OSK+MZfll=Uuc^qYhjDcDlEaqLZANr zH2P<)$?L4;zY`BlXYmf%4oWif z!e5iMRHa<7A8IGT0r@p@~*1L@`% zLJVO5Ip}HkTqq>J2X%W5Q~MJ;zfyqholKUX>rzu6E7U)LXQ{(YMGOf%!acHA_y@F} zwLLt;e6SS9=+)9|i;h2&&F{m|cM(a`&eO^TQJ&g7E)RV`Z*0Enb74 zO~tpjQCVw+-`4qAD28yZ>%%KJym=dKZupVD>D;_oWZ`FtneCSAf|5!~N!nkNWYzj* zR#FD2a{M3$r+M2}65mL@TGUcmVENUqbKMT>ZnGy{4!L-`PYwZ|IVQ`NR00iAo1SmnCM!#aAXoVerSnMk20waYG~e>RvKPXK)MVMSV?H z+pB`OR$N9|mKck?m@J)Zxi0iH8+fbgjtKEDf@hNX7-x_x2&x729KEl4uY#sWGn1(% z^o;#<1?G<6RGRlzCE7IJ5E!|(n<;5I1%!CzyQ+v#hOm!q zNzlxd$tP_s-Ub`+E++V$E0dK_%F_w5=05)X@y_^^BboM$cHS7oSj@rDQQP%xxJox) zU2?CV9lO5)>ry4s-)m_XKD4gMOfiO^*0hG;_1bW2M2KFMj_1d=xi1xxzMt`Dt8d`` zxUI`gaw-ezhI74~1M1dh>;jCPL>nPxAF4~aa6%_S170J<4$5b?!M{(R_BThG=v5~) z6&j*|B0WH^-hf~OQ6;3; z(w-VimY_PPR6vBi)$(qoh+aFEa^@o6lUt7rx_C5rqc9Xhe^=&7Coy0eJHmB!Q9qqo zKeo9_wMZUf11G=WbLlyFT*N#vYHR5drJQSM(Tfg+zWu4zxsrlKg)3xg28je1R|HDg znb)I+A48(|)@pd(wCJ$YivN&pa34Y1Fur9!CC|oHH)&8qPny|}SEmZ(bC=bwzt-M3 z_jNp=w>Chw+rC5ICg+C0fX;5I^h8}#x~E2={$2{-O^m1E2sNC15$L#<7KFfJO_g4D z^<-!WUO&;`yK`KOo9AfpXi#^o-Heoup(~&RtHS6so)m_)nGk?7JMk*AAw!~R?adkY zDqoIeV0WH??&agq=9oF((?v9q>iikFjM2X85T6WM+QUXYAEYd8e|vcKZH%z&tw+y} z9#HLt4594^AE*^Z!(4dgHi+ZqX0>iv_}y5zG(F+nTl23XG3n2#LLzZjYzhI@)ZVhR zuJ;U7TJp6|r^)cEw_{U2PMy)ZXr=c-SO}c*!e819HQwxW38_w>*m4Gh%lgsu9|yOohw_95ikI3aF*plNXtj3qhG)9hCX;JkTd!2>78uRHldz*)n>VimYFPWxV<`Uetd$ zkIAO`w>nIth(d)vM)bRcH4dfPch=62ja<#*{QV!D>=)$}3BA2B7f2ju(pBvZ9DO*Ws}ZEk`=<2KLB{=U(Ca<_a{yMA$ToUIQ-{F^!O!op8*?D0 zVL-a@R4Lh=5hWIdCMg#8Ead+B56qY{@Wwh6cnrx8foH0kq!Ins$>xlsRUi^}iVM8( z-lrI#w6$z#iqeb)&qALC{_FGq+MNGCYffOKMro*KOsHDWtKR`H-Aa(*=;?G$ zRA0e^WkZK)n6FXjPq;|qoK*7mbBTm@<7dYdH}!r)S^%Gti2yNJb;Q7Jl45T+XP%%7()@O0jM-^;IO(kTn6p@2isX|k& zQ!fN~NjB+tV{Yx4*mHU#B5HIcuR5!dQx1{|-wVH+%c{lJbRH(blxs`8G*c;W_Np38 zNb!`o9}FI<@)tGM7{yq~)xf~n;RZXd@Z9TM7bhM^Xf_l$v7Dc_J%mwAr#%b8f!KkP zpJU55l2ug2Txv!0u2gA7dKn?Ktf`^yZIVc3*w9#)jc(TUNG-RoYwn?7s+wu3vVi8g{u16=0=EQrV%@cFux7{UiT9jwHXa3k4@A8X+-CInSYCWYI)`(@5o&|h1a@4qeR6VX?>yfIB&rngVK?hxrTI%3Ikps)T0=0)UQP~cM*&sEjs??RQUwQ=Y z9#DE%VGhD0sUkoRy98C8z*&?_Rc{8aV!fDE4KeRLD%aJKhIB}M>^T$V3ajyP%a$^T zm9@D3p;o?E4=#FL*8OpS0!I=XNnsn)b2;Rr;D|{!AXY1HP|K-YSL}Q&!O|)z=V3sC znVEFDt&qE`)u9@PQ0fI>0BNc}*|-_R)?i)a6Wpo>dZ}@WaBjws-d*yggIcQQQ}4FoQfrt+Q4# zEaq!Gs;SV)`94v)r{<^3h09S%90@LWWLLDsZ?OD4KDK>HusHr-oFK zj+>k3$8LiDroW*IUeJY=;@g%Hsk1l86A}E~t%}CM==aX9Lam^try`Y`xYL{0eOkJf z&MZEkWT}oin%E|H{F>-&ieOUFXyMc7#59t%1LZQ=vvY+bkc4dUlUfUQX%hE&uSd?P z%Z|>rY$_X+`4p2@+A<**Eb@)S9CIg5Bimk^jLDhl^*Aq-u=2jb;1AE%Vp+!>5gWL2djxCh3%H z67i?n@JFf{`1EAEJAAK*w|L_1^c8d>`tmhqK1zaoOw2GiW{PBle2K2)YExh1ptd%1 z?si5fa>Tkw>ou`y`~|*QIKPYtjmJ&%pEVctE2cjt{2J6 zeA8-$4W@0gQVlAnc8@&(z}mNfS~4n(0^~7YLg>fHQ!zo}mP?ZdQ1!vB3^Ce~mzlcc zpg!g?sh^`tU*$K{&u3)$VM-9K8T*C(yQ0--sv?kjAZ)#@-E^f3V_o+SUq-C$0%d0) zQnC|z^9*opp_dto8>w{FPKqng_y_}XD#4I71{i2-{OvbaLAn6?kWm4E6L_tY0Tku$ zL;D?90(@R=77&erCO}pHHt^=(e{v&rV{srUV@G@OCN!zVoF4 zKxxpCSHo}@#%UkbM|D+i!%!D4h?sTms_*g+?3E&>Q_u$(=Y4Ej9r9>EdTq-bm35=BHuYP~ zwQolSbPpx4p2wJ-W?+>xN;VRwoh4%P?aJ#XJnHCuiBCQ2jc(%|AJ#+}z(?~U_~x!@ zqD%cj0OcmgkQzVD%16z%tOX=M$}iEC)c5qa)`CUmJ$sk(M(M1%FjK8pOK=)VlX=D~ z^__F9FK@SSeb05CxKX`_%!s01zwDPjdi3*lKBMX8l%K6`?Pf7ZlcXgnG9<4H_Y>7+vaa zS~G~WrCoSAIif%F(zGmPXG*|pppEHf#>t0Ssg(G=%m6UyTpMR~`68Mv)!6zL&Zub8 z5|^JzXUQZw6JE~0%%OH#_FxzyK1XSk=lvQNT$<$IclC#6htmu$R6%^7WD3ST*k-=W zep$E}E++iso`5vaQDZGOU0pgob*vR%91);hgmX4eE_Yl{H zC=r1t3y&Ax>)z`*|7=fb?(?S1Sj9cyQ62A)yKHU=&q}9QD5~$7e%(EFZF1o2`JHvw z&@n^lft=%q>jfDXAU}eJ%+y#r5yzIX_Q{?=jVdZwVPVr>O38vl3}(xc-}E-$I>A1e^nU8zU;x+qQ?|MoB0kzw;BvJ=-kVPGM2^F!bM6Gx9!Yl>)Og4w z;{-!M2aaEsOqE~QeY%oTz2Uu767I7>u^CbTI$eY0fP`fDTo^%*lsR3Ue0LZrwMl&i z-pvtClIsu-diL%oQHA?x9cw6nO5#94h0qN=@%-oTN9M|ACZ0Gc^FqU=9@eCd`-+;S zT;2If{=;1Gr+~Zl(R`ZQV%>HOJy33OH#^K@ifhUeiW^@Tp{#kcrz~F#+Ewk*rHz=4 z-+%QoI&PXABtUJQ;bBM^5oFHV1n3TJ6&qP~HgvG%pWWm{ z@rIQBVR=a?akwB++v?+?mtQ;u%`Iq}L0x{G?Qn=;{qTE6A89q;)&E(H3~{C@Qwsm; ztmkxy+-8~w6o;JtaKHc+jm_=1#NvluHWMA2@fHY#;t{fdS-1Ll9kq?<-+^Kks{(`=0mhU)94d z)iN_5wg-CM+osqruLd3Sac!YZD4uC1(8J^C#y;EbG66t67$ zBvPeBsjd+ItoNpQXY4?F%ERNh1STX%ahah&eGBNWIaLv;oICAAB!o1PN`G!^d6Zdp zRIt3g;Pmoj#5V%Z>M4?0THU6hi7fSERf1LbAKpWDj6QahL#4-%g(4tQg4~ zkx@*CkSD`_rIStT?46ryHCp&tW4;>?soRuD5~dw_!iV+xNQ2brMBAAPKWt1I`$e9- zjXduge|%j%m~=lwrTd}2VoYG^Fh#vs?-)VkXl+SpBhs^aPkHl)d(ihZyX=*jd}?>) z6llp??R9AT;Dpo2=k9!`@2PTMiq%1xdaJGH7Vw{eW%P|<*C;Or8Z7Hz->}ue4sbW8 zgT%A9p47#0$(7L(->a*-G#}SEfhMKm*Fu6=ow#`OL($Qn8qD2uCl-z13MA>*b$GGR zJQ0M?SB{=zU9>NDaT4JxG@AA}{Xl<%CDy4f+;XH;>e1)gXqO)EuvKx~H|8}V3AxwJ z-&fAQbVmC+z}E^V9YDqI&3!QS{KecY*Fv37B<7X=85#aQf^TIyn$M@+nB+T1e9!n& z7H1f1YB4roVXMo0M)HD$@wa;~-7;7esavG)pkM?yU^7{8Q`PyWW8HsBwF<`KX3elm z#}8oC9~``Dr?{|d+=*stbtM#E^7xa9%g5G;TsDY2>?eP%x4`Plx<-GS1!ntX-@3z` zN%Q%GJ~CJk{u`J#NVK=q5<3BblxCKO=&qV5PJC@XaZNPy%YppB^fz(yO#qKzP@oq8 zR}DB`m(7RwUj9TW?m`>tcb=~?99W?_Ad;c+j(mIs6mZadfneVi%=|xzBkZJ-KwXnuUFB+76eF=Pg z>Zcxe_6|-yNvI@u1QdulXWf5QwAe+68n4|e3F5b)>!L_8SrUVoHt!@Ib|v*EQ$pW9 zlK=1`?z|q_TGPn~2$Drr>NpKEJ(lW-+nbtfQ5&buQTnJ*yy1l9WM_vOyba4=f zW`AYin`(7-Fq3sIe)$};ew{=VsrV{sF1gsoXYf|A5r)(3b4Fgi%XQ=OTTE~HAaPfV zQBzfYt%1s&W?DSi?P&pib9wOgsvHaD9!jWLIJ1k^vn5^QAUC~z7G!43C4}1IFo1lw z;HSwk=ch+ZBz4+gs*I%jI(VOM?_TEOQvMv8tVG7mf465z+>Z57`}8Sg zzykLw*<<+`dy6(MK0}TmhE~ZP9b%aOW&Za$=CIxX1PI2A9F+48M@8LEe=uM+yGnf7 zU3b=n`wQr+36Dz>-2UVE! z1v(1X(KTB8dv8+=yc4|d4qrk_HtjC~sFfVtS_GD^m$#JM3wL^MYF%N#Id$dgSavGF zbhre!rF^B@zJsaRWcw0MDY5jrOinI!TuidZi~q?q^tpvF*T>V@bcrIr!kFHjWu1rm zEu$v=JUQl-5LKwwSe;Hx7FReRxim@^hQ-aE(}>K?k@hsRX6M=!70sGn6X1&P3$?jG zqLda}PzP_-3~mZX8ScCmVR0!(r}79E+@2%E^shLK?KN+G1%0pE0`|WytLTQWbswNS zpZc0dngo+O(jG5tV@xfyJxs03jd+XI4pgX~SOT~UldbkDgSdR0=MBPW9hUHhwAm8n z5sB^#wCi$7LT;hcSC~JY`uDvFXLxRZD*Rw#j~Y9cgy$DZvM=roI+k;_R=F7>>KEQE{NduYaFO zyLqebKhN7LyLzgnCV+=4B7J|_6*kpbsSpt?=EO@K0VRhE4+P%KWg>&HQ!E-+PA7{1 z-eP5s2d0#5_e=TBpg|V-i#07ImVFw&f9MR$@|(aN!glV^2c#wxi<%+Oja`q#a{#YW zQgX@M=qMZ^tIy?YMZ|6E`i*!6)1J~L$(nJco{tEcN^_@%GW3@%sK)*IZT^As@bTI>H_5QKZP^(rfolNUrdS#x~ zO*5mbN8_IcopR-HBD4+MN?CqXZ7kdMmET<5YdsGj5(I1SRRwWU-+J2G5~P%-9hH)x zP@;j}SdQ*}Z&jPC_9h+13j18`&lpwLDD&2+{nKgszTp!H2b;0(3>G7w01OM97fV*y z0j69f<{@1aTibnKtUx8#Y7eEw;p3_^peVV<>y$!K+my3q_pMiLi4*`z*DW zD2m|Mdo8CAD)2=sIb<08)IrZI=pGG4O#LKGi=xohIweV;GtUI00u z-}c8eF=8eO-R6z0s)~>Cz(sDVTOeH%O|>aLk5pcNH&3V~CjYWotjiuUE}l5g+Qm)t z*VrH&;>215$QRj4&{W(E)S8o2k4{J;Mp+z^yTB)(bTyMEQsA&DYG6wfDmQj>HMr}1pztjJywx%)Ua(XEN)tAvJ8K2Co(sx__ANF=+!Df89 zdO4`Gz&whsU)PVJbdVb4ET)tiCfSDsSn_KQj!Ct*H*Gpne%8YCl%|;uKb)&70mIRu ze%T>gQqQM4(w?NSR;4v5eOz`r%!9u;-Z&O3)82>GY4NtZ%*^ilu}kNfvuJa+)>Y6DjYG)4z-T#6I^4>ksZ~sTPgdxhI@n=V!5n+T7&@{f3W@ zWcepJ2@kzPe~VrTfM{NbuJmaz4demc6M)S_sh6kdiThYpqtA`^#Qb(8|5{dw0g#20WucgB>3JfBsKQdN&Lxs_6Nc?CtK{6?a4*4|py4>e+tNz&O_(g4o`=Bklau zMu#!&=a0i4_p)u%89v>AG6ke*bt==z$&4L^1ONK)f4vp|)M=dh4^g232~MXft!vZH z2VC6{@|XzpF@H_~FPLm1!E%g4J0OD=^xATNw#DC)PXApuQgko7!Jv=|rs$Gvii@pv zYzr0{T;*9yc-x4XSa?DO=#fpuU%9`VJd?JK2GP@LDciigG1!7+))(-})$fwo6$b9y z)ka){u%^Mny(I`>WZ`eW?rtD~|K$lLoY!3SR1MmW>^aXU6Z&j^?6 za?VUzAJLG6Y4!HAQl69;fZJoF9ygSk8_0VfZi|=*0$74O0wspgMAm6OWQ4iE+4wd~ z7&U3c;&J-zV+?1Fo4tZyX}OOj#+tGIh8EXm-B0Q{e))R(h|Jgpwzc)#sXbyYIFl>< z7nQhVO5=9?IAxgvFd_##qN~DNL2L_Y1v$3(UV)`n?NDi%x$DXBJQuv8{f-*fLzPu0 zJHSuoZk^!{xXzFoFxSf>mt1TV7fZpMc=65l=lAJ6P!7@?!fo>4@)f4>GiTiOMjwfz zx}#m(V}p7AL5h4pqb7PEj`z#2SpREL?6fuBue`()?3t@k#gTn6BQX)ikq zuAlpq!HedDlwcBHfIi|VGD2aBYA|Atg3IbMo+aL^h1SP}9^OJaz__6epZ>a1#-&Y~%si!ObO<`0KQEBM!2AH@+W(8wsj^x?6OC350J=M^w=p_*rRx_H|yY5&e2 z!*+0;;N^aE36QFIP&ZfloUh4FrZ=32zVMGRt*ZWHO4HQp`8SSaUJ%io)YtZ7sq_)P zQFKv(&%6men)G;p-Oenr>Gq4adIFV+j1kaD7mW)t5nSzQI*!#Cr^3d$ZjH*!gu2dc zfVSTF)+4pzNc{IOas#$x0~>ba=$AvND46*{~s*7X3;cL zx+0nXdy%UxzEpI{=rk6F+q|B<$H^VR)-@$|L)d}62LT8oc00tSYJm>P?ap1?zG7kQ z#bzwc^WMqo=q4rxm!;j`Tz@ zfG&w?nOO=N2?*B}+cOjQBF70J6ppS;4+L9T*tE z%Z@XlfQ(VXPq`Ir_M|I#clm1QRoiQd!@GTkd_;$C3z4SMzHZ*i@@8t*aFEPZ2 z6!@U$oZOwUH^&Wp9Y4^B?KQSnTvI`UX5Tf{hmDjepQ(>ZeQW_tnE;DCD<@0wP#Kr| z!t2Ca-^>K3M1$-PpY$_tWKp@_bIf*ptQ#xmK9caD<-^wt5t5$!qW*`OJZ(xBdsrk945W^C zN74&zd~$DAH;qiAYRf|tx2zo~d$~cNDeB}#3yB(kE7@icJ8d=k5&ZV}3yXrjp7=zQ$oaC5 zmd3#Itg}r(U|7)*%<06X<`R;qxw(||w9*3l#IDI0A9Pe5(-5tHJrvL@Wizat6K4m9 znaN3Q$cJO9?@7Nn-4D$o>HW(TXb&-Pj~;()KRCjT^)Vf6W22(YpFV%U?FpSo2X;gwGmYAnVEQ-2Jze(i#bT0RdTlURudE4Xr z$4GmBxu@({UVpp7pL^(%%HLM&NQNP>pfAecGoLhsKoK4ovPfllyHbV_LQz1(Apw-i zbKguXtK-A(S61i#l{NeCSJqb#=`U{Mn1;Qiv%txd&;&rZu<|Vp#a^}q0ao8NQf7LXd=$t_ z9#{Jg$K5)M&YY7-k=%Amc$Za)J_#5PD}pD2~~(cXU1D*cu68x~+fMbAKk z(=;2mqZNx!aeiNNy~rb7QEea#ab!A|aS$f<`=bF(vOw<$?L+H+{t3V}ZB+}T6`5#n zskm$0`#fraiK>rBKm>p{!l^zIlo8$sH1|%)A@=FP-RVFfWX?t_diz^8K!UB)Z-b#2 zf#(47Y|vRo4IVK+{)?0vJX6bH0W!inGgGuV<9)hr7sSgynKre2ycs*mK-mGZ%Make zE~NJX0ewLLiP;UjvHFYW8$=ohlB_^SdjM%|Zv%)_p4Fo(gPRe%APsfkaREFq0BUhf zfLeTz;qZ&oS|xjD`xo{0-^R#g@MTY<_gJ&|frk$8*a4`%1@@`FfojNKG~wrs>9k*D z=AFNf0C-S;T{*`N>pt~1K)3UYxI0da5!gxIXBa2^KE@>=I6P040hp`i!x3w5!2pGN z_ zVLRevz(cAwUemB8tv#-)u+_oSRd}Qpn{>=TLYry+BPpMVgh~XEpKd+3 zxmX(0_VHZQ%|6)X&VMs4;wv_KzVEjL$7-u-6W}UOXQXgh+GHOs{eYuh#2UiHOBiGP zgV9e~6of&tpw;QQrjY$gmH*5m4ql-6Rp$a}=9NMCCg0S`gtO~moYd?722O;-E1W21 zBho|w+@RlCY7LI{d-E8%;^5K;r)a$)eM>T$Y1GV%eVkaDD;<@7kUv*Y=Bb}o@o;*c zwg=;ERm3>FLCLWoNTe;DX=-771CvH44^@G<=<0h3=;<6wt$gyKjx4#M04~KPCv0^o zf|2UdH^Z+)47n(qh!e|Jh$(CN5Ly2sBbn9x!AktV2}%5(JcUG!CD}qT4PxYLJu@yZ z5Ui~=bk7{;erHsnQB|Z%PI=0}9#%;5Yxy|jmCqgD_ObgMVaBiMj(ZJsUowi~PX>VA zoGZ!RAwj|myaylnIOEJofX#1%7=F0W6sXkik_+iM{xh`TsA-F4$_nGDTZP;aP~MRF zeEdou&B`Zt#&u;4X~B9%C)cNrfVas3sPptXb03gJ8)x|398=FaJyyS;vSymvbMU9Z z`FOTg&-I>*9m-xHAec3smkrd@!T6PDvSLZ|Em_A0HSF-N$EK)gYgaZan}Td7j;#E> zw-g@^A&c$aXXQ$Z?A#{0n_lXUhlop(>uW$(Ae8Jr?cE0E_rX~X1}W(f3_%eB`#J54 z8#jiZB!6kuuBjb_+q5{EJqgoGoNI_PRy(Aw-mw_Uf*ODW9CeHy5K1^mj9Qoza1CH% zkH}`T>!*w3)mbCGF}z0MxLol^F!vmuq{9)fJx&%0n%g~Qo@4?A_CvFQ?(&hhc-Aop z1=U|U70t`qri*F11Y-zSkT6kPcsoi}&g2#U9;#ISz>`-u99vp6%U(qeTOO1PiCG7i zp^4_?wN_{u@RoCB=`EltEzJY{RIS8otzUNEFPU>$<`+~s<@R_xDZR6YO6$*`sZg5Q zGODu3JNbYqDAfd-1}fWHi&T^WiVeUlp2bkCq+S_$yQT3qp)-MsX;Z#tKO!Zqu+Y1j ztu~>NvSPV9!$UlI=kt}PJkZop^mGR3q!fnojyi@%pQJ|Pk8HQXPppCtIC`b6MQ>^P zO)_@7Dwa|gDPwcE=t$1~n`gj3W*J*df$9H4%(eqIl4LQRvSdmpsVNWJSmO6ei+xCn zFnChhK25X+B_$j1^hDkf!HlHVLrg;;9`JM?;xG^ZSc~v(Vkd{km25)KIQ8&ZSsK`R z3SH+vRQN4XE~EHR^O_A8!Idf@vocl8=ucyfcuyW0G_rd)F}l|%<#Ober0@^aEApFw zR-va|kJkSvVHGxeHIcsQpmkB|!{Trd0+Z@Kj<7}VTmti=SKPt7Ixw3>=ut*5m~;>N z$K(-!!$GnXg6D|M;01ch8B-5?A9+nV2Ct-$*Z3Z*>?DSrX62V0LCla56|Q?6r@(-t zf&x(TE7VvE9FcF)zyCg-8US6v{mFDg#~1)Mg>M5?1MmLt{^jYy|H=_`r2yfLmw5q4 zsPQw|$8ZbiGbOGCg8|U|C3A^tF4!!jUEE1{=RMB})i3iuc*(|8@ipQyDVfmM3OYIJ zW7wfo*FF*Var<0HO6u7iw>K6#b_A257ww)8f96_BW!?6@?J3J~;fU8jziXYA(Yp+D z^>=jRfvr8GQlZ4$bl(TtGCi^@+S{8d$oJrf04^?ulKevfmkH7x!dSk+DWpK}mg52XCbqz;l>ZGPX> zLgmO^tOm-tG{oP3^KkU+h3OAZ;##&jWdYCic#i#B!CL?K`K4nP7A8C|zz%#jS9^Wq z-qIADc*DQ!8Q-Z1HyO|{ETH;<&fK7s+K=e?^7XO%DmiC8HIeJ7ed5r^lo-aIa>1sr zHm`haz39f|5+hB_rEwRx>dfFWXs}h~M!rASiUwS7hbipMKJDQgjA#uhpIkLm#ee;V zXMvRw#Q~>0mLiK+sJS9*I^cRkm1laZZY^dLbYKB+M>X8=nlN3mK#`khW1&&;f|?ww z(gl}DxLC5nGvU&cgXgc@QNbmnUW{OSp2gXe?93(K?(4aieSd0DuV~$j9P`sOxW=tF zP}P@^4gw0u*c8ybuq@#Qj6yb_&+QonBU8|+vChE4nvC|;ipLkm^oG14_eQG-{>m8r+hz+!Adu9(cskhR-k<6^MD4IorF(#+ckcGzp^?^ zQoo@7m`H4zEX2b(nA^eR4@b&gsv*P1#@51Rd|-s)RZ+F(Qe3rF9qSShclriL6&ybE8J?%&%>QB>q=<-vQOswyllQL_idz2trg4 zlp+>-3+SPVNKt7KqDYSrK#`UZEOev_CcXeiX>_?_ zBVuaKB`popXFm#&`qrGMOfzhO>>FL0;zntbmfzpagc8!r5=*Le_}*5AUm)WYHCnxg zolSJ#UrP~uDnvg)H29jG33cJy-$5#mYZ7*^Zj`a2I}{*Xie8OvQO`SNDWBcbN$e5! z7Q4?<1j;o8jHildu9c82=A9SmyNkKWF@PN$y|ESI5YguRPF!CBjU}TIrr-%Qrs*uAA8s3KZo4>#E5b*EV`W_tM}Fjnw+E4f`?Z}xVhJ%LVm@J`l5P!{a(1;Kw0BsBU5 zNC@~3gM{Fh`Zuxx>4L_WrA3%qOE?L3T-+D>*A*X{QTH2X26ihOGI2P4Tka={_W0R777_m|acKPj=tKN;S~fQWQphj>JLCzxF1_ToM?_5Y z%0~8~FF{Z2>Oe8UVxwiHk8CZIB4qwpjodJ{y_@*G)kqRrImaqa2pgGO<>hSukR@wD z{L9XTaiXT=!UWh4g`psgDY$5*C?(!oS8AwUnVxwQCPhGuc~f0gB{r=Wi>=PGO*gT* znZbYT{CmMc8{fQrpp$;!#5n%2mDrs;5|I04-CIeDg5&u$1TJsjpwK6i2#(m%D+2z}W(zdi~$y_b;k2*=rgrKA);a3V7IgBuc*DGuDEs+9WW+UsFnE~Q6A zVYy3%FV{U#juU%YBK34k{{8E{Z{uZ0eIky()>Ir(C5=Nw2nbJaeqDdPgbNYr^BdV0 zRxs`^s-)^5HGow3E^_LsLg$-k+h+<+8E#WjN3XBH@9h0_(3~?B#k&)yB|z6%-mbys zmPcY04*zLFlSL1rlTGXX)3RNGQSy^1F3+sPG zr?kiQ<-OYjbD-*iBJa{LWWSCQml9o=Yv$N%9U=?<77kGI2n-2k#8;=Z) zK_Mgu@SV_3reKaf3%FBTB=f=Kogx^vz60}Sd!eKQpD~NVh2XjEEGEp&LKkEbqeL!2 zzjOD4FAhOl;(_1pW^c1Efi+g1Y?2+gtoK94o5cD{QIP2i64|#TR>c=z-+PxQ*{dZX zw-67=XFYaxrQB)m=FNOAPaF?@O1dV6&fH-~pZq8Ahvj#_vq;Fq6Ci7f#fx~Q5FZ;i zs~~$qKMZ8R8|HB^=185%*xS7R6;#wq zePaAvVDK)B{=KII53ieoOjd?7tE^h9FM*_Q>RyVo14P^0X!5nXkQ$yYeldnVCHOwUpHu;bh4lXNwj z@Q?>_ytii6s{@Uun*}UiF;(!w&et(J0%mdUrAHE<rH4i zJa!f3iX?MkUnW~t)u@16xl>*v1k+bP^%FPToA@Ei!YN#(e}|KeuC>rmESyL*{*Q(*lf)uFVsm8_Xp zgh=S{#7I@@%Z|4^x*eH!(oDgdMK{O$Oso$m1`mF^-p1>xkMwVwn%z*^GirMVxzXgh zsds0wN4uoY9qr79uUa`ZP^QLT1@6RVK3;a#Q_7@nlgL$k3k8k(Eod0smQ2f5^vJAKG0bC_}6;8Ux z{jyWA?=9)^p4nj8 z%&K%oyRF(c$;qhD`D}ZM?VAJdwlW3=k$j9Gs&&{#qoifkK(0wQN88;A9bA z6Mdc9E0vf$o?csm@}{~p-XtSnjlSPt$V2u8OstTgwRxtvl+@YrcnQ!NS88No>uaBmd}iBhO@yONHSV zK0S0G{Vn39p`Qk}-6ew+hu>KoS0b3n1!^C9W_z^ye9*=&Kt0vkgklgmgxQBu^(lVS zC6VI}9`;UzbL)ovW}v#ZZ+bb&faBw?Ub|1AJ}AmKqHWu=u{`vy=3^nJ8dY35sKH9C zVqb4*o6Tg}${+kDMvJdc|8k<37PDnN!*qdIr!g35W^>6iU<-LT{{!Vdc>@yDr>obbMqQn_G7pAwX7~tWp1~&J9VF&M zzru07+nbyG0SSi3^GoBk4`?ON@7W@bicvpV+v-d7VPwH#hW4Z1Sr%sNFkr<*a=4sy zE4>n^rHaBOrtGc<2H`NB=!-`^u>(_9CU@|rh=mUCZ5N8-}@593_o#|Hr*yb#8e3OQV2X2r~6GDYBgJY`82Wa%6AQ^%zi}{v7|ivcAVJX6N&VL(VYFu??TC z&4Az+lNWptJn1o=WD5b?9>P}Ti#4;Sug-*T`MbU-tBcmoWnKe|JWrd-p<7#uj5Lr6 z@ynf>DWRsLP(nt`Nfd8x2kCCAbHr7!jWfzzPUPv>7W|~k-aV7|!eyR2R#hwhxV35p z2m1@8-iVS7?f{GGp_i$@G_JIvb6Cl_e*q^STVvr z%l)>Hf9?5vr~Q2D@A8O>bMW}Bi)Ta)%Cu!ia*`j~;O>9-Asy9}{W3K+ zv!bxy@*1;u&QIY!4#TpbU974LbW<%?O$Vnef9z__;$ZXbyA}b@FX5_?7IXE1tX=kD z8TZ6eSP#$Ru=vA^A&2TJBP@riGc`Od2v!yoV4&cjzF5g|r2cT6uuQrlm^NS1AZVb6 zu=#dhgax<*N?jV_)C-=x@)6DPY>Q54*6rlfKCuA*fQPKhjhJjr!NE}NR3=pHDU?#% zB#UVgpp^sGIuPb0-9Lo}_UU#c(~ycf{+YAr74RRs8`XO~ltDE0l%Rsrium`b2px&W zy0_S=R~?E`1UY9NnuMKd0;+995O*`s@k~3y+5spaVfG@1wylcDa6b2-g2}04A66~H zxuZ74i{Gf+=uVx!aX3qME<5IX<^_Vkopw|4%ia>37fJIin)!%gXYE5|PTz-#4|F-= z-S8ooMS~V))O}4TdZA{1yPs`wp|aO59w6VEdkfd zJ1-cLl=&g2L2SHGbMZy#%GzkRyY2l+6LZni9OFr+YfXz|8HH-(#=YS2O58w9wH9a6 z^~n9${d^1gS8sVf)OK>$`s%(LKQ~~}^T(*ID^pfMfa@Ckj~)F%AKcM-0jCg!;G%B~ zTWvVT5dxlzIE>^doJ@O9dPutGIeT~^))IjSEtAR`r#%H3@@p+s9uaVEA5p<8a@-iXdD*s5vG(Z~)aTc?9Wwb$;O3RA;RZU!!hOWm*9SELSn6LDCh8DR%#Sy~Nk< z!r2dB2$j1kD&F@TOcr6O`P3?-@u$n$i7gND2N{cY#)K6@&Y`YhI3}-mgATTDIdMW? z92-ehb8`tTJ|yLiicd*-5L$oPUm!8s)OAL+4Tjw3E?~n@qihTnc_(EW5X}zmnW?$~ z#7br9t*gp>yv)(xp^YetxL*0TF`B5^WqeBd;fe*JO3qkHo%Q3jo{LWR-})q}&zJti zqjhdKtRUS%@3{(u7^((S?~vskbSkSKzDt*%6n`?dQ*43UAC|@P;U&6a3#!t7iu0rD z3TG@8ZUD4WRTi!fIyM357sMqRZIiEX+>op%Iq~5S5Q?sf}gde#r7uK=@>WLeun_dBL z+D1#EVB8L+PT6QL_wMJW8&Y-SJYTPI;?JkK#$hmeKnoqLRk4mDACA-U{-k1@YB;;vwf^ zKQqDg%xJrCicgV~Y5U|}|If7io~sUSOmYMW7mWZ`uf zL!I)SIM9CySK~?cPJ|h#Y+Zh{#Tj_J#-I=9HoFk_;jbQcnlR!IC$oxgU0XqFUpBM( zu@YnUpK~h#%!>*4&HXT)93Pn|Hl(}Ut=`|vq_!UD+^aEQ+!fSLYwL}C1JJfV&w#6| zBOW=928&($!?V0!xAT75y5L3I!}sx?m(24!pZg4d*{GOiwQjK=8!&sH$2$DST9g%e zRb*-mK@W0CHaG$aT3Nblw8D@3_RVyNBLrHTM5qz*P0EWQT2zCye89_7#-OP$`x175 zcPqI&olEkG@!X4!Z{vcJl5w>&{UCqZJ!ACmET9ubdIjRVXcL-+x$s%=q4lfGAxNe3 z%b{HFr`j231al&LO&Z{1c1(b18pHH;C~#u~Pg*^Bt#Ri{+~PtT>1Je}tg=cf4SEU>(i@9l z7P?wNjtBUkM9J-ttu@Ljlh^tOQSyMDTs_?qmo@>Gxwn32*)U|CgoADFl55K#c`Y>p zq&Nm)HV?1;pg8`6gZ1VQ>SPfAGd+j_(s%T8<5w0L|M?|PFb7KK z{$xx3A9%S3o|Z#X=D4b)`AN$TMxnhtW-oGt7bnq}=AbOb*|IrMjx&G_j@q_02Z}tG<`)fMcHT-+M zNpMX00XLPQP2+}y)N(Dfjs#{5;ekJ$rEeN|KY`%T{ir`Ob8ey{nH5v$taIj!U@s+6ttTLZx1Ip z&Oy<;!xUuK*9}-t%BzV*XhpB{JfoJ$jw7(xS1&bc2-qF?6B%5OP6E-;>Qb#bkdOp0j+l`v-&hzh~k& zt&?n1x>zRVR>{6~J`zgs8TxftTL0VK zz<;HaYbxJ~#kiXBhjc@$I3Fuv4jSGP-}s*9pIBPj@jUmO+pDu9hv)bNzhiGPE`XsM zx*Rj2vH&6vlyAYrElUEABgCIdKw3z$bPsDhh=2`M{a2JXV>kExB)l0|_!R`MNdEwB zGTX-Zc4RJY#77bjGEV@(V7`iU{zu1fsLJFzw~r zQbDRrq5YtbOlSGNE%y;hQUnG;?Sgm%Fj3tCVV*N&fTtQ!{9@wb6kr41e!ClM|6)1l zOCRB9tatde#XzQjJs6-HLP=wQa&EbXc^J}7zCX|yX1NWmOfVzx82;S|Bp{MM3dTK3 zm=-v-m>DJU7-VyK1ZB8{YU})`4lW!l1tl->YlbF$Wy>;9SL5sk)A}Q;t(e15cfQ@r>4onQP+kVKKLi3K zwTpAY`sFWaIPXjrxh>ZX$s~!)WeH~Ka$MxRo#LpEX8mO@f0;&INLIEu-WmT`LTV>Q zOOnDwh6TMWDjH=7RekzeUUFAk^6Fq3EcRl#vwtvJnttNuwFkci`!r@gFUc6x+bAbq zpzR%Cp0r~QWJ#xy#zlwY8uSVSt_R3mOBcziEPXoEos5&zOl6wkBT|t?E)VUUmcCnkCK~Z;374bhl z|K+4mfYX|@{F1!SH}hj`Xj_?9xJWnkCF5ZBXaH2si0@_xfZ!{&Rk+V9enwLGWV7_a ztm7)50$D~o#WZ4HZn(Vfl<56eW^yk|dM8E8l+vZUleNl}WcJi`5SAgFj5EV`72x{e z;!ulZgMl)vjY|}dDu2H3YM2xJ4A)2*?4?gFrJr^ez|&;|WLmr@QJPd40jvhz=z?RH3pK%O~u zxYcaG;f!MVy`Z5=1YOBZg17H-4w9FUS~sq;o$wYHTB&n=NfH)9_12RMUxo=!RhCC# z%_8x_@?}k8#;35< zio?5gS$KAyIe-E4PhRU+OOwdf4~+Q5YyIM|Sc1@k^S^klyk8vFF0g<5>Cay4*I6#? z`)?T>=GQ)l8T=c?R=i+ zem{3eH3+5jK2Vibh$t}>-DtgPtuqJw1rMxwj+CoJp{E}=^Csoq|*w_qJ}4*&C` zL-&8xN-S-xN2~6cCXcb>YW_%M((Y&E)py&$Alql17PU9bZp3V{n3s059L2y)Ej zftP(=8{)p)F$Qa{&u+d|*puLF+*0fF8P~+l?1c+KoeV_;y4zgOfC77CFTu z?}q>@v0$&yA%sw|C;O+$;e!)fw7zgD`DrD1gE*^qt2B;zh}jt;{aNSSQd-s#0O12} z)%EF9@uZUt=C{AjBl(txhH#Y==o%NFvzi42i><_nsp=v%yKUSr_W84EhU#41NkOqv z1%LL2|Vj3P|2l*>1aUoTDvx z>R)g*s6KiCLt;QjB_NeaC#^Q)ql0AHOC5(V+Ov`c%SzH6G77#rB|3aBSY-Enlq{8C z>@{!p?5f(0!Nu$vtIQds1d#oXDfy`h{Aq)^4?R%g*Xb!4{9AI{5dV_3)$qBc51&K3 zcQwg4y;O0z-?AWUR`f3{Tvwgp*C=Z83`n%UA7%0eN13{Rn_);5*K*0?qn75UowYZ- zl}~ivzCC>Y14=4JNx(i3HpZh6Bi@(j074Vk@&WkF>H~a*YU}D=hEZkzUA%x>&6}2? zdx>?}5(8H!W1J?d;zvS?rnhzMe0}y$&!zvV@#nuB9G74sF>7YnRXPXQlePG=O!DWQ zt!zfH8*VQRZY74rGi1@CP#nw{iD&Wym%A6O)z$*RNc>3jv?wqo4CWD>X)7Y^7gvI|kk&Wonnw)hS#e@KIxUcJXUUj(h5sqh(N#BlC8CBb# zH(N1@3DhcDQP}%Ve{jG{7ZKq9Aus9ae5g)RZUYP~Ha~d8INsMJ5a8bdp(Htc(Gj6u zD{Z4?`xh+Qh39s&*N*wfSB||~)2+aD9Zk7%YquEN{*w$ah4vr?B@D15ZbBnbaH#|j zkQlP+V4cBZOi71$%50W>0io2DC-=kxCRHWjRl);dHKkqOs;v{hU~sN2jbdbi4p$vj zQ?I-;*t4Ln<{7BDVzT_i8uun^`2$@`*itUX=6IsB<|ofC7PN2Zzel(CA9_!&IYcsK zBAu@pLlo{{icu4*zyshza4LmOsRdv&pG(TfZ)w)Jnv!_YoEDkUYq;GZTklz1Dtj4% z!?eIS@|Qz8M@k#p9#Qp!dA3jG9ZX00eqAg3CisFwOkImG>VL!r;!OzuW2$j&_!|G# z?A?r;Zz!iq%j|7{hvsl%7C|8B0iZ{_J~ka?mJ#{>XhpyaJ};0%Yua_C zlF~~{q$;IDA2t{fQ(x3Ti8a*jLVMd&1Gl?j050?LIZiEvJARvEBaNyJy$@T4BJHi=@u;w4ZG*msG5G+k&yb z?Hzt%&iSrxfgymIQAwi|gACg7WF#NlEq9^a(nI46)u8kRF>t%7QeovW-Nf+$EZLK- zT+FW4Xtcl)5{j6|#Qn~~i9*t~83sDMgG!I@5QsYWDZTHQ5)G7|)by}MJc0iB*vaQ( zcD)?W?uw(<1QZpDdB>axhUkz^3tb5JB9wB;)3TQqM^%kfhVTX;+p-Q)adAhe>pG<~ z_6W23Pp7zQQl>SW-v*1}!V%t7qmR#5c=o;P_y)qFcmEfC^CjYMV@5dJ;MLagO=mpP zFBg#L2xMgTKBah0n}XS-@bEAFR~qXR=eWgo2^ji@3XYp_e7OI>jF`;i0yE?BaRSVN z&+b8K5v8!1_|{C-=IxWUfmcT-V1`A-#@<%vYR)1Zn@v~NR9RV}*ZXuKybND(;V`Hh zKt0`P1R<;=^$BantqO}@S~%RC18OCuxGYUpH*Bl zvW7pyJ5NPO90y{kSd zojV73Nqqs%(117UU>8RKKvx$a1^@sq0_e`&1DvOh7^q@Oa}GdD?axu=B@NAg?f;Ge z23P+z{`z%hX1HiHZ5jWv0L5zkl-g=YM}i zeP6yvfAQ}b|Nq+A4*<(Wnj`urH0SOB&a<4OVL5l!4dA2Bl$N?af0yXLwR7ibF3{3Z zr^LWWJppl*I(-_N^VG@GUZ5&|E|hv4aDj#P+UwjqWAM|3O>UExa4KC3C z)$82(0IJZiT%f&ukM7z-1Nv89taoJIUA+D%A*ZV262Gh=p3T;Kgn?Z^4lPLdtJ;6n z?B7!??0-wM|Do9b)N2NCnQEfvS!h@QK)@+EH%bKXZ%fM=01L+%;ExU+>#LGb*Hr@G$wVYUU)hR z(g3%k?>a7&g4K1R*OZQ81f~`eFsnRT$xBXKgoaYzg{aX?+k=@S*D1B++$t4%0{=r| z&bP;<;9%3LNx9>_>R^}0B^@0{H&lGA{8lCk9vJy<2y;quxefy5f@y3+p)yX0!zo z1NjBscCeT)rQ8aRLQ{C49j0w`WWBjbHL2jIDNz#jGa-i~jW@%Sjxnv}(oBvG={AeZ z5hWs?ewXAujS(s;0oUz`53>6?qKfp)5Cuv_keOEIJdyG^_Z}W2Nev2wM&omWKG?Chh4Lt4vD=9&^Mke3BPV&_}oDKI^h4Ryof|X z;~`9^S!BtZ2(0H-7Yfhl>K7}Nmw&fJ;#KoofNOf!GtbSmSUoudgtdlTAP7e-kG>9h zB|r8!_DHK>?)1xfudI~^!I;+9Opm|Xy~5oE2aa1~IpvGeHRHc}v74ne+f~V}x2lOE zm2u6QW7o~3@xxgCa5b5A-B5IBu=0);rnlPq|O1E~!i{WZ*H!B$8Qk)xl zJSD+nAENZ{C}d z2m$7z#5$(}o|&rRvdVr4B)xyDD&8g0z64(c9Dvx*0CvBLNVoO#+ioA_`cvx-KFhYh zJiyu1(cBrR8d>Z3m8HIL&pmYgriMc|rz5Bk$ie`{B-`yxRHzIW7FTMb15hwJvTIE` zwzKWdn0ztrw?yuOe!5H`prq?EDQdH9xcgPv^Wt^0=$O>?!t?`uqjtuaoT&UVbYT&F zO|&jhqw`4E7yAoh+aCL0SSM=*iCz){6Q+dP%x{kK;*y zqA0#(2aw(`#BtA9@BMvy-aXieUt5UvYEkDe{`cvl_KVPh5)$akUA#B$*QI=$8L^(POB^dx>4Qj_ z^mG-lznH31 z@jkN88GyFwG@Hx`O!LIJ8+BiaS$j6>=r>%rex8ARp^-R`j(+&A)T*}@+Arutc~oLe z4j403Jv@ORz^&=x3Nwq3yle{in8DgfrZuC46lk~h9hn66ZpEBi9(T|J2_mdOpT5_slBQrp<4GvvpT@mKouv%3dF2WpQ@oY`U0MPRd+o%dd*$=3?NfKfbGK{M>s@w z8&l0DttMV(IdjHxW4ZB~>tf%|uF58gkGn;efOj1O=k+u#)VR? z!-~L!FoDL~`b*;Lpx@k`(Mx9lh7I;Qisfw2?dNxOUN*i;%J#}MTflG2a+tAku@#Fv zPPHtVKk?2_JkZ^OfKWGH@LsM2m)`iK=la}5NuaU~Ws?;t~=TSiBCA>fOajQ?~`LbSO-piFl*`)&wOqCIZ4 z$X)z)(J!_Lip(a=WC%@+BHzH%cWuUBQWuaRMt5_S`MdSM6S}{pa0XDB@Vp`%|GGu@ zj%&aIcf(jfRXDaj!Q=cgj{bs_S4=u_F5ul)YyBhF3DNqI-9hP7L5lDn3(rwSrNZSZ z<3AHIL6t$?X$8kqOYwigd7i#YjxgoC;46n9a^z7ap{#Ap;0%(YI_L((R_toou#>de^?xd^9p+ zS=WqiXIdzKYx`8%er(2rERoZAxY`QufZ;8m5pB2PfH+Cv?sT2I++Xj+ZO8+6`0e9E z+rvX3%AMPSU9R{cw-z1;qA>Ia>2*8O<7$sH!8HSCC=-bl$RomR3~YdCzS%XZ@yFUI zZIJ`=9!uoY#^GD@oW_nyb6X!jA~>dDK86g+8jc;@IN3%tXr=8Lx?u@l^^`B-qVpa= zG12j>2}jeAz%;ul1$q{SGE!1ZyU-wmNN&sCIQ8@Ge=cy;8Q>bU!?R6x4l=eNf{(2x zI_Zyvs20DNMb&6au5~B&C9EuH2v+e8#Nk(Sxq8^BY^1dDc@` z+@<@S&!;D_z2^)-6nDbOo+65!9}Gtq;s;k^25Ea*#SB=qvbXg)~BJ&3q~$k*{+s#lMxxe{W1I zSZeUNtpv^;&ud6vZbF(PyzNG{I+R}!h_f7`f{qP$*mlN1WHY_S<+%W(J1QGy%BN45xRQO*E78)_rTK#>yJ zz6y*mhlyFvTjQxf4QOVj0m%UiCn}Yp+Y-Y=fJ@LDZbnWNo8|hdn0Q+*4f42ID!87) zBu@|=myvppAAVBfctIfTBr}bh`E}_Xr#&`;Qx}|v7>}VRvp()luaAT<&1_6@DMq75 zD?Qi7-Y=@V9@e}-^nT$%@;f};g}k`?6P|5kLg?9DRpoeEGfX~eSJM&L|JCxO04G%27%`*p z^McgxyFaz7KQqZ(Jc55vd72HZi}Z1|l*|lqK-bCGskV6>>`UXiNaElm!_!EpBb3ke zrb1KWUWjb49XRTPu3V8p>XXlP+Jx&Re6|<}vM(@W`U~{rr3`Qf9*!h4x3( z_v)xP|J7>MYXuzACDVlwquPzmH0rCjp1Y)<_Y0~ovJ!B7(+?XTyC6g)%Dc1}VCFra zH}7(BTg~emCSu$i9hBaT-c#Y>Isd@63I2{f8WSI0KSLWCtfS`TSYicnFln#z-+mn5mdteo`baBA0p1%ycSUPj{=< zMc8MdkT8T;0y~V$O*Vncv9gXx0=CgM4|pezvs&pm+z&_xU8(@F6ipb;J|mcQi>%hK z9

%Pa|{iBOy1Y_^YN0@g5}#zi{q;tmf7a4*4(BsUC72E}nyS zxYzQU{Bp~_Jbvp_r0jay$UGI~n>v)v0Pi4d$7NXQ2pc{^#m~lx+TbgXFjUmmz3TX! z>7U^uRnexYE?q?neAO5Z>&`F6t{e+w-+Od!xUWy?#0-gTkw(v%%;Brzo#;gkJS` zBfaW2L1raA2D4&RtZIQbzyU^o7`mXGe#50}Z^-J9%U~Mx0qR3;B4>y?=88$P{K}6c zo%K(&I(Md#w|S|rD^k@=^}fz1c@wX>|0qu2pT5W9m%|=#S&|t?&H!$}PmA;| zx~Cbr@EE%)_wuwLy>h#HN7Vq0?ehu5sqYq4eViadA#B0osy^L2cjm%=L0w&3`~^EW z>C7Xxu_ZSr2n}Zd8Xqo~e#Va<5-AQMkVMd@6FRf~kNssznjG&-I0XH+Rv6lXg*lq^IdW_XIeqU2uvJ_UCgXADgMAVWLEP7wD=c-(hMJ zMpy95m4O6JKQ7o&LbYbwVrLtW7aed7jQ~5zFEGiFS=*OF>p)n=i?+33gzjMp=VL;m z0a+g|0_VfW9KV*hoWz6%Sk4Lb zAD74zhyz8uAN+epvVa72)ZSClR=Y}n)n*3G$8eL=UL+{Vks8z61aACWQ9avNthFPE z39M*3e%sik{B>bNjMA~r9ofqydowJm6Hb&GM(%v12u`#R(7__L?Vo20KbtDP7Pn_l z_)>Ypry&4diMC-F0KXf285lY%JTNm+I8hFB4DsafnzZWj#TwmPtxa-E0X-QFMoi=# zx<-+`@WW*Gox>R7G<+5e?F`}zLsq@1G?3gsauJweyrwc;clq3}sm~#gsPNJZ8OfGT z!NEgVIHjS_rqYMoE(ioX=5R)xpk32p_(32dC?!Ip!a_gRns{EmmU0j|T`- zxNrhLvMv#Yk^Tg^+k*HEIgmm$k`tA;t4yAhPa62Ry23>moO+OiBn~ zwCa65byc9RUH0mRsrv1Ae*(|9ih^|`yG+7)y7+ErYayJKEfrdqmlNkb-S$-)h=`9p z23aA^=R}c`!hvNAW{BL}*=by`A23wKj2f)oCNeKI=EspR@b@7DHMMKH+uL@g^{XQh3lU>fx92*}eS>we@XvNDU>PJkUVl!icKYHSmIp{OM#ZhQ0vMIjSfrF^7MG;N{bxYkXZTETi7jSh5Nl|3x znRs4k2U3l84CWL;;UkurdU#JAJvT4jza^TSaL^Oz84FyZViqUJ>L=Cz2tcS3CZ~;d&!RRtC;leByK^y)-apkFZlruopEi8|A=V;zK zE*xL^tnCF-K5a)S_U^g_Nv1;ADn>rXcc=$(@-gEJoV9)NCPG+IP1i*Il$oPZuEU_p zBD&CS9*al$w~ctU84P0Ti@1HnXNLCtw`)V0+vTZVnv^Gw5i>ox&}E39Oc5OWh}Gfc z}DHc-;B(LI@Fq^h9|=4olkJGU0?w7CzW&G#5Ea` zy;Mwfc!s(YFC{*4xkNAU(ca7s<-&x3;1X3V^nU)k65g+;+3 zd-_~uqW1&b2QJQ0NgQ^!DK*w|h$3DpQ4Y=`+3fP5xUrF>(#Y|Y{+=51{Oh%BB{srp z4qoTk_^nw#uTq=(Q-vmFYFxXy_`CTD(`Hq zKIZ1I@s97*j9B(co`CA@c5Q%NukKEJ)^|<$z*M)E5|2_cn!UY8UgMlfCu+vwZTGpH*;+jN0GcY5Cu>=#VXqJewjPD2(~B9sC^} z{Mh2VA)V0Jwr$Jf1Nv7q>*?eUf?_B13OL%7GSc>If3_L7^tIrvtO6ZfDEJ@`31xgu zHp@kMx_bqF8{`(R82W-)u%QD8FqvgX{xa*qwn(7ow7pZ=)c2Q@^N64{ywe}*{lTO2 z!56E~dtVb5{Pd9v5EKhfciKXQwJ#L`>B#ml#ZR*uX-c~X;##+XN55#HmIqM!5H~!u zTihYzF+t;9nFfkxtDf-FXZ@`YD9q*>-fuComQ!-dW>R|Ff+ zI$8f$^w;4@>ta}6?k89Cs!&$g?ya~Yh*SpL0I zhg2y;zB5Z^uIne;p)Xq}B1P0DuICVY1&J^-0~@oY#o1uAV!TrvuOce_T)KWU?*MXQ z7%gIClalW?`QWrZ>s6pzDDx3+)q-QsP(1`9D~Wu?Cnc8r?F`V$!8gdi^M(^i!+Y~J z8`#(yEz-aH8lisO?9!iqk3ctD(Z|84Mi@t|#|Iy75SY7o>43}@&Q0n|aSQ0$F1c#t zwL2a2nX%JJ)p6o~8r&l8eO(nqzj;xrWxb~?ZY}lc!1=gM1e3}BIQ(Q)IU^MpT)OjQ z(QibpXam7UT*0&xr4^Bb+*S=E?sCI4gKsK7v2~~WA~huF@t?xhgc=s(KzLLAnWWC9 z)2T@EL%2Fxc|zSOVpt0iTUQ-ns^#Cvf5E#v^ucP|808klzwFeed|gBDP|pJ=Zk?Yy ztG&6SCZ%@I-ly}_BSx%`%k^7%#)QyZqV8-X32KapzqY=>8x6@RwlFW0ZNm6{wA<)5 z+e2W6XS=W*(I)K(H8C(o-Lr+mJe3w0CKI*R66la`L8kfD;#UgZ)``{cwk%lrT-9Wj zAV5<3__NcyoBgKi@?Bm{`E_G_oRW<`j_38Op<|P83w56U6Ddg(e**VIY*O#(lN4!1 zccq*Hi`RW?O#=el)JsStENnJ!X|c;KHPVupu~a7hVI_w$5v8R`K6lA;w51FM#ant# zuXWsDA@*UdKd*@wtf#TMQ+;rmQVN26Iv3)M?}7EnyP2saIGh1K%WFg9PINTeCTC6C zo;mrldl*xM+15w!JK>_rw)e<8wGPa^qJN`SMRpUjt`%~0OCE5rA@6VpNoC@_=sR1v z=(+elw76BgfC924x6qztoSc!Tc(;BPgEIgne`n|J1meow7xUUuQdH`SmEQXz_KK7S(?c9cwGx4K z!}t@Sx>bh_t#g`7xy`%TylBy>GL0LSDZC1a{$=9x!cad!J}pbqmX{-0y=NT1@5S0P zT39MH(<(AEG8ivR~s3EP{tBzT0wFld-Bjr$mf0aEnZ{XOQw4%Y7=<9cE0Y5xb%mCIWt0x ztUQvpV<0L4@NXdwqT|7veHD9^s}h^sLBQ{%kCB+ja_E6HG>2K2h>YkomLy+FxOklbXo zHcy4Xq26hnc+X<(PITLY&6N;q%nl1NY-pZQ!_V5#Dz-?e?>OcWUhkg%>DSva&JXG% zt$ND*%3oOMTwBba&Q@>~xZ5Eta&hOk*-#>tif{Z_FaRJ*MSEC-mJ~uND;Kt=k6wri4sQlKri^<}1C}$L zF2uFE1)x32Y@QgpwDO(;7m6V9tDV=SnOiOOi5qe>aRHv^pxQ(49`&oy$xJJq7|X(k zIY!&CoGEC6X)IVn=Px4+`?wOg@|rudxX&wT=6Q*jyXrw*{Mh653K|Nd7k1~uhCn@`zPsIuL$#Lu>!k0Ey`+dcO7r5w!mCt!AmK( zwT1%WF(@#1im_RjafxnQ_SPtwm8?JFy_j$6rgfW$nod73mO7ItB zKxzh*P3_{J!x}d=k#Y8+^{|o&3ET1Wb+U-8b6!6VP^}@3z#ns=$}Lht*L04PbBLsV zf*MKeM14tIB-Dr2P%1e1{-Yu^SZAf*=P)RW{FCTMg_z}uS?!2WCw2T#2~7eefv3i$ z=Ts@_&0sK;$`;oyGLs*Bvbv@-f0UbgwI^Sr9(wgeraI{i5E6BK*)u#uCt_-YyxAhi z3E`jytwS`(Y?f|PO*cu@zvNQqf-a(;2(ve^5k~V7MPME6H&A>@Sa&s~7T|P(>}^@i z_sIGY<;heT*b_Cw5o2dM>Q^CdY|2SDx_zG-gTGAz@y7Q0T)z3GiM5C#f;i6Gky*Ia zw?`X7CzJ)k>&cKaT5foXMb|r+T z?zPO-*Edz}I7w~hgNzPjqR2jkcrr&C)-1v=I#y^=7q8mgcU99zDobJ49O~!DppSkL zk76c@OXGCWlD4`WHj@=(VIR3mcjLyMA`C}XRb?Rox*z+EJdPx^i-_{sOo_Iv#C0jn}MUv9J^&t1|Wr!df)RF>`g2;kx`&THFwf_6@(&TW% z3~XB*VH@$@xXniIkHj2&^&hxid}N5I)+|f>8a2}@-61O5{})$H&AJ@s{yVOo0g&zZ zH*1}t>NJ34p1=}qZW0IDAsINYI5VadU;H#Y zM4m)T)|ylk3Xvr)+Il^Vu=pk^DaUqr-V_D(H^#or~~8>4HkZlmI>tr!{`USgC`rfib34X-t%f_i;L z=wu|jd-n^_Wl5fA1mhBt&b&>syOW!ZZX0zb_Q`uHGX>^$ zvYG-pqJnxpBW12QCezemQ`3gO=6wN z;Xy}RlKAb#4j{`8ZB)6}vs^Cw&~H>!!i0jbzp=D$)iy|0#w5870_f?v&W@hs*w+#O594T&$`#sp1&>Pv2VidjNK^xUTjs; zK>97b7!|Q1Q#Cb@scumd74ysC6da|p!oyuM1G|;i@;8idtpqABIg=;16js zEv?4g%(2jbWJFH3tG0lavV>9W39}m~ew8_w1MMxaSH35NX%-ldKzN0|zjt}*pUCZP_4=z{ptl~ChxG4Y^Y zpOALegRzc9ylMOg8=-8a9jSvs+Wd{FlRy!e>BrU}R2bBaF9OEm$l^>q-m5VZwG`hY96gEJEhfc}Aj^to*|xV(Au;t>!%ct6gBn5vPL0&H=PY4% zDUc@PBk{QL8O@Lvh$9ZKELz<7{Xopfvj8jCa;Uc`yC2iM&XP4&wzY|jbuI`-Q1R7M z5K8h#?E_r`uaFrfuA$`IN3bvZpe%-FOS~v*LXa=yS}P)>tBM%5;&F~uC)ZWdO+X2$ zDEjgSPd3@M)sE@}gdxc0L|ODtKVGBmrg6)V^5QOIM5N)a)({QbW$Xr%>L#{|LW?cg zQxOvT7sggP5MhBSq5N;;lW)q>D(yM9W}*!i$&#HQaS&p7Mq7OM2u(!=*o%hIYiq`% zWa)CtHHwly*RcM3CiOl$nXE-u2V0*+QK7UWX=vu~}@U&&1!`=OXENNFw3c3;w z{lhIxOy7LCm27N1)s)(7F>~Sp21d7C2K)0~(-I8BVAT0t8k!QHW$t$>&Iz;Ph~h0THTxQ^cByS9Vac$;)MRuD2GcH(ID+%kA;-=dd<$4aG9xD|DQa7L*i@ z6rZN3bvhE&@q#glA#LZMQxmquDLZEXtKEXHZ?;)UR`dt1UWLgS{!uS_H7u1a&j891 zM%`afNmv`P@>0d?Da(uE-ce1hI8X7L@|X&lnXV4YrrFuq&UV|3k61=B{V$_$X}x$A znd{dA{fmF_S?7_R2`@#_q3!{)%-^4C=y~nRi&<2xb$w|yoxiBG|LhE)eNt&yp#u(U0IwIA#opGJk1MH#9L7=84-f@CNrM9!LNanNJ2lYG(RWl7u|of z{8_OO;573t)Z-k|sY6x6+N!$C%A97iv;# zrCL!~9j8OL)9f?AmSNi&U<@&@;q|WIrBo3AA1s>V@2&CjWR>FO6wE}1j`^s;dw za7UA%C#%q`AkdcV8Q_++6{UIb43L-JI>%k-exst%;rI!Wa%U=Hqjv{Pk<7+%kb`?E zd(a23Gi@p!0EtJ?qxil2BnptdZNv}Ax~+yiH5#Em3D5mk|Dl8=GMvll-qW3&$h`k$ zeew)IAP5S;2z$I{pag5kBaqZGpQY8$?$6%mmTXh^PC;1R_J+90!R6E^aq=`0xLRgp z(N}>C9;q1}AH^QX<xWIrcVRMe4ga9I$4w!P_-36+6& zoz!HutN%D-oP4{a2Mu=Lw}u?)H->k~9l0Iu+|%ℜ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 WQM*K`MM)f}rSj`25>K9ozKk* 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

ELGAnY;zx7%*WYF@r^l?Je>`3C{ro*n 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 We=E=PJIBy3v`PHa+EMoF(BA+=HthBQ literal 0 HcmV?d00001 diff --git a/docs/userguide/storagedriver/images/shared-volume.jpg b/docs/userguide/storagedriver/images/shared-volume.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cd39b86a1f56642ad20c403bc2c77d09ee739a59 GIT binary patch literal 48857 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 Icx8zVhGD`m~6r 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 literal 0 HcmV?d00001 diff --git a/docs/userguide/storagedriver/images/sharing-layers.jpg b/docs/userguide/storagedriver/images/sharing-layers.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e59fa833c99055df1588c265a275e10a0ca23c18 GIT binary patch literal 56036 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

!^Ml#f*B?ev;^+U`r;r_E)ocaQ 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 VOqbC#@ literal 0 HcmV?d00001 diff --git a/docs/userguide/storagedriver/images/two_dm_container.jpg b/docs/userguide/storagedriver/images/two_dm_container.jpg new file mode 100644 index 0000000000000000000000000000000000000000..18d840d0b053d898b236c6f4bd3e80ab14efb422 GIT binary patch literal 65447 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 DwU#> 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 literal 0 HcmV?d00001 diff --git a/docs/userguide/storagedriver/images/zfs_clones.jpg b/docs/userguide/storagedriver/images/zfs_clones.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0849aa2454ec03f921ab1f2a2c9e2d5c30700299 GIT binary patch literal 23027 zcmc$`byyusvoAagcXxuj26qTDH66US?0r>bYK-(LRfV{UG34jl+ z$Wf3)bO7)w25%@BdO;Qd?cg2GKma`Y_-VqwdqYvgB#FNKIobv(8M(N)&Jm7x2OJJ5Gu>Q=jD zbPt?orXHQ$iREMgU=!-=uZn;GT0sEx*H{UUt&2gg=f{WBn)hoUb9(zXwh$&ULD{Ev zm(r^PLqC_B$Lz^TraKQwFYy;ZD`xtrmOz<%AUUA_O%wuE5kzgv#6D$~=uNa9qUSrr zK!6|fLFbup7#V$yGh=Y>X!t*@2Tt$W>VsA|9*KD#o)rcr0H9`;_)QD(VaP22oV=d| zX4(Y-e!MQoSHs(~CExByBe!qdBF|!kp93u_@z_~bx6NomfzoBrI z=1>2iHjW}{S;rl3sAxRWKAeaBYg@@G9Cev#NLU!xDk6(f7(d)@^&)4ZJO%3ORC(d?Va~hp`&8>oO z0|;SN3NU>iQtvAu8JaW)K7Ts!1L$@aHG?keTTDU#hd6`~f&mpux`{tLwh?Mgvo?Po zWUSASt@U&SRB}@dVI9eW{HyWo|F0g|8{J@dIbyJ0%hKgKcjC4z=0TS4cc8Vt>o?GNSu}K2}934TP zcV9yK>Tz4GH&n5~!N%2h=x0rQJOEYJs_8+1NQ_n~!|VZmTpv);qO0A_%HVKN;qguD zcpdng|2xagPm9Qd8NwK)i`OHm~Xv&!s<{ZH_(CqK{?_{OQ}gis}R zZYAq36qXHUbO*n#AOm6)^tLC|!8F?#1oQazxUS!86z@^7*F=GZyOw0Jn$l+XntY@XCnR0fZvxnSc6!f4n6({MYRD zmfZdm0~`_z0ty8ES84-$i>H4RV$_--(AWGIz*0E^KxGzIo#q0-~84*jWzF`mZp^W(Dr& z(}`7jBkK(xrzx$wQcVk3x&=GEtKjB-$EyI?QWJ*8!u0a5VJQ4RDb<*KG>f_b@J{D- z^#FjDn;-xw2j`j+j@#z#=hYU zo)rLCPB0^{uOPtw?EMA-Qoji*0A#L1RHOEN0B7dt4FmDF=>u$kuh_T#|7(IAjmZLx zr2g{Rt1a-+ZHTD_(ds*0GEV*UED=QCh5xaTAa5_z;Mk*=o~4tcZLDpCmrvy_kGzp< zy~CkNrkyDal|jbeQ~e4e&7LSAJZN4}%%*=}k?{K4Z?8CbZS(BZqS0yoYFqL%zPtfL z#D5xc4Bnr`)$}jDC$^m$!R`j+b-;ff4XLbaeK=AF{kK0}H4>E0v%A*Td3EFZ_fGn~ z0{9J$c06RBcVt}uB;r*S~sygKtX!@9!n>kJMiue=mW*RscB4 ze`WwMY<_@A`(H~HXI~!xw=-6AQ1An6Z&=>Mp!me>0VtiM%7Is#2$DM9FxB&Q0J7fP z;rHwJx`Fc>g6Sm*z6NNz_7h1%{mK#W?cjyadc%u1#qyt{NBO_HlZ}5Oov!1$muJ|F zv252y>e_$f)859p1-aDne^uJcF_D6`SbFD84S?o#-guCDXu#hp?Y4HauD#tFqW?80 z)^_LJg-5kA{HOV?gO8D3m6P)kqp2H9`7{4g@8wH^xu~UZdF81<#8g`-YcnzDAGkOwkq}0DN!x`Ua7${}BWTT-GNh z0MJQU#~Xw)vk8DZ{wo)~A^&elS?v~pQsd3~$BLpI%GqRG(lNfRVPvo2LGLq@e;7yt zDQk|GG8EzF1tCUyMyfVGUF-j_xDKtaFZw=s1yTkjGyXl%uN9@wYwkH0Gl~?SY z3lsg$jg}>*`ak}9#kPw4t4k}(Pc^#!iH%o`n+F8|uc>=&y%8cKoi+eAT|M%57r?Rd z18&Uz^5#Ftt6i_rFMtAoAV6SX5Qt!q;E-?M0}x=~kWe518WkH29fKGK1rv*el!;SN zUQxw_nFE%LoJEM0O&EuQl3kzcpUYnm0_X+UAwNbre`F?hvf=*LUB00kN$|}Ht^GGq znW5VAmp>!YFTjLPW0>AO@*FeW3m|mAOX0i!LA$5?3Hm5C=>^Do`fo}fw04#;tis#p z+v9c)i0k`FnFsrPk`jT&tAh8gQ+KncvuREIu}Reb|L-XON1Hx#j7!{8Dyjjhek_kq{7BLzpaH+n z%s{Ua;9G~=kKhFto3w{J{*E!p=(7x`5jpmB?8VAArRKv+%C7MGw3dkM zxOH|fF)uDDjUZtf<&@p1HKe9*- zI2)cW6->A1J%JCCvu0ac;+R=HGp&_Z$RxQn--#o;AVm|K4KetwLl~j^1w^d5lzCx_ z7zJT5aE9$gh?)6vX#HWhICGBQ2a7X@c)5)G$ny`!v>y-|s7pphS`2fElQR>}b1+3Z zYJ<6O5)bv-nUbV`hh+W80Zs7a&6VA z%^Y}_`ta3 zp+$2L4Gz7=t_vA-{(8i;6mCWP(P~IBiNOmciH=+N{OcaID)Xe8+SLAD&P?;%4_pg( z_Al%Y~`3GbV#YMTSp%skB3;4|gz+{zI>;#l4WEA*2erQ-=UH5=3yC;u^-59{{ZY-bV=y~GCBprB+p zc*~IxqV!1lWmX~+O}tbrnvx5XcLdJ(Je85_SQn(DpDot3RQgxQyhX{l$5iDIKA{J` z0CukbJ}dK0jdC<}%RXqB{*JHal}Ul^Uj}=<{F>B{kLQgSqD?Y07;`;xLNsvuG@KMJ z>3m&9Bi9S@#=dr~Tsr}KR zS-J2%fj&$K#>HYd5A7g94&&J&rN#3S^ADX7GZEEBF6WLG?131W82|iKk1xSQGQmrWDE93C*M-pr_@y~ zHAl@PMB2}TUD_xkCv`l!VVlg+T0Y)Sj!Rc(N6Bm7eYNWzAMSVR_txeZ+6A5)ZSHRV z4BL&jm;Dz)zgL&4XI}sw>%44IFh&Fwa?m}aSEec+hb1e5WMqo{=~B#`S@kH^BKLox zanhMtFhs+2VU}z8zqDlayUg7?GV0ilcL794pU9R}O7@1&qBc>sSrXlMN=?Mvl~k>4 zBgYd2bdO4z;9~?f@lEmFdn(<{BGkjBogkOjB52}EGBiUs3s*|3a4AG78+i{Gp7GkF z2Op)%bDmQ#mTB!YD2gdzEFm~|t`Cbj;k1}32C%5HbjncZL=m0{N%Bp3GbV8fvHeR6 ze}J0=Kt#n@nkYs=4Uzlcz5oXw!tLpv(6_invnw>aq?JuqHCF3xKlG1aC6bElEPdno zc@wa_5m`*bSo%)%(@>WL&O>?1@*0txKlzmV!S^R4J!HpC>=4&^$rqr%QL%`$Q1lsx zT@Lp0fGahvG7V2&Tw21-^VWCrZeWb-p!}NQQQ-Ij%gT?wt#;rQq3rBJFgR*bET=$t4?RbEJczrY@+@)mTyFjyWKG($xPQ+?Q zj2m5(T&gTu)Fc{@I|I`|l+nV?RDwz{1ent-R1Mtd)2q)*a#}{4oG|nQxAcpmeO$$M z(U&O!y~=HTD$6CGq{B9xYzxQIH;5Rjm`>We)4>|*Qno)`pZYRIwV`jq6Hv?zq53XV z1v2=+W(S_#w{{iBiQAUgL|wC{+43p%6BP{Q?f8GmNqbLuqX;7wL!ygwfjN!e+R%9P zNXd~)G`HzG9q4m^>h1aaGzK4i`>z=>8=ee$wVfM=(&DeW_+bV5m9 zl8TQ4nY5u^7bD9kxm6(IF-JjUQnS7FLVwOCCkHy~Gu$i?n;C}@_EnvakeK3LhvKHR z4qk=Rxsq@HE-_n7x_@*ADlviv;>)GXUI6PiII%BIZhlTUgRwJu1~w@&NPVA%i+L7P zlSFRG)fU`vPl!;z8pxW^_G6rAR>NC0DiDk#-Nz?Y$%f*oSCmRQrRm5P@+b83ubxFH#VK@R>jQtZ6&Su~8aVZ-!;qsIP`s zYQOfpisi!oqBUDpR6eb5cS6}sDMH&#hO9F_TuNaVNqw>L1=#e##iUF3ew`ZwDrjWZ zg~wt%W{)b$zS4HjQ8h5p9)_XyjCK3ECT0;nYd_RZk^^uF)cj?ejrgrk6n&2#V@zN7 zxHrONJ7A1d`Q(TAM@}*JK28!4MLI!wo?I;MBHS!()sEnv)%RPavaZ=h&oP;`tr3*z zk9)WAYIkNnc)2!y;lI8;`EQG7EOQ4o_wHxShSC{ZZ#{h@V2(Lmw3(Q>FE$$~Jti%; ztX+8T?y~4v$?izJG29BvqHKRA?MaRN*xlXd+W%dbN-P}*)slR39>e6+1QKc^Pjjou zAwdD1*knJA8PRIp)GTkOJW!?R&;qje;?gG%6|3DfrzI9bGbi7y*@o%v#mga^VH5A}B}x9I>Fqp;0GC zsMIfxl6;GzHE9|I?A!)q(dBzCcyIayS@x3lXxih4B zCf#2`EjBYe86>m}jQ5sc%yds!SSl}2_kBbYD<)jcX<$uzk)#SFIRvTK<1cpDctzxd z(@6b$6EXLOm;^<;^AiV7D%nw#190%BTX_0cpla67}x&*au;jQwnWU#to9DsehNB4Y=-LsYCzRO>p_XUa>d9?jvuS*p2&mGw*2Dd(5lk4xEl>`RGL zUT?DK+x@ooay{$n@QiRDvOG{!9a0T4kh-3uFwwg8Vcpy(D>-mRpK?gfw9CGdbYSp< z7dMy z@S36CRU1cO@rn}RA>^7u^EtN-)S3hNi}dP!i{s- zxa!fv#C?SN(n;>P`4L#`h_U9NK`~dO5RqdS&atqa=0t}O&C9p zSE3fIw{XT-GP!*klb|`eQP^XVECz|0takRexBkMWO=*+P3y?DYIs84gyWlsgqLO(hbaq>T6Pb@MoY0GU}8;DawKsBq$2KR zLjWWnZDl97m4+gOsD`vIUFNq zl96(`k`pIlqHoSgJq%j-{LL_^rI8Gml)ROVMv$QUM|nYM|WariU@ zU%P@owWIaA3wPWq_hZu=bKsTsm2zfbcn#AzjLNn>c2e=hYGKs)R>~`V&zWkA%ib@* zp!?tksD2(&AWW^NQ;S#U1!xGT%#i{W7D-Bh-7|a?L&aOa)7mFi57=7HOF<;Q>=mq_ zwQ}A9k5!b01Q$+c>-Yk;#n@9ZJqfcvyB4dc?=OCnhI5J?yJSkL_=ENbpdM4kM`;cg z-y#t0``zF3Nsb^Ufu2Wz_F1rT7UfBM0E zQpzd)M*zM&jhn`!fme5bsLG_afcU^5nA{5;1jcI=R!skis?NudhDgbM?Fy%FkA!LE#rY}(1as7-BT z(oV?ttU!B5c^iB}B?{OgjnD?3>3Fm~795A}?{3RX){5DDm{$ECbVI7AR>Cu2Az|X| zmP~H|w5O&t_4Z#UIp+~^!m`(HM8220)Y)Q4tK5_<1*vTJ8&!cAS7<-U2eAfoL<5rN zg8UwZjgm)F-*cx~aL53}-2^fKEXsV?FpGX$ZGI0AqXHuvT~yx}^7$&3GMd_A#1Zjk zDG0K@_uNqlmU2Fs{B_F7Npv{u`n#lGNa}NTHc6LHgyHOLNQLU_7I`=k%aQJUe|`_5 zH?5ww)x`mqWT8ikH&@iZi`>>ufZ&?DyLi1lg^jT=T;>VV3#Xt|Kk=xv z(Cl02RNLb#Fnq9~uB zBg!`ljnk#or4(r(u*TJuF{Bb{25GbXAz6n9A?Ik%BgbmIuyru$kj$J(NL?$QlVW7p zt)$sWx%NXmIa$R>Jl#@}iUbTiaUIvPZKngHL319dC6R(wifG#YIvo=~hopYo=gu{o z-G?$1UW%&e^12Wghnp9G;ykK;XJE62+V5^x% zrc=X}SuyU@mi4hg!SiI3Dy2hs-3k2jl0(;fo$p>y<5~AV<}ZV9IG-jk&1y5CF4$vs z6%1B>{cd(1Tlq5^)$)$x+V;-#8wKiA+ZjhLlA5YF<8uvga;a$(%|Yxscp#$Cit*#;a#!piOTQ|F@Lz$$30)Oin)xX zm2=@aR)CSQ>1URAM?YKlqhr4DtFPcXw*Sd6vG5ddIc@y;9AeXpOh9nXt(!tyY0*pO z5HpTzGvN_h7GDdw?CPi;2ORx(#hrhNR}J54*=HL_3gZ({ z+rSEcczFL>vsFL=z}`x>FmO=N5YVu%HCqrEIDiU)hEBpP1W8P)VBp~FkAlI(A}H?| z_oI?bSkbVv8w!&(zIyVUTtq2=LO(mFYHEW`R7}~(DPi;CpE54uhu1Q$3erG7&A5G; zLy+Zhlulf66}zi=*}$&_yk;^rAT8wV9levZul)I2cW;ukuR=HCXZ_ZpML{&>A;SwGCmK75?i?BT z{7x3)1KEBi|DDC59YM(6c4wuP!^rG-6_*P9cfc z)x7}T?p-MiL6d83%fG0H;lUcuSTNL77WC)9OdI}&`zAuK ziFPNnkh-Q~kj;`}W8Ph!p(8;YZI?|g|1HBcyocI-b0VecTTt|2REbT+1>4yp$EG>S z9T9PH0vt1%3EZc-ay;RCnrhT?Ej)`1YK<`A_91`OIQ69Xr8&m?c*P;UkQg7$&l&v& zcNY$34^rvYdFaPV`r9?J!}drUKA#ze%na{%Y~gk8053v-ceJ(Wju&38HC zSzIIR$_d3oIr3fbPvp=_E=KNjS)iH`e=onA5Y(O@w8^}WR}UxQS?t#-6fWW*v|uqd zsEN`%Vy}5EvxOD-Zq%a-ArIVB{mX=V%5N_~@ba54db*RK={3%L?yU|Y?M+;EE9O2& zpj8qtD_{Mdfq&hIX$z= zi##3^N&K+^a-sec|r;qKlK?h1Tp^#2H{d-v); zw&k-_v16*s-bx1>7t(<3q~)1S#TlOna^GI`saT z+Ref1&`psc6uo^qep)}{s$n)FN!K#FvWZKIanQ7_cWjE#$p)AX2|{WugCdCQDn6~a zgs7-T>-f7WN2P^g&GlTI4a+IHAAb-Kphc9m3W{?ng?0X6Q_fymS7DOpFRZ<{{C@Ov zTm0I?z7Alu}$;kQITz8h^!_sryGw(H^O ze)#ELvAFMdbT}CIIqSG@bqDy~NJ{1tUgJ!;S_3bD&DiwWAGXOvegbyF?k1Jx;e8Pc zc2e{6!mb|5Ykn`)tGwh7;Q&2$OYe&1Fy8uW^;3|doU~xtK#QBaI;@lHf<#r0BdLOjD6 zRnw2I>{0Uaw1_30aXpOT5TdjADrAY2zKW1mcCXupXB01Edz{Ni4x69sM}NG~=`V7Q z2DXeGGO&Y*&Qz;nB)WZKy8?f6y#PnOeOsT9-ywYNuDEB-aD4&Xinn`^5Z6yWM=IDD z%-URRNp-2K5IrW@Npx&6-R7OfA}8R*v)g-|az&9!R&@!wFUWRcXECJJeODbM4nvrN zMCvz;INLAjt+DSzwRO!vVq42qe=R6&KI_KL!@?Yay#PUkNMY)~f9`xh?e%IQ>6D{r zH*~?ks{_9jC|#Fq*Vux8PpKhyJXhe>A47G(NKiV1e|IlB$!1mg_6>dF?XCvi zx*NT5a2!fTM$_7OeLhI(BIT$-Muj)%7yEy-2s*ihlvu0U>!s|pG**8@m_?YC!+2438796G(E`Kp zcuWubyDRh5C@b&l%XhdP69&*iv0?mF;9cFwp|Ac|(hHE1lE^Um0&u>IKoRRK;bYir;3~zdwpD1*o=dk+-AFS2 z;BZZ(F!%zPb&0ada__1Kl5dJeaG0-r++3u33b7dU-FTnX74mp~zNhCTxVj#f`uC)6;56k-! z%l)D?(yene*oqaKYYA`GK($PHzsh+=x5@b-TVzbxbnOL0=yG2d=jOS(KPRo8cDhUd z$AkN?0vmlYn)Pp&BTK!_6R>p{f4r}K;6 z&HuS4N0Z3g-t!$cjQFbKKziBdzINXPwXq_qUsjr1tCpla8%j8Ru#CBmi6=`RIV_0K zq2hGtJ^1lXPD?*)i(^YxBWji)mDc5_(8q2p(~&NX72T$YvR|1=Vr9cjmPT85~m6dMCj;1e_ZH*@}^j>y1TN;+# z)6}&hr&3i=s?602ju(hU*FK%aaD|LTv%DTTG5BO%SPiB=V~gOFU>(&|DJr&k06*WZ zQ))RyZ-{{5vCX-^TSQ)S+hSde8|E`BQWu%SL8|WTN7?MheXR`5BC0JG(ug&iepZ*b zV*B1`hdFBVD?ZwhSz0X^wD_XRl;QRZfMz<|zgFB-$$S7dV1tD8{vwZWx!a;N-^;zl zA=7;GN?vVVy@T)&Z>#^@Vd7Ir=23mypD@D~+-|r=Yz*J7B%hMHQ#(O8vMzC-mNCby zr%gKTh8Cf#N_38Tr&%u)apNRI$Rt4yzS5Nab9*iKGiXVumV=T4rHCQ|_K5D>;Yo+8 z2J^`G2qsth8HX~y<+bCY+9)nWj1{8L4Y%AUWsTqLM2t!VQ3aRjLvg@A?heHINqB>& z(nr`MzE3|ufsp487^uh2?hGEr=Bc5YEag}!;|-r!rESEm;fpwglk!-ES| z>v90qS8;kPV~<}DR#o3NcXoN{6K~M-ZI}%CI^dQ-y6M~cN#P41P9G=iui}$>dfp$_ zD?wDnj0QR{kul}Q_EkT?w`6YzGg?yI(qGP;c&(D@B~Nr;3P4?n>kBv+rNsqYpW2$n=SWI(btgz)Rf}CIN{5+W5=tqtpeabdAl+1I=aqDC}WwO1N{!B#l zSaW$%WVK2Fq^@E%Z1Qx`yNe;u-EvB;)nUD0J zIKQZF$1{S;(JxqX0@f&FIBe2&+F&-xkO}Whc;cY7P-x$AO=o%_(>#;klG;!r<6;#t zP1%}!2m((4?(H3%EH;uQM?8#Otmfwk0F3=2MtMLhITK<y2=RASm^!@ol6-ef zM0w)Zj2IUK(y5$-hy(01Jzo_8BDUMqJ?4{Eo>K*Co)1=LIBz8ZuqH>d#3=6R-8;o$@!&EUP(ExEYo96!0is>+v3~MP5{Dm^$|R ze$v4H&~ELkS?Dtj8G6q>@jzwgmiQK7FiRX_t8U7H8EI&9&niL4Y0-Tnc0a34HL~JI zc<5a;D?0XnEgnlP%9=KfhQyEB8Y1c|mpGUHhp!AR=n&uZCViVno zU1?Qt7Sx#_uTT*_Ydhp7oB7_EsBOrKb~ku1^1}iR--sTZA;NLUY0-AM{nm-LD{75W z(cTEh&1%AE=Xupm`f``TZ91VVDbAxI@+yAo?fU!;6OdLuu+Zv!{%U)K%&0+Up-;nn-AVwkW(1>$8` z@l&Jh6Bkv9>@D@8n<5eUriRp#Mu4Jd19beW+S%9gzp#_e(RQ=axeA9a&<7oD(!hOsiuXnB&M)4B+5Ss1k>_cbUXJu$00Q|qlpEEe8np{T$~LCIp(d9@T7+DgG}#hvEbNkn;jl~FQ+oJq<(1kx6d`<_(;Tr zg@_-e&+=w8H5QW5n#~8E5eOiCk=#goZ@uz|ZnvJ3Y3sGHw^LAF$z@FI{Y_$$8K>U) z0Dyet_l6x0i9K#--e+r8lvO)`cK=P7pmuMq@h%C!>nc4AjCh~0eK!tVty)Lr*Z=0F z%sus+9a2I6!`z40>J}Xe2mpbDy>8|C*Or^t-8ip>Ed^o&2Y)6(`M4h_m6PWeo%$R9 zdi?MM$_tPfCY?fZ#ypOZNaRRFOdJ67uNTO~5cBYh`g0t@aUWs*Z$II9Xs=215veB} zKEHoU!EmG5cBs@G_gv@bPIq%4#Sxd|_4rv9@Ss^yeM{m?jNfo&_qBBk|A*@L7I@(_d4PoIO{k<=Z|{!%E6JXWAbbl zS3p6Rr`bVJuV5!`9S=(rVUXOw+r{{i$Vc@3x6z-BTVwn@DRgrA2xCjBFscLMt^{FB z7)WT7o;zKSTxFmeHl2D8noJTvp&73gNj#56uf{JDBo;mEipsf=-v$F|2u|#*d%XZT zL?vrods03ymo}qAD&;?eF;h`W=eyOxT!3Zbj11*_5^N51tYUOEMR61S*8HTO!}KsZ zs#X%2-ExLeGwFi}=x2g^*+P=`fzo!U;Z*rhk{}j4OVN2rb4j^Yolpy&07pr$5WZo- zATUc)D5T#=GBDYfxXA@8hEg@?$X)4RsfJi*cu^V-L2$O|jGnhdANKo?IL{XXaxp7d zKDAoHeX{0bh|TUrYcOCAt-Fw+{|(jnd8g!jY(0-RUh!e7_=}Qr|ATBGX#vlL<>%Wf zur^9l&cV(F!6STPMLOp&Eex7-zcN2*a}H4 zB_+2y0&c^eVGPuTU|1abX%5U~k6WpScHXga%Q`i~xAYrwY#ZK5TNKjIFs)b=*$|bV z9~EZ#m~;*e5l-=0@n^O7;Abqlubv})s(ta!e=bCDFlHspV7%v8V%q}O@92;rPog>T zYpwZ`uJuz{gPTK*M-ieZuf)LLMX6SUA~IA#nv{ky&tl7IM5hMPtA`}Ul3kYAn3Zo5GR6f>T5I4t z40X}r)#xJw6A4ZWsYMJX$*4?OkP%8O(^v>?u!TRmARMHti8DCXXXR2n1U|9AE~e=+ zS6pGkDov&J5P^Luk6%R*eFnsdvXat7%7LO~ihd!3g;nOv-~g$yRk-$PAfd=7v|F!nu3Zq_ z-VnP?jECKKam17a1K7Ju|J+s_qKgiG5!iOnPZajXQpooUu2Do3Hz1a}D9szq3BtSI z`eI*YhQU+2oJ*@KV%$Tv26raxK4hS18oX;duhov-Tj}NQxhf*4Ai23jFv96$j(mM% z5`Jtjsto5c2;nI>iIHp)?fu@k8qw%k=7|h?;~UM&Oo_Qc`@KnnQu7SIVmlKxd3ij@ zz>7(LWt4z8lE4++fkd(zJcRlsxHh049x#HnUny16pv6lHzwrxI<&SAcl%2jO)o?@; zxMcKEN2j0tB&qJZ)1VPxVb3Z2#Q8uwgxc6P~-{<$|W+jI|UbzAs?7q2fOHnNG)r8)i2LbP4??nADLDK z*j1yz#R$@lt7+BWk9EW4?iyD;%#>X5(&>V6L0TS>)9Swhy>&IE36tz_z>mJjgO z-sH33X^;`(c*IHSQzCA#qGd_Kb$wR1P=JN<(~$;Dgi|c-N#a4|rs$FI*9aF^}=T%dsZA+3}Mw8!)OkGGyhs3-8Td>p!Ey9hb#|KjZpfv<;2qz}|ZZE1#ix zq+0kDlFarz-L6j|?B{2z`u}7SIiNhNK+hdEB)0#;5d(EOqTLTINoB_hs)43(V$Jau z@8dmvbiY76ud8-*;(#~TdTbEvvL!RNB^L-s7=ftxc%mk%!iH1P^C@2s3Of+5cVXG6 zRZ^+5;i2F7is}c1CEI1z6q`b2yDn=*A{U3KlQahfA*P(Nwai37Xau}h)24y*cM)e) zvEg;ZO&el!ymXT@&hxc&{!Wcq)iMF_@jvry^Z)K(85(KUSbLuDVh`_Z?K2AM@ z{d$f{pt-!1M|Ue*rkHS5SP-;f%cx;y1G2=`tsh83Sz?t>IjT5hX4*dPZ?i5l@K2Mc zB1|`P)K0KJq_$>5t5F3T9B=}GGLfE9oWa0dX4mGmaCAw#iEdM9aPtTl9YglwU|Qa=26C)Xq?Q{nSld|j)ygHWT|=<-qQ)Pn zdZ&V|-k89>vtXrb1XzDXeo7;Ke}MGG6*qknQ>r67lEil)OH zOh2*Z@n;g*U&YK)787{QccBS33@kN4MzNg5S~VDB@kiRVh7?A`HGJCLBdeIlaBeDTr&#f8_m9` zV8W+T5W4H^wBTWpjmTPA$22Mq2#KVPyogKqvryXUdkeo-@X{YbU)Ll-McSXl+m{oRY8yEnea(KaFEB85X6wg%BhzAm4q^w;L z6v7vous$3Iznr*z%oejJ!qt-znf)So%0`4_P!qDImj!Y_mNpldWnQDkaQuyegf8m0 zA0!#Z@PmaRhm_Uk%eKguO$FmlZb75Z9zlZ`XcZiOOC`4SOyKdsqcXD>Qa08f1JOfGq4*J1-U$Wzi z0ha_iGowNER&-d6EX@1XtVM4AJLSC5>N9lIOQph0jOcx}{NJfxv%LJar6`2iaKlh> zt9NpLg!D?05Mbr6`(4|2;Y?%T{0^Mz2qS4){c>o1j%Guvy-+iXK>A6r3m&Og#6v8r zsRG_1y^6OCVW&o)j;fgziO^ghaa$Ag_oZT zmX^+K?7b&eHi3VK3m?B}S{DvawN{2o^n*J>K?j^6D`_I|Miov6LDsxi6hi ze&2c%xSK`3dPEO#=Tj%v3B_8#T3;1v#m315k);H1=HocSgJbjXW7*TyM*$eo02!E>9wG#F@jZ@o2y3AQ z)QaJ#gD`P{@64N6@7X~=Jg#YWGW0wN2FZ2HUF|NcmSgu4Oi~ zW^Cah53K6&}`)VB+rOl7#YkfC&mv6Wu39<`GV&hsSFq23S zIU!cC<^K7;vG}{k2gib;AVuNbW4t_=7Uj5x074joOjj?^NGwTMf=89ro??lhrZk}W zbTH^b5PlPU?P@*_Y-vYr|0HRkLG!ow=YE)t3OFovG;XB`YY;^ElCzvP!X~qdRWQ>3Y=PD?_EH)B#>;#IHq?io);Fh4fRrBl!R&o zK7X5a5sjzu$F^z~7T$*Gz~TeVYr_uOHB2Yv_~~~zs@GCa#8D3%km`=&ph;%z@?@WV zTs~a}voe-WIHhS>){y$4jzyH#e}zE}D>}HT{xQH$5ac)gjNm-RT_i<`U%XkG@X5C3 z4Ac7@92^zzy>vG9P)V^|orcI}bgZHwD;6_vFB}AkF&B~_LBq4rTu`LBhNg}b;a;L2 zngm-D-Jf0fE9*$Urg1ZAcV-}@mSYEIK=cy94G)eW6CpizueZcXaNb~o^r5M&y!=>Q zhJcEoD8Y)8=)~9RuKdK8{i%Gnz4meLx;_1Q?n-a*+3G=W^#8!JNb8CGU@DCf0t3}y z^gRB0L(lyB&osb5AZT!KFevc9HXpw3IEMfrQPD85h}q;(&;_9|NSK&OU$+*r>$3`F z{dHUaIsgWE0hDFcc%Qi@Z9my-rFGhxK{kHH+Wo&bLvogJmLfGuUNzH!-1+4@ z{>(PB>`(COP5d&+6)emk-@)4q(uX@*t+{45PRP$uvDf(XuOnq37_Y4I<_@cDIVaQN zk}upaqQK0{aFaKdTlTsL%Q^JBSo4&)u*aJnFE<^)ejc-<-f&S3U~(})RTjKI(n)hR zCn)ckdni0#r1Bs_*E}B15j^?KuxzLar`K$&tVChlH$g?-M`b)H_-qPecD;n}YSzHZ zvGY;LM85M2?zcE{=7+&ZAG4f)Cdy^m!yjyR$u>fut7z%wCh+UEY-?}X`L_>^Ln-3D zc0QjTd{$gP|0x@3VndqCeq^zu#4dsl{=z@?S~e^x)u@Y?Ntvz?%qC@r`(;5K-}v5K z1tM(JRSi`&iBa9+G!9HzRp}3<2(6OTtaWClsLKCn~;n1nIqq1f(~SCV~ir(3BF94u;-~s5B9kUInBR5Tr;4K~SU!ezCmwdH(O6@Bi)D zbIPcFtWNz}31x+f>Ygv~`lhxHnQ^s@A3tp2w9F$?ePG+TGL;V-K95E@?! zrv|8}s<)UV>R;gy6}d?L>G(;bDpHc_;5^jq`mA#+)Y(3){~q=9`mP^IS)Ulq`PtZQ zIr1WAY**R^$Aa{+JnQxJj-Z*k4eMa_3qjnO5DCcMBe&h!mzhf?MPrXoQOjrD^`v{2 zY?0oyw!mVKdLa`^dL!Cv|`HeYUm6M5J_csbYwEt9gZv@(l6cME|vaw`^5X z8+rS-$ySr;WAton0nO2#yF?PD%zwF{jvhs4G<#}!TW)NRiyu$qR`1ChVdcRTnXZL~ z-WMpp(4&=Q*b1mYtDRmDUAJRilIk{a#hRDf)`792(#a$>t)-L*%m9mUfenQG*|9=* zaa^q9I>T?)La=yOu9~a&eMGvf^f%-HrYO!^im}T+`)=(UcnV7#jQ@%sYu;07R)g~6 z^&;x;E|gjbc8g4VFwaa{FBzI%3|TFjt7~1ZVs^K(ep*UDXP7sr+xUZ7JM5SMbH{!V zX|8R3Tl^G9X_D@OzG=;DP1Cz{`f-jKEl46-FZ~VXoNjSWLdvyrp)hsfqB-7zadmD) zL`6Z&*1l2$G*WE&BlXc@`RhqlBH4Ql1I_Ix6JAWzf2%Bjly^B!SVX@NMa*Gc^6KzR z(qg<5jtJC8RM4W!3cZNjHQsK+sah=QrYRiV&Y3-?vvR`12+Da&u$*rN&1Q-XjpgI3i9;klV+D8q{G8=e_ z?9hGW*kIB5RYZVw)>*VGN3)`>F*5kljZ?M|k7<9BY1FOfhWqA`3D^BgehZrihUx5DR$C|_F(tV!|Xd}Gakx=9^0n(&+Q>C{C+=)-`dF62zqJE!#nU?r7H!(_K!e3fCN&MS|s?zj*bdqHW&(CwRXbwSLg>n>C*H7vh)Mhq-& zrN%;}W?ytrIWTPzv1h1CR~orV_g_e+Rm5&eO3H-7M@#bjCI$JiUQIy*JVr_NYkni= zRDt|hv8H5%>yQ_%8Mw*6Vm`=f>{r4*k+`Jy375I~dyWGs01gXDnir`U{;E)&kUFaP zp}*T?VvMwPKZ~-Twj{d8Biz6L`1}Rd47G^&CNB2N;#0_1qIUtAvv&!@^wc!q{eoM| z`%mWi3A6rHa110oByrWH_LJ0k>*y=N3ixSD>I;KAk?gv+k+xW9q<@J7q?0X-y^Qmj zc~pBvD~MiU6B~Jx$-yZsnFsC`rR~K@akQ$;zO(>TIzwGMDHY8P#@TG=*@6JHSp3TBu_kdt zu_uY^m9RtoB3~L#q22-Sm^+43_W5)y5g(jCzcz(@2M97@X$CrcP1h`{S4s^Vf;%aY zo0k)~sert?;x^_R>t^ydz7_G#YJ3Oq>&!?#K8_M%yiU3uz0akp@x|yAn_EAkV0!X) zwydDc=1BxL+?Q+0mGCi+kFdG@IpxNNz-Qa*&&wg7$;WI|VQ*8RN)DQ!EJ-B(50XNn z4h*OLC;x{C>U?1TxjN|Jf0VcR{)f4-c(6M#MGg{_!JBhT4%)*&1KZS=#PZ>8=YX0g zVM?fUbbk<$_Y;OaD(c)$Krl!Xu%!rTMNxKwIP?KLI7TQryAp_?#0Z7Q@NMDo9XzQ& zSwI!Cbo?>-ZzpHufzyGYA?fb`V~nC!BnC4)0UfOz`81Ju)K|0bQUlFUZdx_b0zEZH z4_-OUDxPt73#$aiwlht7x&aFXwm22J`T2Of3&0(UdkG5!nbCCx6S>CRi_?{4Z6Ln*$D^(sifuvV`5_Qo97|Q%l?lOpbcyc zkIn}40{1S9cFgC*H)(PnFP}}9#sJP!FJqjzGz)vY7M&xYIvWU z`GEp(7_{l}p0qyk#?O4-kSuvJlGzr5~)-~Wni3QX!Ex^(nTd(R*+z>e= zPKof$gV;m%EGKG&SyBeH#bZj+)U!@_9qX_zz3Q_?)|D(Gv3J^^Utk2a25jWAs!2R* z*w>&ws^;J45jDQ>t?9P5z`R^R{1#Ea>re7O{ld=Vd`>^Hvv+R6w?&hpv{sENLAy`-=kBG{8}!WP-(ZsP{TC+6 z+bL8I#R}%c?t6*>XH|=b*k=BpUCH_)tHT4sF?qKJN}|N_n-3>D%wC>yh@Jp?&Qrq=IEZji;VCv^wl0%Y3I?Yy@Q5*a8>qle%aUZ_J!lV z>-EZviFqPNpuOtH_8g0kf!0;`>4?e`fh>acQ%}518e`6~PkVLQ+_UrhEINKS&19V~ z|Nc5^Mo1<3iQi@R13n(+$B**1Jw}YvXRetHflegw5$Qu{(G<;z94{q1+u~eNu%rRICoPZF8G1Tq|}m7W3lWXUZ`;L=%Uj4T`&2?)f$994v;0#k;Xx zi1e^l)YDDe^-Aj~`%DmW%AZ>%R=47&81FVhDK9a#8H%h-^{}1G@)`U*-tLI@dPf1) zM=XBWlhIq`qLS>@$jx=F)#VSJ@EA~Ir>A-#?N|BZ74|1Tfqs${OD_35UfXFvJaZDm zmUa`(UL{abZpHP02;FoZJ@*tK#LZZEH9*K13zJM zge-+8R?7v{6D(7~Cr!1`kUj=Y`zI?TGn;SgUa_*f^uGA^?cAnInUQ1rQT`Uh>lqajYqeLc2VdlKDn`y=H)nZ63-Ob%ZRUZp9_eI0)> z@4Z0%(#}+X;cl807xHrHXp&%>QdfBTP?)T*Nc@`c?P#a#d_kv;gSHujNpgUa+Uv#6X8C@>s{)eOmf{95?ST)3&ap_s;@n5d?5KUQq=aDPGti@(!`1Gk?YWU zCGQDlR@|bwl#m99Qa?Fv!ThW#wu8V%_31RMWA#na)as*AT)YN9;F0({Q7_XKt3oZyU_k|~3tv`}waa9N?zjaOIsP}aOSUW{c##eGh9%^U~HZd?Er~)&S>(#g{oIy6FC56tBv9g!6uT~TaBqjObwge91`G%hUIVFYY{_^fhoAT9QV8Hq*0NNm7uu-mT`kYO?VY-&Qz+1Eud6f+SG**YPaD3k9{yoIp-kQ) z*GZj;M7&l;SDo={^g?;nQJLf{i&f!ZOOh*R-aQA43dD_Mw|>t_QI$O=g!eO}j6M9% z5J)yvw@TW=H~IF_vzc|m_uAq43ZWcy@O#aXJ&7zvK|zMCm0U?jfOWb>1EFRI)6lY><1X;w}R=a{9R?(PGvS| z{cjtlt3`T_xdX|5@@|B73Ru25*5Io_zu$iSEQt82#yJUlqk#vJt2UPucSG-W495(> z?6?Cc-Tq0RcO4Da#idY6jesoh1Nw1!IV>kC`NU$vSMwv9y1?_$r?gRsnl-NdC{5Ex=F|FfZMIuX1P|DS@52z zwRYE=9a9(6QDfSct`KGdm|xtcgSx2S{5t^6w79ocilTrNwh3to3aJIlm(IxpX!oWJF=_+odFL;1-6C_Gnd4ua&DB4>kwTR{Q@3Pyb(2jO8OD!3D* z3HMOa*UkpQ5ES_MW)4Ce{`6sz8h(zWA4j~r5Cz*(mBbdr(W3M}z_0}!ax?>Eit}=# zHZKgDBlVu-bv1t?;y{z&iyy!L9WcSgh;L%>DZMMJBC*^TXgl~mU>E$b`GzNJf$n?4 z%w|;OSWXGjegn5EHB#1O!;HI_)@I$UXqobGo$dwI!5ySiVq|^CY^C4yJ%Y<^cg>Na zU{v8~$9qtQR z310bt<#f}P-@jF>Agb0y|+{i_c=wAWq~fFl}BMn`X;4-=>8kF&}y?pO}JmNWLQ zpn-=yvoPa9Jk^%KHRk-dKSr22qncfinFSa2*?vD33yL{<1@z@QVRY)7I#vS|j6za1 OBtt3wBo7_Gll}+2jA70I literal 0 HcmV?d00001 diff --git a/docs/userguide/storagedriver/images/zfs_zpool.jpg b/docs/userguide/storagedriver/images/zfs_zpool.jpg new file mode 100644 index 0000000000000000000000000000000000000000..17ab2ace50d456aae6f506dbe933d937a379b846 GIT binary patch literal 30560 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

F7Ei6C$5KBHAQ2_SN!+* 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 O?UJOdic_-F>Hh$hF~lJN literal 0 HcmV?d00001 diff --git a/docs/userguide/storagedriver/imagesandcontainers.md b/docs/userguide/storagedriver/imagesandcontainers.md new file mode 100644 index 00000000..3f2594d3 --- /dev/null +++ b/docs/userguide/storagedriver/imagesandcontainers.md @@ -0,0 +1,495 @@ + + + +# Understand images, containers, and storage drivers + +To use storage drivers effectively, you must understand how Docker builds and +stores images. Then, you need an understanding of how these images are used by +containers. Finally, you'll need a short introduction to the technologies that +enable both images and container operations. + +## Images and layers + +Each Docker image references a list of read-only layers that represent +filesystem differences. Layers are stacked on top of each other to form a base +for a container's root filesystem. The diagram below shows the Ubuntu 15.04 +image comprising 4 stacked image layers. + +![](images/image-layers.jpg) + +The Docker storage driver is responsible for stacking these layers and +providing a single unified view. + +When you create a new container, you add a new, thin, writable layer on top of +the underlying stack. This layer is often called the "container layer". All +changes made to the running container - such as writing new files, modifying +existing files, and deleting files - are written to this thin writable +container layer. The diagram below shows a container based on the Ubuntu 15.04 +image. + +![](images/container-layers.jpg) + +### Content addressable storage + +Docker 1.10 introduced a new content addressable storage model. This is a +completely new way to address image and layer data on disk. Previously, image +and layer data was referenced and stored using a a randomly generated UUID. In +the new model this is replaced by a secure *content hash*. + +The new model improves security, provides a built-in way to avoid ID +collisions, and guarantees data integrity after pull, push, load, and save +operations. It also enables better sharing of layers by allowing many images to + freely share their layers even if they didn’t come from the same build. + +The diagram below shows an updated version of the previous diagram, +highlighting the changes implemented by Docker 1.10. + +![](images/container-layers-cas.jpg) + +As can be seen, all image layer IDs are cryptographic hashes, whereas the +container ID is still a randomly generated UUID. + +There are several things to note regarding the new model. These include: + +1. Migration of existing images +2. Image and layer filesystem structures + +Existing images, those created and pulled by earlier versions of Docker, need +to be migrated before they can be used with the new model. This migration +involves calculating new secure checksums and is performed automatically the +first time you start an updated Docker daemon. After the migration is complete, + all images and tags will have brand new secure IDs. + +Although the migration is automatic and transparent, it is computationally +intensive. This means it and can take time if you have lots of image data. +During this time your Docker daemon will not respond to other requests. + +A migration tool exists that allows you to migrate existing images to the new +format before upgrading your Docker daemon. This means that upgraded Docker +daemons do not need to perform the migration in-band, and therefore avoids any +associated downtime. It also provides a way to manually migrate existing images + so that they can be distributed to other Docker daemons in your environment +that are already running the latest versions of Docker. + +The migration tool is provided by Docker, Inc., and runs as a container. You +can download it from [https://github.com/docker/v1.10-migrator/releases](https://github.com/docker/v1.10-migrator/releases). + +While running the "migrator" image you need to expose your Docker host's data +directory to the container. If you are using the default Docker data path, the +command to run the container will look like this + + $ sudo docker run --rm -v /var/lib/docker:/var/lib/docker docker/v1.10-migrator + +If you use the `devicemapper` storage driver, you will need to include the +`--privileged` option so that the container has access to your storage devices. + +#### Migration example + +The following example shows the migration tool in use on a Docker host running +version 1.9.1 of the Docker daemon and the AUFS storage driver. The Docker host + is running on a **t2.micro** AWS EC2 instance with 1 vCPU, 1GB RAM, and a +single 8GB general purpose SSD EBS volume. The Docker data directory +(`/var/lib/docker`) was consuming 2GB of space. + + $ docker images + REPOSITORY TAG IMAGE ID CREATED SIZE + jenkins latest 285c9f0f9d3d 17 hours ago 708.5 MB + mysql latest d39c3fa09ced 8 days ago 360.3 MB + mongo latest a74137af4532 13 days ago 317.4 MB + postgres latest 9aae83d4127f 13 days ago 270.7 MB + redis latest 8bccd73928d9 2 weeks ago 151.3 MB + centos latest c8a648134623 4 weeks ago 196.6 MB + ubuntu 15.04 c8be1ac8145a 7 weeks ago 131.3 MB + + $ sudo du -hs /var/lib/docker + 2.0G /var/lib/docker + + $ time docker run --rm -v /var/lib/docker:/var/lib/docker docker/v1.10-migrator + Unable to find image 'docker/v1.10-migrator:latest' locally + latest: Pulling from docker/v1.10-migrator + ed1f33c5883d: Pull complete + b3ca410aa2c1: Pull complete + 2b9c6ed9099e: Pull complete + dce7e318b173: Pull complete + Digest: sha256:bd2b245d5d22dd94ec4a8417a9b81bb5e90b171031c6e216484db3fe300c2097 + Status: Downloaded newer image for docker/v1.10-migrator:latest + time="2016-01-27T12:31:06Z" level=debug msg="Assembling tar data for 01e70da302a553ba13485ad020a0d77dbb47575a31c4f48221137bb08f45878d from /var/lib/docker/aufs/diff/01e70da302a553ba13485ad020a0d77dbb47575a31c4f48221137bb08f45878d" + time="2016-01-27T12:31:06Z" level=debug msg="Assembling tar data for 07ac220aeeef9febf1ac16a9d1a4eff7ef3c8cbf5ed0be6b6f4c35952ed7920d from /var/lib/docker/aufs/diff/07ac220aeeef9febf1ac16a9d1a4eff7ef3c8cbf5ed0be6b6f4c35952ed7920d" + + time="2016-01-27T12:32:00Z" level=debug msg="layer dbacfa057b30b1feaf15937c28bd8ca0d6c634fc311ccc35bd8d56d017595d5b took 10.80 seconds" + + real 0m59.583s + user 0m0.046s + sys 0m0.008s + +The Unix `time` command prepends the `docker run` command to produce timings +for the operation. As can be seen, the overall time taken to migrate 7 images +comprising 2GB of disk space took approximately 1 minute. However, this +included the time taken to pull the `docker/v1.10-migrator` image +(approximately 3.5 seconds). The same operation on an m4.10xlarge EC2 instance +with 40 vCPUs, 160GB RAM and an 8GB provisioned IOPS EBS volume resulted in the + following improved timings: + + real 0m9.871s + user 0m0.094s + sys 0m0.021s + +This shows that the migration operation is affected by the hardware spec of the + machine performing the migration. + +## Container and layers + +The major difference between a container and an image is the top writable +layer. All writes to the container that add new or modify existing data are +stored in this writable layer. When the container is deleted the writable layer + is also deleted. The underlying image remains unchanged. + +Because each container has its own thin writable container layer, and all +changes are stored this container layer, this means that multiple containers +can share access to the same underlying image and yet have their own data +state. The diagram below shows multiple containers sharing the same Ubuntu +15.04 image. + +![](images/sharing-layers.jpg) + +The Docker storage driver is responsible for enabling and managing both the +image layers and the writable container layer. How a storage driver +accomplishes these can vary between drivers. Two key technologies behind Docker + image and container management are stackable image layers and copy-on-write +(CoW). + + +## The copy-on-write strategy + +Sharing is a good way to optimize resources. People do this instinctively in +daily life. For example, twins Jane and Joseph taking an Algebra class at +different times from different teachers can share the same exercise book by +passing it between each other. Now, suppose Jane gets an assignment to complete +the homework on page 11 in the book. At that point, Jane copies page 11, +completes the homework, and hands in her copy. The original exercise book is +unchanged and only Jane has a copy of the changed page 11. + +Copy-on-write is a similar strategy of sharing and copying. In this strategy, +system processes that need the same data share the same instance of that data +rather than having their own copy. At some point, if one process needs to +modify or write to the data, only then does the operating system make a copy of + the data for that process to use. Only the process that needs to write has +access to the data copy. All the other processes continue to use the original +data. + +Docker uses a copy-on-write technology with both images and containers. This +CoW strategy optimizes both image disk space usage and the performance of +container start times. The next sections look at how copy-on-write is leveraged + with images and containers through sharing and copying. + +### Sharing promotes smaller images + +This section looks at image layers and copy-on-write technology. All image and + container layers exist inside the Docker host's *local storage area* and are +managed by the storage driver. On Linux-based Docker hosts this is usually +located under `/var/lib/docker/`. + +The Docker client reports on image layers when instructed to pull and push +images with `docker pull` and `docker push`. The command below pulls the +`ubuntu:15.04` Docker image from Docker Hub. + + $ docker pull ubuntu:15.04 + 15.04: Pulling from library/ubuntu + 1ba8ac955b97: Pull complete + f157c4e5ede7: Pull complete + 0b7e98f84c4c: Pull complete + a3ed95caeb02: Pull complete + Digest: sha256:5e279a9df07990286cce22e1b0f5b0490629ca6d187698746ae5e28e604a640e + Status: Downloaded newer image for ubuntu:15.04 + +From the output, you'll see that the command actually pulls 4 image layers. +Each of the above lines lists an image layer and its UUID or cryptographic +hash. The combination of these four layers makes up the `ubuntu:15.04` Docker +image. + +Each of these layers is stored in its own directory inside the Docker host's +local storage are. + +Versions of Docker prior to 1.10 stored each layer in a directory with the same + name as the image layer ID. However, this is not the case for images pulled +with Docker version 1.10 and later. For example, the command below shows an +image being pulled from Docker Hub, followed by a directory listing on a host +running version 1.9.1 of the Docker Engine. + + $ docker pull ubuntu:15.04 + 15.04: Pulling from library/ubuntu + 47984b517ca9: Pull complete + df6e891a3ea9: Pull complete + e65155041eed: Pull complete + c8be1ac8145a: Pull complete + Digest: sha256:5e279a9df07990286cce22e1b0f5b0490629ca6d187698746ae5e28e604a640e + Status: Downloaded newer image for ubuntu:15.04 + + $ ls /var/lib/docker/aufs/layers + 47984b517ca9ca0312aced5c9698753ffa964c2015f2a5f18e5efa9848cf30e2 + c8be1ac8145a6e59a55667f573883749ad66eaeef92b4df17e5ea1260e2d7356 + df6e891a3ea9cdce2a388a2cf1b1711629557454fd120abd5be6d32329a0e0ac + e65155041eed7ec58dea78d90286048055ca75d41ea893c7246e794389ecf203 + +Notice how the four directories match up with the layer IDs of the downloaded +image. Now compare this with the same operations performed on a host running +version 1.10 of the Docker Engine. + + $ docker pull ubuntu:15.04 + 15.04: Pulling from library/ubuntu + 1ba8ac955b97: Pull complete + f157c4e5ede7: Pull complete + 0b7e98f84c4c: Pull complete + a3ed95caeb02: Pull complete + Digest: sha256:5e279a9df07990286cce22e1b0f5b0490629ca6d187698746ae5e28e604a640e + Status: Downloaded newer image for ubuntu:15.04 + + $ ls /var/lib/docker/aufs/layers/ + 1d6674ff835b10f76e354806e16b950f91a191d3b471236609ab13a930275e24 + 5dbb0cbe0148cf447b9464a358c1587be586058d9a4c9ce079320265e2bb94e7 + bef7199f2ed8e86fa4ada1309cfad3089e0542fec8894690529e4c04a7ca2d73 + ebf814eccfe98f2704660ca1d844e4348db3b5ccc637eb905d4818fbfb00a06a + +See how the four directories do not match up with the image layer IDs pulled in + the previous step. + +Despite the differences between image management before and after version 1.10, +all versions of Docker still allow images to share layers. For example, If you +`pull` an image that shares some of the same image layers as an image that has +already been pulled, the Docker daemon recognizes this, and only pulls the +layers it doesn't already have stored locally. After the second pull, the two +images will share any common image layers. + +You can illustrate this now for yourself. Starting with the `ubuntu:15.04` +image that you just pulled, make a change to it, and build a new image based on + the change. One way to do this is using a `Dockerfile` and the `docker build` +command. + +1. In an empty directory, create a simple `Dockerfile` that starts with the + ubuntu:15.04 image. + + FROM ubuntu:15.04 + +2. Add a new file called "newfile" in the image's `/tmp` directory with the + text "Hello world" in it. + + When you are done, the `Dockerfile` contains two lines: + + FROM ubuntu:15.04 + + RUN echo "Hello world" > /tmp/newfile + +3. Save and close the file. + +4. From a terminal in the same folder as your `Dockerfile`, run the following + command: + + $ docker build -t changed-ubuntu . + Sending build context to Docker daemon 2.048 kB + Step 1 : FROM ubuntu:15.04 + ---> 3f7bcee56709 + Step 2 : RUN echo "Hello world" > /tmp/newfile + ---> Running in d14acd6fad4e + ---> 94e6b7d2c720 + Removing intermediate container d14acd6fad4e + Successfully built 94e6b7d2c720 + + > **Note:** The period (.) at the end of the above command is important. It + > tells the `docker build` command to use the current working directory as + > its build context. + + The output above shows a new image with image ID `94e6b7d2c720`. + +5. Run the `docker images` command to verify the new `changed-ubuntu` image is + in the Docker host's local storage area. + + REPOSITORY TAG IMAGE ID CREATED SIZE + changed-ubuntu latest 03b964f68d06 33 seconds ago 131.4 MB + ubuntu 15.04 013f3d01d247 6 weeks ago 131.3 MB + +6. Run the `docker history` command to see which image layers were used to + create the new `changed-ubuntu` image. + + $ docker history changed-ubuntu + IMAGE CREATED CREATED BY SIZE COMMENT + 94e6b7d2c720 2 minutes ago /bin/sh -c echo "Hello world" > /tmp/newfile 12 B + 3f7bcee56709 6 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B + 6 weeks ago /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/ 1.879 kB + 6 weeks ago /bin/sh -c echo '#!/bin/sh' > /usr/sbin/polic 701 B + 6 weeks ago /bin/sh -c #(nop) ADD file:8e4943cd86e9b2ca13 131.3 MB + + The `docker history` output shows the new `94e6b7d2c720` image layer at the + top. You know that this is the new image layer added because it was created + by the `echo "Hello world" > /tmp/newfile` command in your `Dockerfile`. + The 4 image layers below it are the exact same image layers + that make up the `ubuntu:15.04` image. + +> **Note:** Under the content addressable storage model introduced with Docker +> 1.10, image history data is no longer stored in a config file with each image +> layer. It is now stored as a string of text in a single config file that +> relates to the overall image. This can result in some image layers showing as +> "missing" in the output of the `docker history` command. This is normal +> behaviour and can be ignored. +> +> You may hear images like these referred to as *flat images*. + +Notice the new `changed-ubuntu` image does not have its own copies of every +layer. As can be seen in the diagram below, the new image is sharing its four +underlying layers with the `ubuntu:15.04` image. + +![](images/saving-space.jpg) + +The `docker history` command also shows the size of each image layer. As you +can see, the `94e6b7d2c720` layer is only consuming 12 Bytes of disk space. +This means that the `changed-ubuntu` image we just created is only consuming an + additional 12 Bytes of disk space on the Docker host - all layers below the +`94e6b7d2c720` layer already exist on the Docker host and are shared by other +images. + +This sharing of image layers is what makes Docker images and containers so +space efficient. + +### Copying makes containers efficient + +You learned earlier that a container is a Docker image with a thin writable, +container layer added. The diagram below shows the layers of a container based +on the `ubuntu:15.04` image: + +![](images/container-layers-cas.jpg) + +All writes made to a container are stored in the thin writable container layer. + The other layers are read-only (RO) image layers and can't be changed. This +means that multiple containers can safely share a single underlying image. The +diagram below shows multiple containers sharing a single copy of the +`ubuntu:15.04` image. Each container has its own thin RW layer, but they all +share a single instance of the ubuntu:15.04 image: + +![](images/sharing-layers.jpg) + +When an existing file in a container is modified, Docker uses the storage +driver to perform a copy-on-write operation. The specifics of operation depends + on the storage driver. For the AUFS and OverlayFS storage drivers, the +copy-on-write operation is pretty much as follows: + +* Search through the image layers for the file to update. The process starts +at the top, newest layer and works down to the base layer one layer at a +time. +* Perform a "copy-up" operation on the first copy of the file that is found. A + "copy up" copies the file up to the container's own thin writable layer. +* Modify the *copy of the file* in container's thin writable layer. + +Btrfs, ZFS, and other drivers handle the copy-on-write differently. You can +read more about the methods of these drivers later in their detailed +descriptions. + +Containers that write a lot of data will consume more space than containers +that do not. This is because most write operations consume new space in the +container's thin writable top layer. If your container needs to write a lot of +data, you should consider using a data volume. + +A copy-up operation can incur a noticeable performance overhead. This overhead +is different depending on which storage driver is in use. However, large files, + lots of layers, and deep directory trees can make the impact more noticeable. +Fortunately, the operation only occurs the first time any particular file is +modified. Subsequent modifications to the same file do not cause a copy-up +operation and can operate directly on the file's existing copy already present +in the container layer. + +Let's see what happens if we spin up 5 containers based on our `changed-ubuntu` + image we built earlier: + +1. From a terminal on your Docker host, run the following `docker run` command +5 times. + + $ docker run -dit changed-ubuntu bash + 75bab0d54f3cf193cfdc3a86483466363f442fba30859f7dcd1b816b6ede82d4 + $ docker run -dit changed-ubuntu bash + 9280e777d109e2eb4b13ab211553516124a3d4d4280a0edfc7abf75c59024d47 + $ docker run -dit changed-ubuntu bash + a651680bd6c2ef64902e154eeb8a064b85c9abf08ac46f922ad8dfc11bb5cd8a + $ docker run -dit changed-ubuntu bash + 8eb24b3b2d246f225b24f2fca39625aaad71689c392a7b552b78baf264647373 + $ docker run -dit changed-ubuntu bash + 0ad25d06bdf6fca0dedc38301b2aff7478b3e1ce3d1acd676573bba57cb1cfef + + This launches 5 containers based on the `changed-ubuntu` image. As each +container is created, Docker adds a writable layer and assigns it a random +UUID. This is the value returned from the `docker run` command. + +2. Run the `docker ps` command to verify the 5 containers are running. + + $ docker ps + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 0ad25d06bdf6 changed-ubuntu "bash" About a minute ago Up About a minute stoic_ptolemy + 8eb24b3b2d24 changed-ubuntu "bash" About a minute ago Up About a minute pensive_bartik + a651680bd6c2 changed-ubuntu "bash" 2 minutes ago Up 2 minutes hopeful_turing + 9280e777d109 changed-ubuntu "bash" 2 minutes ago Up 2 minutes backstabbing_mahavira + 75bab0d54f3c changed-ubuntu "bash" 2 minutes ago Up 2 minutes boring_pasteur + + The output above shows 5 running containers, all sharing the +`changed-ubuntu` image. Each `CONTAINER ID` is derived from the UUID when +creating each container. + +3. List the contents of the local storage area. + + $ sudo ls /var/lib/docker/containers + 0ad25d06bdf6fca0dedc38301b2aff7478b3e1ce3d1acd676573bba57cb1cfef + 9280e777d109e2eb4b13ab211553516124a3d4d4280a0edfc7abf75c59024d47 + 75bab0d54f3cf193cfdc3a86483466363f442fba30859f7dcd1b816b6ede82d4 + a651680bd6c2ef64902e154eeb8a064b85c9abf08ac46f922ad8dfc11bb5cd8a + 8eb24b3b2d246f225b24f2fca39625aaad71689c392a7b552b78baf264647373 + +Docker's copy-on-write strategy not only reduces the amount of space consumed +by containers, it also reduces the time required to start a container. At start + time, Docker only has to create the thin writable layer for each container. +The diagram below shows these 5 containers sharing a single read-only (RO) +copy of the `changed-ubuntu` image. + +![](images/shared-uuid.jpg) + +If Docker had to make an entire copy of the underlying image stack each time it +started a new container, container start times and disk space used would be +significantly increased. + +## Data volumes and the storage driver + +When a container is deleted, any data written to the container that is not +stored in a *data volume* is deleted along with the container. + +A data volume is a directory or file in the Docker host's filesystem that is +mounted directly into a container. Data volumes are not controlled by the +storage driver. Reads and writes to data volumes bypass the storage driver and +operate at native host speeds. You can mount any number of data volumes into a +container. Multiple containers can also share one or more data volumes. + +The diagram below shows a single Docker host running two containers. Each +container exists inside of its own address space within the Docker host's local + storage area (`/var/lib/docker/...`). There is also a single shared data +volume located at `/data` on the Docker host. This is mounted directly into +both containers. + +![](images/shared-volume.jpg) + +Data volumes reside outside of the local storage area on the Docker host, +further reinforcing their independence from the storage driver's control. When +a container is deleted, any data stored in data volumes persists on the Docker +host. + +For detailed information about data volumes +[Managing data in containers](https://docs.docker.com/userguide/dockervolumes/). + +## Related information + +* [Select a storage driver](selectadriver.md) +* [AUFS storage driver in practice](aufs-driver.md) +* [Btrfs storage driver in practice](btrfs-driver.md) +* [Device Mapper storage driver in practice](device-mapper-driver.md) diff --git a/docs/userguide/storagedriver/index.md b/docs/userguide/storagedriver/index.md new file mode 100644 index 00000000..60d1255d --- /dev/null +++ b/docs/userguide/storagedriver/index.md @@ -0,0 +1,38 @@ + + + +# Docker storage drivers + +Docker relies on driver technology to manage the storage and interactions associated with images and the containers that run them. This section contains the following pages: + +* [Understand images, containers, and storage drivers](imagesandcontainers.md) +* [Select a storage driver](selectadriver.md) +* [AUFS storage driver in practice](aufs-driver.md) +* [Btrfs storage driver in practice](btrfs-driver.md) +* [Device Mapper storage driver in practice](device-mapper-driver.md) +* [OverlayFS in practice](overlayfs-driver.md) +* [ZFS storage in practice](zfs-driver.md) + +If you are new to Docker containers make sure you read ["Understand images, containers, and storage drivers"](imagesandcontainers.md) first. It explains key concepts and technologies that can help you when working with storage drivers. + +### Acknowledgment + +The Docker storage driver material was created in large part by our guest author +Nigel Poulton with a bit of help from Docker's own Jérôme Petazzoni. In his +spare time Nigel creates [IT training +videos](http://www.pluralsight.com/author/nigel-poulton), co-hosts the weekly +[In Tech We Trust podcast](http://intechwetrustpodcast.com/), and lives it large +on [Twitter](https://twitter.com/nigelpoulton). + + +  diff --git a/docs/userguide/storagedriver/overlayfs-driver.md b/docs/userguide/storagedriver/overlayfs-driver.md new file mode 100644 index 00000000..70aa0167 --- /dev/null +++ b/docs/userguide/storagedriver/overlayfs-driver.md @@ -0,0 +1,299 @@ + + +# Docker and OverlayFS in practice + +OverlayFS is a modern *union filesystem* that is similar to AUFS. In comparison + to AUFS, OverlayFS: + +* has a simpler design +* has been in the mainline Linux kernel since version 3.18 +* is potentially faster + +As a result, OverlayFS is rapidly gaining popularity in the Docker community +and is seen by many as a natural successor to AUFS. As promising as OverlayFS +is, it is still relatively young. Therefore caution should be taken before +using it in production Docker environments. + +Docker's `overlay` storage driver leverages several OverlayFS features to build + and manage the on-disk structures of images and containers. + +>**Note**: Since it was merged into the mainline kernel, the OverlayFS *kernel +>module* was renamed from "overlayfs" to "overlay". As a result you may see the +> two terms used interchangeably in some documentation. However, this document +> uses "OverlayFS" to refer to the overall filesystem, and `overlay` to refer +> to Docker's storage-driver. + +## Image layering and sharing with OverlayFS + +OverlayFS takes two directories on a single Linux host, layers one on top of +the other, and provides a single unified view. These directories are often +referred to as *layers* and the technology used to layer them is known as a +*union mount*. The OverlayFS terminology is "lowerdir" for the bottom layer and + "upperdir" for the top layer. The unified view is exposed through its own +directory called "merged". + +The diagram below shows how a Docker image and a Docker container are layered. +The image layer is the "lowerdir" and the container layer is the "upperdir". +The unified view is exposed through a directory called "merged" which is +effectively the containers mount point. The diagram shows how Docker constructs + map to OverlayFS constructs. + +![](images/overlay_constructs.jpg) + +Notice how the image layer and container layer can contain the same files. When + this happens, the files in the container layer ("upperdir") are dominant and +obscure the existence of the same files in the image layer ("lowerdir"). The +container mount ("merged") presents the unified view. + +OverlayFS only works with two layers. This means that multi-layered images +cannot be implemented as multiple OverlayFS layers. Instead, each image layer +is implemented as its own directory under `/var/lib/docker/overlay`. +Hard links are then used as a space-efficient way to reference data shared with + lower layers. As of Docker 1.10, image layer IDs no longer correspond to +directory names in `/var/lib/docker/` + +To create a container, the `overlay` driver combines the directory representing + the image's top layer plus a new directory for the container. The image's top +layer is the "lowerdir" in the overlay and read-only. The new directory for the + container is the "upperdir" and is writable. + +## Example: Image and container on-disk constructs + +The following `docker pull` command shows a Docker host with downloading a +Docker image comprising four layers. + + $ sudo docker pull ubuntu + Using default tag: latest + latest: Pulling from library/ubuntu + 8387d9ff0016: Pull complete + 3b52deaaf0ed: Pull complete + 4bd501fad6de: Pull complete + a3ed95caeb02: Pull complete + Digest: sha256:457b05828bdb5dcc044d93d042863fba3f2158ae249a6db5ae3934307c757c54 + Status: Downloaded newer image for ubuntu:latest + +Each image layer has it's own directory under `/var/lib/docker/overlay/`. This +is where the contents of each image layer are stored. + +The output of the command below shows the four directories that store the +contents of each image layer just pulled. However, as can be seen, the image +layer IDs do not match the directory names in `/var/lib/docker/overlay`. This +is normal behavior in Docker 1.10 and later. + + $ ls -l /var/lib/docker/overlay/ + total 24 + drwx------ 3 root root 4096 Oct 28 11:02 1d073211c498fd5022699b46a936b4e4bdacb04f637ad64d3475f558783f5c3e + drwx------ 3 root root 4096 Oct 28 11:02 5a4526e952f0aa24f3fcc1b6971f7744eb5465d572a48d47c492cb6bbf9cbcda + drwx------ 5 root root 4096 Oct 28 11:06 99fcaefe76ef1aa4077b90a413af57fd17d19dce4e50d7964a273aae67055235 + drwx------ 3 root root 4096 Oct 28 11:01 c63fb41c2213f511f12f294dd729b9903a64d88f098c20d2350905ac1fdbcbba + +The image layer directories contain the files unique to that layer as well as +hard links to the data that is shared with lower layers. This allows for +efficient use of disk space. + +Containers also exist on-disk in the Docker host's filesystem under +`/var/lib/docker/overlay/`. If you inspect the directory relating to a running +container using the `ls -l` command, you find the following file and +directories. + + $ ls -l /var/lib/docker/overlay/ + total 16 + -rw-r--r-- 1 root root 64 Oct 28 11:06 lower-id + drwxr-xr-x 1 root root 4096 Oct 28 11:06 merged + drwxr-xr-x 4 root root 4096 Oct 28 11:06 upper + drwx------ 3 root root 4096 Oct 28 11:06 work + +These four filesystem objects are all artifacts of OverlayFS. The "lower-id" +file contains the ID of the top layer of the image the container is based on. +This is used by OverlayFS as the "lowerdir". + + $ cat /var/lib/docker/overlay/73de7176c223a6c82fd46c48c5f152f2c8a7e49ecb795a7197c3bb795c4d879e/lower-id + 1d073211c498fd5022699b46a936b4e4bdacb04f637ad64d3475f558783f5c3e + +The "upper" directory is the containers read-write layer. Any changes made to +the container are written to this directory. + +The "merged" directory is effectively the containers mount point. This is where + the unified view of the image ("lowerdir") and container ("upperdir") is +exposed. Any changes written to the container are immediately reflected in this + directory. + +The "work" directory is required for OverlayFS to function. It is used for +things such as *copy_up* operations. + +You can verify all of these constructs from the output of the `mount` command. +(Ellipses and line breaks are used in the output below to enhance readability.) + + $ mount | grep overlay + overlay on /var/lib/docker/overlay/73de7176c223.../merged + type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay/1d073211c498.../root, + upperdir=/var/lib/docker/overlay/73de7176c223.../upper, + workdir=/var/lib/docker/overlay/73de7176c223.../work) + +The output reflects that the overlay is mounted as read-write ("rw"). + +## Container reads and writes with overlay + +Consider three scenarios where a container opens a file for read access with +overlay. + +- **The file does not exist in the container layer**. If a container opens a +file for read access and the file does not already exist in the container +("upperdir") it is read from the image ("lowerdir"). This should incur very +little performance overhead. + +- **The file only exists in the container layer**. If a container opens a file +for read access and the file exists in the container ("upperdir") and not in +the image ("lowerdir"), it is read directly from the container. + +- **The file exists in the container layer and the image layer**. If a +container opens a file for read access and the file exists in the image layer +and the container layer, the file's version in the container layer is read. +This is because files in the container layer ("upperdir") obscure files with +the same name in the image layer ("lowerdir"). + +Consider some scenarios where files in a container are modified. + +- **Writing to a file for the first time**. The first time a container writes +to an existing file, that file does not exist in the container ("upperdir"). +The `overlay` driver performs a *copy_up* operation to copy the file from the +image ("lowerdir") to the container ("upperdir"). The container then writes the + changes to the new copy of the file in the container layer. + + However, OverlayFS works at the file level not the block level. This means +that all OverlayFS copy-up operations copy entire files, even if the file is +very large and only a small part of it is being modified. This can have a +noticeable impact on container write performance. However, two things are +worth noting: + + * The copy_up operation only occurs the first time any given file is +written to. Subsequent writes to the same file will operate against the copy of + the file already copied up to the container. + + * OverlayFS only works with two layers. This means that performance should +be better than AUFS which can suffer noticeable latencies when searching for +files in images with many layers. + +- **Deleting files and directories**. When files are deleted within a container + a *whiteout* file is created in the containers "upperdir". The version of the +file in the image layer ("lowerdir") is not deleted. However, the whiteout file + in the container obscures it. + + Deleting a directory in a container results in *opaque directory* being +created in the "upperdir". This has the same effect as a whiteout file and +effectively masks the existence of the directory in the image's "lowerdir". + +## Configure Docker with the overlay storage driver + +To configure Docker to use the overlay storage driver your Docker host must be +running version 3.18 of the Linux kernel (preferably newer) with the overlay +kernel module loaded. OverlayFS can operate on top of most supported Linux +filesystems. However, ext4 is currently recommended for use in production +environments. + +The following procedure shows you how to configure your Docker host to use +OverlayFS. The procedure assumes that the Docker daemon is in a stopped state. + +> **Caution:** If you have already run the Docker daemon on your Docker host +> and have images you want to keep, `push` them Docker Hub or your private +> Docker Trusted Registry before attempting this procedure. + +1. If it is running, stop the Docker `daemon`. + +2. Verify your kernel version and that the overlay kernel module is loaded. + + $ uname -r + 3.19.0-21-generic + + $ lsmod | grep overlay + overlay + +3. Start the Docker daemon with the `overlay` storage driver. + + $ docker daemon --storage-driver=overlay & + [1] 29403 + root@ip-10-0-0-174:/home/ubuntu# INFO[0000] Listening for HTTP on unix (/var/run/docker.sock) + INFO[0000] Option DefaultDriver: bridge + INFO[0000] Option DefaultNetwork: bridge + + + Alternatively, you can force the Docker daemon to automatically start with + the `overlay` driver by editing the Docker config file and adding the + `--storage-driver=overlay` flag to the `DOCKER_OPTS` line. Once this option + is set you can start the daemon using normal startup scripts without having + to manually pass in the `--storage-driver` flag. + +4. Verify that the daemon is using the `overlay` storage driver + + $ docker info + Containers: 0 + Images: 0 + Storage Driver: overlay + Backing Filesystem: extfs + + + Notice that the *Backing filesystem* in the output above is showing as +`extfs`. Multiple backing filesystems are supported but `extfs` (ext4) is +recommended for production use cases. + +Your Docker host is now using the `overlay` storage driver. If you run the +`mount` command, you'll find Docker has automatically created the `overlay` +mount with the required "lowerdir", "upperdir", "merged" and "workdir" +constructs. + +## OverlayFS and Docker Performance + +As a general rule, the `overlay` driver should be fast. Almost certainly faster + than `aufs` and `devicemapper`. In certain circumstances it may also be faster + than `btrfs`. That said, there are a few things to be aware of relative to the + performance of Docker using the `overlay` storage driver. + +- **Page Caching**. OverlayFS supports page cache sharing. This means multiple +containers accessing the same file can share a single page cache entry (or +entries). This makes the `overlay` driver efficient with memory and a good +option for PaaS and other high density use cases. + +- **copy_up**. As with AUFS, OverlayFS has to perform copy-up operations any +time a container writes to a file for the first time. This can insert latency +into the write operation — especially if the file being copied up is +large. However, once the file has been copied up, all subsequent writes to that + file occur without the need for further copy-up operations. + + The OverlayFS copy_up operation should be faster than the same operation +with AUFS. This is because AUFS supports more layers than OverlayFS and it is +possible to incur far larger latencies if searching through many AUFS layers. + +- **RPMs and Yum**. OverlayFS only implements a subset of the POSIX standards. +This can result in certain OverlayFS operations breaking POSIX standards. One +such operation is the *copy-up* operation. Therefore, using `yum` inside of a +container on a Docker host using the `overlay` storage driver is unlikely to +work without implementing workarounds. + +- **Inode limits**. Use of the `overlay` storage driver can cause excessive +inode consumption. This is especially so as the number of images and containers + on the Docker host grows. A Docker host with a large number of images and lots + of started and stopped containers can quickly run out of inodes. + +Unfortunately you can only specify the number of inodes in a filesystem at the +time of creation. For this reason, you may wish to consider putting +`/var/lib/docker` on a separate device with its own filesystem, or manually +specifying the number of inodes when creating the filesystem. + +The following generic performance best practices also apply to OverlayFS. + +- **Solid State Devices (SSD)**. For best performance it is always a good idea +to use fast storage media such as solid state devices (SSD). + +- **Use Data Volumes**. Data volumes provide the best and most predictable +performance. This is because they bypass the storage driver and do not incur +any of the potential overheads introduced by thin provisioning and +copy-on-write. For this reason, you should place heavy write workloads on data +volumes. diff --git a/docs/userguide/storagedriver/selectadriver.md b/docs/userguide/storagedriver/selectadriver.md new file mode 100644 index 00000000..a741a137 --- /dev/null +++ b/docs/userguide/storagedriver/selectadriver.md @@ -0,0 +1,206 @@ + + +# Select a storage driver + +This page describes Docker's storage driver feature. It lists the storage +driver's that Docker supports and the basic commands associated with managing +them. Finally, this page provides guidance on choosing a storage driver. + +The material on this page is intended for readers who already have an +[understanding of the storage driver technology](imagesandcontainers.md). + +## A pluggable storage driver architecture + +Docker has a pluggable storage driver architecture. This gives you the +flexibility to "plug in" the storage driver that is best for your environment +and use-case. Each Docker storage driver is based on a Linux filesystem or +volume manager. Further, each storage driver is free to implement the +management of image layers and the container layer in its own unique way. This +means some storage drivers perform better than others in different +circumstances. + +Once you decide which driver is best, you set this driver on the Docker daemon +at start time. As a result, the Docker daemon can only run one storage driver, +and all containers created by that daemon instance use the same storage driver. + The table below shows the supported storage driver technologies and their +driver names: + +|Technology |Storage driver name | +|--------------|---------------------| +|OverlayFS |`overlay` | +|AUFS |`aufs` | +|Btrfs |`btrfs` | +|Device Mapper |`devicemapper` | +|VFS* |`vfs` | +|ZFS |`zfs` | + +To find out which storage driver is set on the daemon , you use the +`docker info` command: + + $ docker info + Containers: 0 + Images: 0 + Storage Driver: overlay + Backing Filesystem: extfs + Execution Driver: native-0.2 + Logging Driver: json-file + Kernel Version: 3.19.0-15-generic + Operating System: Ubuntu 15.04 + ... output truncated ... + +The `info` subcommand reveals that the Docker daemon is using the `overlay` +storage driver with a `Backing Filesystem` value of `extfs`. The `extfs` value +means that the `overlay` storage driver is operating on top of an existing +(ext) filesystem. The backing filesystem refers to the filesystem that was used + to create the Docker host's local storage area under `/var/lib/docker`. + +Which storage driver you use, in part, depends on the backing filesystem you +plan to use for your Docker host's local storage area. Some storage drivers can + operate on top of different backing filesystems. However, other storage +drivers require the backing filesystem to be the same as the storage driver. +For example, the `btrfs` storage driver on a Btrfs backing filesystem. The +following table lists each storage driver and whether it must match the host's +backing file system: + +|Storage driver |Must match backing filesystem |Incompatible with | +|---------------|------------------------------|--------------------| +|`overlay` |No |`btrfs` `aufs` `zfs`| +|`aufs` |No |`btrfs` `aufs` | +|`btrfs` |Yes | N/A | +|`devicemapper` |No | N/A | +|`vfs` |No | N/A | +|`zfs` |Yes | N/A | + + +> **Note** +> Incompatible with means some storage drivers can not run over certain backing +> filesystem. + +You can set the storage driver by passing the `--storage-driver=` option +to the `docker daemon` command line, or by setting the option on the +`DOCKER_OPTS` line in the `/etc/default/docker` file. + +The following command shows how to start the Docker daemon with the +`devicemapper` storage driver using the `docker daemon` command: + + $ docker daemon --storage-driver=devicemapper & + + $ docker info + Containers: 0 + Images: 0 + Storage Driver: devicemapper + Pool Name: docker-252:0-147544-pool + Pool Blocksize: 65.54 kB + Backing Filesystem: extfs + Data file: /dev/loop0 + Metadata file: /dev/loop1 + Data Space Used: 1.821 GB + Data Space Total: 107.4 GB + Data Space Available: 3.174 GB + Metadata Space Used: 1.479 MB + Metadata Space Total: 2.147 GB + Metadata Space Available: 2.146 GB + Udev Sync Supported: true + Deferred Removal Enabled: false + Data loop file: /var/lib/docker/devicemapper/devicemapper/data + Metadata loop file: /var/lib/docker/devicemapper/devicemapper/metadata + Library Version: 1.02.90 (2014-09-01) + Execution Driver: native-0.2 + Logging Driver: json-file + Kernel Version: 3.19.0-15-generic + Operating System: Ubuntu 15.04 + + +Your choice of storage driver can affect the performance of your containerized +applications. So it's important to understand the different storage driver +options available and select the right one for your application. Later, in this + page you'll find some advice for choosing an appropriate driver. + +## Shared storage systems and the storage driver + +Many enterprises consume storage from shared storage systems such as SAN and +NAS arrays. These often provide increased performance and availability, as well + as advanced features such as thin provisioning, deduplication and compression. + +The Docker storage driver and data volumes can both operate on top of storage +provided by shared storage systems. This allows Docker to leverage the +increased performance and availability these systems provide. However, Docker +does not integrate with these underlying systems. + +Remember that each Docker storage driver is based on a Linux filesystem or +volume manager. Be sure to follow existing best practices for operating your +storage driver (filesystem or volume manager) on top of your shared storage +system. For example, if using the ZFS storage driver on top of *XYZ* shared +storage system, be sure to follow best practices for operating ZFS filesystems +on top of XYZ shared storage system. + +## Which storage driver should you choose? + +Several factors influence the selection of a storage driver. However, these two + facts must be kept in mind: + +1. No single driver is well suited to every use-case +2. Storage drivers are improving and evolving all of the time + +With these factors in mind, the following points, coupled with the table below, + should provide some guidance. + +### Stability +For the most stable and hassle-free Docker experience, you should consider the +following: + +- **Use the default storage driver for your distribution**. When Docker +installs, it chooses a default storage driver based on the configuration of +your system. Stability is an important factor influencing which storage driver +is used by default. Straying from this default may increase your chances of +encountering bugs and nuances. +- **Follow the configuration specified on the CS Engine +[compatibility matrix](https://www.docker.com/compatibility-maintenance)**. The + CS Engine is the commercially supported version of the Docker Engine. It's +code-base is identical to the open source Engine, but it has a limited set of +supported configurations. These *supported configurations* use the most stable +and mature storage drivers. Straying from these configurations may also +increase your chances of encountering bugs and nuances. + +### Experience and expertise + +Choose a storage driver that you and your team/organization have experience +with. For example, if you use RHEL or one of its downstream forks, you may +already have experience with LVM and Device Mapper. If so, you may wish to use +the `devicemapper` driver. + +If you do not feel you have expertise with any of the storage drivers supported + by Docker, and you want an easy-to-use stable Docker experience, you should +consider using the default driver installed by your distribution's Docker +package. + +### Future-proofing + +Many people consider OverlayFS as the future of the Docker storage driver. +However, it is less mature, and potentially less stable than some of the more +mature drivers such as `aufs` and `devicemapper`. For this reason, you should +use the OverlayFS driver with caution and expect to encounter more bugs and +nuances than if you were using a more mature driver. + +The following diagram lists each storage driver and provides insight into some +of their pros and cons. When selecting which storage driver to use, consider +the guidance offered by the table below along with the points mentioned above. + +![](images/driver-pros-cons.png) + + +## Related information + +* [Understand images, containers, and storage drivers](imagesandcontainers.md) +* [AUFS storage driver in practice](aufs-driver.md) +* [Btrfs storage driver in practice](btrfs-driver.md) +* [Device Mapper storage driver in practice](device-mapper-driver.md) diff --git a/docs/userguide/storagedriver/zfs-driver.md b/docs/userguide/storagedriver/zfs-driver.md new file mode 100644 index 00000000..e55e7396 --- /dev/null +++ b/docs/userguide/storagedriver/zfs-driver.md @@ -0,0 +1,296 @@ + + +# Docker and ZFS in practice + +ZFS is a next generation filesystem that supports many advanced storage +technologies such as volume management, snapshots, checksumming, compression +and deduplication, replication and more. + +It was created by Sun Microsystems (now Oracle Corporation) and is open sourced + under the CDDL license. Due to licensing incompatibilities between the CDDL +and GPL, ZFS cannot be shipped as part of the mainline Linux kernel. However, +the ZFS On Linux (ZoL) project provides an out-of-tree kernel module and +userspace tools which can be installed separately. + +The ZFS on Linux (ZoL) port is healthy and maturing. However, at this point in +time it is not recommended to use the `zfs` Docker storage driver for +production use unless you have substantial experience with ZFS on Linux. + +> **Note:** There is also a FUSE implementation of ZFS on the Linux platform. +> This should work with Docker but is not recommended. The native ZFS driver +> (ZoL) is more tested, more performant, and is more widely used. The remainder +> of this document will relate to the native ZoL port. + + +## Image layering and sharing with ZFS + +The Docker `zfs` storage driver makes extensive use of three ZFS datasets: + +- filesystems +- snapshots +- clones + +ZFS filesystems are thinly provisioned and have space allocated to them from a +ZFS pool (zpool) via allocate on demand operations. Snapshots and clones are +space-efficient point-in-time copies of ZFS filesystems. Snapshots are +read-only. Clones are read-write. Clones can only be created from snapshots. +This simple relationship is shown in the diagram below. + +![](images/zfs_clones.jpg) + +The solid line in the diagram shows the process flow for creating a clone. Step + 1 creates a snapshot of the filesystem, and step two creates the clone from +the snapshot. The dashed line shows the relationship between the clone and the +filesystem, via the snapshot. All three ZFS datasets draw space form the same +underlying zpool. + +On Docker hosts using the `zfs` storage driver, the base layer of an image is a + ZFS filesystem. Each child layer is a ZFS clone based on a ZFS snapshot of the + layer below it. A container is a ZFS clone based on a ZFS Snapshot of the top +layer of the image it's created from. All ZFS datasets draw their space from a +common zpool. The diagram below shows how this is put together with a running +container based on a two-layer image. + +![](images/zfs_zpool.jpg) + +The following process explains how images are layered and containers created. +The process is based on the diagram above. + +1. The base layer of the image exists on the Docker host as a ZFS filesystem. + + This filesystem consumes space from the zpool used to create the Docker +host's local storage area at `/var/lib/docker`. + +2. Additional image layers are clones of the dataset hosting the image layer +directly below it. + + In the diagram, "Layer 1" is added by making a ZFS snapshot of the base +layer and then creating a clone from that snapshot. The clone is writable and +consumes space on-demand from the zpool. The snapshot is read-only, maintaining + the base layer as an immutable object. + +3. When the container is launched, a read-write layer is added above the image. + + In the diagram above, the container's read-write layer is created by making + a snapshot of the top layer of the image (Layer 1) and creating a clone from +that snapshot. + + As changes are made to the container, space is allocated to it from the +zpool via allocate-on-demand operations. By default, ZFS will allocate space in + blocks of 128K. + +This process of creating child layers and containers from *read-only* snapshots + allows images to be maintained as immutable objects. + +## Container reads and writes with ZFS + +Container reads with the `zfs` storage driver are very simple. A newly launched + container is based on a ZFS clone. This clone initially shares all of its data + with the dataset it was created from. This means that read operations with the + `zfs` storage driver are fast – even if the data being read was note +copied into the container yet. This sharing of data blocks is shown in the +diagram below. + +![](images/zpool_blocks.jpg) + +Writing new data to a container is accomplished via an allocate-on-demand +operation. Every time a new area of the container needs writing to, a new block + is allocated from the zpool. This means that containers consume additional +space as new data is written to them. New space is allocated to the container +(ZFS Clone) from the underlying zpool. + +Updating *existing data* in a container is accomplished by allocating new +blocks to the containers clone and storing the changed data in those new +blocks. The original blocks are unchanged, allowing the underlying image +dataset to remain immutable. This is the same as writing to a normal ZFS +filesystem and is an implementation of copy-on-write semantics. + +## Configure Docker with the ZFS storage driver + +The `zfs` storage driver is only supported on a Docker host where +`/var/lib/docker` is mounted as a ZFS filesystem. This section shows you how to + install and configure native ZFS on Linux (ZoL) on an Ubuntu 14.04 system. + +### Prerequisites + +If you have already used the Docker daemon on your Docker host and have images +you want to keep, `push` them Docker Hub or your private Docker Trusted +Registry before attempting this procedure. + +Stop the Docker daemon. Then, ensure that you have a spare block device at +`/dev/xvdb`. The device identifier may be be different in your environment and +you should substitute your own values throughout the procedure. + +### Install Zfs on Ubuntu 14.04 LTS + +1. If it is running, stop the Docker `daemon`. + +1. Install the `software-properties-common` package. + + This is required for the `add-apt-repository` command. + + $ sudo apt-get install -y software-properties-common + Reading package lists... Done + Building dependency tree + + +2. Add the `zfs-native` package archive. + + $ sudo add-apt-repository ppa:zfs-native/stable + The native ZFS filesystem for Linux. Install the ubuntu-zfs package. + + gpg: key F6B0FC61: public key "Launchpad PPA for Native ZFS for Linux" imported + gpg: Total number processed: 1 + gpg: imported: 1 (RSA: 1) + OK + +3. Get the latest package lists for all registered repositories and package +archives. + + $ sudo apt-get update + Ign http://us-west-2.ec2.archive.ubuntu.com trusty InRelease + Get:1 http://us-west-2.ec2.archive.ubuntu.com trusty-updates InRelease [64.4 kB] + + Fetched 10.3 MB in 4s (2,370 kB/s) + Reading package lists... Done + +4. Install the `ubuntu-zfs` package. + + $ sudo apt-get install -y ubuntu-zfs + Reading package lists... Done + Building dependency tree + + +5. Load the `zfs` module. + + $ sudo modprobe zfs + +6. Verify that it loaded correctly. + + $ lsmod | grep zfs + zfs 2768247 0 + zunicode 331170 1 zfs + zcommon 55411 1 zfs + znvpair 89086 2 zfs,zcommon + spl 96378 3 zfs,zcommon,znvpair + zavl 15236 1 zfs + +## Configure ZFS for Docker + +Once ZFS is installed and loaded, you're ready to configure ZFS for Docker. + + +1. Create a new `zpool`. + + $ sudo zpool create -f zpool-docker /dev/xvdb + + The command creates the `zpool` and gives it the name "zpool-docker". The name is arbitrary. + +2. Check that the `zpool` exists. + + $ sudo zfs list + NAME USED AVAIL REFER MOUNTPOINT + zpool-docker 55K 3.84G 19K /zpool-docker + +3. Create and mount a new ZFS filesystem to `/var/lib/docker`. + + $ sudo zfs create -o mountpoint=/var/lib/docker zpool-docker/docker + +4. Check that the previous step worked. + + $ sudo zfs list -t all + NAME USED AVAIL REFER MOUNTPOINT + zpool-docker 93.5K 3.84G 19K /zpool-docker + zpool-docker/docker 19K 3.84G 19K /var/lib/docker + + Now that you have a ZFS filesystem mounted to `/var/lib/docker`, the daemon + should automatically load with the `zfs` storage driver. + +5. Start the Docker daemon. + + $ sudo service docker start + docker start/running, process 2315 + + The procedure for starting the Docker daemon may differ depending on the + Linux distribution you are using. It is possible to force the Docker daemon + to start with the `zfs` storage driver by passing the + `--storage-driver=zfs`flag to the `docker daemon` command, or to the + `DOCKER_OPTS` line in the Docker config file. + +6. Verify that the daemon is using the `zfs` storage driver. + + $ sudo docker info + Containers: 0 + Images: 0 + Storage Driver: zfs + Zpool: zpool-docker + Zpool Health: ONLINE + Parent Dataset: zpool-docker/docker + Space Used By Parent: 27648 + Space Available: 4128139776 + Parent Quota: no + Compression: off + Execution Driver: native-0.2 + [...] + + The output of the command above shows that the Docker daemon is using the + `zfs` storage driver and that the parent dataset is the + `zpool-docker/docker` filesystem created earlier. + +Your Docker host is now using ZFS to store to manage its images and containers. + +## ZFS and Docker performance + +There are several factors that influence the performance of Docker using the +`zfs` storage driver. + +- **Memory**. Memory has a major impact on ZFS performance. This goes back to +the fact that ZFS was originally designed for use on big Sun Solaris servers +with large amounts of memory. Keep this in mind when sizing your Docker hosts. + +- **ZFS Features**. Using ZFS features, such as deduplication, can +significantly increase the amount of memory ZFS uses. For memory consumption +and performance reasons it is recommended to turn off ZFS deduplication. +However, deduplication at other layers in the stack (such as SAN or NAS arrays) + can still be used as these do not impact ZFS memory usage and performance. If +using SAN, NAS or other hardware RAID technologies you should continue to +follow existing best practices for using them with ZFS. + +- **ZFS Caching**. ZFS caches disk blocks in a memory structure called the +adaptive replacement cache (ARC). The *Single Copy ARC* feature of ZFS allows a + single cached copy of a block to be shared by multiple clones of a filesystem. + This means that multiple running containers can share a single copy of cached +block. This means that ZFS is a good option for PaaS and other high density use + cases. + +- **Fragmentation**. Fragmentation is a natural byproduct of copy-on-write +filesystems like ZFS. However, ZFS writes in 128K blocks and allocates *slabs* +(multiple 128K blocks) to CoW operations in an attempt to reduce fragmentation. + The ZFS intent log (ZIL) and the coalescing of writes (delayed writes) also +help to reduce fragmentation. + +- **Use the native ZFS driver for Linux**. Although the Docker `zfs` storage +driver supports the ZFS FUSE implementation, it is not recommended when high +performance is required. The native ZFS on Linux driver tends to perform better + than the FUSE implementation. + +The following generic performance best practices also apply to ZFS. + +- **Use of SSD**. For best performance it is always a good idea to use fast +storage media such as solid state devices (SSD). However, if you only have a +limited amount of SSD storage available it is recommended to place the ZIL on +SSD. + +- **Use Data Volumes**. Data volumes provide the best and most predictable +performance. This is because they bypass the storage driver and do not incur +any of the potential overheads introduced by thin provisioning and +copy-on-write. For this reason, you should place heavy write workloads on data +volumes. diff --git a/errors/errors.go b/errors/errors.go new file mode 100644 index 00000000..8070f48f --- /dev/null +++ b/errors/errors.go @@ -0,0 +1,41 @@ +package errors + +import "net/http" + +// apiError is an error wrapper that also +// holds information about response status codes. +type apiError struct { + error + statusCode int +} + +// HTTPErrorStatusCode returns a status code. +func (e apiError) HTTPErrorStatusCode() int { + return e.statusCode +} + +// NewErrorWithStatusCode allows you to associate +// a specific HTTP Status Code to an error. +// The Server will take that code and set +// it as the response status. +func NewErrorWithStatusCode(err error, code int) error { + return apiError{err, code} +} + +// NewBadRequestError creates a new API error +// that has the 400 HTTP status code associated to it. +func NewBadRequestError(err error) error { + return NewErrorWithStatusCode(err, http.StatusBadRequest) +} + +// NewRequestNotFoundError creates a new API error +// that has the 404 HTTP status code associated to it. +func NewRequestNotFoundError(err error) error { + return NewErrorWithStatusCode(err, http.StatusNotFound) +} + +// NewRequestConflictError creates a new API error +// that has the 409 HTTP status code associated to it. +func NewRequestConflictError(err error) error { + return NewErrorWithStatusCode(err, http.StatusConflict) +} diff --git a/experimental/README.md b/experimental/README.md new file mode 100644 index 00000000..659780e3 --- /dev/null +++ b/experimental/README.md @@ -0,0 +1,81 @@ +# Docker Experimental Features + +This page contains a list of features in the Docker engine which are +experimental. Experimental features are **not** ready for production. They are +provided for test and evaluation in your sandbox environments. + +The information below describes each feature and the GitHub pull requests and +issues associated with it. If necessary, links are provided to additional +documentation on an issue. As an active Docker user and community member, +please feel free to provide any feedback on these features you wish. + +## Install Docker experimental + +Unlike the regular Docker binary, the experimental channels is built and +updated nightly on https://experimental.docker.com. From one day to the +next, new features may appear, while existing experimental features may be +refined or entirely removed. + +1. Verify that you have `curl` installed. + + $ which curl + + If `curl` isn't installed, install it after updating your manager: + + $ sudo apt-get update + $ sudo apt-get install curl + +2. Get the latest Docker package. + + $ curl -sSL https://experimental.docker.com/ | sh + + The system prompts you for your `sudo` password. Then, it downloads and + installs Docker and its dependencies. + + >**Note**: If your company is behind a filtering proxy, you may find that the + >`apt-key` + >command fails for the Docker repo during installation. To work around this, + >add the key directly using the following: + > + > $ curl -sSL https://experimental.docker.com/gpg | sudo apt-key add - + +3. Verify `docker` is installed correctly. + + $ sudo docker run hello-world + + This command downloads a test image and runs it in a container. + +### Get the Linux binary +To download the latest experimental `docker` binary for Linux, +use the following URLs: + + https://experimental.docker.com/builds/Linux/i386/docker-latest + + https://experimental.docker.com/builds/Linux/x86_64/docker-latest + +After downloading the appropriate binary, you can follow the instructions +[here](https://docs.docker.com/installation/binaries/#get-the-docker-binary) to run the `docker` daemon. + +> **Note** +> +> 1) You can get the MD5 and SHA256 hashes by appending .md5 and .sha256 to the URLs respectively +> +> 2) You can get the compressed binaries by appending .tgz to the URLs + +### Build an experimental binary +You can also build the experimental binary from the standard development environment by adding +`DOCKER_EXPERIMENTAL=1` to the environment where you run `make` to build Docker binaries. For example, +to build a Docker binary with the experimental features enabled: + + $ DOCKER_EXPERIMENTAL=1 make binary + +## Current experimental features + + * [External graphdriver plugins](plugins_graphdriver.md) + * The user namespaces feature has graduated from experimental. + +## How to comment on an experimental feature + +Each feature's documentation includes a list of proposal pull requests or PRs associated with the feature. If you want to comment on or suggest a change to a feature, please add it to the existing feature PR. + +Issues or problems with a feature? Inquire for help on the `#docker` IRC channel or in on the [Docker Google group](https://groups.google.com/forum/#!forum/docker-user). diff --git a/experimental/images/ipvlan-l3.gliffy b/experimental/images/ipvlan-l3.gliffy new file mode 100644 index 00000000..bf0512af --- /dev/null +++ b/experimental/images/ipvlan-l3.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#FFFFFF","width":447,"height":422,"nodeIndex":326,"autoFit":true,"exportBorder":false,"gridOn":true,"snapToGrid":false,"drawingGuidesOn":false,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"maxWidth":5000,"maxHeight":5000,"themeData":{"uid":"com.gliffy.theme.beach_day","name":"Beach Day","shape":{"primary":{"strokeWidth":2,"strokeColor":"#00A4DA","fillColor":"#AEE4F4","gradient":false,"dropShadow":false,"opacity":1,"text":{"color":"#004257"}},"secondary":{"strokeWidth":2,"strokeColor":"#CDB25E","fillColor":"#EACF81","gradient":false,"dropShadow":false,"opacity":1,"text":{"color":"#332D1A"}},"tertiary":{"strokeWidth":2,"strokeColor":"#FFBE00","fillColor":"#FFF1CB","gradient":false,"dropShadow":false,"opacity":1,"text":{"color":"#000000"}},"highlight":{"strokeWidth":2,"strokeColor":"#00A4DA","fillColor":"#00A4DA","gradient":false,"dropShadow":false,"opacity":1,"text":{"color":"#ffffff"}}},"line":{"strokeWidth":2,"strokeColor":"#00A4DA","fillColor":"none","arrowType":2,"interpolationType":"quadratic","cornerRadius":0,"text":{"color":"#002248"}},"text":{"color":"#002248"},"stage":{"color":"#FFFFFF"}},"viewportType":"default","fitBB":{"min":{"x":9,"y":10.461511948529278},"max":{"x":447,"y":421.5}},"printModel":{"pageSize":"a4","portrait":false,"fitToOnePage":false,"displayPageBreaks":false},"objects":[{"x":12.0,"y":200.0,"rotation":0.0,"id":276,"width":434.00000000000006,"height":197.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":10,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#434343","fillColor":"#c5e4fc","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":0.93,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":275.0,"y":8.93295288085936,"rotation":0.0,"id":269,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.uml.uml_v2.sequence.anchor_line","order":14,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":272,"py":0.5,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":290,"py":1.0,"px":0.7071067811865476}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[82.0,295.5670471191406],[-4.628896294384617,211.06704711914062]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":285.0,"y":18.93295288085936,"rotation":0.0,"id":268,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.uml.uml_v2.sequence.anchor_line","order":15,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":316,"py":0.5,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":290,"py":0.9999999999999996,"px":0.29289321881345254}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-204.0,285.5670471191406],[-100.37110370561533,201.06704711914062]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":8.0,"y":203.5,"rotation":0.0,"id":267,"width":116.0,"height":16.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":16,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Docker Host

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":10.0,"y":28.93295288085936,"rotation":0.0,"id":278,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.uml.uml_v2.sequence.anchor_line","order":17,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":290,"py":0.5,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":246,"py":0.5,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[217.5,167.06704711914062],[219.11774189711457,53.02855906766992]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":57.51435447730654,"y":10.461511948529278,"rotation":0.0,"id":246,"width":343.20677483961606,"height":143.0,"uid":"com.gliffy.shape.cisco.cisco_v1.storage.cloud","order":18,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.storage.cloud","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#434343","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":106.0,"y":55.19999694824217,"rotation":0.0,"id":262,"width":262.0,"height":75.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":22,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Unless notified about the container networks, the physical network does not have a route to their subnets

Who has 10.16.20.0/24?

Who has 10.1.20.0/24?

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":7.0,"y":403.5,"rotation":0.0,"id":282,"width":442.0,"height":18.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":23,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Containers can be on different subnets and reach each other

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":106.0,"y":252.5,"rotation":0.0,"id":288,"width":238.0,"height":22.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":24,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

 Ipvlan L3 Mode

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":124.0,"y":172.0,"rotation":0.0,"id":290,"width":207.0,"height":48.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":25,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#cccccc","gradient":false,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":3.568965517241383,"y":0.0,"rotation":0.0,"id":291,"width":199.86206896551747,"height":42.0,"uid":null,"order":27,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Eth0

192.168.50.10/24

Parent interface acts as a Router

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":29.0,"y":358.1999969482422,"rotation":0.0,"id":304,"width":390.99999999999994,"height":32.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":29,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

All containers can ping each other without a router if

they share the same parent interface (example eth0)

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":24.0,"y":276.0,"rotation":0.0,"id":320,"width":134.0,"height":77.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":48,"lockAspectRatio":false,"lockShape":false,"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":316,"width":114.0,"height":57.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":44,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":0.97,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.279999999999999,"y":0.0,"rotation":0.0,"id":317,"width":109.44000000000001,"height":43.0,"uid":null,"order":47,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container(s)

Eth0 

172.16.20.x/24

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":10.0,"y":10.0,"rotation":0.0,"id":318,"width":114.0,"height":57.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":42,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":0.97,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":20.0,"y":20.0,"rotation":0.0,"id":319,"width":114.0,"height":57.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":40,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":0.97,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":300.0,"y":276.0,"rotation":0.0,"id":321,"width":134.0,"height":77.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":49,"lockAspectRatio":false,"lockShape":false,"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":272,"width":114.0,"height":57.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":35,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":0.97,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.279999999999999,"y":0.0,"rotation":0.0,"id":273,"width":109.44000000000001,"height":44.0,"uid":null,"order":38,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container(s)

Eth0 10.1.20.x/24

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":10.0,"y":10.0,"rotation":0.0,"id":310,"width":114.0,"height":57.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":33,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":0.97,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":20.0,"y":20.0,"rotation":0.0,"id":312,"width":114.0,"height":57.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":31,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":0.97,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":368.0,"y":85.93295288085938,"rotation":0.0,"id":322,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.uml.uml_v2.sequence.anchor_line","order":50,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#434343","fillColor":"none","dashStyle":"4.0,4.0","startArrow":2,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-191.0,222.06704711914062],[-80.9272967534639,222.06704711914062]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":167.0,"y":25.499999999999986,"rotation":0.0,"id":323,"width":135.0,"height":32.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":51,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Physical Network

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"layers":[{"guid":"9wom3rMkTrb3","order":0,"name":"Layer 0","active":true,"locked":false,"visible":true,"nodeIndex":53}],"shapeStyles":{},"lineStyles":{"global":{"fill":"none","stroke":"#434343","strokeWidth":2,"dashStyle":"4.0,4.0","startArrow":2,"endArrow":2,"orthoMode":2}},"textStyles":{"global":{"face":"Arial","size":"13px","color":"#000000"}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":["com.gliffy.libraries.basic.basic_v1.default","com.gliffy.libraries.flowchart.flowchart_v1.default","com.gliffy.libraries.swimlanes.swimlanes_v1.default","com.gliffy.libraries.images","com.gliffy.libraries.network.network_v4.home","com.gliffy.libraries.network.network_v4.business","com.gliffy.libraries.network.network_v4.rack","com.gliffy.libraries.network.network_v3.home","com.gliffy.libraries.network.network_v3.business","com.gliffy.libraries.network.network_v3.rack"],"lastSerialized":1458117032939,"analyticsProduct":"Confluence"},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/experimental/images/ipvlan-l3.png b/experimental/images/ipvlan-l3.png new file mode 100644 index 0000000000000000000000000000000000000000..3227a83ca1541ec68e06b0aa105e22fdf5ae9e6f GIT binary patch literal 18260 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,thephysicalnetworkdoesnothavearoutetotheirsubnetsWhohas10.16.20.0/24?Whohas10.1.20.0/24?ContainerscanbeondifferentsubnetsandreacheachotherIpvlanL3ModeEth0192.168.50.10/24ParentinterfaceactsasaRouterAllcontainerscanpingeachotherarouterifwithouttheysharetheparentinterface (sameexampleeth0)Container(s)Eth010.1.20.x/24Container(s)Eth0172.16.20.x/24PhysicalNetwork \ No newline at end of file diff --git a/experimental/images/ipvlan_l2_simple.gliffy b/experimental/images/ipvlan_l2_simple.gliffy new file mode 100644 index 00000000..41b0475d --- /dev/null +++ b/experimental/images/ipvlan_l2_simple.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#ffffff","width":323,"height":292,"nodeIndex":211,"autoFit":true,"exportBorder":false,"gridOn":true,"snapToGrid":false,"drawingGuidesOn":false,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"maxWidth":5000,"maxHeight":5000,"themeData":null,"viewportType":"default","fitBB":{"min":{"x":16,"y":21.51999694824218},"max":{"x":323,"y":291.5}},"printModel":{"pageSize":"a4","portrait":false,"fitToOnePage":false,"displayPageBreaks":false},"objects":[{"x":241.0,"y":36.0,"rotation":0.0,"id":199,"width":73.00000000000003,"height":40.150000000000006,"uid":"com.gliffy.shape.network.network_v4.business.router","order":41,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.router","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":85.0,"y":50.0,"rotation":0.0,"id":150,"width":211.0,"height":31.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":37,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":6.0,"strokeColor":"#999999","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[3.1159999999999997,6.359996948242184],[85.55799999999999,6.359996948242184],[85.55799999999999,62.0],[84.0,62.0]],"lockSegments":{"1":true},"ortho":true}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":22.803646598905374,"y":21.51999694824218,"rotation":0.0,"id":134,"width":64.31235340109463,"height":90.0,"uid":"com.gliffy.shape.cisco.cisco_v1.servers.standard_host","order":43,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.servers.standard_host","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#3d85c6","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":87.0,"y":24.199996948242188,"rotation":0.0,"id":187,"width":105.0,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":39,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 192.168.1.0/24

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":147.0,"y":50.0,"rotation":0.0,"id":196,"width":211.0,"height":31.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":40,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":199,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":6.0,"strokeColor":"#999999","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-82.00001598011289,6.075000000000003],[94.0,6.075000000000003]],"lockSegments":{"1":true},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":220.0,"y":79.19999694824219,"rotation":0.0,"id":207,"width":105.0,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":42,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Network Router

192.168.1.1/24

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":27.38636363636374,"y":108.14285409109937,"rotation":0.0,"id":129,"width":262.0,"height":124.0,"uid":"com.gliffy.shape.iphone.iphone_ios7.icons_glyphs.glyph_cloud","order":44,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.iphone.iphone_ios7.icons_glyphs.glyph_cloud","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#929292","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":33.0,"y":157.96785409109907,"rotation":0.0,"id":114,"width":150.0,"height":60.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":16,"lockAspectRatio":false,"lockShape":false,"children":[{"x":44.0,"y":2.9951060358893704,"rotation":0.0,"id":95,"width":62.0,"height":36.17618270799329,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":4,"lockAspectRatio":false,"lockShape":false,"children":[{"x":29.139999999999997,"y":3.2300163132136848,"rotation":0.0,"id":96,"width":3.719999999999998,"height":29.7161500815659,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":13,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":99,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":99,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.8599999999999994,-1.2920065252854727],[1.8599999999999994,31.0081566068514]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":51.46,"y":3.2300163132136848,"rotation":0.0,"id":97,"width":1.2156862745098034,"height":31.008156606851365,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":10,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-1.4193795664340882,-1.292006525285804],[-1.4193795664340882,31.008156606851536]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":9.919999999999993,"y":1.5073409461663854,"rotation":0.0,"id":98,"width":1.239999999999999,"height":31.008156606851365,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":7,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[2.0393795664339223,0.4306688417619762],[2.0393795664339223,32.73083197389853]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":1.9380097879282103,"rotation":0.0,"id":99,"width":62.0,"height":32.300163132136866,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":2,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#6fa8dc","fillColor":"#3d85c6","gradient":true,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":38.326264274062034,"rotation":0.0,"id":112,"width":150.0,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":15,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

container1

192.168.1.2/24

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":124.0,"y":157.96785409109907,"rotation":0.0,"id":115,"width":150.0,"height":58.99999999999999,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":33,"lockAspectRatio":false,"lockShape":false,"children":[{"x":44.0,"y":2.94518760195788,"rotation":0.0,"id":116,"width":62.0,"height":35.573246329526725,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":21,"lockAspectRatio":false,"lockShape":false,"children":[{"x":29.139999999999997,"y":3.1761827079934557,"rotation":0.0,"id":117,"width":3.719999999999998,"height":29.220880913539798,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":30,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":120,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":120,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.8600000000000136,-1.2704730831974018],[1.8600000000000136,30.49135399673719]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":51.46,"y":3.1761827079934557,"rotation":0.0,"id":118,"width":1.2156862745098034,"height":30.49135399673717,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":27,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-1.4193795664340882,-1.2704730831977067],[-1.4193795664340882,30.491353996737335]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":9.919999999999993,"y":1.482218597063612,"rotation":0.0,"id":119,"width":1.239999999999999,"height":30.49135399673717,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":24,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[2.0393795664339223,0.42349102773260977],[2.0393795664339223,32.185318107666895]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":1.9057096247960732,"rotation":0.0,"id":120,"width":62.0,"height":31.76182707993458,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":19,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#6fa8dc","fillColor":"#3d85c6","gradient":true,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":36.36247960848299,"rotation":0.0,"id":121,"width":150.0,"height":30.183360522022674,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":32,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

container2

192.168.1.3/24

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":102.0,"y":130.1999969482422,"rotation":0.0,"id":130,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":34,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

pub_net (eth0)

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":93.0,"y":92.69999694824219,"rotation":0.0,"id":140,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":35,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"


","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":14.0,"y":114.19999694824219,"rotation":0.0,"id":142,"width":78.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":36,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Docker Host

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":71.0,"y":235.5,"rotation":0.0,"id":184,"width":196.0,"height":56.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":38,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

docker network create -d ipvlan \\

    --subnet=192.168.1.0/24 \\

    --gateway=192.168.1.1 \\

    -o parent=eth0 pub_net

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"layers":[{"guid":"9wom3rMkTrb3","order":0,"name":"Layer 0","active":true,"locked":false,"visible":true,"nodeIndex":45}],"shapeStyles":{},"lineStyles":{"global":{"stroke":"#999999","strokeWidth":6,"orthoMode":1}},"textStyles":{"global":{"bold":true,"face":"Arial","size":"12px","color":"#000000"}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":["com.gliffy.libraries.network.network_v4.home","com.gliffy.libraries.network.network_v4.business","com.gliffy.libraries.network.network_v4.rack","com.gliffy.libraries.network.network_v3.home","com.gliffy.libraries.network.network_v3.business","com.gliffy.libraries.network.network_v3.rack"],"lastSerialized":1457584497063,"analyticsProduct":"Confluence"},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/experimental/images/ipvlan_l2_simple.png b/experimental/images/ipvlan_l2_simple.png new file mode 100644 index 0000000000000000000000000000000000000000..e489a446ddd255ce9360445f0f895acad31ae214 GIT binary patch literal 20145 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.168.1.2/24container2192.168.1.3/24pub_net (eth0)DockerHostdockernetworkcreate -dipvlan \--subnet=192.168.1.0/24 \--gateway=192.168.1.1 \-oparent=eth0pub_neteth0192.168.1.0/24NetworkRouter192.168.1.1/24 \ No newline at end of file diff --git a/experimental/images/macvlan-bridge-ipvlan-l2.gliffy b/experimental/images/macvlan-bridge-ipvlan-l2.gliffy new file mode 100644 index 00000000..eceec778 --- /dev/null +++ b/experimental/images/macvlan-bridge-ipvlan-l2.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#FFFFFF","width":541,"height":352,"nodeIndex":290,"autoFit":true,"exportBorder":false,"gridOn":true,"snapToGrid":false,"drawingGuidesOn":false,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"maxWidth":5000,"maxHeight":5000,"themeData":{"uid":"com.gliffy.theme.beach_day","name":"Beach Day","shape":{"primary":{"strokeWidth":2,"strokeColor":"#00A4DA","fillColor":"#AEE4F4","gradient":false,"dropShadow":false,"opacity":1,"text":{"color":"#004257"}},"secondary":{"strokeWidth":2,"strokeColor":"#CDB25E","fillColor":"#EACF81","gradient":false,"dropShadow":false,"opacity":1,"text":{"color":"#332D1A"}},"tertiary":{"strokeWidth":2,"strokeColor":"#FFBE00","fillColor":"#FFF1CB","gradient":false,"dropShadow":false,"opacity":1,"text":{"color":"#000000"}},"highlight":{"strokeWidth":2,"strokeColor":"#00A4DA","fillColor":"#00A4DA","gradient":false,"dropShadow":false,"opacity":1,"text":{"color":"#ffffff"}}},"line":{"strokeWidth":2,"strokeColor":"#00A4DA","fillColor":"none","arrowType":2,"interpolationType":"quadratic","cornerRadius":0,"text":{"color":"#002248"}},"text":{"color":"#002248"},"stage":{"color":"#FFFFFF"}},"viewportType":"default","fitBB":{"min":{"x":2,"y":6.5},"max":{"x":541,"y":334.5}},"printModel":{"pageSize":"a4","portrait":false,"fitToOnePage":false,"displayPageBreaks":false},"objects":[{"x":2.0,"y":6.5,"rotation":0.0,"id":288,"width":541.0,"height":22.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":31,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Macvlan Bridge Mode & Ipvlan L2 Mode

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":8.0,"y":177.0,"rotation":0.0,"id":234,"width":252.0,"height":129.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":0,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#434343","fillColor":"#c5e4fc","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":0.93,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":16.0,"y":240.0,"rotation":0.0,"id":225,"width":111.0,"height":57.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":1,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":true,"state":0,"opacity":0.73,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.2199999999999993,"y":0.0,"rotation":0.0,"id":235,"width":106.56,"height":45.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container #1

eth0

172.16.1.10/24

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":138.0,"y":240.0,"rotation":0.0,"id":237,"width":111.0,"height":57.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":4,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":true,"state":0,"opacity":0.73,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.2199999999999993,"y":0.0,"rotation":0.0,"id":238,"width":106.56,"height":44.0,"uid":null,"order":6,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container #2

eth0 172.16.1.11/24

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":40.0,"y":-26.067047119140625,"rotation":0.0,"id":258,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.uml.uml_v2.sequence.anchor_line","order":7,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":237,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":241,"py":1.0,"px":0.7071067811865476}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[153.5,266.0670471191406],[117.36753236814712,224.06704711914062]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":50.0,"y":-16.067047119140625,"rotation":0.0,"id":259,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.uml.uml_v2.sequence.anchor_line","order":8,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":225,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":241,"py":0.9999999999999996,"px":0.29289321881345254}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[21.5,256.0670471191406],[62.632467631852876,214.0670471191406]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":60.0,"y":-6.067047119140625,"rotation":0.0,"id":260,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.uml.uml_v2.sequence.anchor_line","order":9,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":241,"py":0.5,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":246,"py":0.5,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[75.0,180.06704711914062],[215.32345076546227,90.06897143333742]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":3.0,"y":184.5,"rotation":0.0,"id":261,"width":79.0,"height":32.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":10,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Docker

Host #1

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":283.0,"y":177.0,"rotation":0.0,"id":276,"width":252.0,"height":129.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":11,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#434343","fillColor":"#c5e4fc","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":0.93,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":291.0,"y":240.0,"rotation":0.0,"id":274,"width":111.0,"height":57.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":12,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":true,"state":0,"opacity":0.73,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.2199999999999993,"y":0.0,"rotation":0.0,"id":275,"width":106.56,"height":45.0,"uid":null,"order":14,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container #3

eth0

172.16.1.12/24

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":413.0,"y":240.0,"rotation":0.0,"id":272,"width":111.0,"height":57.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":15,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":true,"state":0,"opacity":0.73,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.2199999999999993,"y":0.0,"rotation":0.0,"id":273,"width":106.56,"height":44.0,"uid":null,"order":17,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container #4

eth0 172.16.1.13/24

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":315.0,"y":-26.067047119140625,"rotation":0.0,"id":269,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.uml.uml_v2.sequence.anchor_line","order":18,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":272,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":270,"py":1.0,"px":0.7071067811865476}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[153.5,266.0670471191406],[117.36753236814712,224.06704711914062]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":325.0,"y":-16.067047119140625,"rotation":0.0,"id":268,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.uml.uml_v2.sequence.anchor_line","order":19,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":274,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":270,"py":0.9999999999999996,"px":0.29289321881345254}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[21.5,256.0670471191406],[62.632467631852876,214.0670471191406]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":278.0,"y":184.5,"rotation":0.0,"id":267,"width":79.0,"height":32.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":20,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Docker

Host #2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":70.0,"y":3.932952880859375,"rotation":0.0,"id":278,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.uml.uml_v2.sequence.anchor_line","order":21,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":270,"py":0.5,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":246,"py":0.5,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[340.0,170.06704711914062],[205.32345076546227,80.06897143333742]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":167.32131882292583,"y":39.0019243141968,"rotation":0.0,"id":246,"width":216.0042638850729,"height":90.0,"uid":"com.gliffy.shape.cisco.cisco_v1.storage.cloud","order":22,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.storage.cloud","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#434343","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":356.0,"y":150.0,"rotation":0.0,"id":270,"width":108.0,"height":48.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":23,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#cccccc","gradient":false,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":1.8620689655172418,"y":0.0,"rotation":0.0,"id":271,"width":104.27586206896557,"height":42.0,"uid":null,"order":25,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

(Host) eth0

172.16.1.253/24

(IP Optional)

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":81.0,"y":150.0,"rotation":0.0,"id":241,"width":108.0,"height":48.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":26,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#cccccc","gradient":false,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":1.8620689655172415,"y":0.0,"rotation":0.0,"id":242,"width":104.27586206896555,"height":42.0,"uid":null,"order":28,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

(Host) eth0

172.16.1.254/24

(IP Optional)

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":224.0,"y":64.19999694824219,"rotation":0.0,"id":262,"width":120.00000000000001,"height":32.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":29,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Network Gateway

172.16.1.1/24

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":307.5,"rotation":0.0,"id":282,"width":541.0,"height":36.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":30,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Containers Attached Directly to Parent Interface. No Bridge Used (Docker0)

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"layers":[{"guid":"9wom3rMkTrb3","order":0,"name":"Layer 0","active":true,"locked":false,"visible":true,"nodeIndex":32}],"shapeStyles":{},"lineStyles":{"global":{"fill":"none","stroke":"#000000","strokeWidth":1,"orthoMode":2}},"textStyles":{"global":{"italic":true,"face":"Arial","size":"20px","color":"#000000","bold":false}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":["com.gliffy.libraries.basic.basic_v1.default","com.gliffy.libraries.flowchart.flowchart_v1.default","com.gliffy.libraries.swimlanes.swimlanes_v1.default","com.gliffy.libraries.images","com.gliffy.libraries.network.network_v4.home","com.gliffy.libraries.network.network_v4.business","com.gliffy.libraries.network.network_v4.rack","com.gliffy.libraries.network.network_v3.home","com.gliffy.libraries.network.network_v3.business","com.gliffy.libraries.network.network_v3.rack"],"lastSerialized":1458124258706,"analyticsProduct":"Confluence"},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/experimental/images/macvlan-bridge-ipvlan-l2.png b/experimental/images/macvlan-bridge-ipvlan-l2.png new file mode 100644 index 0000000000000000000000000000000000000000..13aa4f212d9db346f307dfbe111fd657406bb943 GIT binary patch literal 14527 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 #1eth0172.16.1.10/24Container #2eth0172.16.1.11/24DockerHost #1Container #3eth0172.16.1.12/24Container #4eth0172.16.1.13/24DockerHost #2(Host)eth0172.16.1.253/24(IPOptional)(Host)eth0172.16.1.254/24(IPOptional)NetworkGateway172.16.1.1/24ContainersAttachedDirectlytoParentInterface.NoBridgeUsed (Docker0)MacvlanBridgeMode &IpvlanL2Mode \ No newline at end of file diff --git a/experimental/images/macvlan_bridge_simple.gliffy b/experimental/images/macvlan_bridge_simple.gliffy new file mode 100644 index 00000000..9d77e1f6 --- /dev/null +++ b/experimental/images/macvlan_bridge_simple.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#ffffff","width":328,"height":292,"nodeIndex":215,"autoFit":true,"exportBorder":false,"gridOn":true,"snapToGrid":false,"drawingGuidesOn":false,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"maxWidth":5000,"maxHeight":5000,"themeData":null,"viewportType":"default","fitBB":{"min":{"x":16,"y":21.51999694824218},"max":{"x":328,"y":291.5}},"printModel":{"pageSize":"a4","portrait":false,"fitToOnePage":false,"displayPageBreaks":false},"objects":[{"x":241.0,"y":36.0,"rotation":0.0,"id":199,"width":73.00000000000003,"height":40.150000000000006,"uid":"com.gliffy.shape.network.network_v4.business.router","order":42,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.router","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":85.0,"y":50.0,"rotation":0.0,"id":150,"width":211.0,"height":31.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":38,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":6.0,"strokeColor":"#999999","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[3.1159999999999997,6.359996948242184],[85.55799999999999,6.359996948242184],[85.55799999999999,62.0],[84.0,62.0]],"lockSegments":{"1":true},"ortho":true}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":22.803646598905374,"y":21.51999694824218,"rotation":0.0,"id":134,"width":64.31235340109463,"height":90.0,"uid":"com.gliffy.shape.cisco.cisco_v1.servers.standard_host","order":44,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.servers.standard_host","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#3d85c6","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":89.0,"y":22.199996948242188,"rotation":0.0,"id":187,"width":105.0,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":40,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth1 172.16.86.0/24

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":147.0,"y":50.0,"rotation":0.0,"id":196,"width":211.0,"height":31.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":41,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":199,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":6.0,"strokeColor":"#999999","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-82.00001598011289,6.075000000000003],[94.0,6.075000000000003]],"lockSegments":{"1":true},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":210.0,"y":80.19999694824219,"rotation":0.0,"id":207,"width":120.00000000000001,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":43,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Network Router

172.16.86.1/24

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":27.38636363636374,"y":108.14285409109937,"rotation":0.0,"id":129,"width":262.0,"height":124.0,"uid":"com.gliffy.shape.iphone.iphone_ios7.icons_glyphs.glyph_cloud","order":0,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.iphone.iphone_ios7.icons_glyphs.glyph_cloud","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#929292","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":33.0,"y":157.96785409109907,"rotation":0.0,"id":114,"width":150.0,"height":60.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":1,"lockAspectRatio":false,"lockShape":false,"children":[{"x":44.0,"y":2.9951060358893704,"rotation":0.0,"id":95,"width":62.0,"height":36.17618270799329,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":6,"lockAspectRatio":false,"lockShape":false,"children":[{"x":29.139999999999997,"y":3.2300163132136848,"rotation":0.0,"id":96,"width":3.719999999999998,"height":29.7161500815659,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":15,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":99,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":99,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.8599999999999994,-1.2920065252854727],[1.8599999999999994,31.0081566068514]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":51.46,"y":3.2300163132136848,"rotation":0.0,"id":97,"width":1.2156862745098034,"height":31.008156606851365,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":12,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-1.4193795664340882,-1.292006525285804],[-1.4193795664340882,31.008156606851536]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":9.919999999999993,"y":1.5073409461663854,"rotation":0.0,"id":98,"width":1.239999999999999,"height":31.008156606851365,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":9,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[2.0393795664339223,0.4306688417619762],[2.0393795664339223,32.73083197389853]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":1.9380097879282103,"rotation":0.0,"id":99,"width":62.0,"height":32.300163132136866,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":4,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#6fa8dc","fillColor":"#3d85c6","gradient":true,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":38.326264274062034,"rotation":0.0,"id":112,"width":150.0,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":17,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

container1

172.16.86.2/24

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":124.0,"y":157.96785409109907,"rotation":0.0,"id":115,"width":150.0,"height":58.99999999999999,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":34,"lockAspectRatio":false,"lockShape":false,"children":[{"x":44.0,"y":2.94518760195788,"rotation":0.0,"id":116,"width":62.0,"height":35.573246329526725,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":22,"lockAspectRatio":false,"lockShape":false,"children":[{"x":29.139999999999997,"y":3.1761827079934557,"rotation":0.0,"id":117,"width":3.719999999999998,"height":29.220880913539798,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":31,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":120,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":120,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.8600000000000136,-1.2704730831974018],[1.8600000000000136,30.49135399673719]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":51.46,"y":3.1761827079934557,"rotation":0.0,"id":118,"width":1.2156862745098034,"height":30.49135399673717,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":28,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-1.4193795664340882,-1.2704730831977067],[-1.4193795664340882,30.491353996737335]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":9.919999999999993,"y":1.482218597063612,"rotation":0.0,"id":119,"width":1.239999999999999,"height":30.49135399673717,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":25,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[2.0393795664339223,0.42349102773260977],[2.0393795664339223,32.185318107666895]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":1.9057096247960732,"rotation":0.0,"id":120,"width":62.0,"height":31.76182707993458,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":20,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#6fa8dc","fillColor":"#3d85c6","gradient":true,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":37.45415986949433,"rotation":0.0,"id":121,"width":150.0,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":33,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

container2

172.16.86.3/24

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":102.0,"y":130.1999969482422,"rotation":0.0,"id":130,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":35,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

pub_net (eth0)

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":93.0,"y":92.69999694824219,"rotation":0.0,"id":140,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":36,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"


","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":14.0,"y":114.19999694824219,"rotation":0.0,"id":142,"width":78.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":37,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Docker Host

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":71.0,"y":235.5,"rotation":0.0,"id":184,"width":196.0,"height":56.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":39,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

docker network create -d macvlan \\

    --subnet=172.16.86.0/24 \\

    --gateway=172.16.86.1  \\

    -o parent=eth1 pub_net

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"layers":[{"guid":"9wom3rMkTrb3","order":0,"name":"Layer 0","active":true,"locked":false,"visible":true,"nodeIndex":45}],"shapeStyles":{},"lineStyles":{"global":{"stroke":"#999999","strokeWidth":6,"orthoMode":1}},"textStyles":{"global":{"bold":true,"face":"Arial","size":"12px","color":"#000000"}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":["com.gliffy.libraries.network.network_v4.home","com.gliffy.libraries.network.network_v4.business","com.gliffy.libraries.network.network_v4.rack","com.gliffy.libraries.network.network_v3.home","com.gliffy.libraries.network.network_v3.business","com.gliffy.libraries.network.network_v3.rack"],"lastSerialized":1457586216662,"analyticsProduct":"Confluence"},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/experimental/images/macvlan_bridge_simple.png b/experimental/images/macvlan_bridge_simple.png new file mode 100644 index 0000000000000000000000000000000000000000..51fa66e263793573edbad30350a6e59cfbdd0256 GIT binary patch literal 22392 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 literal 0 HcmV?d00001 diff --git a/experimental/images/macvlan_bridge_simple.svg b/experimental/images/macvlan_bridge_simple.svg new file mode 100644 index 00000000..bce931f0 --- /dev/null +++ b/experimental/images/macvlan_bridge_simple.svg @@ -0,0 +1 @@ +container1172.16.86.2/24container2172.16.86.3/24pub_net (eth0)DockerHostdockernetworkcreate -dmacvlan \--subnet=172.16.86.0/24 \--gateway=172.16.86.1  \-oparent=eth1pub_neteth1172.16.86.0/24NetworkRouter172.16.86.1/24 \ No newline at end of file diff --git a/experimental/images/multi_tenant_8021q_vlans.gliffy b/experimental/images/multi_tenant_8021q_vlans.gliffy new file mode 100644 index 00000000..40eed172 --- /dev/null +++ b/experimental/images/multi_tenant_8021q_vlans.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#ffffff","width":389,"height":213,"nodeIndex":276,"autoFit":true,"exportBorder":false,"gridOn":true,"snapToGrid":false,"drawingGuidesOn":false,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"maxWidth":5000,"maxHeight":5000,"themeData":null,"viewportType":"default","fitBB":{"min":{"x":5,"y":6.6999969482421875},"max":{"x":389,"y":212.14285409109937}},"printModel":{"pageSize":"a4","portrait":false,"fitToOnePage":false,"displayPageBreaks":false},"objects":[{"x":64.0,"y":36.0,"rotation":0.0,"id":216,"width":211.0,"height":31.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":10,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":5.0,"strokeColor":"#e69138","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[-12.0,33.0],[84.0,33.0],[84.0,86.0],[120.0,86.0]],"lockSegments":{"1":true},"ortho":true}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":190.0,"y":32.0,"rotation":0.0,"id":254,"width":211.0,"height":31.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":11,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":5.0,"strokeColor":"#f1c232","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[-142.0,16.0],[54.0,16.0],[54.0,115.0],[87.0,115.0]],"lockSegments":{"1":true},"ortho":true}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":133.38636363636374,"y":108.14285409109937,"rotation":0.0,"id":226,"width":123.00000000000001,"height":104.0,"uid":"com.gliffy.shape.iphone.iphone_ios7.icons_glyphs.glyph_cloud","order":12,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.iphone.iphone_ios7.icons_glyphs.glyph_cloud","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#999999","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":15.147567221510933,"y":139.96785409109907,"rotation":0.0,"id":115,"width":107.40845070422536,"height":49.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":29,"lockAspectRatio":false,"lockShape":false,"children":[{"x":31.506478873239438,"y":2.4460032626429853,"rotation":0.0,"id":116,"width":44.395492957746484,"height":29.54388254486117,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":17,"lockAspectRatio":false,"lockShape":false,"children":[{"x":20.86588169014084,"y":2.637846655791175,"rotation":0.0,"id":117,"width":2.663729577464789,"height":24.268189233278818,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":26,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":120,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":120,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.3318647887324033,-1.055138662316466],[1.3318647887324033,25.3233278955953]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":36.84825915492961,"y":2.637846655791175,"rotation":0.0,"id":118,"width":1.0000000000000002,"height":25.323327895595277,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":23,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-0.8875219090985048,-1.0551386623167391],[-0.8875219090985048,25.323327895595412]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":7.103278873239435,"y":1.230995106035881,"rotation":0.0,"id":119,"width":1.0000000000000002,"height":25.323327895595277,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":20,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.2752008616871728,0.3517128874389471],[1.2752008616871728,26.73017944535047]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":1.5827079934747048,"rotation":0.0,"id":120,"width":44.395492957746484,"height":26.378466557911768,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":15,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#6fa8dc","fillColor":"#3d85c6","gradient":true,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":37.199347471451986,"rotation":0.0,"id":121,"width":107.40845070422536,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":28,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

container1 - vlan10

192.168.1.2/24

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":68.0,"y":82.69999694824219,"rotation":0.0,"id":140,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":30,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"


","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":71.0,"y":4.1999969482421875,"rotation":0.0,"id":187,"width":108.99999999999999,"height":19.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":31,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 - 802.1q trunk

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":282.0,"y":8.0,"rotation":0.0,"id":199,"width":73.00000000000003,"height":40.150000000000006,"uid":"com.gliffy.shape.network.network_v4.business.router","order":32,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.router","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":62.0,"y":55.0,"rotation":0.0,"id":210,"width":211.0,"height":31.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":34,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":5.0,"strokeColor":"#e06666","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[-8.0,11.0],[-8.0,34.0],[26.0,34.0],[26.0,57.0]],"lockSegments":{},"ortho":true}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":12.805718530101615,"y":11.940280333547719,"rotation":0.0,"id":134,"width":59.31028146989837,"height":83.0,"uid":"com.gliffy.shape.cisco.cisco_v1.servers.standard_host","order":35,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.servers.standard_host","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#3d85c6","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":64.0,"y":73.19999694824219,"rotation":0.0,"id":211,"width":60.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":36,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0.10

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":65.0,"y":52.19999694824219,"rotation":0.0,"id":212,"width":60.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":37,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0.20

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":7.386363636363733,"y":108.14285409109937,"rotation":0.0,"id":219,"width":123.00000000000001,"height":104.0,"uid":"com.gliffy.shape.iphone.iphone_ios7.icons_glyphs.glyph_cloud","order":38,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.iphone.iphone_ios7.icons_glyphs.glyph_cloud","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#999999","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":139.1475672215109,"y":139.96785409109907,"rotation":0.0,"id":227,"width":107.40845070422536,"height":49.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":55,"lockAspectRatio":false,"lockShape":false,"children":[{"x":31.506478873239438,"y":2.4460032626429853,"rotation":0.0,"id":228,"width":44.395492957746484,"height":29.54388254486117,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":43,"lockAspectRatio":false,"lockShape":false,"children":[{"x":20.86588169014084,"y":2.637846655791175,"rotation":0.0,"id":229,"width":2.663729577464789,"height":24.268189233278818,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":52,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":232,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":232,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.3318647887323891,-1.055138662316466],[1.3318647887323891,25.3233278955953]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":36.84825915492961,"y":2.637846655791175,"rotation":0.0,"id":230,"width":1.0000000000000002,"height":25.323327895595277,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":49,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-0.8875219090985048,-1.0551386623167391],[-0.8875219090985048,25.323327895595412]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":7.103278873239435,"y":1.230995106035881,"rotation":0.0,"id":231,"width":1.0000000000000002,"height":25.323327895595277,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":46,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.2752008616871728,0.3517128874389471],[1.2752008616871728,26.73017944535047]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":1.5827079934747048,"rotation":0.0,"id":232,"width":44.395492957746484,"height":26.378466557911768,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":41,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#6fa8dc","fillColor":"#3d85c6","gradient":true,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":37.199347471451986,"rotation":0.0,"id":233,"width":107.40845070422536,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":54,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

container2 - vlan20

172.16.1.2/24

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":259.38636363636374,"y":108.14285409109937,"rotation":0.0,"id":248,"width":123.00000000000001,"height":104.0,"uid":"com.gliffy.shape.iphone.iphone_ios7.icons_glyphs.glyph_cloud","order":56,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.iphone.iphone_ios7.icons_glyphs.glyph_cloud","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#999999","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":265.14756722151094,"y":139.96785409109907,"rotation":0.0,"id":241,"width":107.40845070422536,"height":49.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":73,"lockAspectRatio":false,"lockShape":false,"children":[{"x":31.506478873239438,"y":2.4460032626429853,"rotation":0.0,"id":242,"width":44.395492957746484,"height":29.54388254486117,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":61,"lockAspectRatio":false,"lockShape":false,"children":[{"x":20.86588169014084,"y":2.637846655791175,"rotation":0.0,"id":243,"width":2.663729577464789,"height":24.268189233278818,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":70,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":246,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":246,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.3318647887323891,-1.055138662316466],[1.3318647887323891,25.3233278955953]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":36.84825915492961,"y":2.637846655791175,"rotation":0.0,"id":244,"width":1.0000000000000002,"height":25.323327895595277,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":67,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-0.8875219090985048,-1.0551386623167391],[-0.8875219090985048,25.323327895595412]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":7.103278873239435,"y":1.230995106035881,"rotation":0.0,"id":245,"width":1.0000000000000002,"height":25.323327895595277,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":64,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#0b5394","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.2752008616871728,0.3517128874389471],[1.2752008616871728,26.73017944535047]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":1.5827079934747048,"rotation":0.0,"id":246,"width":44.395492957746484,"height":26.378466557911768,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":59,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#6fa8dc","fillColor":"#3d85c6","gradient":true,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":37.199347471451986,"rotation":0.0,"id":247,"width":107.40845070422536,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":72,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

container3 - vlan30

10.1.1.2/16

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":65.0,"y":31.199996948242188,"rotation":0.0,"id":253,"width":60.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":74,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0.30

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":44.49612211422149,"y":17.874999999999943,"rotation":0.0,"id":266,"width":275.00609168449375,"height":15.70000000000006,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":75,"lockAspectRatio":false,"lockShape":false,"children":[{"x":68.50387788577851,"y":43.12500000000006,"rotation":0.0,"id":258,"width":211.0,"height":31.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":9,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#999999","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-64.00387788577851,-31.924999999999997],[197.00221379871527,-31.925000000000153]],"lockSegments":{"1":true},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":68.50387788577851,"y":38.55333333333314,"rotation":0.0,"id":262,"width":211.0,"height":33.06666666666631,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":7,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#999999","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-64.00387788577851,-34.053333333332965],[197.00221379871527,-34.05333333333314]],"lockSegments":{"1":true},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":70.50387788577851,"y":40.7533333333331,"rotation":0.0,"id":261,"width":211.0,"height":33.06666666666631,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":5,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#e06666","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-64.00387788577851,-34.053333333332965],[197.00221379871527,-34.05333333333314]],"lockSegments":{"1":true},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":70.50387788577851,"y":42.88666666666643,"rotation":0.0,"id":260,"width":211.0,"height":33.06666666666631,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":3,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#e69138","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-64.00387788577851,-34.053333333332965],[197.00221379871527,-34.05333333333314]],"lockSegments":{"1":true},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":73.50387788577851,"y":43.95333333333309,"rotation":0.0,"id":259,"width":211.0,"height":33.06666666666631,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":1,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#ffe599","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-64.00387788577851,-34.053333333332965],[197.00221379871527,-34.05333333333314]],"lockSegments":{"1":true},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":248.0,"y":51.19999694824219,"rotation":0.0,"id":207,"width":143.0,"height":70.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":33,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Network Router (gateway)

vlan10 - 192.168.1.1/24

vlan20 - 172.16.1.1/24

vlan30 - 10.1.1.1/16

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":3.0,"y":88.19999694824219,"rotation":0.0,"id":272,"width":77.99999999999999,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":76,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Docker Host

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"layers":[{"guid":"9wom3rMkTrb3","order":0,"name":"Layer 0","active":true,"locked":false,"visible":true,"nodeIndex":80}],"shapeStyles":{},"lineStyles":{"global":{"stroke":"#e06666","strokeWidth":2,"orthoMode":1}},"textStyles":{"global":{"bold":true,"face":"Arial","size":"12px","color":"#000000"}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":["com.gliffy.libraries.network.network_v4.home","com.gliffy.libraries.network.network_v4.business","com.gliffy.libraries.network.network_v4.rack","com.gliffy.libraries.network.network_v3.home","com.gliffy.libraries.network.network_v3.business","com.gliffy.libraries.network.network_v3.rack"],"lastSerialized":1457586821719,"analyticsProduct":"Confluence"},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/experimental/images/multi_tenant_8021q_vlans.png b/experimental/images/multi_tenant_8021q_vlans.png new file mode 100644 index 0000000000000000000000000000000000000000..a38633cdbc23014364bfc611d650b2a17dc72ae0 GIT binary patch literal 17879 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 -vlan10192.168.1.2/24eth0 -802.1qtrunkNetworkRouter (gateway)vlan10 -192.168.1.1/24vlan20172.16.1.1/24vlan3010.1.1.1/16eth0.10eth0.20container2 -vlan20172.16.1.2/24container3 -vlan3010.1.1.2/16eth0.30DockerHost \ No newline at end of file diff --git a/experimental/images/vlans-deeper-look.gliffy b/experimental/images/vlans-deeper-look.gliffy new file mode 100644 index 00000000..4d9f2761 --- /dev/null +++ b/experimental/images/vlans-deeper-look.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#FFFFFF","width":566,"height":581,"nodeIndex":500,"autoFit":true,"exportBorder":false,"gridOn":true,"snapToGrid":false,"drawingGuidesOn":false,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"maxWidth":5000,"maxHeight":5000,"themeData":{"uid":"com.gliffy.theme.beach_day","name":"Beach Day","shape":{"primary":{"strokeWidth":2,"strokeColor":"#00A4DA","fillColor":"#AEE4F4","gradient":false,"dropShadow":false,"opacity":1,"text":{"color":"#004257"}},"secondary":{"strokeWidth":2,"strokeColor":"#CDB25E","fillColor":"#EACF81","gradient":false,"dropShadow":false,"opacity":1,"text":{"color":"#332D1A"}},"tertiary":{"strokeWidth":2,"strokeColor":"#FFBE00","fillColor":"#FFF1CB","gradient":false,"dropShadow":false,"opacity":1,"text":{"color":"#000000"}},"highlight":{"strokeWidth":2,"strokeColor":"#00A4DA","fillColor":"#00A4DA","gradient":false,"dropShadow":false,"opacity":1,"text":{"color":"#ffffff"}}},"line":{"strokeWidth":2,"strokeColor":"#00A4DA","fillColor":"none","arrowType":2,"interpolationType":"quadratic","cornerRadius":0,"text":{"color":"#002248"}},"text":{"color":"#002248"},"stage":{"color":"#FFFFFF"}},"viewportType":"default","fitBB":{"min":{"x":-3,"y":-1.0100878848684474},"max":{"x":566,"y":581}},"printModel":{"pageSize":"a4","portrait":false,"fitToOnePage":false,"displayPageBreaks":false},"objects":[{"x":-5.0,"y":-1.0100878848684474,"rotation":0.0,"id":499,"width":569.0,"height":582.0100878848684,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":103,"lockAspectRatio":false,"lockShape":false,"children":[{"x":374.0,"y":44.510087884868476,"rotation":0.0,"id":497,"width":145.0,"height":32.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":101,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Network & other

Docker Hosts

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":157.40277777777783,"y":108.18042331083174,"rotation":0.0,"id":492,"width":121.19444444444446,"height":256.03113588084784,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":99,"lockAspectRatio":false,"lockShape":false,"children":[{"x":-126.13675213675185,"y":31.971494223140525,"rotation":180.0,"id":453,"width":11.1452323717951,"height":61.19357171974171,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":57,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":6.0,"strokeColor":"#38761d","fillColor":"#38761d","dashStyle":"1.0,1.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[-121.4915197649562,-156.36606993796556],[-121.49151976495622,-99.52846483047983],[-229.68596420939843,-99.52846483047591],[-229.68596420939843,-34.22088765589871]],"lockSegments":{"1":true},"ortho":true}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":289.82598824786317,"y":137.23816896148608,"rotation":180.0,"id":454,"width":11.1452323717951,"height":61.19357171974171,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":55,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":6.0,"strokeColor":"#38761d","fillColor":"#38761d","dashStyle":"1.0,1.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[291.05455395299924,191.93174068122784],[291.05455395299924,106.06051735724502],[186.27677617521402,106.06051735724502],[186.27677617521402,69.78655839914467]],"lockSegments":{},"ortho":true}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":372.0,"y":332.0100878848684,"rotation":0.0,"id":490,"width":144.0,"height":60.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":97,"lockAspectRatio":false,"lockShape":false,"children":[{"x":0.0,"y":9.5,"rotation":0.0,"id":365,"width":141.0,"height":40.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":98,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

 Parent: eth0.30

VLAN: 30

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":0.0,"rotation":0.0,"id":342,"width":144.0,"height":60.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":96,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#eb6c6c","gradient":false,"dashStyle":null,"dropShadow":true,"state":0,"opacity":0.99,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":52.0,"y":332.0100878848684,"rotation":0.0,"id":489,"width":144.0,"height":60.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":92,"lockAspectRatio":false,"lockShape":false,"children":[{"x":1.0,"y":10.5,"rotation":0.0,"id":367,"width":138.0,"height":40.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":93,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Parent: eth0.10

VLAN ID: 10

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":0.0,"rotation":0.0,"id":340,"width":144.0,"height":60.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":91,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#5fcc5a","gradient":false,"dashStyle":null,"dropShadow":true,"state":0,"opacity":0.99,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":289.40277777777794,"y":126.43727235088903,"rotation":0.0,"id":486,"width":121.19444444444446,"height":250.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":88,"lockAspectRatio":false,"lockShape":false,"children":[{"x":236.18596420940128,"y":158.89044937932732,"rotation":0.0,"id":449,"width":11.1452323717951,"height":59.50782702798556,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":53,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":6.0,"strokeColor":"#cc0000","fillColor":"#cc0000","dashStyle":"1.0,1.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[-121.49151976495682,-152.05853787273531],[-121.49151976495682,-81.64750068755309],[-229.68596420940125,-81.64750068755139],[-229.68596420940125,-33.27817949077674]],"lockSegments":{},"ortho":true}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":-179.77677617521388,"y":56.523633779319084,"rotation":0.0,"id":450,"width":11.1452323717951,"height":59.50782702798556,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":51,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":6.0,"strokeColor":"#cc0000","fillColor":"#cc0000","dashStyle":"1.0,1.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[291.0545539529992,186.6444547140887],[291.0545539529992,117.79470574474337],[186.276776175214,117.79470574474337],[186.276776175214,67.8640963321146]],"lockSegments":{"1":true},"ortho":true}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":447.0,"y":150.01008788486848,"rotation":0.0,"id":472,"width":46.99999999999994,"height":27.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":87,"lockAspectRatio":false,"lockShape":false,"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":473,"width":37.09803921568625,"height":18.000000000000004,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":86,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#666666","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":5.485490196078445,"y":5.153846153846132,"rotation":0.0,"id":474,"width":37.09803921568625,"height":18.000000000000004,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":84,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#666666","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":9.901960784313701,"y":9.0,"rotation":0.0,"id":475,"width":37.09803921568625,"height":18.000000000000004,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":82,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#666666","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":368.0,"y":101.71008483311067,"rotation":0.0,"id":477,"width":140.0,"height":56.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":80,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Gateway 10.1.30.1

  and other containers on the same VLAN/subnet

 

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":350.51767083236393,"y":87.47159983339776,"rotation":0.0,"id":478,"width":175.20345848455912,"height":73.0,"uid":"com.gliffy.shape.cisco.cisco_v1.storage.cloud","order":79,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.storage.cloud","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#cc0000","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":94.0,"y":155.01008788486848,"rotation":0.0,"id":463,"width":46.99999999999994,"height":27.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":78,"lockAspectRatio":false,"lockShape":false,"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":464,"width":37.09803921568625,"height":18.000000000000004,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":77,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#666666","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":5.485490196078445,"y":5.153846153846132,"rotation":0.0,"id":465,"width":37.09803921568625,"height":18.000000000000004,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":75,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#666666","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":9.901960784313701,"y":9.0,"rotation":0.0,"id":466,"width":37.09803921568625,"height":18.000000000000004,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":73,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#666666","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":80.0,"y":109.71008483311067,"rotation":0.0,"id":468,"width":140.0,"height":56.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":71,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Gateway 10.1.10.1

  and other containers on the same VLAN/subnet

 

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":62.51767083236396,"y":95.47159983339776,"rotation":0.0,"id":469,"width":175.20345848455912,"height":73.0,"uid":"com.gliffy.shape.cisco.cisco_v1.storage.cloud","order":70,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.storage.cloud","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#38761d","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":341.0,"y":40.010087884868476,"rotation":0.0,"id":460,"width":46.99999999999994,"height":27.0,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":69,"lockAspectRatio":false,"lockShape":false,"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":417,"width":37.09803921568625,"height":18.000000000000004,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":68,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#666666","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":5.485490196078445,"y":5.153846153846132,"rotation":0.0,"id":418,"width":37.09803921568625,"height":18.000000000000004,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":66,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#666666","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":9.901960784313701,"y":9.0,"rotation":0.0,"id":419,"width":37.09803921568625,"height":18.000000000000004,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":64,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#666666","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":198.51767083236396,"y":41.471599833397754,"rotation":0.0,"id":459,"width":175.20345848455912,"height":79.73848499971291,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":62,"lockAspectRatio":false,"lockShape":false,"children":[{"x":17.482329167636067,"y":14.23848499971291,"rotation":0.0,"id":458,"width":140.0,"height":56.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":61,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Gateway 10.1.20.1

  and other containers on the same VLAN/subnet

 

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":0.0,"rotation":0.0,"id":330,"width":175.20345848455912,"height":73.0,"uid":"com.gliffy.shape.cisco.cisco_v1.storage.cloud","order":59,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.storage.cloud","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ff9900","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":279.0,"y":129.01008788486848,"rotation":0.0,"id":440,"width":5.0,"height":227.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":49,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":6.0,"strokeColor":"#ff9900","fillColor":"#ff9900","dashStyle":"1.0,1.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[4.000000000000057,-25.08952732449731],[4.000000000000114,176.01117206537933]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":56.0,"y":503.0913886978766,"rotation":0.0,"id":386,"width":135.0,"height":20.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":48,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Frontend

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":62.0,"y":420.0100878848684,"rotation":0.0,"id":381,"width":120.0,"height":74.18803418803415,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":41,"lockAspectRatio":false,"lockShape":false,"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":382,"width":102.08955223880598,"height":54.91841491841488,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":44,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":true,"state":0,"opacity":0.97,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0417910447761187,"y":0.0,"rotation":0.0,"id":383,"width":98.00597014925374,"height":44.0,"uid":null,"order":47,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container(s)

Eth0 10.1.10.0/24

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":8.955223880597016,"y":9.634809634809635,"rotation":0.0,"id":384,"width":102.08955223880598,"height":54.91841491841488,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":42,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":true,"state":0,"opacity":0.97,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":17.910447761194032,"y":19.26961926961927,"rotation":0.0,"id":385,"width":102.08955223880598,"height":54.91841491841488,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":40,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":true,"state":0,"opacity":0.97,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":382.0,"y":420.0100878848684,"rotation":0.0,"id":376,"width":120.0,"height":74.18803418803415,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":31,"lockAspectRatio":false,"lockShape":false,"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":377,"width":102.08955223880598,"height":54.91841491841488,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":34,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":true,"state":0,"opacity":0.97,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0417910447761187,"y":0.0,"rotation":0.0,"id":378,"width":98.00597014925374,"height":44.0,"uid":null,"order":37,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container(s)

Eth0 10.1.30.0/24

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":8.955223880597016,"y":9.634809634809635,"rotation":0.0,"id":379,"width":102.08955223880598,"height":54.91841491841488,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":32,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":true,"state":0,"opacity":0.97,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":17.910447761194032,"y":19.26961926961927,"rotation":0.0,"id":380,"width":102.08955223880598,"height":54.91841491841488,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":30,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":true,"state":0,"opacity":0.97,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":214.0,"y":503.0100878848685,"rotation":0.0,"id":374,"width":135.0,"height":20.162601626016258,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":27,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Backend

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":376.0,"y":502.0100878848684,"rotation":0.0,"id":373,"width":135.0,"height":20.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":26,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Credit Cards

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":627.0,"y":99.94304076572786,"rotation":0.0,"id":364,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.uml.uml_v2.sequence.anchor_line","order":25,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":363,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":342,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-183.0,310.0670471191406],[-183.0,292.0670471191406]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":372.0,"y":410.0100878848684,"rotation":0.0,"id":363,"width":144.0,"height":117.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":24,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#eb6c6c","gradient":false,"dashStyle":null,"dropShadow":true,"state":0,"opacity":0.99,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":218.0,"y":341.5100878848684,"rotation":0.0,"id":366,"width":132.0,"height":40.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":23,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Parent: eth0.20

VLAN ID: 20

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":297.0,"y":89.94304076572786,"rotation":0.0,"id":356,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.uml.uml_v2.sequence.anchor_line","order":22,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":353,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":343,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-13.0,320.0670471191406],[-13.0,302.0670471191406]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":222.0,"y":420.0100878848684,"rotation":0.0,"id":348,"width":120.0,"height":74.18803418803415,"uid":"com.gliffy.shape.basic.basic_v1.default.group","order":21,"lockAspectRatio":false,"lockShape":false,"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":349,"width":102.08955223880598,"height":54.91841491841488,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":17,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":true,"state":0,"opacity":0.97,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0417910447761187,"y":0.0,"rotation":0.0,"id":350,"width":98.00597014925374,"height":44.0,"uid":null,"order":20,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container(s)

Eth0 10.1.20.0/24

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":8.955223880597016,"y":9.634809634809635,"rotation":0.0,"id":351,"width":102.08955223880598,"height":54.91841491841488,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":15,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":true,"state":0,"opacity":0.97,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":17.910447761194032,"y":19.26961926961927,"rotation":0.0,"id":352,"width":102.08955223880598,"height":54.91841491841488,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":13,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#4cacf5","gradient":false,"dashStyle":null,"dropShadow":true,"state":0,"opacity":0.97,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":212.0,"y":410.0100878848684,"rotation":0.0,"id":353,"width":144.0,"height":119.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":11,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#fca13f","gradient":false,"dashStyle":null,"dropShadow":true,"state":0,"opacity":0.99,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":212.0,"y":332.0100878848684,"rotation":0.0,"id":343,"width":144.0,"height":60.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":10,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#fca13f","gradient":false,"dashStyle":null,"dropShadow":true,"state":0,"opacity":0.99,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":203.0,"y":307.5100878848684,"rotation":0.0,"id":333,"width":160.0,"height":22.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":9,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 Interface

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":303.0,"y":240.51008788486845,"rotation":0.0,"id":323,"width":261.0,"height":48.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":8,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

802.1Q Trunk - can be a single Ethernet link or Multiple Bonded Ethernet links

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":36.0,"y":291.0100878848684,"rotation":0.0,"id":290,"width":497.0,"height":80.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":7,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#cccccc","gradient":false,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":0.0,"y":543.5100878848684,"rotation":0.0,"id":282,"width":569.0,"height":32.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":6,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Docker Host: Frontend, Backend & Credit Card App Tiers are Isolated but can still communicate inside parent interface or any other Docker hosts using the VLAN ID

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":-33.0,"y":79.94304076572786,"rotation":0.0,"id":269,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.uml.uml_v2.sequence.anchor_line","order":5,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":345,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":340,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[157.0,330.0670471191406],[157.0,312.0670471191406]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":52.0,"y":410.0100878848684,"rotation":0.0,"id":345,"width":144.0,"height":119.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":4,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#434343","fillColor":"#5fcc5a","gradient":false,"dashStyle":null,"dropShadow":true,"state":0,"opacity":0.99,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":20.0,"y":323.0100878848684,"rotation":0.0,"id":276,"width":531.0,"height":259.0,"uid":"com.gliffy.shape.basic.basic_v1.default.round_rectangle","order":3,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.round_rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#434343","fillColor":"#c5e4fc","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":0.93,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":19.609892022503004,"y":20.27621073737908,"rotation":355.62347411485274,"id":246,"width":540.0106597126834,"height":225.00000000000003,"uid":"com.gliffy.shape.cisco.cisco_v1.storage.cloud","order":2,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.cisco.cisco_v1.storage.cloud","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#999999","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":1.0,"y":99.94304076572786,"rotation":0.0,"id":394,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.uml.uml_v2.sequence.anchor_line","order":1,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":3.0,"strokeColor":"#666666","fillColor":"#999999","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[261.0,233.5670471191406],[261.0,108.05111187584177]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"},{"x":44.0,"y":90.94304076572786,"rotation":0.0,"id":481,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.uml.uml_v2.sequence.anchor_line","order":0,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":3.0,"strokeColor":"#666666","fillColor":"#999999","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[261.0,233.56704711914062],[261.0,108.05111187584174]],"lockSegments":{},"ortho":false}},"linkMap":[],"hidden":false,"layerId":"9wom3rMkTrb3"}],"hidden":false,"layerId":"9wom3rMkTrb3"}],"layers":[{"guid":"9wom3rMkTrb3","order":0,"name":"Layer 0","active":true,"locked":false,"visible":true,"nodeIndex":104}],"shapeStyles":{},"lineStyles":{"global":{"fill":"#999999","stroke":"#38761d","strokeWidth":3,"dashStyle":"1.0,1.0","orthoMode":2}},"textStyles":{"global":{"bold":true,"face":"Arial","size":"14px","color":"#000000"}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":["com.gliffy.libraries.basic.basic_v1.default","com.gliffy.libraries.flowchart.flowchart_v1.default","com.gliffy.libraries.swimlanes.swimlanes_v1.default","com.gliffy.libraries.images","com.gliffy.libraries.network.network_v4.home","com.gliffy.libraries.network.network_v4.business","com.gliffy.libraries.network.network_v4.rack","com.gliffy.libraries.network.network_v3.home","com.gliffy.libraries.network.network_v3.business","com.gliffy.libraries.network.network_v3.rack"],"lastSerialized":1458117295143,"analyticsProduct":"Confluence"},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/experimental/images/vlans-deeper-look.png b/experimental/images/vlans-deeper-look.png new file mode 100644 index 0000000000000000000000000000000000000000..32d95f600e1d0f028e5a354584d7b3eac1639e35 GIT binary patch literal 38837 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
a|h4b@&t6RTTq&{B^&KttO8_~^T5gW5$(YwQ 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 N002ovPDHLkV1f$s#LfT! literal 0 HcmV?d00001 diff --git a/experimental/images/vlans-deeper-look.svg b/experimental/images/vlans-deeper-look.svg new file mode 100644 index 00000000..96cd21d5 --- /dev/null +++ b/experimental/images/vlans-deeper-look.svg @@ -0,0 +1 @@ +DockerHost:Frontend,Backend &CreditCardAppTiersareIsolatedbutcanstillcommunicateinsideinterfaceoranyotherDockerhostsusingtheparentVLANID802.1QTrunk -canbeasingleEthernetlinkorMultipleBondedEthernetlinksInterfaceeth0Container(s)Eth010.1.20.0/24Parent:eth0.20VLANID:20CreditCardsBackendContainer(s)Eth010.1.30.0/24Container(s)Eth010.1.10.0/24FrontendGateway10.1.20.1andothercontainersonthesameVLAN/subnetGateway10.1.10.1andothercontainersonthesameVLAN/subnetGateway10.1.30.1andothercontainersonthesameVLAN/subnet:Parenteth0.10VLANID:10Parent:eth0.30VLAN:30NetworkotherDockerHosts \ No newline at end of file diff --git a/experimental/plugins_graphdriver.md b/experimental/plugins_graphdriver.md new file mode 100644 index 00000000..6a855489 --- /dev/null +++ b/experimental/plugins_graphdriver.md @@ -0,0 +1,321 @@ +# Experimental: Docker graph driver plugins + +Docker graph driver plugins enable admins to use an external/out-of-process +graph driver for use with Docker engine. This is an alternative to using the +built-in storage drivers, such as aufs/overlay/devicemapper/btrfs. + +A graph driver plugin is used for image and container fs storage, as such +the plugin must be started and available for connections prior to Docker Engine +being started. + +# Write a graph driver plugin + +See the [plugin documentation](/docs/extend/plugins.md) for detailed information +on the underlying plugin protocol. + + +## Graph Driver plugin protocol + +If a plugin registers itself as a `GraphDriver` when activated, then it is +expected to provide the rootfs for containers as well as image layer storage. + +### /GraphDriver.Init + +**Request**: +``` +{ + "Home": "/graph/home/path", + "Opts": [] +} +``` + +Initialize the graph driver plugin with a home directory and array of options. +Plugins are not required to accept these options as the Docker Engine does not +require that the plugin use this path or options, they are only being passed +through from the user. + +**Response**: +``` +{ + "Err": null +} +``` + +Respond with a string error if an error occurred. + + +### /GraphDriver.Create + +**Request**: +``` +{ + "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187", + "Parent": "2cd9c322cb78a55e8212aa3ea8425a4180236d7106938ec921d0935a4b8ca142" +} +``` + +Create a new, empty, filesystem layer with the specified `ID` and `Parent`. +`Parent` may be an empty string, which would indicate that there is no parent +layer. + +**Response**: +``` +{ + "Err: null +} +``` + +Respond with a string error if an error occurred. + + +### /GraphDriver.Remove + +**Request**: +``` +{ + "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187" +} +``` + +Remove the filesystem layer with this given `ID`. + +**Response**: +``` +{ + "Err: null +} +``` + +Respond with a string error if an error occurred. + +### /GraphDriver.Get + +**Request**: +``` +{ + "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187" + "MountLabel": "" +} +``` + +Get the mountpoint for the layered filesystem referred to by the given `ID`. + +**Response**: +``` +{ + "Dir": "/var/mygraph/46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187", + "Err": "" +} +``` + +Respond with the absolute path to the mounted layered filesystem. +Respond with a string error if an error occurred. + +### /GraphDriver.Put + +**Request**: +``` +{ + "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187" +} +``` + +Release the system resources for the specified `ID`, such as unmounting the +filesystem layer. + +**Response**: +``` +{ + "Err: null +} +``` + +Respond with a string error if an error occurred. + +### /GraphDriver.Exists + +**Request**: +``` +{ + "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187" +} +``` + +Determine if a filesystem layer with the specified `ID` exists. + +**Response**: +``` +{ + "Exists": true +} +``` + +Respond with a boolean for whether or not the filesystem layer with the specified +`ID` exists. + +### /GraphDriver.Status + +**Request**: +``` +{} +``` + +Get low-level diagnostic information about the graph driver. + +**Response**: +``` +{ + "Status": [[]] +} +``` + +Respond with a 2-D array with key/value pairs for the underlying status +information. + + +### /GraphDriver.GetMetadata + +**Request**: +``` +{ + "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187" +} +``` + +Get low-level diagnostic information about the layered filesystem with the +with the specified `ID` + +**Response**: +``` +{ + "Metadata": {}, + "Err": null +} +``` + +Respond with a set of key/value pairs containing the low-level diagnostic +information about the layered filesystem. +Respond with a string error if an error occurred. + +### /GraphDriver.Cleanup + +**Request**: +``` +{} +``` + +Perform necessary tasks to release resources help by the plugin, for example +unmounting all the layered file systems. + +**Response**: +``` +{ + "Err: null +} +``` + +Respond with a string error if an error occurred. + + +### /GraphDriver.Diff + +**Request**: +``` +{ + "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187", + "Parent": "2cd9c322cb78a55e8212aa3ea8425a4180236d7106938ec921d0935a4b8ca142" +} +``` + +Get an archive of the changes between the filesystem layers specified by the `ID` +and `Parent`. `Parent` may be an empty string, in which case there is no parent. + +**Response**: +``` +{{ TAR STREAM }} +``` + +### /GraphDriver.Changes + +**Request**: +``` +{ + "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187", + "Parent": "2cd9c322cb78a55e8212aa3ea8425a4180236d7106938ec921d0935a4b8ca142" +} +``` + +Get a list of changes between the filesystem layers specified by the `ID` and +`Parent`. `Parent` may be an empty string, in which case there is no parent. + +**Response**: +``` +{ + "Changes": [{}], + "Err": null +} +``` + +Responds with a list of changes. The structure of a change is: +``` + "Path": "/some/path", + "Kind": 0, +``` + +Where the `Path` is the filesystem path within the layered filesystem that is +changed and `Kind` is an integer specifying the type of change that occurred: + +- 0 - Modified +- 1 - Added +- 2 - Deleted + +Respond with a string error if an error occurred. + +### /GraphDriver.ApplyDiff + +**Request**: +``` +{{ TAR STREAM }} +``` + +Extract the changeset from the given diff into the layer with the specified `ID` +and `Parent` + +**Query Parameters**: + +- id (required)- the `ID` of the new filesystem layer to extract the diff to +- parent (required)- the `Parent` of the given `ID` + +**Response**: +``` +{ + "Size": 512366, + "Err": null +} +``` + +Respond with the size of the new layer in bytes. +Respond with a string error if an error occurred. + +### /GraphDriver.DiffSize + +**Request**: +``` +{ + "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187", + "Parent": "2cd9c322cb78a55e8212aa3ea8425a4180236d7106938ec921d0935a4b8ca142" +} +``` + +Calculate the changes between the specified `ID` + +**Response**: +``` +{ + "Size": 512366, + "Err": null +} +``` + +Respond with the size changes between the specified `ID` and `Parent` +Respond with a string error if an error occurred. diff --git a/experimental/vlan-networks.md b/experimental/vlan-networks.md new file mode 100644 index 00000000..66723b9b --- /dev/null +++ b/experimental/vlan-networks.md @@ -0,0 +1,721 @@ + + +# Macvlan and Ipvlan Network Drivers + +### Getting Started + +The Macvlan and Ipvlan drivers are currently in experimental mode in order to incubate Docker users use cases and vet the implementation to ensure a hardened, production ready driver in a future release. Libnetwork now gives users total control over both IPv4 and IPv6 addressing. The VLAN drivers build on top of that in giving operators complete control of layer 2 VLAN tagging and even Ipvlan L3 routing for users interested in underlay network integration. For overlay deployments that abstract away physical constraints see the [multi-host overlay ](https://docs.docker.com/engine/userguide/networking/get-started-overlay/) driver. + +Macvlan and Ipvlan are a new twist on the tried and true network virtualization technique. The Linux implementations are extremely lightweight because rather than using the traditional Linux bridge for isolation, they are simply associated to a Linux Ethernet interface or sub-interface to enforce separation between networks and connectivity to the physical network. + +Macvlan and Ipvlan offer a number of unique features and plenty of room for further innovations with the various modes. Two high level advantages of these approaches are, the positive performance implications of bypassing the Linux bridge and the simplicity of having less moving parts. Removing the bridge that traditionally resides in between the Docker host NIC and container interface leaves a very simple setup consisting of container interfaces, attached directly to the Docker host interface. This result is easy access for external facing services as there is no port mappings in these scenarios. + + +### Pre-Requisites + +- The examples on this page are all single host and setup using Docker experimental builds that can be installed with the following instructions: [Install Docker experimental](https://github.com/docker/docker/tree/master/experimental) + +- All of the examples can be performed on a single host running Docker. Any examples using a sub-interface like `eth0.10` can be replaced with `eth0` or any other valid parent interface on the Docker host. Sub-interfaces with a `.` are created on the fly. `-o parent` interfaces can also be left out of the `docker network create` all together and the driver will create a `dummy` interface that will enable local host connectivity to perform the examples. + +- Kernel requirements: + + - To check your current kernel version, use `uname -r` to display your kernel version + - Macvlan Linux kernel v3.9–3.19 and 4.0+ + - Ipvlan Linux kernel v4.2+ (support for earlier kernels exists but is buggy) + + +### MacVlan Bridge Mode Example Usage + +Macvlan Bridge mode has a unique MAC address per container used to track MAC to port mappings by the Docker host. This is the largest difference from Ipvlan L2 mode which uses the same MAC address as the parent interface for each container `eth0` interface. + +- Macvlan and Ipvlan driver networks are attached to a parent Docker host interface. Examples are a physical interface such as `eth0`, a sub-interface for 802.1q VLAN tagging like `eth0.10` (`.10` representing VLAN `10`) or even bonded host adaptors which bundle two Ethernet interfaces into a single logical interface. + +- The specified gateway is external to the host provided by the network infrastructure. + +- Each Macvlan Bridge mode Docker network is isolated from one another and there can be only one network attached to a parent interface at a time. There is a theoretical limit of 4,094 sub-interfaces per host adaptor that a Docker network could be attached to. + +- It is not recommended to mix ipvlan and macvlan networks on the same `-o parent=` interface. Older kernel versions will throw uninformative netlink errors such as `device is busy`. + +- Any container inside the same subnet can talk any other container in the same network without a gateway in both `macvlan bridge` mode and `ipvlan L2` modes. + +- The same `docker network` commands apply to the vlan drivers. Some are irrelevant such as `-icc` or `--set-macaddress` for the Ipvlan driver. + +- In Macvlan and Ipvlan L2 mode, containers on separate networks cannot reach one another without an external process routing between the two networks/subnets. This also applies to multiple subnets within the same `docker network`. See Ipvlan L3 mode for inter-subnet communications without a router. + +In the following example, `eth0` on the docker host has an IP on the `172.16.86.0/24` network and a default gateway of `172.16.86.1`. The gateway is an external router with an address of `172.16.86.1`. An IP address is not required on the Docker host interface `eth0` in `bridge` mode, it merely needs to be on the proper upstream network to get forwarded by a network switch or network router. + +![Simple Macvlan Bridge Mode Example](images/macvlan_bridge_simple.png) + + +**Note** For Macvlan bridge mode and Ipvlan L2 mode the subnet values need to match the NIC's interface of the Docker host. For example, Use the same subnet and gateway of the Docker host ethernet interface that is specified by the `-o parent=` option. + +- The parent interface used in this example is `eth0` and it is on the subnet `172.16.86.0/24`. The containers in the `docker network` will also need to be on this same subnet as the parent `-o parent=`. The gateway is an external router on the network, not any ip masquerading or any other local proxy. + +- The driver is specified with `-d driver_name` option. In this case `-d macvlan` + +- The parent interface `-o parent=eth0` is configured as followed: + +``` +ip addr show eth0 +3: eth0: mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 + inet 172.16.86.250/24 brd 172.16.86.255 scope global eth0 +``` + +Create the macvlan network and run a couple of containers attached to it: + +``` +# Macvlan (-o macvlan_mode= Defaults to Bridge mode if not specified) +docker network create -d macvlan \ + --subnet=172.16.86.0/24 \ + --gateway=172.16.86.1 \ + -o parent=eth0 pub_net + +# Run a container on the new network specifying the --ip address. +docker run --net=pub_net --ip=172.16.86.10 -itd alpine /bin/sh + +# Start a second container and ping the first +docker run --net=pub_net -it --rm alpine /bin/sh +ping -c 4 172.16.86.10 + +``` + + Take a look at the containers ip and routing table: + +``` + +ip a show eth0 + eth0@if3: mtu 1500 qdisc noqueue state UNKNOWN + link/ether 46:b2:6b:26:2f:69 brd ff:ff:ff:ff:ff:ff + inet 172.16.86.2/24 scope global eth0 + +ip route + default via 172.16.86.1 dev eth0 + 172.16.86.0/24 dev eth0 src 172.16.86.2 + +# NOTE: the containers can NOT ping the underlying host interfaces as +# they are intentionally filtered by Linux for additional isolation. +# In this case the containers cannot ping the -o parent=172.16.86.250 +``` + + +You can explicitly specify the `bridge` mode option `-o macvlan_mode=bridge`. It is the default so will be in `bridge` mode either way. + +While the `eth0` interface does not need to have an IP address in Macvlan Bridge mode or Ipvlan L2 mode it is not uncommon to have an IP address on the interface. Addresses can be excluded from getting an address from the default built in IPAM by using the `--aux-address=x.x.x.x` flag. This will blacklist the specified address from being handed out to containers. The same network example above blocking the `-o parent=eth0` address from being handed out to a container. + +``` +docker network create -d macvlan \ + --subnet=172.16.86.0/24 \ + --gateway=172.16.86.1 \ + --aux-address="exclude_host=172.16.86.250" \ + -o parent=eth0 pub_net +``` + +Another option for subpool IP address selection in a network provided by the default Docker IPAM driver is to use `--ip-range=`. This specifies the driver to allocate container addresses from this pool rather then the broader range from the `--subnet=` argument from a network create as seen in the following example that will allocate addresses beginning at `192.168.32.128` and increment upwards from there. + +``` +docker network create -d macvlan \ + --subnet=192.168.32.0/24 \ + --ip-range=192.168.32.128/25 \ + --gateway=192.168.32.254 \ + -o parent=eth0 macnet32 + +# Start a container and verify the address is 192.168.32.128 +docker run --net=macnet32 -it --rm alpine /bin/sh +``` + +The network can then be deleted with: + +``` +docker network rm +``` + +- **Note:** In both Macvlan and Ipvlan you are not able to ping or communicate with the default namespace IP address. For example, if you create a container and try to ping the Docker host's `eth0` it will **not** work. That traffic is explicitly filtered by the kernel modules themselves to offer additional provider isolation and security. + +For more on Docker networking commands see [Working with Docker network commands](https://docs.docker.com/engine/userguide/networking/work-with-networks/) + +### Ipvlan L2 Mode Example Usage + +The ipvlan `L2` mode example is virtually identical to the macvlan `bridge` mode example. The driver is specified with `-d driver_name` option. In this case `-d ipvlan` + +![Simple Ipvlan L2 Mode Example](images/ipvlan_l2_simple.png) + +The parent interface in the next example `-o parent=eth0` is configured as followed: + +``` +ip addr show eth0 +3: eth0: mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 + inet 192.168.1.250/24 brd 192.168.1.255 scope global eth0 +``` + +Use the network from the host's interface as the `--subnet` in the `docker network create`. The container will be attached to the same network as the host interface as set via the `-o parent=` option. + +Create the ipvlan network and run a container attaching to it: + +``` +# Ipvlan (-o ipvlan_mode= Defaults to L2 mode if not specified) +docker network create -d ipvlan \ + --subnet=192.168.1.0/24 \ + --gateway=192.168.1.1 \ + -o ipvlan_mode=l2 \ + -o parent=eth0 db_net + +# Start a container on the db_net network +docker run --net=db_net -it --rm alpine /bin/sh + +# NOTE: the containers can NOT ping the underlying host interfaces as +# they are intentionally filtered by Linux for additional isolation. +``` + +The default mode for Ipvlan is `l2`. The default mode for Macvlan is `bridge`. If `-o ipvlan_mode=` or `-o macvlan_mode=` are left unspecified, the default modes will be used. Similarly, if the `--gateway` is left empty, the first usable address on the network will be set as the gateway. For example, if the subnet provided in the network create is `--subnet=192.168.1.0/24` then the gateway the container receives is `192.168.1.1`. + +To help understand how this mode interacts with other hosts, the following figure shows the same layer 2 segment between two Docker hosts that applies to both Macvlan Bride mode and Ipvlan L2 mode. + +![Multiple Ipvlan and Macvlan Hosts](images/macvlan-bridge-ipvlan-l2.png) + +The following will create the exact same network as the network `db_net` created prior, with the driver defaults for `--gateway=192.168.1.1` and `-o ipvlan_mode=l2`. + +``` +# Ipvlan (-o ipvlan_mode= Defaults to L2 mode if not specified) +docker network create -d ipvlan \ + --subnet=192.168.1.0/24 \ + -o parent=eth0 db_net_ipv + +# Start a container with an explicit name in daemon mode +docker run --net=db_net_ipv --name=ipv1 -itd alpine /bin/sh + +# Start a second container and ping using the container name +# to see the docker included name resolution functionality +docker run --net=db_net_ipv --name=ipv2 -it --rm alpine /bin/sh +ping -c 4 ipv1 + +# NOTE: the containers can NOT ping the underlying host interfaces as +# they are intentionally filtered by Linux for additional isolation. +``` + +The drivers also support the `--internal` flag that will completely isolate containers on a network from any communications external to that network. Since network isolation is tightly coupled to the network's parent interface the result of leaving the `-o parent=` option off of a network create is the exact same as the `--internal` option. If the parent interface is not specified or the `--internal` flag is used, a netlink type `dummy` parent interface is created for the user and used as the parent interface effectively isolating the network completely. + +The following two `docker network create` examples result in identical networks that you can attach container to: + +``` +# Empty '-o parent=' creates an isolated network +docker network create -d ipvlan \ + --subnet=192.168.10.0/24 isolated1 + +# Explicit '--internal' flag is the same: +docker network create -d ipvlan \ + --subnet=192.168.11.0/24 --internal isolated2 + +# Even the '--subnet=' can be left empty and the default +# IPAM subnet of 172.18.0.0/16 will be assigned +docker network create -d ipvlan isolated3 + +docker run --net=isolated1 --name=cid1 -it --rm alpine /bin/sh +docker run --net=isolated2 --name=cid2 -it --rm alpine /bin/sh +docker run --net=isolated3 --name=cid3 -it --rm alpine /bin/sh + +# To attach to any use `docker exec` and start a shell +docker exec -it cid1 /bin/sh +docker exec -it cid2 /bin/sh +docker exec -it cid3 /bin/sh +``` + +### Macvlan 802.1q Trunk Bridge Mode Example Usage + +VLANs (Virtual Local Area Networks) have long been a primary means of virtualizing data center networks and are still in virtually all existing networks today. VLANs work by tagging a Layer-2 isolation domain with a 12-bit identifier ranging from 1-4094 that is inserted into a packet header that enables a logical grouping of a single or multiple subnets of both IPv4 and IPv6. It is very common for network operators to separate traffic using VLANs based on a subnet(s) function or security profile such as `web`, `db` or any other isolation needs. + +It is very common to have a compute host requirement of running multiple virtual networks concurrently on a host. Linux networking has long supported VLAN tagging, also known by it's standard 802.1q, for maintaining datapath isolation between networks. The Ethernet link connected to a Docker host can be configured to support the 802.1q VLAN IDs, by creating Linux sub-interfaces, each one dedicated to a unique VLAN ID. + +![Simple Ipvlan L2 Mode Example](images/multi_tenant_8021q_vlans.png) + +Trunking 802.1q to a Linux host is notoriously painful for many in operations. It requires configuration file changes in order to be persistent through a reboot. If a bridge is involved, a physical NIC needs to be moved into the bridge and the bridge then gets the IP address. This has lead to many a stranded servers since the risk of cutting off access during that convoluted process is high. + +Like all of the Docker network drivers, the overarching goal is to alleviate the operational pains of managing network resources. To that end, when a network receives a sub-interface as the parent that does not exist, the drivers create the VLAN tagged interfaces while creating the network. + +In the case of a host reboot, instead of needing to modify often complex network configuration files the driver will recreate all network links when the Docker daemon restarts. The driver tracks if it created the VLAN tagged sub-interface originally with the network create and will **only** recreate the sub-interface after a restart or delete `docker network rm` the link if it created it in the first place with `docker network create`. + +If the user doesn't want Docker to modify the `-o parent` sub-interface, the user simply needs to pass an existing link that already exists as the parent interface. Parent interfaces such as `eth0` are not deleted, only sub-interfaces that are not master links. + +For the driver to add/delete the vlan sub-interfaces the format needs to be `interface_name.vlan_tag`. + +For example: `eth0.50` denotes a parent interface of `eth0` with a slave of `eth0.50` tagged with vlan id `50`. The equivalent `ip link` command would be `ip link add link eth0 name eth0.50 type vlan id 50`. + +Replace the `macvlan` with `ipvlan` in the `-d` driver argument to create macvlan 802.1q trunks. + +**Vlan ID 50** + +In the first network tagged and isolated by the Docker host, `eth0.50` is the parent interface tagged with vlan id `50` specified with `-o parent=eth0.50`. Other naming formats can be used, but the links need to be added and deleted manually using `ip link` or Linux configuration files. As long as the `-o parent` exists anything can be used if compliant with Linux netlink. + +``` +# now add networks and hosts as you would normally by attaching to the master (sub)interface that is tagged +docker network create -d macvlan \ + --subnet=192.168.50.0/24 \ + --gateway=192.168.50.1 \ + -o parent=eth0.50 macvlan50 + +# In two separate terminals, start a Docker container and the containers can now ping one another. +docker run --net=macvlan50 -it --name macvlan_test5 --rm alpine /bin/sh +docker run --net=macvlan50 -it --name macvlan_test6 --rm alpine /bin/sh +``` + +**Vlan ID 60** + +In the second network, tagged and isolated by the Docker host, `eth0.60` is the parent interface tagged with vlan id `60` specified with `-o parent=eth0.60`. The `macvlan_mode=` defaults to `macvlan_mode=bridge`. It can also be explicitly set with the same result as shown in the next example. + +``` +# now add networks and hosts as you would normally by attaching to the master (sub)interface that is tagged. +docker network create -d macvlan \ + --subnet=192.168.60.0/24 \ + --gateway=192.168.60.1 \ + -o parent=eth0.60 -o \ + -o macvlan_mode=bridge macvlan60 + +# In two separate terminals, start a Docker container and the containers can now ping one another. +docker run --net=macvlan60 -it --name macvlan_test7 --rm alpine /bin/sh +docker run --net=macvlan60 -it --name macvlan_test8 --rm alpine /bin/sh +``` + +**Example:** Multi-Subnet Macvlan 802.1q Trunking + +The same as the example before except there is an additional subnet bound to the network that the user can choose to provision containers on. In MacVlan/Bridge mode, containers can only ping one another if they are on the same subnet/broadcast domain unless there is an external router that routes the traffic (answers ARP etc) between the two subnets. + +``` +### Create multiple L2 subnets +docker network create -d ipvlan \ + --subnet=192.168.210.0/24 \ + --subnet=192.168.212.0/24 \ + --gateway=192.168.210.254 \ + --gateway=192.168.212.254 \ + -o ipvlan_mode=l2 ipvlan210 + +# Test 192.168.210.0/24 connectivity between containers +docker run --net=ipvlan210 --ip=192.168.210.10 -itd alpine /bin/sh +docker run --net=ipvlan210 --ip=192.168.210.9 -it --rm alpine ping -c 2 192.168.210.10 + +# Test 192.168.212.0/24 connectivity between containers +docker run --net=ipvlan210 --ip=192.168.212.10 -itd alpine /bin/sh +docker run --net=ipvlan210 --ip=192.168.212.9 -it --rm alpine ping -c 2 192.168.212.10 + +``` + +### Ipvlan 802.1q Trunk L2 Mode Example Usage + +Architecturally, Ipvlan L2 mode trunking is the same as Macvlan with regard to gateways and L2 path isolation. There are nuances that can be advantageous for CAM table pressure in ToR switches, one MAC per port and MAC exhaustion on a host's parent NIC to name a few. The 802.1q trunk scenario looks the same. Both modes adhere to tagging standards and have seamless integration with the physical network for underlay integration and hardware vendor plugin integrations. + +Hosts on the same VLAN are typically on the same subnet and almost always are grouped together based on their security policy. In most scenarios, a multi-tier application is tiered into different subnets because the security profile of each process requires some form of isolation. For example, hosting your credit card processing on the same virtual network as the frontend webserver would be a regulatory compliance issue, along with circumventing the long standing best practice of layered defense in depth architectures. VLANs or the equivocal VNI (Virtual Network Identifier) when using the Overlay driver, are the first step in isolating tenant traffic. + +![Docker VLANs in Depth](images/vlans-deeper-look.png) + +The Linux sub-interface tagged with a vlan can either already exist or will be created when you call a `docker network create`. `docker network rm` will delete the sub-interface. Parent interfaces such as `eth0` are not deleted, only sub-interfaces with a netlink parent index > 0. + +For the driver to add/delete the vlan sub-interfaces the format needs to be `interface_name.vlan_tag`. Other sub-interface naming can be used as the specified parent, but the link will not be deleted automatically when `docker network rm` is invoked. + +The option to use either existing parent vlan sub-interfaces or let Docker manage them enables the user to either completely manage the Linux interfaces and networking or let Docker create and delete the Vlan parent sub-interfaces (netlink `ip link`) with no effort from the user. + +For example: `eth0.10` to denote a sub-interface of `eth0` tagged with vlan id `10`. The equivalent `ip link` command would be `ip link add link eth0 name eth0.10 type vlan id 10`. + +The example creates the vlan tagged networks and then start two containers to test connectivity between containers. Different Vlans cannot ping one another without a router routing between the two networks. The default namespace is not reachable per ipvlan design in order to isolate container namespaces from the underlying host. + +**Vlan ID 20** + +In the first network tagged and isolated by the Docker host, `eth0.20` is the parent interface tagged with vlan id `20` specified with `-o parent=eth0.20`. Other naming formats can be used, but the links need to be added and deleted manually using `ip link` or Linux configuration files. As long as the `-o parent` exists anything can be used if compliant with Linux netlink. + +``` +# now add networks and hosts as you would normally by attaching to the master (sub)interface that is tagged +docker network create -d ipvlan \ + --subnet=192.168.20.0/24 \ + --gateway=192.168.20.1 \ + -o parent=eth0.20 ipvlan20 + +# in two separate terminals, start a Docker container and the containers can now ping one another. +docker run --net=ipvlan20 -it --name ivlan_test1 --rm alpine /bin/sh +docker run --net=ipvlan20 -it --name ivlan_test2 --rm alpine /bin/sh +``` + +**Vlan ID 30** + +In the second network, tagged and isolated by the Docker host, `eth0.30` is the parent interface tagged with vlan id `30` specified with `-o parent=eth0.30`. The `ipvlan_mode=` defaults to l2 mode `ipvlan_mode=l2`. It can also be explicitly set with the same result as shown in the next example. + +``` +# now add networks and hosts as you would normally by attaching to the master (sub)interface that is tagged. +docker network create -d ipvlan \ + --subnet=192.168.30.0/24 \ + --gateway=192.168.30.1 \ + -o parent=eth0.30 \ + -o ipvlan_mode=l2 ipvlan30 + +# in two separate terminals, start a Docker container and the containers can now ping one another. +docker run --net=ipvlan30 -it --name ivlan_test3 --rm alpine /bin/sh +docker run --net=ipvlan30 -it --name ivlan_test4 --rm alpine /bin/sh +``` + +The gateway is set inside of the container as the default gateway. That gateway would typically be an external router on the network. + +``` +$ ip route + default via 192.168.30.1 dev eth0 + 192.168.30.0/24 dev eth0 src 192.168.30.2 +``` + +Example: Multi-Subnet Ipvlan L2 Mode starting two containers on the same subnet and pinging one another. In order for the `192.168.114.0/24` to reach `192.168.116.0/24` it requires an external router in L2 mode. L3 mode can route between subnets that share a common `-o parent=`. This same multi-subnet example is also valid for Macvlan `bridge` mode. + +Secondary addresses on network routers are common as an address space becomes exhausted to add another secondary to a L3 vlan interface or commonly referred to as a "switched virtual interface" (SVI). + +``` +docker network create -d ipvlan \ + --subnet=192.168.114.0/24 --subnet=192.168.116.0/24 \ + --gateway=192.168.114.254 --gateway=192.168.116.254 \ + -o parent=eth0.114 \ + -o ipvlan_mode=l2 ipvlan114 + +docker run --net=ipvlan114 --ip=192.168.114.10 -it --rm alpine /bin/sh +docker run --net=ipvlan114 --ip=192.168.114.11 -it --rm alpine /bin/sh +``` + +A key takeaway is, operators have the ability to map their physical network into their virtual network for integrating containers into their environment with no operational overhauls required. NetOps simply drops an 802.1q trunk into the Docker host. That virtual link would be the `-o parent=` passed in the network creation. For untagged (non-VLAN) links, it is as simple as `-o parent=eth0` or for 802.1q trunks with VLAN IDs each network gets mapped to the corresponding VLAN/Subnet from the network. + +An example being, NetOps provides VLAN ID and the associated subnets for VLANs being passed on the Ethernet link to the Docker host server. Those values are simply plugged into the `docker network create` commands when provisioning the Docker networks. These are persistent configurations that are applied every time the Docker engine starts which alleviates having to manage often complex configuration files. The network interfaces can also be managed manually by being pre-created and docker networking will never modify them, simply use them as parent interfaces. Example mappings from NetOps to Docker network commands are as follows: + +- VLAN: 10, Subnet: 172.16.80.0/24, Gateway: 172.16.80.1 + + - `--subnet=172.16.80.0/24 --gateway=172.16.80.1 -o parent=eth0.10` + +- VLAN: 20, IP subnet: 172.16.50.0/22, Gateway: 172.16.50.1 + + - `--subnet=172.16.50.0/22 --gateway=172.16.50.1 -o parent=eth0.20 ` + +- VLAN: 30, Subnet: 10.1.100.0/16, Gateway: 10.1.100.1 + + - `--subnet=10.1.100.0/16 --gateway=10.1.100.1 -o parent=eth0.30` + +### IPVlan L3 Mode Example + +IPVlan will require routes to be distributed to each endpoint. The driver only builds the Ipvlan L3 mode port and attaches the container to the interface. Route distribution throughout a cluster is beyond the initial implementation of this single host scoped driver. In L3 mode, the Docker host is very similar to a router starting new networks in the container. They are on networks that the upstream network will not know about without route distribution. For those curious how Ipvlan L3 will fit into container networking see the following examples. + +![Docker Ipvlan L2 Mode](images/ipvlan-l3.png) + +Ipvlan L3 mode drops all broadcast and multicast traffic. This reason alone makes Ipvlan L3 mode a prime candidate for those looking for massive scale and predictable network integrations. It is predictable and in turn will lead to greater uptimes because there is no bridging involved. Bridging loops have been responsible for high profile outages that can be hard to pinpoint depending on the size of the failure domain. This is due to the cascading nature of BPDUs (Bridge Port Data Units) that are flooded throughout a broadcast domain (VLAN) to find and block topology loops. Eliminating bridging domains, or at the least, keeping them isolated to a pair of ToRs (top of rack switches) will reduce hard to troubleshoot bridging instabilities. Macvlan Bridge and Ipvlan L2 modes are well suited for isolated VLANs only trunked into a pair of ToRs that can provide a loop-free non-blocking fabric. The next step further is to route at the edge via Ipvlan L3 mode that reduces a failure domain to a local host only. + +- L3 mode needs to be on a separate subnet as the default namespace since it requires a netlink route in the default namespace pointing to the Ipvlan parent interface. + +- The parent interface used in this example is `eth0` and it is on the subnet `192.168.1.0/24`. Notice the `docker network` is **not** on the same subnet as `eth0`. + +- Unlike macvlan bridge mode and ipvlan l2 modes, different subnets/networks can ping one another as long as they share the same parent interface `-o parent=`. + +``` +ip a show eth0 +3: eth0: mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 + link/ether 00:50:56:39:45:2e brd ff:ff:ff:ff:ff:ff + inet 192.168.1.250/24 brd 192.168.1.255 scope global eth0 +``` + +-A traditional gateway doesn't mean much to an L3 mode Ipvlan interface since there is no broadcast traffic allowed. Because of that, the container default gateway simply point the the containers `eth0` device. See below for CLI output of `ip route` or `ip -6 route` from inside an L3 container for details. + +The mode ` -o ipvlan_mode=l3` must be explicitly specified since the default ipvlan mode is `l2`. + +The following example does not specify a parent interface. The network drivers will create a dummy type link for the user rather then rejecting the network creation and isolating containers from only communicating with one another. + +``` +# Create the Ipvlan L3 network +docker network create -d ipvlan \ + --subnet=192.168.214.0/24 \ + --subnet=10.1.214.0/24 \ + -o ipvlan_mode=l3 ipnet210 + +# Test 192.168.214.0/24 connectivity +docker run --net=ipnet210 --ip=192.168.214.10 -itd alpine /bin/sh +docker run --net=ipnet210 --ip=10.1.214.10 -itd alpine /bin/sh + +# Test L3 connectivity from 10.1.214.0/24 to 192.168.212.0/24 +docker run --net=ipnet210 --ip=192.168.214.9 -it --rm alpine ping -c 2 10.1.214.10 + +# Test L3 connectivity from 192.168.212.0/24 to 10.1.214.0/24 +docker run --net=ipnet210 --ip=10.1.214.9 -it --rm alpine ping -c 2 192.168.214.10 + +``` + +Notice there is no `--gateway=` option in the network create. The field is ignored if one is specified `l3` mode. Take a look at the container routing table from inside of the container: + +``` +# Inside an L3 mode container +$ ip route + default dev eth0 + 192.168.120.0/24 dev eth0 src 192.168.120.2 +``` + +In order to ping the containers from a remote Docker host or the container be able to ping a remote host, the remote host or the physical network in between need to have a route pointing to the host IP address of the container's Docker host eth interface. More on this as we evolve the Ipvlan `L3` story. + +### Dual Stack IPv4 IPv6 Macvlan Bridge Mode + +**Example:** Macvlan Bridge mode, 802.1q trunk, VLAN ID: 218, Multi-Subnet, Dual Stack + +``` +# Create multiple bridge subnets with a gateway of x.x.x.1: +docker network create -d macvlan \ + --subnet=192.168.216.0/24 --subnet=192.168.218.0/24 \ + --gateway=192.168.216.1 --gateway=192.168.218.1 \ + --subnet=2001:db8:abc8::/64 --gateway=2001:db8:abc8::10 \ + -o parent=eth0.218 \ + -o macvlan_mode=bridge macvlan216 + +# Start a container on the first subnet 192.168.216.0/24 +docker run --net=macvlan216 --name=macnet216_test --ip=192.168.216.10 -itd alpine /bin/sh + +# Start a container on the second subnet 192.168.218.0/24 +docker run --net=macvlan216 --name=macnet216_test --ip=192.168.218.10 -itd alpine /bin/sh + +# Ping the first container started on the 192.168.216.0/24 subnet +docker run --net=macvlan216 --ip=192.168.216.11 -it --rm alpine /bin/sh +ping 192.168.216.10 + +# Ping the first container started on the 192.168.218.0/24 subnet +docker run --net=macvlan216 --ip=192.168.218.11 -it --rm alpine /bin/sh +ping 192.168.218.10 +``` + +View the details of one of the containers: + +``` +docker run --net=macvlan216 --ip=192.168.216.11 -it --rm alpine /bin/sh + +root@526f3060d759:/# ip a show eth0 + eth0@if92: mtu 1500 qdisc noqueue state UNKNOWN group default + link/ether 8e:9a:99:25:b6:16 brd ff:ff:ff:ff:ff:ff + inet 192.168.216.11/24 scope global eth0 + valid_lft forever preferred_lft forever + inet6 2001:db8:abc4::8c9a:99ff:fe25:b616/64 scope link tentative + valid_lft forever preferred_lft forever + inet6 2001:db8:abc8::2/64 scope link nodad + valid_lft forever preferred_lft forever + +# Specified v4 gateway of 192.168.216.1 +root@526f3060d759:/# ip route + default via 192.168.216.1 dev eth0 + 192.168.216.0/24 dev eth0 proto kernel scope link src 192.168.216.11 + +# Specified v6 gateway of 2001:db8:abc8::10 +root@526f3060d759:/# ip -6 route + 2001:db8:abc4::/64 dev eth0 proto kernel metric 256 + 2001:db8:abc8::/64 dev eth0 proto kernel metric 256 + default via 2001:db8:abc8::10 dev eth0 metric 1024 +``` + +### Dual Stack IPv4 IPv6 Ipvlan L2 Mode + +- Not only does Libnetwork give you complete control over IPv4 addressing, but it also gives you total control over IPv6 addressing as well as feature parity between the two address families. + +- The next example will start with IPv6 only. Start two containers on the same VLAN `139` and ping one another. Since the IPv4 subnet is not specified, the default IPAM will provision a default IPv4 subnet. That subnet is isolated unless the upstream network is explicitly routing it on VLAN `139`. + +``` +# Create a v6 network +docker network create -d ipvlan \ + --subnet=2001:db8:abc2::/64 --gateway=2001:db8:abc2::22 \ + -o parent=eth0.139 v6ipvlan139 + +# Start a container on the network +docker run --net=v6ipvlan139 -it --rm alpine /bin/sh + +``` + +View the container eth0 interface and v6 routing table: + +``` + eth0@if55: mtu 1500 qdisc noqueue state UNKNOWN group default + link/ether 00:50:56:2b:29:40 brd ff:ff:ff:ff:ff:ff + inet 172.18.0.2/16 scope global eth0 + valid_lft forever preferred_lft forever + inet6 2001:db8:abc4::250:56ff:fe2b:2940/64 scope link + valid_lft forever preferred_lft forever + inet6 2001:db8:abc2::1/64 scope link nodad + valid_lft forever preferred_lft forever + +root@5c1dc74b1daa:/# ip -6 route +2001:db8:abc4::/64 dev eth0 proto kernel metric 256 +2001:db8:abc2::/64 dev eth0 proto kernel metric 256 +default via 2001:db8:abc2::22 dev eth0 metric 1024 +``` + +Start a second container and ping the first container's v6 address. + +``` +$ docker run --net=v6ipvlan139 -it --rm alpine /bin/sh + +root@b817e42fcc54:/# ip a show eth0 +75: eth0@if55: mtu 1500 qdisc noqueue state UNKNOWN group default + link/ether 00:50:56:2b:29:40 brd ff:ff:ff:ff:ff:ff + inet 172.18.0.3/16 scope global eth0 + valid_lft forever preferred_lft forever + inet6 2001:db8:abc4::250:56ff:fe2b:2940/64 scope link tentative dadfailed + valid_lft forever preferred_lft forever + inet6 2001:db8:abc2::2/64 scope link nodad + valid_lft forever preferred_lft forever + +root@b817e42fcc54:/# ping6 2001:db8:abc2::1 +PING 2001:db8:abc2::1 (2001:db8:abc2::1): 56 data bytes +64 bytes from 2001:db8:abc2::1%eth0: icmp_seq=0 ttl=64 time=0.044 ms +64 bytes from 2001:db8:abc2::1%eth0: icmp_seq=1 ttl=64 time=0.058 ms + +2 packets transmitted, 2 packets received, 0% packet loss +round-trip min/avg/max/stddev = 0.044/0.051/0.058/0.000 ms +``` + +The next example with setup a dual stack IPv4/IPv6 network with an example VLAN ID of `140`. + +Next create a network with two IPv4 subnets and one IPv6 subnets, all of which have explicit gateways: + +``` +docker network create -d ipvlan \ + --subnet=192.168.140.0/24 --subnet=192.168.142.0/24 \ + --gateway=192.168.140.1 --gateway=192.168.142.1 \ + --subnet=2001:db8:abc9::/64 --gateway=2001:db8:abc9::22 \ + -o parent=eth0.140 \ + -o ipvlan_mode=l2 ipvlan140 +``` + +Start a container and view eth0 and both v4 & v6 routing tables: + +``` +docker run --net=v6ipvlan139 --ip6=2001:db8:abc2::51 -it --rm alpine /bin/sh + +root@3cce0d3575f3:/# ip a show eth0 +78: eth0@if77: mtu 1500 qdisc noqueue state UNKNOWN group default + link/ether 00:50:56:2b:29:40 brd ff:ff:ff:ff:ff:ff + inet 192.168.140.2/24 scope global eth0 + valid_lft forever preferred_lft forever + inet6 2001:db8:abc4::250:56ff:fe2b:2940/64 scope link + valid_lft forever preferred_lft forever + inet6 2001:db8:abc9::1/64 scope link nodad + valid_lft forever preferred_lft forever + +root@3cce0d3575f3:/# ip route +default via 192.168.140.1 dev eth0 +192.168.140.0/24 dev eth0 proto kernel scope link src 192.168.140.2 + +root@3cce0d3575f3:/# ip -6 route +2001:db8:abc4::/64 dev eth0 proto kernel metric 256 +2001:db8:abc9::/64 dev eth0 proto kernel metric 256 +default via 2001:db8:abc9::22 dev eth0 metric 1024 +``` + +Start a second container with a specific `--ip4` address and ping the first host using ipv4 packets: + +``` +docker run --net=ipvlan140 --ip=192.168.140.10 -it --rm alpine /bin/sh +``` + +**Note**: Different subnets on the same parent interface in both Ipvlan `L2` mode and Macvlan `bridge` mode cannot ping one another. That requires a router to proxy-arp the requests with a secondary subnet. However, Ipvlan `L3` will route the unicast traffic between disparate subnets as long as they share the same `-o parent` parent link. + + + +### Dual Stack IPv4 IPv6 Ipvlan L3 Mode + + +**Example:** IpVlan L3 Mode Dual Stack IPv4/IPv6, Multi-Subnet w/ 802.1q Vlan Tag:118 + +As in all of the examples, a tagged VLAN interface does not have to be used. The sub-interfaces can be swapped with `eth0`, `eth1`, `bond0` or any other valid interface on the host other then the `lo` loopback. + +The primary difference you will see is that L3 mode does not create a default route with a next-hop but rather sets a default route pointing to `dev eth` only since ARP/Broadcasts/Multicast are all filtered by Linux as per the design. Since the parent interface is essentially acting as a router, the parent interface IP and subnet needs to be different from the container networks. That is the opposite of bridge and L2 modes, which need to be on the same subnet (broadcast domain) in order to forward broadcast and multicast packets. + +``` +# Create an IPv6+IPv4 Dual Stack Ipvlan L3 network +# Gateways for both v4 and v6 are set to a dev e.g. 'default dev eth0' +docker network create -d ipvlan \ + --subnet=192.168.110.0/24 \ + --subnet=192.168.112.0/24 \ + --subnet=2001:db8:abc6::/64 \ + -o parent=eth0 \ + -o ipvlan_mode=l3 ipnet110 + + +# Start a few of containers on the network (ipnet110) +# in seperate terminals and check connectivity +docker run --net=ipnet110 -it --rm alpine /bin/sh +# Start a second container specifying the v6 address +docker run --net=ipnet110 --ip6=2001:db8:abc6::10 -it --rm alpine /bin/sh +# Start a third specifying the IPv4 address +docker run --net=ipnet110 --ip=192.168.112.50 -it --rm alpine /bin/sh +# Start a 4th specifying both the IPv4 and IPv6 addresses +docker run --net=ipnet110 --ip6=2001:db8:abc6::50 --ip=192.168.112.50 -it --rm alpine /bin/sh +``` + +Interface and routing table outputs are as follows: + +``` +root@3a368b2a982e:/# ip a show eth0 +63: eth0@if59: mtu 1500 qdisc noqueue state UNKNOWN group default + link/ether 00:50:56:2b:29:40 brd ff:ff:ff:ff:ff:ff + inet 192.168.112.2/24 scope global eth0 + valid_lft forever preferred_lft forever + inet6 2001:db8:abc4::250:56ff:fe2b:2940/64 scope link + valid_lft forever preferred_lft forever + inet6 2001:db8:abc6::10/64 scope link nodad + valid_lft forever preferred_lft forever + +# Note the default route is simply the eth device because ARPs are filtered. +root@3a368b2a982e:/# ip route + default dev eth0 scope link + 192.168.112.0/24 dev eth0 proto kernel scope link src 192.168.112.2 + +root@3a368b2a982e:/# ip -6 route +2001:db8:abc4::/64 dev eth0 proto kernel metric 256 +2001:db8:abc6::/64 dev eth0 proto kernel metric 256 +default dev eth0 metric 1024 +``` + +*Note:* There may be a bug when specifying `--ip6=` addresses when you delete a container with a specified v6 address and then start a new container with the same v6 address it throws the following like the address isn't properly being released to the v6 pool. It will fail to unmount the container and be left dead. + +``` +docker: Error response from daemon: Address already in use. +``` + +### Manually Creating 802.1q Links + +**Vlan ID 40** + +If a user does not want the driver to create the vlan sub-interface it simply needs to exist prior to the `docker network create`. If you have sub-interface naming that is not `interface.vlan_id` it is honored in the `-o parent=` option again as long as the interface exists and us up. + +Links if manually created can be named anything you want. As long as the exist when the network is created that is all that matters. Manually created links do not get deleted regardless of the name when the network is deleted with `docker network rm`. + +``` +# create a new sub-interface tied to dot1q vlan 40 +ip link add link eth0 name eth0.40 type vlan id 40 + +# enable the new sub-interface +ip link set eth0.40 up + +# now add networks and hosts as you would normally by attaching to the master (sub)interface that is tagged +docker network create -d ipvlan \ + --subnet=192.168.40.0/24 \ + --gateway=192.168.40.1 \ + -o parent=eth0.40 ipvlan40 + +# in two separate terminals, start a Docker container and the containers can now ping one another. +docker run --net=ipvlan40 -it --name ivlan_test5 --rm alpine /bin/sh +docker run --net=ipvlan40 -it --name ivlan_test6 --rm alpine /bin/sh +``` + +**Example:** Vlan sub-interface manually created with any name: + +``` +# create a new sub interface tied to dot1q vlan 40 +ip link add link eth0 name foo type vlan id 40 + +# enable the new sub-interface +ip link set foo up + +# now add networks and hosts as you would normally by attaching to the master (sub)interface that is tagged +docker network create -d ipvlan \ + --subnet=192.168.40.0/24 --gateway=192.168.40.1 \ + -o parent=foo ipvlan40 + +# in two separate terminals, start a Docker container and the containers can now ping one another. +docker run --net=ipvlan40 -it --name ivlan_test5 --rm alpine /bin/sh +docker run --net=ipvlan40 -it --name ivlan_test6 --rm alpine /bin/sh +``` + +Manually created links can be cleaned up with: + +``` +ip link del foo +``` + +As with all of the Libnetwork drivers, they can be mixed and matched, even as far as running 3rd party ecosystem drivers in parallel for maximum flexibility to the Docker user. diff --git a/hack/.vendor-helpers.sh b/hack/.vendor-helpers.sh new file mode 100755 index 00000000..61a6ca56 --- /dev/null +++ b/hack/.vendor-helpers.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +PROJECT=github.com/docker/docker + +# Downloads dependencies into vendor/ directory +mkdir -p vendor + +if ! go list github.com/docker/docker/docker &> /dev/null; then + rm -rf .gopath + mkdir -p .gopath/src/github.com/docker + ln -sf ../../../.. .gopath/src/${PROJECT} + export GOPATH="${PWD}/.gopath:${PWD}/vendor" +fi +export GOPATH="$GOPATH:${PWD}/vendor" + +find='find' +if [ "$(go env GOHOSTOS)" = 'windows' ]; then + find='/usr/bin/find' +fi + +clone() { + local vcs="$1" + local pkg="$2" + local rev="$3" + local url="$4" + + : ${url:=https://$pkg} + local target="vendor/src/$pkg" + + echo -n "$pkg @ $rev: " + + if [ -d "$target" ]; then + echo -n 'rm old, ' + rm -rf "$target" + fi + + echo -n 'clone, ' + case "$vcs" in + git) + git clone --quiet --no-checkout "$url" "$target" + ( cd "$target" && git checkout --quiet "$rev" && git reset --quiet --hard "$rev" ) + ;; + hg) + hg clone --quiet --updaterev "$rev" "$url" "$target" + ;; + esac + + echo -n 'rm VCS, ' + ( cd "$target" && rm -rf .{git,hg} ) + + echo -n 'rm vendor, ' + ( cd "$target" && rm -rf vendor Godeps/_workspace ) + + echo done +} + +# get an ENV from the Dockerfile with support for multiline values +_dockerfile_env() { + local e="$1" + awk ' + $1 == "ENV" && $2 == "'"$e"'" { + sub(/^ENV +([^ ]+) +/, ""); + inEnv = 1; + } + inEnv { + if (sub(/\\$/, "")) { + printf "%s", $0; + next; + } + print; + exit; + } + ' ${DOCKER_FILE:="Dockerfile"} +} + +clean() { + local packages=( + "${PROJECT}/docker" # package main + "${PROJECT}/integration-cli" # external tests + ) + local dockerPlatforms=( ${DOCKER_ENGINE_OSARCH:="linux/amd64"} $(_dockerfile_env DOCKER_CROSSPLATFORMS) ) + local dockerBuildTags="$(_dockerfile_env DOCKER_BUILDTAGS)" + local buildTagCombos=( + '' + 'experimental' + 'pkcs11' + "$dockerBuildTags" + "daemon $dockerBuildTags" + "daemon cgo $dockerBuildTags" + "experimental $dockerBuildTags" + "experimental daemon $dockerBuildTags" + "experimental daemon cgo $dockerBuildTags" + "pkcs11 $dockerBuildTags" + "pkcs11 daemon $dockerBuildTags" + "pkcs11 daemon cgo $dockerBuildTags" + ) + + echo + + echo -n 'collecting import graph, ' + local IFS=$'\n' + local imports=( $( + for platform in "${dockerPlatforms[@]}"; do + export GOOS="${platform%/*}"; + export GOARCH="${platform##*/}"; + for buildTags in "${buildTagCombos[@]}"; do + go list -e -tags "$buildTags" -f '{{join .Deps "\n"}}' "${packages[@]}" + go list -e -tags "$buildTags" -f '{{join .TestImports "\n"}}' "${packages[@]}" + done + done | grep -vE "^${PROJECT}" | sort -u + ) ) + imports=( $(go list -e -f '{{if not .Standard}}{{.ImportPath}}{{end}}' "${imports[@]}") ) + unset IFS + + echo -n 'pruning unused packages, ' + findArgs=( + # This directory contains only .c and .h files which are necessary + -path vendor/src/github.com/mattn/go-sqlite3/code + ) + + # This package is required to build the Etcd client, + # but Etcd hard codes a local Godep full path. + # FIXME: fix_rewritten_imports fixes this problem in most platforms + # but it fails in very small corner cases where it makes the vendor + # script to remove this package. + # See: https://github.com/docker/docker/issues/19231 + findArgs+=( -or -path vendor/src/github.com/ugorji/go/codec ) + for import in "${imports[@]}"; do + [ "${#findArgs[@]}" -eq 0 ] || findArgs+=( -or ) + findArgs+=( -path "vendor/src/$import" ) + done + + local IFS=$'\n' + local prune=( $($find vendor -depth -type d -not '(' "${findArgs[@]}" ')') ) + unset IFS + for dir in "${prune[@]}"; do + $find "$dir" -maxdepth 1 -not -type d -not -name 'LICENSE*' -not -name 'COPYING*' -exec rm -v -f '{}' ';' + rmdir "$dir" 2>/dev/null || true + done + + echo -n 'pruning unused files, ' + $find vendor -type f -name '*_test.go' -exec rm -v '{}' ';' + $find vendor -type f -name 'Vagrantfile' -exec rm -v '{}' ';' + + # These are the files that are left over after fix_rewritten_imports is run. + echo -n 'pruning .orig files, ' + $find vendor -type f -name '*.orig' -exec rm -v '{}' ';' + + echo done +} + +# Fix up hard-coded imports that refer to Godeps paths so they'll work with our vendoring +fix_rewritten_imports () { + local pkg="$1" + local remove="${pkg}/Godeps/_workspace/src/" + local target="vendor/src/$pkg" + + echo "$pkg: fixing rewritten imports" + $find "$target" -name \*.go -exec sed -i'.orig' -e "s|\"${remove}|\"|g" {} \; +} diff --git a/hack/Jenkins/W2L/postbuild.sh b/hack/Jenkins/W2L/postbuild.sh new file mode 100644 index 00000000..f228b008 --- /dev/null +++ b/hack/Jenkins/W2L/postbuild.sh @@ -0,0 +1,35 @@ +set +x +set +e + +echo "" +echo "" +echo "---" +echo "Now starting POST-BUILD steps" +echo "---" +echo "" + +echo INFO: Pointing to $DOCKER_HOST + +if [ ! $(docker ps -aq | wc -l) -eq 0 ]; then + echo INFO: Removing containers... + ! docker rm -vf $(docker ps -aq) +fi + +# Remove all images which don't have docker or ubuntu in the name +if [ ! $(docker images | sed -n '1!p' | grep -v 'docker' | grep -v 'ubuntu' | awk '{ print $3 }' | wc -l) -eq 0 ]; then + echo INFO: Removing images... + ! docker rmi -f $(docker images | sed -n '1!p' | grep -v 'docker' | grep -v 'ubuntu' | awk '{ print $3 }') +fi + +# Kill off any instances of git, go and docker, just in case +! taskkill -F -IM git.exe -T >& /dev/null +! taskkill -F -IM go.exe -T >& /dev/null +! taskkill -F -IM docker.exe -T >& /dev/null + +# Remove everything +! cd /c/jenkins/gopath/src/github.com/docker/docker +! rm -rfd * >& /dev/null +! rm -rfd .* >& /dev/null + +echo INFO: Cleanup complete +exit 0 \ No newline at end of file diff --git a/hack/Jenkins/W2L/setup.sh b/hack/Jenkins/W2L/setup.sh new file mode 100644 index 00000000..ffd9f6b3 --- /dev/null +++ b/hack/Jenkins/W2L/setup.sh @@ -0,0 +1,274 @@ +# Jenkins CI script for Windows to Linux CI. +# Heavily modified by John Howard (@jhowardmsft) December 2015 to try to make it more reliable. +set +xe +SCRIPT_VER="Thu Feb 25 18:54:57 UTC 2016" + +# TODO to make (even) more resilient: +# - Wait for daemon to be running before executing docker commands +# - Check if jq is installed +# - Make sure bash is v4.3 or later. Can't do until all Azure nodes on the latest version +# - Make sure we are not running as local system. Can't do until all Azure nodes are updated. +# - Error if docker versions are not equal. Can't do until all Azure nodes are updated +# - Error if go versions are not equal. Can't do until all Azure nodes are updated. +# - Error if running 32-bit posix tools. Probably can take from bash --version and check contains "x86_64" +# - Warn if the CI directory cannot be deleted afterwards. Otherwise turdlets are left behind +# - Use %systemdrive% ($SYSTEMDRIVE) rather than hard code to c: for TEMP +# - Consider cross builing the Windows binary and copy across. That's a bit of a heavy lift. Only reason +# for doing that is that it mirrors the actual release process for docker.exe which is cross-built. +# However, should absolutely not be a problem if built natively, so nit-picking. +# - Tidy up of images and containers. Either here, or in the teardown script. + +ec=0 +uniques=1 +echo INFO: Started at `date`. Script version $SCRIPT_VER + + +# !README! +# There are two daemons running on the remote Linux host: +# - outer: specified by DOCKER_HOST, this is the daemon that will build and run the inner docker daemon +# from the sources matching the PR. +# - inner: runs on the host network, on a port number similar to that of DOCKER_HOST but the last two digits are inverted +# (2357 if DOCKER_HOST had port 2375; and 2367 if DOCKER_HOST had port 2376). +# The windows integration tests are run against this inner daemon. + +# get the ip, inner and outer ports. +ip="${DOCKER_HOST#*://}" +port_outer="${ip#*:}" +# inner port is like outer port with last two digits inverted. +port_inner=$(echo "$port_outer" | sed -E 's/(.)(.)$/\2\1/') +ip="${ip%%:*}" + +echo "INFO: IP=$ip PORT_OUTER=$port_outer PORT_INNER=$port_inner" + +# If TLS is enabled +if [ -n "$DOCKER_TLS_VERIFY" ]; then + protocol=https + if [ -z "$DOCKER_MACHINE_NAME" ]; then + ec=1 + echo "ERROR: DOCKER_MACHINE_NAME is undefined" + fi + certs=$(echo ~/.docker/machine/machines/$DOCKER_MACHINE_NAME) + curlopts="--cacert $certs/ca.pem --cert $certs/cert.pem --key $certs/key.pem" + run_extra_args="-v tlscerts:/etc/docker" + daemon_extra_args="--tlsverify --tlscacert /etc/docker/ca.pem --tlscert /etc/docker/server.pem --tlskey /etc/docker/server-key.pem" +else + protocol=http +fi + +# Save for use by make.sh and scripts it invokes +export MAIN_DOCKER_HOST="tcp://$ip:$port_inner" + +# Verify we can get the remote node to respond to _ping +if [ $ec -eq 0 ]; then + reply=`curl -s $curlopts $protocol://$ip:$port_outer/_ping` + if [ "$reply" != "OK" ]; then + ec=1 + echo "ERROR: Failed to get an 'OK' response from the docker daemon on the Linux node" + echo " at $ip:$port_outer when called with an http request for '_ping'. This implies that" + echo " either the daemon has crashed/is not running, or the Linux node is unavailable." + echo + echo " A regular ping to the remote Linux node is below. It should reply. If not, the" + echo " machine cannot be reached at all and may have crashed. If it does reply, it is" + echo " likely a case of the Linux daemon not running or having crashed, which requires" + echo " further investigation." + echo + echo " Try re-running this CI job, or ask on #docker-dev or #docker-maintainers" + echo " for someone to perform further diagnostics, or take this node out of rotation." + echo + ping $ip + else + echo "INFO: The Linux nodes outer daemon replied to a ping. Good!" + fi +fi + +# Get the version from the remote node. Note this may fail if jq is not installed. +# That's probably worth checking to make sure, just in case. +if [ $ec -eq 0 ]; then + remoteVersion=`curl -s $curlopts $protocol://$ip:$port_outer/version | jq -c '.Version'` + echo "INFO: Remote daemon is running docker version $remoteVersion" +fi + +# Compare versions. We should really fail if result is no 1. Output at end of script. +if [ $ec -eq 0 ]; then + uniques=`docker version | grep Version | /usr/bin/sort -u | wc -l` +fi + +# Make sure we are in repo +if [ $ec -eq 0 ]; then + if [ ! -d hack ]; then + echo "ERROR: Are you sure this is being launched from a the root of docker repository?" + echo " If this is a Windows CI machine, it should be c:\jenkins\gopath\src\github.com\docker\docker." + echo " Current directory is `pwd`" + ec=1 + fi +fi + +# Get the commit has and verify we have something +if [ $ec -eq 0 ]; then + export COMMITHASH=$(git rev-parse --short HEAD) + echo INFO: Commmit hash is $COMMITHASH + if [ -z $COMMITHASH ]; then + echo "ERROR: Failed to get commit hash. Are you sure this is a docker repository?" + ec=1 + fi +fi + +# Redirect to a temporary location. Check is here for local runs from Jenkins machines just in case not +# in the right directory where the repo is cloned. We also redirect TEMP to not use the environment +# TEMP as when running as a standard user (not local system), it otherwise exposes a bug in posix tar which +# will cause CI to fail from Windows to Linux. Obviously it's not best practice to ever run as local system... +if [ $ec -eq 0 ]; then + export TEMP=/c/CI/CI-$COMMITHASH + export TMP=$TMP + /usr/bin/mkdir -p $TEMP # Make sure Linux mkdir for -p +fi + +# Tidy up time +if [ $ec -eq 0 ]; then + echo INFO: Deleting pre-existing containers and images... + # Force remove all containers based on a previously built image with this commit + ! docker rm -f $(docker ps -aq --filter "ancestor=docker:$COMMITHASH") &>/dev/null + + # Force remove any container with this commithash as a name + ! docker rm -f $(docker ps -aq --filter "name=docker-$COMMITHASH") &>/dev/null + + # Force remove the image if it exists + ! docker rmi -f "docker-$COMMITHASH" &>/dev/null + + # This SHOULD never happen, but just in case, also blow away any containers + # that might be around. + ! if [ ! `docker ps -aq | wc -l` -eq 0 ]; then + echo WARN: There were some leftover containers. Cleaning them up. + ! docker rm -f $(docker ps -aq) + fi +fi + +# Provide the docker version for debugging purposes. If these fail, game over. +# as the Linux box isn't responding for some reason. +if [ $ec -eq 0 ]; then + echo INFO: Docker version and info of the outer daemon on the Linux node + echo + docker version + ec=$? + if [ 0 -ne $ec ]; then + echo "ERROR: The main linux daemon does not appear to be running. Has the Linux node crashed?" + fi + echo +fi + +# Same as above, but docker info +if [ $ec -eq 0 ]; then + echo + docker info + ec=$? + if [ 0 -ne $ec ]; then + echo "ERROR: The main linux daemon does not appear to be running. Has the Linux node crashed?" + fi + echo +fi + +# build the daemon image +if [ $ec -eq 0 ]; then + echo "INFO: Running docker build on Linux host at $DOCKER_HOST" + set -x + docker build --rm --force-rm -t "docker:$COMMITHASH" . + ec=$? + set +x + if [ 0 -ne $ec ]; then + echo "ERROR: docker build failed" + fi +fi + +# Start the docker-in-docker daemon from the image we just built +if [ $ec -eq 0 ]; then + echo "INFO: Starting build of a Linux daemon to test against, and starting it..." + set -x + # aufs in aufs is faster than vfs in aufs + docker run $run_extra_args -e DOCKER_GRAPHDRIVER=aufs --pid host --privileged -d --name "docker-$COMMITHASH" --net host "docker:$COMMITHASH" bash -c "echo 'INFO: Compiling' && date && hack/make.sh binary && echo 'INFO: Compile complete' && date && cp bundles/$(cat VERSION)/binary/docker /bin/docker && echo 'INFO: Starting daemon' && exec docker daemon -D -H tcp://0.0.0.0:$port_inner $daemon_extra_args" + ec=$? + set +x + if [ 0 -ne $ec ]; then + echo "ERROR: Failed to compile and start the linux daemon" + fi +fi + +# Build locally. +if [ $ec -eq 0 ]; then + echo "INFO: Starting local build of Windows binary..." + set -x + export TIMEOUT="5m" + export DOCKER_HOST="tcp://$ip:$port_inner" + export DOCKER_TEST_HOST="tcp://$ip:$port_inner" + unset DOCKER_CLIENTONLY + export DOCKER_REMOTE_DAEMON=1 + hack/make.sh binary + ec=$? + set +x + if [ 0 -ne $ec ]; then + echo "ERROR: Build of binary on Windows failed" + fi +fi + +# Make a local copy of the built binary and ensure that is first in our path +if [ $ec -eq 0 ]; then + VERSION=$(< ./VERSION) + cp bundles/$VERSION/binary/docker.exe $TEMP + ec=$? + if [ 0 -ne $ec ]; then + echo "ERROR: Failed to copy built binary to $TEMP" + fi + export PATH=$TEMP:$PATH +fi + +# Run the integration tests +if [ $ec -eq 0 ]; then + echo "INFO: Running Integration tests..." + set -x + export DOCKER_TEST_TLS_VERIFY="$DOCKER_TLS_VERIFY" + export DOCKER_TEST_CERT_PATH="$DOCKER_CERT_PATH" + hack/make.sh test-integration-cli + ec=$? + set +x + if [ 0 -ne $ec ]; then + echo "ERROR: CLI test failed." + # Next line is useful, but very long winded if included + # docker -H=$MAIN_DOCKER_HOST logs "docker-$COMMITHASH" + fi +fi + +# Tidy up any temporary files from the CI run +if [ ! -z $COMMITHASH ]; then + rm -rf $TEMP +fi + +# CI Integrity check - ensure we are using the same version of go as present in the Dockerfile +GOVER_DOCKERFILE=`grep 'ENV GO_VERSION' Dockerfile | awk '{print $3}'` +GOVER_INSTALLED=`go version | awk '{print $3}'` +if [ "${GOVER_INSTALLED:2}" != "$GOVER_DOCKERFILE" ]; then + #ec=1 # Uncomment to make CI fail once all nodes are updated. + echo + echo "---------------------------------------------------------------------------" + echo "WARN: CI should be using go version $GOVER_DOCKERFILE, but is using ${GOVER_INSTALLED:2}" + echo " Please ping #docker-maintainers on IRC to get this CI server updated." + echo "---------------------------------------------------------------------------" + echo +fi + +# Check the Linux box is running a matching version of docker +if [ "$uniques" -ne 1 ]; then + ec=0 # Uncomment to make CI fail once all nodes are updated. + echo + echo "---------------------------------------------------------------------------" + echo "ERROR: This CI node is not running the same version of docker as the daemon." + echo " This is a CI configuration issue." + echo "---------------------------------------------------------------------------" + echo +fi + +# Tell the user how we did. +if [ $ec -eq 0 ]; then + echo INFO: Completed successfully at `date`. +else + echo ERROR: Failed with exitcode $ec at `date`. +fi +exit $ec diff --git a/hack/Jenkins/readme.md b/hack/Jenkins/readme.md new file mode 100644 index 00000000..ace3f3f5 --- /dev/null +++ b/hack/Jenkins/readme.md @@ -0,0 +1,3 @@ +These files under this directory are for reference only. + +They are used by Jenkins for CI runs. \ No newline at end of file diff --git a/hack/dind b/hack/dind new file mode 100755 index 00000000..e11f5fa8 --- /dev/null +++ b/hack/dind @@ -0,0 +1,33 @@ +#!/bin/bash +set -e + +# DinD: a wrapper script which allows docker to be run inside a docker container. +# Original version by Jerome Petazzoni +# See the blog post: https://blog.docker.com/2013/09/docker-can-now-run-within-docker/ +# +# This script should be executed inside a docker container in privilieged mode +# ('docker run --privileged', introduced in docker 0.6). + +# Usage: dind CMD [ARG...] + +# apparmor sucks and Docker needs to know that it's in a container (c) @tianon +export container=docker + +if [ -d /sys/kernel/security ] && ! mountpoint -q /sys/kernel/security; then + mount -t securityfs none /sys/kernel/security || { + echo >&2 'Could not mount /sys/kernel/security.' + echo >&2 'AppArmor detection and --privileged mode might break.' + } +fi + +# Mount /tmp (conditionally) +if ! mountpoint -q /tmp; then + mount -t tmpfs none /tmp +fi + +if [ $# -gt 0 ]; then + exec "$@" +fi + +echo >&2 'ERROR: No command specified.' +echo >&2 'You probably want to run hack/make.sh, or maybe a shell?' diff --git a/hack/generate-authors.sh b/hack/generate-authors.sh new file mode 100755 index 00000000..e78a97f9 --- /dev/null +++ b/hack/generate-authors.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -e + +cd "$(dirname "$(readlink -f "$BASH_SOURCE")")/.." + +# see also ".mailmap" for how email addresses and names are deduplicated + +{ + cat <<-'EOH' + # This file lists all individuals having contributed content to the repository. + # For how it is generated, see `hack/generate-authors.sh`. + EOH + echo + git log --format='%aN <%aE>' | LC_ALL=C.UTF-8 sort -uf +} > AUTHORS diff --git a/hack/install.sh b/hack/install.sh new file mode 100755 index 00000000..d4c2ef4f --- /dev/null +++ b/hack/install.sh @@ -0,0 +1,506 @@ +#!/bin/sh +set -e +# +# This script is meant for quick & easy install via: +# 'curl -sSL https://get.docker.com/ | sh' +# or: +# 'wget -qO- https://get.docker.com/ | sh' +# +# For test builds (ie. release candidates): +# 'curl -fsSL https://test.docker.com/ | sh' +# or: +# 'wget -qO- https://test.docker.com/ | sh' +# +# For experimental builds: +# 'curl -fsSL https://experimental.docker.com/ | sh' +# or: +# 'wget -qO- https://experimental.docker.com/ | sh' +# +# Docker Maintainers: +# To update this script on https://get.docker.com, +# use hack/release.sh during a normal release, +# or the following one-liner for script hotfixes: +# aws s3 cp --acl public-read hack/install.sh s3://get.docker.com/index +# + +url="https://get.docker.com/" +apt_url="https://apt.dockerproject.org" +yum_url="https://yum.dockerproject.org" +gpg_fingerprint="58118E89F3A912897C070ADBF76221572C52609D" + +key_servers=" +ha.pool.sks-keyservers.net +pgp.mit.edu +keyserver.ubuntu.com +" + +command_exists() { + command -v "$@" > /dev/null 2>&1 +} + +echo_docker_as_nonroot() { + if command_exists docker && [ -e /var/run/docker.sock ]; then + ( + set -x + $sh_c 'docker version' + ) || true + fi + your_user=your-user + [ "$user" != 'root' ] && your_user="$user" + # intentionally mixed spaces and tabs here -- tabs are stripped by "<<-EOF", spaces are kept in the output + cat <<-EOF + + If you would like to use Docker as a non-root user, you should now consider + adding your user to the "docker" group with something like: + + sudo usermod -aG docker $your_user + + Remember that you will have to log out and back in for this to take effect! + + EOF +} + +# Check if this is a forked Linux distro +check_forked() { + + # Check for lsb_release command existence, it usually exists in forked distros + if command_exists lsb_release; then + # Check if the `-u` option is supported + set +e + lsb_release -a -u > /dev/null 2>&1 + lsb_release_exit_code=$? + set -e + + # Check if the command has exited successfully, it means we're in a forked distro + if [ "$lsb_release_exit_code" = "0" ]; then + # Print info about current distro + cat <<-EOF + You're using '$lsb_dist' version '$dist_version'. + EOF + + # Get the upstream release info + lsb_dist=$(lsb_release -a -u 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'id' | cut -d ':' -f 2 | tr -d '[[:space:]]') + dist_version=$(lsb_release -a -u 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'codename' | cut -d ':' -f 2 | tr -d '[[:space:]]') + + # Print info about upstream distro + cat <<-EOF + Upstream release is '$lsb_dist' version '$dist_version'. + EOF + else + if [ -r /etc/debian_version ] && [ "$lsb_dist" != "ubuntu" ]; then + # We're Debian and don't even know it! + lsb_dist=debian + dist_version="$(cat /etc/debian_version | sed 's/\/.*//' | sed 's/\..*//')" + case "$dist_version" in + 8|'Kali Linux 2') + dist_version="jessie" + ;; + 7) + dist_version="wheezy" + ;; + esac + fi + fi + fi +} + +rpm_import_repository_key() { + local key=$1; shift + local tmpdir=$(mktemp -d) + chmod 600 "$tmpdir" + for key_server in $key_servers ; do + gpg --homedir "$tmpdir" --keyserver "$key_server" --recv-keys "$key" && break + done + gpg --homedir "$tmpdir" -k "$key" >/dev/null + gpg --homedir "$tmpdir" --export --armor "$key" > "$tmpdir"/repo.key + rpm --import "$tmpdir"/repo.key + rm -rf "$tmpdir" +} + +semverParse() { + major="${1%%.*}" + minor="${1#$major.}" + minor="${minor%%.*}" + patch="${1#$major.$minor.}" + patch="${patch%%[-.]*}" +} + +do_install() { + case "$(uname -m)" in + *64) + ;; + *) + cat >&2 <<-'EOF' + Error: you are not using a 64bit platform. + Docker currently only supports 64bit platforms. + EOF + exit 1 + ;; + esac + + if command_exists docker; then + version="$(docker -v | awk -F '[ ,]+' '{ print $3 }')" + MAJOR_W=1 + MINOR_W=10 + + semverParse $version + + shouldWarn=0 + if [ $major -lt $MAJOR_W ]; then + shouldWarn=1 + fi + + if [ $major -le $MAJOR_W ] && [ $minor -lt $MINOR_W ]; then + shouldWarn=1 + fi + + cat >&2 <<-'EOF' + Warning: the "docker" command appears to already exist on this system. + + If you already have Docker installed, this script can cause trouble, which is + why we're displaying this warning and provide the opportunity to cancel the + installation. + + If you installed the current Docker package using this script and are using it + EOF + + if [ $shouldWarn -eq 1 ]; then + cat >&2 <<-'EOF' + again to update Docker, we urge you to migrate your image store before upgrading + to v1.10+. + + You can find instructions for this here: + https://github.com/docker/docker/wiki/Engine-v1.10.0-content-addressability-migration + EOF + else + cat >&2 <<-'EOF' + again to update Docker, you can safely ignore this message. + EOF + fi + + cat >&2 <<-'EOF' + + You may press Ctrl+C now to abort this script. + EOF + ( set -x; sleep 20 ) + fi + + user="$(id -un 2>/dev/null || true)" + + sh_c='sh -c' + if [ "$user" != 'root' ]; then + if command_exists sudo; then + sh_c='sudo -E sh -c' + elif command_exists su; then + sh_c='su -c' + else + cat >&2 <<-'EOF' + Error: this installer needs the ability to run commands as root. + We are unable to find either "sudo" or "su" available to make this happen. + EOF + exit 1 + fi + fi + + curl='' + if command_exists curl; then + curl='curl -sSL' + elif command_exists wget; then + curl='wget -qO-' + elif command_exists busybox && busybox --list-modules | grep -q wget; then + curl='busybox wget -qO-' + fi + + # check to see which repo they are trying to install from + if [ -z "$repo" ]; then + repo='main' + if [ "https://test.docker.com/" = "$url" ]; then + repo='testing' + elif [ "https://experimental.docker.com/" = "$url" ]; then + repo='experimental' + fi + fi + + # perform some very rudimentary platform detection + lsb_dist='' + dist_version='' + if command_exists lsb_release; then + lsb_dist="$(lsb_release -si)" + fi + if [ -z "$lsb_dist" ] && [ -r /etc/lsb-release ]; then + lsb_dist="$(. /etc/lsb-release && echo "$DISTRIB_ID")" + fi + if [ -z "$lsb_dist" ] && [ -r /etc/debian_version ]; then + lsb_dist='debian' + fi + if [ -z "$lsb_dist" ] && [ -r /etc/fedora-release ]; then + lsb_dist='fedora' + fi + if [ -z "$lsb_dist" ] && [ -r /etc/oracle-release ]; then + lsb_dist='oracleserver' + fi + if [ -z "$lsb_dist" ]; then + if [ -r /etc/centos-release ] || [ -r /etc/redhat-release ]; then + lsb_dist='centos' + fi + fi + if [ -z "$lsb_dist" ] && [ -r /etc/os-release ]; then + lsb_dist="$(. /etc/os-release && echo "$ID")" + fi + + lsb_dist="$(echo "$lsb_dist" | tr '[:upper:]' '[:lower:]')" + + case "$lsb_dist" in + + ubuntu) + if command_exists lsb_release; then + dist_version="$(lsb_release --codename | cut -f2)" + fi + if [ -z "$dist_version" ] && [ -r /etc/lsb-release ]; then + dist_version="$(. /etc/lsb-release && echo "$DISTRIB_CODENAME")" + fi + ;; + + debian) + dist_version="$(cat /etc/debian_version | sed 's/\/.*//' | sed 's/\..*//')" + case "$dist_version" in + 8) + dist_version="jessie" + ;; + 7) + dist_version="wheezy" + ;; + esac + ;; + + oracleserver) + # need to switch lsb_dist to match yum repo URL + lsb_dist="oraclelinux" + dist_version="$(rpm -q --whatprovides redhat-release --queryformat "%{VERSION}\n" | sed 's/\/.*//' | sed 's/\..*//' | sed 's/Server*//')" + ;; + + fedora|centos) + dist_version="$(rpm -q --whatprovides redhat-release --queryformat "%{VERSION}\n" | sed 's/\/.*//' | sed 's/\..*//' | sed 's/Server*//')" + ;; + + *) + if command_exists lsb_release; then + dist_version="$(lsb_release --codename | cut -f2)" + fi + if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then + dist_version="$(. /etc/os-release && echo "$VERSION_ID")" + fi + ;; + + + esac + + # Check if this is a forked Linux distro + check_forked + + # Run setup for each distro accordingly + case "$lsb_dist" in + amzn) + ( + set -x + $sh_c 'sleep 3; yum -y -q install docker' + ) + echo_docker_as_nonroot + exit 0 + ;; + + 'opensuse project'|opensuse) + echo 'Going to perform the following operations:' + if [ "$repo" != 'main' ]; then + echo ' * add repository obs://Virtualization:containers' + fi + echo ' * install Docker' + $sh_c 'echo "Press CTRL-C to abort"; sleep 3' + + if [ "$repo" != 'main' ]; then + # install experimental packages from OBS://Virtualization:containers + ( + set -x + zypper -n ar -f obs://Virtualization:containers Virtualization:containers + rpm_import_repository_key 55A0B34D49501BB7CA474F5AA193FBB572174FC2 + ) + fi + ( + set -x + zypper -n install docker + ) + echo_docker_as_nonroot + exit 0 + ;; + 'suse linux'|sle[sd]) + echo 'Going to perform the following operations:' + if [ "$repo" != 'main' ]; then + echo ' * add repository obs://Virtualization:containers' + echo ' * install experimental Docker using packages NOT supported by SUSE' + else + echo ' * add the "Containers" module' + echo ' * install Docker using packages supported by SUSE' + fi + $sh_c 'echo "Press CTRL-C to abort"; sleep 3' + + if [ "$repo" != 'main' ]; then + # install experimental packages from OBS://Virtualization:containers + echo >&2 'Warning: installing experimental packages from OBS, these packages are NOT supported by SUSE' + ( + set -x + zypper -n ar -f obs://Virtualization:containers/SLE_12 Virtualization:containers + rpm_import_repository_key 55A0B34D49501BB7CA474F5AA193FBB572174FC2 + ) + else + # Add the containers module + # Note well-1: the SLE machine must already be registered against SUSE Customer Center + # Note well-2: the `-r ""` is required to workaround a known issue of SUSEConnect + ( + set -x + SUSEConnect -p sle-module-containers/12/x86_64 -r "" + ) + fi + ( + set -x + zypper -n install docker + ) + echo_docker_as_nonroot + exit 0 + ;; + + ubuntu|debian) + export DEBIAN_FRONTEND=noninteractive + + did_apt_get_update= + apt_get_update() { + if [ -z "$did_apt_get_update" ]; then + ( set -x; $sh_c 'sleep 3; apt-get update' ) + did_apt_get_update=1 + fi + } + + # aufs is preferred over devicemapper; try to ensure the driver is available. + if ! grep -q aufs /proc/filesystems && ! $sh_c 'modprobe aufs'; then + if uname -r | grep -q -- '-generic' && dpkg -l 'linux-image-*-generic' | grep -qE '^ii|^hi' 2>/dev/null; then + kern_extras="linux-image-extra-$(uname -r) linux-image-extra-virtual" + + apt_get_update + ( set -x; $sh_c 'sleep 3; apt-get install -y -q '"$kern_extras" ) || true + + if ! grep -q aufs /proc/filesystems && ! $sh_c 'modprobe aufs'; then + echo >&2 'Warning: tried to install '"$kern_extras"' (for AUFS)' + echo >&2 ' but we still have no AUFS. Docker may not work. Proceeding anyways!' + ( set -x; sleep 10 ) + fi + else + echo >&2 'Warning: current kernel is not supported by the linux-image-extra-virtual' + echo >&2 ' package. We have no AUFS support. Consider installing the packages' + echo >&2 ' linux-image-virtual kernel and linux-image-extra-virtual for AUFS support.' + ( set -x; sleep 10 ) + fi + fi + + # install apparmor utils if they're missing and apparmor is enabled in the kernel + # otherwise Docker will fail to start + if [ "$(cat /sys/module/apparmor/parameters/enabled 2>/dev/null)" = 'Y' ]; then + if command -v apparmor_parser >/dev/null 2>&1; then + echo 'apparmor is enabled in the kernel and apparmor utils were already installed' + else + echo 'apparmor is enabled in the kernel, but apparmor_parser missing' + apt_get_update + ( set -x; $sh_c 'sleep 3; apt-get install -y -q apparmor' ) + fi + fi + + if [ ! -e /usr/lib/apt/methods/https ]; then + apt_get_update + ( set -x; $sh_c 'sleep 3; apt-get install -y -q apt-transport-https ca-certificates' ) + fi + if [ -z "$curl" ]; then + apt_get_update + ( set -x; $sh_c 'sleep 3; apt-get install -y -q curl ca-certificates' ) + curl='curl -sSL' + fi + ( + set -x + for key_server in $key_servers ; do + $sh_c "apt-key adv --keyserver hkp://${key_server}:80 --recv-keys ${gpg_fingerprint}" && break + done + $sh_c "apt-key adv -k ${gpg_fingerprint} >/dev/null" + $sh_c "mkdir -p /etc/apt/sources.list.d" + $sh_c "echo deb [arch=$(dpkg --print-architecture)] ${apt_url}/repo ${lsb_dist}-${dist_version} ${repo} > /etc/apt/sources.list.d/docker.list" + $sh_c 'sleep 3; apt-get update; apt-get install -y -q docker-engine' + ) + echo_docker_as_nonroot + exit 0 + ;; + + fedora|centos|oraclelinux) + $sh_c "cat >/etc/yum.repos.d/docker-${repo}.repo" <<-EOF + [docker-${repo}-repo] + name=Docker ${repo} Repository + baseurl=${yum_url}/repo/${repo}/${lsb_dist}/${dist_version} + enabled=1 + gpgcheck=1 + gpgkey=${yum_url}/gpg + EOF + if [ "$lsb_dist" = "fedora" ] && [ "$dist_version" -ge "22" ]; then + ( + set -x + $sh_c 'sleep 3; dnf -y -q install docker-engine' + ) + else + ( + set -x + $sh_c 'sleep 3; yum -y -q install docker-engine' + ) + fi + echo_docker_as_nonroot + exit 0 + ;; + gentoo) + if [ "$url" = "https://test.docker.com/" ]; then + # intentionally mixed spaces and tabs here -- tabs are stripped by "<<-'EOF'", spaces are kept in the output + cat >&2 <<-'EOF' + + You appear to be trying to install the latest nightly build in Gentoo.' + The portage tree should contain the latest stable release of Docker, but' + if you want something more recent, you can always use the live ebuild' + provided in the "docker" overlay available via layman. For more' + instructions, please see the following URL:' + + https://github.com/tianon/docker-overlay#using-this-overlay' + + After adding the "docker" overlay, you should be able to:' + + emerge -av =app-emulation/docker-9999' + + EOF + exit 1 + fi + + ( + set -x + $sh_c 'sleep 3; emerge app-emulation/docker' + ) + exit 0 + ;; + esac + + # intentionally mixed spaces and tabs here -- tabs are stripped by "<<-'EOF'", spaces are kept in the output + cat >&2 <<-'EOF' + + Either your platform is not easily detectable, is not supported by this + installer script (yet - PRs welcome! [hack/install.sh]), or does not yet have + a package for Docker. Please visit the following URL for more detailed + installation instructions: + + https://docs.docker.com/engine/installation/ + + EOF + exit 1 +} + +# wrapped up in a function so that we have some protection against only getting +# half the file during "curl | sh" +do_install diff --git a/hack/make.sh b/hack/make.sh new file mode 100755 index 00000000..2674ec9b --- /dev/null +++ b/hack/make.sh @@ -0,0 +1,355 @@ +#!/usr/bin/env bash +set -e + +# This script builds various binary artifacts from a checkout of the docker +# source code. +# +# Requirements: +# - The current directory should be a checkout of the docker source code +# (https://github.com/docker/docker). Whatever version is checked out +# will be built. +# - The VERSION file, at the root of the repository, should exist, and +# will be used as Docker binary version and package version. +# - The hash of the git commit will also be included in the Docker binary, +# with the suffix -unsupported if the repository isn't clean. +# - The script is intended to be run inside the docker container specified +# in the Dockerfile at the root of the source. In other words: +# DO NOT CALL THIS SCRIPT DIRECTLY. +# - The right way to call this script is to invoke "make" from +# your checkout of the Docker repository. +# the Makefile will do a "docker build -t docker ." and then +# "docker run hack/make.sh" in the resulting image. +# + +set -o pipefail + +export DOCKER_PKG='github.com/docker/docker' +export SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +export MAKEDIR="$SCRIPTDIR/make" + +# We're a nice, sexy, little shell script, and people might try to run us; +# but really, they shouldn't. We want to be in a container! +inContainer="AssumeSoInitially" +if [ "$(go env GOHOSTOS)" = 'windows' ]; then + if [ -z "$FROM_DOCKERFILE" ]; then + unset inContainer + fi +else + if [ "$PWD" != "/go/src/$DOCKER_PKG" ] || [ -z "$DOCKER_CROSSPLATFORMS" ]; then + unset inContainer + fi +fi + +if [ -z "$inContainer" ]; then + { + echo "# WARNING! I don't seem to be running in a Docker container." + echo "# The result of this command might be an incorrect build, and will not be" + echo "# officially supported." + echo "#" + echo "# Try this instead: make all" + echo "#" + } >&2 +fi + +echo + +# List of bundles to create when no argument is passed +DEFAULT_BUNDLES=( + validate-dco + validate-default-seccomp + validate-gofmt + validate-lint + validate-pkg + validate-test + validate-toml + validate-vet + + binary + dynbinary + + test-unit + test-integration-cli + test-docker-py + + cover + cross + tgz +) + +VERSION=$(< ./VERSION) +if command -v git &> /dev/null && git rev-parse &> /dev/null; then + GITCOMMIT=$(git rev-parse --short HEAD) + if [ -n "$(git status --porcelain --untracked-files=no)" ]; then + GITCOMMIT="$GITCOMMIT-unsupported" + echo "#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" + echo "# GITCOMMIT = $GITCOMMIT" + echo "# The version you are building is listed as unsupported because" + echo "# there are some files in the git repository that are in an uncommited state." + echo "# Commit these changes, or add to .gitignore to remove the -unsupported from the version." + echo "# Here is the current list:" + git status --porcelain --untracked-files=no + echo "#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" + fi + ! BUILDTIME=$(date --rfc-3339 ns | sed -e 's/ /T/') &> /dev/null + if [ -z $BUILDTIME ]; then + # If using bash 3.1 which doesn't support --rfc-3389, eg Windows CI + BUILDTIME=$(date -u) + fi +elif [ "$DOCKER_GITCOMMIT" ]; then + GITCOMMIT="$DOCKER_GITCOMMIT" +else + echo >&2 'error: .git directory missing and DOCKER_GITCOMMIT not specified' + echo >&2 ' Please either build with the .git directory accessible, or specify the' + echo >&2 ' exact (--short) commit hash you are building using DOCKER_GITCOMMIT for' + echo >&2 ' future accountability in diagnosing build issues. Thanks!' + exit 1 +fi + +if [ "$AUTO_GOPATH" ]; then + rm -rf .gopath + mkdir -p .gopath/src/"$(dirname "${DOCKER_PKG}")" + ln -sf ../../../.. .gopath/src/"${DOCKER_PKG}" + export GOPATH="${PWD}/.gopath:${PWD}/vendor" +fi + +if [ ! "$GOPATH" ]; then + echo >&2 'error: missing GOPATH; please see https://golang.org/doc/code.html#GOPATH' + echo >&2 ' alternatively, set AUTO_GOPATH=1' + exit 1 +fi + +if [ "$DOCKER_EXPERIMENTAL" ]; then + echo >&2 '# WARNING! DOCKER_EXPERIMENTAL is set: building experimental features' + echo >&2 + DOCKER_BUILDTAGS+=" experimental" +fi + +if [ -z "$DOCKER_CLIENTONLY" ]; then + DOCKER_BUILDTAGS+=" daemon" + if pkg-config 'libsystemd >= 209' 2> /dev/null ; then + DOCKER_BUILDTAGS+=" journald" + elif pkg-config 'libsystemd-journal' 2> /dev/null ; then + DOCKER_BUILDTAGS+=" journald journald_compat" + fi +fi + +# test whether "btrfs/version.h" exists and apply btrfs_noversion appropriately +if \ + command -v gcc &> /dev/null \ + && ! gcc -E - -o /dev/null &> /dev/null <<<'#include ' \ +; then + DOCKER_BUILDTAGS+=' btrfs_noversion' +fi + +# test whether "libdevmapper.h" is new enough to support deferred remove +# functionality. +if \ + command -v gcc &> /dev/null \ + && ! ( echo -e '#include \nint main() { dm_task_deferred_remove(NULL); }'| gcc -xc - -o /dev/null -ldevmapper &> /dev/null ) \ +; then + DOCKER_BUILDTAGS+=' libdm_no_deferred_remove' +fi + +# Use these flags when compiling the tests and final binary + +IAMSTATIC='true' +source "$SCRIPTDIR/make/.go-autogen" +if [ -z "$DOCKER_DEBUG" ]; then + LDFLAGS='-w' +fi + +LDFLAGS_STATIC='' +EXTLDFLAGS_STATIC='-static' +# ORIG_BUILDFLAGS is necessary for the cross target which cannot always build +# with options like -race. +ORIG_BUILDFLAGS=( -tags "autogen netgo static_build sqlite_omit_load_extension $DOCKER_BUILDTAGS" -installsuffix netgo ) +# see https://github.com/golang/go/issues/9369#issuecomment-69864440 for why -installsuffix is necessary here + +# When $DOCKER_INCREMENTAL_BINARY is set in the environment, enable incremental +# builds by installing dependent packages to the GOPATH. +REBUILD_FLAG="-a" +if [ "$DOCKER_INCREMENTAL_BINARY" ]; then + REBUILD_FLAG="-i" +fi +ORIG_BUILDFLAGS+=( $REBUILD_FLAG ) + +BUILDFLAGS=( $BUILDFLAGS "${ORIG_BUILDFLAGS[@]}" ) +# Test timeout. + +if [ "${DOCKER_ENGINE_GOARCH}" == "arm" ]; then + : ${TIMEOUT:=10m} +elif [ "${DOCKER_ENGINE_GOARCH}" == "windows" ]; then + : ${TIMEOUT:=8m} +else + : ${TIMEOUT:=5m} +fi + +LDFLAGS_STATIC_DOCKER=" + $LDFLAGS_STATIC + -extldflags \"$EXTLDFLAGS_STATIC\" +" + +if [ "$(uname -s)" = 'FreeBSD' ]; then + # Tell cgo the compiler is Clang, not GCC + # https://code.google.com/p/go/source/browse/src/cmd/cgo/gcc.go?spec=svne77e74371f2340ee08622ce602e9f7b15f29d8d3&r=e6794866ebeba2bf8818b9261b54e2eef1c9e588#752 + export CC=clang + + # "-extld clang" is a workaround for + # https://code.google.com/p/go/issues/detail?id=6845 + LDFLAGS="$LDFLAGS -extld clang" +fi + +# If sqlite3.h doesn't exist under /usr/include, +# check /usr/local/include also just in case +# (e.g. FreeBSD Ports installs it under the directory) +if [ ! -e /usr/include/sqlite3.h ] && [ -e /usr/local/include/sqlite3.h ]; then + export CGO_CFLAGS='-I/usr/local/include' + export CGO_LDFLAGS='-L/usr/local/lib' +fi + +HAVE_GO_TEST_COVER= +if \ + go help testflag | grep -- -cover > /dev/null \ + && go tool -n cover > /dev/null 2>&1 \ +; then + HAVE_GO_TEST_COVER=1 +fi + +# If $TESTFLAGS is set in the environment, it is passed as extra arguments to 'go test'. +# You can use this to select certain tests to run, eg. +# +# TESTFLAGS='-test.run ^TestBuild$' ./hack/make.sh test-unit +# +# For integration-cli test, we use [gocheck](https://labix.org/gocheck), if you want +# to run certain tests on your local host, you should run with command: +# +# TESTFLAGS='-check.f DockerSuite.TestBuild*' ./hack/make.sh binary test-integration-cli +# +go_test_dir() { + dir=$1 + coverpkg=$2 + testcover=() + if [ "$HAVE_GO_TEST_COVER" ]; then + # if our current go install has -cover, we want to use it :) + mkdir -p "$DEST/coverprofiles" + coverprofile="docker${dir#.}" + coverprofile="$ABS_DEST/coverprofiles/${coverprofile//\//-}" + testcover=( -cover -coverprofile "$coverprofile" $coverpkg ) + fi + ( + echo '+ go test' $TESTFLAGS "${DOCKER_PKG}${dir#.}" + cd "$dir" + export DEST="$ABS_DEST" # we're in a subshell, so this is safe -- our integration-cli tests need DEST, and "cd" screws it up + test_env go test ${testcover[@]} -ldflags "$LDFLAGS" "${BUILDFLAGS[@]}" $TESTFLAGS + ) +} +test_env() { + # use "env -i" to tightly control the environment variables that bleed into the tests + env -i \ + DEST="$DEST" \ + DOCKER_TLS_VERIFY="$DOCKER_TEST_TLS_VERIFY" \ + DOCKER_CERT_PATH="$DOCKER_TEST_CERT_PATH" \ + DOCKER_ENGINE_GOARCH="$DOCKER_ENGINE_GOARCH" \ + DOCKER_GRAPHDRIVER="$DOCKER_GRAPHDRIVER" \ + DOCKER_USERLANDPROXY="$DOCKER_USERLANDPROXY" \ + DOCKER_HOST="$DOCKER_HOST" \ + DOCKER_REMAP_ROOT="$DOCKER_REMAP_ROOT" \ + DOCKER_REMOTE_DAEMON="$DOCKER_REMOTE_DAEMON" \ + GOPATH="$GOPATH" \ + GOTRACEBACK=all \ + HOME="$ABS_DEST/fake-HOME" \ + PATH="$PATH" \ + TEMP="$TEMP" \ + "$@" +} + +# a helper to provide ".exe" when it's appropriate +binary_extension() { + if [ "$(go env GOOS)" = 'windows' ]; then + echo -n '.exe' + fi +} + +hash_files() { + while [ $# -gt 0 ]; do + f="$1" + shift + dir="$(dirname "$f")" + base="$(basename "$f")" + for hashAlgo in md5 sha256; do + if command -v "${hashAlgo}sum" &> /dev/null; then + ( + # subshell and cd so that we get output files like: + # $HASH docker-$VERSION + # instead of: + # $HASH /go/src/github.com/.../$VERSION/binary/docker-$VERSION + cd "$dir" + "${hashAlgo}sum" "$base" > "$base.$hashAlgo" + ) + fi + done + done +} + +bundle() { + local bundle="$1"; shift + echo "---> Making bundle: $(basename "$bundle") (in $DEST)" + source "$SCRIPTDIR/make/$bundle" "$@" +} + +copy_containerd() { + dir="$1" + # Add nested executables to bundle dir so we have complete set of + # them available, but only if the native OS/ARCH is the same as the + # OS/ARCH of the build target + if [ "$(go env GOOS)/$(go env GOARCH)" == "$(go env GOHOSTOS)/$(go env GOHOSTARCH)" ]; then + (set -x + if [ -x /usr/local/bin/docker-runc ]; then + echo "Copying nested executables into $dir" + for file in containerd containerd-shim containerd-ctr runc; do + cp `which "docker-$file"` "$dir/" + if [ "$2" == "hash" ]; then + hash_files "$dir/docker-$file" + fi + done + fi + ) + fi +} + +main() { + # We want this to fail if the bundles already exist and cannot be removed. + # This is to avoid mixing bundles from different versions of the code. + mkdir -p bundles + if [ -e "bundles/$VERSION" ] && [ -z "$KEEPBUNDLE" ]; then + echo "bundles/$VERSION already exists. Removing." + rm -fr "bundles/$VERSION" && mkdir "bundles/$VERSION" || exit 1 + echo + fi + + if [ "$(go env GOHOSTOS)" != 'windows' ]; then + # Windows and symlinks don't get along well + + rm -f bundles/latest + ln -s "$VERSION" bundles/latest + fi + + if [ $# -lt 1 ]; then + bundles=(${DEFAULT_BUNDLES[@]}) + else + bundles=($@) + fi + for bundle in ${bundles[@]}; do + export DEST="bundles/$VERSION/$(basename "$bundle")" + # Cygdrive paths don't play well with go build -o. + if [[ "$(uname -s)" == CYGWIN* ]]; then + export DEST="$(cygpath -mw "$DEST")" + fi + mkdir -p "$DEST" + ABS_DEST="$(cd "$DEST" && pwd -P)" + bundle "$bundle" + echo + done +} + +main "$@" diff --git a/hack/make/.build-deb/compat b/hack/make/.build-deb/compat new file mode 100644 index 00000000..ec635144 --- /dev/null +++ b/hack/make/.build-deb/compat @@ -0,0 +1 @@ +9 diff --git a/hack/make/.build-deb/control b/hack/make/.build-deb/control new file mode 100644 index 00000000..0f543994 --- /dev/null +++ b/hack/make/.build-deb/control @@ -0,0 +1,29 @@ +Source: docker-engine +Section: admin +Priority: optional +Maintainer: Docker +Standards-Version: 3.9.6 +Homepage: https://dockerproject.org +Vcs-Browser: https://github.com/docker/docker +Vcs-Git: git://github.com/docker/docker.git + +Package: docker-engine +Architecture: linux-any +Depends: iptables, ${misc:Depends}, ${perl:Depends}, ${shlibs:Depends} +Recommends: aufs-tools, + ca-certificates, + cgroupfs-mount | cgroup-lite, + git, + xz-utils, + ${apparmor:Recommends} +Conflicts: docker (<< 1.5~), docker.io, lxc-docker, lxc-docker-virtual-package, docker-engine-cs +Description: Docker: the open-source application container engine + Docker is an open source project to build, ship and run any application as a + lightweight container + . + Docker containers are both hardware-agnostic and platform-agnostic. This means + they can run anywhere, from your laptop to the largest EC2 compute instance and + everything in between - and they don't require you to use a particular + language, framework or packaging system. That makes them great building blocks + for deploying and scaling web apps, databases, and backend services without + depending on a particular stack or provider. diff --git a/hack/make/.build-deb/docker-engine.bash-completion b/hack/make/.build-deb/docker-engine.bash-completion new file mode 100644 index 00000000..6ea11193 --- /dev/null +++ b/hack/make/.build-deb/docker-engine.bash-completion @@ -0,0 +1 @@ +contrib/completion/bash/docker diff --git a/hack/make/.build-deb/docker-engine.docker.default b/hack/make/.build-deb/docker-engine.docker.default new file mode 120000 index 00000000..4278533d --- /dev/null +++ b/hack/make/.build-deb/docker-engine.docker.default @@ -0,0 +1 @@ +../../../contrib/init/sysvinit-debian/docker.default \ No newline at end of file diff --git a/hack/make/.build-deb/docker-engine.docker.init b/hack/make/.build-deb/docker-engine.docker.init new file mode 120000 index 00000000..8cb89d30 --- /dev/null +++ b/hack/make/.build-deb/docker-engine.docker.init @@ -0,0 +1 @@ +../../../contrib/init/sysvinit-debian/docker \ No newline at end of file diff --git a/hack/make/.build-deb/docker-engine.docker.upstart b/hack/make/.build-deb/docker-engine.docker.upstart new file mode 120000 index 00000000..7e1b64a3 --- /dev/null +++ b/hack/make/.build-deb/docker-engine.docker.upstart @@ -0,0 +1 @@ +../../../contrib/init/upstart/docker.conf \ No newline at end of file diff --git a/hack/make/.build-deb/docker-engine.install b/hack/make/.build-deb/docker-engine.install new file mode 100644 index 00000000..dc6b25f0 --- /dev/null +++ b/hack/make/.build-deb/docker-engine.install @@ -0,0 +1,12 @@ +#contrib/syntax/vim/doc/* /usr/share/vim/vimfiles/doc/ +#contrib/syntax/vim/ftdetect/* /usr/share/vim/vimfiles/ftdetect/ +#contrib/syntax/vim/syntax/* /usr/share/vim/vimfiles/syntax/ +contrib/*-integration usr/share/docker-engine/contrib/ +contrib/check-config.sh usr/share/docker-engine/contrib/ +contrib/completion/fish/docker.fish usr/share/fish/vendor_completions.d/ +contrib/completion/zsh/_docker usr/share/zsh/vendor-completions/ +contrib/init/systemd/docker.service lib/systemd/system/ +contrib/init/systemd/docker.socket lib/systemd/system/ +contrib/mk* usr/share/docker-engine/contrib/ +contrib/nuke-graph-directory.sh usr/share/docker-engine/contrib/ +contrib/syntax/nano/Dockerfile.nanorc usr/share/nano/ diff --git a/hack/make/.build-deb/docker-engine.manpages b/hack/make/.build-deb/docker-engine.manpages new file mode 100644 index 00000000..1aa62186 --- /dev/null +++ b/hack/make/.build-deb/docker-engine.manpages @@ -0,0 +1 @@ +man/man*/* diff --git a/hack/make/.build-deb/docker-engine.postinst b/hack/make/.build-deb/docker-engine.postinst new file mode 100644 index 00000000..eeef6ca8 --- /dev/null +++ b/hack/make/.build-deb/docker-engine.postinst @@ -0,0 +1,20 @@ +#!/bin/sh +set -e + +case "$1" in + configure) + if [ -z "$2" ]; then + if ! getent group docker > /dev/null; then + groupadd --system docker + fi + fi + ;; + abort-*) + # How'd we get here?? + exit 1 + ;; + *) + ;; +esac + +#DEBHELPER# diff --git a/hack/make/.build-deb/docker-engine.udev b/hack/make/.build-deb/docker-engine.udev new file mode 120000 index 00000000..914a3619 --- /dev/null +++ b/hack/make/.build-deb/docker-engine.udev @@ -0,0 +1 @@ +../../../contrib/udev/80-docker.rules \ No newline at end of file diff --git a/hack/make/.build-deb/docs b/hack/make/.build-deb/docs new file mode 100644 index 00000000..b43bf86b --- /dev/null +++ b/hack/make/.build-deb/docs @@ -0,0 +1 @@ +README.md diff --git a/hack/make/.build-deb/rules b/hack/make/.build-deb/rules new file mode 100755 index 00000000..9d999eba --- /dev/null +++ b/hack/make/.build-deb/rules @@ -0,0 +1,45 @@ +#!/usr/bin/make -f + +VERSION = $(shell cat VERSION) + +override_dh_gencontrol: + # if we're on Ubuntu, we need to Recommends: apparmor + echo 'apparmor:Recommends=$(shell dpkg-vendor --is Ubuntu && echo apparmor)' >> debian/docker-engine.substvars + dh_gencontrol + +override_dh_auto_build: + ./hack/make.sh dynbinary + # ./man/md2man-all.sh runs outside the build container (if at all), since we don't have go-md2man here + +override_dh_auto_test: + ./bundles/$(VERSION)/dynbinary/docker -v + +override_dh_strip: + # Go has lots of problems with stripping, so just don't + +override_dh_auto_install: + mkdir -p debian/docker-engine/usr/bin + cp -aT "$$(readlink -f bundles/$(VERSION)/dynbinary/docker)" debian/docker-engine/usr/bin/docker + cp -aT /usr/local/bin/containerd debian/docker-engine/usr/bin/docker-containerd + cp -aT /usr/local/bin/containerd-shim debian/docker-engine/usr/bin/docker-containerd-shim + cp -aT /usr/local/bin/ctr debian/docker-engine/usr/bin/docker-containerd-ctr + cp -aT /usr/local/sbin/runc debian/docker-engine/usr/bin/docker-runc + mkdir -p debian/docker-engine/usr/lib/docker + +override_dh_installinit: + # use "docker" as our service name, not "docker-engine" + dh_installinit --name=docker + +override_dh_installudev: + # match our existing priority + dh_installudev --priority=z80 + +override_dh_install: + dh_install + dh_apparmor --profile-name=docker-engine -pdocker-engine + +override_dh_shlibdeps: + dh_shlibdeps --dpkg-shlibdeps-params=--ignore-missing-info + +%: + dh $@ --with=bash-completion $(shell command -v dh_systemd_enable > /dev/null 2>&1 && echo --with=systemd) diff --git a/hack/make/.build-rpm/docker-engine-selinux.spec b/hack/make/.build-rpm/docker-engine-selinux.spec new file mode 100644 index 00000000..706af36a --- /dev/null +++ b/hack/make/.build-rpm/docker-engine-selinux.spec @@ -0,0 +1,109 @@ +# Some bits borrowed from the openstack-selinux package +Name: docker-engine-selinux +Version: %{_version} +Release: %{_release}%{?dist} +Summary: SELinux Policies for the open-source application container engine +BuildArch: noarch +Group: Tools/Docker + +License: GPLv2 +Source: %{name}.tar.gz + +URL: https://dockerproject.org +Vendor: Docker +Packager: Docker + +# Version of SELinux we were using +%if 0%{?fedora} == 20 +%global selinux_policyver 3.12.1-197 +%endif # fedora 20 +%if 0%{?fedora} == 21 +%global selinux_policyver 3.13.1-105 +%endif # fedora 21 +%if 0%{?fedora} >= 22 +%global selinux_policyver 3.13.1-128 +%endif # fedora 22 +%if 0%{?centos} >= 7 || 0%{?rhel} >= 7 || 0%{?oraclelinux} >= 7 +%global selinux_policyver 3.13.1-23 +%endif # centos,rhel,oraclelinux 7 + +%global selinuxtype targeted +%global moduletype services +%global modulenames docker + +Requires(post): selinux-policy-base >= %{selinux_policyver}, selinux-policy-targeted >= %{selinux_policyver}, policycoreutils, policycoreutils-python libselinux-utils +BuildRequires: selinux-policy selinux-policy-devel + +# conflicting packages +Conflicts: docker-selinux + +# Usage: _format var format +# Expand 'modulenames' into various formats as needed +# Format must contain '$x' somewhere to do anything useful +%global _format() export %1=""; for x in %{modulenames}; do %1+=%2; %1+=" "; done; + +# Relabel files +%global relabel_files() \ + /sbin/restorecon -R %{_bindir}/docker %{_localstatedir}/run/docker.sock %{_localstatedir}/run/docker.pid %{_sysconfdir}/docker %{_localstatedir}/log/docker %{_localstatedir}/log/lxc %{_localstatedir}/lock/lxc %{_usr}/lib/systemd/system/docker.service /root/.docker &> /dev/null || : \ + +%description +SELinux policy modules for use with Docker + +%prep +%if 0%{?centos} <= 6 +%setup -n %{name} +%else +%autosetup -n %{name} +%endif + +%build +make SHARE="%{_datadir}" TARGETS="%{modulenames}" + +%install + +# Install SELinux interfaces +%_format INTERFACES $x.if +install -d %{buildroot}%{_datadir}/selinux/devel/include/%{moduletype} +install -p -m 644 $INTERFACES %{buildroot}%{_datadir}/selinux/devel/include/%{moduletype} + +# Install policy modules +%_format MODULES $x.pp.bz2 +install -d %{buildroot}%{_datadir}/selinux/packages +install -m 0644 $MODULES %{buildroot}%{_datadir}/selinux/packages + +%post +# +# Install all modules in a single transaction +# +if [ $1 -eq 1 ]; then + %{_sbindir}/setsebool -P -N virt_use_nfs=1 virt_sandbox_use_all_caps=1 +fi +%_format MODULES %{_datadir}/selinux/packages/$x.pp.bz2 +%{_sbindir}/semodule -n -s %{selinuxtype} -i $MODULES +if %{_sbindir}/selinuxenabled ; then + %{_sbindir}/load_policy + %relabel_files + if [ $1 -eq 1 ]; then + restorecon -R %{_sharedstatedir}/docker + fi +fi + +%postun +if [ $1 -eq 0 ]; then + %{_sbindir}/semodule -n -r %{modulenames} &> /dev/null || : + if %{_sbindir}/selinuxenabled ; then + %{_sbindir}/load_policy + %relabel_files + fi +fi + +%files +%doc LICENSE +%defattr(-,root,root,0755) +%attr(0644,root,root) %{_datadir}/selinux/packages/*.pp.bz2 +%attr(0644,root,root) %{_datadir}/selinux/devel/include/%{moduletype}/*.if + +%changelog +* Tue Dec 1 2015 Jessica Frazelle 1.9.1-1 +- add licence to rpm +- add selinux-policy and docker-engine-selinux rpm diff --git a/hack/make/.build-rpm/docker-engine.spec b/hack/make/.build-rpm/docker-engine.spec new file mode 100644 index 00000000..e0fb5c88 --- /dev/null +++ b/hack/make/.build-rpm/docker-engine.spec @@ -0,0 +1,230 @@ +Name: docker-engine +Version: %{_version} +Release: %{_release}%{?dist} +Summary: The open-source application container engine +Group: Tools/Docker + +License: ASL 2.0 +Source: %{name}.tar.gz + +URL: https://dockerproject.org +Vendor: Docker +Packager: Docker + +# is_systemd conditional +%if 0%{?fedora} >= 21 || 0%{?centos} >= 7 || 0%{?rhel} >= 7 || 0%{?suse_version} >= 1210 +%global is_systemd 1 +%endif + +# required packages for build +# most are already in the container (see contrib/builder/rpm/ARCH/generate.sh) +# only require systemd on those systems +%if 0%{?is_systemd} +%if 0%{?suse_version} >= 1210 +BuildRequires: systemd-rpm-macros +%{?systemd_requires} +%else +BuildRequires: pkgconfig(systemd) +Requires: systemd-units +BuildRequires: pkgconfig(libsystemd-journal) +%endif +%else +Requires(post): chkconfig +Requires(preun): chkconfig +# This is for /sbin/service +Requires(preun): initscripts +%endif + +# required packages on install +Requires: /bin/sh +Requires: iptables +%if !0%{?suse_version} +Requires: libcgroup +%else +Requires: libcgroup1 +%endif +Requires: tar +Requires: xz +%if 0%{?fedora} >= 21 || 0%{?centos} >= 7 || 0%{?rhel} >= 7 || 0%{?oraclelinux} >= 7 +# Resolves: rhbz#1165615 +Requires: device-mapper-libs >= 1.02.90-1 +%endif +%if 0%{?oraclelinux} >= 6 +# Require Oracle Unbreakable Enterprise Kernel R4 and newer device-mapper +Requires: kernel-uek >= 4.1 +Requires: device-mapper >= 1.02.90-2 +%endif + +# docker-selinux conditional +%if 0%{?fedora} >= 20 || 0%{?centos} >= 7 || 0%{?rhel} >= 7 || 0%{?oraclelinux} >= 7 +%global with_selinux 1 +%endif + +# start if with_selinux +%if 0%{?with_selinux} +# Version of SELinux we were using +%if 0%{?fedora} == 20 +%global selinux_policyver 3.12.1-197 +%endif # fedora 20 +%if 0%{?fedora} == 21 +%global selinux_policyver 3.13.1-105 +%endif # fedora 21 +%if 0%{?fedora} >= 22 +%global selinux_policyver 3.13.1-128 +%endif # fedora 22 +%if 0%{?centos} >= 7 || 0%{?rhel} >= 7 || 0%{?oraclelinux} >= 7 +%global selinux_policyver 3.13.1-23 +%endif # centos,oraclelinux 7 +%endif # with_selinux + +# RE: rhbz#1195804 - ensure min NVR for selinux-policy +%if 0%{?with_selinux} +Requires: selinux-policy >= %{selinux_policyver} +Requires(pre): %{name}-selinux >= %{epoch}:%{version}-%{release} +%endif # with_selinux + +# conflicting packages +Conflicts: docker +Conflicts: docker-io +Conflicts: docker-engine-cs + +%description +Docker is an open source project to build, ship and run any application as a +lightweight container. + +Docker containers are both hardware-agnostic and platform-agnostic. This means +they can run anywhere, from your laptop to the largest EC2 compute instance and +everything in between - and they don't require you to use a particular +language, framework or packaging system. That makes them great building blocks +for deploying and scaling web apps, databases, and backend services without +depending on a particular stack or provider. + +%prep +%if 0%{?centos} <= 6 || 0%{?oraclelinux} <=6 +%setup -n %{name} +%else +%autosetup -n %{name} +%endif + +%build +export DOCKER_GITCOMMIT=%{_gitcommit} +./hack/make.sh dynbinary +# ./man/md2man-all.sh runs outside the build container (if at all), since we don't have go-md2man here + +%check +./bundles/%{_origversion}/dynbinary/docker -v + +%install +# install binary +install -d $RPM_BUILD_ROOT/%{_bindir} +install -p -m 755 bundles/%{_origversion}/dynbinary/docker-%{_origversion} $RPM_BUILD_ROOT/%{_bindir}/docker + +# install containerd +install -p -m 755 /usr/local/bin/containerd $RPM_BUILD_ROOT/%{_bindir}/docker-containerd +install -p -m 755 /usr/local/bin/containerd-shim $RPM_BUILD_ROOT/%{_bindir}/docker-containerd-shim +install -p -m 755 /usr/local/bin/ctr $RPM_BUILD_ROOT/%{_bindir}/docker-containerd-ctr + +# install runc +install -p -m 755 /usr/local/sbin/runc $RPM_BUILD_ROOT/%{_bindir}/docker-runc + +# install udev rules +install -d $RPM_BUILD_ROOT/%{_sysconfdir}/udev/rules.d +install -p -m 644 contrib/udev/80-docker.rules $RPM_BUILD_ROOT/%{_sysconfdir}/udev/rules.d/80-docker.rules + +# add init scripts +install -d $RPM_BUILD_ROOT/etc/sysconfig +install -d $RPM_BUILD_ROOT/%{_initddir} + + +%if 0%{?is_systemd} +install -d $RPM_BUILD_ROOT/%{_unitdir} +install -p -m 644 contrib/init/systemd/docker.service $RPM_BUILD_ROOT/%{_unitdir}/docker.service +install -p -m 644 contrib/init/systemd/docker.socket $RPM_BUILD_ROOT/%{_unitdir}/docker.socket +%else +install -p -m 644 contrib/init/sysvinit-redhat/docker.sysconfig $RPM_BUILD_ROOT/etc/sysconfig/docker +install -p -m 755 contrib/init/sysvinit-redhat/docker $RPM_BUILD_ROOT/%{_initddir}/docker +%endif +# add bash, zsh, and fish completions +install -d $RPM_BUILD_ROOT/usr/share/bash-completion/completions +install -d $RPM_BUILD_ROOT/usr/share/zsh/vendor-completions +install -d $RPM_BUILD_ROOT/usr/share/fish/vendor_completions.d +install -p -m 644 contrib/completion/bash/docker $RPM_BUILD_ROOT/usr/share/bash-completion/completions/docker +install -p -m 644 contrib/completion/zsh/_docker $RPM_BUILD_ROOT/usr/share/zsh/vendor-completions/_docker +install -p -m 644 contrib/completion/fish/docker.fish $RPM_BUILD_ROOT/usr/share/fish/vendor_completions.d/docker.fish + +# install manpages +install -d %{buildroot}%{_mandir}/man1 +install -p -m 644 man/man1/*.1 $RPM_BUILD_ROOT/%{_mandir}/man1 +install -d %{buildroot}%{_mandir}/man5 +install -p -m 644 man/man5/*.5 $RPM_BUILD_ROOT/%{_mandir}/man5 + +# add vimfiles +install -d $RPM_BUILD_ROOT/usr/share/vim/vimfiles/doc +install -d $RPM_BUILD_ROOT/usr/share/vim/vimfiles/ftdetect +install -d $RPM_BUILD_ROOT/usr/share/vim/vimfiles/syntax +install -p -m 644 contrib/syntax/vim/doc/dockerfile.txt $RPM_BUILD_ROOT/usr/share/vim/vimfiles/doc/dockerfile.txt +install -p -m 644 contrib/syntax/vim/ftdetect/dockerfile.vim $RPM_BUILD_ROOT/usr/share/vim/vimfiles/ftdetect/dockerfile.vim +install -p -m 644 contrib/syntax/vim/syntax/dockerfile.vim $RPM_BUILD_ROOT/usr/share/vim/vimfiles/syntax/dockerfile.vim + +# add nano +install -d $RPM_BUILD_ROOT/usr/share/nano +install -p -m 644 contrib/syntax/nano/Dockerfile.nanorc $RPM_BUILD_ROOT/usr/share/nano/Dockerfile.nanorc + +# list files owned by the package here +%files +%doc AUTHORS CHANGELOG.md CONTRIBUTING.md LICENSE MAINTAINERS NOTICE README.md +/%{_bindir}/docker +/%{_bindir}/docker-containerd +/%{_bindir}/docker-containerd-shim +/%{_bindir}/docker-containerd-ctr +/%{_bindir}/docker-runc +/%{_sysconfdir}/udev/rules.d/80-docker.rules +%if 0%{?is_systemd} +/%{_unitdir}/docker.service +/%{_unitdir}/docker.socket +%else +%config(noreplace,missingok) /etc/sysconfig/docker +/%{_initddir}/docker +%endif +/usr/share/bash-completion/completions/docker +/usr/share/zsh/vendor-completions/_docker +/usr/share/fish/vendor_completions.d/docker.fish +%doc +/%{_mandir}/man1/* +/%{_mandir}/man5/* +/usr/share/vim/vimfiles/doc/dockerfile.txt +/usr/share/vim/vimfiles/ftdetect/dockerfile.vim +/usr/share/vim/vimfiles/syntax/dockerfile.vim +/usr/share/nano/Dockerfile.nanorc + +%post +%if 0%{?is_systemd} +%systemd_post docker +%else +# This adds the proper /etc/rc*.d links for the script +/sbin/chkconfig --add docker +%endif +if ! getent group docker > /dev/null; then + groupadd --system docker +fi + +%preun +%if 0%{?is_systemd} +%systemd_preun docker +%else +if [ $1 -eq 0 ] ; then + /sbin/service docker stop >/dev/null 2>&1 + /sbin/chkconfig --del docker +fi +%endif + +%postun +%if 0%{?is_systemd} +%systemd_postun_with_restart docker +%else +if [ "$1" -ge "1" ] ; then + /sbin/service docker condrestart >/dev/null 2>&1 || : +fi +%endif + +%changelog diff --git a/hack/make/.detect-daemon-osarch b/hack/make/.detect-daemon-osarch new file mode 100644 index 00000000..571e802e --- /dev/null +++ b/hack/make/.detect-daemon-osarch @@ -0,0 +1,66 @@ +#!/bin/bash +set -e + +docker-version-osarch() { + local target="$1" # "Client" or "Server" + local fmtStr="{{.${target}.Os}}/{{.${target}.Arch}}" + if docker version -f "$fmtStr" 2>/dev/null; then + # if "docker version -f" works, let's just use that! + return + fi + docker version | awk ' + $1 ~ /^(Client|Server):$/ { section = 0 } + $1 == "'"$target"':" { section = 1; next } + section && $1 == "OS/Arch:" { print $2 } + + # old versions of Docker + $1 == "OS/Arch" && $2 == "('"${target,,}"'):" { print $3 } + ' +} + +# Retrieve OS/ARCH of docker daemon, eg. linux/amd64 +export DOCKER_ENGINE_OSARCH="$(docker-version-osarch 'Server')" +export DOCKER_ENGINE_GOOS="${DOCKER_ENGINE_OSARCH%/*}" +export DOCKER_ENGINE_GOARCH="${DOCKER_ENGINE_OSARCH##*/}" +DOCKER_ENGINE_GOARCH=${DOCKER_ENGINE_GOARCH:=amd64} + +# and the client, just in case +export DOCKER_CLIENT_OSARCH="$(docker-version-osarch 'Client')" +export DOCKER_CLIENT_GOOS="${DOCKER_CLIENT_OSARCH%/*}" +export DOCKER_CLIENT_GOARCH="${DOCKER_CLIENT_OSARCH##*/}" +DOCKER_CLIENT_GOARCH=${DOCKER_CLIENT_GOARCH:=amd64} + +# Retrieve the architecture used in contrib/builder/(deb|rpm)/$PACKAGE_ARCH/ +PACKAGE_ARCH='amd64' +case "${DOCKER_ENGINE_GOARCH:-$DOCKER_CLIENT_GOARCH}" in + arm) + PACKAGE_ARCH='armhf' + ;; + arm64) + PACKAGE_ARCH='aarch64' + ;; + amd64|ppc64le|s390x) + PACKAGE_ARCH="${DOCKER_ENGINE_GOARCH:-$DOCKER_CLIENT_GOARCH}" + ;; + *) + echo >&2 "warning: not sure how to convert '$DOCKER_ENGINE_GOARCH' to a 'Docker' arch, assuming '$PACKAGE_ARCH'" + ;; +esac +export PACKAGE_ARCH + +DOCKERFILE='Dockerfile' +TEST_IMAGE_NAMESPACE= +case "$PACKAGE_ARCH" in + amd64) + case "${DOCKER_ENGINE_GOOS:-$DOCKER_CLIENT_GOOS}" in + windows) + DOCKERFILE='Dockerfile.windows' + ;; + esac + ;; + *) + DOCKERFILE="Dockerfile.$PACKAGE_ARCH" + TEST_IMAGE_NAMESPACE="$PACKAGE_ARCH" + ;; +esac +export DOCKERFILE TEST_IMAGE_NAMESPACE diff --git a/hack/make/.ensure-emptyfs b/hack/make/.ensure-emptyfs new file mode 100644 index 00000000..e71a30ae --- /dev/null +++ b/hack/make/.ensure-emptyfs @@ -0,0 +1,23 @@ +#!/bin/bash +set -e + +if ! docker inspect emptyfs &> /dev/null; then + # let's build a "docker save" tarball for "emptyfs" + # see https://github.com/docker/docker/pull/5262 + # and also https://github.com/docker/docker/issues/4242 + dir="$DEST/emptyfs" + mkdir -p "$dir" + ( + cd "$dir" + echo '{"emptyfs":{"latest":"511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158"}}' > repositories + mkdir -p 511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158 + ( + cd 511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158 + echo '{"id":"511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158","comment":"Imported from -","created":"2013-06-13T14:03:50.821769-07:00","container_config":{"Hostname":"","Domainname":"","User":"","Memory":0,"MemorySwap":0,"CpuShares":0,"AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"PortSpecs":null,"ExposedPorts":null,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":null,"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"NetworkDisabled":false,"OnBuild":null},"docker_version":"0.4.0","architecture":"x86_64","Size":0}' > json + echo '1.0' > VERSION + tar -cf layer.tar --files-from /dev/null + ) + ) + ( set -x; tar -cC "$dir" . | docker load ) + rm -rf "$dir" +fi diff --git a/hack/make/.ensure-frozen-images b/hack/make/.ensure-frozen-images new file mode 100644 index 00000000..d0c55e50 --- /dev/null +++ b/hack/make/.ensure-frozen-images @@ -0,0 +1,67 @@ +#!/bin/bash +set -e + +# image list should match what's in the Dockerfile (minus the explicit images IDs) +images=( + buildpack-deps:jessie + busybox:latest + debian:jessie + hello-world:latest +) + +if [ "$TEST_IMAGE_NAMESPACE" ]; then + for (( i = 0; i < ${#images[@]}; i++ )); do + images[$i]="$TEST_IMAGE_NAMESPACE/${images[$i]}" + done +fi + +if ! docker inspect "${images[@]}" &> /dev/null; then + hardCodedDir='/docker-frozen-images' + if [ -d "$hardCodedDir" ]; then + # Do not use a subshell for the following command. Windows to Linux CI + # runs bash 3.x so will not trap an error in a subshell. + # http://stackoverflow.com/questions/22630363/how-does-set-e-work-with-subshells + set -x; tar -cC "$hardCodedDir" . | docker load; set +x + else + dir="$DEST/frozen-images" + # extract the exact "RUN download-frozen-image-v2.sh" line from the Dockerfile itself for consistency + # NOTE: this will fail if either "curl" or "jq" is not installed or if the Dockerfile is not available/readable + awk ' + $1 == "RUN" && $2 == "./contrib/download-frozen-image-v2.sh" { + for (i = 2; i < NF; i++) + printf ( $i == "'"$hardCodedDir"'" ? "'"$dir"'" : $i ) " "; + print $NF; + if (/\\$/) { + inCont = 1; + next; + } + } + inCont { + print; + if (!/\\$/) { + inCont = 0; + } + } + ' "$DOCKERFILE" | sh -x + # Do not use a subshell for the following command. Windows to Linux CI + # runs bash 3.x so will not trap an error in a subshell. + # http://stackoverflow.com/questions/22630363/how-does-set-e-work-with-subshells + set -x; tar -cC "$dir" . | docker load; set +x + fi +fi + +if [ "$TEST_IMAGE_NAMESPACE" ]; then + for image in "${images[@]}"; do + target="${image#$TEST_IMAGE_NAMESPACE/}" + if [ "$target" != "$image" ]; then + # tag images to ensure that all integrations work with the defined image names + docker tag "$image" "$target" + # then remove original tags as these make problems with later tests (e.g., TestInspectApiImageResponse) + docker rmi "$image" + fi + done +fi + +# explicitly rename "hello-world:latest" to ":frozen" for the test that uses it +docker tag hello-world:latest hello-world:frozen +docker rmi hello-world:latest diff --git a/hack/make/.ensure-frozen-images-windows b/hack/make/.ensure-frozen-images-windows new file mode 100644 index 00000000..f55a2e41 --- /dev/null +++ b/hack/make/.ensure-frozen-images-windows @@ -0,0 +1,25 @@ +#!/bin/bash +set -e + +# This scripts sets up the required images for Windows to Windows CI + +# Tag windowsservercore as latest +set +e +! BUILD=$(docker images | grep windowsservercore | grep -v latest | awk '{print $2}') +if [ -z $BUILD ]; then + echo "ERROR: Could not find windowsservercore images" + exit 1 +fi + +! LATESTCOUNT=$(docker images | grep windowsservercore | grep -v $BUILD | wc -l) +if [ $LATESTCOUNT -ne 1 ]; then + set -e + docker tag windowsservercore:$BUILD windowsservercore:latest + echo "INFO: Tagged windowsservercore:$BUILD with latest" +fi + +# Busybox (requires windowsservercore) +if [ -z "$(docker images | grep busybox)" ]; then + echo "INFO: Building busybox" + docker build -t busybox https://raw.githubusercontent.com/jhowardmsft/busybox/master/Dockerfile +fi \ No newline at end of file diff --git a/hack/make/.ensure-httpserver b/hack/make/.ensure-httpserver new file mode 100644 index 00000000..3fc84b2f --- /dev/null +++ b/hack/make/.ensure-httpserver @@ -0,0 +1,15 @@ +#!/bin/bash +set -e + +# Build a Go static web server on top of busybox image +# and compile it for target daemon + +dir="$DEST/httpserver" +mkdir -p "$dir" +( + cd "$dir" + GOOS=${DOCKER_ENGINE_GOOS:="linux"} GOARCH=${DOCKER_ENGINE_GOARCH:="amd64"} CGO_ENABLED=0 go build -o httpserver github.com/docker/docker/contrib/httpserver + cp ../../../../contrib/httpserver/Dockerfile . + docker build -qt httpserver . > /dev/null +) +rm -rf "$dir" diff --git a/hack/make/.ensure-nnp-test b/hack/make/.ensure-nnp-test new file mode 100644 index 00000000..26b11b9a --- /dev/null +++ b/hack/make/.ensure-nnp-test @@ -0,0 +1,22 @@ +#!/bin/bash +set -e + +# Build a C binary for testing no-new-privileges +# and compile it for target daemon +if [ "$DOCKER_ENGINE_GOOS" = "linux" ]; then + if [ "$DOCKER_ENGINE_OSARCH" = "$DOCKER_CLIENT_OSARCH" ]; then + tmpdir=$(mktemp -d) + gcc -g -Wall -static contrib/nnp-test/nnp-test.c -o "${tmpdir}/nnp-test" + + dockerfile="${tmpdir}/Dockerfile" + cat <<-EOF > "$dockerfile" + FROM debian:jessie + COPY . /usr/bin/ + RUN chmod +s /usr/bin/nnp-test + EOF + docker build --force-rm ${DOCKER_BUILD_ARGS} -qt nnp-test "${tmpdir}" > /dev/null + rm -rf "${tmpdir}" + else + docker build ${DOCKER_BUILD_ARGS} -qt nnp-test contrib/nnp-test > /dev/null + fi +fi diff --git a/hack/make/.ensure-syscall-test b/hack/make/.ensure-syscall-test new file mode 100644 index 00000000..376fef1c --- /dev/null +++ b/hack/make/.ensure-syscall-test @@ -0,0 +1,23 @@ +#!/bin/bash +set -e + +# Build a C binary for cloning a userns for seccomp tests +# and compile it for target daemon +if [ "$DOCKER_ENGINE_GOOS" = "linux" ]; then + if [ "$DOCKER_ENGINE_OSARCH" = "$DOCKER_CLIENT_OSARCH" ]; then + tmpdir=$(mktemp -d) + gcc -g -Wall -static contrib/syscall-test/userns.c -o "${tmpdir}/userns-test" + gcc -g -Wall -static contrib/syscall-test/ns.c -o "${tmpdir}/ns-test" + gcc -g -Wall -static contrib/syscall-test/acct.c -o "${tmpdir}/acct-test" + + dockerfile="${tmpdir}/Dockerfile" + cat <<-EOF > "$dockerfile" + FROM debian:jessie + COPY . /usr/bin/ + EOF + docker build --force-rm ${DOCKER_BUILD_ARGS} -qt syscall-test "${tmpdir}" > /dev/null + rm -rf "${tmpdir}" + else + docker build ${DOCKER_BUILD_ARGS} -qt syscall-test contrib/syscall-test > /dev/null + fi +fi diff --git a/hack/make/.go-autogen b/hack/make/.go-autogen new file mode 100644 index 00000000..c8e13b47 --- /dev/null +++ b/hack/make/.go-autogen @@ -0,0 +1,63 @@ +#!/bin/bash + +rm -rf autogen + +cat > dockerversion/version_autogen.go < autogen/winresources/resources.go < /dev/null + + rsrc \ + -manifest hack/make/.resources-windows/docker.exe.manifest \ + -ico hack/make/.resources-windows/docker.ico \ + -arch "386" \ + -o autogen/winresources/rsrc_386.syso > /dev/null +fi diff --git a/hack/make/.integration-daemon-setup b/hack/make/.integration-daemon-setup new file mode 100644 index 00000000..b50f9454 --- /dev/null +++ b/hack/make/.integration-daemon-setup @@ -0,0 +1,14 @@ +#!/bin/bash +set -e + +bundle .detect-daemon-osarch +if [ $DOCKER_ENGINE_GOOS != "windows" ]; then + bundle .ensure-emptyfs + bundle .ensure-frozen-images + bundle .ensure-httpserver + bundle .ensure-syscall-test + bundle .ensure-nnp-test +else + # Note this is Windows to Windows CI, not Windows to Linux CI + bundle .ensure-frozen-images-windows +fi diff --git a/hack/make/.integration-daemon-start b/hack/make/.integration-daemon-start new file mode 100644 index 00000000..ab4c8aae --- /dev/null +++ b/hack/make/.integration-daemon-start @@ -0,0 +1,99 @@ +#!/bin/bash + +# see test-integration-cli for example usage of this script + +export PATH="$ABS_DEST/../binary:$ABS_DEST/../dynbinary:$ABS_DEST/../gccgo:$ABS_DEST/../dyngccgo:$PATH" + +if ! command -v docker &> /dev/null; then + echo >&2 'error: binary or dynbinary must be run before .integration-daemon-start' + false +fi + +if [ -z "$DOCKER_TEST_HOST" ]; then + if docker version &> /dev/null; then + echo >&2 'skipping daemon start, since daemon appears to be already started' + return + fi +fi + +# intentionally open a couple bogus file descriptors to help test that they get scrubbed in containers +exec 41>&1 42>&2 + +export DOCKER_GRAPHDRIVER=${DOCKER_GRAPHDRIVER:-vfs} +export DOCKER_USERLANDPROXY=${DOCKER_USERLANDPROXY:-true} + +# example usage: DOCKER_STORAGE_OPTS="dm.basesize=20G,dm.loopdatasize=200G" +storage_params="" +if [ -n "$DOCKER_STORAGE_OPTS" ]; then + IFS=',' + for i in ${DOCKER_STORAGE_OPTS}; do + storage_params="--storage-opt $i $storage_params" + done + unset IFS +fi + +# example usage: DOCKER_STORAGE_OPTS="dm.basesize=20G,dm.loopdatasize=200G" +extra_params="" +if [ "$DOCKER_REMAP_ROOT" ]; then + extra_params="--userns-remap $DOCKER_REMAP_ROOT" +fi + +if [ -z "$DOCKER_TEST_HOST" ]; then + # Start apparmor if it is enabled + if [ -e "/sys/module/apparmor/parameters/enabled" ] && [ "$(cat /sys/module/apparmor/parameters/enabled)" == "Y" ]; then + # reset container variable so apparmor profile is applied to process + # see https://github.com/docker/libcontainer/blob/master/apparmor/apparmor.go#L16 + export container="" + ( + set -x + /etc/init.d/apparmor start + ) + fi + + export DOCKER_HOST="unix://$(cd "$DEST" && pwd)/docker.sock" # "pwd" tricks to make sure $DEST is an absolute path, not a relative one + ( set -x; exec \ + docker daemon --debug \ + --host "$DOCKER_HOST" \ + --storage-driver "$DOCKER_GRAPHDRIVER" \ + --pidfile "$DEST/docker.pid" \ + --userland-proxy="$DOCKER_USERLANDPROXY" \ + $storage_params \ + $extra_params \ + &> "$DEST/docker.log" + ) & + # make sure that if the script exits unexpectedly, we stop this daemon we just started + trap 'bundle .integration-daemon-stop' EXIT +else + export DOCKER_HOST="$DOCKER_TEST_HOST" +fi + +# give it a little time to come up so it's "ready" +tries=60 +echo "INFO: Waiting for daemon to start..." +while ! docker version &> /dev/null; do + (( tries-- )) + if [ $tries -le 0 ]; then + printf "\n" + if [ -z "$DOCKER_HOST" ]; then + echo >&2 "error: daemon failed to start" + echo >&2 " check $DEST/docker.log for details" + else + echo >&2 "error: daemon at $DOCKER_HOST fails to 'docker version':" + docker version >&2 || true + # Additional Windows CI debugging as this is a common error as of + # January 2016 + if [ "$(go env GOOS)" = 'windows' ]; then + echo >&2 "Container log below:" + echo >&2 "---" + # Important - use the docker on the CI host, not the one built locally + # which is currently in our path. + ! /c/bin/docker -H=$MAIN_DOCKER_HOST logs docker-$COMMITHASH + echo >&2 "---" + fi + fi + false + fi + printf "." + sleep 2 +done +printf "\n" diff --git a/hack/make/.integration-daemon-stop b/hack/make/.integration-daemon-stop new file mode 100644 index 00000000..03c1b146 --- /dev/null +++ b/hack/make/.integration-daemon-stop @@ -0,0 +1,27 @@ +#!/bin/bash + +if [ ! "$(go env GOOS)" = 'windows' ]; then + trap - EXIT # reset EXIT trap applied in .integration-daemon-start + + for pidFile in $(find "$DEST" -name docker.pid); do + pid=$(set -x; cat "$pidFile") + ( set -x; kill "$pid" ) + if ! wait "$pid"; then + echo >&2 "warning: PID $pid from $pidFile had a nonzero exit code" + fi + done + + if [ -z "$DOCKER_TEST_HOST" ]; then + # Stop apparmor if it is enabled + if [ -e "/sys/module/apparmor/parameters/enabled" ] && [ "$(cat /sys/module/apparmor/parameters/enabled)" == "Y" ]; then + ( + set -x + /etc/init.d/apparmor stop + ) + fi + fi +else + # Note this script is not actionable on Windows to Linux CI. Instead the + # DIND daemon under test is torn down by the Jenkins tear-down script + echo "INFO: Not stopping daemon on Windows CI" +fi diff --git a/hack/make/.resources-windows/docker.exe.manifest b/hack/make/.resources-windows/docker.exe.manifest new file mode 100644 index 00000000..674bc942 --- /dev/null +++ b/hack/make/.resources-windows/docker.exe.manifest @@ -0,0 +1,18 @@ + + + Docker + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hack/make/.resources-windows/docker.ico b/hack/make/.resources-windows/docker.ico new file mode 100644 index 0000000000000000000000000000000000000000..c6506ec8dbd8e295d98a084412e9d2c358a6ba39 GIT binary patch literal 370070 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

-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

{U$3Iv~D8^4{;l+zqdvpkHAO zb~)yJ=3?A$KF0X0b3$7EfcYM4jJqxq82vEP&pFsC5; z`N{BGfidlG!6)rmWsPU}yg*)Hd)_}|ta<>=fnw}B1^G`w-mbuWfbR=-FCNIBk01I7 zBGSe_A>smz@h`~^HuxH2o>tJuY|JH5o;3Hu*P6hs1#p=$&dmEAP|aRXrduh^M;ZT= z(*nhqxM)Lj63BlxbiNe%FG}&zo&&uv1Akvr+MqS{3i)z3s0GcNV9zO2ij(c#OdGQE zx=8mAb0L2n;6s%8aOD3g$FmmmIAbZD%pi4n*G&`iA4=?V(YmhR zUv*<1hU*x5p#972F}^#I(yT}{yx0=nn=!yYM< z0PLIB`8#?JWBiTgfjZcz|Gn~hkiR@u3fdm%2Jktv2R-+{7EXEqXdLs2>i2(5Pk(25 zXmsJBE|{`0jFdg}x9CI%Eq%Yd_y zi^eHvSi%IT8K-!p-VqNzqYLMhh-c!KiCE+LNcBH6HAu`kAA6US^xp;Fth6TjzG>kb z8)rJSMH^sktgfsAd^g{7?W>dXTK?SSvP%8@OPTh$=cCRi?C4XBZ{`aj ztHng{uqbSuq8+8>I=`)Q=+Ge|zDYSh@y;Tr_9{Vncz192)R?a>oeCa#G0v?S*1KWN zqKhceH#7d17RNXU?@}5;D{J3}^Cop|rZk$|ddK3HU;C|AvhejnRfv;;z}pxc5L4+pN|+M{xqdmi{6&+4G48^mS=E_+ zJqlAjOv{6v&C8SbnwKBmVO|#I()~)Nw^{Mgon}(lhBjvhy!@(pmmZ^g5ijz^*jr*j zoX*=hK{~5%AL%;(a-`nE%MmRX-a2Z!FfUZK>QY3Dmlzw_tTeLSx;$U4GZ+1Y@8xG_ zPwvUTDGu!x+Me)K{xdVE<-*J$gGE`v20vv4=+8K^M|ky&moDEou{gBrt_uOr?>PCZ z@Mb}Nrs+W4r)0qRRW|&7GUmVlv%S#dMr=2;Vn88wB6Dz`d%HIt`@$JCN0`#sr zLEj~S;!XMPEdI^h2te4?+VHw@{||kfaxDM= literal 0 HcmV?d00001 diff --git a/hack/make/.resources-windows/docker.png b/hack/make/.resources-windows/docker.png new file mode 100644 index 0000000000000000000000000000000000000000..88df0b66dfcf0b298de8cd296b793b9220c4a914 GIT binary patch literal 658195 zcmeI52VfLM-^S;9dgxUo6akTtgq{#WZz3H8tb`;KDWNHduM$KQ)K9@e6$LBOqzFor zPy}pLL3-~cKzhILKbJeW9LeSS?%qA4!|l#a{mo{dow~Dn_2}&HQ^kid=HInThdzwS zW^?PMCYOZm&Hknz@3CD5O<>HoEVnYY@QX@}`ScvozJ0G=!{aB$PZ%CQHmGa+_CaIE z#}6GbDvq(qCl~h}*01l43av8Ernc)gGvTvt@qNmB1@&q7K|;AXb?VpfF7@=x8cR=< z@4cgR$BxRX`?(x#+r|;dp=fdTNZKnCu9?MF3t&goSA+MH| z`BcqB&2LOL`G~IHd8)o_dRRVNtUp9 zex1H-iINTeD`vSW5tgtxbHN)j_WWlx%FC9_VL@f%W_Do1Te6i0o_)U~ z8yL#U4eho&oE;BlpqF@?FQg{8(u3nKPbb$~V~HeYI*$X74OuQdM06R8F3A~)3z?>Oywui$Z-0RtL7UT=Tbicghm7A=on*rwsm*B4Z7G_6VC z`4p;$C3DJCX;bnh?+;kLe}A)&HU_*Ld~>^6TXV>eOZit0tQgydu?rL5I+WiaWPO!{ zmgMrFMfnVXSz{s#;jVywfGY4z8%t(v-{QiDYGV>?u*clhQGZuU;Q+PinQE7i^?HsP`HgP&aacDs7- z{fq zYklNZalwqt8auiu%Y0Bf=#QD@de`hWr)ih4KJWJn>ffbWa;x$3@b_Nmw0Ta;eXP;j zx$kc69QN6?b-i2t>$jo(re)2mywmAx--EFe!xnyAVacL{ceJBuN_=5Jn-bl zQSJAJCSI?kX}o(>Xv00b+O2KT!28pfzt+}V>9=A}l{L?~XxyvY^LmHz|G6yScaGo!&d^*8Fr?x1Z~5E}gZz(vU`B?+xtI zpw_W6AD0a&^~TKhyPMZ8Gpl{0x|?RpyRQAV-{CTcI~^X=r`C-IKQ3z0rAn<&|2P>t za-Ampvj#y=?VE9V@#S5Y8(*$^xkmEyK8uI68TV~w-;4G7{Q1h$m9Lb#(&UOdZhoV- z{l8h*=ghLD{Yp3e__=ngmTv6xQ}6edm2L9s!tj5lSLoX9yH#I*ex%Cqio-KT{B(Wd z=F(?Af4s-Mg98r#b))n3dDj)Gll=VOEkAwl%whE^FRG-eG_3NZNG$LE@0Xul?%O}5|6lz+{_&;0 zZ?5jtzj5D7KYIT=cSZl@i+kPbTea`7A6qVaZ~3rg@%>tV`_`bmS?WQ%p4rv2{kOfB ztbO^?);&SKF_qU(Zct;S|B82Coz`@Bowfg*_}^>wx6IG0w&jCX%YR%N-oEkEjR&mn zlk(Zn)|yN6$7Oyov4{7ydOyrddF%bNAEk}?L6$`{i?;+`kv@}<_Di2Ds*oZk+!ir0rvVNSgoUZ!aZ$`Aqx|XJ6lTE&H)IL-P9Njn4idt+U@i^?JYB>QjFED@Fd^ zbXzEnOg%A2}VE^Y{T;FNc;#E%^+p=oQ52b(D)Z^10E51B7 zY}cCm8~@n&<1dxhTsf{jwR+>|jc-N|8&YxDFTbzemGtShPv6}9=Ktlj()ho3B49Im;pg$?BA$&nyKw&(Nv+@fJ$3GlkYNK4lsa&{@uiJr|9W!wh{b&u9qBx*;;>B< zrl-vOU{<%c|7tids^Oxld(Q0JcKPXvY2#kJFy(OC;zjEh?Yy(>)K5vxl13!GclxWp zn|$@;mxCHlIJj$H`}4i89BS?R)#srj&#io`XZxPhhHbc3DQ~p>m*@qt{#4j~&!RUyc4?*IRydXS_FKz`LzK8as1fl|y0knoc@&y8VQ69kM&j?QnL{ zu&|!tKLtPCw0_j`hOb1PY`(e4=GJ=}4juIO(SeHxb{*C2vG9iVS2r6o>bG;bcT(QF zxcH*)r8g?h583zP*w>ft-pQFM99A>rP_sibkGFrJ{^LE8yVUPK<&)cUwl;me_V10qYj9=LJ9U4Yz4aOYXU;5& z__D&n*MdejY8A09Z2Bj2W{+#Pyhis=Hx9cw?CQ|h{#qF~V_p95V?OvL7r3 zJ3aYI)%W86+`i!SXFvbAV)v%l149oFy>azy@R!dA%w0b3>b!t4{wGfU?0@drpYNol zcRUp`@1J_7woKpHe$zMGBY)i*_ro9i|7qE}-?#lz`(^iQmUQ^chUot?#~ezk`o`Au zCby5CdVXt0Ueq7Io$4>^-~H&&qj5Q1bJl0HUOj91jm)_@qhDOrWAwN?dliQjd74@Q z8~@w%@6l?9TlMSn@v*0Fbq?5D{kQzZcP9n=H}miRe_!mmSZi#D*Dv45@IIIExPQdS z*FJn>aSHBRxRX}b_R>Q2*gHL_E@r`RS`_H`+7=L$jsfia3chr7xt>xC1 z`(H@@bz<_mQ&-9k_;tX-pCVo!eRJZ?$ox-_^xV7Ui!BRRZ+c<;0}ANvpQ@-1>5An>%e! zv_JFS(bmuBpFV!~^wd`pCoM@ibh>kfddBnv`E$}f4s5Ubc)`biz3cz(tuI?dg@<2k zJwES7?how`#2%>gWySoj^Z#01u|mjOnMZH^xBt-mLrdo+&---y=>L6}cQf%;<)2RN z*}3QO-~O1~IP9g=XLIiCm~?tlwY%S(3+#FMe?e30X@_U6P3wE`hn!C&auIV@AeJ3>p$QY{ZyW zHSccOUNdM!Y^$0BnsyKEKDK?_@DW|6jF0O(rANP*DI;UTV{5jK@`;=rK@p6On;0E5 zdGx3;6Cx(Js;P@Nf{wX!NX;Ofmx&`=)ojZX3>w(IS5W)-@o_;-8#WD&32hV>)I7Xl zSktD>n>T6@)F?EpNl0i|NLb_Gu<(c`O(H@=gY>3mln)(6j*lH0(WgTveR6cxs^;*C z6URn`giM+=so|u?4dcfT3keGk4-W}#6w;_sF!>0c@cNjE(UXJ6OsG{5i6Nd2aT8+3 zj~F{~MEsZ_9&hxJ_*W;ks#%jK^uXxLHG1rWgvLzJvm=TjlcUFmgf$EeDJD?&?&gDy z9$jR}2@^ZMMmf=!tjGi>^m~15Tu7g|3GuIvkBRH}THKh4wTj9_Y|Mjr#=bg!l#Wkq zOi0|QxX~130%bm|$SjOCv_KvNFO-c3my0d^#1TUuR!*U?{7SLG;wFzMCK$g`5KMO~ z6x2{8-G347$Hzraj33`GK7LeG!Cm&S{Y1?tepdz6>mD65VhnFoJsBBd8t7q-Ka4M~ zL-fSBC<@y&IJ8-CXybliP3d-wXiCS;>A)zG0-q*vbf>#NHhN;TC9xFpYZ^~{?1-VS zTNKMfpM~S--aVr0mVQ}ie!KfElllvvZD zEt-T6iES1f8yhn;xJhX2kl^r`A+f>H;mu>CW1EFGj*X5sl%4`$ z(cPwd_VkS#SJ>5XVbloSGh}S^_z7|RPqS4`!{6qEht7j%3J;_E6OV|F;ZL8a7=8oC z#YTn{a($Ss2O;&rMm$=bmLVvJ-za$TX-pa(H^$uUUl5hfSMiZdh#xv}QuO$^w!`QS zvFvZNAl||OO%sR=;m_+)Bb-nb6QW>Vq0zHVrz+d{E=dra>Qu>_2A2#3*|9>oRKM zY!r&?afA+PUwUD@Kt!OQC-kC6w@TSp!|ERrK*HHiy}m;f<<3S8#-m|O)e6CmbK zfy+D}ldHgG0>u0&aGB?0auv8tfS5l8F7td$t^$_{5c8+NWuA}8Rp2rKV*V7k%=0n1 z3S1^Y%%1|6c|Imrfy)Gl`BUIB&&T8{aG3xxe+pdY`IuY3XxXkl0xe8n+K+K;4mw7%W zSAoj}i1}0CGSA24DsY(qF@FkN=J}Xh1uhdH=1+mkJRg&*z-0o&{3&pm=VNjexJ-bU zKQ)WX=izeDxG}T_bP_H0%;}f-4=of8is{m)J7ZIxpj}L+GnS>L-y4j*8p_zs!L-J7 zK4TT*KY980PR#rF?5-W!_M0qYTJj{*@_%7K00ck)1VF$_0&*+;!#fCo00@8p200hJ)YzPDbAOHd&00F^000JNY0w7=$0SJgq*boQ= zKmY_l00M%000ck)1VF$h0uT_Juptl#fB*=900ad000@8p2!Mc11Rx+bVM8Dg009sH z0SE~00T2KI5C8$22tYt=!iGQ~00JNY0uT_~10VnbAOHe35rBZ$gbjf}00ck)1Rx-| z2S5M>KmY`6A^-ug2^#`|00@8p2tYt^4}bs&fB*>CL;wO}6E*|_0T2KI5P*Q-9smIl z009uNi2ww|CTs`<0w4eaAOHcuJpckA00JOj69EW_P1q0!1V8`;KmY=QdjJGL00i8N zfGi<@s_RMw6>%MFqyz#W00M4B00QDx)rrD@00@A9>j*$VT*n$IfdB}AfLjrOfVfq4 zqA(x;0wCZz0uT__u|`TD00JQ3Rs_1jKc$krD`i00_7h0SJg& zRVNAq0w4eat|I^eaUE-<1Ogxc0&YbB0^(NHiNb&Y2!Md=2tYtw#~LYt00@A9TM>YO zxK(wcFdzT|AmBOz5D?d~MoJ(60wCa41Rx-8Rh=jd2!H?xxQ+k>#C5EZ5(t0*2)Gpi z2#8x%Ckg`sAOHfcBLD$$9c!cn0w4eaZbbkB;#Sp(!hiq>fPm`=KtNo_8YzJQ2!McF z5rBZWRdu2;AOHd&;5q^j5ZAFrO2rUJ&`vGp3YKtP-XARywb7vu^AKmY_pPXGcU`YJ*OK>!3mK%4|1 zAmXeSKtP-Xl-6WRCX-p?9ezPT^aQH!Od3L&3~+Hq zzq=4s=9Y`oL~H4(!v2uOs$Ta?f0F3xKrr3V3VF?1y7cmn9c zay(PS2m&Ag0vP%AOHd&U=sldh)viK2n0X?1V8`+f_nf2KmY_lz$O9^5Sy?e5D0((2!H?t1or?4 zfB*=9fK3D-AU0t`AP@in5C8!P2<`z8009sH0hKp+4DAOHdo5ZnVG00JNY z0yYtVfY^i$fj|HRKmY_FAh-uW00ck)1Z*Mz0kH`i0)YSsfB*v)C^@f`AkVRNt9&p7L3pY#Q0yWR+yAlI4FG7s+x{iL8d~Ewbrk{m7Ojn?d#_ zS#Ee7+*~1>OSTMIf3o+<-XrTpwjx<>!Y)LWIaqLre?dSv0@ghLi^?VcV}Sty5Ma!g zAaH9i{B6k~3I@0Gh6DYtcM*QL_!yf&I^h-u1l*1Q1jOyC)L{im&`xz&T%wO?CFQW3 zi*uRe;`ERl2sniR`mvmX6_J7f2!Me23E10@g^_ge*D-Pk0w4eaq9gzT5oNU?Qy>5W zARvAM5D@X#5poCuAOHfQB!H20QC5qSGqv(?BBjvU$*Gt|-<$PoxW?c}b7*_f@lS?x z7ya&_IkasGrM2?#c{(?pa#hHS6Hda21?KWP0W6|)EdjJZT+17&fdB}AfZGs23&d@z z(x!r3pioQ5@|A9r$Z|8S;D_-RLI~MSWcQH`BKtDg8)WB`!W+`uK>-rU8p87xd9%rKGn_16mA91ad9p3Yb|z~`lMdNb{tua1 zWHZSQCRo0$1?2cpW2tYur;|1R!00I&sK!fITAEolmqOvR_#{iXP zB#Fq}qOQZOWC9S7k{Q7V2!Mb@2~fAycomI%n9Dwi$OwptIYPx>gMbvz23J4;1SC#C zqfkt=qzF0@S$BDcC4S-OApr=;Ll!s!0T6H_0yOg~ipD++U-BxkWd3UP6dErXUZO{M zct8LG@&E-6KmY{XiGWfrpJGjU_`o_%^_@#od|O)+&XfH@Ks>qjQFah;V*+Z0!s-Qs ze5$XnQoV@=-wVy!a$`((6%7Jn7d`|A0T2))0cu6Gphbhm-zl~@n<}|nL+eKVvN(VT zdxU^^aOI=uAmFY96f$|7J!SJ&D63Xje`UEnad<*l2#6=tGRg=79!7vxn_JU;m7q0j zg=xykPo?fp&6!@N?l^x)04nm31&%-f1l)uGEdU9!wBu8>P>HQPJeZ<;*yJxb=PA_Y|Bz{>_fxOnl$Ft+^HV>)TMu#z zrDJZnsjc_(c)@9j|MsZ9Gl_>aq!~)*gX!E5-cNtdBPzrHA=8E2g6OvyKOs}w$WMU_ zW6zOWI-OK9IGFi0I)t2Qd9A$&V>jV%Sa~>+O5y4cAM|GZ8m=+WqX&C0I{wLU?xNov z7oy6vDU{aA!{_O|dLcLGpM(*MgU(OJGgGMk%Z(hROzyOoWB$avmzDW=QqO9aq)ynH z=YLUUMHg04RNw{zARt)+^iG`T%*^xGXxg!Hj5E`aO59FUX{iA4(0o2a<#u)5r*)qvSe||dp!^`< zW(2BkxORhPOI2`Yv9j~>QZ99?6yVG>ToE_Auv|eIX@CF-NQOYZR_pW@9u1`XO9qG& zGwSN5yAz9os6hY(K!7n4kSosSz$X;ZdJw}*J7*^++W1}F4=!2|F3H%+!+bjEaF?X@ zFp+yS<7f86Q<`#AFhw@V*v$pU%V-AF@dxRwJj|!H4t#L#nuGJiVu^85#9}O+moj#9 z!SQ@zas5F$D-XAz<1P=*U2|~IcvhTil5|Zf`AmD1Qi^hRYNR2}DUD;!PDjk~LqJ?x zJ6u4fx;9C6fzm4^Aap*MtgEN-9;7rM3J7CdKwQmY2c@}$tg+z&Qr+0W1;=YBjfVol z7#9#%vsg-L@_e|Rp%%yMLy;}{vpK1DY(;Mo(QJ6z|KW_U&tNgCzRobF7 zmY?)y%TGp<9YOYUvRlcXB%4X$lgVBn%j@NUk>&kk7s;lPzq@3Qko}2l0@=P~d6$OZ z2F(z`s{k1Q0T6IBft>vOQ!2TFFZ6Mm(K1H=7h#s4iHQ6w-_B?BO6NTFb zWP6bstiCSAl00w5q50U8JSpSGBBZhJFz zH!G8+=em5%+S+WNrT`194P=df2#7Vb;1>iyKr#fV8|y+wZZ4mxc~9M`Vw{{Bh8UfIxDWFBp#cc6}cf^2#A3M zen9{P+>XGV9@W-UE99ah3P$^+uxsgQe8)ydMCZ&n&Qs)Lotc0kZU~5h1b#sP1l*E9 zR(|dfM?CU*%Y(N-L~i)N%m8YA+;c@fAt0`xj5I(11Oy{Mt&nXrZt;gb8Mu>~A%#J6 zUc{j!9)H?XR%^mSK&+t!zaRhtk|aQ%5$r-!ZfzP+PR`C^_pX$MuXm(jDW>TCNF}U-cfZLOJEEF;`5D+2hBW(}>0YL~{f2K+}b!YujRN^->(phF+Zc&%S z;sRH_SJ3S|}J1o>3m`!r=&geHaZ#6MU3l$gYWb3zM* zQ-A>h5D+1O1g*nIhpFiyr>4jEq#y%iOjfASDb)1fD+Wbo_(#*{k^PX=qNauN+LAd)Z;5J^-MG6@18 zU=0Bhj*2R|d?^V>C%H^kly)%M?_IaxLo3V{yH}_@yUA`Pn@FO;zW^mUBeMoxG4&w1 zLJOARm4`zq?F0JsdDE0n81y2mA}T_%T4bKQDxQ?9MZol5BH*ff6|{D!W+`$>G5<8 z;rR<9?+@mNv*C;k{5U(Ft|3i2e87T-;bD0mcw9S(MH~8kV2H(eI;l?95WXn>hVZ;h z+@G4PH(6ePhAswE{`@-kXDEw-A33owVsVfk=*-H)W9j!}{h1-pw$!S#N?Hob(^|CP<01j#ag`&>8Cz=j-x6Q6ab9oF z(C;C#2gNd`Va@ZO^8JY6H#Lkf1o#8O*zku9x5mcsP%^mjhZ462!yk&KehlJc@MG?` zsPLxo@jn>jKN|jrWa`Ht5{9(6aGClsTr29=G&~mtZViTulxcjX{EAA`Ac}@~%>5P> z-ZV`vaK?uJ{h0bO|VlgZxW3)CHRr-WprUlw(5{^5C8#72$WrWDNG@k zk5bCyok%nuvqm%w@fUoPHq~13xJKcBHpGG7M+u+{>rp0f0Ra#YkN~wZno}#|1zPmi zhFTg=(4e^Sc$n2WGl)l)^_yHRrq&0aC;?Lp0SHJjVBib_AmCgABoq~CyqnLKicl~a zZ)MaW!6-xGVLC8vb4Co}VZ#oIeDIu)kHMHB;JE^_{A6X~(%cN2M=k}bH8p9bNBKuC zzySn6;86lHR+_d9QPZ5La%9WUGJrr5i?U<`6f(KDBZcD8#A&{{{=*(NhzE~uGqpa> z6%{Ys9un|e0b#5yVfgMLijt2s3MKPbna;E=IUIa|00_92fSuyOGyAb?b1co&9M)8n zVDZaJFwe*`2!KEd1RNk9$0&)-B_xB(M8J~;qyp{$5C8%9BH#e=;6P?m_Z1%lfhmE2 zCwF0)t{5%>6)uAS2#A@0jq5y2i*%bDzBP5kdC>$sSwJjWxmq+fxPkx(h>3u1dlFh< z&NoQ3)6ApR2j6oJ#sUJKEFi{vN?Cx4dqJz#LQwDq0xl!KH%8=pTk&5z4c~syP^PYl7`i9;m#;`c=x_l$eMm@!5K-}6{p#oH91VF%Y0;xIK zy5@)3KT6A^<34#4GUrW@qCOZS7(;p%@-M#qNS6PK)`&tt z_O4%bl?ZhwyNbZMsyV_x9cGoW@Ee#`#R<=a324q9WTn=9!erT=H=RFV}aHD5lOG=l2c5_1wkIklzdbyMAP{oUB4wm1m~1 z(rf;YrH6H9X-&E^t;*0yFXYXwPxt~4E*^#@=7HBvv08gpk$BKOlXRJ#iRjPLO7l-M z*lhUH^RqUGc2Gci&hb{p+hoU+HGQdnj@*7HD@r5yhtUaM4I*=tY;jFB-Um2|?Dr%j zyfy0>v;5>Nis&_KBdg{Y!05vu76;?qFus5P!o2@Yq^}ehJs{68%cS0~bf$Ri(aktiDhOxnCo^q+xua3K?Q^W@&WTivP~_Zz(s~n zwOc_j^DOXh-plfnQ^>;%^PV2PTFKj&`C9N{5apdK%=_?m=5_K9#vhi&8Quy>i5kfA z0?QZY#jV^G$+vYaFz=(gtRLf`2L=~h{zjhf6DUb8^7Q-6_n%GlLa~EhD6G4U^Syjo z=2NX$#*;1G`hH2HzZcz1D{Ap;Qt#@7qx6*%#nyK|eXQqpjZk)-1lEcc$(tk|f=u0| z=N@lm^d-xmL7~MK?BNC8trh8vo*2mr2tni@y za{3?Y=eNIKUrP^+%zEK0qhVW?Tc(O6=qQt}G4CUZI`NcI7mU3or*bWpRjV<}su{}i z{L9!IooK?}rGfG!nr+2}qso7~Sw;Q=W`)ouR|wLHNfN!7<#_oOSFY=Hfww-K(KTLN znv9-#{APWL>~ONxi}hIah27-bkDhrqioR@{t0V=4KyuOenyj@DjVbZ&tVFV0gnlNl z=jqpSv;1T?^8B;q0SbKb&k|OkR*23}pS|+dC8oY|lBqBLtNXQY4!?|LS8u>F>a}3m zK@BC_UBUbFxlpJtpCB$r8E=g_%j8t4%`)q@V41ZVF|Ec`Gr%n?4WEkab$UNdn%GAJ z;#Ry+SQh;QU%6$gvg~U0$<||BShT95UvM4-5a9v;W4@hwkEzZdVdc;4XQeM6Cl~H& zqoFUZ->+DM-K$!UCDU&jy=d?*FTEK`;<3nZ;>=%q*70W?f7bLN+m`HuOwh5l<}w9% zk)Cyl)&?xjzoZ0&U_MR-nMQVSaRsu&{S1XoB)f;~-(>$I(8a&%EkD_W{QO1MLc^QW z_gJa>H(15gJ2Xq^0zD*8(Ru~mMd^6M2Til9Jz4Nzp#J~7QWYGZhy!Be-E+!2mvutQ zCkUv>DsR($ZZaV5fTYYvL9Mv#s&!Zv2`}fCSGtmU|4zEfXH==_;ioxwm_*|wlR0L# zkmb3h2WM`%$5>AJ>MXZ>HL{Ph+_H~}_GN@m-Bw=yk7>^yW&y-YPHSPDVe;tjDY-%o zmKjj)TBYNAM#;4KhbW!1^o+1Wr3&Yio^MKWsZX{s*$A?&$krw6Xv1H=-=w@>v^Qc& z2nc~~Km{F5Hkz!fMgemvE#6(pciZXi8S2VY-nmLI8N`gHY74y9 zCzW!4mh2_H=`7t-VS!Zb45j4hUb8ySwRCw3a6J%?Xb=C8YzZ@?i z`xyzzVfwYnh*m&IG+T(z$;4k&I1}#1?-|{FLt{=_dePGI$s=<9lC3wy#d!KL=OO33_%FYI_=g9!B}!p6ltcQzd#MQDkmVDMteRCODghzUXh?;4T5ow1&hq)L@Ko9NKlTO zX33=j649d{UslfthFa@?c^@DQ2!H?xfPnZ3+@dV*Buk5sbfR)-&$^X%O(GC0`cr8pj^Ji*80}w@JU`UAOHep3D9ct{L~C##Dizd+h;Gt!)y)Nb=ww-=9x^T;&Z*NHrkv#n}o;egb)A(BuGG;MO{`Yv{Q)j2BNMmhmX%lHX`D#tCRJl z^gLTYcugynd?Ard2HjXTVM8FP6G+fbHD3fnzg4;bgC52?^Y#6k4G;gq*a^puCjb>u zD7V{+Rhx~$5L?+Hocj^r{Z@I&X-x39E0rpXX}mhRA7Sy82m+!|EU~p(bblx|45HYY z9>RfuSPAfHRt5c5)(TQ%2xIC#JUiw%;>`6p%?IVCO5CgwC+i$Y0EStYht_Y-WXv%2-4+aF>kpORXq|;c37-t^( z`0in&TH(79Hr#g@1F->GkwH}IUy5|M&@mrlAr_K?H>nZevki6AbHykg8ch;YtJ_MA zsk`JCU08;iRHb zy24DBqWHML4FY0-9M|k5y~$YhNiRcC-7o)RqisiFK)@XcXtQaC6(8#mW2=J$=IcFt z{bFgi#FOs8TdHM%fEd@DT5Z?9&wN^=DBskK%ZOtTFh_u|>)>1H@|_UHGQ+B<{QCMW zrL9Vq7Ig`3t|Nd!bYs?E{8f(KyL*#%fwOruT&7krxv!UTtR8pF;XPT}o7MK$Fn_gz zC1>Zc6ZbRONwP5FC7}Co2X94-SvWX8AKK??RI8@qB`KYp**l010Wq!XPj?<=8R_*+ zovk^S(-)^?9{9y6Idv=as$Pl>t5=>?^nP@8{)N_%*Q8)4GX_1h3rZpj}Q=N)Y9zj$t*j|-=3&FX^$)gwLBKJ3Sy1Q7KrA) zJ$u>4pEt1U*RJUhDwRq$>5VC@dQkNTNVi>0W)uIogqFwyBCb8)qM`fFtk}D&3|JJ3 zJQ_3)r*V(J3{Lp9pMbprfOiCUvM6V3+;o4OVx0PDO(87w`+V&(55(h}B}>_t zi@tau9z2RR?b;O*5AN2!N&xFxEs!6=xR8MEQ-SogC%!09^kW?cE^@h+`T4&9@h|`p z&o2a|sJc)nCe!yY?c3mrcMs4UdUrQ!T74f=r(gm9j_rwT*N&Z!6uE4%L&wm0<*Ij0bcldEF0SHJ@y!rQTy?uTv>XJ2X zy1_{|UmMc2ye|u+nO3|hykY&%MdIOITDkNgd4oB6l`-5>y$FI}P; z(07ZB>B9N*?7itT*oJlMb=_N^d_0@+0d{VlEahEf7&zKbz_9V5q{cdoi|yzC6@A!z zvau76#YI3VuIz{?H$$5zvAMf9u+&t$KTgBD4io`2z|5QDZf4483T~m~`^jcYcIo0p z#;w8JCjkkd*;%Ei^#bEq0=kwjpIs&87Z+dOpV=!dhd7oDVskVBw{L+YXs6oAOsN`d z_kak`f_{mwow*TZ@O!jUkQ-mWhjVx>$*8X{ZDC9%CC9$;CQH)t7(X;OTDO#3YJ(l7 z73Ax;m5pp=_uHFkv^aM>0k>BXyO8mwh_}yTyW-FX=C6dh=`W6mOhj>#647QmD*U!IjITSgb|RB}Oh5fw>#SN#DwJ$VMD|0zMzrbSJa-W~y#gphBTw9lLb4sAyfEd0OZ1PF61a_inmHVc@9< z0(^Q^dX|3YLkW(On-$Zw) zSxkxHEK&m45X;hvnUi>(Wo1^h)O$((_^yk5_eFOwK1w~{4Yv~xUUWWR7gMe*xCxx&1p?w6u0_ZC z`R=tWEwx9{7j1KOClTQ-k=VNB+4Hr^GL7QVdCO_k-SySU+wAL8w^=q?9wpx}d~lq% zIOq#cZntw7pV#i|_aTYLt0jYg4`&g8fH;e6QSr{-t6>=#Nh~MZvgt-e#ZiQt+*`wB zUg{#Qh`@=uN)$3hhFq!MDpx39q88X=IoTP1(xa`|Wq`gUBrPlYv)Ta~7C?(VcuRwS z5MTB}(`j9d^BPB;mn_xH3T3auHjrbmASirqGjs63Om|=ru&$P zAHCSSa@jg{CCO#-TxwlhlgpLcWD1>N+$*UxszrMpLy$K3RnL;tmQEjVw1WM!miNO>HVMURWfod?E? zs!WaMI#X-HNIb3=bs28bB!F31w$|H%ePJ}M_h))Ady%F2t6CM)<|}9(tDMg5ezKN0 zE~1;oN&ggdIdOHML}j@$nLJY_m*1n~n@lc0M8|6~UJfgIuUs3-?!EM2hup064f!&y zNC$0fD3l9>=r*h3Gpe!+^$NN$2LdGJ>l-MU1eHX zGPNR(()BYmE^);uj8hN2mmY}C&&f{6%ge2zmGdDI7^?_S>yLL+vRq|-0QpA8LE{$FSruPU_yz2qMrxz1K!DAA(Y&}-SPo$3T@2y-vOEb+A)e9tr9MMcLRX*8N!wEa&6&9uTzBk~)-UI8im zq!4*wkv-c9Y(IbXi5)jHMyX`7j+K0r)hl_cyvup3WZoneCG|;F^3|}&ioR_3jWpXc z;Lag+T~ss$m(Qk>@`pGaQAfVMTbN4Sg$Bz#R6QL5Q95y83`9D0^+-s?x{RQjtGGL2F}6Q)X%iASO@Uwvd}g zpK#HjZ<**s!=2Y{SX#;a6P~_)6R68-wxvPfPj~|Mwm={t4ywc7PMr(P%+vlxkLfVJ zO-DwaR!gg{^K)q%ql{c_9=Y-1(_Ff)n?FU{deH1yS-y-$(&^!@n@LNLBsrgL%d^C% zRe961pC3`HK~9UimEN?vPbFt^H3bONC}rjSePtT8>n1~u+k2jEzwW`U5S?i7mIhh= zp?6(PI_L(Hax@yR8_dhA4Ru)^mvS*YG4Bu%5!E`0h|i6Ty!{ou6?I*eE&d%2UoS7_ z3L&{k-=65S$-}y}4c$|Gl{i`&g>P%#s`d5zmiDuHvG4_)NriyD0)p>>IIyPMJy9c_ z#G}!rwA@n;jMi*ad3kv(IW3Ldp%0CxXJ%4+#(A&B6?`;oKn>Gb$7Zu9$%|TI%2$Tx z)2Ctc?xpI!Bnj~_sY;bPmH7vBf_Rv4@vu{LVL73~sU@O5m7H_ri2zj{Cq!>HO8%`4 zwMxajsB4S=I@+YuTX@vQlkW8$4@1`!AJgEi3@3`PqwB*sz69-5<5MRd^QMQ7?^gO$ zXLlMccgW{DotRrhT>=4n-#VcpB>=YdGAW7TU{coKp=C6ow#H#^IQ|h14Jirl<5DRd zH|uWG#UwWApKJDJPq^@0HE7C|ZrT)A4r+G5SU^CnPGR0Y18KlxqXoX5Sx)H+e=#Y9D;@q9$y z8gqkI7LJ* z98?`jWe)T4na+ksOm|R>h(?42ARr>Fl;W~9WYghZ9V>gUuIQ~4QCpyrvpHgg& z2LUBP00JV3YI>N-*iDD(gqGGEu3uWix8s$(IdLzOjoEdX(W*(^_Y7$fWWiUl%=Xuj zXYQYigT8x7cO&b?Lh?|gS^#gKGfb`K!f{ZlML@o-BLD%hj+ZEWPfa|m&SLTtovQi< zi6TRGW@7Exdu+}jfx5En%sYY;0gWb!c0e3S!m$iIJc|GXM8fqQv-$9%-qn0##X2rx z4chnrxz2vQh<)F!VJ5g=l`5Tt<8AtA$1K6~f(!^k00JTicE=?ev-wEJ@N(*nHT~6& zTd3i<>{%RBE{$1C{QVL;d^^M9Fz_f40hKC?gkuiPl6W0k=L%E@B1skkB9e-*CmSE_ zJ?eEmOM9eq72n!shYsvX#06oKv-8-f9T(Z<)NB`|h9s;epd{h&_FBjk%JEpwVKqIs z3;+RfqiTJ5)8WaTD|x?5AMKFLEG<*~H`8<2=$#kYtqiO;Gi4*#xk{bJyfmNDEVeh; z$W~Z(FIYv0FJTCX`0B*29L?HutV~uu+aFocySiNw2^jc7N;Vs}>ms|Cg=dw3)xZbG z@6sZ`59mW3?+Ka@BraM45D?K;jeQv#y7kDIh_afG9`{pOw?y2&WE~dzTyhqhu=^53 z#9>4&jEMJvdU>6uPXoS5!hu;;7Gm>MZx9eSuKBO_pHTmolDoNWMeo)ch4gy0AR;cj zIr(P+y}WkN0>Du;9ru_EQ$|t}CIA7EaOGK=^_lyQH_9z<8>A6%M6wHUS8Tvk8kR{xQG(`&_*M)xvs#YF`m$ ztHi9_&B|qy_grD8Xw^E50|@ZehQ@0jZL9nS8y24EfEW>lhZ2B*cwoi9z30DiK|acN zAM;guNu~H1x%q6y-`CilYj{bvsm}P=hQ{kMZK1q`wttyHp96#%+XNDUj0r$MjA?k# zF@3N~w*5xtq-x&sw`vEdy`@lezWvo#$8NLlPTbLLg(ZbN3!X=f<_-;rucj`lX*9NR zMezI}18zkC0^(NHX^&Y`PA&gG`i^dC+hTBSs9jf6*!%mhv$P!AZ5O660p8M3 ztM8I%Y@%(CXHXZ_zlA;E3*VFU92ri|n?=8~rju;^GLG|f> zU1i4|y%oG60V)0JL_>3px~G0-O64pX3_l~K>?5zzB_LG+NzhJ}E_6xcocg!|cHheU zxQ4ItxvDoK$&ocb0O=|UVntQu~_QC&z`lzrSrDxp%qA0CY zRYVlIbOk`+_1eS$SIa<*{3A^_hDt zPyeMU5oOQ%tjOj0G?;ON`lbFPyPWI_7SjYPVyq{L5Kbfj0dXQ+ca55h$*Gjwc9+s~ z{#VUg(X6(=+MD;qi@}_|pT%a=7pnffBeA{VRjN$tjyg|Mrhcc7|NcmOiEI%APvijv z%n^Wqn4^Ph77-vpDZBetR=iTC9az;{5%id^N+qz6v`~7RNSIg#c3v2kXXoeFc}HP(B|nb%?lC(0VffF zfH(;(qAg6||NcBt>&U(AI6sA~bvcc^R%LHRDVpJ>a>X}!lc*K)_3=Awl1ThP?tik_rdbY43lV{U2nj$yL|7@Fl%;Wr$Eu!7&FSZ( zkTvsD%IXBFWR**)rtnuOWHeyUln!XX+@vp6@ulp)6elFS)xZa@WwKoQ z%_1R4qu={%?W61<00JOj2LT9(9qI5JlQm;Ye9|S-E1Oy@g z0TBp1k_7<}00F5JfPhH729bXd009sXhyVmcAn-^Q1V8`;q)q?=BJ~!3mK51f)&?0wVPqME*el1VBI_ z0uT^^z#~}@009t?IspiX)N2s=2LTWO0f7iWKm-DhWI+G~KtSpQARtn&LF69DM7j00JNY0)h~LfCz#ei3(33K|59WOdtm!AbkQ*5$V?dd5|BF?0XxIq8}K){U& zKtSBMa#1u8009tiCIJYDGwC935C8!XaAN`x5I3$|6b%GG00f*#00QDnx`-PDKmY{X zm;eOCjVl*L0|5{K0cR3`fH;#b;syZ_00B2900D91%0!3m zz>Nt&K-{=;Q8W+$0T6H|0SJgQ=^}0r009tiV*(HmH?CY14Fo^{1e{3#0^&@%h#Lez z00i8a00hL1D;Grr0T2KIXA*#bIFl~o1_2NN0XHT90deEXMbSV21VF%<1Rx;Jq>H#g z00cn5jR`0T6Iw0uT^4u3Qui1V8`;oJjxz;!L`T z+j;^C+NstDLI@B50l^4BMFc~Sgh2oVKtS3AARyANKja<+KmY^;BLD#r3_TJC0T2KI zX%m2eNW1=!dk_Et5D<(21Vk|ONEie_00g8>00JWI`a|wP00cllFai(|!O$aN5C8!X zkTwAbh_veuxd#Cd00F@WKtKdTkAy)01VBLA1Rx;Nu0P}+1V8`;1S0?e5ez*N1_2NN z0cjI}fJnRkkb4jS0T2+300cxZ^hg*4KmY`!O#lKS?fOIRK>!3mKrjLj5W&zRVGsZT z5Rf(j2#B=n54i^c5C8$e2tYstLyv?(00cll+5{jV(yl+`9t1!D1Oy`h0TB#65(WVf z00C(efPhH5{*ZeR00D^-nB6gcvqZCAINMN>!aQ&W0w4eaAYdT@2#AHC;0**o00cmw zFaZciVIDXG0T2KI5U`K{1jIs6@CE`P00JOTm;eN%Fb|x800@8p2v|q}0%9R3cmn|t z009svOaKB>m 1.5.0~rc1 > 1.5.0~git20150128.112847.17e840a > 1.5.0~dev~git20150128.112847.17e840a + fi + + debSource="$(awk -F ': ' '$1 == "Source" { print $2; exit }' hack/make/.build-deb/control)" + debMaintainer="$(awk -F ': ' '$1 == "Maintainer" { print $2; exit }' hack/make/.build-deb/control)" + debDate="$(date --rfc-2822)" + + # if go-md2man is available, pre-generate the man pages + ./man/md2man-all.sh -q || true + # TODO decide if it's worth getting go-md2man in _each_ builder environment to avoid this + + builderDir="contrib/builder/deb/${PACKAGE_ARCH}" + pkgs=( $(find "${builderDir}/"*/ -type d) ) + if [ ! -z "$DOCKER_BUILD_PKGS" ]; then + pkgs=( $(echo ${DOCKER_BUILD_PKGS[@]/#/$builderDir\/}) ) + fi + for dir in "${pkgs[@]}"; do + [ -d "$dir" ] || { echo >&2 "skipping nonexistent $dir"; continue; } + version="$(basename "$dir")" + suite="${version##*-}" + + image="dockercore/builder-deb:$version" + if ! docker inspect "$image" &> /dev/null; then + ( set -x && docker build ${DOCKER_BUILD_ARGS} -t "$image" "$dir" ) + fi + + mkdir -p "$DEST/$version" + cat > "$DEST/$version/Dockerfile.build" <<-EOF + FROM $image + WORKDIR /usr/src/docker + COPY . /usr/src/docker + RUN mkdir -p /go/src/github.com/docker && mkdir -p /go/src/github.com/opencontainers \ + && ln -snf /usr/src/docker /go/src/github.com/docker/docker + EOF + + # get the RUNC and CONTAINERD commit from the root Dockerfile, this keeps the commits in sync + awk '$1 == "ENV" && $2 == "RUNC_COMMIT" { print; exit }' Dockerfile >> "$DEST/$version/Dockerfile.build" + awk '$1 == "ENV" && $2 == "CONTAINERD_COMMIT" { print; exit }' Dockerfile >> "$DEST/$version/Dockerfile.build" + + # add runc and containerd compile and install + cat >> "$DEST/$version/Dockerfile.build" <<-EOF + # Install runc + RUN git clone git://github.com/opencontainers/runc.git "/go/src/github.com/opencontainers/runc" \ + && cd "/go/src/github.com/opencontainers/runc" \ + && git checkout -q "\$RUNC_COMMIT" + RUN set -x && export GOPATH="/go" && cd "/go/src/github.com/opencontainers/runc" \ + && make BUILDTAGS="\$RUNC_BUILDTAGS" && make install + # Install containerd + RUN git clone git://github.com/docker/containerd.git "/go/src/github.com/docker/containerd" \ + && cd "/go/src/github.com/docker/containerd" \ + && git checkout -q "\$CONTAINERD_COMMIT" + RUN set -x && export GOPATH="/go" && cd "/go/src/github.com/docker/containerd" && make && make install + EOF + if [ "$DOCKER_EXPERIMENTAL" ]; then + echo 'ENV DOCKER_EXPERIMENTAL 1' >> "$DEST/$version/Dockerfile.build" + fi + cat >> "$DEST/$version/Dockerfile.build" <<-EOF + RUN cp -aL hack/make/.build-deb debian + RUN { echo '$debSource (${debVersion}-0~${suite}) $suite; urgency=low'; echo; echo ' * Version: $VERSION'; echo; echo " -- $debMaintainer $debDate"; } > debian/changelog && cat >&2 debian/changelog + RUN dpkg-buildpackage -uc -us + EOF + tempImage="docker-temp/build-deb:$version" + ( set -x && docker build -t "$tempImage" -f "$DEST/$version/Dockerfile.build" . ) + docker run --rm "$tempImage" bash -c 'cd .. && tar -c *_*' | tar -xvC "$DEST/$version" + docker rmi "$tempImage" + done + + bundle .integration-daemon-stop +) 2>&1 | tee -a "$DEST/test.log" diff --git a/hack/make/build-rpm b/hack/make/build-rpm new file mode 100644 index 00000000..f0e496bb --- /dev/null +++ b/hack/make/build-rpm @@ -0,0 +1,153 @@ +#!/bin/bash +set -e + +# subshell so that we can export PATH and TZ without breaking other things +( + export TZ=UTC # make sure our "date" variables are UTC-based + + source "$(dirname "$BASH_SOURCE")/.integration-daemon-start" + source "$(dirname "$BASH_SOURCE")/.detect-daemon-osarch" + + # TODO consider using frozen images for the dockercore/builder-rpm tags + + rpmName=docker-engine + rpmVersion="$VERSION" + rpmRelease=1 + + # rpmRelease versioning is as follows + # Docker 1.7.0: version=1.7.0, release=1 + # Docker 1.7.0-rc1: version=1.7.0, release=0.1.rc1 + # Docker 1.7.0-cs1: version=1.7.0.cs1, release=1 + # Docker 1.7.0-cs1-rc1: version=1.7.0.cs1, release=0.1.rc1 + # Docker 1.7.0-dev nightly: version=1.7.0, release=0.0.YYYYMMDD.HHMMSS.gitHASH + + # if we have a "-rc*" suffix, set appropriate release + if [[ "$rpmVersion" =~ .*-rc[0-9]+$ ]] ; then + rcVersion=${rpmVersion#*-rc} + rpmVersion=${rpmVersion%-rc*} + rpmRelease="0.${rcVersion}.rc${rcVersion}" + fi + + DOCKER_GITCOMMIT=$(git rev-parse --short HEAD) + if [ -n "$(git status --porcelain --untracked-files=no)" ]; then + DOCKER_GITCOMMIT="$DOCKER_GITCOMMIT-unsupported" + fi + + # if we have a "-dev" suffix or have change in Git, let's make this package version more complex so it works better + if [[ "$rpmVersion" == *-dev ]] || [ -n "$(git status --porcelain)" ]; then + gitUnix="$(git log -1 --pretty='%at')" + gitDate="$(date --date "@$gitUnix" +'%Y%m%d.%H%M%S')" + gitCommit="$(git log -1 --pretty='%h')" + gitVersion="${gitDate}.git${gitCommit}" + # gitVersion is now something like '20150128.112847.17e840a' + rpmVersion="${rpmVersion%-dev}" + rpmRelease="0.0.$gitVersion" + fi + + # Replace any other dashes with periods + rpmVersion="${rpmVersion/-/.}" + + rpmPackager="$(awk -F ': ' '$1 == "Packager" { print $2; exit }' hack/make/.build-rpm/${rpmName}.spec)" + rpmDate="$(date +'%a %b %d %Y')" + + # if go-md2man is available, pre-generate the man pages + ./man/md2man-all.sh -q || true + # TODO decide if it's worth getting go-md2man in _each_ builder environment to avoid this + + # Convert the CHANGELOG.md file into RPM changelog format + VERSION_REGEX="^\W\W (.*) \((.*)\)$" + ENTRY_REGEX="^[-+*] (.*)$" + while read -r line || [[ -n "$line" ]]; do + if [ -z "$line" ]; then continue; fi + if [[ "$line" =~ $VERSION_REGEX ]]; then + echo >> contrib/builder/rpm/${PACKAGE_ARCH}/changelog + echo "* `date -d ${BASH_REMATCH[2]} '+%a %b %d %Y'` ${rpmPackager} - ${BASH_REMATCH[1]}" >> contrib/builder/rpm/${PACKAGE_ARCH}/changelog + fi + if [[ "$line" =~ $ENTRY_REGEX ]]; then + echo "- ${BASH_REMATCH[1]//\`}" >> contrib/builder/rpm/${PACKAGE_ARCH}/changelog + fi + done < CHANGELOG.md + + builderDir="contrib/builder/rpm/${PACKAGE_ARCH}" + pkgs=( $(find "${builderDir}/"*/ -type d) ) + if [ ! -z "$DOCKER_BUILD_PKGS" ]; then + pkgs=( $(echo ${DOCKER_BUILD_PKGS[@]/#/$builderDir\/}) ) + fi + for dir in "${pkgs[@]}"; do + [ -d "$dir" ] || { echo >&2 "skipping nonexistent $dir"; continue; } + version="$(basename "$dir")" + suite="${version##*-}" + + image="dockercore/builder-rpm:$version" + if ! docker inspect "$image" &> /dev/null; then + ( set -x && docker build ${DOCKER_BUILD_ARGS} -t "$image" "$dir" ) + fi + + mkdir -p "$DEST/$version" + cat > "$DEST/$version/Dockerfile.build" <<-EOF + FROM $image + COPY . /usr/src/${rpmName} + RUN mkdir -p /go/src/github.com/docker && mkdir -p /go/src/github.com/opencontainers + EOF + + # get the RUNC and CONTAINERD commit from the root Dockerfile, this keeps the commits in sync + awk '$1 == "ENV" && $2 == "RUNC_COMMIT" { print; exit }' Dockerfile >> "$DEST/$version/Dockerfile.build" + awk '$1 == "ENV" && $2 == "CONTAINERD_COMMIT" { print; exit }' Dockerfile >> "$DEST/$version/Dockerfile.build" + + # add runc and containerd compile and install + cat >> "$DEST/$version/Dockerfile.build" <<-EOF + # Install runc + RUN git clone git://github.com/opencontainers/runc.git "/go/src/github.com/opencontainers/runc" \ + && cd "/go/src/github.com/opencontainers/runc" \ + && git checkout -q "\$RUNC_COMMIT" + RUN set -x && export GOPATH="/go" && cd "/go/src/github.com/opencontainers/runc" \ + && make BUILDTAGS="\$RUNC_BUILDTAGS" && make install + # Install containerd + RUN git clone git://github.com/docker/containerd.git "/go/src/github.com/docker/containerd" \ + && cd "/go/src/github.com/docker/containerd" \ + && git checkout -q "\$CONTAINERD_COMMIT" + RUN set -x && export GOPATH="/go" && cd "/go/src/github.com/docker/containerd" && make && make install + EOF + if [ "$DOCKER_EXPERIMENTAL" ]; then + echo 'ENV DOCKER_EXPERIMENTAL 1' >> "$DEST/$version/Dockerfile.build" + fi + cat >> "$DEST/$version/Dockerfile.build" <<-EOF + RUN mkdir -p /root/rpmbuild/SOURCES \ + && echo '%_topdir /root/rpmbuild' > /root/.rpmmacros + WORKDIR /root/rpmbuild + RUN ln -sfv /usr/src/${rpmName}/hack/make/.build-rpm SPECS + WORKDIR /root/rpmbuild/SPECS + RUN tar -r -C /usr/src -f /root/rpmbuild/SOURCES/${rpmName}.tar ${rpmName} + RUN tar -r -C /go/src/github.com/docker -f /root/rpmbuild/SOURCES/${rpmName}.tar containerd + RUN tar -r -C /go/src/github.com/opencontainers -f /root/rpmbuild/SOURCES/${rpmName}.tar runc + RUN gzip /root/rpmbuild/SOURCES/${rpmName}.tar + RUN { cat /usr/src/${rpmName}/contrib/builder/rpm/${PACKAGE_ARCH}/changelog; } >> ${rpmName}.spec && tail >&2 ${rpmName}.spec + RUN rpmbuild -ba \ + --define '_gitcommit $DOCKER_GITCOMMIT' \ + --define '_release $rpmRelease' \ + --define '_version $rpmVersion' \ + --define '_origversion $VERSION' \ + --define '_experimental ${DOCKER_EXPERIMENTAL:-0}' \ + ${rpmName}.spec + EOF + # selinux policy referencing systemd things won't work on non-systemd versions + # of centos or rhel, which we don't support anyways + if [ "${suite%.*}" -gt 6 ] && [[ "$version" != opensuse* ]]; then + cat >> "$DEST/$version/Dockerfile.build" <<-EOF + RUN tar -cz -C /usr/src/${rpmName}/contrib -f /root/rpmbuild/SOURCES/${rpmName}-selinux.tar.gz ${rpmName}-selinux + RUN rpmbuild -ba \ + --define '_gitcommit $DOCKER_GITCOMMIT' \ + --define '_release $rpmRelease' \ + --define '_version $rpmVersion' \ + --define '_origversion $VERSION' \ + ${rpmName}-selinux.spec + EOF + fi + tempImage="docker-temp/build-rpm:$version" + ( set -x && docker build -t "$tempImage" -f $DEST/$version/Dockerfile.build . ) + docker run --rm "$tempImage" bash -c 'cd /root/rpmbuild && tar -c *RPMS' | tar -xvC "$DEST/$version" + docker rmi "$tempImage" + done + + source "$(dirname "$BASH_SOURCE")/.integration-daemon-stop" +) 2>&1 | tee -a $DEST/test.log diff --git a/hack/make/clean-apt-repo b/hack/make/clean-apt-repo new file mode 100755 index 00000000..1c37d98e --- /dev/null +++ b/hack/make/clean-apt-repo @@ -0,0 +1,43 @@ +#!/bin/bash +set -e + +# This script cleans the experimental pool for the apt repo. +# This is useful when there are a lot of old experimental debs and you only want to keep the most recent. +# + +: ${DOCKER_RELEASE_DIR:=$DEST} +APTDIR=$DOCKER_RELEASE_DIR/apt/repo/pool/experimental +: ${DOCKER_ARCHIVE_DIR:=$DEST/archive} +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +latest_versions=$(dpkg-scanpackages "$APTDIR" /dev/null 2>/dev/null | awk -F ': ' '$1 == "Filename" { print $2 }') + +# get the latest version +latest_docker_engine_file=$(echo "$latest_versions" | grep docker-engine) +latest_docker_engine_version=$(basename ${latest_docker_engine_file%~*}) + +echo "latest docker-engine version: $latest_docker_engine_version" + +# remove all the files that are not that version in experimental +pool_dir=$(dirname "$latest_docker_engine_file") +old_pkgs=( $(ls "$pool_dir" | grep -v "^${latest_docker_engine_version}" | grep "${latest_docker_engine_version%%~git*}") ) + +echo "${old_pkgs[@]}" + +mkdir -p "$DOCKER_ARCHIVE_DIR" +for old_pkg in "${old_pkgs[@]}"; do + echo "moving ${pool_dir}/${old_pkg} to $DOCKER_ARCHIVE_DIR" + mv "${pool_dir}/${old_pkg}" "$DOCKER_ARCHIVE_DIR" +done + +echo +echo "$pool_dir now has contents:" +ls "$pool_dir" + +# now regenerate release files for experimental +export COMPONENT=experimental +source "${DIR}/update-apt-repo" + +echo "You will now want to: " +echo " - re-sign the repo with hack/make/sign-repo" +echo " - re-generate index files with hack/make/generate-index-listing" diff --git a/hack/make/clean-yum-repo b/hack/make/clean-yum-repo new file mode 100755 index 00000000..1cafbbd9 --- /dev/null +++ b/hack/make/clean-yum-repo @@ -0,0 +1,20 @@ +#!/bin/bash +set -e + +# This script cleans the experimental pool for the yum repo. +# This is useful when there are a lot of old experimental rpms and you only want to keep the most recent. +# + +: ${DOCKER_RELEASE_DIR:=$DEST} +YUMDIR=$DOCKER_RELEASE_DIR/yum/repo/experimental + +suites=( $(find "$YUMDIR" -mindepth 1 -maxdepth 1 -type d) ) + +for suite in "${suites[@]}"; do + echo "cleanup in: $suite" + ( set -x; repomanage -k2 --old "$suite" | xargs rm -f ) +done + +echo "You will now want to: " +echo " - re-sign the repo with hack/make/sign-repo" +echo " - re-generate index files with hack/make/generate-index-listing" diff --git a/hack/make/cover b/hack/make/cover new file mode 100644 index 00000000..624943b8 --- /dev/null +++ b/hack/make/cover @@ -0,0 +1,20 @@ +#!/bin/bash +set -e + +bundle_cover() { + coverprofiles=( "$DEST/../"*"/coverprofiles/"* ) + for p in "${coverprofiles[@]}"; do + echo + ( + set -x + go tool cover -func="$p" + ) + done +} + +if [ "$HAVE_GO_TEST_COVER" ]; then + bundle_cover 2>&1 | tee "$DEST/report.log" +else + echo >&2 'warning: the current version of go does not support -cover' + echo >&2 ' skipping test coverage report' +fi diff --git a/hack/make/cross b/hack/make/cross new file mode 100644 index 00000000..ea8eee6c --- /dev/null +++ b/hack/make/cross @@ -0,0 +1,34 @@ +#!/bin/bash +set -e + +# explicit list of os/arch combos that support being a daemon +declare -A daemonSupporting +daemonSupporting=( + [linux/amd64]=1 + [windows/amd64]=1 +) + +# if we have our linux/amd64 version compiled, let's symlink it in +if [ -x "$DEST/../binary/docker-$VERSION" ]; then + mkdir -p "$DEST/linux/amd64" + ( + cd "$DEST/linux/amd64" + ln -s ../../../binary/* ./ + ) + echo "Created symlinks:" "$DEST/linux/amd64/"* +fi + +for platform in $DOCKER_CROSSPLATFORMS; do + ( + export DEST="$DEST/$platform" # bundles/VERSION/cross/GOOS/GOARCH/docker-VERSION + mkdir -p "$DEST" + ABS_DEST="$(cd "$DEST" && pwd -P)" + export GOOS=${platform%/*} + export GOARCH=${platform##*/} + if [ -z "${daemonSupporting[$platform]}" ]; then + export LDFLAGS_STATIC_DOCKER="" # we just need a simple client for these platforms + export BUILDFLAGS=( "${ORIG_BUILDFLAGS[@]/ daemon/}" ) # remove the "daemon" build tag from platforms that aren't supported + fi + source "${MAKEDIR}/binary" + ) +done diff --git a/hack/make/dynbinary b/hack/make/dynbinary new file mode 100644 index 00000000..1d1a8e3d --- /dev/null +++ b/hack/make/dynbinary @@ -0,0 +1,10 @@ +#!/bin/bash +set -e + +( + export IAMSTATIC="false" + export LDFLAGS_STATIC_DOCKER='' + export BUILDFLAGS=( "${BUILDFLAGS[@]/netgo /}" ) # disable netgo, since we don't need it for a dynamic binary + export BUILDFLAGS=( "${BUILDFLAGS[@]/static_build /}" ) # we're not building a "static" binary here + source "${MAKEDIR}/binary" +) diff --git a/hack/make/dyngccgo b/hack/make/dyngccgo new file mode 100644 index 00000000..a9019e8c --- /dev/null +++ b/hack/make/dyngccgo @@ -0,0 +1,11 @@ +#!/bin/bash +set -e + +( + export IAMSTATIC="false" + export EXTLDFLAGS_STATIC='' + export LDFLAGS_STATIC_DOCKER='' + export BUILDFLAGS=( "${BUILDFLAGS[@]/netgo /}" ) # disable netgo, since we don't need it for a dynamic binary + export BUILDFLAGS=( "${BUILDFLAGS[@]/static_build /}" ) # we're not building a "static" binary here + source "${MAKEDIR}/gccgo" +) diff --git a/hack/make/gccgo b/hack/make/gccgo new file mode 100644 index 00000000..9da203ff --- /dev/null +++ b/hack/make/gccgo @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +BINARY_NAME="docker-$VERSION" +BINARY_EXTENSION="$(binary_extension)" +BINARY_FULLNAME="$BINARY_NAME$BINARY_EXTENSION" + +source "${MAKEDIR}/.go-autogen" + +if [[ "${BUILDFLAGS[@]}" =~ 'netgo ' ]]; then + EXTLDFLAGS_STATIC+=' -lnetgo' +fi +# gccgo require explicit flag -pthread to allow goroutines to work. +go build -compiler=gccgo \ + -o "$DEST/$BINARY_FULLNAME" \ + "${BUILDFLAGS[@]}" \ + -gccgoflags " + -g + $EXTLDFLAGS_STATIC + -Wl,--no-export-dynamic + -ldl + -pthread + " \ + ./docker + +echo "Created binary: $DEST/$BINARY_FULLNAME" +ln -sf "$BINARY_FULLNAME" "$DEST/docker$BINARY_EXTENSION" + +copy_containerd "$DEST" "hash" +hash_files "$DEST/$BINARY_FULLNAME" diff --git a/hack/make/generate-index-listing b/hack/make/generate-index-listing new file mode 100755 index 00000000..1167ed12 --- /dev/null +++ b/hack/make/generate-index-listing @@ -0,0 +1,74 @@ +#!/bin/bash +set -e + +# This script generates index files for the directory structure +# of the apt and yum repos + +: ${DOCKER_RELEASE_DIR:=$DEST} +APTDIR=$DOCKER_RELEASE_DIR/apt +YUMDIR=$DOCKER_RELEASE_DIR/yum + +if [ ! -d $APTDIR ] && [ ! -d $YUMDIR ]; then + echo >&2 'release-rpm or release-deb must be run before generate-index-listing' + exit 1 +fi + +create_index() { + local directory=$1 + local original=$2 + local cleaned=${directory#$original} + + # the index file to create + local index_file="${directory}/index" + + # cd into dir & touch the index file + cd $directory + touch $index_file + + # print the html header + cat <<-EOF > "$index_file" + + + Index of ${cleaned}/ + +

Index of ${cleaned}/


+
../
+	EOF
+
+	# start of content output
+	(
+	# change IFS locally within subshell so the for loop saves line correctly to L var
+	IFS=$'\n';
+
+	# pretty sweet, will mimick the normal apache output
+	for L in $(find -L . -mount -depth -maxdepth 1 -type f ! -name 'index' -printf "%f|@_@%Td-%Tb-%TY %Tk:%TM  @%f@\n"|sort|column -t -s '|' | sed 's,\([\ ]\+\)@_@,\1,g');
+	do
+		# file
+		F=$(sed -e 's,^.*@\([^@]\+\)@.*$,\1,g'<<<"$L");
+
+		# file with file size
+		F=$(du -bh $F | cut -f1);
+
+		# output with correct format
+		sed -e 's,\ @.*$, '"$F"',g'<<<"$L";
+	done;
+	) >> $index_file;
+
+	# now output a list of all directories in this dir (maxdepth 1) other than '.' outputting in a sorted manner exactly like apache
+	find -L . -mount -depth -maxdepth 1 -type d ! -name '.' -printf "%-43f@_@%Td-%Tb-%TY %Tk:%TM  -\n"|sort -d|sed 's,\([\ ]\+\)@_@,/\1,g' >> $index_file
+
+	# print the footer html
+	echo "

" >> $index_file + +} + +get_dirs() { + local directory=$1 + + for d in `find ${directory} -type d`; do + create_index $d $directory + done +} + +get_dirs $APTDIR +get_dirs $YUMDIR diff --git a/hack/make/install-script b/hack/make/install-script new file mode 100644 index 00000000..feadac2f --- /dev/null +++ b/hack/make/install-script @@ -0,0 +1,63 @@ +#!/bin/bash +set -e + +# This script modifies the install.sh script for domains and keys other than +# those used by the primary opensource releases. +# +# You can provide `url`, `yum_url`, `apt_url` and optionally `gpg_fingerprint` +# or `GPG_KEYID` as environment variables, or the defaults for open source are used. +# +# The lower-case variables are substituted into install.sh. +# +# gpg_fingerprint and GPG_KEYID are optional, defaulting to the opensource release +# key ("releasedocker"). Other GPG_KEYIDs will require you to mount a volume with +# the correct contents to /root/.gnupg. +# +# It outputs the modified `install.sh` file to $DOCKER_RELEASE_DIR (default: $DEST) +# +# Example usage: +# +# docker run \ +# --rm \ +# --privileged \ +# -e "GPG_KEYID=deadbeef" \ +# -e "GNUPGHOME=/root/.gnupg" \ +# -v $HOME/.gnupg:/root/.gnupg \ +# -v $(pwd):/go/src/github.com/docker/docker/bundles \ +# "$IMAGE_DOCKER" \ +# hack/make.sh install-script + +: ${DOCKER_RELEASE_DIR:=$DEST} +: ${GPG_KEYID:=releasedocker} + +DEFAULT_URL="https://get.docker.com/" +DEFAULT_APT_URL="https://apt.dockerproject.org" +DEFAULT_YUM_URL="https://yum.dockerproject.org" +DEFAULT_GPG_FINGERPRINT="58118E89F3A912897C070ADBF76221572C52609D" + +: ${url:=$DEFAULT_URL} +: ${apt_url:=$DEFAULT_APT_URL} +: ${yum_url:=$DEFAULT_YUM_URL} +if [[ "$GPG_KEYID" == "releasedocker" ]] ; then + : ${gpg_fingerprint:=$DEFAULT_GPG_FINGERPRINT} +fi + +DEST_FILE="$DOCKER_RELEASE_DIR/install.sh" + +bundle_install_script() { + mkdir -p "$DOCKER_RELEASE_DIR" + + if [[ -z "$gpg_fingerprint" ]] ; then + # NOTE: if no key matching key is in /root/.gnupg, this will fail + gpg_fingerprint=$(gpg --with-fingerprint -k "$GPG_KEYID" | grep "Key fingerprint" | awk -F "=" '{print $2};' | tr -d ' ') + fi + + cp hack/install.sh "$DEST_FILE" + sed -i.bak 's#^url=".*"$#url="'"$url"'"#' "$DEST_FILE" + sed -i.bak 's#^apt_url=".*"$#apt_url="'"$apt_url"'"#' "$DEST_FILE" + sed -i.bak 's#^yum_url=".*"$#yum_url="'"$yum_url"'"#' "$DEST_FILE" + sed -i.bak 's#^gpg_fingerprint=".*"$#gpg_fingerprint="'"$gpg_fingerprint"'"#' "$DEST_FILE" + rm "${DEST_FILE}.bak" +} + +bundle_install_script diff --git a/hack/make/release-deb b/hack/make/release-deb new file mode 100755 index 00000000..946e5de8 --- /dev/null +++ b/hack/make/release-deb @@ -0,0 +1,150 @@ +#!/bin/bash +set -e + +# This script creates the apt repos for the .deb files generated by hack/make/build-deb +# +# The following can then be used as apt sources: +# deb http://apt.dockerproject.org/repo $distro-$release $version +# +# For example: +# deb http://apt.dockerproject.org/repo ubuntu-trusty main +# deb http://apt.dockerproject.org/repo ubuntu-trusty testing +# deb http://apt.dockerproject.org/repo debian-wheezy experimental +# deb http://apt.dockerproject.org/repo debian-jessie main +# +# ... and so on and so forth for the builds created by hack/make/build-deb + +source "$(dirname "$BASH_SOURCE")/.integration-daemon-start" +source "$(dirname "$BASH_SOURCE")/.detect-daemon-osarch" + +: ${DOCKER_RELEASE_DIR:=$DEST} +: ${GPG_KEYID:=releasedocker} +APTDIR=$DOCKER_RELEASE_DIR/apt/repo + +# setup the apt repo (if it does not exist) +mkdir -p "$APTDIR/conf" "$APTDIR/db" + +# supported arches/sections +arches=( amd64 i386 ) + +# Preserve existing components but don't add any non-existing ones +for component in main testing experimental ; do + if ls "$APTDIR/dists/*/$component" >/dev/null 2>&1 ; then + components+=( $component ) + fi +done + +# set the component for the version being released +component="main" + +if [[ "$VERSION" == *-rc* ]]; then + component="testing" +fi + +if [ "$DOCKER_EXPERIMENTAL" ] || [[ "$VERSION" == *-dev ]] || [ -n "$(git status --porcelain)" ]; then + component="experimental" +fi + +# Make sure our component is in the list of components +if [[ ! "${components[*]}" =~ $component ]] ; then + components+=( $component ) +fi + +# create apt-ftparchive file on every run. This is essential to avoid +# using stale versions of the config file that could cause unnecessary +# refreshing of bits for EOL-ed releases. +cat <<-EOF > "$APTDIR/conf/apt-ftparchive.conf" +Dir { + ArchiveDir "${APTDIR}"; + CacheDir "${APTDIR}/db"; +}; + +Default { + Packages::Compress ". gzip bzip2"; + Sources::Compress ". gzip bzip2"; + Contents::Compress ". gzip bzip2"; +}; + +TreeDefault { + BinCacheDB "packages-\$(SECTION)-\$(ARCH).db"; + Directory "pool/\$(SECTION)"; + Packages "\$(DIST)/\$(SECTION)/binary-\$(ARCH)/Packages"; + SrcDirectory "pool/\$(SECTION)"; + Sources "\$(DIST)/\$(SECTION)/source/Sources"; + Contents "\$(DIST)/\$(SECTION)/Contents-\$(ARCH)"; + FileList "$APTDIR/\$(DIST)/\$(SECTION)/filelist"; +}; +EOF + +for dir in contrib/builder/deb/${PACKAGE_ARCH}/*/; do + version="$(basename "$dir")" + suite="${version//debootstrap-}" + + cat <<-EOF + Tree "dists/${suite}" { + Sections "${components[*]}"; + Architectures "${arches[*]}"; + } + + EOF +done >> "$APTDIR/conf/apt-ftparchive.conf" + +if [ ! -f "$APTDIR/conf/docker-engine-release.conf" ]; then + cat <<-EOF > "$APTDIR/conf/docker-engine-release.conf" + APT::FTPArchive::Release::Origin "Docker"; + APT::FTPArchive::Release::Components "${components[*]}"; + APT::FTPArchive::Release::Label "Docker APT Repository"; + APT::FTPArchive::Release::Architectures "${arches[*]}"; + EOF +fi + +# release the debs +for dir in contrib/builder/deb/${PACKAGE_ARCH}/*/; do + version="$(basename "$dir")" + codename="${version//debootstrap-}" + + DEBFILE=( "bundles/$VERSION/build-deb/$version/docker-engine"*.deb ) + + # if we have a $GPG_PASSPHRASE we may as well + # dpkg-sign before copying the deb into the pool + if [ ! -z "$GPG_PASSPHRASE" ]; then + dpkg-sig -g "--no-tty --passphrase '$GPG_PASSPHRASE'" \ + -k "$GPG_KEYID" --sign builder "${DEBFILE[@]}" + fi + + # add the deb for each component for the distro version into the pool + mkdir -p "$APTDIR/pool/$component/d/docker-engine/" + cp "${DEBFILE[@]}" "$APTDIR/pool/$component/d/docker-engine/" + + # update the filelist for this codename/component + mkdir -p "$APTDIR/dists/$codename/$component" + find "$APTDIR/pool/$component" \ + -name *~${codename#*-}*.deb > "$APTDIR/dists/$codename/$component/filelist" +done + +# run the apt-ftparchive commands so we can have pinning +apt-ftparchive generate "$APTDIR/conf/apt-ftparchive.conf" + +for dir in contrib/builder/deb/${PACKAGE_ARCH}/*/; do + version="$(basename "$dir")" + codename="${version//debootstrap-}" + + apt-ftparchive \ + -o "APT::FTPArchive::Release::Codename=$codename" \ + -o "APT::FTPArchive::Release::Suite=$codename" \ + -c "$APTDIR/conf/docker-engine-release.conf" \ + release \ + "$APTDIR/dists/$codename" > "$APTDIR/dists/$codename/Release" + + for arch in "${arches[@]}"; do + mkdir -p "$APTDIR/dists/$codename/$component/binary-$arch" + apt-ftparchive \ + -o "APT::FTPArchive::Release::Codename=$codename" \ + -o "APT::FTPArchive::Release::Suite=$codename" \ + -o "APT::FTPArchive::Release::Component=$component" \ + -o "APT::FTPArchive::Release::Architecture=$arch" \ + -c "$APTDIR/conf/docker-engine-release.conf" \ + release \ + "$APTDIR/dists/$codename/$component/binary-$arch" > "$APTDIR/dists/$codename/$component/binary-$arch/Release" + done +done diff --git a/hack/make/release-rpm b/hack/make/release-rpm new file mode 100755 index 00000000..b952b79e --- /dev/null +++ b/hack/make/release-rpm @@ -0,0 +1,78 @@ +#!/bin/bash +set -e + +# This script creates the yum repos for the .rpm files generated by hack/make/build-rpm +# +# The following can then be used as a yum repo: +# http://yum.dockerproject.org/repo/$release/$distro/$distro-version +# +# For example: +# http://yum.dockerproject.org/repo/main/fedora/23 +# http://yum.dockerproject.org/repo/testing/centos/7 +# http://yum.dockerproject.org/repo/experimental/fedora/23 +# http://yum.dockerproject.org/repo/main/centos/7 +# +# ... and so on and so forth for the builds created by hack/make/build-rpm + +source "$(dirname "$BASH_SOURCE")/.integration-daemon-start" +source "$(dirname "$BASH_SOURCE")/.detect-daemon-osarch" + +: ${DOCKER_RELEASE_DIR:=$DEST} +YUMDIR=$DOCKER_RELEASE_DIR/yum/repo +: ${GPG_KEYID:=releasedocker} + +# manage the repos for each distribution separately +distros=( fedora centos opensuse oraclelinux ) + +# get the release +release="main" + +if [[ "$VERSION" == *-rc* ]]; then + release="testing" +fi + +if [ $DOCKER_EXPERIMENTAL ] || [[ "$VERSION" == *-dev ]] || [ -n "$(git status --porcelain)" ]; then + release="experimental" +fi + +for distro in "${distros[@]}"; do + # Setup the yum repo + REPO=$YUMDIR/$release/$distro + + for dir in contrib/builder/rpm/${PACKAGE_ARCH}/$distro-*/; do + version="$(basename "$dir")" + suite="${version##*-}" + + # if the directory does not exist, initialize the yum repo + if [[ ! -d $REPO/$suite/Packages ]]; then + mkdir -p "$REPO/$suite/Packages" + + createrepo --pretty "$REPO/$suite" + fi + + # path to rpms + RPMFILE=( "bundles/$VERSION/build-rpm/$version/RPMS/"*"/docker-engine"*.rpm "bundles/$VERSION/build-rpm/$version/SRPMS/docker-engine"*.rpm ) + + # if we have a $GPG_PASSPHRASE we may as well + # sign the rpms before adding to repo + if [ ! -z $GPG_PASSPHRASE ]; then + # export our key to rpm import + gpg --armor --export "$GPG_KEYID" > /tmp/gpg + rpm --import /tmp/gpg + + # sign the rpms + echo "yes" | setsid rpm \ + --define "_gpg_name $GPG_KEYID" \ + --define "_signature gpg" \ + --define "__gpg_check_password_cmd /bin/true" \ + --define "__gpg_sign_cmd %{__gpg} gpg --batch --no-armor --passphrase '$GPG_PASSPHRASE' --no-secmem-warning -u '%{_gpg_name}' --sign --detach-sign --output %{__signature_filename} %{__plaintext_filename}" \ + --resign "${RPMFILE[@]}" + fi + + # copy the rpms to the packages folder + cp "${RPMFILE[@]}" "$REPO/$suite/Packages" + + # update the repo + createrepo --pretty --update "$REPO/$suite" + done +done diff --git a/hack/make/sign-repos b/hack/make/sign-repos new file mode 100755 index 00000000..93b640d7 --- /dev/null +++ b/hack/make/sign-repos @@ -0,0 +1,55 @@ +#!/bin/bash + +# This script signs the deliverables from release-deb and release-rpm +# with a designated GPG key. + +: ${DOCKER_RELEASE_DIR:=$DEST} +: ${GPG_KEYID:=releasedocker} +APTDIR=$DOCKER_RELEASE_DIR/apt/repo +YUMDIR=$DOCKER_RELEASE_DIR/yum/repo + +if [ -z "$GPG_PASSPHRASE" ]; then + echo >&2 'you need to set GPG_PASSPHRASE in order to sign artifacts' + exit 1 +fi + +if [ ! -d $APTDIR ] && [ ! -d $YUMDIR ]; then + echo >&2 'release-rpm or release-deb must be run before sign-repos' + exit 1 +fi + +sign_packages(){ + # sign apt repo metadata + if [ -d $APTDIR ]; then + # create file with public key + gpg --armor --export "$GPG_KEYID" > "$DOCKER_RELEASE_DIR/apt/gpg" + + # sign the repo metadata + for F in $(find $APTDIR -name Release); do + if test "$F" -nt "$F.gpg" ; then + gpg -u "$GPG_KEYID" --passphrase "$GPG_PASSPHRASE" \ + --armor --sign --detach-sign \ + --batch --yes \ + --output "$F.gpg" "$F" + fi + done + fi + + # sign yum repo metadata + if [ -d $YUMDIR ]; then + # create file with public key + gpg --armor --export "$GPG_KEYID" > "$DOCKER_RELEASE_DIR/yum/gpg" + + # sign the repo metadata + for F in $(find $YUMDIR -name repomd.xml); do + if test "$F" -nt "$F.asc" ; then + gpg -u "$GPG_KEYID" --passphrase "$GPG_PASSPHRASE" \ + --armor --sign --detach-sign \ + --batch --yes \ + --output "$F.asc" "$F" + fi + done + fi +} + +sign_packages diff --git a/hack/make/test-deb-install b/hack/make/test-deb-install new file mode 100755 index 00000000..c4482cd4 --- /dev/null +++ b/hack/make/test-deb-install @@ -0,0 +1,68 @@ +#!/bin/bash +# This script is used for testing install.sh and that it works for +# each of component of our apt and yum repos +set -e + +: ${DEB_DIR:="$(pwd)/bundles/$(cat VERSION)/build-deb"} + +if [[ ! -d "${DEB_DIR}" ]]; then + echo "you must first run `make deb` or hack/make/build-deb" + exit 1 +fi + +test_deb_install(){ + # test for each Dockerfile in contrib/builder + + builderDir="contrib/builder/deb/${PACKAGE_ARCH}" + pkgs=( $(find "${builderDir}/"*/ -type d) ) + if [ ! -z "$DOCKER_BUILD_PKGS" ]; then + pkgs=( $(echo ${DOCKER_BUILD_PKGS[@]/#/$builderDir\/}) ) + fi + for dir in "${pkgs[@]}"; do + [ -d "$dir" ] || { echo >&2 "skipping nonexistent $dir"; continue; } + local from="$(awk 'toupper($1) == "FROM" { print $2; exit }' "$dir/Dockerfile")" + local dir=$(basename "$dir") + + if [[ ! -d "${DEB_DIR}/${dir}" ]]; then + echo "No deb found for ${dir}" + exit 1 + fi + + local script=$(mktemp /tmp/install-XXXXXXXXXX.sh) + cat <<-EOF > "${script}" + #!/bin/bash + set -e + set -x + + apt-get update && apt-get install -y apparmor + + dpkg -i /root/debs/*.deb || true + + apt-get install -yf + + /etc/init.d/apparmor start + + # this will do everything _except_ load the profile into the kernel + ( + cd /etc/apparmor.d + /sbin/apparmor_parser --skip-kernel-load docker-engine + ) + EOF + + chmod +x "${script}" + + echo "testing deb install for ${from}" + docker run --rm -i --privileged \ + -v ${DEB_DIR}/${dir}:/root/debs \ + -v ${script}:/install.sh \ + ${from} /install.sh + + rm -f ${script} + done +} + +( + bundle .integration-daemon-start + test_deb_install + bundle .integration-daemon-stop +) 2>&1 | tee -a "$DEST/test.log" diff --git a/hack/make/test-docker-py b/hack/make/test-docker-py new file mode 100644 index 00000000..dece8315 --- /dev/null +++ b/hack/make/test-docker-py @@ -0,0 +1,18 @@ +#!/bin/bash +set -e + +# subshell so that we can export PATH without breaking other things +( + bundle .integration-daemon-start + + dockerPy='/docker-py' + [ -d "$dockerPy" ] || { + dockerPy="$DEST/docker-py" + git clone https://github.com/docker/docker-py.git "$dockerPy" + } + + # exporting PYTHONPATH to import "docker" from our local docker-py + test_env PYTHONPATH="$dockerPy" py.test "$dockerPy/tests/integration" + + bundle .integration-daemon-stop +) 2>&1 | tee -a "$DEST/test.log" diff --git a/hack/make/test-install-script b/hack/make/test-install-script new file mode 100755 index 00000000..4782cbea --- /dev/null +++ b/hack/make/test-install-script @@ -0,0 +1,31 @@ +#!/bin/bash +# This script is used for testing install.sh and that it works for +# each of component of our apt and yum repos +set -e + +test_install_script(){ + # these are equivalent to main, testing, experimental components + # in the repos, but its the url that will do the conversion + components=( experimental test get ) + + for component in "${components[@]}"; do + # change url to specific component for testing + local test_url=https://${component}.docker.com + local script=$(mktemp /tmp/install-XXXXXXXXXX.sh) + sed "s,url='https://get.docker.com/',url='${test_url}/'," hack/install.sh > "${script}" + + chmod +x "${script}" + + # test for each Dockerfile in contrib/builder + for dir in contrib/builder/*/*/; do + local from="$(awk 'toupper($1) == "FROM" { print $2; exit }' "$dir/Dockerfile")" + + echo "running install.sh for ${component} with ${from}" + docker run --rm -i -v ${script}:/install.sh ${from} /install.sh + done + + rm -f ${script} + done +} + +test_install_script diff --git a/hack/make/test-integration-cli b/hack/make/test-integration-cli new file mode 100644 index 00000000..27897aeb --- /dev/null +++ b/hack/make/test-integration-cli @@ -0,0 +1,18 @@ +#!/bin/bash +set -e + +bundle_test_integration_cli() { + TESTFLAGS="$TESTFLAGS -check.v -check.timeout=${TIMEOUT} -timeout=360m" + go_test_dir ./integration-cli +} + +# subshell so that we can export PATH without breaking other things +( + bundle .integration-daemon-start + + bundle .integration-daemon-setup + + bundle_test_integration_cli + + bundle .integration-daemon-stop +) 2>&1 | tee -a "$DEST/test.log" diff --git a/hack/make/test-old-apt-repo b/hack/make/test-old-apt-repo new file mode 100755 index 00000000..bb20128e --- /dev/null +++ b/hack/make/test-old-apt-repo @@ -0,0 +1,29 @@ +#!/bin/bash +set -e + +versions=( 1.3.3 1.4.1 1.5.0 1.6.2 ) + +install() { + local version=$1 + local tmpdir=$(mktemp -d /tmp/XXXXXXXXXX) + local dockerfile="${tmpdir}/Dockerfile" + cat <<-EOF > "$dockerfile" + FROM debian:jessie + ENV VERSION ${version} + RUN apt-get update && apt-get install -y \ + apt-transport-https \ + ca-certificates \ + --no-install-recommends + RUN echo "deb https://get.docker.com/ubuntu docker main" > /etc/apt/sources.list.d/docker.list + RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 \ + --recv-keys 36A1D7869245C8950F966E92D8576A8BA88D21E9 + RUN apt-get update && apt-get install -y \ + lxc-docker-\${VERSION} + EOF + + docker build --rm --force-rm --no-cache -t docker-old-repo:${version} -f $dockerfile $tmpdir +} + +for v in "${versions[@]}"; do + install "$v" +done diff --git a/hack/make/test-unit b/hack/make/test-unit new file mode 100644 index 00000000..eee97896 --- /dev/null +++ b/hack/make/test-unit @@ -0,0 +1,35 @@ +#!/bin/bash +set -e + +# Run Docker's test suite, including sub-packages, and store their output as a bundle +# If $TESTFLAGS is set in the environment, it is passed as extra arguments to 'go test'. +# You can use this to select certain tests to run, eg. +# +# TESTFLAGS='-test.run ^TestBuild$' ./hack/make.sh test-unit +# +bundle_test_unit() { + TESTFLAGS+=" -test.timeout=${TIMEOUT}" + date + if [ -z "$TESTDIRS" ]; then + TEST_PATH=./... + else + TEST_PATH=./${TESTDIRS} + fi + pkg_list=$(go list -e \ + -f '{{if ne .Name "github.com/docker/docker"}} + {{.ImportPath}} + {{end}}' \ + "${BUILDFLAGS[@]}" $TEST_PATH \ + | grep github.com/docker/docker \ + | grep -v github.com/docker/docker/vendor \ + | grep -v github.com/docker/docker/integration-cli) + go test $COVER $GCCGOFLAGS -ldflags "$LDFLAGS" "${BUILDFLAGS[@]}" $TESTFLAGS $pkg_list +} + + +if [[ "$(go version)" == *"gccgo"* ]]; then + GCCGOFLAGS=-gccgoflags="-lpthread" +else + COVER=-cover +fi +bundle_test_unit 2>&1 | tee -a "$DEST/test.log" diff --git a/hack/make/tgz b/hack/make/tgz new file mode 100644 index 00000000..40aa0101 --- /dev/null +++ b/hack/make/tgz @@ -0,0 +1,69 @@ +#!/bin/bash + +CROSS="$DEST/../cross" + +set -e + +if [ ! -d "$CROSS/linux/amd64" ]; then + echo >&2 'error: binary and cross must be run before tgz' + false +fi + +( +for d in "$CROSS/"*/*; do + export GOARCH="$(basename "$d")" + export GOOS="$(basename "$(dirname "$d")")" + BINARY_NAME="docker-$VERSION" + BINARY_EXTENSION="$(export GOOS && binary_extension)" + if [ "$GOOS" = 'windows' ]; then + # if windows use a zip, not tgz + BUNDLE_EXTENSION=".zip" + IS_TAR="false" + else + BUNDLE_EXTENSION=".tgz" + IS_TAR="true" + fi + BINARY_FULLNAME="$BINARY_NAME$BINARY_EXTENSION" + mkdir -p "$DEST/$GOOS/$GOARCH" + TGZ="$DEST/$GOOS/$GOARCH/$BINARY_NAME$BUNDLE_EXTENSION" + + # The staging directory for the files in the tgz + BUILD_PATH="$DEST/build" + + # The directory that is at the root of the tar file + TAR_BASE_DIRECTORY="docker" + + # $DEST/build/docker + TAR_PATH="$BUILD_PATH/$TAR_BASE_DIRECTORY" + + # Copy the correct docker binary + mkdir -p $TAR_PATH + cp -L "$d/$BINARY_FULLNAME" "$TAR_PATH/docker$BINARY_EXTENSION" + + # copy over all the containerd binaries + copy_containerd $TAR_PATH + + if [ "$IS_TAR" == "true" ]; then + echo "Creating tgz from $BUILD_PATH and naming it $TGZ" + tar --numeric-owner --owner 0 -C "$BUILD_PATH" -czf "$TGZ" $TAR_BASE_DIRECTORY + else + # ZIP needs to full absolute dir path, not the absolute path + ZIP=`pwd`"/$TGZ" + # keep track of where we are, for later. + pushd . + # go into the BUILD_PATH since zip does not have a -C equivalent. + cd $BUILD_PATH + echo "Creating zip from $BUILD_PATH and naming it $ZIP" + zip -q -r $ZIP $TAR_BASE_DIRECTORY + # go back to where we started + popd + fi + + hash_files "$TGZ" + + # cleanup after ourselves + rm -rf "$BUILD_PATH" + + echo "Created tgz: $TGZ" +done +) diff --git a/hack/make/ubuntu b/hack/make/ubuntu new file mode 100644 index 00000000..0421dc36 --- /dev/null +++ b/hack/make/ubuntu @@ -0,0 +1,190 @@ +#!/bin/bash + +PKGVERSION="${VERSION//-/'~'}" +# if we have a "-dev" suffix or have change in Git, let's make this package version more complex so it works better +if [[ "$VERSION" == *-dev ]] || [ -n "$(git status --porcelain)" ]; then + GIT_UNIX="$(git log -1 --pretty='%at')" + GIT_DATE="$(date --date "@$GIT_UNIX" +'%Y%m%d.%H%M%S')" + GIT_COMMIT="$(git log -1 --pretty='%h')" + GIT_VERSION="git${GIT_DATE}.0.${GIT_COMMIT}" + # GIT_VERSION is now something like 'git20150128.112847.0.17e840a' + PKGVERSION="$PKGVERSION~$GIT_VERSION" +fi + +# $ dpkg --compare-versions 1.5.0 gt 1.5.0~rc1 && echo true || echo false +# true +# $ dpkg --compare-versions 1.5.0~rc1 gt 1.5.0~git20150128.112847.17e840a && echo true || echo false +# true +# $ dpkg --compare-versions 1.5.0~git20150128.112847.17e840a gt 1.5.0~dev~git20150128.112847.17e840a && echo true || echo false +# true + +# ie, 1.5.0 > 1.5.0~rc1 > 1.5.0~git20150128.112847.17e840a > 1.5.0~dev~git20150128.112847.17e840a + +PACKAGE_ARCHITECTURE="$(dpkg-architecture -qDEB_HOST_ARCH)" +PACKAGE_URL="https://www.docker.com/" +PACKAGE_MAINTAINER="support@docker.com" +PACKAGE_DESCRIPTION="Linux container runtime +Docker complements LXC with a high-level API which operates at the process +level. It runs unix processes with strong guarantees of isolation and +repeatability across servers. +Docker is a great building block for automating distributed systems: +large-scale web deployments, database clusters, continuous deployment systems, +private PaaS, service-oriented architectures, etc." +PACKAGE_LICENSE="Apache-2.0" + +# Build docker as an ubuntu package using FPM and REPREPRO (sue me). +# bundle_binary must be called first. +bundle_ubuntu() { + DIR="$ABS_DEST/build" + + # Include our udev rules + mkdir -p "$DIR/etc/udev/rules.d" + cp contrib/udev/80-docker.rules "$DIR/etc/udev/rules.d/" + + # Include our init scripts + mkdir -p "$DIR/etc/init" + cp contrib/init/upstart/docker.conf "$DIR/etc/init/" + mkdir -p "$DIR/etc/init.d" + cp contrib/init/sysvinit-debian/docker "$DIR/etc/init.d/" + mkdir -p "$DIR/etc/default" + cp contrib/init/sysvinit-debian/docker.default "$DIR/etc/default/docker" + mkdir -p "$DIR/lib/systemd/system" + cp contrib/init/systemd/docker.{service,socket} "$DIR/lib/systemd/system/" + + # Include contributed completions + mkdir -p "$DIR/etc/bash_completion.d" + cp contrib/completion/bash/docker "$DIR/etc/bash_completion.d/" + mkdir -p "$DIR/usr/share/zsh/vendor-completions" + cp contrib/completion/zsh/_docker "$DIR/usr/share/zsh/vendor-completions/" + mkdir -p "$DIR/etc/fish/completions" + cp contrib/completion/fish/docker.fish "$DIR/etc/fish/completions/" + + # Include contributed man pages + man/md2man-all.sh -q + manRoot="$DIR/usr/share/man" + mkdir -p "$manRoot" + for manDir in man/man?; do + manBase="$(basename "$manDir")" # "man1" + for manFile in "$manDir"/*; do + manName="$(basename "$manFile")" # "docker-build.1" + mkdir -p "$manRoot/$manBase" + gzip -c "$manFile" > "$manRoot/$manBase/$manName.gz" + done + done + + # Copy the binary + # This will fail if the binary bundle hasn't been built + mkdir -p "$DIR/usr/bin" + cp "$DEST/../binary/docker-$VERSION" "$DIR/usr/bin/docker" + + # Generate postinst/prerm/postrm scripts + cat > "$DEST/postinst" <<'EOF' +#!/bin/sh +set -e +set -u + +if [ "$1" = 'configure' ] && [ -z "$2" ]; then + if ! getent group docker > /dev/null; then + groupadd --system docker + fi +fi + +if ! { [ -x /sbin/initctl ] && /sbin/initctl version 2>/dev/null | grep -q upstart; }; then + # we only need to do this if upstart isn't in charge + update-rc.d docker defaults > /dev/null || true +fi +if [ -n "$2" ]; then + _dh_action=restart +else + _dh_action=start +fi +service docker $_dh_action 2>/dev/null || true + +#DEBHELPER# +EOF + cat > "$DEST/prerm" <<'EOF' +#!/bin/sh +set -e +set -u + +service docker stop 2>/dev/null || true + +#DEBHELPER# +EOF + cat > "$DEST/postrm" <<'EOF' +#!/bin/sh +set -e +set -u + +if [ "$1" = "purge" ] ; then + update-rc.d docker remove > /dev/null || true +fi + +# In case this system is running systemd, we make systemd reload the unit files +# to pick up changes. +if [ -d /run/systemd/system ] ; then + systemctl --system daemon-reload > /dev/null || true +fi + +#DEBHELPER# +EOF + # TODO swaths of these were borrowed from debhelper's auto-inserted stuff, because we're still using fpm - we need to use debhelper instead, and somehow reconcile Ubuntu that way + chmod +x "$DEST/postinst" "$DEST/prerm" "$DEST/postrm" + + ( + # switch directories so we create *.deb in the right folder + cd "$DEST" + + # create lxc-docker-VERSION package + fpm -s dir -C "$DIR" \ + --name "lxc-docker-$VERSION" --version "$PKGVERSION" \ + --after-install "$ABS_DEST/postinst" \ + --before-remove "$ABS_DEST/prerm" \ + --after-remove "$ABS_DEST/postrm" \ + --architecture "$PACKAGE_ARCHITECTURE" \ + --prefix / \ + --depends iptables \ + --deb-recommends aufs-tools \ + --deb-recommends ca-certificates \ + --deb-recommends git \ + --deb-recommends xz-utils \ + --deb-recommends 'cgroupfs-mount | cgroup-lite' \ + --deb-suggests apparmor \ + --description "$PACKAGE_DESCRIPTION" \ + --maintainer "$PACKAGE_MAINTAINER" \ + --conflicts docker \ + --conflicts docker.io \ + --conflicts lxc-docker-virtual-package \ + --provides lxc-docker \ + --provides lxc-docker-virtual-package \ + --replaces lxc-docker \ + --replaces lxc-docker-virtual-package \ + --url "$PACKAGE_URL" \ + --license "$PACKAGE_LICENSE" \ + --config-files /etc/udev/rules.d/80-docker.rules \ + --config-files /etc/init/docker.conf \ + --config-files /etc/init.d/docker \ + --config-files /etc/default/docker \ + --deb-compression gz \ + -t deb . + # TODO replace "Suggests: cgroup-lite" with "Recommends: cgroupfs-mount | cgroup-lite" once cgroupfs-mount is available + + # create empty lxc-docker wrapper package + fpm -s empty \ + --name lxc-docker --version "$PKGVERSION" \ + --architecture "$PACKAGE_ARCHITECTURE" \ + --depends lxc-docker-$VERSION \ + --description "$PACKAGE_DESCRIPTION" \ + --maintainer "$PACKAGE_MAINTAINER" \ + --url "$PACKAGE_URL" \ + --license "$PACKAGE_LICENSE" \ + --deb-compression gz \ + -t deb + ) + + # clean up after ourselves so we have a clean output directory + rm "$DEST/postinst" "$DEST/prerm" "$DEST/postrm" + rm -r "$DIR" +} + +bundle_ubuntu diff --git a/hack/make/update-apt-repo b/hack/make/update-apt-repo new file mode 100755 index 00000000..7354a2ec --- /dev/null +++ b/hack/make/update-apt-repo @@ -0,0 +1,70 @@ +#!/bin/bash +set -e + +# This script updates the apt repo in $DOCKER_RELEASE_DIR/apt/repo. +# This script is a "fix all" for any sort of problems that might have occurred with +# the Release or Package files in the repo. +# It should only be used in the rare case of extreme emergencies to regenerate +# Release and Package files for the apt repo. +# +# NOTE: Always be sure to re-sign the repo with hack/make/sign-repos after running +# this script. + +: ${DOCKER_RELEASE_DIR:=$DEST} +APTDIR=$DOCKER_RELEASE_DIR/apt/repo + +# supported arches/sections +arches=( amd64 i386 ) + +# Preserve existing components but don't add any non-existing ones +for component in main testing experimental ; do + if ls "$APTDIR/dists/*/$component" >/dev/null 2>&1 ; then + components+=( $component ) + fi +done + +dists=( $(find "${APTDIR}/dists" -maxdepth 1 -mindepth 1 -type d) ) + +# override component if it is set +if [ "$COMPONENT" ]; then + components=( $COMPONENT ) +fi + +# release the debs +for version in "${dists[@]}"; do + for component in "${components[@]}"; do + codename="${version//debootstrap-}" + + # update the filelist for this codename/component + find "$APTDIR/pool/$component" \ + -name *~${codename#*-}*.deb > "$APTDIR/dists/$codename/$component/filelist" + done +done + +# run the apt-ftparchive commands so we can have pinning +apt-ftparchive generate "$APTDIR/conf/apt-ftparchive.conf" + +for dist in "${dists[@]}"; do + version=$(basename "$dist") + for component in "${components[@]}"; do + codename="${version//debootstrap-}" + + apt-ftparchive \ + -o "APT::FTPArchive::Release::Codename=$codename" \ + -o "APT::FTPArchive::Release::Suite=$codename" \ + -c "$APTDIR/conf/docker-engine-release.conf" \ + release \ + "$APTDIR/dists/$codename" > "$APTDIR/dists/$codename/Release" + + for arch in "${arches[@]}"; do + apt-ftparchive \ + -o "APT::FTPArchive::Release::Codename=$codename" \ + -o "APT::FTPArchive::Release::Suite=$codename" \ + -o "APT::FTPArchive::Release::Component=$component" \ + -o "APT::FTPArchive::Release::Architecture=$arch" \ + -c "$APTDIR/conf/docker-engine-release.conf" \ + release \ + "$APTDIR/dists/$codename/$component/binary-$arch" > "$APTDIR/dists/$codename/$component/binary-$arch/Release" + done + done +done diff --git a/hack/make/validate-dco b/hack/make/validate-dco new file mode 100644 index 00000000..5ac98728 --- /dev/null +++ b/hack/make/validate-dco @@ -0,0 +1,54 @@ +#!/bin/bash + +source "${MAKEDIR}/.validate" + +adds=$(validate_diff --numstat | awk '{ s += $1 } END { print s }') +dels=$(validate_diff --numstat | awk '{ s += $2 } END { print s }') +#notDocs="$(validate_diff --numstat | awk '$3 !~ /^docs\// { print $3 }')" + +: ${adds:=0} +: ${dels:=0} + +# "Username may only contain alphanumeric characters or dashes and cannot begin with a dash" +githubUsernameRegex='[a-zA-Z0-9][a-zA-Z0-9-]+' + +# https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work +dcoPrefix='Signed-off-by:' +dcoRegex="^(Docker-DCO-1.1-)?$dcoPrefix ([^<]+) <([^<>@]+@[^<>]+)>( \\(github: ($githubUsernameRegex)\\))?$" + +check_dco() { + grep -qE "$dcoRegex" +} + +if [ $adds -eq 0 -a $dels -eq 0 ]; then + echo '0 adds, 0 deletions; nothing to validate! :)' +else + commits=( $(validate_log --format='format:%H%n') ) + badCommits=() + for commit in "${commits[@]}"; do + if [ -z "$(git log -1 --format='format:' --name-status "$commit")" ]; then + # no content (ie, Merge commit, etc) + continue + fi + if ! git log -1 --format='format:%B' "$commit" | check_dco; then + badCommits+=( "$commit" ) + fi + done + if [ ${#badCommits[@]} -eq 0 ]; then + echo "Congratulations! All commits are properly signed with the DCO!" + else + { + echo "These commits do not have a proper '$dcoPrefix' marker:" + for commit in "${badCommits[@]}"; do + echo " - $commit" + done + echo + echo 'Please amend each commit to include a properly formatted DCO marker.' + echo + echo 'Visit the following URL for information about the Docker DCO:' + echo ' https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work' + echo + } >&2 + false + fi +fi diff --git a/hack/make/validate-default-seccomp b/hack/make/validate-default-seccomp new file mode 100644 index 00000000..4facec74 --- /dev/null +++ b/hack/make/validate-default-seccomp @@ -0,0 +1,27 @@ +#!/bin/bash + +source "${MAKEDIR}/.validate" + +IFS=$'\n' +files=( $(validate_diff --diff-filter=ACMR --name-only -- 'profiles/seccomp' || true) ) +unset IFS + +if [ ${#files[@]} -gt 0 ]; then + # We run vendor.sh to and see if we have a diff afterwards + go generate ./profiles/seccomp/ >/dev/null + # Let see if the working directory is clean + diffs="$(git status --porcelain -- profiles/seccomp 2>/dev/null)" + if [ "$diffs" ]; then + { + echo 'The result of go generate ./profiles/seccomp/ differs' + echo + echo "$diffs" + echo + echo 'Please re-run go generate ./profiles/seccomp/' + echo + } >&2 + false + else + echo 'Congratulations! Seccomp profile generation is done correctly.' + fi +fi diff --git a/hack/make/validate-gofmt b/hack/make/validate-gofmt new file mode 100644 index 00000000..7ad9e855 --- /dev/null +++ b/hack/make/validate-gofmt @@ -0,0 +1,30 @@ +#!/bin/bash + +source "${MAKEDIR}/.validate" + +IFS=$'\n' +files=( $(validate_diff --diff-filter=ACMR --name-only -- '*.go' | grep -v '^vendor/' || true) ) +unset IFS + +badFiles=() +for f in "${files[@]}"; do + # we use "git show" here to validate that what's committed is formatted + if [ "$(git show "$VALIDATE_HEAD:$f" | gofmt -s -l)" ]; then + badFiles+=( "$f" ) + fi +done + +if [ ${#badFiles[@]} -eq 0 ]; then + echo 'Congratulations! All Go source files are properly formatted.' +else + { + echo "These files are not properly gofmt'd:" + for f in "${badFiles[@]}"; do + echo " - $f" + done + echo + echo 'Please reformat the above files using "gofmt -s -w" and commit the result.' + echo + } >&2 + false +fi diff --git a/hack/make/validate-lint b/hack/make/validate-lint new file mode 100644 index 00000000..df7f2b00 --- /dev/null +++ b/hack/make/validate-lint @@ -0,0 +1,30 @@ +#!/bin/bash + +source "${MAKEDIR}/.validate" + +IFS=$'\n' +files=( $(validate_diff --diff-filter=ACMR --name-only -- '*.go' | grep -v '^vendor/' || true) ) +unset IFS + +errors=() +for f in "${files[@]}"; do + failedLint=$(golint "$f") + if [ "$failedLint" ]; then + errors+=( "$failedLint" ) + fi +done + +if [ ${#errors[@]} -eq 0 ]; then + echo 'Congratulations! All Go source files have been linted.' +else + { + echo "Errors from golint:" + for err in "${errors[@]}"; do + echo "$err" + done + echo + echo 'Please fix the above errors. You can test via "golint" and commit the result.' + echo + } >&2 + false +fi diff --git a/hack/make/validate-pkg b/hack/make/validate-pkg new file mode 100644 index 00000000..d5843417 --- /dev/null +++ b/hack/make/validate-pkg @@ -0,0 +1,32 @@ +#!/bin/bash +set -e + +source "${MAKEDIR}/.validate" + +IFS=$'\n' +files=( $(validate_diff --diff-filter=ACMR --name-only -- 'pkg/*.go' || true) ) +unset IFS + +badFiles=() +for f in "${files[@]}"; do + IFS=$'\n' + badImports=( $(go list -e -f '{{ join .Deps "\n" }}' "$f" | sort -u | grep -vE '^github.com/docker/docker/pkg/' | grep -E '^github.com/docker/docker' || true) ) + unset IFS + + for import in "${badImports[@]}"; do + badFiles+=( "$f imports $import" ) + done +done + +if [ ${#badFiles[@]} -eq 0 ]; then + echo 'Congratulations! "./pkg/..." is safely isolated from internal code.' +else + { + echo 'These files import internal code: (either directly or indirectly)' + for f in "${badFiles[@]}"; do + echo " - $f" + done + echo + } >&2 + false +fi diff --git a/hack/make/validate-test b/hack/make/validate-test new file mode 100644 index 00000000..8dc86f11 --- /dev/null +++ b/hack/make/validate-test @@ -0,0 +1,35 @@ +#!/bin/bash + +# Make sure we're not using gos' Testing package any more in integration-cli + +source "${MAKEDIR}/.validate" + +IFS=$'\n' +files=( $(validate_diff --diff-filter=ACMR --name-only -- 'integration-cli/*.go' || true) ) +unset IFS + +badFiles=() +for f in "${files[@]}"; do + # skip check_test.go since it *does* use the testing package + if [ "$f" = "integration-cli/check_test.go" ]; then + continue + fi + + # we use "git show" here to validate that what's committed doesn't contain golang built-in testing + if git show "$VALIDATE_HEAD:$f" | grep -q testing.T; then + badFiles+=( "$f" ) + fi +done + +if [ ${#badFiles[@]} -eq 0 ]; then + echo 'Congratulations! No testing.T found.' +else + { + echo "These files use the wrong testing infrastructure:" + for f in "${badFiles[@]}"; do + echo " - $f" + done + echo + } >&2 + false +fi diff --git a/hack/make/validate-toml b/hack/make/validate-toml new file mode 100644 index 00000000..f6393c85 --- /dev/null +++ b/hack/make/validate-toml @@ -0,0 +1,30 @@ +#!/bin/bash + +source "${MAKEDIR}/.validate" + +IFS=$'\n' +files=( $(validate_diff --diff-filter=ACMR --name-only -- 'MAINTAINERS' || true) ) +unset IFS + +badFiles=() +for f in "${files[@]}"; do + # we use "git show" here to validate that what's committed has valid toml syntax + if ! git show "$VALIDATE_HEAD:$f" | tomlv /proc/self/fd/0 ; then + badFiles+=( "$f" ) + fi +done + +if [ ${#badFiles[@]} -eq 0 ]; then + echo 'Congratulations! All toml source files changed here have valid syntax.' +else + { + echo "These files are not valid toml:" + for f in "${badFiles[@]}"; do + echo " - $f" + done + echo + echo 'Please reformat the above files as valid toml' + echo + } >&2 + false +fi diff --git a/hack/make/validate-vendor b/hack/make/validate-vendor new file mode 100644 index 00000000..7c2cf33c --- /dev/null +++ b/hack/make/validate-vendor @@ -0,0 +1,27 @@ +#!/bin/bash + +source "${MAKEDIR}/.validate" + +IFS=$'\n' +files=( $(validate_diff --diff-filter=ACMR --name-only -- 'hack/vendor.sh' 'hack/.vendor-helpers.sh' 'vendor/' || true) ) +unset IFS + +if [ ${#files[@]} -gt 0 ]; then + # We run vendor.sh to and see if we have a diff afterwards + ./hack/vendor.sh >/dev/null + # Let see if the working directory is clean + diffs="$(git status --porcelain -- vendor 2>/dev/null)" + if [ "$diffs" ]; then + { + echo 'The result of ./hack/vendor.sh differs' + echo + echo "$diffs" + echo + echo 'Please vendor your package with ./hack/vendor.sh.' + echo + } >&2 + false + else + echo 'Congratulations! All vendoring changes are done the right way.' + fi +fi diff --git a/hack/make/validate-vet b/hack/make/validate-vet new file mode 100644 index 00000000..d543509a --- /dev/null +++ b/hack/make/validate-vet @@ -0,0 +1,31 @@ +#!/bin/bash + +source "${MAKEDIR}/.validate" + +IFS=$'\n' +files=( $(validate_diff --diff-filter=ACMR --name-only -- '*.go' | grep -v '^vendor/' || true) ) +unset IFS + +errors=() +for f in "${files[@]}"; do + failedVet=$(go vet "$f") + if [ "$failedVet" ]; then + errors+=( "$failedVet" ) + fi +done + + +if [ ${#errors[@]} -eq 0 ]; then + echo 'Congratulations! All Go source files have been vetted.' +else + { + echo "Errors from go vet:" + for err in "${errors[@]}"; do + echo " - $err" + done + echo + echo 'Please fix the above errors. You can test via "go vet" and commit the result.' + echo + } >&2 + false +fi diff --git a/hack/make/win b/hack/make/win new file mode 100644 index 00000000..f9f41112 --- /dev/null +++ b/hack/make/win @@ -0,0 +1,20 @@ +#!/bin/bash +set -e + +# explicit list of os/arch combos that support being a daemon +declare -A daemonSupporting +daemonSupporting=( + [linux/amd64]=1 + [windows/amd64]=1 +) +platform="windows/amd64" +export DEST="$DEST/$platform" # bundles/VERSION/cross/GOOS/GOARCH/docker-VERSION +mkdir -p "$DEST" +ABS_DEST="$(cd "$DEST" && pwd -P)" +export GOOS=${platform%/*} +export GOARCH=${platform##*/} +if [ -z "${daemonSupporting[$platform]}" ]; then + export LDFLAGS_STATIC_DOCKER="" # we just need a simple client for these platforms + export BUILDFLAGS=( "${ORIG_BUILDFLAGS[@]/ daemon/}" ) # remove the "daemon" build tag from platforms that aren't supported +fi +source "${MAKEDIR}/binary" diff --git a/hack/release.sh b/hack/release.sh new file mode 100755 index 00000000..25d6af72 --- /dev/null +++ b/hack/release.sh @@ -0,0 +1,318 @@ +#!/usr/bin/env bash +set -e + +# This script looks for bundles built by make.sh, and releases them on a +# public S3 bucket. +# +# Bundles should be available for the VERSION string passed as argument. +# +# The correct way to call this script is inside a container built by the +# official Dockerfile at the root of the Docker source code. The Dockerfile, +# make.sh and release.sh should all be from the same source code revision. + +set -o pipefail + +# Print a usage message and exit. +usage() { + cat >&2 <<'EOF' +To run, I need: +- to be in a container generated by the Dockerfile at the top of the Docker + repository; +- to be provided with the location of an S3 bucket and path, in + environment variables AWS_S3_BUCKET and AWS_S3_BUCKET_PATH (default: ''); +- to be provided with AWS credentials for this S3 bucket, in environment + variables AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY; +- a generous amount of good will and nice manners. +The canonical way to run me is to run the image produced by the Dockerfile: e.g.:" + +docker run -e AWS_S3_BUCKET=test.docker.com \ + -e AWS_ACCESS_KEY_ID \ + -e AWS_SECRET_ACCESS_KEY \ + -e AWS_DEFAULT_REGION \ + -it --privileged \ + docker ./hack/release.sh +EOF + exit 1 +} + +[ "$AWS_S3_BUCKET" ] || usage +[ "$AWS_ACCESS_KEY_ID" ] || usage +[ "$AWS_SECRET_ACCESS_KEY" ] || usage +[ -d /go/src/github.com/docker/docker ] || usage +cd /go/src/github.com/docker/docker +[ -x hack/make.sh ] || usage + +export AWS_DEFAULT_REGION +: ${AWS_DEFAULT_REGION:=us-west-1} + +RELEASE_BUNDLES=( + binary + cross + tgz +) + +if [ "$1" != '--release-regardless-of-test-failure' ]; then + RELEASE_BUNDLES=( + test-unit + "${RELEASE_BUNDLES[@]}" + test-integration-cli + ) +fi + +VERSION=$(< VERSION) +BUCKET=$AWS_S3_BUCKET +BUCKET_PATH=$BUCKET +[[ -n "$AWS_S3_BUCKET_PATH" ]] && BUCKET_PATH+=/$AWS_S3_BUCKET_PATH + +if command -v git &> /dev/null && git rev-parse &> /dev/null; then + if [ -n "$(git status --porcelain --untracked-files=no)" ]; then + echo "You cannot run the release script on a repo with uncommitted changes" + usage + fi +fi + +# These are the 2 keys we've used to sign the deb's +# release (get.docker.com) +# GPG_KEY="36A1D7869245C8950F966E92D8576A8BA88D21E9" +# test (test.docker.com) +# GPG_KEY="740B314AE3941731B942C66ADF4FD13717AAD7D6" + +setup_s3() { + echo "Setting up S3" + # Try creating the bucket. Ignore errors (it might already exist). + aws s3 mb "s3://$BUCKET" 2>/dev/null || true + # Check access to the bucket. + aws s3 ls "s3://$BUCKET" >/dev/null + # Make the bucket accessible through website endpoints. + aws s3 website --index-document index --error-document error "s3://$BUCKET" +} + +# write_to_s3 uploads the contents of standard input to the specified S3 url. +write_to_s3() { + DEST=$1 + F=`mktemp` + cat > "$F" + aws s3 cp --acl public-read --content-type 'text/plain' "$F" "$DEST" + rm -f "$F" +} + +s3_url() { + case "$BUCKET" in + get.docker.com|test.docker.com|experimental.docker.com) + echo "https://$BUCKET_PATH" + ;; + *) + BASE_URL="http://${BUCKET}.s3-website-${AWS_DEFAULT_REGION}.amazonaws.com" + if [[ -n "$AWS_S3_BUCKET_PATH" ]] ; then + echo "$BASE_URL/$AWS_S3_BUCKET_PATH" + else + echo "$BASE_URL" + fi + ;; + esac +} + +build_all() { + echo "Building release" + if ! ./hack/make.sh "${RELEASE_BUNDLES[@]}"; then + echo >&2 + echo >&2 'The build or tests appear to have failed.' + echo >&2 + echo >&2 'You, as the release maintainer, now have a couple options:' + echo >&2 '- delay release and fix issues' + echo >&2 '- delay release and fix issues' + echo >&2 '- did we mention how important this is? issues need fixing :)' + echo >&2 + echo >&2 'As a final LAST RESORT, you (because only you, the release maintainer,' + echo >&2 ' really knows all the hairy problems at hand with the current release' + echo >&2 ' issues) may bypass this checking by running this script again with the' + echo >&2 ' single argument of "--release-regardless-of-test-failure", which will skip' + echo >&2 ' running the test suite, and will only build the binaries and packages. Please' + echo >&2 ' avoid using this if at all possible.' + echo >&2 + echo >&2 'Regardless, we cannot stress enough the scarcity with which this bypass' + echo >&2 ' should be used. If there are release issues, we should always err on the' + echo >&2 ' side of caution.' + echo >&2 + exit 1 + fi +} + +upload_release_build() { + src="$1" + dst="$2" + latest="$3" + + echo + echo "Uploading $src" + echo " to $dst" + echo + aws s3 cp --follow-symlinks --acl public-read "$src" "$dst" + if [ "$latest" ]; then + echo + echo "Copying to $latest" + echo + aws s3 cp --acl public-read "$dst" "$latest" + fi + + # get hash files too (see hash_files() in hack/make.sh) + for hashAlgo in md5 sha256; do + if [ -e "$src.$hashAlgo" ]; then + echo + echo "Uploading $src.$hashAlgo" + echo " to $dst.$hashAlgo" + echo + aws s3 cp --follow-symlinks --acl public-read --content-type='text/plain' "$src.$hashAlgo" "$dst.$hashAlgo" + if [ "$latest" ]; then + echo + echo "Copying to $latest.$hashAlgo" + echo + aws s3 cp --acl public-read "$dst.$hashAlgo" "$latest.$hashAlgo" + fi + fi + done +} + +release_build() { + echo "Releasing binaries" + GOOS=$1 + GOARCH=$2 + + binDir=bundles/$VERSION/cross/$GOOS/$GOARCH + tgzDir=bundles/$VERSION/tgz/$GOOS/$GOARCH + binary=docker-$VERSION + zipExt=".tgz" + binaryExt="" + tgz=$binary$zipExt + + latestBase= + if [ -z "$NOLATEST" ]; then + latestBase=docker-latest + fi + + # we need to map our GOOS and GOARCH to uname values + # see https://en.wikipedia.org/wiki/Uname + # ie, GOOS=linux -> "uname -s"=Linux + + s3Os=$GOOS + case "$s3Os" in + darwin) + s3Os=Darwin + ;; + freebsd) + s3Os=FreeBSD + ;; + linux) + s3Os=Linux + ;; + windows) + # this is windows use the .zip and .exe extentions for the files. + s3Os=Windows + zipExt=".zip" + binaryExt=".exe" + tgz=$binary$zipExt + binary+=$binaryExt + ;; + *) + echo >&2 "error: can't convert $s3Os to an appropriate value for 'uname -s'" + exit 1 + ;; + esac + + s3Arch=$GOARCH + case "$s3Arch" in + amd64) + s3Arch=x86_64 + ;; + 386) + s3Arch=i386 + ;; + arm) + s3Arch=armel + # someday, we might potentially support multiple GOARM values, in which case we might get armhf here too + ;; + *) + echo >&2 "error: can't convert $s3Arch to an appropriate value for 'uname -m'" + exit 1 + ;; + esac + + s3Dir="s3://$BUCKET_PATH/builds/$s3Os/$s3Arch" + # latest= + latestTgz= + if [ "$latestBase" ]; then + # commented out since we aren't uploading binaries right now. + # latest="$s3Dir/$latestBase$binaryExt" + # we don't include the $binaryExt because we don't want docker.exe.zip + latestTgz="$s3Dir/$latestBase$zipExt" + fi + + if [ ! -f "$tgzDir/$tgz" ]; then + echo >&2 "error: can't find $tgzDir/$tgz - was it packaged properly?" + exit 1 + fi + # disable binary uploads for now. Only providing tgz downloads + # upload_release_build "$binDir/$binary" "$s3Dir/$binary" "$latest" + upload_release_build "$tgzDir/$tgz" "$s3Dir/$tgz" "$latestTgz" +} + +# Upload binaries and tgz files to S3 +release_binaries() { + [ -e "bundles/$VERSION/cross/linux/amd64/docker-$VERSION" ] || { + echo >&2 './hack/make.sh must be run before release_binaries' + exit 1 + } + + for d in bundles/$VERSION/cross/*/*; do + GOARCH="$(basename "$d")" + GOOS="$(basename "$(dirname "$d")")" + release_build "$GOOS" "$GOARCH" + done + + # TODO create redirect from builds/*/i686 to builds/*/i386 + + cat < +
+ Layer +
+
+ Images are composed of layers. Image layer is a general + term which may be used to refer to one or both of the following: + +
    +
  1. The metadata for the layer, described in the JSON format.
  2. +
  3. The filesystem changes described by a layer.
  4. +
+ + To refer to the former you may use the term Layer JSON or + Layer Metadata. To refer to the latter you may use the term + Image Filesystem Changeset or Image Diff. +
+
+ Image JSON +
+
+ Each layer has an associated JSON structure which describes some + basic information about the image such as date created, author, and the + ID of its parent image as well as execution/runtime configuration like + its entry point, default arguments, CPU/memory shares, networking, and + volumes. +
+
+ Image Filesystem Changeset +
+
+ Each layer has an archive of the files which have been added, changed, + or deleted relative to its parent layer. Using a layer-based or union + filesystem such as AUFS, or by computing the diff from filesystem + snapshots, the filesystem changeset can be used to present a series of + image layers as if they were one cohesive filesystem. +
+
+ Image ID +
+
+ Each layer is given an ID upon its creation. It is + represented as a hexadecimal encoding of 256 bits, e.g., + a9561eb1b190625c9adb5a9513e72c4dedafc1cb2d4c5236c9a6957ec7dfd5a9. + Image IDs should be sufficiently random so as to be globally unique. + 32 bytes read from /dev/urandom is sufficient for all + practical purposes. Alternatively, an image ID may be derived as a + cryptographic hash of image contents as the result is considered + indistinguishable from random. The choice is left up to implementors. +
+
+ Image Parent +
+
+ Most layer metadata structs contain a parent field which + refers to the Image from which another directly descends. An image + contains a separate JSON metadata file and set of changes relative to + the filesystem of its parent image. Image Ancestor and + Image Descendant are also common terms. +
+
+ Image Checksum +
+
+ Layer metadata structs contain a cryptographic hash of the contents of + the layer's filesystem changeset. Though the set of changes exists as a + simple Tar archive, two archives with identical filenames and content + will have different SHA digests if the last-access or last-modified + times of any entries differ. For this reason, image checksums are + generated using the TarSum algorithm which produces a cryptographic + hash of file contents and selected headers only. Details of this + algorithm are described in the separate TarSum specification. +
+
+ Tag +
+
+ A tag serves to map a descriptive, user-given name to any single image + ID. An image name suffix (the name component after :) is + often referred to as a tag as well, though it strictly refers to the + full name of an image. Acceptable values for a tag suffix are + implementation specific, but they SHOULD be limited to the set of + alphanumeric characters [a-zA-z0-9], punctuation + characters [._-], and MUST NOT contain a : + character. +
+
+ Repository +
+
+ A collection of tags grouped under a common prefix (the name component + before :). For example, in an image tagged with the name + my-app:3.1.4, my-app is the Repository + component of the name. Acceptable values for repository name are + implementation specific, but they SHOULD be limited to the set of + alphanumeric characters [a-zA-z0-9], and punctuation + characters [._-], however it MAY contain additional + / and : characters for organizational + purposes, with the last : character being interpreted + dividing the repository component of the name from the tag suffix + component. +
+
+ +## Image JSON Description + +Here is an example image JSON file: + +``` +{ + "id": "a9561eb1b190625c9adb5a9513e72c4dedafc1cb2d4c5236c9a6957ec7dfd5a9", + "parent": "c6e3cedcda2e3982a1a6760e178355e8e65f7b80e4e5248743fa3549d284e024", + "checksum": "tarsum.v1+sha256:e58fcf7418d2390dec8e8fb69d88c06ec07039d651fedc3aa72af9972e7d046b", + "created": "2014-10-13T21:19:18.674353812Z", + "author": "Alyssa P. Hacker <alyspdev@example.com>", + "architecture": "amd64", + "os": "linux", + "Size": 271828, + "config": { + "User": "alice", + "Memory": 2048, + "MemorySwap": 4096, + "CpuShares": 8, + "ExposedPorts": { + "8080/tcp": {} + }, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "FOO=docker_is_a_really", + "BAR=great_tool_you_know" + ], + "Entrypoint": [ + "/bin/my-app-binary" + ], + "Cmd": [ + "--foreground", + "--config", + "/etc/my-app.d/default.cfg" + ], + "Volumes": { + "/var/job-result-data": {}, + "/var/log/my-app-logs": {}, + }, + "WorkingDir": "/home/alice", + } +} +``` + +### Image JSON Field Descriptions + +
+
+ id string +
+
+ Randomly generated, 256-bit, hexadecimal encoded. Uniquely identifies + the image. +
+
+ parent string +
+
+ ID of the parent image. If there is no parent image then this field + should be omitted. A collection of images may share many of the same + ancestor layers. This organizational structure is strictly a tree with + any one layer having either no parent or a single parent and zero or + more descendant layers. Cycles are not allowed and implementations + should be careful to avoid creating them or iterating through a cycle + indefinitely. +
+
+ created string +
+
+ ISO-8601 formatted combined date and time at which the image was + created. +
+
+ author string +
+
+ Gives the name and/or email address of the person or entity which + created and is responsible for maintaining the image. +
+
+ architecture string +
+
+ The CPU architecture which the binaries in this image are built to run + on. Possible values include: +
    +
  • 386
  • +
  • amd64
  • +
  • arm
  • +
+ More values may be supported in the future and any of these may or may + not be supported by a given container runtime implementation. +
+
+ os string +
+
+ The name of the operating system which the image is built to run on. + Possible values include: +
    +
  • darwin
  • +
  • freebsd
  • +
  • linux
  • +
+ More values may be supported in the future and any of these may or may + not be supported by a given container runtime implementation. +
+
+ checksum string +
+
+ Image Checksum of the filesystem changeset associated with the image + layer. +
+
+ Size integer +
+
+ The size in bytes of the filesystem changeset associated with the image + layer. +
+
+ config struct +
+
+ The execution parameters which should be used as a base when running a + container using the image. This field can be null, in + which case any execution parameters should be specified at creation of + the container. + +

Container RunConfig Field Descriptions

+ +
+
+ User string +
+
+

The username or UID which the process in the container should + run as. This acts as a default value to use when the value is + not specified when creating a container.

+ +

All of the following are valid:

+ +
    +
  • user
  • +
  • uid
  • +
  • user:group
  • +
  • uid:gid
  • +
  • uid:group
  • +
  • user:gid
  • +
+ +

If group/gid is not specified, the + default group and supplementary groups of the given + user/uid in /etc/passwd + from the container are applied.

+
+
+ Memory integer +
+
+ Memory limit (in bytes). This acts as a default value to use + when the value is not specified when creating a container. +
+
+ MemorySwap integer +
+
+ Total memory usage (memory + swap); set to -1 to + disable swap. This acts as a default value to use when the + value is not specified when creating a container. +
+
+ CpuShares integer +
+
+ CPU shares (relative weight vs. other containers). This acts as + a default value to use when the value is not specified when + creating a container. +
+
+ ExposedPorts struct +
+
+ A set of ports to expose from a container running this image. + This JSON structure value is unusual because it is a direct + JSON serialization of the Go type + map[string]struct{} and is represented in JSON as + an object mapping its keys to an empty object. Here is an + example: + +
{
+    "8080": {},
+    "53/udp": {},
+    "2356/tcp": {}
+}
+ + Its keys can be in the format of: +
    +
  • + "port/tcp" +
  • +
  • + "port/udp" +
  • +
  • + "port" +
  • +
+ with the default protocol being "tcp" if not + specified. + + These values act as defaults and are merged with any specified + when creating a container. +
+
+ Env array of strings +
+
+ Entries are in the format of VARNAME="var value". + These values act as defaults and are merged with any specified + when creating a container. +
+
+ Entrypoint array of strings +
+
+ A list of arguments to use as the command to execute when the + container starts. This value acts as a default and is replaced + by an entrypoint specified when creating a container. +
+
+ Cmd array of strings +
+
+ Default arguments to the entry point of the container. These + values act as defaults and are replaced with any specified when + creating a container. If an Entrypoint value is + not specified, then the first entry of the Cmd + array should be interpreted as the executable to run. +
+
+ Volumes struct +
+
+ A set of directories which should be created as data volumes in + a container running this image. This JSON structure value is + unusual because it is a direct JSON serialization of the Go + type map[string]struct{} and is represented in + JSON as an object mapping its keys to an empty object. Here is + an example: +
{
+    "/var/my-app-data/": {},
+    "/etc/some-config.d/": {},
+}
+
+
+ WorkingDir string +
+
+ Sets the current working directory of the entry point process + in the container. This value acts as a default and is replaced + by a working directory specified when creating a container. +
+
+
+
+ +Any extra fields in the Image JSON struct are considered implementation +specific and should be ignored by any implementations which are unable to +interpret them. + +## Creating an Image Filesystem Changeset + +An example of creating an Image Filesystem Changeset follows. + +An image root filesystem is first created as an empty directory named with the +ID of the image being created. Here is the initial empty directory structure +for the changeset for an image with ID `c3167915dc9d` ([real IDs are much +longer](#id_desc), but this example use a truncated one here for brevity. +Implementations need not name the rootfs directory in this way but it may be +convenient for keeping record of a large number of image layers.): + +``` +c3167915dc9d/ +``` + +Files and directories are then created: + +``` +c3167915dc9d/ + etc/ + my-app-config + bin/ + my-app-binary + my-app-tools +``` + +The `c3167915dc9d` directory is then committed as a plain Tar archive with +entries for the following files: + +``` +etc/my-app-config +bin/my-app-binary +bin/my-app-tools +``` + +The TarSum checksum for the archive file is then computed and placed in the +JSON metadata along with the execution parameters. + +To make changes to the filesystem of this container image, create a new +directory named with a new ID, such as `f60c56784b83`, and initialize it with +a snapshot of the parent image's root filesystem, so that the directory is +identical to that of `c3167915dc9d`. NOTE: a copy-on-write or union filesystem +can make this very efficient: + +``` +f60c56784b83/ + etc/ + my-app-config + bin/ + my-app-binary + my-app-tools +``` + +This example change is going add a configuration directory at `/etc/my-app.d` +which contains a default config file. There's also a change to the +`my-app-tools` binary to handle the config layout change. The `f60c56784b83` +directory then looks like this: + +``` +f60c56784b83/ + etc/ + my-app.d/ + default.cfg + bin/ + my-app-binary + my-app-tools +``` + +This reflects the removal of `/etc/my-app-config` and creation of a file and +directory at `/etc/my-app.d/default.cfg`. `/bin/my-app-tools` has also been +replaced with an updated version. Before committing this directory to a +changeset, because it has a parent image, it is first compared with the +directory tree of the parent snapshot, `f60c56784b83`, looking for files and +directories that have been added, modified, or removed. The following changeset +is found: + +``` +Added: /etc/my-app.d/default.cfg +Modified: /bin/my-app-tools +Deleted: /etc/my-app-config +``` + +A Tar Archive is then created which contains *only* this changeset: The added +and modified files and directories in their entirety, and for each deleted item +an entry for an empty file at the same location but with the basename of the +deleted file or directory prefixed with `.wh.`. The filenames prefixed with +`.wh.` are known as "whiteout" files. NOTE: For this reason, it is not possible +to create an image root filesystem which contains a file or directory with a +name beginning with `.wh.`. The resulting Tar archive for `f60c56784b83` has +the following entries: + +``` +/etc/my-app.d/default.cfg +/bin/my-app-tools +/etc/.wh.my-app-config +``` + +Any given image is likely to be composed of several of these Image Filesystem +Changeset tar archives. + +## Combined Image JSON + Filesystem Changeset Format + +There is also a format for a single archive which contains complete information +about an image, including: + + - repository names/tags + - all image layer JSON files + - all tar archives of each layer filesystem changesets + +For example, here's what the full archive of `library/busybox` is (displayed in +`tree` format): + +``` +. +├── 5785b62b697b99a5af6cd5d0aabc804d5748abbb6d3d07da5d1d3795f2dcc83e +│   ├── VERSION +│   ├── json +│   └── layer.tar +├── a7b8b41220991bfc754d7ad445ad27b7f272ab8b4a2c175b9512b97471d02a8a +│   ├── VERSION +│   ├── json +│   └── layer.tar +├── a936027c5ca8bf8f517923169a233e391cbb38469a75de8383b5228dc2d26ceb +│   ├── VERSION +│   ├── json +│   └── layer.tar +├── f60c56784b832dd990022afc120b8136ab3da9528094752ae13fe63a2d28dc8c +│   ├── VERSION +│   ├── json +│   └── layer.tar +└── repositories +``` + +There are one or more directories named with the ID for each layer in a full +image. Each of these directories contains 3 files: + + * `VERSION` - The schema version of the `json` file + * `json` - The JSON metadata for an image layer + * `layer.tar` - The Tar archive of the filesystem changeset for an image + layer. + +The content of the `VERSION` files is simply the semantic version of the JSON +metadata schema: + +``` +1.0 +``` + +And the `repositories` file is another JSON file which describes names/tags: + +``` +{ + "busybox":{ + "latest":"5785b62b697b99a5af6cd5d0aabc804d5748abbb6d3d07da5d1d3795f2dcc83e" + } +} +``` + +Every key in this object is the name of a repository, and maps to a collection +of tag suffixes. Each tag maps to the ID of the image represented by that tag. + +## Loading an Image Filesystem Changeset + +Unpacking a bundle of image layer JSON files and their corresponding filesystem +changesets can be done using a series of steps: + +1. Follow the parent IDs of image layers to find the root ancestor (an image +with no parent ID specified). +2. For every image layer, in order from root ancestor and descending down, +extract the contents of that layer's filesystem changeset archive into a +directory which will be used as the root of a container filesystem. + + - Extract all contents of each archive. + - Walk the directory tree once more, removing any files with the prefix + `.wh.` and the corresponding file or directory named without this prefix. + + +## Implementations + +This specification is an admittedly imperfect description of an +imperfectly-understood problem. The Docker project is, in turn, an attempt to +implement this specification. Our goal and our execution toward it will evolve +over time, but our primary concern in this specification and in our +implementation is compatibility and interoperability. diff --git a/image/store.go b/image/store.go new file mode 100644 index 00000000..92ac438d --- /dev/null +++ b/image/store.go @@ -0,0 +1,295 @@ +package image + +import ( + "encoding/json" + "errors" + "fmt" + "sync" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/digest" + "github.com/docker/docker/layer" +) + +// Store is an interface for creating and accessing images +type Store interface { + Create(config []byte) (ID, error) + Get(id ID) (*Image, error) + Delete(id ID) ([]layer.Metadata, error) + Search(partialID string) (ID, error) + SetParent(id ID, parent ID) error + GetParent(id ID) (ID, error) + Children(id ID) []ID + Map() map[ID]*Image + Heads() map[ID]*Image +} + +// LayerGetReleaser is a minimal interface for getting and releasing images. +type LayerGetReleaser interface { + Get(layer.ChainID) (layer.Layer, error) + Release(layer.Layer) ([]layer.Metadata, error) +} + +type imageMeta struct { + layer layer.Layer + children map[ID]struct{} +} + +type store struct { + sync.Mutex + ls LayerGetReleaser + images map[ID]*imageMeta + fs StoreBackend + digestSet *digest.Set +} + +// NewImageStore returns new store object for given layer store +func NewImageStore(fs StoreBackend, ls LayerGetReleaser) (Store, error) { + is := &store{ + ls: ls, + images: make(map[ID]*imageMeta), + fs: fs, + digestSet: digest.NewSet(), + } + + // load all current images and retain layers + if err := is.restore(); err != nil { + return nil, err + } + + return is, nil +} + +func (is *store) restore() error { + err := is.fs.Walk(func(id ID) error { + img, err := is.Get(id) + if err != nil { + logrus.Errorf("invalid image %v, %v", id, err) + return nil + } + var l layer.Layer + if chainID := img.RootFS.ChainID(); chainID != "" { + l, err = is.ls.Get(chainID) + if err != nil { + return err + } + } + if err := is.digestSet.Add(digest.Digest(id)); err != nil { + return err + } + + imageMeta := &imageMeta{ + layer: l, + children: make(map[ID]struct{}), + } + + is.images[ID(id)] = imageMeta + + return nil + }) + if err != nil { + return err + } + + // Second pass to fill in children maps + for id := range is.images { + if parent, err := is.GetParent(id); err == nil { + if parentMeta := is.images[parent]; parentMeta != nil { + parentMeta.children[id] = struct{}{} + } + } + } + + return nil +} + +func (is *store) Create(config []byte) (ID, error) { + var img Image + err := json.Unmarshal(config, &img) + if err != nil { + return "", err + } + + // Must reject any config that references diffIDs from the history + // which aren't among the rootfs layers. + rootFSLayers := make(map[layer.DiffID]struct{}) + for _, diffID := range img.RootFS.DiffIDs { + rootFSLayers[diffID] = struct{}{} + } + + layerCounter := 0 + for _, h := range img.History { + if !h.EmptyLayer { + layerCounter++ + } + } + if layerCounter > len(img.RootFS.DiffIDs) { + return "", errors.New("too many non-empty layers in History section") + } + + dgst, err := is.fs.Set(config) + if err != nil { + return "", err + } + imageID := ID(dgst) + + is.Lock() + defer is.Unlock() + + if _, exists := is.images[imageID]; exists { + return imageID, nil + } + + layerID := img.RootFS.ChainID() + + var l layer.Layer + if layerID != "" { + l, err = is.ls.Get(layerID) + if err != nil { + return "", err + } + } + + imageMeta := &imageMeta{ + layer: l, + children: make(map[ID]struct{}), + } + + is.images[imageID] = imageMeta + if err := is.digestSet.Add(digest.Digest(imageID)); err != nil { + delete(is.images, imageID) + return "", err + } + + return imageID, nil +} + +func (is *store) Search(term string) (ID, error) { + is.Lock() + defer is.Unlock() + + dgst, err := is.digestSet.Lookup(term) + if err != nil { + if err == digest.ErrDigestNotFound { + err = fmt.Errorf("No such image: %s", term) + } + return "", err + } + return ID(dgst), nil +} + +func (is *store) Get(id ID) (*Image, error) { + // todo: Check if image is in images + // todo: Detect manual insertions and start using them + config, err := is.fs.Get(id) + if err != nil { + return nil, err + } + + img, err := NewFromJSON(config) + if err != nil { + return nil, err + } + img.computedID = id + + img.Parent, err = is.GetParent(id) + if err != nil { + img.Parent = "" + } + + return img, nil +} + +func (is *store) Delete(id ID) ([]layer.Metadata, error) { + is.Lock() + defer is.Unlock() + + imageMeta := is.images[id] + if imageMeta == nil { + return nil, fmt.Errorf("unrecognized image ID %s", id.String()) + } + for id := range imageMeta.children { + is.fs.DeleteMetadata(id, "parent") + } + if parent, err := is.GetParent(id); err == nil && is.images[parent] != nil { + delete(is.images[parent].children, id) + } + + if err := is.digestSet.Remove(digest.Digest(id)); err != nil { + logrus.Errorf("error removing %s from digest set: %q", id, err) + } + delete(is.images, id) + is.fs.Delete(id) + + if imageMeta.layer != nil { + return is.ls.Release(imageMeta.layer) + } + return nil, nil +} + +func (is *store) SetParent(id, parent ID) error { + is.Lock() + defer is.Unlock() + parentMeta := is.images[parent] + if parentMeta == nil { + return fmt.Errorf("unknown parent image ID %s", parent.String()) + } + if parent, err := is.GetParent(id); err == nil && is.images[parent] != nil { + delete(is.images[parent].children, id) + } + parentMeta.children[id] = struct{}{} + return is.fs.SetMetadata(id, "parent", []byte(parent)) +} + +func (is *store) GetParent(id ID) (ID, error) { + d, err := is.fs.GetMetadata(id, "parent") + if err != nil { + return "", err + } + return ID(d), nil // todo: validate? +} + +func (is *store) Children(id ID) []ID { + is.Lock() + defer is.Unlock() + + return is.children(id) +} + +func (is *store) children(id ID) []ID { + var ids []ID + if is.images[id] != nil { + for id := range is.images[id].children { + ids = append(ids, id) + } + } + return ids +} + +func (is *store) Heads() map[ID]*Image { + return is.imagesMap(false) +} + +func (is *store) Map() map[ID]*Image { + return is.imagesMap(true) +} + +func (is *store) imagesMap(all bool) map[ID]*Image { + is.Lock() + defer is.Unlock() + + images := make(map[ID]*Image) + + for id := range is.images { + if !all && len(is.children(id)) > 0 { + continue + } + img, err := is.Get(id) + if err != nil { + logrus.Errorf("invalid image access: %q, error: %q", id, err) + continue + } + images[id] = img + } + return images +} diff --git a/image/store_test.go b/image/store_test.go new file mode 100644 index 00000000..50f8aa8b --- /dev/null +++ b/image/store_test.go @@ -0,0 +1,300 @@ +package image + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/docker/distribution/digest" + "github.com/docker/docker/layer" +) + +func TestRestore(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "images-fs-store") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + fs, err := NewFSStoreBackend(tmpdir) + if err != nil { + t.Fatal(err) + } + + id1, err := fs.Set([]byte(`{"comment": "abc", "rootfs": {"type": "layers"}}`)) + if err != nil { + t.Fatal(err) + } + _, err = fs.Set([]byte(`invalid`)) + if err != nil { + t.Fatal(err) + } + id2, err := fs.Set([]byte(`{"comment": "def", "rootfs": {"type": "layers", "diff_ids": ["2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"]}}`)) + if err != nil { + t.Fatal(err) + } + err = fs.SetMetadata(id2, "parent", []byte(id1)) + if err != nil { + t.Fatal(err) + } + + is, err := NewImageStore(fs, &mockLayerGetReleaser{}) + if err != nil { + t.Fatal(err) + } + + imgs := is.Map() + if actual, expected := len(imgs), 2; actual != expected { + t.Fatalf("invalid images length, expected 2, got %q", len(imgs)) + } + + img1, err := is.Get(ID(id1)) + if err != nil { + t.Fatal(err) + } + + if actual, expected := img1.computedID, ID(id1); actual != expected { + t.Fatalf("invalid image ID: expected %q, got %q", expected, actual) + } + + if actual, expected := img1.computedID.String(), string(id1); actual != expected { + t.Fatalf("invalid image ID string: expected %q, got %q", expected, actual) + } + + img2, err := is.Get(ID(id2)) + if err != nil { + t.Fatal(err) + } + + if actual, expected := img1.Comment, "abc"; actual != expected { + t.Fatalf("invalid comment for image1: expected %q, got %q", expected, actual) + } + + if actual, expected := img2.Comment, "def"; actual != expected { + t.Fatalf("invalid comment for image2: expected %q, got %q", expected, actual) + } + + p, err := is.GetParent(ID(id1)) + if err == nil { + t.Fatal("expected error for getting parent") + } + + p, err = is.GetParent(ID(id2)) + if err != nil { + t.Fatal(err) + } + if actual, expected := p, ID(id1); actual != expected { + t.Fatalf("invalid parent: expected %q, got %q", expected, actual) + } + + children := is.Children(ID(id1)) + if len(children) != 1 { + t.Fatalf("invalid children length: %q", len(children)) + } + if actual, expected := children[0], ID(id2); actual != expected { + t.Fatalf("invalid child for id1: expected %q, got %q", expected, actual) + } + + heads := is.Heads() + if actual, expected := len(heads), 1; actual != expected { + t.Fatalf("invalid images length: expected %q, got %q", expected, actual) + } + + sid1, err := is.Search(string(id1)[:10]) + if err != nil { + t.Fatal(err) + } + if actual, expected := sid1, ID(id1); actual != expected { + t.Fatalf("searched ID mismatch: expected %q, got %q", expected, actual) + } + + sid1, err = is.Search(digest.Digest(id1).Hex()[:6]) + if err != nil { + t.Fatal(err) + } + if actual, expected := sid1, ID(id1); actual != expected { + t.Fatalf("searched ID mismatch: expected %q, got %q", expected, actual) + } + + invalidPattern := digest.Digest(id1).Hex()[1:6] + _, err = is.Search(invalidPattern) + if err == nil { + t.Fatalf("expected search for %q to fail", invalidPattern) + } + +} + +func TestAddDelete(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "images-fs-store") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + fs, err := NewFSStoreBackend(tmpdir) + if err != nil { + t.Fatal(err) + } + + is, err := NewImageStore(fs, &mockLayerGetReleaser{}) + if err != nil { + t.Fatal(err) + } + + id1, err := is.Create([]byte(`{"comment": "abc", "rootfs": {"type": "layers", "diff_ids": ["2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"]}}`)) + if err != nil { + t.Fatal(err) + } + + if actual, expected := id1, ID("sha256:8d25a9c45df515f9d0fe8e4a6b1c64dd3b965a84790ddbcc7954bb9bc89eb993"); actual != expected { + t.Fatalf("create ID mismatch: expected %q, got %q", expected, actual) + } + + img, err := is.Get(id1) + if err != nil { + t.Fatal(err) + } + + if actual, expected := img.Comment, "abc"; actual != expected { + t.Fatalf("invalid comment in image: expected %q, got %q", expected, actual) + } + + id2, err := is.Create([]byte(`{"comment": "def", "rootfs": {"type": "layers", "diff_ids": ["2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"]}}`)) + if err != nil { + t.Fatal(err) + } + + err = is.SetParent(id2, id1) + if err != nil { + t.Fatal(err) + } + + pid1, err := is.GetParent(id2) + if err != nil { + t.Fatal(err) + } + if actual, expected := pid1, id1; actual != expected { + t.Fatalf("invalid parent for image: expected %q, got %q", expected, actual) + } + + _, err = is.Delete(id1) + if err != nil { + t.Fatal(err) + } + _, err = is.Get(id1) + if err == nil { + t.Fatalf("expected get for deleted image %q to fail", id1) + } + _, err = is.Get(id2) + if err != nil { + t.Fatal(err) + } + pid1, err = is.GetParent(id2) + if err == nil { + t.Fatalf("expected parent check for image %q to fail, got %q", id2, pid1) + } + +} + +func TestSearchAfterDelete(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "images-fs-store") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + fs, err := NewFSStoreBackend(tmpdir) + if err != nil { + t.Fatal(err) + } + + is, err := NewImageStore(fs, &mockLayerGetReleaser{}) + if err != nil { + t.Fatal(err) + } + + id, err := is.Create([]byte(`{"comment": "abc", "rootfs": {"type": "layers"}}`)) + if err != nil { + t.Fatal(err) + } + + id1, err := is.Search(string(id)[:15]) + if err != nil { + t.Fatal(err) + } + + if actual, expected := id1, id; expected != actual { + t.Fatalf("wrong id returned from search: expected %q, got %q", expected, actual) + } + + if _, err := is.Delete(id); err != nil { + t.Fatal(err) + } + + if _, err := is.Search(string(id)[:15]); err == nil { + t.Fatal("expected search after deletion to fail") + } +} + +func TestParentReset(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "images-fs-store") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + fs, err := NewFSStoreBackend(tmpdir) + if err != nil { + t.Fatal(err) + } + + is, err := NewImageStore(fs, &mockLayerGetReleaser{}) + if err != nil { + t.Fatal(err) + } + + id, err := is.Create([]byte(`{"comment": "abc1", "rootfs": {"type": "layers"}}`)) + if err != nil { + t.Fatal(err) + } + + id2, err := is.Create([]byte(`{"comment": "abc2", "rootfs": {"type": "layers"}}`)) + if err != nil { + t.Fatal(err) + } + + id3, err := is.Create([]byte(`{"comment": "abc3", "rootfs": {"type": "layers"}}`)) + if err != nil { + t.Fatal(err) + } + + if err := is.SetParent(id, id2); err != nil { + t.Fatal(err) + } + + ids := is.Children(id2) + if actual, expected := len(ids), 1; expected != actual { + t.Fatalf("wrong number of children: %d, got %d", expected, actual) + } + + if err := is.SetParent(id, id3); err != nil { + t.Fatal(err) + } + + ids = is.Children(id2) + if actual, expected := len(ids), 0; expected != actual { + t.Fatalf("wrong number of children after parent reset: %d, got %d", expected, actual) + } + + ids = is.Children(id3) + if actual, expected := len(ids), 1; expected != actual { + t.Fatalf("wrong number of children after parent reset: %d, got %d", expected, actual) + } + +} + +type mockLayerGetReleaser struct{} + +func (ls *mockLayerGetReleaser) Get(layer.ChainID) (layer.Layer, error) { + return nil, nil +} + +func (ls *mockLayerGetReleaser) Release(layer.Layer) ([]layer.Metadata, error) { + return nil, nil +} diff --git a/image/tarexport/load.go b/image/tarexport/load.go new file mode 100644 index 00000000..42eaa40b --- /dev/null +++ b/image/tarexport/load.go @@ -0,0 +1,372 @@ +package tarexport + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "reflect" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/image" + "github.com/docker/docker/image/v1" + "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/chrootarchive" + "github.com/docker/docker/pkg/progress" + "github.com/docker/docker/pkg/streamformatter" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/symlink" + "github.com/docker/docker/reference" +) + +func (l *tarexporter) Load(inTar io.ReadCloser, outStream io.Writer, quiet bool) error { + var ( + sf = streamformatter.NewJSONStreamFormatter() + progressOutput progress.Output + ) + if !quiet { + progressOutput = sf.NewProgressOutput(outStream, false) + outStream = &streamformatter.StdoutFormatter{Writer: outStream, StreamFormatter: streamformatter.NewJSONStreamFormatter()} + } + + tmpDir, err := ioutil.TempDir("", "docker-import-") + if err != nil { + return err + } + defer os.RemoveAll(tmpDir) + + if err := chrootarchive.Untar(inTar, tmpDir, nil); err != nil { + return err + } + // read manifest, if no file then load in legacy mode + manifestPath, err := safePath(tmpDir, manifestFileName) + if err != nil { + return err + } + manifestFile, err := os.Open(manifestPath) + if err != nil { + if os.IsNotExist(err) { + return l.legacyLoad(tmpDir, outStream, progressOutput) + } + return manifestFile.Close() + } + defer manifestFile.Close() + + var manifest []manifestItem + if err := json.NewDecoder(manifestFile).Decode(&manifest); err != nil { + return err + } + + var parentLinks []parentLink + + for _, m := range manifest { + configPath, err := safePath(tmpDir, m.Config) + if err != nil { + return err + } + config, err := ioutil.ReadFile(configPath) + if err != nil { + return err + } + img, err := image.NewFromJSON(config) + if err != nil { + return err + } + var rootFS image.RootFS + rootFS = *img.RootFS + rootFS.DiffIDs = nil + + if expected, actual := len(m.Layers), len(img.RootFS.DiffIDs); expected != actual { + return fmt.Errorf("invalid manifest, layers length mismatch: expected %q, got %q", expected, actual) + } + + for i, diffID := range img.RootFS.DiffIDs { + layerPath, err := safePath(tmpDir, m.Layers[i]) + if err != nil { + return err + } + r := rootFS + r.Append(diffID) + newLayer, err := l.ls.Get(r.ChainID()) + if err != nil { + newLayer, err = l.loadLayer(layerPath, rootFS, diffID.String(), progressOutput) + if err != nil { + return err + } + } + defer layer.ReleaseAndLog(l.ls, newLayer) + if expected, actual := diffID, newLayer.DiffID(); expected != actual { + return fmt.Errorf("invalid diffID for layer %d: expected %q, got %q", i, expected, actual) + } + rootFS.Append(diffID) + } + + imgID, err := l.is.Create(config) + if err != nil { + return err + } + + for _, repoTag := range m.RepoTags { + named, err := reference.ParseNamed(repoTag) + if err != nil { + return err + } + ref, ok := named.(reference.NamedTagged) + if !ok { + return fmt.Errorf("invalid tag %q", repoTag) + } + l.setLoadedTag(ref, imgID, outStream) + } + + parentLinks = append(parentLinks, parentLink{imgID, m.Parent}) + } + + for _, p := range validatedParentLinks(parentLinks) { + if p.parentID != "" { + if err := l.setParentID(p.id, p.parentID); err != nil { + return err + } + } + } + + return nil +} + +func (l *tarexporter) setParentID(id, parentID image.ID) error { + img, err := l.is.Get(id) + if err != nil { + return err + } + parent, err := l.is.Get(parentID) + if err != nil { + return err + } + if !checkValidParent(img, parent) { + return fmt.Errorf("image %v is not a valid parent for %v", parent.ID, img.ID) + } + return l.is.SetParent(id, parentID) +} + +func (l *tarexporter) loadLayer(filename string, rootFS image.RootFS, id string, progressOutput progress.Output) (layer.Layer, error) { + rawTar, err := os.Open(filename) + if err != nil { + logrus.Debugf("Error reading embedded tar: %v", err) + return nil, err + } + defer rawTar.Close() + + inflatedLayerData, err := archive.DecompressStream(rawTar) + if err != nil { + return nil, err + } + defer inflatedLayerData.Close() + + if progressOutput != nil { + fileInfo, err := os.Stat(filename) + if err != nil { + logrus.Debugf("Error statting file: %v", err) + return nil, err + } + + progressReader := progress.NewProgressReader(inflatedLayerData, progressOutput, fileInfo.Size(), stringid.TruncateID(id), "Loading layer") + + return l.ls.Register(progressReader, rootFS.ChainID()) + } + return l.ls.Register(inflatedLayerData, rootFS.ChainID()) +} + +func (l *tarexporter) setLoadedTag(ref reference.NamedTagged, imgID image.ID, outStream io.Writer) error { + if prevID, err := l.rs.Get(ref); err == nil && prevID != imgID { + fmt.Fprintf(outStream, "The image %s already exists, renaming the old one with ID %s to empty string\n", ref.String(), string(prevID)) // todo: this message is wrong in case of multiple tags + } + + if err := l.rs.AddTag(ref, imgID, true); err != nil { + return err + } + return nil +} + +func (l *tarexporter) legacyLoad(tmpDir string, outStream io.Writer, progressOutput progress.Output) error { + legacyLoadedMap := make(map[string]image.ID) + + dirs, err := ioutil.ReadDir(tmpDir) + if err != nil { + return err + } + + // every dir represents an image + for _, d := range dirs { + if d.IsDir() { + if err := l.legacyLoadImage(d.Name(), tmpDir, legacyLoadedMap, progressOutput); err != nil { + return err + } + } + } + + // load tags from repositories file + repositoriesPath, err := safePath(tmpDir, legacyRepositoriesFileName) + if err != nil { + return err + } + repositoriesFile, err := os.Open(repositoriesPath) + if err != nil { + if !os.IsNotExist(err) { + return err + } + return repositoriesFile.Close() + } + defer repositoriesFile.Close() + + repositories := make(map[string]map[string]string) + if err := json.NewDecoder(repositoriesFile).Decode(&repositories); err != nil { + return err + } + + for name, tagMap := range repositories { + for tag, oldID := range tagMap { + imgID, ok := legacyLoadedMap[oldID] + if !ok { + return fmt.Errorf("invalid target ID: %v", oldID) + } + named, err := reference.WithName(name) + if err != nil { + return err + } + ref, err := reference.WithTag(named, tag) + if err != nil { + return err + } + l.setLoadedTag(ref, imgID, outStream) + } + } + + return nil +} + +func (l *tarexporter) legacyLoadImage(oldID, sourceDir string, loadedMap map[string]image.ID, progressOutput progress.Output) error { + if _, loaded := loadedMap[oldID]; loaded { + return nil + } + configPath, err := safePath(sourceDir, filepath.Join(oldID, legacyConfigFileName)) + if err != nil { + return err + } + imageJSON, err := ioutil.ReadFile(configPath) + if err != nil { + logrus.Debugf("Error reading json: %v", err) + return err + } + + var img struct{ Parent string } + if err := json.Unmarshal(imageJSON, &img); err != nil { + return err + } + + var parentID image.ID + if img.Parent != "" { + for { + var loaded bool + if parentID, loaded = loadedMap[img.Parent]; !loaded { + if err := l.legacyLoadImage(img.Parent, sourceDir, loadedMap, progressOutput); err != nil { + return err + } + } else { + break + } + } + } + + // todo: try to connect with migrate code + rootFS := image.NewRootFS() + var history []image.History + + if parentID != "" { + parentImg, err := l.is.Get(parentID) + if err != nil { + return err + } + + rootFS = parentImg.RootFS + history = parentImg.History + } + + layerPath, err := safePath(sourceDir, filepath.Join(oldID, legacyLayerFileName)) + if err != nil { + return err + } + newLayer, err := l.loadLayer(layerPath, *rootFS, oldID, progressOutput) + if err != nil { + return err + } + rootFS.Append(newLayer.DiffID()) + + h, err := v1.HistoryFromConfig(imageJSON, false) + if err != nil { + return err + } + history = append(history, h) + + config, err := v1.MakeConfigFromV1Config(imageJSON, rootFS, history) + if err != nil { + return err + } + imgID, err := l.is.Create(config) + if err != nil { + return err + } + + metadata, err := l.ls.Release(newLayer) + layer.LogReleaseMetadata(metadata) + if err != nil { + return err + } + + if parentID != "" { + if err := l.is.SetParent(imgID, parentID); err != nil { + return err + } + } + + loadedMap[oldID] = imgID + return nil +} + +func safePath(base, path string) (string, error) { + return symlink.FollowSymlinkInScope(filepath.Join(base, path), base) +} + +type parentLink struct { + id, parentID image.ID +} + +func validatedParentLinks(pl []parentLink) (ret []parentLink) { +mainloop: + for i, p := range pl { + ret = append(ret, p) + for _, p2 := range pl { + if p2.id == p.parentID && p2.id != p.id { + continue mainloop + } + } + ret[i].parentID = "" + } + return +} + +func checkValidParent(img, parent *image.Image) bool { + if len(img.History) == 0 && len(parent.History) == 0 { + return true // having history is not mandatory + } + if len(img.History)-len(parent.History) != 1 { + return false + } + for i, h := range parent.History { + if !reflect.DeepEqual(h, img.History[i]) { + return false + } + } + return true +} diff --git a/image/tarexport/save.go b/image/tarexport/save.go new file mode 100644 index 00000000..9ec3cc9f --- /dev/null +++ b/image/tarexport/save.go @@ -0,0 +1,319 @@ +package tarexport + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "time" + + "github.com/docker/distribution/digest" + "github.com/docker/docker/image" + "github.com/docker/docker/image/v1" + "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/system" + "github.com/docker/docker/reference" +) + +type imageDescriptor struct { + refs []reference.NamedTagged + layers []string +} + +type saveSession struct { + *tarexporter + outDir string + images map[image.ID]*imageDescriptor + savedLayers map[string]struct{} +} + +func (l *tarexporter) Save(names []string, outStream io.Writer) error { + images, err := l.parseNames(names) + if err != nil { + return err + } + + return (&saveSession{tarexporter: l, images: images}).save(outStream) +} + +func (l *tarexporter) parseNames(names []string) (map[image.ID]*imageDescriptor, error) { + imgDescr := make(map[image.ID]*imageDescriptor) + + addAssoc := func(id image.ID, ref reference.Named) { + if _, ok := imgDescr[id]; !ok { + imgDescr[id] = &imageDescriptor{} + } + + if ref != nil { + var tagged reference.NamedTagged + if _, ok := ref.(reference.Canonical); ok { + return + } + var ok bool + if tagged, ok = ref.(reference.NamedTagged); !ok { + var err error + if tagged, err = reference.WithTag(ref, reference.DefaultTag); err != nil { + return + } + } + + for _, t := range imgDescr[id].refs { + if tagged.String() == t.String() { + return + } + } + imgDescr[id].refs = append(imgDescr[id].refs, tagged) + } + } + + for _, name := range names { + id, ref, err := reference.ParseIDOrReference(name) + if err != nil { + return nil, err + } + if id != "" { + _, err := l.is.Get(image.ID(id)) + if err != nil { + return nil, err + } + addAssoc(image.ID(id), nil) + continue + } + if ref.Name() == string(digest.Canonical) { + imgID, err := l.is.Search(name) + if err != nil { + return nil, err + } + addAssoc(imgID, nil) + continue + } + if reference.IsNameOnly(ref) { + assocs := l.rs.ReferencesByName(ref) + for _, assoc := range assocs { + addAssoc(assoc.ImageID, assoc.Ref) + } + if len(assocs) == 0 { + imgID, err := l.is.Search(name) + if err != nil { + return nil, err + } + addAssoc(imgID, nil) + } + continue + } + var imgID image.ID + if imgID, err = l.rs.Get(ref); err != nil { + return nil, err + } + addAssoc(imgID, ref) + + } + return imgDescr, nil +} + +func (s *saveSession) save(outStream io.Writer) error { + s.savedLayers = make(map[string]struct{}) + + // get image json + tempDir, err := ioutil.TempDir("", "docker-export-") + if err != nil { + return err + } + defer os.RemoveAll(tempDir) + + s.outDir = tempDir + reposLegacy := make(map[string]map[string]string) + + var manifest []manifestItem + var parentLinks []parentLink + + for id, imageDescr := range s.images { + if err = s.saveImage(id); err != nil { + return err + } + + var repoTags []string + var layers []string + + for _, ref := range imageDescr.refs { + if _, ok := reposLegacy[ref.Name()]; !ok { + reposLegacy[ref.Name()] = make(map[string]string) + } + reposLegacy[ref.Name()][ref.Tag()] = imageDescr.layers[len(imageDescr.layers)-1] + repoTags = append(repoTags, ref.String()) + } + + for _, l := range imageDescr.layers { + layers = append(layers, filepath.Join(l, legacyLayerFileName)) + } + + manifest = append(manifest, manifestItem{ + Config: digest.Digest(id).Hex() + ".json", + RepoTags: repoTags, + Layers: layers, + }) + + parentID, _ := s.is.GetParent(id) + parentLinks = append(parentLinks, parentLink{id, parentID}) + } + + for i, p := range validatedParentLinks(parentLinks) { + if p.parentID != "" { + manifest[i].Parent = p.parentID + } + } + + if len(reposLegacy) > 0 { + reposFile := filepath.Join(tempDir, legacyRepositoriesFileName) + f, err := os.OpenFile(reposFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + f.Close() + return err + } + if err := json.NewEncoder(f).Encode(reposLegacy); err != nil { + return err + } + if err := f.Close(); err != nil { + return err + } + if err := system.Chtimes(reposFile, time.Unix(0, 0), time.Unix(0, 0)); err != nil { + return err + } + } + + manifestFileName := filepath.Join(tempDir, manifestFileName) + f, err := os.OpenFile(manifestFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + f.Close() + return err + } + if err := json.NewEncoder(f).Encode(manifest); err != nil { + return err + } + if err := f.Close(); err != nil { + return err + } + if err := system.Chtimes(manifestFileName, time.Unix(0, 0), time.Unix(0, 0)); err != nil { + return err + } + + fs, err := archive.Tar(tempDir, archive.Uncompressed) + if err != nil { + return err + } + defer fs.Close() + + if _, err := io.Copy(outStream, fs); err != nil { + return err + } + return nil +} + +func (s *saveSession) saveImage(id image.ID) error { + img, err := s.is.Get(id) + if err != nil { + return err + } + + if len(img.RootFS.DiffIDs) == 0 { + return fmt.Errorf("empty export - not implemented") + } + + var parent digest.Digest + var layers []string + for i := range img.RootFS.DiffIDs { + v1Img := image.V1Image{} + if i == len(img.RootFS.DiffIDs)-1 { + v1Img = img.V1Image + } + rootFS := *img.RootFS + rootFS.DiffIDs = rootFS.DiffIDs[:i+1] + v1ID, err := v1.CreateID(v1Img, rootFS.ChainID(), parent) + if err != nil { + return err + } + + v1Img.ID = v1ID.Hex() + if parent != "" { + v1Img.Parent = parent.Hex() + } + + if err := s.saveLayer(rootFS.ChainID(), v1Img, img.Created); err != nil { + return err + } + layers = append(layers, v1Img.ID) + parent = v1ID + } + + configFile := filepath.Join(s.outDir, digest.Digest(id).Hex()+".json") + if err := ioutil.WriteFile(configFile, img.RawJSON(), 0644); err != nil { + return err + } + if err := system.Chtimes(configFile, img.Created, img.Created); err != nil { + return err + } + + s.images[id].layers = layers + return nil +} + +func (s *saveSession) saveLayer(id layer.ChainID, legacyImg image.V1Image, createdTime time.Time) error { + if _, exists := s.savedLayers[legacyImg.ID]; exists { + return nil + } + + outDir := filepath.Join(s.outDir, legacyImg.ID) + if err := os.Mkdir(outDir, 0755); err != nil { + return err + } + + // todo: why is this version file here? + if err := ioutil.WriteFile(filepath.Join(outDir, legacyVersionFileName), []byte("1.0"), 0644); err != nil { + return err + } + + imageConfig, err := json.Marshal(legacyImg) + if err != nil { + return err + } + + if err := ioutil.WriteFile(filepath.Join(outDir, legacyConfigFileName), imageConfig, 0644); err != nil { + return err + } + + // serialize filesystem + tarFile, err := os.Create(filepath.Join(outDir, legacyLayerFileName)) + if err != nil { + return err + } + defer tarFile.Close() + + l, err := s.ls.Get(id) + if err != nil { + return err + } + defer layer.ReleaseAndLog(s.ls, l) + + arch, err := l.TarStream() + if err != nil { + return err + } + defer arch.Close() + + if _, err := io.Copy(tarFile, arch); err != nil { + return err + } + + for _, fname := range []string{"", legacyVersionFileName, legacyConfigFileName, legacyLayerFileName} { + // todo: maybe save layer created timestamp? + if err := system.Chtimes(filepath.Join(outDir, fname), createdTime, createdTime); err != nil { + return err + } + } + + s.savedLayers[legacyImg.ID] = struct{}{} + return nil +} diff --git a/image/tarexport/tarexport.go b/image/tarexport/tarexport.go new file mode 100644 index 00000000..5e208777 --- /dev/null +++ b/image/tarexport/tarexport.go @@ -0,0 +1,37 @@ +package tarexport + +import ( + "github.com/docker/docker/image" + "github.com/docker/docker/layer" + "github.com/docker/docker/reference" +) + +const ( + manifestFileName = "manifest.json" + legacyLayerFileName = "layer.tar" + legacyConfigFileName = "json" + legacyVersionFileName = "VERSION" + legacyRepositoriesFileName = "repositories" +) + +type manifestItem struct { + Config string + RepoTags []string + Layers []string + Parent image.ID `json:",omitempty"` +} + +type tarexporter struct { + is image.Store + ls layer.Store + rs reference.Store +} + +// NewTarExporter returns new ImageExporter for tar packages +func NewTarExporter(is image.Store, ls layer.Store, rs reference.Store) image.Exporter { + return &tarexporter{ + is: is, + ls: ls, + rs: rs, + } +} diff --git a/image/v1/imagev1.go b/image/v1/imagev1.go new file mode 100644 index 00000000..e27ebd4c --- /dev/null +++ b/image/v1/imagev1.go @@ -0,0 +1,148 @@ +package v1 + +import ( + "encoding/json" + "fmt" + "regexp" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/digest" + "github.com/docker/docker/image" + "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/version" +) + +var validHex = regexp.MustCompile(`^([a-f0-9]{64})$`) + +// noFallbackMinVersion is the minimum version for which v1compatibility +// information will not be marshaled through the Image struct to remove +// blank fields. +var noFallbackMinVersion = version.Version("1.8.3") + +// HistoryFromConfig creates a History struct from v1 configuration JSON +func HistoryFromConfig(imageJSON []byte, emptyLayer bool) (image.History, error) { + h := image.History{} + var v1Image image.V1Image + if err := json.Unmarshal(imageJSON, &v1Image); err != nil { + return h, err + } + + return image.History{ + Author: v1Image.Author, + Created: v1Image.Created, + CreatedBy: strings.Join(v1Image.ContainerConfig.Cmd, " "), + Comment: v1Image.Comment, + EmptyLayer: emptyLayer, + }, nil +} + +// CreateID creates an ID from v1 image, layerID and parent ID. +// Used for backwards compatibility with old clients. +func CreateID(v1Image image.V1Image, layerID layer.ChainID, parent digest.Digest) (digest.Digest, error) { + v1Image.ID = "" + v1JSON, err := json.Marshal(v1Image) + if err != nil { + return "", err + } + + var config map[string]*json.RawMessage + if err := json.Unmarshal(v1JSON, &config); err != nil { + return "", err + } + + // FIXME: note that this is slightly incompatible with RootFS logic + config["layer_id"] = rawJSON(layerID) + if parent != "" { + config["parent"] = rawJSON(parent) + } + + configJSON, err := json.Marshal(config) + if err != nil { + return "", err + } + logrus.Debugf("CreateV1ID %s", configJSON) + + return digest.FromBytes(configJSON), nil +} + +// MakeConfigFromV1Config creates an image config from the legacy V1 config format. +func MakeConfigFromV1Config(imageJSON []byte, rootfs *image.RootFS, history []image.History) ([]byte, error) { + var dver struct { + DockerVersion string `json:"docker_version"` + } + + if err := json.Unmarshal(imageJSON, &dver); err != nil { + return nil, err + } + + useFallback := version.Version(dver.DockerVersion).LessThan(noFallbackMinVersion) + + if useFallback { + var v1Image image.V1Image + err := json.Unmarshal(imageJSON, &v1Image) + if err != nil { + return nil, err + } + imageJSON, err = json.Marshal(v1Image) + if err != nil { + return nil, err + } + } + + var c map[string]*json.RawMessage + if err := json.Unmarshal(imageJSON, &c); err != nil { + return nil, err + } + + delete(c, "id") + delete(c, "parent") + delete(c, "Size") // Size is calculated from data on disk and is inconsistent + delete(c, "parent_id") + delete(c, "layer_id") + delete(c, "throwaway") + + c["rootfs"] = rawJSON(rootfs) + c["history"] = rawJSON(history) + + return json.Marshal(c) +} + +// MakeV1ConfigFromConfig creates an legacy V1 image config from an Image struct +func MakeV1ConfigFromConfig(img *image.Image, v1ID, parentV1ID string, throwaway bool) ([]byte, error) { + // Top-level v1compatibility string should be a modified version of the + // image config. + var configAsMap map[string]*json.RawMessage + if err := json.Unmarshal(img.RawJSON(), &configAsMap); err != nil { + return nil, err + } + + // Delete fields that didn't exist in old manifest + delete(configAsMap, "rootfs") + delete(configAsMap, "history") + configAsMap["id"] = rawJSON(v1ID) + if parentV1ID != "" { + configAsMap["parent"] = rawJSON(parentV1ID) + } + if throwaway { + configAsMap["throwaway"] = rawJSON(true) + } + + return json.Marshal(configAsMap) +} + +func rawJSON(value interface{}) *json.RawMessage { + jsonval, err := json.Marshal(value) + if err != nil { + return nil + } + return (*json.RawMessage)(&jsonval) +} + +// ValidateID checks whether an ID string is a valid image ID. +func ValidateID(id string) error { + if ok := validHex.MatchString(id); !ok { + return fmt.Errorf("image ID %q is invalid", id) + } + return nil +} diff --git a/integration-cli/benchmark_test.go b/integration-cli/benchmark_test.go new file mode 100644 index 00000000..647d014d --- /dev/null +++ b/integration-cli/benchmark_test.go @@ -0,0 +1,95 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "runtime" + "strings" + "sync" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) BenchmarkConcurrentContainerActions(c *check.C) { + maxConcurrency := runtime.GOMAXPROCS(0) + numIterations := c.N + outerGroup := &sync.WaitGroup{} + outerGroup.Add(maxConcurrency) + chErr := make(chan error, numIterations*2*maxConcurrency) + + for i := 0; i < maxConcurrency; i++ { + go func() { + defer outerGroup.Done() + innerGroup := &sync.WaitGroup{} + innerGroup.Add(2) + + go func() { + defer innerGroup.Done() + for i := 0; i < numIterations; i++ { + args := []string{"run", "-d", defaultSleepImage} + args = append(args, defaultSleepCommand...) + out, _, err := dockerCmdWithError(args...) + if err != nil { + chErr <- fmt.Errorf(out) + return + } + + id := strings.TrimSpace(out) + tmpDir, err := ioutil.TempDir("", "docker-concurrent-test-"+id) + if err != nil { + chErr <- err + return + } + defer os.RemoveAll(tmpDir) + out, _, err = dockerCmdWithError("cp", id+":/tmp", tmpDir) + if err != nil { + chErr <- fmt.Errorf(out) + return + } + + out, _, err = dockerCmdWithError("kill", id) + if err != nil { + chErr <- fmt.Errorf(out) + } + + out, _, err = dockerCmdWithError("start", id) + if err != nil { + chErr <- fmt.Errorf(out) + } + + out, _, err = dockerCmdWithError("kill", id) + if err != nil { + chErr <- fmt.Errorf(out) + } + + // don't do an rm -f here since it can potentially ignore errors from the graphdriver + out, _, err = dockerCmdWithError("rm", id) + if err != nil { + chErr <- fmt.Errorf(out) + } + } + }() + + go func() { + defer innerGroup.Done() + for i := 0; i < numIterations; i++ { + out, _, err := dockerCmdWithError("ps") + if err != nil { + chErr <- fmt.Errorf(out) + } + } + }() + + innerGroup.Wait() + }() + } + + outerGroup.Wait() + close(chErr) + + for err := range chErr { + c.Assert(err, checker.IsNil) + } +} diff --git a/integration-cli/check_test.go b/integration-cli/check_test.go new file mode 100644 index 00000000..aab8e4a7 --- /dev/null +++ b/integration-cli/check_test.go @@ -0,0 +1,216 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/pkg/reexec" + "github.com/go-check/check" +) + +func Test(t *testing.T) { + reexec.Init() // This is required for external graphdriver tests + + if !isLocalDaemon { + fmt.Println("INFO: Testing against a remote daemon") + } else { + fmt.Println("INFO: Testing against a local daemon") + } + + check.TestingT(t) +} + +func init() { + check.Suite(&DockerSuite{}) +} + +type DockerSuite struct { +} + +func (s *DockerSuite) TearDownTest(c *check.C) { + unpauseAllContainers() + deleteAllContainers() + deleteAllImages() + deleteAllVolumes() + deleteAllNetworks() +} + +func init() { + check.Suite(&DockerRegistrySuite{ + ds: &DockerSuite{}, + }) +} + +type DockerRegistrySuite struct { + ds *DockerSuite + reg *testRegistryV2 + d *Daemon +} + +func (s *DockerRegistrySuite) SetUpTest(c *check.C) { + testRequires(c, DaemonIsLinux, RegistryHosting) + s.reg = setupRegistry(c, false, "", "") + s.d = NewDaemon(c) +} + +func (s *DockerRegistrySuite) TearDownTest(c *check.C) { + if s.reg != nil { + s.reg.Close() + } + if s.d != nil { + s.d.Stop() + } + s.ds.TearDownTest(c) +} + +func init() { + check.Suite(&DockerSchema1RegistrySuite{ + ds: &DockerSuite{}, + }) +} + +type DockerSchema1RegistrySuite struct { + ds *DockerSuite + reg *testRegistryV2 + d *Daemon +} + +func (s *DockerSchema1RegistrySuite) SetUpTest(c *check.C) { + testRequires(c, DaemonIsLinux, RegistryHosting) + s.reg = setupRegistry(c, true, "", "") + s.d = NewDaemon(c) +} + +func (s *DockerSchema1RegistrySuite) TearDownTest(c *check.C) { + if s.reg != nil { + s.reg.Close() + } + if s.d != nil { + s.d.Stop() + } + s.ds.TearDownTest(c) +} + +func init() { + check.Suite(&DockerRegistryAuthHtpasswdSuite{ + ds: &DockerSuite{}, + }) +} + +type DockerRegistryAuthHtpasswdSuite struct { + ds *DockerSuite + reg *testRegistryV2 + d *Daemon +} + +func (s *DockerRegistryAuthHtpasswdSuite) SetUpTest(c *check.C) { + testRequires(c, DaemonIsLinux, RegistryHosting) + s.reg = setupRegistry(c, false, "htpasswd", "") + s.d = NewDaemon(c) +} + +func (s *DockerRegistryAuthHtpasswdSuite) TearDownTest(c *check.C) { + if s.reg != nil { + out, err := s.d.Cmd("logout", privateRegistryURL) + c.Assert(err, check.IsNil, check.Commentf(out)) + s.reg.Close() + } + if s.d != nil { + s.d.Stop() + } + s.ds.TearDownTest(c) +} + +func init() { + check.Suite(&DockerRegistryAuthTokenSuite{ + ds: &DockerSuite{}, + }) +} + +type DockerRegistryAuthTokenSuite struct { + ds *DockerSuite + reg *testRegistryV2 + d *Daemon +} + +func (s *DockerRegistryAuthTokenSuite) SetUpTest(c *check.C) { + testRequires(c, DaemonIsLinux, RegistryHosting) + s.d = NewDaemon(c) +} + +func (s *DockerRegistryAuthTokenSuite) TearDownTest(c *check.C) { + if s.reg != nil { + out, err := s.d.Cmd("logout", privateRegistryURL) + c.Assert(err, check.IsNil, check.Commentf(out)) + s.reg.Close() + } + if s.d != nil { + s.d.Stop() + } + s.ds.TearDownTest(c) +} + +func (s *DockerRegistryAuthTokenSuite) setupRegistryWithTokenService(c *check.C, tokenURL string) { + if s == nil { + c.Fatal("registry suite isn't initialized") + } + s.reg = setupRegistry(c, false, "token", tokenURL) +} + +func init() { + check.Suite(&DockerDaemonSuite{ + ds: &DockerSuite{}, + }) +} + +type DockerDaemonSuite struct { + ds *DockerSuite + d *Daemon +} + +func (s *DockerDaemonSuite) SetUpTest(c *check.C) { + testRequires(c, DaemonIsLinux) + s.d = NewDaemon(c) +} + +func (s *DockerDaemonSuite) TearDownTest(c *check.C) { + testRequires(c, DaemonIsLinux) + if s.d != nil { + s.d.Stop() + } + s.ds.TearDownTest(c) +} + +func init() { + check.Suite(&DockerTrustSuite{ + ds: &DockerSuite{}, + }) +} + +type DockerTrustSuite struct { + ds *DockerSuite + reg *testRegistryV2 + not *testNotary +} + +func (s *DockerTrustSuite) SetUpTest(c *check.C) { + testRequires(c, RegistryHosting, NotaryServerHosting) + s.reg = setupRegistry(c, false, "", "") + s.not = setupNotary(c) +} + +func (s *DockerTrustSuite) TearDownTest(c *check.C) { + if s.reg != nil { + s.reg.Close() + } + if s.not != nil { + s.not.Close() + } + + // Remove trusted keys and metadata after test + os.RemoveAll(filepath.Join(cliconfig.ConfigDir(), "trust")) + s.ds.TearDownTest(c) +} diff --git a/integration-cli/daemon.go b/integration-cli/daemon.go new file mode 100644 index 00000000..2e3985af --- /dev/null +++ b/integration-cli/daemon.go @@ -0,0 +1,486 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/tlsconfig" + "github.com/docker/go-connections/sockets" + "github.com/go-check/check" +) + +// Daemon represents a Docker daemon for the testing framework. +type Daemon struct { + // Defaults to "daemon" + // Useful to set to --daemon or -d for checking backwards compatibility + Command string + GlobalFlags []string + + id string + c *check.C + logFile *os.File + folder string + root string + stdin io.WriteCloser + stdout, stderr io.ReadCloser + cmd *exec.Cmd + storageDriver string + wait chan error + userlandProxy bool + useDefaultHost bool + useDefaultTLSHost bool +} + +type clientConfig struct { + transport *http.Transport + scheme string + addr string +} + +// NewDaemon returns a Daemon instance to be used for testing. +// This will create a directory such as d123456789 in the folder specified by $DEST. +// The daemon will not automatically start. +func NewDaemon(c *check.C) *Daemon { + dest := os.Getenv("DEST") + c.Assert(dest, check.Not(check.Equals), "", check.Commentf("Please set the DEST environment variable")) + + id := fmt.Sprintf("d%d", time.Now().UnixNano()%100000000) + dir := filepath.Join(dest, id) + daemonFolder, err := filepath.Abs(dir) + c.Assert(err, check.IsNil, check.Commentf("Could not make %q an absolute path", dir)) + daemonRoot := filepath.Join(daemonFolder, "root") + + c.Assert(os.MkdirAll(daemonRoot, 0755), check.IsNil, check.Commentf("Could not create daemon root %q", dir)) + + userlandProxy := true + if env := os.Getenv("DOCKER_USERLANDPROXY"); env != "" { + if val, err := strconv.ParseBool(env); err != nil { + userlandProxy = val + } + } + + return &Daemon{ + Command: "daemon", + id: id, + c: c, + folder: daemonFolder, + root: daemonRoot, + storageDriver: os.Getenv("DOCKER_GRAPHDRIVER"), + userlandProxy: userlandProxy, + } +} + +func (d *Daemon) getClientConfig() (*clientConfig, error) { + var ( + transport *http.Transport + scheme string + addr string + proto string + ) + if d.useDefaultTLSHost { + option := &tlsconfig.Options{ + CAFile: "fixtures/https/ca.pem", + CertFile: "fixtures/https/client-cert.pem", + KeyFile: "fixtures/https/client-key.pem", + } + tlsConfig, err := tlsconfig.Client(*option) + if err != nil { + return nil, err + } + transport = &http.Transport{ + TLSClientConfig: tlsConfig, + } + addr = fmt.Sprintf("%s:%d", opts.DefaultHTTPHost, opts.DefaultTLSHTTPPort) + scheme = "https" + proto = "tcp" + } else if d.useDefaultHost { + addr = opts.DefaultUnixSocket + proto = "unix" + scheme = "http" + transport = &http.Transport{} + } else { + addr = filepath.Join(d.folder, "docker.sock") + proto = "unix" + scheme = "http" + transport = &http.Transport{} + } + + d.c.Assert(sockets.ConfigureTransport(transport, proto, addr), check.IsNil) + + return &clientConfig{ + transport: transport, + scheme: scheme, + addr: addr, + }, nil +} + +// Start will start the daemon and return once it is ready to receive requests. +// You can specify additional daemon flags. +func (d *Daemon) Start(args ...string) error { + logFile, err := os.OpenFile(filepath.Join(d.folder, "docker.log"), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600) + d.c.Assert(err, check.IsNil, check.Commentf("[%s] Could not create %s/docker.log", d.id, d.folder)) + + return d.StartWithLogFile(logFile, args...) +} + +// StartWithLogFile will start the daemon and attach its streams to a given file. +func (d *Daemon) StartWithLogFile(out *os.File, providedArgs ...string) error { + dockerBinary, err := exec.LookPath(dockerBinary) + d.c.Assert(err, check.IsNil, check.Commentf("[%s] could not find docker binary in $PATH", d.id)) + + args := append(d.GlobalFlags, + d.Command, + "--containerd", "/var/run/docker/libcontainerd/docker-containerd.sock", + "--graph", d.root, + "--exec-root", filepath.Join(d.folder, "exec-root"), + "--pidfile", fmt.Sprintf("%s/docker.pid", d.folder), + fmt.Sprintf("--userland-proxy=%t", d.userlandProxy), + ) + if !(d.useDefaultHost || d.useDefaultTLSHost) { + args = append(args, []string{"--host", d.sock()}...) + } + if root := os.Getenv("DOCKER_REMAP_ROOT"); root != "" { + args = append(args, []string{"--userns-remap", root}...) + } + + // If we don't explicitly set the log-level or debug flag(-D) then + // turn on debug mode + foundLog := false + foundSd := false + for _, a := range providedArgs { + if strings.Contains(a, "--log-level") || strings.Contains(a, "-D") || strings.Contains(a, "--debug") { + foundLog = true + } + if strings.Contains(a, "--storage-driver") { + foundSd = true + } + } + if !foundLog { + args = append(args, "--debug") + } + if d.storageDriver != "" && !foundSd { + args = append(args, "--storage-driver", d.storageDriver) + } + + args = append(args, providedArgs...) + d.cmd = exec.Command(dockerBinary, args...) + + d.cmd.Stdout = out + d.cmd.Stderr = out + d.logFile = out + + if err := d.cmd.Start(); err != nil { + return fmt.Errorf("[%s] could not start daemon container: %v", d.id, err) + } + + wait := make(chan error) + + go func() { + wait <- d.cmd.Wait() + d.c.Logf("[%s] exiting daemon", d.id) + close(wait) + }() + + d.wait = wait + + tick := time.Tick(500 * time.Millisecond) + // make sure daemon is ready to receive requests + startTime := time.Now().Unix() + for { + d.c.Logf("[%s] waiting for daemon to start", d.id) + if time.Now().Unix()-startTime > 5 { + // After 5 seconds, give up + return fmt.Errorf("[%s] Daemon exited and never started", d.id) + } + select { + case <-time.After(2 * time.Second): + return fmt.Errorf("[%s] timeout: daemon does not respond", d.id) + case <-tick: + clientConfig, err := d.getClientConfig() + if err != nil { + return err + } + + client := &http.Client{ + Transport: clientConfig.transport, + } + + req, err := http.NewRequest("GET", "/_ping", nil) + d.c.Assert(err, check.IsNil, check.Commentf("[%s] could not create new request", d.id)) + req.URL.Host = clientConfig.addr + req.URL.Scheme = clientConfig.scheme + resp, err := client.Do(req) + if err != nil { + continue + } + if resp.StatusCode != http.StatusOK { + d.c.Logf("[%s] received status != 200 OK: %s", d.id, resp.Status) + } + d.c.Logf("[%s] daemon started", d.id) + d.root, err = d.queryRootDir() + if err != nil { + return fmt.Errorf("[%s] error querying daemon for root directory: %v", d.id, err) + } + return nil + } + } +} + +// StartWithBusybox will first start the daemon with Daemon.Start() +// then save the busybox image from the main daemon and load it into this Daemon instance. +func (d *Daemon) StartWithBusybox(arg ...string) error { + if err := d.Start(arg...); err != nil { + return err + } + return d.LoadBusybox() +} + +// Kill will send a SIGKILL to the daemon +func (d *Daemon) Kill() error { + if d.cmd == nil || d.wait == nil { + return errors.New("daemon not started") + } + + defer func() { + d.logFile.Close() + d.cmd = nil + }() + + if err := d.cmd.Process.Kill(); err != nil { + d.c.Logf("Could not kill daemon: %v", err) + return err + } + + if err := os.Remove(fmt.Sprintf("%s/docker.pid", d.folder)); err != nil { + return err + } + + return nil +} + +// Stop will send a SIGINT every second and wait for the daemon to stop. +// If it timeouts, a SIGKILL is sent. +// Stop will not delete the daemon directory. If a purged daemon is needed, +// instantiate a new one with NewDaemon. +func (d *Daemon) Stop() error { + if d.cmd == nil || d.wait == nil { + return errors.New("daemon not started") + } + + defer func() { + d.logFile.Close() + d.cmd = nil + }() + + i := 1 + tick := time.Tick(time.Second) + + if err := d.cmd.Process.Signal(os.Interrupt); err != nil { + return fmt.Errorf("could not send signal: %v", err) + } +out1: + for { + select { + case err := <-d.wait: + return err + case <-time.After(15 * time.Second): + // time for stopping jobs and run onShutdown hooks + d.c.Log("timeout") + break out1 + } + } + +out2: + for { + select { + case err := <-d.wait: + return err + case <-tick: + i++ + if i > 4 { + d.c.Logf("tried to interrupt daemon for %d times, now try to kill it", i) + break out2 + } + d.c.Logf("Attempt #%d: daemon is still running with pid %d", i, d.cmd.Process.Pid) + if err := d.cmd.Process.Signal(os.Interrupt); err != nil { + return fmt.Errorf("could not send signal: %v", err) + } + } + } + + if err := d.cmd.Process.Kill(); err != nil { + d.c.Logf("Could not kill daemon: %v", err) + return err + } + + if err := os.Remove(fmt.Sprintf("%s/docker.pid", d.folder)); err != nil { + return err + } + + return nil +} + +// Restart will restart the daemon by first stopping it and then starting it. +func (d *Daemon) Restart(arg ...string) error { + d.Stop() + // in the case of tests running a user namespace-enabled daemon, we have resolved + // d.root to be the actual final path of the graph dir after the "uid.gid" of + // remapped root is added--we need to subtract it from the path before calling + // start or else we will continue making subdirectories rather than truly restarting + // with the same location/root: + if root := os.Getenv("DOCKER_REMAP_ROOT"); root != "" { + d.root = filepath.Dir(d.root) + } + return d.Start(arg...) +} + +// LoadBusybox will load the stored busybox into a newly started daemon +func (d *Daemon) LoadBusybox() error { + bb := filepath.Join(d.folder, "busybox.tar") + if _, err := os.Stat(bb); err != nil { + if !os.IsNotExist(err) { + return fmt.Errorf("unexpected error on busybox.tar stat: %v", err) + } + // saving busybox image from main daemon + if err := exec.Command(dockerBinary, "save", "--output", bb, "busybox:latest").Run(); err != nil { + return fmt.Errorf("could not save busybox image: %v", err) + } + } + // loading busybox image to this daemon + if out, err := d.Cmd("load", "--input", bb); err != nil { + return fmt.Errorf("could not load busybox image: %s", out) + } + if err := os.Remove(bb); err != nil { + d.c.Logf("could not remove %s: %v", bb, err) + } + return nil +} + +func (d *Daemon) queryRootDir() (string, error) { + // update daemon root by asking /info endpoint (to support user + // namespaced daemon with root remapped uid.gid directory) + clientConfig, err := d.getClientConfig() + if err != nil { + return "", err + } + + client := &http.Client{ + Transport: clientConfig.transport, + } + + req, err := http.NewRequest("GET", "/info", nil) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/json") + req.URL.Host = clientConfig.addr + req.URL.Scheme = clientConfig.scheme + + resp, err := client.Do(req) + if err != nil { + return "", err + } + body := ioutils.NewReadCloserWrapper(resp.Body, func() error { + return resp.Body.Close() + }) + + type Info struct { + DockerRootDir string + } + var b []byte + var i Info + b, err = readBody(body) + if err == nil && resp.StatusCode == 200 { + // read the docker root dir + if err = json.Unmarshal(b, &i); err == nil { + return i.DockerRootDir, nil + } + } + return "", err +} + +func (d *Daemon) sock() string { + return fmt.Sprintf("unix://%s/docker.sock", d.folder) +} + +func (d *Daemon) waitRun(contID string) error { + args := []string{"--host", d.sock()} + return waitInspectWithArgs(contID, "{{.State.Running}}", "true", 10*time.Second, args...) +} + +func (d *Daemon) getBaseDeviceSize(c *check.C) int64 { + infoCmdOutput, _, err := runCommandPipelineWithOutput( + exec.Command(dockerBinary, "-H", d.sock(), "info"), + exec.Command("grep", "Base Device Size"), + ) + c.Assert(err, checker.IsNil) + basesizeSlice := strings.Split(infoCmdOutput, ":") + basesize := strings.Trim(basesizeSlice[1], " ") + basesize = strings.Trim(basesize, "\n")[:len(basesize)-3] + basesizeFloat, err := strconv.ParseFloat(strings.Trim(basesize, " "), 64) + c.Assert(err, checker.IsNil) + basesizeBytes := int64(basesizeFloat) * (1024 * 1024 * 1024) + return basesizeBytes +} + +// Cmd will execute a docker CLI command against this Daemon. +// Example: d.Cmd("version") will run docker -H unix://path/to/unix.sock version +func (d *Daemon) Cmd(name string, arg ...string) (string, error) { + args := []string{"--host", d.sock(), name} + args = append(args, arg...) + c := exec.Command(dockerBinary, args...) + b, err := c.CombinedOutput() + return string(b), err +} + +// CmdWithArgs will execute a docker CLI command against a daemon with the +// given additional arguments +func (d *Daemon) CmdWithArgs(daemonArgs []string, name string, arg ...string) (string, error) { + args := append(daemonArgs, name) + args = append(args, arg...) + c := exec.Command(dockerBinary, args...) + b, err := c.CombinedOutput() + return string(b), err +} + +// LogFileName returns the path the the daemon's log file +func (d *Daemon) LogFileName() string { + return d.logFile.Name() +} + +func (d *Daemon) getIDByName(name string) (string, error) { + return d.inspectFieldWithError(name, "Id") +} + +func (d *Daemon) inspectFilter(name, filter string) (string, error) { + format := fmt.Sprintf("{{%s}}", filter) + out, err := d.Cmd("inspect", "-f", format, name) + if err != nil { + return "", fmt.Errorf("failed to inspect %s: %s", name, out) + } + return strings.TrimSpace(out), nil +} + +func (d *Daemon) inspectFieldWithError(name, field string) (string, error) { + return d.inspectFilter(name, fmt.Sprintf(".%s", field)) +} + +func (d *Daemon) findContainerIP(id string) string { + out, err := d.Cmd("inspect", fmt.Sprintf("--format='{{ .NetworkSettings.Networks.bridge.IPAddress }}'"), id) + if err != nil { + d.c.Log(err) + } + return strings.Trim(out, " \r\n'") +} diff --git a/integration-cli/docker_api_attach_test.go b/integration-cli/docker_api_attach_test.go new file mode 100644 index 00000000..a0a8e7d6 --- /dev/null +++ b/integration-cli/docker_api_attach_test.go @@ -0,0 +1,164 @@ +package main + +import ( + "bufio" + "io" + "net" + "net/http" + "strings" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" + "golang.org/x/net/websocket" +) + +func (s *DockerSuite) TestGetContainersAttachWebsocket(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-dit", "busybox", "cat") + + rwc, err := sockConn(time.Duration(10 * time.Second)) + c.Assert(err, checker.IsNil) + + cleanedContainerID := strings.TrimSpace(out) + config, err := websocket.NewConfig( + "/containers/"+cleanedContainerID+"/attach/ws?stream=1&stdin=1&stdout=1&stderr=1", + "http://localhost", + ) + c.Assert(err, checker.IsNil) + + ws, err := websocket.NewClient(config, rwc) + c.Assert(err, checker.IsNil) + defer ws.Close() + + expected := []byte("hello") + actual := make([]byte, len(expected)) + + outChan := make(chan error) + go func() { + _, err := ws.Read(actual) + outChan <- err + close(outChan) + }() + + inChan := make(chan error) + go func() { + _, err := ws.Write(expected) + inChan <- err + close(inChan) + }() + + select { + case err := <-inChan: + c.Assert(err, checker.IsNil) + case <-time.After(5 * time.Second): + c.Fatal("Timeout writing to ws") + } + + select { + case err := <-outChan: + c.Assert(err, checker.IsNil) + case <-time.After(5 * time.Second): + c.Fatal("Timeout reading from ws") + } + + c.Assert(actual, checker.DeepEquals, expected, check.Commentf("Websocket didn't return the expected data")) +} + +// regression gh14320 +func (s *DockerSuite) TestPostContainersAttachContainerNotFound(c *check.C) { + status, body, err := sockRequest("POST", "/containers/doesnotexist/attach", nil) + c.Assert(status, checker.Equals, http.StatusNotFound) + c.Assert(err, checker.IsNil) + expected := "No such container: doesnotexist\n" + c.Assert(string(body), checker.Contains, expected) +} + +func (s *DockerSuite) TestGetContainersWsAttachContainerNotFound(c *check.C) { + status, body, err := sockRequest("GET", "/containers/doesnotexist/attach/ws", nil) + c.Assert(status, checker.Equals, http.StatusNotFound) + c.Assert(err, checker.IsNil) + expected := "No such container: doesnotexist\n" + c.Assert(string(body), checker.Contains, expected) +} + +func (s *DockerSuite) TestPostContainersAttach(c *check.C) { + testRequires(c, DaemonIsLinux) + + expectSuccess := func(conn net.Conn, br *bufio.Reader, stream string, tty bool) { + defer conn.Close() + expected := []byte("success") + _, err := conn.Write(expected) + c.Assert(err, checker.IsNil) + + conn.SetReadDeadline(time.Now().Add(time.Second)) + lenHeader := 0 + if !tty { + lenHeader = 8 + } + actual := make([]byte, len(expected)+lenHeader) + _, err = io.ReadFull(br, actual) + c.Assert(err, checker.IsNil) + if !tty { + fdMap := map[string]byte{ + "stdin": 0, + "stdout": 1, + "stderr": 2, + } + c.Assert(actual[0], checker.Equals, fdMap[stream]) + } + c.Assert(actual[lenHeader:], checker.DeepEquals, expected, check.Commentf("Attach didn't return the expected data from %s", stream)) + } + + expectTimeout := func(conn net.Conn, br *bufio.Reader, stream string) { + defer conn.Close() + _, err := conn.Write([]byte{'t'}) + c.Assert(err, checker.IsNil) + + conn.SetReadDeadline(time.Now().Add(time.Second)) + actual := make([]byte, 1) + _, err = io.ReadFull(br, actual) + opErr, ok := err.(*net.OpError) + c.Assert(ok, checker.Equals, true, check.Commentf("Error is expected to be *net.OpError, got %v", err)) + c.Assert(opErr.Timeout(), checker.Equals, true, check.Commentf("Read from %s is expected to timeout", stream)) + } + + // Create a container that only emits stdout. + cid, _ := dockerCmd(c, "run", "-di", "busybox", "cat") + cid = strings.TrimSpace(cid) + // Attach to the container's stdout stream. + conn, br, err := sockRequestHijack("POST", "/containers/"+cid+"/attach?stream=1&stdin=1&stdout=1", nil, "text/plain") + c.Assert(err, checker.IsNil) + // Check if the data from stdout can be received. + expectSuccess(conn, br, "stdout", false) + // Attach to the container's stderr stream. + conn, br, err = sockRequestHijack("POST", "/containers/"+cid+"/attach?stream=1&stdin=1&stderr=1", nil, "text/plain") + c.Assert(err, checker.IsNil) + // Since the container only emits stdout, attaching to stderr should return nothing. + expectTimeout(conn, br, "stdout") + + // Test the similar functions of the stderr stream. + cid, _ = dockerCmd(c, "run", "-di", "busybox", "/bin/sh", "-c", "cat >&2") + cid = strings.TrimSpace(cid) + conn, br, err = sockRequestHijack("POST", "/containers/"+cid+"/attach?stream=1&stdin=1&stderr=1", nil, "text/plain") + c.Assert(err, checker.IsNil) + expectSuccess(conn, br, "stderr", false) + conn, br, err = sockRequestHijack("POST", "/containers/"+cid+"/attach?stream=1&stdin=1&stdout=1", nil, "text/plain") + c.Assert(err, checker.IsNil) + expectTimeout(conn, br, "stderr") + + // Test with tty. + cid, _ = dockerCmd(c, "run", "-dit", "busybox", "/bin/sh", "-c", "cat >&2") + cid = strings.TrimSpace(cid) + // Attach to stdout only. + conn, br, err = sockRequestHijack("POST", "/containers/"+cid+"/attach?stream=1&stdin=1&stdout=1", nil, "text/plain") + c.Assert(err, checker.IsNil) + expectSuccess(conn, br, "stdout", true) + + // Attach without stdout stream. + conn, br, err = sockRequestHijack("POST", "/containers/"+cid+"/attach?stream=1&stdin=1&stderr=1", nil, "text/plain") + c.Assert(err, checker.IsNil) + // Nothing should be received because both the stdout and stderr of the container will be + // sent to the client as stdout when tty is enabled. + expectTimeout(conn, br, "stdout") +} diff --git a/integration-cli/docker_api_auth_test.go b/integration-cli/docker_api_auth_test.go new file mode 100644 index 00000000..63e78ab5 --- /dev/null +++ b/integration-cli/docker_api_auth_test.go @@ -0,0 +1,23 @@ +package main + +import ( + "net/http" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/engine-api/types" + "github.com/go-check/check" +) + +// Test case for #22244 +func (s *DockerSuite) TestAuthApi(c *check.C) { + config := types.AuthConfig{ + Username: "no-user", + Password: "no-password", + } + + expected := "Get https://registry-1.docker.io/v2/: unauthorized: incorrect username or password\n" + status, body, err := sockRequest("POST", "/auth", config) + c.Assert(err, check.IsNil) + c.Assert(status, check.Equals, http.StatusUnauthorized) + c.Assert(string(body), checker.Contains, expected, check.Commentf("Expected: %v, got: %v", expected, string(body))) +} diff --git a/integration-cli/docker_api_build_test.go b/integration-cli/docker_api_build_test.go new file mode 100644 index 00000000..49de71c9 --- /dev/null +++ b/integration-cli/docker_api_build_test.go @@ -0,0 +1,257 @@ +package main + +import ( + "archive/tar" + "bytes" + "net/http" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestBuildApiDockerfilePath(c *check.C) { + // Test to make sure we stop people from trying to leave the + // build context when specifying the path to the dockerfile + buffer := new(bytes.Buffer) + tw := tar.NewWriter(buffer) + defer tw.Close() + + dockerfile := []byte("FROM busybox") + err := tw.WriteHeader(&tar.Header{ + Name: "Dockerfile", + Size: int64(len(dockerfile)), + }) + //failed to write tar file header + c.Assert(err, checker.IsNil) + + _, err = tw.Write(dockerfile) + // failed to write tar file content + c.Assert(err, checker.IsNil) + + // failed to close tar archive + c.Assert(tw.Close(), checker.IsNil) + + res, body, err := sockRequestRaw("POST", "/build?dockerfile=../Dockerfile", buffer, "application/x-tar") + c.Assert(err, checker.IsNil) + c.Assert(res.StatusCode, checker.Equals, http.StatusInternalServerError) + + out, err := readBody(body) + c.Assert(err, checker.IsNil) + + // Didn't complain about leaving build context + c.Assert(string(out), checker.Contains, "Forbidden path outside the build context") +} + +func (s *DockerSuite) TestBuildApiDockerFileRemote(c *check.C) { + testRequires(c, NotUserNamespace) + testRequires(c, DaemonIsLinux) + server, err := fakeStorage(map[string]string{ + "testD": `FROM busybox +COPY * /tmp/ +RUN find / -name ba* +RUN find /tmp/`, + }) + c.Assert(err, checker.IsNil) + defer server.Close() + + res, body, err := sockRequestRaw("POST", "/build?dockerfile=baz&remote="+server.URL()+"/testD", nil, "application/json") + c.Assert(err, checker.IsNil) + c.Assert(res.StatusCode, checker.Equals, http.StatusOK) + + buf, err := readBody(body) + c.Assert(err, checker.IsNil) + + // Make sure Dockerfile exists. + // Make sure 'baz' doesn't exist ANYWHERE despite being mentioned in the URL + out := string(buf) + c.Assert(out, checker.Contains, "/tmp/Dockerfile") + c.Assert(out, checker.Not(checker.Contains), "baz") +} + +func (s *DockerSuite) TestBuildApiRemoteTarballContext(c *check.C) { + testRequires(c, DaemonIsLinux) + buffer := new(bytes.Buffer) + tw := tar.NewWriter(buffer) + defer tw.Close() + + dockerfile := []byte("FROM busybox") + err := tw.WriteHeader(&tar.Header{ + Name: "Dockerfile", + Size: int64(len(dockerfile)), + }) + // failed to write tar file header + c.Assert(err, checker.IsNil) + + _, err = tw.Write(dockerfile) + // failed to write tar file content + c.Assert(err, checker.IsNil) + + // failed to close tar archive + c.Assert(tw.Close(), checker.IsNil) + + server, err := fakeBinaryStorage(map[string]*bytes.Buffer{ + "testT.tar": buffer, + }) + c.Assert(err, checker.IsNil) + + defer server.Close() + + res, b, err := sockRequestRaw("POST", "/build?remote="+server.URL()+"/testT.tar", nil, "application/tar") + c.Assert(err, checker.IsNil) + c.Assert(res.StatusCode, checker.Equals, http.StatusOK) + b.Close() +} + +func (s *DockerSuite) TestBuildApiRemoteTarballContextWithCustomDockerfile(c *check.C) { + testRequires(c, DaemonIsLinux) + buffer := new(bytes.Buffer) + tw := tar.NewWriter(buffer) + defer tw.Close() + + dockerfile := []byte(`FROM busybox +RUN echo 'wrong'`) + err := tw.WriteHeader(&tar.Header{ + Name: "Dockerfile", + Size: int64(len(dockerfile)), + }) + // failed to write tar file header + c.Assert(err, checker.IsNil) + + _, err = tw.Write(dockerfile) + // failed to write tar file content + c.Assert(err, checker.IsNil) + + custom := []byte(`FROM busybox +RUN echo 'right' +`) + err = tw.WriteHeader(&tar.Header{ + Name: "custom", + Size: int64(len(custom)), + }) + + // failed to write tar file header + c.Assert(err, checker.IsNil) + + _, err = tw.Write(custom) + // failed to write tar file content + c.Assert(err, checker.IsNil) + + // failed to close tar archive + c.Assert(tw.Close(), checker.IsNil) + + server, err := fakeBinaryStorage(map[string]*bytes.Buffer{ + "testT.tar": buffer, + }) + c.Assert(err, checker.IsNil) + + defer server.Close() + url := "/build?dockerfile=custom&remote=" + server.URL() + "/testT.tar" + res, body, err := sockRequestRaw("POST", url, nil, "application/tar") + c.Assert(err, checker.IsNil) + c.Assert(res.StatusCode, checker.Equals, http.StatusOK) + + defer body.Close() + content, err := readBody(body) + c.Assert(err, checker.IsNil) + + // Build used the wrong dockerfile. + c.Assert(string(content), checker.Not(checker.Contains), "wrong") +} + +func (s *DockerSuite) TestBuildApiLowerDockerfile(c *check.C) { + testRequires(c, DaemonIsLinux) + git, err := newFakeGit("repo", map[string]string{ + "dockerfile": `FROM busybox +RUN echo from dockerfile`, + }, false) + c.Assert(err, checker.IsNil) + defer git.Close() + + res, body, err := sockRequestRaw("POST", "/build?remote="+git.RepoURL, nil, "application/json") + c.Assert(err, checker.IsNil) + c.Assert(res.StatusCode, checker.Equals, http.StatusOK) + + buf, err := readBody(body) + c.Assert(err, checker.IsNil) + + out := string(buf) + c.Assert(out, checker.Contains, "from dockerfile") +} + +func (s *DockerSuite) TestBuildApiBuildGitWithF(c *check.C) { + testRequires(c, DaemonIsLinux) + git, err := newFakeGit("repo", map[string]string{ + "baz": `FROM busybox +RUN echo from baz`, + "Dockerfile": `FROM busybox +RUN echo from Dockerfile`, + }, false) + c.Assert(err, checker.IsNil) + defer git.Close() + + // Make sure it tries to 'dockerfile' query param value + res, body, err := sockRequestRaw("POST", "/build?dockerfile=baz&remote="+git.RepoURL, nil, "application/json") + c.Assert(err, checker.IsNil) + c.Assert(res.StatusCode, checker.Equals, http.StatusOK) + + buf, err := readBody(body) + c.Assert(err, checker.IsNil) + + out := string(buf) + c.Assert(out, checker.Contains, "from baz") +} + +func (s *DockerSuite) TestBuildApiDoubleDockerfile(c *check.C) { + testRequires(c, UnixCli) // dockerfile overwrites Dockerfile on Windows + git, err := newFakeGit("repo", map[string]string{ + "Dockerfile": `FROM busybox +RUN echo from Dockerfile`, + "dockerfile": `FROM busybox +RUN echo from dockerfile`, + }, false) + c.Assert(err, checker.IsNil) + defer git.Close() + + // Make sure it tries to 'dockerfile' query param value + res, body, err := sockRequestRaw("POST", "/build?remote="+git.RepoURL, nil, "application/json") + c.Assert(err, checker.IsNil) + c.Assert(res.StatusCode, checker.Equals, http.StatusOK) + + buf, err := readBody(body) + c.Assert(err, checker.IsNil) + + out := string(buf) + c.Assert(out, checker.Contains, "from Dockerfile") +} + +func (s *DockerSuite) TestBuildApiDockerfileSymlink(c *check.C) { + // Test to make sure we stop people from trying to leave the + // build context when specifying a symlink as the path to the dockerfile + buffer := new(bytes.Buffer) + tw := tar.NewWriter(buffer) + defer tw.Close() + + err := tw.WriteHeader(&tar.Header{ + Name: "Dockerfile", + Typeflag: tar.TypeSymlink, + Linkname: "/etc/passwd", + }) + // failed to write tar file header + c.Assert(err, checker.IsNil) + + // failed to close tar archive + c.Assert(tw.Close(), checker.IsNil) + + res, body, err := sockRequestRaw("POST", "/build", buffer, "application/x-tar") + c.Assert(err, checker.IsNil) + c.Assert(res.StatusCode, checker.Equals, http.StatusInternalServerError) + + out, err := readBody(body) + c.Assert(err, checker.IsNil) + + // The reason the error is "Cannot locate specified Dockerfile" is because + // in the builder, the symlink is resolved within the context, therefore + // Dockerfile -> /etc/passwd becomes etc/passwd from the context which is + // a nonexistent file. + c.Assert(string(out), checker.Contains, "Cannot locate specified Dockerfile: Dockerfile", check.Commentf("Didn't complain about leaving build context")) +} diff --git a/integration-cli/docker_api_containers_test.go b/integration-cli/docker_api_containers_test.go new file mode 100644 index 00000000..2622a544 --- /dev/null +++ b/integration-cli/docker_api_containers_test.go @@ -0,0 +1,1626 @@ +package main + +import ( + "archive/tar" + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httputil" + "net/url" + "os" + "regexp" + "strconv" + "strings" + "time" + + "github.com/docker/docker/pkg/integration" + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/engine-api/types" + containertypes "github.com/docker/engine-api/types/container" + networktypes "github.com/docker/engine-api/types/network" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestContainerApiGetAll(c *check.C) { + startCount, err := getContainerCount() + c.Assert(err, checker.IsNil, check.Commentf("Cannot query container count")) + + name := "getall" + dockerCmd(c, "run", "--name", name, "busybox", "true") + + status, body, err := sockRequest("GET", "/containers/json?all=1", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusOK) + + var inspectJSON []struct { + Names []string + } + err = json.Unmarshal(body, &inspectJSON) + c.Assert(err, checker.IsNil, check.Commentf("unable to unmarshal response body")) + + c.Assert(inspectJSON, checker.HasLen, startCount+1) + + actual := inspectJSON[0].Names[0] + c.Assert(actual, checker.Equals, "/"+name) +} + +// regression test for empty json field being omitted #13691 +func (s *DockerSuite) TestContainerApiGetJSONNoFieldsOmitted(c *check.C) { + dockerCmd(c, "run", "busybox", "true") + + status, body, err := sockRequest("GET", "/containers/json?all=1", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusOK) + + // empty Labels field triggered this bug, make sense to check for everything + // cause even Ports for instance can trigger this bug + // better safe than sorry.. + fields := []string{ + "Id", + "Names", + "Image", + "Command", + "Created", + "Ports", + "Labels", + "Status", + "NetworkSettings", + } + + // decoding into types.Container do not work since it eventually unmarshal + // and empty field to an empty go map, so we just check for a string + for _, f := range fields { + if !strings.Contains(string(body), f) { + c.Fatalf("Field %s is missing and it shouldn't", f) + } + } +} + +type containerPs struct { + Names []string + Ports []map[string]interface{} +} + +// regression test for non-empty fields from #13901 +func (s *DockerSuite) TestContainerApiPsOmitFields(c *check.C) { + // Problematic for Windows porting due to networking not yet being passed back + testRequires(c, DaemonIsLinux) + name := "pstest" + port := 80 + runSleepingContainer(c, "--name", name, "--expose", strconv.Itoa(port)) + + status, body, err := sockRequest("GET", "/containers/json?all=1", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusOK) + + var resp []containerPs + err = json.Unmarshal(body, &resp) + c.Assert(err, checker.IsNil) + + var foundContainer *containerPs + for _, container := range resp { + for _, testName := range container.Names { + if "/"+name == testName { + foundContainer = &container + break + } + } + } + + c.Assert(foundContainer.Ports, checker.HasLen, 1) + c.Assert(foundContainer.Ports[0]["PrivatePort"], checker.Equals, float64(port)) + _, ok := foundContainer.Ports[0]["PublicPort"] + c.Assert(ok, checker.Not(checker.Equals), true) + _, ok = foundContainer.Ports[0]["IP"] + c.Assert(ok, checker.Not(checker.Equals), true) +} + +func (s *DockerSuite) TestContainerApiGetExport(c *check.C) { + // TODO: Investigate why this fails on Windows to Windows CI + testRequires(c, DaemonIsLinux) + name := "exportcontainer" + dockerCmd(c, "run", "--name", name, "busybox", "touch", "/test") + + status, body, err := sockRequest("GET", "/containers/"+name+"/export", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusOK) + + found := false + for tarReader := tar.NewReader(bytes.NewReader(body)); ; { + h, err := tarReader.Next() + if err != nil && err == io.EOF { + break + } + if h.Name == "test" { + found = true + break + } + } + c.Assert(found, checker.True, check.Commentf("The created test file has not been found in the exported image")) +} + +func (s *DockerSuite) TestContainerApiGetChanges(c *check.C) { + // Not supported on Windows as Windows does not support docker diff (/containers/name/changes) + testRequires(c, DaemonIsLinux) + name := "changescontainer" + dockerCmd(c, "run", "--name", name, "busybox", "rm", "/etc/passwd") + + status, body, err := sockRequest("GET", "/containers/"+name+"/changes", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusOK) + + changes := []struct { + Kind int + Path string + }{} + c.Assert(json.Unmarshal(body, &changes), checker.IsNil, check.Commentf("unable to unmarshal response body")) + + // Check the changelog for removal of /etc/passwd + success := false + for _, elem := range changes { + if elem.Path == "/etc/passwd" && elem.Kind == 2 { + success = true + } + } + c.Assert(success, checker.True, check.Commentf("/etc/passwd has been removed but is not present in the diff")) +} + +func (s *DockerSuite) TestContainerApiStartVolumeBinds(c *check.C) { + // TODO Windows CI: Investigate further why this fails on Windows to Windows CI. + testRequires(c, DaemonIsLinux) + path := "/foo" + if daemonPlatform == "windows" { + path = `c:\foo` + } + name := "testing" + config := map[string]interface{}{ + "Image": "busybox", + "Volumes": map[string]struct{}{path: {}}, + } + + status, _, err := sockRequest("POST", "/containers/create?name="+name, config) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusCreated) + + bindPath := randomTmpDirPath("test", daemonPlatform) + config = map[string]interface{}{ + "Binds": []string{bindPath + ":" + path}, + } + status, _, err = sockRequest("POST", "/containers/"+name+"/start", config) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusNoContent) + + pth, err := inspectMountSourceField(name, path) + c.Assert(err, checker.IsNil) + c.Assert(pth, checker.Equals, bindPath, check.Commentf("expected volume host path to be %s, got %s", bindPath, pth)) +} + +// Test for GH#10618 +func (s *DockerSuite) TestContainerApiStartDupVolumeBinds(c *check.C) { + // TODO Windows to Windows CI - Port this + testRequires(c, DaemonIsLinux) + name := "testdups" + config := map[string]interface{}{ + "Image": "busybox", + "Volumes": map[string]struct{}{"/tmp": {}}, + } + + status, _, err := sockRequest("POST", "/containers/create?name="+name, config) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusCreated) + + bindPath1 := randomTmpDirPath("test1", daemonPlatform) + bindPath2 := randomTmpDirPath("test2", daemonPlatform) + + config = map[string]interface{}{ + "Binds": []string{bindPath1 + ":/tmp", bindPath2 + ":/tmp"}, + } + status, body, err := sockRequest("POST", "/containers/"+name+"/start", config) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusInternalServerError) + c.Assert(string(body), checker.Contains, "Duplicate mount point", check.Commentf("Expected failure due to duplicate bind mounts to same path, instead got: %q with error: %v", string(body), err)) +} + +func (s *DockerSuite) TestContainerApiStartVolumesFrom(c *check.C) { + // TODO Windows to Windows CI - Port this + testRequires(c, DaemonIsLinux) + volName := "voltst" + volPath := "/tmp" + + dockerCmd(c, "run", "--name", volName, "-v", volPath, "busybox") + + name := "TestContainerApiStartVolumesFrom" + config := map[string]interface{}{ + "Image": "busybox", + "Volumes": map[string]struct{}{volPath: {}}, + } + + status, _, err := sockRequest("POST", "/containers/create?name="+name, config) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusCreated) + + config = map[string]interface{}{ + "VolumesFrom": []string{volName}, + } + status, _, err = sockRequest("POST", "/containers/"+name+"/start", config) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusNoContent) + + pth, err := inspectMountSourceField(name, volPath) + c.Assert(err, checker.IsNil) + pth2, err := inspectMountSourceField(volName, volPath) + c.Assert(err, checker.IsNil) + c.Assert(pth, checker.Equals, pth2, check.Commentf("expected volume host path to be %s, got %s", pth, pth2)) +} + +func (s *DockerSuite) TestGetContainerStats(c *check.C) { + // Problematic on Windows as Windows does not support stats + testRequires(c, DaemonIsLinux) + var ( + name = "statscontainer" + ) + dockerCmd(c, "run", "-d", "--name", name, "busybox", "top") + + type b struct { + status int + body []byte + err error + } + bc := make(chan b, 1) + go func() { + status, body, err := sockRequest("GET", "/containers/"+name+"/stats", nil) + bc <- b{status, body, err} + }() + + // allow some time to stream the stats from the container + time.Sleep(4 * time.Second) + dockerCmd(c, "rm", "-f", name) + + // collect the results from the stats stream or timeout and fail + // if the stream was not disconnected. + select { + case <-time.After(2 * time.Second): + c.Fatal("stream was not closed after container was removed") + case sr := <-bc: + c.Assert(sr.err, checker.IsNil) + c.Assert(sr.status, checker.Equals, http.StatusOK) + + dec := json.NewDecoder(bytes.NewBuffer(sr.body)) + var s *types.Stats + // decode only one object from the stream + c.Assert(dec.Decode(&s), checker.IsNil) + } +} + +func (s *DockerSuite) TestGetContainerStatsRmRunning(c *check.C) { + // Problematic on Windows as Windows does not support stats + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "top") + id := strings.TrimSpace(out) + + buf := &integration.ChannelBuffer{make(chan []byte, 1)} + defer buf.Close() + chErr := make(chan error, 1) + go func() { + _, body, err := sockRequestRaw("GET", "/containers/"+id+"/stats?stream=1", nil, "application/json") + if err != nil { + chErr <- err + } + defer body.Close() + _, err = io.Copy(buf, body) + chErr <- err + }() + defer func() { + select { + case err := <-chErr: + c.Assert(err, checker.IsNil) + default: + return + } + }() + + b := make([]byte, 32) + // make sure we've got some stats + _, err := buf.ReadTimeout(b, 2*time.Second) + c.Assert(err, checker.IsNil) + + // Now remove without `-f` and make sure we are still pulling stats + _, _, err = dockerCmdWithError("rm", id) + c.Assert(err, checker.Not(checker.IsNil), check.Commentf("rm should have failed but didn't")) + _, err = buf.ReadTimeout(b, 2*time.Second) + c.Assert(err, checker.IsNil) + + dockerCmd(c, "kill", id) +} + +// regression test for gh13421 +// previous test was just checking one stat entry so it didn't fail (stats with +// stream false always return one stat) +func (s *DockerSuite) TestGetContainerStatsStream(c *check.C) { + // Problematic on Windows as Windows does not support stats + testRequires(c, DaemonIsLinux) + name := "statscontainer" + dockerCmd(c, "run", "-d", "--name", name, "busybox", "top") + + type b struct { + status int + body []byte + err error + } + bc := make(chan b, 1) + go func() { + status, body, err := sockRequest("GET", "/containers/"+name+"/stats", nil) + bc <- b{status, body, err} + }() + + // allow some time to stream the stats from the container + time.Sleep(4 * time.Second) + dockerCmd(c, "rm", "-f", name) + + // collect the results from the stats stream or timeout and fail + // if the stream was not disconnected. + select { + case <-time.After(2 * time.Second): + c.Fatal("stream was not closed after container was removed") + case sr := <-bc: + c.Assert(sr.err, checker.IsNil) + c.Assert(sr.status, checker.Equals, http.StatusOK) + + s := string(sr.body) + // count occurrences of "read" of types.Stats + if l := strings.Count(s, "read"); l < 2 { + c.Fatalf("Expected more than one stat streamed, got %d", l) + } + } +} + +func (s *DockerSuite) TestGetContainerStatsNoStream(c *check.C) { + // Problematic on Windows as Windows does not support stats + testRequires(c, DaemonIsLinux) + name := "statscontainer" + dockerCmd(c, "run", "-d", "--name", name, "busybox", "top") + + type b struct { + status int + body []byte + err error + } + bc := make(chan b, 1) + go func() { + status, body, err := sockRequest("GET", "/containers/"+name+"/stats?stream=0", nil) + bc <- b{status, body, err} + }() + + // allow some time to stream the stats from the container + time.Sleep(4 * time.Second) + dockerCmd(c, "rm", "-f", name) + + // collect the results from the stats stream or timeout and fail + // if the stream was not disconnected. + select { + case <-time.After(2 * time.Second): + c.Fatal("stream was not closed after container was removed") + case sr := <-bc: + c.Assert(sr.err, checker.IsNil) + c.Assert(sr.status, checker.Equals, http.StatusOK) + + s := string(sr.body) + // count occurrences of "read" of types.Stats + c.Assert(strings.Count(s, "read"), checker.Equals, 1, check.Commentf("Expected only one stat streamed, got %d", strings.Count(s, "read"))) + } +} + +func (s *DockerSuite) TestGetStoppedContainerStats(c *check.C) { + // Problematic on Windows as Windows does not support stats + testRequires(c, DaemonIsLinux) + name := "statscontainer" + dockerCmd(c, "create", "--name", name, "busybox", "top") + + type stats struct { + status int + err error + } + chResp := make(chan stats) + + // We expect an immediate response, but if it's not immediate, the test would hang, so put it in a goroutine + // below we'll check this on a timeout. + go func() { + resp, body, err := sockRequestRaw("GET", "/containers/"+name+"/stats", nil, "") + body.Close() + chResp <- stats{resp.StatusCode, err} + }() + + select { + case r := <-chResp: + c.Assert(r.err, checker.IsNil) + c.Assert(r.status, checker.Equals, http.StatusOK) + case <-time.After(10 * time.Second): + c.Fatal("timeout waiting for stats response for stopped container") + } +} + +// #9981 - Allow a docker created volume (ie, one in /var/lib/docker/volumes) to be used to overwrite (via passing in Binds on api start) an existing volume +func (s *DockerSuite) TestPostContainerBindNormalVolume(c *check.C) { + // TODO Windows to Windows CI - Port this + testRequires(c, DaemonIsLinux) + dockerCmd(c, "create", "-v", "/foo", "--name=one", "busybox") + + fooDir, err := inspectMountSourceField("one", "/foo") + c.Assert(err, checker.IsNil) + + dockerCmd(c, "create", "-v", "/foo", "--name=two", "busybox") + + bindSpec := map[string][]string{"Binds": {fooDir + ":/foo"}} + status, _, err := sockRequest("POST", "/containers/two/start", bindSpec) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusNoContent) + + fooDir2, err := inspectMountSourceField("two", "/foo") + c.Assert(err, checker.IsNil) + c.Assert(fooDir2, checker.Equals, fooDir, check.Commentf("expected volume path to be %s, got: %s", fooDir, fooDir2)) +} + +func (s *DockerSuite) TestContainerApiPause(c *check.C) { + // Problematic on Windows as Windows does not support pause + testRequires(c, DaemonIsLinux) + defer unpauseAllContainers() + out, _ := dockerCmd(c, "run", "-d", "busybox", "sleep", "30") + ContainerID := strings.TrimSpace(out) + + status, _, err := sockRequest("POST", "/containers/"+ContainerID+"/pause", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusNoContent) + + pausedContainers, err := getSliceOfPausedContainers() + c.Assert(err, checker.IsNil, check.Commentf("error thrown while checking if containers were paused")) + + if len(pausedContainers) != 1 || stringid.TruncateID(ContainerID) != pausedContainers[0] { + c.Fatalf("there should be one paused container and not %d", len(pausedContainers)) + } + + status, _, err = sockRequest("POST", "/containers/"+ContainerID+"/unpause", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusNoContent) + + pausedContainers, err = getSliceOfPausedContainers() + c.Assert(err, checker.IsNil, check.Commentf("error thrown while checking if containers were paused")) + c.Assert(pausedContainers, checker.IsNil, check.Commentf("There should be no paused container.")) +} + +func (s *DockerSuite) TestContainerApiTop(c *check.C) { + // Problematic on Windows as Windows does not support top + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "top") + id := strings.TrimSpace(string(out)) + c.Assert(waitRun(id), checker.IsNil) + + type topResp struct { + Titles []string + Processes [][]string + } + var top topResp + status, b, err := sockRequest("GET", "/containers/"+id+"/top?ps_args=aux", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusOK) + c.Assert(json.Unmarshal(b, &top), checker.IsNil) + c.Assert(top.Titles, checker.HasLen, 11, check.Commentf("expected 11 titles, found %d: %v", len(top.Titles), top.Titles)) + + if top.Titles[0] != "USER" || top.Titles[10] != "COMMAND" { + c.Fatalf("expected `USER` at `Titles[0]` and `COMMAND` at Titles[10]: %v", top.Titles) + } + c.Assert(top.Processes, checker.HasLen, 2, check.Commentf("expected 2 processes, found %d: %v", len(top.Processes), top.Processes)) + c.Assert(top.Processes[0][10], checker.Equals, "/bin/sh -c top") + c.Assert(top.Processes[1][10], checker.Equals, "top") +} + +func (s *DockerSuite) TestContainerApiCommit(c *check.C) { + cName := "testapicommit" + dockerCmd(c, "run", "--name="+cName, "busybox", "/bin/sh", "-c", "touch /test") + + name := "testcontainerapicommit" + status, b, err := sockRequest("POST", "/commit?repo="+name+"&testtag=tag&container="+cName, nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusCreated) + + type resp struct { + ID string + } + var img resp + c.Assert(json.Unmarshal(b, &img), checker.IsNil) + + cmd := inspectField(c, img.ID, "Config.Cmd") + c.Assert(cmd, checker.Equals, "[/bin/sh -c touch /test]", check.Commentf("got wrong Cmd from commit: %q", cmd)) + + // sanity check, make sure the image is what we think it is + dockerCmd(c, "run", img.ID, "ls", "/test") +} + +func (s *DockerSuite) TestContainerApiCommitWithLabelInConfig(c *check.C) { + cName := "testapicommitwithconfig" + dockerCmd(c, "run", "--name="+cName, "busybox", "/bin/sh", "-c", "touch /test") + + config := map[string]interface{}{ + "Labels": map[string]string{"key1": "value1", "key2": "value2"}, + } + + name := "testcontainerapicommitwithconfig" + status, b, err := sockRequest("POST", "/commit?repo="+name+"&container="+cName, config) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusCreated) + + type resp struct { + ID string + } + var img resp + c.Assert(json.Unmarshal(b, &img), checker.IsNil) + + label1 := inspectFieldMap(c, img.ID, "Config.Labels", "key1") + c.Assert(label1, checker.Equals, "value1") + + label2 := inspectFieldMap(c, img.ID, "Config.Labels", "key2") + c.Assert(label2, checker.Equals, "value2") + + cmd := inspectField(c, img.ID, "Config.Cmd") + c.Assert(cmd, checker.Equals, "[/bin/sh -c touch /test]", check.Commentf("got wrong Cmd from commit: %q", cmd)) + + // sanity check, make sure the image is what we think it is + dockerCmd(c, "run", img.ID, "ls", "/test") +} + +func (s *DockerSuite) TestContainerApiBadPort(c *check.C) { + // TODO Windows to Windows CI - Port this test + testRequires(c, DaemonIsLinux) + config := map[string]interface{}{ + "Image": "busybox", + "Cmd": []string{"/bin/sh", "-c", "echo test"}, + "PortBindings": map[string]interface{}{ + "8080/tcp": []map[string]interface{}{ + { + "HostIP": "", + "HostPort": "aa80", + }, + }, + }, + } + + jsonData := bytes.NewBuffer(nil) + json.NewEncoder(jsonData).Encode(config) + + status, b, err := sockRequest("POST", "/containers/create", config) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusInternalServerError) + c.Assert(strings.TrimSpace(string(b)), checker.Equals, `Invalid port specification: "aa80"`, check.Commentf("Incorrect error msg: %s", string(b))) +} + +func (s *DockerSuite) TestContainerApiCreate(c *check.C) { + config := map[string]interface{}{ + "Image": "busybox", + "Cmd": []string{"/bin/sh", "-c", "touch /test && ls /test"}, + } + + status, b, err := sockRequest("POST", "/containers/create", config) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusCreated) + + type createResp struct { + ID string + } + var container createResp + c.Assert(json.Unmarshal(b, &container), checker.IsNil) + + out, _ := dockerCmd(c, "start", "-a", container.ID) + c.Assert(strings.TrimSpace(out), checker.Equals, "/test") +} + +func (s *DockerSuite) TestContainerApiCreateEmptyConfig(c *check.C) { + config := map[string]interface{}{} + + status, b, err := sockRequest("POST", "/containers/create", config) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusInternalServerError) + + expected := "Config cannot be empty in order to create a container\n" + c.Assert(string(b), checker.Equals, expected) +} + +func (s *DockerSuite) TestContainerApiCreateMultipleNetworksConfig(c *check.C) { + // Container creation must fail if client specified configurations for more than one network + config := map[string]interface{}{ + "Image": "busybox", + "NetworkingConfig": networktypes.NetworkingConfig{ + EndpointsConfig: map[string]*networktypes.EndpointSettings{ + "net1": {}, + "net2": {}, + "net3": {}, + }, + }, + } + + status, b, err := sockRequest("POST", "/containers/create", config) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusBadRequest) + // network name order in error message is not deterministic + c.Assert(string(b), checker.Contains, "Container cannot be connected to network endpoints") + c.Assert(string(b), checker.Contains, "net1") + c.Assert(string(b), checker.Contains, "net2") + c.Assert(string(b), checker.Contains, "net3") +} + +func (s *DockerSuite) TestContainerApiCreateWithHostName(c *check.C) { + // TODO Windows: Port this test once hostname is supported + testRequires(c, DaemonIsLinux) + hostName := "test-host" + config := map[string]interface{}{ + "Image": "busybox", + "Hostname": hostName, + } + + status, body, err := sockRequest("POST", "/containers/create", config) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusCreated) + + var container types.ContainerCreateResponse + c.Assert(json.Unmarshal(body, &container), checker.IsNil) + + status, body, err = sockRequest("GET", "/containers/"+container.ID+"/json", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusOK) + + var containerJSON types.ContainerJSON + c.Assert(json.Unmarshal(body, &containerJSON), checker.IsNil) + c.Assert(containerJSON.Config.Hostname, checker.Equals, hostName, check.Commentf("Mismatched Hostname")) +} + +func (s *DockerSuite) TestContainerApiCreateWithDomainName(c *check.C) { + // TODO Windows: Port this test once domain name is supported + testRequires(c, DaemonIsLinux) + domainName := "test-domain" + config := map[string]interface{}{ + "Image": "busybox", + "Domainname": domainName, + } + + status, body, err := sockRequest("POST", "/containers/create", config) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusCreated) + + var container types.ContainerCreateResponse + c.Assert(json.Unmarshal(body, &container), checker.IsNil) + + status, body, err = sockRequest("GET", "/containers/"+container.ID+"/json", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusOK) + + var containerJSON types.ContainerJSON + c.Assert(json.Unmarshal(body, &containerJSON), checker.IsNil) + c.Assert(containerJSON.Config.Domainname, checker.Equals, domainName, check.Commentf("Mismatched Domainname")) +} + +func (s *DockerSuite) TestContainerApiCreateBridgeNetworkMode(c *check.C) { + // Windows does not support bridge + testRequires(c, DaemonIsLinux) + UtilCreateNetworkMode(c, "bridge") +} + +func (s *DockerSuite) TestContainerApiCreateOtherNetworkModes(c *check.C) { + // Windows does not support these network modes + testRequires(c, DaemonIsLinux, NotUserNamespace) + UtilCreateNetworkMode(c, "host") + UtilCreateNetworkMode(c, "container:web1") +} + +func UtilCreateNetworkMode(c *check.C, networkMode string) { + config := map[string]interface{}{ + "Image": "busybox", + "HostConfig": map[string]interface{}{"NetworkMode": networkMode}, + } + + status, body, err := sockRequest("POST", "/containers/create", config) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusCreated) + + var container types.ContainerCreateResponse + c.Assert(json.Unmarshal(body, &container), checker.IsNil) + + status, body, err = sockRequest("GET", "/containers/"+container.ID+"/json", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusOK) + + var containerJSON types.ContainerJSON + c.Assert(json.Unmarshal(body, &containerJSON), checker.IsNil) + c.Assert(containerJSON.HostConfig.NetworkMode, checker.Equals, containertypes.NetworkMode(networkMode), check.Commentf("Mismatched NetworkMode")) +} + +func (s *DockerSuite) TestContainerApiCreateWithCpuSharesCpuset(c *check.C) { + // TODO Windows to Windows CI. The CpuShares part could be ported. + testRequires(c, DaemonIsLinux) + config := map[string]interface{}{ + "Image": "busybox", + "CpuShares": 512, + "CpusetCpus": "0", + } + + status, body, err := sockRequest("POST", "/containers/create", config) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusCreated) + + var container types.ContainerCreateResponse + c.Assert(json.Unmarshal(body, &container), checker.IsNil) + + status, body, err = sockRequest("GET", "/containers/"+container.ID+"/json", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusOK) + + var containerJSON types.ContainerJSON + + c.Assert(json.Unmarshal(body, &containerJSON), checker.IsNil) + + out := inspectField(c, containerJSON.ID, "HostConfig.CpuShares") + c.Assert(out, checker.Equals, "512") + + outCpuset := inspectField(c, containerJSON.ID, "HostConfig.CpusetCpus") + c.Assert(outCpuset, checker.Equals, "0") +} + +func (s *DockerSuite) TestContainerApiVerifyHeader(c *check.C) { + config := map[string]interface{}{ + "Image": "busybox", + } + + create := func(ct string) (*http.Response, io.ReadCloser, error) { + jsonData := bytes.NewBuffer(nil) + c.Assert(json.NewEncoder(jsonData).Encode(config), checker.IsNil) + return sockRequestRaw("POST", "/containers/create", jsonData, ct) + } + + // Try with no content-type + res, body, err := create("") + c.Assert(err, checker.IsNil) + c.Assert(res.StatusCode, checker.Equals, http.StatusInternalServerError) + body.Close() + + // Try with wrong content-type + res, body, err = create("application/xml") + c.Assert(err, checker.IsNil) + c.Assert(res.StatusCode, checker.Equals, http.StatusInternalServerError) + body.Close() + + // now application/json + res, body, err = create("application/json") + c.Assert(err, checker.IsNil) + c.Assert(res.StatusCode, checker.Equals, http.StatusCreated) + body.Close() +} + +//Issue 14230. daemon should return 500 for invalid port syntax +func (s *DockerSuite) TestContainerApiInvalidPortSyntax(c *check.C) { + config := `{ + "Image": "busybox", + "HostConfig": { + "NetworkMode": "default", + "PortBindings": { + "19039;1230": [ + {} + ] + } + } + }` + + res, body, err := sockRequestRaw("POST", "/containers/create", strings.NewReader(config), "application/json") + c.Assert(err, checker.IsNil) + c.Assert(res.StatusCode, checker.Equals, http.StatusInternalServerError) + + b, err := readBody(body) + c.Assert(err, checker.IsNil) + c.Assert(string(b[:]), checker.Contains, "Invalid port") +} + +// Issue 7941 - test to make sure a "null" in JSON is just ignored. +// W/o this fix a null in JSON would be parsed into a string var as "null" +func (s *DockerSuite) TestContainerApiPostCreateNull(c *check.C) { + // TODO Windows to Windows CI. Bit of this with alternate fields checked + // can probably be ported. + testRequires(c, DaemonIsLinux) + config := `{ + "Hostname":"", + "Domainname":"", + "Memory":0, + "MemorySwap":0, + "CpuShares":0, + "Cpuset":null, + "AttachStdin":true, + "AttachStdout":true, + "AttachStderr":true, + "ExposedPorts":{}, + "Tty":true, + "OpenStdin":true, + "StdinOnce":true, + "Env":[], + "Cmd":"ls", + "Image":"busybox", + "Volumes":{}, + "WorkingDir":"", + "Entrypoint":null, + "NetworkDisabled":false, + "OnBuild":null}` + + res, body, err := sockRequestRaw("POST", "/containers/create", strings.NewReader(config), "application/json") + c.Assert(err, checker.IsNil) + c.Assert(res.StatusCode, checker.Equals, http.StatusCreated) + + b, err := readBody(body) + c.Assert(err, checker.IsNil) + type createResp struct { + ID string + } + var container createResp + c.Assert(json.Unmarshal(b, &container), checker.IsNil) + out := inspectField(c, container.ID, "HostConfig.CpusetCpus") + c.Assert(out, checker.Equals, "") + + outMemory := inspectField(c, container.ID, "HostConfig.Memory") + c.Assert(outMemory, checker.Equals, "0") + outMemorySwap := inspectField(c, container.ID, "HostConfig.MemorySwap") + c.Assert(outMemorySwap, checker.Equals, "0") +} + +func (s *DockerSuite) TestCreateWithTooLowMemoryLimit(c *check.C) { + // TODO Windows: Port once memory is supported + testRequires(c, DaemonIsLinux) + config := `{ + "Image": "busybox", + "Cmd": "ls", + "OpenStdin": true, + "CpuShares": 100, + "Memory": 524287 + }` + + res, body, err := sockRequestRaw("POST", "/containers/create", strings.NewReader(config), "application/json") + c.Assert(err, checker.IsNil) + b, err2 := readBody(body) + c.Assert(err2, checker.IsNil) + + c.Assert(res.StatusCode, checker.Equals, http.StatusInternalServerError) + c.Assert(string(b), checker.Contains, "Minimum memory limit allowed is 4MB") +} + +func (s *DockerSuite) TestStartWithTooLowMemoryLimit(c *check.C) { + // TODO Windows: Port once memory is supported + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "create", "busybox") + + containerID := strings.TrimSpace(out) + + config := `{ + "CpuShares": 100, + "Memory": 524287 + }` + + res, body, err := sockRequestRaw("POST", "/containers/"+containerID+"/start", strings.NewReader(config), "application/json") + c.Assert(err, checker.IsNil) + b, err2 := readBody(body) + c.Assert(err2, checker.IsNil) + c.Assert(res.StatusCode, checker.Equals, http.StatusInternalServerError) + c.Assert(string(b), checker.Contains, "Minimum memory limit allowed is 4MB") +} + +func (s *DockerSuite) TestContainerApiRename(c *check.C) { + // TODO Windows: Enable for TP5. Fails on TP4. + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "--name", "TestContainerApiRename", "-d", "busybox", "sh") + + containerID := strings.TrimSpace(out) + newName := "TestContainerApiRenameNew" + statusCode, _, err := sockRequest("POST", "/containers/"+containerID+"/rename?name="+newName, nil) + c.Assert(err, checker.IsNil) + // 204 No Content is expected, not 200 + c.Assert(statusCode, checker.Equals, http.StatusNoContent) + + name := inspectField(c, containerID, "Name") + c.Assert(name, checker.Equals, "/"+newName, check.Commentf("Failed to rename container")) +} + +func (s *DockerSuite) TestContainerApiKill(c *check.C) { + name := "test-api-kill" + runSleepingContainer(c, "-i", "--name", name) + + status, _, err := sockRequest("POST", "/containers/"+name+"/kill", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusNoContent) + + state := inspectField(c, name, "State.Running") + c.Assert(state, checker.Equals, "false", check.Commentf("got wrong State from container %s: %q", name, state)) +} + +func (s *DockerSuite) TestContainerApiRestart(c *check.C) { + // TODO Windows to Windows CI. This is flaky due to the timing + testRequires(c, DaemonIsLinux) + name := "test-api-restart" + dockerCmd(c, "run", "-di", "--name", name, "busybox", "top") + + status, _, err := sockRequest("POST", "/containers/"+name+"/restart?t=1", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusNoContent) + c.Assert(waitInspect(name, "{{ .State.Restarting }} {{ .State.Running }}", "false true", 5*time.Second), checker.IsNil) +} + +func (s *DockerSuite) TestContainerApiRestartNotimeoutParam(c *check.C) { + // TODO Windows to Windows CI. This is flaky due to the timing + testRequires(c, DaemonIsLinux) + name := "test-api-restart-no-timeout-param" + out, _ := dockerCmd(c, "run", "-di", "--name", name, "busybox", "top") + id := strings.TrimSpace(out) + c.Assert(waitRun(id), checker.IsNil) + + status, _, err := sockRequest("POST", "/containers/"+name+"/restart", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusNoContent) + c.Assert(waitInspect(name, "{{ .State.Restarting }} {{ .State.Running }}", "false true", 5*time.Second), checker.IsNil) +} + +func (s *DockerSuite) TestContainerApiStart(c *check.C) { + name := "testing-start" + config := map[string]interface{}{ + "Image": "busybox", + "Cmd": append([]string{"/bin/sh", "-c"}, defaultSleepCommand...), + "OpenStdin": true, + } + + status, _, err := sockRequest("POST", "/containers/create?name="+name, config) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusCreated) + + conf := make(map[string]interface{}) + status, _, err = sockRequest("POST", "/containers/"+name+"/start", conf) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusNoContent) + + // second call to start should give 304 + status, _, err = sockRequest("POST", "/containers/"+name+"/start", conf) + c.Assert(err, checker.IsNil) + + // TODO(tibor): figure out why this doesn't work on windows + if isLocalDaemon { + c.Assert(status, checker.Equals, http.StatusNotModified) + } +} + +func (s *DockerSuite) TestContainerApiStop(c *check.C) { + name := "test-api-stop" + runSleepingContainer(c, "-i", "--name", name) + + status, _, err := sockRequest("POST", "/containers/"+name+"/stop?t=30", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusNoContent) + c.Assert(waitInspect(name, "{{ .State.Running }}", "false", 60*time.Second), checker.IsNil) + + // second call to start should give 304 + status, _, err = sockRequest("POST", "/containers/"+name+"/stop?t=30", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusNotModified) +} + +func (s *DockerSuite) TestContainerApiWait(c *check.C) { + name := "test-api-wait" + + sleepCmd := "/bin/sleep" + if daemonPlatform == "windows" { + sleepCmd = "sleep" + } + dockerCmd(c, "run", "--name", name, "busybox", sleepCmd, "5") + + status, body, err := sockRequest("POST", "/containers/"+name+"/wait", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusOK) + c.Assert(waitInspect(name, "{{ .State.Running }}", "false", 60*time.Second), checker.IsNil) + + var waitres types.ContainerWaitResponse + c.Assert(json.Unmarshal(body, &waitres), checker.IsNil) + c.Assert(waitres.StatusCode, checker.Equals, 0) +} + +func (s *DockerSuite) TestContainerApiCopy(c *check.C) { + // TODO Windows to Windows CI. This can be ported. + testRequires(c, DaemonIsLinux) + name := "test-container-api-copy" + dockerCmd(c, "run", "--name", name, "busybox", "touch", "/test.txt") + + postData := types.CopyConfig{ + Resource: "/test.txt", + } + + status, body, err := sockRequest("POST", "/containers/"+name+"/copy", postData) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusOK) + + found := false + for tarReader := tar.NewReader(bytes.NewReader(body)); ; { + h, err := tarReader.Next() + if err != nil { + if err == io.EOF { + break + } + c.Fatal(err) + } + if h.Name == "test.txt" { + found = true + break + } + } + c.Assert(found, checker.True) +} + +func (s *DockerSuite) TestContainerApiCopyResourcePathEmpty(c *check.C) { + // TODO Windows to Windows CI. This can be ported. + testRequires(c, DaemonIsLinux) + name := "test-container-api-copy-resource-empty" + dockerCmd(c, "run", "--name", name, "busybox", "touch", "/test.txt") + + postData := types.CopyConfig{ + Resource: "", + } + + status, body, err := sockRequest("POST", "/containers/"+name+"/copy", postData) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusInternalServerError) + c.Assert(string(body), checker.Matches, "Path cannot be empty\n") +} + +func (s *DockerSuite) TestContainerApiCopyResourcePathNotFound(c *check.C) { + // TODO Windows to Windows CI. This can be ported. + testRequires(c, DaemonIsLinux) + name := "test-container-api-copy-resource-not-found" + dockerCmd(c, "run", "--name", name, "busybox") + + postData := types.CopyConfig{ + Resource: "/notexist", + } + + status, body, err := sockRequest("POST", "/containers/"+name+"/copy", postData) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusInternalServerError) + c.Assert(string(body), checker.Matches, "Could not find the file /notexist in container "+name+"\n") +} + +func (s *DockerSuite) TestContainerApiCopyContainerNotFound(c *check.C) { + postData := types.CopyConfig{ + Resource: "/something", + } + + status, _, err := sockRequest("POST", "/containers/notexists/copy", postData) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusNotFound) +} + +func (s *DockerSuite) TestContainerApiDelete(c *check.C) { + out, _ := runSleepingContainer(c) + + id := strings.TrimSpace(out) + c.Assert(waitRun(id), checker.IsNil) + + dockerCmd(c, "stop", id) + + status, _, err := sockRequest("DELETE", "/containers/"+id, nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusNoContent) +} + +func (s *DockerSuite) TestContainerApiDeleteNotExist(c *check.C) { + status, body, err := sockRequest("DELETE", "/containers/doesnotexist", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusNotFound) + c.Assert(string(body), checker.Matches, "No such container: doesnotexist\n") +} + +func (s *DockerSuite) TestContainerApiDeleteForce(c *check.C) { + out, _ := runSleepingContainer(c) + + id := strings.TrimSpace(out) + c.Assert(waitRun(id), checker.IsNil) + + status, _, err := sockRequest("DELETE", "/containers/"+id+"?force=1", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusNoContent) +} + +func (s *DockerSuite) TestContainerApiDeleteRemoveLinks(c *check.C) { + // Windows does not support links + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "--name", "tlink1", "busybox", "top") + + id := strings.TrimSpace(out) + c.Assert(waitRun(id), checker.IsNil) + + out, _ = dockerCmd(c, "run", "--link", "tlink1:tlink1", "--name", "tlink2", "-d", "busybox", "top") + + id2 := strings.TrimSpace(out) + c.Assert(waitRun(id2), checker.IsNil) + + links := inspectFieldJSON(c, id2, "HostConfig.Links") + c.Assert(links, checker.Equals, "[\"/tlink1:/tlink2/tlink1\"]", check.Commentf("expected to have links between containers")) + + status, b, err := sockRequest("DELETE", "/containers/tlink2/tlink1?link=1", nil) + c.Assert(err, check.IsNil) + c.Assert(status, check.Equals, http.StatusNoContent, check.Commentf(string(b))) + + linksPostRm := inspectFieldJSON(c, id2, "HostConfig.Links") + c.Assert(linksPostRm, checker.Equals, "null", check.Commentf("call to api deleteContainer links should have removed the specified links")) +} + +func (s *DockerSuite) TestContainerApiDeleteConflict(c *check.C) { + out, _ := runSleepingContainer(c) + + id := strings.TrimSpace(out) + c.Assert(waitRun(id), checker.IsNil) + + status, _, err := sockRequest("DELETE", "/containers/"+id, nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusConflict) +} + +func (s *DockerSuite) TestContainerApiDeleteRemoveVolume(c *check.C) { + testRequires(c, SameHostDaemon) + + vol := "/testvolume" + if daemonPlatform == "windows" { + vol = `c:\testvolume` + } + + out, _ := runSleepingContainer(c, "-v", vol) + + id := strings.TrimSpace(out) + c.Assert(waitRun(id), checker.IsNil) + + source, err := inspectMountSourceField(id, vol) + _, err = os.Stat(source) + c.Assert(err, checker.IsNil) + + status, _, err := sockRequest("DELETE", "/containers/"+id+"?v=1&force=1", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusNoContent) + _, err = os.Stat(source) + c.Assert(os.IsNotExist(err), checker.True, check.Commentf("expected to get ErrNotExist error, got %v", err)) +} + +// Regression test for https://github.com/docker/docker/issues/6231 +func (s *DockerSuite) TestContainerApiChunkedEncoding(c *check.C) { + // TODO Windows CI: This can be ported + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "create", "-v", "/foo", "busybox", "true") + id := strings.TrimSpace(out) + + conn, err := sockConn(time.Duration(10 * time.Second)) + c.Assert(err, checker.IsNil) + client := httputil.NewClientConn(conn, nil) + defer client.Close() + + bindCfg := strings.NewReader(`{"Binds": ["/tmp:/foo"]}`) + req, err := http.NewRequest("POST", "/containers/"+id+"/start", bindCfg) + c.Assert(err, checker.IsNil) + req.Header.Set("Content-Type", "application/json") + // This is a cheat to make the http request do chunked encoding + // Otherwise (just setting the Content-Encoding to chunked) net/http will overwrite + // https://golang.org/src/pkg/net/http/request.go?s=11980:12172 + req.ContentLength = -1 + + resp, err := client.Do(req) + c.Assert(err, checker.IsNil, check.Commentf("error starting container with chunked encoding")) + resp.Body.Close() + c.Assert(resp.StatusCode, checker.Equals, 204) + + out = inspectFieldJSON(c, id, "HostConfig.Binds") + + var binds []string + c.Assert(json.NewDecoder(strings.NewReader(out)).Decode(&binds), checker.IsNil) + c.Assert(binds, checker.HasLen, 1, check.Commentf("Got unexpected binds: %v", binds)) + + expected := "/tmp:/foo" + c.Assert(binds[0], checker.Equals, expected, check.Commentf("got incorrect bind spec")) +} + +func (s *DockerSuite) TestContainerApiPostContainerStop(c *check.C) { + out, _ := runSleepingContainer(c) + + containerID := strings.TrimSpace(out) + c.Assert(waitRun(containerID), checker.IsNil) + + statusCode, _, err := sockRequest("POST", "/containers/"+containerID+"/stop", nil) + c.Assert(err, checker.IsNil) + // 204 No Content is expected, not 200 + c.Assert(statusCode, checker.Equals, http.StatusNoContent) + c.Assert(waitInspect(containerID, "{{ .State.Running }}", "false", 5*time.Second), checker.IsNil) +} + +// #14170 +func (s *DockerSuite) TestPostContainerApiCreateWithStringOrSliceEntrypoint(c *check.C) { + config := struct { + Image string + Entrypoint string + Cmd []string + }{"busybox", "echo", []string{"hello", "world"}} + _, _, err := sockRequest("POST", "/containers/create?name=echotest", config) + c.Assert(err, checker.IsNil) + out, _ := dockerCmd(c, "start", "-a", "echotest") + c.Assert(strings.TrimSpace(out), checker.Equals, "hello world") + + config2 := struct { + Image string + Entrypoint []string + Cmd []string + }{"busybox", []string{"echo"}, []string{"hello", "world"}} + _, _, err = sockRequest("POST", "/containers/create?name=echotest2", config2) + c.Assert(err, checker.IsNil) + out, _ = dockerCmd(c, "start", "-a", "echotest2") + c.Assert(strings.TrimSpace(out), checker.Equals, "hello world") +} + +// #14170 +func (s *DockerSuite) TestPostContainersCreateWithStringOrSliceCmd(c *check.C) { + config := struct { + Image string + Entrypoint string + Cmd string + }{"busybox", "echo", "hello world"} + _, _, err := sockRequest("POST", "/containers/create?name=echotest", config) + c.Assert(err, checker.IsNil) + out, _ := dockerCmd(c, "start", "-a", "echotest") + c.Assert(strings.TrimSpace(out), checker.Equals, "hello world") + + config2 := struct { + Image string + Cmd []string + }{"busybox", []string{"echo", "hello", "world"}} + _, _, err = sockRequest("POST", "/containers/create?name=echotest2", config2) + c.Assert(err, checker.IsNil) + out, _ = dockerCmd(c, "start", "-a", "echotest2") + c.Assert(strings.TrimSpace(out), checker.Equals, "hello world") +} + +// regression #14318 +func (s *DockerSuite) TestPostContainersCreateWithStringOrSliceCapAddDrop(c *check.C) { + // Windows doesn't support CapAdd/CapDrop + testRequires(c, DaemonIsLinux) + config := struct { + Image string + CapAdd string + CapDrop string + }{"busybox", "NET_ADMIN", "SYS_ADMIN"} + status, _, err := sockRequest("POST", "/containers/create?name=capaddtest0", config) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusCreated) + + config2 := struct { + Image string + CapAdd []string + CapDrop []string + }{"busybox", []string{"NET_ADMIN", "SYS_ADMIN"}, []string{"SETGID"}} + status, _, err = sockRequest("POST", "/containers/create?name=capaddtest1", config2) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusCreated) +} + +// #14640 +func (s *DockerSuite) TestPostContainersStartWithoutLinksInHostConfig(c *check.C) { + // TODO Windows: Windows doesn't support supplying a hostconfig on start. + // An alternate test could be written to validate the negative testing aspect of this + testRequires(c, DaemonIsLinux) + name := "test-host-config-links" + dockerCmd(c, append([]string{"create", "--name", name, "busybox"}, defaultSleepCommand...)...) + + hc := inspectFieldJSON(c, name, "HostConfig") + config := `{"HostConfig":` + hc + `}` + + res, b, err := sockRequestRaw("POST", "/containers/"+name+"/start", strings.NewReader(config), "application/json") + c.Assert(err, checker.IsNil) + c.Assert(res.StatusCode, checker.Equals, http.StatusNoContent) + b.Close() +} + +// #14640 +func (s *DockerSuite) TestPostContainersStartWithLinksInHostConfig(c *check.C) { + // TODO Windows: Windows doesn't support supplying a hostconfig on start. + // An alternate test could be written to validate the negative testing aspect of this + testRequires(c, DaemonIsLinux) + name := "test-host-config-links" + dockerCmd(c, "run", "--name", "foo", "-d", "busybox", "top") + dockerCmd(c, "create", "--name", name, "--link", "foo:bar", "busybox", "top") + + hc := inspectFieldJSON(c, name, "HostConfig") + config := `{"HostConfig":` + hc + `}` + + res, b, err := sockRequestRaw("POST", "/containers/"+name+"/start", strings.NewReader(config), "application/json") + c.Assert(err, checker.IsNil) + c.Assert(res.StatusCode, checker.Equals, http.StatusNoContent) + b.Close() +} + +// #14640 +func (s *DockerSuite) TestPostContainersStartWithLinksInHostConfigIdLinked(c *check.C) { + // Windows does not support links + testRequires(c, DaemonIsLinux) + name := "test-host-config-links" + out, _ := dockerCmd(c, "run", "--name", "link0", "-d", "busybox", "top") + id := strings.TrimSpace(out) + dockerCmd(c, "create", "--name", name, "--link", id, "busybox", "top") + + hc := inspectFieldJSON(c, name, "HostConfig") + config := `{"HostConfig":` + hc + `}` + + res, b, err := sockRequestRaw("POST", "/containers/"+name+"/start", strings.NewReader(config), "application/json") + c.Assert(err, checker.IsNil) + c.Assert(res.StatusCode, checker.Equals, http.StatusNoContent) + b.Close() +} + +// #14915 +func (s *DockerSuite) TestContainerApiCreateNoHostConfig118(c *check.C) { + config := struct { + Image string + }{"busybox"} + status, _, err := sockRequest("POST", "/v1.18/containers/create", config) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusCreated) +} + +// Ensure an error occurs when you have a container read-only rootfs but you +// extract an archive to a symlink in a writable volume which points to a +// directory outside of the volume. +func (s *DockerSuite) TestPutContainerArchiveErrSymlinkInVolumeToReadOnlyRootfs(c *check.C) { + // Windows does not support read-only rootfs + // Requires local volume mount bind. + // --read-only + userns has remount issues + testRequires(c, SameHostDaemon, NotUserNamespace, DaemonIsLinux) + + testVol := getTestDir(c, "test-put-container-archive-err-symlink-in-volume-to-read-only-rootfs-") + defer os.RemoveAll(testVol) + + makeTestContentInDir(c, testVol) + + cID := makeTestContainer(c, testContainerOptions{ + readOnly: true, + volumes: defaultVolumes(testVol), // Our bind mount is at /vol2 + }) + defer deleteContainer(cID) + + // Attempt to extract to a symlink in the volume which points to a + // directory outside the volume. This should cause an error because the + // rootfs is read-only. + query := make(url.Values, 1) + query.Set("path", "/vol2/symlinkToAbsDir") + urlPath := fmt.Sprintf("/v1.20/containers/%s/archive?%s", cID, query.Encode()) + + statusCode, body, err := sockRequest("PUT", urlPath, nil) + c.Assert(err, checker.IsNil) + + if !isCpCannotCopyReadOnly(fmt.Errorf(string(body))) { + c.Fatalf("expected ErrContainerRootfsReadonly error, but got %d: %s", statusCode, string(body)) + } +} + +func (s *DockerSuite) TestContainerApiGetContainersJSONEmpty(c *check.C) { + status, body, err := sockRequest("GET", "/containers/json?all=1", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusOK) + c.Assert(string(body), checker.Equals, "[]\n") +} + +func (s *DockerSuite) TestPostContainersCreateWithWrongCpusetValues(c *check.C) { + // Not supported on Windows + testRequires(c, DaemonIsLinux) + + c1 := struct { + Image string + CpusetCpus string + }{"busybox", "1-42,,"} + name := "wrong-cpuset-cpus" + status, body, err := sockRequest("POST", "/containers/create?name="+name, c1) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusInternalServerError) + expected := "Invalid value 1-42,, for cpuset cpus.\n" + c.Assert(string(body), checker.Equals, expected) + + c2 := struct { + Image string + CpusetMems string + }{"busybox", "42-3,1--"} + name = "wrong-cpuset-mems" + status, body, err = sockRequest("POST", "/containers/create?name="+name, c2) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusInternalServerError) + expected = "Invalid value 42-3,1-- for cpuset mems.\n" + c.Assert(string(body), checker.Equals, expected) +} + +func (s *DockerSuite) TestStartWithNilDNS(c *check.C) { + // TODO Windows: Add once DNS is supported + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "create", "busybox") + containerID := strings.TrimSpace(out) + + config := `{"HostConfig": {"Dns": null}}` + + res, b, err := sockRequestRaw("POST", "/containers/"+containerID+"/start", strings.NewReader(config), "application/json") + c.Assert(err, checker.IsNil) + c.Assert(res.StatusCode, checker.Equals, http.StatusNoContent) + b.Close() + + dns := inspectFieldJSON(c, containerID, "HostConfig.Dns") + c.Assert(dns, checker.Equals, "[]") +} + +func (s *DockerSuite) TestPostContainersCreateShmSizeNegative(c *check.C) { + // ShmSize is not supported on Windows + testRequires(c, DaemonIsLinux) + config := map[string]interface{}{ + "Image": "busybox", + "HostConfig": map[string]interface{}{"ShmSize": -1}, + } + + status, body, err := sockRequest("POST", "/containers/create", config) + c.Assert(err, check.IsNil) + c.Assert(status, check.Equals, http.StatusInternalServerError) + c.Assert(string(body), checker.Contains, "SHM size must be greater then 0") +} + +func (s *DockerSuite) TestPostContainersCreateShmSizeHostConfigOmitted(c *check.C) { + // ShmSize is not supported on Windows + testRequires(c, DaemonIsLinux) + var defaultSHMSize int64 = 67108864 + config := map[string]interface{}{ + "Image": "busybox", + "Cmd": "mount", + } + + status, body, err := sockRequest("POST", "/containers/create", config) + c.Assert(err, check.IsNil) + c.Assert(status, check.Equals, http.StatusCreated) + + var container types.ContainerCreateResponse + c.Assert(json.Unmarshal(body, &container), check.IsNil) + + status, body, err = sockRequest("GET", "/containers/"+container.ID+"/json", nil) + c.Assert(err, check.IsNil) + c.Assert(status, check.Equals, http.StatusOK) + + var containerJSON types.ContainerJSON + c.Assert(json.Unmarshal(body, &containerJSON), check.IsNil) + + c.Assert(containerJSON.HostConfig.ShmSize, check.Equals, defaultSHMSize) + + out, _ := dockerCmd(c, "start", "-i", containerJSON.ID) + shmRegexp := regexp.MustCompile(`shm on /dev/shm type tmpfs(.*)size=65536k`) + if !shmRegexp.MatchString(out) { + c.Fatalf("Expected shm of 64MB in mount command, got %v", out) + } +} + +func (s *DockerSuite) TestPostContainersCreateShmSizeOmitted(c *check.C) { + // ShmSize is not supported on Windows + testRequires(c, DaemonIsLinux) + config := map[string]interface{}{ + "Image": "busybox", + "HostConfig": map[string]interface{}{}, + "Cmd": "mount", + } + + status, body, err := sockRequest("POST", "/containers/create", config) + c.Assert(err, check.IsNil) + c.Assert(status, check.Equals, http.StatusCreated) + + var container types.ContainerCreateResponse + c.Assert(json.Unmarshal(body, &container), check.IsNil) + + status, body, err = sockRequest("GET", "/containers/"+container.ID+"/json", nil) + c.Assert(err, check.IsNil) + c.Assert(status, check.Equals, http.StatusOK) + + var containerJSON types.ContainerJSON + c.Assert(json.Unmarshal(body, &containerJSON), check.IsNil) + + c.Assert(containerJSON.HostConfig.ShmSize, check.Equals, int64(67108864)) + + out, _ := dockerCmd(c, "start", "-i", containerJSON.ID) + shmRegexp := regexp.MustCompile(`shm on /dev/shm type tmpfs(.*)size=65536k`) + if !shmRegexp.MatchString(out) { + c.Fatalf("Expected shm of 64MB in mount command, got %v", out) + } +} + +func (s *DockerSuite) TestPostContainersCreateWithShmSize(c *check.C) { + // ShmSize is not supported on Windows + testRequires(c, DaemonIsLinux) + config := map[string]interface{}{ + "Image": "busybox", + "Cmd": "mount", + "HostConfig": map[string]interface{}{"ShmSize": 1073741824}, + } + + status, body, err := sockRequest("POST", "/containers/create", config) + c.Assert(err, check.IsNil) + c.Assert(status, check.Equals, http.StatusCreated) + + var container types.ContainerCreateResponse + c.Assert(json.Unmarshal(body, &container), check.IsNil) + + status, body, err = sockRequest("GET", "/containers/"+container.ID+"/json", nil) + c.Assert(err, check.IsNil) + c.Assert(status, check.Equals, http.StatusOK) + + var containerJSON types.ContainerJSON + c.Assert(json.Unmarshal(body, &containerJSON), check.IsNil) + + c.Assert(containerJSON.HostConfig.ShmSize, check.Equals, int64(1073741824)) + + out, _ := dockerCmd(c, "start", "-i", containerJSON.ID) + shmRegex := regexp.MustCompile(`shm on /dev/shm type tmpfs(.*)size=1048576k`) + if !shmRegex.MatchString(out) { + c.Fatalf("Expected shm of 1GB in mount command, got %v", out) + } +} + +func (s *DockerSuite) TestPostContainersCreateMemorySwappinessHostConfigOmitted(c *check.C) { + // Swappiness is not supported on Windows + testRequires(c, DaemonIsLinux) + config := map[string]interface{}{ + "Image": "busybox", + } + + status, body, err := sockRequest("POST", "/containers/create", config) + c.Assert(err, check.IsNil) + c.Assert(status, check.Equals, http.StatusCreated) + + var container types.ContainerCreateResponse + c.Assert(json.Unmarshal(body, &container), check.IsNil) + + status, body, err = sockRequest("GET", "/containers/"+container.ID+"/json", nil) + c.Assert(err, check.IsNil) + c.Assert(status, check.Equals, http.StatusOK) + + var containerJSON types.ContainerJSON + c.Assert(json.Unmarshal(body, &containerJSON), check.IsNil) + + c.Assert(*containerJSON.HostConfig.MemorySwappiness, check.Equals, int64(-1)) +} + +// check validation is done daemon side and not only in cli +func (s *DockerSuite) TestPostContainersCreateWithOomScoreAdjInvalidRange(c *check.C) { + // OomScoreAdj is not supported on Windows + testRequires(c, DaemonIsLinux) + + config := struct { + Image string + OomScoreAdj int + }{"busybox", 1001} + name := "oomscoreadj-over" + status, b, err := sockRequest("POST", "/containers/create?name="+name, config) + c.Assert(err, check.IsNil) + c.Assert(status, check.Equals, http.StatusInternalServerError) + expected := "Invalid value 1001, range for oom score adj is [-1000, 1000]." + if !strings.Contains(string(b), expected) { + c.Fatalf("Expected output to contain %q, got %q", expected, string(b)) + } + + config = struct { + Image string + OomScoreAdj int + }{"busybox", -1001} + name = "oomscoreadj-low" + status, b, err = sockRequest("POST", "/containers/create?name="+name, config) + c.Assert(err, check.IsNil) + c.Assert(status, check.Equals, http.StatusInternalServerError) + expected = "Invalid value -1001, range for oom score adj is [-1000, 1000]." + if !strings.Contains(string(b), expected) { + c.Fatalf("Expected output to contain %q, got %q", expected, string(b)) + } +} + +// test case for #22210 where an emtpy container name caused panic. +func (s *DockerSuite) TestContainerApiDeleteWithEmptyName(c *check.C) { + status, out, err := sockRequest("DELETE", "/containers/", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusBadRequest) + c.Assert(string(out), checker.Contains, "No container name or ID supplied") +} diff --git a/integration-cli/docker_api_create_test.go b/integration-cli/docker_api_create_test.go new file mode 100644 index 00000000..d29b3550 --- /dev/null +++ b/integration-cli/docker_api_create_test.go @@ -0,0 +1,45 @@ +package main + +import ( + "net/http" + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestApiCreateWithNotExistImage(c *check.C) { + name := "test" + config := map[string]interface{}{ + "Image": "test456:v1", + "Volumes": map[string]struct{}{"/tmp": {}}, + } + + status, resp, err := sockRequest("POST", "/containers/create?name="+name, config) + c.Assert(err, check.IsNil) + c.Assert(status, check.Equals, http.StatusNotFound) + expected := "No such image: test456:v1" + c.Assert(strings.TrimSpace(string(resp)), checker.Contains, expected) + + config2 := map[string]interface{}{ + "Image": "test456", + "Volumes": map[string]struct{}{"/tmp": {}}, + } + + status, resp, err = sockRequest("POST", "/containers/create?name="+name, config2) + c.Assert(err, check.IsNil) + c.Assert(status, check.Equals, http.StatusNotFound) + expected = "No such image: test456:latest" + c.Assert(strings.TrimSpace(string(resp)), checker.Equals, expected) + + config3 := map[string]interface{}{ + "Image": "sha256:0cb40641836c461bc97c793971d84d758371ed682042457523e4ae701efeaaaa", + } + + status, resp, err = sockRequest("POST", "/containers/create?name="+name, config3) + c.Assert(err, check.IsNil) + c.Assert(status, check.Equals, http.StatusNotFound) + expected = "No such image: sha256:0cb40641836c461bc97c793971d84d758371ed682042457523e4ae701efeaaaa" + c.Assert(strings.TrimSpace(string(resp)), checker.Equals, expected) + +} diff --git a/integration-cli/docker_api_events_test.go b/integration-cli/docker_api_events_test.go new file mode 100644 index 00000000..cb219fbc --- /dev/null +++ b/integration-cli/docker_api_events_test.go @@ -0,0 +1,73 @@ +package main + +import ( + "encoding/json" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/jsonmessage" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestEventsApiEmptyOutput(c *check.C) { + type apiResp struct { + resp *http.Response + err error + } + chResp := make(chan *apiResp) + go func() { + resp, body, err := sockRequestRaw("GET", "/events", nil, "") + body.Close() + chResp <- &apiResp{resp, err} + }() + + select { + case r := <-chResp: + c.Assert(r.err, checker.IsNil) + c.Assert(r.resp.StatusCode, checker.Equals, http.StatusOK) + case <-time.After(3 * time.Second): + c.Fatal("timeout waiting for events api to respond, should have responded immediately") + } +} + +func (s *DockerSuite) TestEventsApiBackwardsCompatible(c *check.C) { + since := daemonTime(c).Unix() + ts := strconv.FormatInt(since, 10) + + out, _ := runSleepingContainer(c, "--name=foo", "-d") + containerID := strings.TrimSpace(out) + c.Assert(waitRun(containerID), checker.IsNil) + + q := url.Values{} + q.Set("since", ts) + + _, body, err := sockRequestRaw("GET", "/events?"+q.Encode(), nil, "") + c.Assert(err, checker.IsNil) + defer body.Close() + + dec := json.NewDecoder(body) + var containerCreateEvent *jsonmessage.JSONMessage + for { + var event jsonmessage.JSONMessage + if err := dec.Decode(&event); err != nil { + if err == io.EOF { + break + } + c.Fatal(err) + } + if event.Status == "create" && event.ID == containerID { + containerCreateEvent = &event + break + } + } + + c.Assert(containerCreateEvent, checker.Not(checker.IsNil)) + c.Assert(containerCreateEvent.Status, checker.Equals, "create") + c.Assert(containerCreateEvent.ID, checker.Equals, containerID) + c.Assert(containerCreateEvent.From, checker.Equals, "busybox") +} diff --git a/integration-cli/docker_api_exec_resize_test.go b/integration-cli/docker_api_exec_resize_test.go new file mode 100644 index 00000000..2c0c8766 --- /dev/null +++ b/integration-cli/docker_api_exec_resize_test.go @@ -0,0 +1,105 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "sync" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestExecResizeApiHeightWidthNoInt(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "top") + cleanedContainerID := strings.TrimSpace(out) + + endpoint := "/exec/" + cleanedContainerID + "/resize?h=foo&w=bar" + status, _, err := sockRequest("POST", endpoint, nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusInternalServerError) +} + +// Part of #14845 +func (s *DockerSuite) TestExecResizeImmediatelyAfterExecStart(c *check.C) { + testRequires(c, DaemonIsLinux) + + name := "exec_resize_test" + dockerCmd(c, "run", "-d", "-i", "-t", "--name", name, "--restart", "always", "busybox", "/bin/sh") + + testExecResize := func() error { + data := map[string]interface{}{ + "AttachStdin": true, + "Cmd": []string{"/bin/sh"}, + } + uri := fmt.Sprintf("/containers/%s/exec", name) + status, body, err := sockRequest("POST", uri, data) + if err != nil { + return err + } + if status != http.StatusCreated { + return fmt.Errorf("POST %s is expected to return %d, got %d", uri, http.StatusCreated, status) + } + + out := map[string]string{} + err = json.Unmarshal(body, &out) + if err != nil { + return fmt.Errorf("ExecCreate returned invalid json. Error: %q", err.Error()) + } + + execID := out["Id"] + if len(execID) < 1 { + return fmt.Errorf("ExecCreate got invalid execID") + } + + payload := bytes.NewBufferString(`{"Tty":true}`) + conn, _, err := sockRequestHijack("POST", fmt.Sprintf("/exec/%s/start", execID), payload, "application/json") + if err != nil { + return fmt.Errorf("Failed to start the exec: %q", err.Error()) + } + defer conn.Close() + + _, rc, err := sockRequestRaw("POST", fmt.Sprintf("/exec/%s/resize?h=24&w=80", execID), nil, "text/plain") + // It's probably a panic of the daemon if io.ErrUnexpectedEOF is returned. + if err == io.ErrUnexpectedEOF { + return fmt.Errorf("The daemon might have crashed.") + } + + if err == nil { + rc.Close() + } + + // We only interested in the io.ErrUnexpectedEOF error, so we return nil otherwise. + return nil + } + + // The panic happens when daemon.ContainerExecStart is called but the + // container.Exec is not called. + // Because the panic is not 100% reproducible, we send the requests concurrently + // to increase the probability that the problem is triggered. + var ( + n = 10 + ch = make(chan error, n) + wg sync.WaitGroup + ) + for i := 0; i < n; i++ { + wg.Add(1) + go func() { + defer wg.Done() + if err := testExecResize(); err != nil { + ch <- err + } + }() + } + + wg.Wait() + select { + case err := <-ch: + c.Fatal(err.Error()) + default: + } +} diff --git a/integration-cli/docker_api_exec_test.go b/integration-cli/docker_api_exec_test.go new file mode 100644 index 00000000..f16582f4 --- /dev/null +++ b/integration-cli/docker_api_exec_test.go @@ -0,0 +1,183 @@ +// +build !test_no_exec + +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +// Regression test for #9414 +func (s *DockerSuite) TestExecApiCreateNoCmd(c *check.C) { + name := "exec_test" + dockerCmd(c, "run", "-d", "-t", "--name", name, "busybox", "/bin/sh") + + status, body, err := sockRequest("POST", fmt.Sprintf("/containers/%s/exec", name), map[string]interface{}{"Cmd": nil}) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusInternalServerError) + + comment := check.Commentf("Expected message when creating exec command with no Cmd specified") + c.Assert(string(body), checker.Contains, "No exec command specified", comment) +} + +func (s *DockerSuite) TestExecApiCreateNoValidContentType(c *check.C) { + name := "exec_test" + dockerCmd(c, "run", "-d", "-t", "--name", name, "busybox", "/bin/sh") + + jsonData := bytes.NewBuffer(nil) + if err := json.NewEncoder(jsonData).Encode(map[string]interface{}{"Cmd": nil}); err != nil { + c.Fatalf("Can not encode data to json %s", err) + } + + res, body, err := sockRequestRaw("POST", fmt.Sprintf("/containers/%s/exec", name), jsonData, "text/plain") + c.Assert(err, checker.IsNil) + c.Assert(res.StatusCode, checker.Equals, http.StatusInternalServerError) + + b, err := readBody(body) + c.Assert(err, checker.IsNil) + + comment := check.Commentf("Expected message when creating exec command with invalid Content-Type specified") + c.Assert(string(b), checker.Contains, "Content-Type specified", comment) +} + +func (s *DockerSuite) TestExecApiCreateContainerPaused(c *check.C) { + // Not relevant on Windows as Windows containers cannot be paused + testRequires(c, DaemonIsLinux) + name := "exec_create_test" + dockerCmd(c, "run", "-d", "-t", "--name", name, "busybox", "/bin/sh") + + dockerCmd(c, "pause", name) + status, body, err := sockRequest("POST", fmt.Sprintf("/containers/%s/exec", name), map[string]interface{}{"Cmd": []string{"true"}}) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusConflict) + + comment := check.Commentf("Expected message when creating exec command with Container %s is paused", name) + c.Assert(string(body), checker.Contains, "Container "+name+" is paused, unpause the container before exec", comment) +} + +func (s *DockerSuite) TestExecApiStart(c *check.C) { + testRequires(c, DaemonIsLinux) // Uses pause/unpause but bits may be salvagable to Windows to Windows CI + dockerCmd(c, "run", "-d", "--name", "test", "busybox", "top") + + id := createExec(c, "test") + startExec(c, id, http.StatusOK) + + id = createExec(c, "test") + dockerCmd(c, "stop", "test") + + startExec(c, id, http.StatusNotFound) + + dockerCmd(c, "start", "test") + startExec(c, id, http.StatusNotFound) + + // make sure exec is created before pausing + id = createExec(c, "test") + dockerCmd(c, "pause", "test") + startExec(c, id, http.StatusConflict) + dockerCmd(c, "unpause", "test") + startExec(c, id, http.StatusOK) +} + +func (s *DockerSuite) TestExecApiStartBackwardsCompatible(c *check.C) { + runSleepingContainer(c, "-d", "--name", "test") + id := createExec(c, "test") + + resp, body, err := sockRequestRaw("POST", fmt.Sprintf("/v1.20/exec/%s/start", id), strings.NewReader(`{"Detach": true}`), "text/plain") + c.Assert(err, checker.IsNil) + + b, err := readBody(body) + comment := check.Commentf("response body: %s", b) + c.Assert(err, checker.IsNil, comment) + c.Assert(resp.StatusCode, checker.Equals, http.StatusOK, comment) +} + +// #19362 +func (s *DockerSuite) TestExecApiStartMultipleTimesError(c *check.C) { + runSleepingContainer(c, "-d", "--name", "test") + execID := createExec(c, "test") + startExec(c, execID, http.StatusOK) + + timeout := time.After(60 * time.Second) + var execJSON struct{ Running bool } + for { + select { + case <-timeout: + c.Fatal("timeout waiting for exec to start") + default: + } + + inspectExec(c, execID, &execJSON) + if !execJSON.Running { + break + } + } + + startExec(c, execID, http.StatusConflict) +} + +// #20638 +func (s *DockerSuite) TestExecApiStartWithDetach(c *check.C) { + name := "foo" + runSleepingContainer(c, "-d", "-t", "--name", name) + data := map[string]interface{}{ + "cmd": []string{"true"}, + "AttachStdin": true, + } + _, b, err := sockRequest("POST", fmt.Sprintf("/containers/%s/exec", name), data) + c.Assert(err, checker.IsNil, check.Commentf(string(b))) + + createResp := struct { + ID string `json:"Id"` + }{} + c.Assert(json.Unmarshal(b, &createResp), checker.IsNil, check.Commentf(string(b))) + + _, body, err := sockRequestRaw("POST", fmt.Sprintf("/exec/%s/start", createResp.ID), strings.NewReader(`{"Detach": true}`), "application/json") + c.Assert(err, checker.IsNil) + + b, err = readBody(body) + comment := check.Commentf("response body: %s", b) + c.Assert(err, checker.IsNil, comment) + + resp, _, err := sockRequestRaw("GET", "/_ping", nil, "") + c.Assert(err, checker.IsNil) + if resp.StatusCode != http.StatusOK { + c.Fatal("daemon is down, it should alive") + } +} + +func createExec(c *check.C, name string) string { + _, b, err := sockRequest("POST", fmt.Sprintf("/containers/%s/exec", name), map[string]interface{}{"Cmd": []string{"true"}}) + c.Assert(err, checker.IsNil, check.Commentf(string(b))) + + createResp := struct { + ID string `json:"Id"` + }{} + c.Assert(json.Unmarshal(b, &createResp), checker.IsNil, check.Commentf(string(b))) + return createResp.ID +} + +func startExec(c *check.C, id string, code int) { + resp, body, err := sockRequestRaw("POST", fmt.Sprintf("/exec/%s/start", id), strings.NewReader(`{"Detach": true}`), "application/json") + c.Assert(err, checker.IsNil) + + b, err := readBody(body) + comment := check.Commentf("response body: %s", b) + c.Assert(err, checker.IsNil, comment) + c.Assert(resp.StatusCode, checker.Equals, code, comment) +} + +func inspectExec(c *check.C, id string, out interface{}) { + resp, body, err := sockRequestRaw("GET", fmt.Sprintf("/exec/%s/json", id), nil, "") + c.Assert(err, checker.IsNil) + defer body.Close() + c.Assert(resp.StatusCode, checker.Equals, http.StatusOK) + err = json.NewDecoder(body).Decode(out) + c.Assert(err, checker.IsNil) +} diff --git a/integration-cli/docker_api_images_test.go b/integration-cli/docker_api_images_test.go new file mode 100644 index 00000000..9d35b0c9 --- /dev/null +++ b/integration-cli/docker_api_images_test.go @@ -0,0 +1,129 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/url" + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/engine-api/types" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestApiImagesFilter(c *check.C) { + name := "utest:tag1" + name2 := "utest/docker:tag2" + name3 := "utest:5000/docker:tag3" + for _, n := range []string{name, name2, name3} { + dockerCmd(c, "tag", "busybox", n) + } + type image types.Image + getImages := func(filter string) []image { + v := url.Values{} + v.Set("filter", filter) + status, b, err := sockRequest("GET", "/images/json?"+v.Encode(), nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusOK) + + var images []image + err = json.Unmarshal(b, &images) + c.Assert(err, checker.IsNil) + + return images + } + + //incorrect number of matches returned + images := getImages("utest*/*") + c.Assert(images[0].RepoTags, checker.HasLen, 2) + + images = getImages("utest") + c.Assert(images[0].RepoTags, checker.HasLen, 1) + + images = getImages("utest*") + c.Assert(images[0].RepoTags, checker.HasLen, 1) + + images = getImages("*5000*/*") + c.Assert(images[0].RepoTags, checker.HasLen, 1) +} + +func (s *DockerSuite) TestApiImagesSaveAndLoad(c *check.C) { + // TODO Windows to Windows CI: Investigate further why this test fails. + testRequires(c, Network) + testRequires(c, DaemonIsLinux) + out, err := buildImage("saveandload", "FROM busybox\nENV FOO bar", false) + c.Assert(err, checker.IsNil) + id := strings.TrimSpace(out) + + res, body, err := sockRequestRaw("GET", "/images/"+id+"/get", nil, "") + c.Assert(err, checker.IsNil) + defer body.Close() + c.Assert(res.StatusCode, checker.Equals, http.StatusOK) + + dockerCmd(c, "rmi", id) + + res, loadBody, err := sockRequestRaw("POST", "/images/load", body, "application/x-tar") + c.Assert(err, checker.IsNil) + defer loadBody.Close() + c.Assert(res.StatusCode, checker.Equals, http.StatusOK) + + inspectOut := inspectField(c, id, "Id") + c.Assert(strings.TrimSpace(string(inspectOut)), checker.Equals, id, check.Commentf("load did not work properly")) +} + +func (s *DockerSuite) TestApiImagesDelete(c *check.C) { + if daemonPlatform != "windows" { + testRequires(c, Network) + } + name := "test-api-images-delete" + out, err := buildImage(name, "FROM busybox\nENV FOO bar", false) + c.Assert(err, checker.IsNil) + id := strings.TrimSpace(out) + + dockerCmd(c, "tag", name, "test:tag1") + + status, _, err := sockRequest("DELETE", "/images/"+id, nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusConflict) + + status, _, err = sockRequest("DELETE", "/images/test:noexist", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusNotFound) //Status Codes:404 – no such image + + status, _, err = sockRequest("DELETE", "/images/test:tag1", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusOK) +} + +func (s *DockerSuite) TestApiImagesHistory(c *check.C) { + if daemonPlatform != "windows" { + testRequires(c, Network) + } + name := "test-api-images-history" + out, err := buildImage(name, "FROM busybox\nENV FOO bar", false) + c.Assert(err, checker.IsNil) + + id := strings.TrimSpace(out) + + status, body, err := sockRequest("GET", "/images/"+id+"/history", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusOK) + + var historydata []types.ImageHistory + err = json.Unmarshal(body, &historydata) + c.Assert(err, checker.IsNil, check.Commentf("Error on unmarshal")) + + c.Assert(historydata, checker.Not(checker.HasLen), 0) + c.Assert(historydata[0].Tags[0], checker.Equals, "test-api-images-history:latest") +} + +// #14846 +func (s *DockerSuite) TestApiImagesSearchJSONContentType(c *check.C) { + testRequires(c, Network) + + res, b, err := sockRequestRaw("GET", "/images/search?term=test", nil, "application/json") + c.Assert(err, check.IsNil) + b.Close() + c.Assert(res.StatusCode, checker.Equals, http.StatusOK) + c.Assert(res.Header.Get("Content-Type"), checker.Equals, "application/json") +} diff --git a/integration-cli/docker_api_info_test.go b/integration-cli/docker_api_info_test.go new file mode 100644 index 00000000..9e6af66e --- /dev/null +++ b/integration-cli/docker_api_info_test.go @@ -0,0 +1,40 @@ +package main + +import ( + "net/http" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestInfoApi(c *check.C) { + endpoint := "/info" + + status, body, err := sockRequest("GET", endpoint, nil) + c.Assert(status, checker.Equals, http.StatusOK) + c.Assert(err, checker.IsNil) + + // always shown fields + stringsToCheck := []string{ + "ID", + "Containers", + "ContainersRunning", + "ContainersPaused", + "ContainersStopped", + "Images", + "ExecutionDriver", + "LoggingDriver", + "OperatingSystem", + "NCPU", + "OSType", + "Architecture", + "MemTotal", + "KernelVersion", + "Driver", + "ServerVersion"} + + out := string(body) + for _, linePrefix := range stringsToCheck { + c.Assert(out, checker.Contains, linePrefix) + } +} diff --git a/integration-cli/docker_api_inspect_test.go b/integration-cli/docker_api_inspect_test.go new file mode 100644 index 00000000..6b55159a --- /dev/null +++ b/integration-cli/docker_api_inspect_test.go @@ -0,0 +1,183 @@ +package main + +import ( + "encoding/json" + "net/http" + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/stringutils" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/versions/v1p20" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestInspectApiContainerResponse(c *check.C) { + out, _ := dockerCmd(c, "run", "-d", "busybox", "true") + + cleanedContainerID := strings.TrimSpace(out) + keysBase := []string{"Id", "State", "Created", "Path", "Args", "Config", "Image", "NetworkSettings", + "ResolvConfPath", "HostnamePath", "HostsPath", "LogPath", "Name", "Driver", "MountLabel", "ProcessLabel", "GraphDriver"} + + type acase struct { + version string + keys []string + } + + var cases []acase + + if daemonPlatform == "windows" { + cases = []acase{ + {"v1.20", append(keysBase, "Mounts")}, + } + + } else { + cases = []acase{ + {"v1.20", append(keysBase, "Mounts")}, + {"v1.19", append(keysBase, "Volumes", "VolumesRW")}, + } + } + + for _, cs := range cases { + body := getInspectBody(c, cs.version, cleanedContainerID) + + var inspectJSON map[string]interface{} + err := json.Unmarshal(body, &inspectJSON) + c.Assert(err, checker.IsNil, check.Commentf("Unable to unmarshal body for version %s", cs.version)) + + for _, key := range cs.keys { + _, ok := inspectJSON[key] + c.Check(ok, checker.True, check.Commentf("%s does not exist in response for version %s", key, cs.version)) + } + + //Issue #6830: type not properly converted to JSON/back + _, ok := inspectJSON["Path"].(bool) + c.Assert(ok, checker.False, check.Commentf("Path of `true` should not be converted to boolean `true` via JSON marshalling")) + } +} + +func (s *DockerSuite) TestInspectApiContainerVolumeDriverLegacy(c *check.C) { + // No legacy implications for Windows + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "true") + + cleanedContainerID := strings.TrimSpace(out) + + cases := []string{"v1.19", "v1.20"} + for _, version := range cases { + body := getInspectBody(c, version, cleanedContainerID) + + var inspectJSON map[string]interface{} + err := json.Unmarshal(body, &inspectJSON) + c.Assert(err, checker.IsNil, check.Commentf("Unable to unmarshal body for version %s", version)) + + config, ok := inspectJSON["Config"] + c.Assert(ok, checker.True, check.Commentf("Unable to find 'Config'")) + cfg := config.(map[string]interface{}) + _, ok = cfg["VolumeDriver"] + c.Assert(ok, checker.True, check.Commentf("Api version %s expected to include VolumeDriver in 'Config'", version)) + } +} + +func (s *DockerSuite) TestInspectApiContainerVolumeDriver(c *check.C) { + out, _ := dockerCmd(c, "run", "-d", "--volume-driver", "local", "busybox", "true") + + cleanedContainerID := strings.TrimSpace(out) + + body := getInspectBody(c, "v1.21", cleanedContainerID) + + var inspectJSON map[string]interface{} + err := json.Unmarshal(body, &inspectJSON) + c.Assert(err, checker.IsNil, check.Commentf("Unable to unmarshal body for version 1.21")) + + config, ok := inspectJSON["Config"] + c.Assert(ok, checker.True, check.Commentf("Unable to find 'Config'")) + cfg := config.(map[string]interface{}) + _, ok = cfg["VolumeDriver"] + c.Assert(ok, checker.False, check.Commentf("Api version 1.21 expected to not include VolumeDriver in 'Config'")) + + config, ok = inspectJSON["HostConfig"] + c.Assert(ok, checker.True, check.Commentf("Unable to find 'Config'")) + cfg = config.(map[string]interface{}) + _, ok = cfg["VolumeDriver"] + c.Assert(ok, checker.True, check.Commentf("Api version 1.21 expected to include VolumeDriver in 'HostConfig'")) +} + +func (s *DockerSuite) TestInspectApiImageResponse(c *check.C) { + dockerCmd(c, "tag", "busybox:latest", "busybox:mytag") + + endpoint := "/images/busybox/json" + status, body, err := sockRequest("GET", endpoint, nil) + + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusOK) + + var imageJSON types.ImageInspect + err = json.Unmarshal(body, &imageJSON) + c.Assert(err, checker.IsNil, check.Commentf("Unable to unmarshal body for latest version")) + c.Assert(imageJSON.RepoTags, checker.HasLen, 2) + + c.Assert(stringutils.InSlice(imageJSON.RepoTags, "busybox:latest"), checker.Equals, true) + c.Assert(stringutils.InSlice(imageJSON.RepoTags, "busybox:mytag"), checker.Equals, true) +} + +// #17131, #17139, #17173 +func (s *DockerSuite) TestInspectApiEmptyFieldsInConfigPre121(c *check.C) { + // Not relevant on Windows + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "true") + + cleanedContainerID := strings.TrimSpace(out) + + cases := []string{"v1.19", "v1.20"} + for _, version := range cases { + body := getInspectBody(c, version, cleanedContainerID) + + var inspectJSON map[string]interface{} + err := json.Unmarshal(body, &inspectJSON) + c.Assert(err, checker.IsNil, check.Commentf("Unable to unmarshal body for version %s", version)) + config, ok := inspectJSON["Config"] + c.Assert(ok, checker.True, check.Commentf("Unable to find 'Config'")) + cfg := config.(map[string]interface{}) + for _, f := range []string{"MacAddress", "NetworkDisabled", "ExposedPorts"} { + _, ok := cfg[f] + c.Check(ok, checker.True, check.Commentf("Api version %s expected to include %s in 'Config'", version, f)) + } + } +} + +func (s *DockerSuite) TestInspectApiBridgeNetworkSettings120(c *check.C) { + // Not relevant on Windows, and besides it doesn't have any bridge network settings + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "top") + containerID := strings.TrimSpace(out) + waitRun(containerID) + + body := getInspectBody(c, "v1.20", containerID) + + var inspectJSON v1p20.ContainerJSON + err := json.Unmarshal(body, &inspectJSON) + c.Assert(err, checker.IsNil) + + settings := inspectJSON.NetworkSettings + c.Assert(settings.IPAddress, checker.Not(checker.HasLen), 0) +} + +func (s *DockerSuite) TestInspectApiBridgeNetworkSettings121(c *check.C) { + // Windows doesn't have any bridge network settings + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "top") + containerID := strings.TrimSpace(out) + waitRun(containerID) + + body := getInspectBody(c, "v1.21", containerID) + + var inspectJSON types.ContainerJSON + err := json.Unmarshal(body, &inspectJSON) + c.Assert(err, checker.IsNil) + + settings := inspectJSON.NetworkSettings + c.Assert(settings.IPAddress, checker.Not(checker.HasLen), 0) + c.Assert(settings.Networks["bridge"], checker.Not(checker.IsNil)) + c.Assert(settings.IPAddress, checker.Equals, settings.Networks["bridge"].IPAddress) +} diff --git a/integration-cli/docker_api_inspect_unix_test.go b/integration-cli/docker_api_inspect_unix_test.go new file mode 100644 index 00000000..fe59860d --- /dev/null +++ b/integration-cli/docker_api_inspect_unix_test.go @@ -0,0 +1,35 @@ +// +build !windows + +package main + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +// #16665 +func (s *DockerSuite) TestInspectApiCpusetInConfigPre120(c *check.C) { + testRequires(c, DaemonIsLinux) + testRequires(c, cgroupCpuset) + + name := "cpusetinconfig-pre120" + dockerCmd(c, "run", "--name", name, "--cpuset-cpus", "0", "busybox", "true") + + status, body, err := sockRequest("GET", fmt.Sprintf("/v1.19/containers/%s/json", name), nil) + c.Assert(status, check.Equals, http.StatusOK) + c.Assert(err, check.IsNil) + + var inspectJSON map[string]interface{} + err = json.Unmarshal(body, &inspectJSON) + c.Assert(err, checker.IsNil, check.Commentf("unable to unmarshal body for version 1.19")) + + config, ok := inspectJSON["Config"] + c.Assert(ok, checker.True, check.Commentf("Unable to find 'Config'")) + cfg := config.(map[string]interface{}) + _, ok = cfg["Cpuset"] + c.Assert(ok, checker.True, check.Commentf("Api version 1.19 expected to include Cpuset in 'Config'")) +} diff --git a/integration-cli/docker_api_logs_test.go b/integration-cli/docker_api_logs_test.go new file mode 100644 index 00000000..2ff27f8e --- /dev/null +++ b/integration-cli/docker_api_logs_test.go @@ -0,0 +1,89 @@ +package main + +import ( + "bufio" + "bytes" + "fmt" + "net/http" + "strings" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestLogsApiWithStdout(c *check.C) { + out, _ := dockerCmd(c, "run", "-d", "-t", "busybox", "/bin/sh", "-c", "while true; do echo hello; sleep 1; done") + id := strings.TrimSpace(out) + c.Assert(waitRun(id), checker.IsNil) + + type logOut struct { + out string + res *http.Response + err error + } + chLog := make(chan logOut) + + go func() { + res, body, err := sockRequestRaw("GET", fmt.Sprintf("/containers/%s/logs?follow=1&stdout=1×tamps=1", id), nil, "") + if err != nil { + chLog <- logOut{"", nil, err} + return + } + defer body.Close() + out, err := bufio.NewReader(body).ReadString('\n') + if err != nil { + chLog <- logOut{"", nil, err} + return + } + chLog <- logOut{strings.TrimSpace(out), res, err} + }() + + select { + case l := <-chLog: + c.Assert(l.err, checker.IsNil) + c.Assert(l.res.StatusCode, checker.Equals, http.StatusOK) + if !strings.HasSuffix(l.out, "hello") { + c.Fatalf("expected log output to container 'hello', but it does not") + } + case <-time.After(20 * time.Second): + c.Fatal("timeout waiting for logs to exit") + } +} + +func (s *DockerSuite) TestLogsApiNoStdoutNorStderr(c *check.C) { + name := "logs_test" + dockerCmd(c, "run", "-d", "-t", "--name", name, "busybox", "/bin/sh") + + status, body, err := sockRequest("GET", fmt.Sprintf("/containers/%s/logs", name), nil) + c.Assert(status, checker.Equals, http.StatusBadRequest) + c.Assert(err, checker.IsNil) + + expected := "Bad parameters: you must choose at least one stream" + if !bytes.Contains(body, []byte(expected)) { + c.Fatalf("Expected %s, got %s", expected, string(body[:])) + } +} + +// Regression test for #12704 +func (s *DockerSuite) TestLogsApiFollowEmptyOutput(c *check.C) { + name := "logs_test" + t0 := time.Now() + dockerCmd(c, "run", "-d", "-t", "--name", name, "busybox", "sleep", "10") + + _, body, err := sockRequestRaw("GET", fmt.Sprintf("/containers/%s/logs?follow=1&stdout=1&stderr=1&tail=all", name), bytes.NewBuffer(nil), "") + t1 := time.Now() + c.Assert(err, checker.IsNil) + body.Close() + elapsed := t1.Sub(t0).Seconds() + if elapsed > 20.0 { + c.Fatalf("HTTP response was not immediate (elapsed %.1fs)", elapsed) + } +} + +func (s *DockerSuite) TestLogsApiContainerNotFound(c *check.C) { + name := "nonExistentContainer" + resp, _, err := sockRequestRaw("GET", fmt.Sprintf("/containers/%s/logs?follow=1&stdout=1&stderr=1&tail=all", name), bytes.NewBuffer(nil), "") + c.Assert(err, checker.IsNil) + c.Assert(resp.StatusCode, checker.Equals, http.StatusNotFound) +} diff --git a/integration-cli/docker_api_network_test.go b/integration-cli/docker_api_network_test.go new file mode 100644 index 00000000..e65c7b5e --- /dev/null +++ b/integration-cli/docker_api_network_test.go @@ -0,0 +1,337 @@ +package main + +import ( + "encoding/json" + "fmt" + "net" + "net/http" + "net/url" + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/filters" + "github.com/docker/engine-api/types/network" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestApiNetworkGetDefaults(c *check.C) { + testRequires(c, DaemonIsLinux) + // By default docker daemon creates 3 networks. check if they are present + defaults := []string{"bridge", "host", "none"} + for _, nn := range defaults { + c.Assert(isNetworkAvailable(c, nn), checker.Equals, true) + } +} + +func (s *DockerSuite) TestApiNetworkCreateDelete(c *check.C) { + testRequires(c, DaemonIsLinux) + // Create a network + name := "testnetwork" + config := types.NetworkCreate{ + Name: name, + CheckDuplicate: true, + } + id := createNetwork(c, config, true) + c.Assert(isNetworkAvailable(c, name), checker.Equals, true) + + // delete the network and make sure it is deleted + deleteNetwork(c, id, true) + c.Assert(isNetworkAvailable(c, name), checker.Equals, false) +} + +func (s *DockerSuite) TestApiNetworkCreateCheckDuplicate(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "testcheckduplicate" + configOnCheck := types.NetworkCreate{ + Name: name, + CheckDuplicate: true, + } + configNotCheck := types.NetworkCreate{ + Name: name, + CheckDuplicate: false, + } + + // Creating a new network first + createNetwork(c, configOnCheck, true) + c.Assert(isNetworkAvailable(c, name), checker.Equals, true) + + // Creating another network with same name and CheckDuplicate must fail + createNetwork(c, configOnCheck, false) + + // Creating another network with same name and not CheckDuplicate must succeed + createNetwork(c, configNotCheck, true) +} + +func (s *DockerSuite) TestApiNetworkFilter(c *check.C) { + testRequires(c, DaemonIsLinux) + nr := getNetworkResource(c, getNetworkIDByName(c, "bridge")) + c.Assert(nr.Name, checker.Equals, "bridge") +} + +func (s *DockerSuite) TestApiNetworkInspect(c *check.C) { + testRequires(c, DaemonIsLinux) + // Inspect default bridge network + nr := getNetworkResource(c, "bridge") + c.Assert(nr.Name, checker.Equals, "bridge") + + // run a container and attach it to the default bridge network + out, _ := dockerCmd(c, "run", "-d", "--name", "test", "busybox", "top") + containerID := strings.TrimSpace(out) + containerIP := findContainerIP(c, "test", "bridge") + + // inspect default bridge network again and make sure the container is connected + nr = getNetworkResource(c, nr.ID) + c.Assert(nr.Driver, checker.Equals, "bridge") + c.Assert(nr.Scope, checker.Equals, "local") + c.Assert(nr.Internal, checker.Equals, false) + c.Assert(nr.EnableIPv6, checker.Equals, false) + c.Assert(nr.IPAM.Driver, checker.Equals, "default") + c.Assert(len(nr.Containers), checker.Equals, 1) + c.Assert(nr.Containers[containerID], checker.NotNil) + + ip, _, err := net.ParseCIDR(nr.Containers[containerID].IPv4Address) + c.Assert(err, checker.IsNil) + c.Assert(ip.String(), checker.Equals, containerIP) + + // IPAM configuration inspect + ipam := network.IPAM{ + Driver: "default", + Config: []network.IPAMConfig{{Subnet: "172.28.0.0/16", IPRange: "172.28.5.0/24", Gateway: "172.28.5.254"}}, + } + config := types.NetworkCreate{ + Name: "br0", + Driver: "bridge", + IPAM: ipam, + Options: map[string]string{"foo": "bar", "opts": "dopts"}, + } + id0 := createNetwork(c, config, true) + c.Assert(isNetworkAvailable(c, "br0"), checker.Equals, true) + + nr = getNetworkResource(c, id0) + c.Assert(len(nr.IPAM.Config), checker.Equals, 1) + c.Assert(nr.IPAM.Config[0].Subnet, checker.Equals, "172.28.0.0/16") + c.Assert(nr.IPAM.Config[0].IPRange, checker.Equals, "172.28.5.0/24") + c.Assert(nr.IPAM.Config[0].Gateway, checker.Equals, "172.28.5.254") + c.Assert(nr.Options["foo"], checker.Equals, "bar") + c.Assert(nr.Options["opts"], checker.Equals, "dopts") + + // delete the network and make sure it is deleted + deleteNetwork(c, id0, true) + c.Assert(isNetworkAvailable(c, "br0"), checker.Equals, false) +} + +func (s *DockerSuite) TestApiNetworkConnectDisconnect(c *check.C) { + testRequires(c, DaemonIsLinux) + // Create test network + name := "testnetwork" + config := types.NetworkCreate{ + Name: name, + } + id := createNetwork(c, config, true) + nr := getNetworkResource(c, id) + c.Assert(nr.Name, checker.Equals, name) + c.Assert(nr.ID, checker.Equals, id) + c.Assert(len(nr.Containers), checker.Equals, 0) + + // run a container + out, _ := dockerCmd(c, "run", "-d", "--name", "test", "busybox", "top") + containerID := strings.TrimSpace(out) + + // connect the container to the test network + connectNetwork(c, nr.ID, containerID) + + // inspect the network to make sure container is connected + nr = getNetworkResource(c, nr.ID) + c.Assert(len(nr.Containers), checker.Equals, 1) + c.Assert(nr.Containers[containerID], checker.NotNil) + + // check if container IP matches network inspect + ip, _, err := net.ParseCIDR(nr.Containers[containerID].IPv4Address) + c.Assert(err, checker.IsNil) + containerIP := findContainerIP(c, "test", "testnetwork") + c.Assert(ip.String(), checker.Equals, containerIP) + + // disconnect container from the network + disconnectNetwork(c, nr.ID, containerID) + nr = getNetworkResource(c, nr.ID) + c.Assert(nr.Name, checker.Equals, name) + c.Assert(len(nr.Containers), checker.Equals, 0) + + // delete the network + deleteNetwork(c, nr.ID, true) +} + +func (s *DockerSuite) TestApiNetworkIpamMultipleBridgeNetworks(c *check.C) { + testRequires(c, DaemonIsLinux) + // test0 bridge network + ipam0 := network.IPAM{ + Driver: "default", + Config: []network.IPAMConfig{{Subnet: "192.178.0.0/16", IPRange: "192.178.128.0/17", Gateway: "192.178.138.100"}}, + } + config0 := types.NetworkCreate{ + Name: "test0", + Driver: "bridge", + IPAM: ipam0, + } + id0 := createNetwork(c, config0, true) + c.Assert(isNetworkAvailable(c, "test0"), checker.Equals, true) + + ipam1 := network.IPAM{ + Driver: "default", + Config: []network.IPAMConfig{{Subnet: "192.178.128.0/17", Gateway: "192.178.128.1"}}, + } + // test1 bridge network overlaps with test0 + config1 := types.NetworkCreate{ + Name: "test1", + Driver: "bridge", + IPAM: ipam1, + } + createNetwork(c, config1, false) + c.Assert(isNetworkAvailable(c, "test1"), checker.Equals, false) + + ipam2 := network.IPAM{ + Driver: "default", + Config: []network.IPAMConfig{{Subnet: "192.169.0.0/16", Gateway: "192.169.100.100"}}, + } + // test2 bridge network does not overlap + config2 := types.NetworkCreate{ + Name: "test2", + Driver: "bridge", + IPAM: ipam2, + } + createNetwork(c, config2, true) + c.Assert(isNetworkAvailable(c, "test2"), checker.Equals, true) + + // remove test0 and retry to create test1 + deleteNetwork(c, id0, true) + createNetwork(c, config1, true) + c.Assert(isNetworkAvailable(c, "test1"), checker.Equals, true) + + // for networks w/o ipam specified, docker will choose proper non-overlapping subnets + createNetwork(c, types.NetworkCreate{Name: "test3"}, true) + c.Assert(isNetworkAvailable(c, "test3"), checker.Equals, true) + createNetwork(c, types.NetworkCreate{Name: "test4"}, true) + c.Assert(isNetworkAvailable(c, "test4"), checker.Equals, true) + createNetwork(c, types.NetworkCreate{Name: "test5"}, true) + c.Assert(isNetworkAvailable(c, "test5"), checker.Equals, true) + + for i := 1; i < 6; i++ { + deleteNetwork(c, fmt.Sprintf("test%d", i), true) + } +} + +func (s *DockerSuite) TestApiCreateDeletePredefinedNetworks(c *check.C) { + testRequires(c, DaemonIsLinux) + createDeletePredefinedNetwork(c, "bridge") + createDeletePredefinedNetwork(c, "none") + createDeletePredefinedNetwork(c, "host") +} + +func createDeletePredefinedNetwork(c *check.C, name string) { + // Create pre-defined network + config := types.NetworkCreate{ + Name: name, + CheckDuplicate: true, + } + shouldSucceed := false + createNetwork(c, config, shouldSucceed) + deleteNetwork(c, name, shouldSucceed) +} + +func isNetworkAvailable(c *check.C, name string) bool { + status, body, err := sockRequest("GET", "/networks", nil) + c.Assert(status, checker.Equals, http.StatusOK) + c.Assert(err, checker.IsNil) + + nJSON := []types.NetworkResource{} + err = json.Unmarshal(body, &nJSON) + c.Assert(err, checker.IsNil) + + for _, n := range nJSON { + if n.Name == name { + return true + } + } + return false +} + +func getNetworkIDByName(c *check.C, name string) string { + var ( + v = url.Values{} + filterArgs = filters.NewArgs() + ) + filterArgs.Add("name", name) + filterJSON, err := filters.ToParam(filterArgs) + c.Assert(err, checker.IsNil) + v.Set("filters", filterJSON) + + status, body, err := sockRequest("GET", "/networks?"+v.Encode(), nil) + c.Assert(status, checker.Equals, http.StatusOK) + c.Assert(err, checker.IsNil) + + nJSON := []types.NetworkResource{} + err = json.Unmarshal(body, &nJSON) + c.Assert(err, checker.IsNil) + c.Assert(len(nJSON), checker.Equals, 1) + + return nJSON[0].ID +} + +func getNetworkResource(c *check.C, id string) *types.NetworkResource { + _, obj, err := sockRequest("GET", "/networks/"+id, nil) + c.Assert(err, checker.IsNil) + + nr := types.NetworkResource{} + err = json.Unmarshal(obj, &nr) + c.Assert(err, checker.IsNil) + + return &nr +} + +func createNetwork(c *check.C, config types.NetworkCreate, shouldSucceed bool) string { + status, resp, err := sockRequest("POST", "/networks/create", config) + if !shouldSucceed { + c.Assert(status, checker.Not(checker.Equals), http.StatusCreated) + return "" + } + + c.Assert(status, checker.Equals, http.StatusCreated) + c.Assert(err, checker.IsNil) + + var nr types.NetworkCreateResponse + err = json.Unmarshal(resp, &nr) + c.Assert(err, checker.IsNil) + + return nr.ID +} + +func connectNetwork(c *check.C, nid, cid string) { + config := types.NetworkConnect{ + Container: cid, + } + + status, _, err := sockRequest("POST", "/networks/"+nid+"/connect", config) + c.Assert(status, checker.Equals, http.StatusOK) + c.Assert(err, checker.IsNil) +} + +func disconnectNetwork(c *check.C, nid, cid string) { + config := types.NetworkConnect{ + Container: cid, + } + + status, _, err := sockRequest("POST", "/networks/"+nid+"/disconnect", config) + c.Assert(status, checker.Equals, http.StatusOK) + c.Assert(err, checker.IsNil) +} + +func deleteNetwork(c *check.C, id string, shouldSucceed bool) { + status, _, err := sockRequest("DELETE", "/networks/"+id, nil) + if !shouldSucceed { + c.Assert(status, checker.Not(checker.Equals), http.StatusOK) + return + } + c.Assert(status, checker.Equals, http.StatusNoContent) + c.Assert(err, checker.IsNil) +} diff --git a/integration-cli/docker_api_resize_test.go b/integration-cli/docker_api_resize_test.go new file mode 100644 index 00000000..73023dd2 --- /dev/null +++ b/integration-cli/docker_api_resize_test.go @@ -0,0 +1,44 @@ +package main + +import ( + "net/http" + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestResizeApiResponse(c *check.C) { + out, _ := runSleepingContainer(c, "-d") + cleanedContainerID := strings.TrimSpace(out) + + endpoint := "/containers/" + cleanedContainerID + "/resize?h=40&w=40" + status, _, err := sockRequest("POST", endpoint, nil) + c.Assert(status, check.Equals, http.StatusOK) + c.Assert(err, check.IsNil) +} + +func (s *DockerSuite) TestResizeApiHeightWidthNoInt(c *check.C) { + out, _ := runSleepingContainer(c, "-d") + cleanedContainerID := strings.TrimSpace(out) + + endpoint := "/containers/" + cleanedContainerID + "/resize?h=foo&w=bar" + status, _, err := sockRequest("POST", endpoint, nil) + c.Assert(status, check.Equals, http.StatusInternalServerError) + c.Assert(err, check.IsNil) +} + +func (s *DockerSuite) TestResizeApiResponseWhenContainerNotStarted(c *check.C) { + out, _ := dockerCmd(c, "run", "-d", "busybox", "true") + cleanedContainerID := strings.TrimSpace(out) + + // make sure the exited container is not running + dockerCmd(c, "wait", cleanedContainerID) + + endpoint := "/containers/" + cleanedContainerID + "/resize?h=40&w=40" + status, body, err := sockRequest("POST", endpoint, nil) + c.Assert(status, check.Equals, http.StatusInternalServerError) + c.Assert(err, check.IsNil) + + c.Assert(string(body), checker.Contains, "is not running", check.Commentf("resize should fail with message 'Container is not running'")) +} diff --git a/integration-cli/docker_api_stats_test.go b/integration-cli/docker_api_stats_test.go new file mode 100644 index 00000000..7c3f8d39 --- /dev/null +++ b/integration-cli/docker_api_stats_test.go @@ -0,0 +1,257 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "os/exec" + "runtime" + "strconv" + "strings" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/version" + "github.com/docker/engine-api/types" + "github.com/go-check/check" +) + +var expectedNetworkInterfaceStats = strings.Split("rx_bytes rx_dropped rx_errors rx_packets tx_bytes tx_dropped tx_errors tx_packets", " ") + +func (s *DockerSuite) TestApiStatsNoStreamGetCpu(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "while true;do echo 'Hello'; usleep 100000; done") + + id := strings.TrimSpace(out) + c.Assert(waitRun(id), checker.IsNil) + + resp, body, err := sockRequestRaw("GET", fmt.Sprintf("/containers/%s/stats?stream=false", id), nil, "") + c.Assert(err, checker.IsNil) + c.Assert(resp.StatusCode, checker.Equals, http.StatusOK) + c.Assert(resp.Header.Get("Content-Type"), checker.Equals, "application/json") + + var v *types.Stats + err = json.NewDecoder(body).Decode(&v) + c.Assert(err, checker.IsNil) + body.Close() + + var cpuPercent = 0.0 + cpuDelta := float64(v.CPUStats.CPUUsage.TotalUsage - v.PreCPUStats.CPUUsage.TotalUsage) + systemDelta := float64(v.CPUStats.SystemUsage - v.PreCPUStats.SystemUsage) + cpuPercent = (cpuDelta / systemDelta) * float64(len(v.CPUStats.CPUUsage.PercpuUsage)) * 100.0 + + c.Assert(cpuPercent, check.Not(checker.Equals), 0.0, check.Commentf("docker stats with no-stream get cpu usage failed: was %v", cpuPercent)) +} + +func (s *DockerSuite) TestApiStatsStoppedContainerInGoroutines(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "echo 1") + id := strings.TrimSpace(out) + + getGoRoutines := func() int { + _, body, err := sockRequestRaw("GET", fmt.Sprintf("/info"), nil, "") + c.Assert(err, checker.IsNil) + info := types.Info{} + err = json.NewDecoder(body).Decode(&info) + c.Assert(err, checker.IsNil) + body.Close() + return info.NGoroutines + } + + // When the HTTP connection is closed, the number of goroutines should not increase. + routines := getGoRoutines() + _, body, err := sockRequestRaw("GET", fmt.Sprintf("/containers/%s/stats", id), nil, "") + c.Assert(err, checker.IsNil) + body.Close() + + t := time.After(30 * time.Second) + for { + select { + case <-t: + c.Assert(getGoRoutines(), checker.LessOrEqualThan, routines) + return + default: + if n := getGoRoutines(); n <= routines { + return + } + time.Sleep(200 * time.Millisecond) + } + } +} + +func (s *DockerSuite) TestApiStatsNetworkStats(c *check.C) { + testRequires(c, SameHostDaemon) + testRequires(c, DaemonIsLinux) + + out, _ := runSleepingContainer(c) + id := strings.TrimSpace(out) + c.Assert(waitRun(id), checker.IsNil) + + // Retrieve the container address + contIP := findContainerIP(c, id, "bridge") + numPings := 4 + + var preRxPackets uint64 + var preTxPackets uint64 + var postRxPackets uint64 + var postTxPackets uint64 + + // Get the container networking stats before and after pinging the container + nwStatsPre := getNetworkStats(c, id) + for _, v := range nwStatsPre { + preRxPackets += v.RxPackets + preTxPackets += v.TxPackets + } + + countParam := "-c" + if runtime.GOOS == "windows" { + countParam = "-n" // Ping count parameter is -n on Windows + } + pingout, err := exec.Command("ping", contIP, countParam, strconv.Itoa(numPings)).Output() + pingouts := string(pingout[:]) + c.Assert(err, checker.IsNil) + nwStatsPost := getNetworkStats(c, id) + for _, v := range nwStatsPost { + postRxPackets += v.RxPackets + postTxPackets += v.TxPackets + } + + // Verify the stats contain at least the expected number of packets (account for ARP) + expRxPkts := 1 + preRxPackets + uint64(numPings) + expTxPkts := 1 + preTxPackets + uint64(numPings) + c.Assert(postTxPackets, checker.GreaterOrEqualThan, expTxPkts, + check.Commentf("Reported less TxPackets than expected. Expected >= %d. Found %d. %s", expTxPkts, postTxPackets, pingouts)) + c.Assert(postRxPackets, checker.GreaterOrEqualThan, expRxPkts, + check.Commentf("Reported less Txbytes than expected. Expected >= %d. Found %d. %s", expRxPkts, postRxPackets, pingouts)) +} + +func (s *DockerSuite) TestApiStatsNetworkStatsVersioning(c *check.C) { + testRequires(c, SameHostDaemon) + testRequires(c, DaemonIsLinux) + + out, _ := runSleepingContainer(c) + id := strings.TrimSpace(out) + c.Assert(waitRun(id), checker.IsNil) + + for i := 17; i <= 21; i++ { + apiVersion := fmt.Sprintf("v1.%d", i) + statsJSONBlob := getVersionedStats(c, id, apiVersion) + if version.Version(apiVersion).LessThan("v1.21") { + c.Assert(jsonBlobHasLTv121NetworkStats(statsJSONBlob), checker.Equals, true, + check.Commentf("Stats JSON blob from API %s %#v does not look like a =v1.21 API stats structure", apiVersion, statsJSONBlob)) + } + } +} + +func getNetworkStats(c *check.C, id string) map[string]types.NetworkStats { + var st *types.StatsJSON + + _, body, err := sockRequestRaw("GET", fmt.Sprintf("/containers/%s/stats?stream=false", id), nil, "") + c.Assert(err, checker.IsNil) + + err = json.NewDecoder(body).Decode(&st) + c.Assert(err, checker.IsNil) + body.Close() + + return st.Networks +} + +// getVersionedStats returns stats result for the +// container with id using an API call with version apiVersion. Since the +// stats result type differs between API versions, we simply return +// map[string]interface{}. +func getVersionedStats(c *check.C, id string, apiVersion string) map[string]interface{} { + stats := make(map[string]interface{}) + + _, body, err := sockRequestRaw("GET", fmt.Sprintf("/%s/containers/%s/stats?stream=false", apiVersion, id), nil, "") + c.Assert(err, checker.IsNil) + defer body.Close() + + err = json.NewDecoder(body).Decode(&stats) + c.Assert(err, checker.IsNil, check.Commentf("failed to decode stat: %s", err)) + + return stats +} + +func jsonBlobHasLTv121NetworkStats(blob map[string]interface{}) bool { + networkStatsIntfc, ok := blob["network"] + if !ok { + return false + } + networkStats, ok := networkStatsIntfc.(map[string]interface{}) + if !ok { + return false + } + for _, expectedKey := range expectedNetworkInterfaceStats { + if _, ok := networkStats[expectedKey]; !ok { + return false + } + } + return true +} + +func jsonBlobHasGTE121NetworkStats(blob map[string]interface{}) bool { + networksStatsIntfc, ok := blob["networks"] + if !ok { + return false + } + networksStats, ok := networksStatsIntfc.(map[string]interface{}) + if !ok { + return false + } + for _, networkInterfaceStatsIntfc := range networksStats { + networkInterfaceStats, ok := networkInterfaceStatsIntfc.(map[string]interface{}) + if !ok { + return false + } + for _, expectedKey := range expectedNetworkInterfaceStats { + if _, ok := networkInterfaceStats[expectedKey]; !ok { + return false + } + } + } + return true +} + +func (s *DockerSuite) TestApiStatsContainerNotFound(c *check.C) { + testRequires(c, DaemonIsLinux) + + status, _, err := sockRequest("GET", "/containers/nonexistent/stats", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusNotFound) + + status, _, err = sockRequest("GET", "/containers/nonexistent/stats?stream=0", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusNotFound) +} + +func (s *DockerSuite) TestApiStatsContainerGetMemoryLimit(c *check.C) { + testRequires(c, DaemonIsLinux) + + resp, body, err := sockRequestRaw("GET", "/info", nil, "application/json") + c.Assert(err, checker.IsNil) + c.Assert(resp.StatusCode, checker.Equals, http.StatusOK) + var info types.Info + err = json.NewDecoder(body).Decode(&info) + c.Assert(err, checker.IsNil) + body.Close() + + // don't set a memory limit, the memory limit should be system memory + conName := "foo" + dockerCmd(c, "run", "-d", "--name", conName, "busybox", "top") + c.Assert(waitRun(conName), checker.IsNil) + + resp, body, err = sockRequestRaw("GET", fmt.Sprintf("/containers/%s/stats?stream=false", conName), nil, "") + c.Assert(err, checker.IsNil) + c.Assert(resp.StatusCode, checker.Equals, http.StatusOK) + c.Assert(resp.Header.Get("Content-Type"), checker.Equals, "application/json") + + var v *types.Stats + err = json.NewDecoder(body).Decode(&v) + c.Assert(err, checker.IsNil) + body.Close() + c.Assert(fmt.Sprintf("%d", v.MemoryStats.Limit), checker.Equals, fmt.Sprintf("%d", info.MemTotal)) +} diff --git a/integration-cli/docker_api_test.go b/integration-cli/docker_api_test.go new file mode 100644 index 00000000..a7257779 --- /dev/null +++ b/integration-cli/docker_api_test.go @@ -0,0 +1,101 @@ +package main + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/http/httputil" + "os/exec" + "strconv" + "strings" + "time" + + "github.com/docker/docker/api" + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestApiOptionsRoute(c *check.C) { + status, _, err := sockRequest("OPTIONS", "/", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusOK) +} + +func (s *DockerSuite) TestApiGetEnabledCors(c *check.C) { + res, body, err := sockRequestRaw("GET", "/version", nil, "") + c.Assert(err, checker.IsNil) + c.Assert(res.StatusCode, checker.Equals, http.StatusOK) + body.Close() + // TODO: @runcom incomplete tests, why old integration tests had this headers + // and here none of the headers below are in the response? + //c.Log(res.Header) + //c.Assert(res.Header.Get("Access-Control-Allow-Origin"), check.Equals, "*") + //c.Assert(res.Header.Get("Access-Control-Allow-Headers"), check.Equals, "Origin, X-Requested-With, Content-Type, Accept, X-Registry-Auth") +} + +func (s *DockerSuite) TestApiVersionStatusCode(c *check.C) { + conn, err := sockConn(time.Duration(10 * time.Second)) + c.Assert(err, checker.IsNil) + + client := httputil.NewClientConn(conn, nil) + defer client.Close() + + req, err := http.NewRequest("GET", "/v999.0/version", nil) + c.Assert(err, checker.IsNil) + req.Header.Set("User-Agent", "Docker-Client/999.0 (os)") + + res, err := client.Do(req) + c.Assert(res.StatusCode, checker.Equals, http.StatusBadRequest) +} + +func (s *DockerSuite) TestApiClientVersionNewerThanServer(c *check.C) { + v := strings.Split(api.DefaultVersion.String(), ".") + vMinInt, err := strconv.Atoi(v[1]) + c.Assert(err, checker.IsNil) + vMinInt++ + v[1] = strconv.Itoa(vMinInt) + version := strings.Join(v, ".") + + status, body, err := sockRequest("GET", "/v"+version+"/version", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusBadRequest) + expected := fmt.Sprintf("client is newer than server (client API version: %s, server API version: %s)", version, api.DefaultVersion) + c.Assert(strings.TrimSpace(string(body)), checker.Equals, expected) +} + +func (s *DockerSuite) TestApiClientVersionOldNotSupported(c *check.C) { + v := strings.Split(api.MinVersion.String(), ".") + vMinInt, err := strconv.Atoi(v[1]) + c.Assert(err, checker.IsNil) + vMinInt-- + v[1] = strconv.Itoa(vMinInt) + version := strings.Join(v, ".") + + status, body, err := sockRequest("GET", "/v"+version+"/version", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusBadRequest) + expected := fmt.Sprintf("client version %s is too old. Minimum supported API version is %s, please upgrade your client to a newer version", version, api.MinVersion) + c.Assert(strings.TrimSpace(string(body)), checker.Equals, expected) +} + +func (s *DockerSuite) TestApiDockerApiVersion(c *check.C) { + var svrVersion string + + server := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + url := r.URL.Path + svrVersion = url + })) + defer server.Close() + + // Test using the env var first + cmd := exec.Command(dockerBinary, "-H="+server.URL[7:], "version") + cmd.Env = appendBaseEnv(false, "DOCKER_API_VERSION=xxx") + out, _, _ := runCommandWithOutput(cmd) + + c.Assert(svrVersion, check.Equals, "/vxxx/version") + + if !strings.Contains(out, "API version: xxx") { + c.Fatalf("Out didn't have 'xxx' for the API version, had:\n%s", out) + } +} diff --git a/integration-cli/docker_api_update_unix_test.go b/integration-cli/docker_api_update_unix_test.go new file mode 100644 index 00000000..607e76a4 --- /dev/null +++ b/integration-cli/docker_api_update_unix_test.go @@ -0,0 +1,35 @@ +// +build !windows + +package main + +import ( + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestApiUpdateContainer(c *check.C) { + testRequires(c, DaemonIsLinux) + testRequires(c, memoryLimitSupport) + testRequires(c, swapMemorySupport) + + name := "apiUpdateContainer" + hostConfig := map[string]interface{}{ + "Memory": 314572800, + "MemorySwap": 524288000, + } + dockerCmd(c, "run", "-d", "--name", name, "-m", "200M", "busybox", "top") + _, _, err := sockRequest("POST", "/containers/"+name+"/update", hostConfig) + c.Assert(err, check.IsNil) + + c.Assert(inspectField(c, name, "HostConfig.Memory"), checker.Equals, "314572800") + file := "/sys/fs/cgroup/memory/memory.limit_in_bytes" + out, _ := dockerCmd(c, "exec", name, "cat", file) + c.Assert(strings.TrimSpace(out), checker.Equals, "314572800") + + c.Assert(inspectField(c, name, "HostConfig.MemorySwap"), checker.Equals, "524288000") + file = "/sys/fs/cgroup/memory/memory.memsw.limit_in_bytes" + out, _ = dockerCmd(c, "exec", name, "cat", file) + c.Assert(strings.TrimSpace(out), checker.Equals, "524288000") +} diff --git a/integration-cli/docker_api_version_test.go b/integration-cli/docker_api_version_test.go new file mode 100644 index 00000000..ccb14841 --- /dev/null +++ b/integration-cli/docker_api_version_test.go @@ -0,0 +1,23 @@ +package main + +import ( + "encoding/json" + "net/http" + + "github.com/docker/docker/dockerversion" + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/engine-api/types" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestGetVersion(c *check.C) { + status, body, err := sockRequest("GET", "/version", nil) + c.Assert(status, checker.Equals, http.StatusOK) + c.Assert(err, checker.IsNil) + + var v types.Version + + c.Assert(json.Unmarshal(body, &v), checker.IsNil) + + c.Assert(v.Version, checker.Equals, dockerversion.Version, check.Commentf("Version mismatch")) +} diff --git a/integration-cli/docker_api_volumes_test.go b/integration-cli/docker_api_volumes_test.go new file mode 100644 index 00000000..732271d0 --- /dev/null +++ b/integration-cli/docker_api_volumes_test.go @@ -0,0 +1,88 @@ +package main + +import ( + "encoding/json" + "net/http" + "path/filepath" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/engine-api/types" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestVolumesApiList(c *check.C) { + prefix, _ := getPrefixAndSlashFromDaemonPlatform() + dockerCmd(c, "run", "-v", prefix+"/foo", "busybox") + + status, b, err := sockRequest("GET", "/volumes", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusOK) + + var volumes types.VolumesListResponse + c.Assert(json.Unmarshal(b, &volumes), checker.IsNil) + + c.Assert(len(volumes.Volumes), checker.Equals, 1, check.Commentf("\n%v", volumes.Volumes)) +} + +func (s *DockerSuite) TestVolumesApiCreate(c *check.C) { + config := types.VolumeCreateRequest{ + Name: "test", + } + status, b, err := sockRequest("POST", "/volumes/create", config) + c.Assert(err, check.IsNil) + c.Assert(status, check.Equals, http.StatusCreated, check.Commentf(string(b))) + + var vol types.Volume + err = json.Unmarshal(b, &vol) + c.Assert(err, checker.IsNil) + + c.Assert(filepath.Base(filepath.Dir(vol.Mountpoint)), checker.Equals, config.Name) +} + +func (s *DockerSuite) TestVolumesApiRemove(c *check.C) { + prefix, _ := getPrefixAndSlashFromDaemonPlatform() + dockerCmd(c, "run", "-v", prefix+"/foo", "--name=test", "busybox") + + status, b, err := sockRequest("GET", "/volumes", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusOK) + + var volumes types.VolumesListResponse + c.Assert(json.Unmarshal(b, &volumes), checker.IsNil) + c.Assert(len(volumes.Volumes), checker.Equals, 1, check.Commentf("\n%v", volumes.Volumes)) + + v := volumes.Volumes[0] + status, _, err = sockRequest("DELETE", "/volumes/"+v.Name, nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusConflict, check.Commentf("Should not be able to remove a volume that is in use")) + + dockerCmd(c, "rm", "-f", "test") + status, data, err := sockRequest("DELETE", "/volumes/"+v.Name, nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusNoContent, check.Commentf(string(data))) + +} + +func (s *DockerSuite) TestVolumesApiInspect(c *check.C) { + config := types.VolumeCreateRequest{ + Name: "test", + } + status, b, err := sockRequest("POST", "/volumes/create", config) + c.Assert(err, check.IsNil) + c.Assert(status, check.Equals, http.StatusCreated, check.Commentf(string(b))) + + status, b, err = sockRequest("GET", "/volumes", nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusOK, check.Commentf(string(b))) + + var volumes types.VolumesListResponse + c.Assert(json.Unmarshal(b, &volumes), checker.IsNil) + c.Assert(len(volumes.Volumes), checker.Equals, 1, check.Commentf("\n%v", volumes.Volumes)) + + var vol types.Volume + status, b, err = sockRequest("GET", "/volumes/"+config.Name, nil) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusOK, check.Commentf(string(b))) + c.Assert(json.Unmarshal(b, &vol), checker.IsNil) + c.Assert(vol.Name, checker.Equals, config.Name) +} diff --git a/integration-cli/docker_cli_attach_test.go b/integration-cli/docker_cli_attach_test.go new file mode 100644 index 00000000..0ac3e1ac --- /dev/null +++ b/integration-cli/docker_cli_attach_test.go @@ -0,0 +1,162 @@ +package main + +import ( + "bufio" + "fmt" + "io" + "os/exec" + "strings" + "sync" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +const attachWait = 5 * time.Second + +func (s *DockerSuite) TestAttachMultipleAndRestart(c *check.C) { + testRequires(c, DaemonIsLinux) + + endGroup := &sync.WaitGroup{} + startGroup := &sync.WaitGroup{} + endGroup.Add(3) + startGroup.Add(3) + + err := waitForContainer("attacher", "-d", "busybox", "/bin/sh", "-c", "while true; do sleep 1; echo hello; done") + c.Assert(err, check.IsNil) + + startDone := make(chan struct{}) + endDone := make(chan struct{}) + + go func() { + endGroup.Wait() + close(endDone) + }() + + go func() { + startGroup.Wait() + close(startDone) + }() + + for i := 0; i < 3; i++ { + go func() { + cmd := exec.Command(dockerBinary, "attach", "attacher") + + defer func() { + cmd.Wait() + endGroup.Done() + }() + + out, err := cmd.StdoutPipe() + if err != nil { + c.Fatal(err) + } + + if err := cmd.Start(); err != nil { + c.Fatal(err) + } + + buf := make([]byte, 1024) + + if _, err := out.Read(buf); err != nil && err != io.EOF { + c.Fatal(err) + } + + startGroup.Done() + + if !strings.Contains(string(buf), "hello") { + c.Fatalf("unexpected output %s expected hello\n", string(buf)) + } + }() + } + + select { + case <-startDone: + case <-time.After(attachWait): + c.Fatalf("Attaches did not initialize properly") + } + + dockerCmd(c, "kill", "attacher") + + select { + case <-endDone: + case <-time.After(attachWait): + c.Fatalf("Attaches did not finish properly") + } +} + +func (s *DockerSuite) TestAttachTTYWithoutStdin(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "-ti", "busybox") + + id := strings.TrimSpace(out) + c.Assert(waitRun(id), check.IsNil) + + done := make(chan error) + go func() { + defer close(done) + + cmd := exec.Command(dockerBinary, "attach", id) + if _, err := cmd.StdinPipe(); err != nil { + done <- err + return + } + + expected := "cannot enable tty mode" + if out, _, err := runCommandWithOutput(cmd); err == nil { + done <- fmt.Errorf("attach should have failed") + return + } else if !strings.Contains(out, expected) { + done <- fmt.Errorf("attach failed with error %q: expected %q", out, expected) + return + } + }() + + select { + case err := <-done: + c.Assert(err, check.IsNil) + case <-time.After(attachWait): + c.Fatal("attach is running but should have failed") + } +} + +func (s *DockerSuite) TestAttachDisconnect(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-di", "busybox", "/bin/cat") + id := strings.TrimSpace(out) + + cmd := exec.Command(dockerBinary, "attach", id) + stdin, err := cmd.StdinPipe() + if err != nil { + c.Fatal(err) + } + defer stdin.Close() + stdout, err := cmd.StdoutPipe() + c.Assert(err, check.IsNil) + defer stdout.Close() + c.Assert(cmd.Start(), check.IsNil) + defer cmd.Process.Kill() + + _, err = stdin.Write([]byte("hello\n")) + c.Assert(err, check.IsNil) + out, err = bufio.NewReader(stdout).ReadString('\n') + c.Assert(err, check.IsNil) + c.Assert(strings.TrimSpace(out), check.Equals, "hello") + + c.Assert(stdin.Close(), check.IsNil) + + // Expect container to still be running after stdin is closed + running := inspectField(c, id, "State.Running") + c.Assert(running, check.Equals, "true") +} + +func (s *DockerSuite) TestAttachPausedContainer(c *check.C) { + testRequires(c, DaemonIsLinux) // Containers cannot be paused on Windows + defer unpauseAllContainers() + dockerCmd(c, "run", "-d", "--name=test", "busybox", "top") + dockerCmd(c, "pause", "test") + out, _, err := dockerCmdWithError("attach", "test") + c.Assert(err, checker.NotNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "You cannot attach to a paused container, unpause it first") +} diff --git a/integration-cli/docker_cli_attach_unix_test.go b/integration-cli/docker_cli_attach_unix_test.go new file mode 100644 index 00000000..7af761d7 --- /dev/null +++ b/integration-cli/docker_cli_attach_unix_test.go @@ -0,0 +1,230 @@ +// +build !windows + +package main + +import ( + "bufio" + "os/exec" + "strings" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/stringid" + "github.com/go-check/check" + "github.com/kr/pty" +) + +// #9860 Make sure attach ends when container ends (with no errors) +func (s *DockerSuite) TestAttachClosedOnContainerStop(c *check.C) { + testRequires(c, SameHostDaemon) + + out, _ := dockerCmd(c, "run", "-dti", "busybox", "/bin/sh", "-c", `trap 'exit 0' SIGTERM; while true; do sleep 1; done`) + + id := strings.TrimSpace(out) + c.Assert(waitRun(id), check.IsNil) + + _, tty, err := pty.Open() + c.Assert(err, check.IsNil) + + attachCmd := exec.Command(dockerBinary, "attach", id) + attachCmd.Stdin = tty + attachCmd.Stdout = tty + attachCmd.Stderr = tty + err = attachCmd.Start() + c.Assert(err, check.IsNil) + + errChan := make(chan error) + go func() { + defer close(errChan) + // Container is waiting for us to signal it to stop + dockerCmd(c, "stop", id) + // And wait for the attach command to end + errChan <- attachCmd.Wait() + }() + + // Wait for the docker to end (should be done by the + // stop command in the go routine) + dockerCmd(c, "wait", id) + + select { + case err := <-errChan: + c.Assert(err, check.IsNil) + case <-time.After(attachWait): + c.Fatal("timed out without attach returning") + } + +} + +func (s *DockerSuite) TestAttachAfterDetach(c *check.C) { + + name := "detachtest" + + cpty, tty, err := pty.Open() + c.Assert(err, checker.IsNil, check.Commentf("Could not open pty: %v", err)) + cmd := exec.Command(dockerBinary, "run", "-ti", "--name", name, "busybox") + cmd.Stdin = tty + cmd.Stdout = tty + cmd.Stderr = tty + + errChan := make(chan error) + go func() { + errChan <- cmd.Run() + close(errChan) + }() + + c.Assert(waitRun(name), check.IsNil) + + cpty.Write([]byte{16}) + time.Sleep(100 * time.Millisecond) + cpty.Write([]byte{17}) + + select { + case err := <-errChan: + c.Assert(err, check.IsNil) + case <-time.After(5 * time.Second): + c.Fatal("timeout while detaching") + } + + cpty, tty, err = pty.Open() + c.Assert(err, checker.IsNil, check.Commentf("Could not open pty: %v", err)) + + cmd = exec.Command(dockerBinary, "attach", name) + cmd.Stdin = tty + cmd.Stdout = tty + cmd.Stderr = tty + + err = cmd.Start() + c.Assert(err, checker.IsNil) + + bytes := make([]byte, 10) + var nBytes int + readErr := make(chan error, 1) + + go func() { + time.Sleep(500 * time.Millisecond) + cpty.Write([]byte("\n")) + time.Sleep(500 * time.Millisecond) + + nBytes, err = cpty.Read(bytes) + cpty.Close() + readErr <- err + }() + + select { + case err := <-readErr: + c.Assert(err, check.IsNil) + case <-time.After(2 * time.Second): + c.Fatal("timeout waiting for attach read") + } + + err = cmd.Wait() + c.Assert(err, checker.IsNil) + + c.Assert(string(bytes[:nBytes]), checker.Contains, "/ #") + +} + +// TestAttachDetach checks that attach in tty mode can be detached using the long container ID +func (s *DockerSuite) TestAttachDetach(c *check.C) { + out, _ := dockerCmd(c, "run", "-itd", "busybox", "cat") + id := strings.TrimSpace(out) + c.Assert(waitRun(id), check.IsNil) + + cpty, tty, err := pty.Open() + c.Assert(err, check.IsNil) + defer cpty.Close() + + cmd := exec.Command(dockerBinary, "attach", id) + cmd.Stdin = tty + stdout, err := cmd.StdoutPipe() + c.Assert(err, check.IsNil) + defer stdout.Close() + err = cmd.Start() + c.Assert(err, check.IsNil) + c.Assert(waitRun(id), check.IsNil) + + _, err = cpty.Write([]byte("hello\n")) + c.Assert(err, check.IsNil) + out, err = bufio.NewReader(stdout).ReadString('\n') + c.Assert(err, check.IsNil) + c.Assert(strings.TrimSpace(out), checker.Equals, "hello", check.Commentf("expected 'hello', got %q", out)) + + // escape sequence + _, err = cpty.Write([]byte{16}) + c.Assert(err, checker.IsNil) + time.Sleep(100 * time.Millisecond) + _, err = cpty.Write([]byte{17}) + c.Assert(err, checker.IsNil) + + ch := make(chan struct{}) + go func() { + cmd.Wait() + ch <- struct{}{} + }() + + running := inspectField(c, id, "State.Running") + c.Assert(running, checker.Equals, "true", check.Commentf("expected container to still be running")) + + go func() { + dockerCmd(c, "kill", id) + }() + + select { + case <-ch: + case <-time.After(10 * time.Millisecond): + c.Fatal("timed out waiting for container to exit") + } + +} + +// TestAttachDetachTruncatedID checks that attach in tty mode can be detached +func (s *DockerSuite) TestAttachDetachTruncatedID(c *check.C) { + out, _ := dockerCmd(c, "run", "-itd", "busybox", "cat") + id := stringid.TruncateID(strings.TrimSpace(out)) + c.Assert(waitRun(id), check.IsNil) + + cpty, tty, err := pty.Open() + c.Assert(err, checker.IsNil) + defer cpty.Close() + + cmd := exec.Command(dockerBinary, "attach", id) + cmd.Stdin = tty + stdout, err := cmd.StdoutPipe() + c.Assert(err, checker.IsNil) + defer stdout.Close() + err = cmd.Start() + c.Assert(err, checker.IsNil) + + _, err = cpty.Write([]byte("hello\n")) + c.Assert(err, checker.IsNil) + out, err = bufio.NewReader(stdout).ReadString('\n') + c.Assert(err, checker.IsNil) + c.Assert(strings.TrimSpace(out), checker.Equals, "hello", check.Commentf("expected 'hello', got %q", out)) + + // escape sequence + _, err = cpty.Write([]byte{16}) + c.Assert(err, checker.IsNil) + time.Sleep(100 * time.Millisecond) + _, err = cpty.Write([]byte{17}) + c.Assert(err, checker.IsNil) + + ch := make(chan struct{}) + go func() { + cmd.Wait() + ch <- struct{}{} + }() + + running := inspectField(c, id, "State.Running") + c.Assert(running, checker.Equals, "true", check.Commentf("expected container to still be running")) + + go func() { + dockerCmd(c, "kill", id) + }() + + select { + case <-ch: + case <-time.After(10 * time.Millisecond): + c.Fatal("timed out waiting for container to exit") + } + +} diff --git a/integration-cli/docker_cli_authz_unix_test.go b/integration-cli/docker_cli_authz_unix_test.go new file mode 100644 index 00000000..0a208d75 --- /dev/null +++ b/integration-cli/docker_cli_authz_unix_test.go @@ -0,0 +1,363 @@ +// +build !windows + +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "strings" + + "bufio" + "bytes" + "os/exec" + "strconv" + "time" + + "github.com/docker/docker/pkg/authorization" + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/plugins" + "github.com/go-check/check" +) + +const ( + testAuthZPlugin = "authzplugin" + unauthorizedMessage = "User unauthorized authz plugin" + errorMessage = "something went wrong..." + containerListAPI = "/containers/json" +) + +var ( + alwaysAllowed = []string{"/_ping", "/info"} +) + +func init() { + check.Suite(&DockerAuthzSuite{ + ds: &DockerSuite{}, + }) +} + +type DockerAuthzSuite struct { + server *httptest.Server + ds *DockerSuite + d *Daemon + ctrl *authorizationController +} + +type authorizationController struct { + reqRes authorization.Response // reqRes holds the plugin response to the initial client request + resRes authorization.Response // resRes holds the plugin response to the daemon response + psRequestCnt int // psRequestCnt counts the number of calls to list container request api + psResponseCnt int // psResponseCnt counts the number of calls to list containers response API + requestsURIs []string // requestsURIs stores all request URIs that are sent to the authorization controller +} + +func (s *DockerAuthzSuite) SetUpTest(c *check.C) { + s.d = NewDaemon(c) + s.ctrl = &authorizationController{} +} + +func (s *DockerAuthzSuite) TearDownTest(c *check.C) { + s.d.Stop() + s.ds.TearDownTest(c) + s.ctrl = nil +} + +func (s *DockerAuthzSuite) SetUpSuite(c *check.C) { + mux := http.NewServeMux() + s.server = httptest.NewServer(mux) + c.Assert(s.server, check.NotNil, check.Commentf("Failed to start a HTTP Server")) + + mux.HandleFunc("/Plugin.Activate", func(w http.ResponseWriter, r *http.Request) { + b, err := json.Marshal(plugins.Manifest{Implements: []string{authorization.AuthZApiImplements}}) + c.Assert(err, check.IsNil) + w.Write(b) + }) + + mux.HandleFunc("/AuthZPlugin.AuthZReq", func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + body, err := ioutil.ReadAll(r.Body) + c.Assert(err, check.IsNil) + authReq := authorization.Request{} + err = json.Unmarshal(body, &authReq) + c.Assert(err, check.IsNil) + + assertBody(c, authReq.RequestURI, authReq.RequestHeaders, authReq.RequestBody) + assertAuthHeaders(c, authReq.RequestHeaders) + + // Count only container list api + if strings.HasSuffix(authReq.RequestURI, containerListAPI) { + s.ctrl.psRequestCnt++ + } + + s.ctrl.requestsURIs = append(s.ctrl.requestsURIs, authReq.RequestURI) + + reqRes := s.ctrl.reqRes + if isAllowed(authReq.RequestURI) { + reqRes = authorization.Response{Allow: true} + } + if reqRes.Err != "" { + w.WriteHeader(http.StatusInternalServerError) + } + b, err := json.Marshal(reqRes) + c.Assert(err, check.IsNil) + w.Write(b) + }) + + mux.HandleFunc("/AuthZPlugin.AuthZRes", func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + body, err := ioutil.ReadAll(r.Body) + c.Assert(err, check.IsNil) + authReq := authorization.Request{} + err = json.Unmarshal(body, &authReq) + c.Assert(err, check.IsNil) + + assertBody(c, authReq.RequestURI, authReq.ResponseHeaders, authReq.ResponseBody) + assertAuthHeaders(c, authReq.ResponseHeaders) + + // Count only container list api + if strings.HasSuffix(authReq.RequestURI, containerListAPI) { + s.ctrl.psResponseCnt++ + } + resRes := s.ctrl.resRes + if isAllowed(authReq.RequestURI) { + resRes = authorization.Response{Allow: true} + } + if resRes.Err != "" { + w.WriteHeader(http.StatusInternalServerError) + } + b, err := json.Marshal(resRes) + c.Assert(err, check.IsNil) + w.Write(b) + }) + + err := os.MkdirAll("/etc/docker/plugins", 0755) + c.Assert(err, checker.IsNil) + + fileName := fmt.Sprintf("/etc/docker/plugins/%s.spec", testAuthZPlugin) + err = ioutil.WriteFile(fileName, []byte(s.server.URL), 0644) + c.Assert(err, checker.IsNil) +} + +// check for always allowed endpoints to not inhibit test framework functions +func isAllowed(reqURI string) bool { + for _, endpoint := range alwaysAllowed { + if strings.HasSuffix(reqURI, endpoint) { + return true + } + } + return false +} + +// assertAuthHeaders validates authentication headers are removed +func assertAuthHeaders(c *check.C, headers map[string]string) error { + for k := range headers { + if strings.Contains(strings.ToLower(k), "auth") || strings.Contains(strings.ToLower(k), "x-registry") { + c.Errorf("Found authentication headers in request '%v'", headers) + } + } + return nil +} + +// assertBody asserts that body is removed for non text/json requests +func assertBody(c *check.C, requestURI string, headers map[string]string, body []byte) { + if strings.Contains(strings.ToLower(requestURI), "auth") && len(body) > 0 { + //return fmt.Errorf("Body included for authentication endpoint %s", string(body)) + c.Errorf("Body included for authentication endpoint %s", string(body)) + } + + for k, v := range headers { + if strings.EqualFold(k, "Content-Type") && strings.HasPrefix(v, "text/") || v == "application/json" { + return + } + } + if len(body) > 0 { + c.Errorf("Body included while it should not (Headers: '%v')", headers) + } +} + +func (s *DockerAuthzSuite) TearDownSuite(c *check.C) { + if s.server == nil { + return + } + + s.server.Close() + + err := os.RemoveAll("/etc/docker/plugins") + c.Assert(err, checker.IsNil) +} + +func (s *DockerAuthzSuite) TestAuthZPluginAllowRequest(c *check.C) { + // start the daemon and load busybox, --net=none build fails otherwise + // cause it needs to pull busybox + c.Assert(s.d.Start("--authorization-plugin="+testAuthZPlugin), check.IsNil) + s.ctrl.reqRes.Allow = true + s.ctrl.resRes.Allow = true + c.Assert(s.d.LoadBusybox(), check.IsNil) + + // Ensure command successful + out, err := s.d.Cmd("run", "-d", "busybox", "top") + c.Assert(err, check.IsNil) + + id := strings.TrimSpace(out) + assertURIRecorded(c, s.ctrl.requestsURIs, "/containers/create") + assertURIRecorded(c, s.ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", id)) + + out, err = s.d.Cmd("ps") + c.Assert(err, check.IsNil) + c.Assert(assertContainerList(out, []string{id}), check.Equals, true) + c.Assert(s.ctrl.psRequestCnt, check.Equals, 1) + c.Assert(s.ctrl.psResponseCnt, check.Equals, 1) +} + +func (s *DockerAuthzSuite) TestAuthZPluginDenyRequest(c *check.C) { + err := s.d.Start("--authorization-plugin=" + testAuthZPlugin) + c.Assert(err, check.IsNil) + s.ctrl.reqRes.Allow = false + s.ctrl.reqRes.Msg = unauthorizedMessage + + // Ensure command is blocked + res, err := s.d.Cmd("ps") + c.Assert(err, check.NotNil) + c.Assert(s.ctrl.psRequestCnt, check.Equals, 1) + c.Assert(s.ctrl.psResponseCnt, check.Equals, 0) + + // Ensure unauthorized message appears in response + c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s\n", testAuthZPlugin, unauthorizedMessage)) +} + +func (s *DockerAuthzSuite) TestAuthZPluginDenyResponse(c *check.C) { + err := s.d.Start("--authorization-plugin=" + testAuthZPlugin) + c.Assert(err, check.IsNil) + s.ctrl.reqRes.Allow = true + s.ctrl.resRes.Allow = false + s.ctrl.resRes.Msg = unauthorizedMessage + + // Ensure command is blocked + res, err := s.d.Cmd("ps") + c.Assert(err, check.NotNil) + c.Assert(s.ctrl.psRequestCnt, check.Equals, 1) + c.Assert(s.ctrl.psResponseCnt, check.Equals, 1) + + // Ensure unauthorized message appears in response + c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s\n", testAuthZPlugin, unauthorizedMessage)) +} + +// TestAuthZPluginAllowEventStream verifies event stream propagates correctly after request pass through by the authorization plugin +func (s *DockerAuthzSuite) TestAuthZPluginAllowEventStream(c *check.C) { + testRequires(c, DaemonIsLinux) + + // start the daemon and load busybox to avoid pulling busybox from Docker Hub + c.Assert(s.d.Start("--authorization-plugin="+testAuthZPlugin), check.IsNil) + s.ctrl.reqRes.Allow = true + s.ctrl.resRes.Allow = true + c.Assert(s.d.LoadBusybox(), check.IsNil) + + startTime := strconv.FormatInt(daemonTime(c).Unix(), 10) + // Add another command to to enable event pipelining + eventsCmd := exec.Command(s.d.cmd.Path, "--host", s.d.sock(), "events", "--since", startTime) + stdout, err := eventsCmd.StdoutPipe() + if err != nil { + c.Assert(err, check.IsNil) + } + + observer := eventObserver{ + buffer: new(bytes.Buffer), + command: eventsCmd, + scanner: bufio.NewScanner(stdout), + startTime: startTime, + } + + err = observer.Start() + c.Assert(err, checker.IsNil) + defer observer.Stop() + + // Create a container and wait for the creation events + out, err := s.d.Cmd("run", "-d", "busybox", "top") + c.Assert(err, check.IsNil, check.Commentf(out)) + containerID := strings.TrimSpace(out) + c.Assert(s.d.waitRun(containerID), checker.IsNil) + + events := map[string]chan bool{ + "create": make(chan bool, 1), + "start": make(chan bool, 1), + } + + matcher := matchEventLine(containerID, "container", events) + processor := processEventMatch(events) + go observer.Match(matcher, processor) + + // Ensure all events are received + for event, eventChannel := range events { + + select { + case <-time.After(30 * time.Second): + // Fail the test + observer.CheckEventError(c, containerID, event, matcher) + c.FailNow() + case <-eventChannel: + // Ignore, event received + } + } + + // Ensure both events and container endpoints are passed to the authorization plugin + assertURIRecorded(c, s.ctrl.requestsURIs, "/events") + assertURIRecorded(c, s.ctrl.requestsURIs, "/containers/create") + assertURIRecorded(c, s.ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", containerID)) +} + +func (s *DockerAuthzSuite) TestAuthZPluginErrorResponse(c *check.C) { + err := s.d.Start("--authorization-plugin=" + testAuthZPlugin) + c.Assert(err, check.IsNil) + s.ctrl.reqRes.Allow = true + s.ctrl.resRes.Err = errorMessage + + // Ensure command is blocked + res, err := s.d.Cmd("ps") + c.Assert(err, check.NotNil) + + c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s\n", testAuthZPlugin, authorization.AuthZApiResponse, errorMessage)) +} + +func (s *DockerAuthzSuite) TestAuthZPluginErrorRequest(c *check.C) { + err := s.d.Start("--authorization-plugin=" + testAuthZPlugin) + c.Assert(err, check.IsNil) + s.ctrl.reqRes.Err = errorMessage + + // Ensure command is blocked + res, err := s.d.Cmd("ps") + c.Assert(err, check.NotNil) + + c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s\n", testAuthZPlugin, authorization.AuthZApiRequest, errorMessage)) +} + +func (s *DockerAuthzSuite) TestAuthZPluginEnsureNoDuplicatePluginRegistration(c *check.C) { + c.Assert(s.d.Start("--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin), check.IsNil) + + s.ctrl.reqRes.Allow = true + s.ctrl.resRes.Allow = true + + out, err := s.d.Cmd("ps") + c.Assert(err, check.IsNil, check.Commentf(out)) + + // assert plugin is only called once.. + c.Assert(s.ctrl.psRequestCnt, check.Equals, 1) + c.Assert(s.ctrl.psResponseCnt, check.Equals, 1) +} + +// assertURIRecorded verifies that the given URI was sent and recorded in the authz plugin +func assertURIRecorded(c *check.C, uris []string, uri string) { + var found bool + for _, u := range uris { + if strings.Contains(u, uri) { + found = true + break + } + } + if !found { + c.Fatalf("Expected to find URI '%s', recorded uris '%s'", uri, strings.Join(uris, ",")) + } +} diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go new file mode 100644 index 00000000..a53df642 --- /dev/null +++ b/integration-cli/docker_cli_build_test.go @@ -0,0 +1,6976 @@ +package main + +import ( + "archive/tar" + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "reflect" + "regexp" + "runtime" + "strconv" + "strings" + "text/template" + "time" + + "github.com/docker/docker/builder/dockerfile/command" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/stringutils" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestBuildJSONEmptyRun(c *check.C) { + name := "testbuildjsonemptyrun" + + _, err := buildImage( + name, + ` + FROM busybox + RUN [] + `, + true) + + if err != nil { + c.Fatal("error when dealing with a RUN statement with empty JSON array") + } + +} + +func (s *DockerSuite) TestBuildEmptyWhitespace(c *check.C) { + name := "testbuildemptywhitespace" + + _, err := buildImage( + name, + ` + FROM busybox + COPY + quux \ + bar + `, + true) + + if err == nil { + c.Fatal("no error when dealing with a COPY statement with no content on the same line") + } + +} + +func (s *DockerSuite) TestBuildShCmdJSONEntrypoint(c *check.C) { + name := "testbuildshcmdjsonentrypoint" + + _, err := buildImage( + name, + ` + FROM busybox + ENTRYPOINT ["echo"] + CMD echo test + `, + true) + if err != nil { + c.Fatal(err) + } + + out, _ := dockerCmd(c, "run", "--rm", name) + + if daemonPlatform == "windows" { + if !strings.Contains(out, "cmd /S /C echo test") { + c.Fatalf("CMD did not contain cmd /S /C echo test : %q", out) + } + } else { + if strings.TrimSpace(out) != "/bin/sh -c echo test" { + c.Fatalf("CMD did not contain /bin/sh -c : %q", out) + } + } + +} + +func (s *DockerSuite) TestBuildEnvironmentReplacementUser(c *check.C) { + // Windows does not support FROM scratch or the USER command + testRequires(c, DaemonIsLinux) + name := "testbuildenvironmentreplacement" + + _, err := buildImage(name, ` + FROM scratch + ENV user foo + USER ${user} + `, true) + if err != nil { + c.Fatal(err) + } + + res := inspectFieldJSON(c, name, "Config.User") + + if res != `"foo"` { + c.Fatal("User foo from environment not in Config.User on image") + } + +} + +func (s *DockerSuite) TestBuildEnvironmentReplacementVolume(c *check.C) { + name := "testbuildenvironmentreplacement" + + var volumePath string + + if daemonPlatform == "windows" { + volumePath = "c:/quux" + } else { + volumePath = "/quux" + } + + _, err := buildImage(name, ` + FROM `+minimalBaseImage()+` + ENV volume `+volumePath+` + VOLUME ${volume} + `, true) + if err != nil { + c.Fatal(err) + } + + res := inspectFieldJSON(c, name, "Config.Volumes") + + var volumes map[string]interface{} + + if err := json.Unmarshal([]byte(res), &volumes); err != nil { + c.Fatal(err) + } + + if _, ok := volumes[volumePath]; !ok { + c.Fatal("Volume " + volumePath + " from environment not in Config.Volumes on image") + } + +} + +func (s *DockerSuite) TestBuildEnvironmentReplacementExpose(c *check.C) { + // Windows does not support FROM scratch or the EXPOSE command + testRequires(c, DaemonIsLinux) + name := "testbuildenvironmentreplacement" + + _, err := buildImage(name, ` + FROM scratch + ENV port 80 + EXPOSE ${port} + ENV ports " 99 100 " + EXPOSE ${ports} + `, true) + if err != nil { + c.Fatal(err) + } + + res := inspectFieldJSON(c, name, "Config.ExposedPorts") + + var exposedPorts map[string]interface{} + + if err := json.Unmarshal([]byte(res), &exposedPorts); err != nil { + c.Fatal(err) + } + + exp := []int{80, 99, 100} + + for _, p := range exp { + tmp := fmt.Sprintf("%d/tcp", p) + if _, ok := exposedPorts[tmp]; !ok { + c.Fatalf("Exposed port %d from environment not in Config.ExposedPorts on image", p) + } + } + +} + +func (s *DockerSuite) TestBuildEnvironmentReplacementWorkdir(c *check.C) { + name := "testbuildenvironmentreplacement" + + _, err := buildImage(name, ` + FROM busybox + ENV MYWORKDIR /work + RUN mkdir ${MYWORKDIR} + WORKDIR ${MYWORKDIR} + `, true) + + if err != nil { + c.Fatal(err) + } + +} + +func (s *DockerSuite) TestBuildEnvironmentReplacementAddCopy(c *check.C) { + name := "testbuildenvironmentreplacement" + + ctx, err := fakeContext(` + FROM `+minimalBaseImage()+` + ENV baz foo + ENV quux bar + ENV dot . + ENV fee fff + ENV gee ggg + + ADD ${baz} ${dot} + COPY ${quux} ${dot} + ADD ${zzz:-${fee}} ${dot} + COPY ${zzz:-${gee}} ${dot} + `, + map[string]string{ + "foo": "test1", + "bar": "test2", + "fff": "test3", + "ggg": "test4", + }) + + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } + +} + +func (s *DockerSuite) TestBuildEnvironmentReplacementEnv(c *check.C) { + // ENV expansions work differently in Windows + testRequires(c, DaemonIsLinux) + name := "testbuildenvironmentreplacement" + + _, err := buildImage(name, + ` + FROM busybox + ENV foo zzz + ENV bar ${foo} + ENV abc1='$foo' + ENV env1=$foo env2=${foo} env3="$foo" env4="${foo}" + RUN [ "$abc1" = '$foo' ] && (echo "$abc1" | grep -q foo) + ENV abc2="\$foo" + RUN [ "$abc2" = '$foo' ] && (echo "$abc2" | grep -q foo) + ENV abc3 '$foo' + RUN [ "$abc3" = '$foo' ] && (echo "$abc3" | grep -q foo) + ENV abc4 "\$foo" + RUN [ "$abc4" = '$foo' ] && (echo "$abc4" | grep -q foo) + `, true) + + if err != nil { + c.Fatal(err) + } + + res := inspectFieldJSON(c, name, "Config.Env") + + envResult := []string{} + + if err = unmarshalJSON([]byte(res), &envResult); err != nil { + c.Fatal(err) + } + + found := false + envCount := 0 + + for _, env := range envResult { + parts := strings.SplitN(env, "=", 2) + if parts[0] == "bar" { + found = true + if parts[1] != "zzz" { + c.Fatalf("Could not find replaced var for env `bar`: got %q instead of `zzz`", parts[1]) + } + } else if strings.HasPrefix(parts[0], "env") { + envCount++ + if parts[1] != "zzz" { + c.Fatalf("%s should be 'foo' but instead its %q", parts[0], parts[1]) + } + } else if strings.HasPrefix(parts[0], "env") { + envCount++ + if parts[1] != "foo" { + c.Fatalf("%s should be 'foo' but instead its %q", parts[0], parts[1]) + } + } + } + + if !found { + c.Fatal("Never found the `bar` env variable") + } + + if envCount != 4 { + c.Fatalf("Didn't find all env vars - only saw %d\n%s", envCount, envResult) + } + +} + +func (s *DockerSuite) TestBuildHandleEscapes(c *check.C) { + // The volume paths used in this test are invalid on Windows + testRequires(c, DaemonIsLinux) + name := "testbuildhandleescapes" + + _, err := buildImage(name, + ` + FROM scratch + ENV FOO bar + VOLUME ${FOO} + `, true) + + if err != nil { + c.Fatal(err) + } + + var result map[string]map[string]struct{} + + res := inspectFieldJSON(c, name, "Config.Volumes") + + if err = unmarshalJSON([]byte(res), &result); err != nil { + c.Fatal(err) + } + + if _, ok := result["bar"]; !ok { + c.Fatal("Could not find volume bar set from env foo in volumes table") + } + + deleteImages(name) + + _, err = buildImage(name, + ` + FROM scratch + ENV FOO bar + VOLUME \${FOO} + `, true) + + if err != nil { + c.Fatal(err) + } + + res = inspectFieldJSON(c, name, "Config.Volumes") + + if err = unmarshalJSON([]byte(res), &result); err != nil { + c.Fatal(err) + } + + if _, ok := result["${FOO}"]; !ok { + c.Fatal("Could not find volume ${FOO} set from env foo in volumes table") + } + + deleteImages(name) + + // this test in particular provides *7* backslashes and expects 6 to come back. + // Like above, the first escape is swallowed and the rest are treated as + // literals, this one is just less obvious because of all the character noise. + + _, err = buildImage(name, + ` + FROM scratch + ENV FOO bar + VOLUME \\\\\\\${FOO} + `, true) + + if err != nil { + c.Fatal(err) + } + + res = inspectFieldJSON(c, name, "Config.Volumes") + + if err = unmarshalJSON([]byte(res), &result); err != nil { + c.Fatal(err) + } + + if _, ok := result[`\\\${FOO}`]; !ok { + c.Fatal(`Could not find volume \\\${FOO} set from env foo in volumes table`, result) + } + +} + +func (s *DockerSuite) TestBuildOnBuildLowercase(c *check.C) { + name := "testbuildonbuildlowercase" + name2 := "testbuildonbuildlowercase2" + + _, err := buildImage(name, + ` + FROM busybox + onbuild run echo quux + `, true) + + if err != nil { + c.Fatal(err) + } + + _, out, err := buildImageWithOut(name2, fmt.Sprintf(` + FROM %s + `, name), true) + + if err != nil { + c.Fatal(err) + } + + if !strings.Contains(out, "quux") { + c.Fatalf("Did not receive the expected echo text, got %s", out) + } + + if strings.Contains(out, "ONBUILD ONBUILD") { + c.Fatalf("Got an ONBUILD ONBUILD error with no error: got %s", out) + } + +} + +func (s *DockerSuite) TestBuildEnvEscapes(c *check.C) { + // ENV expansions work differently in Windows + testRequires(c, DaemonIsLinux) + name := "testbuildenvescapes" + _, err := buildImage(name, + ` + FROM busybox + ENV TEST foo + CMD echo \$ + `, + true) + + if err != nil { + c.Fatal(err) + } + + out, _ := dockerCmd(c, "run", "-t", name) + + if strings.TrimSpace(out) != "$" { + c.Fatalf("Env TEST was not overwritten with bar when foo was supplied to dockerfile: was %q", strings.TrimSpace(out)) + } + +} + +func (s *DockerSuite) TestBuildEnvOverwrite(c *check.C) { + // ENV expansions work differently in Windows + testRequires(c, DaemonIsLinux) + name := "testbuildenvoverwrite" + + _, err := buildImage(name, + ` + FROM busybox + ENV TEST foo + CMD echo ${TEST} + `, + true) + + if err != nil { + c.Fatal(err) + } + + out, _ := dockerCmd(c, "run", "-e", "TEST=bar", "-t", name) + + if strings.TrimSpace(out) != "bar" { + c.Fatalf("Env TEST was not overwritten with bar when foo was supplied to dockerfile: was %q", strings.TrimSpace(out)) + } + +} + +func (s *DockerSuite) TestBuildOnBuildForbiddenMaintainerInSourceImage(c *check.C) { + name := "testbuildonbuildforbiddenmaintainerinsourceimage" + + out, _ := dockerCmd(c, "create", "busybox", "true") + + cleanedContainerID := strings.TrimSpace(out) + + dockerCmd(c, "commit", "--run", "{\"OnBuild\":[\"MAINTAINER docker.io\"]}", cleanedContainerID, "onbuild") + + _, err := buildImage(name, + `FROM onbuild`, + true) + if err != nil { + if !strings.Contains(err.Error(), "maintainer isn't allowed as an ONBUILD trigger") { + c.Fatalf("Wrong error %v, must be about MAINTAINER and ONBUILD in source image", err) + } + } else { + c.Fatal("Error must not be nil") + } + +} + +func (s *DockerSuite) TestBuildOnBuildForbiddenFromInSourceImage(c *check.C) { + name := "testbuildonbuildforbiddenfrominsourceimage" + + out, _ := dockerCmd(c, "create", "busybox", "true") + + cleanedContainerID := strings.TrimSpace(out) + + dockerCmd(c, "commit", "--run", "{\"OnBuild\":[\"FROM busybox\"]}", cleanedContainerID, "onbuild") + + _, err := buildImage(name, + `FROM onbuild`, + true) + if err != nil { + if !strings.Contains(err.Error(), "from isn't allowed as an ONBUILD trigger") { + c.Fatalf("Wrong error %v, must be about FROM and ONBUILD in source image", err) + } + } else { + c.Fatal("Error must not be nil") + } + +} + +func (s *DockerSuite) TestBuildOnBuildForbiddenChainedInSourceImage(c *check.C) { + name := "testbuildonbuildforbiddenchainedinsourceimage" + + out, _ := dockerCmd(c, "create", "busybox", "true") + + cleanedContainerID := strings.TrimSpace(out) + + dockerCmd(c, "commit", "--run", "{\"OnBuild\":[\"ONBUILD RUN ls\"]}", cleanedContainerID, "onbuild") + + _, err := buildImage(name, + `FROM onbuild`, + true) + if err != nil { + if !strings.Contains(err.Error(), "Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed") { + c.Fatalf("Wrong error %v, must be about chaining ONBUILD in source image", err) + } + } else { + c.Fatal("Error must not be nil") + } + +} + +func (s *DockerSuite) TestBuildOnBuildCmdEntrypointJSON(c *check.C) { + name1 := "onbuildcmd" + name2 := "onbuildgenerated" + + _, err := buildImage(name1, ` +FROM busybox +ONBUILD CMD ["hello world"] +ONBUILD ENTRYPOINT ["echo"] +ONBUILD RUN ["true"]`, + false) + + if err != nil { + c.Fatal(err) + } + + _, err = buildImage(name2, fmt.Sprintf(`FROM %s`, name1), false) + + if err != nil { + c.Fatal(err) + } + + out, _ := dockerCmd(c, "run", name2) + + if !regexp.MustCompile(`(?m)^hello world`).MatchString(out) { + c.Fatalf("did not get echo output from onbuild. Got: %q", out) + } + +} + +func (s *DockerSuite) TestBuildOnBuildEntrypointJSON(c *check.C) { + name1 := "onbuildcmd" + name2 := "onbuildgenerated" + + _, err := buildImage(name1, ` +FROM busybox +ONBUILD ENTRYPOINT ["echo"]`, + false) + + if err != nil { + c.Fatal(err) + } + + _, err = buildImage(name2, fmt.Sprintf("FROM %s\nCMD [\"hello world\"]\n", name1), false) + + if err != nil { + c.Fatal(err) + } + + out, _ := dockerCmd(c, "run", name2) + + if !regexp.MustCompile(`(?m)^hello world`).MatchString(out) { + c.Fatal("got malformed output from onbuild", out) + } + +} + +func (s *DockerSuite) TestBuildCacheAdd(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows doesn't have httpserver image yet + name := "testbuildtwoimageswithadd" + server, err := fakeStorage(map[string]string{ + "robots.txt": "hello", + "index.html": "world", + }) + if err != nil { + c.Fatal(err) + } + defer server.Close() + + if _, err := buildImage(name, + fmt.Sprintf(`FROM scratch + ADD %s/robots.txt /`, server.URL()), + true); err != nil { + c.Fatal(err) + } + if err != nil { + c.Fatal(err) + } + deleteImages(name) + _, out, err := buildImageWithOut(name, + fmt.Sprintf(`FROM scratch + ADD %s/index.html /`, server.URL()), + true) + if err != nil { + c.Fatal(err) + } + if strings.Contains(out, "Using cache") { + c.Fatal("2nd build used cache on ADD, it shouldn't") + } + +} + +func (s *DockerSuite) TestBuildLastModified(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows doesn't have httpserver image yet + name := "testbuildlastmodified" + + server, err := fakeStorage(map[string]string{ + "file": "hello", + }) + if err != nil { + c.Fatal(err) + } + defer server.Close() + + var out, out2 string + + dFmt := `FROM busybox +ADD %s/file / +RUN ls -le /file` + + dockerfile := fmt.Sprintf(dFmt, server.URL()) + + if _, out, err = buildImageWithOut(name, dockerfile, false); err != nil { + c.Fatal(err) + } + + originMTime := regexp.MustCompile(`root.*/file.*\n`).FindString(out) + // Make sure our regexp is correct + if strings.Index(originMTime, "/file") < 0 { + c.Fatalf("Missing ls info on 'file':\n%s", out) + } + + // Build it again and make sure the mtime of the file didn't change. + // Wait a few seconds to make sure the time changed enough to notice + time.Sleep(2 * time.Second) + + if _, out2, err = buildImageWithOut(name, dockerfile, false); err != nil { + c.Fatal(err) + } + + newMTime := regexp.MustCompile(`root.*/file.*\n`).FindString(out2) + if newMTime != originMTime { + c.Fatalf("MTime changed:\nOrigin:%s\nNew:%s", originMTime, newMTime) + } + + // Now 'touch' the file and make sure the timestamp DID change this time + // Create a new fakeStorage instead of just using Add() to help windows + server, err = fakeStorage(map[string]string{ + "file": "hello", + }) + if err != nil { + c.Fatal(err) + } + defer server.Close() + + dockerfile = fmt.Sprintf(dFmt, server.URL()) + + if _, out2, err = buildImageWithOut(name, dockerfile, false); err != nil { + c.Fatal(err) + } + + newMTime = regexp.MustCompile(`root.*/file.*\n`).FindString(out2) + if newMTime == originMTime { + c.Fatalf("MTime didn't change:\nOrigin:%s\nNew:%s", originMTime, newMTime) + } + +} + +func (s *DockerSuite) TestBuildSixtySteps(c *check.C) { + testRequires(c, DaemonIsLinux) // TODO Windows: This test passes on Windows, + // but currently adds a disproportionate amount of time for the value it has. + // Removing it from Windows CI for now, but this will be revisited in the + // TP5 timeframe when perf is better. + name := "foobuildsixtysteps" + + ctx, err := fakeContext("FROM "+minimalBaseImage()+"\n"+strings.Repeat("ADD foo /\n", 60), + map[string]string{ + "foo": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildAddSingleFileToRoot(c *check.C) { + testRequires(c, DaemonIsLinux) // Linux specific test + name := "testaddimg" + ctx, err := fakeContext(fmt.Sprintf(`FROM busybox +RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd +RUN echo 'dockerio:x:1001:' >> /etc/group +RUN touch /exists +RUN chown dockerio.dockerio /exists +ADD test_file / +RUN [ $(ls -l /test_file | awk '{print $3":"$4}') = 'root:root' ] +RUN [ $(ls -l /test_file | awk '{print $1}') = '%s' ] +RUN [ $(ls -l /exists | awk '{print $3":"$4}') = 'dockerio:dockerio' ]`, expectedFileChmod), + map[string]string{ + "test_file": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +// Issue #3960: "ADD src ." hangs +func (s *DockerSuite) TestBuildAddSingleFileToWorkdir(c *check.C) { + name := "testaddsinglefiletoworkdir" + ctx, err := fakeContext(`FROM busybox +ADD test_file .`, + map[string]string{ + "test_file": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + errChan := make(chan error) + go func() { + _, err := buildImageFromContext(name, ctx, true) + errChan <- err + close(errChan) + }() + select { + case <-time.After(15 * time.Second): + c.Fatal("Build with adding to workdir timed out") + case err := <-errChan: + c.Assert(err, check.IsNil) + } +} + +func (s *DockerSuite) TestBuildAddSingleFileToExistDir(c *check.C) { + testRequires(c, DaemonIsLinux) // Linux specific test + name := "testaddsinglefiletoexistdir" + ctx, err := fakeContext(`FROM busybox +RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd +RUN echo 'dockerio:x:1001:' >> /etc/group +RUN mkdir /exists +RUN touch /exists/exists_file +RUN chown -R dockerio.dockerio /exists +ADD test_file /exists/ +RUN [ $(ls -l / | grep exists | awk '{print $3":"$4}') = 'dockerio:dockerio' ] +RUN [ $(ls -l /exists/test_file | awk '{print $3":"$4}') = 'root:root' ] +RUN [ $(ls -l /exists/exists_file | awk '{print $3":"$4}') = 'dockerio:dockerio' ]`, + map[string]string{ + "test_file": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildCopyAddMultipleFiles(c *check.C) { + testRequires(c, DaemonIsLinux) // Linux specific test + server, err := fakeStorage(map[string]string{ + "robots.txt": "hello", + }) + if err != nil { + c.Fatal(err) + } + defer server.Close() + + name := "testcopymultiplefilestofile" + ctx, err := fakeContext(fmt.Sprintf(`FROM busybox +RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd +RUN echo 'dockerio:x:1001:' >> /etc/group +RUN mkdir /exists +RUN touch /exists/exists_file +RUN chown -R dockerio.dockerio /exists +COPY test_file1 test_file2 /exists/ +ADD test_file3 test_file4 %s/robots.txt /exists/ +RUN [ $(ls -l / | grep exists | awk '{print $3":"$4}') = 'dockerio:dockerio' ] +RUN [ $(ls -l /exists/test_file1 | awk '{print $3":"$4}') = 'root:root' ] +RUN [ $(ls -l /exists/test_file2 | awk '{print $3":"$4}') = 'root:root' ] + +RUN [ $(ls -l /exists/test_file3 | awk '{print $3":"$4}') = 'root:root' ] +RUN [ $(ls -l /exists/test_file4 | awk '{print $3":"$4}') = 'root:root' ] +RUN [ $(ls -l /exists/robots.txt | awk '{print $3":"$4}') = 'root:root' ] + +RUN [ $(ls -l /exists/exists_file | awk '{print $3":"$4}') = 'dockerio:dockerio' ] +`, server.URL()), + map[string]string{ + "test_file1": "test1", + "test_file2": "test2", + "test_file3": "test3", + "test_file4": "test4", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +// This test is mainly for user namespaces to verify that new directories +// are created as the remapped root uid/gid pair +func (s *DockerSuite) TestBuildAddToNewDestination(c *check.C) { + testRequires(c, DaemonIsLinux) // Linux specific test + name := "testaddtonewdest" + ctx, err := fakeContext(`FROM busybox +ADD . /new_dir +RUN ls -l / +RUN [ $(ls -l / | grep new_dir | awk '{print $3":"$4}') = 'root:root' ]`, + map[string]string{ + "test_dir/test_file": "test file", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +// This test is mainly for user namespaces to verify that new directories +// are created as the remapped root uid/gid pair +func (s *DockerSuite) TestBuildCopyToNewParentDirectory(c *check.C) { + testRequires(c, DaemonIsLinux) // Linux specific test + name := "testcopytonewdir" + ctx, err := fakeContext(`FROM busybox +COPY test_dir /new_dir +RUN ls -l /new_dir +RUN [ $(ls -l / | grep new_dir | awk '{print $3":"$4}') = 'root:root' ]`, + map[string]string{ + "test_dir/test_file": "test file", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +// This test is mainly for user namespaces to verify that new directories +// are created as the remapped root uid/gid pair +func (s *DockerSuite) TestBuildWorkdirIsContainerRoot(c *check.C) { + testRequires(c, DaemonIsLinux) // Linux specific test + name := "testworkdirownership" + if _, err := buildImage(name, `FROM busybox +WORKDIR /new_dir +RUN ls -l / +RUN [ $(ls -l / | grep new_dir | awk '{print $3":"$4}') = 'root:root' ]`, true); err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildAddMultipleFilesToFile(c *check.C) { + name := "testaddmultiplefilestofile" + + ctx, err := fakeContext(`FROM `+minimalBaseImage()+` + ADD file1.txt file2.txt test + `, + map[string]string{ + "file1.txt": "test1", + "file2.txt": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + expected := "When using ADD with more than one source file, the destination must be a directory and end with a /" + if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) { + c.Fatalf("Wrong error: (should contain %q) got:\n%v", expected, err) + } + +} + +func (s *DockerSuite) TestBuildJSONAddMultipleFilesToFile(c *check.C) { + name := "testjsonaddmultiplefilestofile" + + ctx, err := fakeContext(`FROM `+minimalBaseImage()+` + ADD ["file1.txt", "file2.txt", "test"] + `, + map[string]string{ + "file1.txt": "test1", + "file2.txt": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + expected := "When using ADD with more than one source file, the destination must be a directory and end with a /" + if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) { + c.Fatalf("Wrong error: (should contain %q) got:\n%v", expected, err) + } + +} + +func (s *DockerSuite) TestBuildAddMultipleFilesToFileWild(c *check.C) { + name := "testaddmultiplefilestofilewild" + + ctx, err := fakeContext(`FROM `+minimalBaseImage()+` + ADD file*.txt test + `, + map[string]string{ + "file1.txt": "test1", + "file2.txt": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + expected := "When using ADD with more than one source file, the destination must be a directory and end with a /" + if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) { + c.Fatalf("Wrong error: (should contain %q) got:\n%v", expected, err) + } + +} + +func (s *DockerSuite) TestBuildJSONAddMultipleFilesToFileWild(c *check.C) { + name := "testjsonaddmultiplefilestofilewild" + + ctx, err := fakeContext(`FROM `+minimalBaseImage()+` + ADD ["file*.txt", "test"] + `, + map[string]string{ + "file1.txt": "test1", + "file2.txt": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + expected := "When using ADD with more than one source file, the destination must be a directory and end with a /" + if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) { + c.Fatalf("Wrong error: (should contain %q) got:\n%v", expected, err) + } + +} + +func (s *DockerSuite) TestBuildCopyMultipleFilesToFile(c *check.C) { + name := "testcopymultiplefilestofile" + + ctx, err := fakeContext(`FROM `+minimalBaseImage()+` + COPY file1.txt file2.txt test + `, + map[string]string{ + "file1.txt": "test1", + "file2.txt": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + expected := "When using COPY with more than one source file, the destination must be a directory and end with a /" + if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) { + c.Fatalf("Wrong error: (should contain %q) got:\n%v", expected, err) + } + +} + +func (s *DockerSuite) TestBuildJSONCopyMultipleFilesToFile(c *check.C) { + name := "testjsoncopymultiplefilestofile" + + ctx, err := fakeContext(`FROM `+minimalBaseImage()+` + COPY ["file1.txt", "file2.txt", "test"] + `, + map[string]string{ + "file1.txt": "test1", + "file2.txt": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + expected := "When using COPY with more than one source file, the destination must be a directory and end with a /" + if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) { + c.Fatalf("Wrong error: (should contain %q) got:\n%v", expected, err) + } + +} + +func (s *DockerSuite) TestBuildAddFileWithWhitespace(c *check.C) { + testRequires(c, DaemonIsLinux) // Not currently passing on Windows + name := "testaddfilewithwhitespace" + ctx, err := fakeContext(`FROM busybox +RUN mkdir "/test dir" +RUN mkdir "/test_dir" +ADD [ "test file1", "/test_file1" ] +ADD [ "test_file2", "/test file2" ] +ADD [ "test file3", "/test file3" ] +ADD [ "test dir/test_file4", "/test_dir/test_file4" ] +ADD [ "test_dir/test_file5", "/test dir/test_file5" ] +ADD [ "test dir/test_file6", "/test dir/test_file6" ] +RUN [ $(cat "/test_file1") = 'test1' ] +RUN [ $(cat "/test file2") = 'test2' ] +RUN [ $(cat "/test file3") = 'test3' ] +RUN [ $(cat "/test_dir/test_file4") = 'test4' ] +RUN [ $(cat "/test dir/test_file5") = 'test5' ] +RUN [ $(cat "/test dir/test_file6") = 'test6' ]`, + map[string]string{ + "test file1": "test1", + "test_file2": "test2", + "test file3": "test3", + "test dir/test_file4": "test4", + "test_dir/test_file5": "test5", + "test dir/test_file6": "test6", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildCopyFileWithWhitespace(c *check.C) { + testRequires(c, DaemonIsLinux) // Not currently passing on Windows + name := "testcopyfilewithwhitespace" + ctx, err := fakeContext(`FROM busybox +RUN mkdir "/test dir" +RUN mkdir "/test_dir" +COPY [ "test file1", "/test_file1" ] +COPY [ "test_file2", "/test file2" ] +COPY [ "test file3", "/test file3" ] +COPY [ "test dir/test_file4", "/test_dir/test_file4" ] +COPY [ "test_dir/test_file5", "/test dir/test_file5" ] +COPY [ "test dir/test_file6", "/test dir/test_file6" ] +RUN [ $(cat "/test_file1") = 'test1' ] +RUN [ $(cat "/test file2") = 'test2' ] +RUN [ $(cat "/test file3") = 'test3' ] +RUN [ $(cat "/test_dir/test_file4") = 'test4' ] +RUN [ $(cat "/test dir/test_file5") = 'test5' ] +RUN [ $(cat "/test dir/test_file6") = 'test6' ]`, + map[string]string{ + "test file1": "test1", + "test_file2": "test2", + "test file3": "test3", + "test dir/test_file4": "test4", + "test_dir/test_file5": "test5", + "test dir/test_file6": "test6", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildAddMultipleFilesToFileWithWhitespace(c *check.C) { + name := "testaddmultiplefilestofilewithwhitespace" + ctx, err := fakeContext(`FROM busybox + ADD [ "test file1", "test file2", "test" ] + `, + map[string]string{ + "test file1": "test1", + "test file2": "test2", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + expected := "When using ADD with more than one source file, the destination must be a directory and end with a /" + if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) { + c.Fatalf("Wrong error: (should contain %q) got:\n%v", expected, err) + } + +} + +func (s *DockerSuite) TestBuildCopyMultipleFilesToFileWithWhitespace(c *check.C) { + name := "testcopymultiplefilestofilewithwhitespace" + ctx, err := fakeContext(`FROM busybox + COPY [ "test file1", "test file2", "test" ] + `, + map[string]string{ + "test file1": "test1", + "test file2": "test2", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + expected := "When using COPY with more than one source file, the destination must be a directory and end with a /" + if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) { + c.Fatalf("Wrong error: (should contain %q) got:\n%v", expected, err) + } + +} + +func (s *DockerSuite) TestBuildCopyWildcard(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows doesn't have httpserver image yet + name := "testcopywildcard" + server, err := fakeStorage(map[string]string{ + "robots.txt": "hello", + "index.html": "world", + }) + if err != nil { + c.Fatal(err) + } + defer server.Close() + + ctx, err := fakeContext(fmt.Sprintf(`FROM busybox + COPY file*.txt /tmp/ + RUN ls /tmp/file1.txt /tmp/file2.txt + RUN mkdir /tmp1 + COPY dir* /tmp1/ + RUN ls /tmp1/dirt /tmp1/nested_file /tmp1/nested_dir/nest_nest_file + RUN mkdir /tmp2 + ADD dir/*dir %s/robots.txt /tmp2/ + RUN ls /tmp2/nest_nest_file /tmp2/robots.txt + `, server.URL()), + map[string]string{ + "file1.txt": "test1", + "file2.txt": "test2", + "dir/nested_file": "nested file", + "dir/nested_dir/nest_nest_file": "2 times nested", + "dirt": "dirty", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + id1, err := buildImageFromContext(name, ctx, true) + if err != nil { + c.Fatal(err) + } + + // Now make sure we use a cache the 2nd time + id2, err := buildImageFromContext(name, ctx, true) + if err != nil { + c.Fatal(err) + } + + if id1 != id2 { + c.Fatal("didn't use the cache") + } + +} + +func (s *DockerSuite) TestBuildCopyWildcardNoFind(c *check.C) { + name := "testcopywildcardnofind" + ctx, err := fakeContext(`FROM busybox + COPY file*.txt /tmp/ + `, nil) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + _, err = buildImageFromContext(name, ctx, true) + if err == nil { + c.Fatal("should have failed to find a file") + } + if !strings.Contains(err.Error(), "No source files were specified") { + c.Fatalf("Wrong error %v, must be about no source files", err) + } + +} + +func (s *DockerSuite) TestBuildCopyWildcardInName(c *check.C) { + name := "testcopywildcardinname" + ctx, err := fakeContext(`FROM busybox + COPY *.txt /tmp/ + RUN [ "$(cat /tmp/\*.txt)" = 'hi there' ] + `, map[string]string{"*.txt": "hi there"}) + + if err != nil { + // Normally we would do c.Fatal(err) here but given that + // the odds of this failing are so rare, it must be because + // the OS we're running the client on doesn't support * in + // filenames (like windows). So, instead of failing the test + // just let it pass. Then we don't need to explicitly + // say which OSs this works on or not. + return + } + defer ctx.Close() + + _, err = buildImageFromContext(name, ctx, true) + if err != nil { + c.Fatalf("should have built: %q", err) + } +} + +func (s *DockerSuite) TestBuildCopyWildcardCache(c *check.C) { + name := "testcopywildcardcache" + ctx, err := fakeContext(`FROM busybox + COPY file1.txt /tmp/`, + map[string]string{ + "file1.txt": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + id1, err := buildImageFromContext(name, ctx, true) + if err != nil { + c.Fatal(err) + } + + // Now make sure we use a cache the 2nd time even with wild cards. + // Use the same context so the file is the same and the checksum will match + ctx.Add("Dockerfile", `FROM busybox + COPY file*.txt /tmp/`) + + id2, err := buildImageFromContext(name, ctx, true) + if err != nil { + c.Fatal(err) + } + + if id1 != id2 { + c.Fatal("didn't use the cache") + } + +} + +func (s *DockerSuite) TestBuildAddSingleFileToNonExistingDir(c *check.C) { + testRequires(c, DaemonIsLinux) // Linux specific test + name := "testaddsinglefiletononexistingdir" + ctx, err := fakeContext(`FROM busybox +RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd +RUN echo 'dockerio:x:1001:' >> /etc/group +RUN touch /exists +RUN chown dockerio.dockerio /exists +ADD test_file /test_dir/ +RUN [ $(ls -l / | grep test_dir | awk '{print $3":"$4}') = 'root:root' ] +RUN [ $(ls -l /test_dir/test_file | awk '{print $3":"$4}') = 'root:root' ] +RUN [ $(ls -l /exists | awk '{print $3":"$4}') = 'dockerio:dockerio' ]`, + map[string]string{ + "test_file": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } + +} + +func (s *DockerSuite) TestBuildAddDirContentToRoot(c *check.C) { + testRequires(c, DaemonIsLinux) // Linux specific test + name := "testadddircontenttoroot" + ctx, err := fakeContext(`FROM busybox +RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd +RUN echo 'dockerio:x:1001:' >> /etc/group +RUN touch /exists +RUN chown dockerio.dockerio exists +ADD test_dir / +RUN [ $(ls -l /test_file | awk '{print $3":"$4}') = 'root:root' ] +RUN [ $(ls -l /exists | awk '{print $3":"$4}') = 'dockerio:dockerio' ]`, + map[string]string{ + "test_dir/test_file": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildAddDirContentToExistingDir(c *check.C) { + testRequires(c, DaemonIsLinux) // Linux specific test + name := "testadddircontenttoexistingdir" + ctx, err := fakeContext(`FROM busybox +RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd +RUN echo 'dockerio:x:1001:' >> /etc/group +RUN mkdir /exists +RUN touch /exists/exists_file +RUN chown -R dockerio.dockerio /exists +ADD test_dir/ /exists/ +RUN [ $(ls -l / | grep exists | awk '{print $3":"$4}') = 'dockerio:dockerio' ] +RUN [ $(ls -l /exists/exists_file | awk '{print $3":"$4}') = 'dockerio:dockerio' ] +RUN [ $(ls -l /exists/test_file | awk '{print $3":"$4}') = 'root:root' ]`, + map[string]string{ + "test_dir/test_file": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildAddWholeDirToRoot(c *check.C) { + testRequires(c, DaemonIsLinux) // Linux specific test + name := "testaddwholedirtoroot" + ctx, err := fakeContext(fmt.Sprintf(`FROM busybox +RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd +RUN echo 'dockerio:x:1001:' >> /etc/group +RUN touch /exists +RUN chown dockerio.dockerio exists +ADD test_dir /test_dir +RUN [ $(ls -l / | grep test_dir | awk '{print $3":"$4}') = 'root:root' ] +RUN [ $(ls -l / | grep test_dir | awk '{print $1}') = 'drwxr-xr-x' ] +RUN [ $(ls -l /test_dir/test_file | awk '{print $3":"$4}') = 'root:root' ] +RUN [ $(ls -l /test_dir/test_file | awk '{print $1}') = '%s' ] +RUN [ $(ls -l /exists | awk '{print $3":"$4}') = 'dockerio:dockerio' ]`, expectedFileChmod), + map[string]string{ + "test_dir/test_file": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +// Testing #5941 +func (s *DockerSuite) TestBuildAddEtcToRoot(c *check.C) { + name := "testaddetctoroot" + + ctx, err := fakeContext(`FROM `+minimalBaseImage()+` +ADD . /`, + map[string]string{ + "etc/test_file": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +// Testing #9401 +func (s *DockerSuite) TestBuildAddPreservesFilesSpecialBits(c *check.C) { + testRequires(c, DaemonIsLinux) // Linux specific test + name := "testaddpreservesfilesspecialbits" + ctx, err := fakeContext(`FROM busybox +ADD suidbin /usr/bin/suidbin +RUN chmod 4755 /usr/bin/suidbin +RUN [ $(ls -l /usr/bin/suidbin | awk '{print $1}') = '-rwsr-xr-x' ] +ADD ./data/ / +RUN [ $(ls -l /usr/bin/suidbin | awk '{print $1}') = '-rwsr-xr-x' ]`, + map[string]string{ + "suidbin": "suidbin", + "/data/usr/test_file": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildCopySingleFileToRoot(c *check.C) { + testRequires(c, DaemonIsLinux) // Linux specific test + name := "testcopysinglefiletoroot" + ctx, err := fakeContext(fmt.Sprintf(`FROM busybox +RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd +RUN echo 'dockerio:x:1001:' >> /etc/group +RUN touch /exists +RUN chown dockerio.dockerio /exists +COPY test_file / +RUN [ $(ls -l /test_file | awk '{print $3":"$4}') = 'root:root' ] +RUN [ $(ls -l /test_file | awk '{print $1}') = '%s' ] +RUN [ $(ls -l /exists | awk '{print $3":"$4}') = 'dockerio:dockerio' ]`, expectedFileChmod), + map[string]string{ + "test_file": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +// Issue #3960: "ADD src ." hangs - adapted for COPY +func (s *DockerSuite) TestBuildCopySingleFileToWorkdir(c *check.C) { + name := "testcopysinglefiletoworkdir" + ctx, err := fakeContext(`FROM busybox +COPY test_file .`, + map[string]string{ + "test_file": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + errChan := make(chan error) + go func() { + _, err := buildImageFromContext(name, ctx, true) + errChan <- err + close(errChan) + }() + select { + case <-time.After(15 * time.Second): + c.Fatal("Build with adding to workdir timed out") + case err := <-errChan: + c.Assert(err, check.IsNil) + } +} + +func (s *DockerSuite) TestBuildCopySingleFileToExistDir(c *check.C) { + testRequires(c, DaemonIsLinux) // Linux specific test + name := "testcopysinglefiletoexistdir" + ctx, err := fakeContext(`FROM busybox +RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd +RUN echo 'dockerio:x:1001:' >> /etc/group +RUN mkdir /exists +RUN touch /exists/exists_file +RUN chown -R dockerio.dockerio /exists +COPY test_file /exists/ +RUN [ $(ls -l / | grep exists | awk '{print $3":"$4}') = 'dockerio:dockerio' ] +RUN [ $(ls -l /exists/test_file | awk '{print $3":"$4}') = 'root:root' ] +RUN [ $(ls -l /exists/exists_file | awk '{print $3":"$4}') = 'dockerio:dockerio' ]`, + map[string]string{ + "test_file": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildCopySingleFileToNonExistDir(c *check.C) { + testRequires(c, DaemonIsLinux) // Linux specific test + name := "testcopysinglefiletononexistdir" + ctx, err := fakeContext(`FROM busybox +RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd +RUN echo 'dockerio:x:1001:' >> /etc/group +RUN touch /exists +RUN chown dockerio.dockerio /exists +COPY test_file /test_dir/ +RUN [ $(ls -l / | grep test_dir | awk '{print $3":"$4}') = 'root:root' ] +RUN [ $(ls -l /test_dir/test_file | awk '{print $3":"$4}') = 'root:root' ] +RUN [ $(ls -l /exists | awk '{print $3":"$4}') = 'dockerio:dockerio' ]`, + map[string]string{ + "test_file": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildCopyDirContentToRoot(c *check.C) { + testRequires(c, DaemonIsLinux) // Linux specific test + name := "testcopydircontenttoroot" + ctx, err := fakeContext(`FROM busybox +RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd +RUN echo 'dockerio:x:1001:' >> /etc/group +RUN touch /exists +RUN chown dockerio.dockerio exists +COPY test_dir / +RUN [ $(ls -l /test_file | awk '{print $3":"$4}') = 'root:root' ] +RUN [ $(ls -l /exists | awk '{print $3":"$4}') = 'dockerio:dockerio' ]`, + map[string]string{ + "test_dir/test_file": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildCopyDirContentToExistDir(c *check.C) { + testRequires(c, DaemonIsLinux) // Linux specific test + name := "testcopydircontenttoexistdir" + ctx, err := fakeContext(`FROM busybox +RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd +RUN echo 'dockerio:x:1001:' >> /etc/group +RUN mkdir /exists +RUN touch /exists/exists_file +RUN chown -R dockerio.dockerio /exists +COPY test_dir/ /exists/ +RUN [ $(ls -l / | grep exists | awk '{print $3":"$4}') = 'dockerio:dockerio' ] +RUN [ $(ls -l /exists/exists_file | awk '{print $3":"$4}') = 'dockerio:dockerio' ] +RUN [ $(ls -l /exists/test_file | awk '{print $3":"$4}') = 'root:root' ]`, + map[string]string{ + "test_dir/test_file": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildCopyWholeDirToRoot(c *check.C) { + testRequires(c, DaemonIsLinux) // Linux specific test + name := "testcopywholedirtoroot" + ctx, err := fakeContext(fmt.Sprintf(`FROM busybox +RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd +RUN echo 'dockerio:x:1001:' >> /etc/group +RUN touch /exists +RUN chown dockerio.dockerio exists +COPY test_dir /test_dir +RUN [ $(ls -l / | grep test_dir | awk '{print $3":"$4}') = 'root:root' ] +RUN [ $(ls -l / | grep test_dir | awk '{print $1}') = 'drwxr-xr-x' ] +RUN [ $(ls -l /test_dir/test_file | awk '{print $3":"$4}') = 'root:root' ] +RUN [ $(ls -l /test_dir/test_file | awk '{print $1}') = '%s' ] +RUN [ $(ls -l /exists | awk '{print $3":"$4}') = 'dockerio:dockerio' ]`, expectedFileChmod), + map[string]string{ + "test_dir/test_file": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildCopyEtcToRoot(c *check.C) { + name := "testcopyetctoroot" + + ctx, err := fakeContext(`FROM `+minimalBaseImage()+` +COPY . /`, + map[string]string{ + "etc/test_file": "test1", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildCopyDisallowRemote(c *check.C) { + name := "testcopydisallowremote" + + _, out, err := buildImageWithOut(name, `FROM `+minimalBaseImage()+` +COPY https://index.docker.io/robots.txt /`, + true) + if err == nil || !strings.Contains(out, "Source can't be a URL for COPY") { + c.Fatalf("Error should be about disallowed remote source, got err: %s, out: %q", err, out) + } +} + +func (s *DockerSuite) TestBuildAddBadLinks(c *check.C) { + testRequires(c, DaemonIsLinux) // Not currently working on Windows + + dockerfile := ` + FROM scratch + ADD links.tar / + ADD foo.txt /symlink/ + ` + targetFile := "foo.txt" + var ( + name = "test-link-absolute" + ) + ctx, err := fakeContext(dockerfile, nil) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + tempDir, err := ioutil.TempDir("", "test-link-absolute-temp-") + if err != nil { + c.Fatalf("failed to create temporary directory: %s", tempDir) + } + defer os.RemoveAll(tempDir) + + var symlinkTarget string + if runtime.GOOS == "windows" { + var driveLetter string + if abs, err := filepath.Abs(tempDir); err != nil { + c.Fatal(err) + } else { + driveLetter = abs[:1] + } + tempDirWithoutDrive := tempDir[2:] + symlinkTarget = fmt.Sprintf(`%s:\..\..\..\..\..\..\..\..\..\..\..\..%s`, driveLetter, tempDirWithoutDrive) + } else { + symlinkTarget = fmt.Sprintf("/../../../../../../../../../../../..%s", tempDir) + } + + tarPath := filepath.Join(ctx.Dir, "links.tar") + nonExistingFile := filepath.Join(tempDir, targetFile) + fooPath := filepath.Join(ctx.Dir, targetFile) + + tarOut, err := os.Create(tarPath) + if err != nil { + c.Fatal(err) + } + + tarWriter := tar.NewWriter(tarOut) + + header := &tar.Header{ + Name: "symlink", + Typeflag: tar.TypeSymlink, + Linkname: symlinkTarget, + Mode: 0755, + Uid: 0, + Gid: 0, + } + + err = tarWriter.WriteHeader(header) + if err != nil { + c.Fatal(err) + } + + tarWriter.Close() + tarOut.Close() + + foo, err := os.Create(fooPath) + if err != nil { + c.Fatal(err) + } + defer foo.Close() + + if _, err := foo.WriteString("test"); err != nil { + c.Fatal(err) + } + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } + + if _, err := os.Stat(nonExistingFile); err == nil || err != nil && !os.IsNotExist(err) { + c.Fatalf("%s shouldn't have been written and it shouldn't exist", nonExistingFile) + } + +} + +func (s *DockerSuite) TestBuildAddBadLinksVolume(c *check.C) { + testRequires(c, DaemonIsLinux) // ln not implemented on Windows busybox + const ( + dockerfileTemplate = ` + FROM busybox + RUN ln -s /../../../../../../../../%s /x + VOLUME /x + ADD foo.txt /x/` + targetFile = "foo.txt" + ) + var ( + name = "test-link-absolute-volume" + dockerfile = "" + ) + + tempDir, err := ioutil.TempDir("", "test-link-absolute-volume-temp-") + if err != nil { + c.Fatalf("failed to create temporary directory: %s", tempDir) + } + defer os.RemoveAll(tempDir) + + dockerfile = fmt.Sprintf(dockerfileTemplate, tempDir) + nonExistingFile := filepath.Join(tempDir, targetFile) + + ctx, err := fakeContext(dockerfile, nil) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + fooPath := filepath.Join(ctx.Dir, targetFile) + + foo, err := os.Create(fooPath) + if err != nil { + c.Fatal(err) + } + defer foo.Close() + + if _, err := foo.WriteString("test"); err != nil { + c.Fatal(err) + } + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } + + if _, err := os.Stat(nonExistingFile); err == nil || err != nil && !os.IsNotExist(err) { + c.Fatalf("%s shouldn't have been written and it shouldn't exist", nonExistingFile) + } + +} + +// Issue #5270 - ensure we throw a better error than "unexpected EOF" +// when we can't access files in the context. +func (s *DockerSuite) TestBuildWithInaccessibleFilesInContext(c *check.C) { + testRequires(c, DaemonIsLinux, UnixCli) // test uses chown/chmod: not available on windows + + { + name := "testbuildinaccessiblefiles" + ctx, err := fakeContext("FROM scratch\nADD . /foo/", map[string]string{"fileWithoutReadAccess": "foo"}) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + // This is used to ensure we detect inaccessible files early during build in the cli client + pathToFileWithoutReadAccess := filepath.Join(ctx.Dir, "fileWithoutReadAccess") + + if err = os.Chown(pathToFileWithoutReadAccess, 0, 0); err != nil { + c.Fatalf("failed to chown file to root: %s", err) + } + if err = os.Chmod(pathToFileWithoutReadAccess, 0700); err != nil { + c.Fatalf("failed to chmod file to 700: %s", err) + } + buildCmd := exec.Command("su", "unprivilegeduser", "-c", fmt.Sprintf("%s build -t %s .", dockerBinary, name)) + buildCmd.Dir = ctx.Dir + out, _, err := runCommandWithOutput(buildCmd) + if err == nil { + c.Fatalf("build should have failed: %s %s", err, out) + } + + // check if we've detected the failure before we started building + if !strings.Contains(out, "no permission to read from ") { + c.Fatalf("output should've contained the string: no permission to read from but contained: %s", out) + } + + if !strings.Contains(out, "Error checking context") { + c.Fatalf("output should've contained the string: Error checking context") + } + } + { + name := "testbuildinaccessibledirectory" + ctx, err := fakeContext("FROM scratch\nADD . /foo/", map[string]string{"directoryWeCantStat/bar": "foo"}) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + // This is used to ensure we detect inaccessible directories early during build in the cli client + pathToDirectoryWithoutReadAccess := filepath.Join(ctx.Dir, "directoryWeCantStat") + pathToFileInDirectoryWithoutReadAccess := filepath.Join(pathToDirectoryWithoutReadAccess, "bar") + + if err = os.Chown(pathToDirectoryWithoutReadAccess, 0, 0); err != nil { + c.Fatalf("failed to chown directory to root: %s", err) + } + if err = os.Chmod(pathToDirectoryWithoutReadAccess, 0444); err != nil { + c.Fatalf("failed to chmod directory to 444: %s", err) + } + if err = os.Chmod(pathToFileInDirectoryWithoutReadAccess, 0700); err != nil { + c.Fatalf("failed to chmod file to 700: %s", err) + } + + buildCmd := exec.Command("su", "unprivilegeduser", "-c", fmt.Sprintf("%s build -t %s .", dockerBinary, name)) + buildCmd.Dir = ctx.Dir + out, _, err := runCommandWithOutput(buildCmd) + if err == nil { + c.Fatalf("build should have failed: %s %s", err, out) + } + + // check if we've detected the failure before we started building + if !strings.Contains(out, "can't stat") { + c.Fatalf("output should've contained the string: can't access %s", out) + } + + if !strings.Contains(out, "Error checking context") { + c.Fatalf("output should've contained the string: Error checking context\ngot:%s", out) + } + + } + { + name := "testlinksok" + ctx, err := fakeContext("FROM scratch\nADD . /foo/", nil) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + target := "../../../../../../../../../../../../../../../../../../../azA" + if err := os.Symlink(filepath.Join(ctx.Dir, "g"), target); err != nil { + c.Fatal(err) + } + defer os.Remove(target) + // This is used to ensure we don't follow links when checking if everything in the context is accessible + // This test doesn't require that we run commands as an unprivileged user + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } + } + { + name := "testbuildignoredinaccessible" + ctx, err := fakeContext("FROM scratch\nADD . /foo/", + map[string]string{ + "directoryWeCantStat/bar": "foo", + ".dockerignore": "directoryWeCantStat", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + // This is used to ensure we don't try to add inaccessible files when they are ignored by a .dockerignore pattern + pathToDirectoryWithoutReadAccess := filepath.Join(ctx.Dir, "directoryWeCantStat") + pathToFileInDirectoryWithoutReadAccess := filepath.Join(pathToDirectoryWithoutReadAccess, "bar") + if err = os.Chown(pathToDirectoryWithoutReadAccess, 0, 0); err != nil { + c.Fatalf("failed to chown directory to root: %s", err) + } + if err = os.Chmod(pathToDirectoryWithoutReadAccess, 0444); err != nil { + c.Fatalf("failed to chmod directory to 755: %s", err) + } + if err = os.Chmod(pathToFileInDirectoryWithoutReadAccess, 0700); err != nil { + c.Fatalf("failed to chmod file to 444: %s", err) + } + + buildCmd := exec.Command("su", "unprivilegeduser", "-c", fmt.Sprintf("%s build -t %s .", dockerBinary, name)) + buildCmd.Dir = ctx.Dir + if out, _, err := runCommandWithOutput(buildCmd); err != nil { + c.Fatalf("build should have worked: %s %s", err, out) + } + + } +} + +func (s *DockerSuite) TestBuildForceRm(c *check.C) { + containerCountBefore, err := getContainerCount() + if err != nil { + c.Fatalf("failed to get the container count: %s", err) + } + name := "testbuildforcerm" + + ctx, err := fakeContext(`FROM `+minimalBaseImage()+` + RUN true + RUN thiswillfail`, nil) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + dockerCmdInDir(c, ctx.Dir, "build", "-t", name, "--force-rm", ".") + + containerCountAfter, err := getContainerCount() + if err != nil { + c.Fatalf("failed to get the container count: %s", err) + } + + if containerCountBefore != containerCountAfter { + c.Fatalf("--force-rm shouldn't have left containers behind") + } + +} + +func (s *DockerSuite) TestBuildRm(c *check.C) { + name := "testbuildrm" + + ctx, err := fakeContext(`FROM `+minimalBaseImage()+` + ADD foo / + ADD foo /`, map[string]string{"foo": "bar"}) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + { + containerCountBefore, err := getContainerCount() + if err != nil { + c.Fatalf("failed to get the container count: %s", err) + } + + out, _, err := dockerCmdInDir(c, ctx.Dir, "build", "--rm", "-t", name, ".") + + if err != nil { + c.Fatal("failed to build the image", out) + } + + containerCountAfter, err := getContainerCount() + if err != nil { + c.Fatalf("failed to get the container count: %s", err) + } + + if containerCountBefore != containerCountAfter { + c.Fatalf("-rm shouldn't have left containers behind") + } + deleteImages(name) + } + + { + containerCountBefore, err := getContainerCount() + if err != nil { + c.Fatalf("failed to get the container count: %s", err) + } + + out, _, err := dockerCmdInDir(c, ctx.Dir, "build", "-t", name, ".") + + if err != nil { + c.Fatal("failed to build the image", out) + } + + containerCountAfter, err := getContainerCount() + if err != nil { + c.Fatalf("failed to get the container count: %s", err) + } + + if containerCountBefore != containerCountAfter { + c.Fatalf("--rm shouldn't have left containers behind") + } + deleteImages(name) + } + + { + containerCountBefore, err := getContainerCount() + if err != nil { + c.Fatalf("failed to get the container count: %s", err) + } + + out, _, err := dockerCmdInDir(c, ctx.Dir, "build", "--rm=false", "-t", name, ".") + + if err != nil { + c.Fatal("failed to build the image", out) + } + + containerCountAfter, err := getContainerCount() + if err != nil { + c.Fatalf("failed to get the container count: %s", err) + } + + if containerCountBefore == containerCountAfter { + c.Fatalf("--rm=false should have left containers behind") + } + deleteImages(name) + + } + +} + +func (s *DockerSuite) TestBuildWithVolumes(c *check.C) { + testRequires(c, DaemonIsLinux) // Invalid volume paths on Windows + var ( + result map[string]map[string]struct{} + name = "testbuildvolumes" + emptyMap = make(map[string]struct{}) + expected = map[string]map[string]struct{}{ + "/test1": emptyMap, + "/test2": emptyMap, + "/test3": emptyMap, + "/test4": emptyMap, + "/test5": emptyMap, + "/test6": emptyMap, + "[/test7": emptyMap, + "/test8]": emptyMap, + } + ) + _, err := buildImage(name, + `FROM scratch + VOLUME /test1 + VOLUME /test2 + VOLUME /test3 /test4 + VOLUME ["/test5", "/test6"] + VOLUME [/test7 /test8] + `, + true) + if err != nil { + c.Fatal(err) + } + res := inspectFieldJSON(c, name, "Config.Volumes") + + err = unmarshalJSON([]byte(res), &result) + if err != nil { + c.Fatal(err) + } + + equal := reflect.DeepEqual(&result, &expected) + + if !equal { + c.Fatalf("Volumes %s, expected %s", result, expected) + } + +} + +func (s *DockerSuite) TestBuildMaintainer(c *check.C) { + name := "testbuildmaintainer" + + expected := "dockerio" + _, err := buildImage(name, + `FROM `+minimalBaseImage()+` + MAINTAINER dockerio`, + true) + if err != nil { + c.Fatal(err) + } + res := inspectField(c, name, "Author") + if res != expected { + c.Fatalf("Maintainer %s, expected %s", res, expected) + } +} + +func (s *DockerSuite) TestBuildUser(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "testbuilduser" + expected := "dockerio" + _, err := buildImage(name, + `FROM busybox + RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd + USER dockerio + RUN [ $(whoami) = 'dockerio' ]`, + true) + if err != nil { + c.Fatal(err) + } + res := inspectField(c, name, "Config.User") + if res != expected { + c.Fatalf("User %s, expected %s", res, expected) + } +} + +func (s *DockerSuite) TestBuildRelativeWorkdir(c *check.C) { + name := "testbuildrelativeworkdir" + + var ( + expected1 string + expected2 string + expected3 string + expected4 string + expectedFinal string + ) + if daemonPlatform == "windows" { + expected1 = `C:/Windows/system32` + expected2 = `C:/test1` + expected3 = `C:/test2` + expected4 = `C:/test2/test3` + expectedFinal = `\test2\test3` + } else { + expected1 = `/` + expected2 = `/test1` + expected3 = `/test2` + expected4 = `/test2/test3` + expectedFinal = `/test2/test3` + } + + _, err := buildImage(name, + `FROM busybox + RUN sh -c "[ "$PWD" = '`+expected1+`' ]" + WORKDIR test1 + RUN sh -c "[ "$PWD" = '`+expected2+`' ]" + WORKDIR /test2 + RUN sh -c "[ "$PWD" = '`+expected3+`' ]" + WORKDIR test3 + RUN sh -c "[ "$PWD" = '`+expected4+`' ]"`, + true) + if err != nil { + c.Fatal(err) + } + res := inspectField(c, name, "Config.WorkingDir") + if res != expectedFinal { + c.Fatalf("Workdir %s, expected %s", res, expectedFinal) + } +} + +func (s *DockerSuite) TestBuildWorkdirWithEnvVariables(c *check.C) { + name := "testbuildworkdirwithenvvariables" + + var expected string + if daemonPlatform == "windows" { + expected = `\test1\test2` + } else { + expected = `/test1/test2` + } + + _, err := buildImage(name, + `FROM busybox + ENV DIRPATH /test1 + ENV SUBDIRNAME test2 + WORKDIR $DIRPATH + WORKDIR $SUBDIRNAME/$MISSING_VAR`, + true) + if err != nil { + c.Fatal(err) + } + res := inspectField(c, name, "Config.WorkingDir") + if res != expected { + c.Fatalf("Workdir %s, expected %s", res, expected) + } +} + +func (s *DockerSuite) TestBuildRelativeCopy(c *check.C) { + // cat /test1/test2/foo gets permission denied for the user + testRequires(c, NotUserNamespace) + + var expected string + if daemonPlatform == "windows" { + expected = `C:/test1/test2` + } else { + expected = `/test1/test2` + } + + name := "testbuildrelativecopy" + dockerfile := ` + FROM busybox + WORKDIR /test1 + WORKDIR test2 + RUN sh -c "[ "$PWD" = '` + expected + `' ]" + COPY foo ./ + RUN sh -c "[ $(cat /test1/test2/foo) = 'hello' ]" + ADD foo ./bar/baz + RUN sh -c "[ $(cat /test1/test2/bar/baz) = 'hello' ]" + COPY foo ./bar/baz2 + RUN sh -c "[ $(cat /test1/test2/bar/baz2) = 'hello' ]" + WORKDIR .. + COPY foo ./ + RUN sh -c "[ $(cat /test1/foo) = 'hello' ]" + COPY foo /test3/ + RUN sh -c "[ $(cat /test3/foo) = 'hello' ]" + WORKDIR /test4 + COPY . . + RUN sh -c "[ $(cat /test4/foo) = 'hello' ]" + WORKDIR /test5/test6 + COPY foo ../ + RUN sh -c "[ $(cat /test5/foo) = 'hello' ]" + ` + ctx, err := fakeContext(dockerfile, map[string]string{ + "foo": "hello", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + _, err = buildImageFromContext(name, ctx, false) + if err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildEnv(c *check.C) { + testRequires(c, DaemonIsLinux) // ENV expansion is different in Windows + name := "testbuildenv" + expected := "[PATH=/test:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin PORT=2375]" + _, err := buildImage(name, + `FROM busybox + ENV PATH /test:$PATH + ENV PORT 2375 + RUN [ $(env | grep PORT) = 'PORT=2375' ]`, + true) + if err != nil { + c.Fatal(err) + } + res := inspectField(c, name, "Config.Env") + if res != expected { + c.Fatalf("Env %s, expected %s", res, expected) + } +} + +func (s *DockerSuite) TestBuildPATH(c *check.C) { + testRequires(c, DaemonIsLinux) // ENV expansion is different in Windows + + defPath := "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + + fn := func(dockerfile string, exp string) { + _, err := buildImage("testbldpath", dockerfile, true) + c.Assert(err, check.IsNil) + + res := inspectField(c, "testbldpath", "Config.Env") + + if res != exp { + c.Fatalf("Env %q, expected %q for dockerfile:%q", res, exp, dockerfile) + } + } + + tests := []struct{ dockerfile, exp string }{ + {"FROM scratch\nMAINTAINER me", "[PATH=" + defPath + "]"}, + {"FROM busybox\nMAINTAINER me", "[PATH=" + defPath + "]"}, + {"FROM scratch\nENV FOO=bar", "[PATH=" + defPath + " FOO=bar]"}, + {"FROM busybox\nENV FOO=bar", "[PATH=" + defPath + " FOO=bar]"}, + {"FROM scratch\nENV PATH=/test", "[PATH=/test]"}, + {"FROM busybox\nENV PATH=/test", "[PATH=/test]"}, + {"FROM scratch\nENV PATH=''", "[PATH=]"}, + {"FROM busybox\nENV PATH=''", "[PATH=]"}, + } + + for _, test := range tests { + fn(test.dockerfile, test.exp) + } +} + +func (s *DockerSuite) TestBuildContextCleanup(c *check.C) { + testRequires(c, DaemonIsLinux) + testRequires(c, SameHostDaemon) + + name := "testbuildcontextcleanup" + entries, err := ioutil.ReadDir(filepath.Join(dockerBasePath, "tmp")) + if err != nil { + c.Fatalf("failed to list contents of tmp dir: %s", err) + } + _, err = buildImage(name, + `FROM scratch + ENTRYPOINT ["/bin/echo"]`, + true) + if err != nil { + c.Fatal(err) + } + entriesFinal, err := ioutil.ReadDir(filepath.Join(dockerBasePath, "tmp")) + if err != nil { + c.Fatalf("failed to list contents of tmp dir: %s", err) + } + if err = compareDirectoryEntries(entries, entriesFinal); err != nil { + c.Fatalf("context should have been deleted, but wasn't") + } + +} + +func (s *DockerSuite) TestBuildContextCleanupFailedBuild(c *check.C) { + testRequires(c, DaemonIsLinux) + testRequires(c, SameHostDaemon) + + name := "testbuildcontextcleanup" + entries, err := ioutil.ReadDir(filepath.Join(dockerBasePath, "tmp")) + if err != nil { + c.Fatalf("failed to list contents of tmp dir: %s", err) + } + _, err = buildImage(name, + `FROM scratch + RUN /non/existing/command`, + true) + if err == nil { + c.Fatalf("expected build to fail, but it didn't") + } + entriesFinal, err := ioutil.ReadDir(filepath.Join(dockerBasePath, "tmp")) + if err != nil { + c.Fatalf("failed to list contents of tmp dir: %s", err) + } + if err = compareDirectoryEntries(entries, entriesFinal); err != nil { + c.Fatalf("context should have been deleted, but wasn't") + } + +} + +func (s *DockerSuite) TestBuildCmd(c *check.C) { + name := "testbuildcmd" + + expected := "[/bin/echo Hello World]" + _, err := buildImage(name, + `FROM `+minimalBaseImage()+` + CMD ["/bin/echo", "Hello World"]`, + true) + if err != nil { + c.Fatal(err) + } + res := inspectField(c, name, "Config.Cmd") + if res != expected { + c.Fatalf("Cmd %s, expected %s", res, expected) + } +} + +func (s *DockerSuite) TestBuildExpose(c *check.C) { + testRequires(c, DaemonIsLinux) // Expose not implemented on Windows + name := "testbuildexpose" + expected := "map[2375/tcp:{}]" + _, err := buildImage(name, + `FROM scratch + EXPOSE 2375`, + true) + if err != nil { + c.Fatal(err) + } + res := inspectField(c, name, "Config.ExposedPorts") + if res != expected { + c.Fatalf("Exposed ports %s, expected %s", res, expected) + } +} + +func (s *DockerSuite) TestBuildExposeMorePorts(c *check.C) { + testRequires(c, DaemonIsLinux) // Expose not implemented on Windows + // start building docker file with a large number of ports + portList := make([]string, 50) + line := make([]string, 100) + expectedPorts := make([]int, len(portList)*len(line)) + for i := 0; i < len(portList); i++ { + for j := 0; j < len(line); j++ { + p := i*len(line) + j + 1 + line[j] = strconv.Itoa(p) + expectedPorts[p-1] = p + } + if i == len(portList)-1 { + portList[i] = strings.Join(line, " ") + } else { + portList[i] = strings.Join(line, " ") + ` \` + } + } + + dockerfile := `FROM scratch + EXPOSE {{range .}} {{.}} + {{end}}` + tmpl := template.Must(template.New("dockerfile").Parse(dockerfile)) + buf := bytes.NewBuffer(nil) + tmpl.Execute(buf, portList) + + name := "testbuildexpose" + _, err := buildImage(name, buf.String(), true) + if err != nil { + c.Fatal(err) + } + + // check if all the ports are saved inside Config.ExposedPorts + res := inspectFieldJSON(c, name, "Config.ExposedPorts") + var exposedPorts map[string]interface{} + if err := json.Unmarshal([]byte(res), &exposedPorts); err != nil { + c.Fatal(err) + } + + for _, p := range expectedPorts { + ep := fmt.Sprintf("%d/tcp", p) + if _, ok := exposedPorts[ep]; !ok { + c.Errorf("Port(%s) is not exposed", ep) + } else { + delete(exposedPorts, ep) + } + } + if len(exposedPorts) != 0 { + c.Errorf("Unexpected extra exposed ports %v", exposedPorts) + } +} + +func (s *DockerSuite) TestBuildExposeOrder(c *check.C) { + testRequires(c, DaemonIsLinux) // Expose not implemented on Windows + buildID := func(name, exposed string) string { + _, err := buildImage(name, fmt.Sprintf(`FROM scratch + EXPOSE %s`, exposed), true) + if err != nil { + c.Fatal(err) + } + id := inspectField(c, name, "Id") + return id + } + + id1 := buildID("testbuildexpose1", "80 2375") + id2 := buildID("testbuildexpose2", "2375 80") + if id1 != id2 { + c.Errorf("EXPOSE should invalidate the cache only when ports actually changed") + } +} + +func (s *DockerSuite) TestBuildExposeUpperCaseProto(c *check.C) { + testRequires(c, DaemonIsLinux) // Expose not implemented on Windows + name := "testbuildexposeuppercaseproto" + expected := "map[5678/udp:{}]" + _, err := buildImage(name, + `FROM scratch + EXPOSE 5678/UDP`, + true) + if err != nil { + c.Fatal(err) + } + res := inspectField(c, name, "Config.ExposedPorts") + if res != expected { + c.Fatalf("Exposed ports %s, expected %s", res, expected) + } +} + +func (s *DockerSuite) TestBuildEmptyEntrypointInheritance(c *check.C) { + name := "testbuildentrypointinheritance" + name2 := "testbuildentrypointinheritance2" + + _, err := buildImage(name, + `FROM busybox + ENTRYPOINT ["/bin/echo"]`, + true) + if err != nil { + c.Fatal(err) + } + res := inspectField(c, name, "Config.Entrypoint") + + expected := "[/bin/echo]" + if res != expected { + c.Fatalf("Entrypoint %s, expected %s", res, expected) + } + + _, err = buildImage(name2, + fmt.Sprintf(`FROM %s + ENTRYPOINT []`, name), + true) + if err != nil { + c.Fatal(err) + } + res = inspectField(c, name2, "Config.Entrypoint") + + expected = "[]" + + if res != expected { + c.Fatalf("Entrypoint %s, expected %s", res, expected) + } + +} + +func (s *DockerSuite) TestBuildEmptyEntrypoint(c *check.C) { + name := "testbuildentrypoint" + expected := "[]" + + _, err := buildImage(name, + `FROM busybox + ENTRYPOINT []`, + true) + if err != nil { + c.Fatal(err) + } + res := inspectField(c, name, "Config.Entrypoint") + if res != expected { + c.Fatalf("Entrypoint %s, expected %s", res, expected) + } + +} + +func (s *DockerSuite) TestBuildEntrypoint(c *check.C) { + name := "testbuildentrypoint" + + expected := "[/bin/echo]" + _, err := buildImage(name, + `FROM `+minimalBaseImage()+` + ENTRYPOINT ["/bin/echo"]`, + true) + if err != nil { + c.Fatal(err) + } + res := inspectField(c, name, "Config.Entrypoint") + if res != expected { + c.Fatalf("Entrypoint %s, expected %s", res, expected) + } + +} + +// #6445 ensure ONBUILD triggers aren't committed to grandchildren +func (s *DockerSuite) TestBuildOnBuildLimitedInheritence(c *check.C) { + var ( + out2, out3 string + ) + { + name1 := "testonbuildtrigger1" + dockerfile1 := ` + FROM busybox + RUN echo "GRANDPARENT" + ONBUILD RUN echo "ONBUILD PARENT" + ` + ctx, err := fakeContext(dockerfile1, nil) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + out1, _, err := dockerCmdInDir(c, ctx.Dir, "build", "-t", name1, ".") + if err != nil { + c.Fatalf("build failed to complete: %s, %v", out1, err) + } + } + { + name2 := "testonbuildtrigger2" + dockerfile2 := ` + FROM testonbuildtrigger1 + ` + ctx, err := fakeContext(dockerfile2, nil) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + out2, _, err = dockerCmdInDir(c, ctx.Dir, "build", "-t", name2, ".") + if err != nil { + c.Fatalf("build failed to complete: %s, %v", out2, err) + } + } + { + name3 := "testonbuildtrigger3" + dockerfile3 := ` + FROM testonbuildtrigger2 + ` + ctx, err := fakeContext(dockerfile3, nil) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + out3, _, err = dockerCmdInDir(c, ctx.Dir, "build", "-t", name3, ".") + if err != nil { + c.Fatalf("build failed to complete: %s, %v", out3, err) + } + + } + + // ONBUILD should be run in second build. + if !strings.Contains(out2, "ONBUILD PARENT") { + c.Fatalf("ONBUILD instruction did not run in child of ONBUILD parent") + } + + // ONBUILD should *not* be run in third build. + if strings.Contains(out3, "ONBUILD PARENT") { + c.Fatalf("ONBUILD instruction ran in grandchild of ONBUILD parent") + } + +} + +func (s *DockerSuite) TestBuildWithCache(c *check.C) { + testRequires(c, DaemonIsLinux) // Expose not implemented on Windows + name := "testbuildwithcache" + id1, err := buildImage(name, + `FROM scratch + MAINTAINER dockerio + EXPOSE 5432 + ENTRYPOINT ["/bin/echo"]`, + true) + if err != nil { + c.Fatal(err) + } + id2, err := buildImage(name, + `FROM scratch + MAINTAINER dockerio + EXPOSE 5432 + ENTRYPOINT ["/bin/echo"]`, + true) + if err != nil { + c.Fatal(err) + } + if id1 != id2 { + c.Fatal("The cache should have been used but hasn't.") + } +} + +func (s *DockerSuite) TestBuildWithoutCache(c *check.C) { + testRequires(c, DaemonIsLinux) // Expose not implemented on Windows + name := "testbuildwithoutcache" + name2 := "testbuildwithoutcache2" + id1, err := buildImage(name, + `FROM scratch + MAINTAINER dockerio + EXPOSE 5432 + ENTRYPOINT ["/bin/echo"]`, + true) + if err != nil { + c.Fatal(err) + } + + id2, err := buildImage(name2, + `FROM scratch + MAINTAINER dockerio + EXPOSE 5432 + ENTRYPOINT ["/bin/echo"]`, + false) + if err != nil { + c.Fatal(err) + } + if id1 == id2 { + c.Fatal("The cache should have been invalided but hasn't.") + } +} + +func (s *DockerSuite) TestBuildConditionalCache(c *check.C) { + name := "testbuildconditionalcache" + + dockerfile := ` + FROM busybox + ADD foo /tmp/` + ctx, err := fakeContext(dockerfile, map[string]string{ + "foo": "hello", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + id1, err := buildImageFromContext(name, ctx, true) + if err != nil { + c.Fatalf("Error building #1: %s", err) + } + + if err := ctx.Add("foo", "bye"); err != nil { + c.Fatalf("Error modifying foo: %s", err) + } + + id2, err := buildImageFromContext(name, ctx, false) + if err != nil { + c.Fatalf("Error building #2: %s", err) + } + if id2 == id1 { + c.Fatal("Should not have used the cache") + } + + id3, err := buildImageFromContext(name, ctx, true) + if err != nil { + c.Fatalf("Error building #3: %s", err) + } + if id3 != id2 { + c.Fatal("Should have used the cache") + } +} + +func (s *DockerSuite) TestBuildAddLocalFileWithCache(c *check.C) { + // local files are not owned by the correct user + testRequires(c, NotUserNamespace) + name := "testbuildaddlocalfilewithcache" + name2 := "testbuildaddlocalfilewithcache2" + dockerfile := ` + FROM busybox + MAINTAINER dockerio + ADD foo /usr/lib/bla/bar + RUN sh -c "[ $(cat /usr/lib/bla/bar) = "hello" ]"` + ctx, err := fakeContext(dockerfile, map[string]string{ + "foo": "hello", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + id1, err := buildImageFromContext(name, ctx, true) + if err != nil { + c.Fatal(err) + } + id2, err := buildImageFromContext(name2, ctx, true) + if err != nil { + c.Fatal(err) + } + if id1 != id2 { + c.Fatal("The cache should have been used but hasn't.") + } +} + +func (s *DockerSuite) TestBuildAddMultipleLocalFileWithCache(c *check.C) { + name := "testbuildaddmultiplelocalfilewithcache" + name2 := "testbuildaddmultiplelocalfilewithcache2" + dockerfile := ` + FROM busybox + MAINTAINER dockerio + ADD foo Dockerfile /usr/lib/bla/ + RUN sh -c "[ $(cat /usr/lib/bla/foo) = "hello" ]"` + ctx, err := fakeContext(dockerfile, map[string]string{ + "foo": "hello", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + id1, err := buildImageFromContext(name, ctx, true) + if err != nil { + c.Fatal(err) + } + id2, err := buildImageFromContext(name2, ctx, true) + if err != nil { + c.Fatal(err) + } + if id1 != id2 { + c.Fatal("The cache should have been used but hasn't.") + } +} + +func (s *DockerSuite) TestBuildAddLocalFileWithoutCache(c *check.C) { + // local files are not owned by the correct user + testRequires(c, NotUserNamespace) + name := "testbuildaddlocalfilewithoutcache" + name2 := "testbuildaddlocalfilewithoutcache2" + dockerfile := ` + FROM busybox + MAINTAINER dockerio + ADD foo /usr/lib/bla/bar + RUN sh -c "[ $(cat /usr/lib/bla/bar) = "hello" ]"` + ctx, err := fakeContext(dockerfile, map[string]string{ + "foo": "hello", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + id1, err := buildImageFromContext(name, ctx, true) + if err != nil { + c.Fatal(err) + } + id2, err := buildImageFromContext(name2, ctx, false) + if err != nil { + c.Fatal(err) + } + if id1 == id2 { + c.Fatal("The cache should have been invalided but hasn't.") + } +} + +func (s *DockerSuite) TestBuildCopyDirButNotFile(c *check.C) { + name := "testbuildcopydirbutnotfile" + name2 := "testbuildcopydirbutnotfile2" + + dockerfile := ` + FROM ` + minimalBaseImage() + ` + COPY dir /tmp/` + ctx, err := fakeContext(dockerfile, map[string]string{ + "dir/foo": "hello", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + id1, err := buildImageFromContext(name, ctx, true) + if err != nil { + c.Fatal(err) + } + // Check that adding file with similar name doesn't mess with cache + if err := ctx.Add("dir_file", "hello2"); err != nil { + c.Fatal(err) + } + id2, err := buildImageFromContext(name2, ctx, true) + if err != nil { + c.Fatal(err) + } + if id1 != id2 { + c.Fatal("The cache should have been used but wasn't") + } +} + +func (s *DockerSuite) TestBuildAddCurrentDirWithCache(c *check.C) { + name := "testbuildaddcurrentdirwithcache" + name2 := name + "2" + name3 := name + "3" + name4 := name + "4" + dockerfile := ` + FROM ` + minimalBaseImage() + ` + MAINTAINER dockerio + ADD . /usr/lib/bla` + ctx, err := fakeContext(dockerfile, map[string]string{ + "foo": "hello", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + id1, err := buildImageFromContext(name, ctx, true) + if err != nil { + c.Fatal(err) + } + // Check that adding file invalidate cache of "ADD ." + if err := ctx.Add("bar", "hello2"); err != nil { + c.Fatal(err) + } + id2, err := buildImageFromContext(name2, ctx, true) + if err != nil { + c.Fatal(err) + } + if id1 == id2 { + c.Fatal("The cache should have been invalided but hasn't.") + } + // Check that changing file invalidate cache of "ADD ." + if err := ctx.Add("foo", "hello1"); err != nil { + c.Fatal(err) + } + id3, err := buildImageFromContext(name3, ctx, true) + if err != nil { + c.Fatal(err) + } + if id2 == id3 { + c.Fatal("The cache should have been invalided but hasn't.") + } + // Check that changing file to same content with different mtime does not + // invalidate cache of "ADD ." + time.Sleep(1 * time.Second) // wait second because of mtime precision + if err := ctx.Add("foo", "hello1"); err != nil { + c.Fatal(err) + } + id4, err := buildImageFromContext(name4, ctx, true) + if err != nil { + c.Fatal(err) + } + if id3 != id4 { + c.Fatal("The cache should have been used but hasn't.") + } +} + +func (s *DockerSuite) TestBuildAddCurrentDirWithoutCache(c *check.C) { + name := "testbuildaddcurrentdirwithoutcache" + name2 := "testbuildaddcurrentdirwithoutcache2" + dockerfile := ` + FROM ` + minimalBaseImage() + ` + MAINTAINER dockerio + ADD . /usr/lib/bla` + ctx, err := fakeContext(dockerfile, map[string]string{ + "foo": "hello", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + id1, err := buildImageFromContext(name, ctx, true) + if err != nil { + c.Fatal(err) + } + id2, err := buildImageFromContext(name2, ctx, false) + if err != nil { + c.Fatal(err) + } + if id1 == id2 { + c.Fatal("The cache should have been invalided but hasn't.") + } +} + +func (s *DockerSuite) TestBuildAddRemoteFileWithCache(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows doesn't have httpserver image yet + name := "testbuildaddremotefilewithcache" + server, err := fakeStorage(map[string]string{ + "baz": "hello", + }) + if err != nil { + c.Fatal(err) + } + defer server.Close() + + id1, err := buildImage(name, + fmt.Sprintf(`FROM scratch + MAINTAINER dockerio + ADD %s/baz /usr/lib/baz/quux`, server.URL()), + true) + if err != nil { + c.Fatal(err) + } + id2, err := buildImage(name, + fmt.Sprintf(`FROM scratch + MAINTAINER dockerio + ADD %s/baz /usr/lib/baz/quux`, server.URL()), + true) + if err != nil { + c.Fatal(err) + } + if id1 != id2 { + c.Fatal("The cache should have been used but hasn't.") + } +} + +func (s *DockerSuite) TestBuildAddRemoteFileWithoutCache(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows doesn't have httpserver image yet + name := "testbuildaddremotefilewithoutcache" + name2 := "testbuildaddremotefilewithoutcache2" + server, err := fakeStorage(map[string]string{ + "baz": "hello", + }) + if err != nil { + c.Fatal(err) + } + defer server.Close() + + id1, err := buildImage(name, + fmt.Sprintf(`FROM scratch + MAINTAINER dockerio + ADD %s/baz /usr/lib/baz/quux`, server.URL()), + true) + if err != nil { + c.Fatal(err) + } + id2, err := buildImage(name2, + fmt.Sprintf(`FROM scratch + MAINTAINER dockerio + ADD %s/baz /usr/lib/baz/quux`, server.URL()), + false) + if err != nil { + c.Fatal(err) + } + if id1 == id2 { + c.Fatal("The cache should have been invalided but hasn't.") + } +} + +func (s *DockerSuite) TestBuildAddRemoteFileMTime(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows doesn't have httpserver image yet + name := "testbuildaddremotefilemtime" + name2 := name + "2" + name3 := name + "3" + + files := map[string]string{"baz": "hello"} + server, err := fakeStorage(files) + if err != nil { + c.Fatal(err) + } + defer server.Close() + + ctx, err := fakeContext(fmt.Sprintf(`FROM scratch + MAINTAINER dockerio + ADD %s/baz /usr/lib/baz/quux`, server.URL()), nil) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + id1, err := buildImageFromContext(name, ctx, true) + if err != nil { + c.Fatal(err) + } + + id2, err := buildImageFromContext(name2, ctx, true) + if err != nil { + c.Fatal(err) + } + if id1 != id2 { + c.Fatal("The cache should have been used but wasn't - #1") + } + + // Now create a different server with same contents (causes different mtime) + // The cache should still be used + + // allow some time for clock to pass as mtime precision is only 1s + time.Sleep(2 * time.Second) + + server2, err := fakeStorage(files) + if err != nil { + c.Fatal(err) + } + defer server2.Close() + + ctx2, err := fakeContext(fmt.Sprintf(`FROM scratch + MAINTAINER dockerio + ADD %s/baz /usr/lib/baz/quux`, server2.URL()), nil) + if err != nil { + c.Fatal(err) + } + defer ctx2.Close() + id3, err := buildImageFromContext(name3, ctx2, true) + if err != nil { + c.Fatal(err) + } + if id1 != id3 { + c.Fatal("The cache should have been used but wasn't") + } +} + +func (s *DockerSuite) TestBuildAddLocalAndRemoteFilesWithCache(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows doesn't have httpserver image yet + name := "testbuildaddlocalandremotefilewithcache" + server, err := fakeStorage(map[string]string{ + "baz": "hello", + }) + if err != nil { + c.Fatal(err) + } + defer server.Close() + + ctx, err := fakeContext(fmt.Sprintf(`FROM scratch + MAINTAINER dockerio + ADD foo /usr/lib/bla/bar + ADD %s/baz /usr/lib/baz/quux`, server.URL()), + map[string]string{ + "foo": "hello world", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + id1, err := buildImageFromContext(name, ctx, true) + if err != nil { + c.Fatal(err) + } + id2, err := buildImageFromContext(name, ctx, true) + if err != nil { + c.Fatal(err) + } + if id1 != id2 { + c.Fatal("The cache should have been used but hasn't.") + } +} + +func testContextTar(c *check.C, compression archive.Compression) { + ctx, err := fakeContext( + `FROM busybox +ADD foo /foo +CMD ["cat", "/foo"]`, + map[string]string{ + "foo": "bar", + }, + ) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + context, err := archive.Tar(ctx.Dir, compression) + if err != nil { + c.Fatalf("failed to build context tar: %v", err) + } + name := "contexttar" + buildCmd := exec.Command(dockerBinary, "build", "-t", name, "-") + buildCmd.Stdin = context + + if out, _, err := runCommandWithOutput(buildCmd); err != nil { + c.Fatalf("build failed to complete: %v %v", out, err) + } +} + +func (s *DockerSuite) TestBuildContextTarGzip(c *check.C) { + testContextTar(c, archive.Gzip) +} + +func (s *DockerSuite) TestBuildContextTarNoCompression(c *check.C) { + testContextTar(c, archive.Uncompressed) +} + +func (s *DockerSuite) TestBuildNoContext(c *check.C) { + buildCmd := exec.Command(dockerBinary, "build", "-t", "nocontext", "-") + buildCmd.Stdin = strings.NewReader( + `FROM busybox + CMD ["echo", "ok"]`) + + if out, _, err := runCommandWithOutput(buildCmd); err != nil { + c.Fatalf("build failed to complete: %v %v", out, err) + } + + if out, _ := dockerCmd(c, "run", "--rm", "nocontext"); out != "ok\n" { + c.Fatalf("run produced invalid output: %q, expected %q", out, "ok") + } +} + +// TODO: TestCaching +func (s *DockerSuite) TestBuildAddLocalAndRemoteFilesWithoutCache(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows doesn't have httpserver image yet + name := "testbuildaddlocalandremotefilewithoutcache" + name2 := "testbuildaddlocalandremotefilewithoutcache2" + server, err := fakeStorage(map[string]string{ + "baz": "hello", + }) + if err != nil { + c.Fatal(err) + } + defer server.Close() + + ctx, err := fakeContext(fmt.Sprintf(`FROM scratch + MAINTAINER dockerio + ADD foo /usr/lib/bla/bar + ADD %s/baz /usr/lib/baz/quux`, server.URL()), + map[string]string{ + "foo": "hello world", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + id1, err := buildImageFromContext(name, ctx, true) + if err != nil { + c.Fatal(err) + } + id2, err := buildImageFromContext(name2, ctx, false) + if err != nil { + c.Fatal(err) + } + if id1 == id2 { + c.Fatal("The cache should have been invalided but hasn't.") + } +} + +func (s *DockerSuite) TestBuildWithVolumeOwnership(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "testbuildimg" + + _, err := buildImage(name, + `FROM busybox:latest + RUN mkdir /test && chown daemon:daemon /test && chmod 0600 /test + VOLUME /test`, + true) + + if err != nil { + c.Fatal(err) + } + + out, _ := dockerCmd(c, "run", "--rm", "testbuildimg", "ls", "-la", "/test") + + if expected := "drw-------"; !strings.Contains(out, expected) { + c.Fatalf("expected %s received %s", expected, out) + } + + if expected := "daemon daemon"; !strings.Contains(out, expected) { + c.Fatalf("expected %s received %s", expected, out) + } + +} + +// testing #1405 - config.Cmd does not get cleaned up if +// utilizing cache +func (s *DockerSuite) TestBuildEntrypointRunCleanup(c *check.C) { + name := "testbuildcmdcleanup" + if _, err := buildImage(name, + `FROM busybox + RUN echo "hello"`, + true); err != nil { + c.Fatal(err) + } + + ctx, err := fakeContext(`FROM busybox + RUN echo "hello" + ADD foo /foo + ENTRYPOINT ["/bin/echo"]`, + map[string]string{ + "foo": "hello", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } + res := inspectField(c, name, "Config.Cmd") + // Cmd must be cleaned up + if res != "[]" { + c.Fatalf("Cmd %s, expected nil", res) + } +} + +func (s *DockerSuite) TestBuildForbiddenContextPath(c *check.C) { + name := "testbuildforbidpath" + ctx, err := fakeContext(`FROM `+minimalBaseImage()+` + ADD ../../ test/ + `, + map[string]string{ + "test.txt": "test1", + "other.txt": "other", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + expected := "Forbidden path outside the build context: ../../ " + + if daemonPlatform == "windows" { + expected = "Forbidden path outside the build context: ..\\..\\ " + } + + if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) { + c.Fatalf("Wrong error: (should contain \"%s\") got:\n%v", expected, err) + } + +} + +func (s *DockerSuite) TestBuildAddFileNotFound(c *check.C) { + name := "testbuildaddnotfound" + expected := "foo: no such file or directory" + + if daemonPlatform == "windows" { + expected = "foo: The system cannot find the file specified" + } + + ctx, err := fakeContext(`FROM `+minimalBaseImage()+` + ADD foo /usr/local/bar`, + map[string]string{"bar": "hello"}) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + if _, err := buildImageFromContext(name, ctx, true); err != nil { + if !strings.Contains(err.Error(), expected) { + c.Fatalf("Wrong error %v, must be about missing foo file or directory", err) + } + } else { + c.Fatal("Error must not be nil") + } +} + +func (s *DockerSuite) TestBuildInheritance(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "testbuildinheritance" + + _, err := buildImage(name, + `FROM scratch + EXPOSE 2375`, + true) + if err != nil { + c.Fatal(err) + } + ports1 := inspectField(c, name, "Config.ExposedPorts") + + _, err = buildImage(name, + fmt.Sprintf(`FROM %s + ENTRYPOINT ["/bin/echo"]`, name), + true) + if err != nil { + c.Fatal(err) + } + + res := inspectField(c, name, "Config.Entrypoint") + if expected := "[/bin/echo]"; res != expected { + c.Fatalf("Entrypoint %s, expected %s", res, expected) + } + ports2 := inspectField(c, name, "Config.ExposedPorts") + if ports1 != ports2 { + c.Fatalf("Ports must be same: %s != %s", ports1, ports2) + } +} + +func (s *DockerSuite) TestBuildFails(c *check.C) { + name := "testbuildfails" + _, err := buildImage(name, + `FROM busybox + RUN sh -c "exit 23"`, + true) + if err != nil { + if !strings.Contains(err.Error(), "returned a non-zero code: 23") { + c.Fatalf("Wrong error %v, must be about non-zero code 23", err) + } + } else { + c.Fatal("Error must not be nil") + } +} + +func (s *DockerSuite) TestBuildFailsDockerfileEmpty(c *check.C) { + name := "testbuildfails" + _, err := buildImage(name, ``, true) + if err != nil { + if !strings.Contains(err.Error(), "The Dockerfile (Dockerfile) cannot be empty") { + c.Fatalf("Wrong error %v, must be about empty Dockerfile", err) + } + } else { + c.Fatal("Error must not be nil") + } +} + +func (s *DockerSuite) TestBuildOnBuild(c *check.C) { + name := "testbuildonbuild" + _, err := buildImage(name, + `FROM busybox + ONBUILD RUN touch foobar`, + true) + if err != nil { + c.Fatal(err) + } + _, err = buildImage(name, + fmt.Sprintf(`FROM %s + RUN [ -f foobar ]`, name), + true) + if err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildOnBuildForbiddenChained(c *check.C) { + name := "testbuildonbuildforbiddenchained" + _, err := buildImage(name, + `FROM busybox + ONBUILD ONBUILD RUN touch foobar`, + true) + if err != nil { + if !strings.Contains(err.Error(), "Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed") { + c.Fatalf("Wrong error %v, must be about chaining ONBUILD", err) + } + } else { + c.Fatal("Error must not be nil") + } +} + +func (s *DockerSuite) TestBuildOnBuildForbiddenFrom(c *check.C) { + name := "testbuildonbuildforbiddenfrom" + _, err := buildImage(name, + `FROM busybox + ONBUILD FROM scratch`, + true) + if err != nil { + if !strings.Contains(err.Error(), "FROM isn't allowed as an ONBUILD trigger") { + c.Fatalf("Wrong error %v, must be about FROM forbidden", err) + } + } else { + c.Fatal("Error must not be nil") + } +} + +func (s *DockerSuite) TestBuildOnBuildForbiddenMaintainer(c *check.C) { + name := "testbuildonbuildforbiddenmaintainer" + _, err := buildImage(name, + `FROM busybox + ONBUILD MAINTAINER docker.io`, + true) + if err != nil { + if !strings.Contains(err.Error(), "MAINTAINER isn't allowed as an ONBUILD trigger") { + c.Fatalf("Wrong error %v, must be about MAINTAINER forbidden", err) + } + } else { + c.Fatal("Error must not be nil") + } +} + +// gh #2446 +func (s *DockerSuite) TestBuildAddToSymlinkDest(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "testbuildaddtosymlinkdest" + ctx, err := fakeContext(`FROM busybox + RUN mkdir /foo + RUN ln -s /foo /bar + ADD foo /bar/ + RUN [ -f /bar/foo ] + RUN [ -f /foo/foo ]`, + map[string]string{ + "foo": "hello", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildEscapeWhitespace(c *check.C) { + name := "testbuildescaping" + + _, err := buildImage(name, ` + FROM busybox + MAINTAINER "Docker \ +IO " + `, true) + if err != nil { + c.Fatal(err) + } + + res := inspectField(c, name, "Author") + + if res != "\"Docker IO \"" { + c.Fatalf("Parsed string did not match the escaped string. Got: %q", res) + } + +} + +func (s *DockerSuite) TestBuildVerifyIntString(c *check.C) { + // Verify that strings that look like ints are still passed as strings + name := "testbuildstringing" + + _, err := buildImage(name, ` + FROM busybox + MAINTAINER 123 + `, true) + + if err != nil { + c.Fatal(err) + } + + out, _ := dockerCmd(c, "inspect", name) + + if !strings.Contains(out, "\"123\"") { + c.Fatalf("Output does not contain the int as a string:\n%s", out) + } + +} + +func (s *DockerSuite) TestBuildDockerignore(c *check.C) { + testRequires(c, DaemonIsLinux) // TODO Windows: This test passes on Windows, + // but currently adds a disproportionate amount of time for the value it has. + // Removing it from Windows CI for now, but this will be revisited in the + // TP5 timeframe when perf is better. + name := "testbuilddockerignore" + dockerfile := ` + FROM busybox + ADD . /bla + RUN sh -c "[[ -f /bla/src/x.go ]]" + RUN sh -c "[[ -f /bla/Makefile ]]" + RUN sh -c "[[ ! -e /bla/src/_vendor ]]" + RUN sh -c "[[ ! -e /bla/.gitignore ]]" + RUN sh -c "[[ ! -e /bla/README.md ]]" + RUN sh -c "[[ ! -e /bla/dir/foo ]]" + RUN sh -c "[[ ! -e /bla/foo ]]" + RUN sh -c "[[ ! -e /bla/.git ]]" + RUN sh -c "[[ ! -e v.cc ]]" + RUN sh -c "[[ ! -e src/v.cc ]]" + RUN sh -c "[[ ! -e src/_vendor/v.cc ]]"` + ctx, err := fakeContext(dockerfile, map[string]string{ + "Makefile": "all:", + ".git/HEAD": "ref: foo", + "src/x.go": "package main", + "src/_vendor/v.go": "package main", + "src/_vendor/v.cc": "package main", + "src/v.cc": "package main", + "v.cc": "package main", + "dir/foo": "", + ".gitignore": "", + "README.md": "readme", + ".dockerignore": ` +.git +pkg +.gitignore +src/_vendor +*.md +**/*.cc +dir`, + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildDockerignoreCleanPaths(c *check.C) { + name := "testbuilddockerignorecleanpaths" + dockerfile := ` + FROM busybox + ADD . /tmp/ + RUN sh -c "(! ls /tmp/foo) && (! ls /tmp/foo2) && (! ls /tmp/dir1/foo)"` + ctx, err := fakeContext(dockerfile, map[string]string{ + "foo": "foo", + "foo2": "foo2", + "dir1/foo": "foo in dir1", + ".dockerignore": "./foo\ndir1//foo\n./dir1/../foo2", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildDockerignoreExceptions(c *check.C) { + testRequires(c, DaemonIsLinux) // TODO Windows: This test passes on Windows, + // but currently adds a disproportionate amount of time for the value it has. + // Removing it from Windows CI for now, but this will be revisited in the + // TP5 timeframe when perf is better. + name := "testbuilddockerignoreexceptions" + dockerfile := ` + FROM busybox + ADD . /bla + RUN sh -c "[[ -f /bla/src/x.go ]]" + RUN sh -c "[[ -f /bla/Makefile ]]" + RUN sh -c "[[ ! -e /bla/src/_vendor ]]" + RUN sh -c "[[ ! -e /bla/.gitignore ]]" + RUN sh -c "[[ ! -e /bla/README.md ]]" + RUN sh -c "[[ -e /bla/dir/dir/foo ]]" + RUN sh -c "[[ ! -e /bla/dir/foo1 ]]" + RUN sh -c "[[ -f /bla/dir/e ]]" + RUN sh -c "[[ -f /bla/dir/e-dir/foo ]]" + RUN sh -c "[[ ! -e /bla/foo ]]" + RUN sh -c "[[ ! -e /bla/.git ]]" + RUN sh -c "[[ -e /bla/dir/a.cc ]]"` + ctx, err := fakeContext(dockerfile, map[string]string{ + "Makefile": "all:", + ".git/HEAD": "ref: foo", + "src/x.go": "package main", + "src/_vendor/v.go": "package main", + "dir/foo": "", + "dir/foo1": "", + "dir/dir/f1": "", + "dir/dir/foo": "", + "dir/e": "", + "dir/e-dir/foo": "", + ".gitignore": "", + "README.md": "readme", + "dir/a.cc": "hello", + ".dockerignore": ` +.git +pkg +.gitignore +src/_vendor +*.md +dir +!dir/e* +!dir/dir/foo +**/*.cc +!**/*.cc`, + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildDockerignoringDockerfile(c *check.C) { + name := "testbuilddockerignoredockerfile" + dockerfile := ` + FROM busybox + ADD . /tmp/ + RUN sh -c "! ls /tmp/Dockerfile" + RUN ls /tmp/.dockerignore` + ctx, err := fakeContext(dockerfile, map[string]string{ + "Dockerfile": dockerfile, + ".dockerignore": "Dockerfile\n", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err = buildImageFromContext(name, ctx, true); err != nil { + c.Fatalf("Didn't ignore Dockerfile correctly:%s", err) + } + + // now try it with ./Dockerfile + ctx.Add(".dockerignore", "./Dockerfile\n") + if _, err = buildImageFromContext(name, ctx, true); err != nil { + c.Fatalf("Didn't ignore ./Dockerfile correctly:%s", err) + } + +} + +func (s *DockerSuite) TestBuildDockerignoringRenamedDockerfile(c *check.C) { + name := "testbuilddockerignoredockerfile" + dockerfile := ` + FROM busybox + ADD . /tmp/ + RUN ls /tmp/Dockerfile + RUN sh -c "! ls /tmp/MyDockerfile" + RUN ls /tmp/.dockerignore` + ctx, err := fakeContext(dockerfile, map[string]string{ + "Dockerfile": "Should not use me", + "MyDockerfile": dockerfile, + ".dockerignore": "MyDockerfile\n", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err = buildImageFromContext(name, ctx, true); err != nil { + c.Fatalf("Didn't ignore MyDockerfile correctly:%s", err) + } + + // now try it with ./MyDockerfile + ctx.Add(".dockerignore", "./MyDockerfile\n") + if _, err = buildImageFromContext(name, ctx, true); err != nil { + c.Fatalf("Didn't ignore ./MyDockerfile correctly:%s", err) + } + +} + +func (s *DockerSuite) TestBuildDockerignoringDockerignore(c *check.C) { + name := "testbuilddockerignoredockerignore" + dockerfile := ` + FROM busybox + ADD . /tmp/ + RUN sh -c "! ls /tmp/.dockerignore" + RUN ls /tmp/Dockerfile` + ctx, err := fakeContext(dockerfile, map[string]string{ + "Dockerfile": dockerfile, + ".dockerignore": ".dockerignore\n", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + if _, err = buildImageFromContext(name, ctx, true); err != nil { + c.Fatalf("Didn't ignore .dockerignore correctly:%s", err) + } +} + +func (s *DockerSuite) TestBuildDockerignoreTouchDockerfile(c *check.C) { + var id1 string + var id2 string + + name := "testbuilddockerignoretouchdockerfile" + dockerfile := ` + FROM busybox + ADD . /tmp/` + ctx, err := fakeContext(dockerfile, map[string]string{ + "Dockerfile": dockerfile, + ".dockerignore": "Dockerfile\n", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if id1, err = buildImageFromContext(name, ctx, true); err != nil { + c.Fatalf("Didn't build it correctly:%s", err) + } + + if id2, err = buildImageFromContext(name, ctx, true); err != nil { + c.Fatalf("Didn't build it correctly:%s", err) + } + if id1 != id2 { + c.Fatalf("Didn't use the cache - 1") + } + + // Now make sure touching Dockerfile doesn't invalidate the cache + if err = ctx.Add("Dockerfile", dockerfile+"\n# hi"); err != nil { + c.Fatalf("Didn't add Dockerfile: %s", err) + } + if id2, err = buildImageFromContext(name, ctx, true); err != nil { + c.Fatalf("Didn't build it correctly:%s", err) + } + if id1 != id2 { + c.Fatalf("Didn't use the cache - 2") + } + + // One more time but just 'touch' it instead of changing the content + if err = ctx.Add("Dockerfile", dockerfile+"\n# hi"); err != nil { + c.Fatalf("Didn't add Dockerfile: %s", err) + } + if id2, err = buildImageFromContext(name, ctx, true); err != nil { + c.Fatalf("Didn't build it correctly:%s", err) + } + if id1 != id2 { + c.Fatalf("Didn't use the cache - 3") + } + +} + +func (s *DockerSuite) TestBuildDockerignoringWholeDir(c *check.C) { + name := "testbuilddockerignorewholedir" + dockerfile := ` + FROM busybox + COPY . / + RUN sh -c "[[ ! -e /.gitignore ]]" + RUN sh -c "[[ -f /Makefile ]]"` + ctx, err := fakeContext(dockerfile, map[string]string{ + "Dockerfile": "FROM scratch", + "Makefile": "all:", + ".gitignore": "", + ".dockerignore": ".*\n", + }) + c.Assert(err, check.IsNil) + defer ctx.Close() + if _, err = buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } + + c.Assert(ctx.Add(".dockerfile", "*"), check.IsNil) + if _, err = buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } + + c.Assert(ctx.Add(".dockerfile", "."), check.IsNil) + if _, err = buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } + + c.Assert(ctx.Add(".dockerfile", "?"), check.IsNil) + if _, err = buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildDockerignoringBadExclusion(c *check.C) { + name := "testbuilddockerignorebadexclusion" + dockerfile := ` + FROM busybox + COPY . / + RUN sh -c "[[ ! -e /.gitignore ]]" + RUN sh -c "[[ -f /Makefile ]]"` + ctx, err := fakeContext(dockerfile, map[string]string{ + "Dockerfile": "FROM scratch", + "Makefile": "all:", + ".gitignore": "", + ".dockerignore": "!\n", + }) + c.Assert(err, check.IsNil) + defer ctx.Close() + if _, err = buildImageFromContext(name, ctx, true); err == nil { + c.Fatalf("Build was supposed to fail but didn't") + } + + if err.Error() != "failed to build the image: Error checking context: 'Illegal exclusion pattern: !'.\n" { + c.Fatalf("Incorrect output, got:%q", err.Error()) + } +} + +func (s *DockerSuite) TestBuildDockerignoringWildTopDir(c *check.C) { + dockerfile := ` + FROM busybox + COPY . / + RUN sh -c "[[ ! -e /.dockerignore ]]" + RUN sh -c "[[ ! -e /Dockerfile ]]" + RUN sh -c "[[ ! -e /file1 ]]" + RUN sh -c "[[ ! -e /dir ]]"` + + ctx, err := fakeContext(dockerfile, map[string]string{ + "Dockerfile": "FROM scratch", + "file1": "", + "dir/dfile1": "", + }) + c.Assert(err, check.IsNil) + defer ctx.Close() + + // All of these should result in ignoring all files + for _, variant := range []string{"**", "**/", "**/**", "*"} { + ctx.Add(".dockerignore", variant) + _, err = buildImageFromContext("noname", ctx, true) + c.Assert(err, check.IsNil, check.Commentf("variant: %s", variant)) + } +} + +func (s *DockerSuite) TestBuildDockerignoringWildDirs(c *check.C) { + testRequires(c, DaemonIsLinux) // TODO Windows: Fix this test; also perf + + dockerfile := ` + FROM busybox + COPY . / + #RUN sh -c "[[ -e /.dockerignore ]]" + RUN sh -c "[[ -e /Dockerfile ]] && \ + [[ ! -e /file0 ]] && \ + [[ ! -e /dir1/file0 ]] && \ + [[ ! -e /dir2/file0 ]] && \ + [[ ! -e /file1 ]] && \ + [[ ! -e /dir1/file1 ]] && \ + [[ ! -e /dir1/dir2/file1 ]] && \ + [[ ! -e /dir1/file2 ]] && \ + [[ -e /dir1/dir2/file2 ]] && \ + [[ ! -e /dir1/dir2/file4 ]] && \ + [[ ! -e /dir1/dir2/file5 ]] && \ + [[ ! -e /dir1/dir2/file6 ]] && \ + [[ ! -e /dir1/dir3/file7 ]] && \ + [[ ! -e /dir1/dir3/file8 ]] && \ + [[ -e /dir1/dir3 ]] && \ + [[ -e /dir1/dir4 ]] && \ + [[ ! -e 'dir1/dir5/fileAA' ]] && \ + [[ -e 'dir1/dir5/fileAB' ]] && \ + [[ -e 'dir1/dir5/fileB' ]]" # "." in pattern means nothing + + RUN echo all done!` + + ctx, err := fakeContext(dockerfile, map[string]string{ + "Dockerfile": "FROM scratch", + "file0": "", + "dir1/file0": "", + "dir1/dir2/file0": "", + + "file1": "", + "dir1/file1": "", + "dir1/dir2/file1": "", + + "dir1/file2": "", + "dir1/dir2/file2": "", // remains + + "dir1/dir2/file4": "", + "dir1/dir2/file5": "", + "dir1/dir2/file6": "", + "dir1/dir3/file7": "", + "dir1/dir3/file8": "", + "dir1/dir4/file9": "", + + "dir1/dir5/fileAA": "", + "dir1/dir5/fileAB": "", + "dir1/dir5/fileB": "", + + ".dockerignore": ` +**/file0 +**/*file1 +**/dir1/file2 +dir1/**/file4 +**/dir2/file5 +**/dir1/dir2/file6 +dir1/dir3/** +**/dir4/** +**/file?A +**/file\?B +**/dir5/file. +`, + }) + c.Assert(err, check.IsNil) + defer ctx.Close() + + _, err = buildImageFromContext("noname", ctx, true) + c.Assert(err, check.IsNil) +} + +func (s *DockerSuite) TestBuildLineBreak(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "testbuildlinebreak" + _, err := buildImage(name, + `FROM busybox +RUN sh -c 'echo root:testpass \ + > /tmp/passwd' +RUN mkdir -p /var/run/sshd +RUN sh -c "[ "$(cat /tmp/passwd)" = "root:testpass" ]" +RUN sh -c "[ "$(ls -d /var/run/sshd)" = "/var/run/sshd" ]"`, + true) + if err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildEOLInLine(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "testbuildeolinline" + _, err := buildImage(name, + `FROM busybox +RUN sh -c 'echo root:testpass > /tmp/passwd' +RUN echo "foo \n bar"; echo "baz" +RUN mkdir -p /var/run/sshd +RUN sh -c "[ "$(cat /tmp/passwd)" = "root:testpass" ]" +RUN sh -c "[ "$(ls -d /var/run/sshd)" = "/var/run/sshd" ]"`, + true) + if err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildCommentsShebangs(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "testbuildcomments" + _, err := buildImage(name, + `FROM busybox +# This is an ordinary comment. +RUN { echo '#!/bin/sh'; echo 'echo hello world'; } > /hello.sh +RUN [ ! -x /hello.sh ] +# comment with line break \ +RUN chmod +x /hello.sh +RUN [ -x /hello.sh ] +RUN [ "$(cat /hello.sh)" = $'#!/bin/sh\necho hello world' ] +RUN [ "$(/hello.sh)" = "hello world" ]`, + true) + if err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildUsersAndGroups(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "testbuildusers" + _, err := buildImage(name, + `FROM busybox + +# Make sure our defaults work +RUN [ "$(id -u):$(id -g)/$(id -un):$(id -gn)" = '0:0/root:root' ] + +# TODO decide if "args.user = strconv.Itoa(syscall.Getuid())" is acceptable behavior for changeUser in sysvinit instead of "return nil" when "USER" isn't specified (so that we get the proper group list even if that is the empty list, even in the default case of not supplying an explicit USER to run as, which implies USER 0) +USER root +RUN [ "$(id -G):$(id -Gn)" = '0 10:root wheel' ] + +# Setup dockerio user and group +RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd && \ + echo 'dockerio:x:1001:' >> /etc/group + +# Make sure we can switch to our user and all the information is exactly as we expect it to be +USER dockerio +RUN [ "$(id -u):$(id -g)/$(id -un):$(id -gn)/$(id -G):$(id -Gn)" = '1001:1001/dockerio:dockerio/1001:dockerio' ] + +# Switch back to root and double check that worked exactly as we might expect it to +USER root +RUN [ "$(id -u):$(id -g)/$(id -un):$(id -gn)/$(id -G):$(id -Gn)" = '0:0/root:root/0 10:root wheel' ] && \ + # Add a "supplementary" group for our dockerio user \ + echo 'supplementary:x:1002:dockerio' >> /etc/group + +# ... and then go verify that we get it like we expect +USER dockerio +RUN [ "$(id -u):$(id -g)/$(id -un):$(id -gn)/$(id -G):$(id -Gn)" = '1001:1001/dockerio:dockerio/1001 1002:dockerio supplementary' ] +USER 1001 +RUN [ "$(id -u):$(id -g)/$(id -un):$(id -gn)/$(id -G):$(id -Gn)" = '1001:1001/dockerio:dockerio/1001 1002:dockerio supplementary' ] + +# super test the new "user:group" syntax +USER dockerio:dockerio +RUN [ "$(id -u):$(id -g)/$(id -un):$(id -gn)/$(id -G):$(id -Gn)" = '1001:1001/dockerio:dockerio/1001:dockerio' ] +USER 1001:dockerio +RUN [ "$(id -u):$(id -g)/$(id -un):$(id -gn)/$(id -G):$(id -Gn)" = '1001:1001/dockerio:dockerio/1001:dockerio' ] +USER dockerio:1001 +RUN [ "$(id -u):$(id -g)/$(id -un):$(id -gn)/$(id -G):$(id -Gn)" = '1001:1001/dockerio:dockerio/1001:dockerio' ] +USER 1001:1001 +RUN [ "$(id -u):$(id -g)/$(id -un):$(id -gn)/$(id -G):$(id -Gn)" = '1001:1001/dockerio:dockerio/1001:dockerio' ] +USER dockerio:supplementary +RUN [ "$(id -u):$(id -g)/$(id -un):$(id -gn)/$(id -G):$(id -Gn)" = '1001:1002/dockerio:supplementary/1002:supplementary' ] +USER dockerio:1002 +RUN [ "$(id -u):$(id -g)/$(id -un):$(id -gn)/$(id -G):$(id -Gn)" = '1001:1002/dockerio:supplementary/1002:supplementary' ] +USER 1001:supplementary +RUN [ "$(id -u):$(id -g)/$(id -un):$(id -gn)/$(id -G):$(id -Gn)" = '1001:1002/dockerio:supplementary/1002:supplementary' ] +USER 1001:1002 +RUN [ "$(id -u):$(id -g)/$(id -un):$(id -gn)/$(id -G):$(id -Gn)" = '1001:1002/dockerio:supplementary/1002:supplementary' ] + +# make sure unknown uid/gid still works properly +USER 1042:1043 +RUN [ "$(id -u):$(id -g)/$(id -un):$(id -gn)/$(id -G):$(id -Gn)" = '1042:1043/1042:1043/1043:1043' ]`, + true) + if err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildEnvUsage(c *check.C) { + // /docker/world/hello is not owned by the correct user + testRequires(c, NotUserNamespace) + testRequires(c, DaemonIsLinux) + name := "testbuildenvusage" + dockerfile := `FROM busybox +ENV HOME /root +ENV PATH $HOME/bin:$PATH +ENV PATH /tmp:$PATH +RUN [ "$PATH" = "/tmp:$HOME/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ] +ENV FOO /foo/baz +ENV BAR /bar +ENV BAZ $BAR +ENV FOOPATH $PATH:$FOO +RUN [ "$BAR" = "$BAZ" ] +RUN [ "$FOOPATH" = "$PATH:/foo/baz" ] +ENV FROM hello/docker/world +ENV TO /docker/world/hello +ADD $FROM $TO +RUN [ "$(cat $TO)" = "hello" ] +ENV abc=def +ENV ghi=$abc +RUN [ "$ghi" = "def" ] +` + ctx, err := fakeContext(dockerfile, map[string]string{ + "hello/docker/world": "hello", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + _, err = buildImageFromContext(name, ctx, true) + if err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildEnvUsage2(c *check.C) { + // /docker/world/hello is not owned by the correct user + testRequires(c, NotUserNamespace) + testRequires(c, DaemonIsLinux) + name := "testbuildenvusage2" + dockerfile := `FROM busybox +ENV abc=def def="hello world" +RUN [ "$abc,$def" = "def,hello world" ] +ENV def=hello\ world v1=abc v2="hi there" v3='boogie nights' v4="with'quotes too" +RUN [ "$def,$v1,$v2,$v3,$v4" = "hello world,abc,hi there,boogie nights,with'quotes too" ] +ENV abc=zzz FROM=hello/docker/world +ENV abc=zzz TO=/docker/world/hello +ADD $FROM $TO +RUN [ "$abc,$(cat $TO)" = "zzz,hello" ] +ENV abc 'yyy' +RUN [ $abc = 'yyy' ] +ENV abc= +RUN [ "$abc" = "" ] + +# use grep to make sure if the builder substitutes \$foo by mistake +# we don't get a false positive +ENV abc=\$foo +RUN [ "$abc" = "\$foo" ] && (echo "$abc" | grep foo) +ENV abc \$foo +RUN [ "$abc" = "\$foo" ] && (echo "$abc" | grep foo) + +ENV abc=\'foo\' abc2=\"foo\" +RUN [ "$abc,$abc2" = "'foo',\"foo\"" ] +ENV abc "foo" +RUN [ "$abc" = "foo" ] +ENV abc 'foo' +RUN [ "$abc" = 'foo' ] +ENV abc \'foo\' +RUN [ "$abc" = "'foo'" ] +ENV abc \"foo\" +RUN [ "$abc" = '"foo"' ] + +ENV abc=ABC +RUN [ "$abc" = "ABC" ] +ENV def1=${abc:-DEF} def2=${ccc:-DEF} +ENV def3=${ccc:-${def2}xx} def4=${abc:+ALT} def5=${def2:+${abc}:} def6=${ccc:-\$abc:} def7=${ccc:-\${abc}:} +RUN [ "$def1,$def2,$def3,$def4,$def5,$def6,$def7" = 'ABC,DEF,DEFxx,ALT,ABC:,$abc:,${abc:}' ] +ENV mypath=${mypath:+$mypath:}/home +ENV mypath=${mypath:+$mypath:}/away +RUN [ "$mypath" = '/home:/away' ] + +ENV e1=bar +ENV e2=$e1 e3=$e11 e4=\$e1 e5=\$e11 +RUN [ "$e0,$e1,$e2,$e3,$e4,$e5" = ',bar,bar,,$e1,$e11' ] + +ENV ee1 bar +ENV ee2 $ee1 +ENV ee3 $ee11 +ENV ee4 \$ee1 +ENV ee5 \$ee11 +RUN [ "$ee1,$ee2,$ee3,$ee4,$ee5" = 'bar,bar,,$ee1,$ee11' ] + +ENV eee1="foo" eee2='foo' +ENV eee3 "foo" +ENV eee4 'foo' +RUN [ "$eee1,$eee2,$eee3,$eee4" = 'foo,foo,foo,foo' ] + +` + ctx, err := fakeContext(dockerfile, map[string]string{ + "hello/docker/world": "hello", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + _, err = buildImageFromContext(name, ctx, true) + if err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildAddScript(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "testbuildaddscript" + dockerfile := ` +FROM busybox +ADD test /test +RUN ["chmod","+x","/test"] +RUN ["/test"] +RUN [ "$(cat /testfile)" = 'test!' ]` + ctx, err := fakeContext(dockerfile, map[string]string{ + "test": "#!/bin/sh\necho 'test!' > /testfile", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + _, err = buildImageFromContext(name, ctx, true) + if err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildAddTar(c *check.C) { + // /test/foo is not owned by the correct user + testRequires(c, NotUserNamespace) + name := "testbuildaddtar" + + ctx := func() *FakeContext { + dockerfile := ` +FROM busybox +ADD test.tar / +RUN cat /test/foo | grep Hi +ADD test.tar /test.tar +RUN cat /test.tar/test/foo | grep Hi +ADD test.tar /unlikely-to-exist +RUN cat /unlikely-to-exist/test/foo | grep Hi +ADD test.tar /unlikely-to-exist-trailing-slash/ +RUN cat /unlikely-to-exist-trailing-slash/test/foo | grep Hi +RUN sh -c "mkdir /existing-directory" #sh -c is needed on Windows to use the correct mkdir +ADD test.tar /existing-directory +RUN cat /existing-directory/test/foo | grep Hi +ADD test.tar /existing-directory-trailing-slash/ +RUN cat /existing-directory-trailing-slash/test/foo | grep Hi` + tmpDir, err := ioutil.TempDir("", "fake-context") + c.Assert(err, check.IsNil) + testTar, err := os.Create(filepath.Join(tmpDir, "test.tar")) + if err != nil { + c.Fatalf("failed to create test.tar archive: %v", err) + } + defer testTar.Close() + + tw := tar.NewWriter(testTar) + + if err := tw.WriteHeader(&tar.Header{ + Name: "test/foo", + Size: 2, + }); err != nil { + c.Fatalf("failed to write tar file header: %v", err) + } + if _, err := tw.Write([]byte("Hi")); err != nil { + c.Fatalf("failed to write tar file content: %v", err) + } + if err := tw.Close(); err != nil { + c.Fatalf("failed to close tar archive: %v", err) + } + + if err := ioutil.WriteFile(filepath.Join(tmpDir, "Dockerfile"), []byte(dockerfile), 0644); err != nil { + c.Fatalf("failed to open destination dockerfile: %v", err) + } + return fakeContextFromDir(tmpDir) + }() + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatalf("build failed to complete for TestBuildAddTar: %v", err) + } + +} + +func (s *DockerSuite) TestBuildAddBrokenTar(c *check.C) { + name := "testbuildaddbrokentar" + + ctx := func() *FakeContext { + dockerfile := ` +FROM busybox +ADD test.tar /` + tmpDir, err := ioutil.TempDir("", "fake-context") + c.Assert(err, check.IsNil) + testTar, err := os.Create(filepath.Join(tmpDir, "test.tar")) + if err != nil { + c.Fatalf("failed to create test.tar archive: %v", err) + } + defer testTar.Close() + + tw := tar.NewWriter(testTar) + + if err := tw.WriteHeader(&tar.Header{ + Name: "test/foo", + Size: 2, + }); err != nil { + c.Fatalf("failed to write tar file header: %v", err) + } + if _, err := tw.Write([]byte("Hi")); err != nil { + c.Fatalf("failed to write tar file content: %v", err) + } + if err := tw.Close(); err != nil { + c.Fatalf("failed to close tar archive: %v", err) + } + + // Corrupt the tar by removing one byte off the end + stat, err := testTar.Stat() + if err != nil { + c.Fatalf("failed to stat tar archive: %v", err) + } + if err := testTar.Truncate(stat.Size() - 1); err != nil { + c.Fatalf("failed to truncate tar archive: %v", err) + } + + if err := ioutil.WriteFile(filepath.Join(tmpDir, "Dockerfile"), []byte(dockerfile), 0644); err != nil { + c.Fatalf("failed to open destination dockerfile: %v", err) + } + return fakeContextFromDir(tmpDir) + }() + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err == nil { + c.Fatalf("build should have failed for TestBuildAddBrokenTar") + } +} + +func (s *DockerSuite) TestBuildAddNonTar(c *check.C) { + name := "testbuildaddnontar" + + // Should not try to extract test.tar + ctx, err := fakeContext(` + FROM busybox + ADD test.tar / + RUN test -f /test.tar`, + map[string]string{"test.tar": "not_a_tar_file"}) + + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatalf("build failed for TestBuildAddNonTar") + } +} + +func (s *DockerSuite) TestBuildAddTarXz(c *check.C) { + // /test/foo is not owned by the correct user + testRequires(c, NotUserNamespace) + testRequires(c, DaemonIsLinux) + name := "testbuildaddtarxz" + + ctx := func() *FakeContext { + dockerfile := ` + FROM busybox + ADD test.tar.xz / + RUN cat /test/foo | grep Hi` + tmpDir, err := ioutil.TempDir("", "fake-context") + c.Assert(err, check.IsNil) + testTar, err := os.Create(filepath.Join(tmpDir, "test.tar")) + if err != nil { + c.Fatalf("failed to create test.tar archive: %v", err) + } + defer testTar.Close() + + tw := tar.NewWriter(testTar) + + if err := tw.WriteHeader(&tar.Header{ + Name: "test/foo", + Size: 2, + }); err != nil { + c.Fatalf("failed to write tar file header: %v", err) + } + if _, err := tw.Write([]byte("Hi")); err != nil { + c.Fatalf("failed to write tar file content: %v", err) + } + if err := tw.Close(); err != nil { + c.Fatalf("failed to close tar archive: %v", err) + } + + xzCompressCmd := exec.Command("xz", "-k", "test.tar") + xzCompressCmd.Dir = tmpDir + out, _, err := runCommandWithOutput(xzCompressCmd) + if err != nil { + c.Fatal(err, out) + } + + if err := ioutil.WriteFile(filepath.Join(tmpDir, "Dockerfile"), []byte(dockerfile), 0644); err != nil { + c.Fatalf("failed to open destination dockerfile: %v", err) + } + return fakeContextFromDir(tmpDir) + }() + + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatalf("build failed to complete for TestBuildAddTarXz: %v", err) + } + +} + +func (s *DockerSuite) TestBuildAddTarXzGz(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "testbuildaddtarxzgz" + + ctx := func() *FakeContext { + dockerfile := ` + FROM busybox + ADD test.tar.xz.gz / + RUN ls /test.tar.xz.gz` + tmpDir, err := ioutil.TempDir("", "fake-context") + c.Assert(err, check.IsNil) + testTar, err := os.Create(filepath.Join(tmpDir, "test.tar")) + if err != nil { + c.Fatalf("failed to create test.tar archive: %v", err) + } + defer testTar.Close() + + tw := tar.NewWriter(testTar) + + if err := tw.WriteHeader(&tar.Header{ + Name: "test/foo", + Size: 2, + }); err != nil { + c.Fatalf("failed to write tar file header: %v", err) + } + if _, err := tw.Write([]byte("Hi")); err != nil { + c.Fatalf("failed to write tar file content: %v", err) + } + if err := tw.Close(); err != nil { + c.Fatalf("failed to close tar archive: %v", err) + } + + xzCompressCmd := exec.Command("xz", "-k", "test.tar") + xzCompressCmd.Dir = tmpDir + out, _, err := runCommandWithOutput(xzCompressCmd) + if err != nil { + c.Fatal(err, out) + } + + gzipCompressCmd := exec.Command("gzip", "test.tar.xz") + gzipCompressCmd.Dir = tmpDir + out, _, err = runCommandWithOutput(gzipCompressCmd) + if err != nil { + c.Fatal(err, out) + } + + if err := ioutil.WriteFile(filepath.Join(tmpDir, "Dockerfile"), []byte(dockerfile), 0644); err != nil { + c.Fatalf("failed to open destination dockerfile: %v", err) + } + return fakeContextFromDir(tmpDir) + }() + + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatalf("build failed to complete for TestBuildAddTarXz: %v", err) + } + +} + +func (s *DockerSuite) TestBuildFromGIT(c *check.C) { + name := "testbuildfromgit" + git, err := newFakeGit("repo", map[string]string{ + "Dockerfile": `FROM busybox + ADD first /first + RUN [ -f /first ] + MAINTAINER docker`, + "first": "test git data", + }, true) + if err != nil { + c.Fatal(err) + } + defer git.Close() + + _, err = buildImageFromPath(name, git.RepoURL, true) + if err != nil { + c.Fatal(err) + } + res := inspectField(c, name, "Author") + if res != "docker" { + c.Fatalf("Maintainer should be docker, got %s", res) + } +} + +func (s *DockerSuite) TestBuildFromGITWithContext(c *check.C) { + name := "testbuildfromgit" + git, err := newFakeGit("repo", map[string]string{ + "docker/Dockerfile": `FROM busybox + ADD first /first + RUN [ -f /first ] + MAINTAINER docker`, + "docker/first": "test git data", + }, true) + if err != nil { + c.Fatal(err) + } + defer git.Close() + + u := fmt.Sprintf("%s#master:docker", git.RepoURL) + _, err = buildImageFromPath(name, u, true) + if err != nil { + c.Fatal(err) + } + res := inspectField(c, name, "Author") + if res != "docker" { + c.Fatalf("Maintainer should be docker, got %s", res) + } +} + +func (s *DockerSuite) TestBuildFromGITwithF(c *check.C) { + name := "testbuildfromgitwithf" + git, err := newFakeGit("repo", map[string]string{ + "myApp/myDockerfile": `FROM busybox + RUN echo hi from Dockerfile`, + }, true) + if err != nil { + c.Fatal(err) + } + defer git.Close() + + out, _, err := dockerCmdWithError("build", "-t", name, "--no-cache", "-f", "myApp/myDockerfile", git.RepoURL) + if err != nil { + c.Fatalf("Error on build. Out: %s\nErr: %v", out, err) + } + + if !strings.Contains(out, "hi from Dockerfile") { + c.Fatalf("Missing expected output, got:\n%s", out) + } +} + +func (s *DockerSuite) TestBuildFromRemoteTarball(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "testbuildfromremotetarball" + + buffer := new(bytes.Buffer) + tw := tar.NewWriter(buffer) + defer tw.Close() + + dockerfile := []byte(`FROM busybox + MAINTAINER docker`) + if err := tw.WriteHeader(&tar.Header{ + Name: "Dockerfile", + Size: int64(len(dockerfile)), + }); err != nil { + c.Fatalf("failed to write tar file header: %v", err) + } + if _, err := tw.Write(dockerfile); err != nil { + c.Fatalf("failed to write tar file content: %v", err) + } + if err := tw.Close(); err != nil { + c.Fatalf("failed to close tar archive: %v", err) + } + + server, err := fakeBinaryStorage(map[string]*bytes.Buffer{ + "testT.tar": buffer, + }) + c.Assert(err, check.IsNil) + + defer server.Close() + + _, err = buildImageFromPath(name, server.URL()+"/testT.tar", true) + c.Assert(err, check.IsNil) + + res := inspectField(c, name, "Author") + + if res != "docker" { + c.Fatalf("Maintainer should be docker, got %s", res) + } +} + +func (s *DockerSuite) TestBuildCleanupCmdOnEntrypoint(c *check.C) { + name := "testbuildcmdcleanuponentrypoint" + if _, err := buildImage(name, + `FROM `+minimalBaseImage()+` + CMD ["test"] + ENTRYPOINT ["echo"]`, + true); err != nil { + c.Fatal(err) + } + if _, err := buildImage(name, + fmt.Sprintf(`FROM %s + ENTRYPOINT ["cat"]`, name), + true); err != nil { + c.Fatal(err) + } + res := inspectField(c, name, "Config.Cmd") + if res != "[]" { + c.Fatalf("Cmd %s, expected nil", res) + } + + res = inspectField(c, name, "Config.Entrypoint") + if expected := "[cat]"; res != expected { + c.Fatalf("Entrypoint %s, expected %s", res, expected) + } +} + +func (s *DockerSuite) TestBuildClearCmd(c *check.C) { + name := "testbuildclearcmd" + _, err := buildImage(name, + `From `+minimalBaseImage()+` + ENTRYPOINT ["/bin/bash"] + CMD []`, + true) + if err != nil { + c.Fatal(err) + } + res := inspectFieldJSON(c, name, "Config.Cmd") + if res != "[]" { + c.Fatalf("Cmd %s, expected %s", res, "[]") + } +} + +func (s *DockerSuite) TestBuildEmptyCmd(c *check.C) { + name := "testbuildemptycmd" + if _, err := buildImage(name, "FROM "+minimalBaseImage()+"\nMAINTAINER quux\n", true); err != nil { + c.Fatal(err) + } + res := inspectFieldJSON(c, name, "Config.Cmd") + if res != "null" { + c.Fatalf("Cmd %s, expected %s", res, "null") + } +} + +func (s *DockerSuite) TestBuildOnBuildOutput(c *check.C) { + name := "testbuildonbuildparent" + if _, err := buildImage(name, "FROM busybox\nONBUILD RUN echo foo\n", true); err != nil { + c.Fatal(err) + } + + _, out, err := buildImageWithOut(name, "FROM "+name+"\nMAINTAINER quux\n", true) + if err != nil { + c.Fatal(err) + } + + if !strings.Contains(out, "# Executing 1 build trigger") { + c.Fatal("failed to find the build trigger output", out) + } +} + +func (s *DockerSuite) TestBuildInvalidTag(c *check.C) { + name := "abcd:" + stringutils.GenerateRandomAlphaOnlyString(200) + _, out, err := buildImageWithOut(name, "FROM "+minimalBaseImage()+"\nMAINTAINER quux\n", true) + // if the error doesn't check for illegal tag name, or the image is built + // then this should fail + if !strings.Contains(out, "Error parsing reference") || strings.Contains(out, "Sending build context to Docker daemon") { + c.Fatalf("failed to stop before building. Error: %s, Output: %s", err, out) + } +} + +func (s *DockerSuite) TestBuildCmdShDashC(c *check.C) { + name := "testbuildcmdshc" + if _, err := buildImage(name, "FROM busybox\nCMD echo cmd\n", true); err != nil { + c.Fatal(err) + } + + res := inspectFieldJSON(c, name, "Config.Cmd") + + expected := `["/bin/sh","-c","echo cmd"]` + if daemonPlatform == "windows" { + expected = `["cmd","/S","/C","echo cmd"]` + } + + if res != expected { + c.Fatalf("Expected value %s not in Config.Cmd: %s", expected, res) + } + +} + +func (s *DockerSuite) TestBuildCmdSpaces(c *check.C) { + // Test to make sure that when we strcat arrays we take into account + // the arg separator to make sure ["echo","hi"] and ["echo hi"] don't + // look the same + name := "testbuildcmdspaces" + var id1 string + var id2 string + var err error + + if id1, err = buildImage(name, "FROM busybox\nCMD [\"echo hi\"]\n", true); err != nil { + c.Fatal(err) + } + + if id2, err = buildImage(name, "FROM busybox\nCMD [\"echo\", \"hi\"]\n", true); err != nil { + c.Fatal(err) + } + + if id1 == id2 { + c.Fatal("Should not have resulted in the same CMD") + } + + // Now do the same with ENTRYPOINT + if id1, err = buildImage(name, "FROM busybox\nENTRYPOINT [\"echo hi\"]\n", true); err != nil { + c.Fatal(err) + } + + if id2, err = buildImage(name, "FROM busybox\nENTRYPOINT [\"echo\", \"hi\"]\n", true); err != nil { + c.Fatal(err) + } + + if id1 == id2 { + c.Fatal("Should not have resulted in the same ENTRYPOINT") + } + +} + +func (s *DockerSuite) TestBuildCmdJSONNoShDashC(c *check.C) { + name := "testbuildcmdjson" + if _, err := buildImage(name, "FROM busybox\nCMD [\"echo\", \"cmd\"]", true); err != nil { + c.Fatal(err) + } + + res := inspectFieldJSON(c, name, "Config.Cmd") + + expected := `["echo","cmd"]` + + if res != expected { + c.Fatalf("Expected value %s not in Config.Cmd: %s", expected, res) + } + +} + +func (s *DockerSuite) TestBuildErrorInvalidInstruction(c *check.C) { + name := "testbuildignoreinvalidinstruction" + + out, _, err := buildImageWithOut(name, "FROM busybox\nfoo bar", true) + if err == nil { + c.Fatalf("Should have failed: %s", out) + } + +} + +func (s *DockerSuite) TestBuildEntrypointInheritance(c *check.C) { + + if _, err := buildImage("parent", ` + FROM busybox + ENTRYPOINT exit 130 + `, true); err != nil { + c.Fatal(err) + } + + if _, status, _ := dockerCmdWithError("run", "parent"); status != 130 { + c.Fatalf("expected exit code 130 but received %d", status) + } + + if _, err := buildImage("child", ` + FROM parent + ENTRYPOINT exit 5 + `, true); err != nil { + c.Fatal(err) + } + + if _, status, _ := dockerCmdWithError("run", "child"); status != 5 { + c.Fatalf("expected exit code 5 but received %d", status) + } + +} + +func (s *DockerSuite) TestBuildEntrypointInheritanceInspect(c *check.C) { + var ( + name = "testbuildepinherit" + name2 = "testbuildepinherit2" + expected = `["/bin/sh","-c","echo quux"]` + ) + + if daemonPlatform == "windows" { + expected = `["cmd","/S","/C","echo quux"]` + } + + if _, err := buildImage(name, "FROM busybox\nENTRYPOINT /foo/bar", true); err != nil { + c.Fatal(err) + } + + if _, err := buildImage(name2, fmt.Sprintf("FROM %s\nENTRYPOINT echo quux", name), true); err != nil { + c.Fatal(err) + } + + res := inspectFieldJSON(c, name2, "Config.Entrypoint") + + if res != expected { + c.Fatalf("Expected value %s not in Config.Entrypoint: %s", expected, res) + } + + out, _ := dockerCmd(c, "run", name2) + + expected = "quux" + + if strings.TrimSpace(out) != expected { + c.Fatalf("Expected output is %s, got %s", expected, out) + } + +} + +func (s *DockerSuite) TestBuildRunShEntrypoint(c *check.C) { + name := "testbuildentrypoint" + _, err := buildImage(name, + `FROM busybox + ENTRYPOINT echo`, + true) + if err != nil { + c.Fatal(err) + } + + dockerCmd(c, "run", "--rm", name) +} + +func (s *DockerSuite) TestBuildExoticShellInterpolation(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "testbuildexoticshellinterpolation" + + _, err := buildImage(name, ` + FROM busybox + + ENV SOME_VAR a.b.c + + RUN [ "$SOME_VAR" = 'a.b.c' ] + RUN [ "${SOME_VAR}" = 'a.b.c' ] + RUN [ "${SOME_VAR%.*}" = 'a.b' ] + RUN [ "${SOME_VAR%%.*}" = 'a' ] + RUN [ "${SOME_VAR#*.}" = 'b.c' ] + RUN [ "${SOME_VAR##*.}" = 'c' ] + RUN [ "${SOME_VAR/c/d}" = 'a.b.d' ] + RUN [ "${#SOME_VAR}" = '5' ] + + RUN [ "${SOME_UNSET_VAR:-$SOME_VAR}" = 'a.b.c' ] + RUN [ "${SOME_VAR:+Version: ${SOME_VAR}}" = 'Version: a.b.c' ] + RUN [ "${SOME_UNSET_VAR:+${SOME_VAR}}" = '' ] + RUN [ "${SOME_UNSET_VAR:-${SOME_VAR:-d.e.f}}" = 'a.b.c' ] + `, false) + if err != nil { + c.Fatal(err) + } + +} + +func (s *DockerSuite) TestBuildVerifySingleQuoteFails(c *check.C) { + // This testcase is supposed to generate an error because the + // JSON array we're passing in on the CMD uses single quotes instead + // of double quotes (per the JSON spec). This means we interpret it + // as a "string" instead of "JSON array" and pass it on to "sh -c" and + // it should barf on it. + name := "testbuildsinglequotefails" + + if _, err := buildImage(name, + `FROM busybox + CMD [ '/bin/sh', '-c', 'echo hi' ]`, + true); err != nil { + c.Fatal(err) + } + + if _, _, err := dockerCmdWithError("run", "--rm", name); err == nil { + c.Fatal("The image was not supposed to be able to run") + } + +} + +func (s *DockerSuite) TestBuildVerboseOut(c *check.C) { + name := "testbuildverboseout" + expected := "\n123\n" + + if daemonPlatform == "windows" { + expected = "\n123\r\n" + } + + _, out, err := buildImageWithOut(name, + `FROM busybox +RUN echo 123`, + false) + + if err != nil { + c.Fatal(err) + } + if !strings.Contains(out, expected) { + c.Fatalf("Output should contain %q: %q", "123", out) + } + +} + +func (s *DockerSuite) TestBuildWithTabs(c *check.C) { + name := "testbuildwithtabs" + _, err := buildImage(name, + "FROM busybox\nRUN echo\tone\t\ttwo", true) + if err != nil { + c.Fatal(err) + } + res := inspectFieldJSON(c, name, "ContainerConfig.Cmd") + expected1 := `["/bin/sh","-c","echo\tone\t\ttwo"]` + expected2 := `["/bin/sh","-c","echo\u0009one\u0009\u0009two"]` // syntactically equivalent, and what Go 1.3 generates + if daemonPlatform == "windows" { + expected1 = `["cmd","/S","/C","echo\tone\t\ttwo"]` + expected2 = `["cmd","/S","/C","echo\u0009one\u0009\u0009two"]` // syntactically equivalent, and what Go 1.3 generates + } + if res != expected1 && res != expected2 { + c.Fatalf("Missing tabs.\nGot: %s\nExp: %s or %s", res, expected1, expected2) + } +} + +func (s *DockerSuite) TestBuildLabels(c *check.C) { + name := "testbuildlabel" + expected := `{"License":"GPL","Vendor":"Acme"}` + _, err := buildImage(name, + `FROM busybox + LABEL Vendor=Acme + LABEL License GPL`, + true) + if err != nil { + c.Fatal(err) + } + res := inspectFieldJSON(c, name, "Config.Labels") + if res != expected { + c.Fatalf("Labels %s, expected %s", res, expected) + } +} + +func (s *DockerSuite) TestBuildLabelsCache(c *check.C) { + name := "testbuildlabelcache" + + id1, err := buildImage(name, + `FROM busybox + LABEL Vendor=Acme`, false) + if err != nil { + c.Fatalf("Build 1 should have worked: %v", err) + } + + id2, err := buildImage(name, + `FROM busybox + LABEL Vendor=Acme`, true) + if err != nil || id1 != id2 { + c.Fatalf("Build 2 should have worked & used cache(%s,%s): %v", id1, id2, err) + } + + id2, err = buildImage(name, + `FROM busybox + LABEL Vendor=Acme1`, true) + if err != nil || id1 == id2 { + c.Fatalf("Build 3 should have worked & NOT used cache(%s,%s): %v", id1, id2, err) + } + + id2, err = buildImage(name, + `FROM busybox + LABEL Vendor Acme`, true) // Note: " " and "=" should be same + if err != nil || id1 != id2 { + c.Fatalf("Build 4 should have worked & used cache(%s,%s): %v", id1, id2, err) + } + + // Now make sure the cache isn't used by mistake + id1, err = buildImage(name, + `FROM busybox + LABEL f1=b1 f2=b2`, false) + if err != nil { + c.Fatalf("Build 5 should have worked: %q", err) + } + + id2, err = buildImage(name, + `FROM busybox + LABEL f1="b1 f2=b2"`, true) + if err != nil || id1 == id2 { + c.Fatalf("Build 6 should have worked & NOT used the cache(%s,%s): %q", id1, id2, err) + } + +} + +func (s *DockerSuite) TestBuildNotVerboseSuccess(c *check.C) { + // This test makes sure that -q works correctly when build is successful: + // stdout has only the image ID (long image ID) and stderr is empty. + var stdout, stderr string + var err error + outRegexp := regexp.MustCompile("^(sha256:|)[a-z0-9]{64}\\n$") + + tt := []struct { + Name string + BuildFunc func(string) + }{ + { + Name: "quiet_build_stdin_success", + BuildFunc: func(name string) { + _, stdout, stderr, err = buildImageWithStdoutStderr(name, "FROM busybox", true, "-q", "--force-rm", "--rm") + }, + }, + { + Name: "quiet_build_ctx_success", + BuildFunc: func(name string) { + ctx, err := fakeContext("FROM busybox", map[string]string{ + "quiet_build_success_fctx": "test", + }) + if err != nil { + c.Fatalf("Failed to create context: %s", err.Error()) + } + defer ctx.Close() + _, stdout, stderr, err = buildImageFromContextWithStdoutStderr(name, ctx, true, "-q", "--force-rm", "--rm") + }, + }, + { + Name: "quiet_build_git_success", + BuildFunc: func(name string) { + git, err := newFakeGit("repo", map[string]string{ + "Dockerfile": "FROM busybox", + }, true) + if err != nil { + c.Fatalf("Failed to create the git repo: %s", err.Error()) + } + defer git.Close() + _, stdout, stderr, err = buildImageFromGitWithStdoutStderr(name, git, true, "-q", "--force-rm", "--rm") + + }, + }, + } + + for _, te := range tt { + te.BuildFunc(te.Name) + if err != nil { + c.Fatalf("Test %s shouldn't fail, but got the following error: %s", te.Name, err.Error()) + } + if outRegexp.Find([]byte(stdout)) == nil { + c.Fatalf("Test %s expected stdout to match the [%v] regexp, but it is [%v]", te.Name, outRegexp, stdout) + } + if runtime.GOOS == "windows" { + // stderr contains a security warning on Windows if the daemon isn't Windows + lines := strings.Split(stderr, "\n") + warningCount := 0 + for _, v := range lines { + warningText := "SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host." + if strings.Contains(v, warningText) { + warningCount++ + } + if v != "" && !strings.Contains(v, warningText) { + c.Fatalf("Stderr contains unexpected output line: %q", v) + } + } + if warningCount != 1 && daemonPlatform != "windows" { + c.Fatalf("Test %s didn't get security warning running from Windows to non-Windows", te.Name) + } + } else { + if stderr != "" { + c.Fatalf("Test %s expected stderr to be empty, but it is [%#v]", te.Name, stderr) + } + } + } + +} + +func (s *DockerSuite) TestBuildNotVerboseFailure(c *check.C) { + // This test makes sure that -q works correctly when build fails by + // comparing between the stderr output in quiet mode and in stdout + // and stderr output in verbose mode + tt := []struct { + TestName string + BuildCmds string + }{ + {"quiet_build_no_from_at_the_beginning", "RUN whoami"}, + {"quiet_build_unknown_instr", "FROMD busybox"}, + {"quiet_build_not_exists_image", "FROM busybox11"}, + } + + for _, te := range tt { + _, _, qstderr, qerr := buildImageWithStdoutStderr(te.TestName, te.BuildCmds, false, "-q", "--force-rm", "--rm") + _, vstdout, vstderr, verr := buildImageWithStdoutStderr(te.TestName, te.BuildCmds, false, "--force-rm", "--rm") + if verr == nil || qerr == nil { + c.Fatal(fmt.Errorf("Test [%s] expected to fail but didn't", te.TestName)) + } + if qstderr != vstdout+vstderr { + c.Fatal(fmt.Errorf("Test[%s] expected that quiet stderr and verbose stdout are equal; quiet [%v], verbose [%v]", te.TestName, qstderr, vstdout+vstderr)) + } + } +} + +func (s *DockerSuite) TestBuildNotVerboseFailureRemote(c *check.C) { + // This test ensures that when given a wrong URL, stderr in quiet mode and + // stdout and stderr in verbose mode are identical. + URL := "http://bla.bla.com" + Name := "quiet_build_wrong_remote" + _, _, qstderr, qerr := buildImageWithStdoutStderr(Name, "", false, "-q", "--force-rm", "--rm", URL) + _, vstdout, vstderr, verr := buildImageWithStdoutStderr(Name, "", false, "--force-rm", "--rm", URL) + if qerr == nil || verr == nil { + c.Fatal(fmt.Errorf("Test [%s] expected to fail but didn't", Name)) + } + if qstderr != vstdout+vstderr { + c.Fatal(fmt.Errorf("Test[%s] expected that quiet stderr and verbose stdout are equal; quiet [%v], verbose [%v]", Name, qstderr, vstdout)) + } +} + +func (s *DockerSuite) TestBuildStderr(c *check.C) { + // This test just makes sure that no non-error output goes + // to stderr + name := "testbuildstderr" + _, _, stderr, err := buildImageWithStdoutStderr(name, + "FROM busybox\nRUN echo one", true) + if err != nil { + c.Fatal(err) + } + + if runtime.GOOS == "windows" { + // stderr might contain a security warning on windows + lines := strings.Split(stderr, "\n") + for _, v := range lines { + if v != "" && !strings.Contains(v, "SECURITY WARNING:") { + c.Fatalf("Stderr contains unexpected output line: %q", v) + } + } + } else { + if stderr != "" { + c.Fatalf("Stderr should have been empty, instead its: %q", stderr) + } + } +} + +func (s *DockerSuite) TestBuildChownSingleFile(c *check.C) { + testRequires(c, UnixCli) // test uses chown: not available on windows + testRequires(c, DaemonIsLinux) + + name := "testbuildchownsinglefile" + + ctx, err := fakeContext(` +FROM busybox +COPY test / +RUN ls -l /test +RUN [ $(ls -l /test | awk '{print $3":"$4}') = 'root:root' ] +`, map[string]string{ + "test": "test", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if err := os.Chown(filepath.Join(ctx.Dir, "test"), 4242, 4242); err != nil { + c.Fatal(err) + } + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } + +} + +func (s *DockerSuite) TestBuildSymlinkBreakout(c *check.C) { + name := "testbuildsymlinkbreakout" + tmpdir, err := ioutil.TempDir("", name) + c.Assert(err, check.IsNil) + defer os.RemoveAll(tmpdir) + ctx := filepath.Join(tmpdir, "context") + if err := os.MkdirAll(ctx, 0755); err != nil { + c.Fatal(err) + } + if err := ioutil.WriteFile(filepath.Join(ctx, "Dockerfile"), []byte(` + from busybox + add symlink.tar / + add inject /symlink/ + `), 0644); err != nil { + c.Fatal(err) + } + inject := filepath.Join(ctx, "inject") + if err := ioutil.WriteFile(inject, nil, 0644); err != nil { + c.Fatal(err) + } + f, err := os.Create(filepath.Join(ctx, "symlink.tar")) + if err != nil { + c.Fatal(err) + } + w := tar.NewWriter(f) + w.WriteHeader(&tar.Header{ + Name: "symlink2", + Typeflag: tar.TypeSymlink, + Linkname: "/../../../../../../../../../../../../../../", + Uid: os.Getuid(), + Gid: os.Getgid(), + }) + w.WriteHeader(&tar.Header{ + Name: "symlink", + Typeflag: tar.TypeSymlink, + Linkname: filepath.Join("symlink2", tmpdir), + Uid: os.Getuid(), + Gid: os.Getgid(), + }) + w.Close() + f.Close() + if _, err := buildImageFromContext(name, fakeContextFromDir(ctx), false); err != nil { + c.Fatal(err) + } + if _, err := os.Lstat(filepath.Join(tmpdir, "inject")); err == nil { + c.Fatal("symlink breakout - inject") + } else if !os.IsNotExist(err) { + c.Fatalf("unexpected error: %v", err) + } +} + +func (s *DockerSuite) TestBuildXZHost(c *check.C) { + // /usr/local/sbin/xz gets permission denied for the user + testRequires(c, NotUserNamespace) + testRequires(c, DaemonIsLinux) + name := "testbuildxzhost" + + ctx, err := fakeContext(` +FROM busybox +ADD xz /usr/local/sbin/ +RUN chmod 755 /usr/local/sbin/xz +ADD test.xz / +RUN [ ! -e /injected ]`, + map[string]string{ + "test.xz": "\xfd\x37\x7a\x58\x5a\x00\x00\x04\xe6\xd6\xb4\x46\x02\x00" + + "\x21\x01\x16\x00\x00\x00\x74\x2f\xe5\xa3\x01\x00\x3f\xfd" + + "\x37\x7a\x58\x5a\x00\x00\x04\xe6\xd6\xb4\x46\x02\x00\x21", + "xz": "#!/bin/sh\ntouch /injected", + }) + + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatal(err) + } + +} + +func (s *DockerSuite) TestBuildVolumesRetainContents(c *check.C) { + // /foo/file gets permission denied for the user + testRequires(c, NotUserNamespace) + testRequires(c, DaemonIsLinux) // TODO Windows: Issue #20127 + var ( + name = "testbuildvolumescontent" + expected = "some text" + volName = "/foo" + ) + + if daemonPlatform == "windows" { + volName = "C:/foo" + } + + ctx, err := fakeContext(` +FROM busybox +COPY content /foo/file +VOLUME `+volName+` +CMD cat /foo/file`, + map[string]string{ + "content": expected, + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, false); err != nil { + c.Fatal(err) + } + + out, _ := dockerCmd(c, "run", "--rm", name) + if out != expected { + c.Fatalf("expected file contents for /foo/file to be %q but received %q", expected, out) + } + +} + +func (s *DockerSuite) TestBuildRenamedDockerfile(c *check.C) { + + ctx, err := fakeContext(`FROM busybox + RUN echo from Dockerfile`, + map[string]string{ + "Dockerfile": "FROM busybox\nRUN echo from Dockerfile", + "files/Dockerfile": "FROM busybox\nRUN echo from files/Dockerfile", + "files/dFile": "FROM busybox\nRUN echo from files/dFile", + "dFile": "FROM busybox\nRUN echo from dFile", + "files/dFile2": "FROM busybox\nRUN echo from files/dFile2", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + out, _, err := dockerCmdInDir(c, ctx.Dir, "build", "-t", "test1", ".") + if err != nil { + c.Fatalf("Failed to build: %s\n%s", out, err) + } + if !strings.Contains(out, "from Dockerfile") { + c.Fatalf("test1 should have used Dockerfile, output:%s", out) + } + + out, _, err = dockerCmdInDir(c, ctx.Dir, "build", "-f", filepath.Join("files", "Dockerfile"), "-t", "test2", ".") + if err != nil { + c.Fatal(err) + } + if !strings.Contains(out, "from files/Dockerfile") { + c.Fatalf("test2 should have used files/Dockerfile, output:%s", out) + } + + out, _, err = dockerCmdInDir(c, ctx.Dir, "build", fmt.Sprintf("--file=%s", filepath.Join("files", "dFile")), "-t", "test3", ".") + if err != nil { + c.Fatal(err) + } + if !strings.Contains(out, "from files/dFile") { + c.Fatalf("test3 should have used files/dFile, output:%s", out) + } + + out, _, err = dockerCmdInDir(c, ctx.Dir, "build", "--file=dFile", "-t", "test4", ".") + if err != nil { + c.Fatal(err) + } + if !strings.Contains(out, "from dFile") { + c.Fatalf("test4 should have used dFile, output:%s", out) + } + + dirWithNoDockerfile, err := ioutil.TempDir(os.TempDir(), "test5") + c.Assert(err, check.IsNil) + nonDockerfileFile := filepath.Join(dirWithNoDockerfile, "notDockerfile") + if _, err = os.Create(nonDockerfileFile); err != nil { + c.Fatal(err) + } + out, _, err = dockerCmdInDir(c, ctx.Dir, "build", fmt.Sprintf("--file=%s", nonDockerfileFile), "-t", "test5", ".") + + if err == nil { + c.Fatalf("test5 was supposed to fail to find passwd") + } + + if expected := fmt.Sprintf("The Dockerfile (%s) must be within the build context (.)", nonDockerfileFile); !strings.Contains(out, expected) { + c.Fatalf("wrong error messsage:%v\nexpected to contain=%v", out, expected) + } + + out, _, err = dockerCmdInDir(c, filepath.Join(ctx.Dir, "files"), "build", "-f", filepath.Join("..", "Dockerfile"), "-t", "test6", "..") + if err != nil { + c.Fatalf("test6 failed: %s", err) + } + if !strings.Contains(out, "from Dockerfile") { + c.Fatalf("test6 should have used root Dockerfile, output:%s", out) + } + + out, _, err = dockerCmdInDir(c, filepath.Join(ctx.Dir, "files"), "build", "-f", filepath.Join(ctx.Dir, "files", "Dockerfile"), "-t", "test7", "..") + if err != nil { + c.Fatalf("test7 failed: %s", err) + } + if !strings.Contains(out, "from files/Dockerfile") { + c.Fatalf("test7 should have used files Dockerfile, output:%s", out) + } + + out, _, err = dockerCmdInDir(c, filepath.Join(ctx.Dir, "files"), "build", "-f", filepath.Join("..", "Dockerfile"), "-t", "test8", ".") + if err == nil || !strings.Contains(out, "must be within the build context") { + c.Fatalf("test8 should have failed with Dockerfile out of context: %s", err) + } + + tmpDir := os.TempDir() + out, _, err = dockerCmdInDir(c, tmpDir, "build", "-t", "test9", ctx.Dir) + if err != nil { + c.Fatalf("test9 - failed: %s", err) + } + if !strings.Contains(out, "from Dockerfile") { + c.Fatalf("test9 should have used root Dockerfile, output:%s", out) + } + + out, _, err = dockerCmdInDir(c, filepath.Join(ctx.Dir, "files"), "build", "-f", "dFile2", "-t", "test10", ".") + if err != nil { + c.Fatalf("test10 should have worked: %s", err) + } + if !strings.Contains(out, "from files/dFile2") { + c.Fatalf("test10 should have used files/dFile2, output:%s", out) + } + +} + +func (s *DockerSuite) TestBuildFromMixedcaseDockerfile(c *check.C) { + testRequires(c, UnixCli) // Dockerfile overwrites dockerfile on windows + testRequires(c, DaemonIsLinux) + + ctx, err := fakeContext(`FROM busybox + RUN echo from dockerfile`, + map[string]string{ + "dockerfile": "FROM busybox\nRUN echo from dockerfile", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + out, _, err := dockerCmdInDir(c, ctx.Dir, "build", "-t", "test1", ".") + if err != nil { + c.Fatalf("Failed to build: %s\n%s", out, err) + } + + if !strings.Contains(out, "from dockerfile") { + c.Fatalf("Missing proper output: %s", out) + } + +} + +func (s *DockerSuite) TestBuildWithTwoDockerfiles(c *check.C) { + testRequires(c, UnixCli) // Dockerfile overwrites dockerfile on windows + testRequires(c, DaemonIsLinux) + + ctx, err := fakeContext(`FROM busybox +RUN echo from Dockerfile`, + map[string]string{ + "dockerfile": "FROM busybox\nRUN echo from dockerfile", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + out, _, err := dockerCmdInDir(c, ctx.Dir, "build", "-t", "test1", ".") + if err != nil { + c.Fatalf("Failed to build: %s\n%s", out, err) + } + + if !strings.Contains(out, "from Dockerfile") { + c.Fatalf("Missing proper output: %s", out) + } + +} + +func (s *DockerSuite) TestBuildFromURLWithF(c *check.C) { + testRequires(c, DaemonIsLinux) + + server, err := fakeStorage(map[string]string{"baz": `FROM busybox +RUN echo from baz +COPY * /tmp/ +RUN find /tmp/`}) + if err != nil { + c.Fatal(err) + } + defer server.Close() + + ctx, err := fakeContext(`FROM busybox +RUN echo from Dockerfile`, + map[string]string{}) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + // Make sure that -f is ignored and that we don't use the Dockerfile + // that's in the current dir + out, _, err := dockerCmdInDir(c, ctx.Dir, "build", "-f", "baz", "-t", "test1", server.URL()+"/baz") + if err != nil { + c.Fatalf("Failed to build: %s\n%s", out, err) + } + + if !strings.Contains(out, "from baz") || + strings.Contains(out, "/tmp/baz") || + !strings.Contains(out, "/tmp/Dockerfile") { + c.Fatalf("Missing proper output: %s", out) + } + +} + +func (s *DockerSuite) TestBuildFromStdinWithF(c *check.C) { + testRequires(c, DaemonIsLinux) // TODO Windows: This test is flaky; no idea why + ctx, err := fakeContext(`FROM busybox +RUN echo "from Dockerfile"`, + map[string]string{}) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + // Make sure that -f is ignored and that we don't use the Dockerfile + // that's in the current dir + dockerCommand := exec.Command(dockerBinary, "build", "-f", "baz", "-t", "test1", "-") + dockerCommand.Dir = ctx.Dir + dockerCommand.Stdin = strings.NewReader(`FROM busybox +RUN echo "from baz" +COPY * /tmp/ +RUN sh -c "find /tmp/" # sh -c is needed on Windows to use the correct find`) + out, status, err := runCommandWithOutput(dockerCommand) + if err != nil || status != 0 { + c.Fatalf("Error building: %s", err) + } + + if !strings.Contains(out, "from baz") || + strings.Contains(out, "/tmp/baz") || + !strings.Contains(out, "/tmp/Dockerfile") { + c.Fatalf("Missing proper output: %s", out) + } + +} + +func (s *DockerSuite) TestBuildFromOfficialNames(c *check.C) { + name := "testbuildfromofficial" + fromNames := []string{ + "busybox", + "docker.io/busybox", + "index.docker.io/busybox", + "library/busybox", + "docker.io/library/busybox", + "index.docker.io/library/busybox", + } + for idx, fromName := range fromNames { + imgName := fmt.Sprintf("%s%d", name, idx) + _, err := buildImage(imgName, "FROM "+fromName, true) + if err != nil { + c.Errorf("Build failed using FROM %s: %s", fromName, err) + } + deleteImages(imgName) + } +} + +func (s *DockerSuite) TestBuildDockerfileOutsideContext(c *check.C) { + testRequires(c, UnixCli) // uses os.Symlink: not implemented in windows at the time of writing (go-1.4.2) + testRequires(c, DaemonIsLinux) + + name := "testbuilddockerfileoutsidecontext" + tmpdir, err := ioutil.TempDir("", name) + c.Assert(err, check.IsNil) + defer os.RemoveAll(tmpdir) + ctx := filepath.Join(tmpdir, "context") + if err := os.MkdirAll(ctx, 0755); err != nil { + c.Fatal(err) + } + if err := ioutil.WriteFile(filepath.Join(ctx, "Dockerfile"), []byte("FROM scratch\nENV X Y"), 0644); err != nil { + c.Fatal(err) + } + wd, err := os.Getwd() + if err != nil { + c.Fatal(err) + } + defer os.Chdir(wd) + if err := os.Chdir(ctx); err != nil { + c.Fatal(err) + } + if err := ioutil.WriteFile(filepath.Join(tmpdir, "outsideDockerfile"), []byte("FROM scratch\nENV x y"), 0644); err != nil { + c.Fatal(err) + } + if err := os.Symlink(filepath.Join("..", "outsideDockerfile"), filepath.Join(ctx, "dockerfile1")); err != nil { + c.Fatal(err) + } + if err := os.Symlink(filepath.Join(tmpdir, "outsideDockerfile"), filepath.Join(ctx, "dockerfile2")); err != nil { + c.Fatal(err) + } + + for _, dockerfilePath := range []string{ + filepath.Join("..", "outsideDockerfile"), + filepath.Join(ctx, "dockerfile1"), + filepath.Join(ctx, "dockerfile2"), + } { + out, _, err := dockerCmdWithError("build", "-t", name, "--no-cache", "-f", dockerfilePath, ".") + if err == nil { + c.Fatalf("Expected error with %s. Out: %s", dockerfilePath, out) + } + if !strings.Contains(out, "must be within the build context") && !strings.Contains(out, "Cannot locate Dockerfile") { + c.Fatalf("Unexpected error with %s. Out: %s", dockerfilePath, out) + } + deleteImages(name) + } + + os.Chdir(tmpdir) + + // Path to Dockerfile should be resolved relative to working directory, not relative to context. + // There is a Dockerfile in the context, but since there is no Dockerfile in the current directory, the following should fail + out, _, err := dockerCmdWithError("build", "-t", name, "--no-cache", "-f", "Dockerfile", ctx) + if err == nil { + c.Fatalf("Expected error. Out: %s", out) + } +} + +func (s *DockerSuite) TestBuildSpaces(c *check.C) { + // Test to make sure that leading/trailing spaces on a command + // doesn't change the error msg we get + var ( + err1 error + err2 error + ) + + name := "testspaces" + ctx, err := fakeContext("FROM busybox\nCOPY\n", + map[string]string{ + "Dockerfile": "FROM busybox\nCOPY\n", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err1 = buildImageFromContext(name, ctx, false); err1 == nil { + c.Fatal("Build 1 was supposed to fail, but didn't") + } + + ctx.Add("Dockerfile", "FROM busybox\nCOPY ") + if _, err2 = buildImageFromContext(name, ctx, false); err2 == nil { + c.Fatal("Build 2 was supposed to fail, but didn't") + } + + removeLogTimestamps := func(s string) string { + return regexp.MustCompile(`time="(.*?)"`).ReplaceAllString(s, `time=[TIMESTAMP]`) + } + + // Skip over the times + e1 := removeLogTimestamps(err1.Error()) + e2 := removeLogTimestamps(err2.Error()) + + // Ignore whitespace since that's what were verifying doesn't change stuff + if strings.Replace(e1, " ", "", -1) != strings.Replace(e2, " ", "", -1) { + c.Fatalf("Build 2's error wasn't the same as build 1's\n1:%s\n2:%s", err1, err2) + } + + ctx.Add("Dockerfile", "FROM busybox\n COPY") + if _, err2 = buildImageFromContext(name, ctx, false); err2 == nil { + c.Fatal("Build 3 was supposed to fail, but didn't") + } + + // Skip over the times + e1 = removeLogTimestamps(err1.Error()) + e2 = removeLogTimestamps(err2.Error()) + + // Ignore whitespace since that's what were verifying doesn't change stuff + if strings.Replace(e1, " ", "", -1) != strings.Replace(e2, " ", "", -1) { + c.Fatalf("Build 3's error wasn't the same as build 1's\n1:%s\n3:%s", err1, err2) + } + + ctx.Add("Dockerfile", "FROM busybox\n COPY ") + if _, err2 = buildImageFromContext(name, ctx, false); err2 == nil { + c.Fatal("Build 4 was supposed to fail, but didn't") + } + + // Skip over the times + e1 = removeLogTimestamps(err1.Error()) + e2 = removeLogTimestamps(err2.Error()) + + // Ignore whitespace since that's what were verifying doesn't change stuff + if strings.Replace(e1, " ", "", -1) != strings.Replace(e2, " ", "", -1) { + c.Fatalf("Build 4's error wasn't the same as build 1's\n1:%s\n4:%s", err1, err2) + } + +} + +func (s *DockerSuite) TestBuildSpacesWithQuotes(c *check.C) { + testRequires(c, DaemonIsLinux) + // Test to make sure that spaces in quotes aren't lost + name := "testspacesquotes" + + dockerfile := `FROM busybox +RUN echo " \ + foo "` + + _, out, err := buildImageWithOut(name, dockerfile, false) + if err != nil { + c.Fatal("Build failed:", err) + } + + expecting := "\n foo \n" + if !strings.Contains(out, expecting) { + c.Fatalf("Bad output: %q expecting to contain %q", out, expecting) + } + +} + +// #4393 +func (s *DockerSuite) TestBuildVolumeFileExistsinContainer(c *check.C) { + testRequires(c, DaemonIsLinux) // TODO Windows: This should error out + buildCmd := exec.Command(dockerBinary, "build", "-t", "docker-test-errcreatevolumewithfile", "-") + buildCmd.Stdin = strings.NewReader(` + FROM busybox + RUN touch /foo + VOLUME /foo + `) + + out, _, err := runCommandWithOutput(buildCmd) + if err == nil || !strings.Contains(out, "file exists") { + c.Fatalf("expected build to fail when file exists in container at requested volume path") + } + +} + +func (s *DockerSuite) TestBuildMissingArgs(c *check.C) { + // Test to make sure that all Dockerfile commands (except the ones listed + // in skipCmds) will generate an error if no args are provided. + // Note: INSERT is deprecated so we exclude it because of that. + skipCmds := map[string]struct{}{ + "CMD": {}, + "RUN": {}, + "ENTRYPOINT": {}, + "INSERT": {}, + } + + if daemonPlatform == "windows" { + skipCmds = map[string]struct{}{ + "CMD": {}, + "RUN": {}, + "ENTRYPOINT": {}, + "INSERT": {}, + "STOPSIGNAL": {}, + "ARG": {}, + "USER": {}, + "EXPOSE": {}, + } + } + + for cmd := range command.Commands { + cmd = strings.ToUpper(cmd) + if _, ok := skipCmds[cmd]; ok { + continue + } + + var dockerfile string + if cmd == "FROM" { + dockerfile = cmd + } else { + // Add FROM to make sure we don't complain about it missing + dockerfile = "FROM busybox\n" + cmd + } + + ctx, err := fakeContext(dockerfile, map[string]string{}) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + var out string + if out, err = buildImageFromContext("args", ctx, true); err == nil { + c.Fatalf("%s was supposed to fail. Out:%s", cmd, out) + } + if !strings.Contains(err.Error(), cmd+" requires") { + c.Fatalf("%s returned the wrong type of error:%s", cmd, err) + } + } + +} + +func (s *DockerSuite) TestBuildEmptyScratch(c *check.C) { + testRequires(c, DaemonIsLinux) + _, out, err := buildImageWithOut("sc", "FROM scratch", true) + if err == nil { + c.Fatalf("Build was supposed to fail") + } + if !strings.Contains(out, "No image was generated") { + c.Fatalf("Wrong error message: %v", out) + } +} + +func (s *DockerSuite) TestBuildDotDotFile(c *check.C) { + ctx, err := fakeContext("FROM busybox\n", + map[string]string{ + "..gitme": "", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err = buildImageFromContext("sc", ctx, false); err != nil { + c.Fatalf("Build was supposed to work: %s", err) + } +} + +func (s *DockerSuite) TestBuildRUNoneJSON(c *check.C) { + testRequires(c, DaemonIsLinux) // No hello-world Windows image + name := "testbuildrunonejson" + + ctx, err := fakeContext(`FROM hello-world:frozen +RUN [ "/hello" ]`, map[string]string{}) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + out, _, err := dockerCmdInDir(c, ctx.Dir, "build", "--no-cache", "-t", name, ".") + if err != nil { + c.Fatalf("failed to build the image: %s, %v", out, err) + } + + if !strings.Contains(out, "Hello from Docker") { + c.Fatalf("bad output: %s", out) + } + +} + +func (s *DockerSuite) TestBuildEmptyStringVolume(c *check.C) { + name := "testbuildemptystringvolume" + + _, err := buildImage(name, ` + FROM busybox + ENV foo="" + VOLUME $foo + `, false) + if err == nil { + c.Fatal("Should have failed to build") + } + +} + +func (s *DockerSuite) TestBuildContainerWithCgroupParent(c *check.C) { + testRequires(c, SameHostDaemon) + testRequires(c, DaemonIsLinux) + + cgroupParent := "test" + data, err := ioutil.ReadFile("/proc/self/cgroup") + if err != nil { + c.Fatalf("failed to read '/proc/self/cgroup - %v", err) + } + selfCgroupPaths := parseCgroupPaths(string(data)) + _, found := selfCgroupPaths["memory"] + if !found { + c.Fatalf("unable to find self memory cgroup path. CgroupsPath: %v", selfCgroupPaths) + } + cmd := exec.Command(dockerBinary, "build", "--cgroup-parent", cgroupParent, "-") + cmd.Stdin = strings.NewReader(` +FROM busybox +RUN cat /proc/self/cgroup +`) + + out, _, err := runCommandWithOutput(cmd) + if err != nil { + c.Fatalf("unexpected failure when running container with --cgroup-parent option - %s\n%v", string(out), err) + } + m, err := regexp.MatchString(fmt.Sprintf("memory:.*/%s/.*", cgroupParent), out) + c.Assert(err, check.IsNil) + if !m { + c.Fatalf("There is no expected memory cgroup with parent /%s/: %s", cgroupParent, out) + } +} + +func (s *DockerSuite) TestBuildNoDupOutput(c *check.C) { + // Check to make sure our build output prints the Dockerfile cmd + // property - there was a bug that caused it to be duplicated on the + // Step X line + name := "testbuildnodupoutput" + + _, out, err := buildImageWithOut(name, ` + FROM busybox + RUN env`, false) + if err != nil { + c.Fatalf("Build should have worked: %q", err) + } + + exp := "\nStep 2 : RUN env\n" + if !strings.Contains(out, exp) { + c.Fatalf("Bad output\nGot:%s\n\nExpected to contain:%s\n", out, exp) + } +} + +// GH15826 +func (s *DockerSuite) TestBuildStartsFromOne(c *check.C) { + // Explicit check to ensure that build starts from step 1 rather than 0 + name := "testbuildstartsfromone" + + _, out, err := buildImageWithOut(name, ` + FROM busybox`, false) + if err != nil { + c.Fatalf("Build should have worked: %q", err) + } + + exp := "\nStep 1 : FROM busybox\n" + if !strings.Contains(out, exp) { + c.Fatalf("Bad output\nGot:%s\n\nExpected to contain:%s\n", out, exp) + } +} + +func (s *DockerSuite) TestBuildBadCmdFlag(c *check.C) { + name := "testbuildbadcmdflag" + + _, out, err := buildImageWithOut(name, ` + FROM busybox + MAINTAINER --boo joe@example.com`, false) + if err == nil { + c.Fatal("Build should have failed") + } + + exp := "\nUnknown flag: boo\n" + if !strings.Contains(out, exp) { + c.Fatalf("Bad output\nGot:%s\n\nExpected to contain:%s\n", out, exp) + } +} + +func (s *DockerSuite) TestBuildRUNErrMsg(c *check.C) { + // Test to make sure the bad command is quoted with just "s and + // not as a Go []string + name := "testbuildbadrunerrmsg" + _, out, err := buildImageWithOut(name, ` + FROM busybox + RUN badEXE a1 \& a2 a3`, false) // tab between a2 and a3 + if err == nil { + c.Fatal("Should have failed to build") + } + shell := "/bin/sh -c" + exitCode := "127" + if daemonPlatform == "windows" { + shell = "cmd /S /C" + // architectural - Windows has to start the container to determine the exe is bad, Linux does not + exitCode = "1" + } + exp := `The command '` + shell + ` badEXE a1 \& a2 a3' returned a non-zero code: ` + exitCode + if !strings.Contains(out, exp) { + c.Fatalf("RUN doesn't have the correct output:\nGot:%s\nExpected:%s", out, exp) + } +} + +func (s *DockerTrustSuite) TestTrustedBuild(c *check.C) { + repoName := s.setupTrustedImage(c, "trusted-build") + dockerFile := fmt.Sprintf(` + FROM %s + RUN [] + `, repoName) + + name := "testtrustedbuild" + + buildCmd := buildImageCmd(name, dockerFile, true) + s.trustedCmd(buildCmd) + out, _, err := runCommandWithOutput(buildCmd) + if err != nil { + c.Fatalf("Error running trusted build: %s\n%s", err, out) + } + + if !strings.Contains(out, fmt.Sprintf("FROM %s@sha", repoName[:len(repoName)-7])) { + c.Fatalf("Unexpected output on trusted build:\n%s", out) + } + + // We should also have a tag reference for the image. + if out, exitCode := dockerCmd(c, "inspect", repoName); exitCode != 0 { + c.Fatalf("unexpected exit code inspecting image %q: %d: %s", repoName, exitCode, out) + } + + // We should now be able to remove the tag reference. + if out, exitCode := dockerCmd(c, "rmi", repoName); exitCode != 0 { + c.Fatalf("unexpected exit code inspecting image %q: %d: %s", repoName, exitCode, out) + } +} + +func (s *DockerTrustSuite) TestTrustedBuildUntrustedTag(c *check.C) { + repoName := fmt.Sprintf("%v/dockercli/build-untrusted-tag:latest", privateRegistryURL) + dockerFile := fmt.Sprintf(` + FROM %s + RUN [] + `, repoName) + + name := "testtrustedbuilduntrustedtag" + + buildCmd := buildImageCmd(name, dockerFile, true) + s.trustedCmd(buildCmd) + out, _, err := runCommandWithOutput(buildCmd) + if err == nil { + c.Fatalf("Expected error on trusted build with untrusted tag: %s\n%s", err, out) + } + + if !strings.Contains(out, "does not have trust data for") { + c.Fatalf("Unexpected output on trusted build with untrusted tag:\n%s", out) + } +} + +func (s *DockerTrustSuite) TestBuildContextDirIsSymlink(c *check.C) { + testRequires(c, DaemonIsLinux) + tempDir, err := ioutil.TempDir("", "test-build-dir-is-symlink-") + c.Assert(err, check.IsNil) + defer os.RemoveAll(tempDir) + + // Make a real context directory in this temp directory with a simple + // Dockerfile. + realContextDirname := filepath.Join(tempDir, "context") + if err := os.Mkdir(realContextDirname, os.FileMode(0755)); err != nil { + c.Fatal(err) + } + + if err = ioutil.WriteFile( + filepath.Join(realContextDirname, "Dockerfile"), + []byte(` + FROM busybox + RUN echo hello world + `), + os.FileMode(0644), + ); err != nil { + c.Fatal(err) + } + + // Make a symlink to the real context directory. + contextSymlinkName := filepath.Join(tempDir, "context_link") + if err := os.Symlink(realContextDirname, contextSymlinkName); err != nil { + c.Fatal(err) + } + + // Executing the build with the symlink as the specified context should + // *not* fail. + if out, exitStatus := dockerCmd(c, "build", contextSymlinkName); exitStatus != 0 { + c.Fatalf("build failed with exit status %d: %s", exitStatus, out) + } +} + +func (s *DockerTrustSuite) TestTrustedBuildTagFromReleasesRole(c *check.C) { + testRequires(c, NotaryHosting) + + latestTag := s.setupTrustedImage(c, "trusted-build-releases-role") + repoName := strings.TrimSuffix(latestTag, ":latest") + + // Now create the releases role + s.notaryCreateDelegation(c, repoName, "targets/releases", s.not.keys[0].Public) + s.notaryImportKey(c, repoName, "targets/releases", s.not.keys[0].Private) + s.notaryPublish(c, repoName) + + // push a different tag to the releases role + otherTag := fmt.Sprintf("%s:other", repoName) + dockerCmd(c, "tag", "busybox", otherTag) + + pushCmd := exec.Command(dockerBinary, "push", otherTag) + s.trustedCmd(pushCmd) + out, _, err := runCommandWithOutput(pushCmd) + c.Assert(err, check.IsNil, check.Commentf("Trusted push failed: %s", out)) + s.assertTargetInRoles(c, repoName, "other", "targets/releases") + s.assertTargetNotInRoles(c, repoName, "other", "targets") + + out, status := dockerCmd(c, "rmi", otherTag) + c.Assert(status, check.Equals, 0, check.Commentf("docker rmi failed: %s", out)) + + dockerFile := fmt.Sprintf(` + FROM %s + RUN [] + `, otherTag) + + name := "testtrustedbuildreleasesrole" + + buildCmd := buildImageCmd(name, dockerFile, true) + s.trustedCmd(buildCmd) + out, _, err = runCommandWithOutput(buildCmd) + c.Assert(err, check.IsNil, check.Commentf("Trusted build failed: %s", out)) + c.Assert(out, checker.Contains, fmt.Sprintf("FROM %s@sha", repoName)) +} + +func (s *DockerTrustSuite) TestTrustedBuildTagIgnoresOtherDelegationRoles(c *check.C) { + testRequires(c, NotaryHosting) + + latestTag := s.setupTrustedImage(c, "trusted-build-releases-role") + repoName := strings.TrimSuffix(latestTag, ":latest") + + // Now create a non-releases delegation role + s.notaryCreateDelegation(c, repoName, "targets/other", s.not.keys[0].Public) + s.notaryImportKey(c, repoName, "targets/other", s.not.keys[0].Private) + s.notaryPublish(c, repoName) + + // push a different tag to the other role + otherTag := fmt.Sprintf("%s:other", repoName) + dockerCmd(c, "tag", "busybox", otherTag) + + pushCmd := exec.Command(dockerBinary, "push", otherTag) + s.trustedCmd(pushCmd) + out, _, err := runCommandWithOutput(pushCmd) + c.Assert(err, check.IsNil, check.Commentf("Trusted push failed: %s", out)) + s.assertTargetInRoles(c, repoName, "other", "targets/other") + s.assertTargetNotInRoles(c, repoName, "other", "targets") + + out, status := dockerCmd(c, "rmi", otherTag) + c.Assert(status, check.Equals, 0, check.Commentf("docker rmi failed: %s", out)) + + dockerFile := fmt.Sprintf(` + FROM %s + RUN [] + `, otherTag) + + name := "testtrustedbuildotherrole" + + buildCmd := buildImageCmd(name, dockerFile, true) + s.trustedCmd(buildCmd) + out, _, err = runCommandWithOutput(buildCmd) + c.Assert(err, check.NotNil, check.Commentf("Trusted build expected to fail: %s", out)) +} + +// Issue #15634: COPY fails when path starts with "null" +func (s *DockerSuite) TestBuildNullStringInAddCopyVolume(c *check.C) { + name := "testbuildnullstringinaddcopyvolume" + + volName := "nullvolume" + + if daemonPlatform == "windows" { + volName = `C:\\nullvolume` + } + + ctx, err := fakeContext(` + FROM busybox + + ADD null / + COPY nullfile / + VOLUME `+volName+` + `, + map[string]string{ + "null": "test1", + "nullfile": "test2", + }, + ) + c.Assert(err, check.IsNil) + defer ctx.Close() + + _, err = buildImageFromContext(name, ctx, true) + c.Assert(err, check.IsNil) +} + +func (s *DockerSuite) TestBuildStopSignal(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows does not support STOPSIGNAL yet + imgName := "test_build_stop_signal" + _, err := buildImage(imgName, + `FROM busybox + STOPSIGNAL SIGKILL`, + true) + c.Assert(err, check.IsNil) + res := inspectFieldJSON(c, imgName, "Config.StopSignal") + if res != `"SIGKILL"` { + c.Fatalf("Signal %s, expected SIGKILL", res) + } + + containerName := "test-container-stop-signal" + dockerCmd(c, "run", "-d", "--name", containerName, imgName, "top") + + res = inspectFieldJSON(c, containerName, "Config.StopSignal") + if res != `"SIGKILL"` { + c.Fatalf("Signal %s, expected SIGKILL", res) + } +} + +func (s *DockerSuite) TestBuildBuildTimeArg(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows does not support ARG + imgName := "bldargtest" + envKey := "foo" + envVal := "bar" + args := []string{ + "--build-arg", fmt.Sprintf("%s=%s", envKey, envVal), + } + dockerfile := fmt.Sprintf(`FROM busybox + ARG %s + RUN echo $%s + CMD echo $%s`, envKey, envKey, envKey) + + if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err != nil || !strings.Contains(out, envVal) { + if err != nil { + c.Fatalf("build failed to complete: %q %q", out, err) + } + c.Fatalf("failed to access environment variable in output: %q expected: %q", out, envVal) + } + + containerName := "bldargCont" + if out, _ := dockerCmd(c, "run", "--name", containerName, imgName); out != "\n" { + c.Fatalf("run produced invalid output: %q, expected empty string", out) + } +} + +func (s *DockerSuite) TestBuildBuildTimeArgHistory(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows does not support ARG + imgName := "bldargtest" + envKey := "foo" + envVal := "bar" + envDef := "bar1" + args := []string{ + "--build-arg", fmt.Sprintf("%s=%s", envKey, envVal), + } + dockerfile := fmt.Sprintf(`FROM busybox + ARG %s=%s`, envKey, envDef) + + if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err != nil || !strings.Contains(out, envVal) { + if err != nil { + c.Fatalf("build failed to complete: %q %q", out, err) + } + c.Fatalf("failed to access environment variable in output: %q expected: %q", out, envVal) + } + + out, _ := dockerCmd(c, "history", "--no-trunc", imgName) + outputTabs := strings.Split(out, "\n")[1] + if !strings.Contains(outputTabs, envDef) { + c.Fatalf("failed to find arg default in image history output: %q expected: %q", outputTabs, envDef) + } +} + +func (s *DockerSuite) TestBuildBuildTimeArgCacheHit(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows does not support ARG + imgName := "bldargtest" + envKey := "foo" + envVal := "bar" + args := []string{ + "--build-arg", fmt.Sprintf("%s=%s", envKey, envVal), + } + dockerfile := fmt.Sprintf(`FROM busybox + ARG %s + RUN echo $%s`, envKey, envKey) + + origImgID := "" + var err error + if origImgID, err = buildImage(imgName, dockerfile, true, args...); err != nil { + c.Fatal(err) + } + + imgNameCache := "bldargtestcachehit" + if newImgID, err := buildImage(imgNameCache, dockerfile, true, args...); err != nil || newImgID != origImgID { + if err != nil { + c.Fatal(err) + } + c.Fatalf("build didn't use cache! expected image id: %q built image id: %q", origImgID, newImgID) + } +} + +func (s *DockerSuite) TestBuildBuildTimeArgCacheMissExtraArg(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows does not support ARG + imgName := "bldargtest" + envKey := "foo" + envVal := "bar" + extraEnvKey := "foo1" + extraEnvVal := "bar1" + args := []string{ + "--build-arg", fmt.Sprintf("%s=%s", envKey, envVal), + } + + dockerfile := fmt.Sprintf(`FROM busybox + ARG %s + ARG %s + RUN echo $%s`, envKey, extraEnvKey, envKey) + + origImgID := "" + var err error + if origImgID, err = buildImage(imgName, dockerfile, true, args...); err != nil { + c.Fatal(err) + } + + imgNameCache := "bldargtestcachemiss" + args = append(args, "--build-arg", fmt.Sprintf("%s=%s", extraEnvKey, extraEnvVal)) + if newImgID, err := buildImage(imgNameCache, dockerfile, true, args...); err != nil || newImgID == origImgID { + if err != nil { + c.Fatal(err) + } + c.Fatalf("build used cache, expected a miss!") + } +} + +func (s *DockerSuite) TestBuildBuildTimeArgCacheMissSameArgDiffVal(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows does not support ARG + imgName := "bldargtest" + envKey := "foo" + envVal := "bar" + newEnvVal := "bar1" + args := []string{ + "--build-arg", fmt.Sprintf("%s=%s", envKey, envVal), + } + + dockerfile := fmt.Sprintf(`FROM busybox + ARG %s + RUN echo $%s`, envKey, envKey) + + origImgID := "" + var err error + if origImgID, err = buildImage(imgName, dockerfile, true, args...); err != nil { + c.Fatal(err) + } + + imgNameCache := "bldargtestcachemiss" + args = []string{ + "--build-arg", fmt.Sprintf("%s=%s", envKey, newEnvVal), + } + if newImgID, err := buildImage(imgNameCache, dockerfile, true, args...); err != nil || newImgID == origImgID { + if err != nil { + c.Fatal(err) + } + c.Fatalf("build used cache, expected a miss!") + } +} + +func (s *DockerSuite) TestBuildBuildTimeArgOverrideArgDefinedBeforeEnv(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows does not support ARG + imgName := "bldargtest" + envKey := "foo" + envVal := "bar" + envValOveride := "barOverride" + args := []string{ + "--build-arg", fmt.Sprintf("%s=%s", envKey, envVal), + } + dockerfile := fmt.Sprintf(`FROM busybox + ARG %s + ENV %s %s + RUN echo $%s + CMD echo $%s + `, envKey, envKey, envValOveride, envKey, envKey) + + if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err != nil || strings.Count(out, envValOveride) != 2 { + if err != nil { + c.Fatalf("build failed to complete: %q %q", out, err) + } + c.Fatalf("failed to access environment variable in output: %q expected: %q", out, envValOveride) + } + + containerName := "bldargCont" + if out, _ := dockerCmd(c, "run", "--name", containerName, imgName); !strings.Contains(out, envValOveride) { + c.Fatalf("run produced invalid output: %q, expected %q", out, envValOveride) + } +} + +func (s *DockerSuite) TestBuildBuildTimeArgOverrideEnvDefinedBeforeArg(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows does not support ARG + imgName := "bldargtest" + envKey := "foo" + envVal := "bar" + envValOveride := "barOverride" + args := []string{ + "--build-arg", fmt.Sprintf("%s=%s", envKey, envVal), + } + dockerfile := fmt.Sprintf(`FROM busybox + ENV %s %s + ARG %s + RUN echo $%s + CMD echo $%s + `, envKey, envValOveride, envKey, envKey, envKey) + + if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err != nil || strings.Count(out, envValOveride) != 2 { + if err != nil { + c.Fatalf("build failed to complete: %q %q", out, err) + } + c.Fatalf("failed to access environment variable in output: %q expected: %q", out, envValOveride) + } + + containerName := "bldargCont" + if out, _ := dockerCmd(c, "run", "--name", containerName, imgName); !strings.Contains(out, envValOveride) { + c.Fatalf("run produced invalid output: %q, expected %q", out, envValOveride) + } +} + +func (s *DockerSuite) TestBuildBuildTimeArgExpansion(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows does not support ARG + imgName := "bldvarstest" + + wdVar := "WDIR" + wdVal := "/tmp/" + addVar := "AFILE" + addVal := "addFile" + copyVar := "CFILE" + copyVal := "copyFile" + envVar := "foo" + envVal := "bar" + exposeVar := "EPORT" + exposeVal := "9999" + userVar := "USER" + userVal := "testUser" + volVar := "VOL" + volVal := "/testVol/" + args := []string{ + "--build-arg", fmt.Sprintf("%s=%s", wdVar, wdVal), + "--build-arg", fmt.Sprintf("%s=%s", addVar, addVal), + "--build-arg", fmt.Sprintf("%s=%s", copyVar, copyVal), + "--build-arg", fmt.Sprintf("%s=%s", envVar, envVal), + "--build-arg", fmt.Sprintf("%s=%s", exposeVar, exposeVal), + "--build-arg", fmt.Sprintf("%s=%s", userVar, userVal), + "--build-arg", fmt.Sprintf("%s=%s", volVar, volVal), + } + ctx, err := fakeContext(fmt.Sprintf(`FROM busybox + ARG %s + WORKDIR ${%s} + ARG %s + ADD ${%s} testDir/ + ARG %s + COPY $%s testDir/ + ARG %s + ENV %s=${%s} + ARG %s + EXPOSE $%s + ARG %s + USER $%s + ARG %s + VOLUME ${%s}`, + wdVar, wdVar, addVar, addVar, copyVar, copyVar, envVar, envVar, + envVar, exposeVar, exposeVar, userVar, userVar, volVar, volVar), + map[string]string{ + addVal: "some stuff", + copyVal: "some stuff", + }) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(imgName, ctx, true, args...); err != nil { + c.Fatal(err) + } + + var resMap map[string]interface{} + var resArr []string + res := "" + res = inspectField(c, imgName, "Config.WorkingDir") + if res != filepath.ToSlash(filepath.Clean(wdVal)) { + c.Fatalf("Config.WorkingDir value mismatch. Expected: %s, got: %s", filepath.ToSlash(filepath.Clean(wdVal)), res) + } + + inspectFieldAndMarshall(c, imgName, "Config.Env", &resArr) + + found := false + for _, v := range resArr { + if fmt.Sprintf("%s=%s", envVar, envVal) == v { + found = true + break + } + } + if !found { + c.Fatalf("Config.Env value mismatch. Expected to exist: %s=%s, got: %v", + envVar, envVal, resArr) + } + + inspectFieldAndMarshall(c, imgName, "Config.ExposedPorts", &resMap) + if _, ok := resMap[fmt.Sprintf("%s/tcp", exposeVal)]; !ok { + c.Fatalf("Config.ExposedPorts value mismatch. Expected exposed port: %s/tcp, got: %v", exposeVal, resMap) + } + + res = inspectField(c, imgName, "Config.User") + if res != userVal { + c.Fatalf("Config.User value mismatch. Expected: %s, got: %s", userVal, res) + } + + inspectFieldAndMarshall(c, imgName, "Config.Volumes", &resMap) + if _, ok := resMap[volVal]; !ok { + c.Fatalf("Config.Volumes value mismatch. Expected volume: %s, got: %v", volVal, resMap) + } +} + +func (s *DockerSuite) TestBuildBuildTimeArgExpansionOverride(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows does not support ARG + imgName := "bldvarstest" + envKey := "foo" + envVal := "bar" + envKey1 := "foo1" + envValOveride := "barOverride" + args := []string{ + "--build-arg", fmt.Sprintf("%s=%s", envKey, envVal), + } + dockerfile := fmt.Sprintf(`FROM busybox + ARG %s + ENV %s %s + ENV %s ${%s} + RUN echo $%s + CMD echo $%s`, envKey, envKey, envValOveride, envKey1, envKey, envKey1, envKey1) + + if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err != nil || strings.Count(out, envValOveride) != 2 { + if err != nil { + c.Fatalf("build failed to complete: %q %q", out, err) + } + c.Fatalf("failed to access environment variable in output: %q expected: %q", out, envValOveride) + } + + containerName := "bldargCont" + if out, _ := dockerCmd(c, "run", "--name", containerName, imgName); !strings.Contains(out, envValOveride) { + c.Fatalf("run produced invalid output: %q, expected %q", out, envValOveride) + } +} + +func (s *DockerSuite) TestBuildBuildTimeArgUntrustedDefinedAfterUse(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows does not support ARG + imgName := "bldargtest" + envKey := "foo" + envVal := "bar" + args := []string{ + "--build-arg", fmt.Sprintf("%s=%s", envKey, envVal), + } + dockerfile := fmt.Sprintf(`FROM busybox + RUN echo $%s + ARG %s + CMD echo $%s`, envKey, envKey, envKey) + + if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err != nil || strings.Contains(out, envVal) { + if err != nil { + c.Fatalf("build failed to complete: %q %q", out, err) + } + c.Fatalf("able to access environment variable in output: %q expected to be missing", out) + } + + containerName := "bldargCont" + if out, _ := dockerCmd(c, "run", "--name", containerName, imgName); out != "\n" { + c.Fatalf("run produced invalid output: %q, expected empty string", out) + } +} + +func (s *DockerSuite) TestBuildBuildTimeArgBuiltinArg(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows does not support --build-arg + imgName := "bldargtest" + envKey := "HTTP_PROXY" + envVal := "bar" + args := []string{ + "--build-arg", fmt.Sprintf("%s=%s", envKey, envVal), + } + dockerfile := fmt.Sprintf(`FROM busybox + RUN echo $%s + CMD echo $%s`, envKey, envKey) + + if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err != nil || !strings.Contains(out, envVal) { + if err != nil { + c.Fatalf("build failed to complete: %q %q", out, err) + } + c.Fatalf("failed to access environment variable in output: %q expected: %q", out, envVal) + } + + containerName := "bldargCont" + if out, _ := dockerCmd(c, "run", "--name", containerName, imgName); out != "\n" { + c.Fatalf("run produced invalid output: %q, expected empty string", out) + } +} + +func (s *DockerSuite) TestBuildBuildTimeArgDefaultOverride(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows does not support ARG + imgName := "bldargtest" + envKey := "foo" + envVal := "bar" + envValOveride := "barOverride" + args := []string{ + "--build-arg", fmt.Sprintf("%s=%s", envKey, envValOveride), + } + dockerfile := fmt.Sprintf(`FROM busybox + ARG %s=%s + ENV %s $%s + RUN echo $%s + CMD echo $%s`, envKey, envVal, envKey, envKey, envKey, envKey) + + if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err != nil || strings.Count(out, envValOveride) != 1 { + if err != nil { + c.Fatalf("build failed to complete: %q %q", out, err) + } + c.Fatalf("failed to access environment variable in output: %q expected: %q", out, envValOveride) + } + + containerName := "bldargCont" + if out, _ := dockerCmd(c, "run", "--name", containerName, imgName); !strings.Contains(out, envValOveride) { + c.Fatalf("run produced invalid output: %q, expected %q", out, envValOveride) + } +} + +func (s *DockerSuite) TestBuildBuildTimeArgMultiArgsSameLine(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows does not support ARG + imgName := "bldargtest" + envKey := "foo" + envKey1 := "foo1" + args := []string{} + dockerfile := fmt.Sprintf(`FROM busybox + ARG %s %s`, envKey, envKey1) + + errStr := "ARG requires exactly one argument definition" + if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err == nil { + c.Fatalf("build succeeded, expected to fail. Output: %v", out) + } else if !strings.Contains(out, errStr) { + c.Fatalf("Unexpected error. output: %q, expected error: %q", out, errStr) + } +} + +func (s *DockerSuite) TestBuildBuildTimeArgUnconsumedArg(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows does not support --build-arg + imgName := "bldargtest" + envKey := "foo" + envVal := "bar" + args := []string{ + "--build-arg", fmt.Sprintf("%s=%s", envKey, envVal), + } + dockerfile := fmt.Sprintf(`FROM busybox + RUN echo $%s + CMD echo $%s`, envKey, envKey) + + errStr := "One or more build-args" + if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err == nil { + c.Fatalf("build succeeded, expected to fail. Output: %v", out) + } else if !strings.Contains(out, errStr) { + c.Fatalf("Unexpected error. output: %q, expected error: %q", out, errStr) + } + +} + +func (s *DockerSuite) TestBuildBuildTimeArgQuotedValVariants(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows does not support ARG + imgName := "bldargtest" + envKey := "foo" + envKey1 := "foo1" + envKey2 := "foo2" + envKey3 := "foo3" + args := []string{} + dockerfile := fmt.Sprintf(`FROM busybox + ARG %s="" + ARG %s='' + ARG %s="''" + ARG %s='""' + RUN [ "$%s" != "$%s" ] + RUN [ "$%s" != "$%s" ] + RUN [ "$%s" != "$%s" ] + RUN [ "$%s" != "$%s" ] + RUN [ "$%s" != "$%s" ]`, envKey, envKey1, envKey2, envKey3, + envKey, envKey2, envKey, envKey3, envKey1, envKey2, envKey1, envKey3, + envKey2, envKey3) + + if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err != nil { + c.Fatalf("build failed to complete: %q %q", out, err) + } +} + +func (s *DockerSuite) TestBuildBuildTimeArgEmptyValVariants(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows does not support ARG + imgName := "bldargtest" + envKey := "foo" + envKey1 := "foo1" + envKey2 := "foo2" + args := []string{} + dockerfile := fmt.Sprintf(`FROM busybox + ARG %s= + ARG %s="" + ARG %s='' + RUN [ "$%s" == "$%s" ] + RUN [ "$%s" == "$%s" ] + RUN [ "$%s" == "$%s" ]`, envKey, envKey1, envKey2, envKey, envKey1, envKey1, envKey2, envKey, envKey2) + + if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err != nil { + c.Fatalf("build failed to complete: %q %q", out, err) + } +} + +func (s *DockerSuite) TestBuildBuildTimeArgDefintionWithNoEnvInjection(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows does not support ARG + imgName := "bldargtest" + envKey := "foo" + args := []string{} + dockerfile := fmt.Sprintf(`FROM busybox + ARG %s + RUN env`, envKey) + + if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err != nil || strings.Count(out, envKey) != 1 { + if err != nil { + c.Fatalf("build failed to complete: %q %q", out, err) + } + c.Fatalf("unexpected number of occurrences of the arg in output: %q expected: 1", out) + } +} + +func (s *DockerSuite) TestBuildNoNamedVolume(c *check.C) { + volName := "testname:/foo" + + if daemonPlatform == "windows" { + volName = "testname:C:\\foo" + } + dockerCmd(c, "run", "-v", volName, "busybox", "sh", "-c", "touch /foo/oops") + + dockerFile := `FROM busybox + VOLUME ` + volName + ` + RUN ls /foo/oops + ` + _, err := buildImage("test", dockerFile, false) + c.Assert(err, check.NotNil, check.Commentf("image build should have failed")) +} + +func (s *DockerSuite) TestBuildTagEvent(c *check.C) { + since := daemonTime(c).Unix() + + dockerFile := `FROM busybox + RUN echo events + ` + _, err := buildImage("test", dockerFile, false) + c.Assert(err, check.IsNil) + + out, _ := dockerCmd(c, "events", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix()), "--filter", "type=image") + events := strings.Split(strings.TrimSpace(out), "\n") + actions := eventActionsByIDAndType(c, events, "test:latest", "image") + var foundTag bool + for _, a := range actions { + if a == "tag" { + foundTag = true + break + } + } + + c.Assert(foundTag, checker.True, check.Commentf("No tag event found:\n%s", out)) +} + +// #15780 +func (s *DockerSuite) TestBuildMultipleTags(c *check.C) { + dockerfile := ` + FROM busybox + MAINTAINER test-15780 + ` + cmd := exec.Command(dockerBinary, "build", "-t", "tag1", "-t", "tag2:v2", + "-t", "tag1:latest", "-t", "tag1", "--no-cache", "-") + cmd.Stdin = strings.NewReader(dockerfile) + _, err := runCommand(cmd) + c.Assert(err, check.IsNil) + + id1, err := getIDByName("tag1") + c.Assert(err, check.IsNil) + id2, err := getIDByName("tag2:v2") + c.Assert(err, check.IsNil) + c.Assert(id1, check.Equals, id2) +} + +// #17290 +func (s *DockerSuite) TestBuildCacheBrokenSymlink(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "testbuildbrokensymlink" + ctx, err := fakeContext(` + FROM busybox + COPY . ./`, + map[string]string{ + "foo": "bar", + }) + c.Assert(err, checker.IsNil) + defer ctx.Close() + + err = os.Symlink(filepath.Join(ctx.Dir, "nosuchfile"), filepath.Join(ctx.Dir, "asymlink")) + c.Assert(err, checker.IsNil) + + // warm up cache + _, err = buildImageFromContext(name, ctx, true) + c.Assert(err, checker.IsNil) + + // add new file to context, should invalidate cache + err = ioutil.WriteFile(filepath.Join(ctx.Dir, "newfile"), []byte("foo"), 0644) + c.Assert(err, checker.IsNil) + + _, out, err := buildImageFromContextWithOut(name, ctx, true) + c.Assert(err, checker.IsNil) + + c.Assert(out, checker.Not(checker.Contains), "Using cache") + +} + +func (s *DockerSuite) TestBuildFollowSymlinkToFile(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "testbuildbrokensymlink" + ctx, err := fakeContext(` + FROM busybox + COPY asymlink target`, + map[string]string{ + "foo": "bar", + }) + c.Assert(err, checker.IsNil) + defer ctx.Close() + + err = os.Symlink("foo", filepath.Join(ctx.Dir, "asymlink")) + c.Assert(err, checker.IsNil) + + id, err := buildImageFromContext(name, ctx, true) + c.Assert(err, checker.IsNil) + + out, _ := dockerCmd(c, "run", "--rm", id, "cat", "target") + c.Assert(out, checker.Matches, "bar") + + // change target file should invalidate cache + err = ioutil.WriteFile(filepath.Join(ctx.Dir, "foo"), []byte("baz"), 0644) + c.Assert(err, checker.IsNil) + + id, out, err = buildImageFromContextWithOut(name, ctx, true) + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Not(checker.Contains), "Using cache") + + out, _ = dockerCmd(c, "run", "--rm", id, "cat", "target") + c.Assert(out, checker.Matches, "baz") +} + +func (s *DockerSuite) TestBuildFollowSymlinkToDir(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "testbuildbrokensymlink" + ctx, err := fakeContext(` + FROM busybox + COPY asymlink /`, + map[string]string{ + "foo/abc": "bar", + "foo/def": "baz", + }) + c.Assert(err, checker.IsNil) + defer ctx.Close() + + err = os.Symlink("foo", filepath.Join(ctx.Dir, "asymlink")) + c.Assert(err, checker.IsNil) + + id, err := buildImageFromContext(name, ctx, true) + c.Assert(err, checker.IsNil) + + out, _ := dockerCmd(c, "run", "--rm", id, "cat", "abc", "def") + c.Assert(out, checker.Matches, "barbaz") + + // change target file should invalidate cache + err = ioutil.WriteFile(filepath.Join(ctx.Dir, "foo/def"), []byte("bax"), 0644) + c.Assert(err, checker.IsNil) + + id, out, err = buildImageFromContextWithOut(name, ctx, true) + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Not(checker.Contains), "Using cache") + + out, _ = dockerCmd(c, "run", "--rm", id, "cat", "abc", "def") + c.Assert(out, checker.Matches, "barbax") + +} + +// TestBuildSymlinkBasename tests that target file gets basename from symlink, +// not from the target file. +func (s *DockerSuite) TestBuildSymlinkBasename(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "testbuildbrokensymlink" + ctx, err := fakeContext(` + FROM busybox + COPY asymlink /`, + map[string]string{ + "foo": "bar", + }) + c.Assert(err, checker.IsNil) + defer ctx.Close() + + err = os.Symlink("foo", filepath.Join(ctx.Dir, "asymlink")) + c.Assert(err, checker.IsNil) + + id, err := buildImageFromContext(name, ctx, true) + c.Assert(err, checker.IsNil) + + out, _ := dockerCmd(c, "run", "--rm", id, "cat", "asymlink") + c.Assert(out, checker.Matches, "bar") + +} + +// #17827 +func (s *DockerSuite) TestBuildCacheRootSource(c *check.C) { + name := "testbuildrootsource" + ctx, err := fakeContext(` + FROM busybox + COPY / /data`, + map[string]string{ + "foo": "bar", + }) + c.Assert(err, checker.IsNil) + defer ctx.Close() + + // warm up cache + _, err = buildImageFromContext(name, ctx, true) + c.Assert(err, checker.IsNil) + + // change file, should invalidate cache + err = ioutil.WriteFile(filepath.Join(ctx.Dir, "foo"), []byte("baz"), 0644) + c.Assert(err, checker.IsNil) + + _, out, err := buildImageFromContextWithOut(name, ctx, true) + c.Assert(err, checker.IsNil) + + c.Assert(out, checker.Not(checker.Contains), "Using cache") +} + +// #19375 +func (s *DockerSuite) TestBuildFailsGitNotCallable(c *check.C) { + cmd := exec.Command(dockerBinary, "build", "github.com/docker/v1.10-migrator.git") + cmd.Env = append(cmd.Env, "PATH=") + out, _, err := runCommandWithOutput(cmd) + c.Assert(err, checker.NotNil) + c.Assert(out, checker.Contains, "unable to prepare context: unable to find 'git': ") + + cmd = exec.Command(dockerBinary, "build", "https://github.com/docker/v1.10-migrator.git") + cmd.Env = append(cmd.Env, "PATH=") + out, _, err = runCommandWithOutput(cmd) + c.Assert(err, checker.NotNil) + c.Assert(out, checker.Contains, "unable to prepare context: unable to find 'git': ") +} + +// TestBuildWorkdirWindowsPath tests that a Windows style path works as a workdir +func (s *DockerSuite) TestBuildWorkdirWindowsPath(c *check.C) { + testRequires(c, DaemonIsWindows) + name := "testbuildworkdirwindowspath" + + _, err := buildImage(name, ` + FROM windowsservercore + RUN mkdir C:\\work + WORKDIR C:\\work + RUN if "%CD%" NEQ "C:\work" exit -1 + `, true) + + if err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestBuildLabel(c *check.C) { + name := "testbuildlabel" + testLabel := "foo" + + _, err := buildImage(name, ` + FROM `+minimalBaseImage()+` + LABEL default foo +`, false, "--label", testLabel) + + c.Assert(err, checker.IsNil) + + res := inspectFieldJSON(c, name, "Config.Labels") + + var labels map[string]string + + if err := json.Unmarshal([]byte(res), &labels); err != nil { + c.Fatal(err) + } + + if _, ok := labels[testLabel]; !ok { + c.Fatal("label not found in image") + } +} + +func (s *DockerSuite) TestBuildLabelOneNode(c *check.C) { + name := "testbuildlabel" + + _, err := buildImage(name, "FROM busybox", false, "--label", "foo=bar") + + c.Assert(err, checker.IsNil) + + res, err := inspectImage(name, "json .Config.Labels") + c.Assert(err, checker.IsNil) + var labels map[string]string + + if err := json.Unmarshal([]byte(res), &labels); err != nil { + c.Fatal(err) + } + + v, ok := labels["foo"] + if !ok { + c.Fatal("label `foo` not found in image") + } + c.Assert(v, checker.Equals, "bar") +} + +func (s *DockerSuite) TestBuildLabelCacheCommit(c *check.C) { + name := "testbuildlabelcachecommit" + testLabel := "foo" + + if _, err := buildImage(name, ` + FROM `+minimalBaseImage()+` + LABEL default foo + `, false); err != nil { + c.Fatal(err) + } + + _, err := buildImage(name, ` + FROM `+minimalBaseImage()+` + LABEL default foo +`, true, "--label", testLabel) + + c.Assert(err, checker.IsNil) + + res := inspectFieldJSON(c, name, "Config.Labels") + + var labels map[string]string + + if err := json.Unmarshal([]byte(res), &labels); err != nil { + c.Fatal(err) + } + + if _, ok := labels[testLabel]; !ok { + c.Fatal("label not found in image") + } +} + +func (s *DockerSuite) TestBuildLabelMultiple(c *check.C) { + name := "testbuildlabelmultiple" + testLabels := map[string]string{ + "foo": "bar", + "123": "456", + } + + labelArgs := []string{} + + for k, v := range testLabels { + labelArgs = append(labelArgs, "--label", k+"="+v) + } + + _, err := buildImage(name, ` + FROM `+minimalBaseImage()+` + LABEL default foo +`, false, labelArgs...) + + if err != nil { + c.Fatal("error building image with labels", err) + } + + res := inspectFieldJSON(c, name, "Config.Labels") + + var labels map[string]string + + if err := json.Unmarshal([]byte(res), &labels); err != nil { + c.Fatal(err) + } + + for k, v := range testLabels { + if x, ok := labels[k]; !ok || x != v { + c.Fatalf("label %s=%s not found in image", k, v) + } + } +} + +func (s *DockerSuite) TestBuildLabelOverwrite(c *check.C) { + name := "testbuildlabeloverwrite" + testLabel := "foo" + testValue := "bar" + + _, err := buildImage(name, ` + FROM `+minimalBaseImage()+` + LABEL `+testLabel+`+ foo +`, false, []string{"--label", testLabel + "=" + testValue}...) + + if err != nil { + c.Fatal("error building image with labels", err) + } + + res := inspectFieldJSON(c, name, "Config.Labels") + + var labels map[string]string + + if err := json.Unmarshal([]byte(res), &labels); err != nil { + c.Fatal(err) + } + + v, ok := labels[testLabel] + if !ok { + c.Fatal("label not found in image") + } + + if v != testValue { + c.Fatal("label not overwritten") + } +} + +func (s *DockerRegistryAuthHtpasswdSuite) TestBuildFromAuthenticatedRegistry(c *check.C) { + dockerCmd(c, "login", "-u", s.reg.username, "-p", s.reg.password, privateRegistryURL) + + baseImage := privateRegistryURL + "/baseimage" + + _, err := buildImage(baseImage, ` + FROM busybox + ENV env1 val1 + `, true) + + c.Assert(err, checker.IsNil) + + dockerCmd(c, "push", baseImage) + dockerCmd(c, "rmi", baseImage) + + _, err = buildImage(baseImage, fmt.Sprintf(` + FROM %s + ENV env2 val2 + `, baseImage), true) + + c.Assert(err, checker.IsNil) +} + +func (s *DockerRegistryAuthHtpasswdSuite) TestBuildWithExternalAuth(c *check.C) { + osPath := os.Getenv("PATH") + defer os.Setenv("PATH", osPath) + + workingDir, err := os.Getwd() + c.Assert(err, checker.IsNil) + absolute, err := filepath.Abs(filepath.Join(workingDir, "fixtures", "auth")) + c.Assert(err, checker.IsNil) + testPath := fmt.Sprintf("%s%c%s", osPath, filepath.ListSeparator, absolute) + + os.Setenv("PATH", testPath) + + repoName := fmt.Sprintf("%v/dockercli/busybox:authtest", privateRegistryURL) + + tmp, err := ioutil.TempDir("", "integration-cli-") + c.Assert(err, checker.IsNil) + + externalAuthConfig := `{ "credsStore": "shell-test" }` + + configPath := filepath.Join(tmp, "config.json") + err = ioutil.WriteFile(configPath, []byte(externalAuthConfig), 0644) + c.Assert(err, checker.IsNil) + + dockerCmd(c, "--config", tmp, "login", "-u", s.reg.username, "-p", s.reg.password, privateRegistryURL) + + b, err := ioutil.ReadFile(configPath) + c.Assert(err, checker.IsNil) + c.Assert(string(b), checker.Not(checker.Contains), "\"auth\":") + + dockerCmd(c, "--config", tmp, "tag", "busybox", repoName) + dockerCmd(c, "--config", tmp, "push", repoName) + + // make sure the image is pulled when building + dockerCmd(c, "rmi", repoName) + + buildCmd := exec.Command(dockerBinary, "--config", tmp, "build", "-") + buildCmd.Stdin = strings.NewReader(fmt.Sprintf("FROM %s", repoName)) + + out, _, err := runCommandWithOutput(buildCmd) + c.Assert(err, check.IsNil, check.Commentf(out)) +} + +// Test cases in #22036 +func (s *DockerSuite) TestBuildLabelsOverride(c *check.C) { + testRequires(c, DaemonIsLinux) + + // Command line option labels will always override + name := "scratchy" + expected := `{"bar":"from-flag","foo":"from-flag"}` + _, err := buildImage(name, + `FROM scratch + LABEL foo=from-dockerfile`, + true, "--label", "foo=from-flag", "--label", "bar=from-flag") + c.Assert(err, check.IsNil) + + res := inspectFieldJSON(c, name, "Config.Labels") + if res != expected { + c.Fatalf("Labels %s, expected %s", res, expected) + } + + name = "from" + expected = `{"foo":"from-dockerfile"}` + _, err = buildImage(name, + `FROM scratch + LABEL foo from-dockerfile`, + true) + c.Assert(err, check.IsNil) + + res = inspectFieldJSON(c, name, "Config.Labels") + if res != expected { + c.Fatalf("Labels %s, expected %s", res, expected) + } + + // Command line option label will override even via `FROM` + name = "new" + expected = `{"bar":"from-dockerfile2","foo":"new"}` + _, err = buildImage(name, + `FROM from + LABEL bar from-dockerfile2`, + true, "--label", "foo=new") + c.Assert(err, check.IsNil) + + res = inspectFieldJSON(c, name, "Config.Labels") + if res != expected { + c.Fatalf("Labels %s, expected %s", res, expected) + } + + // Command line option without a value set (--label foo, --label bar=) + // will be treated as --label foo="", --label bar="" + name = "scratchy2" + expected = `{"bar":"","foo":""}` + _, err = buildImage(name, + `FROM scratch + LABEL foo=from-dockerfile`, + true, "--label", "foo", "--label", "bar=") + c.Assert(err, check.IsNil) + + res = inspectFieldJSON(c, name, "Config.Labels") + if res != expected { + c.Fatalf("Labels %s, expected %s", res, expected) + } + + // Command line option without a value set (--label foo, --label bar=) + // will be treated as --label foo="", --label bar="" + // This time is for inherited images + name = "new2" + expected = `{"bar":"","foo":""}` + _, err = buildImage(name, + `FROM from + LABEL bar from-dockerfile2`, + true, "--label", "foo=", "--label", "bar") + c.Assert(err, check.IsNil) + + res = inspectFieldJSON(c, name, "Config.Labels") + if res != expected { + c.Fatalf("Labels %s, expected %s", res, expected) + } + + // Command line option labels with only `FROM` + name = "scratchy" + expected = `{"bar":"from-flag","foo":"from-flag"}` + _, err = buildImage(name, + `FROM scratch`, + true, "--label", "foo=from-flag", "--label", "bar=from-flag") + c.Assert(err, check.IsNil) + + res = inspectFieldJSON(c, name, "Config.Labels") + if res != expected { + c.Fatalf("Labels %s, expected %s", res, expected) + } + +} diff --git a/integration-cli/docker_cli_build_unix_test.go b/integration-cli/docker_cli_build_unix_test.go new file mode 100644 index 00000000..56ab66ef --- /dev/null +++ b/integration-cli/docker_cli_build_unix_test.go @@ -0,0 +1,206 @@ +// +build !windows + +package main + +import ( + "bufio" + "bytes" + "encoding/json" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/go-units" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestBuildResourceConstraintsAreUsed(c *check.C) { + testRequires(c, cpuCfsQuota) + name := "testbuildresourceconstraints" + + ctx, err := fakeContext(` + FROM hello-world:frozen + RUN ["/hello"] + `, map[string]string{}) + c.Assert(err, checker.IsNil) + + _, _, err = dockerCmdInDir(c, ctx.Dir, "build", "--no-cache", "--rm=false", "--memory=64m", "--memory-swap=-1", "--cpuset-cpus=0", "--cpuset-mems=0", "--cpu-shares=100", "--cpu-quota=8000", "--ulimit", "nofile=42", "-t", name, ".") + if err != nil { + c.Fatal(err) + } + + out, _ := dockerCmd(c, "ps", "-lq") + cID := strings.TrimSpace(out) + + type hostConfig struct { + Memory int64 + MemorySwap int64 + CpusetCpus string + CpusetMems string + CPUShares int64 + CPUQuota int64 + Ulimits []*units.Ulimit + } + + cfg := inspectFieldJSON(c, cID, "HostConfig") + + var c1 hostConfig + err = json.Unmarshal([]byte(cfg), &c1) + c.Assert(err, checker.IsNil, check.Commentf(cfg)) + + c.Assert(c1.Memory, checker.Equals, int64(64*1024*1024), check.Commentf("resource constraints not set properly for Memory")) + c.Assert(c1.MemorySwap, checker.Equals, int64(-1), check.Commentf("resource constraints not set properly for MemorySwap")) + c.Assert(c1.CpusetCpus, checker.Equals, "0", check.Commentf("resource constraints not set properly for CpusetCpus")) + c.Assert(c1.CpusetMems, checker.Equals, "0", check.Commentf("resource constraints not set properly for CpusetMems")) + c.Assert(c1.CPUShares, checker.Equals, int64(100), check.Commentf("resource constraints not set properly for CPUShares")) + c.Assert(c1.CPUQuota, checker.Equals, int64(8000), check.Commentf("resource constraints not set properly for CPUQuota")) + c.Assert(c1.Ulimits[0].Name, checker.Equals, "nofile", check.Commentf("resource constraints not set properly for Ulimits")) + c.Assert(c1.Ulimits[0].Hard, checker.Equals, int64(42), check.Commentf("resource constraints not set properly for Ulimits")) + + // Make sure constraints aren't saved to image + dockerCmd(c, "run", "--name=test", name) + + cfg = inspectFieldJSON(c, "test", "HostConfig") + + var c2 hostConfig + err = json.Unmarshal([]byte(cfg), &c2) + c.Assert(err, checker.IsNil, check.Commentf(cfg)) + + c.Assert(c2.Memory, check.Not(checker.Equals), int64(64*1024*1024), check.Commentf("resource leaked from build for Memory")) + c.Assert(c2.MemorySwap, check.Not(checker.Equals), int64(-1), check.Commentf("resource leaked from build for MemorySwap")) + c.Assert(c2.CpusetCpus, check.Not(checker.Equals), "0", check.Commentf("resource leaked from build for CpusetCpus")) + c.Assert(c2.CpusetMems, check.Not(checker.Equals), "0", check.Commentf("resource leaked from build for CpusetMems")) + c.Assert(c2.CPUShares, check.Not(checker.Equals), int64(100), check.Commentf("resource leaked from build for CPUShares")) + c.Assert(c2.CPUQuota, check.Not(checker.Equals), int64(8000), check.Commentf("resource leaked from build for CPUQuota")) + c.Assert(c2.Ulimits, checker.IsNil, check.Commentf("resource leaked from build for Ulimits")) +} + +func (s *DockerSuite) TestBuildAddChangeOwnership(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "testbuildaddown" + + ctx := func() *FakeContext { + dockerfile := ` + FROM busybox + ADD foo /bar/ + RUN [ $(stat -c %U:%G "/bar") = 'root:root' ] + RUN [ $(stat -c %U:%G "/bar/foo") = 'root:root' ] + ` + tmpDir, err := ioutil.TempDir("", "fake-context") + c.Assert(err, check.IsNil) + testFile, err := os.Create(filepath.Join(tmpDir, "foo")) + if err != nil { + c.Fatalf("failed to create foo file: %v", err) + } + defer testFile.Close() + + chownCmd := exec.Command("chown", "daemon:daemon", "foo") + chownCmd.Dir = tmpDir + out, _, err := runCommandWithOutput(chownCmd) + if err != nil { + c.Fatal(err, out) + } + + if err := ioutil.WriteFile(filepath.Join(tmpDir, "Dockerfile"), []byte(dockerfile), 0644); err != nil { + c.Fatalf("failed to open destination dockerfile: %v", err) + } + return fakeContextFromDir(tmpDir) + }() + + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + c.Fatalf("build failed to complete for TestBuildAddChangeOwnership: %v", err) + } +} + +// Test that an infinite sleep during a build is killed if the client disconnects. +// This test is fairly hairy because there are lots of ways to race. +// Strategy: +// * Monitor the output of docker events starting from before +// * Run a 1-year-long sleep from a docker build. +// * When docker events sees container start, close the "docker build" command +// * Wait for docker events to emit a dying event. +func (s *DockerSuite) TestBuildCancellationKillsSleep(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "testbuildcancellation" + + observer, err := newEventObserver(c) + c.Assert(err, checker.IsNil) + err = observer.Start() + c.Assert(err, checker.IsNil) + defer observer.Stop() + + // (Note: one year, will never finish) + ctx, err := fakeContext("FROM busybox\nRUN sleep 31536000", nil) + if err != nil { + c.Fatal(err) + } + defer ctx.Close() + + buildCmd := exec.Command(dockerBinary, "build", "-t", name, ".") + buildCmd.Dir = ctx.Dir + + stdoutBuild, err := buildCmd.StdoutPipe() + if err := buildCmd.Start(); err != nil { + c.Fatalf("failed to run build: %s", err) + } + + matchCID := regexp.MustCompile("Running in (.+)") + scanner := bufio.NewScanner(stdoutBuild) + + outputBuffer := new(bytes.Buffer) + var buildID string + for scanner.Scan() { + line := scanner.Text() + outputBuffer.WriteString(line) + outputBuffer.WriteString("\n") + if matches := matchCID.FindStringSubmatch(line); len(matches) > 0 { + buildID = matches[1] + break + } + } + + if buildID == "" { + c.Fatalf("Unable to find build container id in build output:\n%s", outputBuffer.String()) + } + + testActions := map[string]chan bool{ + "start": make(chan bool, 1), + "die": make(chan bool, 1), + } + + matcher := matchEventLine(buildID, "container", testActions) + processor := processEventMatch(testActions) + go observer.Match(matcher, processor) + + select { + case <-time.After(10 * time.Second): + observer.CheckEventError(c, buildID, "start", matcher) + case <-testActions["start"]: + // ignore, done + } + + // Send a kill to the `docker build` command. + // Causes the underlying build to be cancelled due to socket close. + if err := buildCmd.Process.Kill(); err != nil { + c.Fatalf("error killing build command: %s", err) + } + + // Get the exit status of `docker build`, check it exited because killed. + if err := buildCmd.Wait(); err != nil && !isKilled(err) { + c.Fatalf("wait failed during build run: %T %s", err, err) + } + + select { + case <-time.After(10 * time.Second): + observer.CheckEventError(c, buildID, "die", matcher) + case <-testActions["die"]: + // ignore, done + } +} diff --git a/integration-cli/docker_cli_by_digest_test.go b/integration-cli/docker_cli_by_digest_test.go new file mode 100644 index 00000000..f3948d67 --- /dev/null +++ b/integration-cli/docker_cli_by_digest_test.go @@ -0,0 +1,585 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/docker/distribution/digest" + "github.com/docker/distribution/manifest/schema1" + "github.com/docker/distribution/manifest/schema2" + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/stringutils" + "github.com/docker/engine-api/types" + "github.com/go-check/check" +) + +var ( + remoteRepoName = "dockercli/busybox-by-dgst" + repoName = fmt.Sprintf("%s/%s", privateRegistryURL, remoteRepoName) + pushDigestRegex = regexp.MustCompile("[\\S]+: digest: ([\\S]+) size: [0-9]+") + digestRegex = regexp.MustCompile("Digest: ([\\S]+)") +) + +func setupImage(c *check.C) (digest.Digest, error) { + return setupImageWithTag(c, "latest") +} + +func setupImageWithTag(c *check.C, tag string) (digest.Digest, error) { + containerName := "busyboxbydigest" + + dockerCmd(c, "run", "-e", "digest=1", "--name", containerName, "busybox") + + // tag the image to upload it to the private registry + repoAndTag := repoName + ":" + tag + out, _, err := dockerCmdWithError("commit", containerName, repoAndTag) + c.Assert(err, checker.IsNil, check.Commentf("image tagging failed: %s", out)) + + // delete the container as we don't need it any more + err = deleteContainer(containerName) + c.Assert(err, checker.IsNil) + + // push the image + out, _, err = dockerCmdWithError("push", repoAndTag) + c.Assert(err, checker.IsNil, check.Commentf("pushing the image to the private registry has failed: %s", out)) + + // delete our local repo that we previously tagged + rmiout, _, err := dockerCmdWithError("rmi", repoAndTag) + c.Assert(err, checker.IsNil, check.Commentf("error deleting images prior to real test: %s", rmiout)) + + matches := pushDigestRegex.FindStringSubmatch(out) + c.Assert(matches, checker.HasLen, 2, check.Commentf("unable to parse digest from push output: %s", out)) + pushDigest := matches[1] + + return digest.Digest(pushDigest), nil +} + +func testPullByTagDisplaysDigest(c *check.C) { + testRequires(c, DaemonIsLinux) + pushDigest, err := setupImage(c) + c.Assert(err, checker.IsNil, check.Commentf("error setting up image")) + + // pull from the registry using the tag + out, _ := dockerCmd(c, "pull", repoName) + + // the pull output includes "Digest: ", so find that + matches := digestRegex.FindStringSubmatch(out) + c.Assert(matches, checker.HasLen, 2, check.Commentf("unable to parse digest from pull output: %s", out)) + pullDigest := matches[1] + + // make sure the pushed and pull digests match + c.Assert(pushDigest.String(), checker.Equals, pullDigest) +} + +func (s *DockerRegistrySuite) TestPullByTagDisplaysDigest(c *check.C) { + testPullByTagDisplaysDigest(c) +} + +func (s *DockerSchema1RegistrySuite) TestPullByTagDisplaysDigest(c *check.C) { + testPullByTagDisplaysDigest(c) +} + +func testPullByDigest(c *check.C) { + testRequires(c, DaemonIsLinux) + pushDigest, err := setupImage(c) + c.Assert(err, checker.IsNil, check.Commentf("error setting up image")) + + // pull from the registry using the @ reference + imageReference := fmt.Sprintf("%s@%s", repoName, pushDigest) + out, _ := dockerCmd(c, "pull", imageReference) + + // the pull output includes "Digest: ", so find that + matches := digestRegex.FindStringSubmatch(out) + c.Assert(matches, checker.HasLen, 2, check.Commentf("unable to parse digest from pull output: %s", out)) + pullDigest := matches[1] + + // make sure the pushed and pull digests match + c.Assert(pushDigest.String(), checker.Equals, pullDigest) +} + +func (s *DockerRegistrySuite) TestPullByDigest(c *check.C) { + testPullByDigest(c) +} + +func (s *DockerSchema1RegistrySuite) TestPullByDigest(c *check.C) { + testPullByDigest(c) +} + +func testPullByDigestNoFallback(c *check.C) { + testRequires(c, DaemonIsLinux) + // pull from the registry using the @ reference + imageReference := fmt.Sprintf("%s@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", repoName) + out, _, err := dockerCmdWithError("pull", imageReference) + c.Assert(err, checker.NotNil, check.Commentf("expected non-zero exit status and correct error message when pulling non-existing image")) + c.Assert(out, checker.Contains, "manifest unknown", check.Commentf("expected non-zero exit status and correct error message when pulling non-existing image")) +} + +func (s *DockerRegistrySuite) TestPullByDigestNoFallback(c *check.C) { + testPullByDigestNoFallback(c) +} + +func (s *DockerSchema1RegistrySuite) TestPullByDigestNoFallback(c *check.C) { + testPullByDigestNoFallback(c) +} + +func (s *DockerRegistrySuite) TestCreateByDigest(c *check.C) { + pushDigest, err := setupImage(c) + c.Assert(err, checker.IsNil, check.Commentf("error setting up image")) + + imageReference := fmt.Sprintf("%s@%s", repoName, pushDigest) + + containerName := "createByDigest" + dockerCmd(c, "create", "--name", containerName, imageReference) + + res := inspectField(c, containerName, "Config.Image") + c.Assert(res, checker.Equals, imageReference) +} + +func (s *DockerRegistrySuite) TestRunByDigest(c *check.C) { + pushDigest, err := setupImage(c) + c.Assert(err, checker.IsNil) + + imageReference := fmt.Sprintf("%s@%s", repoName, pushDigest) + + containerName := "runByDigest" + out, _ := dockerCmd(c, "run", "--name", containerName, imageReference, "sh", "-c", "echo found=$digest") + + foundRegex := regexp.MustCompile("found=([^\n]+)") + matches := foundRegex.FindStringSubmatch(out) + c.Assert(matches, checker.HasLen, 2, check.Commentf("unable to parse digest from pull output: %s", out)) + c.Assert(matches[1], checker.Equals, "1", check.Commentf("Expected %q, got %q", "1", matches[1])) + + res := inspectField(c, containerName, "Config.Image") + c.Assert(res, checker.Equals, imageReference) +} + +func (s *DockerRegistrySuite) TestRemoveImageByDigest(c *check.C) { + digest, err := setupImage(c) + c.Assert(err, checker.IsNil, check.Commentf("error setting up image")) + + imageReference := fmt.Sprintf("%s@%s", repoName, digest) + + // pull from the registry using the @ reference + dockerCmd(c, "pull", imageReference) + + // make sure inspect runs ok + inspectField(c, imageReference, "Id") + + // do the delete + err = deleteImages(imageReference) + c.Assert(err, checker.IsNil, check.Commentf("unexpected error deleting image")) + + // try to inspect again - it should error this time + _, err = inspectFieldWithError(imageReference, "Id") + //unexpected nil err trying to inspect what should be a non-existent image + c.Assert(err, checker.NotNil) + c.Assert(err.Error(), checker.Contains, "No such image") +} + +func (s *DockerRegistrySuite) TestBuildByDigest(c *check.C) { + digest, err := setupImage(c) + c.Assert(err, checker.IsNil, check.Commentf("error setting up image")) + + imageReference := fmt.Sprintf("%s@%s", repoName, digest) + + // pull from the registry using the @ reference + dockerCmd(c, "pull", imageReference) + + // get the image id + imageID := inspectField(c, imageReference, "Id") + + // do the build + name := "buildbydigest" + _, err = buildImage(name, fmt.Sprintf( + `FROM %s + CMD ["/bin/echo", "Hello World"]`, imageReference), + true) + c.Assert(err, checker.IsNil) + + // get the build's image id + res := inspectField(c, name, "Config.Image") + // make sure they match + c.Assert(res, checker.Equals, imageID) +} + +func (s *DockerRegistrySuite) TestTagByDigest(c *check.C) { + digest, err := setupImage(c) + c.Assert(err, checker.IsNil, check.Commentf("error setting up image")) + + imageReference := fmt.Sprintf("%s@%s", repoName, digest) + + // pull from the registry using the @ reference + dockerCmd(c, "pull", imageReference) + + // tag it + tag := "tagbydigest" + dockerCmd(c, "tag", imageReference, tag) + + expectedID := inspectField(c, imageReference, "Id") + + tagID := inspectField(c, tag, "Id") + c.Assert(tagID, checker.Equals, expectedID) +} + +func (s *DockerRegistrySuite) TestListImagesWithoutDigests(c *check.C) { + digest, err := setupImage(c) + c.Assert(err, checker.IsNil, check.Commentf("error setting up image")) + + imageReference := fmt.Sprintf("%s@%s", repoName, digest) + + // pull from the registry using the @ reference + dockerCmd(c, "pull", imageReference) + + out, _ := dockerCmd(c, "images") + c.Assert(out, checker.Not(checker.Contains), "DIGEST", check.Commentf("list output should not have contained DIGEST header")) +} + +func (s *DockerRegistrySuite) TestListImagesWithDigests(c *check.C) { + + // setup image1 + digest1, err := setupImageWithTag(c, "tag1") + c.Assert(err, checker.IsNil, check.Commentf("error setting up image")) + imageReference1 := fmt.Sprintf("%s@%s", repoName, digest1) + c.Logf("imageReference1 = %s", imageReference1) + + // pull image1 by digest + dockerCmd(c, "pull", imageReference1) + + // list images + out, _ := dockerCmd(c, "images", "--digests") + + // make sure repo shown, tag=, digest = $digest1 + re1 := regexp.MustCompile(`\s*` + repoName + `\s*\s*` + digest1.String() + `\s`) + c.Assert(re1.MatchString(out), checker.True, check.Commentf("expected %q: %s", re1.String(), out)) + // setup image2 + digest2, err := setupImageWithTag(c, "tag2") + //error setting up image + c.Assert(err, checker.IsNil) + imageReference2 := fmt.Sprintf("%s@%s", repoName, digest2) + c.Logf("imageReference2 = %s", imageReference2) + + // pull image1 by digest + dockerCmd(c, "pull", imageReference1) + + // pull image2 by digest + dockerCmd(c, "pull", imageReference2) + + // list images + out, _ = dockerCmd(c, "images", "--digests") + + // make sure repo shown, tag=, digest = $digest1 + c.Assert(re1.MatchString(out), checker.True, check.Commentf("expected %q: %s", re1.String(), out)) + + // make sure repo shown, tag=, digest = $digest2 + re2 := regexp.MustCompile(`\s*` + repoName + `\s*\s*` + digest2.String() + `\s`) + c.Assert(re2.MatchString(out), checker.True, check.Commentf("expected %q: %s", re2.String(), out)) + + // pull tag1 + dockerCmd(c, "pull", repoName+":tag1") + + // list images + out, _ = dockerCmd(c, "images", "--digests") + + // make sure image 1 has repo, tag, AND repo, , digest + reWithTag1 := regexp.MustCompile(`\s*` + repoName + `\s*tag1\s*\s`) + reWithDigest1 := regexp.MustCompile(`\s*` + repoName + `\s*\s*` + digest1.String() + `\s`) + c.Assert(reWithDigest1.MatchString(out), checker.True, check.Commentf("expected %q: %s", reWithDigest1.String(), out)) + c.Assert(reWithTag1.MatchString(out), checker.True, check.Commentf("expected %q: %s", reWithTag1.String(), out)) + // make sure image 2 has repo, , digest + c.Assert(re2.MatchString(out), checker.True, check.Commentf("expected %q: %s", re2.String(), out)) + + // pull tag 2 + dockerCmd(c, "pull", repoName+":tag2") + + // list images + out, _ = dockerCmd(c, "images", "--digests") + + // make sure image 1 has repo, tag, digest + c.Assert(reWithTag1.MatchString(out), checker.True, check.Commentf("expected %q: %s", reWithTag1.String(), out)) + + // make sure image 2 has repo, tag, digest + reWithTag2 := regexp.MustCompile(`\s*` + repoName + `\s*tag2\s*\s`) + reWithDigest2 := regexp.MustCompile(`\s*` + repoName + `\s*\s*` + digest2.String() + `\s`) + c.Assert(reWithTag2.MatchString(out), checker.True, check.Commentf("expected %q: %s", reWithTag2.String(), out)) + c.Assert(reWithDigest2.MatchString(out), checker.True, check.Commentf("expected %q: %s", reWithDigest2.String(), out)) + + // list images + out, _ = dockerCmd(c, "images", "--digests") + + // make sure image 1 has repo, tag, digest + c.Assert(reWithTag1.MatchString(out), checker.True, check.Commentf("expected %q: %s", reWithTag1.String(), out)) + // make sure image 2 has repo, tag, digest + c.Assert(reWithTag2.MatchString(out), checker.True, check.Commentf("expected %q: %s", reWithTag2.String(), out)) + // make sure busybox has tag, but not digest + busyboxRe := regexp.MustCompile(`\s*busybox\s*latest\s*\s`) + c.Assert(busyboxRe.MatchString(out), checker.True, check.Commentf("expected %q: %s", busyboxRe.String(), out)) +} + +func (s *DockerRegistrySuite) TestInspectImageWithDigests(c *check.C) { + digest, err := setupImage(c) + c.Assert(err, check.IsNil, check.Commentf("error setting up image")) + + imageReference := fmt.Sprintf("%s@%s", repoName, digest) + + // pull from the registry using the @ reference + dockerCmd(c, "pull", imageReference) + + out, _ := dockerCmd(c, "inspect", imageReference) + + var imageJSON []types.ImageInspect + err = json.Unmarshal([]byte(out), &imageJSON) + c.Assert(err, checker.IsNil) + c.Assert(imageJSON, checker.HasLen, 1) + c.Assert(imageJSON[0].RepoDigests, checker.HasLen, 1) + c.Assert(stringutils.InSlice(imageJSON[0].RepoDigests, imageReference), checker.Equals, true) +} + +func (s *DockerRegistrySuite) TestPsListContainersFilterAncestorImageByDigest(c *check.C) { + digest, err := setupImage(c) + c.Assert(err, checker.IsNil, check.Commentf("error setting up image")) + + imageReference := fmt.Sprintf("%s@%s", repoName, digest) + + // pull from the registry using the @ reference + dockerCmd(c, "pull", imageReference) + + // build a image from it + imageName1 := "images_ps_filter_test" + _, err = buildImage(imageName1, fmt.Sprintf( + `FROM %s + LABEL match me 1`, imageReference), true) + c.Assert(err, checker.IsNil) + + // run a container based on that + dockerCmd(c, "run", "--name=test1", imageReference, "echo", "hello") + expectedID, err := getIDByName("test1") + c.Assert(err, check.IsNil) + + // run a container based on the a descendant of that too + dockerCmd(c, "run", "--name=test2", imageName1, "echo", "hello") + expectedID1, err := getIDByName("test2") + c.Assert(err, check.IsNil) + + expectedIDs := []string{expectedID, expectedID1} + + // Invalid imageReference + out, _ := dockerCmd(c, "ps", "-a", "-q", "--no-trunc", fmt.Sprintf("--filter=ancestor=busybox@%s", digest)) + // Filter container for ancestor filter should be empty + c.Assert(strings.TrimSpace(out), checker.Equals, "") + + // Valid imageReference + out, _ = dockerCmd(c, "ps", "-a", "-q", "--no-trunc", "--filter=ancestor="+imageReference) + checkPsAncestorFilterOutput(c, out, imageReference, expectedIDs) +} + +func (s *DockerRegistrySuite) TestDeleteImageByIDOnlyPulledByDigest(c *check.C) { + pushDigest, err := setupImage(c) + c.Assert(err, checker.IsNil, check.Commentf("error setting up image")) + + // pull from the registry using the @ reference + imageReference := fmt.Sprintf("%s@%s", repoName, pushDigest) + dockerCmd(c, "pull", imageReference) + // just in case... + + imageID := inspectField(c, imageReference, "Id") + + dockerCmd(c, "rmi", imageID) +} + +func (s *DockerRegistrySuite) TestDeleteImageWithDigestAndTag(c *check.C) { + pushDigest, err := setupImage(c) + c.Assert(err, checker.IsNil, check.Commentf("error setting up image")) + + // pull from the registry using the @ reference + imageReference := fmt.Sprintf("%s@%s", repoName, pushDigest) + dockerCmd(c, "pull", imageReference) + + imageID := inspectField(c, imageReference, "Id") + + repoTag := repoName + ":sometag" + repoTag2 := repoName + ":othertag" + dockerCmd(c, "tag", imageReference, repoTag) + dockerCmd(c, "tag", imageReference, repoTag2) + + dockerCmd(c, "rmi", repoTag2) + + // rmi should have deleted only repoTag2, because there's another tag + inspectField(c, repoTag, "Id") + + dockerCmd(c, "rmi", repoTag) + + // rmi should have deleted the tag, the digest reference, and the image itself + _, err = inspectFieldWithError(imageID, "Id") + c.Assert(err, checker.NotNil, check.Commentf("image should have been deleted")) +} + +// TestPullFailsWithAlteredManifest tests that a `docker pull` fails when +// we have modified a manifest blob and its digest cannot be verified. +// This is the schema2 version of the test. +func (s *DockerRegistrySuite) TestPullFailsWithAlteredManifest(c *check.C) { + testRequires(c, DaemonIsLinux) + manifestDigest, err := setupImage(c) + c.Assert(err, checker.IsNil, check.Commentf("error setting up image")) + + // Load the target manifest blob. + manifestBlob := s.reg.readBlobContents(c, manifestDigest) + + var imgManifest schema2.Manifest + err = json.Unmarshal(manifestBlob, &imgManifest) + c.Assert(err, checker.IsNil, check.Commentf("unable to decode image manifest from blob")) + + // Change a layer in the manifest. + imgManifest.Layers[0].Digest = digest.Digest("sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + + // Move the existing data file aside, so that we can replace it with a + // malicious blob of data. NOTE: we defer the returned undo func. + undo := s.reg.tempMoveBlobData(c, manifestDigest) + defer undo() + + alteredManifestBlob, err := json.MarshalIndent(imgManifest, "", " ") + c.Assert(err, checker.IsNil, check.Commentf("unable to encode altered image manifest to JSON")) + + s.reg.writeBlobContents(c, manifestDigest, alteredManifestBlob) + + // Now try pulling that image by digest. We should get an error about + // digest verification for the manifest digest. + + // Pull from the registry using the @ reference. + imageReference := fmt.Sprintf("%s@%s", repoName, manifestDigest) + out, exitStatus, _ := dockerCmdWithError("pull", imageReference) + c.Assert(exitStatus, checker.Not(check.Equals), 0) + + expectedErrorMsg := fmt.Sprintf("manifest verification failed for digest %s", manifestDigest) + c.Assert(out, checker.Contains, expectedErrorMsg) +} + +// TestPullFailsWithAlteredManifest tests that a `docker pull` fails when +// we have modified a manifest blob and its digest cannot be verified. +// This is the schema1 version of the test. +func (s *DockerSchema1RegistrySuite) TestPullFailsWithAlteredManifest(c *check.C) { + testRequires(c, DaemonIsLinux) + manifestDigest, err := setupImage(c) + c.Assert(err, checker.IsNil, check.Commentf("error setting up image")) + + // Load the target manifest blob. + manifestBlob := s.reg.readBlobContents(c, manifestDigest) + + var imgManifest schema1.Manifest + err = json.Unmarshal(manifestBlob, &imgManifest) + c.Assert(err, checker.IsNil, check.Commentf("unable to decode image manifest from blob")) + + // Change a layer in the manifest. + imgManifest.FSLayers[0] = schema1.FSLayer{ + BlobSum: digest.Digest("sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"), + } + + // Move the existing data file aside, so that we can replace it with a + // malicious blob of data. NOTE: we defer the returned undo func. + undo := s.reg.tempMoveBlobData(c, manifestDigest) + defer undo() + + alteredManifestBlob, err := json.MarshalIndent(imgManifest, "", " ") + c.Assert(err, checker.IsNil, check.Commentf("unable to encode altered image manifest to JSON")) + + s.reg.writeBlobContents(c, manifestDigest, alteredManifestBlob) + + // Now try pulling that image by digest. We should get an error about + // digest verification for the manifest digest. + + // Pull from the registry using the @ reference. + imageReference := fmt.Sprintf("%s@%s", repoName, manifestDigest) + out, exitStatus, _ := dockerCmdWithError("pull", imageReference) + c.Assert(exitStatus, checker.Not(check.Equals), 0) + + expectedErrorMsg := fmt.Sprintf("image verification failed for digest %s", manifestDigest) + c.Assert(out, checker.Contains, expectedErrorMsg) +} + +// TestPullFailsWithAlteredLayer tests that a `docker pull` fails when +// we have modified a layer blob and its digest cannot be verified. +// This is the schema2 version of the test. +func (s *DockerRegistrySuite) TestPullFailsWithAlteredLayer(c *check.C) { + testRequires(c, DaemonIsLinux) + manifestDigest, err := setupImage(c) + c.Assert(err, checker.IsNil) + + // Load the target manifest blob. + manifestBlob := s.reg.readBlobContents(c, manifestDigest) + + var imgManifest schema2.Manifest + err = json.Unmarshal(manifestBlob, &imgManifest) + c.Assert(err, checker.IsNil) + + // Next, get the digest of one of the layers from the manifest. + targetLayerDigest := imgManifest.Layers[0].Digest + + // Move the existing data file aside, so that we can replace it with a + // malicious blob of data. NOTE: we defer the returned undo func. + undo := s.reg.tempMoveBlobData(c, targetLayerDigest) + defer undo() + + // Now make a fake data blob in this directory. + s.reg.writeBlobContents(c, targetLayerDigest, []byte("This is not the data you are looking for.")) + + // Now try pulling that image by digest. We should get an error about + // digest verification for the target layer digest. + + // Remove distribution cache to force a re-pull of the blobs + if err := os.RemoveAll(filepath.Join(dockerBasePath, "image", s.d.storageDriver, "distribution")); err != nil { + c.Fatalf("error clearing distribution cache: %v", err) + } + + // Pull from the registry using the @ reference. + imageReference := fmt.Sprintf("%s@%s", repoName, manifestDigest) + out, exitStatus, _ := dockerCmdWithError("pull", imageReference) + c.Assert(exitStatus, checker.Not(check.Equals), 0, check.Commentf("expected a zero exit status")) + + expectedErrorMsg := fmt.Sprintf("filesystem layer verification failed for digest %s", targetLayerDigest) + c.Assert(out, checker.Contains, expectedErrorMsg, check.Commentf("expected error message in output: %s", out)) +} + +// TestPullFailsWithAlteredLayer tests that a `docker pull` fails when +// we have modified a layer blob and its digest cannot be verified. +// This is the schema1 version of the test. +func (s *DockerSchema1RegistrySuite) TestPullFailsWithAlteredLayer(c *check.C) { + testRequires(c, DaemonIsLinux) + manifestDigest, err := setupImage(c) + c.Assert(err, checker.IsNil) + + // Load the target manifest blob. + manifestBlob := s.reg.readBlobContents(c, manifestDigest) + + var imgManifest schema1.Manifest + err = json.Unmarshal(manifestBlob, &imgManifest) + c.Assert(err, checker.IsNil) + + // Next, get the digest of one of the layers from the manifest. + targetLayerDigest := imgManifest.FSLayers[0].BlobSum + + // Move the existing data file aside, so that we can replace it with a + // malicious blob of data. NOTE: we defer the returned undo func. + undo := s.reg.tempMoveBlobData(c, targetLayerDigest) + defer undo() + + // Now make a fake data blob in this directory. + s.reg.writeBlobContents(c, targetLayerDigest, []byte("This is not the data you are looking for.")) + + // Now try pulling that image by digest. We should get an error about + // digest verification for the target layer digest. + + // Remove distribution cache to force a re-pull of the blobs + if err := os.RemoveAll(filepath.Join(dockerBasePath, "image", s.d.storageDriver, "distribution")); err != nil { + c.Fatalf("error clearing distribution cache: %v", err) + } + + // Pull from the registry using the @ reference. + imageReference := fmt.Sprintf("%s@%s", repoName, manifestDigest) + out, exitStatus, _ := dockerCmdWithError("pull", imageReference) + c.Assert(exitStatus, checker.Not(check.Equals), 0, check.Commentf("expected a zero exit status")) + + expectedErrorMsg := fmt.Sprintf("filesystem layer verification failed for digest %s", targetLayerDigest) + c.Assert(out, checker.Contains, expectedErrorMsg, check.Commentf("expected error message in output: %s", out)) +} diff --git a/integration-cli/docker_cli_commit_test.go b/integration-cli/docker_cli_commit_test.go new file mode 100644 index 00000000..086a2031 --- /dev/null +++ b/integration-cli/docker_cli_commit_test.go @@ -0,0 +1,189 @@ +package main + +import ( + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestCommitAfterContainerIsDone(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-i", "-a", "stdin", "busybox", "echo", "foo") + + cleanedContainerID := strings.TrimSpace(out) + + dockerCmd(c, "wait", cleanedContainerID) + + out, _ = dockerCmd(c, "commit", cleanedContainerID) + + cleanedImageID := strings.TrimSpace(out) + + dockerCmd(c, "inspect", cleanedImageID) +} + +func (s *DockerSuite) TestCommitWithoutPause(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-i", "-a", "stdin", "busybox", "echo", "foo") + + cleanedContainerID := strings.TrimSpace(out) + + dockerCmd(c, "wait", cleanedContainerID) + + out, _ = dockerCmd(c, "commit", "-p=false", cleanedContainerID) + + cleanedImageID := strings.TrimSpace(out) + + dockerCmd(c, "inspect", cleanedImageID) +} + +//test commit a paused container should not unpause it after commit +func (s *DockerSuite) TestCommitPausedContainer(c *check.C) { + testRequires(c, DaemonIsLinux) + defer unpauseAllContainers() + out, _ := dockerCmd(c, "run", "-i", "-d", "busybox") + + cleanedContainerID := strings.TrimSpace(out) + + dockerCmd(c, "pause", cleanedContainerID) + + out, _ = dockerCmd(c, "commit", cleanedContainerID) + + out = inspectField(c, cleanedContainerID, "State.Paused") + // commit should not unpause a paused container + c.Assert(out, checker.Contains, "true") +} + +func (s *DockerSuite) TestCommitNewFile(c *check.C) { + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "--name", "commiter", "busybox", "/bin/sh", "-c", "echo koye > /foo") + + imageID, _ := dockerCmd(c, "commit", "commiter") + imageID = strings.TrimSpace(imageID) + + out, _ := dockerCmd(c, "run", imageID, "cat", "/foo") + actual := strings.TrimSpace(out) + c.Assert(actual, checker.Equals, "koye") +} + +func (s *DockerSuite) TestCommitHardlink(c *check.C) { + testRequires(c, DaemonIsLinux) + firstOutput, _ := dockerCmd(c, "run", "-t", "--name", "hardlinks", "busybox", "sh", "-c", "touch file1 && ln file1 file2 && ls -di file1 file2") + + chunks := strings.Split(strings.TrimSpace(firstOutput), " ") + inode := chunks[0] + chunks = strings.SplitAfterN(strings.TrimSpace(firstOutput), " ", 2) + c.Assert(chunks[1], checker.Contains, chunks[0], check.Commentf("Failed to create hardlink in a container. Expected to find %q in %q", inode, chunks[1:])) + + imageID, _ := dockerCmd(c, "commit", "hardlinks", "hardlinks") + imageID = strings.TrimSpace(imageID) + + secondOutput, _ := dockerCmd(c, "run", "-t", "hardlinks", "ls", "-di", "file1", "file2") + + chunks = strings.Split(strings.TrimSpace(secondOutput), " ") + inode = chunks[0] + chunks = strings.SplitAfterN(strings.TrimSpace(secondOutput), " ", 2) + c.Assert(chunks[1], checker.Contains, chunks[0], check.Commentf("Failed to create hardlink in a container. Expected to find %q in %q", inode, chunks[1:])) +} + +func (s *DockerSuite) TestCommitTTY(c *check.C) { + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "-t", "--name", "tty", "busybox", "/bin/ls") + + imageID, _ := dockerCmd(c, "commit", "tty", "ttytest") + imageID = strings.TrimSpace(imageID) + + dockerCmd(c, "run", "ttytest", "/bin/ls") +} + +func (s *DockerSuite) TestCommitWithHostBindMount(c *check.C) { + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "--name", "bind-commit", "-v", "/dev/null:/winning", "busybox", "true") + + imageID, _ := dockerCmd(c, "commit", "bind-commit", "bindtest") + imageID = strings.TrimSpace(imageID) + + dockerCmd(c, "run", "bindtest", "true") +} + +func (s *DockerSuite) TestCommitChange(c *check.C) { + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "--name", "test", "busybox", "true") + + imageID, _ := dockerCmd(c, "commit", + "--change", "EXPOSE 8080", + "--change", "ENV DEBUG true", + "--change", "ENV test 1", + "--change", "ENV PATH /foo", + "--change", "LABEL foo bar", + "--change", "CMD [\"/bin/sh\"]", + "--change", "WORKDIR /opt", + "--change", "ENTRYPOINT [\"/bin/sh\"]", + "--change", "USER testuser", + "--change", "VOLUME /var/lib/docker", + "--change", "ONBUILD /usr/local/bin/python-build --dir /app/src", + "test", "test-commit") + imageID = strings.TrimSpace(imageID) + + expected := map[string]string{ + "Config.ExposedPorts": "map[8080/tcp:{}]", + "Config.Env": "[DEBUG=true test=1 PATH=/foo]", + "Config.Labels": "map[foo:bar]", + "Config.Cmd": "[/bin/sh]", + "Config.WorkingDir": "/opt", + "Config.Entrypoint": "[/bin/sh]", + "Config.User": "testuser", + "Config.Volumes": "map[/var/lib/docker:{}]", + "Config.OnBuild": "[/usr/local/bin/python-build --dir /app/src]", + } + + for conf, value := range expected { + res := inspectField(c, imageID, conf) + if res != value { + c.Errorf("%s('%s'), expected %s", conf, res, value) + } + } +} + +// TODO: commit --run is deprecated, remove this once --run is removed +func (s *DockerSuite) TestCommitMergeConfigRun(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "commit-test" + out, _ := dockerCmd(c, "run", "-d", "-e=FOO=bar", "busybox", "/bin/sh", "-c", "echo testing > /tmp/foo") + id := strings.TrimSpace(out) + + dockerCmd(c, "commit", `--run={"Cmd": ["cat", "/tmp/foo"]}`, id, "commit-test") + + out, _ = dockerCmd(c, "run", "--name", name, "commit-test") + //run config in committed container was not merged + c.Assert(strings.TrimSpace(out), checker.Equals, "testing") + + type cfg struct { + Env []string + Cmd []string + } + config1 := cfg{} + inspectFieldAndMarshall(c, id, "Config", &config1) + + config2 := cfg{} + inspectFieldAndMarshall(c, name, "Config", &config2) + + // Env has at least PATH loaded as well here, so let's just grab the FOO one + var env1, env2 string + for _, e := range config1.Env { + if strings.HasPrefix(e, "FOO") { + env1 = e + break + } + } + for _, e := range config2.Env { + if strings.HasPrefix(e, "FOO") { + env2 = e + break + } + } + + if len(config1.Env) != len(config2.Env) || env1 != env2 && env2 != "" { + c.Fatalf("expected envs to match: %v - %v", config1.Env, config2.Env) + } +} diff --git a/integration-cli/docker_cli_config_test.go b/integration-cli/docker_cli_config_test.go new file mode 100644 index 00000000..60152310 --- /dev/null +++ b/integration-cli/docker_cli_config_test.go @@ -0,0 +1,138 @@ +package main + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "runtime" + + "github.com/docker/docker/dockerversion" + "github.com/docker/docker/pkg/homedir" + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestConfigHttpHeader(c *check.C) { + testRequires(c, UnixCli) // Can't set/unset HOME on windows right now + // We either need a level of Go that supports Unsetenv (for cases + // when HOME/USERPROFILE isn't set), or we need to be able to use + // os/user but user.Current() only works if we aren't statically compiling + + var headers map[string][]string + + server := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + headers = r.Header + })) + defer server.Close() + + homeKey := homedir.Key() + homeVal := homedir.Get() + tmpDir, err := ioutil.TempDir("", "fake-home") + c.Assert(err, checker.IsNil) + defer os.RemoveAll(tmpDir) + + dotDocker := filepath.Join(tmpDir, ".docker") + os.Mkdir(dotDocker, 0600) + tmpCfg := filepath.Join(dotDocker, "config.json") + + defer func() { os.Setenv(homeKey, homeVal) }() + os.Setenv(homeKey, tmpDir) + + data := `{ + "HttpHeaders": { "MyHeader": "MyValue" } + }` + + err = ioutil.WriteFile(tmpCfg, []byte(data), 0600) + c.Assert(err, checker.IsNil) + + cmd := exec.Command(dockerBinary, "-H="+server.URL[7:], "ps") + out, _, _ := runCommandWithOutput(cmd) + + c.Assert(headers["User-Agent"], checker.NotNil, check.Commentf("Missing User-Agent")) + + c.Assert(headers["User-Agent"][0], checker.Equals, "Docker-Client/"+dockerversion.Version+" ("+runtime.GOOS+")", check.Commentf("Badly formatted User-Agent,out:%v", out)) + + c.Assert(headers["Myheader"], checker.NotNil) + c.Assert(headers["Myheader"][0], checker.Equals, "MyValue", check.Commentf("Missing/bad header,out:%v", out)) + +} + +func (s *DockerSuite) TestConfigDir(c *check.C) { + cDir, err := ioutil.TempDir("", "fake-home") + c.Assert(err, checker.IsNil) + defer os.RemoveAll(cDir) + + // First make sure pointing to empty dir doesn't generate an error + dockerCmd(c, "--config", cDir, "ps") + + // Test with env var too + cmd := exec.Command(dockerBinary, "ps") + cmd.Env = appendBaseEnv(true, "DOCKER_CONFIG="+cDir) + out, _, err := runCommandWithOutput(cmd) + + c.Assert(err, checker.IsNil, check.Commentf("ps2 didn't work,out:%v", out)) + + // Start a server so we can check to see if the config file was + // loaded properly + var headers map[string][]string + + server := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + headers = r.Header + })) + defer server.Close() + + // Create a dummy config file in our new config dir + data := `{ + "HttpHeaders": { "MyHeader": "MyValue" } + }` + + tmpCfg := filepath.Join(cDir, "config.json") + err = ioutil.WriteFile(tmpCfg, []byte(data), 0600) + c.Assert(err, checker.IsNil, check.Commentf("Err creating file")) + + env := appendBaseEnv(false) + + cmd = exec.Command(dockerBinary, "--config", cDir, "-H="+server.URL[7:], "ps") + cmd.Env = env + out, _, err = runCommandWithOutput(cmd) + + c.Assert(err, checker.NotNil, check.Commentf("out:%v", out)) + c.Assert(headers["Myheader"], checker.NotNil) + c.Assert(headers["Myheader"][0], checker.Equals, "MyValue", check.Commentf("ps3 - Missing header,out:%v", out)) + + // Reset headers and try again using env var this time + headers = map[string][]string{} + cmd = exec.Command(dockerBinary, "-H="+server.URL[7:], "ps") + cmd.Env = append(env, "DOCKER_CONFIG="+cDir) + out, _, err = runCommandWithOutput(cmd) + + c.Assert(err, checker.NotNil, check.Commentf("%v", out)) + c.Assert(headers["Myheader"], checker.NotNil) + c.Assert(headers["Myheader"][0], checker.Equals, "MyValue", check.Commentf("ps4 - Missing header,out:%v", out)) + + // Reset headers and make sure flag overrides the env var + headers = map[string][]string{} + cmd = exec.Command(dockerBinary, "--config", cDir, "-H="+server.URL[7:], "ps") + cmd.Env = append(env, "DOCKER_CONFIG=MissingDir") + out, _, err = runCommandWithOutput(cmd) + + c.Assert(err, checker.NotNil, check.Commentf("out:%v", out)) + c.Assert(headers["Myheader"], checker.NotNil) + c.Assert(headers["Myheader"][0], checker.Equals, "MyValue", check.Commentf("ps5 - Missing header,out:%v", out)) + + // Reset headers and make sure flag overrides the env var. + // Almost same as previous but make sure the "MissingDir" isn't + // ignore - we don't want to default back to the env var. + headers = map[string][]string{} + cmd = exec.Command(dockerBinary, "--config", "MissingDir", "-H="+server.URL[7:], "ps") + cmd.Env = append(env, "DOCKER_CONFIG="+cDir) + out, _, err = runCommandWithOutput(cmd) + + c.Assert(err, checker.NotNil, check.Commentf("out:%v", out)) + c.Assert(headers["Myheader"], checker.IsNil, check.Commentf("ps6 - Headers shouldn't be the expected value,out:%v", out)) +} diff --git a/integration-cli/docker_cli_cp_from_container_test.go b/integration-cli/docker_cli_cp_from_container_test.go new file mode 100644 index 00000000..677085a1 --- /dev/null +++ b/integration-cli/docker_cli_cp_from_container_test.go @@ -0,0 +1,489 @@ +package main + +import ( + "os" + "path/filepath" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +// docker cp CONTAINER:PATH LOCALPATH + +// Try all of the test cases from the archive package which implements the +// internals of `docker cp` and ensure that the behavior matches when actually +// copying to and from containers. + +// Basic assumptions about SRC and DST: +// 1. SRC must exist. +// 2. If SRC ends with a trailing separator, it must be a directory. +// 3. DST parent directory must exist. +// 4. If DST exists as a file, it must not end with a trailing separator. + +// First get these easy error cases out of the way. + +// Test for error when SRC does not exist. +func (s *DockerSuite) TestCpFromErrSrcNotExists(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{}) + + tmpDir := getTestDir(c, "test-cp-from-err-src-not-exists") + defer os.RemoveAll(tmpDir) + + err := runDockerCp(c, containerCpPath(containerID, "file1"), tmpDir) + c.Assert(err, checker.NotNil) + + c.Assert(isCpNotExist(err), checker.True, check.Commentf("expected IsNotExist error, but got %T: %s", err, err)) +} + +// Test for error when SRC ends in a trailing +// path separator but it exists as a file. +func (s *DockerSuite) TestCpFromErrSrcNotDir(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{addContent: true}) + + tmpDir := getTestDir(c, "test-cp-from-err-src-not-dir") + defer os.RemoveAll(tmpDir) + + err := runDockerCp(c, containerCpPathTrailingSep(containerID, "file1"), tmpDir) + c.Assert(err, checker.NotNil) + + c.Assert(isCpNotDir(err), checker.True, check.Commentf("expected IsNotDir error, but got %T: %s", err, err)) +} + +// Test for error when SRC is a valid file or directory, +// bu the DST parent directory does not exist. +func (s *DockerSuite) TestCpFromErrDstParentNotExists(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{addContent: true}) + + tmpDir := getTestDir(c, "test-cp-from-err-dst-parent-not-exists") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + // Try with a file source. + srcPath := containerCpPath(containerID, "/file1") + dstPath := cpPath(tmpDir, "notExists", "file1") + + err := runDockerCp(c, srcPath, dstPath) + c.Assert(err, checker.NotNil) + + c.Assert(isCpNotExist(err), checker.True, check.Commentf("expected IsNotExist error, but got %T: %s", err, err)) + + // Try with a directory source. + srcPath = containerCpPath(containerID, "/dir1") + + err = runDockerCp(c, srcPath, dstPath) + c.Assert(err, checker.NotNil) + + c.Assert(isCpNotExist(err), checker.True, check.Commentf("expected IsNotExist error, but got %T: %s", err, err)) +} + +// Test for error when DST ends in a trailing +// path separator but exists as a file. +func (s *DockerSuite) TestCpFromErrDstNotDir(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{addContent: true}) + + tmpDir := getTestDir(c, "test-cp-from-err-dst-not-dir") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + // Try with a file source. + srcPath := containerCpPath(containerID, "/file1") + dstPath := cpPathTrailingSep(tmpDir, "file1") + + err := runDockerCp(c, srcPath, dstPath) + c.Assert(err, checker.NotNil) + + c.Assert(isCpNotDir(err), checker.True, check.Commentf("expected IsNotDir error, but got %T: %s", err, err)) + + // Try with a directory source. + srcPath = containerCpPath(containerID, "/dir1") + + err = runDockerCp(c, srcPath, dstPath) + c.Assert(err, checker.NotNil) + + c.Assert(isCpNotDir(err), checker.True, check.Commentf("expected IsNotDir error, but got %T: %s", err, err)) +} + +// Check that copying from a container to a local symlink copies to the symlink +// target and does not overwrite the local symlink itself. +func (s *DockerSuite) TestCpFromSymlinkDestination(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{addContent: true}) + + tmpDir := getTestDir(c, "test-cp-from-err-dst-not-dir") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + // First, copy a file from the container to a symlink to a file. This + // should overwrite the symlink target contents with the source contents. + srcPath := containerCpPath(containerID, "/file2") + dstPath := cpPath(tmpDir, "symlinkToFile1") + + c.Assert(runDockerCp(c, srcPath, dstPath), checker.IsNil) + + // The symlink should not have been modified. + c.Assert(symlinkTargetEquals(c, dstPath, "file1"), checker.IsNil) + + // The file should have the contents of "file2" now. + c.Assert(fileContentEquals(c, cpPath(tmpDir, "file1"), "file2\n"), checker.IsNil) + + // Next, copy a file from the container to a symlink to a directory. This + // should copy the file into the symlink target directory. + dstPath = cpPath(tmpDir, "symlinkToDir1") + + c.Assert(runDockerCp(c, srcPath, dstPath), checker.IsNil) + + // The symlink should not have been modified. + c.Assert(symlinkTargetEquals(c, dstPath, "dir1"), checker.IsNil) + + // The file should have the contents of "file2" now. + c.Assert(fileContentEquals(c, cpPath(tmpDir, "file2"), "file2\n"), checker.IsNil) + + // Next, copy a file from the container to a symlink to a file that does + // not exist (a broken symlink). This should create the target file with + // the contents of the source file. + dstPath = cpPath(tmpDir, "brokenSymlinkToFileX") + + c.Assert(runDockerCp(c, srcPath, dstPath), checker.IsNil) + + // The symlink should not have been modified. + c.Assert(symlinkTargetEquals(c, dstPath, "fileX"), checker.IsNil) + + // The file should have the contents of "file2" now. + c.Assert(fileContentEquals(c, cpPath(tmpDir, "fileX"), "file2\n"), checker.IsNil) + + // Next, copy a directory from the container to a symlink to a local + // directory. This should copy the directory into the symlink target + // directory and not modify the symlink. + srcPath = containerCpPath(containerID, "/dir2") + dstPath = cpPath(tmpDir, "symlinkToDir1") + + c.Assert(runDockerCp(c, srcPath, dstPath), checker.IsNil) + + // The symlink should not have been modified. + c.Assert(symlinkTargetEquals(c, dstPath, "dir1"), checker.IsNil) + + // The directory should now contain a copy of "dir2". + c.Assert(fileContentEquals(c, cpPath(tmpDir, "dir1/dir2/file2-1"), "file2-1\n"), checker.IsNil) + + // Next, copy a directory from the container to a symlink to a local + // directory that does not exist (a broken symlink). This should create + // the target as a directory with the contents of the source directory. It + // should not modify the symlink. + dstPath = cpPath(tmpDir, "brokenSymlinkToDirX") + + c.Assert(runDockerCp(c, srcPath, dstPath), checker.IsNil) + + // The symlink should not have been modified. + c.Assert(symlinkTargetEquals(c, dstPath, "dirX"), checker.IsNil) + + // The "dirX" directory should now be a copy of "dir2". + c.Assert(fileContentEquals(c, cpPath(tmpDir, "dirX/file2-1"), "file2-1\n"), checker.IsNil) +} + +// Possibilities are reduced to the remaining 10 cases: +// +// case | srcIsDir | onlyDirContents | dstExists | dstIsDir | dstTrSep | action +// =================================================================================================== +// A | no | - | no | - | no | create file +// B | no | - | no | - | yes | error +// C | no | - | yes | no | - | overwrite file +// D | no | - | yes | yes | - | create file in dst dir +// E | yes | no | no | - | - | create dir, copy contents +// F | yes | no | yes | no | - | error +// G | yes | no | yes | yes | - | copy dir and contents +// H | yes | yes | no | - | - | create dir, copy contents +// I | yes | yes | yes | no | - | error +// J | yes | yes | yes | yes | - | copy dir contents +// + +// A. SRC specifies a file and DST (no trailing path separator) doesn't +// exist. This should create a file with the name DST and copy the +// contents of the source file into it. +func (s *DockerSuite) TestCpFromCaseA(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{ + addContent: true, workDir: "/root", + }) + + tmpDir := getTestDir(c, "test-cp-from-case-a") + defer os.RemoveAll(tmpDir) + + srcPath := containerCpPath(containerID, "/root/file1") + dstPath := cpPath(tmpDir, "itWorks.txt") + + c.Assert(runDockerCp(c, srcPath, dstPath), checker.IsNil) + + c.Assert(fileContentEquals(c, dstPath, "file1\n"), checker.IsNil) +} + +// B. SRC specifies a file and DST (with trailing path separator) doesn't +// exist. This should cause an error because the copy operation cannot +// create a directory when copying a single file. +func (s *DockerSuite) TestCpFromCaseB(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{addContent: true}) + + tmpDir := getTestDir(c, "test-cp-from-case-b") + defer os.RemoveAll(tmpDir) + + srcPath := containerCpPath(containerID, "/file1") + dstDir := cpPathTrailingSep(tmpDir, "testDir") + + err := runDockerCp(c, srcPath, dstDir) + c.Assert(err, checker.NotNil) + + c.Assert(isCpDirNotExist(err), checker.True, check.Commentf("expected DirNotExists error, but got %T: %s", err, err)) +} + +// C. SRC specifies a file and DST exists as a file. This should overwrite +// the file at DST with the contents of the source file. +func (s *DockerSuite) TestCpFromCaseC(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{ + addContent: true, workDir: "/root", + }) + + tmpDir := getTestDir(c, "test-cp-from-case-c") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + srcPath := containerCpPath(containerID, "/root/file1") + dstPath := cpPath(tmpDir, "file2") + + // Ensure the local file starts with different content. + c.Assert(fileContentEquals(c, dstPath, "file2\n"), checker.IsNil) + + c.Assert(runDockerCp(c, srcPath, dstPath), checker.IsNil) + + c.Assert(fileContentEquals(c, dstPath, "file1\n"), checker.IsNil) +} + +// D. SRC specifies a file and DST exists as a directory. This should place +// a copy of the source file inside it using the basename from SRC. Ensure +// this works whether DST has a trailing path separator or not. +func (s *DockerSuite) TestCpFromCaseD(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{addContent: true}) + + tmpDir := getTestDir(c, "test-cp-from-case-d") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + srcPath := containerCpPath(containerID, "/file1") + dstDir := cpPath(tmpDir, "dir1") + dstPath := filepath.Join(dstDir, "file1") + + // Ensure that dstPath doesn't exist. + _, err := os.Stat(dstPath) + c.Assert(os.IsNotExist(err), checker.True, check.Commentf("did not expect dstPath %q to exist", dstPath)) + + c.Assert(runDockerCp(c, srcPath, dstDir), checker.IsNil) + + c.Assert(fileContentEquals(c, dstPath, "file1\n"), checker.IsNil) + + // Now try again but using a trailing path separator for dstDir. + + // unable to remove dstDir + c.Assert(os.RemoveAll(dstDir), checker.IsNil) + + // unable to make dstDir + c.Assert(os.MkdirAll(dstDir, os.FileMode(0755)), checker.IsNil) + + dstDir = cpPathTrailingSep(tmpDir, "dir1") + + c.Assert(runDockerCp(c, srcPath, dstDir), checker.IsNil) + + c.Assert(fileContentEquals(c, dstPath, "file1\n"), checker.IsNil) +} + +// E. SRC specifies a directory and DST does not exist. This should create a +// directory at DST and copy the contents of the SRC directory into the DST +// directory. Ensure this works whether DST has a trailing path separator or +// not. +func (s *DockerSuite) TestCpFromCaseE(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{addContent: true}) + + tmpDir := getTestDir(c, "test-cp-from-case-e") + defer os.RemoveAll(tmpDir) + + srcDir := containerCpPath(containerID, "dir1") + dstDir := cpPath(tmpDir, "testDir") + dstPath := filepath.Join(dstDir, "file1-1") + + c.Assert(runDockerCp(c, srcDir, dstDir), checker.IsNil) + + c.Assert(fileContentEquals(c, dstPath, "file1-1\n"), checker.IsNil) + + // Now try again but using a trailing path separator for dstDir. + + // unable to remove dstDir + c.Assert(os.RemoveAll(dstDir), checker.IsNil) + + dstDir = cpPathTrailingSep(tmpDir, "testDir") + + c.Assert(runDockerCp(c, srcDir, dstDir), checker.IsNil) + + c.Assert(fileContentEquals(c, dstPath, "file1-1\n"), checker.IsNil) +} + +// F. SRC specifies a directory and DST exists as a file. This should cause an +// error as it is not possible to overwrite a file with a directory. +func (s *DockerSuite) TestCpFromCaseF(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{ + addContent: true, workDir: "/root", + }) + + tmpDir := getTestDir(c, "test-cp-from-case-f") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + srcDir := containerCpPath(containerID, "/root/dir1") + dstFile := cpPath(tmpDir, "file1") + + err := runDockerCp(c, srcDir, dstFile) + c.Assert(err, checker.NotNil) + + c.Assert(isCpCannotCopyDir(err), checker.True, check.Commentf("expected ErrCannotCopyDir error, but got %T: %s", err, err)) +} + +// G. SRC specifies a directory and DST exists as a directory. This should copy +// the SRC directory and all its contents to the DST directory. Ensure this +// works whether DST has a trailing path separator or not. +func (s *DockerSuite) TestCpFromCaseG(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{ + addContent: true, workDir: "/root", + }) + + tmpDir := getTestDir(c, "test-cp-from-case-g") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + srcDir := containerCpPath(containerID, "/root/dir1") + dstDir := cpPath(tmpDir, "dir2") + resultDir := filepath.Join(dstDir, "dir1") + dstPath := filepath.Join(resultDir, "file1-1") + + c.Assert(runDockerCp(c, srcDir, dstDir), checker.IsNil) + + c.Assert(fileContentEquals(c, dstPath, "file1-1\n"), checker.IsNil) + + // Now try again but using a trailing path separator for dstDir. + + // unable to remove dstDir + c.Assert(os.RemoveAll(dstDir), checker.IsNil) + + // unable to make dstDir + c.Assert(os.MkdirAll(dstDir, os.FileMode(0755)), checker.IsNil) + + dstDir = cpPathTrailingSep(tmpDir, "dir2") + + c.Assert(runDockerCp(c, srcDir, dstDir), checker.IsNil) + + c.Assert(fileContentEquals(c, dstPath, "file1-1\n"), checker.IsNil) +} + +// H. SRC specifies a directory's contents only and DST does not exist. This +// should create a directory at DST and copy the contents of the SRC +// directory (but not the directory itself) into the DST directory. Ensure +// this works whether DST has a trailing path separator or not. +func (s *DockerSuite) TestCpFromCaseH(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{addContent: true}) + + tmpDir := getTestDir(c, "test-cp-from-case-h") + defer os.RemoveAll(tmpDir) + + srcDir := containerCpPathTrailingSep(containerID, "dir1") + "." + dstDir := cpPath(tmpDir, "testDir") + dstPath := filepath.Join(dstDir, "file1-1") + + c.Assert(runDockerCp(c, srcDir, dstDir), checker.IsNil) + + c.Assert(fileContentEquals(c, dstPath, "file1-1\n"), checker.IsNil) + + // Now try again but using a trailing path separator for dstDir. + + // unable to remove resultDir + c.Assert(os.RemoveAll(dstDir), checker.IsNil) + + dstDir = cpPathTrailingSep(tmpDir, "testDir") + + c.Assert(runDockerCp(c, srcDir, dstDir), checker.IsNil) + + c.Assert(fileContentEquals(c, dstPath, "file1-1\n"), checker.IsNil) +} + +// I. SRC specifies a directory's contents only and DST exists as a file. This +// should cause an error as it is not possible to overwrite a file with a +// directory. +func (s *DockerSuite) TestCpFromCaseI(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{ + addContent: true, workDir: "/root", + }) + + tmpDir := getTestDir(c, "test-cp-from-case-i") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + srcDir := containerCpPathTrailingSep(containerID, "/root/dir1") + "." + dstFile := cpPath(tmpDir, "file1") + + err := runDockerCp(c, srcDir, dstFile) + c.Assert(err, checker.NotNil) + + c.Assert(isCpCannotCopyDir(err), checker.True, check.Commentf("expected ErrCannotCopyDir error, but got %T: %s", err, err)) +} + +// J. SRC specifies a directory's contents only and DST exists as a directory. +// This should copy the contents of the SRC directory (but not the directory +// itself) into the DST directory. Ensure this works whether DST has a +// trailing path separator or not. +func (s *DockerSuite) TestCpFromCaseJ(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{ + addContent: true, workDir: "/root", + }) + + tmpDir := getTestDir(c, "test-cp-from-case-j") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + srcDir := containerCpPathTrailingSep(containerID, "/root/dir1") + "." + dstDir := cpPath(tmpDir, "dir2") + dstPath := filepath.Join(dstDir, "file1-1") + + c.Assert(runDockerCp(c, srcDir, dstDir), checker.IsNil) + + c.Assert(fileContentEquals(c, dstPath, "file1-1\n"), checker.IsNil) + + // Now try again but using a trailing path separator for dstDir. + + // unable to remove dstDir + c.Assert(os.RemoveAll(dstDir), checker.IsNil) + + // unable to make dstDir + c.Assert(os.MkdirAll(dstDir, os.FileMode(0755)), checker.IsNil) + + dstDir = cpPathTrailingSep(tmpDir, "dir2") + + c.Assert(runDockerCp(c, srcDir, dstDir), checker.IsNil) + + c.Assert(fileContentEquals(c, dstPath, "file1-1\n"), checker.IsNil) +} diff --git a/integration-cli/docker_cli_cp_test.go b/integration-cli/docker_cli_cp_test.go new file mode 100644 index 00000000..f1ae7607 --- /dev/null +++ b/integration-cli/docker_cli_cp_test.go @@ -0,0 +1,665 @@ +package main + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +const ( + cpTestPathParent = "/some" + cpTestPath = "/some/path" + cpTestName = "test" + cpFullPath = "/some/path/test" + + cpContainerContents = "holla, i am the container" + cpHostContents = "hello, i am the host" +) + +// Ensure that an all-local path case returns an error. +func (s *DockerSuite) TestCpLocalOnly(c *check.C) { + err := runDockerCp(c, "foo", "bar") + c.Assert(err, checker.NotNil) + + c.Assert(err.Error(), checker.Contains, "must specify at least one container source") +} + +// Test for #5656 +// Check that garbage paths don't escape the container's rootfs +func (s *DockerSuite) TestCpGarbagePath(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "mkdir -p '"+cpTestPath+"' && echo -n '"+cpContainerContents+"' > "+cpFullPath) + + containerID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "wait", containerID) + // failed to set up container + c.Assert(strings.TrimSpace(out), checker.Equals, "0") + + c.Assert(os.MkdirAll(cpTestPath, os.ModeDir), checker.IsNil) + + hostFile, err := os.Create(cpFullPath) + c.Assert(err, checker.IsNil) + defer hostFile.Close() + defer os.RemoveAll(cpTestPathParent) + + fmt.Fprintf(hostFile, "%s", cpHostContents) + + tmpdir, err := ioutil.TempDir("", "docker-integration") + c.Assert(err, checker.IsNil) + + tmpname := filepath.Join(tmpdir, cpTestName) + defer os.RemoveAll(tmpdir) + + path := path.Join("../../../../../../../../../../../../", cpFullPath) + + dockerCmd(c, "cp", containerID+":"+path, tmpdir) + + file, _ := os.Open(tmpname) + defer file.Close() + + test, err := ioutil.ReadAll(file) + c.Assert(err, checker.IsNil) + + // output matched host file -- garbage path can escape container rootfs + c.Assert(string(test), checker.Not(checker.Equals), cpHostContents) + + // output doesn't match the input for garbage path + c.Assert(string(test), checker.Equals, cpContainerContents) +} + +// Check that relative paths are relative to the container's rootfs +func (s *DockerSuite) TestCpRelativePath(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "mkdir -p '"+cpTestPath+"' && echo -n '"+cpContainerContents+"' > "+cpFullPath) + + containerID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "wait", containerID) + // failed to set up container + c.Assert(strings.TrimSpace(out), checker.Equals, "0") + + c.Assert(os.MkdirAll(cpTestPath, os.ModeDir), checker.IsNil) + + hostFile, err := os.Create(cpFullPath) + c.Assert(err, checker.IsNil) + defer hostFile.Close() + defer os.RemoveAll(cpTestPathParent) + + fmt.Fprintf(hostFile, "%s", cpHostContents) + + tmpdir, err := ioutil.TempDir("", "docker-integration") + c.Assert(err, checker.IsNil) + + tmpname := filepath.Join(tmpdir, cpTestName) + defer os.RemoveAll(tmpdir) + + var relPath string + if path.IsAbs(cpFullPath) { + // normally this is `filepath.Rel("/", cpFullPath)` but we cannot + // get this unix-path manipulation on windows with filepath. + relPath = cpFullPath[1:] + } + c.Assert(path.IsAbs(cpFullPath), checker.True, check.Commentf("path %s was assumed to be an absolute path", cpFullPath)) + + dockerCmd(c, "cp", containerID+":"+relPath, tmpdir) + + file, _ := os.Open(tmpname) + defer file.Close() + + test, err := ioutil.ReadAll(file) + c.Assert(err, checker.IsNil) + + // output matched host file -- relative path can escape container rootfs + c.Assert(string(test), checker.Not(checker.Equals), cpHostContents) + + // output doesn't match the input for relative path + c.Assert(string(test), checker.Equals, cpContainerContents) +} + +// Check that absolute paths are relative to the container's rootfs +func (s *DockerSuite) TestCpAbsolutePath(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "mkdir -p '"+cpTestPath+"' && echo -n '"+cpContainerContents+"' > "+cpFullPath) + + containerID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "wait", containerID) + // failed to set up container + c.Assert(strings.TrimSpace(out), checker.Equals, "0") + + c.Assert(os.MkdirAll(cpTestPath, os.ModeDir), checker.IsNil) + + hostFile, err := os.Create(cpFullPath) + c.Assert(err, checker.IsNil) + defer hostFile.Close() + defer os.RemoveAll(cpTestPathParent) + + fmt.Fprintf(hostFile, "%s", cpHostContents) + + tmpdir, err := ioutil.TempDir("", "docker-integration") + c.Assert(err, checker.IsNil) + + tmpname := filepath.Join(tmpdir, cpTestName) + defer os.RemoveAll(tmpdir) + + path := cpFullPath + + dockerCmd(c, "cp", containerID+":"+path, tmpdir) + + file, _ := os.Open(tmpname) + defer file.Close() + + test, err := ioutil.ReadAll(file) + c.Assert(err, checker.IsNil) + + // output matched host file -- absolute path can escape container rootfs + c.Assert(string(test), checker.Not(checker.Equals), cpHostContents) + + // output doesn't match the input for absolute path + c.Assert(string(test), checker.Equals, cpContainerContents) +} + +// Test for #5619 +// Check that absolute symlinks are still relative to the container's rootfs +func (s *DockerSuite) TestCpAbsoluteSymlink(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "mkdir -p '"+cpTestPath+"' && echo -n '"+cpContainerContents+"' > "+cpFullPath+" && ln -s "+cpFullPath+" container_path") + + containerID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "wait", containerID) + // failed to set up container + c.Assert(strings.TrimSpace(out), checker.Equals, "0") + + c.Assert(os.MkdirAll(cpTestPath, os.ModeDir), checker.IsNil) + + hostFile, err := os.Create(cpFullPath) + c.Assert(err, checker.IsNil) + defer hostFile.Close() + defer os.RemoveAll(cpTestPathParent) + + fmt.Fprintf(hostFile, "%s", cpHostContents) + + tmpdir, err := ioutil.TempDir("", "docker-integration") + c.Assert(err, checker.IsNil) + + tmpname := filepath.Join(tmpdir, "container_path") + defer os.RemoveAll(tmpdir) + + path := path.Join("/", "container_path") + + dockerCmd(c, "cp", containerID+":"+path, tmpdir) + + // We should have copied a symlink *NOT* the file itself! + linkTarget, err := os.Readlink(tmpname) + c.Assert(err, checker.IsNil) + + c.Assert(linkTarget, checker.Equals, filepath.FromSlash(cpFullPath)) +} + +// Check that symlinks to a directory behave as expected when copying one from +// a container. +func (s *DockerSuite) TestCpFromSymlinkToDirectory(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "mkdir -p '"+cpTestPath+"' && echo -n '"+cpContainerContents+"' > "+cpFullPath+" && ln -s "+cpTestPathParent+" /dir_link") + + containerID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "wait", containerID) + // failed to set up container + c.Assert(strings.TrimSpace(out), checker.Equals, "0") + + testDir, err := ioutil.TempDir("", "test-cp-from-symlink-to-dir-") + c.Assert(err, checker.IsNil) + defer os.RemoveAll(testDir) + + // This copy command should copy the symlink, not the target, into the + // temporary directory. + dockerCmd(c, "cp", containerID+":"+"/dir_link", testDir) + + expectedPath := filepath.Join(testDir, "dir_link") + linkTarget, err := os.Readlink(expectedPath) + c.Assert(err, checker.IsNil) + + c.Assert(linkTarget, checker.Equals, filepath.FromSlash(cpTestPathParent)) + + os.Remove(expectedPath) + + // This copy command should resolve the symlink (note the trailing + // separator), copying the target into the temporary directory. + dockerCmd(c, "cp", containerID+":"+"/dir_link/", testDir) + + // It *should not* have copied the directory using the target's name, but + // used the given name instead. + unexpectedPath := filepath.Join(testDir, cpTestPathParent) + stat, err := os.Lstat(unexpectedPath) + if err == nil { + out = fmt.Sprintf("target name was copied: %q - %q", stat.Mode(), stat.Name()) + } + c.Assert(err, checker.NotNil, check.Commentf(out)) + + // It *should* have copied the directory using the asked name "dir_link". + stat, err = os.Lstat(expectedPath) + c.Assert(err, checker.IsNil, check.Commentf("unable to stat resource at %q", expectedPath)) + + c.Assert(stat.IsDir(), checker.True, check.Commentf("should have copied a directory but got %q instead", stat.Mode())) +} + +// Check that symlinks to a directory behave as expected when copying one to a +// container. +func (s *DockerSuite) TestCpToSymlinkToDirectory(c *check.C) { + testRequires(c, DaemonIsLinux) + testRequires(c, SameHostDaemon) // Requires local volume mount bind. + + testVol, err := ioutil.TempDir("", "test-cp-to-symlink-to-dir-") + c.Assert(err, checker.IsNil) + defer os.RemoveAll(testVol) + + // Create a test container with a local volume. We will test by copying + // to the volume path in the container which we can then verify locally. + out, _ := dockerCmd(c, "create", "-v", testVol+":/testVol", "busybox") + + containerID := strings.TrimSpace(out) + + // Create a temp directory to hold a test file nested in a direcotry. + testDir, err := ioutil.TempDir("", "test-cp-to-symlink-to-dir-") + c.Assert(err, checker.IsNil) + defer os.RemoveAll(testDir) + + // This file will be at "/testDir/some/path/test" and will be copied into + // the test volume later. + hostTestFilename := filepath.Join(testDir, cpFullPath) + c.Assert(os.MkdirAll(filepath.Dir(hostTestFilename), os.FileMode(0700)), checker.IsNil) + c.Assert(ioutil.WriteFile(hostTestFilename, []byte(cpHostContents), os.FileMode(0600)), checker.IsNil) + + // Now create another temp directory to hold a symlink to the + // "/testDir/some" directory. + linkDir, err := ioutil.TempDir("", "test-cp-to-symlink-to-dir-") + c.Assert(err, checker.IsNil) + defer os.RemoveAll(linkDir) + + // Then symlink "/linkDir/dir_link" to "/testdir/some". + linkTarget := filepath.Join(testDir, cpTestPathParent) + localLink := filepath.Join(linkDir, "dir_link") + c.Assert(os.Symlink(linkTarget, localLink), checker.IsNil) + + // Now copy that symlink into the test volume in the container. + dockerCmd(c, "cp", localLink, containerID+":/testVol") + + // This copy command should have copied the symlink *not* the target. + expectedPath := filepath.Join(testVol, "dir_link") + actualLinkTarget, err := os.Readlink(expectedPath) + c.Assert(err, checker.IsNil, check.Commentf("unable to read symlink at %q", expectedPath)) + + c.Assert(actualLinkTarget, checker.Equals, linkTarget) + + // Good, now remove that copied link for the next test. + os.Remove(expectedPath) + + // This copy command should resolve the symlink (note the trailing + // separator), copying the target into the test volume directory in the + // container. + dockerCmd(c, "cp", localLink+"/", containerID+":/testVol") + + // It *should not* have copied the directory using the target's name, but + // used the given name instead. + unexpectedPath := filepath.Join(testVol, cpTestPathParent) + stat, err := os.Lstat(unexpectedPath) + if err == nil { + out = fmt.Sprintf("target name was copied: %q - %q", stat.Mode(), stat.Name()) + } + c.Assert(err, checker.NotNil, check.Commentf(out)) + + // It *should* have copied the directory using the asked name "dir_link". + stat, err = os.Lstat(expectedPath) + c.Assert(err, checker.IsNil, check.Commentf("unable to stat resource at %q", expectedPath)) + + c.Assert(stat.IsDir(), checker.True, check.Commentf("should have copied a directory but got %q instead", stat.Mode())) + + // And this directory should contain the file copied from the host at the + // expected location: "/testVol/dir_link/path/test" + expectedFilepath := filepath.Join(testVol, "dir_link/path/test") + fileContents, err := ioutil.ReadFile(expectedFilepath) + c.Assert(err, checker.IsNil) + + c.Assert(string(fileContents), checker.Equals, cpHostContents) +} + +// Test for #5619 +// Check that symlinks which are part of the resource path are still relative to the container's rootfs +func (s *DockerSuite) TestCpSymlinkComponent(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "mkdir -p '"+cpTestPath+"' && echo -n '"+cpContainerContents+"' > "+cpFullPath+" && ln -s "+cpTestPath+" container_path") + + containerID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "wait", containerID) + // failed to set up container + c.Assert(strings.TrimSpace(out), checker.Equals, "0") + + c.Assert(os.MkdirAll(cpTestPath, os.ModeDir), checker.IsNil) + + hostFile, err := os.Create(cpFullPath) + c.Assert(err, checker.IsNil) + defer hostFile.Close() + defer os.RemoveAll(cpTestPathParent) + + fmt.Fprintf(hostFile, "%s", cpHostContents) + + tmpdir, err := ioutil.TempDir("", "docker-integration") + + c.Assert(err, checker.IsNil) + + tmpname := filepath.Join(tmpdir, cpTestName) + defer os.RemoveAll(tmpdir) + + path := path.Join("/", "container_path", cpTestName) + + dockerCmd(c, "cp", containerID+":"+path, tmpdir) + + file, _ := os.Open(tmpname) + defer file.Close() + + test, err := ioutil.ReadAll(file) + c.Assert(err, checker.IsNil) + + // output matched host file -- symlink path component can escape container rootfs + c.Assert(string(test), checker.Not(checker.Equals), cpHostContents) + + // output doesn't match the input for symlink path component + c.Assert(string(test), checker.Equals, cpContainerContents) +} + +// Check that cp with unprivileged user doesn't return any error +func (s *DockerSuite) TestCpUnprivilegedUser(c *check.C) { + testRequires(c, DaemonIsLinux) + testRequires(c, UnixCli) // uses chmod/su: not available on windows + + out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "touch "+cpTestName) + + containerID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "wait", containerID) + // failed to set up container + c.Assert(strings.TrimSpace(out), checker.Equals, "0") + + tmpdir, err := ioutil.TempDir("", "docker-integration") + c.Assert(err, checker.IsNil) + + defer os.RemoveAll(tmpdir) + + c.Assert(os.Chmod(tmpdir, 0777), checker.IsNil) + + path := cpTestName + + _, _, err = runCommandWithOutput(exec.Command("su", "unprivilegeduser", "-c", dockerBinary+" cp "+containerID+":"+path+" "+tmpdir)) + c.Assert(err, checker.IsNil, check.Commentf("couldn't copy with unprivileged user: %s:%s", containerID, path)) +} + +func (s *DockerSuite) TestCpSpecialFiles(c *check.C) { + testRequires(c, DaemonIsLinux) + testRequires(c, SameHostDaemon) + + outDir, err := ioutil.TempDir("", "cp-test-special-files") + c.Assert(err, checker.IsNil) + defer os.RemoveAll(outDir) + + out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "touch /foo") + + containerID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "wait", containerID) + // failed to set up container + c.Assert(strings.TrimSpace(out), checker.Equals, "0") + + // Copy actual /etc/resolv.conf + dockerCmd(c, "cp", containerID+":/etc/resolv.conf", outDir) + + expected, err := readContainerFile(containerID, "resolv.conf") + actual, err := ioutil.ReadFile(outDir + "/resolv.conf") + + // Expected copied file to be duplicate of the container resolvconf + c.Assert(bytes.Equal(actual, expected), checker.True) + + // Copy actual /etc/hosts + dockerCmd(c, "cp", containerID+":/etc/hosts", outDir) + + expected, err = readContainerFile(containerID, "hosts") + actual, err = ioutil.ReadFile(outDir + "/hosts") + + // Expected copied file to be duplicate of the container hosts + c.Assert(bytes.Equal(actual, expected), checker.True) + + // Copy actual /etc/resolv.conf + dockerCmd(c, "cp", containerID+":/etc/hostname", outDir) + + expected, err = readContainerFile(containerID, "hostname") + actual, err = ioutil.ReadFile(outDir + "/hostname") + + // Expected copied file to be duplicate of the container resolvconf + c.Assert(bytes.Equal(actual, expected), checker.True) +} + +func (s *DockerSuite) TestCpVolumePath(c *check.C) { + // stat /tmp/cp-test-volumepath851508420/test gets permission denied for the user + testRequires(c, NotUserNamespace) + testRequires(c, DaemonIsLinux) + testRequires(c, SameHostDaemon) + + tmpDir, err := ioutil.TempDir("", "cp-test-volumepath") + c.Assert(err, checker.IsNil) + defer os.RemoveAll(tmpDir) + outDir, err := ioutil.TempDir("", "cp-test-volumepath-out") + c.Assert(err, checker.IsNil) + defer os.RemoveAll(outDir) + _, err = os.Create(tmpDir + "/test") + c.Assert(err, checker.IsNil) + + out, _ := dockerCmd(c, "run", "-d", "-v", "/foo", "-v", tmpDir+"/test:/test", "-v", tmpDir+":/baz", "busybox", "/bin/sh", "-c", "touch /foo/bar") + + containerID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "wait", containerID) + // failed to set up container + c.Assert(strings.TrimSpace(out), checker.Equals, "0") + + // Copy actual volume path + dockerCmd(c, "cp", containerID+":/foo", outDir) + + stat, err := os.Stat(outDir + "/foo") + c.Assert(err, checker.IsNil) + // expected copied content to be dir + c.Assert(stat.IsDir(), checker.True) + stat, err = os.Stat(outDir + "/foo/bar") + c.Assert(err, checker.IsNil) + // Expected file `bar` to be a file + c.Assert(stat.IsDir(), checker.False) + + // Copy file nested in volume + dockerCmd(c, "cp", containerID+":/foo/bar", outDir) + + stat, err = os.Stat(outDir + "/bar") + c.Assert(err, checker.IsNil) + // Expected file `bar` to be a file + c.Assert(stat.IsDir(), checker.False) + + // Copy Bind-mounted dir + dockerCmd(c, "cp", containerID+":/baz", outDir) + stat, err = os.Stat(outDir + "/baz") + c.Assert(err, checker.IsNil) + // Expected `baz` to be a dir + c.Assert(stat.IsDir(), checker.True) + + // Copy file nested in bind-mounted dir + dockerCmd(c, "cp", containerID+":/baz/test", outDir) + fb, err := ioutil.ReadFile(outDir + "/baz/test") + c.Assert(err, checker.IsNil) + fb2, err := ioutil.ReadFile(tmpDir + "/test") + c.Assert(err, checker.IsNil) + // Expected copied file to be duplicate of bind-mounted file + c.Assert(bytes.Equal(fb, fb2), checker.True) + + // Copy bind-mounted file + dockerCmd(c, "cp", containerID+":/test", outDir) + fb, err = ioutil.ReadFile(outDir + "/test") + c.Assert(err, checker.IsNil) + fb2, err = ioutil.ReadFile(tmpDir + "/test") + c.Assert(err, checker.IsNil) + // Expected copied file to be duplicate of bind-mounted file + c.Assert(bytes.Equal(fb, fb2), checker.True) +} + +func (s *DockerSuite) TestCpToDot(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "echo lololol > /test") + + containerID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "wait", containerID) + // failed to set up container + c.Assert(strings.TrimSpace(out), checker.Equals, "0") + + tmpdir, err := ioutil.TempDir("", "docker-integration") + c.Assert(err, checker.IsNil) + defer os.RemoveAll(tmpdir) + cwd, err := os.Getwd() + c.Assert(err, checker.IsNil) + defer os.Chdir(cwd) + c.Assert(os.Chdir(tmpdir), checker.IsNil) + dockerCmd(c, "cp", containerID+":/test", ".") + content, err := ioutil.ReadFile("./test") + c.Assert(string(content), checker.Equals, "lololol\n") +} + +func (s *DockerSuite) TestCpToStdout(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "echo lololol > /test") + + containerID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "wait", containerID) + // failed to set up container + c.Assert(strings.TrimSpace(out), checker.Equals, "0") + + out, _, err := runCommandPipelineWithOutput( + exec.Command(dockerBinary, "cp", containerID+":/test", "-"), + exec.Command("tar", "-vtf", "-")) + + c.Assert(err, checker.IsNil) + + c.Assert(out, checker.Contains, "test") + c.Assert(out, checker.Contains, "-rw") +} + +func (s *DockerSuite) TestCpNameHasColon(c *check.C) { + testRequires(c, SameHostDaemon, DaemonIsLinux) + + out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "echo lololol > /te:s:t") + + containerID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "wait", containerID) + // failed to set up container + c.Assert(strings.TrimSpace(out), checker.Equals, "0") + + tmpdir, err := ioutil.TempDir("", "docker-integration") + c.Assert(err, checker.IsNil) + defer os.RemoveAll(tmpdir) + dockerCmd(c, "cp", containerID+":/te:s:t", tmpdir) + content, err := ioutil.ReadFile(tmpdir + "/te:s:t") + c.Assert(string(content), checker.Equals, "lololol\n") +} + +func (s *DockerSuite) TestCopyAndRestart(c *check.C) { + testRequires(c, DaemonIsLinux) + expectedMsg := "hello" + out, _ := dockerCmd(c, "run", "-d", "busybox", "echo", expectedMsg) + containerID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "wait", containerID) + // failed to set up container + c.Assert(strings.TrimSpace(out), checker.Equals, "0") + + tmpDir, err := ioutil.TempDir("", "test-docker-restart-after-copy-") + c.Assert(err, checker.IsNil) + defer os.RemoveAll(tmpDir) + + dockerCmd(c, "cp", fmt.Sprintf("%s:/etc/group", containerID), tmpDir) + + out, _ = dockerCmd(c, "start", "-a", containerID) + + c.Assert(strings.TrimSpace(out), checker.Equals, expectedMsg) +} + +func (s *DockerSuite) TestCopyCreatedContainer(c *check.C) { + testRequires(c, DaemonIsLinux) + dockerCmd(c, "create", "--name", "test_cp", "-v", "/test", "busybox") + + tmpDir, err := ioutil.TempDir("", "test") + c.Assert(err, checker.IsNil) + defer os.RemoveAll(tmpDir) + dockerCmd(c, "cp", "test_cp:/bin/sh", tmpDir) +} + +// test copy with option `-L`: following symbol link +// Check that symlinks to a file behave as expected when copying one from +// a container to host following symbol link +func (s *DockerSuite) TestCpSymlinkFromConToHostFollowSymlink(c *check.C) { + testRequires(c, DaemonIsLinux) + out, exitCode := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "mkdir -p '"+cpTestPath+"' && echo -n '"+cpContainerContents+"' > "+cpFullPath+" && ln -s "+cpFullPath+" /dir_link") + if exitCode != 0 { + c.Fatal("failed to create a container", out) + } + + cleanedContainerID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "wait", cleanedContainerID) + if strings.TrimSpace(out) != "0" { + c.Fatal("failed to set up container", out) + } + + testDir, err := ioutil.TempDir("", "test-cp-symlink-container-to-host-follow-symlink") + if err != nil { + c.Fatal(err) + } + defer os.RemoveAll(testDir) + + // This copy command should copy the symlink, not the target, into the + // temporary directory. + dockerCmd(c, "cp", "-L", cleanedContainerID+":"+"/dir_link", testDir) + + expectedPath := filepath.Join(testDir, "dir_link") + + expected := []byte(cpContainerContents) + actual, err := ioutil.ReadFile(expectedPath) + + if !bytes.Equal(actual, expected) { + c.Fatalf("Expected copied file to be duplicate of the container symbol link target") + } + os.Remove(expectedPath) + + // now test copy symbol link to an non-existing file in host + expectedPath = filepath.Join(testDir, "somefile_host") + // expectedPath shouldn't exist, if exists, remove it + if _, err := os.Lstat(expectedPath); err == nil { + os.Remove(expectedPath) + } + + dockerCmd(c, "cp", "-L", cleanedContainerID+":"+"/dir_link", expectedPath) + + actual, err = ioutil.ReadFile(expectedPath) + + if !bytes.Equal(actual, expected) { + c.Fatalf("Expected copied file to be duplicate of the container symbol link target") + } + defer os.Remove(expectedPath) +} diff --git a/integration-cli/docker_cli_cp_to_container_test.go b/integration-cli/docker_cli_cp_to_container_test.go new file mode 100644 index 00000000..63fbd446 --- /dev/null +++ b/integration-cli/docker_cli_cp_to_container_test.go @@ -0,0 +1,605 @@ +package main + +import ( + "os" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +// docker cp LOCALPATH CONTAINER:PATH + +// Try all of the test cases from the archive package which implements the +// internals of `docker cp` and ensure that the behavior matches when actually +// copying to and from containers. + +// Basic assumptions about SRC and DST: +// 1. SRC must exist. +// 2. If SRC ends with a trailing separator, it must be a directory. +// 3. DST parent directory must exist. +// 4. If DST exists as a file, it must not end with a trailing separator. + +// First get these easy error cases out of the way. + +// Test for error when SRC does not exist. +func (s *DockerSuite) TestCpToErrSrcNotExists(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{}) + + tmpDir := getTestDir(c, "test-cp-to-err-src-not-exists") + defer os.RemoveAll(tmpDir) + + srcPath := cpPath(tmpDir, "file1") + dstPath := containerCpPath(containerID, "file1") + + err := runDockerCp(c, srcPath, dstPath) + c.Assert(err, checker.NotNil) + + c.Assert(isCpNotExist(err), checker.True, check.Commentf("expected IsNotExist error, but got %T: %s", err, err)) +} + +// Test for error when SRC ends in a trailing +// path separator but it exists as a file. +func (s *DockerSuite) TestCpToErrSrcNotDir(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{}) + + tmpDir := getTestDir(c, "test-cp-to-err-src-not-dir") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + srcPath := cpPathTrailingSep(tmpDir, "file1") + dstPath := containerCpPath(containerID, "testDir") + + err := runDockerCp(c, srcPath, dstPath) + c.Assert(err, checker.NotNil) + + c.Assert(isCpNotDir(err), checker.True, check.Commentf("expected IsNotDir error, but got %T: %s", err, err)) +} + +// Test for error when SRC is a valid file or directory, +// bu the DST parent directory does not exist. +func (s *DockerSuite) TestCpToErrDstParentNotExists(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{addContent: true}) + + tmpDir := getTestDir(c, "test-cp-to-err-dst-parent-not-exists") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + // Try with a file source. + srcPath := cpPath(tmpDir, "file1") + dstPath := containerCpPath(containerID, "/notExists", "file1") + + err := runDockerCp(c, srcPath, dstPath) + c.Assert(err, checker.NotNil) + + c.Assert(isCpNotExist(err), checker.True, check.Commentf("expected IsNotExist error, but got %T: %s", err, err)) + + // Try with a directory source. + srcPath = cpPath(tmpDir, "dir1") + + c.Assert(err, checker.NotNil) + + c.Assert(isCpNotExist(err), checker.True, check.Commentf("expected IsNotExist error, but got %T: %s", err, err)) +} + +// Test for error when DST ends in a trailing path separator but exists as a +// file. Also test that we cannot overwrite an existing directory with a +// non-directory and cannot overwrite an existing +func (s *DockerSuite) TestCpToErrDstNotDir(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{addContent: true}) + + tmpDir := getTestDir(c, "test-cp-to-err-dst-not-dir") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + // Try with a file source. + srcPath := cpPath(tmpDir, "dir1/file1-1") + dstPath := containerCpPathTrailingSep(containerID, "file1") + + // The client should encounter an error trying to stat the destination + // and then be unable to copy since the destination is asserted to be a + // directory but does not exist. + err := runDockerCp(c, srcPath, dstPath) + c.Assert(err, checker.NotNil) + + c.Assert(isCpDirNotExist(err), checker.True, check.Commentf("expected DirNotExist error, but got %T: %s", err, err)) + + // Try with a directory source. + srcPath = cpPath(tmpDir, "dir1") + + // The client should encounter an error trying to stat the destination and + // then decide to extract to the parent directory instead with a rebased + // name in the source archive, but this directory would overwrite the + // existing file with the same name. + err = runDockerCp(c, srcPath, dstPath) + c.Assert(err, checker.NotNil) + + c.Assert(isCannotOverwriteNonDirWithDir(err), checker.True, check.Commentf("expected CannotOverwriteNonDirWithDir error, but got %T: %s", err, err)) +} + +// Check that copying from a local path to a symlink in a container copies to +// the symlink target and does not overwrite the container symlink itself. +func (s *DockerSuite) TestCpToSymlinkDestination(c *check.C) { + // stat /tmp/test-cp-to-symlink-destination-262430901/vol3 gets permission denied for the user + testRequires(c, NotUserNamespace) + testRequires(c, DaemonIsLinux) + testRequires(c, SameHostDaemon) // Requires local volume mount bind. + + testVol := getTestDir(c, "test-cp-to-symlink-destination-") + defer os.RemoveAll(testVol) + + makeTestContentInDir(c, testVol) + + containerID := makeTestContainer(c, testContainerOptions{ + volumes: defaultVolumes(testVol), // Our bind mount is at /vol2 + }) + + // First, copy a local file to a symlink to a file in the container. This + // should overwrite the symlink target contents with the source contents. + srcPath := cpPath(testVol, "file2") + dstPath := containerCpPath(containerID, "/vol2/symlinkToFile1") + + c.Assert(runDockerCp(c, srcPath, dstPath), checker.IsNil) + + // The symlink should not have been modified. + c.Assert(symlinkTargetEquals(c, cpPath(testVol, "symlinkToFile1"), "file1"), checker.IsNil) + + // The file should have the contents of "file2" now. + c.Assert(fileContentEquals(c, cpPath(testVol, "file1"), "file2\n"), checker.IsNil) + + // Next, copy a local file to a symlink to a directory in the container. + // This should copy the file into the symlink target directory. + dstPath = containerCpPath(containerID, "/vol2/symlinkToDir1") + + c.Assert(runDockerCp(c, srcPath, dstPath), checker.IsNil) + + // The symlink should not have been modified. + c.Assert(symlinkTargetEquals(c, cpPath(testVol, "symlinkToDir1"), "dir1"), checker.IsNil) + + // The file should have the contents of "file2" now. + c.Assert(fileContentEquals(c, cpPath(testVol, "file2"), "file2\n"), checker.IsNil) + + // Next, copy a file to a symlink to a file that does not exist (a broken + // symlink) in the container. This should create the target file with the + // contents of the source file. + dstPath = containerCpPath(containerID, "/vol2/brokenSymlinkToFileX") + + c.Assert(runDockerCp(c, srcPath, dstPath), checker.IsNil) + + // The symlink should not have been modified. + c.Assert(symlinkTargetEquals(c, cpPath(testVol, "brokenSymlinkToFileX"), "fileX"), checker.IsNil) + + // The file should have the contents of "file2" now. + c.Assert(fileContentEquals(c, cpPath(testVol, "fileX"), "file2\n"), checker.IsNil) + + // Next, copy a local directory to a symlink to a directory in the + // container. This should copy the directory into the symlink target + // directory and not modify the symlink. + srcPath = cpPath(testVol, "/dir2") + dstPath = containerCpPath(containerID, "/vol2/symlinkToDir1") + + c.Assert(runDockerCp(c, srcPath, dstPath), checker.IsNil) + + // The symlink should not have been modified. + c.Assert(symlinkTargetEquals(c, cpPath(testVol, "symlinkToDir1"), "dir1"), checker.IsNil) + + // The directory should now contain a copy of "dir2". + c.Assert(fileContentEquals(c, cpPath(testVol, "dir1/dir2/file2-1"), "file2-1\n"), checker.IsNil) + + // Next, copy a local directory to a symlink to a local directory that does + // not exist (a broken symlink) in the container. This should create the + // target as a directory with the contents of the source directory. It + // should not modify the symlink. + dstPath = containerCpPath(containerID, "/vol2/brokenSymlinkToDirX") + + c.Assert(runDockerCp(c, srcPath, dstPath), checker.IsNil) + + // The symlink should not have been modified. + c.Assert(symlinkTargetEquals(c, cpPath(testVol, "brokenSymlinkToDirX"), "dirX"), checker.IsNil) + + // The "dirX" directory should now be a copy of "dir2". + c.Assert(fileContentEquals(c, cpPath(testVol, "dirX/file2-1"), "file2-1\n"), checker.IsNil) +} + +// Possibilities are reduced to the remaining 10 cases: +// +// case | srcIsDir | onlyDirContents | dstExists | dstIsDir | dstTrSep | action +// =================================================================================================== +// A | no | - | no | - | no | create file +// B | no | - | no | - | yes | error +// C | no | - | yes | no | - | overwrite file +// D | no | - | yes | yes | - | create file in dst dir +// E | yes | no | no | - | - | create dir, copy contents +// F | yes | no | yes | no | - | error +// G | yes | no | yes | yes | - | copy dir and contents +// H | yes | yes | no | - | - | create dir, copy contents +// I | yes | yes | yes | no | - | error +// J | yes | yes | yes | yes | - | copy dir contents +// + +// A. SRC specifies a file and DST (no trailing path separator) doesn't +// exist. This should create a file with the name DST and copy the +// contents of the source file into it. +func (s *DockerSuite) TestCpToCaseA(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{ + workDir: "/root", command: makeCatFileCommand("itWorks.txt"), + }) + + tmpDir := getTestDir(c, "test-cp-to-case-a") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + srcPath := cpPath(tmpDir, "file1") + dstPath := containerCpPath(containerID, "/root/itWorks.txt") + + c.Assert(runDockerCp(c, srcPath, dstPath), checker.IsNil) + + c.Assert(containerStartOutputEquals(c, containerID, "file1\n"), checker.IsNil) +} + +// B. SRC specifies a file and DST (with trailing path separator) doesn't +// exist. This should cause an error because the copy operation cannot +// create a directory when copying a single file. +func (s *DockerSuite) TestCpToCaseB(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{ + command: makeCatFileCommand("testDir/file1"), + }) + + tmpDir := getTestDir(c, "test-cp-to-case-b") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + srcPath := cpPath(tmpDir, "file1") + dstDir := containerCpPathTrailingSep(containerID, "testDir") + + err := runDockerCp(c, srcPath, dstDir) + c.Assert(err, checker.NotNil) + + c.Assert(isCpDirNotExist(err), checker.True, check.Commentf("expected DirNotExists error, but got %T: %s", err, err)) +} + +// C. SRC specifies a file and DST exists as a file. This should overwrite +// the file at DST with the contents of the source file. +func (s *DockerSuite) TestCpToCaseC(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{ + addContent: true, workDir: "/root", + command: makeCatFileCommand("file2"), + }) + + tmpDir := getTestDir(c, "test-cp-to-case-c") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + srcPath := cpPath(tmpDir, "file1") + dstPath := containerCpPath(containerID, "/root/file2") + + // Ensure the container's file starts with the original content. + c.Assert(containerStartOutputEquals(c, containerID, "file2\n"), checker.IsNil) + + c.Assert(runDockerCp(c, srcPath, dstPath), checker.IsNil) + + // Should now contain file1's contents. + c.Assert(containerStartOutputEquals(c, containerID, "file1\n"), checker.IsNil) +} + +// D. SRC specifies a file and DST exists as a directory. This should place +// a copy of the source file inside it using the basename from SRC. Ensure +// this works whether DST has a trailing path separator or not. +func (s *DockerSuite) TestCpToCaseD(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{ + addContent: true, + command: makeCatFileCommand("/dir1/file1"), + }) + + tmpDir := getTestDir(c, "test-cp-to-case-d") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + srcPath := cpPath(tmpDir, "file1") + dstDir := containerCpPath(containerID, "dir1") + + // Ensure that dstPath doesn't exist. + c.Assert(containerStartOutputEquals(c, containerID, ""), checker.IsNil) + + c.Assert(runDockerCp(c, srcPath, dstDir), checker.IsNil) + + // Should now contain file1's contents. + c.Assert(containerStartOutputEquals(c, containerID, "file1\n"), checker.IsNil) + + // Now try again but using a trailing path separator for dstDir. + + // Make new destination container. + containerID = makeTestContainer(c, testContainerOptions{ + addContent: true, + command: makeCatFileCommand("/dir1/file1"), + }) + + dstDir = containerCpPathTrailingSep(containerID, "dir1") + + // Ensure that dstPath doesn't exist. + c.Assert(containerStartOutputEquals(c, containerID, ""), checker.IsNil) + + c.Assert(runDockerCp(c, srcPath, dstDir), checker.IsNil) + + // Should now contain file1's contents. + c.Assert(containerStartOutputEquals(c, containerID, "file1\n"), checker.IsNil) +} + +// E. SRC specifies a directory and DST does not exist. This should create a +// directory at DST and copy the contents of the SRC directory into the DST +// directory. Ensure this works whether DST has a trailing path separator or +// not. +func (s *DockerSuite) TestCpToCaseE(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{ + command: makeCatFileCommand("/testDir/file1-1"), + }) + + tmpDir := getTestDir(c, "test-cp-to-case-e") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + srcDir := cpPath(tmpDir, "dir1") + dstDir := containerCpPath(containerID, "testDir") + + c.Assert(runDockerCp(c, srcDir, dstDir), checker.IsNil) + + // Should now contain file1-1's contents. + c.Assert(containerStartOutputEquals(c, containerID, "file1-1\n"), checker.IsNil) + + // Now try again but using a trailing path separator for dstDir. + + // Make new destination container. + containerID = makeTestContainer(c, testContainerOptions{ + command: makeCatFileCommand("/testDir/file1-1"), + }) + + dstDir = containerCpPathTrailingSep(containerID, "testDir") + + c.Assert(runDockerCp(c, srcDir, dstDir), checker.IsNil) + + // Should now contain file1-1's contents. + c.Assert(containerStartOutputEquals(c, containerID, "file1-1\n"), checker.IsNil) +} + +// F. SRC specifies a directory and DST exists as a file. This should cause an +// error as it is not possible to overwrite a file with a directory. +func (s *DockerSuite) TestCpToCaseF(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{ + addContent: true, workDir: "/root", + }) + + tmpDir := getTestDir(c, "test-cp-to-case-f") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + srcDir := cpPath(tmpDir, "dir1") + dstFile := containerCpPath(containerID, "/root/file1") + + err := runDockerCp(c, srcDir, dstFile) + c.Assert(err, checker.NotNil) + + c.Assert(isCpCannotCopyDir(err), checker.True, check.Commentf("expected ErrCannotCopyDir error, but got %T: %s", err, err)) +} + +// G. SRC specifies a directory and DST exists as a directory. This should copy +// the SRC directory and all its contents to the DST directory. Ensure this +// works whether DST has a trailing path separator or not. +func (s *DockerSuite) TestCpToCaseG(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{ + addContent: true, workDir: "/root", + command: makeCatFileCommand("dir2/dir1/file1-1"), + }) + + tmpDir := getTestDir(c, "test-cp-to-case-g") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + srcDir := cpPath(tmpDir, "dir1") + dstDir := containerCpPath(containerID, "/root/dir2") + + // Ensure that dstPath doesn't exist. + c.Assert(containerStartOutputEquals(c, containerID, ""), checker.IsNil) + + c.Assert(runDockerCp(c, srcDir, dstDir), checker.IsNil) + + // Should now contain file1-1's contents. + c.Assert(containerStartOutputEquals(c, containerID, "file1-1\n"), checker.IsNil) + + // Now try again but using a trailing path separator for dstDir. + + // Make new destination container. + containerID = makeTestContainer(c, testContainerOptions{ + addContent: true, + command: makeCatFileCommand("/dir2/dir1/file1-1"), + }) + + dstDir = containerCpPathTrailingSep(containerID, "/dir2") + + // Ensure that dstPath doesn't exist. + c.Assert(containerStartOutputEquals(c, containerID, ""), checker.IsNil) + + c.Assert(runDockerCp(c, srcDir, dstDir), checker.IsNil) + + // Should now contain file1-1's contents. + c.Assert(containerStartOutputEquals(c, containerID, "file1-1\n"), checker.IsNil) +} + +// H. SRC specifies a directory's contents only and DST does not exist. This +// should create a directory at DST and copy the contents of the SRC +// directory (but not the directory itself) into the DST directory. Ensure +// this works whether DST has a trailing path separator or not. +func (s *DockerSuite) TestCpToCaseH(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{ + command: makeCatFileCommand("/testDir/file1-1"), + }) + + tmpDir := getTestDir(c, "test-cp-to-case-h") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + srcDir := cpPathTrailingSep(tmpDir, "dir1") + "." + dstDir := containerCpPath(containerID, "testDir") + + c.Assert(runDockerCp(c, srcDir, dstDir), checker.IsNil) + + // Should now contain file1-1's contents. + c.Assert(containerStartOutputEquals(c, containerID, "file1-1\n"), checker.IsNil) + + // Now try again but using a trailing path separator for dstDir. + + // Make new destination container. + containerID = makeTestContainer(c, testContainerOptions{ + command: makeCatFileCommand("/testDir/file1-1"), + }) + + dstDir = containerCpPathTrailingSep(containerID, "testDir") + + c.Assert(runDockerCp(c, srcDir, dstDir), checker.IsNil) + + // Should now contain file1-1's contents. + c.Assert(containerStartOutputEquals(c, containerID, "file1-1\n"), checker.IsNil) +} + +// I. SRC specifies a directory's contents only and DST exists as a file. This +// should cause an error as it is not possible to overwrite a file with a +// directory. +func (s *DockerSuite) TestCpToCaseI(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{ + addContent: true, workDir: "/root", + }) + + tmpDir := getTestDir(c, "test-cp-to-case-i") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + srcDir := cpPathTrailingSep(tmpDir, "dir1") + "." + dstFile := containerCpPath(containerID, "/root/file1") + + err := runDockerCp(c, srcDir, dstFile) + c.Assert(err, checker.NotNil) + + c.Assert(isCpCannotCopyDir(err), checker.True, check.Commentf("expected ErrCannotCopyDir error, but got %T: %s", err, err)) +} + +// J. SRC specifies a directory's contents only and DST exists as a directory. +// This should copy the contents of the SRC directory (but not the directory +// itself) into the DST directory. Ensure this works whether DST has a +// trailing path separator or not. +func (s *DockerSuite) TestCpToCaseJ(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := makeTestContainer(c, testContainerOptions{ + addContent: true, workDir: "/root", + command: makeCatFileCommand("/dir2/file1-1"), + }) + + tmpDir := getTestDir(c, "test-cp-to-case-j") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + srcDir := cpPathTrailingSep(tmpDir, "dir1") + "." + dstDir := containerCpPath(containerID, "/dir2") + + // Ensure that dstPath doesn't exist. + c.Assert(containerStartOutputEquals(c, containerID, ""), checker.IsNil) + + c.Assert(runDockerCp(c, srcDir, dstDir), checker.IsNil) + + // Should now contain file1-1's contents. + c.Assert(containerStartOutputEquals(c, containerID, "file1-1\n"), checker.IsNil) + + // Now try again but using a trailing path separator for dstDir. + + // Make new destination container. + containerID = makeTestContainer(c, testContainerOptions{ + command: makeCatFileCommand("/dir2/file1-1"), + }) + + dstDir = containerCpPathTrailingSep(containerID, "/dir2") + + // Ensure that dstPath doesn't exist. + c.Assert(containerStartOutputEquals(c, containerID, ""), checker.IsNil) + + c.Assert(runDockerCp(c, srcDir, dstDir), checker.IsNil) + + // Should now contain file1-1's contents. + c.Assert(containerStartOutputEquals(c, containerID, "file1-1\n"), checker.IsNil) +} + +// The `docker cp` command should also ensure that you cannot +// write to a container rootfs that is marked as read-only. +func (s *DockerSuite) TestCpToErrReadOnlyRootfs(c *check.C) { + // --read-only + userns has remount issues + testRequires(c, DaemonIsLinux, NotUserNamespace) + tmpDir := getTestDir(c, "test-cp-to-err-read-only-rootfs") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + containerID := makeTestContainer(c, testContainerOptions{ + readOnly: true, workDir: "/root", + command: makeCatFileCommand("shouldNotExist"), + }) + + srcPath := cpPath(tmpDir, "file1") + dstPath := containerCpPath(containerID, "/root/shouldNotExist") + + err := runDockerCp(c, srcPath, dstPath) + c.Assert(err, checker.NotNil) + + c.Assert(isCpCannotCopyReadOnly(err), checker.True, check.Commentf("expected ErrContainerRootfsReadonly error, but got %T: %s", err, err)) + + // Ensure that dstPath doesn't exist. + c.Assert(containerStartOutputEquals(c, containerID, ""), checker.IsNil) +} + +// The `docker cp` command should also ensure that you +// cannot write to a volume that is mounted as read-only. +func (s *DockerSuite) TestCpToErrReadOnlyVolume(c *check.C) { + // --read-only + userns has remount issues + testRequires(c, DaemonIsLinux, NotUserNamespace) + tmpDir := getTestDir(c, "test-cp-to-err-read-only-volume") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + containerID := makeTestContainer(c, testContainerOptions{ + volumes: defaultVolumes(tmpDir), workDir: "/root", + command: makeCatFileCommand("/vol_ro/shouldNotExist"), + }) + + srcPath := cpPath(tmpDir, "file1") + dstPath := containerCpPath(containerID, "/vol_ro/shouldNotExist") + + err := runDockerCp(c, srcPath, dstPath) + c.Assert(err, checker.NotNil) + + c.Assert(isCpCannotCopyReadOnly(err), checker.True, check.Commentf("expected ErrVolumeReadonly error, but got %T: %s", err, err)) + + // Ensure that dstPath doesn't exist. + c.Assert(containerStartOutputEquals(c, containerID, ""), checker.IsNil) +} diff --git a/integration-cli/docker_cli_cp_to_container_unix_test.go b/integration-cli/docker_cli_cp_to_container_unix_test.go new file mode 100644 index 00000000..45d85ba5 --- /dev/null +++ b/integration-cli/docker_cli_cp_to_container_unix_test.go @@ -0,0 +1,39 @@ +// +build !windows + +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/system" + "github.com/go-check/check" +) + +// Check ownership is root, both in non-userns and userns enabled modes +func (s *DockerSuite) TestCpCheckDestOwnership(c *check.C) { + testRequires(c, DaemonIsLinux, SameHostDaemon) + tmpVolDir := getTestDir(c, "test-cp-tmpvol") + containerID := makeTestContainer(c, + testContainerOptions{volumes: []string{fmt.Sprintf("%s:/tmpvol", tmpVolDir)}}) + + tmpDir := getTestDir(c, "test-cp-to-check-ownership") + defer os.RemoveAll(tmpDir) + + makeTestContentInDir(c, tmpDir) + + srcPath := cpPath(tmpDir, "file1") + dstPath := containerCpPath(containerID, "/tmpvol", "file1") + + err := runDockerCp(c, srcPath, dstPath) + c.Assert(err, checker.IsNil) + + stat, err := system.Stat(filepath.Join(tmpVolDir, "file1")) + c.Assert(err, checker.IsNil) + uid, gid, err := getRootUIDGID() + c.Assert(err, checker.IsNil) + c.Assert(stat.UID(), checker.Equals, uint32(uid), check.Commentf("Copied file not owned by container root UID")) + c.Assert(stat.GID(), checker.Equals, uint32(gid), check.Commentf("Copied file not owned by container root GID")) +} diff --git a/integration-cli/docker_cli_cp_utils.go b/integration-cli/docker_cli_cp_utils.go new file mode 100644 index 00000000..0501c5d7 --- /dev/null +++ b/integration-cli/docker_cli_cp_utils.go @@ -0,0 +1,303 @@ +package main + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +type fileType uint32 + +const ( + ftRegular fileType = iota + ftDir + ftSymlink +) + +type fileData struct { + filetype fileType + path string + contents string +} + +func (fd fileData) creationCommand() string { + var command string + + switch fd.filetype { + case ftRegular: + // Don't overwrite the file if it already exists! + command = fmt.Sprintf("if [ ! -f %s ]; then echo %q > %s; fi", fd.path, fd.contents, fd.path) + case ftDir: + command = fmt.Sprintf("mkdir -p %s", fd.path) + case ftSymlink: + command = fmt.Sprintf("ln -fs %s %s", fd.contents, fd.path) + } + + return command +} + +func mkFilesCommand(fds []fileData) string { + commands := make([]string, len(fds)) + + for i, fd := range fds { + commands[i] = fd.creationCommand() + } + + return strings.Join(commands, " && ") +} + +var defaultFileData = []fileData{ + {ftRegular, "file1", "file1"}, + {ftRegular, "file2", "file2"}, + {ftRegular, "file3", "file3"}, + {ftRegular, "file4", "file4"}, + {ftRegular, "file5", "file5"}, + {ftRegular, "file6", "file6"}, + {ftRegular, "file7", "file7"}, + {ftDir, "dir1", ""}, + {ftRegular, "dir1/file1-1", "file1-1"}, + {ftRegular, "dir1/file1-2", "file1-2"}, + {ftDir, "dir2", ""}, + {ftRegular, "dir2/file2-1", "file2-1"}, + {ftRegular, "dir2/file2-2", "file2-2"}, + {ftDir, "dir3", ""}, + {ftRegular, "dir3/file3-1", "file3-1"}, + {ftRegular, "dir3/file3-2", "file3-2"}, + {ftDir, "dir4", ""}, + {ftRegular, "dir4/file3-1", "file4-1"}, + {ftRegular, "dir4/file3-2", "file4-2"}, + {ftDir, "dir5", ""}, + {ftSymlink, "symlinkToFile1", "file1"}, + {ftSymlink, "symlinkToDir1", "dir1"}, + {ftSymlink, "brokenSymlinkToFileX", "fileX"}, + {ftSymlink, "brokenSymlinkToDirX", "dirX"}, + {ftSymlink, "symlinkToAbsDir", "/root"}, +} + +func defaultMkContentCommand() string { + return mkFilesCommand(defaultFileData) +} + +func makeTestContentInDir(c *check.C, dir string) { + for _, fd := range defaultFileData { + path := filepath.Join(dir, filepath.FromSlash(fd.path)) + switch fd.filetype { + case ftRegular: + c.Assert(ioutil.WriteFile(path, []byte(fd.contents+"\n"), os.FileMode(0666)), checker.IsNil) + case ftDir: + c.Assert(os.Mkdir(path, os.FileMode(0777)), checker.IsNil) + case ftSymlink: + c.Assert(os.Symlink(fd.contents, path), checker.IsNil) + } + } +} + +type testContainerOptions struct { + addContent bool + readOnly bool + volumes []string + workDir string + command string +} + +func makeTestContainer(c *check.C, options testContainerOptions) (containerID string) { + if options.addContent { + mkContentCmd := defaultMkContentCommand() + if options.command == "" { + options.command = mkContentCmd + } else { + options.command = fmt.Sprintf("%s && %s", defaultMkContentCommand(), options.command) + } + } + + if options.command == "" { + options.command = "#(nop)" + } + + args := []string{"run", "-d"} + + for _, volume := range options.volumes { + args = append(args, "-v", volume) + } + + if options.workDir != "" { + args = append(args, "-w", options.workDir) + } + + if options.readOnly { + args = append(args, "--read-only") + } + + args = append(args, "busybox", "/bin/sh", "-c", options.command) + + out, _ := dockerCmd(c, args...) + + containerID = strings.TrimSpace(out) + + out, _ = dockerCmd(c, "wait", containerID) + + exitCode := strings.TrimSpace(out) + if exitCode != "0" { + out, _ = dockerCmd(c, "logs", containerID) + } + c.Assert(exitCode, checker.Equals, "0", check.Commentf("failed to make test container: %s", out)) + + return +} + +func makeCatFileCommand(path string) string { + return fmt.Sprintf("if [ -f %s ]; then cat %s; fi", path, path) +} + +func cpPath(pathElements ...string) string { + localizedPathElements := make([]string, len(pathElements)) + for i, path := range pathElements { + localizedPathElements[i] = filepath.FromSlash(path) + } + return strings.Join(localizedPathElements, string(filepath.Separator)) +} + +func cpPathTrailingSep(pathElements ...string) string { + return fmt.Sprintf("%s%c", cpPath(pathElements...), filepath.Separator) +} + +func containerCpPath(containerID string, pathElements ...string) string { + joined := strings.Join(pathElements, "/") + return fmt.Sprintf("%s:%s", containerID, joined) +} + +func containerCpPathTrailingSep(containerID string, pathElements ...string) string { + return fmt.Sprintf("%s/", containerCpPath(containerID, pathElements...)) +} + +func runDockerCp(c *check.C, src, dst string) (err error) { + c.Logf("running `docker cp %s %s`", src, dst) + + args := []string{"cp", src, dst} + + out, _, err := runCommandWithOutput(exec.Command(dockerBinary, args...)) + if err != nil { + err = fmt.Errorf("error executing `docker cp` command: %s: %s", err, out) + } + + return +} + +func startContainerGetOutput(c *check.C, containerID string) (out string, err error) { + c.Logf("running `docker start -a %s`", containerID) + + args := []string{"start", "-a", containerID} + + out, _, err = runCommandWithOutput(exec.Command(dockerBinary, args...)) + if err != nil { + err = fmt.Errorf("error executing `docker start` command: %s: %s", err, out) + } + + return +} + +func getTestDir(c *check.C, label string) (tmpDir string) { + var err error + + tmpDir, err = ioutil.TempDir("", label) + // unable to make temporary directory + c.Assert(err, checker.IsNil) + + return +} + +func isCpNotExist(err error) bool { + return strings.Contains(err.Error(), "no such file or directory") || strings.Contains(err.Error(), "cannot find the file specified") +} + +func isCpDirNotExist(err error) bool { + return strings.Contains(err.Error(), archive.ErrDirNotExists.Error()) +} + +func isCpNotDir(err error) bool { + return strings.Contains(err.Error(), archive.ErrNotDirectory.Error()) || strings.Contains(err.Error(), "filename, directory name, or volume label syntax is incorrect") +} + +func isCpCannotCopyDir(err error) bool { + return strings.Contains(err.Error(), archive.ErrCannotCopyDir.Error()) +} + +func isCpCannotCopyReadOnly(err error) bool { + return strings.Contains(err.Error(), "marked read-only") +} + +func isCannotOverwriteNonDirWithDir(err error) bool { + return strings.Contains(err.Error(), "cannot overwrite non-directory") +} + +func fileContentEquals(c *check.C, filename, contents string) (err error) { + c.Logf("checking that file %q contains %q\n", filename, contents) + + fileBytes, err := ioutil.ReadFile(filename) + if err != nil { + return + } + + expectedBytes, err := ioutil.ReadAll(strings.NewReader(contents)) + if err != nil { + return + } + + if !bytes.Equal(fileBytes, expectedBytes) { + err = fmt.Errorf("file content not equal - expected %q, got %q", string(expectedBytes), string(fileBytes)) + } + + return +} + +func symlinkTargetEquals(c *check.C, symlink, expectedTarget string) (err error) { + c.Logf("checking that the symlink %q points to %q\n", symlink, expectedTarget) + + actualTarget, err := os.Readlink(symlink) + if err != nil { + return + } + + if actualTarget != expectedTarget { + err = fmt.Errorf("symlink target points to %q not %q", actualTarget, expectedTarget) + } + + return +} + +func containerStartOutputEquals(c *check.C, containerID, contents string) (err error) { + c.Logf("checking that container %q start output contains %q\n", containerID, contents) + + out, err := startContainerGetOutput(c, containerID) + if err != nil { + return + } + + if out != contents { + err = fmt.Errorf("output contents not equal - expected %q, got %q", contents, out) + } + + return +} + +func defaultVolumes(tmpDir string) []string { + if SameHostDaemon.Condition() { + return []string{ + "/vol1", + fmt.Sprintf("%s:/vol2", tmpDir), + fmt.Sprintf("%s:/vol3", filepath.Join(tmpDir, "vol3")), + fmt.Sprintf("%s:/vol_ro:ro", filepath.Join(tmpDir, "vol_ro")), + } + } + + // Can't bind-mount volumes with separate host daemon. + return []string{"/vol1", "/vol2", "/vol3", "/vol_ro:/vol_ro:ro"} +} diff --git a/integration-cli/docker_cli_create_test.go b/integration-cli/docker_cli_create_test.go new file mode 100644 index 00000000..a22bb3ec --- /dev/null +++ b/integration-cli/docker_cli_create_test.go @@ -0,0 +1,460 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "reflect" + "strings" + "time" + + "os/exec" + + "io/ioutil" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/go-connections/nat" + "github.com/go-check/check" +) + +// Make sure we can create a simple container with some args +func (s *DockerSuite) TestCreateArgs(c *check.C) { + // TODO Windows. This requires further investigation for porting to + // Windows CI. Currently fails. + if daemonPlatform == "windows" { + c.Skip("Fails on Windows CI") + } + out, _ := dockerCmd(c, "create", "busybox", "command", "arg1", "arg2", "arg with space") + + cleanedContainerID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "inspect", cleanedContainerID) + + containers := []struct { + ID string + Created time.Time + Path string + Args []string + Image string + }{} + + err := json.Unmarshal([]byte(out), &containers) + c.Assert(err, check.IsNil, check.Commentf("Error inspecting the container: %s", err)) + c.Assert(containers, checker.HasLen, 1) + + cont := containers[0] + c.Assert(string(cont.Path), checker.Equals, "command", check.Commentf("Unexpected container path. Expected command, received: %s", cont.Path)) + + b := false + expected := []string{"arg1", "arg2", "arg with space"} + for i, arg := range expected { + if arg != cont.Args[i] { + b = true + break + } + } + if len(cont.Args) != len(expected) || b { + c.Fatalf("Unexpected args. Expected %v, received: %v", expected, cont.Args) + } + +} + +// Make sure we can set hostconfig options too +func (s *DockerSuite) TestCreateHostConfig(c *check.C) { + out, _ := dockerCmd(c, "create", "-P", "busybox", "echo") + + cleanedContainerID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "inspect", cleanedContainerID) + + containers := []struct { + HostConfig *struct { + PublishAllPorts bool + } + }{} + + err := json.Unmarshal([]byte(out), &containers) + c.Assert(err, check.IsNil, check.Commentf("Error inspecting the container: %s", err)) + c.Assert(containers, checker.HasLen, 1) + + cont := containers[0] + c.Assert(cont.HostConfig, check.NotNil, check.Commentf("Expected HostConfig, got none")) + c.Assert(cont.HostConfig.PublishAllPorts, check.NotNil, check.Commentf("Expected PublishAllPorts, got false")) +} + +func (s *DockerSuite) TestCreateWithPortRange(c *check.C) { + // Windows does not currently support port ranges. + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "create", "-p", "3300-3303:3300-3303/tcp", "busybox", "echo") + + cleanedContainerID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "inspect", cleanedContainerID) + + containers := []struct { + HostConfig *struct { + PortBindings map[nat.Port][]nat.PortBinding + } + }{} + err := json.Unmarshal([]byte(out), &containers) + c.Assert(err, check.IsNil, check.Commentf("Error inspecting the container: %s", err)) + c.Assert(containers, checker.HasLen, 1) + + cont := containers[0] + + c.Assert(cont.HostConfig, check.NotNil, check.Commentf("Expected HostConfig, got none")) + c.Assert(cont.HostConfig.PortBindings, checker.HasLen, 4, check.Commentf("Expected 4 ports bindings, got %d", len(cont.HostConfig.PortBindings))) + + for k, v := range cont.HostConfig.PortBindings { + c.Assert(v, checker.HasLen, 1, check.Commentf("Expected 1 ports binding, for the port %s but found %s", k, v)) + c.Assert(k.Port(), checker.Equals, v[0].HostPort, check.Commentf("Expected host port %s to match published port %s", k.Port(), v[0].HostPort)) + + } + +} + +func (s *DockerSuite) TestCreateWithLargePortRange(c *check.C) { + // Windows does not currently support port ranges. + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "create", "-p", "1-65535:1-65535/tcp", "busybox", "echo") + + cleanedContainerID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "inspect", cleanedContainerID) + + containers := []struct { + HostConfig *struct { + PortBindings map[nat.Port][]nat.PortBinding + } + }{} + + err := json.Unmarshal([]byte(out), &containers) + c.Assert(err, check.IsNil, check.Commentf("Error inspecting the container: %s", err)) + c.Assert(containers, checker.HasLen, 1) + + cont := containers[0] + c.Assert(cont.HostConfig, check.NotNil, check.Commentf("Expected HostConfig, got none")) + c.Assert(cont.HostConfig.PortBindings, checker.HasLen, 65535) + + for k, v := range cont.HostConfig.PortBindings { + c.Assert(v, checker.HasLen, 1) + c.Assert(k.Port(), checker.Equals, v[0].HostPort, check.Commentf("Expected host port %s to match published port %s", k.Port(), v[0].HostPort)) + } + +} + +// "test123" should be printed by docker create + start +func (s *DockerSuite) TestCreateEchoStdout(c *check.C) { + out, _ := dockerCmd(c, "create", "busybox", "echo", "test123") + + cleanedContainerID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "start", "-ai", cleanedContainerID) + c.Assert(out, checker.Equals, "test123\n", check.Commentf("container should've printed 'test123', got %q", out)) + +} + +func (s *DockerSuite) TestCreateVolumesCreated(c *check.C) { + testRequires(c, SameHostDaemon) + prefix := "/" + if daemonPlatform == "windows" { + prefix = `c:\` + } + + name := "test_create_volume" + dockerCmd(c, "create", "--name", name, "-v", prefix+"foo", "busybox") + + dir, err := inspectMountSourceField(name, prefix+"foo") + c.Assert(err, check.IsNil, check.Commentf("Error getting volume host path: %q", err)) + + if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) { + c.Fatalf("Volume was not created") + } + if err != nil { + c.Fatalf("Error statting volume host path: %q", err) + } + +} + +func (s *DockerSuite) TestCreateLabels(c *check.C) { + name := "test_create_labels" + expected := map[string]string{"k1": "v1", "k2": "v2"} + dockerCmd(c, "create", "--name", name, "-l", "k1=v1", "--label", "k2=v2", "busybox") + + actual := make(map[string]string) + inspectFieldAndMarshall(c, name, "Config.Labels", &actual) + + if !reflect.DeepEqual(expected, actual) { + c.Fatalf("Expected %s got %s", expected, actual) + } +} + +func (s *DockerSuite) TestCreateLabelFromImage(c *check.C) { + imageName := "testcreatebuildlabel" + _, err := buildImage(imageName, + `FROM busybox + LABEL k1=v1 k2=v2`, + true) + + c.Assert(err, check.IsNil) + + name := "test_create_labels_from_image" + expected := map[string]string{"k2": "x", "k3": "v3", "k1": "v1"} + dockerCmd(c, "create", "--name", name, "-l", "k2=x", "--label", "k3=v3", imageName) + + actual := make(map[string]string) + inspectFieldAndMarshall(c, name, "Config.Labels", &actual) + + if !reflect.DeepEqual(expected, actual) { + c.Fatalf("Expected %s got %s", expected, actual) + } +} + +func (s *DockerSuite) TestCreateHostnameWithNumber(c *check.C) { + // TODO Windows. Consider enabling this in TP5 timeframe if Windows support + // is fully hooked up. The hostname is passed through, but only to the + // environment variable "COMPUTERNAME". It is not hooked up to hostname.exe + // or returned in ipconfig. Needs platform support in networking. + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-h", "web.0", "busybox", "hostname") + c.Assert(strings.TrimSpace(out), checker.Equals, "web.0", check.Commentf("hostname not set, expected `web.0`, got: %s", out)) + +} + +func (s *DockerSuite) TestCreateRM(c *check.C) { + // Test to make sure we can 'rm' a new container that is in + // "Created" state, and has ever been run. Test "rm -f" too. + + // create a container + out, _ := dockerCmd(c, "create", "busybox") + cID := strings.TrimSpace(out) + + dockerCmd(c, "rm", cID) + + // Now do it again so we can "rm -f" this time + out, _ = dockerCmd(c, "create", "busybox") + + cID = strings.TrimSpace(out) + dockerCmd(c, "rm", "-f", cID) +} + +func (s *DockerSuite) TestCreateModeIpcContainer(c *check.C) { + // Uses Linux specific functionality (--ipc) + testRequires(c, DaemonIsLinux) + testRequires(c, SameHostDaemon, NotUserNamespace) + + out, _ := dockerCmd(c, "create", "busybox") + id := strings.TrimSpace(out) + + dockerCmd(c, "create", fmt.Sprintf("--ipc=container:%s", id), "busybox") +} + +func (s *DockerSuite) TestCreateByImageID(c *check.C) { + imageName := "testcreatebyimageid" + imageID, err := buildImage(imageName, + `FROM busybox + MAINTAINER dockerio`, + true) + if err != nil { + c.Fatal(err) + } + truncatedImageID := stringid.TruncateID(imageID) + + dockerCmd(c, "create", imageID) + dockerCmd(c, "create", truncatedImageID) + dockerCmd(c, "create", fmt.Sprintf("%s:%s", imageName, truncatedImageID)) + + // Ensure this fails + out, exit, _ := dockerCmdWithError("create", fmt.Sprintf("%s:%s", imageName, imageID)) + if exit == 0 { + c.Fatalf("expected non-zero exit code; received %d", exit) + } + + if expected := "Error parsing reference"; !strings.Contains(out, expected) { + c.Fatalf(`Expected %q in output; got: %s`, expected, out) + } + + out, exit, _ = dockerCmdWithError("create", fmt.Sprintf("%s:%s", "wrongimage", truncatedImageID)) + if exit == 0 { + c.Fatalf("expected non-zero exit code; received %d", exit) + } + + if expected := "Unable to find image"; !strings.Contains(out, expected) { + c.Fatalf(`Expected %q in output; got: %s`, expected, out) + } +} + +func (s *DockerTrustSuite) TestTrustedCreate(c *check.C) { + repoName := s.setupTrustedImage(c, "trusted-create") + + // Try create + createCmd := exec.Command(dockerBinary, "create", repoName) + s.trustedCmd(createCmd) + out, _, err := runCommandWithOutput(createCmd) + c.Assert(err, check.IsNil) + c.Assert(string(out), checker.Contains, "Tagging", check.Commentf("Missing expected output on trusted push:\n%s", out)) + + dockerCmd(c, "rmi", repoName) + + // Try untrusted create to ensure we pushed the tag to the registry + createCmd = exec.Command(dockerBinary, "create", "--disable-content-trust=true", repoName) + s.trustedCmd(createCmd) + out, _, err = runCommandWithOutput(createCmd) + c.Assert(err, check.IsNil) + c.Assert(string(out), checker.Contains, "Status: Downloaded", check.Commentf("Missing expected output on trusted create with --disable-content-trust:\n%s", out)) + +} + +func (s *DockerTrustSuite) TestUntrustedCreate(c *check.C) { + repoName := fmt.Sprintf("%v/dockercliuntrusted/createtest", privateRegistryURL) + withTagName := fmt.Sprintf("%s:latest", repoName) + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", withTagName) + dockerCmd(c, "push", withTagName) + dockerCmd(c, "rmi", withTagName) + + // Try trusted create on untrusted tag + createCmd := exec.Command(dockerBinary, "create", withTagName) + s.trustedCmd(createCmd) + out, _, err := runCommandWithOutput(createCmd) + c.Assert(err, check.Not(check.IsNil)) + c.Assert(string(out), checker.Contains, fmt.Sprintf("does not have trust data for %s", repoName), check.Commentf("Missing expected output on trusted create:\n%s", out)) + +} + +func (s *DockerTrustSuite) TestTrustedIsolatedCreate(c *check.C) { + repoName := s.setupTrustedImage(c, "trusted-isolated-create") + + // Try create + createCmd := exec.Command(dockerBinary, "--config", "/tmp/docker-isolated-create", "create", repoName) + s.trustedCmd(createCmd) + out, _, err := runCommandWithOutput(createCmd) + c.Assert(err, check.IsNil) + c.Assert(string(out), checker.Contains, "Tagging", check.Commentf("Missing expected output on trusted push:\n%s", out)) + + dockerCmd(c, "rmi", repoName) +} + +func (s *DockerTrustSuite) TestCreateWhenCertExpired(c *check.C) { + c.Skip("Currently changes system time, causing instability") + repoName := s.setupTrustedImage(c, "trusted-create-expired") + + // Certificates have 10 years of expiration + elevenYearsFromNow := time.Now().Add(time.Hour * 24 * 365 * 11) + + runAtDifferentDate(elevenYearsFromNow, func() { + // Try create + createCmd := exec.Command(dockerBinary, "create", repoName) + s.trustedCmd(createCmd) + out, _, err := runCommandWithOutput(createCmd) + c.Assert(err, check.Not(check.IsNil)) + c.Assert(string(out), checker.Contains, "could not validate the path to a trusted root", check.Commentf("Missing expected output on trusted create in the distant future:\n%s", out)) + }) + + runAtDifferentDate(elevenYearsFromNow, func() { + // Try create + createCmd := exec.Command(dockerBinary, "create", "--disable-content-trust", repoName) + s.trustedCmd(createCmd) + out, _, err := runCommandWithOutput(createCmd) + c.Assert(err, check.Not(check.IsNil)) + c.Assert(string(out), checker.Contains, "Status: Downloaded", check.Commentf("Missing expected output on trusted create in the distant future:\n%s", out)) + + }) +} + +func (s *DockerTrustSuite) TestTrustedCreateFromBadTrustServer(c *check.C) { + repoName := fmt.Sprintf("%v/dockerclievilcreate/trusted:latest", privateRegistryURL) + evilLocalConfigDir, err := ioutil.TempDir("", "evilcreate-local-config-dir") + c.Assert(err, check.IsNil) + + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", repoName) + + pushCmd := exec.Command(dockerBinary, "push", repoName) + s.trustedCmd(pushCmd) + out, _, err := runCommandWithOutput(pushCmd) + c.Assert(err, check.IsNil) + c.Assert(string(out), checker.Contains, "Signing and pushing trust metadata", check.Commentf("Missing expected output on trusted push:\n%s", out)) + + dockerCmd(c, "rmi", repoName) + + // Try create + createCmd := exec.Command(dockerBinary, "create", repoName) + s.trustedCmd(createCmd) + out, _, err = runCommandWithOutput(createCmd) + c.Assert(err, check.IsNil) + c.Assert(string(out), checker.Contains, "Tagging", check.Commentf("Missing expected output on trusted push:\n%s", out)) + + dockerCmd(c, "rmi", repoName) + + // Kill the notary server, start a new "evil" one. + s.not.Close() + s.not, err = newTestNotary(c) + c.Assert(err, check.IsNil) + + // In order to make an evil server, lets re-init a client (with a different trust dir) and push new data. + // tag an image and upload it to the private registry + dockerCmd(c, "--config", evilLocalConfigDir, "tag", "busybox", repoName) + + // Push up to the new server + pushCmd = exec.Command(dockerBinary, "--config", evilLocalConfigDir, "push", repoName) + s.trustedCmd(pushCmd) + out, _, err = runCommandWithOutput(pushCmd) + c.Assert(err, check.IsNil) + c.Assert(string(out), checker.Contains, "Signing and pushing trust metadata", check.Commentf("Missing expected output on trusted push:\n%s", out)) + + // Now, try creating with the original client from this new trust server. This should fallback to our cached timestamp and metadata. + createCmd = exec.Command(dockerBinary, "create", repoName) + s.trustedCmd(createCmd) + out, _, err = runCommandWithOutput(createCmd) + if err != nil { + c.Fatalf("Error falling back to cached trust data: %s\n%s", err, out) + } + if !strings.Contains(string(out), "Error while downloading remote metadata, using cached timestamp") { + c.Fatalf("Missing expected output on trusted create:\n%s", out) + } + +} + +func (s *DockerSuite) TestCreateStopSignal(c *check.C) { + name := "test_create_stop_signal" + dockerCmd(c, "create", "--name", name, "--stop-signal", "9", "busybox") + + res := inspectFieldJSON(c, name, "Config.StopSignal") + c.Assert(res, checker.Contains, "9") + +} + +func (s *DockerSuite) TestCreateWithWorkdir(c *check.C) { + // TODO Windows. This requires further investigation for porting to + // Windows CI. Currently fails. + if daemonPlatform == "windows" { + c.Skip("Fails on Windows CI") + } + name := "foo" + + prefix, slash := getPrefixAndSlashFromDaemonPlatform() + dir := prefix + slash + "home" + slash + "foo" + slash + "bar" + + dockerCmd(c, "create", "--name", name, "-w", dir, "busybox") + dockerCmd(c, "cp", fmt.Sprintf("%s:%s", name, dir), prefix+slash+"tmp") +} + +func (s *DockerSuite) TestCreateWithInvalidLogOpts(c *check.C) { + name := "test-invalidate-log-opts" + out, _, err := dockerCmdWithError("create", "--name", name, "--log-opt", "invalid=true", "busybox") + c.Assert(err, checker.NotNil) + c.Assert(out, checker.Contains, "unknown log opt") + + out, _ = dockerCmd(c, "ps", "-a") + c.Assert(out, checker.Not(checker.Contains), name) +} + +// #20972 +func (s *DockerSuite) TestCreate64ByteHexID(c *check.C) { + out := inspectField(c, "busybox", "Id") + imageID := strings.TrimPrefix(strings.TrimSpace(string(out)), "sha256:") + + dockerCmd(c, "create", imageID) +} diff --git a/integration-cli/docker_cli_daemon_experimental_test.go b/integration-cli/docker_cli_daemon_experimental_test.go new file mode 100644 index 00000000..fea887d7 --- /dev/null +++ b/integration-cli/docker_cli_daemon_experimental_test.go @@ -0,0 +1,195 @@ +// +build daemon,!windows,experimental + +package main + +import ( + "io/ioutil" + "os" + "os/exec" + "strings" + "time" + + "github.com/go-check/check" +) + +// TestDaemonRestartWithKilledRunningContainer requires live restore of running containers +func (s *DockerDaemonSuite) TestDaemonRestartWithKilledRunningContainer(t *check.C) { + // TODO(mlaventure): Not sure what would the exit code be on windows + testRequires(t, DaemonIsLinux) + if err := s.d.StartWithBusybox(); err != nil { + t.Fatal(err) + } + + cid, err := s.d.Cmd("run", "-d", "--name", "test", "busybox", "top") + defer s.d.Stop() + if err != nil { + t.Fatal(cid, err) + } + cid = strings.TrimSpace(cid) + + // Kill the daemon + if err := s.d.Kill(); err != nil { + t.Fatal(err) + } + + // kill the container + runCmd := exec.Command(ctrBinary, "--address", "/var/run/docker/libcontainerd/docker-containerd.sock", "containers", "kill", cid) + if out, ec, err := runCommandWithOutput(runCmd); err != nil { + t.Fatalf("Failed to run ctr, ExitCode: %d, err: '%v' output: '%s' cid: '%s'\n", ec, err, out, cid) + } + + // Give time to containerd to process the command if we don't + // the exit event might be received after we do the inspect + time.Sleep(3 * time.Second) + + // restart the daemon + if err := s.d.Start(); err != nil { + t.Fatal(err) + } + + // Check that we've got the correct exit code + out, err := s.d.Cmd("inspect", "-f", "{{.State.ExitCode}}", cid) + t.Assert(err, check.IsNil) + + out = strings.TrimSpace(out) + if out != "143" { + t.Fatalf("Expected exit code '%s' got '%s' for container '%s'\n", "143", out, cid) + } + +} + +// os.Kill should kill daemon ungracefully, leaving behind live containers. +// The live containers should be known to the restarted daemon. Stopping +// them now, should remove the mounts. +func (s *DockerDaemonSuite) TestCleanupMountsAfterDaemonCrash(c *check.C) { + testRequires(c, DaemonIsLinux) + c.Assert(s.d.StartWithBusybox(), check.IsNil) + + out, err := s.d.Cmd("run", "-d", "busybox", "top") + c.Assert(err, check.IsNil, check.Commentf("Output: %s", out)) + id := strings.TrimSpace(out) + + c.Assert(s.d.cmd.Process.Signal(os.Kill), check.IsNil) + mountOut, err := ioutil.ReadFile("/proc/self/mountinfo") + c.Assert(err, check.IsNil, check.Commentf("Output: %s", mountOut)) + + // container mounts should exist even after daemon has crashed. + comment := check.Commentf("%s should stay mounted from older daemon start:\nDaemon root repository %s\n%s", id, s.d.folder, mountOut) + c.Assert(strings.Contains(string(mountOut), id), check.Equals, true, comment) + + // restart daemon. + if err := s.d.Restart(); err != nil { + c.Fatal(err) + } + + // container should be running. + out, err = s.d.Cmd("inspect", "--format='{{.State.Running}}'", id) + c.Assert(err, check.IsNil, check.Commentf("Output: %s", out)) + out = strings.TrimSpace(out) + if out != "true" { + c.Fatalf("Container %s expected to stay alive after daemon restart", id) + } + + // 'docker stop' should work. + out, err = s.d.Cmd("stop", id) + c.Assert(err, check.IsNil, check.Commentf("Output: %s", out)) + + // Now, container mounts should be gone. + mountOut, err = ioutil.ReadFile("/proc/self/mountinfo") + c.Assert(err, check.IsNil, check.Commentf("Output: %s", mountOut)) + comment = check.Commentf("%s is still mounted from older daemon start:\nDaemon root repository %s\n%s", id, s.d.folder, mountOut) + c.Assert(strings.Contains(string(mountOut), id), check.Equals, false, comment) +} + +// TestDaemonRestartWithPausedRunningContainer requires live restore of running containers +func (s *DockerDaemonSuite) TestDaemonRestartWithPausedRunningContainer(t *check.C) { + if err := s.d.StartWithBusybox(); err != nil { + t.Fatal(err) + } + + cid, err := s.d.Cmd("run", "-d", "--name", "test", "busybox", "top") + defer s.d.Stop() + if err != nil { + t.Fatal(cid, err) + } + cid = strings.TrimSpace(cid) + + // Kill the daemon + if err := s.d.Kill(); err != nil { + t.Fatal(err) + } + + // kill the container + runCmd := exec.Command(ctrBinary, "--address", "/var/run/docker/libcontainerd/docker-containerd.sock", "containers", "pause", cid) + if out, ec, err := runCommandWithOutput(runCmd); err != nil { + t.Fatalf("Failed to run ctr, ExitCode: %d, err: '%v' output: '%s' cid: '%s'\n", ec, err, out, cid) + } + + // Give time to containerd to process the command if we don't + // the pause event might be received after we do the inspect + time.Sleep(3 * time.Second) + + // restart the daemon + if err := s.d.Start(); err != nil { + t.Fatal(err) + } + + // Check that we've got the correct status + out, err := s.d.Cmd("inspect", "-f", "{{.State.Status}}", cid) + t.Assert(err, check.IsNil) + + out = strings.TrimSpace(out) + if out != "paused" { + t.Fatalf("Expected exit code '%s' got '%s' for container '%s'\n", "paused", out, cid) + } +} + +// TestDaemonRestartWithUnpausedRunningContainer requires live restore of running containers. +func (s *DockerDaemonSuite) TestDaemonRestartWithUnpausedRunningContainer(t *check.C) { + // TODO(mlaventure): Not sure what would the exit code be on windows + testRequires(t, DaemonIsLinux) + if err := s.d.StartWithBusybox(); err != nil { + t.Fatal(err) + } + + cid, err := s.d.Cmd("run", "-d", "--name", "test", "busybox", "top") + defer s.d.Stop() + if err != nil { + t.Fatal(cid, err) + } + cid = strings.TrimSpace(cid) + + // pause the container + if _, err := s.d.Cmd("pause", cid); err != nil { + t.Fatal(cid, err) + } + + // Kill the daemon + if err := s.d.Kill(); err != nil { + t.Fatal(err) + } + + // resume the container + runCmd := exec.Command(ctrBinary, "--address", "/var/run/docker/libcontainerd/docker-containerd.sock", "containers", "resume", cid) + if out, ec, err := runCommandWithOutput(runCmd); err != nil { + t.Fatalf("Failed to run ctr, ExitCode: %d, err: '%v' output: '%s' cid: '%s'\n", ec, err, out, cid) + } + + // Give time to containerd to process the command if we don't + // the resume event might be received after we do the inspect + time.Sleep(3 * time.Second) + + // restart the daemon + if err := s.d.Start(); err != nil { + t.Fatal(err) + } + + // Check that we've got the correct status + out, err := s.d.Cmd("inspect", "-f", "{{.State.Status}}", cid) + t.Assert(err, check.IsNil) + + out = strings.TrimSpace(out) + if out != "running" { + t.Fatalf("Expected exit code '%s' got '%s' for container '%s'\n", "running", out, cid) + } +} diff --git a/integration-cli/docker_cli_daemon_not_experimental_test.go b/integration-cli/docker_cli_daemon_not_experimental_test.go new file mode 100644 index 00000000..8c5634cc --- /dev/null +++ b/integration-cli/docker_cli_daemon_not_experimental_test.go @@ -0,0 +1,59 @@ +// +build daemon,!windows,!experimental + +package main + +import ( + "io/ioutil" + "os" + "strings" + + "github.com/go-check/check" +) + +// os.Kill should kill daemon ungracefully, leaving behind container mounts. +// A subsequent daemon restart shoud clean up said mounts. +func (s *DockerDaemonSuite) TestCleanupMountsAfterDaemonKill(c *check.C) { + c.Assert(s.d.StartWithBusybox(), check.IsNil) + + out, err := s.d.Cmd("run", "-d", "busybox", "top") + c.Assert(err, check.IsNil, check.Commentf("Output: %s", out)) + id := strings.TrimSpace(out) + c.Assert(s.d.cmd.Process.Signal(os.Kill), check.IsNil) + mountOut, err := ioutil.ReadFile("/proc/self/mountinfo") + c.Assert(err, check.IsNil, check.Commentf("Output: %s", mountOut)) + + // container mounts should exist even after daemon has crashed. + comment := check.Commentf("%s should stay mounted from older daemon start:\nDaemon root repository %s\n%s", id, s.d.folder, mountOut) + c.Assert(strings.Contains(string(mountOut), id), check.Equals, true, comment) + + // restart daemon. + if err := s.d.Restart(); err != nil { + c.Fatal(err) + } + + // Now, container mounts should be gone. + mountOut, err = ioutil.ReadFile("/proc/self/mountinfo") + c.Assert(err, check.IsNil, check.Commentf("Output: %s", mountOut)) + comment = check.Commentf("%s is still mounted from older daemon start:\nDaemon root repository %s\n%s", id, s.d.folder, mountOut) + c.Assert(strings.Contains(string(mountOut), id), check.Equals, false, comment) +} + +// #22913 +func (s *DockerDaemonSuite) TestContainerStartAfterDaemonKill(c *check.C) { + testRequires(c, DaemonIsLinux) + c.Assert(s.d.StartWithBusybox(), check.IsNil) + + // the application is chosen so it generates output and doesn't react to SIGTERM + out, err := s.d.Cmd("run", "-d", "busybox", "sh", "-c", "while true;do date;done") + c.Assert(err, check.IsNil, check.Commentf("Output: %s", out)) + id := strings.TrimSpace(out) + c.Assert(s.d.cmd.Process.Signal(os.Kill), check.IsNil) + + // restart daemon. + if err := s.d.Restart(); err != nil { + c.Fatal(err) + } + + out, err = s.d.Cmd("start", id) + c.Assert(err, check.IsNil, check.Commentf("Output: %s", out)) +} diff --git a/integration-cli/docker_cli_daemon_test.go b/integration-cli/docker_cli_daemon_test.go new file mode 100644 index 00000000..348653bf --- /dev/null +++ b/integration-cli/docker_cli_daemon_test.go @@ -0,0 +1,2161 @@ +// +build daemon,!windows + +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net" + "os" + "os/exec" + "path" + "path/filepath" + "regexp" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/mount" + "github.com/docker/go-units" + "github.com/docker/libnetwork/iptables" + "github.com/docker/libtrust" + "github.com/go-check/check" + "github.com/kr/pty" +) + +func (s *DockerDaemonSuite) TestDaemonRestartWithRunningContainersPorts(c *check.C) { + if err := s.d.StartWithBusybox(); err != nil { + c.Fatalf("Could not start daemon with busybox: %v", err) + } + + if out, err := s.d.Cmd("run", "-d", "--name", "top1", "-p", "1234:80", "--restart", "always", "busybox:latest", "top"); err != nil { + c.Fatalf("Could not run top1: err=%v\n%s", err, out) + } + // --restart=no by default + if out, err := s.d.Cmd("run", "-d", "--name", "top2", "-p", "80", "busybox:latest", "top"); err != nil { + c.Fatalf("Could not run top2: err=%v\n%s", err, out) + } + + testRun := func(m map[string]bool, prefix string) { + var format string + for cont, shouldRun := range m { + out, err := s.d.Cmd("ps") + if err != nil { + c.Fatalf("Could not run ps: err=%v\n%q", err, out) + } + if shouldRun { + format = "%scontainer %q is not running" + } else { + format = "%scontainer %q is running" + } + if shouldRun != strings.Contains(out, cont) { + c.Fatalf(format, prefix, cont) + } + } + } + + testRun(map[string]bool{"top1": true, "top2": true}, "") + + if err := s.d.Restart(); err != nil { + c.Fatalf("Could not restart daemon: %v", err) + } + testRun(map[string]bool{"top1": true, "top2": false}, "After daemon restart: ") +} + +func (s *DockerDaemonSuite) TestDaemonRestartWithVolumesRefs(c *check.C) { + if err := s.d.StartWithBusybox(); err != nil { + c.Fatal(err) + } + + if out, err := s.d.Cmd("run", "--name", "volrestarttest1", "-v", "/foo", "busybox"); err != nil { + c.Fatal(err, out) + } + + if err := s.d.Restart(); err != nil { + c.Fatal(err) + } + + if _, err := s.d.Cmd("run", "-d", "--volumes-from", "volrestarttest1", "--name", "volrestarttest2", "busybox", "top"); err != nil { + c.Fatal(err) + } + + if out, err := s.d.Cmd("rm", "-fv", "volrestarttest2"); err != nil { + c.Fatal(err, out) + } + + out, err := s.d.Cmd("inspect", "-f", "{{json .Mounts}}", "volrestarttest1") + c.Assert(err, check.IsNil) + + if _, err := inspectMountPointJSON(out, "/foo"); err != nil { + c.Fatalf("Expected volume to exist: /foo, error: %v\n", err) + } +} + +// #11008 +func (s *DockerDaemonSuite) TestDaemonRestartUnlessStopped(c *check.C) { + err := s.d.StartWithBusybox() + c.Assert(err, check.IsNil) + + out, err := s.d.Cmd("run", "-d", "--name", "top1", "--restart", "always", "busybox:latest", "top") + c.Assert(err, check.IsNil, check.Commentf("run top1: %v", out)) + + out, err = s.d.Cmd("run", "-d", "--name", "top2", "--restart", "unless-stopped", "busybox:latest", "top") + c.Assert(err, check.IsNil, check.Commentf("run top2: %v", out)) + + testRun := func(m map[string]bool, prefix string) { + var format string + for name, shouldRun := range m { + out, err := s.d.Cmd("ps") + c.Assert(err, check.IsNil, check.Commentf("run ps: %v", out)) + if shouldRun { + format = "%scontainer %q is not running" + } else { + format = "%scontainer %q is running" + } + c.Assert(strings.Contains(out, name), check.Equals, shouldRun, check.Commentf(format, prefix, name)) + } + } + + // both running + testRun(map[string]bool{"top1": true, "top2": true}, "") + + out, err = s.d.Cmd("stop", "top1") + c.Assert(err, check.IsNil, check.Commentf(out)) + + out, err = s.d.Cmd("stop", "top2") + c.Assert(err, check.IsNil, check.Commentf(out)) + + // both stopped + testRun(map[string]bool{"top1": false, "top2": false}, "") + + err = s.d.Restart() + c.Assert(err, check.IsNil) + + // restart=always running + testRun(map[string]bool{"top1": true, "top2": false}, "After daemon restart: ") + + out, err = s.d.Cmd("start", "top2") + c.Assert(err, check.IsNil, check.Commentf("start top2: %v", out)) + + err = s.d.Restart() + c.Assert(err, check.IsNil) + + // both running + testRun(map[string]bool{"top1": true, "top2": true}, "After second daemon restart: ") + +} + +func (s *DockerDaemonSuite) TestDaemonStartIptablesFalse(c *check.C) { + if err := s.d.Start("--iptables=false"); err != nil { + c.Fatalf("we should have been able to start the daemon with passing iptables=false: %v", err) + } +} + +// Make sure we cannot shrink base device at daemon restart. +func (s *DockerDaemonSuite) TestDaemonRestartWithInvalidBasesize(c *check.C) { + testRequires(c, Devicemapper) + c.Assert(s.d.Start(), check.IsNil) + + oldBasesizeBytes := s.d.getBaseDeviceSize(c) + var newBasesizeBytes int64 = 1073741824 //1GB in bytes + + if newBasesizeBytes < oldBasesizeBytes { + err := s.d.Restart("--storage-opt", fmt.Sprintf("dm.basesize=%d", newBasesizeBytes)) + c.Assert(err, check.IsNil, check.Commentf("daemon should not have started as new base device size is less than existing base device size: %v", err)) + } + c.Assert(s.d.Stop(), check.IsNil) +} + +// Make sure we can grow base device at daemon restart. +func (s *DockerDaemonSuite) TestDaemonRestartWithIncreasedBasesize(c *check.C) { + testRequires(c, Devicemapper) + c.Assert(s.d.Start(), check.IsNil) + + oldBasesizeBytes := s.d.getBaseDeviceSize(c) + + var newBasesizeBytes int64 = 53687091200 //50GB in bytes + + if newBasesizeBytes < oldBasesizeBytes { + c.Skip(fmt.Sprintf("New base device size (%v) must be greater than (%s)", units.HumanSize(float64(newBasesizeBytes)), units.HumanSize(float64(oldBasesizeBytes)))) + } + + err := s.d.Restart("--storage-opt", fmt.Sprintf("dm.basesize=%d", newBasesizeBytes)) + c.Assert(err, check.IsNil, check.Commentf("we should have been able to start the daemon with increased base device size: %v", err)) + + basesizeAfterRestart := s.d.getBaseDeviceSize(c) + newBasesize, err := convertBasesize(newBasesizeBytes) + c.Assert(err, check.IsNil, check.Commentf("Error in converting base device size: %v", err)) + c.Assert(newBasesize, check.Equals, basesizeAfterRestart, check.Commentf("Basesize passed is not equal to Basesize set")) + c.Assert(s.d.Stop(), check.IsNil) +} + +// Issue #8444: If docker0 bridge is modified (intentionally or unintentionally) and +// no longer has an IP associated, we should gracefully handle that case and associate +// an IP with it rather than fail daemon start +func (s *DockerDaemonSuite) TestDaemonStartBridgeWithoutIPAssociation(c *check.C) { + // rather than depending on brctl commands to verify docker0 is created and up + // let's start the daemon and stop it, and then make a modification to run the + // actual test + if err := s.d.Start(); err != nil { + c.Fatalf("Could not start daemon: %v", err) + } + if err := s.d.Stop(); err != nil { + c.Fatalf("Could not stop daemon: %v", err) + } + + // now we will remove the ip from docker0 and then try starting the daemon + ipCmd := exec.Command("ip", "addr", "flush", "dev", "docker0") + stdout, stderr, _, err := runCommandWithStdoutStderr(ipCmd) + if err != nil { + c.Fatalf("failed to remove docker0 IP association: %v, stdout: %q, stderr: %q", err, stdout, stderr) + } + + if err := s.d.Start(); err != nil { + warning := "**WARNING: Docker bridge network in bad state--delete docker0 bridge interface to fix" + c.Fatalf("Could not start daemon when docker0 has no IP address: %v\n%s", err, warning) + } +} + +func (s *DockerDaemonSuite) TestDaemonIptablesClean(c *check.C) { + if err := s.d.StartWithBusybox(); err != nil { + c.Fatalf("Could not start daemon with busybox: %v", err) + } + + if out, err := s.d.Cmd("run", "-d", "--name", "top", "-p", "80", "busybox:latest", "top"); err != nil { + c.Fatalf("Could not run top: %s, %v", out, err) + } + + // get output from iptables with container running + ipTablesSearchString := "tcp dpt:80" + ipTablesCmd := exec.Command("iptables", "-nvL") + out, _, err := runCommandWithOutput(ipTablesCmd) + if err != nil { + c.Fatalf("Could not run iptables -nvL: %s, %v", out, err) + } + + if !strings.Contains(out, ipTablesSearchString) { + c.Fatalf("iptables output should have contained %q, but was %q", ipTablesSearchString, out) + } + + if err := s.d.Stop(); err != nil { + c.Fatalf("Could not stop daemon: %v", err) + } + + // get output from iptables after restart + ipTablesCmd = exec.Command("iptables", "-nvL") + out, _, err = runCommandWithOutput(ipTablesCmd) + if err != nil { + c.Fatalf("Could not run iptables -nvL: %s, %v", out, err) + } + + if strings.Contains(out, ipTablesSearchString) { + c.Fatalf("iptables output should not have contained %q, but was %q", ipTablesSearchString, out) + } +} + +func (s *DockerDaemonSuite) TestDaemonIptablesCreate(c *check.C) { + if err := s.d.StartWithBusybox(); err != nil { + c.Fatalf("Could not start daemon with busybox: %v", err) + } + + if out, err := s.d.Cmd("run", "-d", "--name", "top", "--restart=always", "-p", "80", "busybox:latest", "top"); err != nil { + c.Fatalf("Could not run top: %s, %v", out, err) + } + + // get output from iptables with container running + ipTablesSearchString := "tcp dpt:80" + ipTablesCmd := exec.Command("iptables", "-nvL") + out, _, err := runCommandWithOutput(ipTablesCmd) + if err != nil { + c.Fatalf("Could not run iptables -nvL: %s, %v", out, err) + } + + if !strings.Contains(out, ipTablesSearchString) { + c.Fatalf("iptables output should have contained %q, but was %q", ipTablesSearchString, out) + } + + if err := s.d.Restart(); err != nil { + c.Fatalf("Could not restart daemon: %v", err) + } + + // make sure the container is not running + runningOut, err := s.d.Cmd("inspect", "--format='{{.State.Running}}'", "top") + if err != nil { + c.Fatalf("Could not inspect on container: %s, %v", out, err) + } + if strings.TrimSpace(runningOut) != "true" { + c.Fatalf("Container should have been restarted after daemon restart. Status running should have been true but was: %q", strings.TrimSpace(runningOut)) + } + + // get output from iptables after restart + ipTablesCmd = exec.Command("iptables", "-nvL") + out, _, err = runCommandWithOutput(ipTablesCmd) + if err != nil { + c.Fatalf("Could not run iptables -nvL: %s, %v", out, err) + } + + if !strings.Contains(out, ipTablesSearchString) { + c.Fatalf("iptables output after restart should have contained %q, but was %q", ipTablesSearchString, out) + } +} + +// TestDaemonIPv6Enabled checks that when the daemon is started with --ipv6=true that the docker0 bridge +// has the fe80::1 address and that a container is assigned a link-local address +func (s *DockerSuite) TestDaemonIPv6Enabled(c *check.C) { + testRequires(c, IPv6) + + if err := setupV6(); err != nil { + c.Fatal("Could not set up host for IPv6 tests") + } + + d := NewDaemon(c) + + if err := d.StartWithBusybox("--ipv6"); err != nil { + c.Fatal(err) + } + defer d.Stop() + + iface, err := net.InterfaceByName("docker0") + if err != nil { + c.Fatalf("Error getting docker0 interface: %v", err) + } + + addrs, err := iface.Addrs() + if err != nil { + c.Fatalf("Error getting addresses for docker0 interface: %v", err) + } + + var found bool + expected := "fe80::1/64" + + for i := range addrs { + if addrs[i].String() == expected { + found = true + } + } + + if !found { + c.Fatalf("Bridge does not have an IPv6 Address") + } + + if out, err := d.Cmd("run", "-itd", "--name=ipv6test", "busybox:latest"); err != nil { + c.Fatalf("Could not run container: %s, %v", out, err) + } + + out, err := d.Cmd("inspect", "--format", "'{{.NetworkSettings.Networks.bridge.LinkLocalIPv6Address}}'", "ipv6test") + out = strings.Trim(out, " \r\n'") + + if err != nil { + c.Fatalf("Error inspecting container: %s, %v", out, err) + } + + if ip := net.ParseIP(out); ip == nil { + c.Fatalf("Container should have a link-local IPv6 address") + } + + out, err = d.Cmd("inspect", "--format", "'{{.NetworkSettings.Networks.bridge.GlobalIPv6Address}}'", "ipv6test") + out = strings.Trim(out, " \r\n'") + + if err != nil { + c.Fatalf("Error inspecting container: %s, %v", out, err) + } + + if ip := net.ParseIP(out); ip != nil { + c.Fatalf("Container should not have a global IPv6 address: %v", out) + } + + if err := teardownV6(); err != nil { + c.Fatal("Could not perform teardown for IPv6 tests") + } + +} + +// TestDaemonIPv6FixedCIDR checks that when the daemon is started with --ipv6=true and a fixed CIDR +// that running containers are given a link-local and global IPv6 address +func (s *DockerDaemonSuite) TestDaemonIPv6FixedCIDR(c *check.C) { + // IPv6 setup is messing with local bridge address. + testRequires(c, SameHostDaemon) + err := setupV6() + c.Assert(err, checker.IsNil, check.Commentf("Could not set up host for IPv6 tests")) + + err = s.d.StartWithBusybox("--ipv6", "--fixed-cidr-v6='2001:db8:2::/64'", "--default-gateway-v6='2001:db8:2::100'") + c.Assert(err, checker.IsNil, check.Commentf("Could not start daemon with busybox: %v", err)) + + out, err := s.d.Cmd("run", "-itd", "--name=ipv6test", "busybox:latest") + c.Assert(err, checker.IsNil, check.Commentf("Could not run container: %s, %v", out, err)) + + out, err = s.d.Cmd("inspect", "--format", "'{{.NetworkSettings.Networks.bridge.GlobalIPv6Address}}'", "ipv6test") + out = strings.Trim(out, " \r\n'") + + c.Assert(err, checker.IsNil, check.Commentf(out)) + + ip := net.ParseIP(out) + c.Assert(ip, checker.NotNil, check.Commentf("Container should have a global IPv6 address")) + + out, err = s.d.Cmd("inspect", "--format", "'{{.NetworkSettings.Networks.bridge.IPv6Gateway}}'", "ipv6test") + c.Assert(err, checker.IsNil, check.Commentf(out)) + + c.Assert(strings.Trim(out, " \r\n'"), checker.Equals, "2001:db8:2::100", check.Commentf("Container should have a global IPv6 gateway")) + + err = teardownV6() + c.Assert(err, checker.IsNil, check.Commentf("Could not perform teardown for IPv6 tests")) +} + +// TestDaemonIPv6FixedCIDRAndMac checks that when the daemon is started with ipv6 fixed CIDR +// the running containers are given a an IPv6 address derived from the MAC address and the ipv6 fixed CIDR +func (s *DockerDaemonSuite) TestDaemonIPv6FixedCIDRAndMac(c *check.C) { + // IPv6 setup is messing with local bridge address. + testRequires(c, SameHostDaemon) + err := setupV6() + c.Assert(err, checker.IsNil) + + err = s.d.StartWithBusybox("--ipv6", "--fixed-cidr-v6='2001:db8:1::/64'") + c.Assert(err, checker.IsNil) + + out, err := s.d.Cmd("run", "-itd", "--name=ipv6test", "--mac-address", "AA:BB:CC:DD:EE:FF", "busybox") + c.Assert(err, checker.IsNil) + + out, err = s.d.Cmd("inspect", "--format", "'{{.NetworkSettings.Networks.bridge.GlobalIPv6Address}}'", "ipv6test") + c.Assert(err, checker.IsNil) + c.Assert(strings.Trim(out, " \r\n'"), checker.Equals, "2001:db8:1::aabb:ccdd:eeff") + + err = teardownV6() + c.Assert(err, checker.IsNil) +} + +func (s *DockerDaemonSuite) TestDaemonLogLevelWrong(c *check.C) { + c.Assert(s.d.Start("--log-level=bogus"), check.NotNil, check.Commentf("Daemon shouldn't start with wrong log level")) +} + +func (s *DockerDaemonSuite) TestDaemonLogLevelDebug(c *check.C) { + if err := s.d.Start("--log-level=debug"); err != nil { + c.Fatal(err) + } + content, _ := ioutil.ReadFile(s.d.logFile.Name()) + if !strings.Contains(string(content), `level=debug`) { + c.Fatalf(`Missing level="debug" in log file:\n%s`, string(content)) + } +} + +func (s *DockerDaemonSuite) TestDaemonLogLevelFatal(c *check.C) { + // we creating new daemons to create new logFile + if err := s.d.Start("--log-level=fatal"); err != nil { + c.Fatal(err) + } + content, _ := ioutil.ReadFile(s.d.logFile.Name()) + if strings.Contains(string(content), `level=debug`) { + c.Fatalf(`Should not have level="debug" in log file:\n%s`, string(content)) + } +} + +func (s *DockerDaemonSuite) TestDaemonFlagD(c *check.C) { + if err := s.d.Start("-D"); err != nil { + c.Fatal(err) + } + content, _ := ioutil.ReadFile(s.d.logFile.Name()) + if !strings.Contains(string(content), `level=debug`) { + c.Fatalf(`Should have level="debug" in log file using -D:\n%s`, string(content)) + } +} + +func (s *DockerDaemonSuite) TestDaemonFlagDebug(c *check.C) { + if err := s.d.Start("--debug"); err != nil { + c.Fatal(err) + } + content, _ := ioutil.ReadFile(s.d.logFile.Name()) + if !strings.Contains(string(content), `level=debug`) { + c.Fatalf(`Should have level="debug" in log file using --debug:\n%s`, string(content)) + } +} + +func (s *DockerDaemonSuite) TestDaemonFlagDebugLogLevelFatal(c *check.C) { + if err := s.d.Start("--debug", "--log-level=fatal"); err != nil { + c.Fatal(err) + } + content, _ := ioutil.ReadFile(s.d.logFile.Name()) + if !strings.Contains(string(content), `level=debug`) { + c.Fatalf(`Should have level="debug" in log file when using both --debug and --log-level=fatal:\n%s`, string(content)) + } +} + +func (s *DockerDaemonSuite) TestDaemonAllocatesListeningPort(c *check.C) { + listeningPorts := [][]string{ + {"0.0.0.0", "0.0.0.0", "5678"}, + {"127.0.0.1", "127.0.0.1", "1234"}, + {"localhost", "127.0.0.1", "1235"}, + } + + cmdArgs := make([]string, 0, len(listeningPorts)*2) + for _, hostDirective := range listeningPorts { + cmdArgs = append(cmdArgs, "--host", fmt.Sprintf("tcp://%s:%s", hostDirective[0], hostDirective[2])) + } + + if err := s.d.StartWithBusybox(cmdArgs...); err != nil { + c.Fatalf("Could not start daemon with busybox: %v", err) + } + + for _, hostDirective := range listeningPorts { + output, err := s.d.Cmd("run", "-p", fmt.Sprintf("%s:%s:80", hostDirective[1], hostDirective[2]), "busybox", "true") + if err == nil { + c.Fatalf("Container should not start, expected port already allocated error: %q", output) + } else if !strings.Contains(output, "port is already allocated") { + c.Fatalf("Expected port is already allocated error: %q", output) + } + } +} + +func (s *DockerDaemonSuite) TestDaemonKeyGeneration(c *check.C) { + // TODO: skip or update for Windows daemon + os.Remove("/etc/docker/key.json") + if err := s.d.Start(); err != nil { + c.Fatalf("Could not start daemon: %v", err) + } + s.d.Stop() + + k, err := libtrust.LoadKeyFile("/etc/docker/key.json") + if err != nil { + c.Fatalf("Error opening key file") + } + kid := k.KeyID() + // Test Key ID is a valid fingerprint (e.g. QQXN:JY5W:TBXI:MK3X:GX6P:PD5D:F56N:NHCS:LVRZ:JA46:R24J:XEFF) + if len(kid) != 59 { + c.Fatalf("Bad key ID: %s", kid) + } +} + +func (s *DockerDaemonSuite) TestDaemonKeyMigration(c *check.C) { + // TODO: skip or update for Windows daemon + os.Remove("/etc/docker/key.json") + k1, err := libtrust.GenerateECP256PrivateKey() + if err != nil { + c.Fatalf("Error generating private key: %s", err) + } + if err := os.MkdirAll(filepath.Join(os.Getenv("HOME"), ".docker"), 0755); err != nil { + c.Fatalf("Error creating .docker directory: %s", err) + } + if err := libtrust.SaveKey(filepath.Join(os.Getenv("HOME"), ".docker", "key.json"), k1); err != nil { + c.Fatalf("Error saving private key: %s", err) + } + + if err := s.d.Start(); err != nil { + c.Fatalf("Could not start daemon: %v", err) + } + s.d.Stop() + + k2, err := libtrust.LoadKeyFile("/etc/docker/key.json") + if err != nil { + c.Fatalf("Error opening key file") + } + if k1.KeyID() != k2.KeyID() { + c.Fatalf("Key not migrated") + } +} + +// GH#11320 - verify that the daemon exits on failure properly +// Note that this explicitly tests the conflict of {-b,--bridge} and {--bip} options as the means +// to get a daemon init failure; no other tests for -b/--bip conflict are therefore required +func (s *DockerDaemonSuite) TestDaemonExitOnFailure(c *check.C) { + //attempt to start daemon with incorrect flags (we know -b and --bip conflict) + if err := s.d.Start("--bridge", "nosuchbridge", "--bip", "1.1.1.1"); err != nil { + //verify we got the right error + if !strings.Contains(err.Error(), "Daemon exited and never started") { + c.Fatalf("Expected daemon not to start, got %v", err) + } + // look in the log and make sure we got the message that daemon is shutting down + runCmd := exec.Command("grep", "Error starting daemon", s.d.LogFileName()) + if out, _, err := runCommandWithOutput(runCmd); err != nil { + c.Fatalf("Expected 'Error starting daemon' message; but doesn't exist in log: %q, err: %v", out, err) + } + } else { + //if we didn't get an error and the daemon is running, this is a failure + c.Fatal("Conflicting options should cause the daemon to error out with a failure") + } +} + +func (s *DockerDaemonSuite) TestDaemonBridgeExternal(c *check.C) { + d := s.d + err := d.Start("--bridge", "nosuchbridge") + c.Assert(err, check.NotNil, check.Commentf("--bridge option with an invalid bridge should cause the daemon to fail")) + defer d.Restart() + + bridgeName := "external-bridge" + bridgeIP := "192.169.1.1/24" + _, bridgeIPNet, _ := net.ParseCIDR(bridgeIP) + + out, err := createInterface(c, "bridge", bridgeName, bridgeIP) + c.Assert(err, check.IsNil, check.Commentf(out)) + defer deleteInterface(c, bridgeName) + + err = d.StartWithBusybox("--bridge", bridgeName) + c.Assert(err, check.IsNil) + + ipTablesSearchString := bridgeIPNet.String() + ipTablesCmd := exec.Command("iptables", "-t", "nat", "-nvL") + out, _, err = runCommandWithOutput(ipTablesCmd) + c.Assert(err, check.IsNil) + + c.Assert(strings.Contains(out, ipTablesSearchString), check.Equals, true, + check.Commentf("iptables output should have contained %q, but was %q", + ipTablesSearchString, out)) + + _, err = d.Cmd("run", "-d", "--name", "ExtContainer", "busybox", "top") + c.Assert(err, check.IsNil) + + containerIP := d.findContainerIP("ExtContainer") + ip := net.ParseIP(containerIP) + c.Assert(bridgeIPNet.Contains(ip), check.Equals, true, + check.Commentf("Container IP-Address must be in the same subnet range : %s", + containerIP)) +} + +func createInterface(c *check.C, ifType string, ifName string, ipNet string) (string, error) { + args := []string{"link", "add", "name", ifName, "type", ifType} + ipLinkCmd := exec.Command("ip", args...) + out, _, err := runCommandWithOutput(ipLinkCmd) + if err != nil { + return out, err + } + + ifCfgCmd := exec.Command("ifconfig", ifName, ipNet, "up") + out, _, err = runCommandWithOutput(ifCfgCmd) + return out, err +} + +func deleteInterface(c *check.C, ifName string) { + ifCmd := exec.Command("ip", "link", "delete", ifName) + out, _, err := runCommandWithOutput(ifCmd) + c.Assert(err, check.IsNil, check.Commentf(out)) + + flushCmd := exec.Command("iptables", "-t", "nat", "--flush") + out, _, err = runCommandWithOutput(flushCmd) + c.Assert(err, check.IsNil, check.Commentf(out)) + + flushCmd = exec.Command("iptables", "--flush") + out, _, err = runCommandWithOutput(flushCmd) + c.Assert(err, check.IsNil, check.Commentf(out)) +} + +func (s *DockerDaemonSuite) TestDaemonBridgeIP(c *check.C) { + // TestDaemonBridgeIP Steps + // 1. Delete the existing docker0 Bridge + // 2. Set --bip daemon configuration and start the new Docker Daemon + // 3. Check if the bip config has taken effect using ifconfig and iptables commands + // 4. Launch a Container and make sure the IP-Address is in the expected subnet + // 5. Delete the docker0 Bridge + // 6. Restart the Docker Daemon (via deferred action) + // This Restart takes care of bringing docker0 interface back to auto-assigned IP + + defaultNetworkBridge := "docker0" + deleteInterface(c, defaultNetworkBridge) + + d := s.d + + bridgeIP := "192.169.1.1/24" + ip, bridgeIPNet, _ := net.ParseCIDR(bridgeIP) + + err := d.StartWithBusybox("--bip", bridgeIP) + c.Assert(err, check.IsNil) + defer d.Restart() + + ifconfigSearchString := ip.String() + ifconfigCmd := exec.Command("ifconfig", defaultNetworkBridge) + out, _, _, err := runCommandWithStdoutStderr(ifconfigCmd) + c.Assert(err, check.IsNil) + + c.Assert(strings.Contains(out, ifconfigSearchString), check.Equals, true, + check.Commentf("ifconfig output should have contained %q, but was %q", + ifconfigSearchString, out)) + + ipTablesSearchString := bridgeIPNet.String() + ipTablesCmd := exec.Command("iptables", "-t", "nat", "-nvL") + out, _, err = runCommandWithOutput(ipTablesCmd) + c.Assert(err, check.IsNil) + + c.Assert(strings.Contains(out, ipTablesSearchString), check.Equals, true, + check.Commentf("iptables output should have contained %q, but was %q", + ipTablesSearchString, out)) + + out, err = d.Cmd("run", "-d", "--name", "test", "busybox", "top") + c.Assert(err, check.IsNil) + + containerIP := d.findContainerIP("test") + ip = net.ParseIP(containerIP) + c.Assert(bridgeIPNet.Contains(ip), check.Equals, true, + check.Commentf("Container IP-Address must be in the same subnet range : %s", + containerIP)) + deleteInterface(c, defaultNetworkBridge) +} + +func (s *DockerDaemonSuite) TestDaemonRestartWithBridgeIPChange(c *check.C) { + if err := s.d.Start(); err != nil { + c.Fatalf("Could not start daemon: %v", err) + } + defer s.d.Restart() + if err := s.d.Stop(); err != nil { + c.Fatalf("Could not stop daemon: %v", err) + } + + // now we will change the docker0's IP and then try starting the daemon + bridgeIP := "192.169.100.1/24" + _, bridgeIPNet, _ := net.ParseCIDR(bridgeIP) + + ipCmd := exec.Command("ifconfig", "docker0", bridgeIP) + stdout, stderr, _, err := runCommandWithStdoutStderr(ipCmd) + if err != nil { + c.Fatalf("failed to change docker0's IP association: %v, stdout: %q, stderr: %q", err, stdout, stderr) + } + + if err := s.d.Start("--bip", bridgeIP); err != nil { + c.Fatalf("Could not start daemon: %v", err) + } + + //check if the iptables contains new bridgeIP MASQUERADE rule + ipTablesSearchString := bridgeIPNet.String() + ipTablesCmd := exec.Command("iptables", "-t", "nat", "-nvL") + out, _, err := runCommandWithOutput(ipTablesCmd) + if err != nil { + c.Fatalf("Could not run iptables -nvL: %s, %v", out, err) + } + if !strings.Contains(out, ipTablesSearchString) { + c.Fatalf("iptables output should have contained new MASQUERADE rule with IP %q, but was %q", ipTablesSearchString, out) + } +} + +func (s *DockerDaemonSuite) TestDaemonBridgeFixedCidr(c *check.C) { + d := s.d + + bridgeName := "external-bridge" + bridgeIP := "192.169.1.1/24" + + out, err := createInterface(c, "bridge", bridgeName, bridgeIP) + c.Assert(err, check.IsNil, check.Commentf(out)) + defer deleteInterface(c, bridgeName) + + args := []string{"--bridge", bridgeName, "--fixed-cidr", "192.169.1.0/30"} + err = d.StartWithBusybox(args...) + c.Assert(err, check.IsNil) + defer d.Restart() + + for i := 0; i < 4; i++ { + cName := "Container" + strconv.Itoa(i) + out, err := d.Cmd("run", "-d", "--name", cName, "busybox", "top") + if err != nil { + c.Assert(strings.Contains(out, "no available IPv4 addresses"), check.Equals, true, + check.Commentf("Could not run a Container : %s %s", err.Error(), out)) + } + } +} + +func (s *DockerDaemonSuite) TestDaemonBridgeFixedCidr2(c *check.C) { + d := s.d + + bridgeName := "external-bridge" + bridgeIP := "10.2.2.1/16" + + out, err := createInterface(c, "bridge", bridgeName, bridgeIP) + c.Assert(err, check.IsNil, check.Commentf(out)) + defer deleteInterface(c, bridgeName) + + err = d.StartWithBusybox("--bip", bridgeIP, "--fixed-cidr", "10.2.2.0/24") + c.Assert(err, check.IsNil) + defer s.d.Restart() + + out, err = d.Cmd("run", "-d", "--name", "bb", "busybox", "top") + c.Assert(err, checker.IsNil, check.Commentf(out)) + defer d.Cmd("stop", "bb") + + out, err = d.Cmd("exec", "bb", "/bin/sh", "-c", "ifconfig eth0 | awk '/inet addr/{print substr($2,6)}'") + c.Assert(out, checker.Equals, "10.2.2.0\n") + + out, err = d.Cmd("run", "--rm", "busybox", "/bin/sh", "-c", "ifconfig eth0 | awk '/inet addr/{print substr($2,6)}'") + c.Assert(err, checker.IsNil, check.Commentf(out)) + c.Assert(out, checker.Equals, "10.2.2.2\n") +} + +func (s *DockerDaemonSuite) TestDaemonBridgeFixedCIDREqualBridgeNetwork(c *check.C) { + d := s.d + + bridgeName := "external-bridge" + bridgeIP := "172.27.42.1/16" + + out, err := createInterface(c, "bridge", bridgeName, bridgeIP) + c.Assert(err, check.IsNil, check.Commentf(out)) + defer deleteInterface(c, bridgeName) + + err = d.StartWithBusybox("--bridge", bridgeName, "--fixed-cidr", bridgeIP) + c.Assert(err, check.IsNil) + defer s.d.Restart() + + out, err = d.Cmd("run", "-d", "busybox", "top") + c.Assert(err, check.IsNil, check.Commentf(out)) + cid1 := strings.TrimSpace(out) + defer d.Cmd("stop", cid1) +} + +func (s *DockerDaemonSuite) TestDaemonDefaultGatewayIPv4Implicit(c *check.C) { + defaultNetworkBridge := "docker0" + deleteInterface(c, defaultNetworkBridge) + + d := s.d + + bridgeIP := "192.169.1.1" + bridgeIPNet := fmt.Sprintf("%s/24", bridgeIP) + + err := d.StartWithBusybox("--bip", bridgeIPNet) + c.Assert(err, check.IsNil) + defer d.Restart() + + expectedMessage := fmt.Sprintf("default via %s dev", bridgeIP) + out, err := d.Cmd("run", "busybox", "ip", "-4", "route", "list", "0/0") + c.Assert(strings.Contains(out, expectedMessage), check.Equals, true, + check.Commentf("Implicit default gateway should be bridge IP %s, but default route was '%s'", + bridgeIP, strings.TrimSpace(out))) + deleteInterface(c, defaultNetworkBridge) +} + +func (s *DockerDaemonSuite) TestDaemonDefaultGatewayIPv4Explicit(c *check.C) { + defaultNetworkBridge := "docker0" + deleteInterface(c, defaultNetworkBridge) + + d := s.d + + bridgeIP := "192.169.1.1" + bridgeIPNet := fmt.Sprintf("%s/24", bridgeIP) + gatewayIP := "192.169.1.254" + + err := d.StartWithBusybox("--bip", bridgeIPNet, "--default-gateway", gatewayIP) + c.Assert(err, check.IsNil) + defer d.Restart() + + expectedMessage := fmt.Sprintf("default via %s dev", gatewayIP) + out, err := d.Cmd("run", "busybox", "ip", "-4", "route", "list", "0/0") + c.Assert(strings.Contains(out, expectedMessage), check.Equals, true, + check.Commentf("Explicit default gateway should be %s, but default route was '%s'", + gatewayIP, strings.TrimSpace(out))) + deleteInterface(c, defaultNetworkBridge) +} + +func (s *DockerDaemonSuite) TestDaemonDefaultGatewayIPv4ExplicitOutsideContainerSubnet(c *check.C) { + defaultNetworkBridge := "docker0" + deleteInterface(c, defaultNetworkBridge) + + // Program a custom default gateway outside of the container subnet, daemon should accept it and start + err := s.d.StartWithBusybox("--bip", "172.16.0.10/16", "--fixed-cidr", "172.16.1.0/24", "--default-gateway", "172.16.0.254") + c.Assert(err, check.IsNil) + + deleteInterface(c, defaultNetworkBridge) + s.d.Restart() +} + +func (s *DockerDaemonSuite) TestDaemonDefaultNetworkInvalidClusterConfig(c *check.C) { + testRequires(c, DaemonIsLinux, SameHostDaemon) + + // Start daemon without docker0 bridge + defaultNetworkBridge := "docker0" + deleteInterface(c, defaultNetworkBridge) + + d := NewDaemon(c) + discoveryBackend := "consul://consuladdr:consulport/some/path" + err := d.Start(fmt.Sprintf("--cluster-store=%s", discoveryBackend)) + c.Assert(err, checker.IsNil) + + // Start daemon with docker0 bridge + ifconfigCmd := exec.Command("ifconfig", defaultNetworkBridge) + _, err = runCommand(ifconfigCmd) + c.Assert(err, check.IsNil) + + err = d.Restart(fmt.Sprintf("--cluster-store=%s", discoveryBackend)) + c.Assert(err, checker.IsNil) + + d.Stop() +} + +func (s *DockerDaemonSuite) TestDaemonIP(c *check.C) { + d := s.d + + ipStr := "192.170.1.1/24" + ip, _, _ := net.ParseCIDR(ipStr) + args := []string{"--ip", ip.String()} + err := d.StartWithBusybox(args...) + c.Assert(err, check.IsNil) + defer d.Restart() + + out, err := d.Cmd("run", "-d", "-p", "8000:8000", "busybox", "top") + c.Assert(err, check.NotNil, + check.Commentf("Running a container must fail with an invalid --ip option")) + c.Assert(strings.Contains(out, "Error starting userland proxy"), check.Equals, true) + + ifName := "dummy" + out, err = createInterface(c, "dummy", ifName, ipStr) + c.Assert(err, check.IsNil, check.Commentf(out)) + defer deleteInterface(c, ifName) + + _, err = d.Cmd("run", "-d", "-p", "8000:8000", "busybox", "top") + c.Assert(err, check.IsNil) + + ipTablesCmd := exec.Command("iptables", "-t", "nat", "-nvL") + out, _, err = runCommandWithOutput(ipTablesCmd) + c.Assert(err, check.IsNil) + + regex := fmt.Sprintf("DNAT.*%s.*dpt:8000", ip.String()) + matched, _ := regexp.MatchString(regex, out) + c.Assert(matched, check.Equals, true, + check.Commentf("iptables output should have contained %q, but was %q", regex, out)) +} + +func (s *DockerDaemonSuite) TestDaemonICCPing(c *check.C) { + testRequires(c, bridgeNfIptables) + d := s.d + + bridgeName := "external-bridge" + bridgeIP := "192.169.1.1/24" + + out, err := createInterface(c, "bridge", bridgeName, bridgeIP) + c.Assert(err, check.IsNil, check.Commentf(out)) + defer deleteInterface(c, bridgeName) + + args := []string{"--bridge", bridgeName, "--icc=false"} + err = d.StartWithBusybox(args...) + c.Assert(err, check.IsNil) + defer d.Restart() + + ipTablesCmd := exec.Command("iptables", "-nvL", "FORWARD") + out, _, err = runCommandWithOutput(ipTablesCmd) + c.Assert(err, check.IsNil) + + regex := fmt.Sprintf("DROP.*all.*%s.*%s", bridgeName, bridgeName) + matched, _ := regexp.MatchString(regex, out) + c.Assert(matched, check.Equals, true, + check.Commentf("iptables output should have contained %q, but was %q", regex, out)) + + // Pinging another container must fail with --icc=false + pingContainers(c, d, true) + + ipStr := "192.171.1.1/24" + ip, _, _ := net.ParseCIDR(ipStr) + ifName := "icc-dummy" + + createInterface(c, "dummy", ifName, ipStr) + + // But, Pinging external or a Host interface must succeed + pingCmd := fmt.Sprintf("ping -c 1 %s -W 1", ip.String()) + runArgs := []string{"--rm", "busybox", "sh", "-c", pingCmd} + _, err = d.Cmd("run", runArgs...) + c.Assert(err, check.IsNil) +} + +func (s *DockerDaemonSuite) TestDaemonICCLinkExpose(c *check.C) { + d := s.d + + bridgeName := "external-bridge" + bridgeIP := "192.169.1.1/24" + + out, err := createInterface(c, "bridge", bridgeName, bridgeIP) + c.Assert(err, check.IsNil, check.Commentf(out)) + defer deleteInterface(c, bridgeName) + + args := []string{"--bridge", bridgeName, "--icc=false"} + err = d.StartWithBusybox(args...) + c.Assert(err, check.IsNil) + defer d.Restart() + + ipTablesCmd := exec.Command("iptables", "-nvL", "FORWARD") + out, _, err = runCommandWithOutput(ipTablesCmd) + c.Assert(err, check.IsNil) + + regex := fmt.Sprintf("DROP.*all.*%s.*%s", bridgeName, bridgeName) + matched, _ := regexp.MatchString(regex, out) + c.Assert(matched, check.Equals, true, + check.Commentf("iptables output should have contained %q, but was %q", regex, out)) + + out, err = d.Cmd("run", "-d", "--expose", "4567", "--name", "icc1", "busybox", "nc", "-l", "-p", "4567") + c.Assert(err, check.IsNil, check.Commentf(out)) + + out, err = d.Cmd("run", "--link", "icc1:icc1", "busybox", "nc", "icc1", "4567") + c.Assert(err, check.IsNil, check.Commentf(out)) +} + +func (s *DockerDaemonSuite) TestDaemonLinksIpTablesRulesWhenLinkAndUnlink(c *check.C) { + bridgeName := "external-bridge" + bridgeIP := "192.169.1.1/24" + + out, err := createInterface(c, "bridge", bridgeName, bridgeIP) + c.Assert(err, check.IsNil, check.Commentf(out)) + defer deleteInterface(c, bridgeName) + + err = s.d.StartWithBusybox("--bridge", bridgeName, "--icc=false") + c.Assert(err, check.IsNil) + defer s.d.Restart() + + _, err = s.d.Cmd("run", "-d", "--name", "child", "--publish", "8080:80", "busybox", "top") + c.Assert(err, check.IsNil) + _, err = s.d.Cmd("run", "-d", "--name", "parent", "--link", "child:http", "busybox", "top") + c.Assert(err, check.IsNil) + + childIP := s.d.findContainerIP("child") + parentIP := s.d.findContainerIP("parent") + + sourceRule := []string{"-i", bridgeName, "-o", bridgeName, "-p", "tcp", "-s", childIP, "--sport", "80", "-d", parentIP, "-j", "ACCEPT"} + destinationRule := []string{"-i", bridgeName, "-o", bridgeName, "-p", "tcp", "-s", parentIP, "--dport", "80", "-d", childIP, "-j", "ACCEPT"} + if !iptables.Exists("filter", "DOCKER", sourceRule...) || !iptables.Exists("filter", "DOCKER", destinationRule...) { + c.Fatal("Iptables rules not found") + } + + s.d.Cmd("rm", "--link", "parent/http") + if iptables.Exists("filter", "DOCKER", sourceRule...) || iptables.Exists("filter", "DOCKER", destinationRule...) { + c.Fatal("Iptables rules should be removed when unlink") + } + + s.d.Cmd("kill", "child") + s.d.Cmd("kill", "parent") +} + +func (s *DockerDaemonSuite) TestDaemonUlimitDefaults(c *check.C) { + testRequires(c, DaemonIsLinux) + + if err := s.d.StartWithBusybox("--default-ulimit", "nofile=42:42", "--default-ulimit", "nproc=1024:1024"); err != nil { + c.Fatal(err) + } + + out, err := s.d.Cmd("run", "--ulimit", "nproc=2048", "--name=test", "busybox", "/bin/sh", "-c", "echo $(ulimit -n); echo $(ulimit -p)") + if err != nil { + c.Fatal(out, err) + } + + outArr := strings.Split(out, "\n") + if len(outArr) < 2 { + c.Fatalf("got unexpected output: %s", out) + } + nofile := strings.TrimSpace(outArr[0]) + nproc := strings.TrimSpace(outArr[1]) + + if nofile != "42" { + c.Fatalf("expected `ulimit -n` to be `42`, got: %s", nofile) + } + if nproc != "2048" { + c.Fatalf("exepcted `ulimit -p` to be 2048, got: %s", nproc) + } + + // Now restart daemon with a new default + if err := s.d.Restart("--default-ulimit", "nofile=43"); err != nil { + c.Fatal(err) + } + + out, err = s.d.Cmd("start", "-a", "test") + if err != nil { + c.Fatal(err) + } + + outArr = strings.Split(out, "\n") + if len(outArr) < 2 { + c.Fatalf("got unexpected output: %s", out) + } + nofile = strings.TrimSpace(outArr[0]) + nproc = strings.TrimSpace(outArr[1]) + + if nofile != "43" { + c.Fatalf("expected `ulimit -n` to be `43`, got: %s", nofile) + } + if nproc != "2048" { + c.Fatalf("exepcted `ulimit -p` to be 2048, got: %s", nproc) + } +} + +// #11315 +func (s *DockerDaemonSuite) TestDaemonRestartRenameContainer(c *check.C) { + if err := s.d.StartWithBusybox(); err != nil { + c.Fatal(err) + } + + if out, err := s.d.Cmd("run", "--name=test", "busybox"); err != nil { + c.Fatal(err, out) + } + + if out, err := s.d.Cmd("rename", "test", "test2"); err != nil { + c.Fatal(err, out) + } + + if err := s.d.Restart(); err != nil { + c.Fatal(err) + } + + if out, err := s.d.Cmd("start", "test2"); err != nil { + c.Fatal(err, out) + } +} + +func (s *DockerDaemonSuite) TestDaemonLoggingDriverDefault(c *check.C) { + if err := s.d.StartWithBusybox(); err != nil { + c.Fatal(err) + } + + out, err := s.d.Cmd("run", "--name=test", "busybox", "echo", "testline") + c.Assert(err, check.IsNil, check.Commentf(out)) + id, err := s.d.getIDByName("test") + c.Assert(err, check.IsNil) + + logPath := filepath.Join(s.d.root, "containers", id, id+"-json.log") + + if _, err := os.Stat(logPath); err != nil { + c.Fatal(err) + } + f, err := os.Open(logPath) + if err != nil { + c.Fatal(err) + } + var res struct { + Log string `json:"log"` + Stream string `json:"stream"` + Time time.Time `json:"time"` + } + if err := json.NewDecoder(f).Decode(&res); err != nil { + c.Fatal(err) + } + if res.Log != "testline\n" { + c.Fatalf("Unexpected log line: %q, expected: %q", res.Log, "testline\n") + } + if res.Stream != "stdout" { + c.Fatalf("Unexpected stream: %q, expected: %q", res.Stream, "stdout") + } + if !time.Now().After(res.Time) { + c.Fatalf("Log time %v in future", res.Time) + } +} + +func (s *DockerDaemonSuite) TestDaemonLoggingDriverDefaultOverride(c *check.C) { + if err := s.d.StartWithBusybox(); err != nil { + c.Fatal(err) + } + + out, err := s.d.Cmd("run", "--name=test", "--log-driver=none", "busybox", "echo", "testline") + if err != nil { + c.Fatal(out, err) + } + id, err := s.d.getIDByName("test") + c.Assert(err, check.IsNil) + + logPath := filepath.Join(s.d.root, "containers", id, id+"-json.log") + + if _, err := os.Stat(logPath); err == nil || !os.IsNotExist(err) { + c.Fatalf("%s shouldn't exits, error on Stat: %s", logPath, err) + } +} + +func (s *DockerDaemonSuite) TestDaemonLoggingDriverNone(c *check.C) { + if err := s.d.StartWithBusybox("--log-driver=none"); err != nil { + c.Fatal(err) + } + + out, err := s.d.Cmd("run", "--name=test", "busybox", "echo", "testline") + if err != nil { + c.Fatal(out, err) + } + id, err := s.d.getIDByName("test") + c.Assert(err, check.IsNil) + + logPath := filepath.Join(s.d.folder, "graph", "containers", id, id+"-json.log") + + if _, err := os.Stat(logPath); err == nil || !os.IsNotExist(err) { + c.Fatalf("%s shouldn't exits, error on Stat: %s", logPath, err) + } +} + +func (s *DockerDaemonSuite) TestDaemonLoggingDriverNoneOverride(c *check.C) { + if err := s.d.StartWithBusybox("--log-driver=none"); err != nil { + c.Fatal(err) + } + + out, err := s.d.Cmd("run", "--name=test", "--log-driver=json-file", "busybox", "echo", "testline") + if err != nil { + c.Fatal(out, err) + } + id, err := s.d.getIDByName("test") + c.Assert(err, check.IsNil) + + logPath := filepath.Join(s.d.root, "containers", id, id+"-json.log") + + if _, err := os.Stat(logPath); err != nil { + c.Fatal(err) + } + f, err := os.Open(logPath) + if err != nil { + c.Fatal(err) + } + var res struct { + Log string `json:"log"` + Stream string `json:"stream"` + Time time.Time `json:"time"` + } + if err := json.NewDecoder(f).Decode(&res); err != nil { + c.Fatal(err) + } + if res.Log != "testline\n" { + c.Fatalf("Unexpected log line: %q, expected: %q", res.Log, "testline\n") + } + if res.Stream != "stdout" { + c.Fatalf("Unexpected stream: %q, expected: %q", res.Stream, "stdout") + } + if !time.Now().After(res.Time) { + c.Fatalf("Log time %v in future", res.Time) + } +} + +func (s *DockerDaemonSuite) TestDaemonLoggingDriverNoneLogsError(c *check.C) { + c.Assert(s.d.StartWithBusybox("--log-driver=none"), checker.IsNil) + + out, err := s.d.Cmd("run", "--name=test", "busybox", "echo", "testline") + c.Assert(err, checker.IsNil, check.Commentf(out)) + + out, err = s.d.Cmd("logs", "test") + c.Assert(err, check.NotNil, check.Commentf("Logs should fail with 'none' driver")) + expected := `"logs" command is supported only for "json-file" and "journald" logging drivers (got: none)` + c.Assert(out, checker.Contains, expected) +} + +func (s *DockerDaemonSuite) TestDaemonDots(c *check.C) { + if err := s.d.StartWithBusybox(); err != nil { + c.Fatal(err) + } + + // Now create 4 containers + if _, err := s.d.Cmd("create", "busybox"); err != nil { + c.Fatalf("Error creating container: %q", err) + } + if _, err := s.d.Cmd("create", "busybox"); err != nil { + c.Fatalf("Error creating container: %q", err) + } + if _, err := s.d.Cmd("create", "busybox"); err != nil { + c.Fatalf("Error creating container: %q", err) + } + if _, err := s.d.Cmd("create", "busybox"); err != nil { + c.Fatalf("Error creating container: %q", err) + } + + s.d.Stop() + + s.d.Start("--log-level=debug") + s.d.Stop() + content, _ := ioutil.ReadFile(s.d.logFile.Name()) + if strings.Contains(string(content), "....") { + c.Fatalf("Debug level should not have ....\n%s", string(content)) + } + + s.d.Start("--log-level=error") + s.d.Stop() + content, _ = ioutil.ReadFile(s.d.logFile.Name()) + if strings.Contains(string(content), "....") { + c.Fatalf("Error level should not have ....\n%s", string(content)) + } + + s.d.Start("--log-level=info") + s.d.Stop() + content, _ = ioutil.ReadFile(s.d.logFile.Name()) + if !strings.Contains(string(content), "....") { + c.Fatalf("Info level should have ....\n%s", string(content)) + } +} + +func (s *DockerDaemonSuite) TestDaemonUnixSockCleanedUp(c *check.C) { + dir, err := ioutil.TempDir("", "socket-cleanup-test") + if err != nil { + c.Fatal(err) + } + defer os.RemoveAll(dir) + + sockPath := filepath.Join(dir, "docker.sock") + if err := s.d.Start("--host", "unix://"+sockPath); err != nil { + c.Fatal(err) + } + + if _, err := os.Stat(sockPath); err != nil { + c.Fatal("socket does not exist") + } + + if err := s.d.Stop(); err != nil { + c.Fatal(err) + } + + if _, err := os.Stat(sockPath); err == nil || !os.IsNotExist(err) { + c.Fatal("unix socket is not cleaned up") + } +} + +func (s *DockerDaemonSuite) TestDaemonWithWrongkey(c *check.C) { + type Config struct { + Crv string `json:"crv"` + D string `json:"d"` + Kid string `json:"kid"` + Kty string `json:"kty"` + X string `json:"x"` + Y string `json:"y"` + } + + os.Remove("/etc/docker/key.json") + if err := s.d.Start(); err != nil { + c.Fatalf("Failed to start daemon: %v", err) + } + + if err := s.d.Stop(); err != nil { + c.Fatalf("Could not stop daemon: %v", err) + } + + config := &Config{} + bytes, err := ioutil.ReadFile("/etc/docker/key.json") + if err != nil { + c.Fatalf("Error reading key.json file: %s", err) + } + + // byte[] to Data-Struct + if err := json.Unmarshal(bytes, &config); err != nil { + c.Fatalf("Error Unmarshal: %s", err) + } + + //replace config.Kid with the fake value + config.Kid = "VSAJ:FUYR:X3H2:B2VZ:KZ6U:CJD5:K7BX:ZXHY:UZXT:P4FT:MJWG:HRJ4" + + // NEW Data-Struct to byte[] + newBytes, err := json.Marshal(&config) + if err != nil { + c.Fatalf("Error Marshal: %s", err) + } + + // write back + if err := ioutil.WriteFile("/etc/docker/key.json", newBytes, 0400); err != nil { + c.Fatalf("Error ioutil.WriteFile: %s", err) + } + + defer os.Remove("/etc/docker/key.json") + + if err := s.d.Start(); err == nil { + c.Fatalf("It should not be successful to start daemon with wrong key: %v", err) + } + + content, _ := ioutil.ReadFile(s.d.logFile.Name()) + + if !strings.Contains(string(content), "Public Key ID does not match") { + c.Fatal("Missing KeyID message from daemon logs") + } +} + +func (s *DockerDaemonSuite) TestDaemonRestartKillWait(c *check.C) { + if err := s.d.StartWithBusybox(); err != nil { + c.Fatalf("Could not start daemon with busybox: %v", err) + } + + out, err := s.d.Cmd("run", "-id", "busybox", "/bin/cat") + if err != nil { + c.Fatalf("Could not run /bin/cat: err=%v\n%s", err, out) + } + containerID := strings.TrimSpace(out) + + if out, err := s.d.Cmd("kill", containerID); err != nil { + c.Fatalf("Could not kill %s: err=%v\n%s", containerID, err, out) + } + + if err := s.d.Restart(); err != nil { + c.Fatalf("Could not restart daemon: %v", err) + } + + errchan := make(chan error) + go func() { + if out, err := s.d.Cmd("wait", containerID); err != nil { + errchan <- fmt.Errorf("%v:\n%s", err, out) + } + close(errchan) + }() + + select { + case <-time.After(5 * time.Second): + c.Fatal("Waiting on a stopped (killed) container timed out") + case err := <-errchan: + if err != nil { + c.Fatal(err) + } + } +} + +// TestHttpsInfo connects via two-way authenticated HTTPS to the info endpoint +func (s *DockerDaemonSuite) TestHttpsInfo(c *check.C) { + const ( + testDaemonHTTPSAddr = "tcp://localhost:4271" + ) + + if err := s.d.Start("--tlsverify", "--tlscacert", "fixtures/https/ca.pem", "--tlscert", "fixtures/https/server-cert.pem", + "--tlskey", "fixtures/https/server-key.pem", "-H", testDaemonHTTPSAddr); err != nil { + c.Fatalf("Could not start daemon with busybox: %v", err) + } + + daemonArgs := []string{"--host", testDaemonHTTPSAddr, "--tlsverify", "--tlscacert", "fixtures/https/ca.pem", "--tlscert", "fixtures/https/client-cert.pem", "--tlskey", "fixtures/https/client-key.pem"} + out, err := s.d.CmdWithArgs(daemonArgs, "info") + if err != nil { + c.Fatalf("Error Occurred: %s and output: %s", err, out) + } +} + +// TestHttpsRun connects via two-way authenticated HTTPS to the create, attach, start, and wait endpoints. +// https://github.com/docker/docker/issues/19280 +func (s *DockerDaemonSuite) TestHttpsRun(c *check.C) { + const ( + testDaemonHTTPSAddr = "tcp://localhost:4271" + ) + + if err := s.d.StartWithBusybox("--tlsverify", "--tlscacert", "fixtures/https/ca.pem", "--tlscert", "fixtures/https/server-cert.pem", + "--tlskey", "fixtures/https/server-key.pem", "-H", testDaemonHTTPSAddr); err != nil { + c.Fatalf("Could not start daemon with busybox: %v", err) + } + + daemonArgs := []string{"--host", testDaemonHTTPSAddr, "--tlsverify", "--tlscacert", "fixtures/https/ca.pem", "--tlscert", "fixtures/https/client-cert.pem", "--tlskey", "fixtures/https/client-key.pem"} + out, err := s.d.CmdWithArgs(daemonArgs, "run", "busybox", "echo", "TLS response") + if err != nil { + c.Fatalf("Error Occurred: %s and output: %s", err, out) + } + + if !strings.Contains(out, "TLS response") { + c.Fatalf("expected output to include `TLS response`, got %v", out) + } +} + +// TestTlsVerify verifies that --tlsverify=false turns on tls +func (s *DockerDaemonSuite) TestTlsVerify(c *check.C) { + out, err := exec.Command(dockerBinary, "daemon", "--tlsverify=false").CombinedOutput() + if err == nil || !strings.Contains(string(out), "Could not load X509 key pair") { + c.Fatalf("Daemon should not have started due to missing certs: %v\n%s", err, string(out)) + } +} + +// TestHttpsInfoRogueCert connects via two-way authenticated HTTPS to the info endpoint +// by using a rogue client certificate and checks that it fails with the expected error. +func (s *DockerDaemonSuite) TestHttpsInfoRogueCert(c *check.C) { + const ( + errBadCertificate = "remote error: bad certificate" + testDaemonHTTPSAddr = "tcp://localhost:4271" + ) + + if err := s.d.Start("--tlsverify", "--tlscacert", "fixtures/https/ca.pem", "--tlscert", "fixtures/https/server-cert.pem", + "--tlskey", "fixtures/https/server-key.pem", "-H", testDaemonHTTPSAddr); err != nil { + c.Fatalf("Could not start daemon with busybox: %v", err) + } + + daemonArgs := []string{"--host", testDaemonHTTPSAddr, "--tlsverify", "--tlscacert", "fixtures/https/ca.pem", "--tlscert", "fixtures/https/client-rogue-cert.pem", "--tlskey", "fixtures/https/client-rogue-key.pem"} + out, err := s.d.CmdWithArgs(daemonArgs, "info") + if err == nil || !strings.Contains(out, errBadCertificate) { + c.Fatalf("Expected err: %s, got instead: %s and output: %s", errBadCertificate, err, out) + } +} + +// TestHttpsInfoRogueServerCert connects via two-way authenticated HTTPS to the info endpoint +// which provides a rogue server certificate and checks that it fails with the expected error +func (s *DockerDaemonSuite) TestHttpsInfoRogueServerCert(c *check.C) { + const ( + errCaUnknown = "x509: certificate signed by unknown authority" + testDaemonRogueHTTPSAddr = "tcp://localhost:4272" + ) + if err := s.d.Start("--tlsverify", "--tlscacert", "fixtures/https/ca.pem", "--tlscert", "fixtures/https/server-rogue-cert.pem", + "--tlskey", "fixtures/https/server-rogue-key.pem", "-H", testDaemonRogueHTTPSAddr); err != nil { + c.Fatalf("Could not start daemon with busybox: %v", err) + } + + daemonArgs := []string{"--host", testDaemonRogueHTTPSAddr, "--tlsverify", "--tlscacert", "fixtures/https/ca.pem", "--tlscert", "fixtures/https/client-rogue-cert.pem", "--tlskey", "fixtures/https/client-rogue-key.pem"} + out, err := s.d.CmdWithArgs(daemonArgs, "info") + if err == nil || !strings.Contains(out, errCaUnknown) { + c.Fatalf("Expected err: %s, got instead: %s and output: %s", errCaUnknown, err, out) + } +} + +func pingContainers(c *check.C, d *Daemon, expectFailure bool) { + var dargs []string + if d != nil { + dargs = []string{"--host", d.sock()} + } + + args := append(dargs, "run", "-d", "--name", "container1", "busybox", "top") + dockerCmd(c, args...) + + args = append(dargs, "run", "--rm", "--link", "container1:alias1", "busybox", "sh", "-c") + pingCmd := "ping -c 1 %s -W 1" + args = append(args, fmt.Sprintf(pingCmd, "alias1")) + _, _, err := dockerCmdWithError(args...) + + if expectFailure { + c.Assert(err, check.NotNil) + } else { + c.Assert(err, check.IsNil) + } + + args = append(dargs, "rm", "-f", "container1") + dockerCmd(c, args...) +} + +func (s *DockerDaemonSuite) TestDaemonRestartWithSocketAsVolume(c *check.C) { + c.Assert(s.d.StartWithBusybox(), check.IsNil) + + socket := filepath.Join(s.d.folder, "docker.sock") + + out, err := s.d.Cmd("run", "--restart=always", "-v", socket+":/sock", "busybox") + c.Assert(err, check.IsNil, check.Commentf("Output: %s", out)) + c.Assert(s.d.Restart(), check.IsNil) +} + +// os.Kill should kill daemon ungracefully, leaving behind container mounts. +// A subsequent daemon restart shoud clean up said mounts. +func (s *DockerDaemonSuite) TestCleanupMountsAfterDaemonAndContainerKill(c *check.C) { + c.Assert(s.d.StartWithBusybox(), check.IsNil) + + out, err := s.d.Cmd("run", "-d", "busybox", "top") + c.Assert(err, check.IsNil, check.Commentf("Output: %s", out)) + id := strings.TrimSpace(out) + c.Assert(s.d.cmd.Process.Signal(os.Kill), check.IsNil) + mountOut, err := ioutil.ReadFile("/proc/self/mountinfo") + c.Assert(err, check.IsNil, check.Commentf("Output: %s", mountOut)) + + // container mounts should exist even after daemon has crashed. + comment := check.Commentf("%s should stay mounted from older daemon start:\nDaemon root repository %s\n%s", id, s.d.folder, mountOut) + c.Assert(strings.Contains(string(mountOut), id), check.Equals, true, comment) + + // kill the container + runCmd := exec.Command(ctrBinary, "--address", "/var/run/docker/libcontainerd/docker-containerd.sock", "containers", "kill", id) + if out, ec, err := runCommandWithOutput(runCmd); err != nil { + c.Fatalf("Failed to run ctr, ExitCode: %d, err: %v output: %s id: %s\n", ec, err, out, id) + } + + // restart daemon. + if err := s.d.Restart(); err != nil { + c.Fatal(err) + } + + // Now, container mounts should be gone. + mountOut, err = ioutil.ReadFile("/proc/self/mountinfo") + c.Assert(err, check.IsNil, check.Commentf("Output: %s", mountOut)) + comment = check.Commentf("%s is still mounted from older daemon start:\nDaemon root repository %s\n%s", id, s.d.folder, mountOut) + c.Assert(strings.Contains(string(mountOut), id), check.Equals, false, comment) +} + +// os.Interrupt should perform a graceful daemon shutdown and hence cleanup mounts. +func (s *DockerDaemonSuite) TestCleanupMountsAfterGracefulShutdown(c *check.C) { + c.Assert(s.d.StartWithBusybox(), check.IsNil) + + out, err := s.d.Cmd("run", "-d", "busybox", "top") + c.Assert(err, check.IsNil, check.Commentf("Output: %s", out)) + id := strings.TrimSpace(out) + + // Send SIGINT and daemon should clean up + c.Assert(s.d.cmd.Process.Signal(os.Interrupt), check.IsNil) + + // Wait a bit for the daemon to handle cleanups. + time.Sleep(3 * time.Second) + + mountOut, err := ioutil.ReadFile("/proc/self/mountinfo") + c.Assert(err, check.IsNil, check.Commentf("Output: %s", mountOut)) + + comment := check.Commentf("%s is still mounted from older daemon start:\nDaemon root repository %s\n%s", id, s.d.folder, mountOut) + c.Assert(strings.Contains(string(mountOut), id), check.Equals, false, comment) +} + +func (s *DockerDaemonSuite) TestRunContainerWithBridgeNone(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace) + c.Assert(s.d.StartWithBusybox("-b", "none"), check.IsNil) + + out, err := s.d.Cmd("run", "--rm", "busybox", "ip", "l") + c.Assert(err, check.IsNil, check.Commentf("Output: %s", out)) + c.Assert(strings.Contains(out, "eth0"), check.Equals, false, + check.Commentf("There shouldn't be eth0 in container in default(bridge) mode when bridge network is disabled: %s", out)) + + out, err = s.d.Cmd("run", "--rm", "--net=bridge", "busybox", "ip", "l") + c.Assert(err, check.IsNil, check.Commentf("Output: %s", out)) + c.Assert(strings.Contains(out, "eth0"), check.Equals, false, + check.Commentf("There shouldn't be eth0 in container in bridge mode when bridge network is disabled: %s", out)) + // the extra grep and awk clean up the output of `ip` to only list the number and name of + // interfaces, allowing for different versions of ip (e.g. inside and outside the container) to + // be used while still verifying that the interface list is the exact same + cmd := exec.Command("sh", "-c", "ip l | grep -E '^[0-9]+:' | awk -F: ' { print $1\":\"$2 } '") + stdout := bytes.NewBuffer(nil) + cmd.Stdout = stdout + if err := cmd.Run(); err != nil { + c.Fatal("Failed to get host network interface") + } + out, err = s.d.Cmd("run", "--rm", "--net=host", "busybox", "sh", "-c", "ip l | grep -E '^[0-9]+:' | awk -F: ' { print $1\":\"$2 } '") + c.Assert(err, check.IsNil, check.Commentf("Output: %s", out)) + c.Assert(out, check.Equals, fmt.Sprintf("%s", stdout), + check.Commentf("The network interfaces in container should be the same with host when --net=host when bridge network is disabled: %s", out)) +} + +func (s *DockerDaemonSuite) TestDaemonRestartWithContainerRunning(t *check.C) { + if err := s.d.StartWithBusybox(); err != nil { + t.Fatal(err) + } + if out, err := s.d.Cmd("run", "-d", "--name", "test", "busybox", "top"); err != nil { + t.Fatal(out, err) + } + + if err := s.d.Restart(); err != nil { + t.Fatal(err) + } + // Container 'test' should be removed without error + if out, err := s.d.Cmd("rm", "test"); err != nil { + t.Fatal(out, err) + } +} + +func (s *DockerDaemonSuite) TestDaemonRestartCleanupNetns(c *check.C) { + if err := s.d.StartWithBusybox(); err != nil { + c.Fatal(err) + } + out, err := s.d.Cmd("run", "--name", "netns", "-d", "busybox", "top") + if err != nil { + c.Fatal(out, err) + } + + // Get sandbox key via inspect + out, err = s.d.Cmd("inspect", "--format", "'{{.NetworkSettings.SandboxKey}}'", "netns") + if err != nil { + c.Fatalf("Error inspecting container: %s, %v", out, err) + } + fileName := strings.Trim(out, " \r\n'") + + if out, err := s.d.Cmd("stop", "netns"); err != nil { + c.Fatal(out, err) + } + + // Test if the file still exists + out, _, err = runCommandWithOutput(exec.Command("stat", "-c", "%n", fileName)) + out = strings.TrimSpace(out) + c.Assert(err, check.IsNil, check.Commentf("Output: %s", out)) + c.Assert(out, check.Equals, fileName, check.Commentf("Output: %s", out)) + + // Remove the container and restart the daemon + if out, err := s.d.Cmd("rm", "netns"); err != nil { + c.Fatal(out, err) + } + + if err := s.d.Restart(); err != nil { + c.Fatal(err) + } + + // Test again and see now the netns file does not exist + out, _, err = runCommandWithOutput(exec.Command("stat", "-c", "%n", fileName)) + out = strings.TrimSpace(out) + c.Assert(err, check.Not(check.IsNil), check.Commentf("Output: %s", out)) +} + +// tests regression detailed in #13964 where DOCKER_TLS_VERIFY env is ignored +func (s *DockerDaemonSuite) TestDaemonNoTlsCliTlsVerifyWithEnv(c *check.C) { + host := "tcp://localhost:4271" + c.Assert(s.d.Start("-H", host), check.IsNil) + cmd := exec.Command(dockerBinary, "-H", host, "info") + cmd.Env = []string{"DOCKER_TLS_VERIFY=1", "DOCKER_CERT_PATH=fixtures/https"} + out, _, err := runCommandWithOutput(cmd) + c.Assert(err, check.Not(check.IsNil), check.Commentf("%s", out)) + c.Assert(strings.Contains(out, "error occurred trying to connect"), check.Equals, true) + +} + +func setupV6() error { + // Hack to get the right IPv6 address on docker0, which has already been created + return exec.Command("ip", "addr", "add", "fe80::1/64", "dev", "docker0").Run() +} + +func teardownV6() error { + return exec.Command("ip", "addr", "del", "fe80::1/64", "dev", "docker0").Run() +} + +func (s *DockerDaemonSuite) TestDaemonRestartWithContainerWithRestartPolicyAlways(c *check.C) { + c.Assert(s.d.StartWithBusybox(), check.IsNil) + + out, err := s.d.Cmd("run", "-d", "--restart", "always", "busybox", "top") + c.Assert(err, check.IsNil) + id := strings.TrimSpace(out) + + _, err = s.d.Cmd("stop", id) + c.Assert(err, check.IsNil) + _, err = s.d.Cmd("wait", id) + c.Assert(err, check.IsNil) + + out, err = s.d.Cmd("ps", "-q") + c.Assert(err, check.IsNil) + c.Assert(out, check.Equals, "") + + c.Assert(s.d.Restart(), check.IsNil) + + out, err = s.d.Cmd("ps", "-q") + c.Assert(err, check.IsNil) + c.Assert(strings.TrimSpace(out), check.Equals, id[:12]) +} + +func (s *DockerDaemonSuite) TestDaemonWideLogConfig(c *check.C) { + if err := s.d.StartWithBusybox("--log-driver=json-file", "--log-opt=max-size=1k"); err != nil { + c.Fatal(err) + } + out, err := s.d.Cmd("run", "-d", "--name=logtest", "busybox", "top") + c.Assert(err, check.IsNil, check.Commentf("Output: %s, err: %v", out, err)) + out, err = s.d.Cmd("inspect", "-f", "{{ .HostConfig.LogConfig.Config }}", "logtest") + c.Assert(err, check.IsNil, check.Commentf("Output: %s", out)) + cfg := strings.TrimSpace(out) + if cfg != "map[max-size:1k]" { + c.Fatalf("Unexpected log-opt: %s, expected map[max-size:1k]", cfg) + } +} + +func (s *DockerDaemonSuite) TestDaemonRestartWithPausedContainer(c *check.C) { + if err := s.d.StartWithBusybox(); err != nil { + c.Fatal(err) + } + if out, err := s.d.Cmd("run", "-i", "-d", "--name", "test", "busybox", "top"); err != nil { + c.Fatal(err, out) + } + if out, err := s.d.Cmd("pause", "test"); err != nil { + c.Fatal(err, out) + } + if err := s.d.Restart(); err != nil { + c.Fatal(err) + } + + errchan := make(chan error) + go func() { + out, err := s.d.Cmd("start", "test") + if err != nil { + errchan <- fmt.Errorf("%v:\n%s", err, out) + } + name := strings.TrimSpace(out) + if name != "test" { + errchan <- fmt.Errorf("Paused container start error on docker daemon restart, expected 'test' but got '%s'", name) + } + close(errchan) + }() + + select { + case <-time.After(5 * time.Second): + c.Fatal("Waiting on start a container timed out") + case err := <-errchan: + if err != nil { + c.Fatal(err) + } + } +} + +func (s *DockerDaemonSuite) TestDaemonRestartRmVolumeInUse(c *check.C) { + c.Assert(s.d.StartWithBusybox(), check.IsNil) + + out, err := s.d.Cmd("create", "-v", "test:/foo", "busybox") + c.Assert(err, check.IsNil, check.Commentf(out)) + + c.Assert(s.d.Restart(), check.IsNil) + + out, err = s.d.Cmd("volume", "rm", "test") + c.Assert(err, check.NotNil, check.Commentf("should not be able to remove in use volume after daemon restart")) + c.Assert(out, checker.Contains, "in use") +} + +func (s *DockerDaemonSuite) TestDaemonRestartLocalVolumes(c *check.C) { + c.Assert(s.d.Start(), check.IsNil) + + _, err := s.d.Cmd("volume", "create", "--name", "test") + c.Assert(err, check.IsNil) + c.Assert(s.d.Restart(), check.IsNil) + + _, err = s.d.Cmd("volume", "inspect", "test") + c.Assert(err, check.IsNil) +} + +func (s *DockerDaemonSuite) TestDaemonCorruptedLogDriverAddress(c *check.C) { + c.Assert(s.d.Start("--log-driver=syslog", "--log-opt", "syslog-address=corrupted:42"), check.NotNil) + expected := "Failed to set log opts: syslog-address should be in form proto://address" + runCmd := exec.Command("grep", expected, s.d.LogFileName()) + if out, _, err := runCommandWithOutput(runCmd); err != nil { + c.Fatalf("Expected %q message; but doesn't exist in log: %q, err: %v", expected, out, err) + } +} + +func (s *DockerDaemonSuite) TestDaemonCorruptedFluentdAddress(c *check.C) { + c.Assert(s.d.Start("--log-driver=fluentd", "--log-opt", "fluentd-address=corrupted:c"), check.NotNil) + expected := "Failed to set log opts: invalid fluentd-address corrupted:c: " + runCmd := exec.Command("grep", expected, s.d.LogFileName()) + if out, _, err := runCommandWithOutput(runCmd); err != nil { + c.Fatalf("Expected %q message; but doesn't exist in log: %q, err: %v", expected, out, err) + } +} + +func (s *DockerDaemonSuite) TestDaemonStartWithoutHost(c *check.C) { + s.d.useDefaultHost = true + defer func() { + s.d.useDefaultHost = false + }() + c.Assert(s.d.Start(), check.IsNil) +} + +func (s *DockerDaemonSuite) TestDaemonStartWithDefalutTlsHost(c *check.C) { + s.d.useDefaultTLSHost = true + defer func() { + s.d.useDefaultTLSHost = false + }() + if err := s.d.Start( + "--tlsverify", + "--tlscacert", "fixtures/https/ca.pem", + "--tlscert", "fixtures/https/server-cert.pem", + "--tlskey", "fixtures/https/server-key.pem"); err != nil { + c.Fatalf("Could not start daemon: %v", err) + } + + // The client with --tlsverify should also use default host localhost:2376 + tmpHost := os.Getenv("DOCKER_HOST") + defer func() { + os.Setenv("DOCKER_HOST", tmpHost) + }() + + os.Setenv("DOCKER_HOST", "") + + out, _ := dockerCmd( + c, + "--tlsverify", + "--tlscacert", "fixtures/https/ca.pem", + "--tlscert", "fixtures/https/client-cert.pem", + "--tlskey", "fixtures/https/client-key.pem", + "version", + ) + if !strings.Contains(out, "Server") { + c.Fatalf("docker version should return information of server side") + } +} + +func (s *DockerDaemonSuite) TestBridgeIPIsExcludedFromAllocatorPool(c *check.C) { + defaultNetworkBridge := "docker0" + deleteInterface(c, defaultNetworkBridge) + + bridgeIP := "192.169.1.1" + bridgeRange := bridgeIP + "/30" + + err := s.d.StartWithBusybox("--bip", bridgeRange) + c.Assert(err, check.IsNil) + defer s.d.Restart() + + var cont int + for { + contName := fmt.Sprintf("container%d", cont) + _, err = s.d.Cmd("run", "--name", contName, "-d", "busybox", "/bin/sleep", "2") + if err != nil { + // pool exhausted + break + } + ip, err := s.d.Cmd("inspect", "--format", "'{{.NetworkSettings.IPAddress}}'", contName) + c.Assert(err, check.IsNil) + + c.Assert(ip, check.Not(check.Equals), bridgeIP) + cont++ + } +} + +// Test daemon for no space left on device error +func (s *DockerDaemonSuite) TestDaemonNoSpaceLeftOnDeviceError(c *check.C) { + testRequires(c, SameHostDaemon, DaemonIsLinux, Network) + + testDir, err := ioutil.TempDir("", "no-space-left-on-device-test") + c.Assert(err, checker.IsNil) + defer os.RemoveAll(testDir) + c.Assert(mount.MakeRShared(testDir), checker.IsNil) + defer mount.Unmount(testDir) + defer mount.Unmount(filepath.Join(testDir, "test-mount")) + + // create a 2MiB image and mount it as graph root + // Why in a container? Because `mount` sometimes behaves weirdly and often fails outright on this test in debian:jessie (which is what the test suite runs under if run from the Makefile) + dockerCmd(c, "run", "--rm", "-v", testDir+":/test", "busybox", "sh", "-c", "dd of=/test/testfs.img bs=1M seek=2 count=0") + out, _, err := runCommandWithOutput(exec.Command("mkfs.ext4", "-F", filepath.Join(testDir, "testfs.img"))) // `mkfs.ext4` is not in busybox + c.Assert(err, checker.IsNil, check.Commentf(out)) + dockerCmd(c, "run", "--privileged", "--rm", "-v", testDir+":/test:shared", "busybox", "sh", "-c", "mkdir -p /test/test-mount && mount -t ext4 -no loop,rw /test/testfs.img /test/test-mount") + + err = s.d.Start("--graph", filepath.Join(testDir, "test-mount")) + c.Assert(err, check.IsNil) + + // pull a repository large enough to fill the mount point + out, err = s.d.Cmd("pull", "registry:2") + c.Assert(out, checker.Contains, "no space left on device") +} + +// Test daemon restart with container links + auto restart +func (s *DockerDaemonSuite) TestDaemonRestartContainerLinksRestart(c *check.C) { + d := NewDaemon(c) + defer d.Stop() + err := d.StartWithBusybox() + c.Assert(err, checker.IsNil) + + parent1Args := []string{} + parent2Args := []string{} + wg := sync.WaitGroup{} + maxChildren := 10 + chErr := make(chan error, maxChildren) + + for i := 0; i < maxChildren; i++ { + wg.Add(1) + name := fmt.Sprintf("test%d", i) + + if i < maxChildren/2 { + parent1Args = append(parent1Args, []string{"--link", name}...) + } else { + parent2Args = append(parent2Args, []string{"--link", name}...) + } + + go func() { + _, err = d.Cmd("run", "-d", "--name", name, "--restart=always", "busybox", "top") + chErr <- err + wg.Done() + }() + } + + wg.Wait() + close(chErr) + for err := range chErr { + c.Assert(err, check.IsNil) + } + + parent1Args = append([]string{"run", "-d"}, parent1Args...) + parent1Args = append(parent1Args, []string{"--name=parent1", "--restart=always", "busybox", "top"}...) + parent2Args = append([]string{"run", "-d"}, parent2Args...) + parent2Args = append(parent2Args, []string{"--name=parent2", "--restart=always", "busybox", "top"}...) + + _, err = d.Cmd(parent1Args[0], parent1Args[1:]...) + c.Assert(err, check.IsNil) + _, err = d.Cmd(parent2Args[0], parent2Args[1:]...) + c.Assert(err, check.IsNil) + + err = d.Stop() + c.Assert(err, check.IsNil) + // clear the log file -- we don't need any of it but may for the next part + // can ignore the error here, this is just a cleanup + os.Truncate(d.LogFileName(), 0) + err = d.Start() + c.Assert(err, check.IsNil) + + for _, num := range []string{"1", "2"} { + out, err := d.Cmd("inspect", "-f", "{{ .State.Running }}", "parent"+num) + c.Assert(err, check.IsNil) + if strings.TrimSpace(out) != "true" { + log, _ := ioutil.ReadFile(d.LogFileName()) + c.Fatalf("parent container is not running\n%s", string(log)) + } + } +} + +func (s *DockerDaemonSuite) TestDaemonCgroupParent(c *check.C) { + testRequires(c, DaemonIsLinux) + + cgroupParent := "test" + name := "cgroup-test" + + err := s.d.StartWithBusybox("--cgroup-parent", cgroupParent) + c.Assert(err, check.IsNil) + defer s.d.Restart() + + out, err := s.d.Cmd("run", "--name", name, "busybox", "cat", "/proc/self/cgroup") + c.Assert(err, checker.IsNil) + cgroupPaths := parseCgroupPaths(string(out)) + c.Assert(len(cgroupPaths), checker.Not(checker.Equals), 0, check.Commentf("unexpected output - %q", string(out))) + out, err = s.d.Cmd("inspect", "-f", "{{.Id}}", name) + c.Assert(err, checker.IsNil) + id := strings.TrimSpace(string(out)) + expectedCgroup := path.Join(cgroupParent, id) + found := false + for _, path := range cgroupPaths { + if strings.HasSuffix(path, expectedCgroup) { + found = true + break + } + } + c.Assert(found, checker.True, check.Commentf("Cgroup path for container (%s) doesn't found in cgroups file: %s", expectedCgroup, cgroupPaths)) +} + +func (s *DockerDaemonSuite) TestDaemonRestartWithLinks(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows does not support links + err := s.d.StartWithBusybox() + c.Assert(err, check.IsNil) + + out, err := s.d.Cmd("run", "-d", "--name=test", "busybox", "top") + c.Assert(err, check.IsNil, check.Commentf(out)) + + out, err = s.d.Cmd("run", "--name=test2", "--link", "test:abc", "busybox", "sh", "-c", "ping -c 1 -w 1 abc") + c.Assert(err, check.IsNil, check.Commentf(out)) + + c.Assert(s.d.Restart(), check.IsNil) + + // should fail since test is not running yet + out, err = s.d.Cmd("start", "test2") + c.Assert(err, check.NotNil, check.Commentf(out)) + + out, err = s.d.Cmd("start", "test") + c.Assert(err, check.IsNil, check.Commentf(out)) + out, err = s.d.Cmd("start", "-a", "test2") + c.Assert(err, check.IsNil, check.Commentf(out)) + c.Assert(strings.Contains(out, "1 packets transmitted, 1 packets received"), check.Equals, true, check.Commentf(out)) +} + +func (s *DockerDaemonSuite) TestDaemonRestartWithNames(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows does not support links + err := s.d.StartWithBusybox() + c.Assert(err, check.IsNil) + + out, err := s.d.Cmd("create", "--name=test", "busybox") + c.Assert(err, check.IsNil, check.Commentf(out)) + + out, err = s.d.Cmd("run", "-d", "--name=test2", "busybox", "top") + c.Assert(err, check.IsNil, check.Commentf(out)) + test2ID := strings.TrimSpace(out) + + out, err = s.d.Cmd("run", "-d", "--name=test3", "--link", "test2:abc", "busybox", "top") + test3ID := strings.TrimSpace(out) + + c.Assert(s.d.Restart(), check.IsNil) + + out, err = s.d.Cmd("create", "--name=test", "busybox") + c.Assert(err, check.NotNil, check.Commentf("expected error trying to create container with duplicate name")) + // this one is no longer needed, removing simplifies the remainder of the test + out, err = s.d.Cmd("rm", "-f", "test") + c.Assert(err, check.IsNil, check.Commentf(out)) + + out, err = s.d.Cmd("ps", "-a", "--no-trunc") + c.Assert(err, check.IsNil, check.Commentf(out)) + + lines := strings.Split(strings.TrimSpace(out), "\n")[1:] + + test2validated := false + test3validated := false + for _, line := range lines { + fields := strings.Fields(line) + names := fields[len(fields)-1] + switch fields[0] { + case test2ID: + c.Assert(names, check.Equals, "test2,test3/abc") + test2validated = true + case test3ID: + c.Assert(names, check.Equals, "test3") + test3validated = true + } + } + + c.Assert(test2validated, check.Equals, true) + c.Assert(test3validated, check.Equals, true) +} + +// TestRunLinksChanged checks that creating a new container with the same name does not update links +// this ensures that the old, pre gh#16032 functionality continues on +func (s *DockerDaemonSuite) TestRunLinksChanged(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows does not support links + err := s.d.StartWithBusybox() + c.Assert(err, check.IsNil) + + out, err := s.d.Cmd("run", "-d", "--name=test", "busybox", "top") + c.Assert(err, check.IsNil, check.Commentf(out)) + + out, err = s.d.Cmd("run", "--name=test2", "--link=test:abc", "busybox", "sh", "-c", "ping -c 1 abc") + c.Assert(err, check.IsNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "1 packets transmitted, 1 packets received") + + out, err = s.d.Cmd("rm", "-f", "test") + c.Assert(err, check.IsNil, check.Commentf(out)) + + out, err = s.d.Cmd("run", "-d", "--name=test", "busybox", "top") + c.Assert(err, check.IsNil, check.Commentf(out)) + out, err = s.d.Cmd("start", "-a", "test2") + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(out, check.Not(checker.Contains), "1 packets transmitted, 1 packets received") + + err = s.d.Restart() + c.Assert(err, check.IsNil) + out, err = s.d.Cmd("start", "-a", "test2") + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(out, check.Not(checker.Contains), "1 packets transmitted, 1 packets received") +} + +func (s *DockerDaemonSuite) TestDaemonStartWithoutColors(c *check.C) { + testRequires(c, DaemonIsLinux, NotPpc64le) + newD := NewDaemon(c) + + infoLog := "\x1b[34mINFO\x1b" + + p, tty, err := pty.Open() + c.Assert(err, checker.IsNil) + defer func() { + tty.Close() + p.Close() + }() + + b := bytes.NewBuffer(nil) + go io.Copy(b, p) + + // Enable coloring explicitly + newD.StartWithLogFile(tty, "--raw-logs=false") + newD.Stop() + c.Assert(b.String(), checker.Contains, infoLog) + + b.Reset() + + // Disable coloring explicitly + newD.StartWithLogFile(tty, "--raw-logs=true") + newD.Stop() + c.Assert(b.String(), check.Not(checker.Contains), infoLog) +} + +func (s *DockerDaemonSuite) TestDaemonDebugLog(c *check.C) { + testRequires(c, DaemonIsLinux, NotPpc64le) + newD := NewDaemon(c) + + debugLog := "\x1b[37mDEBU\x1b" + + p, tty, err := pty.Open() + c.Assert(err, checker.IsNil) + defer func() { + tty.Close() + p.Close() + }() + + b := bytes.NewBuffer(nil) + go io.Copy(b, p) + + newD.StartWithLogFile(tty, "--debug") + newD.Stop() + c.Assert(b.String(), checker.Contains, debugLog) +} + +func (s *DockerSuite) TestDaemonDiscoveryBackendConfigReload(c *check.C) { + testRequires(c, SameHostDaemon, DaemonIsLinux) + + // daemon config file + daemonConfig := `{ "debug" : false }` + configFilePath := "test.json" + + configFile, err := os.Create(configFilePath) + c.Assert(err, checker.IsNil) + fmt.Fprintf(configFile, "%s", daemonConfig) + + d := NewDaemon(c) + err = d.Start(fmt.Sprintf("--config-file=%s", configFilePath)) + c.Assert(err, checker.IsNil) + defer d.Stop() + + // daemon config file + daemonConfig = `{ + "cluster-store": "consul://consuladdr:consulport/some/path", + "cluster-advertise": "192.168.56.100:0", + "debug" : false + }` + + configFile.Close() + os.Remove(configFilePath) + + configFile, err = os.Create(configFilePath) + c.Assert(err, checker.IsNil) + defer os.Remove(configFilePath) + fmt.Fprintf(configFile, "%s", daemonConfig) + configFile.Close() + + syscall.Kill(d.cmd.Process.Pid, syscall.SIGHUP) + + time.Sleep(3 * time.Second) + + out, err := d.Cmd("info") + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Contains, fmt.Sprintf("Cluster store: consul://consuladdr:consulport/some/path")) + c.Assert(out, checker.Contains, fmt.Sprintf("Cluster advertise: 192.168.56.100:0")) +} diff --git a/integration-cli/docker_cli_diff_test.go b/integration-cli/docker_cli_diff_test.go new file mode 100644 index 00000000..e0d0914f --- /dev/null +++ b/integration-cli/docker_cli_diff_test.go @@ -0,0 +1,87 @@ +package main + +import ( + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +// ensure that an added file shows up in docker diff +func (s *DockerSuite) TestDiffFilenameShownInOutput(c *check.C) { + testRequires(c, DaemonIsLinux) + containerCmd := `echo foo > /root/bar` + out, _ := dockerCmd(c, "run", "-d", "busybox", "sh", "-c", containerCmd) + + cleanCID := strings.TrimSpace(out) + out, _ = dockerCmd(c, "diff", cleanCID) + + found := false + for _, line := range strings.Split(out, "\n") { + if strings.Contains("A /root/bar", line) { + found = true + break + } + } + c.Assert(found, checker.True) +} + +// test to ensure GH #3840 doesn't occur any more +func (s *DockerSuite) TestDiffEnsureInitLayerFilesAreIgnored(c *check.C) { + testRequires(c, DaemonIsLinux) + // this is a list of files which shouldn't show up in `docker diff` + initLayerFiles := []string{"/etc/resolv.conf", "/etc/hostname", "/etc/hosts", "/.dockerenv"} + containerCount := 5 + + // we might not run into this problem from the first run, so start a few containers + for i := 0; i < containerCount; i++ { + containerCmd := `echo foo > /root/bar` + out, _ := dockerCmd(c, "run", "-d", "busybox", "sh", "-c", containerCmd) + + cleanCID := strings.TrimSpace(out) + out, _ = dockerCmd(c, "diff", cleanCID) + + for _, filename := range initLayerFiles { + c.Assert(out, checker.Not(checker.Contains), filename) + } + } +} + +func (s *DockerSuite) TestDiffEnsureOnlyKmsgAndPtmx(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "sleep", "0") + + cleanCID := strings.TrimSpace(out) + out, _ = dockerCmd(c, "diff", cleanCID) + + expected := map[string]bool{ + "C /dev": true, + "A /dev/full": true, // busybox + "C /dev/ptmx": true, // libcontainer + "A /dev/mqueue": true, + "A /dev/kmsg": true, + "A /dev/fd": true, + "A /dev/fuse": true, + "A /dev/ptmx": true, + "A /dev/null": true, + "A /dev/random": true, + "A /dev/stdout": true, + "A /dev/stderr": true, + "A /dev/tty1": true, + "A /dev/stdin": true, + "A /dev/tty": true, + "A /dev/urandom": true, + "A /dev/zero": true, + } + + for _, line := range strings.Split(out, "\n") { + c.Assert(line == "" || expected[line], checker.True) + } +} + +// https://github.com/docker/docker/pull/14381#discussion_r33859347 +func (s *DockerSuite) TestDiffEmptyArgClientError(c *check.C) { + out, _, err := dockerCmdWithError("diff", "") + c.Assert(err, checker.NotNil) + c.Assert(strings.TrimSpace(out), checker.Equals, "Container name cannot be empty") +} diff --git a/integration-cli/docker_cli_events_test.go b/integration-cli/docker_cli_events_test.go new file mode 100644 index 00000000..f17b711c --- /dev/null +++ b/integration-cli/docker_cli_events_test.go @@ -0,0 +1,647 @@ +package main + +import ( + "bufio" + "fmt" + "io/ioutil" + "net/http" + "os" + "os/exec" + "strconv" + "strings" + "sync" + "time" + + "github.com/docker/docker/daemon/events/testutils" + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestEventsTimestampFormats(c *check.C) { + image := "busybox" + + // Start stopwatch, generate an event + time.Sleep(1 * time.Second) // so that we don't grab events from previous test occurred in the same second + start := daemonTime(c) + dockerCmd(c, "tag", image, "timestamptest:1") + dockerCmd(c, "rmi", "timestamptest:1") + time.Sleep(1 * time.Second) // so that until > since + end := daemonTime(c) + + // List of available time formats to --since + unixTs := func(t time.Time) string { return fmt.Sprintf("%v", t.Unix()) } + rfc3339 := func(t time.Time) string { return t.Format(time.RFC3339) } + duration := func(t time.Time) string { return time.Now().Sub(t).String() } + + // --since=$start must contain only the 'untag' event + for _, f := range []func(time.Time) string{unixTs, rfc3339, duration} { + since, until := f(start), f(end) + out, _ := dockerCmd(c, "events", "--since="+since, "--until="+until) + events := strings.Split(strings.TrimSpace(out), "\n") + c.Assert(events, checker.HasLen, 2, check.Commentf("unexpected events, was expecting only 2 events tag/untag (since=%s, until=%s) out=%s", since, until, out)) + c.Assert(out, checker.Contains, "untag", check.Commentf("expected 'untag' event not found (since=%s, until=%s)", since, until)) + } + +} + +func (s *DockerSuite) TestEventsUntag(c *check.C) { + image := "busybox" + dockerCmd(c, "tag", image, "utest:tag1") + dockerCmd(c, "tag", image, "utest:tag2") + dockerCmd(c, "rmi", "utest:tag1") + dockerCmd(c, "rmi", "utest:tag2") + eventsCmd := exec.Command(dockerBinary, "events", "--since=1") + out, exitCode, _, err := runCommandWithOutputForDuration(eventsCmd, time.Duration(time.Millisecond*2500)) + c.Assert(err, checker.IsNil) + c.Assert(exitCode, checker.Equals, 0, check.Commentf("Failed to get events")) + events := strings.Split(out, "\n") + nEvents := len(events) + // The last element after the split above will be an empty string, so we + // get the two elements before the last, which are the untags we're + // looking for. + for _, v := range events[nEvents-3 : nEvents-1] { + c.Assert(v, checker.Contains, "untag", check.Commentf("event should be untag")) + } +} + +func (s *DockerSuite) TestEventsContainerFailStartDie(c *check.C) { + _, _, err := dockerCmdWithError("run", "--name", "testeventdie", "busybox", "blerg") + c.Assert(err, checker.NotNil, check.Commentf("Container run with command blerg should have failed, but it did not")) + + out, _ := dockerCmd(c, "events", "--since=0", fmt.Sprintf("--until=%d", daemonTime(c).Unix())) + events := strings.Split(strings.TrimSpace(out), "\n") + + nEvents := len(events) + c.Assert(nEvents, checker.GreaterOrEqualThan, 1) //Missing expected event + + actions := eventActionsByIDAndType(c, events, "testeventdie", "container") + + var startEvent bool + var dieEvent bool + for _, a := range actions { + switch a { + case "start": + startEvent = true + case "die": + dieEvent = true + } + } + c.Assert(startEvent, checker.True, check.Commentf("Start event not found: %v\n%v", actions, events)) + c.Assert(dieEvent, checker.True, check.Commentf("Die event not found: %v\n%v", actions, events)) +} + +func (s *DockerSuite) TestEventsLimit(c *check.C) { + // TODO Windows CI: This test is not reliable enough on Windows TP4. Reports + // multiple errors in the analytic log sometimes. + // [NetSetupHelper::InstallVirtualMiniport()@2153] NetSetup install of ROOT\VMS_MP\0001 failed with error 0x80070002 + // This should be able to be enabled on TP5. + testRequires(c, DaemonIsLinux) + var waitGroup sync.WaitGroup + errChan := make(chan error, 17) + + args := []string{"run", "--rm", "busybox", "true"} + for i := 0; i < 17; i++ { + waitGroup.Add(1) + go func() { + defer waitGroup.Done() + errChan <- exec.Command(dockerBinary, args...).Run() + }() + } + + waitGroup.Wait() + close(errChan) + + for err := range errChan { + c.Assert(err, checker.IsNil, check.Commentf("%q failed with error", strings.Join(args, " "))) + } + + out, _ := dockerCmd(c, "events", "--since=0", fmt.Sprintf("--until=%d", daemonTime(c).Unix())) + events := strings.Split(out, "\n") + nEvents := len(events) - 1 + c.Assert(nEvents, checker.Equals, 64, check.Commentf("events should be limited to 64, but received %d", nEvents)) +} + +func (s *DockerSuite) TestEventsContainerEvents(c *check.C) { + containerID, _ := dockerCmd(c, "run", "--rm", "--name", "container-events-test", "busybox", "true") + containerID = strings.TrimSpace(containerID) + + out, _ := dockerCmd(c, "events", "--since=0", fmt.Sprintf("--until=%d", daemonTime(c).Unix())) + events := strings.Split(out, "\n") + events = events[:len(events)-1] + + nEvents := len(events) + c.Assert(nEvents, checker.GreaterOrEqualThan, 5) //Missing expected event + containerEvents := eventActionsByIDAndType(c, events, "container-events-test", "container") + c.Assert(containerEvents, checker.HasLen, 5, check.Commentf("events: %v", events)) + + c.Assert(containerEvents[0], checker.Equals, "create", check.Commentf(out)) + c.Assert(containerEvents[1], checker.Equals, "attach", check.Commentf(out)) + c.Assert(containerEvents[2], checker.Equals, "start", check.Commentf(out)) + c.Assert(containerEvents[3], checker.Equals, "die", check.Commentf(out)) + c.Assert(containerEvents[4], checker.Equals, "destroy", check.Commentf(out)) +} + +func (s *DockerSuite) TestEventsContainerEventsAttrSort(c *check.C) { + since := daemonTime(c).Unix() + containerID, _ := dockerCmd(c, "run", "-d", "--name", "container-events-test", "busybox", "true") + containerID = strings.TrimSpace(containerID) + + out, _ := dockerCmd(c, "events", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix())) + events := strings.Split(out, "\n") + + nEvents := len(events) + c.Assert(nEvents, checker.GreaterOrEqualThan, 3) //Missing expected event + matchedEvents := 0 + for _, event := range events { + matches := eventstestutils.ScanMap(event) + if matches["id"] != containerID { + continue + } + if matches["eventType"] == "container" && matches["action"] == "create" { + matchedEvents++ + c.Assert(out, checker.Contains, "(image=busybox, name=container-events-test)", check.Commentf("Event attributes not sorted")) + } else if matches["eventType"] == "container" && matches["action"] == "start" { + matchedEvents++ + c.Assert(out, checker.Contains, "(image=busybox, name=container-events-test)", check.Commentf("Event attributes not sorted")) + } + } + c.Assert(matchedEvents, checker.Equals, 2) +} + +func (s *DockerSuite) TestEventsContainerEventsSinceUnixEpoch(c *check.C) { + dockerCmd(c, "run", "--rm", "--name", "since-epoch-test", "busybox", "true") + timeBeginning := time.Unix(0, 0).Format(time.RFC3339Nano) + timeBeginning = strings.Replace(timeBeginning, "Z", ".000000000Z", -1) + out, _ := dockerCmd(c, "events", fmt.Sprintf("--since='%s'", timeBeginning), fmt.Sprintf("--until=%d", daemonTime(c).Unix())) + events := strings.Split(out, "\n") + events = events[:len(events)-1] + + nEvents := len(events) + c.Assert(nEvents, checker.GreaterOrEqualThan, 5) //Missing expected event + containerEvents := eventActionsByIDAndType(c, events, "since-epoch-test", "container") + c.Assert(containerEvents, checker.HasLen, 5, check.Commentf("events: %v", events)) + + c.Assert(containerEvents[0], checker.Equals, "create", check.Commentf(out)) + c.Assert(containerEvents[1], checker.Equals, "attach", check.Commentf(out)) + c.Assert(containerEvents[2], checker.Equals, "start", check.Commentf(out)) + c.Assert(containerEvents[3], checker.Equals, "die", check.Commentf(out)) + c.Assert(containerEvents[4], checker.Equals, "destroy", check.Commentf(out)) +} + +func (s *DockerSuite) TestEventsImageTag(c *check.C) { + time.Sleep(1 * time.Second) // because API has seconds granularity + since := daemonTime(c).Unix() + image := "testimageevents:tag" + dockerCmd(c, "tag", "busybox", image) + + out, _ := dockerCmd(c, "events", + fmt.Sprintf("--since=%d", since), + fmt.Sprintf("--until=%d", daemonTime(c).Unix())) + + events := strings.Split(strings.TrimSpace(out), "\n") + c.Assert(events, checker.HasLen, 1, check.Commentf("was expecting 1 event. out=%s", out)) + event := strings.TrimSpace(events[0]) + + matches := eventstestutils.ScanMap(event) + c.Assert(matchEventID(matches, image), checker.True, check.Commentf("matches: %v\nout:\n%s", matches, out)) + c.Assert(matches["action"], checker.Equals, "tag") +} + +func (s *DockerSuite) TestEventsImagePull(c *check.C) { + // TODO Windows: Enable this test once pull and reliable image names are available + testRequires(c, DaemonIsLinux) + since := daemonTime(c).Unix() + testRequires(c, Network) + + dockerCmd(c, "pull", "hello-world") + + out, _ := dockerCmd(c, "events", + fmt.Sprintf("--since=%d", since), + fmt.Sprintf("--until=%d", daemonTime(c).Unix())) + + events := strings.Split(strings.TrimSpace(out), "\n") + event := strings.TrimSpace(events[len(events)-1]) + matches := eventstestutils.ScanMap(event) + c.Assert(matches["id"], checker.Equals, "hello-world:latest") + c.Assert(matches["action"], checker.Equals, "pull") + +} + +func (s *DockerSuite) TestEventsImageImport(c *check.C) { + // TODO Windows CI. This should be portable once export/import are + // more reliable (@swernli) + testRequires(c, DaemonIsLinux) + + out, _ := dockerCmd(c, "run", "-d", "busybox", "true") + cleanedContainerID := strings.TrimSpace(out) + + since := daemonTime(c).Unix() + out, _, err := runCommandPipelineWithOutput( + exec.Command(dockerBinary, "export", cleanedContainerID), + exec.Command(dockerBinary, "import", "-"), + ) + c.Assert(err, checker.IsNil, check.Commentf("import failed with output: %q", out)) + imageRef := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "events", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix()), "--filter", "event=import") + events := strings.Split(strings.TrimSpace(out), "\n") + c.Assert(events, checker.HasLen, 1) + matches := eventstestutils.ScanMap(events[0]) + c.Assert(matches["id"], checker.Equals, imageRef, check.Commentf("matches: %v\nout:\n%s\n", matches, out)) + c.Assert(matches["action"], checker.Equals, "import", check.Commentf("matches: %v\nout:\n%s\n", matches, out)) +} + +func (s *DockerSuite) TestEventsFilters(c *check.C) { + since := daemonTime(c).Unix() + dockerCmd(c, "run", "--rm", "busybox", "true") + dockerCmd(c, "run", "--rm", "busybox", "true") + out, _ := dockerCmd(c, "events", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix()), "--filter", "event=die") + parseEvents(c, out, "die") + + out, _ = dockerCmd(c, "events", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix()), "--filter", "event=die", "--filter", "event=start") + parseEvents(c, out, "die|start") + + // make sure we at least got 2 start events + count := strings.Count(out, "start") + c.Assert(strings.Count(out, "start"), checker.GreaterOrEqualThan, 2, check.Commentf("should have had 2 start events but had %d, out: %s", count, out)) + +} + +func (s *DockerSuite) TestEventsFilterImageName(c *check.C) { + since := daemonTime(c).Unix() + + out, _ := dockerCmd(c, "run", "--name", "container_1", "-d", "busybox:latest", "true") + container1 := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "run", "--name", "container_2", "-d", "busybox", "true") + container2 := strings.TrimSpace(out) + + name := "busybox" + out, _ = dockerCmd(c, "events", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix()), "--filter", fmt.Sprintf("image=%s", name)) + events := strings.Split(out, "\n") + events = events[:len(events)-1] + c.Assert(events, checker.Not(checker.HasLen), 0) //Expected events but found none for the image busybox:latest + count1 := 0 + count2 := 0 + + for _, e := range events { + if strings.Contains(e, container1) { + count1++ + } else if strings.Contains(e, container2) { + count2++ + } + } + c.Assert(count1, checker.Not(checker.Equals), 0, check.Commentf("Expected event from container but got %d from %s", count1, container1)) + c.Assert(count2, checker.Not(checker.Equals), 0, check.Commentf("Expected event from container but got %d from %s", count2, container2)) + +} + +func (s *DockerSuite) TestEventsFilterLabels(c *check.C) { + since := daemonTime(c).Unix() + label := "io.docker.testing=foo" + + out, _ := dockerCmd(c, "run", "-d", "-l", label, "busybox:latest", "true") + container1 := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "run", "-d", "busybox", "true") + container2 := strings.TrimSpace(out) + + out, _ = dockerCmd( + c, + "events", + fmt.Sprintf("--since=%d", since), + fmt.Sprintf("--until=%d", daemonTime(c).Unix()), + "--filter", fmt.Sprintf("label=%s", label)) + + events := strings.Split(strings.TrimSpace(out), "\n") + c.Assert(len(events), checker.Equals, 3) + + for _, e := range events { + c.Assert(e, checker.Contains, container1) + c.Assert(e, checker.Not(checker.Contains), container2) + } +} + +func (s *DockerSuite) TestEventsFilterImageLabels(c *check.C) { + since := daemonTime(c).Unix() + name := "labelfiltertest" + label := "io.docker.testing=image" + + // Build a test image. + _, err := buildImage(name, fmt.Sprintf(` + FROM busybox:latest + LABEL %s`, label), true) + c.Assert(err, checker.IsNil, check.Commentf("Couldn't create image")) + + dockerCmd(c, "tag", name, "labelfiltertest:tag1") + dockerCmd(c, "tag", name, "labelfiltertest:tag2") + dockerCmd(c, "tag", "busybox:latest", "labelfiltertest:tag3") + + out, _ := dockerCmd( + c, + "events", + fmt.Sprintf("--since=%d", since), + fmt.Sprintf("--until=%d", daemonTime(c).Unix()), + "--filter", fmt.Sprintf("label=%s", label), + "--filter", "type=image") + + events := strings.Split(strings.TrimSpace(out), "\n") + + // 2 events from the "docker tag" command, another one is from "docker build" + c.Assert(events, checker.HasLen, 3, check.Commentf("Events == %s", events)) + for _, e := range events { + c.Assert(e, checker.Contains, "labelfiltertest") + } +} + +func (s *DockerSuite) TestEventsFilterContainer(c *check.C) { + since := fmt.Sprintf("%d", daemonTime(c).Unix()) + nameID := make(map[string]string) + + for _, name := range []string{"container_1", "container_2"} { + dockerCmd(c, "run", "--name", name, "busybox", "true") + id := inspectField(c, name, "Id") + nameID[name] = id + } + + until := fmt.Sprintf("%d", daemonTime(c).Unix()) + + checkEvents := func(id string, events []string) error { + if len(events) != 4 { // create, attach, start, die + return fmt.Errorf("expected 4 events, got %v", events) + } + for _, event := range events { + matches := eventstestutils.ScanMap(event) + if !matchEventID(matches, id) { + return fmt.Errorf("expected event for container id %s: %s - parsed container id: %s", id, event, matches["id"]) + } + } + return nil + } + + for name, ID := range nameID { + // filter by names + out, _ := dockerCmd(c, "events", "--since", since, "--until", until, "--filter", "container="+name) + events := strings.Split(strings.TrimSuffix(out, "\n"), "\n") + c.Assert(checkEvents(ID, events), checker.IsNil) + + // filter by ID's + out, _ = dockerCmd(c, "events", "--since", since, "--until", until, "--filter", "container="+ID) + events = strings.Split(strings.TrimSuffix(out, "\n"), "\n") + c.Assert(checkEvents(ID, events), checker.IsNil) + } +} + +func (s *DockerSuite) TestEventsCommit(c *check.C) { + // Problematic on Windows as cannot commit a running container + testRequires(c, DaemonIsLinux) + since := daemonTime(c).Unix() + + out, _ := runSleepingContainer(c, "-d") + cID := strings.TrimSpace(out) + c.Assert(waitRun(cID), checker.IsNil) + + dockerCmd(c, "commit", "-m", "test", cID) + dockerCmd(c, "stop", cID) + + out, _ = dockerCmd(c, "events", "--since=0", "-f", "container="+cID, "--until="+strconv.Itoa(int(since))) + c.Assert(out, checker.Contains, "commit", check.Commentf("Missing 'commit' log event")) +} + +func (s *DockerSuite) TestEventsCopy(c *check.C) { + since := daemonTime(c).Unix() + + // Build a test image. + id, err := buildImage("cpimg", ` + FROM busybox + RUN echo HI > /file`, true) + c.Assert(err, checker.IsNil, check.Commentf("Couldn't create image")) + + // Create an empty test file. + tempFile, err := ioutil.TempFile("", "test-events-copy-") + c.Assert(err, checker.IsNil) + defer os.Remove(tempFile.Name()) + + c.Assert(tempFile.Close(), checker.IsNil) + + dockerCmd(c, "create", "--name=cptest", id) + + dockerCmd(c, "cp", "cptest:/file", tempFile.Name()) + + out, _ := dockerCmd(c, "events", "--since=0", "-f", "container=cptest", "--until="+strconv.Itoa(int(since))) + c.Assert(out, checker.Contains, "archive-path", check.Commentf("Missing 'archive-path' log event\n")) + + dockerCmd(c, "cp", tempFile.Name(), "cptest:/filecopy") + + out, _ = dockerCmd(c, "events", "--since=0", "-f", "container=cptest", "--until="+strconv.Itoa(int(since))) + c.Assert(out, checker.Contains, "extract-to-dir", check.Commentf("Missing 'extract-to-dir' log event")) +} + +func (s *DockerSuite) TestEventsResize(c *check.C) { + since := daemonTime(c).Unix() + + out, _ := runSleepingContainer(c, "-d") + cID := strings.TrimSpace(out) + c.Assert(waitRun(cID), checker.IsNil) + + endpoint := "/containers/" + cID + "/resize?h=80&w=24" + status, _, err := sockRequest("POST", endpoint, nil) + c.Assert(status, checker.Equals, http.StatusOK) + c.Assert(err, checker.IsNil) + + dockerCmd(c, "stop", cID) + + out, _ = dockerCmd(c, "events", "--since=0", "-f", "container="+cID, "--until="+strconv.Itoa(int(since))) + c.Assert(out, checker.Contains, "resize", check.Commentf("Missing 'resize' log event")) +} + +func (s *DockerSuite) TestEventsAttach(c *check.C) { + // TODO Windows CI: Figure out why this test fails intermittently (TP4 and TP5). + testRequires(c, DaemonIsLinux) + since := daemonTime(c).Unix() + + out, _ := dockerCmd(c, "run", "-di", "busybox", "cat") + cID := strings.TrimSpace(out) + + cmd := exec.Command(dockerBinary, "attach", cID) + stdin, err := cmd.StdinPipe() + c.Assert(err, checker.IsNil) + defer stdin.Close() + stdout, err := cmd.StdoutPipe() + c.Assert(err, checker.IsNil) + defer stdout.Close() + c.Assert(cmd.Start(), checker.IsNil) + defer cmd.Process.Kill() + + // Make sure we're done attaching by writing/reading some stuff + _, err = stdin.Write([]byte("hello\n")) + c.Assert(err, checker.IsNil) + out, err = bufio.NewReader(stdout).ReadString('\n') + c.Assert(err, checker.IsNil) + c.Assert(strings.TrimSpace(out), checker.Equals, "hello", check.Commentf("expected 'hello'")) + + c.Assert(stdin.Close(), checker.IsNil) + + dockerCmd(c, "kill", cID) + + out, _ = dockerCmd(c, "events", "--since=0", "-f", "container="+cID, "--until="+strconv.Itoa(int(since))) + c.Assert(out, checker.Contains, "attach", check.Commentf("Missing 'attach' log event")) +} + +func (s *DockerSuite) TestEventsRename(c *check.C) { + since := daemonTime(c).Unix() + + dockerCmd(c, "run", "--name", "oldName", "busybox", "true") + dockerCmd(c, "rename", "oldName", "newName") + + out, _ := dockerCmd(c, "events", "--since=0", "-f", "container=newName", "--until="+strconv.Itoa(int(since))) + c.Assert(out, checker.Contains, "rename", check.Commentf("Missing 'rename' log event\n")) +} + +func (s *DockerSuite) TestEventsTop(c *check.C) { + // Problematic on Windows as Windows does not support top + testRequires(c, DaemonIsLinux) + since := daemonTime(c).Unix() + + out, _ := runSleepingContainer(c, "-d") + cID := strings.TrimSpace(out) + c.Assert(waitRun(cID), checker.IsNil) + + dockerCmd(c, "top", cID) + dockerCmd(c, "stop", cID) + + out, _ = dockerCmd(c, "events", "--since=0", "-f", "container="+cID, "--until="+strconv.Itoa(int(since))) + c.Assert(out, checker.Contains, " top", check.Commentf("Missing 'top' log event")) +} + +// #13753 +func (s *DockerSuite) TestEventsDefaultEmpty(c *check.C) { + dockerCmd(c, "run", "busybox") + out, _ := dockerCmd(c, "events", fmt.Sprintf("--until=%d", daemonTime(c).Unix())) + c.Assert(strings.TrimSpace(out), checker.Equals, "") +} + +// #14316 +func (s *DockerRegistrySuite) TestEventsImageFilterPush(c *check.C) { + // Problematic to port for Windows CI during TP4/TP5 timeframe while + // not supporting push + testRequires(c, DaemonIsLinux) + testRequires(c, Network) + since := daemonTime(c).Unix() + repoName := fmt.Sprintf("%v/dockercli/testf", privateRegistryURL) + + out, _ := dockerCmd(c, "run", "-d", "busybox", "top") + cID := strings.TrimSpace(out) + c.Assert(waitRun(cID), checker.IsNil) + + dockerCmd(c, "commit", cID, repoName) + dockerCmd(c, "stop", cID) + dockerCmd(c, "push", repoName) + + out, _ = dockerCmd(c, "events", "--since=0", "-f", "image="+repoName, "-f", "event=push", "--until="+strconv.Itoa(int(since))) + c.Assert(out, checker.Contains, repoName, check.Commentf("Missing 'push' log event for %s", repoName)) +} + +func (s *DockerSuite) TestEventsFilterType(c *check.C) { + since := daemonTime(c).Unix() + name := "labelfiltertest" + label := "io.docker.testing=image" + + // Build a test image. + _, err := buildImage(name, fmt.Sprintf(` + FROM busybox:latest + LABEL %s`, label), true) + c.Assert(err, checker.IsNil, check.Commentf("Couldn't create image")) + + dockerCmd(c, "tag", name, "labelfiltertest:tag1") + dockerCmd(c, "tag", name, "labelfiltertest:tag2") + dockerCmd(c, "tag", "busybox:latest", "labelfiltertest:tag3") + + out, _ := dockerCmd( + c, + "events", + fmt.Sprintf("--since=%d", since), + fmt.Sprintf("--until=%d", daemonTime(c).Unix()), + "--filter", fmt.Sprintf("label=%s", label), + "--filter", "type=image") + + events := strings.Split(strings.TrimSpace(out), "\n") + + // 2 events from the "docker tag" command, another one is from "docker build" + c.Assert(events, checker.HasLen, 3, check.Commentf("Events == %s", events)) + for _, e := range events { + c.Assert(e, checker.Contains, "labelfiltertest") + } + + out, _ = dockerCmd( + c, + "events", + fmt.Sprintf("--since=%d", since), + fmt.Sprintf("--until=%d", daemonTime(c).Unix()), + "--filter", fmt.Sprintf("label=%s", label), + "--filter", "type=container") + events = strings.Split(strings.TrimSpace(out), "\n") + + // Events generated by the container that builds the image + c.Assert(events, checker.HasLen, 3, check.Commentf("Events == %s", events)) + + out, _ = dockerCmd( + c, + "events", + fmt.Sprintf("--since=%d", since), + fmt.Sprintf("--until=%d", daemonTime(c).Unix()), + "--filter", "type=network") + events = strings.Split(strings.TrimSpace(out), "\n") + c.Assert(len(events), checker.GreaterOrEqualThan, 1, check.Commentf("Events == %s", events)) +} + +func (s *DockerSuite) TestEventsFilterImageInContainerAction(c *check.C) { + since := daemonTime(c).Unix() + dockerCmd(c, "run", "--name", "test-container", "-d", "busybox", "true") + waitRun("test-container") + + out, _ := dockerCmd(c, "events", "--filter", "image=busybox", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix())) + events := strings.Split(strings.TrimSpace(out), "\n") + c.Assert(len(events), checker.GreaterThan, 1, check.Commentf(out)) +} + +func (s *DockerSuite) TestEventsContainerRestart(c *check.C) { + dockerCmd(c, "run", "-d", "--name=testEvent", "--restart=on-failure:3", "busybox", "false") + + // wait until test2 is auto removed. + waitTime := 10 * time.Second + if daemonPlatform == "windows" { + // nslookup isn't present in Windows busybox. Is built-in. + waitTime = 90 * time.Second + } + + err := waitInspect("testEvent", "{{ .State.Restarting }} {{ .State.Running }}", "false false", waitTime) + c.Assert(err, checker.IsNil) + + var ( + createCount int + startCount int + dieCount int + ) + out, _ := dockerCmd(c, "events", "--since=0", fmt.Sprintf("--until=%d", daemonTime(c).Unix()), "-f", "container=testEvent") + events := strings.Split(strings.TrimSpace(out), "\n") + + nEvents := len(events) + c.Assert(nEvents, checker.GreaterOrEqualThan, 1) //Missing expected event + actions := eventActionsByIDAndType(c, events, "testEvent", "container") + + for _, a := range actions { + switch a { + case "create": + createCount++ + case "start": + startCount++ + case "die": + dieCount++ + } + } + c.Assert(createCount, checker.Equals, 1, check.Commentf("testEvent should be created 1 times: %v", actions)) + c.Assert(startCount, checker.Equals, 4, check.Commentf("testEvent should start 4 times: %v", actions)) + c.Assert(dieCount, checker.Equals, 4, check.Commentf("testEvent should die 4 times: %v", actions)) + +} diff --git a/integration-cli/docker_cli_events_unix_test.go b/integration-cli/docker_cli_events_unix_test.go new file mode 100644 index 00000000..c5e48f21 --- /dev/null +++ b/integration-cli/docker_cli_events_unix_test.go @@ -0,0 +1,362 @@ +// +build !windows + +package main + +import ( + "bufio" + "fmt" + "io/ioutil" + "os" + "os/exec" + "strings" + "time" + "unicode" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" + "github.com/kr/pty" +) + +// #5979 +func (s *DockerSuite) TestEventsRedirectStdout(c *check.C) { + since := daemonTime(c).Unix() + dockerCmd(c, "run", "busybox", "true") + + file, err := ioutil.TempFile("", "") + c.Assert(err, checker.IsNil, check.Commentf("could not create temp file")) + defer os.Remove(file.Name()) + + command := fmt.Sprintf("%s events --since=%d --until=%d > %s", dockerBinary, since, daemonTime(c).Unix(), file.Name()) + _, tty, err := pty.Open() + c.Assert(err, checker.IsNil, check.Commentf("Could not open pty")) + cmd := exec.Command("sh", "-c", command) + cmd.Stdin = tty + cmd.Stdout = tty + cmd.Stderr = tty + c.Assert(cmd.Run(), checker.IsNil, check.Commentf("run err for command %q", command)) + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + for _, ch := range scanner.Text() { + c.Assert(unicode.IsControl(ch), checker.False, check.Commentf("found control character %v", []byte(string(ch)))) + } + } + c.Assert(scanner.Err(), checker.IsNil, check.Commentf("Scan err for command %q", command)) + +} + +func (s *DockerSuite) TestEventsOOMDisableFalse(c *check.C) { + testRequires(c, DaemonIsLinux, oomControl, memoryLimitSupport, NotGCCGO, swapMemorySupport) + + errChan := make(chan error) + go func() { + defer close(errChan) + out, exitCode, _ := dockerCmdWithError("run", "--name", "oomFalse", "-m", "10MB", "busybox", "sh", "-c", "x=a; while true; do x=$x$x$x$x; done") + if expected := 137; exitCode != expected { + errChan <- fmt.Errorf("wrong exit code for OOM container: expected %d, got %d (output: %q)", expected, exitCode, out) + } + }() + select { + case err := <-errChan: + c.Assert(err, checker.IsNil) + case <-time.After(30 * time.Second): + c.Fatal("Timeout waiting for container to die on OOM") + } + + out, _ := dockerCmd(c, "events", "--since=0", "-f", "container=oomFalse", fmt.Sprintf("--until=%d", daemonTime(c).Unix())) + events := strings.Split(strings.TrimSuffix(out, "\n"), "\n") + nEvents := len(events) + + c.Assert(nEvents, checker.GreaterOrEqualThan, 5) //Missing expected event + c.Assert(parseEventAction(c, events[nEvents-5]), checker.Equals, "create") + c.Assert(parseEventAction(c, events[nEvents-4]), checker.Equals, "attach") + c.Assert(parseEventAction(c, events[nEvents-3]), checker.Equals, "start") + c.Assert(parseEventAction(c, events[nEvents-2]), checker.Equals, "oom") + c.Assert(parseEventAction(c, events[nEvents-1]), checker.Equals, "die") +} + +func (s *DockerSuite) TestEventsOOMDisableTrue(c *check.C) { + testRequires(c, DaemonIsLinux, oomControl, memoryLimitSupport, NotGCCGO, NotArm, swapMemorySupport) + + errChan := make(chan error) + observer, err := newEventObserver(c) + c.Assert(err, checker.IsNil) + err = observer.Start() + c.Assert(err, checker.IsNil) + defer observer.Stop() + + go func() { + defer close(errChan) + out, exitCode, _ := dockerCmdWithError("run", "--oom-kill-disable=true", "--name", "oomTrue", "-m", "10MB", "busybox", "sh", "-c", "x=a; while true; do x=$x$x$x$x; done") + if expected := 137; exitCode != expected { + errChan <- fmt.Errorf("wrong exit code for OOM container: expected %d, got %d (output: %q)", expected, exitCode, out) + } + }() + + c.Assert(waitRun("oomTrue"), checker.IsNil) + defer dockerCmd(c, "kill", "oomTrue") + containerID := inspectField(c, "oomTrue", "Id") + + testActions := map[string]chan bool{ + "oom": make(chan bool), + } + + matcher := matchEventLine(containerID, "container", testActions) + processor := processEventMatch(testActions) + go observer.Match(matcher, processor) + + select { + case <-time.After(20 * time.Second): + observer.CheckEventError(c, containerID, "oom", matcher) + case <-testActions["oom"]: + // ignore, done + case errRun := <-errChan: + if errRun != nil { + c.Fatalf("%v", errRun) + } else { + c.Fatalf("container should be still running but it's not") + } + } + + status := inspectField(c, "oomTrue", "State.Status") + c.Assert(strings.TrimSpace(status), checker.Equals, "running", check.Commentf("container should be still running")) +} + +// #18453 +func (s *DockerSuite) TestEventsContainerFilterByName(c *check.C) { + testRequires(c, DaemonIsLinux) + cOut, _ := dockerCmd(c, "run", "--name=foo", "-d", "busybox", "top") + c1 := strings.TrimSpace(cOut) + waitRun("foo") + cOut, _ = dockerCmd(c, "run", "--name=bar", "-d", "busybox", "top") + c2 := strings.TrimSpace(cOut) + waitRun("bar") + out, _ := dockerCmd(c, "events", "-f", "container=foo", "--since=0", fmt.Sprintf("--until=%d", daemonTime(c).Unix())) + c.Assert(out, checker.Contains, c1, check.Commentf(out)) + c.Assert(out, checker.Not(checker.Contains), c2, check.Commentf(out)) +} + +// #18453 +func (s *DockerSuite) TestEventsContainerFilterBeforeCreate(c *check.C) { + testRequires(c, DaemonIsLinux) + var ( + out string + ch chan struct{} + ) + ch = make(chan struct{}) + + // calculate the time it takes to create and start a container and sleep 2 seconds + // this is to make sure the docker event will recevie the event of container + since := daemonTime(c).Unix() + id, _ := dockerCmd(c, "run", "-d", "busybox", "top") + cID := strings.TrimSpace(id) + waitRun(cID) + time.Sleep(2 * time.Second) + duration := daemonTime(c).Unix() - since + + go func() { + out, _ = dockerCmd(c, "events", "-f", "container=foo", "--since=0", fmt.Sprintf("--until=%d", daemonTime(c).Unix()+2*duration)) + close(ch) + }() + // Sleep 2 second to wait docker event to start + time.Sleep(2 * time.Second) + id, _ = dockerCmd(c, "run", "--name=foo", "-d", "busybox", "top") + cID = strings.TrimSpace(id) + waitRun(cID) + <-ch + c.Assert(out, checker.Contains, cID, check.Commentf("Missing event of container (foo)")) +} + +func (s *DockerSuite) TestVolumeEvents(c *check.C) { + testRequires(c, DaemonIsLinux) + + since := daemonTime(c).Unix() + + // Observe create/mount volume actions + dockerCmd(c, "volume", "create", "--name", "test-event-volume-local") + dockerCmd(c, "run", "--name", "test-volume-container", "--volume", "test-event-volume-local:/foo", "-d", "busybox", "true") + waitRun("test-volume-container") + + // Observe unmount/destroy volume actions + dockerCmd(c, "rm", "-f", "test-volume-container") + dockerCmd(c, "volume", "rm", "test-event-volume-local") + + out, _ := dockerCmd(c, "events", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix())) + events := strings.Split(strings.TrimSpace(out), "\n") + c.Assert(len(events), checker.GreaterThan, 4) + + volumeEvents := eventActionsByIDAndType(c, events, "test-event-volume-local", "volume") + c.Assert(volumeEvents, checker.HasLen, 4) + c.Assert(volumeEvents[0], checker.Equals, "create") + c.Assert(volumeEvents[1], checker.Equals, "mount") + c.Assert(volumeEvents[2], checker.Equals, "unmount") + c.Assert(volumeEvents[3], checker.Equals, "destroy") +} + +func (s *DockerSuite) TestNetworkEvents(c *check.C) { + testRequires(c, DaemonIsLinux) + + since := daemonTime(c).Unix() + + // Observe create/connect network actions + dockerCmd(c, "network", "create", "test-event-network-local") + dockerCmd(c, "run", "--name", "test-network-container", "--net", "test-event-network-local", "-d", "busybox", "true") + waitRun("test-network-container") + + // Observe disconnect/destroy network actions + dockerCmd(c, "rm", "-f", "test-network-container") + dockerCmd(c, "network", "rm", "test-event-network-local") + + out, _ := dockerCmd(c, "events", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix())) + events := strings.Split(strings.TrimSpace(out), "\n") + c.Assert(len(events), checker.GreaterThan, 4) + + netEvents := eventActionsByIDAndType(c, events, "test-event-network-local", "network") + c.Assert(netEvents, checker.HasLen, 4) + c.Assert(netEvents[0], checker.Equals, "create") + c.Assert(netEvents[1], checker.Equals, "connect") + c.Assert(netEvents[2], checker.Equals, "disconnect") + c.Assert(netEvents[3], checker.Equals, "destroy") +} + +func (s *DockerSuite) TestEventsStreaming(c *check.C) { + testRequires(c, DaemonIsLinux) + + observer, err := newEventObserver(c) + c.Assert(err, checker.IsNil) + err = observer.Start() + c.Assert(err, checker.IsNil) + defer observer.Stop() + + out, _ := dockerCmd(c, "run", "-d", "busybox:latest", "true") + containerID := strings.TrimSpace(out) + + testActions := map[string]chan bool{ + "create": make(chan bool, 1), + "start": make(chan bool, 1), + "die": make(chan bool, 1), + "destroy": make(chan bool, 1), + } + + matcher := matchEventLine(containerID, "container", testActions) + processor := processEventMatch(testActions) + go observer.Match(matcher, processor) + + select { + case <-time.After(5 * time.Second): + observer.CheckEventError(c, containerID, "create", matcher) + case <-testActions["create"]: + // ignore, done + } + + select { + case <-time.After(5 * time.Second): + observer.CheckEventError(c, containerID, "start", matcher) + case <-testActions["start"]: + // ignore, done + } + + select { + case <-time.After(5 * time.Second): + observer.CheckEventError(c, containerID, "die", matcher) + case <-testActions["die"]: + // ignore, done + } + + dockerCmd(c, "rm", containerID) + + select { + case <-time.After(5 * time.Second): + observer.CheckEventError(c, containerID, "destroy", matcher) + case <-testActions["destroy"]: + // ignore, done + } +} + +func (s *DockerSuite) TestEventsImageUntagDelete(c *check.C) { + testRequires(c, DaemonIsLinux) + + observer, err := newEventObserver(c) + c.Assert(err, checker.IsNil) + err = observer.Start() + c.Assert(err, checker.IsNil) + defer observer.Stop() + + name := "testimageevents" + imageID, err := buildImage(name, + `FROM scratch + MAINTAINER "docker"`, + true) + c.Assert(err, checker.IsNil) + c.Assert(deleteImages(name), checker.IsNil) + + testActions := map[string]chan bool{ + "untag": make(chan bool, 1), + "delete": make(chan bool, 1), + } + + matcher := matchEventLine(imageID, "image", testActions) + processor := processEventMatch(testActions) + go observer.Match(matcher, processor) + + select { + case <-time.After(10 * time.Second): + observer.CheckEventError(c, imageID, "untag", matcher) + case <-testActions["untag"]: + // ignore, done + } + + select { + case <-time.After(10 * time.Second): + observer.CheckEventError(c, imageID, "delete", matcher) + case <-testActions["delete"]: + // ignore, done + } +} + +func (s *DockerSuite) TestEventsFilterVolumeAndNetworkType(c *check.C) { + testRequires(c, DaemonIsLinux) + + since := daemonTime(c).Unix() + + dockerCmd(c, "network", "create", "test-event-network-type") + dockerCmd(c, "volume", "create", "--name", "test-event-volume-type") + + out, _ := dockerCmd(c, "events", "--filter", "type=volume", "--filter", "type=network", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix())) + events := strings.Split(strings.TrimSpace(out), "\n") + c.Assert(len(events), checker.GreaterOrEqualThan, 2, check.Commentf(out)) + + networkActions := eventActionsByIDAndType(c, events, "test-event-network-type", "network") + volumeActions := eventActionsByIDAndType(c, events, "test-event-volume-type", "volume") + + c.Assert(volumeActions[0], checker.Equals, "create") + c.Assert(networkActions[0], checker.Equals, "create") +} + +func (s *DockerSuite) TestEventsFilterVolumeID(c *check.C) { + testRequires(c, DaemonIsLinux) + + since := daemonTime(c).Unix() + + dockerCmd(c, "volume", "create", "--name", "test-event-volume-id") + out, _ := dockerCmd(c, "events", "--filter", "volume=test-event-volume-id", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix())) + events := strings.Split(strings.TrimSpace(out), "\n") + c.Assert(events, checker.HasLen, 1) + + c.Assert(events[0], checker.Contains, "test-event-volume-id") + c.Assert(events[0], checker.Contains, "driver=local") +} + +func (s *DockerSuite) TestEventsFilterNetworkID(c *check.C) { + testRequires(c, DaemonIsLinux) + + since := daemonTime(c).Unix() + + dockerCmd(c, "network", "create", "test-event-network-local") + out, _ := dockerCmd(c, "events", "--filter", "network=test-event-network-local", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", daemonTime(c).Unix())) + events := strings.Split(strings.TrimSpace(out), "\n") + c.Assert(events, checker.HasLen, 1) + + c.Assert(events[0], checker.Contains, "test-event-network-local") + c.Assert(events[0], checker.Contains, "type=bridge") +} diff --git a/integration-cli/docker_cli_exec_test.go b/integration-cli/docker_cli_exec_test.go new file mode 100644 index 00000000..a8150ad2 --- /dev/null +++ b/integration-cli/docker_cli_exec_test.go @@ -0,0 +1,511 @@ +// +build !test_no_exec + +package main + +import ( + "bufio" + "fmt" + "net/http" + "os" + "os/exec" + "reflect" + "sort" + "strings" + "sync" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestExec(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "--name", "testing", "busybox", "sh", "-c", "echo test > /tmp/file && top") + c.Assert(waitRun(strings.TrimSpace(out)), check.IsNil) + + out, _ = dockerCmd(c, "exec", "testing", "cat", "/tmp/file") + out = strings.Trim(out, "\r\n") + c.Assert(out, checker.Equals, "test") + +} + +func (s *DockerSuite) TestExecInteractive(c *check.C) { + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "-d", "--name", "testing", "busybox", "sh", "-c", "echo test > /tmp/file && top") + + execCmd := exec.Command(dockerBinary, "exec", "-i", "testing", "sh") + stdin, err := execCmd.StdinPipe() + c.Assert(err, checker.IsNil) + stdout, err := execCmd.StdoutPipe() + c.Assert(err, checker.IsNil) + + err = execCmd.Start() + c.Assert(err, checker.IsNil) + _, err = stdin.Write([]byte("cat /tmp/file\n")) + c.Assert(err, checker.IsNil) + + r := bufio.NewReader(stdout) + line, err := r.ReadString('\n') + c.Assert(err, checker.IsNil) + line = strings.TrimSpace(line) + c.Assert(line, checker.Equals, "test") + err = stdin.Close() + c.Assert(err, checker.IsNil) + errChan := make(chan error) + go func() { + errChan <- execCmd.Wait() + close(errChan) + }() + select { + case err := <-errChan: + c.Assert(err, checker.IsNil) + case <-time.After(1 * time.Second): + c.Fatal("docker exec failed to exit on stdin close") + } + +} + +func (s *DockerSuite) TestExecAfterContainerRestart(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := runSleepingContainer(c) + cleanedContainerID := strings.TrimSpace(out) + c.Assert(waitRun(cleanedContainerID), check.IsNil) + dockerCmd(c, "restart", cleanedContainerID) + c.Assert(waitRun(cleanedContainerID), check.IsNil) + + out, _ = dockerCmd(c, "exec", cleanedContainerID, "echo", "hello") + outStr := strings.TrimSpace(out) + c.Assert(outStr, checker.Equals, "hello") +} + +func (s *DockerDaemonSuite) TestExecAfterDaemonRestart(c *check.C) { + // TODO Windows CI: Requires a little work to get this ported. + testRequires(c, DaemonIsLinux) + testRequires(c, SameHostDaemon) + + err := s.d.StartWithBusybox() + c.Assert(err, checker.IsNil) + + out, err := s.d.Cmd("run", "-d", "--name", "top", "-p", "80", "busybox:latest", "top") + c.Assert(err, checker.IsNil, check.Commentf("Could not run top: %s", out)) + + err = s.d.Restart() + c.Assert(err, checker.IsNil, check.Commentf("Could not restart daemon")) + + out, err = s.d.Cmd("start", "top") + c.Assert(err, checker.IsNil, check.Commentf("Could not start top after daemon restart: %s", out)) + + out, err = s.d.Cmd("exec", "top", "echo", "hello") + c.Assert(err, checker.IsNil, check.Commentf("Could not exec on container top: %s", out)) + + outStr := strings.TrimSpace(string(out)) + c.Assert(outStr, checker.Equals, "hello") +} + +// Regression test for #9155, #9044 +func (s *DockerSuite) TestExecEnv(c *check.C) { + // TODO Windows CI: This one is interesting and may just end up being a feature + // difference between Windows and Linux. On Windows, the environment is passed + // into the process that is launched, not into the machine environment. Hence + // a subsequent exec will not have LALA set/ + testRequires(c, DaemonIsLinux) + runSleepingContainer(c, "-e", "LALA=value1", "-e", "LALA=value2", "-d", "--name", "testing") + c.Assert(waitRun("testing"), check.IsNil) + + out, _ := dockerCmd(c, "exec", "testing", "env") + c.Assert(out, checker.Not(checker.Contains), "LALA=value1") + c.Assert(out, checker.Contains, "LALA=value2") + c.Assert(out, checker.Contains, "HOME=/root") +} + +func (s *DockerSuite) TestExecExitStatus(c *check.C) { + runSleepingContainer(c, "-d", "--name", "top") + + // Test normal (non-detached) case first + cmd := exec.Command(dockerBinary, "exec", "top", "sh", "-c", "exit 23") + ec, _ := runCommand(cmd) + c.Assert(ec, checker.Equals, 23) +} + +func (s *DockerSuite) TestExecPausedContainer(c *check.C) { + // Windows does not support pause + testRequires(c, DaemonIsLinux) + defer unpauseAllContainers() + + out, _ := dockerCmd(c, "run", "-d", "--name", "testing", "busybox", "top") + ContainerID := strings.TrimSpace(out) + + dockerCmd(c, "pause", "testing") + out, _, err := dockerCmdWithError("exec", "-i", "-t", ContainerID, "echo", "hello") + c.Assert(err, checker.NotNil, check.Commentf("container should fail to exec new conmmand if it is paused")) + + expected := ContainerID + " is paused, unpause the container before exec" + c.Assert(out, checker.Contains, expected, check.Commentf("container should not exec new command if it is paused")) +} + +// regression test for #9476 +func (s *DockerSuite) TestExecTTYCloseStdin(c *check.C) { + // TODO Windows CI: This requires some work to port to Windows. + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "-d", "-it", "--name", "exec_tty_stdin", "busybox") + + cmd := exec.Command(dockerBinary, "exec", "-i", "exec_tty_stdin", "cat") + stdinRw, err := cmd.StdinPipe() + c.Assert(err, checker.IsNil) + + stdinRw.Write([]byte("test")) + stdinRw.Close() + + out, _, err := runCommandWithOutput(cmd) + c.Assert(err, checker.IsNil, check.Commentf(out)) + + out, _ = dockerCmd(c, "top", "exec_tty_stdin") + outArr := strings.Split(out, "\n") + c.Assert(len(outArr), checker.LessOrEqualThan, 3, check.Commentf("exec process left running")) + c.Assert(out, checker.Not(checker.Contains), "nsenter-exec") +} + +func (s *DockerSuite) TestExecTTYWithoutStdin(c *check.C) { + // TODO Windows CI: This requires some work to port to Windows. + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "-ti", "busybox") + id := strings.TrimSpace(out) + c.Assert(waitRun(id), checker.IsNil) + + errChan := make(chan error) + go func() { + defer close(errChan) + + cmd := exec.Command(dockerBinary, "exec", "-ti", id, "true") + if _, err := cmd.StdinPipe(); err != nil { + errChan <- err + return + } + + expected := "cannot enable tty mode" + if out, _, err := runCommandWithOutput(cmd); err == nil { + errChan <- fmt.Errorf("exec should have failed") + return + } else if !strings.Contains(out, expected) { + errChan <- fmt.Errorf("exec failed with error %q: expected %q", out, expected) + return + } + }() + + select { + case err := <-errChan: + c.Assert(err, check.IsNil) + case <-time.After(3 * time.Second): + c.Fatal("exec is running but should have failed") + } +} + +func (s *DockerSuite) TestExecParseError(c *check.C) { + // TODO Windows CI: Requires some extra work. Consider copying the + // runSleepingContainer helper to have an exec version. + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "-d", "--name", "top", "busybox", "top") + + // Test normal (non-detached) case first + cmd := exec.Command(dockerBinary, "exec", "top") + _, stderr, _, err := runCommandWithStdoutStderr(cmd) + c.Assert(err, checker.NotNil) + c.Assert(stderr, checker.Contains, "See '"+dockerBinary+" exec --help'") +} + +func (s *DockerSuite) TestExecStopNotHanging(c *check.C) { + // TODO Windows CI: Requires some extra work. Consider copying the + // runSleepingContainer helper to have an exec version. + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "-d", "--name", "testing", "busybox", "top") + + err := exec.Command(dockerBinary, "exec", "testing", "top").Start() + c.Assert(err, checker.IsNil) + + type dstop struct { + out []byte + err error + } + + ch := make(chan dstop) + go func() { + out, err := exec.Command(dockerBinary, "stop", "testing").CombinedOutput() + ch <- dstop{out, err} + close(ch) + }() + select { + case <-time.After(3 * time.Second): + c.Fatal("Container stop timed out") + case s := <-ch: + c.Assert(s.err, check.IsNil) + } +} + +func (s *DockerSuite) TestExecCgroup(c *check.C) { + // Not applicable on Windows - using Linux specific functionality + testRequires(c, NotUserNamespace) + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "-d", "--name", "testing", "busybox", "top") + + out, _ := dockerCmd(c, "exec", "testing", "cat", "/proc/1/cgroup") + containerCgroups := sort.StringSlice(strings.Split(out, "\n")) + + var wg sync.WaitGroup + var mu sync.Mutex + execCgroups := []sort.StringSlice{} + errChan := make(chan error) + // exec a few times concurrently to get consistent failure + for i := 0; i < 5; i++ { + wg.Add(1) + go func() { + out, _, err := dockerCmdWithError("exec", "testing", "cat", "/proc/self/cgroup") + if err != nil { + errChan <- err + return + } + cg := sort.StringSlice(strings.Split(out, "\n")) + + mu.Lock() + execCgroups = append(execCgroups, cg) + mu.Unlock() + wg.Done() + }() + } + wg.Wait() + close(errChan) + + for err := range errChan { + c.Assert(err, checker.IsNil) + } + + for _, cg := range execCgroups { + if !reflect.DeepEqual(cg, containerCgroups) { + fmt.Println("exec cgroups:") + for _, name := range cg { + fmt.Printf(" %s\n", name) + } + + fmt.Println("container cgroups:") + for _, name := range containerCgroups { + fmt.Printf(" %s\n", name) + } + c.Fatal("cgroups mismatched") + } + } +} + +func (s *DockerSuite) TestExecInspectID(c *check.C) { + out, _ := runSleepingContainer(c, "-d") + id := strings.TrimSuffix(out, "\n") + + out = inspectField(c, id, "ExecIDs") + c.Assert(out, checker.Equals, "[]", check.Commentf("ExecIDs should be empty, got: %s", out)) + + // Start an exec, have it block waiting so we can do some checking + cmd := exec.Command(dockerBinary, "exec", id, "sh", "-c", + "while ! test -e /execid1; do sleep 1; done") + + err := cmd.Start() + c.Assert(err, checker.IsNil, check.Commentf("failed to start the exec cmd")) + + // Give the exec 10 chances/seconds to start then give up and stop the test + tries := 10 + for i := 0; i < tries; i++ { + // Since its still running we should see exec as part of the container + out = strings.TrimSpace(inspectField(c, id, "ExecIDs")) + + if out != "[]" && out != "" { + break + } + c.Assert(i+1, checker.Not(checker.Equals), tries, check.Commentf("ExecIDs still empty after 10 second")) + time.Sleep(1 * time.Second) + } + + // Save execID for later + execID, err := inspectFilter(id, "index .ExecIDs 0") + c.Assert(err, checker.IsNil, check.Commentf("failed to get the exec id")) + + // End the exec by creating the missing file + err = exec.Command(dockerBinary, "exec", id, + "sh", "-c", "touch /execid1").Run() + + c.Assert(err, checker.IsNil, check.Commentf("failed to run the 2nd exec cmd")) + + // Wait for 1st exec to complete + cmd.Wait() + + // Give the exec 10 chances/seconds to stop then give up and stop the test + for i := 0; i < tries; i++ { + // Since its still running we should see exec as part of the container + out = strings.TrimSpace(inspectField(c, id, "ExecIDs")) + + if out == "[]" { + break + } + c.Assert(i+1, checker.Not(checker.Equals), tries, check.Commentf("ExecIDs still not empty after 10 second")) + time.Sleep(1 * time.Second) + } + + // But we should still be able to query the execID + sc, body, err := sockRequest("GET", "/exec/"+execID+"/json", nil) + c.Assert(sc, checker.Equals, http.StatusOK, check.Commentf("received status != 200 OK: %d\n%s", sc, body)) + + // Now delete the container and then an 'inspect' on the exec should + // result in a 404 (not 'container not running') + out, ec := dockerCmd(c, "rm", "-f", id) + c.Assert(ec, checker.Equals, 0, check.Commentf("error removing container: %s", out)) + sc, body, err = sockRequest("GET", "/exec/"+execID+"/json", nil) + c.Assert(sc, checker.Equals, http.StatusNotFound, check.Commentf("received status != 404: %d\n%s", sc, body)) +} + +func (s *DockerSuite) TestLinksPingLinkedContainersOnRename(c *check.C) { + // Problematic on Windows as Windows does not support links + testRequires(c, DaemonIsLinux) + var out string + out, _ = dockerCmd(c, "run", "-d", "--name", "container1", "busybox", "top") + idA := strings.TrimSpace(out) + c.Assert(idA, checker.Not(checker.Equals), "", check.Commentf("%s, id should not be nil", out)) + out, _ = dockerCmd(c, "run", "-d", "--link", "container1:alias1", "--name", "container2", "busybox", "top") + idB := strings.TrimSpace(out) + c.Assert(idB, checker.Not(checker.Equals), "", check.Commentf("%s, id should not be nil", out)) + + dockerCmd(c, "exec", "container2", "ping", "-c", "1", "alias1", "-W", "1") + dockerCmd(c, "rename", "container1", "container_new") + dockerCmd(c, "exec", "container2", "ping", "-c", "1", "alias1", "-W", "1") +} + +func (s *DockerSuite) TestRunMutableNetworkFiles(c *check.C) { + // Not applicable on Windows to Windows CI. + testRequires(c, SameHostDaemon, DaemonIsLinux) + for _, fn := range []string{"resolv.conf", "hosts"} { + deleteAllContainers() + + content, err := runCommandAndReadContainerFile(fn, exec.Command(dockerBinary, "run", "-d", "--name", "c1", "busybox", "sh", "-c", fmt.Sprintf("echo success >/etc/%s && top", fn))) + c.Assert(err, checker.IsNil) + + c.Assert(strings.TrimSpace(string(content)), checker.Equals, "success", check.Commentf("Content was not what was modified in the container", string(content))) + + out, _ := dockerCmd(c, "run", "-d", "--name", "c2", "busybox", "top") + contID := strings.TrimSpace(out) + netFilePath := containerStorageFile(contID, fn) + + f, err := os.OpenFile(netFilePath, os.O_WRONLY|os.O_SYNC|os.O_APPEND, 0644) + c.Assert(err, checker.IsNil) + + if _, err := f.Seek(0, 0); err != nil { + f.Close() + c.Fatal(err) + } + + if err := f.Truncate(0); err != nil { + f.Close() + c.Fatal(err) + } + + if _, err := f.Write([]byte("success2\n")); err != nil { + f.Close() + c.Fatal(err) + } + f.Close() + + res, _ := dockerCmd(c, "exec", contID, "cat", "/etc/"+fn) + c.Assert(res, checker.Equals, "success2\n") + } +} + +func (s *DockerSuite) TestExecWithUser(c *check.C) { + // TODO Windows CI: This may be fixable in the future once Windows + // supports users + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "-d", "--name", "parent", "busybox", "top") + + out, _ := dockerCmd(c, "exec", "-u", "1", "parent", "id") + c.Assert(out, checker.Contains, "uid=1(daemon) gid=1(daemon)") + + out, _ = dockerCmd(c, "exec", "-u", "root", "parent", "id") + c.Assert(out, checker.Contains, "uid=0(root) gid=0(root)", check.Commentf("exec with user by id expected daemon user got %s", out)) +} + +func (s *DockerSuite) TestExecWithPrivileged(c *check.C) { + // Not applicable on Windows + testRequires(c, DaemonIsLinux, NotUserNamespace) + // Start main loop which attempts mknod repeatedly + dockerCmd(c, "run", "-d", "--name", "parent", "--cap-drop=ALL", "busybox", "sh", "-c", `while (true); do if [ -e /exec_priv ]; then cat /exec_priv && mknod /tmp/sda b 8 0 && echo "Success"; else echo "Privileged exec has not run yet"; fi; usleep 10000; done`) + + // Check exec mknod doesn't work + cmd := exec.Command(dockerBinary, "exec", "parent", "sh", "-c", "mknod /tmp/sdb b 8 16") + out, _, err := runCommandWithOutput(cmd) + c.Assert(err, checker.NotNil, check.Commentf("exec mknod in --cap-drop=ALL container without --privileged should fail")) + c.Assert(out, checker.Contains, "Operation not permitted", check.Commentf("exec mknod in --cap-drop=ALL container without --privileged should fail")) + + // Check exec mknod does work with --privileged + cmd = exec.Command(dockerBinary, "exec", "--privileged", "parent", "sh", "-c", `echo "Running exec --privileged" > /exec_priv && mknod /tmp/sdb b 8 16 && usleep 50000 && echo "Finished exec --privileged" > /exec_priv && echo ok`) + out, _, err = runCommandWithOutput(cmd) + c.Assert(err, checker.IsNil) + + actual := strings.TrimSpace(out) + c.Assert(actual, checker.Equals, "ok", check.Commentf("exec mknod in --cap-drop=ALL container with --privileged failed, output: %q", out)) + + // Check subsequent unprivileged exec cannot mknod + cmd = exec.Command(dockerBinary, "exec", "parent", "sh", "-c", "mknod /tmp/sdc b 8 32") + out, _, err = runCommandWithOutput(cmd) + c.Assert(err, checker.NotNil, check.Commentf("repeating exec mknod in --cap-drop=ALL container after --privileged without --privileged should fail")) + c.Assert(out, checker.Contains, "Operation not permitted", check.Commentf("repeating exec mknod in --cap-drop=ALL container after --privileged without --privileged should fail")) + + // Confirm at no point was mknod allowed + logCmd := exec.Command(dockerBinary, "logs", "parent") + out, _, err = runCommandWithOutput(logCmd) + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Not(checker.Contains), "Success") + +} + +func (s *DockerSuite) TestExecWithImageUser(c *check.C) { + // Not applicable on Windows + testRequires(c, DaemonIsLinux) + name := "testbuilduser" + _, err := buildImage(name, + `FROM busybox + RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd + USER dockerio`, + true) + c.Assert(err, checker.IsNil) + + dockerCmd(c, "run", "-d", "--name", "dockerioexec", name, "top") + + out, _ := dockerCmd(c, "exec", "dockerioexec", "whoami") + c.Assert(out, checker.Contains, "dockerio", check.Commentf("exec with user by id expected dockerio user got %s", out)) +} + +func (s *DockerSuite) TestExecOnReadonlyContainer(c *check.C) { + // Windows does not support read-only + // --read-only + userns has remount issues + testRequires(c, DaemonIsLinux, NotUserNamespace) + dockerCmd(c, "run", "-d", "--read-only", "--name", "parent", "busybox", "top") + dockerCmd(c, "exec", "parent", "true") +} + +func (s *DockerSuite) TestExecUlimits(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "testexeculimits" + runSleepingContainer(c, "-d", "--ulimit", "nproc=21", "--name", name) + c.Assert(waitRun(name), checker.IsNil) + + out, _, err := dockerCmdWithError("exec", name, "sh", "-c", "ulimit -p") + c.Assert(err, checker.IsNil) + c.Assert(strings.TrimSpace(out), checker.Equals, "21") +} + +// #15750 +func (s *DockerSuite) TestExecStartFails(c *check.C) { + // TODO Windows CI. This test should be portable. Figure out why it fails + // currently. + testRequires(c, DaemonIsLinux) + name := "exec-15750" + runSleepingContainer(c, "-d", "--name", name) + c.Assert(waitRun(name), checker.IsNil) + + out, _, err := dockerCmdWithError("exec", name, "no-such-cmd") + c.Assert(err, checker.NotNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "executable file not found") +} diff --git a/integration-cli/docker_cli_exec_unix_test.go b/integration-cli/docker_cli_exec_unix_test.go new file mode 100644 index 00000000..42db4091 --- /dev/null +++ b/integration-cli/docker_cli_exec_unix_test.go @@ -0,0 +1,70 @@ +// +build !windows,!test_no_exec + +package main + +import ( + "bytes" + "io" + "os/exec" + "strings" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" + "github.com/kr/pty" +) + +// regression test for #12546 +func (s *DockerSuite) TestExecInteractiveStdinClose(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-itd", "busybox", "/bin/cat") + contID := strings.TrimSpace(out) + + cmd := exec.Command(dockerBinary, "exec", "-i", contID, "echo", "-n", "hello") + p, err := pty.Start(cmd) + c.Assert(err, checker.IsNil) + + b := bytes.NewBuffer(nil) + go io.Copy(b, p) + + ch := make(chan error) + go func() { ch <- cmd.Wait() }() + + select { + case err := <-ch: + c.Assert(err, checker.IsNil) + output := b.String() + c.Assert(strings.TrimSpace(output), checker.Equals, "hello") + case <-time.After(5 * time.Second): + c.Fatal("timed out running docker exec") + } +} + +func (s *DockerSuite) TestExecTTY(c *check.C) { + testRequires(c, DaemonIsLinux, SameHostDaemon) + dockerCmd(c, "run", "-d", "--name=test", "busybox", "sh", "-c", "echo hello > /foo && top") + + cmd := exec.Command(dockerBinary, "exec", "-it", "test", "sh") + p, err := pty.Start(cmd) + c.Assert(err, checker.IsNil) + defer p.Close() + + _, err = p.Write([]byte("cat /foo && exit\n")) + c.Assert(err, checker.IsNil) + + chErr := make(chan error) + go func() { + chErr <- cmd.Wait() + }() + select { + case err := <-chErr: + c.Assert(err, checker.IsNil) + case <-time.After(3 * time.Second): + c.Fatal("timeout waiting for exec to exit") + } + + buf := make([]byte, 256) + read, err := p.Read(buf) + c.Assert(err, checker.IsNil) + c.Assert(bytes.Contains(buf, []byte("hello")), checker.Equals, true, check.Commentf(string(buf[:read]))) +} diff --git a/integration-cli/docker_cli_experimental_test.go b/integration-cli/docker_cli_experimental_test.go new file mode 100644 index 00000000..8795078f --- /dev/null +++ b/integration-cli/docker_cli_experimental_test.go @@ -0,0 +1,21 @@ +// +build experimental + +package main + +import ( + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" + "strings" +) + +func (s *DockerSuite) TestExperimentalVersion(c *check.C) { + out, _ := dockerCmd(c, "version") + for _, line := range strings.Split(out, "\n") { + if strings.HasPrefix(line, "Experimental (client):") || strings.HasPrefix(line, "Experimental (server):") { + c.Assert(line, checker.Matches, "*true") + } + } + + out, _ = dockerCmd(c, "-v") + c.Assert(out, checker.Contains, ", experimental", check.Commentf("docker version did not contain experimental")) +} diff --git a/integration-cli/docker_cli_export_import_test.go b/integration-cli/docker_cli_export_import_test.go new file mode 100644 index 00000000..069dc081 --- /dev/null +++ b/integration-cli/docker_cli_export_import_test.go @@ -0,0 +1,49 @@ +package main + +import ( + "os" + "os/exec" + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +// export an image and try to import it into a new one +func (s *DockerSuite) TestExportContainerAndImportImage(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := "testexportcontainerandimportimage" + + dockerCmd(c, "run", "--name", containerID, "busybox", "true") + + out, _ := dockerCmd(c, "export", containerID) + + importCmd := exec.Command(dockerBinary, "import", "-", "repo/testexp:v1") + importCmd.Stdin = strings.NewReader(out) + out, _, err := runCommandWithOutput(importCmd) + c.Assert(err, checker.IsNil, check.Commentf("failed to import image repo/testexp:v1: %s", out)) + + cleanedImageID := strings.TrimSpace(out) + c.Assert(cleanedImageID, checker.Not(checker.Equals), "", check.Commentf("output should have been an image id")) +} + +// Used to test output flag in the export command +func (s *DockerSuite) TestExportContainerWithOutputAndImportImage(c *check.C) { + testRequires(c, DaemonIsLinux) + containerID := "testexportcontainerwithoutputandimportimage" + + dockerCmd(c, "run", "--name", containerID, "busybox", "true") + dockerCmd(c, "export", "--output=testexp.tar", containerID) + defer os.Remove("testexp.tar") + + out, _, err := runCommandWithOutput(exec.Command("cat", "testexp.tar")) + c.Assert(err, checker.IsNil, check.Commentf(out)) + + importCmd := exec.Command(dockerBinary, "import", "-", "repo/testexp:v1") + importCmd.Stdin = strings.NewReader(out) + out, _, err = runCommandWithOutput(importCmd) + c.Assert(err, checker.IsNil, check.Commentf("failed to import image repo/testexp:v1: %s", out)) + + cleanedImageID := strings.TrimSpace(out) + c.Assert(cleanedImageID, checker.Not(checker.Equals), "", check.Commentf("output should have been an image id")) +} diff --git a/integration-cli/docker_cli_external_graphdriver_unix_test.go b/integration-cli/docker_cli_external_graphdriver_unix_test.go new file mode 100644 index 00000000..33fe4058 --- /dev/null +++ b/integration-cli/docker_cli_external_graphdriver_unix_test.go @@ -0,0 +1,384 @@ +// +build experimental +// +build !windows + +package main + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "strings" + + "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/daemon/graphdriver/vfs" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/plugins" + "github.com/go-check/check" +) + +func init() { + check.Suite(&DockerExternalGraphdriverSuite{ + ds: &DockerSuite{}, + }) +} + +type DockerExternalGraphdriverSuite struct { + server *httptest.Server + jserver *httptest.Server + ds *DockerSuite + d *Daemon + ec map[string]*graphEventsCounter +} + +type graphEventsCounter struct { + activations int + creations int + removals int + gets int + puts int + stats int + cleanups int + exists int + init int + metadata int + diff int + applydiff int + changes int + diffsize int +} + +func (s *DockerExternalGraphdriverSuite) SetUpTest(c *check.C) { + s.d = NewDaemon(c) +} + +func (s *DockerExternalGraphdriverSuite) TearDownTest(c *check.C) { + s.d.Stop() + s.ds.TearDownTest(c) +} + +func (s *DockerExternalGraphdriverSuite) SetUpSuite(c *check.C) { + s.ec = make(map[string]*graphEventsCounter) + s.setUpPluginViaSpecFile(c) + s.setUpPluginViaJSONFile(c) +} + +func (s *DockerExternalGraphdriverSuite) setUpPluginViaSpecFile(c *check.C) { + mux := http.NewServeMux() + s.server = httptest.NewServer(mux) + + s.setUpPlugin(c, "test-external-graph-driver", "spec", mux, []byte(s.server.URL)) +} + +func (s *DockerExternalGraphdriverSuite) setUpPluginViaJSONFile(c *check.C) { + mux := http.NewServeMux() + s.jserver = httptest.NewServer(mux) + + p := plugins.Plugin{Name: "json-external-graph-driver", Addr: s.jserver.URL} + b, err := json.Marshal(p) + c.Assert(err, check.IsNil) + + s.setUpPlugin(c, "json-external-graph-driver", "json", mux, b) +} + +func (s *DockerExternalGraphdriverSuite) setUpPlugin(c *check.C, name string, ext string, mux *http.ServeMux, b []byte) { + type graphDriverRequest struct { + ID string `json:",omitempty"` + Parent string `json:",omitempty"` + MountLabel string `json:",omitempty"` + } + + type graphDriverResponse struct { + Err error `json:",omitempty"` + Dir string `json:",omitempty"` + Exists bool `json:",omitempty"` + Status [][2]string `json:",omitempty"` + Metadata map[string]string `json:",omitempty"` + Changes []archive.Change `json:",omitempty"` + Size int64 `json:",omitempty"` + } + + respond := func(w http.ResponseWriter, data interface{}) { + w.Header().Set("Content-Type", "appplication/vnd.docker.plugins.v1+json") + switch t := data.(type) { + case error: + fmt.Fprintln(w, fmt.Sprintf(`{"Err": %q}`, t.Error())) + case string: + fmt.Fprintln(w, t) + default: + json.NewEncoder(w).Encode(&data) + } + } + + decReq := func(b io.ReadCloser, out interface{}, w http.ResponseWriter) error { + defer b.Close() + if err := json.NewDecoder(b).Decode(&out); err != nil { + http.Error(w, fmt.Sprintf("error decoding json: %s", err.Error()), 500) + } + return nil + } + + base, err := ioutil.TempDir("", name) + c.Assert(err, check.IsNil) + vfsProto, err := vfs.Init(base, []string{}, nil, nil) + c.Assert(err, check.IsNil, check.Commentf("error initializing graph driver")) + driver := graphdriver.NewNaiveDiffDriver(vfsProto, nil, nil) + + s.ec[ext] = &graphEventsCounter{} + mux.HandleFunc("/Plugin.Activate", func(w http.ResponseWriter, r *http.Request) { + s.ec[ext].activations++ + respond(w, `{"Implements": ["GraphDriver"]}`) + }) + + mux.HandleFunc("/GraphDriver.Init", func(w http.ResponseWriter, r *http.Request) { + s.ec[ext].init++ + respond(w, "{}") + }) + + mux.HandleFunc("/GraphDriver.Create", func(w http.ResponseWriter, r *http.Request) { + s.ec[ext].creations++ + + var req graphDriverRequest + if err := decReq(r.Body, &req, w); err != nil { + return + } + if err := driver.Create(req.ID, req.Parent, ""); err != nil { + respond(w, err) + return + } + respond(w, "{}") + }) + + mux.HandleFunc("/GraphDriver.Remove", func(w http.ResponseWriter, r *http.Request) { + s.ec[ext].removals++ + + var req graphDriverRequest + if err := decReq(r.Body, &req, w); err != nil { + return + } + + if err := driver.Remove(req.ID); err != nil { + respond(w, err) + return + } + respond(w, "{}") + }) + + mux.HandleFunc("/GraphDriver.Get", func(w http.ResponseWriter, r *http.Request) { + s.ec[ext].gets++ + + var req graphDriverRequest + if err := decReq(r.Body, &req, w); err != nil { + return + } + + dir, err := driver.Get(req.ID, req.MountLabel) + if err != nil { + respond(w, err) + return + } + respond(w, &graphDriverResponse{Dir: dir}) + }) + + mux.HandleFunc("/GraphDriver.Put", func(w http.ResponseWriter, r *http.Request) { + s.ec[ext].puts++ + + var req graphDriverRequest + if err := decReq(r.Body, &req, w); err != nil { + return + } + + if err := driver.Put(req.ID); err != nil { + respond(w, err) + return + } + respond(w, "{}") + }) + + mux.HandleFunc("/GraphDriver.Exists", func(w http.ResponseWriter, r *http.Request) { + s.ec[ext].exists++ + + var req graphDriverRequest + if err := decReq(r.Body, &req, w); err != nil { + return + } + respond(w, &graphDriverResponse{Exists: driver.Exists(req.ID)}) + }) + + mux.HandleFunc("/GraphDriver.Status", func(w http.ResponseWriter, r *http.Request) { + s.ec[ext].stats++ + respond(w, &graphDriverResponse{Status: driver.Status()}) + }) + + mux.HandleFunc("/GraphDriver.Cleanup", func(w http.ResponseWriter, r *http.Request) { + s.ec[ext].cleanups++ + err := driver.Cleanup() + if err != nil { + respond(w, err) + return + } + respond(w, `{}`) + }) + + mux.HandleFunc("/GraphDriver.GetMetadata", func(w http.ResponseWriter, r *http.Request) { + s.ec[ext].metadata++ + + var req graphDriverRequest + if err := decReq(r.Body, &req, w); err != nil { + return + } + + data, err := driver.GetMetadata(req.ID) + if err != nil { + respond(w, err) + return + } + respond(w, &graphDriverResponse{Metadata: data}) + }) + + mux.HandleFunc("/GraphDriver.Diff", func(w http.ResponseWriter, r *http.Request) { + s.ec[ext].diff++ + + var req graphDriverRequest + if err := decReq(r.Body, &req, w); err != nil { + return + } + + diff, err := driver.Diff(req.ID, req.Parent) + if err != nil { + respond(w, err) + return + } + io.Copy(w, diff) + }) + + mux.HandleFunc("/GraphDriver.Changes", func(w http.ResponseWriter, r *http.Request) { + s.ec[ext].changes++ + var req graphDriverRequest + if err := decReq(r.Body, &req, w); err != nil { + return + } + + changes, err := driver.Changes(req.ID, req.Parent) + if err != nil { + respond(w, err) + return + } + respond(w, &graphDriverResponse{Changes: changes}) + }) + + mux.HandleFunc("/GraphDriver.ApplyDiff", func(w http.ResponseWriter, r *http.Request) { + s.ec[ext].applydiff++ + var diff archive.Reader = r.Body + defer r.Body.Close() + + id := r.URL.Query().Get("id") + parent := r.URL.Query().Get("parent") + + if id == "" { + http.Error(w, fmt.Sprintf("missing id"), 409) + } + + size, err := driver.ApplyDiff(id, parent, diff) + if err != nil { + respond(w, err) + return + } + respond(w, &graphDriverResponse{Size: size}) + }) + + mux.HandleFunc("/GraphDriver.DiffSize", func(w http.ResponseWriter, r *http.Request) { + s.ec[ext].diffsize++ + + var req graphDriverRequest + if err := decReq(r.Body, &req, w); err != nil { + return + } + + size, err := driver.DiffSize(req.ID, req.Parent) + if err != nil { + respond(w, err) + return + } + respond(w, &graphDriverResponse{Size: size}) + }) + + err = os.MkdirAll("/etc/docker/plugins", 0755) + c.Assert(err, check.IsNil, check.Commentf("error creating /etc/docker/plugins")) + + specFile := "/etc/docker/plugins/" + name + "." + ext + err = ioutil.WriteFile(specFile, b, 0644) + c.Assert(err, check.IsNil, check.Commentf("error writing to %s", specFile)) +} + +func (s *DockerExternalGraphdriverSuite) TearDownSuite(c *check.C) { + s.server.Close() + s.jserver.Close() + + err := os.RemoveAll("/etc/docker/plugins") + c.Assert(err, check.IsNil, check.Commentf("error removing /etc/docker/plugins")) +} + +func (s *DockerExternalGraphdriverSuite) TestExternalGraphDriver(c *check.C) { + s.testExternalGraphDriver("test-external-graph-driver", "spec", c) + s.testExternalGraphDriver("json-external-graph-driver", "json", c) +} + +func (s *DockerExternalGraphdriverSuite) testExternalGraphDriver(name string, ext string, c *check.C) { + if err := s.d.StartWithBusybox("-s", name); err != nil { + b, _ := ioutil.ReadFile(s.d.LogFileName()) + c.Assert(err, check.IsNil, check.Commentf("\n%s", string(b))) + } + + out, err := s.d.Cmd("run", "-d", "--name=graphtest", "busybox", "sh", "-c", "echo hello > /hello") + c.Assert(err, check.IsNil, check.Commentf(out)) + + err = s.d.Restart("-s", name) + + out, err = s.d.Cmd("inspect", "--format='{{.GraphDriver.Name}}'", "graphtest") + c.Assert(err, check.IsNil, check.Commentf(out)) + c.Assert(strings.TrimSpace(out), check.Equals, name) + + out, err = s.d.Cmd("diff", "graphtest") + c.Assert(err, check.IsNil, check.Commentf(out)) + c.Assert(strings.Contains(out, "A /hello"), check.Equals, true) + + out, err = s.d.Cmd("rm", "-f", "graphtest") + c.Assert(err, check.IsNil, check.Commentf(out)) + + out, err = s.d.Cmd("info") + c.Assert(err, check.IsNil, check.Commentf(out)) + + err = s.d.Stop() + c.Assert(err, check.IsNil) + + // Don't check s.ec.exists, because the daemon no longer calls the + // Exists function. + c.Assert(s.ec[ext].activations, check.Equals, 2) + c.Assert(s.ec[ext].init, check.Equals, 2) + c.Assert(s.ec[ext].creations >= 1, check.Equals, true) + c.Assert(s.ec[ext].removals >= 1, check.Equals, true) + c.Assert(s.ec[ext].gets >= 1, check.Equals, true) + c.Assert(s.ec[ext].puts >= 1, check.Equals, true) + c.Assert(s.ec[ext].stats, check.Equals, 3) + c.Assert(s.ec[ext].cleanups, check.Equals, 2) + c.Assert(s.ec[ext].applydiff >= 1, check.Equals, true) + c.Assert(s.ec[ext].changes, check.Equals, 1) + c.Assert(s.ec[ext].diffsize, check.Equals, 0) + c.Assert(s.ec[ext].diff, check.Equals, 0) + c.Assert(s.ec[ext].metadata, check.Equals, 1) +} + +func (s *DockerExternalGraphdriverSuite) TestExternalGraphDriverPull(c *check.C) { + testRequires(c, Network) + c.Assert(s.d.Start(), check.IsNil) + + out, err := s.d.Cmd("pull", "busybox:latest") + c.Assert(err, check.IsNil, check.Commentf(out)) + + out, err = s.d.Cmd("run", "-d", "busybox", "top") + c.Assert(err, check.IsNil, check.Commentf(out)) +} diff --git a/integration-cli/docker_cli_help_test.go b/integration-cli/docker_cli_help_test.go new file mode 100644 index 00000000..93ccbeb8 --- /dev/null +++ b/integration-cli/docker_cli_help_test.go @@ -0,0 +1,298 @@ +package main + +import ( + "os/exec" + "runtime" + "strings" + "unicode" + + "github.com/docker/docker/pkg/homedir" + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestHelpTextVerify(c *check.C) { + testRequires(c, DaemonIsLinux) + // Make sure main help text fits within 80 chars and that + // on non-windows system we use ~ when possible (to shorten things). + // Test for HOME set to its default value and set to "/" on linux + // Yes on windows setting up an array and looping (right now) isn't + // necessary because we just have one value, but we'll need the + // array/loop on linux so we might as well set it up so that we can + // test any number of home dirs later on and all we need to do is + // modify the array - the rest of the testing infrastructure should work + homes := []string{homedir.Get()} + + // Non-Windows machines need to test for this special case of $HOME + if runtime.GOOS != "windows" { + homes = append(homes, "/") + } + + homeKey := homedir.Key() + baseEnvs := appendBaseEnv(true) + + // Remove HOME env var from list so we can add a new value later. + for i, env := range baseEnvs { + if strings.HasPrefix(env, homeKey+"=") { + baseEnvs = append(baseEnvs[:i], baseEnvs[i+1:]...) + break + } + } + + for _, home := range homes { + // Dup baseEnvs and add our new HOME value + newEnvs := make([]string, len(baseEnvs)+1) + copy(newEnvs, baseEnvs) + newEnvs[len(newEnvs)-1] = homeKey + "=" + home + + scanForHome := runtime.GOOS != "windows" && home != "/" + + // Check main help text to make sure its not over 80 chars + helpCmd := exec.Command(dockerBinary, "help") + helpCmd.Env = newEnvs + out, _, err := runCommandWithOutput(helpCmd) + c.Assert(err, checker.IsNil, check.Commentf(out)) + lines := strings.Split(out, "\n") + foundTooLongLine := false + for _, line := range lines { + if !foundTooLongLine && len(line) > 80 { + c.Logf("Line is too long:\n%s", line) + foundTooLongLine = true + } + // All lines should not end with a space + c.Assert(line, checker.Not(checker.HasSuffix), " ", check.Commentf("Line should not end with a space")) + + if scanForHome && strings.Contains(line, `=`+home) { + c.Fatalf("Line should use '%q' instead of %q:\n%s", homedir.GetShortcutString(), home, line) + } + if runtime.GOOS != "windows" { + i := strings.Index(line, homedir.GetShortcutString()) + if i >= 0 && i != len(line)-1 && line[i+1] != '/' { + c.Fatalf("Main help should not have used home shortcut:\n%s", line) + } + } + } + + // Make sure each cmd's help text fits within 90 chars and that + // on non-windows system we use ~ when possible (to shorten things). + // Pull the list of commands from the "Commands:" section of docker help + helpCmd = exec.Command(dockerBinary, "help") + helpCmd.Env = newEnvs + out, _, err = runCommandWithOutput(helpCmd) + c.Assert(err, checker.IsNil, check.Commentf(out)) + i := strings.Index(out, "Commands:") + c.Assert(i, checker.GreaterOrEqualThan, 0, check.Commentf("Missing 'Commands:' in:\n%s", out)) + + cmds := []string{} + // Grab all chars starting at "Commands:" + helpOut := strings.Split(out[i:], "\n") + // First line is just "Commands:" + if isLocalDaemon { + // Replace first line with "daemon" command since it's not part of the list of commands. + helpOut[0] = " daemon" + } else { + // Skip first line + helpOut = helpOut[1:] + } + + // Create the list of commands we want to test + cmdsToTest := []string{} + for _, cmd := range helpOut { + // Stop on blank line or non-idented line + if cmd == "" || !unicode.IsSpace(rune(cmd[0])) { + break + } + + // Grab just the first word of each line + cmd = strings.Split(strings.TrimSpace(cmd), " ")[0] + cmds = append(cmds, cmd) // Saving count for later + + cmdsToTest = append(cmdsToTest, cmd) + } + + // Add some 'two word' commands - would be nice to automatically + // calculate this list - somehow + cmdsToTest = append(cmdsToTest, "volume create") + cmdsToTest = append(cmdsToTest, "volume inspect") + cmdsToTest = append(cmdsToTest, "volume ls") + cmdsToTest = append(cmdsToTest, "volume rm") + + for _, cmd := range cmdsToTest { + var stderr string + + args := strings.Split(cmd+" --help", " ") + + // Check the full usage text + helpCmd := exec.Command(dockerBinary, args...) + helpCmd.Env = newEnvs + out, stderr, _, err = runCommandWithStdoutStderr(helpCmd) + c.Assert(len(stderr), checker.Equals, 0, check.Commentf("Error on %q help. non-empty stderr:%q", cmd, stderr)) + c.Assert(out, checker.Not(checker.HasSuffix), "\n\n", check.Commentf("Should not have blank line on %q\n", cmd)) + c.Assert(out, checker.Contains, "--help", check.Commentf("All commands should mention '--help'. Command '%v' did not.\n", cmd)) + + c.Assert(err, checker.IsNil, check.Commentf(out)) + + // Check each line for lots of stuff + lines := strings.Split(out, "\n") + for _, line := range lines { + c.Assert(len(line), checker.LessOrEqualThan, 107, check.Commentf("Help for %q is too long:\n%s", cmd, line)) + + if scanForHome && strings.Contains(line, `"`+home) { + c.Fatalf("Help for %q should use ~ instead of %q on:\n%s", + cmd, home, line) + } + i := strings.Index(line, "~") + if i >= 0 && i != len(line)-1 && line[i+1] != '/' { + c.Fatalf("Help for %q should not have used ~:\n%s", cmd, line) + } + + // If a line starts with 4 spaces then assume someone + // added a multi-line description for an option and we need + // to flag it + c.Assert(line, checker.Not(checker.HasPrefix), " ", check.Commentf("Help for %q should not have a multi-line option", cmd)) + + // Options should NOT end with a period + if strings.HasPrefix(line, " -") && strings.HasSuffix(line, ".") { + c.Fatalf("Help for %q should not end with a period: %s", cmd, line) + } + + // Options should NOT end with a space + c.Assert(line, checker.Not(checker.HasSuffix), " ", check.Commentf("Help for %q should not end with a space", cmd)) + + } + + // For each command make sure we generate an error + // if we give a bad arg + args = strings.Split(cmd+" --badArg", " ") + + out, _, err = dockerCmdWithError(args...) + c.Assert(err, checker.NotNil, check.Commentf(out)) + // Be really picky + c.Assert(stderr, checker.Not(checker.HasSuffix), "\n\n", check.Commentf("Should not have a blank line at the end of 'docker rm'\n")) + + // Now make sure that each command will print a short-usage + // (not a full usage - meaning no opts section) if we + // are missing a required arg or pass in a bad arg + + // These commands will never print a short-usage so don't test + noShortUsage := map[string]string{ + "images": "", + "login": "", + "logout": "", + "network": "", + "stats": "", + } + + if _, ok := noShortUsage[cmd]; !ok { + // For each command run it w/o any args. It will either return + // valid output or print a short-usage + var dCmd *exec.Cmd + var stdout, stderr string + var args []string + + // skipNoArgs are ones that we don't want to try w/o + // any args. Either because it'll hang the test or + // lead to incorrect test result (like false negative). + // Whatever the reason, skip trying to run w/o args and + // jump to trying with a bogus arg. + skipNoArgs := map[string]struct{}{ + "daemon": {}, + "events": {}, + "load": {}, + } + + ec := 0 + if _, ok := skipNoArgs[cmd]; !ok { + args = strings.Split(cmd, " ") + dCmd = exec.Command(dockerBinary, args...) + stdout, stderr, ec, err = runCommandWithStdoutStderr(dCmd) + } + + // If its ok w/o any args then try again with an arg + if ec == 0 { + args = strings.Split(cmd+" badArg", " ") + dCmd = exec.Command(dockerBinary, args...) + stdout, stderr, ec, err = runCommandWithStdoutStderr(dCmd) + } + + if len(stdout) != 0 || len(stderr) == 0 || ec == 0 || err == nil { + c.Fatalf("Bad output from %q\nstdout:%q\nstderr:%q\nec:%d\nerr:%q", args, stdout, stderr, ec, err) + } + // Should have just short usage + c.Assert(stderr, checker.Contains, "\nUsage:\t", check.Commentf("Missing short usage on %q\n", args)) + // But shouldn't have full usage + c.Assert(stderr, checker.Not(checker.Contains), "--help=false", check.Commentf("Should not have full usage on %q\n", args)) + c.Assert(stderr, checker.Not(checker.HasSuffix), "\n\n", check.Commentf("Should not have a blank line on %q\n", args)) + } + + } + + // Number of commands for standard release and experimental release + standard := 41 + experimental := 1 + expected := standard + experimental + if isLocalDaemon { + expected++ // for the daemon command + } + c.Assert(len(cmds), checker.LessOrEqualThan, expected, check.Commentf("Wrong # of cmds, it should be: %d\nThe list:\n%q", expected, cmds)) + } + +} + +func (s *DockerSuite) TestHelpExitCodesHelpOutput(c *check.C) { + testRequires(c, DaemonIsLinux) + // Test to make sure the exit code and output (stdout vs stderr) of + // various good and bad cases are what we expect + + // docker : stdout=all, stderr=empty, rc=0 + out, _, err := dockerCmdWithError() + c.Assert(err, checker.IsNil, check.Commentf(out)) + // Be really pick + c.Assert(out, checker.Not(checker.HasSuffix), "\n\n", check.Commentf("Should not have a blank line at the end of 'docker'\n")) + + // docker help: stdout=all, stderr=empty, rc=0 + out, _, err = dockerCmdWithError("help") + c.Assert(err, checker.IsNil, check.Commentf(out)) + // Be really pick + c.Assert(out, checker.Not(checker.HasSuffix), "\n\n", check.Commentf("Should not have a blank line at the end of 'docker help'\n")) + + // docker --help: stdout=all, stderr=empty, rc=0 + out, _, err = dockerCmdWithError("--help") + c.Assert(err, checker.IsNil, check.Commentf(out)) + // Be really pick + c.Assert(out, checker.Not(checker.HasSuffix), "\n\n", check.Commentf("Should not have a blank line at the end of 'docker --help'\n")) + + // docker inspect busybox: stdout=all, stderr=empty, rc=0 + // Just making sure stderr is empty on valid cmd + out, _, err = dockerCmdWithError("inspect", "busybox") + c.Assert(err, checker.IsNil, check.Commentf(out)) + // Be really pick + c.Assert(out, checker.Not(checker.HasSuffix), "\n\n", check.Commentf("Should not have a blank line at the end of 'docker inspect busyBox'\n")) + + // docker rm: stdout=empty, stderr=all, rc!=0 + // testing the min arg error msg + cmd := exec.Command(dockerBinary, "rm") + stdout, stderr, _, err := runCommandWithStdoutStderr(cmd) + c.Assert(err, checker.NotNil) + c.Assert(stdout, checker.Equals, "") + // Should not contain full help text but should contain info about + // # of args and Usage line + c.Assert(stderr, checker.Contains, "requires a minimum", check.Commentf("Missing # of args text from 'docker rm'\n")) + + // docker rm NoSuchContainer: stdout=empty, stderr=all, rc=0 + // testing to make sure no blank line on error + cmd = exec.Command(dockerBinary, "rm", "NoSuchContainer") + stdout, stderr, _, err = runCommandWithStdoutStderr(cmd) + c.Assert(err, checker.NotNil) + c.Assert(len(stderr), checker.Not(checker.Equals), 0) + c.Assert(stdout, checker.Equals, "") + // Be really picky + c.Assert(stderr, checker.Not(checker.HasSuffix), "\n\n", check.Commentf("Should not have a blank line at the end of 'docker rm'\n")) + + // docker BadCmd: stdout=empty, stderr=all, rc=0 + cmd = exec.Command(dockerBinary, "BadCmd") + stdout, stderr, _, err = runCommandWithStdoutStderr(cmd) + c.Assert(err, checker.NotNil) + c.Assert(stdout, checker.Equals, "") + c.Assert(stderr, checker.Equals, "docker: 'BadCmd' is not a docker command.\nSee 'docker --help'.\n", check.Commentf("Unexcepted output for 'docker badCmd'\n")) +} diff --git a/integration-cli/docker_cli_history_test.go b/integration-cli/docker_cli_history_test.go new file mode 100644 index 00000000..0ee1c46b --- /dev/null +++ b/integration-cli/docker_cli_history_test.go @@ -0,0 +1,125 @@ +package main + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +// This is a heisen-test. Because the created timestamp of images and the behavior of +// sort is not predictable it doesn't always fail. +func (s *DockerSuite) TestBuildHistory(c *check.C) { + testRequires(c, DaemonIsLinux) // TODO Windows: This test passes on Windows, + // but currently adds a disproportionate amount of time for the value it has. + // Removing it from Windows CI for now, but this will be revisited in the + // TP5 timeframe when perf is better. + name := "testbuildhistory" + _, err := buildImage(name, `FROM `+minimalBaseImage()+` +LABEL label.A="A" +LABEL label.B="B" +LABEL label.C="C" +LABEL label.D="D" +LABEL label.E="E" +LABEL label.F="F" +LABEL label.G="G" +LABEL label.H="H" +LABEL label.I="I" +LABEL label.J="J" +LABEL label.K="K" +LABEL label.L="L" +LABEL label.M="M" +LABEL label.N="N" +LABEL label.O="O" +LABEL label.P="P" +LABEL label.Q="Q" +LABEL label.R="R" +LABEL label.S="S" +LABEL label.T="T" +LABEL label.U="U" +LABEL label.V="V" +LABEL label.W="W" +LABEL label.X="X" +LABEL label.Y="Y" +LABEL label.Z="Z"`, + true) + + c.Assert(err, checker.IsNil) + + out, _ := dockerCmd(c, "history", "testbuildhistory") + actualValues := strings.Split(out, "\n")[1:27] + expectedValues := [26]string{"Z", "Y", "X", "W", "V", "U", "T", "S", "R", "Q", "P", "O", "N", "M", "L", "K", "J", "I", "H", "G", "F", "E", "D", "C", "B", "A"} + + for i := 0; i < 26; i++ { + echoValue := fmt.Sprintf("LABEL label.%s=%s", expectedValues[i], expectedValues[i]) + actualValue := actualValues[i] + c.Assert(actualValue, checker.Contains, echoValue) + } + +} + +func (s *DockerSuite) TestHistoryExistentImage(c *check.C) { + dockerCmd(c, "history", "busybox") +} + +func (s *DockerSuite) TestHistoryNonExistentImage(c *check.C) { + _, _, err := dockerCmdWithError("history", "testHistoryNonExistentImage") + c.Assert(err, checker.NotNil, check.Commentf("history on a non-existent image should fail.")) +} + +func (s *DockerSuite) TestHistoryImageWithComment(c *check.C) { + name := "testhistoryimagewithcomment" + + // make a image through docker commit [ -m messages ] + + dockerCmd(c, "run", "--name", name, "busybox", "true") + dockerCmd(c, "wait", name) + + comment := "This_is_a_comment" + dockerCmd(c, "commit", "-m="+comment, name, name) + + // test docker history to check comment messages + + out, _ := dockerCmd(c, "history", name) + outputTabs := strings.Fields(strings.Split(out, "\n")[1]) + actualValue := outputTabs[len(outputTabs)-1] + c.Assert(actualValue, checker.Contains, comment) +} + +func (s *DockerSuite) TestHistoryHumanOptionFalse(c *check.C) { + out, _ := dockerCmd(c, "history", "--human=false", "busybox") + lines := strings.Split(out, "\n") + sizeColumnRegex, _ := regexp.Compile("SIZE +") + indices := sizeColumnRegex.FindStringIndex(lines[0]) + startIndex := indices[0] + endIndex := indices[1] + for i := 1; i < len(lines)-1; i++ { + if endIndex > len(lines[i]) { + endIndex = len(lines[i]) + } + sizeString := lines[i][startIndex:endIndex] + + _, err := strconv.Atoi(strings.TrimSpace(sizeString)) + c.Assert(err, checker.IsNil, check.Commentf("The size '%s' was not an Integer", sizeString)) + } +} + +func (s *DockerSuite) TestHistoryHumanOptionTrue(c *check.C) { + out, _ := dockerCmd(c, "history", "--human=true", "busybox") + lines := strings.Split(out, "\n") + sizeColumnRegex, _ := regexp.Compile("SIZE +") + humanSizeRegexRaw := "\\d+.*B" // Matches human sizes like 10 MB, 3.2 KB, etc + indices := sizeColumnRegex.FindStringIndex(lines[0]) + startIndex := indices[0] + endIndex := indices[1] + for i := 1; i < len(lines)-1; i++ { + if endIndex > len(lines[i]) { + endIndex = len(lines[i]) + } + sizeString := lines[i][startIndex:endIndex] + c.Assert(strings.TrimSpace(sizeString), checker.Matches, humanSizeRegexRaw, check.Commentf("The size '%s' was not in human format", sizeString)) + } +} diff --git a/integration-cli/docker_cli_images_test.go b/integration-cli/docker_cli_images_test.go new file mode 100644 index 00000000..dbceddf1 --- /dev/null +++ b/integration-cli/docker_cli_images_test.go @@ -0,0 +1,290 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "sort" + "strings" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/stringid" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestImagesEnsureImageIsListed(c *check.C) { + testRequires(c, DaemonIsLinux) + imagesOut, _ := dockerCmd(c, "images") + c.Assert(imagesOut, checker.Contains, "busybox") +} + +func (s *DockerSuite) TestImagesEnsureImageWithTagIsListed(c *check.C) { + testRequires(c, DaemonIsLinux) + + name := "imagewithtag" + dockerCmd(c, "tag", "busybox", name+":v1") + dockerCmd(c, "tag", "busybox", name+":v1v1") + dockerCmd(c, "tag", "busybox", name+":v2") + + imagesOut, _ := dockerCmd(c, "images", name+":v1") + c.Assert(imagesOut, checker.Contains, name) + c.Assert(imagesOut, checker.Contains, "v1") + c.Assert(imagesOut, checker.Not(checker.Contains), "v2") + c.Assert(imagesOut, checker.Not(checker.Contains), "v1v1") + + imagesOut, _ = dockerCmd(c, "images", name) + c.Assert(imagesOut, checker.Contains, name) + c.Assert(imagesOut, checker.Contains, "v1") + c.Assert(imagesOut, checker.Contains, "v1v1") + c.Assert(imagesOut, checker.Contains, "v2") +} + +func (s *DockerSuite) TestImagesEnsureImageWithBadTagIsNotListed(c *check.C) { + imagesOut, _ := dockerCmd(c, "images", "busybox:nonexistent") + c.Assert(imagesOut, checker.Not(checker.Contains), "busybox") +} + +func (s *DockerSuite) TestImagesOrderedByCreationDate(c *check.C) { + testRequires(c, DaemonIsLinux) + id1, err := buildImage("order:test_a", + `FROM scratch + MAINTAINER dockerio1`, true) + c.Assert(err, checker.IsNil) + time.Sleep(1 * time.Second) + id2, err := buildImage("order:test_c", + `FROM scratch + MAINTAINER dockerio2`, true) + c.Assert(err, checker.IsNil) + time.Sleep(1 * time.Second) + id3, err := buildImage("order:test_b", + `FROM scratch + MAINTAINER dockerio3`, true) + c.Assert(err, checker.IsNil) + + out, _ := dockerCmd(c, "images", "-q", "--no-trunc") + imgs := strings.Split(out, "\n") + c.Assert(imgs[0], checker.Equals, id3, check.Commentf("First image must be %s, got %s", id3, imgs[0])) + c.Assert(imgs[1], checker.Equals, id2, check.Commentf("First image must be %s, got %s", id2, imgs[1])) + c.Assert(imgs[2], checker.Equals, id1, check.Commentf("First image must be %s, got %s", id1, imgs[2])) +} + +func (s *DockerSuite) TestImagesErrorWithInvalidFilterNameTest(c *check.C) { + out, _, err := dockerCmdWithError("images", "-f", "FOO=123") + c.Assert(err, checker.NotNil) + c.Assert(out, checker.Contains, "Invalid filter") +} + +func (s *DockerSuite) TestImagesFilterLabelMatch(c *check.C) { + testRequires(c, DaemonIsLinux) + imageName1 := "images_filter_test1" + imageName2 := "images_filter_test2" + imageName3 := "images_filter_test3" + image1ID, err := buildImage(imageName1, + `FROM scratch + LABEL match me`, true) + c.Assert(err, check.IsNil) + + image2ID, err := buildImage(imageName2, + `FROM scratch + LABEL match="me too"`, true) + c.Assert(err, check.IsNil) + + image3ID, err := buildImage(imageName3, + `FROM scratch + LABEL nomatch me`, true) + c.Assert(err, check.IsNil) + + out, _ := dockerCmd(c, "images", "--no-trunc", "-q", "-f", "label=match") + out = strings.TrimSpace(out) + c.Assert(out, check.Matches, fmt.Sprintf("[\\s\\w:]*%s[\\s\\w:]*", image1ID)) + c.Assert(out, check.Matches, fmt.Sprintf("[\\s\\w:]*%s[\\s\\w:]*", image2ID)) + c.Assert(out, check.Not(check.Matches), fmt.Sprintf("[\\s\\w:]*%s[\\s\\w:]*", image3ID)) + + out, _ = dockerCmd(c, "images", "--no-trunc", "-q", "-f", "label=match=me too") + out = strings.TrimSpace(out) + c.Assert(out, check.Equals, image2ID) +} + +// Regression : #15659 +func (s *DockerSuite) TestImagesFilterLabelWithCommit(c *check.C) { + // Create a container + dockerCmd(c, "run", "--name", "bar", "busybox", "/bin/sh") + // Commit with labels "using changes" + out, _ := dockerCmd(c, "commit", "-c", "LABEL foo.version=1.0.0-1", "-c", "LABEL foo.name=bar", "-c", "LABEL foo.author=starlord", "bar", "bar:1.0.0-1") + imageID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "images", "--no-trunc", "-q", "-f", "label=foo.version=1.0.0-1") + out = strings.TrimSpace(out) + c.Assert(out, check.Equals, imageID) +} + +func (s *DockerSuite) TestImagesFilterSpaceTrimCase(c *check.C) { + testRequires(c, DaemonIsLinux) + imageName := "images_filter_test" + buildImage(imageName, + `FROM scratch + RUN touch /test/foo + RUN touch /test/bar + RUN touch /test/baz`, true) + + filters := []string{ + "dangling=true", + "Dangling=true", + " dangling=true", + "dangling=true ", + "dangling = true", + } + + imageListings := make([][]string, 5, 5) + for idx, filter := range filters { + out, _ := dockerCmd(c, "images", "-q", "-f", filter) + listing := strings.Split(out, "\n") + sort.Strings(listing) + imageListings[idx] = listing + } + + for idx, listing := range imageListings { + if idx < 4 && !reflect.DeepEqual(listing, imageListings[idx+1]) { + for idx, errListing := range imageListings { + fmt.Printf("out %d", idx) + for _, image := range errListing { + fmt.Print(image) + } + fmt.Print("") + } + c.Fatalf("All output must be the same") + } + } +} + +func (s *DockerSuite) TestImagesEnsureDanglingImageOnlyListedOnce(c *check.C) { + testRequires(c, DaemonIsLinux) + // create container 1 + out, _ := dockerCmd(c, "run", "-d", "busybox", "true") + containerID1 := strings.TrimSpace(out) + + // tag as foobox + out, _ = dockerCmd(c, "commit", containerID1, "foobox") + imageID := stringid.TruncateID(strings.TrimSpace(out)) + + // overwrite the tag, making the previous image dangling + dockerCmd(c, "tag", "-f", "busybox", "foobox") + + out, _ = dockerCmd(c, "images", "-q", "-f", "dangling=true") + // Expect one dangling image + c.Assert(strings.Count(out, imageID), checker.Equals, 1) + + out, _ = dockerCmd(c, "images", "-q", "-f", "dangling=false") + //dangling=false would not include dangling images + c.Assert(out, checker.Not(checker.Contains), imageID) + + out, _ = dockerCmd(c, "images") + //docker images still include dangling images + c.Assert(out, checker.Contains, imageID) + +} + +func (s *DockerSuite) TestImagesWithIncorrectFilter(c *check.C) { + out, _, err := dockerCmdWithError("images", "-f", "dangling=invalid") + c.Assert(err, check.NotNil) + c.Assert(out, checker.Contains, "Invalid filter") +} + +func (s *DockerSuite) TestImagesEnsureOnlyHeadsImagesShown(c *check.C) { + testRequires(c, DaemonIsLinux) + + dockerfile := ` + FROM scratch + MAINTAINER docker + ENV foo bar` + + head, out, err := buildImageWithOut("scratch-image", dockerfile, false) + c.Assert(err, check.IsNil) + + // this is just the output of docker build + // we're interested in getting the image id of the MAINTAINER instruction + // and that's located at output, line 5, from 7 to end + split := strings.Split(out, "\n") + intermediate := strings.TrimSpace(split[5][7:]) + + out, _ = dockerCmd(c, "images") + // images shouldn't show non-heads images + c.Assert(out, checker.Not(checker.Contains), intermediate) + // images should contain final built images + c.Assert(out, checker.Contains, stringid.TruncateID(head)) +} + +func (s *DockerSuite) TestImagesEnsureImagesFromScratchShown(c *check.C) { + testRequires(c, DaemonIsLinux) + + dockerfile := ` + FROM scratch + MAINTAINER docker` + + id, _, err := buildImageWithOut("scratch-image", dockerfile, false) + c.Assert(err, check.IsNil) + + out, _ := dockerCmd(c, "images") + // images should contain images built from scratch + c.Assert(out, checker.Contains, stringid.TruncateID(id)) +} + +// #18181 +func (s *DockerSuite) TestImagesFilterNameWithPort(c *check.C) { + tag := "a.b.c.d:5000/hello" + dockerCmd(c, "tag", "busybox", tag) + out, _ := dockerCmd(c, "images", tag) + c.Assert(out, checker.Contains, tag) + + out, _ = dockerCmd(c, "images", tag+":latest") + c.Assert(out, checker.Contains, tag) + + out, _ = dockerCmd(c, "images", tag+":no-such-tag") + c.Assert(out, checker.Not(checker.Contains), tag) +} + +func (s *DockerSuite) TestImagesFormat(c *check.C) { + // testRequires(c, DaemonIsLinux) + tag := "myimage" + dockerCmd(c, "tag", "busybox", tag+":v1") + dockerCmd(c, "tag", "busybox", tag+":v2") + + out, _ := dockerCmd(c, "images", "--format", "{{.Repository}}", tag) + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + + expected := []string{"myimage", "myimage"} + var names []string + for _, l := range lines { + names = append(names, l) + } + c.Assert(expected, checker.DeepEquals, names, check.Commentf("Expected array with truncated names: %v, got: %v", expected, names)) +} + +// ImagesDefaultFormatAndQuiet +func (s *DockerSuite) TestImagesFormatDefaultFormat(c *check.C) { + testRequires(c, DaemonIsLinux) + + // create container 1 + out, _ := dockerCmd(c, "run", "-d", "busybox", "true") + containerID1 := strings.TrimSpace(out) + + // tag as foobox + out, _ = dockerCmd(c, "commit", containerID1, "myimage") + imageID := stringid.TruncateID(strings.TrimSpace(out)) + + config := `{ + "imagesFormat": "{{ .ID }} default" +}` + d, err := ioutil.TempDir("", "integration-cli-") + c.Assert(err, checker.IsNil) + defer os.RemoveAll(d) + + err = ioutil.WriteFile(filepath.Join(d, "config.json"), []byte(config), 0644) + c.Assert(err, checker.IsNil) + + out, _ = dockerCmd(c, "--config", d, "images", "-q", "myimage") + c.Assert(out, checker.Equals, imageID+"\n", check.Commentf("Expected to print only the image id, got %v\n", out)) +} diff --git a/integration-cli/docker_cli_import_test.go b/integration-cli/docker_cli_import_test.go new file mode 100644 index 00000000..9420dafa --- /dev/null +++ b/integration-cli/docker_cli_import_test.go @@ -0,0 +1,123 @@ +package main + +import ( + "bufio" + "compress/gzip" + "io/ioutil" + "os" + "os/exec" + "regexp" + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestImportDisplay(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "true") + cleanedContainerID := strings.TrimSpace(out) + + out, _, err := runCommandPipelineWithOutput( + exec.Command(dockerBinary, "export", cleanedContainerID), + exec.Command(dockerBinary, "import", "-"), + ) + c.Assert(err, checker.IsNil) + + c.Assert(out, checker.Count, "\n", 1, check.Commentf("display is expected 1 '\\n' but didn't")) + + image := strings.TrimSpace(out) + out, _ = dockerCmd(c, "run", "--rm", image, "true") + c.Assert(out, checker.Equals, "", check.Commentf("command output should've been nothing.")) +} + +func (s *DockerSuite) TestImportBadURL(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _, err := dockerCmdWithError("import", "http://nourl/bad") + c.Assert(err, checker.NotNil, check.Commentf("import was supposed to fail but didn't")) + c.Assert(out, checker.Contains, "dial tcp", check.Commentf("expected an error msg but didn't get one")) +} + +func (s *DockerSuite) TestImportFile(c *check.C) { + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "--name", "test-import", "busybox", "true") + + temporaryFile, err := ioutil.TempFile("", "exportImportTest") + c.Assert(err, checker.IsNil, check.Commentf("failed to create temporary file")) + defer os.Remove(temporaryFile.Name()) + + runCmd := exec.Command(dockerBinary, "export", "test-import") + runCmd.Stdout = bufio.NewWriter(temporaryFile) + + _, err = runCommand(runCmd) + c.Assert(err, checker.IsNil, check.Commentf("failed to export a container")) + + out, _ := dockerCmd(c, "import", temporaryFile.Name()) + c.Assert(out, checker.Count, "\n", 1, check.Commentf("display is expected 1 '\\n' but didn't")) + image := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "run", "--rm", image, "true") + c.Assert(out, checker.Equals, "", check.Commentf("command output should've been nothing.")) +} + +func (s *DockerSuite) TestImportGzipped(c *check.C) { + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "--name", "test-import", "busybox", "true") + + temporaryFile, err := ioutil.TempFile("", "exportImportTest") + c.Assert(err, checker.IsNil, check.Commentf("failed to create temporary file")) + defer os.Remove(temporaryFile.Name()) + + runCmd := exec.Command(dockerBinary, "export", "test-import") + w := gzip.NewWriter(temporaryFile) + runCmd.Stdout = w + + _, err = runCommand(runCmd) + c.Assert(err, checker.IsNil, check.Commentf("failed to export a container")) + err = w.Close() + c.Assert(err, checker.IsNil, check.Commentf("failed to close gzip writer")) + temporaryFile.Close() + out, _ := dockerCmd(c, "import", temporaryFile.Name()) + c.Assert(out, checker.Count, "\n", 1, check.Commentf("display is expected 1 '\\n' but didn't")) + image := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "run", "--rm", image, "true") + c.Assert(out, checker.Equals, "", check.Commentf("command output should've been nothing.")) +} + +func (s *DockerSuite) TestImportFileWithMessage(c *check.C) { + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "--name", "test-import", "busybox", "true") + + temporaryFile, err := ioutil.TempFile("", "exportImportTest") + c.Assert(err, checker.IsNil, check.Commentf("failed to create temporary file")) + defer os.Remove(temporaryFile.Name()) + + runCmd := exec.Command(dockerBinary, "export", "test-import") + runCmd.Stdout = bufio.NewWriter(temporaryFile) + + _, err = runCommand(runCmd) + c.Assert(err, checker.IsNil, check.Commentf("failed to export a container")) + + message := "Testing commit message" + out, _ := dockerCmd(c, "import", "-m", message, temporaryFile.Name()) + c.Assert(out, checker.Count, "\n", 1, check.Commentf("display is expected 1 '\\n' but didn't")) + image := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "history", image) + split := strings.Split(out, "\n") + + c.Assert(split, checker.HasLen, 3, check.Commentf("expected 3 lines from image history")) + r := regexp.MustCompile("[\\s]{2,}") + split = r.Split(split[1], -1) + + c.Assert(message, checker.Equals, split[3], check.Commentf("didn't get expected value in commit message")) + + out, _ = dockerCmd(c, "run", "--rm", image, "true") + c.Assert(out, checker.Equals, "", check.Commentf("command output should've been nothing")) +} + +func (s *DockerSuite) TestImportFileNonExistentFile(c *check.C) { + _, _, err := dockerCmdWithError("import", "example.com/myImage.tar") + c.Assert(err, checker.NotNil, check.Commentf("import non-existing file must failed")) +} diff --git a/integration-cli/docker_cli_info_test.go b/integration-cli/docker_cli_info_test.go new file mode 100644 index 00000000..dd236945 --- /dev/null +++ b/integration-cli/docker_cli_info_test.go @@ -0,0 +1,166 @@ +package main + +import ( + "fmt" + "net" + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/utils" + "github.com/go-check/check" +) + +// ensure docker info succeeds +func (s *DockerSuite) TestInfoEnsureSucceeds(c *check.C) { + out, _ := dockerCmd(c, "info") + + // always shown fields + stringsToCheck := []string{ + "ID:", + "Containers:", + " Running:", + " Paused:", + " Stopped:", + "Images:", + "OSType:", + "Architecture:", + "Logging Driver:", + "Operating System:", + "CPUs:", + "Total Memory:", + "Kernel Version:", + "Storage Driver:", + "Volume:", + "Network:", + } + + if utils.ExperimentalBuild() { + stringsToCheck = append(stringsToCheck, "Experimental: true") + } + + for _, linePrefix := range stringsToCheck { + c.Assert(out, checker.Contains, linePrefix, check.Commentf("couldn't find string %v in output", linePrefix)) + } +} + +// TestInfoDiscoveryBackend verifies that a daemon run with `--cluster-advertise` and +// `--cluster-store` properly show the backend's endpoint in info output. +func (s *DockerSuite) TestInfoDiscoveryBackend(c *check.C) { + testRequires(c, SameHostDaemon, DaemonIsLinux) + + d := NewDaemon(c) + discoveryBackend := "consul://consuladdr:consulport/some/path" + discoveryAdvertise := "1.1.1.1:2375" + err := d.Start(fmt.Sprintf("--cluster-store=%s", discoveryBackend), fmt.Sprintf("--cluster-advertise=%s", discoveryAdvertise)) + c.Assert(err, checker.IsNil) + defer d.Stop() + + out, err := d.Cmd("info") + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Contains, fmt.Sprintf("Cluster store: %s\n", discoveryBackend)) + c.Assert(out, checker.Contains, fmt.Sprintf("Cluster advertise: %s\n", discoveryAdvertise)) +} + +// TestInfoDiscoveryInvalidAdvertise verifies that a daemon run with +// an invalid `--cluster-advertise` configuration +func (s *DockerSuite) TestInfoDiscoveryInvalidAdvertise(c *check.C) { + testRequires(c, SameHostDaemon, DaemonIsLinux) + + d := NewDaemon(c) + discoveryBackend := "consul://consuladdr:consulport/some/path" + + // --cluster-advertise with an invalid string is an error + err := d.Start(fmt.Sprintf("--cluster-store=%s", discoveryBackend), "--cluster-advertise=invalid") + c.Assert(err, checker.Not(checker.IsNil)) + + // --cluster-advertise without --cluster-store is also an error + err = d.Start("--cluster-advertise=1.1.1.1:2375") + c.Assert(err, checker.Not(checker.IsNil)) +} + +// TestInfoDiscoveryAdvertiseInterfaceName verifies that a daemon run with `--cluster-advertise` +// configured with interface name properly show the advertise ip-address in info output. +func (s *DockerSuite) TestInfoDiscoveryAdvertiseInterfaceName(c *check.C) { + testRequires(c, SameHostDaemon, Network, DaemonIsLinux) + + d := NewDaemon(c) + discoveryBackend := "consul://consuladdr:consulport/some/path" + discoveryAdvertise := "eth0" + + err := d.Start(fmt.Sprintf("--cluster-store=%s", discoveryBackend), fmt.Sprintf("--cluster-advertise=%s:2375", discoveryAdvertise)) + c.Assert(err, checker.IsNil) + defer d.Stop() + + iface, err := net.InterfaceByName(discoveryAdvertise) + c.Assert(err, checker.IsNil) + addrs, err := iface.Addrs() + c.Assert(err, checker.IsNil) + c.Assert(len(addrs), checker.GreaterThan, 0) + ip, _, err := net.ParseCIDR(addrs[0].String()) + c.Assert(err, checker.IsNil) + + out, err := d.Cmd("info") + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Contains, fmt.Sprintf("Cluster store: %s\n", discoveryBackend)) + c.Assert(out, checker.Contains, fmt.Sprintf("Cluster advertise: %s:2375\n", ip.String())) +} + +func (s *DockerSuite) TestInfoDisplaysRunningContainers(c *check.C) { + testRequires(c, DaemonIsLinux) + + dockerCmd(c, "run", "-d", "busybox", "top") + out, _ := dockerCmd(c, "info") + c.Assert(out, checker.Contains, fmt.Sprintf("Containers: %d\n", 1)) + c.Assert(out, checker.Contains, fmt.Sprintf(" Running: %d\n", 1)) + c.Assert(out, checker.Contains, fmt.Sprintf(" Paused: %d\n", 0)) + c.Assert(out, checker.Contains, fmt.Sprintf(" Stopped: %d\n", 0)) +} + +func (s *DockerSuite) TestInfoDisplaysPausedContainers(c *check.C) { + testRequires(c, DaemonIsLinux) + + out, _ := dockerCmd(c, "run", "-d", "busybox", "top") + cleanedContainerID := strings.TrimSpace(out) + + dockerCmd(c, "pause", cleanedContainerID) + + out, _ = dockerCmd(c, "info") + c.Assert(out, checker.Contains, fmt.Sprintf("Containers: %d\n", 1)) + c.Assert(out, checker.Contains, fmt.Sprintf(" Running: %d\n", 0)) + c.Assert(out, checker.Contains, fmt.Sprintf(" Paused: %d\n", 1)) + c.Assert(out, checker.Contains, fmt.Sprintf(" Stopped: %d\n", 0)) +} + +func (s *DockerSuite) TestInfoDisplaysStoppedContainers(c *check.C) { + testRequires(c, DaemonIsLinux) + + out, _ := dockerCmd(c, "run", "-d", "busybox", "top") + cleanedContainerID := strings.TrimSpace(out) + + dockerCmd(c, "stop", cleanedContainerID) + + out, _ = dockerCmd(c, "info") + c.Assert(out, checker.Contains, fmt.Sprintf("Containers: %d\n", 1)) + c.Assert(out, checker.Contains, fmt.Sprintf(" Running: %d\n", 0)) + c.Assert(out, checker.Contains, fmt.Sprintf(" Paused: %d\n", 0)) + c.Assert(out, checker.Contains, fmt.Sprintf(" Stopped: %d\n", 1)) +} + +func (s *DockerSuite) TestInfoDebug(c *check.C) { + testRequires(c, SameHostDaemon, DaemonIsLinux) + + d := NewDaemon(c) + err := d.Start("--debug") + c.Assert(err, checker.IsNil) + defer d.Stop() + + out, err := d.Cmd("--debug", "info") + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Contains, "Debug mode (client): true\n") + c.Assert(out, checker.Contains, "Debug mode (server): true\n") + c.Assert(out, checker.Contains, "File Descriptors") + c.Assert(out, checker.Contains, "Goroutines") + c.Assert(out, checker.Contains, "System Time") + c.Assert(out, checker.Contains, "EventsListeners") + c.Assert(out, checker.Contains, "Docker Root Dir") +} diff --git a/integration-cli/docker_cli_inspect_experimental_test.go b/integration-cli/docker_cli_inspect_experimental_test.go new file mode 100644 index 00000000..0d9a261d --- /dev/null +++ b/integration-cli/docker_cli_inspect_experimental_test.go @@ -0,0 +1,33 @@ +// +build experimental + +package main + +import ( + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/engine-api/types" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestInspectNamedMountPoint(c *check.C) { + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "-d", "--name", "test", "-v", "data:/data", "busybox", "cat") + + vol := inspectFieldJSON(c, "test", "Mounts") + + var mp []types.MountPoint + err := unmarshalJSON([]byte(vol), &mp) + c.Assert(err, checker.IsNil) + + c.Assert(mp, checker.HasLen, 1, check.Commentf("Expected 1 mount point")) + + m := mp[0] + c.Assert(m.Name, checker.Equals, "data", check.Commentf("Expected name data")) + + c.Assert(m.Driver, checker.Equals, "local", check.Commentf("Expected driver local")) + + c.Assert(m.Source, checker.Not(checker.Equals), "", check.Commentf("Expected source to not be empty")) + + c.Assert(m.RW, checker.Equals, true) + + c.Assert(m.Destination, checker.Equals, "/data", check.Commentf("Expected destination /data")) +} diff --git a/integration-cli/docker_cli_inspect_test.go b/integration-cli/docker_cli_inspect_test.go new file mode 100644 index 00000000..29614537 --- /dev/null +++ b/integration-cli/docker_cli_inspect_test.go @@ -0,0 +1,393 @@ +package main + +import ( + "encoding/json" + "fmt" + "os/exec" + "strconv" + "strings" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/container" + "github.com/go-check/check" +) + +func checkValidGraphDriver(c *check.C, name string) { + if name != "devicemapper" && name != "overlay" && name != "vfs" && name != "zfs" && name != "btrfs" && name != "aufs" { + c.Fatalf("%v is not a valid graph driver name", name) + } +} + +func (s *DockerSuite) TestInspectImage(c *check.C) { + testRequires(c, DaemonIsLinux) + imageTest := "emptyfs" + // It is important that this ID remain stable. If a code change causes + // it to be different, this is equivalent to a cache bust when pulling + // a legacy-format manifest. If the check at the end of this function + // fails, fix the difference in the image serialization instead of + // updating this hash. + imageTestID := "sha256:11f64303f0f7ffdc71f001788132bca5346831939a956e3e975c93267d89a16d" + id := inspectField(c, imageTest, "Id") + + c.Assert(id, checker.Equals, imageTestID) +} + +func (s *DockerSuite) TestInspectInt64(c *check.C) { + testRequires(c, DaemonIsLinux) + + dockerCmd(c, "run", "-d", "-m=300M", "--name", "inspectTest", "busybox", "true") + inspectOut := inspectField(c, "inspectTest", "HostConfig.Memory") + c.Assert(inspectOut, checker.Equals, "314572800") +} + +func (s *DockerSuite) TestInspectDefault(c *check.C) { + testRequires(c, DaemonIsLinux) + //Both the container and image are named busybox. docker inspect will fetch the container JSON. + //If the container JSON is not available, it will go for the image JSON. + + out, _ := dockerCmd(c, "run", "--name=busybox", "-d", "busybox", "true") + containerID := strings.TrimSpace(out) + + inspectOut := inspectField(c, "busybox", "Id") + c.Assert(strings.TrimSpace(inspectOut), checker.Equals, containerID) +} + +func (s *DockerSuite) TestInspectStatus(c *check.C) { + defer unpauseAllContainers() + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "top") + out = strings.TrimSpace(out) + + inspectOut := inspectField(c, out, "State.Status") + c.Assert(inspectOut, checker.Equals, "running") + + dockerCmd(c, "pause", out) + inspectOut = inspectField(c, out, "State.Status") + c.Assert(inspectOut, checker.Equals, "paused") + + dockerCmd(c, "unpause", out) + inspectOut = inspectField(c, out, "State.Status") + c.Assert(inspectOut, checker.Equals, "running") + + dockerCmd(c, "stop", out) + inspectOut = inspectField(c, out, "State.Status") + c.Assert(inspectOut, checker.Equals, "exited") + +} + +func (s *DockerSuite) TestInspectTypeFlagContainer(c *check.C) { + testRequires(c, DaemonIsLinux) + //Both the container and image are named busybox. docker inspect will fetch container + //JSON State.Running field. If the field is true, it's a container. + + dockerCmd(c, "run", "--name=busybox", "-d", "busybox", "top") + + formatStr := "--format='{{.State.Running}}'" + out, _ := dockerCmd(c, "inspect", "--type=container", formatStr, "busybox") + c.Assert(out, checker.Equals, "true\n") // not a container JSON +} + +func (s *DockerSuite) TestInspectTypeFlagWithNoContainer(c *check.C) { + testRequires(c, DaemonIsLinux) + //Run this test on an image named busybox. docker inspect will try to fetch container + //JSON. Since there is no container named busybox and --type=container, docker inspect will + //not try to get the image JSON. It will throw an error. + + dockerCmd(c, "run", "-d", "busybox", "true") + + _, _, err := dockerCmdWithError("inspect", "--type=container", "busybox") + // docker inspect should fail, as there is no container named busybox + c.Assert(err, checker.NotNil) +} + +func (s *DockerSuite) TestInspectTypeFlagWithImage(c *check.C) { + testRequires(c, DaemonIsLinux) + //Both the container and image are named busybox. docker inspect will fetch image + //JSON as --type=image. if there is no image with name busybox, docker inspect + //will throw an error. + + dockerCmd(c, "run", "--name=busybox", "-d", "busybox", "true") + + out, _ := dockerCmd(c, "inspect", "--type=image", "busybox") + c.Assert(out, checker.Not(checker.Contains), "State") // not an image JSON +} + +func (s *DockerSuite) TestInspectTypeFlagWithInvalidValue(c *check.C) { + testRequires(c, DaemonIsLinux) + //Both the container and image are named busybox. docker inspect will fail + //as --type=foobar is not a valid value for the flag. + + dockerCmd(c, "run", "--name=busybox", "-d", "busybox", "true") + + out, exitCode, err := dockerCmdWithError("inspect", "--type=foobar", "busybox") + c.Assert(err, checker.NotNil, check.Commentf("%s", exitCode)) + c.Assert(exitCode, checker.Equals, 1, check.Commentf("%s", err)) + c.Assert(out, checker.Contains, "not a valid value for --type") +} + +func (s *DockerSuite) TestInspectImageFilterInt(c *check.C) { + testRequires(c, DaemonIsLinux) + imageTest := "emptyfs" + out := inspectField(c, imageTest, "Size") + + size, err := strconv.Atoi(out) + c.Assert(err, checker.IsNil, check.Commentf("failed to inspect size of the image: %s, %v", out, err)) + + //now see if the size turns out to be the same + formatStr := fmt.Sprintf("--format='{{eq .Size %d}}'", size) + out, _ = dockerCmd(c, "inspect", formatStr, imageTest) + result, err := strconv.ParseBool(strings.TrimSuffix(out, "\n")) + c.Assert(err, checker.IsNil) + c.Assert(result, checker.Equals, true) +} + +func (s *DockerSuite) TestInspectContainerFilterInt(c *check.C) { + testRequires(c, DaemonIsLinux) + runCmd := exec.Command(dockerBinary, "run", "-i", "-a", "stdin", "busybox", "cat") + runCmd.Stdin = strings.NewReader("blahblah") + out, _, _, err := runCommandWithStdoutStderr(runCmd) + c.Assert(err, checker.IsNil, check.Commentf("failed to run container: %v, output: %q", err, out)) + + id := strings.TrimSpace(out) + + out = inspectField(c, id, "State.ExitCode") + + exitCode, err := strconv.Atoi(out) + c.Assert(err, checker.IsNil, check.Commentf("failed to inspect exitcode of the container: %s, %v", out, err)) + + //now get the exit code to verify + formatStr := fmt.Sprintf("--format='{{eq .State.ExitCode %d}}'", exitCode) + out, _ = dockerCmd(c, "inspect", formatStr, id) + result, err := strconv.ParseBool(strings.TrimSuffix(out, "\n")) + c.Assert(err, checker.IsNil) + c.Assert(result, checker.Equals, true) +} + +func (s *DockerSuite) TestInspectImageGraphDriver(c *check.C) { + testRequires(c, DaemonIsLinux, Devicemapper) + imageTest := "emptyfs" + name := inspectField(c, imageTest, "GraphDriver.Name") + + checkValidGraphDriver(c, name) + + deviceID := inspectField(c, imageTest, "GraphDriver.Data.DeviceId") + + _, err := strconv.Atoi(deviceID) + c.Assert(err, checker.IsNil, check.Commentf("failed to inspect DeviceId of the image: %s, %v", deviceID, err)) + + deviceSize := inspectField(c, imageTest, "GraphDriver.Data.DeviceSize") + + _, err = strconv.ParseUint(deviceSize, 10, 64) + c.Assert(err, checker.IsNil, check.Commentf("failed to inspect DeviceSize of the image: %s, %v", deviceSize, err)) +} + +func (s *DockerSuite) TestInspectContainerGraphDriver(c *check.C) { + testRequires(c, DaemonIsLinux, Devicemapper) + + out, _ := dockerCmd(c, "run", "-d", "busybox", "true") + out = strings.TrimSpace(out) + + name := inspectField(c, out, "GraphDriver.Name") + + checkValidGraphDriver(c, name) + + imageDeviceID := inspectField(c, "busybox", "GraphDriver.Data.DeviceId") + + deviceID := inspectField(c, out, "GraphDriver.Data.DeviceId") + + c.Assert(imageDeviceID, checker.Not(checker.Equals), deviceID) + + _, err := strconv.Atoi(deviceID) + c.Assert(err, checker.IsNil, check.Commentf("failed to inspect DeviceId of the image: %s, %v", deviceID, err)) + + deviceSize := inspectField(c, out, "GraphDriver.Data.DeviceSize") + + _, err = strconv.ParseUint(deviceSize, 10, 64) + c.Assert(err, checker.IsNil, check.Commentf("failed to inspect DeviceSize of the image: %s, %v", deviceSize, err)) +} + +func (s *DockerSuite) TestInspectBindMountPoint(c *check.C) { + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "-d", "--name", "test", "-v", "/data:/data:ro,z", "busybox", "cat") + + vol := inspectFieldJSON(c, "test", "Mounts") + + var mp []types.MountPoint + err := unmarshalJSON([]byte(vol), &mp) + c.Assert(err, checker.IsNil) + + // check that there is only one mountpoint + c.Assert(mp, check.HasLen, 1) + + m := mp[0] + + c.Assert(m.Name, checker.Equals, "") + c.Assert(m.Driver, checker.Equals, "") + c.Assert(m.Source, checker.Equals, "/data") + c.Assert(m.Destination, checker.Equals, "/data") + c.Assert(m.Mode, checker.Equals, "ro,z") + c.Assert(m.RW, checker.Equals, false) +} + +// #14947 +func (s *DockerSuite) TestInspectTimesAsRFC3339Nano(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "true") + id := strings.TrimSpace(out) + startedAt := inspectField(c, id, "State.StartedAt") + finishedAt := inspectField(c, id, "State.FinishedAt") + created := inspectField(c, id, "Created") + + _, err := time.Parse(time.RFC3339Nano, startedAt) + c.Assert(err, checker.IsNil) + _, err = time.Parse(time.RFC3339Nano, finishedAt) + c.Assert(err, checker.IsNil) + _, err = time.Parse(time.RFC3339Nano, created) + c.Assert(err, checker.IsNil) + + created = inspectField(c, "busybox", "Created") + + _, err = time.Parse(time.RFC3339Nano, created) + c.Assert(err, checker.IsNil) +} + +// #15633 +func (s *DockerSuite) TestInspectLogConfigNoType(c *check.C) { + testRequires(c, DaemonIsLinux) + dockerCmd(c, "create", "--name=test", "--log-opt", "max-file=42", "busybox") + var logConfig container.LogConfig + + out := inspectFieldJSON(c, "test", "HostConfig.LogConfig") + + err := json.NewDecoder(strings.NewReader(out)).Decode(&logConfig) + c.Assert(err, checker.IsNil, check.Commentf("%v", out)) + + c.Assert(logConfig.Type, checker.Equals, "json-file") + c.Assert(logConfig.Config["max-file"], checker.Equals, "42", check.Commentf("%v", logConfig)) +} + +func (s *DockerSuite) TestInspectNoSizeFlagContainer(c *check.C) { + + //Both the container and image are named busybox. docker inspect will fetch container + //JSON SizeRw and SizeRootFs field. If there is no flag --size/-s, there are no size fields. + + runSleepingContainer(c, "--name=busybox", "-d") + + formatStr := "--format='{{.SizeRw}},{{.SizeRootFs}}'" + out, _ := dockerCmd(c, "inspect", "--type=container", formatStr, "busybox") + c.Assert(strings.TrimSpace(out), check.Equals, ",", check.Commentf("Exepcted not to display size info: %s", out)) +} + +func (s *DockerSuite) TestInspectSizeFlagContainer(c *check.C) { + runSleepingContainer(c, "--name=busybox", "-d") + + formatStr := "--format='{{.SizeRw}},{{.SizeRootFs}}'" + out, _ := dockerCmd(c, "inspect", "-s", "--type=container", formatStr, "busybox") + sz := strings.Split(out, ",") + + c.Assert(strings.TrimSpace(sz[0]), check.Not(check.Equals), "") + c.Assert(strings.TrimSpace(sz[1]), check.Not(check.Equals), "") +} + +func (s *DockerSuite) TestInspectSizeFlagImage(c *check.C) { + runSleepingContainer(c, "-d") + + formatStr := "--format='{{.SizeRw}},{{.SizeRootFs}}'" + out, _, err := dockerCmdWithError("inspect", "-s", "--type=image", formatStr, "busybox") + + // Template error rather than + // This is a more correct behavior because images don't have sizes associated. + c.Assert(err, check.Not(check.IsNil)) + c.Assert(out, checker.Contains, "Template parsing error") +} + +func (s *DockerSuite) TestInspectTemplateError(c *check.C) { + // Template parsing error for both the container and image. + + runSleepingContainer(c, "--name=container1", "-d") + + out, _, err := dockerCmdWithError("inspect", "--type=container", "--format='Format container: {{.ThisDoesNotExist}}'", "container1") + c.Assert(err, check.Not(check.IsNil)) + c.Assert(out, checker.Contains, "Template parsing error") + + out, _, err = dockerCmdWithError("inspect", "--type=image", "--format='Format container: {{.ThisDoesNotExist}}'", "busybox") + c.Assert(err, check.Not(check.IsNil)) + c.Assert(out, checker.Contains, "Template parsing error") +} + +func (s *DockerSuite) TestInspectJSONFields(c *check.C) { + runSleepingContainer(c, "--name=busybox", "-d") + out, _, err := dockerCmdWithError("inspect", "--type=container", "--format='{{.HostConfig.Dns}}'", "busybox") + + c.Assert(err, check.IsNil) + c.Assert(out, checker.Equals, "[]\n") +} + +func (s *DockerSuite) TestInspectByPrefix(c *check.C) { + id := inspectField(c, "busybox", "Id") + c.Assert(id, checker.HasPrefix, "sha256:") + + id2 := inspectField(c, id[:12], "Id") + c.Assert(id, checker.Equals, id2) + + id3 := inspectField(c, strings.TrimPrefix(id, "sha256:")[:12], "Id") + c.Assert(id, checker.Equals, id3) +} + +func (s *DockerSuite) TestInspectStopWhenNotFound(c *check.C) { + runSleepingContainer(c, "--name=busybox", "-d") + runSleepingContainer(c, "--name=not-shown", "-d") + out, _, err := dockerCmdWithError("inspect", "--type=container", "--format='{{.Name}}'", "busybox", "missing", "not-shown") + + c.Assert(err, checker.Not(check.IsNil)) + c.Assert(out, checker.Contains, "busybox") + c.Assert(out, checker.Not(checker.Contains), "not-shown") + c.Assert(out, checker.Contains, "Error: No such container: missing") +} + +func (s *DockerSuite) TestInspectHistory(c *check.C) { + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "--name=testcont", "-d", "busybox", "top") + dockerCmd(c, "commit", "-m", "test comment", "testcont", "testimg") + out, _, err := dockerCmdWithError("inspect", "--format='{{.Comment}}'", "testimg") + + c.Assert(err, check.IsNil) + c.Assert(out, checker.Contains, "test comment") +} + +func (s *DockerSuite) TestInspectContainerNetworkDefault(c *check.C) { + testRequires(c, DaemonIsLinux) + + contName := "test1" + dockerCmd(c, "run", "--name", contName, "-d", "busybox", "top") + netOut, _ := dockerCmd(c, "network", "inspect", "--format='{{.ID}}'", "bridge") + out := inspectField(c, contName, "NetworkSettings.Networks") + c.Assert(out, checker.Contains, "bridge") + out = inspectField(c, contName, "NetworkSettings.Networks.bridge.NetworkID") + c.Assert(strings.TrimSpace(out), checker.Equals, strings.TrimSpace(netOut)) +} + +func (s *DockerSuite) TestInspectContainerNetworkCustom(c *check.C) { + testRequires(c, DaemonIsLinux) + + netOut, _ := dockerCmd(c, "network", "create", "net1") + dockerCmd(c, "run", "--name=container1", "--net=net1", "-d", "busybox", "top") + out := inspectField(c, "container1", "NetworkSettings.Networks") + c.Assert(out, checker.Contains, "net1") + out = inspectField(c, "container1", "NetworkSettings.Networks.net1.NetworkID") + c.Assert(strings.TrimSpace(out), checker.Equals, strings.TrimSpace(netOut)) +} + +func (s *DockerSuite) TestInspectRootFS(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _, err := dockerCmdWithError("inspect", "busybox") + c.Assert(err, check.IsNil) + + var imageJSON []types.ImageInspect + err = json.Unmarshal([]byte(out), &imageJSON) + c.Assert(err, checker.IsNil) + + c.Assert(len(imageJSON[0].RootFS.Layers), checker.GreaterOrEqualThan, 1) +} diff --git a/integration-cli/docker_cli_kill_test.go b/integration-cli/docker_cli_kill_test.go new file mode 100644 index 00000000..05d9a558 --- /dev/null +++ b/integration-cli/docker_cli_kill_test.go @@ -0,0 +1,95 @@ +package main + +import ( + "fmt" + "net/http" + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestKillContainer(c *check.C) { + out, _ := runSleepingContainer(c, "-d") + cleanedContainerID := strings.TrimSpace(out) + c.Assert(waitRun(cleanedContainerID), check.IsNil) + + dockerCmd(c, "kill", cleanedContainerID) + + out, _ = dockerCmd(c, "ps", "-q") + c.Assert(out, checker.Not(checker.Contains), cleanedContainerID, check.Commentf("killed container is still running")) + +} + +func (s *DockerSuite) TestKillOffStoppedContainer(c *check.C) { + out, _ := runSleepingContainer(c, "-d") + cleanedContainerID := strings.TrimSpace(out) + + dockerCmd(c, "stop", cleanedContainerID) + + _, _, err := dockerCmdWithError("kill", "-s", "30", cleanedContainerID) + c.Assert(err, check.Not(check.IsNil), check.Commentf("Container %s is not running", cleanedContainerID)) +} + +func (s *DockerSuite) TestKillDifferentUserContainer(c *check.C) { + // TODO Windows: Windows does not yet support -u (Feb 2016). + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-u", "daemon", "-d", "busybox", "top") + cleanedContainerID := strings.TrimSpace(out) + c.Assert(waitRun(cleanedContainerID), check.IsNil) + + dockerCmd(c, "kill", cleanedContainerID) + + out, _ = dockerCmd(c, "ps", "-q") + c.Assert(out, checker.Not(checker.Contains), cleanedContainerID, check.Commentf("killed container is still running")) + +} + +// regression test about correct signal parsing see #13665 +func (s *DockerSuite) TestKillWithSignal(c *check.C) { + // Cannot port to Windows - does not support signals in the same was a Linux does + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "top") + cid := strings.TrimSpace(out) + c.Assert(waitRun(cid), check.IsNil) + + dockerCmd(c, "kill", "-s", "SIGWINCH", cid) + + running := inspectField(c, cid, "State.Running") + + c.Assert(running, checker.Equals, "true", check.Commentf("Container should be in running state after SIGWINCH")) +} + +func (s *DockerSuite) TestKillWithInvalidSignal(c *check.C) { + out, _ := runSleepingContainer(c, "-d") + cid := strings.TrimSpace(out) + c.Assert(waitRun(cid), check.IsNil) + + out, _, err := dockerCmdWithError("kill", "-s", "0", cid) + c.Assert(err, check.NotNil) + c.Assert(out, checker.Contains, "Invalid signal: 0", check.Commentf("Kill with an invalid signal didn't error out correctly")) + + running := inspectField(c, cid, "State.Running") + c.Assert(running, checker.Equals, "true", check.Commentf("Container should be in running state after an invalid signal")) + + out, _ = runSleepingContainer(c, "-d") + cid = strings.TrimSpace(out) + c.Assert(waitRun(cid), check.IsNil) + + out, _, err = dockerCmdWithError("kill", "-s", "SIG42", cid) + c.Assert(err, check.NotNil) + c.Assert(out, checker.Contains, "Invalid signal: SIG42", check.Commentf("Kill with an invalid signal error out correctly")) + + running = inspectField(c, cid, "State.Running") + c.Assert(running, checker.Equals, "true", check.Commentf("Container should be in running state after an invalid signal")) + +} + +func (s *DockerSuite) TestKillStoppedContainerAPIPre120(c *check.C) { + runSleepingContainer(c, "--name", "docker-kill-test-api", "-d") + dockerCmd(c, "stop", "docker-kill-test-api") + + status, _, err := sockRequest("POST", fmt.Sprintf("/v1.19/containers/%s/kill", "docker-kill-test-api"), nil) + c.Assert(err, check.IsNil) + c.Assert(status, check.Equals, http.StatusNoContent) +} diff --git a/integration-cli/docker_cli_links_test.go b/integration-cli/docker_cli_links_test.go new file mode 100644 index 00000000..322b58c6 --- /dev/null +++ b/integration-cli/docker_cli_links_test.go @@ -0,0 +1,213 @@ +package main + +import ( + "fmt" + "regexp" + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/runconfig" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestLinksPingUnlinkedContainers(c *check.C) { + testRequires(c, DaemonIsLinux) + _, exitCode, err := dockerCmdWithError("run", "--rm", "busybox", "sh", "-c", "ping -c 1 alias1 -W 1 && ping -c 1 alias2 -W 1") + + // run ping failed with error + c.Assert(exitCode, checker.Equals, 1, check.Commentf("error: %v", err)) +} + +// Test for appropriate error when calling --link with an invalid target container +func (s *DockerSuite) TestLinksInvalidContainerTarget(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _, err := dockerCmdWithError("run", "--link", "bogus:alias", "busybox", "true") + + // an invalid container target should produce an error + c.Assert(err, checker.NotNil, check.Commentf("out: %s", out)) + // an invalid container target should produce an error + c.Assert(out, checker.Contains, "Could not get container") +} + +func (s *DockerSuite) TestLinksPingLinkedContainers(c *check.C) { + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "-d", "--name", "container1", "--hostname", "fred", "busybox", "top") + dockerCmd(c, "run", "-d", "--name", "container2", "--hostname", "wilma", "busybox", "top") + + runArgs := []string{"run", "--rm", "--link", "container1:alias1", "--link", "container2:alias2", "busybox", "sh", "-c"} + pingCmd := "ping -c 1 %s -W 1 && ping -c 1 %s -W 1" + + // test ping by alias, ping by name, and ping by hostname + // 1. Ping by alias + dockerCmd(c, append(runArgs, fmt.Sprintf(pingCmd, "alias1", "alias2"))...) + // 2. Ping by container name + dockerCmd(c, append(runArgs, fmt.Sprintf(pingCmd, "container1", "container2"))...) + // 3. Ping by hostname + dockerCmd(c, append(runArgs, fmt.Sprintf(pingCmd, "fred", "wilma"))...) + +} + +func (s *DockerSuite) TestLinksPingLinkedContainersAfterRename(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "--name", "container1", "busybox", "top") + idA := strings.TrimSpace(out) + out, _ = dockerCmd(c, "run", "-d", "--name", "container2", "busybox", "top") + idB := strings.TrimSpace(out) + dockerCmd(c, "rename", "container1", "container_new") + dockerCmd(c, "run", "--rm", "--link", "container_new:alias1", "--link", "container2:alias2", "busybox", "sh", "-c", "ping -c 1 alias1 -W 1 && ping -c 1 alias2 -W 1") + dockerCmd(c, "kill", idA) + dockerCmd(c, "kill", idB) + +} + +func (s *DockerSuite) TestLinksInspectLinksStarted(c *check.C) { + testRequires(c, DaemonIsLinux) + var ( + expected = map[string]struct{}{"/container1:/testinspectlink/alias1": {}, "/container2:/testinspectlink/alias2": {}} + result []string + ) + dockerCmd(c, "run", "-d", "--name", "container1", "busybox", "top") + dockerCmd(c, "run", "-d", "--name", "container2", "busybox", "top") + dockerCmd(c, "run", "-d", "--name", "testinspectlink", "--link", "container1:alias1", "--link", "container2:alias2", "busybox", "top") + links := inspectFieldJSON(c, "testinspectlink", "HostConfig.Links") + + err := unmarshalJSON([]byte(links), &result) + c.Assert(err, checker.IsNil) + + output := convertSliceOfStringsToMap(result) + + c.Assert(output, checker.DeepEquals, expected) +} + +func (s *DockerSuite) TestLinksInspectLinksStopped(c *check.C) { + testRequires(c, DaemonIsLinux) + var ( + expected = map[string]struct{}{"/container1:/testinspectlink/alias1": {}, "/container2:/testinspectlink/alias2": {}} + result []string + ) + dockerCmd(c, "run", "-d", "--name", "container1", "busybox", "top") + dockerCmd(c, "run", "-d", "--name", "container2", "busybox", "top") + dockerCmd(c, "run", "-d", "--name", "testinspectlink", "--link", "container1:alias1", "--link", "container2:alias2", "busybox", "true") + links := inspectFieldJSON(c, "testinspectlink", "HostConfig.Links") + + err := unmarshalJSON([]byte(links), &result) + c.Assert(err, checker.IsNil) + + output := convertSliceOfStringsToMap(result) + + c.Assert(output, checker.DeepEquals, expected) +} + +func (s *DockerSuite) TestLinksNotStartedParentNotFail(c *check.C) { + testRequires(c, DaemonIsLinux) + dockerCmd(c, "create", "--name=first", "busybox", "top") + dockerCmd(c, "create", "--name=second", "--link=first:first", "busybox", "top") + dockerCmd(c, "start", "first") + +} + +func (s *DockerSuite) TestLinksHostsFilesInject(c *check.C) { + testRequires(c, DaemonIsLinux) + testRequires(c, SameHostDaemon, ExecSupport) + + out, _ := dockerCmd(c, "run", "-itd", "--name", "one", "busybox", "top") + idOne := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "run", "-itd", "--name", "two", "--link", "one:onetwo", "busybox", "top") + idTwo := strings.TrimSpace(out) + + c.Assert(waitRun(idTwo), checker.IsNil) + + contentOne, err := readContainerFileWithExec(idOne, "/etc/hosts") + c.Assert(err, checker.IsNil, check.Commentf("contentOne: %s", string(contentOne))) + + contentTwo, err := readContainerFileWithExec(idTwo, "/etc/hosts") + c.Assert(err, checker.IsNil, check.Commentf("contentTwo: %s", string(contentTwo))) + // Host is not present in updated hosts file + c.Assert(string(contentTwo), checker.Contains, "onetwo") +} + +func (s *DockerSuite) TestLinksUpdateOnRestart(c *check.C) { + testRequires(c, DaemonIsLinux) + testRequires(c, SameHostDaemon, ExecSupport) + dockerCmd(c, "run", "-d", "--name", "one", "busybox", "top") + out, _ := dockerCmd(c, "run", "-d", "--name", "two", "--link", "one:onetwo", "--link", "one:one", "busybox", "top") + id := strings.TrimSpace(string(out)) + + realIP := inspectField(c, "one", "NetworkSettings.Networks.bridge.IPAddress") + content, err := readContainerFileWithExec(id, "/etc/hosts") + c.Assert(err, checker.IsNil) + + getIP := func(hosts []byte, hostname string) string { + re := regexp.MustCompile(fmt.Sprintf(`(\S*)\t%s`, regexp.QuoteMeta(hostname))) + matches := re.FindSubmatch(hosts) + c.Assert(matches, checker.NotNil, check.Commentf("Hostname %s have no matches in hosts", hostname)) + return string(matches[1]) + } + ip := getIP(content, "one") + c.Assert(ip, checker.Equals, realIP) + + ip = getIP(content, "onetwo") + c.Assert(ip, checker.Equals, realIP) + + dockerCmd(c, "restart", "one") + realIP = inspectField(c, "one", "NetworkSettings.Networks.bridge.IPAddress") + + content, err = readContainerFileWithExec(id, "/etc/hosts") + c.Assert(err, checker.IsNil, check.Commentf("content: %s", string(content))) + ip = getIP(content, "one") + c.Assert(ip, checker.Equals, realIP) + + ip = getIP(content, "onetwo") + c.Assert(ip, checker.Equals, realIP) +} + +func (s *DockerSuite) TestLinksEnvs(c *check.C) { + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "-d", "-e", "e1=", "-e", "e2=v2", "-e", "e3=v3=v3", "--name=first", "busybox", "top") + out, _ := dockerCmd(c, "run", "--name=second", "--link=first:first", "busybox", "env") + c.Assert(out, checker.Contains, "FIRST_ENV_e1=\n") + c.Assert(out, checker.Contains, "FIRST_ENV_e2=v2") + c.Assert(out, checker.Contains, "FIRST_ENV_e3=v3=v3") +} + +func (s *DockerSuite) TestLinkShortDefinition(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "--name", "shortlinkdef", "busybox", "top") + + cid := strings.TrimSpace(out) + c.Assert(waitRun(cid), checker.IsNil) + + out, _ = dockerCmd(c, "run", "-d", "--name", "link2", "--link", "shortlinkdef", "busybox", "top") + + cid2 := strings.TrimSpace(out) + c.Assert(waitRun(cid2), checker.IsNil) + + links := inspectFieldJSON(c, cid2, "HostConfig.Links") + c.Assert(links, checker.Equals, "[\"/shortlinkdef:/link2/shortlinkdef\"]") +} + +func (s *DockerSuite) TestLinksNetworkHostContainer(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace) + dockerCmd(c, "run", "-d", "--net", "host", "--name", "host_container", "busybox", "top") + out, _, err := dockerCmdWithError("run", "--name", "should_fail", "--link", "host_container:tester", "busybox", "true") + + // Running container linking to a container with --net host should have failed + c.Assert(err, checker.NotNil, check.Commentf("out: %s", out)) + // Running container linking to a container with --net host should have failed + c.Assert(out, checker.Contains, runconfig.ErrConflictHostNetworkAndLinks.Error()) +} + +func (s *DockerSuite) TestLinksEtcHostsRegularFile(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace) + out, _ := dockerCmd(c, "run", "--net=host", "busybox", "ls", "-la", "/etc/hosts") + // /etc/hosts should be a regular file + c.Assert(out, checker.Matches, "^-.+\n") +} + +func (s *DockerSuite) TestLinksMultipleWithSameName(c *check.C) { + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "-d", "--name=upstream-a", "busybox", "top") + dockerCmd(c, "run", "-d", "--name=upstream-b", "busybox", "top") + dockerCmd(c, "run", "--link", "upstream-a:upstream", "--link", "upstream-b:upstream", "busybox", "sh", "-c", "ping -c 1 upstream") +} diff --git a/integration-cli/docker_cli_links_unix_test.go b/integration-cli/docker_cli_links_unix_test.go new file mode 100644 index 00000000..1af92793 --- /dev/null +++ b/integration-cli/docker_cli_links_unix_test.go @@ -0,0 +1,26 @@ +// +build !windows + +package main + +import ( + "io/ioutil" + "os" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestLinksEtcHostsContentMatch(c *check.C) { + // In a _unix file as using Unix specific files, and must be on the + // same host as the daemon. + testRequires(c, SameHostDaemon, NotUserNamespace) + + out, _ := dockerCmd(c, "run", "--net=host", "busybox", "cat", "/etc/hosts") + hosts, err := ioutil.ReadFile("/etc/hosts") + if os.IsNotExist(err) { + c.Skip("/etc/hosts does not exist, skip this test") + } + + c.Assert(out, checker.Equals, string(hosts), check.Commentf("container: %s\n\nhost:%s", out, hosts)) + +} diff --git a/integration-cli/docker_cli_login_test.go b/integration-cli/docker_cli_login_test.go new file mode 100644 index 00000000..01de75d9 --- /dev/null +++ b/integration-cli/docker_cli_login_test.go @@ -0,0 +1,44 @@ +package main + +import ( + "bytes" + "os/exec" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestLoginWithoutTTY(c *check.C) { + cmd := exec.Command(dockerBinary, "login") + + // Send to stdin so the process does not get the TTY + cmd.Stdin = bytes.NewBufferString("buffer test string \n") + + // run the command and block until it's done + err := cmd.Run() + c.Assert(err, checker.NotNil) //"Expected non nil err when loginning in & TTY not available" +} + +func (s *DockerRegistryAuthHtpasswdSuite) TestLoginToPrivateRegistry(c *check.C) { + // wrong credentials + out, _, err := dockerCmdWithError("login", "-u", s.reg.username, "-p", "WRONGPASSWORD", privateRegistryURL) + c.Assert(err, checker.NotNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "401 Unauthorized") + + // now it's fine + dockerCmd(c, "login", "-u", s.reg.username, "-p", s.reg.password, privateRegistryURL) +} + +func (s *DockerRegistryAuthHtpasswdSuite) TestLoginToPrivateRegistryDeprecatedEmailFlag(c *check.C) { + // Test to make sure login still works with the deprecated -e and --email flags + // wrong credentials + out, _, err := dockerCmdWithError("login", "-u", s.reg.username, "-p", "WRONGPASSWORD", "-e", s.reg.email, privateRegistryURL) + c.Assert(err, checker.NotNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "401 Unauthorized") + + // now it's fine + // -e flag + dockerCmd(c, "login", "-u", s.reg.username, "-p", s.reg.password, "-e", s.reg.email, privateRegistryURL) + // --email flag + dockerCmd(c, "login", "-u", s.reg.username, "-p", s.reg.password, "--email", s.reg.email, privateRegistryURL) +} diff --git a/integration-cli/docker_cli_logout_test.go b/integration-cli/docker_cli_logout_test.go new file mode 100644 index 00000000..6658da54 --- /dev/null +++ b/integration-cli/docker_cli_logout_test.go @@ -0,0 +1,56 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerRegistryAuthHtpasswdSuite) TestLogoutWithExternalAuth(c *check.C) { + osPath := os.Getenv("PATH") + defer os.Setenv("PATH", osPath) + + workingDir, err := os.Getwd() + c.Assert(err, checker.IsNil) + absolute, err := filepath.Abs(filepath.Join(workingDir, "fixtures", "auth")) + c.Assert(err, checker.IsNil) + testPath := fmt.Sprintf("%s%c%s", osPath, filepath.ListSeparator, absolute) + + os.Setenv("PATH", testPath) + + repoName := fmt.Sprintf("%v/dockercli/busybox:authtest", privateRegistryURL) + + tmp, err := ioutil.TempDir("", "integration-cli-") + c.Assert(err, checker.IsNil) + + externalAuthConfig := `{ "credsStore": "shell-test" }` + + configPath := filepath.Join(tmp, "config.json") + err = ioutil.WriteFile(configPath, []byte(externalAuthConfig), 0644) + c.Assert(err, checker.IsNil) + + dockerCmd(c, "--config", tmp, "login", "-u", s.reg.username, "-p", s.reg.password, privateRegistryURL) + + b, err := ioutil.ReadFile(configPath) + c.Assert(err, checker.IsNil) + c.Assert(string(b), checker.Not(checker.Contains), "\"auth\":") + c.Assert(string(b), checker.Contains, privateRegistryURL) + + dockerCmd(c, "--config", tmp, "tag", "busybox", repoName) + dockerCmd(c, "--config", tmp, "push", repoName) + + dockerCmd(c, "--config", tmp, "logout", privateRegistryURL) + + b, err = ioutil.ReadFile(configPath) + c.Assert(err, checker.IsNil) + c.Assert(string(b), checker.Not(checker.Contains), privateRegistryURL) + + // check I cannot pull anymore + out, _, err := dockerCmdWithError("--config", tmp, "pull", repoName) + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(out, checker.Contains, fmt.Sprintf("Error: image dockercli/busybox not found")) +} diff --git a/integration-cli/docker_cli_logs_bench_test.go b/integration-cli/docker_cli_logs_bench_test.go new file mode 100644 index 00000000..eeb008de --- /dev/null +++ b/integration-cli/docker_cli_logs_bench_test.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" + "strings" + "time" + + "github.com/go-check/check" +) + +func (s *DockerSuite) BenchmarkLogsCLIRotateFollow(c *check.C) { + out, _ := dockerCmd(c, "run", "-d", "--log-opt", "max-size=1b", "--log-opt", "max-file=10", "busybox", "sh", "-c", "while true; do usleep 50000; echo hello; done") + id := strings.TrimSpace(out) + ch := make(chan error, 1) + go func() { + ch <- nil + out, _, _ := dockerCmdWithError("logs", "-f", id) + // if this returns at all, it's an error + ch <- fmt.Errorf(out) + }() + + <-ch + select { + case <-time.After(30 * time.Second): + // ran for 30 seconds with no problem + return + case err := <-ch: + if err != nil { + c.Fatal(err) + } + } +} diff --git a/integration-cli/docker_cli_logs_test.go b/integration-cli/docker_cli_logs_test.go new file mode 100644 index 00000000..e18916db --- /dev/null +++ b/integration-cli/docker_cli_logs_test.go @@ -0,0 +1,309 @@ +package main + +import ( + "fmt" + "io" + "os/exec" + "regexp" + "strings" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/jsonlog" + "github.com/go-check/check" +) + +// This used to work, it test a log of PageSize-1 (gh#4851) +func (s *DockerSuite) TestLogsContainerSmallerThanPage(c *check.C) { + testLen := 32767 + out, _ := dockerCmd(c, "run", "-d", "busybox", "sh", "-c", fmt.Sprintf("for i in $(seq 1 %d); do echo -n = >> a.a; done; echo >> a.a; cat a.a", testLen)) + + id := strings.TrimSpace(out) + dockerCmd(c, "wait", id) + + out, _ = dockerCmd(c, "logs", id) + + c.Assert(out, checker.HasLen, testLen+1) +} + +// Regression test: When going over the PageSize, it used to panic (gh#4851) +func (s *DockerSuite) TestLogsContainerBiggerThanPage(c *check.C) { + testLen := 32768 + out, _ := dockerCmd(c, "run", "-d", "busybox", "sh", "-c", fmt.Sprintf("for i in $(seq 1 %d); do echo -n = >> a.a; done; echo >> a.a; cat a.a", testLen)) + + id := strings.TrimSpace(out) + dockerCmd(c, "wait", id) + + out, _ = dockerCmd(c, "logs", id) + + c.Assert(out, checker.HasLen, testLen+1) +} + +// Regression test: When going much over the PageSize, it used to block (gh#4851) +func (s *DockerSuite) TestLogsContainerMuchBiggerThanPage(c *check.C) { + testLen := 33000 + out, _ := dockerCmd(c, "run", "-d", "busybox", "sh", "-c", fmt.Sprintf("for i in $(seq 1 %d); do echo -n = >> a.a; done; echo >> a.a; cat a.a", testLen)) + + id := strings.TrimSpace(out) + dockerCmd(c, "wait", id) + + out, _ = dockerCmd(c, "logs", id) + + c.Assert(out, checker.HasLen, testLen+1) +} + +func (s *DockerSuite) TestLogsTimestamps(c *check.C) { + testLen := 100 + out, _ := dockerCmd(c, "run", "-d", "busybox", "sh", "-c", fmt.Sprintf("for i in $(seq 1 %d); do echo = >> a.a; done; cat a.a", testLen)) + + id := strings.TrimSpace(out) + dockerCmd(c, "wait", id) + + out, _ = dockerCmd(c, "logs", "-t", id) + + lines := strings.Split(out, "\n") + + c.Assert(lines, checker.HasLen, testLen+1) + + ts := regexp.MustCompile(`^.* `) + + for _, l := range lines { + if l != "" { + _, err := time.Parse(jsonlog.RFC3339NanoFixed+" ", ts.FindString(l)) + c.Assert(err, checker.IsNil, check.Commentf("Failed to parse timestamp from %v", l)) + // ensure we have padded 0's + c.Assert(l[29], checker.Equals, uint8('Z')) + } + } +} + +func (s *DockerSuite) TestLogsSeparateStderr(c *check.C) { + msg := "stderr_log" + out, _ := dockerCmd(c, "run", "-d", "busybox", "sh", "-c", fmt.Sprintf("echo %s 1>&2", msg)) + + id := strings.TrimSpace(out) + dockerCmd(c, "wait", id) + + stdout, stderr, _ := dockerCmdWithStdoutStderr(c, "logs", id) + + c.Assert(stdout, checker.Equals, "") + + stderr = strings.TrimSpace(stderr) + + c.Assert(stderr, checker.Equals, msg) +} + +func (s *DockerSuite) TestLogsStderrInStdout(c *check.C) { + // TODO Windows: Needs investigation why this fails. Obtained string includes + // a bunch of ANSI escape sequences before the "stderr_log" message. + testRequires(c, DaemonIsLinux) + msg := "stderr_log" + out, _ := dockerCmd(c, "run", "-d", "-t", "busybox", "sh", "-c", fmt.Sprintf("echo %s 1>&2", msg)) + + id := strings.TrimSpace(out) + dockerCmd(c, "wait", id) + + stdout, stderr, _ := dockerCmdWithStdoutStderr(c, "logs", id) + c.Assert(stderr, checker.Equals, "") + + stdout = strings.TrimSpace(stdout) + c.Assert(stdout, checker.Equals, msg) +} + +func (s *DockerSuite) TestLogsTail(c *check.C) { + testLen := 100 + out, _ := dockerCmd(c, "run", "-d", "busybox", "sh", "-c", fmt.Sprintf("for i in $(seq 1 %d); do echo =; done;", testLen)) + + id := strings.TrimSpace(out) + dockerCmd(c, "wait", id) + + out, _ = dockerCmd(c, "logs", "--tail", "5", id) + + lines := strings.Split(out, "\n") + + c.Assert(lines, checker.HasLen, 6) + + out, _ = dockerCmd(c, "logs", "--tail", "all", id) + + lines = strings.Split(out, "\n") + + c.Assert(lines, checker.HasLen, testLen+1) + + out, _, _ = dockerCmdWithStdoutStderr(c, "logs", "--tail", "random", id) + + lines = strings.Split(out, "\n") + + c.Assert(lines, checker.HasLen, testLen+1) +} + +func (s *DockerSuite) TestLogsFollowStopped(c *check.C) { + dockerCmd(c, "run", "--name=test", "busybox", "echo", "hello") + id, err := getIDByName("test") + c.Assert(err, check.IsNil) + + logsCmd := exec.Command(dockerBinary, "logs", "-f", id) + c.Assert(logsCmd.Start(), checker.IsNil) + + errChan := make(chan error) + go func() { + errChan <- logsCmd.Wait() + close(errChan) + }() + + select { + case err := <-errChan: + c.Assert(err, checker.IsNil) + case <-time.After(30 * time.Second): + c.Fatal("Following logs is hanged") + } +} + +func (s *DockerSuite) TestLogsSince(c *check.C) { + name := "testlogssince" + dockerCmd(c, "run", "--name="+name, "busybox", "/bin/sh", "-c", "for i in $(seq 1 3); do sleep 2; echo log$i; done") + out, _ := dockerCmd(c, "logs", "-t", name) + + log2Line := strings.Split(strings.Split(out, "\n")[1], " ") + t, err := time.Parse(time.RFC3339Nano, log2Line[0]) // the timestamp log2 is written + c.Assert(err, checker.IsNil) + since := t.Unix() + 1 // add 1s so log1 & log2 doesn't show up + out, _ = dockerCmd(c, "logs", "-t", fmt.Sprintf("--since=%v", since), name) + + // Skip 2 seconds + unexpected := []string{"log1", "log2"} + for _, v := range unexpected { + c.Assert(out, checker.Not(checker.Contains), v, check.Commentf("unexpected log message returned, since=%v", since)) + } + + // Test to make sure a bad since format is caught by the client + out, _, _ = dockerCmdWithError("logs", "-t", "--since=2006-01-02T15:04:0Z", name) + c.Assert(out, checker.Contains, "cannot parse \"0Z\" as \"05\"", check.Commentf("bad since format passed to server")) + + // Test with default value specified and parameter omitted + expected := []string{"log1", "log2", "log3"} + for _, cmd := range []*exec.Cmd{ + exec.Command(dockerBinary, "logs", "-t", name), + exec.Command(dockerBinary, "logs", "-t", "--since=0", name), + } { + out, _, err = runCommandWithOutput(cmd) + c.Assert(err, checker.IsNil, check.Commentf("failed to log container: %s", out)) + for _, v := range expected { + c.Assert(out, checker.Contains, v) + } + } +} + +func (s *DockerSuite) TestLogsSinceFutureFollow(c *check.C) { + // TODO Windows: Flakey on TP4. Enable for next technical preview. + testRequires(c, DaemonIsLinux) + name := "testlogssincefuturefollow" + out, _ := dockerCmd(c, "run", "-d", "--name", name, "busybox", "/bin/sh", "-c", `for i in $(seq 1 5); do echo log$i; sleep 1; done`) + + // Extract one timestamp from the log file to give us a starting point for + // our `--since` argument. Because the log producer runs in the background, + // we need to check repeatedly for some output to be produced. + var timestamp string + for i := 0; i != 100 && timestamp == ""; i++ { + if out, _ = dockerCmd(c, "logs", "-t", name); out == "" { + time.Sleep(time.Millisecond * 100) // Retry + } else { + timestamp = strings.Split(strings.Split(out, "\n")[0], " ")[0] + } + } + + c.Assert(timestamp, checker.Not(checker.Equals), "") + t, err := time.Parse(time.RFC3339Nano, timestamp) + c.Assert(err, check.IsNil) + + since := t.Unix() + 2 + out, _ = dockerCmd(c, "logs", "-t", "-f", fmt.Sprintf("--since=%v", since), name) + c.Assert(out, checker.Not(checker.HasLen), 0, check.Commentf("cannot read from empty log")) + lines := strings.Split(strings.TrimSpace(out), "\n") + for _, v := range lines { + ts, err := time.Parse(time.RFC3339Nano, strings.Split(v, " ")[0]) + c.Assert(err, checker.IsNil, check.Commentf("cannot parse timestamp output from log: '%v'", v)) + c.Assert(ts.Unix() >= since, checker.Equals, true, check.Commentf("earlier log found. since=%v logdate=%v", since, ts)) + } +} + +// Regression test for #8832 +func (s *DockerSuite) TestLogsFollowSlowStdoutConsumer(c *check.C) { + // TODO Windows: Consider enabling post-TP4. Too expensive to run on TP4 + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", `usleep 600000;yes X | head -c 200000`) + + id := strings.TrimSpace(out) + + stopSlowRead := make(chan bool) + + go func() { + exec.Command(dockerBinary, "wait", id).Run() + stopSlowRead <- true + }() + + logCmd := exec.Command(dockerBinary, "logs", "-f", id) + stdout, err := logCmd.StdoutPipe() + c.Assert(err, checker.IsNil) + c.Assert(logCmd.Start(), checker.IsNil) + + // First read slowly + bytes1, err := consumeWithSpeed(stdout, 10, 50*time.Millisecond, stopSlowRead) + c.Assert(err, checker.IsNil) + + // After the container has finished we can continue reading fast + bytes2, err := consumeWithSpeed(stdout, 32*1024, 0, nil) + c.Assert(err, checker.IsNil) + + actual := bytes1 + bytes2 + expected := 200000 + c.Assert(actual, checker.Equals, expected) +} + +func (s *DockerSuite) TestLogsFollowGoroutinesWithStdout(c *check.C) { + out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "while true; do echo hello; sleep 2; done") + id := strings.TrimSpace(out) + c.Assert(waitRun(id), checker.IsNil) + + nroutines, err := getGoroutineNumber() + c.Assert(err, checker.IsNil) + cmd := exec.Command(dockerBinary, "logs", "-f", id) + r, w := io.Pipe() + cmd.Stdout = w + c.Assert(cmd.Start(), checker.IsNil) + + // Make sure pipe is written to + chErr := make(chan error) + go func() { + b := make([]byte, 1) + _, err := r.Read(b) + chErr <- err + }() + c.Assert(<-chErr, checker.IsNil) + c.Assert(cmd.Process.Kill(), checker.IsNil) + + // NGoroutines is not updated right away, so we need to wait before failing + c.Assert(waitForGoroutines(nroutines), checker.IsNil) +} + +func (s *DockerSuite) TestLogsFollowGoroutinesNoOutput(c *check.C) { + out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "while true; do sleep 2; done") + id := strings.TrimSpace(out) + c.Assert(waitRun(id), checker.IsNil) + + nroutines, err := getGoroutineNumber() + c.Assert(err, checker.IsNil) + cmd := exec.Command(dockerBinary, "logs", "-f", id) + c.Assert(cmd.Start(), checker.IsNil) + time.Sleep(200 * time.Millisecond) + c.Assert(cmd.Process.Kill(), checker.IsNil) + + // NGoroutines is not updated right away, so we need to wait before failing + c.Assert(waitForGoroutines(nroutines), checker.IsNil) +} + +func (s *DockerSuite) TestLogsCLIContainerNotFound(c *check.C) { + name := "testlogsnocontainer" + out, _, _ := dockerCmdWithError("logs", name) + message := fmt.Sprintf("Error: No such container: %s\n", name) + c.Assert(out, checker.Equals, message) +} diff --git a/integration-cli/docker_cli_nat_test.go b/integration-cli/docker_cli_nat_test.go new file mode 100644 index 00000000..7f4cc2cb --- /dev/null +++ b/integration-cli/docker_cli_nat_test.go @@ -0,0 +1,93 @@ +package main + +import ( + "fmt" + "io/ioutil" + "net" + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func startServerContainer(c *check.C, msg string, port int) string { + name := "server" + cmd := []string{ + "-d", + "-p", fmt.Sprintf("%d:%d", port, port), + "busybox", + "sh", "-c", fmt.Sprintf("echo %q | nc -lp %d", msg, port), + } + c.Assert(waitForContainer(name, cmd...), check.IsNil) + return name +} + +func getExternalAddress(c *check.C) net.IP { + iface, err := net.InterfaceByName("eth0") + if err != nil { + c.Skip(fmt.Sprintf("Test not running with `make test`. Interface eth0 not found: %v", err)) + } + + ifaceAddrs, err := iface.Addrs() + c.Assert(err, check.IsNil) + c.Assert(ifaceAddrs, checker.Not(checker.HasLen), 0) + + ifaceIP, _, err := net.ParseCIDR(ifaceAddrs[0].String()) + c.Assert(err, check.IsNil) + + return ifaceIP +} + +func getContainerLogs(c *check.C, containerID string) string { + out, _ := dockerCmd(c, "logs", containerID) + return strings.Trim(out, "\r\n") +} + +func getContainerStatus(c *check.C, containerID string) string { + out := inspectField(c, containerID, "State.Running") + return out +} + +func (s *DockerSuite) TestNetworkNat(c *check.C) { + testRequires(c, DaemonIsLinux, SameHostDaemon) + msg := "it works" + startServerContainer(c, msg, 8080) + endpoint := getExternalAddress(c) + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", endpoint.String(), 8080)) + c.Assert(err, check.IsNil) + + data, err := ioutil.ReadAll(conn) + conn.Close() + c.Assert(err, check.IsNil) + + final := strings.TrimRight(string(data), "\n") + c.Assert(final, checker.Equals, msg) +} + +func (s *DockerSuite) TestNetworkLocalhostTCPNat(c *check.C) { + testRequires(c, DaemonIsLinux, SameHostDaemon) + var ( + msg = "hi yall" + ) + startServerContainer(c, msg, 8081) + conn, err := net.Dial("tcp", "localhost:8081") + c.Assert(err, check.IsNil) + + data, err := ioutil.ReadAll(conn) + conn.Close() + c.Assert(err, check.IsNil) + + final := strings.TrimRight(string(data), "\n") + c.Assert(final, checker.Equals, msg) +} + +func (s *DockerSuite) TestNetworkLoopbackNat(c *check.C) { + testRequires(c, DaemonIsLinux, SameHostDaemon, NotUserNamespace) + msg := "it works" + startServerContainer(c, msg, 8080) + endpoint := getExternalAddress(c) + out, _ := dockerCmd(c, "run", "-t", "--net=container:server", "busybox", + "sh", "-c", fmt.Sprintf("stty raw && nc -w 5 %s 8080", endpoint.String())) + final := strings.TrimRight(string(out), "\n") + c.Assert(final, checker.Equals, msg) +} diff --git a/integration-cli/docker_cli_netmode_test.go b/integration-cli/docker_cli_netmode_test.go new file mode 100644 index 00000000..142f192d --- /dev/null +++ b/integration-cli/docker_cli_netmode_test.go @@ -0,0 +1,88 @@ +package main + +import ( + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/runconfig" + "github.com/go-check/check" +) + +// GH14530. Validates combinations of --net= with other options + +// stringCheckPS is how the output of PS starts in order to validate that +// the command executed in a container did really run PS correctly. +const stringCheckPS = "PID USER" + +// DockerCmdWithFail executes a docker command that is supposed to fail and returns +// the output, the exit code. If the command returns an Nil error, it will fail and +// stop the tests. +func dockerCmdWithFail(c *check.C, args ...string) (string, int) { + out, status, err := dockerCmdWithError(args...) + c.Assert(err, check.NotNil, check.Commentf("%v", out)) + return out, status +} + +func (s *DockerSuite) TestNetHostname(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace) + + out, _ := dockerCmd(c, "run", "-h=name", "busybox", "ps") + c.Assert(out, checker.Contains, stringCheckPS) + + out, _ = dockerCmd(c, "run", "--net=host", "busybox", "ps") + c.Assert(out, checker.Contains, stringCheckPS) + + out, _ = dockerCmd(c, "run", "-h=name", "--net=bridge", "busybox", "ps") + c.Assert(out, checker.Contains, stringCheckPS) + + out, _ = dockerCmd(c, "run", "-h=name", "--net=none", "busybox", "ps") + c.Assert(out, checker.Contains, stringCheckPS) + + out, _ = dockerCmdWithFail(c, "run", "-h=name", "--net=container:other", "busybox", "ps") + c.Assert(out, checker.Contains, runconfig.ErrConflictNetworkHostname.Error()) + + out, _ = dockerCmdWithFail(c, "run", "--net=container", "busybox", "ps") + c.Assert(out, checker.Contains, "--net: invalid net mode: invalid container format container:") + + out, _ = dockerCmdWithFail(c, "run", "--net=weird", "busybox", "ps") + c.Assert(out, checker.Contains, "network weird not found") +} + +func (s *DockerSuite) TestConflictContainerNetworkAndLinks(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace) + + out, _ := dockerCmdWithFail(c, "run", "--net=container:other", "--link=zip:zap", "busybox", "ps") + c.Assert(out, checker.Contains, runconfig.ErrConflictContainerNetworkAndLinks.Error()) + + out, _ = dockerCmdWithFail(c, "run", "--net=host", "--link=zip:zap", "busybox", "ps") + c.Assert(out, checker.Contains, runconfig.ErrConflictHostNetworkAndLinks.Error()) +} + +func (s *DockerSuite) TestConflictNetworkModeAndOptions(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace) + + out, _ := dockerCmdWithFail(c, "run", "--net=host", "--dns=8.8.8.8", "busybox", "ps") + c.Assert(out, checker.Contains, runconfig.ErrConflictNetworkAndDNS.Error()) + + out, _ = dockerCmdWithFail(c, "run", "--net=container:other", "--dns=8.8.8.8", "busybox", "ps") + c.Assert(out, checker.Contains, runconfig.ErrConflictNetworkAndDNS.Error()) + + out, _ = dockerCmdWithFail(c, "run", "--net=host", "--add-host=name:8.8.8.8", "busybox", "ps") + c.Assert(out, checker.Contains, runconfig.ErrConflictNetworkHosts.Error()) + + out, _ = dockerCmdWithFail(c, "run", "--net=container:other", "--add-host=name:8.8.8.8", "busybox", "ps") + c.Assert(out, checker.Contains, runconfig.ErrConflictNetworkHosts.Error()) + + out, _ = dockerCmdWithFail(c, "run", "--net=host", "--mac-address=92:d0:c6:0a:29:33", "busybox", "ps") + c.Assert(out, checker.Contains, runconfig.ErrConflictContainerNetworkAndMac.Error()) + + out, _ = dockerCmdWithFail(c, "run", "--net=container:other", "--mac-address=92:d0:c6:0a:29:33", "busybox", "ps") + c.Assert(out, checker.Contains, runconfig.ErrConflictContainerNetworkAndMac.Error()) + + out, _ = dockerCmdWithFail(c, "run", "--net=container:other", "-P", "busybox", "ps") + c.Assert(out, checker.Contains, runconfig.ErrConflictNetworkPublishPorts.Error()) + + out, _ = dockerCmdWithFail(c, "run", "--net=container:other", "-p", "8080", "busybox", "ps") + c.Assert(out, checker.Contains, runconfig.ErrConflictNetworkPublishPorts.Error()) + + out, _ = dockerCmdWithFail(c, "run", "--net=container:other", "--expose", "8000-9000", "busybox", "ps") + c.Assert(out, checker.Contains, runconfig.ErrConflictNetworkExposePorts.Error()) +} diff --git a/integration-cli/docker_cli_network_unix_test.go b/integration-cli/docker_cli_network_unix_test.go new file mode 100644 index 00000000..902766d0 --- /dev/null +++ b/integration-cli/docker_cli_network_unix_test.go @@ -0,0 +1,1513 @@ +// +build !windows + +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/http/httptest" + "os" + "strings" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/runconfig" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/versions/v1p20" + "github.com/docker/libnetwork/driverapi" + remoteapi "github.com/docker/libnetwork/drivers/remote/api" + "github.com/docker/libnetwork/ipamapi" + remoteipam "github.com/docker/libnetwork/ipams/remote/api" + "github.com/docker/libnetwork/netlabel" + "github.com/go-check/check" + "github.com/vishvananda/netlink" +) + +const dummyNetworkDriver = "dummy-network-driver" +const dummyIpamDriver = "dummy-ipam-driver" + +var remoteDriverNetworkRequest remoteapi.CreateNetworkRequest + +func init() { + check.Suite(&DockerNetworkSuite{ + ds: &DockerSuite{}, + }) +} + +type DockerNetworkSuite struct { + server *httptest.Server + ds *DockerSuite + d *Daemon +} + +func (s *DockerNetworkSuite) SetUpTest(c *check.C) { + s.d = NewDaemon(c) +} + +func (s *DockerNetworkSuite) TearDownTest(c *check.C) { + s.d.Stop() + s.ds.TearDownTest(c) +} + +func (s *DockerNetworkSuite) SetUpSuite(c *check.C) { + mux := http.NewServeMux() + s.server = httptest.NewServer(mux) + c.Assert(s.server, check.NotNil, check.Commentf("Failed to start a HTTP Server")) + setupRemoteNetworkDrivers(c, mux, s.server.URL, dummyNetworkDriver, dummyIpamDriver) +} + +func setupRemoteNetworkDrivers(c *check.C, mux *http.ServeMux, url, netDrv, ipamDrv string) { + + mux.HandleFunc("/Plugin.Activate", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + fmt.Fprintf(w, `{"Implements": ["%s", "%s"]}`, driverapi.NetworkPluginEndpointType, ipamapi.PluginEndpointType) + }) + + // Network driver implementation + mux.HandleFunc(fmt.Sprintf("/%s.GetCapabilities", driverapi.NetworkPluginEndpointType), func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + fmt.Fprintf(w, `{"Scope":"local"}`) + }) + + mux.HandleFunc(fmt.Sprintf("/%s.CreateNetwork", driverapi.NetworkPluginEndpointType), func(w http.ResponseWriter, r *http.Request) { + err := json.NewDecoder(r.Body).Decode(&remoteDriverNetworkRequest) + if err != nil { + http.Error(w, "Unable to decode JSON payload: "+err.Error(), http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + fmt.Fprintf(w, "null") + }) + + mux.HandleFunc(fmt.Sprintf("/%s.DeleteNetwork", driverapi.NetworkPluginEndpointType), func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + fmt.Fprintf(w, "null") + }) + + mux.HandleFunc(fmt.Sprintf("/%s.CreateEndpoint", driverapi.NetworkPluginEndpointType), func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + fmt.Fprintf(w, `{"Interface":{"MacAddress":"a0:b1:c2:d3:e4:f5"}}`) + }) + + mux.HandleFunc(fmt.Sprintf("/%s.Join", driverapi.NetworkPluginEndpointType), func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + + veth := &netlink.Veth{ + LinkAttrs: netlink.LinkAttrs{Name: "randomIfName", TxQLen: 0}, PeerName: "cnt0"} + if err := netlink.LinkAdd(veth); err != nil { + fmt.Fprintf(w, `{"Error":"failed to add veth pair: `+err.Error()+`"}`) + } else { + fmt.Fprintf(w, `{"InterfaceName":{ "SrcName":"cnt0", "DstPrefix":"veth"}}`) + } + }) + + mux.HandleFunc(fmt.Sprintf("/%s.Leave", driverapi.NetworkPluginEndpointType), func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + fmt.Fprintf(w, "null") + }) + + mux.HandleFunc(fmt.Sprintf("/%s.DeleteEndpoint", driverapi.NetworkPluginEndpointType), func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + if link, err := netlink.LinkByName("cnt0"); err == nil { + netlink.LinkDel(link) + } + fmt.Fprintf(w, "null") + }) + + // Ipam Driver implementation + var ( + poolRequest remoteipam.RequestPoolRequest + poolReleaseReq remoteipam.ReleasePoolRequest + addressRequest remoteipam.RequestAddressRequest + addressReleaseReq remoteipam.ReleaseAddressRequest + lAS = "localAS" + gAS = "globalAS" + pool = "172.28.0.0/16" + poolID = lAS + "/" + pool + gw = "172.28.255.254/16" + ) + + mux.HandleFunc(fmt.Sprintf("/%s.GetDefaultAddressSpaces", ipamapi.PluginEndpointType), func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + fmt.Fprintf(w, `{"LocalDefaultAddressSpace":"`+lAS+`", "GlobalDefaultAddressSpace": "`+gAS+`"}`) + }) + + mux.HandleFunc(fmt.Sprintf("/%s.RequestPool", ipamapi.PluginEndpointType), func(w http.ResponseWriter, r *http.Request) { + err := json.NewDecoder(r.Body).Decode(&poolRequest) + if err != nil { + http.Error(w, "Unable to decode JSON payload: "+err.Error(), http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + if poolRequest.AddressSpace != lAS && poolRequest.AddressSpace != gAS { + fmt.Fprintf(w, `{"Error":"Unknown address space in pool request: `+poolRequest.AddressSpace+`"}`) + } else if poolRequest.Pool != "" && poolRequest.Pool != pool { + fmt.Fprintf(w, `{"Error":"Cannot handle explicit pool requests yet"}`) + } else { + fmt.Fprintf(w, `{"PoolID":"`+poolID+`", "Pool":"`+pool+`"}`) + } + }) + + mux.HandleFunc(fmt.Sprintf("/%s.RequestAddress", ipamapi.PluginEndpointType), func(w http.ResponseWriter, r *http.Request) { + err := json.NewDecoder(r.Body).Decode(&addressRequest) + if err != nil { + http.Error(w, "Unable to decode JSON payload: "+err.Error(), http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + // make sure libnetwork is now querying on the expected pool id + if addressRequest.PoolID != poolID { + fmt.Fprintf(w, `{"Error":"unknown pool id"}`) + } else if addressRequest.Address != "" { + fmt.Fprintf(w, `{"Error":"Cannot handle explicit address requests yet"}`) + } else { + fmt.Fprintf(w, `{"Address":"`+gw+`"}`) + } + }) + + mux.HandleFunc(fmt.Sprintf("/%s.ReleaseAddress", ipamapi.PluginEndpointType), func(w http.ResponseWriter, r *http.Request) { + err := json.NewDecoder(r.Body).Decode(&addressReleaseReq) + if err != nil { + http.Error(w, "Unable to decode JSON payload: "+err.Error(), http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + // make sure libnetwork is now asking to release the expected address from the expected poolid + if addressRequest.PoolID != poolID { + fmt.Fprintf(w, `{"Error":"unknown pool id"}`) + } else if addressReleaseReq.Address != gw { + fmt.Fprintf(w, `{"Error":"unknown address"}`) + } else { + fmt.Fprintf(w, "null") + } + }) + + mux.HandleFunc(fmt.Sprintf("/%s.ReleasePool", ipamapi.PluginEndpointType), func(w http.ResponseWriter, r *http.Request) { + err := json.NewDecoder(r.Body).Decode(&poolReleaseReq) + if err != nil { + http.Error(w, "Unable to decode JSON payload: "+err.Error(), http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + // make sure libnetwork is now asking to release the expected poolid + if addressRequest.PoolID != poolID { + fmt.Fprintf(w, `{"Error":"unknown pool id"}`) + } else { + fmt.Fprintf(w, "null") + } + }) + + err := os.MkdirAll("/etc/docker/plugins", 0755) + c.Assert(err, checker.IsNil) + + fileName := fmt.Sprintf("/etc/docker/plugins/%s.spec", netDrv) + err = ioutil.WriteFile(fileName, []byte(url), 0644) + c.Assert(err, checker.IsNil) + + ipamFileName := fmt.Sprintf("/etc/docker/plugins/%s.spec", ipamDrv) + err = ioutil.WriteFile(ipamFileName, []byte(url), 0644) + c.Assert(err, checker.IsNil) +} + +func (s *DockerNetworkSuite) TearDownSuite(c *check.C) { + if s.server == nil { + return + } + + s.server.Close() + + err := os.RemoveAll("/etc/docker/plugins") + c.Assert(err, checker.IsNil) +} + +func assertNwIsAvailable(c *check.C, name string) { + if !isNwPresent(c, name) { + c.Fatalf("Network %s not found in network ls o/p", name) + } +} + +func assertNwNotAvailable(c *check.C, name string) { + if isNwPresent(c, name) { + c.Fatalf("Found network %s in network ls o/p", name) + } +} + +func isNwPresent(c *check.C, name string) bool { + out, _ := dockerCmd(c, "network", "ls") + lines := strings.Split(out, "\n") + for i := 1; i < len(lines)-1; i++ { + netFields := strings.Fields(lines[i]) + if netFields[1] == name { + return true + } + } + return false +} + +// assertNwList checks network list retrieved with ls command +// equals to expected network list +// note: out should be `network ls [option]` result +func assertNwList(c *check.C, out string, expectNws []string) { + lines := strings.Split(out, "\n") + var nwList []string + for _, line := range lines[1 : len(lines)-1] { + netFields := strings.Fields(line) + // wrap all network name in nwList + nwList = append(nwList, netFields[1]) + } + + // network ls should contains all expected networks + c.Assert(nwList, checker.DeepEquals, expectNws) +} + +func getNwResource(c *check.C, name string) *types.NetworkResource { + out, _ := dockerCmd(c, "network", "inspect", name) + nr := []types.NetworkResource{} + err := json.Unmarshal([]byte(out), &nr) + c.Assert(err, check.IsNil) + return &nr[0] +} + +func (s *DockerNetworkSuite) TestDockerNetworkLsDefault(c *check.C) { + defaults := []string{"bridge", "host", "none"} + for _, nn := range defaults { + assertNwIsAvailable(c, nn) + } +} + +func (s *DockerNetworkSuite) TestDockerNetworkCreatePredefined(c *check.C) { + predefined := []string{"bridge", "host", "none", "default"} + for _, net := range predefined { + // predefined networks can't be created again + out, _, err := dockerCmdWithError("network", "create", net) + c.Assert(err, checker.NotNil, check.Commentf("%v", out)) + } +} + +func (s *DockerNetworkSuite) TestDockerNetworkCreateHostBind(c *check.C) { + dockerCmd(c, "network", "create", "--subnet=192.168.10.0/24", "--gateway=192.168.10.1", "-o", "com.docker.network.bridge.host_binding_ipv4=192.168.10.1", "testbind") + assertNwIsAvailable(c, "testbind") + + out, _ := runSleepingContainer(c, "--net=testbind", "-p", "5000:5000") + id := strings.TrimSpace(out) + c.Assert(waitRun(id), checker.IsNil) + out, _ = dockerCmd(c, "ps") + c.Assert(out, checker.Contains, "192.168.10.1:5000->5000/tcp") +} + +func (s *DockerNetworkSuite) TestDockerNetworkRmPredefined(c *check.C) { + predefined := []string{"bridge", "host", "none", "default"} + for _, net := range predefined { + // predefined networks can't be removed + out, _, err := dockerCmdWithError("network", "rm", net) + c.Assert(err, checker.NotNil, check.Commentf("%v", out)) + } +} + +func (s *DockerNetworkSuite) TestDockerNetworkLsFilter(c *check.C) { + out, _ := dockerCmd(c, "network", "create", "dev") + defer func() { + dockerCmd(c, "network", "rm", "dev") + }() + networkID := strings.TrimSpace(out) + + // filter with partial ID and partial name + // only show 'bridge' and 'dev' network + out, _ = dockerCmd(c, "network", "ls", "-f", "id="+networkID[0:5], "-f", "name=dge") + assertNwList(c, out, []string{"bridge", "dev"}) + + // only show built-in network (bridge, none, host) + out, _ = dockerCmd(c, "network", "ls", "-f", "type=builtin") + assertNwList(c, out, []string{"bridge", "host", "none"}) + + // only show custom networks (dev) + out, _ = dockerCmd(c, "network", "ls", "-f", "type=custom") + assertNwList(c, out, []string{"dev"}) + + // show all networks with filter + // it should be equivalent of ls without option + out, _ = dockerCmd(c, "network", "ls", "-f", "type=custom", "-f", "type=builtin") + assertNwList(c, out, []string{"bridge", "dev", "host", "none"}) +} + +func (s *DockerNetworkSuite) TestDockerNetworkCreateDelete(c *check.C) { + dockerCmd(c, "network", "create", "test") + assertNwIsAvailable(c, "test") + + dockerCmd(c, "network", "rm", "test") + assertNwNotAvailable(c, "test") +} + +func (s *DockerNetworkSuite) TestDockerNetworkCreateLabel(c *check.C) { + testNet := "testnetcreatelabel" + testLabel := "foo" + testValue := "bar" + + dockerCmd(c, "network", "create", "--label", testLabel+"="+testValue, testNet) + assertNwIsAvailable(c, testNet) + + out, _, err := dockerCmdWithError("network", "inspect", "--format='{{ .Labels."+testLabel+" }}'", testNet) + c.Assert(err, check.IsNil) + c.Assert(strings.TrimSpace(out), check.Equals, testValue) + + dockerCmd(c, "network", "rm", testNet) + assertNwNotAvailable(c, testNet) +} + +func (s *DockerSuite) TestDockerNetworkDeleteNotExists(c *check.C) { + out, _, err := dockerCmdWithError("network", "rm", "test") + c.Assert(err, checker.NotNil, check.Commentf("%v", out)) +} + +func (s *DockerSuite) TestDockerNetworkDeleteMultiple(c *check.C) { + dockerCmd(c, "network", "create", "testDelMulti0") + assertNwIsAvailable(c, "testDelMulti0") + dockerCmd(c, "network", "create", "testDelMulti1") + assertNwIsAvailable(c, "testDelMulti1") + dockerCmd(c, "network", "create", "testDelMulti2") + assertNwIsAvailable(c, "testDelMulti2") + out, _ := dockerCmd(c, "run", "-d", "--net", "testDelMulti2", "busybox", "top") + containerID := strings.TrimSpace(out) + waitRun(containerID) + + // delete three networks at the same time, since testDelMulti2 + // contains active container, its deletion should fail. + out, _, err := dockerCmdWithError("network", "rm", "testDelMulti0", "testDelMulti1", "testDelMulti2") + // err should not be nil due to deleting testDelMulti2 failed. + c.Assert(err, checker.NotNil, check.Commentf("out: %s", out)) + // testDelMulti2 should fail due to network has active endpoints + c.Assert(out, checker.Contains, "has active endpoints") + assertNwNotAvailable(c, "testDelMulti0") + assertNwNotAvailable(c, "testDelMulti1") + // testDelMulti2 can't be deleted, so it should exist + assertNwIsAvailable(c, "testDelMulti2") +} + +func (s *DockerSuite) TestDockerNetworkInspect(c *check.C) { + out, _ := dockerCmd(c, "network", "inspect", "host") + networkResources := []types.NetworkResource{} + err := json.Unmarshal([]byte(out), &networkResources) + c.Assert(err, check.IsNil) + c.Assert(networkResources, checker.HasLen, 1) + + out, _ = dockerCmd(c, "network", "inspect", "--format='{{ .Name }}'", "host") + c.Assert(strings.TrimSpace(out), check.Equals, "host") +} + +func (s *DockerSuite) TestDockerInspectMultipleNetwork(c *check.C) { + out, _ := dockerCmd(c, "network", "inspect", "host", "none") + networkResources := []types.NetworkResource{} + err := json.Unmarshal([]byte(out), &networkResources) + c.Assert(err, check.IsNil) + c.Assert(networkResources, checker.HasLen, 2) + + // Should print an error, return an exitCode 1 *but* should print the host network + out, exitCode, err := dockerCmdWithError("network", "inspect", "host", "nonexistent") + c.Assert(err, checker.NotNil) + c.Assert(exitCode, checker.Equals, 1) + c.Assert(out, checker.Contains, "Error: No such network: nonexistent") + networkResources = []types.NetworkResource{} + inspectOut := strings.SplitN(out, "\nError: No such network: nonexistent\n", 2)[0] + err = json.Unmarshal([]byte(inspectOut), &networkResources) + c.Assert(networkResources, checker.HasLen, 1) + + // Should print an error and return an exitCode, nothing else + out, exitCode, err = dockerCmdWithError("network", "inspect", "nonexistent") + c.Assert(err, checker.NotNil) + c.Assert(exitCode, checker.Equals, 1) + c.Assert(out, checker.Contains, "Error: No such network: nonexistent") +} + +func (s *DockerSuite) TestDockerInspectNetworkWithContainerName(c *check.C) { + dockerCmd(c, "network", "create", "brNetForInspect") + assertNwIsAvailable(c, "brNetForInspect") + defer func() { + dockerCmd(c, "network", "rm", "brNetForInspect") + assertNwNotAvailable(c, "brNetForInspect") + }() + + out, _ := dockerCmd(c, "run", "-d", "--name", "testNetInspect1", "--net", "brNetForInspect", "busybox", "top") + c.Assert(waitRun("testNetInspect1"), check.IsNil) + containerID := strings.TrimSpace(out) + defer func() { + // we don't stop container by name, because we'll rename it later + dockerCmd(c, "stop", containerID) + }() + + out, _ = dockerCmd(c, "network", "inspect", "brNetForInspect") + networkResources := []types.NetworkResource{} + err := json.Unmarshal([]byte(out), &networkResources) + c.Assert(err, check.IsNil) + c.Assert(networkResources, checker.HasLen, 1) + container, ok := networkResources[0].Containers[containerID] + c.Assert(ok, checker.True) + c.Assert(container.Name, checker.Equals, "testNetInspect1") + + // rename container and check docker inspect output update + newName := "HappyNewName" + dockerCmd(c, "rename", "testNetInspect1", newName) + + // check whether network inspect works properly + out, _ = dockerCmd(c, "network", "inspect", "brNetForInspect") + newNetRes := []types.NetworkResource{} + err = json.Unmarshal([]byte(out), &newNetRes) + c.Assert(err, check.IsNil) + c.Assert(newNetRes, checker.HasLen, 1) + container1, ok := newNetRes[0].Containers[containerID] + c.Assert(ok, checker.True) + c.Assert(container1.Name, checker.Equals, newName) + +} + +func (s *DockerNetworkSuite) TestDockerNetworkConnectDisconnect(c *check.C) { + dockerCmd(c, "network", "create", "test") + assertNwIsAvailable(c, "test") + nr := getNwResource(c, "test") + + c.Assert(nr.Name, checker.Equals, "test") + c.Assert(len(nr.Containers), checker.Equals, 0) + + // run a container + out, _ := dockerCmd(c, "run", "-d", "--name", "test", "busybox", "top") + c.Assert(waitRun("test"), check.IsNil) + containerID := strings.TrimSpace(out) + + // connect the container to the test network + dockerCmd(c, "network", "connect", "test", containerID) + + // inspect the network to make sure container is connected + nr = getNetworkResource(c, nr.ID) + c.Assert(len(nr.Containers), checker.Equals, 1) + c.Assert(nr.Containers[containerID], check.NotNil) + + // check if container IP matches network inspect + ip, _, err := net.ParseCIDR(nr.Containers[containerID].IPv4Address) + c.Assert(err, check.IsNil) + containerIP := findContainerIP(c, "test", "test") + c.Assert(ip.String(), checker.Equals, containerIP) + + // disconnect container from the network + dockerCmd(c, "network", "disconnect", "test", containerID) + nr = getNwResource(c, "test") + c.Assert(nr.Name, checker.Equals, "test") + c.Assert(len(nr.Containers), checker.Equals, 0) + + // run another container + out, _ = dockerCmd(c, "run", "-d", "--net", "test", "--name", "test2", "busybox", "top") + c.Assert(waitRun("test2"), check.IsNil) + containerID = strings.TrimSpace(out) + + nr = getNwResource(c, "test") + c.Assert(nr.Name, checker.Equals, "test") + c.Assert(len(nr.Containers), checker.Equals, 1) + + // force disconnect the container to the test network + dockerCmd(c, "network", "disconnect", "-f", "test", containerID) + + nr = getNwResource(c, "test") + c.Assert(nr.Name, checker.Equals, "test") + c.Assert(len(nr.Containers), checker.Equals, 0) + + dockerCmd(c, "network", "rm", "test") + assertNwNotAvailable(c, "test") +} + +func (s *DockerNetworkSuite) TestDockerNetworkIpamMultipleNetworks(c *check.C) { + // test0 bridge network + dockerCmd(c, "network", "create", "--subnet=192.168.0.0/16", "test1") + assertNwIsAvailable(c, "test1") + + // test2 bridge network does not overlap + dockerCmd(c, "network", "create", "--subnet=192.169.0.0/16", "test2") + assertNwIsAvailable(c, "test2") + + // for networks w/o ipam specified, docker will choose proper non-overlapping subnets + dockerCmd(c, "network", "create", "test3") + assertNwIsAvailable(c, "test3") + dockerCmd(c, "network", "create", "test4") + assertNwIsAvailable(c, "test4") + dockerCmd(c, "network", "create", "test5") + assertNwIsAvailable(c, "test5") + + // test network with multiple subnets + // bridge network doesn't support multiple subnets. hence, use a dummy driver that supports + + dockerCmd(c, "network", "create", "-d", dummyNetworkDriver, "--subnet=192.168.0.0/16", "--subnet=192.170.0.0/16", "test6") + assertNwIsAvailable(c, "test6") + + // test network with multiple subnets with valid ipam combinations + // also check same subnet across networks when the driver supports it. + dockerCmd(c, "network", "create", "-d", dummyNetworkDriver, + "--subnet=192.168.0.0/16", "--subnet=192.170.0.0/16", + "--gateway=192.168.0.100", "--gateway=192.170.0.100", + "--ip-range=192.168.1.0/24", + "--aux-address", "a=192.168.1.5", "--aux-address", "b=192.168.1.6", + "--aux-address", "a=192.170.1.5", "--aux-address", "b=192.170.1.6", + "test7") + assertNwIsAvailable(c, "test7") + + // cleanup + for i := 1; i < 8; i++ { + dockerCmd(c, "network", "rm", fmt.Sprintf("test%d", i)) + } +} + +func (s *DockerNetworkSuite) TestDockerNetworkCustomIpam(c *check.C) { + // Create a bridge network using custom ipam driver + dockerCmd(c, "network", "create", "--ipam-driver", dummyIpamDriver, "br0") + assertNwIsAvailable(c, "br0") + + // Verify expected network ipam fields are there + nr := getNetworkResource(c, "br0") + c.Assert(nr.Driver, checker.Equals, "bridge") + c.Assert(nr.IPAM.Driver, checker.Equals, dummyIpamDriver) + + // remove network and exercise remote ipam driver + dockerCmd(c, "network", "rm", "br0") + assertNwNotAvailable(c, "br0") +} + +func (s *DockerNetworkSuite) TestDockerNetworkIpamOptions(c *check.C) { + // Create a bridge network using custom ipam driver and options + dockerCmd(c, "network", "create", "--ipam-driver", dummyIpamDriver, "--ipam-opt", "opt1=drv1", "--ipam-opt", "opt2=drv2", "br0") + assertNwIsAvailable(c, "br0") + + // Verify expected network ipam options + nr := getNetworkResource(c, "br0") + opts := nr.IPAM.Options + c.Assert(opts["opt1"], checker.Equals, "drv1") + c.Assert(opts["opt2"], checker.Equals, "drv2") +} + +func (s *DockerNetworkSuite) TestDockerNetworkInspectDefault(c *check.C) { + nr := getNetworkResource(c, "none") + c.Assert(nr.Driver, checker.Equals, "null") + c.Assert(nr.Scope, checker.Equals, "local") + c.Assert(nr.Internal, checker.Equals, false) + c.Assert(nr.EnableIPv6, checker.Equals, false) + c.Assert(nr.IPAM.Driver, checker.Equals, "default") + c.Assert(len(nr.IPAM.Config), checker.Equals, 0) + + nr = getNetworkResource(c, "host") + c.Assert(nr.Driver, checker.Equals, "host") + c.Assert(nr.Scope, checker.Equals, "local") + c.Assert(nr.Internal, checker.Equals, false) + c.Assert(nr.EnableIPv6, checker.Equals, false) + c.Assert(nr.IPAM.Driver, checker.Equals, "default") + c.Assert(len(nr.IPAM.Config), checker.Equals, 0) + + nr = getNetworkResource(c, "bridge") + c.Assert(nr.Driver, checker.Equals, "bridge") + c.Assert(nr.Scope, checker.Equals, "local") + c.Assert(nr.Internal, checker.Equals, false) + c.Assert(nr.EnableIPv6, checker.Equals, false) + c.Assert(nr.IPAM.Driver, checker.Equals, "default") + c.Assert(len(nr.IPAM.Config), checker.Equals, 1) + c.Assert(nr.IPAM.Config[0].Subnet, checker.NotNil) + c.Assert(nr.IPAM.Config[0].Gateway, checker.NotNil) +} + +func (s *DockerNetworkSuite) TestDockerNetworkInspectCustomUnspecified(c *check.C) { + // if unspecified, network subnet will be selected from inside preferred pool + dockerCmd(c, "network", "create", "test01") + assertNwIsAvailable(c, "test01") + + nr := getNetworkResource(c, "test01") + c.Assert(nr.Driver, checker.Equals, "bridge") + c.Assert(nr.Scope, checker.Equals, "local") + c.Assert(nr.Internal, checker.Equals, false) + c.Assert(nr.EnableIPv6, checker.Equals, false) + c.Assert(nr.IPAM.Driver, checker.Equals, "default") + c.Assert(len(nr.IPAM.Config), checker.Equals, 1) + c.Assert(nr.IPAM.Config[0].Subnet, checker.NotNil) + c.Assert(nr.IPAM.Config[0].Gateway, checker.NotNil) + + dockerCmd(c, "network", "rm", "test01") + assertNwNotAvailable(c, "test01") +} + +func (s *DockerNetworkSuite) TestDockerNetworkInspectCustomSpecified(c *check.C) { + dockerCmd(c, "network", "create", "--driver=bridge", "--ipv6", "--subnet=172.28.0.0/16", "--ip-range=172.28.5.0/24", "--gateway=172.28.5.254", "br0") + assertNwIsAvailable(c, "br0") + + nr := getNetworkResource(c, "br0") + c.Assert(nr.Driver, checker.Equals, "bridge") + c.Assert(nr.Scope, checker.Equals, "local") + c.Assert(nr.Internal, checker.Equals, false) + c.Assert(nr.EnableIPv6, checker.Equals, true) + c.Assert(nr.IPAM.Driver, checker.Equals, "default") + c.Assert(len(nr.IPAM.Config), checker.Equals, 1) + c.Assert(nr.IPAM.Config[0].Subnet, checker.Equals, "172.28.0.0/16") + c.Assert(nr.IPAM.Config[0].IPRange, checker.Equals, "172.28.5.0/24") + c.Assert(nr.IPAM.Config[0].Gateway, checker.Equals, "172.28.5.254") + c.Assert(nr.Internal, checker.False) + dockerCmd(c, "network", "rm", "br0") + assertNwNotAvailable(c, "test01") +} + +func (s *DockerNetworkSuite) TestDockerNetworkIpamInvalidCombinations(c *check.C) { + // network with ip-range out of subnet range + _, _, err := dockerCmdWithError("network", "create", "--subnet=192.168.0.0/16", "--ip-range=192.170.0.0/16", "test") + c.Assert(err, check.NotNil) + + // network with multiple gateways for a single subnet + _, _, err = dockerCmdWithError("network", "create", "--subnet=192.168.0.0/16", "--gateway=192.168.0.1", "--gateway=192.168.0.2", "test") + c.Assert(err, check.NotNil) + + // Multiple overlapping subnets in the same network must fail + _, _, err = dockerCmdWithError("network", "create", "--subnet=192.168.0.0/16", "--subnet=192.168.1.0/16", "test") + c.Assert(err, check.NotNil) + + // overlapping subnets across networks must fail + // create a valid test0 network + dockerCmd(c, "network", "create", "--subnet=192.168.0.0/16", "test0") + assertNwIsAvailable(c, "test0") + // create an overlapping test1 network + _, _, err = dockerCmdWithError("network", "create", "--subnet=192.168.128.0/17", "test1") + c.Assert(err, check.NotNil) + dockerCmd(c, "network", "rm", "test0") + assertNwNotAvailable(c, "test0") +} + +func (s *DockerNetworkSuite) TestDockerNetworkDriverOptions(c *check.C) { + dockerCmd(c, "network", "create", "-d", dummyNetworkDriver, "-o", "opt1=drv1", "-o", "opt2=drv2", "testopt") + assertNwIsAvailable(c, "testopt") + gopts := remoteDriverNetworkRequest.Options[netlabel.GenericData] + c.Assert(gopts, checker.NotNil) + opts, ok := gopts.(map[string]interface{}) + c.Assert(ok, checker.Equals, true) + c.Assert(opts["opt1"], checker.Equals, "drv1") + c.Assert(opts["opt2"], checker.Equals, "drv2") + dockerCmd(c, "network", "rm", "testopt") + assertNwNotAvailable(c, "testopt") + +} + +func (s *DockerDaemonSuite) TestDockerNetworkNoDiscoveryDefaultBridgeNetwork(c *check.C) { + testRequires(c, ExecSupport) + // On default bridge network built-in service discovery should not happen + hostsFile := "/etc/hosts" + bridgeName := "external-bridge" + bridgeIP := "192.169.255.254/24" + out, err := createInterface(c, "bridge", bridgeName, bridgeIP) + c.Assert(err, check.IsNil, check.Commentf(out)) + defer deleteInterface(c, bridgeName) + + err = s.d.StartWithBusybox("--bridge", bridgeName) + c.Assert(err, check.IsNil) + defer s.d.Restart() + + // run two containers and store first container's etc/hosts content + out, err = s.d.Cmd("run", "-d", "busybox", "top") + c.Assert(err, check.IsNil) + cid1 := strings.TrimSpace(out) + defer s.d.Cmd("stop", cid1) + + hosts, err := s.d.Cmd("exec", cid1, "cat", hostsFile) + c.Assert(err, checker.IsNil) + + out, err = s.d.Cmd("run", "-d", "--name", "container2", "busybox", "top") + c.Assert(err, check.IsNil) + cid2 := strings.TrimSpace(out) + + // verify first container's etc/hosts file has not changed after spawning the second named container + hostsPost, err := s.d.Cmd("exec", cid1, "cat", hostsFile) + c.Assert(err, checker.IsNil) + c.Assert(string(hosts), checker.Equals, string(hostsPost), + check.Commentf("Unexpected %s change on second container creation", hostsFile)) + + // stop container 2 and verify first container's etc/hosts has not changed + _, err = s.d.Cmd("stop", cid2) + c.Assert(err, check.IsNil) + + hostsPost, err = s.d.Cmd("exec", cid1, "cat", hostsFile) + c.Assert(err, checker.IsNil) + c.Assert(string(hosts), checker.Equals, string(hostsPost), + check.Commentf("Unexpected %s change on second container creation", hostsFile)) + + // but discovery is on when connecting to non default bridge network + network := "anotherbridge" + out, err = s.d.Cmd("network", "create", network) + c.Assert(err, check.IsNil, check.Commentf(out)) + defer s.d.Cmd("network", "rm", network) + + out, err = s.d.Cmd("network", "connect", network, cid1) + c.Assert(err, check.IsNil, check.Commentf(out)) + + hosts, err = s.d.Cmd("exec", cid1, "cat", hostsFile) + c.Assert(err, checker.IsNil) + + hostsPost, err = s.d.Cmd("exec", cid1, "cat", hostsFile) + c.Assert(err, checker.IsNil) + c.Assert(string(hosts), checker.Equals, string(hostsPost), + check.Commentf("Unexpected %s change on second network connection", hostsFile)) +} + +func (s *DockerNetworkSuite) TestDockerNetworkAnonymousEndpoint(c *check.C) { + testRequires(c, ExecSupport, NotArm) + hostsFile := "/etc/hosts" + cstmBridgeNw := "custom-bridge-nw" + cstmBridgeNw1 := "custom-bridge-nw1" + + dockerCmd(c, "network", "create", "-d", "bridge", cstmBridgeNw) + assertNwIsAvailable(c, cstmBridgeNw) + + // run two anonymous containers and store their etc/hosts content + out, _ := dockerCmd(c, "run", "-d", "--net", cstmBridgeNw, "busybox", "top") + cid1 := strings.TrimSpace(out) + + hosts1, err := readContainerFileWithExec(cid1, hostsFile) + c.Assert(err, checker.IsNil) + + out, _ = dockerCmd(c, "run", "-d", "--net", cstmBridgeNw, "busybox", "top") + cid2 := strings.TrimSpace(out) + + hosts2, err := readContainerFileWithExec(cid2, hostsFile) + c.Assert(err, checker.IsNil) + + // verify first container etc/hosts file has not changed + hosts1post, err := readContainerFileWithExec(cid1, hostsFile) + c.Assert(err, checker.IsNil) + c.Assert(string(hosts1), checker.Equals, string(hosts1post), + check.Commentf("Unexpected %s change on anonymous container creation", hostsFile)) + + // Connect the 2nd container to a new network and verify the + // first container /etc/hosts file still hasn't changed. + dockerCmd(c, "network", "create", "-d", "bridge", cstmBridgeNw1) + assertNwIsAvailable(c, cstmBridgeNw1) + + dockerCmd(c, "network", "connect", cstmBridgeNw1, cid2) + + hosts2, err = readContainerFileWithExec(cid2, hostsFile) + c.Assert(err, checker.IsNil) + + hosts1post, err = readContainerFileWithExec(cid1, hostsFile) + c.Assert(err, checker.IsNil) + c.Assert(string(hosts1), checker.Equals, string(hosts1post), + check.Commentf("Unexpected %s change on container connect", hostsFile)) + + // start a named container + cName := "AnyName" + out, _ = dockerCmd(c, "run", "-d", "--net", cstmBridgeNw, "--name", cName, "busybox", "top") + cid3 := strings.TrimSpace(out) + + // verify that container 1 and 2 can ping the named container + dockerCmd(c, "exec", cid1, "ping", "-c", "1", cName) + dockerCmd(c, "exec", cid2, "ping", "-c", "1", cName) + + // Stop named container and verify first two containers' etc/hosts file hasn't changed + dockerCmd(c, "stop", cid3) + hosts1post, err = readContainerFileWithExec(cid1, hostsFile) + c.Assert(err, checker.IsNil) + c.Assert(string(hosts1), checker.Equals, string(hosts1post), + check.Commentf("Unexpected %s change on name container creation", hostsFile)) + + hosts2post, err := readContainerFileWithExec(cid2, hostsFile) + c.Assert(err, checker.IsNil) + c.Assert(string(hosts2), checker.Equals, string(hosts2post), + check.Commentf("Unexpected %s change on name container creation", hostsFile)) + + // verify that container 1 and 2 can't ping the named container now + _, _, err = dockerCmdWithError("exec", cid1, "ping", "-c", "1", cName) + c.Assert(err, check.NotNil) + _, _, err = dockerCmdWithError("exec", cid2, "ping", "-c", "1", cName) + c.Assert(err, check.NotNil) +} + +func (s *DockerNetworkSuite) TestDockerNetworkLinkOnDefaultNetworkOnly(c *check.C) { + // Legacy Link feature must work only on default network, and not across networks + cnt1 := "container1" + cnt2 := "container2" + network := "anotherbridge" + + // Run first container on default network + dockerCmd(c, "run", "-d", "--name", cnt1, "busybox", "top") + + // Create another network and run the second container on it + dockerCmd(c, "network", "create", network) + assertNwIsAvailable(c, network) + dockerCmd(c, "run", "-d", "--net", network, "--name", cnt2, "busybox", "top") + + // Try launching a container on default network, linking to the first container. Must succeed + dockerCmd(c, "run", "-d", "--link", fmt.Sprintf("%s:%s", cnt1, cnt1), "busybox", "top") + + // Try launching a container on default network, linking to the second container. Must fail + _, _, err := dockerCmdWithError("run", "-d", "--link", fmt.Sprintf("%s:%s", cnt2, cnt2), "busybox", "top") + c.Assert(err, checker.NotNil) + + // Connect second container to default network. Now a container on default network can link to it + dockerCmd(c, "network", "connect", "bridge", cnt2) + dockerCmd(c, "run", "-d", "--link", fmt.Sprintf("%s:%s", cnt2, cnt2), "busybox", "top") +} + +func (s *DockerNetworkSuite) TestDockerNetworkOverlayPortMapping(c *check.C) { + // Verify exposed ports are present in ps output when running a container on + // a network managed by a driver which does not provide the default gateway + // for the container + nwn := "ov" + ctn := "bb" + port1 := 80 + port2 := 443 + expose1 := fmt.Sprintf("--expose=%d", port1) + expose2 := fmt.Sprintf("--expose=%d", port2) + + dockerCmd(c, "network", "create", "-d", dummyNetworkDriver, nwn) + assertNwIsAvailable(c, nwn) + + dockerCmd(c, "run", "-d", "--net", nwn, "--name", ctn, expose1, expose2, "busybox", "top") + + // Check docker ps o/p for last created container reports the unpublished ports + unpPort1 := fmt.Sprintf("%d/tcp", port1) + unpPort2 := fmt.Sprintf("%d/tcp", port2) + out, _ := dockerCmd(c, "ps", "-n=1") + // Missing unpublished ports in docker ps output + c.Assert(out, checker.Contains, unpPort1) + // Missing unpublished ports in docker ps output + c.Assert(out, checker.Contains, unpPort2) +} + +func (s *DockerNetworkSuite) TestDockerNetworkDriverUngracefulRestart(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace) + dnd := "dnd" + did := "did" + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + setupRemoteNetworkDrivers(c, mux, server.URL, dnd, did) + + s.d.StartWithBusybox() + _, err := s.d.Cmd("network", "create", "-d", dnd, "--subnet", "1.1.1.0/24", "net1") + c.Assert(err, checker.IsNil) + + _, err = s.d.Cmd("run", "-itd", "--net", "net1", "--name", "foo", "--ip", "1.1.1.10", "busybox", "sh") + c.Assert(err, checker.IsNil) + + // Kill daemon and restart + if err = s.d.cmd.Process.Kill(); err != nil { + c.Fatal(err) + } + + server.Close() + + startTime := time.Now().Unix() + if err = s.d.Restart(); err != nil { + c.Fatal(err) + } + lapse := time.Now().Unix() - startTime + if lapse > 60 { + // In normal scenarios, daemon restart takes ~1 second. + // Plugin retry mechanism can delay the daemon start. systemd may not like it. + // Avoid accessing plugins during daemon bootup + c.Logf("daemon restart took too long : %d seconds", lapse) + } + + // Restart the custom dummy plugin + mux = http.NewServeMux() + server = httptest.NewServer(mux) + setupRemoteNetworkDrivers(c, mux, server.URL, dnd, did) + + // trying to reuse the same ip must succeed + _, err = s.d.Cmd("run", "-itd", "--net", "net1", "--name", "bar", "--ip", "1.1.1.10", "busybox", "sh") + c.Assert(err, checker.IsNil) +} + +func (s *DockerNetworkSuite) TestDockerNetworkMacInspect(c *check.C) { + // Verify endpoint MAC address is correctly populated in container's network settings + nwn := "ov" + ctn := "bb" + + dockerCmd(c, "network", "create", "-d", dummyNetworkDriver, nwn) + assertNwIsAvailable(c, nwn) + + dockerCmd(c, "run", "-d", "--net", nwn, "--name", ctn, "busybox", "top") + + mac := inspectField(c, ctn, "NetworkSettings.Networks."+nwn+".MacAddress") + c.Assert(mac, checker.Equals, "a0:b1:c2:d3:e4:f5") +} + +func (s *DockerSuite) TestInspectApiMultipleNetworks(c *check.C) { + dockerCmd(c, "network", "create", "mybridge1") + dockerCmd(c, "network", "create", "mybridge2") + out, _ := dockerCmd(c, "run", "-d", "busybox", "top") + id := strings.TrimSpace(out) + c.Assert(waitRun(id), check.IsNil) + + dockerCmd(c, "network", "connect", "mybridge1", id) + dockerCmd(c, "network", "connect", "mybridge2", id) + + body := getInspectBody(c, "v1.20", id) + var inspect120 v1p20.ContainerJSON + err := json.Unmarshal(body, &inspect120) + c.Assert(err, checker.IsNil) + + versionedIP := inspect120.NetworkSettings.IPAddress + + body = getInspectBody(c, "v1.21", id) + var inspect121 types.ContainerJSON + err = json.Unmarshal(body, &inspect121) + c.Assert(err, checker.IsNil) + c.Assert(inspect121.NetworkSettings.Networks, checker.HasLen, 3) + + bridge := inspect121.NetworkSettings.Networks["bridge"] + c.Assert(bridge.IPAddress, checker.Equals, versionedIP) + c.Assert(bridge.IPAddress, checker.Equals, inspect121.NetworkSettings.IPAddress) +} + +func connectContainerToNetworks(c *check.C, d *Daemon, cName string, nws []string) { + // Run a container on the default network + out, err := d.Cmd("run", "-d", "--name", cName, "busybox", "top") + c.Assert(err, checker.IsNil, check.Commentf(out)) + + // Attach the container to other networks + for _, nw := range nws { + out, err = d.Cmd("network", "create", nw) + c.Assert(err, checker.IsNil, check.Commentf(out)) + out, err = d.Cmd("network", "connect", nw, cName) + c.Assert(err, checker.IsNil, check.Commentf(out)) + } +} + +func verifyContainerIsConnectedToNetworks(c *check.C, d *Daemon, cName string, nws []string) { + // Verify container is connected to all the networks + for _, nw := range nws { + out, err := d.Cmd("inspect", "-f", fmt.Sprintf("{{.NetworkSettings.Networks.%s}}", nw), cName) + c.Assert(err, checker.IsNil, check.Commentf(out)) + c.Assert(out, checker.Not(checker.Equals), "\n") + } +} + +func (s *DockerNetworkSuite) TestDockerNetworkMultipleNetworksGracefulDaemonRestart(c *check.C) { + cName := "bb" + nwList := []string{"nw1", "nw2", "nw3"} + + s.d.StartWithBusybox() + + connectContainerToNetworks(c, s.d, cName, nwList) + verifyContainerIsConnectedToNetworks(c, s.d, cName, nwList) + + // Reload daemon + s.d.Restart() + + _, err := s.d.Cmd("start", cName) + c.Assert(err, checker.IsNil) + + verifyContainerIsConnectedToNetworks(c, s.d, cName, nwList) +} + +func (s *DockerNetworkSuite) TestDockerNetworkMultipleNetworksUngracefulDaemonRestart(c *check.C) { + cName := "cc" + nwList := []string{"nw1", "nw2", "nw3"} + + s.d.StartWithBusybox() + + connectContainerToNetworks(c, s.d, cName, nwList) + verifyContainerIsConnectedToNetworks(c, s.d, cName, nwList) + + // Kill daemon and restart + if err := s.d.cmd.Process.Kill(); err != nil { + c.Fatal(err) + } + s.d.Restart() + + // Restart container + _, err := s.d.Cmd("start", cName) + c.Assert(err, checker.IsNil) + + verifyContainerIsConnectedToNetworks(c, s.d, cName, nwList) +} + +func (s *DockerNetworkSuite) TestDockerNetworkRunNetByID(c *check.C) { + out, _ := dockerCmd(c, "network", "create", "one") + containerOut, _, err := dockerCmdWithError("run", "-d", "--net", strings.TrimSpace(out), "busybox", "top") + c.Assert(err, checker.IsNil, check.Commentf(containerOut)) +} + +func (s *DockerNetworkSuite) TestDockerNetworkHostModeUngracefulDaemonRestart(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace) + s.d.StartWithBusybox() + + // Run a few containers on host network + for i := 0; i < 10; i++ { + cName := fmt.Sprintf("hostc-%d", i) + out, err := s.d.Cmd("run", "-d", "--name", cName, "--net=host", "--restart=always", "busybox", "top") + c.Assert(err, checker.IsNil, check.Commentf(out)) + + // verfiy container has finished starting before killing daemon + err = s.d.waitRun(cName) + c.Assert(err, checker.IsNil) + } + + // Kill daemon ungracefully and restart + if err := s.d.cmd.Process.Kill(); err != nil { + c.Fatal(err) + } + if err := s.d.Restart(); err != nil { + c.Fatal(err) + } + + // make sure all the containers are up and running + for i := 0; i < 10; i++ { + err := s.d.waitRun(fmt.Sprintf("hostc-%d", i)) + c.Assert(err, checker.IsNil) + } +} + +func (s *DockerNetworkSuite) TestDockerNetworkConnectToHostFromOtherNetwork(c *check.C) { + dockerCmd(c, "run", "-d", "--name", "container1", "busybox", "top") + c.Assert(waitRun("container1"), check.IsNil) + dockerCmd(c, "network", "disconnect", "bridge", "container1") + out, _, err := dockerCmdWithError("network", "connect", "host", "container1") + c.Assert(err, checker.NotNil, check.Commentf(out)) + c.Assert(out, checker.Contains, runconfig.ErrConflictHostNetwork.Error()) +} + +func (s *DockerNetworkSuite) TestDockerNetworkDisconnectFromHost(c *check.C) { + dockerCmd(c, "run", "-d", "--name", "container1", "--net=host", "busybox", "top") + c.Assert(waitRun("container1"), check.IsNil) + out, _, err := dockerCmdWithError("network", "disconnect", "host", "container1") + c.Assert(err, checker.NotNil, check.Commentf("Should err out disconnect from host")) + c.Assert(out, checker.Contains, runconfig.ErrConflictHostNetwork.Error()) +} + +func (s *DockerNetworkSuite) TestDockerNetworkConnectWithPortMapping(c *check.C) { + testRequires(c, NotArm) + dockerCmd(c, "network", "create", "test1") + dockerCmd(c, "run", "-d", "--name", "c1", "-p", "5000:5000", "busybox", "top") + c.Assert(waitRun("c1"), check.IsNil) + dockerCmd(c, "network", "connect", "test1", "c1") +} + +func verifyPortMap(c *check.C, container, port, originalMapping string, mustBeEqual bool) { + chk := checker.Equals + if !mustBeEqual { + chk = checker.Not(checker.Equals) + } + currentMapping, _ := dockerCmd(c, "port", container, port) + c.Assert(currentMapping, chk, originalMapping) +} + +func (s *DockerNetworkSuite) TestDockerNetworkConnectDisconnectWithPortMapping(c *check.C) { + // Connect and disconnect a container with explicit and non-explicit + // host port mapping to/from networks which do cause and do not cause + // the container default gateway to change, and verify docker port cmd + // returns congruent information + testRequires(c, NotArm) + cnt := "c1" + dockerCmd(c, "network", "create", "aaa") + dockerCmd(c, "network", "create", "ccc") + + dockerCmd(c, "run", "-d", "--name", cnt, "-p", "9000:90", "-p", "70", "busybox", "top") + c.Assert(waitRun(cnt), check.IsNil) + curPortMap, _ := dockerCmd(c, "port", cnt, "70") + curExplPortMap, _ := dockerCmd(c, "port", cnt, "90") + + // Connect to a network which causes the container's default gw switch + dockerCmd(c, "network", "connect", "aaa", cnt) + verifyPortMap(c, cnt, "70", curPortMap, false) + verifyPortMap(c, cnt, "90", curExplPortMap, true) + + // Read current mapping + curPortMap, _ = dockerCmd(c, "port", cnt, "70") + + // Disconnect from a network which causes the container's default gw switch + dockerCmd(c, "network", "disconnect", "aaa", cnt) + verifyPortMap(c, cnt, "70", curPortMap, false) + verifyPortMap(c, cnt, "90", curExplPortMap, true) + + // Read current mapping + curPortMap, _ = dockerCmd(c, "port", cnt, "70") + + // Connect to a network which does not cause the container's default gw switch + dockerCmd(c, "network", "connect", "ccc", cnt) + verifyPortMap(c, cnt, "70", curPortMap, true) + verifyPortMap(c, cnt, "90", curExplPortMap, true) +} + +func (s *DockerNetworkSuite) TestDockerNetworkConnectWithMac(c *check.C) { + macAddress := "02:42:ac:11:00:02" + dockerCmd(c, "network", "create", "mynetwork") + dockerCmd(c, "run", "--name=test", "-d", "--mac-address", macAddress, "busybox", "top") + c.Assert(waitRun("test"), check.IsNil) + mac1 := inspectField(c, "test", "NetworkSettings.Networks.bridge.MacAddress") + c.Assert(strings.TrimSpace(mac1), checker.Equals, macAddress) + dockerCmd(c, "network", "connect", "mynetwork", "test") + mac2 := inspectField(c, "test", "NetworkSettings.Networks.mynetwork.MacAddress") + c.Assert(strings.TrimSpace(mac2), checker.Not(checker.Equals), strings.TrimSpace(mac1)) +} + +func (s *DockerNetworkSuite) TestDockerNetworkInspectCreatedContainer(c *check.C) { + dockerCmd(c, "create", "--name", "test", "busybox") + networks := inspectField(c, "test", "NetworkSettings.Networks") + c.Assert(networks, checker.Contains, "bridge", check.Commentf("Should return 'bridge' network")) +} + +func (s *DockerNetworkSuite) TestDockerNetworkRestartWithMultipleNetworks(c *check.C) { + dockerCmd(c, "network", "create", "test") + dockerCmd(c, "run", "--name=foo", "-d", "busybox", "top") + c.Assert(waitRun("foo"), checker.IsNil) + dockerCmd(c, "network", "connect", "test", "foo") + dockerCmd(c, "restart", "foo") + networks := inspectField(c, "foo", "NetworkSettings.Networks") + c.Assert(networks, checker.Contains, "bridge", check.Commentf("Should contain 'bridge' network")) + c.Assert(networks, checker.Contains, "test", check.Commentf("Should contain 'test' network")) +} + +func (s *DockerNetworkSuite) TestDockerNetworkConnectDisconnectToStoppedContainer(c *check.C) { + dockerCmd(c, "network", "create", "test") + dockerCmd(c, "create", "--name=foo", "busybox", "top") + dockerCmd(c, "network", "connect", "test", "foo") + networks := inspectField(c, "foo", "NetworkSettings.Networks") + c.Assert(networks, checker.Contains, "test", check.Commentf("Should contain 'test' network")) + + // Restart docker daemon to test the config has persisted to disk + s.d.Restart() + networks = inspectField(c, "foo", "NetworkSettings.Networks") + c.Assert(networks, checker.Contains, "test", check.Commentf("Should contain 'test' network")) + + // start the container and test if we can ping it from another container in the same network + dockerCmd(c, "start", "foo") + c.Assert(waitRun("foo"), checker.IsNil) + ip := inspectField(c, "foo", "NetworkSettings.Networks.test.IPAddress") + ip = strings.TrimSpace(ip) + dockerCmd(c, "run", "--net=test", "busybox", "sh", "-c", fmt.Sprintf("ping -c 1 %s", ip)) + + dockerCmd(c, "stop", "foo") + + // Test disconnect + dockerCmd(c, "network", "disconnect", "test", "foo") + networks = inspectField(c, "foo", "NetworkSettings.Networks") + c.Assert(networks, checker.Not(checker.Contains), "test", check.Commentf("Should not contain 'test' network")) + + // Restart docker daemon to test the config has persisted to disk + s.d.Restart() + networks = inspectField(c, "foo", "NetworkSettings.Networks") + c.Assert(networks, checker.Not(checker.Contains), "test", check.Commentf("Should not contain 'test' network")) + +} + +func (s *DockerNetworkSuite) TestDockerNetworkConnectPreferredIP(c *check.C) { + // create two networks + dockerCmd(c, "network", "create", "--ipv6", "--subnet=172.28.0.0/16", "--subnet=2001:db8:1234::/64", "n0") + assertNwIsAvailable(c, "n0") + + dockerCmd(c, "network", "create", "--ipv6", "--subnet=172.30.0.0/16", "--ip-range=172.30.5.0/24", "--subnet=2001:db8:abcd::/64", "--ip-range=2001:db8:abcd::/80", "n1") + assertNwIsAvailable(c, "n1") + + // run a container on first network specifying the ip addresses + dockerCmd(c, "run", "-d", "--name", "c0", "--net=n0", "--ip", "172.28.99.88", "--ip6", "2001:db8:1234::9988", "busybox", "top") + c.Assert(waitRun("c0"), check.IsNil) + verifyIPAddressConfig(c, "c0", "n0", "172.28.99.88", "2001:db8:1234::9988") + verifyIPAddresses(c, "c0", "n0", "172.28.99.88", "2001:db8:1234::9988") + + // connect the container to the second network specifying an ip addresses + dockerCmd(c, "network", "connect", "--ip", "172.30.55.44", "--ip6", "2001:db8:abcd::5544", "n1", "c0") + verifyIPAddressConfig(c, "c0", "n1", "172.30.55.44", "2001:db8:abcd::5544") + verifyIPAddresses(c, "c0", "n1", "172.30.55.44", "2001:db8:abcd::5544") + + // Stop and restart the container + dockerCmd(c, "stop", "c0") + dockerCmd(c, "start", "c0") + + // verify requested addresses are applied and configs are still there + verifyIPAddressConfig(c, "c0", "n0", "172.28.99.88", "2001:db8:1234::9988") + verifyIPAddresses(c, "c0", "n0", "172.28.99.88", "2001:db8:1234::9988") + verifyIPAddressConfig(c, "c0", "n1", "172.30.55.44", "2001:db8:abcd::5544") + verifyIPAddresses(c, "c0", "n1", "172.30.55.44", "2001:db8:abcd::5544") + + // Still it should fail to connect to the default network with a specified IP (whatever ip) + out, _, err := dockerCmdWithError("network", "connect", "--ip", "172.21.55.44", "bridge", "c0") + c.Assert(err, checker.NotNil, check.Commentf("out: %s", out)) + c.Assert(out, checker.Contains, runconfig.ErrUnsupportedNetworkAndIP.Error()) + +} + +func (s *DockerNetworkSuite) TestDockerNetworkConnectPreferredIPStoppedContainer(c *check.C) { + // create a container + dockerCmd(c, "create", "--name", "c0", "busybox", "top") + + // create a network + dockerCmd(c, "network", "create", "--ipv6", "--subnet=172.30.0.0/16", "--subnet=2001:db8:abcd::/64", "n0") + assertNwIsAvailable(c, "n0") + + // connect the container to the network specifying an ip addresses + dockerCmd(c, "network", "connect", "--ip", "172.30.55.44", "--ip6", "2001:db8:abcd::5544", "n0", "c0") + verifyIPAddressConfig(c, "c0", "n0", "172.30.55.44", "2001:db8:abcd::5544") + + // start the container, verify config has not changed and ip addresses are assigned + dockerCmd(c, "start", "c0") + c.Assert(waitRun("c0"), check.IsNil) + verifyIPAddressConfig(c, "c0", "n0", "172.30.55.44", "2001:db8:abcd::5544") + verifyIPAddresses(c, "c0", "n0", "172.30.55.44", "2001:db8:abcd::5544") + + // stop the container and check ip config has not changed + dockerCmd(c, "stop", "c0") + verifyIPAddressConfig(c, "c0", "n0", "172.30.55.44", "2001:db8:abcd::5544") +} + +func (s *DockerNetworkSuite) TestDockerNetworkUnsupportedRequiredIP(c *check.C) { + // requested IP is not supported on predefined networks + for _, mode := range []string{"none", "host", "bridge", "default"} { + checkUnsupportedNetworkAndIP(c, mode) + } + + // requested IP is not supported on networks with no user defined subnets + dockerCmd(c, "network", "create", "n0") + assertNwIsAvailable(c, "n0") + + out, _, err := dockerCmdWithError("run", "-d", "--ip", "172.28.99.88", "--net", "n0", "busybox", "top") + c.Assert(err, checker.NotNil, check.Commentf("out: %s", out)) + c.Assert(out, checker.Contains, runconfig.ErrUnsupportedNetworkNoSubnetAndIP.Error()) + + out, _, err = dockerCmdWithError("run", "-d", "--ip6", "2001:db8:1234::9988", "--net", "n0", "busybox", "top") + c.Assert(err, checker.NotNil, check.Commentf("out: %s", out)) + c.Assert(out, checker.Contains, runconfig.ErrUnsupportedNetworkNoSubnetAndIP.Error()) + + dockerCmd(c, "network", "rm", "n0") + assertNwNotAvailable(c, "n0") +} + +func checkUnsupportedNetworkAndIP(c *check.C, nwMode string) { + out, _, err := dockerCmdWithError("run", "-d", "--net", nwMode, "--ip", "172.28.99.88", "--ip6", "2001:db8:1234::9988", "busybox", "top") + c.Assert(err, checker.NotNil, check.Commentf("out: %s", out)) + c.Assert(out, checker.Contains, runconfig.ErrUnsupportedNetworkAndIP.Error()) +} + +func verifyIPAddressConfig(c *check.C, cName, nwname, ipv4, ipv6 string) { + if ipv4 != "" { + out := inspectField(c, cName, fmt.Sprintf("NetworkSettings.Networks.%s.IPAMConfig.IPv4Address", nwname)) + c.Assert(strings.TrimSpace(out), check.Equals, ipv4) + } + + if ipv6 != "" { + out := inspectField(c, cName, fmt.Sprintf("NetworkSettings.Networks.%s.IPAMConfig.IPv6Address", nwname)) + c.Assert(strings.TrimSpace(out), check.Equals, ipv6) + } +} + +func verifyIPAddresses(c *check.C, cName, nwname, ipv4, ipv6 string) { + out := inspectField(c, cName, fmt.Sprintf("NetworkSettings.Networks.%s.IPAddress", nwname)) + c.Assert(strings.TrimSpace(out), check.Equals, ipv4) + + out = inspectField(c, cName, fmt.Sprintf("NetworkSettings.Networks.%s.GlobalIPv6Address", nwname)) + c.Assert(strings.TrimSpace(out), check.Equals, ipv6) +} + +func (s *DockerSuite) TestUserDefinedNetworkConnectDisconnectLink(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace, NotArm) + dockerCmd(c, "network", "create", "-d", "bridge", "foo1") + dockerCmd(c, "network", "create", "-d", "bridge", "foo2") + + dockerCmd(c, "run", "-d", "--net=foo1", "--name=first", "busybox", "top") + c.Assert(waitRun("first"), check.IsNil) + + // run a container in a user-defined network with a link for an existing container + // and a link for a container that doesn't exist + dockerCmd(c, "run", "-d", "--net=foo1", "--name=second", "--link=first:FirstInFoo1", + "--link=third:bar", "busybox", "top") + c.Assert(waitRun("second"), check.IsNil) + + // ping to first and its alias FirstInFoo1 must succeed + _, _, err := dockerCmdWithError("exec", "second", "ping", "-c", "1", "first") + c.Assert(err, check.IsNil) + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "FirstInFoo1") + c.Assert(err, check.IsNil) + + // connect first container to foo2 network + dockerCmd(c, "network", "connect", "foo2", "first") + // connect second container to foo2 network with a different alias for first container + dockerCmd(c, "network", "connect", "--link=first:FirstInFoo2", "foo2", "second") + + // ping the new alias in network foo2 + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "FirstInFoo2") + c.Assert(err, check.IsNil) + + // disconnect first container from foo1 network + dockerCmd(c, "network", "disconnect", "foo1", "first") + + // link in foo1 network must fail + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "FirstInFoo1") + c.Assert(err, check.NotNil) + + // link in foo2 network must succeed + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "FirstInFoo2") + c.Assert(err, check.IsNil) +} + +// #19100 This is a deprecated feature test, it should be removed in Docker 1.12 +func (s *DockerNetworkSuite) TestDockerNetworkStartAPIWithHostconfig(c *check.C) { + netName := "test" + conName := "foo" + dockerCmd(c, "network", "create", netName) + dockerCmd(c, "create", "--name", conName, "busybox", "top") + + config := map[string]interface{}{ + "HostConfig": map[string]interface{}{ + "NetworkMode": netName, + }, + } + _, _, err := sockRequest("POST", "/containers/"+conName+"/start", config) + c.Assert(err, checker.IsNil) + c.Assert(waitRun(conName), checker.IsNil) + networks := inspectField(c, conName, "NetworkSettings.Networks") + c.Assert(networks, checker.Contains, netName, check.Commentf(fmt.Sprintf("Should contain '%s' network", netName))) + c.Assert(networks, checker.Not(checker.Contains), "bridge", check.Commentf("Should not contain 'bridge' network")) +} + +func (s *DockerNetworkSuite) TestDockerNetworkDisconnectDefault(c *check.C) { + netWorkName1 := "test1" + netWorkName2 := "test2" + containerName := "foo" + + dockerCmd(c, "network", "create", netWorkName1) + dockerCmd(c, "network", "create", netWorkName2) + dockerCmd(c, "create", "--name", containerName, "busybox", "top") + dockerCmd(c, "network", "connect", netWorkName1, containerName) + dockerCmd(c, "network", "connect", netWorkName2, containerName) + dockerCmd(c, "network", "disconnect", "bridge", containerName) + + dockerCmd(c, "start", containerName) + c.Assert(waitRun(containerName), checker.IsNil) + networks := inspectField(c, containerName, "NetworkSettings.Networks") + c.Assert(networks, checker.Contains, netWorkName1, check.Commentf(fmt.Sprintf("Should contain '%s' network", netWorkName1))) + c.Assert(networks, checker.Contains, netWorkName2, check.Commentf(fmt.Sprintf("Should contain '%s' network", netWorkName2))) + c.Assert(networks, checker.Not(checker.Contains), "bridge", check.Commentf("Should not contain 'bridge' network")) +} + +func (s *DockerNetworkSuite) TestDockerNetworkConnectWithAliasOnDefaultNetworks(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace, NotArm) + + defaults := []string{"bridge", "host", "none"} + out, _ := dockerCmd(c, "run", "-d", "--net=none", "busybox", "top") + containerID := strings.TrimSpace(out) + for _, net := range defaults { + res, _, err := dockerCmdWithError("network", "connect", "--alias", "alias"+net, net, containerID) + c.Assert(err, checker.NotNil) + c.Assert(res, checker.Contains, runconfig.ErrUnsupportedNetworkAndAlias.Error()) + } +} + +func (s *DockerSuite) TestUserDefinedNetworkConnectDisconnectAlias(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace, NotArm) + dockerCmd(c, "network", "create", "-d", "bridge", "net1") + dockerCmd(c, "network", "create", "-d", "bridge", "net2") + + dockerCmd(c, "run", "-d", "--net=net1", "--name=first", "--net-alias=foo", "busybox", "top") + c.Assert(waitRun("first"), check.IsNil) + + dockerCmd(c, "run", "-d", "--net=net1", "--name=second", "busybox", "top") + c.Assert(waitRun("second"), check.IsNil) + + // ping first container and its alias + _, _, err := dockerCmdWithError("exec", "second", "ping", "-c", "1", "first") + c.Assert(err, check.IsNil) + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "foo") + c.Assert(err, check.IsNil) + + // connect first container to net2 network + dockerCmd(c, "network", "connect", "--alias=bar", "net2", "first") + // connect second container to foo2 network with a different alias for first container + dockerCmd(c, "network", "connect", "net2", "second") + + // ping the new alias in network foo2 + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "bar") + c.Assert(err, check.IsNil) + + // disconnect first container from net1 network + dockerCmd(c, "network", "disconnect", "net1", "first") + + // ping to net1 scoped alias "foo" must fail + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "foo") + c.Assert(err, check.NotNil) + + // ping to net2 scoped alias "bar" must still succeed + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "bar") + c.Assert(err, check.IsNil) + + // verify the alias option is rejected when running on predefined network + out, _, err := dockerCmdWithError("run", "--rm", "--name=any", "--net-alias=any", "busybox", "top") + c.Assert(err, checker.NotNil, check.Commentf("out: %s", out)) + c.Assert(out, checker.Contains, runconfig.ErrUnsupportedNetworkAndAlias.Error()) + + // verify the alias option is rejected when connecting to predefined network + out, _, err = dockerCmdWithError("network", "connect", "--alias=any", "bridge", "first") + c.Assert(err, checker.NotNil, check.Commentf("out: %s", out)) + c.Assert(out, checker.Contains, runconfig.ErrUnsupportedNetworkAndAlias.Error()) +} + +func (s *DockerSuite) TestUserDefinedNetworkConnectivity(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace) + dockerCmd(c, "network", "create", "-d", "bridge", "br.net1") + + dockerCmd(c, "run", "-d", "--net=br.net1", "--name=c1.net1", "busybox", "top") + c.Assert(waitRun("c1.net1"), check.IsNil) + + dockerCmd(c, "run", "-d", "--net=br.net1", "--name=c2.net1", "busybox", "top") + c.Assert(waitRun("c2.net1"), check.IsNil) + + // ping first container by its unqualified name + _, _, err := dockerCmdWithError("exec", "c2.net1", "ping", "-c", "1", "c1.net1") + c.Assert(err, check.IsNil) + + // ping first container by its qualified name + _, _, err = dockerCmdWithError("exec", "c2.net1", "ping", "-c", "1", "c1.net1.br.net1") + c.Assert(err, check.IsNil) + + // ping with first qualified name masked by an additional domain. should fail + _, _, err = dockerCmdWithError("exec", "c2.net1", "ping", "-c", "1", "c1.net1.br.net1.google.com") + c.Assert(err, check.NotNil) +} + +func (s *DockerSuite) TestEmbeddedDNSInvalidInput(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace) + dockerCmd(c, "network", "create", "-d", "bridge", "nw1") + + // Sending garbage to embedded DNS shouldn't crash the daemon + dockerCmd(c, "run", "-i", "--net=nw1", "--name=c1", "debian:jessie", "bash", "-c", "echo InvalidQuery > /dev/udp/127.0.0.11/53") +} + +func (s *DockerSuite) TestDockerNetworkConnectFailsNoInspectChange(c *check.C) { + dockerCmd(c, "run", "-d", "--name=bb", "busybox", "top") + c.Assert(waitRun("bb"), check.IsNil) + + ns0 := inspectField(c, "bb", "NetworkSettings.Networks.bridge") + + // A failing redundant network connect should not alter current container's endpoint settings + _, _, err := dockerCmdWithError("network", "connect", "bridge", "bb") + c.Assert(err, check.NotNil) + + ns1 := inspectField(c, "bb", "NetworkSettings.Networks.bridge") + c.Assert(ns1, check.Equals, ns0) +} + +func (s *DockerSuite) TestDockerNetworkInternalMode(c *check.C) { + dockerCmd(c, "network", "create", "--driver=bridge", "--internal", "internal") + assertNwIsAvailable(c, "internal") + nr := getNetworkResource(c, "internal") + c.Assert(nr.Internal, checker.True) + + dockerCmd(c, "run", "-d", "--net=internal", "--name=first", "busybox", "top") + c.Assert(waitRun("first"), check.IsNil) + dockerCmd(c, "run", "-d", "--net=internal", "--name=second", "busybox", "top") + c.Assert(waitRun("second"), check.IsNil) + out, _, err := dockerCmdWithError("exec", "first", "ping", "-W", "4", "-c", "1", "www.google.com") + c.Assert(err, check.NotNil) + c.Assert(out, checker.Contains, "ping: bad address") + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "first") + c.Assert(err, check.IsNil) +} + +// Test for #21401 +func (s *DockerNetworkSuite) TestDockerNetworkCreateDeleteSpecialCharacters(c *check.C) { + dockerCmd(c, "network", "create", "test@#$") + assertNwIsAvailable(c, "test@#$") + dockerCmd(c, "network", "rm", "test@#$") + assertNwNotAvailable(c, "test@#$") + + dockerCmd(c, "network", "create", "kiwl$%^") + assertNwIsAvailable(c, "kiwl$%^") + dockerCmd(c, "network", "rm", "kiwl$%^") + assertNwNotAvailable(c, "kiwl$%^") +} diff --git a/integration-cli/docker_cli_oom_killed_test.go b/integration-cli/docker_cli_oom_killed_test.go new file mode 100644 index 00000000..ff77f572 --- /dev/null +++ b/integration-cli/docker_cli_oom_killed_test.go @@ -0,0 +1,30 @@ +// +build !windows + +package main + +import ( + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestInspectOomKilledTrue(c *check.C) { + testRequires(c, DaemonIsLinux, memoryLimitSupport) + + name := "testoomkilled" + _, exitCode, _ := dockerCmdWithError("run", "--name", name, "--memory", "32MB", "busybox", "sh", "-c", "x=a; while true; do x=$x$x$x$x; done") + + c.Assert(exitCode, checker.Equals, 137, check.Commentf("OOM exit should be 137")) + + oomKilled := inspectField(c, name, "State.OOMKilled") + c.Assert(oomKilled, checker.Equals, "true") +} + +func (s *DockerSuite) TestInspectOomKilledFalse(c *check.C) { + testRequires(c, DaemonIsLinux, memoryLimitSupport) + + name := "testoomkilled" + dockerCmd(c, "run", "--name", name, "--memory", "32MB", "busybox", "sh", "-c", "echo hello world") + + oomKilled := inspectField(c, name, "State.OOMKilled") + c.Assert(oomKilled, checker.Equals, "false") +} diff --git a/integration-cli/docker_cli_pause_test.go b/integration-cli/docker_cli_pause_test.go new file mode 100644 index 00000000..a42c2f54 --- /dev/null +++ b/integration-cli/docker_cli_pause_test.go @@ -0,0 +1,67 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestPause(c *check.C) { + testRequires(c, DaemonIsLinux) + defer unpauseAllContainers() + + name := "testeventpause" + dockerCmd(c, "run", "-d", "--name", name, "busybox", "top") + + dockerCmd(c, "pause", name) + pausedContainers, err := getSliceOfPausedContainers() + c.Assert(err, checker.IsNil) + c.Assert(len(pausedContainers), checker.Equals, 1) + + dockerCmd(c, "unpause", name) + + out, _ := dockerCmd(c, "events", "--since=0", fmt.Sprintf("--until=%d", daemonTime(c).Unix())) + events := strings.Split(strings.TrimSpace(out), "\n") + actions := eventActionsByIDAndType(c, events, name, "container") + + c.Assert(actions[len(actions)-2], checker.Equals, "pause") + c.Assert(actions[len(actions)-1], checker.Equals, "unpause") +} + +func (s *DockerSuite) TestPauseMultipleContainers(c *check.C) { + testRequires(c, DaemonIsLinux) + defer unpauseAllContainers() + + containers := []string{ + "testpausewithmorecontainers1", + "testpausewithmorecontainers2", + } + for _, name := range containers { + dockerCmd(c, "run", "-d", "--name", name, "busybox", "top") + } + dockerCmd(c, append([]string{"pause"}, containers...)...) + pausedContainers, err := getSliceOfPausedContainers() + c.Assert(err, checker.IsNil) + c.Assert(len(pausedContainers), checker.Equals, len(containers)) + + dockerCmd(c, append([]string{"unpause"}, containers...)...) + + out, _ := dockerCmd(c, "events", "--since=0", fmt.Sprintf("--until=%d", daemonTime(c).Unix())) + events := strings.Split(strings.TrimSpace(out), "\n") + + for _, name := range containers { + actions := eventActionsByIDAndType(c, events, name, "container") + + c.Assert(actions[len(actions)-2], checker.Equals, "pause") + c.Assert(actions[len(actions)-1], checker.Equals, "unpause") + } +} + +func (s *DockerSuite) TestPauseFailsOnWindows(c *check.C) { + testRequires(c, DaemonIsWindows) + dockerCmd(c, "run", "-d", "--name=test", "busybox", "sleep 3") + out, _, _ := dockerCmdWithError("pause", "test") + c.Assert(out, checker.Contains, "Windows: Containers cannot be paused") +} diff --git a/integration-cli/docker_cli_port_test.go b/integration-cli/docker_cli_port_test.go new file mode 100644 index 00000000..80b00fe9 --- /dev/null +++ b/integration-cli/docker_cli_port_test.go @@ -0,0 +1,319 @@ +package main + +import ( + "fmt" + "net" + "regexp" + "sort" + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestPortList(c *check.C) { + testRequires(c, DaemonIsLinux) + // one port + out, _ := dockerCmd(c, "run", "-d", "-p", "9876:80", "busybox", "top") + firstID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "port", firstID, "80") + + err := assertPortList(c, out, []string{"0.0.0.0:9876"}) + // Port list is not correct + c.Assert(err, checker.IsNil) + + out, _ = dockerCmd(c, "port", firstID) + + err = assertPortList(c, out, []string{"80/tcp -> 0.0.0.0:9876"}) + // Port list is not correct + c.Assert(err, checker.IsNil) + + dockerCmd(c, "rm", "-f", firstID) + + // three port + out, _ = dockerCmd(c, "run", "-d", + "-p", "9876:80", + "-p", "9877:81", + "-p", "9878:82", + "busybox", "top") + ID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "port", ID, "80") + + err = assertPortList(c, out, []string{"0.0.0.0:9876"}) + // Port list is not correct + c.Assert(err, checker.IsNil) + + out, _ = dockerCmd(c, "port", ID) + + err = assertPortList(c, out, []string{ + "80/tcp -> 0.0.0.0:9876", + "81/tcp -> 0.0.0.0:9877", + "82/tcp -> 0.0.0.0:9878"}) + // Port list is not correct + c.Assert(err, checker.IsNil) + + dockerCmd(c, "rm", "-f", ID) + + // more and one port mapped to the same container port + out, _ = dockerCmd(c, "run", "-d", + "-p", "9876:80", + "-p", "9999:80", + "-p", "9877:81", + "-p", "9878:82", + "busybox", "top") + ID = strings.TrimSpace(out) + + out, _ = dockerCmd(c, "port", ID, "80") + + err = assertPortList(c, out, []string{"0.0.0.0:9876", "0.0.0.0:9999"}) + // Port list is not correct + c.Assert(err, checker.IsNil) + + out, _ = dockerCmd(c, "port", ID) + + err = assertPortList(c, out, []string{ + "80/tcp -> 0.0.0.0:9876", + "80/tcp -> 0.0.0.0:9999", + "81/tcp -> 0.0.0.0:9877", + "82/tcp -> 0.0.0.0:9878"}) + // Port list is not correct + c.Assert(err, checker.IsNil) + dockerCmd(c, "rm", "-f", ID) + + testRange := func() { + // host port ranges used + IDs := make([]string, 3) + for i := 0; i < 3; i++ { + out, _ = dockerCmd(c, "run", "-d", + "-p", "9090-9092:80", + "busybox", "top") + IDs[i] = strings.TrimSpace(out) + + out, _ = dockerCmd(c, "port", IDs[i]) + + err = assertPortList(c, out, []string{fmt.Sprintf("80/tcp -> 0.0.0.0:%d", 9090+i)}) + // Port list is not correct + c.Assert(err, checker.IsNil) + } + + // test port range exhaustion + out, _, err = dockerCmdWithError("run", "-d", + "-p", "9090-9092:80", + "busybox", "top") + // Exhausted port range did not return an error + c.Assert(err, checker.NotNil, check.Commentf("out: %s", out)) + + for i := 0; i < 3; i++ { + dockerCmd(c, "rm", "-f", IDs[i]) + } + } + testRange() + // Verify we ran re-use port ranges after they are no longer in use. + testRange() + + // test invalid port ranges + for _, invalidRange := range []string{"9090-9089:80", "9090-:80", "-9090:80"} { + out, _, err = dockerCmdWithError("run", "-d", + "-p", invalidRange, + "busybox", "top") + // Port range should have returned an error + c.Assert(err, checker.NotNil, check.Commentf("out: %s", out)) + } + + // test host range:container range spec. + out, _ = dockerCmd(c, "run", "-d", + "-p", "9800-9803:80-83", + "busybox", "top") + ID = strings.TrimSpace(out) + + out, _ = dockerCmd(c, "port", ID) + + err = assertPortList(c, out, []string{ + "80/tcp -> 0.0.0.0:9800", + "81/tcp -> 0.0.0.0:9801", + "82/tcp -> 0.0.0.0:9802", + "83/tcp -> 0.0.0.0:9803"}) + // Port list is not correct + c.Assert(err, checker.IsNil) + dockerCmd(c, "rm", "-f", ID) + + // test mixing protocols in same port range + out, _ = dockerCmd(c, "run", "-d", + "-p", "8000-8080:80", + "-p", "8000-8080:80/udp", + "busybox", "top") + ID = strings.TrimSpace(out) + + out, _ = dockerCmd(c, "port", ID) + + err = assertPortList(c, out, []string{ + "80/tcp -> 0.0.0.0:8000", + "80/udp -> 0.0.0.0:8000"}) + // Port list is not correct + c.Assert(err, checker.IsNil) + dockerCmd(c, "rm", "-f", ID) +} + +func assertPortList(c *check.C, out string, expected []string) error { + lines := strings.Split(strings.Trim(out, "\n "), "\n") + if len(lines) != len(expected) { + return fmt.Errorf("different size lists %s, %d, %d", out, len(lines), len(expected)) + } + sort.Strings(lines) + sort.Strings(expected) + + for i := 0; i < len(expected); i++ { + if lines[i] != expected[i] { + return fmt.Errorf("|" + lines[i] + "!=" + expected[i] + "|") + } + } + + return nil +} + +func stopRemoveContainer(id string, c *check.C) { + dockerCmd(c, "rm", "-f", id) +} + +func (s *DockerSuite) TestUnpublishedPortsInPsOutput(c *check.C) { + testRequires(c, DaemonIsLinux) + // Run busybox with command line expose (equivalent to EXPOSE in image's Dockerfile) for the following ports + port1 := 80 + port2 := 443 + expose1 := fmt.Sprintf("--expose=%d", port1) + expose2 := fmt.Sprintf("--expose=%d", port2) + dockerCmd(c, "run", "-d", expose1, expose2, "busybox", "sleep", "5") + + // Check docker ps o/p for last created container reports the unpublished ports + unpPort1 := fmt.Sprintf("%d/tcp", port1) + unpPort2 := fmt.Sprintf("%d/tcp", port2) + out, _ := dockerCmd(c, "ps", "-n=1") + // Missing unpublished ports in docker ps output + c.Assert(out, checker.Contains, unpPort1) + // Missing unpublished ports in docker ps output + c.Assert(out, checker.Contains, unpPort2) + + // Run the container forcing to publish the exposed ports + dockerCmd(c, "run", "-d", "-P", expose1, expose2, "busybox", "sleep", "5") + + // Check docker ps o/p for last created container reports the exposed ports in the port bindings + expBndRegx1 := regexp.MustCompile(`0.0.0.0:\d\d\d\d\d->` + unpPort1) + expBndRegx2 := regexp.MustCompile(`0.0.0.0:\d\d\d\d\d->` + unpPort2) + out, _ = dockerCmd(c, "ps", "-n=1") + // Cannot find expected port binding port (0.0.0.0:xxxxx->unpPort1) in docker ps output + c.Assert(expBndRegx1.MatchString(out), checker.Equals, true, check.Commentf("out: %s; unpPort1: %s", out, unpPort1)) + // Cannot find expected port binding port (0.0.0.0:xxxxx->unpPort2) in docker ps output + c.Assert(expBndRegx2.MatchString(out), checker.Equals, true, check.Commentf("out: %s; unpPort2: %s", out, unpPort2)) + + // Run the container specifying explicit port bindings for the exposed ports + offset := 10000 + pFlag1 := fmt.Sprintf("%d:%d", offset+port1, port1) + pFlag2 := fmt.Sprintf("%d:%d", offset+port2, port2) + out, _ = dockerCmd(c, "run", "-d", "-p", pFlag1, "-p", pFlag2, expose1, expose2, "busybox", "sleep", "5") + id := strings.TrimSpace(out) + + // Check docker ps o/p for last created container reports the specified port mappings + expBnd1 := fmt.Sprintf("0.0.0.0:%d->%s", offset+port1, unpPort1) + expBnd2 := fmt.Sprintf("0.0.0.0:%d->%s", offset+port2, unpPort2) + out, _ = dockerCmd(c, "ps", "-n=1") + // Cannot find expected port binding (expBnd1) in docker ps output + c.Assert(out, checker.Contains, expBnd1) + // Cannot find expected port binding (expBnd2) in docker ps output + c.Assert(out, checker.Contains, expBnd2) + + // Remove container now otherwise it will interfere with next test + stopRemoveContainer(id, c) + + // Run the container with explicit port bindings and no exposed ports + out, _ = dockerCmd(c, "run", "-d", "-p", pFlag1, "-p", pFlag2, "busybox", "sleep", "5") + id = strings.TrimSpace(out) + + // Check docker ps o/p for last created container reports the specified port mappings + out, _ = dockerCmd(c, "ps", "-n=1") + // Cannot find expected port binding (expBnd1) in docker ps output + c.Assert(out, checker.Contains, expBnd1) + // Cannot find expected port binding (expBnd2) in docker ps output + c.Assert(out, checker.Contains, expBnd2) + // Remove container now otherwise it will interfere with next test + stopRemoveContainer(id, c) + + // Run the container with one unpublished exposed port and one explicit port binding + dockerCmd(c, "run", "-d", expose1, "-p", pFlag2, "busybox", "sleep", "5") + + // Check docker ps o/p for last created container reports the specified unpublished port and port mapping + out, _ = dockerCmd(c, "ps", "-n=1") + // Missing unpublished exposed ports (unpPort1) in docker ps output + c.Assert(out, checker.Contains, unpPort1) + // Missing port binding (expBnd2) in docker ps output + c.Assert(out, checker.Contains, expBnd2) +} + +func (s *DockerSuite) TestPortHostBinding(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace) + out, _ := dockerCmd(c, "run", "-d", "-p", "9876:80", "busybox", + "nc", "-l", "-p", "80") + firstID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "port", firstID, "80") + + err := assertPortList(c, out, []string{"0.0.0.0:9876"}) + // Port list is not correct + c.Assert(err, checker.IsNil) + + dockerCmd(c, "run", "--net=host", "busybox", + "nc", "localhost", "9876") + + dockerCmd(c, "rm", "-f", firstID) + + out, _, err = dockerCmdWithError("run", "--net=host", "busybox", "nc", "localhost", "9876") + // Port is still bound after the Container is removed + c.Assert(err, checker.NotNil, check.Commentf("out: %s", out)) +} + +func (s *DockerSuite) TestPortExposeHostBinding(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace) + out, _ := dockerCmd(c, "run", "-d", "-P", "--expose", "80", "busybox", + "nc", "-l", "-p", "80") + firstID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "port", firstID, "80") + + _, exposedPort, err := net.SplitHostPort(out) + c.Assert(err, checker.IsNil, check.Commentf("out: %s", out)) + + dockerCmd(c, "run", "--net=host", "busybox", + "nc", "localhost", strings.TrimSpace(exposedPort)) + + dockerCmd(c, "rm", "-f", firstID) + + out, _, err = dockerCmdWithError("run", "--net=host", "busybox", + "nc", "localhost", strings.TrimSpace(exposedPort)) + // Port is still bound after the Container is removed + c.Assert(err, checker.NotNil, check.Commentf("out: %s", out)) +} + +func (s *DockerSuite) TestPortBindingOnSandbox(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace) + dockerCmd(c, "network", "create", "--internal", "-d", "bridge", "internal-net") + nr := getNetworkResource(c, "internal-net") + c.Assert(nr.Internal, checker.Equals, true) + + dockerCmd(c, "run", "--net", "internal-net", "-d", "--name", "c1", + "-p", "8080:8080", "busybox", "nc", "-l", "-p", "8080") + c.Assert(waitRun("c1"), check.IsNil) + + _, _, err := dockerCmdWithError("run", "--net=host", "busybox", "nc", "localhost", "8080") + c.Assert(err, check.NotNil, + check.Commentf("Port mapping on internal network is expected to fail")) + + // Connect container to another normal bridge network + dockerCmd(c, "network", "create", "-d", "bridge", "foo-net") + dockerCmd(c, "network", "connect", "foo-net", "c1") + + _, _, err = dockerCmdWithError("run", "--net=host", "busybox", "nc", "localhost", "8080") + c.Assert(err, check.IsNil, + check.Commentf("Port mapping on the new network is expected to succeed")) + +} diff --git a/integration-cli/docker_cli_proxy_test.go b/integration-cli/docker_cli_proxy_test.go new file mode 100644 index 00000000..e5699ca5 --- /dev/null +++ b/integration-cli/docker_cli_proxy_test.go @@ -0,0 +1,53 @@ +package main + +import ( + "net" + "os/exec" + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestCliProxyDisableProxyUnixSock(c *check.C) { + testRequires(c, DaemonIsLinux) + testRequires(c, SameHostDaemon) // test is valid when DOCKER_HOST=unix://.. + + cmd := exec.Command(dockerBinary, "info") + cmd.Env = appendBaseEnv(false, "HTTP_PROXY=http://127.0.0.1:9999") + + out, _, err := runCommandWithOutput(cmd) + c.Assert(err, checker.IsNil, check.Commentf("%v", out)) + +} + +// Can't use localhost here since go has a special case to not use proxy if connecting to localhost +// See https://golang.org/pkg/net/http/#ProxyFromEnvironment +func (s *DockerDaemonSuite) TestCliProxyProxyTCPSock(c *check.C) { + testRequires(c, SameHostDaemon) + // get the IP to use to connect since we can't use localhost + addrs, err := net.InterfaceAddrs() + c.Assert(err, checker.IsNil) + var ip string + for _, addr := range addrs { + sAddr := addr.String() + if !strings.Contains(sAddr, "127.0.0.1") { + addrArr := strings.Split(sAddr, "/") + ip = addrArr[0] + break + } + } + + c.Assert(ip, checker.Not(checker.Equals), "") + + err = s.d.Start("-H", "tcp://"+ip+":2375") + c.Assert(err, checker.IsNil) + cmd := exec.Command(dockerBinary, "info") + cmd.Env = []string{"DOCKER_HOST=tcp://" + ip + ":2375", "HTTP_PROXY=127.0.0.1:9999"} + out, _, err := runCommandWithOutput(cmd) + c.Assert(err, checker.NotNil, check.Commentf("%v", out)) + // Test with no_proxy + cmd.Env = append(cmd.Env, "NO_PROXY="+ip) + out, _, err = runCommandWithOutput(exec.Command(dockerBinary, "info")) + c.Assert(err, checker.IsNil, check.Commentf("%v", out)) +} diff --git a/integration-cli/docker_cli_ps_test.go b/integration-cli/docker_cli_ps_test.go new file mode 100644 index 00000000..72650ff2 --- /dev/null +++ b/integration-cli/docker_cli_ps_test.go @@ -0,0 +1,852 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "sort" + "strconv" + "strings" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/stringid" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestPsListContainersBase(c *check.C) { + out, _ := runSleepingContainer(c, "-d") + firstID := strings.TrimSpace(out) + + out, _ = runSleepingContainer(c, "-d") + secondID := strings.TrimSpace(out) + + // not long running + out, _ = dockerCmd(c, "run", "-d", "busybox", "true") + thirdID := strings.TrimSpace(out) + + out, _ = runSleepingContainer(c, "-d") + fourthID := strings.TrimSpace(out) + + // make sure the second is running + c.Assert(waitRun(secondID), checker.IsNil) + + // make sure third one is not running + dockerCmd(c, "wait", thirdID) + + // make sure the forth is running + c.Assert(waitRun(fourthID), checker.IsNil) + + // all + out, _ = dockerCmd(c, "ps", "-a") + c.Assert(assertContainerList(out, []string{fourthID, thirdID, secondID, firstID}), checker.Equals, true, check.Commentf("ALL: Container list is not in the correct order: \n%s", out)) + + // running + out, _ = dockerCmd(c, "ps") + c.Assert(assertContainerList(out, []string{fourthID, secondID, firstID}), checker.Equals, true, check.Commentf("RUNNING: Container list is not in the correct order: \n%s", out)) + + // limit + out, _ = dockerCmd(c, "ps", "-n=2", "-a") + expected := []string{fourthID, thirdID} + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("LIMIT & ALL: Container list is not in the correct order: \n%s", out)) + + out, _ = dockerCmd(c, "ps", "-n=2") + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("LIMIT: Container list is not in the correct order: \n%s", out)) + + // filter since + out, _ = dockerCmd(c, "ps", "-f", "since="+firstID, "-a") + expected = []string{fourthID, thirdID, secondID} + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("SINCE filter & ALL: Container list is not in the correct order: \n%s", out)) + + out, _ = dockerCmd(c, "ps", "-f", "since="+firstID) + expected = []string{fourthID, secondID} + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("SINCE filter: Container list is not in the correct order: \n%s", out)) + + out, _ = dockerCmd(c, "ps", "-f", "since="+thirdID) + expected = []string{fourthID} + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("SINCE filter: Container list is not in the correct order: \n%s", out)) + + // filter before + out, _ = dockerCmd(c, "ps", "-f", "before="+fourthID, "-a") + expected = []string{thirdID, secondID, firstID} + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("BEFORE filter & ALL: Container list is not in the correct order: \n%s", out)) + + out, _ = dockerCmd(c, "ps", "-f", "before="+fourthID) + expected = []string{secondID, firstID} + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("BEFORE filter: Container list is not in the correct order: \n%s", out)) + + out, _ = dockerCmd(c, "ps", "-f", "before="+thirdID) + expected = []string{secondID, firstID} + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("SINCE filter: Container list is not in the correct order: \n%s", out)) + + // filter since & before + out, _ = dockerCmd(c, "ps", "-f", "since="+firstID, "-f", "before="+fourthID, "-a") + expected = []string{thirdID, secondID} + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("SINCE filter, BEFORE filter & ALL: Container list is not in the correct order: \n%s", out)) + + out, _ = dockerCmd(c, "ps", "-f", "since="+firstID, "-f", "before="+fourthID) + expected = []string{secondID} + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("SINCE filter, BEFORE filter: Container list is not in the correct order: \n%s", out)) + + // filter since & limit + out, _ = dockerCmd(c, "ps", "-f", "since="+firstID, "-n=2", "-a") + expected = []string{fourthID, thirdID} + + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("SINCE filter, LIMIT & ALL: Container list is not in the correct order: \n%s", out)) + + out, _ = dockerCmd(c, "ps", "-f", "since="+firstID, "-n=2") + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("SINCE filter, LIMIT: Container list is not in the correct order: \n%s", out)) + + // filter before & limit + out, _ = dockerCmd(c, "ps", "-f", "before="+fourthID, "-n=1", "-a") + expected = []string{thirdID} + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("BEFORE filter, LIMIT & ALL: Container list is not in the correct order: \n%s", out)) + + out, _ = dockerCmd(c, "ps", "-f", "before="+fourthID, "-n=1") + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("BEFORE filter, LIMIT: Container list is not in the correct order: \n%s", out)) + + // filter since & filter before & limit + out, _ = dockerCmd(c, "ps", "-f", "since="+firstID, "-f", "before="+fourthID, "-n=1", "-a") + expected = []string{thirdID} + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("SINCE filter, BEFORE filter, LIMIT & ALL: Container list is not in the correct order: \n%s", out)) + + out, _ = dockerCmd(c, "ps", "-f", "since="+firstID, "-f", "before="+fourthID, "-n=1") + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("SINCE filter, BEFORE filter, LIMIT: Container list is not in the correct order: \n%s", out)) + +} + +// FIXME remove this for 1.12 as --since and --before are deprecated +func (s *DockerSuite) TestPsListContainersDeprecatedSinceAndBefore(c *check.C) { + out, _ := runSleepingContainer(c, "-d") + firstID := strings.TrimSpace(out) + + out, _ = runSleepingContainer(c, "-d") + secondID := strings.TrimSpace(out) + + // not long running + out, _ = dockerCmd(c, "run", "-d", "busybox", "true") + thirdID := strings.TrimSpace(out) + + out, _ = runSleepingContainer(c, "-d") + fourthID := strings.TrimSpace(out) + + // make sure the second is running + c.Assert(waitRun(secondID), checker.IsNil) + + // make sure third one is not running + dockerCmd(c, "wait", thirdID) + + // make sure the forth is running + c.Assert(waitRun(fourthID), checker.IsNil) + + // since + out, _ = dockerCmd(c, "ps", "--since="+firstID, "-a") + expected := []string{fourthID, thirdID, secondID} + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("SINCE & ALL: Container list is not in the correct order: %v \n%s", expected, out)) + + out, _ = dockerCmd(c, "ps", "--since="+firstID) + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("SINCE: Container list is not in the correct order: %v \n%s", expected, out)) + + // before + out, _ = dockerCmd(c, "ps", "--before="+thirdID, "-a") + expected = []string{secondID, firstID} + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("BEFORE & ALL: Container list is not in the correct order: %v \n%s", expected, out)) + + out, _ = dockerCmd(c, "ps", "--before="+thirdID) + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("BEFORE: Container list is not in the correct order: %v \n%s", expected, out)) + + // since & before + out, _ = dockerCmd(c, "ps", "--since="+firstID, "--before="+fourthID, "-a") + expected = []string{thirdID, secondID} + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("SINCE, BEFORE & ALL: Container list is not in the correct order: %v \n%s", expected, out)) + + out, _ = dockerCmd(c, "ps", "--since="+firstID, "--before="+fourthID) + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("SINCE, BEFORE: Container list is not in the correct order: %v \n%s", expected, out)) + + // since & limit + out, _ = dockerCmd(c, "ps", "--since="+firstID, "-n=2", "-a") + expected = []string{fourthID, thirdID} + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("SINCE, LIMIT & ALL: Container list is not in the correct order: %v \n%s", expected, out)) + + out, _ = dockerCmd(c, "ps", "--since="+firstID, "-n=2") + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("SINCE, LIMIT: Container list is not in the correct order: %v \n%s", expected, out)) + + // before & limit + out, _ = dockerCmd(c, "ps", "--before="+fourthID, "-n=1", "-a") + expected = []string{thirdID} + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("BEFORE, LIMIT & ALL: Container list is not in the correct order: %v \n%s", expected, out)) + + out, _ = dockerCmd(c, "ps", "--before="+fourthID, "-n=1") + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("BEFORE, LIMIT: Container list is not in the correct order: %v \n%s", expected, out)) + + // since & before & limit + out, _ = dockerCmd(c, "ps", "--since="+firstID, "--before="+fourthID, "-n=1", "-a") + expected = []string{thirdID} + c.Assert(assertContainerList(out, expected), checker.Equals, true, check.Commentf("SINCE, BEFORE, LIMIT & ALL: Container list is not in the correct order: %v \n%s", expected, out)) + +} + +func assertContainerList(out string, expected []string) bool { + lines := strings.Split(strings.Trim(out, "\n "), "\n") + // FIXME remove this for 1.12 as --since and --before are deprecated + // This is here to remove potential Warning: lines (printed out with deprecated flags) + for i := 0; i < 2; i++ { + if strings.Contains(lines[0], "Warning:") { + lines = lines[1:] + } + } + + if len(lines)-1 != len(expected) { + return false + } + + containerIDIndex := strings.Index(lines[0], "CONTAINER ID") + for i := 0; i < len(expected); i++ { + foundID := lines[i+1][containerIDIndex : containerIDIndex+12] + if foundID != expected[i][:12] { + return false + } + } + + return true +} + +func (s *DockerSuite) TestPsListContainersInvalidFilterName(c *check.C) { + out, _, err := dockerCmdWithError("ps", "-f", "invalidFilter=test") + c.Assert(err, checker.NotNil) + c.Assert(out, checker.Contains, "Invalid filter") +} + +func (s *DockerSuite) TestPsListContainersSize(c *check.C) { + // Problematic on Windows as it doesn't report the size correctly @swernli + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "-d", "busybox") + + baseOut, _ := dockerCmd(c, "ps", "-s", "-n=1") + baseLines := strings.Split(strings.Trim(baseOut, "\n "), "\n") + baseSizeIndex := strings.Index(baseLines[0], "SIZE") + baseFoundsize := baseLines[1][baseSizeIndex:] + baseBytes, err := strconv.Atoi(strings.Split(baseFoundsize, " ")[0]) + c.Assert(err, checker.IsNil) + + name := "test_size" + dockerCmd(c, "run", "--name", name, "busybox", "sh", "-c", "echo 1 > test") + id, err := getIDByName(name) + c.Assert(err, checker.IsNil) + + runCmd := exec.Command(dockerBinary, "ps", "-s", "-n=1") + var out string + + wait := make(chan struct{}) + go func() { + out, _, err = runCommandWithOutput(runCmd) + close(wait) + }() + select { + case <-wait: + case <-time.After(3 * time.Second): + c.Fatalf("Calling \"docker ps -s\" timed out!") + } + c.Assert(err, checker.IsNil) + lines := strings.Split(strings.Trim(out, "\n "), "\n") + c.Assert(lines, checker.HasLen, 2, check.Commentf("Expected 2 lines for 'ps -s -n=1' output, got %d", len(lines))) + sizeIndex := strings.Index(lines[0], "SIZE") + idIndex := strings.Index(lines[0], "CONTAINER ID") + foundID := lines[1][idIndex : idIndex+12] + c.Assert(foundID, checker.Equals, id[:12], check.Commentf("Expected id %s, got %s", id[:12], foundID)) + expectedSize := fmt.Sprintf("%d B", (2 + baseBytes)) + foundSize := lines[1][sizeIndex:] + c.Assert(foundSize, checker.Contains, expectedSize, check.Commentf("Expected size %q, got %q", expectedSize, foundSize)) +} + +func (s *DockerSuite) TestPsListContainersFilterStatus(c *check.C) { + // start exited container + out, _ := dockerCmd(c, "run", "-d", "busybox") + firstID := strings.TrimSpace(out) + + // make sure the exited container is not running + dockerCmd(c, "wait", firstID) + + // start running container + out, _ = dockerCmd(c, "run", "-itd", "busybox") + secondID := strings.TrimSpace(out) + + // filter containers by exited + out, _ = dockerCmd(c, "ps", "--no-trunc", "-q", "--filter=status=exited") + containerOut := strings.TrimSpace(out) + c.Assert(containerOut, checker.Equals, firstID) + + out, _ = dockerCmd(c, "ps", "-a", "--no-trunc", "-q", "--filter=status=running") + containerOut = strings.TrimSpace(out) + c.Assert(containerOut, checker.Equals, secondID) + + out, _, _ = dockerCmdWithTimeout(time.Second*60, "ps", "-a", "-q", "--filter=status=rubbish") + c.Assert(out, checker.Contains, "Unrecognised filter value for status", check.Commentf("Expected error response due to invalid status filter output: %q", out)) + + // Windows doesn't support pausing of containers + if daemonPlatform != "windows" { + // pause running container + out, _ = dockerCmd(c, "run", "-itd", "busybox") + pausedID := strings.TrimSpace(out) + dockerCmd(c, "pause", pausedID) + // make sure the container is unpaused to let the daemon stop it properly + defer func() { dockerCmd(c, "unpause", pausedID) }() + + out, _ = dockerCmd(c, "ps", "--no-trunc", "-q", "--filter=status=paused") + containerOut = strings.TrimSpace(out) + c.Assert(containerOut, checker.Equals, pausedID) + } +} + +func (s *DockerSuite) TestPsListContainersFilterID(c *check.C) { + // start container + out, _ := dockerCmd(c, "run", "-d", "busybox") + firstID := strings.TrimSpace(out) + + // start another container + runSleepingContainer(c) + + // filter containers by id + out, _ = dockerCmd(c, "ps", "-a", "-q", "--filter=id="+firstID) + containerOut := strings.TrimSpace(out) + c.Assert(containerOut, checker.Equals, firstID[:12], check.Commentf("Expected id %s, got %s for exited filter, output: %q", firstID[:12], containerOut, out)) + +} + +func (s *DockerSuite) TestPsListContainersFilterName(c *check.C) { + // start container + dockerCmd(c, "run", "--name=a_name_to_match", "busybox") + id, err := getIDByName("a_name_to_match") + c.Assert(err, check.IsNil) + + // start another container + runSleepingContainer(c, "--name=b_name_to_match") + + // filter containers by name + out, _ := dockerCmd(c, "ps", "-a", "-q", "--filter=name=a_name_to_match") + containerOut := strings.TrimSpace(out) + c.Assert(containerOut, checker.Equals, id[:12], check.Commentf("Expected id %s, got %s for exited filter, output: %q", id[:12], containerOut, out)) +} + +// Test for the ancestor filter for ps. +// There is also the same test but with image:tag@digest in docker_cli_by_digest_test.go +// +// What the test setups : +// - Create 2 image based on busybox using the same repository but different tags +// - Create an image based on the previous image (images_ps_filter_test2) +// - Run containers for each of those image (busybox, images_ps_filter_test1, images_ps_filter_test2) +// - Filter them out :P +func (s *DockerSuite) TestPsListContainersFilterAncestorImage(c *check.C) { + // Build images + imageName1 := "images_ps_filter_test1" + imageID1, err := buildImage(imageName1, + `FROM busybox + LABEL match me 1`, true) + c.Assert(err, checker.IsNil) + + imageName1Tagged := "images_ps_filter_test1:tag" + imageID1Tagged, err := buildImage(imageName1Tagged, + `FROM busybox + LABEL match me 1 tagged`, true) + c.Assert(err, checker.IsNil) + + imageName2 := "images_ps_filter_test2" + imageID2, err := buildImage(imageName2, + fmt.Sprintf(`FROM %s + LABEL match me 2`, imageName1), true) + c.Assert(err, checker.IsNil) + + // start containers + dockerCmd(c, "run", "--name=first", "busybox", "echo", "hello") + firstID, err := getIDByName("first") + c.Assert(err, check.IsNil) + + // start another container + dockerCmd(c, "run", "--name=second", "busybox", "echo", "hello") + secondID, err := getIDByName("second") + c.Assert(err, check.IsNil) + + // start third container + dockerCmd(c, "run", "--name=third", imageName1, "echo", "hello") + thirdID, err := getIDByName("third") + c.Assert(err, check.IsNil) + + // start fourth container + dockerCmd(c, "run", "--name=fourth", imageName1Tagged, "echo", "hello") + fourthID, err := getIDByName("fourth") + c.Assert(err, check.IsNil) + + // start fifth container + dockerCmd(c, "run", "--name=fifth", imageName2, "echo", "hello") + fifthID, err := getIDByName("fifth") + c.Assert(err, check.IsNil) + + var filterTestSuite = []struct { + filterName string + expectedIDs []string + }{ + // non existent stuff + {"nonexistent", []string{}}, + {"nonexistent:tag", []string{}}, + // image + {"busybox", []string{firstID, secondID, thirdID, fourthID, fifthID}}, + {imageName1, []string{thirdID, fifthID}}, + {imageName2, []string{fifthID}}, + // image:tag + {fmt.Sprintf("%s:latest", imageName1), []string{thirdID, fifthID}}, + {imageName1Tagged, []string{fourthID}}, + // short-id + {stringid.TruncateID(imageID1), []string{thirdID, fifthID}}, + {stringid.TruncateID(imageID2), []string{fifthID}}, + // full-id + {imageID1, []string{thirdID, fifthID}}, + {imageID1Tagged, []string{fourthID}}, + {imageID2, []string{fifthID}}, + } + + var out string + for _, filter := range filterTestSuite { + out, _ = dockerCmd(c, "ps", "-a", "-q", "--no-trunc", "--filter=ancestor="+filter.filterName) + checkPsAncestorFilterOutput(c, out, filter.filterName, filter.expectedIDs) + } + + // Multiple ancestor filter + out, _ = dockerCmd(c, "ps", "-a", "-q", "--no-trunc", "--filter=ancestor="+imageName2, "--filter=ancestor="+imageName1Tagged) + checkPsAncestorFilterOutput(c, out, imageName2+","+imageName1Tagged, []string{fourthID, fifthID}) +} + +func checkPsAncestorFilterOutput(c *check.C, out string, filterName string, expectedIDs []string) { + actualIDs := []string{} + if out != "" { + actualIDs = strings.Split(out[:len(out)-1], "\n") + } + sort.Strings(actualIDs) + sort.Strings(expectedIDs) + + c.Assert(actualIDs, checker.HasLen, len(expectedIDs), check.Commentf("Expected filtered container(s) for %s ancestor filter to be %v:%v, got %v:%v", filterName, len(expectedIDs), expectedIDs, len(actualIDs), actualIDs)) + if len(expectedIDs) > 0 { + same := true + for i := range expectedIDs { + if actualIDs[i] != expectedIDs[i] { + c.Logf("%s, %s", actualIDs[i], expectedIDs[i]) + same = false + break + } + } + c.Assert(same, checker.Equals, true, check.Commentf("Expected filtered container(s) for %s ancestor filter to be %v, got %v", filterName, expectedIDs, actualIDs)) + } +} + +func (s *DockerSuite) TestPsListContainersFilterLabel(c *check.C) { + // start container + dockerCmd(c, "run", "--name=first", "-l", "match=me", "-l", "second=tag", "busybox") + firstID, err := getIDByName("first") + c.Assert(err, check.IsNil) + + // start another container + dockerCmd(c, "run", "--name=second", "-l", "match=me too", "busybox") + secondID, err := getIDByName("second") + c.Assert(err, check.IsNil) + + // start third container + dockerCmd(c, "run", "--name=third", "-l", "nomatch=me", "busybox") + thirdID, err := getIDByName("third") + c.Assert(err, check.IsNil) + + // filter containers by exact match + out, _ := dockerCmd(c, "ps", "-a", "-q", "--no-trunc", "--filter=label=match=me") + containerOut := strings.TrimSpace(out) + c.Assert(containerOut, checker.Equals, firstID, check.Commentf("Expected id %s, got %s for exited filter, output: %q", firstID, containerOut, out)) + + // filter containers by two labels + out, _ = dockerCmd(c, "ps", "-a", "-q", "--no-trunc", "--filter=label=match=me", "--filter=label=second=tag") + containerOut = strings.TrimSpace(out) + c.Assert(containerOut, checker.Equals, firstID, check.Commentf("Expected id %s, got %s for exited filter, output: %q", firstID, containerOut, out)) + + // filter containers by two labels, but expect not found because of AND behavior + out, _ = dockerCmd(c, "ps", "-a", "-q", "--no-trunc", "--filter=label=match=me", "--filter=label=second=tag-no") + containerOut = strings.TrimSpace(out) + c.Assert(containerOut, checker.Equals, "", check.Commentf("Expected nothing, got %s for exited filter, output: %q", containerOut, out)) + + // filter containers by exact key + out, _ = dockerCmd(c, "ps", "-a", "-q", "--no-trunc", "--filter=label=match") + containerOut = strings.TrimSpace(out) + c.Assert(containerOut, checker.Contains, firstID) + c.Assert(containerOut, checker.Contains, secondID) + c.Assert(containerOut, checker.Not(checker.Contains), thirdID) +} + +func (s *DockerSuite) TestPsListContainersFilterExited(c *check.C) { + runSleepingContainer(c, "--name=sleep") + + dockerCmd(c, "run", "--name", "zero1", "busybox", "true") + firstZero, err := getIDByName("zero1") + c.Assert(err, checker.IsNil) + + dockerCmd(c, "run", "--name", "zero2", "busybox", "true") + secondZero, err := getIDByName("zero2") + c.Assert(err, checker.IsNil) + + out, _, err := dockerCmdWithError("run", "--name", "nonzero1", "busybox", "false") + c.Assert(err, checker.NotNil, check.Commentf("Should fail.", out, err)) + + firstNonZero, err := getIDByName("nonzero1") + c.Assert(err, checker.IsNil) + + out, _, err = dockerCmdWithError("run", "--name", "nonzero2", "busybox", "false") + c.Assert(err, checker.NotNil, check.Commentf("Should fail.", out, err)) + secondNonZero, err := getIDByName("nonzero2") + c.Assert(err, checker.IsNil) + + // filter containers by exited=0 + out, _ = dockerCmd(c, "ps", "-a", "-q", "--no-trunc", "--filter=exited=0") + ids := strings.Split(strings.TrimSpace(out), "\n") + c.Assert(ids, checker.HasLen, 2, check.Commentf("Should be 2 zero exited containers got %d: %s", len(ids), out)) + c.Assert(ids[0], checker.Equals, secondZero, check.Commentf("First in list should be %q, got %q", secondZero, ids[0])) + c.Assert(ids[1], checker.Equals, firstZero, check.Commentf("Second in list should be %q, got %q", firstZero, ids[1])) + + out, _ = dockerCmd(c, "ps", "-a", "-q", "--no-trunc", "--filter=exited=1") + ids = strings.Split(strings.TrimSpace(out), "\n") + c.Assert(ids, checker.HasLen, 2, check.Commentf("Should be 2 zero exited containers got %d", len(ids))) + c.Assert(ids[0], checker.Equals, secondNonZero, check.Commentf("First in list should be %q, got %q", secondNonZero, ids[0])) + c.Assert(ids[1], checker.Equals, firstNonZero, check.Commentf("Second in list should be %q, got %q", firstNonZero, ids[1])) + +} + +func (s *DockerSuite) TestPsRightTagName(c *check.C) { + // TODO Investigate further why this fails on Windows to Windows CI + testRequires(c, DaemonIsLinux) + tag := "asybox:shmatest" + dockerCmd(c, "tag", "busybox", tag) + + var id1 string + out, _ := runSleepingContainer(c) + id1 = strings.TrimSpace(string(out)) + + var id2 string + out, _ = runSleepingContainerInImage(c, tag) + id2 = strings.TrimSpace(string(out)) + + var imageID string + out = inspectField(c, "busybox", "Id") + imageID = strings.TrimSpace(string(out)) + + var id3 string + out, _ = runSleepingContainerInImage(c, imageID) + id3 = strings.TrimSpace(string(out)) + + out, _ = dockerCmd(c, "ps", "--no-trunc") + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + // skip header + lines = lines[1:] + c.Assert(lines, checker.HasLen, 3, check.Commentf("There should be 3 running container, got %d", len(lines))) + for _, line := range lines { + f := strings.Fields(line) + switch f[0] { + case id1: + c.Assert(f[1], checker.Equals, "busybox", check.Commentf("Expected %s tag for id %s, got %s", "busybox", id1, f[1])) + case id2: + c.Assert(f[1], checker.Equals, tag, check.Commentf("Expected %s tag for id %s, got %s", tag, id2, f[1])) + case id3: + c.Assert(f[1], checker.Equals, imageID, check.Commentf("Expected %s imageID for id %s, got %s", tag, id3, f[1])) + default: + c.Fatalf("Unexpected id %s, expected %s and %s and %s", f[0], id1, id2, id3) + } + } +} + +func (s *DockerSuite) TestPsLinkedWithNoTrunc(c *check.C) { + // Problematic on Windows as it doesn't support links as of Jan 2016 + testRequires(c, DaemonIsLinux) + runSleepingContainer(c, "--name=first") + runSleepingContainer(c, "--name=second", "--link=first:first") + + out, _ := dockerCmd(c, "ps", "--no-trunc") + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + // strip header + lines = lines[1:] + expected := []string{"second", "first,second/first"} + var names []string + for _, l := range lines { + fields := strings.Fields(l) + names = append(names, fields[len(fields)-1]) + } + c.Assert(expected, checker.DeepEquals, names, check.Commentf("Expected array: %v, got: %v", expected, names)) +} + +func (s *DockerSuite) TestPsGroupPortRange(c *check.C) { + // Problematic on Windows as it doesn't support port ranges as of Jan 2016 + testRequires(c, DaemonIsLinux) + portRange := "3800-3900" + dockerCmd(c, "run", "-d", "--name", "porttest", "-p", portRange+":"+portRange, "busybox", "top") + + out, _ := dockerCmd(c, "ps") + + c.Assert(string(out), checker.Contains, portRange, check.Commentf("docker ps output should have had the port range %q: %s", portRange, string(out))) + +} + +func (s *DockerSuite) TestPsWithSize(c *check.C) { + // Problematic on Windows as it doesn't report the size correctly @swernli + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "-d", "--name", "sizetest", "busybox", "top") + + out, _ := dockerCmd(c, "ps", "--size") + c.Assert(out, checker.Contains, "virtual", check.Commentf("docker ps with --size should show virtual size of container")) +} + +func (s *DockerSuite) TestPsListContainersFilterCreated(c *check.C) { + // create a container + out, _ := dockerCmd(c, "create", "busybox") + cID := strings.TrimSpace(out) + shortCID := cID[:12] + + // Make sure it DOESN'T show up w/o a '-a' for normal 'ps' + out, _ = dockerCmd(c, "ps", "-q") + c.Assert(out, checker.Not(checker.Contains), shortCID, check.Commentf("Should have not seen '%s' in ps output:\n%s", shortCID, out)) + + // Make sure it DOES show up as 'Created' for 'ps -a' + out, _ = dockerCmd(c, "ps", "-a") + + hits := 0 + for _, line := range strings.Split(out, "\n") { + if !strings.Contains(line, shortCID) { + continue + } + hits++ + c.Assert(line, checker.Contains, "Created", check.Commentf("Missing 'Created' on '%s'", line)) + } + + c.Assert(hits, checker.Equals, 1, check.Commentf("Should have seen '%s' in ps -a output once:%d\n%s", shortCID, hits, out)) + + // filter containers by 'create' - note, no -a needed + out, _ = dockerCmd(c, "ps", "-q", "-f", "status=created") + containerOut := strings.TrimSpace(out) + c.Assert(cID, checker.HasPrefix, containerOut) +} + +func (s *DockerSuite) TestPsFormatMultiNames(c *check.C) { + // Problematic on Windows as it doesn't support link as of Jan 2016 + testRequires(c, DaemonIsLinux) + //create 2 containers and link them + dockerCmd(c, "run", "--name=child", "-d", "busybox", "top") + dockerCmd(c, "run", "--name=parent", "--link=child:linkedone", "-d", "busybox", "top") + + //use the new format capabilities to only list the names and --no-trunc to get all names + out, _ := dockerCmd(c, "ps", "--format", "{{.Names}}", "--no-trunc") + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + expected := []string{"parent", "child,parent/linkedone"} + var names []string + for _, l := range lines { + names = append(names, l) + } + c.Assert(expected, checker.DeepEquals, names, check.Commentf("Expected array with non-truncated names: %v, got: %v", expected, names)) + + //now list without turning off truncation and make sure we only get the non-link names + out, _ = dockerCmd(c, "ps", "--format", "{{.Names}}") + lines = strings.Split(strings.TrimSpace(string(out)), "\n") + expected = []string{"parent", "child"} + var truncNames []string + for _, l := range lines { + truncNames = append(truncNames, l) + } + c.Assert(expected, checker.DeepEquals, truncNames, check.Commentf("Expected array with truncated names: %v, got: %v", expected, truncNames)) + +} + +func (s *DockerSuite) TestPsFormatHeaders(c *check.C) { + // make sure no-container "docker ps" still prints the header row + out, _ := dockerCmd(c, "ps", "--format", "table {{.ID}}") + c.Assert(out, checker.Equals, "CONTAINER ID\n", check.Commentf(`Expected 'CONTAINER ID\n', got %v`, out)) + + // verify that "docker ps" with a container still prints the header row also + runSleepingContainer(c, "--name=test") + out, _ = dockerCmd(c, "ps", "--format", "table {{.Names}}") + c.Assert(out, checker.Equals, "NAMES\ntest\n", check.Commentf(`Expected 'NAMES\ntest\n', got %v`, out)) +} + +func (s *DockerSuite) TestPsDefaultFormatAndQuiet(c *check.C) { + config := `{ + "psFormat": "default {{ .ID }}" +}` + d, err := ioutil.TempDir("", "integration-cli-") + c.Assert(err, checker.IsNil) + defer os.RemoveAll(d) + + err = ioutil.WriteFile(filepath.Join(d, "config.json"), []byte(config), 0644) + c.Assert(err, checker.IsNil) + + out, _ := runSleepingContainer(c, "--name=test") + id := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "--config", d, "ps", "-q") + c.Assert(id, checker.HasPrefix, strings.TrimSpace(out), check.Commentf("Expected to print only the container id, got %v\n", out)) +} + +// Test for GitHub issue #12595 +func (s *DockerSuite) TestPsImageIDAfterUpdate(c *check.C) { + // TODO: Investigate why this fails on Windows to Windows CI further. + testRequires(c, DaemonIsLinux) + originalImageName := "busybox:TestPsImageIDAfterUpdate-original" + updatedImageName := "busybox:TestPsImageIDAfterUpdate-updated" + + runCmd := exec.Command(dockerBinary, "tag", "busybox:latest", originalImageName) + out, _, err := runCommandWithOutput(runCmd) + c.Assert(err, checker.IsNil) + + originalImageID, err := getIDByName(originalImageName) + c.Assert(err, checker.IsNil) + + runCmd = exec.Command(dockerBinary, append([]string{"run", "-d", originalImageName}, defaultSleepCommand...)...) + out, _, err = runCommandWithOutput(runCmd) + c.Assert(err, checker.IsNil) + containerID := strings.TrimSpace(out) + + linesOut, err := exec.Command(dockerBinary, "ps", "--no-trunc").CombinedOutput() + c.Assert(err, checker.IsNil) + + lines := strings.Split(strings.TrimSpace(string(linesOut)), "\n") + // skip header + lines = lines[1:] + c.Assert(len(lines), checker.Equals, 1) + + for _, line := range lines { + f := strings.Fields(line) + c.Assert(f[1], checker.Equals, originalImageName) + } + + runCmd = exec.Command(dockerBinary, "commit", containerID, updatedImageName) + out, _, err = runCommandWithOutput(runCmd) + c.Assert(err, checker.IsNil) + + runCmd = exec.Command(dockerBinary, "tag", "-f", updatedImageName, originalImageName) + out, _, err = runCommandWithOutput(runCmd) + c.Assert(err, checker.IsNil) + + linesOut, err = exec.Command(dockerBinary, "ps", "--no-trunc").CombinedOutput() + c.Assert(err, checker.IsNil) + + lines = strings.Split(strings.TrimSpace(string(linesOut)), "\n") + // skip header + lines = lines[1:] + c.Assert(len(lines), checker.Equals, 1) + + for _, line := range lines { + f := strings.Fields(line) + c.Assert(f[1], checker.Equals, originalImageID) + } + +} + +func (s *DockerSuite) TestPsNotShowPortsOfStoppedContainer(c *check.C) { + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "--name=foo", "-d", "-p", "5000:5000", "busybox", "top") + c.Assert(waitRun("foo"), checker.IsNil) + out, _ := dockerCmd(c, "ps") + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + expected := "0.0.0.0:5000->5000/tcp" + fields := strings.Fields(lines[1]) + c.Assert(fields[len(fields)-2], checker.Equals, expected, check.Commentf("Expected: %v, got: %v", expected, fields[len(fields)-2])) + + dockerCmd(c, "kill", "foo") + dockerCmd(c, "wait", "foo") + out, _ = dockerCmd(c, "ps", "-l") + lines = strings.Split(strings.TrimSpace(string(out)), "\n") + fields = strings.Fields(lines[1]) + c.Assert(fields[len(fields)-2], checker.Not(checker.Equals), expected, check.Commentf("Should not got %v", expected)) +} + +func (s *DockerSuite) TestPsShowMounts(c *check.C) { + prefix, slash := getPrefixAndSlashFromDaemonPlatform() + + mp := prefix + slash + "test" + + dockerCmd(c, "volume", "create", "--name", "ps-volume-test") + // volume mount containers + runSleepingContainer(c, "--name=volume-test-1", "--volume", "ps-volume-test:"+mp) + c.Assert(waitRun("volume-test-1"), checker.IsNil) + runSleepingContainer(c, "--name=volume-test-2", "--volume", mp) + c.Assert(waitRun("volume-test-2"), checker.IsNil) + // bind mount container + var bindMountSource string + var bindMountDestination string + if DaemonIsWindows.Condition() { + bindMountSource = "c:\\" + bindMountDestination = "c:\\t" + } else { + bindMountSource = "/tmp" + bindMountDestination = "/t" + } + runSleepingContainer(c, "--name=bind-mount-test", "-v", bindMountSource+":"+bindMountDestination) + c.Assert(waitRun("bind-mount-test"), checker.IsNil) + + out, _ := dockerCmd(c, "ps", "--format", "{{.Names}} {{.Mounts}}") + + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + c.Assert(lines, checker.HasLen, 3) + + fields := strings.Fields(lines[0]) + c.Assert(fields, checker.HasLen, 2) + c.Assert(fields[0], checker.Equals, "bind-mount-test") + c.Assert(fields[1], checker.Equals, bindMountSource) + + fields = strings.Fields(lines[1]) + c.Assert(fields, checker.HasLen, 2) + + annonymounsVolumeID := fields[1] + + fields = strings.Fields(lines[2]) + c.Assert(fields[1], checker.Equals, "ps-volume-test") + + // filter by volume name + out, _ = dockerCmd(c, "ps", "--format", "{{.Names}} {{.Mounts}}", "--filter", "volume=ps-volume-test") + + lines = strings.Split(strings.TrimSpace(string(out)), "\n") + c.Assert(lines, checker.HasLen, 1) + + fields = strings.Fields(lines[0]) + c.Assert(fields[1], checker.Equals, "ps-volume-test") + + // empty results filtering by unknown volume + out, _ = dockerCmd(c, "ps", "--format", "{{.Names}} {{.Mounts}}", "--filter", "volume=this-volume-should-not-exist") + c.Assert(strings.TrimSpace(string(out)), checker.HasLen, 0) + + // filter by mount destination + out, _ = dockerCmd(c, "ps", "--format", "{{.Names}} {{.Mounts}}", "--filter", "volume="+mp) + + lines = strings.Split(strings.TrimSpace(string(out)), "\n") + c.Assert(lines, checker.HasLen, 2) + + fields = strings.Fields(lines[0]) + c.Assert(fields[1], checker.Equals, annonymounsVolumeID) + fields = strings.Fields(lines[1]) + c.Assert(fields[1], checker.Equals, "ps-volume-test") + + // filter by bind mount source + out, _ = dockerCmd(c, "ps", "--format", "{{.Names}} {{.Mounts}}", "--filter", "volume="+bindMountSource) + + lines = strings.Split(strings.TrimSpace(string(out)), "\n") + c.Assert(lines, checker.HasLen, 1) + + fields = strings.Fields(lines[0]) + c.Assert(fields, checker.HasLen, 2) + c.Assert(fields[0], checker.Equals, "bind-mount-test") + c.Assert(fields[1], checker.Equals, bindMountSource) + + // filter by bind mount destination + out, _ = dockerCmd(c, "ps", "--format", "{{.Names}} {{.Mounts}}", "--filter", "volume="+bindMountDestination) + + lines = strings.Split(strings.TrimSpace(string(out)), "\n") + c.Assert(lines, checker.HasLen, 1) + + fields = strings.Fields(lines[0]) + c.Assert(fields, checker.HasLen, 2) + c.Assert(fields[0], checker.Equals, "bind-mount-test") + c.Assert(fields[1], checker.Equals, bindMountSource) + + // empty results filtering by unknown mount point + out, _ = dockerCmd(c, "ps", "--format", "{{.Names}} {{.Mounts}}", "--filter", "volume="+prefix+slash+"this-path-was-never-mounted") + c.Assert(strings.TrimSpace(string(out)), checker.HasLen, 0) +} diff --git a/integration-cli/docker_cli_pull_local_test.go b/integration-cli/docker_cli_pull_local_test.go new file mode 100644 index 00000000..1f858320 --- /dev/null +++ b/integration-cli/docker_cli_pull_local_test.go @@ -0,0 +1,423 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + + "github.com/docker/distribution" + "github.com/docker/distribution/digest" + "github.com/docker/distribution/manifest" + "github.com/docker/distribution/manifest/manifestlist" + "github.com/docker/distribution/manifest/schema2" + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +// testPullImageWithAliases pulls a specific image tag and verifies that any aliases (i.e., other +// tags for the same image) are not also pulled down. +// +// Ref: docker/docker#8141 +func testPullImageWithAliases(c *check.C) { + repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL) + + repos := []string{} + for _, tag := range []string{"recent", "fresh"} { + repos = append(repos, fmt.Sprintf("%v:%v", repoName, tag)) + } + + // Tag and push the same image multiple times. + for _, repo := range repos { + dockerCmd(c, "tag", "busybox", repo) + dockerCmd(c, "push", repo) + } + + // Clear local images store. + args := append([]string{"rmi"}, repos...) + dockerCmd(c, args...) + + // Pull a single tag and verify it doesn't bring down all aliases. + dockerCmd(c, "pull", repos[0]) + dockerCmd(c, "inspect", repos[0]) + for _, repo := range repos[1:] { + _, _, err := dockerCmdWithError("inspect", repo) + c.Assert(err, checker.NotNil, check.Commentf("Image %v shouldn't have been pulled down", repo)) + } +} + +func (s *DockerRegistrySuite) TestPullImageWithAliases(c *check.C) { + testPullImageWithAliases(c) +} + +func (s *DockerSchema1RegistrySuite) TestPullImageWithAliases(c *check.C) { + testPullImageWithAliases(c) +} + +// testConcurrentPullWholeRepo pulls the same repo concurrently. +func testConcurrentPullWholeRepo(c *check.C) { + repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL) + + repos := []string{} + for _, tag := range []string{"recent", "fresh", "todays"} { + repo := fmt.Sprintf("%v:%v", repoName, tag) + _, err := buildImage(repo, fmt.Sprintf(` + FROM busybox + ENTRYPOINT ["/bin/echo"] + ENV FOO foo + ENV BAR bar + CMD echo %s + `, repo), true) + c.Assert(err, checker.IsNil) + dockerCmd(c, "push", repo) + repos = append(repos, repo) + } + + // Clear local images store. + args := append([]string{"rmi"}, repos...) + dockerCmd(c, args...) + + // Run multiple re-pulls concurrently + results := make(chan error) + numPulls := 3 + + for i := 0; i != numPulls; i++ { + go func() { + _, _, err := runCommandWithOutput(exec.Command(dockerBinary, "pull", "-a", repoName)) + results <- err + }() + } + + // These checks are separate from the loop above because the check + // package is not goroutine-safe. + for i := 0; i != numPulls; i++ { + err := <-results + c.Assert(err, checker.IsNil, check.Commentf("concurrent pull failed with error: %v", err)) + } + + // Ensure all tags were pulled successfully + for _, repo := range repos { + dockerCmd(c, "inspect", repo) + out, _ := dockerCmd(c, "run", "--rm", repo) + c.Assert(strings.TrimSpace(out), checker.Equals, "/bin/sh -c echo "+repo) + } +} + +func (s *DockerRegistrySuite) testConcurrentPullWholeRepo(c *check.C) { + testConcurrentPullWholeRepo(c) +} + +func (s *DockerSchema1RegistrySuite) testConcurrentPullWholeRepo(c *check.C) { + testConcurrentPullWholeRepo(c) +} + +// testConcurrentFailingPull tries a concurrent pull that doesn't succeed. +func testConcurrentFailingPull(c *check.C) { + repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL) + + // Run multiple pulls concurrently + results := make(chan error) + numPulls := 3 + + for i := 0; i != numPulls; i++ { + go func() { + _, _, err := runCommandWithOutput(exec.Command(dockerBinary, "pull", repoName+":asdfasdf")) + results <- err + }() + } + + // These checks are separate from the loop above because the check + // package is not goroutine-safe. + for i := 0; i != numPulls; i++ { + err := <-results + c.Assert(err, checker.NotNil, check.Commentf("expected pull to fail")) + } +} + +func (s *DockerRegistrySuite) testConcurrentFailingPull(c *check.C) { + testConcurrentFailingPull(c) +} + +func (s *DockerSchema1RegistrySuite) testConcurrentFailingPull(c *check.C) { + testConcurrentFailingPull(c) +} + +// testConcurrentPullMultipleTags pulls multiple tags from the same repo +// concurrently. +func testConcurrentPullMultipleTags(c *check.C) { + repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL) + + repos := []string{} + for _, tag := range []string{"recent", "fresh", "todays"} { + repo := fmt.Sprintf("%v:%v", repoName, tag) + _, err := buildImage(repo, fmt.Sprintf(` + FROM busybox + ENTRYPOINT ["/bin/echo"] + ENV FOO foo + ENV BAR bar + CMD echo %s + `, repo), true) + c.Assert(err, checker.IsNil) + dockerCmd(c, "push", repo) + repos = append(repos, repo) + } + + // Clear local images store. + args := append([]string{"rmi"}, repos...) + dockerCmd(c, args...) + + // Re-pull individual tags, in parallel + results := make(chan error) + + for _, repo := range repos { + go func(repo string) { + _, _, err := runCommandWithOutput(exec.Command(dockerBinary, "pull", repo)) + results <- err + }(repo) + } + + // These checks are separate from the loop above because the check + // package is not goroutine-safe. + for range repos { + err := <-results + c.Assert(err, checker.IsNil, check.Commentf("concurrent pull failed with error: %v", err)) + } + + // Ensure all tags were pulled successfully + for _, repo := range repos { + dockerCmd(c, "inspect", repo) + out, _ := dockerCmd(c, "run", "--rm", repo) + c.Assert(strings.TrimSpace(out), checker.Equals, "/bin/sh -c echo "+repo) + } +} + +func (s *DockerRegistrySuite) TestConcurrentPullMultipleTags(c *check.C) { + testConcurrentPullMultipleTags(c) +} + +func (s *DockerSchema1RegistrySuite) TestConcurrentPullMultipleTags(c *check.C) { + testConcurrentPullMultipleTags(c) +} + +// testPullIDStability verifies that pushing an image and pulling it back +// preserves the image ID. +func testPullIDStability(c *check.C) { + derivedImage := privateRegistryURL + "/dockercli/id-stability" + baseImage := "busybox" + + _, err := buildImage(derivedImage, fmt.Sprintf(` + FROM %s + ENV derived true + ENV asdf true + RUN dd if=/dev/zero of=/file bs=1024 count=1024 + CMD echo %s + `, baseImage, derivedImage), true) + if err != nil { + c.Fatal(err) + } + + originalID, err := getIDByName(derivedImage) + if err != nil { + c.Fatalf("error inspecting: %v", err) + } + dockerCmd(c, "push", derivedImage) + + // Pull + out, _ := dockerCmd(c, "pull", derivedImage) + if strings.Contains(out, "Pull complete") { + c.Fatalf("repull redownloaded a layer: %s", out) + } + + derivedIDAfterPull, err := getIDByName(derivedImage) + if err != nil { + c.Fatalf("error inspecting: %v", err) + } + + if derivedIDAfterPull != originalID { + c.Fatal("image's ID unexpectedly changed after a repush/repull") + } + + // Make sure the image runs correctly + out, _ = dockerCmd(c, "run", "--rm", derivedImage) + if strings.TrimSpace(out) != derivedImage { + c.Fatalf("expected %s; got %s", derivedImage, out) + } + + // Confirm that repushing and repulling does not change the computed ID + dockerCmd(c, "push", derivedImage) + dockerCmd(c, "rmi", derivedImage) + dockerCmd(c, "pull", derivedImage) + + derivedIDAfterPull, err = getIDByName(derivedImage) + if err != nil { + c.Fatalf("error inspecting: %v", err) + } + + if derivedIDAfterPull != originalID { + c.Fatal("image's ID unexpectedly changed after a repush/repull") + } + if err != nil { + c.Fatalf("error inspecting: %v", err) + } + + // Make sure the image still runs + out, _ = dockerCmd(c, "run", "--rm", derivedImage) + if strings.TrimSpace(out) != derivedImage { + c.Fatalf("expected %s; got %s", derivedImage, out) + } +} + +func (s *DockerRegistrySuite) TestPullIDStability(c *check.C) { + testPullIDStability(c) +} + +func (s *DockerSchema1RegistrySuite) TestPullIDStability(c *check.C) { + testPullIDStability(c) +} + +// #21213 +func testPullNoLayers(c *check.C) { + repoName := fmt.Sprintf("%v/dockercli/scratch", privateRegistryURL) + + _, err := buildImage(repoName, ` + FROM scratch + ENV foo bar`, + true) + if err != nil { + c.Fatal(err) + } + + dockerCmd(c, "push", repoName) + dockerCmd(c, "rmi", repoName) + dockerCmd(c, "pull", repoName) +} + +func (s *DockerRegistrySuite) TestPullNoLayers(c *check.C) { + testPullNoLayers(c) +} + +func (s *DockerSchema1RegistrySuite) TestPullNoLayers(c *check.C) { + testPullNoLayers(c) +} + +func (s *DockerRegistrySuite) TestPullManifestList(c *check.C) { + testRequires(c, NotArm) + pushDigest, err := setupImage(c) + c.Assert(err, checker.IsNil, check.Commentf("error setting up image")) + + // Inject a manifest list into the registry + manifestList := &manifestlist.ManifestList{ + Versioned: manifest.Versioned{ + SchemaVersion: 2, + MediaType: manifestlist.MediaTypeManifestList, + }, + Manifests: []manifestlist.ManifestDescriptor{ + { + Descriptor: distribution.Descriptor{ + Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b", + Size: 3253, + MediaType: schema2.MediaTypeManifest, + }, + Platform: manifestlist.PlatformSpec{ + Architecture: "bogus_arch", + OS: "bogus_os", + }, + }, + { + Descriptor: distribution.Descriptor{ + Digest: pushDigest, + Size: 3253, + MediaType: schema2.MediaTypeManifest, + }, + Platform: manifestlist.PlatformSpec{ + Architecture: runtime.GOARCH, + OS: runtime.GOOS, + }, + }, + }, + } + + manifestListJSON, err := json.MarshalIndent(manifestList, "", " ") + c.Assert(err, checker.IsNil, check.Commentf("error marshalling manifest list")) + + manifestListDigest := digest.FromBytes(manifestListJSON) + hexDigest := manifestListDigest.Hex() + + registryV2Path := filepath.Join(s.reg.dir, "docker", "registry", "v2") + + // Write manifest list to blob store + blobDir := filepath.Join(registryV2Path, "blobs", "sha256", hexDigest[:2], hexDigest) + err = os.MkdirAll(blobDir, 0755) + c.Assert(err, checker.IsNil, check.Commentf("error creating blob dir")) + blobPath := filepath.Join(blobDir, "data") + err = ioutil.WriteFile(blobPath, []byte(manifestListJSON), 0644) + c.Assert(err, checker.IsNil, check.Commentf("error writing manifest list")) + + // Add to revision store + revisionDir := filepath.Join(registryV2Path, "repositories", remoteRepoName, "_manifests", "revisions", "sha256", hexDigest) + err = os.Mkdir(revisionDir, 0755) + c.Assert(err, checker.IsNil, check.Commentf("error creating revision dir")) + revisionPath := filepath.Join(revisionDir, "link") + err = ioutil.WriteFile(revisionPath, []byte(manifestListDigest.String()), 0644) + c.Assert(err, checker.IsNil, check.Commentf("error writing revision link")) + + // Update tag + tagPath := filepath.Join(registryV2Path, "repositories", remoteRepoName, "_manifests", "tags", "latest", "current", "link") + err = ioutil.WriteFile(tagPath, []byte(manifestListDigest.String()), 0644) + c.Assert(err, checker.IsNil, check.Commentf("error writing tag link")) + + // Verify that the image can be pulled through the manifest list. + out, _ := dockerCmd(c, "pull", repoName) + + // The pull output includes "Digest: ", so find that + matches := digestRegex.FindStringSubmatch(out) + c.Assert(matches, checker.HasLen, 2, check.Commentf("unable to parse digest from pull output: %s", out)) + pullDigest := matches[1] + + // Make sure the pushed and pull digests match + c.Assert(manifestListDigest.String(), checker.Equals, pullDigest) + + // Was the image actually created? + dockerCmd(c, "inspect", repoName) + + dockerCmd(c, "rmi", repoName) +} + +func (s *DockerRegistryAuthHtpasswdSuite) TestPullWithExternalAuth(c *check.C) { + osPath := os.Getenv("PATH") + defer os.Setenv("PATH", osPath) + + workingDir, err := os.Getwd() + c.Assert(err, checker.IsNil) + absolute, err := filepath.Abs(filepath.Join(workingDir, "fixtures", "auth")) + c.Assert(err, checker.IsNil) + testPath := fmt.Sprintf("%s%c%s", osPath, filepath.ListSeparator, absolute) + + os.Setenv("PATH", testPath) + + repoName := fmt.Sprintf("%v/dockercli/busybox:authtest", privateRegistryURL) + + tmp, err := ioutil.TempDir("", "integration-cli-") + c.Assert(err, checker.IsNil) + + externalAuthConfig := `{ "credsStore": "shell-test" }` + + configPath := filepath.Join(tmp, "config.json") + err = ioutil.WriteFile(configPath, []byte(externalAuthConfig), 0644) + c.Assert(err, checker.IsNil) + + dockerCmd(c, "--config", tmp, "login", "-u", s.reg.username, "-p", s.reg.password, privateRegistryURL) + + b, err := ioutil.ReadFile(configPath) + c.Assert(err, checker.IsNil) + c.Assert(string(b), checker.Not(checker.Contains), "\"auth\":") + + dockerCmd(c, "--config", tmp, "tag", "busybox", repoName) + dockerCmd(c, "--config", tmp, "push", repoName) + + dockerCmd(c, "--config", tmp, "pull", repoName) +} diff --git a/integration-cli/docker_cli_pull_test.go b/integration-cli/docker_cli_pull_test.go new file mode 100644 index 00000000..ac211d25 --- /dev/null +++ b/integration-cli/docker_cli_pull_test.go @@ -0,0 +1,265 @@ +package main + +import ( + "fmt" + "regexp" + "strings" + "sync" + "time" + + "github.com/docker/distribution/digest" + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +// TestPullFromCentralRegistry pulls an image from the central registry and verifies that the client +// prints all expected output. +func (s *DockerHubPullSuite) TestPullFromCentralRegistry(c *check.C) { + testRequires(c, DaemonIsLinux) + out := s.Cmd(c, "pull", "hello-world") + defer deleteImages("hello-world") + + c.Assert(out, checker.Contains, "Using default tag: latest", check.Commentf("expected the 'latest' tag to be automatically assumed")) + c.Assert(out, checker.Contains, "Pulling from library/hello-world", check.Commentf("expected the 'library/' prefix to be automatically assumed")) + c.Assert(out, checker.Contains, "Downloaded newer image for hello-world:latest") + + matches := regexp.MustCompile(`Digest: (.+)\n`).FindAllStringSubmatch(out, -1) + c.Assert(len(matches), checker.Equals, 1, check.Commentf("expected exactly one image digest in the output")) + c.Assert(len(matches[0]), checker.Equals, 2, check.Commentf("unexpected number of submatches for the digest")) + _, err := digest.ParseDigest(matches[0][1]) + c.Check(err, checker.IsNil, check.Commentf("invalid digest %q in output", matches[0][1])) + + // We should have a single entry in images. + img := strings.TrimSpace(s.Cmd(c, "images")) + splitImg := strings.Split(img, "\n") + c.Assert(splitImg, checker.HasLen, 2) + c.Assert(splitImg[1], checker.Matches, `hello-world\s+latest.*?`, check.Commentf("invalid output for `docker images` (expected image and tag name")) +} + +// TestPullNonExistingImage pulls non-existing images from the central registry, with different +// combinations of implicit tag and library prefix. +func (s *DockerHubPullSuite) TestPullNonExistingImage(c *check.C) { + testRequires(c, DaemonIsLinux) + + type entry struct { + Repo string + Alias string + } + + entries := []entry{ + {"library/asdfasdf", "asdfasdf:foobar"}, + {"library/asdfasdf", "library/asdfasdf:foobar"}, + {"library/asdfasdf", "asdfasdf"}, + {"library/asdfasdf", "asdfasdf:latest"}, + {"library/asdfasdf", "library/asdfasdf"}, + {"library/asdfasdf", "library/asdfasdf:latest"}, + } + + // The option field indicates "-a" or not. + type record struct { + e entry + option string + out string + err error + } + + // Execute 'docker pull' in parallel, pass results (out, err) and + // necessary information ("-a" or not, and the image name) to channel. + var group sync.WaitGroup + recordChan := make(chan record, len(entries)*2) + for _, e := range entries { + group.Add(1) + go func(e entry) { + defer group.Done() + out, err := s.CmdWithError("pull", e.Alias) + recordChan <- record{e, "", out, err} + }(e) + if !strings.ContainsRune(e.Alias, ':') { + // pull -a on a nonexistent registry should fall back as well + group.Add(1) + go func(e entry) { + defer group.Done() + out, err := s.CmdWithError("pull", "-a", e.Alias) + recordChan <- record{e, "-a", out, err} + }(e) + } + } + + // Wait for completion + group.Wait() + close(recordChan) + + // Process the results (out, err). + for record := range recordChan { + if len(record.option) == 0 { + c.Assert(record.err, checker.NotNil, check.Commentf("expected non-zero exit status when pulling non-existing image: %s", record.out)) + // Hub returns 401 rather than 404 for nonexistent repos over + // the v2 protocol - but we should end up falling back to v1, + // which does return a 404. + c.Assert(record.out, checker.Contains, fmt.Sprintf("Error: image %s not found", record.e.Repo), check.Commentf("expected image not found error messages")) + } else { + // pull -a on a nonexistent registry should fall back as well + c.Assert(record.err, checker.NotNil, check.Commentf("expected non-zero exit status when pulling non-existing image: %s", record.out)) + c.Assert(record.out, checker.Contains, fmt.Sprintf("Error: image %s not found", record.e.Repo), check.Commentf("expected image not found error messages")) + c.Assert(record.out, checker.Not(checker.Contains), "unauthorized", check.Commentf(`message should not contain "unauthorized"`)) + } + } + +} + +// TestPullFromCentralRegistryImplicitRefParts pulls an image from the central registry and verifies +// that pulling the same image with different combinations of implicit elements of the the image +// reference (tag, repository, central registry url, ...) doesn't trigger a new pull nor leads to +// multiple images. +func (s *DockerHubPullSuite) TestPullFromCentralRegistryImplicitRefParts(c *check.C) { + testRequires(c, DaemonIsLinux) + + // Pull hello-world from v2 + pullFromV2 := func(ref string) (int, string) { + out := s.Cmd(c, "pull", "hello-world") + v1Retries := 0 + for strings.Contains(out, "this image was pulled from a legacy registry") { + // Some network errors may cause fallbacks to the v1 + // protocol, which would violate the test's assumption + // that it will get the same images. To make the test + // more robust against these network glitches, allow a + // few retries if we end up with a v1 pull. + + if v1Retries > 2 { + c.Fatalf("too many v1 fallback incidents when pulling %s", ref) + } + + s.Cmd(c, "rmi", ref) + out = s.Cmd(c, "pull", ref) + + v1Retries++ + } + + return v1Retries, out + } + + pullFromV2("hello-world") + defer deleteImages("hello-world") + + s.Cmd(c, "tag", "hello-world", "hello-world-backup") + + for _, ref := range []string{ + "hello-world", + "hello-world:latest", + "library/hello-world", + "library/hello-world:latest", + "docker.io/library/hello-world", + "index.docker.io/library/hello-world", + } { + var out string + for { + var v1Retries int + v1Retries, out = pullFromV2(ref) + + // Keep repeating the test case until we don't hit a v1 + // fallback case. We won't get the right "Image is up + // to date" message if the local image was replaced + // with one pulled from v1. + if v1Retries == 0 { + break + } + s.Cmd(c, "rmi", ref) + s.Cmd(c, "tag", "hello-world-backup", "hello-world") + } + c.Assert(out, checker.Contains, "Image is up to date for hello-world:latest") + } + + s.Cmd(c, "rmi", "hello-world-backup") + + // We should have a single entry in images. + img := strings.TrimSpace(s.Cmd(c, "images")) + splitImg := strings.Split(img, "\n") + c.Assert(splitImg, checker.HasLen, 2) + c.Assert(splitImg[1], checker.Matches, `hello-world\s+latest.*?`, check.Commentf("invalid output for `docker images` (expected image and tag name")) +} + +// TestPullScratchNotAllowed verifies that pulling 'scratch' is rejected. +func (s *DockerHubPullSuite) TestPullScratchNotAllowed(c *check.C) { + testRequires(c, DaemonIsLinux) + out, err := s.CmdWithError("pull", "scratch") + c.Assert(err, checker.NotNil, check.Commentf("expected pull of scratch to fail")) + c.Assert(out, checker.Contains, "'scratch' is a reserved name") + c.Assert(out, checker.Not(checker.Contains), "Pulling repository scratch") +} + +// TestPullAllTagsFromCentralRegistry pulls using `all-tags` for a given image and verifies that it +// results in more images than a naked pull. +func (s *DockerHubPullSuite) TestPullAllTagsFromCentralRegistry(c *check.C) { + testRequires(c, DaemonIsLinux) + s.Cmd(c, "pull", "busybox") + outImageCmd := s.Cmd(c, "images", "busybox") + splitOutImageCmd := strings.Split(strings.TrimSpace(outImageCmd), "\n") + c.Assert(splitOutImageCmd, checker.HasLen, 2) + + s.Cmd(c, "pull", "--all-tags=true", "busybox") + outImageAllTagCmd := s.Cmd(c, "images", "busybox") + linesCount := strings.Count(outImageAllTagCmd, "\n") + c.Assert(linesCount, checker.GreaterThan, 2, check.Commentf("pulling all tags should provide more than two images, got %s", outImageAllTagCmd)) + + // Verify that the line for 'busybox:latest' is left unchanged. + var latestLine string + for _, line := range strings.Split(outImageAllTagCmd, "\n") { + if strings.HasPrefix(line, "busybox") && strings.Contains(line, "latest") { + latestLine = line + break + } + } + c.Assert(latestLine, checker.Not(checker.Equals), "", check.Commentf("no entry for busybox:latest found after pulling all tags")) + splitLatest := strings.Fields(latestLine) + splitCurrent := strings.Fields(splitOutImageCmd[1]) + + // Clear relative creation times, since these can easily change between + // two invocations of "docker images". Without this, the test can fail + // like this: + // ... obtained []string = []string{"busybox", "latest", "d9551b4026f0", "27", "minutes", "ago", "1.113", "MB"} + // ... expected []string = []string{"busybox", "latest", "d9551b4026f0", "26", "minutes", "ago", "1.113", "MB"} + splitLatest[3] = "" + splitLatest[4] = "" + splitLatest[5] = "" + splitCurrent[3] = "" + splitCurrent[4] = "" + splitCurrent[5] = "" + + c.Assert(splitLatest, checker.DeepEquals, splitCurrent, check.Commentf("busybox:latest was changed after pulling all tags")) +} + +// TestPullClientDisconnect kills the client during a pull operation and verifies that the operation +// gets cancelled. +// +// Ref: docker/docker#15589 +func (s *DockerHubPullSuite) TestPullClientDisconnect(c *check.C) { + testRequires(c, DaemonIsLinux) + repoName := "hello-world:latest" + + pullCmd := s.MakeCmd("pull", repoName) + stdout, err := pullCmd.StdoutPipe() + c.Assert(err, checker.IsNil) + err = pullCmd.Start() + c.Assert(err, checker.IsNil) + + // Cancel as soon as we get some output. + buf := make([]byte, 10) + _, err = stdout.Read(buf) + c.Assert(err, checker.IsNil) + + err = pullCmd.Process.Kill() + c.Assert(err, checker.IsNil) + + time.Sleep(2 * time.Second) + _, err = s.CmdWithError("inspect", repoName) + c.Assert(err, checker.NotNil, check.Commentf("image was pulled after client disconnected")) +} + +func (s *DockerRegistryAuthHtpasswdSuite) TestPullNoCredentialsNotFound(c *check.C) { + // we don't care about the actual image, we just want to see image not found + // because that means v2 call returned 401 and we fell back to v1 which usually + // gives a 404 (in this case the test registry doesn't handle v1 at all) + out, _, err := dockerCmdWithError("pull", privateRegistryURL+"/busybox") + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "Error: image busybox not found") +} diff --git a/integration-cli/docker_cli_pull_trusted_test.go b/integration-cli/docker_cli_pull_trusted_test.go new file mode 100644 index 00000000..6bc38e69 --- /dev/null +++ b/integration-cli/docker_cli_pull_trusted_test.go @@ -0,0 +1,365 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os/exec" + "strings" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerTrustSuite) TestTrustedPull(c *check.C) { + repoName := s.setupTrustedImage(c, "trusted-pull") + + // Try pull + pullCmd := exec.Command(dockerBinary, "pull", repoName) + s.trustedCmd(pullCmd) + out, _, err := runCommandWithOutput(pullCmd) + + c.Assert(err, check.IsNil, check.Commentf(out)) + c.Assert(string(out), checker.Contains, "Tagging", check.Commentf(out)) + + dockerCmd(c, "rmi", repoName) + // Try untrusted pull to ensure we pushed the tag to the registry + pullCmd = exec.Command(dockerBinary, "pull", "--disable-content-trust=true", repoName) + s.trustedCmd(pullCmd) + out, _, err = runCommandWithOutput(pullCmd) + c.Assert(err, check.IsNil, check.Commentf(out)) + c.Assert(string(out), checker.Contains, "Status: Downloaded", check.Commentf(out)) + +} + +func (s *DockerTrustSuite) TestTrustedIsolatedPull(c *check.C) { + repoName := s.setupTrustedImage(c, "trusted-isolated-pull") + + // Try pull (run from isolated directory without trust information) + pullCmd := exec.Command(dockerBinary, "--config", "/tmp/docker-isolated", "pull", repoName) + s.trustedCmd(pullCmd) + out, _, err := runCommandWithOutput(pullCmd) + + c.Assert(err, check.IsNil, check.Commentf(out)) + c.Assert(string(out), checker.Contains, "Tagging", check.Commentf(string(out))) + + dockerCmd(c, "rmi", repoName) +} + +func (s *DockerTrustSuite) TestUntrustedPull(c *check.C) { + repoName := fmt.Sprintf("%v/dockercliuntrusted/pulltest:latest", privateRegistryURL) + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", repoName) + dockerCmd(c, "push", repoName) + dockerCmd(c, "rmi", repoName) + + // Try trusted pull on untrusted tag + pullCmd := exec.Command(dockerBinary, "pull", repoName) + s.trustedCmd(pullCmd) + out, _, err := runCommandWithOutput(pullCmd) + + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(string(out), checker.Contains, "Error: remote trust data does not exist", check.Commentf(out)) +} + +func (s *DockerTrustSuite) TestPullWhenCertExpired(c *check.C) { + c.Skip("Currently changes system time, causing instability") + repoName := s.setupTrustedImage(c, "trusted-cert-expired") + + // Certificates have 10 years of expiration + elevenYearsFromNow := time.Now().Add(time.Hour * 24 * 365 * 11) + + runAtDifferentDate(elevenYearsFromNow, func() { + // Try pull + pullCmd := exec.Command(dockerBinary, "pull", repoName) + s.trustedCmd(pullCmd) + out, _, err := runCommandWithOutput(pullCmd) + + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(string(out), checker.Contains, "could not validate the path to a trusted root", check.Commentf(out)) + }) + + runAtDifferentDate(elevenYearsFromNow, func() { + // Try pull + pullCmd := exec.Command(dockerBinary, "pull", "--disable-content-trust", repoName) + s.trustedCmd(pullCmd) + out, _, err := runCommandWithOutput(pullCmd) + + c.Assert(err, check.IsNil, check.Commentf(out)) + c.Assert(string(out), checker.Contains, "Status: Downloaded", check.Commentf(out)) + }) +} + +func (s *DockerTrustSuite) TestTrustedPullFromBadTrustServer(c *check.C) { + repoName := fmt.Sprintf("%v/dockerclievilpull/trusted:latest", privateRegistryURL) + evilLocalConfigDir, err := ioutil.TempDir("", "evil-local-config-dir") + if err != nil { + c.Fatalf("Failed to create local temp dir") + } + + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", repoName) + + pushCmd := exec.Command(dockerBinary, "push", repoName) + s.trustedCmd(pushCmd) + out, _, err := runCommandWithOutput(pushCmd) + + c.Assert(err, check.IsNil, check.Commentf(out)) + c.Assert(string(out), checker.Contains, "Signing and pushing trust metadata", check.Commentf(out)) + dockerCmd(c, "rmi", repoName) + + // Try pull + pullCmd := exec.Command(dockerBinary, "pull", repoName) + s.trustedCmd(pullCmd) + out, _, err = runCommandWithOutput(pullCmd) + + c.Assert(err, check.IsNil, check.Commentf(out)) + c.Assert(string(out), checker.Contains, "Tagging", check.Commentf(out)) + dockerCmd(c, "rmi", repoName) + + // Kill the notary server, start a new "evil" one. + s.not.Close() + s.not, err = newTestNotary(c) + + c.Assert(err, check.IsNil, check.Commentf("Restarting notary server failed.")) + + // In order to make an evil server, lets re-init a client (with a different trust dir) and push new data. + // tag an image and upload it to the private registry + dockerCmd(c, "--config", evilLocalConfigDir, "tag", "busybox", repoName) + + // Push up to the new server + pushCmd = exec.Command(dockerBinary, "--config", evilLocalConfigDir, "push", repoName) + s.trustedCmd(pushCmd) + out, _, err = runCommandWithOutput(pushCmd) + + c.Assert(err, check.IsNil, check.Commentf(out)) + c.Assert(string(out), checker.Contains, "Signing and pushing trust metadata", check.Commentf(out)) + + // Now, try pulling with the original client from this new trust server. This should fall back to cached metadata. + pullCmd = exec.Command(dockerBinary, "pull", repoName) + s.trustedCmd(pullCmd) + out, _, err = runCommandWithOutput(pullCmd) + if err != nil { + c.Fatalf("Error falling back to cached trust data: %s\n%s", err, out) + } + if !strings.Contains(string(out), "Error while downloading remote metadata, using cached timestamp") { + c.Fatalf("Missing expected output on trusted pull:\n%s", out) + } +} + +func (s *DockerTrustSuite) TestTrustedPullWithExpiredSnapshot(c *check.C) { + c.Skip("Currently changes system time, causing instability") + repoName := fmt.Sprintf("%v/dockercliexpiredtimestamppull/trusted:latest", privateRegistryURL) + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", repoName) + + // Push with default passphrases + pushCmd := exec.Command(dockerBinary, "push", repoName) + s.trustedCmd(pushCmd) + out, _, err := runCommandWithOutput(pushCmd) + + c.Assert(err, check.IsNil, check.Commentf(out)) + c.Assert(string(out), checker.Contains, "Signing and pushing trust metadata", check.Commentf(out)) + + dockerCmd(c, "rmi", repoName) + + // Snapshots last for three years. This should be expired + fourYearsLater := time.Now().Add(time.Hour * 24 * 365 * 4) + + runAtDifferentDate(fourYearsLater, func() { + // Try pull + pullCmd := exec.Command(dockerBinary, "pull", repoName) + s.trustedCmd(pullCmd) + out, _, err = runCommandWithOutput(pullCmd) + + c.Assert(err, check.NotNil, check.Commentf("Missing expected error running trusted pull with expired snapshots")) + c.Assert(string(out), checker.Contains, "repository out-of-date", check.Commentf(out)) + }) +} + +func (s *DockerTrustSuite) TestTrustedOfflinePull(c *check.C) { + repoName := s.setupTrustedImage(c, "trusted-offline-pull") + + pullCmd := exec.Command(dockerBinary, "pull", repoName) + s.trustedCmdWithServer(pullCmd, "https://invalidnotaryserver") + out, _, err := runCommandWithOutput(pullCmd) + + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(string(out), checker.Contains, "error contacting notary server", check.Commentf(out)) + // Do valid trusted pull to warm cache + pullCmd = exec.Command(dockerBinary, "pull", repoName) + s.trustedCmd(pullCmd) + out, _, err = runCommandWithOutput(pullCmd) + + c.Assert(err, check.IsNil, check.Commentf(out)) + c.Assert(string(out), checker.Contains, "Tagging", check.Commentf(out)) + + dockerCmd(c, "rmi", repoName) + + // Try pull again with invalid notary server, should use cache + pullCmd = exec.Command(dockerBinary, "pull", repoName) + s.trustedCmdWithServer(pullCmd, "https://invalidnotaryserver") + out, _, err = runCommandWithOutput(pullCmd) + + c.Assert(err, check.IsNil, check.Commentf(out)) + c.Assert(string(out), checker.Contains, "Tagging", check.Commentf(out)) +} + +func (s *DockerTrustSuite) TestTrustedPullDelete(c *check.C) { + repoName := fmt.Sprintf("%v/dockercli/%s:latest", privateRegistryURL, "trusted-pull-delete") + // tag the image and upload it to the private registry + _, err := buildImage(repoName, ` + FROM busybox + CMD echo trustedpulldelete + `, true) + + pushCmd := exec.Command(dockerBinary, "push", repoName) + s.trustedCmd(pushCmd) + out, _, err := runCommandWithOutput(pushCmd) + if err != nil { + c.Fatalf("Error running trusted push: %s\n%s", err, out) + } + if !strings.Contains(string(out), "Signing and pushing trust metadata") { + c.Fatalf("Missing expected output on trusted push:\n%s", out) + } + + if out, status := dockerCmd(c, "rmi", repoName); status != 0 { + c.Fatalf("Error removing image %q\n%s", repoName, out) + } + + // Try pull + pullCmd := exec.Command(dockerBinary, "pull", repoName) + s.trustedCmd(pullCmd) + out, _, err = runCommandWithOutput(pullCmd) + + c.Assert(err, check.IsNil, check.Commentf(out)) + + matches := digestRegex.FindStringSubmatch(out) + c.Assert(matches, checker.HasLen, 2, check.Commentf("unable to parse digest from pull output: %s", out)) + pullDigest := matches[1] + + imageID := inspectField(c, repoName, "Id") + + imageByDigest := repoName + "@" + pullDigest + byDigestID := inspectField(c, imageByDigest, "Id") + + c.Assert(byDigestID, checker.Equals, imageID) + + // rmi of tag should also remove the digest reference + dockerCmd(c, "rmi", repoName) + + _, err = inspectFieldWithError(imageByDigest, "Id") + c.Assert(err, checker.NotNil, check.Commentf("digest reference should have been removed")) + + _, err = inspectFieldWithError(imageID, "Id") + c.Assert(err, checker.NotNil, check.Commentf("image should have been deleted")) +} + +func (s *DockerTrustSuite) TestTrustedPullReadsFromReleasesRole(c *check.C) { + testRequires(c, NotaryHosting) + repoName := fmt.Sprintf("%v/dockerclireleasesdelegationpulling/trusted", privateRegistryURL) + targetName := fmt.Sprintf("%s:latest", repoName) + + // Push with targets first, initializing the repo + dockerCmd(c, "tag", "busybox", targetName) + pushCmd := exec.Command(dockerBinary, "push", targetName) + s.trustedCmd(pushCmd) + out, _, err := runCommandWithOutput(pushCmd) + c.Assert(err, check.IsNil, check.Commentf(out)) + s.assertTargetInRoles(c, repoName, "latest", "targets") + + // Try pull, check we retrieve from targets role + pullCmd := exec.Command(dockerBinary, "-D", "pull", repoName) + s.trustedCmd(pullCmd) + out, _, err = runCommandWithOutput(pullCmd) + c.Assert(err, check.IsNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "retrieving target for targets role") + + // Now we'll create the releases role, and try pushing and pulling + s.notaryCreateDelegation(c, repoName, "targets/releases", s.not.keys[0].Public) + s.notaryImportKey(c, repoName, "targets/releases", s.not.keys[0].Private) + s.notaryPublish(c, repoName) + + // try a pull, check that we can still pull because we can still read the + // old tag in the targets role + pullCmd = exec.Command(dockerBinary, "-D", "pull", repoName) + s.trustedCmd(pullCmd) + out, _, err = runCommandWithOutput(pullCmd) + c.Assert(err, check.IsNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "retrieving target for targets role") + + // try a pull -a, check that it succeeds because we can still pull from the + // targets role + pullCmd = exec.Command(dockerBinary, "-D", "pull", "-a", repoName) + s.trustedCmd(pullCmd) + out, _, err = runCommandWithOutput(pullCmd) + c.Assert(err, check.IsNil, check.Commentf(out)) + + // Push, should sign with targets/releases + dockerCmd(c, "tag", "busybox", targetName) + pushCmd = exec.Command(dockerBinary, "push", targetName) + s.trustedCmd(pushCmd) + out, _, err = runCommandWithOutput(pushCmd) + s.assertTargetInRoles(c, repoName, "latest", "targets", "targets/releases") + + // Try pull, check we retrieve from targets/releases role + pullCmd = exec.Command(dockerBinary, "-D", "pull", repoName) + s.trustedCmd(pullCmd) + out, _, err = runCommandWithOutput(pullCmd) + c.Assert(out, checker.Contains, "retrieving target for targets/releases role") + + // Create another delegation that we'll sign with + s.notaryCreateDelegation(c, repoName, "targets/other", s.not.keys[1].Public) + s.notaryImportKey(c, repoName, "targets/other", s.not.keys[1].Private) + s.notaryPublish(c, repoName) + + dockerCmd(c, "tag", "busybox", targetName) + pushCmd = exec.Command(dockerBinary, "push", targetName) + s.trustedCmd(pushCmd) + out, _, err = runCommandWithOutput(pushCmd) + s.assertTargetInRoles(c, repoName, "latest", "targets", "targets/releases", "targets/other") + + // Try pull, check we retrieve from targets/releases role + pullCmd = exec.Command(dockerBinary, "-D", "pull", repoName) + s.trustedCmd(pullCmd) + out, _, err = runCommandWithOutput(pullCmd) + c.Assert(out, checker.Contains, "retrieving target for targets/releases role") +} + +func (s *DockerTrustSuite) TestTrustedPullIgnoresOtherDelegationRoles(c *check.C) { + testRequires(c, NotaryHosting) + repoName := fmt.Sprintf("%v/dockerclipullotherdelegation/trusted", privateRegistryURL) + targetName := fmt.Sprintf("%s:latest", repoName) + + // We'll create a repo first with a non-release delegation role, so that when we + // push we'll sign it into the delegation role + s.notaryInitRepo(c, repoName) + s.notaryCreateDelegation(c, repoName, "targets/other", s.not.keys[0].Public) + s.notaryImportKey(c, repoName, "targets/other", s.not.keys[0].Private) + s.notaryPublish(c, repoName) + + // Push should write to the delegation role, not targets + dockerCmd(c, "tag", "busybox", targetName) + pushCmd := exec.Command(dockerBinary, "push", targetName) + s.trustedCmd(pushCmd) + out, _, err := runCommandWithOutput(pushCmd) + c.Assert(err, check.IsNil, check.Commentf(out)) + s.assertTargetInRoles(c, repoName, "latest", "targets/other") + s.assertTargetNotInRoles(c, repoName, "latest", "targets") + + // Try pull - we should fail, since pull will only pull from the targets/releases + // role or the targets role + pullCmd := exec.Command(dockerBinary, "-D", "pull", repoName) + s.trustedCmd(pullCmd) + out, _, err = runCommandWithOutput(pullCmd) + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "No trust data for") + + // try a pull -a: we should fail since pull will only pull from the targets/releases + // role or the targets role + pullCmd = exec.Command(dockerBinary, "-D", "pull", "-a", repoName) + s.trustedCmd(pullCmd) + out, _, err = runCommandWithOutput(pullCmd) + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "No trusted tags for") +} diff --git a/integration-cli/docker_cli_push_test.go b/integration-cli/docker_cli_push_test.go new file mode 100644 index 00000000..6b3d8232 --- /dev/null +++ b/integration-cli/docker_cli_push_test.go @@ -0,0 +1,728 @@ +package main + +import ( + "archive/tar" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/docker/distribution/digest" + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +// Pushing an image to a private registry. +func testPushBusyboxImage(c *check.C) { + repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL) + // tag the image to upload it to the private registry + dockerCmd(c, "tag", "busybox", repoName) + // push the image to the registry + dockerCmd(c, "push", repoName) +} + +func (s *DockerRegistrySuite) TestPushBusyboxImage(c *check.C) { + testPushBusyboxImage(c) +} + +func (s *DockerSchema1RegistrySuite) TestPushBusyboxImage(c *check.C) { + testPushBusyboxImage(c) +} + +// pushing an image without a prefix should throw an error +func (s *DockerSuite) TestPushUnprefixedRepo(c *check.C) { + out, _, err := dockerCmdWithError("push", "busybox") + c.Assert(err, check.NotNil, check.Commentf("pushing an unprefixed repo didn't result in a non-zero exit status: %s", out)) +} + +func testPushUntagged(c *check.C) { + repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL) + expected := "Repository does not exist" + + out, _, err := dockerCmdWithError("push", repoName) + c.Assert(err, check.NotNil, check.Commentf("pushing the image to the private registry should have failed: output %q", out)) + c.Assert(out, checker.Contains, expected, check.Commentf("pushing the image failed")) +} + +func (s *DockerRegistrySuite) TestPushUntagged(c *check.C) { + testPushUntagged(c) +} + +func (s *DockerSchema1RegistrySuite) TestPushUntagged(c *check.C) { + testPushUntagged(c) +} + +func testPushBadTag(c *check.C) { + repoName := fmt.Sprintf("%v/dockercli/busybox:latest", privateRegistryURL) + expected := "does not exist" + + out, _, err := dockerCmdWithError("push", repoName) + c.Assert(err, check.NotNil, check.Commentf("pushing the image to the private registry should have failed: output %q", out)) + c.Assert(out, checker.Contains, expected, check.Commentf("pushing the image failed")) +} + +func (s *DockerRegistrySuite) TestPushBadTag(c *check.C) { + testPushBadTag(c) +} + +func (s *DockerSchema1RegistrySuite) TestPushBadTag(c *check.C) { + testPushBadTag(c) +} + +func testPushMultipleTags(c *check.C) { + repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL) + repoTag1 := fmt.Sprintf("%v/dockercli/busybox:t1", privateRegistryURL) + repoTag2 := fmt.Sprintf("%v/dockercli/busybox:t2", privateRegistryURL) + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", repoTag1) + + dockerCmd(c, "tag", "busybox", repoTag2) + + dockerCmd(c, "push", repoName) + + // Ensure layer list is equivalent for repoTag1 and repoTag2 + out1, _ := dockerCmd(c, "pull", repoTag1) + + imageAlreadyExists := ": Image already exists" + var out1Lines []string + for _, outputLine := range strings.Split(out1, "\n") { + if strings.Contains(outputLine, imageAlreadyExists) { + out1Lines = append(out1Lines, outputLine) + } + } + + out2, _ := dockerCmd(c, "pull", repoTag2) + + var out2Lines []string + for _, outputLine := range strings.Split(out2, "\n") { + if strings.Contains(outputLine, imageAlreadyExists) { + out1Lines = append(out1Lines, outputLine) + } + } + c.Assert(out2Lines, checker.HasLen, len(out1Lines)) + + for i := range out1Lines { + c.Assert(out1Lines[i], checker.Equals, out2Lines[i]) + } +} + +func (s *DockerRegistrySuite) TestPushMultipleTags(c *check.C) { + testPushMultipleTags(c) +} + +func (s *DockerSchema1RegistrySuite) TestPushMultipleTags(c *check.C) { + testPushMultipleTags(c) +} + +func testPushEmptyLayer(c *check.C) { + repoName := fmt.Sprintf("%v/dockercli/emptylayer", privateRegistryURL) + emptyTarball, err := ioutil.TempFile("", "empty_tarball") + c.Assert(err, check.IsNil, check.Commentf("Unable to create test file")) + + tw := tar.NewWriter(emptyTarball) + err = tw.Close() + c.Assert(err, check.IsNil, check.Commentf("Error creating empty tarball")) + + freader, err := os.Open(emptyTarball.Name()) + c.Assert(err, check.IsNil, check.Commentf("Could not open test tarball")) + + importCmd := exec.Command(dockerBinary, "import", "-", repoName) + importCmd.Stdin = freader + out, _, err := runCommandWithOutput(importCmd) + c.Assert(err, check.IsNil, check.Commentf("import failed: %q", out)) + + // Now verify we can push it + out, _, err = dockerCmdWithError("push", repoName) + c.Assert(err, check.IsNil, check.Commentf("pushing the image to the private registry has failed: %s", out)) +} + +func (s *DockerRegistrySuite) TestPushEmptyLayer(c *check.C) { + testPushEmptyLayer(c) +} + +func (s *DockerSchema1RegistrySuite) TestPushEmptyLayer(c *check.C) { + testPushEmptyLayer(c) +} + +// testConcurrentPush pushes multiple tags to the same repo +// concurrently. +func testConcurrentPush(c *check.C) { + repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL) + + repos := []string{} + for _, tag := range []string{"push1", "push2", "push3"} { + repo := fmt.Sprintf("%v:%v", repoName, tag) + _, err := buildImage(repo, fmt.Sprintf(` + FROM busybox + ENTRYPOINT ["/bin/echo"] + ENV FOO foo + ENV BAR bar + CMD echo %s +`, repo), true) + c.Assert(err, checker.IsNil) + repos = append(repos, repo) + } + + // Push tags, in parallel + results := make(chan error) + + for _, repo := range repos { + go func(repo string) { + _, _, err := runCommandWithOutput(exec.Command(dockerBinary, "push", repo)) + results <- err + }(repo) + } + + for range repos { + err := <-results + c.Assert(err, checker.IsNil, check.Commentf("concurrent push failed with error: %v", err)) + } + + // Clear local images store. + args := append([]string{"rmi"}, repos...) + dockerCmd(c, args...) + + // Re-pull and run individual tags, to make sure pushes succeeded + for _, repo := range repos { + dockerCmd(c, "pull", repo) + dockerCmd(c, "inspect", repo) + out, _ := dockerCmd(c, "run", "--rm", repo) + c.Assert(strings.TrimSpace(out), checker.Equals, "/bin/sh -c echo "+repo) + } +} + +func (s *DockerRegistrySuite) TestConcurrentPush(c *check.C) { + testConcurrentPush(c) +} + +func (s *DockerSchema1RegistrySuite) TestConcurrentPush(c *check.C) { + testConcurrentPush(c) +} + +func (s *DockerRegistrySuite) TestCrossRepositoryLayerPush(c *check.C) { + sourceRepoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL) + // tag the image to upload it to the private registry + dockerCmd(c, "tag", "busybox", sourceRepoName) + // push the image to the registry + out1, _, err := dockerCmdWithError("push", sourceRepoName) + c.Assert(err, check.IsNil, check.Commentf("pushing the image to the private registry has failed: %s", out1)) + // ensure that none of the layers were mounted from another repository during push + c.Assert(strings.Contains(out1, "Mounted from"), check.Equals, false) + + digest1 := digest.DigestRegexp.FindString(out1) + c.Assert(len(digest1), checker.GreaterThan, 0, check.Commentf("no digest found for pushed manifest")) + + destRepoName := fmt.Sprintf("%v/dockercli/crossrepopush", privateRegistryURL) + // retag the image to upload the same layers to another repo in the same registry + dockerCmd(c, "tag", "busybox", destRepoName) + // push the image to the registry + out2, _, err := dockerCmdWithError("push", destRepoName) + c.Assert(err, check.IsNil, check.Commentf("pushing the image to the private registry has failed: %s", out2)) + // ensure that layers were mounted from the first repo during push + c.Assert(strings.Contains(out2, "Mounted from dockercli/busybox"), check.Equals, true) + + digest2 := digest.DigestRegexp.FindString(out2) + c.Assert(len(digest2), checker.GreaterThan, 0, check.Commentf("no digest found for pushed manifest")) + c.Assert(digest1, check.Equals, digest2) + + // ensure that we can pull and run the cross-repo-pushed repository + dockerCmd(c, "rmi", destRepoName) + dockerCmd(c, "pull", destRepoName) + out3, _ := dockerCmd(c, "run", destRepoName, "echo", "-n", "hello world") + c.Assert(out3, check.Equals, "hello world") +} + +func (s *DockerSchema1RegistrySuite) TestCrossRepositoryLayerPushNotSupported(c *check.C) { + sourceRepoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL) + // tag the image to upload it to the private registry + dockerCmd(c, "tag", "busybox", sourceRepoName) + // push the image to the registry + out1, _, err := dockerCmdWithError("push", sourceRepoName) + c.Assert(err, check.IsNil, check.Commentf("pushing the image to the private registry has failed: %s", out1)) + // ensure that none of the layers were mounted from another repository during push + c.Assert(strings.Contains(out1, "Mounted from"), check.Equals, false) + + digest1 := digest.DigestRegexp.FindString(out1) + c.Assert(len(digest1), checker.GreaterThan, 0, check.Commentf("no digest found for pushed manifest")) + + destRepoName := fmt.Sprintf("%v/dockercli/crossrepopush", privateRegistryURL) + // retag the image to upload the same layers to another repo in the same registry + dockerCmd(c, "tag", "busybox", destRepoName) + // push the image to the registry + out2, _, err := dockerCmdWithError("push", destRepoName) + c.Assert(err, check.IsNil, check.Commentf("pushing the image to the private registry has failed: %s", out2)) + // schema1 registry should not support cross-repo layer mounts, so ensure that this does not happen + c.Assert(strings.Contains(out2, "Mounted from"), check.Equals, false) + + digest2 := digest.DigestRegexp.FindString(out2) + c.Assert(len(digest2), checker.GreaterThan, 0, check.Commentf("no digest found for pushed manifest")) + c.Assert(digest1, check.Equals, digest2) + + // ensure that we can pull and run the second pushed repository + dockerCmd(c, "rmi", destRepoName) + dockerCmd(c, "pull", destRepoName) + out3, _ := dockerCmd(c, "run", destRepoName, "echo", "-n", "hello world") + c.Assert(out3, check.Equals, "hello world") +} + +func (s *DockerTrustSuite) TestTrustedPush(c *check.C) { + repoName := fmt.Sprintf("%v/dockerclitrusted/pushtest:latest", privateRegistryURL) + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", repoName) + + pushCmd := exec.Command(dockerBinary, "push", repoName) + s.trustedCmd(pushCmd) + out, _, err := runCommandWithOutput(pushCmd) + c.Assert(err, check.IsNil, check.Commentf("Error running trusted push: %s\n%s", err, out)) + c.Assert(out, checker.Contains, "Signing and pushing trust metadata", check.Commentf("Missing expected output on trusted push")) + + // Try pull after push + pullCmd := exec.Command(dockerBinary, "pull", repoName) + s.trustedCmd(pullCmd) + out, _, err = runCommandWithOutput(pullCmd) + c.Assert(err, check.IsNil, check.Commentf(out)) + c.Assert(string(out), checker.Contains, "Status: Downloaded", check.Commentf(out)) + + // Assert that we rotated the snapshot key to the server by checking our local keystore + contents, err := ioutil.ReadDir(filepath.Join(cliconfig.ConfigDir(), "trust/private/tuf_keys", privateRegistryURL, "dockerclitrusted/pushtest")) + c.Assert(err, check.IsNil, check.Commentf("Unable to read local tuf key files")) + // Check that we only have 1 key (targets key) + c.Assert(contents, checker.HasLen, 1) +} + +func (s *DockerTrustSuite) TestTrustedPushWithEnvPasswords(c *check.C) { + repoName := fmt.Sprintf("%v/dockerclienv/trusted:latest", privateRegistryURL) + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", repoName) + + pushCmd := exec.Command(dockerBinary, "push", repoName) + s.trustedCmdWithPassphrases(pushCmd, "12345678", "12345678") + out, _, err := runCommandWithOutput(pushCmd) + c.Assert(err, check.IsNil, check.Commentf("Error running trusted push: %s\n%s", err, out)) + c.Assert(out, checker.Contains, "Signing and pushing trust metadata", check.Commentf("Missing expected output on trusted push")) + + // Try pull after push + pullCmd := exec.Command(dockerBinary, "pull", repoName) + s.trustedCmd(pullCmd) + out, _, err = runCommandWithOutput(pullCmd) + c.Assert(err, check.IsNil, check.Commentf(out)) + c.Assert(string(out), checker.Contains, "Status: Downloaded", check.Commentf(out)) +} + +// This test ensures backwards compatibility with old ENV variables. Should be +// deprecated by 1.10 +func (s *DockerTrustSuite) TestTrustedPushWithDeprecatedEnvPasswords(c *check.C) { + repoName := fmt.Sprintf("%v/dockercli/trusteddeprecated:latest", privateRegistryURL) + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", repoName) + + pushCmd := exec.Command(dockerBinary, "push", repoName) + s.trustedCmdWithDeprecatedEnvPassphrases(pushCmd, "12345678", "12345678") + out, _, err := runCommandWithOutput(pushCmd) + c.Assert(err, check.IsNil, check.Commentf("Error running trusted push: %s\n%s", err, out)) + c.Assert(out, checker.Contains, "Signing and pushing trust metadata", check.Commentf("Missing expected output on trusted push")) +} + +func (s *DockerTrustSuite) TestTrustedPushWithFailingServer(c *check.C) { + repoName := fmt.Sprintf("%v/dockerclitrusted/failingserver:latest", privateRegistryURL) + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", repoName) + + pushCmd := exec.Command(dockerBinary, "push", repoName) + s.trustedCmdWithServer(pushCmd, "https://example.com:81/") + out, _, err := runCommandWithOutput(pushCmd) + c.Assert(err, check.NotNil, check.Commentf("Missing error while running trusted push w/ no server")) + c.Assert(out, checker.Contains, "error contacting notary server", check.Commentf("Missing expected output on trusted push")) +} + +func (s *DockerTrustSuite) TestTrustedPushWithoutServerAndUntrusted(c *check.C) { + repoName := fmt.Sprintf("%v/dockerclitrusted/trustedandnot:latest", privateRegistryURL) + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", repoName) + + pushCmd := exec.Command(dockerBinary, "push", "--disable-content-trust", repoName) + s.trustedCmdWithServer(pushCmd, "https://example.com/") + out, _, err := runCommandWithOutput(pushCmd) + c.Assert(err, check.IsNil, check.Commentf("trusted push with no server and --disable-content-trust failed: %s\n%s", err, out)) + c.Assert(out, check.Not(checker.Contains), "Error establishing connection to notary repository", check.Commentf("Missing expected output on trusted push with --disable-content-trust:")) +} + +func (s *DockerTrustSuite) TestTrustedPushWithExistingTag(c *check.C) { + repoName := fmt.Sprintf("%v/dockerclitag/trusted:latest", privateRegistryURL) + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", repoName) + dockerCmd(c, "push", repoName) + + pushCmd := exec.Command(dockerBinary, "push", repoName) + s.trustedCmd(pushCmd) + out, _, err := runCommandWithOutput(pushCmd) + c.Assert(err, check.IsNil, check.Commentf("trusted push failed: %s\n%s", err, out)) + c.Assert(out, checker.Contains, "Signing and pushing trust metadata", check.Commentf("Missing expected output on trusted push with existing tag")) + + // Try pull after push + pullCmd := exec.Command(dockerBinary, "pull", repoName) + s.trustedCmd(pullCmd) + out, _, err = runCommandWithOutput(pullCmd) + c.Assert(err, check.IsNil, check.Commentf(out)) + c.Assert(string(out), checker.Contains, "Status: Downloaded", check.Commentf(out)) +} + +func (s *DockerTrustSuite) TestTrustedPushWithExistingSignedTag(c *check.C) { + repoName := fmt.Sprintf("%v/dockerclipushpush/trusted:latest", privateRegistryURL) + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", repoName) + + // Do a trusted push + pushCmd := exec.Command(dockerBinary, "push", repoName) + s.trustedCmd(pushCmd) + out, _, err := runCommandWithOutput(pushCmd) + c.Assert(err, check.IsNil, check.Commentf("trusted push failed: %s\n%s", err, out)) + c.Assert(out, checker.Contains, "Signing and pushing trust metadata", check.Commentf("Missing expected output on trusted push with existing tag")) + + // Do another trusted push + pushCmd = exec.Command(dockerBinary, "push", repoName) + s.trustedCmd(pushCmd) + out, _, err = runCommandWithOutput(pushCmd) + c.Assert(err, check.IsNil, check.Commentf("trusted push failed: %s\n%s", err, out)) + c.Assert(out, checker.Contains, "Signing and pushing trust metadata", check.Commentf("Missing expected output on trusted push with existing tag")) + + dockerCmd(c, "rmi", repoName) + + // Try pull to ensure the double push did not break our ability to pull + pullCmd := exec.Command(dockerBinary, "pull", repoName) + s.trustedCmd(pullCmd) + out, _, err = runCommandWithOutput(pullCmd) + c.Assert(err, check.IsNil, check.Commentf("Error running trusted pull: %s\n%s", err, out)) + c.Assert(out, checker.Contains, "Status: Downloaded", check.Commentf("Missing expected output on trusted pull with --disable-content-trust")) + +} + +func (s *DockerTrustSuite) TestTrustedPushWithIncorrectPassphraseForNonRoot(c *check.C) { + repoName := fmt.Sprintf("%v/dockercliincorretpwd/trusted:latest", privateRegistryURL) + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", repoName) + + // Push with default passphrases + pushCmd := exec.Command(dockerBinary, "push", repoName) + s.trustedCmd(pushCmd) + out, _, err := runCommandWithOutput(pushCmd) + c.Assert(err, check.IsNil, check.Commentf("trusted push failed: %s\n%s", err, out)) + c.Assert(out, checker.Contains, "Signing and pushing trust metadata", check.Commentf("Missing expected output on trusted push:\n%s", out)) + + // Push with wrong passphrases + pushCmd = exec.Command(dockerBinary, "push", repoName) + s.trustedCmdWithPassphrases(pushCmd, "12345678", "87654321") + out, _, err = runCommandWithOutput(pushCmd) + c.Assert(err, check.NotNil, check.Commentf("Error missing from trusted push with short targets passphrase: \n%s", out)) + c.Assert(out, checker.Contains, "could not find necessary signing keys", check.Commentf("Missing expected output on trusted push with short targets/snapsnot passphrase")) +} + +// This test ensures backwards compatibility with old ENV variables. Should be +// deprecated by 1.10 +func (s *DockerTrustSuite) TestTrustedPushWithIncorrectDeprecatedPassphraseForNonRoot(c *check.C) { + repoName := fmt.Sprintf("%v/dockercliincorretdeprecatedpwd/trusted:latest", privateRegistryURL) + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", repoName) + + // Push with default passphrases + pushCmd := exec.Command(dockerBinary, "push", repoName) + s.trustedCmd(pushCmd) + out, _, err := runCommandWithOutput(pushCmd) + c.Assert(err, check.IsNil, check.Commentf("trusted push failed: %s\n%s", err, out)) + c.Assert(out, checker.Contains, "Signing and pushing trust metadata", check.Commentf("Missing expected output on trusted push")) + + // Push with wrong passphrases + pushCmd = exec.Command(dockerBinary, "push", repoName) + s.trustedCmdWithDeprecatedEnvPassphrases(pushCmd, "12345678", "87654321") + out, _, err = runCommandWithOutput(pushCmd) + c.Assert(err, check.NotNil, check.Commentf("Error missing from trusted push with short targets passphrase: \n%s", out)) + c.Assert(out, checker.Contains, "could not find necessary signing keys", check.Commentf("Missing expected output on trusted push with short targets/snapsnot passphrase")) +} + +func (s *DockerTrustSuite) TestTrustedPushWithExpiredSnapshot(c *check.C) { + c.Skip("Currently changes system time, causing instability") + repoName := fmt.Sprintf("%v/dockercliexpiredsnapshot/trusted:latest", privateRegistryURL) + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", repoName) + + // Push with default passphrases + pushCmd := exec.Command(dockerBinary, "push", repoName) + s.trustedCmd(pushCmd) + out, _, err := runCommandWithOutput(pushCmd) + c.Assert(err, check.IsNil, check.Commentf("trusted push failed: %s\n%s", err, out)) + c.Assert(out, checker.Contains, "Signing and pushing trust metadata", check.Commentf("Missing expected output on trusted push")) + + // Snapshots last for three years. This should be expired + fourYearsLater := time.Now().Add(time.Hour * 24 * 365 * 4) + + runAtDifferentDate(fourYearsLater, func() { + // Push with wrong passphrases + pushCmd = exec.Command(dockerBinary, "push", repoName) + s.trustedCmd(pushCmd) + out, _, err = runCommandWithOutput(pushCmd) + c.Assert(err, check.NotNil, check.Commentf("Error missing from trusted push with expired snapshot: \n%s", out)) + c.Assert(out, checker.Contains, "repository out-of-date", check.Commentf("Missing expected output on trusted push with expired snapshot")) + }) +} + +func (s *DockerTrustSuite) TestTrustedPushWithExpiredTimestamp(c *check.C) { + c.Skip("Currently changes system time, causing instability") + repoName := fmt.Sprintf("%v/dockercliexpiredtimestamppush/trusted:latest", privateRegistryURL) + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", repoName) + + // Push with default passphrases + pushCmd := exec.Command(dockerBinary, "push", repoName) + s.trustedCmd(pushCmd) + out, _, err := runCommandWithOutput(pushCmd) + c.Assert(err, check.IsNil, check.Commentf("trusted push failed: %s\n%s", err, out)) + c.Assert(out, checker.Contains, "Signing and pushing trust metadata", check.Commentf("Missing expected output on trusted push")) + + // The timestamps expire in two weeks. Lets check three + threeWeeksLater := time.Now().Add(time.Hour * 24 * 21) + + // Should succeed because the server transparently re-signs one + runAtDifferentDate(threeWeeksLater, func() { + pushCmd := exec.Command(dockerBinary, "push", repoName) + s.trustedCmd(pushCmd) + out, _, err := runCommandWithOutput(pushCmd) + c.Assert(err, check.IsNil, check.Commentf("Error running trusted push: %s\n%s", err, out)) + c.Assert(out, checker.Contains, "Signing and pushing trust metadata", check.Commentf("Missing expected output on trusted push with expired timestamp")) + }) +} + +func (s *DockerTrustSuite) TestTrustedPushWithReleasesDelegationOnly(c *check.C) { + testRequires(c, NotaryHosting) + repoName := fmt.Sprintf("%v/dockerclireleasedelegationinitfirst/trusted", privateRegistryURL) + targetName := fmt.Sprintf("%s:latest", repoName) + s.notaryInitRepo(c, repoName) + s.notaryCreateDelegation(c, repoName, "targets/releases", s.not.keys[0].Public) + s.notaryPublish(c, repoName) + + s.notaryImportKey(c, repoName, "targets/releases", s.not.keys[0].Private) + + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", targetName) + + pushCmd := exec.Command(dockerBinary, "push", targetName) + s.trustedCmd(pushCmd) + out, _, err := runCommandWithOutput(pushCmd) + c.Assert(err, check.IsNil, check.Commentf("trusted push failed: %s\n%s", err, out)) + c.Assert(out, checker.Contains, "Signing and pushing trust metadata", check.Commentf("Missing expected output on trusted push with existing tag")) + // check to make sure that the target has been added to targets/releases and not targets + s.assertTargetInRoles(c, repoName, "latest", "targets/releases") + s.assertTargetNotInRoles(c, repoName, "latest", "targets") + + // Try pull after push + os.RemoveAll(filepath.Join(cliconfig.ConfigDir(), "trust")) + + pullCmd := exec.Command(dockerBinary, "pull", targetName) + s.trustedCmd(pullCmd) + out, _, err = runCommandWithOutput(pullCmd) + c.Assert(err, check.IsNil, check.Commentf(out)) + c.Assert(string(out), checker.Contains, "Status: Downloaded", check.Commentf(out)) +} + +func (s *DockerTrustSuite) TestTrustedPushSignsAllFirstLevelRolesWeHaveKeysFor(c *check.C) { + testRequires(c, NotaryHosting) + repoName := fmt.Sprintf("%v/dockerclimanyroles/trusted", privateRegistryURL) + targetName := fmt.Sprintf("%s:latest", repoName) + s.notaryInitRepo(c, repoName) + s.notaryCreateDelegation(c, repoName, "targets/role1", s.not.keys[0].Public) + s.notaryCreateDelegation(c, repoName, "targets/role2", s.not.keys[1].Public) + s.notaryCreateDelegation(c, repoName, "targets/role3", s.not.keys[2].Public) + + // import everything except the third key + s.notaryImportKey(c, repoName, "targets/role1", s.not.keys[0].Private) + s.notaryImportKey(c, repoName, "targets/role2", s.not.keys[1].Private) + + s.notaryCreateDelegation(c, repoName, "targets/role1/subrole", s.not.keys[3].Public) + s.notaryImportKey(c, repoName, "targets/role1/subrole", s.not.keys[3].Private) + + s.notaryPublish(c, repoName) + + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", targetName) + + pushCmd := exec.Command(dockerBinary, "push", targetName) + s.trustedCmd(pushCmd) + out, _, err := runCommandWithOutput(pushCmd) + c.Assert(err, check.IsNil, check.Commentf("trusted push failed: %s\n%s", err, out)) + c.Assert(out, checker.Contains, "Signing and pushing trust metadata", check.Commentf("Missing expected output on trusted push with existing tag")) + + // check to make sure that the target has been added to targets/role1 and targets/role2, and + // not targets (because there are delegations) or targets/role3 (due to missing key) or + // targets/role1/subrole (due to it being a second level delegation) + s.assertTargetInRoles(c, repoName, "latest", "targets/role1", "targets/role2") + s.assertTargetNotInRoles(c, repoName, "latest", "targets") + + // Try pull after push + os.RemoveAll(filepath.Join(cliconfig.ConfigDir(), "trust")) + + // pull should fail because none of these are the releases role + pullCmd := exec.Command(dockerBinary, "pull", targetName) + s.trustedCmd(pullCmd) + out, _, err = runCommandWithOutput(pullCmd) + c.Assert(err, check.NotNil, check.Commentf(out)) +} + +func (s *DockerTrustSuite) TestTrustedPushSignsForRolesWithKeysAndValidPaths(c *check.C) { + repoName := fmt.Sprintf("%v/dockerclirolesbykeysandpaths/trusted", privateRegistryURL) + targetName := fmt.Sprintf("%s:latest", repoName) + s.notaryInitRepo(c, repoName) + s.notaryCreateDelegation(c, repoName, "targets/role1", s.not.keys[0].Public, "l", "z") + s.notaryCreateDelegation(c, repoName, "targets/role2", s.not.keys[1].Public, "x", "y") + s.notaryCreateDelegation(c, repoName, "targets/role3", s.not.keys[2].Public, "latest") + s.notaryCreateDelegation(c, repoName, "targets/role4", s.not.keys[3].Public, "latest") + + // import everything except the third key + s.notaryImportKey(c, repoName, "targets/role1", s.not.keys[0].Private) + s.notaryImportKey(c, repoName, "targets/role2", s.not.keys[1].Private) + s.notaryImportKey(c, repoName, "targets/role4", s.not.keys[3].Private) + + s.notaryPublish(c, repoName) + + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", targetName) + + pushCmd := exec.Command(dockerBinary, "push", targetName) + s.trustedCmd(pushCmd) + out, _, err := runCommandWithOutput(pushCmd) + c.Assert(err, check.IsNil, check.Commentf("trusted push failed: %s\n%s", err, out)) + c.Assert(out, checker.Contains, "Signing and pushing trust metadata", check.Commentf("Missing expected output on trusted push with existing tag")) + + // check to make sure that the target has been added to targets/role1 and targets/role4, and + // not targets (because there are delegations) or targets/role2 (due to path restrictions) or + // targets/role3 (due to missing key) + s.assertTargetInRoles(c, repoName, "latest", "targets/role1", "targets/role4") + s.assertTargetNotInRoles(c, repoName, "latest", "targets") + + // Try pull after push + os.RemoveAll(filepath.Join(cliconfig.ConfigDir(), "trust")) + + // pull should fail because none of these are the releases role + pullCmd := exec.Command(dockerBinary, "pull", targetName) + s.trustedCmd(pullCmd) + out, _, err = runCommandWithOutput(pullCmd) + c.Assert(err, check.NotNil, check.Commentf(out)) +} + +func (s *DockerTrustSuite) TestTrustedPushDoesntSignTargetsIfDelegationsExist(c *check.C) { + testRequires(c, NotaryHosting) + repoName := fmt.Sprintf("%v/dockerclireleasedelegationnotsignable/trusted", privateRegistryURL) + targetName := fmt.Sprintf("%s:latest", repoName) + s.notaryInitRepo(c, repoName) + s.notaryCreateDelegation(c, repoName, "targets/role1", s.not.keys[0].Public) + s.notaryPublish(c, repoName) + + // do not import any delegations key + + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", targetName) + + pushCmd := exec.Command(dockerBinary, "push", targetName) + s.trustedCmd(pushCmd) + out, _, err := runCommandWithOutput(pushCmd) + c.Assert(err, check.NotNil, check.Commentf("trusted push succeeded but should have failed:\n%s", out)) + c.Assert(out, checker.Contains, "no valid signing keys", + check.Commentf("Missing expected output on trusted push without keys")) + + s.assertTargetNotInRoles(c, repoName, "latest", "targets", "targets/role1") +} + +func (s *DockerRegistryAuthHtpasswdSuite) TestPushNoCredentialsNoRetry(c *check.C) { + repoName := fmt.Sprintf("%s/busybox", privateRegistryURL) + dockerCmd(c, "tag", "busybox", repoName) + out, _, err := dockerCmdWithError("push", repoName) + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(out, check.Not(checker.Contains), "Retrying") + c.Assert(out, checker.Contains, "no basic auth credentials") +} + +// This may be flaky but it's needed not to regress on unauthorized push, see #21054 +func (s *DockerSuite) TestPushToCentralRegistryUnauthorized(c *check.C) { + testRequires(c, Network) + repoName := "test/busybox" + dockerCmd(c, "tag", "busybox", repoName) + out, _, err := dockerCmdWithError("push", repoName) + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(out, check.Not(checker.Contains), "Retrying") +} + +func getTestTokenService(status int, body string) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(status) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(body)) + })) +} + +func (s *DockerRegistryAuthTokenSuite) TestPushTokenServiceUnauthResponse(c *check.C) { + ts := getTestTokenService(http.StatusUnauthorized, `{"errors": [{"Code":"UNAUTHORIZED", "message": "a message", "detail": null}]}`) + defer ts.Close() + s.setupRegistryWithTokenService(c, ts.URL) + repoName := fmt.Sprintf("%s/busybox", privateRegistryURL) + dockerCmd(c, "tag", "busybox", repoName) + out, _, err := dockerCmdWithError("push", repoName) + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(out, checker.Not(checker.Contains), "Retrying") + c.Assert(out, checker.Contains, "unauthorized: a message") +} + +func (s *DockerRegistryAuthTokenSuite) TestPushMisconfiguredTokenServiceResponseUnauthorized(c *check.C) { + ts := getTestTokenService(http.StatusUnauthorized, `{"error": "unauthorized"}`) + defer ts.Close() + s.setupRegistryWithTokenService(c, ts.URL) + repoName := fmt.Sprintf("%s/busybox", privateRegistryURL) + dockerCmd(c, "tag", "busybox", repoName) + out, _, err := dockerCmdWithError("push", repoName) + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(out, checker.Not(checker.Contains), "Retrying") + split := strings.Split(out, "\n") + c.Assert(split[len(split)-2], check.Equals, "unauthorized: authentication required") +} + +func (s *DockerRegistryAuthTokenSuite) TestPushMisconfiguredTokenServiceResponseError(c *check.C) { + ts := getTestTokenService(http.StatusInternalServerError, `{"error": "unexpected"}`) + defer ts.Close() + s.setupRegistryWithTokenService(c, ts.URL) + repoName := fmt.Sprintf("%s/busybox", privateRegistryURL) + dockerCmd(c, "tag", "busybox", repoName) + out, _, err := dockerCmdWithError("push", repoName) + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "Retrying") + split := strings.Split(out, "\n") + c.Assert(split[len(split)-2], check.Equals, "received unexpected HTTP status: 500 Internal Server Error") +} + +func (s *DockerRegistryAuthTokenSuite) TestPushMisconfiguredTokenServiceResponseUnparsable(c *check.C) { + ts := getTestTokenService(http.StatusForbidden, `no way`) + defer ts.Close() + s.setupRegistryWithTokenService(c, ts.URL) + repoName := fmt.Sprintf("%s/busybox", privateRegistryURL) + dockerCmd(c, "tag", "busybox", repoName) + out, _, err := dockerCmdWithError("push", repoName) + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(out, checker.Not(checker.Contains), "Retrying") + split := strings.Split(out, "\n") + c.Assert(split[len(split)-2], checker.Contains, "error parsing HTTP 403 response body: ") +} + +func (s *DockerRegistryAuthTokenSuite) TestPushMisconfiguredTokenServiceResponseNoToken(c *check.C) { + ts := getTestTokenService(http.StatusOK, `{"something": "wrong"}`) + defer ts.Close() + s.setupRegistryWithTokenService(c, ts.URL) + repoName := fmt.Sprintf("%s/busybox", privateRegistryURL) + dockerCmd(c, "tag", "busybox", repoName) + out, _, err := dockerCmdWithError("push", repoName) + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(out, checker.Not(checker.Contains), "Retrying") + split := strings.Split(out, "\n") + c.Assert(split[len(split)-2], check.Equals, "authorization server did not include a token in the response") +} diff --git a/integration-cli/docker_cli_registry_user_agent_test.go b/integration-cli/docker_cli_registry_user_agent_test.go new file mode 100644 index 00000000..67a950ca --- /dev/null +++ b/integration-cli/docker_cli_registry_user_agent_test.go @@ -0,0 +1,120 @@ +package main + +import ( + "fmt" + "net/http" + "regexp" + + "github.com/go-check/check" +) + +// unescapeBackslashSemicolonParens unescapes \;() +func unescapeBackslashSemicolonParens(s string) string { + re := regexp.MustCompile(`\\;`) + ret := re.ReplaceAll([]byte(s), []byte(";")) + + re = regexp.MustCompile(`\\\(`) + ret = re.ReplaceAll([]byte(ret), []byte("(")) + + re = regexp.MustCompile(`\\\)`) + ret = re.ReplaceAll([]byte(ret), []byte(")")) + + re = regexp.MustCompile(`\\\\`) + ret = re.ReplaceAll([]byte(ret), []byte(`\`)) + + return string(ret) +} + +func regexpCheckUA(c *check.C, ua string) { + re := regexp.MustCompile("(?P.+) UpstreamClient(?P.+)") + substrArr := re.FindStringSubmatch(ua) + + c.Assert(substrArr, check.HasLen, 3, check.Commentf("Expected 'UpstreamClient()' with upstream client UA")) + dockerUA := substrArr[1] + upstreamUAEscaped := substrArr[2] + + // check dockerUA looks correct + reDockerUA := regexp.MustCompile("^docker/[0-9A-Za-z+]") + bMatchDockerUA := reDockerUA.MatchString(dockerUA) + c.Assert(bMatchDockerUA, check.Equals, true, check.Commentf("Docker Engine User-Agent malformed")) + + // check upstreamUA looks correct + // Expecting something like: Docker-Client/1.11.0-dev (linux) + upstreamUA := unescapeBackslashSemicolonParens(upstreamUAEscaped) + reUpstreamUA := regexp.MustCompile("^\\(Docker-Client/[0-9A-Za-z+]") + bMatchUpstreamUA := reUpstreamUA.MatchString(upstreamUA) + c.Assert(bMatchUpstreamUA, check.Equals, true, check.Commentf("(Upstream) Docker Client User-Agent malformed")) +} + +func registerUserAgentHandler(reg *testRegistry, result *string) { + reg.registerHandler("/v2/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(404) + var ua string + for k, v := range r.Header { + if k == "User-Agent" { + ua = v[0] + } + } + *result = ua + }) +} + +// TestUserAgentPassThroughOnPull verifies that when an image is pulled from +// a registry, the registry should see a User-Agent string of the form +// [docker engine UA] UptreamClientSTREAM-CLIENT([client UA]) +func (s *DockerRegistrySuite) TestUserAgentPassThrough(c *check.C) { + var ( + buildUA string + pullUA string + pushUA string + loginUA string + ) + + buildReg, err := newTestRegistry(c) + c.Assert(err, check.IsNil) + registerUserAgentHandler(buildReg, &buildUA) + buildRepoName := fmt.Sprintf("%s/busybox", buildReg.hostport) + + pullReg, err := newTestRegistry(c) + c.Assert(err, check.IsNil) + registerUserAgentHandler(pullReg, &pullUA) + pullRepoName := fmt.Sprintf("%s/busybox", pullReg.hostport) + + pushReg, err := newTestRegistry(c) + c.Assert(err, check.IsNil) + registerUserAgentHandler(pushReg, &pushUA) + pushRepoName := fmt.Sprintf("%s/busybox", pushReg.hostport) + + loginReg, err := newTestRegistry(c) + c.Assert(err, check.IsNil) + registerUserAgentHandler(loginReg, &loginUA) + + err = s.d.Start( + "--insecure-registry", buildReg.hostport, + "--insecure-registry", pullReg.hostport, + "--insecure-registry", pushReg.hostport, + "--insecure-registry", loginReg.hostport, + "--disable-legacy-registry=true") + c.Assert(err, check.IsNil) + + dockerfileName, cleanup1, err := makefile(fmt.Sprintf("FROM %s", buildRepoName)) + c.Assert(err, check.IsNil, check.Commentf("Unable to create test dockerfile")) + defer cleanup1() + s.d.Cmd("build", "--file", dockerfileName, ".") + regexpCheckUA(c, buildUA) + + s.d.Cmd("login", "-u", "richard", "-p", "testtest", "-e", "testuser@testdomain.com", loginReg.hostport) + regexpCheckUA(c, loginUA) + + s.d.Cmd("pull", pullRepoName) + regexpCheckUA(c, pullUA) + + dockerfileName, cleanup2, err := makefile(`FROM scratch + ENV foo bar`) + c.Assert(err, check.IsNil, check.Commentf("Unable to create test dockerfile")) + defer cleanup2() + s.d.Cmd("build", "-t", pushRepoName, "--file", dockerfileName, ".") + + s.d.Cmd("push", pushRepoName) + regexpCheckUA(c, pushUA) +} diff --git a/integration-cli/docker_cli_rename_test.go b/integration-cli/docker_cli_rename_test.go new file mode 100644 index 00000000..cbb60f85 --- /dev/null +++ b/integration-cli/docker_cli_rename_test.go @@ -0,0 +1,86 @@ +package main + +import ( + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/stringid" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestRenameStoppedContainer(c *check.C) { + out, _ := dockerCmd(c, "run", "--name", "first_name", "-d", "busybox", "sh") + + cleanedContainerID := strings.TrimSpace(out) + dockerCmd(c, "wait", cleanedContainerID) + + name := inspectField(c, cleanedContainerID, "Name") + newName := "new_name" + stringid.GenerateNonCryptoID() + dockerCmd(c, "rename", "first_name", newName) + + name = inspectField(c, cleanedContainerID, "Name") + c.Assert(name, checker.Equals, "/"+newName, check.Commentf("Failed to rename container %s", name)) + +} + +func (s *DockerSuite) TestRenameRunningContainer(c *check.C) { + out, _ := dockerCmd(c, "run", "--name", "first_name", "-d", "busybox", "sh") + + newName := "new_name" + stringid.GenerateNonCryptoID() + cleanedContainerID := strings.TrimSpace(out) + dockerCmd(c, "rename", "first_name", newName) + + name := inspectField(c, cleanedContainerID, "Name") + c.Assert(name, checker.Equals, "/"+newName, check.Commentf("Failed to rename container %s", name)) +} + +func (s *DockerSuite) TestRenameRunningContainerAndReuse(c *check.C) { + out, _ := runSleepingContainer(c, "--name", "first_name") + c.Assert(waitRun("first_name"), check.IsNil) + + newName := "new_name" + ContainerID := strings.TrimSpace(out) + dockerCmd(c, "rename", "first_name", newName) + + name := inspectField(c, ContainerID, "Name") + c.Assert(name, checker.Equals, "/"+newName, check.Commentf("Failed to rename container")) + + out, _ = runSleepingContainer(c, "--name", "first_name") + c.Assert(waitRun("first_name"), check.IsNil) + newContainerID := strings.TrimSpace(out) + name = inspectField(c, newContainerID, "Name") + c.Assert(name, checker.Equals, "/first_name", check.Commentf("Failed to reuse container name")) +} + +func (s *DockerSuite) TestRenameCheckNames(c *check.C) { + dockerCmd(c, "run", "--name", "first_name", "-d", "busybox", "sh") + + newName := "new_name" + stringid.GenerateNonCryptoID() + dockerCmd(c, "rename", "first_name", newName) + + name := inspectField(c, newName, "Name") + c.Assert(name, checker.Equals, "/"+newName, check.Commentf("Failed to rename container %s", name)) + + name, err := inspectFieldWithError("first_name", "Name") + c.Assert(err, checker.NotNil, check.Commentf(name)) + c.Assert(err.Error(), checker.Contains, "No such image or container: first_name") +} + +func (s *DockerSuite) TestRenameInvalidName(c *check.C) { + runSleepingContainer(c, "--name", "myname") + + out, _, err := dockerCmdWithError("rename", "myname", "new:invalid") + c.Assert(err, checker.NotNil, check.Commentf("Renaming container to invalid name should have failed: %s", out)) + c.Assert(out, checker.Contains, "Invalid container name", check.Commentf("%v", err)) + + out, _, err = dockerCmdWithError("rename", "myname", "") + c.Assert(err, checker.NotNil, check.Commentf("Renaming container to invalid name should have failed: %s", out)) + c.Assert(out, checker.Contains, "may be empty", check.Commentf("%v", err)) + + out, _, err = dockerCmdWithError("rename", "", "newname") + c.Assert(err, checker.NotNil, check.Commentf("Renaming container with empty name should have failed: %s", out)) + c.Assert(out, checker.Contains, "may be empty", check.Commentf("%v", err)) + + out, _ = dockerCmd(c, "ps", "-a") + c.Assert(out, checker.Contains, "myname", check.Commentf("Output of docker ps should have included 'myname': %s", out)) +} diff --git a/integration-cli/docker_cli_restart_test.go b/integration-cli/docker_cli_restart_test.go new file mode 100644 index 00000000..31c89203 --- /dev/null +++ b/integration-cli/docker_cli_restart_test.go @@ -0,0 +1,249 @@ +package main + +import ( + "os" + "strconv" + "strings" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestRestartStoppedContainer(c *check.C) { + dockerCmd(c, "run", "--name=test", "busybox", "echo", "foobar") + cleanedContainerID, err := getIDByName("test") + c.Assert(err, check.IsNil) + + out, _ := dockerCmd(c, "logs", cleanedContainerID) + c.Assert(out, checker.Equals, "foobar\n") + + dockerCmd(c, "restart", cleanedContainerID) + + // Wait until the container has stopped + err = waitInspect(cleanedContainerID, "{{.State.Running}}", "false", 20*time.Second) + c.Assert(err, checker.IsNil) + + out, _ = dockerCmd(c, "logs", cleanedContainerID) + c.Assert(out, checker.Equals, "foobar\nfoobar\n") +} + +func (s *DockerSuite) TestRestartRunningContainer(c *check.C) { + out, _ := dockerCmd(c, "run", "-d", "busybox", "sh", "-c", "echo foobar && sleep 30 && echo 'should not print this'") + + cleanedContainerID := strings.TrimSpace(out) + + c.Assert(waitRun(cleanedContainerID), checker.IsNil) + + out, _ = dockerCmd(c, "logs", cleanedContainerID) + c.Assert(out, checker.Equals, "foobar\n") + + dockerCmd(c, "restart", "-t", "1", cleanedContainerID) + + out, _ = dockerCmd(c, "logs", cleanedContainerID) + + c.Assert(waitRun(cleanedContainerID), checker.IsNil) + + c.Assert(out, checker.Equals, "foobar\nfoobar\n") +} + +// Test that restarting a container with a volume does not create a new volume on restart. Regression test for #819. +func (s *DockerSuite) TestRestartWithVolumes(c *check.C) { + prefix, slash := getPrefixAndSlashFromDaemonPlatform() + out, _ := runSleepingContainer(c, "-d", "-v", prefix+slash+"test") + + cleanedContainerID := strings.TrimSpace(out) + out, err := inspectFilter(cleanedContainerID, "len .Mounts") + c.Assert(err, check.IsNil, check.Commentf("failed to inspect %s: %s", cleanedContainerID, out)) + out = strings.Trim(out, " \n\r") + c.Assert(out, checker.Equals, "1") + + source, err := inspectMountSourceField(cleanedContainerID, prefix+slash+"test") + c.Assert(err, checker.IsNil) + + dockerCmd(c, "restart", cleanedContainerID) + + out, err = inspectFilter(cleanedContainerID, "len .Mounts") + c.Assert(err, check.IsNil, check.Commentf("failed to inspect %s: %s", cleanedContainerID, out)) + out = strings.Trim(out, " \n\r") + c.Assert(out, checker.Equals, "1") + + sourceAfterRestart, err := inspectMountSourceField(cleanedContainerID, prefix+slash+"test") + c.Assert(err, checker.IsNil) + c.Assert(source, checker.Equals, sourceAfterRestart) +} + +func (s *DockerSuite) TestRestartPolicyNO(c *check.C) { + out, _ := dockerCmd(c, "run", "-d", "--restart=no", "busybox", "false") + + id := strings.TrimSpace(string(out)) + name := inspectField(c, id, "HostConfig.RestartPolicy.Name") + c.Assert(name, checker.Equals, "no") +} + +func (s *DockerSuite) TestRestartPolicyAlways(c *check.C) { + out, _ := dockerCmd(c, "run", "-d", "--restart=always", "busybox", "false") + + id := strings.TrimSpace(string(out)) + name := inspectField(c, id, "HostConfig.RestartPolicy.Name") + c.Assert(name, checker.Equals, "always") + + MaximumRetryCount := inspectField(c, id, "HostConfig.RestartPolicy.MaximumRetryCount") + + // MaximumRetryCount=0 if the restart policy is always + c.Assert(MaximumRetryCount, checker.Equals, "0") +} + +func (s *DockerSuite) TestRestartPolicyOnFailure(c *check.C) { + out, _ := dockerCmd(c, "run", "-d", "--restart=on-failure:1", "busybox", "false") + + id := strings.TrimSpace(string(out)) + name := inspectField(c, id, "HostConfig.RestartPolicy.Name") + c.Assert(name, checker.Equals, "on-failure") + +} + +// a good container with --restart=on-failure:3 +// MaximumRetryCount!=0; RestartCount=0 +func (s *DockerSuite) TestRestartContainerwithGoodContainer(c *check.C) { + out, _ := dockerCmd(c, "run", "-d", "--restart=on-failure:3", "busybox", "true") + + id := strings.TrimSpace(string(out)) + err := waitInspect(id, "{{ .State.Restarting }} {{ .State.Running }}", "false false", 30*time.Second) + c.Assert(err, checker.IsNil) + + count := inspectField(c, id, "RestartCount") + c.Assert(count, checker.Equals, "0") + + MaximumRetryCount := inspectField(c, id, "HostConfig.RestartPolicy.MaximumRetryCount") + c.Assert(MaximumRetryCount, checker.Equals, "3") + +} + +func (s *DockerSuite) TestRestartContainerSuccess(c *check.C) { + testRequires(c, SameHostDaemon) + + out, _ := runSleepingContainer(c, "-d", "--restart=always") + id := strings.TrimSpace(out) + c.Assert(waitRun(id), check.IsNil) + + pidStr := inspectField(c, id, "State.Pid") + + pid, err := strconv.Atoi(pidStr) + c.Assert(err, check.IsNil) + + p, err := os.FindProcess(pid) + c.Assert(err, check.IsNil) + c.Assert(p, check.NotNil) + + err = p.Kill() + c.Assert(err, check.IsNil) + + err = waitInspect(id, "{{.RestartCount}}", "1", 30*time.Second) + c.Assert(err, check.IsNil) + + err = waitInspect(id, "{{.State.Status}}", "running", 30*time.Second) + c.Assert(err, check.IsNil) +} + +func (s *DockerSuite) TestRestartWithPolicyUserDefinedNetwork(c *check.C) { + // TODO Windows. This may be portable following HNS integration post TP5. + testRequires(c, DaemonIsLinux, SameHostDaemon, NotUserNamespace, NotArm) + dockerCmd(c, "network", "create", "-d", "bridge", "udNet") + + dockerCmd(c, "run", "-d", "--net=udNet", "--name=first", "busybox", "top") + c.Assert(waitRun("first"), check.IsNil) + + dockerCmd(c, "run", "-d", "--restart=always", "--net=udNet", "--name=second", + "--link=first:foo", "busybox", "top") + c.Assert(waitRun("second"), check.IsNil) + + // ping to first and its alias foo must succeed + _, _, err := dockerCmdWithError("exec", "second", "ping", "-c", "1", "first") + c.Assert(err, check.IsNil) + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "foo") + c.Assert(err, check.IsNil) + + // Now kill the second container and let the restart policy kick in + pidStr := inspectField(c, "second", "State.Pid") + + pid, err := strconv.Atoi(pidStr) + c.Assert(err, check.IsNil) + + p, err := os.FindProcess(pid) + c.Assert(err, check.IsNil) + c.Assert(p, check.NotNil) + + err = p.Kill() + c.Assert(err, check.IsNil) + + err = waitInspect("second", "{{.RestartCount}}", "1", 5*time.Second) + c.Assert(err, check.IsNil) + + err = waitInspect("second", "{{.State.Status}}", "running", 5*time.Second) + + // ping to first and its alias foo must still succeed + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "first") + c.Assert(err, check.IsNil) + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "foo") + c.Assert(err, check.IsNil) +} + +func (s *DockerSuite) TestRestartPolicyAfterRestart(c *check.C) { + testRequires(c, SameHostDaemon) + + out, _ := runSleepingContainer(c, "-d", "--restart=always") + id := strings.TrimSpace(out) + c.Assert(waitRun(id), check.IsNil) + + dockerCmd(c, "restart", id) + + c.Assert(waitRun(id), check.IsNil) + + pidStr := inspectField(c, id, "State.Pid") + + pid, err := strconv.Atoi(pidStr) + c.Assert(err, check.IsNil) + + p, err := os.FindProcess(pid) + c.Assert(err, check.IsNil) + c.Assert(p, check.NotNil) + + err = p.Kill() + c.Assert(err, check.IsNil) + + err = waitInspect(id, "{{.RestartCount}}", "1", 30*time.Second) + c.Assert(err, check.IsNil) + + err = waitInspect(id, "{{.State.Status}}", "running", 30*time.Second) + c.Assert(err, check.IsNil) +} + +func (s *DockerSuite) TestRestartContainerwithRestartPolicy(c *check.C) { + out1, _ := dockerCmd(c, "run", "-d", "--restart=on-failure:3", "busybox", "false") + out2, _ := dockerCmd(c, "run", "-d", "--restart=always", "busybox", "false") + + id1 := strings.TrimSpace(string(out1)) + id2 := strings.TrimSpace(string(out2)) + err := waitInspect(id1, "{{ .State.Restarting }} {{ .State.Running }}", "false false", 30*time.Second) + c.Assert(err, checker.IsNil) + + // TODO: fix racey problem during restart: + // https://jenkins.dockerproject.org/job/Docker-PRs-Win2Lin/24665/console + // Error response from daemon: Cannot restart container 6655f620d90b390527db23c0a15b3e46d86a58ecec20a5697ab228d860174251: remove /var/run/docker/libcontainerd/6655f620d90b390527db23c0a15b3e46d86a58ecec20a5697ab228d860174251/rootfs: device or resource busy + if _, _, err := dockerCmdWithError("restart", id1); err != nil { + // if restart met racey problem, try again + time.Sleep(500 * time.Millisecond) + dockerCmd(c, "restart", id1) + } + if _, _, err := dockerCmdWithError("restart", id2); err != nil { + // if restart met racey problem, try again + time.Sleep(500 * time.Millisecond) + dockerCmd(c, "restart", id2) + } + + dockerCmd(c, "stop", id1) + dockerCmd(c, "stop", id2) + dockerCmd(c, "start", id1) + dockerCmd(c, "start", id2) +} diff --git a/integration-cli/docker_cli_rm_test.go b/integration-cli/docker_cli_rm_test.go new file mode 100644 index 00000000..0186c567 --- /dev/null +++ b/integration-cli/docker_cli_rm_test.go @@ -0,0 +1,86 @@ +package main + +import ( + "io/ioutil" + "os" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestRmContainerWithRemovedVolume(c *check.C) { + testRequires(c, SameHostDaemon) + + prefix, slash := getPrefixAndSlashFromDaemonPlatform() + + tempDir, err := ioutil.TempDir("", "test-rm-container-with-removed-volume-") + if err != nil { + c.Fatalf("failed to create temporary directory: %s", tempDir) + } + defer os.RemoveAll(tempDir) + + dockerCmd(c, "run", "--name", "losemyvolumes", "-v", tempDir+":"+prefix+slash+"test", "busybox", "true") + + err = os.RemoveAll(tempDir) + c.Assert(err, check.IsNil) + + dockerCmd(c, "rm", "-v", "losemyvolumes") +} + +func (s *DockerSuite) TestRmContainerWithVolume(c *check.C) { + prefix, slash := getPrefixAndSlashFromDaemonPlatform() + + dockerCmd(c, "run", "--name", "foo", "-v", prefix+slash+"srv", "busybox", "true") + + dockerCmd(c, "rm", "-v", "foo") +} + +func (s *DockerSuite) TestRmContainerRunning(c *check.C) { + createRunningContainer(c, "foo") + + _, _, err := dockerCmdWithError("rm", "foo") + c.Assert(err, checker.NotNil, check.Commentf("Expected error, can't rm a running container")) +} + +func (s *DockerSuite) TestRmContainerForceRemoveRunning(c *check.C) { + createRunningContainer(c, "foo") + + // Stop then remove with -s + dockerCmd(c, "rm", "-f", "foo") +} + +func (s *DockerSuite) TestRmContainerOrphaning(c *check.C) { + dockerfile1 := `FROM busybox:latest + ENTRYPOINT ["true"]` + img := "test-container-orphaning" + dockerfile2 := `FROM busybox:latest + ENTRYPOINT ["true"] + MAINTAINER Integration Tests` + + // build first dockerfile + img1, err := buildImage(img, dockerfile1, true) + c.Assert(err, check.IsNil, check.Commentf("Could not build image %s", img)) + // run container on first image + dockerCmd(c, "run", img) + // rebuild dockerfile with a small addition at the end + _, err = buildImage(img, dockerfile2, true) + c.Assert(err, check.IsNil, check.Commentf("Could not rebuild image %s", img)) + // try to remove the image, should not error out. + out, _, err := dockerCmdWithError("rmi", img) + c.Assert(err, check.IsNil, check.Commentf("Expected to removing the image, but failed: %s", out)) + + // check if we deleted the first image + out, _ = dockerCmd(c, "images", "-q", "--no-trunc") + c.Assert(out, checker.Contains, img1, check.Commentf("Orphaned container (could not find %q in docker images): %s", img1, out)) + +} + +func (s *DockerSuite) TestRmInvalidContainer(c *check.C) { + out, _, err := dockerCmdWithError("rm", "unknown") + c.Assert(err, checker.NotNil, check.Commentf("Expected error on rm unknown container, got none")) + c.Assert(out, checker.Contains, "No such container") +} + +func createRunningContainer(c *check.C, name string) { + runSleepingContainer(c, "-dt", "--name", name) +} diff --git a/integration-cli/docker_cli_rmi_test.go b/integration-cli/docker_cli_rmi_test.go new file mode 100644 index 00000000..697be326 --- /dev/null +++ b/integration-cli/docker_cli_rmi_test.go @@ -0,0 +1,362 @@ +package main + +import ( + "fmt" + "os/exec" + "strings" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/stringid" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestRmiWithContainerFails(c *check.C) { + errSubstr := "is using it" + + // create a container + out, _ := dockerCmd(c, "run", "-d", "busybox", "true") + + cleanedContainerID := strings.TrimSpace(out) + + // try to delete the image + out, _, err := dockerCmdWithError("rmi", "busybox") + // Container is using image, should not be able to rmi + c.Assert(err, checker.NotNil) + // Container is using image, error message should contain errSubstr + c.Assert(out, checker.Contains, errSubstr, check.Commentf("Container: %q", cleanedContainerID)) + + // make sure it didn't delete the busybox name + images, _ := dockerCmd(c, "images") + // The name 'busybox' should not have been removed from images + c.Assert(images, checker.Contains, "busybox") +} + +func (s *DockerSuite) TestRmiTag(c *check.C) { + imagesBefore, _ := dockerCmd(c, "images", "-a") + dockerCmd(c, "tag", "busybox", "utest:tag1") + dockerCmd(c, "tag", "busybox", "utest/docker:tag2") + dockerCmd(c, "tag", "busybox", "utest:5000/docker:tag3") + { + imagesAfter, _ := dockerCmd(c, "images", "-a") + c.Assert(strings.Count(imagesAfter, "\n"), checker.Equals, strings.Count(imagesBefore, "\n")+3, check.Commentf("before: %q\n\nafter: %q\n", imagesBefore, imagesAfter)) + } + dockerCmd(c, "rmi", "utest/docker:tag2") + { + imagesAfter, _ := dockerCmd(c, "images", "-a") + c.Assert(strings.Count(imagesAfter, "\n"), checker.Equals, strings.Count(imagesBefore, "\n")+2, check.Commentf("before: %q\n\nafter: %q\n", imagesBefore, imagesAfter)) + } + dockerCmd(c, "rmi", "utest:5000/docker:tag3") + { + imagesAfter, _ := dockerCmd(c, "images", "-a") + c.Assert(strings.Count(imagesAfter, "\n"), checker.Equals, strings.Count(imagesBefore, "\n")+1, check.Commentf("before: %q\n\nafter: %q\n", imagesBefore, imagesAfter)) + + } + dockerCmd(c, "rmi", "utest:tag1") + { + imagesAfter, _ := dockerCmd(c, "images", "-a") + c.Assert(strings.Count(imagesAfter, "\n"), checker.Equals, strings.Count(imagesBefore, "\n"), check.Commentf("before: %q\n\nafter: %q\n", imagesBefore, imagesAfter)) + + } +} + +func (s *DockerSuite) TestRmiImgIDMultipleTag(c *check.C) { + out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "mkdir '/busybox-one'") + + containerID := strings.TrimSpace(out) + + // Wait for it to exit as cannot commit a running container on Windows, and + // it will take a few seconds to exit + if daemonPlatform == "windows" { + err := waitExited(containerID, 60*time.Second) + c.Assert(err, check.IsNil) + } + + dockerCmd(c, "commit", containerID, "busybox-one") + + imagesBefore, _ := dockerCmd(c, "images", "-a") + dockerCmd(c, "tag", "busybox-one", "busybox-one:tag1") + dockerCmd(c, "tag", "busybox-one", "busybox-one:tag2") + + imagesAfter, _ := dockerCmd(c, "images", "-a") + // tag busybox to create 2 more images with same imageID + c.Assert(strings.Count(imagesAfter, "\n"), checker.Equals, strings.Count(imagesBefore, "\n")+2, check.Commentf("docker images shows: %q\n", imagesAfter)) + + imgID := inspectField(c, "busybox-one:tag1", "Id") + + // run a container with the image + out, _ = runSleepingContainerInImage(c, "busybox-one") + + containerID = strings.TrimSpace(out) + + // first checkout without force it fails + out, _, err := dockerCmdWithError("rmi", imgID) + expected := fmt.Sprintf("conflict: unable to delete %s (cannot be forced) - image is being used by running container %s", stringid.TruncateID(imgID), stringid.TruncateID(containerID)) + // rmi tagged in multiple repos should have failed without force + c.Assert(err, checker.NotNil) + c.Assert(out, checker.Contains, expected) + + dockerCmd(c, "stop", containerID) + dockerCmd(c, "rmi", "-f", imgID) + + imagesAfter, _ = dockerCmd(c, "images", "-a") + // rmi -f failed, image still exists + c.Assert(imagesAfter, checker.Not(checker.Contains), imgID[:12], check.Commentf("ImageID:%q; ImagesAfter: %q", imgID, imagesAfter)) +} + +func (s *DockerSuite) TestRmiImgIDForce(c *check.C) { + out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "mkdir '/busybox-test'") + + containerID := strings.TrimSpace(out) + + // Wait for it to exit as cannot commit a running container on Windows, and + // it will take a few seconds to exit + if daemonPlatform == "windows" { + err := waitExited(containerID, 60*time.Second) + c.Assert(err, check.IsNil) + } + + dockerCmd(c, "commit", containerID, "busybox-test") + + imagesBefore, _ := dockerCmd(c, "images", "-a") + dockerCmd(c, "tag", "busybox-test", "utest:tag1") + dockerCmd(c, "tag", "busybox-test", "utest:tag2") + dockerCmd(c, "tag", "busybox-test", "utest/docker:tag3") + dockerCmd(c, "tag", "busybox-test", "utest:5000/docker:tag4") + { + imagesAfter, _ := dockerCmd(c, "images", "-a") + c.Assert(strings.Count(imagesAfter, "\n"), checker.Equals, strings.Count(imagesBefore, "\n")+4, check.Commentf("before: %q\n\nafter: %q\n", imagesBefore, imagesAfter)) + } + imgID := inspectField(c, "busybox-test", "Id") + + // first checkout without force it fails + out, _, err := dockerCmdWithError("rmi", imgID) + // rmi tagged in multiple repos should have failed without force + c.Assert(err, checker.NotNil) + // rmi tagged in multiple repos should have failed without force + c.Assert(out, checker.Contains, "(must be forced) - image is referenced in one or more repositories", check.Commentf("out: %s; err: %v;", out, err)) + + dockerCmd(c, "rmi", "-f", imgID) + { + imagesAfter, _ := dockerCmd(c, "images", "-a") + // rmi failed, image still exists + c.Assert(imagesAfter, checker.Not(checker.Contains), imgID[:12]) + } +} + +// See https://github.com/docker/docker/issues/14116 +func (s *DockerSuite) TestRmiImageIDForceWithRunningContainersAndMultipleTags(c *check.C) { + dockerfile := "FROM busybox\nRUN echo test 14116\n" + imgID, err := buildImage("test-14116", dockerfile, false) + c.Assert(err, checker.IsNil) + + newTag := "newtag" + dockerCmd(c, "tag", imgID, newTag) + runSleepingContainerInImage(c, imgID) + + out, _, err := dockerCmdWithError("rmi", "-f", imgID) + // rmi -f should not delete image with running containers + c.Assert(err, checker.NotNil) + c.Assert(out, checker.Contains, "(cannot be forced) - image is being used by running container") +} + +func (s *DockerSuite) TestRmiTagWithExistingContainers(c *check.C) { + container := "test-delete-tag" + newtag := "busybox:newtag" + bb := "busybox:latest" + dockerCmd(c, "tag", bb, newtag) + + dockerCmd(c, "run", "--name", container, bb, "/bin/true") + + out, _ := dockerCmd(c, "rmi", newtag) + c.Assert(strings.Count(out, "Untagged: "), checker.Equals, 1) +} + +func (s *DockerSuite) TestRmiForceWithExistingContainers(c *check.C) { + image := "busybox-clone" + + cmd := exec.Command(dockerBinary, "build", "--no-cache", "-t", image, "-") + cmd.Stdin = strings.NewReader(`FROM busybox +MAINTAINER foo`) + + out, _, err := runCommandWithOutput(cmd) + c.Assert(err, checker.IsNil, check.Commentf("Could not build %s: %s", image, out)) + + dockerCmd(c, "run", "--name", "test-force-rmi", image, "/bin/true") + + dockerCmd(c, "rmi", "-f", image) +} + +func (s *DockerSuite) TestRmiWithMultipleRepositories(c *check.C) { + newRepo := "127.0.0.1:5000/busybox" + oldRepo := "busybox" + newTag := "busybox:test" + dockerCmd(c, "tag", oldRepo, newRepo) + + dockerCmd(c, "run", "--name", "test", oldRepo, "touch", "/abcd") + + dockerCmd(c, "commit", "test", newTag) + + out, _ := dockerCmd(c, "rmi", newTag) + c.Assert(out, checker.Contains, "Untagged: "+newTag) +} + +func (s *DockerSuite) TestRmiForceWithMultipleRepositories(c *check.C) { + imageName := "rmiimage" + tag1 := imageName + ":tag1" + tag2 := imageName + ":tag2" + + _, err := buildImage(tag1, + `FROM busybox + MAINTAINER "docker"`, + true) + if err != nil { + c.Fatal(err) + } + + dockerCmd(c, "tag", tag1, tag2) + + out, _ := dockerCmd(c, "rmi", "-f", tag2) + c.Assert(out, checker.Contains, "Untagged: "+tag2) + c.Assert(out, checker.Not(checker.Contains), "Untagged: "+tag1) + + // Check built image still exists + images, _ := dockerCmd(c, "images", "-a") + c.Assert(images, checker.Contains, imageName, check.Commentf("Built image missing %q; Images: %q", imageName, images)) +} + +func (s *DockerSuite) TestRmiBlank(c *check.C) { + // try to delete a blank image name + out, _, err := dockerCmdWithError("rmi", "") + // Should have failed to delete '' image + c.Assert(err, checker.NotNil) + // Wrong error message generated + c.Assert(out, checker.Not(checker.Contains), "no such id", check.Commentf("out: %s", out)) + // Expected error message not generated + c.Assert(out, checker.Contains, "image name cannot be blank", check.Commentf("out: %s", out)) + + out, _, err = dockerCmdWithError("rmi", " ") + // Should have failed to delete ' ' image + c.Assert(err, checker.NotNil) + // Expected error message not generated + c.Assert(out, checker.Contains, "image name cannot be blank", check.Commentf("out: %s", out)) +} + +func (s *DockerSuite) TestRmiContainerImageNotFound(c *check.C) { + // Build 2 images for testing. + imageNames := []string{"test1", "test2"} + imageIds := make([]string, 2) + for i, name := range imageNames { + dockerfile := fmt.Sprintf("FROM busybox\nMAINTAINER %s\nRUN echo %s\n", name, name) + id, err := buildImage(name, dockerfile, false) + c.Assert(err, checker.IsNil) + imageIds[i] = id + } + + // Create a long-running container. + runSleepingContainerInImage(c, imageNames[0]) + + // Create a stopped container, and then force remove its image. + dockerCmd(c, "run", imageNames[1], "true") + dockerCmd(c, "rmi", "-f", imageIds[1]) + + // Try to remove the image of the running container and see if it fails as expected. + out, _, err := dockerCmdWithError("rmi", "-f", imageIds[0]) + // The image of the running container should not be removed. + c.Assert(err, checker.NotNil) + c.Assert(out, checker.Contains, "image is being used by running container", check.Commentf("out: %s", out)) +} + +// #13422 +func (s *DockerSuite) TestRmiUntagHistoryLayer(c *check.C) { + image := "tmp1" + // Build a image for testing. + dockerfile := `FROM busybox +MAINTAINER foo +RUN echo 0 #layer0 +RUN echo 1 #layer1 +RUN echo 2 #layer2 +` + _, err := buildImage(image, dockerfile, false) + c.Assert(err, checker.IsNil) + + out, _ := dockerCmd(c, "history", "-q", image) + ids := strings.Split(out, "\n") + idToTag := ids[2] + + // Tag layer0 to "tmp2". + newTag := "tmp2" + dockerCmd(c, "tag", idToTag, newTag) + // Create a container based on "tmp1". + dockerCmd(c, "run", "-d", image, "true") + + // See if the "tmp2" can be untagged. + out, _ = dockerCmd(c, "rmi", newTag) + // Expected 1 untagged entry + c.Assert(strings.Count(out, "Untagged: "), checker.Equals, 1, check.Commentf("out: %s", out)) + + // Now let's add the tag again and create a container based on it. + dockerCmd(c, "tag", idToTag, newTag) + out, _ = dockerCmd(c, "run", "-d", newTag, "true") + cid := strings.TrimSpace(out) + + // At this point we have 2 containers, one based on layer2 and another based on layer0. + // Try to untag "tmp2" without the -f flag. + out, _, err = dockerCmdWithError("rmi", newTag) + // should not be untagged without the -f flag + c.Assert(err, checker.NotNil) + c.Assert(out, checker.Contains, cid[:12]) + c.Assert(out, checker.Contains, "(must force)") + + // Add the -f flag and test again. + out, _ = dockerCmd(c, "rmi", "-f", newTag) + // should be allowed to untag with the -f flag + c.Assert(out, checker.Contains, fmt.Sprintf("Untagged: %s:latest", newTag)) +} + +func (*DockerSuite) TestRmiParentImageFail(c *check.C) { + parent := inspectField(c, "busybox", "Parent") + out, _, err := dockerCmdWithError("rmi", parent) + c.Assert(err, check.NotNil) + if !strings.Contains(out, "image has dependent child images") { + c.Fatalf("rmi should have failed because it's a parent image, got %s", out) + } +} + +func (s *DockerSuite) TestRmiWithParentInUse(c *check.C) { + // TODO Windows. There is a bug either in Windows TP4, or the TP4 compatible + // docker which means this test fails. It has been verified to have been fixed + // in TP5 and docker/master, hence enable it once CI switch to TP5. + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "create", "busybox") + cID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "commit", cID) + imageID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "create", imageID) + cID = strings.TrimSpace(out) + + out, _ = dockerCmd(c, "commit", cID) + imageID = strings.TrimSpace(out) + + dockerCmd(c, "rmi", imageID) +} + +// #18873 +func (s *DockerSuite) TestRmiByIDHardConflict(c *check.C) { + // TODO Windows CI. This will work on a TP5 compatible docker which + // has content addressibility fixes. Do not run this on TP4 as it + // will end up deleting the busybox image causing subsequent tests to fail. + testRequires(c, DaemonIsLinux) + dockerCmd(c, "create", "busybox") + + imgID := inspectField(c, "busybox:latest", "Id") + + _, _, err := dockerCmdWithError("rmi", imgID[:12]) + c.Assert(err, checker.NotNil) + + // check that tag was not removed + imgID2 := inspectField(c, "busybox:latest", "Id") + c.Assert(imgID, checker.Equals, imgID2) +} diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go new file mode 100644 index 00000000..6e45fca9 --- /dev/null +++ b/integration-cli/docker_cli_run_test.go @@ -0,0 +1,4311 @@ +package main + +import ( + "bufio" + "bytes" + "fmt" + "io/ioutil" + "net" + "os" + "os/exec" + "path" + "path/filepath" + "reflect" + "regexp" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/mount" + "github.com/docker/docker/pkg/stringutils" + "github.com/docker/docker/runconfig" + "github.com/docker/go-connections/nat" + "github.com/docker/libnetwork/netutils" + "github.com/docker/libnetwork/resolvconf" + "github.com/go-check/check" + libcontainerUser "github.com/opencontainers/runc/libcontainer/user" +) + +// "test123" should be printed by docker run +func (s *DockerSuite) TestRunEchoStdout(c *check.C) { + out, _ := dockerCmd(c, "run", "busybox", "echo", "test123") + if out != "test123\n" { + c.Fatalf("container should've printed 'test123', got '%s'", out) + } +} + +// "test" should be printed +func (s *DockerSuite) TestRunEchoNamedContainer(c *check.C) { + out, _ := dockerCmd(c, "run", "--name", "testfoonamedcontainer", "busybox", "echo", "test") + if out != "test\n" { + c.Errorf("container should've printed 'test'") + } +} + +// docker run should not leak file descriptors. This test relies on Unix +// specific functionality and cannot run on Windows. +func (s *DockerSuite) TestRunLeakyFileDescriptors(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "busybox", "ls", "-C", "/proc/self/fd") + + // normally, we should only get 0, 1, and 2, but 3 gets created by "ls" when it does "opendir" on the "fd" directory + if out != "0 1 2 3\n" { + c.Errorf("container should've printed '0 1 2 3', not: %s", out) + } +} + +// it should be possible to lookup Google DNS +// this will fail when Internet access is unavailable +func (s *DockerSuite) TestRunLookupGoogleDns(c *check.C) { + testRequires(c, Network, NotArm) + image := DefaultImage + if daemonPlatform == "windows" { + // nslookup isn't present in Windows busybox. Is built-in. + image = WindowsBaseImage + } + dockerCmd(c, "run", image, "nslookup", "google.com") +} + +// the exit code should be 0 +func (s *DockerSuite) TestRunExitCodeZero(c *check.C) { + dockerCmd(c, "run", "busybox", "true") +} + +// the exit code should be 1 +func (s *DockerSuite) TestRunExitCodeOne(c *check.C) { + _, exitCode, err := dockerCmdWithError("run", "busybox", "false") + if err != nil && !strings.Contains("exit status 1", fmt.Sprintf("%s", err)) { + c.Fatal(err) + } + if exitCode != 1 { + c.Errorf("container should've exited with exit code 1. Got %d", exitCode) + } +} + +// it should be possible to pipe in data via stdin to a process running in a container +func (s *DockerSuite) TestRunStdinPipe(c *check.C) { + // TODO Windows: This needs some work to make compatible. + testRequires(c, DaemonIsLinux) + runCmd := exec.Command(dockerBinary, "run", "-i", "-a", "stdin", "busybox", "cat") + runCmd.Stdin = strings.NewReader("blahblah") + out, _, _, err := runCommandWithStdoutStderr(runCmd) + if err != nil { + c.Fatalf("failed to run container: %v, output: %q", err, out) + } + + out = strings.TrimSpace(out) + dockerCmd(c, "wait", out) + + logsOut, _ := dockerCmd(c, "logs", out) + + containerLogs := strings.TrimSpace(logsOut) + if containerLogs != "blahblah" { + c.Errorf("logs didn't print the container's logs %s", containerLogs) + } + + dockerCmd(c, "rm", out) +} + +// the container's ID should be printed when starting a container in detached mode +func (s *DockerSuite) TestRunDetachedContainerIDPrinting(c *check.C) { + out, _ := dockerCmd(c, "run", "-d", "busybox", "true") + + out = strings.TrimSpace(out) + dockerCmd(c, "wait", out) + + rmOut, _ := dockerCmd(c, "rm", out) + + rmOut = strings.TrimSpace(rmOut) + if rmOut != out { + c.Errorf("rm didn't print the container ID %s %s", out, rmOut) + } +} + +// the working directory should be set correctly +func (s *DockerSuite) TestRunWorkingDirectory(c *check.C) { + // TODO Windows: There's a Windows bug stopping this from working. + testRequires(c, DaemonIsLinux) + dir := "/root" + image := "busybox" + if daemonPlatform == "windows" { + dir = `/windows` + image = WindowsBaseImage + } + + // First with -w + out, _ := dockerCmd(c, "run", "-w", dir, image, "pwd") + out = strings.TrimSpace(out) + if out != dir { + c.Errorf("-w failed to set working directory") + } + + // Then with --workdir + out, _ = dockerCmd(c, "run", "--workdir", dir, image, "pwd") + out = strings.TrimSpace(out) + if out != dir { + c.Errorf("--workdir failed to set working directory") + } +} + +// pinging Google's DNS resolver should fail when we disable the networking +func (s *DockerSuite) TestRunWithoutNetworking(c *check.C) { + count := "-c" + image := "busybox" + if daemonPlatform == "windows" { + count = "-n" + image = WindowsBaseImage + } + + // First using the long form --net + out, exitCode, err := dockerCmdWithError("run", "--net=none", image, "ping", count, "1", "8.8.8.8") + if err != nil && exitCode != 1 { + c.Fatal(out, err) + } + if exitCode != 1 { + c.Errorf("--net=none should've disabled the network; the container shouldn't have been able to ping 8.8.8.8") + } +} + +//test --link use container name to link target +func (s *DockerSuite) TestRunLinksContainerWithContainerName(c *check.C) { + // TODO Windows: This test cannot run on a Windows daemon as the networking + // settings are not populated back yet on inspect. + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "-i", "-t", "-d", "--name", "parent", "busybox") + + ip := inspectField(c, "parent", "NetworkSettings.Networks.bridge.IPAddress") + + out, _ := dockerCmd(c, "run", "--link", "parent:test", "busybox", "/bin/cat", "/etc/hosts") + if !strings.Contains(out, ip+" test") { + c.Fatalf("use a container name to link target failed") + } +} + +//test --link use container id to link target +func (s *DockerSuite) TestRunLinksContainerWithContainerId(c *check.C) { + // TODO Windows: This test cannot run on a Windows daemon as the networking + // settings are not populated back yet on inspect. + testRequires(c, DaemonIsLinux) + cID, _ := dockerCmd(c, "run", "-i", "-t", "-d", "busybox") + + cID = strings.TrimSpace(cID) + ip := inspectField(c, cID, "NetworkSettings.Networks.bridge.IPAddress") + + out, _ := dockerCmd(c, "run", "--link", cID+":test", "busybox", "/bin/cat", "/etc/hosts") + if !strings.Contains(out, ip+" test") { + c.Fatalf("use a container id to link target failed") + } +} + +func (s *DockerSuite) TestUserDefinedNetworkLinks(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace, NotArm) + dockerCmd(c, "network", "create", "-d", "bridge", "udlinkNet") + + dockerCmd(c, "run", "-d", "--net=udlinkNet", "--name=first", "busybox", "top") + c.Assert(waitRun("first"), check.IsNil) + + // run a container in user-defined network udlinkNet with a link for an existing container + // and a link for a container that doesn't exist + dockerCmd(c, "run", "-d", "--net=udlinkNet", "--name=second", "--link=first:foo", + "--link=third:bar", "busybox", "top") + c.Assert(waitRun("second"), check.IsNil) + + // ping to first and its alias foo must succeed + _, _, err := dockerCmdWithError("exec", "second", "ping", "-c", "1", "first") + c.Assert(err, check.IsNil) + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "foo") + c.Assert(err, check.IsNil) + + // ping to third and its alias must fail + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "third") + c.Assert(err, check.NotNil) + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "bar") + c.Assert(err, check.NotNil) + + // start third container now + dockerCmd(c, "run", "-d", "--net=udlinkNet", "--name=third", "busybox", "top") + c.Assert(waitRun("third"), check.IsNil) + + // ping to third and its alias must succeed now + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "third") + c.Assert(err, check.IsNil) + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "bar") + c.Assert(err, check.IsNil) +} + +func (s *DockerSuite) TestUserDefinedNetworkLinksWithRestart(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace, NotArm) + dockerCmd(c, "network", "create", "-d", "bridge", "udlinkNet") + + dockerCmd(c, "run", "-d", "--net=udlinkNet", "--name=first", "busybox", "top") + c.Assert(waitRun("first"), check.IsNil) + + dockerCmd(c, "run", "-d", "--net=udlinkNet", "--name=second", "--link=first:foo", + "busybox", "top") + c.Assert(waitRun("second"), check.IsNil) + + // ping to first and its alias foo must succeed + _, _, err := dockerCmdWithError("exec", "second", "ping", "-c", "1", "first") + c.Assert(err, check.IsNil) + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "foo") + c.Assert(err, check.IsNil) + + // Restart first container + dockerCmd(c, "restart", "first") + c.Assert(waitRun("first"), check.IsNil) + + // ping to first and its alias foo must still succeed + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "first") + c.Assert(err, check.IsNil) + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "foo") + c.Assert(err, check.IsNil) + + // Restart second container + dockerCmd(c, "restart", "second") + c.Assert(waitRun("second"), check.IsNil) + + // ping to first and its alias foo must still succeed + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "first") + c.Assert(err, check.IsNil) + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "foo") + c.Assert(err, check.IsNil) +} + +func (s *DockerSuite) TestRunWithNetAliasOnDefaultNetworks(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace, NotArm) + + defaults := []string{"bridge", "host", "none"} + for _, net := range defaults { + out, _, err := dockerCmdWithError("run", "-d", "--net", net, "--net-alias", "alias_"+net, "busybox", "top") + c.Assert(err, checker.NotNil) + c.Assert(out, checker.Contains, runconfig.ErrUnsupportedNetworkAndAlias.Error()) + } +} + +func (s *DockerSuite) TestUserDefinedNetworkAlias(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace, NotArm) + dockerCmd(c, "network", "create", "-d", "bridge", "net1") + + dockerCmd(c, "run", "-d", "--net=net1", "--name=first", "--net-alias=foo1", "--net-alias=foo2", "busybox", "top") + c.Assert(waitRun("first"), check.IsNil) + + dockerCmd(c, "run", "-d", "--net=net1", "--name=second", "busybox", "top") + c.Assert(waitRun("second"), check.IsNil) + + // ping to first and its network-scoped aliases + _, _, err := dockerCmdWithError("exec", "second", "ping", "-c", "1", "first") + c.Assert(err, check.IsNil) + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "foo1") + c.Assert(err, check.IsNil) + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "foo2") + c.Assert(err, check.IsNil) + + // Restart first container + dockerCmd(c, "restart", "first") + c.Assert(waitRun("first"), check.IsNil) + + // ping to first and its network-scoped aliases must succeed + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "first") + c.Assert(err, check.IsNil) + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "foo1") + c.Assert(err, check.IsNil) + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "foo2") + c.Assert(err, check.IsNil) +} + +// Issue 9677. +func (s *DockerSuite) TestRunWithDaemonFlags(c *check.C) { + out, _, err := dockerCmdWithError("--exec-opt", "foo=bar", "run", "-i", "busybox", "true") + if err != nil { + if !strings.Contains(out, "flag provided but not defined: --exec-opt") { // no daemon (client-only) + c.Fatal(err, out) + } + } +} + +// Regression test for #4979 +func (s *DockerSuite) TestRunWithVolumesFromExited(c *check.C) { + + var ( + out string + exitCode int + ) + + // Create a file in a volume + if daemonPlatform == "windows" { + out, exitCode = dockerCmd(c, "run", "--name", "test-data", "--volume", `c:\some\dir`, WindowsBaseImage, `cmd /c echo hello > c:\some\dir\file`) + } else { + out, exitCode = dockerCmd(c, "run", "--name", "test-data", "--volume", "/some/dir", "busybox", "touch", "/some/dir/file") + } + if exitCode != 0 { + c.Fatal("1", out, exitCode) + } + + // Read the file from another container using --volumes-from to access the volume in the second container + if daemonPlatform == "windows" { + out, exitCode = dockerCmd(c, "run", "--volumes-from", "test-data", WindowsBaseImage, `cmd /c type c:\some\dir\file`) + } else { + out, exitCode = dockerCmd(c, "run", "--volumes-from", "test-data", "busybox", "cat", "/some/dir/file") + } + if exitCode != 0 { + c.Fatal("2", out, exitCode) + } +} + +// Volume path is a symlink which also exists on the host, and the host side is a file not a dir +// But the volume call is just a normal volume, not a bind mount +func (s *DockerSuite) TestRunCreateVolumesInSymlinkDir(c *check.C) { + var ( + dockerFile string + containerPath string + cmd string + ) + testRequires(c, SameHostDaemon) + name := "test-volume-symlink" + + dir, err := ioutil.TempDir("", name) + if err != nil { + c.Fatal(err) + } + defer os.RemoveAll(dir) + + // In the case of Windows to Windows CI, if the machine is setup so that + // the temp directory is not the C: drive, this test is invalid and will + // not work. + if daemonPlatform == "windows" && strings.ToLower(dir[:1]) != "c" { + c.Skip("Requires TEMP to point to C: drive") + } + + f, err := os.OpenFile(filepath.Join(dir, "test"), os.O_CREATE, 0700) + if err != nil { + c.Fatal(err) + } + f.Close() + + if daemonPlatform == "windows" { + dockerFile = fmt.Sprintf("FROM %s\nRUN mkdir %s\nRUN mklink /D c:\\test %s", WindowsBaseImage, dir, dir) + containerPath = `c:\test\test` + cmd = "tasklist" + } else { + dockerFile = fmt.Sprintf("FROM busybox\nRUN mkdir -p %s\nRUN ln -s %s /test", dir, dir) + containerPath = "/test/test" + cmd = "true" + } + if _, err := buildImage(name, dockerFile, false); err != nil { + c.Fatal(err) + } + + dockerCmd(c, "run", "-v", containerPath, name, cmd) +} + +// Volume path is a symlink in the container +func (s *DockerSuite) TestRunCreateVolumesInSymlinkDir2(c *check.C) { + var ( + dockerFile string + containerPath string + cmd string + ) + testRequires(c, SameHostDaemon) + name := "test-volume-symlink2" + + if daemonPlatform == "windows" { + dockerFile = fmt.Sprintf("FROM %s\nRUN mkdir c:\\%s\nRUN mklink /D c:\\test c:\\%s", WindowsBaseImage, name, name) + containerPath = `c:\test\test` + cmd = "tasklist" + } else { + dockerFile = fmt.Sprintf("FROM busybox\nRUN mkdir -p /%s\nRUN ln -s /%s /test", name, name) + containerPath = "/test/test" + cmd = "true" + } + if _, err := buildImage(name, dockerFile, false); err != nil { + c.Fatal(err) + } + + dockerCmd(c, "run", "-v", containerPath, name, cmd) +} + +func (s *DockerSuite) TestRunVolumesMountedAsReadonly(c *check.C) { + // TODO Windows (Post TP4): This test cannot run on a Windows daemon as + // Windows does not support read-only bind mounts. + testRequires(c, DaemonIsLinux) + if _, code, err := dockerCmdWithError("run", "-v", "/test:/test:ro", "busybox", "touch", "/test/somefile"); err == nil || code == 0 { + c.Fatalf("run should fail because volume is ro: exit code %d", code) + } +} + +func (s *DockerSuite) TestRunVolumesFromInReadonlyModeFails(c *check.C) { + // TODO Windows (Post TP4): This test cannot run on a Windows daemon as + // Windows does not support read-only bind mounts. Modified for when ro is supported. + testRequires(c, DaemonIsLinux) + var ( + volumeDir string + fileInVol string + ) + if daemonPlatform == "windows" { + volumeDir = `c:/test` // Forward-slash as using busybox + fileInVol = `c:/test/file` + } else { + testRequires(c, DaemonIsLinux) + volumeDir = "/test" + fileInVol = `/test/file` + } + dockerCmd(c, "run", "--name", "parent", "-v", volumeDir, "busybox", "true") + + if _, code, err := dockerCmdWithError("run", "--volumes-from", "parent:ro", "busybox", "touch", fileInVol); err == nil || code == 0 { + c.Fatalf("run should fail because volume is ro: exit code %d", code) + } +} + +// Regression test for #1201 +func (s *DockerSuite) TestRunVolumesFromInReadWriteMode(c *check.C) { + var ( + volumeDir string + fileInVol string + ) + if daemonPlatform == "windows" { + volumeDir = `c:/test` // Forward-slash as using busybox + fileInVol = `c:/test/file` + } else { + volumeDir = "/test" + fileInVol = "/test/file" + } + + dockerCmd(c, "run", "--name", "parent", "-v", volumeDir, "busybox", "true") + dockerCmd(c, "run", "--volumes-from", "parent:rw", "busybox", "touch", fileInVol) + + if out, _, err := dockerCmdWithError("run", "--volumes-from", "parent:bar", "busybox", "touch", fileInVol); err == nil || !strings.Contains(out, `invalid mode: bar`) { + c.Fatalf("running --volumes-from parent:bar should have failed with invalid mode: %q", out) + } + + dockerCmd(c, "run", "--volumes-from", "parent", "busybox", "touch", fileInVol) +} + +func (s *DockerSuite) TestVolumesFromGetsProperMode(c *check.C) { + // TODO Windows: This test cannot yet run on a Windows daemon as Windows does + // not support read-only bind mounts as at TP4 + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "--name", "parent", "-v", "/test:/test:ro", "busybox", "true") + + // Expect this "rw" mode to be be ignored since the inherited volume is "ro" + if _, _, err := dockerCmdWithError("run", "--volumes-from", "parent:rw", "busybox", "touch", "/test/file"); err == nil { + c.Fatal("Expected volumes-from to inherit read-only volume even when passing in `rw`") + } + + dockerCmd(c, "run", "--name", "parent2", "-v", "/test:/test:ro", "busybox", "true") + + // Expect this to be read-only since both are "ro" + if _, _, err := dockerCmdWithError("run", "--volumes-from", "parent2:ro", "busybox", "touch", "/test/file"); err == nil { + c.Fatal("Expected volumes-from to inherit read-only volume even when passing in `ro`") + } +} + +// Test for GH#10618 +func (s *DockerSuite) TestRunNoDupVolumes(c *check.C) { + path1 := randomTmpDirPath("test1", daemonPlatform) + path2 := randomTmpDirPath("test2", daemonPlatform) + + someplace := ":/someplace" + if daemonPlatform == "windows" { + // Windows requires that the source directory exists before calling HCS + testRequires(c, SameHostDaemon) + someplace = `:c:\someplace` + if err := os.MkdirAll(path1, 0755); err != nil { + c.Fatalf("Failed to create %s: %q", path1, err) + } + defer os.RemoveAll(path1) + if err := os.MkdirAll(path2, 0755); err != nil { + c.Fatalf("Failed to create %s: %q", path1, err) + } + defer os.RemoveAll(path2) + } + mountstr1 := path1 + someplace + mountstr2 := path2 + someplace + + if out, _, err := dockerCmdWithError("run", "-v", mountstr1, "-v", mountstr2, "busybox", "true"); err == nil { + c.Fatal("Expected error about duplicate mount definitions") + } else { + if !strings.Contains(out, "Duplicate mount point") { + c.Fatalf("Expected 'duplicate mount point' error, got %v", out) + } + } +} + +// Test for #1351 +func (s *DockerSuite) TestRunApplyVolumesFromBeforeVolumes(c *check.C) { + prefix := "" + if daemonPlatform == "windows" { + prefix = `c:` + } + dockerCmd(c, "run", "--name", "parent", "-v", prefix+"/test", "busybox", "touch", prefix+"/test/foo") + dockerCmd(c, "run", "--volumes-from", "parent", "-v", prefix+"/test", "busybox", "cat", prefix+"/test/foo") +} + +func (s *DockerSuite) TestRunMultipleVolumesFrom(c *check.C) { + prefix := "" + if daemonPlatform == "windows" { + prefix = `c:` + } + dockerCmd(c, "run", "--name", "parent1", "-v", prefix+"/test", "busybox", "touch", prefix+"/test/foo") + dockerCmd(c, "run", "--name", "parent2", "-v", prefix+"/other", "busybox", "touch", prefix+"/other/bar") + dockerCmd(c, "run", "--volumes-from", "parent1", "--volumes-from", "parent2", "busybox", "sh", "-c", "cat /test/foo && cat /other/bar") +} + +// this tests verifies the ID format for the container +func (s *DockerSuite) TestRunVerifyContainerID(c *check.C) { + out, exit, err := dockerCmdWithError("run", "-d", "busybox", "true") + if err != nil { + c.Fatal(err) + } + if exit != 0 { + c.Fatalf("expected exit code 0 received %d", exit) + } + + match, err := regexp.MatchString("^[0-9a-f]{64}$", strings.TrimSuffix(out, "\n")) + if err != nil { + c.Fatal(err) + } + if !match { + c.Fatalf("Invalid container ID: %s", out) + } +} + +// Test that creating a container with a volume doesn't crash. Regression test for #995. +func (s *DockerSuite) TestRunCreateVolume(c *check.C) { + prefix := "" + if daemonPlatform == "windows" { + prefix = `c:` + } + dockerCmd(c, "run", "-v", prefix+"/var/lib/data", "busybox", "true") +} + +// Test that creating a volume with a symlink in its path works correctly. Test for #5152. +// Note that this bug happens only with symlinks with a target that starts with '/'. +func (s *DockerSuite) TestRunCreateVolumeWithSymlink(c *check.C) { + // Cannot run on Windows as relies on Linux-specific functionality (sh -c mount...) + testRequires(c, DaemonIsLinux) + image := "docker-test-createvolumewithsymlink" + + buildCmd := exec.Command(dockerBinary, "build", "-t", image, "-") + buildCmd.Stdin = strings.NewReader(`FROM busybox + RUN ln -s home /bar`) + buildCmd.Dir = workingDirectory + err := buildCmd.Run() + if err != nil { + c.Fatalf("could not build '%s': %v", image, err) + } + + _, exitCode, err := dockerCmdWithError("run", "-v", "/bar/foo", "--name", "test-createvolumewithsymlink", image, "sh", "-c", "mount | grep -q /home/foo") + if err != nil || exitCode != 0 { + c.Fatalf("[run] err: %v, exitcode: %d", err, exitCode) + } + + volPath, err := inspectMountSourceField("test-createvolumewithsymlink", "/bar/foo") + c.Assert(err, checker.IsNil) + + _, exitCode, err = dockerCmdWithError("rm", "-v", "test-createvolumewithsymlink") + if err != nil || exitCode != 0 { + c.Fatalf("[rm] err: %v, exitcode: %d", err, exitCode) + } + + _, err = os.Stat(volPath) + if !os.IsNotExist(err) { + c.Fatalf("[open] (expecting 'file does not exist' error) err: %v, volPath: %s", err, volPath) + } +} + +// Tests that a volume path that has a symlink exists in a container mounting it with `--volumes-from`. +func (s *DockerSuite) TestRunVolumesFromSymlinkPath(c *check.C) { + name := "docker-test-volumesfromsymlinkpath" + prefix := "" + dfContents := `FROM busybox + RUN ln -s home /foo + VOLUME ["/foo/bar"]` + + if daemonPlatform == "windows" { + prefix = `c:` + dfContents = `FROM ` + WindowsBaseImage + ` + RUN mkdir c:\home + RUN mklink /D c:\foo c:\home + VOLUME ["c:/foo/bar"] + ENTRYPOINT c:\windows\system32\cmd.exe` + } + + buildCmd := exec.Command(dockerBinary, "build", "-t", name, "-") + buildCmd.Stdin = strings.NewReader(dfContents) + buildCmd.Dir = workingDirectory + err := buildCmd.Run() + if err != nil { + c.Fatalf("could not build 'docker-test-volumesfromsymlinkpath': %v", err) + } + + out, exitCode, err := dockerCmdWithError("run", "--name", "test-volumesfromsymlinkpath", name) + if err != nil || exitCode != 0 { + c.Fatalf("[run] (volume) err: %v, exitcode: %d, out: %s", err, exitCode, out) + } + + _, exitCode, err = dockerCmdWithError("run", "--volumes-from", "test-volumesfromsymlinkpath", "busybox", "sh", "-c", "ls "+prefix+"/foo | grep -q bar") + if err != nil || exitCode != 0 { + c.Fatalf("[run] err: %v, exitcode: %d", err, exitCode) + } +} + +func (s *DockerSuite) TestRunExitCode(c *check.C) { + var ( + exit int + err error + ) + + _, exit, err = dockerCmdWithError("run", "busybox", "/bin/sh", "-c", "exit 72") + + if err == nil { + c.Fatal("should not have a non nil error") + } + if exit != 72 { + c.Fatalf("expected exit code 72 received %d", exit) + } +} + +func (s *DockerSuite) TestRunUserDefaults(c *check.C) { + expected := "uid=0(root) gid=0(root)" + if daemonPlatform == "windows" { + // TODO Windows: Remove this check once TP4 is no longer supported. + if windowsDaemonKV < 14250 { + expected = "uid=1000(SYSTEM) gid=1000(SYSTEM)" + } else { + expected = "uid=1000(ContainerAdministrator) gid=1000(ContainerAdministrator)" + } + } + out, _ := dockerCmd(c, "run", "busybox", "id") + if !strings.Contains(out, expected) { + c.Fatalf("expected '%s' got %s", expected, out) + } +} + +func (s *DockerSuite) TestRunUserByName(c *check.C) { + // TODO Windows: This test cannot run on a Windows daemon as Windows does + // not support the use of -u + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-u", "root", "busybox", "id") + if !strings.Contains(out, "uid=0(root) gid=0(root)") { + c.Fatalf("expected root user got %s", out) + } +} + +func (s *DockerSuite) TestRunUserByID(c *check.C) { + // TODO Windows: This test cannot run on a Windows daemon as Windows does + // not support the use of -u + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-u", "1", "busybox", "id") + if !strings.Contains(out, "uid=1(daemon) gid=1(daemon)") { + c.Fatalf("expected daemon user got %s", out) + } +} + +func (s *DockerSuite) TestRunUserByIDBig(c *check.C) { + // TODO Windows: This test cannot run on a Windows daemon as Windows does + // not support the use of -u + testRequires(c, DaemonIsLinux, NotArm) + out, _, err := dockerCmdWithError("run", "-u", "2147483648", "busybox", "id") + if err == nil { + c.Fatal("No error, but must be.", out) + } + if !strings.Contains(out, libcontainerUser.ErrRange.Error()) { + c.Fatalf("expected error about uids range, got %s", out) + } +} + +func (s *DockerSuite) TestRunUserByIDNegative(c *check.C) { + // TODO Windows: This test cannot run on a Windows daemon as Windows does + // not support the use of -u + testRequires(c, DaemonIsLinux) + out, _, err := dockerCmdWithError("run", "-u", "-1", "busybox", "id") + if err == nil { + c.Fatal("No error, but must be.", out) + } + if !strings.Contains(out, libcontainerUser.ErrRange.Error()) { + c.Fatalf("expected error about uids range, got %s", out) + } +} + +func (s *DockerSuite) TestRunUserByIDZero(c *check.C) { + // TODO Windows: This test cannot run on a Windows daemon as Windows does + // not support the use of -u + testRequires(c, DaemonIsLinux) + out, _, err := dockerCmdWithError("run", "-u", "0", "busybox", "id") + if err != nil { + c.Fatal(err, out) + } + if !strings.Contains(out, "uid=0(root) gid=0(root) groups=10(wheel)") { + c.Fatalf("expected daemon user got %s", out) + } +} + +func (s *DockerSuite) TestRunUserNotFound(c *check.C) { + // TODO Windows: This test cannot run on a Windows daemon as Windows does + // not support the use of -u + testRequires(c, DaemonIsLinux) + _, _, err := dockerCmdWithError("run", "-u", "notme", "busybox", "id") + if err == nil { + c.Fatal("unknown user should cause container to fail") + } +} + +func (s *DockerSuite) TestRunTwoConcurrentContainers(c *check.C) { + // TODO Windows. There are two bugs in TP4 which means this test cannot + // be reliably enabled. The first is a race condition where sometimes + // HCS CreateComputeSystem() will fail "Invalid class string". #4985252 and + // #4493430. + // + // The second, which is seen more readily by increasing the number of concurrent + // containers to 5 or more, is that CSRSS hangs. This may fixed in the TP4 ZDP. + // #4898773. + testRequires(c, DaemonIsLinux) + sleepTime := "2" + if daemonPlatform == "windows" { + sleepTime = "5" // Make more reliable on Windows + } + group := sync.WaitGroup{} + group.Add(2) + + errChan := make(chan error, 2) + for i := 0; i < 2; i++ { + go func() { + defer group.Done() + _, _, err := dockerCmdWithError("run", "busybox", "sleep", sleepTime) + errChan <- err + }() + } + + group.Wait() + close(errChan) + + for err := range errChan { + c.Assert(err, check.IsNil) + } +} + +func (s *DockerSuite) TestRunEnvironment(c *check.C) { + // TODO Windows: Environment handling is different between Linux and + // Windows and this test relies currently on unix functionality. + testRequires(c, DaemonIsLinux) + cmd := exec.Command(dockerBinary, "run", "-h", "testing", "-e=FALSE=true", "-e=TRUE", "-e=TRICKY", "-e=HOME=", "busybox", "env") + cmd.Env = append(os.Environ(), + "TRUE=false", + "TRICKY=tri\ncky\n", + ) + + out, _, err := runCommandWithOutput(cmd) + if err != nil { + c.Fatal(err, out) + } + + actualEnv := strings.Split(strings.TrimSpace(out), "\n") + sort.Strings(actualEnv) + + goodEnv := []string{ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "HOSTNAME=testing", + "FALSE=true", + "TRUE=false", + "TRICKY=tri", + "cky", + "", + "HOME=/root", + } + sort.Strings(goodEnv) + if len(goodEnv) != len(actualEnv) { + c.Fatalf("Wrong environment: should be %d variables, not: %q\n", len(goodEnv), strings.Join(actualEnv, ", ")) + } + for i := range goodEnv { + if actualEnv[i] != goodEnv[i] { + c.Fatalf("Wrong environment variable: should be %s, not %s", goodEnv[i], actualEnv[i]) + } + } +} + +func (s *DockerSuite) TestRunEnvironmentErase(c *check.C) { + // TODO Windows: Environment handling is different between Linux and + // Windows and this test relies currently on unix functionality. + testRequires(c, DaemonIsLinux) + + // Test to make sure that when we use -e on env vars that are + // not set in our local env that they're removed (if present) in + // the container + + cmd := exec.Command(dockerBinary, "run", "-e", "FOO", "-e", "HOSTNAME", "busybox", "env") + cmd.Env = appendBaseEnv(true) + + out, _, err := runCommandWithOutput(cmd) + if err != nil { + c.Fatal(err, out) + } + + actualEnv := strings.Split(strings.TrimSpace(out), "\n") + sort.Strings(actualEnv) + + goodEnv := []string{ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "HOME=/root", + } + sort.Strings(goodEnv) + if len(goodEnv) != len(actualEnv) { + c.Fatalf("Wrong environment: should be %d variables, not: %q\n", len(goodEnv), strings.Join(actualEnv, ", ")) + } + for i := range goodEnv { + if actualEnv[i] != goodEnv[i] { + c.Fatalf("Wrong environment variable: should be %s, not %s", goodEnv[i], actualEnv[i]) + } + } +} + +func (s *DockerSuite) TestRunEnvironmentOverride(c *check.C) { + // TODO Windows: Environment handling is different between Linux and + // Windows and this test relies currently on unix functionality. + testRequires(c, DaemonIsLinux) + + // Test to make sure that when we use -e on env vars that are + // already in the env that we're overriding them + + cmd := exec.Command(dockerBinary, "run", "-e", "HOSTNAME", "-e", "HOME=/root2", "busybox", "env") + cmd.Env = appendBaseEnv(true, "HOSTNAME=bar") + + out, _, err := runCommandWithOutput(cmd) + if err != nil { + c.Fatal(err, out) + } + + actualEnv := strings.Split(strings.TrimSpace(out), "\n") + sort.Strings(actualEnv) + + goodEnv := []string{ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "HOME=/root2", + "HOSTNAME=bar", + } + sort.Strings(goodEnv) + if len(goodEnv) != len(actualEnv) { + c.Fatalf("Wrong environment: should be %d variables, not: %q\n", len(goodEnv), strings.Join(actualEnv, ", ")) + } + for i := range goodEnv { + if actualEnv[i] != goodEnv[i] { + c.Fatalf("Wrong environment variable: should be %s, not %s", goodEnv[i], actualEnv[i]) + } + } +} + +func (s *DockerSuite) TestRunContainerNetwork(c *check.C) { + if daemonPlatform == "windows" { + // Windows busybox does not have ping. Use built in ping instead. + dockerCmd(c, "run", WindowsBaseImage, "ping", "-n", "1", "127.0.0.1") + } else { + dockerCmd(c, "run", "busybox", "ping", "-c", "1", "127.0.0.1") + } +} + +func (s *DockerSuite) TestRunNetHostNotAllowedWithLinks(c *check.C) { + // TODO Windows: This is Linux specific as --link is not supported and + // this will be deprecated in favor of container networking model. + testRequires(c, DaemonIsLinux, NotUserNamespace) + dockerCmd(c, "run", "--name", "linked", "busybox", "true") + + _, _, err := dockerCmdWithError("run", "--net=host", "--link", "linked:linked", "busybox", "true") + if err == nil { + c.Fatal("Expected error") + } +} + +// #7851 hostname outside container shows FQDN, inside only shortname +// For testing purposes it is not required to set host's hostname directly +// and use "--net=host" (as the original issue submitter did), as the same +// codepath is executed with "docker run -h ". Both were manually +// tested, but this testcase takes the simpler path of using "run -h .." +func (s *DockerSuite) TestRunFullHostnameSet(c *check.C) { + // TODO Windows: -h is not yet functional. + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-h", "foo.bar.baz", "busybox", "hostname") + if actual := strings.Trim(out, "\r\n"); actual != "foo.bar.baz" { + c.Fatalf("expected hostname 'foo.bar.baz', received %s", actual) + } +} + +func (s *DockerSuite) TestRunPrivilegedCanMknod(c *check.C) { + // Not applicable for Windows as Windows daemon does not support + // the concept of --privileged, and mknod is a Unix concept. + testRequires(c, DaemonIsLinux, NotUserNamespace) + out, _ := dockerCmd(c, "run", "--privileged", "busybox", "sh", "-c", "mknod /tmp/sda b 8 0 && echo ok") + if actual := strings.Trim(out, "\r\n"); actual != "ok" { + c.Fatalf("expected output ok received %s", actual) + } +} + +func (s *DockerSuite) TestRunUnprivilegedCanMknod(c *check.C) { + // Not applicable for Windows as Windows daemon does not support + // the concept of --privileged, and mknod is a Unix concept. + testRequires(c, DaemonIsLinux, NotUserNamespace) + out, _ := dockerCmd(c, "run", "busybox", "sh", "-c", "mknod /tmp/sda b 8 0 && echo ok") + if actual := strings.Trim(out, "\r\n"); actual != "ok" { + c.Fatalf("expected output ok received %s", actual) + } +} + +func (s *DockerSuite) TestRunCapDropInvalid(c *check.C) { + // Not applicable for Windows as there is no concept of --cap-drop + testRequires(c, DaemonIsLinux) + out, _, err := dockerCmdWithError("run", "--cap-drop=CHPASS", "busybox", "ls") + if err == nil { + c.Fatal(err, out) + } +} + +func (s *DockerSuite) TestRunCapDropCannotMknod(c *check.C) { + // Not applicable for Windows as there is no concept of --cap-drop or mknod + testRequires(c, DaemonIsLinux) + out, _, err := dockerCmdWithError("run", "--cap-drop=MKNOD", "busybox", "sh", "-c", "mknod /tmp/sda b 8 0 && echo ok") + + if err == nil { + c.Fatal(err, out) + } + if actual := strings.Trim(out, "\r\n"); actual == "ok" { + c.Fatalf("expected output not ok received %s", actual) + } +} + +func (s *DockerSuite) TestRunCapDropCannotMknodLowerCase(c *check.C) { + // Not applicable for Windows as there is no concept of --cap-drop or mknod + testRequires(c, DaemonIsLinux) + out, _, err := dockerCmdWithError("run", "--cap-drop=mknod", "busybox", "sh", "-c", "mknod /tmp/sda b 8 0 && echo ok") + + if err == nil { + c.Fatal(err, out) + } + if actual := strings.Trim(out, "\r\n"); actual == "ok" { + c.Fatalf("expected output not ok received %s", actual) + } +} + +func (s *DockerSuite) TestRunCapDropALLCannotMknod(c *check.C) { + // Not applicable for Windows as there is no concept of --cap-drop or mknod + testRequires(c, DaemonIsLinux) + out, _, err := dockerCmdWithError("run", "--cap-drop=ALL", "--cap-add=SETGID", "busybox", "sh", "-c", "mknod /tmp/sda b 8 0 && echo ok") + if err == nil { + c.Fatal(err, out) + } + if actual := strings.Trim(out, "\r\n"); actual == "ok" { + c.Fatalf("expected output not ok received %s", actual) + } +} + +func (s *DockerSuite) TestRunCapDropALLAddMknodCanMknod(c *check.C) { + // Not applicable for Windows as there is no concept of --cap-drop or mknod + testRequires(c, DaemonIsLinux, NotUserNamespace) + out, _ := dockerCmd(c, "run", "--cap-drop=ALL", "--cap-add=MKNOD", "--cap-add=SETGID", "busybox", "sh", "-c", "mknod /tmp/sda b 8 0 && echo ok") + + if actual := strings.Trim(out, "\r\n"); actual != "ok" { + c.Fatalf("expected output ok received %s", actual) + } +} + +func (s *DockerSuite) TestRunCapAddInvalid(c *check.C) { + // Not applicable for Windows as there is no concept of --cap-add + testRequires(c, DaemonIsLinux) + out, _, err := dockerCmdWithError("run", "--cap-add=CHPASS", "busybox", "ls") + if err == nil { + c.Fatal(err, out) + } +} + +func (s *DockerSuite) TestRunCapAddCanDownInterface(c *check.C) { + // Not applicable for Windows as there is no concept of --cap-add + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "--cap-add=NET_ADMIN", "busybox", "sh", "-c", "ip link set eth0 down && echo ok") + + if actual := strings.Trim(out, "\r\n"); actual != "ok" { + c.Fatalf("expected output ok received %s", actual) + } +} + +func (s *DockerSuite) TestRunCapAddALLCanDownInterface(c *check.C) { + // Not applicable for Windows as there is no concept of --cap-add + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "--cap-add=ALL", "busybox", "sh", "-c", "ip link set eth0 down && echo ok") + + if actual := strings.Trim(out, "\r\n"); actual != "ok" { + c.Fatalf("expected output ok received %s", actual) + } +} + +func (s *DockerSuite) TestRunCapAddALLDropNetAdminCanDownInterface(c *check.C) { + // Not applicable for Windows as there is no concept of --cap-add + testRequires(c, DaemonIsLinux) + out, _, err := dockerCmdWithError("run", "--cap-add=ALL", "--cap-drop=NET_ADMIN", "busybox", "sh", "-c", "ip link set eth0 down && echo ok") + if err == nil { + c.Fatal(err, out) + } + if actual := strings.Trim(out, "\r\n"); actual == "ok" { + c.Fatalf("expected output not ok received %s", actual) + } +} + +func (s *DockerSuite) TestRunGroupAdd(c *check.C) { + // Not applicable for Windows as there is no concept of --group-add + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "--group-add=audio", "--group-add=staff", "--group-add=777", "busybox", "sh", "-c", "id") + + groupsList := "uid=0(root) gid=0(root) groups=10(wheel),29(audio),50(staff),777" + if actual := strings.Trim(out, "\r\n"); actual != groupsList { + c.Fatalf("expected output %s received %s", groupsList, actual) + } +} + +func (s *DockerSuite) TestRunPrivilegedCanMount(c *check.C) { + // Not applicable for Windows as there is no concept of --privileged + testRequires(c, DaemonIsLinux, NotUserNamespace) + out, _ := dockerCmd(c, "run", "--privileged", "busybox", "sh", "-c", "mount -t tmpfs none /tmp && echo ok") + + if actual := strings.Trim(out, "\r\n"); actual != "ok" { + c.Fatalf("expected output ok received %s", actual) + } +} + +func (s *DockerSuite) TestRunUnprivilegedCannotMount(c *check.C) { + // Not applicable for Windows as there is no concept of unprivileged + testRequires(c, DaemonIsLinux) + out, _, err := dockerCmdWithError("run", "busybox", "sh", "-c", "mount -t tmpfs none /tmp && echo ok") + + if err == nil { + c.Fatal(err, out) + } + if actual := strings.Trim(out, "\r\n"); actual == "ok" { + c.Fatalf("expected output not ok received %s", actual) + } +} + +func (s *DockerSuite) TestRunSysNotWritableInNonPrivilegedContainers(c *check.C) { + // Not applicable for Windows as there is no concept of unprivileged + testRequires(c, DaemonIsLinux, NotArm) + if _, code, err := dockerCmdWithError("run", "busybox", "touch", "/sys/kernel/profiling"); err == nil || code == 0 { + c.Fatal("sys should not be writable in a non privileged container") + } +} + +func (s *DockerSuite) TestRunSysWritableInPrivilegedContainers(c *check.C) { + // Not applicable for Windows as there is no concept of unprivileged + testRequires(c, DaemonIsLinux, NotUserNamespace, NotArm) + if _, code, err := dockerCmdWithError("run", "--privileged", "busybox", "touch", "/sys/kernel/profiling"); err != nil || code != 0 { + c.Fatalf("sys should be writable in privileged container") + } +} + +func (s *DockerSuite) TestRunProcNotWritableInNonPrivilegedContainers(c *check.C) { + // Not applicable for Windows as there is no concept of unprivileged + testRequires(c, DaemonIsLinux) + if _, code, err := dockerCmdWithError("run", "busybox", "touch", "/proc/sysrq-trigger"); err == nil || code == 0 { + c.Fatal("proc should not be writable in a non privileged container") + } +} + +func (s *DockerSuite) TestRunProcWritableInPrivilegedContainers(c *check.C) { + // Not applicable for Windows as there is no concept of --privileged + testRequires(c, DaemonIsLinux, NotUserNamespace) + if _, code := dockerCmd(c, "run", "--privileged", "busybox", "sh", "-c", "touch /proc/sysrq-trigger"); code != 0 { + c.Fatalf("proc should be writable in privileged container") + } +} + +func (s *DockerSuite) TestRunDeviceNumbers(c *check.C) { + // Not applicable on Windows as /dev/ is a Unix specific concept + // TODO: NotUserNamespace could be removed here if "root" "root" is replaced w user + testRequires(c, DaemonIsLinux, NotUserNamespace) + out, _ := dockerCmd(c, "run", "busybox", "sh", "-c", "ls -l /dev/null") + deviceLineFields := strings.Fields(out) + deviceLineFields[6] = "" + deviceLineFields[7] = "" + deviceLineFields[8] = "" + expected := []string{"crw-rw-rw-", "1", "root", "root", "1,", "3", "", "", "", "/dev/null"} + + if !(reflect.DeepEqual(deviceLineFields, expected)) { + c.Fatalf("expected output\ncrw-rw-rw- 1 root root 1, 3 May 24 13:29 /dev/null\n received\n %s\n", out) + } +} + +func (s *DockerSuite) TestRunThatCharacterDevicesActLikeCharacterDevices(c *check.C) { + // Not applicable on Windows as /dev/ is a Unix specific concept + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "busybox", "sh", "-c", "dd if=/dev/zero of=/zero bs=1k count=5 2> /dev/null ; du -h /zero") + if actual := strings.Trim(out, "\r\n"); actual[0] == '0' { + c.Fatalf("expected a new file called /zero to be create that is greater than 0 bytes long, but du says: %s", actual) + } +} + +func (s *DockerSuite) TestRunUnprivilegedWithChroot(c *check.C) { + // Not applicable on Windows as it does not support chroot + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "busybox", "chroot", "/", "true") +} + +func (s *DockerSuite) TestRunAddingOptionalDevices(c *check.C) { + // Not applicable on Windows as Windows does not support --device + testRequires(c, DaemonIsLinux, NotUserNamespace) + out, _ := dockerCmd(c, "run", "--device", "/dev/zero:/dev/nulo", "busybox", "sh", "-c", "ls /dev/nulo") + if actual := strings.Trim(out, "\r\n"); actual != "/dev/nulo" { + c.Fatalf("expected output /dev/nulo, received %s", actual) + } +} + +func (s *DockerSuite) TestRunAddingOptionalDevicesNoSrc(c *check.C) { + // Not applicable on Windows as Windows does not support --device + testRequires(c, DaemonIsLinux, NotUserNamespace) + out, _ := dockerCmd(c, "run", "--device", "/dev/zero:rw", "busybox", "sh", "-c", "ls /dev/zero") + if actual := strings.Trim(out, "\r\n"); actual != "/dev/zero" { + c.Fatalf("expected output /dev/zero, received %s", actual) + } +} + +func (s *DockerSuite) TestRunAddingOptionalDevicesInvalidMode(c *check.C) { + // Not applicable on Windows as Windows does not support --device + testRequires(c, DaemonIsLinux, NotUserNamespace) + _, _, err := dockerCmdWithError("run", "--device", "/dev/zero:ro", "busybox", "sh", "-c", "ls /dev/zero") + if err == nil { + c.Fatalf("run container with device mode ro should fail") + } +} + +func (s *DockerSuite) TestRunModeHostname(c *check.C) { + // Not applicable on Windows as Windows does not support -h + testRequires(c, SameHostDaemon, DaemonIsLinux, NotUserNamespace) + + out, _ := dockerCmd(c, "run", "-h=testhostname", "busybox", "cat", "/etc/hostname") + + if actual := strings.Trim(out, "\r\n"); actual != "testhostname" { + c.Fatalf("expected 'testhostname', but says: %q", actual) + } + + out, _ = dockerCmd(c, "run", "--net=host", "busybox", "cat", "/etc/hostname") + + hostname, err := os.Hostname() + if err != nil { + c.Fatal(err) + } + if actual := strings.Trim(out, "\r\n"); actual != hostname { + c.Fatalf("expected %q, but says: %q", hostname, actual) + } +} + +func (s *DockerSuite) TestRunRootWorkdir(c *check.C) { + out, _ := dockerCmd(c, "run", "--workdir", "/", "busybox", "pwd") + expected := "/\n" + if daemonPlatform == "windows" { + expected = "C:" + expected + } + if out != expected { + c.Fatalf("pwd returned %q (expected %s)", s, expected) + } +} + +func (s *DockerSuite) TestRunAllowBindMountingRoot(c *check.C) { + if daemonPlatform == "windows" { + // Windows busybox will fail with Permission Denied on items such as pagefile.sys + dockerCmd(c, "run", "-v", `c:\:c:\host`, WindowsBaseImage, "cmd", "-c", "dir", `c:\host`) + } else { + dockerCmd(c, "run", "-v", "/:/host", "busybox", "ls", "/host") + } +} + +func (s *DockerSuite) TestRunDisallowBindMountingRootToRoot(c *check.C) { + mount := "/:/" + targetDir := "/host" + if daemonPlatform == "windows" { + mount = `c:\:c\` + targetDir = "c:/host" // Forward slash as using busybox + } + out, _, err := dockerCmdWithError("run", "-v", mount, "busybox", "ls", targetDir) + if err == nil { + c.Fatal(out, err) + } +} + +// Verify that a container gets default DNS when only localhost resolvers exist +func (s *DockerSuite) TestRunDnsDefaultOptions(c *check.C) { + // Not applicable on Windows as this is testing Unix specific functionality + testRequires(c, SameHostDaemon, DaemonIsLinux) + + // preserve original resolv.conf for restoring after test + origResolvConf, err := ioutil.ReadFile("/etc/resolv.conf") + if os.IsNotExist(err) { + c.Fatalf("/etc/resolv.conf does not exist") + } + // defer restored original conf + defer func() { + if err := ioutil.WriteFile("/etc/resolv.conf", origResolvConf, 0644); err != nil { + c.Fatal(err) + } + }() + + // test 3 cases: standard IPv4 localhost, commented out localhost, and IPv6 localhost + // 2 are removed from the file at container start, and the 3rd (commented out) one is ignored by + // GetNameservers(), leading to a replacement of nameservers with the default set + tmpResolvConf := []byte("nameserver 127.0.0.1\n#nameserver 127.0.2.1\nnameserver ::1") + if err := ioutil.WriteFile("/etc/resolv.conf", tmpResolvConf, 0644); err != nil { + c.Fatal(err) + } + + actual, _ := dockerCmd(c, "run", "busybox", "cat", "/etc/resolv.conf") + // check that the actual defaults are appended to the commented out + // localhost resolver (which should be preserved) + // NOTE: if we ever change the defaults from google dns, this will break + expected := "#nameserver 127.0.2.1\n\nnameserver 8.8.8.8\nnameserver 8.8.4.4\n" + if actual != expected { + c.Fatalf("expected resolv.conf be: %q, but was: %q", expected, actual) + } +} + +func (s *DockerSuite) TestRunDnsOptions(c *check.C) { + // Not applicable on Windows as Windows does not support --dns*, or + // the Unix-specific functionality of resolv.conf. + testRequires(c, DaemonIsLinux) + out, stderr, _ := dockerCmdWithStdoutStderr(c, "run", "--dns=127.0.0.1", "--dns-search=mydomain", "--dns-opt=ndots:9", "busybox", "cat", "/etc/resolv.conf") + + // The client will get a warning on stderr when setting DNS to a localhost address; verify this: + if !strings.Contains(stderr, "Localhost DNS setting") { + c.Fatalf("Expected warning on stderr about localhost resolver, but got %q", stderr) + } + + actual := strings.Replace(strings.Trim(out, "\r\n"), "\n", " ", -1) + if actual != "search mydomain nameserver 127.0.0.1 options ndots:9" { + c.Fatalf("expected 'search mydomain nameserver 127.0.0.1 options ndots:9', but says: %q", actual) + } + + out, stderr, _ = dockerCmdWithStdoutStderr(c, "run", "--dns=127.0.0.1", "--dns-search=.", "--dns-opt=ndots:3", "busybox", "cat", "/etc/resolv.conf") + + actual = strings.Replace(strings.Trim(strings.Trim(out, "\r\n"), " "), "\n", " ", -1) + if actual != "nameserver 127.0.0.1 options ndots:3" { + c.Fatalf("expected 'nameserver 127.0.0.1 options ndots:3', but says: %q", actual) + } +} + +func (s *DockerSuite) TestRunDnsRepeatOptions(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _, _ := dockerCmdWithStdoutStderr(c, "run", "--dns=1.1.1.1", "--dns=2.2.2.2", "--dns-search=mydomain", "--dns-search=mydomain2", "--dns-opt=ndots:9", "--dns-opt=timeout:3", "busybox", "cat", "/etc/resolv.conf") + + actual := strings.Replace(strings.Trim(out, "\r\n"), "\n", " ", -1) + if actual != "search mydomain mydomain2 nameserver 1.1.1.1 nameserver 2.2.2.2 options ndots:9 timeout:3" { + c.Fatalf("expected 'search mydomain mydomain2 nameserver 1.1.1.1 nameserver 2.2.2.2 options ndots:9 timeout:3', but says: %q", actual) + } +} + +func (s *DockerSuite) TestRunDnsOptionsBasedOnHostResolvConf(c *check.C) { + // Not applicable on Windows as testing Unix specific functionality + testRequires(c, SameHostDaemon, DaemonIsLinux) + + origResolvConf, err := ioutil.ReadFile("/etc/resolv.conf") + if os.IsNotExist(err) { + c.Fatalf("/etc/resolv.conf does not exist") + } + + hostNamservers := resolvconf.GetNameservers(origResolvConf, netutils.IP) + hostSearch := resolvconf.GetSearchDomains(origResolvConf) + + var out string + out, _ = dockerCmd(c, "run", "--dns=127.0.0.1", "busybox", "cat", "/etc/resolv.conf") + + if actualNameservers := resolvconf.GetNameservers([]byte(out), netutils.IP); string(actualNameservers[0]) != "127.0.0.1" { + c.Fatalf("expected '127.0.0.1', but says: %q", string(actualNameservers[0])) + } + + actualSearch := resolvconf.GetSearchDomains([]byte(out)) + if len(actualSearch) != len(hostSearch) { + c.Fatalf("expected %q search domain(s), but it has: %q", len(hostSearch), len(actualSearch)) + } + for i := range actualSearch { + if actualSearch[i] != hostSearch[i] { + c.Fatalf("expected %q domain, but says: %q", actualSearch[i], hostSearch[i]) + } + } + + out, _ = dockerCmd(c, "run", "--dns-search=mydomain", "busybox", "cat", "/etc/resolv.conf") + + actualNameservers := resolvconf.GetNameservers([]byte(out), netutils.IP) + if len(actualNameservers) != len(hostNamservers) { + c.Fatalf("expected %q nameserver(s), but it has: %q", len(hostNamservers), len(actualNameservers)) + } + for i := range actualNameservers { + if actualNameservers[i] != hostNamservers[i] { + c.Fatalf("expected %q nameserver, but says: %q", actualNameservers[i], hostNamservers[i]) + } + } + + if actualSearch = resolvconf.GetSearchDomains([]byte(out)); string(actualSearch[0]) != "mydomain" { + c.Fatalf("expected 'mydomain', but says: %q", string(actualSearch[0])) + } + + // test with file + tmpResolvConf := []byte("search example.com\nnameserver 12.34.56.78\nnameserver 127.0.0.1") + if err := ioutil.WriteFile("/etc/resolv.conf", tmpResolvConf, 0644); err != nil { + c.Fatal(err) + } + // put the old resolvconf back + defer func() { + if err := ioutil.WriteFile("/etc/resolv.conf", origResolvConf, 0644); err != nil { + c.Fatal(err) + } + }() + + resolvConf, err := ioutil.ReadFile("/etc/resolv.conf") + if os.IsNotExist(err) { + c.Fatalf("/etc/resolv.conf does not exist") + } + + hostNamservers = resolvconf.GetNameservers(resolvConf, netutils.IP) + hostSearch = resolvconf.GetSearchDomains(resolvConf) + + out, _ = dockerCmd(c, "run", "busybox", "cat", "/etc/resolv.conf") + if actualNameservers = resolvconf.GetNameservers([]byte(out), netutils.IP); string(actualNameservers[0]) != "12.34.56.78" || len(actualNameservers) != 1 { + c.Fatalf("expected '12.34.56.78', but has: %v", actualNameservers) + } + + actualSearch = resolvconf.GetSearchDomains([]byte(out)) + if len(actualSearch) != len(hostSearch) { + c.Fatalf("expected %q search domain(s), but it has: %q", len(hostSearch), len(actualSearch)) + } + for i := range actualSearch { + if actualSearch[i] != hostSearch[i] { + c.Fatalf("expected %q domain, but says: %q", actualSearch[i], hostSearch[i]) + } + } +} + +// Test to see if a non-root user can resolve a DNS name. Also +// check if the container resolv.conf file has at least 0644 perm. +func (s *DockerSuite) TestRunNonRootUserResolvName(c *check.C) { + // Not applicable on Windows as Windows does not support --user + testRequires(c, SameHostDaemon, Network, DaemonIsLinux, NotArm) + + dockerCmd(c, "run", "--name=testperm", "--user=nobody", "busybox", "nslookup", "apt.dockerproject.org") + + cID, err := getIDByName("testperm") + if err != nil { + c.Fatal(err) + } + + fmode := (os.FileMode)(0644) + finfo, err := os.Stat(containerStorageFile(cID, "resolv.conf")) + if err != nil { + c.Fatal(err) + } + + if (finfo.Mode() & fmode) != fmode { + c.Fatalf("Expected container resolv.conf mode to be at least %s, instead got %s", fmode.String(), finfo.Mode().String()) + } +} + +// Test if container resolv.conf gets updated the next time it restarts +// if host /etc/resolv.conf has changed. This only applies if the container +// uses the host's /etc/resolv.conf and does not have any dns options provided. +func (s *DockerSuite) TestRunResolvconfUpdate(c *check.C) { + // Not applicable on Windows as testing unix specific functionality + testRequires(c, SameHostDaemon, DaemonIsLinux) + + tmpResolvConf := []byte("search pommesfrites.fr\nnameserver 12.34.56.78\n") + tmpLocalhostResolvConf := []byte("nameserver 127.0.0.1") + + //take a copy of resolv.conf for restoring after test completes + resolvConfSystem, err := ioutil.ReadFile("/etc/resolv.conf") + if err != nil { + c.Fatal(err) + } + + // This test case is meant to test monitoring resolv.conf when it is + // a regular file not a bind mounc. So we unmount resolv.conf and replace + // it with a file containing the original settings. + cmd := exec.Command("umount", "/etc/resolv.conf") + if _, err = runCommand(cmd); err != nil { + c.Fatal(err) + } + + //cleanup + defer func() { + if err := ioutil.WriteFile("/etc/resolv.conf", resolvConfSystem, 0644); err != nil { + c.Fatal(err) + } + }() + + //1. test that a restarting container gets an updated resolv.conf + dockerCmd(c, "run", "--name='first'", "busybox", "true") + containerID1, err := getIDByName("first") + if err != nil { + c.Fatal(err) + } + + // replace resolv.conf with our temporary copy + bytesResolvConf := []byte(tmpResolvConf) + if err := ioutil.WriteFile("/etc/resolv.conf", bytesResolvConf, 0644); err != nil { + c.Fatal(err) + } + + // start the container again to pickup changes + dockerCmd(c, "start", "first") + + // check for update in container + containerResolv, err := readContainerFile(containerID1, "resolv.conf") + if err != nil { + c.Fatal(err) + } + if !bytes.Equal(containerResolv, bytesResolvConf) { + c.Fatalf("Restarted container does not have updated resolv.conf; expected %q, got %q", tmpResolvConf, string(containerResolv)) + } + + /* //make a change to resolv.conf (in this case replacing our tmp copy with orig copy) + if err := ioutil.WriteFile("/etc/resolv.conf", resolvConfSystem, 0644); err != nil { + c.Fatal(err) + } */ + //2. test that a restarting container does not receive resolv.conf updates + // if it modified the container copy of the starting point resolv.conf + dockerCmd(c, "run", "--name='second'", "busybox", "sh", "-c", "echo 'search mylittlepony.com' >>/etc/resolv.conf") + containerID2, err := getIDByName("second") + if err != nil { + c.Fatal(err) + } + + //make a change to resolv.conf (in this case replacing our tmp copy with orig copy) + if err := ioutil.WriteFile("/etc/resolv.conf", resolvConfSystem, 0644); err != nil { + c.Fatal(err) + } + + // start the container again + dockerCmd(c, "start", "second") + + // check for update in container + containerResolv, err = readContainerFile(containerID2, "resolv.conf") + if err != nil { + c.Fatal(err) + } + + if bytes.Equal(containerResolv, resolvConfSystem) { + c.Fatalf("Container's resolv.conf should not have been updated with host resolv.conf: %q", string(containerResolv)) + } + + //3. test that a running container's resolv.conf is not modified while running + out, _ := dockerCmd(c, "run", "-d", "busybox", "top") + runningContainerID := strings.TrimSpace(out) + + // replace resolv.conf + if err := ioutil.WriteFile("/etc/resolv.conf", bytesResolvConf, 0644); err != nil { + c.Fatal(err) + } + + // check for update in container + containerResolv, err = readContainerFile(runningContainerID, "resolv.conf") + if err != nil { + c.Fatal(err) + } + + if bytes.Equal(containerResolv, bytesResolvConf) { + c.Fatalf("Running container should not have updated resolv.conf; expected %q, got %q", string(resolvConfSystem), string(containerResolv)) + } + + //4. test that a running container's resolv.conf is updated upon restart + // (the above container is still running..) + dockerCmd(c, "restart", runningContainerID) + + // check for update in container + containerResolv, err = readContainerFile(runningContainerID, "resolv.conf") + if err != nil { + c.Fatal(err) + } + if !bytes.Equal(containerResolv, bytesResolvConf) { + c.Fatalf("Restarted container should have updated resolv.conf; expected %q, got %q", string(bytesResolvConf), string(containerResolv)) + } + + //5. test that additions of a localhost resolver are cleaned from + // host resolv.conf before updating container's resolv.conf copies + + // replace resolv.conf with a localhost-only nameserver copy + bytesResolvConf = []byte(tmpLocalhostResolvConf) + if err = ioutil.WriteFile("/etc/resolv.conf", bytesResolvConf, 0644); err != nil { + c.Fatal(err) + } + + // start the container again to pickup changes + dockerCmd(c, "start", "first") + + // our first exited container ID should have been updated, but with default DNS + // after the cleanup of resolv.conf found only a localhost nameserver: + containerResolv, err = readContainerFile(containerID1, "resolv.conf") + if err != nil { + c.Fatal(err) + } + + expected := "\nnameserver 8.8.8.8\nnameserver 8.8.4.4\n" + if !bytes.Equal(containerResolv, []byte(expected)) { + c.Fatalf("Container does not have cleaned/replaced DNS in resolv.conf; expected %q, got %q", expected, string(containerResolv)) + } + + //6. Test that replacing (as opposed to modifying) resolv.conf triggers an update + // of containers' resolv.conf. + + // Restore the original resolv.conf + if err := ioutil.WriteFile("/etc/resolv.conf", resolvConfSystem, 0644); err != nil { + c.Fatal(err) + } + + // Run the container so it picks up the old settings + dockerCmd(c, "run", "--name='third'", "busybox", "true") + containerID3, err := getIDByName("third") + if err != nil { + c.Fatal(err) + } + + // Create a modified resolv.conf.aside and override resolv.conf with it + bytesResolvConf = []byte(tmpResolvConf) + if err := ioutil.WriteFile("/etc/resolv.conf.aside", bytesResolvConf, 0644); err != nil { + c.Fatal(err) + } + + err = os.Rename("/etc/resolv.conf.aside", "/etc/resolv.conf") + if err != nil { + c.Fatal(err) + } + + // start the container again to pickup changes + dockerCmd(c, "start", "third") + + // check for update in container + containerResolv, err = readContainerFile(containerID3, "resolv.conf") + if err != nil { + c.Fatal(err) + } + if !bytes.Equal(containerResolv, bytesResolvConf) { + c.Fatalf("Stopped container does not have updated resolv.conf; expected\n%q\n got\n%q", tmpResolvConf, string(containerResolv)) + } + + //cleanup, restore original resolv.conf happens in defer func() +} + +func (s *DockerSuite) TestRunAddHost(c *check.C) { + // Not applicable on Windows as it does not support --add-host + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "--add-host=extra:86.75.30.9", "busybox", "grep", "extra", "/etc/hosts") + + actual := strings.Trim(out, "\r\n") + if actual != "86.75.30.9\textra" { + c.Fatalf("expected '86.75.30.9\textra', but says: %q", actual) + } +} + +// Regression test for #6983 +func (s *DockerSuite) TestRunAttachStdErrOnlyTTYMode(c *check.C) { + _, exitCode := dockerCmd(c, "run", "-t", "-a", "stderr", "busybox", "true") + if exitCode != 0 { + c.Fatalf("Container should have exited with error code 0") + } +} + +// Regression test for #6983 +func (s *DockerSuite) TestRunAttachStdOutOnlyTTYMode(c *check.C) { + _, exitCode := dockerCmd(c, "run", "-t", "-a", "stdout", "busybox", "true") + if exitCode != 0 { + c.Fatalf("Container should have exited with error code 0") + } +} + +// Regression test for #6983 +func (s *DockerSuite) TestRunAttachStdOutAndErrTTYMode(c *check.C) { + _, exitCode := dockerCmd(c, "run", "-t", "-a", "stdout", "-a", "stderr", "busybox", "true") + if exitCode != 0 { + c.Fatalf("Container should have exited with error code 0") + } +} + +// Test for #10388 - this will run the same test as TestRunAttachStdOutAndErrTTYMode +// but using --attach instead of -a to make sure we read the flag correctly +func (s *DockerSuite) TestRunAttachWithDetach(c *check.C) { + cmd := exec.Command(dockerBinary, "run", "-d", "--attach", "stdout", "busybox", "true") + _, stderr, _, err := runCommandWithStdoutStderr(cmd) + if err == nil { + c.Fatal("Container should have exited with error code different than 0") + } else if !strings.Contains(stderr, "Conflicting options: -a and -d") { + c.Fatal("Should have been returned an error with conflicting options -a and -d") + } +} + +func (s *DockerSuite) TestRunState(c *check.C) { + // TODO Windows: This needs some rework as Windows busybox does not support top + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "top") + + id := strings.TrimSpace(out) + state := inspectField(c, id, "State.Running") + if state != "true" { + c.Fatal("Container state is 'not running'") + } + pid1 := inspectField(c, id, "State.Pid") + if pid1 == "0" { + c.Fatal("Container state Pid 0") + } + + dockerCmd(c, "stop", id) + state = inspectField(c, id, "State.Running") + if state != "false" { + c.Fatal("Container state is 'running'") + } + pid2 := inspectField(c, id, "State.Pid") + if pid2 == pid1 { + c.Fatalf("Container state Pid %s, but expected %s", pid2, pid1) + } + + dockerCmd(c, "start", id) + state = inspectField(c, id, "State.Running") + if state != "true" { + c.Fatal("Container state is 'not running'") + } + pid3 := inspectField(c, id, "State.Pid") + if pid3 == pid1 { + c.Fatalf("Container state Pid %s, but expected %s", pid2, pid1) + } +} + +// Test for #1737 +func (s *DockerSuite) TestRunCopyVolumeUidGid(c *check.C) { + // Not applicable on Windows as it does not support uid or gid in this way + testRequires(c, DaemonIsLinux) + name := "testrunvolumesuidgid" + _, err := buildImage(name, + `FROM busybox + RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd + RUN echo 'dockerio:x:1001:' >> /etc/group + RUN mkdir -p /hello && touch /hello/test && chown dockerio.dockerio /hello`, + true) + if err != nil { + c.Fatal(err) + } + + // Test that the uid and gid is copied from the image to the volume + out, _ := dockerCmd(c, "run", "--rm", "-v", "/hello", name, "sh", "-c", "ls -l / | grep hello | awk '{print $3\":\"$4}'") + out = strings.TrimSpace(out) + if out != "dockerio:dockerio" { + c.Fatalf("Wrong /hello ownership: %s, expected dockerio:dockerio", out) + } +} + +// Test for #1582 +func (s *DockerSuite) TestRunCopyVolumeContent(c *check.C) { + // TODO Windows, post TP4. Windows does not yet support volume functionality + // that copies from the image to the volume. + testRequires(c, DaemonIsLinux) + name := "testruncopyvolumecontent" + _, err := buildImage(name, + `FROM busybox + RUN mkdir -p /hello/local && echo hello > /hello/local/world`, + true) + if err != nil { + c.Fatal(err) + } + + // Test that the content is copied from the image to the volume + out, _ := dockerCmd(c, "run", "--rm", "-v", "/hello", name, "find", "/hello") + if !(strings.Contains(out, "/hello/local/world") && strings.Contains(out, "/hello/local")) { + c.Fatal("Container failed to transfer content to volume") + } +} + +func (s *DockerSuite) TestRunCleanupCmdOnEntrypoint(c *check.C) { + name := "testrunmdcleanuponentrypoint" + if _, err := buildImage(name, + `FROM busybox + ENTRYPOINT ["echo"] + CMD ["testingpoint"]`, + true); err != nil { + c.Fatal(err) + } + + out, exit := dockerCmd(c, "run", "--entrypoint", "whoami", name) + if exit != 0 { + c.Fatalf("expected exit code 0 received %d, out: %q", exit, out) + } + out = strings.TrimSpace(out) + expected := "root" + if daemonPlatform == "windows" { + // TODO Windows: Remove this check once TP4 is no longer supported. + if windowsDaemonKV < 14250 { + expected = `nt authority\system` + } else { + expected = `user manager\containeradministrator` + } + } + if out != expected { + c.Fatalf("Expected output %s, got %q", expected, out) + } +} + +// TestRunWorkdirExistsAndIsFile checks that if 'docker run -w' with existing file can be detected +func (s *DockerSuite) TestRunWorkdirExistsAndIsFile(c *check.C) { + existingFile := "/bin/cat" + expected := "not a directory" + if daemonPlatform == "windows" { + existingFile = `\windows\system32\ntdll.dll` + expected = `Cannot mkdir: \windows\system32\ntdll.dll is not a directory.` + } + + out, exitCode, err := dockerCmdWithError("run", "-w", existingFile, "busybox") + if !(err != nil && exitCode == 125 && strings.Contains(out, expected)) { + c.Fatalf("Existing binary as a directory should error out with exitCode 125; we got: %s, exitCode: %d", out, exitCode) + } +} + +func (s *DockerSuite) TestRunExitOnStdinClose(c *check.C) { + name := "testrunexitonstdinclose" + + meow := "/bin/cat" + delay := 1 + if daemonPlatform == "windows" { + meow = "cat" + delay = 60 + } + runCmd := exec.Command(dockerBinary, "run", "--name", name, "-i", "busybox", meow) + + stdin, err := runCmd.StdinPipe() + if err != nil { + c.Fatal(err) + } + stdout, err := runCmd.StdoutPipe() + if err != nil { + c.Fatal(err) + } + + if err := runCmd.Start(); err != nil { + c.Fatal(err) + } + if _, err := stdin.Write([]byte("hello\n")); err != nil { + c.Fatal(err) + } + + r := bufio.NewReader(stdout) + line, err := r.ReadString('\n') + if err != nil { + c.Fatal(err) + } + line = strings.TrimSpace(line) + if line != "hello" { + c.Fatalf("Output should be 'hello', got '%q'", line) + } + if err := stdin.Close(); err != nil { + c.Fatal(err) + } + finish := make(chan error) + go func() { + finish <- runCmd.Wait() + close(finish) + }() + select { + case err := <-finish: + c.Assert(err, check.IsNil) + case <-time.After(time.Duration(delay) * time.Second): + c.Fatal("docker run failed to exit on stdin close") + } + state := inspectField(c, name, "State.Running") + + if state != "false" { + c.Fatal("Container must be stopped after stdin closing") + } +} + +// Test for #2267 +func (s *DockerSuite) TestRunWriteHostsFileAndNotCommit(c *check.C) { + // Cannot run on Windows as Windows does not support diff. + testRequires(c, DaemonIsLinux) + name := "writehosts" + out, _ := dockerCmd(c, "run", "--name", name, "busybox", "sh", "-c", "echo test2267 >> /etc/hosts && cat /etc/hosts") + if !strings.Contains(out, "test2267") { + c.Fatal("/etc/hosts should contain 'test2267'") + } + + out, _ = dockerCmd(c, "diff", name) + if len(strings.Trim(out, "\r\n")) != 0 && !eqToBaseDiff(out, c) { + c.Fatal("diff should be empty") + } +} + +func eqToBaseDiff(out string, c *check.C) bool { + name := "eqToBaseDiff" + stringutils.GenerateRandomAlphaOnlyString(32) + dockerCmd(c, "run", "--name", name, "busybox", "echo", "hello") + cID, err := getIDByName(name) + c.Assert(err, check.IsNil) + + baseDiff, _ := dockerCmd(c, "diff", cID) + baseArr := strings.Split(baseDiff, "\n") + sort.Strings(baseArr) + outArr := strings.Split(out, "\n") + sort.Strings(outArr) + return sliceEq(baseArr, outArr) +} + +func sliceEq(a, b []string) bool { + if len(a) != len(b) { + return false + } + + for i := range a { + if a[i] != b[i] { + return false + } + } + + return true +} + +// Test for #2267 +func (s *DockerSuite) TestRunWriteHostnameFileAndNotCommit(c *check.C) { + // Cannot run on Windows as Windows does not support diff. + testRequires(c, DaemonIsLinux) + name := "writehostname" + out, _ := dockerCmd(c, "run", "--name", name, "busybox", "sh", "-c", "echo test2267 >> /etc/hostname && cat /etc/hostname") + if !strings.Contains(out, "test2267") { + c.Fatal("/etc/hostname should contain 'test2267'") + } + + out, _ = dockerCmd(c, "diff", name) + if len(strings.Trim(out, "\r\n")) != 0 && !eqToBaseDiff(out, c) { + c.Fatal("diff should be empty") + } +} + +// Test for #2267 +func (s *DockerSuite) TestRunWriteResolvFileAndNotCommit(c *check.C) { + // Cannot run on Windows as Windows does not support diff. + testRequires(c, DaemonIsLinux) + name := "writeresolv" + out, _ := dockerCmd(c, "run", "--name", name, "busybox", "sh", "-c", "echo test2267 >> /etc/resolv.conf && cat /etc/resolv.conf") + if !strings.Contains(out, "test2267") { + c.Fatal("/etc/resolv.conf should contain 'test2267'") + } + + out, _ = dockerCmd(c, "diff", name) + if len(strings.Trim(out, "\r\n")) != 0 && !eqToBaseDiff(out, c) { + c.Fatal("diff should be empty") + } +} + +func (s *DockerSuite) TestRunWithBadDevice(c *check.C) { + // Cannot run on Windows as Windows does not support --device + testRequires(c, DaemonIsLinux) + name := "baddevice" + out, _, err := dockerCmdWithError("run", "--name", name, "--device", "/etc", "busybox", "true") + + if err == nil { + c.Fatal("Run should fail with bad device") + } + expected := `"/etc": not a device node` + if !strings.Contains(out, expected) { + c.Fatalf("Output should contain %q, actual out: %q", expected, out) + } +} + +func (s *DockerSuite) TestRunEntrypoint(c *check.C) { + name := "entrypoint" + + // Note Windows does not have an echo.exe built in. + var out, expected string + if daemonPlatform == "windows" { + out, _ = dockerCmd(c, "run", "--name", name, "--entrypoint", "cmd /s /c echo", "busybox", "foobar") + expected = "foobar\r\n" + } else { + out, _ = dockerCmd(c, "run", "--name", name, "--entrypoint", "/bin/echo", "busybox", "-n", "foobar") + expected = "foobar" + } + + if out != expected { + c.Fatalf("Output should be %q, actual out: %q", expected, out) + } +} + +func (s *DockerSuite) TestRunBindMounts(c *check.C) { + testRequires(c, SameHostDaemon) + if daemonPlatform == "linux" { + testRequires(c, DaemonIsLinux, NotUserNamespace) + } + + tmpDir, err := ioutil.TempDir("", "docker-test-container") + if err != nil { + c.Fatal(err) + } + + defer os.RemoveAll(tmpDir) + writeFile(path.Join(tmpDir, "touch-me"), "", c) + + // TODO Windows Post TP4. Windows does not yet support :ro binds + if daemonPlatform != "windows" { + // Test reading from a read-only bind mount + out, _ := dockerCmd(c, "run", "-v", fmt.Sprintf("%s:/tmp:ro", tmpDir), "busybox", "ls", "/tmp") + if !strings.Contains(out, "touch-me") { + c.Fatal("Container failed to read from bind mount") + } + } + + // test writing to bind mount + if daemonPlatform == "windows" { + dockerCmd(c, "run", "-v", fmt.Sprintf(`%s:c:\tmp:rw`, tmpDir), "busybox", "touch", "c:/tmp/holla") + } else { + dockerCmd(c, "run", "-v", fmt.Sprintf("%s:/tmp:rw", tmpDir), "busybox", "touch", "/tmp/holla") + } + + readFile(path.Join(tmpDir, "holla"), c) // Will fail if the file doesn't exist + + // test mounting to an illegal destination directory + _, _, err = dockerCmdWithError("run", "-v", fmt.Sprintf("%s:.", tmpDir), "busybox", "ls", ".") + if err == nil { + c.Fatal("Container bind mounted illegal directory") + } + + // Windows does not (and likely never will) support mounting a single file + if daemonPlatform != "windows" { + // test mount a file + dockerCmd(c, "run", "-v", fmt.Sprintf("%s/holla:/tmp/holla:rw", tmpDir), "busybox", "sh", "-c", "echo -n 'yotta' > /tmp/holla") + content := readFile(path.Join(tmpDir, "holla"), c) // Will fail if the file doesn't exist + expected := "yotta" + if content != expected { + c.Fatalf("Output should be %q, actual out: %q", expected, content) + } + } +} + +// Ensure that CIDFile gets deleted if it's empty +// Perform this test by making `docker run` fail +func (s *DockerSuite) TestRunCidFileCleanupIfEmpty(c *check.C) { + tmpDir, err := ioutil.TempDir("", "TestRunCidFile") + if err != nil { + c.Fatal(err) + } + defer os.RemoveAll(tmpDir) + tmpCidFile := path.Join(tmpDir, "cid") + + image := "emptyfs" + if daemonPlatform == "windows" { + // Windows can't support an emptyfs image. Just use the regular Windows image + image = WindowsBaseImage + } + out, _, err := dockerCmdWithError("run", "--cidfile", tmpCidFile, image) + if err == nil { + c.Fatalf("Run without command must fail. out=%s", out) + } else if !strings.Contains(out, "No command specified") { + c.Fatalf("Run without command failed with wrong output. out=%s\nerr=%v", out, err) + } + + if _, err := os.Stat(tmpCidFile); err == nil { + c.Fatalf("empty CIDFile %q should've been deleted", tmpCidFile) + } +} + +// #2098 - Docker cidFiles only contain short version of the containerId +//sudo docker run --cidfile /tmp/docker_tesc.cid ubuntu echo "test" +// TestRunCidFile tests that run --cidfile returns the longid +func (s *DockerSuite) TestRunCidFileCheckIDLength(c *check.C) { + tmpDir, err := ioutil.TempDir("", "TestRunCidFile") + if err != nil { + c.Fatal(err) + } + tmpCidFile := path.Join(tmpDir, "cid") + defer os.RemoveAll(tmpDir) + + out, _ := dockerCmd(c, "run", "-d", "--cidfile", tmpCidFile, "busybox", "true") + + id := strings.TrimSpace(out) + buffer, err := ioutil.ReadFile(tmpCidFile) + if err != nil { + c.Fatal(err) + } + cid := string(buffer) + if len(cid) != 64 { + c.Fatalf("--cidfile should be a long id, not %q", id) + } + if cid != id { + c.Fatalf("cid must be equal to %s, got %s", id, cid) + } +} + +func (s *DockerSuite) TestRunSetMacAddress(c *check.C) { + mac := "12:34:56:78:9a:bc" + var out string + if daemonPlatform == "windows" { + out, _ = dockerCmd(c, "run", "-i", "--rm", fmt.Sprintf("--mac-address=%s", mac), "busybox", "sh", "-c", "ipconfig /all | grep 'Physical Address' | awk '{print $12}'") + mac = strings.Replace(strings.ToUpper(mac), ":", "-", -1) // To Windows-style MACs + } else { + out, _ = dockerCmd(c, "run", "-i", "--rm", fmt.Sprintf("--mac-address=%s", mac), "busybox", "/bin/sh", "-c", "ip link show eth0 | tail -1 | awk '{print $2}'") + } + + actualMac := strings.TrimSpace(out) + if actualMac != mac { + c.Fatalf("Set MAC address with --mac-address failed. The container has an incorrect MAC address: %q, expected: %q", actualMac, mac) + } +} + +func (s *DockerSuite) TestRunInspectMacAddress(c *check.C) { + // TODO Windows. Network settings are not propagated back to inspect. + testRequires(c, DaemonIsLinux) + mac := "12:34:56:78:9a:bc" + out, _ := dockerCmd(c, "run", "-d", "--mac-address="+mac, "busybox", "top") + + id := strings.TrimSpace(out) + inspectedMac := inspectField(c, id, "NetworkSettings.Networks.bridge.MacAddress") + if inspectedMac != mac { + c.Fatalf("docker inspect outputs wrong MAC address: %q, should be: %q", inspectedMac, mac) + } +} + +// test docker run use an invalid mac address +func (s *DockerSuite) TestRunWithInvalidMacAddress(c *check.C) { + out, _, err := dockerCmdWithError("run", "--mac-address", "92:d0:c6:0a:29", "busybox") + //use an invalid mac address should with an error out + if err == nil || !strings.Contains(out, "is not a valid mac address") { + c.Fatalf("run with an invalid --mac-address should with error out") + } +} + +func (s *DockerSuite) TestRunDeallocatePortOnMissingIptablesRule(c *check.C) { + // TODO Windows. Network settings are not propagated back to inspect. + testRequires(c, SameHostDaemon, DaemonIsLinux) + + out, _ := dockerCmd(c, "run", "-d", "-p", "23:23", "busybox", "top") + + id := strings.TrimSpace(out) + ip := inspectField(c, id, "NetworkSettings.Networks.bridge.IPAddress") + iptCmd := exec.Command("iptables", "-D", "DOCKER", "-d", fmt.Sprintf("%s/32", ip), + "!", "-i", "docker0", "-o", "docker0", "-p", "tcp", "-m", "tcp", "--dport", "23", "-j", "ACCEPT") + out, _, err := runCommandWithOutput(iptCmd) + if err != nil { + c.Fatal(err, out) + } + if err := deleteContainer(id); err != nil { + c.Fatal(err) + } + + dockerCmd(c, "run", "-d", "-p", "23:23", "busybox", "top") +} + +func (s *DockerSuite) TestRunPortInUse(c *check.C) { + // TODO Windows. The duplicate NAT message returned by Windows will be + // changing as is currently completely undecipherable. Does need modifying + // to run sh rather than top though as top isn't in Windows busybox. + testRequires(c, SameHostDaemon, DaemonIsLinux) + + port := "1234" + dockerCmd(c, "run", "-d", "-p", port+":80", "busybox", "top") + + out, _, err := dockerCmdWithError("run", "-d", "-p", port+":80", "busybox", "top") + if err == nil { + c.Fatalf("Binding on used port must fail") + } + if !strings.Contains(out, "port is already allocated") { + c.Fatalf("Out must be about \"port is already allocated\", got %s", out) + } +} + +// https://github.com/docker/docker/issues/12148 +func (s *DockerSuite) TestRunAllocatePortInReservedRange(c *check.C) { + // TODO Windows. -P is not yet supported + testRequires(c, DaemonIsLinux) + // allocate a dynamic port to get the most recent + out, _ := dockerCmd(c, "run", "-d", "-P", "-p", "80", "busybox", "top") + + id := strings.TrimSpace(out) + out, _ = dockerCmd(c, "port", id, "80") + + strPort := strings.Split(strings.TrimSpace(out), ":")[1] + port, err := strconv.ParseInt(strPort, 10, 64) + if err != nil { + c.Fatalf("invalid port, got: %s, error: %s", strPort, err) + } + + // allocate a static port and a dynamic port together, with static port + // takes the next recent port in dynamic port range. + dockerCmd(c, "run", "-d", "-P", "-p", "80", "-p", fmt.Sprintf("%d:8080", port+1), "busybox", "top") +} + +// Regression test for #7792 +func (s *DockerSuite) TestRunMountOrdering(c *check.C) { + // TODO Windows: Post TP4. Updated, but Windows does not support nested mounts currently. + testRequires(c, SameHostDaemon, DaemonIsLinux, NotUserNamespace) + prefix, _ := getPrefixAndSlashFromDaemonPlatform() + + tmpDir, err := ioutil.TempDir("", "docker_nested_mount_test") + if err != nil { + c.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + tmpDir2, err := ioutil.TempDir("", "docker_nested_mount_test2") + if err != nil { + c.Fatal(err) + } + defer os.RemoveAll(tmpDir2) + + // Create a temporary tmpfs mounc. + fooDir := filepath.Join(tmpDir, "foo") + if err := os.MkdirAll(filepath.Join(tmpDir, "foo"), 0755); err != nil { + c.Fatalf("failed to mkdir at %s - %s", fooDir, err) + } + + if err := ioutil.WriteFile(fmt.Sprintf("%s/touch-me", fooDir), []byte{}, 0644); err != nil { + c.Fatal(err) + } + + if err := ioutil.WriteFile(fmt.Sprintf("%s/touch-me", tmpDir), []byte{}, 0644); err != nil { + c.Fatal(err) + } + + if err := ioutil.WriteFile(fmt.Sprintf("%s/touch-me", tmpDir2), []byte{}, 0644); err != nil { + c.Fatal(err) + } + + dockerCmd(c, "run", + "-v", fmt.Sprintf("%s:"+prefix+"/tmp", tmpDir), + "-v", fmt.Sprintf("%s:"+prefix+"/tmp/foo", fooDir), + "-v", fmt.Sprintf("%s:"+prefix+"/tmp/tmp2", tmpDir2), + "-v", fmt.Sprintf("%s:"+prefix+"/tmp/tmp2/foo", fooDir), + "busybox:latest", "sh", "-c", + "ls "+prefix+"/tmp/touch-me && ls "+prefix+"/tmp/foo/touch-me && ls "+prefix+"/tmp/tmp2/touch-me && ls "+prefix+"/tmp/tmp2/foo/touch-me") +} + +// Regression test for https://github.com/docker/docker/issues/8259 +func (s *DockerSuite) TestRunReuseBindVolumeThatIsSymlink(c *check.C) { + // Not applicable on Windows as Windows does not support volumes + testRequires(c, SameHostDaemon, DaemonIsLinux, NotUserNamespace) + prefix, _ := getPrefixAndSlashFromDaemonPlatform() + + tmpDir, err := ioutil.TempDir(os.TempDir(), "testlink") + if err != nil { + c.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + linkPath := os.TempDir() + "/testlink2" + if err := os.Symlink(tmpDir, linkPath); err != nil { + c.Fatal(err) + } + defer os.RemoveAll(linkPath) + + // Create first container + dockerCmd(c, "run", "-v", fmt.Sprintf("%s:"+prefix+"/tmp/test", linkPath), "busybox", "ls", prefix+"/tmp/test") + + // Create second container with same symlinked path + // This will fail if the referenced issue is hit with a "Volume exists" error + dockerCmd(c, "run", "-v", fmt.Sprintf("%s:"+prefix+"/tmp/test", linkPath), "busybox", "ls", prefix+"/tmp/test") +} + +//GH#10604: Test an "/etc" volume doesn't overlay special bind mounts in container +func (s *DockerSuite) TestRunCreateVolumeEtc(c *check.C) { + // While Windows supports volumes, it does not support --add-host hence + // this test is not applicable on Windows. + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "--dns=127.0.0.1", "-v", "/etc", "busybox", "cat", "/etc/resolv.conf") + if !strings.Contains(out, "nameserver 127.0.0.1") { + c.Fatal("/etc volume mount hides /etc/resolv.conf") + } + + out, _ = dockerCmd(c, "run", "-h=test123", "-v", "/etc", "busybox", "cat", "/etc/hostname") + if !strings.Contains(out, "test123") { + c.Fatal("/etc volume mount hides /etc/hostname") + } + + out, _ = dockerCmd(c, "run", "--add-host=test:192.168.0.1", "-v", "/etc", "busybox", "cat", "/etc/hosts") + out = strings.Replace(out, "\n", " ", -1) + if !strings.Contains(out, "192.168.0.1\ttest") || !strings.Contains(out, "127.0.0.1\tlocalhost") { + c.Fatal("/etc volume mount hides /etc/hosts") + } +} + +func (s *DockerSuite) TestVolumesNoCopyData(c *check.C) { + // TODO Windows (Post TP4). Windows does not support volumes which + // are pre-populated such as is built in the dockerfile used in this test. + testRequires(c, DaemonIsLinux) + if _, err := buildImage("dataimage", + `FROM busybox + RUN mkdir -p /foo + RUN touch /foo/bar`, + true); err != nil { + c.Fatal(err) + } + + dockerCmd(c, "run", "--name", "test", "-v", "/foo", "busybox") + + if out, _, err := dockerCmdWithError("run", "--volumes-from", "test", "dataimage", "ls", "-lh", "/foo/bar"); err == nil || !strings.Contains(out, "No such file or directory") { + c.Fatalf("Data was copied on volumes-from but shouldn't be:\n%q", out) + } + + tmpDir := randomTmpDirPath("docker_test_bind_mount_copy_data", daemonPlatform) + if out, _, err := dockerCmdWithError("run", "-v", tmpDir+":/foo", "dataimage", "ls", "-lh", "/foo/bar"); err == nil || !strings.Contains(out, "No such file or directory") { + c.Fatalf("Data was copied on bind-mount but shouldn't be:\n%q", out) + } +} + +func (s *DockerSuite) TestRunNoOutputFromPullInStdout(c *check.C) { + // just run with unknown image + cmd := exec.Command(dockerBinary, "run", "asdfsg") + stdout := bytes.NewBuffer(nil) + cmd.Stdout = stdout + if err := cmd.Run(); err == nil { + c.Fatal("Run with unknown image should fail") + } + if stdout.Len() != 0 { + c.Fatalf("Stdout contains output from pull: %s", stdout) + } +} + +func (s *DockerSuite) TestRunVolumesCleanPaths(c *check.C) { + testRequires(c, SameHostDaemon) + prefix, slash := getPrefixAndSlashFromDaemonPlatform() + if _, err := buildImage("run_volumes_clean_paths", + `FROM busybox + VOLUME `+prefix+`/foo/`, + true); err != nil { + c.Fatal(err) + } + + dockerCmd(c, "run", "-v", prefix+"/foo", "-v", prefix+"/bar/", "--name", "dark_helmet", "run_volumes_clean_paths") + + out, err := inspectMountSourceField("dark_helmet", prefix+slash+"foo"+slash) + if err != errMountNotFound { + c.Fatalf("Found unexpected volume entry for '%s/foo/' in volumes\n%q", prefix, out) + } + + out, err = inspectMountSourceField("dark_helmet", prefix+slash+`foo`) + c.Assert(err, check.IsNil) + if !strings.Contains(strings.ToLower(out), strings.ToLower(volumesConfigPath)) { + c.Fatalf("Volume was not defined for %s/foo\n%q", prefix, out) + } + + out, err = inspectMountSourceField("dark_helmet", prefix+slash+"bar"+slash) + if err != errMountNotFound { + c.Fatalf("Found unexpected volume entry for '%s/bar/' in volumes\n%q", prefix, out) + } + + out, err = inspectMountSourceField("dark_helmet", prefix+slash+"bar") + c.Assert(err, check.IsNil) + if !strings.Contains(strings.ToLower(out), strings.ToLower(volumesConfigPath)) { + c.Fatalf("Volume was not defined for %s/bar\n%q", prefix, out) + } +} + +// Regression test for #3631 +func (s *DockerSuite) TestRunSlowStdoutConsumer(c *check.C) { + // TODO Windows: This should be able to run on Windows if can find an + // alternate to /dev/zero and /dev/stdout. + testRequires(c, DaemonIsLinux) + cont := exec.Command(dockerBinary, "run", "--rm", "busybox", "/bin/sh", "-c", "dd if=/dev/zero of=/dev/stdout bs=1024 count=2000 | catv") + + stdout, err := cont.StdoutPipe() + if err != nil { + c.Fatal(err) + } + + if err := cont.Start(); err != nil { + c.Fatal(err) + } + n, err := consumeWithSpeed(stdout, 10000, 5*time.Millisecond, nil) + if err != nil { + c.Fatal(err) + } + + expected := 2 * 1024 * 2000 + if n != expected { + c.Fatalf("Expected %d, got %d", expected, n) + } +} + +func (s *DockerSuite) TestRunAllowPortRangeThroughExpose(c *check.C) { + // TODO Windows: -P is not currently supported. Also network + // settings are not propagated back. + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "--expose", "3000-3003", "-P", "busybox", "top") + + id := strings.TrimSpace(out) + portstr := inspectFieldJSON(c, id, "NetworkSettings.Ports") + var ports nat.PortMap + if err := unmarshalJSON([]byte(portstr), &ports); err != nil { + c.Fatal(err) + } + for port, binding := range ports { + portnum, _ := strconv.Atoi(strings.Split(string(port), "/")[0]) + if portnum < 3000 || portnum > 3003 { + c.Fatalf("Port %d is out of range ", portnum) + } + if binding == nil || len(binding) != 1 || len(binding[0].HostPort) == 0 { + c.Fatalf("Port is not mapped for the port %s", port) + } + } +} + +func (s *DockerSuite) TestRunExposePort(c *check.C) { + out, _, err := dockerCmdWithError("run", "--expose", "80000", "busybox") + c.Assert(err, checker.NotNil, check.Commentf("--expose with an invalid port should error out")) + c.Assert(out, checker.Contains, "invalid range format for --expose") +} + +func (s *DockerSuite) TestRunUnknownCommand(c *check.C) { + out, _, _ := dockerCmdWithStdoutStderr(c, "create", "busybox", "/bin/nada") + + cID := strings.TrimSpace(out) + _, _, err := dockerCmdWithError("start", cID) + + // Windows and Linux are different here by architectural design. Linux will + // fail to start the container, so an error is expected. Windows will + // successfully start the container, and once started attempt to execute + // the command which will fail. + if daemonPlatform == "windows" { + // Wait for it to exit. + waitExited(cID, 30*time.Second) + c.Assert(err, check.IsNil) + } else { + c.Assert(err, check.NotNil) + } + + rc := inspectField(c, cID, "State.ExitCode") + if rc == "0" { + c.Fatalf("ExitCode(%v) cannot be 0", rc) + } +} + +func (s *DockerSuite) TestRunModeIpcHost(c *check.C) { + // Not applicable on Windows as uses Unix-specific capabilities + testRequires(c, SameHostDaemon, DaemonIsLinux, NotUserNamespace) + + hostIpc, err := os.Readlink("/proc/1/ns/ipc") + if err != nil { + c.Fatal(err) + } + + out, _ := dockerCmd(c, "run", "--ipc=host", "busybox", "readlink", "/proc/self/ns/ipc") + out = strings.Trim(out, "\n") + if hostIpc != out { + c.Fatalf("IPC different with --ipc=host %s != %s\n", hostIpc, out) + } + + out, _ = dockerCmd(c, "run", "busybox", "readlink", "/proc/self/ns/ipc") + out = strings.Trim(out, "\n") + if hostIpc == out { + c.Fatalf("IPC should be different without --ipc=host %s == %s\n", hostIpc, out) + } +} + +func (s *DockerSuite) TestRunModeIpcContainer(c *check.C) { + // Not applicable on Windows as uses Unix-specific capabilities + testRequires(c, SameHostDaemon, DaemonIsLinux, NotUserNamespace) + + out, _ := dockerCmd(c, "run", "-d", "busybox", "sh", "-c", "echo -n test > /dev/shm/test && touch /dev/mqueue/toto && top") + + id := strings.TrimSpace(out) + state := inspectField(c, id, "State.Running") + if state != "true" { + c.Fatal("Container state is 'not running'") + } + pid1 := inspectField(c, id, "State.Pid") + + parentContainerIpc, err := os.Readlink(fmt.Sprintf("/proc/%s/ns/ipc", pid1)) + if err != nil { + c.Fatal(err) + } + + out, _ = dockerCmd(c, "run", fmt.Sprintf("--ipc=container:%s", id), "busybox", "readlink", "/proc/self/ns/ipc") + out = strings.Trim(out, "\n") + if parentContainerIpc != out { + c.Fatalf("IPC different with --ipc=container:%s %s != %s\n", id, parentContainerIpc, out) + } + + catOutput, _ := dockerCmd(c, "run", fmt.Sprintf("--ipc=container:%s", id), "busybox", "cat", "/dev/shm/test") + if catOutput != "test" { + c.Fatalf("Output of /dev/shm/test expected test but found: %s", catOutput) + } + + // check that /dev/mqueue is actually of mqueue type + grepOutput, _ := dockerCmd(c, "run", fmt.Sprintf("--ipc=container:%s", id), "busybox", "grep", "/dev/mqueue", "/proc/mounts") + if !strings.HasPrefix(grepOutput, "mqueue /dev/mqueue mqueue rw") { + c.Fatalf("Output of 'grep /proc/mounts' expected 'mqueue /dev/mqueue mqueue rw' but found: %s", grepOutput) + } + + lsOutput, _ := dockerCmd(c, "run", fmt.Sprintf("--ipc=container:%s", id), "busybox", "ls", "/dev/mqueue") + lsOutput = strings.Trim(lsOutput, "\n") + if lsOutput != "toto" { + c.Fatalf("Output of 'ls /dev/mqueue' expected 'toto' but found: %s", lsOutput) + } +} + +func (s *DockerSuite) TestRunModeIpcContainerNotExists(c *check.C) { + // Not applicable on Windows as uses Unix-specific capabilities + testRequires(c, DaemonIsLinux, NotUserNamespace) + out, _, err := dockerCmdWithError("run", "-d", "--ipc", "container:abcd1234", "busybox", "top") + if !strings.Contains(out, "abcd1234") || err == nil { + c.Fatalf("run IPC from a non exists container should with correct error out") + } +} + +func (s *DockerSuite) TestRunModeIpcContainerNotRunning(c *check.C) { + // Not applicable on Windows as uses Unix-specific capabilities + testRequires(c, SameHostDaemon, DaemonIsLinux, NotUserNamespace) + + out, _ := dockerCmd(c, "create", "busybox") + + id := strings.TrimSpace(out) + out, _, err := dockerCmdWithError("run", fmt.Sprintf("--ipc=container:%s", id), "busybox") + if err == nil { + c.Fatalf("Run container with ipc mode container should fail with non running container: %s\n%s", out, err) + } +} + +func (s *DockerSuite) TestRunMountShmMqueueFromHost(c *check.C) { + // Not applicable on Windows as uses Unix-specific capabilities + testRequires(c, SameHostDaemon, DaemonIsLinux, NotUserNamespace) + + dockerCmd(c, "run", "-d", "--name", "shmfromhost", "-v", "/dev/shm:/dev/shm", "-v", "/dev/mqueue:/dev/mqueue", "busybox", "sh", "-c", "echo -n test > /dev/shm/test && touch /dev/mqueue/toto && top") + defer os.Remove("/dev/mqueue/toto") + defer os.Remove("/dev/shm/test") + volPath, err := inspectMountSourceField("shmfromhost", "/dev/shm") + c.Assert(err, checker.IsNil) + if volPath != "/dev/shm" { + c.Fatalf("volumePath should have been /dev/shm, was %s", volPath) + } + + out, _ := dockerCmd(c, "run", "--name", "ipchost", "--ipc", "host", "busybox", "cat", "/dev/shm/test") + if out != "test" { + c.Fatalf("Output of /dev/shm/test expected test but found: %s", out) + } + + // Check that the mq was created + if _, err := os.Stat("/dev/mqueue/toto"); err != nil { + c.Fatalf("Failed to confirm '/dev/mqueue/toto' presence on host: %s", err.Error()) + } +} + +func (s *DockerSuite) TestContainerNetworkMode(c *check.C) { + // Not applicable on Windows as uses Unix-specific capabilities + testRequires(c, SameHostDaemon, DaemonIsLinux, NotUserNamespace) + + out, _ := dockerCmd(c, "run", "-d", "busybox", "top") + id := strings.TrimSpace(out) + c.Assert(waitRun(id), check.IsNil) + pid1 := inspectField(c, id, "State.Pid") + + parentContainerNet, err := os.Readlink(fmt.Sprintf("/proc/%s/ns/net", pid1)) + if err != nil { + c.Fatal(err) + } + + out, _ = dockerCmd(c, "run", fmt.Sprintf("--net=container:%s", id), "busybox", "readlink", "/proc/self/ns/net") + out = strings.Trim(out, "\n") + if parentContainerNet != out { + c.Fatalf("NET different with --net=container:%s %s != %s\n", id, parentContainerNet, out) + } +} + +func (s *DockerSuite) TestRunModePidHost(c *check.C) { + // Not applicable on Windows as uses Unix-specific capabilities + testRequires(c, SameHostDaemon, DaemonIsLinux, NotUserNamespace) + + hostPid, err := os.Readlink("/proc/1/ns/pid") + if err != nil { + c.Fatal(err) + } + + out, _ := dockerCmd(c, "run", "--pid=host", "busybox", "readlink", "/proc/self/ns/pid") + out = strings.Trim(out, "\n") + if hostPid != out { + c.Fatalf("PID different with --pid=host %s != %s\n", hostPid, out) + } + + out, _ = dockerCmd(c, "run", "busybox", "readlink", "/proc/self/ns/pid") + out = strings.Trim(out, "\n") + if hostPid == out { + c.Fatalf("PID should be different without --pid=host %s == %s\n", hostPid, out) + } +} + +func (s *DockerSuite) TestRunModeUTSHost(c *check.C) { + // Not applicable on Windows as uses Unix-specific capabilities + testRequires(c, SameHostDaemon, DaemonIsLinux) + + hostUTS, err := os.Readlink("/proc/1/ns/uts") + if err != nil { + c.Fatal(err) + } + + out, _ := dockerCmd(c, "run", "--uts=host", "busybox", "readlink", "/proc/self/ns/uts") + out = strings.Trim(out, "\n") + if hostUTS != out { + c.Fatalf("UTS different with --uts=host %s != %s\n", hostUTS, out) + } + + out, _ = dockerCmd(c, "run", "busybox", "readlink", "/proc/self/ns/uts") + out = strings.Trim(out, "\n") + if hostUTS == out { + c.Fatalf("UTS should be different without --uts=host %s == %s\n", hostUTS, out) + } + + out, _ = dockerCmdWithFail(c, "run", "-h=name", "--uts=host", "busybox", "ps") + c.Assert(out, checker.Contains, runconfig.ErrConflictUTSHostname.Error()) +} + +func (s *DockerSuite) TestRunTLSverify(c *check.C) { + // Remote daemons use TLS and this test is not applicable when TLS is required. + testRequires(c, SameHostDaemon) + if out, code, err := dockerCmdWithError("ps"); err != nil || code != 0 { + c.Fatalf("Should have worked: %v:\n%v", err, out) + } + + // Regardless of whether we specify true or false we need to + // test to make sure tls is turned on if --tlsverify is specified at all + out, code, err := dockerCmdWithError("--tlsverify=false", "ps") + if err == nil || code == 0 || !strings.Contains(out, "trying to connect") { + c.Fatalf("Should have failed: \net:%v\nout:%v\nerr:%v", code, out, err) + } + + out, code, err = dockerCmdWithError("--tlsverify=true", "ps") + if err == nil || code == 0 || !strings.Contains(out, "cert") { + c.Fatalf("Should have failed: \net:%v\nout:%v\nerr:%v", code, out, err) + } +} + +func (s *DockerSuite) TestRunPortFromDockerRangeInUse(c *check.C) { + // TODO Windows. Once moved to libnetwork/CNM, this may be able to be + // re-instated. + testRequires(c, DaemonIsLinux) + // first find allocator current position + out, _ := dockerCmd(c, "run", "-d", "-p", ":80", "busybox", "top") + + id := strings.TrimSpace(out) + out, _ = dockerCmd(c, "port", id) + + out = strings.TrimSpace(out) + if out == "" { + c.Fatal("docker port command output is empty") + } + out = strings.Split(out, ":")[1] + lastPort, err := strconv.Atoi(out) + if err != nil { + c.Fatal(err) + } + port := lastPort + 1 + l, err := net.Listen("tcp", ":"+strconv.Itoa(port)) + if err != nil { + c.Fatal(err) + } + defer l.Close() + + out, _ = dockerCmd(c, "run", "-d", "-p", ":80", "busybox", "top") + + id = strings.TrimSpace(out) + dockerCmd(c, "port", id) +} + +func (s *DockerSuite) TestRunTTYWithPipe(c *check.C) { + errChan := make(chan error) + go func() { + defer close(errChan) + + cmd := exec.Command(dockerBinary, "run", "-ti", "busybox", "true") + if _, err := cmd.StdinPipe(); err != nil { + errChan <- err + return + } + + expected := "cannot enable tty mode" + if out, _, err := runCommandWithOutput(cmd); err == nil { + errChan <- fmt.Errorf("run should have failed") + return + } else if !strings.Contains(out, expected) { + errChan <- fmt.Errorf("run failed with error %q: expected %q", out, expected) + return + } + }() + + select { + case err := <-errChan: + c.Assert(err, check.IsNil) + case <-time.After(6 * time.Second): + c.Fatal("container is running but should have failed") + } +} + +func (s *DockerSuite) TestRunNonLocalMacAddress(c *check.C) { + addr := "00:16:3E:08:00:50" + cmd := "ifconfig" + image := "busybox" + expected := addr + + if daemonPlatform == "windows" { + cmd = "ipconfig /all" + image = WindowsBaseImage + expected = strings.Replace(strings.ToUpper(addr), ":", "-", -1) + + } + + if out, _ := dockerCmd(c, "run", "--mac-address", addr, image, cmd); !strings.Contains(out, expected) { + c.Fatalf("Output should have contained %q: %s", expected, out) + } +} + +func (s *DockerSuite) TestRunNetHost(c *check.C) { + // Not applicable on Windows as uses Unix-specific capabilities + testRequires(c, SameHostDaemon, DaemonIsLinux, NotUserNamespace) + + hostNet, err := os.Readlink("/proc/1/ns/net") + if err != nil { + c.Fatal(err) + } + + out, _ := dockerCmd(c, "run", "--net=host", "busybox", "readlink", "/proc/self/ns/net") + out = strings.Trim(out, "\n") + if hostNet != out { + c.Fatalf("Net namespace different with --net=host %s != %s\n", hostNet, out) + } + + out, _ = dockerCmd(c, "run", "busybox", "readlink", "/proc/self/ns/net") + out = strings.Trim(out, "\n") + if hostNet == out { + c.Fatalf("Net namespace should be different without --net=host %s == %s\n", hostNet, out) + } +} + +func (s *DockerSuite) TestRunNetHostTwiceSameName(c *check.C) { + // TODO Windows. As Windows networking evolves and converges towards + // CNM, this test may be possible to enable on Windows. + testRequires(c, SameHostDaemon, DaemonIsLinux, NotUserNamespace) + + dockerCmd(c, "run", "--rm", "--name=thost", "--net=host", "busybox", "true") + dockerCmd(c, "run", "--rm", "--name=thost", "--net=host", "busybox", "true") +} + +func (s *DockerSuite) TestRunNetContainerWhichHost(c *check.C) { + // Not applicable on Windows as uses Unix-specific capabilities + testRequires(c, SameHostDaemon, DaemonIsLinux, NotUserNamespace) + + hostNet, err := os.Readlink("/proc/1/ns/net") + if err != nil { + c.Fatal(err) + } + + dockerCmd(c, "run", "-d", "--net=host", "--name=test", "busybox", "top") + + out, _ := dockerCmd(c, "run", "--net=container:test", "busybox", "readlink", "/proc/self/ns/net") + out = strings.Trim(out, "\n") + if hostNet != out { + c.Fatalf("Container should have host network namespace") + } +} + +func (s *DockerSuite) TestRunAllowPortRangeThroughPublish(c *check.C) { + // TODO Windows. This may be possible to enable in the future. However, + // Windows does not currently support --expose, or populate the network + // settings seen through inspect. + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "--expose", "3000-3003", "-p", "3000-3003", "busybox", "top") + + id := strings.TrimSpace(out) + portstr := inspectFieldJSON(c, id, "NetworkSettings.Ports") + + var ports nat.PortMap + err := unmarshalJSON([]byte(portstr), &ports) + c.Assert(err, checker.IsNil, check.Commentf("failed to unmarshal: %v", portstr)) + for port, binding := range ports { + portnum, _ := strconv.Atoi(strings.Split(string(port), "/")[0]) + if portnum < 3000 || portnum > 3003 { + c.Fatalf("Port %d is out of range ", portnum) + } + if binding == nil || len(binding) != 1 || len(binding[0].HostPort) == 0 { + c.Fatal("Port is not mapped for the port "+port, out) + } + } +} + +func (s *DockerSuite) TestRunSetDefaultRestartPolicy(c *check.C) { + dockerCmd(c, "run", "-d", "--name", "test", "busybox", "sleep", "30") + out := inspectField(c, "test", "HostConfig.RestartPolicy.Name") + if out != "no" { + c.Fatalf("Set default restart policy failed") + } +} + +func (s *DockerSuite) TestRunRestartMaxRetries(c *check.C) { + out, _ := dockerCmd(c, "run", "-d", "--restart=on-failure:3", "busybox", "false") + timeout := 10 * time.Second + if daemonPlatform == "windows" { + timeout = 120 * time.Second + } + + id := strings.TrimSpace(string(out)) + if err := waitInspect(id, "{{ .State.Restarting }} {{ .State.Running }}", "false false", timeout); err != nil { + c.Fatal(err) + } + + count := inspectField(c, id, "RestartCount") + if count != "3" { + c.Fatalf("Container was restarted %s times, expected %d", count, 3) + } + + MaximumRetryCount := inspectField(c, id, "HostConfig.RestartPolicy.MaximumRetryCount") + if MaximumRetryCount != "3" { + c.Fatalf("Container Maximum Retry Count is %s, expected %s", MaximumRetryCount, "3") + } +} + +func (s *DockerSuite) TestRunContainerWithWritableRootfs(c *check.C) { + dockerCmd(c, "run", "--rm", "busybox", "touch", "/file") +} + +func (s *DockerSuite) TestRunContainerWithReadonlyRootfs(c *check.C) { + // Not applicable on Windows which does not support --read-only + testRequires(c, DaemonIsLinux) + + testReadOnlyFile(c, "/file", "/etc/hosts", "/etc/resolv.conf", "/etc/hostname", "/sys/kernel", "/dev/.dont.touch.me") +} + +func (s *DockerSuite) TestPermissionsPtsReadonlyRootfs(c *check.C) { + // Not applicable on Windows due to use of Unix specific functionality, plus + // the use of --read-only which is not supported. + // --read-only + userns has remount issues + testRequires(c, DaemonIsLinux, NotUserNamespace) + + // Ensure we have not broken writing /dev/pts + out, status := dockerCmd(c, "run", "--read-only", "--rm", "busybox", "mount") + if status != 0 { + c.Fatal("Could not obtain mounts when checking /dev/pts mntpnt.") + } + expected := "type devpts (rw," + if !strings.Contains(string(out), expected) { + c.Fatalf("expected output to contain %s but contains %s", expected, out) + } +} + +func testReadOnlyFile(c *check.C, filenames ...string) { + // Not applicable on Windows which does not support --read-only + testRequires(c, DaemonIsLinux, NotUserNamespace) + touch := "touch " + strings.Join(filenames, " ") + out, _, err := dockerCmdWithError("run", "--read-only", "--rm", "busybox", "sh", "-c", touch) + c.Assert(err, checker.NotNil) + + for _, f := range filenames { + expected := "touch: " + f + ": Read-only file system" + c.Assert(out, checker.Contains, expected) + } + + out, _, err = dockerCmdWithError("run", "--read-only", "--privileged", "--rm", "busybox", "sh", "-c", touch) + c.Assert(err, checker.NotNil) + + for _, f := range filenames { + expected := "touch: " + f + ": Read-only file system" + c.Assert(out, checker.Contains, expected) + } +} + +func (s *DockerSuite) TestRunContainerWithReadonlyEtcHostsAndLinkedContainer(c *check.C) { + // Not applicable on Windows which does not support --link + // --read-only + userns has remount issues + testRequires(c, DaemonIsLinux, NotUserNamespace) + + dockerCmd(c, "run", "-d", "--name", "test-etc-hosts-ro-linked", "busybox", "top") + + out, _ := dockerCmd(c, "run", "--read-only", "--link", "test-etc-hosts-ro-linked:testlinked", "busybox", "cat", "/etc/hosts") + if !strings.Contains(string(out), "testlinked") { + c.Fatal("Expected /etc/hosts to be updated even if --read-only enabled") + } +} + +func (s *DockerSuite) TestRunContainerWithReadonlyRootfsWithDnsFlag(c *check.C) { + // Not applicable on Windows which does not support either --read-only or --dns. + // --read-only + userns has remount issues + testRequires(c, DaemonIsLinux, NotUserNamespace) + + out, _ := dockerCmd(c, "run", "--read-only", "--dns", "1.1.1.1", "busybox", "/bin/cat", "/etc/resolv.conf") + if !strings.Contains(string(out), "1.1.1.1") { + c.Fatal("Expected /etc/resolv.conf to be updated even if --read-only enabled and --dns flag used") + } +} + +func (s *DockerSuite) TestRunContainerWithReadonlyRootfsWithAddHostFlag(c *check.C) { + // Not applicable on Windows which does not support --read-only + // --read-only + userns has remount issues + testRequires(c, DaemonIsLinux, NotUserNamespace) + + out, _ := dockerCmd(c, "run", "--read-only", "--add-host", "testreadonly:127.0.0.1", "busybox", "/bin/cat", "/etc/hosts") + if !strings.Contains(string(out), "testreadonly") { + c.Fatal("Expected /etc/hosts to be updated even if --read-only enabled and --add-host flag used") + } +} + +func (s *DockerSuite) TestRunVolumesFromRestartAfterRemoved(c *check.C) { + prefix, _ := getPrefixAndSlashFromDaemonPlatform() + dockerCmd(c, "run", "-d", "--name", "voltest", "-v", prefix+"/foo", "busybox", "sleep", "60") + dockerCmd(c, "run", "-d", "--name", "restarter", "--volumes-from", "voltest", "busybox", "sleep", "60") + + // Remove the main volume container and restart the consuming container + dockerCmd(c, "rm", "-f", "voltest") + + // This should not fail since the volumes-from were already applied + dockerCmd(c, "restart", "restarter") +} + +// run container with --rm should remove container if exit code != 0 +func (s *DockerSuite) TestRunContainerWithRmFlagExitCodeNotEqualToZero(c *check.C) { + name := "flowers" + out, _, err := dockerCmdWithError("run", "--name", name, "--rm", "busybox", "ls", "/notexists") + if err == nil { + c.Fatal("Expected docker run to fail", out, err) + } + + out, err = getAllContainers() + if err != nil { + c.Fatal(out, err) + } + + if out != "" { + c.Fatal("Expected not to have containers", out) + } +} + +func (s *DockerSuite) TestRunContainerWithRmFlagCannotStartContainer(c *check.C) { + name := "sparkles" + out, _, err := dockerCmdWithError("run", "--name", name, "--rm", "busybox", "commandNotFound") + if err == nil { + c.Fatal("Expected docker run to fail", out, err) + } + + out, err = getAllContainers() + if err != nil { + c.Fatal(out, err) + } + + if out != "" { + c.Fatal("Expected not to have containers", out) + } +} + +func (s *DockerSuite) TestRunPidHostWithChildIsKillable(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + testRequires(c, DaemonIsLinux, NotUserNamespace) + name := "ibuildthecloud" + dockerCmd(c, "run", "-d", "--pid=host", "--name", name, "busybox", "sh", "-c", "sleep 30; echo hi") + + c.Assert(waitRun(name), check.IsNil) + + errchan := make(chan error) + go func() { + if out, _, err := dockerCmdWithError("kill", name); err != nil { + errchan <- fmt.Errorf("%v:\n%s", err, out) + } + close(errchan) + }() + select { + case err := <-errchan: + c.Assert(err, check.IsNil) + case <-time.After(5 * time.Second): + c.Fatal("Kill container timed out") + } +} + +func (s *DockerSuite) TestRunWithTooSmallMemoryLimit(c *check.C) { + // TODO Windows. This may be possible to enable once Windows supports + // memory limits on containers + testRequires(c, DaemonIsLinux) + // this memory limit is 1 byte less than the min, which is 4MB + // https://github.com/docker/docker/blob/v1.5.0/daemon/create.go#L22 + out, _, err := dockerCmdWithError("run", "-m", "4194303", "busybox") + if err == nil || !strings.Contains(out, "Minimum memory limit allowed is 4MB") { + c.Fatalf("expected run to fail when using too low a memory limit: %q", out) + } +} + +func (s *DockerSuite) TestRunWriteToProcAsound(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + testRequires(c, DaemonIsLinux) + _, code, err := dockerCmdWithError("run", "busybox", "sh", "-c", "echo 111 >> /proc/asound/version") + if err == nil || code == 0 { + c.Fatal("standard container should not be able to write to /proc/asound") + } +} + +func (s *DockerSuite) TestRunReadProcTimer(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + testRequires(c, DaemonIsLinux) + out, code, err := dockerCmdWithError("run", "busybox", "cat", "/proc/timer_stats") + if code != 0 { + return + } + if err != nil { + c.Fatal(err) + } + if strings.Trim(out, "\n ") != "" { + c.Fatalf("expected to receive no output from /proc/timer_stats but received %q", out) + } +} + +func (s *DockerSuite) TestRunReadProcLatency(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + testRequires(c, DaemonIsLinux) + // some kernels don't have this configured so skip the test if this file is not found + // on the host running the tests. + if _, err := os.Stat("/proc/latency_stats"); err != nil { + c.Skip("kernel doesn't have latency_stats configured") + return + } + out, code, err := dockerCmdWithError("run", "busybox", "cat", "/proc/latency_stats") + if code != 0 { + return + } + if err != nil { + c.Fatal(err) + } + if strings.Trim(out, "\n ") != "" { + c.Fatalf("expected to receive no output from /proc/latency_stats but received %q", out) + } +} + +func (s *DockerSuite) TestRunReadFilteredProc(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + testRequires(c, Apparmor, DaemonIsLinux, NotUserNamespace) + + testReadPaths := []string{ + "/proc/latency_stats", + "/proc/timer_stats", + "/proc/kcore", + } + for i, filePath := range testReadPaths { + name := fmt.Sprintf("procsieve-%d", i) + shellCmd := fmt.Sprintf("exec 3<%s", filePath) + + out, exitCode, err := dockerCmdWithError("run", "--privileged", "--security-opt", "apparmor=docker-default", "--name", name, "busybox", "sh", "-c", shellCmd) + if exitCode != 0 { + return + } + if err != nil { + c.Fatalf("Open FD for read should have failed with permission denied, got: %s, %v", out, err) + } + } +} + +func (s *DockerSuite) TestMountIntoProc(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + testRequires(c, DaemonIsLinux) + _, code, err := dockerCmdWithError("run", "-v", "/proc//sys", "busybox", "true") + if err == nil || code == 0 { + c.Fatal("container should not be able to mount into /proc") + } +} + +func (s *DockerSuite) TestMountIntoSys(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + testRequires(c, DaemonIsLinux) + testRequires(c, NotUserNamespace) + dockerCmd(c, "run", "-v", "/sys/fs/cgroup", "busybox", "true") +} + +func (s *DockerSuite) TestRunUnshareProc(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + testRequires(c, Apparmor, DaemonIsLinux, NotUserNamespace) + + // In this test goroutines are used to run test cases in parallel to prevent the test from taking a long time to run. + errChan := make(chan error) + + go func() { + name := "acidburn" + out, _, err := dockerCmdWithError("run", "--name", name, "--security-opt", "seccomp=unconfined", "debian:jessie", "unshare", "-p", "-m", "-f", "-r", "--mount-proc=/proc", "mount") + if err == nil || + !(strings.Contains(strings.ToLower(out), "permission denied") || + strings.Contains(strings.ToLower(out), "operation not permitted")) { + errChan <- fmt.Errorf("unshare with --mount-proc should have failed with 'permission denied' or 'operation not permitted', got: %s, %v", out, err) + } else { + errChan <- nil + } + }() + + go func() { + name := "cereal" + out, _, err := dockerCmdWithError("run", "--name", name, "--security-opt", "seccomp=unconfined", "debian:jessie", "unshare", "-p", "-m", "-f", "-r", "mount", "-t", "proc", "none", "/proc") + if err == nil || + !(strings.Contains(strings.ToLower(out), "mount: cannot mount none") || + strings.Contains(strings.ToLower(out), "permission denied") || + strings.Contains(strings.ToLower(out), "operation not permitted")) { + errChan <- fmt.Errorf("unshare and mount of /proc should have failed with 'mount: cannot mount none' or 'permission denied', got: %s, %v", out, err) + } else { + errChan <- nil + } + }() + + /* Ensure still fails if running privileged with the default policy */ + go func() { + name := "crashoverride" + out, _, err := dockerCmdWithError("run", "--privileged", "--security-opt", "seccomp=unconfined", "--security-opt", "apparmor=docker-default", "--name", name, "debian:jessie", "unshare", "-p", "-m", "-f", "-r", "mount", "-t", "proc", "none", "/proc") + if err == nil || + !(strings.Contains(strings.ToLower(out), "mount: cannot mount none") || + strings.Contains(strings.ToLower(out), "permission denied") || + strings.Contains(strings.ToLower(out), "operation not permitted")) { + errChan <- fmt.Errorf("privileged unshare with apparmor should have failed with 'mount: cannot mount none' or 'permission denied', got: %s, %v", out, err) + } else { + errChan <- nil + } + }() + + for i := 0; i < 3; i++ { + err := <-errChan + if err != nil { + c.Fatal(err) + } + } +} + +func (s *DockerSuite) TestRunPublishPort(c *check.C) { + // TODO Windows: This may be possible once Windows moves to libnetwork and CNM + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "-d", "--name", "test", "--expose", "8080", "busybox", "top") + out, _ := dockerCmd(c, "port", "test") + out = strings.Trim(out, "\r\n") + if out != "" { + c.Fatalf("run without --publish-all should not publish port, out should be nil, but got: %s", out) + } +} + +// Issue #10184. +func (s *DockerSuite) TestDevicePermissions(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + testRequires(c, DaemonIsLinux) + const permissions = "crw-rw-rw-" + out, status := dockerCmd(c, "run", "--device", "/dev/fuse:/dev/fuse:mrw", "busybox:latest", "ls", "-l", "/dev/fuse") + if status != 0 { + c.Fatalf("expected status 0, got %d", status) + } + if !strings.HasPrefix(out, permissions) { + c.Fatalf("output should begin with %q, got %q", permissions, out) + } +} + +func (s *DockerSuite) TestRunCapAddCHOWN(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "--cap-drop=ALL", "--cap-add=CHOWN", "busybox", "sh", "-c", "adduser -D -H newuser && chown newuser /home && echo ok") + + if actual := strings.Trim(out, "\r\n"); actual != "ok" { + c.Fatalf("expected output ok received %s", actual) + } +} + +// https://github.com/docker/docker/pull/14498 +func (s *DockerSuite) TestVolumeFromMixedRWOptions(c *check.C) { + // TODO Windows post TP4. Enable the read-only bits once they are + // supported on the platform. + prefix, slash := getPrefixAndSlashFromDaemonPlatform() + + dockerCmd(c, "run", "--name", "parent", "-v", prefix+"/test", "busybox", "true") + if daemonPlatform != "windows" { + dockerCmd(c, "run", "--volumes-from", "parent:ro", "--name", "test-volumes-1", "busybox", "true") + } + dockerCmd(c, "run", "--volumes-from", "parent:rw", "--name", "test-volumes-2", "busybox", "true") + + if daemonPlatform != "windows" { + mRO, err := inspectMountPoint("test-volumes-1", prefix+slash+"test") + c.Assert(err, checker.IsNil, check.Commentf("failed to inspect mount point")) + if mRO.RW { + c.Fatalf("Expected RO volume was RW") + } + } + + mRW, err := inspectMountPoint("test-volumes-2", prefix+slash+"test") + c.Assert(err, checker.IsNil, check.Commentf("failed to inspect mount point")) + if !mRW.RW { + c.Fatalf("Expected RW volume was RO") + } +} + +func (s *DockerSuite) TestRunWriteFilteredProc(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + testRequires(c, Apparmor, DaemonIsLinux, NotUserNamespace) + + testWritePaths := []string{ + /* modprobe and core_pattern should both be denied by generic + * policy of denials for /proc/sys/kernel. These files have been + * picked to be checked as they are particularly sensitive to writes */ + "/proc/sys/kernel/modprobe", + "/proc/sys/kernel/core_pattern", + "/proc/sysrq-trigger", + "/proc/kcore", + } + for i, filePath := range testWritePaths { + name := fmt.Sprintf("writeprocsieve-%d", i) + + shellCmd := fmt.Sprintf("exec 3>%s", filePath) + out, code, err := dockerCmdWithError("run", "--privileged", "--security-opt", "apparmor=docker-default", "--name", name, "busybox", "sh", "-c", shellCmd) + if code != 0 { + return + } + if err != nil { + c.Fatalf("Open FD for write should have failed with permission denied, got: %s, %v", out, err) + } + } +} + +func (s *DockerSuite) TestRunNetworkFilesBindMount(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + testRequires(c, SameHostDaemon, DaemonIsLinux) + + expected := "test123" + + filename := createTmpFile(c, expected) + defer os.Remove(filename) + + nwfiles := []string{"/etc/resolv.conf", "/etc/hosts", "/etc/hostname"} + + for i := range nwfiles { + actual, _ := dockerCmd(c, "run", "-v", filename+":"+nwfiles[i], "busybox", "cat", nwfiles[i]) + if actual != expected { + c.Fatalf("expected %s be: %q, but was: %q", nwfiles[i], expected, actual) + } + } +} + +func (s *DockerSuite) TestRunNetworkFilesBindMountRO(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + testRequires(c, SameHostDaemon, DaemonIsLinux) + + filename := createTmpFile(c, "test123") + defer os.Remove(filename) + + nwfiles := []string{"/etc/resolv.conf", "/etc/hosts", "/etc/hostname"} + + for i := range nwfiles { + _, exitCode, err := dockerCmdWithError("run", "-v", filename+":"+nwfiles[i]+":ro", "busybox", "touch", nwfiles[i]) + if err == nil || exitCode == 0 { + c.Fatalf("run should fail because bind mount of %s is ro: exit code %d", nwfiles[i], exitCode) + } + } +} + +func (s *DockerSuite) TestRunNetworkFilesBindMountROFilesystem(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + // --read-only + userns has remount issues + testRequires(c, SameHostDaemon, DaemonIsLinux, NotUserNamespace) + + filename := createTmpFile(c, "test123") + defer os.Remove(filename) + + nwfiles := []string{"/etc/resolv.conf", "/etc/hosts", "/etc/hostname"} + + for i := range nwfiles { + _, exitCode := dockerCmd(c, "run", "-v", filename+":"+nwfiles[i], "--read-only", "busybox", "touch", nwfiles[i]) + if exitCode != 0 { + c.Fatalf("run should not fail because %s is mounted writable on read-only root filesystem: exit code %d", nwfiles[i], exitCode) + } + } + + for i := range nwfiles { + _, exitCode, err := dockerCmdWithError("run", "-v", filename+":"+nwfiles[i]+":ro", "--read-only", "busybox", "touch", nwfiles[i]) + if err == nil || exitCode == 0 { + c.Fatalf("run should fail because %s is mounted read-only on read-only root filesystem: exit code %d", nwfiles[i], exitCode) + } + } +} + +func (s *DockerTrustSuite) TestTrustedRun(c *check.C) { + // Windows does not support this functionality + testRequires(c, DaemonIsLinux) + repoName := s.setupTrustedImage(c, "trusted-run") + + // Try run + runCmd := exec.Command(dockerBinary, "run", repoName) + s.trustedCmd(runCmd) + out, _, err := runCommandWithOutput(runCmd) + if err != nil { + c.Fatalf("Error running trusted run: %s\n%s\n", err, out) + } + + if !strings.Contains(string(out), "Tagging") { + c.Fatalf("Missing expected output on trusted push:\n%s", out) + } + + dockerCmd(c, "rmi", repoName) + + // Try untrusted run to ensure we pushed the tag to the registry + runCmd = exec.Command(dockerBinary, "run", "--disable-content-trust=true", repoName) + s.trustedCmd(runCmd) + out, _, err = runCommandWithOutput(runCmd) + if err != nil { + c.Fatalf("Error running trusted run: %s\n%s", err, out) + } + + if !strings.Contains(string(out), "Status: Downloaded") { + c.Fatalf("Missing expected output on trusted run with --disable-content-trust:\n%s", out) + } +} + +func (s *DockerTrustSuite) TestUntrustedRun(c *check.C) { + // Windows does not support this functionality + testRequires(c, DaemonIsLinux) + repoName := fmt.Sprintf("%v/dockercliuntrusted/runtest:latest", privateRegistryURL) + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", repoName) + dockerCmd(c, "push", repoName) + dockerCmd(c, "rmi", repoName) + + // Try trusted run on untrusted tag + runCmd := exec.Command(dockerBinary, "run", repoName) + s.trustedCmd(runCmd) + out, _, err := runCommandWithOutput(runCmd) + if err == nil { + c.Fatalf("Error expected when running trusted run with:\n%s", out) + } + + if !strings.Contains(string(out), "does not have trust data for") { + c.Fatalf("Missing expected output on trusted run:\n%s", out) + } +} + +func (s *DockerTrustSuite) TestRunWhenCertExpired(c *check.C) { + // Windows does not support this functionality + testRequires(c, DaemonIsLinux) + c.Skip("Currently changes system time, causing instability") + repoName := s.setupTrustedImage(c, "trusted-run-expired") + + // Certificates have 10 years of expiration + elevenYearsFromNow := time.Now().Add(time.Hour * 24 * 365 * 11) + + runAtDifferentDate(elevenYearsFromNow, func() { + // Try run + runCmd := exec.Command(dockerBinary, "run", repoName) + s.trustedCmd(runCmd) + out, _, err := runCommandWithOutput(runCmd) + if err == nil { + c.Fatalf("Error running trusted run in the distant future: %s\n%s", err, out) + } + + if !strings.Contains(string(out), "could not validate the path to a trusted root") { + c.Fatalf("Missing expected output on trusted run in the distant future:\n%s", out) + } + }) + + runAtDifferentDate(elevenYearsFromNow, func() { + // Try run + runCmd := exec.Command(dockerBinary, "run", "--disable-content-trust", repoName) + s.trustedCmd(runCmd) + out, _, err := runCommandWithOutput(runCmd) + if err != nil { + c.Fatalf("Error running untrusted run in the distant future: %s\n%s", err, out) + } + + if !strings.Contains(string(out), "Status: Downloaded") { + c.Fatalf("Missing expected output on untrusted run in the distant future:\n%s", out) + } + }) +} + +func (s *DockerTrustSuite) TestTrustedRunFromBadTrustServer(c *check.C) { + // Windows does not support this functionality + testRequires(c, DaemonIsLinux) + repoName := fmt.Sprintf("%v/dockerclievilrun/trusted:latest", privateRegistryURL) + evilLocalConfigDir, err := ioutil.TempDir("", "evilrun-local-config-dir") + if err != nil { + c.Fatalf("Failed to create local temp dir") + } + + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", repoName) + + pushCmd := exec.Command(dockerBinary, "push", repoName) + s.trustedCmd(pushCmd) + out, _, err := runCommandWithOutput(pushCmd) + if err != nil { + c.Fatalf("Error running trusted push: %s\n%s", err, out) + } + if !strings.Contains(string(out), "Signing and pushing trust metadata") { + c.Fatalf("Missing expected output on trusted push:\n%s", out) + } + + dockerCmd(c, "rmi", repoName) + + // Try run + runCmd := exec.Command(dockerBinary, "run", repoName) + s.trustedCmd(runCmd) + out, _, err = runCommandWithOutput(runCmd) + if err != nil { + c.Fatalf("Error running trusted run: %s\n%s", err, out) + } + + if !strings.Contains(string(out), "Tagging") { + c.Fatalf("Missing expected output on trusted push:\n%s", out) + } + + dockerCmd(c, "rmi", repoName) + + // Kill the notary server, start a new "evil" one. + s.not.Close() + s.not, err = newTestNotary(c) + if err != nil { + c.Fatalf("Restarting notary server failed.") + } + + // In order to make an evil server, lets re-init a client (with a different trust dir) and push new data. + // tag an image and upload it to the private registry + dockerCmd(c, "--config", evilLocalConfigDir, "tag", "busybox", repoName) + + // Push up to the new server + pushCmd = exec.Command(dockerBinary, "--config", evilLocalConfigDir, "push", repoName) + s.trustedCmd(pushCmd) + out, _, err = runCommandWithOutput(pushCmd) + if err != nil { + c.Fatalf("Error running trusted push: %s\n%s", err, out) + } + if !strings.Contains(string(out), "Signing and pushing trust metadata") { + c.Fatalf("Missing expected output on trusted push:\n%s", out) + } + + // Now, try running with the original client from this new trust server. This should fallback to our cached timestamp and metadata. + runCmd = exec.Command(dockerBinary, "run", repoName) + s.trustedCmd(runCmd) + out, _, err = runCommandWithOutput(runCmd) + + if err != nil { + c.Fatalf("Error falling back to cached trust data: %s\n%s", err, out) + } + if !strings.Contains(string(out), "Error while downloading remote metadata, using cached timestamp") { + c.Fatalf("Missing expected output on trusted push:\n%s", out) + } +} + +func (s *DockerSuite) TestPtraceContainerProcsFromHost(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + testRequires(c, DaemonIsLinux, SameHostDaemon) + + out, _ := dockerCmd(c, "run", "-d", "busybox", "top") + id := strings.TrimSpace(out) + c.Assert(waitRun(id), check.IsNil) + pid1 := inspectField(c, id, "State.Pid") + + _, err := os.Readlink(fmt.Sprintf("/proc/%s/ns/net", pid1)) + if err != nil { + c.Fatal(err) + } +} + +func (s *DockerSuite) TestAppArmorDeniesPtrace(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + testRequires(c, SameHostDaemon, Apparmor, DaemonIsLinux, NotGCCGO) + + // Run through 'sh' so we are NOT pid 1. Pid 1 may be able to trace + // itself, but pid>1 should not be able to trace pid1. + _, exitCode, _ := dockerCmdWithError("run", "busybox", "sh", "-c", "sh -c readlink /proc/1/ns/net") + if exitCode == 0 { + c.Fatal("ptrace was not successfully restricted by AppArmor") + } +} + +func (s *DockerSuite) TestAppArmorTraceSelf(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + testRequires(c, DaemonIsLinux, SameHostDaemon, Apparmor) + + _, exitCode, _ := dockerCmdWithError("run", "busybox", "readlink", "/proc/1/ns/net") + if exitCode != 0 { + c.Fatal("ptrace of self failed.") + } +} + +func (s *DockerSuite) TestAppArmorDeniesChmodProc(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + testRequires(c, SameHostDaemon, Apparmor, DaemonIsLinux, NotUserNamespace) + _, exitCode, _ := dockerCmdWithError("run", "busybox", "chmod", "744", "/proc/cpuinfo") + if exitCode == 0 { + // If our test failed, attempt to repair the host system... + _, exitCode, _ := dockerCmdWithError("run", "busybox", "chmod", "444", "/proc/cpuinfo") + if exitCode == 0 { + c.Fatal("AppArmor was unsuccessful in prohibiting chmod of /proc/* files.") + } + } +} + +func (s *DockerSuite) TestRunCapAddSYSTIME(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + testRequires(c, DaemonIsLinux) + + dockerCmd(c, "run", "--cap-drop=ALL", "--cap-add=SYS_TIME", "busybox", "sh", "-c", "grep ^CapEff /proc/self/status | sed 's/^CapEff:\t//' | grep ^0000000002000000$") +} + +// run create container failed should clean up the container +func (s *DockerSuite) TestRunCreateContainerFailedCleanUp(c *check.C) { + // TODO Windows. This may be possible to enable once link is supported + testRequires(c, DaemonIsLinux) + name := "unique_name" + _, _, err := dockerCmdWithError("run", "--name", name, "--link", "nothing:nothing", "busybox") + c.Assert(err, check.NotNil, check.Commentf("Expected docker run to fail!")) + + containerID, err := inspectFieldWithError(name, "Id") + c.Assert(err, checker.NotNil, check.Commentf("Expected not to have this container: %s!", containerID)) + c.Assert(containerID, check.Equals, "", check.Commentf("Expected not to have this container: %s!", containerID)) +} + +func (s *DockerSuite) TestRunNamedVolume(c *check.C) { + prefix, _ := getPrefixAndSlashFromDaemonPlatform() + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "--name=test", "-v", "testing:"+prefix+"/foo", "busybox", "sh", "-c", "echo hello > "+prefix+"/foo/bar") + + out, _ := dockerCmd(c, "run", "--volumes-from", "test", "busybox", "sh", "-c", "cat "+prefix+"/foo/bar") + c.Assert(strings.TrimSpace(out), check.Equals, "hello") + + out, _ = dockerCmd(c, "run", "-v", "testing:"+prefix+"/foo", "busybox", "sh", "-c", "cat "+prefix+"/foo/bar") + c.Assert(strings.TrimSpace(out), check.Equals, "hello") +} + +func (s *DockerSuite) TestRunWithUlimits(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + testRequires(c, DaemonIsLinux) + + out, _ := dockerCmd(c, "run", "--name=testulimits", "--ulimit", "nofile=42", "busybox", "/bin/sh", "-c", "ulimit -n") + ul := strings.TrimSpace(out) + if ul != "42" { + c.Fatalf("expected `ulimit -n` to be 42, got %s", ul) + } +} + +func (s *DockerSuite) TestRunContainerWithCgroupParent(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + testRequires(c, DaemonIsLinux) + + cgroupParent := "test" + name := "cgroup-test" + + out, _, err := dockerCmdWithError("run", "--cgroup-parent", cgroupParent, "--name", name, "busybox", "cat", "/proc/self/cgroup") + if err != nil { + c.Fatalf("unexpected failure when running container with --cgroup-parent option - %s\n%v", string(out), err) + } + cgroupPaths := parseCgroupPaths(string(out)) + if len(cgroupPaths) == 0 { + c.Fatalf("unexpected output - %q", string(out)) + } + id, err := getIDByName(name) + c.Assert(err, check.IsNil) + expectedCgroup := path.Join(cgroupParent, id) + found := false + for _, path := range cgroupPaths { + if strings.HasSuffix(path, expectedCgroup) { + found = true + break + } + } + if !found { + c.Fatalf("unexpected cgroup paths. Expected at least one cgroup path to have suffix %q. Cgroup Paths: %v", expectedCgroup, cgroupPaths) + } +} + +func (s *DockerSuite) TestRunContainerWithCgroupParentAbsPath(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + testRequires(c, DaemonIsLinux) + + cgroupParent := "/cgroup-parent/test" + name := "cgroup-test" + out, _, err := dockerCmdWithError("run", "--cgroup-parent", cgroupParent, "--name", name, "busybox", "cat", "/proc/self/cgroup") + if err != nil { + c.Fatalf("unexpected failure when running container with --cgroup-parent option - %s\n%v", string(out), err) + } + cgroupPaths := parseCgroupPaths(string(out)) + if len(cgroupPaths) == 0 { + c.Fatalf("unexpected output - %q", string(out)) + } + id, err := getIDByName(name) + c.Assert(err, check.IsNil) + expectedCgroup := path.Join(cgroupParent, id) + found := false + for _, path := range cgroupPaths { + if strings.HasSuffix(path, expectedCgroup) { + found = true + break + } + } + if !found { + c.Fatalf("unexpected cgroup paths. Expected at least one cgroup path to have suffix %q. Cgroup Paths: %v", expectedCgroup, cgroupPaths) + } +} + +// TestRunInvalidCgroupParent checks that a specially-crafted cgroup parent doesn't cause Docker to crash or start modifying /. +func (s *DockerSuite) TestRunInvalidCgroupParent(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + testRequires(c, DaemonIsLinux) + + cgroupParent := "../../../../../../../../SHOULD_NOT_EXIST" + cleanCgroupParent := "SHOULD_NOT_EXIST" + name := "cgroup-invalid-test" + + out, _, err := dockerCmdWithError("run", "--cgroup-parent", cgroupParent, "--name", name, "busybox", "cat", "/proc/self/cgroup") + if err != nil { + // XXX: This may include a daemon crash. + c.Fatalf("unexpected failure when running container with --cgroup-parent option - %s\n%v", string(out), err) + } + + // We expect "/SHOULD_NOT_EXIST" to not exist. If not, we have a security issue. + if _, err := os.Stat("/SHOULD_NOT_EXIST"); err == nil || !os.IsNotExist(err) { + c.Fatalf("SECURITY: --cgroup-parent with ../../ relative paths cause files to be created in the host (this is bad) !!") + } + + cgroupPaths := parseCgroupPaths(string(out)) + if len(cgroupPaths) == 0 { + c.Fatalf("unexpected output - %q", string(out)) + } + id, err := getIDByName(name) + c.Assert(err, check.IsNil) + expectedCgroup := path.Join(cleanCgroupParent, id) + found := false + for _, path := range cgroupPaths { + if strings.HasSuffix(path, expectedCgroup) { + found = true + break + } + } + if !found { + c.Fatalf("unexpected cgroup paths. Expected at least one cgroup path to have suffix %q. Cgroup Paths: %v", expectedCgroup, cgroupPaths) + } +} + +// TestRunInvalidCgroupParent checks that a specially-crafted cgroup parent doesn't cause Docker to crash or start modifying /. +func (s *DockerSuite) TestRunAbsoluteInvalidCgroupParent(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + testRequires(c, DaemonIsLinux) + + cgroupParent := "/../../../../../../../../SHOULD_NOT_EXIST" + cleanCgroupParent := "/SHOULD_NOT_EXIST" + name := "cgroup-absolute-invalid-test" + + out, _, err := dockerCmdWithError("run", "--cgroup-parent", cgroupParent, "--name", name, "busybox", "cat", "/proc/self/cgroup") + if err != nil { + // XXX: This may include a daemon crash. + c.Fatalf("unexpected failure when running container with --cgroup-parent option - %s\n%v", string(out), err) + } + + // We expect "/SHOULD_NOT_EXIST" to not exist. If not, we have a security issue. + if _, err := os.Stat("/SHOULD_NOT_EXIST"); err == nil || !os.IsNotExist(err) { + c.Fatalf("SECURITY: --cgroup-parent with /../../ garbage paths cause files to be created in the host (this is bad) !!") + } + + cgroupPaths := parseCgroupPaths(string(out)) + if len(cgroupPaths) == 0 { + c.Fatalf("unexpected output - %q", string(out)) + } + id, err := getIDByName(name) + c.Assert(err, check.IsNil) + expectedCgroup := path.Join(cleanCgroupParent, id) + found := false + for _, path := range cgroupPaths { + if strings.HasSuffix(path, expectedCgroup) { + found = true + break + } + } + if !found { + c.Fatalf("unexpected cgroup paths. Expected at least one cgroup path to have suffix %q. Cgroup Paths: %v", expectedCgroup, cgroupPaths) + } +} + +func (s *DockerSuite) TestRunContainerWithCgroupMountRO(c *check.C) { + // Not applicable on Windows as uses Unix specific functionality + // --read-only + userns has remount issues + testRequires(c, DaemonIsLinux, NotUserNamespace) + + filename := "/sys/fs/cgroup/devices/test123" + out, _, err := dockerCmdWithError("run", "busybox", "touch", filename) + if err == nil { + c.Fatal("expected cgroup mount point to be read-only, touch file should fail") + } + expected := "Read-only file system" + if !strings.Contains(out, expected) { + c.Fatalf("expected output from failure to contain %s but contains %s", expected, out) + } +} + +func (s *DockerSuite) TestRunContainerNetworkModeToSelf(c *check.C) { + // Not applicable on Windows which does not support --net=container + testRequires(c, DaemonIsLinux, NotUserNamespace) + out, _, err := dockerCmdWithError("run", "--name=me", "--net=container:me", "busybox", "true") + if err == nil || !strings.Contains(out, "cannot join own network") { + c.Fatalf("using container net mode to self should result in an error\nerr: %q\nout: %s", err, out) + } +} + +func (s *DockerSuite) TestRunContainerNetModeWithDnsMacHosts(c *check.C) { + // Not applicable on Windows which does not support --net=container + testRequires(c, DaemonIsLinux, NotUserNamespace) + out, _, err := dockerCmdWithError("run", "-d", "--name", "parent", "busybox", "top") + if err != nil { + c.Fatalf("failed to run container: %v, output: %q", err, out) + } + + out, _, err = dockerCmdWithError("run", "--dns", "1.2.3.4", "--net=container:parent", "busybox") + if err == nil || !strings.Contains(out, runconfig.ErrConflictNetworkAndDNS.Error()) { + c.Fatalf("run --net=container with --dns should error out") + } + + out, _, err = dockerCmdWithError("run", "--mac-address", "92:d0:c6:0a:29:33", "--net=container:parent", "busybox") + if err == nil || !strings.Contains(out, runconfig.ErrConflictContainerNetworkAndMac.Error()) { + c.Fatalf("run --net=container with --mac-address should error out") + } + + out, _, err = dockerCmdWithError("run", "--add-host", "test:192.168.2.109", "--net=container:parent", "busybox") + if err == nil || !strings.Contains(out, runconfig.ErrConflictNetworkHosts.Error()) { + c.Fatalf("run --net=container with --add-host should error out") + } +} + +func (s *DockerSuite) TestRunContainerNetModeWithExposePort(c *check.C) { + // Not applicable on Windows which does not support --net=container + testRequires(c, DaemonIsLinux, NotUserNamespace) + dockerCmd(c, "run", "-d", "--name", "parent", "busybox", "top") + + out, _, err := dockerCmdWithError("run", "-p", "5000:5000", "--net=container:parent", "busybox") + if err == nil || !strings.Contains(out, runconfig.ErrConflictNetworkPublishPorts.Error()) { + c.Fatalf("run --net=container with -p should error out") + } + + out, _, err = dockerCmdWithError("run", "-P", "--net=container:parent", "busybox") + if err == nil || !strings.Contains(out, runconfig.ErrConflictNetworkPublishPorts.Error()) { + c.Fatalf("run --net=container with -P should error out") + } + + out, _, err = dockerCmdWithError("run", "--expose", "5000", "--net=container:parent", "busybox") + if err == nil || !strings.Contains(out, runconfig.ErrConflictNetworkExposePorts.Error()) { + c.Fatalf("run --net=container with --expose should error out") + } +} + +func (s *DockerSuite) TestRunLinkToContainerNetMode(c *check.C) { + // Not applicable on Windows which does not support --net=container or --link + testRequires(c, DaemonIsLinux, NotUserNamespace) + dockerCmd(c, "run", "--name", "test", "-d", "busybox", "top") + dockerCmd(c, "run", "--name", "parent", "-d", "--net=container:test", "busybox", "top") + dockerCmd(c, "run", "-d", "--link=parent:parent", "busybox", "top") + dockerCmd(c, "run", "--name", "child", "-d", "--net=container:parent", "busybox", "top") + dockerCmd(c, "run", "-d", "--link=child:child", "busybox", "top") +} + +func (s *DockerSuite) TestRunLoopbackOnlyExistsWhenNetworkingDisabled(c *check.C) { + // TODO Windows: This may be possible to convert. + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "--net=none", "busybox", "ip", "-o", "-4", "a", "show", "up") + + var ( + count = 0 + parts = strings.Split(out, "\n") + ) + + for _, l := range parts { + if l != "" { + count++ + } + } + + if count != 1 { + c.Fatalf("Wrong interface count in container %d", count) + } + + if !strings.HasPrefix(out, "1: lo") { + c.Fatalf("Wrong interface in test container: expected [1: lo], got %s", out) + } +} + +// Issue #4681 +func (s *DockerSuite) TestRunLoopbackWhenNetworkDisabled(c *check.C) { + if daemonPlatform == "windows" { + dockerCmd(c, "run", "--net=none", WindowsBaseImage, "ping", "-n", "1", "127.0.0.1") + } else { + dockerCmd(c, "run", "--net=none", "busybox", "ping", "-c", "1", "127.0.0.1") + } +} + +func (s *DockerSuite) TestRunModeNetContainerHostname(c *check.C) { + // Windows does not support --net=container + testRequires(c, DaemonIsLinux, ExecSupport, NotUserNamespace) + + dockerCmd(c, "run", "-i", "-d", "--name", "parent", "busybox", "top") + out, _ := dockerCmd(c, "exec", "parent", "cat", "/etc/hostname") + out1, _ := dockerCmd(c, "run", "--net=container:parent", "busybox", "cat", "/etc/hostname") + + if out1 != out { + c.Fatal("containers with shared net namespace should have same hostname") + } +} + +func (s *DockerSuite) TestRunNetworkNotInitializedNoneMode(c *check.C) { + // TODO Windows: Network settings are not currently propagated. This may + // be resolved in the future with the move to libnetwork and CNM. + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "--net=none", "busybox", "top") + id := strings.TrimSpace(out) + res := inspectField(c, id, "NetworkSettings.Networks.none.IPAddress") + if res != "" { + c.Fatalf("For 'none' mode network must not be initialized, but container got IP: %s", res) + } +} + +func (s *DockerSuite) TestTwoContainersInNetHost(c *check.C) { + // Not applicable as Windows does not support --net=host + testRequires(c, DaemonIsLinux, NotUserNamespace, NotUserNamespace) + dockerCmd(c, "run", "-d", "--net=host", "--name=first", "busybox", "top") + dockerCmd(c, "run", "-d", "--net=host", "--name=second", "busybox", "top") + dockerCmd(c, "stop", "first") + dockerCmd(c, "stop", "second") +} + +func (s *DockerSuite) TestContainersInUserDefinedNetwork(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace, NotArm) + dockerCmd(c, "network", "create", "-d", "bridge", "testnetwork") + dockerCmd(c, "run", "-d", "--net=testnetwork", "--name=first", "busybox", "top") + c.Assert(waitRun("first"), check.IsNil) + dockerCmd(c, "run", "-t", "--net=testnetwork", "--name=second", "busybox", "ping", "-c", "1", "first") +} + +func (s *DockerSuite) TestContainersInMultipleNetworks(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace, NotArm) + // Create 2 networks using bridge driver + dockerCmd(c, "network", "create", "-d", "bridge", "testnetwork1") + dockerCmd(c, "network", "create", "-d", "bridge", "testnetwork2") + // Run and connect containers to testnetwork1 + dockerCmd(c, "run", "-d", "--net=testnetwork1", "--name=first", "busybox", "top") + c.Assert(waitRun("first"), check.IsNil) + dockerCmd(c, "run", "-d", "--net=testnetwork1", "--name=second", "busybox", "top") + c.Assert(waitRun("second"), check.IsNil) + // Check connectivity between containers in testnetwork2 + dockerCmd(c, "exec", "first", "ping", "-c", "1", "second.testnetwork1") + // Connect containers to testnetwork2 + dockerCmd(c, "network", "connect", "testnetwork2", "first") + dockerCmd(c, "network", "connect", "testnetwork2", "second") + // Check connectivity between containers + dockerCmd(c, "exec", "second", "ping", "-c", "1", "first.testnetwork2") +} + +func (s *DockerSuite) TestContainersNetworkIsolation(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace, NotArm) + // Create 2 networks using bridge driver + dockerCmd(c, "network", "create", "-d", "bridge", "testnetwork1") + dockerCmd(c, "network", "create", "-d", "bridge", "testnetwork2") + // Run 1 container in testnetwork1 and another in testnetwork2 + dockerCmd(c, "run", "-d", "--net=testnetwork1", "--name=first", "busybox", "top") + c.Assert(waitRun("first"), check.IsNil) + dockerCmd(c, "run", "-d", "--net=testnetwork2", "--name=second", "busybox", "top") + c.Assert(waitRun("second"), check.IsNil) + + // Check Isolation between containers : ping must fail + _, _, err := dockerCmdWithError("exec", "first", "ping", "-c", "1", "second") + c.Assert(err, check.NotNil) + // Connect first container to testnetwork2 + dockerCmd(c, "network", "connect", "testnetwork2", "first") + // ping must succeed now + _, _, err = dockerCmdWithError("exec", "first", "ping", "-c", "1", "second") + c.Assert(err, check.IsNil) + + // Disconnect first container from testnetwork2 + dockerCmd(c, "network", "disconnect", "testnetwork2", "first") + // ping must fail again + _, _, err = dockerCmdWithError("exec", "first", "ping", "-c", "1", "second") + c.Assert(err, check.NotNil) +} + +func (s *DockerSuite) TestNetworkRmWithActiveContainers(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace) + // Create 2 networks using bridge driver + dockerCmd(c, "network", "create", "-d", "bridge", "testnetwork1") + // Run and connect containers to testnetwork1 + dockerCmd(c, "run", "-d", "--net=testnetwork1", "--name=first", "busybox", "top") + c.Assert(waitRun("first"), check.IsNil) + dockerCmd(c, "run", "-d", "--net=testnetwork1", "--name=second", "busybox", "top") + c.Assert(waitRun("second"), check.IsNil) + // Network delete with active containers must fail + _, _, err := dockerCmdWithError("network", "rm", "testnetwork1") + c.Assert(err, check.NotNil) + + dockerCmd(c, "stop", "first") + _, _, err = dockerCmdWithError("network", "rm", "testnetwork1") + c.Assert(err, check.NotNil) +} + +func (s *DockerSuite) TestContainerRestartInMultipleNetworks(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace, NotArm) + // Create 2 networks using bridge driver + dockerCmd(c, "network", "create", "-d", "bridge", "testnetwork1") + dockerCmd(c, "network", "create", "-d", "bridge", "testnetwork2") + + // Run and connect containers to testnetwork1 + dockerCmd(c, "run", "-d", "--net=testnetwork1", "--name=first", "busybox", "top") + c.Assert(waitRun("first"), check.IsNil) + dockerCmd(c, "run", "-d", "--net=testnetwork1", "--name=second", "busybox", "top") + c.Assert(waitRun("second"), check.IsNil) + // Check connectivity between containers in testnetwork2 + dockerCmd(c, "exec", "first", "ping", "-c", "1", "second.testnetwork1") + // Connect containers to testnetwork2 + dockerCmd(c, "network", "connect", "testnetwork2", "first") + dockerCmd(c, "network", "connect", "testnetwork2", "second") + // Check connectivity between containers + dockerCmd(c, "exec", "second", "ping", "-c", "1", "first.testnetwork2") + + // Stop second container and test ping failures on both networks + dockerCmd(c, "stop", "second") + _, _, err := dockerCmdWithError("exec", "first", "ping", "-c", "1", "second.testnetwork1") + c.Assert(err, check.NotNil) + _, _, err = dockerCmdWithError("exec", "first", "ping", "-c", "1", "second.testnetwork2") + c.Assert(err, check.NotNil) + + // Start second container and connectivity must be restored on both networks + dockerCmd(c, "start", "second") + dockerCmd(c, "exec", "first", "ping", "-c", "1", "second.testnetwork1") + dockerCmd(c, "exec", "second", "ping", "-c", "1", "first.testnetwork2") +} + +func (s *DockerSuite) TestContainerWithConflictingHostNetworks(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace) + // Run a container with --net=host + dockerCmd(c, "run", "-d", "--net=host", "--name=first", "busybox", "top") + c.Assert(waitRun("first"), check.IsNil) + + // Create a network using bridge driver + dockerCmd(c, "network", "create", "-d", "bridge", "testnetwork1") + + // Connecting to the user defined network must fail + _, _, err := dockerCmdWithError("network", "connect", "testnetwork1", "first") + c.Assert(err, check.NotNil) +} + +func (s *DockerSuite) TestContainerWithConflictingSharedNetwork(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace) + dockerCmd(c, "run", "-d", "--name=first", "busybox", "top") + c.Assert(waitRun("first"), check.IsNil) + // Run second container in first container's network namespace + dockerCmd(c, "run", "-d", "--net=container:first", "--name=second", "busybox", "top") + c.Assert(waitRun("second"), check.IsNil) + + // Create a network using bridge driver + dockerCmd(c, "network", "create", "-d", "bridge", "testnetwork1") + + // Connecting to the user defined network must fail + out, _, err := dockerCmdWithError("network", "connect", "testnetwork1", "second") + c.Assert(err, check.NotNil) + c.Assert(out, checker.Contains, runconfig.ErrConflictSharedNetwork.Error()) +} + +func (s *DockerSuite) TestContainerWithConflictingNoneNetwork(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace) + dockerCmd(c, "run", "-d", "--net=none", "--name=first", "busybox", "top") + c.Assert(waitRun("first"), check.IsNil) + + // Create a network using bridge driver + dockerCmd(c, "network", "create", "-d", "bridge", "testnetwork1") + + // Connecting to the user defined network must fail + out, _, err := dockerCmdWithError("network", "connect", "testnetwork1", "first") + c.Assert(err, check.NotNil) + c.Assert(out, checker.Contains, runconfig.ErrConflictNoNetwork.Error()) + + // create a container connected to testnetwork1 + dockerCmd(c, "run", "-d", "--net=testnetwork1", "--name=second", "busybox", "top") + c.Assert(waitRun("second"), check.IsNil) + + // Connect second container to none network. it must fail as well + _, _, err = dockerCmdWithError("network", "connect", "none", "second") + c.Assert(err, check.NotNil) +} + +// #11957 - stdin with no tty does not exit if stdin is not closed even though container exited +func (s *DockerSuite) TestRunStdinBlockedAfterContainerExit(c *check.C) { + cmd := exec.Command(dockerBinary, "run", "-i", "--name=test", "busybox", "true") + in, err := cmd.StdinPipe() + c.Assert(err, check.IsNil) + defer in.Close() + c.Assert(cmd.Start(), check.IsNil) + + waitChan := make(chan error) + go func() { + waitChan <- cmd.Wait() + }() + + select { + case err := <-waitChan: + c.Assert(err, check.IsNil) + case <-time.After(30 * time.Second): + c.Fatal("timeout waiting for command to exit") + } +} + +func (s *DockerSuite) TestRunWrongCpusetCpusFlagValue(c *check.C) { + // TODO Windows: This needs validation (error out) in the daemon. + testRequires(c, DaemonIsLinux) + out, exitCode, err := dockerCmdWithError("run", "--cpuset-cpus", "1-10,11--", "busybox", "true") + c.Assert(err, check.NotNil) + expected := "Error response from daemon: Invalid value 1-10,11-- for cpuset cpus.\n" + if !(strings.Contains(out, expected) || exitCode == 125) { + c.Fatalf("Expected output to contain %q with exitCode 125, got out: %q exitCode: %v", expected, out, exitCode) + } +} + +func (s *DockerSuite) TestRunWrongCpusetMemsFlagValue(c *check.C) { + // TODO Windows: This needs validation (error out) in the daemon. + testRequires(c, DaemonIsLinux) + out, exitCode, err := dockerCmdWithError("run", "--cpuset-mems", "1-42--", "busybox", "true") + c.Assert(err, check.NotNil) + expected := "Error response from daemon: Invalid value 1-42-- for cpuset mems.\n" + if !(strings.Contains(out, expected) || exitCode == 125) { + c.Fatalf("Expected output to contain %q with exitCode 125, got out: %q exitCode: %v", expected, out, exitCode) + } +} + +// TestRunNonExecutableCmd checks that 'docker run busybox foo' exits with error code 127' +func (s *DockerSuite) TestRunNonExecutableCmd(c *check.C) { + name := "testNonExecutableCmd" + runCmd := exec.Command(dockerBinary, "run", "--name", name, "busybox", "foo") + _, exit, _ := runCommandWithOutput(runCmd) + stateExitCode := findContainerExitCode(c, name) + if !(exit == 127 && strings.Contains(stateExitCode, "127")) { + c.Fatalf("Run non-executable command should have errored with exit code 127, but we got exit: %d, State.ExitCode: %s", exit, stateExitCode) + } +} + +// TestRunNonExistingCmd checks that 'docker run busybox /bin/foo' exits with code 127. +func (s *DockerSuite) TestRunNonExistingCmd(c *check.C) { + name := "testNonExistingCmd" + runCmd := exec.Command(dockerBinary, "run", "--name", name, "busybox", "/bin/foo") + _, exit, _ := runCommandWithOutput(runCmd) + stateExitCode := findContainerExitCode(c, name) + if !(exit == 127 && strings.Contains(stateExitCode, "127")) { + c.Fatalf("Run non-existing command should have errored with exit code 127, but we got exit: %d, State.ExitCode: %s", exit, stateExitCode) + } +} + +// TestCmdCannotBeInvoked checks that 'docker run busybox /etc' exits with 126, or +// 127 on Windows. The difference is that in Windows, the container must be started +// as that's when the check is made (and yes, by it's design...) +func (s *DockerSuite) TestCmdCannotBeInvoked(c *check.C) { + expected := 126 + if daemonPlatform == "windows" { + expected = 127 + } + name := "testCmdCannotBeInvoked" + runCmd := exec.Command(dockerBinary, "run", "--name", name, "busybox", "/etc") + _, exit, _ := runCommandWithOutput(runCmd) + stateExitCode := findContainerExitCode(c, name) + if !(exit == expected && strings.Contains(stateExitCode, strconv.Itoa(expected))) { + c.Fatalf("Run cmd that cannot be invoked should have errored with code %d, but we got exit: %d, State.ExitCode: %s", expected, exit, stateExitCode) + } +} + +// TestRunNonExistingImage checks that 'docker run foo' exits with error msg 125 and contains 'Unable to find image' +func (s *DockerSuite) TestRunNonExistingImage(c *check.C) { + runCmd := exec.Command(dockerBinary, "run", "foo") + out, exit, err := runCommandWithOutput(runCmd) + if !(err != nil && exit == 125 && strings.Contains(out, "Unable to find image")) { + c.Fatalf("Run non-existing image should have errored with 'Unable to find image' code 125, but we got out: %s, exit: %d, err: %s", out, exit, err) + } +} + +// TestDockerFails checks that 'docker run -foo busybox' exits with 125 to signal docker run failed +func (s *DockerSuite) TestDockerFails(c *check.C) { + runCmd := exec.Command(dockerBinary, "run", "-foo", "busybox") + out, exit, err := runCommandWithOutput(runCmd) + if !(err != nil && exit == 125) { + c.Fatalf("Docker run with flag not defined should exit with 125, but we got out: %s, exit: %d, err: %s", out, exit, err) + } +} + +// TestRunInvalidReference invokes docker run with a bad reference. +func (s *DockerSuite) TestRunInvalidReference(c *check.C) { + out, exit, _ := dockerCmdWithError("run", "busybox@foo") + if exit == 0 { + c.Fatalf("expected non-zero exist code; received %d", exit) + } + + if !strings.Contains(out, "Error parsing reference") { + c.Fatalf(`Expected "Error parsing reference" in output; got: %s`, out) + } +} + +// Test fix for issue #17854 +func (s *DockerSuite) TestRunInitLayerPathOwnership(c *check.C) { + // Not applicable on Windows as it does not support Linux uid/gid ownership + testRequires(c, DaemonIsLinux) + name := "testetcfileownership" + _, err := buildImage(name, + `FROM busybox + RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd + RUN echo 'dockerio:x:1001:' >> /etc/group + RUN chown dockerio:dockerio /etc`, + true) + if err != nil { + c.Fatal(err) + } + + // Test that dockerio ownership of /etc is retained at runtime + out, _ := dockerCmd(c, "run", "--rm", name, "stat", "-c", "%U:%G", "/etc") + out = strings.TrimSpace(out) + if out != "dockerio:dockerio" { + c.Fatalf("Wrong /etc ownership: expected dockerio:dockerio, got %q", out) + } +} + +func (s *DockerSuite) TestRunWithOomScoreAdj(c *check.C) { + testRequires(c, DaemonIsLinux) + + expected := "642" + out, _ := dockerCmd(c, "run", "--oom-score-adj", expected, "busybox", "cat", "/proc/self/oom_score_adj") + oomScoreAdj := strings.TrimSpace(out) + if oomScoreAdj != "642" { + c.Fatalf("Expected oom_score_adj set to %q, got %q instead", expected, oomScoreAdj) + } +} + +func (s *DockerSuite) TestRunWithOomScoreAdjInvalidRange(c *check.C) { + testRequires(c, DaemonIsLinux) + + out, _, err := dockerCmdWithError("run", "--oom-score-adj", "1001", "busybox", "true") + c.Assert(err, check.NotNil) + expected := "Invalid value 1001, range for oom score adj is [-1000, 1000]." + if !strings.Contains(out, expected) { + c.Fatalf("Expected output to contain %q, got %q instead", expected, out) + } + out, _, err = dockerCmdWithError("run", "--oom-score-adj", "-1001", "busybox", "true") + c.Assert(err, check.NotNil) + expected = "Invalid value -1001, range for oom score adj is [-1000, 1000]." + if !strings.Contains(out, expected) { + c.Fatalf("Expected output to contain %q, got %q instead", expected, out) + } +} + +func (s *DockerSuite) TestRunVolumesMountedAsShared(c *check.C) { + // Volume propagation is linux only. Also it creates directories for + // bind mounting, so needs to be same host. + testRequires(c, DaemonIsLinux, SameHostDaemon, NotUserNamespace) + + // Prepare a source directory to bind mount + tmpDir, err := ioutil.TempDir("", "volume-source") + if err != nil { + c.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + if err := os.Mkdir(path.Join(tmpDir, "mnt1"), 0755); err != nil { + c.Fatal(err) + } + + // Convert this directory into a shared mount point so that we do + // not rely on propagation properties of parent mount. + cmd := exec.Command("mount", "--bind", tmpDir, tmpDir) + if _, err = runCommand(cmd); err != nil { + c.Fatal(err) + } + + cmd = exec.Command("mount", "--make-private", "--make-shared", tmpDir) + if _, err = runCommand(cmd); err != nil { + c.Fatal(err) + } + + dockerCmd(c, "run", "--privileged", "-v", fmt.Sprintf("%s:/volume-dest:shared", tmpDir), "busybox", "mount", "--bind", "/volume-dest/mnt1", "/volume-dest/mnt1") + + // Make sure a bind mount under a shared volume propagated to host. + if mounted, _ := mount.Mounted(path.Join(tmpDir, "mnt1")); !mounted { + c.Fatalf("Bind mount under shared volume did not propagate to host") + } + + mount.Unmount(path.Join(tmpDir, "mnt1")) +} + +func (s *DockerSuite) TestRunVolumesMountedAsSlave(c *check.C) { + // Volume propagation is linux only. Also it creates directories for + // bind mounting, so needs to be same host. + testRequires(c, DaemonIsLinux, SameHostDaemon, NotUserNamespace) + + // Prepare a source directory to bind mount + tmpDir, err := ioutil.TempDir("", "volume-source") + if err != nil { + c.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + if err := os.Mkdir(path.Join(tmpDir, "mnt1"), 0755); err != nil { + c.Fatal(err) + } + + // Prepare a source directory with file in it. We will bind mount this + // direcotry and see if file shows up. + tmpDir2, err := ioutil.TempDir("", "volume-source2") + if err != nil { + c.Fatal(err) + } + defer os.RemoveAll(tmpDir2) + + if err := ioutil.WriteFile(path.Join(tmpDir2, "slave-testfile"), []byte("Test"), 0644); err != nil { + c.Fatal(err) + } + + // Convert this directory into a shared mount point so that we do + // not rely on propagation properties of parent mount. + cmd := exec.Command("mount", "--bind", tmpDir, tmpDir) + if _, err = runCommand(cmd); err != nil { + c.Fatal(err) + } + + cmd = exec.Command("mount", "--make-private", "--make-shared", tmpDir) + if _, err = runCommand(cmd); err != nil { + c.Fatal(err) + } + + dockerCmd(c, "run", "-i", "-d", "--name", "parent", "-v", fmt.Sprintf("%s:/volume-dest:slave", tmpDir), "busybox", "top") + + // Bind mount tmpDir2/ onto tmpDir/mnt1. If mount propagates inside + // container then contents of tmpDir2/slave-testfile should become + // visible at "/volume-dest/mnt1/slave-testfile" + cmd = exec.Command("mount", "--bind", tmpDir2, path.Join(tmpDir, "mnt1")) + if _, err = runCommand(cmd); err != nil { + c.Fatal(err) + } + + out, _ := dockerCmd(c, "exec", "parent", "cat", "/volume-dest/mnt1/slave-testfile") + + mount.Unmount(path.Join(tmpDir, "mnt1")) + + if out != "Test" { + c.Fatalf("Bind mount under slave volume did not propagate to container") + } +} + +func (s *DockerSuite) TestRunNamedVolumesMountedAsShared(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace) + out, exitcode, _ := dockerCmdWithError("run", "-v", "foo:/test:shared", "busybox", "touch", "/test/somefile") + + if exitcode == 0 { + c.Fatalf("expected non-zero exit code; received %d", exitcode) + } + + if expected := "Invalid volume specification"; !strings.Contains(out, expected) { + c.Fatalf(`Expected %q in output; got: %s`, expected, out) + } +} + +func (s *DockerSuite) TestRunNamedVolumeCopyImageData(c *check.C) { + testRequires(c, DaemonIsLinux) + + testImg := "testvolumecopy" + _, err := buildImage(testImg, ` + FROM busybox + RUN mkdir -p /foo && echo hello > /foo/hello + `, true) + c.Assert(err, check.IsNil) + + dockerCmd(c, "run", "-v", "foo:/foo", testImg) + out, _ := dockerCmd(c, "run", "-v", "foo:/foo", "busybox", "cat", "/foo/hello") + c.Assert(strings.TrimSpace(out), check.Equals, "hello") +} + +func (s *DockerSuite) TestRunNamedVolumeNotRemoved(c *check.C) { + prefix, _ := getPrefixAndSlashFromDaemonPlatform() + + dockerCmd(c, "volume", "create", "--name", "test") + + dockerCmd(c, "run", "--rm", "-v", "test:"+prefix+"/foo", "-v", prefix+"/bar", "busybox", "true") + dockerCmd(c, "volume", "inspect", "test") + out, _ := dockerCmd(c, "volume", "ls", "-q") + c.Assert(strings.TrimSpace(out), checker.Equals, "test") + + dockerCmd(c, "run", "--name=test", "-v", "test:"+prefix+"/foo", "-v", prefix+"/bar", "busybox", "true") + dockerCmd(c, "rm", "-fv", "test") + dockerCmd(c, "volume", "inspect", "test") + out, _ = dockerCmd(c, "volume", "ls", "-q") + c.Assert(strings.TrimSpace(out), checker.Equals, "test") +} + +func (s *DockerSuite) TestRunNamedVolumesFromNotRemoved(c *check.C) { + prefix, _ := getPrefixAndSlashFromDaemonPlatform() + + dockerCmd(c, "volume", "create", "--name", "test") + dockerCmd(c, "run", "--name=parent", "-v", "test:"+prefix+"/foo", "-v", prefix+"/bar", "busybox", "true") + dockerCmd(c, "run", "--name=child", "--volumes-from=parent", "busybox", "true") + + // Remove the parent so there are not other references to the volumes + dockerCmd(c, "rm", "-f", "parent") + // now remove the child and ensure the named volume (and only the named volume) still exists + dockerCmd(c, "rm", "-fv", "child") + dockerCmd(c, "volume", "inspect", "test") + out, _ := dockerCmd(c, "volume", "ls", "-q") + c.Assert(strings.TrimSpace(out), checker.Equals, "test") +} + +func (s *DockerSuite) TestRunAttachFailedNoLeak(c *check.C) { + nroutines, err := getGoroutineNumber() + c.Assert(err, checker.IsNil) + + runSleepingContainer(c, "--name=test", "-p", "8000:8000") + + // Wait until container is fully up and running + c.Assert(waitRun("test"), check.IsNil) + + out, _, err := dockerCmdWithError("run", "-p", "8000:8000", "busybox", "true") + c.Assert(err, checker.NotNil) + // check for windows error as well + // TODO Windows Post TP5. Fix the error message string + c.Assert(strings.Contains(string(out), "port is already allocated") || + strings.Contains(string(out), "were not connected because a duplicate name exists") || + strings.Contains(string(out), "HNS failed with error : Failed to create endpoint"), checker.Equals, true, check.Commentf("Output: %s", out)) + dockerCmd(c, "rm", "-f", "test") + + // NGoroutines is not updated right away, so we need to wait before failing + c.Assert(waitForGoroutines(nroutines), checker.IsNil) +} + +// Test for one character directory name case (#20122) +func (s *DockerSuite) TestRunVolumeWithOneCharacter(c *check.C) { + testRequires(c, DaemonIsLinux) + + out, _ := dockerCmd(c, "run", "-v", "/tmp/q:/foo", "busybox", "sh", "-c", "find /foo") + c.Assert(strings.TrimSpace(out), checker.Equals, "/foo") +} + +func (s *DockerSuite) TestRunVolumeCopyFlag(c *check.C) { + testRequires(c, DaemonIsLinux) // Windows does not support copying data from image to the volume + _, err := buildImage("volumecopy", + `FROM busybox + RUN mkdir /foo && echo hello > /foo/bar + CMD cat /foo/bar`, + true, + ) + c.Assert(err, checker.IsNil) + + dockerCmd(c, "volume", "create", "--name=test") + + // test with the nocopy flag + out, _, err := dockerCmdWithError("run", "-v", "test:/foo:nocopy", "volumecopy") + c.Assert(err, checker.NotNil, check.Commentf(out)) + // test default behavior which is to copy for non-binds + out, _ = dockerCmd(c, "run", "-v", "test:/foo", "volumecopy") + c.Assert(strings.TrimSpace(out), checker.Equals, "hello") + // error out when the volume is already populated + out, _, err = dockerCmdWithError("run", "-v", "test:/foo:copy", "volumecopy") + c.Assert(err, checker.NotNil, check.Commentf(out)) + // do not error out when copy isn't explicitly set even though it's already populated + out, _ = dockerCmd(c, "run", "-v", "test:/foo", "volumecopy") + c.Assert(strings.TrimSpace(out), checker.Equals, "hello") + + // do not allow copy modes on volumes-from + dockerCmd(c, "run", "--name=test", "-v", "/foo", "busybox", "true") + out, _, err = dockerCmdWithError("run", "--volumes-from=test:copy", "busybox", "true") + c.Assert(err, checker.NotNil, check.Commentf(out)) + out, _, err = dockerCmdWithError("run", "--volumes-from=test:nocopy", "busybox", "true") + c.Assert(err, checker.NotNil, check.Commentf(out)) + + // do not allow copy modes on binds + out, _, err = dockerCmdWithError("run", "-v", "/foo:/bar:copy", "busybox", "true") + c.Assert(err, checker.NotNil, check.Commentf(out)) + out, _, err = dockerCmdWithError("run", "-v", "/foo:/bar:nocopy", "busybox", "true") + c.Assert(err, checker.NotNil, check.Commentf(out)) +} + +func (s *DockerSuite) TestRunTooLongHostname(c *check.C) { + // Test case in #21445 + hostname1 := "this-is-a-way-too-long-hostname-but-it-should-give-a-nice-error.local" + out, _, err := dockerCmdWithError("run", "--hostname", hostname1, "busybox", "echo", "test") + c.Assert(err, checker.NotNil, check.Commentf("Expected docker run to fail!")) + c.Assert(out, checker.Contains, "invalid hostname format for --hostname:", check.Commentf("Expected to have 'invalid hostname format for --hostname:' in the output, get: %s!", out)) + + // HOST_NAME_MAX=64 so 65 bytes will fail + hostname2 := "this-is-a-hostname-with-65-bytes-so-it-should-give-an-error.local" + out, _, err = dockerCmdWithError("run", "--hostname", hostname2, "busybox", "echo", "test") + c.Assert(err, checker.NotNil, check.Commentf("Expected docker run to fail!")) + c.Assert(out, checker.Contains, "invalid hostname format for --hostname:", check.Commentf("Expected to have 'invalid hostname format for --hostname:' in the output, get: %s!", out)) + + // 64 bytes will be OK + hostname3 := "this-is-a-hostname-with-64-bytes-so-will-not-give-an-error.local" + dockerCmd(c, "run", "--hostname", hostname3, "busybox", "echo", "test") +} diff --git a/integration-cli/docker_cli_run_unix_test.go b/integration-cli/docker_cli_run_unix_test.go new file mode 100644 index 00000000..fe7cc697 --- /dev/null +++ b/integration-cli/docker_cli_run_unix_test.go @@ -0,0 +1,1009 @@ +// +build !windows + +package main + +import ( + "bufio" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "regexp" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/docker/docker/pkg/homedir" + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/mount" + "github.com/docker/docker/pkg/parsers" + "github.com/docker/docker/pkg/sysinfo" + "github.com/go-check/check" + "github.com/kr/pty" +) + +// #6509 +func (s *DockerSuite) TestRunRedirectStdout(c *check.C) { + checkRedirect := func(command string) { + _, tty, err := pty.Open() + c.Assert(err, checker.IsNil, check.Commentf("Could not open pty")) + cmd := exec.Command("sh", "-c", command) + cmd.Stdin = tty + cmd.Stdout = tty + cmd.Stderr = tty + c.Assert(cmd.Start(), checker.IsNil) + ch := make(chan error) + go func() { + ch <- cmd.Wait() + close(ch) + }() + + select { + case <-time.After(10 * time.Second): + c.Fatal("command timeout") + case err := <-ch: + c.Assert(err, checker.IsNil, check.Commentf("wait err")) + } + } + + checkRedirect(dockerBinary + " run -i busybox cat /etc/passwd | grep -q root") + checkRedirect(dockerBinary + " run busybox cat /etc/passwd | grep -q root") +} + +// Test recursive bind mount works by default +func (s *DockerSuite) TestRunWithVolumesIsRecursive(c *check.C) { + // /tmp gets permission denied + testRequires(c, NotUserNamespace, SameHostDaemon) + tmpDir, err := ioutil.TempDir("", "docker_recursive_mount_test") + c.Assert(err, checker.IsNil) + + defer os.RemoveAll(tmpDir) + + // Create a temporary tmpfs mount. + tmpfsDir := filepath.Join(tmpDir, "tmpfs") + c.Assert(os.MkdirAll(tmpfsDir, 0777), checker.IsNil, check.Commentf("failed to mkdir at %s", tmpfsDir)) + c.Assert(mount.Mount("tmpfs", tmpfsDir, "tmpfs", ""), checker.IsNil, check.Commentf("failed to create a tmpfs mount at %s", tmpfsDir)) + + f, err := ioutil.TempFile(tmpfsDir, "touch-me") + c.Assert(err, checker.IsNil) + defer f.Close() + + runCmd := exec.Command(dockerBinary, "run", "--name", "test-data", "--volume", fmt.Sprintf("%s:/tmp:ro", tmpDir), "busybox:latest", "ls", "/tmp/tmpfs") + out, _, _, err := runCommandWithStdoutStderr(runCmd) + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Contains, filepath.Base(f.Name()), check.Commentf("Recursive bind mount test failed. Expected file not found")) +} + +func (s *DockerSuite) TestRunDeviceDirectory(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace, NotArm) + if _, err := os.Stat("/dev/snd"); err != nil { + c.Skip("Host does not have /dev/snd") + } + + out, _ := dockerCmd(c, "run", "--device", "/dev/snd:/dev/snd", "busybox", "sh", "-c", "ls /dev/snd/") + c.Assert(strings.Trim(out, "\r\n"), checker.Contains, "timer", check.Commentf("expected output /dev/snd/timer")) + + out, _ = dockerCmd(c, "run", "--device", "/dev/snd:/dev/othersnd", "busybox", "sh", "-c", "ls /dev/othersnd/") + c.Assert(strings.Trim(out, "\r\n"), checker.Contains, "seq", check.Commentf("expected output /dev/othersnd/seq")) +} + +// TestRunDetach checks attaching and detaching with the default escape sequence. +func (s *DockerSuite) TestRunAttachDetach(c *check.C) { + name := "attach-detach" + + dockerCmd(c, "run", "--name", name, "-itd", "busybox", "cat") + + cmd := exec.Command(dockerBinary, "attach", name) + stdout, err := cmd.StdoutPipe() + c.Assert(err, checker.IsNil) + cpty, tty, err := pty.Open() + c.Assert(err, checker.IsNil) + defer cpty.Close() + cmd.Stdin = tty + c.Assert(cmd.Start(), checker.IsNil) + c.Assert(waitRun(name), check.IsNil) + + _, err = cpty.Write([]byte("hello\n")) + c.Assert(err, checker.IsNil) + + out, err := bufio.NewReader(stdout).ReadString('\n') + c.Assert(err, checker.IsNil) + c.Assert(strings.TrimSpace(out), checker.Equals, "hello") + + // escape sequence + _, err = cpty.Write([]byte{16}) + c.Assert(err, checker.IsNil) + time.Sleep(100 * time.Millisecond) + _, err = cpty.Write([]byte{17}) + c.Assert(err, checker.IsNil) + + ch := make(chan struct{}) + go func() { + cmd.Wait() + ch <- struct{}{} + }() + + select { + case <-ch: + case <-time.After(10 * time.Second): + c.Fatal("timed out waiting for container to exit") + } + + running := inspectField(c, name, "State.Running") + c.Assert(running, checker.Equals, "true", check.Commentf("expected container to still be running")) +} + +// TestRunDetach checks attaching and detaching with the escape sequence specified via flags. +func (s *DockerSuite) TestRunAttachDetachFromFlag(c *check.C) { + name := "attach-detach" + keyCtrlA := []byte{1} + keyA := []byte{97} + + dockerCmd(c, "run", "--name", name, "-itd", "busybox", "cat") + + cmd := exec.Command(dockerBinary, "attach", "--detach-keys='ctrl-a,a'", name) + stdout, err := cmd.StdoutPipe() + if err != nil { + c.Fatal(err) + } + cpty, tty, err := pty.Open() + if err != nil { + c.Fatal(err) + } + defer cpty.Close() + cmd.Stdin = tty + if err := cmd.Start(); err != nil { + c.Fatal(err) + } + c.Assert(waitRun(name), check.IsNil) + + if _, err := cpty.Write([]byte("hello\n")); err != nil { + c.Fatal(err) + } + + out, err := bufio.NewReader(stdout).ReadString('\n') + if err != nil { + c.Fatal(err) + } + if strings.TrimSpace(out) != "hello" { + c.Fatalf("expected 'hello', got %q", out) + } + + // escape sequence + if _, err := cpty.Write(keyCtrlA); err != nil { + c.Fatal(err) + } + time.Sleep(100 * time.Millisecond) + if _, err := cpty.Write(keyA); err != nil { + c.Fatal(err) + } + + ch := make(chan struct{}) + go func() { + cmd.Wait() + ch <- struct{}{} + }() + + select { + case <-ch: + case <-time.After(10 * time.Second): + c.Fatal("timed out waiting for container to exit") + } + + running := inspectField(c, name, "State.Running") + c.Assert(running, checker.Equals, "true", check.Commentf("expected container to still be running")) +} + +// TestRunDetach checks attaching and detaching with the escape sequence specified via config file. +func (s *DockerSuite) TestRunAttachDetachFromConfig(c *check.C) { + keyCtrlA := []byte{1} + keyA := []byte{97} + + // Setup config + homeKey := homedir.Key() + homeVal := homedir.Get() + tmpDir, err := ioutil.TempDir("", "fake-home") + c.Assert(err, checker.IsNil) + defer os.RemoveAll(tmpDir) + + dotDocker := filepath.Join(tmpDir, ".docker") + os.Mkdir(dotDocker, 0600) + tmpCfg := filepath.Join(dotDocker, "config.json") + + defer func() { os.Setenv(homeKey, homeVal) }() + os.Setenv(homeKey, tmpDir) + + data := `{ + "detachKeys": "ctrl-a,a" + }` + + err = ioutil.WriteFile(tmpCfg, []byte(data), 0600) + c.Assert(err, checker.IsNil) + + // Then do the work + name := "attach-detach" + dockerCmd(c, "run", "--name", name, "-itd", "busybox", "cat") + + cmd := exec.Command(dockerBinary, "attach", name) + stdout, err := cmd.StdoutPipe() + if err != nil { + c.Fatal(err) + } + cpty, tty, err := pty.Open() + if err != nil { + c.Fatal(err) + } + defer cpty.Close() + cmd.Stdin = tty + if err := cmd.Start(); err != nil { + c.Fatal(err) + } + c.Assert(waitRun(name), check.IsNil) + + if _, err := cpty.Write([]byte("hello\n")); err != nil { + c.Fatal(err) + } + + out, err := bufio.NewReader(stdout).ReadString('\n') + if err != nil { + c.Fatal(err) + } + if strings.TrimSpace(out) != "hello" { + c.Fatalf("expected 'hello', got %q", out) + } + + // escape sequence + if _, err := cpty.Write(keyCtrlA); err != nil { + c.Fatal(err) + } + time.Sleep(100 * time.Millisecond) + if _, err := cpty.Write(keyA); err != nil { + c.Fatal(err) + } + + ch := make(chan struct{}) + go func() { + cmd.Wait() + ch <- struct{}{} + }() + + select { + case <-ch: + case <-time.After(10 * time.Second): + c.Fatal("timed out waiting for container to exit") + } + + running := inspectField(c, name, "State.Running") + c.Assert(running, checker.Equals, "true", check.Commentf("expected container to still be running")) +} + +// TestRunDetach checks attaching and detaching with the detach flags, making sure it overrides config file +func (s *DockerSuite) TestRunAttachDetachKeysOverrideConfig(c *check.C) { + keyCtrlA := []byte{1} + keyA := []byte{97} + + // Setup config + homeKey := homedir.Key() + homeVal := homedir.Get() + tmpDir, err := ioutil.TempDir("", "fake-home") + c.Assert(err, checker.IsNil) + defer os.RemoveAll(tmpDir) + + dotDocker := filepath.Join(tmpDir, ".docker") + os.Mkdir(dotDocker, 0600) + tmpCfg := filepath.Join(dotDocker, "config.json") + + defer func() { os.Setenv(homeKey, homeVal) }() + os.Setenv(homeKey, tmpDir) + + data := `{ + "detachKeys": "ctrl-e,e" + }` + + err = ioutil.WriteFile(tmpCfg, []byte(data), 0600) + c.Assert(err, checker.IsNil) + + // Then do the work + name := "attach-detach" + dockerCmd(c, "run", "--name", name, "-itd", "busybox", "cat") + + cmd := exec.Command(dockerBinary, "attach", "--detach-keys='ctrl-a,a'", name) + stdout, err := cmd.StdoutPipe() + if err != nil { + c.Fatal(err) + } + cpty, tty, err := pty.Open() + if err != nil { + c.Fatal(err) + } + defer cpty.Close() + cmd.Stdin = tty + if err := cmd.Start(); err != nil { + c.Fatal(err) + } + c.Assert(waitRun(name), check.IsNil) + + if _, err := cpty.Write([]byte("hello\n")); err != nil { + c.Fatal(err) + } + + out, err := bufio.NewReader(stdout).ReadString('\n') + if err != nil { + c.Fatal(err) + } + if strings.TrimSpace(out) != "hello" { + c.Fatalf("expected 'hello', got %q", out) + } + + // escape sequence + if _, err := cpty.Write(keyCtrlA); err != nil { + c.Fatal(err) + } + time.Sleep(100 * time.Millisecond) + if _, err := cpty.Write(keyA); err != nil { + c.Fatal(err) + } + + ch := make(chan struct{}) + go func() { + cmd.Wait() + ch <- struct{}{} + }() + + select { + case <-ch: + case <-time.After(10 * time.Second): + c.Fatal("timed out waiting for container to exit") + } + + running := inspectField(c, name, "State.Running") + c.Assert(running, checker.Equals, "true", check.Commentf("expected container to still be running")) +} + +// "test" should be printed +func (s *DockerSuite) TestRunWithCPUQuota(c *check.C) { + testRequires(c, cpuCfsQuota) + + file := "/sys/fs/cgroup/cpu/cpu.cfs_quota_us" + out, _ := dockerCmd(c, "run", "--cpu-quota", "8000", "--name", "test", "busybox", "cat", file) + c.Assert(strings.TrimSpace(out), checker.Equals, "8000") + + out = inspectField(c, "test", "HostConfig.CpuQuota") + c.Assert(out, checker.Equals, "8000", check.Commentf("setting the CPU CFS quota failed")) +} + +func (s *DockerSuite) TestRunWithCpuPeriod(c *check.C) { + testRequires(c, cpuCfsPeriod) + + file := "/sys/fs/cgroup/cpu/cpu.cfs_period_us" + out, _ := dockerCmd(c, "run", "--cpu-period", "50000", "--name", "test", "busybox", "cat", file) + c.Assert(strings.TrimSpace(out), checker.Equals, "50000") + + out = inspectField(c, "test", "HostConfig.CpuPeriod") + c.Assert(out, checker.Equals, "50000", check.Commentf("setting the CPU CFS period failed")) +} + +func (s *DockerSuite) TestRunWithKernelMemory(c *check.C) { + testRequires(c, kernelMemorySupport) + + file := "/sys/fs/cgroup/memory/memory.kmem.limit_in_bytes" + stdout, _, _ := dockerCmdWithStdoutStderr(c, "run", "--kernel-memory", "50M", "--name", "test1", "busybox", "cat", file) + c.Assert(strings.TrimSpace(stdout), checker.Equals, "52428800") + + out := inspectField(c, "test1", "HostConfig.KernelMemory") + c.Assert(out, check.Equals, "52428800") +} + +func (s *DockerSuite) TestRunWithInvalidKernelMemory(c *check.C) { + testRequires(c, kernelMemorySupport) + + out, _, err := dockerCmdWithError("run", "--kernel-memory", "2M", "busybox", "true") + c.Assert(err, check.NotNil) + expected := "Minimum kernel memory limit allowed is 4MB" + c.Assert(out, checker.Contains, expected) + + out, _, err = dockerCmdWithError("run", "--kernel-memory", "-16m", "--name", "test2", "busybox", "echo", "test") + c.Assert(err, check.NotNil) + expected = "invalid size" + c.Assert(out, checker.Contains, expected) +} + +func (s *DockerSuite) TestRunWithCPUShares(c *check.C) { + testRequires(c, cpuShare) + + file := "/sys/fs/cgroup/cpu/cpu.shares" + out, _ := dockerCmd(c, "run", "--cpu-shares", "1000", "--name", "test", "busybox", "cat", file) + c.Assert(strings.TrimSpace(out), checker.Equals, "1000") + + out = inspectField(c, "test", "HostConfig.CPUShares") + c.Assert(out, check.Equals, "1000") +} + +// "test" should be printed +func (s *DockerSuite) TestRunEchoStdoutWithCPUSharesAndMemoryLimit(c *check.C) { + testRequires(c, cpuShare) + testRequires(c, memoryLimitSupport) + out, _, _ := dockerCmdWithStdoutStderr(c, "run", "--cpu-shares", "1000", "-m", "32m", "busybox", "echo", "test") + c.Assert(out, checker.Equals, "test\n", check.Commentf("container should've printed 'test'")) +} + +func (s *DockerSuite) TestRunWithCpusetCpus(c *check.C) { + testRequires(c, cgroupCpuset) + + file := "/sys/fs/cgroup/cpuset/cpuset.cpus" + out, _ := dockerCmd(c, "run", "--cpuset-cpus", "0", "--name", "test", "busybox", "cat", file) + c.Assert(strings.TrimSpace(out), checker.Equals, "0") + + out = inspectField(c, "test", "HostConfig.CpusetCpus") + c.Assert(out, check.Equals, "0") +} + +func (s *DockerSuite) TestRunWithCpusetMems(c *check.C) { + testRequires(c, cgroupCpuset) + + file := "/sys/fs/cgroup/cpuset/cpuset.mems" + out, _ := dockerCmd(c, "run", "--cpuset-mems", "0", "--name", "test", "busybox", "cat", file) + c.Assert(strings.TrimSpace(out), checker.Equals, "0") + + out = inspectField(c, "test", "HostConfig.CpusetMems") + c.Assert(out, check.Equals, "0") +} + +func (s *DockerSuite) TestRunWithBlkioWeight(c *check.C) { + testRequires(c, blkioWeight) + + file := "/sys/fs/cgroup/blkio/blkio.weight" + out, _ := dockerCmd(c, "run", "--blkio-weight", "300", "--name", "test", "busybox", "cat", file) + c.Assert(strings.TrimSpace(out), checker.Equals, "300") + + out = inspectField(c, "test", "HostConfig.BlkioWeight") + c.Assert(out, check.Equals, "300") +} + +func (s *DockerSuite) TestRunWithInvalidBlkioWeight(c *check.C) { + testRequires(c, blkioWeight) + out, _, err := dockerCmdWithError("run", "--blkio-weight", "5", "busybox", "true") + c.Assert(err, check.NotNil, check.Commentf(out)) + expected := "Range of blkio weight is from 10 to 1000" + c.Assert(out, checker.Contains, expected) +} + +func (s *DockerSuite) TestRunWithInvalidPathforBlkioWeightDevice(c *check.C) { + testRequires(c, blkioWeight) + out, _, err := dockerCmdWithError("run", "--blkio-weight-device", "/dev/sdX:100", "busybox", "true") + c.Assert(err, check.NotNil, check.Commentf(out)) +} + +func (s *DockerSuite) TestRunWithInvalidPathforBlkioDeviceReadBps(c *check.C) { + testRequires(c, blkioWeight) + out, _, err := dockerCmdWithError("run", "--device-read-bps", "/dev/sdX:500", "busybox", "true") + c.Assert(err, check.NotNil, check.Commentf(out)) +} + +func (s *DockerSuite) TestRunWithInvalidPathforBlkioDeviceWriteBps(c *check.C) { + testRequires(c, blkioWeight) + out, _, err := dockerCmdWithError("run", "--device-write-bps", "/dev/sdX:500", "busybox", "true") + c.Assert(err, check.NotNil, check.Commentf(out)) +} + +func (s *DockerSuite) TestRunWithInvalidPathforBlkioDeviceReadIOps(c *check.C) { + testRequires(c, blkioWeight) + out, _, err := dockerCmdWithError("run", "--device-read-iops", "/dev/sdX:500", "busybox", "true") + c.Assert(err, check.NotNil, check.Commentf(out)) +} + +func (s *DockerSuite) TestRunWithInvalidPathforBlkioDeviceWriteIOps(c *check.C) { + testRequires(c, blkioWeight) + out, _, err := dockerCmdWithError("run", "--device-write-iops", "/dev/sdX:500", "busybox", "true") + c.Assert(err, check.NotNil, check.Commentf(out)) +} + +func (s *DockerSuite) TestRunOOMExitCode(c *check.C) { + testRequires(c, oomControl) + errChan := make(chan error) + go func() { + defer close(errChan) + //changing memory to 40MB from 4MB due to an issue with GCCGO that test fails to start the container. + out, exitCode, _ := dockerCmdWithError("run", "-m", "40MB", "busybox", "sh", "-c", "x=a; while true; do x=$x$x$x$x; done") + if expected := 137; exitCode != expected { + errChan <- fmt.Errorf("wrong exit code for OOM container: expected %d, got %d (output: %q)", expected, exitCode, out) + } + }() + + select { + case err := <-errChan: + c.Assert(err, check.IsNil) + case <-time.After(600 * time.Second): + c.Fatal("Timeout waiting for container to die on OOM") + } +} + +func (s *DockerSuite) TestRunWithMemoryLimit(c *check.C) { + testRequires(c, memoryLimitSupport) + + file := "/sys/fs/cgroup/memory/memory.limit_in_bytes" + stdout, _, _ := dockerCmdWithStdoutStderr(c, "run", "-m", "32M", "--name", "test", "busybox", "cat", file) + c.Assert(strings.TrimSpace(stdout), checker.Equals, "33554432") + + out := inspectField(c, "test", "HostConfig.Memory") + c.Assert(out, check.Equals, "33554432") +} + +// TestRunWithoutMemoryswapLimit sets memory limit and disables swap +// memory limit, this means the processes in the container can use +// 16M memory and as much swap memory as they need (if the host +// supports swap memory). +func (s *DockerSuite) TestRunWithoutMemoryswapLimit(c *check.C) { + testRequires(c, DaemonIsLinux) + testRequires(c, memoryLimitSupport) + testRequires(c, swapMemorySupport) + dockerCmd(c, "run", "-m", "32m", "--memory-swap", "-1", "busybox", "true") +} + +func (s *DockerSuite) TestRunWithSwappiness(c *check.C) { + testRequires(c, memorySwappinessSupport) + file := "/sys/fs/cgroup/memory/memory.swappiness" + out, _ := dockerCmd(c, "run", "--memory-swappiness", "0", "--name", "test", "busybox", "cat", file) + c.Assert(strings.TrimSpace(out), checker.Equals, "0") + + out = inspectField(c, "test", "HostConfig.MemorySwappiness") + c.Assert(out, check.Equals, "0") +} + +func (s *DockerSuite) TestRunWithSwappinessInvalid(c *check.C) { + testRequires(c, memorySwappinessSupport) + out, _, err := dockerCmdWithError("run", "--memory-swappiness", "101", "busybox", "true") + c.Assert(err, check.NotNil) + expected := "Valid memory swappiness range is 0-100" + c.Assert(out, checker.Contains, expected, check.Commentf("Expected output to contain %q, not %q", out, expected)) + + out, _, err = dockerCmdWithError("run", "--memory-swappiness", "-10", "busybox", "true") + c.Assert(err, check.NotNil) + c.Assert(out, checker.Contains, expected, check.Commentf("Expected output to contain %q, not %q", out, expected)) +} + +func (s *DockerSuite) TestRunWithMemoryReservation(c *check.C) { + testRequires(c, memoryReservationSupport) + + file := "/sys/fs/cgroup/memory/memory.soft_limit_in_bytes" + out, _ := dockerCmd(c, "run", "--memory-reservation", "200M", "--name", "test", "busybox", "cat", file) + c.Assert(strings.TrimSpace(out), checker.Equals, "209715200") + + out = inspectField(c, "test", "HostConfig.MemoryReservation") + c.Assert(out, check.Equals, "209715200") +} + +func (s *DockerSuite) TestRunWithMemoryReservationInvalid(c *check.C) { + testRequires(c, memoryLimitSupport) + testRequires(c, memoryReservationSupport) + out, _, err := dockerCmdWithError("run", "-m", "500M", "--memory-reservation", "800M", "busybox", "true") + c.Assert(err, check.NotNil) + expected := "Minimum memory limit should be larger than memory reservation limit" + c.Assert(strings.TrimSpace(out), checker.Contains, expected, check.Commentf("run container should fail with invalid memory reservation")) +} + +func (s *DockerSuite) TestStopContainerSignal(c *check.C) { + out, _ := dockerCmd(c, "run", "--stop-signal", "SIGUSR1", "-d", "busybox", "/bin/sh", "-c", `trap 'echo "exit trapped"; exit 0' USR1; while true; do sleep 1; done`) + containerID := strings.TrimSpace(out) + + c.Assert(waitRun(containerID), checker.IsNil) + + dockerCmd(c, "stop", containerID) + out, _ = dockerCmd(c, "logs", containerID) + + c.Assert(out, checker.Contains, "exit trapped", check.Commentf("Expected `exit trapped` in the log")) +} + +func (s *DockerSuite) TestRunSwapLessThanMemoryLimit(c *check.C) { + testRequires(c, memoryLimitSupport) + testRequires(c, swapMemorySupport) + out, _, err := dockerCmdWithError("run", "-m", "16m", "--memory-swap", "15m", "busybox", "echo", "test") + expected := "Minimum memoryswap limit should be larger than memory limit" + c.Assert(err, check.NotNil) + + c.Assert(out, checker.Contains, expected) +} + +func (s *DockerSuite) TestRunInvalidCpusetCpusFlagValue(c *check.C) { + testRequires(c, cgroupCpuset, SameHostDaemon) + + sysInfo := sysinfo.New(true) + cpus, err := parsers.ParseUintList(sysInfo.Cpus) + c.Assert(err, check.IsNil) + var invalid int + for i := 0; i <= len(cpus)+1; i++ { + if !cpus[i] { + invalid = i + break + } + } + out, _, err := dockerCmdWithError("run", "--cpuset-cpus", strconv.Itoa(invalid), "busybox", "true") + c.Assert(err, check.NotNil) + expected := fmt.Sprintf("Error response from daemon: Requested CPUs are not available - requested %s, available: %s", strconv.Itoa(invalid), sysInfo.Cpus) + c.Assert(out, checker.Contains, expected) +} + +func (s *DockerSuite) TestRunInvalidCpusetMemsFlagValue(c *check.C) { + testRequires(c, cgroupCpuset) + + sysInfo := sysinfo.New(true) + mems, err := parsers.ParseUintList(sysInfo.Mems) + c.Assert(err, check.IsNil) + var invalid int + for i := 0; i <= len(mems)+1; i++ { + if !mems[i] { + invalid = i + break + } + } + out, _, err := dockerCmdWithError("run", "--cpuset-mems", strconv.Itoa(invalid), "busybox", "true") + c.Assert(err, check.NotNil) + expected := fmt.Sprintf("Error response from daemon: Requested memory nodes are not available - requested %s, available: %s", strconv.Itoa(invalid), sysInfo.Mems) + c.Assert(out, checker.Contains, expected) +} + +func (s *DockerSuite) TestRunInvalidCPUShares(c *check.C) { + testRequires(c, cpuShare, DaemonIsLinux) + out, _, err := dockerCmdWithError("run", "--cpu-shares", "1", "busybox", "echo", "test") + c.Assert(err, check.NotNil, check.Commentf(out)) + expected := "The minimum allowed cpu-shares is 2" + c.Assert(out, checker.Contains, expected) + + out, _, err = dockerCmdWithError("run", "--cpu-shares", "-1", "busybox", "echo", "test") + c.Assert(err, check.NotNil, check.Commentf(out)) + expected = "shares: invalid argument" + c.Assert(out, checker.Contains, expected) + + out, _, err = dockerCmdWithError("run", "--cpu-shares", "99999999", "busybox", "echo", "test") + c.Assert(err, check.NotNil, check.Commentf(out)) + expected = "The maximum allowed cpu-shares is" + c.Assert(out, checker.Contains, expected) +} + +func (s *DockerSuite) TestRunWithDefaultShmSize(c *check.C) { + testRequires(c, DaemonIsLinux) + + name := "shm-default" + out, _ := dockerCmd(c, "run", "--name", name, "busybox", "mount") + shmRegex := regexp.MustCompile(`shm on /dev/shm type tmpfs(.*)size=65536k`) + if !shmRegex.MatchString(out) { + c.Fatalf("Expected shm of 64MB in mount command, got %v", out) + } + shmSize := inspectField(c, name, "HostConfig.ShmSize") + c.Assert(shmSize, check.Equals, "67108864") +} + +func (s *DockerSuite) TestRunWithShmSize(c *check.C) { + testRequires(c, DaemonIsLinux) + + name := "shm" + out, _ := dockerCmd(c, "run", "--name", name, "--shm-size=1G", "busybox", "mount") + shmRegex := regexp.MustCompile(`shm on /dev/shm type tmpfs(.*)size=1048576k`) + if !shmRegex.MatchString(out) { + c.Fatalf("Expected shm of 1GB in mount command, got %v", out) + } + shmSize := inspectField(c, name, "HostConfig.ShmSize") + c.Assert(shmSize, check.Equals, "1073741824") +} + +func (s *DockerSuite) TestRunTmpfsMounts(c *check.C) { + // TODO Windows (Post TP4): This test cannot run on a Windows daemon as + // Windows does not support tmpfs mounts. + testRequires(c, DaemonIsLinux) + if out, _, err := dockerCmdWithError("run", "--tmpfs", "/run", "busybox", "touch", "/run/somefile"); err != nil { + c.Fatalf("/run directory not mounted on tmpfs %q %s", err, out) + } + if out, _, err := dockerCmdWithError("run", "--tmpfs", "/run:noexec", "busybox", "touch", "/run/somefile"); err != nil { + c.Fatalf("/run directory not mounted on tmpfs %q %s", err, out) + } + if out, _, err := dockerCmdWithError("run", "--tmpfs", "/run:noexec,nosuid,rw,size=5k,mode=700", "busybox", "touch", "/run/somefile"); err != nil { + c.Fatalf("/run failed to mount on tmpfs with valid options %q %s", err, out) + } + if _, _, err := dockerCmdWithError("run", "--tmpfs", "/run:foobar", "busybox", "touch", "/run/somefile"); err == nil { + c.Fatalf("/run mounted on tmpfs when it should have vailed within invalid mount option") + } + if _, _, err := dockerCmdWithError("run", "--tmpfs", "/run", "-v", "/run:/run", "busybox", "touch", "/run/somefile"); err == nil { + c.Fatalf("Should have generated an error saying Duplicate mount points") + } +} + +// TestRunSeccompProfileDenyUnshare checks that 'docker run --security-opt seccomp=/tmp/profile.json debian:jessie unshare' exits with operation not permitted. +func (s *DockerSuite) TestRunSeccompProfileDenyUnshare(c *check.C) { + testRequires(c, SameHostDaemon, seccompEnabled, NotArm, Apparmor) + jsonData := `{ + "defaultAction": "SCMP_ACT_ALLOW", + "syscalls": [ + { + "name": "unshare", + "action": "SCMP_ACT_ERRNO" + } + ] +}` + tmpFile, err := ioutil.TempFile("", "profile.json") + defer tmpFile.Close() + if err != nil { + c.Fatal(err) + } + + if _, err := tmpFile.Write([]byte(jsonData)); err != nil { + c.Fatal(err) + } + runCmd := exec.Command(dockerBinary, "run", "--security-opt", "apparmor=unconfined", "--security-opt", "seccomp="+tmpFile.Name(), "debian:jessie", "unshare", "-p", "-m", "-f", "-r", "mount", "-t", "proc", "none", "/proc") + out, _, _ := runCommandWithOutput(runCmd) + if !strings.Contains(out, "Operation not permitted") { + c.Fatalf("expected unshare with seccomp profile denied to fail, got %s", out) + } +} + +// TestRunSeccompProfileDenyChmod checks that 'docker run --security-opt seccomp=/tmp/profile.json busybox chmod 400 /etc/hostname' exits with operation not permitted. +func (s *DockerSuite) TestRunSeccompProfileDenyChmod(c *check.C) { + testRequires(c, SameHostDaemon, seccompEnabled) + jsonData := `{ + "defaultAction": "SCMP_ACT_ALLOW", + "syscalls": [ + { + "name": "chmod", + "action": "SCMP_ACT_ERRNO" + } + ] +}` + tmpFile, err := ioutil.TempFile("", "profile.json") + defer tmpFile.Close() + if err != nil { + c.Fatal(err) + } + + if _, err := tmpFile.Write([]byte(jsonData)); err != nil { + c.Fatal(err) + } + runCmd := exec.Command(dockerBinary, "run", "--security-opt", "seccomp="+tmpFile.Name(), "busybox", "chmod", "400", "/etc/hostname") + out, _, _ := runCommandWithOutput(runCmd) + if !strings.Contains(out, "Operation not permitted") { + c.Fatalf("expected chmod with seccomp profile denied to fail, got %s", out) + } +} + +// TestRunSeccompProfileDenyUnshareUserns checks that 'docker run debian:jessie unshare --map-root-user --user sh -c whoami' with a specific profile to +// deny unhare of a userns exits with operation not permitted. +func (s *DockerSuite) TestRunSeccompProfileDenyUnshareUserns(c *check.C) { + testRequires(c, SameHostDaemon, seccompEnabled, NotArm, Apparmor) + // from sched.h + jsonData := fmt.Sprintf(`{ + "defaultAction": "SCMP_ACT_ALLOW", + "syscalls": [ + { + "name": "unshare", + "action": "SCMP_ACT_ERRNO", + "args": [ + { + "index": 0, + "value": %d, + "op": "SCMP_CMP_EQ" + } + ] + } + ] +}`, uint64(0x10000000)) + tmpFile, err := ioutil.TempFile("", "profile.json") + defer tmpFile.Close() + if err != nil { + c.Fatal(err) + } + + if _, err := tmpFile.Write([]byte(jsonData)); err != nil { + c.Fatal(err) + } + runCmd := exec.Command(dockerBinary, "run", "--security-opt", "apparmor=unconfined", "--security-opt", "seccomp="+tmpFile.Name(), "debian:jessie", "unshare", "--map-root-user", "--user", "sh", "-c", "whoami") + out, _, _ := runCommandWithOutput(runCmd) + if !strings.Contains(out, "Operation not permitted") { + c.Fatalf("expected unshare userns with seccomp profile denied to fail, got %s", out) + } +} + +// TestRunSeccompProfileDenyCloneUserns checks that 'docker run syscall-test' +// with a the default seccomp profile exits with operation not permitted. +func (s *DockerSuite) TestRunSeccompProfileDenyCloneUserns(c *check.C) { + testRequires(c, SameHostDaemon, seccompEnabled) + + runCmd := exec.Command(dockerBinary, "run", "syscall-test", "userns-test", "id") + out, _, err := runCommandWithOutput(runCmd) + if err == nil || !strings.Contains(out, "clone failed: Operation not permitted") { + c.Fatalf("expected clone userns with default seccomp profile denied to fail, got %s: %v", out, err) + } +} + +// TestRunSeccompUnconfinedCloneUserns checks that +// 'docker run --security-opt seccomp=unconfined syscall-test' allows creating a userns. +func (s *DockerSuite) TestRunSeccompUnconfinedCloneUserns(c *check.C) { + testRequires(c, SameHostDaemon, seccompEnabled, UserNamespaceInKernel, NotUserNamespace) + + // make sure running w privileged is ok + runCmd := exec.Command(dockerBinary, "run", "--security-opt", "seccomp=unconfined", "syscall-test", "userns-test", "id") + if out, _, err := runCommandWithOutput(runCmd); err != nil || !strings.Contains(out, "nobody") { + c.Fatalf("expected clone userns with --security-opt seccomp=unconfined to succeed, got %s: %v", out, err) + } +} + +// TestRunSeccompAllowPrivCloneUserns checks that 'docker run --privileged syscall-test' +// allows creating a userns. +func (s *DockerSuite) TestRunSeccompAllowPrivCloneUserns(c *check.C) { + testRequires(c, SameHostDaemon, seccompEnabled, UserNamespaceInKernel, NotUserNamespace) + + // make sure running w privileged is ok + runCmd := exec.Command(dockerBinary, "run", "--privileged", "syscall-test", "userns-test", "id") + if out, _, err := runCommandWithOutput(runCmd); err != nil || !strings.Contains(out, "nobody") { + c.Fatalf("expected clone userns with --privileged to succeed, got %s: %v", out, err) + } +} + +// TestRunSeccompAllowSetrlimit checks that 'docker run debian:jessie ulimit -v 1048510' succeeds. +func (s *DockerSuite) TestRunSeccompAllowSetrlimit(c *check.C) { + testRequires(c, SameHostDaemon, seccompEnabled) + + // ulimit uses setrlimit, so we want to make sure we don't break it + runCmd := exec.Command(dockerBinary, "run", "debian:jessie", "bash", "-c", "ulimit -v 1048510") + if out, _, err := runCommandWithOutput(runCmd); err != nil { + c.Fatalf("expected ulimit with seccomp to succeed, got %s: %v", out, err) + } +} + +func (s *DockerSuite) TestRunSeccompDefaultProfile(c *check.C) { + testRequires(c, SameHostDaemon, seccompEnabled, NotUserNamespace) + + var group sync.WaitGroup + group.Add(4) + errChan := make(chan error, 4) + go func() { + out, _, err := dockerCmdWithError("run", "--cap-add", "ALL", "syscall-test", "acct-test") + if err == nil || !strings.Contains(out, "Operation not permitted") { + errChan <- fmt.Errorf("expected Operation not permitted, got: %s", out) + } + group.Done() + }() + + go func() { + out, _, err := dockerCmdWithError("run", "--cap-add", "ALL", "syscall-test", "ns-test", "echo", "hello") + if err == nil || !strings.Contains(out, "Operation not permitted") { + errChan <- fmt.Errorf("expected Operation not permitted, got: %s", out) + } + group.Done() + }() + + go func() { + out, _, err := dockerCmdWithError("run", "--cap-add", "ALL", "--security-opt", "seccomp=unconfined", "syscall-test", "acct-test") + if err == nil || !strings.Contains(out, "No such file or directory") { + errChan <- fmt.Errorf("expected No such file or directory, got: %s", out) + } + group.Done() + }() + + go func() { + out, _, err := dockerCmdWithError("run", "--cap-add", "ALL", "--security-opt", "seccomp=unconfined", "syscall-test", "ns-test", "echo", "hello") + if err != nil || !strings.Contains(out, "hello") { + errChan <- fmt.Errorf("expected hello, got: %s, %v", out, err) + } + group.Done() + }() + + group.Wait() + close(errChan) + + for err := range errChan { + c.Assert(err, checker.IsNil) + } +} + +// TestRunNoNewPrivSetuid checks that --security-opt=no-new-privileges prevents +// effective uid transtions on executing setuid binaries. +func (s *DockerSuite) TestRunNoNewPrivSetuid(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace, SameHostDaemon) + + // test that running a setuid binary results in no effective uid transition + runCmd := exec.Command(dockerBinary, "run", "--security-opt", "no-new-privileges", "--user", "1000", "nnp-test", "/usr/bin/nnp-test") + if out, _, err := runCommandWithOutput(runCmd); err != nil || !strings.Contains(out, "EUID=1000") { + c.Fatalf("expected output to contain EUID=1000, got %s: %v", out, err) + } +} + +func (s *DockerSuite) TestRunApparmorProcDirectory(c *check.C) { + testRequires(c, SameHostDaemon, Apparmor) + + // running w seccomp unconfined tests the apparmor profile + runCmd := exec.Command(dockerBinary, "run", "--security-opt", "seccomp=unconfined", "busybox", "chmod", "777", "/proc/1/cgroup") + if out, _, err := runCommandWithOutput(runCmd); err == nil || !(strings.Contains(out, "Permission denied") || strings.Contains(out, "Operation not permitted")) { + c.Fatalf("expected chmod 777 /proc/1/cgroup to fail, got %s: %v", out, err) + } + + runCmd = exec.Command(dockerBinary, "run", "--security-opt", "seccomp=unconfined", "busybox", "chmod", "777", "/proc/1/attr/current") + if out, _, err := runCommandWithOutput(runCmd); err == nil || !(strings.Contains(out, "Permission denied") || strings.Contains(out, "Operation not permitted")) { + c.Fatalf("expected chmod 777 /proc/1/attr/current to fail, got %s: %v", out, err) + } +} + +// make sure the default profile can be successfully parsed (using unshare as it is +// something which we know is blocked in the default profile) +func (s *DockerSuite) TestRunSeccompWithDefaultProfile(c *check.C) { + testRequires(c, SameHostDaemon, seccompEnabled) + + out, _, err := dockerCmdWithError("run", "--security-opt", "seccomp=../profiles/seccomp/default.json", "debian:jessie", "unshare", "--map-root-user", "--user", "sh", "-c", "whoami") + c.Assert(err, checker.NotNil, check.Commentf(out)) + c.Assert(strings.TrimSpace(out), checker.Equals, "unshare: unshare failed: Operation not permitted") +} + +// TestRunDeviceSymlink checks run with device that follows symlink (#13840) +func (s *DockerSuite) TestRunDeviceSymlink(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace, NotArm, SameHostDaemon) + if _, err := os.Stat("/dev/zero"); err != nil { + c.Skip("Host does not have /dev/zero") + } + + // Create a temporary directory to create symlink + tmpDir, err := ioutil.TempDir("", "docker_device_follow_symlink_tests") + c.Assert(err, checker.IsNil) + + defer os.RemoveAll(tmpDir) + + // Create a symbolic link to /dev/zero + symZero := filepath.Join(tmpDir, "zero") + err = os.Symlink("/dev/zero", symZero) + c.Assert(err, checker.IsNil) + + // Create a temporary file "temp" inside tmpDir, write some data to "tmpDir/temp", + // then create a symlink "tmpDir/file" to the temporary file "tmpDir/temp". + tmpFile := filepath.Join(tmpDir, "temp") + err = ioutil.WriteFile(tmpFile, []byte("temp"), 0666) + c.Assert(err, checker.IsNil) + symFile := filepath.Join(tmpDir, "file") + err = os.Symlink(tmpFile, symFile) + c.Assert(err, checker.IsNil) + + // md5sum of 'dd if=/dev/zero bs=4K count=8' is bb7df04e1b0a2570657527a7e108ae23 + out, _ := dockerCmd(c, "run", "--device", symZero+":/dev/symzero", "busybox", "sh", "-c", "dd if=/dev/symzero bs=4K count=8 | md5sum") + c.Assert(strings.Trim(out, "\r\n"), checker.Contains, "bb7df04e1b0a2570657527a7e108ae23", check.Commentf("expected output bb7df04e1b0a2570657527a7e108ae23")) + + // symlink "tmpDir/file" to a file "tmpDir/temp" will result in an error as it is not a device. + out, _, err = dockerCmdWithError("run", "--device", symFile+":/dev/symzero", "busybox", "sh", "-c", "dd if=/dev/symzero bs=4K count=8 | md5sum") + c.Assert(err, check.NotNil) + c.Assert(strings.Trim(out, "\r\n"), checker.Contains, "not a device node", check.Commentf("expected output 'not a device node'")) +} + +// TestRunPidsLimit makes sure the pids cgroup is set with --pids-limit +func (s *DockerSuite) TestRunPidsLimit(c *check.C) { + testRequires(c, pidsLimit) + + file := "/sys/fs/cgroup/pids/pids.max" + out, _ := dockerCmd(c, "run", "--name", "skittles", "--pids-limit", "2", "busybox", "cat", file) + c.Assert(strings.TrimSpace(out), checker.Equals, "2") + + out = inspectField(c, "skittles", "HostConfig.PidsLimit") + c.Assert(out, checker.Equals, "2", check.Commentf("setting the pids limit failed")) +} + +func (s *DockerSuite) TestRunPrivilegedAllowedDevices(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace) + + file := "/sys/fs/cgroup/devices/devices.list" + out, _ := dockerCmd(c, "run", "--privileged", "busybox", "cat", file) + c.Logf("out: %q", out) + c.Assert(strings.TrimSpace(out), checker.Equals, "a *:* rwm") +} + +func (s *DockerSuite) TestRunUserDeviceAllowed(c *check.C) { + testRequires(c, DaemonIsLinux) + + fi, err := os.Stat("/dev/snd/timer") + if err != nil { + c.Skip("Host does not have /dev/snd/timer") + } + stat, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + c.Skip("Could not stat /dev/snd/timer") + } + + file := "/sys/fs/cgroup/devices/devices.list" + out, _ := dockerCmd(c, "run", "--device", "/dev/snd/timer:w", "busybox", "cat", file) + c.Assert(out, checker.Contains, fmt.Sprintf("c %d:%d w", stat.Rdev/256, stat.Rdev%256)) +} diff --git a/integration-cli/docker_cli_save_load_test.go b/integration-cli/docker_cli_save_load_test.go new file mode 100644 index 00000000..b21c987b --- /dev/null +++ b/integration-cli/docker_cli_save_load_test.go @@ -0,0 +1,352 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "reflect" + "regexp" + "sort" + "strings" + "time" + + "github.com/docker/distribution/digest" + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +// save a repo using gz compression and try to load it using stdout +func (s *DockerSuite) TestSaveXzAndLoadRepoStdout(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "test-save-xz-and-load-repo-stdout" + dockerCmd(c, "run", "--name", name, "busybox", "true") + + repoName := "foobar-save-load-test-xz-gz" + out, _ := dockerCmd(c, "commit", name, repoName) + + dockerCmd(c, "inspect", repoName) + + repoTarball, _, err := runCommandPipelineWithOutput( + exec.Command(dockerBinary, "save", repoName), + exec.Command("xz", "-c"), + exec.Command("gzip", "-c")) + c.Assert(err, checker.IsNil, check.Commentf("failed to save repo: %v %v", out, err)) + deleteImages(repoName) + + loadCmd := exec.Command(dockerBinary, "load") + loadCmd.Stdin = strings.NewReader(repoTarball) + out, _, err = runCommandWithOutput(loadCmd) + c.Assert(err, checker.NotNil, check.Commentf("expected error, but succeeded with no error and output: %v", out)) + + after, _, err := dockerCmdWithError("inspect", repoName) + c.Assert(err, checker.NotNil, check.Commentf("the repo should not exist: %v", after)) +} + +// save a repo using xz+gz compression and try to load it using stdout +func (s *DockerSuite) TestSaveXzGzAndLoadRepoStdout(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "test-save-xz-gz-and-load-repo-stdout" + dockerCmd(c, "run", "--name", name, "busybox", "true") + + repoName := "foobar-save-load-test-xz-gz" + dockerCmd(c, "commit", name, repoName) + + dockerCmd(c, "inspect", repoName) + + out, _, err := runCommandPipelineWithOutput( + exec.Command(dockerBinary, "save", repoName), + exec.Command("xz", "-c"), + exec.Command("gzip", "-c")) + c.Assert(err, checker.IsNil, check.Commentf("failed to save repo: %v %v", out, err)) + + deleteImages(repoName) + + loadCmd := exec.Command(dockerBinary, "load") + loadCmd.Stdin = strings.NewReader(out) + out, _, err = runCommandWithOutput(loadCmd) + c.Assert(err, checker.NotNil, check.Commentf("expected error, but succeeded with no error and output: %v", out)) + + after, _, err := dockerCmdWithError("inspect", repoName) + c.Assert(err, checker.NotNil, check.Commentf("the repo should not exist: %v", after)) +} + +func (s *DockerSuite) TestSaveSingleTag(c *check.C) { + testRequires(c, DaemonIsLinux) + repoName := "foobar-save-single-tag-test" + dockerCmd(c, "tag", "busybox:latest", fmt.Sprintf("%v:latest", repoName)) + + out, _ := dockerCmd(c, "images", "-q", "--no-trunc", repoName) + cleanedImageID := strings.TrimSpace(out) + + out, _, err := runCommandPipelineWithOutput( + exec.Command(dockerBinary, "save", fmt.Sprintf("%v:latest", repoName)), + exec.Command("tar", "t"), + exec.Command("grep", "-E", fmt.Sprintf("(^repositories$|%v)", cleanedImageID))) + c.Assert(err, checker.IsNil, check.Commentf("failed to save repo with image ID and 'repositories' file: %s, %v", out, err)) +} + +func (s *DockerSuite) TestSaveCheckTimes(c *check.C) { + testRequires(c, DaemonIsLinux) + repoName := "busybox:latest" + out, _ := dockerCmd(c, "inspect", repoName) + data := []struct { + ID string + Created time.Time + }{} + err := json.Unmarshal([]byte(out), &data) + c.Assert(err, checker.IsNil, check.Commentf("failed to marshal from %q: err %v", repoName, err)) + c.Assert(len(data), checker.Not(checker.Equals), 0, check.Commentf("failed to marshal the data from %q", repoName)) + tarTvTimeFormat := "2006-01-02 15:04" + out, _, err = runCommandPipelineWithOutput( + exec.Command(dockerBinary, "save", repoName), + exec.Command("tar", "tv"), + exec.Command("grep", "-E", fmt.Sprintf("%s %s", data[0].Created.Format(tarTvTimeFormat), digest.Digest(data[0].ID).Hex()))) + c.Assert(err, checker.IsNil, check.Commentf("failed to save repo with image ID and 'repositories' file: %s, %v", out, err)) +} + +func (s *DockerSuite) TestSaveImageId(c *check.C) { + testRequires(c, DaemonIsLinux) + repoName := "foobar-save-image-id-test" + dockerCmd(c, "tag", "emptyfs:latest", fmt.Sprintf("%v:latest", repoName)) + + out, _ := dockerCmd(c, "images", "-q", "--no-trunc", repoName) + cleanedLongImageID := strings.TrimPrefix(strings.TrimSpace(out), "sha256:") + + out, _ = dockerCmd(c, "images", "-q", repoName) + cleanedShortImageID := strings.TrimSpace(out) + + // Make sure IDs are not empty + c.Assert(cleanedLongImageID, checker.Not(check.Equals), "", check.Commentf("Id should not be empty.")) + c.Assert(cleanedShortImageID, checker.Not(check.Equals), "", check.Commentf("Id should not be empty.")) + + saveCmd := exec.Command(dockerBinary, "save", cleanedShortImageID) + tarCmd := exec.Command("tar", "t") + + var err error + tarCmd.Stdin, err = saveCmd.StdoutPipe() + c.Assert(err, checker.IsNil, check.Commentf("cannot set stdout pipe for tar: %v", err)) + grepCmd := exec.Command("grep", cleanedLongImageID) + grepCmd.Stdin, err = tarCmd.StdoutPipe() + c.Assert(err, checker.IsNil, check.Commentf("cannot set stdout pipe for grep: %v", err)) + + c.Assert(tarCmd.Start(), checker.IsNil, check.Commentf("tar failed with error: %v", err)) + c.Assert(saveCmd.Start(), checker.IsNil, check.Commentf("docker save failed with error: %v", err)) + defer func() { + saveCmd.Wait() + tarCmd.Wait() + dockerCmd(c, "rmi", repoName) + }() + + out, _, err = runCommandWithOutput(grepCmd) + + c.Assert(err, checker.IsNil, check.Commentf("failed to save repo with image ID: %s, %v", out, err)) +} + +// save a repo and try to load it using flags +func (s *DockerSuite) TestSaveAndLoadRepoFlags(c *check.C) { + testRequires(c, DaemonIsLinux) + name := "test-save-and-load-repo-flags" + dockerCmd(c, "run", "--name", name, "busybox", "true") + + repoName := "foobar-save-load-test" + + deleteImages(repoName) + dockerCmd(c, "commit", name, repoName) + + before, _ := dockerCmd(c, "inspect", repoName) + + out, _, err := runCommandPipelineWithOutput( + exec.Command(dockerBinary, "save", repoName), + exec.Command(dockerBinary, "load")) + c.Assert(err, checker.IsNil, check.Commentf("failed to save and load repo: %s, %v", out, err)) + + after, _ := dockerCmd(c, "inspect", repoName) + c.Assert(before, checker.Equals, after, check.Commentf("inspect is not the same after a save / load")) +} + +func (s *DockerSuite) TestSaveWithNoExistImage(c *check.C) { + testRequires(c, DaemonIsLinux) + + imgName := "foobar-non-existing-image" + + out, _, err := dockerCmdWithError("save", "-o", "test-img.tar", imgName) + c.Assert(err, checker.NotNil, check.Commentf("save image should fail for non-existing image")) + c.Assert(out, checker.Contains, fmt.Sprintf("No such image: %s", imgName)) +} + +func (s *DockerSuite) TestSaveMultipleNames(c *check.C) { + testRequires(c, DaemonIsLinux) + repoName := "foobar-save-multi-name-test" + + // Make one image + dockerCmd(c, "tag", "emptyfs:latest", fmt.Sprintf("%v-one:latest", repoName)) + + // Make two images + dockerCmd(c, "tag", "emptyfs:latest", fmt.Sprintf("%v-two:latest", repoName)) + + out, _, err := runCommandPipelineWithOutput( + exec.Command(dockerBinary, "save", fmt.Sprintf("%v-one", repoName), fmt.Sprintf("%v-two:latest", repoName)), + exec.Command("tar", "xO", "repositories"), + exec.Command("grep", "-q", "-E", "(-one|-two)"), + ) + c.Assert(err, checker.IsNil, check.Commentf("failed to save multiple repos: %s, %v", out, err)) +} + +func (s *DockerSuite) TestSaveRepoWithMultipleImages(c *check.C) { + testRequires(c, DaemonIsLinux) + makeImage := func(from string, tag string) string { + var ( + out string + ) + out, _ = dockerCmd(c, "run", "-d", from, "true") + cleanedContainerID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "commit", cleanedContainerID, tag) + imageID := strings.TrimSpace(out) + return imageID + } + + repoName := "foobar-save-multi-images-test" + tagFoo := repoName + ":foo" + tagBar := repoName + ":bar" + + idFoo := makeImage("busybox:latest", tagFoo) + idBar := makeImage("busybox:latest", tagBar) + + deleteImages(repoName) + + // create the archive + out, _, err := runCommandPipelineWithOutput( + exec.Command(dockerBinary, "save", repoName, "busybox:latest"), + exec.Command("tar", "t")) + c.Assert(err, checker.IsNil, check.Commentf("failed to save multiple images: %s, %v", out, err)) + + lines := strings.Split(strings.TrimSpace(out), "\n") + var actual []string + for _, l := range lines { + if regexp.MustCompile("^[a-f0-9]{64}\\.json$").Match([]byte(l)) { + actual = append(actual, strings.TrimSuffix(l, ".json")) + } + } + + // make the list of expected layers + out = inspectField(c, "busybox:latest", "Id") + expected := []string{strings.TrimSpace(out), idFoo, idBar} + + // prefixes are not in tar + for i := range expected { + expected[i] = digest.Digest(expected[i]).Hex() + } + + sort.Strings(actual) + sort.Strings(expected) + c.Assert(actual, checker.DeepEquals, expected, check.Commentf("archive does not contains the right layers: got %v, expected %v, output: %q", actual, expected, out)) +} + +// Issue #6722 #5892 ensure directories are included in changes +func (s *DockerSuite) TestSaveDirectoryPermissions(c *check.C) { + testRequires(c, DaemonIsLinux) + layerEntries := []string{"opt/", "opt/a/", "opt/a/b/", "opt/a/b/c"} + layerEntriesAUFS := []string{"./", ".wh..wh.aufs", ".wh..wh.orph/", ".wh..wh.plnk/", "opt/", "opt/a/", "opt/a/b/", "opt/a/b/c"} + + name := "save-directory-permissions" + tmpDir, err := ioutil.TempDir("", "save-layers-with-directories") + c.Assert(err, checker.IsNil, check.Commentf("failed to create temporary directory: %s", err)) + extractionDirectory := filepath.Join(tmpDir, "image-extraction-dir") + os.Mkdir(extractionDirectory, 0777) + + defer os.RemoveAll(tmpDir) + _, err = buildImage(name, + `FROM busybox + RUN adduser -D user && mkdir -p /opt/a/b && chown -R user:user /opt/a + RUN touch /opt/a/b/c && chown user:user /opt/a/b/c`, + true) + c.Assert(err, checker.IsNil, check.Commentf("%v", err)) + + out, _, err := runCommandPipelineWithOutput( + exec.Command(dockerBinary, "save", name), + exec.Command("tar", "-xf", "-", "-C", extractionDirectory), + ) + c.Assert(err, checker.IsNil, check.Commentf("failed to save and extract image: %s", out)) + + dirs, err := ioutil.ReadDir(extractionDirectory) + c.Assert(err, checker.IsNil, check.Commentf("failed to get a listing of the layer directories: %s", err)) + + found := false + for _, entry := range dirs { + var entriesSansDev []string + if entry.IsDir() { + layerPath := filepath.Join(extractionDirectory, entry.Name(), "layer.tar") + + f, err := os.Open(layerPath) + c.Assert(err, checker.IsNil, check.Commentf("failed to open %s: %s", layerPath, err)) + + entries, err := listTar(f) + for _, e := range entries { + if !strings.Contains(e, "dev/") { + entriesSansDev = append(entriesSansDev, e) + } + } + c.Assert(err, checker.IsNil, check.Commentf("encountered error while listing tar entries: %s", err)) + + if reflect.DeepEqual(entriesSansDev, layerEntries) || reflect.DeepEqual(entriesSansDev, layerEntriesAUFS) { + found = true + break + } + } + } + + c.Assert(found, checker.Equals, true, check.Commentf("failed to find the layer with the right content listing")) + +} + +// Test loading a weird image where one of the layers is of zero size. +// The layer.tar file is actually zero bytes, no padding or anything else. +// See issue: 18170 +func (s *DockerSuite) TestLoadZeroSizeLayer(c *check.C) { + testRequires(c, DaemonIsLinux) + + dockerCmd(c, "load", "-i", "fixtures/load/emptyLayer.tar") +} + +func (s *DockerSuite) TestSaveLoadParents(c *check.C) { + testRequires(c, DaemonIsLinux) + + makeImage := func(from string, addfile string) string { + var ( + out string + ) + out, _ = dockerCmd(c, "run", "-d", from, "touch", addfile) + cleanedContainerID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "commit", cleanedContainerID) + imageID := strings.TrimSpace(out) + + dockerCmd(c, "rm", cleanedContainerID) + return imageID + } + + idFoo := makeImage("busybox", "foo") + idBar := makeImage(idFoo, "bar") + + tmpDir, err := ioutil.TempDir("", "save-load-parents") + c.Assert(err, checker.IsNil) + defer os.RemoveAll(tmpDir) + + c.Log("tmpdir", tmpDir) + + outfile := filepath.Join(tmpDir, "out.tar") + + dockerCmd(c, "save", "-o", outfile, idBar, idFoo) + dockerCmd(c, "rmi", idBar) + dockerCmd(c, "load", "-i", outfile) + + inspectOut := inspectField(c, idBar, "Parent") + c.Assert(inspectOut, checker.Equals, idFoo) + + inspectOut = inspectField(c, idFoo, "Parent") + c.Assert(inspectOut, checker.Equals, "") +} diff --git a/integration-cli/docker_cli_save_load_unix_test.go b/integration-cli/docker_cli_save_load_unix_test.go new file mode 100644 index 00000000..d9dd95f1 --- /dev/null +++ b/integration-cli/docker_cli_save_load_unix_test.go @@ -0,0 +1,87 @@ +// +build !windows + +package main + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" + "github.com/kr/pty" +) + +// save a repo and try to load it using stdout +func (s *DockerSuite) TestSaveAndLoadRepoStdout(c *check.C) { + name := "test-save-and-load-repo-stdout" + dockerCmd(c, "run", "--name", name, "busybox", "true") + + repoName := "foobar-save-load-test" + before, _ := dockerCmd(c, "commit", name, repoName) + before = strings.TrimRight(before, "\n") + + tmpFile, err := ioutil.TempFile("", "foobar-save-load-test.tar") + c.Assert(err, check.IsNil) + defer os.Remove(tmpFile.Name()) + + saveCmd := exec.Command(dockerBinary, "save", repoName) + saveCmd.Stdout = tmpFile + + _, err = runCommand(saveCmd) + c.Assert(err, check.IsNil) + + tmpFile, err = os.Open(tmpFile.Name()) + c.Assert(err, check.IsNil) + + deleteImages(repoName) + + loadCmd := exec.Command(dockerBinary, "load") + loadCmd.Stdin = tmpFile + + out, _, err := runCommandWithOutput(loadCmd) + c.Assert(err, check.IsNil, check.Commentf(out)) + + after := inspectField(c, repoName, "Id") + after = strings.TrimRight(after, "\n") + + c.Assert(after, check.Equals, before) //inspect is not the same after a save / load + + deleteImages(repoName) + + pty, tty, err := pty.Open() + c.Assert(err, check.IsNil) + cmd := exec.Command(dockerBinary, "save", repoName) + cmd.Stdin = tty + cmd.Stdout = tty + cmd.Stderr = tty + c.Assert(cmd.Start(), check.IsNil) + c.Assert(cmd.Wait(), check.NotNil) //did not break writing to a TTY + + buf := make([]byte, 1024) + + n, err := pty.Read(buf) + c.Assert(err, check.IsNil) //could not read tty output + c.Assert(string(buf[:n]), checker.Contains, "Cowardly refusing", check.Commentf("help output is not being yielded", out)) +} + +func (s *DockerSuite) TestSaveAndLoadWithProgressBar(c *check.C) { + name := "test-load" + _, err := buildImage(name, ` + FROM busybox + RUN touch aa + `, true) + c.Assert(err, check.IsNil) + + tmptar := name + ".tar" + dockerCmd(c, "save", "-o", tmptar, name) + defer os.Remove(tmptar) + + dockerCmd(c, "rmi", name) + dockerCmd(c, "tag", "busybox", name) + out, _ := dockerCmd(c, "load", "-i", tmptar) + expected := fmt.Sprintf("The image %s:latest already exists, renaming the old one with ID", name) + c.Assert(out, checker.Contains, expected) +} diff --git a/integration-cli/docker_cli_search_test.go b/integration-cli/docker_cli_search_test.go new file mode 100644 index 00000000..dfab8104 --- /dev/null +++ b/integration-cli/docker_cli_search_test.go @@ -0,0 +1,55 @@ +package main + +import ( + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +// search for repos named "registry" on the central registry +func (s *DockerSuite) TestSearchOnCentralRegistry(c *check.C) { + testRequires(c, Network, DaemonIsLinux) + + out, _ := dockerCmd(c, "search", "busybox") + c.Assert(out, checker.Contains, "Busybox base image.", check.Commentf("couldn't find any repository named (or containing) 'Busybox base image.'")) +} + +func (s *DockerSuite) TestSearchStarsOptionWithWrongParameter(c *check.C) { + out, _, err := dockerCmdWithError("search", "--stars=a", "busybox") + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "invalid value", check.Commentf("couldn't find the invalid value warning")) + + out, _, err = dockerCmdWithError("search", "-s=-1", "busybox") + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "invalid value", check.Commentf("couldn't find the invalid value warning")) +} + +func (s *DockerSuite) TestSearchCmdOptions(c *check.C) { + testRequires(c, Network) + + out, _ := dockerCmd(c, "search", "--help") + c.Assert(out, checker.Contains, "Usage:\tdocker search [OPTIONS] TERM") + + outSearchCmd, _ := dockerCmd(c, "search", "busybox") + outSearchCmdNotrunc, _ := dockerCmd(c, "search", "--no-trunc=true", "busybox") + c.Assert(len(outSearchCmd) > len(outSearchCmdNotrunc), check.Equals, false, check.Commentf("The no-trunc option can't take effect.")) + + outSearchCmdautomated, _ := dockerCmd(c, "search", "--automated=true", "busybox") //The busybox is a busybox base image, not an AUTOMATED image. + outSearchCmdautomatedSlice := strings.Split(outSearchCmdautomated, "\n") + for i := range outSearchCmdautomatedSlice { + c.Assert(strings.HasPrefix(outSearchCmdautomatedSlice[i], "busybox "), check.Equals, false, check.Commentf("The busybox is not an AUTOMATED image: %s", out)) + } + + outSearchCmdStars, _ := dockerCmd(c, "search", "-s=2", "busybox") + c.Assert(strings.Count(outSearchCmdStars, "[OK]") > strings.Count(outSearchCmd, "[OK]"), check.Equals, false, check.Commentf("The quantity of images with stars should be less than that of all images: %s", outSearchCmdStars)) + + dockerCmd(c, "search", "--stars=2", "--automated=true", "--no-trunc=true", "busybox") +} + +// search for repos which start with "ubuntu-" on the central registry +func (s *DockerSuite) TestSearchOnCentralRegistryWithDash(c *check.C) { + testRequires(c, Network, DaemonIsLinux) + + dockerCmd(c, "search", "ubuntu-") +} diff --git a/integration-cli/docker_cli_sni_test.go b/integration-cli/docker_cli_sni_test.go new file mode 100644 index 00000000..fb896d52 --- /dev/null +++ b/integration-cli/docker_cli_sni_test.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "io/ioutil" + "log" + "net/http" + "net/http/httptest" + "net/url" + "os/exec" + "strings" + + "github.com/go-check/check" +) + +func (s *DockerSuite) TestClientSetsTLSServerName(c *check.C) { + c.Skip("Flakey test") + // there may be more than one hit to the server for each registry request + serverNameReceived := []string{} + var serverName string + + virtualHostServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + serverNameReceived = append(serverNameReceived, r.TLS.ServerName) + })) + defer virtualHostServer.Close() + // discard TLS handshake errors written by default to os.Stderr + virtualHostServer.Config.ErrorLog = log.New(ioutil.Discard, "", 0) + + u, err := url.Parse(virtualHostServer.URL) + c.Assert(err, check.IsNil) + hostPort := u.Host + serverName = strings.Split(hostPort, ":")[0] + + repoName := fmt.Sprintf("%v/dockercli/image:latest", hostPort) + cmd := exec.Command(dockerBinary, "pull", repoName) + cmd.Run() + + // check that the fake server was hit at least once + c.Assert(len(serverNameReceived) > 0, check.Equals, true) + // check that for each hit the right server name was received + for _, item := range serverNameReceived { + c.Check(item, check.Equals, serverName) + } +} diff --git a/integration-cli/docker_cli_start_test.go b/integration-cli/docker_cli_start_test.go new file mode 100644 index 00000000..43342191 --- /dev/null +++ b/integration-cli/docker_cli_start_test.go @@ -0,0 +1,173 @@ +package main + +import ( + "fmt" + "strings" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +// Regression test for https://github.com/docker/docker/issues/7843 +func (s *DockerSuite) TestStartAttachReturnsOnError(c *check.C) { + // Windows does not support link + testRequires(c, DaemonIsLinux) + dockerCmd(c, "run", "--name", "test", "busybox") + + // Expect this to fail because the above container is stopped, this is what we want + out, _, err := dockerCmdWithError("run", "--name", "test2", "--link", "test:test", "busybox") + // err shouldn't be nil because container test2 try to link to stopped container + c.Assert(err, checker.NotNil, check.Commentf("out: %s", out)) + + ch := make(chan error) + go func() { + // Attempt to start attached to the container that won't start + // This should return an error immediately since the container can't be started + if _, _, err := dockerCmdWithError("start", "-a", "test2"); err == nil { + ch <- fmt.Errorf("Expected error but got none") + } + close(ch) + }() + + select { + case err := <-ch: + c.Assert(err, check.IsNil) + case <-time.After(5 * time.Second): + c.Fatalf("Attach did not exit properly") + } +} + +// gh#8555: Exit code should be passed through when using start -a +func (s *DockerSuite) TestStartAttachCorrectExitCode(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _, _ := dockerCmdWithStdoutStderr(c, "run", "-d", "busybox", "sh", "-c", "sleep 2; exit 1") + out = strings.TrimSpace(out) + + // make sure the container has exited before trying the "start -a" + dockerCmd(c, "wait", out) + + startOut, exitCode, err := dockerCmdWithError("start", "-a", out) + // start command should fail + c.Assert(err, checker.NotNil, check.Commentf("startOut: %s", startOut)) + // start -a did not respond with proper exit code + c.Assert(exitCode, checker.Equals, 1, check.Commentf("startOut: %s", startOut)) + +} + +func (s *DockerSuite) TestStartAttachSilent(c *check.C) { + name := "teststartattachcorrectexitcode" + dockerCmd(c, "run", "--name", name, "busybox", "echo", "test") + + // make sure the container has exited before trying the "start -a" + dockerCmd(c, "wait", name) + + startOut, _ := dockerCmd(c, "start", "-a", name) + // start -a produced unexpected output + c.Assert(startOut, checker.Equals, "test\n") +} + +func (s *DockerSuite) TestStartRecordError(c *check.C) { + // TODO Windows CI: Requires further porting work. Should be possible. + testRequires(c, DaemonIsLinux) + // when container runs successfully, we should not have state.Error + dockerCmd(c, "run", "-d", "-p", "9999:9999", "--name", "test", "busybox", "top") + stateErr := inspectField(c, "test", "State.Error") + // Expected to not have state error + c.Assert(stateErr, checker.Equals, "") + + // Expect this to fail and records error because of ports conflict + out, _, err := dockerCmdWithError("run", "-d", "--name", "test2", "-p", "9999:9999", "busybox", "top") + // err shouldn't be nil because docker run will fail + c.Assert(err, checker.NotNil, check.Commentf("out: %s", out)) + + stateErr = inspectField(c, "test2", "State.Error") + c.Assert(stateErr, checker.Contains, "port is already allocated") + + // Expect the conflict to be resolved when we stop the initial container + dockerCmd(c, "stop", "test") + dockerCmd(c, "start", "test2") + stateErr = inspectField(c, "test2", "State.Error") + // Expected to not have state error but got one + c.Assert(stateErr, checker.Equals, "") +} + +func (s *DockerSuite) TestStartPausedContainer(c *check.C) { + // Windows does not support pausing containers + testRequires(c, DaemonIsLinux) + defer unpauseAllContainers() + + dockerCmd(c, "run", "-d", "--name", "testing", "busybox", "top") + + dockerCmd(c, "pause", "testing") + + out, _, err := dockerCmdWithError("start", "testing") + // an error should have been shown that you cannot start paused container + c.Assert(err, checker.NotNil, check.Commentf("out: %s", out)) + // an error should have been shown that you cannot start paused container + c.Assert(out, checker.Contains, "Cannot start a paused container, try unpause instead.") +} + +func (s *DockerSuite) TestStartMultipleContainers(c *check.C) { + // Windows does not support --link + testRequires(c, DaemonIsLinux) + // run a container named 'parent' and create two container link to `parent` + dockerCmd(c, "run", "-d", "--name", "parent", "busybox", "top") + + for _, container := range []string{"child_first", "child_second"} { + dockerCmd(c, "create", "--name", container, "--link", "parent:parent", "busybox", "top") + } + + // stop 'parent' container + dockerCmd(c, "stop", "parent") + + out := inspectField(c, "parent", "State.Running") + // Container should be stopped + c.Assert(out, checker.Equals, "false") + + // start all the three containers, container `child_first` start first which should be failed + // container 'parent' start second and then start container 'child_second' + expOut := "Cannot link to a non running container" + expErr := "failed to start containers: [child_first]" + out, _, err := dockerCmdWithError("start", "child_first", "parent", "child_second") + // err shouldn't be nil because start will fail + c.Assert(err, checker.NotNil, check.Commentf("out: %s", out)) + // output does not correspond to what was expected + if !(strings.Contains(out, expOut) || strings.Contains(err.Error(), expErr)) { + c.Fatalf("Expected out: %v with err: %v but got out: %v with err: %v", expOut, expErr, out, err) + } + + for container, expected := range map[string]string{"parent": "true", "child_first": "false", "child_second": "true"} { + out := inspectField(c, container, "State.Running") + // Container running state wrong + c.Assert(out, checker.Equals, expected) + } +} + +func (s *DockerSuite) TestStartAttachMultipleContainers(c *check.C) { + // run multiple containers to test + for _, container := range []string{"test1", "test2", "test3"} { + runSleepingContainer(c, "--name", container) + } + + // stop all the containers + for _, container := range []string{"test1", "test2", "test3"} { + dockerCmd(c, "stop", container) + } + + // test start and attach multiple containers at once, expected error + for _, option := range []string{"-a", "-i", "-ai"} { + out, _, err := dockerCmdWithError("start", option, "test1", "test2", "test3") + // err shouldn't be nil because start will fail + c.Assert(err, checker.NotNil, check.Commentf("out: %s", out)) + // output does not correspond to what was expected + c.Assert(out, checker.Contains, "You cannot start and attach multiple containers at once.") + } + + // confirm the state of all the containers be stopped + for container, expected := range map[string]string{"test1": "false", "test2": "false", "test3": "false"} { + out := inspectField(c, container, "State.Running") + // Container running state wrong + c.Assert(out, checker.Equals, expected) + } +} diff --git a/integration-cli/docker_cli_start_volume_driver_unix_test.go b/integration-cli/docker_cli_start_volume_driver_unix_test.go new file mode 100644 index 00000000..cc5d0b11 --- /dev/null +++ b/integration-cli/docker_cli_start_volume_driver_unix_test.go @@ -0,0 +1,443 @@ +// +build !windows + +package main + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/engine-api/types" + "github.com/go-check/check" +) + +func init() { + check.Suite(&DockerExternalVolumeSuite{ + ds: &DockerSuite{}, + }) +} + +type eventCounter struct { + activations int + creations int + removals int + mounts int + unmounts int + paths int + lists int + gets int +} + +type DockerExternalVolumeSuite struct { + server *httptest.Server + ds *DockerSuite + d *Daemon + ec *eventCounter +} + +func (s *DockerExternalVolumeSuite) SetUpTest(c *check.C) { + s.d = NewDaemon(c) + s.ec = &eventCounter{} +} + +func (s *DockerExternalVolumeSuite) TearDownTest(c *check.C) { + s.d.Stop() + s.ds.TearDownTest(c) +} + +func (s *DockerExternalVolumeSuite) SetUpSuite(c *check.C) { + mux := http.NewServeMux() + s.server = httptest.NewServer(mux) + + type pluginRequest struct { + Name string + Opts map[string]string + } + + type pluginResp struct { + Mountpoint string `json:",omitempty"` + Err string `json:",omitempty"` + } + + type vol struct { + Name string + Mountpoint string + Ninja bool // hack used to trigger an null volume return on `Get` + } + var volList []vol + + read := func(b io.ReadCloser) (pluginRequest, error) { + defer b.Close() + var pr pluginRequest + if err := json.NewDecoder(b).Decode(&pr); err != nil { + return pr, err + } + return pr, nil + } + + send := func(w http.ResponseWriter, data interface{}) { + switch t := data.(type) { + case error: + http.Error(w, t.Error(), 500) + case string: + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + fmt.Fprintln(w, t) + default: + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + json.NewEncoder(w).Encode(&data) + } + } + + mux.HandleFunc("/Plugin.Activate", func(w http.ResponseWriter, r *http.Request) { + s.ec.activations++ + send(w, `{"Implements": ["VolumeDriver"]}`) + }) + + mux.HandleFunc("/VolumeDriver.Create", func(w http.ResponseWriter, r *http.Request) { + s.ec.creations++ + pr, err := read(r.Body) + if err != nil { + send(w, err) + return + } + _, isNinja := pr.Opts["ninja"] + volList = append(volList, vol{Name: pr.Name, Ninja: isNinja}) + send(w, nil) + }) + + mux.HandleFunc("/VolumeDriver.List", func(w http.ResponseWriter, r *http.Request) { + s.ec.lists++ + vols := []vol{} + for _, v := range volList { + if v.Ninja { + continue + } + vols = append(vols, v) + } + send(w, map[string][]vol{"Volumes": vols}) + }) + + mux.HandleFunc("/VolumeDriver.Get", func(w http.ResponseWriter, r *http.Request) { + s.ec.gets++ + pr, err := read(r.Body) + if err != nil { + send(w, err) + return + } + + for _, v := range volList { + if v.Name == pr.Name { + if v.Ninja { + send(w, map[string]vol{}) + return + } + v.Mountpoint = hostVolumePath(pr.Name) + send(w, map[string]vol{"Volume": v}) + return + } + } + send(w, `{"Err": "no such volume"}`) + }) + + mux.HandleFunc("/VolumeDriver.Remove", func(w http.ResponseWriter, r *http.Request) { + s.ec.removals++ + pr, err := read(r.Body) + if err != nil { + send(w, err) + return + } + + for i, v := range volList { + if v.Name == pr.Name { + if err := os.RemoveAll(hostVolumePath(v.Name)); err != nil { + send(w, &pluginResp{Err: err.Error()}) + return + } + volList = append(volList[:i], volList[i+1:]...) + break + } + } + send(w, nil) + }) + + mux.HandleFunc("/VolumeDriver.Path", func(w http.ResponseWriter, r *http.Request) { + s.ec.paths++ + + pr, err := read(r.Body) + if err != nil { + send(w, err) + return + } + p := hostVolumePath(pr.Name) + send(w, &pluginResp{Mountpoint: p}) + }) + + mux.HandleFunc("/VolumeDriver.Mount", func(w http.ResponseWriter, r *http.Request) { + s.ec.mounts++ + + pr, err := read(r.Body) + if err != nil { + send(w, err) + return + } + + p := hostVolumePath(pr.Name) + if err := os.MkdirAll(p, 0755); err != nil { + send(w, &pluginResp{Err: err.Error()}) + return + } + + if err := ioutil.WriteFile(filepath.Join(p, "test"), []byte(s.server.URL), 0644); err != nil { + send(w, err) + return + } + + send(w, &pluginResp{Mountpoint: p}) + }) + + mux.HandleFunc("/VolumeDriver.Unmount", func(w http.ResponseWriter, r *http.Request) { + s.ec.unmounts++ + + _, err := read(r.Body) + if err != nil { + send(w, err) + return + } + + send(w, nil) + }) + + err := os.MkdirAll("/etc/docker/plugins", 0755) + c.Assert(err, checker.IsNil) + + err = ioutil.WriteFile("/etc/docker/plugins/test-external-volume-driver.spec", []byte(s.server.URL), 0644) + c.Assert(err, checker.IsNil) +} + +func (s *DockerExternalVolumeSuite) TearDownSuite(c *check.C) { + s.server.Close() + + err := os.RemoveAll("/etc/docker/plugins") + c.Assert(err, checker.IsNil) +} + +func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverNamed(c *check.C) { + err := s.d.StartWithBusybox() + c.Assert(err, checker.IsNil) + + out, err := s.d.Cmd("run", "--rm", "--name", "test-data", "-v", "external-volume-test:/tmp/external-volume-test", "--volume-driver", "test-external-volume-driver", "busybox:latest", "cat", "/tmp/external-volume-test/test") + c.Assert(err, checker.IsNil, check.Commentf(out)) + c.Assert(out, checker.Contains, s.server.URL) + + _, err = s.d.Cmd("volume", "rm", "external-volume-test") + c.Assert(err, checker.IsNil) + + p := hostVolumePath("external-volume-test") + _, err = os.Lstat(p) + c.Assert(err, checker.NotNil) + c.Assert(os.IsNotExist(err), checker.True, check.Commentf("Expected volume path in host to not exist: %s, %v\n", p, err)) + + c.Assert(s.ec.activations, checker.Equals, 1) + c.Assert(s.ec.creations, checker.Equals, 1) + c.Assert(s.ec.removals, checker.Equals, 1) + c.Assert(s.ec.mounts, checker.Equals, 1) + c.Assert(s.ec.unmounts, checker.Equals, 1) +} + +func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverUnnamed(c *check.C) { + err := s.d.StartWithBusybox() + c.Assert(err, checker.IsNil) + + out, err := s.d.Cmd("run", "--rm", "--name", "test-data", "-v", "/tmp/external-volume-test", "--volume-driver", "test-external-volume-driver", "busybox:latest", "cat", "/tmp/external-volume-test/test") + c.Assert(err, checker.IsNil, check.Commentf(out)) + c.Assert(out, checker.Contains, s.server.URL) + + c.Assert(s.ec.activations, checker.Equals, 1) + c.Assert(s.ec.creations, checker.Equals, 1) + c.Assert(s.ec.removals, checker.Equals, 1) + c.Assert(s.ec.mounts, checker.Equals, 1) + c.Assert(s.ec.unmounts, checker.Equals, 1) +} + +func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverVolumesFrom(c *check.C) { + err := s.d.StartWithBusybox() + c.Assert(err, checker.IsNil) + + out, err := s.d.Cmd("run", "--name", "vol-test1", "-v", "/foo", "--volume-driver", "test-external-volume-driver", "busybox:latest") + c.Assert(err, checker.IsNil, check.Commentf(out)) + + out, err = s.d.Cmd("run", "--rm", "--volumes-from", "vol-test1", "--name", "vol-test2", "busybox", "ls", "/tmp") + c.Assert(err, checker.IsNil, check.Commentf(out)) + + out, err = s.d.Cmd("rm", "-fv", "vol-test1") + c.Assert(err, checker.IsNil, check.Commentf(out)) + + c.Assert(s.ec.activations, checker.Equals, 1) + c.Assert(s.ec.creations, checker.Equals, 1) + c.Assert(s.ec.removals, checker.Equals, 1) + c.Assert(s.ec.mounts, checker.Equals, 2) + c.Assert(s.ec.unmounts, checker.Equals, 2) +} + +func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverDeleteContainer(c *check.C) { + err := s.d.StartWithBusybox() + c.Assert(err, checker.IsNil) + + out, err := s.d.Cmd("run", "--name", "vol-test1", "-v", "/foo", "--volume-driver", "test-external-volume-driver", "busybox:latest") + c.Assert(err, checker.IsNil, check.Commentf(out)) + + out, err = s.d.Cmd("rm", "-fv", "vol-test1") + c.Assert(err, checker.IsNil, check.Commentf(out)) + + c.Assert(s.ec.activations, checker.Equals, 1) + c.Assert(s.ec.creations, checker.Equals, 1) + c.Assert(s.ec.removals, checker.Equals, 1) + c.Assert(s.ec.mounts, checker.Equals, 1) + c.Assert(s.ec.unmounts, checker.Equals, 1) +} + +func hostVolumePath(name string) string { + return fmt.Sprintf("/var/lib/docker/volumes/%s", name) +} + +// Make sure a request to use a down driver doesn't block other requests +func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverLookupNotBlocked(c *check.C) { + specPath := "/etc/docker/plugins/down-driver.spec" + err := ioutil.WriteFile(specPath, []byte("tcp://127.0.0.7:9999"), 0644) + c.Assert(err, check.IsNil) + defer os.RemoveAll(specPath) + + chCmd1 := make(chan struct{}) + chCmd2 := make(chan error) + cmd1 := exec.Command(dockerBinary, "volume", "create", "-d", "down-driver") + cmd2 := exec.Command(dockerBinary, "volume", "create") + + c.Assert(cmd1.Start(), checker.IsNil) + defer cmd1.Process.Kill() + time.Sleep(100 * time.Millisecond) // ensure API has been called + c.Assert(cmd2.Start(), checker.IsNil) + + go func() { + cmd1.Wait() + close(chCmd1) + }() + go func() { + chCmd2 <- cmd2.Wait() + }() + + select { + case <-chCmd1: + cmd2.Process.Kill() + c.Fatalf("volume create with down driver finished unexpectedly") + case err := <-chCmd2: + c.Assert(err, checker.IsNil) + case <-time.After(5 * time.Second): + cmd2.Process.Kill() + c.Fatal("volume creates are blocked by previous create requests when previous driver is down") + } +} + +func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverRetryNotImmediatelyExists(c *check.C) { + err := s.d.StartWithBusybox() + c.Assert(err, checker.IsNil) + + specPath := "/etc/docker/plugins/test-external-volume-driver-retry.spec" + os.RemoveAll(specPath) + defer os.RemoveAll(specPath) + + errchan := make(chan error) + go func() { + if out, err := s.d.Cmd("run", "--rm", "--name", "test-data-retry", "-v", "external-volume-test:/tmp/external-volume-test", "--volume-driver", "test-external-volume-driver-retry", "busybox:latest"); err != nil { + errchan <- fmt.Errorf("%v:\n%s", err, out) + } + close(errchan) + }() + go func() { + // wait for a retry to occur, then create spec to allow plugin to register + time.Sleep(2000 * time.Millisecond) + // no need to check for an error here since it will get picked up by the timeout later + ioutil.WriteFile(specPath, []byte(s.server.URL), 0644) + }() + + select { + case err := <-errchan: + c.Assert(err, checker.IsNil) + case <-time.After(8 * time.Second): + c.Fatal("volume creates fail when plugin not immediately available") + } + + _, err = s.d.Cmd("volume", "rm", "external-volume-test") + c.Assert(err, checker.IsNil) + + c.Assert(s.ec.activations, checker.Equals, 1) + c.Assert(s.ec.creations, checker.Equals, 1) + c.Assert(s.ec.removals, checker.Equals, 1) + c.Assert(s.ec.mounts, checker.Equals, 1) + c.Assert(s.ec.unmounts, checker.Equals, 1) +} + +func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverBindExternalVolume(c *check.C) { + dockerCmd(c, "volume", "create", "-d", "test-external-volume-driver", "--name", "foo") + dockerCmd(c, "run", "-d", "--name", "testing", "-v", "foo:/bar", "busybox", "top") + + var mounts []struct { + Name string + Driver string + } + out := inspectFieldJSON(c, "testing", "Mounts") + c.Assert(json.NewDecoder(strings.NewReader(out)).Decode(&mounts), checker.IsNil) + c.Assert(len(mounts), checker.Equals, 1, check.Commentf(out)) + c.Assert(mounts[0].Name, checker.Equals, "foo") + c.Assert(mounts[0].Driver, checker.Equals, "test-external-volume-driver") +} + +func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverList(c *check.C) { + dockerCmd(c, "volume", "create", "-d", "test-external-volume-driver", "--name", "abc3") + out, _ := dockerCmd(c, "volume", "ls") + ls := strings.Split(strings.TrimSpace(out), "\n") + c.Assert(len(ls), check.Equals, 2, check.Commentf("\n%s", out)) + + vol := strings.Fields(ls[len(ls)-1]) + c.Assert(len(vol), check.Equals, 2, check.Commentf("%v", vol)) + c.Assert(vol[0], check.Equals, "test-external-volume-driver") + c.Assert(vol[1], check.Equals, "abc3") + + c.Assert(s.ec.lists, check.Equals, 1) +} + +func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverGet(c *check.C) { + out, _, err := dockerCmdWithError("volume", "inspect", "dummy") + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(s.ec.gets, check.Equals, 1) + c.Assert(out, checker.Contains, "No such volume") +} + +func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverWithDaemnRestart(c *check.C) { + dockerCmd(c, "volume", "create", "-d", "test-external-volume-driver", "--name", "abc1") + err := s.d.Restart() + c.Assert(err, checker.IsNil) + + dockerCmd(c, "run", "--name=test", "-v", "abc1:/foo", "busybox", "true") + var mounts []types.MountPoint + inspectFieldAndMarshall(c, "test", "Mounts", &mounts) + c.Assert(mounts, checker.HasLen, 1) + c.Assert(mounts[0].Driver, checker.Equals, "test-external-volume-driver") +} + +// Ensures that the daemon handles when the plugin responds to a `Get` request with a null volume and a null error. +// Prior the daemon would panic in this scenario. +func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverGetEmptyResponse(c *check.C) { + dockerCmd(c, "volume", "create", "-d", "test-external-volume-driver", "--name", "abc2", "--opt", "ninja=1") + out, _, err := dockerCmdWithError("volume", "inspect", "abc2") + c.Assert(err, checker.NotNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "No such volume") +} diff --git a/integration-cli/docker_cli_stats_test.go b/integration-cli/docker_cli_stats_test.go new file mode 100644 index 00000000..e3c7a3e2 --- /dev/null +++ b/integration-cli/docker_cli_stats_test.go @@ -0,0 +1,162 @@ +package main + +import ( + "bufio" + "os/exec" + "regexp" + "strings" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestStatsNoStream(c *check.C) { + // Windows does not support stats + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "top") + id := strings.TrimSpace(out) + c.Assert(waitRun(id), checker.IsNil) + + statsCmd := exec.Command(dockerBinary, "stats", "--no-stream", id) + type output struct { + out []byte + err error + } + + ch := make(chan output) + go func() { + out, err := statsCmd.Output() + ch <- output{out, err} + }() + + select { + case outerr := <-ch: + c.Assert(outerr.err, checker.IsNil, check.Commentf("Error running stats: %v", outerr.err)) + c.Assert(string(outerr.out), checker.Contains, id) //running container wasn't present in output + case <-time.After(3 * time.Second): + statsCmd.Process.Kill() + c.Fatalf("stats did not return immediately when not streaming") + } +} + +func (s *DockerSuite) TestStatsContainerNotFound(c *check.C) { + // Windows does not support stats + testRequires(c, DaemonIsLinux) + + out, _, err := dockerCmdWithError("stats", "notfound") + c.Assert(err, checker.NotNil) + c.Assert(out, checker.Contains, "No such container: notfound", check.Commentf("Expected to fail on not found container stats, got %q instead", out)) + + out, _, err = dockerCmdWithError("stats", "--no-stream", "notfound") + c.Assert(err, checker.NotNil) + c.Assert(out, checker.Contains, "No such container: notfound", check.Commentf("Expected to fail on not found container stats with --no-stream, got %q instead", out)) +} + +func (s *DockerSuite) TestStatsAllRunningNoStream(c *check.C) { + // Windows does not support stats + testRequires(c, DaemonIsLinux) + + out, _ := dockerCmd(c, "run", "-d", "busybox", "top") + id1 := strings.TrimSpace(out)[:12] + c.Assert(waitRun(id1), check.IsNil) + out, _ = dockerCmd(c, "run", "-d", "busybox", "top") + id2 := strings.TrimSpace(out)[:12] + c.Assert(waitRun(id2), check.IsNil) + out, _ = dockerCmd(c, "run", "-d", "busybox", "top") + id3 := strings.TrimSpace(out)[:12] + c.Assert(waitRun(id3), check.IsNil) + dockerCmd(c, "stop", id3) + + out, _ = dockerCmd(c, "stats", "--no-stream") + if !strings.Contains(out, id1) || !strings.Contains(out, id2) { + c.Fatalf("Expected stats output to contain both %s and %s, got %s", id1, id2, out) + } + if strings.Contains(out, id3) { + c.Fatalf("Did not expect %s in stats, got %s", id3, out) + } + + // check output contains real data, but not all zeros + reg, _ := regexp.Compile("[1-9]+") + // split output with "\n", outLines[1] is id2's output + // outLines[2] is id1's output + outLines := strings.Split(out, "\n") + // check stat result of id2 contains real data + realData := reg.Find([]byte(outLines[1][12:])) + c.Assert(realData, checker.NotNil, check.Commentf("stat result are empty: %s", out)) + // check stat result of id1 contains real data + realData = reg.Find([]byte(outLines[2][12:])) + c.Assert(realData, checker.NotNil, check.Commentf("stat result are empty: %s", out)) +} + +func (s *DockerSuite) TestStatsAllNoStream(c *check.C) { + // Windows does not support stats + testRequires(c, DaemonIsLinux) + + out, _ := dockerCmd(c, "run", "-d", "busybox", "top") + id1 := strings.TrimSpace(out)[:12] + c.Assert(waitRun(id1), check.IsNil) + dockerCmd(c, "stop", id1) + out, _ = dockerCmd(c, "run", "-d", "busybox", "top") + id2 := strings.TrimSpace(out)[:12] + c.Assert(waitRun(id2), check.IsNil) + + out, _ = dockerCmd(c, "stats", "--all", "--no-stream") + if !strings.Contains(out, id1) || !strings.Contains(out, id2) { + c.Fatalf("Expected stats output to contain both %s and %s, got %s", id1, id2, out) + } + + // check output contains real data, but not all zeros + reg, _ := regexp.Compile("[1-9]+") + // split output with "\n", outLines[1] is id2's output + outLines := strings.Split(out, "\n") + // check stat result of id2 contains real data + realData := reg.Find([]byte(outLines[1][12:])) + c.Assert(realData, checker.NotNil, check.Commentf("stat result of %s is empty: %s", id2, out)) + // check stat result of id1 contains all zero + realData = reg.Find([]byte(outLines[2][12:])) + c.Assert(realData, checker.IsNil, check.Commentf("stat result of %s should be empty : %s", id1, out)) +} + +func (s *DockerSuite) TestStatsAllNewContainersAdded(c *check.C) { + // Windows does not support stats + // TODO: remove SameHostDaemon + // The reason it was added is because, there seems to be some race that makes this test fail + // for remote daemons (namely in the win2lin CI). We highly welcome contributions to fix this. + testRequires(c, DaemonIsLinux, SameHostDaemon) + + id := make(chan string) + addedChan := make(chan struct{}) + + runSleepingContainer(c, "-d") + statsCmd := exec.Command(dockerBinary, "stats") + stdout, err := statsCmd.StdoutPipe() + c.Assert(err, check.IsNil) + c.Assert(statsCmd.Start(), check.IsNil) + defer statsCmd.Process.Kill() + + go func() { + containerID := <-id + matchID := regexp.MustCompile(containerID) + + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + switch { + case matchID.MatchString(scanner.Text()): + close(addedChan) + return + } + } + }() + + out, _ := runSleepingContainer(c, "-d") + c.Assert(waitRun(strings.TrimSpace(out)), check.IsNil) + id <- strings.TrimSpace(out)[:12] + + select { + case <-time.After(30 * time.Second): + c.Fatal("failed to observe new container created added to stats") + case <-addedChan: + // ignore, done + } +} diff --git a/integration-cli/docker_cli_tag_test.go b/integration-cli/docker_cli_tag_test.go new file mode 100644 index 00000000..1e601527 --- /dev/null +++ b/integration-cli/docker_cli_tag_test.go @@ -0,0 +1,251 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/stringutils" + "github.com/go-check/check" +) + +// tagging a named image in a new unprefixed repo should work +func (s *DockerSuite) TestTagUnprefixedRepoByName(c *check.C) { + // Don't attempt to pull on Windows as not in hub. It's installed + // as an image through .ensure-frozen-images-windows + if daemonPlatform != "windows" { + if err := pullImageIfNotExist("busybox:latest"); err != nil { + c.Fatal("couldn't find the busybox:latest image locally and failed to pull it") + } + } + + dockerCmd(c, "tag", "busybox:latest", "testfoobarbaz") +} + +// tagging an image by ID in a new unprefixed repo should work +func (s *DockerSuite) TestTagUnprefixedRepoByID(c *check.C) { + imageID := inspectField(c, "busybox", "Id") + dockerCmd(c, "tag", imageID, "testfoobarbaz") +} + +// ensure we don't allow the use of invalid repository names; these tag operations should fail +func (s *DockerSuite) TestTagInvalidUnprefixedRepo(c *check.C) { + invalidRepos := []string{"fo$z$", "Foo@3cc", "Foo$3", "Foo*3", "Fo^3", "Foo!3", "F)xcz(", "fo%asd", "FOO/bar"} + + for _, repo := range invalidRepos { + out, _, err := dockerCmdWithError("tag", "busybox", repo) + c.Assert(err, checker.NotNil, check.Commentf("tag busybox %v should have failed : %v", repo, out)) + } +} + +// ensure we don't allow the use of invalid tags; these tag operations should fail +func (s *DockerSuite) TestTagInvalidPrefixedRepo(c *check.C) { + longTag := stringutils.GenerateRandomAlphaOnlyString(121) + + invalidTags := []string{"repo:fo$z$", "repo:Foo@3cc", "repo:Foo$3", "repo:Foo*3", "repo:Fo^3", "repo:Foo!3", "repo:%goodbye", "repo:#hashtagit", "repo:F)xcz(", "repo:-foo", "repo:..", longTag} + + for _, repotag := range invalidTags { + out, _, err := dockerCmdWithError("tag", "busybox", repotag) + c.Assert(err, checker.NotNil, check.Commentf("tag busybox %v should have failed : %v", repotag, out)) + } +} + +// ensure we allow the use of valid tags +func (s *DockerSuite) TestTagValidPrefixedRepo(c *check.C) { + // Don't attempt to pull on Windows as not in hub. It's installed + // as an image through .ensure-frozen-images-windows + if daemonPlatform != "windows" { + if err := pullImageIfNotExist("busybox:latest"); err != nil { + c.Fatal("couldn't find the busybox:latest image locally and failed to pull it") + } + } + + validRepos := []string{"fooo/bar", "fooaa/test", "foooo:t", "HOSTNAME.DOMAIN.COM:443/foo/bar"} + + for _, repo := range validRepos { + _, _, err := dockerCmdWithError("tag", "busybox:latest", repo) + if err != nil { + c.Errorf("tag busybox %v should have worked: %s", repo, err) + continue + } + deleteImages(repo) + } +} + +// tag an image with an existed tag name without -f option should work +func (s *DockerSuite) TestTagExistedNameWithoutForce(c *check.C) { + // Don't attempt to pull on Windows as not in hub. It's installed + // as an image through .ensure-frozen-images-windows + if daemonPlatform != "windows" { + if err := pullImageIfNotExist("busybox:latest"); err != nil { + c.Fatal("couldn't find the busybox:latest image locally and failed to pull it") + } + } + + dockerCmd(c, "tag", "busybox:latest", "busybox:test") +} + +// tag an image with an existed tag name with -f option should work +func (s *DockerSuite) TestTagExistedNameWithForce(c *check.C) { + // Don't attempt to pull on Windows as not in hub. It's installed + // as an image through .ensure-frozen-images-windows + if daemonPlatform != "windows" { + if err := pullImageIfNotExist("busybox:latest"); err != nil { + c.Fatal("couldn't find the busybox:latest image locally and failed to pull it") + } + } + dockerCmd(c, "tag", "busybox:latest", "busybox:test") + dockerCmd(c, "tag", "-f", "busybox:latest", "busybox:test") +} + +func (s *DockerSuite) TestTagWithPrefixHyphen(c *check.C) { + // TODO Windows CI. This fails on TP4 docker, but has since been fixed. + // Enable these tests for TP5. + testRequires(c, DaemonIsLinux) + // Don't attempt to pull on Windows as not in hub. It's installed + // as an image through .ensure-frozen-images-windows + if daemonPlatform != "windows" { + if err := pullImageIfNotExist("busybox:latest"); err != nil { + c.Fatal("couldn't find the busybox:latest image locally and failed to pull it") + } + } + // test repository name begin with '-' + out, _, err := dockerCmdWithError("tag", "busybox:latest", "-busybox:test") + c.Assert(err, checker.NotNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "Error parsing reference", check.Commentf("tag a name begin with '-' should failed")) + + // test namespace name begin with '-' + out, _, err = dockerCmdWithError("tag", "busybox:latest", "-test/busybox:test") + c.Assert(err, checker.NotNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "Error parsing reference", check.Commentf("tag a name begin with '-' should failed")) + + // test index name begin with '-' + out, _, err = dockerCmdWithError("tag", "busybox:latest", "-index:5000/busybox:test") + c.Assert(err, checker.NotNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "Error parsing reference", check.Commentf("tag a name begin with '-' should failed")) +} + +// ensure tagging using official names works +// ensure all tags result in the same name +func (s *DockerSuite) TestTagOfficialNames(c *check.C) { + // TODO Windows CI. This fails on TP4 docker, but has since been fixed. + // Enable these tests for TP5. + testRequires(c, DaemonIsLinux) + names := []string{ + "docker.io/busybox", + "index.docker.io/busybox", + "library/busybox", + "docker.io/library/busybox", + "index.docker.io/library/busybox", + } + + for _, name := range names { + out, exitCode, err := dockerCmdWithError("tag", "busybox:latest", name+":latest") + if err != nil || exitCode != 0 { + c.Errorf("tag busybox %v should have worked: %s, %s", name, err, out) + continue + } + + // ensure we don't have multiple tag names. + out, _, err = dockerCmdWithError("images") + if err != nil { + c.Errorf("listing images failed with errors: %v, %s", err, out) + } else if strings.Contains(out, name) { + c.Errorf("images should not have listed '%s'", name) + deleteImages(name + ":latest") + } + } + + for _, name := range names { + _, exitCode, err := dockerCmdWithError("tag", name+":latest", "fooo/bar:latest") + if err != nil || exitCode != 0 { + c.Errorf("tag %v fooo/bar should have worked: %s", name, err) + continue + } + deleteImages("fooo/bar:latest") + } +} + +// ensure tags can not match digests +func (s *DockerSuite) TestTagMatchesDigest(c *check.C) { + // TODO Windows CI. This can be enabled for TP5, but will fail on TP4. + // This is due to the content addressibility changes which are not + // in the TP4 version of Docker. + testRequires(c, DaemonIsLinux) + // Don't attempt to pull on Windows as not in hub. It's installed + // as an image through .ensure-frozen-images-windows + if daemonPlatform != "windows" { + if err := pullImageIfNotExist("busybox:latest"); err != nil { + c.Fatal("couldn't find the busybox:latest image locally and failed to pull it") + } + } + digest := "busybox@sha256:abcdef76720241213f5303bda7704ec4c2ef75613173910a56fb1b6e20251507" + // test setting tag fails + _, _, err := dockerCmdWithError("tag", "busybox:latest", digest) + if err == nil { + c.Fatal("digest tag a name should have failed") + } + // check that no new image matches the digest + _, _, err = dockerCmdWithError("inspect", digest) + if err == nil { + c.Fatal("inspecting by digest should have failed") + } +} + +func (s *DockerSuite) TestTagInvalidRepoName(c *check.C) { + // TODO Windows CI. This can be enabled for TP5, but will fail on the + // TP4 version of docker. + testRequires(c, DaemonIsLinux) + // Don't attempt to pull on Windows as not in hub. It's installed + // as an image through .ensure-frozen-images-windows + if daemonPlatform != "windows" { + if err := pullImageIfNotExist("busybox:latest"); err != nil { + c.Fatal("couldn't find the busybox:latest image locally and failed to pull it") + } + } + + // test setting tag fails + _, _, err := dockerCmdWithError("tag", "busybox:latest", "sha256:sometag") + if err == nil { + c.Fatal("tagging with image named \"sha256\" should have failed") + } +} + +// ensure tags cannot create ambiguity with image ids +func (s *DockerSuite) TestTagTruncationAmbiguity(c *check.C) { + //testRequires(c, DaemonIsLinux) + // Don't attempt to pull on Windows as not in hub. It's installed + // as an image through .ensure-frozen-images-windows + if daemonPlatform != "windows" { + if err := pullImageIfNotExist("busybox:latest"); err != nil { + c.Fatal("couldn't find the busybox:latest image locally and failed to pull it") + } + } + imageID, err := buildImage("notbusybox:latest", + `FROM busybox + MAINTAINER dockerio`, + true) + if err != nil { + c.Fatal(err) + } + truncatedImageID := stringid.TruncateID(imageID) + truncatedTag := fmt.Sprintf("notbusybox:%s", truncatedImageID) + + id := inspectField(c, truncatedTag, "Id") + + // Ensure inspect by image id returns image for image id + c.Assert(id, checker.Equals, imageID) + c.Logf("Built image: %s", imageID) + + // test setting tag fails + _, _, err = dockerCmdWithError("tag", "busybox:latest", truncatedTag) + if err != nil { + c.Fatalf("Error tagging with an image id: %s", err) + } + + id = inspectField(c, truncatedTag, "Id") + + // Ensure id is imageID and not busybox:latest + c.Assert(id, checker.Not(checker.Equals), imageID) +} diff --git a/integration-cli/docker_cli_top_test.go b/integration-cli/docker_cli_top_test.go new file mode 100644 index 00000000..e0865b92 --- /dev/null +++ b/integration-cli/docker_cli_top_test.go @@ -0,0 +1,43 @@ +package main + +import ( + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestTopMultipleArgs(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-i", "-d", "busybox", "top") + cleanedContainerID := strings.TrimSpace(out) + + out, _ = dockerCmd(c, "top", cleanedContainerID, "-o", "pid") + c.Assert(out, checker.Contains, "PID", check.Commentf("did not see PID after top -o pid: %s", out)) +} + +func (s *DockerSuite) TestTopNonPrivileged(c *check.C) { + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-i", "-d", "busybox", "top") + cleanedContainerID := strings.TrimSpace(out) + + out1, _ := dockerCmd(c, "top", cleanedContainerID) + out2, _ := dockerCmd(c, "top", cleanedContainerID) + dockerCmd(c, "kill", cleanedContainerID) + + c.Assert(out1, checker.Contains, "top", check.Commentf("top should've listed `top` in the process list, but failed the first time")) + c.Assert(out2, checker.Contains, "top", check.Commentf("top should've listed `top` in the process list, but failed the second time")) +} + +func (s *DockerSuite) TestTopPrivileged(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace) + out, _ := dockerCmd(c, "run", "--privileged", "-i", "-d", "busybox", "top") + cleanedContainerID := strings.TrimSpace(out) + + out1, _ := dockerCmd(c, "top", cleanedContainerID) + out2, _ := dockerCmd(c, "top", cleanedContainerID) + dockerCmd(c, "kill", cleanedContainerID) + + c.Assert(out1, checker.Contains, "top", check.Commentf("top should've listed `top` in the process list, but failed the first time")) + c.Assert(out2, checker.Contains, "top", check.Commentf("top should've listed `top` in the process list, but failed the second time")) +} diff --git a/integration-cli/docker_cli_update_test.go b/integration-cli/docker_cli_update_test.go new file mode 100644 index 00000000..188030ff --- /dev/null +++ b/integration-cli/docker_cli_update_test.go @@ -0,0 +1,31 @@ +package main + +import ( + "strings" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestUpdateRestartPolicy(c *check.C) { + out, _ := dockerCmd(c, "run", "-d", "--restart=on-failure:3", "busybox", "sh", "-c", "sleep 1 && false") + timeout := 60 * time.Second + if daemonPlatform == "windows" { + timeout = 180 * time.Second + } + + id := strings.TrimSpace(string(out)) + + // update restart policy to on-failure:5 + dockerCmd(c, "update", "--restart=on-failure:5", id) + + err := waitExited(id, timeout) + c.Assert(err, checker.IsNil) + + count := inspectField(c, id, "RestartCount") + c.Assert(count, checker.Equals, "5") + + maximumRetryCount := inspectField(c, id, "HostConfig.RestartPolicy.MaximumRetryCount") + c.Assert(maximumRetryCount, checker.Equals, "5") +} diff --git a/integration-cli/docker_cli_update_unix_test.go b/integration-cli/docker_cli_update_unix_test.go new file mode 100644 index 00000000..c40ad3ee --- /dev/null +++ b/integration-cli/docker_cli_update_unix_test.go @@ -0,0 +1,212 @@ +// +build !windows + +package main + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/engine-api/types" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestUpdateRunningContainer(c *check.C) { + testRequires(c, DaemonIsLinux) + testRequires(c, memoryLimitSupport) + + name := "test-update-container" + dockerCmd(c, "run", "-d", "--name", name, "-m", "300M", "busybox", "top") + dockerCmd(c, "update", "-m", "500M", name) + + c.Assert(inspectField(c, name, "HostConfig.Memory"), checker.Equals, "524288000") + + file := "/sys/fs/cgroup/memory/memory.limit_in_bytes" + out, _ := dockerCmd(c, "exec", name, "cat", file) + c.Assert(strings.TrimSpace(out), checker.Equals, "524288000") +} + +func (s *DockerSuite) TestUpdateRunningContainerWithRestart(c *check.C) { + testRequires(c, DaemonIsLinux) + testRequires(c, memoryLimitSupport) + + name := "test-update-container" + dockerCmd(c, "run", "-d", "--name", name, "-m", "300M", "busybox", "top") + dockerCmd(c, "update", "-m", "500M", name) + dockerCmd(c, "restart", name) + + c.Assert(inspectField(c, name, "HostConfig.Memory"), checker.Equals, "524288000") + + file := "/sys/fs/cgroup/memory/memory.limit_in_bytes" + out, _ := dockerCmd(c, "exec", name, "cat", file) + c.Assert(strings.TrimSpace(out), checker.Equals, "524288000") +} + +func (s *DockerSuite) TestUpdateStoppedContainer(c *check.C) { + testRequires(c, DaemonIsLinux) + testRequires(c, memoryLimitSupport) + + name := "test-update-container" + file := "/sys/fs/cgroup/memory/memory.limit_in_bytes" + dockerCmd(c, "run", "--name", name, "-m", "300M", "busybox", "cat", file) + dockerCmd(c, "update", "-m", "500M", name) + + c.Assert(inspectField(c, name, "HostConfig.Memory"), checker.Equals, "524288000") + + out, _ := dockerCmd(c, "start", "-a", name) + c.Assert(strings.TrimSpace(out), checker.Equals, "524288000") +} + +func (s *DockerSuite) TestUpdatePausedContainer(c *check.C) { + testRequires(c, DaemonIsLinux) + testRequires(c, cpuShare) + + name := "test-update-container" + dockerCmd(c, "run", "-d", "--name", name, "--cpu-shares", "1000", "busybox", "top") + dockerCmd(c, "pause", name) + dockerCmd(c, "update", "--cpu-shares", "500", name) + + c.Assert(inspectField(c, name, "HostConfig.CPUShares"), checker.Equals, "500") + + dockerCmd(c, "unpause", name) + file := "/sys/fs/cgroup/cpu/cpu.shares" + out, _ := dockerCmd(c, "exec", name, "cat", file) + c.Assert(strings.TrimSpace(out), checker.Equals, "500") +} + +func (s *DockerSuite) TestUpdateWithUntouchedFields(c *check.C) { + testRequires(c, DaemonIsLinux) + testRequires(c, memoryLimitSupport) + testRequires(c, cpuShare) + + name := "test-update-container" + dockerCmd(c, "run", "-d", "--name", name, "-m", "300M", "--cpu-shares", "800", "busybox", "top") + dockerCmd(c, "update", "-m", "500M", name) + + // Update memory and not touch cpus, `cpuset.cpus` should still have the old value + out := inspectField(c, name, "HostConfig.CPUShares") + c.Assert(out, check.Equals, "800") + + file := "/sys/fs/cgroup/cpu/cpu.shares" + out, _ = dockerCmd(c, "exec", name, "cat", file) + c.Assert(strings.TrimSpace(out), checker.Equals, "800") +} + +func (s *DockerSuite) TestUpdateContainerInvalidValue(c *check.C) { + testRequires(c, DaemonIsLinux) + testRequires(c, memoryLimitSupport) + + name := "test-update-container" + dockerCmd(c, "run", "-d", "--name", name, "-m", "300M", "busybox", "true") + out, _, err := dockerCmdWithError("update", "-m", "2M", name) + c.Assert(err, check.NotNil) + expected := "Minimum memory limit allowed is 4MB" + c.Assert(out, checker.Contains, expected) +} + +func (s *DockerSuite) TestUpdateContainerWithoutFlags(c *check.C) { + testRequires(c, DaemonIsLinux) + testRequires(c, memoryLimitSupport) + + name := "test-update-container" + dockerCmd(c, "run", "-d", "--name", name, "-m", "300M", "busybox", "true") + _, _, err := dockerCmdWithError("update", name) + c.Assert(err, check.NotNil) +} + +func (s *DockerSuite) TestUpdateKernelMemory(c *check.C) { + testRequires(c, DaemonIsLinux) + testRequires(c, kernelMemorySupport) + + name := "test-update-container" + dockerCmd(c, "run", "-d", "--name", name, "--kernel-memory", "50M", "busybox", "top") + _, _, err := dockerCmdWithError("update", "--kernel-memory", "100M", name) + // Update kernel memory to a running container is not allowed. + c.Assert(err, check.NotNil) + + // Update kernel memory to a running container with failure should not change HostConfig + c.Assert(inspectField(c, name, "HostConfig.KernelMemory"), checker.Equals, "52428800") + + dockerCmd(c, "stop", name) + dockerCmd(c, "update", "--kernel-memory", "100M", name) + dockerCmd(c, "start", name) + + c.Assert(inspectField(c, name, "HostConfig.KernelMemory"), checker.Equals, "104857600") + + file := "/sys/fs/cgroup/memory/memory.kmem.limit_in_bytes" + out, _ := dockerCmd(c, "exec", name, "cat", file) + c.Assert(strings.TrimSpace(out), checker.Equals, "104857600") +} + +func (s *DockerSuite) TestUpdateSwapMemoryOnly(c *check.C) { + testRequires(c, DaemonIsLinux) + testRequires(c, memoryLimitSupport) + testRequires(c, swapMemorySupport) + + name := "test-update-container" + dockerCmd(c, "run", "-d", "--name", name, "--memory", "300M", "--memory-swap", "500M", "busybox", "top") + dockerCmd(c, "update", "--memory-swap", "600M", name) + + c.Assert(inspectField(c, name, "HostConfig.MemorySwap"), checker.Equals, "629145600") + + file := "/sys/fs/cgroup/memory/memory.memsw.limit_in_bytes" + out, _ := dockerCmd(c, "exec", name, "cat", file) + c.Assert(strings.TrimSpace(out), checker.Equals, "629145600") +} + +func (s *DockerSuite) TestUpdateInvalidSwapMemory(c *check.C) { + testRequires(c, DaemonIsLinux) + testRequires(c, memoryLimitSupport) + testRequires(c, swapMemorySupport) + + name := "test-update-container" + dockerCmd(c, "run", "-d", "--name", name, "--memory", "300M", "--memory-swap", "500M", "busybox", "top") + _, _, err := dockerCmdWithError("update", "--memory-swap", "200M", name) + // Update invalid swap memory should fail. + // This will pass docker config validation, but failed at kernel validation + c.Assert(err, check.NotNil) + + // Update invalid swap memory with failure should not change HostConfig + c.Assert(inspectField(c, name, "HostConfig.Memory"), checker.Equals, "314572800") + c.Assert(inspectField(c, name, "HostConfig.MemorySwap"), checker.Equals, "524288000") + + dockerCmd(c, "update", "--memory-swap", "600M", name) + + c.Assert(inspectField(c, name, "HostConfig.MemorySwap"), checker.Equals, "629145600") + + file := "/sys/fs/cgroup/memory/memory.memsw.limit_in_bytes" + out, _ := dockerCmd(c, "exec", name, "cat", file) + c.Assert(strings.TrimSpace(out), checker.Equals, "629145600") +} + +func (s *DockerSuite) TestUpdateStats(c *check.C) { + testRequires(c, DaemonIsLinux) + testRequires(c, memoryLimitSupport) + testRequires(c, cpuCfsQuota) + name := "foo" + dockerCmd(c, "run", "-d", "-ti", "--name", name, "-m", "500m", "busybox") + + c.Assert(waitRun(name), checker.IsNil) + + getMemLimit := func(id string) uint64 { + resp, body, err := sockRequestRaw("GET", fmt.Sprintf("/containers/%s/stats?stream=false", id), nil, "") + c.Assert(err, checker.IsNil) + c.Assert(resp.Header.Get("Content-Type"), checker.Equals, "application/json") + + var v *types.Stats + err = json.NewDecoder(body).Decode(&v) + c.Assert(err, checker.IsNil) + body.Close() + + return v.MemoryStats.Limit + } + preMemLimit := getMemLimit(name) + + dockerCmd(c, "update", "--cpu-quota", "2000", name) + + curMemLimit := getMemLimit(name) + + c.Assert(preMemLimit, checker.Equals, curMemLimit) + +} diff --git a/integration-cli/docker_cli_userns_test.go b/integration-cli/docker_cli_userns_test.go new file mode 100644 index 00000000..f8b3f77b --- /dev/null +++ b/integration-cli/docker_cli_userns_test.go @@ -0,0 +1,86 @@ +// +build !windows + +package main + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/system" + "github.com/go-check/check" +) + +// user namespaces test: run daemon with remapped root setting +// 1. validate uid/gid maps are set properly +// 2. verify that files created are owned by remapped root +func (s *DockerDaemonSuite) TestDaemonUserNamespaceRootSetting(c *check.C) { + testRequires(c, DaemonIsLinux, SameHostDaemon, UserNamespaceInKernel) + + c.Assert(s.d.StartWithBusybox("--userns-remap", "default"), checker.IsNil) + + tmpDir, err := ioutil.TempDir("", "userns") + c.Assert(err, checker.IsNil) + + defer os.RemoveAll(tmpDir) + + // we need to find the uid and gid of the remapped root from the daemon's root dir info + uidgid := strings.Split(filepath.Base(s.d.root), ".") + c.Assert(uidgid, checker.HasLen, 2, check.Commentf("Should have gotten uid/gid strings from root dirname: %s", filepath.Base(s.d.root))) + uid, err := strconv.Atoi(uidgid[0]) + c.Assert(err, checker.IsNil, check.Commentf("Can't parse uid")) + gid, err := strconv.Atoi(uidgid[1]) + c.Assert(err, checker.IsNil, check.Commentf("Can't parse gid")) + + // writable by the remapped root UID/GID pair + c.Assert(os.Chown(tmpDir, uid, gid), checker.IsNil) + + out, err := s.d.Cmd("run", "-d", "--name", "userns", "-v", tmpDir+":/goofy", "busybox", "sh", "-c", "touch /goofy/testfile; top") + c.Assert(err, checker.IsNil, check.Commentf("Output: %s", out)) + user := s.findUser(c, "userns") + c.Assert(uidgid[0], checker.Equals, user) + + pid, err := s.d.Cmd("inspect", "--format='{{.State.Pid}}'", "userns") + c.Assert(err, checker.IsNil, check.Commentf("Could not inspect running container: out: %q", pid)) + // check the uid and gid maps for the PID to ensure root is remapped + // (cmd = cat /proc//uid_map | grep -E '0\s+9999\s+1') + out, rc1, err := runCommandPipelineWithOutput( + exec.Command("cat", "/proc/"+strings.TrimSpace(pid)+"/uid_map"), + exec.Command("grep", "-E", fmt.Sprintf("0[[:space:]]+%d[[:space:]]+", uid))) + c.Assert(rc1, checker.Equals, 0, check.Commentf("Didn't match uid_map: output: %s", out)) + + out, rc2, err := runCommandPipelineWithOutput( + exec.Command("cat", "/proc/"+strings.TrimSpace(pid)+"/gid_map"), + exec.Command("grep", "-E", fmt.Sprintf("0[[:space:]]+%d[[:space:]]+", gid))) + c.Assert(rc2, checker.Equals, 0, check.Commentf("Didn't match gid_map: output: %s", out)) + + // check that the touched file is owned by remapped uid:gid + stat, err := system.Stat(filepath.Join(tmpDir, "testfile")) + c.Assert(err, checker.IsNil) + c.Assert(stat.UID(), checker.Equals, uint32(uid), check.Commentf("Touched file not owned by remapped root UID")) + c.Assert(stat.GID(), checker.Equals, uint32(gid), check.Commentf("Touched file not owned by remapped root GID")) + + // use host usernamespace + out, err = s.d.Cmd("run", "-d", "--name", "userns_skip", "--userns", "host", "busybox", "sh", "-c", "touch /goofy/testfile; top") + c.Assert(err, checker.IsNil, check.Commentf("Output: %s", out)) + user = s.findUser(c, "userns_skip") + // userns are skipped, user is root + c.Assert(user, checker.Equals, "root") +} + +// findUser finds the uid or name of the user of the first process that runs in a container +func (s *DockerDaemonSuite) findUser(c *check.C, container string) string { + out, err := s.d.Cmd("top", container) + c.Assert(err, checker.IsNil, check.Commentf("Output: %s", out)) + rows := strings.Split(out, "\n") + if len(rows) < 2 { + // No process rows founds + c.FailNow() + } + return strings.Fields(rows[1])[0] +} diff --git a/integration-cli/docker_cli_v2_only_test.go b/integration-cli/docker_cli_v2_only_test.go new file mode 100644 index 00000000..889936a0 --- /dev/null +++ b/integration-cli/docker_cli_v2_only_test.go @@ -0,0 +1,125 @@ +package main + +import ( + "fmt" + "io/ioutil" + "net/http" + "os" + + "github.com/go-check/check" +) + +func makefile(contents string) (string, func(), error) { + cleanup := func() { + + } + + f, err := ioutil.TempFile(".", "tmp") + if err != nil { + return "", cleanup, err + } + err = ioutil.WriteFile(f.Name(), []byte(contents), os.ModePerm) + if err != nil { + return "", cleanup, err + } + + cleanup = func() { + err := os.Remove(f.Name()) + if err != nil { + fmt.Println("Error removing tmpfile") + } + } + return f.Name(), cleanup, nil + +} + +// TestV2Only ensures that a daemon in v2-only mode does not +// attempt to contact any v1 registry endpoints. +func (s *DockerRegistrySuite) TestV2Only(c *check.C) { + reg, err := newTestRegistry(c) + c.Assert(err, check.IsNil) + + reg.registerHandler("/v2/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(404) + }) + + reg.registerHandler("/v1/.*", func(w http.ResponseWriter, r *http.Request) { + c.Fatal("V1 registry contacted") + }) + + repoName := fmt.Sprintf("%s/busybox", reg.hostport) + + err = s.d.Start("--insecure-registry", reg.hostport, "--disable-legacy-registry=true") + c.Assert(err, check.IsNil) + + dockerfileName, cleanup, err := makefile(fmt.Sprintf("FROM %s/busybox", reg.hostport)) + c.Assert(err, check.IsNil, check.Commentf("Unable to create test dockerfile")) + defer cleanup() + + s.d.Cmd("build", "--file", dockerfileName, ".") + + s.d.Cmd("run", repoName) + s.d.Cmd("login", "-u", "richard", "-p", "testtest", "-e", "testuser@testdomain.com", reg.hostport) + s.d.Cmd("tag", "busybox", repoName) + s.d.Cmd("push", repoName) + s.d.Cmd("pull", repoName) +} + +// TestV1 starts a daemon in 'normal' mode +// and ensure v1 endpoints are hit for the following operations: +// login, push, pull, build & run +func (s *DockerRegistrySuite) TestV1(c *check.C) { + reg, err := newTestRegistry(c) + c.Assert(err, check.IsNil) + + v2Pings := 0 + reg.registerHandler("/v2/", func(w http.ResponseWriter, r *http.Request) { + v2Pings++ + // V2 ping 404 causes fallback to v1 + w.WriteHeader(404) + }) + + v1Pings := 0 + reg.registerHandler("/v1/_ping", func(w http.ResponseWriter, r *http.Request) { + v1Pings++ + }) + + v1Logins := 0 + reg.registerHandler("/v1/users/", func(w http.ResponseWriter, r *http.Request) { + v1Logins++ + }) + + v1Repo := 0 + reg.registerHandler("/v1/repositories/busybox/", func(w http.ResponseWriter, r *http.Request) { + v1Repo++ + }) + + reg.registerHandler("/v1/repositories/busybox/images", func(w http.ResponseWriter, r *http.Request) { + v1Repo++ + }) + + err = s.d.Start("--insecure-registry", reg.hostport, "--disable-legacy-registry=false") + c.Assert(err, check.IsNil) + + dockerfileName, cleanup, err := makefile(fmt.Sprintf("FROM %s/busybox", reg.hostport)) + c.Assert(err, check.IsNil, check.Commentf("Unable to create test dockerfile")) + defer cleanup() + + s.d.Cmd("build", "--file", dockerfileName, ".") + c.Assert(v1Repo, check.Equals, 1, check.Commentf("Expected v1 repository access after build")) + + repoName := fmt.Sprintf("%s/busybox", reg.hostport) + s.d.Cmd("run", repoName) + c.Assert(v1Repo, check.Equals, 2, check.Commentf("Expected v1 repository access after run")) + + s.d.Cmd("login", "-u", "richard", "-p", "testtest", reg.hostport) + c.Assert(v1Logins, check.Equals, 1, check.Commentf("Expected v1 login attempt")) + + s.d.Cmd("tag", "busybox", repoName) + s.d.Cmd("push", repoName) + + c.Assert(v1Repo, check.Equals, 2) + + s.d.Cmd("pull", repoName) + c.Assert(v1Repo, check.Equals, 3, check.Commentf("Expected v1 repository access after pull")) +} diff --git a/integration-cli/docker_cli_version_test.go b/integration-cli/docker_cli_version_test.go new file mode 100644 index 00000000..7672beb7 --- /dev/null +++ b/integration-cli/docker_cli_version_test.go @@ -0,0 +1,58 @@ +package main + +import ( + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +// ensure docker version works +func (s *DockerSuite) TestVersionEnsureSucceeds(c *check.C) { + out, _ := dockerCmd(c, "version") + stringsToCheck := map[string]int{ + "Client:": 1, + "Server:": 1, + " Version:": 2, + " API version:": 2, + " Go version:": 2, + " Git commit:": 2, + " OS/Arch:": 2, + " Built:": 2, + } + + for k, v := range stringsToCheck { + c.Assert(strings.Count(out, k), checker.Equals, v, check.Commentf("The count of %v in %s does not match excepted", k, out)) + } +} + +// ensure the Windows daemon return the correct platform string +func (s *DockerSuite) TestVersionPlatform_w(c *check.C) { + testRequires(c, DaemonIsWindows) + testVersionPlatform(c, "windows/amd64") +} + +// ensure the Linux daemon return the correct platform string +func (s *DockerSuite) TestVersionPlatform_l(c *check.C) { + testRequires(c, DaemonIsLinux) + testVersionPlatform(c, "linux") +} + +func testVersionPlatform(c *check.C, platform string) { + out, _ := dockerCmd(c, "version") + expected := "OS/Arch: " + platform + + split := strings.Split(out, "\n") + c.Assert(len(split) >= 14, checker.Equals, true, check.Commentf("got %d lines from version", len(split))) + + // Verify the second 'OS/Arch' matches the platform. Experimental has + // more lines of output than 'regular' + bFound := false + for i := 14; i < len(split); i++ { + if strings.Contains(split[i], expected) { + bFound = true + break + } + } + c.Assert(bFound, checker.Equals, true, check.Commentf("Could not find server '%s' in '%s'", expected, out)) +} diff --git a/integration-cli/docker_cli_volume_test.go b/integration-cli/docker_cli_volume_test.go new file mode 100644 index 00000000..a10316f6 --- /dev/null +++ b/integration-cli/docker_cli_volume_test.go @@ -0,0 +1,283 @@ +package main + +import ( + "os/exec" + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestVolumeCliCreate(c *check.C) { + dockerCmd(c, "volume", "create") + + _, err := runCommand(exec.Command(dockerBinary, "volume", "create", "-d", "nosuchdriver")) + c.Assert(err, check.Not(check.IsNil)) + + out, _ := dockerCmd(c, "volume", "create", "--name=test") + name := strings.TrimSpace(out) + c.Assert(name, check.Equals, "test") +} + +func (s *DockerSuite) TestVolumeCliCreateOptionConflict(c *check.C) { + dockerCmd(c, "volume", "create", "--name=test") + out, _, err := dockerCmdWithError("volume", "create", "--name", "test", "--driver", "nosuchdriver") + c.Assert(err, check.NotNil, check.Commentf("volume create exception name already in use with another driver")) + c.Assert(out, checker.Contains, "A volume named test already exists") + + out, _ = dockerCmd(c, "volume", "inspect", "--format='{{ .Driver }}'", "test") + _, _, err = dockerCmdWithError("volume", "create", "--name", "test", "--driver", strings.TrimSpace(out)) + c.Assert(err, check.IsNil) +} + +func (s *DockerSuite) TestVolumeCliInspect(c *check.C) { + c.Assert( + exec.Command(dockerBinary, "volume", "inspect", "doesntexist").Run(), + check.Not(check.IsNil), + check.Commentf("volume inspect should error on non-existent volume"), + ) + + out, _ := dockerCmd(c, "volume", "create") + name := strings.TrimSpace(out) + out, _ = dockerCmd(c, "volume", "inspect", "--format='{{ .Name }}'", name) + c.Assert(strings.TrimSpace(out), check.Equals, name) + + dockerCmd(c, "volume", "create", "--name", "test") + out, _ = dockerCmd(c, "volume", "inspect", "--format='{{ .Name }}'", "test") + c.Assert(strings.TrimSpace(out), check.Equals, "test") +} + +func (s *DockerSuite) TestVolumeCliInspectMulti(c *check.C) { + dockerCmd(c, "volume", "create", "--name", "test1") + dockerCmd(c, "volume", "create", "--name", "test2") + dockerCmd(c, "volume", "create", "--name", "not-shown") + + out, _, err := dockerCmdWithError("volume", "inspect", "--format='{{ .Name }}'", "test1", "test2", "doesntexist", "not-shown") + c.Assert(err, checker.NotNil) + outArr := strings.Split(strings.TrimSpace(out), "\n") + c.Assert(len(outArr), check.Equals, 3, check.Commentf("\n%s", out)) + + c.Assert(out, checker.Contains, "test1") + c.Assert(out, checker.Contains, "test2") + c.Assert(out, checker.Contains, "Error: No such volume: doesntexist") + c.Assert(out, checker.Not(checker.Contains), "not-shown") +} + +func (s *DockerSuite) TestVolumeCliLs(c *check.C) { + prefix, _ := getPrefixAndSlashFromDaemonPlatform() + out, _ := dockerCmd(c, "volume", "create", "--name", "aaa") + + dockerCmd(c, "volume", "create", "--name", "test") + + dockerCmd(c, "volume", "create", "--name", "soo") + dockerCmd(c, "run", "-v", "soo:"+prefix+"/foo", "busybox", "ls", "/") + + out, _ = dockerCmd(c, "volume", "ls") + outArr := strings.Split(strings.TrimSpace(out), "\n") + c.Assert(len(outArr), check.Equals, 4, check.Commentf("\n%s", out)) + + assertVolList(c, out, []string{"aaa", "soo", "test"}) +} + +// assertVolList checks volume retrieved with ls command +// equals to expected volume list +// note: out should be `volume ls [option]` result +func assertVolList(c *check.C, out string, expectVols []string) { + lines := strings.Split(out, "\n") + var volList []string + for _, line := range lines[1 : len(lines)-1] { + volFields := strings.Fields(line) + // wrap all volume name in volList + volList = append(volList, volFields[1]) + } + + // volume ls should contains all expected volumes + c.Assert(volList, checker.DeepEquals, expectVols) +} + +func (s *DockerSuite) TestVolumeCliLsFilterDangling(c *check.C) { + prefix, _ := getPrefixAndSlashFromDaemonPlatform() + dockerCmd(c, "volume", "create", "--name", "testnotinuse1") + dockerCmd(c, "volume", "create", "--name", "testisinuse1") + dockerCmd(c, "volume", "create", "--name", "testisinuse2") + + // Make sure both "created" (but not started), and started + // containers are included in reference counting + dockerCmd(c, "run", "--name", "volume-test1", "-v", "testisinuse1:"+prefix+"/foo", "busybox", "true") + dockerCmd(c, "create", "--name", "volume-test2", "-v", "testisinuse2:"+prefix+"/foo", "busybox", "true") + + out, _ := dockerCmd(c, "volume", "ls") + + // No filter, all volumes should show + c.Assert(out, checker.Contains, "testnotinuse1\n", check.Commentf("expected volume 'testnotinuse1' in output")) + c.Assert(out, checker.Contains, "testisinuse1\n", check.Commentf("expected volume 'testisinuse1' in output")) + c.Assert(out, checker.Contains, "testisinuse2\n", check.Commentf("expected volume 'testisinuse2' in output")) + + out, _ = dockerCmd(c, "volume", "ls", "--filter", "dangling=false") + + // Explicitly disabling dangling + c.Assert(out, check.Not(checker.Contains), "testnotinuse1\n", check.Commentf("expected volume 'testnotinuse1' in output")) + c.Assert(out, checker.Contains, "testisinuse1\n", check.Commentf("expected volume 'testisinuse1' in output")) + c.Assert(out, checker.Contains, "testisinuse2\n", check.Commentf("expected volume 'testisinuse2' in output")) + + out, _ = dockerCmd(c, "volume", "ls", "--filter", "dangling=true") + + // Filter "dangling" volumes; only "dangling" (unused) volumes should be in the output + c.Assert(out, checker.Contains, "testnotinuse1\n", check.Commentf("expected volume 'testnotinuse1' in output")) + c.Assert(out, check.Not(checker.Contains), "testisinuse1\n", check.Commentf("volume 'testisinuse1' in output, but not expected")) + c.Assert(out, check.Not(checker.Contains), "testisinuse2\n", check.Commentf("volume 'testisinuse2' in output, but not expected")) + + out, _ = dockerCmd(c, "volume", "ls", "--filter", "dangling=1") + // Filter "dangling" volumes; only "dangling" (unused) volumes should be in the output, dangling also accept 1 + c.Assert(out, checker.Contains, "testnotinuse1\n", check.Commentf("expected volume 'testnotinuse1' in output")) + c.Assert(out, check.Not(checker.Contains), "testisinuse1\n", check.Commentf("volume 'testisinuse1' in output, but not expected")) + c.Assert(out, check.Not(checker.Contains), "testisinuse2\n", check.Commentf("volume 'testisinuse2' in output, but not expected")) + + out, _ = dockerCmd(c, "volume", "ls", "--filter", "dangling=0") + // dangling=0 is same as dangling=false case + c.Assert(out, check.Not(checker.Contains), "testnotinuse1\n", check.Commentf("expected volume 'testnotinuse1' in output")) + c.Assert(out, checker.Contains, "testisinuse1\n", check.Commentf("expected volume 'testisinuse1' in output")) + c.Assert(out, checker.Contains, "testisinuse2\n", check.Commentf("expected volume 'testisinuse2' in output")) +} + +func (s *DockerSuite) TestVolumeCliLsErrorWithInvalidFilterName(c *check.C) { + out, _, err := dockerCmdWithError("volume", "ls", "-f", "FOO=123") + c.Assert(err, checker.NotNil) + c.Assert(out, checker.Contains, "Invalid filter") +} + +func (s *DockerSuite) TestVolumeCliLsWithIncorrectFilterValue(c *check.C) { + out, _, err := dockerCmdWithError("volume", "ls", "-f", "dangling=invalid") + c.Assert(err, check.NotNil) + c.Assert(out, checker.Contains, "Invalid filter") +} + +func (s *DockerSuite) TestVolumeCliRm(c *check.C) { + prefix, _ := getPrefixAndSlashFromDaemonPlatform() + out, _ := dockerCmd(c, "volume", "create") + id := strings.TrimSpace(out) + + dockerCmd(c, "volume", "create", "--name", "test") + dockerCmd(c, "volume", "rm", id) + dockerCmd(c, "volume", "rm", "test") + + out, _ = dockerCmd(c, "volume", "ls") + outArr := strings.Split(strings.TrimSpace(out), "\n") + c.Assert(len(outArr), check.Equals, 1, check.Commentf("%s\n", out)) + + volumeID := "testing" + dockerCmd(c, "run", "-v", volumeID+":"+prefix+"/foo", "--name=test", "busybox", "sh", "-c", "echo hello > /foo/bar") + out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "volume", "rm", "testing")) + c.Assert( + err, + check.Not(check.IsNil), + check.Commentf("Should not be able to remove volume that is in use by a container\n%s", out)) + + out, _ = dockerCmd(c, "run", "--volumes-from=test", "--name=test2", "busybox", "sh", "-c", "cat /foo/bar") + c.Assert(strings.TrimSpace(out), check.Equals, "hello") + dockerCmd(c, "rm", "-fv", "test2") + dockerCmd(c, "volume", "inspect", volumeID) + dockerCmd(c, "rm", "-f", "test") + + out, _ = dockerCmd(c, "run", "--name=test2", "-v", volumeID+":"+prefix+"/foo", "busybox", "sh", "-c", "cat /foo/bar") + c.Assert(strings.TrimSpace(out), check.Equals, "hello", check.Commentf("volume data was removed")) + dockerCmd(c, "rm", "test2") + + dockerCmd(c, "volume", "rm", volumeID) + c.Assert( + exec.Command("volume", "rm", "doesntexist").Run(), + check.Not(check.IsNil), + check.Commentf("volume rm should fail with non-existent volume"), + ) +} + +func (s *DockerSuite) TestVolumeCliNoArgs(c *check.C) { + out, _ := dockerCmd(c, "volume") + // no args should produce the cmd usage output + usage := "Usage: docker volume [OPTIONS] [COMMAND]" + c.Assert(out, checker.Contains, usage) + + // invalid arg should error and show the command usage on stderr + _, stderr, _, err := runCommandWithStdoutStderr(exec.Command(dockerBinary, "volume", "somearg")) + c.Assert(err, check.NotNil, check.Commentf(stderr)) + c.Assert(stderr, checker.Contains, usage) + + // invalid flag should error and show the flag error and cmd usage + _, stderr, _, err = runCommandWithStdoutStderr(exec.Command(dockerBinary, "volume", "--no-such-flag")) + c.Assert(err, check.NotNil, check.Commentf(stderr)) + c.Assert(stderr, checker.Contains, usage) + c.Assert(stderr, checker.Contains, "flag provided but not defined: --no-such-flag") +} + +func (s *DockerSuite) TestVolumeCliInspectTmplError(c *check.C) { + out, _ := dockerCmd(c, "volume", "create") + name := strings.TrimSpace(out) + + out, exitCode, err := dockerCmdWithError("volume", "inspect", "--format='{{ .FooBar }}'", name) + c.Assert(err, checker.NotNil, check.Commentf("Output: %s", out)) + c.Assert(exitCode, checker.Equals, 1, check.Commentf("Output: %s", out)) + c.Assert(out, checker.Contains, "Template parsing error") +} + +func (s *DockerSuite) TestVolumeCliCreateWithOpts(c *check.C) { + testRequires(c, DaemonIsLinux) + + dockerCmd(c, "volume", "create", "-d", "local", "--name", "test", "--opt=type=tmpfs", "--opt=device=tmpfs", "--opt=o=size=1m,uid=1000") + out, _ := dockerCmd(c, "run", "-v", "test:/foo", "busybox", "mount") + + mounts := strings.Split(out, "\n") + var found bool + for _, m := range mounts { + if strings.Contains(m, "/foo") { + found = true + info := strings.Fields(m) + // tmpfs on type tmpfs (rw,relatime,size=1024k,uid=1000) + c.Assert(info[0], checker.Equals, "tmpfs") + c.Assert(info[2], checker.Equals, "/foo") + c.Assert(info[4], checker.Equals, "tmpfs") + c.Assert(info[5], checker.Contains, "uid=1000") + c.Assert(info[5], checker.Contains, "size=1024k") + } + } + c.Assert(found, checker.Equals, true) +} + +func (s *DockerSuite) TestVolumeCliCreateLabel(c *check.C) { + testVol := "testvolcreatelabel" + testLabel := "foo" + testValue := "bar" + + out, _, err := dockerCmdWithError("volume", "create", "--label", testLabel+"="+testValue, "--name", testVol) + c.Assert(err, check.IsNil) + + out, _ = dockerCmd(c, "volume", "inspect", "--format='{{ .Labels."+testLabel+" }}'", testVol) + c.Assert(strings.TrimSpace(out), check.Equals, testValue) +} + +func (s *DockerSuite) TestVolumeCliCreateLabelMultiple(c *check.C) { + testVol := "testvolcreatelabel" + + testLabels := map[string]string{ + "foo": "bar", + "baz": "foo", + } + + args := []string{ + "volume", + "create", + "--name", + testVol, + } + + for k, v := range testLabels { + args = append(args, "--label", k+"="+v) + } + + out, _, err := dockerCmdWithError(args...) + c.Assert(err, check.IsNil) + + for k, v := range testLabels { + out, _ = dockerCmd(c, "volume", "inspect", "--format='{{ .Labels."+k+" }}'", testVol) + c.Assert(strings.TrimSpace(out), check.Equals, v) + } +} diff --git a/integration-cli/docker_cli_wait_test.go b/integration-cli/docker_cli_wait_test.go new file mode 100644 index 00000000..29933975 --- /dev/null +++ b/integration-cli/docker_cli_wait_test.go @@ -0,0 +1,95 @@ +package main + +import ( + "bytes" + "os/exec" + "strings" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +// non-blocking wait with 0 exit code +func (s *DockerSuite) TestWaitNonBlockedExitZero(c *check.C) { + out, _ := dockerCmd(c, "run", "-d", "busybox", "sh", "-c", "true") + containerID := strings.TrimSpace(out) + + err := waitInspect(containerID, "{{.State.Running}}", "false", 30*time.Second) + c.Assert(err, checker.IsNil) //Container should have stopped by now + + out, _ = dockerCmd(c, "wait", containerID) + c.Assert(strings.TrimSpace(out), checker.Equals, "0", check.Commentf("failed to set up container, %v", out)) + +} + +// blocking wait with 0 exit code +func (s *DockerSuite) TestWaitBlockedExitZero(c *check.C) { + // Windows busybox does not support trap in this way, not sleep with sub-second + // granularity. It will always exit 0x40010004. + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "trap 'exit 0' TERM; while true; do usleep 10; done") + containerID := strings.TrimSpace(out) + + c.Assert(waitRun(containerID), checker.IsNil) + + chWait := make(chan string) + go func() { + out, _, _ := runCommandWithOutput(exec.Command(dockerBinary, "wait", containerID)) + chWait <- out + }() + + time.Sleep(100 * time.Millisecond) + dockerCmd(c, "stop", containerID) + + select { + case status := <-chWait: + c.Assert(strings.TrimSpace(status), checker.Equals, "0", check.Commentf("expected exit 0, got %s", status)) + case <-time.After(2 * time.Second): + c.Fatal("timeout waiting for `docker wait` to exit") + } + +} + +// non-blocking wait with random exit code +func (s *DockerSuite) TestWaitNonBlockedExitRandom(c *check.C) { + out, _ := dockerCmd(c, "run", "-d", "busybox", "sh", "-c", "exit 99") + containerID := strings.TrimSpace(out) + + err := waitInspect(containerID, "{{.State.Running}}", "false", 30*time.Second) + c.Assert(err, checker.IsNil) //Container should have stopped by now + out, _ = dockerCmd(c, "wait", containerID) + c.Assert(strings.TrimSpace(out), checker.Equals, "99", check.Commentf("failed to set up container, %v", out)) + +} + +// blocking wait with random exit code +func (s *DockerSuite) TestWaitBlockedExitRandom(c *check.C) { + // Cannot run on Windows as trap in Windows busybox does not support trap in this way. + testRequires(c, DaemonIsLinux) + out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "trap 'exit 99' TERM; while true; do usleep 10; done") + containerID := strings.TrimSpace(out) + c.Assert(waitRun(containerID), checker.IsNil) + + chWait := make(chan error) + waitCmd := exec.Command(dockerBinary, "wait", containerID) + waitCmdOut := bytes.NewBuffer(nil) + waitCmd.Stdout = waitCmdOut + c.Assert(waitCmd.Start(), checker.IsNil) + go func() { + chWait <- waitCmd.Wait() + }() + + dockerCmd(c, "stop", containerID) + + select { + case err := <-chWait: + c.Assert(err, checker.IsNil) + status, err := waitCmdOut.ReadString('\n') + c.Assert(err, checker.IsNil) + c.Assert(strings.TrimSpace(status), checker.Equals, "99", check.Commentf("expected exit 99, got %s", status)) + case <-time.After(2 * time.Second): + waitCmd.Process.Kill() + c.Fatal("timeout waiting for `docker wait` to exit") + } +} diff --git a/integration-cli/docker_experimental_network_test.go b/integration-cli/docker_experimental_network_test.go new file mode 100644 index 00000000..f33dbd1c --- /dev/null +++ b/integration-cli/docker_experimental_network_test.go @@ -0,0 +1,591 @@ +// +build experimental + +package main + +import ( + "os/exec" + "strings" + "time" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/parsers/kernel" + "github.com/go-check/check" +) + +var ( + MacvlanKernelSupport = testRequirement{ + func() bool { + const macvlanKernelVer = 3 // minimum macvlan kernel support + const macvlanMajorVer = 9 // minimum macvlan major kernel support + kv, err := kernel.GetKernelVersion() + if err != nil { + return false + } + // ensure Kernel version is >= v3.9 for macvlan support + if kv.Kernel < macvlanKernelVer || (kv.Kernel == macvlanKernelVer && kv.Major < macvlanMajorVer) { + return false + } + return true + }, + "kernel version failed to meet the minimum macvlan kernel requirement of 3.9", + } + IpvlanKernelSupport = testRequirement{ + func() bool { + const ipvlanKernelVer = 4 // minimum ipvlan kernel support + const ipvlanMajorVer = 2 // minimum ipvlan major kernel support + kv, err := kernel.GetKernelVersion() + if err != nil { + return false + } + // ensure Kernel version is >= v4.2 for ipvlan support + if kv.Kernel < ipvlanKernelVer || (kv.Kernel == ipvlanKernelVer && kv.Major < ipvlanMajorVer) { + return false + } + return true + }, + "kernel version failed to meet the minimum ipvlan kernel requirement of 4.0.0", + } +) + +func (s *DockerNetworkSuite) TestDockerNetworkMacvlanPersistance(c *check.C) { + // verify the driver automatically provisions the 802.1q link (dm-dummy0.60) + testRequires(c, DaemonIsLinux, MacvlanKernelSupport, NotUserNamespace, NotArm) + // master dummy interface 'dm' abbreviation represents 'docker macvlan' + master := "dm-dummy0" + // simulate the master link the vlan tagged subinterface parent link will use + out, err := createMasterDummy(c, master) + c.Assert(err, check.IsNil, check.Commentf(out)) + // create a network specifying the desired sub-interface name + dockerCmd(c, "network", "create", "--driver=macvlan", "-o", "parent=dm-dummy0.60", "dm-persist") + assertNwIsAvailable(c, "dm-persist") + // Restart docker daemon to test the config has persisted to disk + s.d.Restart() + // verify network is recreated from persistence + assertNwIsAvailable(c, "dm-persist") + // cleanup the master interface that also collects the slave dev + deleteInterface(c, "dm-dummy0") +} + +func (s *DockerNetworkSuite) TestDockerNetworkIpvlanPersistance(c *check.C) { + // verify the driver automatically provisions the 802.1q link (di-dummy0.70) + testRequires(c, DaemonIsLinux, IpvlanKernelSupport, NotUserNamespace, NotArm) + // master dummy interface 'di' notation represent 'docker ipvlan' + master := "di-dummy0" + // simulate the master link the vlan tagged subinterface parent link will use + out, err := createMasterDummy(c, master) + c.Assert(err, check.IsNil, check.Commentf(out)) + // create a network specifying the desired sub-interface name + dockerCmd(c, "network", "create", "--driver=ipvlan", "-o", "parent=di-dummy0.70", "di-persist") + assertNwIsAvailable(c, "di-persist") + // Restart docker daemon to test the config has persisted to disk + s.d.Restart() + // verify network is recreated from persistence + assertNwIsAvailable(c, "di-persist") + // cleanup the master interface that also collects the slave dev + deleteInterface(c, "di-dummy0") +} + +func (s *DockerNetworkSuite) TestDockerNetworkMacvlanSubIntCreate(c *check.C) { + // verify the driver automatically provisions the 802.1q link (dm-dummy0.50) + testRequires(c, DaemonIsLinux, MacvlanKernelSupport, NotUserNamespace, NotArm) + // master dummy interface 'dm' abbreviation represents 'docker macvlan' + master := "dm-dummy0" + // simulate the master link the vlan tagged subinterface parent link will use + out, err := createMasterDummy(c, master) + c.Assert(err, check.IsNil, check.Commentf(out)) + // create a network specifying the desired sub-interface name + dockerCmd(c, "network", "create", "--driver=macvlan", "-o", "parent=dm-dummy0.50", "dm-subinterface") + assertNwIsAvailable(c, "dm-subinterface") + // cleanup the master interface which also collects the slave dev + deleteInterface(c, "dm-dummy0") +} + +func (s *DockerNetworkSuite) TestDockerNetworkIpvlanSubIntCreate(c *check.C) { + // verify the driver automatically provisions the 802.1q link (di-dummy0.50) + testRequires(c, DaemonIsLinux, IpvlanKernelSupport, NotUserNamespace, NotArm) + // master dummy interface 'dm' abbreviation represents 'docker ipvlan' + master := "di-dummy0" + // simulate the master link the vlan tagged subinterface parent link will use + out, err := createMasterDummy(c, master) + c.Assert(err, check.IsNil, check.Commentf(out)) + // create a network specifying the desired sub-interface name + dockerCmd(c, "network", "create", "--driver=ipvlan", "-o", "parent=di-dummy0.60", "di-subinterface") + assertNwIsAvailable(c, "di-subinterface") + // cleanup the master interface which also collects the slave dev + deleteInterface(c, "di-dummy0") +} + +func (s *DockerNetworkSuite) TestDockerNetworkMacvlanOverlapParent(c *check.C) { + // verify the same parent interface cannot be used if already in use by an existing network + testRequires(c, DaemonIsLinux, MacvlanKernelSupport, NotUserNamespace, NotArm) + // master dummy interface 'dm' abbreviation represents 'docker macvlan' + master := "dm-dummy0" + out, err := createMasterDummy(c, master) + c.Assert(err, check.IsNil, check.Commentf(out)) + out, err = createVlanInterface(c, master, "dm-dummy0.40", "40") + c.Assert(err, check.IsNil, check.Commentf(out)) + // create a network using an existing parent interface + dockerCmd(c, "network", "create", "--driver=macvlan", "-o", "parent=dm-dummy0.40", "dm-subinterface") + assertNwIsAvailable(c, "dm-subinterface") + // attempt to create another network using the same parent iface that should fail + out, _, err = dockerCmdWithError("network", "create", "--driver=macvlan", "-o", "parent=dm-dummy0.40", "dm-parent-net-overlap") + // verify that the overlap returns an error + c.Assert(err, check.NotNil) + // cleanup the master interface which also collects the slave dev + deleteInterface(c, "dm-dummy0") +} + +func (s *DockerNetworkSuite) TestDockerNetworkIpvlanOverlapParent(c *check.C) { + // verify the same parent interface cannot be used if already in use by an existing network + testRequires(c, DaemonIsLinux, IpvlanKernelSupport, NotUserNamespace, NotArm) + // master dummy interface 'dm' abbreviation represents 'docker ipvlan' + master := "di-dummy0" + out, err := createMasterDummy(c, master) + c.Assert(err, check.IsNil, check.Commentf(out)) + out, err = createVlanInterface(c, master, "di-dummy0.30", "30") + c.Assert(err, check.IsNil, check.Commentf(out)) + // create a network using an existing parent interface + dockerCmd(c, "network", "create", "--driver=ipvlan", "-o", "parent=di-dummy0.30", "di-subinterface") + assertNwIsAvailable(c, "di-subinterface") + // attempt to create another network using the same parent iface that should fail + out, _, err = dockerCmdWithError("network", "create", "--driver=ipvlan", "-o", "parent=di-dummy0.30", "di-parent-net-overlap") + // verify that the overlap returns an error + c.Assert(err, check.NotNil) + // cleanup the master interface which also collects the slave dev + deleteInterface(c, "di-dummy0") +} + +func (s *DockerNetworkSuite) TestDockerNetworkMacvlanMultiSubnet(c *check.C) { + // create a dual stack multi-subnet Macvlan bridge mode network and validate connectivity between four containers, two on each subnet + testRequires(c, DaemonIsLinux, IPv6, MacvlanKernelSupport, NotUserNamespace, NotArm) + dockerCmd(c, "network", "create", "--driver=macvlan", "--ipv6", "--subnet=172.28.100.0/24", "--subnet=172.28.102.0/24", "--gateway=172.28.102.254", + "--subnet=2001:db8:abc2::/64", "--subnet=2001:db8:abc4::/64", "--gateway=2001:db8:abc4::254", "dualstackbridge") + // Ensure the network was created + assertNwIsAvailable(c, "dualstackbridge") + // start dual stack containers and verify the user specified --ip and --ip6 addresses on subnets 172.28.100.0/24 and 2001:db8:abc2::/64 + dockerCmd(c, "run", "-d", "--net=dualstackbridge", "--name=first", "--ip", "172.28.100.20", "--ip6", "2001:db8:abc2::20", "busybox", "top") + dockerCmd(c, "run", "-d", "--net=dualstackbridge", "--name=second", "--ip", "172.28.100.21", "--ip6", "2001:db8:abc2::21", "busybox", "top") + + // Inspect and store the v4 address from specified container on the network dualstackbridge + ip := inspectField(c, "first", "NetworkSettings.Networks.dualstackbridge.IPAddress") + // Inspect and store the v6 address from specified container on the network dualstackbridge + ip6 := inspectField(c, "first", "NetworkSettings.Networks.dualstackbridge.GlobalIPv6Address") + + // verify ipv4 connectivity to the explicit --ipv address second to first + _, _, err := dockerCmdWithError("exec", "second", "ping", "-c", "1", strings.TrimSpace(ip)) + c.Assert(err, check.IsNil) + // verify ipv6 connectivity to the explicit --ipv6 address second to first + c.Skip("Temporarily skipping while invesitigating sporadic v6 CI issues") + _, _, err = dockerCmdWithError("exec", "second", "ping6", "-c", "1", strings.TrimSpace(ip6)) + c.Assert(err, check.IsNil) + + // start dual stack containers and verify the user specified --ip and --ip6 addresses on subnets 172.28.102.0/24 and 2001:db8:abc4::/64 + dockerCmd(c, "run", "-d", "--net=dualstackbridge", "--name=third", "--ip", "172.28.102.20", "--ip6", "2001:db8:abc4::20", "busybox", "top") + dockerCmd(c, "run", "-d", "--net=dualstackbridge", "--name=fourth", "--ip", "172.28.102.21", "--ip6", "2001:db8:abc4::21", "busybox", "top") + + // Inspect and store the v4 address from specified container on the network dualstackbridge + ip = inspectField(c, "third", "NetworkSettings.Networks.dualstackbridge.IPAddress") + // Inspect and store the v6 address from specified container on the network dualstackbridge + ip6 = inspectField(c, "third", "NetworkSettings.Networks.dualstackbridge.GlobalIPv6Address") + + // verify ipv4 connectivity to the explicit --ipv address from third to fourth + _, _, err = dockerCmdWithError("exec", "fourth", "ping", "-c", "1", strings.TrimSpace(ip)) + c.Assert(err, check.IsNil) + // verify ipv6 connectivity to the explicit --ipv6 address from third to fourth + _, _, err = dockerCmdWithError("exec", "fourth", "ping6", "-c", "1", strings.TrimSpace(ip6)) + c.Assert(err, check.IsNil) + + // Inspect the v4 gateway to ensure the proper default GW was assigned + ip4gw := inspectField(c, "first", "NetworkSettings.Networks.dualstackbridge.Gateway") + c.Assert(strings.TrimSpace(ip4gw), check.Equals, "172.28.100.1") + // Inspect the v6 gateway to ensure the proper default GW was assigned + ip6gw := inspectField(c, "first", "NetworkSettings.Networks.dualstackbridge.IPv6Gateway") + c.Assert(strings.TrimSpace(ip6gw), check.Equals, "2001:db8:abc2::1") + + // Inspect the v4 gateway to ensure the proper explicitly assigned default GW was assigned + ip4gw = inspectField(c, "third", "NetworkSettings.Networks.dualstackbridge.Gateway") + c.Assert(strings.TrimSpace(ip4gw), check.Equals, "172.28.102.254") + // Inspect the v6 gateway to ensure the proper explicitly assigned default GW was assigned + ip6gw = inspectField(c, "third", "NetworkSettings.Networks.dualstackbridge.IPv6Gateway") + c.Assert(strings.TrimSpace(ip6gw), check.Equals, "2001:db8:abc4::254") +} + +func (s *DockerNetworkSuite) TestDockerNetworkIpvlanL2MultiSubnet(c *check.C) { + // create a dual stack multi-subnet Ipvlan L2 network and validate connectivity within the subnets, two on each subnet + testRequires(c, DaemonIsLinux, IPv6, IpvlanKernelSupport, NotUserNamespace, NotArm) + dockerCmd(c, "network", "create", "--driver=ipvlan", "--ipv6", "--subnet=172.28.200.0/24", "--subnet=172.28.202.0/24", "--gateway=172.28.202.254", + "--subnet=2001:db8:abc8::/64", "--subnet=2001:db8:abc6::/64", "--gateway=2001:db8:abc6::254", "dualstackl2") + // Ensure the network was created + assertNwIsAvailable(c, "dualstackl2") + // start dual stack containers and verify the user specified --ip and --ip6 addresses on subnets 172.28.200.0/24 and 2001:db8:abc8::/64 + dockerCmd(c, "run", "-d", "--net=dualstackl2", "--name=first", "--ip", "172.28.200.20", "--ip6", "2001:db8:abc8::20", "busybox", "top") + dockerCmd(c, "run", "-d", "--net=dualstackl2", "--name=second", "--ip", "172.28.200.21", "--ip6", "2001:db8:abc8::21", "busybox", "top") + + // Inspect and store the v4 address from specified container on the network dualstackl2 + ip := inspectField(c, "first", "NetworkSettings.Networks.dualstackl2.IPAddress") + // Inspect and store the v6 address from specified container on the network dualstackl2 + ip6 := inspectField(c, "first", "NetworkSettings.Networks.dualstackl2.GlobalIPv6Address") + + // verify ipv4 connectivity to the explicit --ipv address second to first + _, _, err := dockerCmdWithError("exec", "second", "ping", "-c", "1", strings.TrimSpace(ip)) + c.Assert(err, check.IsNil) + // verify ipv6 connectivity to the explicit --ipv6 address second to first + _, _, err = dockerCmdWithError("exec", "second", "ping6", "-c", "1", strings.TrimSpace(ip6)) + c.Assert(err, check.IsNil) + + // start dual stack containers and verify the user specified --ip and --ip6 addresses on subnets 172.28.202.0/24 and 2001:db8:abc6::/64 + dockerCmd(c, "run", "-d", "--net=dualstackl2", "--name=third", "--ip", "172.28.202.20", "--ip6", "2001:db8:abc6::20", "busybox", "top") + dockerCmd(c, "run", "-d", "--net=dualstackl2", "--name=fourth", "--ip", "172.28.202.21", "--ip6", "2001:db8:abc6::21", "busybox", "top") + + // Inspect and store the v4 address from specified container on the network dualstackl2 + ip = inspectField(c, "third", "NetworkSettings.Networks.dualstackl2.IPAddress") + // Inspect and store the v6 address from specified container on the network dualstackl2 + ip6 = inspectField(c, "third", "NetworkSettings.Networks.dualstackl2.GlobalIPv6Address") + + // verify ipv4 connectivity to the explicit --ipv address from third to fourth + _, _, err = dockerCmdWithError("exec", "fourth", "ping", "-c", "1", strings.TrimSpace(ip)) + c.Assert(err, check.IsNil) + // verify ipv6 connectivity to the explicit --ipv6 address from third to fourth + _, _, err = dockerCmdWithError("exec", "fourth", "ping6", "-c", "1", strings.TrimSpace(ip6)) + c.Assert(err, check.IsNil) + + // Inspect the v4 gateway to ensure the proper default GW was assigned + ip4gw := inspectField(c, "first", "NetworkSettings.Networks.dualstackl2.Gateway") + c.Assert(strings.TrimSpace(ip4gw), check.Equals, "172.28.200.1") + // Inspect the v6 gateway to ensure the proper default GW was assigned + ip6gw := inspectField(c, "first", "NetworkSettings.Networks.dualstackl2.IPv6Gateway") + c.Assert(strings.TrimSpace(ip6gw), check.Equals, "2001:db8:abc8::1") + + // Inspect the v4 gateway to ensure the proper explicitly assigned default GW was assigned + ip4gw = inspectField(c, "third", "NetworkSettings.Networks.dualstackl2.Gateway") + c.Assert(strings.TrimSpace(ip4gw), check.Equals, "172.28.202.254") + // Inspect the v6 gateway to ensure the proper explicitly assigned default GW was assigned + ip6gw = inspectField(c, "third", "NetworkSettings.Networks.dualstackl2.IPv6Gateway") + c.Assert(strings.TrimSpace(ip6gw), check.Equals, "2001:db8:abc6::254") +} + +func (s *DockerNetworkSuite) TestDockerNetworkIpvlanL3MultiSubnet(c *check.C) { + // create a dual stack multi-subnet Ipvlan L3 network and validate connectivity between all four containers per L3 mode + testRequires(c, DaemonIsLinux, IPv6, IpvlanKernelSupport, NotUserNamespace, NotArm, IPv6) + dockerCmd(c, "network", "create", "--driver=ipvlan", "--ipv6", "--subnet=172.28.10.0/24", "--subnet=172.28.12.0/24", "--gateway=172.28.12.254", + "--subnet=2001:db8:abc9::/64", "--subnet=2001:db8:abc7::/64", "--gateway=2001:db8:abc7::254", "-o", "ipvlan_mode=l3", "dualstackl3") + // Ensure the network was created + assertNwIsAvailable(c, "dualstackl3") + + // start dual stack containers and verify the user specified --ip and --ip6 addresses on subnets 172.28.10.0/24 and 2001:db8:abc9::/64 + dockerCmd(c, "run", "-d", "--net=dualstackl3", "--name=first", "--ip", "172.28.10.20", "--ip6", "2001:db8:abc9::20", "busybox", "top") + dockerCmd(c, "run", "-d", "--net=dualstackl3", "--name=second", "--ip", "172.28.10.21", "--ip6", "2001:db8:abc9::21", "busybox", "top") + + // Inspect and store the v4 address from specified container on the network dualstackl3 + ip := inspectField(c, "first", "NetworkSettings.Networks.dualstackl3.IPAddress") + // Inspect and store the v6 address from specified container on the network dualstackl3 + ip6 := inspectField(c, "first", "NetworkSettings.Networks.dualstackl3.GlobalIPv6Address") + + // verify ipv4 connectivity to the explicit --ipv address second to first + _, _, err := dockerCmdWithError("exec", "second", "ping", "-c", "1", strings.TrimSpace(ip)) + c.Assert(err, check.IsNil) + // verify ipv6 connectivity to the explicit --ipv6 address second to first + _, _, err = dockerCmdWithError("exec", "second", "ping6", "-c", "1", strings.TrimSpace(ip6)) + c.Assert(err, check.IsNil) + + // start dual stack containers and verify the user specified --ip and --ip6 addresses on subnets 172.28.12.0/24 and 2001:db8:abc7::/64 + dockerCmd(c, "run", "-d", "--net=dualstackl3", "--name=third", "--ip", "172.28.12.20", "--ip6", "2001:db8:abc7::20", "busybox", "top") + dockerCmd(c, "run", "-d", "--net=dualstackl3", "--name=fourth", "--ip", "172.28.12.21", "--ip6", "2001:db8:abc7::21", "busybox", "top") + + // Inspect and store the v4 address from specified container on the network dualstackl3 + ip = inspectField(c, "third", "NetworkSettings.Networks.dualstackl3.IPAddress") + // Inspect and store the v6 address from specified container on the network dualstackl3 + ip6 = inspectField(c, "third", "NetworkSettings.Networks.dualstackl3.GlobalIPv6Address") + + // verify ipv4 connectivity to the explicit --ipv address from third to fourth + _, _, err = dockerCmdWithError("exec", "fourth", "ping", "-c", "1", strings.TrimSpace(ip)) + c.Assert(err, check.IsNil) + // verify ipv6 connectivity to the explicit --ipv6 address from third to fourth + _, _, err = dockerCmdWithError("exec", "fourth", "ping6", "-c", "1", strings.TrimSpace(ip6)) + c.Assert(err, check.IsNil) + + // Inspect and store the v4 address from specified container on the network dualstackl3 + ip = inspectField(c, "second", "NetworkSettings.Networks.dualstackl3.IPAddress") + // Inspect and store the v6 address from specified container on the network dualstackl3 + ip6 = inspectField(c, "second", "NetworkSettings.Networks.dualstackl3.GlobalIPv6Address") + + // Verify connectivity across disparate subnets which is unique to L3 mode only + _, _, err = dockerCmdWithError("exec", "third", "ping", "-c", "1", strings.TrimSpace(ip)) + c.Assert(err, check.IsNil) + _, _, err = dockerCmdWithError("exec", "third", "ping6", "-c", "1", strings.TrimSpace(ip6)) + c.Assert(err, check.IsNil) + + // Inspect the v4 gateway to ensure no next hop is assigned in L3 mode + ip4gw := inspectField(c, "first", "NetworkSettings.Networks.dualstackl3.Gateway") + c.Assert(strings.TrimSpace(ip4gw), check.Equals, "") + // Inspect the v6 gateway to ensure the explicitly specified default GW is ignored per L3 mode enabled + ip6gw := inspectField(c, "third", "NetworkSettings.Networks.dualstackl3.IPv6Gateway") + c.Assert(strings.TrimSpace(ip6gw), check.Equals, "") +} + +func (s *DockerNetworkSuite) TestDockerNetworkIpvlanAddressing(c *check.C) { + // Ensure the default gateways, next-hops and default dev devices are properly set + testRequires(c, DaemonIsLinux, IPv6, IpvlanKernelSupport, NotUserNamespace, NotArm) + dockerCmd(c, "network", "create", "--driver=macvlan", "--ipv6", "--subnet=172.28.130.0/24", + "--subnet=2001:db8:abca::/64", "--gateway=2001:db8:abca::254", "-o", "macvlan_mode=bridge", "dualstackbridge") + assertNwIsAvailable(c, "dualstackbridge") + dockerCmd(c, "run", "-d", "--net=dualstackbridge", "--name=first", "busybox", "top") + // Validate macvlan bridge mode defaults gateway sets the default IPAM next-hop inferred from the subnet + out, _, err := dockerCmdWithError("exec", "first", "ip", "route") + c.Assert(err, check.IsNil) + c.Assert(out, checker.Contains, "default via 172.28.130.1 dev eth0") + // Validate macvlan bridge mode sets the v6 gateway to the user specified default gateway/next-hop + out, _, err = dockerCmdWithError("exec", "first", "ip", "-6", "route") + c.Assert(err, check.IsNil) + c.Assert(out, checker.Contains, "default via 2001:db8:abca::254 dev eth0") + + // Verify ipvlan l2 mode sets the proper default gateway routes via netlink + // for either an explicitly set route by the user or inferred via default IPAM + dockerCmd(c, "network", "create", "--driver=ipvlan", "--ipv6", "--subnet=172.28.140.0/24", "--gateway=172.28.140.254", + "--subnet=2001:db8:abcb::/64", "-o", "ipvlan_mode=l2", "dualstackl2") + assertNwIsAvailable(c, "dualstackl2") + dockerCmd(c, "run", "-d", "--net=dualstackl2", "--name=second", "busybox", "top") + // Validate ipvlan l2 mode defaults gateway sets the default IPAM next-hop inferred from the subnet + out, _, err = dockerCmdWithError("exec", "second", "ip", "route") + c.Assert(err, check.IsNil) + c.Assert(out, checker.Contains, "default via 172.28.140.254 dev eth0") + // Validate ipvlan l2 mode sets the v6 gateway to the user specified default gateway/next-hop + out, _, err = dockerCmdWithError("exec", "second", "ip", "-6", "route") + c.Assert(err, check.IsNil) + c.Assert(out, checker.Contains, "default via 2001:db8:abcb::1 dev eth0") + + // Validate ipvlan l3 mode sets the v4 gateway to dev eth0 and disregards any explicit or inferred next-hops + dockerCmd(c, "network", "create", "--driver=ipvlan", "--ipv6", "--subnet=172.28.160.0/24", "--gateway=172.28.160.254", + "--subnet=2001:db8:abcd::/64", "--gateway=2001:db8:abcd::254", "-o", "ipvlan_mode=l3", "dualstackl3") + assertNwIsAvailable(c, "dualstackl3") + dockerCmd(c, "run", "-d", "--net=dualstackl3", "--name=third", "busybox", "top") + // Validate ipvlan l3 mode sets the v4 gateway to dev eth0 and disregards any explicit or inferred next-hops + out, _, err = dockerCmdWithError("exec", "third", "ip", "route") + c.Assert(err, check.IsNil) + c.Assert(out, checker.Contains, "default dev eth0") + // Validate ipvlan l3 mode sets the v6 gateway to dev eth0 and disregards any explicit or inferred next-hops + out, _, err = dockerCmdWithError("exec", "third", "ip", "-6", "route") + c.Assert(err, check.IsNil) + c.Assert(out, checker.Contains, "default dev eth0") +} + +func (s *DockerSuite) TestDockerNetworkMacVlanBridgeNilParent(c *check.C) { + // macvlan bridge mode - dummy parent interface is provisioned dynamically + testRequires(c, DaemonIsLinux, MacvlanKernelSupport, NotUserNamespace, NotArm) + dockerCmd(c, "network", "create", "--driver=macvlan", "dm-nil-parent") + assertNwIsAvailable(c, "dm-nil-parent") + + // start two containers on the same subnet + dockerCmd(c, "run", "-d", "--net=dm-nil-parent", "--name=first", "busybox", "top") + c.Assert(waitRun("first"), check.IsNil) + dockerCmd(c, "run", "-d", "--net=dm-nil-parent", "--name=second", "busybox", "top") + c.Assert(waitRun("second"), check.IsNil) + + // intra-network communications should succeed + _, _, err := dockerCmdWithError("exec", "second", "ping", "-c", "1", "first") + c.Assert(err, check.IsNil) +} + +func (s *DockerSuite) TestDockerNetworkMacVlanBridgeInternalMode(c *check.C) { + // macvlan bridge mode --internal containers can communicate inside the network but not externally + testRequires(c, DaemonIsLinux, MacvlanKernelSupport, NotUserNamespace, NotArm) + dockerCmd(c, "network", "create", "--driver=macvlan", "--internal", "dm-internal") + assertNwIsAvailable(c, "dm-internal") + nr := getNetworkResource(c, "dm-internal") + c.Assert(nr.Internal, checker.True) + + // start two containers on the same subnet + dockerCmd(c, "run", "-d", "--net=dm-internal", "--name=first", "busybox", "top") + c.Assert(waitRun("first"), check.IsNil) + dockerCmd(c, "run", "-d", "--net=dm-internal", "--name=second", "busybox", "top") + c.Assert(waitRun("second"), check.IsNil) + + // access outside of the network should fail + _, _, err := dockerCmdWithTimeout(time.Second, "exec", "first", "ping", "-c", "1", "-w", "1", "8.8.8.8") + c.Assert(err, check.NotNil) + // intra-network communications should succeed + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "first") + c.Assert(err, check.IsNil) +} + +func (s *DockerSuite) TestDockerNetworkIpvlanL2NilParent(c *check.C) { + // ipvlan l2 mode - dummy parent interface is provisioned dynamically + testRequires(c, DaemonIsLinux, IpvlanKernelSupport, NotUserNamespace, NotArm) + dockerCmd(c, "network", "create", "--driver=ipvlan", "di-nil-parent") + assertNwIsAvailable(c, "di-nil-parent") + + // start two containers on the same subnet + dockerCmd(c, "run", "-d", "--net=di-nil-parent", "--name=first", "busybox", "top") + c.Assert(waitRun("first"), check.IsNil) + dockerCmd(c, "run", "-d", "--net=di-nil-parent", "--name=second", "busybox", "top") + c.Assert(waitRun("second"), check.IsNil) + + // intra-network communications should succeed + _, _, err := dockerCmdWithError("exec", "second", "ping", "-c", "1", "first") + c.Assert(err, check.IsNil) +} + +func (s *DockerSuite) TestDockerNetworkIpvlanL2InternalMode(c *check.C) { + // ipvlan l2 mode --internal containers can communicate inside the network but not externally + testRequires(c, DaemonIsLinux, IpvlanKernelSupport, NotUserNamespace, NotArm) + dockerCmd(c, "network", "create", "--driver=ipvlan", "--internal", "di-internal") + assertNwIsAvailable(c, "di-internal") + nr := getNetworkResource(c, "di-internal") + c.Assert(nr.Internal, checker.True) + + // start two containers on the same subnet + dockerCmd(c, "run", "-d", "--net=di-internal", "--name=first", "busybox", "top") + c.Assert(waitRun("first"), check.IsNil) + dockerCmd(c, "run", "-d", "--net=di-internal", "--name=second", "busybox", "top") + c.Assert(waitRun("second"), check.IsNil) + + // access outside of the network should fail + _, _, err := dockerCmdWithTimeout(time.Second, "exec", "first", "ping", "-c", "1", "-w", "1", "8.8.8.8") + c.Assert(err, check.NotNil) + // intra-network communications should succeed + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "first") + c.Assert(err, check.IsNil) +} + +func (s *DockerSuite) TestDockerNetworkIpvlanL3NilParent(c *check.C) { + // ipvlan l3 mode - dummy parent interface is provisioned dynamically + testRequires(c, DaemonIsLinux, IpvlanKernelSupport, NotUserNamespace, NotArm) + dockerCmd(c, "network", "create", "--driver=ipvlan", "--subnet=172.28.230.0/24", + "--subnet=172.28.220.0/24", "-o", "ipvlan_mode=l3", "di-nil-parent-l3") + assertNwIsAvailable(c, "di-nil-parent-l3") + + // start two containers on separate subnets + dockerCmd(c, "run", "-d", "--ip=172.28.220.10", "--net=di-nil-parent-l3", "--name=first", "busybox", "top") + c.Assert(waitRun("first"), check.IsNil) + dockerCmd(c, "run", "-d", "--ip=172.28.230.10", "--net=di-nil-parent-l3", "--name=second", "busybox", "top") + c.Assert(waitRun("second"), check.IsNil) + + // intra-network communications should succeed + _, _, err := dockerCmdWithError("exec", "second", "ping", "-c", "1", "first") + c.Assert(err, check.IsNil) +} + +func (s *DockerSuite) TestDockerNetworkIpvlanL3InternalMode(c *check.C) { + // ipvlan l3 mode --internal containers can communicate inside the network but not externally + testRequires(c, DaemonIsLinux, IpvlanKernelSupport, NotUserNamespace, NotArm) + dockerCmd(c, "network", "create", "--driver=ipvlan", "--subnet=172.28.230.0/24", + "--subnet=172.28.220.0/24", "-o", "ipvlan_mode=l3", "--internal", "di-internal-l3") + assertNwIsAvailable(c, "di-internal-l3") + nr := getNetworkResource(c, "di-internal-l3") + c.Assert(nr.Internal, checker.True) + + // start two containers on separate subnets + dockerCmd(c, "run", "-d", "--ip=172.28.220.10", "--net=di-internal-l3", "--name=first", "busybox", "top") + c.Assert(waitRun("first"), check.IsNil) + dockerCmd(c, "run", "-d", "--ip=172.28.230.10", "--net=di-internal-l3", "--name=second", "busybox", "top") + c.Assert(waitRun("second"), check.IsNil) + + // access outside of the network should fail + _, _, err := dockerCmdWithTimeout(time.Second, "exec", "first", "ping", "-c", "1", "-w", "1", "8.8.8.8") + c.Assert(err, check.NotNil) + // intra-network communications should succeed + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "first") + c.Assert(err, check.IsNil) +} + +func (s *DockerSuite) TestDockerNetworkMacVlanExistingParent(c *check.C) { + // macvlan bridge mode - empty parent interface containers can reach each other internally but not externally + testRequires(c, DaemonIsLinux, MacvlanKernelSupport, NotUserNamespace, NotArm) + netName := "dm-parent-exists" + out, err := createMasterDummy(c, "dm-dummy0") + //out, err := createVlanInterface(c, "dm-parent", "dm-slave", "macvlan", "bridge") + c.Assert(err, check.IsNil, check.Commentf(out)) + // create a network using an existing parent interface + dockerCmd(c, "network", "create", "--driver=macvlan", "-o", "parent=dm-dummy0", netName) + assertNwIsAvailable(c, netName) + // delete the network while preserving the parent link + dockerCmd(c, "network", "rm", netName) + assertNwNotAvailable(c, netName) + // verify the network delete did not delete the predefined link + out, err = linkExists(c, "dm-dummy0") + c.Assert(err, check.IsNil, check.Commentf(out)) + deleteInterface(c, "dm-dummy0") + c.Assert(err, check.IsNil, check.Commentf(out)) +} + +func (s *DockerSuite) TestDockerNetworkMacVlanSubinterface(c *check.C) { + // macvlan bridge mode - empty parent interface containers can reach each other internally but not externally + testRequires(c, DaemonIsLinux, MacvlanKernelSupport, NotUserNamespace, NotArm) + netName := "dm-subinterface" + out, err := createMasterDummy(c, "dm-dummy0") + c.Assert(err, check.IsNil, check.Commentf(out)) + out, err = createVlanInterface(c, "dm-dummy0", "dm-dummy0.20", "20") + c.Assert(err, check.IsNil, check.Commentf(out)) + // create a network using an existing parent interface + dockerCmd(c, "network", "create", "--driver=macvlan", "-o", "parent=dm-dummy0.20", netName) + assertNwIsAvailable(c, netName) + + // start containers on 802.1q tagged '-o parent' sub-interface + dockerCmd(c, "run", "-d", "--net=dm-subinterface", "--name=first", "busybox", "top") + c.Assert(waitRun("first"), check.IsNil) + dockerCmd(c, "run", "-d", "--net=dm-subinterface", "--name=second", "busybox", "top") + c.Assert(waitRun("second"), check.IsNil) + // verify containers can communicate + _, _, err = dockerCmdWithError("exec", "second", "ping", "-c", "1", "first") + c.Assert(err, check.IsNil) + + // remove the containers + dockerCmd(c, "rm", "-f", "first") + dockerCmd(c, "rm", "-f", "second") + // delete the network while preserving the parent link + dockerCmd(c, "network", "rm", netName) + assertNwNotAvailable(c, netName) + // verify the network delete did not delete the predefined sub-interface + out, err = linkExists(c, "dm-dummy0.20") + c.Assert(err, check.IsNil, check.Commentf(out)) + // delete the parent interface which also collects the slave + deleteInterface(c, "dm-dummy0") + c.Assert(err, check.IsNil, check.Commentf(out)) +} + +func createMasterDummy(c *check.C, master string) (string, error) { + // ip link add type dummy + args := []string{"link", "add", master, "type", "dummy"} + ipLinkCmd := exec.Command("ip", args...) + out, _, err := runCommandWithOutput(ipLinkCmd) + if err != nil { + return out, err + } + // ip link set dummy_name up + args = []string{"link", "set", master, "up"} + ipLinkCmd = exec.Command("ip", args...) + out, _, err = runCommandWithOutput(ipLinkCmd) + if err != nil { + return out, err + } + return out, err +} + +func createVlanInterface(c *check.C, master, slave, id string) (string, error) { + // ip link add link name . type vlan id + args := []string{"link", "add", "link", master, "name", slave, "type", "vlan", "id", id} + ipLinkCmd := exec.Command("ip", args...) + out, _, err := runCommandWithOutput(ipLinkCmd) + if err != nil { + return out, err + } + // ip link set up + args = []string{"link", "set", slave, "up"} + ipLinkCmd = exec.Command("ip", args...) + out, _, err = runCommandWithOutput(ipLinkCmd) + if err != nil { + return out, err + } + return out, err +} + +func linkExists(c *check.C, master string) (string, error) { + // verify the specified link exists, ip link show + args := []string{"link", "show", master} + ipLinkCmd := exec.Command("ip", args...) + out, _, err := runCommandWithOutput(ipLinkCmd) + if err != nil { + return out, err + } + return out, err +} diff --git a/integration-cli/docker_hub_pull_suite_test.go b/integration-cli/docker_hub_pull_suite_test.go new file mode 100644 index 00000000..6aa93469 --- /dev/null +++ b/integration-cli/docker_hub_pull_suite_test.go @@ -0,0 +1,90 @@ +package main + +import ( + "os/exec" + "runtime" + "strings" + + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +func init() { + // FIXME. Temporarily turning this off for Windows as GH16039 was breaking + // Windows to Linux CI @icecrime + if runtime.GOOS != "windows" { + check.Suite(newDockerHubPullSuite()) + } +} + +// DockerHubPullSuite provides a isolated daemon that doesn't have all the +// images that are baked into our 'global' test environment daemon (e.g., +// busybox, httpserver, ...). +// +// We use it for push/pull tests where we want to start fresh, and measure the +// relative impact of each individual operation. As part of this suite, all +// images are removed after each test. +type DockerHubPullSuite struct { + d *Daemon + ds *DockerSuite +} + +// newDockerHubPullSuite returns a new instance of a DockerHubPullSuite. +func newDockerHubPullSuite() *DockerHubPullSuite { + return &DockerHubPullSuite{ + ds: &DockerSuite{}, + } +} + +// SetUpSuite starts the suite daemon. +func (s *DockerHubPullSuite) SetUpSuite(c *check.C) { + testRequires(c, DaemonIsLinux) + s.d = NewDaemon(c) + err := s.d.Start() + c.Assert(err, checker.IsNil, check.Commentf("starting push/pull test daemon: %v", err)) +} + +// TearDownSuite stops the suite daemon. +func (s *DockerHubPullSuite) TearDownSuite(c *check.C) { + if s.d != nil { + err := s.d.Stop() + c.Assert(err, checker.IsNil, check.Commentf("stopping push/pull test daemon: %v", err)) + } +} + +// SetUpTest declares that all tests of this suite require network. +func (s *DockerHubPullSuite) SetUpTest(c *check.C) { + testRequires(c, Network) +} + +// TearDownTest removes all images from the suite daemon. +func (s *DockerHubPullSuite) TearDownTest(c *check.C) { + out := s.Cmd(c, "images", "-aq") + images := strings.Split(out, "\n") + images = append([]string{"-f"}, images...) + s.d.Cmd("rmi", images...) + s.ds.TearDownTest(c) +} + +// Cmd executes a command against the suite daemon and returns the combined +// output. The function fails the test when the command returns an error. +func (s *DockerHubPullSuite) Cmd(c *check.C, name string, arg ...string) string { + out, err := s.CmdWithError(name, arg...) + c.Assert(err, checker.IsNil, check.Commentf("%q failed with errors: %s, %v", strings.Join(arg, " "), out, err)) + return out +} + +// CmdWithError executes a command against the suite daemon and returns the +// combined output as well as any error. +func (s *DockerHubPullSuite) CmdWithError(name string, arg ...string) (string, error) { + c := s.MakeCmd(name, arg...) + b, err := c.CombinedOutput() + return string(b), err +} + +// MakeCmd returns a exec.Cmd command to run against the suite daemon. +func (s *DockerHubPullSuite) MakeCmd(name string, arg ...string) *exec.Cmd { + args := []string{"--host", s.d.sock(), name} + args = append(args, arg...) + return exec.Command(dockerBinary, args...) +} diff --git a/integration-cli/docker_test_vars.go b/integration-cli/docker_test_vars.go new file mode 100644 index 00000000..b6113992 --- /dev/null +++ b/integration-cli/docker_test_vars.go @@ -0,0 +1,129 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + + "github.com/docker/docker/pkg/reexec" +) + +var ( + // the docker binary to use + dockerBinary = "docker" + + // path to containerd's ctr binary + ctrBinary = "docker-containerd-ctr" + + // the private registry image to use for tests involving the registry + registryImageName = "registry" + + // the private registry to use for tests + privateRegistryURL = "127.0.0.1:5000" + + // TODO Windows CI. These are incorrect and need fixing into + // platform specific pieces. + runtimePath = "/var/run/docker" + + workingDirectory string + + // isLocalDaemon is true if the daemon under test is on the same + // host as the CLI. + isLocalDaemon bool + + // daemonPlatform is held globally so that tests can make intelligent + // decisions on how to configure themselves according to the platform + // of the daemon. This is initialized in docker_utils by sending + // a version call to the daemon and examining the response header. + daemonPlatform string + + // windowsDaemonKV is used on Windows to distinguish between different + // versions. This is necessary to enable certain tests based on whether + // the platform supports it. For example, Windows Server 2016 TP3 does + // not support volumes, but TP4 does. + windowsDaemonKV int + + // daemonDefaultImage is the name of the default image to use when running + // tests. This is platform dependent. + daemonDefaultImage string + + // For a local daemon on Linux, these values will be used for testing + // user namespace support as the standard graph path(s) will be + // appended with the root remapped uid.gid prefix + dockerBasePath string + volumesConfigPath string + containerStoragePath string + + // daemonStorageDriver is held globally so that tests can know the storage + // driver of the daemon. This is initialized in docker_utils by sending + // a version call to the daemon and examining the response header. + daemonStorageDriver string +) + +const ( + // WindowsBaseImage is the name of the base image for Windows testing + WindowsBaseImage = "windowsservercore" + + // DefaultImage is the name of the base image for the majority of tests that + // are run across suites + DefaultImage = "busybox" +) + +func init() { + reexec.Init() + if dockerBin := os.Getenv("DOCKER_BINARY"); dockerBin != "" { + dockerBinary = dockerBin + } + var err error + dockerBinary, err = exec.LookPath(dockerBinary) + if err != nil { + fmt.Printf("ERROR: couldn't resolve full path to the Docker binary (%v)", err) + os.Exit(1) + } + if registryImage := os.Getenv("REGISTRY_IMAGE"); registryImage != "" { + registryImageName = registryImage + } + if registry := os.Getenv("REGISTRY_URL"); registry != "" { + privateRegistryURL = registry + } + workingDirectory, _ = os.Getwd() + + // Deterministically working out the environment in which CI is running + // to evaluate whether the daemon is local or remote is not possible through + // a build tag. + // + // For example Windows to Linux CI under Jenkins tests the 64-bit + // Windows binary build with the daemon build tag, but calls a remote + // Linux daemon. + // + // We can't just say if Windows then assume the daemon is local as at + // some point, we will be testing the Windows CLI against a Windows daemon. + // + // Similarly, it will be perfectly valid to also run CLI tests from + // a Linux CLI (built with the daemon tag) against a Windows daemon. + if len(os.Getenv("DOCKER_REMOTE_DAEMON")) > 0 { + isLocalDaemon = false + } else { + isLocalDaemon = true + } + + // TODO Windows CI. This are incorrect and need fixing into + // platform specific pieces. + // This is only used for a tests with local daemon true (Linux-only today) + // default is "/var/lib/docker", but we'll try and ask the + // /info endpoint for the specific root dir + dockerBasePath = "/var/lib/docker" + type Info struct { + DockerRootDir string + } + var i Info + status, b, err := sockRequest("GET", "/info", nil) + if err == nil && status == 200 { + if err = json.Unmarshal(b, &i); err == nil { + dockerBasePath = i.DockerRootDir + } + } + volumesConfigPath = dockerBasePath + "/volumes" + containerStoragePath = dockerBasePath + "/containers" +} diff --git a/integration-cli/docker_utils.go b/integration-cli/docker_utils.go new file mode 100644 index 00000000..31982871 --- /dev/null +++ b/integration-cli/docker_utils.go @@ -0,0 +1,1495 @@ +package main + +import ( + "bufio" + "bytes" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "net/http/httptest" + "net/http/httputil" + "net/url" + "os" + "os/exec" + "path" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/httputils" + "github.com/docker/docker/pkg/integration" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/stringutils" + "github.com/docker/engine-api/types" + "github.com/docker/go-connections/tlsconfig" + "github.com/docker/go-units" + "github.com/go-check/check" +) + +func init() { + cmd := exec.Command(dockerBinary, "images") + cmd.Env = appendBaseEnv(true) + out, err := cmd.CombinedOutput() + if err != nil { + panic(fmt.Errorf("err=%v\nout=%s\n", err, out)) + } + lines := strings.Split(string(out), "\n")[1:] + for _, l := range lines { + if l == "" { + continue + } + fields := strings.Fields(l) + imgTag := fields[0] + ":" + fields[1] + // just for case if we have dangling images in tested daemon + if imgTag != ":" { + protectedImages[imgTag] = struct{}{} + } + } + + // Obtain the daemon platform so that it can be used by tests to make + // intelligent decisions about how to configure themselves, and validate + // that the target platform is valid. + res, _, err := sockRequestRaw("GET", "/version", nil, "application/json") + if err != nil || res == nil || (res != nil && res.StatusCode != http.StatusOK) { + panic(fmt.Errorf("Init failed to get version: %v. Res=%v", err.Error(), res)) + } + svrHeader, _ := httputils.ParseServerHeader(res.Header.Get("Server")) + daemonPlatform = svrHeader.OS + if daemonPlatform != "linux" && daemonPlatform != "windows" { + panic("Cannot run tests against platform: " + daemonPlatform) + } + + // On Windows, extract out the version as we need to make selective + // decisions during integration testing as and when features are implemented. + if daemonPlatform == "windows" { + if body, err := ioutil.ReadAll(res.Body); err == nil { + var server types.Version + if err := json.Unmarshal(body, &server); err == nil { + // eg in "10.0 10550 (10550.1000.amd64fre.branch.date-time)" we want 10550 + windowsDaemonKV, _ = strconv.Atoi(strings.Split(server.KernelVersion, " ")[1]) + } + } + } + + // Now we know the daemon platform, can set paths used by tests. + _, body, err := sockRequest("GET", "/info", nil) + if err != nil { + panic(err) + } + + var info types.Info + err = json.Unmarshal(body, &info) + daemonStorageDriver = info.Driver + dockerBasePath = info.DockerRootDir + volumesConfigPath = filepath.Join(dockerBasePath, "volumes") + containerStoragePath = filepath.Join(dockerBasePath, "containers") + // Make sure in context of daemon, not the local platform. Note we can't + // use filepath.FromSlash or ToSlash here as they are a no-op on Unix. + if daemonPlatform == "windows" { + volumesConfigPath = strings.Replace(volumesConfigPath, `/`, `\`, -1) + containerStoragePath = strings.Replace(containerStoragePath, `/`, `\`, -1) + } else { + volumesConfigPath = strings.Replace(volumesConfigPath, `\`, `/`, -1) + containerStoragePath = strings.Replace(containerStoragePath, `\`, `/`, -1) + } +} + +func convertBasesize(basesizeBytes int64) (int64, error) { + basesize := units.HumanSize(float64(basesizeBytes)) + basesize = strings.Trim(basesize, " ")[:len(basesize)-3] + basesizeFloat, err := strconv.ParseFloat(strings.Trim(basesize, " "), 64) + if err != nil { + return 0, err + } + return int64(basesizeFloat) * 1024 * 1024 * 1024, nil +} + +func daemonHost() string { + daemonURLStr := "unix://" + opts.DefaultUnixSocket + if daemonHostVar := os.Getenv("DOCKER_HOST"); daemonHostVar != "" { + daemonURLStr = daemonHostVar + } + return daemonURLStr +} + +func getTLSConfig() (*tls.Config, error) { + dockerCertPath := os.Getenv("DOCKER_CERT_PATH") + + if dockerCertPath == "" { + return nil, fmt.Errorf("DOCKER_TLS_VERIFY specified, but no DOCKER_CERT_PATH environment variable") + } + + option := &tlsconfig.Options{ + CAFile: filepath.Join(dockerCertPath, "ca.pem"), + CertFile: filepath.Join(dockerCertPath, "cert.pem"), + KeyFile: filepath.Join(dockerCertPath, "key.pem"), + } + tlsConfig, err := tlsconfig.Client(*option) + if err != nil { + return nil, err + } + + return tlsConfig, nil +} + +func sockConn(timeout time.Duration) (net.Conn, error) { + daemon := daemonHost() + daemonURL, err := url.Parse(daemon) + if err != nil { + return nil, fmt.Errorf("could not parse url %q: %v", daemon, err) + } + + var c net.Conn + switch daemonURL.Scheme { + case "npipe": + return npipeDial(daemonURL.Path, timeout) + case "unix": + return net.DialTimeout(daemonURL.Scheme, daemonURL.Path, timeout) + case "tcp": + if os.Getenv("DOCKER_TLS_VERIFY") != "" { + // Setup the socket TLS configuration. + tlsConfig, err := getTLSConfig() + if err != nil { + return nil, err + } + dialer := &net.Dialer{Timeout: timeout} + return tls.DialWithDialer(dialer, daemonURL.Scheme, daemonURL.Host, tlsConfig) + } + return net.DialTimeout(daemonURL.Scheme, daemonURL.Host, timeout) + default: + return c, fmt.Errorf("unknown scheme %v (%s)", daemonURL.Scheme, daemon) + } +} + +func sockRequest(method, endpoint string, data interface{}) (int, []byte, error) { + jsonData := bytes.NewBuffer(nil) + if err := json.NewEncoder(jsonData).Encode(data); err != nil { + return -1, nil, err + } + + res, body, err := sockRequestRaw(method, endpoint, jsonData, "application/json") + if err != nil { + return -1, nil, err + } + b, err := readBody(body) + return res.StatusCode, b, err +} + +func sockRequestRaw(method, endpoint string, data io.Reader, ct string) (*http.Response, io.ReadCloser, error) { + req, client, err := newRequestClient(method, endpoint, data, ct) + if err != nil { + return nil, nil, err + } + + resp, err := client.Do(req) + if err != nil { + client.Close() + return nil, nil, err + } + body := ioutils.NewReadCloserWrapper(resp.Body, func() error { + defer resp.Body.Close() + return client.Close() + }) + + return resp, body, nil +} + +func sockRequestHijack(method, endpoint string, data io.Reader, ct string) (net.Conn, *bufio.Reader, error) { + req, client, err := newRequestClient(method, endpoint, data, ct) + if err != nil { + return nil, nil, err + } + + client.Do(req) + conn, br := client.Hijack() + return conn, br, nil +} + +func newRequestClient(method, endpoint string, data io.Reader, ct string) (*http.Request, *httputil.ClientConn, error) { + c, err := sockConn(time.Duration(10 * time.Second)) + if err != nil { + return nil, nil, fmt.Errorf("could not dial docker daemon: %v", err) + } + + client := httputil.NewClientConn(c, nil) + + req, err := http.NewRequest(method, endpoint, data) + if err != nil { + client.Close() + return nil, nil, fmt.Errorf("could not create new request: %v", err) + } + + if ct != "" { + req.Header.Set("Content-Type", ct) + } + return req, client, nil +} + +func readBody(b io.ReadCloser) ([]byte, error) { + defer b.Close() + return ioutil.ReadAll(b) +} + +func deleteContainer(container string) error { + container = strings.TrimSpace(strings.Replace(container, "\n", " ", -1)) + rmArgs := strings.Split(fmt.Sprintf("rm -fv %v", container), " ") + exitCode, err := runCommand(exec.Command(dockerBinary, rmArgs...)) + // set error manually if not set + if exitCode != 0 && err == nil { + err = fmt.Errorf("failed to remove container: `docker rm` exit is non-zero") + } + + return err +} + +func getAllContainers() (string, error) { + getContainersCmd := exec.Command(dockerBinary, "ps", "-q", "-a") + out, exitCode, err := runCommandWithOutput(getContainersCmd) + if exitCode != 0 && err == nil { + err = fmt.Errorf("failed to get a list of containers: %v\n", out) + } + + return out, err +} + +func deleteAllContainers() error { + containers, err := getAllContainers() + if err != nil { + fmt.Println(containers) + return err + } + + if containers != "" { + if err = deleteContainer(containers); err != nil { + return err + } + } + return nil +} + +func deleteAllNetworks() error { + networks, err := getAllNetworks() + if err != nil { + return err + } + var errors []string + for _, n := range networks { + if n.Name == "bridge" || n.Name == "none" || n.Name == "host" { + continue + } + status, b, err := sockRequest("DELETE", "/networks/"+n.Name, nil) + if err != nil { + errors = append(errors, err.Error()) + continue + } + if status != http.StatusNoContent { + errors = append(errors, fmt.Sprintf("error deleting network %s: %s", n.Name, string(b))) + } + } + if len(errors) > 0 { + return fmt.Errorf(strings.Join(errors, "\n")) + } + return nil +} + +func getAllNetworks() ([]types.NetworkResource, error) { + var networks []types.NetworkResource + _, b, err := sockRequest("GET", "/networks", nil) + if err != nil { + return nil, err + } + if err := json.Unmarshal(b, &networks); err != nil { + return nil, err + } + return networks, nil +} + +func deleteAllVolumes() error { + volumes, err := getAllVolumes() + if err != nil { + return err + } + var errors []string + for _, v := range volumes { + status, b, err := sockRequest("DELETE", "/volumes/"+v.Name, nil) + if err != nil { + errors = append(errors, err.Error()) + continue + } + if status != http.StatusNoContent { + errors = append(errors, fmt.Sprintf("error deleting volume %s: %s", v.Name, string(b))) + } + } + if len(errors) > 0 { + return fmt.Errorf(strings.Join(errors, "\n")) + } + return nil +} + +func getAllVolumes() ([]*types.Volume, error) { + var volumes types.VolumesListResponse + _, b, err := sockRequest("GET", "/volumes", nil) + if err != nil { + return nil, err + } + if err := json.Unmarshal(b, &volumes); err != nil { + return nil, err + } + return volumes.Volumes, nil +} + +var protectedImages = map[string]struct{}{} + +func deleteAllImages() error { + cmd := exec.Command(dockerBinary, "images") + cmd.Env = appendBaseEnv(true) + out, err := cmd.CombinedOutput() + if err != nil { + return err + } + lines := strings.Split(string(out), "\n")[1:] + var imgs []string + for _, l := range lines { + if l == "" { + continue + } + fields := strings.Fields(l) + imgTag := fields[0] + ":" + fields[1] + if _, ok := protectedImages[imgTag]; !ok { + if fields[0] == "" { + imgs = append(imgs, fields[2]) + continue + } + imgs = append(imgs, imgTag) + } + } + if len(imgs) == 0 { + return nil + } + args := append([]string{"rmi", "-f"}, imgs...) + if err := exec.Command(dockerBinary, args...).Run(); err != nil { + return err + } + return nil +} + +func getPausedContainers() (string, error) { + getPausedContainersCmd := exec.Command(dockerBinary, "ps", "-f", "status=paused", "-q", "-a") + out, exitCode, err := runCommandWithOutput(getPausedContainersCmd) + if exitCode != 0 && err == nil { + err = fmt.Errorf("failed to get a list of paused containers: %v\n", out) + } + + return out, err +} + +func getSliceOfPausedContainers() ([]string, error) { + out, err := getPausedContainers() + if err == nil { + if len(out) == 0 { + return nil, err + } + slice := strings.Split(strings.TrimSpace(out), "\n") + return slice, err + } + return []string{out}, err +} + +func unpauseContainer(container string) error { + unpauseCmd := exec.Command(dockerBinary, "unpause", container) + exitCode, err := runCommand(unpauseCmd) + if exitCode != 0 && err == nil { + err = fmt.Errorf("failed to unpause container") + } + + return nil +} + +func unpauseAllContainers() error { + containers, err := getPausedContainers() + if err != nil { + fmt.Println(containers) + return err + } + + containers = strings.Replace(containers, "\n", " ", -1) + containers = strings.Trim(containers, " ") + containerList := strings.Split(containers, " ") + + for _, value := range containerList { + if err = unpauseContainer(value); err != nil { + return err + } + } + + return nil +} + +func deleteImages(images ...string) error { + args := []string{"rmi", "-f"} + args = append(args, images...) + rmiCmd := exec.Command(dockerBinary, args...) + exitCode, err := runCommand(rmiCmd) + // set error manually if not set + if exitCode != 0 && err == nil { + err = fmt.Errorf("failed to remove image: `docker rmi` exit is non-zero") + } + return err +} + +func imageExists(image string) error { + inspectCmd := exec.Command(dockerBinary, "inspect", image) + exitCode, err := runCommand(inspectCmd) + if exitCode != 0 && err == nil { + err = fmt.Errorf("couldn't find image %q", image) + } + return err +} + +func pullImageIfNotExist(image string) error { + if err := imageExists(image); err != nil { + pullCmd := exec.Command(dockerBinary, "pull", image) + _, exitCode, err := runCommandWithOutput(pullCmd) + + if err != nil || exitCode != 0 { + return fmt.Errorf("image %q wasn't found locally and it couldn't be pulled: %s", image, err) + } + } + return nil +} + +func dockerCmdWithError(args ...string) (string, int, error) { + if err := validateArgs(args...); err != nil { + return "", 0, err + } + return integration.DockerCmdWithError(dockerBinary, args...) +} + +func dockerCmdWithStdoutStderr(c *check.C, args ...string) (string, string, int) { + if err := validateArgs(args...); err != nil { + c.Fatalf(err.Error()) + } + return integration.DockerCmdWithStdoutStderr(dockerBinary, c, args...) +} + +func dockerCmd(c *check.C, args ...string) (string, int) { + if err := validateArgs(args...); err != nil { + c.Fatalf(err.Error()) + } + return integration.DockerCmd(dockerBinary, c, args...) +} + +// execute a docker command with a timeout +func dockerCmdWithTimeout(timeout time.Duration, args ...string) (string, int, error) { + if err := validateArgs(args...); err != nil { + return "", 0, err + } + return integration.DockerCmdWithTimeout(dockerBinary, timeout, args...) +} + +// execute a docker command in a directory +func dockerCmdInDir(c *check.C, path string, args ...string) (string, int, error) { + if err := validateArgs(args...); err != nil { + c.Fatalf(err.Error()) + } + return integration.DockerCmdInDir(dockerBinary, path, args...) +} + +// execute a docker command in a directory with a timeout +func dockerCmdInDirWithTimeout(timeout time.Duration, path string, args ...string) (string, int, error) { + if err := validateArgs(args...); err != nil { + return "", 0, err + } + return integration.DockerCmdInDirWithTimeout(dockerBinary, timeout, path, args...) +} + +// validateArgs is a checker to ensure tests are not running commands which are +// not supported on platforms. Specifically on Windows this is 'busybox top'. +func validateArgs(args ...string) error { + if daemonPlatform != "windows" { + return nil + } + foundBusybox := -1 + for key, value := range args { + if strings.ToLower(value) == "busybox" { + foundBusybox = key + } + if (foundBusybox != -1) && (key == foundBusybox+1) && (strings.ToLower(value) == "top") { + return errors.New("Cannot use 'busybox top' in tests on Windows. Use runSleepingContainer()") + } + } + return nil +} + +// find the State.ExitCode in container metadata +func findContainerExitCode(c *check.C, name string, vargs ...string) string { + args := append(vargs, "inspect", "--format='{{ .State.ExitCode }} {{ .State.Error }}'", name) + cmd := exec.Command(dockerBinary, args...) + out, _, err := runCommandWithOutput(cmd) + if err != nil { + c.Fatal(err, out) + } + return out +} + +func findContainerIP(c *check.C, id string, network string) string { + out, _ := dockerCmd(c, "inspect", fmt.Sprintf("--format='{{ .NetworkSettings.Networks.%s.IPAddress }}'", network), id) + return strings.Trim(out, " \r\n'") +} + +func getContainerCount() (int, error) { + const containers = "Containers:" + + cmd := exec.Command(dockerBinary, "info") + out, _, err := runCommandWithOutput(cmd) + if err != nil { + return 0, err + } + + lines := strings.Split(out, "\n") + for _, line := range lines { + if strings.Contains(line, containers) { + output := strings.TrimSpace(line) + output = strings.TrimLeft(output, containers) + output = strings.Trim(output, " ") + containerCount, err := strconv.Atoi(output) + if err != nil { + return 0, err + } + return containerCount, nil + } + } + return 0, fmt.Errorf("couldn't find the Container count in the output") +} + +// FakeContext creates directories that can be used as a build context +type FakeContext struct { + Dir string +} + +// Add a file at a path, creating directories where necessary +func (f *FakeContext) Add(file, content string) error { + return f.addFile(file, []byte(content)) +} + +func (f *FakeContext) addFile(file string, content []byte) error { + filepath := path.Join(f.Dir, file) + dirpath := path.Dir(filepath) + if dirpath != "." { + if err := os.MkdirAll(dirpath, 0755); err != nil { + return err + } + } + return ioutil.WriteFile(filepath, content, 0644) + +} + +// Delete a file at a path +func (f *FakeContext) Delete(file string) error { + filepath := path.Join(f.Dir, file) + return os.RemoveAll(filepath) +} + +// Close deletes the context +func (f *FakeContext) Close() error { + return os.RemoveAll(f.Dir) +} + +func fakeContextFromNewTempDir() (*FakeContext, error) { + tmp, err := ioutil.TempDir("", "fake-context") + if err != nil { + return nil, err + } + if err := os.Chmod(tmp, 0755); err != nil { + return nil, err + } + return fakeContextFromDir(tmp), nil +} + +func fakeContextFromDir(dir string) *FakeContext { + return &FakeContext{dir} +} + +func fakeContextWithFiles(files map[string]string) (*FakeContext, error) { + ctx, err := fakeContextFromNewTempDir() + if err != nil { + return nil, err + } + for file, content := range files { + if err := ctx.Add(file, content); err != nil { + ctx.Close() + return nil, err + } + } + return ctx, nil +} + +func fakeContextAddDockerfile(ctx *FakeContext, dockerfile string) error { + if err := ctx.Add("Dockerfile", dockerfile); err != nil { + ctx.Close() + return err + } + return nil +} + +func fakeContext(dockerfile string, files map[string]string) (*FakeContext, error) { + ctx, err := fakeContextWithFiles(files) + if err != nil { + return nil, err + } + if err := fakeContextAddDockerfile(ctx, dockerfile); err != nil { + return nil, err + } + return ctx, nil +} + +// FakeStorage is a static file server. It might be running locally or remotely +// on test host. +type FakeStorage interface { + Close() error + URL() string + CtxDir() string +} + +func fakeBinaryStorage(archives map[string]*bytes.Buffer) (FakeStorage, error) { + ctx, err := fakeContextFromNewTempDir() + if err != nil { + return nil, err + } + for name, content := range archives { + if err := ctx.addFile(name, content.Bytes()); err != nil { + return nil, err + } + } + return fakeStorageWithContext(ctx) +} + +// fakeStorage returns either a local or remote (at daemon machine) file server +func fakeStorage(files map[string]string) (FakeStorage, error) { + ctx, err := fakeContextWithFiles(files) + if err != nil { + return nil, err + } + return fakeStorageWithContext(ctx) +} + +// fakeStorageWithContext returns either a local or remote (at daemon machine) file server +func fakeStorageWithContext(ctx *FakeContext) (FakeStorage, error) { + if isLocalDaemon { + return newLocalFakeStorage(ctx) + } + return newRemoteFileServer(ctx) +} + +// localFileStorage is a file storage on the running machine +type localFileStorage struct { + *FakeContext + *httptest.Server +} + +func (s *localFileStorage) URL() string { + return s.Server.URL +} + +func (s *localFileStorage) CtxDir() string { + return s.FakeContext.Dir +} + +func (s *localFileStorage) Close() error { + defer s.Server.Close() + return s.FakeContext.Close() +} + +func newLocalFakeStorage(ctx *FakeContext) (*localFileStorage, error) { + handler := http.FileServer(http.Dir(ctx.Dir)) + server := httptest.NewServer(handler) + return &localFileStorage{ + FakeContext: ctx, + Server: server, + }, nil +} + +// remoteFileServer is a containerized static file server started on the remote +// testing machine to be used in URL-accepting docker build functionality. +type remoteFileServer struct { + host string // hostname/port web server is listening to on docker host e.g. 0.0.0.0:43712 + container string + image string + ctx *FakeContext +} + +func (f *remoteFileServer) URL() string { + u := url.URL{ + Scheme: "http", + Host: f.host} + return u.String() +} + +func (f *remoteFileServer) CtxDir() string { + return f.ctx.Dir +} + +func (f *remoteFileServer) Close() error { + defer func() { + if f.ctx != nil { + f.ctx.Close() + } + if f.image != "" { + deleteImages(f.image) + } + }() + if f.container == "" { + return nil + } + return deleteContainer(f.container) +} + +func newRemoteFileServer(ctx *FakeContext) (*remoteFileServer, error) { + var ( + image = fmt.Sprintf("fileserver-img-%s", strings.ToLower(stringutils.GenerateRandomAlphaOnlyString(10))) + container = fmt.Sprintf("fileserver-cnt-%s", strings.ToLower(stringutils.GenerateRandomAlphaOnlyString(10))) + ) + + // Build the image + if err := fakeContextAddDockerfile(ctx, `FROM httpserver +COPY . /static`); err != nil { + return nil, fmt.Errorf("Cannot add Dockerfile to context: %v", err) + } + if _, err := buildImageFromContext(image, ctx, false); err != nil { + return nil, fmt.Errorf("failed building file storage container image: %v", err) + } + + // Start the container + runCmd := exec.Command(dockerBinary, "run", "-d", "-P", "--name", container, image) + if out, ec, err := runCommandWithOutput(runCmd); err != nil { + return nil, fmt.Errorf("failed to start file storage container. ec=%v\nout=%s\nerr=%v", ec, out, err) + } + + // Find out the system assigned port + out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "port", container, "80/tcp")) + if err != nil { + return nil, fmt.Errorf("failed to find container port: err=%v\nout=%s", err, out) + } + + fileserverHostPort := strings.Trim(out, "\n") + _, port, err := net.SplitHostPort(fileserverHostPort) + if err != nil { + return nil, fmt.Errorf("unable to parse file server host:port: %v", err) + } + + dockerHostURL, err := url.Parse(daemonHost()) + if err != nil { + return nil, fmt.Errorf("unable to parse daemon host URL: %v", err) + } + + host, _, err := net.SplitHostPort(dockerHostURL.Host) + if err != nil { + return nil, fmt.Errorf("unable to parse docker daemon host:port: %v", err) + } + + return &remoteFileServer{ + container: container, + image: image, + host: fmt.Sprintf("%s:%s", host, port), + ctx: ctx}, nil +} + +func inspectFieldAndMarshall(c *check.C, name, field string, output interface{}) { + str := inspectFieldJSON(c, name, field) + err := json.Unmarshal([]byte(str), output) + if c != nil { + c.Assert(err, check.IsNil, check.Commentf("failed to unmarshal: %v", err)) + } +} + +func inspectFilter(name, filter string) (string, error) { + format := fmt.Sprintf("{{%s}}", filter) + inspectCmd := exec.Command(dockerBinary, "inspect", "-f", format, name) + out, exitCode, err := runCommandWithOutput(inspectCmd) + if err != nil || exitCode != 0 { + return "", fmt.Errorf("failed to inspect %s: %s", name, out) + } + return strings.TrimSpace(out), nil +} + +func inspectFieldWithError(name, field string) (string, error) { + return inspectFilter(name, fmt.Sprintf(".%s", field)) +} + +func inspectField(c *check.C, name, field string) string { + out, err := inspectFilter(name, fmt.Sprintf(".%s", field)) + if c != nil { + c.Assert(err, check.IsNil) + } + return out +} + +func inspectFieldJSON(c *check.C, name, field string) string { + out, err := inspectFilter(name, fmt.Sprintf("json .%s", field)) + if c != nil { + c.Assert(err, check.IsNil) + } + return out +} + +func inspectFieldMap(c *check.C, name, path, field string) string { + out, err := inspectFilter(name, fmt.Sprintf("index .%s %q", path, field)) + if c != nil { + c.Assert(err, check.IsNil) + } + return out +} + +func inspectMountSourceField(name, destination string) (string, error) { + m, err := inspectMountPoint(name, destination) + if err != nil { + return "", err + } + return m.Source, nil +} + +func inspectMountPoint(name, destination string) (types.MountPoint, error) { + out, err := inspectFilter(name, "json .Mounts") + if err != nil { + return types.MountPoint{}, err + } + + return inspectMountPointJSON(out, destination) +} + +var errMountNotFound = errors.New("mount point not found") + +func inspectMountPointJSON(j, destination string) (types.MountPoint, error) { + var mp []types.MountPoint + if err := unmarshalJSON([]byte(j), &mp); err != nil { + return types.MountPoint{}, err + } + + var m *types.MountPoint + for _, c := range mp { + if c.Destination == destination { + m = &c + break + } + } + + if m == nil { + return types.MountPoint{}, errMountNotFound + } + + return *m, nil +} + +func inspectImage(name, filter string) (string, error) { + args := []string{"inspect", "--type", "image"} + if filter != "" { + format := fmt.Sprintf("{{%s}}", filter) + args = append(args, "-f", format) + } + args = append(args, name) + inspectCmd := exec.Command(dockerBinary, args...) + out, exitCode, err := runCommandWithOutput(inspectCmd) + if err != nil || exitCode != 0 { + return "", fmt.Errorf("failed to inspect %s: %s", name, out) + } + return strings.TrimSpace(out), nil +} + +func getIDByName(name string) (string, error) { + return inspectFieldWithError(name, "Id") +} + +// getContainerState returns the exit code of the container +// and true if it's running +// the exit code should be ignored if it's running +func getContainerState(c *check.C, id string) (int, bool, error) { + var ( + exitStatus int + running bool + ) + out, exitCode := dockerCmd(c, "inspect", "--format={{.State.Running}} {{.State.ExitCode}}", id) + if exitCode != 0 { + return 0, false, fmt.Errorf("%q doesn't exist: %s", id, out) + } + + out = strings.Trim(out, "\n") + splitOutput := strings.Split(out, " ") + if len(splitOutput) != 2 { + return 0, false, fmt.Errorf("failed to get container state: output is broken") + } + if splitOutput[0] == "true" { + running = true + } + if n, err := strconv.Atoi(splitOutput[1]); err == nil { + exitStatus = n + } else { + return 0, false, fmt.Errorf("failed to get container state: couldn't parse integer") + } + + return exitStatus, running, nil +} + +func buildImageCmd(name, dockerfile string, useCache bool, buildFlags ...string) *exec.Cmd { + args := []string{"build", "-t", name} + if !useCache { + args = append(args, "--no-cache") + } + args = append(args, buildFlags...) + args = append(args, "-") + buildCmd := exec.Command(dockerBinary, args...) + buildCmd.Stdin = strings.NewReader(dockerfile) + return buildCmd +} + +func buildImageWithOut(name, dockerfile string, useCache bool, buildFlags ...string) (string, string, error) { + buildCmd := buildImageCmd(name, dockerfile, useCache, buildFlags...) + out, exitCode, err := runCommandWithOutput(buildCmd) + if err != nil || exitCode != 0 { + return "", out, fmt.Errorf("failed to build the image: %s", out) + } + id, err := getIDByName(name) + if err != nil { + return "", out, err + } + return id, out, nil +} + +func buildImageWithStdoutStderr(name, dockerfile string, useCache bool, buildFlags ...string) (string, string, string, error) { + buildCmd := buildImageCmd(name, dockerfile, useCache, buildFlags...) + stdout, stderr, exitCode, err := runCommandWithStdoutStderr(buildCmd) + if err != nil || exitCode != 0 { + return "", stdout, stderr, fmt.Errorf("failed to build the image: %s", stdout) + } + id, err := getIDByName(name) + if err != nil { + return "", stdout, stderr, err + } + return id, stdout, stderr, nil +} + +func buildImage(name, dockerfile string, useCache bool, buildFlags ...string) (string, error) { + id, _, err := buildImageWithOut(name, dockerfile, useCache, buildFlags...) + return id, err +} + +func buildImageFromContext(name string, ctx *FakeContext, useCache bool, buildFlags ...string) (string, error) { + id, _, err := buildImageFromContextWithOut(name, ctx, useCache, buildFlags...) + if err != nil { + return "", err + } + return id, nil +} + +func buildImageFromContextWithOut(name string, ctx *FakeContext, useCache bool, buildFlags ...string) (string, string, error) { + args := []string{"build", "-t", name} + if !useCache { + args = append(args, "--no-cache") + } + args = append(args, buildFlags...) + args = append(args, ".") + buildCmd := exec.Command(dockerBinary, args...) + buildCmd.Dir = ctx.Dir + out, exitCode, err := runCommandWithOutput(buildCmd) + if err != nil || exitCode != 0 { + return "", "", fmt.Errorf("failed to build the image: %s", out) + } + id, err := getIDByName(name) + if err != nil { + return "", "", err + } + return id, out, nil +} + +func buildImageFromContextWithStdoutStderr(name string, ctx *FakeContext, useCache bool, buildFlags ...string) (string, string, string, error) { + args := []string{"build", "-t", name} + if !useCache { + args = append(args, "--no-cache") + } + args = append(args, buildFlags...) + args = append(args, ".") + buildCmd := exec.Command(dockerBinary, args...) + buildCmd.Dir = ctx.Dir + + stdout, stderr, exitCode, err := runCommandWithStdoutStderr(buildCmd) + if err != nil || exitCode != 0 { + return "", stdout, stderr, fmt.Errorf("failed to build the image: %s", stdout) + } + id, err := getIDByName(name) + if err != nil { + return "", stdout, stderr, err + } + return id, stdout, stderr, nil +} + +func buildImageFromGitWithStdoutStderr(name string, ctx *fakeGit, useCache bool, buildFlags ...string) (string, string, string, error) { + args := []string{"build", "-t", name} + if !useCache { + args = append(args, "--no-cache") + } + args = append(args, buildFlags...) + args = append(args, ctx.RepoURL) + buildCmd := exec.Command(dockerBinary, args...) + + stdout, stderr, exitCode, err := runCommandWithStdoutStderr(buildCmd) + if err != nil || exitCode != 0 { + return "", stdout, stderr, fmt.Errorf("failed to build the image: %s", stdout) + } + id, err := getIDByName(name) + if err != nil { + return "", stdout, stderr, err + } + return id, stdout, stderr, nil +} + +func buildImageFromPath(name, path string, useCache bool, buildFlags ...string) (string, error) { + args := []string{"build", "-t", name} + if !useCache { + args = append(args, "--no-cache") + } + args = append(args, buildFlags...) + args = append(args, path) + buildCmd := exec.Command(dockerBinary, args...) + out, exitCode, err := runCommandWithOutput(buildCmd) + if err != nil || exitCode != 0 { + return "", fmt.Errorf("failed to build the image: %s", out) + } + return getIDByName(name) +} + +type gitServer interface { + URL() string + Close() error +} + +type localGitServer struct { + *httptest.Server +} + +func (r *localGitServer) Close() error { + r.Server.Close() + return nil +} + +func (r *localGitServer) URL() string { + return r.Server.URL +} + +type fakeGit struct { + root string + server gitServer + RepoURL string +} + +func (g *fakeGit) Close() { + g.server.Close() + os.RemoveAll(g.root) +} + +func newFakeGit(name string, files map[string]string, enforceLocalServer bool) (*fakeGit, error) { + ctx, err := fakeContextWithFiles(files) + if err != nil { + return nil, err + } + defer ctx.Close() + curdir, err := os.Getwd() + if err != nil { + return nil, err + } + defer os.Chdir(curdir) + + if output, err := exec.Command("git", "init", ctx.Dir).CombinedOutput(); err != nil { + return nil, fmt.Errorf("error trying to init repo: %s (%s)", err, output) + } + err = os.Chdir(ctx.Dir) + if err != nil { + return nil, err + } + if output, err := exec.Command("git", "config", "user.name", "Fake User").CombinedOutput(); err != nil { + return nil, fmt.Errorf("error trying to set 'user.name': %s (%s)", err, output) + } + if output, err := exec.Command("git", "config", "user.email", "fake.user@example.com").CombinedOutput(); err != nil { + return nil, fmt.Errorf("error trying to set 'user.email': %s (%s)", err, output) + } + if output, err := exec.Command("git", "add", "*").CombinedOutput(); err != nil { + return nil, fmt.Errorf("error trying to add files to repo: %s (%s)", err, output) + } + if output, err := exec.Command("git", "commit", "-a", "-m", "Initial commit").CombinedOutput(); err != nil { + return nil, fmt.Errorf("error trying to commit to repo: %s (%s)", err, output) + } + + root, err := ioutil.TempDir("", "docker-test-git-repo") + if err != nil { + return nil, err + } + repoPath := filepath.Join(root, name+".git") + if output, err := exec.Command("git", "clone", "--bare", ctx.Dir, repoPath).CombinedOutput(); err != nil { + os.RemoveAll(root) + return nil, fmt.Errorf("error trying to clone --bare: %s (%s)", err, output) + } + err = os.Chdir(repoPath) + if err != nil { + os.RemoveAll(root) + return nil, err + } + if output, err := exec.Command("git", "update-server-info").CombinedOutput(); err != nil { + os.RemoveAll(root) + return nil, fmt.Errorf("error trying to git update-server-info: %s (%s)", err, output) + } + err = os.Chdir(curdir) + if err != nil { + os.RemoveAll(root) + return nil, err + } + + var server gitServer + if !enforceLocalServer { + // use fakeStorage server, which might be local or remote (at test daemon) + server, err = fakeStorageWithContext(fakeContextFromDir(root)) + if err != nil { + return nil, fmt.Errorf("cannot start fake storage: %v", err) + } + } else { + // always start a local http server on CLI test machine + httpServer := httptest.NewServer(http.FileServer(http.Dir(root))) + server = &localGitServer{httpServer} + } + return &fakeGit{ + root: root, + server: server, + RepoURL: fmt.Sprintf("%s/%s.git", server.URL(), name), + }, nil +} + +// Write `content` to the file at path `dst`, creating it if necessary, +// as well as any missing directories. +// The file is truncated if it already exists. +// Fail the test when error occurs. +func writeFile(dst, content string, c *check.C) { + // Create subdirectories if necessary + c.Assert(os.MkdirAll(path.Dir(dst), 0700), check.IsNil) + f, err := os.OpenFile(dst, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0700) + c.Assert(err, check.IsNil) + defer f.Close() + // Write content (truncate if it exists) + _, err = io.Copy(f, strings.NewReader(content)) + c.Assert(err, check.IsNil) +} + +// Return the contents of file at path `src`. +// Fail the test when error occurs. +func readFile(src string, c *check.C) (content string) { + data, err := ioutil.ReadFile(src) + c.Assert(err, check.IsNil) + + return string(data) +} + +func containerStorageFile(containerID, basename string) string { + return filepath.Join(containerStoragePath, containerID, basename) +} + +// docker commands that use this function must be run with the '-d' switch. +func runCommandAndReadContainerFile(filename string, cmd *exec.Cmd) ([]byte, error) { + out, _, err := runCommandWithOutput(cmd) + if err != nil { + return nil, fmt.Errorf("%v: %q", err, out) + } + + contID := strings.TrimSpace(out) + + if err := waitRun(contID); err != nil { + return nil, fmt.Errorf("%v: %q", contID, err) + } + + return readContainerFile(contID, filename) +} + +func readContainerFile(containerID, filename string) ([]byte, error) { + f, err := os.Open(containerStorageFile(containerID, filename)) + if err != nil { + return nil, err + } + defer f.Close() + + content, err := ioutil.ReadAll(f) + if err != nil { + return nil, err + } + + return content, nil +} + +func readContainerFileWithExec(containerID, filename string) ([]byte, error) { + out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "exec", containerID, "cat", filename)) + return []byte(out), err +} + +// daemonTime provides the current time on the daemon host +func daemonTime(c *check.C) time.Time { + if isLocalDaemon { + return time.Now() + } + + status, body, err := sockRequest("GET", "/info", nil) + c.Assert(err, check.IsNil) + c.Assert(status, check.Equals, http.StatusOK) + + type infoJSON struct { + SystemTime string + } + var info infoJSON + err = json.Unmarshal(body, &info) + c.Assert(err, check.IsNil, check.Commentf("unable to unmarshal GET /info response")) + + dt, err := time.Parse(time.RFC3339Nano, info.SystemTime) + c.Assert(err, check.IsNil, check.Commentf("invalid time format in GET /info response")) + return dt +} + +func setupRegistry(c *check.C, schema1 bool, auth, tokenURL string) *testRegistryV2 { + reg, err := newTestRegistryV2(c, schema1, auth, tokenURL) + c.Assert(err, check.IsNil) + + // Wait for registry to be ready to serve requests. + for i := 0; i != 50; i++ { + if err = reg.Ping(); err == nil { + break + } + time.Sleep(100 * time.Millisecond) + } + + c.Assert(err, check.IsNil, check.Commentf("Timeout waiting for test registry to become available: %v", err)) + return reg +} + +func setupNotary(c *check.C) *testNotary { + ts, err := newTestNotary(c) + c.Assert(err, check.IsNil) + + return ts +} + +// appendBaseEnv appends the minimum set of environment variables to exec the +// docker cli binary for testing with correct configuration to the given env +// list. +func appendBaseEnv(isTLS bool, env ...string) []string { + preserveList := []string{ + // preserve remote test host + "DOCKER_HOST", + + // windows: requires preserving SystemRoot, otherwise dial tcp fails + // with "GetAddrInfoW: A non-recoverable error occurred during a database lookup." + "SystemRoot", + } + if isTLS { + preserveList = append(preserveList, "DOCKER_TLS_VERIFY", "DOCKER_CERT_PATH") + } + + for _, key := range preserveList { + if val := os.Getenv(key); val != "" { + env = append(env, fmt.Sprintf("%s=%s", key, val)) + } + } + return env +} + +func createTmpFile(c *check.C, content string) string { + f, err := ioutil.TempFile("", "testfile") + c.Assert(err, check.IsNil) + + filename := f.Name() + + err = ioutil.WriteFile(filename, []byte(content), 0644) + c.Assert(err, check.IsNil) + + return filename +} + +func buildImageWithOutInDamon(socket string, name, dockerfile string, useCache bool) (string, error) { + args := []string{"--host", socket} + buildCmd := buildImageCmdArgs(args, name, dockerfile, useCache) + out, exitCode, err := runCommandWithOutput(buildCmd) + if err != nil || exitCode != 0 { + return out, fmt.Errorf("failed to build the image: %s, error: %v", out, err) + } + return out, nil +} + +func buildImageCmdArgs(args []string, name, dockerfile string, useCache bool) *exec.Cmd { + args = append(args, []string{"-D", "build", "-t", name}...) + if !useCache { + args = append(args, "--no-cache") + } + args = append(args, "-") + buildCmd := exec.Command(dockerBinary, args...) + buildCmd.Stdin = strings.NewReader(dockerfile) + return buildCmd + +} + +func waitForContainer(contID string, args ...string) error { + args = append([]string{"run", "--name", contID}, args...) + cmd := exec.Command(dockerBinary, args...) + if _, err := runCommand(cmd); err != nil { + return err + } + + if err := waitRun(contID); err != nil { + return err + } + + return nil +} + +// waitRun will wait for the specified container to be running, maximum 5 seconds. +func waitRun(contID string) error { + return waitInspect(contID, "{{.State.Running}}", "true", 5*time.Second) +} + +// waitExited will wait for the specified container to state exit, subject +// to a maximum time limit in seconds supplied by the caller +func waitExited(contID string, duration time.Duration) error { + return waitInspect(contID, "{{.State.Status}}", "exited", duration) +} + +// waitInspect will wait for the specified container to have the specified string +// in the inspect output. It will wait until the specified timeout (in seconds) +// is reached. +func waitInspect(name, expr, expected string, timeout time.Duration) error { + return waitInspectWithArgs(name, expr, expected, timeout) +} + +func waitInspectWithArgs(name, expr, expected string, timeout time.Duration, arg ...string) error { + after := time.After(timeout) + + args := append(arg, "inspect", "-f", expr, name) + for { + cmd := exec.Command(dockerBinary, args...) + out, _, err := runCommandWithOutput(cmd) + if err != nil { + if !strings.Contains(out, "No such") { + return fmt.Errorf("error executing docker inspect: %v\n%s", err, out) + } + select { + case <-after: + return err + default: + time.Sleep(10 * time.Millisecond) + continue + } + } + + out = strings.TrimSpace(out) + if out == expected { + break + } + + select { + case <-after: + return fmt.Errorf("condition \"%q == %q\" not true in time", out, expected) + default: + } + + time.Sleep(100 * time.Millisecond) + } + return nil +} + +func getInspectBody(c *check.C, version, id string) []byte { + endpoint := fmt.Sprintf("/%s/containers/%s/json", version, id) + status, body, err := sockRequest("GET", endpoint, nil) + c.Assert(err, check.IsNil) + c.Assert(status, check.Equals, http.StatusOK) + return body +} + +// Run a long running idle task in a background container using the +// system-specific default image and command. +func runSleepingContainer(c *check.C, extraArgs ...string) (string, int) { + return runSleepingContainerInImage(c, defaultSleepImage, extraArgs...) +} + +// Run a long running idle task in a background container using the specified +// image and the system-specific command. +func runSleepingContainerInImage(c *check.C, image string, extraArgs ...string) (string, int) { + args := []string{"run", "-d"} + args = append(args, extraArgs...) + args = append(args, image) + args = append(args, defaultSleepCommand...) + return dockerCmd(c, args...) +} + +func getRootUIDGID() (int, int, error) { + uidgid := strings.Split(filepath.Base(dockerBasePath), ".") + if len(uidgid) == 1 { + //user namespace remapping is not turned on; return 0 + return 0, 0, nil + } + uid, err := strconv.Atoi(uidgid[0]) + if err != nil { + return 0, 0, err + } + gid, err := strconv.Atoi(uidgid[1]) + if err != nil { + return 0, 0, err + } + return uid, gid, nil +} + +// minimalBaseImage returns the name of the minimal base image for the current +// daemon platform. +func minimalBaseImage() string { + if daemonPlatform == "windows" { + return WindowsBaseImage + } + return "scratch" +} + +func getGoroutineNumber() (int, error) { + i := struct { + NGoroutines int + }{} + status, b, err := sockRequest("GET", "/info", nil) + if err != nil { + return 0, err + } + if status != http.StatusOK { + return 0, fmt.Errorf("http status code: %d", status) + } + if err := json.Unmarshal(b, &i); err != nil { + return 0, err + } + return i.NGoroutines, nil +} + +func waitForGoroutines(expected int) error { + t := time.After(30 * time.Second) + for { + select { + case <-t: + n, err := getGoroutineNumber() + if err != nil { + return err + } + if n > expected { + return fmt.Errorf("leaked goroutines: expected less than or equal to %d, got: %d", expected, n) + } + default: + n, err := getGoroutineNumber() + if err != nil { + return err + } + if n <= expected { + return nil + } + time.Sleep(200 * time.Millisecond) + } + } +} diff --git a/integration-cli/events_utils.go b/integration-cli/events_utils.go new file mode 100644 index 00000000..cdd3106b --- /dev/null +++ b/integration-cli/events_utils.go @@ -0,0 +1,206 @@ +package main + +import ( + "bufio" + "bytes" + "io" + "os/exec" + "regexp" + "strconv" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/daemon/events/testutils" + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" +) + +// eventMatcher is a function that tries to match an event input. +// It returns true if the event matches and a map with +// a set of key/value to identify the match. +type eventMatcher func(text string) (map[string]string, bool) + +// eventMatchProcessor is a function to handle an event match. +// It receives a map of key/value with the information extracted in a match. +type eventMatchProcessor func(matches map[string]string) + +// eventObserver runs an events commands and observes its output. +type eventObserver struct { + buffer *bytes.Buffer + command *exec.Cmd + scanner *bufio.Scanner + startTime string + disconnectionError error +} + +// newEventObserver creates the observer and initializes the command +// without running it. Users must call `eventObserver.Start` to start the command. +func newEventObserver(c *check.C, args ...string) (*eventObserver, error) { + since := daemonTime(c).Unix() + return newEventObserverWithBacklog(c, since, args...) +} + +// newEventObserverWithBacklog creates a new observer changing the start time of the backlog to return. +func newEventObserverWithBacklog(c *check.C, since int64, args ...string) (*eventObserver, error) { + startTime := strconv.FormatInt(since, 10) + cmdArgs := []string{"events", "--since", startTime} + if len(args) > 0 { + cmdArgs = append(cmdArgs, args...) + } + eventsCmd := exec.Command(dockerBinary, cmdArgs...) + stdout, err := eventsCmd.StdoutPipe() + if err != nil { + return nil, err + } + + return &eventObserver{ + buffer: new(bytes.Buffer), + command: eventsCmd, + scanner: bufio.NewScanner(stdout), + startTime: startTime, + }, nil +} + +// Start starts the events command. +func (e *eventObserver) Start() error { + return e.command.Start() +} + +// Stop stops the events command. +func (e *eventObserver) Stop() { + e.command.Process.Kill() + e.command.Process.Release() +} + +// Match tries to match the events output with a given matcher. +func (e *eventObserver) Match(match eventMatcher, process eventMatchProcessor) { + for e.scanner.Scan() { + text := e.scanner.Text() + e.buffer.WriteString(text) + e.buffer.WriteString("\n") + + if matches, ok := match(text); ok { + process(matches) + } + } + + err := e.scanner.Err() + if err == nil { + err = io.EOF + } + + logrus.Debug("EventObserver scanner loop finished: %v", err) + e.disconnectionError = err +} + +func (e *eventObserver) CheckEventError(c *check.C, id, event string, match eventMatcher) { + var foundEvent bool + scannerOut := e.buffer.String() + + if e.disconnectionError != nil { + until := strconv.FormatInt(daemonTime(c).Unix(), 10) + out, _ := dockerCmd(c, "events", "--since", e.startTime, "--until", until) + events := strings.Split(strings.TrimSpace(out), "\n") + for _, e := range events { + if _, ok := match(e); ok { + foundEvent = true + break + } + } + scannerOut = out + } + if !foundEvent { + c.Fatalf("failed to observe event `%s` for %s. Disconnection error: %v\nout:\n%v", event, id, e.disconnectionError, scannerOut) + } +} + +// matchEventLine matches a text with the event regular expression. +// It returns the matches and true if the regular expression matches with the given id and event type. +// It returns an empty map and false if there is no match. +func matchEventLine(id, eventType string, actions map[string]chan bool) eventMatcher { + return func(text string) (map[string]string, bool) { + matches := eventstestutils.ScanMap(text) + if len(matches) == 0 { + return matches, false + } + + if matchIDAndEventType(matches, id, eventType) { + if _, ok := actions[matches["action"]]; ok { + return matches, true + } + } + return matches, false + } +} + +// processEventMatch closes an action channel when an event line matches the expected action. +func processEventMatch(actions map[string]chan bool) eventMatchProcessor { + return func(matches map[string]string) { + if ch, ok := actions[matches["action"]]; ok { + ch <- true + } + } +} + +// parseEventAction parses an event text and returns the action. +// It fails if the text is not in the event format. +func parseEventAction(c *check.C, text string) string { + matches := eventstestutils.ScanMap(text) + return matches["action"] +} + +// eventActionsByIDAndType returns the actions for a given id and type. +// It fails if the text is not in the event format. +func eventActionsByIDAndType(c *check.C, events []string, id, eventType string) []string { + var filtered []string + for _, event := range events { + matches := eventstestutils.ScanMap(event) + c.Assert(matches, checker.Not(checker.IsNil)) + if matchIDAndEventType(matches, id, eventType) { + filtered = append(filtered, matches["action"]) + } + } + return filtered +} + +// matchIDAndEventType returns true if an event matches a given id and type. +// It also resolves names in the event attributes if the id doesn't match. +func matchIDAndEventType(matches map[string]string, id, eventType string) bool { + return matchEventID(matches, id) && matches["eventType"] == eventType +} + +func matchEventID(matches map[string]string, id string) bool { + matchID := matches["id"] == id || strings.HasPrefix(matches["id"], id) + if !matchID && matches["attributes"] != "" { + // try matching a name in the attributes + attributes := map[string]string{} + for _, a := range strings.Split(matches["attributes"], ", ") { + kv := strings.Split(a, "=") + attributes[kv[0]] = kv[1] + } + matchID = attributes["name"] == id + } + return matchID +} + +func parseEvents(c *check.C, out, match string) { + events := strings.Split(strings.TrimSpace(out), "\n") + for _, event := range events { + matches := eventstestutils.ScanMap(event) + matched, err := regexp.MatchString(match, matches["action"]) + c.Assert(err, checker.IsNil) + c.Assert(matched, checker.True, check.Commentf("Matcher: %s did not match %s", match, matches["action"])) + } +} + +func parseEventsWithID(c *check.C, out, match, id string) { + events := strings.Split(strings.TrimSpace(out), "\n") + for _, event := range events { + matches := eventstestutils.ScanMap(event) + c.Assert(matchEventID(matches, id), checker.True) + + matched, err := regexp.MatchString(match, matches["action"]) + c.Assert(err, checker.IsNil) + c.Assert(matched, checker.True, check.Commentf("Matcher: %s did not match %s", match, matches["action"])) + } +} diff --git a/integration-cli/fixtures/auth/docker-credential-shell-test b/integration-cli/fixtures/auth/docker-credential-shell-test new file mode 100755 index 00000000..1980bb18 --- /dev/null +++ b/integration-cli/fixtures/auth/docker-credential-shell-test @@ -0,0 +1,33 @@ +#!/bin/bash + +set -e + +case $1 in + "store") + in=$( $TEMP/$server + ;; + "get") + in=$(c* zovW*2`my-BS#-6VC#(TWylUoH;FtY>E>gSBdD(R?T)*vNRTtCjW{dQq*rhc~JiEJz zsl`J#iI?4n-S>-|RPWcb+PkO&p2Vj7>-wi+S}$j_7sXd|yt?(*H*av1 z+nj$j?T=M`^{U#O?d!U0Kio7`Pi=kdMg6yBHQQD7cjwb=bNHthMQp;4h~=l$E~*AE zuwWB8>5;iMTvuHR-Lmal{@LpHT4MUleZcZ73oou6S9)q>^d-k>oX_}Qcxyo^p;T#I z4yBjEs}MP78A@rGh|&6(B$v4q&IKEaPbcYiRLK(|Fm&Lbue#U-=g_}>K?m>u=o9SE z|2Wfh|Nm{EC(M}~AUk1=_?|0jAs(MS-F>HlGq1Fv!G%u}F zxq~RdI2NK#f|*#Nn~(V{vFLOO8UqEuV@wWH0lWbP;+XE!kN?B|zvfDCV+{KLI33XJ zG420+{r|!CKbik8jnddl(C`@lF&t?A{}{IbApid{FhAKg29N*Y{y*OI0q1{v<6mif z{ttd8A4C0_$Nx6||IluLzUGZ<*Y1BAOzlPXfBaM3-28d; z^3|(Ru4ZY9j6K(!m62PF7=SBvvY>6u9_MkwI>$54a7OCT$T3-m=lJE( zmUnm}hhi;&u@=Bq+_JlF`T)-5bsBwzUH;iw#FV;!sB`|AkTbLnLJ;j*XJ0q(@q+&r#AY7K_tidj`Je@64Y zt^SAcHvH+*phv{y_9nO&aXZ27@H1gNA7uuCn|I7u4AJ4h5%>!(#?o4@01sM9=?Y<}U-wbn}~IT=+q) zpnU-y`(0?Hj|bqk-Cup*+8>Y5g-CYfCvx)B0vO)01+SpM1Tko z0U|&IhyW2F0z`la5CI}U1c(3;cp3zTzMXo^%Y*&eXctdy*>?6A8Sq zp|r}Cz7&k%LWj}|qk_p@3pT0=6BEu*_dHZBTzvXOYO{wlC)G^^hyW2F0z`la5CI}U k1c(3;AOb{y2oM1xKm>>Y5g-CYfCvx)B0vO)z%wTBe{YUtx&QzG literal 0 HcmV?d00001 diff --git a/integration-cli/fixtures/notary/delgkey1.crt b/integration-cli/fixtures/notary/delgkey1.crt new file mode 100644 index 00000000..306eeec9 --- /dev/null +++ b/integration-cli/fixtures/notary/delgkey1.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIID8jCCAtqgAwIBAgIJAJkxr+7rAgXbMA0GCSqGSIb3DQEBBQUAMFgxCzAJBgNV +BAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEPMA0G +A1UEChMGRG9ja2VyMRMwEQYDVQQDEwpkZWxlZ2F0aW9uMCAXDTE2MDMwODAyNDEy +MFoYDzIxMTYwMjEzMDI0MTIwWjBYMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0Ex +FjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xDzANBgNVBAoTBkRvY2tlcjETMBEGA1UE +AxMKZGVsZWdhdGlvbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJuz +To1qoL/RY5pNxdPkP/jiO3f/RTvz20C90EweaKgRdIV/vTUUE+mMRQulpf1vpCP9 +uidGfEoJcq4jM1H59XTYUoUvGbAMP3Iu7Uz0rF5v+Glm82Z0WGI+PkOnwRN2bJi4 +LhAch6QlA/48IOFH/O9jnHYMb45lQFpm+gOvatRyGkPZCftD3ntkhVMk1OJ7EZC4 +LYiwzmuPEYusO/qVgcHkGtIxLWAjGmDzrV3Q5orPVwwUOxNQdRRU1L2bhfUsodcb +Fgi/LCz4xnGx4YpF0O24Y7/0SPotSyaT0RYyj/j/bIKvYB20g4P7469klde1Ariz +UEIf12PlaJ/H/PaIlEcCAwEAAaOBvDCBuTAdBgNVHQ4EFgQUXZK4ZGswIq54W4VZ +OJY7zXvvndwwgYkGA1UdIwSBgTB/gBRdkrhkazAirnhbhVk4ljvNe++d3KFcpFow +WDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNp +c2NvMQ8wDQYDVQQKEwZEb2NrZXIxEzARBgNVBAMTCmRlbGVnYXRpb26CCQCZMa/u +6wIF2zAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQA2ktnjxB2lXF+g +lTc2qp9LhjofgCWKwLgWEeCwXI2nUBNE4n00hA9or2wer2viWC4IJnG0kTyMzzYT +m1lBpZ8BP6S3sSkvhohaqS+gBIUVB7U65tAof/SY2UHpeVJ1YpTE4F1GAUfqSY7V +6IGHZAGiLeUS5kC6pzZA4siBhyCoYKRKEb9R82jSCHeFYS3ntwY1/gqcO/uIidVE +2hLHlx6vBx9BEfXv31AGLoB3YocSTZLATwlrDHUQG1+oNh5ejQU1x/z+Y62EG5Jb +u0yLDdJeSgup/DzPEoNpSihtdQZytKMK+KBmh22gDA5h+a6620zTZwCvJYxH9kkM +IClUWwuD +-----END CERTIFICATE----- diff --git a/integration-cli/fixtures/notary/delgkey1.key b/integration-cli/fixtures/notary/delgkey1.key new file mode 100644 index 00000000..a0d0a30e --- /dev/null +++ b/integration-cli/fixtures/notary/delgkey1.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAm7NOjWqgv9Fjmk3F0+Q/+OI7d/9FO/PbQL3QTB5oqBF0hX+9 +NRQT6YxFC6Wl/W+kI/26J0Z8SglyriMzUfn1dNhShS8ZsAw/ci7tTPSsXm/4aWbz +ZnRYYj4+Q6fBE3ZsmLguEByHpCUD/jwg4Uf872OcdgxvjmVAWmb6A69q1HIaQ9kJ ++0Pee2SFUyTU4nsRkLgtiLDOa48Ri6w7+pWBweQa0jEtYCMaYPOtXdDmis9XDBQ7 +E1B1FFTUvZuF9Syh1xsWCL8sLPjGcbHhikXQ7bhjv/RI+i1LJpPRFjKP+P9sgq9g +HbSDg/vjr2SV17UCuLNQQh/XY+Von8f89oiURwIDAQABAoIBAB7DhfDRMaPU5n41 +gbIFNlKhuKhUCsT2wMqA9qgjlgAnOsOp4qObLPgHXBkaCLsTlPX7iw15ktM6HKul +jt1SqxoEKAHitYugT+Tqur5q1afvLcD9s3f54wC+VaUefzquOnTOZ2ONj4tyOODB +1qlMhQBzyRVWDbCv9tAl6p5RyaTh+8IULctlER6w9m3upT9NxoRi1PrPBCRiEKKo +4zDRvfbT/0ucLD20GS6trPv4ihTCTU7ydFujioDkFyNzCzYNGBnImpQ9/xeT5/Ys +IJQy9Tdn6V0rXMBBb1EhyBQYw5Oxy6d6tzhjvva6LaJBGo9yzX0NHt58Ymhgm1q/ +vscj1pECgYEAyegQFP7dkmUdXdNpdrIdCvKlvni3r/hwB/9H0sJHIJbfTusfzeLL +5Q8QSZAsaR7tSgJfr9GMdOjntvefYjKLfl3SnG/wF91m05eYfkeiZXc9RGe+XXGu +wv5u2m/G7a05XpW1JFX+1ORyj2x5KsvF7KDtWJyR5ryIsOwHZNGQpJ8CgYEAxWoo +r2eJBc9Xj5bhhS0VxUFODXImfeQF2aG2rSeuWMY7k4vmVkJwhBZiPW/dHBu1aMPh +/SY1W7cgzdVIf2RIF5MgzzkmoisEApZTiSwmP6A2bTx6miXwFCLTCHIDfiXJ0tQA +Nb+Ln+exks4BfCgKHOqWTcWizKNE/8Gb6SnhB1kCgYAgM1Z9QrhrpJyuXg0v1PA0 +0sYEPpRtCB416Ey4HCvj0qwClhUYbNc/zMs4MDok+b22U/KWw8C21H4/+/X7XzxI +BwaT1HZiF/lSPZcgbKRFsmKfCjyeAodwqctcIv+C4GGJ6C5fgSeHJHfwz8fzP1Rt +jKzNuQq71c2nCb2UIqgC2QKBgEieoJDFmVYVy7P6YMNIrnV9bGTt1NMCilRgdH6F +1lC5uzivge/BSPqN8V2AROoOF1GOnRcucvpmBx8wkhaqoQprCOqxr1CAWl1JRzly +kC9flCXi1YbW5dXCabb1metRo0h2zAz5hTcxV9UVCt7NK8svUFMTnKuCc+NRKTVA +PpMhAoGBAJ9rFgZpWHRVuzsangbGslq3fDYGENLJ2fPNjLgfgVLi+YotG9jfgQPW +QCvoSA1CChxzEJEB5hzEOEv9pThnBNg1LWNj+a3N5anW2UBHMEWeCrVFZwJMVdSd +srUFtap7da8iUddc+sHC5hHHFDBdqG4pDck/uTs3CNWRF/ZqzE/G +-----END RSA PRIVATE KEY----- diff --git a/integration-cli/fixtures/notary/delgkey2.crt b/integration-cli/fixtures/notary/delgkey2.crt new file mode 100644 index 00000000..40f2db2c --- /dev/null +++ b/integration-cli/fixtures/notary/delgkey2.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIID8jCCAtqgAwIBAgIJAMi/AxlwFquJMA0GCSqGSIb3DQEBBQUAMFgxCzAJBgNV +BAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEPMA0G +A1UEChMGRG9ja2VyMRMwEQYDVQQDEwpkZWxlZ2F0aW9uMCAXDTE2MDMwODAyNDEy +MloYDzIxMTYwMjEzMDI0MTIyWjBYMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0Ex +FjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xDzANBgNVBAoTBkRvY2tlcjETMBEGA1UE +AxMKZGVsZWdhdGlvbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL/a +1GO+02jt1p0sME+YGaK4+uZ9jezrpkCXKMsMfItgqCKRTX7YVuR7tnRt/Y1DNVqR +nMeGc77soDag6eW4xrYrv9LwylUsOLanvK1d/8hDxZhzJjqlJBmz6BvLWDZUF9uu +OjULL8yuP2cmRogjn0bqmdeKztrZtDQqQiwsG02nVjfuvVi3rP4G4DhL5fUoHB0R +E6L9Su3/2OWGpdxZqkT7GAbjgLl4/4CXs00493m8xZIHXQ9559PiVlLfk6p6FjEV +7irZp7XXSe1My/0HGebFXkYqEL9+My2od4w+qJmBT23aTduGTo8IZC7g9lwKEykA +hWrYhR5tjkLvOsQIE7ECAwEAAaOBvDCBuTAdBgNVHQ4EFgQUHtEAVcwI3k7W5B6c +L3w+eKQRsIYwgYkGA1UdIwSBgTB/gBQe0QBVzAjeTtbkHpwvfD54pBGwhqFcpFow +WDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNp +c2NvMQ8wDQYDVQQKEwZEb2NrZXIxEzARBgNVBAMTCmRlbGVnYXRpb26CCQDIvwMZ +cBariTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAfjsMtZ+nJ7y5t +rH9xPwWMLmtC5MwzDRvTUAGbNbFwwm8AncfvsDmmERqsr8L2qhY8CZ9vsN4NjjBn +QRmM/ynYA8JTbf/5ZNDnD4D6qTXLgGFqyHcBaorcB9uQ8eiMOFAbhxLYfPrKaYdV +qj+MejcFa3HmzmYCSqsvxRhSje5b4sORe9/3jNheXsX8VZUpWtCHc3k4GiCU6KyS +gpnXkShU4sG92cK72L8pxmGTz8ynNMj/9WKkLxpNIv5u0/D01a3z4wx5k1zfRZiz +IQS+xqxV/ztY844MDknxENlYzcqGj0Fd6hE5OKZxnGaH83A5adldMLlnhG1rscGP +as9uwPYP +-----END CERTIFICATE----- diff --git a/integration-cli/fixtures/notary/delgkey2.key b/integration-cli/fixtures/notary/delgkey2.key new file mode 100644 index 00000000..59e85478 --- /dev/null +++ b/integration-cli/fixtures/notary/delgkey2.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAv9rUY77TaO3WnSwwT5gZorj65n2N7OumQJcoywx8i2CoIpFN +fthW5Hu2dG39jUM1WpGcx4ZzvuygNqDp5bjGtiu/0vDKVSw4tqe8rV3/yEPFmHMm +OqUkGbPoG8tYNlQX2646NQsvzK4/ZyZGiCOfRuqZ14rO2tm0NCpCLCwbTadWN+69 +WLes/gbgOEvl9SgcHRETov1K7f/Y5Yal3FmqRPsYBuOAuXj/gJezTTj3ebzFkgdd +D3nn0+JWUt+TqnoWMRXuKtmntddJ7UzL/QcZ5sVeRioQv34zLah3jD6omYFPbdpN +24ZOjwhkLuD2XAoTKQCFatiFHm2OQu86xAgTsQIDAQABAoIBAQCDdASic1WXR58N +AgH4B1dJT0VaOBzOgIfhKbEhruSG+ys4xCY9Cy4+TyWskNBxweMEs1CgxVb5Mlh0 +Fb0tUXWVzFQazDFWOn6BaFy2zPk81nLFCeDfvdcGZWZb5PAECYpvUuk+/vM5Ywq+ +OlOJZB72EDhonwssmI4IUAwXCAGNKjLfC4L+3ZgA3+I1xgxisJ2XWNYSLwHzIDRh +U3zO2NpJi1edTNPltDBTb4iFhajX0SFgbARc+XVTpA3pgQujWo6CNB5YKCPuzIqr +GFsvGSZDVzOUnfOlitaYNW+QIWAQf8VLWULwyFrS5Cb2WR/k7AmojZVuDHvzWrtg +ZMG6b1mBAoGBAOV+3SiX8+khKBpxnOJLq0XlGjFNDWNNB34UIMVkFehRxpUr2261 +HDp4YiC9n7l47sjeFBk4IQf2vG/5MPpuqIixsk2W3siCASdMQypVZMG+zj6xDFfH +8rwQSeZhwjmk2a+A7qgnhqvd/qa7EYOnsn1tLf2iBB2EaHV9lWBJFX0lAoGBANYD +GbAPEiwh4Fns8pf59T3Lp0Q9XvAN3uh4DUU0mFrQ1HQHeXIw1IDCJ9JiRjLX7aHu +79EtDssVPQ9dv0MN5rRULtrutCfRLsomm385PLLBIgBdVApnVvJJIWhQkFFMrhFt +UP+483utiDOcCVXMxAy+1jx23EiWvl2H0xGIwsSdAoGBAMIcM+OJ4vxk1w7G2fNu +HUfZJ/ZbPd+n35Z8X9uVdBI0WMsDdW6GMYIjIJygxuCRsSak8EsEdqvNvkTXeN3Z +iyNTaYTG/1iI3YDnuEeuQrK9OKU+CzqUHHOFM3xxY15uWNFhNHt2MypbcnCD+aRp +y0bbefL1fpWY0OHPfvEZ39shAoGAPbVdJc/irIkEGMni1YGEflIHo/ySMGO/f4aG +RQs6Vw1aBS7WjN+ZlprlQpuFpElwwr2TttvoJRS1q4WbjakneZ3AeO5VUhnWBQIG +2jNV1jEsLbC7d9h+UJRXpq18P4T9uBauQV5CDspluIPoiS3m5cntGjgnomKc93kf +mjG1/10CgYA7kgOOva64sjWakL/IgDRiwr0YrJkAfPUZYwxYLHuiW9izUgngpqWd +1wtq+YCsc4l7t8u9Tahb8OE0KSN5RC6QM6b8yW9qFDZ68QAX00+sN6di4qyAZlm+ +rK05W/3JmyvQbvO+JVRQtegZ1ExCj7LGuGOQ5KIpWsBEM3ic9ZP9gw== +-----END RSA PRIVATE KEY----- diff --git a/integration-cli/fixtures/notary/delgkey3.crt b/integration-cli/fixtures/notary/delgkey3.crt new file mode 100644 index 00000000..be34eab5 --- /dev/null +++ b/integration-cli/fixtures/notary/delgkey3.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIID8jCCAtqgAwIBAgIJAI3uONxeFQJtMA0GCSqGSIb3DQEBBQUAMFgxCzAJBgNV +BAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEPMA0G +A1UEChMGRG9ja2VyMRMwEQYDVQQDEwpkZWxlZ2F0aW9uMCAXDTE2MDMwODAyNDEy +NFoYDzIxMTYwMjEzMDI0MTI0WjBYMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0Ex +FjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xDzANBgNVBAoTBkRvY2tlcjETMBEGA1UE +AxMKZGVsZWdhdGlvbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOii +Ij01MkSExgurs5owUNxNgRvrZFxNSNGfnscYZiaT/wNcocrOz40vvY29SOBEbCSW +oBlCi0rYu/7LZBqvsP3YItmifpJHGfRiZ6xEQ4rKznY8+8E3FHVChlmVv9x6QPhA +9OpATlSLvcdiXHbohdc+kQsl9qM93+QadRQLmtZ6H5Sv90d1MHNViX+8d/k2WyT0 +8u6fNv0ZHeltnZFYruF82YKJCOPdAJnCLUOXWRSG6xDhhvSewjxz6gFla5n8m+D9 +jvmIUUjoMEhjORUIVeA/lXT0AT3Lx0xE8uyhJQbp+hGtcPCcwYFZdz3yLcrxKO47 +nh6qOygf7I2fiR1ogqECAwEAAaOBvDCBuTAdBgNVHQ4EFgQUUqsFJdVoos2aewDh +m1r66zyXeI4wgYkGA1UdIwSBgTB/gBRSqwUl1WiizZp7AOGbWvrrPJd4jqFcpFow +WDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNp +c2NvMQ8wDQYDVQQKEwZEb2NrZXIxEzARBgNVBAMTCmRlbGVnYXRpb26CCQCN7jjc +XhUCbTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQADcyno8/AwNatf +pjgILCZl1DVrqaKEVbp6ciNgVRCF4cM0bE5W4mjd5tO8d3/yTilry2FPicqxiMps +oGroMFR+X1cZbOf0U32FyEW4EyWm2jjbiuEpnM5J/EeB/QfckqP6whS/QAM7PxDV +Sxd8sKDb9SOGZiickFU4QpG1fdmY/knrrtbzRl7Nk/3tBgRaq+Brg7YNZZKlpUNB +Hp3q0E+MFgVAojpcL7w1oSgoNev+cUNaBdPEmWIEi7F5rosCzmAIhuIY+ghmo9Qg +zy+byAcxLpujl8vZvE1nZKMKZ7oJayOOgjB2Ztk6bO1r+GPtK5VfqEPhKTRDbBlo +xS3tSCDJ +-----END CERTIFICATE----- diff --git a/integration-cli/fixtures/notary/delgkey3.key b/integration-cli/fixtures/notary/delgkey3.key new file mode 100644 index 00000000..4790c957 --- /dev/null +++ b/integration-cli/fixtures/notary/delgkey3.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA6KIiPTUyRITGC6uzmjBQ3E2BG+tkXE1I0Z+exxhmJpP/A1yh +ys7PjS+9jb1I4ERsJJagGUKLSti7/stkGq+w/dgi2aJ+kkcZ9GJnrERDisrOdjz7 +wTcUdUKGWZW/3HpA+ED06kBOVIu9x2JcduiF1z6RCyX2oz3f5Bp1FAua1noflK/3 +R3Uwc1WJf7x3+TZbJPTy7p82/Rkd6W2dkViu4XzZgokI490AmcItQ5dZFIbrEOGG +9J7CPHPqAWVrmfyb4P2O+YhRSOgwSGM5FQhV4D+VdPQBPcvHTETy7KElBun6Ea1w +8JzBgVl3PfItyvEo7jueHqo7KB/sjZ+JHWiCoQIDAQABAoIBADvh8HpdBTGKFAjR +DAx2v3nWIZP0RgNUiZgcRJzvdOwdUJmm8KbqEZdAYMTpoqbINiY8971I2d5MaCgw +ZvZPn3nYdzAamgZBczbrVdCMSe6iQf9Bt3SHHycIFtlcqOSyO6Mr5V+fagptZk66 +zR52wG0l1+RMw25F8SogfV7JlfP7Qh5Bob0lEN2xpbhwLiNaaB+IHNe0FelmRvmJ +VUonoD0xaos25EXUES7J/9coiBqgRlDVHdUM0oaa/94UnxNPJnoNfte0yd+mC4LZ +JVHo0Zti3x/8SiCYMbLQs5L8AL8VtPu9OPfur/J8+9Rv0Rh+L1Ben+JWzCzUw1Cj +abH1zvkCgYEA9Q06Lu69ZLD31fTv46CphN+dGS/VgvMELkob6VQOhbV3RPhe6vqL +p7D67J53iq4rZY5KX3zuXZ+A5s48atc8gz+hTsrE022QVXmO2ZrE22bEpL+mwpsB +8//ul1UG51XTw6YR9CmLLD3Y4BgMjhSllx4Wwr9e9+PKl+DuSreqhxMCgYEA8wbf +P3zh85jPN92gBG8+VIbVlXYOTK0OllYUoLt4URmLRllmrB6LyRlpODCyb9+JymMg +WvAq5Bc0h8gMbSQEkYaAUq2CfSbyExASUHA+/nZglsTZhPkg5PJImntK6S58KAM7 +RJzyz20gxYA5H4KXFSiF+ONOE9X/cFUPxzF1AfsCgYBfgUY54GYEBkyxIIMWDhnD +ZXtOw6vNG3V3rP5v04jNZ8oSIVKs9fTT6FADREeGzxauv+QQjxo/dtjAG4TEhxpY +dMYjdTd8x2jHR1b7TCyI7eaZ5u/RTKRYOlj8tfC43GRqDiFVLZPGLFyIChdqkHVx +DhME15zls+vTgaCdkjNt7QKBgQCfwDywNx8wSZqtVnoBcD7AwYFUpi3wKTIVkLAu +mA0XAnuS2uGq8slgf9uynBAvifnBmDeEj6siFD7roozIkYyPPKLNtlC4hAlMjpv7 +VE2UZ6xGb0+tITaGSN2A7trnPS9P/g/PonvZ7hpEuWzTUbyOo/ytBn4ke99VsBSX +E+OeUQKBgQCgmcwCj2/IH+GOpe9qAG6MTMKK7k22O8fBCrcDybL1pMWIesJEbzpv +T5Atcx9L5ff6Q4Ysghb8ebXsErv4oZ72xyAwWJmbIaPllWn2ffUikzL3grSriWZy +0bz6P9sRqYpbdmX3oVvTfBP5kbv+mtDXOB3h5rGfczKWNMyuZmxDOg== +-----END RSA PRIVATE KEY----- diff --git a/integration-cli/fixtures/notary/delgkey4.crt b/integration-cli/fixtures/notary/delgkey4.crt new file mode 100644 index 00000000..42869c8e --- /dev/null +++ b/integration-cli/fixtures/notary/delgkey4.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIID8jCCAtqgAwIBAgIJAKKDRMrryBRKMA0GCSqGSIb3DQEBBQUAMFgxCzAJBgNV +BAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEPMA0G +A1UEChMGRG9ja2VyMRMwEQYDVQQDEwpkZWxlZ2F0aW9uMCAXDTE2MDMwODAyNDEy +N1oYDzIxMTYwMjEzMDI0MTI3WjBYMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0Ex +FjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xDzANBgNVBAoTBkRvY2tlcjETMBEGA1UE +AxMKZGVsZWdhdGlvbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOCf +Wfff5mX/ko/Y790O04eR7h8/4YtZU3LFItcjhkphMf2V2BRlhWwwW6v96gTN1xsZ +1il6/YXjviWiLjhrtOVLQBE2yK0A7Wwdh9KJg3QgNqwtFrR1MA1LgWto1F7NyEMC +9H6Hc95+bgWx1jN0IflfPh1C1m/sA5xGqHDl+8YzJJUOoa5bh04Yk3aIeecatso/ +z7P5c6KicPcZIjhgjxHYB95It/oj8ZuY0hQZb7B5HEGNyBbT2F0vuElWtp+mXexr +6mzgzvHgaKG36bNCTLxr8BxGA/sbVn01LyI3wpk2uqWzyUFk21M4g2X46OPgKrh7 +2h5b+C0X8DUPi45djHcCAwEAAaOBvDCBuTAdBgNVHQ4EFgQUKcrfRFg+6o2l4xbt +Ll6hV9pjJh8wgYkGA1UdIwSBgTB/gBQpyt9EWD7qjaXjFu0uXqFX2mMmH6FcpFow +WDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNp +c2NvMQ8wDQYDVQQKEwZEb2NrZXIxEzARBgNVBAMTCmRlbGVnYXRpb26CCQCig0TK +68gUSjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAhdKgYUQ36JSPS +f3Dws09pM5hzSsSae4+nG9XckX6dVQ7sLKmjeeeLrXuzjfygir/+h9cHyShgXFH4 +ZbGpdzf6APG1KRag3/njqEWi+kKZZduxZKvI2EHJhj1xBtf8Qru0TgS7bHPlp9bl +1/61+aIrtj05LQhqzWzehuJFrmSdWP9cnNbvlPdOdgfgkKakAiLGwwGNvMQbqxaO +FIB4UPuPdQgm5bpimd5/CThKbpK9/0nr9K4po/m519nvEKxZzsDw5tefGp9Xqly3 +4pk9uyAxO/E2cL0cVA/WHTVTsHPbO7lXxBi6/EjiTUi0Nj1X+btO8+jCLkJyNY0m +qaiL5k9h +-----END CERTIFICATE----- diff --git a/integration-cli/fixtures/notary/delgkey4.key b/integration-cli/fixtures/notary/delgkey4.key new file mode 100644 index 00000000..7573c208 --- /dev/null +++ b/integration-cli/fixtures/notary/delgkey4.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA4J9Z99/mZf+Sj9jv3Q7Th5HuHz/hi1lTcsUi1yOGSmEx/ZXY +FGWFbDBbq/3qBM3XGxnWKXr9heO+JaIuOGu05UtAETbIrQDtbB2H0omDdCA2rC0W +tHUwDUuBa2jUXs3IQwL0fodz3n5uBbHWM3Qh+V8+HULWb+wDnEaocOX7xjMklQ6h +rluHThiTdoh55xq2yj/Ps/lzoqJw9xkiOGCPEdgH3ki3+iPxm5jSFBlvsHkcQY3I +FtPYXS+4SVa2n6Zd7GvqbODO8eBoobfps0JMvGvwHEYD+xtWfTUvIjfCmTa6pbPJ +QWTbUziDZfjo4+AquHvaHlv4LRfwNQ+Ljl2MdwIDAQABAoIBAQCrN2wZsFZr2zK5 +aS/0/Y8poIe01Dm0wWMFcdULzm1XltzHIgfyDCx2ein2YPaXsNtNMhV3yuMiwqU3 +BHdc1GSv/vsX4/11Oea/6YaVafKEeuWRulC7PzRgffRpjh+LICqNQdxh8hfVOePd +fV/8GoKnFf0/yqmv6GQcJBPS8stGmFmjo4rkBGvBBMoiUtMYllQqdfH0DtpI24Jh +nR3lZKAPECkAciV7/Lx6+CUEaNOML2XPbLv6EyRh+J/r80jwE8myzpO7R6I+KCzo +R/xuBb/hrUh5Sd5YmuBMa6WfF9yqawTgmVvkpD9fkRusSPSQCq3oe+AugYWu6Fht +XBiZlvjJAoGBAPPBuUaagaUgHyjIzjbRPBHDhSYJpgYR4l/jcypfrl+m0OFC5acA +QG7Hr8AbStIPGtaJRn2pm8dNVPtFecPoi5jVWux2n5RqYlOnwY0tziuxbhU9GQ/W +oCp+99TJSMHFep0E7IoDk8YSxyA/86qk/Tx7KkUUlXv4sjJts17ZHxstAoGBAOvn +mF9rm8Y+Og17WlUQyf5j7g4soWG/4zMnoGpjocDfHVms/pASKbIBp5aFtDgWCmM5 +H7InptvBUInROHlooK6paJRDLbDgzVa/m+NLHoct7N25J4NiG8xV6Wv7hlrRp+XK +zyWL8iL95GnB21HJKvEiVBWvOuZnqfVcnzhbmzyzAoGAYT46jMkcyWRMKfgaFFJa +lXebybX1rtw5pClYC2KKbQxerk8C0SHPkqJFIe2BZtWxzj6LiZw9UkAuk+N+lUJT +VpBfKpCUTyA1w8vb8leAtXueQAjU07W6xdlLQ29dgDgpFzUcrF6K+G0LVXlN2xjh +EdzM2yxACmoHpQiQk1kpCK0CgYAz640Fs1FdmGR+gx+miUNr0eKbDAeY0/rVT2tm +/vai1HhJPGHqo5S5sNOJtXOsxG0U2YW4WDHJPArVyk57qiNzTaXOu9pai5+l8BYH +OIlHhzwSsKWZrQYhOudc9MblRi+Fy9U7lkl8mhSjkh8LKRNibwPCogZ8n2QwtGn2 +pXLNMQKBgQDxvs46CA0M9lGvpl0ggnC7bIYYUEvIlszlBh+o2CgF3IOFlGVcvCia +r18i7hTM5wbcct9OWDzZG4ejBIhtE+gMQ333ofQ64PPJOcfuHxT3Z/fMWfv/yDEj +4e4ZPK44ktcTvuusxAoSe5C5dbcNX2ymAhlRg/F0LyMkhw+qGh4xOQ== +-----END RSA PRIVATE KEY----- diff --git a/integration-cli/fixtures/notary/localhost.cert b/integration-cli/fixtures/notary/localhost.cert new file mode 100644 index 00000000..d1233a1b --- /dev/null +++ b/integration-cli/fixtures/notary/localhost.cert @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCTCCAfOgAwIBAgIQTOoFF+ypXwgdXnXHuCTvYDALBgkqhkiG9w0BAQswJjER +MA8GA1UEChMIUXVpY2tUTFMxETAPBgNVBAMTCFF1aWNrVExTMB4XDTE1MDcxNzE5 +NDg1M1oXDTE4MDcwMTE5NDg1M1owJzERMA8GA1UEChMIUXVpY2tUTFMxEjAQBgNV +BAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMDO +qvTBAi0ApXLfe90ApJkdkRGwF838Qzt1UFSxomu5fHRV6l3FjX5XCVHiFQ4w3ROh +dMOu9NahfGLJv9VvWU2MV3YoY9Y7lIXpKwnK1v064wuls4nPh13BUWKQKofcY/e2 +qaSPd6/qmSRc/kJUvOI9jZMSX6ZRPu9K4PCqm2CivlbLq9UYuo1AbRGfuqHRvTxg +mQG7WQCzGSvSjuSg5qX3TEh0HckTczJG9ODULNRWNE7ld0W4sfv4VF8R7Uc/G7LO +8QwLCZ9TIl3gYMPCrhUL3Q6z9Jnn1SQS4mhDnPi6ugRYO1X8k3jjdxV9C2sXwUvN +OZI1rLEWl9TJNA7ZXtMCAwEAAaM2MDQwDgYDVR0PAQH/BAQDAgCgMAwGA1UdEwEB +/wQCMAAwFAYDVR0RBA0wC4IJbG9jYWxob3N0MAsGCSqGSIb3DQEBCwOCAQEAH6iq +kM2+UMukGDLEQKHHiauioWJlHDlLXv76bJiNfjSz94B/2XOQMb9PT04//tnGUyPK +K8Dx7RoxSodU6T5VRiz/A36mLOvt2t3bcL/1nHf9sAOHcexGtnCbQbW91V7RKfIL +sjiLNFDkQ9VfVNY+ynQptZoyH1sy07+dplfkIiPzRs5WuVAnEGsX3r6BrhgUITzi +g1B4kpmGZIohP4m6ZEBY5xuo/NQ0+GhjAENQMU38GpuoMyFS0i0dGcbx8weqnI/B +Er/qa0+GE/rBnWY8TiRow8dzpneSFQnUZpJ4EwD9IoOIDHo7k2Nbz2P50HMiCXZf +4RqzctVssRlrRVnO5w== +-----END CERTIFICATE----- diff --git a/integration-cli/fixtures/notary/localhost.key b/integration-cli/fixtures/notary/localhost.key new file mode 100644 index 00000000..d7778359 --- /dev/null +++ b/integration-cli/fixtures/notary/localhost.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAwM6q9MECLQClct973QCkmR2REbAXzfxDO3VQVLGia7l8dFXq +XcWNflcJUeIVDjDdE6F0w6701qF8Ysm/1W9ZTYxXdihj1juUhekrCcrW/TrjC6Wz +ic+HXcFRYpAqh9xj97appI93r+qZJFz+QlS84j2NkxJfplE+70rg8KqbYKK+Vsur +1Ri6jUBtEZ+6odG9PGCZAbtZALMZK9KO5KDmpfdMSHQdyRNzMkb04NQs1FY0TuV3 +Rbix+/hUXxHtRz8bss7xDAsJn1MiXeBgw8KuFQvdDrP0mefVJBLiaEOc+Lq6BFg7 +VfyTeON3FX0LaxfBS805kjWssRaX1Mk0Dtle0wIDAQABAoIBAHbuhNHZROhRn70O +Ui9vOBki/dt1ThnH5AkHQngb4t6kWjrAzILvW2p1cdBKr0ZDqftz+rzCbVD/5+Rg +Iq8bsnB9g23lWEBMHD/GJsAxmRA3hNooamk11IBmwTcVSsbnkdq5mEdkICYphjHC +Ey0DbEf6RBxWlx3WvAWLoNmTw6iFaOCH8IyLavPpe7kLbZc219oNUw2qjCnCXCZE +/NuViADHJBPN8r7g1gmyclJmTumdUK6oHgXEMMPe43vhReGcgcReK9QZjnTcIXPM +4oJOraw+BtoZXVvvIPnC+5ntoLFOzjIzM0kaveReZbdgffqF4zy2vRfCHhWssanc +7a0xR4ECgYEA3Xuvcqy5Xw+v/jVCO0VZj++Z7apA78dY4tWsPx5/0DUTTziTlXkC +ADduEbwX6HgZ/iLvA9j4C3Z4mO8qByby/6UoBU8NEe+PQt6fT7S+dKSP4uy5ZxVM +i5opkEyrJsMbve9Jrlj4bk5CICsydrZ+SBFHnpNGjbduGQick5LORWECgYEA3trt +gepteDGiUYmnnBgjbYtcD11RvpKC8Z/QwGnzN5vk4eBu8r7DkMcLN+SiHjAovlJo +r5j3EbF8sla1zBf/yySdQZFqUGcwtw7MaAKCLdhQl5WsViNMIx6p2OJapu0dzbv2 +KTXrnoRCafcH92k0dUX1ahE9eyc8KX6VhbWwXLMCgYATGCCuEDoC+gVAMzM8jOQF +xrBMjwr+IP+GvskUv/pg5tJ9V/FRR5dmkWDJ4p9lCUWkZTqZ6FCqHFKVTLkg2LjG +VWS34HLOAwskxrCRXJG22KEW/TWWr31j46yFpjZzJwrzOvftMfpo+BI3V8IH/f+x +EtxLzYKdoRy6x8VH67YgwQKBgHor2vjV45142FuK83AHa6SqOZXSuvWWrGJ6Ep7p +doSN2jRaLXi2S9AaznOdy6JxFGUCGJHrcccpXgsGrjNtFLXxJKTFa1sYtwQkALsk +ZOltJQF09D1krGC0driHntrUMvqOiKye+sS0DRS6cIuaCUAhUiELwoC5SaoV0zKy +IDUxAoGAOK8Xq+3/sqe79vTpw25RXl+nkAmOAeKjqf3Kh6jbnBhr81rmefyKXB9a +uj0b980tzUnliwA5cCOsyxfN2vASvMnJxFE721QZI04arlcPFHcFqCtmNnUYTcLp +0hgn/yLZptcoxpy+eTBu3eNsxz1Bu/Tx/198+2Wr3MbtGpLNIcA= +-----END RSA PRIVATE KEY----- diff --git a/integration-cli/fixtures/registry/cert.pem b/integration-cli/fixtures/registry/cert.pem new file mode 100644 index 00000000..37605403 --- /dev/null +++ b/integration-cli/fixtures/registry/cert.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDfzCCAmegAwIBAgIJAKZjzF7N4zFJMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV +BAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQg +Q29tcGFueSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNjAzMTQxOTAzMDZa +Fw0xNzAzMTQxOTAzMDZaMFYxCzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0 +IENpdHkxHDAaBgNVBAoME0RlZmF1bHQgQ29tcGFueSBMdGQxEjAQBgNVBAMMCWxv +Y2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMAVEPA6tSNy +MoExHvT8CWvbe0MyYqZjMmUUdGVYyAaoZgmj9HvtGKaUWY/hCtgTond3OKhPq69u +fQSDlHQA/scq4KZovKQJhvBaRb2DqD31KcbcDyh5KUAL1aalbjTLbKmAYSFSoY93 +57KiBei2BmvS55HLhOiO8ccQOq3feH/J/XcszAdAaiGXW3woDOIumYzur6Q8Suyn +cIUEX5Ik7mxS7oGYN1IM++Y+B6aAFT7htAZEvF7RF7sjG7QBfxNPOFg9lBWXzVSv +0vRbVme9OCDD2QOpj8O7XAPuLDwW5b2A8Iex3CJRngBI9vAK5h1Wssst8117bur9 +AiubOrF6cxUCAwEAAaNQME4wHQYDVR0OBBYEFNTGYK7uX19yjCPeGXhmel98amoA +MB8GA1UdIwQYMBaAFNTGYK7uX19yjCPeGXhmel98amoAMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQELBQADggEBACW/oF6RgLbTPxb8oPI9424Uv/erYYdxdqIaO3Mz +fQfBEvGu62A0ZLH+av4BTeqBM6iVhN6/Y3hUb8UzbbZAIo/dVJSglW7PXAfUITMM +ca9U2r2cFqgXELZkhde6mTFTYwM3swMCP0HUEo+Hu62NX5gunKr4QMNfTlE3vHEj +jitnkTR0ZVEKHvmdTJC9S92j+NuaJVcwe5UNP1Nj/Ksd/iUUCa2DBnw2N7YwHTDB +jb9cQb8aNVNSrjKP3sknMslVy1JVbUB1LXsth/h+kkVFNP4dsk+dZHn20uIA/VeJ +mJ3Wo54CeTAa3DysiWbIIYsFSASCPvki08ZKI373tCf2RvE= +-----END CERTIFICATE----- diff --git a/integration-cli/npipe.go b/integration-cli/npipe.go new file mode 100644 index 00000000..fa531a1b --- /dev/null +++ b/integration-cli/npipe.go @@ -0,0 +1,12 @@ +// +build !windows + +package main + +import ( + "net" + "time" +) + +func npipeDial(path string, timeout time.Duration) (net.Conn, error) { + panic("npipe protocol only supported on Windows") +} diff --git a/integration-cli/npipe_windows.go b/integration-cli/npipe_windows.go new file mode 100644 index 00000000..4fd735f2 --- /dev/null +++ b/integration-cli/npipe_windows.go @@ -0,0 +1,12 @@ +package main + +import ( + "net" + "time" + + "github.com/Microsoft/go-winio" +) + +func npipeDial(path string, timeout time.Duration) (net.Conn, error) { + return winio.DialPipe(path, &timeout) +} diff --git a/integration-cli/registry.go b/integration-cli/registry.go new file mode 100644 index 00000000..fa3fb875 --- /dev/null +++ b/integration-cli/registry.go @@ -0,0 +1,175 @@ +package main + +import ( + "fmt" + "io/ioutil" + "net/http" + "os" + "os/exec" + "path/filepath" + + "github.com/docker/distribution/digest" + "github.com/go-check/check" +) + +const ( + v2binary = "registry-v2" + v2binarySchema1 = "registry-v2-schema1" +) + +type testRegistryV2 struct { + cmd *exec.Cmd + dir string + auth string + username string + password string + email string +} + +func newTestRegistryV2(c *check.C, schema1 bool, auth, tokenURL string) (*testRegistryV2, error) { + tmp, err := ioutil.TempDir("", "registry-test-") + if err != nil { + return nil, err + } + template := `version: 0.1 +loglevel: debug +storage: + filesystem: + rootdirectory: %s +http: + addr: %s +%s` + var ( + authTemplate string + username string + password string + email string + ) + switch auth { + case "htpasswd": + htpasswdPath := filepath.Join(tmp, "htpasswd") + // generated with: htpasswd -Bbn testuser testpassword + userpasswd := "testuser:$2y$05$sBsSqk0OpSD1uTZkHXc4FeJ0Z70wLQdAX/82UiHuQOKbNbBrzs63m" + username = "testuser" + password = "testpassword" + email = "test@test.org" + if err := ioutil.WriteFile(htpasswdPath, []byte(userpasswd), os.FileMode(0644)); err != nil { + return nil, err + } + authTemplate = fmt.Sprintf(`auth: + htpasswd: + realm: basic-realm + path: %s +`, htpasswdPath) + case "token": + authTemplate = fmt.Sprintf(`auth: + token: + realm: %s + service: "registry" + issuer: "auth-registry" + rootcertbundle: "fixtures/registry/cert.pem" +`, tokenURL) + } + + confPath := filepath.Join(tmp, "config.yaml") + config, err := os.Create(confPath) + if err != nil { + return nil, err + } + if _, err := fmt.Fprintf(config, template, tmp, privateRegistryURL, authTemplate); err != nil { + os.RemoveAll(tmp) + return nil, err + } + + binary := v2binary + if schema1 { + binary = v2binarySchema1 + } + cmd := exec.Command(binary, confPath) + if err := cmd.Start(); err != nil { + os.RemoveAll(tmp) + if os.IsNotExist(err) { + c.Skip(err.Error()) + } + return nil, err + } + return &testRegistryV2{ + cmd: cmd, + dir: tmp, + auth: auth, + username: username, + password: password, + email: email, + }, nil +} + +func (t *testRegistryV2) Ping() error { + // We always ping through HTTP for our test registry. + resp, err := http.Get(fmt.Sprintf("http://%s/v2/", privateRegistryURL)) + if err != nil { + return err + } + resp.Body.Close() + + fail := resp.StatusCode != http.StatusOK + if t.auth != "" { + // unauthorized is a _good_ status when pinging v2/ and it needs auth + fail = fail && resp.StatusCode != http.StatusUnauthorized + } + if fail { + return fmt.Errorf("registry ping replied with an unexpected status code %d", resp.StatusCode) + } + return nil +} + +func (t *testRegistryV2) Close() { + t.cmd.Process.Kill() + os.RemoveAll(t.dir) +} + +func (t *testRegistryV2) getBlobFilename(blobDigest digest.Digest) string { + // Split the digest into it's algorithm and hex components. + dgstAlg, dgstHex := blobDigest.Algorithm(), blobDigest.Hex() + + // The path to the target blob data looks something like: + // baseDir + "docker/registry/v2/blobs/sha256/a3/a3ed...46d4/data" + return fmt.Sprintf("%s/docker/registry/v2/blobs/%s/%s/%s/data", t.dir, dgstAlg, dgstHex[:2], dgstHex) +} + +func (t *testRegistryV2) readBlobContents(c *check.C, blobDigest digest.Digest) []byte { + // Load the target manifest blob. + manifestBlob, err := ioutil.ReadFile(t.getBlobFilename(blobDigest)) + if err != nil { + c.Fatalf("unable to read blob: %s", err) + } + + return manifestBlob +} + +func (t *testRegistryV2) writeBlobContents(c *check.C, blobDigest digest.Digest, data []byte) { + if err := ioutil.WriteFile(t.getBlobFilename(blobDigest), data, os.FileMode(0644)); err != nil { + c.Fatalf("unable to write malicious data blob: %s", err) + } +} + +func (t *testRegistryV2) tempMoveBlobData(c *check.C, blobDigest digest.Digest) (undo func()) { + tempFile, err := ioutil.TempFile("", "registry-temp-blob-") + if err != nil { + c.Fatalf("unable to get temporary blob file: %s", err) + } + tempFile.Close() + + blobFilename := t.getBlobFilename(blobDigest) + + // Move the existing data file aside, so that we can replace it with a + // another blob of data. + if err := os.Rename(blobFilename, tempFile.Name()); err != nil { + os.Remove(tempFile.Name()) + c.Fatalf("unable to move data blob: %s", err) + } + + return func() { + os.Rename(tempFile.Name(), blobFilename) + os.Remove(tempFile.Name()) + } +} diff --git a/integration-cli/registry_mock.go b/integration-cli/registry_mock.go new file mode 100644 index 00000000..300bf464 --- /dev/null +++ b/integration-cli/registry_mock.go @@ -0,0 +1,55 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "regexp" + "strings" + "sync" + + "github.com/go-check/check" +) + +type handlerFunc func(w http.ResponseWriter, r *http.Request) + +type testRegistry struct { + server *httptest.Server + hostport string + handlers map[string]handlerFunc + mu sync.Mutex +} + +func (tr *testRegistry) registerHandler(path string, h handlerFunc) { + tr.mu.Lock() + defer tr.mu.Unlock() + tr.handlers[path] = h +} + +func newTestRegistry(c *check.C) (*testRegistry, error) { + testReg := &testRegistry{handlers: make(map[string]handlerFunc)} + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + url := r.URL.String() + + var matched bool + var err error + for re, function := range testReg.handlers { + matched, err = regexp.MatchString(re, url) + if err != nil { + c.Fatal("Error with handler regexp") + } + if matched { + function(w, r) + break + } + } + + if !matched { + c.Fatalf("Unable to match %s with regexp", url) + } + })) + + testReg.server = ts + testReg.hostport = strings.Replace(ts.URL, "http://", "", 1) + return testReg, nil +} diff --git a/integration-cli/requirements.go b/integration-cli/requirements.go new file mode 100644 index 00000000..e948c0aa --- /dev/null +++ b/integration-cli/requirements.go @@ -0,0 +1,189 @@ +package main + +import ( + "fmt" + "io/ioutil" + "net/http" + "os" + "os/exec" + "strings" + "time" + + "github.com/go-check/check" +) + +type testCondition func() bool + +type testRequirement struct { + Condition testCondition + SkipMessage string +} + +// List test requirements +var ( + DaemonIsWindows = testRequirement{ + func() bool { return daemonPlatform == "windows" }, + "Test requires a Windows daemon", + } + DaemonIsLinux = testRequirement{ + func() bool { return daemonPlatform == "linux" }, + "Test requires a Linux daemon", + } + NotArm = testRequirement{ + func() bool { return os.Getenv("DOCKER_ENGINE_GOARCH") != "arm" }, + "Test requires a daemon not running on ARM", + } + NotPpc64le = testRequirement{ + func() bool { return os.Getenv("DOCKER_ENGINE_GOARCH") != "ppc64le" }, + "Test requires a daemon not running on ppc64le", + } + SameHostDaemon = testRequirement{ + func() bool { return isLocalDaemon }, + "Test requires docker daemon to run on the same machine as CLI", + } + UnixCli = testRequirement{ + func() bool { return isUnixCli }, + "Test requires posix utilities or functionality to run.", + } + ExecSupport = testRequirement{ + func() bool { return supportsExec }, + "Test requires 'docker exec' capabilities on the tested daemon.", + } + Network = testRequirement{ + func() bool { + // Set a timeout on the GET at 15s + var timeout = time.Duration(15 * time.Second) + var url = "https://hub.docker.com" + + client := http.Client{ + Timeout: timeout, + } + + resp, err := client.Get(url) + if err != nil && strings.Contains(err.Error(), "use of closed network connection") { + panic(fmt.Sprintf("Timeout for GET request on %s", url)) + } + if resp != nil { + resp.Body.Close() + } + return err == nil + }, + "Test requires network availability, environment variable set to none to run in a non-network enabled mode.", + } + Apparmor = testRequirement{ + func() bool { + buf, err := ioutil.ReadFile("/sys/module/apparmor/parameters/enabled") + return err == nil && len(buf) > 1 && buf[0] == 'Y' + }, + "Test requires apparmor is enabled.", + } + RegistryHosting = testRequirement{ + func() bool { + // for now registry binary is built only if we're running inside + // container through `make test`. Figure that out by testing if + // registry binary is in PATH. + _, err := exec.LookPath(v2binary) + return err == nil + }, + fmt.Sprintf("Test requires an environment that can host %s in the same host", v2binary), + } + NotaryHosting = testRequirement{ + func() bool { + // for now notary binary is built only if we're running inside + // container through `make test`. Figure that out by testing if + // notary-server binary is in PATH. + _, err := exec.LookPath(notaryServerBinary) + return err == nil + }, + fmt.Sprintf("Test requires an environment that can host %s in the same host", notaryServerBinary), + } + NotaryServerHosting = testRequirement{ + func() bool { + // for now notary-server binary is built only if we're running inside + // container through `make test`. Figure that out by testing if + // notary-server binary is in PATH. + _, err := exec.LookPath(notaryServerBinary) + return err == nil + }, + fmt.Sprintf("Test requires an environment that can host %s in the same host", notaryServerBinary), + } + NotOverlay = testRequirement{ + func() bool { + return !strings.HasPrefix(daemonStorageDriver, "overlay") + }, + "Test requires underlying root filesystem not be backed by overlay.", + } + + Devicemapper = testRequirement{ + func() bool { + return strings.HasPrefix(daemonStorageDriver, "devicemapper") + }, + "Test requires underlying root filesystem to be backed by devicemapper.", + } + + IPv6 = testRequirement{ + func() bool { + cmd := exec.Command("test", "-f", "/proc/net/if_inet6") + + if err := cmd.Run(); err != nil { + return true + } + return false + }, + "Test requires support for IPv6", + } + NotGCCGO = testRequirement{ + func() bool { + out, err := exec.Command("go", "version").Output() + if err == nil && strings.Contains(string(out), "gccgo") { + return false + } + return true + }, + "Test requires native Golang compiler instead of GCCGO", + } + UserNamespaceInKernel = testRequirement{ + func() bool { + if _, err := os.Stat("/proc/self/uid_map"); os.IsNotExist(err) { + /* + * This kernel-provided file only exists if user namespaces are + * supported + */ + return false + } + + // We need extra check on redhat based distributions + if f, err := os.Open("/sys/module/user_namespace/parameters/enable"); err == nil { + b := make([]byte, 1) + _, _ = f.Read(b) + if string(b) == "N" { + return false + } + return true + } + + return true + }, + "Kernel must have user namespaces configured and enabled.", + } + NotUserNamespace = testRequirement{ + func() bool { + root := os.Getenv("DOCKER_REMAP_ROOT") + if root != "" { + return false + } + return true + }, + "Test cannot be run when remapping root", + } +) + +// testRequires checks if the environment satisfies the requirements +// for the test to run or skips the tests. +func testRequires(c *check.C, requirements ...testRequirement) { + for _, r := range requirements { + if !r.Condition() { + c.Skip(r.SkipMessage) + } + } +} diff --git a/integration-cli/requirements_unix.go b/integration-cli/requirements_unix.go new file mode 100644 index 00000000..edc7bc1f --- /dev/null +++ b/integration-cli/requirements_unix.go @@ -0,0 +1,106 @@ +// +build !windows + +package main + +import ( + "github.com/docker/docker/pkg/sysinfo" +) + +var ( + // SysInfo stores information about which features a kernel supports. + SysInfo *sysinfo.SysInfo + cpuCfsPeriod = testRequirement{ + func() bool { + return SysInfo.CPUCfsPeriod + }, + "Test requires an environment that supports cgroup cfs period.", + } + cpuCfsQuota = testRequirement{ + func() bool { + return SysInfo.CPUCfsQuota + }, + "Test requires an environment that supports cgroup cfs quota.", + } + cpuShare = testRequirement{ + func() bool { + return SysInfo.CPUShares + }, + "Test requires an environment that supports cgroup cpu shares.", + } + oomControl = testRequirement{ + func() bool { + return SysInfo.OomKillDisable + }, + "Test requires Oom control enabled.", + } + pidsLimit = testRequirement{ + func() bool { + return SysInfo.PidsLimit + }, + "Test requires pids limit enabled.", + } + kernelMemorySupport = testRequirement{ + func() bool { + return SysInfo.KernelMemory + }, + "Test requires an environment that supports cgroup kernel memory.", + } + memoryLimitSupport = testRequirement{ + func() bool { + return SysInfo.MemoryLimit + }, + "Test requires an environment that supports cgroup memory limit.", + } + memoryReservationSupport = testRequirement{ + func() bool { + return SysInfo.MemoryReservation + }, + "Test requires an environment that supports cgroup memory reservation.", + } + swapMemorySupport = testRequirement{ + func() bool { + return SysInfo.SwapLimit + }, + "Test requires an environment that supports cgroup swap memory limit.", + } + memorySwappinessSupport = testRequirement{ + func() bool { + return SysInfo.MemorySwappiness + }, + "Test requires an environment that supports cgroup memory swappiness.", + } + blkioWeight = testRequirement{ + func() bool { + return SysInfo.BlkioWeight + }, + "Test requires an environment that supports blkio weight.", + } + cgroupCpuset = testRequirement{ + func() bool { + return SysInfo.Cpuset + }, + "Test requires an environment that supports cgroup cpuset.", + } + seccompEnabled = testRequirement{ + func() bool { + return supportsSeccomp && SysInfo.Seccomp + }, + "Test requires that seccomp support be enabled in the daemon.", + } + bridgeNfIptables = testRequirement{ + func() bool { + return !SysInfo.BridgeNFCallIPTablesDisabled + }, + "Test requires that bridge-nf-call-iptables support be enabled in the daemon.", + } + bridgeNfIP6tables = testRequirement{ + func() bool { + return !SysInfo.BridgeNFCallIP6TablesDisabled + }, + "Test requires that bridge-nf-call-ip6tables support be enabled in the daemon.", + } +) + +func init() { + SysInfo = sysinfo.New(true) +} diff --git a/integration-cli/test_vars_exec.go b/integration-cli/test_vars_exec.go new file mode 100644 index 00000000..7633b346 --- /dev/null +++ b/integration-cli/test_vars_exec.go @@ -0,0 +1,8 @@ +// +build !test_no_exec + +package main + +const ( + // indicates docker daemon tested supports 'docker exec' + supportsExec = true +) diff --git a/integration-cli/test_vars_noexec.go b/integration-cli/test_vars_noexec.go new file mode 100644 index 00000000..08450905 --- /dev/null +++ b/integration-cli/test_vars_noexec.go @@ -0,0 +1,8 @@ +// +build test_no_exec + +package main + +const ( + // indicates docker daemon tested supports 'docker exec' + supportsExec = false +) diff --git a/integration-cli/test_vars_noseccomp.go b/integration-cli/test_vars_noseccomp.go new file mode 100644 index 00000000..2f47ab07 --- /dev/null +++ b/integration-cli/test_vars_noseccomp.go @@ -0,0 +1,8 @@ +// +build !seccomp + +package main + +const ( + // indicates docker daemon built with seccomp support + supportsSeccomp = false +) diff --git a/integration-cli/test_vars_seccomp.go b/integration-cli/test_vars_seccomp.go new file mode 100644 index 00000000..00cf6972 --- /dev/null +++ b/integration-cli/test_vars_seccomp.go @@ -0,0 +1,8 @@ +// +build seccomp + +package main + +const ( + // indicates docker daemon built with seccomp support + supportsSeccomp = true +) diff --git a/integration-cli/test_vars_unix.go b/integration-cli/test_vars_unix.go new file mode 100644 index 00000000..853889ab --- /dev/null +++ b/integration-cli/test_vars_unix.go @@ -0,0 +1,16 @@ +// +build !windows + +package main + +const ( + // identifies if test suite is running on a unix platform + isUnixCli = true + + expectedFileChmod = "-rw-r--r--" + + // On Unix variants, the busybox image comes with the `top` command which + // runs indefinitely while still being interruptible by a signal. + defaultSleepImage = "busybox" +) + +var defaultSleepCommand = []string{"top"} diff --git a/integration-cli/test_vars_windows.go b/integration-cli/test_vars_windows.go new file mode 100644 index 00000000..ff51f89b --- /dev/null +++ b/integration-cli/test_vars_windows.go @@ -0,0 +1,18 @@ +// +build windows + +package main + +const ( + // identifies if test suite is running on a unix platform + isUnixCli = false + + // this is the expected file permission set on windows: gh#11395 + expectedFileChmod = "-rwxr-xr-x" + + // On Windows, the busybox image doesn't have the `top` command, so we rely + // on `sleep` with a high duration. + defaultSleepImage = "busybox" +) + +// TODO Windows: In TP5, decrease this sleep time, as performance will be better +var defaultSleepCommand = []string{"sleep", "240"} diff --git a/integration-cli/trust_server.go b/integration-cli/trust_server.go new file mode 100644 index 00000000..77314c39 --- /dev/null +++ b/integration-cli/trust_server.go @@ -0,0 +1,336 @@ +package main + +import ( + "fmt" + "io/ioutil" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/pkg/integration/checker" + "github.com/docker/docker/pkg/tlsconfig" + "github.com/go-check/check" +) + +var notaryBinary = "notary" +var notaryServerBinary = "notary-server" + +type keyPair struct { + Public string + Private string +} + +type testNotary struct { + cmd *exec.Cmd + dir string + keys []keyPair +} + +const notaryHost = "localhost:4443" +const notaryURL = "https://" + notaryHost + +func newTestNotary(c *check.C) (*testNotary, error) { + // generate server config + template := `{ + "server": { + "http_addr": "%s", + "tls_key_file": "%s", + "tls_cert_file": "%s" + }, + "trust_service": { + "type": "local", + "hostname": "", + "port": "", + "key_algorithm": "ed25519" + }, + "logging": { + "level": "debug" + }, + "storage": { + "backend": "memory" + } +}` + tmp, err := ioutil.TempDir("", "notary-test-") + if err != nil { + return nil, err + } + confPath := filepath.Join(tmp, "config.json") + config, err := os.Create(confPath) + defer config.Close() + if err != nil { + return nil, err + } + + workingDir, err := os.Getwd() + if err != nil { + return nil, err + } + if _, err := fmt.Fprintf(config, template, notaryHost, filepath.Join(workingDir, "fixtures/notary/localhost.key"), filepath.Join(workingDir, "fixtures/notary/localhost.cert")); err != nil { + os.RemoveAll(tmp) + return nil, err + } + + // generate client config + clientConfPath := filepath.Join(tmp, "client-config.json") + clientConfig, err := os.Create(clientConfPath) + defer clientConfig.Close() + if err != nil { + return nil, err + } + template = `{ + "trust_dir" : "%s", + "remote_server": { + "url": "%s", + "skipTLSVerify": true + } +}` + if _, err = fmt.Fprintf(clientConfig, template, filepath.Join(cliconfig.ConfigDir(), "trust"), notaryURL); err != nil { + os.RemoveAll(tmp) + return nil, err + } + + // load key fixture filenames + var keys []keyPair + for i := 1; i < 5; i++ { + keys = append(keys, keyPair{ + Public: filepath.Join(workingDir, fmt.Sprintf("fixtures/notary/delgkey%v.crt", i)), + Private: filepath.Join(workingDir, fmt.Sprintf("fixtures/notary/delgkey%v.key", i)), + }) + } + + // run notary-server + cmd := exec.Command(notaryServerBinary, "-config", confPath) + if err := cmd.Start(); err != nil { + os.RemoveAll(tmp) + if os.IsNotExist(err) { + c.Skip(err.Error()) + } + return nil, err + } + + testNotary := &testNotary{ + cmd: cmd, + dir: tmp, + keys: keys, + } + + // Wait for notary to be ready to serve requests. + for i := 1; i <= 20; i++ { + if err = testNotary.Ping(); err == nil { + break + } + time.Sleep(10 * time.Millisecond * time.Duration(i*i)) + } + + if err != nil { + c.Fatalf("Timeout waiting for test notary to become available: %s", err) + } + + return testNotary, nil +} + +func (t *testNotary) Ping() error { + tlsConfig := tlsconfig.ClientDefault + tlsConfig.InsecureSkipVerify = true + client := http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + TLSClientConfig: &tlsConfig, + }, + } + resp, err := client.Get(fmt.Sprintf("%s/v2/", notaryURL)) + if err != nil { + return err + } + if resp.StatusCode != 200 { + return fmt.Errorf("notary ping replied with an unexpected status code %d", resp.StatusCode) + } + return nil +} + +func (t *testNotary) Close() { + t.cmd.Process.Kill() + os.RemoveAll(t.dir) +} + +func (s *DockerTrustSuite) trustedCmd(cmd *exec.Cmd) { + pwd := "12345678" + trustCmdEnv(cmd, notaryURL, pwd, pwd) +} + +func (s *DockerTrustSuite) trustedCmdWithServer(cmd *exec.Cmd, server string) { + pwd := "12345678" + trustCmdEnv(cmd, server, pwd, pwd) +} + +func (s *DockerTrustSuite) trustedCmdWithPassphrases(cmd *exec.Cmd, rootPwd, repositoryPwd string) { + trustCmdEnv(cmd, notaryURL, rootPwd, repositoryPwd) +} + +func (s *DockerTrustSuite) trustedCmdWithDeprecatedEnvPassphrases(cmd *exec.Cmd, offlinePwd, taggingPwd string) { + trustCmdDeprecatedEnv(cmd, notaryURL, offlinePwd, taggingPwd) +} + +func trustCmdEnv(cmd *exec.Cmd, server, rootPwd, repositoryPwd string) { + env := []string{ + "DOCKER_CONTENT_TRUST=1", + fmt.Sprintf("DOCKER_CONTENT_TRUST_SERVER=%s", server), + fmt.Sprintf("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE=%s", rootPwd), + fmt.Sprintf("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE=%s", repositoryPwd), + } + cmd.Env = append(os.Environ(), env...) +} + +// Helper method to test the old env variables OFFLINE and TAGGING that will +// be deprecated by 1.10 +func trustCmdDeprecatedEnv(cmd *exec.Cmd, server, offlinePwd, taggingPwd string) { + env := []string{ + "DOCKER_CONTENT_TRUST=1", + fmt.Sprintf("DOCKER_CONTENT_TRUST_SERVER=%s", server), + fmt.Sprintf("DOCKER_CONTENT_TRUST_OFFLINE_PASSPHRASE=%s", offlinePwd), + fmt.Sprintf("DOCKER_CONTENT_TRUST_TAGGING_PASSPHRASE=%s", taggingPwd), + } + cmd.Env = append(os.Environ(), env...) +} + +func (s *DockerTrustSuite) setupTrustedImage(c *check.C, name string) string { + repoName := fmt.Sprintf("%v/dockercli/%s:latest", privateRegistryURL, name) + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", repoName) + + pushCmd := exec.Command(dockerBinary, "push", repoName) + s.trustedCmd(pushCmd) + out, _, err := runCommandWithOutput(pushCmd) + + if err != nil { + c.Fatalf("Error running trusted push: %s\n%s", err, out) + } + if !strings.Contains(string(out), "Signing and pushing trust metadata") { + c.Fatalf("Missing expected output on trusted push:\n%s", out) + } + + if out, status := dockerCmd(c, "rmi", repoName); status != 0 { + c.Fatalf("Error removing image %q\n%s", repoName, out) + } + + return repoName +} + +func notaryClientEnv(cmd *exec.Cmd) { + pwd := "12345678" + env := []string{ + fmt.Sprintf("NOTARY_ROOT_PASSPHRASE=%s", pwd), + fmt.Sprintf("NOTARY_TARGETS_PASSPHRASE=%s", pwd), + fmt.Sprintf("NOTARY_SNAPSHOT_PASSPHRASE=%s", pwd), + fmt.Sprintf("NOTARY_DELEGATION_PASSPHRASE=%s", pwd), + } + cmd.Env = append(os.Environ(), env...) +} + +func (s *DockerTrustSuite) notaryInitRepo(c *check.C, repoName string) { + initCmd := exec.Command(notaryBinary, "-c", filepath.Join(s.not.dir, "client-config.json"), "init", repoName) + notaryClientEnv(initCmd) + out, _, err := runCommandWithOutput(initCmd) + if err != nil { + c.Fatalf("Error initializing notary repository: %s\n", out) + } +} + +func (s *DockerTrustSuite) notaryCreateDelegation(c *check.C, repoName, role string, pubKey string, paths ...string) { + pathsArg := "--all-paths" + if len(paths) > 0 { + pathsArg = "--paths=" + strings.Join(paths, ",") + } + + delgCmd := exec.Command(notaryBinary, "-c", filepath.Join(s.not.dir, "client-config.json"), + "delegation", "add", repoName, role, pubKey, pathsArg) + notaryClientEnv(delgCmd) + out, _, err := runCommandWithOutput(delgCmd) + if err != nil { + c.Fatalf("Error adding %s role to notary repository: %s\n", role, out) + } +} + +func (s *DockerTrustSuite) notaryPublish(c *check.C, repoName string) { + pubCmd := exec.Command(notaryBinary, "-c", filepath.Join(s.not.dir, "client-config.json"), "publish", repoName) + notaryClientEnv(pubCmd) + out, _, err := runCommandWithOutput(pubCmd) + if err != nil { + c.Fatalf("Error publishing notary repository: %s\n", out) + } +} + +func (s *DockerTrustSuite) notaryImportKey(c *check.C, repoName, role string, privKey string) { + impCmd := exec.Command(notaryBinary, "-c", filepath.Join(s.not.dir, "client-config.json"), "key", + "import", privKey, "-g", repoName, "-r", role) + notaryClientEnv(impCmd) + out, _, err := runCommandWithOutput(impCmd) + if err != nil { + c.Fatalf("Error importing key to notary repository: %s\n", out) + } +} + +func (s *DockerTrustSuite) notaryListTargetsInRole(c *check.C, repoName, role string) map[string]string { + listCmd := exec.Command(notaryBinary, "-c", filepath.Join(s.not.dir, "client-config.json"), "list", + repoName, "-r", role) + notaryClientEnv(listCmd) + out, _, err := runCommandWithOutput(listCmd) + if err != nil { + c.Fatalf("Error listing targets in notary repository: %s\n", out) + } + + // should look something like: + // NAME DIGEST SIZE (BYTES) ROLE + // ------------------------------------------------------------------------------------------------------ + // latest 24a36bbc059b1345b7e8be0df20f1b23caa3602e85d42fff7ecd9d0bd255de56 1377 targets + + targets := make(map[string]string) + + // no target + lines := strings.Split(strings.TrimSpace(out), "\n") + if len(lines) == 1 && strings.Contains(out, "No targets present in this repository.") { + return targets + } + + // otherwise, there is at least one target + c.Assert(len(lines), checker.GreaterOrEqualThan, 3) + + for _, line := range lines[2:] { + tokens := strings.Fields(line) + c.Assert(tokens, checker.HasLen, 4) + targets[tokens[0]] = tokens[3] + } + + return targets +} + +func (s *DockerTrustSuite) assertTargetInRoles(c *check.C, repoName, target string, roles ...string) { + // check all the roles + for _, role := range roles { + targets := s.notaryListTargetsInRole(c, repoName, role) + roleName, ok := targets[target] + c.Assert(ok, checker.True) + c.Assert(roleName, checker.Equals, role) + } +} + +func (s *DockerTrustSuite) assertTargetNotInRoles(c *check.C, repoName, target string, roles ...string) { + targets := s.notaryListTargetsInRole(c, repoName, "targets") + + roleName, ok := targets[target] + if ok { + for _, role := range roles { + c.Assert(roleName, checker.Not(checker.Equals), role) + } + } +} diff --git a/integration-cli/utils.go b/integration-cli/utils.go new file mode 100644 index 00000000..149f1c45 --- /dev/null +++ b/integration-cli/utils.go @@ -0,0 +1,85 @@ +package main + +import ( + "io" + "os" + "os/exec" + "time" + + "github.com/docker/docker/pkg/integration" +) + +func getPrefixAndSlashFromDaemonPlatform() (prefix, slash string) { + if daemonPlatform == "windows" { + return "c:", `\` + } + return "", "/" +} + +func getExitCode(err error) (int, error) { + return integration.GetExitCode(err) +} + +func processExitCode(err error) (exitCode int) { + return integration.ProcessExitCode(err) +} + +func isKilled(err error) bool { + return integration.IsKilled(err) +} + +func runCommandWithOutput(cmd *exec.Cmd) (output string, exitCode int, err error) { + return integration.RunCommandWithOutput(cmd) +} + +func runCommandWithStdoutStderr(cmd *exec.Cmd) (stdout string, stderr string, exitCode int, err error) { + return integration.RunCommandWithStdoutStderr(cmd) +} + +func runCommandWithOutputForDuration(cmd *exec.Cmd, duration time.Duration) (output string, exitCode int, timedOut bool, err error) { + return integration.RunCommandWithOutputForDuration(cmd, duration) +} + +func runCommandWithOutputAndTimeout(cmd *exec.Cmd, timeout time.Duration) (output string, exitCode int, err error) { + return integration.RunCommandWithOutputAndTimeout(cmd, timeout) +} + +func runCommand(cmd *exec.Cmd) (exitCode int, err error) { + return integration.RunCommand(cmd) +} + +func runCommandPipelineWithOutput(cmds ...*exec.Cmd) (output string, exitCode int, err error) { + return integration.RunCommandPipelineWithOutput(cmds...) +} + +func unmarshalJSON(data []byte, result interface{}) error { + return integration.UnmarshalJSON(data, result) +} + +func convertSliceOfStringsToMap(input []string) map[string]struct{} { + return integration.ConvertSliceOfStringsToMap(input) +} + +func compareDirectoryEntries(e1 []os.FileInfo, e2 []os.FileInfo) error { + return integration.CompareDirectoryEntries(e1, e2) +} + +func listTar(f io.Reader) ([]string, error) { + return integration.ListTar(f) +} + +func randomTmpDirPath(s string, platform string) string { + return integration.RandomTmpDirPath(s, platform) +} + +func consumeWithSpeed(reader io.Reader, chunkSize int, interval time.Duration, stop chan bool) (n int, err error) { + return integration.ConsumeWithSpeed(reader, chunkSize, interval, stop) +} + +func parseCgroupPaths(procCgroupData string) map[string]string { + return integration.ParseCgroupPaths(procCgroupData) +} + +func runAtDifferentDate(date time.Time, block func()) { + integration.RunAtDifferentDate(date, block) +} diff --git a/layer/empty.go b/layer/empty.go new file mode 100644 index 00000000..5e1cb184 --- /dev/null +++ b/layer/empty.go @@ -0,0 +1,48 @@ +package layer + +import ( + "archive/tar" + "bytes" + "io" + "io/ioutil" +) + +// DigestSHA256EmptyTar is the canonical sha256 digest of empty tar file - +// (1024 NULL bytes) +const DigestSHA256EmptyTar = DiffID("sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef") + +type emptyLayer struct{} + +// EmptyLayer is a layer that corresponds to empty tar. +var EmptyLayer = &emptyLayer{} + +func (el *emptyLayer) TarStream() (io.ReadCloser, error) { + buf := new(bytes.Buffer) + tarWriter := tar.NewWriter(buf) + tarWriter.Close() + return ioutil.NopCloser(buf), nil +} + +func (el *emptyLayer) ChainID() ChainID { + return ChainID(DigestSHA256EmptyTar) +} + +func (el *emptyLayer) DiffID() DiffID { + return DigestSHA256EmptyTar +} + +func (el *emptyLayer) Parent() Layer { + return nil +} + +func (el *emptyLayer) Size() (size int64, err error) { + return 0, nil +} + +func (el *emptyLayer) DiffSize() (size int64, err error) { + return 0, nil +} + +func (el *emptyLayer) Metadata() (map[string]string, error) { + return make(map[string]string), nil +} diff --git a/layer/empty_test.go b/layer/empty_test.go new file mode 100644 index 00000000..c22da766 --- /dev/null +++ b/layer/empty_test.go @@ -0,0 +1,46 @@ +package layer + +import ( + "io" + "testing" + + "github.com/docker/distribution/digest" +) + +func TestEmptyLayer(t *testing.T) { + if EmptyLayer.ChainID() != ChainID(DigestSHA256EmptyTar) { + t.Fatal("wrong ID for empty layer") + } + + if EmptyLayer.DiffID() != DigestSHA256EmptyTar { + t.Fatal("wrong DiffID for empty layer") + } + + if EmptyLayer.Parent() != nil { + t.Fatal("expected no parent for empty layer") + } + + if size, err := EmptyLayer.Size(); err != nil || size != 0 { + t.Fatal("expected zero size for empty layer") + } + + if diffSize, err := EmptyLayer.DiffSize(); err != nil || diffSize != 0 { + t.Fatal("expected zero diffsize for empty layer") + } + + tarStream, err := EmptyLayer.TarStream() + if err != nil { + t.Fatalf("error streaming tar for empty layer: %v", err) + } + + digester := digest.Canonical.New() + _, err = io.Copy(digester.Hash(), tarStream) + + if err != nil { + t.Fatalf("error hashing empty tar layer: %v", err) + } + + if digester.Digest() != digest.Digest(DigestSHA256EmptyTar) { + t.Fatal("empty layer tar stream hashes to wrong value") + } +} diff --git a/layer/filestore.go b/layer/filestore.go new file mode 100644 index 00000000..a0044b36 --- /dev/null +++ b/layer/filestore.go @@ -0,0 +1,326 @@ +package layer + +import ( + "compress/gzip" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/digest" + "github.com/docker/docker/pkg/ioutils" +) + +var ( + stringIDRegexp = regexp.MustCompile(`^[a-f0-9]{64}(-init)?$`) + supportedAlgorithms = []digest.Algorithm{ + digest.SHA256, + // digest.SHA384, // Currently not used + // digest.SHA512, // Currently not used + } +) + +type fileMetadataStore struct { + root string +} + +type fileMetadataTransaction struct { + store *fileMetadataStore + root string +} + +// NewFSMetadataStore returns an instance of a metadata store +// which is backed by files on disk using the provided root +// as the root of metadata files. +func NewFSMetadataStore(root string) (MetadataStore, error) { + if err := os.MkdirAll(root, 0700); err != nil { + return nil, err + } + return &fileMetadataStore{ + root: root, + }, nil +} + +func (fms *fileMetadataStore) getLayerDirectory(layer ChainID) string { + dgst := digest.Digest(layer) + return filepath.Join(fms.root, string(dgst.Algorithm()), dgst.Hex()) +} + +func (fms *fileMetadataStore) getLayerFilename(layer ChainID, filename string) string { + return filepath.Join(fms.getLayerDirectory(layer), filename) +} + +func (fms *fileMetadataStore) getMountDirectory(mount string) string { + return filepath.Join(fms.root, "mounts", mount) +} + +func (fms *fileMetadataStore) getMountFilename(mount, filename string) string { + return filepath.Join(fms.getMountDirectory(mount), filename) +} + +func (fms *fileMetadataStore) StartTransaction() (MetadataTransaction, error) { + tmpDir := filepath.Join(fms.root, "tmp") + if err := os.MkdirAll(tmpDir, 0755); err != nil { + return nil, err + } + + td, err := ioutil.TempDir(tmpDir, "layer-") + if err != nil { + return nil, err + } + // Create a new tempdir + return &fileMetadataTransaction{ + store: fms, + root: td, + }, nil +} + +func (fm *fileMetadataTransaction) SetSize(size int64) error { + content := fmt.Sprintf("%d", size) + return ioutil.WriteFile(filepath.Join(fm.root, "size"), []byte(content), 0644) +} + +func (fm *fileMetadataTransaction) SetParent(parent ChainID) error { + return ioutil.WriteFile(filepath.Join(fm.root, "parent"), []byte(digest.Digest(parent).String()), 0644) +} + +func (fm *fileMetadataTransaction) SetDiffID(diff DiffID) error { + return ioutil.WriteFile(filepath.Join(fm.root, "diff"), []byte(digest.Digest(diff).String()), 0644) +} + +func (fm *fileMetadataTransaction) SetCacheID(cacheID string) error { + return ioutil.WriteFile(filepath.Join(fm.root, "cache-id"), []byte(cacheID), 0644) +} + +func (fm *fileMetadataTransaction) TarSplitWriter(compressInput bool) (io.WriteCloser, error) { + f, err := os.OpenFile(filepath.Join(fm.root, "tar-split.json.gz"), os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return nil, err + } + var wc io.WriteCloser + if compressInput { + wc = gzip.NewWriter(f) + } else { + wc = f + } + + return ioutils.NewWriteCloserWrapper(wc, func() error { + wc.Close() + return f.Close() + }), nil +} + +func (fm *fileMetadataTransaction) Commit(layer ChainID) error { + finalDir := fm.store.getLayerDirectory(layer) + if err := os.MkdirAll(filepath.Dir(finalDir), 0755); err != nil { + return err + } + return os.Rename(fm.root, finalDir) +} + +func (fm *fileMetadataTransaction) Cancel() error { + return os.RemoveAll(fm.root) +} + +func (fm *fileMetadataTransaction) String() string { + return fm.root +} + +func (fms *fileMetadataStore) GetSize(layer ChainID) (int64, error) { + content, err := ioutil.ReadFile(fms.getLayerFilename(layer, "size")) + if err != nil { + return 0, err + } + + size, err := strconv.ParseInt(string(content), 10, 64) + if err != nil { + return 0, err + } + + return size, nil +} + +func (fms *fileMetadataStore) GetParent(layer ChainID) (ChainID, error) { + content, err := ioutil.ReadFile(fms.getLayerFilename(layer, "parent")) + if err != nil { + if os.IsNotExist(err) { + return "", nil + } + return "", err + } + + dgst, err := digest.ParseDigest(strings.TrimSpace(string(content))) + if err != nil { + return "", err + } + + return ChainID(dgst), nil +} + +func (fms *fileMetadataStore) GetDiffID(layer ChainID) (DiffID, error) { + content, err := ioutil.ReadFile(fms.getLayerFilename(layer, "diff")) + if err != nil { + return "", err + } + + dgst, err := digest.ParseDigest(strings.TrimSpace(string(content))) + if err != nil { + return "", err + } + + return DiffID(dgst), nil +} + +func (fms *fileMetadataStore) GetCacheID(layer ChainID) (string, error) { + contentBytes, err := ioutil.ReadFile(fms.getLayerFilename(layer, "cache-id")) + if err != nil { + return "", err + } + content := strings.TrimSpace(string(contentBytes)) + + if !stringIDRegexp.MatchString(content) { + return "", errors.New("invalid cache id value") + } + + return content, nil +} + +func (fms *fileMetadataStore) TarSplitReader(layer ChainID) (io.ReadCloser, error) { + fz, err := os.Open(fms.getLayerFilename(layer, "tar-split.json.gz")) + if err != nil { + return nil, err + } + f, err := gzip.NewReader(fz) + if err != nil { + return nil, err + } + + return ioutils.NewReadCloserWrapper(f, func() error { + f.Close() + return fz.Close() + }), nil +} + +func (fms *fileMetadataStore) SetMountID(mount string, mountID string) error { + if err := os.MkdirAll(fms.getMountDirectory(mount), 0755); err != nil { + return err + } + return ioutil.WriteFile(fms.getMountFilename(mount, "mount-id"), []byte(mountID), 0644) +} + +func (fms *fileMetadataStore) SetInitID(mount string, init string) error { + if err := os.MkdirAll(fms.getMountDirectory(mount), 0755); err != nil { + return err + } + return ioutil.WriteFile(fms.getMountFilename(mount, "init-id"), []byte(init), 0644) +} + +func (fms *fileMetadataStore) SetMountParent(mount string, parent ChainID) error { + if err := os.MkdirAll(fms.getMountDirectory(mount), 0755); err != nil { + return err + } + return ioutil.WriteFile(fms.getMountFilename(mount, "parent"), []byte(digest.Digest(parent).String()), 0644) +} + +func (fms *fileMetadataStore) GetMountID(mount string) (string, error) { + contentBytes, err := ioutil.ReadFile(fms.getMountFilename(mount, "mount-id")) + if err != nil { + return "", err + } + content := strings.TrimSpace(string(contentBytes)) + + if !stringIDRegexp.MatchString(content) { + return "", errors.New("invalid mount id value") + } + + return content, nil +} + +func (fms *fileMetadataStore) GetInitID(mount string) (string, error) { + contentBytes, err := ioutil.ReadFile(fms.getMountFilename(mount, "init-id")) + if err != nil { + if os.IsNotExist(err) { + return "", nil + } + return "", err + } + content := strings.TrimSpace(string(contentBytes)) + + if !stringIDRegexp.MatchString(content) { + return "", errors.New("invalid init id value") + } + + return content, nil +} + +func (fms *fileMetadataStore) GetMountParent(mount string) (ChainID, error) { + content, err := ioutil.ReadFile(fms.getMountFilename(mount, "parent")) + if err != nil { + if os.IsNotExist(err) { + return "", nil + } + return "", err + } + + dgst, err := digest.ParseDigest(strings.TrimSpace(string(content))) + if err != nil { + return "", err + } + + return ChainID(dgst), nil +} + +func (fms *fileMetadataStore) List() ([]ChainID, []string, error) { + var ids []ChainID + for _, algorithm := range supportedAlgorithms { + fileInfos, err := ioutil.ReadDir(filepath.Join(fms.root, string(algorithm))) + if err != nil { + if os.IsNotExist(err) { + continue + } + return nil, nil, err + } + + for _, fi := range fileInfos { + if fi.IsDir() && fi.Name() != "mounts" { + dgst := digest.NewDigestFromHex(string(algorithm), fi.Name()) + if err := dgst.Validate(); err != nil { + logrus.Debugf("Ignoring invalid digest %s:%s", algorithm, fi.Name()) + } else { + ids = append(ids, ChainID(dgst)) + } + } + } + } + + fileInfos, err := ioutil.ReadDir(filepath.Join(fms.root, "mounts")) + if err != nil { + if os.IsNotExist(err) { + return ids, []string{}, nil + } + return nil, nil, err + } + + var mounts []string + for _, fi := range fileInfos { + if fi.IsDir() { + mounts = append(mounts, fi.Name()) + } + } + + return ids, mounts, nil +} + +func (fms *fileMetadataStore) Remove(layer ChainID) error { + return os.RemoveAll(fms.getLayerDirectory(layer)) +} + +func (fms *fileMetadataStore) RemoveMount(mount string) error { + return os.RemoveAll(fms.getMountDirectory(mount)) +} diff --git a/layer/filestore_test.go b/layer/filestore_test.go new file mode 100644 index 00000000..55e3b285 --- /dev/null +++ b/layer/filestore_test.go @@ -0,0 +1,104 @@ +package layer + +import ( + "fmt" + "io/ioutil" + "math/rand" + "os" + "path/filepath" + "strings" + "syscall" + "testing" + + "github.com/docker/distribution/digest" +) + +func randomLayerID(seed int64) ChainID { + r := rand.New(rand.NewSource(seed)) + + return ChainID(digest.FromBytes([]byte(fmt.Sprintf("%d", r.Int63())))) +} + +func newFileMetadataStore(t *testing.T) (*fileMetadataStore, string, func()) { + td, err := ioutil.TempDir("", "layers-") + if err != nil { + t.Fatal(err) + } + fms, err := NewFSMetadataStore(td) + if err != nil { + t.Fatal(err) + } + + return fms.(*fileMetadataStore), td, func() { + if err := os.RemoveAll(td); err != nil { + t.Logf("Failed to cleanup %q: %s", td, err) + } + } +} + +func assertNotDirectoryError(t *testing.T, err error) { + perr, ok := err.(*os.PathError) + if !ok { + t.Fatalf("Unexpected error %#v, expected path error", err) + } + + if perr.Err != syscall.ENOTDIR { + t.Fatalf("Unexpected error %s, expected %s", perr.Err, syscall.ENOTDIR) + } +} + +func TestCommitFailure(t *testing.T) { + fms, td, cleanup := newFileMetadataStore(t) + defer cleanup() + + if err := ioutil.WriteFile(filepath.Join(td, "sha256"), []byte("was here first!"), 0644); err != nil { + t.Fatal(err) + } + + tx, err := fms.StartTransaction() + if err != nil { + t.Fatal(err) + } + + if err := tx.SetSize(0); err != nil { + t.Fatal(err) + } + + err = tx.Commit(randomLayerID(5)) + if err == nil { + t.Fatalf("Expected error committing with invalid layer parent directory") + } + assertNotDirectoryError(t, err) +} + +func TestStartTransactionFailure(t *testing.T) { + fms, td, cleanup := newFileMetadataStore(t) + defer cleanup() + + if err := ioutil.WriteFile(filepath.Join(td, "tmp"), []byte("was here first!"), 0644); err != nil { + t.Fatal(err) + } + + _, err := fms.StartTransaction() + if err == nil { + t.Fatalf("Expected error starting transaction with invalid layer parent directory") + } + assertNotDirectoryError(t, err) + + if err := os.Remove(filepath.Join(td, "tmp")); err != nil { + t.Fatal(err) + } + + tx, err := fms.StartTransaction() + if err != nil { + t.Fatal(err) + } + + if expected := filepath.Join(td, "tmp"); strings.HasPrefix(expected, tx.String()) { + t.Fatalf("Unexpected transaction string %q, expected prefix %q", tx.String(), expected) + } + + if err := tx.Cancel(); err != nil { + t.Fatal(err) + } +} diff --git a/layer/layer.go b/layer/layer.go new file mode 100644 index 00000000..ad01e89a --- /dev/null +++ b/layer/layer.go @@ -0,0 +1,262 @@ +// Package layer is package for managing read-only +// and read-write mounts on the union file system +// driver. Read-only mounts are referenced using a +// content hash and are protected from mutation in +// the exposed interface. The tar format is used +// to create read-only layers and export both +// read-only and writable layers. The exported +// tar data for a read-only layer should match +// the tar used to create the layer. +package layer + +import ( + "errors" + "io" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/digest" + "github.com/docker/docker/pkg/archive" +) + +var ( + // ErrLayerDoesNotExist is used when an operation is + // attempted on a layer which does not exist. + ErrLayerDoesNotExist = errors.New("layer does not exist") + + // ErrLayerNotRetained is used when a release is + // attempted on a layer which is not retained. + ErrLayerNotRetained = errors.New("layer not retained") + + // ErrMountDoesNotExist is used when an operation is + // attempted on a mount layer which does not exist. + ErrMountDoesNotExist = errors.New("mount does not exist") + + // ErrMountNameConflict is used when a mount is attempted + // to be created but there is already a mount with the name + // used for creation. + ErrMountNameConflict = errors.New("mount already exists with name") + + // ErrActiveMount is used when an operation on a + // mount is attempted but the layer is still + // mounted and the operation cannot be performed. + ErrActiveMount = errors.New("mount still active") + + // ErrNotMounted is used when requesting an active + // mount but the layer is not mounted. + ErrNotMounted = errors.New("not mounted") + + // ErrMaxDepthExceeded is used when a layer is attempted + // to be created which would result in a layer depth + // greater than the 125 max. + ErrMaxDepthExceeded = errors.New("max depth exceeded") + + // ErrNotSupported is used when the action is not supppoted + // on the current platform + ErrNotSupported = errors.New("not support on this platform") +) + +// ChainID is the content-addressable ID of a layer. +type ChainID digest.Digest + +// String returns a string rendition of a layer ID +func (id ChainID) String() string { + return string(id) +} + +// DiffID is the hash of an individual layer tar. +type DiffID digest.Digest + +// String returns a string rendition of a layer DiffID +func (diffID DiffID) String() string { + return string(diffID) +} + +// TarStreamer represents an object which may +// have its contents exported as a tar stream. +type TarStreamer interface { + // TarStream returns a tar archive stream + // for the contents of a layer. + TarStream() (io.ReadCloser, error) +} + +// Layer represents a read-only layer +type Layer interface { + TarStreamer + + // ChainID returns the content hash of the entire layer chain. The hash + // chain is made up of DiffID of top layer and all of its parents. + ChainID() ChainID + + // DiffID returns the content hash of the layer + // tar stream used to create this layer. + DiffID() DiffID + + // Parent returns the next layer in the layer chain. + Parent() Layer + + // Size returns the size of the entire layer chain. The size + // is calculated from the total size of all files in the layers. + Size() (int64, error) + + // DiffSize returns the size difference of the top layer + // from parent layer. + DiffSize() (int64, error) + + // Metadata returns the low level storage metadata associated + // with layer. + Metadata() (map[string]string, error) +} + +// RWLayer represents a layer which is +// read and writable +type RWLayer interface { + TarStreamer + + // Name of mounted layer + Name() string + + // Parent returns the layer which the writable + // layer was created from. + Parent() Layer + + // Mount mounts the RWLayer and returns the filesystem path + // the to the writable layer. + Mount(mountLabel string) (string, error) + + // Unmount unmounts the RWLayer. This should be called + // for every mount. If there are multiple mount calls + // this operation will only decrement the internal mount counter. + Unmount() error + + // Size represents the size of the writable layer + // as calculated by the total size of the files + // changed in the mutable layer. + Size() (int64, error) + + // Changes returns the set of changes for the mutable layer + // from the base layer. + Changes() ([]archive.Change, error) + + // Metadata returns the low level metadata for the mutable layer + Metadata() (map[string]string, error) +} + +// Metadata holds information about a +// read-only layer +type Metadata struct { + // ChainID is the content hash of the layer + ChainID ChainID + + // DiffID is the hash of the tar data used to + // create the layer + DiffID DiffID + + // Size is the size of the layer and all parents + Size int64 + + // DiffSize is the size of the top layer + DiffSize int64 +} + +// MountInit is a function to initialize a +// writable mount. Changes made here will +// not be included in the Tar stream of the +// RWLayer. +type MountInit func(root string) error + +// Store represents a backend for managing both +// read-only and read-write layers. +type Store interface { + Register(io.Reader, ChainID) (Layer, error) + Get(ChainID) (Layer, error) + Release(Layer) ([]Metadata, error) + + CreateRWLayer(id string, parent ChainID, mountLabel string, initFunc MountInit) (RWLayer, error) + GetRWLayer(id string) (RWLayer, error) + GetMountID(id string) (string, error) + ReinitRWLayer(l RWLayer) error + ReleaseRWLayer(RWLayer) ([]Metadata, error) + + Cleanup() error + DriverStatus() [][2]string + DriverName() string +} + +// MetadataTransaction represents functions for setting layer metadata +// with a single transaction. +type MetadataTransaction interface { + SetSize(int64) error + SetParent(parent ChainID) error + SetDiffID(DiffID) error + SetCacheID(string) error + TarSplitWriter(compressInput bool) (io.WriteCloser, error) + + Commit(ChainID) error + Cancel() error + String() string +} + +// MetadataStore represents a backend for persisting +// metadata about layers and providing the metadata +// for restoring a Store. +type MetadataStore interface { + // StartTransaction starts an update for new metadata + // which will be used to represent an ID on commit. + StartTransaction() (MetadataTransaction, error) + + GetSize(ChainID) (int64, error) + GetParent(ChainID) (ChainID, error) + GetDiffID(ChainID) (DiffID, error) + GetCacheID(ChainID) (string, error) + TarSplitReader(ChainID) (io.ReadCloser, error) + + SetMountID(string, string) error + SetInitID(string, string) error + SetMountParent(string, ChainID) error + + GetMountID(string) (string, error) + GetInitID(string) (string, error) + GetMountParent(string) (ChainID, error) + + // List returns the full list of referenced + // read-only and read-write layers + List() ([]ChainID, []string, error) + + Remove(ChainID) error + RemoveMount(string) error +} + +// CreateChainID returns ID for a layerDigest slice +func CreateChainID(dgsts []DiffID) ChainID { + return createChainIDFromParent("", dgsts...) +} + +func createChainIDFromParent(parent ChainID, dgsts ...DiffID) ChainID { + if len(dgsts) == 0 { + return parent + } + if parent == "" { + return createChainIDFromParent(ChainID(dgsts[0]), dgsts[1:]...) + } + // H = "H(n-1) SHA256(n)" + dgst := digest.FromBytes([]byte(string(parent) + " " + string(dgsts[0]))) + return createChainIDFromParent(ChainID(dgst), dgsts[1:]...) +} + +// ReleaseAndLog releases the provided layer from the given layer +// store, logging any error and release metadata +func ReleaseAndLog(ls Store, l Layer) { + metadata, err := ls.Release(l) + if err != nil { + logrus.Errorf("Error releasing layer %s: %v", l.ChainID(), err) + } + LogReleaseMetadata(metadata) +} + +// LogReleaseMetadata logs a metadata array, uses this to +// ensure consistent logging for release metadata +func LogReleaseMetadata(metadatas []Metadata) { + for _, metadata := range metadatas { + logrus.Infof("Layer %s cleaned up", metadata.ChainID) + } +} diff --git a/layer/layer_store.go b/layer/layer_store.go new file mode 100644 index 00000000..73a1b34c --- /dev/null +++ b/layer/layer_store.go @@ -0,0 +1,666 @@ +package layer + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "sync" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/digest" + "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/stringid" + "github.com/vbatts/tar-split/tar/asm" + "github.com/vbatts/tar-split/tar/storage" +) + +// maxLayerDepth represents the maximum number of +// layers which can be chained together. 125 was +// chosen to account for the 127 max in some +// graphdrivers plus the 2 additional layers +// used to create a rwlayer. +const maxLayerDepth = 125 + +type layerStore struct { + store MetadataStore + driver graphdriver.Driver + + layerMap map[ChainID]*roLayer + layerL sync.Mutex + + mounts map[string]*mountedLayer + mountL sync.Mutex +} + +// StoreOptions are the options used to create a new Store instance +type StoreOptions struct { + StorePath string + MetadataStorePathTemplate string + GraphDriver string + GraphDriverOptions []string + UIDMaps []idtools.IDMap + GIDMaps []idtools.IDMap +} + +// NewStoreFromOptions creates a new Store instance +func NewStoreFromOptions(options StoreOptions) (Store, error) { + driver, err := graphdriver.New( + options.StorePath, + options.GraphDriver, + options.GraphDriverOptions, + options.UIDMaps, + options.GIDMaps) + if err != nil { + return nil, fmt.Errorf("error initializing graphdriver: %v", err) + } + logrus.Debugf("Using graph driver %s", driver) + + fms, err := NewFSMetadataStore(fmt.Sprintf(options.MetadataStorePathTemplate, driver)) + if err != nil { + return nil, err + } + + return NewStoreFromGraphDriver(fms, driver) +} + +// NewStoreFromGraphDriver creates a new Store instance using the provided +// metadata store and graph driver. The metadata store will be used to restore +// the Store. +func NewStoreFromGraphDriver(store MetadataStore, driver graphdriver.Driver) (Store, error) { + ls := &layerStore{ + store: store, + driver: driver, + layerMap: map[ChainID]*roLayer{}, + mounts: map[string]*mountedLayer{}, + } + + ids, mounts, err := store.List() + if err != nil { + return nil, err + } + + for _, id := range ids { + l, err := ls.loadLayer(id) + if err != nil { + logrus.Debugf("Failed to load layer %s: %s", id, err) + continue + } + if l.parent != nil { + l.parent.referenceCount++ + } + } + + for _, mount := range mounts { + if err := ls.loadMount(mount); err != nil { + logrus.Debugf("Failed to load mount %s: %s", mount, err) + } + } + + return ls, nil +} + +func (ls *layerStore) loadLayer(layer ChainID) (*roLayer, error) { + cl, ok := ls.layerMap[layer] + if ok { + return cl, nil + } + + diff, err := ls.store.GetDiffID(layer) + if err != nil { + return nil, fmt.Errorf("failed to get diff id for %s: %s", layer, err) + } + + size, err := ls.store.GetSize(layer) + if err != nil { + return nil, fmt.Errorf("failed to get size for %s: %s", layer, err) + } + + cacheID, err := ls.store.GetCacheID(layer) + if err != nil { + return nil, fmt.Errorf("failed to get cache id for %s: %s", layer, err) + } + + parent, err := ls.store.GetParent(layer) + if err != nil { + return nil, fmt.Errorf("failed to get parent for %s: %s", layer, err) + } + + cl = &roLayer{ + chainID: layer, + diffID: diff, + size: size, + cacheID: cacheID, + layerStore: ls, + references: map[Layer]struct{}{}, + } + + if parent != "" { + p, err := ls.loadLayer(parent) + if err != nil { + return nil, err + } + cl.parent = p + } + + ls.layerMap[cl.chainID] = cl + + return cl, nil +} + +func (ls *layerStore) loadMount(mount string) error { + if _, ok := ls.mounts[mount]; ok { + return nil + } + + mountID, err := ls.store.GetMountID(mount) + if err != nil { + return err + } + + initID, err := ls.store.GetInitID(mount) + if err != nil { + return err + } + + parent, err := ls.store.GetMountParent(mount) + if err != nil { + return err + } + + ml := &mountedLayer{ + name: mount, + mountID: mountID, + initID: initID, + layerStore: ls, + references: map[RWLayer]*referencedRWLayer{}, + } + + if parent != "" { + p, err := ls.loadLayer(parent) + if err != nil { + return err + } + ml.parent = p + + p.referenceCount++ + } + + ls.mounts[ml.name] = ml + + return nil +} + +func (ls *layerStore) applyTar(tx MetadataTransaction, ts io.Reader, parent string, layer *roLayer) error { + digester := digest.Canonical.New() + tr := io.TeeReader(ts, digester.Hash()) + + tsw, err := tx.TarSplitWriter(true) + if err != nil { + return err + } + metaPacker := storage.NewJSONPacker(tsw) + defer tsw.Close() + + // we're passing nil here for the file putter, because the ApplyDiff will + // handle the extraction of the archive + rdr, err := asm.NewInputTarStream(tr, metaPacker, nil) + if err != nil { + return err + } + + applySize, err := ls.driver.ApplyDiff(layer.cacheID, parent, archive.Reader(rdr)) + if err != nil { + return err + } + + // Discard trailing data but ensure metadata is picked up to reconstruct stream + io.Copy(ioutil.Discard, rdr) // ignore error as reader may be closed + + layer.size = applySize + layer.diffID = DiffID(digester.Digest()) + + logrus.Debugf("Applied tar %s to %s, size: %d", layer.diffID, layer.cacheID, applySize) + + return nil +} + +func (ls *layerStore) Register(ts io.Reader, parent ChainID) (Layer, error) { + // err is used to hold the error which will always trigger + // cleanup of creates sources but may not be an error returned + // to the caller (already exists). + var err error + var pid string + var p *roLayer + if string(parent) != "" { + p = ls.get(parent) + if p == nil { + return nil, ErrLayerDoesNotExist + } + pid = p.cacheID + // Release parent chain if error + defer func() { + if err != nil { + ls.layerL.Lock() + ls.releaseLayer(p) + ls.layerL.Unlock() + } + }() + if p.depth() >= maxLayerDepth { + err = ErrMaxDepthExceeded + return nil, err + } + } + + // Create new roLayer + layer := &roLayer{ + parent: p, + cacheID: stringid.GenerateRandomID(), + referenceCount: 1, + layerStore: ls, + references: map[Layer]struct{}{}, + } + + if err = ls.driver.Create(layer.cacheID, pid, ""); err != nil { + return nil, err + } + + tx, err := ls.store.StartTransaction() + if err != nil { + return nil, err + } + + defer func() { + if err != nil { + logrus.Debugf("Cleaning up layer %s: %v", layer.cacheID, err) + if err := ls.driver.Remove(layer.cacheID); err != nil { + logrus.Errorf("Error cleaning up cache layer %s: %v", layer.cacheID, err) + } + if err := tx.Cancel(); err != nil { + logrus.Errorf("Error canceling metadata transaction %q: %s", tx.String(), err) + } + } + }() + + if err = ls.applyTar(tx, ts, pid, layer); err != nil { + return nil, err + } + + if layer.parent == nil { + layer.chainID = ChainID(layer.diffID) + } else { + layer.chainID = createChainIDFromParent(layer.parent.chainID, layer.diffID) + } + + if err = storeLayer(tx, layer); err != nil { + return nil, err + } + + ls.layerL.Lock() + defer ls.layerL.Unlock() + + if existingLayer := ls.getWithoutLock(layer.chainID); existingLayer != nil { + // Set error for cleanup, but do not return the error + err = errors.New("layer already exists") + return existingLayer.getReference(), nil + } + + if err = tx.Commit(layer.chainID); err != nil { + return nil, err + } + + ls.layerMap[layer.chainID] = layer + + return layer.getReference(), nil +} + +func (ls *layerStore) getWithoutLock(layer ChainID) *roLayer { + l, ok := ls.layerMap[layer] + if !ok { + return nil + } + + l.referenceCount++ + + return l +} + +func (ls *layerStore) get(l ChainID) *roLayer { + ls.layerL.Lock() + defer ls.layerL.Unlock() + return ls.getWithoutLock(l) +} + +func (ls *layerStore) Get(l ChainID) (Layer, error) { + ls.layerL.Lock() + defer ls.layerL.Unlock() + + layer := ls.getWithoutLock(l) + if layer == nil { + return nil, ErrLayerDoesNotExist + } + + return layer.getReference(), nil +} + +func (ls *layerStore) deleteLayer(layer *roLayer, metadata *Metadata) error { + err := ls.driver.Remove(layer.cacheID) + if err != nil { + return err + } + + err = ls.store.Remove(layer.chainID) + if err != nil { + return err + } + metadata.DiffID = layer.diffID + metadata.ChainID = layer.chainID + metadata.Size, err = layer.Size() + if err != nil { + return err + } + metadata.DiffSize = layer.size + + return nil +} + +func (ls *layerStore) releaseLayer(l *roLayer) ([]Metadata, error) { + depth := 0 + removed := []Metadata{} + for { + if l.referenceCount == 0 { + panic("layer not retained") + } + l.referenceCount-- + if l.referenceCount != 0 { + return removed, nil + } + + if len(removed) == 0 && depth > 0 { + panic("cannot remove layer with child") + } + if l.hasReferences() { + panic("cannot delete referenced layer") + } + var metadata Metadata + if err := ls.deleteLayer(l, &metadata); err != nil { + return nil, err + } + + delete(ls.layerMap, l.chainID) + removed = append(removed, metadata) + + if l.parent == nil { + return removed, nil + } + + depth++ + l = l.parent + } +} + +func (ls *layerStore) Release(l Layer) ([]Metadata, error) { + ls.layerL.Lock() + defer ls.layerL.Unlock() + layer, ok := ls.layerMap[l.ChainID()] + if !ok { + return []Metadata{}, nil + } + if !layer.hasReference(l) { + return nil, ErrLayerNotRetained + } + + layer.deleteReference(l) + + return ls.releaseLayer(layer) +} + +func (ls *layerStore) CreateRWLayer(name string, parent ChainID, mountLabel string, initFunc MountInit) (RWLayer, error) { + ls.mountL.Lock() + defer ls.mountL.Unlock() + m, ok := ls.mounts[name] + if ok { + return nil, ErrMountNameConflict + } + + var err error + var pid string + var p *roLayer + if string(parent) != "" { + p = ls.get(parent) + if p == nil { + return nil, ErrLayerDoesNotExist + } + pid = p.cacheID + + // Release parent chain if error + defer func() { + if err != nil { + ls.layerL.Lock() + ls.releaseLayer(p) + ls.layerL.Unlock() + } + }() + } + + m = &mountedLayer{ + name: name, + parent: p, + mountID: ls.mountID(name), + layerStore: ls, + references: map[RWLayer]*referencedRWLayer{}, + } + + if initFunc != nil { + pid, err = ls.initMount(m.mountID, pid, mountLabel, initFunc) + if err != nil { + return nil, err + } + m.initID = pid + } + + if err = ls.driver.Create(m.mountID, pid, ""); err != nil { + return nil, err + } + + if err = ls.saveMount(m); err != nil { + return nil, err + } + + return m.getReference(), nil +} + +func (ls *layerStore) GetRWLayer(id string) (RWLayer, error) { + ls.mountL.Lock() + defer ls.mountL.Unlock() + mount, ok := ls.mounts[id] + if !ok { + return nil, ErrMountDoesNotExist + } + + return mount.getReference(), nil +} + +func (ls *layerStore) GetMountID(id string) (string, error) { + ls.mountL.Lock() + defer ls.mountL.Unlock() + mount, ok := ls.mounts[id] + if !ok { + return "", ErrMountDoesNotExist + } + logrus.Debugf("GetMountID id: %s -> mountID: %s", id, mount.mountID) + + return mount.mountID, nil +} + +// ReinitRWLayer reinitializes a given mount to the layerstore, specifically +// initializing the usage count. It should strictly only be used in the +// daemon's restore path to restore state of live containers. +func (ls *layerStore) ReinitRWLayer(l RWLayer) error { + ls.mountL.Lock() + defer ls.mountL.Unlock() + + m, ok := ls.mounts[l.Name()] + if !ok { + return ErrMountDoesNotExist + } + + if err := m.incActivityCount(l); err != nil { + return err + } + + return nil +} + +func (ls *layerStore) ReleaseRWLayer(l RWLayer) ([]Metadata, error) { + ls.mountL.Lock() + defer ls.mountL.Unlock() + m, ok := ls.mounts[l.Name()] + if !ok { + return []Metadata{}, nil + } + + if err := m.deleteReference(l); err != nil { + return nil, err + } + + if m.hasReferences() { + return []Metadata{}, nil + } + + if err := ls.driver.Remove(m.mountID); err != nil { + logrus.Errorf("Error removing mounted layer %s: %s", m.name, err) + m.retakeReference(l) + return nil, err + } + + if m.initID != "" { + if err := ls.driver.Remove(m.initID); err != nil { + logrus.Errorf("Error removing init layer %s: %s", m.name, err) + m.retakeReference(l) + return nil, err + } + } + + if err := ls.store.RemoveMount(m.name); err != nil { + logrus.Errorf("Error removing mount metadata: %s: %s", m.name, err) + m.retakeReference(l) + return nil, err + } + + delete(ls.mounts, m.Name()) + + ls.layerL.Lock() + defer ls.layerL.Unlock() + if m.parent != nil { + return ls.releaseLayer(m.parent) + } + + return []Metadata{}, nil +} + +func (ls *layerStore) saveMount(mount *mountedLayer) error { + if err := ls.store.SetMountID(mount.name, mount.mountID); err != nil { + return err + } + + if mount.initID != "" { + if err := ls.store.SetInitID(mount.name, mount.initID); err != nil { + return err + } + } + + if mount.parent != nil { + if err := ls.store.SetMountParent(mount.name, mount.parent.chainID); err != nil { + return err + } + } + + ls.mounts[mount.name] = mount + + return nil +} + +func (ls *layerStore) initMount(graphID, parent, mountLabel string, initFunc MountInit) (string, error) { + // Use "-init" to maintain compatibility with graph drivers + // which are expecting this layer with this special name. If all + // graph drivers can be updated to not rely on knowing about this layer + // then the initID should be randomly generated. + initID := fmt.Sprintf("%s-init", graphID) + + if err := ls.driver.Create(initID, parent, mountLabel); err != nil { + return "", err + } + p, err := ls.driver.Get(initID, "") + if err != nil { + return "", err + } + + if err := initFunc(p); err != nil { + ls.driver.Put(initID) + return "", err + } + + if err := ls.driver.Put(initID); err != nil { + return "", err + } + + return initID, nil +} + +func (ls *layerStore) assembleTarTo(graphID string, metadata io.ReadCloser, size *int64, w io.Writer) error { + diffDriver, ok := ls.driver.(graphdriver.DiffGetterDriver) + if !ok { + diffDriver = &naiveDiffPathDriver{ls.driver} + } + + defer metadata.Close() + + // get our relative path to the container + fileGetCloser, err := diffDriver.DiffGetter(graphID) + if err != nil { + return err + } + defer fileGetCloser.Close() + + metaUnpacker := storage.NewJSONUnpacker(metadata) + upackerCounter := &unpackSizeCounter{metaUnpacker, size} + logrus.Debugf("Assembling tar data for %s", graphID) + return asm.WriteOutputTarStream(fileGetCloser, upackerCounter, w) +} + +func (ls *layerStore) Cleanup() error { + return ls.driver.Cleanup() +} + +func (ls *layerStore) DriverStatus() [][2]string { + return ls.driver.Status() +} + +func (ls *layerStore) DriverName() string { + return ls.driver.String() +} + +type naiveDiffPathDriver struct { + graphdriver.Driver +} + +type fileGetPutter struct { + storage.FileGetter + driver graphdriver.Driver + id string +} + +func (w *fileGetPutter) Close() error { + return w.driver.Put(w.id) +} + +func (n *naiveDiffPathDriver) DiffGetter(id string) (graphdriver.FileGetCloser, error) { + p, err := n.Driver.Get(id, "") + if err != nil { + return nil, err + } + return &fileGetPutter{storage.NewPathFileGetter(p), n.Driver, id}, nil +} diff --git a/layer/layer_test.go b/layer/layer_test.go new file mode 100644 index 00000000..7fb792dc --- /dev/null +++ b/layer/layer_test.go @@ -0,0 +1,788 @@ +package layer + +import ( + "bytes" + "io" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/docker/distribution/digest" + "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/daemon/graphdriver/vfs" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/stringid" +) + +func init() { + graphdriver.ApplyUncompressedLayer = archive.UnpackLayer + vfs.CopyWithTar = archive.CopyWithTar +} + +func newVFSGraphDriver(td string) (graphdriver.Driver, error) { + uidMap := []idtools.IDMap{ + { + ContainerID: 0, + HostID: os.Getuid(), + Size: 1, + }, + } + gidMap := []idtools.IDMap{ + { + ContainerID: 0, + HostID: os.Getgid(), + Size: 1, + }, + } + + return graphdriver.GetDriver("vfs", td, nil, uidMap, gidMap) +} + +func newTestGraphDriver(t *testing.T) (graphdriver.Driver, func()) { + td, err := ioutil.TempDir("", "graph-") + if err != nil { + t.Fatal(err) + } + + driver, err := newVFSGraphDriver(td) + if err != nil { + t.Fatal(err) + } + + return driver, func() { + os.RemoveAll(td) + } +} + +func newTestStore(t *testing.T) (Store, string, func()) { + td, err := ioutil.TempDir("", "layerstore-") + if err != nil { + t.Fatal(err) + } + + graph, graphcleanup := newTestGraphDriver(t) + fms, err := NewFSMetadataStore(td) + if err != nil { + t.Fatal(err) + } + ls, err := NewStoreFromGraphDriver(fms, graph) + if err != nil { + t.Fatal(err) + } + + return ls, td, func() { + graphcleanup() + os.RemoveAll(td) + } +} + +type layerInit func(root string) error + +func createLayer(ls Store, parent ChainID, layerFunc layerInit) (Layer, error) { + containerID := stringid.GenerateRandomID() + mount, err := ls.CreateRWLayer(containerID, parent, "", nil) + if err != nil { + return nil, err + } + + path, err := mount.Mount("") + if err != nil { + return nil, err + } + + if err := layerFunc(path); err != nil { + return nil, err + } + + ts, err := mount.TarStream() + if err != nil { + return nil, err + } + defer ts.Close() + + layer, err := ls.Register(ts, parent) + if err != nil { + return nil, err + } + + if err := mount.Unmount(); err != nil { + return nil, err + } + + if _, err := ls.ReleaseRWLayer(mount); err != nil { + return nil, err + } + + return layer, nil +} + +type FileApplier interface { + ApplyFile(root string) error +} + +type testFile struct { + name string + content []byte + permission os.FileMode +} + +func newTestFile(name string, content []byte, perm os.FileMode) FileApplier { + return &testFile{ + name: name, + content: content, + permission: perm, + } +} + +func (tf *testFile) ApplyFile(root string) error { + fullPath := filepath.Join(root, tf.name) + if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil { + return err + } + // Check if already exists + if stat, err := os.Stat(fullPath); err == nil && stat.Mode().Perm() != tf.permission { + if err := os.Chmod(fullPath, tf.permission); err != nil { + return err + } + } + if err := ioutil.WriteFile(fullPath, tf.content, tf.permission); err != nil { + return err + } + return nil +} + +func initWithFiles(files ...FileApplier) layerInit { + return func(root string) error { + for _, f := range files { + if err := f.ApplyFile(root); err != nil { + return err + } + } + return nil + } +} + +func getCachedLayer(l Layer) *roLayer { + if rl, ok := l.(*referencedCacheLayer); ok { + return rl.roLayer + } + return l.(*roLayer) +} + +func getMountLayer(l RWLayer) *mountedLayer { + if rl, ok := l.(*referencedRWLayer); ok { + return rl.mountedLayer + } + return l.(*mountedLayer) +} + +func createMetadata(layers ...Layer) []Metadata { + metadata := make([]Metadata, len(layers)) + for i := range layers { + size, err := layers[i].Size() + if err != nil { + panic(err) + } + + metadata[i].ChainID = layers[i].ChainID() + metadata[i].DiffID = layers[i].DiffID() + metadata[i].Size = size + metadata[i].DiffSize = getCachedLayer(layers[i]).size + } + + return metadata +} + +func assertMetadata(t *testing.T, metadata, expectedMetadata []Metadata) { + if len(metadata) != len(expectedMetadata) { + t.Fatalf("Unexpected number of deletes %d, expected %d", len(metadata), len(expectedMetadata)) + } + + for i := range metadata { + if metadata[i] != expectedMetadata[i] { + t.Errorf("Unexpected metadata\n\tExpected: %#v\n\tActual: %#v", expectedMetadata[i], metadata[i]) + } + } + if t.Failed() { + t.FailNow() + } +} + +func releaseAndCheckDeleted(t *testing.T, ls Store, layer Layer, removed ...Layer) { + layerCount := len(ls.(*layerStore).layerMap) + expectedMetadata := createMetadata(removed...) + metadata, err := ls.Release(layer) + if err != nil { + t.Fatal(err) + } + + assertMetadata(t, metadata, expectedMetadata) + + if expected := layerCount - len(removed); len(ls.(*layerStore).layerMap) != expected { + t.Fatalf("Unexpected number of layers %d, expected %d", len(ls.(*layerStore).layerMap), expected) + } +} + +func cacheID(l Layer) string { + return getCachedLayer(l).cacheID +} + +func assertLayerEqual(t *testing.T, l1, l2 Layer) { + if l1.ChainID() != l2.ChainID() { + t.Fatalf("Mismatched ID: %s vs %s", l1.ChainID(), l2.ChainID()) + } + if l1.DiffID() != l2.DiffID() { + t.Fatalf("Mismatched DiffID: %s vs %s", l1.DiffID(), l2.DiffID()) + } + + size1, err := l1.Size() + if err != nil { + t.Fatal(err) + } + + size2, err := l2.Size() + if err != nil { + t.Fatal(err) + } + + if size1 != size2 { + t.Fatalf("Mismatched size: %d vs %d", size1, size2) + } + + if cacheID(l1) != cacheID(l2) { + t.Fatalf("Mismatched cache id: %s vs %s", cacheID(l1), cacheID(l2)) + } + + p1 := l1.Parent() + p2 := l2.Parent() + if p1 != nil && p2 != nil { + assertLayerEqual(t, p1, p2) + } else if p1 != nil || p2 != nil { + t.Fatalf("Mismatched parents: %v vs %v", p1, p2) + } +} + +func TestMountAndRegister(t *testing.T) { + ls, _, cleanup := newTestStore(t) + defer cleanup() + + li := initWithFiles(newTestFile("testfile.txt", []byte("some test data"), 0644)) + layer, err := createLayer(ls, "", li) + if err != nil { + t.Fatal(err) + } + + size, _ := layer.Size() + t.Logf("Layer size: %d", size) + + mount2, err := ls.CreateRWLayer("new-test-mount", layer.ChainID(), "", nil) + if err != nil { + t.Fatal(err) + } + + path2, err := mount2.Mount("") + if err != nil { + t.Fatal(err) + } + + b, err := ioutil.ReadFile(filepath.Join(path2, "testfile.txt")) + if err != nil { + t.Fatal(err) + } + + if expected := "some test data"; string(b) != expected { + t.Fatalf("Wrong file data, expected %q, got %q", expected, string(b)) + } + + if err := mount2.Unmount(); err != nil { + t.Fatal(err) + } + + if _, err := ls.ReleaseRWLayer(mount2); err != nil { + t.Fatal(err) + } +} + +func TestLayerRelease(t *testing.T) { + // TODO Windows: Figure out why this is failing + if runtime.GOOS == "windows" { + t.Skip("Failing on Windows") + } + ls, _, cleanup := newTestStore(t) + defer cleanup() + + layer1, err := createLayer(ls, "", initWithFiles(newTestFile("layer1.txt", []byte("layer 1 file"), 0644))) + if err != nil { + t.Fatal(err) + } + + layer2, err := createLayer(ls, layer1.ChainID(), initWithFiles(newTestFile("layer2.txt", []byte("layer 2 file"), 0644))) + if err != nil { + t.Fatal(err) + } + + if _, err := ls.Release(layer1); err != nil { + t.Fatal(err) + } + + layer3a, err := createLayer(ls, layer2.ChainID(), initWithFiles(newTestFile("layer3.txt", []byte("layer 3a file"), 0644))) + if err != nil { + t.Fatal(err) + } + + layer3b, err := createLayer(ls, layer2.ChainID(), initWithFiles(newTestFile("layer3.txt", []byte("layer 3b file"), 0644))) + if err != nil { + t.Fatal(err) + } + + if _, err := ls.Release(layer2); err != nil { + t.Fatal(err) + } + + t.Logf("Layer1: %s", layer1.ChainID()) + t.Logf("Layer2: %s", layer2.ChainID()) + t.Logf("Layer3a: %s", layer3a.ChainID()) + t.Logf("Layer3b: %s", layer3b.ChainID()) + + if expected := 4; len(ls.(*layerStore).layerMap) != expected { + t.Fatalf("Unexpected number of layers %d, expected %d", len(ls.(*layerStore).layerMap), expected) + } + + releaseAndCheckDeleted(t, ls, layer3b, layer3b) + releaseAndCheckDeleted(t, ls, layer3a, layer3a, layer2, layer1) +} + +func TestStoreRestore(t *testing.T) { + // TODO Windows: Figure out why this is failing + if runtime.GOOS == "windows" { + t.Skip("Failing on Windows") + } + ls, _, cleanup := newTestStore(t) + defer cleanup() + + layer1, err := createLayer(ls, "", initWithFiles(newTestFile("layer1.txt", []byte("layer 1 file"), 0644))) + if err != nil { + t.Fatal(err) + } + + layer2, err := createLayer(ls, layer1.ChainID(), initWithFiles(newTestFile("layer2.txt", []byte("layer 2 file"), 0644))) + if err != nil { + t.Fatal(err) + } + + if _, err := ls.Release(layer1); err != nil { + t.Fatal(err) + } + + layer3, err := createLayer(ls, layer2.ChainID(), initWithFiles(newTestFile("layer3.txt", []byte("layer 3 file"), 0644))) + if err != nil { + t.Fatal(err) + } + + if _, err := ls.Release(layer2); err != nil { + t.Fatal(err) + } + + m, err := ls.CreateRWLayer("some-mount_name", layer3.ChainID(), "", nil) + if err != nil { + t.Fatal(err) + } + + path, err := m.Mount("") + if err != nil { + t.Fatal(err) + } + + if err := ioutil.WriteFile(filepath.Join(path, "testfile.txt"), []byte("nothing here"), 0644); err != nil { + t.Fatal(err) + } + assertActivityCount(t, m, 1) + + if err := m.Unmount(); err != nil { + t.Fatal(err) + } + + assertActivityCount(t, m, 0) + + ls2, err := NewStoreFromGraphDriver(ls.(*layerStore).store, ls.(*layerStore).driver) + if err != nil { + t.Fatal(err) + } + + layer3b, err := ls2.Get(layer3.ChainID()) + if err != nil { + t.Fatal(err) + } + + assertLayerEqual(t, layer3b, layer3) + + // Create again with same name, should return error + if _, err := ls2.CreateRWLayer("some-mount_name", layer3b.ChainID(), "", nil); err == nil { + t.Fatal("Expected error creating mount with same name") + } else if err != ErrMountNameConflict { + t.Fatal(err) + } + + m2, err := ls2.GetRWLayer("some-mount_name") + if err != nil { + t.Fatal(err) + } + + if mountPath, err := m2.Mount(""); err != nil { + t.Fatal(err) + } else if path != mountPath { + t.Fatalf("Unexpected path %s, expected %s", mountPath, path) + } + + assertActivityCount(t, m2, 1) + + if mountPath, err := m2.Mount(""); err != nil { + t.Fatal(err) + } else if path != mountPath { + t.Fatalf("Unexpected path %s, expected %s", mountPath, path) + } + assertActivityCount(t, m2, 2) + if err := m2.Unmount(); err != nil { + t.Fatal(err) + } + + assertActivityCount(t, m2, 1) + + b, err := ioutil.ReadFile(filepath.Join(path, "testfile.txt")) + if err != nil { + t.Fatal(err) + } + if expected := "nothing here"; string(b) != expected { + t.Fatalf("Unexpected content %q, expected %q", string(b), expected) + } + + if err := m2.Unmount(); err != nil { + t.Fatal(err) + } + + assertActivityCount(t, m2, 0) + + if metadata, err := ls2.ReleaseRWLayer(m2); err != nil { + t.Fatal(err) + } else if len(metadata) != 0 { + t.Fatalf("Unexpectedly deleted layers: %#v", metadata) + } + + if metadata, err := ls2.ReleaseRWLayer(m2); err != nil { + t.Fatal(err) + } else if len(metadata) != 0 { + t.Fatalf("Unexpectedly deleted layers: %#v", metadata) + } + + releaseAndCheckDeleted(t, ls2, layer3b, layer3, layer2, layer1) +} + +func TestTarStreamStability(t *testing.T) { + // TODO Windows: Figure out why this is failing + if runtime.GOOS == "windows" { + t.Skip("Failing on Windows") + } + ls, _, cleanup := newTestStore(t) + defer cleanup() + + files1 := []FileApplier{ + newTestFile("/etc/hosts", []byte("mydomain 10.0.0.1"), 0644), + newTestFile("/etc/profile", []byte("PATH=/usr/bin"), 0644), + } + addedFile := newTestFile("/etc/shadow", []byte("root:::::::"), 0644) + files2 := []FileApplier{ + newTestFile("/etc/hosts", []byte("mydomain 10.0.0.2"), 0644), + newTestFile("/etc/profile", []byte("PATH=/usr/bin"), 0664), + newTestFile("/root/.bashrc", []byte("PATH=/usr/sbin:/usr/bin"), 0644), + } + + tar1, err := tarFromFiles(files1...) + if err != nil { + t.Fatal(err) + } + + tar2, err := tarFromFiles(files2...) + if err != nil { + t.Fatal(err) + } + + layer1, err := ls.Register(bytes.NewReader(tar1), "") + if err != nil { + t.Fatal(err) + } + + // hack layer to add file + p, err := ls.(*layerStore).driver.Get(layer1.(*referencedCacheLayer).cacheID, "") + if err != nil { + t.Fatal(err) + } + + if err := addedFile.ApplyFile(p); err != nil { + t.Fatal(err) + } + + if err := ls.(*layerStore).driver.Put(layer1.(*referencedCacheLayer).cacheID); err != nil { + t.Fatal(err) + } + + layer2, err := ls.Register(bytes.NewReader(tar2), layer1.ChainID()) + if err != nil { + t.Fatal(err) + } + + id1 := layer1.ChainID() + t.Logf("Layer 1: %s", layer1.ChainID()) + t.Logf("Layer 2: %s", layer2.ChainID()) + + if _, err := ls.Release(layer1); err != nil { + t.Fatal(err) + } + + assertLayerDiff(t, tar2, layer2) + + layer1b, err := ls.Get(id1) + if err != nil { + t.Logf("Content of layer map: %#v", ls.(*layerStore).layerMap) + t.Fatal(err) + } + + if _, err := ls.Release(layer2); err != nil { + t.Fatal(err) + } + + assertLayerDiff(t, tar1, layer1b) + + if _, err := ls.Release(layer1b); err != nil { + t.Fatal(err) + } +} + +func assertLayerDiff(t *testing.T, expected []byte, layer Layer) { + expectedDigest := digest.FromBytes(expected) + + if digest.Digest(layer.DiffID()) != expectedDigest { + t.Fatalf("Mismatched diff id for %s, got %s, expected %s", layer.ChainID(), layer.DiffID(), expected) + } + + ts, err := layer.TarStream() + if err != nil { + t.Fatal(err) + } + defer ts.Close() + + actual, err := ioutil.ReadAll(ts) + if err != nil { + t.Fatal(err) + } + + if len(actual) != len(expected) { + logByteDiff(t, actual, expected) + t.Fatalf("Mismatched tar stream size for %s, got %d, expected %d", layer.ChainID(), len(actual), len(expected)) + } + + actualDigest := digest.FromBytes(actual) + + if actualDigest != expectedDigest { + logByteDiff(t, actual, expected) + t.Fatalf("Wrong digest of tar stream, got %s, expected %s", actualDigest, expectedDigest) + } +} + +const maxByteLog = 4 * 1024 + +func logByteDiff(t *testing.T, actual, expected []byte) { + d1, d2 := byteDiff(actual, expected) + if len(d1) == 0 && len(d2) == 0 { + return + } + + prefix := len(actual) - len(d1) + if len(d1) > maxByteLog || len(d2) > maxByteLog { + t.Logf("Byte diff after %d matching bytes", prefix) + } else { + t.Logf("Byte diff after %d matching bytes\nActual bytes after prefix:\n%x\nExpected bytes after prefix:\n%x", prefix, d1, d2) + } +} + +// byteDiff returns the differing bytes after the matching prefix +func byteDiff(b1, b2 []byte) ([]byte, []byte) { + i := 0 + for i < len(b1) && i < len(b2) { + if b1[i] != b2[i] { + break + } + i++ + } + + return b1[i:], b2[i:] +} + +func tarFromFiles(files ...FileApplier) ([]byte, error) { + td, err := ioutil.TempDir("", "tar-") + if err != nil { + return nil, err + } + defer os.RemoveAll(td) + + for _, f := range files { + if err := f.ApplyFile(td); err != nil { + return nil, err + } + } + + r, err := archive.Tar(td, archive.Uncompressed) + if err != nil { + return nil, err + } + + buf := bytes.NewBuffer(nil) + if _, err := io.Copy(buf, r); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +// assertReferences asserts that all the references are to the same +// image and represent the full set of references to that image. +func assertReferences(t *testing.T, references ...Layer) { + if len(references) == 0 { + return + } + base := references[0].(*referencedCacheLayer).roLayer + seenReferences := map[Layer]struct{}{ + references[0]: {}, + } + for i := 1; i < len(references); i++ { + other := references[i].(*referencedCacheLayer).roLayer + if base != other { + t.Fatalf("Unexpected referenced cache layer %s, expecting %s", other.ChainID(), base.ChainID()) + } + if _, ok := base.references[references[i]]; !ok { + t.Fatalf("Reference not part of reference list: %v", references[i]) + } + if _, ok := seenReferences[references[i]]; ok { + t.Fatalf("Duplicated reference %v", references[i]) + } + } + if rc := len(base.references); rc != len(references) { + t.Fatalf("Unexpected number of references %d, expecting %d", rc, len(references)) + } +} + +func assertActivityCount(t *testing.T, l RWLayer, expected int) { + rl := l.(*referencedRWLayer) + if rl.activityCount != expected { + t.Fatalf("Unexpected activity count %d, expected %d", rl.activityCount, expected) + } +} + +func TestRegisterExistingLayer(t *testing.T) { + ls, _, cleanup := newTestStore(t) + defer cleanup() + + baseFiles := []FileApplier{ + newTestFile("/etc/profile", []byte("# Base configuration"), 0644), + } + + layerFiles := []FileApplier{ + newTestFile("/root/.bashrc", []byte("# Root configuration"), 0644), + } + + li := initWithFiles(baseFiles...) + layer1, err := createLayer(ls, "", li) + if err != nil { + t.Fatal(err) + } + + tar1, err := tarFromFiles(layerFiles...) + if err != nil { + t.Fatal(err) + } + + layer2a, err := ls.Register(bytes.NewReader(tar1), layer1.ChainID()) + if err != nil { + t.Fatal(err) + } + + layer2b, err := ls.Register(bytes.NewReader(tar1), layer1.ChainID()) + if err != nil { + t.Fatal(err) + } + + assertReferences(t, layer2a, layer2b) +} + +func TestTarStreamVerification(t *testing.T) { + // TODO Windows: Figure out why this is failing + if runtime.GOOS == "windows" { + t.Skip("Failing on Windows") + } + ls, tmpdir, cleanup := newTestStore(t) + defer cleanup() + + files1 := []FileApplier{ + newTestFile("/foo", []byte("abc"), 0644), + newTestFile("/bar", []byte("def"), 0644), + } + files2 := []FileApplier{ + newTestFile("/foo", []byte("abc"), 0644), + newTestFile("/bar", []byte("def"), 0600), // different perm + } + + tar1, err := tarFromFiles(files1...) + if err != nil { + t.Fatal(err) + } + + tar2, err := tarFromFiles(files2...) + if err != nil { + t.Fatal(err) + } + + layer1, err := ls.Register(bytes.NewReader(tar1), "") + if err != nil { + t.Fatal(err) + } + + layer2, err := ls.Register(bytes.NewReader(tar2), "") + if err != nil { + t.Fatal(err) + } + id1 := digest.Digest(layer1.ChainID()) + id2 := digest.Digest(layer2.ChainID()) + + // Replace tar data files + src, err := os.Open(filepath.Join(tmpdir, id1.Algorithm().String(), id1.Hex(), "tar-split.json.gz")) + if err != nil { + t.Fatal(err) + } + + dst, err := os.Create(filepath.Join(tmpdir, id2.Algorithm().String(), id2.Hex(), "tar-split.json.gz")) + if err != nil { + t.Fatal(err) + } + + if _, err := io.Copy(dst, src); err != nil { + t.Fatal(err) + } + + src.Close() + dst.Close() + + ts, err := layer2.TarStream() + if err != nil { + t.Fatal(err) + } + _, err = io.Copy(ioutil.Discard, ts) + if err == nil { + t.Fatal("expected data verification to fail") + } + if !strings.Contains(err.Error(), "could not verify layer data") { + t.Fatalf("wrong error returned from tarstream: %q", err) + } +} diff --git a/layer/layer_unix.go b/layer/layer_unix.go new file mode 100644 index 00000000..d77e2fc6 --- /dev/null +++ b/layer/layer_unix.go @@ -0,0 +1,9 @@ +// +build linux freebsd darwin openbsd + +package layer + +import "github.com/docker/docker/pkg/stringid" + +func (ls *layerStore) mountID(name string) string { + return stringid.GenerateRandomID() +} diff --git a/layer/layer_unix_test.go b/layer/layer_unix_test.go new file mode 100644 index 00000000..9aa1afd5 --- /dev/null +++ b/layer/layer_unix_test.go @@ -0,0 +1,71 @@ +// +build !windows + +package layer + +import "testing" + +func graphDiffSize(ls Store, l Layer) (int64, error) { + cl := getCachedLayer(l) + var parent string + if cl.parent != nil { + parent = cl.parent.cacheID + } + return ls.(*layerStore).driver.DiffSize(cl.cacheID, parent) +} + +// Unix as Windows graph driver does not support Changes which is indirectly +// invoked by calling DiffSize on the driver +func TestLayerSize(t *testing.T) { + ls, _, cleanup := newTestStore(t) + defer cleanup() + + content1 := []byte("Base contents") + content2 := []byte("Added contents") + + layer1, err := createLayer(ls, "", initWithFiles(newTestFile("file1", content1, 0644))) + if err != nil { + t.Fatal(err) + } + + layer2, err := createLayer(ls, layer1.ChainID(), initWithFiles(newTestFile("file2", content2, 0644))) + if err != nil { + t.Fatal(err) + } + + layer1DiffSize, err := graphDiffSize(ls, layer1) + if err != nil { + t.Fatal(err) + } + + if int(layer1DiffSize) != len(content1) { + t.Fatalf("Unexpected diff size %d, expected %d", layer1DiffSize, len(content1)) + } + + layer1Size, err := layer1.Size() + if err != nil { + t.Fatal(err) + } + + if expected := len(content1); int(layer1Size) != expected { + t.Fatalf("Unexpected size %d, expected %d", layer1Size, expected) + } + + layer2DiffSize, err := graphDiffSize(ls, layer2) + if err != nil { + t.Fatal(err) + } + + if int(layer2DiffSize) != len(content2) { + t.Fatalf("Unexpected diff size %d, expected %d", layer2DiffSize, len(content2)) + } + + layer2Size, err := layer2.Size() + if err != nil { + t.Fatal(err) + } + + if expected := len(content1) + len(content2); int(layer2Size) != expected { + t.Fatalf("Unexpected size %d, expected %d", layer2Size, expected) + } + +} diff --git a/layer/layer_windows.go b/layer/layer_windows.go new file mode 100644 index 00000000..e20311a0 --- /dev/null +++ b/layer/layer_windows.go @@ -0,0 +1,98 @@ +package layer + +import ( + "errors" + "fmt" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/digest" + "github.com/docker/docker/daemon/graphdriver" +) + +// GetLayerPath returns the path to a layer +func GetLayerPath(s Store, layer ChainID) (string, error) { + ls, ok := s.(*layerStore) + if !ok { + return "", errors.New("unsupported layer store") + } + ls.layerL.Lock() + defer ls.layerL.Unlock() + + rl, ok := ls.layerMap[layer] + if !ok { + return "", ErrLayerDoesNotExist + } + + path, err := ls.driver.Get(rl.cacheID, "") + if err != nil { + return "", err + } + + if err := ls.driver.Put(rl.cacheID); err != nil { + return "", err + } + + return path, nil +} + +func (ls *layerStore) RegisterDiffID(graphID string, size int64) (Layer, error) { + var err error // this is used for cleanup in existingLayer case + diffID := digest.FromBytes([]byte(graphID)) + + // Create new roLayer + layer := &roLayer{ + cacheID: graphID, + diffID: DiffID(diffID), + referenceCount: 1, + layerStore: ls, + references: map[Layer]struct{}{}, + size: size, + } + + tx, err := ls.store.StartTransaction() + if err != nil { + return nil, err + } + defer func() { + if err != nil { + if err := tx.Cancel(); err != nil { + logrus.Errorf("Error canceling metadata transaction %q: %s", tx.String(), err) + } + } + }() + + layer.chainID = createChainIDFromParent("", layer.diffID) + + if !ls.driver.Exists(layer.cacheID) { + return nil, fmt.Errorf("layer %q is unknown to driver", layer.cacheID) + } + if err = storeLayer(tx, layer); err != nil { + return nil, err + } + + ls.layerL.Lock() + defer ls.layerL.Unlock() + + if existingLayer := ls.getWithoutLock(layer.chainID); existingLayer != nil { + // Set error for cleanup, but do not return + err = errors.New("layer already exists") + return existingLayer.getReference(), nil + } + + if err = tx.Commit(layer.chainID); err != nil { + return nil, err + } + + ls.layerMap[layer.chainID] = layer + + return layer.getReference(), nil +} + +func (ls *layerStore) mountID(name string) string { + // windows has issues if container ID doesn't match mount ID + return name +} + +func (ls *layerStore) GraphDriver() graphdriver.Driver { + return ls.driver +} diff --git a/layer/migration.go b/layer/migration.go new file mode 100644 index 00000000..b45c3109 --- /dev/null +++ b/layer/migration.go @@ -0,0 +1,256 @@ +package layer + +import ( + "compress/gzip" + "errors" + "fmt" + "io" + "os" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/digest" + "github.com/vbatts/tar-split/tar/asm" + "github.com/vbatts/tar-split/tar/storage" +) + +// CreateRWLayerByGraphID creates a RWLayer in the layer store using +// the provided name with the given graphID. To get the RWLayer +// after migration the layer may be retrieved by the given name. +func (ls *layerStore) CreateRWLayerByGraphID(name string, graphID string, parent ChainID) (err error) { + ls.mountL.Lock() + defer ls.mountL.Unlock() + m, ok := ls.mounts[name] + if ok { + if m.parent.chainID != parent { + return errors.New("name conflict, mismatched parent") + } + if m.mountID != graphID { + return errors.New("mount already exists") + } + + return nil + } + + if !ls.driver.Exists(graphID) { + return fmt.Errorf("graph ID does not exist: %q", graphID) + } + + var p *roLayer + if string(parent) != "" { + p = ls.get(parent) + if p == nil { + return ErrLayerDoesNotExist + } + + // Release parent chain if error + defer func() { + if err != nil { + ls.layerL.Lock() + ls.releaseLayer(p) + ls.layerL.Unlock() + } + }() + } + + // TODO: Ensure graphID has correct parent + + m = &mountedLayer{ + name: name, + parent: p, + mountID: graphID, + layerStore: ls, + references: map[RWLayer]*referencedRWLayer{}, + } + + // Check for existing init layer + initID := fmt.Sprintf("%s-init", graphID) + if ls.driver.Exists(initID) { + m.initID = initID + } + + if err = ls.saveMount(m); err != nil { + return err + } + + return nil +} + +func (ls *layerStore) ChecksumForGraphID(id, parent, oldTarDataPath, newTarDataPath string) (diffID DiffID, size int64, err error) { + defer func() { + if err != nil { + logrus.Debugf("could not get checksum for %q with tar-split: %q", id, err) + diffID, size, err = ls.checksumForGraphIDNoTarsplit(id, parent, newTarDataPath) + } + }() + + if oldTarDataPath == "" { + err = errors.New("no tar-split file") + return + } + + tarDataFile, err := os.Open(oldTarDataPath) + if err != nil { + return + } + defer tarDataFile.Close() + uncompressed, err := gzip.NewReader(tarDataFile) + if err != nil { + return + } + + dgst := digest.Canonical.New() + err = ls.assembleTarTo(id, uncompressed, &size, dgst.Hash()) + if err != nil { + return + } + + diffID = DiffID(dgst.Digest()) + err = os.RemoveAll(newTarDataPath) + if err != nil { + return + } + err = os.Link(oldTarDataPath, newTarDataPath) + + return +} + +func (ls *layerStore) checksumForGraphIDNoTarsplit(id, parent, newTarDataPath string) (diffID DiffID, size int64, err error) { + rawarchive, err := ls.driver.Diff(id, parent) + if err != nil { + return + } + defer rawarchive.Close() + + f, err := os.Create(newTarDataPath) + if err != nil { + return + } + defer f.Close() + mfz := gzip.NewWriter(f) + defer mfz.Close() + metaPacker := storage.NewJSONPacker(mfz) + + packerCounter := &packSizeCounter{metaPacker, &size} + + archive, err := asm.NewInputTarStream(rawarchive, packerCounter, nil) + if err != nil { + return + } + dgst, err := digest.FromReader(archive) + if err != nil { + return + } + diffID = DiffID(dgst) + return +} + +func (ls *layerStore) RegisterByGraphID(graphID string, parent ChainID, diffID DiffID, tarDataFile string, size int64) (Layer, error) { + // err is used to hold the error which will always trigger + // cleanup of creates sources but may not be an error returned + // to the caller (already exists). + var err error + var p *roLayer + if string(parent) != "" { + p = ls.get(parent) + if p == nil { + return nil, ErrLayerDoesNotExist + } + + // Release parent chain if error + defer func() { + if err != nil { + ls.layerL.Lock() + ls.releaseLayer(p) + ls.layerL.Unlock() + } + }() + } + + // Create new roLayer + layer := &roLayer{ + parent: p, + cacheID: graphID, + referenceCount: 1, + layerStore: ls, + references: map[Layer]struct{}{}, + diffID: diffID, + size: size, + chainID: createChainIDFromParent(parent, diffID), + } + + ls.layerL.Lock() + defer ls.layerL.Unlock() + + if existingLayer := ls.getWithoutLock(layer.chainID); existingLayer != nil { + // Set error for cleanup, but do not return + err = errors.New("layer already exists") + return existingLayer.getReference(), nil + } + + tx, err := ls.store.StartTransaction() + if err != nil { + return nil, err + } + + defer func() { + if err != nil { + logrus.Debugf("Cleaning up transaction after failed migration for %s: %v", graphID, err) + if err := tx.Cancel(); err != nil { + logrus.Errorf("Error canceling metadata transaction %q: %s", tx.String(), err) + } + } + }() + + tsw, err := tx.TarSplitWriter(false) + if err != nil { + return nil, err + } + defer tsw.Close() + tdf, err := os.Open(tarDataFile) + if err != nil { + return nil, err + } + defer tdf.Close() + _, err = io.Copy(tsw, tdf) + if err != nil { + return nil, err + } + + if err = storeLayer(tx, layer); err != nil { + return nil, err + } + + if err = tx.Commit(layer.chainID); err != nil { + return nil, err + } + + ls.layerMap[layer.chainID] = layer + + return layer.getReference(), nil +} + +type unpackSizeCounter struct { + unpacker storage.Unpacker + size *int64 +} + +func (u *unpackSizeCounter) Next() (*storage.Entry, error) { + e, err := u.unpacker.Next() + if err == nil && u.size != nil { + *u.size += e.Size + } + return e, err +} + +type packSizeCounter struct { + packer storage.Packer + size *int64 +} + +func (p *packSizeCounter) AddEntry(e storage.Entry) (int, error) { + n, err := p.packer.AddEntry(e) + if err == nil && p.size != nil { + *p.size += e.Size + } + return n, err +} diff --git a/layer/migration_test.go b/layer/migration_test.go new file mode 100644 index 00000000..0ac73a4c --- /dev/null +++ b/layer/migration_test.go @@ -0,0 +1,448 @@ +package layer + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/stringid" + "github.com/vbatts/tar-split/tar/asm" + "github.com/vbatts/tar-split/tar/storage" +) + +func writeTarSplitFile(name string, tarContent []byte) error { + f, err := os.OpenFile(name, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + + fz := gzip.NewWriter(f) + + metaPacker := storage.NewJSONPacker(fz) + defer fz.Close() + + rdr, err := asm.NewInputTarStream(bytes.NewReader(tarContent), metaPacker, nil) + if err != nil { + return err + } + + if _, err := io.Copy(ioutil.Discard, rdr); err != nil { + return err + } + + return nil +} + +func TestLayerMigration(t *testing.T) { + // TODO Windows: Figure out why this is failing + if runtime.GOOS == "windows" { + t.Skip("Failing on Windows") + } + td, err := ioutil.TempDir("", "migration-test-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(td) + + layer1Files := []FileApplier{ + newTestFile("/root/.bashrc", []byte("# Boring configuration"), 0644), + newTestFile("/etc/profile", []byte("# Base configuration"), 0644), + } + + layer2Files := []FileApplier{ + newTestFile("/root/.bashrc", []byte("# Updated configuration"), 0644), + } + + tar1, err := tarFromFiles(layer1Files...) + if err != nil { + t.Fatal(err) + } + + tar2, err := tarFromFiles(layer2Files...) + if err != nil { + t.Fatal(err) + } + + graph, err := newVFSGraphDriver(filepath.Join(td, "graphdriver-")) + if err != nil { + t.Fatal(err) + } + + graphID1 := stringid.GenerateRandomID() + if err := graph.Create(graphID1, "", ""); err != nil { + t.Fatal(err) + } + if _, err := graph.ApplyDiff(graphID1, "", archive.Reader(bytes.NewReader(tar1))); err != nil { + t.Fatal(err) + } + + tf1 := filepath.Join(td, "tar1.json.gz") + if err := writeTarSplitFile(tf1, tar1); err != nil { + t.Fatal(err) + } + + fms, err := NewFSMetadataStore(filepath.Join(td, "layers")) + if err != nil { + t.Fatal(err) + } + ls, err := NewStoreFromGraphDriver(fms, graph) + if err != nil { + t.Fatal(err) + } + + newTarDataPath := filepath.Join(td, ".migration-tardata") + diffID, size, err := ls.(*layerStore).ChecksumForGraphID(graphID1, "", tf1, newTarDataPath) + if err != nil { + t.Fatal(err) + } + + layer1a, err := ls.(*layerStore).RegisterByGraphID(graphID1, "", diffID, newTarDataPath, size) + if err != nil { + t.Fatal(err) + } + + layer1b, err := ls.Register(bytes.NewReader(tar1), "") + if err != nil { + t.Fatal(err) + } + + assertReferences(t, layer1a, layer1b) + // Attempt register, should be same + layer2a, err := ls.Register(bytes.NewReader(tar2), layer1a.ChainID()) + if err != nil { + t.Fatal(err) + } + + graphID2 := stringid.GenerateRandomID() + if err := graph.Create(graphID2, graphID1, ""); err != nil { + t.Fatal(err) + } + if _, err := graph.ApplyDiff(graphID2, graphID1, archive.Reader(bytes.NewReader(tar2))); err != nil { + t.Fatal(err) + } + + tf2 := filepath.Join(td, "tar2.json.gz") + if err := writeTarSplitFile(tf2, tar2); err != nil { + t.Fatal(err) + } + diffID, size, err = ls.(*layerStore).ChecksumForGraphID(graphID2, graphID1, tf2, newTarDataPath) + if err != nil { + t.Fatal(err) + } + + layer2b, err := ls.(*layerStore).RegisterByGraphID(graphID2, layer1a.ChainID(), diffID, tf2, size) + if err != nil { + t.Fatal(err) + } + assertReferences(t, layer2a, layer2b) + + if metadata, err := ls.Release(layer2a); err != nil { + t.Fatal(err) + } else if len(metadata) > 0 { + t.Fatalf("Unexpected layer removal after first release: %#v", metadata) + } + + metadata, err := ls.Release(layer2b) + if err != nil { + t.Fatal(err) + } + + assertMetadata(t, metadata, createMetadata(layer2a)) +} + +func tarFromFilesInGraph(graph graphdriver.Driver, graphID, parentID string, files ...FileApplier) ([]byte, error) { + t, err := tarFromFiles(files...) + if err != nil { + return nil, err + } + + if err := graph.Create(graphID, parentID, ""); err != nil { + return nil, err + } + if _, err := graph.ApplyDiff(graphID, parentID, archive.Reader(bytes.NewReader(t))); err != nil { + return nil, err + } + + ar, err := graph.Diff(graphID, parentID) + if err != nil { + return nil, err + } + defer ar.Close() + + return ioutil.ReadAll(ar) +} + +func TestLayerMigrationNoTarsplit(t *testing.T) { + // TODO Windows: Figure out why this is failing + if runtime.GOOS == "windows" { + t.Skip("Failing on Windows") + } + td, err := ioutil.TempDir("", "migration-test-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(td) + + layer1Files := []FileApplier{ + newTestFile("/root/.bashrc", []byte("# Boring configuration"), 0644), + newTestFile("/etc/profile", []byte("# Base configuration"), 0644), + } + + layer2Files := []FileApplier{ + newTestFile("/root/.bashrc", []byte("# Updated configuration"), 0644), + } + + graph, err := newVFSGraphDriver(filepath.Join(td, "graphdriver-")) + if err != nil { + t.Fatal(err) + } + graphID1 := stringid.GenerateRandomID() + graphID2 := stringid.GenerateRandomID() + + tar1, err := tarFromFilesInGraph(graph, graphID1, "", layer1Files...) + if err != nil { + t.Fatal(err) + } + + tar2, err := tarFromFilesInGraph(graph, graphID2, graphID1, layer2Files...) + if err != nil { + t.Fatal(err) + } + + fms, err := NewFSMetadataStore(filepath.Join(td, "layers")) + if err != nil { + t.Fatal(err) + } + ls, err := NewStoreFromGraphDriver(fms, graph) + if err != nil { + t.Fatal(err) + } + + newTarDataPath := filepath.Join(td, ".migration-tardata") + diffID, size, err := ls.(*layerStore).ChecksumForGraphID(graphID1, "", "", newTarDataPath) + if err != nil { + t.Fatal(err) + } + + layer1a, err := ls.(*layerStore).RegisterByGraphID(graphID1, "", diffID, newTarDataPath, size) + if err != nil { + t.Fatal(err) + } + + layer1b, err := ls.Register(bytes.NewReader(tar1), "") + if err != nil { + t.Fatal(err) + } + + assertReferences(t, layer1a, layer1b) + + // Attempt register, should be same + layer2a, err := ls.Register(bytes.NewReader(tar2), layer1a.ChainID()) + if err != nil { + t.Fatal(err) + } + + diffID, size, err = ls.(*layerStore).ChecksumForGraphID(graphID2, graphID1, "", newTarDataPath) + if err != nil { + t.Fatal(err) + } + + layer2b, err := ls.(*layerStore).RegisterByGraphID(graphID2, layer1a.ChainID(), diffID, newTarDataPath, size) + if err != nil { + t.Fatal(err) + } + assertReferences(t, layer2a, layer2b) + + if metadata, err := ls.Release(layer2a); err != nil { + t.Fatal(err) + } else if len(metadata) > 0 { + t.Fatalf("Unexpected layer removal after first release: %#v", metadata) + } + + metadata, err := ls.Release(layer2b) + if err != nil { + t.Fatal(err) + } + + assertMetadata(t, metadata, createMetadata(layer2a)) +} + +func TestMountMigration(t *testing.T) { + // TODO Windows: Figure out why this is failing (obvious - paths... needs porting) + if runtime.GOOS == "windows" { + t.Skip("Failing on Windows") + } + ls, _, cleanup := newTestStore(t) + defer cleanup() + + baseFiles := []FileApplier{ + newTestFile("/root/.bashrc", []byte("# Boring configuration"), 0644), + newTestFile("/etc/profile", []byte("# Base configuration"), 0644), + } + initFiles := []FileApplier{ + newTestFile("/etc/hosts", []byte{}, 0644), + newTestFile("/etc/resolv.conf", []byte{}, 0644), + } + mountFiles := []FileApplier{ + newTestFile("/etc/hosts", []byte("localhost 127.0.0.1"), 0644), + newTestFile("/root/.bashrc", []byte("# Updated configuration"), 0644), + newTestFile("/root/testfile1.txt", []byte("nothing valuable"), 0644), + } + + initTar, err := tarFromFiles(initFiles...) + if err != nil { + t.Fatal(err) + } + + mountTar, err := tarFromFiles(mountFiles...) + if err != nil { + t.Fatal(err) + } + + graph := ls.(*layerStore).driver + + layer1, err := createLayer(ls, "", initWithFiles(baseFiles...)) + if err != nil { + t.Fatal(err) + } + + graphID1 := layer1.(*referencedCacheLayer).cacheID + + containerID := stringid.GenerateRandomID() + containerInit := fmt.Sprintf("%s-init", containerID) + + if err := graph.Create(containerInit, graphID1, ""); err != nil { + t.Fatal(err) + } + if _, err := graph.ApplyDiff(containerInit, graphID1, archive.Reader(bytes.NewReader(initTar))); err != nil { + t.Fatal(err) + } + + if err := graph.Create(containerID, containerInit, ""); err != nil { + t.Fatal(err) + } + if _, err := graph.ApplyDiff(containerID, containerInit, archive.Reader(bytes.NewReader(mountTar))); err != nil { + t.Fatal(err) + } + + if err := ls.(*layerStore).CreateRWLayerByGraphID("migration-mount", containerID, layer1.ChainID()); err != nil { + t.Fatal(err) + } + + rwLayer1, err := ls.GetRWLayer("migration-mount") + if err != nil { + t.Fatal(err) + } + + if _, err := rwLayer1.Mount(""); err != nil { + t.Fatal(err) + } + + changes, err := rwLayer1.Changes() + if err != nil { + t.Fatal(err) + } + + if expected := 5; len(changes) != expected { + t.Logf("Changes %#v", changes) + t.Fatalf("Wrong number of changes %d, expected %d", len(changes), expected) + } + + sortChanges(changes) + + assertChange(t, changes[0], archive.Change{ + Path: "/etc", + Kind: archive.ChangeModify, + }) + assertChange(t, changes[1], archive.Change{ + Path: "/etc/hosts", + Kind: archive.ChangeModify, + }) + assertChange(t, changes[2], archive.Change{ + Path: "/root", + Kind: archive.ChangeModify, + }) + assertChange(t, changes[3], archive.Change{ + Path: "/root/.bashrc", + Kind: archive.ChangeModify, + }) + assertChange(t, changes[4], archive.Change{ + Path: "/root/testfile1.txt", + Kind: archive.ChangeAdd, + }) + + assertActivityCount(t, rwLayer1, 1) + + if _, err := ls.CreateRWLayer("migration-mount", layer1.ChainID(), "", nil); err == nil { + t.Fatal("Expected error creating mount with same name") + } else if err != ErrMountNameConflict { + t.Fatal(err) + } + + rwLayer2, err := ls.GetRWLayer("migration-mount") + if err != nil { + t.Fatal(err) + } + + if getMountLayer(rwLayer1) != getMountLayer(rwLayer2) { + t.Fatal("Expected same layer from get with same name as from migrate") + } + + if _, err := rwLayer2.Mount(""); err != nil { + t.Fatal(err) + } + + assertActivityCount(t, rwLayer2, 1) + assertActivityCount(t, rwLayer1, 1) + + if _, err := rwLayer2.Mount(""); err != nil { + t.Fatal(err) + } + + assertActivityCount(t, rwLayer2, 2) + assertActivityCount(t, rwLayer1, 1) + + if metadata, err := ls.Release(layer1); err != nil { + t.Fatal(err) + } else if len(metadata) > 0 { + t.Fatalf("Expected no layers to be deleted, deleted %#v", metadata) + } + + if err := rwLayer1.Unmount(); err != nil { + t.Fatal(err) + } + assertActivityCount(t, rwLayer2, 2) + assertActivityCount(t, rwLayer1, 0) + + if _, err := ls.ReleaseRWLayer(rwLayer1); err != nil { + t.Fatal(err) + } + + if err := rwLayer2.Unmount(); err != nil { + t.Fatal(err) + } + if _, err := ls.ReleaseRWLayer(rwLayer2); err == nil { + t.Fatal("Expected error deleting active mount") + } + if err := rwLayer2.Unmount(); err != nil { + t.Fatal(err) + } + metadata, err := ls.ReleaseRWLayer(rwLayer2) + if err != nil { + t.Fatal(err) + } + if len(metadata) == 0 { + t.Fatal("Expected base layer to be deleted when deleting mount") + } + + assertMetadata(t, metadata, createMetadata(layer1)) +} diff --git a/layer/mount_test.go b/layer/mount_test.go new file mode 100644 index 00000000..5967c2b9 --- /dev/null +++ b/layer/mount_test.go @@ -0,0 +1,230 @@ +package layer + +import ( + "io/ioutil" + "os" + "path/filepath" + "runtime" + "sort" + "testing" + + "github.com/docker/docker/pkg/archive" +) + +func TestMountInit(t *testing.T) { + // TODO Windows: Figure out why this is failing + if runtime.GOOS == "windows" { + t.Skip("Failing on Windows") + } + ls, _, cleanup := newTestStore(t) + defer cleanup() + + basefile := newTestFile("testfile.txt", []byte("base data!"), 0644) + initfile := newTestFile("testfile.txt", []byte("init data!"), 0777) + + li := initWithFiles(basefile) + layer, err := createLayer(ls, "", li) + if err != nil { + t.Fatal(err) + } + + mountInit := func(root string) error { + return initfile.ApplyFile(root) + } + + m, err := ls.CreateRWLayer("fun-mount", layer.ChainID(), "", mountInit) + if err != nil { + t.Fatal(err) + } + + path, err := m.Mount("") + if err != nil { + t.Fatal(err) + } + + f, err := os.Open(filepath.Join(path, "testfile.txt")) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + t.Fatal(err) + } + + b, err := ioutil.ReadAll(f) + if err != nil { + t.Fatal(err) + } + + if expected := "init data!"; string(b) != expected { + t.Fatalf("Unexpected test file contents %q, expected %q", string(b), expected) + } + + if fi.Mode().Perm() != 0777 { + t.Fatalf("Unexpected filemode %o, expecting %o", fi.Mode().Perm(), 0777) + } +} + +func TestMountSize(t *testing.T) { + // TODO Windows: Figure out why this is failing + if runtime.GOOS == "windows" { + t.Skip("Failing on Windows") + } + ls, _, cleanup := newTestStore(t) + defer cleanup() + + content1 := []byte("Base contents") + content2 := []byte("Mutable contents") + contentInit := []byte("why am I excluded from the size ☹") + + li := initWithFiles(newTestFile("file1", content1, 0644)) + layer, err := createLayer(ls, "", li) + if err != nil { + t.Fatal(err) + } + + mountInit := func(root string) error { + return newTestFile("file-init", contentInit, 0777).ApplyFile(root) + } + + m, err := ls.CreateRWLayer("mount-size", layer.ChainID(), "", mountInit) + if err != nil { + t.Fatal(err) + } + + path, err := m.Mount("") + if err != nil { + t.Fatal(err) + } + + if err := ioutil.WriteFile(filepath.Join(path, "file2"), content2, 0755); err != nil { + t.Fatal(err) + } + + mountSize, err := m.Size() + if err != nil { + t.Fatal(err) + } + + if expected := len(content2); int(mountSize) != expected { + t.Fatalf("Unexpected mount size %d, expected %d", int(mountSize), expected) + } +} + +func TestMountChanges(t *testing.T) { + // TODO Windows: Figure out why this is failing + if runtime.GOOS == "windows" { + t.Skip("Failing on Windows") + } + ls, _, cleanup := newTestStore(t) + defer cleanup() + + basefiles := []FileApplier{ + newTestFile("testfile1.txt", []byte("base data!"), 0644), + newTestFile("testfile2.txt", []byte("base data!"), 0644), + newTestFile("testfile3.txt", []byte("base data!"), 0644), + } + initfile := newTestFile("testfile1.txt", []byte("init data!"), 0777) + + li := initWithFiles(basefiles...) + layer, err := createLayer(ls, "", li) + if err != nil { + t.Fatal(err) + } + + mountInit := func(root string) error { + return initfile.ApplyFile(root) + } + + m, err := ls.CreateRWLayer("mount-changes", layer.ChainID(), "", mountInit) + if err != nil { + t.Fatal(err) + } + + path, err := m.Mount("") + if err != nil { + t.Fatal(err) + } + + if err := os.Chmod(filepath.Join(path, "testfile1.txt"), 0755); err != nil { + t.Fatal(err) + } + + if err := ioutil.WriteFile(filepath.Join(path, "testfile1.txt"), []byte("mount data!"), 0755); err != nil { + t.Fatal(err) + } + + if err := os.Remove(filepath.Join(path, "testfile2.txt")); err != nil { + t.Fatal(err) + } + + if err := os.Chmod(filepath.Join(path, "testfile3.txt"), 0755); err != nil { + t.Fatal(err) + } + + if err := ioutil.WriteFile(filepath.Join(path, "testfile4.txt"), []byte("mount data!"), 0644); err != nil { + t.Fatal(err) + } + + changes, err := m.Changes() + if err != nil { + t.Fatal(err) + } + + if expected := 4; len(changes) != expected { + t.Fatalf("Wrong number of changes %d, expected %d", len(changes), expected) + } + + sortChanges(changes) + + assertChange(t, changes[0], archive.Change{ + Path: "/testfile1.txt", + Kind: archive.ChangeModify, + }) + assertChange(t, changes[1], archive.Change{ + Path: "/testfile2.txt", + Kind: archive.ChangeDelete, + }) + assertChange(t, changes[2], archive.Change{ + Path: "/testfile3.txt", + Kind: archive.ChangeModify, + }) + assertChange(t, changes[3], archive.Change{ + Path: "/testfile4.txt", + Kind: archive.ChangeAdd, + }) +} + +func assertChange(t *testing.T, actual, expected archive.Change) { + if actual.Path != expected.Path { + t.Fatalf("Unexpected change path %s, expected %s", actual.Path, expected.Path) + } + if actual.Kind != expected.Kind { + t.Fatalf("Unexpected change type %s, expected %s", actual.Kind, expected.Kind) + } +} + +func sortChanges(changes []archive.Change) { + cs := &changeSorter{ + changes: changes, + } + sort.Sort(cs) +} + +type changeSorter struct { + changes []archive.Change +} + +func (cs *changeSorter) Len() int { + return len(cs.changes) +} + +func (cs *changeSorter) Swap(i, j int) { + cs.changes[i], cs.changes[j] = cs.changes[j], cs.changes[i] +} + +func (cs *changeSorter) Less(i, j int) bool { + return cs.changes[i].Path < cs.changes[j].Path +} diff --git a/layer/mounted_layer.go b/layer/mounted_layer.go new file mode 100644 index 00000000..5a07fd08 --- /dev/null +++ b/layer/mounted_layer.go @@ -0,0 +1,188 @@ +package layer + +import ( + "io" + "sync" + + "github.com/docker/docker/pkg/archive" +) + +type mountedLayer struct { + name string + mountID string + initID string + parent *roLayer + path string + layerStore *layerStore + + references map[RWLayer]*referencedRWLayer +} + +func (ml *mountedLayer) cacheParent() string { + if ml.initID != "" { + return ml.initID + } + if ml.parent != nil { + return ml.parent.cacheID + } + return "" +} + +func (ml *mountedLayer) TarStream() (io.ReadCloser, error) { + archiver, err := ml.layerStore.driver.Diff(ml.mountID, ml.cacheParent()) + if err != nil { + return nil, err + } + return archiver, nil +} + +func (ml *mountedLayer) Name() string { + return ml.name +} + +func (ml *mountedLayer) Parent() Layer { + if ml.parent != nil { + return ml.parent + } + + // Return a nil interface instead of an interface wrapping a nil + // pointer. + return nil +} + +func (ml *mountedLayer) Mount(mountLabel string) (string, error) { + return ml.layerStore.driver.Get(ml.mountID, mountLabel) +} + +func (ml *mountedLayer) Unmount() error { + return ml.layerStore.driver.Put(ml.mountID) +} + +func (ml *mountedLayer) Size() (int64, error) { + return ml.layerStore.driver.DiffSize(ml.mountID, ml.cacheParent()) +} + +func (ml *mountedLayer) Changes() ([]archive.Change, error) { + return ml.layerStore.driver.Changes(ml.mountID, ml.cacheParent()) +} + +func (ml *mountedLayer) Metadata() (map[string]string, error) { + return ml.layerStore.driver.GetMetadata(ml.mountID) +} + +func (ml *mountedLayer) getReference() RWLayer { + ref := &referencedRWLayer{ + mountedLayer: ml, + } + ml.references[ref] = ref + + return ref +} + +func (ml *mountedLayer) hasReferences() bool { + return len(ml.references) > 0 +} + +func (ml *mountedLayer) incActivityCount(ref RWLayer) error { + rl, ok := ml.references[ref] + if !ok { + return ErrLayerNotRetained + } + + if err := rl.acquire(); err != nil { + return err + } + return nil +} + +func (ml *mountedLayer) deleteReference(ref RWLayer) error { + rl, ok := ml.references[ref] + if !ok { + return ErrLayerNotRetained + } + + if err := rl.release(); err != nil { + return err + } + delete(ml.references, ref) + + return nil +} + +func (ml *mountedLayer) retakeReference(r RWLayer) { + if ref, ok := r.(*referencedRWLayer); ok { + ref.activityCount = 0 + ml.references[ref] = ref + } +} + +type referencedRWLayer struct { + *mountedLayer + + activityL sync.Mutex + activityCount int +} + +func (rl *referencedRWLayer) acquire() error { + rl.activityL.Lock() + defer rl.activityL.Unlock() + + rl.activityCount++ + + return nil +} + +func (rl *referencedRWLayer) release() error { + rl.activityL.Lock() + defer rl.activityL.Unlock() + + if rl.activityCount > 0 { + return ErrActiveMount + } + + rl.activityCount = -1 + + return nil +} + +func (rl *referencedRWLayer) Mount(mountLabel string) (string, error) { + rl.activityL.Lock() + defer rl.activityL.Unlock() + + if rl.activityCount == -1 { + return "", ErrLayerNotRetained + } + + if rl.activityCount > 0 { + rl.activityCount++ + return rl.path, nil + } + + m, err := rl.mountedLayer.Mount(mountLabel) + if err == nil { + rl.activityCount++ + rl.path = m + } + return m, err +} + +// Unmount decrements the activity count and unmounts the underlying layer +// Callers should only call `Unmount` once per call to `Mount`, even on error. +func (rl *referencedRWLayer) Unmount() error { + rl.activityL.Lock() + defer rl.activityL.Unlock() + + if rl.activityCount == 0 { + return ErrNotMounted + } + if rl.activityCount == -1 { + return ErrLayerNotRetained + } + + rl.activityCount-- + if rl.activityCount > 0 { + return nil + } + + return rl.mountedLayer.Unmount() +} diff --git a/layer/ro_layer.go b/layer/ro_layer.go new file mode 100644 index 00000000..92b0ea0e --- /dev/null +++ b/layer/ro_layer.go @@ -0,0 +1,164 @@ +package layer + +import ( + "fmt" + "io" + + "github.com/docker/distribution/digest" +) + +type roLayer struct { + chainID ChainID + diffID DiffID + parent *roLayer + cacheID string + size int64 + layerStore *layerStore + + referenceCount int + references map[Layer]struct{} +} + +func (rl *roLayer) TarStream() (io.ReadCloser, error) { + r, err := rl.layerStore.store.TarSplitReader(rl.chainID) + if err != nil { + return nil, err + } + + pr, pw := io.Pipe() + go func() { + err := rl.layerStore.assembleTarTo(rl.cacheID, r, nil, pw) + if err != nil { + pw.CloseWithError(err) + } else { + pw.Close() + } + }() + rc, err := newVerifiedReadCloser(pr, digest.Digest(rl.diffID)) + if err != nil { + return nil, err + } + return rc, nil +} + +func (rl *roLayer) ChainID() ChainID { + return rl.chainID +} + +func (rl *roLayer) DiffID() DiffID { + return rl.diffID +} + +func (rl *roLayer) Parent() Layer { + if rl.parent == nil { + return nil + } + return rl.parent +} + +func (rl *roLayer) Size() (size int64, err error) { + if rl.parent != nil { + size, err = rl.parent.Size() + if err != nil { + return + } + } + + return size + rl.size, nil +} + +func (rl *roLayer) DiffSize() (size int64, err error) { + return rl.size, nil +} + +func (rl *roLayer) Metadata() (map[string]string, error) { + return rl.layerStore.driver.GetMetadata(rl.cacheID) +} + +type referencedCacheLayer struct { + *roLayer +} + +func (rl *roLayer) getReference() Layer { + ref := &referencedCacheLayer{ + roLayer: rl, + } + rl.references[ref] = struct{}{} + + return ref +} + +func (rl *roLayer) hasReference(ref Layer) bool { + _, ok := rl.references[ref] + return ok +} + +func (rl *roLayer) hasReferences() bool { + return len(rl.references) > 0 +} + +func (rl *roLayer) deleteReference(ref Layer) { + delete(rl.references, ref) +} + +func (rl *roLayer) depth() int { + if rl.parent == nil { + return 1 + } + return rl.parent.depth() + 1 +} + +func storeLayer(tx MetadataTransaction, layer *roLayer) error { + if err := tx.SetDiffID(layer.diffID); err != nil { + return err + } + if err := tx.SetSize(layer.size); err != nil { + return err + } + if err := tx.SetCacheID(layer.cacheID); err != nil { + return err + } + if layer.parent != nil { + if err := tx.SetParent(layer.parent.chainID); err != nil { + return err + } + } + + return nil +} + +func newVerifiedReadCloser(rc io.ReadCloser, dgst digest.Digest) (io.ReadCloser, error) { + verifier, err := digest.NewDigestVerifier(dgst) + if err != nil { + return nil, err + } + return &verifiedReadCloser{ + rc: rc, + dgst: dgst, + verifier: verifier, + }, nil +} + +type verifiedReadCloser struct { + rc io.ReadCloser + dgst digest.Digest + verifier digest.Verifier +} + +func (vrc *verifiedReadCloser) Read(p []byte) (n int, err error) { + n, err = vrc.rc.Read(p) + if n > 0 { + if n, err := vrc.verifier.Write(p[:n]); err != nil { + return n, err + } + } + if err == io.EOF { + if !vrc.verifier.Verified() { + err = fmt.Errorf("could not verify layer data for: %s. This may be because internal files in the layer store were modified. Re-pulling or rebuilding this image may resolve the issue", vrc.dgst) + } + } + return +} +func (vrc *verifiedReadCloser) Close() error { + return vrc.rc.Close() +} diff --git a/libcontainerd/client.go b/libcontainerd/client.go new file mode 100644 index 00000000..7e8e47bc --- /dev/null +++ b/libcontainerd/client.go @@ -0,0 +1,46 @@ +package libcontainerd + +import ( + "fmt" + "sync" + + "github.com/docker/docker/pkg/locker" +) + +// clientCommon contains the platform agnostic fields used in the client structure +type clientCommon struct { + backend Backend + containers map[string]*container + locker *locker.Locker + mapMutex sync.RWMutex // protects read/write oprations from containers map +} + +func (clnt *client) lock(containerID string) { + clnt.locker.Lock(containerID) +} + +func (clnt *client) unlock(containerID string) { + clnt.locker.Unlock(containerID) +} + +// must hold a lock for cont.containerID +func (clnt *client) appendContainer(cont *container) { + clnt.mapMutex.Lock() + clnt.containers[cont.containerID] = cont + clnt.mapMutex.Unlock() +} +func (clnt *client) deleteContainer(friendlyName string) { + clnt.mapMutex.Lock() + delete(clnt.containers, friendlyName) + clnt.mapMutex.Unlock() +} + +func (clnt *client) getContainer(containerID string) (*container, error) { + clnt.mapMutex.RLock() + container, ok := clnt.containers[containerID] + defer clnt.mapMutex.RUnlock() + if !ok { + return nil, fmt.Errorf("invalid container: %s", containerID) // fixme: typed error + } + return container, nil +} diff --git a/libcontainerd/client_linux.go b/libcontainerd/client_linux.go new file mode 100644 index 00000000..8eab7512 --- /dev/null +++ b/libcontainerd/client_linux.go @@ -0,0 +1,401 @@ +package libcontainerd + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "sync" + "syscall" + + "github.com/Sirupsen/logrus" + containerd "github.com/docker/containerd/api/grpc/types" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/mount" + "github.com/opencontainers/specs/specs-go" + "golang.org/x/net/context" +) + +type client struct { + clientCommon + + // Platform specific properties below here. + remote *remote + q queue + exitNotifiers map[string]*exitNotifier +} + +func (clnt *client) AddProcess(containerID, processFriendlyName string, specp Process) error { + clnt.lock(containerID) + defer clnt.unlock(containerID) + container, err := clnt.getContainer(containerID) + if err != nil { + return err + } + + spec, err := container.spec() + if err != nil { + return err + } + sp := spec.Process + sp.Args = specp.Args + sp.Terminal = specp.Terminal + if specp.Env != nil { + sp.Env = specp.Env + } + if specp.Cwd != nil { + sp.Cwd = *specp.Cwd + } + if specp.User != nil { + sp.User = specs.User{ + UID: specp.User.UID, + GID: specp.User.GID, + AdditionalGids: specp.User.AdditionalGids, + } + } + if specp.Capabilities != nil { + sp.Capabilities = specp.Capabilities + } + + p := container.newProcess(processFriendlyName) + + r := &containerd.AddProcessRequest{ + Args: sp.Args, + Cwd: sp.Cwd, + Terminal: sp.Terminal, + Id: containerID, + Env: sp.Env, + User: &containerd.User{ + Uid: sp.User.UID, + Gid: sp.User.GID, + AdditionalGids: sp.User.AdditionalGids, + }, + Pid: processFriendlyName, + Stdin: p.fifo(syscall.Stdin), + Stdout: p.fifo(syscall.Stdout), + Stderr: p.fifo(syscall.Stderr), + Capabilities: sp.Capabilities, + ApparmorProfile: sp.ApparmorProfile, + SelinuxLabel: sp.SelinuxLabel, + NoNewPrivileges: sp.NoNewPrivileges, + Rlimits: convertRlimits(sp.Rlimits), + } + + iopipe, err := p.openFifos(sp.Terminal) + if err != nil { + return err + } + + if _, err := clnt.remote.apiClient.AddProcess(context.Background(), r); err != nil { + p.closeFifos(iopipe) + return err + } + + container.processes[processFriendlyName] = p + + clnt.unlock(containerID) + + if err := clnt.backend.AttachStreams(processFriendlyName, *iopipe); err != nil { + return err + } + clnt.lock(containerID) + + return nil +} + +func (clnt *client) prepareBundleDir(uid, gid int) (string, error) { + root, err := filepath.Abs(clnt.remote.stateDir) + if err != nil { + return "", err + } + if uid == 0 && gid == 0 { + return root, nil + } + p := string(filepath.Separator) + for _, d := range strings.Split(root, string(filepath.Separator))[1:] { + p = filepath.Join(p, d) + fi, err := os.Stat(p) + if err != nil && !os.IsNotExist(err) { + return "", err + } + if os.IsNotExist(err) || fi.Mode()&1 == 0 { + p = fmt.Sprintf("%s.%d.%d", p, uid, gid) + if err := idtools.MkdirAs(p, 0700, uid, gid); err != nil && !os.IsExist(err) { + return "", err + } + } + } + return p, nil +} + +func (clnt *client) Create(containerID string, spec Spec, options ...CreateOption) (err error) { + clnt.lock(containerID) + defer clnt.unlock(containerID) + + if ctr, err := clnt.getContainer(containerID); err == nil { + if ctr.restarting { + ctr.restartManager.Cancel() + ctr.clean() + } else { + return fmt.Errorf("Container %s is aleady active", containerID) + } + } + + uid, gid, err := getRootIDs(specs.Spec(spec)) + if err != nil { + return err + } + dir, err := clnt.prepareBundleDir(uid, gid) + if err != nil { + return err + } + + container := clnt.newContainer(filepath.Join(dir, containerID), options...) + if err := container.clean(); err != nil { + return err + } + + defer func() { + if err != nil { + container.clean() + clnt.deleteContainer(containerID) + } + }() + + if err := idtools.MkdirAllAs(container.dir, 0700, uid, gid); err != nil && !os.IsExist(err) { + return err + } + + f, err := os.Create(filepath.Join(container.dir, configFilename)) + if err != nil { + return err + } + defer f.Close() + if err := json.NewEncoder(f).Encode(spec); err != nil { + return err + } + + return container.start() +} + +func (clnt *client) Signal(containerID string, sig int) error { + clnt.lock(containerID) + defer clnt.unlock(containerID) + _, err := clnt.remote.apiClient.Signal(context.Background(), &containerd.SignalRequest{ + Id: containerID, + Pid: InitFriendlyName, + Signal: uint32(sig), + }) + return err +} + +func (clnt *client) Resize(containerID, processFriendlyName string, width, height int) error { + clnt.lock(containerID) + defer clnt.unlock(containerID) + if _, err := clnt.getContainer(containerID); err != nil { + return err + } + _, err := clnt.remote.apiClient.UpdateProcess(context.Background(), &containerd.UpdateProcessRequest{ + Id: containerID, + Pid: processFriendlyName, + Width: uint32(width), + Height: uint32(height), + }) + return err +} + +func (clnt *client) Pause(containerID string) error { + return clnt.setState(containerID, StatePause) +} + +func (clnt *client) setState(containerID, state string) error { + clnt.lock(containerID) + container, err := clnt.getContainer(containerID) + if err != nil { + clnt.unlock(containerID) + return err + } + if container.systemPid == 0 { + clnt.unlock(containerID) + return fmt.Errorf("No active process for container %s", containerID) + } + st := "running" + if state == StatePause { + st = "paused" + } + chstate := make(chan struct{}) + _, err = clnt.remote.apiClient.UpdateContainer(context.Background(), &containerd.UpdateContainerRequest{ + Id: containerID, + Pid: InitFriendlyName, + Status: st, + }) + if err != nil { + clnt.unlock(containerID) + return err + } + container.pauseMonitor.append(state, chstate) + clnt.unlock(containerID) + <-chstate + return nil +} + +func (clnt *client) Resume(containerID string) error { + return clnt.setState(containerID, StateResume) +} + +func (clnt *client) Stats(containerID string) (*Stats, error) { + resp, err := clnt.remote.apiClient.Stats(context.Background(), &containerd.StatsRequest{containerID}) + if err != nil { + return nil, err + } + return (*Stats)(resp), nil +} + +// Take care of the old 1.11.0 behavior in case the version upgrade +// happenned without a clean daemon shutdown +func (clnt *client) cleanupOldRootfs(containerID string) { + // Unmount and delete the bundle folder + if mts, err := mount.GetMounts(); err == nil { + for _, mts := range mts { + if strings.HasSuffix(mts.Mountpoint, containerID+"/rootfs") { + if err := syscall.Unmount(mts.Mountpoint, syscall.MNT_DETACH); err == nil { + os.RemoveAll(strings.TrimSuffix(mts.Mountpoint, "/rootfs")) + } + break + } + } + } +} + +func (clnt *client) setExited(containerID string) error { + clnt.lock(containerID) + defer clnt.unlock(containerID) + + var exitCode uint32 + if event, ok := clnt.remote.pastEvents[containerID]; ok { + exitCode = event.Status + delete(clnt.remote.pastEvents, containerID) + } + + err := clnt.backend.StateChanged(containerID, StateInfo{ + State: StateExit, + ExitCode: exitCode, + }) + + clnt.cleanupOldRootfs(containerID) + + return err +} + +func (clnt *client) GetPidsForContainer(containerID string) ([]int, error) { + cont, err := clnt.getContainerdContainer(containerID) + if err != nil { + return nil, err + } + pids := make([]int, len(cont.Pids)) + for i, p := range cont.Pids { + pids[i] = int(p) + } + return pids, nil +} + +// Summary returns a summary of the processes running in a container. +// This is a no-op on Linux. +func (clnt *client) Summary(containerID string) ([]Summary, error) { + return nil, nil +} + +func (clnt *client) getContainerdContainer(containerID string) (*containerd.Container, error) { + resp, err := clnt.remote.apiClient.State(context.Background(), &containerd.StateRequest{Id: containerID}) + if err != nil { + return nil, err + } + for _, cont := range resp.Containers { + if cont.Id == containerID { + return cont, nil + } + } + return nil, fmt.Errorf("invalid state response") +} + +func (clnt *client) newContainer(dir string, options ...CreateOption) *container { + container := &container{ + containerCommon: containerCommon{ + process: process{ + dir: dir, + processCommon: processCommon{ + containerID: filepath.Base(dir), + client: clnt, + friendlyName: InitFriendlyName, + }, + }, + processes: make(map[string]*process), + }, + } + for _, option := range options { + if err := option.Apply(container); err != nil { + logrus.Error(err) + } + } + return container +} + +func (clnt *client) UpdateResources(containerID string, resources Resources) error { + clnt.lock(containerID) + defer clnt.unlock(containerID) + container, err := clnt.getContainer(containerID) + if err != nil { + return err + } + if container.systemPid == 0 { + return fmt.Errorf("No active process for container %s", containerID) + } + _, err = clnt.remote.apiClient.UpdateContainer(context.Background(), &containerd.UpdateContainerRequest{ + Id: containerID, + Pid: InitFriendlyName, + Resources: (*containerd.UpdateResource)(&resources), + }) + if err != nil { + return err + } + return nil +} + +func (clnt *client) getExitNotifier(containerID string) *exitNotifier { + clnt.mapMutex.RLock() + defer clnt.mapMutex.RUnlock() + return clnt.exitNotifiers[containerID] +} + +func (clnt *client) getOrCreateExitNotifier(containerID string) *exitNotifier { + clnt.mapMutex.Lock() + w, ok := clnt.exitNotifiers[containerID] + defer clnt.mapMutex.Unlock() + if !ok { + w = &exitNotifier{c: make(chan struct{}), client: clnt} + clnt.exitNotifiers[containerID] = w + } + return w +} + +type exitNotifier struct { + id string + client *client + c chan struct{} + once sync.Once +} + +func (en *exitNotifier) close() { + en.once.Do(func() { + close(en.c) + en.client.mapMutex.Lock() + if en == en.client.exitNotifiers[en.id] { + delete(en.client.exitNotifiers, en.id) + } + en.client.mapMutex.Unlock() + }) +} +func (en *exitNotifier) wait() <-chan struct{} { + return en.c +} diff --git a/libcontainerd/client_liverestore_linux.go b/libcontainerd/client_liverestore_linux.go new file mode 100644 index 00000000..1a1f7fe7 --- /dev/null +++ b/libcontainerd/client_liverestore_linux.go @@ -0,0 +1,83 @@ +// +build experimental + +package libcontainerd + +import ( + "fmt" + + "github.com/Sirupsen/logrus" + containerd "github.com/docker/containerd/api/grpc/types" +) + +func (clnt *client) restore(cont *containerd.Container, options ...CreateOption) (err error) { + clnt.lock(cont.Id) + defer clnt.unlock(cont.Id) + + logrus.Debugf("restore container %s state %s", cont.Id, cont.Status) + + containerID := cont.Id + if _, err := clnt.getContainer(containerID); err == nil { + return fmt.Errorf("container %s is aleady active", containerID) + } + + defer func() { + if err != nil { + clnt.deleteContainer(cont.Id) + } + }() + + container := clnt.newContainer(cont.BundlePath, options...) + container.systemPid = systemPid(cont) + + var terminal bool + for _, p := range cont.Processes { + if p.Pid == InitFriendlyName { + terminal = p.Terminal + } + } + + iopipe, err := container.openFifos(terminal) + if err != nil { + return err + } + + if err := clnt.backend.AttachStreams(containerID, *iopipe); err != nil { + return err + } + + clnt.appendContainer(container) + + err = clnt.backend.StateChanged(containerID, StateInfo{ + State: StateRestore, + Pid: container.systemPid, + }) + + if err != nil { + return err + } + + if event, ok := clnt.remote.pastEvents[containerID]; ok { + // This should only be a pause or resume event + if event.Type == StatePause || event.Type == StateResume { + return clnt.backend.StateChanged(containerID, StateInfo{ + State: event.Type, + Pid: container.systemPid, + }) + } + + logrus.Warnf("unexpected backlog event: %#v", event) + } + + return nil +} + +func (clnt *client) Restore(containerID string, options ...CreateOption) error { + cont, err := clnt.getContainerdContainer(containerID) + if err == nil && cont.Status != "stopped" { + if err := clnt.restore(cont, options...); err != nil { + logrus.Errorf("error restoring %s: %v", containerID, err) + } + return nil + } + return clnt.setExited(containerID) +} diff --git a/libcontainerd/client_shutdownrestore_linux.go b/libcontainerd/client_shutdownrestore_linux.go new file mode 100644 index 00000000..1b4a2bc5 --- /dev/null +++ b/libcontainerd/client_shutdownrestore_linux.go @@ -0,0 +1,46 @@ +// +build !experimental + +package libcontainerd + +import ( + "syscall" + "time" + + "github.com/Sirupsen/logrus" +) + +func (clnt *client) Restore(containerID string, options ...CreateOption) error { + w := clnt.getOrCreateExitNotifier(containerID) + defer w.close() + cont, err := clnt.getContainerdContainer(containerID) + if err == nil && cont.Status != "stopped" { + clnt.lock(cont.Id) + container := clnt.newContainer(cont.BundlePath) + container.systemPid = systemPid(cont) + clnt.appendContainer(container) + clnt.unlock(cont.Id) + + container.discardFifos() + + if err := clnt.Signal(containerID, int(syscall.SIGTERM)); err != nil { + logrus.Errorf("error sending sigterm to %v: %v", containerID, err) + } + select { + case <-time.After(10 * time.Second): + if err := clnt.Signal(containerID, int(syscall.SIGKILL)); err != nil { + logrus.Errorf("error sending sigkill to %v: %v", containerID, err) + } + select { + case <-time.After(2 * time.Second): + case <-w.wait(): + return nil + } + case <-w.wait(): + return nil + } + } + + clnt.deleteContainer(containerID) + + return clnt.setExited(containerID) +} diff --git a/libcontainerd/client_windows.go b/libcontainerd/client_windows.go new file mode 100644 index 00000000..50cb4168 --- /dev/null +++ b/libcontainerd/client_windows.go @@ -0,0 +1,567 @@ +package libcontainerd + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "path/filepath" + "strconv" + "strings" + + "syscall" + "time" + + "github.com/Microsoft/hcsshim" + "github.com/Sirupsen/logrus" +) + +type client struct { + clientCommon + + // Platform specific properties below here (none presently on Windows) +} + +// defaultContainerNAT is the default name of the container NAT device that is +// preconfigured on the server. TODO Windows - Remove for TP5 support as not needed. +const defaultContainerNAT = "ContainerNAT" + +// Win32 error codes that are used for various workarounds +// These really should be ALL_CAPS to match golangs syscall library and standard +// Win32 error conventions, but golint insists on CamelCase. +const ( + CoEClassstring = syscall.Errno(0x800401F3) // Invalid class string + ErrorNoNetwork = syscall.Errno(1222) // The network is not present or not started + ErrorBadPathname = syscall.Errno(161) // The specified path is invalid + ErrorInvalidObject = syscall.Errno(0x800710D8) // The object identifier does not represent a valid object +) + +type layer struct { + ID string + Path string +} + +type defConfig struct { + DefFile string +} + +type portBinding struct { + Protocol string + InternalPort int + ExternalPort int +} + +type natSettings struct { + Name string + PortBindings []portBinding +} + +type networkConnection struct { + NetworkName string + Nat natSettings +} +type networkSettings struct { + MacAddress string +} + +type device struct { + DeviceType string + Connection interface{} + Settings interface{} +} + +type mappedDir struct { + HostPath string + ContainerPath string + ReadOnly bool +} + +// TODO Windows: @darrenstahlmsft Add ProcessorCount +type containerInit struct { + SystemType string // HCS requires this to be hard-coded to "Container" + Name string // Name of the container. We use the docker ID. + Owner string // The management platform that created this container + IsDummy bool // Used for development purposes. + VolumePath string // Windows volume path for scratch space + Devices []device // Devices used by the container + IgnoreFlushesDuringBoot bool // Optimization hint for container startup in Windows + LayerFolderPath string // Where the layer folders are located + Layers []layer // List of storage layers + ProcessorWeight uint64 `json:",omitempty"` // CPU Shares 0..10000 on Windows; where 0 will be omitted and HCS will default. + ProcessorMaximum int64 `json:",omitempty"` // CPU maximum usage percent 1..100 + StorageIOPSMaximum uint64 `json:",omitempty"` // Maximum Storage IOPS + StorageBandwidthMaximum uint64 `json:",omitempty"` // Maximum Storage Bandwidth in bytes per second + StorageSandboxSize uint64 `json:",omitempty"` // Size in bytes that the container system drive should be expanded to if smaller + MemoryMaximumInMB int64 `json:",omitempty"` // Maximum memory available to the container in Megabytes + HostName string // Hostname + MappedDirectories []mappedDir // List of mapped directories (volumes/mounts) + SandboxPath string // Location of unmounted sandbox (used for Hyper-V containers) + HvPartition bool // True if it a Hyper-V Container + EndpointList []string // List of networking endpoints to be attached to container +} + +// defaultOwner is a tag passed to HCS to allow it to differentiate between +// container creator management stacks. We hard code "docker" in the case +// of docker. +const defaultOwner = "docker" + +// Create is the entrypoint to create a container from a spec, and if successfully +// created, start it too. +func (clnt *client) Create(containerID string, spec Spec, options ...CreateOption) error { + logrus.Debugln("LCD client.Create() with spec", spec) + + cu := &containerInit{ + SystemType: "Container", + Name: containerID, + Owner: defaultOwner, + + VolumePath: spec.Root.Path, + IgnoreFlushesDuringBoot: spec.Windows.FirstStart, + LayerFolderPath: spec.Windows.LayerFolder, + HostName: spec.Hostname, + } + + if spec.Windows.Networking != nil { + cu.EndpointList = spec.Windows.Networking.EndpointList + } + + if spec.Windows.Resources != nil { + if spec.Windows.Resources.CPU != nil { + if spec.Windows.Resources.CPU.Shares != nil { + cu.ProcessorWeight = *spec.Windows.Resources.CPU.Shares + } + if spec.Windows.Resources.CPU.Percent != nil { + cu.ProcessorMaximum = *spec.Windows.Resources.CPU.Percent * 100 // ProcessorMaximum is a value between 1 and 10000 + } + } + if spec.Windows.Resources.Memory != nil { + if spec.Windows.Resources.Memory.Limit != nil { + cu.MemoryMaximumInMB = *spec.Windows.Resources.Memory.Limit / 1024 / 1024 + } + } + if spec.Windows.Resources.Storage != nil { + if spec.Windows.Resources.Storage.Bps != nil { + cu.StorageBandwidthMaximum = *spec.Windows.Resources.Storage.Bps + } + if spec.Windows.Resources.Storage.Iops != nil { + cu.StorageIOPSMaximum = *spec.Windows.Resources.Storage.Iops + } + if spec.Windows.Resources.Storage.SandboxSize != nil { + cu.StorageSandboxSize = *spec.Windows.Resources.Storage.SandboxSize + } + } + } + + cu.HvPartition = (spec.Windows.HvRuntime != nil) + + // TODO Windows @jhowardmsft. FIXME post TP5. + // if spec.Windows.HvRuntime != nil { + // if spec.WIndows.HVRuntime.ImagePath != "" { + // cu.TBD = spec.Windows.HvRuntime.ImagePath + // } + // } + + if cu.HvPartition { + cu.SandboxPath = filepath.Dir(spec.Windows.LayerFolder) + } else { + cu.VolumePath = spec.Root.Path + cu.LayerFolderPath = spec.Windows.LayerFolder + } + + for _, layerPath := range spec.Windows.LayerPaths { + _, filename := filepath.Split(layerPath) + g, err := hcsshim.NameToGuid(filename) + if err != nil { + return err + } + cu.Layers = append(cu.Layers, layer{ + ID: g.ToString(), + Path: layerPath, + }) + } + + // Add the mounts (volumes, bind mounts etc) to the structure + mds := make([]mappedDir, len(spec.Mounts)) + for i, mount := range spec.Mounts { + mds[i] = mappedDir{ + HostPath: mount.Source, + ContainerPath: mount.Destination, + ReadOnly: mount.Readonly} + } + cu.MappedDirectories = mds + + // TODO Windows: vv START OF TP4 BLOCK OF CODE. REMOVE ONCE TP4 IS NO LONGER SUPPORTED + if hcsshim.IsTP4() && + spec.Windows.Networking != nil && + spec.Windows.Networking.Bridge != "" { + // Enumerate through the port bindings specified by the user and convert + // them into the internal structure matching the JSON blob that can be + // understood by the HCS. + var pbs []portBinding + for i, v := range spec.Windows.Networking.PortBindings { + proto := strings.ToUpper(i.Proto()) + if proto != "TCP" && proto != "UDP" { + return fmt.Errorf("invalid protocol %s", i.Proto()) + } + + if len(v) > 1 { + return fmt.Errorf("Windows does not support more than one host port in NAT settings") + } + + for _, v2 := range v { + var ( + iPort, ePort int + err error + ) + if len(v2.HostIP) != 0 { + return fmt.Errorf("Windows does not support host IP addresses in NAT settings") + } + if ePort, err = strconv.Atoi(v2.HostPort); err != nil { + return fmt.Errorf("invalid container port %s: %s", v2.HostPort, err) + } + if iPort, err = strconv.Atoi(i.Port()); err != nil { + return fmt.Errorf("invalid internal port %s: %s", i.Port(), err) + } + if iPort < 0 || iPort > 65535 || ePort < 0 || ePort > 65535 { + return fmt.Errorf("specified NAT port is not in allowed range") + } + pbs = append(pbs, + portBinding{ExternalPort: ePort, + InternalPort: iPort, + Protocol: proto}) + } + } + + dev := device{ + DeviceType: "Network", + Connection: &networkConnection{ + NetworkName: spec.Windows.Networking.Bridge, + Nat: natSettings{ + Name: defaultContainerNAT, + PortBindings: pbs, + }, + }, + } + + if spec.Windows.Networking.MacAddress != "" { + windowsStyleMAC := strings.Replace( + spec.Windows.Networking.MacAddress, ":", "-", -1) + dev.Settings = networkSettings{ + MacAddress: windowsStyleMAC, + } + } + cu.Devices = append(cu.Devices, dev) + } else { + logrus.Debugln("No network interface") + } + // TODO Windows: ^^ END OF TP4 BLOCK OF CODE. REMOVE ONCE TP4 IS NO LONGER SUPPORTED + + configurationb, err := json.Marshal(cu) + if err != nil { + return err + } + + configuration := string(configurationb) + + // TODO Windows TP5 timeframe. Remove when TP4 is no longer supported. + // The following a workaround for Windows TP4 which has a networking + // bug which fairly frequently returns an error. Back off and retry. + if !hcsshim.IsTP4() { + if err := hcsshim.CreateComputeSystem(containerID, configuration); err != nil { + return err + } + } else { + maxAttempts := 5 + for i := 1; i <= maxAttempts; i++ { + err = hcsshim.CreateComputeSystem(containerID, configuration) + if err == nil { + break + } + + if herr, ok := err.(*hcsshim.HcsError); ok { + if herr.Err != syscall.ERROR_NOT_FOUND && // Element not found + herr.Err != syscall.ERROR_FILE_NOT_FOUND && // The system cannot find the file specified + herr.Err != ErrorNoNetwork && // The network is not present or not started + herr.Err != ErrorBadPathname && // The specified path is invalid + herr.Err != CoEClassstring && // Invalid class string + herr.Err != ErrorInvalidObject { // The object identifier does not represent a valid object + logrus.Debugln("Failed to create temporary container ", err) + return err + } + logrus.Warnf("Invoking Windows TP4 retry hack (%d of %d)", i, maxAttempts-1) + time.Sleep(50 * time.Millisecond) + } + } + } + + // Construct a container object for calling start on it. + container := &container{ + containerCommon: containerCommon{ + process: process{ + processCommon: processCommon{ + containerID: containerID, + client: clnt, + friendlyName: InitFriendlyName, + }, + commandLine: strings.Join(spec.Process.Args, " "), + }, + processes: make(map[string]*process), + }, + ociSpec: spec, + } + + container.options = options + for _, option := range options { + if err := option.Apply(container); err != nil { + logrus.Error(err) + } + } + + // Call start, and if it fails, delete the container from our + // internal structure, and also keep HCS in sync by deleting the + // container there. + logrus.Debugf("Create() id=%s, Calling start()", containerID) + if err := container.start(); err != nil { + clnt.deleteContainer(containerID) + return err + } + + logrus.Debugf("Create() id=%s completed successfully", containerID) + return nil + +} + +// AddProcess is the handler for adding a process to an already running +// container. It's called through docker exec. +func (clnt *client) AddProcess(containerID, processFriendlyName string, procToAdd Process) error { + + clnt.lock(containerID) + defer clnt.unlock(containerID) + container, err := clnt.getContainer(containerID) + if err != nil { + return err + } + + createProcessParms := hcsshim.CreateProcessParams{ + EmulateConsole: procToAdd.Terminal, + ConsoleSize: procToAdd.InitialConsoleSize, + } + + // Take working directory from the process to add if it is defined, + // otherwise take from the first process. + if procToAdd.Cwd != "" { + createProcessParms.WorkingDirectory = procToAdd.Cwd + } else { + createProcessParms.WorkingDirectory = container.ociSpec.Process.Cwd + } + + // Configure the environment for the process + createProcessParms.Environment = setupEnvironmentVariables(procToAdd.Env) + createProcessParms.CommandLine = strings.Join(procToAdd.Args, " ") + + logrus.Debugf("commandLine: %s", createProcessParms.CommandLine) + + // Start the command running in the container. Note we always tell HCS to + // create stdout as it's required regardless of '-i' or '-t' options, so that + // docker can always grab the output through logs. We also tell HCS to always + // create stdin, even if it's not used - it will be closed shortly. Stderr + // is only created if it we're not -t. + var stdout, stderr io.ReadCloser + var pid uint32 + iopipe := &IOPipe{Terminal: procToAdd.Terminal} + pid, iopipe.Stdin, stdout, stderr, err = hcsshim.CreateProcessInComputeSystem( + containerID, + true, + true, + !procToAdd.Terminal, + createProcessParms) + if err != nil { + logrus.Errorf("AddProcess %s CreateProcessInComputeSystem() failed %s", containerID, err) + return err + } + + // Convert io.ReadClosers to io.Readers + if stdout != nil { + iopipe.Stdout = openReaderFromPipe(stdout) + } + if stderr != nil { + iopipe.Stderr = openReaderFromPipe(stderr) + } + + // Add the process to the containers list of processes + container.processes[processFriendlyName] = + &process{ + processCommon: processCommon{ + containerID: containerID, + friendlyName: processFriendlyName, + client: clnt, + systemPid: pid, + }, + commandLine: createProcessParms.CommandLine, + } + + // Make sure the lock is not held while calling back into the daemon + clnt.unlock(containerID) + + // Tell the engine to attach streams back to the client + if err := clnt.backend.AttachStreams(processFriendlyName, *iopipe); err != nil { + return err + } + + // Lock again so that the defer unlock doesn't fail. (I really don't like this code) + clnt.lock(containerID) + + // Spin up a go routine waiting for exit to handle cleanup + go container.waitExit(pid, processFriendlyName, false) + + return nil +} + +// Signal handles `docker stop` on Windows. While Linux has support for +// the full range of signals, signals aren't really implemented on Windows. +// We fake supporting regular stop and -9 to force kill. +func (clnt *client) Signal(containerID string, sig int) error { + var ( + cont *container + err error + ) + + // Get the container as we need it to find the pid of the process. + clnt.lock(containerID) + defer clnt.unlock(containerID) + if cont, err = clnt.getContainer(containerID); err != nil { + return err + } + + logrus.Debugf("lcd: Signal() containerID=%s sig=%d pid=%d", containerID, sig, cont.systemPid) + context := fmt.Sprintf("Signal: sig=%d pid=%d", sig, cont.systemPid) + + if syscall.Signal(sig) == syscall.SIGKILL { + // Terminate the compute system + if err := hcsshim.TerminateComputeSystem(containerID, hcsshim.TimeoutInfinite, context); err != nil { + logrus.Errorf("Failed to terminate %s - %q", containerID, err) + } + + } else { + // Terminate Process + if err = hcsshim.TerminateProcessInComputeSystem(containerID, cont.systemPid); err != nil { + logrus.Warnf("Failed to terminate pid %d in %s: %q", cont.systemPid, containerID, err) + // Ignore errors + err = nil + } + + // Shutdown the compute system + if err := hcsshim.ShutdownComputeSystem(containerID, hcsshim.TimeoutInfinite, context); err != nil { + logrus.Errorf("Failed to shutdown %s - %q", containerID, err) + } + } + return nil +} + +// Resize handles a CLI event to resize an interactive docker run or docker exec +// window. +func (clnt *client) Resize(containerID, processFriendlyName string, width, height int) error { + // Get the libcontainerd container object + clnt.lock(containerID) + defer clnt.unlock(containerID) + cont, err := clnt.getContainer(containerID) + if err != nil { + return err + } + + if processFriendlyName == InitFriendlyName { + logrus.Debugln("Resizing systemPID in", containerID, cont.process.systemPid) + return hcsshim.ResizeConsoleInComputeSystem(containerID, cont.process.systemPid, height, width) + } + + for _, p := range cont.processes { + if p.friendlyName == processFriendlyName { + logrus.Debugln("Resizing exec'd process", containerID, p.systemPid) + return hcsshim.ResizeConsoleInComputeSystem(containerID, p.systemPid, height, width) + } + } + + return fmt.Errorf("Resize could not find containerID %s to resize", containerID) + +} + +// Pause handles pause requests for containers +func (clnt *client) Pause(containerID string) error { + return errors.New("Windows: Containers cannot be paused") +} + +// Resume handles resume requests for containers +func (clnt *client) Resume(containerID string) error { + return errors.New("Windows: Containers cannot be paused") +} + +// Stats handles stats requests for containers +func (clnt *client) Stats(containerID string) (*Stats, error) { + return nil, errors.New("Windows: Stats not implemented") +} + +// Restore is the handler for restoring a container +func (clnt *client) Restore(containerID string, unusedOnWindows ...CreateOption) error { + // TODO Windows: Implement this. For now, just tell the backend the container exited. + logrus.Debugf("lcd Restore %s", containerID) + return clnt.backend.StateChanged(containerID, StateInfo{ + State: StateExit, + ExitCode: 1 << 31, + }) +} + +// GetPidsForContainer returns a list of process IDs running in a container. +// Although implemented, this is not used in Windows. +func (clnt *client) GetPidsForContainer(containerID string) ([]int, error) { + var pids []int + clnt.lock(containerID) + defer clnt.unlock(containerID) + cont, err := clnt.getContainer(containerID) + if err != nil { + return nil, err + } + + // Add the first process + pids = append(pids, int(cont.containerCommon.systemPid)) + // And add all the exec'd processes + for _, p := range cont.processes { + pids = append(pids, int(p.processCommon.systemPid)) + } + return pids, nil +} + +// Summary returns a summary of the processes running in a container. +// This is present in Windows to support docker top. In linux, the +// engine shells out to ps to get process information. On Windows, as +// the containers could be Hyper-V containers, they would not be +// visible on the container host. However, libcontainerd does have +// that information. +func (clnt *client) Summary(containerID string) ([]Summary, error) { + var s []Summary + clnt.lock(containerID) + defer clnt.unlock(containerID) + cont, err := clnt.getContainer(containerID) + if err != nil { + return nil, err + } + + // Add the first process + s = append(s, Summary{ + Pid: cont.containerCommon.systemPid, + Command: cont.ociSpec.Process.Args[0]}) + // And add all the exec'd processes + for _, p := range cont.processes { + s = append(s, Summary{ + Pid: p.processCommon.systemPid, + Command: p.commandLine}) + } + return s, nil + +} + +// UpdateResources updates resources for a running container. +func (clnt *client) UpdateResources(containerID string, resources Resources) error { + // Updating resource isn't supported on Windows + // but we should return nil for enabling updating container + return nil +} diff --git a/libcontainerd/container.go b/libcontainerd/container.go new file mode 100644 index 00000000..30bc9502 --- /dev/null +++ b/libcontainerd/container.go @@ -0,0 +1,40 @@ +package libcontainerd + +import ( + "fmt" + "time" + + "github.com/docker/docker/restartmanager" +) + +const ( + // InitFriendlyName is the name given in the lookup map of processes + // for the first process started in a container. + InitFriendlyName = "init" + configFilename = "config.json" +) + +type containerCommon struct { + process + restartManager restartmanager.RestartManager + restarting bool + processes map[string]*process + startedAt time.Time +} + +// WithRestartManager sets the restartmanager to be used with the container. +func WithRestartManager(rm restartmanager.RestartManager) CreateOption { + return restartManager{rm} +} + +type restartManager struct { + rm restartmanager.RestartManager +} + +func (rm restartManager) Apply(p interface{}) error { + if pr, ok := p.(*container); ok { + pr.restartManager = rm.rm + return nil + } + return fmt.Errorf("WithRestartManager option not supported for this client") +} diff --git a/libcontainerd/container_linux.go b/libcontainerd/container_linux.go new file mode 100644 index 00000000..8a49cde2 --- /dev/null +++ b/libcontainerd/container_linux.go @@ -0,0 +1,209 @@ +package libcontainerd + +import ( + "encoding/json" + "io" + "io/ioutil" + "os" + "path/filepath" + "syscall" + "time" + + "github.com/Sirupsen/logrus" + containerd "github.com/docker/containerd/api/grpc/types" + "github.com/docker/docker/restartmanager" + "github.com/opencontainers/specs/specs-go" + "golang.org/x/net/context" +) + +type container struct { + containerCommon + + // Platform specific fields are below here. + pauseMonitor + oom bool +} + +func (ctr *container) clean() error { + if os.Getenv("LIBCONTAINERD_NOCLEAN") == "1" { + return nil + } + if _, err := os.Lstat(ctr.dir); err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + + if err := os.RemoveAll(ctr.dir); err != nil { + return err + } + return nil +} + +// cleanProcess removes the fifos used by an additional process. +// Caller needs to lock container ID before calling this method. +func (ctr *container) cleanProcess(id string) { + if p, ok := ctr.processes[id]; ok { + for _, i := range []int{syscall.Stdin, syscall.Stdout, syscall.Stderr} { + if err := os.Remove(p.fifo(i)); err != nil { + logrus.Warnf("failed to remove %v for process %v: %v", p.fifo(i), id, err) + } + } + } + delete(ctr.processes, id) +} + +func (ctr *container) spec() (*specs.Spec, error) { + var spec specs.Spec + dt, err := ioutil.ReadFile(filepath.Join(ctr.dir, configFilename)) + if err != nil { + return nil, err + } + if err := json.Unmarshal(dt, &spec); err != nil { + return nil, err + } + return &spec, nil +} + +func (ctr *container) start() error { + spec, err := ctr.spec() + if err != nil { + return nil + } + iopipe, err := ctr.openFifos(spec.Process.Terminal) + if err != nil { + return err + } + + r := &containerd.CreateContainerRequest{ + Id: ctr.containerID, + BundlePath: ctr.dir, + Stdin: ctr.fifo(syscall.Stdin), + Stdout: ctr.fifo(syscall.Stdout), + Stderr: ctr.fifo(syscall.Stderr), + // check to see if we are running in ramdisk to disable pivot root + NoPivotRoot: os.Getenv("DOCKER_RAMDISK") != "", + } + ctr.client.appendContainer(ctr) + + resp, err := ctr.client.remote.apiClient.CreateContainer(context.Background(), r) + if err != nil { + ctr.closeFifos(iopipe) + return err + } + ctr.startedAt = time.Now() + + if err := ctr.client.backend.AttachStreams(ctr.containerID, *iopipe); err != nil { + return err + } + ctr.systemPid = systemPid(resp.Container) + + return ctr.client.backend.StateChanged(ctr.containerID, StateInfo{ + State: StateStart, + Pid: ctr.systemPid, + }) +} + +func (ctr *container) newProcess(friendlyName string) *process { + return &process{ + dir: ctr.dir, + processCommon: processCommon{ + containerID: ctr.containerID, + friendlyName: friendlyName, + client: ctr.client, + }, + } +} + +func (ctr *container) handleEvent(e *containerd.Event) error { + ctr.client.lock(ctr.containerID) + defer ctr.client.unlock(ctr.containerID) + switch e.Type { + case StateExit, StatePause, StateResume, StateOOM: + st := StateInfo{ + State: e.Type, + ExitCode: e.Status, + OOMKilled: e.Type == StateExit && ctr.oom, + } + if e.Type == StateOOM { + ctr.oom = true + } + if e.Type == StateExit && e.Pid != InitFriendlyName { + st.ProcessID = e.Pid + st.State = StateExitProcess + } + if st.State == StateExit && ctr.restartManager != nil { + restart, wait, err := ctr.restartManager.ShouldRestart(e.Status, false, time.Since(ctr.startedAt)) + if err != nil { + logrus.Warnf("container %s %v", ctr.containerID, err) + } else if restart { + st.State = StateRestart + ctr.restarting = true + ctr.client.deleteContainer(e.Id) + go func() { + err := <-wait + ctr.client.lock(ctr.containerID) + defer ctr.client.unlock(ctr.containerID) + ctr.restarting = false + if err != nil { + st.State = StateExit + ctr.clean() + ctr.client.q.append(e.Id, func() { + if err := ctr.client.backend.StateChanged(e.Id, st); err != nil { + logrus.Error(err) + } + }) + if err != restartmanager.ErrRestartCanceled { + logrus.Error(err) + } + } else { + ctr.start() + } + }() + } + } + + // Remove process from list if we have exited + // We need to do so here in case the Message Handler decides to restart it. + switch st.State { + case StateExit: + ctr.clean() + ctr.client.deleteContainer(e.Id) + case StateExitProcess: + ctr.cleanProcess(st.ProcessID) + } + ctr.client.q.append(e.Id, func() { + if err := ctr.client.backend.StateChanged(e.Id, st); err != nil { + logrus.Error(err) + } + if e.Type == StatePause || e.Type == StateResume { + ctr.pauseMonitor.handle(e.Type) + } + if e.Type == StateExit { + if en := ctr.client.getExitNotifier(e.Id); en != nil { + en.close() + } + } + }) + + default: + logrus.Debugf("event unhandled: %+v", e) + } + return nil +} + +// discardFifos attempts to fully read the container fifos to unblock processes +// that may be blocked on the writer side. +func (ctr *container) discardFifos() { + for _, i := range []int{syscall.Stdout, syscall.Stderr} { + f := ctr.fifo(i) + c := make(chan struct{}) + go func() { + close(c) // this channel is used to not close the writer too early, before readonly open has been called. + io.Copy(ioutil.Discard, openReaderFromFifo(f)) + }() + <-c + closeReaderFifo(f) // avoid blocking permanently on open if there is no writer side + } +} diff --git a/libcontainerd/container_windows.go b/libcontainerd/container_windows.go new file mode 100644 index 00000000..b07141ab --- /dev/null +++ b/libcontainerd/container_windows.go @@ -0,0 +1,206 @@ +package libcontainerd + +import ( + "io" + "strings" + "syscall" + "time" + + "github.com/Microsoft/hcsshim" + "github.com/Sirupsen/logrus" +) + +type container struct { + containerCommon + + // Platform specific fields are below here. There are none presently on Windows. + options []CreateOption + + // The ociSpec is required, as client.Create() needs a spec, + // but can be called from the RestartManager context which does not + // otherwise have access to the Spec + ociSpec Spec +} + +func (ctr *container) newProcess(friendlyName string) *process { + return &process{ + processCommon: processCommon{ + containerID: ctr.containerID, + friendlyName: friendlyName, + client: ctr.client, + }, + } +} + +func (ctr *container) start() error { + var err error + + // Start the container + logrus.Debugln("Starting container ", ctr.containerID) + if err = hcsshim.StartComputeSystem(ctr.containerID); err != nil { + logrus.Errorf("Failed to start compute system: %s", err) + return err + } + + createProcessParms := hcsshim.CreateProcessParams{ + EmulateConsole: ctr.ociSpec.Process.Terminal, + WorkingDirectory: ctr.ociSpec.Process.Cwd, + ConsoleSize: ctr.ociSpec.Process.InitialConsoleSize, + } + + // Configure the environment for the process + createProcessParms.Environment = setupEnvironmentVariables(ctr.ociSpec.Process.Env) + createProcessParms.CommandLine = strings.Join(ctr.ociSpec.Process.Args, " ") + + iopipe := &IOPipe{Terminal: ctr.ociSpec.Process.Terminal} + + // Start the command running in the container. Note we always tell HCS to + // create stdout as it's required regardless of '-i' or '-t' options, so that + // docker can always grab the output through logs. We also tell HCS to always + // create stdin, even if it's not used - it will be closed shortly. Stderr + // is only created if it we're not -t. + var pid uint32 + var stdout, stderr io.ReadCloser + pid, iopipe.Stdin, stdout, stderr, err = hcsshim.CreateProcessInComputeSystem( + ctr.containerID, + true, + true, + !ctr.ociSpec.Process.Terminal, + createProcessParms) + if err != nil { + logrus.Errorf("CreateProcessInComputeSystem() failed %s", err) + + // Explicitly terminate the compute system here. + if err2 := hcsshim.TerminateComputeSystem(ctr.containerID, hcsshim.TimeoutInfinite, "CreateProcessInComputeSystem failed"); err2 != nil { + // Ignore this error, there's not a lot we can do except log it + logrus.Warnf("Failed to TerminateComputeSystem after a failed CreateProcessInComputeSystem. Ignoring this.", err2) + } else { + logrus.Debugln("Cleaned up after failed CreateProcessInComputeSystem by calling TerminateComputeSystem") + } + return err + } + ctr.startedAt = time.Now() + + // Convert io.ReadClosers to io.Readers + if stdout != nil { + iopipe.Stdout = openReaderFromPipe(stdout) + } + if stderr != nil { + iopipe.Stderr = openReaderFromPipe(stderr) + } + + // Save the PID + logrus.Debugf("Process started - PID %d", pid) + ctr.systemPid = uint32(pid) + + // Spin up a go routine waiting for exit to handle cleanup + go ctr.waitExit(pid, InitFriendlyName, true) + + ctr.client.appendContainer(ctr) + + if err := ctr.client.backend.AttachStreams(ctr.containerID, *iopipe); err != nil { + // OK to return the error here, as waitExit will handle tear-down in HCS + return err + } + + // Tell the docker engine that the container has started. + si := StateInfo{ + State: StateStart, + Pid: ctr.systemPid, // Not sure this is needed? Double-check monitor.go in daemon BUGBUG @jhowardmsft + } + return ctr.client.backend.StateChanged(ctr.containerID, si) + +} + +// waitExit runs as a goroutine waiting for the process to exit. It's +// equivalent to (in the linux containerd world) where events come in for +// state change notifications from containerd. +func (ctr *container) waitExit(pid uint32, processFriendlyName string, isFirstProcessToStart bool) error { + logrus.Debugln("waitExit on pid", pid) + + // Block indefinitely for the process to exit. + exitCode, err := hcsshim.WaitForProcessInComputeSystem(ctr.containerID, pid, hcsshim.TimeoutInfinite) + if err != nil { + if herr, ok := err.(*hcsshim.HcsError); ok && herr.Err != syscall.ERROR_BROKEN_PIPE { + logrus.Warnf("WaitForProcessInComputeSystem failed (container may have been killed): %s", err) + } + // Fall through here, do not return. This ensures we attempt to continue the + // shutdown in HCS nad tell the docker engine that the process/container + // has exited to avoid a container being dropped on the floor. + } + + // Assume the container has exited + si := StateInfo{ + State: StateExit, + ExitCode: uint32(exitCode), + Pid: pid, + ProcessID: processFriendlyName, + } + + // But it could have been an exec'd process which exited + if !isFirstProcessToStart { + si.State = StateExitProcess + } + + // If this is the init process, always call into vmcompute.dll to + // shutdown the container after we have completed. + if isFirstProcessToStart { + logrus.Debugf("Shutting down container %s", ctr.containerID) + // Explicit timeout here rather than hcsshim.TimeoutInfinte to avoid a + // (remote) possibility that ShutdownComputeSystem hangs indefinitely. + const shutdownTimeout = 5 * 60 * 1000 // 5 minutes + if err := hcsshim.ShutdownComputeSystem(ctr.containerID, shutdownTimeout, "waitExit"); err != nil { + if herr, ok := err.(*hcsshim.HcsError); !ok || + (herr.Err != hcsshim.ERROR_SHUTDOWN_IN_PROGRESS && + herr.Err != ErrorBadPathname && + herr.Err != syscall.ERROR_PATH_NOT_FOUND) { + logrus.Warnf("Ignoring error from ShutdownComputeSystem %s", err) + } + } else { + logrus.Debugf("Completed shutting down container %s", ctr.containerID) + } + + // BUGBUG - Is taking the lock necessary here? Should it just be taken for + // the deleteContainer call, not for the restart logic? @jhowardmsft + ctr.client.lock(ctr.containerID) + defer ctr.client.unlock(ctr.containerID) + + if si.State == StateExit && ctr.restartManager != nil { + restart, wait, err := ctr.restartManager.ShouldRestart(uint32(exitCode), false, time.Since(ctr.startedAt)) + if err != nil { + logrus.Error(err) + } else if restart { + si.State = StateRestart + ctr.restarting = true + go func() { + err := <-wait + ctr.restarting = false + if err != nil { + si.State = StateExit + if err := ctr.client.backend.StateChanged(ctr.containerID, si); err != nil { + logrus.Error(err) + } + logrus.Error(err) + } else { + ctr.client.Create(ctr.containerID, ctr.ociSpec, ctr.options...) + } + }() + } + } + + // Remove process from list if we have exited + // We need to do so here in case the Message Handler decides to restart it. + if si.State == StateExit { + ctr.client.deleteContainer(ctr.friendlyName) + } + } + + // Call into the backend to notify it of the state change. + logrus.Debugf("waitExit() calling backend.StateChanged %v", si) + if err := ctr.client.backend.StateChanged(ctr.containerID, si); err != nil { + logrus.Error(err) + } + + logrus.Debugln("waitExit() completed OK") + return nil +} diff --git a/libcontainerd/pausemonitor_linux.go b/libcontainerd/pausemonitor_linux.go new file mode 100644 index 00000000..379cbf1f --- /dev/null +++ b/libcontainerd/pausemonitor_linux.go @@ -0,0 +1,31 @@ +package libcontainerd + +// pauseMonitor is helper to get notifications from pause state changes. +type pauseMonitor struct { + waiters map[string][]chan struct{} +} + +func (m *pauseMonitor) handle(t string) { + if m.waiters == nil { + return + } + q, ok := m.waiters[t] + if !ok { + return + } + if len(q) > 0 { + close(q[0]) + m.waiters[t] = q[1:] + } +} + +func (m *pauseMonitor) append(t string, waiter chan struct{}) { + if m.waiters == nil { + m.waiters = make(map[string][]chan struct{}) + } + _, ok := m.waiters[t] + if !ok { + m.waiters[t] = make([]chan struct{}, 0) + } + m.waiters[t] = append(m.waiters[t], waiter) +} diff --git a/libcontainerd/process.go b/libcontainerd/process.go new file mode 100644 index 00000000..57562c87 --- /dev/null +++ b/libcontainerd/process.go @@ -0,0 +1,18 @@ +package libcontainerd + +// processCommon are the platform common fields as part of the process structure +// which keeps the state for the main container process, as well as any exec +// processes. +type processCommon struct { + client *client + + // containerID is the Container ID + containerID string + + // friendlyName is an identifier for the process (or `InitFriendlyName` + // for the first process) + friendlyName string + + // systemPid is the PID of the main container process + systemPid uint32 +} diff --git a/libcontainerd/process_linux.go b/libcontainerd/process_linux.go new file mode 100644 index 00000000..3c48576f --- /dev/null +++ b/libcontainerd/process_linux.go @@ -0,0 +1,110 @@ +package libcontainerd + +import ( + "fmt" + "io" + "os" + "path/filepath" + "syscall" + + containerd "github.com/docker/containerd/api/grpc/types" + "github.com/docker/docker/pkg/ioutils" + "golang.org/x/net/context" +) + +var fdNames = map[int]string{ + syscall.Stdin: "stdin", + syscall.Stdout: "stdout", + syscall.Stderr: "stderr", +} + +// process keeps the state for both main container process and exec process. +type process struct { + processCommon + + // Platform specific fields are below here. + dir string +} + +func (p *process) openFifos(terminal bool) (*IOPipe, error) { + bundleDir := p.dir + if err := os.MkdirAll(bundleDir, 0700); err != nil { + return nil, err + } + + for i := 0; i < 3; i++ { + f := p.fifo(i) + if err := syscall.Mkfifo(f, 0700); err != nil && !os.IsExist(err) { + return nil, fmt.Errorf("mkfifo: %s %v", f, err) + } + } + + io := &IOPipe{} + stdinf, err := os.OpenFile(p.fifo(syscall.Stdin), syscall.O_RDWR, 0) + if err != nil { + return nil, err + } + + io.Stdout = openReaderFromFifo(p.fifo(syscall.Stdout)) + if !terminal { + io.Stderr = openReaderFromFifo(p.fifo(syscall.Stderr)) + } else { + io.Stderr = emptyReader{} + } + + io.Stdin = ioutils.NewWriteCloserWrapper(stdinf, func() error { + stdinf.Close() + _, err := p.client.remote.apiClient.UpdateProcess(context.Background(), &containerd.UpdateProcessRequest{ + Id: p.containerID, + Pid: p.friendlyName, + CloseStdin: true, + }) + return err + }) + + return io, nil +} + +func (p *process) closeFifos(io *IOPipe) { + io.Stdin.Close() + closeReaderFifo(p.fifo(syscall.Stdout)) + closeReaderFifo(p.fifo(syscall.Stderr)) +} + +type emptyReader struct{} + +func (r emptyReader) Read(b []byte) (int, error) { + return 0, io.EOF +} + +func openReaderFromFifo(fn string) io.Reader { + r, w := io.Pipe() + c := make(chan struct{}) + go func() { + close(c) + stdoutf, err := os.OpenFile(fn, syscall.O_RDONLY, 0) + if err != nil { + r.CloseWithError(err) + } + if _, err := io.Copy(w, stdoutf); err != nil { + r.CloseWithError(err) + } + w.Close() + stdoutf.Close() + }() + <-c // wait for the goroutine to get scheduled and syscall to block + return r +} + +// closeReaderFifo closes fifo that may be blocked on open by opening the write side. +func closeReaderFifo(fn string) { + f, err := os.OpenFile(fn, syscall.O_WRONLY|syscall.O_NONBLOCK, 0) + if err != nil { + return + } + f.Close() +} + +func (p *process) fifo(index int) string { + return filepath.Join(p.dir, p.friendlyName+"-"+fdNames[index]) +} diff --git a/libcontainerd/process_windows.go b/libcontainerd/process_windows.go new file mode 100644 index 00000000..0371aec9 --- /dev/null +++ b/libcontainerd/process_windows.go @@ -0,0 +1,27 @@ +package libcontainerd + +import ( + "io" +) + +// process keeps the state for both main container process and exec process. +type process struct { + processCommon + + // Platform specific fields are below here. + + // commandLine is to support returning summary information for docker top + commandLine string +} + +func openReaderFromPipe(p io.ReadCloser) io.Reader { + r, w := io.Pipe() + go func() { + if _, err := io.Copy(w, p); err != nil { + r.CloseWithError(err) + } + w.Close() + p.Close() + }() + return r +} diff --git a/libcontainerd/queue_linux.go b/libcontainerd/queue_linux.go new file mode 100644 index 00000000..34bc81d2 --- /dev/null +++ b/libcontainerd/queue_linux.go @@ -0,0 +1,29 @@ +package libcontainerd + +import "sync" + +type queue struct { + sync.Mutex + fns map[string]chan struct{} +} + +func (q *queue) append(id string, f func()) { + q.Lock() + defer q.Unlock() + + if q.fns == nil { + q.fns = make(map[string]chan struct{}) + } + + done := make(chan struct{}) + + fn, ok := q.fns[id] + q.fns[id] = done + go func() { + if ok { + <-fn + } + f() + close(done) + }() +} diff --git a/libcontainerd/remote.go b/libcontainerd/remote.go new file mode 100644 index 00000000..a679edcf --- /dev/null +++ b/libcontainerd/remote.go @@ -0,0 +1,18 @@ +package libcontainerd + +// Remote on Linux defines the accesspoint to the containerd grpc API. +// Remote on Windows is largely an unimplemented interface as there is +// no remote containerd. +type Remote interface { + // Client returns a new Client instance connected with given Backend. + Client(Backend) (Client, error) + // Cleanup stops containerd if it was started by libcontainerd. + // Note this is not used on Windows as there is no remote containerd. + Cleanup() +} + +// RemoteOption allows to configure paramters of remotes. +// This is unused on Windows. +type RemoteOption interface { + Apply(Remote) error +} diff --git a/libcontainerd/remote_linux.go b/libcontainerd/remote_linux.go new file mode 100644 index 00000000..12ce0e10 --- /dev/null +++ b/libcontainerd/remote_linux.go @@ -0,0 +1,449 @@ +package libcontainerd + +import ( + "fmt" + "io" + "io/ioutil" + "log" + "net" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/Sirupsen/logrus" + containerd "github.com/docker/containerd/api/grpc/types" + "github.com/docker/docker/pkg/locker" + sysinfo "github.com/docker/docker/pkg/system" + "github.com/docker/docker/utils" + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/transport" +) + +const ( + maxConnectionRetryCount = 3 + connectionRetryDelay = 3 * time.Second + containerdShutdownTimeout = 15 * time.Second + containerdBinary = "docker-containerd" + containerdPidFilename = "docker-containerd.pid" + containerdSockFilename = "docker-containerd.sock" + eventTimestampFilename = "event.ts" +) + +type remote struct { + sync.RWMutex + apiClient containerd.APIClient + daemonPid int + stateDir string + rpcAddr string + startDaemon bool + closeManually bool + debugLog bool + rpcConn *grpc.ClientConn + clients []*client + eventTsPath string + pastEvents map[string]*containerd.Event + runtimeArgs []string +} + +// New creates a fresh instance of libcontainerd remote. +func New(stateDir string, options ...RemoteOption) (_ Remote, err error) { + defer func() { + if err != nil { + err = fmt.Errorf("Failed to connect to containerd. Please make sure containerd is installed in your PATH or you have specificed the correct address. Got error: %v", err) + } + }() + r := &remote{ + stateDir: stateDir, + daemonPid: -1, + eventTsPath: filepath.Join(stateDir, eventTimestampFilename), + pastEvents: make(map[string]*containerd.Event), + } + for _, option := range options { + if err := option.Apply(r); err != nil { + return nil, err + } + } + + if err := sysinfo.MkdirAll(stateDir, 0700); err != nil { + return nil, err + } + + if r.rpcAddr == "" { + r.rpcAddr = filepath.Join(stateDir, containerdSockFilename) + } + + if r.startDaemon { + if err := r.runContainerdDaemon(); err != nil { + return nil, err + } + } + + // don't output the grpc reconnect logging + grpclog.SetLogger(log.New(ioutil.Discard, "", log.LstdFlags)) + dialOpts := append([]grpc.DialOption{grpc.WithInsecure()}, + grpc.WithDialer(func(addr string, timeout time.Duration) (net.Conn, error) { + return net.DialTimeout("unix", addr, timeout) + }), + ) + conn, err := grpc.Dial(r.rpcAddr, dialOpts...) + if err != nil { + return nil, fmt.Errorf("error connecting to containerd: %v", err) + } + + r.rpcConn = conn + r.apiClient = containerd.NewAPIClient(conn) + + go r.handleConnectionChange() + + if err := r.startEventsMonitor(); err != nil { + return nil, err + } + + return r, nil +} + +func (r *remote) handleConnectionChange() { + var transientFailureCount = 0 + state := grpc.Idle + for { + s, err := r.rpcConn.WaitForStateChange(context.Background(), state) + if err != nil { + break + } + state = s + logrus.Debugf("containerd connection state change: %v", s) + + if r.daemonPid != -1 { + switch state { + case grpc.TransientFailure: + // Reset state to be notified of next failure + transientFailureCount++ + if transientFailureCount >= maxConnectionRetryCount { + transientFailureCount = 0 + if utils.IsProcessAlive(r.daemonPid) { + utils.KillProcess(r.daemonPid) + } + if err := r.runContainerdDaemon(); err != nil { //FIXME: Handle error + logrus.Errorf("error restarting containerd: %v", err) + } + } else { + state = grpc.Idle + time.Sleep(connectionRetryDelay) + } + case grpc.Shutdown: + // Well, we asked for it to stop, just return + return + } + } + } +} + +func (r *remote) Cleanup() { + if r.daemonPid == -1 { + return + } + r.closeManually = true + r.rpcConn.Close() + // Ask the daemon to quit + syscall.Kill(r.daemonPid, syscall.SIGTERM) + + // Wait up to 15secs for it to stop + for i := time.Duration(0); i < containerdShutdownTimeout; i += time.Second { + if !utils.IsProcessAlive(r.daemonPid) { + break + } + time.Sleep(time.Second) + } + + if utils.IsProcessAlive(r.daemonPid) { + logrus.Warnf("libcontainerd: containerd (%d) didn't stop within 15 secs, killing it\n", r.daemonPid) + syscall.Kill(r.daemonPid, syscall.SIGKILL) + } + + // cleanup some files + os.Remove(filepath.Join(r.stateDir, containerdPidFilename)) + os.Remove(filepath.Join(r.stateDir, containerdSockFilename)) +} + +func (r *remote) Client(b Backend) (Client, error) { + c := &client{ + clientCommon: clientCommon{ + backend: b, + containers: make(map[string]*container), + locker: locker.New(), + }, + remote: r, + exitNotifiers: make(map[string]*exitNotifier), + } + + r.Lock() + r.clients = append(r.clients, c) + r.Unlock() + return c, nil +} + +func (r *remote) updateEventTimestamp(t time.Time) { + f, err := os.OpenFile(r.eventTsPath, syscall.O_CREAT|syscall.O_WRONLY|syscall.O_TRUNC, 0600) + defer f.Close() + if err != nil { + logrus.Warnf("libcontainerd: failed to open event timestamp file: %v", err) + return + } + + b, err := t.MarshalText() + if err != nil { + logrus.Warnf("libcontainerd: failed to encode timestamp: %v", err) + return + } + + n, err := f.Write(b) + if err != nil || n != len(b) { + logrus.Warnf("libcontainerd: failed to update event timestamp file: %v", err) + f.Truncate(0) + return + } + +} + +func (r *remote) getLastEventTimestamp() int64 { + t := time.Now() + + fi, err := os.Stat(r.eventTsPath) + if os.IsNotExist(err) || fi.Size() == 0 { + return t.Unix() + } + + f, err := os.Open(r.eventTsPath) + defer f.Close() + if err != nil { + logrus.Warn("libcontainerd: Unable to access last event ts: %v", err) + return t.Unix() + } + + b := make([]byte, fi.Size()) + n, err := f.Read(b) + if err != nil || n != len(b) { + logrus.Warn("libcontainerd: Unable to read last event ts: %v", err) + return t.Unix() + } + + t.UnmarshalText(b) + + return t.Unix() +} + +func (r *remote) startEventsMonitor() error { + // First, get past events + er := &containerd.EventsRequest{ + Timestamp: uint64(r.getLastEventTimestamp()), + } + events, err := r.apiClient.Events(context.Background(), er) + if err != nil { + return err + } + go r.handleEventStream(events) + return nil +} + +func (r *remote) handleEventStream(events containerd.API_EventsClient) { + live := false + for { + e, err := events.Recv() + if err != nil { + if grpc.ErrorDesc(err) == transport.ErrConnClosing.Desc && + r.closeManually { + // ignore error if grpc remote connection is closed manually + return + } + logrus.Errorf("failed to receive event from containerd: %v", err) + go r.startEventsMonitor() + return + } + + if live == false { + logrus.Debugf("received past containerd event: %#v", e) + + // Pause/Resume events should never happens after exit one + switch e.Type { + case StateExit: + r.pastEvents[e.Id] = e + case StatePause: + r.pastEvents[e.Id] = e + case StateResume: + r.pastEvents[e.Id] = e + case stateLive: + live = true + r.updateEventTimestamp(time.Unix(int64(e.Timestamp), 0)) + } + } else { + logrus.Debugf("received containerd event: %#v", e) + + var container *container + var c *client + r.RLock() + for _, c = range r.clients { + container, err = c.getContainer(e.Id) + if err == nil { + break + } + } + r.RUnlock() + if container == nil { + logrus.Errorf("no state for container: %q", err) + continue + } + + if err := container.handleEvent(e); err != nil { + logrus.Errorf("error processing state change for %s: %v", e.Id, err) + } + + r.updateEventTimestamp(time.Unix(int64(e.Timestamp), 0)) + } + } +} + +func (r *remote) runContainerdDaemon() error { + pidFilename := filepath.Join(r.stateDir, containerdPidFilename) + f, err := os.OpenFile(pidFilename, os.O_RDWR|os.O_CREATE, 0600) + defer f.Close() + if err != nil { + return err + } + + // File exist, check if the daemon is alive + b := make([]byte, 8) + n, err := f.Read(b) + if err != nil && err != io.EOF { + return err + } + + if n > 0 { + pid, err := strconv.ParseUint(string(b[:n]), 10, 64) + if err != nil { + return err + } + if utils.IsProcessAlive(int(pid)) { + logrus.Infof("previous instance of containerd still alive (%d)", pid) + r.daemonPid = int(pid) + return nil + } + } + + // rewind the file + _, err = f.Seek(0, os.SEEK_SET) + if err != nil { + return err + } + + // Truncate it + err = f.Truncate(0) + if err != nil { + return err + } + + // Start a new instance + args := []string{"-l", r.rpcAddr, "--runtime", "docker-runc", "--start-timeout", "2m"} + if r.debugLog { + args = append(args, "--debug", "--metrics-interval=0") + } + if len(r.runtimeArgs) > 0 { + for _, v := range r.runtimeArgs { + args = append(args, "--runtime-args") + args = append(args, v) + } + logrus.Debugf("runContainerdDaemon: runtimeArgs: %s", args) + } + + cmd := exec.Command(containerdBinary, args...) + // redirect containerd logs to docker logs + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} + cmd.Env = nil + // clear the NOTIFY_SOCKET from the env when starting containerd + for _, e := range os.Environ() { + if !strings.HasPrefix(e, "NOTIFY_SOCKET") { + cmd.Env = append(cmd.Env, e) + } + } + if err := cmd.Start(); err != nil { + return err + } + logrus.Infof("New containerd process, pid: %d\n", cmd.Process.Pid) + + if _, err := f.WriteString(fmt.Sprintf("%d", cmd.Process.Pid)); err != nil { + utils.KillProcess(cmd.Process.Pid) + return err + } + + go cmd.Wait() // Reap our child when needed + r.daemonPid = cmd.Process.Pid + return nil +} + +// WithRemoteAddr sets the external containerd socket to connect to. +func WithRemoteAddr(addr string) RemoteOption { + return rpcAddr(addr) +} + +type rpcAddr string + +func (a rpcAddr) Apply(r Remote) error { + if remote, ok := r.(*remote); ok { + remote.rpcAddr = string(a) + return nil + } + return fmt.Errorf("WithRemoteAddr option not supported for this remote") +} + +// WithRuntimeArgs sets the list of runtime args passed to containerd +func WithRuntimeArgs(args []string) RemoteOption { + return runtimeArgs(args) +} + +type runtimeArgs []string + +func (rt runtimeArgs) Apply(r Remote) error { + if remote, ok := r.(*remote); ok { + remote.runtimeArgs = rt + return nil + } + return fmt.Errorf("WithRuntimeArgs option not supported for this remote") +} + +// WithStartDaemon defines if libcontainerd should also run containerd daemon. +func WithStartDaemon(start bool) RemoteOption { + return startDaemon(start) +} + +type startDaemon bool + +func (s startDaemon) Apply(r Remote) error { + if remote, ok := r.(*remote); ok { + remote.startDaemon = bool(s) + return nil + } + return fmt.Errorf("WithStartDaemon option not supported for this remote") +} + +// WithDebugLog defines if containerd debug logs will be enabled for daemon. +func WithDebugLog(debug bool) RemoteOption { + return debugLog(debug) +} + +type debugLog bool + +func (d debugLog) Apply(r Remote) error { + if remote, ok := r.(*remote); ok { + remote.debugLog = bool(d) + return nil + } + return fmt.Errorf("WithDebugLog option not supported for this remote") +} diff --git a/libcontainerd/remote_windows.go b/libcontainerd/remote_windows.go new file mode 100644 index 00000000..ce01f74f --- /dev/null +++ b/libcontainerd/remote_windows.go @@ -0,0 +1,27 @@ +package libcontainerd + +import "github.com/docker/docker/pkg/locker" + +type remote struct { +} + +func (r *remote) Client(b Backend) (Client, error) { + c := &client{ + clientCommon: clientCommon{ + backend: b, + containers: make(map[string]*container), + locker: locker.New(), + }, + } + return c, nil +} + +// Cleanup is a no-op on Windows. It is here to implement the interface. +func (r *remote) Cleanup() { +} + +// New creates a fresh instance of libcontainerd remote. On Windows, +// this is not used as there is no remote containerd process. +func New(_ string, _ ...RemoteOption) (Remote, error) { + return &remote{}, nil +} diff --git a/libcontainerd/types.go b/libcontainerd/types.go new file mode 100644 index 00000000..bb82fe40 --- /dev/null +++ b/libcontainerd/types.go @@ -0,0 +1,60 @@ +package libcontainerd + +import "io" + +// State constants used in state change reporting. +const ( + StateStart = "start-container" + StatePause = "pause" + StateResume = "resume" + StateExit = "exit" + StateRestart = "restart" + StateRestore = "restore" + StateStartProcess = "start-process" + StateExitProcess = "exit-process" + StateOOM = "oom" // fake state + stateLive = "live" +) + +// StateInfo contains description about the new state container has entered. +type StateInfo struct { // FIXME: event? + State string + Pid uint32 + ExitCode uint32 + ProcessID string + OOMKilled bool // TODO Windows containerd factor out +} + +// Backend defines callbacks that the client of the library needs to implement. +type Backend interface { + StateChanged(containerID string, state StateInfo) error + AttachStreams(processFriendlyName string, io IOPipe) error +} + +// Client provides access to containerd features. +type Client interface { + Create(containerID string, spec Spec, options ...CreateOption) error + Signal(containerID string, sig int) error + AddProcess(containerID, processFriendlyName string, process Process) error + Resize(containerID, processFriendlyName string, width, height int) error + Pause(containerID string) error + Resume(containerID string) error + Restore(containerID string, options ...CreateOption) error + Stats(containerID string) (*Stats, error) + GetPidsForContainer(containerID string) ([]int, error) + Summary(containerID string) ([]Summary, error) + UpdateResources(containerID string, resources Resources) error +} + +// CreateOption allows to configure parameters of container creation. +type CreateOption interface { + Apply(interface{}) error +} + +// IOPipe contains the stdio streams. +type IOPipe struct { + Stdin io.WriteCloser + Stdout io.Reader + Stderr io.Reader + Terminal bool // Whether stderr is connected on Windows +} diff --git a/libcontainerd/types_linux.go b/libcontainerd/types_linux.go new file mode 100644 index 00000000..bee12d91 --- /dev/null +++ b/libcontainerd/types_linux.go @@ -0,0 +1,47 @@ +package libcontainerd + +import ( + containerd "github.com/docker/containerd/api/grpc/types" + "github.com/opencontainers/specs/specs-go" +) + +// Spec is the base configuration for the container. It specifies platform +// independent configuration. This information must be included when the +// bundle is packaged for distribution. +type Spec specs.Spec + +// Process contains information to start a specific application inside the container. +type Process struct { + // Terminal creates an interactive terminal for the container. + Terminal bool `json:"terminal"` + // User specifies user information for the process. + User *User `json:"user"` + // Args specifies the binary and arguments for the application to execute. + Args []string `json:"args"` + // Env populates the process environment for the process. + Env []string `json:"env,omitempty"` + // Cwd is the current working directory for the process and must be + // relative to the container's root. + Cwd *string `json:"cwd"` + // Capabilities are linux capabilities that are kept for the container. + Capabilities []string `json:"capabilities,omitempty"` + // Rlimits specifies rlimit options to apply to the process. + Rlimits []specs.Rlimit `json:"rlimits,omitempty"` + // ApparmorProfile specified the apparmor profile for the container. + ApparmorProfile *string `json:"apparmorProfile,omitempty"` + // SelinuxProcessLabel specifies the selinux context that the container process is run as. + SelinuxLabel *string `json:"selinuxLabel,omitempty"` +} + +// Stats contains a stats properties from containerd. +type Stats containerd.StatsResponse + +// Summary container a container summary from containerd +type Summary struct{} + +// User specifies linux specific user and group information for the container's +// main process. +type User specs.User + +// Resources defines updatable container resource values. +type Resources containerd.UpdateResource diff --git a/libcontainerd/types_windows.go b/libcontainerd/types_windows.go new file mode 100644 index 00000000..2a915ff1 --- /dev/null +++ b/libcontainerd/types_windows.go @@ -0,0 +1,24 @@ +package libcontainerd + +import "github.com/docker/docker/libcontainerd/windowsoci" + +// Spec is the base configuration for the container. +type Spec windowsoci.WindowsSpec + +// Process contains information to start a specific application inside the container. +type Process windowsoci.Process + +// User specifies user information for the containers main process. +type User windowsoci.User + +// Summary container a container summary from containerd +type Summary struct { + Pid uint32 + Command string +} + +// Stats contains a stats properties from containerd. +type Stats struct{} + +// Resources defines updatable container resource values. +type Resources struct{} diff --git a/libcontainerd/utils_linux.go b/libcontainerd/utils_linux.go new file mode 100644 index 00000000..5b67244f --- /dev/null +++ b/libcontainerd/utils_linux.go @@ -0,0 +1,52 @@ +package libcontainerd + +import ( + containerd "github.com/docker/containerd/api/grpc/types" + "github.com/opencontainers/specs/specs-go" +) + +func getRootIDs(s specs.Spec) (int, int, error) { + var hasUserns bool + for _, ns := range s.Linux.Namespaces { + if ns.Type == specs.UserNamespace { + hasUserns = true + break + } + } + if !hasUserns { + return 0, 0, nil + } + uid := hostIDFromMap(0, s.Linux.UIDMappings) + gid := hostIDFromMap(0, s.Linux.GIDMappings) + return uid, gid, nil +} + +func hostIDFromMap(id uint32, mp []specs.IDMapping) int { + for _, m := range mp { + if id >= m.ContainerID && id <= m.ContainerID+m.Size-1 { + return int(m.HostID + id - m.ContainerID) + } + } + return 0 +} + +func systemPid(ctr *containerd.Container) uint32 { + var pid uint32 + for _, p := range ctr.Processes { + if p.Pid == InitFriendlyName { + pid = p.SystemPid + } + } + return pid +} + +func convertRlimits(sr []specs.Rlimit) (cr []*containerd.Rlimit) { + for _, r := range sr { + cr = append(cr, &containerd.Rlimit{ + Type: r.Type, + Hard: r.Hard, + Soft: r.Soft, + }) + } + return +} diff --git a/libcontainerd/utils_windows.go b/libcontainerd/utils_windows.go new file mode 100644 index 00000000..a9d95d63 --- /dev/null +++ b/libcontainerd/utils_windows.go @@ -0,0 +1,16 @@ +package libcontainerd + +import "strings" + +// setupEnvironmentVariables convert a string array of environment variables +// into a map as required by the HCS. Source array is in format [v1=k1] [v2=k2] etc. +func setupEnvironmentVariables(a []string) map[string]string { + r := make(map[string]string) + for _, s := range a { + arr := strings.Split(s, "=") + if len(arr) == 2 { + r[arr[0]] = arr[1] + } + } + return r +} diff --git a/libcontainerd/windowsoci/oci_windows.go b/libcontainerd/windowsoci/oci_windows.go new file mode 100644 index 00000000..3b9f5a3a --- /dev/null +++ b/libcontainerd/windowsoci/oci_windows.go @@ -0,0 +1,190 @@ +package windowsoci + +// This file contains the Windows spec for a container. At the time of +// writing, Windows does not have a spec defined in opencontainers/specs, +// hence this is an interim workaround. TODO Windows: FIXME @jhowardmsft + +import ( + "fmt" + + "github.com/docker/go-connections/nat" +) + +// WindowsSpec is the full specification for Windows containers. +type WindowsSpec struct { + Spec + + // Windows is platform specific configuration for Windows based containers. + Windows Windows `json:"windows"` +} + +// Spec is the base configuration for the container. It specifies platform +// independent configuration. This information must be included when the +// bundle is packaged for distribution. +type Spec struct { + + // Version is the version of the specification that is supported. + Version string `json:"ociVersion"` + // Platform is the host information for OS and Arch. + Platform Platform `json:"platform"` + // Process is the container's main process. + Process Process `json:"process"` + // Root is the root information for the container's filesystem. + Root Root `json:"root"` + // Hostname is the container's host name. + Hostname string `json:"hostname,omitempty"` + // Mounts profile configuration for adding mounts to the container's filesystem. + Mounts []Mount `json:"mounts"` +} + +// Windows contains platform specific configuration for Windows based containers. +type Windows struct { + // Resources contain information for handling resource constraints for the container + Resources *Resources `json:"resources,omitempty"` + // Networking contains the platform specific network settings for the container. + Networking *Networking `json:"networking,omitempty"` + // FirstStart is used for an optimization on first boot of Windows + FirstStart bool `json:"first_start,omitempty"` + // LayerFolder is the path to the current layer folder + LayerFolder string `json:"layer_folder,omitempty"` + // Layer paths of the parent layers + LayerPaths []string `json:"layer_paths,omitempty"` + // HvRuntime contains settings specific to Hyper-V containers, omitted if not using Hyper-V isolation + HvRuntime *HvRuntime `json:"hv_runtime,omitempty"` +} + +// Process contains information to start a specific application inside the container. +type Process struct { + // Terminal indicates if stderr should NOT be attached for the container. + Terminal bool `json:"terminal"` + // ConsoleSize contains the initial h,w of the console size + InitialConsoleSize [2]int `json:"-"` + // User specifies user information for the process. + User User `json:"user"` + // Args specifies the binary and arguments for the application to execute. + Args []string `json:"args"` + // Env populates the process environment for the process. + Env []string `json:"env,omitempty"` + // Cwd is the current working directory for the process and must be + // relative to the container's root. + Cwd string `json:"cwd"` +} + +// User contains the user information for Windows +type User struct { + User string `json:"user,omitempty"` +} + +// Root contains information about the container's root filesystem on the host. +type Root struct { + // Path is the absolute path to the container's root filesystem. + Path string `json:"path"` + // Readonly makes the root filesystem for the container readonly before the process is executed. + Readonly bool `json:"readonly"` +} + +// Platform specifies OS and arch information for the host system that the container +// is created for. +type Platform struct { + // OS is the operating system. + OS string `json:"os"` + // Arch is the architecture + Arch string `json:"arch"` +} + +// Mount specifies a mount for a container. +type Mount struct { + // Destination is the path where the mount will be placed relative to the container's root. The path and child directories MUST exist, a runtime MUST NOT create directories automatically to a mount point. + Destination string `json:"destination"` + // Type specifies the mount kind. + Type string `json:"type"` + // Source specifies the source path of the mount. In the case of bind mounts + // this would be the file on the host. + Source string `json:"source"` + // Readonly specifies if the mount should be read-only + Readonly bool `json:"readonly"` +} + +// HvRuntime contains settings specific to Hyper-V containers +type HvRuntime struct { + // ImagePath is the path to the Utility VM image for this container + ImagePath string `json:"image_path,omitempty"` +} + +// Networking contains the platform specific network settings for the container +type Networking struct { + // TODO Windows TP5. The following three fields are for 'legacy' non- + // libnetwork networking through HCS. They can be removed once TP4 is + // no longer supported. Also remove in libcontainerd\client_windows.go, + // function Create(), and in daemon\oci_windows.go, function CreateSpec() + MacAddress string `json:"mac,omitempty"` + Bridge string `json:"bridge,omitempty"` + PortBindings nat.PortMap `json:"port_bindings,omitempty"` + // End of TODO Windows TP5. + + // List of endpoints to be attached to the container + EndpointList []string `json:"endpoints,omitempty"` +} + +// Storage contains storage resource management settings +type Storage struct { + // Specifies maximum Iops for the system drive + Iops *uint64 `json:"iops,omitempty"` + // Specifies maximum bytes per second for the system drive + Bps *uint64 `json:"bps,omitempty"` + // Sandbox size indicates the size to expand the system drive to if it is currently smaller + SandboxSize *uint64 `json:"sandbox_size,omitempty"` +} + +// Memory contains memory settings for the container +type Memory struct { + // Memory limit (in bytes). + Limit *int64 `json:"limit,omitempty"` + // Memory reservation (in bytes). + Reservation *uint64 `json:"reservation,omitempty"` +} + +// CPU contains information for cpu resource management +type CPU struct { + // Number of CPUs available to the container. This is an appoximation for Windows Server Containers. + Count *uint64 `json:"count,omitempty"` + // CPU shares (relative weight (ratio) vs. other containers with cpu shares). Range is from 1 to 10000. + Shares *uint64 `json:"shares,omitempty"` + // Percent of available CPUs usable by the container. + Percent *int64 `json:"percent,omitempty"` +} + +// Network network resource management information +type Network struct { + // Bandwidth is the maximum egress bandwidth in bytes per second + Bandwidth *uint64 `json:"bandwidth,omitempty"` +} + +// Resources has container runtime resource constraints +// TODO Windows containerd. This structure needs ratifying with the old resources +// structure used on Windows and the latest OCI spec. +type Resources struct { + // Memory restriction configuration + Memory *Memory `json:"memory,omitempty"` + // CPU resource restriction configuration + CPU *CPU `json:"cpu,omitempty"` + // Storage restriction configuration + Storage *Storage `json:"storage,omitempty"` + // Network restriction configuration + Network *Network `json:"network,omitempty"` +} + +const ( + // VersionMajor is for an API incompatible changes + VersionMajor = 0 + // VersionMinor is for functionality in a backwards-compatible manner + VersionMinor = 3 + // VersionPatch is for backwards-compatible bug fixes + VersionPatch = 0 + + // VersionDev indicates development branch. Releases will be empty string. + VersionDev = "" +) + +// Version is the specification version that the package types support. +var Version = fmt.Sprintf("%d.%d.%d%s (Windows)", VersionMajor, VersionMinor, VersionPatch, VersionDev) diff --git a/libcontainerd/windowsoci/unsupported.go b/libcontainerd/windowsoci/unsupported.go new file mode 100644 index 00000000..a97c2829 --- /dev/null +++ b/libcontainerd/windowsoci/unsupported.go @@ -0,0 +1,3 @@ +// +build !windows + +package windowsoci diff --git a/man/Dockerfile b/man/Dockerfile new file mode 100644 index 00000000..af231952 --- /dev/null +++ b/man/Dockerfile @@ -0,0 +1,7 @@ +FROM golang:1.4 +RUN mkdir -p /go/src/github.com/cpuguy83 +RUN mkdir -p /go/src/github.com/cpuguy83 \ + && git clone -b v1.0.3 https://github.com/cpuguy83/go-md2man.git /go/src/github.com/cpuguy83/go-md2man \ + && cd /go/src/github.com/cpuguy83/go-md2man \ + && go get -v ./... +CMD ["/go/bin/go-md2man", "--help"] diff --git a/man/Dockerfile.5.md b/man/Dockerfile.5.md new file mode 100644 index 00000000..7d56bda0 --- /dev/null +++ b/man/Dockerfile.5.md @@ -0,0 +1,472 @@ +% DOCKERFILE(5) Docker User Manuals +% Zac Dover +% May 2014 +# NAME + +Dockerfile - automate the steps of creating a Docker image + +# INTRODUCTION + +The **Dockerfile** is a configuration file that automates the steps of creating +a Docker image. It is similar to a Makefile. Docker reads instructions from the +**Dockerfile** to automate the steps otherwise performed manually to create an +image. To build an image, create a file called **Dockerfile**. + +The **Dockerfile** describes the steps taken to assemble the image. When the +**Dockerfile** has been created, call the `docker build` command, using the +path of directory that contains **Dockerfile** as the argument. + +# SYNOPSIS + +INSTRUCTION arguments + +For example: + + FROM image + +# DESCRIPTION + +A Dockerfile is a file that automates the steps of creating a Docker image. +A Dockerfile is similar to a Makefile. + +# USAGE + + docker build . + + -- Runs the steps and commits them, building a final image. + The path to the source repository defines where to find the context of the + build. The build is run by the Docker daemon, not the CLI. The whole + context must be transferred to the daemon. The Docker CLI reports + `"Sending build context to Docker daemon"` when the context is sent to the + daemon. + + ``` + docker build -t repository/tag . + ``` + + -- specifies a repository and tag at which to save the new image if the build + succeeds. The Docker daemon runs the steps one-by-one, committing the result + to a new image if necessary, before finally outputting the ID of the new + image. The Docker daemon automatically cleans up the context it is given. + + Docker re-uses intermediate images whenever possible. This significantly + accelerates the *docker build* process. + +# FORMAT + + `FROM image` + + `FROM image:tag` + + `FROM image@digest` + + -- The **FROM** instruction sets the base image for subsequent instructions. A + valid Dockerfile must have **FROM** as its first instruction. The image can be any + valid image. It is easy to start by pulling an image from the public + repositories. + + -- **FROM** must be the first non-comment instruction in Dockerfile. + + -- **FROM** may appear multiple times within a single Dockerfile in order to create + multiple images. Make a note of the last image ID output by the commit before + each new **FROM** command. + + -- If no tag is given to the **FROM** instruction, Docker applies the + `latest` tag. If the used tag does not exist, an error is returned. + + -- If no digest is given to the **FROM** instruction, Docker applies the + `latest` tag. If the used tag does not exist, an error is returned. + +**MAINTAINER** + -- **MAINTAINER** sets the Author field for the generated images. + Useful for providing users with an email or url for support. + +**RUN** + -- **RUN** has two forms: + + ``` + # the command is run in a shell - /bin/sh -c + RUN + + # Executable form + RUN ["executable", "param1", "param2"] + ``` + + + -- The **RUN** instruction executes any commands in a new layer on top of the current + image and commits the results. The committed image is used for the next step in + Dockerfile. + + -- Layering **RUN** instructions and generating commits conforms to the core + concepts of Docker where commits are cheap and containers can be created from + any point in the history of an image. This is similar to source control. The + exec form makes it possible to avoid shell string munging. The exec form makes + it possible to **RUN** commands using a base image that does not contain `/bin/sh`. + + Note that the exec form is parsed as a JSON array, which means that you must + use double-quotes (") around words not single-quotes ('). + +**CMD** + -- **CMD** has three forms: + + ``` + # Executable form + CMD ["executable", "param1", "param2"]` + + # Provide default arguments to ENTRYPOINT + CMD ["param1", "param2"]` + + # the command is run in a shell - /bin/sh -c + CMD command param1 param2 + ``` + + -- There should be only one **CMD** in a Dockerfile. If more than one **CMD** is listed, only + the last **CMD** takes effect. + The main purpose of a **CMD** is to provide defaults for an executing container. + These defaults may include an executable, or they can omit the executable. If + they omit the executable, an **ENTRYPOINT** must be specified. + When used in the shell or exec formats, the **CMD** instruction sets the command to + be executed when running the image. + If you use the shell form of the **CMD**, the `` executes in `/bin/sh -c`: + + Note that the exec form is parsed as a JSON array, which means that you must + use double-quotes (") around words not single-quotes ('). + + ``` + FROM ubuntu + CMD echo "This is a test." | wc - + ``` + + -- If you run **command** without a shell, then you must express the command as a + JSON array and give the full path to the executable. This array form is the + preferred form of **CMD**. All additional parameters must be individually expressed + as strings in the array: + + ``` + FROM ubuntu + CMD ["/usr/bin/wc","--help"] + ``` + + -- To make the container run the same executable every time, use **ENTRYPOINT** in + combination with **CMD**. + If the user specifies arguments to `docker run`, the specified commands + override the default in **CMD**. + Do not confuse **RUN** with **CMD**. **RUN** runs a command and commits the result. + **CMD** executes nothing at build time, but specifies the intended command for + the image. + +**LABEL** + -- `LABEL = [= ...]`or + ``` + LABEL [ ] + LABEL [ ] + ... + ``` + The **LABEL** instruction adds metadata to an image. A **LABEL** is a + key-value pair. To specify a **LABEL** without a value, simply use an empty + string. To include spaces within a **LABEL** value, use quotes and + backslashes as you would in command-line parsing. + + ``` + LABEL com.example.vendor="ACME Incorporated" + LABEL com.example.vendor "ACME Incorporated" + LABEL com.example.vendor.is-beta "" + LABEL com.example.vendor.is-beta= + LABEL com.example.vendor.is-beta="" + ``` + + An image can have more than one label. To specify multiple labels, separate + each key-value pair by a space. + + Labels are additive including `LABEL`s in `FROM` images. As the system + encounters and then applies a new label, new `key`s override any previous + labels with identical keys. + + To display an image's labels, use the `docker inspect` command. + +**EXPOSE** + -- `EXPOSE [...]` + The **EXPOSE** instruction informs Docker that the container listens on the + specified network ports at runtime. Docker uses this information to + interconnect containers using links and to set up port redirection on the host + system. + +**ENV** + -- `ENV ` + The **ENV** instruction sets the environment variable to + the value ``. This value is passed to all future + **RUN**, **ENTRYPOINT**, and **CMD** instructions. This is + functionally equivalent to prefixing the command with `=`. The + environment variables that are set with **ENV** persist when a container is run + from the resulting image. Use `docker inspect` to inspect these values, and + change them using `docker run --env =`. + + Note that setting "`ENV DEBIAN_FRONTEND noninteractive`" may cause + unintended consequences, because it will persist when the container is run + interactively, as with the following command: `docker run -t -i image bash` + +**ADD** + -- **ADD** has two forms: + + ``` + ADD + + # Required for paths with whitespace + ADD ["",... ""] + ``` + + The **ADD** instruction copies new files, directories + or remote file URLs to the filesystem of the container at path ``. + Multiple `` resources may be specified but if they are files or directories + then they must be relative to the source directory that is being built + (the context of the build). The `` is the absolute path, or path relative + to **WORKDIR**, into which the source is copied inside the target container. + If the `` argument is a local file in a recognized compression format + (tar, gzip, bzip2, etc) then it is unpacked at the specified `` in the + container's filesystem. Note that only local compressed files will be unpacked, + i.e., the URL download and archive unpacking features cannot be used together. + All new directories are created with mode 0755 and with the uid and gid of **0**. + +**COPY** + -- **COPY** has two forms: + + ``` + COPY + + # Required for paths with whitespace + COPY ["",... ""] + ``` + + The **COPY** instruction copies new files from `` and + adds them to the filesystem of the container at path . The `` must be + the path to a file or directory relative to the source directory that is + being built (the context of the build) or a remote file URL. The `` is an + absolute path, or a path relative to **WORKDIR**, into which the source will + be copied inside the target container. If you **COPY** an archive file it will + land in the container exactly as it appears in the build context without any + attempt to unpack it. All new files and directories are created with mode **0755** + and with the uid and gid of **0**. + +**ENTRYPOINT** + -- **ENTRYPOINT** has two forms: + + ``` + # executable form + ENTRYPOINT ["executable", "param1", "param2"]` + + # run command in a shell - /bin/sh -c + ENTRYPOINT command param1 param2 + ``` + + -- An **ENTRYPOINT** helps you configure a + container that can be run as an executable. When you specify an **ENTRYPOINT**, + the whole container runs as if it was only that executable. The **ENTRYPOINT** + instruction adds an entry command that is not overwritten when arguments are + passed to docker run. This is different from the behavior of **CMD**. This allows + arguments to be passed to the entrypoint, for instance `docker run -d` + passes the -d argument to the **ENTRYPOINT**. Specify parameters either in the + **ENTRYPOINT** JSON array (as in the preferred exec form above), or by using a **CMD** + statement. Parameters in the **ENTRYPOINT** are not overwritten by the docker run + arguments. Parameters specified via **CMD** are overwritten by docker run + arguments. Specify a plain string for the **ENTRYPOINT**, and it will execute in + `/bin/sh -c`, like a **CMD** instruction: + + ``` + FROM ubuntu + ENTRYPOINT wc -l - + ``` + + This means that the Dockerfile's image always takes stdin as input (that's + what "-" means), and prints the number of lines (that's what "-l" means). To + make this optional but default, use a **CMD**: + + ``` + FROM ubuntu + CMD ["-l", "-"] + ENTRYPOINT ["/usr/bin/wc"] + ``` + +**VOLUME** + -- `VOLUME ["/data"]` + The **VOLUME** instruction creates a mount point with the specified name and marks + it as holding externally-mounted volumes from the native host or from other + containers. + +**USER** + -- `USER daemon` + Sets the username or UID used for running subsequent commands. + + The **USER** instruction can optionally be used to set the group or GID. The + followings examples are all valid: + USER [user | user:group | uid | uid:gid | user:gid | uid:group ] + + Until the **USER** instruction is set, instructions will be run as root. The USER + instruction can be used any number of times in a Dockerfile, and will only affect + subsequent commands. + +**WORKDIR** + -- `WORKDIR /path/to/workdir` + The **WORKDIR** instruction sets the working directory for the **RUN**, **CMD**, + **ENTRYPOINT**, **COPY** and **ADD** Dockerfile commands that follow it. It can + be used multiple times in a single Dockerfile. Relative paths are defined + relative to the path of the previous **WORKDIR** instruction. For example: + + ``` + WORKDIR /a + WORKDIR b + WORKDIR c + RUN pwd + ``` + + In the above example, the output of the **pwd** command is **a/b/c**. + +**ARG** + -- ARG [=] + + The `ARG` instruction defines a variable that users can pass at build-time to + the builder with the `docker build` command using the `--build-arg + =` flag. If a user specifies a build argument that was not + defined in the Dockerfile, the build outputs an error. + + ``` + One or more build-args were not consumed, failing build. + ``` + + The Dockerfile author can define a single variable by specifying `ARG` once or many + variables by specifying `ARG` more than once. For example, a valid Dockerfile: + + ``` + FROM busybox + ARG user1 + ARG buildno + ... + ``` + + A Dockerfile author may optionally specify a default value for an `ARG` instruction: + + ``` + FROM busybox + ARG user1=someuser + ARG buildno=1 + ... + ``` + + If an `ARG` value has a default and if there is no value passed at build-time, the + builder uses the default. + + An `ARG` variable definition comes into effect from the line on which it is + defined in the `Dockerfile` not from the argument's use on the command-line or + elsewhere. For example, consider this Dockerfile: + + ``` + 1 FROM busybox + 2 USER ${user:-some_user} + 3 ARG user + 4 USER $user + ... + ``` + A user builds this file by calling: + + ``` + $ docker build --build-arg user=what_user Dockerfile + ``` + + The `USER` at line 2 evaluates to `some_user` as the `user` variable is defined on the + subsequent line 3. The `USER` at line 4 evaluates to `what_user` as `user` is + defined and the `what_user` value was passed on the command line. Prior to its definition by an + `ARG` instruction, any use of a variable results in an empty string. + + > **Note:** It is not recommended to use build-time variables for + > passing secrets like github keys, user credentials etc. + + You can use an `ARG` or an `ENV` instruction to specify variables that are + available to the `RUN` instruction. Environment variables defined using the + `ENV` instruction always override an `ARG` instruction of the same name. Consider + this Dockerfile with an `ENV` and `ARG` instruction. + + ``` + 1 FROM ubuntu + 2 ARG CONT_IMG_VER + 3 ENV CONT_IMG_VER v1.0.0 + 4 RUN echo $CONT_IMG_VER + ``` + Then, assume this image is built with this command: + + ``` + $ docker build --build-arg CONT_IMG_VER=v2.0.1 Dockerfile + ``` + + In this case, the `RUN` instruction uses `v1.0.0` instead of the `ARG` setting + passed by the user:`v2.0.1` This behavior is similar to a shell + script where a locally scoped variable overrides the variables passed as + arguments or inherited from environment, from its point of definition. + + Using the example above but a different `ENV` specification you can create more + useful interactions between `ARG` and `ENV` instructions: + + ``` + 1 FROM ubuntu + 2 ARG CONT_IMG_VER + 3 ENV CONT_IMG_VER ${CONT_IMG_VER:-v1.0.0} + 4 RUN echo $CONT_IMG_VER + ``` + + Unlike an `ARG` instruction, `ENV` values are always persisted in the built + image. Consider a docker build without the --build-arg flag: + + ``` + $ docker build Dockerfile + ``` + + Using this Dockerfile example, `CONT_IMG_VER` is still persisted in the image but + its value would be `v1.0.0` as it is the default set in line 3 by the `ENV` instruction. + + The variable expansion technique in this example allows you to pass arguments + from the command line and persist them in the final image by leveraging the + `ENV` instruction. Variable expansion is only supported for [a limited set of + Dockerfile instructions.](#environment-replacement) + + Docker has a set of predefined `ARG` variables that you can use without a + corresponding `ARG` instruction in the Dockerfile. + + * `HTTP_PROXY` + * `http_proxy` + * `HTTPS_PROXY` + * `https_proxy` + * `FTP_PROXY` + * `ftp_proxy` + * `NO_PROXY` + * `no_proxy` + + To use these, simply pass them on the command line using the `--build-arg + =` flag. + +**ONBUILD** + -- `ONBUILD [INSTRUCTION]` + The **ONBUILD** instruction adds a trigger instruction to an image. The + trigger is executed at a later time, when the image is used as the base for + another build. Docker executes the trigger in the context of the downstream + build, as if the trigger existed immediately after the **FROM** instruction in + the downstream Dockerfile. + + You can register any build instruction as a trigger. A trigger is useful if + you are defining an image to use as a base for building other images. For + example, if you are defining an application build environment or a daemon that + is customized with a user-specific configuration. + + Consider an image intended as a reusable python application builder. It must + add application source code to a particular directory, and might need a build + script called after that. You can't just call **ADD** and **RUN** now, because + you don't yet have access to the application source code, and it is different + for each application build. + + -- Providing application developers with a boilerplate Dockerfile to copy-paste + into their application is inefficient, error-prone, and + difficult to update because it mixes with application-specific code. + The solution is to use **ONBUILD** to register instructions in advance, to + run later, during the next build stage. + +# HISTORY +*May 2014, Compiled by Zac Dover (zdover at redhat dot com) based on docker.com Dockerfile documentation. +*Feb 2015, updated by Brian Goff (cpuguy83@gmail.com) for readability +*Sept 2015, updated by Sally O'Malley (somalley@redhat.com) diff --git a/man/README.md b/man/README.md new file mode 100644 index 00000000..922bc3cc --- /dev/null +++ b/man/README.md @@ -0,0 +1,33 @@ +Docker Documentation +==================== + +This directory contains the Docker user manual in the Markdown format. +Do *not* edit the man pages in the man1 directory. Instead, amend the +Markdown (*.md) files. + +# Generating man pages from the Markdown files + +The recommended approach for generating the man pages is via a Docker +container using the supplied `Dockerfile` to create an image with the correct +environment. This uses `go-md2man`, a pure Go Markdown to man page generator. + +## Building the md2man image + +There is a `Dockerfile` provided in the `/man` directory of your +'docker/docker' fork. + +Using this `Dockerfile`, create a Docker image tagged `docker/md2man`: + + docker build -t docker/md2man . + +## Utilizing the image + +From within the `/man` directory run the following command: + + docker run -v $(pwd):/man -w /man -i docker/md2man ./md2man-all.sh + +The `md2man` Docker container will process the Markdown files and generate +the man pages inside the `/man/man1` directory of your fork using +Docker volumes. For more information on Docker volumes see the man page for +`docker run` and also look at the article [Sharing Directories via Volumes] +(https://docs.docker.com/use/working_with_volumes/). diff --git a/man/config-json.5.md b/man/config-json.5.md new file mode 100644 index 00000000..49987f08 --- /dev/null +++ b/man/config-json.5.md @@ -0,0 +1,72 @@ +% CONFIG.JSON(5) Docker User Manuals +% Docker Community +% JANUARY 2016 +# NAME +HOME/.docker/config.json - Default Docker configuration file + +# INTRODUCTION + +By default, the Docker command line stores its configuration files in a +directory called `.docker` within your `$HOME` directory. Docker manages most of +the files in the configuration directory and you should not modify them. +However, you *can modify* the `config.json` file to control certain aspects of +how the `docker` command behaves. + +Currently, you can modify the `docker` command behavior using environment +variables or command-line options. You can also use options within +`config.json` to modify some of the same behavior. When using these +mechanisms, you must keep in mind the order of precedence among them. Command +line options override environment variables and environment variables override +properties you specify in a `config.json` file. + +The `config.json` file stores a JSON encoding of several properties: + +* The `HttpHeaders` property specifies a set of headers to include in all messages +sent from the Docker client to the daemon. Docker does not try to interpret or +understand these header; it simply puts them into the messages. Docker does not +allow these headers to change any headers it sets for itself. + +* The `psFormat` property specifies the default format for `docker ps` output. +When the `--format` flag is not provided with the `docker ps` command, +Docker's client uses this property. If this property is not set, the client +falls back to the default table format. For a list of supported formatting +directives, see **docker-ps(1)**. + +* The `detachKeys` property specifies the default key sequence which +detaches the container. When the `--detach-keys` flag is not provide +with the `docker attach`, `docker exec`, `docker run` or `docker +start`, Docker's client uses this property. If this property is not +set, the client falls back to the default sequence `ctrl-p,ctrl-q`. + + +* The `imagesFormat` property specifies the default format for `docker images` +output. When the `--format` flag is not provided with the `docker images` +command, Docker's client uses this property. If this property is not set, the +client falls back to the default table format. For a list of supported +formatting directives, see **docker-images(1)**. + +You can specify a different location for the configuration files via the +`DOCKER_CONFIG` environment variable or the `--config` command line option. If +both are specified, then the `--config` option overrides the `DOCKER_CONFIG` +environment variable: + + docker --config ~/testconfigs/ ps + +This command instructs Docker to use the configuration files in the +`~/testconfigs/` directory when running the `ps` command. + +## Examples + +Following is a sample `config.json` file: + + { + "HttpHeaders": { + "MyHeader": "MyValue" + }, + "psFormat": "table {{.ID}}\\t{{.Image}}\\t{{.Command}}\\t{{.Labels}}", + "imagesFormat": "table {{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}", + "detachKeys": "ctrl-e,e" + } + +# HISTORY +January 2016, created by Moxiegirl diff --git a/man/docker-attach.1.md b/man/docker-attach.1.md new file mode 100644 index 00000000..c78f4fbb --- /dev/null +++ b/man/docker-attach.1.md @@ -0,0 +1,99 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-attach - Attach to a running container + +# SYNOPSIS +**docker attach** +[**--detach-keys**[=*[]*]] +[**--help**] +[**--no-stdin**] +[**--sig-proxy**[=*true*]] +CONTAINER + +# DESCRIPTION +The **docker attach** command allows you to attach to a running container using +the container's ID or name, either to view its ongoing output or to control it +interactively. You can attach to the same contained process multiple times +simultaneously, screen sharing style, or quickly view the progress of your +detached process. + +To stop a container, use `CTRL-c`. This key sequence sends `SIGKILL` to the +container. You can detach from the container (and leave it running) using a +configurable key sequence. The default sequence is `CTRL-p CTRL-q`. You +configure the key sequence using the **--detach-keys** option or a configuration +file. See **config-json(5)** for documentation on using a configuration file. + +It is forbidden to redirect the standard input of a `docker attach` command while +attaching to a tty-enabled container (i.e.: launched with `-t`). + +# OPTIONS +**--detach-keys**="" + Override the key sequence for detaching a container. Format is a single character `[a-Z]` or `ctrl-` where `` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`. + +**--help** + Print usage statement + +**--no-stdin**=*true*|*false* + Do not attach STDIN. The default is *false*. + +**--sig-proxy**=*true*|*false* + Proxy all received signals to the process (non-TTY mode only). SIGCHLD, SIGKILL, and SIGSTOP are not proxied. The default is *true*. + +# Override the detach sequence + +If you want, you can configure a override the Docker key sequence for detach. +This is is useful if the Docker default sequence conflicts with key squence you +use for other applications. There are two ways to defines a your own detach key +sequence, as a per-container override or as a configuration property on your +entire configuration. + +To override the sequence for an individual container, use the +`--detach-keys=""` flag with the `docker attach` command. The format of +the `` is either a letter [a-Z], or the `ctrl-` combined with any of +the following: + +* `a-z` (a single lowercase alpha character ) +* `@` (at sign) +* `[` (left bracket) +* `\\` (two backward slashes) +* `_` (underscore) +* `^` (caret) + +These `a`, `ctrl-a`, `X`, or `ctrl-\\` values are all examples of valid key +sequences. To configure a different configuration default key sequence for all +containers, see **docker(1)**. + +# EXAMPLES + +## Attaching to a container + +In this example the top command is run inside a container, from an image called +fedora, in detached mode. The ID from the container is passed into the **docker +attach** command: + + # ID=$(sudo docker run -d fedora /usr/bin/top -b) + # sudo docker attach $ID + top - 02:05:52 up 3:05, 0 users, load average: 0.01, 0.02, 0.05 + Tasks: 1 total, 1 running, 0 sleeping, 0 stopped, 0 zombie + Cpu(s): 0.1%us, 0.2%sy, 0.0%ni, 99.7%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st + Mem: 373572k total, 355560k used, 18012k free, 27872k buffers + Swap: 786428k total, 0k used, 786428k free, 221740k cached + + PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND + 1 root 20 0 17200 1116 912 R 0 0.3 0:00.03 top + + top - 02:05:55 up 3:05, 0 users, load average: 0.01, 0.02, 0.05 + Tasks: 1 total, 1 running, 0 sleeping, 0 stopped, 0 zombie + Cpu(s): 0.0%us, 0.2%sy, 0.0%ni, 99.8%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st + Mem: 373572k total, 355244k used, 18328k free, 27872k buffers + Swap: 786428k total, 0k used, 786428k free, 221776k cached + + PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND + 1 root 20 0 17208 1144 932 R 0 0.3 0:00.03 top + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit diff --git a/man/docker-build.1.md b/man/docker-build.1.md new file mode 100644 index 00000000..c854bc11 --- /dev/null +++ b/man/docker-build.1.md @@ -0,0 +1,311 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-build - Build a new image from the source code at PATH + +# SYNOPSIS +**docker build** +[**--build-arg**[=*[]*]] +[**--cpu-shares**[=*0*]] +[**--cgroup-parent**[=*CGROUP-PARENT*]] +[**--help**] +[**-f**|**--file**[=*PATH/Dockerfile*]] +[**--force-rm**] +[**--isolation**[=*default*]] +[**--label**[=*[]*]] +[**--no-cache**] +[**--pull**] +[**-q**|**--quiet**] +[**--rm**[=*true*]] +[**-t**|**--tag**[=*[]*]] +[**-m**|**--memory**[=*MEMORY*]] +[**--memory-swap**[=*LIMIT*]] +[**--shm-size**[=*SHM-SIZE*]] +[**--cpu-period**[=*0*]] +[**--cpu-quota**[=*0*]] +[**--cpuset-cpus**[=*CPUSET-CPUS*]] +[**--cpuset-mems**[=*CPUSET-MEMS*]] +[**--ulimit**[=*[]*]] +PATH | URL | - + +# DESCRIPTION +This will read the Dockerfile from the directory specified in **PATH**. +It also sends any other files and directories found in the current +directory to the Docker daemon. The contents of this directory would +be used by **ADD** commands found within the Dockerfile. + +Warning, this will send a lot of data to the Docker daemon depending +on the contents of the current directory. The build is run by the Docker +daemon, not by the CLI, so the whole context must be transferred to the daemon. +The Docker CLI reports "Sending build context to Docker daemon" when the context is sent to +the daemon. + +When the URL to a tarball archive or to a single Dockerfile is given, no context is sent from +the client to the Docker daemon. In this case, the Dockerfile at the root of the archive and +the rest of the archive will get used as the context of the build. When a Git repository is +set as the **URL**, the repository is cloned locally and then sent as the context. + +# OPTIONS +**-f**, **--file**=*PATH/Dockerfile* + Path to the Dockerfile to use. If the path is a relative path and you are + building from a local directory, then the path must be relative to that + directory. If you are building from a remote URL pointing to either a + tarball or a Git repository, then the path must be relative to the root of + the remote context. In all cases, the file must be within the build context. + The default is *Dockerfile*. + +**--build-arg**=*variable* + name and value of a **buildarg**. + + For example, if you want to pass a value for `http_proxy`, use + `--build-arg=http_proxy="http://some.proxy.url"` + + Users pass these values at build-time. Docker uses the `buildargs` as the + environment context for command(s) run via the Dockerfile's `RUN` instruction + or for variable expansion in other Dockerfile instructions. This is not meant + for passing secret values. [Read more about the buildargs instruction](/reference/builder/#arg) + +**--force-rm**=*true*|*false* + Always remove intermediate containers, even after unsuccessful builds. The default is *false*. + +**--isolation**="*default*" + Isolation specifies the type of isolation technology used by containers. + +**--label**=*label* + Set metadata for an image + +**--no-cache**=*true*|*false* + Do not use cache when building the image. The default is *false*. + +**--help** + Print usage statement + +**--pull**=*true*|*false* + Always attempt to pull a newer version of the image. The default is *false*. + +**-q**, **--quiet**=*true*|*false* + Suppress the build output and print image ID on success. The default is *false*. + +**--rm**=*true*|*false* + Remove intermediate containers after a successful build. The default is *true*. + +**-t**, **--tag**="" + Repository names (and optionally with tags) to be applied to the resulting image in case of success. + +**-m**, **--memory**=*MEMORY* + Memory limit + +**--memory-swap**=*LIMIT* + A limit value equal to memory plus swap. Must be used with the **-m** +(**--memory**) flag. The swap `LIMIT` should always be larger than **-m** +(**--memory**) value. + + The format of `LIMIT` is `[]`. Unit can be `b` (bytes), +`k` (kilobytes), `m` (megabytes), or `g` (gigabytes). If you don't specify a +unit, `b` is used. Set LIMIT to `-1` to enable unlimited swap. + +**--shm-size**=*SHM-SIZE* + Size of `/dev/shm`. The format is ``. `number` must be greater than `0`. + Unit is optional and can be `b` (bytes), `k` (kilobytes), `m` (megabytes), or `g` (gigabytes). If you omit the unit, the system uses bytes. + If you omit the size entirely, the system uses `64m`. + +**--cpu-shares**=*0* + CPU shares (relative weight). + + By default, all containers get the same proportion of CPU cycles. + CPU shares is a 'relative weight', relative to the default setting of 1024. + This default value is defined here: + ``` + cat /sys/fs/cgroup/cpu/cpu.shares + 1024 + ``` + You can change this proportion by adjusting the container's CPU share + weighting relative to the weighting of all other running containers. + + To modify the proportion from the default of 1024, use the **--cpu-shares** + flag to set the weighting to 2 or higher. + + Container CPU share Flag + {C0} 60% of CPU --cpu-shares=614 (614 is 60% of 1024) + {C1} 40% of CPU --cpu-shares=410 (410 is 40% of 1024) + + The proportion is only applied when CPU-intensive processes are running. + When tasks in one container are idle, the other containers can use the + left-over CPU time. The actual amount of CPU time used varies depending on + the number of containers running on the system. + + For example, consider three containers, where one has **--cpu-shares=1024** and + two others have **--cpu-shares=512**. When processes in all three + containers attempt to use 100% of CPU, the first container would receive + 50% of the total CPU time. If you add a fourth container with **--cpu-shares=1024**, + the first container only gets 33% of the CPU. The remaining containers + receive 16.5%, 16.5% and 33% of the CPU. + + + Container CPU share Flag CPU time + {C0} 100% --cpu-shares=1024 33% + {C1} 50% --cpu-shares=512 16.5% + {C2} 50% --cpu-shares=512 16.5% + {C4} 100% --cpu-shares=1024 33% + + + On a multi-core system, the shares of CPU time are distributed across the CPU + cores. Even if a container is limited to less than 100% of CPU time, it can + use 100% of each individual CPU core. + + For example, consider a system with more than three cores. If you start one + container **{C0}** with **--cpu-shares=512** running one process, and another container + **{C1}** with **--cpu-shares=1024** running two processes, this can result in the following + division of CPU shares: + + PID container CPU CPU share + 100 {C0} 0 100% of CPU0 + 101 {C1} 1 100% of CPU1 + 102 {C1} 2 100% of CPU2 + +**--cpu-period**=*0* + Limit the CPU CFS (Completely Fair Scheduler) period. + + Limit the container's CPU usage. This flag causes the kernel to restrict the + container's CPU usage to the period you specify. + +**--cpu-quota**=*0* + Limit the CPU CFS (Completely Fair Scheduler) quota. + + By default, containers run with the full CPU resource. This flag causes the +kernel to restrict the container's CPU usage to the quota you specify. + +**--cpuset-cpus**=*CPUSET-CPUS* + CPUs in which to allow execution (0-3, 0,1). + +**--cpuset-mems**=*CPUSET-MEMS* + Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on + NUMA systems. + + For example, if you have four memory nodes on your system (0-3), use `--cpuset-mems=0,1` +to ensure the processes in your Docker container only use memory from the first +two memory nodes. + +**--cgroup-parent**=*CGROUP-PARENT* + Path to `cgroups` under which the container's `cgroup` are created. + + If the path is not absolute, the path is considered relative to the `cgroups` path of the init process. +Cgroups are created if they do not already exist. + +**--ulimit**=[] + Ulimit options + + For more information about `ulimit` see [Setting ulimits in a +container](https://docs.docker.com/reference/commandline/run/#setting-ulimits-in-a-container) + +# EXAMPLES + +## Building an image using a Dockerfile located inside the current directory + +Docker images can be built using the build command and a Dockerfile: + + docker build . + +During the build process Docker creates intermediate images. In order to +keep them, you must explicitly set `--rm=false`. + + docker build --rm=false . + +A good practice is to make a sub-directory with a related name and create +the Dockerfile in that directory. For example, a directory called mongo may +contain a Dockerfile to create a Docker MongoDB image. Likewise, another +directory called httpd may be used to store Dockerfiles for Apache web +server images. + +It is also a good practice to add the files required for the image to the +sub-directory. These files will then be specified with the `COPY` or `ADD` +instructions in the `Dockerfile`. + +Note: If you include a tar file (a good practice), then Docker will +automatically extract the contents of the tar file specified within the `ADD` +instruction into the specified target. + +## Building an image and naming that image + +A good practice is to give a name to the image you are building. Note that +only a-z0-9-_. should be used for consistency. There are no hard rules here but it is best to give the names consideration. + +The **-t**/**--tag** flag is used to rename an image. Here are some examples: + +Though it is not a good practice, image names can be arbitrary: + + docker build -t myimage . + +A better approach is to provide a fully qualified and meaningful repository, +name, and tag (where the tag in this context means the qualifier after +the ":"). In this example we build a JBoss image for the Fedora repository +and give it the version 1.0: + + docker build -t fedora/jboss:1.0 . + +The next example is for the "whenry" user repository and uses Fedora and +JBoss and gives it the version 2.1 : + + docker build -t whenry/fedora-jboss:v2.1 . + +If you do not provide a version tag then Docker will assign `latest`: + + docker build -t whenry/fedora-jboss . + +When you list the images, the image above will have the tag `latest`. + +You can apply multiple tags to an image. For example, you can apply the `latest` +tag to a newly built image and add another tag that references a specific +version. +For example, to tag an image both as `whenry/fedora-jboss:latest` and +`whenry/fedora-jboss:v2.1`, use the following: + + docker build -t whenry/fedora-jboss:latest -t whenry/fedora-jboss:v2.1 . + +So renaming an image is arbitrary but consideration should be given to +a useful convention that makes sense for consumers and should also take +into account Docker community conventions. + + +## Building an image using a URL + +This will clone the specified GitHub repository from the URL and use it +as context. The Dockerfile at the root of the repository is used as +Dockerfile. This only works if the GitHub repository is a dedicated +repository. + + docker build github.com/scollier/purpletest + +Note: You can set an arbitrary Git repository via the `git://` schema. + +## Building an image using a URL to a tarball'ed context + +This will send the URL itself to the Docker daemon. The daemon will fetch the +tarball archive, decompress it and use its contents as the build context. The +Dockerfile at the root of the archive and the rest of the archive will get used +as the context of the build. If you pass an **-f PATH/Dockerfile** option as well, +the system will look for that file inside the contents of the tarball. + + docker build -f dev/Dockerfile https://10.10.10.1/docker/context.tar.gz + +Note: supported compression formats are 'xz', 'bzip2', 'gzip' and 'identity' (no compression). + +## Specify isolation technology for container (--isolation) + +This option is useful in situations where you are running Docker containers on +Windows. The `--isolation=` option sets a container's isolation +technology. On Linux, the only supported is the `default` option which uses +Linux namespaces. On Microsoft Windows, you can specify these values: + +* `default`: Use the value specified by the Docker daemon's `--exec-opt` . If the `daemon` does not specify an isolation technology, Microsoft Windows uses `process` as its default value. +* `process`: Namespace isolation only. +* `hyperv`: Hyper-V hypervisor partition-based isolation. + +Specifying the `--isolation` flag without a value is the same as setting `--isolation="default"`. + +# HISTORY +March 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit +June 2015, updated by Sally O'Malley diff --git a/man/docker-commit.1.md b/man/docker-commit.1.md new file mode 100644 index 00000000..5912d363 --- /dev/null +++ b/man/docker-commit.1.md @@ -0,0 +1,70 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-commit - Create a new image from a container's changes + +# SYNOPSIS +**docker commit** +[**-a**|**--author**[=*AUTHOR*]] +[**-c**|**--change**[=\[*DOCKERFILE INSTRUCTIONS*\]]] +[**--help**] +[**-m**|**--message**[=*MESSAGE*]] +[**-p**|**--pause**[=*true*]] +CONTAINER [REPOSITORY[:TAG]] + +# DESCRIPTION +Create a new image from an existing container specified by name or +container ID. The new image will contain the contents of the +container filesystem, *excluding* any data volumes. + +While the `docker commit` command is a convenient way of extending an +existing image, you should prefer the use of a Dockerfile and `docker +build` for generating images that you intend to share with other +people. + +# OPTIONS +**-a**, **--author**="" + Author (e.g., "John Hannibal Smith ") + +**-c** , **--change**=[] + Apply specified Dockerfile instructions while committing the image + Supported Dockerfile instructions: `CMD`|`ENTRYPOINT`|`ENV`|`EXPOSE`|`LABEL`|`ONBUILD`|`USER`|`VOLUME`|`WORKDIR` + +**--help** + Print usage statement + +**-m**, **--message**="" + Commit message + +**-p**, **--pause**=*true*|*false* + Pause container during commit. The default is *true*. + +# EXAMPLES + +## Creating a new image from an existing container +An existing Fedora based container has had Apache installed while running +in interactive mode with the bash shell. Apache is also running. To +create a new image run `docker ps` to find the container's ID and then run: + + # docker commit -m="Added Apache to Fedora base image" \ + -a="A D Ministrator" 98bd7fc99854 fedora/fedora_httpd:20 + +Note that only a-z0-9-_. are allowed when naming images from an +existing container. + +## Apply specified Dockerfile instructions while committing the image +If an existing container was created without the DEBUG environment +variable set to "true", you can create a new image based on that +container by first getting the container's ID with `docker ps` and +then running: + + # docker commit -c="ENV DEBUG true" 98bd7fc99854 debug-image + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and in +June 2014, updated by Sven Dowideit +July 2014, updated by Sven Dowideit +Oct 2014, updated by Daniel, Dao Quang Minh +June 2015, updated by Sally O'Malley diff --git a/man/docker-cp.1.md b/man/docker-cp.1.md new file mode 100644 index 00000000..84d64c26 --- /dev/null +++ b/man/docker-cp.1.md @@ -0,0 +1,166 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-cp - Copy files/folders between a container and the local filesystem. + +# SYNOPSIS +**docker cp** +[**--help**] +CONTAINER:SRC_PATH DEST_PATH|- + +**docker cp** +[**--help**] +SRC_PATH|- CONTAINER:DEST_PATH + +# DESCRIPTION + +The `docker cp` utility copies the contents of `SRC_PATH` to the `DEST_PATH`. +You can copy from the container's file system to the local machine or the +reverse, from the local filesystem to the container. If `-` is specified for +either the `SRC_PATH` or `DEST_PATH`, you can also stream a tar archive from +`STDIN` or to `STDOUT`. The `CONTAINER` can be a running or stopped container. +The `SRC_PATH` or `DEST_PATH` can be a file or directory. + +The `docker cp` command assumes container paths are relative to the container's +`/` (root) directory. This means supplying the initial forward slash is optional; +The command sees `compassionate_darwin:/tmp/foo/myfile.txt` and +`compassionate_darwin:tmp/foo/myfile.txt` as identical. Local machine paths can +be an absolute or relative value. The command interprets a local machine's +relative paths as relative to the current working directory where `docker cp` is +run. + +The `cp` command behaves like the Unix `cp -a` command in that directories are +copied recursively with permissions preserved if possible. Ownership is set to +the user and primary group at the destination. For example, files copied to a +container are created with `UID:GID` of the root user. Files copied to the local +machine are created with the `UID:GID` of the user which invoked the `docker cp` +command. If you specify the `-L` option, `docker cp` follows any symbolic link +in the `SRC_PATH`. `docker cp` does *not* create parent directories for +`DEST_PATH` if they do not exist. + +Assuming a path separator of `/`, a first argument of `SRC_PATH` and second +argument of `DEST_PATH`, the behavior is as follows: + +- `SRC_PATH` specifies a file + - `DEST_PATH` does not exist + - the file is saved to a file created at `DEST_PATH` + - `DEST_PATH` does not exist and ends with `/` + - Error condition: the destination directory must exist. + - `DEST_PATH` exists and is a file + - the destination is overwritten with the source file's contents + - `DEST_PATH` exists and is a directory + - the file is copied into this directory using the basename from + `SRC_PATH` +- `SRC_PATH` specifies a directory + - `DEST_PATH` does not exist + - `DEST_PATH` is created as a directory and the *contents* of the source + directory are copied into this directory + - `DEST_PATH` exists and is a file + - Error condition: cannot copy a directory to a file + - `DEST_PATH` exists and is a directory + - `SRC_PATH` does not end with `/.` + - the source directory is copied into this directory + - `SRC_PATH` does end with `/.` + - the *content* of the source directory is copied into this + directory + +The command requires `SRC_PATH` and `DEST_PATH` to exist according to the above +rules. If `SRC_PATH` is local and is a symbolic link, the symbolic link, not +the target, is copied by default. To copy the link target and not the link, +specify the `-L` option. + +A colon (`:`) is used as a delimiter between `CONTAINER` and its path. You can +also use `:` when specifying paths to a `SRC_PATH` or `DEST_PATH` on a local +machine, for example `file:name.txt`. If you use a `:` in a local machine path, +you must be explicit with a relative or absolute path, for example: + + `/path/to/file:name.txt` or `./file:name.txt` + +It is not possible to copy certain system files such as resources under +`/proc`, `/sys`, `/dev`, and mounts created by the user in the container. + +Using `-` as the `SRC_PATH` streams the contents of `STDIN` as a tar archive. +The command extracts the content of the tar to the `DEST_PATH` in container's +filesystem. In this case, `DEST_PATH` must specify a directory. Using `-` as +the `DEST_PATH` streams the contents of the resource as a tar archive to `STDOUT`. + +# OPTIONS +**-L**, **--follow-link**=*true*|*false* + Follow symbol link in SRC_PATH + +**--help** + Print usage statement + +# EXAMPLES + +Suppose a container has finished producing some output as a file it saves +to somewhere in its filesystem. This could be the output of a build job or +some other computation. You can copy these outputs from the container to a +location on your local host. + +If you want to copy the `/tmp/foo` directory from a container to the +existing `/tmp` directory on your host. If you run `docker cp` in your `~` +(home) directory on the local host: + + $ docker cp compassionate_darwin:tmp/foo /tmp + +Docker creates a `/tmp/foo` directory on your host. Alternatively, you can omit +the leading slash in the command. If you execute this command from your home +directory: + + $ docker cp compassionate_darwin:tmp/foo tmp + +If `~/tmp` does not exist, Docker will create it and copy the contents of +`/tmp/foo` from the container into this new directory. If `~/tmp` already +exists as a directory, then Docker will copy the contents of `/tmp/foo` from +the container into a directory at `~/tmp/foo`. + +When copying a single file to an existing `LOCALPATH`, the `docker cp` command +will either overwrite the contents of `LOCALPATH` if it is a file or place it +into `LOCALPATH` if it is a directory, overwriting an existing file of the same +name if one exists. For example, this command: + + $ docker cp sharp_ptolemy:/tmp/foo/myfile.txt /test + +If `/test` does not exist on the local machine, it will be created as a file +with the contents of `/tmp/foo/myfile.txt` from the container. If `/test` +exists as a file, it will be overwritten. Lastly, if `/test` exists as a +directory, the file will be copied to `/test/myfile.txt`. + +Next, suppose you want to copy a file or folder into a container. For example, +this could be a configuration file or some other input to a long running +computation that you would like to place into a created container before it +starts. This is useful because it does not require the configuration file or +other input to exist in the container image. + +If you have a file, `config.yml`, in the current directory on your local host +and wish to copy it to an existing directory at `/etc/my-app.d` in a container, +this command can be used: + + $ docker cp config.yml myappcontainer:/etc/my-app.d + +If you have several files in a local directory `/config` which you need to copy +to a directory `/etc/my-app.d` in a container: + + $ docker cp /config/. myappcontainer:/etc/my-app.d + +The above command will copy the contents of the local `/config` directory into +the directory `/etc/my-app.d` in the container. + +Finally, if you want to copy a symbolic link into a container, you typically +want to copy the linked target and not the link itself. To copy the target, use +the `-L` option, for example: + + $ ln -s /tmp/somefile /tmp/somefile.ln + $ docker cp -L /tmp/somefile.ln myappcontainer:/tmp/ + +This command copies content of the local `/tmp/somefile` into the file +`/tmp/somefile.ln` in the container. Without `-L` option, the `/tmp/somefile.ln` +preserves its symbolic link but not its content. + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit +May 2015, updated by Josh Hawn diff --git a/man/docker-create.1.md b/man/docker-create.1.md new file mode 100644 index 00000000..f12edb54 --- /dev/null +++ b/man/docker-create.1.md @@ -0,0 +1,470 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-create - Create a new container + +# SYNOPSIS +**docker create** +[**-a**|**--attach**[=*[]*]] +[**--add-host**[=*[]*]] +[**--blkio-weight**[=*[BLKIO-WEIGHT]*]] +[**--blkio-weight-device**[=*[]*]] +[**--cpu-shares**[=*0*]] +[**--cap-add**[=*[]*]] +[**--cap-drop**[=*[]*]] +[**--cgroup-parent**[=*CGROUP-PATH*]] +[**--cidfile**[=*CIDFILE*]] +[**--cpu-period**[=*0*]] +[**--cpu-quota**[=*0*]] +[**--cpuset-cpus**[=*CPUSET-CPUS*]] +[**--cpuset-mems**[=*CPUSET-MEMS*]] +[**--device**[=*[]*]] +[**--device-read-bps**[=*[]*]] +[**--device-read-iops**[=*[]*]] +[**--device-write-bps**[=*[]*]] +[**--device-write-iops**[=*[]*]] +[**--dns**[=*[]*]] +[**--dns-search**[=*[]*]] +[**--dns-opt**[=*[]*]] +[**-e**|**--env**[=*[]*]] +[**--entrypoint**[=*ENTRYPOINT*]] +[**--env-file**[=*[]*]] +[**--expose**[=*[]*]] +[**--group-add**[=*[]*]] +[**-h**|**--hostname**[=*HOSTNAME*]] +[**--help**] +[**-i**|**--interactive**] +[**--ip**[=*IPv4-ADDRESS*]] +[**--ip6**[=*IPv6-ADDRESS*]] +[**--ipc**[=*IPC*]] +[**--isolation**[=*default*]] +[**--kernel-memory**[=*KERNEL-MEMORY*]] +[**-l**|**--label**[=*[]*]] +[**--label-file**[=*[]*]] +[**--link**[=*[]*]] +[**--log-driver**[=*[]*]] +[**--log-opt**[=*[]*]] +[**-m**|**--memory**[=*MEMORY*]] +[**--mac-address**[=*MAC-ADDRESS*]] +[**--memory-reservation**[=*MEMORY-RESERVATION*]] +[**--memory-swap**[=*LIMIT*]] +[**--memory-swappiness**[=*MEMORY-SWAPPINESS*]] +[**--name**[=*NAME*]] +[**--net**[=*"bridge"*]] +[**--net-alias**[=*[]*]] +[**--oom-kill-disable**] +[**--oom-score-adj**[=*0*]] +[**-P**|**--publish-all**] +[**-p**|**--publish**[=*[]*]] +[**--pid**[=*[]*]] +[**--userns**[=*[]*]] +[**--pids-limit**[=*PIDS_LIMIT*]] +[**--privileged**] +[**--read-only**] +[**--restart**[=*RESTART*]] +[**--security-opt**[=*[]*]] +[**--stop-signal**[=*SIGNAL*]] +[**--shm-size**[=*[]*]] +[**-t**|**--tty**] +[**--tmpfs**[=*[CONTAINER-DIR[:]*]] +[**-u**|**--user**[=*USER*]] +[**--ulimit**[=*[]*]] +[**--uts**[=*[]*]] +[**-v**|**--volume**[=*[[HOST-DIR:]CONTAINER-DIR[:OPTIONS]]*]] +[**--volume-driver**[=*DRIVER*]] +[**--volumes-from**[=*[]*]] +[**-w**|**--workdir**[=*WORKDIR*]] +IMAGE [COMMAND] [ARG...] + +# DESCRIPTION + +Creates a writeable container layer over the specified image and prepares it for +running the specified command. The container ID is then printed to STDOUT. This +is similar to **docker run -d** except the container is never started. You can +then use the **docker start ** command to start the container at +any point. + +The initial status of the container created with **docker create** is 'created'. + +# OPTIONS +**-a**, **--attach**=[] + Attach to STDIN, STDOUT or STDERR. + +**--add-host**=[] + Add a custom host-to-IP mapping (host:ip) + +**--blkio-weight**=*0* + Block IO weight (relative weight) accepts a weight value between 10 and 1000. + +**--blkio-weight-device**=[] + Block IO weight (relative device weight, format: `DEVICE_NAME:WEIGHT`). + +**--cpu-shares**=*0* + CPU shares (relative weight) + +**--cap-add**=[] + Add Linux capabilities + +**--cap-drop**=[] + Drop Linux capabilities + +**--cgroup-parent**="" + Path to cgroups under which the cgroup for the container will be created. If the path is not absolute, the path is considered to be relative to the cgroups path of the init process. Cgroups will be created if they do not already exist. + +**--cidfile**="" + Write the container ID to the file + +**--cpu-period**=*0* + Limit the CPU CFS (Completely Fair Scheduler) period + +**--cpuset-cpus**="" + CPUs in which to allow execution (0-3, 0,1) + +**--cpuset-mems**="" + Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems. + + If you have four memory nodes on your system (0-3), use `--cpuset-mems=0,1` +then processes in your Docker container will only use memory from the first +two memory nodes. + +**--cpu-quota**=*0* + Limit the CPU CFS (Completely Fair Scheduler) quota + +**--device**=[] + Add a host device to the container (e.g. --device=/dev/sdc:/dev/xvdc:rwm) + +**--device-read-bps**=[] + Limit read rate (bytes per second) from a device (e.g. --device-read-bps=/dev/sda:1mb) + +**--device-read-iops**=[] + Limit read rate (IO per second) from a device (e.g. --device-read-iops=/dev/sda:1000) + +**--device-write-bps**=[] + Limit write rate (bytes per second) to a device (e.g. --device-write-bps=/dev/sda:1mb) + +**--device-write-iops**=[] + Limit write rate (IO per second) to a device (e.g. --device-write-iops=/dev/sda:1000) + +**--dns**=[] + Set custom DNS servers + +**--dns-opt**=[] + Set custom DNS options + +**--dns-search**=[] + Set custom DNS search domains (Use --dns-search=. if you don't wish to set the search domain) + +**-e**, **--env**=[] + Set environment variables + +**--entrypoint**="" + Overwrite the default ENTRYPOINT of the image + +**--env-file**=[] + Read in a line-delimited file of environment variables + +**--expose**=[] + Expose a port or a range of ports (e.g. --expose=3300-3310) from the container without publishing it to your host + +**--group-add**=[] + Add additional groups to run as + +**-h**, **--hostname**="" + Container host name + +**--help** + Print usage statement + +**-i**, **--interactive**=*true*|*false* + Keep STDIN open even if not attached. The default is *false*. + +**--ip**="" + Sets the container's interface IPv4 address (e.g. 172.23.0.9) + + It can only be used in conjunction with **--net** for user-defined networks + +**--ip6**="" + Sets the container's interface IPv6 address (e.g. 2001:db8::1b99) + + It can only be used in conjunction with **--net** for user-defined networks + +**--ipc**="" + Default is to create a private IPC namespace (POSIX SysV IPC) for the container + 'container:': reuses another container shared memory, semaphores and message queues + 'host': use the host shared memory,semaphores and message queues inside the container. Note: the host mode gives the container full access to local shared memory and is therefore considered insecure. + +**--isolation**="*default*" + Isolation specifies the type of isolation technology used by containers. + +**--kernel-memory**="" + Kernel memory limit (format: `[]`, where unit = b, k, m or g) + + Constrains the kernel memory available to a container. If a limit of 0 +is specified (not using `--kernel-memory`), the container's kernel memory +is not limited. If you specify a limit, it may be rounded up to a multiple +of the operating system's page size and the value can be very large, +millions of trillions. + +**-l**, **--label**=[] + Adds metadata to a container (e.g., --label=com.example.key=value) + +**--label-file**=[] + Read labels from a file. Delimit each label with an EOL. + +**--link**=[] + Add link to another container in the form of :alias or just + in which case the alias will match the name. + +**--log-driver**="*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*splunk*|*etwlogs*|*gcplogs*|*none*" + Logging driver for container. Default is defined by daemon `--log-driver` flag. + **Warning**: the `docker logs` command works only for the `json-file` and + `journald` logging drivers. + +**--log-opt**=[] + Logging driver specific options. + +**-m**, **--memory**="" + Memory limit (format: [], where unit = b, k, m or g) + + Allows you to constrain the memory available to a container. If the host +supports swap memory, then the **-m** memory setting can be larger than physical +RAM. If a limit of 0 is specified (not using **-m**), the container's memory is +not limited. The actual limit may be rounded up to a multiple of the operating +system's page size (the value would be very large, that's millions of trillions). + +**--mac-address**="" + Container MAC address (e.g. 92:d0:c6:0a:29:33) + +**--memory-reservation**="" + Memory soft limit (format: [], where unit = b, k, m or g) + + After setting memory reservation, when the system detects memory contention +or low memory, containers are forced to restrict their consumption to their +reservation. So you should always set the value below **--memory**, otherwise the +hard limit will take precedence. By default, memory reservation will be the same +as memory limit. + +**--memory-swap**="LIMIT" + A limit value equal to memory plus swap. Must be used with the **-m** +(**--memory**) flag. The swap `LIMIT` should always be larger than **-m** +(**--memory**) value. + + The format of `LIMIT` is `[]`. Unit can be `b` (bytes), +`k` (kilobytes), `m` (megabytes), or `g` (gigabytes). If you don't specify a +unit, `b` is used. Set LIMIT to `-1` to enable unlimited swap. + +**--memory-swappiness**="" + Tune a container's memory swappiness behavior. Accepts an integer between 0 and 100. + +**--name**="" + Assign a name to the container + +**--net**="*bridge*" + Set the Network mode for the container + 'bridge': create a network stack on the default Docker bridge + 'none': no networking + 'container:': reuse another container's network stack + 'host': use the Docker host network stack. Note: the host mode gives the container full access to local system services such as D-bus and is therefore considered insecure. + '|': connect to a user-defined network + +**--net-alias**=[] + Add network-scoped alias for the container + +**--oom-kill-disable**=*true*|*false* + Whether to disable OOM Killer for the container or not. + +**--oom-score-adj**="" + Tune the host's OOM preferences for containers (accepts -1000 to 1000) + +**-P**, **--publish-all**=*true*|*false* + Publish all exposed ports to random ports on the host interfaces. The default is *false*. + +**-p**, **--publish**=[] + Publish a container's port, or a range of ports, to the host + format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort + Both hostPort and containerPort can be specified as a range of ports. + When specifying ranges for both, the number of container ports in the range must match the number of host ports in the range. (e.g., `-p 1234-1236:1234-1236/tcp`) + (use 'docker port' to see the actual mapping) + +**--pid**=*host* + Set the PID mode for the container + **host**: use the host's PID namespace inside the container. + Note: the host mode gives the container full access to local PID and is therefore considered insecure. + +**--userns**="" + Set the usernamespace mode for the container when `userns-remap` option is enabled. + **host**: use the host usernamespace and enable all privileged options (e.g., `pid=host` or `--privileged`). + +**--pids-limit**="" + Tune the container's pids limit. Set `-1` to have unlimited pids for the container. + +**--privileged**=*true*|*false* + Give extended privileges to this container. The default is *false*. + +**--read-only**=*true*|*false* + Mount the container's root filesystem as read only. + +**--restart**="*no*" + Restart policy to apply when a container exits (no, on-failure[:max-retry], always, unless-stopped). + +**--shm-size**="" + Size of `/dev/shm`. The format is ``. `number` must be greater than `0`. + Unit is optional and can be `b` (bytes), `k` (kilobytes), `m` (megabytes), or `g` (gigabytes). If you omit the unit, the system uses bytes. + If you omit the size entirely, the system uses `64m`. + +**--security-opt**=[] + Security Options + + "label:user:USER" : Set the label user for the container + "label:role:ROLE" : Set the label role for the container + "label:type:TYPE" : Set the label type for the container + "label:level:LEVEL" : Set the label level for the container + "label:disable" : Turn off label confinement for the container + "no-new-privileges" : Disable container processes from gaining additional privileges + "seccomp:unconfined" : Turn off seccomp confinement for the container + "seccomp:profile.json : White listed syscalls seccomp Json file to be used as a seccomp filter + +**--stop-signal**=*SIGTERM* + Signal to stop a container. Default is SIGTERM. + +**-t**, **--tty**=*true*|*false* + Allocate a pseudo-TTY. The default is *false*. + +**--tmpfs**=[] Create a tmpfs mount + + Mount a temporary filesystem (`tmpfs`) mount into a container, for example: + + $ docker run -d --tmpfs /tmp:rw,size=787448k,mode=1777 my_image + + This command mounts a `tmpfs` at `/tmp` within the container. The supported mount +options are the same as the Linux default `mount` flags. If you do not specify +any options, the systems uses the following options: +`rw,noexec,nosuid,nodev,size=65536k`. + +**-u**, **--user**="" + Username or UID + +**--ulimit**=[] + Ulimit options + +**--uts**=*host* + Set the UTS mode for the container + **host**: use the host's UTS namespace inside the container. + Note: the host mode gives the container access to changing the host's hostname and is therefore considered insecure. + +**-v**|**--volume**[=*[[HOST-DIR:]CONTAINER-DIR[:OPTIONS]]*] + Create a bind mount. If you specify, ` -v /HOST-DIR:/CONTAINER-DIR`, Docker + bind mounts `/HOST-DIR` in the host to `/CONTAINER-DIR` in the Docker + container. If 'HOST-DIR' is omitted, Docker automatically creates the new + volume on the host. The `OPTIONS` are a comma delimited list and can be: + + * [rw|ro] + * [z|Z] + * [`[r]shared`|`[r]slave`|`[r]private`] + +The `CONTAINER-DIR` must be an absolute path such as `/src/docs`. The `HOST-DIR` +can be an absolute path or a `name` value. A `name` value must start with an +alphanumeric character, followed by `a-z0-9`, `_` (underscore), `.` (period) or +`-` (hyphen). An absolute path starts with a `/` (forward slash). + +If you supply a `HOST-DIR` that is an absolute path, Docker bind-mounts to the +path you specify. If you supply a `name`, Docker creates a named volume by that +`name`. For example, you can specify either `/foo` or `foo` for a `HOST-DIR` +value. If you supply the `/foo` value, Docker creates a bind-mount. If you +supply the `foo` specification, Docker creates a named volume. + +You can specify multiple **-v** options to mount one or more mounts to a +container. To use these same mounts in other containers, specify the +**--volumes-from** option also. + +You can add `:ro` or `:rw` suffix to a volume to mount it read-only or +read-write mode, respectively. By default, the volumes are mounted read-write. +See examples. + +Labeling systems like SELinux require that proper labels are placed on volume +content mounted into a container. Without a label, the security system might +prevent the processes running inside the container from using the content. By +default, Docker does not change the labels set by the OS. + +To change a label in the container context, you can add either of two suffixes +`:z` or `:Z` to the volume mount. These suffixes tell Docker to relabel file +objects on the shared volumes. The `z` option tells Docker that two containers +share the volume content. As a result, Docker labels the content with a shared +content label. Shared volume labels allow all containers to read/write content. +The `Z` option tells Docker to label the content with a private unshared label. +Only the current container can use a private volume. + +By default bind mounted volumes are `private`. That means any mounts done +inside container will not be visible on host and vice-a-versa. One can change +this behavior by specifying a volume mount propagation property. Making a +volume `shared` mounts done under that volume inside container will be +visible on host and vice-a-versa. Making a volume `slave` enables only one +way mount propagation and that is mounts done on host under that volume +will be visible inside container but not the other way around. + +To control mount propagation property of volume one can use `:[r]shared`, +`:[r]slave` or `:[r]private` propagation flag. Propagation property can +be specified only for bind mounted volumes and not for internal volumes or +named volumes. For mount propagation to work source mount point (mount point +where source dir is mounted on) has to have right propagation properties. For +shared volumes, source mount point has to be shared. And for slave volumes, +source mount has to be either shared or slave. + +Use `df ` to figure out the source mount and then use +`findmnt -o TARGET,PROPAGATION ` to figure out propagation +properties of source mount. If `findmnt` utility is not available, then one +can look at mount entry for source mount point in `/proc/self/mountinfo`. Look +at `optional fields` and see if any propagaion properties are specified. +`shared:X` means mount is `shared`, `master:X` means mount is `slave` and if +nothing is there that means mount is `private`. + +To change propagation properties of a mount point use `mount` command. For +example, if one wants to bind mount source directory `/foo` one can do +`mount --bind /foo /foo` and `mount --make-private --make-shared /foo`. This +will convert /foo into a `shared` mount point. Alternatively one can directly +change propagation properties of source mount. Say `/` is source mount for +`/foo`, then use `mount --make-shared /` to convert `/` into a `shared` mount. + +> **Note**: +> When using systemd to manage the Docker daemon's start and stop, in the systemd +> unit file there is an option to control mount propagation for the Docker daemon +> itself, called `MountFlags`. The value of this setting may cause Docker to not +> see mount propagation changes made on the mount point. For example, if this value +> is `slave`, you may not be able to use the `shared` or `rshared` propagation on +> a volume. + + +To disable automatic copying of data from the container path to the volume, use +the `nocopy` flag. The `nocopy` flag can be set on bind mounts and named volumes. + +**--volume-driver**="" + Container's volume driver. This driver creates volumes specified either from + a Dockerfile's `VOLUME` instruction or from the `docker run -v` flag. + See **docker-volume-create(1)** for full details. + +**--volumes-from**=[] + Mount volumes from the specified container(s) + +**-w**, **--workdir**="" + Working directory inside the container + +# EXAMPLES + +## Specify isolation technology for container (--isolation) + +This option is useful in situations where you are running Docker containers on +Windows. The `--isolation=` option sets a container's isolation +technology. On Linux, the only supported is the `default` option which uses +Linux namespaces. On Microsoft Windows, you can specify these values: + +* `default`: Use the value specified by the Docker daemon's `--exec-opt` . If the `daemon` does not specify an isolation technology, Microsoft Windows uses `process` as its default value. +* `process`: Namespace isolation only. +* `hyperv`: Hyper-V hypervisor partition-based isolation. + +Specifying the `--isolation` flag without a value is the same as setting `--isolation="default"`. + +# HISTORY +August 2014, updated by Sven Dowideit +September 2014, updated by Sven Dowideit +November 2014, updated by Sven Dowideit diff --git a/man/docker-daemon.8.md b/man/docker-daemon.8.md new file mode 100644 index 00000000..2b95133b --- /dev/null +++ b/man/docker-daemon.8.md @@ -0,0 +1,568 @@ +% DOCKER(8) Docker User Manuals +% Shishir Mahajan +% SEPTEMBER 2015 +# NAME +docker-daemon - Enable daemon mode + +# SYNOPSIS +**docker daemon** +[**--api-cors-header**=[=*API-CORS-HEADER*]] +[**--authorization-plugin**[=*[]*]] +[**-b**|**--bridge**[=*BRIDGE*]] +[**--bip**[=*BIP*]] +[**--cgroup-parent**[=*[]*]] +[**--cluster-store**[=*[]*]] +[**--cluster-advertise**[=*[]*]] +[**--cluster-store-opt**[=*map[]*]] +[**--config-file**[=*/etc/docker/daemon.json*]] +[**--containerd**[=*SOCKET-PATH*]] +[**-D**|**--debug**] +[**--default-gateway**[=*DEFAULT-GATEWAY*]] +[**--default-gateway-v6**[=*DEFAULT-GATEWAY-V6*]] +[**--default-ulimit**[=*[]*]] +[**--disable-legacy-registry**] +[**--dns**[=*[]*]] +[**--dns-opt**[=*[]*]] +[**--dns-search**[=*[]*]] +[**--exec-opt**[=*[]*]] +[**--exec-root**[=*/var/run/docker*]] +[**--fixed-cidr**[=*FIXED-CIDR*]] +[**--fixed-cidr-v6**[=*FIXED-CIDR-V6*]] +[**-G**|**--group**[=*docker*]] +[**-g**|**--graph**[=*/var/lib/docker*]] +[**-H**|**--host**[=*[]*]] +[**--help**] +[**--icc**[=*true*]] +[**--insecure-registry**[=*[]*]] +[**--ip**[=*0.0.0.0*]] +[**--ip-forward**[=*true*]] +[**--ip-masq**[=*true*]] +[**--iptables**[=*true*]] +[**--ipv6**] +[**-l**|**--log-level**[=*info*]] +[**--label**[=*[]*]] +[**--log-driver**[=*json-file*]] +[**--log-opt**[=*map[]*]] +[**--mtu**[=*0*]] +[**-p**|**--pidfile**[=*/var/run/docker.pid*]] +[**--raw-logs**] +[**--registry-mirror**[=*[]*]] +[**-s**|**--storage-driver**[=*STORAGE-DRIVER*]] +[**--selinux-enabled**] +[**--storage-opt**[=*[]*]] +[**--tls**] +[**--tlscacert**[=*~/.docker/ca.pem*]] +[**--tlscert**[=*~/.docker/cert.pem*]] +[**--tlskey**[=*~/.docker/key.pem*]] +[**--tlsverify**] +[**--userland-proxy**[=*true*]] +[**--userns-remap**[=*default*]] + +# DESCRIPTION +**docker** has two distinct functions. It is used for starting the Docker +daemon and to run the CLI (i.e., to command the daemon to manage images, +containers etc.) So **docker** is both a server, as a daemon, and a client +to the daemon, through the CLI. + +To run the Docker daemon you can specify **docker daemon**. +You can check the daemon options using **docker daemon --help**. +Daemon options should be specified after the **daemon** keyword in the following +format. + +**docker daemon [OPTIONS]** + +# OPTIONS + +**--api-cors-header**="" + Set CORS headers in the remote API. Default is cors disabled. Give urls like "http://foo, http://bar, ...". Give "*" to allow all. + +**--authorization-plugin**="" + Set authorization plugins to load + +**-b**, **--bridge**="" + Attach containers to a pre\-existing network bridge; use 'none' to disable container networking + +**--bip**="" + Use the provided CIDR notation address for the dynamically created bridge (docker0); Mutually exclusive of \-b + +**--cgroup-parent**="" + Set parent cgroup for all containers. Default is "/docker" for fs cgroup driver and "system.slice" for systemd cgroup driver. + +**--cluster-store**="" + URL of the distributed storage backend + +**--cluster-advertise**="" + Specifies the 'host:port' or `interface:port` combination that this particular + daemon instance should use when advertising itself to the cluster. The daemon + is reached through this value. + +**--cluster-store-opt**="" + Specifies options for the Key/Value store. + +**--config-file**="/etc/docker/daemon.json" + Specifies the JSON file path to load the configuration from. + +**--containerd**="" + Path to containerd socket. + +**-D**, **--debug**=*true*|*false* + Enable debug mode. Default is false. + +**--default-gateway**="" + IPv4 address of the container default gateway; this address must be part of the bridge subnet (which is defined by \-b or \--bip) + +**--default-gateway-v6**="" + IPv6 address of the container default gateway + +**--default-ulimit**=[] + Set default ulimits for containers. + +**--disable-legacy-registry**=*true*|*false* + Do not contact legacy registries + +**--dns**="" + Force Docker to use specific DNS servers + +**--dns-opt**="" + DNS options to use. + +**--dns-search**=[] + DNS search domains to use. + +**--exec-opt**=[] + Set runtime execution options. See RUNTIME EXECUTION OPTIONS. + +**--exec-root**="" + Path to use as the root of the Docker execution state files. Default is `/var/run/docker`. + +**--fixed-cidr**="" + IPv4 subnet for fixed IPs (e.g., 10.20.0.0/16); this subnet must be nested in the bridge subnet (which is defined by \-b or \-\-bip) + +**--fixed-cidr-v6**="" + IPv6 subnet for global IPv6 addresses (e.g., 2a00:1450::/64) + +**-G**, **--group**="" + Group to assign the unix socket specified by -H when running in daemon mode. + use '' (the empty string) to disable setting of a group. Default is `docker`. + +**-g**, **--graph**="" + Path to use as the root of the Docker runtime. Default is `/var/lib/docker`. + +**-H**, **--host**=[*unix:///var/run/docker.sock*]: tcp://[host:port] to bind or +unix://[/path/to/socket] to use. + The socket(s) to bind to in daemon mode specified using one or more + tcp://host:port, unix:///path/to/socket, fd://* or fd://socketfd. + +**--help** + Print usage statement + +**--icc**=*true*|*false* + Allow unrestricted inter\-container and Docker daemon host communication. If disabled, containers can still be linked together using the **--link** option (see **docker-run(1)**). Default is true. + +**--insecure-registry**=[] + Enable insecure registry communication, i.e., enable un-encrypted and/or untrusted communication. + + List of insecure registries can contain an element with CIDR notation to specify a whole subnet. Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs. + + Enabling `--insecure-registry` is useful when running a local registry. However, because its use creates security vulnerabilities it should ONLY be enabled for testing purposes. For increased security, users should add their CA to their system's list of trusted CAs instead of using `--insecure-registry`. + +**--ip**="" + Default IP address to use when binding container ports. Default is `0.0.0.0`. + +**--ip-forward**=*true*|*false* + Enables IP forwarding on the Docker host. The default is `true`. This flag interacts with the IP forwarding setting on your host system's kernel. If your system has IP forwarding disabled, this setting enables it. If your system has IP forwarding enabled, setting this flag to `--ip-forward=false` has no effect. + + This setting will also enable IPv6 forwarding if you have both `--ip-forward=true` and `--fixed-cidr-v6` set. Note that this may reject Router Advertisements and interfere with the host's existing IPv6 configuration. For more information, please consult the documentation about "Advanced Networking - IPv6". + +**--ip-masq**=*true*|*false* + Enable IP masquerading for bridge's IP range. Default is true. + +**--iptables**=*true*|*false* + Enable Docker's addition of iptables rules. Default is true. + +**--ipv6**=*true*|*false* + Enable IPv6 support. Default is false. Docker will create an IPv6-enabled bridge with address fe80::1 which will allow you to create IPv6-enabled containers. Use together with `--fixed-cidr-v6` to provide globally routable IPv6 addresses. IPv6 forwarding will be enabled if not used with `--ip-forward=false`. This may collide with your host's current IPv6 settings. For more information please consult the documentation about "Advanced Networking - IPv6". + +**-l**, **--log-level**="*debug*|*info*|*warn*|*error*|*fatal*" + Set the logging level. Default is `info`. + +**--label**="[]" + Set key=value labels to the daemon (displayed in `docker info`) + +**--log-driver**="*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*splunk*|*etwlogs*|*gcplogs*|*none*" + Default driver for container logs. Default is `json-file`. + **Warning**: `docker logs` command works only for `json-file` logging driver. + +**--log-opt**=[] + Logging driver specific options. + +**--mtu**=*0* + Set the containers network mtu. Default is `0`. + +**-p**, **--pidfile**="" + Path to use for daemon PID file. Default is `/var/run/docker.pid` + +**--raw-logs** +Output daemon logs in full timestamp format without ANSI coloring. If this flag is not set, +the daemon outputs condensed, colorized logs if a terminal is detected, or full ("raw") +output otherwise. + +**--registry-mirror**=*://* + Prepend a registry mirror to be used for image pulls. May be specified multiple times. + +**-s**, **--storage-driver**="" + Force the Docker runtime to use a specific storage driver. + +**--selinux-enabled**=*true*|*false* + Enable selinux support. Default is false. SELinux does not presently support the overlay storage driver. + +**--storage-opt**=[] + Set storage driver options. See STORAGE DRIVER OPTIONS. + +**--tls**=*true*|*false* + Use TLS; implied by --tlsverify. Default is false. + +**--tlscacert**=*~/.docker/ca.pem* + Trust certs signed only by this CA. + +**--tlscert**=*~/.docker/cert.pem* + Path to TLS certificate file. + +**--tlskey**=*~/.docker/key.pem* + Path to TLS key file. + +**--tlsverify**=*true*|*false* + Use TLS and verify the remote (daemon: verify client, client: verify daemon). + Default is false. + +**--userland-proxy**=*true*|*false* + Rely on a userland proxy implementation for inter-container and outside-to-container loopback communications. Default is true. + +**--userns-remap**=*default*|*uid:gid*|*user:group*|*user*|*uid* + Enable user namespaces for containers on the daemon. Specifying "default" will cause a new user and group to be created to handle UID and GID range remapping for the user namespace mappings used for contained processes. Specifying a user (or uid) and optionally a group (or gid) will cause the daemon to lookup the user and group's subordinate ID ranges for use as the user namespace mappings for contained processes. + +# STORAGE DRIVER OPTIONS + +Docker uses storage backends (known as "graphdrivers" in the Docker +internals) to create writable containers from images. Many of these +backends use operating system level technologies and can be +configured. + +Specify options to the storage backend with **--storage-opt** flags. The +backends that currently take options are *devicemapper* and *zfs*. +Options for *devicemapper* are prefixed with *dm* and options for *zfs* +start with *zfs*. + +Specifically for devicemapper, the default is a "loopback" model which +requires no pre-configuration, but is extremely inefficient. Do not +use it in production. + +To make the best use of Docker with the devicemapper backend, you must +have a recent version of LVM. Use `lvm` to create a thin pool; for +more information see `man lvmthin`. Then, use `--storage-opt +dm.thinpooldev` to tell the Docker engine to use that pool for +allocating images and container snapshots. + +## Devicemapper options + +#### dm.thinpooldev + +Specifies a custom block storage device to use for the thin pool. + +If using a block device for device mapper storage, it is best to use `lvm` +to create and manage the thin-pool volume. This volume is then handed to Docker +to exclusively create snapshot volumes needed for images and containers. + +Managing the thin-pool outside of Engine makes for the most feature-rich +method of having Docker utilize device mapper thin provisioning as the +backing storage for Docker containers. The highlights of the lvm-based +thin-pool management feature include: automatic or interactive thin-pool +resize support, dynamically changing thin-pool features, automatic thinp +metadata checking when lvm activates the thin-pool, etc. + +As a fallback if no thin pool is provided, loopback files are +created. Loopback is very slow, but can be used without any +pre-configuration of storage. It is strongly recommended that you do +not use loopback in production. Ensure your Engine daemon has a +`--storage-opt dm.thinpooldev` argument provided. + +Example use: + + $ docker daemon \ + --storage-opt dm.thinpooldev=/dev/mapper/thin-pool + +#### dm.basesize + +Specifies the size to use when creating the base device, which limits +the size of images and containers. The default value is 10G. Note, +thin devices are inherently "sparse", so a 10G device which is mostly +empty doesn't use 10 GB of space on the pool. However, the filesystem +will use more space for base images the larger the device +is. + +The base device size can be increased at daemon restart which will allow +all future images and containers (based on those new images) to be of the +new base device size. + +Example use: `docker daemon --storage-opt dm.basesize=50G` + +This will increase the base device size to 50G. The Docker daemon will throw an +error if existing base device size is larger than 50G. A user can use +this option to expand the base device size however shrinking is not permitted. + +This value affects the system-wide "base" empty filesystem that may already +be initialized and inherited by pulled images. Typically, a change to this +value requires additional steps to take effect: + + $ sudo service docker stop + $ sudo rm -rf /var/lib/docker + $ sudo service docker start + +Example use: `docker daemon --storage-opt dm.basesize=20G` + +#### dm.fs + +Specifies the filesystem type to use for the base device. The +supported options are `ext4` and `xfs`. The default is `ext4`. + +Example use: `docker daemon --storage-opt dm.fs=xfs` + +#### dm.mkfsarg + +Specifies extra mkfs arguments to be used when creating the base device. + +Example use: `docker daemon --storage-opt "dm.mkfsarg=-O ^has_journal"` + +#### dm.mountopt + +Specifies extra mount options used when mounting the thin devices. + +Example use: `docker daemon --storage-opt dm.mountopt=nodiscard` + +#### dm.use_deferred_removal + +Enables use of deferred device removal if `libdm` and the kernel driver +support the mechanism. + +Deferred device removal means that if device is busy when devices are +being removed/deactivated, then a deferred removal is scheduled on +device. And devices automatically go away when last user of the device +exits. + +For example, when a container exits, its associated thin device is removed. If +that device has leaked into some other mount namespace and can't be removed, +the container exit still succeeds and this option causes the system to schedule +the device for deferred removal. It does not wait in a loop trying to remove a busy +device. + +Example use: `docker daemon --storage-opt dm.use_deferred_removal=true` + +#### dm.use_deferred_deletion + +Enables use of deferred device deletion for thin pool devices. By default, +thin pool device deletion is synchronous. Before a container is deleted, the +Docker daemon removes any associated devices. If the storage driver can not +remove a device, the container deletion fails and daemon returns. + +`Error deleting container: Error response from daemon: Cannot destroy container` + +To avoid this failure, enable both deferred device deletion and deferred +device removal on the daemon. + +`docker daemon --storage-opt dm.use_deferred_deletion=true --storage-opt dm.use_deferred_removal=true` + +With these two options enabled, if a device is busy when the driver is +deleting a container, the driver marks the device as deleted. Later, when the +device isn't in use, the driver deletes it. + +In general it should be safe to enable this option by default. It will help +when unintentional leaking of mount point happens across multiple mount +namespaces. + +#### dm.loopdatasize + +**Note**: This option configures devicemapper loopback, which should not be used in production. + +Specifies the size to use when creating the loopback file for the +"data" device which is used for the thin pool. The default size is +100G. The file is sparse, so it will not initially take up +this much space. + +Example use: `docker daemon --storage-opt dm.loopdatasize=200G` + +#### dm.loopmetadatasize + +**Note**: This option configures devicemapper loopback, which should not be used in production. + +Specifies the size to use when creating the loopback file for the +"metadata" device which is used for the thin pool. The default size +is 2G. The file is sparse, so it will not initially take up +this much space. + +Example use: `docker daemon --storage-opt dm.loopmetadatasize=4G` + +#### dm.datadev + +(Deprecated, use `dm.thinpooldev`) + +Specifies a custom blockdevice to use for data for a +Docker-managed thin pool. It is better to use `dm.thinpooldev` - see +the documentation for it above for discussion of the advantages. + +#### dm.metadatadev + +(Deprecated, use `dm.thinpooldev`) + +Specifies a custom blockdevice to use for metadata for a +Docker-managed thin pool. See `dm.datadev` for why this is +deprecated. + +#### dm.blocksize + +Specifies a custom blocksize to use for the thin pool. The default +blocksize is 64K. + +Example use: `docker daemon --storage-opt dm.blocksize=512K` + +#### dm.blkdiscard + +Enables or disables the use of `blkdiscard` when removing devicemapper +devices. This is disabled by default due to the additional latency, +but as a special case with loopback devices it will be enabled, in +order to re-sparsify the loopback file on image/container removal. + +Disabling this on loopback can lead to *much* faster container removal +times, but it also prevents the space used in `/var/lib/docker` directory +from being returned to the system for other use when containers are +removed. + +Example use: `docker daemon --storage-opt dm.blkdiscard=false` + +#### dm.override_udev_sync_check + +By default, the devicemapper backend attempts to synchronize with the +`udev` device manager for the Linux kernel. This option allows +disabling that synchronization, to continue even though the +configuration may be buggy. + +To view the `udev` sync support of a Docker daemon that is using the +`devicemapper` driver, run: + + $ docker info + [...] + Udev Sync Supported: true + [...] + +When `udev` sync support is `true`, then `devicemapper` and `udev` can +coordinate the activation and deactivation of devices for containers. + +When `udev` sync support is `false`, a race condition occurs between +the `devicemapper` and `udev` during create and cleanup. The race +condition results in errors and failures. (For information on these +failures, see +[docker#4036](https://github.com/docker/docker/issues/4036)) + +To allow the `docker` daemon to start, regardless of whether `udev` sync is +`false`, set `dm.override_udev_sync_check` to true: + + $ docker daemon --storage-opt dm.override_udev_sync_check=true + +When this value is `true`, the driver continues and simply warns you +the errors are happening. + +**Note**: The ideal is to pursue a `docker` daemon and environment +that does support synchronizing with `udev`. For further discussion on +this topic, see +[docker#4036](https://github.com/docker/docker/issues/4036). +Otherwise, set this flag for migrating existing Docker daemons to a +daemon with a supported environment. + +#### dm.min_free_space + +Specifies the min free space percent in a thin pool require for new device +creation to succeed. This check applies to both free data space as well +as free metadata space. Valid values are from 0% - 99%. Value 0% disables +free space checking logic. If user does not specify a value for this option, +the Engine uses a default value of 10%. + +Whenever a new a thin pool device is created (during `docker pull` or during +container creation), the Engine checks if the minimum free space is +available. If the space is unavailable, then device creation fails and any +relevant `docker` operation fails. + +To recover from this error, you must create more free space in the thin pool to +recover from the error. You can create free space by deleting some images +and containers from tge thin pool. You can also add +more storage to the thin pool. + +To add more space to an LVM (logical volume management) thin pool, just add +more storage to the group container thin pool; this should automatically +resolve any errors. If your configuration uses loop devices, then stop the +Engine daemon, grow the size of loop files and restart the daemon to resolve +the issue. + +Example use:: `docker daemon --storage-opt dm.min_free_space=10%` + +## ZFS options + +#### zfs.fsname + +Set zfs filesystem under which docker will create its own datasets. +By default docker will pick up the zfs filesystem where docker graph +(`/var/lib/docker`) is located. + +Example use: `docker daemon -s zfs --storage-opt zfs.fsname=zroot/docker` + +# CLUSTER STORE OPTIONS + +The daemon uses libkv to advertise +the node within the cluster. Some Key/Value backends support mutual +TLS, and the client TLS settings used by the daemon can be configured +using the **--cluster-store-opt** flag, specifying the paths to PEM encoded +files. + +#### kv.cacertfile + +Specifies the path to a local file with PEM encoded CA certificates to trust + +#### kv.certfile + +Specifies the path to a local file with a PEM encoded certificate. This +certificate is used as the client cert for communication with the +Key/Value store. + +#### kv.keyfile + +Specifies the path to a local file with a PEM encoded private key. This +private key is used as the client key for communication with the +Key/Value store. + +# Access authorization + +Docker's access authorization can be extended by authorization plugins that your +organization can purchase or build themselves. You can install one or more +authorization plugins when you start the Docker `daemon` using the +`--authorization-plugin=PLUGIN_ID` option. + +```bash +docker daemon --authorization-plugin=plugin1 --authorization-plugin=plugin2,... +``` + +The `PLUGIN_ID` value is either the plugin's name or a path to its specification +file. The plugin's implementation determines whether you can specify a name or +path. Consult with your Docker administrator to get information about the +plugins available to you. + +Once a plugin is installed, requests made to the `daemon` through the command +line or Docker's remote API are allowed or denied by the plugin. If you have +multiple plugins installed, at least one must allow the request for it to +complete. + +For information about how to create an authorization plugin, see [authorization +plugin](https://docs.docker.com/engine/extend/authorization/) section in the +Docker extend section of this documentation. + + +# HISTORY +Sept 2015, Originally compiled by Shishir Mahajan +based on docker.com source material and internal work. diff --git a/man/docker-diff.1.md b/man/docker-diff.1.md new file mode 100644 index 00000000..6c6c5025 --- /dev/null +++ b/man/docker-diff.1.md @@ -0,0 +1,49 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-diff - Inspect changes on a container's filesystem + +# SYNOPSIS +**docker diff** +[**--help**] +CONTAINER + +# DESCRIPTION +Inspect changes on a container's filesystem. You can use the full or +shortened container ID or the container name set using +**docker run --name** option. + +# OPTIONS +**--help** + Print usage statement + +# EXAMPLES +Inspect the changes to on a nginx container: + + # docker diff 1fdfd1f54c1b + C /dev + C /dev/console + C /dev/core + C /dev/stdout + C /dev/fd + C /dev/ptmx + C /dev/stderr + C /dev/stdin + C /run + A /run/nginx.pid + C /var/lib/nginx/tmp + A /var/lib/nginx/tmp/client_body + A /var/lib/nginx/tmp/fastcgi + A /var/lib/nginx/tmp/proxy + A /var/lib/nginx/tmp/scgi + A /var/lib/nginx/tmp/uwsgi + C /var/log/nginx + A /var/log/nginx/access.log + A /var/log/nginx/error.log + + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit diff --git a/man/docker-events.1.md b/man/docker-events.1.md new file mode 100644 index 00000000..4d0bff25 --- /dev/null +++ b/man/docker-events.1.md @@ -0,0 +1,96 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-events - Get real time events from the server + +# SYNOPSIS +**docker events** +[**--help**] +[**-f**|**--filter**[=*[]*]] +[**--since**[=*SINCE*]] +[**--until**[=*UNTIL*]] + + +# DESCRIPTION +Get event information from the Docker daemon. Information can include historical +information and real-time information. + +Docker containers will report the following events: + + attach, commit, copy, create, destroy, die, exec_create, exec_start, export, kill, oom, pause, rename, resize, restart, start, stop, top, unpause + +and Docker images will report: + + delete, import, pull, push, tag, untag + +# OPTIONS +**--help** + Print usage statement + +**-f**, **--filter**=[] + Provide filter values (i.e., 'event=stop') + +**--since**="" + Show all events created since timestamp + +**--until**="" + Stream events until this timestamp + +The `--since` and `--until` parameters can be Unix timestamps, date formatted +timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed +relative to the client machine’s time. If you do not provide the `--since` option, +the command returns only new and/or live events. Supported formats for date +formatted time stamps include RFC3339Nano, RFC3339, `2006-01-02T15:04:05`, +`2006-01-02T15:04:05.999999999`, `2006-01-02Z07:00`, and `2006-01-02`. The local +timezone on the client will be used if you do not provide either a `Z` or a +`+-00:00` timezone offset at the end of the timestamp. When providing Unix +timestamps enter seconds[.nanoseconds], where seconds is the number of seconds +that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap +seconds (aka Unix epoch or Unix time), and the optional .nanoseconds field is a +fraction of a second no more than nine digits long. + +# EXAMPLES + +## Listening for Docker events + +After running docker events a container 786d698004576 is started and stopped +(The container name has been shortened in the output below): + + # docker events + 2015-01-28T20:21:31.000000000-08:00 59211849bc10: (from whenry/testimage:latest) start + 2015-01-28T20:21:31.000000000-08:00 59211849bc10: (from whenry/testimage:latest) die + 2015-01-28T20:21:32.000000000-08:00 59211849bc10: (from whenry/testimage:latest) stop + +## Listening for events since a given date +Again the output container IDs have been shortened for the purposes of this document: + + # docker events --since '2015-01-28' + 2015-01-28T20:25:38.000000000-08:00 c21f6c22ba27: (from whenry/testimage:latest) create + 2015-01-28T20:25:38.000000000-08:00 c21f6c22ba27: (from whenry/testimage:latest) start + 2015-01-28T20:25:39.000000000-08:00 c21f6c22ba27: (from whenry/testimage:latest) create + 2015-01-28T20:25:39.000000000-08:00 c21f6c22ba27: (from whenry/testimage:latest) start + 2015-01-28T20:25:40.000000000-08:00 c21f6c22ba27: (from whenry/testimage:latest) die + 2015-01-28T20:25:42.000000000-08:00 c21f6c22ba27: (from whenry/testimage:latest) stop + 2015-01-28T20:25:45.000000000-08:00 c21f6c22ba27: (from whenry/testimage:latest) start + 2015-01-28T20:25:45.000000000-08:00 c21f6c22ba27: (from whenry/testimage:latest) die + 2015-01-28T20:25:46.000000000-08:00 c21f6c22ba27: (from whenry/testimage:latest) stop + +The following example outputs all events that were generated in the last 3 minutes, +relative to the current time on the client machine: + + # docker events --since '3m' + 2015-05-12T11:51:30.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) die + 2015-05-12T15:52:12.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) stop + 2015-05-12T15:53:45.999999999Z07:00 7805c1d35632: (from redis:2.8) die + 2015-05-12T15:54:03.999999999Z07:00 7805c1d35632: (from redis:2.8) stop + +If you do not provide the --since option, the command returns only new and/or +live events. + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit +June 2015, updated by Brian Goff +October 2015, updated by Mike Brown diff --git a/man/docker-exec.1.md b/man/docker-exec.1.md new file mode 100644 index 00000000..16a061d0 --- /dev/null +++ b/man/docker-exec.1.md @@ -0,0 +1,64 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-exec - Run a command in a running container + +# SYNOPSIS +**docker exec** +[**-d**|**--detach**] +[**--detach-keys**[=*[]*]] +[**--help**] +[**-i**|**--interactive**] +[**--privileged**] +[**-t**|**--tty**] +[**-u**|**--user**[=*USER*]] +CONTAINER COMMAND [ARG...] + +# DESCRIPTION + +Run a process in a running container. + +The command started using `docker exec` will only run while the container's primary +process (`PID 1`) is running, and will not be restarted if the container is restarted. + +If the container is paused, then the `docker exec` command will wait until the +container is unpaused, and then run + +# OPTIONS +**-d**, **--detach**=*true*|*false* + Detached mode: run command in the background. The default is *false*. + +**--detach-keys**="" + Override the key sequence for detaching a container. Format is a single character `[a-Z]` or `ctrl-` where `` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`. + +**--help** + Print usage statement + +**-i**, **--interactive**=*true*|*false* + Keep STDIN open even if not attached. The default is *false*. + +**--privileged**=*true*|*false* + Give the process extended [Linux capabilities](http://man7.org/linux/man-pages/man7/capabilities.7.html) +when running in a container. The default is *false*. + + Without this flag, the process run by `docker exec` in a running container has +the same capabilities as the container, which may be limited. Set +`--privileged` to give all capabilities to the process. + +**-t**, **--tty**=*true*|*false* + Allocate a pseudo-TTY. The default is *false*. + +**-u**, **--user**="" + Sets the username or UID used and optionally the groupname or GID for the specified command. + + The followings examples are all valid: + --user [user | user:group | uid | uid:gid | user:gid | uid:group ] + + Without this argument the command will be run as root in the container. + +The **-t** option is incompatible with a redirection of the docker client +standard input. + +# HISTORY +November 2014, updated by Sven Dowideit diff --git a/man/docker-export.1.md b/man/docker-export.1.md new file mode 100644 index 00000000..3d59e478 --- /dev/null +++ b/man/docker-export.1.md @@ -0,0 +1,46 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-export - Export the contents of a container's filesystem as a tar archive + +# SYNOPSIS +**docker export** +[**--help**] +[**-o**|**--output**[=*""*]] +CONTAINER + +# DESCRIPTION +Export the contents of a container's filesystem using the full or shortened +container ID or container name. The output is exported to STDOUT and can be +redirected to a tar file. + +Stream to a file instead of STDOUT by using **-o**. + +# OPTIONS +**--help** + Print usage statement + +**-o**, **--output**="" + Write to a file, instead of STDOUT + +# EXAMPLES +Export the contents of the container called angry_bell to a tar file +called angry_bell.tar: + + # docker export angry_bell > angry_bell.tar + # docker export --output=angry_bell-latest.tar angry_bell + # ls -sh angry_bell.tar + 321M angry_bell.tar + # ls -sh angry_bell-latest.tar + 321M angry_bell-latest.tar + +# See also +**docker-import(1)** to create an empty filesystem image +and import the contents of the tarball into it, then optionally tag it. + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit +January 2015, updated by Joseph Kern (josephakern at gmail dot com) diff --git a/man/docker-history.1.md b/man/docker-history.1.md new file mode 100644 index 00000000..91edefe2 --- /dev/null +++ b/man/docker-history.1.md @@ -0,0 +1,52 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-history - Show the history of an image + +# SYNOPSIS +**docker history** +[**--help**] +[**-H**|**--human**[=*true*]] +[**--no-trunc**] +[**-q**|**--quiet**] +IMAGE + +# DESCRIPTION + +Show the history of when and how an image was created. + +# OPTIONS +**--help** + Print usage statement + +**-H**, **--human**=*true*|*false* + Print sizes and dates in human readable format. The default is *true*. + +**--no-trunc**=*true*|*false* + Don't truncate output. The default is *false*. + +**-q**, **--quiet**=*true*|*false* + Only show numeric IDs. The default is *false*. + +# EXAMPLES + $ docker history fedora + IMAGE CREATED CREATED BY SIZE COMMENT + 105182bb5e8b 5 days ago /bin/sh -c #(nop) ADD file:71356d2ad59aa3119d 372.7 MB + 73bd853d2ea5 13 days ago /bin/sh -c #(nop) MAINTAINER Lokesh Mandvekar 0 B + 511136ea3c5a 10 months ago 0 B Imported from - + +## Display comments in the image history +The `docker commit` command has a **-m** flag for adding comments to the image. These comments will be displayed in the image history. + + $ sudo docker history docker:scm + IMAGE CREATED CREATED BY SIZE COMMENT + 2ac9d1098bf1 3 months ago /bin/bash 241.4 MB Added Apache to Fedora base image + 88b42ffd1f7c 5 months ago /bin/sh -c #(nop) ADD file:1fd8d7f9f6557cafc7 373.7 MB + c69cab00d6ef 5 months ago /bin/sh -c #(nop) MAINTAINER Lokesh Mandvekar 0 B + 511136ea3c5a 19 months ago 0 B Imported from - + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit diff --git a/man/docker-images.1.md b/man/docker-images.1.md new file mode 100644 index 00000000..8410280a --- /dev/null +++ b/man/docker-images.1.md @@ -0,0 +1,111 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-images - List images + +# SYNOPSIS +**docker images** +[**--help**] +[**-a**|**--all**] +[**--digests**] +[**-f**|**--filter**[=*[]*]] +[**--no-trunc**] +[**-q**|**--quiet**] +[REPOSITORY[:TAG]] + +# DESCRIPTION +This command lists the images stored in the local Docker repository. + +By default, intermediate images, used during builds, are not listed. Some of the +output, e.g., image ID, is truncated, for space reasons. However the truncated +image ID, and often the first few characters, are enough to be used in other +Docker commands that use the image ID. The output includes repository, tag, image +ID, date created and the virtual size. + +The title REPOSITORY for the first title may seem confusing. It is essentially +the image name. However, because you can tag a specific image, and multiple tags +(image instances) can be associated with a single name, the name is really a +repository for all tagged images of the same name. For example consider an image +called fedora. It may be tagged with 18, 19, or 20, etc. to manage different +versions. + +# OPTIONS +**-a**, **--all**=*true*|*false* + Show all images (by default filter out the intermediate image layers). The default is *false*. + +**--digests**=*true*|*false* + Show image digests. The default is *false*. + +**-f**, **--filter**=[] + Filters the output. The dangling=true filter finds unused images. While label=com.foo=amd64 filters for images with a com.foo value of amd64. The label=com.foo filter finds images with the label com.foo of any value. + +**--format**="*TEMPLATE*" + Pretty-print containers using a Go template. + Valid placeholders: + .ID - Image ID + .Repository - Image repository + .Tag - Image tag + .Digest - Image digest + .CreatedSince - Elapsed time since the image was created. + .CreatedAt - Time when the image was created.. + .Size - Image disk size. + +**--help** + Print usage statement + +**--no-trunc**=*true*|*false* + Don't truncate output. The default is *false*. + +**-q**, **--quiet**=*true*|*false* + Only show numeric IDs. The default is *false*. + +# EXAMPLES + +## Listing the images + +To list the images in a local repository (not the registry) run: + + docker images + +The list will contain the image repository name, a tag for the image, and an +image ID, when it was created and its virtual size. Columns: REPOSITORY, TAG, +IMAGE ID, CREATED, and SIZE. + +The `docker images` command takes an optional `[REPOSITORY[:TAG]]` argument +that restricts the list to images that match the argument. If you specify +`REPOSITORY`but no `TAG`, the `docker images` command lists all images in the +given repository. + + docker images java + +The `[REPOSITORY[:TAG]]` value must be an "exact match". This means that, for example, +`docker images jav` does not match the image `java`. + +If both `REPOSITORY` and `TAG` are provided, only images matching that +repository and tag are listed. To find all local images in the "java" +repository with tag "8" you can use: + + docker images java:8 + +To get a verbose list of images which contains all the intermediate images +used in builds use **-a**: + + docker images -a + +Previously, the docker images command supported the --tree and --dot arguments, +which displayed different visualizations of the image data. Docker core removed +this functionality in the 1.7 version. If you liked this functionality, you can +still find it in the third-party dockviz tool: https://github.com/justone/dockviz. + +## Listing only the shortened image IDs + +Listing just the shortened image IDs. This can be useful for some automated +tools. + + docker images -q + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit diff --git a/man/docker-import.1.md b/man/docker-import.1.md new file mode 100644 index 00000000..43d65efe --- /dev/null +++ b/man/docker-import.1.md @@ -0,0 +1,72 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-import - Create an empty filesystem image and import the contents of the tarball (.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz) into it, then optionally tag it. + +# SYNOPSIS +**docker import** +[**-c**|**--change**[=*[]*]] +[**-m**|**--message**[=*MESSAGE*]] +[**--help**] +file|URL|**-**[REPOSITORY[:TAG]] + +# OPTIONS +**-c**, **--change**=[] + Apply specified Dockerfile instructions while importing the image + Supported Dockerfile instructions: `CMD`|`ENTRYPOINT`|`ENV`|`EXPOSE`|`ONBUILD`|`USER`|`VOLUME`|`WORKDIR` + +**--help** + Print usage statement + +**-m**, **--message**="" + Set commit message for imported image + +# DESCRIPTION +Create a new filesystem image from the contents of a tarball (`.tar`, +`.tar.gz`, `.tgz`, `.bzip`, `.tar.xz`, `.txz`) into it, then optionally tag it. + + +# EXAMPLES + +## Import from a remote location + + # docker import http://example.com/exampleimage.tgz example/imagerepo + +## Import from a local file + +Import to docker via pipe and stdin: + + # cat exampleimage.tgz | docker import - example/imagelocal + +Import with a commit message. + + # cat exampleimage.tgz | docker import --message "New image imported from tarball" - exampleimagelocal:new + +Import to a Docker image from a local file. + + # docker import /path/to/exampleimage.tgz + + +## Import from a local file and tag + +Import to docker via pipe and stdin: + + # cat exampleimageV2.tgz | docker import - example/imagelocal:V-2.0 + +## Import from a local directory + + # tar -c . | docker import - exampleimagedir + +## Apply specified Dockerfile instructions while importing the image +This example sets the docker image ENV variable DEBUG to true by default. + + # tar -c . | docker import -c="ENV DEBUG true" - exampleimagedir + +# See also +**docker-export(1)** to export the contents of a filesystem as a tar archive to STDOUT. + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit diff --git a/man/docker-info.1.md b/man/docker-info.1.md new file mode 100644 index 00000000..1ebcd1a4 --- /dev/null +++ b/man/docker-info.1.md @@ -0,0 +1,66 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-info - Display system-wide information + +# SYNOPSIS +**docker info** +[**--help**] + + +# DESCRIPTION +This command displays system wide information regarding the Docker installation. +Information displayed includes the number of containers and images, pool name, +data file, metadata file, data space used, total data space, metadata space used +, total metadata space, execution driver, and the kernel version. + +The data file is where the images are stored and the metadata file is where the +meta data regarding those images are stored. When run for the first time Docker +allocates a certain amount of data space and meta data space from the space +available on the volume where `/var/lib/docker` is mounted. + +# OPTIONS +**--help** + Print usage statement + +# EXAMPLES + +## Display Docker system information + +Here is a sample output: + + # docker info + Containers: 14 + Running: 3 + Paused: 1 + Stopped: 10 + Images: 52 + Server Version: 1.9.0 + Storage Driver: aufs + Root Dir: /var/lib/docker/aufs + Dirs: 80 + Execution Driver: native-0.2 + Logging Driver: json-file + Cgroup Driver: cgroupfs + Plugins: + Volume: local + Network: bridge null host + Kernel Version: 3.13.0-24-generic + Operating System: Ubuntu 14.04 LTS + OSType: linux + Architecture: x86_64 + CPUs: 1 + Total Memory: 2 GiB + Name: docker + ID: I54V:OLXT:HVMM:TPKO:JPHQ:CQCD:JNLC:O3BZ:4ZVJ:43XJ:PFHZ:6N2S + Docker Root Dir: /var/lib/docker + Debug mode (client): false + Debug mode (server): false + Username: xyz + Registry: https://index.docker.io/v1/ + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit diff --git a/man/docker-inspect.1.md b/man/docker-inspect.1.md new file mode 100644 index 00000000..1bc2cf0b --- /dev/null +++ b/man/docker-inspect.1.md @@ -0,0 +1,322 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-inspect - Return low-level information on a container or image + +# SYNOPSIS +**docker inspect** +[**--help**] +[**-f**|**--format**[=*FORMAT*]] +[**-s**|**--size**] +[**--type**=*container*|*image*] +CONTAINER|IMAGE [CONTAINER|IMAGE...] + +# DESCRIPTION + +This displays all the information available in Docker for a given +container or image. By default, this will render all results in a JSON +array. If the container and image have the same name, this will return +container JSON for unspecified type. If a format is specified, the given +template will be executed for each result. + +# OPTIONS +**--help** + Print usage statement + +**-f**, **--format**="" + Format the output using the given Go template. + +**-s**, **--size** + Display total file sizes if the type is container. + +**--type**="*container*|*image*" + Return JSON for specified type, permissible values are "image" or "container" + +# EXAMPLES + +Get information about an image when image name conflicts with the container name, +e.g. both image and container are named rhel7: + + $ docker inspect --type=image rhel7 + [ + { + "Id": "fe01a428b9d9de35d29531e9994157978e8c48fa693e1bf1d221dffbbb67b170", + "Parent": "10acc31def5d6f249b548e01e8ffbaccfd61af0240c17315a7ad393d022c5ca2", + .... + } + ] + +## Getting information on a container + +To get information on a container use its ID or instance name: + + $ docker inspect d2cc496561d6 + [{ + "Id": "d2cc496561d6d520cbc0236b4ba88c362c446a7619992123f11c809cded25b47", + "Created": "2015-06-08T16:18:02.505155285Z", + "Path": "bash", + "Args": [], + "State": { + "Running": false, + "Paused": false, + "Restarting": false, + "OOMKilled": false, + "Dead": false, + "Pid": 0, + "ExitCode": 0, + "Error": "", + "StartedAt": "2015-06-08T16:18:03.643865954Z", + "FinishedAt": "2015-06-08T16:57:06.448552862Z" + }, + "Image": "ded7cd95e059788f2586a51c275a4f151653779d6a7f4dad77c2bd34601d94e4", + "NetworkSettings": { + "Bridge": "", + "SandboxID": "6b4851d1903e16dd6a567bd526553a86664361f31036eaaa2f8454d6f4611f6f", + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "Ports": {}, + "SandboxKey": "/var/run/docker/netns/6b4851d1903e", + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "7587b82f0dada3656fda26588aee72630c6fab1536d36e394b2bfbcf898c971d", + "Gateway": "172.17.0.1", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "MacAddress": "02:42:ac:12:00:02", + "Networks": { + "bridge": { + "NetworkID": "7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812", + "EndpointID": "7587b82f0dada3656fda26588aee72630c6fab1536d36e394b2bfbcf898c971d", + "Gateway": "172.17.0.1", + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "02:42:ac:12:00:02" + } + } + + }, + "ResolvConfPath": "/var/lib/docker/containers/d2cc496561d6d520cbc0236b4ba88c362c446a7619992123f11c809cded25b47/resolv.conf", + "HostnamePath": "/var/lib/docker/containers/d2cc496561d6d520cbc0236b4ba88c362c446a7619992123f11c809cded25b47/hostname", + "HostsPath": "/var/lib/docker/containers/d2cc496561d6d520cbc0236b4ba88c362c446a7619992123f11c809cded25b47/hosts", + "LogPath": "/var/lib/docker/containers/d2cc496561d6d520cbc0236b4ba88c362c446a7619992123f11c809cded25b47/d2cc496561d6d520cbc0236b4ba88c362c446a7619992123f11c809cded25b47-json.log", + "Name": "/adoring_wozniak", + "RestartCount": 0, + "Driver": "devicemapper", + "MountLabel": "", + "ProcessLabel": "", + "Mounts": [ + { + "Source": "/data", + "Destination": "/data", + "Mode": "ro,Z", + "RW": false + "Propagation": "" + } + ], + "AppArmorProfile": "", + "ExecIDs": null, + "HostConfig": { + "Binds": null, + "ContainerIDFile": "", + "Memory": 0, + "MemorySwap": 0, + "CpuShares": 0, + "CpuPeriod": 0, + "CpusetCpus": "", + "CpusetMems": "", + "CpuQuota": 0, + "BlkioWeight": 0, + "OomKillDisable": false, + "Privileged": false, + "PortBindings": {}, + "Links": null, + "PublishAllPorts": false, + "Dns": null, + "DnsSearch": null, + "DnsOptions": null, + "ExtraHosts": null, + "VolumesFrom": null, + "Devices": [], + "NetworkMode": "bridge", + "IpcMode": "", + "PidMode": "", + "UTSMode": "", + "CapAdd": null, + "CapDrop": null, + "RestartPolicy": { + "Name": "no", + "MaximumRetryCount": 0 + }, + "SecurityOpt": null, + "ReadonlyRootfs": false, + "Ulimits": null, + "LogConfig": { + "Type": "json-file", + "Config": {} + }, + "CgroupParent": "" + }, + "GraphDriver": { + "Name": "devicemapper", + "Data": { + "DeviceId": "5", + "DeviceName": "docker-253:1-2763198-d2cc496561d6d520cbc0236b4ba88c362c446a7619992123f11c809cded25b47", + "DeviceSize": "171798691840" + } + }, + "Config": { + "Hostname": "d2cc496561d6", + "Domainname": "", + "User": "", + "AttachStdin": true, + "AttachStdout": true, + "AttachStderr": true, + "ExposedPorts": null, + "Tty": true, + "OpenStdin": true, + "StdinOnce": true, + "Env": null, + "Cmd": [ + "bash" + ], + "Image": "fedora", + "Volumes": null, + "VolumeDriver": "", + "WorkingDir": "", + "Entrypoint": null, + "NetworkDisabled": false, + "MacAddress": "", + "OnBuild": null, + "Labels": {}, + "Memory": 0, + "MemorySwap": 0, + "CpuShares": 0, + "Cpuset": "", + "StopSignal": "SIGTERM" + } + } + ] +## Getting the IP address of a container instance + +To get the IP address of a container use: + + $ docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' d2cc496561d6 + 172.17.0.2 + +## Listing all port bindings + +One can loop over arrays and maps in the results to produce simple text +output: + + $ docker inspect --format='{{range $p, $conf := .NetworkSettings.Ports}} \ + {{$p}} -> {{(index $conf 0).HostPort}} {{end}}' d2cc496561d6 + 80/tcp -> 80 + +You can get more information about how to write a Go template from: +https://golang.org/pkg/text/template/. + +## Getting size information on an container + + $ docker inspect -s d2cc496561d6 + [ + { + .... + "SizeRw": 0, + "SizeRootFs": 972, + .... + } + ] + +## Getting information on an image + +Use an image's ID or name (e.g., repository/name[:tag]) to get information +about the image: + + $ docker inspect ded7cd95e059 + [{ + "Id": "ded7cd95e059788f2586a51c275a4f151653779d6a7f4dad77c2bd34601d94e4", + "Parent": "48ecf305d2cf7046c1f5f8fcbcd4994403173441d4a7f125b1bb0ceead9de731", + "Comment": "", + "Created": "2015-05-27T16:58:22.937503085Z", + "Container": "76cf7f67d83a7a047454b33007d03e32a8f474ad332c3a03c94537edd22b312b", + "ContainerConfig": { + "Hostname": "76cf7f67d83a", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "ExposedPorts": null, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "/bin/sh", + "-c", + "#(nop) ADD file:4be46382bcf2b095fcb9fe8334206b584eff60bb3fad8178cbd97697fcb2ea83 in /" + ], + "Image": "48ecf305d2cf7046c1f5f8fcbcd4994403173441d4a7f125b1bb0ceead9de731", + "Volumes": null, + "VolumeDriver": "", + "WorkingDir": "", + "Entrypoint": null, + "NetworkDisabled": false, + "MacAddress": "", + "OnBuild": null, + "Labels": {} + }, + "DockerVersion": "1.6.0", + "Author": "Lokesh Mandvekar \u003clsm5@fedoraproject.org\u003e", + "Config": { + "Hostname": "76cf7f67d83a", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "ExposedPorts": null, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": null, + "Image": "48ecf305d2cf7046c1f5f8fcbcd4994403173441d4a7f125b1bb0ceead9de731", + "Volumes": null, + "VolumeDriver": "", + "WorkingDir": "", + "Entrypoint": null, + "NetworkDisabled": false, + "MacAddress": "", + "OnBuild": null, + "Labels": {} + }, + "Architecture": "amd64", + "Os": "linux", + "Size": 186507296, + "VirtualSize": 186507296, + "GraphDriver": { + "Name": "devicemapper", + "Data": { + "DeviceId": "3", + "DeviceName": "docker-253:1-2763198-ded7cd95e059788f2586a51c275a4f151653779d6a7f4dad77c2bd34601d94e4", + "DeviceSize": "171798691840" + } + } + } + ] + +# HISTORY +April 2014, originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit +April 2015, updated by Qiang Huang +October 2015, updated by Sally O'Malley diff --git a/man/docker-kill.1.md b/man/docker-kill.1.md new file mode 100644 index 00000000..36cbdb90 --- /dev/null +++ b/man/docker-kill.1.md @@ -0,0 +1,28 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-kill - Kill a running container using SIGKILL or a specified signal + +# SYNOPSIS +**docker kill** +[**--help**] +[**-s**|**--signal**[=*"KILL"*]] +CONTAINER [CONTAINER...] + +# DESCRIPTION + +The main process inside each container specified will be sent SIGKILL, + or any signal specified with option --signal. + +# OPTIONS +**--help** + Print usage statement + +**-s**, **--signal**="*KILL*" + Signal to send to the container + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) + based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit diff --git a/man/docker-load.1.md b/man/docker-load.1.md new file mode 100644 index 00000000..c54fe607 --- /dev/null +++ b/man/docker-load.1.md @@ -0,0 +1,49 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-load - Load an image from a tar archive or STDIN + +# SYNOPSIS +**docker load** +[**--help**] +[**-i**|**--input**[=*INPUT*]] +[**-q**|**--quiet**] + +# DESCRIPTION + +Loads a tarred repository from a file or the standard input stream. +Restores both images and tags. + +# OPTIONS +**--help** + Print usage statement + +**-i**, **--input**="" + Read from a tar archive file, instead of STDIN. The tarball may be compressed with gzip, bzip, or xz. + +**-q**, **--quiet** + Suppress the load output. Without this option, a progress bar is displayed. + +# EXAMPLES + + $ docker images + REPOSITORY TAG IMAGE ID CREATED SIZE + busybox latest 769b9341d937 7 weeks ago 2.489 MB + $ docker load --input fedora.tar + $ docker images + REPOSITORY TAG IMAGE ID CREATED SIZE + busybox latest 769b9341d937 7 weeks ago 2.489 MB + fedora rawhide 0d20aec6529d 7 weeks ago 387 MB + fedora 20 58394af37342 7 weeks ago 385.5 MB + fedora heisenbug 58394af37342 7 weeks ago 385.5 MB + fedora latest 58394af37342 7 weeks ago 385.5 MB + +# See also +**docker-save(1)** to save one or more images to a tar archive (streamed to STDOUT by default). + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit +July 2015 update by Mary Anthony diff --git a/man/docker-login.1.md b/man/docker-login.1.md new file mode 100644 index 00000000..ea899376 --- /dev/null +++ b/man/docker-login.1.md @@ -0,0 +1,56 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-login - Log in to a Docker registry. + +# SYNOPSIS +**docker login** +[**--help**] +[**-p**|**--password**[=*PASSWORD*]] +[**-u**|**--username**[=*USERNAME*]] +[SERVER] + +# DESCRIPTION +Log in to a Docker Registry located on the specified +`SERVER`. You can specify a URL or a `hostname` for the `SERVER` value. If you +do not specify a `SERVER`, the command uses Docker's public registry located at +`https://registry-1.docker.io/` by default. To get a username/password for Docker's public registry, create an account on Docker Hub. + +`docker login` requires user to use `sudo` or be `root`, except when: + +1. connecting to a remote daemon, such as a `docker-machine` provisioned `docker engine`. +2. user is added to the `docker` group. This will impact the security of your system; the `docker` group is `root` equivalent. See [Docker Daemon Attack Surface](https://docs.docker.com/articles/security/#docker-daemon-attack-surface) for details. + +You can log into any public or private repository for which you have +credentials. When you log in, the command stores encoded credentials in +`$HOME/.docker/config.json` on Linux or `%USERPROFILE%/.docker/config.json` on Windows. + +> **Note**: When running `sudo docker login` credentials are saved in `/root/.docker/config.json`. +> + +# OPTIONS +**--help** + Print usage statement + +**-p**, **--password**="" + Password + +**-u**, **--username**="" + Username + +# EXAMPLES + +## Login to a registry on your localhost + + # docker login localhost:8080 + +# See also +**docker-logout(1)** to log out from a Docker registry. + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit +April 2015, updated by Mary Anthony for v2 +November 2015, updated by Sally O'Malley diff --git a/man/docker-logout.1.md b/man/docker-logout.1.md new file mode 100644 index 00000000..a8a4b7c3 --- /dev/null +++ b/man/docker-logout.1.md @@ -0,0 +1,32 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-logout - Log out from a Docker registry. + +# SYNOPSIS +**docker logout** +[SERVER] + +# DESCRIPTION +Log out of a Docker Registry located on the specified `SERVER`. You can +specify a URL or a `hostname` for the `SERVER` value. If you do not specify a +`SERVER`, the command attempts to log you out of Docker's public registry +located at `https://registry-1.docker.io/` by default. + +# OPTIONS +There are no available options. + +# EXAMPLES + +## Log out from a registry on your localhost + + # docker logout localhost:8080 + +# See also +**docker-login(1)** to log in to a Docker registry server. + +# HISTORY +June 2014, Originally compiled by Daniel, Dao Quang Minh (daniel at nitrous dot io) +July 2014, updated by Sven Dowideit +April 2015, updated by Mary Anthony for v2 diff --git a/man/docker-logs.1.md b/man/docker-logs.1.md new file mode 100644 index 00000000..f910b535 --- /dev/null +++ b/man/docker-logs.1.md @@ -0,0 +1,64 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-logs - Fetch the logs of a container + +# SYNOPSIS +**docker logs** +[**-f**|**--follow**] +[**--help**] +[**--since**[=*SINCE*]] +[**-t**|**--timestamps**] +[**--tail**[=*"all"*]] +CONTAINER + +# DESCRIPTION +The **docker logs** command batch-retrieves whatever logs are present for +a container at the time of execution. This does not guarantee execution +order when combined with a docker run (i.e., your run may not have generated +any logs at the time you execute docker logs). + +The **docker logs --follow** command combines commands **docker logs** and +**docker attach**. It will first return all logs from the beginning and +then continue streaming new output from the container’s stdout and stderr. + +**Warning**: This command works only for the **json-file** or **journald** +logging drivers. + +# OPTIONS +**--help** + Print usage statement + +**-f**, **--follow**=*true*|*false* + Follow log output. The default is *false*. + +**--since**="" + Show logs since timestamp + +**-t**, **--timestamps**=*true*|*false* + Show timestamps. The default is *false*. + +**--tail**="*all*" + Output the specified number of lines at the end of logs (defaults to all logs) + +The `--since` option can be Unix timestamps, date formatted timestamps, or Go +duration strings (e.g. `10m`, `1h30m`) computed relative to the client machine’s +time. Supported formats for date formatted time stamps include RFC3339Nano, +RFC3339, `2006-01-02T15:04:05`, `2006-01-02T15:04:05.999999999`, +`2006-01-02Z07:00`, and `2006-01-02`. The local timezone on the client will be +used if you do not provide either a `Z` or a `+-00:00` timezone offset at the +end of the timestamp. When providing Unix timestamps enter +seconds[.nanoseconds], where seconds is the number of seconds that have elapsed +since January 1, 1970 (midnight UTC/GMT), not counting leap seconds (aka Unix +epoch or Unix time), and the optional .nanoseconds field is a fraction of a +second no more than nine digits long. You can combine the `--since` option with +either or both of the `--follow` or `--tail` options. + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit +July 2014, updated by Sven Dowideit +April 2015, updated by Ahmet Alp Balkan +October 2015, updated by Mike Brown diff --git a/man/docker-network-connect.1.md b/man/docker-network-connect.1.md new file mode 100644 index 00000000..d6ee1593 --- /dev/null +++ b/man/docker-network-connect.1.md @@ -0,0 +1,69 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% OCT 2015 +# NAME +docker-network-connect - connect a container to a network + +# SYNOPSIS +**docker network connect** +[**--help**] +NETWORK CONTAINER + +# DESCRIPTION + +Connects a container to a network. You can connect a container by name +or by ID. Once connected, the container can communicate with other containers in +the same network. + +```bash +$ docker network connect multi-host-network container1 +``` + +You can also use the `docker run --net=` option to start a container and immediately connect it to a network. + +```bash +$ docker run -itd --net=multi-host-network --ip 172.20.88.22 --ip6 2001:db8::8822 busybox +``` + +You can pause, restart, and stop containers that are connected to a network. +Paused containers remain connected and can be revealed by a `network inspect`. +When the container is stopped, it does not appear on the network until you restart +it. + +If specified, the container's IP address(es) is reapplied when a stopped +container is restarted. If the IP address is no longer available, the container +fails to start. One way to guarantee that the IP address is available is +to specify an `--ip-range` when creating the network, and choose the static IP +address(es) from outside that range. This ensures that the IP address is not +given to another container while this container is not on the network. + +```bash +$ docker network create --subnet 172.20.0.0/16 --ip-range 172.20.240.0/20 multi-host-network +``` + +```bash +$ docker network connect --ip 172.20.128.2 multi-host-network container2 +``` + +To verify the container is connected, use the `docker network inspect` command. Use `docker network disconnect` to remove a container from the network. + +Once connected in network, containers can communicate using only another +container's IP address or name. For `overlay` networks or custom plugins that +support multi-host connectivity, containers connected to the same multi-host +network but launched from different Engines can also communicate in this way. + +You can connect a container to one or more networks. The networks need not be the same type. For example, you can connect a single container bridge and overlay networks. + + +# OPTIONS +**NETWORK** + Specify network name + +**CONTAINER** + Specify container name + +**--help** + Print usage statement + +# HISTORY +OCT 2015, created by Mary Anthony diff --git a/man/docker-network-create.1.md b/man/docker-network-create.1.md new file mode 100644 index 00000000..885a109e --- /dev/null +++ b/man/docker-network-create.1.md @@ -0,0 +1,170 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% OCT 2015 +# NAME +docker-network-create - create a new network + +# SYNOPSIS +**docker network create** +[**--aux-address**=*map[]*] +[**-d**|**--driver**=*DRIVER*] +[**--gateway**=*[]*] +[**--help**] +[**--internal**] +[**--ip-range**=*[]*] +[**--ipam-driver**=*default*] +[**--ipam-opt**=*map[]*] +[**--ipv6**] +[**--label**[=*[]*]] +[**-o**|**--opt**=*map[]*] +[**--subnet**=*[]*] +NETWORK-NAME + +# DESCRIPTION + +Creates a new network. The `DRIVER` accepts `bridge` or `overlay` which are the +built-in network drivers. If you have installed a third party or your own custom +network driver you can specify that `DRIVER` here also. If you don't specify the +`--driver` option, the command automatically creates a `bridge` network for you. +When you install Docker Engine it creates a `bridge` network automatically. This +network corresponds to the `docker0` bridge that Engine has traditionally relied +on. When launch a new container with `docker run` it automatically connects to +this bridge network. You cannot remove this default bridge network but you can +create new ones using the `network create` command. + +```bash +$ docker network create -d bridge my-bridge-network +``` + +Bridge networks are isolated networks on a single Engine installation. If you +want to create a network that spans multiple Docker hosts each running an +Engine, you must create an `overlay` network. Unlike `bridge` networks overlay +networks require some pre-existing conditions before you can create one. These +conditions are: + +* Access to a key-value store. Engine supports Consul, Etcd, and Zookeeper (Distributed store) key-value stores. +* A cluster of hosts with connectivity to the key-value store. +* A properly configured Engine `daemon` on each host in the cluster. + +The `docker daemon` options that support the `overlay` network are: + +* `--cluster-store` +* `--cluster-store-opt` +* `--cluster-advertise` + +To read more about these options and how to configure them, see ["*Get started +with multi-host +network*"](https://docs.docker.com/engine/userguide/networking/get-started-overlay/). + +It is also a good idea, though not required, that you install Docker Swarm on to +manage the cluster that makes up your network. Swarm provides sophisticated +discovery and server management that can assist your implementation. + +Once you have prepared the `overlay` network prerequisites you simply choose a +Docker host in the cluster and issue the following to create the network: + +```bash +$ docker network create -d overlay my-multihost-network +``` + +Network names must be unique. The Docker daemon attempts to identify naming +conflicts but this is not guaranteed. It is the user's responsibility to avoid +name conflicts. + +## Connect containers + +When you start a container use the `--net` flag to connect it to a network. +This adds the `busybox` container to the `mynet` network. + +```bash +$ docker run -itd --net=mynet busybox +``` + +If you want to add a container to a network after the container is already +running use the `docker network connect` subcommand. + +You can connect multiple containers to the same network. Once connected, the +containers can communicate using only another container's IP address or name. +For `overlay` networks or custom plugins that support multi-host connectivity, +containers connected to the same multi-host network but launched from different +Engines can also communicate in this way. + +You can disconnect a container from a network using the `docker network +disconnect` command. + +## Specifying advanced options + +When you create a network, Engine creates a non-overlapping subnetwork for the +network by default. This subnetwork is not a subdivision of an existing network. +It is purely for ip-addressing purposes. You can override this default and +specify subnetwork values directly using the `--subnet` option. On a +`bridge` network you can only create a single subnet: + +```bash +docker network create -d bridge --subnet=192.168.0.0/16 br0 +``` +Additionally, you also specify the `--gateway` `--ip-range` and `--aux-address` options. + +```bash +network create --driver=bridge --subnet=172.28.0.0/16 --ip-range=172.28.5.0/24 --gateway=172.28.5.254 br0 +``` + +If you omit the `--gateway` flag the Engine selects one for you from inside a +preferred pool. For `overlay` networks and for network driver plugins that +support it you can create multiple subnetworks. + +```bash +docker network create -d overlay + --subnet=192.168.0.0/16 --subnet=192.170.0.0/16 + --gateway=192.168.0.100 --gateway=192.170.0.100 + --ip-range=192.168.1.0/24 + --aux-address a=192.168.1.5 --aux-address b=192.168.1.6 + --aux-address a=192.170.1.5 --aux-address b=192.170.1.6 + my-multihost-network +``` +Be sure that your subnetworks do not overlap. If they do, the network create fails and Engine returns an error. + +### Network internal mode + +By default, when you connect a container to an `overlay` network, Docker also connects a bridge network to it to provide external connectivity. +If you want to create an externally isolated `overlay` network, you can specify the `--internal` option. + +# OPTIONS +**--aux-address**=map[] + Auxiliary ipv4 or ipv6 addresses used by network driver + +**-d**, **--driver**=*DRIVER* + Driver to manage the Network bridge or overlay. The default is bridge. + +**--gateway**=[] + ipv4 or ipv6 Gateway for the master subnet + +**--help** + Print usage + +**--internal** + Restricts external access to the network + +**--ip-range**=[] + Allocate container ip from a sub-range + +**--ipam-driver**=*default* + IP Address Management Driver + +**--ipam-opt**=map[] + Set custom IPAM driver options + +**--ipv6** + Enable IPv6 networking + +**--label**=*label* + Set metadata for a network + +**-o**, **--opt**=map[] + Set custom driver options + +**--subnet**=[] + Subnet in CIDR format that represents a network segment + +# HISTORY +OCT 2015, created by Mary Anthony diff --git a/man/docker-network-disconnect.1.md b/man/docker-network-disconnect.1.md new file mode 100644 index 00000000..09bcac51 --- /dev/null +++ b/man/docker-network-disconnect.1.md @@ -0,0 +1,36 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% OCT 2015 +# NAME +docker-network-disconnect - disconnect a container from a network + +# SYNOPSIS +**docker network disconnect** +[**--help**] +[**--force**] +NETWORK CONTAINER + +# DESCRIPTION + +Disconnects a container from a network. + +```bash + $ docker network disconnect multi-host-network container1 +``` + + +# OPTIONS +**NETWORK** + Specify network name + +**CONTAINER** + Specify container name + +**--force** + Force the container to disconnect from a network + +**--help** + Print usage statement + +# HISTORY +OCT 2015, created by Mary Anthony diff --git a/man/docker-network-inspect.1.md b/man/docker-network-inspect.1.md new file mode 100644 index 00000000..da4e7c35 --- /dev/null +++ b/man/docker-network-inspect.1.md @@ -0,0 +1,112 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% OCT 2015 +# NAME +docker-network-inspect - inspect a network + +# SYNOPSIS +**docker network inspect** +[**-f**|**--format**[=*FORMAT*]] +[**--help**] +NETWORK [NETWORK...] + +# DESCRIPTION + +Returns information about one or more networks. By default, this command renders all results in a JSON object. For example, if you connect two containers to the default `bridge` network: + +```bash +$ sudo docker run -itd --name=container1 busybox +f2870c98fd504370fb86e59f32cd0753b1ac9b69b7d80566ffc7192a82b3ed27 + +$ sudo docker run -itd --name=container2 busybox +bda12f8922785d1f160be70736f26c1e331ab8aaf8ed8d56728508f2e2fd4727 +``` + +The `network inspect` command shows the containers, by id, in its +results. You can specify an alternate format to execute a given +template for each result. Go's +[text/template](http://golang.org/pkg/text/template/) package +describes all the details of the format. + +```bash +$ sudo docker network inspect bridge +[ + { + "Name": "bridge", + "Id": "b2b1a2cba717161d984383fd68218cf70bbbd17d328496885f7c921333228b0f", + "Scope": "local", + "Driver": "bridge", + "IPAM": { + "Driver": "default", + "Config": [ + { + "Subnet": "172.17.42.1/16", + "Gateway": "172.17.42.1" + } + ] + }, + "Internal": false, + "Containers": { + "bda12f8922785d1f160be70736f26c1e331ab8aaf8ed8d56728508f2e2fd4727": { + "Name": "container2", + "EndpointID": "0aebb8fcd2b282abe1365979536f21ee4ceaf3ed56177c628eae9f706e00e019", + "MacAddress": "02:42:ac:11:00:02", + "IPv4Address": "172.17.0.2/16", + "IPv6Address": "" + }, + "f2870c98fd504370fb86e59f32cd0753b1ac9b69b7d80566ffc7192a82b3ed27": { + "Name": "container1", + "EndpointID": "a00676d9c91a96bbe5bcfb34f705387a33d7cc365bac1a29e4e9728df92d10ad", + "MacAddress": "02:42:ac:11:00:01", + "IPv4Address": "172.17.0.1/16", + "IPv6Address": "" + } + }, + "Options": { + "com.docker.network.bridge.default_bridge": "true", + "com.docker.network.bridge.enable_icc": "true", + "com.docker.network.bridge.enable_ip_masquerade": "true", + "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", + "com.docker.network.bridge.name": "docker0", + "com.docker.network.driver.mtu": "1500" + } + } +] +``` + +Returns the information about the user-defined network: + +```bash +$ docker network create simple-network +69568e6336d8c96bbf57869030919f7c69524f71183b44d80948bd3927c87f6a +$ docker network inspect simple-network +[ + { + "Name": "simple-network", + "Id": "69568e6336d8c96bbf57869030919f7c69524f71183b44d80948bd3927c87f6a", + "Scope": "local", + "Driver": "bridge", + "IPAM": { + "Driver": "default", + "Config": [ + { + "Subnet": "172.22.0.0/16", + "Gateway": "172.22.0.1/16" + } + ] + }, + "Containers": {}, + "Options": {} + } +] +``` + +# OPTIONS +**-f**, **--format**="" + Format the output using the given go template. + +**--help** + Print usage statement + +# HISTORY +OCT 2015, created by Mary Anthony diff --git a/man/docker-network-ls.1.md b/man/docker-network-ls.1.md new file mode 100644 index 00000000..56a8334a --- /dev/null +++ b/man/docker-network-ls.1.md @@ -0,0 +1,138 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% OCT 2015 +# NAME +docker-network-ls - list networks + +# SYNOPSIS +**docker network ls** +[**-f**|**--filter**[=*[]*]] +[**--no-trunc**[=*true*|*false*]] +[**-q**|**--quiet**[=*true*|*false*]] +[**--help**] + +# DESCRIPTION + +Lists all the networks the Engine `daemon` knows about. This includes the +networks that span across multiple hosts in a cluster, for example: + +```bash + $ docker network ls + NETWORK ID NAME DRIVER + 7fca4eb8c647 bridge bridge + 9f904ee27bf5 none null + cf03ee007fb4 host host + 78b03ee04fc4 multi-host overlay +``` + +Use the `--no-trunc` option to display the full network id: + +```bash +$ docker network ls --no-trunc +NETWORK ID NAME DRIVER +18a2866682b85619a026c81b98a5e375bd33e1b0936a26cc497c283d27bae9b3 none null +c288470c46f6c8949c5f7e5099b5b7947b07eabe8d9a27d79a9cbf111adcbf47 host host +7b369448dccbf865d397c8d2be0cda7cf7edc6b0945f77d2529912ae917a0185 bridge bridge +95e74588f40db048e86320c6526440c504650a1ff3e9f7d60a497c4d2163e5bd foo bridge +63d1ff1f77b07ca51070a8c227e962238358bd310bde1529cf62e6c307ade161 dev bridge +``` + +## Filtering + +The filtering flag (`-f` or `--filter`) format is a `key=value` pair. If there +is more than one filter, then pass multiple flags (e.g. `--filter "foo=bar" --filter "bif=baz"`). +Multiple filter flags are combined as an `OR` filter. For example, +`-f type=custom -f type=builtin` returns both `custom` and `builtin` networks. + +The currently supported filters are: + +* id (network's id) +* name (network's name) +* type (custom|builtin) + +#### Type + +The `type` filter supports two values; `builtin` displays predefined networks +(`bridge`, `none`, `host`), whereas `custom` displays user defined networks. + +The following filter matches all user defined networks: + +```bash +$ docker network ls --filter type=custom +NETWORK ID NAME DRIVER +95e74588f40d foo bridge +63d1ff1f77b0 dev bridge +``` + +By having this flag it allows for batch cleanup. For example, use this filter +to delete all user defined networks: + +```bash +$ docker network rm `docker network ls --filter type=custom -q` +``` + +A warning will be issued when trying to remove a network that has containers +attached. + +#### Name + +The `name` filter matches on all or part of a network's name. + +The following filter matches all networks with a name containing the `foobar` string. + +```bash +$ docker network ls --filter name=foobar +NETWORK ID NAME DRIVER +06e7eef0a170 foobar bridge +``` + +You can also filter for a substring in a name as this shows: + +```bash +$ docker network ls --filter name=foo +NETWORK ID NAME DRIVER +95e74588f40d foo bridge +06e7eef0a170 foobar bridge +``` + +#### ID + +The `id` filter matches on all or part of a network's ID. + +The following filter matches all networks with an ID containing the +`63d1ff1f77b0...` string. + +```bash +$ docker network ls --filter id=63d1ff1f77b07ca51070a8c227e962238358bd310bde1529cf62e6c307ade161 +NETWORK ID NAME DRIVER +63d1ff1f77b0 dev bridge +``` + +You can also filter for a substring in an ID as this shows: + +```bash +$ docker network ls --filter id=95e74588f40d +NETWORK ID NAME DRIVER +95e74588f40d foo bridge + +$ docker network ls --filter id=95e +NETWORK ID NAME DRIVER +95e74588f40d foo bridge +``` + +# OPTIONS + +**-f**, **--filter**=*[]* + filter output based on conditions provided. + +**--no-trunc**=*true*|*false* + Do not truncate the output + +**-q**, **--quiet**=*true*|*false* + Only display numeric IDs + +**--help** + Print usage statement + +# HISTORY +OCT 2015, created by Mary Anthony diff --git a/man/docker-network-rm.1.md b/man/docker-network-rm.1.md new file mode 100644 index 00000000..c094a152 --- /dev/null +++ b/man/docker-network-rm.1.md @@ -0,0 +1,43 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% OCT 2015 +# NAME +docker-network-rm - remove one or more networks + +# SYNOPSIS +**docker network rm** +[**--help**] +NETWORK [NETWORK...] + +# DESCRIPTION + +Removes one or more networks by name or identifier. To remove a network, +you must first disconnect any containers connected to it. +To remove the network named 'my-network': + +```bash + $ docker network rm my-network +``` + +To delete multiple networks in a single `docker network rm` command, provide +multiple network names or ids. The following example deletes a network with id +`3695c422697f` and a network named `my-network`: + +```bash + $ docker network rm 3695c422697f my-network +``` + +When you specify multiple networks, the command attempts to delete each in turn. +If the deletion of one network fails, the command continues to the next on the +list and tries to delete that. The command reports success or failure for each +deletion. + +# OPTIONS +**NETWORK** + Specify network name or id + +**--help** + Print usage statement + +# HISTORY +OCT 2015, created by Mary Anthony diff --git a/man/docker-pause.1.md b/man/docker-pause.1.md new file mode 100644 index 00000000..5d2267af --- /dev/null +++ b/man/docker-pause.1.md @@ -0,0 +1,30 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-pause - Pause all processes within a container + +# SYNOPSIS +**docker pause** +CONTAINER [CONTAINER...] + +# DESCRIPTION + +The `docker pause` command uses the cgroups freezer to suspend all processes in +a container. Traditionally when suspending a process the `SIGSTOP` signal is +used, which is observable by the process being suspended. With the cgroups freezer +the process is unaware, and unable to capture, that it is being suspended, +and subsequently resumed. + +See the [cgroups freezer documentation] +(https://www.kernel.org/doc/Documentation/cgroups/freezer-subsystem.txt) for +further details. + +# OPTIONS +There are no available options. + +# See also +**docker-unpause(1)** to unpause all processes within a container. + +# HISTORY +June 2014, updated by Sven Dowideit diff --git a/man/docker-port.1.md b/man/docker-port.1.md new file mode 100644 index 00000000..83e9cf93 --- /dev/null +++ b/man/docker-port.1.md @@ -0,0 +1,47 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-port - List port mappings for the CONTAINER, or lookup the public-facing port that is NAT-ed to the PRIVATE_PORT + +# SYNOPSIS +**docker port** +[**--help**] +CONTAINER [PRIVATE_PORT[/PROTO]] + +# DESCRIPTION +List port mappings for the CONTAINER, or lookup the public-facing port that is NAT-ed to the PRIVATE_PORT + +# OPTIONS +**--help** + Print usage statement + +# EXAMPLES + + # docker ps + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + b650456536c7 busybox:latest top 54 minutes ago Up 54 minutes 0.0.0.0:1234->9876/tcp, 0.0.0.0:4321->7890/tcp test + +## Find out all the ports mapped + + # docker port test + 7890/tcp -> 0.0.0.0:4321 + 9876/tcp -> 0.0.0.0:1234 + +## Find out a specific mapping + + # docker port test 7890/tcp + 0.0.0.0:4321 + + # docker port test 7890 + 0.0.0.0:4321 + +## An example showing error for non-existent mapping + + # docker port test 7890/udp + 2014/06/24 11:53:36 Error: No public port '7890/udp' published for test + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +June 2014, updated by Sven Dowideit +November 2014, updated by Sven Dowideit diff --git a/man/docker-ps.1.md b/man/docker-ps.1.md new file mode 100644 index 00000000..f5679664 --- /dev/null +++ b/man/docker-ps.1.md @@ -0,0 +1,141 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% FEBRUARY 2015 +# NAME +docker-ps - List containers + +# SYNOPSIS +**docker ps** +[**-a**|**--all**] +[**-f**|**--filter**[=*[]*]] +[**--format**=*"TEMPLATE"*] +[**--help**] +[**-l**|**--latest**] +[**-n**[=*-1*]] +[**--no-trunc**] +[**-q**|**--quiet**] +[**-s**|**--size**] + +# DESCRIPTION + +List the containers in the local repository. By default this shows only +the running containers. + +# OPTIONS +**-a**, **--all**=*true*|*false* + Show all containers. Only running containers are shown by default. The default is *false*. + +**-f**, **--filter**=[] + Filter output based on these conditions: + - exited= an exit code of + - label= or label== + - status=(created|restarting|running|paused|exited|dead) + - name= a container's name + - id= a container's ID + - before=(|) + - since=(|) + - ancestor=([:tag]||) - containers created from an image or a descendant. + - volume=(|) + +**--format**="*TEMPLATE*" + Pretty-print containers using a Go template. + Valid placeholders: + .ID - Container ID + .Image - Image ID + .Command - Quoted command + .CreatedAt - Time when the container was created. + .RunningFor - Elapsed time since the container was started. + .Ports - Exposed ports. + .Status - Container status. + .Size - Container disk size. + .Names - Container names. + .Labels - All labels assigned to the container. + .Label - Value of a specific label for this container. For example `{{.Label "com.docker.swarm.cpu"}}` + .Mounts - Names of the volumes mounted in this container. + +**--help** + Print usage statement + +**-l**, **--latest**=*true*|*false* + Show only the latest created container (includes all states). The default is *false*. + +**-n**=*-1* + Show n last created containers (includes all states). + +**--no-trunc**=*true*|*false* + Don't truncate output. The default is *false*. + +**-q**, **--quiet**=*true*|*false* + Only display numeric IDs. The default is *false*. + +**-s**, **--size**=*true*|*false* + Display total file sizes. The default is *false*. + +# EXAMPLES +# Display all containers, including non-running + + # docker ps -a + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + a87ecb4f327c fedora:20 /bin/sh -c #(nop) MA 20 minutes ago Exit 0 desperate_brattain + 01946d9d34d8 vpavlin/rhel7:latest /bin/sh -c #(nop) MA 33 minutes ago Exit 0 thirsty_bell + c1d3b0166030 acffc0358b9e /bin/sh -c yum -y up 2 weeks ago Exit 1 determined_torvalds + 41d50ecd2f57 fedora:20 /bin/sh -c #(nop) MA 2 weeks ago Exit 0 drunk_pike + +# Display only IDs of all containers, including non-running + + # docker ps -a -q + a87ecb4f327c + 01946d9d34d8 + c1d3b0166030 + 41d50ecd2f57 + +# Display only IDs of all containers that have the name `determined_torvalds` + + # docker ps -a -q --filter=name=determined_torvalds + c1d3b0166030 + +# Display containers with their commands + + # docker ps --format "{{.ID}}: {{.Command}}" + a87ecb4f327c: /bin/sh -c #(nop) MA + 01946d9d34d8: /bin/sh -c #(nop) MA + c1d3b0166030: /bin/sh -c yum -y up + 41d50ecd2f57: /bin/sh -c #(nop) MA + +# Display containers with their labels in a table + + # docker ps --format "table {{.ID}}\t{{.Labels}}" + CONTAINER ID LABELS + a87ecb4f327c com.docker.swarm.node=ubuntu,com.docker.swarm.storage=ssd + 01946d9d34d8 + c1d3b0166030 com.docker.swarm.node=debian,com.docker.swarm.cpu=6 + 41d50ecd2f57 com.docker.swarm.node=fedora,com.docker.swarm.cpu=3,com.docker.swarm.storage=ssd + +# Display containers with their node label in a table + + # docker ps --format 'table {{.ID}}\t{{(.Label "com.docker.swarm.node")}}' + CONTAINER ID NODE + a87ecb4f327c ubuntu + 01946d9d34d8 + c1d3b0166030 debian + 41d50ecd2f57 fedora + +# Display containers with `remote-volume` mounted + + $ docker ps --filter volume=remote-volume --format "table {{.ID}}\t{{.Mounts}}" + CONTAINER ID MOUNTS + 9c3527ed70ce remote-volume + +# Display containers with a volume mounted in `/data` + + $ docker ps --filter volume=/data --format "table {{.ID}}\t{{.Mounts}}" + CONTAINER ID MOUNTS + 9c3527ed70ce remote-volume + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit +August 2014, updated by Sven Dowideit +November 2014, updated by Sven Dowideit +February 2015, updated by André Martins diff --git a/man/docker-pull.1.md b/man/docker-pull.1.md new file mode 100644 index 00000000..c61d0053 --- /dev/null +++ b/man/docker-pull.1.md @@ -0,0 +1,220 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-pull - Pull an image or a repository from a registry + +# SYNOPSIS +**docker pull** +[**-a**|**--all-tags**] +[**--help**] +NAME[:TAG] | [REGISTRY_HOST[:REGISTRY_PORT]/]NAME[:TAG] + +# DESCRIPTION + +This command pulls down an image or a repository from a registry. If +there is more than one image for a repository (e.g., fedora) then all +images for that repository name can be pulled down including any tags +(see the option **-a** or **--all-tags**). + +If you do not specify a `REGISTRY_HOST`, the command uses Docker's public +registry located at `registry-1.docker.io` by default. + +# OPTIONS +**-a**, **--all-tags**=*true*|*false* + Download all tagged images in the repository. The default is *false*. + +**--help** + Print usage statement + +# EXAMPLES + +### Pull an image from Docker Hub + +To download a particular image, or set of images (i.e., a repository), use +`docker pull`. If no tag is provided, Docker Engine uses the `:latest` tag as a +default. This command pulls the `debian:latest` image: + + $ docker pull debian + + Using default tag: latest + latest: Pulling from library/debian + fdd5d7827f33: Pull complete + a3ed95caeb02: Pull complete + Digest: sha256:e7d38b3517548a1c71e41bffe9c8ae6d6d29546ce46bf62159837aad072c90aa + Status: Downloaded newer image for debian:latest + +Docker images can consist of multiple layers. In the example above, the image +consists of two layers; `fdd5d7827f33` and `a3ed95caeb02`. + +Layers can be reused by images. For example, the `debian:jessie` image shares +both layers with `debian:latest`. Pulling the `debian:jessie` image therefore +only pulls its metadata, but not its layers, because all layers are already +present locally: + + $ docker pull debian:jessie + + jessie: Pulling from library/debian + fdd5d7827f33: Already exists + a3ed95caeb02: Already exists + Digest: sha256:a9c958be96d7d40df920e7041608f2f017af81800ca5ad23e327bc402626b58e + Status: Downloaded newer image for debian:jessie + +To see which images are present locally, use the **docker-images(1)** +command: + + $ docker images + + REPOSITORY TAG IMAGE ID CREATED SIZE + debian jessie f50f9524513f 5 days ago 125.1 MB + debian latest f50f9524513f 5 days ago 125.1 MB + +Docker uses a content-addressable image store, and the image ID is a SHA256 +digest covering the image's configuration and layers. In the example above, +`debian:jessie` and `debian:latest` have the same image ID because they are +actually the *same* image tagged with different names. Because they are the +same image, their layers are stored only once and do not consume extra disk +space. + +For more information about images, layers, and the content-addressable store, +refer to [understand images, containers, and storage drivers](https://docs.docker.com/engine/userguide/storagedriver/imagesandcontainers/) +in the online documentation. + + +## Pull an image by digest (immutable identifier) + +So far, you've pulled images by their name (and "tag"). Using names and tags is +a convenient way to work with images. When using tags, you can `docker pull` an +image again to make sure you have the most up-to-date version of that image. +For example, `docker pull ubuntu:14.04` pulls the latest version of the Ubuntu +14.04 image. + +In some cases you don't want images to be updated to newer versions, but prefer +to use a fixed version of an image. Docker enables you to pull an image by its +*digest*. When pulling an image by digest, you specify *exactly* which version +of an image to pull. Doing so, allows you to "pin" an image to that version, +and guarantee that the image you're using is always the same. + +To know the digest of an image, pull the image first. Let's pull the latest +`ubuntu:14.04` image from Docker Hub: + + $ docker pull ubuntu:14.04 + + 14.04: Pulling from library/ubuntu + 5a132a7e7af1: Pull complete + fd2731e4c50c: Pull complete + 28a2f68d1120: Pull complete + a3ed95caeb02: Pull complete + Digest: sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2 + Status: Downloaded newer image for ubuntu:14.04 + +Docker prints the digest of the image after the pull has finished. In the example +above, the digest of the image is: + + sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2 + +Docker also prints the digest of an image when *pushing* to a registry. This +may be useful if you want to pin to a version of the image you just pushed. + +A digest takes the place of the tag when pulling an image, for example, to +pull the above image by digest, run the following command: + + $ docker pull ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2 + + sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2: Pulling from library/ubuntu + 5a132a7e7af1: Already exists + fd2731e4c50c: Already exists + 28a2f68d1120: Already exists + a3ed95caeb02: Already exists + Digest: sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2 + Status: Downloaded newer image for ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2 + +Digest can also be used in the `FROM` of a Dockerfile, for example: + + FROM ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2 + MAINTAINER some maintainer + +> **Note**: Using this feature "pins" an image to a specific version in time. +> Docker will therefore not pull updated versions of an image, which may include +> security updates. If you want to pull an updated image, you need to change the +> digest accordingly. + +## Pulling from a different registry + +By default, `docker pull` pulls images from Docker Hub. It is also possible to +manually specify the path of a registry to pull from. For example, if you have +set up a local registry, you can specify its path to pull from it. A registry +path is similar to a URL, but does not contain a protocol specifier (`https://`). + +The following command pulls the `testing/test-image` image from a local registry +listening on port 5000 (`myregistry.local:5000`): + + $ docker pull myregistry.local:5000/testing/test-image + +Registry credentials are managed by **docker-login(1)**. + +Docker uses the `https://` protocol to communicate with a registry, unless the +registry is allowed to be accessed over an insecure connection. Refer to the +[insecure registries](https://docs.docker.com/engine/reference/commandline/daemon/#insecure-registries) +section in the online documentation for more information. + + +## Pull a repository with multiple images + +By default, `docker pull` pulls a *single* image from the registry. A repository +can contain multiple images. To pull all images from a repository, provide the +`-a` (or `--all-tags`) option when using `docker pull`. + +This command pulls all images from the `fedora` repository: + + $ docker pull --all-tags fedora + + Pulling repository fedora + ad57ef8d78d7: Download complete + 105182bb5e8b: Download complete + 511136ea3c5a: Download complete + 73bd853d2ea5: Download complete + .... + + Status: Downloaded newer image for fedora + +After the pull has completed use the `docker images` command to see the +images that were pulled. The example below shows all the `fedora` images +that are present locally: + + $ docker images fedora + + REPOSITORY TAG IMAGE ID CREATED SIZE + fedora rawhide ad57ef8d78d7 5 days ago 359.3 MB + fedora 20 105182bb5e8b 5 days ago 372.7 MB + fedora heisenbug 105182bb5e8b 5 days ago 372.7 MB + fedora latest 105182bb5e8b 5 days ago 372.7 MB + + +## Canceling a pull + +Killing the `docker pull` process, for example by pressing `CTRL-c` while it is +running in a terminal, will terminate the pull operation. + + $ docker pull fedora + + Using default tag: latest + latest: Pulling from library/fedora + a3ed95caeb02: Pulling fs layer + 236608c7b546: Pulling fs layer + ^C + +> **Note**: Technically, the Engine terminates a pull operation when the +> connection between the Docker Engine daemon and the Docker Engine client +> initiating the pull is lost. If the connection with the Engine daemon is +> lost for other reasons than a manual interaction, the pull is also aborted. + + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit +August 2014, updated by Sven Dowideit +April 2015, updated by John Willis +April 2015, updated by Mary Anthony for v2 +September 2015, updated by Sally O'Malley diff --git a/man/docker-push.1.md b/man/docker-push.1.md new file mode 100644 index 00000000..1b487a0d --- /dev/null +++ b/man/docker-push.1.md @@ -0,0 +1,54 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-push - Push an image or a repository to a registry + +# SYNOPSIS +**docker push** +[**--help**] +NAME[:TAG] | [REGISTRY_HOST[:REGISTRY_PORT]/]NAME[:TAG] + +# DESCRIPTION + +This command pushes an image or a repository to a registry. If you do not +specify a `REGISTRY_HOST`, the command uses Docker's public registry located at +`registry-1.docker.io` by default. + +# OPTIONS +**--help** + Print usage statement + +# EXAMPLES + +# Pushing a new image to a registry + +First save the new image by finding the container ID (using **docker ps**) +and then committing it to a new image name. Note that only a-z0-9-_. are +allowed when naming images: + + # docker commit c16378f943fe rhel-httpd + +Now, push the image to the registry using the image ID. In this example the +registry is on host named `registry-host` and listening on port `5000`. To do +this, tag the image with the host name or IP address, and the port of the +registry: + + # docker tag rhel-httpd registry-host:5000/myadmin/rhel-httpd + # docker push registry-host:5000/myadmin/rhel-httpd + +Check that this worked by running: + + # docker images + +You should see both `rhel-httpd` and `registry-host:5000/myadmin/rhel-httpd` +listed. + +Registry credentials are managed by **docker-login(1)**. + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit +April 2015, updated by Mary Anthony for v2 +June 2015, updated by Sally O'Malley diff --git a/man/docker-rename.1.md b/man/docker-rename.1.md new file mode 100644 index 00000000..aa19a03a --- /dev/null +++ b/man/docker-rename.1.md @@ -0,0 +1,15 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% OCTOBER 2014 +# NAME +docker-rename - Rename a container + +# SYNOPSIS +**docker rename** +OLD_NAME NEW_NAME + +# OPTIONS +There are no available options. + +# DESCRIPTION +Rename a container. Container may be running, paused or stopped. diff --git a/man/docker-restart.1.md b/man/docker-restart.1.md new file mode 100644 index 00000000..e0ffb32d --- /dev/null +++ b/man/docker-restart.1.md @@ -0,0 +1,26 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-restart - Restart a container + +# SYNOPSIS +**docker restart** +[**--help**] +[**-t**|**--time**[=*10*]] +CONTAINER [CONTAINER...] + +# DESCRIPTION +Restart each container listed. + +# OPTIONS +**--help** + Print usage statement + +**-t**, **--time**=*10* + Number of seconds to try to stop for before killing the container. Once killed it will then be restarted. Default is 10 seconds. + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit diff --git a/man/docker-rm.1.md b/man/docker-rm.1.md new file mode 100644 index 00000000..2105288d --- /dev/null +++ b/man/docker-rm.1.md @@ -0,0 +1,72 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-rm - Remove one or more containers + +# SYNOPSIS +**docker rm** +[**-f**|**--force**] +[**-l**|**--link**] +[**-v**|**--volumes**] +CONTAINER [CONTAINER...] + +# DESCRIPTION + +**docker rm** will remove one or more containers from the host node. The +container name or ID can be used. This does not remove images. You cannot +remove a running container unless you use the **-f** option. To see all +containers on a host use the **docker ps -a** command. + +# OPTIONS +**--help** + Print usage statement + +**-f**, **--force**=*true*|*false* + Force the removal of a running container (uses SIGKILL). The default is *false*. + +**-l**, **--link**=*true*|*false* + Remove the specified link and not the underlying container. The default is *false*. + +**-v**, **--volumes**=*true*|*false* + Remove the volumes associated with the container. The default is *false*. + +# EXAMPLES + +## Removing a container using its ID + +To remove a container using its ID, find either from a **docker ps -a** +command, or use the ID returned from the **docker run** command, or retrieve +it from a file used to store it using the **docker run --cidfile**: + + docker rm abebf7571666 + +## Removing a container using the container name + +The name of the container can be found using the **docker ps -a** +command. The use that name as follows: + + docker rm hopeful_morse + +## Removing a container and all associated volumes + + $ docker rm -v redis + redis + +This command will remove the container and any volumes associated with it. +Note that if a volume was specified with a name, it will not be removed. + + $ docker create -v awesome:/foo -v /bar --name hello redis + hello + $ docker rm -v hello + +In this example, the volume for `/foo` will remain in tact, but the volume for +`/bar` will be removed. The same behavior holds for volumes inherited with +`--volumes-from`. + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit +July 2014, updated by Sven Dowideit +August 2014, updated by Sven Dowideit diff --git a/man/docker-rmi.1.md b/man/docker-rmi.1.md new file mode 100644 index 00000000..35bf8aac --- /dev/null +++ b/man/docker-rmi.1.md @@ -0,0 +1,42 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-rmi - Remove one or more images + +# SYNOPSIS +**docker rmi** +[**-f**|**--force**] +[**--help**] +[**--no-prune**] +IMAGE [IMAGE...] + +# DESCRIPTION + +Removes one or more images from the host node. This does not remove images from +a registry. You cannot remove an image of a running container unless you use the +**-f** option. To see all images on a host use the **docker images** command. + +# OPTIONS +**-f**, **--force**=*true*|*false* + Force removal of the image. The default is *false*. + +**--help** + Print usage statement + +**--no-prune**=*true*|*false* + Do not delete untagged parents. The default is *false*. + +# EXAMPLES + +## Removing an image + +Here is an example of removing an image: + + docker rmi fedora/httpd + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit +April 2015, updated by Mary Anthony for v2 diff --git a/man/docker-run.1.md b/man/docker-run.1.md new file mode 100644 index 00000000..57808f33 --- /dev/null +++ b/man/docker-run.1.md @@ -0,0 +1,955 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-run - Run a command in a new container + +# SYNOPSIS +**docker run** +[**-a**|**--attach**[=*[]*]] +[**--add-host**[=*[]*]] +[**--blkio-weight**[=*[BLKIO-WEIGHT]*]] +[**--blkio-weight-device**[=*[]*]] +[**--cpu-shares**[=*0*]] +[**--cap-add**[=*[]*]] +[**--cap-drop**[=*[]*]] +[**--cgroup-parent**[=*CGROUP-PATH*]] +[**--cidfile**[=*CIDFILE*]] +[**--cpu-period**[=*0*]] +[**--cpu-quota**[=*0*]] +[**--cpuset-cpus**[=*CPUSET-CPUS*]] +[**--cpuset-mems**[=*CPUSET-MEMS*]] +[**-d**|**--detach**] +[**--detach-keys**[=*[]*]] +[**--device**[=*[]*]] +[**--device-read-bps**[=*[]*]] +[**--device-read-iops**[=*[]*]] +[**--device-write-bps**[=*[]*]] +[**--device-write-iops**[=*[]*]] +[**--dns**[=*[]*]] +[**--dns-opt**[=*[]*]] +[**--dns-search**[=*[]*]] +[**-e**|**--env**[=*[]*]] +[**--entrypoint**[=*ENTRYPOINT*]] +[**--env-file**[=*[]*]] +[**--expose**[=*[]*]] +[**--group-add**[=*[]*]] +[**-h**|**--hostname**[=*HOSTNAME*]] +[**--help**] +[**-i**|**--interactive**] +[**--ip**[=*IPv4-ADDRESS*]] +[**--ip6**[=*IPv6-ADDRESS*]] +[**--ipc**[=*IPC*]] +[**--isolation**[=*default*]] +[**--kernel-memory**[=*KERNEL-MEMORY*]] +[**-l**|**--label**[=*[]*]] +[**--label-file**[=*[]*]] +[**--link**[=*[]*]] +[**--log-driver**[=*[]*]] +[**--log-opt**[=*[]*]] +[**-m**|**--memory**[=*MEMORY*]] +[**--mac-address**[=*MAC-ADDRESS*]] +[**--memory-reservation**[=*MEMORY-RESERVATION*]] +[**--memory-swap**[=*LIMIT*]] +[**--memory-swappiness**[=*MEMORY-SWAPPINESS*]] +[**--name**[=*NAME*]] +[**--net**[=*"bridge"*]] +[**--net-alias**[=*[]*]] +[**--oom-kill-disable**] +[**--oom-score-adj**[=*0*]] +[**-P**|**--publish-all**] +[**-p**|**--publish**[=*[]*]] +[**--pid**[=*[]*]] +[**--userns**[=*[]*]] +[**--pids-limit**[=*PIDS_LIMIT*]] +[**--privileged**] +[**--read-only**] +[**--restart**[=*RESTART*]] +[**--rm**] +[**--security-opt**[=*[]*]] +[**--stop-signal**[=*SIGNAL*]] +[**--shm-size**[=*[]*]] +[**--sig-proxy**[=*true*]] +[**-t**|**--tty**] +[**--tmpfs**[=*[CONTAINER-DIR[:]*]] +[**-u**|**--user**[=*USER*]] +[**--ulimit**[=*[]*]] +[**--uts**[=*[]*]] +[**-v**|**--volume**[=*[[HOST-DIR:]CONTAINER-DIR[:OPTIONS]]*]] +[**--volume-driver**[=*DRIVER*]] +[**--volumes-from**[=*[]*]] +[**-w**|**--workdir**[=*WORKDIR*]] +IMAGE [COMMAND] [ARG...] + +# DESCRIPTION + +Run a process in a new container. **docker run** starts a process with its own +file system, its own networking, and its own isolated process tree. The IMAGE +which starts the process may define defaults related to the process that will be +run in the container, the networking to expose, and more, but **docker run** +gives final control to the operator or administrator who starts the container +from the image. For that reason **docker run** has more options than any other +Docker command. + +If the IMAGE is not already loaded then **docker run** will pull the IMAGE, and +all image dependencies, from the repository in the same way running **docker +pull** IMAGE, before it starts the container from that image. + +# OPTIONS +**-a**, **--attach**=[] + Attach to STDIN, STDOUT or STDERR. + + In foreground mode (the default when **-d** +is not specified), **docker run** can start the process in the container +and attach the console to the process’s standard input, output, and standard +error. It can even pretend to be a TTY (this is what most commandline +executables expect) and pass along signals. The **-a** option can be set for +each of stdin, stdout, and stderr. + +**--add-host**=[] + Add a custom host-to-IP mapping (host:ip) + + Add a line to /etc/hosts. The format is hostname:ip. The **--add-host** +option can be set multiple times. + +**--blkio-weight**=*0* + Block IO weight (relative weight) accepts a weight value between 10 and 1000. + +**--blkio-weight-device**=[] + Block IO weight (relative device weight, format: `DEVICE_NAME:WEIGHT`). + +**--cpu-shares**=*0* + CPU shares (relative weight) + + By default, all containers get the same proportion of CPU cycles. This proportion +can be modified by changing the container's CPU share weighting relative +to the weighting of all other running containers. + +To modify the proportion from the default of 1024, use the **--cpu-shares** +flag to set the weighting to 2 or higher. + +The proportion will only apply when CPU-intensive processes are running. +When tasks in one container are idle, other containers can use the +left-over CPU time. The actual amount of CPU time will vary depending on +the number of containers running on the system. + +For example, consider three containers, one has a cpu-share of 1024 and +two others have a cpu-share setting of 512. When processes in all three +containers attempt to use 100% of CPU, the first container would receive +50% of the total CPU time. If you add a fourth container with a cpu-share +of 1024, the first container only gets 33% of the CPU. The remaining containers +receive 16.5%, 16.5% and 33% of the CPU. + +On a multi-core system, the shares of CPU time are distributed over all CPU +cores. Even if a container is limited to less than 100% of CPU time, it can +use 100% of each individual CPU core. + +For example, consider a system with more than three cores. If you start one +container **{C0}** with **-c=512** running one process, and another container +**{C1}** with **-c=1024** running two processes, this can result in the following +division of CPU shares: + + PID container CPU CPU share + 100 {C0} 0 100% of CPU0 + 101 {C1} 1 100% of CPU1 + 102 {C1} 2 100% of CPU2 + +**--cap-add**=[] + Add Linux capabilities + +**--cap-drop**=[] + Drop Linux capabilities + +**--cgroup-parent**="" + Path to cgroups under which the cgroup for the container will be created. If the path is not absolute, the path is considered to be relative to the cgroups path of the init process. Cgroups will be created if they do not already exist. + +**--cidfile**="" + Write the container ID to the file + +**--cpu-period**=*0* + Limit the CPU CFS (Completely Fair Scheduler) period + + Limit the container's CPU usage. This flag tell the kernel to restrict the container's CPU usage to the period you specify. + +**--cpuset-cpus**="" + CPUs in which to allow execution (0-3, 0,1) + +**--cpuset-mems**="" + Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems. + + If you have four memory nodes on your system (0-3), use `--cpuset-mems=0,1` +then processes in your Docker container will only use memory from the first +two memory nodes. + +**--cpu-quota**=*0* + Limit the CPU CFS (Completely Fair Scheduler) quota + + Limit the container's CPU usage. By default, containers run with the full +CPU resource. This flag tell the kernel to restrict the container's CPU usage +to the quota you specify. + +**-d**, **--detach**=*true*|*false* + Detached mode: run the container in the background and print the new container ID. The default is *false*. + + At any time you can run **docker ps** in +the other shell to view a list of the running containers. You can reattach to a +detached container with **docker attach**. If you choose to run a container in +the detached mode, then you cannot use the **-rm** option. + + When attached in the tty mode, you can detach from the container (and leave it +running) using a configurable key sequence. The default sequence is `CTRL-p CTRL-q`. +You configure the key sequence using the **--detach-keys** option or a configuration file. +See **config-json(5)** for documentation on using a configuration file. + +**--detach-keys**="" + Override the key sequence for detaching a container. Format is a single character `[a-Z]` or `ctrl-` where `` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`. + +**--device**=[] + Add a host device to the container (e.g. --device=/dev/sdc:/dev/xvdc:rwm) + +**--device-read-bps**=[] + Limit read rate from a device (e.g. --device-read-bps=/dev/sda:1mb) + +**--device-read-iops**=[] + Limit read rate from a device (e.g. --device-read-iops=/dev/sda:1000) + +**--device-write-bps**=[] + Limit write rate to a device (e.g. --device-write-bps=/dev/sda:1mb) + +**--device-write-iops**=[] + Limit write rate a a device (e.g. --device-write-iops=/dev/sda:1000) + +**--dns-search**=[] + Set custom DNS search domains (Use --dns-search=. if you don't wish to set the search domain) + +**--dns-opt**=[] + Set custom DNS options + +**--dns**=[] + Set custom DNS servers + + This option can be used to override the DNS +configuration passed to the container. Typically this is necessary when the +host DNS configuration is invalid for the container (e.g., 127.0.0.1). When this +is the case the **--dns** flags is necessary for every run. + +**-e**, **--env**=[] + Set environment variables + + This option allows you to specify arbitrary +environment variables that are available for the process that will be launched +inside of the container. + +**--entrypoint**="" + Overwrite the default ENTRYPOINT of the image + + This option allows you to overwrite the default entrypoint of the image that +is set in the Dockerfile. The ENTRYPOINT of an image is similar to a COMMAND +because it specifies what executable to run when the container starts, but it is +(purposely) more difficult to override. The ENTRYPOINT gives a container its +default nature or behavior, so that when you set an ENTRYPOINT you can run the +container as if it were that binary, complete with default options, and you can +pass in more options via the COMMAND. But, sometimes an operator may want to run +something else inside the container, so you can override the default ENTRYPOINT +at runtime by using a **--entrypoint** and a string to specify the new +ENTRYPOINT. + +**--env-file**=[] + Read in a line delimited file of environment variables + +**--expose**=[] + Expose a port, or a range of ports (e.g. --expose=3300-3310) informs Docker +that the container listens on the specified network ports at runtime. Docker +uses this information to interconnect containers using links and to set up port +redirection on the host system. + +**--group-add**=[] + Add additional groups to run as + +**-h**, **--hostname**="" + Container host name + + Sets the container host name that is available inside the container. + +**--help** + Print usage statement + +**-i**, **--interactive**=*true*|*false* + Keep STDIN open even if not attached. The default is *false*. + + When set to true, keep stdin open even if not attached. The default is false. + +**--ip**="" + Sets the container's interface IPv4 address (e.g. 172.23.0.9) + + It can only be used in conjunction with **--net** for user-defined networks + +**--ip6**="" + Sets the container's interface IPv6 address (e.g. 2001:db8::1b99) + + It can only be used in conjunction with **--net** for user-defined networks + +**--ipc**="" + Default is to create a private IPC namespace (POSIX SysV IPC) for the container + 'container:': reuses another container shared memory, semaphores and message queues + 'host': use the host shared memory,semaphores and message queues inside the container. Note: the host mode gives the container full access to local shared memory and is therefore considered insecure. + +**--isolation**="*default*" + Isolation specifies the type of isolation technology used by containers. + +**-l**, **--label**=[] + Set metadata on the container (e.g., --label com.example.key=value) + +**--kernel-memory**="" + Kernel memory limit (format: `[]`, where unit = b, k, m or g) + + Constrains the kernel memory available to a container. If a limit of 0 +is specified (not using `--kernel-memory`), the container's kernel memory +is not limited. If you specify a limit, it may be rounded up to a multiple +of the operating system's page size and the value can be very large, +millions of trillions. + +**--label-file**=[] + Read in a line delimited file of labels + +**--link**=[] + Add link to another container in the form of :alias or just +in which case the alias will match the name + + If the operator +uses **--link** when starting the new client container, then the client +container can access the exposed port via a private networking interface. Docker +will set some environment variables in the client container to help indicate +which interface and port to use. + +**--log-driver**="*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*splunk*|*etwlogs*|*gcplogs*|*none*" + Logging driver for container. Default is defined by daemon `--log-driver` flag. + **Warning**: the `docker logs` command works only for the `json-file` and + `journald` logging drivers. + +**--log-opt**=[] + Logging driver specific options. + +**-m**, **--memory**="" + Memory limit (format: [], where unit = b, k, m or g) + + Allows you to constrain the memory available to a container. If the host +supports swap memory, then the **-m** memory setting can be larger than physical +RAM. If a limit of 0 is specified (not using **-m**), the container's memory is +not limited. The actual limit may be rounded up to a multiple of the operating +system's page size (the value would be very large, that's millions of trillions). + +**--memory-reservation**="" + Memory soft limit (format: [], where unit = b, k, m or g) + + After setting memory reservation, when the system detects memory contention +or low memory, containers are forced to restrict their consumption to their +reservation. So you should always set the value below **--memory**, otherwise the +hard limit will take precedence. By default, memory reservation will be the same +as memory limit. + +**--memory-swap**="LIMIT" + A limit value equal to memory plus swap. Must be used with the **-m** +(**--memory**) flag. The swap `LIMIT` should always be larger than **-m** +(**--memory**) value. + + The format of `LIMIT` is `[]`. Unit can be `b` (bytes), +`k` (kilobytes), `m` (megabytes), or `g` (gigabytes). If you don't specify a +unit, `b` is used. Set LIMIT to `-1` to enable unlimited swap. + +**--mac-address**="" + Container MAC address (e.g. 92:d0:c6:0a:29:33) + + Remember that the MAC address in an Ethernet network must be unique. +The IPv6 link-local address will be based on the device's MAC address +according to RFC4862. + +**--name**="" + Assign a name to the container + + The operator can identify a container in three ways: + UUID long identifier (“f78375b1c487e03c9438c729345e54db9d20cfa2ac1fc3494b6eb60872e74778”) + UUID short identifier (“f78375b1c487”) + Name (“jonah”) + + The UUID identifiers come from the Docker daemon, and if a name is not assigned +to the container with **--name** then the daemon will also generate a random +string name. The name is useful when defining links (see **--link**) (or any +other place you need to identify a container). This works for both background +and foreground Docker containers. + +**--net**="*bridge*" + Set the Network mode for the container + 'bridge': create a network stack on the default Docker bridge + 'none': no networking + 'container:': reuse another container's network stack + 'host': use the Docker host network stack. Note: the host mode gives the container full access to local system services such as D-bus and is therefore considered insecure. + '|': connect to a user-defined network + +**--net-alias**=[] + Add network-scoped alias for the container + +**--oom-kill-disable**=*true*|*false* + Whether to disable OOM Killer for the container or not. + +**--oom-score-adj**="" + Tune the host's OOM preferences for containers (accepts -1000 to 1000) + +**-P**, **--publish-all**=*true*|*false* + Publish all exposed ports to random ports on the host interfaces. The default is *false*. + + When set to true publish all exposed ports to the host interfaces. The +default is false. If the operator uses -P (or -p) then Docker will make the +exposed port accessible on the host and the ports will be available to any +client that can reach the host. When using -P, Docker will bind any exposed +port to a random port on the host within an *ephemeral port range* defined by +`/proc/sys/net/ipv4/ip_local_port_range`. To find the mapping between the host +ports and the exposed ports, use `docker port`. + +**-p**, **--publish**=[] + Publish a container's port, or range of ports, to the host. + + Format: `ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort` +Both hostPort and containerPort can be specified as a range of ports. +When specifying ranges for both, the number of container ports in the range must match the number of host ports in the range. +(e.g., `docker run -p 1234-1236:1222-1224 --name thisWorks -t busybox` +but not `docker run -p 1230-1236:1230-1240 --name RangeContainerPortsBiggerThanRangeHostPorts -t busybox`) +With ip: `docker run -p 127.0.0.1:$HOSTPORT:$CONTAINERPORT --name CONTAINER -t someimage` +Use `docker port` to see the actual mapping: `docker port CONTAINER $CONTAINERPORT` + +**--pid**=*host* + Set the PID mode for the container + **host**: use the host's PID namespace inside the container. + Note: the host mode gives the container full access to local PID and is therefore considered insecure. + +**--userns**="" + Set the usernamespace mode for the container when `userns-remap` option is enabled. + **host**: use the host usernamespace and enable all privileged options (e.g., `pid=host` or `--privileged`). + +**--pids-limit**="" + Tune the container's pids limit. Set `-1` to have unlimited pids for the container. + +**--uts**=*host* + Set the UTS mode for the container + **host**: use the host's UTS namespace inside the container. + Note: the host mode gives the container access to changing the host's hostname and is therefore considered insecure. + +**--privileged**=*true*|*false* + Give extended privileges to this container. The default is *false*. + + By default, Docker containers are +“unprivileged” (=false) and cannot, for example, run a Docker daemon inside the +Docker container. This is because by default a container is not allowed to +access any devices. A “privileged” container is given access to all devices. + + When the operator executes **docker run --privileged**, Docker will enable access +to all devices on the host as well as set some configuration in AppArmor to +allow the container nearly all the same access to the host as processes running +outside of a container on the host. + +**--read-only**=*true*|*false* + Mount the container's root filesystem as read only. + + By default a container will have its root filesystem writable allowing processes +to write files anywhere. By specifying the `--read-only` flag the container will have +its root filesystem mounted as read only prohibiting any writes. + +**--restart**="*no*" + Restart policy to apply when a container exits (no, on-failure[:max-retry], always, unless-stopped). + +**--rm**=*true*|*false* + Automatically remove the container when it exits (incompatible with -d). The default is *false*. + +**--security-opt**=[] + Security Options + + "label=user:USER" : Set the label user for the container + "label=role:ROLE" : Set the label role for the container + "label=type:TYPE" : Set the label type for the container + "label=level:LEVEL" : Set the label level for the container + "label=disable" : Turn off label confinement for the container + "no-new-privileges" : Disable container processes from gaining additional privileges + + "seccomp=unconfined" : Turn off seccomp confinement for the container + "seccomp=profile.json : White listed syscalls seccomp Json file to be used as a seccomp filter + + "apparmor=unconfined" : Turn off apparmor confinement for the container + "apparmor=your-profile" : Set the apparmor confinement profile for the container + +**--stop-signal**=*SIGTERM* + Signal to stop a container. Default is SIGTERM. + +**--shm-size**="" + Size of `/dev/shm`. The format is ``. + `number` must be greater than `0`. Unit is optional and can be `b` (bytes), `k` (kilobytes), `m`(megabytes), or `g` (gigabytes). + If you omit the unit, the system uses bytes. If you omit the size entirely, the system uses `64m`. + +**--sig-proxy**=*true*|*false* + Proxy received signals to the process (non-TTY mode only). SIGCHLD, SIGSTOP, and SIGKILL are not proxied. The default is *true*. + +**--memory-swappiness**="" + Tune a container's memory swappiness behavior. Accepts an integer between 0 and 100. + +**-t**, **--tty**=*true*|*false* + Allocate a pseudo-TTY. The default is *false*. + + When set to true Docker can allocate a pseudo-tty and attach to the standard +input of any container. This can be used, for example, to run a throwaway +interactive shell. The default is false. + +The **-t** option is incompatible with a redirection of the docker client +standard input. + +**--tmpfs**=[] Create a tmpfs mount + + Mount a temporary filesystem (`tmpfs`) mount into a container, for example: + + $ docker run -d --tmpfs /tmp:rw,size=787448k,mode=1777 my_image + + This command mounts a `tmpfs` at `/tmp` within the container. The supported mount +options are the same as the Linux default `mount` flags. If you do not specify +any options, the systems uses the following options: +`rw,noexec,nosuid,nodev,size=65536k`. + +**-u**, **--user**="" + Sets the username or UID used and optionally the groupname or GID for the specified command. + + The followings examples are all valid: + --user [user | user:group | uid | uid:gid | user:gid | uid:group ] + + Without this argument the command will be run as root in the container. + +**--ulimit**=[] + Ulimit options + +**-v**|**--volume**[=*[[HOST-DIR:]CONTAINER-DIR[:OPTIONS]]*] + Create a bind mount. If you specify, ` -v /HOST-DIR:/CONTAINER-DIR`, Docker + bind mounts `/HOST-DIR` in the host to `/CONTAINER-DIR` in the Docker + container. If 'HOST-DIR' is omitted, Docker automatically creates the new + volume on the host. The `OPTIONS` are a comma delimited list and can be: + + * [rw|ro] + * [z|Z] + * [`[r]shared`|`[r]slave`|`[r]private`] + * [nocopy] + +The `CONTAINER-DIR` must be an absolute path such as `/src/docs`. The `HOST-DIR` +can be an absolute path or a `name` value. A `name` value must start with an +alphanumeric character, followed by `a-z0-9`, `_` (underscore), `.` (period) or +`-` (hyphen). An absolute path starts with a `/` (forward slash). + +If you supply a `HOST-DIR` that is an absolute path, Docker bind-mounts to the +path you specify. If you supply a `name`, Docker creates a named volume by that +`name`. For example, you can specify either `/foo` or `foo` for a `HOST-DIR` +value. If you supply the `/foo` value, Docker creates a bind-mount. If you +supply the `foo` specification, Docker creates a named volume. + +You can specify multiple **-v** options to mount one or more mounts to a +container. To use these same mounts in other containers, specify the +**--volumes-from** option also. + +You can add `:ro` or `:rw` suffix to a volume to mount it read-only or +read-write mode, respectively. By default, the volumes are mounted read-write. +See examples. + +Labeling systems like SELinux require that proper labels are placed on volume +content mounted into a container. Without a label, the security system might +prevent the processes running inside the container from using the content. By +default, Docker does not change the labels set by the OS. + +To change a label in the container context, you can add either of two suffixes +`:z` or `:Z` to the volume mount. These suffixes tell Docker to relabel file +objects on the shared volumes. The `z` option tells Docker that two containers +share the volume content. As a result, Docker labels the content with a shared +content label. Shared volume labels allow all containers to read/write content. +The `Z` option tells Docker to label the content with a private unshared label. +Only the current container can use a private volume. + +By default bind mounted volumes are `private`. That means any mounts done +inside container will not be visible on host and vice-a-versa. One can change +this behavior by specifying a volume mount propagation property. Making a +volume `shared` mounts done under that volume inside container will be +visible on host and vice-a-versa. Making a volume `slave` enables only one +way mount propagation and that is mounts done on host under that volume +will be visible inside container but not the other way around. + +To control mount propagation property of volume one can use `:[r]shared`, +`:[r]slave` or `:[r]private` propagation flag. Propagation property can +be specified only for bind mounted volumes and not for internal volumes or +named volumes. For mount propagation to work source mount point (mount point +where source dir is mounted on) has to have right propagation properties. For +shared volumes, source mount point has to be shared. And for slave volumes, +source mount has to be either shared or slave. + +Use `df ` to figure out the source mount and then use +`findmnt -o TARGET,PROPAGATION ` to figure out propagation +properties of source mount. If `findmnt` utility is not available, then one +can look at mount entry for source mount point in `/proc/self/mountinfo`. Look +at `optional fields` and see if any propagaion properties are specified. +`shared:X` means mount is `shared`, `master:X` means mount is `slave` and if +nothing is there that means mount is `private`. + +To change propagation properties of a mount point use `mount` command. For +example, if one wants to bind mount source directory `/foo` one can do +`mount --bind /foo /foo` and `mount --make-private --make-shared /foo`. This +will convert /foo into a `shared` mount point. Alternatively one can directly +change propagation properties of source mount. Say `/` is source mount for +`/foo`, then use `mount --make-shared /` to convert `/` into a `shared` mount. + +> **Note**: +> When using systemd to manage the Docker daemon's start and stop, in the systemd +> unit file there is an option to control mount propagation for the Docker daemon +> itself, called `MountFlags`. The value of this setting may cause Docker to not +> see mount propagation changes made on the mount point. For example, if this value +> is `slave`, you may not be able to use the `shared` or `rshared` propagation on +> a volume. + +To disable automatic copying of data from the container path to the volume, use +the `nocopy` flag. The `nocopy` flag can be set on bind mounts and named volumes. + +**--volume-driver**="" + Container's volume driver. This driver creates volumes specified either from + a Dockerfile's `VOLUME` instruction or from the `docker run -v` flag. + See **docker-volume-create(1)** for full details. + +**--volumes-from**=[] + Mount volumes from the specified container(s) + + Mounts already mounted volumes from a source container onto another + container. You must supply the source's container-id. To share + a volume, use the **--volumes-from** option when running + the target container. You can share volumes even if the source container + is not running. + + By default, Docker mounts the volumes in the same mode (read-write or + read-only) as it is mounted in the source container. Optionally, you + can change this by suffixing the container-id with either the `:ro` or + `:rw ` keyword. + + If the location of the volume from the source container overlaps with + data residing on a target container, then the volume hides + that data on the target. + +**-w**, **--workdir**="" + Working directory inside the container + + The default working directory for +running binaries within a container is the root directory (/). The developer can +set a different default with the Dockerfile WORKDIR instruction. The operator +can override the working directory by using the **-w** option. + +# Exit Status + +The exit code from `docker run` gives information about why the container +failed to run or why it exited. When `docker run` exits with a non-zero code, +the exit codes follow the `chroot` standard, see below: + +**_125_** if the error is with Docker daemon **_itself_** + + $ docker run --foo busybox; echo $? + # flag provided but not defined: --foo + See 'docker run --help'. + 125 + +**_126_** if the **_contained command_** cannot be invoked + + $ docker run busybox /etc; echo $? + # exec: "/etc": permission denied + docker: Error response from daemon: Contained command could not be invoked + 126 + +**_127_** if the **_contained command_** cannot be found + + $ docker run busybox foo; echo $? + # exec: "foo": executable file not found in $PATH + docker: Error response from daemon: Contained command not found or does not exist + 127 + +**_Exit code_** of **_contained command_** otherwise + + $ docker run busybox /bin/sh -c 'exit 3' + # 3 + +# EXAMPLES + +## Running container in read-only mode + +During container image development, containers often need to write to the image +content. Installing packages into /usr, for example. In production, +applications seldom need to write to the image. Container applications write +to volumes if they need to write to file systems at all. Applications can be +made more secure by running them in read-only mode using the --read-only switch. +This protects the containers image from modification. Read only containers may +still need to write temporary data. The best way to handle this is to mount +tmpfs directories on /run and /tmp. + + # docker run --read-only --tmpfs /run --tmpfs /tmp -i -t fedora /bin/bash + +## Exposing log messages from the container to the host's log + +If you want messages that are logged in your container to show up in the host's +syslog/journal then you should bind mount the /dev/log directory as follows. + + # docker run -v /dev/log:/dev/log -i -t fedora /bin/bash + +From inside the container you can test this by sending a message to the log. + + (bash)# logger "Hello from my container" + +Then exit and check the journal. + + # exit + + # journalctl -b | grep Hello + +This should list the message sent to logger. + +## Attaching to one or more from STDIN, STDOUT, STDERR + +If you do not specify -a then Docker will attach everything (stdin,stdout,stderr) +. You can specify to which of the three standard streams (stdin, stdout, stderr) +you’d like to connect instead, as in: + + # docker run -a stdin -a stdout -i -t fedora /bin/bash + +## Sharing IPC between containers + +Using shm_server.c available here: https://www.cs.cf.ac.uk/Dave/C/node27.html + +Testing `--ipc=host` mode: + +Host shows a shared memory segment with 7 pids attached, happens to be from httpd: + +``` + $ sudo ipcs -m + + ------ Shared Memory Segments -------- + key shmid owner perms bytes nattch status + 0x01128e25 0 root 600 1000 7 +``` + +Now run a regular container, and it correctly does NOT see the shared memory segment from the host: + +``` + $ docker run -it shm ipcs -m + + ------ Shared Memory Segments -------- + key shmid owner perms bytes nattch status +``` + +Run a container with the new `--ipc=host` option, and it now sees the shared memory segment from the host httpd: + + ``` + $ docker run -it --ipc=host shm ipcs -m + + ------ Shared Memory Segments -------- + key shmid owner perms bytes nattch status + 0x01128e25 0 root 600 1000 7 +``` +Testing `--ipc=container:CONTAINERID` mode: + +Start a container with a program to create a shared memory segment: +``` + $ docker run -it shm bash + $ sudo shm/shm_server & + $ sudo ipcs -m + + ------ Shared Memory Segments -------- + key shmid owner perms bytes nattch status + 0x0000162e 0 root 666 27 1 +``` +Create a 2nd container correctly shows no shared memory segment from 1st container: +``` + $ docker run shm ipcs -m + + ------ Shared Memory Segments -------- + key shmid owner perms bytes nattch status +``` + +Create a 3rd container using the new --ipc=container:CONTAINERID option, now it shows the shared memory segment from the first: + +``` + $ docker run -it --ipc=container:ed735b2264ac shm ipcs -m + $ sudo ipcs -m + + ------ Shared Memory Segments -------- + key shmid owner perms bytes nattch status + 0x0000162e 0 root 666 27 1 +``` + +## Linking Containers + +> **Note**: This section describes linking between containers on the +> default (bridge) network, also known as "legacy links". Using `--link` +> on user-defined networks uses the DNS-based discovery, which does not add +> entries to `/etc/hosts`, and does not set environment variables for +> discovery. + +The link feature allows multiple containers to communicate with each other. For +example, a container whose Dockerfile has exposed port 80 can be run and named +as follows: + + # docker run --name=link-test -d -i -t fedora/httpd + +A second container, in this case called linker, can communicate with the httpd +container, named link-test, by running with the **--link=:** + + # docker run -t -i --link=link-test:lt --name=linker fedora /bin/bash + +Now the container linker is linked to container link-test with the alias lt. +Running the **env** command in the linker container shows environment variables + with the LT (alias) context (**LT_**) + + # env + HOSTNAME=668231cb0978 + TERM=xterm + LT_PORT_80_TCP=tcp://172.17.0.3:80 + LT_PORT_80_TCP_PORT=80 + LT_PORT_80_TCP_PROTO=tcp + LT_PORT=tcp://172.17.0.3:80 + PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + PWD=/ + LT_NAME=/linker/lt + SHLVL=1 + HOME=/ + LT_PORT_80_TCP_ADDR=172.17.0.3 + _=/usr/bin/env + +When linking two containers Docker will use the exposed ports of the container +to create a secure tunnel for the parent to access. + +If a container is connected to the default bridge network and `linked` +with other containers, then the container's `/etc/hosts` file is updated +with the linked container's name. + +> **Note** Since Docker may live update the container’s `/etc/hosts` file, there +may be situations when processes inside the container can end up reading an +empty or incomplete `/etc/hosts` file. In most cases, retrying the read again +should fix the problem. + + +## Mapping Ports for External Usage + +The exposed port of an application can be mapped to a host port using the **-p** +flag. For example, a httpd port 80 can be mapped to the host port 8080 using the +following: + + # docker run -p 8080:80 -d -i -t fedora/httpd + +## Creating and Mounting a Data Volume Container + +Many applications require the sharing of persistent data across several +containers. Docker allows you to create a Data Volume Container that other +containers can mount from. For example, create a named container that contains +directories /var/volume1 and /tmp/volume2. The image will need to contain these +directories so a couple of RUN mkdir instructions might be required for you +fedora-data image: + + # docker run --name=data -v /var/volume1 -v /tmp/volume2 -i -t fedora-data true + # docker run --volumes-from=data --name=fedora-container1 -i -t fedora bash + +Multiple --volumes-from parameters will bring together multiple data volumes from +multiple containers. And it's possible to mount the volumes that came from the +DATA container in yet another container via the fedora-container1 intermediary +container, allowing to abstract the actual data source from users of that data: + + # docker run --volumes-from=fedora-container1 --name=fedora-container2 -i -t fedora bash + +## Mounting External Volumes + +To mount a host directory as a container volume, specify the absolute path to +the directory and the absolute path for the container directory separated by a +colon: + + # docker run -v /var/db:/data1 -i -t fedora bash + +When using SELinux, be aware that the host has no knowledge of container SELinux +policy. Therefore, in the above example, if SELinux policy is enforced, the +`/var/db` directory is not writable to the container. A "Permission Denied" +message will occur and an avc: message in the host's syslog. + + +To work around this, at time of writing this man page, the following command +needs to be run in order for the proper SELinux policy type label to be attached +to the host directory: + + # chcon -Rt svirt_sandbox_file_t /var/db + + +Now, writing to the /data1 volume in the container will be allowed and the +changes will also be reflected on the host in /var/db. + +## Using alternative security labeling + +You can override the default labeling scheme for each container by specifying +the `--security-opt` flag. For example, you can specify the MCS/MLS level, a +requirement for MLS systems. Specifying the level in the following command +allows you to share the same content between containers. + + # docker run --security-opt label=level:s0:c100,c200 -i -t fedora bash + +An MLS example might be: + + # docker run --security-opt label=level:TopSecret -i -t rhel7 bash + +To disable the security labeling for this container versus running with the +`--permissive` flag, use the following command: + + # docker run --security-opt label=disable -i -t fedora bash + +If you want a tighter security policy on the processes within a container, +you can specify an alternate type for the container. You could run a container +that is only allowed to listen on Apache ports by executing the following +command: + + # docker run --security-opt label=type:svirt_apache_t -i -t centos bash + +Note: + +You would have to write policy defining a `svirt_apache_t` type. + +## Setting device weight + +If you want to set `/dev/sda` device weight to `200`, you can specify the device +weight by `--blkio-weight-device` flag. Use the following command: + + # docker run -it --blkio-weight-device "/dev/sda:200" ubuntu + +## Specify isolation technology for container (--isolation) + +This option is useful in situations where you are running Docker containers on +Microsoft Windows. The `--isolation ` option sets a container's isolation +technology. On Linux, the only supported is the `default` option which uses +Linux namespaces. These two commands are equivalent on Linux: + +``` +$ docker run -d busybox top +$ docker run -d --isolation default busybox top +``` + +On Microsoft Windows, can take any of these values: + +* `default`: Use the value specified by the Docker daemon's `--exec-opt` . If the `daemon` does not specify an isolation technology, Microsoft Windows uses `process` as its default value. +* `process`: Namespace isolation only. +* `hyperv`: Hyper-V hypervisor partition-based isolation. + +In practice, when running on Microsoft Windows without a `daemon` option set, these two commands are equivalent: + +``` +$ docker run -d --isolation default busybox top +$ docker run -d --isolation process busybox top +``` + +If you have set the `--exec-opt isolation=hyperv` option on the Docker `daemon`, any of these commands also result in `hyperv` isolation: + +``` +$ docker run -d --isolation default busybox top +$ docker run -d --isolation hyperv busybox top +``` + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit +July 2014, updated by Sven Dowideit +November 2015, updated by Sally O'Malley diff --git a/man/docker-save.1.md b/man/docker-save.1.md new file mode 100644 index 00000000..1d1de8a1 --- /dev/null +++ b/man/docker-save.1.md @@ -0,0 +1,45 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-save - Save one or more images to a tar archive (streamed to STDOUT by default) + +# SYNOPSIS +**docker save** +[**--help**] +[**-o**|**--output**[=*OUTPUT*]] +IMAGE [IMAGE...] + +# DESCRIPTION +Produces a tarred repository to the standard output stream. Contains all +parent layers, and all tags + versions, or specified repo:tag. + +Stream to a file instead of STDOUT by using **-o**. + +# OPTIONS +**--help** + Print usage statement + +**-o**, **--output**="" + Write to a file, instead of STDOUT + +# EXAMPLES + +Save all fedora repository images to a fedora-all.tar and save the latest +fedora image to a fedora-latest.tar: + + $ docker save fedora > fedora-all.tar + $ docker save --output=fedora-latest.tar fedora:latest + $ ls -sh fedora-all.tar + 721M fedora-all.tar + $ ls -sh fedora-latest.tar + 367M fedora-latest.tar + +# See also +**docker-load(1)** to load an image from a tar archive on STDIN. + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit +November 2014, updated by Sven Dowideit diff --git a/man/docker-search.1.md b/man/docker-search.1.md new file mode 100644 index 00000000..a95c0237 --- /dev/null +++ b/man/docker-search.1.md @@ -0,0 +1,65 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-search - Search the Docker Hub for images + +# SYNOPSIS +**docker search** +[**--automated**] +[**--help**] +[**--no-trunc**] +[**-s**|**--stars**[=*0*]] +TERM + +# DESCRIPTION + +Search Docker Hub for images that match the specified `TERM`. The table +of images returned displays the name, description (truncated by default), number +of stars awarded, whether the image is official, and whether it is automated. + +*Note* - Search queries will only return up to 25 results + +# OPTIONS +**--automated**=*true*|*false* + Only show automated builds. The default is *false*. + +**--help** + Print usage statement + +**--no-trunc**=*true*|*false* + Don't truncate output. The default is *false*. + +**-s**, **--stars**=*X* + Only displays with at least X stars. The default is zero. + +# EXAMPLES + +## Search Docker Hub for ranked images + +Search a registry for the term 'fedora' and only display those images +ranked 3 or higher: + + $ docker search -s 3 fedora + NAME DESCRIPTION STARS OFFICIAL AUTOMATED + mattdm/fedora A basic Fedora image corresponding roughly... 50 + fedora (Semi) Official Fedora base image. 38 + mattdm/fedora-small A small Fedora image on which to build. Co... 8 + goldmann/wildfly A WildFly application server running on a ... 3 [OK] + +## Search Docker Hub for automated images + +Search Docker Hub for the term 'fedora' and only display automated images +ranked 1 or higher: + + $ docker search --automated -s 1 fedora + NAME DESCRIPTION STARS OFFICIAL AUTOMATED + goldmann/wildfly A WildFly application server running on a ... 3 [OK] + tutum/fedora-20 Fedora 20 image with SSH access. For the r... 1 [OK] + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit +April 2015, updated by Mary Anthony for v2 + diff --git a/man/docker-start.1.md b/man/docker-start.1.md new file mode 100644 index 00000000..c00b0a16 --- /dev/null +++ b/man/docker-start.1.md @@ -0,0 +1,39 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-start - Start one or more containers + +# SYNOPSIS +**docker start** +[**-a**|**--attach**] +[**--detach-keys**[=*[]*]] +[**--help**] +[**-i**|**--interactive**] +CONTAINER [CONTAINER...] + +# DESCRIPTION + +Start one or more containers. + +# OPTIONS +**-a**, **--attach**=*true*|*false* + Attach container's STDOUT and STDERR and forward all signals to the + process. The default is *false*. + +**--detach-keys**="" + Override the key sequence for detaching a container. Format is a single character `[a-Z]` or `ctrl-` where `` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`. + +**--help** + Print usage statement + +**-i**, **--interactive**=*true*|*false* + Attach container's STDIN. The default is *false*. + +# See also +**docker-stop(1)** to stop a container. + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit diff --git a/man/docker-stats.1.md b/man/docker-stats.1.md new file mode 100644 index 00000000..520466b5 --- /dev/null +++ b/man/docker-stats.1.md @@ -0,0 +1,43 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-stats - Display a live stream of one or more containers' resource usage statistics + +# SYNOPSIS +**docker stats** +[**-a**|**--all**] +[**--help**] +[**--no-stream**] +[CONTAINER...] + +# DESCRIPTION + +Display a live stream of one or more containers' resource usage statistics + +# OPTIONS +**-a**, **--all**=*true*|*false* + Show all containers. Only running containers are shown by default. The default is *false*. + +**--help** + Print usage statement + +**--no-stream**=*true*|*false* + Disable streaming stats and only pull the first result, default setting is false. + +# EXAMPLES + +Running `docker stats` on all running containers + + $ docker stats + CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O + 1285939c1fd3 0.07% 796 KB / 64 MB 1.21% 788 B / 648 B 3.568 MB / 512 KB + 9c76f7834ae2 0.07% 2.746 MB / 64 MB 4.29% 1.266 KB / 648 B 12.4 MB / 0 B + d1ea048f04e4 0.03% 4.583 MB / 64 MB 6.30% 2.854 KB / 648 B 27.7 MB / 0 B + +Running `docker stats` on multiple containers by name and id. + + $ docker stats fervent_panini 5acfcb1b4fd1 + CONTAINER CPU % MEM USAGE/LIMIT MEM % NET I/O + 5acfcb1b4fd1 0.00% 115.2 MB/1.045 GB 11.03% 1.422 kB/648 B + fervent_panini 0.02% 11.08 MB/1.045 GB 1.06% 648 B/648 B diff --git a/man/docker-stop.1.md b/man/docker-stop.1.md new file mode 100644 index 00000000..fa377c92 --- /dev/null +++ b/man/docker-stop.1.md @@ -0,0 +1,30 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-stop - Stop a container by sending SIGTERM and then SIGKILL after a grace period + +# SYNOPSIS +**docker stop** +[**--help**] +[**-t**|**--time**[=*10*]] +CONTAINER [CONTAINER...] + +# DESCRIPTION +Stop a container (Send SIGTERM, and then SIGKILL after + grace period) + +# OPTIONS +**--help** + Print usage statement + +**-t**, **--time**=*10* + Number of seconds to wait for the container to stop before killing it. Default is 10 seconds. + +#See also +**docker-start(1)** to restart a stopped container. + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit diff --git a/man/docker-tag.1.md b/man/docker-tag.1.md new file mode 100644 index 00000000..68c90b76 --- /dev/null +++ b/man/docker-tag.1.md @@ -0,0 +1,61 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-tag - Tag an image into a repository + +# SYNOPSIS +**docker tag** +[**--help**] +IMAGE[:TAG] [REGISTRY_HOST/][USERNAME/]NAME[:TAG] + +# DESCRIPTION +Assigns a new alias to an image in a registry. An alias refers to the +entire image name including the optional `TAG` after the ':'. + +If you do not specify a `REGISTRY_HOST`, the command uses Docker's public +registry located at `registry-1.docker.io` by default. + +# "OPTIONS" +**--help** + Print usage statement. + +**REGISTRY_HOST** + The hostname of the registry if required. This may also include the port +separated by a ':' + +**USERNAME** + The username or other qualifying identifier for the image. + +**NAME** + The image name. + +**TAG** + The tag you are assigning to the image. Though this is arbitrary it is +recommended to be used for a version to distinguish images with the same name. +Also, for consistency tags should only include a-z0-9-_. . +Note that here TAG is a part of the overall name or "tag". + +# EXAMPLES + +## Giving an image a new alias + +Here is an example of aliasing an image (e.g., 0e5574283393) as "httpd" and +tagging it into the "fedora" repository with "version1.0": + + docker tag 0e5574283393 fedora/httpd:version1.0 + +## Tagging an image for a private repository + +To push an image to a private registry and not the central Docker +registry you must tag it with the registry hostname and port (if needed). + + docker tag 0e5574283393 myregistryhost:5000/fedora/httpd:version1.0 + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit +July 2014, updated by Sven Dowideit +April 2015, updated by Mary Anthony for v2 +June 2015, updated by Sally O'Malley diff --git a/man/docker-top.1.md b/man/docker-top.1.md new file mode 100644 index 00000000..a666f7cd --- /dev/null +++ b/man/docker-top.1.md @@ -0,0 +1,36 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-top - Display the running processes of a container + +# SYNOPSIS +**docker top** +[**--help**] +CONTAINER [ps OPTIONS] + +# DESCRIPTION + +Display the running process of the container. ps-OPTION can be any of the options you would pass to a Linux ps command. + +All displayed information is from host's point of view. + +# OPTIONS +**--help** + Print usage statement + +# EXAMPLES + +Run **docker top** with the ps option of -x: + + $ docker top 8601afda2b -x + PID TTY STAT TIME COMMAND + 16623 ? Ss 0:00 sleep 99999 + + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit +June 2015, updated by Ma Shimiao +December 2015, updated by Pavel Pospisil diff --git a/man/docker-unpause.1.md b/man/docker-unpause.1.md new file mode 100644 index 00000000..466e1bb1 --- /dev/null +++ b/man/docker-unpause.1.md @@ -0,0 +1,27 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-unpause - Unpause all processes within a container + +# SYNOPSIS +**docker unpause** +CONTAINER [CONTAINER...] + +# DESCRIPTION + +The `docker unpause` command uses the cgroups freezer to un-suspend all +processes in a container. + +See the [cgroups freezer documentation] +(https://www.kernel.org/doc/Documentation/cgroups/freezer-subsystem.txt) for +further details. + +# OPTIONS +There are no available options. + +# See also +**docker-pause(1)** to pause all processes within a container. + +# HISTORY +June 2014, updated by Sven Dowideit diff --git a/man/docker-update.1.md b/man/docker-update.1.md new file mode 100644 index 00000000..87849ef8 --- /dev/null +++ b/man/docker-update.1.md @@ -0,0 +1,108 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-update - Update configuration of one or more containers + +# SYNOPSIS +**docker update** +[**--blkio-weight**[=*[BLKIO-WEIGHT]*]] +[**--cpu-shares**[=*0*]] +[**--cpu-period**[=*0*]] +[**--cpu-quota**[=*0*]] +[**--cpuset-cpus**[=*CPUSET-CPUS*]] +[**--cpuset-mems**[=*CPUSET-MEMS*]] +[**--help**] +[**--kernel-memory**[=*KERNEL-MEMORY*]] +[**-m**|**--memory**[=*MEMORY*]] +[**--memory-reservation**[=*MEMORY-RESERVATION*]] +[**--memory-swap**[=*MEMORY-SWAP*]] +[**--restart**[=*""*]] +CONTAINER [CONTAINER...] + +# DESCRIPTION + +The `docker update` command dynamically updates container configuration. +you can Use this command to prevent containers from consuming too many +resources from their Docker host. With a single command, you can place +limits on a single container or on many. To specify more than one container, +provide space-separated list of container names or IDs. + +With the exception of the `--kernel-memory` value, you can specify these +options on a running or a stopped container. You can only update +`--kernel-memory` on a stopped container. When you run `docker update` on +stopped container, the next time you restart it, the container uses those +values. + +Another configuration you can change with this command is restart policy, +new restart policy will take effect instantly after you run `docker update` +on a container. + +# OPTIONS +**--blkio-weight**=0 + Block IO weight (relative weight) accepts a weight value between 10 and 1000. + +**--cpu-shares**=0 + CPU shares (relative weight) + +**--cpu-period**=0 + Limit the CPU CFS (Completely Fair Scheduler) period + +**--cpu-quota**=0 + Limit the CPU CFS (Completely Fair Scheduler) quota + +**--cpuset-cpus**="" + CPUs in which to allow execution (0-3, 0,1) + +**--cpuset-mems**="" + Memory nodes(MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems. + +**--help** + Print usage statement + +**--kernel-memory**="" + Kernel memory limit (format: `[]`, where unit = b, k, m or g) + + Note that you can not update kernel memory to a running container, it can only +be updated to a stopped container, and affect after it's started. + +**-m**, **--memory**="" + Memory limit (format: , where unit = b, k, m or g) + +**--memory-reservation**="" + Memory soft limit (format: [], where unit = b, k, m or g) + +**--memory-swap**="" + Total memory limit (memory + swap) + +**--restart**="" + Restart policy to apply when a container exits (no, on-failure[:max-retry], always, unless-stopped). + +# EXAMPLES + +The following sections illustrate ways to use this command. + +### Update a container with cpu-shares=512 + +To limit a container's cpu-shares to 512, first identify the container +name or ID. You can use **docker ps** to find these values. You can also +use the ID returned from the **docker run** command. Then, do the following: + +```bash +$ docker update --cpu-shares 512 abebf7571666 +``` + +### Update a container with cpu-shares and memory + +To update multiple resource configurations for multiple containers: + +```bash +$ docker update --cpu-shares 512 -m 300M abebf7571666 hopeful_morse +``` + +### Update a container's restart policy + +To update restart policy for one or more containers: +```bash +$ docker update --restart=on-failure:3 abebf7571666 hopeful_morse +``` diff --git a/man/docker-version.1.md b/man/docker-version.1.md new file mode 100644 index 00000000..04ae3464 --- /dev/null +++ b/man/docker-version.1.md @@ -0,0 +1,62 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2015 +# NAME +docker-version - Show the Docker version information. + +# SYNOPSIS +**docker version** +[**--help**] +[**-f**|**--format**[=*FORMAT*]] + +# DESCRIPTION +This command displays version information for both the Docker client and +daemon. + +# OPTIONS +**--help** + Print usage statement + +**-f**, **--format**="" + Format the output using the given go template. + +# EXAMPLES + +## Display Docker version information + +The default output: + + $ docker version + Client: + Version: 1.8.0 + API version: 1.20 + Go version: go1.4.2 + Git commit: f5bae0a + Built: Tue Jun 23 17:56:00 UTC 2015 + OS/Arch: linux/amd64 + + Server: + Version: 1.8.0 + API version: 1.20 + Go version: go1.4.2 + Git commit: f5bae0a + Built: Tue Jun 23 17:56:00 UTC 2015 + OS/Arch: linux/amd64 + +Get server version: + + $ docker version --format '{{.Server.Version}}' + 1.8.0 + +Dump raw data: + +To view all available fields, you can use the format `{{json .}}`. + + $ docker version --format '{{json .}}' + {"Client":{"Version":"1.8.0","ApiVersion":"1.20","GitCommit":"f5bae0a","GoVersion":"go1.4.2","Os":"linux","Arch":"amd64","BuildTime":"Tue Jun 23 17:56:00 UTC 2015"},"ServerOK":true,"Server":{"Version":"1.8.0","ApiVersion":"1.20","GitCommit":"f5bae0a","GoVersion":"go1.4.2","Os":"linux","Arch":"amd64","KernelVersion":"3.13.2-gentoo","BuildTime":"Tue Jun 23 17:56:00 UTC 2015"}} + + +# HISTORY +June 2014, updated by Sven Dowideit +June 2015, updated by John Howard +June 2015, updated by Patrick Hemmer diff --git a/man/docker-volume-inspect.1.md b/man/docker-volume-inspect.1.md new file mode 100644 index 00000000..6097e96e --- /dev/null +++ b/man/docker-volume-inspect.1.md @@ -0,0 +1,29 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JULY 2015 +# NAME +docker-volume-inspect - Get low-level information about a volume + +# SYNOPSIS +**docker volume inspect** +[**-f**|**--format**[=*FORMAT*]] +[**--help**] +VOLUME [VOLUME...] + +# DESCRIPTION + +Returns information about one or more volumes. By default, this command renders all results +in a JSON array. You can specify an alternate format to execute a given template +is executed for each result. Go's +http://golang.org/pkg/text/template/ package describes all the details of the +format. + +# OPTIONS +**-f**, **--format**="" + Format the output using the given go template. + +**--help** + Print usage statement + +# HISTORY +July 2015, created by Brian Goff diff --git a/man/docker-volume-ls.1.md b/man/docker-volume-ls.1.md new file mode 100644 index 00000000..b115a039 --- /dev/null +++ b/man/docker-volume-ls.1.md @@ -0,0 +1,30 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JULY 2015 +# NAME +docker-volume-ls - List all volumes + +# SYNOPSIS +**docker volume ls** +[**-f**|**--filter**[=*FILTER*]] +[**--help**] +[**-q**|**--quiet**[=*true*|*false*]] + +# DESCRIPTION + +Lists all the volumes Docker knows about. You can filter using the `-f` or `--filter` flag. The filtering format is a `key=value` pair. To specify more than one filter, pass multiple flags (for example, `--filter "foo=bar" --filter "bif=baz"`) + +There is a single supported filter `dangling=value` which takes a boolean of `true` or `false`. + +# OPTIONS +**-f**, **--filter**="" + Provide filter values (i.e. 'dangling=true') + +**--help** + Print usage statement + +**-q**, **--quiet**=*true*|*false* + Only display volume names + +# HISTORY +July 2015, created by Brian Goff diff --git a/man/docker-volume-rm.1.md b/man/docker-volume-rm.1.md new file mode 100644 index 00000000..876700d4 --- /dev/null +++ b/man/docker-volume-rm.1.md @@ -0,0 +1,26 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JULY 2015 +# NAME +docker-volume-rm - Remove a volume + +# SYNOPSIS +**docker volume rm** +[**--help**] +VOLUME [VOLUME...] + +# DESCRIPTION + +Removes one or more volumes. You cannot remove a volume that is in use by a container. + + ``` + $ docker volume rm hello + hello + ``` + +# OPTIONS +**--help** + Print usage statement + +# HISTORY +July 2015, created by Brian Goff diff --git a/man/docker-volume.1.md b/man/docker-volume.1.md new file mode 100644 index 00000000..5ba8fc4b --- /dev/null +++ b/man/docker-volume.1.md @@ -0,0 +1,51 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% Feb 2016 +# NAME +docker-volume - Create a new volume + +# SYNOPSIS +**docker volume** [OPTIONS] COMMAND +[**--help**] + +# DESCRIPTION + +docker volume has subcommands for managing data volumes. + +## Data volumes + +The `docker volume` command has subcommands for managing data volumes. A data volume is a specially-designated directory that by-passes storage driver management. + +Data volumes persist data independent of a container's life cycle. When you delete a container, the Engine daemon does not delete any data volumes. You can share volumes across multiple containers. Moreover, you can share data volumes with other computing resources in your system. + +To see help for a subcommand, use: + +``` +docker volume CMD help +``` + +For full details on using docker volume visit Docker's online documentation. + +# OPTIONS +**--help** + Print usage statement + +# COMMANDS +**create** + Create a volume + See **docker-volume-create(1)** for full documentation on the **create** command. + +**inspect** + Return low-level information on a volume + See **docker-volume-inspect(1)** for full documentation on the **inspect** command. + +**ls** + List volumes + See **docker-volume-ls(1)** for full documentation on the **ls** command. + +**rm** + Remove a volume + See **docker-volume-rm(1)** for full documentation on the **rm** command. + +# HISTORY +Feb 2016, created by Dan Walsh diff --git a/man/docker-wait.1.md b/man/docker-wait.1.md new file mode 100644 index 00000000..5f07bacc --- /dev/null +++ b/man/docker-wait.1.md @@ -0,0 +1,30 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-wait - Block until a container stops, then print its exit code. + +# SYNOPSIS +**docker wait** +[**--help**] +CONTAINER [CONTAINER...] + +# DESCRIPTION + +Block until a container stops, then print its exit code. + +# OPTIONS +**--help** + Print usage statement + +# EXAMPLES + + $ docker run -d fedora sleep 99 + 079b83f558a2bc52ecad6b2a5de13622d584e6bb1aea058c11b36511e85e7622 + $ docker wait 079b83f558a2bc + 0 + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit diff --git a/man/docker.1.md b/man/docker.1.md new file mode 100644 index 00000000..6f4f6ab8 --- /dev/null +++ b/man/docker.1.md @@ -0,0 +1,244 @@ +% DOCKER(1) Docker User Manuals +% William Henry +% APRIL 2014 +# NAME +docker \- Docker image and container command line interface + +# SYNOPSIS +**docker** [OPTIONS] COMMAND [arg...] + +**docker** daemon [--help|...] + +**docker** [--help|-v|--version] + +# DESCRIPTION +**docker** has two distinct functions. It is used for starting the Docker +daemon and to run the CLI (i.e., to command the daemon to manage images, +containers etc.) So **docker** is both a server, as a daemon, and a client +to the daemon, through the CLI. + +To run the Docker daemon you can specify **docker daemon**. +You can view the daemon options using **docker daemon --help**. +To see the man page for the daemon, run **man docker daemon**. + +The Docker CLI has over 30 commands. The commands are listed below and each has +its own man page which explain usage and arguments. + +To see the man page for a command run **man docker **. + +# OPTIONS +**--help** + Print usage statement + +**--config**="" + Specifies the location of the Docker client configuration files. The default is '~/.docker'. + +**-D**, **--debug**=*true*|*false* + Enable debug mode. Default is false. + +**-H**, **--host**=[*unix:///var/run/docker.sock*]: tcp://[host]:[port][path] to bind or +unix://[/path/to/socket] to use. + The socket(s) to bind to in daemon mode specified using one or more + tcp://host:port/path, unix:///path/to/socket, fd://* or fd://socketfd. + If the tcp port is not specified, then it will default to either `2375` when + `--tls` is off, or `2376` when `--tls` is on, or `--tlsverify` is specified. + +**-l**, **--log-level**="*debug*|*info*|*warn*|*error*|*fatal*" + Set the logging level. Default is `info`. + +**--tls**=*true*|*false* + Use TLS; implied by --tlsverify. Default is false. + +**--tlscacert**=*~/.docker/ca.pem* + Trust certs signed only by this CA. + +**--tlscert**=*~/.docker/cert.pem* + Path to TLS certificate file. + +**--tlskey**=*~/.docker/key.pem* + Path to TLS key file. + +**--tlsverify**=*true*|*false* + Use TLS and verify the remote (daemon: verify client, client: verify daemon). + Default is false. + +**-v**, **--version**=*true*|*false* + Print version information and quit. Default is false. + +# COMMANDS +**attach** + Attach to a running container + See **docker-attach(1)** for full documentation on the **attach** command. + +**build** + Build an image from a Dockerfile + See **docker-build(1)** for full documentation on the **build** command. + +**commit** + Create a new image from a container's changes + See **docker-commit(1)** for full documentation on the **commit** command. + +**cp** + Copy files/folders between a container and the local filesystem + See **docker-cp(1)** for full documentation on the **cp** command. + +**create** + Create a new container + See **docker-create(1)** for full documentation on the **create** command. + +**diff** + Inspect changes on a container's filesystem + See **docker-diff(1)** for full documentation on the **diff** command. + +**events** + Get real time events from the server + See **docker-events(1)** for full documentation on the **events** command. + +**exec** + Run a command in a running container + See **docker-exec(1)** for full documentation on the **exec** command. + +**export** + Stream the contents of a container as a tar archive + See **docker-export(1)** for full documentation on the **export** command. + +**history** + Show the history of an image + See **docker-history(1)** for full documentation on the **history** command. + +**images** + List images + See **docker-images(1)** for full documentation on the **images** command. + +**import** + Create a new filesystem image from the contents of a tarball + See **docker-import(1)** for full documentation on the **import** command. + +**info** + Display system-wide information + See **docker-info(1)** for full documentation on the **info** command. + +**inspect** + Return low-level information on a container or image + See **docker-inspect(1)** for full documentation on the **inspect** command. + +**kill** + Kill a running container (which includes the wrapper process and everything +inside it) + See **docker-kill(1)** for full documentation on the **kill** command. + +**load** + Load an image from a tar archive + See **docker-load(1)** for full documentation on the **load** command. + +**login** + Log in to a Docker Registry + See **docker-login(1)** for full documentation on the **login** command. + +**logout** + Log the user out of a Docker Registry + See **docker-logout(1)** for full documentation on the **logout** command. + +**logs** + Fetch the logs of a container + See **docker-logs(1)** for full documentation on the **logs** command. + +**pause** + Pause all processes within a container + See **docker-pause(1)** for full documentation on the **pause** command. + +**port** + Lookup the public-facing port which is NAT-ed to PRIVATE_PORT + See **docker-port(1)** for full documentation on the **port** command. + +**ps** + List containers + See **docker-ps(1)** for full documentation on the **ps** command. + +**pull** + Pull an image or a repository from a Docker Registry + See **docker-pull(1)** for full documentation on the **pull** command. + +**push** + Push an image or a repository to a Docker Registry + See **docker-push(1)** for full documentation on the **push** command. + +**rename** + Rename a container. + See **docker-rename(1)** for full documentation on the **rename** command. + +**restart** + Restart a container + See **docker-restart(1)** for full documentation on the **restart** command. + +**rm** + Remove one or more containers + See **docker-rm(1)** for full documentation on the **rm** command. + +**rmi** + Remove one or more images + See **docker-rmi(1)** for full documentation on the **rmi** command. + +**run** + Run a command in a new container + See **docker-run(1)** for full documentation on the **run** command. + +**save** + Save an image to a tar archive + See **docker-save(1)** for full documentation on the **save** command. + +**search** + Search for an image in the Docker index + See **docker-search(1)** for full documentation on the **search** command. + +**start** + Start a container + See **docker-start(1)** for full documentation on the **start** command. + +**stats** + Display a live stream of one or more containers' resource usage statistics + See **docker-stats(1)** for full documentation on the **stats** command. + +**stop** + Stop a container + See **docker-stop(1)** for full documentation on the **stop** command. + +**tag** + Tag an image into a repository + See **docker-tag(1)** for full documentation on the **tag** command. + +**top** + Lookup the running processes of a container + See **docker-top(1)** for full documentation on the **top** command. + +**unpause** + Unpause all processes within a container + See **docker-unpause(1)** for full documentation on the **unpause** command. + +**version** + Show the Docker version information + See **docker-version(1)** for full documentation on the **version** command. + +**wait** + Block until a container stops, then print its exit code + See **docker-wait(1)** for full documentation on the **wait** command. + + +# RUNTIME EXECUTION OPTIONS + +Use the **--exec-opt** flags to specify options to the execution driver. +The following options are available: + +#### native.cgroupdriver +Specifies the management of the container's `cgroups`. You can specify `cgroupfs` +or `systemd`. If you specify `systemd` and it is not available, the system errors +out. + +#### Client +For specific client examples please see the man page for the specific Docker +command. For example: + + man docker-run + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) based on docker.com source material and internal work. diff --git a/man/md2man-all.sh b/man/md2man-all.sh new file mode 100755 index 00000000..97c65c93 --- /dev/null +++ b/man/md2man-all.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e + +# get into this script's directory +cd "$(dirname "$(readlink -f "$BASH_SOURCE")")" + +[ "$1" = '-q' ] || { + set -x + pwd +} + +for FILE in *.md; do + base="$(basename "$FILE")" + name="${base%.md}" + num="${name##*.}" + if [ -z "$num" -o "$name" = "$num" ]; then + # skip files that aren't of the format xxxx.N.md (like README.md) + continue + fi + mkdir -p "./man${num}" + go-md2man -in "$FILE" -out "./man${num}/${name}" +done diff --git a/migrate/v1/migratev1.go b/migrate/v1/migratev1.go new file mode 100644 index 00000000..aa9d48cb --- /dev/null +++ b/migrate/v1/migratev1.go @@ -0,0 +1,508 @@ +package v1 + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "strconv" + "sync" + "time" + + "encoding/json" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/digest" + "github.com/docker/docker/distribution/metadata" + "github.com/docker/docker/image" + imagev1 "github.com/docker/docker/image/v1" + "github.com/docker/docker/layer" + "github.com/docker/docker/reference" +) + +type graphIDRegistrar interface { + RegisterByGraphID(string, layer.ChainID, layer.DiffID, string, int64) (layer.Layer, error) + Release(layer.Layer) ([]layer.Metadata, error) +} + +type graphIDMounter interface { + CreateRWLayerByGraphID(string, string, layer.ChainID) error +} + +type checksumCalculator interface { + ChecksumForGraphID(id, parent, oldTarDataPath, newTarDataPath string) (diffID layer.DiffID, size int64, err error) +} + +const ( + graphDirName = "graph" + tarDataFileName = "tar-data.json.gz" + migrationFileName = ".migration-v1-images.json" + migrationTagsFileName = ".migration-v1-tags" + migrationDiffIDFileName = ".migration-diffid" + migrationSizeFileName = ".migration-size" + migrationTarDataFileName = ".migration-tardata" + containersDirName = "containers" + configFileNameLegacy = "config.json" + configFileName = "config.v2.json" + repositoriesFilePrefixLegacy = "repositories-" +) + +var ( + errUnsupported = errors.New("migration is not supported") +) + +// Migrate takes an old graph directory and transforms the metadata into the +// new format. +func Migrate(root, driverName string, ls layer.Store, is image.Store, rs reference.Store, ms metadata.Store) error { + graphDir := filepath.Join(root, graphDirName) + if _, err := os.Lstat(graphDir); os.IsNotExist(err) { + return nil + } + + mappings, err := restoreMappings(root) + if err != nil { + return err + } + + if cc, ok := ls.(checksumCalculator); ok { + CalculateLayerChecksums(root, cc, mappings) + } + + if registrar, ok := ls.(graphIDRegistrar); !ok { + return errUnsupported + } else if err := migrateImages(root, registrar, is, ms, mappings); err != nil { + return err + } + + err = saveMappings(root, mappings) + if err != nil { + return err + } + + if mounter, ok := ls.(graphIDMounter); !ok { + return errUnsupported + } else if err := migrateContainers(root, mounter, is, mappings); err != nil { + return err + } + + if err := migrateRefs(root, driverName, rs, mappings); err != nil { + return err + } + + return nil +} + +// CalculateLayerChecksums walks an old graph directory and calculates checksums +// for each layer. These checksums are later used for migration. +func CalculateLayerChecksums(root string, ls checksumCalculator, mappings map[string]image.ID) { + graphDir := filepath.Join(root, graphDirName) + // spawn some extra workers also for maximum performance because the process is bounded by both cpu and io + workers := runtime.NumCPU() * 3 + workQueue := make(chan string, workers) + + wg := sync.WaitGroup{} + + for i := 0; i < workers; i++ { + wg.Add(1) + go func() { + for id := range workQueue { + start := time.Now() + if err := calculateLayerChecksum(graphDir, id, ls); err != nil { + logrus.Errorf("could not calculate checksum for %q, %q", id, err) + } + elapsed := time.Since(start) + logrus.Debugf("layer %s took %.2f seconds", id, elapsed.Seconds()) + } + wg.Done() + }() + } + + dir, err := ioutil.ReadDir(graphDir) + if err != nil { + logrus.Errorf("could not read directory %q", graphDir) + return + } + for _, v := range dir { + v1ID := v.Name() + if err := imagev1.ValidateID(v1ID); err != nil { + continue + } + if _, ok := mappings[v1ID]; ok { // support old migrations without helper files + continue + } + workQueue <- v1ID + } + close(workQueue) + wg.Wait() +} + +func calculateLayerChecksum(graphDir, id string, ls checksumCalculator) error { + diffIDFile := filepath.Join(graphDir, id, migrationDiffIDFileName) + if _, err := os.Lstat(diffIDFile); err == nil { + return nil + } else if !os.IsNotExist(err) { + return err + } + + parent, err := getParent(filepath.Join(graphDir, id)) + if err != nil { + return err + } + + diffID, size, err := ls.ChecksumForGraphID(id, parent, filepath.Join(graphDir, id, tarDataFileName), filepath.Join(graphDir, id, migrationTarDataFileName)) + if err != nil { + return err + } + + if err := ioutil.WriteFile(filepath.Join(graphDir, id, migrationSizeFileName), []byte(strconv.Itoa(int(size))), 0600); err != nil { + return err + } + + tmpFile := filepath.Join(graphDir, id, migrationDiffIDFileName+".tmp") + if err := ioutil.WriteFile(tmpFile, []byte(diffID), 0600); err != nil { + return err + } + + if err := os.Rename(tmpFile, filepath.Join(graphDir, id, migrationDiffIDFileName)); err != nil { + return err + } + + logrus.Infof("calculated checksum for layer %s: %s", id, diffID) + return nil +} + +func restoreMappings(root string) (map[string]image.ID, error) { + mappings := make(map[string]image.ID) + + mfile := filepath.Join(root, migrationFileName) + f, err := os.Open(mfile) + if err != nil && !os.IsNotExist(err) { + return nil, err + } else if err == nil { + err := json.NewDecoder(f).Decode(&mappings) + if err != nil { + f.Close() + return nil, err + } + f.Close() + } + + return mappings, nil +} + +func saveMappings(root string, mappings map[string]image.ID) error { + mfile := filepath.Join(root, migrationFileName) + f, err := os.OpenFile(mfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer f.Close() + if err := json.NewEncoder(f).Encode(mappings); err != nil { + return err + } + return nil +} + +func migrateImages(root string, ls graphIDRegistrar, is image.Store, ms metadata.Store, mappings map[string]image.ID) error { + graphDir := filepath.Join(root, graphDirName) + + dir, err := ioutil.ReadDir(graphDir) + if err != nil { + return err + } + for _, v := range dir { + v1ID := v.Name() + if err := imagev1.ValidateID(v1ID); err != nil { + continue + } + if _, exists := mappings[v1ID]; exists { + continue + } + if err := migrateImage(v1ID, root, ls, is, ms, mappings); err != nil { + continue + } + } + + return nil +} + +func migrateContainers(root string, ls graphIDMounter, is image.Store, imageMappings map[string]image.ID) error { + containersDir := filepath.Join(root, containersDirName) + dir, err := ioutil.ReadDir(containersDir) + if err != nil { + return err + } + for _, v := range dir { + id := v.Name() + + if _, err := os.Stat(filepath.Join(containersDir, id, configFileName)); err == nil { + continue + } + + containerJSON, err := ioutil.ReadFile(filepath.Join(containersDir, id, configFileNameLegacy)) + if err != nil { + logrus.Errorf("migrate container error: %v", err) + continue + } + + var c map[string]*json.RawMessage + if err := json.Unmarshal(containerJSON, &c); err != nil { + logrus.Errorf("migrate container error: %v", err) + continue + } + + imageStrJSON, ok := c["Image"] + if !ok { + return fmt.Errorf("invalid container configuration for %v", id) + } + + var image string + if err := json.Unmarshal([]byte(*imageStrJSON), &image); err != nil { + logrus.Errorf("migrate container error: %v", err) + continue + } + + imageID, ok := imageMappings[image] + if !ok { + logrus.Errorf("image not migrated %v", imageID) // non-fatal error + continue + } + + c["Image"] = rawJSON(imageID) + + containerJSON, err = json.Marshal(c) + if err != nil { + return err + } + + if err := ioutil.WriteFile(filepath.Join(containersDir, id, configFileName), containerJSON, 0600); err != nil { + return err + } + + img, err := is.Get(imageID) + if err != nil { + return err + } + + if err := ls.CreateRWLayerByGraphID(id, id, img.RootFS.ChainID()); err != nil { + logrus.Errorf("migrate container error: %v", err) + continue + } + + logrus.Infof("migrated container %s to point to %s", id, imageID) + + } + return nil +} + +type refAdder interface { + AddTag(ref reference.Named, id image.ID, force bool) error + AddDigest(ref reference.Canonical, id image.ID, force bool) error +} + +func migrateRefs(root, driverName string, rs refAdder, mappings map[string]image.ID) error { + migrationFile := filepath.Join(root, migrationTagsFileName) + if _, err := os.Lstat(migrationFile); !os.IsNotExist(err) { + return err + } + + type repositories struct { + Repositories map[string]map[string]string + } + + var repos repositories + + f, err := os.Open(filepath.Join(root, repositoriesFilePrefixLegacy+driverName)) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + defer f.Close() + if err := json.NewDecoder(f).Decode(&repos); err != nil { + return err + } + + for name, repo := range repos.Repositories { + for tag, id := range repo { + if strongID, exists := mappings[id]; exists { + ref, err := reference.WithName(name) + if err != nil { + logrus.Errorf("migrate tags: invalid name %q, %q", name, err) + continue + } + if dgst, err := digest.ParseDigest(tag); err == nil { + canonical, err := reference.WithDigest(ref, dgst) + if err != nil { + logrus.Errorf("migrate tags: invalid digest %q, %q", dgst, err) + continue + } + if err := rs.AddDigest(canonical, strongID, false); err != nil { + logrus.Errorf("can't migrate digest %q for %q, err: %q", ref.String(), strongID, err) + } + } else { + tagRef, err := reference.WithTag(ref, tag) + if err != nil { + logrus.Errorf("migrate tags: invalid tag %q, %q", tag, err) + continue + } + if err := rs.AddTag(tagRef, strongID, false); err != nil { + logrus.Errorf("can't migrate tag %q for %q, err: %q", ref.String(), strongID, err) + } + } + logrus.Infof("migrated tag %s:%s to point to %s", name, tag, strongID) + } + } + } + + mf, err := os.Create(migrationFile) + if err != nil { + return err + } + mf.Close() + + return nil +} + +func getParent(confDir string) (string, error) { + jsonFile := filepath.Join(confDir, "json") + imageJSON, err := ioutil.ReadFile(jsonFile) + if err != nil { + return "", err + } + var parent struct { + Parent string + ParentID digest.Digest `json:"parent_id"` + } + if err := json.Unmarshal(imageJSON, &parent); err != nil { + return "", err + } + if parent.Parent == "" && parent.ParentID != "" { // v1.9 + parent.Parent = parent.ParentID.Hex() + } + // compatibilityID for parent + parentCompatibilityID, err := ioutil.ReadFile(filepath.Join(confDir, "parent")) + if err == nil && len(parentCompatibilityID) > 0 { + parent.Parent = string(parentCompatibilityID) + } + return parent.Parent, nil +} + +func migrateImage(id, root string, ls graphIDRegistrar, is image.Store, ms metadata.Store, mappings map[string]image.ID) (err error) { + defer func() { + if err != nil { + logrus.Errorf("migration failed for %v, err: %v", id, err) + } + }() + + parent, err := getParent(filepath.Join(root, graphDirName, id)) + if err != nil { + return err + } + + var parentID image.ID + if parent != "" { + var exists bool + if parentID, exists = mappings[parent]; !exists { + if err := migrateImage(parent, root, ls, is, ms, mappings); err != nil { + // todo: fail or allow broken chains? + return err + } + parentID = mappings[parent] + } + } + + rootFS := image.NewRootFS() + var history []image.History + + if parentID != "" { + parentImg, err := is.Get(parentID) + if err != nil { + return err + } + + rootFS = parentImg.RootFS + history = parentImg.History + } + + diffIDData, err := ioutil.ReadFile(filepath.Join(root, graphDirName, id, migrationDiffIDFileName)) + if err != nil { + return err + } + diffID, err := digest.ParseDigest(string(diffIDData)) + if err != nil { + return err + } + + sizeStr, err := ioutil.ReadFile(filepath.Join(root, graphDirName, id, migrationSizeFileName)) + if err != nil { + return err + } + size, err := strconv.ParseInt(string(sizeStr), 10, 64) + if err != nil { + return err + } + + layer, err := ls.RegisterByGraphID(id, rootFS.ChainID(), layer.DiffID(diffID), filepath.Join(root, graphDirName, id, migrationTarDataFileName), size) + if err != nil { + return err + } + logrus.Infof("migrated layer %s to %s", id, layer.DiffID()) + + jsonFile := filepath.Join(root, graphDirName, id, "json") + imageJSON, err := ioutil.ReadFile(jsonFile) + if err != nil { + return err + } + + h, err := imagev1.HistoryFromConfig(imageJSON, false) + if err != nil { + return err + } + history = append(history, h) + + rootFS.Append(layer.DiffID()) + + config, err := imagev1.MakeConfigFromV1Config(imageJSON, rootFS, history) + if err != nil { + return err + } + strongID, err := is.Create(config) + if err != nil { + return err + } + logrus.Infof("migrated image %s to %s", id, strongID) + + if parentID != "" { + if err := is.SetParent(strongID, parentID); err != nil { + return err + } + } + + checksum, err := ioutil.ReadFile(filepath.Join(root, graphDirName, id, "checksum")) + if err == nil { // best effort + dgst, err := digest.ParseDigest(string(checksum)) + if err == nil { + V2MetadataService := metadata.NewV2MetadataService(ms) + V2MetadataService.Add(layer.DiffID(), metadata.V2Metadata{Digest: dgst}) + } + } + _, err = ls.Release(layer) + if err != nil { + return err + } + + mappings[id] = strongID + return +} + +func rawJSON(value interface{}) *json.RawMessage { + jsonval, err := json.Marshal(value) + if err != nil { + return nil + } + return (*json.RawMessage)(&jsonval) +} diff --git a/migrate/v1/migratev1_test.go b/migrate/v1/migratev1_test.go new file mode 100644 index 00000000..73878f11 --- /dev/null +++ b/migrate/v1/migratev1_test.go @@ -0,0 +1,429 @@ +package v1 + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "runtime" + "testing" + + "github.com/docker/distribution/digest" + "github.com/docker/docker/distribution/metadata" + "github.com/docker/docker/image" + "github.com/docker/docker/layer" + "github.com/docker/docker/reference" +) + +func TestMigrateRefs(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "migrate-tags") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + ioutil.WriteFile(filepath.Join(tmpdir, "repositories-generic"), []byte(`{"Repositories":{"busybox":{"latest":"b3ca410aa2c115c05969a7b2c8cf8a9fcf62c1340ed6a601c9ee50df337ec108","sha256:16a2a52884c2a9481ed267c2d46483eac7693b813a63132368ab098a71303f8a":"b3ca410aa2c115c05969a7b2c8cf8a9fcf62c1340ed6a601c9ee50df337ec108"},"registry":{"2":"5d165b8e4b203685301c815e95663231691d383fd5e3d3185d1ce3f8dddead3d","latest":"8d5547a9f329b1d3f93198cd661fb5117e5a96b721c5cf9a2c389e7dd4877128"}}}`), 0600) + + ta := &mockTagAdder{} + err = migrateRefs(tmpdir, "generic", ta, map[string]image.ID{ + "5d165b8e4b203685301c815e95663231691d383fd5e3d3185d1ce3f8dddead3d": image.ID("sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"), + "b3ca410aa2c115c05969a7b2c8cf8a9fcf62c1340ed6a601c9ee50df337ec108": image.ID("sha256:fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9"), + "abcdef3434c115c05969a7b2c8cf8a9fcf62c1340ed6a601c9ee50df337ec108": image.ID("sha256:56434342345ae68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"), + }) + if err != nil { + t.Fatal(err) + } + + expected := map[string]string{ + "busybox:latest": "sha256:fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9", + "busybox@sha256:16a2a52884c2a9481ed267c2d46483eac7693b813a63132368ab098a71303f8a": "sha256:fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9", + "registry:2": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", + } + + if !reflect.DeepEqual(expected, ta.refs) { + t.Fatalf("Invalid migrated tags: expected %q, got %q", expected, ta.refs) + } + + // second migration is no-op + ioutil.WriteFile(filepath.Join(tmpdir, "repositories-generic"), []byte(`{"Repositories":{"busybox":{"latest":"b3ca410aa2c115c05969a7b2c8cf8a9fcf62c1340ed6a601c9ee50df337ec108"`), 0600) + err = migrateRefs(tmpdir, "generic", ta, map[string]image.ID{ + "b3ca410aa2c115c05969a7b2c8cf8a9fcf62c1340ed6a601c9ee50df337ec108": image.ID("sha256:fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9"), + }) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(expected, ta.refs) { + t.Fatalf("Invalid migrated tags: expected %q, got %q", expected, ta.refs) + } +} + +func TestMigrateContainers(t *testing.T) { + // TODO Windows: Figure out why this is failing + if runtime.GOOS == "windows" { + t.Skip("Failing on Windows") + } + tmpdir, err := ioutil.TempDir("", "migrate-containers") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + err = addContainer(tmpdir, `{"State":{"Running":false,"Paused":false,"Restarting":false,"OOMKilled":false,"Dead":false,"Pid":0,"ExitCode":0,"Error":"","StartedAt":"2015-11-10T21:42:40.604267436Z","FinishedAt":"2015-11-10T21:42:41.869265487Z"},"ID":"f780ee3f80e66e9b432a57049597118a66aab8932be88e5628d4c824edbee37c","Created":"2015-11-10T21:42:40.433831551Z","Path":"sh","Args":[],"Config":{"Hostname":"f780ee3f80e6","Domainname":"","User":"","AttachStdin":true,"AttachStdout":true,"AttachStderr":true,"Tty":true,"OpenStdin":true,"StdinOnce":true,"Env":null,"Cmd":["sh"],"Image":"busybox","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{}},"Image":"2c5ac3f849df8627fcf2822727f87c57f38b7129d3604fbc11d861fe856ff093","NetworkSettings":{"Bridge":"","EndpointID":"","Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"HairpinMode":false,"IPAddress":"","IPPrefixLen":0,"IPv6Gateway":"","LinkLocalIPv6Address":"","LinkLocalIPv6PrefixLen":0,"MacAddress":"","NetworkID":"","PortMapping":null,"Ports":null,"SandboxKey":"","SecondaryIPAddresses":null,"SecondaryIPv6Addresses":null},"ResolvConfPath":"/var/lib/docker/containers/f780ee3f80e66e9b432a57049597118a66aab8932be88e5628d4c824edbee37c/resolv.conf","HostnamePath":"/var/lib/docker/containers/f780ee3f80e66e9b432a57049597118a66aab8932be88e5628d4c824edbee37c/hostname","HostsPath":"/var/lib/docker/containers/f780ee3f80e66e9b432a57049597118a66aab8932be88e5628d4c824edbee37c/hosts","LogPath":"/var/lib/docker/containers/f780ee3f80e66e9b432a57049597118a66aab8932be88e5628d4c824edbee37c/f780ee3f80e66e9b432a57049597118a66aab8932be88e5628d4c824edbee37c-json.log","Name":"/determined_euclid","Driver":"overlay","ExecDriver":"native-0.2","MountLabel":"","ProcessLabel":"","RestartCount":0,"UpdateDns":false,"HasBeenStartedBefore":false,"MountPoints":{},"Volumes":{},"VolumesRW":{},"AppArmorProfile":""}`) + if err != nil { + t.Fatal(err) + } + + // container with invalid image + err = addContainer(tmpdir, `{"State":{"Running":false,"Paused":false,"Restarting":false,"OOMKilled":false,"Dead":false,"Pid":0,"ExitCode":0,"Error":"","StartedAt":"2015-11-10T21:42:40.604267436Z","FinishedAt":"2015-11-10T21:42:41.869265487Z"},"ID":"e780ee3f80e66e9b432a57049597118a66aab8932be88e5628d4c824edbee37c","Created":"2015-11-10T21:42:40.433831551Z","Path":"sh","Args":[],"Config":{"Hostname":"f780ee3f80e6","Domainname":"","User":"","AttachStdin":true,"AttachStdout":true,"AttachStderr":true,"Tty":true,"OpenStdin":true,"StdinOnce":true,"Env":null,"Cmd":["sh"],"Image":"busybox","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{}},"Image":"4c5ac3f849df8627fcf2822727f87c57f38b7129d3604fbc11d861fe856ff093","NetworkSettings":{"Bridge":"","EndpointID":"","Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"HairpinMode":false,"IPAddress":"","IPPrefixLen":0,"IPv6Gateway":"","LinkLocalIPv6Address":"","LinkLocalIPv6PrefixLen":0,"MacAddress":"","NetworkID":"","PortMapping":null,"Ports":null,"SandboxKey":"","SecondaryIPAddresses":null,"SecondaryIPv6Addresses":null},"ResolvConfPath":"/var/lib/docker/containers/f780ee3f80e66e9b432a57049597118a66aab8932be88e5628d4c824edbee37c/resolv.conf","HostnamePath":"/var/lib/docker/containers/f780ee3f80e66e9b432a57049597118a66aab8932be88e5628d4c824edbee37c/hostname","HostsPath":"/var/lib/docker/containers/f780ee3f80e66e9b432a57049597118a66aab8932be88e5628d4c824edbee37c/hosts","LogPath":"/var/lib/docker/containers/f780ee3f80e66e9b432a57049597118a66aab8932be88e5628d4c824edbee37c/f780ee3f80e66e9b432a57049597118a66aab8932be88e5628d4c824edbee37c-json.log","Name":"/determined_euclid","Driver":"overlay","ExecDriver":"native-0.2","MountLabel":"","ProcessLabel":"","RestartCount":0,"UpdateDns":false,"HasBeenStartedBefore":false,"MountPoints":{},"Volumes":{},"VolumesRW":{},"AppArmorProfile":""}`) + if err != nil { + t.Fatal(err) + } + + ls := &mockMounter{} + + ifs, err := image.NewFSStoreBackend(filepath.Join(tmpdir, "imagedb")) + if err != nil { + t.Fatal(err) + } + + is, err := image.NewImageStore(ifs, ls) + if err != nil { + t.Fatal(err) + } + + imgID, err := is.Create([]byte(`{"architecture":"amd64","config":{"AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Cmd":["sh"],"Entrypoint":null,"Env":null,"Hostname":"23304fc829f9","Image":"d1592a710ac323612bd786fa8ac20727c58d8a67847e5a65177c594f43919498","Labels":null,"OnBuild":null,"OpenStdin":false,"StdinOnce":false,"Tty":false,"Volumes":null,"WorkingDir":"","Domainname":"","User":""},"container":"349b014153779e30093d94f6df2a43c7a0a164e05aa207389917b540add39b51","container_config":{"AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Cmd":["/bin/sh","-c","#(nop) CMD [\"sh\"]"],"Entrypoint":null,"Env":null,"Hostname":"23304fc829f9","Image":"d1592a710ac323612bd786fa8ac20727c58d8a67847e5a65177c594f43919498","Labels":null,"OnBuild":null,"OpenStdin":false,"StdinOnce":false,"Tty":false,"Volumes":null,"WorkingDir":"","Domainname":"","User":""},"created":"2015-10-31T22:22:55.613815829Z","docker_version":"1.8.2","history":[{"created":"2015-10-31T22:22:54.690851953Z","created_by":"/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /"},{"created":"2015-10-31T22:22:55.613815829Z","created_by":"/bin/sh -c #(nop) CMD [\"sh\"]"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1","sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"]}}`)) + if err != nil { + t.Fatal(err) + } + + err = migrateContainers(tmpdir, ls, is, map[string]image.ID{ + "2c5ac3f849df8627fcf2822727f87c57f38b7129d3604fbc11d861fe856ff093": imgID, + }) + if err != nil { + t.Fatal(err) + } + + expected := []mountInfo{{ + "f780ee3f80e66e9b432a57049597118a66aab8932be88e5628d4c824edbee37c", + "f780ee3f80e66e9b432a57049597118a66aab8932be88e5628d4c824edbee37c", + "sha256:c3191d32a37d7159b2e30830937d2e30268ad6c375a773a8994911a3aba9b93f", + }} + if !reflect.DeepEqual(expected, ls.mounts) { + t.Fatalf("invalid mounts: expected %q, got %q", expected, ls.mounts) + } + + if actual, expected := ls.count, 0; actual != expected { + t.Fatalf("invalid active mounts: expected %d, got %d", expected, actual) + } + + config2, err := ioutil.ReadFile(filepath.Join(tmpdir, "containers", "f780ee3f80e66e9b432a57049597118a66aab8932be88e5628d4c824edbee37c", "config.v2.json")) + if err != nil { + t.Fatal(err) + } + var config struct{ Image string } + err = json.Unmarshal(config2, &config) + if err != nil { + t.Fatal(err) + } + + if actual, expected := config.Image, string(imgID); actual != expected { + t.Fatalf("invalid image pointer in migrated config: expected %q, got %q", expected, actual) + } + +} + +func TestMigrateImages(t *testing.T) { + // TODO Windows: Figure out why this is failing + if runtime.GOOS == "windows" { + t.Skip("Failing on Windows") + } + tmpdir, err := ioutil.TempDir("", "migrate-images") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + // busybox from 1.9 + id1, err := addImage(tmpdir, `{"architecture":"amd64","config":{"Hostname":"23304fc829f9","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":null,"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"23304fc829f9b9349416f6eb1afec162907eba3a328f51d53a17f8986f865d65","container_config":{"Hostname":"23304fc829f9","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":["/bin/sh","-c","#(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /"],"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"created":"2015-10-31T22:22:54.690851953Z","docker_version":"1.8.2","layer_id":"sha256:55dc925c23d1ed82551fd018c27ac3ee731377b6bad3963a2a4e76e753d70e57","os":"linux"}`, "", "") + if err != nil { + t.Fatal(err) + } + + id2, err := addImage(tmpdir, `{"architecture":"amd64","config":{"Hostname":"23304fc829f9","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":["sh"],"Image":"d1592a710ac323612bd786fa8ac20727c58d8a67847e5a65177c594f43919498","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"349b014153779e30093d94f6df2a43c7a0a164e05aa207389917b540add39b51","container_config":{"Hostname":"23304fc829f9","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":["/bin/sh","-c","#(nop) CMD [\"sh\"]"],"Image":"d1592a710ac323612bd786fa8ac20727c58d8a67847e5a65177c594f43919498","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"created":"2015-10-31T22:22:55.613815829Z","docker_version":"1.8.2","layer_id":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4","os":"linux","parent_id":"sha256:039b63dd2cbaa10d6015ea574392530571ed8d7b174090f032211285a71881d0"}`, id1, "") + if err != nil { + t.Fatal(err) + } + + ls := &mockRegistrar{} + + ifs, err := image.NewFSStoreBackend(filepath.Join(tmpdir, "imagedb")) + if err != nil { + t.Fatal(err) + } + + is, err := image.NewImageStore(ifs, ls) + if err != nil { + t.Fatal(err) + } + + ms, err := metadata.NewFSMetadataStore(filepath.Join(tmpdir, "distribution")) + if err != nil { + t.Fatal(err) + } + mappings := make(map[string]image.ID) + + err = migrateImages(tmpdir, ls, is, ms, mappings) + if err != nil { + t.Fatal(err) + } + + expected := map[string]image.ID{ + id1: image.ID("sha256:ca406eaf9c26898414ff5b7b3a023c33310759d6203be0663dbf1b3a712f432d"), + id2: image.ID("sha256:a488bec94bb96b26a968f913d25ef7d8d204d727ca328b52b4b059c7d03260b6"), + } + + if !reflect.DeepEqual(mappings, expected) { + t.Fatalf("invalid image mappings: expected %q, got %q", expected, mappings) + } + + if actual, expected := ls.count, 2; actual != expected { + t.Fatalf("invalid register count: expected %q, got %q", expected, actual) + } + ls.count = 0 + + // next images are busybox from 1.8.2 + _, err = addImage(tmpdir, `{"id":"17583c7dd0dae6244203b8029733bdb7d17fccbb2b5d93e2b24cf48b8bfd06e2","parent":"d1592a710ac323612bd786fa8ac20727c58d8a67847e5a65177c594f43919498","created":"2015-10-31T22:22:55.613815829Z","container":"349b014153779e30093d94f6df2a43c7a0a164e05aa207389917b540add39b51","container_config":{"Hostname":"23304fc829f9","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":null,"PublishService":"","Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":["/bin/sh","-c","#(nop) CMD [\"sh\"]"],"Image":"d1592a710ac323612bd786fa8ac20727c58d8a67847e5a65177c594f43919498","Volumes":null,"VolumeDriver":"","WorkingDir":"","Entrypoint":null,"NetworkDisabled":false,"MacAddress":"","OnBuild":null,"Labels":null},"docker_version":"1.8.2","config":{"Hostname":"23304fc829f9","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":null,"PublishService":"","Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":["sh"],"Image":"d1592a710ac323612bd786fa8ac20727c58d8a67847e5a65177c594f43919498","Volumes":null,"VolumeDriver":"","WorkingDir":"","Entrypoint":null,"NetworkDisabled":false,"MacAddress":"","OnBuild":null,"Labels":null},"architecture":"amd64","os":"linux","Size":0}`, "", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4") + if err != nil { + t.Fatal(err) + } + + _, err = addImage(tmpdir, `{"id":"d1592a710ac323612bd786fa8ac20727c58d8a67847e5a65177c594f43919498","created":"2015-10-31T22:22:54.690851953Z","container":"23304fc829f9b9349416f6eb1afec162907eba3a328f51d53a17f8986f865d65","container_config":{"Hostname":"23304fc829f9","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":null,"PublishService":"","Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":["/bin/sh","-c","#(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /"],"Image":"","Volumes":null,"VolumeDriver":"","WorkingDir":"","Entrypoint":null,"NetworkDisabled":false,"MacAddress":"","OnBuild":null,"Labels":null},"docker_version":"1.8.2","config":{"Hostname":"23304fc829f9","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":null,"PublishService":"","Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":null,"Image":"","Volumes":null,"VolumeDriver":"","WorkingDir":"","Entrypoint":null,"NetworkDisabled":false,"MacAddress":"","OnBuild":null,"Labels":null},"architecture":"amd64","os":"linux","Size":1108935}`, "", "sha256:55dc925c23d1ed82551fd018c27ac3ee731377b6bad3963a2a4e76e753d70e57") + if err != nil { + t.Fatal(err) + } + + err = migrateImages(tmpdir, ls, is, ms, mappings) + if err != nil { + t.Fatal(err) + } + + expected["d1592a710ac323612bd786fa8ac20727c58d8a67847e5a65177c594f43919498"] = image.ID("sha256:c091bb33854e57e6902b74c08719856d30b5593c7db6143b2b48376b8a588395") + expected["17583c7dd0dae6244203b8029733bdb7d17fccbb2b5d93e2b24cf48b8bfd06e2"] = image.ID("sha256:d963020e755ff2715b936065949472c1f8a6300144b922992a1a421999e71f07") + + if actual, expected := ls.count, 2; actual != expected { + t.Fatalf("invalid register count: expected %q, got %q", expected, actual) + } + + v2MetadataService := metadata.NewV2MetadataService(ms) + receivedMetadata, err := v2MetadataService.GetMetadata(layer.EmptyLayer.DiffID()) + if err != nil { + t.Fatal(err) + } + + expectedMetadata := []metadata.V2Metadata{ + {Digest: digest.Digest("sha256:55dc925c23d1ed82551fd018c27ac3ee731377b6bad3963a2a4e76e753d70e57")}, + {Digest: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")}, + } + + if !reflect.DeepEqual(expectedMetadata, receivedMetadata) { + t.Fatalf("invalid metadata: expected %q, got %q", expectedMetadata, receivedMetadata) + } + +} + +func TestMigrateUnsupported(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "migrate-empty") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + err = os.MkdirAll(filepath.Join(tmpdir, "graph"), 0700) + if err != nil { + t.Fatal(err) + } + + err = Migrate(tmpdir, "generic", nil, nil, nil, nil) + if err != errUnsupported { + t.Fatalf("expected unsupported error, got %q", err) + } +} + +func TestMigrateEmptyDir(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "migrate-empty") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + err = Migrate(tmpdir, "generic", nil, nil, nil, nil) + if err != nil { + t.Fatal(err) + } +} + +func addImage(dest, jsonConfig, parent, checksum string) (string, error) { + var config struct{ ID string } + if err := json.Unmarshal([]byte(jsonConfig), &config); err != nil { + return "", err + } + if config.ID == "" { + b := make([]byte, 32) + rand.Read(b) + config.ID = hex.EncodeToString(b) + } + contDir := filepath.Join(dest, "graph", config.ID) + if err := os.MkdirAll(contDir, 0700); err != nil { + return "", err + } + if err := ioutil.WriteFile(filepath.Join(contDir, "json"), []byte(jsonConfig), 0600); err != nil { + return "", err + } + if checksum != "" { + if err := ioutil.WriteFile(filepath.Join(contDir, "checksum"), []byte(checksum), 0600); err != nil { + return "", err + } + } + if err := ioutil.WriteFile(filepath.Join(contDir, ".migration-diffid"), []byte(layer.EmptyLayer.DiffID()), 0600); err != nil { + return "", err + } + if err := ioutil.WriteFile(filepath.Join(contDir, ".migration-size"), []byte("0"), 0600); err != nil { + return "", err + } + if parent != "" { + if err := ioutil.WriteFile(filepath.Join(contDir, "parent"), []byte(parent), 0600); err != nil { + return "", err + } + } + if checksum != "" { + if err := ioutil.WriteFile(filepath.Join(contDir, "checksum"), []byte(checksum), 0600); err != nil { + return "", err + } + } + return config.ID, nil +} + +func addContainer(dest, jsonConfig string) error { + var config struct{ ID string } + if err := json.Unmarshal([]byte(jsonConfig), &config); err != nil { + return err + } + contDir := filepath.Join(dest, "containers", config.ID) + if err := os.MkdirAll(contDir, 0700); err != nil { + return err + } + if err := ioutil.WriteFile(filepath.Join(contDir, "config.json"), []byte(jsonConfig), 0600); err != nil { + return err + } + return nil +} + +type mockTagAdder struct { + refs map[string]string +} + +func (t *mockTagAdder) AddTag(ref reference.Named, id image.ID, force bool) error { + if t.refs == nil { + t.refs = make(map[string]string) + } + t.refs[ref.String()] = id.String() + return nil +} +func (t *mockTagAdder) AddDigest(ref reference.Canonical, id image.ID, force bool) error { + return t.AddTag(ref, id, force) +} + +type mockRegistrar struct { + layers map[layer.ChainID]*mockLayer + count int +} + +func (r *mockRegistrar) RegisterByGraphID(graphID string, parent layer.ChainID, diffID layer.DiffID, tarDataFile string, size int64) (layer.Layer, error) { + r.count++ + l := &mockLayer{} + if parent != "" { + p, exists := r.layers[parent] + if !exists { + return nil, fmt.Errorf("invalid parent %q", parent) + } + l.parent = p + l.diffIDs = append(l.diffIDs, p.diffIDs...) + } + l.diffIDs = append(l.diffIDs, diffID) + if r.layers == nil { + r.layers = make(map[layer.ChainID]*mockLayer) + } + r.layers[l.ChainID()] = l + return l, nil +} +func (r *mockRegistrar) Release(l layer.Layer) ([]layer.Metadata, error) { + return nil, nil +} +func (r *mockRegistrar) Get(layer.ChainID) (layer.Layer, error) { + return nil, nil +} + +type mountInfo struct { + name, graphID, parent string +} +type mockMounter struct { + mounts []mountInfo + count int +} + +func (r *mockMounter) CreateRWLayerByGraphID(name string, graphID string, parent layer.ChainID) error { + r.mounts = append(r.mounts, mountInfo{name, graphID, string(parent)}) + return nil +} +func (r *mockMounter) Unmount(string) error { + r.count-- + return nil +} +func (r *mockMounter) Get(layer.ChainID) (layer.Layer, error) { + return nil, nil +} + +func (r *mockMounter) Release(layer.Layer) ([]layer.Metadata, error) { + return nil, nil +} + +type mockLayer struct { + diffIDs []layer.DiffID + parent *mockLayer +} + +func (l *mockLayer) TarStream() (io.ReadCloser, error) { + return nil, nil +} + +func (l *mockLayer) ChainID() layer.ChainID { + return layer.CreateChainID(l.diffIDs) +} + +func (l *mockLayer) DiffID() layer.DiffID { + return l.diffIDs[len(l.diffIDs)-1] +} + +func (l *mockLayer) Parent() layer.Layer { + if l.parent == nil { + return nil + } + return l.parent +} + +func (l *mockLayer) Size() (int64, error) { + return 0, nil +} + +func (l *mockLayer) DiffSize() (int64, error) { + return 0, nil +} + +func (l *mockLayer) Metadata() (map[string]string, error) { + return nil, nil +} diff --git a/oci/defaults_linux.go b/oci/defaults_linux.go new file mode 100644 index 00000000..9ee75461 --- /dev/null +++ b/oci/defaults_linux.go @@ -0,0 +1,210 @@ +package oci + +import ( + "os" + "runtime" + + "github.com/opencontainers/specs/specs-go" +) + +func sPtr(s string) *string { return &s } +func rPtr(r rune) *rune { return &r } +func iPtr(i int64) *int64 { return &i } +func u32Ptr(i int64) *uint32 { u := uint32(i); return &u } +func fmPtr(i int64) *os.FileMode { fm := os.FileMode(i); return &fm } + +// DefaultSpec returns default oci spec used by docker. +func DefaultSpec() specs.Spec { + s := specs.Spec{ + Version: specs.Version, + Platform: specs.Platform{ + OS: runtime.GOOS, + Arch: runtime.GOARCH, + }, + } + s.Mounts = []specs.Mount{ + { + Destination: "/proc", + Type: "proc", + Source: "proc", + Options: []string{"nosuid", "noexec", "nodev"}, + }, + { + Destination: "/dev", + Type: "tmpfs", + Source: "tmpfs", + Options: []string{"nosuid", "strictatime", "mode=755"}, + }, + { + Destination: "/dev/pts", + Type: "devpts", + Source: "devpts", + Options: []string{"nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620", "gid=5"}, + }, + { + Destination: "/sys", + Type: "sysfs", + Source: "sysfs", + Options: []string{"nosuid", "noexec", "nodev", "ro"}, + }, + { + Destination: "/sys/fs/cgroup", + Type: "cgroup", + Source: "cgroup", + Options: []string{"ro", "nosuid", "noexec", "nodev"}, + }, + { + Destination: "/dev/mqueue", + Type: "mqueue", + Source: "mqueue", + Options: []string{"nosuid", "noexec", "nodev"}, + }, + } + + s.Process.Capabilities = []string{ + "CAP_CHOWN", + "CAP_DAC_OVERRIDE", + "CAP_FSETID", + "CAP_FOWNER", + "CAP_MKNOD", + "CAP_NET_RAW", + "CAP_SETGID", + "CAP_SETUID", + "CAP_SETFCAP", + "CAP_SETPCAP", + "CAP_NET_BIND_SERVICE", + "CAP_SYS_CHROOT", + "CAP_KILL", + "CAP_AUDIT_WRITE", + } + + s.Linux = specs.Linux{ + MaskedPaths: []string{ + "/proc/kcore", + "/proc/latency_stats", + "/proc/timer_stats", + "/proc/sched_debug", + }, + ReadonlyPaths: []string{ + "/proc/asound", + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger", + }, + Namespaces: []specs.Namespace{ + {Type: "mount"}, + {Type: "network"}, + {Type: "uts"}, + {Type: "pid"}, + {Type: "ipc"}, + }, + Devices: []specs.Device{ + { + Type: "c", + Path: "/dev/zero", + Major: 1, + Minor: 5, + FileMode: fmPtr(0666), + UID: u32Ptr(0), + GID: u32Ptr(0), + }, + { + Type: "c", + Path: "/dev/null", + Major: 1, + Minor: 3, + FileMode: fmPtr(0666), + UID: u32Ptr(0), + GID: u32Ptr(0), + }, + { + Type: "c", + Path: "/dev/urandom", + Major: 1, + Minor: 9, + FileMode: fmPtr(0666), + UID: u32Ptr(0), + GID: u32Ptr(0), + }, + { + Type: "c", + Path: "/dev/random", + Major: 1, + Minor: 8, + FileMode: fmPtr(0666), + UID: u32Ptr(0), + GID: u32Ptr(0), + }, + { + Type: "c", + Path: "/dev/fuse", + Major: 10, + Minor: 229, + FileMode: fmPtr(0666), + UID: u32Ptr(0), + GID: u32Ptr(0), + }, + }, + Resources: &specs.Resources{ + Devices: []specs.DeviceCgroup{ + { + Allow: false, + Access: sPtr("rwm"), + }, + { + Allow: true, + Type: sPtr("c"), + Major: iPtr(1), + Minor: iPtr(5), + Access: sPtr("rwm"), + }, + { + Allow: true, + Type: sPtr("c"), + Major: iPtr(1), + Minor: iPtr(3), + Access: sPtr("rwm"), + }, + { + Allow: true, + Type: sPtr("c"), + Major: iPtr(1), + Minor: iPtr(9), + Access: sPtr("rwm"), + }, + { + Allow: true, + Type: sPtr("c"), + Major: iPtr(1), + Minor: iPtr(8), + Access: sPtr("rwm"), + }, + { + Allow: true, + Type: sPtr("c"), + Major: iPtr(5), + Minor: iPtr(0), + Access: sPtr("rwm"), + }, + { + Allow: true, + Type: sPtr("c"), + Major: iPtr(5), + Minor: iPtr(1), + Access: sPtr("rwm"), + }, + { + Allow: false, + Type: sPtr("c"), + Major: iPtr(10), + Minor: iPtr(229), + Access: sPtr("rwm"), + }, + }, + }, + } + + return s +} diff --git a/oci/defaults_windows.go b/oci/defaults_windows.go new file mode 100644 index 00000000..03dc942e --- /dev/null +++ b/oci/defaults_windows.go @@ -0,0 +1,23 @@ +package oci + +import ( + "runtime" + + "github.com/docker/docker/libcontainerd/windowsoci" +) + +// DefaultSpec returns default spec used by docker. +func DefaultSpec() windowsoci.WindowsSpec { + s := windowsoci.Spec{ + Version: windowsoci.Version, + Platform: windowsoci.Platform{ + OS: runtime.GOOS, + Arch: runtime.GOARCH, + }, + } + + return windowsoci.WindowsSpec{ + Spec: s, + Windows: windowsoci.Windows{}, + } +} diff --git a/opts/hosts.go b/opts/hosts.go new file mode 100644 index 00000000..ad167592 --- /dev/null +++ b/opts/hosts.go @@ -0,0 +1,148 @@ +package opts + +import ( + "fmt" + "net" + "net/url" + "strconv" + "strings" +) + +var ( + // DefaultHTTPPort Default HTTP Port used if only the protocol is provided to -H flag e.g. docker daemon -H tcp:// + // These are the IANA registered port numbers for use with Docker + // see http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=docker + DefaultHTTPPort = 2375 // Default HTTP Port + // DefaultTLSHTTPPort Default HTTP Port used when TLS enabled + DefaultTLSHTTPPort = 2376 // Default TLS encrypted HTTP Port + // DefaultUnixSocket Path for the unix socket. + // Docker daemon by default always listens on the default unix socket + DefaultUnixSocket = "/var/run/docker.sock" + // DefaultTCPHost constant defines the default host string used by docker on Windows + DefaultTCPHost = fmt.Sprintf("tcp://%s:%d", DefaultHTTPHost, DefaultHTTPPort) + // DefaultTLSHost constant defines the default host string used by docker for TLS sockets + DefaultTLSHost = fmt.Sprintf("tcp://%s:%d", DefaultHTTPHost, DefaultTLSHTTPPort) + // DefaultNamedPipe defines the default named pipe used by docker on Windows + DefaultNamedPipe = `//./pipe/docker_engine` +) + +// ValidateHost validates that the specified string is a valid host and returns it. +func ValidateHost(val string) (string, error) { + host := strings.TrimSpace(val) + // The empty string means default and is not handled by parseDockerDaemonHost + if host != "" { + _, err := parseDockerDaemonHost(host) + if err != nil { + return val, err + } + } + // Note: unlike most flag validators, we don't return the mutated value here + // we need to know what the user entered later (using ParseHost) to adjust for tls + return val, nil +} + +// ParseHost and set defaults for a Daemon host string +func ParseHost(defaultToTLS bool, val string) (string, error) { + host := strings.TrimSpace(val) + if host == "" { + if defaultToTLS { + host = DefaultTLSHost + } else { + host = DefaultHost + } + } else { + var err error + host, err = parseDockerDaemonHost(host) + if err != nil { + return val, err + } + } + return host, nil +} + +// parseDockerDaemonHost parses the specified address and returns an address that will be used as the host. +// Depending of the address specified, this may return one of the global Default* strings defined in hosts.go. +func parseDockerDaemonHost(addr string) (string, error) { + addrParts := strings.Split(addr, "://") + if len(addrParts) == 1 && addrParts[0] != "" { + addrParts = []string{"tcp", addrParts[0]} + } + + switch addrParts[0] { + case "tcp": + return parseTCPAddr(addrParts[1], DefaultTCPHost) + case "unix": + return parseSimpleProtoAddr("unix", addrParts[1], DefaultUnixSocket) + case "npipe": + return parseSimpleProtoAddr("npipe", addrParts[1], DefaultNamedPipe) + case "fd": + return addr, nil + default: + return "", fmt.Errorf("Invalid bind address format: %s", addr) + } +} + +// parseSimpleProtoAddr parses and validates that the specified address is a valid +// socket address for simple protocols like unix and npipe. It returns a formatted +// socket address, either using the address parsed from addr, or the contents of +// defaultAddr if addr is a blank string. +func parseSimpleProtoAddr(proto, addr, defaultAddr string) (string, error) { + addr = strings.TrimPrefix(addr, proto+"://") + if strings.Contains(addr, "://") { + return "", fmt.Errorf("Invalid proto, expected %s: %s", proto, addr) + } + if addr == "" { + addr = defaultAddr + } + return fmt.Sprintf("%s://%s", proto, addr), nil +} + +// parseTCPAddr parses and validates that the specified address is a valid TCP +// address. It returns a formatted TCP address, either using the address parsed +// from tryAddr, or the contents of defaultAddr if tryAddr is a blank string. +// tryAddr is expected to have already been Trim()'d +// defaultAddr must be in the full `tcp://host:port` form +func parseTCPAddr(tryAddr string, defaultAddr string) (string, error) { + if tryAddr == "" || tryAddr == "tcp://" { + return defaultAddr, nil + } + addr := strings.TrimPrefix(tryAddr, "tcp://") + if strings.Contains(addr, "://") || addr == "" { + return "", fmt.Errorf("Invalid proto, expected tcp: %s", tryAddr) + } + + defaultAddr = strings.TrimPrefix(defaultAddr, "tcp://") + defaultHost, defaultPort, err := net.SplitHostPort(defaultAddr) + if err != nil { + return "", err + } + // url.Parse fails for trailing colon on IPv6 brackets on Go 1.5, but + // not 1.4. See https://github.com/golang/go/issues/12200 and + // https://github.com/golang/go/issues/6530. + if strings.HasSuffix(addr, "]:") { + addr += defaultPort + } + + u, err := url.Parse("tcp://" + addr) + if err != nil { + return "", err + } + + host, port, err := net.SplitHostPort(u.Host) + if err != nil { + return "", fmt.Errorf("Invalid bind address format: %s", tryAddr) + } + + if host == "" { + host = defaultHost + } + if port == "" { + port = defaultPort + } + p, err := strconv.Atoi(port) + if err != nil && p == 0 { + return "", fmt.Errorf("Invalid bind address format: %s", tryAddr) + } + + return fmt.Sprintf("tcp://%s%s", net.JoinHostPort(host, port), u.Path), nil +} diff --git a/opts/hosts_test.go b/opts/hosts_test.go new file mode 100644 index 00000000..dc527e63 --- /dev/null +++ b/opts/hosts_test.go @@ -0,0 +1,154 @@ +package opts + +import ( + "fmt" + "testing" +) + +func TestParseHost(t *testing.T) { + invalid := []string{ + "anything", + "something with spaces", + "://", + "unknown://", + "tcp://:port", + "tcp://invalid", + "tcp://invalid:port", + } + + valid := map[string]string{ + "": DefaultHost, + " ": DefaultHost, + " ": DefaultHost, + "fd://": "fd://", + "fd://something": "fd://something", + "tcp://host:": fmt.Sprintf("tcp://host:%d", DefaultHTTPPort), + "tcp://": DefaultTCPHost, + "tcp://:2375": fmt.Sprintf("tcp://%s:2375", DefaultHTTPHost), + "tcp://:2376": fmt.Sprintf("tcp://%s:2376", DefaultHTTPHost), + "tcp://0.0.0.0:8080": "tcp://0.0.0.0:8080", + "tcp://192.168.0.0:12000": "tcp://192.168.0.0:12000", + "tcp://192.168:8080": "tcp://192.168:8080", + "tcp://0.0.0.0:1234567890": "tcp://0.0.0.0:1234567890", // yeah it's valid :P + " tcp://:7777/path ": fmt.Sprintf("tcp://%s:7777/path", DefaultHTTPHost), + "tcp://docker.com:2375": "tcp://docker.com:2375", + "unix://": "unix://" + DefaultUnixSocket, + "unix://path/to/socket": "unix://path/to/socket", + "npipe://": "npipe://" + DefaultNamedPipe, + "npipe:////./pipe/foo": "npipe:////./pipe/foo", + } + + for _, value := range invalid { + if _, err := ParseHost(false, value); err == nil { + t.Errorf("Expected an error for %v, got [nil]", value) + } + } + + for value, expected := range valid { + if actual, err := ParseHost(false, value); err != nil || actual != expected { + t.Errorf("Expected for %v [%v], got [%v, %v]", value, expected, actual, err) + } + } +} + +func TestParseDockerDaemonHost(t *testing.T) { + invalids := map[string]string{ + "0.0.0.0": "Invalid bind address format: 0.0.0.0", + "tcp:a.b.c.d": "Invalid bind address format: tcp:a.b.c.d", + "tcp:a.b.c.d/path": "Invalid bind address format: tcp:a.b.c.d/path", + "udp://127.0.0.1": "Invalid bind address format: udp://127.0.0.1", + "udp://127.0.0.1:2375": "Invalid bind address format: udp://127.0.0.1:2375", + "tcp://unix:///run/docker.sock": "Invalid bind address format: unix", + " tcp://:7777/path ": "Invalid bind address format: tcp://:7777/path ", + "tcp": "Invalid bind address format: tcp", + "unix": "Invalid bind address format: unix", + "fd": "Invalid bind address format: fd", + "": "Invalid bind address format: ", + } + valids := map[string]string{ + "0.0.0.1:": "tcp://0.0.0.1:2375", + "0.0.0.1:5555": "tcp://0.0.0.1:5555", + "0.0.0.1:5555/path": "tcp://0.0.0.1:5555/path", + "[::1]:": "tcp://[::1]:2375", + "[::1]:5555/path": "tcp://[::1]:5555/path", + "[0:0:0:0:0:0:0:1]:": "tcp://[0:0:0:0:0:0:0:1]:2375", + "[0:0:0:0:0:0:0:1]:5555/path": "tcp://[0:0:0:0:0:0:0:1]:5555/path", + ":6666": fmt.Sprintf("tcp://%s:6666", DefaultHTTPHost), + ":6666/path": fmt.Sprintf("tcp://%s:6666/path", DefaultHTTPHost), + "tcp://": DefaultTCPHost, + "tcp://:7777": fmt.Sprintf("tcp://%s:7777", DefaultHTTPHost), + "tcp://:7777/path": fmt.Sprintf("tcp://%s:7777/path", DefaultHTTPHost), + "unix:///run/docker.sock": "unix:///run/docker.sock", + "unix://": "unix://" + DefaultUnixSocket, + "fd://": "fd://", + "fd://something": "fd://something", + "localhost:": "tcp://localhost:2375", + "localhost:5555": "tcp://localhost:5555", + "localhost:5555/path": "tcp://localhost:5555/path", + } + for invalidAddr, expectedError := range invalids { + if addr, err := parseDockerDaemonHost(invalidAddr); err == nil || err.Error() != expectedError { + t.Errorf("tcp %v address expected error %v return, got %s and addr %v", invalidAddr, expectedError, err, addr) + } + } + for validAddr, expectedAddr := range valids { + if addr, err := parseDockerDaemonHost(validAddr); err != nil || addr != expectedAddr { + t.Errorf("%v -> expected %v, got (%v) addr (%v)", validAddr, expectedAddr, err, addr) + } + } +} + +func TestParseTCP(t *testing.T) { + var ( + defaultHTTPHost = "tcp://127.0.0.1:2376" + ) + invalids := map[string]string{ + "0.0.0.0": "Invalid bind address format: 0.0.0.0", + "tcp:a.b.c.d": "Invalid bind address format: tcp:a.b.c.d", + "tcp:a.b.c.d/path": "Invalid bind address format: tcp:a.b.c.d/path", + "udp://127.0.0.1": "Invalid proto, expected tcp: udp://127.0.0.1", + "udp://127.0.0.1:2375": "Invalid proto, expected tcp: udp://127.0.0.1:2375", + } + valids := map[string]string{ + "": defaultHTTPHost, + "tcp://": defaultHTTPHost, + "0.0.0.1:": "tcp://0.0.0.1:2376", + "0.0.0.1:5555": "tcp://0.0.0.1:5555", + "0.0.0.1:5555/path": "tcp://0.0.0.1:5555/path", + ":6666": "tcp://127.0.0.1:6666", + ":6666/path": "tcp://127.0.0.1:6666/path", + "tcp://:7777": "tcp://127.0.0.1:7777", + "tcp://:7777/path": "tcp://127.0.0.1:7777/path", + "[::1]:": "tcp://[::1]:2376", + "[::1]:5555": "tcp://[::1]:5555", + "[::1]:5555/path": "tcp://[::1]:5555/path", + "[0:0:0:0:0:0:0:1]:": "tcp://[0:0:0:0:0:0:0:1]:2376", + "[0:0:0:0:0:0:0:1]:5555": "tcp://[0:0:0:0:0:0:0:1]:5555", + "[0:0:0:0:0:0:0:1]:5555/path": "tcp://[0:0:0:0:0:0:0:1]:5555/path", + "localhost:": "tcp://localhost:2376", + "localhost:5555": "tcp://localhost:5555", + "localhost:5555/path": "tcp://localhost:5555/path", + } + for invalidAddr, expectedError := range invalids { + if addr, err := parseTCPAddr(invalidAddr, defaultHTTPHost); err == nil || err.Error() != expectedError { + t.Errorf("tcp %v address expected error %v return, got %s and addr %v", invalidAddr, expectedError, err, addr) + } + } + for validAddr, expectedAddr := range valids { + if addr, err := parseTCPAddr(validAddr, defaultHTTPHost); err != nil || addr != expectedAddr { + t.Errorf("%v -> expected %v, got %v and addr %v", validAddr, expectedAddr, err, addr) + } + } +} + +func TestParseInvalidUnixAddrInvalid(t *testing.T) { + if _, err := parseSimpleProtoAddr("unix", "tcp://127.0.0.1", "unix:///var/run/docker.sock"); err == nil || err.Error() != "Invalid proto, expected unix: tcp://127.0.0.1" { + t.Fatalf("Expected an error, got %v", err) + } + if _, err := parseSimpleProtoAddr("unix", "unix://tcp://127.0.0.1", "/var/run/docker.sock"); err == nil || err.Error() != "Invalid proto, expected unix: tcp://127.0.0.1" { + t.Fatalf("Expected an error, got %v", err) + } + if v, err := parseSimpleProtoAddr("unix", "", "/var/run/docker.sock"); err != nil || v != "unix:///var/run/docker.sock" { + t.Fatalf("Expected an %v, got %v", v, "unix:///var/run/docker.sock") + } +} diff --git a/opts/hosts_unix.go b/opts/hosts_unix.go new file mode 100644 index 00000000..611407a9 --- /dev/null +++ b/opts/hosts_unix.go @@ -0,0 +1,8 @@ +// +build !windows + +package opts + +import "fmt" + +// DefaultHost constant defines the default host string used by docker on other hosts than Windows +var DefaultHost = fmt.Sprintf("unix://%s", DefaultUnixSocket) diff --git a/opts/hosts_windows.go b/opts/hosts_windows.go new file mode 100644 index 00000000..7c239e00 --- /dev/null +++ b/opts/hosts_windows.go @@ -0,0 +1,6 @@ +// +build windows + +package opts + +// DefaultHost constant defines the default host string used by docker on Windows +var DefaultHost = "npipe://" + DefaultNamedPipe diff --git a/opts/ip.go b/opts/ip.go new file mode 100644 index 00000000..c7b0dc99 --- /dev/null +++ b/opts/ip.go @@ -0,0 +1,42 @@ +package opts + +import ( + "fmt" + "net" +) + +// IPOpt holds an IP. It is used to store values from CLI flags. +type IPOpt struct { + *net.IP +} + +// NewIPOpt creates a new IPOpt from a reference net.IP and a +// string representation of an IP. If the string is not a valid +// IP it will fallback to the specified reference. +func NewIPOpt(ref *net.IP, defaultVal string) *IPOpt { + o := &IPOpt{ + IP: ref, + } + o.Set(defaultVal) + return o +} + +// Set sets an IPv4 or IPv6 address from a given string. If the given +// string is not parseable as an IP address it returns an error. +func (o *IPOpt) Set(val string) error { + ip := net.ParseIP(val) + if ip == nil { + return fmt.Errorf("%s is not an ip address", val) + } + *o.IP = ip + return nil +} + +// String returns the IP address stored in the IPOpt. If stored IP is a +// nil pointer, it returns an empty string. +func (o *IPOpt) String() string { + if *o.IP == nil { + return "" + } + return o.IP.String() +} diff --git a/opts/ip_test.go b/opts/ip_test.go new file mode 100644 index 00000000..1027d84a --- /dev/null +++ b/opts/ip_test.go @@ -0,0 +1,54 @@ +package opts + +import ( + "net" + "testing" +) + +func TestIpOptString(t *testing.T) { + addresses := []string{"", "0.0.0.0"} + var ip net.IP + + for _, address := range addresses { + stringAddress := NewIPOpt(&ip, address).String() + if stringAddress != address { + t.Fatalf("IpOpt string should be `%s`, not `%s`", address, stringAddress) + } + } +} + +func TestNewIpOptInvalidDefaultVal(t *testing.T) { + ip := net.IPv4(127, 0, 0, 1) + defaultVal := "Not an ip" + + ipOpt := NewIPOpt(&ip, defaultVal) + + expected := "127.0.0.1" + if ipOpt.String() != expected { + t.Fatalf("Expected [%v], got [%v]", expected, ipOpt.String()) + } +} + +func TestNewIpOptValidDefaultVal(t *testing.T) { + ip := net.IPv4(127, 0, 0, 1) + defaultVal := "192.168.1.1" + + ipOpt := NewIPOpt(&ip, defaultVal) + + expected := "192.168.1.1" + if ipOpt.String() != expected { + t.Fatalf("Expected [%v], got [%v]", expected, ipOpt.String()) + } +} + +func TestIpOptSetInvalidVal(t *testing.T) { + ip := net.IPv4(127, 0, 0, 1) + ipOpt := &IPOpt{IP: &ip} + + invalidIP := "invalid ip" + expectedError := "invalid ip is not an ip address" + err := ipOpt.Set(invalidIP) + if err == nil || err.Error() != expectedError { + t.Fatalf("Expected an Error with [%v], got [%v]", expectedError, err.Error()) + } +} diff --git a/opts/opts.go b/opts/opts.go new file mode 100644 index 00000000..05aadbe7 --- /dev/null +++ b/opts/opts.go @@ -0,0 +1,242 @@ +package opts + +import ( + "fmt" + "net" + "regexp" + "strings" +) + +var ( + alphaRegexp = regexp.MustCompile(`[a-zA-Z]`) + domainRegexp = regexp.MustCompile(`^(:?(:?[a-zA-Z0-9]|(:?[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]))(:?\.(:?[a-zA-Z0-9]|(:?[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])))*)\.?\s*$`) +) + +// ListOpts holds a list of values and a validation function. +type ListOpts struct { + values *[]string + validator ValidatorFctType +} + +// NewListOpts creates a new ListOpts with the specified validator. +func NewListOpts(validator ValidatorFctType) ListOpts { + var values []string + return *NewListOptsRef(&values, validator) +} + +// NewListOptsRef creates a new ListOpts with the specified values and validator. +func NewListOptsRef(values *[]string, validator ValidatorFctType) *ListOpts { + return &ListOpts{ + values: values, + validator: validator, + } +} + +func (opts *ListOpts) String() string { + return fmt.Sprintf("%v", []string((*opts.values))) +} + +// Set validates if needed the input value and add it to the +// internal slice. +func (opts *ListOpts) Set(value string) error { + if opts.validator != nil { + v, err := opts.validator(value) + if err != nil { + return err + } + value = v + } + (*opts.values) = append((*opts.values), value) + return nil +} + +// Delete removes the specified element from the slice. +func (opts *ListOpts) Delete(key string) { + for i, k := range *opts.values { + if k == key { + (*opts.values) = append((*opts.values)[:i], (*opts.values)[i+1:]...) + return + } + } +} + +// GetMap returns the content of values in a map in order to avoid +// duplicates. +func (opts *ListOpts) GetMap() map[string]struct{} { + ret := make(map[string]struct{}) + for _, k := range *opts.values { + ret[k] = struct{}{} + } + return ret +} + +// GetAll returns the values of slice. +func (opts *ListOpts) GetAll() []string { + return (*opts.values) +} + +// GetAllOrEmpty returns the values of the slice +// or an empty slice when there are no values. +func (opts *ListOpts) GetAllOrEmpty() []string { + v := *opts.values + if v == nil { + return make([]string, 0) + } + return v +} + +// Get checks the existence of the specified key. +func (opts *ListOpts) Get(key string) bool { + for _, k := range *opts.values { + if k == key { + return true + } + } + return false +} + +// Len returns the amount of element in the slice. +func (opts *ListOpts) Len() int { + return len((*opts.values)) +} + +// NamedOption is an interface that list and map options +// with names implement. +type NamedOption interface { + Name() string +} + +// NamedListOpts is a ListOpts with a configuration name. +// This struct is useful to keep reference to the assigned +// field name in the internal configuration struct. +type NamedListOpts struct { + name string + ListOpts +} + +var _ NamedOption = &NamedListOpts{} + +// NewNamedListOptsRef creates a reference to a new NamedListOpts struct. +func NewNamedListOptsRef(name string, values *[]string, validator ValidatorFctType) *NamedListOpts { + return &NamedListOpts{ + name: name, + ListOpts: *NewListOptsRef(values, validator), + } +} + +// Name returns the name of the NamedListOpts in the configuration. +func (o *NamedListOpts) Name() string { + return o.name +} + +//MapOpts holds a map of values and a validation function. +type MapOpts struct { + values map[string]string + validator ValidatorFctType +} + +// Set validates if needed the input value and add it to the +// internal map, by splitting on '='. +func (opts *MapOpts) Set(value string) error { + if opts.validator != nil { + v, err := opts.validator(value) + if err != nil { + return err + } + value = v + } + vals := strings.SplitN(value, "=", 2) + if len(vals) == 1 { + (opts.values)[vals[0]] = "" + } else { + (opts.values)[vals[0]] = vals[1] + } + return nil +} + +// GetAll returns the values of MapOpts as a map. +func (opts *MapOpts) GetAll() map[string]string { + return opts.values +} + +func (opts *MapOpts) String() string { + return fmt.Sprintf("%v", map[string]string((opts.values))) +} + +// NewMapOpts creates a new MapOpts with the specified map of values and a validator. +func NewMapOpts(values map[string]string, validator ValidatorFctType) *MapOpts { + if values == nil { + values = make(map[string]string) + } + return &MapOpts{ + values: values, + validator: validator, + } +} + +// NamedMapOpts is a MapOpts struct with a configuration name. +// This struct is useful to keep reference to the assigned +// field name in the internal configuration struct. +type NamedMapOpts struct { + name string + MapOpts +} + +var _ NamedOption = &NamedMapOpts{} + +// NewNamedMapOpts creates a reference to a new NamedMapOpts struct. +func NewNamedMapOpts(name string, values map[string]string, validator ValidatorFctType) *NamedMapOpts { + return &NamedMapOpts{ + name: name, + MapOpts: *NewMapOpts(values, validator), + } +} + +// Name returns the name of the NamedMapOpts in the configuration. +func (o *NamedMapOpts) Name() string { + return o.name +} + +// ValidatorFctType defines a validator function that returns a validated string and/or an error. +type ValidatorFctType func(val string) (string, error) + +// ValidatorFctListType defines a validator function that returns a validated list of string and/or an error +type ValidatorFctListType func(val string) ([]string, error) + +// ValidateIPAddress validates an Ip address. +func ValidateIPAddress(val string) (string, error) { + var ip = net.ParseIP(strings.TrimSpace(val)) + if ip != nil { + return ip.String(), nil + } + return "", fmt.Errorf("%s is not an ip address", val) +} + +// ValidateDNSSearch validates domain for resolvconf search configuration. +// A zero length domain is represented by a dot (.). +func ValidateDNSSearch(val string) (string, error) { + if val = strings.Trim(val, " "); val == "." { + return val, nil + } + return validateDomain(val) +} + +func validateDomain(val string) (string, error) { + if alphaRegexp.FindString(val) == "" { + return "", fmt.Errorf("%s is not a valid domain", val) + } + ns := domainRegexp.FindSubmatch([]byte(val)) + if len(ns) > 0 && len(ns[1]) < 255 { + return string(ns[1]), nil + } + return "", fmt.Errorf("%s is not a valid domain", val) +} + +// ValidateLabel validates that the specified string is a valid label, and returns it. +// Labels are in the form on key=value. +func ValidateLabel(val string) (string, error) { + if strings.Count(val, "=") < 1 { + return "", fmt.Errorf("bad attribute format: %s", val) + } + return val, nil +} diff --git a/opts/opts_test.go b/opts/opts_test.go new file mode 100644 index 00000000..9f41e478 --- /dev/null +++ b/opts/opts_test.go @@ -0,0 +1,232 @@ +package opts + +import ( + "fmt" + "strings" + "testing" +) + +func TestValidateIPAddress(t *testing.T) { + if ret, err := ValidateIPAddress(`1.2.3.4`); err != nil || ret == "" { + t.Fatalf("ValidateIPAddress(`1.2.3.4`) got %s %s", ret, err) + } + + if ret, err := ValidateIPAddress(`127.0.0.1`); err != nil || ret == "" { + t.Fatalf("ValidateIPAddress(`127.0.0.1`) got %s %s", ret, err) + } + + if ret, err := ValidateIPAddress(`::1`); err != nil || ret == "" { + t.Fatalf("ValidateIPAddress(`::1`) got %s %s", ret, err) + } + + if ret, err := ValidateIPAddress(`127`); err == nil || ret != "" { + t.Fatalf("ValidateIPAddress(`127`) got %s %s", ret, err) + } + + if ret, err := ValidateIPAddress(`random invalid string`); err == nil || ret != "" { + t.Fatalf("ValidateIPAddress(`random invalid string`) got %s %s", ret, err) + } + +} + +func TestMapOpts(t *testing.T) { + tmpMap := make(map[string]string) + o := NewMapOpts(tmpMap, logOptsValidator) + o.Set("max-size=1") + if o.String() != "map[max-size:1]" { + t.Errorf("%s != [map[max-size:1]", o.String()) + } + + o.Set("max-file=2") + if len(tmpMap) != 2 { + t.Errorf("map length %d != 2", len(tmpMap)) + } + + if tmpMap["max-file"] != "2" { + t.Errorf("max-file = %s != 2", tmpMap["max-file"]) + } + + if tmpMap["max-size"] != "1" { + t.Errorf("max-size = %s != 1", tmpMap["max-size"]) + } + if o.Set("dummy-val=3") == nil { + t.Errorf("validator is not being called") + } +} + +func TestListOptsWithoutValidator(t *testing.T) { + o := NewListOpts(nil) + o.Set("foo") + if o.String() != "[foo]" { + t.Errorf("%s != [foo]", o.String()) + } + o.Set("bar") + if o.Len() != 2 { + t.Errorf("%d != 2", o.Len()) + } + o.Set("bar") + if o.Len() != 3 { + t.Errorf("%d != 3", o.Len()) + } + if !o.Get("bar") { + t.Error("o.Get(\"bar\") == false") + } + if o.Get("baz") { + t.Error("o.Get(\"baz\") == true") + } + o.Delete("foo") + if o.String() != "[bar bar]" { + t.Errorf("%s != [bar bar]", o.String()) + } + listOpts := o.GetAll() + if len(listOpts) != 2 || listOpts[0] != "bar" || listOpts[1] != "bar" { + t.Errorf("Expected [[bar bar]], got [%v]", listOpts) + } + mapListOpts := o.GetMap() + if len(mapListOpts) != 1 { + t.Errorf("Expected [map[bar:{}]], got [%v]", mapListOpts) + } + +} + +func TestListOptsWithValidator(t *testing.T) { + // Re-using logOptsvalidator (used by MapOpts) + o := NewListOpts(logOptsValidator) + o.Set("foo") + if o.String() != "[]" { + t.Errorf("%s != []", o.String()) + } + o.Set("foo=bar") + if o.String() != "[]" { + t.Errorf("%s != []", o.String()) + } + o.Set("max-file=2") + if o.Len() != 1 { + t.Errorf("%d != 1", o.Len()) + } + if !o.Get("max-file=2") { + t.Error("o.Get(\"max-file=2\") == false") + } + if o.Get("baz") { + t.Error("o.Get(\"baz\") == true") + } + o.Delete("max-file=2") + if o.String() != "[]" { + t.Errorf("%s != []", o.String()) + } +} + +func TestValidateDNSSearch(t *testing.T) { + valid := []string{ + `.`, + `a`, + `a.`, + `1.foo`, + `17.foo`, + `foo.bar`, + `foo.bar.baz`, + `foo.bar.`, + `foo.bar.baz`, + `foo1.bar2`, + `foo1.bar2.baz`, + `1foo.2bar.`, + `1foo.2bar.baz`, + `foo-1.bar-2`, + `foo-1.bar-2.baz`, + `foo-1.bar-2.`, + `foo-1.bar-2.baz`, + `1-foo.2-bar`, + `1-foo.2-bar.baz`, + `1-foo.2-bar.`, + `1-foo.2-bar.baz`, + } + + invalid := []string{ + ``, + ` `, + ` `, + `17`, + `17.`, + `.17`, + `17-.`, + `17-.foo`, + `.foo`, + `foo-.bar`, + `-foo.bar`, + `foo.bar-`, + `foo.bar-.baz`, + `foo.-bar`, + `foo.-bar.baz`, + `foo.bar.baz.this.should.fail.on.long.name.beause.it.is.longer.thanisshouldbethis.should.fail.on.long.name.beause.it.is.longer.thanisshouldbethis.should.fail.on.long.name.beause.it.is.longer.thanisshouldbethis.should.fail.on.long.name.beause.it.is.longer.thanisshouldbe`, + } + + for _, domain := range valid { + if ret, err := ValidateDNSSearch(domain); err != nil || ret == "" { + t.Fatalf("ValidateDNSSearch(`"+domain+"`) got %s %s", ret, err) + } + } + + for _, domain := range invalid { + if ret, err := ValidateDNSSearch(domain); err == nil || ret != "" { + t.Fatalf("ValidateDNSSearch(`"+domain+"`) got %s %s", ret, err) + } + } +} + +func TestValidateLabel(t *testing.T) { + if _, err := ValidateLabel("label"); err == nil || err.Error() != "bad attribute format: label" { + t.Fatalf("Expected an error [bad attribute format: label], go %v", err) + } + if actual, err := ValidateLabel("key1=value1"); err != nil || actual != "key1=value1" { + t.Fatalf("Expected [key1=value1], got [%v,%v]", actual, err) + } + // Validate it's working with more than one = + if actual, err := ValidateLabel("key1=value1=value2"); err != nil { + t.Fatalf("Expected [key1=value1=value2], got [%v,%v]", actual, err) + } + // Validate it's working with one more + if actual, err := ValidateLabel("key1=value1=value2=value3"); err != nil { + t.Fatalf("Expected [key1=value1=value2=value2], got [%v,%v]", actual, err) + } +} + +func logOptsValidator(val string) (string, error) { + allowedKeys := map[string]string{"max-size": "1", "max-file": "2"} + vals := strings.Split(val, "=") + if allowedKeys[vals[0]] != "" { + return val, nil + } + return "", fmt.Errorf("invalid key %s", vals[0]) +} + +func TestNamedListOpts(t *testing.T) { + var v []string + o := NewNamedListOptsRef("foo-name", &v, nil) + + o.Set("foo") + if o.String() != "[foo]" { + t.Errorf("%s != [foo]", o.String()) + } + if o.Name() != "foo-name" { + t.Errorf("%s != foo-name", o.Name()) + } + if len(v) != 1 { + t.Errorf("expected foo to be in the values, got %v", v) + } +} + +func TestNamedMapOpts(t *testing.T) { + tmpMap := make(map[string]string) + o := NewNamedMapOpts("max-name", tmpMap, nil) + + o.Set("max-size=1") + if o.String() != "map[max-size:1]" { + t.Errorf("%s != [map[max-size:1]", o.String()) + } + if o.Name() != "max-name" { + t.Errorf("%s != max-name", o.Name()) + } + if _, exist := tmpMap["max-size"]; !exist { + t.Errorf("expected map-size to be in the values, got %v", tmpMap) + } +} diff --git a/opts/opts_unix.go b/opts/opts_unix.go new file mode 100644 index 00000000..f1ce844a --- /dev/null +++ b/opts/opts_unix.go @@ -0,0 +1,6 @@ +// +build !windows + +package opts + +// DefaultHTTPHost Default HTTP Host used if only port is provided to -H flag e.g. docker daemon -H tcp://:8080 +const DefaultHTTPHost = "localhost" diff --git a/opts/opts_windows.go b/opts/opts_windows.go new file mode 100644 index 00000000..2a9e2be7 --- /dev/null +++ b/opts/opts_windows.go @@ -0,0 +1,56 @@ +package opts + +// TODO Windows. Identify bug in GOLang 1.5.1 and/or Windows Server 2016 TP4. +// @jhowardmsft, @swernli. +// +// On Windows, this mitigates a problem with the default options of running +// a docker client against a local docker daemon on TP4. +// +// What was found that if the default host is "localhost", even if the client +// (and daemon as this is local) is not physically on a network, and the DNS +// cache is flushed (ipconfig /flushdns), then the client will pause for +// exactly one second when connecting to the daemon for calls. For example +// using docker run windowsservercore cmd, the CLI will send a create followed +// by an attach. You see the delay between the attach finishing and the attach +// being seen by the daemon. +// +// Here's some daemon debug logs with additional debug spew put in. The +// AfterWriteJSON log is the very last thing the daemon does as part of the +// create call. The POST /attach is the second CLI call. Notice the second +// time gap. +// +// time="2015-11-06T13:38:37.259627400-08:00" level=debug msg="After createRootfs" +// time="2015-11-06T13:38:37.263626300-08:00" level=debug msg="After setHostConfig" +// time="2015-11-06T13:38:37.267631200-08:00" level=debug msg="before createContainerPl...." +// time="2015-11-06T13:38:37.271629500-08:00" level=debug msg=ToDiskLocking.... +// time="2015-11-06T13:38:37.275643200-08:00" level=debug msg="loggin event...." +// time="2015-11-06T13:38:37.277627600-08:00" level=debug msg="logged event...." +// time="2015-11-06T13:38:37.279631800-08:00" level=debug msg="In defer func" +// time="2015-11-06T13:38:37.282628100-08:00" level=debug msg="After daemon.create" +// time="2015-11-06T13:38:37.286651700-08:00" level=debug msg="return 2" +// time="2015-11-06T13:38:37.289629500-08:00" level=debug msg="Returned from daemon.ContainerCreate" +// time="2015-11-06T13:38:37.311629100-08:00" level=debug msg="After WriteJSON" +// ... 1 second gap here.... +// time="2015-11-06T13:38:38.317866200-08:00" level=debug msg="Calling POST /v1.22/containers/984758282b842f779e805664b2c95d563adc9a979c8a3973e68c807843ee4757/attach" +// time="2015-11-06T13:38:38.326882500-08:00" level=info msg="POST /v1.22/containers/984758282b842f779e805664b2c95d563adc9a979c8a3973e68c807843ee4757/attach?stderr=1&stdin=1&stdout=1&stream=1" +// +// We suspect this is either a bug introduced in GOLang 1.5.1, or that a change +// in GOLang 1.5.1 (from 1.4.3) is exposing a bug in Windows TP4. In theory, +// the Windows networking stack is supposed to resolve "localhost" internally, +// without hitting DNS, or even reading the hosts file (which is why localhost +// is commented out in the hosts file on Windows). +// +// We have validated that working around this using the actual IPv4 localhost +// address does not cause the delay. +// +// This does not occur with the docker client built with 1.4.3 on the same +// Windows TP4 build, regardless of whether the daemon is built using 1.5.1 +// or 1.4.3. It does not occur on Linux. We also verified we see the same thing +// on a cross-compiled Windows binary (from Linux). +// +// Final note: This is a mitigation, not a 'real' fix. It is still susceptible +// to the delay in TP4 if a user were to do 'docker run -H=tcp://localhost:2375...' +// explicitly. + +// DefaultHTTPHost Default HTTP Host used if only port is provided to -H flag e.g. docker daemon -H tcp://:8080 +const DefaultHTTPHost = "127.0.0.1" diff --git a/pkg/README.md b/pkg/README.md new file mode 100644 index 00000000..c4b78a8a --- /dev/null +++ b/pkg/README.md @@ -0,0 +1,11 @@ +pkg/ is a collection of utility packages used by the Docker project without being specific to its internals. + +Utility packages are kept separate from the docker core codebase to keep it as small and concise as possible. +If some utilities grow larger and their APIs stabilize, they may be moved to their own repository under the +Docker organization, to facilitate re-use by other projects. However that is not the priority. + +The directory `pkg` is named after the same directory in the camlistore project. Since Brad is a core +Go maintainer, we thought it made sense to copy his methods for organizing Go code :) Thanks Brad! + +Because utility packages are small and neatly separated from the rest of the codebase, they are a good +place to start for aspiring maintainers and contributors. Get in touch if you want to help maintain them! diff --git a/pkg/aaparser/aaparser.go b/pkg/aaparser/aaparser.go new file mode 100644 index 00000000..507298f4 --- /dev/null +++ b/pkg/aaparser/aaparser.go @@ -0,0 +1,92 @@ +// Package aaparser is a convenience package interacting with `apparmor_parser`. +package aaparser + +import ( + "fmt" + "os/exec" + "path/filepath" + "strconv" + "strings" +) + +const ( + binary = "apparmor_parser" +) + +// GetVersion returns the major and minor version of apparmor_parser. +func GetVersion() (int, error) { + output, err := cmd("", "--version") + if err != nil { + return -1, err + } + + return parseVersion(output) +} + +// LoadProfile runs `apparmor_parser -r -W` on a specified apparmor profile to +// replace and write it to disk. +func LoadProfile(profilePath string) error { + _, err := cmd(filepath.Dir(profilePath), "-r", "-W", filepath.Base(profilePath)) + if err != nil { + return err + } + return nil +} + +// cmd runs `apparmor_parser` with the passed arguments. +func cmd(dir string, arg ...string) (string, error) { + c := exec.Command(binary, arg...) + c.Dir = dir + + output, err := c.CombinedOutput() + if err != nil { + return "", fmt.Errorf("running `%s %s` failed with output: %s\nerror: %v", c.Path, strings.Join(c.Args, " "), string(output), err) + } + + return string(output), nil +} + +// parseVersion takes the output from `apparmor_parser --version` and returns +// a representation of the {major, minor, patch} version as a single number of +// the form MMmmPPP {major, minor, patch}. +func parseVersion(output string) (int, error) { + // output is in the form of the following: + // AppArmor parser version 2.9.1 + // Copyright (C) 1999-2008 Novell Inc. + // Copyright 2009-2012 Canonical Ltd. + + lines := strings.SplitN(output, "\n", 2) + words := strings.Split(lines[0], " ") + version := words[len(words)-1] + + // split by major minor version + v := strings.Split(version, ".") + if len(v) == 0 || len(v) > 3 { + return -1, fmt.Errorf("parsing version failed for output: `%s`", output) + } + + // Default the versions to 0. + var majorVersion, minorVersion, patchLevel int + + majorVersion, err := strconv.Atoi(v[0]) + if err != nil { + return -1, err + } + + if len(v) > 1 { + minorVersion, err = strconv.Atoi(v[1]) + if err != nil { + return -1, err + } + } + if len(v) > 2 { + patchLevel, err = strconv.Atoi(v[2]) + if err != nil { + return -1, err + } + } + + // major*10^5 + minor*10^3 + patch*10^0 + numericVersion := majorVersion*1e5 + minorVersion*1e3 + patchLevel + return numericVersion, nil +} diff --git a/pkg/aaparser/aaparser_test.go b/pkg/aaparser/aaparser_test.go new file mode 100644 index 00000000..69bc8d2f --- /dev/null +++ b/pkg/aaparser/aaparser_test.go @@ -0,0 +1,73 @@ +package aaparser + +import ( + "testing" +) + +type versionExpected struct { + output string + version int +} + +func TestParseVersion(t *testing.T) { + versions := []versionExpected{ + { + output: `AppArmor parser version 2.10 +Copyright (C) 1999-2008 Novell Inc. +Copyright 2009-2012 Canonical Ltd. + +`, + version: 210000, + }, + { + output: `AppArmor parser version 2.8 +Copyright (C) 1999-2008 Novell Inc. +Copyright 2009-2012 Canonical Ltd. + +`, + version: 208000, + }, + { + output: `AppArmor parser version 2.20 +Copyright (C) 1999-2008 Novell Inc. +Copyright 2009-2012 Canonical Ltd. + +`, + version: 220000, + }, + { + output: `AppArmor parser version 2.05 +Copyright (C) 1999-2008 Novell Inc. +Copyright 2009-2012 Canonical Ltd. + +`, + version: 205000, + }, + { + output: `AppArmor parser version 2.9.95 +Copyright (C) 1999-2008 Novell Inc. +Copyright 2009-2012 Canonical Ltd. + +`, + version: 209095, + }, + { + output: `AppArmor parser version 3.14.159 +Copyright (C) 1999-2008 Novell Inc. +Copyright 2009-2012 Canonical Ltd. + +`, + version: 314159, + }, + } + + for _, v := range versions { + version, err := parseVersion(v.output) + if err != nil { + t.Fatalf("expected error to be nil for %#v, got: %v", v, err) + } + if version != v.version { + t.Fatalf("expected version to be %d, was %d, for: %#v\n", v.version, version, v) + } + } +} diff --git a/pkg/archive/README.md b/pkg/archive/README.md new file mode 100644 index 00000000..7307d969 --- /dev/null +++ b/pkg/archive/README.md @@ -0,0 +1 @@ +This code provides helper functions for dealing with archive files. diff --git a/pkg/archive/archive.go b/pkg/archive/archive.go new file mode 100644 index 00000000..47c56389 --- /dev/null +++ b/pkg/archive/archive.go @@ -0,0 +1,1087 @@ +package archive + +import ( + "archive/tar" + "bufio" + "bytes" + "compress/bzip2" + "compress/gzip" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "syscall" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/fileutils" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/pools" + "github.com/docker/docker/pkg/promise" + "github.com/docker/docker/pkg/system" +) + +type ( + // Archive is a type of io.ReadCloser which has two interfaces Read and Closer. + Archive io.ReadCloser + // Reader is a type of io.Reader. + Reader io.Reader + // Compression is the state represents if compressed or not. + Compression int + // TarChownOptions wraps the chown options UID and GID. + TarChownOptions struct { + UID, GID int + } + // TarOptions wraps the tar options. + TarOptions struct { + IncludeFiles []string + ExcludePatterns []string + Compression Compression + NoLchown bool + UIDMaps []idtools.IDMap + GIDMaps []idtools.IDMap + ChownOpts *TarChownOptions + IncludeSourceDir bool + // When unpacking, specifies whether overwriting a directory with a + // non-directory is allowed and vice versa. + NoOverwriteDirNonDir bool + // For each include when creating an archive, the included name will be + // replaced with the matching name from this map. + RebaseNames map[string]string + } + + // Archiver allows the reuse of most utility functions of this package + // with a pluggable Untar function. Also, to facilitate the passing of + // specific id mappings for untar, an archiver can be created with maps + // which will then be passed to Untar operations + Archiver struct { + Untar func(io.Reader, string, *TarOptions) error + UIDMaps []idtools.IDMap + GIDMaps []idtools.IDMap + } + + // breakoutError is used to differentiate errors related to breaking out + // When testing archive breakout in the unit tests, this error is expected + // in order for the test to pass. + breakoutError error +) + +var ( + // ErrNotImplemented is the error message of function not implemented. + ErrNotImplemented = errors.New("Function not implemented") + defaultArchiver = &Archiver{Untar: Untar, UIDMaps: nil, GIDMaps: nil} +) + +const ( + // HeaderSize is the size in bytes of a tar header + HeaderSize = 512 +) + +const ( + // Uncompressed represents the uncompressed. + Uncompressed Compression = iota + // Bzip2 is bzip2 compression algorithm. + Bzip2 + // Gzip is gzip compression algorithm. + Gzip + // Xz is xz compression algorithm. + Xz +) + +// IsArchive checks for the magic bytes of a tar or any supported compression +// algorithm. +func IsArchive(header []byte) bool { + compression := DetectCompression(header) + if compression != Uncompressed { + return true + } + r := tar.NewReader(bytes.NewBuffer(header)) + _, err := r.Next() + return err == nil +} + +// IsArchivePath checks if the (possibly compressed) file at the given path +// starts with a tar file header. +func IsArchivePath(path string) bool { + file, err := os.Open(path) + if err != nil { + return false + } + defer file.Close() + rdr, err := DecompressStream(file) + if err != nil { + return false + } + r := tar.NewReader(rdr) + _, err = r.Next() + return err == nil +} + +// DetectCompression detects the compression algorithm of the source. +func DetectCompression(source []byte) Compression { + for compression, m := range map[Compression][]byte{ + Bzip2: {0x42, 0x5A, 0x68}, + Gzip: {0x1F, 0x8B, 0x08}, + Xz: {0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00}, + } { + if len(source) < len(m) { + logrus.Debugf("Len too short") + continue + } + if bytes.Compare(m, source[:len(m)]) == 0 { + return compression + } + } + return Uncompressed +} + +func xzDecompress(archive io.Reader) (io.ReadCloser, <-chan struct{}, error) { + args := []string{"xz", "-d", "-c", "-q"} + + return cmdStream(exec.Command(args[0], args[1:]...), archive) +} + +// DecompressStream decompress the archive and returns a ReaderCloser with the decompressed archive. +func DecompressStream(archive io.Reader) (io.ReadCloser, error) { + p := pools.BufioReader32KPool + buf := p.Get(archive) + bs, err := buf.Peek(10) + if err != nil && err != io.EOF { + // Note: we'll ignore any io.EOF error because there are some odd + // cases where the layer.tar file will be empty (zero bytes) and + // that results in an io.EOF from the Peek() call. So, in those + // cases we'll just treat it as a non-compressed stream and + // that means just create an empty layer. + // See Issue 18170 + return nil, err + } + + compression := DetectCompression(bs) + switch compression { + case Uncompressed: + readBufWrapper := p.NewReadCloserWrapper(buf, buf) + return readBufWrapper, nil + case Gzip: + gzReader, err := gzip.NewReader(buf) + if err != nil { + return nil, err + } + readBufWrapper := p.NewReadCloserWrapper(buf, gzReader) + return readBufWrapper, nil + case Bzip2: + bz2Reader := bzip2.NewReader(buf) + readBufWrapper := p.NewReadCloserWrapper(buf, bz2Reader) + return readBufWrapper, nil + case Xz: + xzReader, chdone, err := xzDecompress(buf) + if err != nil { + return nil, err + } + readBufWrapper := p.NewReadCloserWrapper(buf, xzReader) + return ioutils.NewReadCloserWrapper(readBufWrapper, func() error { + <-chdone + return readBufWrapper.Close() + }), nil + default: + return nil, fmt.Errorf("Unsupported compression format %s", (&compression).Extension()) + } +} + +// CompressStream compresses the dest with specified compression algorithm. +func CompressStream(dest io.WriteCloser, compression Compression) (io.WriteCloser, error) { + p := pools.BufioWriter32KPool + buf := p.Get(dest) + switch compression { + case Uncompressed: + writeBufWrapper := p.NewWriteCloserWrapper(buf, buf) + return writeBufWrapper, nil + case Gzip: + gzWriter := gzip.NewWriter(dest) + writeBufWrapper := p.NewWriteCloserWrapper(buf, gzWriter) + return writeBufWrapper, nil + case Bzip2, Xz: + // archive/bzip2 does not support writing, and there is no xz support at all + // However, this is not a problem as docker only currently generates gzipped tars + return nil, fmt.Errorf("Unsupported compression format %s", (&compression).Extension()) + default: + return nil, fmt.Errorf("Unsupported compression format %s", (&compression).Extension()) + } +} + +// Extension returns the extension of a file that uses the specified compression algorithm. +func (compression *Compression) Extension() string { + switch *compression { + case Uncompressed: + return "tar" + case Bzip2: + return "tar.bz2" + case Gzip: + return "tar.gz" + case Xz: + return "tar.xz" + } + return "" +} + +type tarAppender struct { + TarWriter *tar.Writer + Buffer *bufio.Writer + + // for hardlink mapping + SeenFiles map[uint64]string + UIDMaps []idtools.IDMap + GIDMaps []idtools.IDMap +} + +// canonicalTarName provides a platform-independent and consistent posix-style +//path for files and directories to be archived regardless of the platform. +func canonicalTarName(name string, isDir bool) (string, error) { + name, err := CanonicalTarNameForPath(name) + if err != nil { + return "", err + } + + // suffix with '/' for directories + if isDir && !strings.HasSuffix(name, "/") { + name += "/" + } + return name, nil +} + +func (ta *tarAppender) addTarFile(path, name string) error { + fi, err := os.Lstat(path) + if err != nil { + return err + } + + link := "" + if fi.Mode()&os.ModeSymlink != 0 { + if link, err = os.Readlink(path); err != nil { + return err + } + } + + hdr, err := tar.FileInfoHeader(fi, link) + if err != nil { + return err + } + hdr.Mode = int64(chmodTarEntry(os.FileMode(hdr.Mode))) + + name, err = canonicalTarName(name, fi.IsDir()) + if err != nil { + return fmt.Errorf("tar: cannot canonicalize path: %v", err) + } + hdr.Name = name + + inode, err := setHeaderForSpecialDevice(hdr, ta, name, fi.Sys()) + if err != nil { + return err + } + + // if it's not a directory and has more than 1 link, + // it's hardlinked, so set the type flag accordingly + if !fi.IsDir() && hasHardlinks(fi) { + // a link should have a name that it links too + // and that linked name should be first in the tar archive + if oldpath, ok := ta.SeenFiles[inode]; ok { + hdr.Typeflag = tar.TypeLink + hdr.Linkname = oldpath + hdr.Size = 0 // This Must be here for the writer math to add up! + } else { + ta.SeenFiles[inode] = name + } + } + + capability, _ := system.Lgetxattr(path, "security.capability") + if capability != nil { + hdr.Xattrs = make(map[string]string) + hdr.Xattrs["security.capability"] = string(capability) + } + + //handle re-mapping container ID mappings back to host ID mappings before + //writing tar headers/files. We skip whiteout files because they were written + //by the kernel and already have proper ownership relative to the host + if !strings.HasPrefix(filepath.Base(hdr.Name), WhiteoutPrefix) && (ta.UIDMaps != nil || ta.GIDMaps != nil) { + uid, gid, err := getFileUIDGID(fi.Sys()) + if err != nil { + return err + } + xUID, err := idtools.ToContainer(uid, ta.UIDMaps) + if err != nil { + return err + } + xGID, err := idtools.ToContainer(gid, ta.GIDMaps) + if err != nil { + return err + } + hdr.Uid = xUID + hdr.Gid = xGID + } + + if err := ta.TarWriter.WriteHeader(hdr); err != nil { + return err + } + + if hdr.Typeflag == tar.TypeReg { + file, err := os.Open(path) + if err != nil { + return err + } + + ta.Buffer.Reset(ta.TarWriter) + defer ta.Buffer.Reset(nil) + _, err = io.Copy(ta.Buffer, file) + file.Close() + if err != nil { + return err + } + err = ta.Buffer.Flush() + if err != nil { + return err + } + } + + return nil +} + +func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, Lchown bool, chownOpts *TarChownOptions) error { + // hdr.Mode is in linux format, which we can use for sycalls, + // but for os.Foo() calls we need the mode converted to os.FileMode, + // so use hdrInfo.Mode() (they differ for e.g. setuid bits) + hdrInfo := hdr.FileInfo() + + switch hdr.Typeflag { + case tar.TypeDir: + // Create directory unless it exists as a directory already. + // In that case we just want to merge the two + if fi, err := os.Lstat(path); !(err == nil && fi.IsDir()) { + if err := os.Mkdir(path, hdrInfo.Mode()); err != nil { + return err + } + } + + case tar.TypeReg, tar.TypeRegA: + // Source is regular file + file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, hdrInfo.Mode()) + if err != nil { + return err + } + if _, err := io.Copy(file, reader); err != nil { + file.Close() + return err + } + file.Close() + + case tar.TypeBlock, tar.TypeChar, tar.TypeFifo: + // Handle this is an OS-specific way + if err := handleTarTypeBlockCharFifo(hdr, path); err != nil { + return err + } + + case tar.TypeLink: + targetPath := filepath.Join(extractDir, hdr.Linkname) + // check for hardlink breakout + if !strings.HasPrefix(targetPath, extractDir) { + return breakoutError(fmt.Errorf("invalid hardlink %q -> %q", targetPath, hdr.Linkname)) + } + if err := os.Link(targetPath, path); err != nil { + return err + } + + case tar.TypeSymlink: + // path -> hdr.Linkname = targetPath + // e.g. /extractDir/path/to/symlink -> ../2/file = /extractDir/path/2/file + targetPath := filepath.Join(filepath.Dir(path), hdr.Linkname) + + // the reason we don't need to check symlinks in the path (with FollowSymlinkInScope) is because + // that symlink would first have to be created, which would be caught earlier, at this very check: + if !strings.HasPrefix(targetPath, extractDir) { + return breakoutError(fmt.Errorf("invalid symlink %q -> %q", path, hdr.Linkname)) + } + if err := os.Symlink(hdr.Linkname, path); err != nil { + return err + } + + case tar.TypeXGlobalHeader: + logrus.Debugf("PAX Global Extended Headers found and ignored") + return nil + + default: + return fmt.Errorf("Unhandled tar header type %d\n", hdr.Typeflag) + } + + // Lchown is not supported on Windows. + if Lchown && runtime.GOOS != "windows" { + if chownOpts == nil { + chownOpts = &TarChownOptions{UID: hdr.Uid, GID: hdr.Gid} + } + if err := os.Lchown(path, chownOpts.UID, chownOpts.GID); err != nil { + return err + } + } + + for key, value := range hdr.Xattrs { + if err := system.Lsetxattr(path, key, []byte(value), 0); err != nil { + return err + } + } + + // There is no LChmod, so ignore mode for symlink. Also, this + // must happen after chown, as that can modify the file mode + if err := handleLChmod(hdr, path, hdrInfo); err != nil { + return err + } + + aTime := hdr.AccessTime + if aTime.Before(hdr.ModTime) { + // Last access time should never be before last modified time. + aTime = hdr.ModTime + } + + // system.Chtimes doesn't support a NOFOLLOW flag atm + if hdr.Typeflag == tar.TypeLink { + if fi, err := os.Lstat(hdr.Linkname); err == nil && (fi.Mode()&os.ModeSymlink == 0) { + if err := system.Chtimes(path, aTime, hdr.ModTime); err != nil { + return err + } + } + } else if hdr.Typeflag != tar.TypeSymlink { + if err := system.Chtimes(path, aTime, hdr.ModTime); err != nil { + return err + } + } else { + ts := []syscall.Timespec{timeToTimespec(aTime), timeToTimespec(hdr.ModTime)} + if err := system.LUtimesNano(path, ts); err != nil && err != system.ErrNotSupportedPlatform { + return err + } + } + return nil +} + +// Tar creates an archive from the directory at `path`, and returns it as a +// stream of bytes. +func Tar(path string, compression Compression) (io.ReadCloser, error) { + return TarWithOptions(path, &TarOptions{Compression: compression}) +} + +// TarWithOptions creates an archive from the directory at `path`, only including files whose relative +// paths are included in `options.IncludeFiles` (if non-nil) or not in `options.ExcludePatterns`. +func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) { + + // Fix the source path to work with long path names. This is a no-op + // on platforms other than Windows. + srcPath = fixVolumePathPrefix(srcPath) + + patterns, patDirs, exceptions, err := fileutils.CleanPatterns(options.ExcludePatterns) + + if err != nil { + return nil, err + } + + pipeReader, pipeWriter := io.Pipe() + + compressWriter, err := CompressStream(pipeWriter, options.Compression) + if err != nil { + return nil, err + } + + go func() { + ta := &tarAppender{ + TarWriter: tar.NewWriter(compressWriter), + Buffer: pools.BufioWriter32KPool.Get(nil), + SeenFiles: make(map[uint64]string), + UIDMaps: options.UIDMaps, + GIDMaps: options.GIDMaps, + } + + defer func() { + // Make sure to check the error on Close. + if err := ta.TarWriter.Close(); err != nil { + logrus.Errorf("Can't close tar writer: %s", err) + } + if err := compressWriter.Close(); err != nil { + logrus.Errorf("Can't close compress writer: %s", err) + } + if err := pipeWriter.Close(); err != nil { + logrus.Errorf("Can't close pipe writer: %s", err) + } + }() + + // this buffer is needed for the duration of this piped stream + defer pools.BufioWriter32KPool.Put(ta.Buffer) + + // In general we log errors here but ignore them because + // during e.g. a diff operation the container can continue + // mutating the filesystem and we can see transient errors + // from this + + stat, err := os.Lstat(srcPath) + if err != nil { + return + } + + if !stat.IsDir() { + // We can't later join a non-dir with any includes because the + // 'walk' will error if "file/." is stat-ed and "file" is not a + // directory. So, we must split the source path and use the + // basename as the include. + if len(options.IncludeFiles) > 0 { + logrus.Warn("Tar: Can't archive a file with includes") + } + + dir, base := SplitPathDirEntry(srcPath) + srcPath = dir + options.IncludeFiles = []string{base} + } + + if len(options.IncludeFiles) == 0 { + options.IncludeFiles = []string{"."} + } + + seen := make(map[string]bool) + + for _, include := range options.IncludeFiles { + rebaseName := options.RebaseNames[include] + + walkRoot := getWalkRoot(srcPath, include) + filepath.Walk(walkRoot, func(filePath string, f os.FileInfo, err error) error { + if err != nil { + logrus.Errorf("Tar: Can't stat file %s to tar: %s", srcPath, err) + return nil + } + + relFilePath, err := filepath.Rel(srcPath, filePath) + if err != nil || (!options.IncludeSourceDir && relFilePath == "." && f.IsDir()) { + // Error getting relative path OR we are looking + // at the source directory path. Skip in both situations. + return nil + } + + if options.IncludeSourceDir && include == "." && relFilePath != "." { + relFilePath = strings.Join([]string{".", relFilePath}, string(filepath.Separator)) + } + + skip := false + + // If "include" is an exact match for the current file + // then even if there's an "excludePatterns" pattern that + // matches it, don't skip it. IOW, assume an explicit 'include' + // is asking for that file no matter what - which is true + // for some files, like .dockerignore and Dockerfile (sometimes) + if include != relFilePath { + skip, err = fileutils.OptimizedMatches(relFilePath, patterns, patDirs) + if err != nil { + logrus.Errorf("Error matching %s: %v", relFilePath, err) + return err + } + } + + if skip { + // If we want to skip this file and its a directory + // then we should first check to see if there's an + // excludes pattern (eg !dir/file) that starts with this + // dir. If so then we can't skip this dir. + + // Its not a dir then so we can just return/skip. + if !f.IsDir() { + return nil + } + + // No exceptions (!...) in patterns so just skip dir + if !exceptions { + return filepath.SkipDir + } + + dirSlash := relFilePath + string(filepath.Separator) + + for _, pat := range patterns { + if pat[0] != '!' { + continue + } + pat = pat[1:] + string(filepath.Separator) + if strings.HasPrefix(pat, dirSlash) { + // found a match - so can't skip this dir + return nil + } + } + + // No matching exclusion dir so just skip dir + return filepath.SkipDir + } + + if seen[relFilePath] { + return nil + } + seen[relFilePath] = true + + // Rename the base resource. + if rebaseName != "" { + var replacement string + if rebaseName != string(filepath.Separator) { + // Special case the root directory to replace with an + // empty string instead so that we don't end up with + // double slashes in the paths. + replacement = rebaseName + } + + relFilePath = strings.Replace(relFilePath, include, replacement, 1) + } + + if err := ta.addTarFile(filePath, relFilePath); err != nil { + logrus.Errorf("Can't add file %s to tar: %s", filePath, err) + // if pipe is broken, stop writting tar stream to it + if err == io.ErrClosedPipe { + return err + } + } + return nil + }) + } + }() + + return pipeReader, nil +} + +// Unpack unpacks the decompressedArchive to dest with options. +func Unpack(decompressedArchive io.Reader, dest string, options *TarOptions) error { + tr := tar.NewReader(decompressedArchive) + trBuf := pools.BufioReader32KPool.Get(nil) + defer pools.BufioReader32KPool.Put(trBuf) + + var dirs []*tar.Header + remappedRootUID, remappedRootGID, err := idtools.GetRootUIDGID(options.UIDMaps, options.GIDMaps) + if err != nil { + return err + } + + // Iterate through the files in the archive. +loop: + for { + hdr, err := tr.Next() + if err == io.EOF { + // end of tar archive + break + } + if err != nil { + return err + } + + // Normalize name, for safety and for a simple is-root check + // This keeps "../" as-is, but normalizes "/../" to "/". Or Windows: + // This keeps "..\" as-is, but normalizes "\..\" to "\". + hdr.Name = filepath.Clean(hdr.Name) + + for _, exclude := range options.ExcludePatterns { + if strings.HasPrefix(hdr.Name, exclude) { + continue loop + } + } + + // After calling filepath.Clean(hdr.Name) above, hdr.Name will now be in + // the filepath format for the OS on which the daemon is running. Hence + // the check for a slash-suffix MUST be done in an OS-agnostic way. + if !strings.HasSuffix(hdr.Name, string(os.PathSeparator)) { + // Not the root directory, ensure that the parent directory exists + parent := filepath.Dir(hdr.Name) + parentPath := filepath.Join(dest, parent) + if _, err := os.Lstat(parentPath); err != nil && os.IsNotExist(err) { + err = idtools.MkdirAllNewAs(parentPath, 0777, remappedRootUID, remappedRootGID) + if err != nil { + return err + } + } + } + + path := filepath.Join(dest, hdr.Name) + rel, err := filepath.Rel(dest, path) + if err != nil { + return err + } + if strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { + return breakoutError(fmt.Errorf("%q is outside of %q", hdr.Name, dest)) + } + + // If path exits we almost always just want to remove and replace it + // The only exception is when it is a directory *and* the file from + // the layer is also a directory. Then we want to merge them (i.e. + // just apply the metadata from the layer). + if fi, err := os.Lstat(path); err == nil { + if options.NoOverwriteDirNonDir && fi.IsDir() && hdr.Typeflag != tar.TypeDir { + // If NoOverwriteDirNonDir is true then we cannot replace + // an existing directory with a non-directory from the archive. + return fmt.Errorf("cannot overwrite directory %q with non-directory %q", path, dest) + } + + if options.NoOverwriteDirNonDir && !fi.IsDir() && hdr.Typeflag == tar.TypeDir { + // If NoOverwriteDirNonDir is true then we cannot replace + // an existing non-directory with a directory from the archive. + return fmt.Errorf("cannot overwrite non-directory %q with directory %q", path, dest) + } + + if fi.IsDir() && hdr.Name == "." { + continue + } + + if !(fi.IsDir() && hdr.Typeflag == tar.TypeDir) { + if err := os.RemoveAll(path); err != nil { + return err + } + } + } + trBuf.Reset(tr) + + // if the options contain a uid & gid maps, convert header uid/gid + // entries using the maps such that lchown sets the proper mapped + // uid/gid after writing the file. We only perform this mapping if + // the file isn't already owned by the remapped root UID or GID, as + // that specific uid/gid has no mapping from container -> host, and + // those files already have the proper ownership for inside the + // container. + if hdr.Uid != remappedRootUID { + xUID, err := idtools.ToHost(hdr.Uid, options.UIDMaps) + if err != nil { + return err + } + hdr.Uid = xUID + } + if hdr.Gid != remappedRootGID { + xGID, err := idtools.ToHost(hdr.Gid, options.GIDMaps) + if err != nil { + return err + } + hdr.Gid = xGID + } + + if err := createTarFile(path, dest, hdr, trBuf, !options.NoLchown, options.ChownOpts); err != nil { + return err + } + + // Directory mtimes must be handled at the end to avoid further + // file creation in them to modify the directory mtime + if hdr.Typeflag == tar.TypeDir { + dirs = append(dirs, hdr) + } + } + + for _, hdr := range dirs { + path := filepath.Join(dest, hdr.Name) + + if err := system.Chtimes(path, hdr.AccessTime, hdr.ModTime); err != nil { + return err + } + } + return nil +} + +// Untar reads a stream of bytes from `archive`, parses it as a tar archive, +// and unpacks it into the directory at `dest`. +// The archive may be compressed with one of the following algorithms: +// identity (uncompressed), gzip, bzip2, xz. +// FIXME: specify behavior when target path exists vs. doesn't exist. +func Untar(tarArchive io.Reader, dest string, options *TarOptions) error { + return untarHandler(tarArchive, dest, options, true) +} + +// UntarUncompressed reads a stream of bytes from `archive`, parses it as a tar archive, +// and unpacks it into the directory at `dest`. +// The archive must be an uncompressed stream. +func UntarUncompressed(tarArchive io.Reader, dest string, options *TarOptions) error { + return untarHandler(tarArchive, dest, options, false) +} + +// Handler for teasing out the automatic decompression +func untarHandler(tarArchive io.Reader, dest string, options *TarOptions, decompress bool) error { + if tarArchive == nil { + return fmt.Errorf("Empty archive") + } + dest = filepath.Clean(dest) + if options == nil { + options = &TarOptions{} + } + if options.ExcludePatterns == nil { + options.ExcludePatterns = []string{} + } + + r := tarArchive + if decompress { + decompressedArchive, err := DecompressStream(tarArchive) + if err != nil { + return err + } + defer decompressedArchive.Close() + r = decompressedArchive + } + + return Unpack(r, dest, options) +} + +// TarUntar is a convenience function which calls Tar and Untar, with the output of one piped into the other. +// If either Tar or Untar fails, TarUntar aborts and returns the error. +func (archiver *Archiver) TarUntar(src, dst string) error { + logrus.Debugf("TarUntar(%s %s)", src, dst) + archive, err := TarWithOptions(src, &TarOptions{Compression: Uncompressed}) + if err != nil { + return err + } + defer archive.Close() + + var options *TarOptions + if archiver.UIDMaps != nil || archiver.GIDMaps != nil { + options = &TarOptions{ + UIDMaps: archiver.UIDMaps, + GIDMaps: archiver.GIDMaps, + } + } + return archiver.Untar(archive, dst, options) +} + +// TarUntar is a convenience function which calls Tar and Untar, with the output of one piped into the other. +// If either Tar or Untar fails, TarUntar aborts and returns the error. +func TarUntar(src, dst string) error { + return defaultArchiver.TarUntar(src, dst) +} + +// UntarPath untar a file from path to a destination, src is the source tar file path. +func (archiver *Archiver) UntarPath(src, dst string) error { + archive, err := os.Open(src) + if err != nil { + return err + } + defer archive.Close() + var options *TarOptions + if archiver.UIDMaps != nil || archiver.GIDMaps != nil { + options = &TarOptions{ + UIDMaps: archiver.UIDMaps, + GIDMaps: archiver.GIDMaps, + } + } + return archiver.Untar(archive, dst, options) +} + +// UntarPath is a convenience function which looks for an archive +// at filesystem path `src`, and unpacks it at `dst`. +func UntarPath(src, dst string) error { + return defaultArchiver.UntarPath(src, dst) +} + +// CopyWithTar creates a tar archive of filesystem path `src`, and +// unpacks it at filesystem path `dst`. +// The archive is streamed directly with fixed buffering and no +// intermediary disk IO. +func (archiver *Archiver) CopyWithTar(src, dst string) error { + srcSt, err := os.Stat(src) + if err != nil { + return err + } + if !srcSt.IsDir() { + return archiver.CopyFileWithTar(src, dst) + } + + // if this archiver is set up with ID mapping we need to create + // the new destination directory with the remapped root UID/GID pair + // as owner + rootUID, rootGID, err := idtools.GetRootUIDGID(archiver.UIDMaps, archiver.GIDMaps) + if err != nil { + return err + } + // Create dst, copy src's content into it + logrus.Debugf("Creating dest directory: %s", dst) + if err := idtools.MkdirAllNewAs(dst, 0755, rootUID, rootGID); err != nil { + return err + } + logrus.Debugf("Calling TarUntar(%s, %s)", src, dst) + return archiver.TarUntar(src, dst) +} + +// CopyWithTar creates a tar archive of filesystem path `src`, and +// unpacks it at filesystem path `dst`. +// The archive is streamed directly with fixed buffering and no +// intermediary disk IO. +func CopyWithTar(src, dst string) error { + return defaultArchiver.CopyWithTar(src, dst) +} + +// CopyFileWithTar emulates the behavior of the 'cp' command-line +// for a single file. It copies a regular file from path `src` to +// path `dst`, and preserves all its metadata. +func (archiver *Archiver) CopyFileWithTar(src, dst string) (err error) { + logrus.Debugf("CopyFileWithTar(%s, %s)", src, dst) + srcSt, err := os.Stat(src) + if err != nil { + return err + } + + if srcSt.IsDir() { + return fmt.Errorf("Can't copy a directory") + } + + // Clean up the trailing slash. This must be done in an operating + // system specific manner. + if dst[len(dst)-1] == os.PathSeparator { + dst = filepath.Join(dst, filepath.Base(src)) + } + // Create the holding directory if necessary + if err := system.MkdirAll(filepath.Dir(dst), 0700); err != nil { + return err + } + + r, w := io.Pipe() + errC := promise.Go(func() error { + defer w.Close() + + srcF, err := os.Open(src) + if err != nil { + return err + } + defer srcF.Close() + + hdr, err := tar.FileInfoHeader(srcSt, "") + if err != nil { + return err + } + hdr.Name = filepath.Base(dst) + hdr.Mode = int64(chmodTarEntry(os.FileMode(hdr.Mode))) + + remappedRootUID, remappedRootGID, err := idtools.GetRootUIDGID(archiver.UIDMaps, archiver.GIDMaps) + if err != nil { + return err + } + + // only perform mapping if the file being copied isn't already owned by the + // uid or gid of the remapped root in the container + if remappedRootUID != hdr.Uid { + xUID, err := idtools.ToHost(hdr.Uid, archiver.UIDMaps) + if err != nil { + return err + } + hdr.Uid = xUID + } + if remappedRootGID != hdr.Gid { + xGID, err := idtools.ToHost(hdr.Gid, archiver.GIDMaps) + if err != nil { + return err + } + hdr.Gid = xGID + } + + tw := tar.NewWriter(w) + defer tw.Close() + if err := tw.WriteHeader(hdr); err != nil { + return err + } + if _, err := io.Copy(tw, srcF); err != nil { + return err + } + return nil + }) + defer func() { + if er := <-errC; err != nil { + err = er + } + }() + + err = archiver.Untar(r, filepath.Dir(dst), nil) + if err != nil { + r.CloseWithError(err) + } + return err +} + +// CopyFileWithTar emulates the behavior of the 'cp' command-line +// for a single file. It copies a regular file from path `src` to +// path `dst`, and preserves all its metadata. +// +// Destination handling is in an operating specific manner depending +// where the daemon is running. If `dst` ends with a trailing slash +// the final destination path will be `dst/base(src)` (Linux) or +// `dst\base(src)` (Windows). +func CopyFileWithTar(src, dst string) (err error) { + return defaultArchiver.CopyFileWithTar(src, dst) +} + +// cmdStream executes a command, and returns its stdout as a stream. +// If the command fails to run or doesn't complete successfully, an error +// will be returned, including anything written on stderr. +func cmdStream(cmd *exec.Cmd, input io.Reader) (io.ReadCloser, <-chan struct{}, error) { + chdone := make(chan struct{}) + cmd.Stdin = input + pipeR, pipeW := io.Pipe() + cmd.Stdout = pipeW + var errBuf bytes.Buffer + cmd.Stderr = &errBuf + + // Run the command and return the pipe + if err := cmd.Start(); err != nil { + return nil, nil, err + } + + // Copy stdout to the returned pipe + go func() { + if err := cmd.Wait(); err != nil { + pipeW.CloseWithError(fmt.Errorf("%s: %s", err, errBuf.String())) + } else { + pipeW.Close() + } + close(chdone) + }() + + return pipeR, chdone, nil +} + +// NewTempArchive reads the content of src into a temporary file, and returns the contents +// of that file as an archive. The archive can only be read once - as soon as reading completes, +// the file will be deleted. +func NewTempArchive(src Archive, dir string) (*TempArchive, error) { + f, err := ioutil.TempFile(dir, "") + if err != nil { + return nil, err + } + if _, err := io.Copy(f, src); err != nil { + return nil, err + } + if _, err := f.Seek(0, 0); err != nil { + return nil, err + } + st, err := f.Stat() + if err != nil { + return nil, err + } + size := st.Size() + return &TempArchive{File: f, Size: size}, nil +} + +// TempArchive is a temporary archive. The archive can only be read once - as soon as reading completes, +// the file will be deleted. +type TempArchive struct { + *os.File + Size int64 // Pre-computed from Stat().Size() as a convenience + read int64 + closed bool +} + +// Close closes the underlying file if it's still open, or does a no-op +// to allow callers to try to close the TempArchive multiple times safely. +func (archive *TempArchive) Close() error { + if archive.closed { + return nil + } + + archive.closed = true + + return archive.File.Close() +} + +func (archive *TempArchive) Read(data []byte) (int, error) { + n, err := archive.File.Read(data) + archive.read += int64(n) + if err != nil || archive.read == archive.Size { + archive.Close() + os.Remove(archive.File.Name()) + } + return n, err +} diff --git a/pkg/archive/archive_test.go b/pkg/archive/archive_test.go new file mode 100644 index 00000000..495cac83 --- /dev/null +++ b/pkg/archive/archive_test.go @@ -0,0 +1,1148 @@ +package archive + +import ( + "archive/tar" + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + "time" +) + +var tmp string + +func init() { + tmp = "/tmp/" + if runtime.GOOS == "windows" { + tmp = os.Getenv("TEMP") + `\` + } +} + +func TestIsArchiveNilHeader(t *testing.T) { + out := IsArchive(nil) + if out { + t.Fatalf("isArchive should return false as nil is not a valid archive header") + } +} + +func TestIsArchiveInvalidHeader(t *testing.T) { + header := []byte{0x00, 0x01, 0x02} + out := IsArchive(header) + if out { + t.Fatalf("isArchive should return false as %s is not a valid archive header", header) + } +} + +func TestIsArchiveBzip2(t *testing.T) { + header := []byte{0x42, 0x5A, 0x68} + out := IsArchive(header) + if !out { + t.Fatalf("isArchive should return true as %s is a bz2 header", header) + } +} + +func TestIsArchive7zip(t *testing.T) { + header := []byte{0x50, 0x4b, 0x03, 0x04} + out := IsArchive(header) + if out { + t.Fatalf("isArchive should return false as %s is a 7z header and it is not supported", header) + } +} + +func TestIsArchivePathDir(t *testing.T) { + cmd := exec.Command("sh", "-c", "mkdir -p /tmp/archivedir") + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("Fail to create an archive file for test : %s.", output) + } + if IsArchivePath(tmp + "archivedir") { + t.Fatalf("Incorrectly recognised directory as an archive") + } +} + +func TestIsArchivePathInvalidFile(t *testing.T) { + cmd := exec.Command("sh", "-c", "dd if=/dev/zero bs=1K count=1 of=/tmp/archive && gzip --stdout /tmp/archive > /tmp/archive.gz") + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("Fail to create an archive file for test : %s.", output) + } + if IsArchivePath(tmp + "archive") { + t.Fatalf("Incorrectly recognised invalid tar path as archive") + } + if IsArchivePath(tmp + "archive.gz") { + t.Fatalf("Incorrectly recognised invalid compressed tar path as archive") + } +} + +func TestIsArchivePathTar(t *testing.T) { + cmd := exec.Command("sh", "-c", "touch /tmp/archivedata && tar -cf /tmp/archive /tmp/archivedata && gzip --stdout /tmp/archive > /tmp/archive.gz") + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("Fail to create an archive file for test : %s.", output) + } + if !IsArchivePath(tmp + "/archive") { + t.Fatalf("Did not recognise valid tar path as archive") + } + if !IsArchivePath(tmp + "archive.gz") { + t.Fatalf("Did not recognise valid compressed tar path as archive") + } +} + +func TestDecompressStreamGzip(t *testing.T) { + cmd := exec.Command("sh", "-c", "touch /tmp/archive && gzip -f /tmp/archive") + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("Fail to create an archive file for test : %s.", output) + } + archive, err := os.Open(tmp + "archive.gz") + _, err = DecompressStream(archive) + if err != nil { + t.Fatalf("Failed to decompress a gzip file.") + } +} + +func TestDecompressStreamBzip2(t *testing.T) { + cmd := exec.Command("sh", "-c", "touch /tmp/archive && bzip2 -f /tmp/archive") + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("Fail to create an archive file for test : %s.", output) + } + archive, err := os.Open(tmp + "archive.bz2") + _, err = DecompressStream(archive) + if err != nil { + t.Fatalf("Failed to decompress a bzip2 file.") + } +} + +func TestDecompressStreamXz(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Xz not present in msys2") + } + cmd := exec.Command("sh", "-c", "touch /tmp/archive && xz -f /tmp/archive") + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("Fail to create an archive file for test : %s.", output) + } + archive, err := os.Open(tmp + "archive.xz") + _, err = DecompressStream(archive) + if err != nil { + t.Fatalf("Failed to decompress a xz file.") + } +} + +func TestCompressStreamXzUnsuported(t *testing.T) { + dest, err := os.Create(tmp + "dest") + if err != nil { + t.Fatalf("Fail to create the destination file") + } + _, err = CompressStream(dest, Xz) + if err == nil { + t.Fatalf("Should fail as xz is unsupported for compression format.") + } +} + +func TestCompressStreamBzip2Unsupported(t *testing.T) { + dest, err := os.Create(tmp + "dest") + if err != nil { + t.Fatalf("Fail to create the destination file") + } + _, err = CompressStream(dest, Xz) + if err == nil { + t.Fatalf("Should fail as xz is unsupported for compression format.") + } +} + +func TestCompressStreamInvalid(t *testing.T) { + dest, err := os.Create(tmp + "dest") + if err != nil { + t.Fatalf("Fail to create the destination file") + } + _, err = CompressStream(dest, -1) + if err == nil { + t.Fatalf("Should fail as xz is unsupported for compression format.") + } +} + +func TestExtensionInvalid(t *testing.T) { + compression := Compression(-1) + output := compression.Extension() + if output != "" { + t.Fatalf("The extension of an invalid compression should be an empty string.") + } +} + +func TestExtensionUncompressed(t *testing.T) { + compression := Uncompressed + output := compression.Extension() + if output != "tar" { + t.Fatalf("The extension of a uncompressed archive should be 'tar'.") + } +} +func TestExtensionBzip2(t *testing.T) { + compression := Bzip2 + output := compression.Extension() + if output != "tar.bz2" { + t.Fatalf("The extension of a bzip2 archive should be 'tar.bz2'") + } +} +func TestExtensionGzip(t *testing.T) { + compression := Gzip + output := compression.Extension() + if output != "tar.gz" { + t.Fatalf("The extension of a bzip2 archive should be 'tar.gz'") + } +} +func TestExtensionXz(t *testing.T) { + compression := Xz + output := compression.Extension() + if output != "tar.xz" { + t.Fatalf("The extension of a bzip2 archive should be 'tar.xz'") + } +} + +func TestCmdStreamLargeStderr(t *testing.T) { + cmd := exec.Command("sh", "-c", "dd if=/dev/zero bs=1k count=1000 of=/dev/stderr; echo hello") + out, _, err := cmdStream(cmd, nil) + if err != nil { + t.Fatalf("Failed to start command: %s", err) + } + errCh := make(chan error) + go func() { + _, err := io.Copy(ioutil.Discard, out) + errCh <- err + }() + select { + case err := <-errCh: + if err != nil { + t.Fatalf("Command should not have failed (err=%.100s...)", err) + } + case <-time.After(5 * time.Second): + t.Fatalf("Command did not complete in 5 seconds; probable deadlock") + } +} + +func TestCmdStreamBad(t *testing.T) { + // TODO Windows: Figure out why this is failing in CI but not locally + if runtime.GOOS == "windows" { + t.Skip("Failing on Windows CI machines") + } + badCmd := exec.Command("sh", "-c", "echo hello; echo >&2 error couldn\\'t reverse the phase pulser; exit 1") + out, _, err := cmdStream(badCmd, nil) + if err != nil { + t.Fatalf("Failed to start command: %s", err) + } + if output, err := ioutil.ReadAll(out); err == nil { + t.Fatalf("Command should have failed") + } else if err.Error() != "exit status 1: error couldn't reverse the phase pulser\n" { + t.Fatalf("Wrong error value (%s)", err) + } else if s := string(output); s != "hello\n" { + t.Fatalf("Command output should be '%s', not '%s'", "hello\\n", output) + } +} + +func TestCmdStreamGood(t *testing.T) { + cmd := exec.Command("sh", "-c", "echo hello; exit 0") + out, _, err := cmdStream(cmd, nil) + if err != nil { + t.Fatal(err) + } + if output, err := ioutil.ReadAll(out); err != nil { + t.Fatalf("Command should not have failed (err=%s)", err) + } else if s := string(output); s != "hello\n" { + t.Fatalf("Command output should be '%s', not '%s'", "hello\\n", output) + } +} + +func TestUntarPathWithInvalidDest(t *testing.T) { + tempFolder, err := ioutil.TempDir("", "docker-archive-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempFolder) + invalidDestFolder := filepath.Join(tempFolder, "invalidDest") + // Create a src file + srcFile := filepath.Join(tempFolder, "src") + tarFile := filepath.Join(tempFolder, "src.tar") + os.Create(srcFile) + os.Create(invalidDestFolder) // being a file (not dir) should cause an error + + // Translate back to Unix semantics as next exec.Command is run under sh + srcFileU := srcFile + tarFileU := tarFile + if runtime.GOOS == "windows" { + tarFileU = "/tmp/" + filepath.Base(filepath.Dir(tarFile)) + "/src.tar" + srcFileU = "/tmp/" + filepath.Base(filepath.Dir(srcFile)) + "/src" + } + + cmd := exec.Command("sh", "-c", "tar cf "+tarFileU+" "+srcFileU) + _, err = cmd.CombinedOutput() + if err != nil { + t.Fatal(err) + } + + err = UntarPath(tarFile, invalidDestFolder) + if err == nil { + t.Fatalf("UntarPath with invalid destination path should throw an error.") + } +} + +func TestUntarPathWithInvalidSrc(t *testing.T) { + dest, err := ioutil.TempDir("", "docker-archive-test") + if err != nil { + t.Fatalf("Fail to create the destination file") + } + defer os.RemoveAll(dest) + err = UntarPath("/invalid/path", dest) + if err == nil { + t.Fatalf("UntarPath with invalid src path should throw an error.") + } +} + +func TestUntarPath(t *testing.T) { + tmpFolder, err := ioutil.TempDir("", "docker-archive-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpFolder) + srcFile := filepath.Join(tmpFolder, "src") + tarFile := filepath.Join(tmpFolder, "src.tar") + os.Create(filepath.Join(tmpFolder, "src")) + + destFolder := filepath.Join(tmpFolder, "dest") + err = os.MkdirAll(destFolder, 0740) + if err != nil { + t.Fatalf("Fail to create the destination file") + } + + // Translate back to Unix semantics as next exec.Command is run under sh + srcFileU := srcFile + tarFileU := tarFile + if runtime.GOOS == "windows" { + tarFileU = "/tmp/" + filepath.Base(filepath.Dir(tarFile)) + "/src.tar" + srcFileU = "/tmp/" + filepath.Base(filepath.Dir(srcFile)) + "/src" + } + cmd := exec.Command("sh", "-c", "tar cf "+tarFileU+" "+srcFileU) + _, err = cmd.CombinedOutput() + if err != nil { + t.Fatal(err) + } + + err = UntarPath(tarFile, destFolder) + if err != nil { + t.Fatalf("UntarPath shouldn't throw an error, %s.", err) + } + expectedFile := filepath.Join(destFolder, srcFileU) + _, err = os.Stat(expectedFile) + if err != nil { + t.Fatalf("Destination folder should contain the source file but did not.") + } +} + +// Do the same test as above but with the destination as file, it should fail +func TestUntarPathWithDestinationFile(t *testing.T) { + tmpFolder, err := ioutil.TempDir("", "docker-archive-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpFolder) + srcFile := filepath.Join(tmpFolder, "src") + tarFile := filepath.Join(tmpFolder, "src.tar") + os.Create(filepath.Join(tmpFolder, "src")) + + // Translate back to Unix semantics as next exec.Command is run under sh + srcFileU := srcFile + tarFileU := tarFile + if runtime.GOOS == "windows" { + tarFileU = "/tmp/" + filepath.Base(filepath.Dir(tarFile)) + "/src.tar" + srcFileU = "/tmp/" + filepath.Base(filepath.Dir(srcFile)) + "/src" + } + cmd := exec.Command("sh", "-c", "tar cf "+tarFileU+" "+srcFileU) + _, err = cmd.CombinedOutput() + if err != nil { + t.Fatal(err) + } + destFile := filepath.Join(tmpFolder, "dest") + _, err = os.Create(destFile) + if err != nil { + t.Fatalf("Fail to create the destination file") + } + err = UntarPath(tarFile, destFile) + if err == nil { + t.Fatalf("UntarPath should throw an error if the destination if a file") + } +} + +// Do the same test as above but with the destination folder already exists +// and the destination file is a directory +// It's working, see https://github.com/docker/docker/issues/10040 +func TestUntarPathWithDestinationSrcFileAsFolder(t *testing.T) { + tmpFolder, err := ioutil.TempDir("", "docker-archive-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpFolder) + srcFile := filepath.Join(tmpFolder, "src") + tarFile := filepath.Join(tmpFolder, "src.tar") + os.Create(srcFile) + + // Translate back to Unix semantics as next exec.Command is run under sh + srcFileU := srcFile + tarFileU := tarFile + if runtime.GOOS == "windows" { + tarFileU = "/tmp/" + filepath.Base(filepath.Dir(tarFile)) + "/src.tar" + srcFileU = "/tmp/" + filepath.Base(filepath.Dir(srcFile)) + "/src" + } + + cmd := exec.Command("sh", "-c", "tar cf "+tarFileU+" "+srcFileU) + _, err = cmd.CombinedOutput() + if err != nil { + t.Fatal(err) + } + destFolder := filepath.Join(tmpFolder, "dest") + err = os.MkdirAll(destFolder, 0740) + if err != nil { + t.Fatalf("Fail to create the destination folder") + } + // Let's create a folder that will has the same path as the extracted file (from tar) + destSrcFileAsFolder := filepath.Join(destFolder, srcFileU) + err = os.MkdirAll(destSrcFileAsFolder, 0740) + if err != nil { + t.Fatal(err) + } + err = UntarPath(tarFile, destFolder) + if err != nil { + t.Fatalf("UntarPath should throw not throw an error if the extracted file already exists and is a folder") + } +} + +func TestCopyWithTarInvalidSrc(t *testing.T) { + tempFolder, err := ioutil.TempDir("", "docker-archive-test") + if err != nil { + t.Fatal(nil) + } + destFolder := filepath.Join(tempFolder, "dest") + invalidSrc := filepath.Join(tempFolder, "doesnotexists") + err = os.MkdirAll(destFolder, 0740) + if err != nil { + t.Fatal(err) + } + err = CopyWithTar(invalidSrc, destFolder) + if err == nil { + t.Fatalf("archiver.CopyWithTar with invalid src path should throw an error.") + } +} + +func TestCopyWithTarInexistentDestWillCreateIt(t *testing.T) { + tempFolder, err := ioutil.TempDir("", "docker-archive-test") + if err != nil { + t.Fatal(nil) + } + srcFolder := filepath.Join(tempFolder, "src") + inexistentDestFolder := filepath.Join(tempFolder, "doesnotexists") + err = os.MkdirAll(srcFolder, 0740) + if err != nil { + t.Fatal(err) + } + err = CopyWithTar(srcFolder, inexistentDestFolder) + if err != nil { + t.Fatalf("CopyWithTar with an inexistent folder shouldn't fail.") + } + _, err = os.Stat(inexistentDestFolder) + if err != nil { + t.Fatalf("CopyWithTar with an inexistent folder should create it.") + } +} + +// Test CopyWithTar with a file as src +func TestCopyWithTarSrcFile(t *testing.T) { + folder, err := ioutil.TempDir("", "docker-archive-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(folder) + dest := filepath.Join(folder, "dest") + srcFolder := filepath.Join(folder, "src") + src := filepath.Join(folder, filepath.Join("src", "src")) + err = os.MkdirAll(srcFolder, 0740) + if err != nil { + t.Fatal(err) + } + err = os.MkdirAll(dest, 0740) + if err != nil { + t.Fatal(err) + } + ioutil.WriteFile(src, []byte("content"), 0777) + err = CopyWithTar(src, dest) + if err != nil { + t.Fatalf("archiver.CopyWithTar shouldn't throw an error, %s.", err) + } + _, err = os.Stat(dest) + // FIXME Check the content + if err != nil { + t.Fatalf("Destination file should be the same as the source.") + } +} + +// Test CopyWithTar with a folder as src +func TestCopyWithTarSrcFolder(t *testing.T) { + folder, err := ioutil.TempDir("", "docker-archive-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(folder) + dest := filepath.Join(folder, "dest") + src := filepath.Join(folder, filepath.Join("src", "folder")) + err = os.MkdirAll(src, 0740) + if err != nil { + t.Fatal(err) + } + err = os.MkdirAll(dest, 0740) + if err != nil { + t.Fatal(err) + } + ioutil.WriteFile(filepath.Join(src, "file"), []byte("content"), 0777) + err = CopyWithTar(src, dest) + if err != nil { + t.Fatalf("archiver.CopyWithTar shouldn't throw an error, %s.", err) + } + _, err = os.Stat(dest) + // FIXME Check the content (the file inside) + if err != nil { + t.Fatalf("Destination folder should contain the source file but did not.") + } +} + +func TestCopyFileWithTarInvalidSrc(t *testing.T) { + tempFolder, err := ioutil.TempDir("", "docker-archive-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempFolder) + destFolder := filepath.Join(tempFolder, "dest") + err = os.MkdirAll(destFolder, 0740) + if err != nil { + t.Fatal(err) + } + invalidFile := filepath.Join(tempFolder, "doesnotexists") + err = CopyFileWithTar(invalidFile, destFolder) + if err == nil { + t.Fatalf("archiver.CopyWithTar with invalid src path should throw an error.") + } +} + +func TestCopyFileWithTarInexistentDestWillCreateIt(t *testing.T) { + tempFolder, err := ioutil.TempDir("", "docker-archive-test") + if err != nil { + t.Fatal(nil) + } + defer os.RemoveAll(tempFolder) + srcFile := filepath.Join(tempFolder, "src") + inexistentDestFolder := filepath.Join(tempFolder, "doesnotexists") + _, err = os.Create(srcFile) + if err != nil { + t.Fatal(err) + } + err = CopyFileWithTar(srcFile, inexistentDestFolder) + if err != nil { + t.Fatalf("CopyWithTar with an inexistent folder shouldn't fail.") + } + _, err = os.Stat(inexistentDestFolder) + if err != nil { + t.Fatalf("CopyWithTar with an inexistent folder should create it.") + } + // FIXME Test the src file and content +} + +func TestCopyFileWithTarSrcFolder(t *testing.T) { + folder, err := ioutil.TempDir("", "docker-archive-copyfilewithtar-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(folder) + dest := filepath.Join(folder, "dest") + src := filepath.Join(folder, "srcfolder") + err = os.MkdirAll(src, 0740) + if err != nil { + t.Fatal(err) + } + err = os.MkdirAll(dest, 0740) + if err != nil { + t.Fatal(err) + } + err = CopyFileWithTar(src, dest) + if err == nil { + t.Fatalf("CopyFileWithTar should throw an error with a folder.") + } +} + +func TestCopyFileWithTarSrcFile(t *testing.T) { + folder, err := ioutil.TempDir("", "docker-archive-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(folder) + dest := filepath.Join(folder, "dest") + srcFolder := filepath.Join(folder, "src") + src := filepath.Join(folder, filepath.Join("src", "src")) + err = os.MkdirAll(srcFolder, 0740) + if err != nil { + t.Fatal(err) + } + err = os.MkdirAll(dest, 0740) + if err != nil { + t.Fatal(err) + } + ioutil.WriteFile(src, []byte("content"), 0777) + err = CopyWithTar(src, dest+"/") + if err != nil { + t.Fatalf("archiver.CopyFileWithTar shouldn't throw an error, %s.", err) + } + _, err = os.Stat(dest) + if err != nil { + t.Fatalf("Destination folder should contain the source file but did not.") + } +} + +func TestTarFiles(t *testing.T) { + // TODO Windows: Figure out how to port this test. + if runtime.GOOS == "windows" { + t.Skip("Failing on Windows") + } + // try without hardlinks + if err := checkNoChanges(1000, false); err != nil { + t.Fatal(err) + } + // try with hardlinks + if err := checkNoChanges(1000, true); err != nil { + t.Fatal(err) + } +} + +func checkNoChanges(fileNum int, hardlinks bool) error { + srcDir, err := ioutil.TempDir("", "docker-test-srcDir") + if err != nil { + return err + } + defer os.RemoveAll(srcDir) + + destDir, err := ioutil.TempDir("", "docker-test-destDir") + if err != nil { + return err + } + defer os.RemoveAll(destDir) + + _, err = prepareUntarSourceDirectory(fileNum, srcDir, hardlinks) + if err != nil { + return err + } + + err = TarUntar(srcDir, destDir) + if err != nil { + return err + } + + changes, err := ChangesDirs(destDir, srcDir) + if err != nil { + return err + } + if len(changes) > 0 { + return fmt.Errorf("with %d files and %v hardlinks: expected 0 changes, got %d", fileNum, hardlinks, len(changes)) + } + return nil +} + +func tarUntar(t *testing.T, origin string, options *TarOptions) ([]Change, error) { + archive, err := TarWithOptions(origin, options) + if err != nil { + t.Fatal(err) + } + defer archive.Close() + + buf := make([]byte, 10) + if _, err := archive.Read(buf); err != nil { + return nil, err + } + wrap := io.MultiReader(bytes.NewReader(buf), archive) + + detectedCompression := DetectCompression(buf) + compression := options.Compression + if detectedCompression.Extension() != compression.Extension() { + return nil, fmt.Errorf("Wrong compression detected. Actual compression: %s, found %s", compression.Extension(), detectedCompression.Extension()) + } + + tmp, err := ioutil.TempDir("", "docker-test-untar") + if err != nil { + return nil, err + } + defer os.RemoveAll(tmp) + if err := Untar(wrap, tmp, nil); err != nil { + return nil, err + } + if _, err := os.Stat(tmp); err != nil { + return nil, err + } + + return ChangesDirs(origin, tmp) +} + +func TestTarUntar(t *testing.T) { + // TODO Windows: Figure out how to fix this test. + if runtime.GOOS == "windows" { + t.Skip("Failing on Windows") + } + origin, err := ioutil.TempDir("", "docker-test-untar-origin") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(origin) + if err := ioutil.WriteFile(filepath.Join(origin, "1"), []byte("hello world"), 0700); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(filepath.Join(origin, "2"), []byte("welcome!"), 0700); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(filepath.Join(origin, "3"), []byte("will be ignored"), 0700); err != nil { + t.Fatal(err) + } + + for _, c := range []Compression{ + Uncompressed, + Gzip, + } { + changes, err := tarUntar(t, origin, &TarOptions{ + Compression: c, + ExcludePatterns: []string{"3"}, + }) + + if err != nil { + t.Fatalf("Error tar/untar for compression %s: %s", c.Extension(), err) + } + + if len(changes) != 1 || changes[0].Path != "/3" { + t.Fatalf("Unexpected differences after tarUntar: %v", changes) + } + } +} + +func TestTarWithOptions(t *testing.T) { + // TODO Windows: Figure out how to fix this test. + if runtime.GOOS == "windows" { + t.Skip("Failing on Windows") + } + origin, err := ioutil.TempDir("", "docker-test-untar-origin") + if err != nil { + t.Fatal(err) + } + if _, err := ioutil.TempDir(origin, "folder"); err != nil { + t.Fatal(err) + } + defer os.RemoveAll(origin) + if err := ioutil.WriteFile(filepath.Join(origin, "1"), []byte("hello world"), 0700); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(filepath.Join(origin, "2"), []byte("welcome!"), 0700); err != nil { + t.Fatal(err) + } + + cases := []struct { + opts *TarOptions + numChanges int + }{ + {&TarOptions{IncludeFiles: []string{"1"}}, 2}, + {&TarOptions{ExcludePatterns: []string{"2"}}, 1}, + {&TarOptions{ExcludePatterns: []string{"1", "folder*"}}, 2}, + {&TarOptions{IncludeFiles: []string{"1", "1"}}, 2}, + {&TarOptions{IncludeFiles: []string{"1"}, RebaseNames: map[string]string{"1": "test"}}, 4}, + } + for _, testCase := range cases { + changes, err := tarUntar(t, origin, testCase.opts) + if err != nil { + t.Fatalf("Error tar/untar when testing inclusion/exclusion: %s", err) + } + if len(changes) != testCase.numChanges { + t.Errorf("Expected %d changes, got %d for %+v:", + testCase.numChanges, len(changes), testCase.opts) + } + } +} + +// Some tar archives such as http://haproxy.1wt.eu/download/1.5/src/devel/haproxy-1.5-dev21.tar.gz +// use PAX Global Extended Headers. +// Failing prevents the archives from being uncompressed during ADD +func TestTypeXGlobalHeaderDoesNotFail(t *testing.T) { + hdr := tar.Header{Typeflag: tar.TypeXGlobalHeader} + tmpDir, err := ioutil.TempDir("", "docker-test-archive-pax-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + err = createTarFile(filepath.Join(tmpDir, "pax_global_header"), tmpDir, &hdr, nil, true, nil) + if err != nil { + t.Fatal(err) + } +} + +// Some tar have both GNU specific (huge uid) and Ustar specific (long name) things. +// Not supposed to happen (should use PAX instead of Ustar for long name) but it does and it should still work. +func TestUntarUstarGnuConflict(t *testing.T) { + f, err := os.Open("testdata/broken.tar") + if err != nil { + t.Fatal(err) + } + found := false + tr := tar.NewReader(f) + // Iterate through the files in the archive. + for { + hdr, err := tr.Next() + if err == io.EOF { + // end of tar archive + break + } + if err != nil { + t.Fatal(err) + } + if hdr.Name == "root/.cpanm/work/1395823785.24209/Plack-1.0030/blib/man3/Plack::Middleware::LighttpdScriptNameFix.3pm" { + found = true + break + } + } + if !found { + t.Fatalf("%s not found in the archive", "root/.cpanm/work/1395823785.24209/Plack-1.0030/blib/man3/Plack::Middleware::LighttpdScriptNameFix.3pm") + } +} + +func prepareUntarSourceDirectory(numberOfFiles int, targetPath string, makeLinks bool) (int, error) { + fileData := []byte("fooo") + for n := 0; n < numberOfFiles; n++ { + fileName := fmt.Sprintf("file-%d", n) + if err := ioutil.WriteFile(filepath.Join(targetPath, fileName), fileData, 0700); err != nil { + return 0, err + } + if makeLinks { + if err := os.Link(filepath.Join(targetPath, fileName), filepath.Join(targetPath, fileName+"-link")); err != nil { + return 0, err + } + } + } + totalSize := numberOfFiles * len(fileData) + return totalSize, nil +} + +func BenchmarkTarUntar(b *testing.B) { + origin, err := ioutil.TempDir("", "docker-test-untar-origin") + if err != nil { + b.Fatal(err) + } + tempDir, err := ioutil.TempDir("", "docker-test-untar-destination") + if err != nil { + b.Fatal(err) + } + target := filepath.Join(tempDir, "dest") + n, err := prepareUntarSourceDirectory(100, origin, false) + if err != nil { + b.Fatal(err) + } + defer os.RemoveAll(origin) + defer os.RemoveAll(tempDir) + + b.ResetTimer() + b.SetBytes(int64(n)) + for n := 0; n < b.N; n++ { + err := TarUntar(origin, target) + if err != nil { + b.Fatal(err) + } + os.RemoveAll(target) + } +} + +func BenchmarkTarUntarWithLinks(b *testing.B) { + origin, err := ioutil.TempDir("", "docker-test-untar-origin") + if err != nil { + b.Fatal(err) + } + tempDir, err := ioutil.TempDir("", "docker-test-untar-destination") + if err != nil { + b.Fatal(err) + } + target := filepath.Join(tempDir, "dest") + n, err := prepareUntarSourceDirectory(100, origin, true) + if err != nil { + b.Fatal(err) + } + defer os.RemoveAll(origin) + defer os.RemoveAll(tempDir) + + b.ResetTimer() + b.SetBytes(int64(n)) + for n := 0; n < b.N; n++ { + err := TarUntar(origin, target) + if err != nil { + b.Fatal(err) + } + os.RemoveAll(target) + } +} + +func TestUntarInvalidFilenames(t *testing.T) { + // TODO Windows: Figure out how to fix this test. + if runtime.GOOS == "windows" { + t.Skip("Passes but hits breakoutError: platform and architecture is not supported") + } + for i, headers := range [][]*tar.Header{ + { + { + Name: "../victim/dotdot", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + { + { + // Note the leading slash + Name: "/../victim/slash-dotdot", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + } { + if err := testBreakout("untar", "docker-TestUntarInvalidFilenames", headers); err != nil { + t.Fatalf("i=%d. %v", i, err) + } + } +} + +func TestUntarHardlinkToSymlink(t *testing.T) { + // TODO Windows. There may be a way of running this, but turning off for now + if runtime.GOOS == "windows" { + t.Skip("hardlinks on Windows") + } + for i, headers := range [][]*tar.Header{ + { + { + Name: "symlink1", + Typeflag: tar.TypeSymlink, + Linkname: "regfile", + Mode: 0644, + }, + { + Name: "symlink2", + Typeflag: tar.TypeLink, + Linkname: "symlink1", + Mode: 0644, + }, + { + Name: "regfile", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + } { + if err := testBreakout("untar", "docker-TestUntarHardlinkToSymlink", headers); err != nil { + t.Fatalf("i=%d. %v", i, err) + } + } +} + +func TestUntarInvalidHardlink(t *testing.T) { + // TODO Windows. There may be a way of running this, but turning off for now + if runtime.GOOS == "windows" { + t.Skip("hardlinks on Windows") + } + for i, headers := range [][]*tar.Header{ + { // try reading victim/hello (../) + { + Name: "dotdot", + Typeflag: tar.TypeLink, + Linkname: "../victim/hello", + Mode: 0644, + }, + }, + { // try reading victim/hello (/../) + { + Name: "slash-dotdot", + Typeflag: tar.TypeLink, + // Note the leading slash + Linkname: "/../victim/hello", + Mode: 0644, + }, + }, + { // try writing victim/file + { + Name: "loophole-victim", + Typeflag: tar.TypeLink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "loophole-victim/file", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + { // try reading victim/hello (hardlink, symlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeLink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "symlink", + Typeflag: tar.TypeSymlink, + Linkname: "loophole-victim/hello", + Mode: 0644, + }, + }, + { // Try reading victim/hello (hardlink, hardlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeLink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "hardlink", + Typeflag: tar.TypeLink, + Linkname: "loophole-victim/hello", + Mode: 0644, + }, + }, + { // Try removing victim directory (hardlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeLink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "loophole-victim", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + } { + if err := testBreakout("untar", "docker-TestUntarInvalidHardlink", headers); err != nil { + t.Fatalf("i=%d. %v", i, err) + } + } +} + +func TestUntarInvalidSymlink(t *testing.T) { + // TODO Windows. There may be a way of running this, but turning off for now + if runtime.GOOS == "windows" { + t.Skip("hardlinks on Windows") + } + for i, headers := range [][]*tar.Header{ + { // try reading victim/hello (../) + { + Name: "dotdot", + Typeflag: tar.TypeSymlink, + Linkname: "../victim/hello", + Mode: 0644, + }, + }, + { // try reading victim/hello (/../) + { + Name: "slash-dotdot", + Typeflag: tar.TypeSymlink, + // Note the leading slash + Linkname: "/../victim/hello", + Mode: 0644, + }, + }, + { // try writing victim/file + { + Name: "loophole-victim", + Typeflag: tar.TypeSymlink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "loophole-victim/file", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + { // try reading victim/hello (symlink, symlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeSymlink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "symlink", + Typeflag: tar.TypeSymlink, + Linkname: "loophole-victim/hello", + Mode: 0644, + }, + }, + { // try reading victim/hello (symlink, hardlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeSymlink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "hardlink", + Typeflag: tar.TypeLink, + Linkname: "loophole-victim/hello", + Mode: 0644, + }, + }, + { // try removing victim directory (symlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeSymlink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "loophole-victim", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + { // try writing to victim/newdir/newfile with a symlink in the path + { + // this header needs to be before the next one, or else there is an error + Name: "dir/loophole", + Typeflag: tar.TypeSymlink, + Linkname: "../../victim", + Mode: 0755, + }, + { + Name: "dir/loophole/newdir/newfile", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + } { + if err := testBreakout("untar", "docker-TestUntarInvalidSymlink", headers); err != nil { + t.Fatalf("i=%d. %v", i, err) + } + } +} + +func TestTempArchiveCloseMultipleTimes(t *testing.T) { + reader := ioutil.NopCloser(strings.NewReader("hello")) + tempArchive, err := NewTempArchive(reader, "") + buf := make([]byte, 10) + n, err := tempArchive.Read(buf) + if n != 5 { + t.Fatalf("Expected to read 5 bytes. Read %d instead", n) + } + for i := 0; i < 3; i++ { + if err = tempArchive.Close(); err != nil { + t.Fatalf("i=%d. Unexpected error closing temp archive: %v", i, err) + } + } +} diff --git a/pkg/archive/archive_unix.go b/pkg/archive/archive_unix.go new file mode 100644 index 00000000..fbc3bb8c --- /dev/null +++ b/pkg/archive/archive_unix.go @@ -0,0 +1,112 @@ +// +build !windows + +package archive + +import ( + "archive/tar" + "errors" + "os" + "path/filepath" + "syscall" + + "github.com/docker/docker/pkg/system" +) + +// fixVolumePathPrefix does platform specific processing to ensure that if +// the path being passed in is not in a volume path format, convert it to one. +func fixVolumePathPrefix(srcPath string) string { + return srcPath +} + +// getWalkRoot calculates the root path when performing a TarWithOptions. +// We use a separate function as this is platform specific. On Linux, we +// can't use filepath.Join(srcPath,include) because this will clean away +// a trailing "." or "/" which may be important. +func getWalkRoot(srcPath string, include string) string { + return srcPath + string(filepath.Separator) + include +} + +// CanonicalTarNameForPath returns platform-specific filepath +// to canonical posix-style path for tar archival. p is relative +// path. +func CanonicalTarNameForPath(p string) (string, error) { + return p, nil // already unix-style +} + +// chmodTarEntry is used to adjust the file permissions used in tar header based +// on the platform the archival is done. + +func chmodTarEntry(perm os.FileMode) os.FileMode { + return perm // noop for unix as golang APIs provide perm bits correctly +} + +func setHeaderForSpecialDevice(hdr *tar.Header, ta *tarAppender, name string, stat interface{}) (inode uint64, err error) { + s, ok := stat.(*syscall.Stat_t) + + if !ok { + err = errors.New("cannot convert stat value to syscall.Stat_t") + return + } + + inode = uint64(s.Ino) + + // Currently go does not fill in the major/minors + if s.Mode&syscall.S_IFBLK != 0 || + s.Mode&syscall.S_IFCHR != 0 { + hdr.Devmajor = int64(major(uint64(s.Rdev))) + hdr.Devminor = int64(minor(uint64(s.Rdev))) + } + + return +} + +func getFileUIDGID(stat interface{}) (int, int, error) { + s, ok := stat.(*syscall.Stat_t) + + if !ok { + return -1, -1, errors.New("cannot convert stat value to syscall.Stat_t") + } + return int(s.Uid), int(s.Gid), nil +} + +func major(device uint64) uint64 { + return (device >> 8) & 0xfff +} + +func minor(device uint64) uint64 { + return (device & 0xff) | ((device >> 12) & 0xfff00) +} + +// handleTarTypeBlockCharFifo is an OS-specific helper function used by +// createTarFile to handle the following types of header: Block; Char; Fifo +func handleTarTypeBlockCharFifo(hdr *tar.Header, path string) error { + mode := uint32(hdr.Mode & 07777) + switch hdr.Typeflag { + case tar.TypeBlock: + mode |= syscall.S_IFBLK + case tar.TypeChar: + mode |= syscall.S_IFCHR + case tar.TypeFifo: + mode |= syscall.S_IFIFO + } + + if err := system.Mknod(path, mode, int(system.Mkdev(hdr.Devmajor, hdr.Devminor))); err != nil { + return err + } + return nil +} + +func handleLChmod(hdr *tar.Header, path string, hdrInfo os.FileInfo) error { + if hdr.Typeflag == tar.TypeLink { + if fi, err := os.Lstat(hdr.Linkname); err == nil && (fi.Mode()&os.ModeSymlink == 0) { + if err := os.Chmod(path, hdrInfo.Mode()); err != nil { + return err + } + } + } else if hdr.Typeflag != tar.TypeSymlink { + if err := os.Chmod(path, hdrInfo.Mode()); err != nil { + return err + } + } + return nil +} diff --git a/pkg/archive/archive_unix_test.go b/pkg/archive/archive_unix_test.go new file mode 100644 index 00000000..548391b3 --- /dev/null +++ b/pkg/archive/archive_unix_test.go @@ -0,0 +1,245 @@ +// +build !windows + +package archive + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "syscall" + "testing" + + "github.com/docker/docker/pkg/system" +) + +func TestCanonicalTarNameForPath(t *testing.T) { + cases := []struct{ in, expected string }{ + {"foo", "foo"}, + {"foo/bar", "foo/bar"}, + {"foo/dir/", "foo/dir/"}, + } + for _, v := range cases { + if out, err := CanonicalTarNameForPath(v.in); err != nil { + t.Fatalf("cannot get canonical name for path: %s: %v", v.in, err) + } else if out != v.expected { + t.Fatalf("wrong canonical tar name. expected:%s got:%s", v.expected, out) + } + } +} + +func TestCanonicalTarName(t *testing.T) { + cases := []struct { + in string + isDir bool + expected string + }{ + {"foo", false, "foo"}, + {"foo", true, "foo/"}, + {"foo/bar", false, "foo/bar"}, + {"foo/bar", true, "foo/bar/"}, + } + for _, v := range cases { + if out, err := canonicalTarName(v.in, v.isDir); err != nil { + t.Fatalf("cannot get canonical name for path: %s: %v", v.in, err) + } else if out != v.expected { + t.Fatalf("wrong canonical tar name. expected:%s got:%s", v.expected, out) + } + } +} + +func TestChmodTarEntry(t *testing.T) { + cases := []struct { + in, expected os.FileMode + }{ + {0000, 0000}, + {0777, 0777}, + {0644, 0644}, + {0755, 0755}, + {0444, 0444}, + } + for _, v := range cases { + if out := chmodTarEntry(v.in); out != v.expected { + t.Fatalf("wrong chmod. expected:%v got:%v", v.expected, out) + } + } +} + +func TestTarWithHardLink(t *testing.T) { + origin, err := ioutil.TempDir("", "docker-test-tar-hardlink") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(origin) + if err := ioutil.WriteFile(filepath.Join(origin, "1"), []byte("hello world"), 0700); err != nil { + t.Fatal(err) + } + if err := os.Link(filepath.Join(origin, "1"), filepath.Join(origin, "2")); err != nil { + t.Fatal(err) + } + + var i1, i2 uint64 + if i1, err = getNlink(filepath.Join(origin, "1")); err != nil { + t.Fatal(err) + } + // sanity check that we can hardlink + if i1 != 2 { + t.Skipf("skipping since hardlinks don't work here; expected 2 links, got %d", i1) + } + + dest, err := ioutil.TempDir("", "docker-test-tar-hardlink-dest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dest) + + // we'll do this in two steps to separate failure + fh, err := Tar(origin, Uncompressed) + if err != nil { + t.Fatal(err) + } + + // ensure we can read the whole thing with no error, before writing back out + buf, err := ioutil.ReadAll(fh) + if err != nil { + t.Fatal(err) + } + + bRdr := bytes.NewReader(buf) + err = Untar(bRdr, dest, &TarOptions{Compression: Uncompressed}) + if err != nil { + t.Fatal(err) + } + + if i1, err = getInode(filepath.Join(dest, "1")); err != nil { + t.Fatal(err) + } + if i2, err = getInode(filepath.Join(dest, "2")); err != nil { + t.Fatal(err) + } + + if i1 != i2 { + t.Errorf("expected matching inodes, but got %d and %d", i1, i2) + } +} + +func getNlink(path string) (uint64, error) { + stat, err := os.Stat(path) + if err != nil { + return 0, err + } + statT, ok := stat.Sys().(*syscall.Stat_t) + if !ok { + return 0, fmt.Errorf("expected type *syscall.Stat_t, got %t", stat.Sys()) + } + // We need this conversion on ARM64 + return uint64(statT.Nlink), nil +} + +func getInode(path string) (uint64, error) { + stat, err := os.Stat(path) + if err != nil { + return 0, err + } + statT, ok := stat.Sys().(*syscall.Stat_t) + if !ok { + return 0, fmt.Errorf("expected type *syscall.Stat_t, got %t", stat.Sys()) + } + return statT.Ino, nil +} + +func TestTarWithBlockCharFifo(t *testing.T) { + origin, err := ioutil.TempDir("", "docker-test-tar-hardlink") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(origin) + if err := ioutil.WriteFile(filepath.Join(origin, "1"), []byte("hello world"), 0700); err != nil { + t.Fatal(err) + } + if err := system.Mknod(filepath.Join(origin, "2"), syscall.S_IFBLK, int(system.Mkdev(int64(12), int64(5)))); err != nil { + t.Fatal(err) + } + if err := system.Mknod(filepath.Join(origin, "3"), syscall.S_IFCHR, int(system.Mkdev(int64(12), int64(5)))); err != nil { + t.Fatal(err) + } + if err := system.Mknod(filepath.Join(origin, "4"), syscall.S_IFIFO, int(system.Mkdev(int64(12), int64(5)))); err != nil { + t.Fatal(err) + } + + dest, err := ioutil.TempDir("", "docker-test-tar-hardlink-dest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dest) + + // we'll do this in two steps to separate failure + fh, err := Tar(origin, Uncompressed) + if err != nil { + t.Fatal(err) + } + + // ensure we can read the whole thing with no error, before writing back out + buf, err := ioutil.ReadAll(fh) + if err != nil { + t.Fatal(err) + } + + bRdr := bytes.NewReader(buf) + err = Untar(bRdr, dest, &TarOptions{Compression: Uncompressed}) + if err != nil { + t.Fatal(err) + } + + changes, err := ChangesDirs(origin, dest) + if err != nil { + t.Fatal(err) + } + if len(changes) > 0 { + t.Fatalf("Tar with special device (block, char, fifo) should keep them (recreate them when untar) : %v", changes) + } +} + +// TestTarUntarWithXattr is Unix as Lsetxattr is not supported on Windows +func TestTarUntarWithXattr(t *testing.T) { + origin, err := ioutil.TempDir("", "docker-test-untar-origin") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(origin) + if err := ioutil.WriteFile(filepath.Join(origin, "1"), []byte("hello world"), 0700); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(filepath.Join(origin, "2"), []byte("welcome!"), 0700); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(filepath.Join(origin, "3"), []byte("will be ignored"), 0700); err != nil { + t.Fatal(err) + } + if err := system.Lsetxattr(filepath.Join(origin, "2"), "security.capability", []byte{0x00}, 0); err != nil { + t.Fatal(err) + } + + for _, c := range []Compression{ + Uncompressed, + Gzip, + } { + changes, err := tarUntar(t, origin, &TarOptions{ + Compression: c, + ExcludePatterns: []string{"3"}, + }) + + if err != nil { + t.Fatalf("Error tar/untar for compression %s: %s", c.Extension(), err) + } + + if len(changes) != 1 || changes[0].Path != "/3" { + t.Fatalf("Unexpected differences after tarUntar: %v", changes) + } + capability, _ := system.Lgetxattr(filepath.Join(origin, "2"), "security.capability") + if capability == nil && capability[0] != 0x00 { + t.Fatalf("Untar should have kept the 'security.capability' xattr.") + } + } +} diff --git a/pkg/archive/archive_windows.go b/pkg/archive/archive_windows.go new file mode 100644 index 00000000..5c3a1be3 --- /dev/null +++ b/pkg/archive/archive_windows.go @@ -0,0 +1,70 @@ +// +build windows + +package archive + +import ( + "archive/tar" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/docker/docker/pkg/longpath" +) + +// fixVolumePathPrefix does platform specific processing to ensure that if +// the path being passed in is not in a volume path format, convert it to one. +func fixVolumePathPrefix(srcPath string) string { + return longpath.AddPrefix(srcPath) +} + +// getWalkRoot calculates the root path when performing a TarWithOptions. +// We use a separate function as this is platform specific. +func getWalkRoot(srcPath string, include string) string { + return filepath.Join(srcPath, include) +} + +// CanonicalTarNameForPath returns platform-specific filepath +// to canonical posix-style path for tar archival. p is relative +// path. +func CanonicalTarNameForPath(p string) (string, error) { + // windows: convert windows style relative path with backslashes + // into forward slashes. Since windows does not allow '/' or '\' + // in file names, it is mostly safe to replace however we must + // check just in case + if strings.Contains(p, "/") { + return "", fmt.Errorf("Windows path contains forward slash: %s", p) + } + return strings.Replace(p, string(os.PathSeparator), "/", -1), nil + +} + +// chmodTarEntry is used to adjust the file permissions used in tar header based +// on the platform the archival is done. +func chmodTarEntry(perm os.FileMode) os.FileMode { + perm &= 0755 + // Add the x bit: make everything +x from windows + perm |= 0111 + + return perm +} + +func setHeaderForSpecialDevice(hdr *tar.Header, ta *tarAppender, name string, stat interface{}) (inode uint64, err error) { + // do nothing. no notion of Rdev, Inode, Nlink in stat on Windows + return +} + +// handleTarTypeBlockCharFifo is an OS-specific helper function used by +// createTarFile to handle the following types of header: Block; Char; Fifo +func handleTarTypeBlockCharFifo(hdr *tar.Header, path string) error { + return nil +} + +func handleLChmod(hdr *tar.Header, path string, hdrInfo os.FileInfo) error { + return nil +} + +func getFileUIDGID(stat interface{}) (int, int, error) { + // no notion of file ownership mapping yet on Windows + return 0, 0, nil +} diff --git a/pkg/archive/archive_windows_test.go b/pkg/archive/archive_windows_test.go new file mode 100644 index 00000000..0c6733d6 --- /dev/null +++ b/pkg/archive/archive_windows_test.go @@ -0,0 +1,91 @@ +// +build windows + +package archive + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +func TestCopyFileWithInvalidDest(t *testing.T) { + // TODO Windows: This is currently failing. Not sure what has + // recently changed in CopyWithTar as used to pass. Further investigation + // is required. + t.Skip("Currently fails") + folder, err := ioutil.TempDir("", "docker-archive-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(folder) + dest := "c:dest" + srcFolder := filepath.Join(folder, "src") + src := filepath.Join(folder, "src", "src") + err = os.MkdirAll(srcFolder, 0740) + if err != nil { + t.Fatal(err) + } + ioutil.WriteFile(src, []byte("content"), 0777) + err = CopyWithTar(src, dest) + if err == nil { + t.Fatalf("archiver.CopyWithTar should throw an error on invalid dest.") + } +} + +func TestCanonicalTarNameForPath(t *testing.T) { + cases := []struct { + in, expected string + shouldFail bool + }{ + {"foo", "foo", false}, + {"foo/bar", "___", true}, // unix-styled windows path must fail + {`foo\bar`, "foo/bar", false}, + } + for _, v := range cases { + if out, err := CanonicalTarNameForPath(v.in); err != nil && !v.shouldFail { + t.Fatalf("cannot get canonical name for path: %s: %v", v.in, err) + } else if v.shouldFail && err == nil { + t.Fatalf("canonical path call should have failed with error. in=%s out=%s", v.in, out) + } else if !v.shouldFail && out != v.expected { + t.Fatalf("wrong canonical tar name. expected:%s got:%s", v.expected, out) + } + } +} + +func TestCanonicalTarName(t *testing.T) { + cases := []struct { + in string + isDir bool + expected string + }{ + {"foo", false, "foo"}, + {"foo", true, "foo/"}, + {`foo\bar`, false, "foo/bar"}, + {`foo\bar`, true, "foo/bar/"}, + } + for _, v := range cases { + if out, err := canonicalTarName(v.in, v.isDir); err != nil { + t.Fatalf("cannot get canonical name for path: %s: %v", v.in, err) + } else if out != v.expected { + t.Fatalf("wrong canonical tar name. expected:%s got:%s", v.expected, out) + } + } +} + +func TestChmodTarEntry(t *testing.T) { + cases := []struct { + in, expected os.FileMode + }{ + {0000, 0111}, + {0777, 0755}, + {0644, 0755}, + {0755, 0755}, + {0444, 0555}, + } + for _, v := range cases { + if out := chmodTarEntry(v.in); out != v.expected { + t.Fatalf("wrong chmod. expected:%v got:%v", v.expected, out) + } + } +} diff --git a/pkg/archive/changes.go b/pkg/archive/changes.go new file mode 100644 index 00000000..81651c61 --- /dev/null +++ b/pkg/archive/changes.go @@ -0,0 +1,416 @@ +package archive + +import ( + "archive/tar" + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "sort" + "strings" + "syscall" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/pools" + "github.com/docker/docker/pkg/system" +) + +// ChangeType represents the change type. +type ChangeType int + +const ( + // ChangeModify represents the modify operation. + ChangeModify = iota + // ChangeAdd represents the add operation. + ChangeAdd + // ChangeDelete represents the delete operation. + ChangeDelete +) + +func (c ChangeType) String() string { + switch c { + case ChangeModify: + return "C" + case ChangeAdd: + return "A" + case ChangeDelete: + return "D" + } + return "" +} + +// Change represents a change, it wraps the change type and path. +// It describes changes of the files in the path respect to the +// parent layers. The change could be modify, add, delete. +// This is used for layer diff. +type Change struct { + Path string + Kind ChangeType +} + +func (change *Change) String() string { + return fmt.Sprintf("%s %s", change.Kind, change.Path) +} + +// for sort.Sort +type changesByPath []Change + +func (c changesByPath) Less(i, j int) bool { return c[i].Path < c[j].Path } +func (c changesByPath) Len() int { return len(c) } +func (c changesByPath) Swap(i, j int) { c[j], c[i] = c[i], c[j] } + +// Gnu tar and the go tar writer don't have sub-second mtime +// precision, which is problematic when we apply changes via tar +// files, we handle this by comparing for exact times, *or* same +// second count and either a or b having exactly 0 nanoseconds +func sameFsTime(a, b time.Time) bool { + return a == b || + (a.Unix() == b.Unix() && + (a.Nanosecond() == 0 || b.Nanosecond() == 0)) +} + +func sameFsTimeSpec(a, b syscall.Timespec) bool { + return a.Sec == b.Sec && + (a.Nsec == b.Nsec || a.Nsec == 0 || b.Nsec == 0) +} + +// Changes walks the path rw and determines changes for the files in the path, +// with respect to the parent layers +func Changes(layers []string, rw string) ([]Change, error) { + var ( + changes []Change + changedDirs = make(map[string]struct{}) + ) + + err := filepath.Walk(rw, func(path string, f os.FileInfo, err error) error { + if err != nil { + return err + } + + // Rebase path + path, err = filepath.Rel(rw, path) + if err != nil { + return err + } + + // As this runs on the daemon side, file paths are OS specific. + path = filepath.Join(string(os.PathSeparator), path) + + // Skip root + if path == string(os.PathSeparator) { + return nil + } + + // Skip AUFS metadata + if matched, err := filepath.Match(string(os.PathSeparator)+WhiteoutMetaPrefix+"*", path); err != nil || matched { + return err + } + + change := Change{ + Path: path, + } + + // Find out what kind of modification happened + file := filepath.Base(path) + // If there is a whiteout, then the file was removed + if strings.HasPrefix(file, WhiteoutPrefix) { + originalFile := file[len(WhiteoutPrefix):] + change.Path = filepath.Join(filepath.Dir(path), originalFile) + change.Kind = ChangeDelete + } else { + // Otherwise, the file was added + change.Kind = ChangeAdd + + // ...Unless it already existed in a top layer, in which case, it's a modification + for _, layer := range layers { + stat, err := os.Stat(filepath.Join(layer, path)) + if err != nil && !os.IsNotExist(err) { + return err + } + if err == nil { + // The file existed in the top layer, so that's a modification + + // However, if it's a directory, maybe it wasn't actually modified. + // If you modify /foo/bar/baz, then /foo will be part of the changed files only because it's the parent of bar + if stat.IsDir() && f.IsDir() { + if f.Size() == stat.Size() && f.Mode() == stat.Mode() && sameFsTime(f.ModTime(), stat.ModTime()) { + // Both directories are the same, don't record the change + return nil + } + } + change.Kind = ChangeModify + break + } + } + } + + // If /foo/bar/file.txt is modified, then /foo/bar must be part of the changed files. + // This block is here to ensure the change is recorded even if the + // modify time, mode and size of the parent directory in the rw and ro layers are all equal. + // Check https://github.com/docker/docker/pull/13590 for details. + if f.IsDir() { + changedDirs[path] = struct{}{} + } + if change.Kind == ChangeAdd || change.Kind == ChangeDelete { + parent := filepath.Dir(path) + if _, ok := changedDirs[parent]; !ok && parent != "/" { + changes = append(changes, Change{Path: parent, Kind: ChangeModify}) + changedDirs[parent] = struct{}{} + } + } + + // Record change + changes = append(changes, change) + return nil + }) + if err != nil && !os.IsNotExist(err) { + return nil, err + } + return changes, nil +} + +// FileInfo describes the information of a file. +type FileInfo struct { + parent *FileInfo + name string + stat *system.StatT + children map[string]*FileInfo + capability []byte + added bool +} + +// LookUp looks up the file information of a file. +func (info *FileInfo) LookUp(path string) *FileInfo { + // As this runs on the daemon side, file paths are OS specific. + parent := info + if path == string(os.PathSeparator) { + return info + } + + pathElements := strings.Split(path, string(os.PathSeparator)) + for _, elem := range pathElements { + if elem != "" { + child := parent.children[elem] + if child == nil { + return nil + } + parent = child + } + } + return parent +} + +func (info *FileInfo) path() string { + if info.parent == nil { + // As this runs on the daemon side, file paths are OS specific. + return string(os.PathSeparator) + } + return filepath.Join(info.parent.path(), info.name) +} + +func (info *FileInfo) addChanges(oldInfo *FileInfo, changes *[]Change) { + + sizeAtEntry := len(*changes) + + if oldInfo == nil { + // add + change := Change{ + Path: info.path(), + Kind: ChangeAdd, + } + *changes = append(*changes, change) + info.added = true + } + + // We make a copy so we can modify it to detect additions + // also, we only recurse on the old dir if the new info is a directory + // otherwise any previous delete/change is considered recursive + oldChildren := make(map[string]*FileInfo) + if oldInfo != nil && info.isDir() { + for k, v := range oldInfo.children { + oldChildren[k] = v + } + } + + for name, newChild := range info.children { + oldChild, _ := oldChildren[name] + if oldChild != nil { + // change? + oldStat := oldChild.stat + newStat := newChild.stat + // Note: We can't compare inode or ctime or blocksize here, because these change + // when copying a file into a container. However, that is not generally a problem + // because any content change will change mtime, and any status change should + // be visible when actually comparing the stat fields. The only time this + // breaks down is if some code intentionally hides a change by setting + // back mtime + if statDifferent(oldStat, newStat) || + bytes.Compare(oldChild.capability, newChild.capability) != 0 { + change := Change{ + Path: newChild.path(), + Kind: ChangeModify, + } + *changes = append(*changes, change) + newChild.added = true + } + + // Remove from copy so we can detect deletions + delete(oldChildren, name) + } + + newChild.addChanges(oldChild, changes) + } + for _, oldChild := range oldChildren { + // delete + change := Change{ + Path: oldChild.path(), + Kind: ChangeDelete, + } + *changes = append(*changes, change) + } + + // If there were changes inside this directory, we need to add it, even if the directory + // itself wasn't changed. This is needed to properly save and restore filesystem permissions. + // As this runs on the daemon side, file paths are OS specific. + if len(*changes) > sizeAtEntry && info.isDir() && !info.added && info.path() != string(os.PathSeparator) { + change := Change{ + Path: info.path(), + Kind: ChangeModify, + } + // Let's insert the directory entry before the recently added entries located inside this dir + *changes = append(*changes, change) // just to resize the slice, will be overwritten + copy((*changes)[sizeAtEntry+1:], (*changes)[sizeAtEntry:]) + (*changes)[sizeAtEntry] = change + } + +} + +// Changes add changes to file information. +func (info *FileInfo) Changes(oldInfo *FileInfo) []Change { + var changes []Change + + info.addChanges(oldInfo, &changes) + + return changes +} + +func newRootFileInfo() *FileInfo { + // As this runs on the daemon side, file paths are OS specific. + root := &FileInfo{ + name: string(os.PathSeparator), + children: make(map[string]*FileInfo), + } + return root +} + +// ChangesDirs compares two directories and generates an array of Change objects describing the changes. +// If oldDir is "", then all files in newDir will be Add-Changes. +func ChangesDirs(newDir, oldDir string) ([]Change, error) { + var ( + oldRoot, newRoot *FileInfo + ) + if oldDir == "" { + emptyDir, err := ioutil.TempDir("", "empty") + if err != nil { + return nil, err + } + defer os.Remove(emptyDir) + oldDir = emptyDir + } + oldRoot, newRoot, err := collectFileInfoForChanges(oldDir, newDir) + if err != nil { + return nil, err + } + + return newRoot.Changes(oldRoot), nil +} + +// ChangesSize calculates the size in bytes of the provided changes, based on newDir. +func ChangesSize(newDir string, changes []Change) int64 { + var ( + size int64 + sf = make(map[uint64]struct{}) + ) + for _, change := range changes { + if change.Kind == ChangeModify || change.Kind == ChangeAdd { + file := filepath.Join(newDir, change.Path) + fileInfo, err := os.Lstat(file) + if err != nil { + logrus.Errorf("Can not stat %q: %s", file, err) + continue + } + + if fileInfo != nil && !fileInfo.IsDir() { + if hasHardlinks(fileInfo) { + inode := getIno(fileInfo) + if _, ok := sf[inode]; !ok { + size += fileInfo.Size() + sf[inode] = struct{}{} + } + } else { + size += fileInfo.Size() + } + } + } + } + return size +} + +// ExportChanges produces an Archive from the provided changes, relative to dir. +func ExportChanges(dir string, changes []Change, uidMaps, gidMaps []idtools.IDMap) (Archive, error) { + reader, writer := io.Pipe() + go func() { + ta := &tarAppender{ + TarWriter: tar.NewWriter(writer), + Buffer: pools.BufioWriter32KPool.Get(nil), + SeenFiles: make(map[uint64]string), + UIDMaps: uidMaps, + GIDMaps: gidMaps, + } + // this buffer is needed for the duration of this piped stream + defer pools.BufioWriter32KPool.Put(ta.Buffer) + + sort.Sort(changesByPath(changes)) + + // In general we log errors here but ignore them because + // during e.g. a diff operation the container can continue + // mutating the filesystem and we can see transient errors + // from this + for _, change := range changes { + if change.Kind == ChangeDelete { + whiteOutDir := filepath.Dir(change.Path) + whiteOutBase := filepath.Base(change.Path) + whiteOut := filepath.Join(whiteOutDir, WhiteoutPrefix+whiteOutBase) + timestamp := time.Now() + hdr := &tar.Header{ + Name: whiteOut[1:], + Size: 0, + ModTime: timestamp, + AccessTime: timestamp, + ChangeTime: timestamp, + } + if err := ta.TarWriter.WriteHeader(hdr); err != nil { + logrus.Debugf("Can't write whiteout header: %s", err) + } + } else { + path := filepath.Join(dir, change.Path) + if err := ta.addTarFile(path, change.Path[1:]); err != nil { + logrus.Debugf("Can't add file %s to tar: %s", path, err) + } + } + } + + // Make sure to check the error on Close. + if err := ta.TarWriter.Close(); err != nil { + logrus.Debugf("Can't close layer: %s", err) + } + if err := writer.Close(); err != nil { + logrus.Debugf("failed close Changes writer: %s", err) + } + }() + return reader, nil +} diff --git a/pkg/archive/changes_linux.go b/pkg/archive/changes_linux.go new file mode 100644 index 00000000..dee8b7c6 --- /dev/null +++ b/pkg/archive/changes_linux.go @@ -0,0 +1,285 @@ +package archive + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "sort" + "syscall" + "unsafe" + + "github.com/docker/docker/pkg/system" +) + +// walker is used to implement collectFileInfoForChanges on linux. Where this +// method in general returns the entire contents of two directory trees, we +// optimize some FS calls out on linux. In particular, we take advantage of the +// fact that getdents(2) returns the inode of each file in the directory being +// walked, which, when walking two trees in parallel to generate a list of +// changes, can be used to prune subtrees without ever having to lstat(2) them +// directly. Eliminating stat calls in this way can save up to seconds on large +// images. +type walker struct { + dir1 string + dir2 string + root1 *FileInfo + root2 *FileInfo +} + +// collectFileInfoForChanges returns a complete representation of the trees +// rooted at dir1 and dir2, with one important exception: any subtree or +// leaf where the inode and device numbers are an exact match between dir1 +// and dir2 will be pruned from the results. This method is *only* to be used +// to generating a list of changes between the two directories, as it does not +// reflect the full contents. +func collectFileInfoForChanges(dir1, dir2 string) (*FileInfo, *FileInfo, error) { + w := &walker{ + dir1: dir1, + dir2: dir2, + root1: newRootFileInfo(), + root2: newRootFileInfo(), + } + + i1, err := os.Lstat(w.dir1) + if err != nil { + return nil, nil, err + } + i2, err := os.Lstat(w.dir2) + if err != nil { + return nil, nil, err + } + + if err := w.walk("/", i1, i2); err != nil { + return nil, nil, err + } + + return w.root1, w.root2, nil +} + +// Given a FileInfo, its path info, and a reference to the root of the tree +// being constructed, register this file with the tree. +func walkchunk(path string, fi os.FileInfo, dir string, root *FileInfo) error { + if fi == nil { + return nil + } + parent := root.LookUp(filepath.Dir(path)) + if parent == nil { + return fmt.Errorf("collectFileInfoForChanges: Unexpectedly no parent for %s", path) + } + info := &FileInfo{ + name: filepath.Base(path), + children: make(map[string]*FileInfo), + parent: parent, + } + cpath := filepath.Join(dir, path) + stat, err := system.FromStatT(fi.Sys().(*syscall.Stat_t)) + if err != nil { + return err + } + info.stat = stat + info.capability, _ = system.Lgetxattr(cpath, "security.capability") // lgetxattr(2): fs access + parent.children[info.name] = info + return nil +} + +// Walk a subtree rooted at the same path in both trees being iterated. For +// example, /docker/overlay/1234/a/b/c/d and /docker/overlay/8888/a/b/c/d +func (w *walker) walk(path string, i1, i2 os.FileInfo) (err error) { + // Register these nodes with the return trees, unless we're still at the + // (already-created) roots: + if path != "/" { + if err := walkchunk(path, i1, w.dir1, w.root1); err != nil { + return err + } + if err := walkchunk(path, i2, w.dir2, w.root2); err != nil { + return err + } + } + + is1Dir := i1 != nil && i1.IsDir() + is2Dir := i2 != nil && i2.IsDir() + + sameDevice := false + if i1 != nil && i2 != nil { + si1 := i1.Sys().(*syscall.Stat_t) + si2 := i2.Sys().(*syscall.Stat_t) + if si1.Dev == si2.Dev { + sameDevice = true + } + } + + // If these files are both non-existent, or leaves (non-dirs), we are done. + if !is1Dir && !is2Dir { + return nil + } + + // Fetch the names of all the files contained in both directories being walked: + var names1, names2 []nameIno + if is1Dir { + names1, err = readdirnames(filepath.Join(w.dir1, path)) // getdents(2): fs access + if err != nil { + return err + } + } + if is2Dir { + names2, err = readdirnames(filepath.Join(w.dir2, path)) // getdents(2): fs access + if err != nil { + return err + } + } + + // We have lists of the files contained in both parallel directories, sorted + // in the same order. Walk them in parallel, generating a unique merged list + // of all items present in either or both directories. + var names []string + ix1 := 0 + ix2 := 0 + + for { + if ix1 >= len(names1) { + break + } + if ix2 >= len(names2) { + break + } + + ni1 := names1[ix1] + ni2 := names2[ix2] + + switch bytes.Compare([]byte(ni1.name), []byte(ni2.name)) { + case -1: // ni1 < ni2 -- advance ni1 + // we will not encounter ni1 in names2 + names = append(names, ni1.name) + ix1++ + case 0: // ni1 == ni2 + if ni1.ino != ni2.ino || !sameDevice { + names = append(names, ni1.name) + } + ix1++ + ix2++ + case 1: // ni1 > ni2 -- advance ni2 + // we will not encounter ni2 in names1 + names = append(names, ni2.name) + ix2++ + } + } + for ix1 < len(names1) { + names = append(names, names1[ix1].name) + ix1++ + } + for ix2 < len(names2) { + names = append(names, names2[ix2].name) + ix2++ + } + + // For each of the names present in either or both of the directories being + // iterated, stat the name under each root, and recurse the pair of them: + for _, name := range names { + fname := filepath.Join(path, name) + var cInfo1, cInfo2 os.FileInfo + if is1Dir { + cInfo1, err = os.Lstat(filepath.Join(w.dir1, fname)) // lstat(2): fs access + if err != nil && !os.IsNotExist(err) { + return err + } + } + if is2Dir { + cInfo2, err = os.Lstat(filepath.Join(w.dir2, fname)) // lstat(2): fs access + if err != nil && !os.IsNotExist(err) { + return err + } + } + if err = w.walk(fname, cInfo1, cInfo2); err != nil { + return err + } + } + return nil +} + +// {name,inode} pairs used to support the early-pruning logic of the walker type +type nameIno struct { + name string + ino uint64 +} + +type nameInoSlice []nameIno + +func (s nameInoSlice) Len() int { return len(s) } +func (s nameInoSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s nameInoSlice) Less(i, j int) bool { return s[i].name < s[j].name } + +// readdirnames is a hacked-apart version of the Go stdlib code, exposing inode +// numbers further up the stack when reading directory contents. Unlike +// os.Readdirnames, which returns a list of filenames, this function returns a +// list of {filename,inode} pairs. +func readdirnames(dirname string) (names []nameIno, err error) { + var ( + size = 100 + buf = make([]byte, 4096) + nbuf int + bufp int + nb int + ) + + f, err := os.Open(dirname) + if err != nil { + return nil, err + } + defer f.Close() + + names = make([]nameIno, 0, size) // Empty with room to grow. + for { + // Refill the buffer if necessary + if bufp >= nbuf { + bufp = 0 + nbuf, err = syscall.ReadDirent(int(f.Fd()), buf) // getdents on linux + if nbuf < 0 { + nbuf = 0 + } + if err != nil { + return nil, os.NewSyscallError("readdirent", err) + } + if nbuf <= 0 { + break // EOF + } + } + + // Drain the buffer + nb, names = parseDirent(buf[bufp:nbuf], names) + bufp += nb + } + + sl := nameInoSlice(names) + sort.Sort(sl) + return sl, nil +} + +// parseDirent is a minor modification of syscall.ParseDirent (linux version) +// which returns {name,inode} pairs instead of just names. +func parseDirent(buf []byte, names []nameIno) (consumed int, newnames []nameIno) { + origlen := len(buf) + for len(buf) > 0 { + dirent := (*syscall.Dirent)(unsafe.Pointer(&buf[0])) + buf = buf[dirent.Reclen:] + if dirent.Ino == 0 { // File absent in directory. + continue + } + bytes := (*[10000]byte)(unsafe.Pointer(&dirent.Name[0])) + var name = string(bytes[0:clen(bytes[:])]) + if name == "." || name == ".." { // Useless names + continue + } + names = append(names, nameIno{name, dirent.Ino}) + } + return origlen - len(buf), names +} + +func clen(n []byte) int { + for i := 0; i < len(n); i++ { + if n[i] == 0 { + return i + } + } + return len(n) +} diff --git a/pkg/archive/changes_other.go b/pkg/archive/changes_other.go new file mode 100644 index 00000000..da70ed37 --- /dev/null +++ b/pkg/archive/changes_other.go @@ -0,0 +1,97 @@ +// +build !linux + +package archive + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/docker/docker/pkg/system" +) + +func collectFileInfoForChanges(oldDir, newDir string) (*FileInfo, *FileInfo, error) { + var ( + oldRoot, newRoot *FileInfo + err1, err2 error + errs = make(chan error, 2) + ) + go func() { + oldRoot, err1 = collectFileInfo(oldDir) + errs <- err1 + }() + go func() { + newRoot, err2 = collectFileInfo(newDir) + errs <- err2 + }() + + // block until both routines have returned + for i := 0; i < 2; i++ { + if err := <-errs; err != nil { + return nil, nil, err + } + } + + return oldRoot, newRoot, nil +} + +func collectFileInfo(sourceDir string) (*FileInfo, error) { + root := newRootFileInfo() + + err := filepath.Walk(sourceDir, func(path string, f os.FileInfo, err error) error { + if err != nil { + return err + } + + // Rebase path + relPath, err := filepath.Rel(sourceDir, path) + if err != nil { + return err + } + + // As this runs on the daemon side, file paths are OS specific. + relPath = filepath.Join(string(os.PathSeparator), relPath) + + // See https://github.com/golang/go/issues/9168 - bug in filepath.Join. + // Temporary workaround. If the returned path starts with two backslashes, + // trim it down to a single backslash. Only relevant on Windows. + if runtime.GOOS == "windows" { + if strings.HasPrefix(relPath, `\\`) { + relPath = relPath[1:] + } + } + + if relPath == string(os.PathSeparator) { + return nil + } + + parent := root.LookUp(filepath.Dir(relPath)) + if parent == nil { + return fmt.Errorf("collectFileInfo: Unexpectedly no parent for %s", relPath) + } + + info := &FileInfo{ + name: filepath.Base(relPath), + children: make(map[string]*FileInfo), + parent: parent, + } + + s, err := system.Lstat(path) + if err != nil { + return err + } + info.stat = s + + info.capability, _ = system.Lgetxattr(path, "security.capability") + + parent.children[info.name] = info + + return nil + }) + if err != nil { + return nil, err + } + return root, nil +} diff --git a/pkg/archive/changes_posix_test.go b/pkg/archive/changes_posix_test.go new file mode 100644 index 00000000..5a3282b5 --- /dev/null +++ b/pkg/archive/changes_posix_test.go @@ -0,0 +1,127 @@ +package archive + +import ( + "archive/tar" + "fmt" + "io" + "io/ioutil" + "os" + "path" + "sort" + "testing" +) + +func TestHardLinkOrder(t *testing.T) { + names := []string{"file1.txt", "file2.txt", "file3.txt"} + msg := []byte("Hey y'all") + + // Create dir + src, err := ioutil.TempDir("", "docker-hardlink-test-src-") + if err != nil { + t.Fatal(err) + } + //defer os.RemoveAll(src) + for _, name := range names { + func() { + fh, err := os.Create(path.Join(src, name)) + if err != nil { + t.Fatal(err) + } + defer fh.Close() + if _, err = fh.Write(msg); err != nil { + t.Fatal(err) + } + }() + } + // Create dest, with changes that includes hardlinks + dest, err := ioutil.TempDir("", "docker-hardlink-test-dest-") + if err != nil { + t.Fatal(err) + } + os.RemoveAll(dest) // we just want the name, at first + if err := copyDir(src, dest); err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dest) + for _, name := range names { + for i := 0; i < 5; i++ { + if err := os.Link(path.Join(dest, name), path.Join(dest, fmt.Sprintf("%s.link%d", name, i))); err != nil { + t.Fatal(err) + } + } + } + + // get changes + changes, err := ChangesDirs(dest, src) + if err != nil { + t.Fatal(err) + } + + // sort + sort.Sort(changesByPath(changes)) + + // ExportChanges + ar, err := ExportChanges(dest, changes, nil, nil) + if err != nil { + t.Fatal(err) + } + hdrs, err := walkHeaders(ar) + if err != nil { + t.Fatal(err) + } + + // reverse sort + sort.Sort(sort.Reverse(changesByPath(changes))) + // ExportChanges + arRev, err := ExportChanges(dest, changes, nil, nil) + if err != nil { + t.Fatal(err) + } + hdrsRev, err := walkHeaders(arRev) + if err != nil { + t.Fatal(err) + } + + // line up the two sets + sort.Sort(tarHeaders(hdrs)) + sort.Sort(tarHeaders(hdrsRev)) + + // compare Size and LinkName + for i := range hdrs { + if hdrs[i].Name != hdrsRev[i].Name { + t.Errorf("headers - expected name %q; but got %q", hdrs[i].Name, hdrsRev[i].Name) + } + if hdrs[i].Size != hdrsRev[i].Size { + t.Errorf("headers - %q expected size %d; but got %d", hdrs[i].Name, hdrs[i].Size, hdrsRev[i].Size) + } + if hdrs[i].Typeflag != hdrsRev[i].Typeflag { + t.Errorf("headers - %q expected type %d; but got %d", hdrs[i].Name, hdrs[i].Typeflag, hdrsRev[i].Typeflag) + } + if hdrs[i].Linkname != hdrsRev[i].Linkname { + t.Errorf("headers - %q expected linkname %q; but got %q", hdrs[i].Name, hdrs[i].Linkname, hdrsRev[i].Linkname) + } + } + +} + +type tarHeaders []tar.Header + +func (th tarHeaders) Len() int { return len(th) } +func (th tarHeaders) Swap(i, j int) { th[j], th[i] = th[i], th[j] } +func (th tarHeaders) Less(i, j int) bool { return th[i].Name < th[j].Name } + +func walkHeaders(r io.Reader) ([]tar.Header, error) { + t := tar.NewReader(r) + headers := []tar.Header{} + for { + hdr, err := t.Next() + if err != nil { + if err == io.EOF { + break + } + return headers, err + } + headers = append(headers, *hdr) + } + return headers, nil +} diff --git a/pkg/archive/changes_test.go b/pkg/archive/changes_test.go new file mode 100644 index 00000000..bca68250 --- /dev/null +++ b/pkg/archive/changes_test.go @@ -0,0 +1,565 @@ +package archive + +import ( + "io/ioutil" + "os" + "os/exec" + "path" + "runtime" + "sort" + "testing" + "time" + + "github.com/docker/docker/pkg/system" +) + +func max(x, y int) int { + if x >= y { + return x + } + return y +} + +func copyDir(src, dst string) error { + cmd := exec.Command("cp", "-a", src, dst) + if err := cmd.Run(); err != nil { + return err + } + return nil +} + +type FileType uint32 + +const ( + Regular FileType = iota + Dir + Symlink +) + +type FileData struct { + filetype FileType + path string + contents string + permissions os.FileMode +} + +func createSampleDir(t *testing.T, root string) { + files := []FileData{ + {Regular, "file1", "file1\n", 0600}, + {Regular, "file2", "file2\n", 0666}, + {Regular, "file3", "file3\n", 0404}, + {Regular, "file4", "file4\n", 0600}, + {Regular, "file5", "file5\n", 0600}, + {Regular, "file6", "file6\n", 0600}, + {Regular, "file7", "file7\n", 0600}, + {Dir, "dir1", "", 0740}, + {Regular, "dir1/file1-1", "file1-1\n", 01444}, + {Regular, "dir1/file1-2", "file1-2\n", 0666}, + {Dir, "dir2", "", 0700}, + {Regular, "dir2/file2-1", "file2-1\n", 0666}, + {Regular, "dir2/file2-2", "file2-2\n", 0666}, + {Dir, "dir3", "", 0700}, + {Regular, "dir3/file3-1", "file3-1\n", 0666}, + {Regular, "dir3/file3-2", "file3-2\n", 0666}, + {Dir, "dir4", "", 0700}, + {Regular, "dir4/file3-1", "file4-1\n", 0666}, + {Regular, "dir4/file3-2", "file4-2\n", 0666}, + {Symlink, "symlink1", "target1", 0666}, + {Symlink, "symlink2", "target2", 0666}, + {Symlink, "symlink3", root + "/file1", 0666}, + {Symlink, "symlink4", root + "/symlink3", 0666}, + {Symlink, "dirSymlink", root + "/dir1", 0740}, + } + + now := time.Now() + for _, info := range files { + p := path.Join(root, info.path) + if info.filetype == Dir { + if err := os.MkdirAll(p, info.permissions); err != nil { + t.Fatal(err) + } + } else if info.filetype == Regular { + if err := ioutil.WriteFile(p, []byte(info.contents), info.permissions); err != nil { + t.Fatal(err) + } + } else if info.filetype == Symlink { + if err := os.Symlink(info.contents, p); err != nil { + t.Fatal(err) + } + } + + if info.filetype != Symlink { + // Set a consistent ctime, atime for all files and dirs + if err := system.Chtimes(p, now, now); err != nil { + t.Fatal(err) + } + } + } +} + +func TestChangeString(t *testing.T) { + modifiyChange := Change{"change", ChangeModify} + toString := modifiyChange.String() + if toString != "C change" { + t.Fatalf("String() of a change with ChangeModifiy Kind should have been %s but was %s", "C change", toString) + } + addChange := Change{"change", ChangeAdd} + toString = addChange.String() + if toString != "A change" { + t.Fatalf("String() of a change with ChangeAdd Kind should have been %s but was %s", "A change", toString) + } + deleteChange := Change{"change", ChangeDelete} + toString = deleteChange.String() + if toString != "D change" { + t.Fatalf("String() of a change with ChangeDelete Kind should have been %s but was %s", "D change", toString) + } +} + +func TestChangesWithNoChanges(t *testing.T) { + // TODO Windows. There may be a way of running this, but turning off for now + // as createSampleDir uses symlinks. + if runtime.GOOS == "windows" { + t.Skip("symlinks on Windows") + } + rwLayer, err := ioutil.TempDir("", "docker-changes-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(rwLayer) + layer, err := ioutil.TempDir("", "docker-changes-test-layer") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(layer) + createSampleDir(t, layer) + changes, err := Changes([]string{layer}, rwLayer) + if err != nil { + t.Fatal(err) + } + if len(changes) != 0 { + t.Fatalf("Changes with no difference should have detect no changes, but detected %d", len(changes)) + } +} + +func TestChangesWithChanges(t *testing.T) { + // TODO Windows. There may be a way of running this, but turning off for now + // as createSampleDir uses symlinks. + if runtime.GOOS == "windows" { + t.Skip("symlinks on Windows") + } + // Mock the readonly layer + layer, err := ioutil.TempDir("", "docker-changes-test-layer") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(layer) + createSampleDir(t, layer) + os.MkdirAll(path.Join(layer, "dir1/subfolder"), 0740) + + // Mock the RW layer + rwLayer, err := ioutil.TempDir("", "docker-changes-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(rwLayer) + + // Create a folder in RW layer + dir1 := path.Join(rwLayer, "dir1") + os.MkdirAll(dir1, 0740) + deletedFile := path.Join(dir1, ".wh.file1-2") + ioutil.WriteFile(deletedFile, []byte{}, 0600) + modifiedFile := path.Join(dir1, "file1-1") + ioutil.WriteFile(modifiedFile, []byte{0x00}, 01444) + // Let's add a subfolder for a newFile + subfolder := path.Join(dir1, "subfolder") + os.MkdirAll(subfolder, 0740) + newFile := path.Join(subfolder, "newFile") + ioutil.WriteFile(newFile, []byte{}, 0740) + + changes, err := Changes([]string{layer}, rwLayer) + if err != nil { + t.Fatal(err) + } + + expectedChanges := []Change{ + {"/dir1", ChangeModify}, + {"/dir1/file1-1", ChangeModify}, + {"/dir1/file1-2", ChangeDelete}, + {"/dir1/subfolder", ChangeModify}, + {"/dir1/subfolder/newFile", ChangeAdd}, + } + checkChanges(expectedChanges, changes, t) +} + +// See https://github.com/docker/docker/pull/13590 +func TestChangesWithChangesGH13590(t *testing.T) { + // TODO Windows. There may be a way of running this, but turning off for now + // as createSampleDir uses symlinks. + if runtime.GOOS == "windows" { + t.Skip("symlinks on Windows") + } + baseLayer, err := ioutil.TempDir("", "docker-changes-test.") + defer os.RemoveAll(baseLayer) + + dir3 := path.Join(baseLayer, "dir1/dir2/dir3") + os.MkdirAll(dir3, 07400) + + file := path.Join(dir3, "file.txt") + ioutil.WriteFile(file, []byte("hello"), 0666) + + layer, err := ioutil.TempDir("", "docker-changes-test2.") + defer os.RemoveAll(layer) + + // Test creating a new file + if err := copyDir(baseLayer+"/dir1", layer+"/"); err != nil { + t.Fatalf("Cmd failed: %q", err) + } + + os.Remove(path.Join(layer, "dir1/dir2/dir3/file.txt")) + file = path.Join(layer, "dir1/dir2/dir3/file1.txt") + ioutil.WriteFile(file, []byte("bye"), 0666) + + changes, err := Changes([]string{baseLayer}, layer) + if err != nil { + t.Fatal(err) + } + + expectedChanges := []Change{ + {"/dir1/dir2/dir3", ChangeModify}, + {"/dir1/dir2/dir3/file1.txt", ChangeAdd}, + } + checkChanges(expectedChanges, changes, t) + + // Now test changing a file + layer, err = ioutil.TempDir("", "docker-changes-test3.") + defer os.RemoveAll(layer) + + if err := copyDir(baseLayer+"/dir1", layer+"/"); err != nil { + t.Fatalf("Cmd failed: %q", err) + } + + file = path.Join(layer, "dir1/dir2/dir3/file.txt") + ioutil.WriteFile(file, []byte("bye"), 0666) + + changes, err = Changes([]string{baseLayer}, layer) + if err != nil { + t.Fatal(err) + } + + expectedChanges = []Change{ + {"/dir1/dir2/dir3/file.txt", ChangeModify}, + } + checkChanges(expectedChanges, changes, t) +} + +// Create an directory, copy it, make sure we report no changes between the two +func TestChangesDirsEmpty(t *testing.T) { + // TODO Windows. There may be a way of running this, but turning off for now + // as createSampleDir uses symlinks. + if runtime.GOOS == "windows" { + t.Skip("symlinks on Windows") + } + src, err := ioutil.TempDir("", "docker-changes-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(src) + createSampleDir(t, src) + dst := src + "-copy" + if err := copyDir(src, dst); err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dst) + changes, err := ChangesDirs(dst, src) + if err != nil { + t.Fatal(err) + } + + if len(changes) != 0 { + t.Fatalf("Reported changes for identical dirs: %v", changes) + } + os.RemoveAll(src) + os.RemoveAll(dst) +} + +func mutateSampleDir(t *testing.T, root string) { + // Remove a regular file + if err := os.RemoveAll(path.Join(root, "file1")); err != nil { + t.Fatal(err) + } + + // Remove a directory + if err := os.RemoveAll(path.Join(root, "dir1")); err != nil { + t.Fatal(err) + } + + // Remove a symlink + if err := os.RemoveAll(path.Join(root, "symlink1")); err != nil { + t.Fatal(err) + } + + // Rewrite a file + if err := ioutil.WriteFile(path.Join(root, "file2"), []byte("fileNN\n"), 0777); err != nil { + t.Fatal(err) + } + + // Replace a file + if err := os.RemoveAll(path.Join(root, "file3")); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(path.Join(root, "file3"), []byte("fileMM\n"), 0404); err != nil { + t.Fatal(err) + } + + // Touch file + if err := system.Chtimes(path.Join(root, "file4"), time.Now().Add(time.Second), time.Now().Add(time.Second)); err != nil { + t.Fatal(err) + } + + // Replace file with dir + if err := os.RemoveAll(path.Join(root, "file5")); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(path.Join(root, "file5"), 0666); err != nil { + t.Fatal(err) + } + + // Create new file + if err := ioutil.WriteFile(path.Join(root, "filenew"), []byte("filenew\n"), 0777); err != nil { + t.Fatal(err) + } + + // Create new dir + if err := os.MkdirAll(path.Join(root, "dirnew"), 0766); err != nil { + t.Fatal(err) + } + + // Create a new symlink + if err := os.Symlink("targetnew", path.Join(root, "symlinknew")); err != nil { + t.Fatal(err) + } + + // Change a symlink + if err := os.RemoveAll(path.Join(root, "symlink2")); err != nil { + t.Fatal(err) + } + if err := os.Symlink("target2change", path.Join(root, "symlink2")); err != nil { + t.Fatal(err) + } + + // Replace dir with file + if err := os.RemoveAll(path.Join(root, "dir2")); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(path.Join(root, "dir2"), []byte("dir2\n"), 0777); err != nil { + t.Fatal(err) + } + + // Touch dir + if err := system.Chtimes(path.Join(root, "dir3"), time.Now().Add(time.Second), time.Now().Add(time.Second)); err != nil { + t.Fatal(err) + } +} + +func TestChangesDirsMutated(t *testing.T) { + // TODO Windows. There may be a way of running this, but turning off for now + // as createSampleDir uses symlinks. + if runtime.GOOS == "windows" { + t.Skip("symlinks on Windows") + } + src, err := ioutil.TempDir("", "docker-changes-test") + if err != nil { + t.Fatal(err) + } + createSampleDir(t, src) + dst := src + "-copy" + if err := copyDir(src, dst); err != nil { + t.Fatal(err) + } + defer os.RemoveAll(src) + defer os.RemoveAll(dst) + + mutateSampleDir(t, dst) + + changes, err := ChangesDirs(dst, src) + if err != nil { + t.Fatal(err) + } + + sort.Sort(changesByPath(changes)) + + expectedChanges := []Change{ + {"/dir1", ChangeDelete}, + {"/dir2", ChangeModify}, + {"/dirnew", ChangeAdd}, + {"/file1", ChangeDelete}, + {"/file2", ChangeModify}, + {"/file3", ChangeModify}, + {"/file4", ChangeModify}, + {"/file5", ChangeModify}, + {"/filenew", ChangeAdd}, + {"/symlink1", ChangeDelete}, + {"/symlink2", ChangeModify}, + {"/symlinknew", ChangeAdd}, + } + + for i := 0; i < max(len(changes), len(expectedChanges)); i++ { + if i >= len(expectedChanges) { + t.Fatalf("unexpected change %s\n", changes[i].String()) + } + if i >= len(changes) { + t.Fatalf("no change for expected change %s\n", expectedChanges[i].String()) + } + if changes[i].Path == expectedChanges[i].Path { + if changes[i] != expectedChanges[i] { + t.Fatalf("Wrong change for %s, expected %s, got %s\n", changes[i].Path, changes[i].String(), expectedChanges[i].String()) + } + } else if changes[i].Path < expectedChanges[i].Path { + t.Fatalf("unexpected change %s\n", changes[i].String()) + } else { + t.Fatalf("no change for expected change %s != %s\n", expectedChanges[i].String(), changes[i].String()) + } + } +} + +func TestApplyLayer(t *testing.T) { + // TODO Windows. There may be a way of running this, but turning off for now + // as createSampleDir uses symlinks. + if runtime.GOOS == "windows" { + t.Skip("symlinks on Windows") + } + src, err := ioutil.TempDir("", "docker-changes-test") + if err != nil { + t.Fatal(err) + } + createSampleDir(t, src) + defer os.RemoveAll(src) + dst := src + "-copy" + if err := copyDir(src, dst); err != nil { + t.Fatal(err) + } + mutateSampleDir(t, dst) + defer os.RemoveAll(dst) + + changes, err := ChangesDirs(dst, src) + if err != nil { + t.Fatal(err) + } + + layer, err := ExportChanges(dst, changes, nil, nil) + if err != nil { + t.Fatal(err) + } + + layerCopy, err := NewTempArchive(layer, "") + if err != nil { + t.Fatal(err) + } + + if _, err := ApplyLayer(src, layerCopy); err != nil { + t.Fatal(err) + } + + changes2, err := ChangesDirs(src, dst) + if err != nil { + t.Fatal(err) + } + + if len(changes2) != 0 { + t.Fatalf("Unexpected differences after reapplying mutation: %v", changes2) + } +} + +func TestChangesSizeWithHardlinks(t *testing.T) { + // TODO Windows. There may be a way of running this, but turning off for now + // as createSampleDir uses symlinks. + if runtime.GOOS == "windows" { + t.Skip("hardlinks on Windows") + } + srcDir, err := ioutil.TempDir("", "docker-test-srcDir") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(srcDir) + + destDir, err := ioutil.TempDir("", "docker-test-destDir") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(destDir) + + creationSize, err := prepareUntarSourceDirectory(100, destDir, true) + if err != nil { + t.Fatal(err) + } + + changes, err := ChangesDirs(destDir, srcDir) + if err != nil { + t.Fatal(err) + } + + got := ChangesSize(destDir, changes) + if got != int64(creationSize) { + t.Errorf("Expected %d bytes of changes, got %d", creationSize, got) + } +} + +func TestChangesSizeWithNoChanges(t *testing.T) { + size := ChangesSize("/tmp", nil) + if size != 0 { + t.Fatalf("ChangesSizes with no changes should be 0, was %d", size) + } +} + +func TestChangesSizeWithOnlyDeleteChanges(t *testing.T) { + changes := []Change{ + {Path: "deletedPath", Kind: ChangeDelete}, + } + size := ChangesSize("/tmp", changes) + if size != 0 { + t.Fatalf("ChangesSizes with only delete changes should be 0, was %d", size) + } +} + +func TestChangesSize(t *testing.T) { + parentPath, err := ioutil.TempDir("", "docker-changes-test") + defer os.RemoveAll(parentPath) + addition := path.Join(parentPath, "addition") + if err := ioutil.WriteFile(addition, []byte{0x01, 0x01, 0x01}, 0744); err != nil { + t.Fatal(err) + } + modification := path.Join(parentPath, "modification") + if err = ioutil.WriteFile(modification, []byte{0x01, 0x01, 0x01}, 0744); err != nil { + t.Fatal(err) + } + changes := []Change{ + {Path: "addition", Kind: ChangeAdd}, + {Path: "modification", Kind: ChangeModify}, + } + size := ChangesSize(parentPath, changes) + if size != 6 { + t.Fatalf("Expected 6 bytes of changes, got %d", size) + } +} + +func checkChanges(expectedChanges, changes []Change, t *testing.T) { + sort.Sort(changesByPath(expectedChanges)) + sort.Sort(changesByPath(changes)) + for i := 0; i < max(len(changes), len(expectedChanges)); i++ { + if i >= len(expectedChanges) { + t.Fatalf("unexpected change %s\n", changes[i].String()) + } + if i >= len(changes) { + t.Fatalf("no change for expected change %s\n", expectedChanges[i].String()) + } + if changes[i].Path == expectedChanges[i].Path { + if changes[i] != expectedChanges[i] { + t.Fatalf("Wrong change for %s, expected %s, got %s\n", changes[i].Path, changes[i].String(), expectedChanges[i].String()) + } + } else if changes[i].Path < expectedChanges[i].Path { + t.Fatalf("unexpected change %s\n", changes[i].String()) + } else { + t.Fatalf("no change for expected change %s != %s\n", expectedChanges[i].String(), changes[i].String()) + } + } +} diff --git a/pkg/archive/changes_unix.go b/pkg/archive/changes_unix.go new file mode 100644 index 00000000..3778b732 --- /dev/null +++ b/pkg/archive/changes_unix.go @@ -0,0 +1,36 @@ +// +build !windows + +package archive + +import ( + "os" + "syscall" + + "github.com/docker/docker/pkg/system" +) + +func statDifferent(oldStat *system.StatT, newStat *system.StatT) bool { + // Don't look at size for dirs, its not a good measure of change + if oldStat.Mode() != newStat.Mode() || + oldStat.UID() != newStat.UID() || + oldStat.GID() != newStat.GID() || + oldStat.Rdev() != newStat.Rdev() || + // Don't look at size for dirs, its not a good measure of change + (oldStat.Mode()&syscall.S_IFDIR != syscall.S_IFDIR && + (!sameFsTimeSpec(oldStat.Mtim(), newStat.Mtim()) || (oldStat.Size() != newStat.Size()))) { + return true + } + return false +} + +func (info *FileInfo) isDir() bool { + return info.parent == nil || info.stat.Mode()&syscall.S_IFDIR != 0 +} + +func getIno(fi os.FileInfo) uint64 { + return uint64(fi.Sys().(*syscall.Stat_t).Ino) +} + +func hasHardlinks(fi os.FileInfo) bool { + return fi.Sys().(*syscall.Stat_t).Nlink > 1 +} diff --git a/pkg/archive/changes_windows.go b/pkg/archive/changes_windows.go new file mode 100644 index 00000000..af94243f --- /dev/null +++ b/pkg/archive/changes_windows.go @@ -0,0 +1,30 @@ +package archive + +import ( + "os" + + "github.com/docker/docker/pkg/system" +) + +func statDifferent(oldStat *system.StatT, newStat *system.StatT) bool { + + // Don't look at size for dirs, its not a good measure of change + if oldStat.ModTime() != newStat.ModTime() || + oldStat.Mode() != newStat.Mode() || + oldStat.Size() != newStat.Size() && !oldStat.IsDir() { + return true + } + return false +} + +func (info *FileInfo) isDir() bool { + return info.parent == nil || info.stat.IsDir() +} + +func getIno(fi os.FileInfo) (inode uint64) { + return +} + +func hasHardlinks(fi os.FileInfo) bool { + return false +} diff --git a/pkg/archive/copy.go b/pkg/archive/copy.go new file mode 100644 index 00000000..e1fa73f3 --- /dev/null +++ b/pkg/archive/copy.go @@ -0,0 +1,458 @@ +package archive + +import ( + "archive/tar" + "errors" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/system" +) + +// Errors used or returned by this file. +var ( + ErrNotDirectory = errors.New("not a directory") + ErrDirNotExists = errors.New("no such directory") + ErrCannotCopyDir = errors.New("cannot copy directory") + ErrInvalidCopySource = errors.New("invalid copy source content") +) + +// PreserveTrailingDotOrSeparator returns the given cleaned path (after +// processing using any utility functions from the path or filepath stdlib +// packages) and appends a trailing `/.` or `/` if its corresponding original +// path (from before being processed by utility functions from the path or +// filepath stdlib packages) ends with a trailing `/.` or `/`. If the cleaned +// path already ends in a `.` path segment, then another is not added. If the +// clean path already ends in a path separator, then another is not added. +func PreserveTrailingDotOrSeparator(cleanedPath, originalPath string) string { + // Ensure paths are in platform semantics + cleanedPath = normalizePath(cleanedPath) + originalPath = normalizePath(originalPath) + + if !specifiesCurrentDir(cleanedPath) && specifiesCurrentDir(originalPath) { + if !hasTrailingPathSeparator(cleanedPath) { + // Add a separator if it doesn't already end with one (a cleaned + // path would only end in a separator if it is the root). + cleanedPath += string(filepath.Separator) + } + cleanedPath += "." + } + + if !hasTrailingPathSeparator(cleanedPath) && hasTrailingPathSeparator(originalPath) { + cleanedPath += string(filepath.Separator) + } + + return cleanedPath +} + +// assertsDirectory returns whether the given path is +// asserted to be a directory, i.e., the path ends with +// a trailing '/' or `/.`, assuming a path separator of `/`. +func assertsDirectory(path string) bool { + return hasTrailingPathSeparator(path) || specifiesCurrentDir(path) +} + +// hasTrailingPathSeparator returns whether the given +// path ends with the system's path separator character. +func hasTrailingPathSeparator(path string) bool { + return len(path) > 0 && os.IsPathSeparator(path[len(path)-1]) +} + +// specifiesCurrentDir returns whether the given path specifies +// a "current directory", i.e., the last path segment is `.`. +func specifiesCurrentDir(path string) bool { + return filepath.Base(path) == "." +} + +// SplitPathDirEntry splits the given path between its directory name and its +// basename by first cleaning the path but preserves a trailing "." if the +// original path specified the current directory. +func SplitPathDirEntry(path string) (dir, base string) { + cleanedPath := filepath.Clean(normalizePath(path)) + + if specifiesCurrentDir(path) { + cleanedPath += string(filepath.Separator) + "." + } + + return filepath.Dir(cleanedPath), filepath.Base(cleanedPath) +} + +// TarResource archives the resource described by the given CopyInfo to a Tar +// archive. A non-nil error is returned if sourcePath does not exist or is +// asserted to be a directory but exists as another type of file. +// +// This function acts as a convenient wrapper around TarWithOptions, which +// requires a directory as the source path. TarResource accepts either a +// directory or a file path and correctly sets the Tar options. +func TarResource(sourceInfo CopyInfo) (content Archive, err error) { + return TarResourceRebase(sourceInfo.Path, sourceInfo.RebaseName) +} + +// TarResourceRebase is like TarResource but renames the first path element of +// items in the resulting tar archive to match the given rebaseName if not "". +func TarResourceRebase(sourcePath, rebaseName string) (content Archive, err error) { + sourcePath = normalizePath(sourcePath) + if _, err = os.Lstat(sourcePath); err != nil { + // Catches the case where the source does not exist or is not a + // directory if asserted to be a directory, as this also causes an + // error. + return + } + + // Separate the source path between it's directory and + // the entry in that directory which we are archiving. + sourceDir, sourceBase := SplitPathDirEntry(sourcePath) + + filter := []string{sourceBase} + + logrus.Debugf("copying %q from %q", sourceBase, sourceDir) + + return TarWithOptions(sourceDir, &TarOptions{ + Compression: Uncompressed, + IncludeFiles: filter, + IncludeSourceDir: true, + RebaseNames: map[string]string{ + sourceBase: rebaseName, + }, + }) +} + +// CopyInfo holds basic info about the source +// or destination path of a copy operation. +type CopyInfo struct { + Path string + Exists bool + IsDir bool + RebaseName string +} + +// CopyInfoSourcePath stats the given path to create a CopyInfo +// struct representing that resource for the source of an archive copy +// operation. The given path should be an absolute local path. A source path +// has all symlinks evaluated that appear before the last path separator ("/" +// on Unix). As it is to be a copy source, the path must exist. +func CopyInfoSourcePath(path string, followLink bool) (CopyInfo, error) { + // normalize the file path and then evaluate the symbol link + // we will use the target file instead of the symbol link if + // followLink is set + path = normalizePath(path) + + resolvedPath, rebaseName, err := ResolveHostSourcePath(path, followLink) + if err != nil { + return CopyInfo{}, err + } + + stat, err := os.Lstat(resolvedPath) + if err != nil { + return CopyInfo{}, err + } + + return CopyInfo{ + Path: resolvedPath, + Exists: true, + IsDir: stat.IsDir(), + RebaseName: rebaseName, + }, nil +} + +// CopyInfoDestinationPath stats the given path to create a CopyInfo +// struct representing that resource for the destination of an archive copy +// operation. The given path should be an absolute local path. +func CopyInfoDestinationPath(path string) (info CopyInfo, err error) { + maxSymlinkIter := 10 // filepath.EvalSymlinks uses 255, but 10 already seems like a lot. + path = normalizePath(path) + originalPath := path + + stat, err := os.Lstat(path) + + if err == nil && stat.Mode()&os.ModeSymlink == 0 { + // The path exists and is not a symlink. + return CopyInfo{ + Path: path, + Exists: true, + IsDir: stat.IsDir(), + }, nil + } + + // While the path is a symlink. + for n := 0; err == nil && stat.Mode()&os.ModeSymlink != 0; n++ { + if n > maxSymlinkIter { + // Don't follow symlinks more than this arbitrary number of times. + return CopyInfo{}, errors.New("too many symlinks in " + originalPath) + } + + // The path is a symbolic link. We need to evaluate it so that the + // destination of the copy operation is the link target and not the + // link itself. This is notably different than CopyInfoSourcePath which + // only evaluates symlinks before the last appearing path separator. + // Also note that it is okay if the last path element is a broken + // symlink as the copy operation should create the target. + var linkTarget string + + linkTarget, err = os.Readlink(path) + if err != nil { + return CopyInfo{}, err + } + + if !system.IsAbs(linkTarget) { + // Join with the parent directory. + dstParent, _ := SplitPathDirEntry(path) + linkTarget = filepath.Join(dstParent, linkTarget) + } + + path = linkTarget + stat, err = os.Lstat(path) + } + + if err != nil { + // It's okay if the destination path doesn't exist. We can still + // continue the copy operation if the parent directory exists. + if !os.IsNotExist(err) { + return CopyInfo{}, err + } + + // Ensure destination parent dir exists. + dstParent, _ := SplitPathDirEntry(path) + + parentDirStat, err := os.Lstat(dstParent) + if err != nil { + return CopyInfo{}, err + } + if !parentDirStat.IsDir() { + return CopyInfo{}, ErrNotDirectory + } + + return CopyInfo{Path: path}, nil + } + + // The path exists after resolving symlinks. + return CopyInfo{ + Path: path, + Exists: true, + IsDir: stat.IsDir(), + }, nil +} + +// PrepareArchiveCopy prepares the given srcContent archive, which should +// contain the archived resource described by srcInfo, to the destination +// described by dstInfo. Returns the possibly modified content archive along +// with the path to the destination directory which it should be extracted to. +func PrepareArchiveCopy(srcContent Reader, srcInfo, dstInfo CopyInfo) (dstDir string, content Archive, err error) { + // Ensure in platform semantics + srcInfo.Path = normalizePath(srcInfo.Path) + dstInfo.Path = normalizePath(dstInfo.Path) + + // Separate the destination path between its directory and base + // components in case the source archive contents need to be rebased. + dstDir, dstBase := SplitPathDirEntry(dstInfo.Path) + _, srcBase := SplitPathDirEntry(srcInfo.Path) + + switch { + case dstInfo.Exists && dstInfo.IsDir: + // The destination exists as a directory. No alteration + // to srcContent is needed as its contents can be + // simply extracted to the destination directory. + return dstInfo.Path, ioutil.NopCloser(srcContent), nil + case dstInfo.Exists && srcInfo.IsDir: + // The destination exists as some type of file and the source + // content is a directory. This is an error condition since + // you cannot copy a directory to an existing file location. + return "", nil, ErrCannotCopyDir + case dstInfo.Exists: + // The destination exists as some type of file and the source content + // is also a file. The source content entry will have to be renamed to + // have a basename which matches the destination path's basename. + if len(srcInfo.RebaseName) != 0 { + srcBase = srcInfo.RebaseName + } + return dstDir, RebaseArchiveEntries(srcContent, srcBase, dstBase), nil + case srcInfo.IsDir: + // The destination does not exist and the source content is an archive + // of a directory. The archive should be extracted to the parent of + // the destination path instead, and when it is, the directory that is + // created as a result should take the name of the destination path. + // The source content entries will have to be renamed to have a + // basename which matches the destination path's basename. + if len(srcInfo.RebaseName) != 0 { + srcBase = srcInfo.RebaseName + } + return dstDir, RebaseArchiveEntries(srcContent, srcBase, dstBase), nil + case assertsDirectory(dstInfo.Path): + // The destination does not exist and is asserted to be created as a + // directory, but the source content is not a directory. This is an + // error condition since you cannot create a directory from a file + // source. + return "", nil, ErrDirNotExists + default: + // The last remaining case is when the destination does not exist, is + // not asserted to be a directory, and the source content is not an + // archive of a directory. It this case, the destination file will need + // to be created when the archive is extracted and the source content + // entry will have to be renamed to have a basename which matches the + // destination path's basename. + if len(srcInfo.RebaseName) != 0 { + srcBase = srcInfo.RebaseName + } + return dstDir, RebaseArchiveEntries(srcContent, srcBase, dstBase), nil + } + +} + +// RebaseArchiveEntries rewrites the given srcContent archive replacing +// an occurrence of oldBase with newBase at the beginning of entry names. +func RebaseArchiveEntries(srcContent Reader, oldBase, newBase string) Archive { + if oldBase == string(os.PathSeparator) { + // If oldBase specifies the root directory, use an empty string as + // oldBase instead so that newBase doesn't replace the path separator + // that all paths will start with. + oldBase = "" + } + + rebased, w := io.Pipe() + + go func() { + srcTar := tar.NewReader(srcContent) + rebasedTar := tar.NewWriter(w) + + for { + hdr, err := srcTar.Next() + if err == io.EOF { + // Signals end of archive. + rebasedTar.Close() + w.Close() + return + } + if err != nil { + w.CloseWithError(err) + return + } + + hdr.Name = strings.Replace(hdr.Name, oldBase, newBase, 1) + + if err = rebasedTar.WriteHeader(hdr); err != nil { + w.CloseWithError(err) + return + } + + if _, err = io.Copy(rebasedTar, srcTar); err != nil { + w.CloseWithError(err) + return + } + } + }() + + return rebased +} + +// CopyResource performs an archive copy from the given source path to the +// given destination path. The source path MUST exist and the destination +// path's parent directory must exist. +func CopyResource(srcPath, dstPath string, followLink bool) error { + var ( + srcInfo CopyInfo + err error + ) + + // Ensure in platform semantics + srcPath = normalizePath(srcPath) + dstPath = normalizePath(dstPath) + + // Clean the source and destination paths. + srcPath = PreserveTrailingDotOrSeparator(filepath.Clean(srcPath), srcPath) + dstPath = PreserveTrailingDotOrSeparator(filepath.Clean(dstPath), dstPath) + + if srcInfo, err = CopyInfoSourcePath(srcPath, followLink); err != nil { + return err + } + + content, err := TarResource(srcInfo) + if err != nil { + return err + } + defer content.Close() + + return CopyTo(content, srcInfo, dstPath) +} + +// CopyTo handles extracting the given content whose +// entries should be sourced from srcInfo to dstPath. +func CopyTo(content Reader, srcInfo CopyInfo, dstPath string) error { + // The destination path need not exist, but CopyInfoDestinationPath will + // ensure that at least the parent directory exists. + dstInfo, err := CopyInfoDestinationPath(normalizePath(dstPath)) + if err != nil { + return err + } + + dstDir, copyArchive, err := PrepareArchiveCopy(content, srcInfo, dstInfo) + if err != nil { + return err + } + defer copyArchive.Close() + + options := &TarOptions{ + NoLchown: true, + NoOverwriteDirNonDir: true, + } + + return Untar(copyArchive, dstDir, options) +} + +// ResolveHostSourcePath decides real path need to be copied with parameters such as +// whether to follow symbol link or not, if followLink is true, resolvedPath will return +// link target of any symbol link file, else it will only resolve symlink of directory +// but return symbol link file itself without resolving. +func ResolveHostSourcePath(path string, followLink bool) (resolvedPath, rebaseName string, err error) { + if followLink { + resolvedPath, err = filepath.EvalSymlinks(path) + if err != nil { + return + } + + resolvedPath, rebaseName = GetRebaseName(path, resolvedPath) + } else { + dirPath, basePath := filepath.Split(path) + + // if not follow symbol link, then resolve symbol link of parent dir + var resolvedDirPath string + resolvedDirPath, err = filepath.EvalSymlinks(dirPath) + if err != nil { + return + } + // resolvedDirPath will have been cleaned (no trailing path separators) so + // we can manually join it with the base path element. + resolvedPath = resolvedDirPath + string(filepath.Separator) + basePath + if hasTrailingPathSeparator(path) && filepath.Base(path) != filepath.Base(resolvedPath) { + rebaseName = filepath.Base(path) + } + } + return resolvedPath, rebaseName, nil +} + +// GetRebaseName normalizes and compares path and resolvedPath, +// return completed resolved path and rebased file name +func GetRebaseName(path, resolvedPath string) (string, string) { + // linkTarget will have been cleaned (no trailing path separators and dot) so + // we can manually join it with them + var rebaseName string + if specifiesCurrentDir(path) && !specifiesCurrentDir(resolvedPath) { + resolvedPath += string(filepath.Separator) + "." + } + + if hasTrailingPathSeparator(path) && !hasTrailingPathSeparator(resolvedPath) { + resolvedPath += string(filepath.Separator) + } + + if filepath.Base(path) != filepath.Base(resolvedPath) { + // In the case where the path had a trailing separator and a symlink + // evaluation has changed the last path component, we will need to + // rebase the name in the archive that is being copied to match the + // originally requested name. + rebaseName = filepath.Base(path) + } + return resolvedPath, rebaseName +} diff --git a/pkg/archive/copy_unix.go b/pkg/archive/copy_unix.go new file mode 100644 index 00000000..e305b5e4 --- /dev/null +++ b/pkg/archive/copy_unix.go @@ -0,0 +1,11 @@ +// +build !windows + +package archive + +import ( + "path/filepath" +) + +func normalizePath(path string) string { + return filepath.ToSlash(path) +} diff --git a/pkg/archive/copy_unix_test.go b/pkg/archive/copy_unix_test.go new file mode 100644 index 00000000..ecbfc172 --- /dev/null +++ b/pkg/archive/copy_unix_test.go @@ -0,0 +1,978 @@ +// +build !windows + +// TODO Windows: Some of these tests may be salvagable and portable to Windows. + +package archive + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" +) + +func removeAllPaths(paths ...string) { + for _, path := range paths { + os.RemoveAll(path) + } +} + +func getTestTempDirs(t *testing.T) (tmpDirA, tmpDirB string) { + var err error + + if tmpDirA, err = ioutil.TempDir("", "archive-copy-test"); err != nil { + t.Fatal(err) + } + + if tmpDirB, err = ioutil.TempDir("", "archive-copy-test"); err != nil { + t.Fatal(err) + } + + return +} + +func isNotDir(err error) bool { + return strings.Contains(err.Error(), "not a directory") +} + +func joinTrailingSep(pathElements ...string) string { + joined := filepath.Join(pathElements...) + + return fmt.Sprintf("%s%c", joined, filepath.Separator) +} + +func fileContentsEqual(t *testing.T, filenameA, filenameB string) (err error) { + t.Logf("checking for equal file contents: %q and %q\n", filenameA, filenameB) + + fileA, err := os.Open(filenameA) + if err != nil { + return + } + defer fileA.Close() + + fileB, err := os.Open(filenameB) + if err != nil { + return + } + defer fileB.Close() + + hasher := sha256.New() + + if _, err = io.Copy(hasher, fileA); err != nil { + return + } + + hashA := hasher.Sum(nil) + hasher.Reset() + + if _, err = io.Copy(hasher, fileB); err != nil { + return + } + + hashB := hasher.Sum(nil) + + if !bytes.Equal(hashA, hashB) { + err = fmt.Errorf("file content hashes not equal - expected %s, got %s", hex.EncodeToString(hashA), hex.EncodeToString(hashB)) + } + + return +} + +func dirContentsEqual(t *testing.T, newDir, oldDir string) (err error) { + t.Logf("checking for equal directory contents: %q and %q\n", newDir, oldDir) + + var changes []Change + + if changes, err = ChangesDirs(newDir, oldDir); err != nil { + return + } + + if len(changes) != 0 { + err = fmt.Errorf("expected no changes between directories, but got: %v", changes) + } + + return +} + +func logDirContents(t *testing.T, dirPath string) { + logWalkedPaths := filepath.WalkFunc(func(path string, info os.FileInfo, err error) error { + if err != nil { + t.Errorf("stat error for path %q: %s", path, err) + return nil + } + + if info.IsDir() { + path = joinTrailingSep(path) + } + + t.Logf("\t%s", path) + + return nil + }) + + t.Logf("logging directory contents: %q", dirPath) + + if err := filepath.Walk(dirPath, logWalkedPaths); err != nil { + t.Fatal(err) + } +} + +func testCopyHelper(t *testing.T, srcPath, dstPath string) (err error) { + t.Logf("copying from %q to %q (not follow symbol link)", srcPath, dstPath) + + return CopyResource(srcPath, dstPath, false) +} + +func testCopyHelperFSym(t *testing.T, srcPath, dstPath string) (err error) { + t.Logf("copying from %q to %q (follow symbol link)", srcPath, dstPath) + + return CopyResource(srcPath, dstPath, true) +} + +// Basic assumptions about SRC and DST: +// 1. SRC must exist. +// 2. If SRC ends with a trailing separator, it must be a directory. +// 3. DST parent directory must exist. +// 4. If DST exists as a file, it must not end with a trailing separator. + +// First get these easy error cases out of the way. + +// Test for error when SRC does not exist. +func TestCopyErrSrcNotExists(t *testing.T) { + tmpDirA, tmpDirB := getTestTempDirs(t) + defer removeAllPaths(tmpDirA, tmpDirB) + + if _, err := CopyInfoSourcePath(filepath.Join(tmpDirA, "file1"), false); !os.IsNotExist(err) { + t.Fatalf("expected IsNotExist error, but got %T: %s", err, err) + } +} + +// Test for error when SRC ends in a trailing +// path separator but it exists as a file. +func TestCopyErrSrcNotDir(t *testing.T) { + tmpDirA, tmpDirB := getTestTempDirs(t) + defer removeAllPaths(tmpDirA, tmpDirB) + + // Load A with some sample files and directories. + createSampleDir(t, tmpDirA) + + if _, err := CopyInfoSourcePath(joinTrailingSep(tmpDirA, "file1"), false); !isNotDir(err) { + t.Fatalf("expected IsNotDir error, but got %T: %s", err, err) + } +} + +// Test for error when SRC is a valid file or directory, +// but the DST parent directory does not exist. +func TestCopyErrDstParentNotExists(t *testing.T) { + tmpDirA, tmpDirB := getTestTempDirs(t) + defer removeAllPaths(tmpDirA, tmpDirB) + + // Load A with some sample files and directories. + createSampleDir(t, tmpDirA) + + srcInfo := CopyInfo{Path: filepath.Join(tmpDirA, "file1"), Exists: true, IsDir: false} + + // Try with a file source. + content, err := TarResource(srcInfo) + if err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + defer content.Close() + + // Copy to a file whose parent does not exist. + if err = CopyTo(content, srcInfo, filepath.Join(tmpDirB, "fakeParentDir", "file1")); err == nil { + t.Fatal("expected IsNotExist error, but got nil instead") + } + + if !os.IsNotExist(err) { + t.Fatalf("expected IsNotExist error, but got %T: %s", err, err) + } + + // Try with a directory source. + srcInfo = CopyInfo{Path: filepath.Join(tmpDirA, "dir1"), Exists: true, IsDir: true} + + content, err = TarResource(srcInfo) + if err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + defer content.Close() + + // Copy to a directory whose parent does not exist. + if err = CopyTo(content, srcInfo, joinTrailingSep(tmpDirB, "fakeParentDir", "fakeDstDir")); err == nil { + t.Fatal("expected IsNotExist error, but got nil instead") + } + + if !os.IsNotExist(err) { + t.Fatalf("expected IsNotExist error, but got %T: %s", err, err) + } +} + +// Test for error when DST ends in a trailing +// path separator but exists as a file. +func TestCopyErrDstNotDir(t *testing.T) { + tmpDirA, tmpDirB := getTestTempDirs(t) + defer removeAllPaths(tmpDirA, tmpDirB) + + // Load A and B with some sample files and directories. + createSampleDir(t, tmpDirA) + createSampleDir(t, tmpDirB) + + // Try with a file source. + srcInfo := CopyInfo{Path: filepath.Join(tmpDirA, "file1"), Exists: true, IsDir: false} + + content, err := TarResource(srcInfo) + if err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + defer content.Close() + + if err = CopyTo(content, srcInfo, joinTrailingSep(tmpDirB, "file1")); err == nil { + t.Fatal("expected IsNotDir error, but got nil instead") + } + + if !isNotDir(err) { + t.Fatalf("expected IsNotDir error, but got %T: %s", err, err) + } + + // Try with a directory source. + srcInfo = CopyInfo{Path: filepath.Join(tmpDirA, "dir1"), Exists: true, IsDir: true} + + content, err = TarResource(srcInfo) + if err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + defer content.Close() + + if err = CopyTo(content, srcInfo, joinTrailingSep(tmpDirB, "file1")); err == nil { + t.Fatal("expected IsNotDir error, but got nil instead") + } + + if !isNotDir(err) { + t.Fatalf("expected IsNotDir error, but got %T: %s", err, err) + } +} + +// Possibilities are reduced to the remaining 10 cases: +// +// case | srcIsDir | onlyDirContents | dstExists | dstIsDir | dstTrSep | action +// =================================================================================================== +// A | no | - | no | - | no | create file +// B | no | - | no | - | yes | error +// C | no | - | yes | no | - | overwrite file +// D | no | - | yes | yes | - | create file in dst dir +// E | yes | no | no | - | - | create dir, copy contents +// F | yes | no | yes | no | - | error +// G | yes | no | yes | yes | - | copy dir and contents +// H | yes | yes | no | - | - | create dir, copy contents +// I | yes | yes | yes | no | - | error +// J | yes | yes | yes | yes | - | copy dir contents +// + +// A. SRC specifies a file and DST (no trailing path separator) doesn't +// exist. This should create a file with the name DST and copy the +// contents of the source file into it. +func TestCopyCaseA(t *testing.T) { + tmpDirA, tmpDirB := getTestTempDirs(t) + defer removeAllPaths(tmpDirA, tmpDirB) + + // Load A with some sample files and directories. + createSampleDir(t, tmpDirA) + + srcPath := filepath.Join(tmpDirA, "file1") + dstPath := filepath.Join(tmpDirB, "itWorks.txt") + + var err error + + if err = testCopyHelper(t, srcPath, dstPath); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = fileContentsEqual(t, srcPath, dstPath); err != nil { + t.Fatal(err) + } + os.Remove(dstPath) + + symlinkPath := filepath.Join(tmpDirA, "symlink3") + symlinkPath1 := filepath.Join(tmpDirA, "symlink4") + linkTarget := filepath.Join(tmpDirA, "file1") + + if err = testCopyHelperFSym(t, symlinkPath, dstPath); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = fileContentsEqual(t, linkTarget, dstPath); err != nil { + t.Fatal(err) + } + os.Remove(dstPath) + if err = testCopyHelperFSym(t, symlinkPath1, dstPath); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = fileContentsEqual(t, linkTarget, dstPath); err != nil { + t.Fatal(err) + } +} + +// B. SRC specifies a file and DST (with trailing path separator) doesn't +// exist. This should cause an error because the copy operation cannot +// create a directory when copying a single file. +func TestCopyCaseB(t *testing.T) { + tmpDirA, tmpDirB := getTestTempDirs(t) + defer removeAllPaths(tmpDirA, tmpDirB) + + // Load A with some sample files and directories. + createSampleDir(t, tmpDirA) + + srcPath := filepath.Join(tmpDirA, "file1") + dstDir := joinTrailingSep(tmpDirB, "testDir") + + var err error + + if err = testCopyHelper(t, srcPath, dstDir); err == nil { + t.Fatal("expected ErrDirNotExists error, but got nil instead") + } + + if err != ErrDirNotExists { + t.Fatalf("expected ErrDirNotExists error, but got %T: %s", err, err) + } + + symlinkPath := filepath.Join(tmpDirA, "symlink3") + + if err = testCopyHelperFSym(t, symlinkPath, dstDir); err == nil { + t.Fatal("expected ErrDirNotExists error, but got nil instead") + } + if err != ErrDirNotExists { + t.Fatalf("expected ErrDirNotExists error, but got %T: %s", err, err) + } + +} + +// C. SRC specifies a file and DST exists as a file. This should overwrite +// the file at DST with the contents of the source file. +func TestCopyCaseC(t *testing.T) { + tmpDirA, tmpDirB := getTestTempDirs(t) + defer removeAllPaths(tmpDirA, tmpDirB) + + // Load A and B with some sample files and directories. + createSampleDir(t, tmpDirA) + createSampleDir(t, tmpDirB) + + srcPath := filepath.Join(tmpDirA, "file1") + dstPath := filepath.Join(tmpDirB, "file2") + + var err error + + // Ensure they start out different. + if err = fileContentsEqual(t, srcPath, dstPath); err == nil { + t.Fatal("expected different file contents") + } + + if err = testCopyHelper(t, srcPath, dstPath); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = fileContentsEqual(t, srcPath, dstPath); err != nil { + t.Fatal(err) + } +} + +// C. Symbol link following version: +// SRC specifies a file and DST exists as a file. This should overwrite +// the file at DST with the contents of the source file. +func TestCopyCaseCFSym(t *testing.T) { + tmpDirA, tmpDirB := getTestTempDirs(t) + defer removeAllPaths(tmpDirA, tmpDirB) + + // Load A and B with some sample files and directories. + createSampleDir(t, tmpDirA) + createSampleDir(t, tmpDirB) + + symlinkPathBad := filepath.Join(tmpDirA, "symlink1") + symlinkPath := filepath.Join(tmpDirA, "symlink3") + linkTarget := filepath.Join(tmpDirA, "file1") + dstPath := filepath.Join(tmpDirB, "file2") + + var err error + + // first to test broken link + if err = testCopyHelperFSym(t, symlinkPathBad, dstPath); err == nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + // test symbol link -> symbol link -> target + // Ensure they start out different. + if err = fileContentsEqual(t, linkTarget, dstPath); err == nil { + t.Fatal("expected different file contents") + } + + if err = testCopyHelperFSym(t, symlinkPath, dstPath); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = fileContentsEqual(t, linkTarget, dstPath); err != nil { + t.Fatal(err) + } +} + +// D. SRC specifies a file and DST exists as a directory. This should place +// a copy of the source file inside it using the basename from SRC. Ensure +// this works whether DST has a trailing path separator or not. +func TestCopyCaseD(t *testing.T) { + tmpDirA, tmpDirB := getTestTempDirs(t) + defer removeAllPaths(tmpDirA, tmpDirB) + + // Load A and B with some sample files and directories. + createSampleDir(t, tmpDirA) + createSampleDir(t, tmpDirB) + + srcPath := filepath.Join(tmpDirA, "file1") + dstDir := filepath.Join(tmpDirB, "dir1") + dstPath := filepath.Join(dstDir, "file1") + + var err error + + // Ensure that dstPath doesn't exist. + if _, err = os.Stat(dstPath); !os.IsNotExist(err) { + t.Fatalf("did not expect dstPath %q to exist", dstPath) + } + + if err = testCopyHelper(t, srcPath, dstDir); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = fileContentsEqual(t, srcPath, dstPath); err != nil { + t.Fatal(err) + } + + // Now try again but using a trailing path separator for dstDir. + + if err = os.RemoveAll(dstDir); err != nil { + t.Fatalf("unable to remove dstDir: %s", err) + } + + if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil { + t.Fatalf("unable to make dstDir: %s", err) + } + + dstDir = joinTrailingSep(tmpDirB, "dir1") + + if err = testCopyHelper(t, srcPath, dstDir); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = fileContentsEqual(t, srcPath, dstPath); err != nil { + t.Fatal(err) + } +} + +// D. Symbol link following version: +// SRC specifies a file and DST exists as a directory. This should place +// a copy of the source file inside it using the basename from SRC. Ensure +// this works whether DST has a trailing path separator or not. +func TestCopyCaseDFSym(t *testing.T) { + tmpDirA, tmpDirB := getTestTempDirs(t) + defer removeAllPaths(tmpDirA, tmpDirB) + + // Load A and B with some sample files and directories. + createSampleDir(t, tmpDirA) + createSampleDir(t, tmpDirB) + + srcPath := filepath.Join(tmpDirA, "symlink4") + linkTarget := filepath.Join(tmpDirA, "file1") + dstDir := filepath.Join(tmpDirB, "dir1") + dstPath := filepath.Join(dstDir, "symlink4") + + var err error + + // Ensure that dstPath doesn't exist. + if _, err = os.Stat(dstPath); !os.IsNotExist(err) { + t.Fatalf("did not expect dstPath %q to exist", dstPath) + } + + if err = testCopyHelperFSym(t, srcPath, dstDir); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = fileContentsEqual(t, linkTarget, dstPath); err != nil { + t.Fatal(err) + } + + // Now try again but using a trailing path separator for dstDir. + + if err = os.RemoveAll(dstDir); err != nil { + t.Fatalf("unable to remove dstDir: %s", err) + } + + if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil { + t.Fatalf("unable to make dstDir: %s", err) + } + + dstDir = joinTrailingSep(tmpDirB, "dir1") + + if err = testCopyHelperFSym(t, srcPath, dstDir); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = fileContentsEqual(t, linkTarget, dstPath); err != nil { + t.Fatal(err) + } +} + +// E. SRC specifies a directory and DST does not exist. This should create a +// directory at DST and copy the contents of the SRC directory into the DST +// directory. Ensure this works whether DST has a trailing path separator or +// not. +func TestCopyCaseE(t *testing.T) { + tmpDirA, tmpDirB := getTestTempDirs(t) + defer removeAllPaths(tmpDirA, tmpDirB) + + // Load A with some sample files and directories. + createSampleDir(t, tmpDirA) + + srcDir := filepath.Join(tmpDirA, "dir1") + dstDir := filepath.Join(tmpDirB, "testDir") + + var err error + + if err = testCopyHelper(t, srcDir, dstDir); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = dirContentsEqual(t, dstDir, srcDir); err != nil { + t.Log("dir contents not equal") + logDirContents(t, tmpDirA) + logDirContents(t, tmpDirB) + t.Fatal(err) + } + + // Now try again but using a trailing path separator for dstDir. + + if err = os.RemoveAll(dstDir); err != nil { + t.Fatalf("unable to remove dstDir: %s", err) + } + + dstDir = joinTrailingSep(tmpDirB, "testDir") + + if err = testCopyHelper(t, srcDir, dstDir); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = dirContentsEqual(t, dstDir, srcDir); err != nil { + t.Fatal(err) + } +} + +// E. Symbol link following version: +// SRC specifies a directory and DST does not exist. This should create a +// directory at DST and copy the contents of the SRC directory into the DST +// directory. Ensure this works whether DST has a trailing path separator or +// not. +func TestCopyCaseEFSym(t *testing.T) { + tmpDirA, tmpDirB := getTestTempDirs(t) + defer removeAllPaths(tmpDirA, tmpDirB) + + // Load A with some sample files and directories. + createSampleDir(t, tmpDirA) + + srcDir := filepath.Join(tmpDirA, "dirSymlink") + linkTarget := filepath.Join(tmpDirA, "dir1") + dstDir := filepath.Join(tmpDirB, "testDir") + + var err error + + if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = dirContentsEqual(t, dstDir, linkTarget); err != nil { + t.Log("dir contents not equal") + logDirContents(t, tmpDirA) + logDirContents(t, tmpDirB) + t.Fatal(err) + } + + // Now try again but using a trailing path separator for dstDir. + + if err = os.RemoveAll(dstDir); err != nil { + t.Fatalf("unable to remove dstDir: %s", err) + } + + dstDir = joinTrailingSep(tmpDirB, "testDir") + + if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = dirContentsEqual(t, dstDir, linkTarget); err != nil { + t.Fatal(err) + } +} + +// F. SRC specifies a directory and DST exists as a file. This should cause an +// error as it is not possible to overwrite a file with a directory. +func TestCopyCaseF(t *testing.T) { + tmpDirA, tmpDirB := getTestTempDirs(t) + defer removeAllPaths(tmpDirA, tmpDirB) + + // Load A and B with some sample files and directories. + createSampleDir(t, tmpDirA) + createSampleDir(t, tmpDirB) + + srcDir := filepath.Join(tmpDirA, "dir1") + symSrcDir := filepath.Join(tmpDirA, "dirSymlink") + dstFile := filepath.Join(tmpDirB, "file1") + + var err error + + if err = testCopyHelper(t, srcDir, dstFile); err == nil { + t.Fatal("expected ErrCannotCopyDir error, but got nil instead") + } + + if err != ErrCannotCopyDir { + t.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err) + } + + // now test with symbol link + if err = testCopyHelperFSym(t, symSrcDir, dstFile); err == nil { + t.Fatal("expected ErrCannotCopyDir error, but got nil instead") + } + + if err != ErrCannotCopyDir { + t.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err) + } +} + +// G. SRC specifies a directory and DST exists as a directory. This should copy +// the SRC directory and all its contents to the DST directory. Ensure this +// works whether DST has a trailing path separator or not. +func TestCopyCaseG(t *testing.T) { + tmpDirA, tmpDirB := getTestTempDirs(t) + defer removeAllPaths(tmpDirA, tmpDirB) + + // Load A and B with some sample files and directories. + createSampleDir(t, tmpDirA) + createSampleDir(t, tmpDirB) + + srcDir := filepath.Join(tmpDirA, "dir1") + dstDir := filepath.Join(tmpDirB, "dir2") + resultDir := filepath.Join(dstDir, "dir1") + + var err error + + if err = testCopyHelper(t, srcDir, dstDir); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = dirContentsEqual(t, resultDir, srcDir); err != nil { + t.Fatal(err) + } + + // Now try again but using a trailing path separator for dstDir. + + if err = os.RemoveAll(dstDir); err != nil { + t.Fatalf("unable to remove dstDir: %s", err) + } + + if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil { + t.Fatalf("unable to make dstDir: %s", err) + } + + dstDir = joinTrailingSep(tmpDirB, "dir2") + + if err = testCopyHelper(t, srcDir, dstDir); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = dirContentsEqual(t, resultDir, srcDir); err != nil { + t.Fatal(err) + } +} + +// G. Symbol link version: +// SRC specifies a directory and DST exists as a directory. This should copy +// the SRC directory and all its contents to the DST directory. Ensure this +// works whether DST has a trailing path separator or not. +func TestCopyCaseGFSym(t *testing.T) { + tmpDirA, tmpDirB := getTestTempDirs(t) + defer removeAllPaths(tmpDirA, tmpDirB) + + // Load A and B with some sample files and directories. + createSampleDir(t, tmpDirA) + createSampleDir(t, tmpDirB) + + srcDir := filepath.Join(tmpDirA, "dirSymlink") + linkTarget := filepath.Join(tmpDirA, "dir1") + dstDir := filepath.Join(tmpDirB, "dir2") + resultDir := filepath.Join(dstDir, "dirSymlink") + + var err error + + if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = dirContentsEqual(t, resultDir, linkTarget); err != nil { + t.Fatal(err) + } + + // Now try again but using a trailing path separator for dstDir. + + if err = os.RemoveAll(dstDir); err != nil { + t.Fatalf("unable to remove dstDir: %s", err) + } + + if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil { + t.Fatalf("unable to make dstDir: %s", err) + } + + dstDir = joinTrailingSep(tmpDirB, "dir2") + + if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = dirContentsEqual(t, resultDir, linkTarget); err != nil { + t.Fatal(err) + } +} + +// H. SRC specifies a directory's contents only and DST does not exist. This +// should create a directory at DST and copy the contents of the SRC +// directory (but not the directory itself) into the DST directory. Ensure +// this works whether DST has a trailing path separator or not. +func TestCopyCaseH(t *testing.T) { + tmpDirA, tmpDirB := getTestTempDirs(t) + defer removeAllPaths(tmpDirA, tmpDirB) + + // Load A with some sample files and directories. + createSampleDir(t, tmpDirA) + + srcDir := joinTrailingSep(tmpDirA, "dir1") + "." + dstDir := filepath.Join(tmpDirB, "testDir") + + var err error + + if err = testCopyHelper(t, srcDir, dstDir); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = dirContentsEqual(t, dstDir, srcDir); err != nil { + t.Log("dir contents not equal") + logDirContents(t, tmpDirA) + logDirContents(t, tmpDirB) + t.Fatal(err) + } + + // Now try again but using a trailing path separator for dstDir. + + if err = os.RemoveAll(dstDir); err != nil { + t.Fatalf("unable to remove dstDir: %s", err) + } + + dstDir = joinTrailingSep(tmpDirB, "testDir") + + if err = testCopyHelper(t, srcDir, dstDir); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = dirContentsEqual(t, dstDir, srcDir); err != nil { + t.Log("dir contents not equal") + logDirContents(t, tmpDirA) + logDirContents(t, tmpDirB) + t.Fatal(err) + } +} + +// H. Symbol link following version: +// SRC specifies a directory's contents only and DST does not exist. This +// should create a directory at DST and copy the contents of the SRC +// directory (but not the directory itself) into the DST directory. Ensure +// this works whether DST has a trailing path separator or not. +func TestCopyCaseHFSym(t *testing.T) { + tmpDirA, tmpDirB := getTestTempDirs(t) + defer removeAllPaths(tmpDirA, tmpDirB) + + // Load A with some sample files and directories. + createSampleDir(t, tmpDirA) + + srcDir := joinTrailingSep(tmpDirA, "dirSymlink") + "." + linkTarget := filepath.Join(tmpDirA, "dir1") + dstDir := filepath.Join(tmpDirB, "testDir") + + var err error + + if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = dirContentsEqual(t, dstDir, linkTarget); err != nil { + t.Log("dir contents not equal") + logDirContents(t, tmpDirA) + logDirContents(t, tmpDirB) + t.Fatal(err) + } + + // Now try again but using a trailing path separator for dstDir. + + if err = os.RemoveAll(dstDir); err != nil { + t.Fatalf("unable to remove dstDir: %s", err) + } + + dstDir = joinTrailingSep(tmpDirB, "testDir") + + if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = dirContentsEqual(t, dstDir, linkTarget); err != nil { + t.Log("dir contents not equal") + logDirContents(t, tmpDirA) + logDirContents(t, tmpDirB) + t.Fatal(err) + } +} + +// I. SRC specifies a directory's contents only and DST exists as a file. This +// should cause an error as it is not possible to overwrite a file with a +// directory. +func TestCopyCaseI(t *testing.T) { + tmpDirA, tmpDirB := getTestTempDirs(t) + defer removeAllPaths(tmpDirA, tmpDirB) + + // Load A and B with some sample files and directories. + createSampleDir(t, tmpDirA) + createSampleDir(t, tmpDirB) + + srcDir := joinTrailingSep(tmpDirA, "dir1") + "." + symSrcDir := filepath.Join(tmpDirB, "dirSymlink") + dstFile := filepath.Join(tmpDirB, "file1") + + var err error + + if err = testCopyHelper(t, srcDir, dstFile); err == nil { + t.Fatal("expected ErrCannotCopyDir error, but got nil instead") + } + + if err != ErrCannotCopyDir { + t.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err) + } + + // now try with symbol link of dir + if err = testCopyHelperFSym(t, symSrcDir, dstFile); err == nil { + t.Fatal("expected ErrCannotCopyDir error, but got nil instead") + } + + if err != ErrCannotCopyDir { + t.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err) + } +} + +// J. SRC specifies a directory's contents only and DST exists as a directory. +// This should copy the contents of the SRC directory (but not the directory +// itself) into the DST directory. Ensure this works whether DST has a +// trailing path separator or not. +func TestCopyCaseJ(t *testing.T) { + tmpDirA, tmpDirB := getTestTempDirs(t) + defer removeAllPaths(tmpDirA, tmpDirB) + + // Load A and B with some sample files and directories. + createSampleDir(t, tmpDirA) + createSampleDir(t, tmpDirB) + + srcDir := joinTrailingSep(tmpDirA, "dir1") + "." + dstDir := filepath.Join(tmpDirB, "dir5") + + var err error + + // first to create an empty dir + if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil { + t.Fatalf("unable to make dstDir: %s", err) + } + + if err = testCopyHelper(t, srcDir, dstDir); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = dirContentsEqual(t, dstDir, srcDir); err != nil { + t.Fatal(err) + } + + // Now try again but using a trailing path separator for dstDir. + + if err = os.RemoveAll(dstDir); err != nil { + t.Fatalf("unable to remove dstDir: %s", err) + } + + if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil { + t.Fatalf("unable to make dstDir: %s", err) + } + + dstDir = joinTrailingSep(tmpDirB, "dir5") + + if err = testCopyHelper(t, srcDir, dstDir); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = dirContentsEqual(t, dstDir, srcDir); err != nil { + t.Fatal(err) + } +} + +// J. Symbol link following version: +// SRC specifies a directory's contents only and DST exists as a directory. +// This should copy the contents of the SRC directory (but not the directory +// itself) into the DST directory. Ensure this works whether DST has a +// trailing path separator or not. +func TestCopyCaseJFSym(t *testing.T) { + tmpDirA, tmpDirB := getTestTempDirs(t) + defer removeAllPaths(tmpDirA, tmpDirB) + + // Load A and B with some sample files and directories. + createSampleDir(t, tmpDirA) + createSampleDir(t, tmpDirB) + + srcDir := joinTrailingSep(tmpDirA, "dirSymlink") + "." + linkTarget := filepath.Join(tmpDirA, "dir1") + dstDir := filepath.Join(tmpDirB, "dir5") + + var err error + + // first to create an empty dir + if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil { + t.Fatalf("unable to make dstDir: %s", err) + } + + if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = dirContentsEqual(t, dstDir, linkTarget); err != nil { + t.Fatal(err) + } + + // Now try again but using a trailing path separator for dstDir. + + if err = os.RemoveAll(dstDir); err != nil { + t.Fatalf("unable to remove dstDir: %s", err) + } + + if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil { + t.Fatalf("unable to make dstDir: %s", err) + } + + dstDir = joinTrailingSep(tmpDirB, "dir5") + + if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil { + t.Fatalf("unexpected error %T: %s", err, err) + } + + if err = dirContentsEqual(t, dstDir, linkTarget); err != nil { + t.Fatal(err) + } +} diff --git a/pkg/archive/copy_windows.go b/pkg/archive/copy_windows.go new file mode 100644 index 00000000..2b775b45 --- /dev/null +++ b/pkg/archive/copy_windows.go @@ -0,0 +1,9 @@ +package archive + +import ( + "path/filepath" +) + +func normalizePath(path string) string { + return filepath.FromSlash(path) +} diff --git a/pkg/archive/diff.go b/pkg/archive/diff.go new file mode 100644 index 00000000..1b08ad33 --- /dev/null +++ b/pkg/archive/diff.go @@ -0,0 +1,279 @@ +package archive + +import ( + "archive/tar" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/pools" + "github.com/docker/docker/pkg/system" +) + +// UnpackLayer unpack `layer` to a `dest`. The stream `layer` can be +// compressed or uncompressed. +// Returns the size in bytes of the contents of the layer. +func UnpackLayer(dest string, layer Reader, options *TarOptions) (size int64, err error) { + tr := tar.NewReader(layer) + trBuf := pools.BufioReader32KPool.Get(tr) + defer pools.BufioReader32KPool.Put(trBuf) + + var dirs []*tar.Header + unpackedPaths := make(map[string]struct{}) + + if options == nil { + options = &TarOptions{} + } + if options.ExcludePatterns == nil { + options.ExcludePatterns = []string{} + } + remappedRootUID, remappedRootGID, err := idtools.GetRootUIDGID(options.UIDMaps, options.GIDMaps) + if err != nil { + return 0, err + } + + aufsTempdir := "" + aufsHardlinks := make(map[string]*tar.Header) + + if options == nil { + options = &TarOptions{} + } + // Iterate through the files in the archive. + for { + hdr, err := tr.Next() + if err == io.EOF { + // end of tar archive + break + } + if err != nil { + return 0, err + } + + size += hdr.Size + + // Normalize name, for safety and for a simple is-root check + hdr.Name = filepath.Clean(hdr.Name) + + // Windows does not support filenames with colons in them. Ignore + // these files. This is not a problem though (although it might + // appear that it is). Let's suppose a client is running docker pull. + // The daemon it points to is Windows. Would it make sense for the + // client to be doing a docker pull Ubuntu for example (which has files + // with colons in the name under /usr/share/man/man3)? No, absolutely + // not as it would really only make sense that they were pulling a + // Windows image. However, for development, it is necessary to be able + // to pull Linux images which are in the repository. + // + // TODO Windows. Once the registry is aware of what images are Windows- + // specific or Linux-specific, this warning should be changed to an error + // to cater for the situation where someone does manage to upload a Linux + // image but have it tagged as Windows inadvertently. + if runtime.GOOS == "windows" { + if strings.Contains(hdr.Name, ":") { + logrus.Warnf("Windows: Ignoring %s (is this a Linux image?)", hdr.Name) + continue + } + } + + // Note as these operations are platform specific, so must the slash be. + if !strings.HasSuffix(hdr.Name, string(os.PathSeparator)) { + // Not the root directory, ensure that the parent directory exists. + // This happened in some tests where an image had a tarfile without any + // parent directories. + parent := filepath.Dir(hdr.Name) + parentPath := filepath.Join(dest, parent) + + if _, err := os.Lstat(parentPath); err != nil && os.IsNotExist(err) { + err = system.MkdirAll(parentPath, 0600) + if err != nil { + return 0, err + } + } + } + + // Skip AUFS metadata dirs + if strings.HasPrefix(hdr.Name, WhiteoutMetaPrefix) { + // Regular files inside /.wh..wh.plnk can be used as hardlink targets + // We don't want this directory, but we need the files in them so that + // such hardlinks can be resolved. + if strings.HasPrefix(hdr.Name, WhiteoutLinkDir) && hdr.Typeflag == tar.TypeReg { + basename := filepath.Base(hdr.Name) + aufsHardlinks[basename] = hdr + if aufsTempdir == "" { + if aufsTempdir, err = ioutil.TempDir("", "dockerplnk"); err != nil { + return 0, err + } + defer os.RemoveAll(aufsTempdir) + } + if err := createTarFile(filepath.Join(aufsTempdir, basename), dest, hdr, tr, true, nil); err != nil { + return 0, err + } + } + + if hdr.Name != WhiteoutOpaqueDir { + continue + } + } + path := filepath.Join(dest, hdr.Name) + rel, err := filepath.Rel(dest, path) + if err != nil { + return 0, err + } + + // Note as these operations are platform specific, so must the slash be. + if strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { + return 0, breakoutError(fmt.Errorf("%q is outside of %q", hdr.Name, dest)) + } + base := filepath.Base(path) + + if strings.HasPrefix(base, WhiteoutPrefix) { + dir := filepath.Dir(path) + if base == WhiteoutOpaqueDir { + _, err := os.Lstat(dir) + if err != nil { + return 0, err + } + err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + if os.IsNotExist(err) { + err = nil // parent was deleted + } + return err + } + if path == dir { + return nil + } + if _, exists := unpackedPaths[path]; !exists { + err := os.RemoveAll(path) + return err + } + return nil + }) + if err != nil { + return 0, err + } + } else { + originalBase := base[len(WhiteoutPrefix):] + originalPath := filepath.Join(dir, originalBase) + if err := os.RemoveAll(originalPath); err != nil { + return 0, err + } + } + } else { + // If path exits we almost always just want to remove and replace it. + // The only exception is when it is a directory *and* the file from + // the layer is also a directory. Then we want to merge them (i.e. + // just apply the metadata from the layer). + if fi, err := os.Lstat(path); err == nil { + if !(fi.IsDir() && hdr.Typeflag == tar.TypeDir) { + if err := os.RemoveAll(path); err != nil { + return 0, err + } + } + } + + trBuf.Reset(tr) + srcData := io.Reader(trBuf) + srcHdr := hdr + + // Hard links into /.wh..wh.plnk don't work, as we don't extract that directory, so + // we manually retarget these into the temporary files we extracted them into + if hdr.Typeflag == tar.TypeLink && strings.HasPrefix(filepath.Clean(hdr.Linkname), WhiteoutLinkDir) { + linkBasename := filepath.Base(hdr.Linkname) + srcHdr = aufsHardlinks[linkBasename] + if srcHdr == nil { + return 0, fmt.Errorf("Invalid aufs hardlink") + } + tmpFile, err := os.Open(filepath.Join(aufsTempdir, linkBasename)) + if err != nil { + return 0, err + } + defer tmpFile.Close() + srcData = tmpFile + } + + // if the options contain a uid & gid maps, convert header uid/gid + // entries using the maps such that lchown sets the proper mapped + // uid/gid after writing the file. We only perform this mapping if + // the file isn't already owned by the remapped root UID or GID, as + // that specific uid/gid has no mapping from container -> host, and + // those files already have the proper ownership for inside the + // container. + if srcHdr.Uid != remappedRootUID { + xUID, err := idtools.ToHost(srcHdr.Uid, options.UIDMaps) + if err != nil { + return 0, err + } + srcHdr.Uid = xUID + } + if srcHdr.Gid != remappedRootGID { + xGID, err := idtools.ToHost(srcHdr.Gid, options.GIDMaps) + if err != nil { + return 0, err + } + srcHdr.Gid = xGID + } + if err := createTarFile(path, dest, srcHdr, srcData, true, nil); err != nil { + return 0, err + } + + // Directory mtimes must be handled at the end to avoid further + // file creation in them to modify the directory mtime + if hdr.Typeflag == tar.TypeDir { + dirs = append(dirs, hdr) + } + unpackedPaths[path] = struct{}{} + } + } + + for _, hdr := range dirs { + path := filepath.Join(dest, hdr.Name) + if err := system.Chtimes(path, hdr.AccessTime, hdr.ModTime); err != nil { + return 0, err + } + } + + return size, nil +} + +// ApplyLayer parses a diff in the standard layer format from `layer`, +// and applies it to the directory `dest`. The stream `layer` can be +// compressed or uncompressed. +// Returns the size in bytes of the contents of the layer. +func ApplyLayer(dest string, layer Reader) (int64, error) { + return applyLayerHandler(dest, layer, &TarOptions{}, true) +} + +// ApplyUncompressedLayer parses a diff in the standard layer format from +// `layer`, and applies it to the directory `dest`. The stream `layer` +// can only be uncompressed. +// Returns the size in bytes of the contents of the layer. +func ApplyUncompressedLayer(dest string, layer Reader, options *TarOptions) (int64, error) { + return applyLayerHandler(dest, layer, options, false) +} + +// do the bulk load of ApplyLayer, but allow for not calling DecompressStream +func applyLayerHandler(dest string, layer Reader, options *TarOptions, decompress bool) (int64, error) { + dest = filepath.Clean(dest) + + // We need to be able to set any perms + oldmask, err := system.Umask(0) + if err != nil { + return 0, err + } + defer system.Umask(oldmask) // ignore err, ErrNotSupportedPlatform + + if decompress { + layer, err = DecompressStream(layer) + if err != nil { + return 0, err + } + } + return UnpackLayer(dest, layer, options) +} diff --git a/pkg/archive/diff_test.go b/pkg/archive/diff_test.go new file mode 100644 index 00000000..8167941a --- /dev/null +++ b/pkg/archive/diff_test.go @@ -0,0 +1,386 @@ +package archive + +import ( + "archive/tar" + "io" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "runtime" + "testing" + + "github.com/docker/docker/pkg/ioutils" +) + +func TestApplyLayerInvalidFilenames(t *testing.T) { + // TODO Windows: Figure out how to fix this test. + if runtime.GOOS == "windows" { + t.Skip("Passes but hits breakoutError: platform and architecture is not supported") + } + for i, headers := range [][]*tar.Header{ + { + { + Name: "../victim/dotdot", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + { + { + // Note the leading slash + Name: "/../victim/slash-dotdot", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + } { + if err := testBreakout("applylayer", "docker-TestApplyLayerInvalidFilenames", headers); err != nil { + t.Fatalf("i=%d. %v", i, err) + } + } +} + +func TestApplyLayerInvalidHardlink(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("TypeLink support on Windows") + } + for i, headers := range [][]*tar.Header{ + { // try reading victim/hello (../) + { + Name: "dotdot", + Typeflag: tar.TypeLink, + Linkname: "../victim/hello", + Mode: 0644, + }, + }, + { // try reading victim/hello (/../) + { + Name: "slash-dotdot", + Typeflag: tar.TypeLink, + // Note the leading slash + Linkname: "/../victim/hello", + Mode: 0644, + }, + }, + { // try writing victim/file + { + Name: "loophole-victim", + Typeflag: tar.TypeLink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "loophole-victim/file", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + { // try reading victim/hello (hardlink, symlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeLink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "symlink", + Typeflag: tar.TypeSymlink, + Linkname: "loophole-victim/hello", + Mode: 0644, + }, + }, + { // Try reading victim/hello (hardlink, hardlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeLink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "hardlink", + Typeflag: tar.TypeLink, + Linkname: "loophole-victim/hello", + Mode: 0644, + }, + }, + { // Try removing victim directory (hardlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeLink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "loophole-victim", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + } { + if err := testBreakout("applylayer", "docker-TestApplyLayerInvalidHardlink", headers); err != nil { + t.Fatalf("i=%d. %v", i, err) + } + } +} + +func TestApplyLayerInvalidSymlink(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("TypeSymLink support on Windows") + } + for i, headers := range [][]*tar.Header{ + { // try reading victim/hello (../) + { + Name: "dotdot", + Typeflag: tar.TypeSymlink, + Linkname: "../victim/hello", + Mode: 0644, + }, + }, + { // try reading victim/hello (/../) + { + Name: "slash-dotdot", + Typeflag: tar.TypeSymlink, + // Note the leading slash + Linkname: "/../victim/hello", + Mode: 0644, + }, + }, + { // try writing victim/file + { + Name: "loophole-victim", + Typeflag: tar.TypeSymlink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "loophole-victim/file", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + { // try reading victim/hello (symlink, symlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeSymlink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "symlink", + Typeflag: tar.TypeSymlink, + Linkname: "loophole-victim/hello", + Mode: 0644, + }, + }, + { // try reading victim/hello (symlink, hardlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeSymlink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "hardlink", + Typeflag: tar.TypeLink, + Linkname: "loophole-victim/hello", + Mode: 0644, + }, + }, + { // try removing victim directory (symlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeSymlink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "loophole-victim", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + } { + if err := testBreakout("applylayer", "docker-TestApplyLayerInvalidSymlink", headers); err != nil { + t.Fatalf("i=%d. %v", i, err) + } + } +} + +func TestApplyLayerWhiteouts(t *testing.T) { + // TODO Windows: Figure out why this test fails + if runtime.GOOS == "windows" { + t.Skip("Failing on Windows") + } + + wd, err := ioutil.TempDir("", "graphdriver-test-whiteouts") + if err != nil { + return + } + defer os.RemoveAll(wd) + + base := []string{ + ".baz", + "bar/", + "bar/bax", + "bar/bay/", + "baz", + "foo/", + "foo/.abc", + "foo/.bcd/", + "foo/.bcd/a", + "foo/cde/", + "foo/cde/def", + "foo/cde/efg", + "foo/fgh", + "foobar", + } + + type tcase struct { + change, expected []string + } + + tcases := []tcase{ + { + base, + base, + }, + { + []string{ + ".bay", + ".wh.baz", + "foo/", + "foo/.bce", + "foo/.wh..wh..opq", + "foo/cde/", + "foo/cde/efg", + }, + []string{ + ".bay", + ".baz", + "bar/", + "bar/bax", + "bar/bay/", + "foo/", + "foo/.bce", + "foo/cde/", + "foo/cde/efg", + "foobar", + }, + }, + { + []string{ + ".bay", + ".wh..baz", + ".wh.foobar", + "foo/", + "foo/.abc", + "foo/.wh.cde", + "bar/", + }, + []string{ + ".bay", + "bar/", + "bar/bax", + "bar/bay/", + "foo/", + "foo/.abc", + "foo/.bce", + }, + }, + { + []string{ + ".abc", + ".wh..wh..opq", + "foobar", + }, + []string{ + ".abc", + "foobar", + }, + }, + } + + for i, tc := range tcases { + l, err := makeTestLayer(tc.change) + if err != nil { + t.Fatal(err) + } + + _, err = UnpackLayer(wd, l, nil) + if err != nil { + t.Fatal(err) + } + err = l.Close() + if err != nil { + t.Fatal(err) + } + + paths, err := readDirContents(wd) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(tc.expected, paths) { + t.Fatalf("invalid files for layer %d: expected %q, got %q", i, tc.expected, paths) + } + } + +} + +func makeTestLayer(paths []string) (rc io.ReadCloser, err error) { + tmpDir, err := ioutil.TempDir("", "graphdriver-test-mklayer") + if err != nil { + return + } + defer func() { + if err != nil { + os.RemoveAll(tmpDir) + } + }() + for _, p := range paths { + if p[len(p)-1] == filepath.Separator { + if err = os.MkdirAll(filepath.Join(tmpDir, p), 0700); err != nil { + return + } + } else { + if err = ioutil.WriteFile(filepath.Join(tmpDir, p), nil, 0600); err != nil { + return + } + } + } + archive, err := Tar(tmpDir, Uncompressed) + if err != nil { + return + } + return ioutils.NewReadCloserWrapper(archive, func() error { + err := archive.Close() + os.RemoveAll(tmpDir) + return err + }), nil +} + +func readDirContents(root string) ([]string, error) { + var files []string + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if path == root { + return nil + } + rel, err := filepath.Rel(root, path) + if err != nil { + return err + } + if info.IsDir() { + rel = rel + "/" + } + files = append(files, rel) + return nil + }) + if err != nil { + return nil, err + } + return files, nil +} diff --git a/pkg/archive/example_changes.go b/pkg/archive/example_changes.go new file mode 100644 index 00000000..cedd46a4 --- /dev/null +++ b/pkg/archive/example_changes.go @@ -0,0 +1,97 @@ +// +build ignore + +// Simple tool to create an archive stream from an old and new directory +// +// By default it will stream the comparison of two temporary directories with junk files +package main + +import ( + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "path" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/archive" +) + +var ( + flDebug = flag.Bool("D", false, "debugging output") + flNewDir = flag.String("newdir", "", "") + flOldDir = flag.String("olddir", "", "") + log = logrus.New() +) + +func main() { + flag.Usage = func() { + fmt.Println("Produce a tar from comparing two directory paths. By default a demo tar is created of around 200 files (including hardlinks)") + fmt.Printf("%s [OPTIONS]\n", os.Args[0]) + flag.PrintDefaults() + } + flag.Parse() + log.Out = os.Stderr + if (len(os.Getenv("DEBUG")) > 0) || *flDebug { + logrus.SetLevel(logrus.DebugLevel) + } + var newDir, oldDir string + + if len(*flNewDir) == 0 { + var err error + newDir, err = ioutil.TempDir("", "docker-test-newDir") + if err != nil { + log.Fatal(err) + } + defer os.RemoveAll(newDir) + if _, err := prepareUntarSourceDirectory(100, newDir, true); err != nil { + log.Fatal(err) + } + } else { + newDir = *flNewDir + } + + if len(*flOldDir) == 0 { + oldDir, err := ioutil.TempDir("", "docker-test-oldDir") + if err != nil { + log.Fatal(err) + } + defer os.RemoveAll(oldDir) + } else { + oldDir = *flOldDir + } + + changes, err := archive.ChangesDirs(newDir, oldDir) + if err != nil { + log.Fatal(err) + } + + a, err := archive.ExportChanges(newDir, changes) + if err != nil { + log.Fatal(err) + } + defer a.Close() + + i, err := io.Copy(os.Stdout, a) + if err != nil && err != io.EOF { + log.Fatal(err) + } + fmt.Fprintf(os.Stderr, "wrote archive of %d bytes", i) +} + +func prepareUntarSourceDirectory(numberOfFiles int, targetPath string, makeLinks bool) (int, error) { + fileData := []byte("fooo") + for n := 0; n < numberOfFiles; n++ { + fileName := fmt.Sprintf("file-%d", n) + if err := ioutil.WriteFile(path.Join(targetPath, fileName), fileData, 0700); err != nil { + return 0, err + } + if makeLinks { + if err := os.Link(path.Join(targetPath, fileName), path.Join(targetPath, fileName+"-link")); err != nil { + return 0, err + } + } + } + totalSize := numberOfFiles * len(fileData) + return totalSize, nil +} diff --git a/pkg/archive/testdata/broken.tar b/pkg/archive/testdata/broken.tar new file mode 100644 index 0000000000000000000000000000000000000000..8f10ea6b87d3eb4fed572349dfe87695603b10a5 GIT binary patch literal 13824 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 literal 0 HcmV?d00001 diff --git a/pkg/archive/time_linux.go b/pkg/archive/time_linux.go new file mode 100644 index 00000000..3448569b --- /dev/null +++ b/pkg/archive/time_linux.go @@ -0,0 +1,16 @@ +package archive + +import ( + "syscall" + "time" +) + +func timeToTimespec(time time.Time) (ts syscall.Timespec) { + if time.IsZero() { + // Return UTIME_OMIT special value + ts.Sec = 0 + ts.Nsec = ((1 << 30) - 2) + return + } + return syscall.NsecToTimespec(time.UnixNano()) +} diff --git a/pkg/archive/time_unsupported.go b/pkg/archive/time_unsupported.go new file mode 100644 index 00000000..e85aac05 --- /dev/null +++ b/pkg/archive/time_unsupported.go @@ -0,0 +1,16 @@ +// +build !linux + +package archive + +import ( + "syscall" + "time" +) + +func timeToTimespec(time time.Time) (ts syscall.Timespec) { + nsec := int64(0) + if !time.IsZero() { + nsec = time.UnixNano() + } + return syscall.NsecToTimespec(nsec) +} diff --git a/pkg/archive/utils_test.go b/pkg/archive/utils_test.go new file mode 100644 index 00000000..98719032 --- /dev/null +++ b/pkg/archive/utils_test.go @@ -0,0 +1,166 @@ +package archive + +import ( + "archive/tar" + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "time" +) + +var testUntarFns = map[string]func(string, io.Reader) error{ + "untar": func(dest string, r io.Reader) error { + return Untar(r, dest, nil) + }, + "applylayer": func(dest string, r io.Reader) error { + _, err := ApplyLayer(dest, Reader(r)) + return err + }, +} + +// testBreakout is a helper function that, within the provided `tmpdir` directory, +// creates a `victim` folder with a generated `hello` file in it. +// `untar` extracts to a directory named `dest`, the tar file created from `headers`. +// +// Here are the tested scenarios: +// - removed `victim` folder (write) +// - removed files from `victim` folder (write) +// - new files in `victim` folder (write) +// - modified files in `victim` folder (write) +// - file in `dest` with same content as `victim/hello` (read) +// +// When using testBreakout make sure you cover one of the scenarios listed above. +func testBreakout(untarFn string, tmpdir string, headers []*tar.Header) error { + tmpdir, err := ioutil.TempDir("", tmpdir) + if err != nil { + return err + } + defer os.RemoveAll(tmpdir) + + dest := filepath.Join(tmpdir, "dest") + if err := os.Mkdir(dest, 0755); err != nil { + return err + } + + victim := filepath.Join(tmpdir, "victim") + if err := os.Mkdir(victim, 0755); err != nil { + return err + } + hello := filepath.Join(victim, "hello") + helloData, err := time.Now().MarshalText() + if err != nil { + return err + } + if err := ioutil.WriteFile(hello, helloData, 0644); err != nil { + return err + } + helloStat, err := os.Stat(hello) + if err != nil { + return err + } + + reader, writer := io.Pipe() + go func() { + t := tar.NewWriter(writer) + for _, hdr := range headers { + t.WriteHeader(hdr) + } + t.Close() + }() + + untar := testUntarFns[untarFn] + if untar == nil { + return fmt.Errorf("could not find untar function %q in testUntarFns", untarFn) + } + if err := untar(dest, reader); err != nil { + if _, ok := err.(breakoutError); !ok { + // If untar returns an error unrelated to an archive breakout, + // then consider this an unexpected error and abort. + return err + } + // Here, untar detected the breakout. + // Let's move on verifying that indeed there was no breakout. + fmt.Printf("breakoutError: %v\n", err) + } + + // Check victim folder + f, err := os.Open(victim) + if err != nil { + // codepath taken if victim folder was removed + return fmt.Errorf("archive breakout: error reading %q: %v", victim, err) + } + defer f.Close() + + // Check contents of victim folder + // + // We are only interested in getting 2 files from the victim folder, because if all is well + // we expect only one result, the `hello` file. If there is a second result, it cannot + // hold the same name `hello` and we assume that a new file got created in the victim folder. + // That is enough to detect an archive breakout. + names, err := f.Readdirnames(2) + if err != nil { + // codepath taken if victim is not a folder + return fmt.Errorf("archive breakout: error reading directory content of %q: %v", victim, err) + } + for _, name := range names { + if name != "hello" { + // codepath taken if new file was created in victim folder + return fmt.Errorf("archive breakout: new file %q", name) + } + } + + // Check victim/hello + f, err = os.Open(hello) + if err != nil { + // codepath taken if read permissions were removed + return fmt.Errorf("archive breakout: could not lstat %q: %v", hello, err) + } + defer f.Close() + b, err := ioutil.ReadAll(f) + if err != nil { + return err + } + fi, err := f.Stat() + if err != nil { + return err + } + if helloStat.IsDir() != fi.IsDir() || + // TODO: cannot check for fi.ModTime() change + helloStat.Mode() != fi.Mode() || + helloStat.Size() != fi.Size() || + !bytes.Equal(helloData, b) { + // codepath taken if hello has been modified + return fmt.Errorf("archive breakout: file %q has been modified. Contents: expected=%q, got=%q. FileInfo: expected=%#v, got=%#v", hello, helloData, b, helloStat, fi) + } + + // Check that nothing in dest/ has the same content as victim/hello. + // Since victim/hello was generated with time.Now(), it is safe to assume + // that any file whose content matches exactly victim/hello, managed somehow + // to access victim/hello. + return filepath.Walk(dest, func(path string, info os.FileInfo, err error) error { + if info.IsDir() { + if err != nil { + // skip directory if error + return filepath.SkipDir + } + // enter directory + return nil + } + if err != nil { + // skip file if error + return nil + } + b, err := ioutil.ReadFile(path) + if err != nil { + // Houston, we have a problem. Aborting (space)walk. + return err + } + if bytes.Equal(helloData, b) { + return fmt.Errorf("archive breakout: file %q has been accessed via %q", hello, path) + } + return nil + }) +} diff --git a/pkg/archive/whiteouts.go b/pkg/archive/whiteouts.go new file mode 100644 index 00000000..d20478a1 --- /dev/null +++ b/pkg/archive/whiteouts.go @@ -0,0 +1,23 @@ +package archive + +// Whiteouts are files with a special meaning for the layered filesystem. +// Docker uses AUFS whiteout files inside exported archives. In other +// filesystems these files are generated/handled on tar creation/extraction. + +// WhiteoutPrefix prefix means file is a whiteout. If this is followed by a +// filename this means that file has been removed from the base layer. +const WhiteoutPrefix = ".wh." + +// WhiteoutMetaPrefix prefix means whiteout has a special meaning and is not +// for removing an actual file. Normally these files are excluded from exported +// archives. +const WhiteoutMetaPrefix = WhiteoutPrefix + WhiteoutPrefix + +// WhiteoutLinkDir is a directory AUFS uses for storing hardlink links to other +// layers. Normally these should not go into exported archives and all changed +// hardlinks should be copied to the top layer. +const WhiteoutLinkDir = WhiteoutMetaPrefix + "plnk" + +// WhiteoutOpaqueDir file means directory has been made opaque - meaning +// readdir calls to this directory do not follow to lower layers. +const WhiteoutOpaqueDir = WhiteoutMetaPrefix + ".opq" diff --git a/pkg/archive/wrap.go b/pkg/archive/wrap.go new file mode 100644 index 00000000..dfb335c0 --- /dev/null +++ b/pkg/archive/wrap.go @@ -0,0 +1,59 @@ +package archive + +import ( + "archive/tar" + "bytes" + "io/ioutil" +) + +// Generate generates a new archive from the content provided +// as input. +// +// `files` is a sequence of path/content pairs. A new file is +// added to the archive for each pair. +// If the last pair is incomplete, the file is created with an +// empty content. For example: +// +// Generate("foo.txt", "hello world", "emptyfile") +// +// The above call will return an archive with 2 files: +// * ./foo.txt with content "hello world" +// * ./empty with empty content +// +// FIXME: stream content instead of buffering +// FIXME: specify permissions and other archive metadata +func Generate(input ...string) (Archive, error) { + files := parseStringPairs(input...) + buf := new(bytes.Buffer) + tw := tar.NewWriter(buf) + for _, file := range files { + name, content := file[0], file[1] + hdr := &tar.Header{ + Name: name, + Size: int64(len(content)), + } + if err := tw.WriteHeader(hdr); err != nil { + return nil, err + } + if _, err := tw.Write([]byte(content)); err != nil { + return nil, err + } + } + if err := tw.Close(); err != nil { + return nil, err + } + return ioutil.NopCloser(buf), nil +} + +func parseStringPairs(input ...string) (output [][2]string) { + output = make([][2]string, 0, len(input)/2+1) + for i := 0; i < len(input); i += 2 { + var pair [2]string + pair[0] = input[i] + if i+1 < len(input) { + pair[1] = input[i+1] + } + output = append(output, pair) + } + return +} diff --git a/pkg/archive/wrap_test.go b/pkg/archive/wrap_test.go new file mode 100644 index 00000000..46ab3669 --- /dev/null +++ b/pkg/archive/wrap_test.go @@ -0,0 +1,98 @@ +package archive + +import ( + "archive/tar" + "bytes" + "io" + "testing" +) + +func TestGenerateEmptyFile(t *testing.T) { + archive, err := Generate("emptyFile") + if err != nil { + t.Fatal(err) + } + if archive == nil { + t.Fatal("The generated archive should not be nil.") + } + + expectedFiles := [][]string{ + {"emptyFile", ""}, + } + + tr := tar.NewReader(archive) + actualFiles := make([][]string, 0, 10) + i := 0 + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + t.Fatal(err) + } + buf := new(bytes.Buffer) + buf.ReadFrom(tr) + content := buf.String() + actualFiles = append(actualFiles, []string{hdr.Name, content}) + i++ + } + if len(actualFiles) != len(expectedFiles) { + t.Fatalf("Number of expected file %d, got %d.", len(expectedFiles), len(actualFiles)) + } + for i := 0; i < len(expectedFiles); i++ { + actual := actualFiles[i] + expected := expectedFiles[i] + if actual[0] != expected[0] { + t.Fatalf("Expected name '%s', Actual name '%s'", expected[0], actual[0]) + } + if actual[1] != expected[1] { + t.Fatalf("Expected content '%s', Actual content '%s'", expected[1], actual[1]) + } + } +} + +func TestGenerateWithContent(t *testing.T) { + archive, err := Generate("file", "content") + if err != nil { + t.Fatal(err) + } + if archive == nil { + t.Fatal("The generated archive should not be nil.") + } + + expectedFiles := [][]string{ + {"file", "content"}, + } + + tr := tar.NewReader(archive) + actualFiles := make([][]string, 0, 10) + i := 0 + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + t.Fatal(err) + } + buf := new(bytes.Buffer) + buf.ReadFrom(tr) + content := buf.String() + actualFiles = append(actualFiles, []string{hdr.Name, content}) + i++ + } + if len(actualFiles) != len(expectedFiles) { + t.Fatalf("Number of expected file %d, got %d.", len(expectedFiles), len(actualFiles)) + } + for i := 0; i < len(expectedFiles); i++ { + actual := actualFiles[i] + expected := expectedFiles[i] + if actual[0] != expected[0] { + t.Fatalf("Expected name '%s', Actual name '%s'", expected[0], actual[0]) + } + if actual[1] != expected[1] { + t.Fatalf("Expected content '%s', Actual content '%s'", expected[1], actual[1]) + } + } +} diff --git a/pkg/authorization/api.go b/pkg/authorization/api.go new file mode 100644 index 00000000..fc82c46b --- /dev/null +++ b/pkg/authorization/api.go @@ -0,0 +1,54 @@ +package authorization + +const ( + // AuthZApiRequest is the url for daemon request authorization + AuthZApiRequest = "AuthZPlugin.AuthZReq" + + // AuthZApiResponse is the url for daemon response authorization + AuthZApiResponse = "AuthZPlugin.AuthZRes" + + // AuthZApiImplements is the name of the interface all AuthZ plugins implement + AuthZApiImplements = "authz" +) + +// Request holds data required for authZ plugins +type Request struct { + // User holds the user extracted by AuthN mechanism + User string `json:"User,omitempty"` + + // UserAuthNMethod holds the mechanism used to extract user details (e.g., krb) + UserAuthNMethod string `json:"UserAuthNMethod,omitempty"` + + // RequestMethod holds the HTTP method (GET/POST/PUT) + RequestMethod string `json:"RequestMethod,omitempty"` + + // RequestUri holds the full HTTP uri (e.g., /v1.21/version) + RequestURI string `json:"RequestUri,omitempty"` + + // RequestBody stores the raw request body sent to the docker daemon + RequestBody []byte `json:"RequestBody,omitempty"` + + // RequestHeaders stores the raw request headers sent to the docker daemon + RequestHeaders map[string]string `json:"RequestHeaders,omitempty"` + + // ResponseStatusCode stores the status code returned from docker daemon + ResponseStatusCode int `json:"ResponseStatusCode,omitempty"` + + // ResponseBody stores the raw response body sent from docker daemon + ResponseBody []byte `json:"ResponseBody,omitempty"` + + // ResponseHeaders stores the response headers sent to the docker daemon + ResponseHeaders map[string]string `json:"ResponseHeaders,omitempty"` +} + +// Response represents authZ plugin response +type Response struct { + // Allow indicating whether the user is allowed or not + Allow bool `json:"Allow"` + + // Msg stores the authorization message + Msg string `json:"Msg,omitempty"` + + // Err stores a message in case there's an error + Err string `json:"Err,omitempty"` +} diff --git a/pkg/authorization/authz.go b/pkg/authorization/authz.go new file mode 100644 index 00000000..f7039086 --- /dev/null +++ b/pkg/authorization/authz.go @@ -0,0 +1,165 @@ +package authorization + +import ( + "bufio" + "bytes" + "fmt" + "io" + "net/http" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/ioutils" +) + +const maxBodySize = 1048576 // 1MB + +// NewCtx creates new authZ context, it is used to store authorization information related to a specific docker +// REST http session +// A context provides two method: +// Authenticate Request: +// Call authZ plugins with current REST request and AuthN response +// Request contains full HTTP packet sent to the docker daemon +// https://docs.docker.com/reference/api/docker_remote_api/ +// +// Authenticate Response: +// Call authZ plugins with full info about current REST request, REST response and AuthN response +// The response from this method may contains content that overrides the daemon response +// This allows authZ plugins to filter privileged content +// +// If multiple authZ plugins are specified, the block/allow decision is based on ANDing all plugin results +// For response manipulation, the response from each plugin is piped between plugins. Plugin execution order +// is determined according to daemon parameters +func NewCtx(authZPlugins []Plugin, user, userAuthNMethod, requestMethod, requestURI string) *Ctx { + return &Ctx{ + plugins: authZPlugins, + user: user, + userAuthNMethod: userAuthNMethod, + requestMethod: requestMethod, + requestURI: requestURI, + } +} + +// Ctx stores a a single request-response interaction context +type Ctx struct { + user string + userAuthNMethod string + requestMethod string + requestURI string + plugins []Plugin + // authReq stores the cached request object for the current transaction + authReq *Request +} + +// AuthZRequest authorized the request to the docker daemon using authZ plugins +func (ctx *Ctx) AuthZRequest(w http.ResponseWriter, r *http.Request) error { + var body []byte + if sendBody(ctx.requestURI, r.Header) && r.ContentLength > 0 && r.ContentLength < maxBodySize { + var err error + body, r.Body, err = drainBody(r.Body) + if err != nil { + return err + } + } + + var h bytes.Buffer + if err := r.Header.Write(&h); err != nil { + return err + } + + ctx.authReq = &Request{ + User: ctx.user, + UserAuthNMethod: ctx.userAuthNMethod, + RequestMethod: ctx.requestMethod, + RequestURI: ctx.requestURI, + RequestBody: body, + RequestHeaders: headers(r.Header), + } + + for _, plugin := range ctx.plugins { + logrus.Debugf("AuthZ request using plugin %s", plugin.Name()) + + authRes, err := plugin.AuthZRequest(ctx.authReq) + if err != nil { + return fmt.Errorf("plugin %s failed with error: %s", plugin.Name(), err) + } + + if !authRes.Allow { + return fmt.Errorf("authorization denied by plugin %s: %s", plugin.Name(), authRes.Msg) + } + } + + return nil +} + +// AuthZResponse authorized and manipulates the response from docker daemon using authZ plugins +func (ctx *Ctx) AuthZResponse(rm ResponseModifier, r *http.Request) error { + ctx.authReq.ResponseStatusCode = rm.StatusCode() + ctx.authReq.ResponseHeaders = headers(rm.Header()) + + if sendBody(ctx.requestURI, rm.Header()) { + ctx.authReq.ResponseBody = rm.RawBody() + } + + for _, plugin := range ctx.plugins { + logrus.Debugf("AuthZ response using plugin %s", plugin.Name()) + + authRes, err := plugin.AuthZResponse(ctx.authReq) + if err != nil { + return fmt.Errorf("plugin %s failed with error: %s", plugin.Name(), err) + } + + if !authRes.Allow { + return fmt.Errorf("authorization denied by plugin %s: %s", plugin.Name(), authRes.Msg) + } + } + + rm.FlushAll() + + return nil +} + +// drainBody dump the body (if it's length is less than 1MB) without modifying the request state +func drainBody(body io.ReadCloser) ([]byte, io.ReadCloser, error) { + bufReader := bufio.NewReaderSize(body, maxBodySize) + newBody := ioutils.NewReadCloserWrapper(bufReader, func() error { return body.Close() }) + + data, err := bufReader.Peek(maxBodySize) + // Body size exceeds max body size + if err == nil { + logrus.Warnf("Request body is larger than: '%d' skipping body", maxBodySize) + return nil, newBody, nil + } + // Body size is less than maximum size + if err == io.EOF { + return data, newBody, nil + } + // Unknown error + return nil, newBody, err +} + +// sendBody returns true when request/response body should be sent to AuthZPlugin +func sendBody(url string, header http.Header) bool { + // Skip body for auth endpoint + if strings.HasSuffix(url, "/auth") { + return false + } + + // body is sent only for text or json messages + return header.Get("Content-Type") == "application/json" +} + +// headers returns flatten version of the http headers excluding authorization +func headers(header http.Header) map[string]string { + v := make(map[string]string, 0) + for k, values := range header { + // Skip authorization headers + if strings.EqualFold(k, "Authorization") || strings.EqualFold(k, "X-Registry-Config") || strings.EqualFold(k, "X-Registry-Auth") { + continue + } + for _, val := range values { + v[k] = val + } + } + return v +} diff --git a/pkg/authorization/authz_unix_test.go b/pkg/authorization/authz_unix_test.go new file mode 100644 index 00000000..1c6dc8a1 --- /dev/null +++ b/pkg/authorization/authz_unix_test.go @@ -0,0 +1,276 @@ +// +build !windows + +// TODO Windows: This uses a Unix socket for testing. This might be possible +// to port to Windows using a named pipe instead. + +package authorization + +import ( + "encoding/json" + "io/ioutil" + "log" + "net" + "net/http" + "net/http/httptest" + "os" + "path" + "reflect" + "testing" + + "bytes" + "strings" + + "github.com/docker/docker/pkg/plugins" + "github.com/docker/go-connections/tlsconfig" + "github.com/gorilla/mux" +) + +const pluginAddress = "authzplugin.sock" + +func TestAuthZRequestPluginError(t *testing.T) { + server := authZPluginTestServer{t: t} + go server.start() + defer server.stop() + + authZPlugin := createTestPlugin(t) + + request := Request{ + User: "user", + RequestBody: []byte("sample body"), + RequestURI: "www.authz.com", + RequestMethod: "GET", + RequestHeaders: map[string]string{"header": "value"}, + } + server.replayResponse = Response{ + Err: "an error", + } + + actualResponse, err := authZPlugin.AuthZRequest(&request) + if err != nil { + t.Fatalf("Failed to authorize request %v", err) + } + + if !reflect.DeepEqual(server.replayResponse, *actualResponse) { + t.Fatalf("Response must be equal") + } + if !reflect.DeepEqual(request, server.recordedRequest) { + t.Fatalf("Requests must be equal") + } +} + +func TestAuthZRequestPlugin(t *testing.T) { + server := authZPluginTestServer{t: t} + go server.start() + defer server.stop() + + authZPlugin := createTestPlugin(t) + + request := Request{ + User: "user", + RequestBody: []byte("sample body"), + RequestURI: "www.authz.com", + RequestMethod: "GET", + RequestHeaders: map[string]string{"header": "value"}, + } + server.replayResponse = Response{ + Allow: true, + Msg: "Sample message", + } + + actualResponse, err := authZPlugin.AuthZRequest(&request) + if err != nil { + t.Fatalf("Failed to authorize request %v", err) + } + + if !reflect.DeepEqual(server.replayResponse, *actualResponse) { + t.Fatalf("Response must be equal") + } + if !reflect.DeepEqual(request, server.recordedRequest) { + t.Fatalf("Requests must be equal") + } +} + +func TestAuthZResponsePlugin(t *testing.T) { + server := authZPluginTestServer{t: t} + go server.start() + defer server.stop() + + authZPlugin := createTestPlugin(t) + + request := Request{ + User: "user", + RequestBody: []byte("sample body"), + } + server.replayResponse = Response{ + Allow: true, + Msg: "Sample message", + } + + actualResponse, err := authZPlugin.AuthZResponse(&request) + if err != nil { + t.Fatalf("Failed to authorize request %v", err) + } + + if !reflect.DeepEqual(server.replayResponse, *actualResponse) { + t.Fatalf("Response must be equal") + } + if !reflect.DeepEqual(request, server.recordedRequest) { + t.Fatalf("Requests must be equal") + } +} + +func TestResponseModifier(t *testing.T) { + r := httptest.NewRecorder() + m := NewResponseModifier(r) + m.Header().Set("h1", "v1") + m.Write([]byte("body")) + m.WriteHeader(500) + + m.FlushAll() + if r.Header().Get("h1") != "v1" { + t.Fatalf("Header value must exists %s", r.Header().Get("h1")) + } + if !reflect.DeepEqual(r.Body.Bytes(), []byte("body")) { + t.Fatalf("Body value must exists %s", r.Body.Bytes()) + } + if r.Code != 500 { + t.Fatalf("Status code must be correct %d", r.Code) + } +} + +func TestDrainBody(t *testing.T) { + + tests := []struct { + length int // length is the message length send to drainBody + expectedBodyLength int // expectedBodyLength is the expected body length after drainBody is called + }{ + {10, 10}, // Small message size + {maxBodySize - 1, maxBodySize - 1}, // Max message size + {maxBodySize * 2, 0}, // Large message size (skip copying body) + + } + + for _, test := range tests { + + msg := strings.Repeat("a", test.length) + body, closer, err := drainBody(ioutil.NopCloser(bytes.NewReader([]byte(msg)))) + if len(body) != test.expectedBodyLength { + t.Fatalf("Body must be copied, actual length: '%d'", len(body)) + } + if closer == nil { + t.Fatalf("Closer must not be nil") + } + if err != nil { + t.Fatalf("Error must not be nil: '%v'", err) + } + modified, err := ioutil.ReadAll(closer) + if err != nil { + t.Fatalf("Error must not be nil: '%v'", err) + } + if len(modified) != len(msg) { + t.Fatalf("Result should not be truncated. Original length: '%d', new length: '%d'", len(msg), len(modified)) + } + } +} + +func TestResponseModifierOverride(t *testing.T) { + r := httptest.NewRecorder() + m := NewResponseModifier(r) + m.Header().Set("h1", "v1") + m.Write([]byte("body")) + m.WriteHeader(500) + + overrideHeader := make(http.Header) + overrideHeader.Add("h1", "v2") + overrideHeaderBytes, err := json.Marshal(overrideHeader) + if err != nil { + t.Fatalf("override header failed %v", err) + } + + m.OverrideHeader(overrideHeaderBytes) + m.OverrideBody([]byte("override body")) + m.OverrideStatusCode(404) + m.FlushAll() + if r.Header().Get("h1") != "v2" { + t.Fatalf("Header value must exists %s", r.Header().Get("h1")) + } + if !reflect.DeepEqual(r.Body.Bytes(), []byte("override body")) { + t.Fatalf("Body value must exists %s", r.Body.Bytes()) + } + if r.Code != 404 { + t.Fatalf("Status code must be correct %d", r.Code) + } +} + +// createTestPlugin creates a new sample authorization plugin +func createTestPlugin(t *testing.T) *authorizationPlugin { + plugin := &plugins.Plugin{Name: "authz"} + pwd, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + + plugin.Client, err = plugins.NewClient("unix:///"+path.Join(pwd, pluginAddress), tlsconfig.Options{InsecureSkipVerify: true}) + if err != nil { + t.Fatalf("Failed to create client %v", err) + } + + return &authorizationPlugin{name: "plugin", plugin: plugin} +} + +// AuthZPluginTestServer is a simple server that implements the authZ plugin interface +type authZPluginTestServer struct { + listener net.Listener + t *testing.T + // request stores the request sent from the daemon to the plugin + recordedRequest Request + // response stores the response sent from the plugin to the daemon + replayResponse Response +} + +// start starts the test server that implements the plugin +func (t *authZPluginTestServer) start() { + r := mux.NewRouter() + os.Remove(pluginAddress) + l, err := net.ListenUnix("unix", &net.UnixAddr{Name: pluginAddress, Net: "unix"}) + if err != nil { + t.t.Fatalf("Failed to listen %v", err) + } + t.listener = l + + r.HandleFunc("/Plugin.Activate", t.activate) + r.HandleFunc("/"+AuthZApiRequest, t.auth) + r.HandleFunc("/"+AuthZApiResponse, t.auth) + t.listener, _ = net.Listen("tcp", pluginAddress) + server := http.Server{Handler: r, Addr: pluginAddress} + server.Serve(l) +} + +// stop stops the test server that implements the plugin +func (t *authZPluginTestServer) stop() { + os.Remove(pluginAddress) + if t.listener != nil { + t.listener.Close() + } +} + +// auth is a used to record/replay the authentication api messages +func (t *authZPluginTestServer) auth(w http.ResponseWriter, r *http.Request) { + t.recordedRequest = Request{} + defer r.Body.Close() + body, err := ioutil.ReadAll(r.Body) + json.Unmarshal(body, &t.recordedRequest) + b, err := json.Marshal(t.replayResponse) + if err != nil { + log.Fatal(err) + } + w.Write(b) +} + +func (t *authZPluginTestServer) activate(w http.ResponseWriter, r *http.Request) { + b, err := json.Marshal(plugins.Manifest{Implements: []string{AuthZApiImplements}}) + if err != nil { + log.Fatal(err) + } + w.Write(b) +} diff --git a/pkg/authorization/plugin.go b/pkg/authorization/plugin.go new file mode 100644 index 00000000..1b65ac0a --- /dev/null +++ b/pkg/authorization/plugin.go @@ -0,0 +1,83 @@ +package authorization + +import "github.com/docker/docker/pkg/plugins" + +// Plugin allows third party plugins to authorize requests and responses +// in the context of docker API +type Plugin interface { + // Name returns the registered plugin name + Name() string + + // AuthZRequest authorize the request from the client to the daemon + AuthZRequest(*Request) (*Response, error) + + // AuthZResponse authorize the response from the daemon to the client + AuthZResponse(*Request) (*Response, error) +} + +// NewPlugins constructs and initialize the authorization plugins based on plugin names +func NewPlugins(names []string) []Plugin { + plugins := []Plugin{} + pluginsMap := make(map[string]struct{}) + for _, name := range names { + if _, ok := pluginsMap[name]; ok { + continue + } + pluginsMap[name] = struct{}{} + plugins = append(plugins, newAuthorizationPlugin(name)) + } + return plugins +} + +// authorizationPlugin is an internal adapter to docker plugin system +type authorizationPlugin struct { + plugin *plugins.Plugin + name string +} + +func newAuthorizationPlugin(name string) Plugin { + return &authorizationPlugin{name: name} +} + +func (a *authorizationPlugin) Name() string { + return a.name +} + +func (a *authorizationPlugin) AuthZRequest(authReq *Request) (*Response, error) { + if err := a.initPlugin(); err != nil { + return nil, err + } + + authRes := &Response{} + if err := a.plugin.Client.Call(AuthZApiRequest, authReq, authRes); err != nil { + return nil, err + } + + return authRes, nil +} + +func (a *authorizationPlugin) AuthZResponse(authReq *Request) (*Response, error) { + if err := a.initPlugin(); err != nil { + return nil, err + } + + authRes := &Response{} + if err := a.plugin.Client.Call(AuthZApiResponse, authReq, authRes); err != nil { + return nil, err + } + + return authRes, nil +} + +// initPlugin initialize the authorization plugin if needed +func (a *authorizationPlugin) initPlugin() error { + // Lazy loading of plugins + if a.plugin == nil { + var err error + a.plugin, err = plugins.Get(a.name, AuthZApiImplements) + if err != nil { + return err + } + } + return nil +} diff --git a/pkg/authorization/response.go b/pkg/authorization/response.go new file mode 100644 index 00000000..245a0ef7 --- /dev/null +++ b/pkg/authorization/response.go @@ -0,0 +1,203 @@ +package authorization + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "github.com/Sirupsen/logrus" + "net" + "net/http" +) + +// ResponseModifier allows authorization plugins to read and modify the content of the http.response +type ResponseModifier interface { + http.ResponseWriter + http.Flusher + http.CloseNotifier + + // RawBody returns the current http content + RawBody() []byte + + // RawHeaders returns the current content of the http headers + RawHeaders() ([]byte, error) + + // StatusCode returns the current status code + StatusCode() int + + // OverrideBody replace the body of the HTTP reply + OverrideBody(b []byte) + + // OverrideHeader replace the headers of the HTTP reply + OverrideHeader(b []byte) error + + // OverrideStatusCode replaces the status code of the HTTP reply + OverrideStatusCode(statusCode int) + + // Flush flushes all data to the HTTP response + FlushAll() error + + // Hijacked indicates the response has been hijacked by the Docker daemon + Hijacked() bool +} + +// NewResponseModifier creates a wrapper to an http.ResponseWriter to allow inspecting and modifying the content +func NewResponseModifier(rw http.ResponseWriter) ResponseModifier { + return &responseModifier{rw: rw, header: make(http.Header)} +} + +// responseModifier is used as an adapter to http.ResponseWriter in order to manipulate and explore +// the http request/response from docker daemon +type responseModifier struct { + // The original response writer + rw http.ResponseWriter + + r *http.Request + + status int + // body holds the response body + body []byte + // header holds the response header + header http.Header + // statusCode holds the response status code + statusCode int + // hijacked indicates the request has been hijacked + hijacked bool +} + +func (rm *responseModifier) Hijacked() bool { + return rm.hijacked +} + +// WriterHeader stores the http status code +func (rm *responseModifier) WriteHeader(s int) { + + // Use original request if hijacked + if rm.hijacked { + rm.rw.WriteHeader(s) + return + } + + rm.statusCode = s +} + +// Header returns the internal http header +func (rm *responseModifier) Header() http.Header { + + // Use original header if hijacked + if rm.hijacked { + return rm.rw.Header() + } + + return rm.header +} + +// Header returns the internal http header +func (rm *responseModifier) StatusCode() int { + return rm.statusCode +} + +// Override replace the body of the HTTP reply +func (rm *responseModifier) OverrideBody(b []byte) { + rm.body = b +} + +func (rm *responseModifier) OverrideStatusCode(statusCode int) { + rm.statusCode = statusCode +} + +// Override replace the headers of the HTTP reply +func (rm *responseModifier) OverrideHeader(b []byte) error { + header := http.Header{} + if err := json.Unmarshal(b, &header); err != nil { + return err + } + rm.header = header + return nil +} + +// Write stores the byte array inside content +func (rm *responseModifier) Write(b []byte) (int, error) { + + if rm.hijacked { + return rm.rw.Write(b) + } + + rm.body = append(rm.body, b...) + return len(b), nil +} + +// Body returns the response body +func (rm *responseModifier) RawBody() []byte { + return rm.body +} + +func (rm *responseModifier) RawHeaders() ([]byte, error) { + var b bytes.Buffer + if err := rm.header.Write(&b); err != nil { + return nil, err + } + return b.Bytes(), nil +} + +// Hijack returns the internal connection of the wrapped http.ResponseWriter +func (rm *responseModifier) Hijack() (net.Conn, *bufio.ReadWriter, error) { + + rm.hijacked = true + rm.FlushAll() + + hijacker, ok := rm.rw.(http.Hijacker) + if !ok { + return nil, nil, fmt.Errorf("Internal response writer doesn't support the Hijacker interface") + } + return hijacker.Hijack() +} + +// CloseNotify uses the internal close notify API of the wrapped http.ResponseWriter +func (rm *responseModifier) CloseNotify() <-chan bool { + closeNotifier, ok := rm.rw.(http.CloseNotifier) + if !ok { + logrus.Errorf("Internal response writer doesn't support the CloseNotifier interface") + return nil + } + return closeNotifier.CloseNotify() +} + +// Flush uses the internal flush API of the wrapped http.ResponseWriter +func (rm *responseModifier) Flush() { + flusher, ok := rm.rw.(http.Flusher) + if !ok { + logrus.Errorf("Internal response writer doesn't support the Flusher interface") + return + } + + rm.FlushAll() + flusher.Flush() +} + +// FlushAll flushes all data to the HTTP response +func (rm *responseModifier) FlushAll() error { + // Copy the status code + if rm.statusCode > 0 { + rm.rw.WriteHeader(rm.statusCode) + } + + // Copy the header + for k, vv := range rm.header { + for _, v := range vv { + rm.rw.Header().Add(k, v) + } + } + + var err error + if len(rm.body) > 0 { + // Write body + _, err = rm.rw.Write(rm.body) + } + + // Clean previous data + rm.body = nil + rm.statusCode = 0 + rm.header = http.Header{} + return err +} diff --git a/pkg/broadcaster/unbuffered.go b/pkg/broadcaster/unbuffered.go new file mode 100644 index 00000000..784d65d6 --- /dev/null +++ b/pkg/broadcaster/unbuffered.go @@ -0,0 +1,49 @@ +package broadcaster + +import ( + "io" + "sync" +) + +// Unbuffered accumulates multiple io.WriteCloser by stream. +type Unbuffered struct { + mu sync.Mutex + writers []io.WriteCloser +} + +// Add adds new io.WriteCloser. +func (w *Unbuffered) Add(writer io.WriteCloser) { + w.mu.Lock() + w.writers = append(w.writers, writer) + w.mu.Unlock() +} + +// Write writes bytes to all writers. Failed writers will be evicted during +// this call. +func (w *Unbuffered) Write(p []byte) (n int, err error) { + w.mu.Lock() + var evict []int + for i, sw := range w.writers { + if n, err := sw.Write(p); err != nil || n != len(p) { + // On error, evict the writer + evict = append(evict, i) + } + } + for n, i := range evict { + w.writers = append(w.writers[:i-n], w.writers[i-n+1:]...) + } + w.mu.Unlock() + return len(p), nil +} + +// Clean closes and removes all writers. Last non-eol-terminated part of data +// will be saved. +func (w *Unbuffered) Clean() error { + w.mu.Lock() + for _, sw := range w.writers { + sw.Close() + } + w.writers = nil + w.mu.Unlock() + return nil +} diff --git a/pkg/broadcaster/unbuffered_test.go b/pkg/broadcaster/unbuffered_test.go new file mode 100644 index 00000000..9f8e72bc --- /dev/null +++ b/pkg/broadcaster/unbuffered_test.go @@ -0,0 +1,162 @@ +package broadcaster + +import ( + "bytes" + "errors" + "strings" + + "testing" +) + +type dummyWriter struct { + buffer bytes.Buffer + failOnWrite bool +} + +func (dw *dummyWriter) Write(p []byte) (n int, err error) { + if dw.failOnWrite { + return 0, errors.New("Fake fail") + } + return dw.buffer.Write(p) +} + +func (dw *dummyWriter) String() string { + return dw.buffer.String() +} + +func (dw *dummyWriter) Close() error { + return nil +} + +func TestUnbuffered(t *testing.T) { + writer := new(Unbuffered) + + // Test 1: Both bufferA and bufferB should contain "foo" + bufferA := &dummyWriter{} + writer.Add(bufferA) + bufferB := &dummyWriter{} + writer.Add(bufferB) + writer.Write([]byte("foo")) + + if bufferA.String() != "foo" { + t.Errorf("Buffer contains %v", bufferA.String()) + } + + if bufferB.String() != "foo" { + t.Errorf("Buffer contains %v", bufferB.String()) + } + + // Test2: bufferA and bufferB should contain "foobar", + // while bufferC should only contain "bar" + bufferC := &dummyWriter{} + writer.Add(bufferC) + writer.Write([]byte("bar")) + + if bufferA.String() != "foobar" { + t.Errorf("Buffer contains %v", bufferA.String()) + } + + if bufferB.String() != "foobar" { + t.Errorf("Buffer contains %v", bufferB.String()) + } + + if bufferC.String() != "bar" { + t.Errorf("Buffer contains %v", bufferC.String()) + } + + // Test3: Test eviction on failure + bufferA.failOnWrite = true + writer.Write([]byte("fail")) + if bufferA.String() != "foobar" { + t.Errorf("Buffer contains %v", bufferA.String()) + } + if bufferC.String() != "barfail" { + t.Errorf("Buffer contains %v", bufferC.String()) + } + // Even though we reset the flag, no more writes should go in there + bufferA.failOnWrite = false + writer.Write([]byte("test")) + if bufferA.String() != "foobar" { + t.Errorf("Buffer contains %v", bufferA.String()) + } + if bufferC.String() != "barfailtest" { + t.Errorf("Buffer contains %v", bufferC.String()) + } + + // Test4: Test eviction on multiple simultaneous failures + bufferB.failOnWrite = true + bufferC.failOnWrite = true + bufferD := &dummyWriter{} + writer.Add(bufferD) + writer.Write([]byte("yo")) + writer.Write([]byte("ink")) + if strings.Contains(bufferB.String(), "yoink") { + t.Errorf("bufferB received write. contents: %q", bufferB) + } + if strings.Contains(bufferC.String(), "yoink") { + t.Errorf("bufferC received write. contents: %q", bufferC) + } + if g, w := bufferD.String(), "yoink"; g != w { + t.Errorf("bufferD = %q, want %q", g, w) + } + + writer.Clean() +} + +type devNullCloser int + +func (d devNullCloser) Close() error { + return nil +} + +func (d devNullCloser) Write(buf []byte) (int, error) { + return len(buf), nil +} + +// This test checks for races. It is only useful when run with the race detector. +func TestRaceUnbuffered(t *testing.T) { + writer := new(Unbuffered) + c := make(chan bool) + go func() { + writer.Add(devNullCloser(0)) + c <- true + }() + writer.Write([]byte("hello")) + <-c +} + +func BenchmarkUnbuffered(b *testing.B) { + writer := new(Unbuffered) + setUpWriter := func() { + for i := 0; i < 100; i++ { + writer.Add(devNullCloser(0)) + writer.Add(devNullCloser(0)) + writer.Add(devNullCloser(0)) + } + } + testLine := "Line that thinks that it is log line from docker" + var buf bytes.Buffer + for i := 0; i < 100; i++ { + buf.Write([]byte(testLine + "\n")) + } + // line without eol + buf.Write([]byte(testLine)) + testText := buf.Bytes() + b.SetBytes(int64(5 * len(testText))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + b.StopTimer() + setUpWriter() + b.StartTimer() + + for j := 0; j < 5; j++ { + if _, err := writer.Write(testText); err != nil { + b.Fatal(err) + } + } + + b.StopTimer() + writer.Clean() + b.StartTimer() + } +} diff --git a/pkg/chrootarchive/archive.go b/pkg/chrootarchive/archive.go new file mode 100644 index 00000000..a7814f5b --- /dev/null +++ b/pkg/chrootarchive/archive.go @@ -0,0 +1,97 @@ +package chrootarchive + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/idtools" +) + +var chrootArchiver = &archive.Archiver{Untar: Untar} + +// Untar reads a stream of bytes from `archive`, parses it as a tar archive, +// and unpacks it into the directory at `dest`. +// The archive may be compressed with one of the following algorithms: +// identity (uncompressed), gzip, bzip2, xz. +func Untar(tarArchive io.Reader, dest string, options *archive.TarOptions) error { + return untarHandler(tarArchive, dest, options, true) +} + +// UntarUncompressed reads a stream of bytes from `archive`, parses it as a tar archive, +// and unpacks it into the directory at `dest`. +// The archive must be an uncompressed stream. +func UntarUncompressed(tarArchive io.Reader, dest string, options *archive.TarOptions) error { + return untarHandler(tarArchive, dest, options, false) +} + +// Handler for teasing out the automatic decompression +func untarHandler(tarArchive io.Reader, dest string, options *archive.TarOptions, decompress bool) error { + + if tarArchive == nil { + return fmt.Errorf("Empty archive") + } + if options == nil { + options = &archive.TarOptions{} + } + if options.ExcludePatterns == nil { + options.ExcludePatterns = []string{} + } + + rootUID, rootGID, err := idtools.GetRootUIDGID(options.UIDMaps, options.GIDMaps) + if err != nil { + return err + } + + dest = filepath.Clean(dest) + if _, err := os.Stat(dest); os.IsNotExist(err) { + if err := idtools.MkdirAllNewAs(dest, 0755, rootUID, rootGID); err != nil { + return err + } + } + + r := ioutil.NopCloser(tarArchive) + if decompress { + decompressedArchive, err := archive.DecompressStream(tarArchive) + if err != nil { + return err + } + defer decompressedArchive.Close() + r = decompressedArchive + } + + return invokeUnpack(r, dest, options) +} + +// TarUntar is a convenience function which calls Tar and Untar, with the output of one piped into the other. +// If either Tar or Untar fails, TarUntar aborts and returns the error. +func TarUntar(src, dst string) error { + return chrootArchiver.TarUntar(src, dst) +} + +// CopyWithTar creates a tar archive of filesystem path `src`, and +// unpacks it at filesystem path `dst`. +// The archive is streamed directly with fixed buffering and no +// intermediary disk IO. +func CopyWithTar(src, dst string) error { + return chrootArchiver.CopyWithTar(src, dst) +} + +// CopyFileWithTar emulates the behavior of the 'cp' command-line +// for a single file. It copies a regular file from path `src` to +// path `dst`, and preserves all its metadata. +// +// If `dst` ends with a trailing slash '/' ('\' on Windows), the final +// destination path will be `dst/base(src)` or `dst\base(src)` +func CopyFileWithTar(src, dst string) (err error) { + return chrootArchiver.CopyFileWithTar(src, dst) +} + +// UntarPath is a convenience function which looks for an archive +// at filesystem path `src`, and unpacks it at `dst`. +func UntarPath(src, dst string) error { + return chrootArchiver.UntarPath(src, dst) +} diff --git a/pkg/chrootarchive/archive_test.go b/pkg/chrootarchive/archive_test.go new file mode 100644 index 00000000..5fbe2084 --- /dev/null +++ b/pkg/chrootarchive/archive_test.go @@ -0,0 +1,394 @@ +package chrootarchive + +import ( + "bytes" + "fmt" + "hash/crc32" + "io" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + "time" + + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/reexec" + "github.com/docker/docker/pkg/system" +) + +func init() { + reexec.Init() +} + +func TestChrootTarUntar(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "docker-TestChrootTarUntar") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + src := filepath.Join(tmpdir, "src") + if err := system.MkdirAll(src, 0700); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(filepath.Join(src, "toto"), []byte("hello toto"), 0644); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(filepath.Join(src, "lolo"), []byte("hello lolo"), 0644); err != nil { + t.Fatal(err) + } + stream, err := archive.Tar(src, archive.Uncompressed) + if err != nil { + t.Fatal(err) + } + dest := filepath.Join(tmpdir, "src") + if err := system.MkdirAll(dest, 0700); err != nil { + t.Fatal(err) + } + if err := Untar(stream, dest, &archive.TarOptions{ExcludePatterns: []string{"lolo"}}); err != nil { + t.Fatal(err) + } +} + +// gh#10426: Verify the fix for having a huge excludes list (like on `docker load` with large # of +// local images) +func TestChrootUntarWithHugeExcludesList(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "docker-TestChrootUntarHugeExcludes") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + src := filepath.Join(tmpdir, "src") + if err := system.MkdirAll(src, 0700); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(filepath.Join(src, "toto"), []byte("hello toto"), 0644); err != nil { + t.Fatal(err) + } + stream, err := archive.Tar(src, archive.Uncompressed) + if err != nil { + t.Fatal(err) + } + dest := filepath.Join(tmpdir, "dest") + if err := system.MkdirAll(dest, 0700); err != nil { + t.Fatal(err) + } + options := &archive.TarOptions{} + //65534 entries of 64-byte strings ~= 4MB of environment space which should overflow + //on most systems when passed via environment or command line arguments + excludes := make([]string, 65534, 65534) + for i := 0; i < 65534; i++ { + excludes[i] = strings.Repeat(string(i), 64) + } + options.ExcludePatterns = excludes + if err := Untar(stream, dest, options); err != nil { + t.Fatal(err) + } +} + +func TestChrootUntarEmptyArchive(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "docker-TestChrootUntarEmptyArchive") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + if err := Untar(nil, tmpdir, nil); err == nil { + t.Fatal("expected error on empty archive") + } +} + +func prepareSourceDirectory(numberOfFiles int, targetPath string, makeSymLinks bool) (int, error) { + fileData := []byte("fooo") + for n := 0; n < numberOfFiles; n++ { + fileName := fmt.Sprintf("file-%d", n) + if err := ioutil.WriteFile(filepath.Join(targetPath, fileName), fileData, 0700); err != nil { + return 0, err + } + if makeSymLinks { + if err := os.Symlink(filepath.Join(targetPath, fileName), filepath.Join(targetPath, fileName+"-link")); err != nil { + return 0, err + } + } + } + totalSize := numberOfFiles * len(fileData) + return totalSize, nil +} + +func getHash(filename string) (uint32, error) { + stream, err := ioutil.ReadFile(filename) + if err != nil { + return 0, err + } + hash := crc32.NewIEEE() + hash.Write(stream) + return hash.Sum32(), nil +} + +func compareDirectories(src string, dest string) error { + changes, err := archive.ChangesDirs(dest, src) + if err != nil { + return err + } + if len(changes) > 0 { + return fmt.Errorf("Unexpected differences after untar: %v", changes) + } + return nil +} + +func compareFiles(src string, dest string) error { + srcHash, err := getHash(src) + if err != nil { + return err + } + destHash, err := getHash(dest) + if err != nil { + return err + } + if srcHash != destHash { + return fmt.Errorf("%s is different from %s", src, dest) + } + return nil +} + +func TestChrootTarUntarWithSymlink(t *testing.T) { + // TODO Windows: Figure out why this is failing + if runtime.GOOS == "windows" { + t.Skip("Failing on Windows") + } + tmpdir, err := ioutil.TempDir("", "docker-TestChrootTarUntarWithSymlink") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + src := filepath.Join(tmpdir, "src") + if err := system.MkdirAll(src, 0700); err != nil { + t.Fatal(err) + } + if _, err := prepareSourceDirectory(10, src, true); err != nil { + t.Fatal(err) + } + dest := filepath.Join(tmpdir, "dest") + if err := TarUntar(src, dest); err != nil { + t.Fatal(err) + } + if err := compareDirectories(src, dest); err != nil { + t.Fatal(err) + } +} + +func TestChrootCopyWithTar(t *testing.T) { + // TODO Windows: Figure out why this is failing + if runtime.GOOS == "windows" { + t.Skip("Failing on Windows") + } + tmpdir, err := ioutil.TempDir("", "docker-TestChrootCopyWithTar") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + src := filepath.Join(tmpdir, "src") + if err := system.MkdirAll(src, 0700); err != nil { + t.Fatal(err) + } + if _, err := prepareSourceDirectory(10, src, true); err != nil { + t.Fatal(err) + } + + // Copy directory + dest := filepath.Join(tmpdir, "dest") + if err := CopyWithTar(src, dest); err != nil { + t.Fatal(err) + } + if err := compareDirectories(src, dest); err != nil { + t.Fatal(err) + } + + // Copy file + srcfile := filepath.Join(src, "file-1") + dest = filepath.Join(tmpdir, "destFile") + destfile := filepath.Join(dest, "file-1") + if err := CopyWithTar(srcfile, destfile); err != nil { + t.Fatal(err) + } + if err := compareFiles(srcfile, destfile); err != nil { + t.Fatal(err) + } + + // Copy symbolic link + srcLinkfile := filepath.Join(src, "file-1-link") + dest = filepath.Join(tmpdir, "destSymlink") + destLinkfile := filepath.Join(dest, "file-1-link") + if err := CopyWithTar(srcLinkfile, destLinkfile); err != nil { + t.Fatal(err) + } + if err := compareFiles(srcLinkfile, destLinkfile); err != nil { + t.Fatal(err) + } +} + +func TestChrootCopyFileWithTar(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "docker-TestChrootCopyFileWithTar") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + src := filepath.Join(tmpdir, "src") + if err := system.MkdirAll(src, 0700); err != nil { + t.Fatal(err) + } + if _, err := prepareSourceDirectory(10, src, true); err != nil { + t.Fatal(err) + } + + // Copy directory + dest := filepath.Join(tmpdir, "dest") + if err := CopyFileWithTar(src, dest); err == nil { + t.Fatal("Expected error on copying directory") + } + + // Copy file + srcfile := filepath.Join(src, "file-1") + dest = filepath.Join(tmpdir, "destFile") + destfile := filepath.Join(dest, "file-1") + if err := CopyFileWithTar(srcfile, destfile); err != nil { + t.Fatal(err) + } + if err := compareFiles(srcfile, destfile); err != nil { + t.Fatal(err) + } + + // Copy symbolic link + srcLinkfile := filepath.Join(src, "file-1-link") + dest = filepath.Join(tmpdir, "destSymlink") + destLinkfile := filepath.Join(dest, "file-1-link") + if err := CopyFileWithTar(srcLinkfile, destLinkfile); err != nil { + t.Fatal(err) + } + if err := compareFiles(srcLinkfile, destLinkfile); err != nil { + t.Fatal(err) + } +} + +func TestChrootUntarPath(t *testing.T) { + // TODO Windows: Figure out why this is failing + if runtime.GOOS == "windows" { + t.Skip("Failing on Windows") + } + tmpdir, err := ioutil.TempDir("", "docker-TestChrootUntarPath") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + src := filepath.Join(tmpdir, "src") + if err := system.MkdirAll(src, 0700); err != nil { + t.Fatal(err) + } + if _, err := prepareSourceDirectory(10, src, true); err != nil { + t.Fatal(err) + } + dest := filepath.Join(tmpdir, "dest") + // Untar a directory + if err := UntarPath(src, dest); err == nil { + t.Fatal("Expected error on untaring a directory") + } + + // Untar a tar file + stream, err := archive.Tar(src, archive.Uncompressed) + if err != nil { + t.Fatal(err) + } + buf := new(bytes.Buffer) + buf.ReadFrom(stream) + tarfile := filepath.Join(tmpdir, "src.tar") + if err := ioutil.WriteFile(tarfile, buf.Bytes(), 0644); err != nil { + t.Fatal(err) + } + if err := UntarPath(tarfile, dest); err != nil { + t.Fatal(err) + } + if err := compareDirectories(src, dest); err != nil { + t.Fatal(err) + } +} + +type slowEmptyTarReader struct { + size int + offset int + chunkSize int +} + +// Read is a slow reader of an empty tar (like the output of "tar c --files-from /dev/null") +func (s *slowEmptyTarReader) Read(p []byte) (int, error) { + time.Sleep(100 * time.Millisecond) + count := s.chunkSize + if len(p) < s.chunkSize { + count = len(p) + } + for i := 0; i < count; i++ { + p[i] = 0 + } + s.offset += count + if s.offset > s.size { + return count, io.EOF + } + return count, nil +} + +func TestChrootUntarEmptyArchiveFromSlowReader(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "docker-TestChrootUntarEmptyArchiveFromSlowReader") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + dest := filepath.Join(tmpdir, "dest") + if err := system.MkdirAll(dest, 0700); err != nil { + t.Fatal(err) + } + stream := &slowEmptyTarReader{size: 10240, chunkSize: 1024} + if err := Untar(stream, dest, nil); err != nil { + t.Fatal(err) + } +} + +func TestChrootApplyEmptyArchiveFromSlowReader(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "docker-TestChrootApplyEmptyArchiveFromSlowReader") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + dest := filepath.Join(tmpdir, "dest") + if err := system.MkdirAll(dest, 0700); err != nil { + t.Fatal(err) + } + stream := &slowEmptyTarReader{size: 10240, chunkSize: 1024} + if _, err := ApplyLayer(dest, stream); err != nil { + t.Fatal(err) + } +} + +func TestChrootApplyDotDotFile(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "docker-TestChrootApplyDotDotFile") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + src := filepath.Join(tmpdir, "src") + if err := system.MkdirAll(src, 0700); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(filepath.Join(src, "..gitme"), []byte(""), 0644); err != nil { + t.Fatal(err) + } + stream, err := archive.Tar(src, archive.Uncompressed) + if err != nil { + t.Fatal(err) + } + dest := filepath.Join(tmpdir, "dest") + if err := system.MkdirAll(dest, 0700); err != nil { + t.Fatal(err) + } + if _, err := ApplyLayer(dest, stream); err != nil { + t.Fatal(err) + } +} diff --git a/pkg/chrootarchive/archive_unix.go b/pkg/chrootarchive/archive_unix.go new file mode 100644 index 00000000..9b268566 --- /dev/null +++ b/pkg/chrootarchive/archive_unix.go @@ -0,0 +1,94 @@ +// +build !windows + +package chrootarchive + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "runtime" + "syscall" + + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/reexec" +) + +func chroot(path string) error { + if err := syscall.Chroot(path); err != nil { + return err + } + return syscall.Chdir("/") +} + +// untar is the entry-point for docker-untar on re-exec. This is not used on +// Windows as it does not support chroot, hence no point sandboxing through +// chroot and rexec. +func untar() { + runtime.LockOSThread() + flag.Parse() + + var options *archive.TarOptions + + //read the options from the pipe "ExtraFiles" + if err := json.NewDecoder(os.NewFile(3, "options")).Decode(&options); err != nil { + fatal(err) + } + + if err := chroot(flag.Arg(0)); err != nil { + fatal(err) + } + + if err := archive.Unpack(os.Stdin, "/", options); err != nil { + fatal(err) + } + // fully consume stdin in case it is zero padded + if _, err := flush(os.Stdin); err != nil { + fatal(err) + } + + os.Exit(0) +} + +func invokeUnpack(decompressedArchive io.Reader, dest string, options *archive.TarOptions) error { + + // We can't pass a potentially large exclude list directly via cmd line + // because we easily overrun the kernel's max argument/environment size + // when the full image list is passed (e.g. when this is used by + // `docker load`). We will marshall the options via a pipe to the + // child + r, w, err := os.Pipe() + if err != nil { + return fmt.Errorf("Untar pipe failure: %v", err) + } + + cmd := reexec.Command("docker-untar", dest) + cmd.Stdin = decompressedArchive + + cmd.ExtraFiles = append(cmd.ExtraFiles, r) + output := bytes.NewBuffer(nil) + cmd.Stdout = output + cmd.Stderr = output + + if err := cmd.Start(); err != nil { + return fmt.Errorf("Untar error on re-exec cmd: %v", err) + } + //write the options to the pipe for the untar exec to read + if err := json.NewEncoder(w).Encode(options); err != nil { + return fmt.Errorf("Untar json encode to pipe failed: %v", err) + } + w.Close() + + if err := cmd.Wait(); err != nil { + // when `xz -d -c -q | docker-untar ...` failed on docker-untar side, + // we need to exhaust `xz`'s output, otherwise the `xz` side will be + // pending on write pipe forever + io.Copy(ioutil.Discard, decompressedArchive) + + return fmt.Errorf("Untar re-exec error: %v: output: %s", err, output) + } + return nil +} diff --git a/pkg/chrootarchive/archive_windows.go b/pkg/chrootarchive/archive_windows.go new file mode 100644 index 00000000..0a500ed5 --- /dev/null +++ b/pkg/chrootarchive/archive_windows.go @@ -0,0 +1,22 @@ +package chrootarchive + +import ( + "io" + + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/longpath" +) + +// chroot is not supported by Windows +func chroot(path string) error { + return nil +} + +func invokeUnpack(decompressedArchive io.ReadCloser, + dest string, + options *archive.TarOptions) error { + // Windows is different to Linux here because Windows does not support + // chroot. Hence there is no point sandboxing a chrooted process to + // do the unpack. We call inline instead within the daemon process. + return archive.Unpack(decompressedArchive, longpath.AddPrefix(dest), options) +} diff --git a/pkg/chrootarchive/diff.go b/pkg/chrootarchive/diff.go new file mode 100644 index 00000000..94131a6e --- /dev/null +++ b/pkg/chrootarchive/diff.go @@ -0,0 +1,19 @@ +package chrootarchive + +import "github.com/docker/docker/pkg/archive" + +// ApplyLayer parses a diff in the standard layer format from `layer`, +// and applies it to the directory `dest`. The stream `layer` can only be +// uncompressed. +// Returns the size in bytes of the contents of the layer. +func ApplyLayer(dest string, layer archive.Reader) (size int64, err error) { + return applyLayerHandler(dest, layer, &archive.TarOptions{}, true) +} + +// ApplyUncompressedLayer parses a diff in the standard layer format from +// `layer`, and applies it to the directory `dest`. The stream `layer` +// can only be uncompressed. +// Returns the size in bytes of the contents of the layer. +func ApplyUncompressedLayer(dest string, layer archive.Reader, options *archive.TarOptions) (int64, error) { + return applyLayerHandler(dest, layer, options, false) +} diff --git a/pkg/chrootarchive/diff_unix.go b/pkg/chrootarchive/diff_unix.go new file mode 100644 index 00000000..a4adb74d --- /dev/null +++ b/pkg/chrootarchive/diff_unix.go @@ -0,0 +1,120 @@ +//+build !windows + +package chrootarchive + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "runtime" + + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/reexec" + "github.com/docker/docker/pkg/system" +) + +type applyLayerResponse struct { + LayerSize int64 `json:"layerSize"` +} + +// applyLayer is the entry-point for docker-applylayer on re-exec. This is not +// used on Windows as it does not support chroot, hence no point sandboxing +// through chroot and rexec. +func applyLayer() { + + var ( + tmpDir = "" + err error + options *archive.TarOptions + ) + runtime.LockOSThread() + flag.Parse() + + if err := chroot(flag.Arg(0)); err != nil { + fatal(err) + } + + // We need to be able to set any perms + oldmask, err := system.Umask(0) + defer system.Umask(oldmask) + if err != nil { + fatal(err) + } + + if err := json.Unmarshal([]byte(os.Getenv("OPT")), &options); err != nil { + fatal(err) + } + + if tmpDir, err = ioutil.TempDir("/", "temp-docker-extract"); err != nil { + fatal(err) + } + + os.Setenv("TMPDIR", tmpDir) + size, err := archive.UnpackLayer("/", os.Stdin, options) + os.RemoveAll(tmpDir) + if err != nil { + fatal(err) + } + + encoder := json.NewEncoder(os.Stdout) + if err := encoder.Encode(applyLayerResponse{size}); err != nil { + fatal(fmt.Errorf("unable to encode layerSize JSON: %s", err)) + } + + if _, err := flush(os.Stdin); err != nil { + fatal(err) + } + + os.Exit(0) +} + +// applyLayerHandler parses a diff in the standard layer format from `layer`, and +// applies it to the directory `dest`. Returns the size in bytes of the +// contents of the layer. +func applyLayerHandler(dest string, layer archive.Reader, options *archive.TarOptions, decompress bool) (size int64, err error) { + dest = filepath.Clean(dest) + if decompress { + decompressed, err := archive.DecompressStream(layer) + if err != nil { + return 0, err + } + defer decompressed.Close() + + layer = decompressed + } + if options == nil { + options = &archive.TarOptions{} + } + if options.ExcludePatterns == nil { + options.ExcludePatterns = []string{} + } + + data, err := json.Marshal(options) + if err != nil { + return 0, fmt.Errorf("ApplyLayer json encode: %v", err) + } + + cmd := reexec.Command("docker-applyLayer", dest) + cmd.Stdin = layer + cmd.Env = append(cmd.Env, fmt.Sprintf("OPT=%s", data)) + + outBuf, errBuf := new(bytes.Buffer), new(bytes.Buffer) + cmd.Stdout, cmd.Stderr = outBuf, errBuf + + if err = cmd.Run(); err != nil { + return 0, fmt.Errorf("ApplyLayer %s stdout: %s stderr: %s", err, outBuf, errBuf) + } + + // Stdout should be a valid JSON struct representing an applyLayerResponse. + response := applyLayerResponse{} + decoder := json.NewDecoder(outBuf) + if err = decoder.Decode(&response); err != nil { + return 0, fmt.Errorf("unable to decode ApplyLayer JSON response: %s", err) + } + + return response.LayerSize, nil +} diff --git a/pkg/chrootarchive/diff_windows.go b/pkg/chrootarchive/diff_windows.go new file mode 100644 index 00000000..8e1830cb --- /dev/null +++ b/pkg/chrootarchive/diff_windows.go @@ -0,0 +1,44 @@ +package chrootarchive + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/longpath" +) + +// applyLayerHandler parses a diff in the standard layer format from `layer`, and +// applies it to the directory `dest`. Returns the size in bytes of the +// contents of the layer. +func applyLayerHandler(dest string, layer archive.Reader, options *archive.TarOptions, decompress bool) (size int64, err error) { + dest = filepath.Clean(dest) + + // Ensure it is a Windows-style volume path + dest = longpath.AddPrefix(dest) + + if decompress { + decompressed, err := archive.DecompressStream(layer) + if err != nil { + return 0, err + } + defer decompressed.Close() + + layer = decompressed + } + + tmpDir, err := ioutil.TempDir(os.Getenv("temp"), "temp-docker-extract") + if err != nil { + return 0, fmt.Errorf("ApplyLayer failed to create temp-docker-extract under %s. %s", dest, err) + } + + s, err := archive.UnpackLayer(dest, layer, nil) + os.RemoveAll(tmpDir) + if err != nil { + return 0, fmt.Errorf("ApplyLayer %s failed UnpackLayer to %s", err, dest) + } + + return s, nil +} diff --git a/pkg/chrootarchive/init_unix.go b/pkg/chrootarchive/init_unix.go new file mode 100644 index 00000000..4f637f17 --- /dev/null +++ b/pkg/chrootarchive/init_unix.go @@ -0,0 +1,28 @@ +// +build !windows + +package chrootarchive + +import ( + "fmt" + "io" + "io/ioutil" + "os" + + "github.com/docker/docker/pkg/reexec" +) + +func init() { + reexec.Register("docker-applyLayer", applyLayer) + reexec.Register("docker-untar", untar) +} + +func fatal(err error) { + fmt.Fprint(os.Stderr, err) + os.Exit(1) +} + +// flush consumes all the bytes from the reader discarding +// any errors +func flush(r io.Reader) (bytes int64, err error) { + return io.Copy(ioutil.Discard, r) +} diff --git a/pkg/chrootarchive/init_windows.go b/pkg/chrootarchive/init_windows.go new file mode 100644 index 00000000..fa17c9bf --- /dev/null +++ b/pkg/chrootarchive/init_windows.go @@ -0,0 +1,4 @@ +package chrootarchive + +func init() { +} diff --git a/pkg/devicemapper/devmapper.go b/pkg/devicemapper/devmapper.go new file mode 100644 index 00000000..db3244ac --- /dev/null +++ b/pkg/devicemapper/devmapper.go @@ -0,0 +1,807 @@ +// +build linux + +package devicemapper + +import ( + "errors" + "fmt" + "os" + "runtime" + "syscall" + "unsafe" + + "github.com/Sirupsen/logrus" +) + +// DevmapperLogger defines methods for logging with devicemapper. +type DevmapperLogger interface { + DMLog(level int, file string, line int, dmError int, message string) +} + +const ( + deviceCreate TaskType = iota + deviceReload + deviceRemove + deviceRemoveAll + deviceSuspend + deviceResume + deviceInfo + deviceDeps + deviceRename + deviceVersion + deviceStatus + deviceTable + deviceWaitevent + deviceList + deviceClear + deviceMknodes + deviceListVersions + deviceTargetMsg + deviceSetGeometry +) + +const ( + addNodeOnResume AddNodeType = iota + addNodeOnCreate +) + +// List of errors returned when using devicemapper. +var ( + ErrTaskRun = errors.New("dm_task_run failed") + ErrTaskSetName = errors.New("dm_task_set_name failed") + ErrTaskSetMessage = errors.New("dm_task_set_message failed") + ErrTaskSetAddNode = errors.New("dm_task_set_add_node failed") + ErrTaskSetRo = errors.New("dm_task_set_ro failed") + ErrTaskAddTarget = errors.New("dm_task_add_target failed") + ErrTaskSetSector = errors.New("dm_task_set_sector failed") + ErrTaskGetDeps = errors.New("dm_task_get_deps failed") + ErrTaskGetInfo = errors.New("dm_task_get_info failed") + ErrTaskGetDriverVersion = errors.New("dm_task_get_driver_version failed") + ErrTaskDeferredRemove = errors.New("dm_task_deferred_remove failed") + ErrTaskSetCookie = errors.New("dm_task_set_cookie failed") + ErrNilCookie = errors.New("cookie ptr can't be nil") + ErrGetBlockSize = errors.New("Can't get block size") + ErrUdevWait = errors.New("wait on udev cookie failed") + ErrSetDevDir = errors.New("dm_set_dev_dir failed") + ErrGetLibraryVersion = errors.New("dm_get_library_version failed") + ErrCreateRemoveTask = errors.New("Can't create task of type deviceRemove") + ErrRunRemoveDevice = errors.New("running RemoveDevice failed") + ErrInvalidAddNode = errors.New("Invalid AddNode type") + ErrBusy = errors.New("Device is Busy") + ErrDeviceIDExists = errors.New("Device Id Exists") + ErrEnxio = errors.New("No such device or address") +) + +var ( + dmSawBusy bool + dmSawExist bool + dmSawEnxio bool // No Such Device or Address +) + +type ( + // Task represents a devicemapper task (like lvcreate, etc.) ; a task is needed for each ioctl + // command to execute. + Task struct { + unmanaged *cdmTask + } + // Deps represents dependents (layer) of a device. + Deps struct { + Count uint32 + Filler uint32 + Device []uint64 + } + // Info represents information about a device. + Info struct { + Exists int + Suspended int + LiveTable int + InactiveTable int + OpenCount int32 + EventNr uint32 + Major uint32 + Minor uint32 + ReadOnly int + TargetCount int32 + DeferredRemove int + } + // TaskType represents a type of task + TaskType int + // AddNodeType represents a type of node to be added + AddNodeType int +) + +// DeviceIDExists returns whether error conveys the information about device Id already +// exist or not. This will be true if device creation or snap creation +// operation fails if device or snap device already exists in pool. +// Current implementation is little crude as it scans the error string +// for exact pattern match. Replacing it with more robust implementation +// is desirable. +func DeviceIDExists(err error) bool { + return fmt.Sprint(err) == fmt.Sprint(ErrDeviceIDExists) +} + +func (t *Task) destroy() { + if t != nil { + DmTaskDestroy(t.unmanaged) + runtime.SetFinalizer(t, nil) + } +} + +// TaskCreateNamed is a convenience function for TaskCreate when a name +// will be set on the task as well +func TaskCreateNamed(t TaskType, name string) (*Task, error) { + task := TaskCreate(t) + if task == nil { + return nil, fmt.Errorf("devicemapper: Can't create task of type %d", int(t)) + } + if err := task.setName(name); err != nil { + return nil, fmt.Errorf("devicemapper: Can't set task name %s", name) + } + return task, nil +} + +// TaskCreate initializes a devicemapper task of tasktype +func TaskCreate(tasktype TaskType) *Task { + Ctask := DmTaskCreate(int(tasktype)) + if Ctask == nil { + return nil + } + task := &Task{unmanaged: Ctask} + runtime.SetFinalizer(task, (*Task).destroy) + return task +} + +func (t *Task) run() error { + if res := DmTaskRun(t.unmanaged); res != 1 { + return ErrTaskRun + } + return nil +} + +func (t *Task) setName(name string) error { + if res := DmTaskSetName(t.unmanaged, name); res != 1 { + return ErrTaskSetName + } + return nil +} + +func (t *Task) setMessage(message string) error { + if res := DmTaskSetMessage(t.unmanaged, message); res != 1 { + return ErrTaskSetMessage + } + return nil +} + +func (t *Task) setSector(sector uint64) error { + if res := DmTaskSetSector(t.unmanaged, sector); res != 1 { + return ErrTaskSetSector + } + return nil +} + +func (t *Task) setCookie(cookie *uint, flags uint16) error { + if cookie == nil { + return ErrNilCookie + } + if res := DmTaskSetCookie(t.unmanaged, cookie, flags); res != 1 { + return ErrTaskSetCookie + } + return nil +} + +func (t *Task) setAddNode(addNode AddNodeType) error { + if addNode != addNodeOnResume && addNode != addNodeOnCreate { + return ErrInvalidAddNode + } + if res := DmTaskSetAddNode(t.unmanaged, addNode); res != 1 { + return ErrTaskSetAddNode + } + return nil +} + +func (t *Task) setRo() error { + if res := DmTaskSetRo(t.unmanaged); res != 1 { + return ErrTaskSetRo + } + return nil +} + +func (t *Task) addTarget(start, size uint64, ttype, params string) error { + if res := DmTaskAddTarget(t.unmanaged, start, size, + ttype, params); res != 1 { + return ErrTaskAddTarget + } + return nil +} + +func (t *Task) getDeps() (*Deps, error) { + var deps *Deps + if deps = DmTaskGetDeps(t.unmanaged); deps == nil { + return nil, ErrTaskGetDeps + } + return deps, nil +} + +func (t *Task) getInfo() (*Info, error) { + info := &Info{} + if res := DmTaskGetInfo(t.unmanaged, info); res != 1 { + return nil, ErrTaskGetInfo + } + return info, nil +} + +func (t *Task) getInfoWithDeferred() (*Info, error) { + info := &Info{} + if res := DmTaskGetInfoWithDeferred(t.unmanaged, info); res != 1 { + return nil, ErrTaskGetInfo + } + return info, nil +} + +func (t *Task) getDriverVersion() (string, error) { + res := DmTaskGetDriverVersion(t.unmanaged) + if res == "" { + return "", ErrTaskGetDriverVersion + } + return res, nil +} + +func (t *Task) getNextTarget(next unsafe.Pointer) (nextPtr unsafe.Pointer, start uint64, + length uint64, targetType string, params string) { + + return DmGetNextTarget(t.unmanaged, next, &start, &length, + &targetType, ¶ms), + start, length, targetType, params +} + +// UdevWait waits for any processes that are waiting for udev to complete the specified cookie. +func UdevWait(cookie *uint) error { + if res := DmUdevWait(*cookie); res != 1 { + logrus.Debugf("devicemapper: Failed to wait on udev cookie %d", *cookie) + return ErrUdevWait + } + return nil +} + +// LogInitVerbose is an interface to initialize the verbose logger for the device mapper library. +func LogInitVerbose(level int) { + DmLogInitVerbose(level) +} + +var dmLogger DevmapperLogger + +// LogInit initializes the logger for the device mapper library. +func LogInit(logger DevmapperLogger) { + dmLogger = logger + LogWithErrnoInit() +} + +// SetDevDir sets the dev folder for the device mapper library (usually /dev). +func SetDevDir(dir string) error { + if res := DmSetDevDir(dir); res != 1 { + logrus.Debugf("devicemapper: Error dm_set_dev_dir") + return ErrSetDevDir + } + return nil +} + +// GetLibraryVersion returns the device mapper library version. +func GetLibraryVersion() (string, error) { + var version string + if res := DmGetLibraryVersion(&version); res != 1 { + return "", ErrGetLibraryVersion + } + return version, nil +} + +// UdevSyncSupported returns whether device-mapper is able to sync with udev +// +// This is essential otherwise race conditions can arise where both udev and +// device-mapper attempt to create and destroy devices. +func UdevSyncSupported() bool { + return DmUdevGetSyncSupport() != 0 +} + +// UdevSetSyncSupport allows setting whether the udev sync should be enabled. +// The return bool indicates the state of whether the sync is enabled. +func UdevSetSyncSupport(enable bool) bool { + if enable { + DmUdevSetSyncSupport(1) + } else { + DmUdevSetSyncSupport(0) + } + + return UdevSyncSupported() +} + +// CookieSupported returns whether the version of device-mapper supports the +// use of cookie's in the tasks. +// This is largely a lower level call that other functions use. +func CookieSupported() bool { + return DmCookieSupported() != 0 +} + +// RemoveDevice is a useful helper for cleaning up a device. +func RemoveDevice(name string) error { + task, err := TaskCreateNamed(deviceRemove, name) + if task == nil { + return err + } + + var cookie uint + if err := task.setCookie(&cookie, 0); err != nil { + return fmt.Errorf("devicemapper: Can not set cookie: %s", err) + } + defer UdevWait(&cookie) + + dmSawBusy = false // reset before the task is run + if err = task.run(); err != nil { + if dmSawBusy { + return ErrBusy + } + return fmt.Errorf("devicemapper: Error running RemoveDevice %s", err) + } + + return nil +} + +// RemoveDeviceDeferred is a useful helper for cleaning up a device, but deferred. +func RemoveDeviceDeferred(name string) error { + logrus.Debugf("devicemapper: RemoveDeviceDeferred START(%s)", name) + defer logrus.Debugf("devicemapper: RemoveDeviceDeferred END(%s)", name) + task, err := TaskCreateNamed(deviceRemove, name) + if task == nil { + return err + } + + if err := DmTaskDeferredRemove(task.unmanaged); err != 1 { + return ErrTaskDeferredRemove + } + + if err = task.run(); err != nil { + return fmt.Errorf("devicemapper: Error running RemoveDeviceDeferred %s", err) + } + + return nil +} + +// CancelDeferredRemove cancels a deferred remove for a device. +func CancelDeferredRemove(deviceName string) error { + task, err := TaskCreateNamed(deviceTargetMsg, deviceName) + if task == nil { + return err + } + + if err := task.setSector(0); err != nil { + return fmt.Errorf("devicemapper: Can't set sector %s", err) + } + + if err := task.setMessage(fmt.Sprintf("@cancel_deferred_remove")); err != nil { + return fmt.Errorf("devicemapper: Can't set message %s", err) + } + + dmSawBusy = false + dmSawEnxio = false + if err := task.run(); err != nil { + // A device might be being deleted already + if dmSawBusy { + return ErrBusy + } else if dmSawEnxio { + return ErrEnxio + } + return fmt.Errorf("devicemapper: Error running CancelDeferredRemove %s", err) + + } + return nil +} + +// GetBlockDeviceSize returns the size of a block device identified by the specified file. +func GetBlockDeviceSize(file *os.File) (uint64, error) { + size, err := ioctlBlkGetSize64(file.Fd()) + if err != nil { + logrus.Errorf("devicemapper: Error getblockdevicesize: %s", err) + return 0, ErrGetBlockSize + } + return uint64(size), nil +} + +// BlockDeviceDiscard runs discard for the given path. +// This is used as a workaround for the kernel not discarding block so +// on the thin pool when we remove a thinp device, so we do it +// manually +func BlockDeviceDiscard(path string) error { + file, err := os.OpenFile(path, os.O_RDWR, 0) + if err != nil { + return err + } + defer file.Close() + + size, err := GetBlockDeviceSize(file) + if err != nil { + return err + } + + if err := ioctlBlkDiscard(file.Fd(), 0, size); err != nil { + return err + } + + // Without this sometimes the remove of the device that happens after + // discard fails with EBUSY. + syscall.Sync() + + return nil +} + +// CreatePool is the programmatic example of "dmsetup create". +// It creates a device with the specified poolName, data and metadata file and block size. +func CreatePool(poolName string, dataFile, metadataFile *os.File, poolBlockSize uint32) error { + task, err := TaskCreateNamed(deviceCreate, poolName) + if task == nil { + return err + } + + size, err := GetBlockDeviceSize(dataFile) + if err != nil { + return fmt.Errorf("devicemapper: Can't get data size %s", err) + } + + params := fmt.Sprintf("%s %s %d 32768 1 skip_block_zeroing", metadataFile.Name(), dataFile.Name(), poolBlockSize) + if err := task.addTarget(0, size/512, "thin-pool", params); err != nil { + return fmt.Errorf("devicemapper: Can't add target %s", err) + } + + var cookie uint + var flags uint16 + flags = DmUdevDisableSubsystemRulesFlag | DmUdevDisableDiskRulesFlag | DmUdevDisableOtherRulesFlag + if err := task.setCookie(&cookie, flags); err != nil { + return fmt.Errorf("devicemapper: Can't set cookie %s", err) + } + defer UdevWait(&cookie) + + if err := task.run(); err != nil { + return fmt.Errorf("devicemapper: Error running deviceCreate (CreatePool) %s", err) + } + + return nil +} + +// ReloadPool is the programmatic example of "dmsetup reload". +// It reloads the table with the specified poolName, data and metadata file and block size. +func ReloadPool(poolName string, dataFile, metadataFile *os.File, poolBlockSize uint32) error { + task, err := TaskCreateNamed(deviceReload, poolName) + if task == nil { + return err + } + + size, err := GetBlockDeviceSize(dataFile) + if err != nil { + return fmt.Errorf("devicemapper: Can't get data size %s", err) + } + + params := fmt.Sprintf("%s %s %d 32768 1 skip_block_zeroing", metadataFile.Name(), dataFile.Name(), poolBlockSize) + if err := task.addTarget(0, size/512, "thin-pool", params); err != nil { + return fmt.Errorf("devicemapper: Can't add target %s", err) + } + + if err := task.run(); err != nil { + return fmt.Errorf("devicemapper: Error running deviceCreate %s", err) + } + + return nil +} + +// GetDeps is the programmatic example of "dmsetup deps". +// It outputs a list of devices referenced by the live table for the specified device. +func GetDeps(name string) (*Deps, error) { + task, err := TaskCreateNamed(deviceDeps, name) + if task == nil { + return nil, err + } + if err := task.run(); err != nil { + return nil, err + } + return task.getDeps() +} + +// GetInfo is the programmatic example of "dmsetup info". +// It outputs some brief information about the device. +func GetInfo(name string) (*Info, error) { + task, err := TaskCreateNamed(deviceInfo, name) + if task == nil { + return nil, err + } + if err := task.run(); err != nil { + return nil, err + } + return task.getInfo() +} + +// GetInfoWithDeferred is the programmatic example of "dmsetup info", but deferred. +// It outputs some brief information about the device. +func GetInfoWithDeferred(name string) (*Info, error) { + task, err := TaskCreateNamed(deviceInfo, name) + if task == nil { + return nil, err + } + if err := task.run(); err != nil { + return nil, err + } + return task.getInfoWithDeferred() +} + +// GetDriverVersion is the programmatic example of "dmsetup version". +// It outputs version information of the driver. +func GetDriverVersion() (string, error) { + task := TaskCreate(deviceVersion) + if task == nil { + return "", fmt.Errorf("devicemapper: Can't create deviceVersion task") + } + if err := task.run(); err != nil { + return "", err + } + return task.getDriverVersion() +} + +// GetStatus is the programmatic example of "dmsetup status". +// It outputs status information for the specified device name. +func GetStatus(name string) (uint64, uint64, string, string, error) { + task, err := TaskCreateNamed(deviceStatus, name) + if task == nil { + logrus.Debugf("devicemapper: GetStatus() Error TaskCreateNamed: %s", err) + return 0, 0, "", "", err + } + if err := task.run(); err != nil { + logrus.Debugf("devicemapper: GetStatus() Error Run: %s", err) + return 0, 0, "", "", err + } + + devinfo, err := task.getInfo() + if err != nil { + logrus.Debugf("devicemapper: GetStatus() Error GetInfo: %s", err) + return 0, 0, "", "", err + } + if devinfo.Exists == 0 { + logrus.Debugf("devicemapper: GetStatus() Non existing device %s", name) + return 0, 0, "", "", fmt.Errorf("devicemapper: Non existing device %s", name) + } + + _, start, length, targetType, params := task.getNextTarget(unsafe.Pointer(nil)) + return start, length, targetType, params, nil +} + +// GetTable is the programmatic example for "dmsetup table". +// It outputs the current table for the specified device name. +func GetTable(name string) (uint64, uint64, string, string, error) { + task, err := TaskCreateNamed(deviceTable, name) + if task == nil { + logrus.Debugf("devicemapper: GetTable() Error TaskCreateNamed: %s", err) + return 0, 0, "", "", err + } + if err := task.run(); err != nil { + logrus.Debugf("devicemapper: GetTable() Error Run: %s", err) + return 0, 0, "", "", err + } + + devinfo, err := task.getInfo() + if err != nil { + logrus.Debugf("devicemapper: GetTable() Error GetInfo: %s", err) + return 0, 0, "", "", err + } + if devinfo.Exists == 0 { + logrus.Debugf("devicemapper: GetTable() Non existing device %s", name) + return 0, 0, "", "", fmt.Errorf("devicemapper: Non existing device %s", name) + } + + _, start, length, targetType, params := task.getNextTarget(unsafe.Pointer(nil)) + return start, length, targetType, params, nil +} + +// SetTransactionID sets a transaction id for the specified device name. +func SetTransactionID(poolName string, oldID uint64, newID uint64) error { + task, err := TaskCreateNamed(deviceTargetMsg, poolName) + if task == nil { + return err + } + + if err := task.setSector(0); err != nil { + return fmt.Errorf("devicemapper: Can't set sector %s", err) + } + + if err := task.setMessage(fmt.Sprintf("set_transaction_id %d %d", oldID, newID)); err != nil { + return fmt.Errorf("devicemapper: Can't set message %s", err) + } + + if err := task.run(); err != nil { + return fmt.Errorf("devicemapper: Error running SetTransactionID %s", err) + } + return nil +} + +// SuspendDevice is the programmatic example of "dmsetup suspend". +// It suspends the specified device. +func SuspendDevice(name string) error { + task, err := TaskCreateNamed(deviceSuspend, name) + if task == nil { + return err + } + if err := task.run(); err != nil { + return fmt.Errorf("devicemapper: Error running deviceSuspend %s", err) + } + return nil +} + +// ResumeDevice is the programmatic example of "dmsetup resume". +// It un-suspends the specified device. +func ResumeDevice(name string) error { + task, err := TaskCreateNamed(deviceResume, name) + if task == nil { + return err + } + + var cookie uint + if err := task.setCookie(&cookie, 0); err != nil { + return fmt.Errorf("devicemapper: Can't set cookie %s", err) + } + defer UdevWait(&cookie) + + if err := task.run(); err != nil { + return fmt.Errorf("devicemapper: Error running deviceResume %s", err) + } + + return nil +} + +// CreateDevice creates a device with the specified poolName with the specified device id. +func CreateDevice(poolName string, deviceID int) error { + logrus.Debugf("devicemapper: CreateDevice(poolName=%v, deviceID=%v)", poolName, deviceID) + task, err := TaskCreateNamed(deviceTargetMsg, poolName) + if task == nil { + return err + } + + if err := task.setSector(0); err != nil { + return fmt.Errorf("devicemapper: Can't set sector %s", err) + } + + if err := task.setMessage(fmt.Sprintf("create_thin %d", deviceID)); err != nil { + return fmt.Errorf("devicemapper: Can't set message %s", err) + } + + dmSawExist = false // reset before the task is run + if err := task.run(); err != nil { + // Caller wants to know about ErrDeviceIDExists so that it can try with a different device id. + if dmSawExist { + return ErrDeviceIDExists + } + + return fmt.Errorf("devicemapper: Error running CreateDevice %s", err) + + } + return nil +} + +// DeleteDevice deletes a device with the specified poolName with the specified device id. +func DeleteDevice(poolName string, deviceID int) error { + task, err := TaskCreateNamed(deviceTargetMsg, poolName) + if task == nil { + return err + } + + if err := task.setSector(0); err != nil { + return fmt.Errorf("devicemapper: Can't set sector %s", err) + } + + if err := task.setMessage(fmt.Sprintf("delete %d", deviceID)); err != nil { + return fmt.Errorf("devicemapper: Can't set message %s", err) + } + + dmSawBusy = false + if err := task.run(); err != nil { + if dmSawBusy { + return ErrBusy + } + return fmt.Errorf("devicemapper: Error running DeleteDevice %s", err) + } + return nil +} + +// ActivateDevice activates the device identified by the specified +// poolName, name and deviceID with the specified size. +func ActivateDevice(poolName string, name string, deviceID int, size uint64) error { + return activateDevice(poolName, name, deviceID, size, "") +} + +// ActivateDeviceWithExternal activates the device identified by the specified +// poolName, name and deviceID with the specified size. +func ActivateDeviceWithExternal(poolName string, name string, deviceID int, size uint64, external string) error { + return activateDevice(poolName, name, deviceID, size, external) +} + +func activateDevice(poolName string, name string, deviceID int, size uint64, external string) error { + task, err := TaskCreateNamed(deviceCreate, name) + if task == nil { + return err + } + + var params string + if len(external) > 0 { + params = fmt.Sprintf("%s %d %s", poolName, deviceID, external) + } else { + params = fmt.Sprintf("%s %d", poolName, deviceID) + } + if err := task.addTarget(0, size/512, "thin", params); err != nil { + return fmt.Errorf("devicemapper: Can't add target %s", err) + } + if err := task.setAddNode(addNodeOnCreate); err != nil { + return fmt.Errorf("devicemapper: Can't add node %s", err) + } + + var cookie uint + if err := task.setCookie(&cookie, 0); err != nil { + return fmt.Errorf("devicemapper: Can't set cookie %s", err) + } + + defer UdevWait(&cookie) + + if err := task.run(); err != nil { + return fmt.Errorf("devicemapper: Error running deviceCreate (ActivateDevice) %s", err) + } + + return nil +} + +// CreateSnapDevice creates a snapshot based on the device identified by the baseName and baseDeviceId, +func CreateSnapDevice(poolName string, deviceID int, baseName string, baseDeviceID int) error { + devinfo, _ := GetInfo(baseName) + doSuspend := devinfo != nil && devinfo.Exists != 0 + + if doSuspend { + if err := SuspendDevice(baseName); err != nil { + return err + } + } + + task, err := TaskCreateNamed(deviceTargetMsg, poolName) + if task == nil { + if doSuspend { + ResumeDevice(baseName) + } + return err + } + + if err := task.setSector(0); err != nil { + if doSuspend { + ResumeDevice(baseName) + } + return fmt.Errorf("devicemapper: Can't set sector %s", err) + } + + if err := task.setMessage(fmt.Sprintf("create_snap %d %d", deviceID, baseDeviceID)); err != nil { + if doSuspend { + ResumeDevice(baseName) + } + return fmt.Errorf("devicemapper: Can't set message %s", err) + } + + dmSawExist = false // reset before the task is run + if err := task.run(); err != nil { + if doSuspend { + ResumeDevice(baseName) + } + // Caller wants to know about ErrDeviceIDExists so that it can try with a different device id. + if dmSawExist { + return ErrDeviceIDExists + } + + return fmt.Errorf("devicemapper: Error running deviceCreate (createSnapDevice) %s", err) + + } + + if doSuspend { + if err := ResumeDevice(baseName); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/devicemapper/devmapper_log.go b/pkg/devicemapper/devmapper_log.go new file mode 100644 index 00000000..8477e36f --- /dev/null +++ b/pkg/devicemapper/devmapper_log.go @@ -0,0 +1,35 @@ +// +build linux + +package devicemapper + +import "C" + +import ( + "strings" +) + +// Due to the way cgo works this has to be in a separate file, as devmapper.go has +// definitions in the cgo block, which is incompatible with using "//export" + +// DevmapperLogCallback exports the devmapper log callback for cgo. +//export DevmapperLogCallback +func DevmapperLogCallback(level C.int, file *C.char, line C.int, dmErrnoOrClass C.int, message *C.char) { + msg := C.GoString(message) + if level < 7 { + if strings.Contains(msg, "busy") { + dmSawBusy = true + } + + if strings.Contains(msg, "File exists") { + dmSawExist = true + } + + if strings.Contains(msg, "No such device or address") { + dmSawEnxio = true + } + } + + if dmLogger != nil { + dmLogger.DMLog(int(level), C.GoString(file), int(line), int(dmErrnoOrClass), msg) + } +} diff --git a/pkg/devicemapper/devmapper_wrapper.go b/pkg/devicemapper/devmapper_wrapper.go new file mode 100644 index 00000000..91fbc85b --- /dev/null +++ b/pkg/devicemapper/devmapper_wrapper.go @@ -0,0 +1,251 @@ +// +build linux + +package devicemapper + +/* +#cgo LDFLAGS: -L. -ldevmapper +#include +#include // FIXME: present only for BLKGETSIZE64, maybe we can remove it? + +// FIXME: Can't we find a way to do the logging in pure Go? +extern void DevmapperLogCallback(int level, char *file, int line, int dm_errno_or_class, char *str); + +static void log_cb(int level, const char *file, int line, int dm_errno_or_class, const char *f, ...) +{ + char buffer[256]; + va_list ap; + + va_start(ap, f); + vsnprintf(buffer, 256, f, ap); + va_end(ap); + + DevmapperLogCallback(level, (char *)file, line, dm_errno_or_class, buffer); +} + +static void log_with_errno_init() +{ + dm_log_with_errno_init(log_cb); +} +*/ +import "C" + +import ( + "reflect" + "unsafe" +) + +type ( + cdmTask C.struct_dm_task +) + +// IOCTL consts +const ( + BlkGetSize64 = C.BLKGETSIZE64 + BlkDiscard = C.BLKDISCARD +) + +// Devicemapper cookie flags. +const ( + DmUdevDisableSubsystemRulesFlag = C.DM_UDEV_DISABLE_SUBSYSTEM_RULES_FLAG + DmUdevDisableDiskRulesFlag = C.DM_UDEV_DISABLE_DISK_RULES_FLAG + DmUdevDisableOtherRulesFlag = C.DM_UDEV_DISABLE_OTHER_RULES_FLAG + DmUdevDisableLibraryFallback = C.DM_UDEV_DISABLE_LIBRARY_FALLBACK +) + +// DeviceMapper mapped functions. +var ( + DmGetLibraryVersion = dmGetLibraryVersionFct + DmGetNextTarget = dmGetNextTargetFct + DmLogInitVerbose = dmLogInitVerboseFct + DmSetDevDir = dmSetDevDirFct + DmTaskAddTarget = dmTaskAddTargetFct + DmTaskCreate = dmTaskCreateFct + DmTaskDestroy = dmTaskDestroyFct + DmTaskGetDeps = dmTaskGetDepsFct + DmTaskGetInfo = dmTaskGetInfoFct + DmTaskGetDriverVersion = dmTaskGetDriverVersionFct + DmTaskRun = dmTaskRunFct + DmTaskSetAddNode = dmTaskSetAddNodeFct + DmTaskSetCookie = dmTaskSetCookieFct + DmTaskSetMessage = dmTaskSetMessageFct + DmTaskSetName = dmTaskSetNameFct + DmTaskSetRo = dmTaskSetRoFct + DmTaskSetSector = dmTaskSetSectorFct + DmUdevWait = dmUdevWaitFct + DmUdevSetSyncSupport = dmUdevSetSyncSupportFct + DmUdevGetSyncSupport = dmUdevGetSyncSupportFct + DmCookieSupported = dmCookieSupportedFct + LogWithErrnoInit = logWithErrnoInitFct + DmTaskDeferredRemove = dmTaskDeferredRemoveFct + DmTaskGetInfoWithDeferred = dmTaskGetInfoWithDeferredFct +) + +func free(p *C.char) { + C.free(unsafe.Pointer(p)) +} + +func dmTaskDestroyFct(task *cdmTask) { + C.dm_task_destroy((*C.struct_dm_task)(task)) +} + +func dmTaskCreateFct(taskType int) *cdmTask { + return (*cdmTask)(C.dm_task_create(C.int(taskType))) +} + +func dmTaskRunFct(task *cdmTask) int { + ret, _ := C.dm_task_run((*C.struct_dm_task)(task)) + return int(ret) +} + +func dmTaskSetNameFct(task *cdmTask, name string) int { + Cname := C.CString(name) + defer free(Cname) + + return int(C.dm_task_set_name((*C.struct_dm_task)(task), Cname)) +} + +func dmTaskSetMessageFct(task *cdmTask, message string) int { + Cmessage := C.CString(message) + defer free(Cmessage) + + return int(C.dm_task_set_message((*C.struct_dm_task)(task), Cmessage)) +} + +func dmTaskSetSectorFct(task *cdmTask, sector uint64) int { + return int(C.dm_task_set_sector((*C.struct_dm_task)(task), C.uint64_t(sector))) +} + +func dmTaskSetCookieFct(task *cdmTask, cookie *uint, flags uint16) int { + cCookie := C.uint32_t(*cookie) + defer func() { + *cookie = uint(cCookie) + }() + return int(C.dm_task_set_cookie((*C.struct_dm_task)(task), &cCookie, C.uint16_t(flags))) +} + +func dmTaskSetAddNodeFct(task *cdmTask, addNode AddNodeType) int { + return int(C.dm_task_set_add_node((*C.struct_dm_task)(task), C.dm_add_node_t(addNode))) +} + +func dmTaskSetRoFct(task *cdmTask) int { + return int(C.dm_task_set_ro((*C.struct_dm_task)(task))) +} + +func dmTaskAddTargetFct(task *cdmTask, + start, size uint64, ttype, params string) int { + + Cttype := C.CString(ttype) + defer free(Cttype) + + Cparams := C.CString(params) + defer free(Cparams) + + return int(C.dm_task_add_target((*C.struct_dm_task)(task), C.uint64_t(start), C.uint64_t(size), Cttype, Cparams)) +} + +func dmTaskGetDepsFct(task *cdmTask) *Deps { + Cdeps := C.dm_task_get_deps((*C.struct_dm_task)(task)) + if Cdeps == nil { + return nil + } + + // golang issue: https://github.com/golang/go/issues/11925 + hdr := reflect.SliceHeader{ + Data: uintptr(unsafe.Pointer(uintptr(unsafe.Pointer(Cdeps)) + unsafe.Sizeof(*Cdeps))), + Len: int(Cdeps.count), + Cap: int(Cdeps.count), + } + devices := *(*[]C.uint64_t)(unsafe.Pointer(&hdr)) + + deps := &Deps{ + Count: uint32(Cdeps.count), + Filler: uint32(Cdeps.filler), + } + for _, device := range devices { + deps.Device = append(deps.Device, uint64(device)) + } + return deps +} + +func dmTaskGetInfoFct(task *cdmTask, info *Info) int { + Cinfo := C.struct_dm_info{} + defer func() { + info.Exists = int(Cinfo.exists) + info.Suspended = int(Cinfo.suspended) + info.LiveTable = int(Cinfo.live_table) + info.InactiveTable = int(Cinfo.inactive_table) + info.OpenCount = int32(Cinfo.open_count) + info.EventNr = uint32(Cinfo.event_nr) + info.Major = uint32(Cinfo.major) + info.Minor = uint32(Cinfo.minor) + info.ReadOnly = int(Cinfo.read_only) + info.TargetCount = int32(Cinfo.target_count) + }() + return int(C.dm_task_get_info((*C.struct_dm_task)(task), &Cinfo)) +} + +func dmTaskGetDriverVersionFct(task *cdmTask) string { + buffer := C.malloc(128) + defer C.free(buffer) + res := C.dm_task_get_driver_version((*C.struct_dm_task)(task), (*C.char)(buffer), 128) + if res == 0 { + return "" + } + return C.GoString((*C.char)(buffer)) +} + +func dmGetNextTargetFct(task *cdmTask, next unsafe.Pointer, start, length *uint64, target, params *string) unsafe.Pointer { + var ( + Cstart, Clength C.uint64_t + CtargetType, Cparams *C.char + ) + defer func() { + *start = uint64(Cstart) + *length = uint64(Clength) + *target = C.GoString(CtargetType) + *params = C.GoString(Cparams) + }() + + nextp := C.dm_get_next_target((*C.struct_dm_task)(task), next, &Cstart, &Clength, &CtargetType, &Cparams) + return nextp +} + +func dmUdevSetSyncSupportFct(syncWithUdev int) { + (C.dm_udev_set_sync_support(C.int(syncWithUdev))) +} + +func dmUdevGetSyncSupportFct() int { + return int(C.dm_udev_get_sync_support()) +} + +func dmUdevWaitFct(cookie uint) int { + return int(C.dm_udev_wait(C.uint32_t(cookie))) +} + +func dmCookieSupportedFct() int { + return int(C.dm_cookie_supported()) +} + +func dmLogInitVerboseFct(level int) { + C.dm_log_init_verbose(C.int(level)) +} + +func logWithErrnoInitFct() { + C.log_with_errno_init() +} + +func dmSetDevDirFct(dir string) int { + Cdir := C.CString(dir) + defer free(Cdir) + + return int(C.dm_set_dev_dir(Cdir)) +} + +func dmGetLibraryVersionFct(version *string) int { + buffer := C.CString(string(make([]byte, 128))) + defer free(buffer) + defer func() { + *version = C.GoString(buffer) + }() + return int(C.dm_get_library_version(buffer, 128)) +} diff --git a/pkg/devicemapper/devmapper_wrapper_deferred_remove.go b/pkg/devicemapper/devmapper_wrapper_deferred_remove.go new file mode 100644 index 00000000..dc361eab --- /dev/null +++ b/pkg/devicemapper/devmapper_wrapper_deferred_remove.go @@ -0,0 +1,34 @@ +// +build linux,!libdm_no_deferred_remove + +package devicemapper + +/* +#cgo LDFLAGS: -L. -ldevmapper +#include +*/ +import "C" + +// LibraryDeferredRemovalSupport is supported when statically linked. +const LibraryDeferredRemovalSupport = true + +func dmTaskDeferredRemoveFct(task *cdmTask) int { + return int(C.dm_task_deferred_remove((*C.struct_dm_task)(task))) +} + +func dmTaskGetInfoWithDeferredFct(task *cdmTask, info *Info) int { + Cinfo := C.struct_dm_info{} + defer func() { + info.Exists = int(Cinfo.exists) + info.Suspended = int(Cinfo.suspended) + info.LiveTable = int(Cinfo.live_table) + info.InactiveTable = int(Cinfo.inactive_table) + info.OpenCount = int32(Cinfo.open_count) + info.EventNr = uint32(Cinfo.event_nr) + info.Major = uint32(Cinfo.major) + info.Minor = uint32(Cinfo.minor) + info.ReadOnly = int(Cinfo.read_only) + info.TargetCount = int32(Cinfo.target_count) + info.DeferredRemove = int(Cinfo.deferred_remove) + }() + return int(C.dm_task_get_info((*C.struct_dm_task)(task), &Cinfo)) +} diff --git a/pkg/devicemapper/devmapper_wrapper_no_deferred_remove.go b/pkg/devicemapper/devmapper_wrapper_no_deferred_remove.go new file mode 100644 index 00000000..4a6665de --- /dev/null +++ b/pkg/devicemapper/devmapper_wrapper_no_deferred_remove.go @@ -0,0 +1,15 @@ +// +build linux,libdm_no_deferred_remove + +package devicemapper + +// LibraryDeferredRemovalsupport is not supported when statically linked. +const LibraryDeferredRemovalSupport = false + +func dmTaskDeferredRemoveFct(task *cdmTask) int { + // Error. Nobody should be calling it. + return -1 +} + +func dmTaskGetInfoWithDeferredFct(task *cdmTask, info *Info) int { + return -1 +} diff --git a/pkg/devicemapper/ioctl.go b/pkg/devicemapper/ioctl.go new file mode 100644 index 00000000..581b57eb --- /dev/null +++ b/pkg/devicemapper/ioctl.go @@ -0,0 +1,27 @@ +// +build linux + +package devicemapper + +import ( + "syscall" + "unsafe" +) + +func ioctlBlkGetSize64(fd uintptr) (int64, error) { + var size int64 + if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, BlkGetSize64, uintptr(unsafe.Pointer(&size))); err != 0 { + return 0, err + } + return size, nil +} + +func ioctlBlkDiscard(fd uintptr, offset, length uint64) error { + var r [2]uint64 + r[0] = offset + r[1] = length + + if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, BlkDiscard, uintptr(unsafe.Pointer(&r[0]))); err != 0 { + return err + } + return nil +} diff --git a/pkg/devicemapper/log.go b/pkg/devicemapper/log.go new file mode 100644 index 00000000..cee5e545 --- /dev/null +++ b/pkg/devicemapper/log.go @@ -0,0 +1,11 @@ +package devicemapper + +// definitions from lvm2 lib/log/log.h +const ( + LogLevelFatal = 2 + iota // _LOG_FATAL + LogLevelErr // _LOG_ERR + LogLevelWarn // _LOG_WARN + LogLevelNotice // _LOG_NOTICE + LogLevelInfo // _LOG_INFO + LogLevelDebug // _LOG_DEBUG +) diff --git a/pkg/directory/directory.go b/pkg/directory/directory.go new file mode 100644 index 00000000..1715ef45 --- /dev/null +++ b/pkg/directory/directory.go @@ -0,0 +1,26 @@ +package directory + +import ( + "io/ioutil" + "os" + "path/filepath" +) + +// MoveToSubdir moves all contents of a directory to a subdirectory underneath the original path +func MoveToSubdir(oldpath, subdir string) error { + + infos, err := ioutil.ReadDir(oldpath) + if err != nil { + return err + } + for _, info := range infos { + if info.Name() != subdir { + oldName := filepath.Join(oldpath, info.Name()) + newName := filepath.Join(oldpath, subdir, info.Name()) + if err := os.Rename(oldName, newName); err != nil { + return err + } + } + } + return nil +} diff --git a/pkg/directory/directory_test.go b/pkg/directory/directory_test.go new file mode 100644 index 00000000..46110628 --- /dev/null +++ b/pkg/directory/directory_test.go @@ -0,0 +1,185 @@ +package directory + +import ( + "io/ioutil" + "os" + "path/filepath" + "reflect" + "sort" + "testing" +) + +// Size of an empty directory should be 0 +func TestSizeEmpty(t *testing.T) { + var dir string + var err error + if dir, err = ioutil.TempDir(os.TempDir(), "testSizeEmptyDirectory"); err != nil { + t.Fatalf("failed to create directory: %s", err) + } + + var size int64 + if size, _ = Size(dir); size != 0 { + t.Fatalf("empty directory has size: %d", size) + } +} + +// Size of a directory with one empty file should be 0 +func TestSizeEmptyFile(t *testing.T) { + var dir string + var err error + if dir, err = ioutil.TempDir(os.TempDir(), "testSizeEmptyFile"); err != nil { + t.Fatalf("failed to create directory: %s", err) + } + + var file *os.File + if file, err = ioutil.TempFile(dir, "file"); err != nil { + t.Fatalf("failed to create file: %s", err) + } + + var size int64 + if size, _ = Size(file.Name()); size != 0 { + t.Fatalf("directory with one file has size: %d", size) + } +} + +// Size of a directory with one 5-byte file should be 5 +func TestSizeNonemptyFile(t *testing.T) { + var dir string + var err error + if dir, err = ioutil.TempDir(os.TempDir(), "testSizeNonemptyFile"); err != nil { + t.Fatalf("failed to create directory: %s", err) + } + + var file *os.File + if file, err = ioutil.TempFile(dir, "file"); err != nil { + t.Fatalf("failed to create file: %s", err) + } + + d := []byte{97, 98, 99, 100, 101} + file.Write(d) + + var size int64 + if size, _ = Size(file.Name()); size != 5 { + t.Fatalf("directory with one 5-byte file has size: %d", size) + } +} + +// Size of a directory with one empty directory should be 0 +func TestSizeNestedDirectoryEmpty(t *testing.T) { + var dir string + var err error + if dir, err = ioutil.TempDir(os.TempDir(), "testSizeNestedDirectoryEmpty"); err != nil { + t.Fatalf("failed to create directory: %s", err) + } + if dir, err = ioutil.TempDir(dir, "nested"); err != nil { + t.Fatalf("failed to create nested directory: %s", err) + } + + var size int64 + if size, _ = Size(dir); size != 0 { + t.Fatalf("directory with one empty directory has size: %d", size) + } +} + +// Test directory with 1 file and 1 empty directory +func TestSizeFileAndNestedDirectoryEmpty(t *testing.T) { + var dir string + var err error + if dir, err = ioutil.TempDir(os.TempDir(), "testSizeFileAndNestedDirectoryEmpty"); err != nil { + t.Fatalf("failed to create directory: %s", err) + } + if dir, err = ioutil.TempDir(dir, "nested"); err != nil { + t.Fatalf("failed to create nested directory: %s", err) + } + + var file *os.File + if file, err = ioutil.TempFile(dir, "file"); err != nil { + t.Fatalf("failed to create file: %s", err) + } + + d := []byte{100, 111, 99, 107, 101, 114} + file.Write(d) + + var size int64 + if size, _ = Size(dir); size != 6 { + t.Fatalf("directory with 6-byte file and empty directory has size: %d", size) + } +} + +// Test directory with 1 file and 1 non-empty directory +func TestSizeFileAndNestedDirectoryNonempty(t *testing.T) { + var dir, dirNested string + var err error + if dir, err = ioutil.TempDir(os.TempDir(), "TestSizeFileAndNestedDirectoryNonempty"); err != nil { + t.Fatalf("failed to create directory: %s", err) + } + if dirNested, err = ioutil.TempDir(dir, "nested"); err != nil { + t.Fatalf("failed to create nested directory: %s", err) + } + + var file *os.File + if file, err = ioutil.TempFile(dir, "file"); err != nil { + t.Fatalf("failed to create file: %s", err) + } + + data := []byte{100, 111, 99, 107, 101, 114} + file.Write(data) + + var nestedFile *os.File + if nestedFile, err = ioutil.TempFile(dirNested, "file"); err != nil { + t.Fatalf("failed to create file in nested directory: %s", err) + } + + nestedData := []byte{100, 111, 99, 107, 101, 114} + nestedFile.Write(nestedData) + + var size int64 + if size, _ = Size(dir); size != 12 { + t.Fatalf("directory with 6-byte file and nested directory with 6-byte file has size: %d", size) + } +} + +// Test migration of directory to a subdir underneath itself +func TestMoveToSubdir(t *testing.T) { + var outerDir, subDir string + var err error + + if outerDir, err = ioutil.TempDir(os.TempDir(), "TestMoveToSubdir"); err != nil { + t.Fatalf("failed to create directory: %v", err) + } + + if subDir, err = ioutil.TempDir(outerDir, "testSub"); err != nil { + t.Fatalf("failed to create subdirectory: %v", err) + } + + // write 4 temp files in the outer dir to get moved + filesList := []string{"a", "b", "c", "d"} + for _, fName := range filesList { + if file, err := os.Create(filepath.Join(outerDir, fName)); err != nil { + t.Fatalf("couldn't create temp file %q: %v", fName, err) + } else { + file.WriteString(fName) + file.Close() + } + } + + if err = MoveToSubdir(outerDir, filepath.Base(subDir)); err != nil { + t.Fatalf("Error during migration of content to subdirectory: %v", err) + } + // validate that the files were moved to the subdirectory + infos, err := ioutil.ReadDir(subDir) + if err != nil { + t.Fatal(err) + } + if len(infos) != 4 { + t.Fatalf("Should be four files in the subdir after the migration: actual length: %d", len(infos)) + } + var results []string + for _, info := range infos { + results = append(results, info.Name()) + } + sort.Sort(sort.StringSlice(results)) + if !reflect.DeepEqual(filesList, results) { + t.Fatalf("Results after migration do not equal list of files: expected: %v, got: %v", filesList, results) + } +} diff --git a/pkg/directory/directory_unix.go b/pkg/directory/directory_unix.go new file mode 100644 index 00000000..dbebdd3c --- /dev/null +++ b/pkg/directory/directory_unix.go @@ -0,0 +1,39 @@ +// +build linux freebsd + +package directory + +import ( + "os" + "path/filepath" + "syscall" +) + +// Size walks a directory tree and returns its total size in bytes. +func Size(dir string) (size int64, err error) { + data := make(map[uint64]struct{}) + err = filepath.Walk(dir, func(d string, fileInfo os.FileInfo, e error) error { + // Ignore directory sizes + if fileInfo == nil { + return nil + } + + s := fileInfo.Size() + if fileInfo.IsDir() || s == 0 { + return nil + } + + // Check inode to handle hard links correctly + inode := fileInfo.Sys().(*syscall.Stat_t).Ino + // inode is not a uint64 on all platforms. Cast it to avoid issues. + if _, exists := data[uint64(inode)]; exists { + return nil + } + // inode is not a uint64 on all platforms. Cast it to avoid issues. + data[uint64(inode)] = struct{}{} + + size += s + + return nil + }) + return +} diff --git a/pkg/directory/directory_windows.go b/pkg/directory/directory_windows.go new file mode 100644 index 00000000..7a9f8cb6 --- /dev/null +++ b/pkg/directory/directory_windows.go @@ -0,0 +1,28 @@ +// +build windows + +package directory + +import ( + "os" + "path/filepath" +) + +// Size walks a directory tree and returns its total size in bytes. +func Size(dir string) (size int64, err error) { + err = filepath.Walk(dir, func(d string, fileInfo os.FileInfo, e error) error { + // Ignore directory sizes + if fileInfo == nil { + return nil + } + + s := fileInfo.Size() + if fileInfo.IsDir() || s == 0 { + return nil + } + + size += s + + return nil + }) + return +} diff --git a/pkg/discovery/README.md b/pkg/discovery/README.md new file mode 100644 index 00000000..39777c21 --- /dev/null +++ b/pkg/discovery/README.md @@ -0,0 +1,41 @@ +--- +page_title: Docker discovery +page_description: discovery +page_keywords: docker, clustering, discovery +--- + +# Discovery + +Docker comes with multiple Discovery backends. + +## Backends + +### Using etcd + +Point your Docker Engine instances to a common etcd instance. You can specify +the address Docker uses to advertise the node using the `--cluster-advertise` +flag. + +```bash +$ docker daemon -H= --cluster-advertise= --cluster-store etcd://,/ +``` + +### Using consul + +Point your Docker Engine instances to a common Consul instance. You can specify +the address Docker uses to advertise the node using the `--cluster-advertise` +flag. + +```bash +$ docker daemon -H= --cluster-advertise= --cluster-store consul:/// +``` + +### Using zookeeper + +Point your Docker Engine instances to a common Zookeeper instance. You can specify +the address Docker uses to advertise the node using the `--cluster-advertise` +flag. + +```bash +$ docker daemon -H= --cluster-advertise= --cluster-store zk://,/ +``` diff --git a/pkg/discovery/backends.go b/pkg/discovery/backends.go new file mode 100644 index 00000000..65364c9a --- /dev/null +++ b/pkg/discovery/backends.go @@ -0,0 +1,107 @@ +package discovery + +import ( + "fmt" + "net" + "strings" + "time" + + log "github.com/Sirupsen/logrus" +) + +var ( + // Backends is a global map of discovery backends indexed by their + // associated scheme. + backends = make(map[string]Backend) +) + +// Register makes a discovery backend available by the provided scheme. +// If Register is called twice with the same scheme an error is returned. +func Register(scheme string, d Backend) error { + if _, exists := backends[scheme]; exists { + return fmt.Errorf("scheme already registered %s", scheme) + } + log.WithField("name", scheme).Debug("Registering discovery service") + backends[scheme] = d + return nil +} + +func parse(rawurl string) (string, string) { + parts := strings.SplitN(rawurl, "://", 2) + + // nodes:port,node2:port => nodes://node1:port,node2:port + if len(parts) == 1 { + return "nodes", parts[0] + } + return parts[0], parts[1] +} + +// ParseAdvertise parses the --cluster-advertise daemon config which accepts +// : or : +func ParseAdvertise(advertise string) (string, error) { + var ( + iface *net.Interface + addrs []net.Addr + err error + ) + + addr, port, err := net.SplitHostPort(advertise) + + if err != nil { + return "", fmt.Errorf("invalid --cluster-advertise configuration: %s: %v", advertise, err) + } + + ip := net.ParseIP(addr) + // If it is a valid ip-address, use it as is + if ip != nil { + return advertise, nil + } + + // If advertise is a valid interface name, get the valid ipv4 address and use it to advertise + ifaceName := addr + iface, err = net.InterfaceByName(ifaceName) + if err != nil { + return "", fmt.Errorf("invalid cluster advertise IP address or interface name (%s) : %v", advertise, err) + } + + addrs, err = iface.Addrs() + if err != nil { + return "", fmt.Errorf("unable to get advertise IP address from interface (%s) : %v", advertise, err) + } + + if addrs == nil || len(addrs) == 0 { + return "", fmt.Errorf("no available advertise IP address in interface (%s)", advertise) + } + + addr = "" + for _, a := range addrs { + ip, _, err := net.ParseCIDR(a.String()) + if err != nil { + return "", fmt.Errorf("error deriving advertise ip-address in interface (%s) : %v", advertise, err) + } + if ip.To4() == nil || ip.IsLoopback() { + continue + } + addr = ip.String() + break + } + if addr == "" { + return "", fmt.Errorf("couldnt find a valid ip-address in interface %s", advertise) + } + + addr = net.JoinHostPort(addr, port) + return addr, nil +} + +// New returns a new Discovery given a URL, heartbeat and ttl settings. +// Returns an error if the URL scheme is not supported. +func New(rawurl string, heartbeat time.Duration, ttl time.Duration, clusterOpts map[string]string) (Backend, error) { + scheme, uri := parse(rawurl) + if backend, exists := backends[scheme]; exists { + log.WithFields(log.Fields{"name": scheme, "uri": uri}).Debug("Initializing discovery service") + err := backend.Initialize(uri, heartbeat, ttl, clusterOpts) + return backend, err + } + + return nil, ErrNotSupported +} diff --git a/pkg/discovery/discovery.go b/pkg/discovery/discovery.go new file mode 100644 index 00000000..ca7f5874 --- /dev/null +++ b/pkg/discovery/discovery.go @@ -0,0 +1,35 @@ +package discovery + +import ( + "errors" + "time" +) + +var ( + // ErrNotSupported is returned when a discovery service is not supported. + ErrNotSupported = errors.New("discovery service not supported") + + // ErrNotImplemented is returned when discovery feature is not implemented + // by discovery backend. + ErrNotImplemented = errors.New("not implemented in this discovery service") +) + +// Watcher provides watching over a cluster for nodes joining and leaving. +type Watcher interface { + // Watch the discovery for entry changes. + // Returns a channel that will receive changes or an error. + // Providing a non-nil stopCh can be used to stop watching. + Watch(stopCh <-chan struct{}) (<-chan Entries, <-chan error) +} + +// Backend is implemented by discovery backends which manage cluster entries. +type Backend interface { + // Watcher must be provided by every backend. + Watcher + + // Initialize the discovery with URIs, a heartbeat, a ttl and optional settings. + Initialize(string, time.Duration, time.Duration, map[string]string) error + + // Register to the discovery. + Register(string) error +} diff --git a/pkg/discovery/discovery_test.go b/pkg/discovery/discovery_test.go new file mode 100644 index 00000000..6084f3ef --- /dev/null +++ b/pkg/discovery/discovery_test.go @@ -0,0 +1,137 @@ +package discovery + +import ( + "testing" + + "github.com/go-check/check" +) + +// Hook up gocheck into the "go test" runner. +func Test(t *testing.T) { check.TestingT(t) } + +type DiscoverySuite struct{} + +var _ = check.Suite(&DiscoverySuite{}) + +func (s *DiscoverySuite) TestNewEntry(c *check.C) { + entry, err := NewEntry("127.0.0.1:2375") + c.Assert(err, check.IsNil) + c.Assert(entry.Equals(&Entry{Host: "127.0.0.1", Port: "2375"}), check.Equals, true) + c.Assert(entry.String(), check.Equals, "127.0.0.1:2375") + + entry, err = NewEntry("[2001:db8:0:f101::2]:2375") + c.Assert(err, check.IsNil) + c.Assert(entry.Equals(&Entry{Host: "2001:db8:0:f101::2", Port: "2375"}), check.Equals, true) + c.Assert(entry.String(), check.Equals, "[2001:db8:0:f101::2]:2375") + + _, err = NewEntry("127.0.0.1") + c.Assert(err, check.NotNil) +} + +func (s *DiscoverySuite) TestParse(c *check.C) { + scheme, uri := parse("127.0.0.1:2375") + c.Assert(scheme, check.Equals, "nodes") + c.Assert(uri, check.Equals, "127.0.0.1:2375") + + scheme, uri = parse("localhost:2375") + c.Assert(scheme, check.Equals, "nodes") + c.Assert(uri, check.Equals, "localhost:2375") + + scheme, uri = parse("scheme://127.0.0.1:2375") + c.Assert(scheme, check.Equals, "scheme") + c.Assert(uri, check.Equals, "127.0.0.1:2375") + + scheme, uri = parse("scheme://localhost:2375") + c.Assert(scheme, check.Equals, "scheme") + c.Assert(uri, check.Equals, "localhost:2375") + + scheme, uri = parse("") + c.Assert(scheme, check.Equals, "nodes") + c.Assert(uri, check.Equals, "") +} + +func (s *DiscoverySuite) TestCreateEntries(c *check.C) { + entries, err := CreateEntries(nil) + c.Assert(entries, check.DeepEquals, Entries{}) + c.Assert(err, check.IsNil) + + entries, err = CreateEntries([]string{"127.0.0.1:2375", "127.0.0.2:2375", "[2001:db8:0:f101::2]:2375", ""}) + c.Assert(err, check.IsNil) + expected := Entries{ + &Entry{Host: "127.0.0.1", Port: "2375"}, + &Entry{Host: "127.0.0.2", Port: "2375"}, + &Entry{Host: "2001:db8:0:f101::2", Port: "2375"}, + } + c.Assert(entries.Equals(expected), check.Equals, true) + + _, err = CreateEntries([]string{"127.0.0.1", "127.0.0.2"}) + c.Assert(err, check.NotNil) +} + +func (s *DiscoverySuite) TestContainsEntry(c *check.C) { + entries, err := CreateEntries([]string{"127.0.0.1:2375", "127.0.0.2:2375", ""}) + c.Assert(err, check.IsNil) + c.Assert(entries.Contains(&Entry{Host: "127.0.0.1", Port: "2375"}), check.Equals, true) + c.Assert(entries.Contains(&Entry{Host: "127.0.0.3", Port: "2375"}), check.Equals, false) +} + +func (s *DiscoverySuite) TestEntriesEquality(c *check.C) { + entries := Entries{ + &Entry{Host: "127.0.0.1", Port: "2375"}, + &Entry{Host: "127.0.0.2", Port: "2375"}, + } + + // Same + c.Assert(entries.Equals(Entries{ + &Entry{Host: "127.0.0.1", Port: "2375"}, + &Entry{Host: "127.0.0.2", Port: "2375"}, + }), check. + Equals, true) + + // Different size + c.Assert(entries.Equals(Entries{ + &Entry{Host: "127.0.0.1", Port: "2375"}, + &Entry{Host: "127.0.0.2", Port: "2375"}, + &Entry{Host: "127.0.0.3", Port: "2375"}, + }), check. + Equals, false) + + // Different content + c.Assert(entries.Equals(Entries{ + &Entry{Host: "127.0.0.1", Port: "2375"}, + &Entry{Host: "127.0.0.42", Port: "2375"}, + }), check. + Equals, false) + +} + +func (s *DiscoverySuite) TestEntriesDiff(c *check.C) { + entry1 := &Entry{Host: "1.1.1.1", Port: "1111"} + entry2 := &Entry{Host: "2.2.2.2", Port: "2222"} + entry3 := &Entry{Host: "3.3.3.3", Port: "3333"} + entries := Entries{entry1, entry2} + + // No diff + added, removed := entries.Diff(Entries{entry2, entry1}) + c.Assert(added, check.HasLen, 0) + c.Assert(removed, check.HasLen, 0) + + // Add + added, removed = entries.Diff(Entries{entry2, entry3, entry1}) + c.Assert(added, check.HasLen, 1) + c.Assert(added.Contains(entry3), check.Equals, true) + c.Assert(removed, check.HasLen, 0) + + // Remove + added, removed = entries.Diff(Entries{entry2}) + c.Assert(added, check.HasLen, 0) + c.Assert(removed, check.HasLen, 1) + c.Assert(removed.Contains(entry1), check.Equals, true) + + // Add and remove + added, removed = entries.Diff(Entries{entry1, entry3}) + c.Assert(added, check.HasLen, 1) + c.Assert(added.Contains(entry3), check.Equals, true) + c.Assert(removed, check.HasLen, 1) + c.Assert(removed.Contains(entry2), check.Equals, true) +} diff --git a/pkg/discovery/entry.go b/pkg/discovery/entry.go new file mode 100644 index 00000000..ce23bbf8 --- /dev/null +++ b/pkg/discovery/entry.go @@ -0,0 +1,94 @@ +package discovery + +import "net" + +// NewEntry creates a new entry. +func NewEntry(url string) (*Entry, error) { + host, port, err := net.SplitHostPort(url) + if err != nil { + return nil, err + } + return &Entry{host, port}, nil +} + +// An Entry represents a host. +type Entry struct { + Host string + Port string +} + +// Equals returns true if cmp contains the same data. +func (e *Entry) Equals(cmp *Entry) bool { + return e.Host == cmp.Host && e.Port == cmp.Port +} + +// String returns the string form of an entry. +func (e *Entry) String() string { + return net.JoinHostPort(e.Host, e.Port) +} + +// Entries is a list of *Entry with some helpers. +type Entries []*Entry + +// Equals returns true if cmp contains the same data. +func (e Entries) Equals(cmp Entries) bool { + // Check if the file has really changed. + if len(e) != len(cmp) { + return false + } + for i := range e { + if !e[i].Equals(cmp[i]) { + return false + } + } + return true +} + +// Contains returns true if the Entries contain a given Entry. +func (e Entries) Contains(entry *Entry) bool { + for _, curr := range e { + if curr.Equals(entry) { + return true + } + } + return false +} + +// Diff compares two entries and returns the added and removed entries. +func (e Entries) Diff(cmp Entries) (Entries, Entries) { + added := Entries{} + for _, entry := range cmp { + if !e.Contains(entry) { + added = append(added, entry) + } + } + + removed := Entries{} + for _, entry := range e { + if !cmp.Contains(entry) { + removed = append(removed, entry) + } + } + + return added, removed +} + +// CreateEntries returns an array of entries based on the given addresses. +func CreateEntries(addrs []string) (Entries, error) { + entries := Entries{} + if addrs == nil { + return entries, nil + } + + for _, addr := range addrs { + if len(addr) == 0 { + continue + } + entry, err := NewEntry(addr) + if err != nil { + return nil, err + } + entries = append(entries, entry) + } + return entries, nil +} diff --git a/pkg/discovery/file/file.go b/pkg/discovery/file/file.go new file mode 100644 index 00000000..b4f870b8 --- /dev/null +++ b/pkg/discovery/file/file.go @@ -0,0 +1,109 @@ +package file + +import ( + "fmt" + "io/ioutil" + "strings" + "time" + + "github.com/docker/docker/pkg/discovery" +) + +// Discovery is exported +type Discovery struct { + heartbeat time.Duration + path string +} + +func init() { + Init() +} + +// Init is exported +func Init() { + discovery.Register("file", &Discovery{}) +} + +// Initialize is exported +func (s *Discovery) Initialize(path string, heartbeat time.Duration, ttl time.Duration, _ map[string]string) error { + s.path = path + s.heartbeat = heartbeat + return nil +} + +func parseFileContent(content []byte) []string { + var result []string + for _, line := range strings.Split(strings.TrimSpace(string(content)), "\n") { + line = strings.TrimSpace(line) + // Ignoring line starts with # + if strings.HasPrefix(line, "#") { + continue + } + // Inlined # comment also ignored. + if strings.Contains(line, "#") { + line = line[0:strings.Index(line, "#")] + // Trim additional spaces caused by above stripping. + line = strings.TrimSpace(line) + } + for _, ip := range discovery.Generate(line) { + result = append(result, ip) + } + } + return result +} + +func (s *Discovery) fetch() (discovery.Entries, error) { + fileContent, err := ioutil.ReadFile(s.path) + if err != nil { + return nil, fmt.Errorf("failed to read '%s': %v", s.path, err) + } + return discovery.CreateEntries(parseFileContent(fileContent)) +} + +// Watch is exported +func (s *Discovery) Watch(stopCh <-chan struct{}) (<-chan discovery.Entries, <-chan error) { + ch := make(chan discovery.Entries) + errCh := make(chan error) + ticker := time.NewTicker(s.heartbeat) + + go func() { + defer close(errCh) + defer close(ch) + + // Send the initial entries if available. + currentEntries, err := s.fetch() + if err != nil { + errCh <- err + } else { + ch <- currentEntries + } + + // Periodically send updates. + for { + select { + case <-ticker.C: + newEntries, err := s.fetch() + if err != nil { + errCh <- err + continue + } + + // Check if the file has really changed. + if !newEntries.Equals(currentEntries) { + ch <- newEntries + } + currentEntries = newEntries + case <-stopCh: + ticker.Stop() + return + } + } + }() + + return ch, errCh +} + +// Register is exported +func (s *Discovery) Register(addr string) error { + return discovery.ErrNotImplemented +} diff --git a/pkg/discovery/file/file_test.go b/pkg/discovery/file/file_test.go new file mode 100644 index 00000000..667f00ba --- /dev/null +++ b/pkg/discovery/file/file_test.go @@ -0,0 +1,114 @@ +package file + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/docker/docker/pkg/discovery" + + "github.com/go-check/check" +) + +// Hook up gocheck into the "go test" runner. +func Test(t *testing.T) { check.TestingT(t) } + +type DiscoverySuite struct{} + +var _ = check.Suite(&DiscoverySuite{}) + +func (s *DiscoverySuite) TestInitialize(c *check.C) { + d := &Discovery{} + d.Initialize("/path/to/file", 1000, 0, nil) + c.Assert(d.path, check.Equals, "/path/to/file") +} + +func (s *DiscoverySuite) TestNew(c *check.C) { + d, err := discovery.New("file:///path/to/file", 0, 0, nil) + c.Assert(err, check.IsNil) + c.Assert(d.(*Discovery).path, check.Equals, "/path/to/file") +} + +func (s *DiscoverySuite) TestContent(c *check.C) { + data := ` +1.1.1.[1:2]:1111 +2.2.2.[2:4]:2222 +` + ips := parseFileContent([]byte(data)) + c.Assert(ips, check.HasLen, 5) + c.Assert(ips[0], check.Equals, "1.1.1.1:1111") + c.Assert(ips[1], check.Equals, "1.1.1.2:1111") + c.Assert(ips[2], check.Equals, "2.2.2.2:2222") + c.Assert(ips[3], check.Equals, "2.2.2.3:2222") + c.Assert(ips[4], check.Equals, "2.2.2.4:2222") +} + +func (s *DiscoverySuite) TestRegister(c *check.C) { + discovery := &Discovery{path: "/path/to/file"} + c.Assert(discovery.Register("0.0.0.0"), check.NotNil) +} + +func (s *DiscoverySuite) TestParsingContentsWithComments(c *check.C) { + data := ` +### test ### +1.1.1.1:1111 # inline comment +# 2.2.2.2:2222 + ### empty line with comment + 3.3.3.3:3333 +### test ### +` + ips := parseFileContent([]byte(data)) + c.Assert(ips, check.HasLen, 2) + c.Assert("1.1.1.1:1111", check.Equals, ips[0]) + c.Assert("3.3.3.3:3333", check.Equals, ips[1]) +} + +func (s *DiscoverySuite) TestWatch(c *check.C) { + data := ` +1.1.1.1:1111 +2.2.2.2:2222 +` + expected := discovery.Entries{ + &discovery.Entry{Host: "1.1.1.1", Port: "1111"}, + &discovery.Entry{Host: "2.2.2.2", Port: "2222"}, + } + + // Create a temporary file and remove it. + tmp, err := ioutil.TempFile(os.TempDir(), "discovery-file-test") + c.Assert(err, check.IsNil) + c.Assert(tmp.Close(), check.IsNil) + c.Assert(os.Remove(tmp.Name()), check.IsNil) + + // Set up file discovery. + d := &Discovery{} + d.Initialize(tmp.Name(), 1000, 0, nil) + stopCh := make(chan struct{}) + ch, errCh := d.Watch(stopCh) + + // Make sure it fires errors since the file doesn't exist. + c.Assert(<-errCh, check.NotNil) + // We have to drain the error channel otherwise Watch will get stuck. + go func() { + for range errCh { + } + }() + + // Write the file and make sure we get the expected value back. + c.Assert(ioutil.WriteFile(tmp.Name(), []byte(data), 0600), check.IsNil) + c.Assert(<-ch, check.DeepEquals, expected) + + // Add a new entry and look it up. + expected = append(expected, &discovery.Entry{Host: "3.3.3.3", Port: "3333"}) + f, err := os.OpenFile(tmp.Name(), os.O_APPEND|os.O_WRONLY, 0600) + c.Assert(err, check.IsNil) + c.Assert(f, check.NotNil) + _, err = f.WriteString("\n3.3.3.3:3333\n") + c.Assert(err, check.IsNil) + f.Close() + c.Assert(<-ch, check.DeepEquals, expected) + + // Stop and make sure it closes all channels. + close(stopCh) + c.Assert(<-ch, check.IsNil) + c.Assert(<-errCh, check.IsNil) +} diff --git a/pkg/discovery/generator.go b/pkg/discovery/generator.go new file mode 100644 index 00000000..d2229829 --- /dev/null +++ b/pkg/discovery/generator.go @@ -0,0 +1,35 @@ +package discovery + +import ( + "fmt" + "regexp" + "strconv" +) + +// Generate takes care of IP generation +func Generate(pattern string) []string { + re, _ := regexp.Compile(`\[(.+):(.+)\]`) + submatch := re.FindStringSubmatch(pattern) + if submatch == nil { + return []string{pattern} + } + + from, err := strconv.Atoi(submatch[1]) + if err != nil { + return []string{pattern} + } + to, err := strconv.Atoi(submatch[2]) + if err != nil { + return []string{pattern} + } + + template := re.ReplaceAllString(pattern, "%d") + + var result []string + for val := from; val <= to; val++ { + entry := fmt.Sprintf(template, val) + result = append(result, entry) + } + + return result +} diff --git a/pkg/discovery/generator_test.go b/pkg/discovery/generator_test.go new file mode 100644 index 00000000..6281c466 --- /dev/null +++ b/pkg/discovery/generator_test.go @@ -0,0 +1,53 @@ +package discovery + +import ( + "github.com/go-check/check" +) + +func (s *DiscoverySuite) TestGeneratorNotGenerate(c *check.C) { + ips := Generate("127.0.0.1") + c.Assert(len(ips), check.Equals, 1) + c.Assert(ips[0], check.Equals, "127.0.0.1") +} + +func (s *DiscoverySuite) TestGeneratorWithPortNotGenerate(c *check.C) { + ips := Generate("127.0.0.1:8080") + c.Assert(len(ips), check.Equals, 1) + c.Assert(ips[0], check.Equals, "127.0.0.1:8080") +} + +func (s *DiscoverySuite) TestGeneratorMatchFailedNotGenerate(c *check.C) { + ips := Generate("127.0.0.[1]") + c.Assert(len(ips), check.Equals, 1) + c.Assert(ips[0], check.Equals, "127.0.0.[1]") +} + +func (s *DiscoverySuite) TestGeneratorWithPort(c *check.C) { + ips := Generate("127.0.0.[1:11]:2375") + c.Assert(len(ips), check.Equals, 11) + c.Assert(ips[0], check.Equals, "127.0.0.1:2375") + c.Assert(ips[1], check.Equals, "127.0.0.2:2375") + c.Assert(ips[2], check.Equals, "127.0.0.3:2375") + c.Assert(ips[3], check.Equals, "127.0.0.4:2375") + c.Assert(ips[4], check.Equals, "127.0.0.5:2375") + c.Assert(ips[5], check.Equals, "127.0.0.6:2375") + c.Assert(ips[6], check.Equals, "127.0.0.7:2375") + c.Assert(ips[7], check.Equals, "127.0.0.8:2375") + c.Assert(ips[8], check.Equals, "127.0.0.9:2375") + c.Assert(ips[9], check.Equals, "127.0.0.10:2375") + c.Assert(ips[10], check.Equals, "127.0.0.11:2375") +} + +func (s *DiscoverySuite) TestGenerateWithMalformedInputAtRangeStart(c *check.C) { + malformedInput := "127.0.0.[x:11]:2375" + ips := Generate(malformedInput) + c.Assert(len(ips), check.Equals, 1) + c.Assert(ips[0], check.Equals, malformedInput) +} + +func (s *DiscoverySuite) TestGenerateWithMalformedInputAtRangeEnd(c *check.C) { + malformedInput := "127.0.0.[1:x]:2375" + ips := Generate(malformedInput) + c.Assert(len(ips), check.Equals, 1) + c.Assert(ips[0], check.Equals, malformedInput) +} diff --git a/pkg/discovery/kv/kv.go b/pkg/discovery/kv/kv.go new file mode 100644 index 00000000..f371c0cb --- /dev/null +++ b/pkg/discovery/kv/kv.go @@ -0,0 +1,192 @@ +package kv + +import ( + "fmt" + "path" + "strings" + "time" + + log "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/discovery" + "github.com/docker/go-connections/tlsconfig" + "github.com/docker/libkv" + "github.com/docker/libkv/store" + "github.com/docker/libkv/store/consul" + "github.com/docker/libkv/store/etcd" + "github.com/docker/libkv/store/zookeeper" +) + +const ( + defaultDiscoveryPath = "docker/nodes" +) + +// Discovery is exported +type Discovery struct { + backend store.Backend + store store.Store + heartbeat time.Duration + ttl time.Duration + prefix string + path string +} + +func init() { + Init() +} + +// Init is exported +func Init() { + // Register to libkv + zookeeper.Register() + consul.Register() + etcd.Register() + + // Register to internal discovery service + discovery.Register("zk", &Discovery{backend: store.ZK}) + discovery.Register("consul", &Discovery{backend: store.CONSUL}) + discovery.Register("etcd", &Discovery{backend: store.ETCD}) +} + +// Initialize is exported +func (s *Discovery) Initialize(uris string, heartbeat time.Duration, ttl time.Duration, clusterOpts map[string]string) error { + var ( + parts = strings.SplitN(uris, "/", 2) + addrs = strings.Split(parts[0], ",") + err error + ) + + // A custom prefix to the path can be optionally used. + if len(parts) == 2 { + s.prefix = parts[1] + } + + s.heartbeat = heartbeat + s.ttl = ttl + + // Use a custom path if specified in discovery options + dpath := defaultDiscoveryPath + if clusterOpts["kv.path"] != "" { + dpath = clusterOpts["kv.path"] + } + + s.path = path.Join(s.prefix, dpath) + + var config *store.Config + if clusterOpts["kv.cacertfile"] != "" && clusterOpts["kv.certfile"] != "" && clusterOpts["kv.keyfile"] != "" { + log.Info("Initializing discovery with TLS") + tlsConfig, err := tlsconfig.Client(tlsconfig.Options{ + CAFile: clusterOpts["kv.cacertfile"], + CertFile: clusterOpts["kv.certfile"], + KeyFile: clusterOpts["kv.keyfile"], + }) + if err != nil { + return err + } + config = &store.Config{ + // Set ClientTLS to trigger https (bug in libkv/etcd) + ClientTLS: &store.ClientTLSConfig{ + CACertFile: clusterOpts["kv.cacertfile"], + CertFile: clusterOpts["kv.certfile"], + KeyFile: clusterOpts["kv.keyfile"], + }, + // The actual TLS config that will be used + TLS: tlsConfig, + } + } else { + log.Info("Initializing discovery without TLS") + } + + // Creates a new store, will ignore options given + // if not supported by the chosen store + s.store, err = libkv.NewStore(s.backend, addrs, config) + return err +} + +// Watch the store until either there's a store error or we receive a stop request. +// Returns false if we shouldn't attempt watching the store anymore (stop request received). +func (s *Discovery) watchOnce(stopCh <-chan struct{}, watchCh <-chan []*store.KVPair, discoveryCh chan discovery.Entries, errCh chan error) bool { + for { + select { + case pairs := <-watchCh: + if pairs == nil { + return true + } + + log.WithField("discovery", s.backend).Debugf("Watch triggered with %d nodes", len(pairs)) + + // Convert `KVPair` into `discovery.Entry`. + addrs := make([]string, len(pairs)) + for _, pair := range pairs { + addrs = append(addrs, string(pair.Value)) + } + + entries, err := discovery.CreateEntries(addrs) + if err != nil { + errCh <- err + } else { + discoveryCh <- entries + } + case <-stopCh: + // We were requested to stop watching. + return false + } + } +} + +// Watch is exported +func (s *Discovery) Watch(stopCh <-chan struct{}) (<-chan discovery.Entries, <-chan error) { + ch := make(chan discovery.Entries) + errCh := make(chan error) + + go func() { + defer close(ch) + defer close(errCh) + + // Forever: Create a store watch, watch until we get an error and then try again. + // Will only stop if we receive a stopCh request. + for { + // Create the path to watch if it does not exist yet + exists, err := s.store.Exists(s.path) + if err != nil { + errCh <- err + } + if !exists { + if err := s.store.Put(s.path, []byte(""), &store.WriteOptions{IsDir: true}); err != nil { + errCh <- err + } + } + + // Set up a watch. + watchCh, err := s.store.WatchTree(s.path, stopCh) + if err != nil { + errCh <- err + } else { + if !s.watchOnce(stopCh, watchCh, ch, errCh) { + return + } + } + + // If we get here it means the store watch channel was closed. This + // is unexpected so let's retry later. + errCh <- fmt.Errorf("Unexpected watch error") + time.Sleep(s.heartbeat) + } + }() + return ch, errCh +} + +// Register is exported +func (s *Discovery) Register(addr string) error { + opts := &store.WriteOptions{TTL: s.ttl} + return s.store.Put(path.Join(s.path, addr), []byte(addr), opts) +} + +// Store returns the underlying store used by KV discovery. +func (s *Discovery) Store() store.Store { + return s.store +} + +// Prefix returns the store prefix +func (s *Discovery) Prefix() string { + return s.prefix +} diff --git a/pkg/discovery/kv/kv_test.go b/pkg/discovery/kv/kv_test.go new file mode 100644 index 00000000..4fe5239d --- /dev/null +++ b/pkg/discovery/kv/kv_test.go @@ -0,0 +1,324 @@ +package kv + +import ( + "errors" + "io/ioutil" + "os" + "path" + "testing" + "time" + + "github.com/docker/docker/pkg/discovery" + "github.com/docker/libkv" + "github.com/docker/libkv/store" + + "github.com/go-check/check" +) + +// Hook up gocheck into the "go test" runner. +func Test(t *testing.T) { check.TestingT(t) } + +type DiscoverySuite struct{} + +var _ = check.Suite(&DiscoverySuite{}) + +func (ds *DiscoverySuite) TestInitialize(c *check.C) { + storeMock := &FakeStore{ + Endpoints: []string{"127.0.0.1"}, + } + d := &Discovery{backend: store.CONSUL} + d.Initialize("127.0.0.1", 0, 0, nil) + d.store = storeMock + + s := d.store.(*FakeStore) + c.Assert(s.Endpoints, check.HasLen, 1) + c.Assert(s.Endpoints[0], check.Equals, "127.0.0.1") + c.Assert(d.path, check.Equals, defaultDiscoveryPath) + + storeMock = &FakeStore{ + Endpoints: []string{"127.0.0.1:1234"}, + } + d = &Discovery{backend: store.CONSUL} + d.Initialize("127.0.0.1:1234/path", 0, 0, nil) + d.store = storeMock + + s = d.store.(*FakeStore) + c.Assert(s.Endpoints, check.HasLen, 1) + c.Assert(s.Endpoints[0], check.Equals, "127.0.0.1:1234") + c.Assert(d.path, check.Equals, "path/"+defaultDiscoveryPath) + + storeMock = &FakeStore{ + Endpoints: []string{"127.0.0.1:1234", "127.0.0.2:1234", "127.0.0.3:1234"}, + } + d = &Discovery{backend: store.CONSUL} + d.Initialize("127.0.0.1:1234,127.0.0.2:1234,127.0.0.3:1234/path", 0, 0, nil) + d.store = storeMock + + s = d.store.(*FakeStore) + c.Assert(s.Endpoints, check.HasLen, 3) + c.Assert(s.Endpoints[0], check.Equals, "127.0.0.1:1234") + c.Assert(s.Endpoints[1], check.Equals, "127.0.0.2:1234") + c.Assert(s.Endpoints[2], check.Equals, "127.0.0.3:1234") + + c.Assert(d.path, check.Equals, "path/"+defaultDiscoveryPath) +} + +// Extremely limited mock store so we can test initialization +type Mock struct { + // Endpoints passed to InitializeMock + Endpoints []string + + // Options passed to InitializeMock + Options *store.Config +} + +func NewMock(endpoints []string, options *store.Config) (store.Store, error) { + s := &Mock{} + s.Endpoints = endpoints + s.Options = options + return s, nil +} +func (s *Mock) Put(key string, value []byte, opts *store.WriteOptions) error { + return errors.New("Put not supported") +} +func (s *Mock) Get(key string) (*store.KVPair, error) { + return nil, errors.New("Get not supported") +} +func (s *Mock) Delete(key string) error { + return errors.New("Delete not supported") +} + +// Exists mock +func (s *Mock) Exists(key string) (bool, error) { + return false, errors.New("Exists not supported") +} + +// Watch mock +func (s *Mock) Watch(key string, stopCh <-chan struct{}) (<-chan *store.KVPair, error) { + return nil, errors.New("Watch not supported") +} + +// WatchTree mock +func (s *Mock) WatchTree(prefix string, stopCh <-chan struct{}) (<-chan []*store.KVPair, error) { + return nil, errors.New("WatchTree not supported") +} + +// NewLock mock +func (s *Mock) NewLock(key string, options *store.LockOptions) (store.Locker, error) { + return nil, errors.New("NewLock not supported") +} + +// List mock +func (s *Mock) List(prefix string) ([]*store.KVPair, error) { + return nil, errors.New("List not supported") +} + +// DeleteTree mock +func (s *Mock) DeleteTree(prefix string) error { + return errors.New("DeleteTree not supported") +} + +// AtomicPut mock +func (s *Mock) AtomicPut(key string, value []byte, previous *store.KVPair, opts *store.WriteOptions) (bool, *store.KVPair, error) { + return false, nil, errors.New("AtomicPut not supported") +} + +// AtomicDelete mock +func (s *Mock) AtomicDelete(key string, previous *store.KVPair) (bool, error) { + return false, errors.New("AtomicDelete not supported") +} + +// Close mock +func (s *Mock) Close() { + return +} + +func (ds *DiscoverySuite) TestInitializeWithCerts(c *check.C) { + cert := `-----BEGIN CERTIFICATE----- +MIIDCDCCAfKgAwIBAgIICifG7YeiQOEwCwYJKoZIhvcNAQELMBIxEDAOBgNVBAMT +B1Rlc3QgQ0EwHhcNMTUxMDAxMjMwMDAwWhcNMjAwOTI5MjMwMDAwWjASMRAwDgYD +VQQDEwdUZXN0IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1wRC +O+flnLTK5ImjTurNRHwSejuqGbc4CAvpB0hS+z0QlSs4+zE9h80aC4hz+6caRpds ++J908Q+RvAittMHbpc7VjbZP72G6fiXk7yPPl6C10HhRSoSi3nY+B7F2E8cuz14q +V2e+ejhWhSrBb/keyXpcyjoW1BOAAJ2TIclRRkICSCZrpXUyXxAvzXfpFXo1RhSb +UywN11pfiCQzDUN7sPww9UzFHuAHZHoyfTr27XnJYVUerVYrCPq8vqfn//01qz55 +Xs0hvzGdlTFXhuabFtQnKFH5SNwo/fcznhB7rePOwHojxOpXTBepUCIJLbtNnWFT +V44t9gh5IqIWtoBReQIDAQABo2YwZDAOBgNVHQ8BAf8EBAMCAAYwEgYDVR0TAQH/ +BAgwBgEB/wIBAjAdBgNVHQ4EFgQUZKUI8IIjIww7X/6hvwggQK4bD24wHwYDVR0j +BBgwFoAUZKUI8IIjIww7X/6hvwggQK4bD24wCwYJKoZIhvcNAQELA4IBAQDES2cz +7sCQfDCxCIWH7X8kpi/JWExzUyQEJ0rBzN1m3/x8ySRxtXyGekimBqQwQdFqlwMI +xzAQKkh3ue8tNSzRbwqMSyH14N1KrSxYS9e9szJHfUasoTpQGPmDmGIoRJuq1h6M +ej5x1SCJ7GWCR6xEXKUIE9OftXm9TdFzWa7Ja3OHz/mXteii8VXDuZ5ACq6EE5bY +8sP4gcICfJ5fTrpTlk9FIqEWWQrCGa5wk95PGEj+GJpNogjXQ97wVoo/Y3p1brEn +t5zjN9PAq4H1fuCMdNNA+p1DHNwd+ELTxcMAnb2ajwHvV6lKPXutrTFc4umJToBX +FpTxDmJHEV4bzUzh +-----END CERTIFICATE----- +` + key := `-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA1wRCO+flnLTK5ImjTurNRHwSejuqGbc4CAvpB0hS+z0QlSs4 ++zE9h80aC4hz+6caRpds+J908Q+RvAittMHbpc7VjbZP72G6fiXk7yPPl6C10HhR +SoSi3nY+B7F2E8cuz14qV2e+ejhWhSrBb/keyXpcyjoW1BOAAJ2TIclRRkICSCZr +pXUyXxAvzXfpFXo1RhSbUywN11pfiCQzDUN7sPww9UzFHuAHZHoyfTr27XnJYVUe +rVYrCPq8vqfn//01qz55Xs0hvzGdlTFXhuabFtQnKFH5SNwo/fcznhB7rePOwHoj +xOpXTBepUCIJLbtNnWFTV44t9gh5IqIWtoBReQIDAQABAoIBAHSWipORGp/uKFXj +i/mut776x8ofsAxhnLBARQr93ID+i49W8H7EJGkOfaDjTICYC1dbpGrri61qk8sx +qX7p3v/5NzKwOIfEpirgwVIqSNYe/ncbxnhxkx6tXtUtFKmEx40JskvSpSYAhmmO +1XSx0E/PWaEN/nLgX/f1eWJIlxlQkk3QeqL+FGbCXI48DEtlJ9+MzMu4pAwZTpj5 +5qtXo5JJ0jRGfJVPAOznRsYqv864AhMdMIWguzk6EGnbaCWwPcfcn+h9a5LMdony +MDHfBS7bb5tkF3+AfnVY3IBMVx7YlsD9eAyajlgiKu4zLbwTRHjXgShy+4Oussz0 +ugNGnkECgYEA/hi+McrZC8C4gg6XqK8+9joD8tnyDZDz88BQB7CZqABUSwvjDqlP +L8hcwo/lzvjBNYGkqaFPUICGWKjeCtd8pPS2DCVXxDQX4aHF1vUur0uYNncJiV3N +XQz4Iemsa6wnKf6M67b5vMXICw7dw0HZCdIHD1hnhdtDz0uVpeevLZ8CgYEA2KCT +Y43lorjrbCgMqtlefkr3GJA9dey+hTzCiWEOOqn9RqGoEGUday0sKhiLofOgmN2B +LEukpKIey8s+Q/cb6lReajDVPDsMweX8i7hz3Wa4Ugp4Xa5BpHqu8qIAE2JUZ7bU +t88aQAYE58pUF+/Lq1QzAQdrjjzQBx6SrBxieecCgYEAvukoPZEC8mmiN1VvbTX+ +QFHmlZha3QaDxChB+QUe7bMRojEUL/fVnzkTOLuVFqSfxevaI/km9n0ac5KtAchV +xjp2bTnBb5EUQFqjopYktWA+xO07JRJtMfSEmjZPbbay1kKC7rdTfBm961EIHaRj +xZUf6M+rOE8964oGrdgdLlECgYEA046GQmx6fh7/82FtdZDRQp9tj3SWQUtSiQZc +qhO59Lq8mjUXz+MgBuJXxkiwXRpzlbaFB0Bca1fUoYw8o915SrDYf/Zu2OKGQ/qa +V81sgiVmDuEgycR7YOlbX6OsVUHrUlpwhY3hgfMe6UtkMvhBvHF/WhroBEIJm1pV +PXZ/CbMCgYEApNWVktFBjOaYfY6SNn4iSts1jgsQbbpglg3kT7PLKjCAhI6lNsbk +dyT7ut01PL6RaW4SeQWtrJIVQaM6vF3pprMKqlc5XihOGAmVqH7rQx9rtQB5TicL +BFrwkQE4HQtQBV60hYQUzzlSk44VFDz+jxIEtacRHaomDRh2FtOTz+I= +-----END RSA PRIVATE KEY----- +` + certFile, err := ioutil.TempFile("", "cert") + c.Assert(err, check.IsNil) + defer os.Remove(certFile.Name()) + certFile.Write([]byte(cert)) + certFile.Close() + keyFile, err := ioutil.TempFile("", "key") + c.Assert(err, check.IsNil) + defer os.Remove(keyFile.Name()) + keyFile.Write([]byte(key)) + keyFile.Close() + + libkv.AddStore("mock", NewMock) + d := &Discovery{backend: "mock"} + err = d.Initialize("127.0.0.3:1234", 0, 0, map[string]string{ + "kv.cacertfile": certFile.Name(), + "kv.certfile": certFile.Name(), + "kv.keyfile": keyFile.Name(), + }) + c.Assert(err, check.IsNil) + s := d.store.(*Mock) + c.Assert(s.Options.TLS, check.NotNil) + c.Assert(s.Options.TLS.RootCAs, check.NotNil) + c.Assert(s.Options.TLS.Certificates, check.HasLen, 1) +} + +func (ds *DiscoverySuite) TestWatch(c *check.C) { + mockCh := make(chan []*store.KVPair) + + storeMock := &FakeStore{ + Endpoints: []string{"127.0.0.1:1234"}, + mockKVChan: mockCh, + } + + d := &Discovery{backend: store.CONSUL} + d.Initialize("127.0.0.1:1234/path", 0, 0, nil) + d.store = storeMock + + expected := discovery.Entries{ + &discovery.Entry{Host: "1.1.1.1", Port: "1111"}, + &discovery.Entry{Host: "2.2.2.2", Port: "2222"}, + } + kvs := []*store.KVPair{ + {Key: path.Join("path", defaultDiscoveryPath, "1.1.1.1"), Value: []byte("1.1.1.1:1111")}, + {Key: path.Join("path", defaultDiscoveryPath, "2.2.2.2"), Value: []byte("2.2.2.2:2222")}, + } + + stopCh := make(chan struct{}) + ch, errCh := d.Watch(stopCh) + + // It should fire an error since the first WatchTree call failed. + c.Assert(<-errCh, check.ErrorMatches, "test error") + // We have to drain the error channel otherwise Watch will get stuck. + go func() { + for range errCh { + } + }() + + // Push the entries into the store channel and make sure discovery emits. + mockCh <- kvs + c.Assert(<-ch, check.DeepEquals, expected) + + // Add a new entry. + expected = append(expected, &discovery.Entry{Host: "3.3.3.3", Port: "3333"}) + kvs = append(kvs, &store.KVPair{Key: path.Join("path", defaultDiscoveryPath, "3.3.3.3"), Value: []byte("3.3.3.3:3333")}) + mockCh <- kvs + c.Assert(<-ch, check.DeepEquals, expected) + + close(mockCh) + // Give it enough time to call WatchTree. + time.Sleep(3) + + // Stop and make sure it closes all channels. + close(stopCh) + c.Assert(<-ch, check.IsNil) + c.Assert(<-errCh, check.IsNil) +} + +// FakeStore implements store.Store methods. It mocks all store +// function in a simple, naive way. +type FakeStore struct { + Endpoints []string + Options *store.Config + mockKVChan <-chan []*store.KVPair + + watchTreeCallCount int +} + +func (s *FakeStore) Put(key string, value []byte, options *store.WriteOptions) error { + return nil +} + +func (s *FakeStore) Get(key string) (*store.KVPair, error) { + return nil, nil +} + +func (s *FakeStore) Delete(key string) error { + return nil +} + +func (s *FakeStore) Exists(key string) (bool, error) { + return true, nil +} + +func (s *FakeStore) Watch(key string, stopCh <-chan struct{}) (<-chan *store.KVPair, error) { + return nil, nil +} + +// WatchTree will fail the first time, and return the mockKVchan afterwards. +// This is the behavior we need for testing.. If we need 'moar', should update this. +func (s *FakeStore) WatchTree(directory string, stopCh <-chan struct{}) (<-chan []*store.KVPair, error) { + if s.watchTreeCallCount == 0 { + s.watchTreeCallCount = 1 + return nil, errors.New("test error") + } + // First calls error + return s.mockKVChan, nil +} + +func (s *FakeStore) NewLock(key string, options *store.LockOptions) (store.Locker, error) { + return nil, nil +} + +func (s *FakeStore) List(directory string) ([]*store.KVPair, error) { + return []*store.KVPair{}, nil +} + +func (s *FakeStore) DeleteTree(directory string) error { + return nil +} + +func (s *FakeStore) AtomicPut(key string, value []byte, previous *store.KVPair, options *store.WriteOptions) (bool, *store.KVPair, error) { + return true, nil, nil +} + +func (s *FakeStore) AtomicDelete(key string, previous *store.KVPair) (bool, error) { + return true, nil +} + +func (s *FakeStore) Close() { +} diff --git a/pkg/discovery/memory/memory.go b/pkg/discovery/memory/memory.go new file mode 100644 index 00000000..777a9a16 --- /dev/null +++ b/pkg/discovery/memory/memory.go @@ -0,0 +1,83 @@ +package memory + +import ( + "time" + + "github.com/docker/docker/pkg/discovery" +) + +// Discovery implements a descovery backend that keeps +// data in memory. +type Discovery struct { + heartbeat time.Duration + values []string +} + +func init() { + Init() +} + +// Init registers the memory backend on demand. +func Init() { + discovery.Register("memory", &Discovery{}) +} + +// Initialize sets the heartbeat for the memory backend. +func (s *Discovery) Initialize(_ string, heartbeat time.Duration, _ time.Duration, _ map[string]string) error { + s.heartbeat = heartbeat + s.values = make([]string, 0) + return nil +} + +// Watch sends periodic discovery updates to a channel. +func (s *Discovery) Watch(stopCh <-chan struct{}) (<-chan discovery.Entries, <-chan error) { + ch := make(chan discovery.Entries) + errCh := make(chan error) + ticker := time.NewTicker(s.heartbeat) + + go func() { + defer close(errCh) + defer close(ch) + + // Send the initial entries if available. + var currentEntries discovery.Entries + if len(s.values) > 0 { + var err error + currentEntries, err = discovery.CreateEntries(s.values) + if err != nil { + errCh <- err + } else { + ch <- currentEntries + } + } + + // Periodically send updates. + for { + select { + case <-ticker.C: + newEntries, err := discovery.CreateEntries(s.values) + if err != nil { + errCh <- err + continue + } + + // Check if the file has really changed. + if !newEntries.Equals(currentEntries) { + ch <- newEntries + } + currentEntries = newEntries + case <-stopCh: + ticker.Stop() + return + } + } + }() + + return ch, errCh +} + +// Register adds a new address to the discovery. +func (s *Discovery) Register(addr string) error { + s.values = append(s.values, addr) + return nil +} diff --git a/pkg/discovery/memory/memory_test.go b/pkg/discovery/memory/memory_test.go new file mode 100644 index 00000000..c2da0a06 --- /dev/null +++ b/pkg/discovery/memory/memory_test.go @@ -0,0 +1,48 @@ +package memory + +import ( + "testing" + + "github.com/docker/docker/pkg/discovery" + "github.com/go-check/check" +) + +// Hook up gocheck into the "go test" runner. +func Test(t *testing.T) { check.TestingT(t) } + +type discoverySuite struct{} + +var _ = check.Suite(&discoverySuite{}) + +func (s *discoverySuite) TestWatch(c *check.C) { + d := &Discovery{} + d.Initialize("foo", 1000, 0, nil) + stopCh := make(chan struct{}) + ch, errCh := d.Watch(stopCh) + + // We have to drain the error channel otherwise Watch will get stuck. + go func() { + for range errCh { + } + }() + + expected := discovery.Entries{ + &discovery.Entry{Host: "1.1.1.1", Port: "1111"}, + } + + c.Assert(d.Register("1.1.1.1:1111"), check.IsNil) + c.Assert(<-ch, check.DeepEquals, expected) + + expected = discovery.Entries{ + &discovery.Entry{Host: "1.1.1.1", Port: "1111"}, + &discovery.Entry{Host: "2.2.2.2", Port: "2222"}, + } + + c.Assert(d.Register("2.2.2.2:2222"), check.IsNil) + c.Assert(<-ch, check.DeepEquals, expected) + + // Stop and make sure it closes all channels. + close(stopCh) + c.Assert(<-ch, check.IsNil) + c.Assert(<-errCh, check.IsNil) +} diff --git a/pkg/discovery/nodes/nodes.go b/pkg/discovery/nodes/nodes.go new file mode 100644 index 00000000..c0e3c07b --- /dev/null +++ b/pkg/discovery/nodes/nodes.go @@ -0,0 +1,54 @@ +package nodes + +import ( + "fmt" + "strings" + "time" + + "github.com/docker/docker/pkg/discovery" +) + +// Discovery is exported +type Discovery struct { + entries discovery.Entries +} + +func init() { + Init() +} + +// Init is exported +func Init() { + discovery.Register("nodes", &Discovery{}) +} + +// Initialize is exported +func (s *Discovery) Initialize(uris string, _ time.Duration, _ time.Duration, _ map[string]string) error { + for _, input := range strings.Split(uris, ",") { + for _, ip := range discovery.Generate(input) { + entry, err := discovery.NewEntry(ip) + if err != nil { + return fmt.Errorf("%s, please check you are using the correct discovery (missing token:// ?)", err.Error()) + } + s.entries = append(s.entries, entry) + } + } + + return nil +} + +// Watch is exported +func (s *Discovery) Watch(stopCh <-chan struct{}) (<-chan discovery.Entries, <-chan error) { + ch := make(chan discovery.Entries) + go func() { + defer close(ch) + ch <- s.entries + <-stopCh + }() + return ch, nil +} + +// Register is exported +func (s *Discovery) Register(addr string) error { + return discovery.ErrNotImplemented +} diff --git a/pkg/discovery/nodes/nodes_test.go b/pkg/discovery/nodes/nodes_test.go new file mode 100644 index 00000000..e26568cf --- /dev/null +++ b/pkg/discovery/nodes/nodes_test.go @@ -0,0 +1,51 @@ +package nodes + +import ( + "testing" + + "github.com/docker/docker/pkg/discovery" + + "github.com/go-check/check" +) + +// Hook up gocheck into the "go test" runner. +func Test(t *testing.T) { check.TestingT(t) } + +type DiscoverySuite struct{} + +var _ = check.Suite(&DiscoverySuite{}) + +func (s *DiscoverySuite) TestInitialize(c *check.C) { + d := &Discovery{} + d.Initialize("1.1.1.1:1111,2.2.2.2:2222", 0, 0, nil) + c.Assert(len(d.entries), check.Equals, 2) + c.Assert(d.entries[0].String(), check.Equals, "1.1.1.1:1111") + c.Assert(d.entries[1].String(), check.Equals, "2.2.2.2:2222") +} + +func (s *DiscoverySuite) TestInitializeWithPattern(c *check.C) { + d := &Discovery{} + d.Initialize("1.1.1.[1:2]:1111,2.2.2.[2:4]:2222", 0, 0, nil) + c.Assert(len(d.entries), check.Equals, 5) + c.Assert(d.entries[0].String(), check.Equals, "1.1.1.1:1111") + c.Assert(d.entries[1].String(), check.Equals, "1.1.1.2:1111") + c.Assert(d.entries[2].String(), check.Equals, "2.2.2.2:2222") + c.Assert(d.entries[3].String(), check.Equals, "2.2.2.3:2222") + c.Assert(d.entries[4].String(), check.Equals, "2.2.2.4:2222") +} + +func (s *DiscoverySuite) TestWatch(c *check.C) { + d := &Discovery{} + d.Initialize("1.1.1.1:1111,2.2.2.2:2222", 0, 0, nil) + expected := discovery.Entries{ + &discovery.Entry{Host: "1.1.1.1", Port: "1111"}, + &discovery.Entry{Host: "2.2.2.2", Port: "2222"}, + } + ch, _ := d.Watch(nil) + c.Assert(expected.Equals(<-ch), check.Equals, true) +} + +func (s *DiscoverySuite) TestRegister(c *check.C) { + d := &Discovery{} + c.Assert(d.Register("0.0.0.0"), check.NotNil) +} diff --git a/pkg/filenotify/filenotify.go b/pkg/filenotify/filenotify.go new file mode 100644 index 00000000..23befae6 --- /dev/null +++ b/pkg/filenotify/filenotify.go @@ -0,0 +1,40 @@ +// Package filenotify provides a mechanism for watching file(s) for changes. +// Generally leans on fsnotify, but provides a poll-based notifier which fsnotify does not support. +// These are wrapped up in a common interface so that either can be used interchangeably in your code. +package filenotify + +import "gopkg.in/fsnotify.v1" + +// FileWatcher is an interface for implementing file notification watchers +type FileWatcher interface { + Events() <-chan fsnotify.Event + Errors() <-chan error + Add(name string) error + Remove(name string) error + Close() error +} + +// New tries to use an fs-event watcher, and falls back to the poller if there is an error +func New() (FileWatcher, error) { + if watcher, err := NewEventWatcher(); err == nil { + return watcher, nil + } + return NewPollingWatcher(), nil +} + +// NewPollingWatcher returns a poll-based file watcher +func NewPollingWatcher() FileWatcher { + return &filePoller{ + events: make(chan fsnotify.Event), + errors: make(chan error), + } +} + +// NewEventWatcher returns an fs-event based file watcher +func NewEventWatcher() (FileWatcher, error) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + return nil, err + } + return &fsNotifyWatcher{watcher}, nil +} diff --git a/pkg/filenotify/fsnotify.go b/pkg/filenotify/fsnotify.go new file mode 100644 index 00000000..42038835 --- /dev/null +++ b/pkg/filenotify/fsnotify.go @@ -0,0 +1,18 @@ +package filenotify + +import "gopkg.in/fsnotify.v1" + +// fsNotify wraps the fsnotify package to satisfy the FileNotifer interface +type fsNotifyWatcher struct { + *fsnotify.Watcher +} + +// GetEvents returns the fsnotify event channel receiver +func (w *fsNotifyWatcher) Events() <-chan fsnotify.Event { + return w.Watcher.Events +} + +// GetErrors returns the fsnotify error channel receiver +func (w *fsNotifyWatcher) Errors() <-chan error { + return w.Watcher.Errors +} diff --git a/pkg/filenotify/poller.go b/pkg/filenotify/poller.go new file mode 100644 index 00000000..52610853 --- /dev/null +++ b/pkg/filenotify/poller.go @@ -0,0 +1,204 @@ +package filenotify + +import ( + "errors" + "fmt" + "os" + "sync" + "time" + + "github.com/Sirupsen/logrus" + + "gopkg.in/fsnotify.v1" +) + +var ( + // errPollerClosed is returned when the poller is closed + errPollerClosed = errors.New("poller is closed") + // errNoSuchPoller is returned when trying to remove a watch that doesn't exist + errNoSuchWatch = errors.New("poller does not exist") +) + +// watchWaitTime is the time to wait between file poll loops +const watchWaitTime = 200 * time.Millisecond + +// filePoller is used to poll files for changes, especially in cases where fsnotify +// can't be run (e.g. when inotify handles are exhausted) +// filePoller satisfies the FileWatcher interface +type filePoller struct { + // watches is the list of files currently being polled, close the associated channel to stop the watch + watches map[string]chan struct{} + // events is the channel to listen to for watch events + events chan fsnotify.Event + // errors is the channel to listen to for watch errors + errors chan error + // mu locks the poller for modification + mu sync.Mutex + // closed is used to specify when the poller has already closed + closed bool +} + +// Add adds a filename to the list of watches +// once added the file is polled for changes in a separate goroutine +func (w *filePoller) Add(name string) error { + w.mu.Lock() + defer w.mu.Unlock() + + if w.closed == true { + return errPollerClosed + } + + f, err := os.Open(name) + if err != nil { + return err + } + fi, err := os.Stat(name) + if err != nil { + return err + } + + if w.watches == nil { + w.watches = make(map[string]chan struct{}) + } + if _, exists := w.watches[name]; exists { + return fmt.Errorf("watch exists") + } + chClose := make(chan struct{}) + w.watches[name] = chClose + + go w.watch(f, fi, chClose) + return nil +} + +// Remove stops and removes watch with the specified name +func (w *filePoller) Remove(name string) error { + w.mu.Lock() + defer w.mu.Unlock() + return w.remove(name) +} + +func (w *filePoller) remove(name string) error { + if w.closed == true { + return errPollerClosed + } + + chClose, exists := w.watches[name] + if !exists { + return errNoSuchWatch + } + close(chClose) + delete(w.watches, name) + return nil +} + +// Events returns the event channel +// This is used for notifications on events about watched files +func (w *filePoller) Events() <-chan fsnotify.Event { + return w.events +} + +// Errors returns the errors channel +// This is used for notifications about errors on watched files +func (w *filePoller) Errors() <-chan error { + return w.errors +} + +// Close closes the poller +// All watches are stopped, removed, and the poller cannot be added to +func (w *filePoller) Close() error { + w.mu.Lock() + defer w.mu.Unlock() + + if w.closed { + return nil + } + + w.closed = true + for name := range w.watches { + w.remove(name) + delete(w.watches, name) + } + return nil +} + +// sendEvent publishes the specified event to the events channel +func (w *filePoller) sendEvent(e fsnotify.Event, chClose <-chan struct{}) error { + select { + case w.events <- e: + case <-chClose: + return fmt.Errorf("closed") + } + return nil +} + +// sendErr publishes the specified error to the errors channel +func (w *filePoller) sendErr(e error, chClose <-chan struct{}) error { + select { + case w.errors <- e: + case <-chClose: + return fmt.Errorf("closed") + } + return nil +} + +// watch is responsible for polling the specified file for changes +// upon finding changes to a file or errors, sendEvent/sendErr is called +func (w *filePoller) watch(f *os.File, lastFi os.FileInfo, chClose chan struct{}) { + defer f.Close() + for { + time.Sleep(watchWaitTime) + select { + case <-chClose: + logrus.Debugf("watch for %s closed", f.Name()) + return + default: + } + + fi, err := os.Stat(f.Name()) + if err != nil { + // if we got an error here and lastFi is not set, we can presume that nothing has changed + // This should be safe since before `watch()` is called, a stat is performed, there is any error `watch` is not called + if lastFi == nil { + continue + } + // If it doesn't exist at this point, it must have been removed + // no need to send the error here since this is a valid operation + if os.IsNotExist(err) { + if err := w.sendEvent(fsnotify.Event{Op: fsnotify.Remove, Name: f.Name()}, chClose); err != nil { + return + } + lastFi = nil + continue + } + // at this point, send the error + if err := w.sendErr(err, chClose); err != nil { + return + } + continue + } + + if lastFi == nil { + if err := w.sendEvent(fsnotify.Event{Op: fsnotify.Create, Name: fi.Name()}, chClose); err != nil { + return + } + lastFi = fi + continue + } + + if fi.Mode() != lastFi.Mode() { + if err := w.sendEvent(fsnotify.Event{Op: fsnotify.Chmod, Name: fi.Name()}, chClose); err != nil { + return + } + lastFi = fi + continue + } + + if fi.ModTime() != lastFi.ModTime() || fi.Size() != lastFi.Size() { + if err := w.sendEvent(fsnotify.Event{Op: fsnotify.Write, Name: fi.Name()}, chClose); err != nil { + return + } + lastFi = fi + continue + } + } +} diff --git a/pkg/filenotify/poller_test.go b/pkg/filenotify/poller_test.go new file mode 100644 index 00000000..4f502623 --- /dev/null +++ b/pkg/filenotify/poller_test.go @@ -0,0 +1,119 @@ +package filenotify + +import ( + "fmt" + "io/ioutil" + "os" + "runtime" + "testing" + "time" + + "gopkg.in/fsnotify.v1" +) + +func TestPollerAddRemove(t *testing.T) { + w := NewPollingWatcher() + + if err := w.Add("no-such-file"); err == nil { + t.Fatal("should have gotten error when adding a non-existent file") + } + if err := w.Remove("no-such-file"); err == nil { + t.Fatal("should have gotten error when removing non-existent watch") + } + + f, err := ioutil.TempFile("", "asdf") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(f.Name()) + + if err := w.Add(f.Name()); err != nil { + t.Fatal(err) + } + + if err := w.Remove(f.Name()); err != nil { + t.Fatal(err) + } +} + +func TestPollerEvent(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("No chmod on Windows") + } + w := NewPollingWatcher() + + f, err := ioutil.TempFile("", "test-poller") + if err != nil { + t.Fatal("error creating temp file") + } + defer os.RemoveAll(f.Name()) + f.Close() + + if err := w.Add(f.Name()); err != nil { + t.Fatal(err) + } + + select { + case <-w.Events(): + t.Fatal("got event before anything happened") + case <-w.Errors(): + t.Fatal("got error before anything happened") + default: + } + + if err := ioutil.WriteFile(f.Name(), []byte("hello"), 644); err != nil { + t.Fatal(err) + } + if err := assertEvent(w, fsnotify.Write); err != nil { + t.Fatal(err) + } + + if err := os.Chmod(f.Name(), 600); err != nil { + t.Fatal(err) + } + if err := assertEvent(w, fsnotify.Chmod); err != nil { + t.Fatal(err) + } + + if err := os.Remove(f.Name()); err != nil { + t.Fatal(err) + } + if err := assertEvent(w, fsnotify.Remove); err != nil { + t.Fatal(err) + } +} + +func TestPollerClose(t *testing.T) { + w := NewPollingWatcher() + if err := w.Close(); err != nil { + t.Fatal(err) + } + // test double-close + if err := w.Close(); err != nil { + t.Fatal(err) + } + + f, err := ioutil.TempFile("", "asdf") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(f.Name()) + if err := w.Add(f.Name()); err == nil { + t.Fatal("should have gotten error adding watch for closed watcher") + } +} + +func assertEvent(w FileWatcher, eType fsnotify.Op) error { + var err error + select { + case e := <-w.Events(): + if e.Op != eType { + err = fmt.Errorf("got wrong event type, expected %q: %v", eType, e) + } + case e := <-w.Errors(): + err = fmt.Errorf("got unexpected error waiting for events %v: %v", eType, e) + case <-time.After(watchWaitTime * 3): + err = fmt.Errorf("timeout waiting for event %v", eType) + } + return err +} diff --git a/pkg/fileutils/fileutils.go b/pkg/fileutils/fileutils.go new file mode 100644 index 00000000..c1e309fe --- /dev/null +++ b/pkg/fileutils/fileutils.go @@ -0,0 +1,283 @@ +package fileutils + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strings" + "text/scanner" + + "github.com/Sirupsen/logrus" +) + +// exclusion return true if the specified pattern is an exclusion +func exclusion(pattern string) bool { + return pattern[0] == '!' +} + +// empty return true if the specified pattern is empty +func empty(pattern string) bool { + return pattern == "" +} + +// CleanPatterns takes a slice of patterns returns a new +// slice of patterns cleaned with filepath.Clean, stripped +// of any empty patterns and lets the caller know whether the +// slice contains any exception patterns (prefixed with !). +func CleanPatterns(patterns []string) ([]string, [][]string, bool, error) { + // Loop over exclusion patterns and: + // 1. Clean them up. + // 2. Indicate whether we are dealing with any exception rules. + // 3. Error if we see a single exclusion marker on it's own (!). + cleanedPatterns := []string{} + patternDirs := [][]string{} + exceptions := false + for _, pattern := range patterns { + // Eliminate leading and trailing whitespace. + pattern = strings.TrimSpace(pattern) + if empty(pattern) { + continue + } + if exclusion(pattern) { + if len(pattern) == 1 { + return nil, nil, false, errors.New("Illegal exclusion pattern: !") + } + exceptions = true + } + pattern = filepath.Clean(pattern) + cleanedPatterns = append(cleanedPatterns, pattern) + if exclusion(pattern) { + pattern = pattern[1:] + } + patternDirs = append(patternDirs, strings.Split(pattern, string(os.PathSeparator))) + } + + return cleanedPatterns, patternDirs, exceptions, nil +} + +// Matches returns true if file matches any of the patterns +// and isn't excluded by any of the subsequent patterns. +func Matches(file string, patterns []string) (bool, error) { + file = filepath.Clean(file) + + if file == "." { + // Don't let them exclude everything, kind of silly. + return false, nil + } + + patterns, patDirs, _, err := CleanPatterns(patterns) + if err != nil { + return false, err + } + + return OptimizedMatches(file, patterns, patDirs) +} + +// OptimizedMatches is basically the same as fileutils.Matches() but optimized for archive.go. +// It will assume that the inputs have been preprocessed and therefore the function +// doesn't need to do as much error checking and clean-up. This was done to avoid +// repeating these steps on each file being checked during the archive process. +// The more generic fileutils.Matches() can't make these assumptions. +func OptimizedMatches(file string, patterns []string, patDirs [][]string) (bool, error) { + matched := false + file = filepath.FromSlash(file) + parentPath := filepath.Dir(file) + parentPathDirs := strings.Split(parentPath, string(os.PathSeparator)) + + for i, pattern := range patterns { + negative := false + + if exclusion(pattern) { + negative = true + pattern = pattern[1:] + } + + match, err := regexpMatch(pattern, file) + if err != nil { + return false, fmt.Errorf("Error in pattern (%s): %s", pattern, err) + } + + if !match && parentPath != "." { + // Check to see if the pattern matches one of our parent dirs. + if len(patDirs[i]) <= len(parentPathDirs) { + match, _ = regexpMatch(strings.Join(patDirs[i], string(os.PathSeparator)), + strings.Join(parentPathDirs[:len(patDirs[i])], string(os.PathSeparator))) + } + } + + if match { + matched = !negative + } + } + + if matched { + logrus.Debugf("Skipping excluded path: %s", file) + } + + return matched, nil +} + +// regexpMatch tries to match the logic of filepath.Match but +// does so using regexp logic. We do this so that we can expand the +// wildcard set to include other things, like "**" to mean any number +// of directories. This means that we should be backwards compatible +// with filepath.Match(). We'll end up supporting more stuff, due to +// the fact that we're using regexp, but that's ok - it does no harm. +// +// As per the comment in golangs filepath.Match, on Windows, escaping +// is disabled. Instead, '\\' is treated as path separator. +func regexpMatch(pattern, path string) (bool, error) { + regStr := "^" + + // Do some syntax checking on the pattern. + // filepath's Match() has some really weird rules that are inconsistent + // so instead of trying to dup their logic, just call Match() for its + // error state and if there is an error in the pattern return it. + // If this becomes an issue we can remove this since its really only + // needed in the error (syntax) case - which isn't really critical. + if _, err := filepath.Match(pattern, path); err != nil { + return false, err + } + + // Go through the pattern and convert it to a regexp. + // We use a scanner so we can support utf-8 chars. + var scan scanner.Scanner + scan.Init(strings.NewReader(pattern)) + + sl := string(os.PathSeparator) + escSL := sl + if sl == `\` { + escSL += `\` + } + + for scan.Peek() != scanner.EOF { + ch := scan.Next() + + if ch == '*' { + if scan.Peek() == '*' { + // is some flavor of "**" + scan.Next() + + if scan.Peek() == scanner.EOF { + // is "**EOF" - to align with .gitignore just accept all + regStr += ".*" + } else { + // is "**" + regStr += "((.*" + escSL + ")|([^" + escSL + "]*))" + } + + // Treat **/ as ** so eat the "/" + if string(scan.Peek()) == sl { + scan.Next() + } + } else { + // is "*" so map it to anything but "/" + regStr += "[^" + escSL + "]*" + } + } else if ch == '?' { + // "?" is any char except "/" + regStr += "[^" + escSL + "]" + } else if strings.Index(".$", string(ch)) != -1 { + // Escape some regexp special chars that have no meaning + // in golang's filepath.Match + regStr += `\` + string(ch) + } else if ch == '\\' { + // escape next char. Note that a trailing \ in the pattern + // will be left alone (but need to escape it) + if sl == `\` { + // On windows map "\" to "\\", meaning an escaped backslash, + // and then just continue because filepath.Match on + // Windows doesn't allow escaping at all + regStr += escSL + continue + } + if scan.Peek() != scanner.EOF { + regStr += `\` + string(scan.Next()) + } else { + regStr += `\` + } + } else { + regStr += string(ch) + } + } + + regStr += "$" + + res, err := regexp.MatchString(regStr, path) + + // Map regexp's error to filepath's so no one knows we're not using filepath + if err != nil { + err = filepath.ErrBadPattern + } + + return res, err +} + +// CopyFile copies from src to dst until either EOF is reached +// on src or an error occurs. It verifies src exists and remove +// the dst if it exists. +func CopyFile(src, dst string) (int64, error) { + cleanSrc := filepath.Clean(src) + cleanDst := filepath.Clean(dst) + if cleanSrc == cleanDst { + return 0, nil + } + sf, err := os.Open(cleanSrc) + if err != nil { + return 0, err + } + defer sf.Close() + if err := os.Remove(cleanDst); err != nil && !os.IsNotExist(err) { + return 0, err + } + df, err := os.Create(cleanDst) + if err != nil { + return 0, err + } + defer df.Close() + return io.Copy(df, sf) +} + +// ReadSymlinkedDirectory returns the target directory of a symlink. +// The target of the symbolic link may not be a file. +func ReadSymlinkedDirectory(path string) (string, error) { + var realPath string + var err error + if realPath, err = filepath.Abs(path); err != nil { + return "", fmt.Errorf("unable to get absolute path for %s: %s", path, err) + } + if realPath, err = filepath.EvalSymlinks(realPath); err != nil { + return "", fmt.Errorf("failed to canonicalise path for %s: %s", path, err) + } + realPathInfo, err := os.Stat(realPath) + if err != nil { + return "", fmt.Errorf("failed to stat target '%s' of '%s': %s", realPath, path, err) + } + if !realPathInfo.Mode().IsDir() { + return "", fmt.Errorf("canonical path points to a file '%s'", realPath) + } + return realPath, nil +} + +// CreateIfNotExists creates a file or a directory only if it does not already exist. +func CreateIfNotExists(path string, isDir bool) error { + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + if isDir { + return os.MkdirAll(path, 0755) + } + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return err + } + f, err := os.OpenFile(path, os.O_CREATE, 0755) + if err != nil { + return err + } + f.Close() + } + } + return nil +} diff --git a/pkg/fileutils/fileutils_test.go b/pkg/fileutils/fileutils_test.go new file mode 100644 index 00000000..6df1be89 --- /dev/null +++ b/pkg/fileutils/fileutils_test.go @@ -0,0 +1,585 @@ +package fileutils + +import ( + "io/ioutil" + "os" + "path" + "path/filepath" + "runtime" + "strings" + "testing" +) + +// CopyFile with invalid src +func TestCopyFileWithInvalidSrc(t *testing.T) { + tempFolder, err := ioutil.TempDir("", "docker-fileutils-test") + defer os.RemoveAll(tempFolder) + if err != nil { + t.Fatal(err) + } + bytes, err := CopyFile("/invalid/file/path", path.Join(tempFolder, "dest")) + if err == nil { + t.Fatal("Should have fail to copy an invalid src file") + } + if bytes != 0 { + t.Fatal("Should have written 0 bytes") + } + +} + +// CopyFile with invalid dest +func TestCopyFileWithInvalidDest(t *testing.T) { + tempFolder, err := ioutil.TempDir("", "docker-fileutils-test") + defer os.RemoveAll(tempFolder) + if err != nil { + t.Fatal(err) + } + src := path.Join(tempFolder, "file") + err = ioutil.WriteFile(src, []byte("content"), 0740) + if err != nil { + t.Fatal(err) + } + bytes, err := CopyFile(src, path.Join(tempFolder, "/invalid/dest/path")) + if err == nil { + t.Fatal("Should have fail to copy an invalid src file") + } + if bytes != 0 { + t.Fatal("Should have written 0 bytes") + } + +} + +// CopyFile with same src and dest +func TestCopyFileWithSameSrcAndDest(t *testing.T) { + tempFolder, err := ioutil.TempDir("", "docker-fileutils-test") + defer os.RemoveAll(tempFolder) + if err != nil { + t.Fatal(err) + } + file := path.Join(tempFolder, "file") + err = ioutil.WriteFile(file, []byte("content"), 0740) + if err != nil { + t.Fatal(err) + } + bytes, err := CopyFile(file, file) + if err != nil { + t.Fatal(err) + } + if bytes != 0 { + t.Fatal("Should have written 0 bytes as it is the same file.") + } +} + +// CopyFile with same src and dest but path is different and not clean +func TestCopyFileWithSameSrcAndDestWithPathNameDifferent(t *testing.T) { + tempFolder, err := ioutil.TempDir("", "docker-fileutils-test") + defer os.RemoveAll(tempFolder) + if err != nil { + t.Fatal(err) + } + testFolder := path.Join(tempFolder, "test") + err = os.MkdirAll(testFolder, 0740) + if err != nil { + t.Fatal(err) + } + file := path.Join(testFolder, "file") + sameFile := testFolder + "/../test/file" + err = ioutil.WriteFile(file, []byte("content"), 0740) + if err != nil { + t.Fatal(err) + } + bytes, err := CopyFile(file, sameFile) + if err != nil { + t.Fatal(err) + } + if bytes != 0 { + t.Fatal("Should have written 0 bytes as it is the same file.") + } +} + +func TestCopyFile(t *testing.T) { + tempFolder, err := ioutil.TempDir("", "docker-fileutils-test") + defer os.RemoveAll(tempFolder) + if err != nil { + t.Fatal(err) + } + src := path.Join(tempFolder, "src") + dest := path.Join(tempFolder, "dest") + ioutil.WriteFile(src, []byte("content"), 0777) + ioutil.WriteFile(dest, []byte("destContent"), 0777) + bytes, err := CopyFile(src, dest) + if err != nil { + t.Fatal(err) + } + if bytes != 7 { + t.Fatalf("Should have written %d bytes but wrote %d", 7, bytes) + } + actual, err := ioutil.ReadFile(dest) + if err != nil { + t.Fatal(err) + } + if string(actual) != "content" { + t.Fatalf("Dest content was '%s', expected '%s'", string(actual), "content") + } +} + +// Reading a symlink to a directory must return the directory +func TestReadSymlinkedDirectoryExistingDirectory(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + var err error + if err = os.Mkdir("/tmp/testReadSymlinkToExistingDirectory", 0777); err != nil { + t.Errorf("failed to create directory: %s", err) + } + + if err = os.Symlink("/tmp/testReadSymlinkToExistingDirectory", "/tmp/dirLinkTest"); err != nil { + t.Errorf("failed to create symlink: %s", err) + } + + var path string + if path, err = ReadSymlinkedDirectory("/tmp/dirLinkTest"); err != nil { + t.Fatalf("failed to read symlink to directory: %s", err) + } + + if path != "/tmp/testReadSymlinkToExistingDirectory" { + t.Fatalf("symlink returned unexpected directory: %s", path) + } + + if err = os.Remove("/tmp/testReadSymlinkToExistingDirectory"); err != nil { + t.Errorf("failed to remove temporary directory: %s", err) + } + + if err = os.Remove("/tmp/dirLinkTest"); err != nil { + t.Errorf("failed to remove symlink: %s", err) + } +} + +// Reading a non-existing symlink must fail +func TestReadSymlinkedDirectoryNonExistingSymlink(t *testing.T) { + var path string + var err error + if path, err = ReadSymlinkedDirectory("/tmp/test/foo/Non/ExistingPath"); err == nil { + t.Fatalf("error expected for non-existing symlink") + } + + if path != "" { + t.Fatalf("expected empty path, but '%s' was returned", path) + } +} + +// Reading a symlink to a file must fail +func TestReadSymlinkedDirectoryToFile(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + var err error + var file *os.File + + if file, err = os.Create("/tmp/testReadSymlinkToFile"); err != nil { + t.Fatalf("failed to create file: %s", err) + } + + file.Close() + + if err = os.Symlink("/tmp/testReadSymlinkToFile", "/tmp/fileLinkTest"); err != nil { + t.Errorf("failed to create symlink: %s", err) + } + + var path string + if path, err = ReadSymlinkedDirectory("/tmp/fileLinkTest"); err == nil { + t.Fatalf("ReadSymlinkedDirectory on a symlink to a file should've failed") + } + + if path != "" { + t.Fatalf("path should've been empty: %s", path) + } + + if err = os.Remove("/tmp/testReadSymlinkToFile"); err != nil { + t.Errorf("failed to remove file: %s", err) + } + + if err = os.Remove("/tmp/fileLinkTest"); err != nil { + t.Errorf("failed to remove symlink: %s", err) + } +} + +func TestWildcardMatches(t *testing.T) { + match, _ := Matches("fileutils.go", []string{"*"}) + if match != true { + t.Errorf("failed to get a wildcard match, got %v", match) + } +} + +// A simple pattern match should return true. +func TestPatternMatches(t *testing.T) { + match, _ := Matches("fileutils.go", []string{"*.go"}) + if match != true { + t.Errorf("failed to get a match, got %v", match) + } +} + +// An exclusion followed by an inclusion should return true. +func TestExclusionPatternMatchesPatternBefore(t *testing.T) { + match, _ := Matches("fileutils.go", []string{"!fileutils.go", "*.go"}) + if match != true { + t.Errorf("failed to get true match on exclusion pattern, got %v", match) + } +} + +// A folder pattern followed by an exception should return false. +func TestPatternMatchesFolderExclusions(t *testing.T) { + match, _ := Matches("docs/README.md", []string{"docs", "!docs/README.md"}) + if match != false { + t.Errorf("failed to get a false match on exclusion pattern, got %v", match) + } +} + +// A folder pattern followed by an exception should return false. +func TestPatternMatchesFolderWithSlashExclusions(t *testing.T) { + match, _ := Matches("docs/README.md", []string{"docs/", "!docs/README.md"}) + if match != false { + t.Errorf("failed to get a false match on exclusion pattern, got %v", match) + } +} + +// A folder pattern followed by an exception should return false. +func TestPatternMatchesFolderWildcardExclusions(t *testing.T) { + match, _ := Matches("docs/README.md", []string{"docs/*", "!docs/README.md"}) + if match != false { + t.Errorf("failed to get a false match on exclusion pattern, got %v", match) + } +} + +// A pattern followed by an exclusion should return false. +func TestExclusionPatternMatchesPatternAfter(t *testing.T) { + match, _ := Matches("fileutils.go", []string{"*.go", "!fileutils.go"}) + if match != false { + t.Errorf("failed to get false match on exclusion pattern, got %v", match) + } +} + +// A filename evaluating to . should return false. +func TestExclusionPatternMatchesWholeDirectory(t *testing.T) { + match, _ := Matches(".", []string{"*.go"}) + if match != false { + t.Errorf("failed to get false match on ., got %v", match) + } +} + +// A single ! pattern should return an error. +func TestSingleExclamationError(t *testing.T) { + _, err := Matches("fileutils.go", []string{"!"}) + if err == nil { + t.Errorf("failed to get an error for a single exclamation point, got %v", err) + } +} + +// A string preceded with a ! should return true from Exclusion. +func TestExclusion(t *testing.T) { + exclusion := exclusion("!") + if !exclusion { + t.Errorf("failed to get true for a single !, got %v", exclusion) + } +} + +// Matches with no patterns +func TestMatchesWithNoPatterns(t *testing.T) { + matches, err := Matches("/any/path/there", []string{}) + if err != nil { + t.Fatal(err) + } + if matches { + t.Fatalf("Should not have match anything") + } +} + +// Matches with malformed patterns +func TestMatchesWithMalformedPatterns(t *testing.T) { + matches, err := Matches("/any/path/there", []string{"["}) + if err == nil { + t.Fatal("Should have failed because of a malformed syntax in the pattern") + } + if matches { + t.Fatalf("Should not have match anything") + } +} + +// Test lots of variants of patterns & strings +func TestMatches(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + tests := []struct { + pattern string + text string + pass bool + }{ + {"**", "file", true}, + {"**", "file/", true}, + {"**/", "file", true}, // weird one + {"**/", "file/", true}, + {"**", "/", true}, + {"**/", "/", true}, + {"**", "dir/file", true}, + {"**/", "dir/file", false}, + {"**", "dir/file/", true}, + {"**/", "dir/file/", true}, + {"**/**", "dir/file", true}, + {"**/**", "dir/file/", true}, + {"dir/**", "dir/file", true}, + {"dir/**", "dir/file/", true}, + {"dir/**", "dir/dir2/file", true}, + {"dir/**", "dir/dir2/file/", true}, + {"**/dir2/*", "dir/dir2/file", true}, + {"**/dir2/*", "dir/dir2/file/", false}, + {"**/dir2/**", "dir/dir2/dir3/file", true}, + {"**/dir2/**", "dir/dir2/dir3/file/", true}, + {"**file", "file", true}, + {"**file", "dir/file", true}, + {"**/file", "dir/file", true}, + {"**file", "dir/dir/file", true}, + {"**/file", "dir/dir/file", true}, + {"**/file*", "dir/dir/file", true}, + {"**/file*", "dir/dir/file.txt", true}, + {"**/file*txt", "dir/dir/file.txt", true}, + {"**/file*.txt", "dir/dir/file.txt", true}, + {"**/file*.txt*", "dir/dir/file.txt", true}, + {"**/**/*.txt", "dir/dir/file.txt", true}, + {"**/**/*.txt2", "dir/dir/file.txt", false}, + {"**/*.txt", "file.txt", true}, + {"**/**/*.txt", "file.txt", true}, + {"a**/*.txt", "a/file.txt", true}, + {"a**/*.txt", "a/dir/file.txt", true}, + {"a**/*.txt", "a/dir/dir/file.txt", true}, + {"a/*.txt", "a/dir/file.txt", false}, + {"a/*.txt", "a/file.txt", true}, + {"a/*.txt**", "a/file.txt", true}, + {"a[b-d]e", "ae", false}, + {"a[b-d]e", "ace", true}, + {"a[b-d]e", "aae", false}, + {"a[^b-d]e", "aze", true}, + {".*", ".foo", true}, + {".*", "foo", false}, + {"abc.def", "abcdef", false}, + {"abc.def", "abc.def", true}, + {"abc.def", "abcZdef", false}, + {"abc?def", "abcZdef", true}, + {"abc?def", "abcdef", false}, + {"a\\*b", "a*b", true}, + {"a\\", "a", false}, + {"a\\", "a\\", false}, + {"a\\\\", "a\\", true}, + {"**/foo/bar", "foo/bar", true}, + {"**/foo/bar", "dir/foo/bar", true}, + {"**/foo/bar", "dir/dir2/foo/bar", true}, + {"abc/**", "abc", false}, + {"abc/**", "abc/def", true}, + {"abc/**", "abc/def/ghi", true}, + } + + for _, test := range tests { + res, _ := regexpMatch(test.pattern, test.text) + if res != test.pass { + t.Fatalf("Failed: %v - res:%v", test, res) + } + } +} + +// An empty string should return true from Empty. +func TestEmpty(t *testing.T) { + empty := empty("") + if !empty { + t.Errorf("failed to get true for an empty string, got %v", empty) + } +} + +func TestCleanPatterns(t *testing.T) { + cleaned, _, _, _ := CleanPatterns([]string{"docs", "config"}) + if len(cleaned) != 2 { + t.Errorf("expected 2 element slice, got %v", len(cleaned)) + } +} + +func TestCleanPatternsStripEmptyPatterns(t *testing.T) { + cleaned, _, _, _ := CleanPatterns([]string{"docs", "config", ""}) + if len(cleaned) != 2 { + t.Errorf("expected 2 element slice, got %v", len(cleaned)) + } +} + +func TestCleanPatternsExceptionFlag(t *testing.T) { + _, _, exceptions, _ := CleanPatterns([]string{"docs", "!docs/README.md"}) + if !exceptions { + t.Errorf("expected exceptions to be true, got %v", exceptions) + } +} + +func TestCleanPatternsLeadingSpaceTrimmed(t *testing.T) { + _, _, exceptions, _ := CleanPatterns([]string{"docs", " !docs/README.md"}) + if !exceptions { + t.Errorf("expected exceptions to be true, got %v", exceptions) + } +} + +func TestCleanPatternsTrailingSpaceTrimmed(t *testing.T) { + _, _, exceptions, _ := CleanPatterns([]string{"docs", "!docs/README.md "}) + if !exceptions { + t.Errorf("expected exceptions to be true, got %v", exceptions) + } +} + +func TestCleanPatternsErrorSingleException(t *testing.T) { + _, _, _, err := CleanPatterns([]string{"!"}) + if err == nil { + t.Errorf("expected error on single exclamation point, got %v", err) + } +} + +func TestCleanPatternsFolderSplit(t *testing.T) { + _, dirs, _, _ := CleanPatterns([]string{"docs/config/CONFIG.md"}) + if dirs[0][0] != "docs" { + t.Errorf("expected first element in dirs slice to be docs, got %v", dirs[0][1]) + } + if dirs[0][1] != "config" { + t.Errorf("expected first element in dirs slice to be config, got %v", dirs[0][1]) + } +} + +func TestCreateIfNotExistsDir(t *testing.T) { + tempFolder, err := ioutil.TempDir("", "docker-fileutils-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempFolder) + + folderToCreate := filepath.Join(tempFolder, "tocreate") + + if err := CreateIfNotExists(folderToCreate, true); err != nil { + t.Fatal(err) + } + fileinfo, err := os.Stat(folderToCreate) + if err != nil { + t.Fatalf("Should have create a folder, got %v", err) + } + + if !fileinfo.IsDir() { + t.Fatalf("Should have been a dir, seems it's not") + } +} + +func TestCreateIfNotExistsFile(t *testing.T) { + tempFolder, err := ioutil.TempDir("", "docker-fileutils-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempFolder) + + fileToCreate := filepath.Join(tempFolder, "file/to/create") + + if err := CreateIfNotExists(fileToCreate, false); err != nil { + t.Fatal(err) + } + fileinfo, err := os.Stat(fileToCreate) + if err != nil { + t.Fatalf("Should have create a file, got %v", err) + } + + if fileinfo.IsDir() { + t.Fatalf("Should have been a file, seems it's not") + } +} + +// These matchTests are stolen from go's filepath Match tests. +type matchTest struct { + pattern, s string + match bool + err error +} + +var matchTests = []matchTest{ + {"abc", "abc", true, nil}, + {"*", "abc", true, nil}, + {"*c", "abc", true, nil}, + {"a*", "a", true, nil}, + {"a*", "abc", true, nil}, + {"a*", "ab/c", false, nil}, + {"a*/b", "abc/b", true, nil}, + {"a*/b", "a/c/b", false, nil}, + {"a*b*c*d*e*/f", "axbxcxdxe/f", true, nil}, + {"a*b*c*d*e*/f", "axbxcxdxexxx/f", true, nil}, + {"a*b*c*d*e*/f", "axbxcxdxe/xxx/f", false, nil}, + {"a*b*c*d*e*/f", "axbxcxdxexxx/fff", false, nil}, + {"a*b?c*x", "abxbbxdbxebxczzx", true, nil}, + {"a*b?c*x", "abxbbxdbxebxczzy", false, nil}, + {"ab[c]", "abc", true, nil}, + {"ab[b-d]", "abc", true, nil}, + {"ab[e-g]", "abc", false, nil}, + {"ab[^c]", "abc", false, nil}, + {"ab[^b-d]", "abc", false, nil}, + {"ab[^e-g]", "abc", true, nil}, + {"a\\*b", "a*b", true, nil}, + {"a\\*b", "ab", false, nil}, + {"a?b", "a☺b", true, nil}, + {"a[^a]b", "a☺b", true, nil}, + {"a???b", "a☺b", false, nil}, + {"a[^a][^a][^a]b", "a☺b", false, nil}, + {"[a-ζ]*", "α", true, nil}, + {"*[a-ζ]", "A", false, nil}, + {"a?b", "a/b", false, nil}, + {"a*b", "a/b", false, nil}, + {"[\\]a]", "]", true, nil}, + {"[\\-]", "-", true, nil}, + {"[x\\-]", "x", true, nil}, + {"[x\\-]", "-", true, nil}, + {"[x\\-]", "z", false, nil}, + {"[\\-x]", "x", true, nil}, + {"[\\-x]", "-", true, nil}, + {"[\\-x]", "a", false, nil}, + {"[]a]", "]", false, filepath.ErrBadPattern}, + {"[-]", "-", false, filepath.ErrBadPattern}, + {"[x-]", "x", false, filepath.ErrBadPattern}, + {"[x-]", "-", false, filepath.ErrBadPattern}, + {"[x-]", "z", false, filepath.ErrBadPattern}, + {"[-x]", "x", false, filepath.ErrBadPattern}, + {"[-x]", "-", false, filepath.ErrBadPattern}, + {"[-x]", "a", false, filepath.ErrBadPattern}, + {"\\", "a", false, filepath.ErrBadPattern}, + {"[a-b-c]", "a", false, filepath.ErrBadPattern}, + {"[", "a", false, filepath.ErrBadPattern}, + {"[^", "a", false, filepath.ErrBadPattern}, + {"[^bc", "a", false, filepath.ErrBadPattern}, + {"a[", "a", false, filepath.ErrBadPattern}, // was nil but IMO its wrong + {"a[", "ab", false, filepath.ErrBadPattern}, + {"*x", "xxx", true, nil}, +} + +func errp(e error) string { + if e == nil { + return "" + } + return e.Error() +} + +// TestMatch test's our version of filepath.Match, called regexpMatch. +func TestMatch(t *testing.T) { + for _, tt := range matchTests { + pattern := tt.pattern + s := tt.s + if runtime.GOOS == "windows" { + if strings.Index(pattern, "\\") >= 0 { + // no escape allowed on windows. + continue + } + pattern = filepath.Clean(pattern) + s = filepath.Clean(s) + } + ok, err := regexpMatch(pattern, s) + if ok != tt.match || err != tt.err { + t.Fatalf("Match(%#q, %#q) = %v, %q want %v, %q", pattern, s, ok, errp(err), tt.match, errp(tt.err)) + } + } +} diff --git a/pkg/fileutils/fileutils_unix.go b/pkg/fileutils/fileutils_unix.go new file mode 100644 index 00000000..d5c3abf5 --- /dev/null +++ b/pkg/fileutils/fileutils_unix.go @@ -0,0 +1,22 @@ +// +build linux freebsd + +package fileutils + +import ( + "fmt" + "io/ioutil" + "os" + + "github.com/Sirupsen/logrus" +) + +// GetTotalUsedFds Returns the number of used File Descriptors by +// reading it via /proc filesystem. +func GetTotalUsedFds() int { + if fds, err := ioutil.ReadDir(fmt.Sprintf("/proc/%d/fd", os.Getpid())); err != nil { + logrus.Errorf("Error opening /proc/%d/fd: %s", os.Getpid(), err) + } else { + return len(fds) + } + return -1 +} diff --git a/pkg/fileutils/fileutils_windows.go b/pkg/fileutils/fileutils_windows.go new file mode 100644 index 00000000..5ec21cac --- /dev/null +++ b/pkg/fileutils/fileutils_windows.go @@ -0,0 +1,7 @@ +package fileutils + +// GetTotalUsedFds Returns the number of used File Descriptors. Not supported +// on Windows. +func GetTotalUsedFds() int { + return -1 +} diff --git a/pkg/gitutils/gitutils.go b/pkg/gitutils/gitutils.go new file mode 100644 index 00000000..ded091f2 --- /dev/null +++ b/pkg/gitutils/gitutils.go @@ -0,0 +1,100 @@ +package gitutils + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/docker/docker/pkg/symlink" + "github.com/docker/docker/pkg/urlutil" +) + +// Clone clones a repository into a newly created directory which +// will be under "docker-build-git" +func Clone(remoteURL string) (string, error) { + if !urlutil.IsGitTransport(remoteURL) { + remoteURL = "https://" + remoteURL + } + root, err := ioutil.TempDir("", "docker-build-git") + if err != nil { + return "", err + } + + u, err := url.Parse(remoteURL) + if err != nil { + return "", err + } + + fragment := u.Fragment + clone := cloneArgs(u, root) + + if output, err := git(clone...); err != nil { + return "", fmt.Errorf("Error trying to use git: %s (%s)", err, output) + } + + return checkoutGit(fragment, root) +} + +func cloneArgs(remoteURL *url.URL, root string) []string { + args := []string{"clone", "--recursive"} + shallow := len(remoteURL.Fragment) == 0 + + if shallow && strings.HasPrefix(remoteURL.Scheme, "http") { + res, err := http.Head(fmt.Sprintf("%s/info/refs?service=git-upload-pack", remoteURL)) + if err != nil || res.Header.Get("Content-Type") != "application/x-git-upload-pack-advertisement" { + shallow = false + } + } + + if shallow { + args = append(args, "--depth", "1") + } + + if remoteURL.Fragment != "" { + remoteURL.Fragment = "" + } + + return append(args, remoteURL.String(), root) +} + +func checkoutGit(fragment, root string) (string, error) { + refAndDir := strings.SplitN(fragment, ":", 2) + + if len(refAndDir[0]) != 0 { + if output, err := gitWithinDir(root, "checkout", refAndDir[0]); err != nil { + return "", fmt.Errorf("Error trying to use git: %s (%s)", err, output) + } + } + + if len(refAndDir) > 1 && len(refAndDir[1]) != 0 { + newCtx, err := symlink.FollowSymlinkInScope(filepath.Join(root, refAndDir[1]), root) + if err != nil { + return "", fmt.Errorf("Error setting git context, %q not within git root: %s", refAndDir[1], err) + } + + fi, err := os.Stat(newCtx) + if err != nil { + return "", err + } + if !fi.IsDir() { + return "", fmt.Errorf("Error setting git context, not a directory: %s", newCtx) + } + root = newCtx + } + + return root, nil +} + +func gitWithinDir(dir string, args ...string) ([]byte, error) { + a := []string{"--work-tree", dir, "--git-dir", filepath.Join(dir, ".git")} + return git(append(a, args...)...) +} + +func git(args ...string) ([]byte, error) { + return exec.Command("git", args...).CombinedOutput() +} diff --git a/pkg/gitutils/gitutils_test.go b/pkg/gitutils/gitutils_test.go new file mode 100644 index 00000000..ec288f00 --- /dev/null +++ b/pkg/gitutils/gitutils_test.go @@ -0,0 +1,204 @@ +package gitutils + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "reflect" + "runtime" + "testing" +) + +func TestCloneArgsSmartHttp(t *testing.T) { + mux := http.NewServeMux() + server := httptest.NewServer(mux) + serverURL, _ := url.Parse(server.URL) + + serverURL.Path = "/repo.git" + gitURL := serverURL.String() + + mux.HandleFunc("/repo.git/info/refs", func(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query().Get("service") + w.Header().Set("Content-Type", fmt.Sprintf("application/x-%s-advertisement", q)) + }) + + args := cloneArgs(serverURL, "/tmp") + exp := []string{"clone", "--recursive", "--depth", "1", gitURL, "/tmp"} + if !reflect.DeepEqual(args, exp) { + t.Fatalf("Expected %v, got %v", exp, args) + } +} + +func TestCloneArgsDumbHttp(t *testing.T) { + mux := http.NewServeMux() + server := httptest.NewServer(mux) + serverURL, _ := url.Parse(server.URL) + + serverURL.Path = "/repo.git" + gitURL := serverURL.String() + + mux.HandleFunc("/repo.git/info/refs", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + }) + + args := cloneArgs(serverURL, "/tmp") + exp := []string{"clone", "--recursive", gitURL, "/tmp"} + if !reflect.DeepEqual(args, exp) { + t.Fatalf("Expected %v, got %v", exp, args) + } +} + +func TestCloneArgsGit(t *testing.T) { + u, _ := url.Parse("git://github.com/docker/docker") + args := cloneArgs(u, "/tmp") + exp := []string{"clone", "--recursive", "--depth", "1", "git://github.com/docker/docker", "/tmp"} + if !reflect.DeepEqual(args, exp) { + t.Fatalf("Expected %v, got %v", exp, args) + } +} + +func TestCloneArgsStripFragment(t *testing.T) { + u, _ := url.Parse("git://github.com/docker/docker#test") + args := cloneArgs(u, "/tmp") + exp := []string{"clone", "--recursive", "git://github.com/docker/docker", "/tmp"} + if !reflect.DeepEqual(args, exp) { + t.Fatalf("Expected %v, got %v", exp, args) + } +} + +func TestCheckoutGit(t *testing.T) { + root, err := ioutil.TempDir("", "docker-build-git-checkout") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(root) + + eol := "\n" + if runtime.GOOS == "windows" { + eol = "\r\n" + } + + gitDir := filepath.Join(root, "repo") + _, err = git("init", gitDir) + if err != nil { + t.Fatal(err) + } + + if _, err = gitWithinDir(gitDir, "config", "user.email", "test@docker.com"); err != nil { + t.Fatal(err) + } + + if _, err = gitWithinDir(gitDir, "config", "user.name", "Docker test"); err != nil { + t.Fatal(err) + } + + if err = ioutil.WriteFile(filepath.Join(gitDir, "Dockerfile"), []byte("FROM scratch"), 0644); err != nil { + t.Fatal(err) + } + + subDir := filepath.Join(gitDir, "subdir") + if err = os.Mkdir(subDir, 0755); err != nil { + t.Fatal(err) + } + + if err = ioutil.WriteFile(filepath.Join(subDir, "Dockerfile"), []byte("FROM scratch\nEXPOSE 5000"), 0644); err != nil { + t.Fatal(err) + } + + if runtime.GOOS != "windows" { + if err = os.Symlink("../subdir", filepath.Join(gitDir, "parentlink")); err != nil { + t.Fatal(err) + } + + if err = os.Symlink("/subdir", filepath.Join(gitDir, "absolutelink")); err != nil { + t.Fatal(err) + } + } + + if _, err = gitWithinDir(gitDir, "add", "-A"); err != nil { + t.Fatal(err) + } + + if _, err = gitWithinDir(gitDir, "commit", "-am", "First commit"); err != nil { + t.Fatal(err) + } + + if _, err = gitWithinDir(gitDir, "checkout", "-b", "test"); err != nil { + t.Fatal(err) + } + + if err = ioutil.WriteFile(filepath.Join(gitDir, "Dockerfile"), []byte("FROM scratch\nEXPOSE 3000"), 0644); err != nil { + t.Fatal(err) + } + + if err = ioutil.WriteFile(filepath.Join(subDir, "Dockerfile"), []byte("FROM busybox\nEXPOSE 5000"), 0644); err != nil { + t.Fatal(err) + } + + if _, err = gitWithinDir(gitDir, "add", "-A"); err != nil { + t.Fatal(err) + } + + if _, err = gitWithinDir(gitDir, "commit", "-am", "Branch commit"); err != nil { + t.Fatal(err) + } + + if _, err = gitWithinDir(gitDir, "checkout", "master"); err != nil { + t.Fatal(err) + } + + type singleCase struct { + frag string + exp string + fail bool + } + + cases := []singleCase{ + {"", "FROM scratch", false}, + {"master", "FROM scratch", false}, + {":subdir", "FROM scratch" + eol + "EXPOSE 5000", false}, + {":nosubdir", "", true}, // missing directory error + {":Dockerfile", "", true}, // not a directory error + {"master:nosubdir", "", true}, + {"master:subdir", "FROM scratch" + eol + "EXPOSE 5000", false}, + {"master:../subdir", "", true}, + {"test", "FROM scratch" + eol + "EXPOSE 3000", false}, + {"test:", "FROM scratch" + eol + "EXPOSE 3000", false}, + {"test:subdir", "FROM busybox" + eol + "EXPOSE 5000", false}, + } + + if runtime.GOOS != "windows" { + // Windows GIT (2.7.1 x64) does not support parentlink/absolutelink. Sample output below + // git --work-tree .\repo --git-dir .\repo\.git add -A + // error: readlink("absolutelink"): Function not implemented + // error: unable to index file absolutelink + // fatal: adding files failed + cases = append(cases, singleCase{frag: "master:absolutelink", exp: "FROM scratch" + eol + "EXPOSE 5000", fail: false}) + cases = append(cases, singleCase{frag: "master:parentlink", exp: "FROM scratch" + eol + "EXPOSE 5000", fail: false}) + } + + for _, c := range cases { + r, err := checkoutGit(c.frag, gitDir) + + fail := err != nil + if fail != c.fail { + t.Fatalf("Expected %v failure, error was %v\n", c.fail, err) + } + if c.fail { + continue + } + + b, err := ioutil.ReadFile(filepath.Join(r, "Dockerfile")) + if err != nil { + t.Fatal(err) + } + + if string(b) != c.exp { + t.Fatalf("Expected %v, was %v\n", c.exp, string(b)) + } + } +} diff --git a/pkg/graphdb/conn_sqlite3.go b/pkg/graphdb/conn_sqlite3.go new file mode 100644 index 00000000..dbcf44c2 --- /dev/null +++ b/pkg/graphdb/conn_sqlite3.go @@ -0,0 +1,15 @@ +// +build cgo + +package graphdb + +import "database/sql" + +// NewSqliteConn opens a connection to a sqlite +// database. +func NewSqliteConn(root string) (*Database, error) { + conn, err := sql.Open("sqlite3", root) + if err != nil { + return nil, err + } + return NewDatabase(conn) +} diff --git a/pkg/graphdb/conn_sqlite3_unix.go b/pkg/graphdb/conn_sqlite3_unix.go new file mode 100644 index 00000000..f932fff2 --- /dev/null +++ b/pkg/graphdb/conn_sqlite3_unix.go @@ -0,0 +1,7 @@ +// +build cgo,!windows + +package graphdb + +import ( + _ "github.com/mattn/go-sqlite3" // registers sqlite +) diff --git a/pkg/graphdb/conn_sqlite3_windows.go b/pkg/graphdb/conn_sqlite3_windows.go new file mode 100644 index 00000000..52590303 --- /dev/null +++ b/pkg/graphdb/conn_sqlite3_windows.go @@ -0,0 +1,7 @@ +// +build cgo,windows + +package graphdb + +import ( + _ "github.com/mattn/go-sqlite3" // registers sqlite +) diff --git a/pkg/graphdb/conn_unsupported.go b/pkg/graphdb/conn_unsupported.go new file mode 100644 index 00000000..cf977050 --- /dev/null +++ b/pkg/graphdb/conn_unsupported.go @@ -0,0 +1,8 @@ +// +build !cgo + +package graphdb + +// NewSqliteConn return a new sqlite connection. +func NewSqliteConn(root string) (*Database, error) { + panic("Not implemented") +} diff --git a/pkg/graphdb/graphdb.go b/pkg/graphdb/graphdb.go new file mode 100644 index 00000000..eca433fa --- /dev/null +++ b/pkg/graphdb/graphdb.go @@ -0,0 +1,551 @@ +package graphdb + +import ( + "database/sql" + "fmt" + "path" + "strings" + "sync" +) + +const ( + createEntityTable = ` + CREATE TABLE IF NOT EXISTS entity ( + id text NOT NULL PRIMARY KEY + );` + + createEdgeTable = ` + CREATE TABLE IF NOT EXISTS edge ( + "entity_id" text NOT NULL, + "parent_id" text NULL, + "name" text NOT NULL, + CONSTRAINT "parent_fk" FOREIGN KEY ("parent_id") REFERENCES "entity" ("id"), + CONSTRAINT "entity_fk" FOREIGN KEY ("entity_id") REFERENCES "entity" ("id") + ); + ` + + createEdgeIndices = ` + CREATE UNIQUE INDEX IF NOT EXISTS "name_parent_ix" ON "edge" (parent_id, name); + ` +) + +// Entity with a unique id. +type Entity struct { + id string +} + +// An Edge connects two entities together. +type Edge struct { + EntityID string + Name string + ParentID string +} + +// Entities stores the list of entities. +type Entities map[string]*Entity + +// Edges stores the relationships between entities. +type Edges []*Edge + +// WalkFunc is a function invoked to process an individual entity. +type WalkFunc func(fullPath string, entity *Entity) error + +// Database is a graph database for storing entities and their relationships. +type Database struct { + conn *sql.DB + mux sync.RWMutex +} + +// IsNonUniqueNameError processes the error to check if it's caused by +// a constraint violation. +// This is necessary because the error isn't the same across various +// sqlite versions. +func IsNonUniqueNameError(err error) bool { + str := err.Error() + // sqlite 3.7.17-1ubuntu1 returns: + // Set failure: Abort due to constraint violation: columns parent_id, name are not unique + if strings.HasSuffix(str, "name are not unique") { + return true + } + // sqlite-3.8.3-1.fc20 returns: + // Set failure: Abort due to constraint violation: UNIQUE constraint failed: edge.parent_id, edge.name + if strings.Contains(str, "UNIQUE constraint failed") && strings.Contains(str, "edge.name") { + return true + } + // sqlite-3.6.20-1.el6 returns: + // Set failure: Abort due to constraint violation: constraint failed + if strings.HasSuffix(str, "constraint failed") { + return true + } + return false +} + +// NewDatabase creates a new graph database initialized with a root entity. +func NewDatabase(conn *sql.DB) (*Database, error) { + if conn == nil { + return nil, fmt.Errorf("Database connection cannot be nil") + } + db := &Database{conn: conn} + + // Create root entities + tx, err := conn.Begin() + if err != nil { + return nil, err + } + + if _, err := tx.Exec(createEntityTable); err != nil { + return nil, err + } + if _, err := tx.Exec(createEdgeTable); err != nil { + return nil, err + } + if _, err := tx.Exec(createEdgeIndices); err != nil { + return nil, err + } + + if _, err := tx.Exec("DELETE FROM entity where id = ?", "0"); err != nil { + tx.Rollback() + return nil, err + } + + if _, err := tx.Exec("INSERT INTO entity (id) VALUES (?);", "0"); err != nil { + tx.Rollback() + return nil, err + } + + if _, err := tx.Exec("DELETE FROM edge where entity_id=? and name=?", "0", "/"); err != nil { + tx.Rollback() + return nil, err + } + + if _, err := tx.Exec("INSERT INTO edge (entity_id, name) VALUES(?,?);", "0", "/"); err != nil { + tx.Rollback() + return nil, err + } + + if err := tx.Commit(); err != nil { + return nil, err + } + + return db, nil +} + +// Close the underlying connection to the database. +func (db *Database) Close() error { + return db.conn.Close() +} + +// Set the entity id for a given path. +func (db *Database) Set(fullPath, id string) (*Entity, error) { + db.mux.Lock() + defer db.mux.Unlock() + + tx, err := db.conn.Begin() + if err != nil { + return nil, err + } + + var entityID string + if err := tx.QueryRow("SELECT id FROM entity WHERE id = ?;", id).Scan(&entityID); err != nil { + if err == sql.ErrNoRows { + if _, err := tx.Exec("INSERT INTO entity (id) VALUES(?);", id); err != nil { + tx.Rollback() + return nil, err + } + } else { + tx.Rollback() + return nil, err + } + } + e := &Entity{id} + + parentPath, name := splitPath(fullPath) + if err := db.setEdge(parentPath, name, e, tx); err != nil { + tx.Rollback() + return nil, err + } + + if err := tx.Commit(); err != nil { + return nil, err + } + return e, nil +} + +// Exists returns true if a name already exists in the database. +func (db *Database) Exists(name string) bool { + db.mux.RLock() + defer db.mux.RUnlock() + + e, err := db.get(name) + if err != nil { + return false + } + return e != nil +} + +func (db *Database) setEdge(parentPath, name string, e *Entity, tx *sql.Tx) error { + parent, err := db.get(parentPath) + if err != nil { + return err + } + if parent.id == e.id { + return fmt.Errorf("Cannot set self as child") + } + + if _, err := tx.Exec("INSERT INTO edge (parent_id, name, entity_id) VALUES (?,?,?);", parent.id, name, e.id); err != nil { + return err + } + return nil +} + +// RootEntity returns the root "/" entity for the database. +func (db *Database) RootEntity() *Entity { + return &Entity{ + id: "0", + } +} + +// Get returns the entity for a given path. +func (db *Database) Get(name string) *Entity { + db.mux.RLock() + defer db.mux.RUnlock() + + e, err := db.get(name) + if err != nil { + return nil + } + return e +} + +func (db *Database) get(name string) (*Entity, error) { + e := db.RootEntity() + // We always know the root name so return it if + // it is requested + if name == "/" { + return e, nil + } + + parts := split(name) + for i := 1; i < len(parts); i++ { + p := parts[i] + if p == "" { + continue + } + + next := db.child(e, p) + if next == nil { + return nil, fmt.Errorf("Cannot find child for %s", name) + } + e = next + } + return e, nil + +} + +// List all entities by from the name. +// The key will be the full path of the entity. +func (db *Database) List(name string, depth int) Entities { + db.mux.RLock() + defer db.mux.RUnlock() + + out := Entities{} + e, err := db.get(name) + if err != nil { + return out + } + + children, err := db.children(e, name, depth, nil) + if err != nil { + return out + } + + for _, c := range children { + out[c.FullPath] = c.Entity + } + return out +} + +// Walk through the child graph of an entity, calling walkFunc for each child entity. +// It is safe for walkFunc to call graph functions. +func (db *Database) Walk(name string, walkFunc WalkFunc, depth int) error { + children, err := db.Children(name, depth) + if err != nil { + return err + } + + // Note: the database lock must not be held while calling walkFunc + for _, c := range children { + if err := walkFunc(c.FullPath, c.Entity); err != nil { + return err + } + } + return nil +} + +// Children returns the children of the specified entity. +func (db *Database) Children(name string, depth int) ([]WalkMeta, error) { + db.mux.RLock() + defer db.mux.RUnlock() + + e, err := db.get(name) + if err != nil { + return nil, err + } + + return db.children(e, name, depth, nil) +} + +// Parents returns the parents of a specified entity. +func (db *Database) Parents(name string) ([]string, error) { + db.mux.RLock() + defer db.mux.RUnlock() + + e, err := db.get(name) + if err != nil { + return nil, err + } + return db.parents(e) +} + +// Refs returns the reference count for a specified id. +func (db *Database) Refs(id string) int { + db.mux.RLock() + defer db.mux.RUnlock() + + var count int + if err := db.conn.QueryRow("SELECT COUNT(*) FROM edge WHERE entity_id = ?;", id).Scan(&count); err != nil { + return 0 + } + return count +} + +// RefPaths returns all the id's path references. +func (db *Database) RefPaths(id string) Edges { + db.mux.RLock() + defer db.mux.RUnlock() + + refs := Edges{} + + rows, err := db.conn.Query("SELECT name, parent_id FROM edge WHERE entity_id = ?;", id) + if err != nil { + return refs + } + defer rows.Close() + + for rows.Next() { + var name string + var parentID string + if err := rows.Scan(&name, &parentID); err != nil { + return refs + } + refs = append(refs, &Edge{ + EntityID: id, + Name: name, + ParentID: parentID, + }) + } + return refs +} + +// Delete the reference to an entity at a given path. +func (db *Database) Delete(name string) error { + db.mux.Lock() + defer db.mux.Unlock() + + if name == "/" { + return fmt.Errorf("Cannot delete root entity") + } + + parentPath, n := splitPath(name) + parent, err := db.get(parentPath) + if err != nil { + return err + } + + if _, err := db.conn.Exec("DELETE FROM edge WHERE parent_id = ? AND name = ?;", parent.id, n); err != nil { + return err + } + return nil +} + +// Purge removes the entity with the specified id +// Walk the graph to make sure all references to the entity +// are removed and return the number of references removed +func (db *Database) Purge(id string) (int, error) { + db.mux.Lock() + defer db.mux.Unlock() + + tx, err := db.conn.Begin() + if err != nil { + return -1, err + } + + // Delete all edges + rows, err := tx.Exec("DELETE FROM edge WHERE entity_id = ?;", id) + if err != nil { + tx.Rollback() + return -1, err + } + changes, err := rows.RowsAffected() + if err != nil { + return -1, err + } + + // Clear who's using this id as parent + refs, err := tx.Exec("DELETE FROM edge WHERE parent_id = ?;", id) + if err != nil { + tx.Rollback() + return -1, err + } + refsCount, err := refs.RowsAffected() + if err != nil { + return -1, err + } + + // Delete entity + if _, err := tx.Exec("DELETE FROM entity where id = ?;", id); err != nil { + tx.Rollback() + return -1, err + } + + if err := tx.Commit(); err != nil { + return -1, err + } + + return int(changes + refsCount), nil +} + +// Rename an edge for a given path +func (db *Database) Rename(currentName, newName string) error { + db.mux.Lock() + defer db.mux.Unlock() + + parentPath, name := splitPath(currentName) + newParentPath, newEdgeName := splitPath(newName) + + if parentPath != newParentPath { + return fmt.Errorf("Cannot rename when root paths do not match %s != %s", parentPath, newParentPath) + } + + parent, err := db.get(parentPath) + if err != nil { + return err + } + + rows, err := db.conn.Exec("UPDATE edge SET name = ? WHERE parent_id = ? AND name = ?;", newEdgeName, parent.id, name) + if err != nil { + return err + } + i, err := rows.RowsAffected() + if err != nil { + return err + } + if i == 0 { + return fmt.Errorf("Cannot locate edge for %s %s", parent.id, name) + } + return nil +} + +// WalkMeta stores the walk metadata. +type WalkMeta struct { + Parent *Entity + Entity *Entity + FullPath string + Edge *Edge +} + +func (db *Database) children(e *Entity, name string, depth int, entities []WalkMeta) ([]WalkMeta, error) { + if e == nil { + return entities, nil + } + + rows, err := db.conn.Query("SELECT entity_id, name FROM edge where parent_id = ?;", e.id) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var entityID, entityName string + if err := rows.Scan(&entityID, &entityName); err != nil { + return nil, err + } + child := &Entity{entityID} + edge := &Edge{ + ParentID: e.id, + Name: entityName, + EntityID: child.id, + } + + meta := WalkMeta{ + Parent: e, + Entity: child, + FullPath: path.Join(name, edge.Name), + Edge: edge, + } + + entities = append(entities, meta) + + if depth != 0 { + nDepth := depth + if depth != -1 { + nDepth-- + } + entities, err = db.children(child, meta.FullPath, nDepth, entities) + if err != nil { + return nil, err + } + } + } + + return entities, nil +} + +func (db *Database) parents(e *Entity) (parents []string, err error) { + if e == nil { + return parents, nil + } + + rows, err := db.conn.Query("SELECT parent_id FROM edge where entity_id = ?;", e.id) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var parentID string + if err := rows.Scan(&parentID); err != nil { + return nil, err + } + parents = append(parents, parentID) + } + + return parents, nil +} + +// Return the entity based on the parent path and name. +func (db *Database) child(parent *Entity, name string) *Entity { + var id string + if err := db.conn.QueryRow("SELECT entity_id FROM edge WHERE parent_id = ? AND name = ?;", parent.id, name).Scan(&id); err != nil { + return nil + } + return &Entity{id} +} + +// ID returns the id used to reference this entity. +func (e *Entity) ID() string { + return e.id +} + +// Paths returns the paths sorted by depth. +func (e Entities) Paths() []string { + out := make([]string, len(e)) + var i int + for k := range e { + out[i] = k + i++ + } + sortByDepth(out) + + return out +} diff --git a/pkg/graphdb/graphdb_test.go b/pkg/graphdb/graphdb_test.go new file mode 100644 index 00000000..f0fb074b --- /dev/null +++ b/pkg/graphdb/graphdb_test.go @@ -0,0 +1,721 @@ +package graphdb + +import ( + "database/sql" + "fmt" + "os" + "path" + "runtime" + "strconv" + "testing" + + _ "github.com/mattn/go-sqlite3" +) + +func newTestDb(t *testing.T) (*Database, string) { + p := path.Join(os.TempDir(), "sqlite.db") + conn, err := sql.Open("sqlite3", p) + db, err := NewDatabase(conn) + if err != nil { + t.Fatal(err) + } + return db, p +} + +func destroyTestDb(dbPath string) { + os.Remove(dbPath) +} + +func TestNewDatabase(t *testing.T) { + db, dbpath := newTestDb(t) + if db == nil { + t.Fatal("Database should not be nil") + } + db.Close() + defer destroyTestDb(dbpath) +} + +func TestCreateRootEntity(t *testing.T) { + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + root := db.RootEntity() + if root == nil { + t.Fatal("Root entity should not be nil") + } +} + +func TestGetRootEntity(t *testing.T) { + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + e := db.Get("/") + if e == nil { + t.Fatal("Entity should not be nil") + } + if e.ID() != "0" { + t.Fatalf("Entity id should be 0, got %s", e.ID()) + } +} + +func TestSetEntityWithDifferentName(t *testing.T) { + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + db.Set("/test", "1") + if _, err := db.Set("/other", "1"); err != nil { + t.Fatal(err) + } +} + +func TestSetDuplicateEntity(t *testing.T) { + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + if _, err := db.Set("/foo", "42"); err != nil { + t.Fatal(err) + } + if _, err := db.Set("/foo", "43"); err == nil { + t.Fatalf("Creating an entry with a duplicate path did not cause an error") + } +} + +func TestCreateChild(t *testing.T) { + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + child, err := db.Set("/db", "1") + if err != nil { + t.Fatal(err) + } + if child == nil { + t.Fatal("Child should not be nil") + } + if child.ID() != "1" { + t.Fail() + } +} + +func TestParents(t *testing.T) { + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + for i := 1; i < 6; i++ { + a := strconv.Itoa(i) + if _, err := db.Set("/"+a, a); err != nil { + t.Fatal(err) + } + } + + for i := 6; i < 11; i++ { + a := strconv.Itoa(i) + p := strconv.Itoa(i - 5) + + key := fmt.Sprintf("/%s/%s", p, a) + + if _, err := db.Set(key, a); err != nil { + t.Fatal(err) + } + + parents, err := db.Parents(key) + if err != nil { + t.Fatal(err) + } + + if len(parents) != 1 { + t.Fatalf("Expected 1 entry for %s got %d", key, len(parents)) + } + + if parents[0] != p { + t.Fatalf("ID %s received, %s expected", parents[0], p) + } + } +} + +func TestChildren(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + str := "/" + for i := 1; i < 6; i++ { + a := strconv.Itoa(i) + if _, err := db.Set(str+a, a); err != nil { + t.Fatal(err) + } + + str = str + a + "/" + } + + str = "/" + for i := 10; i < 30; i++ { // 20 entities + a := strconv.Itoa(i) + if _, err := db.Set(str+a, a); err != nil { + t.Fatal(err) + } + + str = str + a + "/" + } + entries, err := db.Children("/", 5) + if err != nil { + t.Fatal(err) + } + + if len(entries) != 11 { + t.Fatalf("Expect 11 entries for / got %d", len(entries)) + } + + entries, err = db.Children("/", 20) + if err != nil { + t.Fatal(err) + } + + if len(entries) != 25 { + t.Fatalf("Expect 25 entries for / got %d", len(entries)) + } +} + +func TestListAllRootChildren(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + for i := 1; i < 6; i++ { + a := strconv.Itoa(i) + if _, err := db.Set("/"+a, a); err != nil { + t.Fatal(err) + } + } + entries := db.List("/", -1) + if len(entries) != 5 { + t.Fatalf("Expect 5 entries for / got %d", len(entries)) + } +} + +func TestListAllSubChildren(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + _, err := db.Set("/webapp", "1") + if err != nil { + t.Fatal(err) + } + child2, err := db.Set("/db", "2") + if err != nil { + t.Fatal(err) + } + child4, err := db.Set("/logs", "4") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/db/logs", child4.ID()); err != nil { + t.Fatal(err) + } + + child3, err := db.Set("/sentry", "3") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/db", child2.ID()); err != nil { + t.Fatal(err) + } + + entries := db.List("/webapp", 1) + if len(entries) != 3 { + t.Fatalf("Expect 3 entries for / got %d", len(entries)) + } + + entries = db.List("/webapp", 0) + if len(entries) != 2 { + t.Fatalf("Expect 2 entries for / got %d", len(entries)) + } +} + +func TestAddSelfAsChild(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + child, err := db.Set("/test", "1") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/test/other", child.ID()); err == nil { + t.Fatal("Error should not be nil") + } +} + +func TestAddChildToNonExistentRoot(t *testing.T) { + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + if _, err := db.Set("/myapp", "1"); err != nil { + t.Fatal(err) + } + + if _, err := db.Set("/myapp/proxy/db", "2"); err == nil { + t.Fatal("Error should not be nil") + } +} + +func TestWalkAll(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + _, err := db.Set("/webapp", "1") + if err != nil { + t.Fatal(err) + } + child2, err := db.Set("/db", "2") + if err != nil { + t.Fatal(err) + } + child4, err := db.Set("/db/logs", "4") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/logs", child4.ID()); err != nil { + t.Fatal(err) + } + + child3, err := db.Set("/sentry", "3") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/db", child2.ID()); err != nil { + t.Fatal(err) + } + + child5, err := db.Set("/gograph", "5") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/same-ref-diff-name", child5.ID()); err != nil { + t.Fatal(err) + } + + if err := db.Walk("/", func(p string, e *Entity) error { + t.Logf("Path: %s Entity: %s", p, e.ID()) + return nil + }, -1); err != nil { + t.Fatal(err) + } +} + +func TestGetEntityByPath(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + _, err := db.Set("/webapp", "1") + if err != nil { + t.Fatal(err) + } + child2, err := db.Set("/db", "2") + if err != nil { + t.Fatal(err) + } + child4, err := db.Set("/logs", "4") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/db/logs", child4.ID()); err != nil { + t.Fatal(err) + } + + child3, err := db.Set("/sentry", "3") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/db", child2.ID()); err != nil { + t.Fatal(err) + } + + child5, err := db.Set("/gograph", "5") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/same-ref-diff-name", child5.ID()); err != nil { + t.Fatal(err) + } + + entity := db.Get("/webapp/db/logs") + if entity == nil { + t.Fatal("Entity should not be nil") + } + if entity.ID() != "4" { + t.Fatalf("Expected to get entity with id 4, got %s", entity.ID()) + } +} + +func TestEnitiesPaths(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + _, err := db.Set("/webapp", "1") + if err != nil { + t.Fatal(err) + } + child2, err := db.Set("/db", "2") + if err != nil { + t.Fatal(err) + } + child4, err := db.Set("/logs", "4") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/db/logs", child4.ID()); err != nil { + t.Fatal(err) + } + + child3, err := db.Set("/sentry", "3") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/db", child2.ID()); err != nil { + t.Fatal(err) + } + + child5, err := db.Set("/gograph", "5") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/same-ref-diff-name", child5.ID()); err != nil { + t.Fatal(err) + } + + out := db.List("/", -1) + for _, p := range out.Paths() { + t.Log(p) + } +} + +func TestDeleteRootEntity(t *testing.T) { + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + if err := db.Delete("/"); err == nil { + t.Fatal("Error should not be nil") + } +} + +func TestDeleteEntity(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + _, err := db.Set("/webapp", "1") + if err != nil { + t.Fatal(err) + } + child2, err := db.Set("/db", "2") + if err != nil { + t.Fatal(err) + } + child4, err := db.Set("/logs", "4") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/db/logs", child4.ID()); err != nil { + t.Fatal(err) + } + + child3, err := db.Set("/sentry", "3") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/db", child2.ID()); err != nil { + t.Fatal(err) + } + + child5, err := db.Set("/gograph", "5") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/same-ref-diff-name", child5.ID()); err != nil { + t.Fatal(err) + } + + if err := db.Delete("/webapp/sentry"); err != nil { + t.Fatal(err) + } + entity := db.Get("/webapp/sentry") + if entity != nil { + t.Fatal("Entity /webapp/sentry should be nil") + } +} + +func TestCountRefs(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + db.Set("/webapp", "1") + + if db.Refs("1") != 1 { + t.Fatal("Expect reference count to be 1") + } + + db.Set("/db", "2") + db.Set("/webapp/db", "2") + if db.Refs("2") != 2 { + t.Fatal("Expect reference count to be 2") + } +} + +func TestPurgeId(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + db.Set("/webapp", "1") + + if c := db.Refs("1"); c != 1 { + t.Fatalf("Expect reference count to be 1, got %d", c) + } + + db.Set("/db", "2") + db.Set("/webapp/db", "2") + + count, err := db.Purge("2") + if err != nil { + t.Fatal(err) + } + if count != 2 { + t.Fatalf("Expected 2 references to be removed, got %d", count) + } +} + +// Regression test https://github.com/docker/docker/issues/12334 +func TestPurgeIdRefPaths(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + db.Set("/webapp", "1") + db.Set("/db", "2") + + db.Set("/db/webapp", "1") + + if c := db.Refs("1"); c != 2 { + t.Fatalf("Expected 2 reference for webapp, got %d", c) + } + if c := db.Refs("2"); c != 1 { + t.Fatalf("Expected 1 reference for db, got %d", c) + } + + if rp := db.RefPaths("2"); len(rp) != 1 { + t.Fatalf("Expected 1 reference path for db, got %d", len(rp)) + } + + count, err := db.Purge("2") + if err != nil { + t.Fatal(err) + } + + if count != 2 { + t.Fatalf("Expected 2 rows to be removed, got %d", count) + } + + if c := db.Refs("2"); c != 0 { + t.Fatalf("Expected 0 reference for db, got %d", c) + } + if c := db.Refs("1"); c != 1 { + t.Fatalf("Expected 1 reference for webapp, got %d", c) + } +} + +func TestRename(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + db.Set("/webapp", "1") + + if db.Refs("1") != 1 { + t.Fatal("Expect reference count to be 1") + } + + db.Set("/db", "2") + db.Set("/webapp/db", "2") + + if db.Get("/webapp/db") == nil { + t.Fatal("Cannot find entity at path /webapp/db") + } + + if err := db.Rename("/webapp/db", "/webapp/newdb"); err != nil { + t.Fatal(err) + } + if db.Get("/webapp/db") != nil { + t.Fatal("Entity should not exist at /webapp/db") + } + if db.Get("/webapp/newdb") == nil { + t.Fatal("Cannot find entity at path /webapp/newdb") + } + +} + +func TestCreateMultipleNames(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + db.Set("/db", "1") + if _, err := db.Set("/myapp", "1"); err != nil { + t.Fatal(err) + } + + db.Walk("/", func(p string, e *Entity) error { + t.Logf("%s\n", p) + return nil + }, -1) +} + +func TestRefPaths(t *testing.T) { + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + db.Set("/webapp", "1") + + db.Set("/db", "2") + db.Set("/webapp/db", "2") + + refs := db.RefPaths("2") + if len(refs) != 2 { + t.Fatalf("Expected reference count to be 2, got %d", len(refs)) + } +} + +func TestExistsTrue(t *testing.T) { + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + db.Set("/testing", "1") + + if !db.Exists("/testing") { + t.Fatalf("/tesing should exist") + } +} + +func TestExistsFalse(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + db.Set("/toerhe", "1") + + if db.Exists("/testing") { + t.Fatalf("/tesing should not exist") + } + +} + +func TestGetNameWithTrailingSlash(t *testing.T) { + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + db.Set("/todo", "1") + + e := db.Get("/todo/") + if e == nil { + t.Fatalf("Entity should not be nil") + } +} + +func TestConcurrentWrites(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + errs := make(chan error, 2) + + save := func(name string, id string) { + if _, err := db.Set(fmt.Sprintf("/%s", name), id); err != nil { + errs <- err + } + errs <- nil + } + purge := func(id string) { + if _, err := db.Purge(id); err != nil { + errs <- err + } + errs <- nil + } + + save("/1", "1") + + go purge("1") + go save("/2", "2") + + any := false + for i := 0; i < 2; i++ { + if err := <-errs; err != nil { + any = true + t.Log(err) + } + } + if any { + t.Fail() + } +} diff --git a/pkg/graphdb/sort.go b/pkg/graphdb/sort.go new file mode 100644 index 00000000..c07df077 --- /dev/null +++ b/pkg/graphdb/sort.go @@ -0,0 +1,27 @@ +package graphdb + +import "sort" + +type pathSorter struct { + paths []string + by func(i, j string) bool +} + +func sortByDepth(paths []string) { + s := &pathSorter{paths, func(i, j string) bool { + return PathDepth(i) > PathDepth(j) + }} + sort.Sort(s) +} + +func (s *pathSorter) Len() int { + return len(s.paths) +} + +func (s *pathSorter) Swap(i, j int) { + s.paths[i], s.paths[j] = s.paths[j], s.paths[i] +} + +func (s *pathSorter) Less(i, j int) bool { + return s.by(s.paths[i], s.paths[j]) +} diff --git a/pkg/graphdb/sort_test.go b/pkg/graphdb/sort_test.go new file mode 100644 index 00000000..ddf2266f --- /dev/null +++ b/pkg/graphdb/sort_test.go @@ -0,0 +1,29 @@ +package graphdb + +import ( + "testing" +) + +func TestSort(t *testing.T) { + paths := []string{ + "/", + "/myreallylongname", + "/app/db", + } + + sortByDepth(paths) + + if len(paths) != 3 { + t.Fatalf("Expected 3 parts got %d", len(paths)) + } + + if paths[0] != "/app/db" { + t.Fatalf("Expected /app/db got %s", paths[0]) + } + if paths[1] != "/myreallylongname" { + t.Fatalf("Expected /myreallylongname got %s", paths[1]) + } + if paths[2] != "/" { + t.Fatalf("Expected / got %s", paths[2]) + } +} diff --git a/pkg/graphdb/utils.go b/pkg/graphdb/utils.go new file mode 100644 index 00000000..9edd79c3 --- /dev/null +++ b/pkg/graphdb/utils.go @@ -0,0 +1,32 @@ +package graphdb + +import ( + "path" + "strings" +) + +// Split p on / +func split(p string) []string { + return strings.Split(p, "/") +} + +// PathDepth returns the depth or number of / in a given path +func PathDepth(p string) int { + parts := split(p) + if len(parts) == 2 && parts[1] == "" { + return 1 + } + return len(parts) +} + +func splitPath(p string) (parent, name string) { + if p[0] != '/' { + p = "/" + p + } + parent, name = path.Split(p) + l := len(parent) + if parent[l-1] == '/' { + parent = parent[:l-1] + } + return +} diff --git a/pkg/homedir/homedir.go b/pkg/homedir/homedir.go new file mode 100644 index 00000000..8154e83f --- /dev/null +++ b/pkg/homedir/homedir.go @@ -0,0 +1,39 @@ +package homedir + +import ( + "os" + "runtime" + + "github.com/opencontainers/runc/libcontainer/user" +) + +// Key returns the env var name for the user's home dir based on +// the platform being run on +func Key() string { + if runtime.GOOS == "windows" { + return "USERPROFILE" + } + return "HOME" +} + +// Get returns the home directory of the current user with the help of +// environment variables depending on the target operating system. +// Returned path should be used with "path/filepath" to form new paths. +func Get() string { + home := os.Getenv(Key()) + if home == "" && runtime.GOOS != "windows" { + if u, err := user.CurrentUser(); err == nil { + return u.Home + } + } + return home +} + +// GetShortcutString returns the string that is shortcut to user's home directory +// in the native shell of the platform running on. +func GetShortcutString() string { + if runtime.GOOS == "windows" { + return "%USERPROFILE%" // be careful while using in format functions + } + return "~" +} diff --git a/pkg/homedir/homedir_test.go b/pkg/homedir/homedir_test.go new file mode 100644 index 00000000..7a95cb2b --- /dev/null +++ b/pkg/homedir/homedir_test.go @@ -0,0 +1,24 @@ +package homedir + +import ( + "path/filepath" + "testing" +) + +func TestGet(t *testing.T) { + home := Get() + if home == "" { + t.Fatal("returned home directory is empty") + } + + if !filepath.IsAbs(home) { + t.Fatalf("returned path is not absolute: %s", home) + } +} + +func TestGetShortcutString(t *testing.T) { + shortcut := GetShortcutString() + if shortcut == "" { + t.Fatal("returned shortcut string is empty") + } +} diff --git a/pkg/httputils/httputils.go b/pkg/httputils/httputils.go new file mode 100644 index 00000000..d7dc4387 --- /dev/null +++ b/pkg/httputils/httputils.go @@ -0,0 +1,56 @@ +package httputils + +import ( + "errors" + "fmt" + "net/http" + "regexp" + "strings" + + "github.com/docker/docker/pkg/jsonmessage" +) + +var ( + headerRegexp = regexp.MustCompile(`^(?:(.+)/(.+?))\((.+)\).*$`) + errInvalidHeader = errors.New("Bad header, should be in format `docker/version (platform)`") +) + +// Download requests a given URL and returns an io.Reader. +func Download(url string) (resp *http.Response, err error) { + if resp, err = http.Get(url); err != nil { + return nil, err + } + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("Got HTTP status code >= 400: %s", resp.Status) + } + return resp, nil +} + +// NewHTTPRequestError returns a JSON response error. +func NewHTTPRequestError(msg string, res *http.Response) error { + return &jsonmessage.JSONError{ + Message: msg, + Code: res.StatusCode, + } +} + +// ServerHeader contains the server information. +type ServerHeader struct { + App string // docker + Ver string // 1.8.0-dev + OS string // windows or linux +} + +// ParseServerHeader extracts pieces from an HTTP server header +// which is in the format "docker/version (os)" eg docker/1.8.0-dev (windows). +func ParseServerHeader(hdr string) (*ServerHeader, error) { + matches := headerRegexp.FindStringSubmatch(hdr) + if len(matches) != 4 { + return nil, errInvalidHeader + } + return &ServerHeader{ + App: strings.TrimSpace(matches[1]), + Ver: strings.TrimSpace(matches[2]), + OS: strings.TrimSpace(matches[3]), + }, nil +} diff --git a/pkg/httputils/httputils_test.go b/pkg/httputils/httputils_test.go new file mode 100644 index 00000000..d35d0821 --- /dev/null +++ b/pkg/httputils/httputils_test.go @@ -0,0 +1,115 @@ +package httputils + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestDownload(t *testing.T) { + expected := "Hello, docker !" + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, expected) + })) + defer ts.Close() + response, err := Download(ts.URL) + if err != nil { + t.Fatal(err) + } + + actual, err := ioutil.ReadAll(response.Body) + response.Body.Close() + + if err != nil || string(actual) != expected { + t.Fatalf("Expected the response %q, got err:%v, response:%v, actual:%s", expected, err, response, string(actual)) + } +} + +func TestDownload400Errors(t *testing.T) { + expectedError := "Got HTTP status code >= 400: 403 Forbidden" + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // 403 + http.Error(w, "something failed (forbidden)", http.StatusForbidden) + })) + defer ts.Close() + // Expected status code = 403 + if _, err := Download(ts.URL); err == nil || err.Error() != expectedError { + t.Fatalf("Expected the the error %q, got %v", expectedError, err) + } +} + +func TestDownloadOtherErrors(t *testing.T) { + if _, err := Download("I'm not an url.."); err == nil || !strings.Contains(err.Error(), "unsupported protocol scheme") { + t.Fatalf("Expected an error with 'unsupported protocol scheme', got %v", err) + } +} + +func TestNewHTTPRequestError(t *testing.T) { + errorMessage := "Some error message" + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // 403 + http.Error(w, errorMessage, http.StatusForbidden) + })) + defer ts.Close() + httpResponse, err := http.Get(ts.URL) + if err != nil { + t.Fatal(err) + } + if err := NewHTTPRequestError(errorMessage, httpResponse); err.Error() != errorMessage { + t.Fatalf("Expected err to be %q, got %v", errorMessage, err) + } +} + +func TestParseServerHeader(t *testing.T) { + inputs := map[string][]string{ + "bad header": {"error"}, + "(bad header)": {"error"}, + "(without/spaces)": {"error"}, + "(header/with spaces)": {"error"}, + "foo/bar (baz)": {"foo", "bar", "baz"}, + "foo/bar": {"error"}, + "foo": {"error"}, + "foo/bar (baz space)": {"foo", "bar", "baz space"}, + " f f / b b ( b s ) ": {"f f", "b b", "b s"}, + "foo/bar (baz) ignore": {"foo", "bar", "baz"}, + "foo/bar ()": {"error"}, + "foo/bar()": {"error"}, + "foo/bar(baz)": {"foo", "bar", "baz"}, + "foo/bar/zzz(baz)": {"foo/bar", "zzz", "baz"}, + "foo/bar(baz/abc)": {"foo", "bar", "baz/abc"}, + "foo/bar(baz (abc))": {"foo", "bar", "baz (abc)"}, + } + + for header, values := range inputs { + serverHeader, err := ParseServerHeader(header) + if err != nil { + if err != errInvalidHeader { + t.Fatalf("Failed to parse %q, and got some unexpected error: %q", header, err) + } + if values[0] == "error" { + continue + } + t.Fatalf("Header %q failed to parse when it shouldn't have", header) + } + if values[0] == "error" { + t.Fatalf("Header %q parsed ok when it should have failed(%q).", header, serverHeader) + } + + if serverHeader.App != values[0] { + t.Fatalf("Expected serverHeader.App for %q to equal %q, got %q", header, values[0], serverHeader.App) + } + + if serverHeader.Ver != values[1] { + t.Fatalf("Expected serverHeader.Ver for %q to equal %q, got %q", header, values[1], serverHeader.Ver) + } + + if serverHeader.OS != values[2] { + t.Fatalf("Expected serverHeader.OS for %q to equal %q, got %q", header, values[2], serverHeader.OS) + } + + } + +} diff --git a/pkg/httputils/mimetype.go b/pkg/httputils/mimetype.go new file mode 100644 index 00000000..d5cf34e4 --- /dev/null +++ b/pkg/httputils/mimetype.go @@ -0,0 +1,30 @@ +package httputils + +import ( + "mime" + "net/http" +) + +// MimeTypes stores the MIME content type. +var MimeTypes = struct { + TextPlain string + Tar string + OctetStream string +}{"text/plain", "application/tar", "application/octet-stream"} + +// DetectContentType returns a best guess representation of the MIME +// content type for the bytes at c. The value detected by +// http.DetectContentType is guaranteed not be nil, defaulting to +// application/octet-stream when a better guess cannot be made. The +// result of this detection is then run through mime.ParseMediaType() +// which separates the actual MIME string from any parameters. +func DetectContentType(c []byte) (string, map[string]string, error) { + + ct := http.DetectContentType(c) + contentType, args, err := mime.ParseMediaType(ct) + if err != nil { + return "", nil, err + } + + return contentType, args, nil +} diff --git a/pkg/httputils/mimetype_test.go b/pkg/httputils/mimetype_test.go new file mode 100644 index 00000000..9de433ee --- /dev/null +++ b/pkg/httputils/mimetype_test.go @@ -0,0 +1,13 @@ +package httputils + +import ( + "testing" +) + +func TestDetectContentType(t *testing.T) { + input := []byte("That is just a plain text") + + if contentType, _, err := DetectContentType(input); err != nil || contentType != "text/plain" { + t.Errorf("TestDetectContentType failed") + } +} diff --git a/pkg/httputils/resumablerequestreader.go b/pkg/httputils/resumablerequestreader.go new file mode 100644 index 00000000..bebc8608 --- /dev/null +++ b/pkg/httputils/resumablerequestreader.go @@ -0,0 +1,95 @@ +package httputils + +import ( + "fmt" + "io" + "net/http" + "time" + + "github.com/Sirupsen/logrus" +) + +type resumableRequestReader struct { + client *http.Client + request *http.Request + lastRange int64 + totalSize int64 + currentResponse *http.Response + failures uint32 + maxFailures uint32 +} + +// ResumableRequestReader makes it possible to resume reading a request's body transparently +// maxfail is the number of times we retry to make requests again (not resumes) +// totalsize is the total length of the body; auto detect if not provided +func ResumableRequestReader(c *http.Client, r *http.Request, maxfail uint32, totalsize int64) io.ReadCloser { + return &resumableRequestReader{client: c, request: r, maxFailures: maxfail, totalSize: totalsize} +} + +// ResumableRequestReaderWithInitialResponse makes it possible to resume +// reading the body of an already initiated request. +func ResumableRequestReaderWithInitialResponse(c *http.Client, r *http.Request, maxfail uint32, totalsize int64, initialResponse *http.Response) io.ReadCloser { + return &resumableRequestReader{client: c, request: r, maxFailures: maxfail, totalSize: totalsize, currentResponse: initialResponse} +} + +func (r *resumableRequestReader) Read(p []byte) (n int, err error) { + if r.client == nil || r.request == nil { + return 0, fmt.Errorf("client and request can't be nil\n") + } + isFreshRequest := false + if r.lastRange != 0 && r.currentResponse == nil { + readRange := fmt.Sprintf("bytes=%d-%d", r.lastRange, r.totalSize) + r.request.Header.Set("Range", readRange) + time.Sleep(5 * time.Second) + } + if r.currentResponse == nil { + r.currentResponse, err = r.client.Do(r.request) + isFreshRequest = true + } + if err != nil && r.failures+1 != r.maxFailures { + r.cleanUpResponse() + r.failures++ + time.Sleep(5 * time.Duration(r.failures) * time.Second) + return 0, nil + } else if err != nil { + r.cleanUpResponse() + return 0, err + } + if r.currentResponse.StatusCode == 416 && r.lastRange == r.totalSize && r.currentResponse.ContentLength == 0 { + r.cleanUpResponse() + return 0, io.EOF + } else if r.currentResponse.StatusCode != 206 && r.lastRange != 0 && isFreshRequest { + r.cleanUpResponse() + return 0, fmt.Errorf("the server doesn't support byte ranges") + } + if r.totalSize == 0 { + r.totalSize = r.currentResponse.ContentLength + } else if r.totalSize <= 0 { + r.cleanUpResponse() + return 0, fmt.Errorf("failed to auto detect content length") + } + n, err = r.currentResponse.Body.Read(p) + r.lastRange += int64(n) + if err != nil { + r.cleanUpResponse() + } + if err != nil && err != io.EOF { + logrus.Infof("encountered error during pull and clearing it before resume: %s", err) + err = nil + } + return n, err +} + +func (r *resumableRequestReader) Close() error { + r.cleanUpResponse() + r.client = nil + r.request = nil + return nil +} + +func (r *resumableRequestReader) cleanUpResponse() { + if r.currentResponse != nil { + r.currentResponse.Body.Close() + r.currentResponse = nil + } +} diff --git a/pkg/httputils/resumablerequestreader_test.go b/pkg/httputils/resumablerequestreader_test.go new file mode 100644 index 00000000..7006f049 --- /dev/null +++ b/pkg/httputils/resumablerequestreader_test.go @@ -0,0 +1,307 @@ +package httputils + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestResumableRequestHeaderSimpleErrors(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "Hello, world !") + })) + defer ts.Close() + + client := &http.Client{} + + var req *http.Request + req, err := http.NewRequest("GET", ts.URL, nil) + if err != nil { + t.Fatal(err) + } + + expectedError := "client and request can't be nil\n" + resreq := &resumableRequestReader{} + _, err = resreq.Read([]byte{}) + if err == nil || err.Error() != expectedError { + t.Fatalf("Expected an error with '%s', got %v.", expectedError, err) + } + + resreq = &resumableRequestReader{ + client: client, + request: req, + totalSize: -1, + } + expectedError = "failed to auto detect content length" + _, err = resreq.Read([]byte{}) + if err == nil || err.Error() != expectedError { + t.Fatalf("Expected an error with '%s', got %v.", expectedError, err) + } + +} + +// Not too much failures, bails out after some wait +func TestResumableRequestHeaderNotTooMuchFailures(t *testing.T) { + client := &http.Client{} + + var badReq *http.Request + badReq, err := http.NewRequest("GET", "I'm not an url", nil) + if err != nil { + t.Fatal(err) + } + + resreq := &resumableRequestReader{ + client: client, + request: badReq, + failures: 0, + maxFailures: 2, + } + read, err := resreq.Read([]byte{}) + if err != nil || read != 0 { + t.Fatalf("Expected no error and no byte read, got err:%v, read:%v.", err, read) + } +} + +// Too much failures, returns the error +func TestResumableRequestHeaderTooMuchFailures(t *testing.T) { + client := &http.Client{} + + var badReq *http.Request + badReq, err := http.NewRequest("GET", "I'm not an url", nil) + if err != nil { + t.Fatal(err) + } + + resreq := &resumableRequestReader{ + client: client, + request: badReq, + failures: 0, + maxFailures: 1, + } + defer resreq.Close() + + expectedError := `Get I%27m%20not%20an%20url: unsupported protocol scheme ""` + read, err := resreq.Read([]byte{}) + if err == nil || err.Error() != expectedError || read != 0 { + t.Fatalf("Expected the error '%s', got err:%v, read:%v.", expectedError, err, read) + } +} + +type errorReaderCloser struct{} + +func (errorReaderCloser) Close() error { return nil } + +func (errorReaderCloser) Read(p []byte) (n int, err error) { + return 0, fmt.Errorf("An error occurred") +} + +// If a an unknown error is encountered, return 0, nil and log it +func TestResumableRequestReaderWithReadError(t *testing.T) { + var req *http.Request + req, err := http.NewRequest("GET", "", nil) + if err != nil { + t.Fatal(err) + } + + client := &http.Client{} + + response := &http.Response{ + Status: "500 Internal Server", + StatusCode: 500, + ContentLength: 0, + Close: true, + Body: errorReaderCloser{}, + } + + resreq := &resumableRequestReader{ + client: client, + request: req, + currentResponse: response, + lastRange: 1, + totalSize: 1, + } + defer resreq.Close() + + buf := make([]byte, 1) + read, err := resreq.Read(buf) + if err != nil { + t.Fatal(err) + } + + if read != 0 { + t.Fatalf("Expected to have read nothing, but read %v", read) + } +} + +func TestResumableRequestReaderWithEOFWith416Response(t *testing.T) { + var req *http.Request + req, err := http.NewRequest("GET", "", nil) + if err != nil { + t.Fatal(err) + } + + client := &http.Client{} + + response := &http.Response{ + Status: "416 Requested Range Not Satisfiable", + StatusCode: 416, + ContentLength: 0, + Close: true, + Body: ioutil.NopCloser(strings.NewReader("")), + } + + resreq := &resumableRequestReader{ + client: client, + request: req, + currentResponse: response, + lastRange: 1, + totalSize: 1, + } + defer resreq.Close() + + buf := make([]byte, 1) + _, err = resreq.Read(buf) + if err == nil || err != io.EOF { + t.Fatalf("Expected an io.EOF error, got %v", err) + } +} + +func TestResumableRequestReaderWithServerDoesntSupportByteRanges(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Range") == "" { + t.Fatalf("Expected a Range HTTP header, got nothing") + } + })) + defer ts.Close() + + var req *http.Request + req, err := http.NewRequest("GET", ts.URL, nil) + if err != nil { + t.Fatal(err) + } + + client := &http.Client{} + + resreq := &resumableRequestReader{ + client: client, + request: req, + lastRange: 1, + } + defer resreq.Close() + + buf := make([]byte, 2) + _, err = resreq.Read(buf) + if err == nil || err.Error() != "the server doesn't support byte ranges" { + t.Fatalf("Expected an error 'the server doesn't support byte ranges', got %v", err) + } +} + +func TestResumableRequestReaderWithZeroTotalSize(t *testing.T) { + + srvtxt := "some response text data" + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, srvtxt) + })) + defer ts.Close() + + var req *http.Request + req, err := http.NewRequest("GET", ts.URL, nil) + if err != nil { + t.Fatal(err) + } + + client := &http.Client{} + retries := uint32(5) + + resreq := ResumableRequestReader(client, req, retries, 0) + defer resreq.Close() + + data, err := ioutil.ReadAll(resreq) + if err != nil { + t.Fatal(err) + } + + resstr := strings.TrimSuffix(string(data), "\n") + + if resstr != srvtxt { + t.Errorf("resstr != srvtxt") + } +} + +func TestResumableRequestReader(t *testing.T) { + + srvtxt := "some response text data" + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, srvtxt) + })) + defer ts.Close() + + var req *http.Request + req, err := http.NewRequest("GET", ts.URL, nil) + if err != nil { + t.Fatal(err) + } + + client := &http.Client{} + retries := uint32(5) + imgSize := int64(len(srvtxt)) + + resreq := ResumableRequestReader(client, req, retries, imgSize) + defer resreq.Close() + + data, err := ioutil.ReadAll(resreq) + if err != nil { + t.Fatal(err) + } + + resstr := strings.TrimSuffix(string(data), "\n") + + if resstr != srvtxt { + t.Errorf("resstr != srvtxt") + } +} + +func TestResumableRequestReaderWithInitialResponse(t *testing.T) { + + srvtxt := "some response text data" + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, srvtxt) + })) + defer ts.Close() + + var req *http.Request + req, err := http.NewRequest("GET", ts.URL, nil) + if err != nil { + t.Fatal(err) + } + + client := &http.Client{} + retries := uint32(5) + imgSize := int64(len(srvtxt)) + + res, err := client.Do(req) + if err != nil { + t.Fatal(err) + } + + resreq := ResumableRequestReaderWithInitialResponse(client, req, retries, imgSize, res) + defer resreq.Close() + + data, err := ioutil.ReadAll(resreq) + if err != nil { + t.Fatal(err) + } + + resstr := strings.TrimSuffix(string(data), "\n") + + if resstr != srvtxt { + t.Errorf("resstr != srvtxt") + } +} diff --git a/pkg/idtools/idtools.go b/pkg/idtools/idtools.go new file mode 100644 index 00000000..6bca4662 --- /dev/null +++ b/pkg/idtools/idtools.go @@ -0,0 +1,197 @@ +package idtools + +import ( + "bufio" + "fmt" + "os" + "sort" + "strconv" + "strings" +) + +// IDMap contains a single entry for user namespace range remapping. An array +// of IDMap entries represents the structure that will be provided to the Linux +// kernel for creating a user namespace. +type IDMap struct { + ContainerID int `json:"container_id"` + HostID int `json:"host_id"` + Size int `json:"size"` +} + +type subIDRange struct { + Start int + Length int +} + +type ranges []subIDRange + +func (e ranges) Len() int { return len(e) } +func (e ranges) Swap(i, j int) { e[i], e[j] = e[j], e[i] } +func (e ranges) Less(i, j int) bool { return e[i].Start < e[j].Start } + +const ( + subuidFileName string = "/etc/subuid" + subgidFileName string = "/etc/subgid" +) + +// MkdirAllAs creates a directory (include any along the path) and then modifies +// ownership to the requested uid/gid. If the directory already exists, this +// function will still change ownership to the requested uid/gid pair. +func MkdirAllAs(path string, mode os.FileMode, ownerUID, ownerGID int) error { + return mkdirAs(path, mode, ownerUID, ownerGID, true, true) +} + +// MkdirAllNewAs creates a directory (include any along the path) and then modifies +// ownership ONLY of newly created directories to the requested uid/gid. If the +// directories along the path exist, no change of ownership will be performed +func MkdirAllNewAs(path string, mode os.FileMode, ownerUID, ownerGID int) error { + return mkdirAs(path, mode, ownerUID, ownerGID, true, false) +} + +// MkdirAs creates a directory and then modifies ownership to the requested uid/gid. +// If the directory already exists, this function still changes ownership +func MkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int) error { + return mkdirAs(path, mode, ownerUID, ownerGID, false, true) +} + +// GetRootUIDGID retrieves the remapped root uid/gid pair from the set of maps. +// If the maps are empty, then the root uid/gid will default to "real" 0/0 +func GetRootUIDGID(uidMap, gidMap []IDMap) (int, int, error) { + var uid, gid int + + if uidMap != nil { + xUID, err := ToHost(0, uidMap) + if err != nil { + return -1, -1, err + } + uid = xUID + } + if gidMap != nil { + xGID, err := ToHost(0, gidMap) + if err != nil { + return -1, -1, err + } + gid = xGID + } + return uid, gid, nil +} + +// ToContainer takes an id mapping, and uses it to translate a +// host ID to the remapped ID. If no map is provided, then the translation +// assumes a 1-to-1 mapping and returns the passed in id +func ToContainer(hostID int, idMap []IDMap) (int, error) { + if idMap == nil { + return hostID, nil + } + for _, m := range idMap { + if (hostID >= m.HostID) && (hostID <= (m.HostID + m.Size - 1)) { + contID := m.ContainerID + (hostID - m.HostID) + return contID, nil + } + } + return -1, fmt.Errorf("Host ID %d cannot be mapped to a container ID", hostID) +} + +// ToHost takes an id mapping and a remapped ID, and translates the +// ID to the mapped host ID. If no map is provided, then the translation +// assumes a 1-to-1 mapping and returns the passed in id # +func ToHost(contID int, idMap []IDMap) (int, error) { + if idMap == nil { + return contID, nil + } + for _, m := range idMap { + if (contID >= m.ContainerID) && (contID <= (m.ContainerID + m.Size - 1)) { + hostID := m.HostID + (contID - m.ContainerID) + return hostID, nil + } + } + return -1, fmt.Errorf("Container ID %d cannot be mapped to a host ID", contID) +} + +// CreateIDMappings takes a requested user and group name and +// using the data from /etc/sub{uid,gid} ranges, creates the +// proper uid and gid remapping ranges for that user/group pair +func CreateIDMappings(username, groupname string) ([]IDMap, []IDMap, error) { + subuidRanges, err := parseSubuid(username) + if err != nil { + return nil, nil, err + } + subgidRanges, err := parseSubgid(groupname) + if err != nil { + return nil, nil, err + } + if len(subuidRanges) == 0 { + return nil, nil, fmt.Errorf("No subuid ranges found for user %q", username) + } + if len(subgidRanges) == 0 { + return nil, nil, fmt.Errorf("No subgid ranges found for group %q", groupname) + } + + return createIDMap(subuidRanges), createIDMap(subgidRanges), nil +} + +func createIDMap(subidRanges ranges) []IDMap { + idMap := []IDMap{} + + // sort the ranges by lowest ID first + sort.Sort(subidRanges) + containerID := 0 + for _, idrange := range subidRanges { + idMap = append(idMap, IDMap{ + ContainerID: containerID, + HostID: idrange.Start, + Size: idrange.Length, + }) + containerID = containerID + idrange.Length + } + return idMap +} + +func parseSubuid(username string) (ranges, error) { + return parseSubidFile(subuidFileName, username) +} + +func parseSubgid(username string) (ranges, error) { + return parseSubidFile(subgidFileName, username) +} + +// parseSubidFile will read the appropriate file (/etc/subuid or /etc/subgid) +// and return all found ranges for a specified username. If the special value +// "ALL" is supplied for username, then all ranges in the file will be returned +func parseSubidFile(path, username string) (ranges, error) { + var rangeList ranges + + subidFile, err := os.Open(path) + if err != nil { + return rangeList, err + } + defer subidFile.Close() + + s := bufio.NewScanner(subidFile) + for s.Scan() { + if err := s.Err(); err != nil { + return rangeList, err + } + + text := strings.TrimSpace(s.Text()) + if text == "" || strings.HasPrefix(text, "#") { + continue + } + parts := strings.Split(text, ":") + if len(parts) != 3 { + return rangeList, fmt.Errorf("Cannot parse subuid/gid information: Format not correct for %s file", path) + } + if parts[0] == username || username == "ALL" { + startid, err := strconv.Atoi(parts[1]) + if err != nil { + return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err) + } + length, err := strconv.Atoi(parts[2]) + if err != nil { + return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err) + } + rangeList = append(rangeList, subIDRange{startid, length}) + } + } + return rangeList, nil +} diff --git a/pkg/idtools/idtools_unix.go b/pkg/idtools/idtools_unix.go new file mode 100644 index 00000000..b57d6ef1 --- /dev/null +++ b/pkg/idtools/idtools_unix.go @@ -0,0 +1,60 @@ +// +build !windows + +package idtools + +import ( + "os" + "path/filepath" + + "github.com/docker/docker/pkg/system" +) + +func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chownExisting bool) error { + // make an array containing the original path asked for, plus (for mkAll == true) + // all path components leading up to the complete path that don't exist before we MkdirAll + // so that we can chown all of them properly at the end. If chownExisting is false, we won't + // chown the full directory path if it exists + var paths []string + if _, err := os.Stat(path); err != nil && os.IsNotExist(err) { + paths = []string{path} + } else if err == nil && chownExisting { + if err := os.Chown(path, ownerUID, ownerGID); err != nil { + return err + } + // short-circuit--we were called with an existing directory and chown was requested + return nil + } else if err == nil { + // nothing to do; directory path fully exists already and chown was NOT requested + return nil + } + + if mkAll { + // walk back to "/" looking for directories which do not exist + // and add them to the paths array for chown after creation + dirPath := path + for { + dirPath = filepath.Dir(dirPath) + if dirPath == "/" { + break + } + if _, err := os.Stat(dirPath); err != nil && os.IsNotExist(err) { + paths = append(paths, dirPath) + } + } + if err := system.MkdirAll(path, mode); err != nil && !os.IsExist(err) { + return err + } + } else { + if err := os.Mkdir(path, mode); err != nil && !os.IsExist(err) { + return err + } + } + // even if it existed, we will chown the requested path + any subpaths that + // didn't exist when we called MkdirAll + for _, pathComponent := range paths { + if err := os.Chown(pathComponent, ownerUID, ownerGID); err != nil { + return err + } + } + return nil +} diff --git a/pkg/idtools/idtools_unix_test.go b/pkg/idtools/idtools_unix_test.go new file mode 100644 index 00000000..540d3079 --- /dev/null +++ b/pkg/idtools/idtools_unix_test.go @@ -0,0 +1,271 @@ +// +build !windows + +package idtools + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "syscall" + "testing" +) + +type node struct { + uid int + gid int +} + +func TestMkdirAllAs(t *testing.T) { + dirName, err := ioutil.TempDir("", "mkdirall") + if err != nil { + t.Fatalf("Couldn't create temp dir: %v", err) + } + defer os.RemoveAll(dirName) + + testTree := map[string]node{ + "usr": {0, 0}, + "usr/bin": {0, 0}, + "lib": {33, 33}, + "lib/x86_64": {45, 45}, + "lib/x86_64/share": {1, 1}, + } + + if err := buildTree(dirName, testTree); err != nil { + t.Fatal(err) + } + + // test adding a directory to a pre-existing dir; only the new dir is owned by the uid/gid + if err := MkdirAllAs(filepath.Join(dirName, "usr", "share"), 0755, 99, 99); err != nil { + t.Fatal(err) + } + testTree["usr/share"] = node{99, 99} + verifyTree, err := readTree(dirName, "") + if err != nil { + t.Fatal(err) + } + if err := compareTrees(testTree, verifyTree); err != nil { + t.Fatal(err) + } + + // test 2-deep new directories--both should be owned by the uid/gid pair + if err := MkdirAllAs(filepath.Join(dirName, "lib", "some", "other"), 0755, 101, 101); err != nil { + t.Fatal(err) + } + testTree["lib/some"] = node{101, 101} + testTree["lib/some/other"] = node{101, 101} + verifyTree, err = readTree(dirName, "") + if err != nil { + t.Fatal(err) + } + if err := compareTrees(testTree, verifyTree); err != nil { + t.Fatal(err) + } + + // test a directory that already exists; should be chowned, but nothing else + if err := MkdirAllAs(filepath.Join(dirName, "usr"), 0755, 102, 102); err != nil { + t.Fatal(err) + } + testTree["usr"] = node{102, 102} + verifyTree, err = readTree(dirName, "") + if err != nil { + t.Fatal(err) + } + if err := compareTrees(testTree, verifyTree); err != nil { + t.Fatal(err) + } +} + +func TestMkdirAllNewAs(t *testing.T) { + + dirName, err := ioutil.TempDir("", "mkdirnew") + if err != nil { + t.Fatalf("Couldn't create temp dir: %v", err) + } + defer os.RemoveAll(dirName) + + testTree := map[string]node{ + "usr": {0, 0}, + "usr/bin": {0, 0}, + "lib": {33, 33}, + "lib/x86_64": {45, 45}, + "lib/x86_64/share": {1, 1}, + } + + if err := buildTree(dirName, testTree); err != nil { + t.Fatal(err) + } + + // test adding a directory to a pre-existing dir; only the new dir is owned by the uid/gid + if err := MkdirAllNewAs(filepath.Join(dirName, "usr", "share"), 0755, 99, 99); err != nil { + t.Fatal(err) + } + testTree["usr/share"] = node{99, 99} + verifyTree, err := readTree(dirName, "") + if err != nil { + t.Fatal(err) + } + if err := compareTrees(testTree, verifyTree); err != nil { + t.Fatal(err) + } + + // test 2-deep new directories--both should be owned by the uid/gid pair + if err := MkdirAllNewAs(filepath.Join(dirName, "lib", "some", "other"), 0755, 101, 101); err != nil { + t.Fatal(err) + } + testTree["lib/some"] = node{101, 101} + testTree["lib/some/other"] = node{101, 101} + verifyTree, err = readTree(dirName, "") + if err != nil { + t.Fatal(err) + } + if err := compareTrees(testTree, verifyTree); err != nil { + t.Fatal(err) + } + + // test a directory that already exists; should NOT be chowned + if err := MkdirAllNewAs(filepath.Join(dirName, "usr"), 0755, 102, 102); err != nil { + t.Fatal(err) + } + verifyTree, err = readTree(dirName, "") + if err != nil { + t.Fatal(err) + } + if err := compareTrees(testTree, verifyTree); err != nil { + t.Fatal(err) + } +} + +func TestMkdirAs(t *testing.T) { + + dirName, err := ioutil.TempDir("", "mkdir") + if err != nil { + t.Fatalf("Couldn't create temp dir: %v", err) + } + defer os.RemoveAll(dirName) + + testTree := map[string]node{ + "usr": {0, 0}, + } + if err := buildTree(dirName, testTree); err != nil { + t.Fatal(err) + } + + // test a directory that already exists; should just chown to the requested uid/gid + if err := MkdirAs(filepath.Join(dirName, "usr"), 0755, 99, 99); err != nil { + t.Fatal(err) + } + testTree["usr"] = node{99, 99} + verifyTree, err := readTree(dirName, "") + if err != nil { + t.Fatal(err) + } + if err := compareTrees(testTree, verifyTree); err != nil { + t.Fatal(err) + } + + // create a subdir under a dir which doesn't exist--should fail + if err := MkdirAs(filepath.Join(dirName, "usr", "bin", "subdir"), 0755, 102, 102); err == nil { + t.Fatalf("Trying to create a directory with Mkdir where the parent doesn't exist should have failed") + } + + // create a subdir under an existing dir; should only change the ownership of the new subdir + if err := MkdirAs(filepath.Join(dirName, "usr", "bin"), 0755, 102, 102); err != nil { + t.Fatal(err) + } + testTree["usr/bin"] = node{102, 102} + verifyTree, err = readTree(dirName, "") + if err != nil { + t.Fatal(err) + } + if err := compareTrees(testTree, verifyTree); err != nil { + t.Fatal(err) + } +} + +func buildTree(base string, tree map[string]node) error { + for path, node := range tree { + fullPath := filepath.Join(base, path) + if err := os.MkdirAll(fullPath, 0755); err != nil { + return fmt.Errorf("Couldn't create path: %s; error: %v", fullPath, err) + } + if err := os.Chown(fullPath, node.uid, node.gid); err != nil { + return fmt.Errorf("Couldn't chown path: %s; error: %v", fullPath, err) + } + } + return nil +} + +func readTree(base, root string) (map[string]node, error) { + tree := make(map[string]node) + + dirInfos, err := ioutil.ReadDir(base) + if err != nil { + return nil, fmt.Errorf("Couldn't read directory entries for %q: %v", base, err) + } + + for _, info := range dirInfos { + s := &syscall.Stat_t{} + if err := syscall.Stat(filepath.Join(base, info.Name()), s); err != nil { + return nil, fmt.Errorf("Can't stat file %q: %v", filepath.Join(base, info.Name()), err) + } + tree[filepath.Join(root, info.Name())] = node{int(s.Uid), int(s.Gid)} + if info.IsDir() { + // read the subdirectory + subtree, err := readTree(filepath.Join(base, info.Name()), filepath.Join(root, info.Name())) + if err != nil { + return nil, err + } + for path, nodeinfo := range subtree { + tree[path] = nodeinfo + } + } + } + return tree, nil +} + +func compareTrees(left, right map[string]node) error { + if len(left) != len(right) { + return fmt.Errorf("Trees aren't the same size") + } + for path, nodeLeft := range left { + if nodeRight, ok := right[path]; ok { + if nodeRight.uid != nodeLeft.uid || nodeRight.gid != nodeLeft.gid { + // mismatch + return fmt.Errorf("mismatched ownership for %q: expected: %d:%d, got: %d:%d", path, + nodeLeft.uid, nodeLeft.gid, nodeRight.uid, nodeRight.gid) + } + continue + } + return fmt.Errorf("right tree didn't contain path %q", path) + } + return nil +} + +func TestParseSubidFileWithNewlinesAndComments(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "parsesubid") + if err != nil { + t.Fatal(err) + } + fnamePath := filepath.Join(tmpDir, "testsubuid") + fcontent := `tss:100000:65536 +# empty default subuid/subgid file + +dockremap:231072:65536` + if err := ioutil.WriteFile(fnamePath, []byte(fcontent), 0644); err != nil { + t.Fatal(err) + } + ranges, err := parseSubidFile(fnamePath, "dockremap") + if err != nil { + t.Fatal(err) + } + if len(ranges) != 1 { + t.Fatalf("wanted 1 element in ranges, got %d instead", len(ranges)) + } + if ranges[0].Start != 231072 { + t.Fatalf("wanted 231072, got %d instead", ranges[0].Start) + } + if ranges[0].Length != 65536 { + t.Fatalf("wanted 65536, got %d instead", ranges[0].Length) + } +} diff --git a/pkg/idtools/idtools_windows.go b/pkg/idtools/idtools_windows.go new file mode 100644 index 00000000..c9e3c937 --- /dev/null +++ b/pkg/idtools/idtools_windows.go @@ -0,0 +1,18 @@ +// +build windows + +package idtools + +import ( + "os" + + "github.com/docker/docker/pkg/system" +) + +// Platforms such as Windows do not support the UID/GID concept. So make this +// just a wrapper around system.MkdirAll. +func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chownExisting bool) error { + if err := system.MkdirAll(path, mode); err != nil && !os.IsExist(err) { + return err + } + return nil +} diff --git a/pkg/idtools/usergroupadd_linux.go b/pkg/idtools/usergroupadd_linux.go new file mode 100644 index 00000000..86d9e21e --- /dev/null +++ b/pkg/idtools/usergroupadd_linux.go @@ -0,0 +1,188 @@ +package idtools + +import ( + "fmt" + "os/exec" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" +) + +// add a user and/or group to Linux /etc/passwd, /etc/group using standard +// Linux distribution commands: +// adduser --system --shell /bin/false --disabled-login --disabled-password --no-create-home --group +// useradd -r -s /bin/false + +var ( + userCommand string + + cmdTemplates = map[string]string{ + "adduser": "--system --shell /bin/false --no-create-home --disabled-login --disabled-password --group %s", + "useradd": "-r -s /bin/false %s", + "usermod": "-%s %d-%d %s", + } + + idOutRegexp = regexp.MustCompile(`uid=([0-9]+).*gid=([0-9]+)`) + // default length for a UID/GID subordinate range + defaultRangeLen = 65536 + defaultRangeStart = 100000 + userMod = "usermod" +) + +func init() { + // set up which commands are used for adding users/groups dependent on distro + if _, err := resolveBinary("adduser"); err == nil { + userCommand = "adduser" + } else if _, err := resolveBinary("useradd"); err == nil { + userCommand = "useradd" + } +} + +func resolveBinary(binname string) (string, error) { + binaryPath, err := exec.LookPath(binname) + if err != nil { + return "", err + } + resolvedPath, err := filepath.EvalSymlinks(binaryPath) + if err != nil { + return "", err + } + //only return no error if the final resolved binary basename + //matches what was searched for + if filepath.Base(resolvedPath) == binname { + return resolvedPath, nil + } + return "", fmt.Errorf("Binary %q does not resolve to a binary of that name in $PATH (%q)", binname, resolvedPath) +} + +// AddNamespaceRangesUser takes a username and uses the standard system +// utility to create a system user/group pair used to hold the +// /etc/sub{uid,gid} ranges which will be used for user namespace +// mapping ranges in containers. +func AddNamespaceRangesUser(name string) (int, int, error) { + if err := addUser(name); err != nil { + return -1, -1, fmt.Errorf("Error adding user %q: %v", name, err) + } + + // Query the system for the created uid and gid pair + out, err := execCmd("id", name) + if err != nil { + return -1, -1, fmt.Errorf("Error trying to find uid/gid for new user %q: %v", name, err) + } + matches := idOutRegexp.FindStringSubmatch(strings.TrimSpace(string(out))) + if len(matches) != 3 { + return -1, -1, fmt.Errorf("Can't find uid, gid from `id` output: %q", string(out)) + } + uid, err := strconv.Atoi(matches[1]) + if err != nil { + return -1, -1, fmt.Errorf("Can't convert found uid (%s) to int: %v", matches[1], err) + } + gid, err := strconv.Atoi(matches[2]) + if err != nil { + return -1, -1, fmt.Errorf("Can't convert found gid (%s) to int: %v", matches[2], err) + } + + // Now we need to create the subuid/subgid ranges for our new user/group (system users + // do not get auto-created ranges in subuid/subgid) + + if err := createSubordinateRanges(name); err != nil { + return -1, -1, fmt.Errorf("Couldn't create subordinate ID ranges: %v", err) + } + return uid, gid, nil +} + +func addUser(userName string) error { + + if userCommand == "" { + return fmt.Errorf("Cannot add user; no useradd/adduser binary found") + } + args := fmt.Sprintf(cmdTemplates[userCommand], userName) + out, err := execCmd(userCommand, args) + if err != nil { + return fmt.Errorf("Failed to add user with error: %v; output: %q", err, string(out)) + } + return nil +} + +func createSubordinateRanges(name string) error { + + // first, we should verify that ranges weren't automatically created + // by the distro tooling + ranges, err := parseSubuid(name) + if err != nil { + return fmt.Errorf("Error while looking for subuid ranges for user %q: %v", name, err) + } + if len(ranges) == 0 { + // no UID ranges; let's create one + startID, err := findNextUIDRange() + if err != nil { + return fmt.Errorf("Can't find available subuid range: %v", err) + } + out, err := execCmd(userMod, fmt.Sprintf(cmdTemplates[userMod], "v", startID, startID+defaultRangeLen-1, name)) + if err != nil { + return fmt.Errorf("Unable to add subuid range to user: %q; output: %s, err: %v", name, out, err) + } + } + + ranges, err = parseSubgid(name) + if err != nil { + return fmt.Errorf("Error while looking for subgid ranges for user %q: %v", name, err) + } + if len(ranges) == 0 { + // no GID ranges; let's create one + startID, err := findNextGIDRange() + if err != nil { + return fmt.Errorf("Can't find available subgid range: %v", err) + } + out, err := execCmd(userMod, fmt.Sprintf(cmdTemplates[userMod], "w", startID, startID+defaultRangeLen-1, name)) + if err != nil { + return fmt.Errorf("Unable to add subgid range to user: %q; output: %s, err: %v", name, out, err) + } + } + return nil +} + +func findNextUIDRange() (int, error) { + ranges, err := parseSubuid("ALL") + if err != nil { + return -1, fmt.Errorf("Couldn't parse all ranges in /etc/subuid file: %v", err) + } + sort.Sort(ranges) + return findNextRangeStart(ranges) +} + +func findNextGIDRange() (int, error) { + ranges, err := parseSubgid("ALL") + if err != nil { + return -1, fmt.Errorf("Couldn't parse all ranges in /etc/subgid file: %v", err) + } + sort.Sort(ranges) + return findNextRangeStart(ranges) +} + +func findNextRangeStart(rangeList ranges) (int, error) { + startID := defaultRangeStart + for _, arange := range rangeList { + if wouldOverlap(arange, startID) { + startID = arange.Start + arange.Length + } + } + return startID, nil +} + +func wouldOverlap(arange subIDRange, ID int) bool { + low := ID + high := ID + defaultRangeLen + if (low >= arange.Start && low <= arange.Start+arange.Length) || + (high <= arange.Start+arange.Length && high >= arange.Start) { + return true + } + return false +} + +func execCmd(cmd, args string) ([]byte, error) { + execCmd := exec.Command(cmd, strings.Split(args, " ")...) + return execCmd.CombinedOutput() +} diff --git a/pkg/idtools/usergroupadd_unsupported.go b/pkg/idtools/usergroupadd_unsupported.go new file mode 100644 index 00000000..d98b354c --- /dev/null +++ b/pkg/idtools/usergroupadd_unsupported.go @@ -0,0 +1,12 @@ +// +build !linux + +package idtools + +import "fmt" + +// AddNamespaceRangesUser takes a name and finds an unused uid, gid pair +// and calls the appropriate helper function to add the group and then +// the user to the group in /etc/group and /etc/passwd respectively. +func AddNamespaceRangesUser(name string) (int, int, error) { + return -1, -1, fmt.Errorf("No support for adding users or groups on this OS") +} diff --git a/pkg/integration/checker/checker.go b/pkg/integration/checker/checker.go new file mode 100644 index 00000000..a5314994 --- /dev/null +++ b/pkg/integration/checker/checker.go @@ -0,0 +1,46 @@ +// Package checker provide Docker specific implementations of the go-check.Checker interface. +package checker + +import ( + "github.com/go-check/check" + "github.com/vdemeester/shakers" +) + +// As a commodity, we bring all check.Checker variables into the current namespace to avoid having +// to think about check.X versus checker.X. +var ( + DeepEquals = check.DeepEquals + ErrorMatches = check.ErrorMatches + FitsTypeOf = check.FitsTypeOf + HasLen = check.HasLen + Implements = check.Implements + IsNil = check.IsNil + Matches = check.Matches + Not = check.Not + NotNil = check.NotNil + PanicMatches = check.PanicMatches + Panics = check.Panics + + Contains = shakers.Contains + ContainsAny = shakers.ContainsAny + Count = shakers.Count + Equals = shakers.Equals + EqualFold = shakers.EqualFold + False = shakers.False + GreaterOrEqualThan = shakers.GreaterOrEqualThan + GreaterThan = shakers.GreaterThan + HasPrefix = shakers.HasPrefix + HasSuffix = shakers.HasSuffix + Index = shakers.Index + IndexAny = shakers.IndexAny + IsAfter = shakers.IsAfter + IsBefore = shakers.IsBefore + IsBetween = shakers.IsBetween + IsLower = shakers.IsLower + IsUpper = shakers.IsUpper + LessOrEqualThan = shakers.LessOrEqualThan + LessThan = shakers.LessThan + TimeEquals = shakers.TimeEquals + True = shakers.True + TimeIgnore = shakers.TimeIgnore +) diff --git a/pkg/integration/dockerCmd_utils.go b/pkg/integration/dockerCmd_utils.go new file mode 100644 index 00000000..fab3e062 --- /dev/null +++ b/pkg/integration/dockerCmd_utils.go @@ -0,0 +1,78 @@ +package integration + +import ( + "fmt" + "os/exec" + "strings" + "time" + + "github.com/go-check/check" +) + +// We use the elongated quote mechanism for quoting error returns as +// the use of strconv.Quote or %q in fmt.Errorf will escape characters. This +// has a big downside on Windows where the args include paths, so instead +// of something like c:\directory\file.txt, the output would be +// c:\\directory\\file.txt. This is highly misleading. +const quote = `"` + +var execCommand = exec.Command + +// DockerCmdWithError executes a docker command that is supposed to fail and returns +// the output, the exit code and the error. +func DockerCmdWithError(dockerBinary string, args ...string) (string, int, error) { + return RunCommandWithOutput(execCommand(dockerBinary, args...)) +} + +// DockerCmdWithStdoutStderr executes a docker command and returns the content of the +// stdout, stderr and the exit code. If a check.C is passed, it will fail and stop tests +// if the error is not nil. +func DockerCmdWithStdoutStderr(dockerBinary string, c *check.C, args ...string) (string, string, int) { + stdout, stderr, status, err := RunCommandWithStdoutStderr(execCommand(dockerBinary, args...)) + if c != nil { + c.Assert(err, check.IsNil, check.Commentf(quote+"%v"+quote+" failed with errors: %s, %v", strings.Join(args, " "), stderr, err)) + } + return stdout, stderr, status +} + +// DockerCmd executes a docker command and returns the output and the exit code. If the +// command returns an error, it will fail and stop the tests. +func DockerCmd(dockerBinary string, c *check.C, args ...string) (string, int) { + out, status, err := RunCommandWithOutput(execCommand(dockerBinary, args...)) + c.Assert(err, check.IsNil, check.Commentf(quote+"%v"+quote+" failed with errors: %s, %v", strings.Join(args, " "), out, err)) + return out, status +} + +// DockerCmdWithTimeout executes a docker command with a timeout, and returns the output, +// the exit code and the error (if any). +func DockerCmdWithTimeout(dockerBinary string, timeout time.Duration, args ...string) (string, int, error) { + out, status, err := RunCommandWithOutputAndTimeout(execCommand(dockerBinary, args...), timeout) + if err != nil { + return out, status, fmt.Errorf(quote+"%v"+quote+" failed with errors: %v : %q", strings.Join(args, " "), err, out) + } + return out, status, err +} + +// DockerCmdInDir executes a docker command in a directory and returns the output, the +// exit code and the error (if any). +func DockerCmdInDir(dockerBinary string, path string, args ...string) (string, int, error) { + dockerCommand := execCommand(dockerBinary, args...) + dockerCommand.Dir = path + out, status, err := RunCommandWithOutput(dockerCommand) + if err != nil { + return out, status, fmt.Errorf(quote+"%v"+quote+" failed with errors: %v : %q", strings.Join(args, " "), err, out) + } + return out, status, err +} + +// DockerCmdInDirWithTimeout executes a docker command in a directory with a timeout and +// returns the output, the exit code and the error (if any). +func DockerCmdInDirWithTimeout(dockerBinary string, timeout time.Duration, path string, args ...string) (string, int, error) { + dockerCommand := execCommand(dockerBinary, args...) + dockerCommand.Dir = path + out, status, err := RunCommandWithOutputAndTimeout(dockerCommand, timeout) + if err != nil { + return out, status, fmt.Errorf(quote+"%v"+quote+" failed with errors: %v : %q", strings.Join(args, " "), err, out) + } + return out, status, err +} diff --git a/pkg/integration/dockerCmd_utils_test.go b/pkg/integration/dockerCmd_utils_test.go new file mode 100644 index 00000000..3dd5d114 --- /dev/null +++ b/pkg/integration/dockerCmd_utils_test.go @@ -0,0 +1,405 @@ +package integration + +import ( + "fmt" + "os" + "os/exec" + "testing" + + "io/ioutil" + "strings" + "time" + + "github.com/go-check/check" +) + +const dockerBinary = "docker" + +// Setup go-check for this test +func Test(t *testing.T) { + check.TestingT(t) +} + +func init() { + check.Suite(&DockerCmdSuite{}) +} + +type DockerCmdSuite struct{} + +// Fake the exec.Command to use our mock. +func (s *DockerCmdSuite) SetUpTest(c *check.C) { + execCommand = fakeExecCommand +} + +// And bring it back to normal after the test. +func (s *DockerCmdSuite) TearDownTest(c *check.C) { + execCommand = exec.Command +} + +// DockerCmdWithError tests + +func (s *DockerCmdSuite) TestDockerCmdWithError(c *check.C) { + cmds := []struct { + binary string + args []string + expectedOut string + expectedExitCode int + expectedError error + }{ + { + "doesnotexists", + []string{}, + "Command doesnotexists not found.", + 1, + fmt.Errorf("exit status 1"), + }, + { + dockerBinary, + []string{"an", "error"}, + "an error has occurred", + 1, + fmt.Errorf("exit status 1"), + }, + { + dockerBinary, + []string{"an", "exitCode", "127"}, + "an error has occurred with exitCode 127", + 127, + fmt.Errorf("exit status 127"), + }, + { + dockerBinary, + []string{"run", "-ti", "ubuntu", "echo", "hello"}, + "hello", + 0, + nil, + }, + } + for _, cmd := range cmds { + out, exitCode, error := DockerCmdWithError(cmd.binary, cmd.args...) + c.Assert(out, check.Equals, cmd.expectedOut, check.Commentf("Expected output %q for arguments %v, got %q", cmd.expectedOut, cmd.args, out)) + c.Assert(exitCode, check.Equals, cmd.expectedExitCode, check.Commentf("Expected exitCode %q for arguments %v, got %q", cmd.expectedExitCode, cmd.args, exitCode)) + if cmd.expectedError != nil { + c.Assert(error, check.NotNil, check.Commentf("Expected an error %q, got nothing", cmd.expectedError)) + c.Assert(error.Error(), check.Equals, cmd.expectedError.Error(), check.Commentf("Expected error %q for arguments %v, got %q", cmd.expectedError.Error(), cmd.args, error.Error())) + } else { + c.Assert(error, check.IsNil, check.Commentf("Expected no error, got %v", error)) + } + } +} + +// DockerCmdWithStdoutStderr tests + +type dockerCmdWithStdoutStderrErrorSuite struct{} + +func (s *dockerCmdWithStdoutStderrErrorSuite) Test(c *check.C) { + // Should fail, the test too + DockerCmdWithStdoutStderr(dockerBinary, c, "an", "error") +} + +type dockerCmdWithStdoutStderrSuccessSuite struct{} + +func (s *dockerCmdWithStdoutStderrSuccessSuite) Test(c *check.C) { + stdout, stderr, exitCode := DockerCmdWithStdoutStderr(dockerBinary, c, "run", "-ti", "ubuntu", "echo", "hello") + c.Assert(stdout, check.Equals, "hello") + c.Assert(stderr, check.Equals, "") + c.Assert(exitCode, check.Equals, 0) + +} + +func (s *DockerCmdSuite) TestDockerCmdWithStdoutStderrError(c *check.C) { + // Run error suite, should fail. + output := String{} + result := check.Run(&dockerCmdWithStdoutStderrErrorSuite{}, &check.RunConf{Output: &output}) + c.Check(result.Succeeded, check.Equals, 0) + c.Check(result.Failed, check.Equals, 1) +} + +func (s *DockerCmdSuite) TestDockerCmdWithStdoutStderrSuccess(c *check.C) { + // Run error suite, should fail. + output := String{} + result := check.Run(&dockerCmdWithStdoutStderrSuccessSuite{}, &check.RunConf{Output: &output}) + c.Check(result.Succeeded, check.Equals, 1) + c.Check(result.Failed, check.Equals, 0) +} + +// DockerCmd tests + +type dockerCmdErrorSuite struct{} + +func (s *dockerCmdErrorSuite) Test(c *check.C) { + // Should fail, the test too + DockerCmd(dockerBinary, c, "an", "error") +} + +type dockerCmdSuccessSuite struct{} + +func (s *dockerCmdSuccessSuite) Test(c *check.C) { + stdout, exitCode := DockerCmd(dockerBinary, c, "run", "-ti", "ubuntu", "echo", "hello") + c.Assert(stdout, check.Equals, "hello") + c.Assert(exitCode, check.Equals, 0) + +} + +func (s *DockerCmdSuite) TestDockerCmdError(c *check.C) { + // Run error suite, should fail. + output := String{} + result := check.Run(&dockerCmdErrorSuite{}, &check.RunConf{Output: &output}) + c.Check(result.Succeeded, check.Equals, 0) + c.Check(result.Failed, check.Equals, 1) +} + +func (s *DockerCmdSuite) TestDockerCmdSuccess(c *check.C) { + // Run error suite, should fail. + output := String{} + result := check.Run(&dockerCmdSuccessSuite{}, &check.RunConf{Output: &output}) + c.Check(result.Succeeded, check.Equals, 1) + c.Check(result.Failed, check.Equals, 0) +} + +// DockerCmdWithTimeout tests + +func (s *DockerCmdSuite) TestDockerCmdWithTimeout(c *check.C) { + cmds := []struct { + binary string + args []string + timeout time.Duration + expectedOut string + expectedExitCode int + expectedError error + }{ + { + "doesnotexists", + []string{}, + 200 * time.Millisecond, + `Command doesnotexists not found.`, + 1, + fmt.Errorf(`"" failed with errors: exit status 1 : "Command doesnotexists not found."`), + }, + { + dockerBinary, + []string{"an", "error"}, + 200 * time.Millisecond, + `an error has occurred`, + 1, + fmt.Errorf(`"an error" failed with errors: exit status 1 : "an error has occurred"`), + }, + { + dockerBinary, + []string{"a", "command", "that", "times", "out"}, + 5 * time.Millisecond, + "", + 0, + fmt.Errorf(`"a command that times out" failed with errors: command timed out : ""`), + }, + { + dockerBinary, + []string{"run", "-ti", "ubuntu", "echo", "hello"}, + 200 * time.Millisecond, + "hello", + 0, + nil, + }, + } + for _, cmd := range cmds { + out, exitCode, error := DockerCmdWithTimeout(cmd.binary, cmd.timeout, cmd.args...) + c.Assert(out, check.Equals, cmd.expectedOut, check.Commentf("Expected output %q for arguments %v, got %q", cmd.expectedOut, cmd.args, out)) + c.Assert(exitCode, check.Equals, cmd.expectedExitCode, check.Commentf("Expected exitCode %q for arguments %v, got %q", cmd.expectedExitCode, cmd.args, exitCode)) + if cmd.expectedError != nil { + c.Assert(error, check.NotNil, check.Commentf("Expected an error %q, got nothing", cmd.expectedError)) + c.Assert(error.Error(), check.Equals, cmd.expectedError.Error(), check.Commentf("Expected error %q for arguments %v, got %q", cmd.expectedError.Error(), cmd.args, error.Error())) + } else { + c.Assert(error, check.IsNil, check.Commentf("Expected no error, got %v", error)) + } + } +} + +// DockerCmdInDir tests + +func (s *DockerCmdSuite) TestDockerCmdInDir(c *check.C) { + tempFolder, err := ioutil.TempDir("", "test-docker-cmd-in-dir") + c.Assert(err, check.IsNil) + + cmds := []struct { + binary string + args []string + expectedOut string + expectedExitCode int + expectedError error + }{ + { + "doesnotexists", + []string{}, + `Command doesnotexists not found.`, + 1, + fmt.Errorf(`"dir:%s" failed with errors: exit status 1 : "Command doesnotexists not found."`, tempFolder), + }, + { + dockerBinary, + []string{"an", "error"}, + `an error has occurred`, + 1, + fmt.Errorf(`"dir:%s an error" failed with errors: exit status 1 : "an error has occurred"`, tempFolder), + }, + { + dockerBinary, + []string{"run", "-ti", "ubuntu", "echo", "hello"}, + "hello", + 0, + nil, + }, + } + for _, cmd := range cmds { + // We prepend the arguments with dir:thefolder.. the fake command will check + // that the current workdir is the same as the one we are passing. + args := append([]string{"dir:" + tempFolder}, cmd.args...) + out, exitCode, error := DockerCmdInDir(cmd.binary, tempFolder, args...) + c.Assert(out, check.Equals, cmd.expectedOut, check.Commentf("Expected output %q for arguments %v, got %q", cmd.expectedOut, cmd.args, out)) + c.Assert(exitCode, check.Equals, cmd.expectedExitCode, check.Commentf("Expected exitCode %q for arguments %v, got %q", cmd.expectedExitCode, cmd.args, exitCode)) + if cmd.expectedError != nil { + c.Assert(error, check.NotNil, check.Commentf("Expected an error %q, got nothing", cmd.expectedError)) + c.Assert(error.Error(), check.Equals, cmd.expectedError.Error(), check.Commentf("Expected error %q for arguments %v, got %q", cmd.expectedError.Error(), cmd.args, error.Error())) + } else { + c.Assert(error, check.IsNil, check.Commentf("Expected no error, got %v", error)) + } + } +} + +// DockerCmdInDirWithTimeout tests + +func (s *DockerCmdSuite) TestDockerCmdInDirWithTimeout(c *check.C) { + tempFolder, err := ioutil.TempDir("", "test-docker-cmd-in-dir") + c.Assert(err, check.IsNil) + + cmds := []struct { + binary string + args []string + timeout time.Duration + expectedOut string + expectedExitCode int + expectedError error + }{ + { + "doesnotexists", + []string{}, + 200 * time.Millisecond, + `Command doesnotexists not found.`, + 1, + fmt.Errorf(`"dir:%s" failed with errors: exit status 1 : "Command doesnotexists not found."`, tempFolder), + }, + { + dockerBinary, + []string{"an", "error"}, + 200 * time.Millisecond, + `an error has occurred`, + 1, + fmt.Errorf(`"dir:%s an error" failed with errors: exit status 1 : "an error has occurred"`, tempFolder), + }, + { + dockerBinary, + []string{"a", "command", "that", "times", "out"}, + 5 * time.Millisecond, + "", + 0, + fmt.Errorf(`"dir:%s a command that times out" failed with errors: command timed out : ""`, tempFolder), + }, + { + dockerBinary, + []string{"run", "-ti", "ubuntu", "echo", "hello"}, + 200 * time.Millisecond, + "hello", + 0, + nil, + }, + } + for _, cmd := range cmds { + // We prepend the arguments with dir:thefolder.. the fake command will check + // that the current workdir is the same as the one we are passing. + args := append([]string{"dir:" + tempFolder}, cmd.args...) + out, exitCode, error := DockerCmdInDirWithTimeout(cmd.binary, cmd.timeout, tempFolder, args...) + c.Assert(out, check.Equals, cmd.expectedOut, check.Commentf("Expected output %q for arguments %v, got %q", cmd.expectedOut, cmd.args, out)) + c.Assert(exitCode, check.Equals, cmd.expectedExitCode, check.Commentf("Expected exitCode %q for arguments %v, got %q", cmd.expectedExitCode, cmd.args, exitCode)) + if cmd.expectedError != nil { + c.Assert(error, check.NotNil, check.Commentf("Expected an error %q, got nothing", cmd.expectedError)) + c.Assert(error.Error(), check.Equals, cmd.expectedError.Error(), check.Commentf("Expected error %q for arguments %v, got %q", cmd.expectedError.Error(), cmd.args, error.Error())) + } else { + c.Assert(error, check.IsNil, check.Commentf("Expected no error, got %v", error)) + } + } +} + +// Helpers :) + +// Type implementing the io.Writer interface for analyzing output. +type String struct { + value string +} + +// The only function required by the io.Writer interface. Will append +// written data to the String.value string. +func (s *String) Write(p []byte) (n int, err error) { + s.value += string(p) + return len(p), nil +} + +// Helper function that mock the exec.Command call (and call the test binary) +func fakeExecCommand(command string, args ...string) *exec.Cmd { + cs := []string{"-test.run=TestHelperProcess", "--", command} + cs = append(cs, args...) + cmd := exec.Command(os.Args[0], cs...) + cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} + return cmd +} + +func TestHelperProcess(t *testing.T) { + if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { + return + } + args := os.Args + + // Previous arguments are tests stuff, that looks like : + // /tmp/go-build970079519/…/_test/integration.test -test.run=TestHelperProcess -- + cmd, args := args[3], args[4:] + // Handle the case where args[0] is dir:... + if len(args) > 0 && strings.HasPrefix(args[0], "dir:") { + expectedCwd := args[0][4:] + if len(args) > 1 { + args = args[1:] + } + cwd, err := os.Getwd() + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get workingdir: %v", err) + os.Exit(1) + } + // This checks that the given path is the same as the currend working dire + if expectedCwd != cwd { + fmt.Fprintf(os.Stderr, "Current workdir should be %q, but is %q", expectedCwd, cwd) + } + } + switch cmd { + case dockerBinary: + argsStr := strings.Join(args, " ") + switch argsStr { + case "an exitCode 127": + fmt.Fprintf(os.Stderr, "an error has occurred with exitCode 127") + os.Exit(127) + case "an error": + fmt.Fprintf(os.Stderr, "an error has occurred") + os.Exit(1) + case "a command that times out": + time.Sleep(10 * time.Second) + fmt.Fprintf(os.Stdout, "too long, should be killed") + // A random exit code (that should never happened in tests) + os.Exit(7) + case "run -ti ubuntu echo hello": + fmt.Fprintf(os.Stdout, "hello") + default: + fmt.Fprintf(os.Stdout, "no arguments") + } + default: + fmt.Fprintf(os.Stderr, "Command %s not found.", cmd) + os.Exit(1) + } + // some code here to check arguments perhaps? + os.Exit(0) +} diff --git a/pkg/integration/utils.go b/pkg/integration/utils.go new file mode 100644 index 00000000..cfccc801 --- /dev/null +++ b/pkg/integration/utils.go @@ -0,0 +1,361 @@ +package integration + +import ( + "archive/tar" + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "reflect" + "strings" + "syscall" + "time" + + "github.com/docker/docker/pkg/stringutils" +) + +// GetExitCode returns the ExitStatus of the specified error if its type is +// exec.ExitError, returns 0 and an error otherwise. +func GetExitCode(err error) (int, error) { + exitCode := 0 + if exiterr, ok := err.(*exec.ExitError); ok { + if procExit, ok := exiterr.Sys().(syscall.WaitStatus); ok { + return procExit.ExitStatus(), nil + } + } + return exitCode, fmt.Errorf("failed to get exit code") +} + +// ProcessExitCode process the specified error and returns the exit status code +// if the error was of type exec.ExitError, returns nothing otherwise. +func ProcessExitCode(err error) (exitCode int) { + if err != nil { + var exiterr error + if exitCode, exiterr = GetExitCode(err); exiterr != nil { + // TODO: Fix this so we check the error's text. + // we've failed to retrieve exit code, so we set it to 127 + exitCode = 127 + } + } + return +} + +// IsKilled process the specified error and returns whether the process was killed or not. +func IsKilled(err error) bool { + if exitErr, ok := err.(*exec.ExitError); ok { + status, ok := exitErr.Sys().(syscall.WaitStatus) + if !ok { + return false + } + // status.ExitStatus() is required on Windows because it does not + // implement Signal() nor Signaled(). Just check it had a bad exit + // status could mean it was killed (and in tests we do kill) + return (status.Signaled() && status.Signal() == os.Kill) || status.ExitStatus() != 0 + } + return false +} + +// RunCommandWithOutput runs the specified command and returns the combined output (stdout/stderr) +// with the exitCode different from 0 and the error if something bad happened +func RunCommandWithOutput(cmd *exec.Cmd) (output string, exitCode int, err error) { + exitCode = 0 + out, err := cmd.CombinedOutput() + exitCode = ProcessExitCode(err) + output = string(out) + return +} + +// RunCommandWithStdoutStderr runs the specified command and returns stdout and stderr separately +// with the exitCode different from 0 and the error if something bad happened +func RunCommandWithStdoutStderr(cmd *exec.Cmd) (stdout string, stderr string, exitCode int, err error) { + var ( + stderrBuffer, stdoutBuffer bytes.Buffer + ) + exitCode = 0 + cmd.Stderr = &stderrBuffer + cmd.Stdout = &stdoutBuffer + err = cmd.Run() + exitCode = ProcessExitCode(err) + + stdout = stdoutBuffer.String() + stderr = stderrBuffer.String() + return +} + +// RunCommandWithOutputForDuration runs the specified command "timeboxed" by the specified duration. +// If the process is still running when the timebox is finished, the process will be killed and . +// It will returns the output with the exitCode different from 0 and the error if something bad happened +// and a boolean whether it has been killed or not. +func RunCommandWithOutputForDuration(cmd *exec.Cmd, duration time.Duration) (output string, exitCode int, timedOut bool, err error) { + var outputBuffer bytes.Buffer + if cmd.Stdout != nil { + err = errors.New("cmd.Stdout already set") + return + } + cmd.Stdout = &outputBuffer + + if cmd.Stderr != nil { + err = errors.New("cmd.Stderr already set") + return + } + cmd.Stderr = &outputBuffer + + // Start the command in the main thread.. + err = cmd.Start() + if err != nil { + err = fmt.Errorf("Fail to start command %v : %v", cmd, err) + } + + type exitInfo struct { + exitErr error + exitCode int + } + + done := make(chan exitInfo, 1) + + go func() { + // And wait for it to exit in the goroutine :) + info := exitInfo{} + info.exitErr = cmd.Wait() + info.exitCode = ProcessExitCode(info.exitErr) + done <- info + }() + + select { + case <-time.After(duration): + killErr := cmd.Process.Kill() + if killErr != nil { + fmt.Printf("failed to kill (pid=%d): %v\n", cmd.Process.Pid, killErr) + } + timedOut = true + case info := <-done: + err = info.exitErr + exitCode = info.exitCode + } + output = outputBuffer.String() + return +} + +var errCmdTimeout = fmt.Errorf("command timed out") + +// RunCommandWithOutputAndTimeout runs the specified command "timeboxed" by the specified duration. +// It returns the output with the exitCode different from 0 and the error if something bad happened or +// if the process timed out (and has been killed). +func RunCommandWithOutputAndTimeout(cmd *exec.Cmd, timeout time.Duration) (output string, exitCode int, err error) { + var timedOut bool + output, exitCode, timedOut, err = RunCommandWithOutputForDuration(cmd, timeout) + if timedOut { + err = errCmdTimeout + } + return +} + +// RunCommand runs the specified command and returns the exitCode different from 0 +// and the error if something bad happened. +func RunCommand(cmd *exec.Cmd) (exitCode int, err error) { + exitCode = 0 + err = cmd.Run() + exitCode = ProcessExitCode(err) + return +} + +// RunCommandPipelineWithOutput runs the array of commands with the output +// of each pipelined with the following (like cmd1 | cmd2 | cmd3 would do). +// It returns the final output, the exitCode different from 0 and the error +// if something bad happened. +func RunCommandPipelineWithOutput(cmds ...*exec.Cmd) (output string, exitCode int, err error) { + if len(cmds) < 2 { + return "", 0, errors.New("pipeline does not have multiple cmds") + } + + // connect stdin of each cmd to stdout pipe of previous cmd + for i, cmd := range cmds { + if i > 0 { + prevCmd := cmds[i-1] + cmd.Stdin, err = prevCmd.StdoutPipe() + + if err != nil { + return "", 0, fmt.Errorf("cannot set stdout pipe for %s: %v", cmd.Path, err) + } + } + } + + // start all cmds except the last + for _, cmd := range cmds[:len(cmds)-1] { + if err = cmd.Start(); err != nil { + return "", 0, fmt.Errorf("starting %s failed with error: %v", cmd.Path, err) + } + } + + var pipelineError error + defer func() { + // wait all cmds except the last to release their resources + for _, cmd := range cmds[:len(cmds)-1] { + if err := cmd.Wait(); err != nil { + pipelineError = fmt.Errorf("command %s failed with error: %v", cmd.Path, err) + break + } + } + }() + if pipelineError != nil { + return "", 0, pipelineError + } + + // wait on last cmd + return RunCommandWithOutput(cmds[len(cmds)-1]) +} + +// UnmarshalJSON deserialize a JSON in the given interface. +func UnmarshalJSON(data []byte, result interface{}) error { + if err := json.Unmarshal(data, result); err != nil { + return err + } + + return nil +} + +// ConvertSliceOfStringsToMap converts a slices of string in a map +// with the strings as key and an empty string as values. +func ConvertSliceOfStringsToMap(input []string) map[string]struct{} { + output := make(map[string]struct{}) + for _, v := range input { + output[v] = struct{}{} + } + return output +} + +// CompareDirectoryEntries compares two sets of FileInfo (usually taken from a directory) +// and returns an error if different. +func CompareDirectoryEntries(e1 []os.FileInfo, e2 []os.FileInfo) error { + var ( + e1Entries = make(map[string]struct{}) + e2Entries = make(map[string]struct{}) + ) + for _, e := range e1 { + e1Entries[e.Name()] = struct{}{} + } + for _, e := range e2 { + e2Entries[e.Name()] = struct{}{} + } + if !reflect.DeepEqual(e1Entries, e2Entries) { + return fmt.Errorf("entries differ") + } + return nil +} + +// ListTar lists the entries of a tar. +func ListTar(f io.Reader) ([]string, error) { + tr := tar.NewReader(f) + var entries []string + + for { + th, err := tr.Next() + if err == io.EOF { + // end of tar archive + return entries, nil + } + if err != nil { + return entries, err + } + entries = append(entries, th.Name) + } +} + +// RandomTmpDirPath provides a temporary path with rand string appended. +// does not create or checks if it exists. +func RandomTmpDirPath(s string, platform string) string { + tmp := "/tmp" + if platform == "windows" { + tmp = os.Getenv("TEMP") + } + path := filepath.Join(tmp, fmt.Sprintf("%s.%s", s, stringutils.GenerateRandomAlphaOnlyString(10))) + if platform == "windows" { + return filepath.FromSlash(path) // Using \ + } + return filepath.ToSlash(path) // Using / +} + +// ConsumeWithSpeed reads chunkSize bytes from reader before sleeping +// for interval duration. Returns total read bytes. Send true to the +// stop channel to return before reading to EOF on the reader. +func ConsumeWithSpeed(reader io.Reader, chunkSize int, interval time.Duration, stop chan bool) (n int, err error) { + buffer := make([]byte, chunkSize) + for { + var readBytes int + readBytes, err = reader.Read(buffer) + n += readBytes + if err != nil { + if err == io.EOF { + err = nil + } + return + } + select { + case <-stop: + return + case <-time.After(interval): + } + } +} + +// ParseCgroupPaths parses 'procCgroupData', which is output of '/proc//cgroup', and returns +// a map which cgroup name as key and path as value. +func ParseCgroupPaths(procCgroupData string) map[string]string { + cgroupPaths := map[string]string{} + for _, line := range strings.Split(procCgroupData, "\n") { + parts := strings.Split(line, ":") + if len(parts) != 3 { + continue + } + cgroupPaths[parts[1]] = parts[2] + } + return cgroupPaths +} + +// ChannelBuffer holds a chan of byte array that can be populate in a goroutine. +type ChannelBuffer struct { + C chan []byte +} + +// Write implements Writer. +func (c *ChannelBuffer) Write(b []byte) (int, error) { + c.C <- b + return len(b), nil +} + +// Close closes the go channel. +func (c *ChannelBuffer) Close() error { + close(c.C) + return nil +} + +// ReadTimeout reads the content of the channel in the specified byte array with +// the specified duration as timeout. +func (c *ChannelBuffer) ReadTimeout(p []byte, n time.Duration) (int, error) { + select { + case b := <-c.C: + return copy(p[0:], b), nil + case <-time.After(n): + return -1, fmt.Errorf("timeout reading from channel") + } +} + +// RunAtDifferentDate runs the specified function with the given time. +// It changes the date of the system, which can led to weird behaviors. +func RunAtDifferentDate(date time.Time, block func()) { + // Layout for date. MMDDhhmmYYYY + const timeLayout = "010203042006" + // Ensure we bring time back to now + now := time.Now().Format(timeLayout) + dateReset := exec.Command("date", now) + defer RunCommand(dateReset) + + dateChange := exec.Command("date", date.Format(timeLayout)) + RunCommand(dateChange) + block() + return +} diff --git a/pkg/integration/utils_test.go b/pkg/integration/utils_test.go new file mode 100644 index 00000000..d166489e --- /dev/null +++ b/pkg/integration/utils_test.go @@ -0,0 +1,570 @@ +package integration + +import ( + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + "testing" + "time" +) + +func TestIsKilledFalseWithNonKilledProcess(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + + lsCmd := exec.Command("ls") + lsCmd.Start() + // Wait for it to finish + err := lsCmd.Wait() + if IsKilled(err) { + t.Fatalf("Expected the ls command to not be killed, was.") + } +} + +func TestIsKilledTrueWithKilledProcess(t *testing.T) { + // TODO Windows: Using golang 1.5.3, this seems to hit + // a bug in go where Process.Kill() causes a panic. + // Needs further investigation @jhowardmsft + if runtime.GOOS == "windows" { + t.SkipNow() + } + longCmd := exec.Command("top") + // Start a command + longCmd.Start() + // Capture the error when *dying* + done := make(chan error, 1) + go func() { + done <- longCmd.Wait() + }() + // Then kill it + longCmd.Process.Kill() + // Get the error + err := <-done + if !IsKilled(err) { + t.Fatalf("Expected the command to be killed, was not.") + } +} + +func TestRunCommandWithOutput(t *testing.T) { + var ( + echoHelloWorldCmd *exec.Cmd + expected string + ) + if runtime.GOOS != "windows" { + echoHelloWorldCmd = exec.Command("echo", "hello", "world") + expected = "hello world\n" + } else { + echoHelloWorldCmd = exec.Command("cmd", "/s", "/c", "echo", "hello", "world") + expected = "hello world\r\n" + } + + out, exitCode, err := RunCommandWithOutput(echoHelloWorldCmd) + if out != expected || exitCode != 0 || err != nil { + t.Fatalf("Expected command to output %s, got %s, %v with exitCode %v", expected, out, err, exitCode) + } +} + +func TestRunCommandWithOutputError(t *testing.T) { + var ( + p string + wrongCmd *exec.Cmd + expected string + expectedExitCode int + ) + + if runtime.GOOS != "windows" { + p = "$PATH" + wrongCmd = exec.Command("ls", "-z") + expected = `ls: invalid option -- 'z' +Try 'ls --help' for more information. +` + expectedExitCode = 2 + } else { + p = "%PATH%" + wrongCmd = exec.Command("cmd", "/s", "/c", "dir", "/Z") + expected = "Invalid switch - " + strconv.Quote("Z") + ".\r\n" + expectedExitCode = 1 + } + cmd := exec.Command("doesnotexists") + out, exitCode, err := RunCommandWithOutput(cmd) + expectedError := `exec: "doesnotexists": executable file not found in ` + p + if out != "" || exitCode != 127 || err == nil || err.Error() != expectedError { + t.Fatalf("Expected command to output %s, got %s, %v with exitCode %v", expectedError, out, err, exitCode) + } + + out, exitCode, err = RunCommandWithOutput(wrongCmd) + + if out != expected || exitCode != expectedExitCode || err == nil || !strings.Contains(err.Error(), "exit status "+strconv.Itoa(expectedExitCode)) { + t.Fatalf("Expected command to output %s, got out:xxx%sxxx, err:%v with exitCode %v", expected, out, err, exitCode) + } +} + +func TestRunCommandWithStdoutStderr(t *testing.T) { + echoHelloWorldCmd := exec.Command("echo", "hello", "world") + stdout, stderr, exitCode, err := RunCommandWithStdoutStderr(echoHelloWorldCmd) + expected := "hello world\n" + if stdout != expected || stderr != "" || exitCode != 0 || err != nil { + t.Fatalf("Expected command to output %s, got stdout:%s, stderr:%s, err:%v with exitCode %v", expected, stdout, stderr, err, exitCode) + } +} + +func TestRunCommandWithStdoutStderrError(t *testing.T) { + p := "$PATH" + if runtime.GOOS == "windows" { + p = "%PATH%" + } + cmd := exec.Command("doesnotexists") + stdout, stderr, exitCode, err := RunCommandWithStdoutStderr(cmd) + expectedError := `exec: "doesnotexists": executable file not found in ` + p + if stdout != "" || stderr != "" || exitCode != 127 || err == nil || err.Error() != expectedError { + t.Fatalf("Expected command to output out:%s, stderr:%s, got stdout:%s, stderr:%s, err:%v with exitCode %v", "", "", stdout, stderr, err, exitCode) + } + + wrongLsCmd := exec.Command("ls", "-z") + expected := `ls: invalid option -- 'z' +Try 'ls --help' for more information. +` + + stdout, stderr, exitCode, err = RunCommandWithStdoutStderr(wrongLsCmd) + if stdout != "" && stderr != expected || exitCode != 2 || err == nil || err.Error() != "exit status 2" { + t.Fatalf("Expected command to output out:%s, stderr:%s, got stdout:%s, stderr:%s, err:%v with exitCode %v", "", expectedError, stdout, stderr, err, exitCode) + } +} + +func TestRunCommandWithOutputForDurationFinished(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + + cmd := exec.Command("ls") + out, exitCode, timedOut, err := RunCommandWithOutputForDuration(cmd, 50*time.Millisecond) + if out == "" || exitCode != 0 || timedOut || err != nil { + t.Fatalf("Expected the command to run for less 50 milliseconds and thus not time out, but did not : out:[%s], exitCode:[%d], timedOut:[%v], err:[%v]", out, exitCode, timedOut, err) + } +} + +func TestRunCommandWithOutputForDurationKilled(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + cmd := exec.Command("sh", "-c", "while true ; do echo 1 ; sleep .1 ; done") + out, exitCode, timedOut, err := RunCommandWithOutputForDuration(cmd, 500*time.Millisecond) + ones := strings.Split(out, "\n") + if len(ones) != 6 || exitCode != 0 || !timedOut || err != nil { + t.Fatalf("Expected the command to run for 500 milliseconds (and thus print six lines (five with 1, one empty) and time out, but did not : out:[%s], exitCode:%d, timedOut:%v, err:%v", out, exitCode, timedOut, err) + } +} + +func TestRunCommandWithOutputForDurationErrors(t *testing.T) { + cmd := exec.Command("ls") + cmd.Stdout = os.Stdout + if _, _, _, err := RunCommandWithOutputForDuration(cmd, 1*time.Millisecond); err == nil || err.Error() != "cmd.Stdout already set" { + t.Fatalf("Expected an error as cmd.Stdout was already set, did not (err:%s).", err) + } + cmd = exec.Command("ls") + cmd.Stderr = os.Stderr + if _, _, _, err := RunCommandWithOutputForDuration(cmd, 1*time.Millisecond); err == nil || err.Error() != "cmd.Stderr already set" { + t.Fatalf("Expected an error as cmd.Stderr was already set, did not (err:%s).", err) + } +} + +func TestRunCommandWithOutputAndTimeoutFinished(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + + cmd := exec.Command("ls") + out, exitCode, err := RunCommandWithOutputAndTimeout(cmd, 50*time.Millisecond) + if out == "" || exitCode != 0 || err != nil { + t.Fatalf("Expected the command to run for less 50 milliseconds and thus not time out, but did not : out:[%s], exitCode:[%d], err:[%v]", out, exitCode, err) + } +} + +func TestRunCommandWithOutputAndTimeoutKilled(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + + cmd := exec.Command("sh", "-c", "while true ; do echo 1 ; sleep .1 ; done") + out, exitCode, err := RunCommandWithOutputAndTimeout(cmd, 500*time.Millisecond) + ones := strings.Split(out, "\n") + if len(ones) != 6 || exitCode != 0 || err == nil || err.Error() != "command timed out" { + t.Fatalf("Expected the command to run for 500 milliseconds (and thus print six lines (five with 1, one empty) and time out with an error 'command timed out', but did not : out:[%s], exitCode:%d, err:%v", out, exitCode, err) + } +} + +func TestRunCommandWithOutputAndTimeoutErrors(t *testing.T) { + cmd := exec.Command("ls") + cmd.Stdout = os.Stdout + if _, _, err := RunCommandWithOutputAndTimeout(cmd, 1*time.Millisecond); err == nil || err.Error() != "cmd.Stdout already set" { + t.Fatalf("Expected an error as cmd.Stdout was already set, did not (err:%s).", err) + } + cmd = exec.Command("ls") + cmd.Stderr = os.Stderr + if _, _, err := RunCommandWithOutputAndTimeout(cmd, 1*time.Millisecond); err == nil || err.Error() != "cmd.Stderr already set" { + t.Fatalf("Expected an error as cmd.Stderr was already set, did not (err:%s).", err) + } +} + +func TestRunCommand(t *testing.T) { + // TODO Windows: Port this test + if runtime.GOOS == "windows" { + t.Skip("Needs porting to Windows") + } + + p := "$PATH" + if runtime.GOOS == "windows" { + p = "%PATH%" + } + lsCmd := exec.Command("ls") + exitCode, err := RunCommand(lsCmd) + if exitCode != 0 || err != nil { + t.Fatalf("Expected runCommand to run the command successfully, got: exitCode:%d, err:%v", exitCode, err) + } + + var expectedError string + + exitCode, err = RunCommand(exec.Command("doesnotexists")) + expectedError = `exec: "doesnotexists": executable file not found in ` + p + if exitCode != 127 || err == nil || err.Error() != expectedError { + t.Fatalf("Expected runCommand to run the command successfully, got: exitCode:%d, err:%v", exitCode, err) + } + wrongLsCmd := exec.Command("ls", "-z") + expected := 2 + expectedError = `exit status 2` + exitCode, err = RunCommand(wrongLsCmd) + if exitCode != expected || err == nil || err.Error() != expectedError { + t.Fatalf("Expected runCommand to run the command successfully, got: exitCode:%d, err:%v", exitCode, err) + } +} + +func TestRunCommandPipelineWithOutputWithNotEnoughCmds(t *testing.T) { + _, _, err := RunCommandPipelineWithOutput(exec.Command("ls")) + expectedError := "pipeline does not have multiple cmds" + if err == nil || err.Error() != expectedError { + t.Fatalf("Expected an error with %s, got err:%s", expectedError, err) + } +} + +func TestRunCommandPipelineWithOutputErrors(t *testing.T) { + p := "$PATH" + if runtime.GOOS == "windows" { + p = "%PATH%" + } + cmd1 := exec.Command("ls") + cmd1.Stdout = os.Stdout + cmd2 := exec.Command("anything really") + _, _, err := RunCommandPipelineWithOutput(cmd1, cmd2) + if err == nil || err.Error() != "cannot set stdout pipe for anything really: exec: Stdout already set" { + t.Fatalf("Expected an error, got %v", err) + } + + cmdWithError := exec.Command("doesnotexists") + cmdCat := exec.Command("cat") + _, _, err = RunCommandPipelineWithOutput(cmdWithError, cmdCat) + if err == nil || err.Error() != `starting doesnotexists failed with error: exec: "doesnotexists": executable file not found in `+p { + t.Fatalf("Expected an error, got %v", err) + } +} + +func TestRunCommandPipelineWithOutput(t *testing.T) { + cmds := []*exec.Cmd{ + // Print 2 characters + exec.Command("echo", "-n", "11"), + // Count the number or char from stdin (previous command) + exec.Command("wc", "-m"), + } + out, exitCode, err := RunCommandPipelineWithOutput(cmds...) + expectedOutput := "2\n" + if out != expectedOutput || exitCode != 0 || err != nil { + t.Fatalf("Expected %s for commands %v, got out:%s, exitCode:%d, err:%v", expectedOutput, cmds, out, exitCode, err) + } +} + +// Simple simple test as it is just a passthrough for json.Unmarshal +func TestUnmarshalJSON(t *testing.T) { + emptyResult := struct{}{} + if err := UnmarshalJSON([]byte(""), &emptyResult); err == nil { + t.Fatalf("Expected an error, got nothing") + } + result := struct{ Name string }{} + if err := UnmarshalJSON([]byte(`{"name": "name"}`), &result); err != nil { + t.Fatal(err) + } + if result.Name != "name" { + t.Fatalf("Expected result.name to be 'name', was '%s'", result.Name) + } +} + +func TestConvertSliceOfStringsToMap(t *testing.T) { + input := []string{"a", "b"} + actual := ConvertSliceOfStringsToMap(input) + for _, key := range input { + if _, ok := actual[key]; !ok { + t.Fatalf("Expected output to contains key %s, did not: %v", key, actual) + } + } +} + +func TestCompareDirectoryEntries(t *testing.T) { + tmpFolder, err := ioutil.TempDir("", "integration-cli-utils-compare-directories") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpFolder) + + file1 := filepath.Join(tmpFolder, "file1") + file2 := filepath.Join(tmpFolder, "file2") + os.Create(file1) + os.Create(file2) + + fi1, err := os.Stat(file1) + if err != nil { + t.Fatal(err) + } + fi1bis, err := os.Stat(file1) + if err != nil { + t.Fatal(err) + } + fi2, err := os.Stat(file2) + if err != nil { + t.Fatal(err) + } + + cases := []struct { + e1 []os.FileInfo + e2 []os.FileInfo + shouldError bool + }{ + // Empty directories + { + []os.FileInfo{}, + []os.FileInfo{}, + false, + }, + // Same FileInfos + { + []os.FileInfo{fi1}, + []os.FileInfo{fi1}, + false, + }, + // Different FileInfos but same names + { + []os.FileInfo{fi1}, + []os.FileInfo{fi1bis}, + false, + }, + // Different FileInfos, different names + { + []os.FileInfo{fi1}, + []os.FileInfo{fi2}, + true, + }, + } + for _, elt := range cases { + err := CompareDirectoryEntries(elt.e1, elt.e2) + if elt.shouldError && err == nil { + t.Fatalf("Should have return an error, did not with %v and %v", elt.e1, elt.e2) + } + if !elt.shouldError && err != nil { + t.Fatalf("Should have not returned an error, but did : %v with %v and %v", err, elt.e1, elt.e2) + } + } +} + +// FIXME make an "unhappy path" test for ListTar without "panicking" :-) +func TestListTar(t *testing.T) { + // TODO Windows: Figure out why this fails. Should be portable. + if runtime.GOOS == "windows" { + t.Skip("Failing on Windows - needs further investigation") + } + tmpFolder, err := ioutil.TempDir("", "integration-cli-utils-list-tar") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpFolder) + + // Let's create a Tar file + srcFile := filepath.Join(tmpFolder, "src") + tarFile := filepath.Join(tmpFolder, "src.tar") + os.Create(srcFile) + cmd := exec.Command("sh", "-c", "tar cf "+tarFile+" "+srcFile) + _, err = cmd.CombinedOutput() + if err != nil { + t.Fatal(err) + } + + reader, err := os.Open(tarFile) + if err != nil { + t.Fatal(err) + } + defer reader.Close() + + entries, err := ListTar(reader) + if err != nil { + t.Fatal(err) + } + if len(entries) != 1 && entries[0] != "src" { + t.Fatalf("Expected a tar file with 1 entry (%s), got %v", srcFile, entries) + } +} + +func TestRandomTmpDirPath(t *testing.T) { + path := RandomTmpDirPath("something", runtime.GOOS) + + prefix := "/tmp/something" + if runtime.GOOS == "windows" { + prefix = os.Getenv("TEMP") + `\something` + } + expectedSize := len(prefix) + 11 + + if !strings.HasPrefix(path, prefix) { + t.Fatalf("Expected generated path to have '%s' as prefix, got %s'", prefix, path) + } + if len(path) != expectedSize { + t.Fatalf("Expected generated path to be %d, got %d", expectedSize, len(path)) + } +} + +func TestConsumeWithSpeed(t *testing.T) { + reader := strings.NewReader("1234567890") + chunksize := 2 + + bytes1, err := ConsumeWithSpeed(reader, chunksize, 1*time.Second, nil) + if err != nil { + t.Fatal(err) + } + + if bytes1 != 10 { + t.Fatalf("Expected to have read 10 bytes, got %d", bytes1) + } + +} + +func TestConsumeWithSpeedWithStop(t *testing.T) { + reader := strings.NewReader("1234567890") + chunksize := 2 + + stopIt := make(chan bool) + + go func() { + time.Sleep(1 * time.Millisecond) + stopIt <- true + }() + + bytes1, err := ConsumeWithSpeed(reader, chunksize, 20*time.Millisecond, stopIt) + if err != nil { + t.Fatal(err) + } + + if bytes1 != 2 { + t.Fatalf("Expected to have read 2 bytes, got %d", bytes1) + } + +} + +func TestParseCgroupPathsEmpty(t *testing.T) { + cgroupMap := ParseCgroupPaths("") + if len(cgroupMap) != 0 { + t.Fatalf("Expected an empty map, got %v", cgroupMap) + } + cgroupMap = ParseCgroupPaths("\n") + if len(cgroupMap) != 0 { + t.Fatalf("Expected an empty map, got %v", cgroupMap) + } + cgroupMap = ParseCgroupPaths("something:else\nagain:here") + if len(cgroupMap) != 0 { + t.Fatalf("Expected an empty map, got %v", cgroupMap) + } +} + +func TestParseCgroupPaths(t *testing.T) { + cgroupMap := ParseCgroupPaths("2:memory:/a\n1:cpuset:/b") + if len(cgroupMap) != 2 { + t.Fatalf("Expected a map with 2 entries, got %v", cgroupMap) + } + if value, ok := cgroupMap["memory"]; !ok || value != "/a" { + t.Fatalf("Expected cgroupMap to contains an entry for 'memory' with value '/a', got %v", cgroupMap) + } + if value, ok := cgroupMap["cpuset"]; !ok || value != "/b" { + t.Fatalf("Expected cgroupMap to contains an entry for 'cpuset' with value '/b', got %v", cgroupMap) + } +} + +func TestChannelBufferTimeout(t *testing.T) { + expected := "11" + + buf := &ChannelBuffer{make(chan []byte, 1)} + defer buf.Close() + + go func() { + time.Sleep(100 * time.Millisecond) + io.Copy(buf, strings.NewReader(expected)) + }() + + // Wait long enough + b := make([]byte, 2) + _, err := buf.ReadTimeout(b, 50*time.Millisecond) + if err == nil && err.Error() != "timeout reading from channel" { + t.Fatalf("Expected an error, got %s", err) + } + + // Wait for the end :) + time.Sleep(150 * time.Millisecond) +} + +func TestChannelBuffer(t *testing.T) { + expected := "11" + + buf := &ChannelBuffer{make(chan []byte, 1)} + defer buf.Close() + + go func() { + time.Sleep(100 * time.Millisecond) + io.Copy(buf, strings.NewReader(expected)) + }() + + // Wait long enough + b := make([]byte, 2) + _, err := buf.ReadTimeout(b, 200*time.Millisecond) + if err != nil { + t.Fatal(err) + } + + if string(b) != expected { + t.Fatalf("Expected '%s', got '%s'", expected, string(b)) + } +} + +// FIXME doesn't work +// func TestRunAtDifferentDate(t *testing.T) { +// var date string + +// // Layout for date. MMDDhhmmYYYY +// const timeLayout = "20060102" +// expectedDate := "20100201" +// theDate, err := time.Parse(timeLayout, expectedDate) +// if err != nil { +// t.Fatal(err) +// } + +// RunAtDifferentDate(theDate, func() { +// cmd := exec.Command("date", "+%Y%M%d") +// out, err := cmd.Output() +// if err != nil { +// t.Fatal(err) +// } +// date = string(out) +// }) +// } diff --git a/pkg/ioutils/bytespipe.go b/pkg/ioutils/bytespipe.go new file mode 100644 index 00000000..fcaecc37 --- /dev/null +++ b/pkg/ioutils/bytespipe.go @@ -0,0 +1,156 @@ +package ioutils + +import ( + "errors" + "io" + "sync" +) + +// maxCap is the highest capacity to use in byte slices that buffer data. +const maxCap = 1e6 + +// blockThreshold is the minimum number of bytes in the buffer which will cause +// a write to BytesPipe to block when allocating a new slice. +const blockThreshold = 1e6 + +// ErrClosed is returned when Write is called on a closed BytesPipe. +var ErrClosed = errors.New("write to closed BytesPipe") + +// BytesPipe is io.ReadWriteCloser which works similarly to pipe(queue). +// All written data may be read at most once. Also, BytesPipe allocates +// and releases new byte slices to adjust to current needs, so the buffer +// won't be overgrown after peak loads. +type BytesPipe struct { + mu sync.Mutex + wait *sync.Cond + buf [][]byte // slice of byte-slices of buffered data + lastRead int // index in the first slice to a read point + bufLen int // length of data buffered over the slices + closeErr error // error to return from next Read. set to nil if not closed. +} + +// NewBytesPipe creates new BytesPipe, initialized by specified slice. +// If buf is nil, then it will be initialized with slice which cap is 64. +// buf will be adjusted in a way that len(buf) == 0, cap(buf) == cap(buf). +func NewBytesPipe(buf []byte) *BytesPipe { + if cap(buf) == 0 { + buf = make([]byte, 0, 64) + } + bp := &BytesPipe{ + buf: [][]byte{buf[:0]}, + } + bp.wait = sync.NewCond(&bp.mu) + return bp +} + +// Write writes p to BytesPipe. +// It can allocate new []byte slices in a process of writing. +func (bp *BytesPipe) Write(p []byte) (int, error) { + bp.mu.Lock() + defer bp.mu.Unlock() + written := 0 +loop0: + for { + if bp.closeErr != nil { + return written, ErrClosed + } + // write data to the last buffer + b := bp.buf[len(bp.buf)-1] + // copy data to the current empty allocated area + n := copy(b[len(b):cap(b)], p) + // increment buffered data length + bp.bufLen += n + // include written data in last buffer + bp.buf[len(bp.buf)-1] = b[:len(b)+n] + + written += n + + // if there was enough room to write all then break + if len(p) == n { + break + } + + // more data: write to the next slice + p = p[n:] + + // block if too much data is still in the buffer + for bp.bufLen >= blockThreshold { + bp.wait.Wait() + if bp.closeErr != nil { + continue loop0 + } + } + + // allocate slice that has twice the size of the last unless maximum reached + nextCap := 2 * cap(bp.buf[len(bp.buf)-1]) + if nextCap > maxCap { + nextCap = maxCap + } + // add new byte slice to the buffers slice and continue writing + bp.buf = append(bp.buf, make([]byte, 0, nextCap)) + } + bp.wait.Broadcast() + return written, nil +} + +// CloseWithError causes further reads from a BytesPipe to return immediately. +func (bp *BytesPipe) CloseWithError(err error) error { + bp.mu.Lock() + if err != nil { + bp.closeErr = err + } else { + bp.closeErr = io.EOF + } + bp.wait.Broadcast() + bp.mu.Unlock() + return nil +} + +// Close causes further reads from a BytesPipe to return immediately. +func (bp *BytesPipe) Close() error { + return bp.CloseWithError(nil) +} + +func (bp *BytesPipe) len() int { + return bp.bufLen - bp.lastRead +} + +// Read reads bytes from BytesPipe. +// Data could be read only once. +func (bp *BytesPipe) Read(p []byte) (n int, err error) { + bp.mu.Lock() + defer bp.mu.Unlock() + if bp.len() == 0 { + if bp.closeErr != nil { + return 0, bp.closeErr + } + bp.wait.Wait() + if bp.len() == 0 && bp.closeErr != nil { + return 0, bp.closeErr + } + } + for { + read := copy(p, bp.buf[0][bp.lastRead:]) + n += read + bp.lastRead += read + if bp.len() == 0 { + // we have read everything. reset to the beginning. + bp.lastRead = 0 + bp.bufLen -= len(bp.buf[0]) + bp.buf[0] = bp.buf[0][:0] + break + } + // break if everything was read + if len(p) == read { + break + } + // more buffered data and more asked. read from next slice. + p = p[read:] + bp.lastRead = 0 + bp.bufLen -= len(bp.buf[0]) + bp.buf[0] = nil // throw away old slice + bp.buf = bp.buf[1:] // switch to next + } + bp.wait.Broadcast() + return +} diff --git a/pkg/ioutils/bytespipe_test.go b/pkg/ioutils/bytespipe_test.go new file mode 100644 index 00000000..b051139a --- /dev/null +++ b/pkg/ioutils/bytespipe_test.go @@ -0,0 +1,158 @@ +package ioutils + +import ( + "crypto/sha1" + "encoding/hex" + "math/rand" + "testing" + "time" +) + +func TestBytesPipeRead(t *testing.T) { + buf := NewBytesPipe(nil) + buf.Write([]byte("12")) + buf.Write([]byte("34")) + buf.Write([]byte("56")) + buf.Write([]byte("78")) + buf.Write([]byte("90")) + rd := make([]byte, 4) + n, err := buf.Read(rd) + if err != nil { + t.Fatal(err) + } + if n != 4 { + t.Fatalf("Wrong number of bytes read: %d, should be %d", n, 4) + } + if string(rd) != "1234" { + t.Fatalf("Read %s, but must be %s", rd, "1234") + } + n, err = buf.Read(rd) + if err != nil { + t.Fatal(err) + } + if n != 4 { + t.Fatalf("Wrong number of bytes read: %d, should be %d", n, 4) + } + if string(rd) != "5678" { + t.Fatalf("Read %s, but must be %s", rd, "5679") + } + n, err = buf.Read(rd) + if err != nil { + t.Fatal(err) + } + if n != 2 { + t.Fatalf("Wrong number of bytes read: %d, should be %d", n, 2) + } + if string(rd[:n]) != "90" { + t.Fatalf("Read %s, but must be %s", rd, "90") + } +} + +func TestBytesPipeWrite(t *testing.T) { + buf := NewBytesPipe(nil) + buf.Write([]byte("12")) + buf.Write([]byte("34")) + buf.Write([]byte("56")) + buf.Write([]byte("78")) + buf.Write([]byte("90")) + if string(buf.buf[0]) != "1234567890" { + t.Fatalf("Buffer %s, must be %s", buf.buf, "1234567890") + } +} + +// Write and read in different speeds/chunk sizes and check valid data is read. +func TestBytesPipeWriteRandomChunks(t *testing.T) { + cases := []struct{ iterations, writesPerLoop, readsPerLoop int }{ + {100, 10, 1}, + {1000, 10, 5}, + {1000, 100, 0}, + {1000, 5, 6}, + {10000, 50, 25}, + } + + testMessage := []byte("this is a random string for testing") + // random slice sizes to read and write + writeChunks := []int{25, 35, 15, 20} + readChunks := []int{5, 45, 20, 25} + + for _, c := range cases { + // first pass: write directly to hash + hash := sha1.New() + for i := 0; i < c.iterations*c.writesPerLoop; i++ { + if _, err := hash.Write(testMessage[:writeChunks[i%len(writeChunks)]]); err != nil { + t.Fatal(err) + } + } + expected := hex.EncodeToString(hash.Sum(nil)) + + // write/read through buffer + buf := NewBytesPipe(nil) + hash.Reset() + + done := make(chan struct{}) + + go func() { + // random delay before read starts + <-time.After(time.Duration(rand.Intn(10)) * time.Millisecond) + for i := 0; ; i++ { + p := make([]byte, readChunks[(c.iterations*c.readsPerLoop+i)%len(readChunks)]) + n, _ := buf.Read(p) + if n == 0 { + break + } + hash.Write(p[:n]) + } + + close(done) + }() + + for i := 0; i < c.iterations; i++ { + for w := 0; w < c.writesPerLoop; w++ { + buf.Write(testMessage[:writeChunks[(i*c.writesPerLoop+w)%len(writeChunks)]]) + } + } + buf.Close() + <-done + + actual := hex.EncodeToString(hash.Sum(nil)) + + if expected != actual { + t.Fatalf("BytesPipe returned invalid data. Expected checksum %v, got %v", expected, actual) + } + + } +} + +func BenchmarkBytesPipeWrite(b *testing.B) { + for i := 0; i < b.N; i++ { + readBuf := make([]byte, 1024) + buf := NewBytesPipe(nil) + go func() { + var err error + for err == nil { + _, err = buf.Read(readBuf) + } + }() + for j := 0; j < 1000; j++ { + buf.Write([]byte("pretty short line, because why not?")) + } + buf.Close() + } +} + +func BenchmarkBytesPipeRead(b *testing.B) { + rd := make([]byte, 512) + for i := 0; i < b.N; i++ { + b.StopTimer() + buf := NewBytesPipe(nil) + for j := 0; j < 500; j++ { + buf.Write(make([]byte, 1024)) + } + b.StartTimer() + for j := 0; j < 1000; j++ { + if n, _ := buf.Read(rd); n != 512 { + b.Fatalf("Wrong number of bytes: %d", n) + } + } + } +} diff --git a/pkg/ioutils/fmt.go b/pkg/ioutils/fmt.go new file mode 100644 index 00000000..0b04b0ba --- /dev/null +++ b/pkg/ioutils/fmt.go @@ -0,0 +1,22 @@ +package ioutils + +import ( + "fmt" + "io" +) + +// FprintfIfNotEmpty prints the string value if it's not empty +func FprintfIfNotEmpty(w io.Writer, format, value string) (int, error) { + if value != "" { + return fmt.Fprintf(w, format, value) + } + return 0, nil +} + +// FprintfIfTrue prints the boolean value if it's true +func FprintfIfTrue(w io.Writer, format string, ok bool) (int, error) { + if ok { + return fmt.Fprintf(w, format, ok) + } + return 0, nil +} diff --git a/pkg/ioutils/fmt_test.go b/pkg/ioutils/fmt_test.go new file mode 100644 index 00000000..89688632 --- /dev/null +++ b/pkg/ioutils/fmt_test.go @@ -0,0 +1,17 @@ +package ioutils + +import "testing" + +func TestFprintfIfNotEmpty(t *testing.T) { + wc := NewWriteCounter(&NopWriter{}) + n, _ := FprintfIfNotEmpty(wc, "foo%s", "") + + if wc.Count != 0 || n != 0 { + t.Errorf("Wrong count: %v vs. %v vs. 0", wc.Count, n) + } + + n, _ = FprintfIfNotEmpty(wc, "foo%s", "bar") + if wc.Count != 6 || n != 6 { + t.Errorf("Wrong count: %v vs. %v vs. 6", wc.Count, n) + } +} diff --git a/pkg/ioutils/multireader.go b/pkg/ioutils/multireader.go new file mode 100644 index 00000000..0d2d76b4 --- /dev/null +++ b/pkg/ioutils/multireader.go @@ -0,0 +1,226 @@ +package ioutils + +import ( + "bytes" + "fmt" + "io" + "os" +) + +type pos struct { + idx int + offset int64 +} + +type multiReadSeeker struct { + readers []io.ReadSeeker + pos *pos + posIdx map[io.ReadSeeker]int +} + +func (r *multiReadSeeker) Seek(offset int64, whence int) (int64, error) { + var tmpOffset int64 + switch whence { + case os.SEEK_SET: + for i, rdr := range r.readers { + // get size of the current reader + s, err := rdr.Seek(0, os.SEEK_END) + if err != nil { + return -1, err + } + + if offset > tmpOffset+s { + if i == len(r.readers)-1 { + rdrOffset := s + (offset - tmpOffset) + if _, err := rdr.Seek(rdrOffset, os.SEEK_SET); err != nil { + return -1, err + } + r.pos = &pos{i, rdrOffset} + return offset, nil + } + + tmpOffset += s + continue + } + + rdrOffset := offset - tmpOffset + idx := i + + rdr.Seek(rdrOffset, os.SEEK_SET) + // make sure all following readers are at 0 + for _, rdr := range r.readers[i+1:] { + rdr.Seek(0, os.SEEK_SET) + } + + if rdrOffset == s && i != len(r.readers)-1 { + idx++ + rdrOffset = 0 + } + r.pos = &pos{idx, rdrOffset} + return offset, nil + } + case os.SEEK_END: + for _, rdr := range r.readers { + s, err := rdr.Seek(0, os.SEEK_END) + if err != nil { + return -1, err + } + tmpOffset += s + } + r.Seek(tmpOffset+offset, os.SEEK_SET) + return tmpOffset + offset, nil + case os.SEEK_CUR: + if r.pos == nil { + return r.Seek(offset, os.SEEK_SET) + } + // Just return the current offset + if offset == 0 { + return r.getCurOffset() + } + + curOffset, err := r.getCurOffset() + if err != nil { + return -1, err + } + rdr, rdrOffset, err := r.getReaderForOffset(curOffset + offset) + if err != nil { + return -1, err + } + + r.pos = &pos{r.posIdx[rdr], rdrOffset} + return curOffset + offset, nil + default: + return -1, fmt.Errorf("Invalid whence: %d", whence) + } + + return -1, fmt.Errorf("Error seeking for whence: %d, offset: %d", whence, offset) +} + +func (r *multiReadSeeker) getReaderForOffset(offset int64) (io.ReadSeeker, int64, error) { + var rdr io.ReadSeeker + var rdrOffset int64 + + for i, rdr := range r.readers { + offsetTo, err := r.getOffsetToReader(rdr) + if err != nil { + return nil, -1, err + } + if offsetTo > offset { + rdr = r.readers[i-1] + rdrOffset = offsetTo - offset + break + } + + if rdr == r.readers[len(r.readers)-1] { + rdrOffset = offsetTo + offset + break + } + } + + return rdr, rdrOffset, nil +} + +func (r *multiReadSeeker) getCurOffset() (int64, error) { + var totalSize int64 + for _, rdr := range r.readers[:r.pos.idx+1] { + if r.posIdx[rdr] == r.pos.idx { + totalSize += r.pos.offset + break + } + + size, err := getReadSeekerSize(rdr) + if err != nil { + return -1, fmt.Errorf("error getting seeker size: %v", err) + } + totalSize += size + } + return totalSize, nil +} + +func (r *multiReadSeeker) getOffsetToReader(rdr io.ReadSeeker) (int64, error) { + var offset int64 + for _, r := range r.readers { + if r == rdr { + break + } + + size, err := getReadSeekerSize(rdr) + if err != nil { + return -1, err + } + offset += size + } + return offset, nil +} + +func (r *multiReadSeeker) Read(b []byte) (int, error) { + if r.pos == nil { + r.pos = &pos{0, 0} + } + + bCap := int64(cap(b)) + buf := bytes.NewBuffer(nil) + var rdr io.ReadSeeker + + for _, rdr = range r.readers[r.pos.idx:] { + readBytes, err := io.CopyN(buf, rdr, bCap) + if err != nil && err != io.EOF { + return -1, err + } + bCap -= readBytes + + if bCap == 0 { + break + } + } + + rdrPos, err := rdr.Seek(0, os.SEEK_CUR) + if err != nil { + return -1, err + } + r.pos = &pos{r.posIdx[rdr], rdrPos} + return buf.Read(b) +} + +func getReadSeekerSize(rdr io.ReadSeeker) (int64, error) { + // save the current position + pos, err := rdr.Seek(0, os.SEEK_CUR) + if err != nil { + return -1, err + } + + // get the size + size, err := rdr.Seek(0, os.SEEK_END) + if err != nil { + return -1, err + } + + // reset the position + if _, err := rdr.Seek(pos, os.SEEK_SET); err != nil { + return -1, err + } + return size, nil +} + +// MultiReadSeeker returns a ReadSeeker that's the logical concatenation of the provided +// input readseekers. After calling this method the initial position is set to the +// beginning of the first ReadSeeker. At the end of a ReadSeeker, Read always advances +// to the beginning of the next ReadSeeker and returns EOF at the end of the last ReadSeeker. +// Seek can be used over the sum of lengths of all readseekers. +// +// When a MultiReadSeeker is used, no Read and Seek operations should be made on +// its ReadSeeker components. Also, users should make no assumption on the state +// of individual readseekers while the MultiReadSeeker is used. +func MultiReadSeeker(readers ...io.ReadSeeker) io.ReadSeeker { + if len(readers) == 1 { + return readers[0] + } + idx := make(map[io.ReadSeeker]int) + for i, rdr := range readers { + idx[rdr] = i + } + return &multiReadSeeker{ + readers: readers, + posIdx: idx, + } +} diff --git a/pkg/ioutils/multireader_test.go b/pkg/ioutils/multireader_test.go new file mode 100644 index 00000000..de495b56 --- /dev/null +++ b/pkg/ioutils/multireader_test.go @@ -0,0 +1,149 @@ +package ioutils + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + "testing" +) + +func TestMultiReadSeekerReadAll(t *testing.T) { + str := "hello world" + s1 := strings.NewReader(str + " 1") + s2 := strings.NewReader(str + " 2") + s3 := strings.NewReader(str + " 3") + mr := MultiReadSeeker(s1, s2, s3) + + expectedSize := int64(s1.Len() + s2.Len() + s3.Len()) + + b, err := ioutil.ReadAll(mr) + if err != nil { + t.Fatal(err) + } + + expected := "hello world 1hello world 2hello world 3" + if string(b) != expected { + t.Fatalf("ReadAll failed, got: %q, expected %q", string(b), expected) + } + + size, err := mr.Seek(0, os.SEEK_END) + if err != nil { + t.Fatal(err) + } + if size != expectedSize { + t.Fatalf("reader size does not match, got %d, expected %d", size, expectedSize) + } + + // Reset the position and read again + pos, err := mr.Seek(0, os.SEEK_SET) + if err != nil { + t.Fatal(err) + } + if pos != 0 { + t.Fatalf("expected position to be set to 0, got %d", pos) + } + + b, err = ioutil.ReadAll(mr) + if err != nil { + t.Fatal(err) + } + + if string(b) != expected { + t.Fatalf("ReadAll failed, got: %q, expected %q", string(b), expected) + } +} + +func TestMultiReadSeekerReadEach(t *testing.T) { + str := "hello world" + s1 := strings.NewReader(str + " 1") + s2 := strings.NewReader(str + " 2") + s3 := strings.NewReader(str + " 3") + mr := MultiReadSeeker(s1, s2, s3) + + var totalBytes int64 + for i, s := range []*strings.Reader{s1, s2, s3} { + sLen := int64(s.Len()) + buf := make([]byte, s.Len()) + expected := []byte(fmt.Sprintf("%s %d", str, i+1)) + + if _, err := mr.Read(buf); err != nil && err != io.EOF { + t.Fatal(err) + } + + if !bytes.Equal(buf, expected) { + t.Fatalf("expected %q to be %q", string(buf), string(expected)) + } + + pos, err := mr.Seek(0, os.SEEK_CUR) + if err != nil { + t.Fatalf("iteration: %d, error: %v", i+1, err) + } + + // check that the total bytes read is the current position of the seeker + totalBytes += sLen + if pos != totalBytes { + t.Fatalf("expected current position to be: %d, got: %d, iteration: %d", totalBytes, pos, i+1) + } + + // This tests not only that SEEK_SET and SEEK_CUR give the same values, but that the next iteration is in the expected position as well + newPos, err := mr.Seek(pos, os.SEEK_SET) + if err != nil { + t.Fatal(err) + } + if newPos != pos { + t.Fatalf("expected to get same position when calling SEEK_SET with value from SEEK_CUR, cur: %d, set: %d", pos, newPos) + } + } +} + +func TestMultiReadSeekerReadSpanningChunks(t *testing.T) { + str := "hello world" + s1 := strings.NewReader(str + " 1") + s2 := strings.NewReader(str + " 2") + s3 := strings.NewReader(str + " 3") + mr := MultiReadSeeker(s1, s2, s3) + + buf := make([]byte, s1.Len()+3) + _, err := mr.Read(buf) + if err != nil { + t.Fatal(err) + } + + // expected is the contents of s1 + 3 bytes from s2, ie, the `hel` at the end of this string + expected := "hello world 1hel" + if string(buf) != expected { + t.Fatalf("expected %s to be %s", string(buf), expected) + } +} + +func TestMultiReadSeekerNegativeSeek(t *testing.T) { + str := "hello world" + s1 := strings.NewReader(str + " 1") + s2 := strings.NewReader(str + " 2") + s3 := strings.NewReader(str + " 3") + mr := MultiReadSeeker(s1, s2, s3) + + s1Len := s1.Len() + s2Len := s2.Len() + s3Len := s3.Len() + + s, err := mr.Seek(int64(-1*s3.Len()), os.SEEK_END) + if err != nil { + t.Fatal(err) + } + if s != int64(s1Len+s2Len) { + t.Fatalf("expected %d to be %d", s, s1.Len()+s2.Len()) + } + + buf := make([]byte, s3Len) + if _, err := mr.Read(buf); err != nil && err != io.EOF { + t.Fatal(err) + } + expected := fmt.Sprintf("%s %d", str, 3) + if string(buf) != fmt.Sprintf("%s %d", str, 3) { + t.Fatalf("expected %q to be %q", string(buf), expected) + } +} diff --git a/pkg/ioutils/readers.go b/pkg/ioutils/readers.go new file mode 100644 index 00000000..e73b02bb --- /dev/null +++ b/pkg/ioutils/readers.go @@ -0,0 +1,154 @@ +package ioutils + +import ( + "crypto/sha256" + "encoding/hex" + "io" + + "golang.org/x/net/context" +) + +type readCloserWrapper struct { + io.Reader + closer func() error +} + +func (r *readCloserWrapper) Close() error { + return r.closer() +} + +// NewReadCloserWrapper returns a new io.ReadCloser. +func NewReadCloserWrapper(r io.Reader, closer func() error) io.ReadCloser { + return &readCloserWrapper{ + Reader: r, + closer: closer, + } +} + +type readerErrWrapper struct { + reader io.Reader + closer func() +} + +func (r *readerErrWrapper) Read(p []byte) (int, error) { + n, err := r.reader.Read(p) + if err != nil { + r.closer() + } + return n, err +} + +// NewReaderErrWrapper returns a new io.Reader. +func NewReaderErrWrapper(r io.Reader, closer func()) io.Reader { + return &readerErrWrapper{ + reader: r, + closer: closer, + } +} + +// HashData returns the sha256 sum of src. +func HashData(src io.Reader) (string, error) { + h := sha256.New() + if _, err := io.Copy(h, src); err != nil { + return "", err + } + return "sha256:" + hex.EncodeToString(h.Sum(nil)), nil +} + +// OnEOFReader wraps a io.ReadCloser and a function +// the function will run at the end of file or close the file. +type OnEOFReader struct { + Rc io.ReadCloser + Fn func() +} + +func (r *OnEOFReader) Read(p []byte) (n int, err error) { + n, err = r.Rc.Read(p) + if err == io.EOF { + r.runFunc() + } + return +} + +// Close closes the file and run the function. +func (r *OnEOFReader) Close() error { + err := r.Rc.Close() + r.runFunc() + return err +} + +func (r *OnEOFReader) runFunc() { + if fn := r.Fn; fn != nil { + fn() + r.Fn = nil + } +} + +// cancelReadCloser wraps an io.ReadCloser with a context for cancelling read +// operations. +type cancelReadCloser struct { + cancel func() + pR *io.PipeReader // Stream to read from + pW *io.PipeWriter +} + +// NewCancelReadCloser creates a wrapper that closes the ReadCloser when the +// context is cancelled. The returned io.ReadCloser must be closed when it is +// no longer needed. +func NewCancelReadCloser(ctx context.Context, in io.ReadCloser) io.ReadCloser { + pR, pW := io.Pipe() + + // Create a context used to signal when the pipe is closed + doneCtx, cancel := context.WithCancel(context.Background()) + + p := &cancelReadCloser{ + cancel: cancel, + pR: pR, + pW: pW, + } + + go func() { + _, err := io.Copy(pW, in) + select { + case <-ctx.Done(): + // If the context was closed, p.closeWithError + // was already called. Calling it again would + // change the error that Read returns. + default: + p.closeWithError(err) + } + in.Close() + }() + go func() { + for { + select { + case <-ctx.Done(): + p.closeWithError(ctx.Err()) + case <-doneCtx.Done(): + return + } + } + }() + + return p +} + +// Read wraps the Read method of the pipe that provides data from the wrapped +// ReadCloser. +func (p *cancelReadCloser) Read(buf []byte) (n int, err error) { + return p.pR.Read(buf) +} + +// closeWithError closes the wrapper and its underlying reader. It will +// cause future calls to Read to return err. +func (p *cancelReadCloser) closeWithError(err error) { + p.pW.CloseWithError(err) + p.cancel() +} + +// Close closes the wrapper its underlying reader. It will cause +// future calls to Read to return io.EOF. +func (p *cancelReadCloser) Close() error { + p.closeWithError(io.EOF) + return nil +} diff --git a/pkg/ioutils/readers_test.go b/pkg/ioutils/readers_test.go new file mode 100644 index 00000000..9abc1054 --- /dev/null +++ b/pkg/ioutils/readers_test.go @@ -0,0 +1,94 @@ +package ioutils + +import ( + "fmt" + "io/ioutil" + "strings" + "testing" + "time" + + "golang.org/x/net/context" +) + +// Implement io.Reader +type errorReader struct{} + +func (r *errorReader) Read(p []byte) (int, error) { + return 0, fmt.Errorf("Error reader always fail.") +} + +func TestReadCloserWrapperClose(t *testing.T) { + reader := strings.NewReader("A string reader") + wrapper := NewReadCloserWrapper(reader, func() error { + return fmt.Errorf("This will be called when closing") + }) + err := wrapper.Close() + if err == nil || !strings.Contains(err.Error(), "This will be called when closing") { + t.Fatalf("readCloserWrapper should have call the anonymous func and thus, fail.") + } +} + +func TestReaderErrWrapperReadOnError(t *testing.T) { + called := false + reader := &errorReader{} + wrapper := NewReaderErrWrapper(reader, func() { + called = true + }) + _, err := wrapper.Read([]byte{}) + if err == nil || !strings.Contains(err.Error(), "Error reader always fail.") { + t.Fatalf("readErrWrapper should returned an error") + } + if !called { + t.Fatalf("readErrWrapper should have call the anonymous function on failure") + } +} + +func TestReaderErrWrapperRead(t *testing.T) { + reader := strings.NewReader("a string reader.") + wrapper := NewReaderErrWrapper(reader, func() { + t.Fatalf("readErrWrapper should not have called the anonymous function") + }) + // Read 20 byte (should be ok with the string above) + num, err := wrapper.Read(make([]byte, 20)) + if err != nil { + t.Fatal(err) + } + if num != 16 { + t.Fatalf("readerErrWrapper should have read 16 byte, but read %d", num) + } +} + +func TestHashData(t *testing.T) { + reader := strings.NewReader("hash-me") + actual, err := HashData(reader) + if err != nil { + t.Fatal(err) + } + expected := "sha256:4d11186aed035cc624d553e10db358492c84a7cd6b9670d92123c144930450aa" + if actual != expected { + t.Fatalf("Expecting %s, got %s", expected, actual) + } +} + +type perpetualReader struct{} + +func (p *perpetualReader) Read(buf []byte) (n int, err error) { + for i := 0; i != len(buf); i++ { + buf[i] = 'a' + } + return len(buf), nil +} + +func TestCancelReadCloser(t *testing.T) { + ctx, _ := context.WithTimeout(context.Background(), 100*time.Millisecond) + cancelReadCloser := NewCancelReadCloser(ctx, ioutil.NopCloser(&perpetualReader{})) + for { + var buf [128]byte + _, err := cancelReadCloser.Read(buf[:]) + if err == context.DeadlineExceeded { + break + } else if err != nil { + t.Fatalf("got unexpected error: %v", err) + } + } +} diff --git a/pkg/ioutils/scheduler.go b/pkg/ioutils/scheduler.go new file mode 100644 index 00000000..3c88f29e --- /dev/null +++ b/pkg/ioutils/scheduler.go @@ -0,0 +1,6 @@ +// +build !gccgo + +package ioutils + +func callSchedulerIfNecessary() { +} diff --git a/pkg/ioutils/scheduler_gccgo.go b/pkg/ioutils/scheduler_gccgo.go new file mode 100644 index 00000000..c11d02b9 --- /dev/null +++ b/pkg/ioutils/scheduler_gccgo.go @@ -0,0 +1,13 @@ +// +build gccgo + +package ioutils + +import ( + "runtime" +) + +func callSchedulerIfNecessary() { + //allow or force Go scheduler to switch context, without explicitly + //forcing this will make it hang when using gccgo implementation + runtime.Gosched() +} diff --git a/pkg/ioutils/temp_unix.go b/pkg/ioutils/temp_unix.go new file mode 100644 index 00000000..1539ad21 --- /dev/null +++ b/pkg/ioutils/temp_unix.go @@ -0,0 +1,10 @@ +// +build !windows + +package ioutils + +import "io/ioutil" + +// TempDir on Unix systems is equivalent to ioutil.TempDir. +func TempDir(dir, prefix string) (string, error) { + return ioutil.TempDir(dir, prefix) +} diff --git a/pkg/ioutils/temp_windows.go b/pkg/ioutils/temp_windows.go new file mode 100644 index 00000000..c258e5fd --- /dev/null +++ b/pkg/ioutils/temp_windows.go @@ -0,0 +1,18 @@ +// +build windows + +package ioutils + +import ( + "io/ioutil" + + "github.com/docker/docker/pkg/longpath" +) + +// TempDir is the equivalent of ioutil.TempDir, except that the result is in Windows longpath format. +func TempDir(dir, prefix string) (string, error) { + tempDir, err := ioutil.TempDir(dir, prefix) + if err != nil { + return "", err + } + return longpath.AddPrefix(tempDir), nil +} diff --git a/pkg/ioutils/writeflusher.go b/pkg/ioutils/writeflusher.go new file mode 100644 index 00000000..52a4901a --- /dev/null +++ b/pkg/ioutils/writeflusher.go @@ -0,0 +1,92 @@ +package ioutils + +import ( + "io" + "sync" +) + +// WriteFlusher wraps the Write and Flush operation ensuring that every write +// is a flush. In addition, the Close method can be called to intercept +// Read/Write calls if the targets lifecycle has already ended. +type WriteFlusher struct { + w io.Writer + flusher flusher + flushed chan struct{} + flushedOnce sync.Once + closed chan struct{} + closeLock sync.Mutex +} + +type flusher interface { + Flush() +} + +var errWriteFlusherClosed = io.EOF + +func (wf *WriteFlusher) Write(b []byte) (n int, err error) { + select { + case <-wf.closed: + return 0, errWriteFlusherClosed + default: + } + + n, err = wf.w.Write(b) + wf.Flush() // every write is a flush. + return n, err +} + +// Flush the stream immediately. +func (wf *WriteFlusher) Flush() { + select { + case <-wf.closed: + return + default: + } + + wf.flushedOnce.Do(func() { + close(wf.flushed) + }) + wf.flusher.Flush() +} + +// Flushed returns the state of flushed. +// If it's flushed, return true, or else it return false. +func (wf *WriteFlusher) Flushed() bool { + // BUG(stevvooe): Remove this method. Its use is inherently racy. Seems to + // be used to detect whether or a response code has been issued or not. + // Another hook should be used instead. + var flushed bool + select { + case <-wf.flushed: + flushed = true + default: + } + return flushed +} + +// Close closes the write flusher, disallowing any further writes to the +// target. After the flusher is closed, all calls to write or flush will +// result in an error. +func (wf *WriteFlusher) Close() error { + wf.closeLock.Lock() + defer wf.closeLock.Unlock() + + select { + case <-wf.closed: + return errWriteFlusherClosed + default: + close(wf.closed) + } + return nil +} + +// NewWriteFlusher returns a new WriteFlusher. +func NewWriteFlusher(w io.Writer) *WriteFlusher { + var fl flusher + if f, ok := w.(flusher); ok { + fl = f + } else { + fl = &NopFlusher{} + } + return &WriteFlusher{w: w, flusher: fl, closed: make(chan struct{}), flushed: make(chan struct{})} +} diff --git a/pkg/ioutils/writers.go b/pkg/ioutils/writers.go new file mode 100644 index 00000000..ccc7f9c2 --- /dev/null +++ b/pkg/ioutils/writers.go @@ -0,0 +1,66 @@ +package ioutils + +import "io" + +// NopWriter represents a type which write operation is nop. +type NopWriter struct{} + +func (*NopWriter) Write(buf []byte) (int, error) { + return len(buf), nil +} + +type nopWriteCloser struct { + io.Writer +} + +func (w *nopWriteCloser) Close() error { return nil } + +// NopWriteCloser returns a nopWriteCloser. +func NopWriteCloser(w io.Writer) io.WriteCloser { + return &nopWriteCloser{w} +} + +// NopFlusher represents a type which flush operation is nop. +type NopFlusher struct{} + +// Flush is a nop operation. +func (f *NopFlusher) Flush() {} + +type writeCloserWrapper struct { + io.Writer + closer func() error +} + +func (r *writeCloserWrapper) Close() error { + return r.closer() +} + +// NewWriteCloserWrapper returns a new io.WriteCloser. +func NewWriteCloserWrapper(r io.Writer, closer func() error) io.WriteCloser { + return &writeCloserWrapper{ + Writer: r, + closer: closer, + } +} + +// WriteCounter wraps a concrete io.Writer and hold a count of the number +// of bytes written to the writer during a "session". +// This can be convenient when write return is masked +// (e.g., json.Encoder.Encode()) +type WriteCounter struct { + Count int64 + Writer io.Writer +} + +// NewWriteCounter returns a new WriteCounter. +func NewWriteCounter(w io.Writer) *WriteCounter { + return &WriteCounter{ + Writer: w, + } +} + +func (wc *WriteCounter) Write(p []byte) (count int, err error) { + count, err = wc.Writer.Write(p) + wc.Count += int64(count) + return +} diff --git a/pkg/ioutils/writers_test.go b/pkg/ioutils/writers_test.go new file mode 100644 index 00000000..564b1cd4 --- /dev/null +++ b/pkg/ioutils/writers_test.go @@ -0,0 +1,65 @@ +package ioutils + +import ( + "bytes" + "strings" + "testing" +) + +func TestWriteCloserWrapperClose(t *testing.T) { + called := false + writer := bytes.NewBuffer([]byte{}) + wrapper := NewWriteCloserWrapper(writer, func() error { + called = true + return nil + }) + if err := wrapper.Close(); err != nil { + t.Fatal(err) + } + if !called { + t.Fatalf("writeCloserWrapper should have call the anonymous function.") + } +} + +func TestNopWriteCloser(t *testing.T) { + writer := bytes.NewBuffer([]byte{}) + wrapper := NopWriteCloser(writer) + if err := wrapper.Close(); err != nil { + t.Fatal("NopWriteCloser always return nil on Close.") + } + +} + +func TestNopWriter(t *testing.T) { + nw := &NopWriter{} + l, err := nw.Write([]byte{'c'}) + if err != nil { + t.Fatal(err) + } + if l != 1 { + t.Fatalf("Expected 1 got %d", l) + } +} + +func TestWriteCounter(t *testing.T) { + dummy1 := "This is a dummy string." + dummy2 := "This is another dummy string." + totalLength := int64(len(dummy1) + len(dummy2)) + + reader1 := strings.NewReader(dummy1) + reader2 := strings.NewReader(dummy2) + + var buffer bytes.Buffer + wc := NewWriteCounter(&buffer) + + reader1.WriteTo(wc) + reader2.WriteTo(wc) + + if wc.Count != totalLength { + t.Errorf("Wrong count: %d vs. %d", wc.Count, totalLength) + } + + if buffer.String() != dummy1+dummy2 { + t.Error("Wrong message written") + } +} diff --git a/pkg/jsonlog/jsonlog.go b/pkg/jsonlog/jsonlog.go new file mode 100644 index 00000000..422e4bbd --- /dev/null +++ b/pkg/jsonlog/jsonlog.go @@ -0,0 +1,40 @@ +package jsonlog + +import ( + "encoding/json" + "fmt" + "time" +) + +// JSONLog represents a log message, typically a single entry from a given log stream. +// JSONLogs can be easily serialized to and from JSON and support custom formatting. +type JSONLog struct { + // Log is the log message + Log string `json:"log,omitempty"` + // Stream is the log source + Stream string `json:"stream,omitempty"` + // Created is the created timestamp of log + Created time.Time `json:"time"` +} + +// Format returns the log formatted according to format +// If format is nil, returns the log message +// If format is json, returns the log marshaled in json format +// By default, returns the log with the log time formatted according to format. +func (jl *JSONLog) Format(format string) (string, error) { + if format == "" { + return jl.Log, nil + } + if format == "json" { + m, err := json.Marshal(jl) + return string(m), err + } + return fmt.Sprintf("%s %s", jl.Created.Format(format), jl.Log), nil +} + +// Reset resets the log to nil. +func (jl *JSONLog) Reset() { + jl.Log = "" + jl.Stream = "" + jl.Created = time.Time{} +} diff --git a/pkg/jsonlog/jsonlog_marshalling.go b/pkg/jsonlog/jsonlog_marshalling.go new file mode 100644 index 00000000..83ce684a --- /dev/null +++ b/pkg/jsonlog/jsonlog_marshalling.go @@ -0,0 +1,178 @@ +// This code was initially generated by ffjson +// This code was generated via the following steps: +// $ go get -u github.com/pquerna/ffjson +// $ make BIND_DIR=. shell +// $ ffjson pkg/jsonlog/jsonlog.go +// $ mv pkg/jsonglog/jsonlog_ffjson.go pkg/jsonlog/jsonlog_marshalling.go +// +// It has been modified to improve the performance of time marshalling to JSON +// and to clean it up. +// Should this code need to be regenerated when the JSONLog struct is changed, +// the relevant changes which have been made are: +// import ( +// "bytes" +//- +// "unicode/utf8" +// ) +// +// func (mj *JSONLog) MarshalJSON() ([]byte, error) { +//@@ -20,13 +16,13 @@ func (mj *JSONLog) MarshalJSON() ([]byte, error) { +// } +// return buf.Bytes(), nil +// } +//+ +// func (mj *JSONLog) MarshalJSONBuf(buf *bytes.Buffer) error { +//- var err error +//- var obj []byte +//- var first bool = true +//- _ = obj +//- _ = err +//- _ = first +//+ var ( +//+ err error +//+ timestamp string +//+ first bool = true +//+ ) +// buf.WriteString(`{`) +// if len(mj.Log) != 0 { +// if first == true { +//@@ -52,11 +48,11 @@ func (mj *JSONLog) MarshalJSONBuf(buf *bytes.Buffer) error { +// buf.WriteString(`,`) +// } +// buf.WriteString(`"time":`) +//- obj, err = mj.Created.MarshalJSON() +//+ timestamp, err = FastTimeMarshalJSON(mj.Created) +// if err != nil { +// return err +// } +//- buf.Write(obj) +//+ buf.WriteString(timestamp) +// buf.WriteString(`}`) +// return nil +// } +// @@ -81,9 +81,10 @@ func (mj *JSONLog) MarshalJSONBuf(buf *bytes.Buffer) error { +// if len(mj.Log) != 0 { +// - if first == true { +// - first = false +// - } else { +// - buf.WriteString(`,`) +// - } +// + first = false +// buf.WriteString(`"log":`) +// ffjsonWriteJSONString(buf, mj.Log) +// } + +package jsonlog + +import ( + "bytes" + "unicode/utf8" +) + +// MarshalJSON marshals the JSONLog. +func (mj *JSONLog) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + buf.Grow(1024) + if err := mj.MarshalJSONBuf(&buf); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// MarshalJSONBuf marshals the JSONLog and stores the result to a bytes.Buffer. +func (mj *JSONLog) MarshalJSONBuf(buf *bytes.Buffer) error { + var ( + err error + timestamp string + first = true + ) + buf.WriteString(`{`) + if len(mj.Log) != 0 { + first = false + buf.WriteString(`"log":`) + ffjsonWriteJSONString(buf, mj.Log) + } + if len(mj.Stream) != 0 { + if first { + first = false + } else { + buf.WriteString(`,`) + } + buf.WriteString(`"stream":`) + ffjsonWriteJSONString(buf, mj.Stream) + } + if !first { + buf.WriteString(`,`) + } + buf.WriteString(`"time":`) + timestamp, err = FastTimeMarshalJSON(mj.Created) + if err != nil { + return err + } + buf.WriteString(timestamp) + buf.WriteString(`}`) + return nil +} + +func ffjsonWriteJSONString(buf *bytes.Buffer, s string) { + const hex = "0123456789abcdef" + + buf.WriteByte('"') + start := 0 + for i := 0; i < len(s); { + if b := s[i]; b < utf8.RuneSelf { + if 0x20 <= b && b != '\\' && b != '"' && b != '<' && b != '>' && b != '&' { + i++ + continue + } + if start < i { + buf.WriteString(s[start:i]) + } + switch b { + case '\\', '"': + buf.WriteByte('\\') + buf.WriteByte(b) + case '\n': + buf.WriteByte('\\') + buf.WriteByte('n') + case '\r': + buf.WriteByte('\\') + buf.WriteByte('r') + default: + + buf.WriteString(`\u00`) + buf.WriteByte(hex[b>>4]) + buf.WriteByte(hex[b&0xF]) + } + i++ + start = i + continue + } + c, size := utf8.DecodeRuneInString(s[i:]) + if c == utf8.RuneError && size == 1 { + if start < i { + buf.WriteString(s[start:i]) + } + buf.WriteString(`\ufffd`) + i += size + start = i + continue + } + + if c == '\u2028' || c == '\u2029' { + if start < i { + buf.WriteString(s[start:i]) + } + buf.WriteString(`\u202`) + buf.WriteByte(hex[c&0xF]) + i += size + start = i + continue + } + i += size + } + if start < len(s) { + buf.WriteString(s[start:]) + } + buf.WriteByte('"') +} diff --git a/pkg/jsonlog/jsonlog_marshalling_test.go b/pkg/jsonlog/jsonlog_marshalling_test.go new file mode 100644 index 00000000..5e455685 --- /dev/null +++ b/pkg/jsonlog/jsonlog_marshalling_test.go @@ -0,0 +1,34 @@ +package jsonlog + +import ( + "regexp" + "testing" +) + +func TestJSONLogMarshalJSON(t *testing.T) { + logs := map[JSONLog]string{ + JSONLog{Log: `"A log line with \\"`}: `^{\"log\":\"\\\"A log line with \\\\\\\\\\\"\",\"time\":\".{20,}\"}$`, + JSONLog{Log: "A log line"}: `^{\"log\":\"A log line\",\"time\":\".{20,}\"}$`, + JSONLog{Log: "A log line with \r"}: `^{\"log\":\"A log line with \\r\",\"time\":\".{20,}\"}$`, + JSONLog{Log: "A log line with & < >"}: `^{\"log\":\"A log line with \\u0026 \\u003c \\u003e\",\"time\":\".{20,}\"}$`, + JSONLog{Log: "A log line with utf8 : 🚀 ψ ω β"}: `^{\"log\":\"A log line with utf8 : 🚀 ψ ω β\",\"time\":\".{20,}\"}$`, + JSONLog{Stream: "stdout"}: `^{\"stream\":\"stdout\",\"time\":\".{20,}\"}$`, + JSONLog{}: `^{\"time\":\".{20,}\"}$`, + // These ones are a little weird + JSONLog{Log: "\u2028 \u2029"}: `^{\"log\":\"\\u2028 \\u2029\",\"time\":\".{20,}\"}$`, + JSONLog{Log: string([]byte{0xaF})}: `^{\"log\":\"\\ufffd\",\"time\":\".{20,}\"}$`, + JSONLog{Log: string([]byte{0x7F})}: `^{\"log\":\"\x7f\",\"time\":\".{20,}\"}$`, + } + for jsonLog, expression := range logs { + data, err := jsonLog.MarshalJSON() + if err != nil { + t.Fatal(err) + } + res := string(data) + t.Logf("Result of WriteLog: %q", res) + logRe := regexp.MustCompile(expression) + if !logRe.MatchString(res) { + t.Fatalf("Log line not in expected format [%v]: %q", expression, res) + } + } +} diff --git a/pkg/jsonlog/jsonlogbytes.go b/pkg/jsonlog/jsonlogbytes.go new file mode 100644 index 00000000..df522c0d --- /dev/null +++ b/pkg/jsonlog/jsonlogbytes.go @@ -0,0 +1,122 @@ +package jsonlog + +import ( + "bytes" + "encoding/json" + "unicode/utf8" +) + +// JSONLogs is based on JSONLog. +// It allows marshalling JSONLog from Log as []byte +// and an already marshalled Created timestamp. +type JSONLogs struct { + Log []byte `json:"log,omitempty"` + Stream string `json:"stream,omitempty"` + Created string `json:"time"` + + // json-encoded bytes + RawAttrs json.RawMessage `json:"attrs,omitempty"` +} + +// MarshalJSONBuf is based on the same method from JSONLog +// It has been modified to take into account the necessary changes. +func (mj *JSONLogs) MarshalJSONBuf(buf *bytes.Buffer) error { + var first = true + + buf.WriteString(`{`) + if len(mj.Log) != 0 { + first = false + buf.WriteString(`"log":`) + ffjsonWriteJSONBytesAsString(buf, mj.Log) + } + if len(mj.Stream) != 0 { + if first == true { + first = false + } else { + buf.WriteString(`,`) + } + buf.WriteString(`"stream":`) + ffjsonWriteJSONString(buf, mj.Stream) + } + if len(mj.RawAttrs) > 0 { + if first { + first = false + } else { + buf.WriteString(`,`) + } + buf.WriteString(`"attrs":`) + buf.Write(mj.RawAttrs) + } + if !first { + buf.WriteString(`,`) + } + buf.WriteString(`"time":`) + buf.WriteString(mj.Created) + buf.WriteString(`}`) + return nil +} + +// This is based on ffjsonWriteJSONBytesAsString. It has been changed +// to accept a string passed as a slice of bytes. +func ffjsonWriteJSONBytesAsString(buf *bytes.Buffer, s []byte) { + const hex = "0123456789abcdef" + + buf.WriteByte('"') + start := 0 + for i := 0; i < len(s); { + if b := s[i]; b < utf8.RuneSelf { + if 0x20 <= b && b != '\\' && b != '"' && b != '<' && b != '>' && b != '&' { + i++ + continue + } + if start < i { + buf.Write(s[start:i]) + } + switch b { + case '\\', '"': + buf.WriteByte('\\') + buf.WriteByte(b) + case '\n': + buf.WriteByte('\\') + buf.WriteByte('n') + case '\r': + buf.WriteByte('\\') + buf.WriteByte('r') + default: + + buf.WriteString(`\u00`) + buf.WriteByte(hex[b>>4]) + buf.WriteByte(hex[b&0xF]) + } + i++ + start = i + continue + } + c, size := utf8.DecodeRune(s[i:]) + if c == utf8.RuneError && size == 1 { + if start < i { + buf.Write(s[start:i]) + } + buf.WriteString(`\ufffd`) + i += size + start = i + continue + } + + if c == '\u2028' || c == '\u2029' { + if start < i { + buf.Write(s[start:i]) + } + buf.WriteString(`\u202`) + buf.WriteByte(hex[c&0xF]) + i += size + start = i + continue + } + i += size + } + if start < len(s) { + buf.Write(s[start:]) + } + buf.WriteByte('"') +} diff --git a/pkg/jsonlog/jsonlogbytes_test.go b/pkg/jsonlog/jsonlogbytes_test.go new file mode 100644 index 00000000..6d6ad215 --- /dev/null +++ b/pkg/jsonlog/jsonlogbytes_test.go @@ -0,0 +1,39 @@ +package jsonlog + +import ( + "bytes" + "regexp" + "testing" +) + +func TestJSONLogsMarshalJSONBuf(t *testing.T) { + logs := map[*JSONLogs]string{ + &JSONLogs{Log: []byte(`"A log line with \\"`)}: `^{\"log\":\"\\\"A log line with \\\\\\\\\\\"\",\"time\":}$`, + &JSONLogs{Log: []byte("A log line")}: `^{\"log\":\"A log line\",\"time\":}$`, + &JSONLogs{Log: []byte("A log line with \r")}: `^{\"log\":\"A log line with \\r\",\"time\":}$`, + &JSONLogs{Log: []byte("A log line with & < >")}: `^{\"log\":\"A log line with \\u0026 \\u003c \\u003e\",\"time\":}$`, + &JSONLogs{Log: []byte("A log line with utf8 : 🚀 ψ ω β")}: `^{\"log\":\"A log line with utf8 : 🚀 ψ ω β\",\"time\":}$`, + &JSONLogs{Stream: "stdout"}: `^{\"stream\":\"stdout\",\"time\":}$`, + &JSONLogs{Stream: "stdout", Log: []byte("A log line")}: `^{\"log\":\"A log line\",\"stream\":\"stdout\",\"time\":}$`, + &JSONLogs{Created: "time"}: `^{\"time\":time}$`, + &JSONLogs{}: `^{\"time\":}$`, + // These ones are a little weird + &JSONLogs{Log: []byte("\u2028 \u2029")}: `^{\"log\":\"\\u2028 \\u2029\",\"time\":}$`, + &JSONLogs{Log: []byte{0xaF}}: `^{\"log\":\"\\ufffd\",\"time\":}$`, + &JSONLogs{Log: []byte{0x7F}}: `^{\"log\":\"\x7f\",\"time\":}$`, + // with raw attributes + &JSONLogs{Log: []byte("A log line"), RawAttrs: []byte(`{"hello":"world","value":1234}`)}: `^{\"log\":\"A log line\",\"attrs\":{\"hello\":\"world\",\"value\":1234},\"time\":}$`, + } + for jsonLog, expression := range logs { + var buf bytes.Buffer + if err := jsonLog.MarshalJSONBuf(&buf); err != nil { + t.Fatal(err) + } + res := buf.String() + t.Logf("Result of WriteLog: %q", res) + logRe := regexp.MustCompile(expression) + if !logRe.MatchString(res) { + t.Fatalf("Log line not in expected format [%v]: %q", expression, res) + } + } +} diff --git a/pkg/jsonlog/time_marshalling.go b/pkg/jsonlog/time_marshalling.go new file mode 100644 index 00000000..21173381 --- /dev/null +++ b/pkg/jsonlog/time_marshalling.go @@ -0,0 +1,27 @@ +// Package jsonlog provides helper functions to parse and print time (time.Time) as JSON. +package jsonlog + +import ( + "errors" + "time" +) + +const ( + // RFC3339NanoFixed is our own version of RFC339Nano because we want one + // that pads the nano seconds part with zeros to ensure + // the timestamps are aligned in the logs. + RFC3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00" + // JSONFormat is the format used by FastMarshalJSON + JSONFormat = `"` + time.RFC3339Nano + `"` +) + +// FastTimeMarshalJSON avoids one of the extra allocations that +// time.MarshalJSON is making. +func FastTimeMarshalJSON(t time.Time) (string, error) { + if y := t.Year(); y < 0 || y >= 10000 { + // RFC 3339 is clear that years are 4 digits exactly. + // See golang.org/issue/4556#c15 for more discussion. + return "", errors.New("time.MarshalJSON: year outside of range [0,9999]") + } + return t.Format(JSONFormat), nil +} diff --git a/pkg/jsonlog/time_marshalling_test.go b/pkg/jsonlog/time_marshalling_test.go new file mode 100644 index 00000000..02d0302c --- /dev/null +++ b/pkg/jsonlog/time_marshalling_test.go @@ -0,0 +1,47 @@ +package jsonlog + +import ( + "testing" + "time" +) + +// Testing to ensure 'year' fields is between 0 and 9999 +func TestFastTimeMarshalJSONWithInvalidDate(t *testing.T) { + aTime := time.Date(-1, 1, 1, 0, 0, 0, 0, time.Local) + json, err := FastTimeMarshalJSON(aTime) + if err == nil { + t.Fatalf("FastTimeMarshalJSON should throw an error, but was '%v'", json) + } + anotherTime := time.Date(10000, 1, 1, 0, 0, 0, 0, time.Local) + json, err = FastTimeMarshalJSON(anotherTime) + if err == nil { + t.Fatalf("FastTimeMarshalJSON should throw an error, but was '%v'", json) + } + +} + +func TestFastTimeMarshalJSON(t *testing.T) { + aTime := time.Date(2015, 5, 29, 11, 1, 2, 3, time.UTC) + json, err := FastTimeMarshalJSON(aTime) + if err != nil { + t.Fatal(err) + } + expected := "\"2015-05-29T11:01:02.000000003Z\"" + if json != expected { + t.Fatalf("Expected %v, got %v", expected, json) + } + + location, err := time.LoadLocation("Europe/Paris") + if err != nil { + t.Fatal(err) + } + aTime = time.Date(2015, 5, 29, 11, 1, 2, 3, location) + json, err = FastTimeMarshalJSON(aTime) + if err != nil { + t.Fatal(err) + } + expected = "\"2015-05-29T11:01:02.000000003+02:00\"" + if json != expected { + t.Fatalf("Expected %v, got %v", expected, json) + } +} diff --git a/pkg/jsonmessage/jsonmessage.go b/pkg/jsonmessage/jsonmessage.go new file mode 100644 index 00000000..65cccbce --- /dev/null +++ b/pkg/jsonmessage/jsonmessage.go @@ -0,0 +1,221 @@ +package jsonmessage + +import ( + "encoding/json" + "fmt" + "io" + "strings" + "time" + + "github.com/docker/docker/pkg/jsonlog" + "github.com/docker/docker/pkg/term" + "github.com/docker/go-units" +) + +// JSONError wraps a concrete Code and Message, `Code` is +// is a integer error code, `Message` is the error message. +type JSONError struct { + Code int `json:"code,omitempty"` + Message string `json:"message,omitempty"` +} + +func (e *JSONError) Error() string { + return e.Message +} + +// JSONProgress describes a Progress. terminalFd is the fd of the current terminal, +// Start is the initial value for the operation. Current is the current status and +// value of the progress made towards Total. Total is the end value describing when +// we made 100% progress for an operation. +type JSONProgress struct { + terminalFd uintptr + Current int64 `json:"current,omitempty"` + Total int64 `json:"total,omitempty"` + Start int64 `json:"start,omitempty"` +} + +func (p *JSONProgress) String() string { + var ( + width = 200 + pbBox string + numbersBox string + timeLeftBox string + ) + + ws, err := term.GetWinsize(p.terminalFd) + if err == nil { + width = int(ws.Width) + } + + if p.Current <= 0 && p.Total <= 0 { + return "" + } + current := units.HumanSize(float64(p.Current)) + if p.Total <= 0 { + return fmt.Sprintf("%8v", current) + } + total := units.HumanSize(float64(p.Total)) + percentage := int(float64(p.Current)/float64(p.Total)*100) / 2 + if percentage > 50 { + percentage = 50 + } + if width > 110 { + // this number can't be negative gh#7136 + numSpaces := 0 + if 50-percentage > 0 { + numSpaces = 50 - percentage + } + pbBox = fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces)) + } + + numbersBox = fmt.Sprintf("%8v/%v", current, total) + + if p.Current > p.Total { + // remove total display if the reported current is wonky. + numbersBox = fmt.Sprintf("%8v", current) + } + + if p.Current > 0 && p.Start > 0 && percentage < 50 { + fromStart := time.Now().UTC().Sub(time.Unix(p.Start, 0)) + perEntry := fromStart / time.Duration(p.Current) + left := time.Duration(p.Total-p.Current) * perEntry + left = (left / time.Second) * time.Second + + if width > 50 { + timeLeftBox = " " + left.String() + } + } + return pbBox + numbersBox + timeLeftBox +} + +// JSONMessage defines a message struct. It describes +// the created time, where it from, status, ID of the +// message. It's used for docker events. +type JSONMessage struct { + Stream string `json:"stream,omitempty"` + Status string `json:"status,omitempty"` + Progress *JSONProgress `json:"progressDetail,omitempty"` + ProgressMessage string `json:"progress,omitempty"` //deprecated + ID string `json:"id,omitempty"` + From string `json:"from,omitempty"` + Time int64 `json:"time,omitempty"` + TimeNano int64 `json:"timeNano,omitempty"` + Error *JSONError `json:"errorDetail,omitempty"` + ErrorMessage string `json:"error,omitempty"` //deprecated + // Aux contains out-of-band data, such as digests for push signing. + Aux *json.RawMessage `json:"aux,omitempty"` +} + +// Display displays the JSONMessage to `out`. `isTerminal` describes if `out` +// is a terminal. If this is the case, it will erase the entire current line +// when displaying the progressbar. +func (jm *JSONMessage) Display(out io.Writer, isTerminal bool) error { + if jm.Error != nil { + if jm.Error.Code == 401 { + return fmt.Errorf("Authentication is required.") + } + return jm.Error + } + var endl string + if isTerminal && jm.Stream == "" && jm.Progress != nil { + // [2K = erase entire current line + fmt.Fprintf(out, "%c[2K\r", 27) + endl = "\r" + } else if jm.Progress != nil && jm.Progress.String() != "" { //disable progressbar in non-terminal + return nil + } + if jm.TimeNano != 0 { + fmt.Fprintf(out, "%s ", time.Unix(0, jm.TimeNano).Format(jsonlog.RFC3339NanoFixed)) + } else if jm.Time != 0 { + fmt.Fprintf(out, "%s ", time.Unix(jm.Time, 0).Format(jsonlog.RFC3339NanoFixed)) + } + if jm.ID != "" { + fmt.Fprintf(out, "%s: ", jm.ID) + } + if jm.From != "" { + fmt.Fprintf(out, "(from %s) ", jm.From) + } + if jm.Progress != nil && isTerminal { + fmt.Fprintf(out, "%s %s%s", jm.Status, jm.Progress.String(), endl) + } else if jm.ProgressMessage != "" { //deprecated + fmt.Fprintf(out, "%s %s%s", jm.Status, jm.ProgressMessage, endl) + } else if jm.Stream != "" { + fmt.Fprintf(out, "%s%s", jm.Stream, endl) + } else { + fmt.Fprintf(out, "%s%s\n", jm.Status, endl) + } + return nil +} + +// DisplayJSONMessagesStream displays a json message stream from `in` to `out`, `isTerminal` +// describes if `out` is a terminal. If this is the case, it will print `\n` at the end of +// each line and move the cursor while displaying. +func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr, isTerminal bool, auxCallback func(*json.RawMessage)) error { + var ( + dec = json.NewDecoder(in) + ids = make(map[string]int) + ) + for { + diff := 0 + var jm JSONMessage + if err := dec.Decode(&jm); err != nil { + if err == io.EOF { + break + } + return err + } + + if jm.Aux != nil { + if auxCallback != nil { + auxCallback(jm.Aux) + } + continue + } + + if jm.Progress != nil { + jm.Progress.terminalFd = terminalFd + } + if jm.ID != "" && (jm.Progress != nil || jm.ProgressMessage != "") { + line, ok := ids[jm.ID] + if !ok { + // NOTE: This approach of using len(id) to + // figure out the number of lines of history + // only works as long as we clear the history + // when we output something that's not + // accounted for in the map, such as a line + // with no ID. + line = len(ids) + ids[jm.ID] = line + if isTerminal { + fmt.Fprintf(out, "\n") + } + } else { + diff = len(ids) - line + } + if isTerminal { + // NOTE: this appears to be necessary even if + // diff == 0. + // [{diff}A = move cursor up diff rows + fmt.Fprintf(out, "%c[%dA", 27, diff) + } + } else { + // When outputting something that isn't progress + // output, clear the history of previous lines. We + // don't want progress entries from some previous + // operation to be updated (for example, pull -a + // with multiple tags). + ids = make(map[string]int) + } + err := jm.Display(out, isTerminal) + if jm.ID != "" && isTerminal { + // NOTE: this appears to be necessary even if + // diff == 0. + // [{diff}B = move cursor down diff rows + fmt.Fprintf(out, "%c[%dB", 27, diff) + } + if err != nil { + return err + } + } + return nil +} diff --git a/pkg/jsonmessage/jsonmessage_test.go b/pkg/jsonmessage/jsonmessage_test.go new file mode 100644 index 00000000..558effcc --- /dev/null +++ b/pkg/jsonmessage/jsonmessage_test.go @@ -0,0 +1,231 @@ +package jsonmessage + +import ( + "bytes" + "fmt" + "strings" + "testing" + "time" + + "github.com/docker/docker/pkg/jsonlog" + "github.com/docker/docker/pkg/term" +) + +func TestError(t *testing.T) { + je := JSONError{404, "Not found"} + if je.Error() != "Not found" { + t.Fatalf("Expected 'Not found' got '%s'", je.Error()) + } +} + +func TestProgress(t *testing.T) { + jp := JSONProgress{} + if jp.String() != "" { + t.Fatalf("Expected empty string, got '%s'", jp.String()) + } + + expected := " 1 B" + jp2 := JSONProgress{Current: 1} + if jp2.String() != expected { + t.Fatalf("Expected %q, got %q", expected, jp2.String()) + } + + expectedStart := "[==========> ] 20 B/100 B" + jp3 := JSONProgress{Current: 20, Total: 100, Start: time.Now().Unix()} + // Just look at the start of the string + // (the remaining time is really hard to test -_-) + if jp3.String()[:len(expectedStart)] != expectedStart { + t.Fatalf("Expected to start with %q, got %q", expectedStart, jp3.String()) + } + + expected = "[=========================> ] 50 B/100 B" + jp4 := JSONProgress{Current: 50, Total: 100} + if jp4.String() != expected { + t.Fatalf("Expected %q, got %q", expected, jp4.String()) + } + + // this number can't be negative gh#7136 + expected = "[==================================================>] 50 B" + jp5 := JSONProgress{Current: 50, Total: 40} + if jp5.String() != expected { + t.Fatalf("Expected %q, got %q", expected, jp5.String()) + } +} + +func TestJSONMessageDisplay(t *testing.T) { + now := time.Now() + messages := map[JSONMessage][]string{ + // Empty + JSONMessage{}: {"\n", "\n"}, + // Status + JSONMessage{ + Status: "status", + }: { + "status\n", + "status\n", + }, + // General + JSONMessage{ + Time: now.Unix(), + ID: "ID", + From: "From", + Status: "status", + }: { + fmt.Sprintf("%v ID: (from From) status\n", time.Unix(now.Unix(), 0).Format(jsonlog.RFC3339NanoFixed)), + fmt.Sprintf("%v ID: (from From) status\n", time.Unix(now.Unix(), 0).Format(jsonlog.RFC3339NanoFixed)), + }, + // General, with nano precision time + JSONMessage{ + TimeNano: now.UnixNano(), + ID: "ID", + From: "From", + Status: "status", + }: { + fmt.Sprintf("%v ID: (from From) status\n", time.Unix(0, now.UnixNano()).Format(jsonlog.RFC3339NanoFixed)), + fmt.Sprintf("%v ID: (from From) status\n", time.Unix(0, now.UnixNano()).Format(jsonlog.RFC3339NanoFixed)), + }, + // General, with both times Nano is preferred + JSONMessage{ + Time: now.Unix(), + TimeNano: now.UnixNano(), + ID: "ID", + From: "From", + Status: "status", + }: { + fmt.Sprintf("%v ID: (from From) status\n", time.Unix(0, now.UnixNano()).Format(jsonlog.RFC3339NanoFixed)), + fmt.Sprintf("%v ID: (from From) status\n", time.Unix(0, now.UnixNano()).Format(jsonlog.RFC3339NanoFixed)), + }, + // Stream over status + JSONMessage{ + Status: "status", + Stream: "stream", + }: { + "stream", + "stream", + }, + // With progress message + JSONMessage{ + Status: "status", + ProgressMessage: "progressMessage", + }: { + "status progressMessage", + "status progressMessage", + }, + // With progress, stream empty + JSONMessage{ + Status: "status", + Stream: "", + Progress: &JSONProgress{Current: 1}, + }: { + "", + fmt.Sprintf("%c[2K\rstatus 1 B\r", 27), + }, + } + + // The tests :) + for jsonMessage, expectedMessages := range messages { + // Without terminal + data := bytes.NewBuffer([]byte{}) + if err := jsonMessage.Display(data, false); err != nil { + t.Fatal(err) + } + if data.String() != expectedMessages[0] { + t.Fatalf("Expected [%v], got [%v]", expectedMessages[0], data.String()) + } + // With terminal + data = bytes.NewBuffer([]byte{}) + if err := jsonMessage.Display(data, true); err != nil { + t.Fatal(err) + } + if data.String() != expectedMessages[1] { + t.Fatalf("Expected [%v], got [%v]", expectedMessages[1], data.String()) + } + } +} + +// Test JSONMessage with an Error. It will return an error with the text as error, not the meaning of the HTTP code. +func TestJSONMessageDisplayWithJSONError(t *testing.T) { + data := bytes.NewBuffer([]byte{}) + jsonMessage := JSONMessage{Error: &JSONError{404, "Can't find it"}} + + err := jsonMessage.Display(data, true) + if err == nil || err.Error() != "Can't find it" { + t.Fatalf("Expected a JSONError 404, got [%v]", err) + } + + jsonMessage = JSONMessage{Error: &JSONError{401, "Anything"}} + err = jsonMessage.Display(data, true) + if err == nil || err.Error() != "Authentication is required." { + t.Fatalf("Expected an error [Authentication is required.], got [%v]", err) + } +} + +func TestDisplayJSONMessagesStreamInvalidJSON(t *testing.T) { + var ( + inFd uintptr + ) + data := bytes.NewBuffer([]byte{}) + reader := strings.NewReader("This is not a 'valid' JSON []") + inFd, _ = term.GetFdInfo(reader) + + if err := DisplayJSONMessagesStream(reader, data, inFd, false, nil); err == nil && err.Error()[:17] != "invalid character" { + t.Fatalf("Should have thrown an error (invalid character in ..), got [%v]", err) + } +} + +func TestDisplayJSONMessagesStream(t *testing.T) { + var ( + inFd uintptr + ) + + messages := map[string][]string{ + // empty string + "": { + "", + ""}, + // Without progress & ID + "{ \"status\": \"status\" }": { + "status\n", + "status\n", + }, + // Without progress, with ID + "{ \"id\": \"ID\",\"status\": \"status\" }": { + "ID: status\n", + fmt.Sprintf("ID: status\n%c[%dB", 27, 0), + }, + // With progress + "{ \"id\": \"ID\", \"status\": \"status\", \"progress\": \"ProgressMessage\" }": { + "ID: status ProgressMessage", + fmt.Sprintf("\n%c[%dAID: status ProgressMessage%c[%dB", 27, 0, 27, 0), + }, + // With progressDetail + "{ \"id\": \"ID\", \"status\": \"status\", \"progressDetail\": { \"Current\": 1} }": { + "", // progressbar is disabled in non-terminal + fmt.Sprintf("\n%c[%dA%c[2K\rID: status 1 B\r%c[%dB", 27, 0, 27, 27, 0), + }, + } + for jsonMessage, expectedMessages := range messages { + data := bytes.NewBuffer([]byte{}) + reader := strings.NewReader(jsonMessage) + inFd, _ = term.GetFdInfo(reader) + + // Without terminal + if err := DisplayJSONMessagesStream(reader, data, inFd, false, nil); err != nil { + t.Fatal(err) + } + if data.String() != expectedMessages[0] { + t.Fatalf("Expected an [%v], got [%v]", expectedMessages[0], data.String()) + } + + // With terminal + data = bytes.NewBuffer([]byte{}) + reader = strings.NewReader(jsonMessage) + if err := DisplayJSONMessagesStream(reader, data, inFd, true, nil); err != nil { + t.Fatal(err) + } + if data.String() != expectedMessages[1] { + t.Fatalf("Expected an [%v], got [%v]", expectedMessages[1], data.String()) + } + } + +} diff --git a/pkg/locker/README.md b/pkg/locker/README.md new file mode 100644 index 00000000..e84a815c --- /dev/null +++ b/pkg/locker/README.md @@ -0,0 +1,65 @@ +Locker +===== + +locker provides a mechanism for creating finer-grained locking to help +free up more global locks to handle other tasks. + +The implementation looks close to a sync.Mutex, however the user must provide a +reference to use to refer to the underlying lock when locking and unlocking, +and unlock may generate an error. + +If a lock with a given name does not exist when `Lock` is called, one is +created. +Lock references are automatically cleaned up on `Unlock` if nothing else is +waiting for the lock. + + +## Usage + +```go +package important + +import ( + "sync" + "time" + + "github.com/docker/docker/pkg/locker" +) + +type important struct { + locks *locker.Locker + data map[string]interface{} + mu sync.Mutex +} + +func (i *important) Get(name string) interface{} { + i.locks.Lock(name) + defer i.locks.Unlock(name) + return data[name] +} + +func (i *important) Create(name string, data interface{}) { + i.locks.Lock(name) + defer i.locks.Unlock(name) + + i.createImportant(data) + + s.mu.Lock() + i.data[name] = data + s.mu.Unlock() +} + +func (i *important) createImportant(data interface{}) { + time.Sleep(10 * time.Second) +} +``` + +For functions dealing with a given name, always lock at the beginning of the +function (or before doing anything with the underlying state), this ensures any +other function that is dealing with the same name will block. + +When needing to modify the underlying data, use the global lock to ensure nothing +else is modfying it at the same time. +Since name lock is already in place, no reads will occur while the modification +is being performed. + diff --git a/pkg/locker/locker.go b/pkg/locker/locker.go new file mode 100644 index 00000000..0b22ddfa --- /dev/null +++ b/pkg/locker/locker.go @@ -0,0 +1,112 @@ +/* +Package locker provides a mechanism for creating finer-grained locking to help +free up more global locks to handle other tasks. + +The implementation looks close to a sync.Mutex, however the user must provide a +reference to use to refer to the underlying lock when locking and unlocking, +and unlock may generate an error. + +If a lock with a given name does not exist when `Lock` is called, one is +created. +Lock references are automatically cleaned up on `Unlock` if nothing else is +waiting for the lock. +*/ +package locker + +import ( + "errors" + "sync" + "sync/atomic" +) + +// ErrNoSuchLock is returned when the requested lock does not exist +var ErrNoSuchLock = errors.New("no such lock") + +// Locker provides a locking mechanism based on the passed in reference name +type Locker struct { + mu sync.Mutex + locks map[string]*lockCtr +} + +// lockCtr is used by Locker to represent a lock with a given name. +type lockCtr struct { + mu sync.Mutex + // waiters is the number of waiters waiting to acquire the lock + // this is int32 instead of uint32 so we can add `-1` in `dec()` + waiters int32 +} + +// inc increments the number of waiters waiting for the lock +func (l *lockCtr) inc() { + atomic.AddInt32(&l.waiters, 1) +} + +// dec decrements the number of waiters waiting on the lock +func (l *lockCtr) dec() { + atomic.AddInt32(&l.waiters, -1) +} + +// count gets the current number of waiters +func (l *lockCtr) count() int32 { + return atomic.LoadInt32(&l.waiters) +} + +// Lock locks the mutex +func (l *lockCtr) Lock() { + l.mu.Lock() +} + +// Unlock unlocks the mutex +func (l *lockCtr) Unlock() { + l.mu.Unlock() +} + +// New creates a new Locker +func New() *Locker { + return &Locker{ + locks: make(map[string]*lockCtr), + } +} + +// Lock locks a mutex with the given name. If it doesn't exist, one is created +func (l *Locker) Lock(name string) { + l.mu.Lock() + if l.locks == nil { + l.locks = make(map[string]*lockCtr) + } + + nameLock, exists := l.locks[name] + if !exists { + nameLock = &lockCtr{} + l.locks[name] = nameLock + } + + // increment the nameLock waiters while inside the main mutex + // this makes sure that the lock isn't deleted if `Lock` and `Unlock` are called concurrently + nameLock.inc() + l.mu.Unlock() + + // Lock the nameLock outside the main mutex so we don't block other operations + // once locked then we can decrement the number of waiters for this lock + nameLock.Lock() + nameLock.dec() +} + +// Unlock unlocks the mutex with the given name +// If the given lock is not being waited on by any other callers, it is deleted +func (l *Locker) Unlock(name string) error { + l.mu.Lock() + nameLock, exists := l.locks[name] + if !exists { + l.mu.Unlock() + return ErrNoSuchLock + } + + if nameLock.count() == 0 { + delete(l.locks, name) + } + nameLock.Unlock() + + l.mu.Unlock() + return nil +} diff --git a/pkg/locker/locker_test.go b/pkg/locker/locker_test.go new file mode 100644 index 00000000..5a297dd4 --- /dev/null +++ b/pkg/locker/locker_test.go @@ -0,0 +1,124 @@ +package locker + +import ( + "sync" + "testing" + "time" +) + +func TestLockCounter(t *testing.T) { + l := &lockCtr{} + l.inc() + + if l.waiters != 1 { + t.Fatal("counter inc failed") + } + + l.dec() + if l.waiters != 0 { + t.Fatal("counter dec failed") + } +} + +func TestLockerLock(t *testing.T) { + l := New() + l.Lock("test") + ctr := l.locks["test"] + + if ctr.count() != 0 { + t.Fatalf("expected waiters to be 0, got :%d", ctr.waiters) + } + + chDone := make(chan struct{}) + go func() { + l.Lock("test") + close(chDone) + }() + + chWaiting := make(chan struct{}) + go func() { + for range time.Tick(1 * time.Millisecond) { + if ctr.count() == 1 { + close(chWaiting) + break + } + } + }() + + select { + case <-chWaiting: + case <-time.After(3 * time.Second): + t.Fatal("timed out waiting for lock waiters to be incremented") + } + + select { + case <-chDone: + t.Fatal("lock should not have returned while it was still held") + default: + } + + if err := l.Unlock("test"); err != nil { + t.Fatal(err) + } + + select { + case <-chDone: + case <-time.After(3 * time.Second): + t.Fatalf("lock should have completed") + } + + if ctr.count() != 0 { + t.Fatalf("expected waiters to be 0, got: %d", ctr.count()) + } +} + +func TestLockerUnlock(t *testing.T) { + l := New() + + l.Lock("test") + l.Unlock("test") + + chDone := make(chan struct{}) + go func() { + l.Lock("test") + close(chDone) + }() + + select { + case <-chDone: + case <-time.After(3 * time.Second): + t.Fatalf("lock should not be blocked") + } +} + +func TestLockerConcurrency(t *testing.T) { + l := New() + + var wg sync.WaitGroup + for i := 0; i <= 10000; i++ { + wg.Add(1) + go func() { + l.Lock("test") + // if there is a concurrency issue, will very likely panic here + l.Unlock("test") + wg.Done() + }() + } + + chDone := make(chan struct{}) + go func() { + wg.Wait() + close(chDone) + }() + + select { + case <-chDone: + case <-time.After(10 * time.Second): + t.Fatal("timeout waiting for locks to complete") + } + + // Since everything has unlocked this should not exist anymore + if ctr, exists := l.locks["test"]; exists { + t.Fatalf("lock should not exist: %v", ctr) + } +} diff --git a/pkg/longpath/longpath.go b/pkg/longpath/longpath.go new file mode 100644 index 00000000..9b15bfff --- /dev/null +++ b/pkg/longpath/longpath.go @@ -0,0 +1,26 @@ +// longpath introduces some constants and helper functions for handling long paths +// in Windows, which are expected to be prepended with `\\?\` and followed by either +// a drive letter, a UNC server\share, or a volume identifier. + +package longpath + +import ( + "strings" +) + +// Prefix is the longpath prefix for Windows file paths. +const Prefix = `\\?\` + +// AddPrefix will add the Windows long path prefix to the path provided if +// it does not already have it. +func AddPrefix(path string) string { + if !strings.HasPrefix(path, Prefix) { + if strings.HasPrefix(path, `\\`) { + // This is a UNC path, so we need to add 'UNC' to the path as well. + path = Prefix + `UNC` + path[1:] + } else { + path = Prefix + path + } + } + return path +} diff --git a/pkg/longpath/longpath_test.go b/pkg/longpath/longpath_test.go new file mode 100644 index 00000000..01865eff --- /dev/null +++ b/pkg/longpath/longpath_test.go @@ -0,0 +1,22 @@ +package longpath + +import ( + "strings" + "testing" +) + +func TestStandardLongPath(t *testing.T) { + c := `C:\simple\path` + longC := AddPrefix(c) + if !strings.EqualFold(longC, `\\?\C:\simple\path`) { + t.Errorf("Wrong long path returned. Original = %s ; Long = %s", c, longC) + } +} + +func TestUNCLongPath(t *testing.T) { + c := `\\server\share\path` + longC := AddPrefix(c) + if !strings.EqualFold(longC, `\\?\UNC\server\share\path`) { + t.Errorf("Wrong UNC long path returned. Original = %s ; Long = %s", c, longC) + } +} diff --git a/pkg/loopback/attach_loopback.go b/pkg/loopback/attach_loopback.go new file mode 100644 index 00000000..c6d72d73 --- /dev/null +++ b/pkg/loopback/attach_loopback.go @@ -0,0 +1,137 @@ +// +build linux + +package loopback + +import ( + "errors" + "fmt" + "os" + "syscall" + + "github.com/Sirupsen/logrus" +) + +// Loopback related errors +var ( + ErrAttachLoopbackDevice = errors.New("loopback attach failed") + ErrGetLoopbackBackingFile = errors.New("Unable to get loopback backing file") + ErrSetCapacity = errors.New("Unable set loopback capacity") +) + +func stringToLoopName(src string) [LoNameSize]uint8 { + var dst [LoNameSize]uint8 + copy(dst[:], src[:]) + return dst +} + +func getNextFreeLoopbackIndex() (int, error) { + f, err := os.OpenFile("/dev/loop-control", os.O_RDONLY, 0644) + if err != nil { + return 0, err + } + defer f.Close() + + index, err := ioctlLoopCtlGetFree(f.Fd()) + if index < 0 { + index = 0 + } + return index, err +} + +func openNextAvailableLoopback(index int, sparseFile *os.File) (loopFile *os.File, err error) { + // Start looking for a free /dev/loop + for { + target := fmt.Sprintf("/dev/loop%d", index) + index++ + + fi, err := os.Stat(target) + if err != nil { + if os.IsNotExist(err) { + logrus.Errorf("There are no more loopback devices available.") + } + return nil, ErrAttachLoopbackDevice + } + + if fi.Mode()&os.ModeDevice != os.ModeDevice { + logrus.Errorf("Loopback device %s is not a block device.", target) + continue + } + + // OpenFile adds O_CLOEXEC + loopFile, err = os.OpenFile(target, os.O_RDWR, 0644) + if err != nil { + logrus.Errorf("Error opening loopback device: %s", err) + return nil, ErrAttachLoopbackDevice + } + + // Try to attach to the loop file + if err := ioctlLoopSetFd(loopFile.Fd(), sparseFile.Fd()); err != nil { + loopFile.Close() + + // If the error is EBUSY, then try the next loopback + if err != syscall.EBUSY { + logrus.Errorf("Cannot set up loopback device %s: %s", target, err) + return nil, ErrAttachLoopbackDevice + } + + // Otherwise, we keep going with the loop + continue + } + // In case of success, we finished. Break the loop. + break + } + + // This can't happen, but let's be sure + if loopFile == nil { + logrus.Errorf("Unreachable code reached! Error attaching %s to a loopback device.", sparseFile.Name()) + return nil, ErrAttachLoopbackDevice + } + + return loopFile, nil +} + +// AttachLoopDevice attaches the given sparse file to the next +// available loopback device. It returns an opened *os.File. +func AttachLoopDevice(sparseName string) (loop *os.File, err error) { + + // Try to retrieve the next available loopback device via syscall. + // If it fails, we discard error and start looping for a + // loopback from index 0. + startIndex, err := getNextFreeLoopbackIndex() + if err != nil { + logrus.Debugf("Error retrieving the next available loopback: %s", err) + } + + // OpenFile adds O_CLOEXEC + sparseFile, err := os.OpenFile(sparseName, os.O_RDWR, 0644) + if err != nil { + logrus.Errorf("Error opening sparse file %s: %s", sparseName, err) + return nil, ErrAttachLoopbackDevice + } + defer sparseFile.Close() + + loopFile, err := openNextAvailableLoopback(startIndex, sparseFile) + if err != nil { + return nil, err + } + + // Set the status of the loopback device + loopInfo := &loopInfo64{ + loFileName: stringToLoopName(loopFile.Name()), + loOffset: 0, + loFlags: LoFlagsAutoClear, + } + + if err := ioctlLoopSetStatus64(loopFile.Fd(), loopInfo); err != nil { + logrus.Errorf("Cannot set up loopback device info: %s", err) + + // If the call failed, then free the loopback device + if err := ioctlLoopClrFd(loopFile.Fd()); err != nil { + logrus.Errorf("Error while cleaning up the loopback device") + } + loopFile.Close() + return nil, ErrAttachLoopbackDevice + } + + return loopFile, nil +} diff --git a/pkg/loopback/ioctl.go b/pkg/loopback/ioctl.go new file mode 100644 index 00000000..0714eb5f --- /dev/null +++ b/pkg/loopback/ioctl.go @@ -0,0 +1,53 @@ +// +build linux + +package loopback + +import ( + "syscall" + "unsafe" +) + +func ioctlLoopCtlGetFree(fd uintptr) (int, error) { + index, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, LoopCtlGetFree, 0) + if err != 0 { + return 0, err + } + return int(index), nil +} + +func ioctlLoopSetFd(loopFd, sparseFd uintptr) error { + if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, loopFd, LoopSetFd, sparseFd); err != 0 { + return err + } + return nil +} + +func ioctlLoopSetStatus64(loopFd uintptr, loopInfo *loopInfo64) error { + if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, loopFd, LoopSetStatus64, uintptr(unsafe.Pointer(loopInfo))); err != 0 { + return err + } + return nil +} + +func ioctlLoopClrFd(loopFd uintptr) error { + if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, loopFd, LoopClrFd, 0); err != 0 { + return err + } + return nil +} + +func ioctlLoopGetStatus64(loopFd uintptr) (*loopInfo64, error) { + loopInfo := &loopInfo64{} + + if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, loopFd, LoopGetStatus64, uintptr(unsafe.Pointer(loopInfo))); err != 0 { + return nil, err + } + return loopInfo, nil +} + +func ioctlLoopSetCapacity(loopFd uintptr, value int) error { + if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, loopFd, LoopSetCapacity, uintptr(value)); err != 0 { + return err + } + return nil +} diff --git a/pkg/loopback/loop_wrapper.go b/pkg/loopback/loop_wrapper.go new file mode 100644 index 00000000..e1100ce1 --- /dev/null +++ b/pkg/loopback/loop_wrapper.go @@ -0,0 +1,52 @@ +// +build linux + +package loopback + +/* +#include // FIXME: present only for defines, maybe we can remove it? + +#ifndef LOOP_CTL_GET_FREE + #define LOOP_CTL_GET_FREE 0x4C82 +#endif + +#ifndef LO_FLAGS_PARTSCAN + #define LO_FLAGS_PARTSCAN 8 +#endif + +*/ +import "C" + +type loopInfo64 struct { + loDevice uint64 /* ioctl r/o */ + loInode uint64 /* ioctl r/o */ + loRdevice uint64 /* ioctl r/o */ + loOffset uint64 + loSizelimit uint64 /* bytes, 0 == max available */ + loNumber uint32 /* ioctl r/o */ + loEncryptType uint32 + loEncryptKeySize uint32 /* ioctl w/o */ + loFlags uint32 /* ioctl r/o */ + loFileName [LoNameSize]uint8 + loCryptName [LoNameSize]uint8 + loEncryptKey [LoKeySize]uint8 /* ioctl w/o */ + loInit [2]uint64 +} + +// IOCTL consts +const ( + LoopSetFd = C.LOOP_SET_FD + LoopCtlGetFree = C.LOOP_CTL_GET_FREE + LoopGetStatus64 = C.LOOP_GET_STATUS64 + LoopSetStatus64 = C.LOOP_SET_STATUS64 + LoopClrFd = C.LOOP_CLR_FD + LoopSetCapacity = C.LOOP_SET_CAPACITY +) + +// LOOP consts. +const ( + LoFlagsAutoClear = C.LO_FLAGS_AUTOCLEAR + LoFlagsReadOnly = C.LO_FLAGS_READ_ONLY + LoFlagsPartScan = C.LO_FLAGS_PARTSCAN + LoKeySize = C.LO_KEY_SIZE + LoNameSize = C.LO_NAME_SIZE +) diff --git a/pkg/loopback/loopback.go b/pkg/loopback/loopback.go new file mode 100644 index 00000000..bc047928 --- /dev/null +++ b/pkg/loopback/loopback.go @@ -0,0 +1,63 @@ +// +build linux + +package loopback + +import ( + "fmt" + "os" + "syscall" + + "github.com/Sirupsen/logrus" +) + +func getLoopbackBackingFile(file *os.File) (uint64, uint64, error) { + loopInfo, err := ioctlLoopGetStatus64(file.Fd()) + if err != nil { + logrus.Errorf("Error get loopback backing file: %s", err) + return 0, 0, ErrGetLoopbackBackingFile + } + return loopInfo.loDevice, loopInfo.loInode, nil +} + +// SetCapacity reloads the size for the loopback device. +func SetCapacity(file *os.File) error { + if err := ioctlLoopSetCapacity(file.Fd(), 0); err != nil { + logrus.Errorf("Error loopbackSetCapacity: %s", err) + return ErrSetCapacity + } + return nil +} + +// FindLoopDeviceFor returns a loopback device file for the specified file which +// is backing file of a loop back device. +func FindLoopDeviceFor(file *os.File) *os.File { + stat, err := file.Stat() + if err != nil { + return nil + } + targetInode := stat.Sys().(*syscall.Stat_t).Ino + targetDevice := stat.Sys().(*syscall.Stat_t).Dev + + for i := 0; true; i++ { + path := fmt.Sprintf("/dev/loop%d", i) + + file, err := os.OpenFile(path, os.O_RDWR, 0) + if err != nil { + if os.IsNotExist(err) { + return nil + } + + // Ignore all errors until the first not-exist + // we want to continue looking for the file + continue + } + + dev, inode, err := getLoopbackBackingFile(file) + if err == nil && dev == targetDevice && inode == targetInode { + return file + } + file.Close() + } + + return nil +} diff --git a/pkg/mflag/LICENSE b/pkg/mflag/LICENSE new file mode 100644 index 00000000..9b4f4a29 --- /dev/null +++ b/pkg/mflag/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2014-2016 The Docker & Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pkg/mflag/README.md b/pkg/mflag/README.md new file mode 100644 index 00000000..da00efa3 --- /dev/null +++ b/pkg/mflag/README.md @@ -0,0 +1,40 @@ +Package mflag (aka multiple-flag) implements command-line flag parsing. +It's an **hacky** fork of the [official golang package](http://golang.org/pkg/flag/) + +It adds: + +* both short and long flag version +`./example -s red` `./example --string blue` + +* multiple names for the same option +``` +$>./example -h +Usage of example: + -s, --string="": a simple string +``` + +___ +It is very flexible on purpose, so you can do things like: +``` +$>./example -h +Usage of example: + -s, -string, --string="": a simple string +``` + +Or: +``` +$>./example -h +Usage of example: + -oldflag, --newflag="": a simple string +``` + +You can also hide some flags from the usage, so if we want only `--newflag`: +``` +$>./example -h +Usage of example: + --newflag="": a simple string +$>./example -oldflag str +str +``` + +See [example.go](example/example.go) for more details. diff --git a/pkg/mflag/example/example.go b/pkg/mflag/example/example.go new file mode 100644 index 00000000..2e766dd1 --- /dev/null +++ b/pkg/mflag/example/example.go @@ -0,0 +1,36 @@ +package main + +import ( + "fmt" + + flag "github.com/docker/docker/pkg/mflag" +) + +var ( + i int + str string + b, b2, h bool +) + +func init() { + flag.Bool([]string{"#hp", "#-halp"}, false, "display the halp") + flag.BoolVar(&b, []string{"b", "#bal", "#bol", "-bal"}, false, "a simple bool") + flag.BoolVar(&b, []string{"g", "#gil"}, false, "a simple bool") + flag.BoolVar(&b2, []string{"#-bool"}, false, "a simple bool") + flag.IntVar(&i, []string{"-integer", "-number"}, -1, "a simple integer") + flag.StringVar(&str, []string{"s", "#hidden", "-string"}, "", "a simple string") //-s -hidden and --string will work, but -hidden won't be in the usage + flag.BoolVar(&h, []string{"h", "#help", "-help"}, false, "display the help") + flag.StringVar(&str, []string{"mode"}, "mode1", "set the mode\nmode1: use the mode1\nmode2: use the mode2\nmode3: use the mode3") + flag.Parse() +} +func main() { + if h { + flag.PrintDefaults() + } else { + fmt.Printf("s/#hidden/-string: %s\n", str) + fmt.Printf("b: %t\n", b) + fmt.Printf("-bool: %t\n", b2) + fmt.Printf("s/#hidden/-string(via lookup): %s\n", flag.Lookup("s").Value.String()) + fmt.Printf("ARGS: %v\n", flag.Args()) + } +} diff --git a/pkg/mflag/flag.go b/pkg/mflag/flag.go new file mode 100644 index 00000000..e2a0c422 --- /dev/null +++ b/pkg/mflag/flag.go @@ -0,0 +1,1280 @@ +// Copyright 2014-2016 The Docker & Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package mflag implements command-line flag parsing. +// +// Usage: +// +// Define flags using flag.String(), Bool(), Int(), etc. +// +// This declares an integer flag, -f or --flagname, stored in the pointer ip, with type *int. +// import "flag /github.com/docker/docker/pkg/mflag" +// var ip = flag.Int([]string{"f", "-flagname"}, 1234, "help message for flagname") +// If you like, you can bind the flag to a variable using the Var() functions. +// var flagvar int +// func init() { +// // -flaghidden will work, but will be hidden from the usage +// flag.IntVar(&flagvar, []string{"f", "#flaghidden", "-flagname"}, 1234, "help message for flagname") +// } +// Or you can create custom flags that satisfy the Value interface (with +// pointer receivers) and couple them to flag parsing by +// flag.Var(&flagVal, []string{"name"}, "help message for flagname") +// For such flags, the default value is just the initial value of the variable. +// +// You can also add "deprecated" flags, they are still usable, but are not shown +// in the usage and will display a warning when you try to use them. `#` before +// an option means this option is deprecated, if there is an following option +// without `#` ahead, then that's the replacement, if not, it will just be removed: +// var ip = flag.Int([]string{"#f", "#flagname", "-flagname"}, 1234, "help message for flagname") +// this will display: `Warning: '-f' is deprecated, it will be replaced by '--flagname' soon. See usage.` or +// this will display: `Warning: '-flagname' is deprecated, it will be replaced by '--flagname' soon. See usage.` +// var ip = flag.Int([]string{"f", "#flagname"}, 1234, "help message for flagname") +// will display: `Warning: '-flagname' is deprecated, it will be removed soon. See usage.` +// so you can only use `-f`. +// +// You can also group one letter flags, bif you declare +// var v = flag.Bool([]string{"v", "-verbose"}, false, "help message for verbose") +// var s = flag.Bool([]string{"s", "-slow"}, false, "help message for slow") +// you will be able to use the -vs or -sv +// +// After all flags are defined, call +// flag.Parse() +// to parse the command line into the defined flags. +// +// Flags may then be used directly. If you're using the flags themselves, +// they are all pointers; if you bind to variables, they're values. +// fmt.Println("ip has value ", *ip) +// fmt.Println("flagvar has value ", flagvar) +// +// After parsing, the arguments after the flag are available as the +// slice flag.Args() or individually as flag.Arg(i). +// The arguments are indexed from 0 through flag.NArg()-1. +// +// Command line flag syntax: +// -flag +// -flag=x +// -flag="x" +// -flag='x' +// -flag x // non-boolean flags only +// One or two minus signs may be used; they are equivalent. +// The last form is not permitted for boolean flags because the +// meaning of the command +// cmd -x * +// will change if there is a file called 0, false, etc. You must +// use the -flag=false form to turn off a boolean flag. +// +// Flag parsing stops just before the first non-flag argument +// ("-" is a non-flag argument) or after the terminator "--". +// +// Integer flags accept 1234, 0664, 0x1234 and may be negative. +// Boolean flags may be 1, 0, t, f, true, false, TRUE, FALSE, True, False. +// Duration flags accept any input valid for time.ParseDuration. +// +// The default set of command-line flags is controlled by +// top-level functions. The FlagSet type allows one to define +// independent sets of flags, such as to implement subcommands +// in a command-line interface. The methods of FlagSet are +// analogous to the top-level functions for the command-line +// flag set. + +package mflag + +import ( + "errors" + "fmt" + "io" + "os" + "runtime" + "sort" + "strconv" + "strings" + "text/tabwriter" + "time" + + "github.com/docker/docker/pkg/homedir" +) + +// ErrHelp is the error returned if the flag -help is invoked but no such flag is defined. +var ErrHelp = errors.New("flag: help requested") + +// ErrRetry is the error returned if you need to try letter by letter +var ErrRetry = errors.New("flag: retry") + +// -- bool Value +type boolValue bool + +func newBoolValue(val bool, p *bool) *boolValue { + *p = val + return (*boolValue)(p) +} + +func (b *boolValue) Set(s string) error { + v, err := strconv.ParseBool(s) + *b = boolValue(v) + return err +} + +func (b *boolValue) Get() interface{} { return bool(*b) } + +func (b *boolValue) String() string { return fmt.Sprintf("%v", *b) } + +func (b *boolValue) IsBoolFlag() bool { return true } + +// optional interface to indicate boolean flags that can be +// supplied without "=value" text +type boolFlag interface { + Value + IsBoolFlag() bool +} + +// -- int Value +type intValue int + +func newIntValue(val int, p *int) *intValue { + *p = val + return (*intValue)(p) +} + +func (i *intValue) Set(s string) error { + v, err := strconv.ParseInt(s, 0, 64) + *i = intValue(v) + return err +} + +func (i *intValue) Get() interface{} { return int(*i) } + +func (i *intValue) String() string { return fmt.Sprintf("%v", *i) } + +// -- int64 Value +type int64Value int64 + +func newInt64Value(val int64, p *int64) *int64Value { + *p = val + return (*int64Value)(p) +} + +func (i *int64Value) Set(s string) error { + v, err := strconv.ParseInt(s, 0, 64) + *i = int64Value(v) + return err +} + +func (i *int64Value) Get() interface{} { return int64(*i) } + +func (i *int64Value) String() string { return fmt.Sprintf("%v", *i) } + +// -- uint Value +type uintValue uint + +func newUintValue(val uint, p *uint) *uintValue { + *p = val + return (*uintValue)(p) +} + +func (i *uintValue) Set(s string) error { + v, err := strconv.ParseUint(s, 0, 64) + *i = uintValue(v) + return err +} + +func (i *uintValue) Get() interface{} { return uint(*i) } + +func (i *uintValue) String() string { return fmt.Sprintf("%v", *i) } + +// -- uint64 Value +type uint64Value uint64 + +func newUint64Value(val uint64, p *uint64) *uint64Value { + *p = val + return (*uint64Value)(p) +} + +func (i *uint64Value) Set(s string) error { + v, err := strconv.ParseUint(s, 0, 64) + *i = uint64Value(v) + return err +} + +func (i *uint64Value) Get() interface{} { return uint64(*i) } + +func (i *uint64Value) String() string { return fmt.Sprintf("%v", *i) } + +// -- uint16 Value +type uint16Value uint16 + +func newUint16Value(val uint16, p *uint16) *uint16Value { + *p = val + return (*uint16Value)(p) +} + +func (i *uint16Value) Set(s string) error { + v, err := strconv.ParseUint(s, 0, 16) + *i = uint16Value(v) + return err +} + +func (i *uint16Value) Get() interface{} { return uint16(*i) } + +func (i *uint16Value) String() string { return fmt.Sprintf("%v", *i) } + +// -- string Value +type stringValue string + +func newStringValue(val string, p *string) *stringValue { + *p = val + return (*stringValue)(p) +} + +func (s *stringValue) Set(val string) error { + *s = stringValue(val) + return nil +} + +func (s *stringValue) Get() interface{} { return string(*s) } + +func (s *stringValue) String() string { return fmt.Sprintf("%s", *s) } + +// -- float64 Value +type float64Value float64 + +func newFloat64Value(val float64, p *float64) *float64Value { + *p = val + return (*float64Value)(p) +} + +func (f *float64Value) Set(s string) error { + v, err := strconv.ParseFloat(s, 64) + *f = float64Value(v) + return err +} + +func (f *float64Value) Get() interface{} { return float64(*f) } + +func (f *float64Value) String() string { return fmt.Sprintf("%v", *f) } + +// -- time.Duration Value +type durationValue time.Duration + +func newDurationValue(val time.Duration, p *time.Duration) *durationValue { + *p = val + return (*durationValue)(p) +} + +func (d *durationValue) Set(s string) error { + v, err := time.ParseDuration(s) + *d = durationValue(v) + return err +} + +func (d *durationValue) Get() interface{} { return time.Duration(*d) } + +func (d *durationValue) String() string { return (*time.Duration)(d).String() } + +// Value is the interface to the dynamic value stored in a flag. +// (The default value is represented as a string.) +// +// If a Value has an IsBoolFlag() bool method returning true, +// the command-line parser makes -name equivalent to -name=true +// rather than using the next command-line argument. +type Value interface { + String() string + Set(string) error +} + +// Getter is an interface that allows the contents of a Value to be retrieved. +// It wraps the Value interface, rather than being part of it, because it +// appeared after Go 1 and its compatibility rules. All Value types provided +// by this package satisfy the Getter interface. +type Getter interface { + Value + Get() interface{} +} + +// ErrorHandling defines how to handle flag parsing errors. +type ErrorHandling int + +// ErrorHandling strategies available when a flag parsing error occurs +const ( + ContinueOnError ErrorHandling = iota + ExitOnError + PanicOnError +) + +// A FlagSet represents a set of defined flags. The zero value of a FlagSet +// has no name and has ContinueOnError error handling. +type FlagSet struct { + // Usage is the function called when an error occurs while parsing flags. + // The field is a function (not a method) that may be changed to point to + // a custom error handler. + Usage func() + ShortUsage func() + + name string + parsed bool + actual map[string]*Flag + formal map[string]*Flag + args []string // arguments after flags + errorHandling ErrorHandling + output io.Writer // nil means stderr; use Out() accessor + nArgRequirements []nArgRequirement +} + +// A Flag represents the state of a flag. +type Flag struct { + Names []string // name as it appears on command line + Usage string // help message + Value Value // value as set + DefValue string // default value (as text); for usage message +} + +type flagSlice []string + +func (p flagSlice) Len() int { return len(p) } +func (p flagSlice) Less(i, j int) bool { + pi, pj := strings.TrimPrefix(p[i], "-"), strings.TrimPrefix(p[j], "-") + lpi, lpj := strings.ToLower(pi), strings.ToLower(pj) + if lpi != lpj { + return lpi < lpj + } + return pi < pj +} +func (p flagSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } + +// sortFlags returns the flags as a slice in lexicographical sorted order. +func sortFlags(flags map[string]*Flag) []*Flag { + var list flagSlice + + // The sorted list is based on the first name, when flag map might use the other names. + nameMap := make(map[string]string) + + for n, f := range flags { + fName := strings.TrimPrefix(f.Names[0], "#") + nameMap[fName] = n + if len(f.Names) == 1 { + list = append(list, fName) + continue + } + + found := false + for _, name := range list { + if name == fName { + found = true + break + } + } + if !found { + list = append(list, fName) + } + } + sort.Sort(list) + result := make([]*Flag, len(list)) + for i, name := range list { + result[i] = flags[nameMap[name]] + } + return result +} + +// Name returns the name of the FlagSet. +func (fs *FlagSet) Name() string { + return fs.name +} + +// Out returns the destination for usage and error messages. +func (fs *FlagSet) Out() io.Writer { + if fs.output == nil { + return os.Stderr + } + return fs.output +} + +// SetOutput sets the destination for usage and error messages. +// If output is nil, os.Stderr is used. +func (fs *FlagSet) SetOutput(output io.Writer) { + fs.output = output +} + +// VisitAll visits the flags in lexicographical order, calling fn for each. +// It visits all flags, even those not set. +func (fs *FlagSet) VisitAll(fn func(*Flag)) { + for _, flag := range sortFlags(fs.formal) { + fn(flag) + } +} + +// VisitAll visits the command-line flags in lexicographical order, calling +// fn for each. It visits all flags, even those not set. +func VisitAll(fn func(*Flag)) { + CommandLine.VisitAll(fn) +} + +// Visit visits the flags in lexicographical order, calling fn for each. +// It visits only those flags that have been set. +func (fs *FlagSet) Visit(fn func(*Flag)) { + for _, flag := range sortFlags(fs.actual) { + fn(flag) + } +} + +// Visit visits the command-line flags in lexicographical order, calling fn +// for each. It visits only those flags that have been set. +func Visit(fn func(*Flag)) { + CommandLine.Visit(fn) +} + +// Lookup returns the Flag structure of the named flag, returning nil if none exists. +func (fs *FlagSet) Lookup(name string) *Flag { + return fs.formal[name] +} + +// IsSet indicates whether the specified flag is set in the given FlagSet +func (fs *FlagSet) IsSet(name string) bool { + return fs.actual[name] != nil +} + +// Lookup returns the Flag structure of the named command-line flag, +// returning nil if none exists. +func Lookup(name string) *Flag { + return CommandLine.formal[name] +} + +// IsSet indicates whether the specified flag was specified at all on the cmd line. +func IsSet(name string) bool { + return CommandLine.IsSet(name) +} + +type nArgRequirementType int + +// Indicator used to pass to BadArgs function +const ( + Exact nArgRequirementType = iota + Max + Min +) + +type nArgRequirement struct { + Type nArgRequirementType + N int +} + +// Require adds a requirement about the number of arguments for the FlagSet. +// The first parameter can be Exact, Max, or Min to respectively specify the exact, +// the maximum, or the minimal number of arguments required. +// The actual check is done in FlagSet.CheckArgs(). +func (fs *FlagSet) Require(nArgRequirementType nArgRequirementType, nArg int) { + fs.nArgRequirements = append(fs.nArgRequirements, nArgRequirement{nArgRequirementType, nArg}) +} + +// CheckArgs uses the requirements set by FlagSet.Require() to validate +// the number of arguments. If the requirements are not met, +// an error message string is returned. +func (fs *FlagSet) CheckArgs() (message string) { + for _, req := range fs.nArgRequirements { + var arguments string + if req.N == 1 { + arguments = "1 argument" + } else { + arguments = fmt.Sprintf("%d arguments", req.N) + } + + str := func(kind string) string { + return fmt.Sprintf("%q requires %s%s", fs.name, kind, arguments) + } + + switch req.Type { + case Exact: + if fs.NArg() != req.N { + return str("") + } + case Max: + if fs.NArg() > req.N { + return str("a maximum of ") + } + case Min: + if fs.NArg() < req.N { + return str("a minimum of ") + } + } + } + return "" +} + +// Set sets the value of the named flag. +func (fs *FlagSet) Set(name, value string) error { + flag, ok := fs.formal[name] + if !ok { + return fmt.Errorf("no such flag -%v", name) + } + if err := flag.Value.Set(value); err != nil { + return err + } + if fs.actual == nil { + fs.actual = make(map[string]*Flag) + } + fs.actual[name] = flag + return nil +} + +// Set sets the value of the named command-line flag. +func Set(name, value string) error { + return CommandLine.Set(name, value) +} + +// isZeroValue guesses whether the string represents the zero +// value for a flag. It is not accurate but in practice works OK. +func isZeroValue(value string) bool { + switch value { + case "false": + return true + case "": + return true + case "0": + return true + } + return false +} + +// PrintDefaults prints, to standard error unless configured +// otherwise, the default values of all defined flags in the set. +func (fs *FlagSet) PrintDefaults() { + writer := tabwriter.NewWriter(fs.Out(), 20, 1, 3, ' ', 0) + home := homedir.Get() + + // Don't substitute when HOME is / + if runtime.GOOS != "windows" && home == "/" { + home = "" + } + + // Add a blank line between cmd description and list of options + if fs.FlagCount() > 0 { + fmt.Fprintln(writer, "") + } + + fs.VisitAll(func(flag *Flag) { + names := []string{} + for _, name := range flag.Names { + if name[0] != '#' { + names = append(names, name) + } + } + if len(names) > 0 && len(flag.Usage) > 0 { + val := flag.DefValue + + if home != "" && strings.HasPrefix(val, home) { + val = homedir.GetShortcutString() + val[len(home):] + } + + if isZeroValue(val) { + format := " -%s" + fmt.Fprintf(writer, format, strings.Join(names, ", -")) + } else { + format := " -%s=%s" + fmt.Fprintf(writer, format, strings.Join(names, ", -"), val) + } + for _, line := range strings.Split(flag.Usage, "\n") { + fmt.Fprintln(writer, "\t", line) + } + } + }) + writer.Flush() +} + +// PrintDefaults prints to standard error the default values of all defined command-line flags. +func PrintDefaults() { + CommandLine.PrintDefaults() +} + +// defaultUsage is the default function to print a usage message. +func defaultUsage(fs *FlagSet) { + if fs.name == "" { + fmt.Fprintf(fs.Out(), "Usage:\n") + } else { + fmt.Fprintf(fs.Out(), "Usage of %s:\n", fs.name) + } + fs.PrintDefaults() +} + +// NOTE: Usage is not just defaultUsage(CommandLine) +// because it serves (via godoc flag Usage) as the example +// for how to write your own usage function. + +// Usage prints to standard error a usage message documenting all defined command-line flags. +// The function is a variable that may be changed to point to a custom function. +var Usage = func() { + fmt.Fprintf(CommandLine.Out(), "Usage of %s:\n", os.Args[0]) + PrintDefaults() +} + +// ShortUsage prints to standard error a usage message documenting the standard command layout +// The function is a variable that may be changed to point to a custom function. +var ShortUsage = func() { + fmt.Fprintf(CommandLine.output, "Usage of %s:\n", os.Args[0]) +} + +// FlagCount returns the number of flags that have been defined. +func (fs *FlagSet) FlagCount() int { return len(sortFlags(fs.formal)) } + +// FlagCountUndeprecated returns the number of undeprecated flags that have been defined. +func (fs *FlagSet) FlagCountUndeprecated() int { + count := 0 + for _, flag := range sortFlags(fs.formal) { + for _, name := range flag.Names { + if name[0] != '#' { + count++ + break + } + } + } + return count +} + +// NFlag returns the number of flags that have been set. +func (fs *FlagSet) NFlag() int { return len(fs.actual) } + +// NFlag returns the number of command-line flags that have been set. +func NFlag() int { return len(CommandLine.actual) } + +// Arg returns the i'th argument. Arg(0) is the first remaining argument +// after flags have been processed. +func (fs *FlagSet) Arg(i int) string { + if i < 0 || i >= len(fs.args) { + return "" + } + return fs.args[i] +} + +// Arg returns the i'th command-line argument. Arg(0) is the first remaining argument +// after flags have been processed. +func Arg(i int) string { + return CommandLine.Arg(i) +} + +// NArg is the number of arguments remaining after flags have been processed. +func (fs *FlagSet) NArg() int { return len(fs.args) } + +// NArg is the number of arguments remaining after flags have been processed. +func NArg() int { return len(CommandLine.args) } + +// Args returns the non-flag arguments. +func (fs *FlagSet) Args() []string { return fs.args } + +// Args returns the non-flag command-line arguments. +func Args() []string { return CommandLine.args } + +// BoolVar defines a bool flag with specified name, default value, and usage string. +// The argument p points to a bool variable in which to store the value of the flag. +func (fs *FlagSet) BoolVar(p *bool, names []string, value bool, usage string) { + fs.Var(newBoolValue(value, p), names, usage) +} + +// BoolVar defines a bool flag with specified name, default value, and usage string. +// The argument p points to a bool variable in which to store the value of the flag. +func BoolVar(p *bool, names []string, value bool, usage string) { + CommandLine.Var(newBoolValue(value, p), names, usage) +} + +// Bool defines a bool flag with specified name, default value, and usage string. +// The return value is the address of a bool variable that stores the value of the flag. +func (fs *FlagSet) Bool(names []string, value bool, usage string) *bool { + p := new(bool) + fs.BoolVar(p, names, value, usage) + return p +} + +// Bool defines a bool flag with specified name, default value, and usage string. +// The return value is the address of a bool variable that stores the value of the flag. +func Bool(names []string, value bool, usage string) *bool { + return CommandLine.Bool(names, value, usage) +} + +// IntVar defines an int flag with specified name, default value, and usage string. +// The argument p points to an int variable in which to store the value of the flag. +func (fs *FlagSet) IntVar(p *int, names []string, value int, usage string) { + fs.Var(newIntValue(value, p), names, usage) +} + +// IntVar defines an int flag with specified name, default value, and usage string. +// The argument p points to an int variable in which to store the value of the flag. +func IntVar(p *int, names []string, value int, usage string) { + CommandLine.Var(newIntValue(value, p), names, usage) +} + +// Int defines an int flag with specified name, default value, and usage string. +// The return value is the address of an int variable that stores the value of the flag. +func (fs *FlagSet) Int(names []string, value int, usage string) *int { + p := new(int) + fs.IntVar(p, names, value, usage) + return p +} + +// Int defines an int flag with specified name, default value, and usage string. +// The return value is the address of an int variable that stores the value of the flag. +func Int(names []string, value int, usage string) *int { + return CommandLine.Int(names, value, usage) +} + +// Int64Var defines an int64 flag with specified name, default value, and usage string. +// The argument p points to an int64 variable in which to store the value of the flag. +func (fs *FlagSet) Int64Var(p *int64, names []string, value int64, usage string) { + fs.Var(newInt64Value(value, p), names, usage) +} + +// Int64Var defines an int64 flag with specified name, default value, and usage string. +// The argument p points to an int64 variable in which to store the value of the flag. +func Int64Var(p *int64, names []string, value int64, usage string) { + CommandLine.Var(newInt64Value(value, p), names, usage) +} + +// Int64 defines an int64 flag with specified name, default value, and usage string. +// The return value is the address of an int64 variable that stores the value of the flag. +func (fs *FlagSet) Int64(names []string, value int64, usage string) *int64 { + p := new(int64) + fs.Int64Var(p, names, value, usage) + return p +} + +// Int64 defines an int64 flag with specified name, default value, and usage string. +// The return value is the address of an int64 variable that stores the value of the flag. +func Int64(names []string, value int64, usage string) *int64 { + return CommandLine.Int64(names, value, usage) +} + +// UintVar defines a uint flag with specified name, default value, and usage string. +// The argument p points to a uint variable in which to store the value of the flag. +func (fs *FlagSet) UintVar(p *uint, names []string, value uint, usage string) { + fs.Var(newUintValue(value, p), names, usage) +} + +// UintVar defines a uint flag with specified name, default value, and usage string. +// The argument p points to a uint variable in which to store the value of the flag. +func UintVar(p *uint, names []string, value uint, usage string) { + CommandLine.Var(newUintValue(value, p), names, usage) +} + +// Uint defines a uint flag with specified name, default value, and usage string. +// The return value is the address of a uint variable that stores the value of the flag. +func (fs *FlagSet) Uint(names []string, value uint, usage string) *uint { + p := new(uint) + fs.UintVar(p, names, value, usage) + return p +} + +// Uint defines a uint flag with specified name, default value, and usage string. +// The return value is the address of a uint variable that stores the value of the flag. +func Uint(names []string, value uint, usage string) *uint { + return CommandLine.Uint(names, value, usage) +} + +// Uint64Var defines a uint64 flag with specified name, default value, and usage string. +// The argument p points to a uint64 variable in which to store the value of the flag. +func (fs *FlagSet) Uint64Var(p *uint64, names []string, value uint64, usage string) { + fs.Var(newUint64Value(value, p), names, usage) +} + +// Uint64Var defines a uint64 flag with specified name, default value, and usage string. +// The argument p points to a uint64 variable in which to store the value of the flag. +func Uint64Var(p *uint64, names []string, value uint64, usage string) { + CommandLine.Var(newUint64Value(value, p), names, usage) +} + +// Uint64 defines a uint64 flag with specified name, default value, and usage string. +// The return value is the address of a uint64 variable that stores the value of the flag. +func (fs *FlagSet) Uint64(names []string, value uint64, usage string) *uint64 { + p := new(uint64) + fs.Uint64Var(p, names, value, usage) + return p +} + +// Uint64 defines a uint64 flag with specified name, default value, and usage string. +// The return value is the address of a uint64 variable that stores the value of the flag. +func Uint64(names []string, value uint64, usage string) *uint64 { + return CommandLine.Uint64(names, value, usage) +} + +// Uint16Var defines a uint16 flag with specified name, default value, and usage string. +// The argument p points to a uint16 variable in which to store the value of the flag. +func (fs *FlagSet) Uint16Var(p *uint16, names []string, value uint16, usage string) { + fs.Var(newUint16Value(value, p), names, usage) +} + +// Uint16Var defines a uint16 flag with specified name, default value, and usage string. +// The argument p points to a uint16 variable in which to store the value of the flag. +func Uint16Var(p *uint16, names []string, value uint16, usage string) { + CommandLine.Var(newUint16Value(value, p), names, usage) +} + +// Uint16 defines a uint16 flag with specified name, default value, and usage string. +// The return value is the address of a uint16 variable that stores the value of the flag. +func (fs *FlagSet) Uint16(names []string, value uint16, usage string) *uint16 { + p := new(uint16) + fs.Uint16Var(p, names, value, usage) + return p +} + +// Uint16 defines a uint16 flag with specified name, default value, and usage string. +// The return value is the address of a uint16 variable that stores the value of the flag. +func Uint16(names []string, value uint16, usage string) *uint16 { + return CommandLine.Uint16(names, value, usage) +} + +// StringVar defines a string flag with specified name, default value, and usage string. +// The argument p points to a string variable in which to store the value of the flag. +func (fs *FlagSet) StringVar(p *string, names []string, value string, usage string) { + fs.Var(newStringValue(value, p), names, usage) +} + +// StringVar defines a string flag with specified name, default value, and usage string. +// The argument p points to a string variable in which to store the value of the flag. +func StringVar(p *string, names []string, value string, usage string) { + CommandLine.Var(newStringValue(value, p), names, usage) +} + +// String defines a string flag with specified name, default value, and usage string. +// The return value is the address of a string variable that stores the value of the flag. +func (fs *FlagSet) String(names []string, value string, usage string) *string { + p := new(string) + fs.StringVar(p, names, value, usage) + return p +} + +// String defines a string flag with specified name, default value, and usage string. +// The return value is the address of a string variable that stores the value of the flag. +func String(names []string, value string, usage string) *string { + return CommandLine.String(names, value, usage) +} + +// Float64Var defines a float64 flag with specified name, default value, and usage string. +// The argument p points to a float64 variable in which to store the value of the flag. +func (fs *FlagSet) Float64Var(p *float64, names []string, value float64, usage string) { + fs.Var(newFloat64Value(value, p), names, usage) +} + +// Float64Var defines a float64 flag with specified name, default value, and usage string. +// The argument p points to a float64 variable in which to store the value of the flag. +func Float64Var(p *float64, names []string, value float64, usage string) { + CommandLine.Var(newFloat64Value(value, p), names, usage) +} + +// Float64 defines a float64 flag with specified name, default value, and usage string. +// The return value is the address of a float64 variable that stores the value of the flag. +func (fs *FlagSet) Float64(names []string, value float64, usage string) *float64 { + p := new(float64) + fs.Float64Var(p, names, value, usage) + return p +} + +// Float64 defines a float64 flag with specified name, default value, and usage string. +// The return value is the address of a float64 variable that stores the value of the flag. +func Float64(names []string, value float64, usage string) *float64 { + return CommandLine.Float64(names, value, usage) +} + +// DurationVar defines a time.Duration flag with specified name, default value, and usage string. +// The argument p points to a time.Duration variable in which to store the value of the flag. +func (fs *FlagSet) DurationVar(p *time.Duration, names []string, value time.Duration, usage string) { + fs.Var(newDurationValue(value, p), names, usage) +} + +// DurationVar defines a time.Duration flag with specified name, default value, and usage string. +// The argument p points to a time.Duration variable in which to store the value of the flag. +func DurationVar(p *time.Duration, names []string, value time.Duration, usage string) { + CommandLine.Var(newDurationValue(value, p), names, usage) +} + +// Duration defines a time.Duration flag with specified name, default value, and usage string. +// The return value is the address of a time.Duration variable that stores the value of the flag. +func (fs *FlagSet) Duration(names []string, value time.Duration, usage string) *time.Duration { + p := new(time.Duration) + fs.DurationVar(p, names, value, usage) + return p +} + +// Duration defines a time.Duration flag with specified name, default value, and usage string. +// The return value is the address of a time.Duration variable that stores the value of the flag. +func Duration(names []string, value time.Duration, usage string) *time.Duration { + return CommandLine.Duration(names, value, usage) +} + +// Var defines a flag with the specified name and usage string. The type and +// value of the flag are represented by the first argument, of type Value, which +// typically holds a user-defined implementation of Value. For instance, the +// caller could create a flag that turns a comma-separated string into a slice +// of strings by giving the slice the methods of Value; in particular, Set would +// decompose the comma-separated string into the slice. +func (fs *FlagSet) Var(value Value, names []string, usage string) { + // Remember the default value as a string; it won't change. + flag := &Flag{names, usage, value, value.String()} + for _, name := range names { + name = strings.TrimPrefix(name, "#") + _, alreadythere := fs.formal[name] + if alreadythere { + var msg string + if fs.name == "" { + msg = fmt.Sprintf("flag redefined: %s", name) + } else { + msg = fmt.Sprintf("%s flag redefined: %s", fs.name, name) + } + fmt.Fprintln(fs.Out(), msg) + panic(msg) // Happens only if flags are declared with identical names + } + if fs.formal == nil { + fs.formal = make(map[string]*Flag) + } + fs.formal[name] = flag + } +} + +// Var defines a flag with the specified name and usage string. The type and +// value of the flag are represented by the first argument, of type Value, which +// typically holds a user-defined implementation of Value. For instance, the +// caller could create a flag that turns a comma-separated string into a slice +// of strings by giving the slice the methods of Value; in particular, Set would +// decompose the comma-separated string into the slice. +func Var(value Value, names []string, usage string) { + CommandLine.Var(value, names, usage) +} + +// failf prints to standard error a formatted error and usage message and +// returns the error. +func (fs *FlagSet) failf(format string, a ...interface{}) error { + err := fmt.Errorf(format, a...) + fmt.Fprintln(fs.Out(), err) + if os.Args[0] == fs.name { + fmt.Fprintf(fs.Out(), "See '%s --help'.\n", os.Args[0]) + } else { + fmt.Fprintf(fs.Out(), "See '%s %s --help'.\n", os.Args[0], fs.name) + } + return err +} + +// usage calls the Usage method for the flag set, or the usage function if +// the flag set is CommandLine. +func (fs *FlagSet) usage() { + if fs == CommandLine { + Usage() + } else if fs.Usage == nil { + defaultUsage(fs) + } else { + fs.Usage() + } +} + +func trimQuotes(str string) string { + if len(str) == 0 { + return str + } + type quote struct { + start, end byte + } + + // All valid quote types. + quotes := []quote{ + // Double quotes + { + start: '"', + end: '"', + }, + + // Single quotes + { + start: '\'', + end: '\'', + }, + } + + for _, quote := range quotes { + // Only strip if outermost match. + if str[0] == quote.start && str[len(str)-1] == quote.end { + str = str[1 : len(str)-1] + break + } + } + + return str +} + +// parseOne parses one flag. It reports whether a flag was seen. +func (fs *FlagSet) parseOne() (bool, string, error) { + if len(fs.args) == 0 { + return false, "", nil + } + s := fs.args[0] + if len(s) == 0 || s[0] != '-' || len(s) == 1 { + return false, "", nil + } + if s[1] == '-' && len(s) == 2 { // "--" terminates the flags + fs.args = fs.args[1:] + return false, "", nil + } + name := s[1:] + if len(name) == 0 || name[0] == '=' { + return false, "", fs.failf("bad flag syntax: %s", s) + } + + // it's a flag. does it have an argument? + fs.args = fs.args[1:] + hasValue := false + value := "" + if i := strings.Index(name, "="); i != -1 { + value = trimQuotes(name[i+1:]) + hasValue = true + name = name[:i] + } + + m := fs.formal + flag, alreadythere := m[name] // BUG + if !alreadythere { + if name == "-help" || name == "help" || name == "h" { // special case for nice help message. + fs.usage() + return false, "", ErrHelp + } + if len(name) > 0 && name[0] == '-' { + return false, "", fs.failf("flag provided but not defined: -%s", name) + } + return false, name, ErrRetry + } + if fv, ok := flag.Value.(boolFlag); ok && fv.IsBoolFlag() { // special case: doesn't need an arg + if hasValue { + if err := fv.Set(value); err != nil { + return false, "", fs.failf("invalid boolean value %q for -%s: %v", value, name, err) + } + } else { + fv.Set("true") + } + } else { + // It must have a value, which might be the next argument. + if !hasValue && len(fs.args) > 0 { + // value is the next arg + hasValue = true + value, fs.args = fs.args[0], fs.args[1:] + } + if !hasValue { + return false, "", fs.failf("flag needs an argument: -%s", name) + } + if err := flag.Value.Set(value); err != nil { + return false, "", fs.failf("invalid value %q for flag -%s: %v", value, name, err) + } + } + if fs.actual == nil { + fs.actual = make(map[string]*Flag) + } + fs.actual[name] = flag + for i, n := range flag.Names { + if n == fmt.Sprintf("#%s", name) { + replacement := "" + for j := i; j < len(flag.Names); j++ { + if flag.Names[j][0] != '#' { + replacement = flag.Names[j] + break + } + } + if replacement != "" { + fmt.Fprintf(fs.Out(), "Warning: '-%s' is deprecated, it will be replaced by '-%s' soon. See usage.\n", name, replacement) + } else { + fmt.Fprintf(fs.Out(), "Warning: '-%s' is deprecated, it will be removed soon. See usage.\n", name) + } + } + } + return true, "", nil +} + +// Parse parses flag definitions from the argument list, which should not +// include the command name. Must be called after all flags in the FlagSet +// are defined and before flags are accessed by the program. +// The return value will be ErrHelp if -help was set but not defined. +func (fs *FlagSet) Parse(arguments []string) error { + fs.parsed = true + fs.args = arguments + for { + seen, name, err := fs.parseOne() + if seen { + continue + } + if err == nil { + break + } + if err == ErrRetry { + if len(name) > 1 { + err = nil + for _, letter := range strings.Split(name, "") { + fs.args = append([]string{"-" + letter}, fs.args...) + seen2, _, err2 := fs.parseOne() + if seen2 { + continue + } + if err2 != nil { + err = fs.failf("flag provided but not defined: -%s", name) + break + } + } + if err == nil { + continue + } + } else { + err = fs.failf("flag provided but not defined: -%s", name) + } + } + switch fs.errorHandling { + case ContinueOnError: + return err + case ExitOnError: + os.Exit(125) + case PanicOnError: + panic(err) + } + } + return nil +} + +// ParseFlags is a utility function that adds a help flag if withHelp is true, +// calls fs.Parse(args) and prints a relevant error message if there are +// incorrect number of arguments. It returns error only if error handling is +// set to ContinueOnError and parsing fails. If error handling is set to +// ExitOnError, it's safe to ignore the return value. +func (fs *FlagSet) ParseFlags(args []string, withHelp bool) error { + var help *bool + if withHelp { + help = fs.Bool([]string{"#help", "-help"}, false, "Print usage") + } + if err := fs.Parse(args); err != nil { + return err + } + if help != nil && *help { + fs.SetOutput(os.Stdout) + fs.Usage() + os.Exit(0) + } + if str := fs.CheckArgs(); str != "" { + fs.SetOutput(os.Stderr) + fs.ReportError(str, withHelp) + fs.ShortUsage() + os.Exit(1) + } + return 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 (fs *FlagSet) ReportError(str string, withHelp bool) { + if withHelp { + if os.Args[0] == fs.Name() { + str += ".\nSee '" + os.Args[0] + " --help'" + } else { + str += ".\nSee '" + os.Args[0] + " " + fs.Name() + " --help'" + } + } + fmt.Fprintf(fs.Out(), "%s: %s.\n", os.Args[0], str) +} + +// Parsed reports whether fs.Parse has been called. +func (fs *FlagSet) Parsed() bool { + return fs.parsed +} + +// Parse parses the command-line flags from os.Args[1:]. Must be called +// after all flags are defined and before flags are accessed by the program. +func Parse() { + // Ignore errors; CommandLine is set for ExitOnError. + CommandLine.Parse(os.Args[1:]) +} + +// Parsed returns true if the command-line flags have been parsed. +func Parsed() bool { + return CommandLine.Parsed() +} + +// CommandLine is the default set of command-line flags, parsed from os.Args. +// The top-level functions such as BoolVar, Arg, and on are wrappers for the +// methods of CommandLine. +var CommandLine = NewFlagSet(os.Args[0], ExitOnError) + +// NewFlagSet returns a new, empty flag set with the specified name and +// error handling property. +func NewFlagSet(name string, errorHandling ErrorHandling) *FlagSet { + f := &FlagSet{ + name: name, + errorHandling: errorHandling, + } + return f +} + +// Init sets the name and error handling property for a flag set. +// By default, the zero FlagSet uses an empty name and the +// ContinueOnError error handling policy. +func (fs *FlagSet) Init(name string, errorHandling ErrorHandling) { + fs.name = name + fs.errorHandling = errorHandling +} + +type mergeVal struct { + Value + key string + fset *FlagSet +} + +func (v mergeVal) Set(s string) error { + return v.fset.Set(v.key, s) +} + +func (v mergeVal) IsBoolFlag() bool { + if b, ok := v.Value.(boolFlag); ok { + return b.IsBoolFlag() + } + return false +} + +// Name returns the name of a mergeVal. +// If the original value had a name, return the original name, +// otherwise, return the key asinged to this mergeVal. +func (v mergeVal) Name() string { + type namedValue interface { + Name() string + } + if nVal, ok := v.Value.(namedValue); ok { + return nVal.Name() + } + return v.key +} + +// Merge is an helper function that merges n FlagSets into a single dest FlagSet +// In case of name collision between the flagsets it will apply +// the destination FlagSet's errorHandling behavior. +func Merge(dest *FlagSet, flagsets ...*FlagSet) error { + for _, fset := range flagsets { + if fset.formal == nil { + continue + } + for k, f := range fset.formal { + if _, ok := dest.formal[k]; ok { + var err error + if fset.name == "" { + err = fmt.Errorf("flag redefined: %s", k) + } else { + err = fmt.Errorf("%s flag redefined: %s", fset.name, k) + } + fmt.Fprintln(fset.Out(), err.Error()) + // Happens only if flags are declared with identical names + switch dest.errorHandling { + case ContinueOnError: + return err + case ExitOnError: + os.Exit(2) + case PanicOnError: + panic(err) + } + } + newF := *f + newF.Value = mergeVal{f.Value, k, fset} + if dest.formal == nil { + dest.formal = make(map[string]*Flag) + } + dest.formal[k] = &newF + } + } + return nil +} + +// IsEmpty reports if the FlagSet is actually empty. +func (fs *FlagSet) IsEmpty() bool { + return len(fs.actual) == 0 +} diff --git a/pkg/mflag/flag_test.go b/pkg/mflag/flag_test.go new file mode 100644 index 00000000..13835554 --- /dev/null +++ b/pkg/mflag/flag_test.go @@ -0,0 +1,527 @@ +// Copyright 2014-2016 The Docker & Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mflag + +import ( + "bytes" + "fmt" + "os" + "sort" + "strings" + "testing" + "time" +) + +// ResetForTesting clears all flag state and sets the usage function as directed. +// After calling ResetForTesting, parse errors in flag handling will not +// exit the program. +func ResetForTesting(usage func()) { + CommandLine = NewFlagSet(os.Args[0], ContinueOnError) + Usage = usage +} +func boolString(s string) string { + if s == "0" { + return "false" + } + return "true" +} + +func TestEverything(t *testing.T) { + ResetForTesting(nil) + Bool([]string{"test_bool"}, false, "bool value") + Int([]string{"test_int"}, 0, "int value") + Int64([]string{"test_int64"}, 0, "int64 value") + Uint([]string{"test_uint"}, 0, "uint value") + Uint64([]string{"test_uint64"}, 0, "uint64 value") + String([]string{"test_string"}, "0", "string value") + Float64([]string{"test_float64"}, 0, "float64 value") + Duration([]string{"test_duration"}, 0, "time.Duration value") + + m := make(map[string]*Flag) + desired := "0" + visitor := func(f *Flag) { + for _, name := range f.Names { + if len(name) > 5 && name[0:5] == "test_" { + m[name] = f + ok := false + switch { + case f.Value.String() == desired: + ok = true + case name == "test_bool" && f.Value.String() == boolString(desired): + ok = true + case name == "test_duration" && f.Value.String() == desired+"s": + ok = true + } + if !ok { + t.Error("Visit: bad value", f.Value.String(), "for", name) + } + } + } + } + VisitAll(visitor) + if len(m) != 8 { + t.Error("VisitAll misses some flags") + for k, v := range m { + t.Log(k, *v) + } + } + m = make(map[string]*Flag) + Visit(visitor) + if len(m) != 0 { + t.Errorf("Visit sees unset flags") + for k, v := range m { + t.Log(k, *v) + } + } + // Now set all flags + Set("test_bool", "true") + Set("test_int", "1") + Set("test_int64", "1") + Set("test_uint", "1") + Set("test_uint64", "1") + Set("test_string", "1") + Set("test_float64", "1") + Set("test_duration", "1s") + desired = "1" + Visit(visitor) + if len(m) != 8 { + t.Error("Visit fails after set") + for k, v := range m { + t.Log(k, *v) + } + } + // Now test they're visited in sort order. + var flagNames []string + Visit(func(f *Flag) { + for _, name := range f.Names { + flagNames = append(flagNames, name) + } + }) + if !sort.StringsAreSorted(flagNames) { + t.Errorf("flag names not sorted: %v", flagNames) + } +} + +func TestGet(t *testing.T) { + ResetForTesting(nil) + Bool([]string{"test_bool"}, true, "bool value") + Int([]string{"test_int"}, 1, "int value") + Int64([]string{"test_int64"}, 2, "int64 value") + Uint([]string{"test_uint"}, 3, "uint value") + Uint64([]string{"test_uint64"}, 4, "uint64 value") + String([]string{"test_string"}, "5", "string value") + Float64([]string{"test_float64"}, 6, "float64 value") + Duration([]string{"test_duration"}, 7, "time.Duration value") + + visitor := func(f *Flag) { + for _, name := range f.Names { + if len(name) > 5 && name[0:5] == "test_" { + g, ok := f.Value.(Getter) + if !ok { + t.Errorf("Visit: value does not satisfy Getter: %T", f.Value) + return + } + switch name { + case "test_bool": + ok = g.Get() == true + case "test_int": + ok = g.Get() == int(1) + case "test_int64": + ok = g.Get() == int64(2) + case "test_uint": + ok = g.Get() == uint(3) + case "test_uint64": + ok = g.Get() == uint64(4) + case "test_string": + ok = g.Get() == "5" + case "test_float64": + ok = g.Get() == float64(6) + case "test_duration": + ok = g.Get() == time.Duration(7) + } + if !ok { + t.Errorf("Visit: bad value %T(%v) for %s", g.Get(), g.Get(), name) + } + } + } + } + VisitAll(visitor) +} + +func testParse(f *FlagSet, t *testing.T) { + if f.Parsed() { + t.Error("f.Parse() = true before Parse") + } + boolFlag := f.Bool([]string{"bool"}, false, "bool value") + bool2Flag := f.Bool([]string{"bool2"}, false, "bool2 value") + f.Bool([]string{"bool3"}, false, "bool3 value") + bool4Flag := f.Bool([]string{"bool4"}, false, "bool4 value") + intFlag := f.Int([]string{"-int"}, 0, "int value") + int64Flag := f.Int64([]string{"-int64"}, 0, "int64 value") + uintFlag := f.Uint([]string{"uint"}, 0, "uint value") + uint64Flag := f.Uint64([]string{"-uint64"}, 0, "uint64 value") + stringFlag := f.String([]string{"string"}, "0", "string value") + f.String([]string{"string2"}, "0", "string2 value") + singleQuoteFlag := f.String([]string{"squote"}, "", "single quoted value") + doubleQuoteFlag := f.String([]string{"dquote"}, "", "double quoted value") + mixedQuoteFlag := f.String([]string{"mquote"}, "", "mixed quoted value") + mixed2QuoteFlag := f.String([]string{"mquote2"}, "", "mixed2 quoted value") + nestedQuoteFlag := f.String([]string{"nquote"}, "", "nested quoted value") + nested2QuoteFlag := f.String([]string{"nquote2"}, "", "nested2 quoted value") + float64Flag := f.Float64([]string{"float64"}, 0, "float64 value") + durationFlag := f.Duration([]string{"duration"}, 5*time.Second, "time.Duration value") + extra := "one-extra-argument" + args := []string{ + "-bool", + "-bool2=true", + "-bool4=false", + "--int", "22", + "--int64", "0x23", + "-uint", "24", + "--uint64", "25", + "-string", "hello", + "-squote='single'", + `-dquote="double"`, + `-mquote='mixed"`, + `-mquote2="mixed2'`, + `-nquote="'single nested'"`, + `-nquote2='"double nested"'`, + "-float64", "2718e28", + "-duration", "2m", + extra, + } + if err := f.Parse(args); err != nil { + t.Fatal(err) + } + if !f.Parsed() { + t.Error("f.Parse() = false after Parse") + } + if *boolFlag != true { + t.Error("bool flag should be true, is ", *boolFlag) + } + if *bool2Flag != true { + t.Error("bool2 flag should be true, is ", *bool2Flag) + } + if !f.IsSet("bool2") { + t.Error("bool2 should be marked as set") + } + if f.IsSet("bool3") { + t.Error("bool3 should not be marked as set") + } + if !f.IsSet("bool4") { + t.Error("bool4 should be marked as set") + } + if *bool4Flag != false { + t.Error("bool4 flag should be false, is ", *bool4Flag) + } + if *intFlag != 22 { + t.Error("int flag should be 22, is ", *intFlag) + } + if *int64Flag != 0x23 { + t.Error("int64 flag should be 0x23, is ", *int64Flag) + } + if *uintFlag != 24 { + t.Error("uint flag should be 24, is ", *uintFlag) + } + if *uint64Flag != 25 { + t.Error("uint64 flag should be 25, is ", *uint64Flag) + } + if *stringFlag != "hello" { + t.Error("string flag should be `hello`, is ", *stringFlag) + } + if !f.IsSet("string") { + t.Error("string flag should be marked as set") + } + if f.IsSet("string2") { + t.Error("string2 flag should not be marked as set") + } + if *singleQuoteFlag != "single" { + t.Error("single quote string flag should be `single`, is ", *singleQuoteFlag) + } + if *doubleQuoteFlag != "double" { + t.Error("double quote string flag should be `double`, is ", *doubleQuoteFlag) + } + if *mixedQuoteFlag != `'mixed"` { + t.Error("mixed quote string flag should be `'mixed\"`, is ", *mixedQuoteFlag) + } + if *mixed2QuoteFlag != `"mixed2'` { + t.Error("mixed2 quote string flag should be `\"mixed2'`, is ", *mixed2QuoteFlag) + } + if *nestedQuoteFlag != "'single nested'" { + t.Error("nested quote string flag should be `'single nested'`, is ", *nestedQuoteFlag) + } + if *nested2QuoteFlag != `"double nested"` { + t.Error("double quote string flag should be `\"double nested\"`, is ", *nested2QuoteFlag) + } + if *float64Flag != 2718e28 { + t.Error("float64 flag should be 2718e28, is ", *float64Flag) + } + if *durationFlag != 2*time.Minute { + t.Error("duration flag should be 2m, is ", *durationFlag) + } + if len(f.Args()) != 1 { + t.Error("expected one argument, got", len(f.Args())) + } else if f.Args()[0] != extra { + t.Errorf("expected argument %q got %q", extra, f.Args()[0]) + } +} + +func testPanic(f *FlagSet, t *testing.T) { + f.Int([]string{"-int"}, 0, "int value") + if f.Parsed() { + t.Error("f.Parse() = true before Parse") + } + args := []string{ + "-int", "21", + } + f.Parse(args) +} + +func TestParsePanic(t *testing.T) { + ResetForTesting(func() {}) + testPanic(CommandLine, t) +} + +func TestParse(t *testing.T) { + ResetForTesting(func() { t.Error("bad parse") }) + testParse(CommandLine, t) +} + +func TestFlagSetParse(t *testing.T) { + testParse(NewFlagSet("test", ContinueOnError), t) +} + +// Declare a user-defined flag type. +type flagVar []string + +func (f *flagVar) String() string { + return fmt.Sprint([]string(*f)) +} + +func (f *flagVar) Set(value string) error { + *f = append(*f, value) + return nil +} + +func TestUserDefined(t *testing.T) { + var flags FlagSet + flags.Init("test", ContinueOnError) + var v flagVar + flags.Var(&v, []string{"v"}, "usage") + if err := flags.Parse([]string{"-v", "1", "-v", "2", "-v=3"}); err != nil { + t.Error(err) + } + if len(v) != 3 { + t.Fatal("expected 3 args; got ", len(v)) + } + expect := "[1 2 3]" + if v.String() != expect { + t.Errorf("expected value %q got %q", expect, v.String()) + } +} + +// Declare a user-defined boolean flag type. +type boolFlagVar struct { + count int +} + +func (b *boolFlagVar) String() string { + return fmt.Sprintf("%d", b.count) +} + +func (b *boolFlagVar) Set(value string) error { + if value == "true" { + b.count++ + } + return nil +} + +func (b *boolFlagVar) IsBoolFlag() bool { + return b.count < 4 +} + +func TestUserDefinedBool(t *testing.T) { + var flags FlagSet + flags.Init("test", ContinueOnError) + var b boolFlagVar + var err error + flags.Var(&b, []string{"b"}, "usage") + if err = flags.Parse([]string{"-b", "-b", "-b", "-b=true", "-b=false", "-b", "barg", "-b"}); err != nil { + if b.count < 4 { + t.Error(err) + } + } + + if b.count != 4 { + t.Errorf("want: %d; got: %d", 4, b.count) + } + + if err == nil { + t.Error("expected error; got none") + } +} + +func TestSetOutput(t *testing.T) { + var flags FlagSet + var buf bytes.Buffer + flags.SetOutput(&buf) + flags.Init("test", ContinueOnError) + flags.Parse([]string{"-unknown"}) + if out := buf.String(); !strings.Contains(out, "-unknown") { + t.Logf("expected output mentioning unknown; got %q", out) + } +} + +// This tests that one can reset the flags. This still works but not well, and is +// superseded by FlagSet. +func TestChangingArgs(t *testing.T) { + ResetForTesting(func() { t.Fatal("bad parse") }) + oldArgs := os.Args + defer func() { os.Args = oldArgs }() + os.Args = []string{"cmd", "-before", "subcmd", "-after", "args"} + before := Bool([]string{"before"}, false, "") + if err := CommandLine.Parse(os.Args[1:]); err != nil { + t.Fatal(err) + } + cmd := Arg(0) + os.Args = Args() + after := Bool([]string{"after"}, false, "") + Parse() + args := Args() + + if !*before || cmd != "subcmd" || !*after || len(args) != 1 || args[0] != "args" { + t.Fatalf("expected true subcmd true [args] got %v %v %v %v", *before, cmd, *after, args) + } +} + +// Test that -help invokes the usage message and returns ErrHelp. +func TestHelp(t *testing.T) { + var helpCalled = false + fs := NewFlagSet("help test", ContinueOnError) + fs.Usage = func() { helpCalled = true } + var flag bool + fs.BoolVar(&flag, []string{"flag"}, false, "regular flag") + // Regular flag invocation should work + err := fs.Parse([]string{"-flag=true"}) + if err != nil { + t.Fatal("expected no error; got ", err) + } + if !flag { + t.Error("flag was not set by -flag") + } + if helpCalled { + t.Error("help called for regular flag") + helpCalled = false // reset for next test + } + // Help flag should work as expected. + err = fs.Parse([]string{"-help"}) + if err == nil { + t.Fatal("error expected") + } + if err != ErrHelp { + t.Fatal("expected ErrHelp; got ", err) + } + if !helpCalled { + t.Fatal("help was not called") + } + // If we define a help flag, that should override. + var help bool + fs.BoolVar(&help, []string{"help"}, false, "help flag") + helpCalled = false + err = fs.Parse([]string{"-help"}) + if err != nil { + t.Fatal("expected no error for defined -help; got ", err) + } + if helpCalled { + t.Fatal("help was called; should not have been for defined help flag") + } +} + +// Test the flag count functions. +func TestFlagCounts(t *testing.T) { + fs := NewFlagSet("help test", ContinueOnError) + var flag bool + fs.BoolVar(&flag, []string{"flag1"}, false, "regular flag") + fs.BoolVar(&flag, []string{"#deprecated1"}, false, "regular flag") + fs.BoolVar(&flag, []string{"f", "flag2"}, false, "regular flag") + fs.BoolVar(&flag, []string{"#d", "#deprecated2"}, false, "regular flag") + fs.BoolVar(&flag, []string{"flag3"}, false, "regular flag") + fs.BoolVar(&flag, []string{"g", "#flag4", "-flag4"}, false, "regular flag") + + if fs.FlagCount() != 6 { + t.Fatal("FlagCount wrong. ", fs.FlagCount()) + } + if fs.FlagCountUndeprecated() != 4 { + t.Fatal("FlagCountUndeprecated wrong. ", fs.FlagCountUndeprecated()) + } + if fs.NFlag() != 0 { + t.Fatal("NFlag wrong. ", fs.NFlag()) + } + err := fs.Parse([]string{"-fd", "-g", "-flag4"}) + if err != nil { + t.Fatal("expected no error for defined -help; got ", err) + } + if fs.NFlag() != 4 { + t.Fatal("NFlag wrong. ", fs.NFlag()) + } +} + +// Show up bug in sortFlags +func TestSortFlags(t *testing.T) { + fs := NewFlagSet("help TestSortFlags", ContinueOnError) + + var err error + + var b bool + fs.BoolVar(&b, []string{"b", "-banana"}, false, "usage") + + err = fs.Parse([]string{"--banana=true"}) + if err != nil { + t.Fatal("expected no error; got ", err) + } + + count := 0 + + fs.VisitAll(func(flag *Flag) { + count++ + if flag == nil { + t.Fatal("VisitAll should not return a nil flag") + } + }) + flagcount := fs.FlagCount() + if flagcount != count { + t.Fatalf("FlagCount (%d) != number (%d) of elements visited", flagcount, count) + } + // Make sure its idempotent + if flagcount != fs.FlagCount() { + t.Fatalf("FlagCount (%d) != fs.FlagCount() (%d) of elements visited", flagcount, fs.FlagCount()) + } + + count = 0 + fs.Visit(func(flag *Flag) { + count++ + if flag == nil { + t.Fatal("Visit should not return a nil flag") + } + }) + nflag := fs.NFlag() + if nflag != count { + t.Fatalf("NFlag (%d) != number (%d) of elements visited", nflag, count) + } + if nflag != fs.NFlag() { + t.Fatalf("NFlag (%d) != fs.NFlag() (%d) of elements visited", nflag, fs.NFlag()) + } +} + +func TestMergeFlags(t *testing.T) { + base := NewFlagSet("base", ContinueOnError) + base.String([]string{"f"}, "", "") + + fs := NewFlagSet("test", ContinueOnError) + Merge(fs, base) + if len(fs.formal) != 1 { + t.Fatalf("FlagCount (%d) != number (1) of elements merged", len(fs.formal)) + } +} diff --git a/pkg/mount/flags.go b/pkg/mount/flags.go new file mode 100644 index 00000000..d2fb1fb4 --- /dev/null +++ b/pkg/mount/flags.go @@ -0,0 +1,92 @@ +package mount + +import ( + "fmt" + "strings" +) + +// Parse fstab type mount options into mount() flags +// and device specific data +func parseOptions(options string) (int, string) { + var ( + flag int + data []string + ) + + flags := map[string]struct { + clear bool + flag int + }{ + "defaults": {false, 0}, + "ro": {false, RDONLY}, + "rw": {true, RDONLY}, + "suid": {true, NOSUID}, + "nosuid": {false, NOSUID}, + "dev": {true, NODEV}, + "nodev": {false, NODEV}, + "exec": {true, NOEXEC}, + "noexec": {false, NOEXEC}, + "sync": {false, SYNCHRONOUS}, + "async": {true, SYNCHRONOUS}, + "dirsync": {false, DIRSYNC}, + "remount": {false, REMOUNT}, + "mand": {false, MANDLOCK}, + "nomand": {true, MANDLOCK}, + "atime": {true, NOATIME}, + "noatime": {false, NOATIME}, + "diratime": {true, NODIRATIME}, + "nodiratime": {false, NODIRATIME}, + "bind": {false, BIND}, + "rbind": {false, RBIND}, + "unbindable": {false, UNBINDABLE}, + "runbindable": {false, RUNBINDABLE}, + "private": {false, PRIVATE}, + "rprivate": {false, RPRIVATE}, + "shared": {false, SHARED}, + "rshared": {false, RSHARED}, + "slave": {false, SLAVE}, + "rslave": {false, RSLAVE}, + "relatime": {false, RELATIME}, + "norelatime": {true, RELATIME}, + "strictatime": {false, STRICTATIME}, + "nostrictatime": {true, STRICTATIME}, + } + + for _, o := range strings.Split(options, ",") { + // If the option does not exist in the flags table or the flag + // is not supported on the platform, + // then it is a data value for a specific fs type + if f, exists := flags[o]; exists && f.flag != 0 { + if f.clear { + flag &= ^f.flag + } else { + flag |= f.flag + } + } else { + data = append(data, o) + } + } + return flag, strings.Join(data, ",") +} + +// ParseTmpfsOptions parse fstab type mount options into flags and data +func ParseTmpfsOptions(options string) (int, string, error) { + flags, data := parseOptions(options) + validFlags := map[string]bool{ + "": true, + "size": true, + "mode": true, + "uid": true, + "gid": true, + "nr_inodes": true, + "nr_blocks": true, + "mpol": true, + } + for _, o := range strings.Split(data, ",") { + opt := strings.SplitN(o, "=", 2) + if !validFlags[opt[0]] { + return 0, "", fmt.Errorf("Invalid tmpfs option %q", opt) + } + } + return flags, data, nil +} diff --git a/pkg/mount/flags_freebsd.go b/pkg/mount/flags_freebsd.go new file mode 100644 index 00000000..f166cb2f --- /dev/null +++ b/pkg/mount/flags_freebsd.go @@ -0,0 +1,48 @@ +// +build freebsd,cgo + +package mount + +/* +#include +*/ +import "C" + +const ( + // RDONLY will mount the filesystem as read-only. + RDONLY = C.MNT_RDONLY + + // NOSUID will not allow set-user-identifier or set-group-identifier bits to + // take effect. + NOSUID = C.MNT_NOSUID + + // NOEXEC will not allow execution of any binaries on the mounted file system. + NOEXEC = C.MNT_NOEXEC + + // SYNCHRONOUS will allow any I/O to the file system to be done synchronously. + SYNCHRONOUS = C.MNT_SYNCHRONOUS + + // NOATIME will not update the file access time when reading from a file. + NOATIME = C.MNT_NOATIME +) + +// These flags are unsupported. +const ( + BIND = 0 + DIRSYNC = 0 + MANDLOCK = 0 + NODEV = 0 + NODIRATIME = 0 + UNBINDABLE = 0 + RUNBINDABLE = 0 + PRIVATE = 0 + RPRIVATE = 0 + SHARED = 0 + RSHARED = 0 + SLAVE = 0 + RSLAVE = 0 + RBIND = 0 + RELATIVE = 0 + RELATIME = 0 + REMOUNT = 0 + STRICTATIME = 0 +) diff --git a/pkg/mount/flags_linux.go b/pkg/mount/flags_linux.go new file mode 100644 index 00000000..dc696dce --- /dev/null +++ b/pkg/mount/flags_linux.go @@ -0,0 +1,85 @@ +package mount + +import ( + "syscall" +) + +const ( + // RDONLY will mount the file system read-only. + RDONLY = syscall.MS_RDONLY + + // NOSUID will not allow set-user-identifier or set-group-identifier bits to + // take effect. + NOSUID = syscall.MS_NOSUID + + // NODEV will not interpret character or block special devices on the file + // system. + NODEV = syscall.MS_NODEV + + // NOEXEC will not allow execution of any binaries on the mounted file system. + NOEXEC = syscall.MS_NOEXEC + + // SYNCHRONOUS will allow I/O to the file system to be done synchronously. + SYNCHRONOUS = syscall.MS_SYNCHRONOUS + + // DIRSYNC will force all directory updates within the file system to be done + // synchronously. This affects the following system calls: create, link, + // unlink, symlink, mkdir, rmdir, mknod and rename. + DIRSYNC = syscall.MS_DIRSYNC + + // REMOUNT will attempt to remount an already-mounted file system. This is + // commonly used to change the mount flags for a file system, especially to + // make a readonly file system writeable. It does not change device or mount + // point. + REMOUNT = syscall.MS_REMOUNT + + // MANDLOCK will force mandatory locks on a filesystem. + MANDLOCK = syscall.MS_MANDLOCK + + // NOATIME will not update the file access time when reading from a file. + NOATIME = syscall.MS_NOATIME + + // NODIRATIME will not update the directory access time. + NODIRATIME = syscall.MS_NODIRATIME + + // BIND remounts a subtree somewhere else. + BIND = syscall.MS_BIND + + // RBIND remounts a subtree and all possible submounts somewhere else. + RBIND = syscall.MS_BIND | syscall.MS_REC + + // UNBINDABLE creates a mount which cannot be cloned through a bind operation. + UNBINDABLE = syscall.MS_UNBINDABLE + + // RUNBINDABLE marks the entire mount tree as UNBINDABLE. + RUNBINDABLE = syscall.MS_UNBINDABLE | syscall.MS_REC + + // PRIVATE creates a mount which carries no propagation abilities. + PRIVATE = syscall.MS_PRIVATE + + // RPRIVATE marks the entire mount tree as PRIVATE. + RPRIVATE = syscall.MS_PRIVATE | syscall.MS_REC + + // SLAVE creates a mount which receives propagation from its master, but not + // vice versa. + SLAVE = syscall.MS_SLAVE + + // RSLAVE marks the entire mount tree as SLAVE. + RSLAVE = syscall.MS_SLAVE | syscall.MS_REC + + // SHARED creates a mount which provides the ability to create mirrors of + // that mount such that mounts and unmounts within any of the mirrors + // propagate to the other mirrors. + SHARED = syscall.MS_SHARED + + // RSHARED marks the entire mount tree as SHARED. + RSHARED = syscall.MS_SHARED | syscall.MS_REC + + // RELATIME updates inode access times relative to modify or change time. + RELATIME = syscall.MS_RELATIME + + // STRICTATIME allows to explicitly request full atime updates. This makes + // it possible for the kernel to default to relatime or noatime but still + // allow userspace to override it. + STRICTATIME = syscall.MS_STRICTATIME +) diff --git a/pkg/mount/flags_unsupported.go b/pkg/mount/flags_unsupported.go new file mode 100644 index 00000000..a90d3d11 --- /dev/null +++ b/pkg/mount/flags_unsupported.go @@ -0,0 +1,30 @@ +// +build !linux,!freebsd freebsd,!cgo + +package mount + +// These flags are unsupported. +const ( + BIND = 0 + DIRSYNC = 0 + MANDLOCK = 0 + NOATIME = 0 + NODEV = 0 + NODIRATIME = 0 + NOEXEC = 0 + NOSUID = 0 + UNBINDABLE = 0 + RUNBINDABLE = 0 + PRIVATE = 0 + RPRIVATE = 0 + SHARED = 0 + RSHARED = 0 + SLAVE = 0 + RSLAVE = 0 + RBIND = 0 + RELATIME = 0 + RELATIVE = 0 + REMOUNT = 0 + STRICTATIME = 0 + SYNCHRONOUS = 0 + RDONLY = 0 +) diff --git a/pkg/mount/mount.go b/pkg/mount/mount.go new file mode 100644 index 00000000..ed7216e5 --- /dev/null +++ b/pkg/mount/mount.go @@ -0,0 +1,74 @@ +package mount + +import ( + "time" +) + +// GetMounts retrieves a list of mounts for the current running process. +func GetMounts() ([]*Info, error) { + return parseMountTable() +} + +// Mounted looks at /proc/self/mountinfo to determine of the specified +// mountpoint has been mounted +func Mounted(mountpoint string) (bool, error) { + entries, err := parseMountTable() + if err != nil { + return false, err + } + + // Search the table for the mountpoint + for _, e := range entries { + if e.Mountpoint == mountpoint { + return true, nil + } + } + return false, nil +} + +// Mount will mount filesystem according to the specified configuration, on the +// condition that the target path is *not* already mounted. Options must be +// specified like the mount or fstab unix commands: "opt1=val1,opt2=val2". See +// flags.go for supported option flags. +func Mount(device, target, mType, options string) error { + flag, _ := parseOptions(options) + if flag&REMOUNT != REMOUNT { + if mounted, err := Mounted(target); err != nil || mounted { + return err + } + } + return ForceMount(device, target, mType, options) +} + +// ForceMount will mount a filesystem according to the specified configuration, +// *regardless* if the target path is not already mounted. Options must be +// specified like the mount or fstab unix commands: "opt1=val1,opt2=val2". See +// flags.go for supported option flags. +func ForceMount(device, target, mType, options string) error { + flag, data := parseOptions(options) + if err := mount(device, target, mType, uintptr(flag), data); err != nil { + return err + } + return nil +} + +// Unmount will unmount the target filesystem, so long as it is mounted. +func Unmount(target string) error { + if mounted, err := Mounted(target); err != nil || !mounted { + return err + } + return ForceUnmount(target) +} + +// ForceUnmount will force an unmount of the target filesystem, regardless if +// it is mounted or not. +func ForceUnmount(target string) (err error) { + // Simple retry logic for unmount + for i := 0; i < 10; i++ { + if err = unmount(target, 0); err == nil { + return nil + } + time.Sleep(100 * time.Millisecond) + } + return +} diff --git a/pkg/mount/mount_unix_test.go b/pkg/mount/mount_unix_test.go new file mode 100644 index 00000000..d45fbc1b --- /dev/null +++ b/pkg/mount/mount_unix_test.go @@ -0,0 +1,139 @@ +// +build !windows + +package mount + +import ( + "os" + "path" + "testing" +) + +func TestMountOptionsParsing(t *testing.T) { + options := "noatime,ro,size=10k" + + flag, data := parseOptions(options) + + if data != "size=10k" { + t.Fatalf("Expected size=10 got %s", data) + } + + expectedFlag := NOATIME | RDONLY + + if flag != expectedFlag { + t.Fatalf("Expected %d got %d", expectedFlag, flag) + } +} + +func TestMounted(t *testing.T) { + tmp := path.Join(os.TempDir(), "mount-tests") + if err := os.MkdirAll(tmp, 0777); err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + + var ( + sourceDir = path.Join(tmp, "source") + targetDir = path.Join(tmp, "target") + sourcePath = path.Join(sourceDir, "file.txt") + targetPath = path.Join(targetDir, "file.txt") + ) + + os.Mkdir(sourceDir, 0777) + os.Mkdir(targetDir, 0777) + + f, err := os.Create(sourcePath) + if err != nil { + t.Fatal(err) + } + f.WriteString("hello") + f.Close() + + f, err = os.Create(targetPath) + if err != nil { + t.Fatal(err) + } + f.Close() + + if err := Mount(sourceDir, targetDir, "none", "bind,rw"); err != nil { + t.Fatal(err) + } + defer func() { + if err := Unmount(targetDir); err != nil { + t.Fatal(err) + } + }() + + mounted, err := Mounted(targetDir) + if err != nil { + t.Fatal(err) + } + if !mounted { + t.Fatalf("Expected %s to be mounted", targetDir) + } + if _, err := os.Stat(targetDir); err != nil { + t.Fatal(err) + } +} + +func TestMountReadonly(t *testing.T) { + tmp := path.Join(os.TempDir(), "mount-tests") + if err := os.MkdirAll(tmp, 0777); err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + + var ( + sourceDir = path.Join(tmp, "source") + targetDir = path.Join(tmp, "target") + sourcePath = path.Join(sourceDir, "file.txt") + targetPath = path.Join(targetDir, "file.txt") + ) + + os.Mkdir(sourceDir, 0777) + os.Mkdir(targetDir, 0777) + + f, err := os.Create(sourcePath) + if err != nil { + t.Fatal(err) + } + f.WriteString("hello") + f.Close() + + f, err = os.Create(targetPath) + if err != nil { + t.Fatal(err) + } + f.Close() + + if err := Mount(sourceDir, targetDir, "none", "bind,ro"); err != nil { + t.Fatal(err) + } + defer func() { + if err := Unmount(targetDir); err != nil { + t.Fatal(err) + } + }() + + f, err = os.OpenFile(targetPath, os.O_RDWR, 0777) + if err == nil { + t.Fatal("Should not be able to open a ro file as rw") + } +} + +func TestGetMounts(t *testing.T) { + mounts, err := GetMounts() + if err != nil { + t.Fatal(err) + } + + root := false + for _, entry := range mounts { + if entry.Mountpoint == "/" { + root = true + } + } + + if !root { + t.Fatal("/ should be mounted at least") + } +} diff --git a/pkg/mount/mounter_freebsd.go b/pkg/mount/mounter_freebsd.go new file mode 100644 index 00000000..bb870e6f --- /dev/null +++ b/pkg/mount/mounter_freebsd.go @@ -0,0 +1,59 @@ +package mount + +/* +#include +#include +#include +#include +#include +#include +*/ +import "C" + +import ( + "fmt" + "strings" + "syscall" + "unsafe" +) + +func allocateIOVecs(options []string) []C.struct_iovec { + out := make([]C.struct_iovec, len(options)) + for i, option := range options { + out[i].iov_base = unsafe.Pointer(C.CString(option)) + out[i].iov_len = C.size_t(len(option) + 1) + } + return out +} + +func mount(device, target, mType string, flag uintptr, data string) error { + isNullFS := false + + xs := strings.Split(data, ",") + for _, x := range xs { + if x == "bind" { + isNullFS = true + } + } + + options := []string{"fspath", target} + if isNullFS { + options = append(options, "fstype", "nullfs", "target", device) + } else { + options = append(options, "fstype", mType, "from", device) + } + rawOptions := allocateIOVecs(options) + for _, rawOption := range rawOptions { + defer C.free(rawOption.iov_base) + } + + if errno := C.nmount(&rawOptions[0], C.uint(len(options)), C.int(flag)); errno != 0 { + reason := C.GoString(C.strerror(*C.__error())) + return fmt.Errorf("Failed to call nmount: %s", reason) + } + return nil +} + +func unmount(target string, flag int) error { + return syscall.Unmount(target, flag) +} diff --git a/pkg/mount/mounter_linux.go b/pkg/mount/mounter_linux.go new file mode 100644 index 00000000..dd4280c7 --- /dev/null +++ b/pkg/mount/mounter_linux.go @@ -0,0 +1,21 @@ +package mount + +import ( + "syscall" +) + +func mount(device, target, mType string, flag uintptr, data string) error { + if err := syscall.Mount(device, target, mType, flag, data); err != nil { + return err + } + + // If we have a bind mount or remount, remount... + if flag&syscall.MS_BIND == syscall.MS_BIND && flag&syscall.MS_RDONLY == syscall.MS_RDONLY { + return syscall.Mount(device, target, mType, flag|syscall.MS_REMOUNT, data) + } + return nil +} + +func unmount(target string, flag int) error { + return syscall.Unmount(target, flag) +} diff --git a/pkg/mount/mounter_unsupported.go b/pkg/mount/mounter_unsupported.go new file mode 100644 index 00000000..eb93365e --- /dev/null +++ b/pkg/mount/mounter_unsupported.go @@ -0,0 +1,11 @@ +// +build !linux,!freebsd freebsd,!cgo + +package mount + +func mount(device, target, mType string, flag uintptr, data string) error { + panic("Not implemented") +} + +func unmount(target string, flag int) error { + panic("Not implemented") +} diff --git a/pkg/mount/mountinfo.go b/pkg/mount/mountinfo.go new file mode 100644 index 00000000..e3fc3535 --- /dev/null +++ b/pkg/mount/mountinfo.go @@ -0,0 +1,40 @@ +package mount + +// Info reveals information about a particular mounted filesystem. This +// struct is populated from the content in the /proc//mountinfo file. +type Info struct { + // ID is a unique identifier of the mount (may be reused after umount). + ID int + + // Parent indicates the ID of the mount parent (or of self for the top of the + // mount tree). + Parent int + + // Major indicates one half of the device ID which identifies the device class. + Major int + + // Minor indicates one half of the device ID which identifies a specific + // instance of device. + Minor int + + // Root of the mount within the filesystem. + Root string + + // Mountpoint indicates the mount point relative to the process's root. + Mountpoint string + + // Opts represents mount-specific options. + Opts string + + // Optional represents optional fields. + Optional string + + // Fstype indicates the type of filesystem, such as EXT3. + Fstype string + + // Source indicates filesystem specific information or "none". + Source string + + // VfsOpts represents per super block options. + VfsOpts string +} diff --git a/pkg/mount/mountinfo_freebsd.go b/pkg/mount/mountinfo_freebsd.go new file mode 100644 index 00000000..4f32edcd --- /dev/null +++ b/pkg/mount/mountinfo_freebsd.go @@ -0,0 +1,41 @@ +package mount + +/* +#include +#include +#include +*/ +import "C" + +import ( + "fmt" + "reflect" + "unsafe" +) + +// Parse /proc/self/mountinfo because comparing Dev and ino does not work from +// bind mounts. +func parseMountTable() ([]*Info, error) { + var rawEntries *C.struct_statfs + + count := int(C.getmntinfo(&rawEntries, C.MNT_WAIT)) + if count == 0 { + return nil, fmt.Errorf("Failed to call getmntinfo") + } + + var entries []C.struct_statfs + header := (*reflect.SliceHeader)(unsafe.Pointer(&entries)) + header.Cap = count + header.Len = count + header.Data = uintptr(unsafe.Pointer(rawEntries)) + + var out []*Info + for _, entry := range entries { + var mountinfo Info + mountinfo.Mountpoint = C.GoString(&entry.f_mntonname[0]) + mountinfo.Source = C.GoString(&entry.f_mntfromname[0]) + mountinfo.Fstype = C.GoString(&entry.f_fstypename[0]) + out = append(out, &mountinfo) + } + return out, nil +} diff --git a/pkg/mount/mountinfo_linux.go b/pkg/mount/mountinfo_linux.go new file mode 100644 index 00000000..be69fee1 --- /dev/null +++ b/pkg/mount/mountinfo_linux.go @@ -0,0 +1,95 @@ +// +build linux + +package mount + +import ( + "bufio" + "fmt" + "io" + "os" + "strings" +) + +const ( + /* 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue + (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11) + + (1) mount ID: unique identifier of the mount (may be reused after umount) + (2) parent ID: ID of parent (or of self for the top of the mount tree) + (3) major:minor: value of st_dev for files on filesystem + (4) root: root of the mount within the filesystem + (5) mount point: mount point relative to the process's root + (6) mount options: per mount options + (7) optional fields: zero or more fields of the form "tag[:value]" + (8) separator: marks the end of the optional fields + (9) filesystem type: name of filesystem of the form "type[.subtype]" + (10) mount source: filesystem specific information or "none" + (11) super options: per super block options*/ + mountinfoFormat = "%d %d %d:%d %s %s %s %s" +) + +// Parse /proc/self/mountinfo because comparing Dev and ino does not work from +// bind mounts +func parseMountTable() ([]*Info, error) { + f, err := os.Open("/proc/self/mountinfo") + if err != nil { + return nil, err + } + defer f.Close() + + return parseInfoFile(f) +} + +func parseInfoFile(r io.Reader) ([]*Info, error) { + var ( + s = bufio.NewScanner(r) + out = []*Info{} + ) + + for s.Scan() { + if err := s.Err(); err != nil { + return nil, err + } + + var ( + p = &Info{} + text = s.Text() + optionalFields string + ) + + if _, err := fmt.Sscanf(text, mountinfoFormat, + &p.ID, &p.Parent, &p.Major, &p.Minor, + &p.Root, &p.Mountpoint, &p.Opts, &optionalFields); err != nil { + return nil, fmt.Errorf("Scanning '%s' failed: %s", text, err) + } + // Safe as mountinfo encodes mountpoints with spaces as \040. + index := strings.Index(text, " - ") + postSeparatorFields := strings.Fields(text[index+3:]) + if len(postSeparatorFields) < 3 { + return nil, fmt.Errorf("Error found less than 3 fields post '-' in %q", text) + } + + if optionalFields != "-" { + p.Optional = optionalFields + } + + p.Fstype = postSeparatorFields[0] + p.Source = postSeparatorFields[1] + p.VfsOpts = strings.Join(postSeparatorFields[2:], " ") + out = append(out, p) + } + return out, nil +} + +// PidMountInfo collects the mounts for a specific process ID. If the process +// ID is unknown, it is better to use `GetMounts` which will inspect +// "/proc/self/mountinfo" instead. +func PidMountInfo(pid int) ([]*Info, error) { + f, err := os.Open(fmt.Sprintf("/proc/%d/mountinfo", pid)) + if err != nil { + return nil, err + } + defer f.Close() + + return parseInfoFile(f) +} diff --git a/pkg/mount/mountinfo_linux_test.go b/pkg/mount/mountinfo_linux_test.go new file mode 100644 index 00000000..bd100e1d --- /dev/null +++ b/pkg/mount/mountinfo_linux_test.go @@ -0,0 +1,476 @@ +// +build linux + +package mount + +import ( + "bytes" + "testing" +) + +const ( + fedoraMountinfo = `15 35 0:3 / /proc rw,nosuid,nodev,noexec,relatime shared:5 - proc proc rw + 16 35 0:14 / /sys rw,nosuid,nodev,noexec,relatime shared:6 - sysfs sysfs rw,seclabel + 17 35 0:5 / /dev rw,nosuid shared:2 - devtmpfs devtmpfs rw,seclabel,size=8056484k,nr_inodes=2014121,mode=755 + 18 16 0:15 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:7 - securityfs securityfs rw + 19 16 0:13 / /sys/fs/selinux rw,relatime shared:8 - selinuxfs selinuxfs rw + 20 17 0:16 / /dev/shm rw,nosuid,nodev shared:3 - tmpfs tmpfs rw,seclabel + 21 17 0:10 / /dev/pts rw,nosuid,noexec,relatime shared:4 - devpts devpts rw,seclabel,gid=5,mode=620,ptmxmode=000 + 22 35 0:17 / /run rw,nosuid,nodev shared:21 - tmpfs tmpfs rw,seclabel,mode=755 + 23 16 0:18 / /sys/fs/cgroup rw,nosuid,nodev,noexec shared:9 - tmpfs tmpfs rw,seclabel,mode=755 + 24 23 0:19 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:10 - cgroup cgroup rw,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd + 25 16 0:20 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime shared:20 - pstore pstore rw + 26 23 0:21 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:11 - cgroup cgroup rw,cpuset,clone_children + 27 23 0:22 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:12 - cgroup cgroup rw,cpuacct,cpu,clone_children + 28 23 0:23 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:13 - cgroup cgroup rw,memory,clone_children + 29 23 0:24 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:14 - cgroup cgroup rw,devices,clone_children + 30 23 0:25 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:15 - cgroup cgroup rw,freezer,clone_children + 31 23 0:26 / /sys/fs/cgroup/net_cls rw,nosuid,nodev,noexec,relatime shared:16 - cgroup cgroup rw,net_cls,clone_children + 32 23 0:27 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:17 - cgroup cgroup rw,blkio,clone_children + 33 23 0:28 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:18 - cgroup cgroup rw,perf_event,clone_children + 34 23 0:29 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:19 - cgroup cgroup rw,hugetlb,clone_children + 35 1 253:2 / / rw,relatime shared:1 - ext4 /dev/mapper/ssd-root--f20 rw,seclabel,data=ordered + 36 15 0:30 / /proc/sys/fs/binfmt_misc rw,relatime shared:22 - autofs systemd-1 rw,fd=38,pgrp=1,timeout=300,minproto=5,maxproto=5,direct + 37 17 0:12 / /dev/mqueue rw,relatime shared:23 - mqueue mqueue rw,seclabel + 38 35 0:31 / /tmp rw shared:24 - tmpfs tmpfs rw,seclabel + 39 17 0:32 / /dev/hugepages rw,relatime shared:25 - hugetlbfs hugetlbfs rw,seclabel + 40 16 0:7 / /sys/kernel/debug rw,relatime shared:26 - debugfs debugfs rw + 41 16 0:33 / /sys/kernel/config rw,relatime shared:27 - configfs configfs rw + 42 35 0:34 / /var/lib/nfs/rpc_pipefs rw,relatime shared:28 - rpc_pipefs sunrpc rw + 43 15 0:35 / /proc/fs/nfsd rw,relatime shared:29 - nfsd sunrpc rw + 45 35 8:17 / /boot rw,relatime shared:30 - ext4 /dev/sdb1 rw,seclabel,data=ordered + 46 35 253:4 / /home rw,relatime shared:31 - ext4 /dev/mapper/ssd-home rw,seclabel,data=ordered + 47 35 253:5 / /var/lib/libvirt/images rw,noatime,nodiratime shared:32 - ext4 /dev/mapper/ssd-virt rw,seclabel,discard,data=ordered + 48 35 253:12 / /mnt/old rw,relatime shared:33 - ext4 /dev/mapper/HelpDeskRHEL6-FedoraRoot rw,seclabel,data=ordered + 121 22 0:36 / /run/user/1000/gvfs rw,nosuid,nodev,relatime shared:104 - fuse.gvfsd-fuse gvfsd-fuse rw,user_id=1000,group_id=1000 + 124 16 0:37 / /sys/fs/fuse/connections rw,relatime shared:107 - fusectl fusectl rw + 165 38 253:3 / /tmp/mnt rw,relatime shared:147 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered + 167 35 253:15 / /var/lib/docker/devicemapper/mnt/aae4076022f0e2b80a2afbf8fc6df450c52080191fcef7fb679a73e6f073e5c2 rw,relatime shared:149 - ext4 /dev/mapper/docker-253:2-425882-aae4076022f0e2b80a2afbf8fc6df450c52080191fcef7fb679a73e6f073e5c2 rw,seclabel,discard,stripe=16,data=ordered + 171 35 253:16 / /var/lib/docker/devicemapper/mnt/c71be651f114db95180e472f7871b74fa597ee70a58ccc35cb87139ddea15373 rw,relatime shared:153 - ext4 /dev/mapper/docker-253:2-425882-c71be651f114db95180e472f7871b74fa597ee70a58ccc35cb87139ddea15373 rw,seclabel,discard,stripe=16,data=ordered + 175 35 253:17 / /var/lib/docker/devicemapper/mnt/1bac6ab72862d2d5626560df6197cf12036b82e258c53d981fa29adce6f06c3c rw,relatime shared:157 - ext4 /dev/mapper/docker-253:2-425882-1bac6ab72862d2d5626560df6197cf12036b82e258c53d981fa29adce6f06c3c rw,seclabel,discard,stripe=16,data=ordered + 179 35 253:18 / /var/lib/docker/devicemapper/mnt/d710a357d77158e80d5b2c55710ae07c94e76d34d21ee7bae65ce5418f739b09 rw,relatime shared:161 - ext4 /dev/mapper/docker-253:2-425882-d710a357d77158e80d5b2c55710ae07c94e76d34d21ee7bae65ce5418f739b09 rw,seclabel,discard,stripe=16,data=ordered + 183 35 253:19 / /var/lib/docker/devicemapper/mnt/6479f52366114d5f518db6837254baab48fab39f2ac38d5099250e9a6ceae6c7 rw,relatime shared:165 - ext4 /dev/mapper/docker-253:2-425882-6479f52366114d5f518db6837254baab48fab39f2ac38d5099250e9a6ceae6c7 rw,seclabel,discard,stripe=16,data=ordered + 187 35 253:20 / /var/lib/docker/devicemapper/mnt/8d9df91c4cca5aef49eeb2725292aab324646f723a7feab56be34c2ad08268e1 rw,relatime shared:169 - ext4 /dev/mapper/docker-253:2-425882-8d9df91c4cca5aef49eeb2725292aab324646f723a7feab56be34c2ad08268e1 rw,seclabel,discard,stripe=16,data=ordered + 191 35 253:21 / /var/lib/docker/devicemapper/mnt/c8240b768603d32e920d365dc9d1dc2a6af46cd23e7ae819947f969e1b4ec661 rw,relatime shared:173 - ext4 /dev/mapper/docker-253:2-425882-c8240b768603d32e920d365dc9d1dc2a6af46cd23e7ae819947f969e1b4ec661 rw,seclabel,discard,stripe=16,data=ordered + 195 35 253:22 / /var/lib/docker/devicemapper/mnt/2eb3a01278380bbf3ed12d86ac629eaa70a4351301ee307a5cabe7b5f3b1615f rw,relatime shared:177 - ext4 /dev/mapper/docker-253:2-425882-2eb3a01278380bbf3ed12d86ac629eaa70a4351301ee307a5cabe7b5f3b1615f rw,seclabel,discard,stripe=16,data=ordered + 199 35 253:23 / /var/lib/docker/devicemapper/mnt/37a17fb7c9d9b80821235d5f2662879bd3483915f245f9b49cdaa0e38779b70b rw,relatime shared:181 - ext4 /dev/mapper/docker-253:2-425882-37a17fb7c9d9b80821235d5f2662879bd3483915f245f9b49cdaa0e38779b70b rw,seclabel,discard,stripe=16,data=ordered + 203 35 253:24 / /var/lib/docker/devicemapper/mnt/aea459ae930bf1de913e2f29428fd80ee678a1e962d4080019d9f9774331ee2b rw,relatime shared:185 - ext4 /dev/mapper/docker-253:2-425882-aea459ae930bf1de913e2f29428fd80ee678a1e962d4080019d9f9774331ee2b rw,seclabel,discard,stripe=16,data=ordered + 207 35 253:25 / /var/lib/docker/devicemapper/mnt/928ead0bc06c454bd9f269e8585aeae0a6bd697f46dc8754c2a91309bc810882 rw,relatime shared:189 - ext4 /dev/mapper/docker-253:2-425882-928ead0bc06c454bd9f269e8585aeae0a6bd697f46dc8754c2a91309bc810882 rw,seclabel,discard,stripe=16,data=ordered + 211 35 253:26 / /var/lib/docker/devicemapper/mnt/0f284d18481d671644706e7a7244cbcf63d590d634cc882cb8721821929d0420 rw,relatime shared:193 - ext4 /dev/mapper/docker-253:2-425882-0f284d18481d671644706e7a7244cbcf63d590d634cc882cb8721821929d0420 rw,seclabel,discard,stripe=16,data=ordered + 215 35 253:27 / /var/lib/docker/devicemapper/mnt/d9dd16722ab34c38db2733e23f69e8f4803ce59658250dd63e98adff95d04919 rw,relatime shared:197 - ext4 /dev/mapper/docker-253:2-425882-d9dd16722ab34c38db2733e23f69e8f4803ce59658250dd63e98adff95d04919 rw,seclabel,discard,stripe=16,data=ordered + 219 35 253:28 / /var/lib/docker/devicemapper/mnt/bc4500479f18c2c08c21ad5282e5f826a016a386177d9874c2764751c031d634 rw,relatime shared:201 - ext4 /dev/mapper/docker-253:2-425882-bc4500479f18c2c08c21ad5282e5f826a016a386177d9874c2764751c031d634 rw,seclabel,discard,stripe=16,data=ordered + 223 35 253:29 / /var/lib/docker/devicemapper/mnt/7770c8b24eb3d5cc159a065910076938910d307ab2f5d94e1dc3b24c06ee2c8a rw,relatime shared:205 - ext4 /dev/mapper/docker-253:2-425882-7770c8b24eb3d5cc159a065910076938910d307ab2f5d94e1dc3b24c06ee2c8a rw,seclabel,discard,stripe=16,data=ordered + 227 35 253:30 / /var/lib/docker/devicemapper/mnt/c280cd3d0bf0aa36b478b292279671624cceafc1a67eaa920fa1082601297adf rw,relatime shared:209 - ext4 /dev/mapper/docker-253:2-425882-c280cd3d0bf0aa36b478b292279671624cceafc1a67eaa920fa1082601297adf rw,seclabel,discard,stripe=16,data=ordered + 231 35 253:31 / /var/lib/docker/devicemapper/mnt/8b59a7d9340279f09fea67fd6ad89ddef711e9e7050eb647984f8b5ef006335f rw,relatime shared:213 - ext4 /dev/mapper/docker-253:2-425882-8b59a7d9340279f09fea67fd6ad89ddef711e9e7050eb647984f8b5ef006335f rw,seclabel,discard,stripe=16,data=ordered + 235 35 253:32 / /var/lib/docker/devicemapper/mnt/1a28059f29eda821578b1bb27a60cc71f76f846a551abefabce6efd0146dce9f rw,relatime shared:217 - ext4 /dev/mapper/docker-253:2-425882-1a28059f29eda821578b1bb27a60cc71f76f846a551abefabce6efd0146dce9f rw,seclabel,discard,stripe=16,data=ordered + 239 35 253:33 / /var/lib/docker/devicemapper/mnt/e9aa60c60128cad1 rw,relatime shared:221 - ext4 /dev/mapper/docker-253:2-425882-e9aa60c60128cad1 rw,seclabel,discard,stripe=16,data=ordered + 243 35 253:34 / /var/lib/docker/devicemapper/mnt/5fec11304b6f4713fea7b6ccdcc1adc0a1966187f590fe25a8227428a8df275d-init rw,relatime shared:225 - ext4 /dev/mapper/docker-253:2-425882-5fec11304b6f4713fea7b6ccdcc1adc0a1966187f590fe25a8227428a8df275d-init rw,seclabel,discard,stripe=16,data=ordered + 247 35 253:35 / /var/lib/docker/devicemapper/mnt/5fec11304b6f4713fea7b6ccdcc1adc0a1966187f590fe25a8227428a8df275d rw,relatime shared:229 - ext4 /dev/mapper/docker-253:2-425882-5fec11304b6f4713fea7b6ccdcc1adc0a1966187f590fe25a8227428a8df275d rw,seclabel,discard,stripe=16,data=ordered + 31 21 0:23 / /DATA/foo_bla_bla rw,relatime - cifs //foo/BLA\040BLA\040BLA/ rw,sec=ntlm,cache=loose,unc=\\foo\BLA BLA BLA,username=my_login,domain=mydomain.com,uid=12345678,forceuid,gid=12345678,forcegid,addr=10.1.30.10,file_mode=0755,dir_mode=0755,nounix,rsize=61440,wsize=65536,actimeo=1` + + ubuntuMountInfo = `15 20 0:14 / /sys rw,nosuid,nodev,noexec,relatime - sysfs sysfs rw +16 20 0:3 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1015140k,nr_inodes=253785,mode=755 +18 17 0:11 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +19 20 0:15 / /run rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=205044k,mode=755 +20 1 253:0 / / rw,relatime - ext4 /dev/disk/by-label/DOROOT rw,errors=remount-ro,data=ordered +21 15 0:16 / /sys/fs/cgroup rw,relatime - tmpfs none rw,size=4k,mode=755 +22 15 0:17 / /sys/fs/fuse/connections rw,relatime - fusectl none rw +23 15 0:6 / /sys/kernel/debug rw,relatime - debugfs none rw +24 15 0:10 / /sys/kernel/security rw,relatime - securityfs none rw +25 19 0:18 / /run/lock rw,nosuid,nodev,noexec,relatime - tmpfs none rw,size=5120k +26 21 0:19 / /sys/fs/cgroup/cpuset rw,relatime - cgroup cgroup rw,cpuset,clone_children +27 19 0:20 / /run/shm rw,nosuid,nodev,relatime - tmpfs none rw +28 21 0:21 / /sys/fs/cgroup/cpu rw,relatime - cgroup cgroup rw,cpu +29 19 0:22 / /run/user rw,nosuid,nodev,noexec,relatime - tmpfs none rw,size=102400k,mode=755 +30 15 0:23 / /sys/fs/pstore rw,relatime - pstore none rw +31 21 0:24 / /sys/fs/cgroup/cpuacct rw,relatime - cgroup cgroup rw,cpuacct +32 21 0:25 / /sys/fs/cgroup/memory rw,relatime - cgroup cgroup rw,memory +33 21 0:26 / /sys/fs/cgroup/devices rw,relatime - cgroup cgroup rw,devices +34 21 0:27 / /sys/fs/cgroup/freezer rw,relatime - cgroup cgroup rw,freezer +35 21 0:28 / /sys/fs/cgroup/blkio rw,relatime - cgroup cgroup rw,blkio +36 21 0:29 / /sys/fs/cgroup/perf_event rw,relatime - cgroup cgroup rw,perf_event +37 21 0:30 / /sys/fs/cgroup/hugetlb rw,relatime - cgroup cgroup rw,hugetlb +38 21 0:31 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime - cgroup systemd rw,name=systemd +39 20 0:32 / /var/lib/docker/aufs/mnt/b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc rw,relatime - aufs none rw,si=caafa54fdc06525 +40 20 0:33 / /var/lib/docker/aufs/mnt/2eed44ac7ce7c75af04f088ed6cb4ce9d164801e91d78c6db65d7ef6d572bba8-init rw,relatime - aufs none rw,si=caafa54f882b525 +41 20 0:34 / /var/lib/docker/aufs/mnt/2eed44ac7ce7c75af04f088ed6cb4ce9d164801e91d78c6db65d7ef6d572bba8 rw,relatime - aufs none rw,si=caafa54f8829525 +42 20 0:35 / /var/lib/docker/aufs/mnt/16f4d7e96dd612903f425bfe856762f291ff2e36a8ecd55a2209b7d7cd81c30b rw,relatime - aufs none rw,si=caafa54f882d525 +43 20 0:36 / /var/lib/docker/aufs/mnt/63ca08b75d7438a9469a5954e003f48ffede73541f6286ce1cb4d7dd4811da7e-init rw,relatime - aufs none rw,si=caafa54f882f525 +44 20 0:37 / /var/lib/docker/aufs/mnt/63ca08b75d7438a9469a5954e003f48ffede73541f6286ce1cb4d7dd4811da7e rw,relatime - aufs none rw,si=caafa54f88ba525 +45 20 0:38 / /var/lib/docker/aufs/mnt/283f35a910233c756409313be71ecd8fcfef0df57108b8d740b61b3e88860452 rw,relatime - aufs none rw,si=caafa54f88b8525 +46 20 0:39 / /var/lib/docker/aufs/mnt/2c6c7253d4090faa3886871fb21bd660609daeb0206588c0602007f7d0f254b1-init rw,relatime - aufs none rw,si=caafa54f88be525 +47 20 0:40 / /var/lib/docker/aufs/mnt/2c6c7253d4090faa3886871fb21bd660609daeb0206588c0602007f7d0f254b1 rw,relatime - aufs none rw,si=caafa54f882c525 +48 20 0:41 / /var/lib/docker/aufs/mnt/de2b538c97d6366cc80e8658547c923ea1d042f85580df379846f36a4df7049d rw,relatime - aufs none rw,si=caafa54f85bb525 +49 20 0:42 / /var/lib/docker/aufs/mnt/94a3d8ed7c27e5b0aa71eba46c736bfb2742afda038e74f2dd6035fb28415b49-init rw,relatime - aufs none rw,si=caafa54fdc00525 +50 20 0:43 / /var/lib/docker/aufs/mnt/94a3d8ed7c27e5b0aa71eba46c736bfb2742afda038e74f2dd6035fb28415b49 rw,relatime - aufs none rw,si=caafa54fbaec525 +51 20 0:44 / /var/lib/docker/aufs/mnt/6ac1cace985c9fc9bea32234de8b36dba49bdd5e29a2972b327ff939d78a6274 rw,relatime - aufs none rw,si=caafa54f8e1a525 +52 20 0:45 / /var/lib/docker/aufs/mnt/dff147033e3a0ef061e1de1ad34256b523d4a8c1fa6bba71a0ab538e8628ff0b-init rw,relatime - aufs none rw,si=caafa54f8e1d525 +53 20 0:46 / /var/lib/docker/aufs/mnt/dff147033e3a0ef061e1de1ad34256b523d4a8c1fa6bba71a0ab538e8628ff0b rw,relatime - aufs none rw,si=caafa54f8e1b525 +54 20 0:47 / /var/lib/docker/aufs/mnt/cabb117d997f0f93519185aea58389a9762770b7496ed0b74a3e4a083fa45902 rw,relatime - aufs none rw,si=caafa54f810a525 +55 20 0:48 / /var/lib/docker/aufs/mnt/e1c8a94ffaa9d532bbbdc6ef771ce8a6c2c06757806ecaf8b68e9108fec65f33-init rw,relatime - aufs none rw,si=caafa54f8529525 +56 20 0:49 / /var/lib/docker/aufs/mnt/e1c8a94ffaa9d532bbbdc6ef771ce8a6c2c06757806ecaf8b68e9108fec65f33 rw,relatime - aufs none rw,si=caafa54f852f525 +57 20 0:50 / /var/lib/docker/aufs/mnt/16a1526fa445b84ce84f89506d219e87fa488a814063baf045d88b02f21166b3 rw,relatime - aufs none rw,si=caafa54f9e1d525 +58 20 0:51 / /var/lib/docker/aufs/mnt/57b9c92e1e368fa7dbe5079f7462e917777829caae732828b003c355fe49da9f-init rw,relatime - aufs none rw,si=caafa54f854d525 +59 20 0:52 / /var/lib/docker/aufs/mnt/57b9c92e1e368fa7dbe5079f7462e917777829caae732828b003c355fe49da9f rw,relatime - aufs none rw,si=caafa54f854e525 +60 20 0:53 / /var/lib/docker/aufs/mnt/e370c3e286bea027917baa0e4d251262681a472a87056e880dfd0513516dffd9 rw,relatime - aufs none rw,si=caafa54f840a525 +61 20 0:54 / /var/lib/docker/aufs/mnt/6b00d3b4f32b41997ec07412b5e18204f82fbe643e7122251cdeb3582abd424e-init rw,relatime - aufs none rw,si=caafa54f8408525 +62 20 0:55 / /var/lib/docker/aufs/mnt/6b00d3b4f32b41997ec07412b5e18204f82fbe643e7122251cdeb3582abd424e rw,relatime - aufs none rw,si=caafa54f8409525 +63 20 0:56 / /var/lib/docker/aufs/mnt/abd0b5ea5d355a67f911475e271924a5388ee60c27185fcd60d095afc4a09dc7 rw,relatime - aufs none rw,si=caafa54f9eb1525 +64 20 0:57 / /var/lib/docker/aufs/mnt/336222effc3f7b89867bb39ff7792ae5412c35c749f127c29159d046b6feedd2-init rw,relatime - aufs none rw,si=caafa54f85bf525 +65 20 0:58 / /var/lib/docker/aufs/mnt/336222effc3f7b89867bb39ff7792ae5412c35c749f127c29159d046b6feedd2 rw,relatime - aufs none rw,si=caafa54f85b8525 +66 20 0:59 / /var/lib/docker/aufs/mnt/912e1bf28b80a09644503924a8a1a4fb8ed10b808ca847bda27a369919aa52fa rw,relatime - aufs none rw,si=caafa54fbaea525 +67 20 0:60 / /var/lib/docker/aufs/mnt/386f722875013b4a875118367abc783fc6617a3cb7cf08b2b4dcf550b4b9c576-init rw,relatime - aufs none rw,si=caafa54f8472525 +68 20 0:61 / /var/lib/docker/aufs/mnt/386f722875013b4a875118367abc783fc6617a3cb7cf08b2b4dcf550b4b9c576 rw,relatime - aufs none rw,si=caafa54f8474525 +69 20 0:62 / /var/lib/docker/aufs/mnt/5aaebb79ef3097dfca377889aeb61a0c9d5e3795117d2b08d0751473c671dfb2 rw,relatime - aufs none rw,si=caafa54f8c5e525 +70 20 0:63 / /var/lib/docker/aufs/mnt/5ba3e493279d01277d583600b81c7c079e691b73c3a2bdea8e4b12a35a418be2-init rw,relatime - aufs none rw,si=caafa54f8c3b525 +71 20 0:64 / /var/lib/docker/aufs/mnt/5ba3e493279d01277d583600b81c7c079e691b73c3a2bdea8e4b12a35a418be2 rw,relatime - aufs none rw,si=caafa54f8c3d525 +72 20 0:65 / /var/lib/docker/aufs/mnt/2777f0763da4de93f8bebbe1595cc77f739806a158657b033eca06f827b6028a rw,relatime - aufs none rw,si=caafa54f8c3e525 +73 20 0:66 / /var/lib/docker/aufs/mnt/5d7445562acf73c6f0ae34c3dd0921d7457de1ba92a587d9e06a44fa209eeb3e-init rw,relatime - aufs none rw,si=caafa54f8c39525 +74 20 0:67 / /var/lib/docker/aufs/mnt/5d7445562acf73c6f0ae34c3dd0921d7457de1ba92a587d9e06a44fa209eeb3e rw,relatime - aufs none rw,si=caafa54f854f525 +75 20 0:68 / /var/lib/docker/aufs/mnt/06400b526ec18b66639c96efc41a84f4ae0b117cb28dafd56be420651b4084a0 rw,relatime - aufs none rw,si=caafa54f840b525 +76 20 0:69 / /var/lib/docker/aufs/mnt/e051d45ec42d8e3e1cc57bb39871a40de486dc123522e9c067fbf2ca6a357785-init rw,relatime - aufs none rw,si=caafa54fdddf525 +77 20 0:70 / /var/lib/docker/aufs/mnt/e051d45ec42d8e3e1cc57bb39871a40de486dc123522e9c067fbf2ca6a357785 rw,relatime - aufs none rw,si=caafa54f854b525 +78 20 0:71 / /var/lib/docker/aufs/mnt/1ff414fa93fd61ec81b0ab7b365a841ff6545accae03cceac702833aaeaf718f rw,relatime - aufs none rw,si=caafa54f8d85525 +79 20 0:72 / /var/lib/docker/aufs/mnt/c661b2f871dd5360e46a2aebf8f970f6d39a2ff64e06979aa0361227c88128b8-init rw,relatime - aufs none rw,si=caafa54f8da3525 +80 20 0:73 / /var/lib/docker/aufs/mnt/c661b2f871dd5360e46a2aebf8f970f6d39a2ff64e06979aa0361227c88128b8 rw,relatime - aufs none rw,si=caafa54f8da2525 +81 20 0:74 / /var/lib/docker/aufs/mnt/b68b1d4fe4d30016c552398e78b379a39f651661d8e1fa5f2460c24a5e723420 rw,relatime - aufs none rw,si=caafa54f8d81525 +82 20 0:75 / /var/lib/docker/aufs/mnt/c5c5979c936cd0153a4c626fa9d69ce4fce7d924cc74fa68b025d2f585031739-init rw,relatime - aufs none rw,si=caafa54f8da1525 +83 20 0:76 / /var/lib/docker/aufs/mnt/c5c5979c936cd0153a4c626fa9d69ce4fce7d924cc74fa68b025d2f585031739 rw,relatime - aufs none rw,si=caafa54f8da0525 +84 20 0:77 / /var/lib/docker/aufs/mnt/53e10b0329afc0e0d3322d31efaed4064139dc7027fe6ae445cffd7104bcc94f rw,relatime - aufs none rw,si=caafa54f8c35525 +85 20 0:78 / /var/lib/docker/aufs/mnt/3bfafd09ff2603e2165efacc2215c1f51afabba6c42d04a68cc2df0e8cc31494-init rw,relatime - aufs none rw,si=caafa54f8db8525 +86 20 0:79 / /var/lib/docker/aufs/mnt/3bfafd09ff2603e2165efacc2215c1f51afabba6c42d04a68cc2df0e8cc31494 rw,relatime - aufs none rw,si=caafa54f8dba525 +87 20 0:80 / /var/lib/docker/aufs/mnt/90fdd2c03eeaf65311f88f4200e18aef6d2772482712d9aea01cd793c64781b5 rw,relatime - aufs none rw,si=caafa54f8315525 +88 20 0:81 / /var/lib/docker/aufs/mnt/7bdf2591c06c154ceb23f5e74b1d03b18fbf6fe96e35fbf539b82d446922442f-init rw,relatime - aufs none rw,si=caafa54f8fc6525 +89 20 0:82 / /var/lib/docker/aufs/mnt/7bdf2591c06c154ceb23f5e74b1d03b18fbf6fe96e35fbf539b82d446922442f rw,relatime - aufs none rw,si=caafa54f8468525 +90 20 0:83 / /var/lib/docker/aufs/mnt/8cf9a993f50f3305abad3da268c0fc44ff78a1e7bba595ef9de963497496c3f9 rw,relatime - aufs none rw,si=caafa54f8c59525 +91 20 0:84 / /var/lib/docker/aufs/mnt/ecc896fd74b21840a8d35e8316b92a08b1b9c83d722a12acff847e9f0ff17173-init rw,relatime - aufs none rw,si=caafa54f846a525 +92 20 0:85 / /var/lib/docker/aufs/mnt/ecc896fd74b21840a8d35e8316b92a08b1b9c83d722a12acff847e9f0ff17173 rw,relatime - aufs none rw,si=caafa54f846b525 +93 20 0:86 / /var/lib/docker/aufs/mnt/d8c8288ec920439a48b5796bab5883ee47a019240da65e8d8f33400c31bac5df rw,relatime - aufs none rw,si=caafa54f8dbf525 +94 20 0:87 / /var/lib/docker/aufs/mnt/ecba66710bcd03199b9398e46c005cd6b68d0266ec81dc8b722a29cc417997c6-init rw,relatime - aufs none rw,si=caafa54f810f525 +95 20 0:88 / /var/lib/docker/aufs/mnt/ecba66710bcd03199b9398e46c005cd6b68d0266ec81dc8b722a29cc417997c6 rw,relatime - aufs none rw,si=caafa54fbae9525 +96 20 0:89 / /var/lib/docker/aufs/mnt/befc1c67600df449dddbe796c0d06da7caff1d2bbff64cde1f0ba82d224996b5 rw,relatime - aufs none rw,si=caafa54f8dab525 +97 20 0:90 / /var/lib/docker/aufs/mnt/c9f470e73d2742629cdc4084a1b2c1a8302914f2aa0d0ec4542371df9a050562-init rw,relatime - aufs none rw,si=caafa54fdc02525 +98 20 0:91 / /var/lib/docker/aufs/mnt/c9f470e73d2742629cdc4084a1b2c1a8302914f2aa0d0ec4542371df9a050562 rw,relatime - aufs none rw,si=caafa54f9eb0525 +99 20 0:92 / /var/lib/docker/aufs/mnt/2a31f10029f04ff9d4381167a9b739609853d7220d55a56cb654779a700ee246 rw,relatime - aufs none rw,si=caafa54f8c37525 +100 20 0:93 / /var/lib/docker/aufs/mnt/8c4261b8e3e4b21ebba60389bd64b6261217e7e6b9fd09e201d5a7f6760f6927-init rw,relatime - aufs none rw,si=caafa54fd173525 +101 20 0:94 / /var/lib/docker/aufs/mnt/8c4261b8e3e4b21ebba60389bd64b6261217e7e6b9fd09e201d5a7f6760f6927 rw,relatime - aufs none rw,si=caafa54f8108525 +102 20 0:95 / /var/lib/docker/aufs/mnt/eaa0f57403a3dc685268f91df3fbcd7a8423cee50e1a9ee5c3e1688d9d676bb4 rw,relatime - aufs none rw,si=caafa54f852d525 +103 20 0:96 / /var/lib/docker/aufs/mnt/9cfe69a2cbffd9bfc7f396d4754f6fe5cc457ef417b277797be3762dfe955a6b-init rw,relatime - aufs none rw,si=caafa54f8d80525 +104 20 0:97 / /var/lib/docker/aufs/mnt/9cfe69a2cbffd9bfc7f396d4754f6fe5cc457ef417b277797be3762dfe955a6b rw,relatime - aufs none rw,si=caafa54f8fc3525 +105 20 0:98 / /var/lib/docker/aufs/mnt/d1b322ae17613c6adee84e709641a9244ac56675244a89a64dc0075075fcbb83 rw,relatime - aufs none rw,si=caafa54f8c58525 +106 20 0:99 / /var/lib/docker/aufs/mnt/d46c2a8e9da7e91ab34fd9c192851c246a4e770a46720bda09e55c7554b9dbbd-init rw,relatime - aufs none rw,si=caafa54f8c63525 +107 20 0:100 / /var/lib/docker/aufs/mnt/d46c2a8e9da7e91ab34fd9c192851c246a4e770a46720bda09e55c7554b9dbbd rw,relatime - aufs none rw,si=caafa54f8c67525 +108 20 0:101 / /var/lib/docker/aufs/mnt/bc9d2a264158f83a617a069bf17cbbf2a2ba453db7d3951d9dc63cc1558b1c2b rw,relatime - aufs none rw,si=caafa54f8dbe525 +109 20 0:102 / /var/lib/docker/aufs/mnt/9e6abb8d72bbeb4d5cf24b96018528015ba830ce42b4859965bd482cbd034e99-init rw,relatime - aufs none rw,si=caafa54f9e0d525 +110 20 0:103 / /var/lib/docker/aufs/mnt/9e6abb8d72bbeb4d5cf24b96018528015ba830ce42b4859965bd482cbd034e99 rw,relatime - aufs none rw,si=caafa54f9e1b525 +111 20 0:104 / /var/lib/docker/aufs/mnt/d4dca7b02569c732e740071e1c654d4ad282de5c41edb619af1f0aafa618be26 rw,relatime - aufs none rw,si=caafa54f8dae525 +112 20 0:105 / /var/lib/docker/aufs/mnt/fea63da40fa1c5ffbad430dde0bc64a8fc2edab09a051fff55b673c40a08f6b7-init rw,relatime - aufs none rw,si=caafa54f8c5c525 +113 20 0:106 / /var/lib/docker/aufs/mnt/fea63da40fa1c5ffbad430dde0bc64a8fc2edab09a051fff55b673c40a08f6b7 rw,relatime - aufs none rw,si=caafa54fd172525 +114 20 0:107 / /var/lib/docker/aufs/mnt/e60c57499c0b198a6734f77f660cdbbd950a5b78aa23f470ca4f0cfcc376abef rw,relatime - aufs none rw,si=caafa54909c4525 +115 20 0:108 / /var/lib/docker/aufs/mnt/099c78e7ccd9c8717471bb1bbfff838c0a9913321ba2f214fbeaf92c678e5b35-init rw,relatime - aufs none rw,si=caafa54909c3525 +116 20 0:109 / /var/lib/docker/aufs/mnt/099c78e7ccd9c8717471bb1bbfff838c0a9913321ba2f214fbeaf92c678e5b35 rw,relatime - aufs none rw,si=caafa54909c7525 +117 20 0:110 / /var/lib/docker/aufs/mnt/2997be666d58b9e71469759bcb8bd9608dad0e533a1a7570a896919ba3388825 rw,relatime - aufs none rw,si=caafa54f8557525 +118 20 0:111 / /var/lib/docker/aufs/mnt/730694eff438ef20569df38dfb38a920969d7ff2170cc9aa7cb32a7ed8147a93-init rw,relatime - aufs none rw,si=caafa54c6e88525 +119 20 0:112 / /var/lib/docker/aufs/mnt/730694eff438ef20569df38dfb38a920969d7ff2170cc9aa7cb32a7ed8147a93 rw,relatime - aufs none rw,si=caafa54c6e8e525 +120 20 0:113 / /var/lib/docker/aufs/mnt/a672a1e2f2f051f6e19ed1dfbe80860a2d774174c49f7c476695f5dd1d5b2f67 rw,relatime - aufs none rw,si=caafa54c6e15525 +121 20 0:114 / /var/lib/docker/aufs/mnt/aba3570e17859f76cf29d282d0d150659c6bd80780fdc52a465ba05245c2a420-init rw,relatime - aufs none rw,si=caafa54f8dad525 +122 20 0:115 / /var/lib/docker/aufs/mnt/aba3570e17859f76cf29d282d0d150659c6bd80780fdc52a465ba05245c2a420 rw,relatime - aufs none rw,si=caafa54f8d84525 +123 20 0:116 / /var/lib/docker/aufs/mnt/2abc86007aca46fb4a817a033e2a05ccacae40b78ea4b03f8ea616b9ada40e2e rw,relatime - aufs none rw,si=caafa54c6e8b525 +124 20 0:117 / /var/lib/docker/aufs/mnt/36352f27f7878e648367a135bd1ec3ed497adcb8ac13577ee892a0bd921d2374-init rw,relatime - aufs none rw,si=caafa54c6e8d525 +125 20 0:118 / /var/lib/docker/aufs/mnt/36352f27f7878e648367a135bd1ec3ed497adcb8ac13577ee892a0bd921d2374 rw,relatime - aufs none rw,si=caafa54f8c34525 +126 20 0:119 / /var/lib/docker/aufs/mnt/2f95ca1a629cea8363b829faa727dd52896d5561f2c96ddee4f697ea2fc872c2 rw,relatime - aufs none rw,si=caafa54c6e8a525 +127 20 0:120 / /var/lib/docker/aufs/mnt/f108c8291654f179ef143a3e07de2b5a34adbc0b28194a0ab17742b6db9a7fb2-init rw,relatime - aufs none rw,si=caafa54f8e19525 +128 20 0:121 / /var/lib/docker/aufs/mnt/f108c8291654f179ef143a3e07de2b5a34adbc0b28194a0ab17742b6db9a7fb2 rw,relatime - aufs none rw,si=caafa54fa8c6525 +129 20 0:122 / /var/lib/docker/aufs/mnt/c1d04dfdf8cccb3676d5a91e84e9b0781ce40623d127d038bcfbe4c761b27401 rw,relatime - aufs none rw,si=caafa54f8c30525 +130 20 0:123 / /var/lib/docker/aufs/mnt/3f4898ffd0e1239aeebf1d1412590cdb7254207fa3883663e2c40cf772e5f05a-init rw,relatime - aufs none rw,si=caafa54c6e1a525 +131 20 0:124 / /var/lib/docker/aufs/mnt/3f4898ffd0e1239aeebf1d1412590cdb7254207fa3883663e2c40cf772e5f05a rw,relatime - aufs none rw,si=caafa54c6e1c525 +132 20 0:125 / /var/lib/docker/aufs/mnt/5ae3b6fccb1539fc02d420e86f3e9637bef5b711fed2ca31a2f426c8f5deddbf rw,relatime - aufs none rw,si=caafa54c4fea525 +133 20 0:126 / /var/lib/docker/aufs/mnt/310bfaf80d57020f2e73b06aeffb0b9b0ca2f54895f88bf5e4d1529ccac58fe0-init rw,relatime - aufs none rw,si=caafa54c6e1e525 +134 20 0:127 / /var/lib/docker/aufs/mnt/310bfaf80d57020f2e73b06aeffb0b9b0ca2f54895f88bf5e4d1529ccac58fe0 rw,relatime - aufs none rw,si=caafa54fa8c0525 +135 20 0:128 / /var/lib/docker/aufs/mnt/f382bd5aaccaf2d04a59089ac7cb12ec87efd769fd0c14d623358fbfd2a3f896 rw,relatime - aufs none rw,si=caafa54c4fec525 +136 20 0:129 / /var/lib/docker/aufs/mnt/50d45e9bb2d779bc6362824085564c7578c231af5ae3b3da116acf7e17d00735-init rw,relatime - aufs none rw,si=caafa54c4fef525 +137 20 0:130 / /var/lib/docker/aufs/mnt/50d45e9bb2d779bc6362824085564c7578c231af5ae3b3da116acf7e17d00735 rw,relatime - aufs none rw,si=caafa54c4feb525 +138 20 0:131 / /var/lib/docker/aufs/mnt/a9c5ee0854dc083b6bf62b7eb1e5291aefbb10702289a446471ce73aba0d5d7d rw,relatime - aufs none rw,si=caafa54909c6525 +139 20 0:134 / /var/lib/docker/aufs/mnt/03a613e7bd5078819d1fd92df4e671c0127559a5e0b5a885cc8d5616875162f0-init rw,relatime - aufs none rw,si=caafa54804fe525 +140 20 0:135 / /var/lib/docker/aufs/mnt/03a613e7bd5078819d1fd92df4e671c0127559a5e0b5a885cc8d5616875162f0 rw,relatime - aufs none rw,si=caafa54804fa525 +141 20 0:136 / /var/lib/docker/aufs/mnt/7ec3277e5c04c907051caf9c9c35889f5fcd6463e5485971b25404566830bb70 rw,relatime - aufs none rw,si=caafa54804f9525 +142 20 0:139 / /var/lib/docker/aufs/mnt/26b5b5d71d79a5b2bfcf8bc4b2280ee829f261eb886745dd90997ed410f7e8b8-init rw,relatime - aufs none rw,si=caafa54c6ef6525 +143 20 0:140 / /var/lib/docker/aufs/mnt/26b5b5d71d79a5b2bfcf8bc4b2280ee829f261eb886745dd90997ed410f7e8b8 rw,relatime - aufs none rw,si=caafa54c6ef5525 +144 20 0:356 / /var/lib/docker/aufs/mnt/e6ecde9e2c18cd3c75f424c67b6d89685cfee0fc67abf2cb6bdc0867eb998026 rw,relatime - aufs none rw,si=caafa548068e525` + + gentooMountinfo = `15 1 8:6 / / rw,noatime,nodiratime - ext4 /dev/sda6 rw,data=ordered +16 15 0:3 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +17 15 0:14 / /run rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=3292172k,mode=755 +18 15 0:5 / /dev rw,nosuid,relatime - devtmpfs udev rw,size=10240k,nr_inodes=4106451,mode=755 +19 18 0:12 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +20 18 0:10 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +21 18 0:15 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw +22 15 0:16 / /sys rw,nosuid,nodev,noexec,relatime - sysfs sysfs rw +23 22 0:7 / /sys/kernel/debug rw,nosuid,nodev,noexec,relatime - debugfs debugfs rw +24 22 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs cgroup_root rw,size=10240k,mode=755 +25 24 0:18 / /sys/fs/cgroup/openrc rw,nosuid,nodev,noexec,relatime - cgroup openrc rw,release_agent=/lib64/rc/sh/cgroup-release-agent.sh,name=openrc +26 24 0:19 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime - cgroup cpuset rw,cpuset,clone_children +27 24 0:20 / /sys/fs/cgroup/cpu rw,nosuid,nodev,noexec,relatime - cgroup cpu rw,cpu,clone_children +28 24 0:21 / /sys/fs/cgroup/cpuacct rw,nosuid,nodev,noexec,relatime - cgroup cpuacct rw,cpuacct,clone_children +29 24 0:22 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime - cgroup memory rw,memory,clone_children +30 24 0:23 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime - cgroup devices rw,devices,clone_children +31 24 0:24 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime - cgroup freezer rw,freezer,clone_children +32 24 0:25 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime - cgroup blkio rw,blkio,clone_children +33 15 8:1 / /boot rw,noatime,nodiratime - vfat /dev/sda1 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +34 15 8:18 / /mnt/xfs rw,noatime,nodiratime - xfs /dev/sdb2 rw,attr2,inode64,noquota +35 15 0:26 / /tmp rw,relatime - tmpfs tmpfs rw +36 16 0:27 / /proc/sys/fs/binfmt_misc rw,nosuid,nodev,noexec,relatime - binfmt_misc binfmt_misc rw +42 15 0:33 / /var/lib/nfs/rpc_pipefs rw,relatime - rpc_pipefs rpc_pipefs rw +43 16 0:34 / /proc/fs/nfsd rw,nosuid,nodev,noexec,relatime - nfsd nfsd rw +44 15 0:35 / /home/tianon/.gvfs rw,nosuid,nodev,relatime - fuse.gvfs-fuse-daemon gvfs-fuse-daemon rw,user_id=1000,group_id=1000 +68 15 0:3336 / /var/lib/docker/aufs/mnt/3597a1a6d6298c1decc339ebb90aad6f7d6ba2e15af3131b1f85e7ee4787a0cd rw,relatime - aufs none rw,si=9b4a7640128db39c +86 68 8:6 /var/lib/docker/containers/3597a1a6d6298c1decc339ebb90aad6f7d6ba2e15af3131b1f85e7ee4787a0cd/config.env /var/lib/docker/aufs/mnt/3597a1a6d6298c1decc339ebb90aad6f7d6ba2e15af3131b1f85e7ee4787a0cd/.dockerenv rw,noatime,nodiratime - ext4 /dev/sda6 rw,data=ordered +87 68 8:6 /etc/resolv.conf /var/lib/docker/aufs/mnt/3597a1a6d6298c1decc339ebb90aad6f7d6ba2e15af3131b1f85e7ee4787a0cd/etc/resolv.conf rw,noatime,nodiratime - ext4 /dev/sda6 rw,data=ordered +88 68 8:6 /var/lib/docker/containers/3597a1a6d6298c1decc339ebb90aad6f7d6ba2e15af3131b1f85e7ee4787a0cd/hostname /var/lib/docker/aufs/mnt/3597a1a6d6298c1decc339ebb90aad6f7d6ba2e15af3131b1f85e7ee4787a0cd/etc/hostname rw,noatime,nodiratime - ext4 /dev/sda6 rw,data=ordered +89 68 8:6 /var/lib/docker/containers/3597a1a6d6298c1decc339ebb90aad6f7d6ba2e15af3131b1f85e7ee4787a0cd/hosts /var/lib/docker/aufs/mnt/3597a1a6d6298c1decc339ebb90aad6f7d6ba2e15af3131b1f85e7ee4787a0cd/etc/hosts rw,noatime,nodiratime - ext4 /dev/sda6 rw,data=ordered +38 15 0:3384 / /var/lib/docker/aufs/mnt/0292005a9292401bb5197657f2b682d97d8edcb3b72b5e390d2a680139985b55 rw,relatime - aufs none rw,si=9b4a7642b584939c +39 15 0:3385 / /var/lib/docker/aufs/mnt/59db98c889de5f71b70cfb82c40cbe47b64332f0f56042a2987a9e5df6e5e3aa rw,relatime - aufs none rw,si=9b4a7642b584e39c +40 15 0:3386 / /var/lib/docker/aufs/mnt/0545f0f2b6548eb9601d08f35a08f5a0a385407d36027a28f58e06e9f61e0278 rw,relatime - aufs none rw,si=9b4a7642b584b39c +41 15 0:3387 / /var/lib/docker/aufs/mnt/d882cfa16d1aa8fe0331a36e79be3d80b151e49f24fc39a39c3fed1735d5feb5 rw,relatime - aufs none rw,si=9b4a76453040039c +45 15 0:3388 / /var/lib/docker/aufs/mnt/055ca3befcb1626e74f5344b3398724ff05c0de0e20021683d04305c9e70a3f6 rw,relatime - aufs none rw,si=9b4a76453040739c +46 15 0:3389 / /var/lib/docker/aufs/mnt/b899e4567a351745d4285e7f1c18fdece75d877deb3041981cd290be348b7aa6 rw,relatime - aufs none rw,si=9b4a7647def4039c +47 15 0:3390 / /var/lib/docker/aufs/mnt/067ca040292c58954c5129f953219accfae0d40faca26b4d05e76ca76a998f16 rw,relatime - aufs none rw,si=9b4a7647def4239c +48 15 0:3391 / /var/lib/docker/aufs/mnt/8c995e7cb6e5082742daeea720e340b021d288d25d92e0412c03d200df308a11 rw,relatime - aufs none rw,si=9b4a764479c1639c +49 15 0:3392 / /var/lib/docker/aufs/mnt/07cc54dfae5b45300efdacdd53cc72c01b9044956a86ce7bff42d087e426096d rw,relatime - aufs none rw,si=9b4a764479c1739c +50 15 0:3393 / /var/lib/docker/aufs/mnt/0a9c95cf4c589c05b06baa79150b0cc1d8e7102759fe3ce4afaabb8247ca4f85 rw,relatime - aufs none rw,si=9b4a7644059c839c +51 15 0:3394 / /var/lib/docker/aufs/mnt/468fa98cececcf4e226e8370f18f4f848d63faf287fb8321a07f73086441a3a0 rw,relatime - aufs none rw,si=9b4a7644059ca39c +52 15 0:3395 / /var/lib/docker/aufs/mnt/0b826192231c5ce066fffb5beff4397337b5fc19a377aa7c6282c7c0ce7f111f rw,relatime - aufs none rw,si=9b4a764479c1339c +53 15 0:3396 / /var/lib/docker/aufs/mnt/93b8ba1b772fbe79709b909c43ea4b2c30d712e53548f467db1ffdc7a384f196 rw,relatime - aufs none rw,si=9b4a7640798a739c +54 15 0:3397 / /var/lib/docker/aufs/mnt/0c0d0acfb506859b12ef18cdfef9ebed0b43a611482403564224bde9149d373c rw,relatime - aufs none rw,si=9b4a7640798a039c +55 15 0:3398 / /var/lib/docker/aufs/mnt/33648c39ab6c7c74af0243d6d6a81b052e9e25ad1e04b19892eb2dde013e358b rw,relatime - aufs none rw,si=9b4a7644b439b39c +56 15 0:3399 / /var/lib/docker/aufs/mnt/0c12bea97a1c958a3c739fb148536c1c89351d48e885ecda8f0499b5cc44407e rw,relatime - aufs none rw,si=9b4a7640798a239c +57 15 0:3400 / /var/lib/docker/aufs/mnt/ed443988ce125f172d7512e84a4de2627405990fd767a16adefa8ce700c19ce8 rw,relatime - aufs none rw,si=9b4a7644c8ed339c +59 15 0:3402 / /var/lib/docker/aufs/mnt/f61612c324ff3c924d3f7a82fb00a0f8d8f73c248c41897061949e9f5ab7e3b1 rw,relatime - aufs none rw,si=9b4a76442810c39c +60 15 0:3403 / /var/lib/docker/aufs/mnt/0f1ee55c6c4e25027b80de8e64b8b6fb542b3b41aa0caab9261da75752e22bfd rw,relatime - aufs none rw,si=9b4a76442810e39c +61 15 0:3404 / /var/lib/docker/aufs/mnt/956f6cc4af5785cb3ee6963dcbca668219437d9b28f513290b1453ac64a34f97 rw,relatime - aufs none rw,si=9b4a7644303ec39c +62 15 0:3405 / /var/lib/docker/aufs/mnt/1099769158c4b4773e2569e38024e8717e400f87a002c41d8cf47cb81b051ba6 rw,relatime - aufs none rw,si=9b4a7644303ee39c +63 15 0:3406 / /var/lib/docker/aufs/mnt/11890ceb98d4442595b676085cd7b21550ab85c5df841e0fba997ff54e3d522d rw,relatime - aufs none rw,si=9b4a7644303ed39c +64 15 0:3407 / /var/lib/docker/aufs/mnt/acdb90dc378e8ed2420b43a6d291f1c789a081cd1904018780cc038fcd7aae53 rw,relatime - aufs none rw,si=9b4a76434be2139c +65 15 0:3408 / /var/lib/docker/aufs/mnt/120e716f19d4714fbe63cc1ed246204f2c1106eefebc6537ba2587d7e7711959 rw,relatime - aufs none rw,si=9b4a76434be2339c +66 15 0:3409 / /var/lib/docker/aufs/mnt/b197b7fffb61d89e0ba1c40de9a9fc0d912e778b3c1bd828cf981ff37c1963bc rw,relatime - aufs none rw,si=9b4a76434be2039c +70 15 0:3412 / /var/lib/docker/aufs/mnt/1434b69d2e1bb18a9f0b96b9cdac30132b2688f5d1379f68a39a5e120c2f93eb rw,relatime - aufs none rw,si=9b4a76434be2639c +71 15 0:3413 / /var/lib/docker/aufs/mnt/16006e83caf33ab5eb0cd6afc92ea2ee8edeff897496b0bb3ec3a75b767374b3 rw,relatime - aufs none rw,si=9b4a7644d790439c +72 15 0:3414 / /var/lib/docker/aufs/mnt/55bfa5f44e94d27f91f79ba901b118b15098449165c87abf1b53ffff147ff164 rw,relatime - aufs none rw,si=9b4a7644d790239c +73 15 0:3415 / /var/lib/docker/aufs/mnt/1912b97a07ab21ccd98a2a27bc779bf3cf364a3138afa3c3e6f7f169a3c3eab5 rw,relatime - aufs none rw,si=9b4a76441822739c +76 15 0:3418 / /var/lib/docker/aufs/mnt/1a7c3292e8879bd91ffd9282e954f643b1db5683093574c248ff14a9609f2f56 rw,relatime - aufs none rw,si=9b4a76438cb7239c +77 15 0:3419 / /var/lib/docker/aufs/mnt/bb1faaf0d076ddba82c2318305a85f490dafa4e8a8640a8db8ed657c439120cc rw,relatime - aufs none rw,si=9b4a76438cb7339c +78 15 0:3420 / /var/lib/docker/aufs/mnt/1ab869f21d2241a73ac840c7f988490313f909ac642eba71d092204fec66dd7c rw,relatime - aufs none rw,si=9b4a76438cb7639c +79 15 0:3421 / /var/lib/docker/aufs/mnt/fd7245b2cfe3890fa5f5b452260e4edf9e7fb7746532ed9d83f7a0d7dbaa610e rw,relatime - aufs none rw,si=9b4a7644bdc0139c +80 15 0:3422 / /var/lib/docker/aufs/mnt/1e5686c5301f26b9b3cd24e322c608913465cc6c5d0dcd7c5e498d1314747d61 rw,relatime - aufs none rw,si=9b4a7644bdc0639c +81 15 0:3423 / /var/lib/docker/aufs/mnt/52edf6ee6e40bfec1e9301a4d4a92ab83d144e2ae4ce5099e99df6138cb844bf rw,relatime - aufs none rw,si=9b4a7644bdc0239c +82 15 0:3424 / /var/lib/docker/aufs/mnt/1ea10fb7085d28cda4904657dff0454e52598d28e1d77e4f2965bbc3666e808f rw,relatime - aufs none rw,si=9b4a76438cb7139c +83 15 0:3425 / /var/lib/docker/aufs/mnt/9c03e98c3593946dbd4087f8d83f9ca262f4a2efdc952ce60690838b9ba6c526 rw,relatime - aufs none rw,si=9b4a76443020639c +84 15 0:3426 / /var/lib/docker/aufs/mnt/220a2344d67437602c6d2cee9a98c46be13f82c2a8063919dd2fad52bf2fb7dd rw,relatime - aufs none rw,si=9b4a76434bff339c +94 15 0:3427 / /var/lib/docker/aufs/mnt/3b32876c5b200312c50baa476ff342248e88c8ea96e6a1032cd53a88738a1cf2 rw,relatime - aufs none rw,si=9b4a76434bff139c +95 15 0:3428 / /var/lib/docker/aufs/mnt/23ee2b8b0d4ae8db6f6d1e168e2c6f79f8a18f953b09f65e0d22cc1e67a3a6fa rw,relatime - aufs none rw,si=9b4a7646c305c39c +96 15 0:3429 / /var/lib/docker/aufs/mnt/e86e6daa70b61b57945fa178222615f3c3d6bcef12c9f28e9f8623d44dc2d429 rw,relatime - aufs none rw,si=9b4a7646c305f39c +97 15 0:3430 / /var/lib/docker/aufs/mnt/2413d07623e80860bb2e9e306fbdee699afd07525785c025c591231e864aa162 rw,relatime - aufs none rw,si=9b4a76434bff039c +98 15 0:3431 / /var/lib/docker/aufs/mnt/adfd622eb22340fc80b429e5564b125668e260bf9068096c46dd59f1386a4b7d rw,relatime - aufs none rw,si=9b4a7646a7a1039c +102 15 0:3435 / /var/lib/docker/aufs/mnt/27cd92e7a91d02e2d6b44d16679a00fb6d169b19b88822891084e7fd1a84882d rw,relatime - aufs none rw,si=9b4a7646f25ec39c +103 15 0:3436 / /var/lib/docker/aufs/mnt/27dfdaf94cfbf45055c748293c37dd68d9140240bff4c646cb09216015914a88 rw,relatime - aufs none rw,si=9b4a7646732f939c +104 15 0:3437 / /var/lib/docker/aufs/mnt/5ed7524aff68dfbf0fc601cbaeac01bab14391850a973dabf3653282a627920f rw,relatime - aufs none rw,si=9b4a7646732f839c +105 15 0:3438 / /var/lib/docker/aufs/mnt/2a0d4767e536beb5785b60e071e3ac8e5e812613ab143a9627bee77d0c9ab062 rw,relatime - aufs none rw,si=9b4a7646732fe39c +106 15 0:3439 / /var/lib/docker/aufs/mnt/dea3fc045d9f4ae51ba952450b948a822cf85c39411489ca5224f6d9a8d02bad rw,relatime - aufs none rw,si=9b4a764012ad839c +107 15 0:3440 / /var/lib/docker/aufs/mnt/2d140a787160798da60cb67c21b1210054ad4dafecdcf832f015995b9aa99cfd rw,relatime - aufs none rw,si=9b4a764012add39c +108 15 0:3441 / /var/lib/docker/aufs/mnt/cb190b2a8e984475914430fbad2382e0d20b9b659f8ef83ae8d170cc672e519c rw,relatime - aufs none rw,si=9b4a76454d9c239c +109 15 0:3442 / /var/lib/docker/aufs/mnt/2f4a012d5a7ffd90256a6e9aa479054b3dddbc3c6a343f26dafbf3196890223b rw,relatime - aufs none rw,si=9b4a76454d9c439c +110 15 0:3443 / /var/lib/docker/aufs/mnt/63cc77904b80c4ffbf49cb974c5d8733dc52ad7640d3ae87554b325d7312d87f rw,relatime - aufs none rw,si=9b4a76454d9c339c +111 15 0:3444 / /var/lib/docker/aufs/mnt/30333e872c451482ea2d235ff2192e875bd234006b238ae2bdde3b91a86d7522 rw,relatime - aufs none rw,si=9b4a76422cebf39c +112 15 0:3445 / /var/lib/docker/aufs/mnt/6c54fc1125da3925cae65b5c9a98f3be55b0a2c2666082e5094a4ba71beb5bff rw,relatime - aufs none rw,si=9b4a7646dd5a439c +113 15 0:3446 / /var/lib/docker/aufs/mnt/3087d48cb01cda9d0a83a9ca301e6ea40e8593d18c4921be4794c91a420ab9a3 rw,relatime - aufs none rw,si=9b4a7646dd5a739c +114 15 0:3447 / /var/lib/docker/aufs/mnt/cc2607462a8f55b179a749b144c3fdbb50678e1a4f3065ea04e283e9b1f1d8e2 rw,relatime - aufs none rw,si=9b4a7646dd5a239c +117 15 0:3450 / /var/lib/docker/aufs/mnt/310c5e8392b29e8658a22e08d96d63936633b7e2c38e8d220047928b00a03d24 rw,relatime - aufs none rw,si=9b4a7647932d739c +118 15 0:3451 / /var/lib/docker/aufs/mnt/38a1f0029406ba9c3b6058f2f406d8a1d23c855046cf355c91d87d446fcc1460 rw,relatime - aufs none rw,si=9b4a76445abc939c +119 15 0:3452 / /var/lib/docker/aufs/mnt/42e109ab7914ae997a11ccd860fd18e4d488c50c044c3240423ce15774b8b62e rw,relatime - aufs none rw,si=9b4a76445abca39c +120 15 0:3453 / /var/lib/docker/aufs/mnt/365d832af0402d052b389c1e9c0d353b48487533d20cd4351df8e24ec4e4f9d8 rw,relatime - aufs none rw,si=9b4a7644066aa39c +121 15 0:3454 / /var/lib/docker/aufs/mnt/d3fa8a24d695b6cda9b64f96188f701963d28bef0473343f8b212df1a2cf1d2b rw,relatime - aufs none rw,si=9b4a7644066af39c +122 15 0:3455 / /var/lib/docker/aufs/mnt/37d4f491919abc49a15d0c7a7cc8383f087573525d7d288accd14f0b4af9eae0 rw,relatime - aufs none rw,si=9b4a7644066ad39c +123 15 0:3456 / /var/lib/docker/aufs/mnt/93902707fe12cbdd0068ce73f2baad4b3a299189b1b19cb5f8a2025e106ae3f5 rw,relatime - aufs none rw,si=9b4a76444445f39c +126 15 0:3459 / /var/lib/docker/aufs/mnt/3b49291670a625b9bbb329ffba99bf7fa7abff80cefef040f8b89e2b3aad4f9f rw,relatime - aufs none rw,si=9b4a7640798a339c +127 15 0:3460 / /var/lib/docker/aufs/mnt/8d9c7b943cc8f854f4d0d4ec19f7c16c13b0cc4f67a41472a072648610cecb59 rw,relatime - aufs none rw,si=9b4a76427383039c +128 15 0:3461 / /var/lib/docker/aufs/mnt/3b6c90036526c376307df71d49c9f5fce334c01b926faa6a78186842de74beac rw,relatime - aufs none rw,si=9b4a7644badd439c +130 15 0:3463 / /var/lib/docker/aufs/mnt/7b24158eeddfb5d31b7e932e406ea4899fd728344335ff8e0765e89ddeb351dd rw,relatime - aufs none rw,si=9b4a7644badd539c +131 15 0:3464 / /var/lib/docker/aufs/mnt/3ead6dd5773765c74850cf6c769f21fe65c29d622ffa712664f9f5b80364ce27 rw,relatime - aufs none rw,si=9b4a7642f469939c +132 15 0:3465 / /var/lib/docker/aufs/mnt/3f825573b29547744a37b65597a9d6d15a8350be4429b7038d126a4c9a8e178f rw,relatime - aufs none rw,si=9b4a7642f469c39c +133 15 0:3466 / /var/lib/docker/aufs/mnt/f67aaaeb3681e5dcb99a41f847087370bd1c206680cb8c7b6a9819fd6c97a331 rw,relatime - aufs none rw,si=9b4a7647cc25939c +134 15 0:3467 / /var/lib/docker/aufs/mnt/41afe6cfb3c1fc2280b869db07699da88552786e28793f0bc048a265c01bd942 rw,relatime - aufs none rw,si=9b4a7647cc25c39c +135 15 0:3468 / /var/lib/docker/aufs/mnt/b8092ea59da34a40b120e8718c3ae9fa8436996edc4fc50e4b99c72dfd81e1af rw,relatime - aufs none rw,si=9b4a76445abc439c +136 15 0:3469 / /var/lib/docker/aufs/mnt/42c69d2cc179e2684458bb8596a9da6dad182c08eae9b74d5f0e615b399f75a5 rw,relatime - aufs none rw,si=9b4a76455ddbe39c +137 15 0:3470 / /var/lib/docker/aufs/mnt/ea0871954acd2d62a211ac60e05969622044d4c74597870c4f818fbb0c56b09b rw,relatime - aufs none rw,si=9b4a76455ddbf39c +138 15 0:3471 / /var/lib/docker/aufs/mnt/4307906b275ab3fc971786b3841ae3217ac85b6756ddeb7ad4ba09cd044c2597 rw,relatime - aufs none rw,si=9b4a76455ddb839c +139 15 0:3472 / /var/lib/docker/aufs/mnt/4390b872928c53500a5035634f3421622ed6299dc1472b631fc45de9f56dc180 rw,relatime - aufs none rw,si=9b4a76402f2fd39c +140 15 0:3473 / /var/lib/docker/aufs/mnt/6bb41e78863b85e4aa7da89455314855c8c3bda64e52a583bab15dc1fa2e80c2 rw,relatime - aufs none rw,si=9b4a76402f2fa39c +141 15 0:3474 / /var/lib/docker/aufs/mnt/4444f583c2a79c66608f4673a32c9c812154f027045fbd558c2d69920c53f835 rw,relatime - aufs none rw,si=9b4a764479dbd39c +142 15 0:3475 / /var/lib/docker/aufs/mnt/6f11883af4a05ea362e0c54df89058da4859f977efd07b6f539e1f55c1d2a668 rw,relatime - aufs none rw,si=9b4a76402f30b39c +143 15 0:3476 / /var/lib/docker/aufs/mnt/453490dd32e7c2e9ef906f995d8fb3c2753923d1a5e0ba3fd3296e2e4dc238e7 rw,relatime - aufs none rw,si=9b4a76402f30c39c +144 15 0:3477 / /var/lib/docker/aufs/mnt/45e5945735ee102b5e891c91650c57ec4b52bb53017d68f02d50ea8a6e230610 rw,relatime - aufs none rw,si=9b4a76423260739c +147 15 0:3480 / /var/lib/docker/aufs/mnt/4727a64a5553a1125f315b96bed10d3073d6988225a292cce732617c925b56ab rw,relatime - aufs none rw,si=9b4a76443030339c +150 15 0:3483 / /var/lib/docker/aufs/mnt/4e348b5187b9a567059306afc72d42e0ec5c893b0d4abd547526d5f9b6fb4590 rw,relatime - aufs none rw,si=9b4a7644f5d8c39c +151 15 0:3484 / /var/lib/docker/aufs/mnt/4efc616bfbc3f906718b052da22e4335f8e9f91ee9b15866ed3a8029645189ef rw,relatime - aufs none rw,si=9b4a7644f5d8939c +152 15 0:3485 / /var/lib/docker/aufs/mnt/83e730ae9754d5adb853b64735472d98dfa17136b8812ac9cfcd1eba7f4e7d2d rw,relatime - aufs none rw,si=9b4a76469aa7139c +153 15 0:3486 / /var/lib/docker/aufs/mnt/4fc5ba8a5b333be2b7eefacccb626772eeec0ae8a6975112b56c9fb36c0d342f rw,relatime - aufs none rw,si=9b4a7640128dc39c +154 15 0:3487 / /var/lib/docker/aufs/mnt/50200d5edff5dfe8d1ef3c78b0bbd709793ac6e936aa16d74ff66f7ea577b6f9 rw,relatime - aufs none rw,si=9b4a7640128da39c +155 15 0:3488 / /var/lib/docker/aufs/mnt/51e5e51604361448f0b9777f38329f414bc5ba9cf238f26d465ff479bd574b61 rw,relatime - aufs none rw,si=9b4a76444f68939c +156 15 0:3489 / /var/lib/docker/aufs/mnt/52a142149aa98bba83df8766bbb1c629a97b9799944ead90dd206c4bdf0b8385 rw,relatime - aufs none rw,si=9b4a76444f68b39c +157 15 0:3490 / /var/lib/docker/aufs/mnt/52dd21a94a00f58a1ed489312fcfffb91578089c76c5650364476f1d5de031bc rw,relatime - aufs none rw,si=9b4a76444f68f39c +158 15 0:3491 / /var/lib/docker/aufs/mnt/ee562415ddaad353ed22c88d0ca768a0c74bfba6333b6e25c46849ee22d990da rw,relatime - aufs none rw,si=9b4a7640128d839c +159 15 0:3492 / /var/lib/docker/aufs/mnt/db47a9e87173f7554f550c8a01891de79cf12acdd32e01f95c1a527a08bdfb2c rw,relatime - aufs none rw,si=9b4a764405a1d39c +160 15 0:3493 / /var/lib/docker/aufs/mnt/55e827bf6d44d930ec0b827c98356eb8b68c3301e2d60d1429aa72e05b4c17df rw,relatime - aufs none rw,si=9b4a764405a1a39c +162 15 0:3495 / /var/lib/docker/aufs/mnt/578dc4e0a87fc37ec081ca098430499a59639c09f6f12a8f48de29828a091aa6 rw,relatime - aufs none rw,si=9b4a76406d7d439c +163 15 0:3496 / /var/lib/docker/aufs/mnt/728cc1cb04fa4bc6f7bf7a90980beda6d8fc0beb71630874c0747b994efb0798 rw,relatime - aufs none rw,si=9b4a76444f20e39c +164 15 0:3497 / /var/lib/docker/aufs/mnt/5850cc4bd9b55aea46c7ad598f1785117607974084ea643580f58ce3222e683a rw,relatime - aufs none rw,si=9b4a7644a824239c +165 15 0:3498 / /var/lib/docker/aufs/mnt/89443b3f766d5a37bc8b84e29da8b84e6a3ea8486d3cf154e2aae1816516e4a8 rw,relatime - aufs none rw,si=9b4a7644a824139c +166 15 0:3499 / /var/lib/docker/aufs/mnt/f5ae8fd5a41a337907d16515bc3162525154b59c32314c695ecd092c3b47943d rw,relatime - aufs none rw,si=9b4a7644a824439c +167 15 0:3500 / /var/lib/docker/aufs/mnt/5a430854f2a03a9e5f7cbc9f3fb46a8ebca526a5b3f435236d8295e5998798f5 rw,relatime - aufs none rw,si=9b4a7647fc82439c +168 15 0:3501 / /var/lib/docker/aufs/mnt/eda16901ae4cead35070c39845cbf1e10bd6b8cb0ffa7879ae2d8a186e460f91 rw,relatime - aufs none rw,si=9b4a76441e0df39c +169 15 0:3502 / /var/lib/docker/aufs/mnt/5a593721430c2a51b119ff86a7e06ea2b37e3b4131f8f1344d402b61b0c8d868 rw,relatime - aufs none rw,si=9b4a764248bad39c +170 15 0:3503 / /var/lib/docker/aufs/mnt/d662ad0a30fbfa902e0962108685b9330597e1ee2abb16dc9462eb5a67fdd23f rw,relatime - aufs none rw,si=9b4a764248bae39c +171 15 0:3504 / /var/lib/docker/aufs/mnt/5bc9de5c79812843fb36eee96bef1ddba812407861f572e33242f4ee10da2c15 rw,relatime - aufs none rw,si=9b4a764248ba839c +172 15 0:3505 / /var/lib/docker/aufs/mnt/5e763de8e9b0f7d58d2e12a341e029ab4efb3b99788b175090d8209e971156c1 rw,relatime - aufs none rw,si=9b4a764248baa39c +173 15 0:3506 / /var/lib/docker/aufs/mnt/b4431dc2739936f1df6387e337f5a0c99cf051900c896bd7fd46a870ce61c873 rw,relatime - aufs none rw,si=9b4a76401263539c +174 15 0:3507 / /var/lib/docker/aufs/mnt/5f37830e5a02561ab8c67ea3113137ba69f67a60e41c05cb0e7a0edaa1925b24 rw,relatime - aufs none rw,si=9b4a76401263639c +184 15 0:3508 / /var/lib/docker/aufs/mnt/62ea10b957e6533538a4633a1e1d678502f50ddcdd354b2ca275c54dd7a7793a rw,relatime - aufs none rw,si=9b4a76401263039c +187 15 0:3509 / /var/lib/docker/aufs/mnt/d56ee9d44195fe390e042fda75ec15af5132adb6d5c69468fa8792f4e54a6953 rw,relatime - aufs none rw,si=9b4a76401263239c +188 15 0:3510 / /var/lib/docker/aufs/mnt/6a300930673174549c2b62f36c933f0332a20735978c007c805a301f897146c5 rw,relatime - aufs none rw,si=9b4a76455d4c539c +189 15 0:3511 / /var/lib/docker/aufs/mnt/64496c45c84d348c24d410015456d101601c30cab4d1998c395591caf7e57a70 rw,relatime - aufs none rw,si=9b4a76455d4c639c +190 15 0:3512 / /var/lib/docker/aufs/mnt/65a6a645883fe97a7422cd5e71ebe0bc17c8e6302a5361edf52e89747387e908 rw,relatime - aufs none rw,si=9b4a76455d4c039c +191 15 0:3513 / /var/lib/docker/aufs/mnt/672be40695f7b6e13b0a3ed9fc996c73727dede3481f58155950fcfad57ed616 rw,relatime - aufs none rw,si=9b4a76455d4c239c +192 15 0:3514 / /var/lib/docker/aufs/mnt/d42438acb2bfb2169e1c0d8e917fc824f7c85d336dadb0b0af36dfe0f001b3ba rw,relatime - aufs none rw,si=9b4a7642bfded39c +193 15 0:3515 / /var/lib/docker/aufs/mnt/b48a54abf26d01cb2ddd908b1ed6034d17397c1341bf0eb2b251a3e5b79be854 rw,relatime - aufs none rw,si=9b4a7642bfdee39c +194 15 0:3516 / /var/lib/docker/aufs/mnt/76f27134491f052bfb87f59092126e53ef875d6851990e59195a9da16a9412f8 rw,relatime - aufs none rw,si=9b4a7642bfde839c +195 15 0:3517 / /var/lib/docker/aufs/mnt/6bd626a5462b4f8a8e1cc7d10351326dca97a59b2758e5ea549a4f6350ce8a90 rw,relatime - aufs none rw,si=9b4a7642bfdea39c +196 15 0:3518 / /var/lib/docker/aufs/mnt/f1fe3549dbd6f5ca615e9139d9b53f0c83a3b825565df37628eacc13e70cbd6d rw,relatime - aufs none rw,si=9b4a7642bfdf539c +197 15 0:3519 / /var/lib/docker/aufs/mnt/6d0458c8426a9e93d58d0625737e6122e725c9408488ed9e3e649a9984e15c34 rw,relatime - aufs none rw,si=9b4a7642bfdf639c +198 15 0:3520 / /var/lib/docker/aufs/mnt/6e4c97db83aa82145c9cf2bafc20d500c0b5389643b689e3ae84188c270a48c5 rw,relatime - aufs none rw,si=9b4a7642bfdf039c +199 15 0:3521 / /var/lib/docker/aufs/mnt/eb94d6498f2c5969eaa9fa11ac2934f1ab90ef88e2d002258dca08e5ba74ea27 rw,relatime - aufs none rw,si=9b4a7642bfdf239c +200 15 0:3522 / /var/lib/docker/aufs/mnt/fe3f88f0c511608a2eec5f13a98703aa16e55dbf930309723d8a37101f539fe1 rw,relatime - aufs none rw,si=9b4a7642bfc3539c +201 15 0:3523 / /var/lib/docker/aufs/mnt/6f40c229fb9cad85fabf4b64a2640a5403ec03fe5ac1a57d0609fb8b606b9c83 rw,relatime - aufs none rw,si=9b4a7642bfc3639c +202 15 0:3524 / /var/lib/docker/aufs/mnt/7513e9131f7a8acf58ff15248237feb767c78732ca46e159f4d791e6ef031dbc rw,relatime - aufs none rw,si=9b4a7642bfc3039c +203 15 0:3525 / /var/lib/docker/aufs/mnt/79f48b00aa713cdf809c6bb7c7cb911b66e9a8076c81d6c9d2504139984ea2da rw,relatime - aufs none rw,si=9b4a7642bfc3239c +204 15 0:3526 / /var/lib/docker/aufs/mnt/c3680418350d11358f0a96c676bc5aa74fa00a7c89e629ef5909d3557b060300 rw,relatime - aufs none rw,si=9b4a7642f47cd39c +205 15 0:3527 / /var/lib/docker/aufs/mnt/7a1744dd350d7fcc0cccb6f1757ca4cbe5453f203a5888b0f1014d96ad5a5ef9 rw,relatime - aufs none rw,si=9b4a7642f47ce39c +206 15 0:3528 / /var/lib/docker/aufs/mnt/7fa99662db046be9f03c33c35251afda9ccdc0085636bbba1d90592cec3ff68d rw,relatime - aufs none rw,si=9b4a7642f47c839c +207 15 0:3529 / /var/lib/docker/aufs/mnt/f815021ef20da9c9b056bd1d52d8aaf6e2c0c19f11122fc793eb2b04eb995e35 rw,relatime - aufs none rw,si=9b4a7642f47ca39c +208 15 0:3530 / /var/lib/docker/aufs/mnt/801086ae3110192d601dfcebdba2db92e86ce6b6a9dba6678ea04488e4513669 rw,relatime - aufs none rw,si=9b4a7642dc6dd39c +209 15 0:3531 / /var/lib/docker/aufs/mnt/822ba7db69f21daddda87c01cfbfbf73013fc03a879daf96d16cdde6f9b1fbd6 rw,relatime - aufs none rw,si=9b4a7642dc6de39c +210 15 0:3532 / /var/lib/docker/aufs/mnt/834227c1a950fef8cae3827489129d0dd220541e60c6b731caaa765bf2e6a199 rw,relatime - aufs none rw,si=9b4a7642dc6d839c +211 15 0:3533 / /var/lib/docker/aufs/mnt/83dccbc385299bd1c7cf19326e791b33a544eea7b4cdfb6db70ea94eed4389fb rw,relatime - aufs none rw,si=9b4a7642dc6da39c +212 15 0:3534 / /var/lib/docker/aufs/mnt/f1b8e6f0e7c8928b5dcdab944db89306ebcae3e0b32f9ff40d2daa8329f21600 rw,relatime - aufs none rw,si=9b4a7645a126039c +213 15 0:3535 / /var/lib/docker/aufs/mnt/970efb262c7a020c2404cbcc5b3259efba0d110a786079faeef05bc2952abf3a rw,relatime - aufs none rw,si=9b4a7644c8ed139c +214 15 0:3536 / /var/lib/docker/aufs/mnt/84b6d73af7450f3117a77e15a5ca1255871fea6182cd8e8a7be6bc744be18c2c rw,relatime - aufs none rw,si=9b4a76406559139c +215 15 0:3537 / /var/lib/docker/aufs/mnt/88be2716e026bc681b5e63fe7942068773efbd0b6e901ca7ba441412006a96b6 rw,relatime - aufs none rw,si=9b4a76406559339c +216 15 0:3538 / /var/lib/docker/aufs/mnt/c81939aa166ce50cd8bca5cfbbcc420a78e0318dd5cd7c755209b9166a00a752 rw,relatime - aufs none rw,si=9b4a76406559239c +217 15 0:3539 / /var/lib/docker/aufs/mnt/e0f241645d64b7dc5ff6a8414087cca226be08fb54ce987d1d1f6350c57083aa rw,relatime - aufs none rw,si=9b4a7647cfc0f39c +218 15 0:3540 / /var/lib/docker/aufs/mnt/e10e2bf75234ed51d8a6a4bb39e465404fecbe318e54400d3879cdb2b0679c78 rw,relatime - aufs none rw,si=9b4a7647cfc0939c +219 15 0:3541 / /var/lib/docker/aufs/mnt/8f71d74c8cfc3228b82564aa9f09b2e576cff0083ddfb6aa5cb350346063f080 rw,relatime - aufs none rw,si=9b4a7647cfc0a39c +220 15 0:3542 / /var/lib/docker/aufs/mnt/9159f1eba2aef7f5205cc18d015cda7f5933cd29bba3b1b8aed5ccb5824c69ee rw,relatime - aufs none rw,si=9b4a76468cedd39c +221 15 0:3543 / /var/lib/docker/aufs/mnt/932cad71e652e048e500d9fbb5b8ea4fc9a269d42a3134ce527ceef42a2be56b rw,relatime - aufs none rw,si=9b4a76468cede39c +222 15 0:3544 / /var/lib/docker/aufs/mnt/bf1e1b5f529e8943cc0144ee86dbaaa37885c1ddffcef29537e0078ee7dd316a rw,relatime - aufs none rw,si=9b4a76468ced839c +223 15 0:3545 / /var/lib/docker/aufs/mnt/949d93ecf3322e09f858ce81d5f4b434068ec44ff84c375de03104f7b45ee955 rw,relatime - aufs none rw,si=9b4a76468ceda39c +224 15 0:3546 / /var/lib/docker/aufs/mnt/d65c6087f92dc2a3841b5251d2fe9ca07d4c6e5b021597692479740816e4e2a1 rw,relatime - aufs none rw,si=9b4a7645a126239c +225 15 0:3547 / /var/lib/docker/aufs/mnt/98a0153119d0651c193d053d254f6e16a68345a141baa80c87ae487e9d33f290 rw,relatime - aufs none rw,si=9b4a7640787cf39c +226 15 0:3548 / /var/lib/docker/aufs/mnt/99daf7fe5847c017392f6e59aa9706b3dfdd9e6d1ba11dae0f7fffde0a60b5e5 rw,relatime - aufs none rw,si=9b4a7640787c839c +227 15 0:3549 / /var/lib/docker/aufs/mnt/9ad1f2fe8a5599d4e10c5a6effa7f03d932d4e92ee13149031a372087a359079 rw,relatime - aufs none rw,si=9b4a7640787ca39c +228 15 0:3550 / /var/lib/docker/aufs/mnt/c26d64494da782ddac26f8370d86ac93e7c1666d88a7b99110fc86b35ea6a85d rw,relatime - aufs none rw,si=9b4a7642fc6b539c +229 15 0:3551 / /var/lib/docker/aufs/mnt/a49e4a8275133c230ec640997f35f172312eb0ea5bd2bbe10abf34aae98f30eb rw,relatime - aufs none rw,si=9b4a7642fc6b639c +230 15 0:3552 / /var/lib/docker/aufs/mnt/b5e2740c867ed843025f49d84e8d769de9e8e6039b3c8cb0735b5bf358994bc7 rw,relatime - aufs none rw,si=9b4a7642fc6b039c +231 15 0:3553 / /var/lib/docker/aufs/mnt/a826fdcf3a7039b30570054579b65763db605a314275d7aef31b872c13311b4b rw,relatime - aufs none rw,si=9b4a7642fc6b239c +232 15 0:3554 / /var/lib/docker/aufs/mnt/addf3025babf5e43b5a3f4a0da7ad863dda3c01fb8365c58fd8d28bb61dc11bc rw,relatime - aufs none rw,si=9b4a76407871d39c +233 15 0:3555 / /var/lib/docker/aufs/mnt/c5b6c6813ab3e5ebdc6d22cb2a3d3106a62095f2c298be52b07a3b0fa20ff690 rw,relatime - aufs none rw,si=9b4a76407871e39c +234 15 0:3556 / /var/lib/docker/aufs/mnt/af0609eaaf64e2392060cb46f5a9f3d681a219bb4c651d4f015bf573fbe6c4cf rw,relatime - aufs none rw,si=9b4a76407871839c +235 15 0:3557 / /var/lib/docker/aufs/mnt/e7f20e3c37ecad39cd90a97cd3549466d0d106ce4f0a930b8495442634fa4a1f rw,relatime - aufs none rw,si=9b4a76407871a39c +237 15 0:3559 / /var/lib/docker/aufs/mnt/b57a53d440ffd0c1295804fa68cdde35d2fed5409484627e71b9c37e4249fd5c rw,relatime - aufs none rw,si=9b4a76444445a39c +238 15 0:3560 / /var/lib/docker/aufs/mnt/b5e7d7b8f35e47efbba3d80c5d722f5e7bd43e54c824e54b4a4b351714d36d42 rw,relatime - aufs none rw,si=9b4a7647932d439c +239 15 0:3561 / /var/lib/docker/aufs/mnt/f1b136def157e9465640658f277f3347de593c6ae76412a2e79f7002f091cae2 rw,relatime - aufs none rw,si=9b4a76445abcd39c +240 15 0:3562 / /var/lib/docker/aufs/mnt/b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc rw,relatime - aufs none rw,si=9b4a7644403b339c +241 15 0:3563 / /var/lib/docker/aufs/mnt/b89b140cdbc95063761864e0a23346207fa27ee4c5c63a1ae85c9069a9d9cf1d rw,relatime - aufs none rw,si=9b4a7644aa19739c +242 15 0:3564 / /var/lib/docker/aufs/mnt/bc6a69ed51c07f5228f6b4f161c892e6a949c0e7e86a9c3432049d4c0e5cd298 rw,relatime - aufs none rw,si=9b4a7644aa19139c +243 15 0:3565 / /var/lib/docker/aufs/mnt/be4e2ba3f136933e239f7cf3d136f484fb9004f1fbdfee24a62a2c7b0ab30670 rw,relatime - aufs none rw,si=9b4a7644aa19339c +244 15 0:3566 / /var/lib/docker/aufs/mnt/e04ca1a4a5171e30d20f0c92f90a50b8b6f8600af5459c4b4fb25e42e864dfe1 rw,relatime - aufs none rw,si=9b4a7647932d139c +245 15 0:3567 / /var/lib/docker/aufs/mnt/be61576b31db893129aaffcd3dcb5ce35e49c4b71b30c392a78609a45c7323d8 rw,relatime - aufs none rw,si=9b4a7642d85f739c +246 15 0:3568 / /var/lib/docker/aufs/mnt/dda42c191e56becf672327658ab84fcb563322db3764b91c2fefe4aaef04c624 rw,relatime - aufs none rw,si=9b4a7642d85f139c +247 15 0:3569 / /var/lib/docker/aufs/mnt/c0a7995053330f3d88969247a2e72b07e2dd692133f5668a4a35ea3905561072 rw,relatime - aufs none rw,si=9b4a7642d85f339c +249 15 0:3571 / /var/lib/docker/aufs/mnt/c3594b2e5f08c59ff5ed338a1ba1eceeeb1f7fc5d180068338110c00b1eb8502 rw,relatime - aufs none rw,si=9b4a7642738c739c +250 15 0:3572 / /var/lib/docker/aufs/mnt/c58dce03a0ab0a7588393880379dc3bce9f96ec08ed3f99cf1555260ff0031e8 rw,relatime - aufs none rw,si=9b4a7642738c139c +251 15 0:3573 / /var/lib/docker/aufs/mnt/c73e9f1d109c9d14cb36e1c7489df85649be3911116d76c2fd3648ec8fd94e23 rw,relatime - aufs none rw,si=9b4a7642738c339c +252 15 0:3574 / /var/lib/docker/aufs/mnt/c9eef28c344877cd68aa09e543c0710ab2b305a0ff96dbb859bfa7808c3e8d01 rw,relatime - aufs none rw,si=9b4a7642d85f439c +253 15 0:3575 / /var/lib/docker/aufs/mnt/feb67148f548d70cb7484f2aaad2a86051cd6867a561741a2f13b552457d666e rw,relatime - aufs none rw,si=9b4a76468c55739c +254 15 0:3576 / /var/lib/docker/aufs/mnt/cdf1f96c36d35a96041a896bf398ec0f7dc3b0fb0643612a0f4b6ff96e04e1bb rw,relatime - aufs none rw,si=9b4a76468c55139c +255 15 0:3577 / /var/lib/docker/aufs/mnt/ec6e505872353268451ac4bc034c1df00f3bae4a3ea2261c6e48f7bd5417c1b3 rw,relatime - aufs none rw,si=9b4a76468c55339c +256 15 0:3578 / /var/lib/docker/aufs/mnt/d6dc8aca64efd90e0bc10274001882d0efb310d42ccbf5712b99b169053b8b1a rw,relatime - aufs none rw,si=9b4a7642738c439c +257 15 0:3579 / /var/lib/docker/aufs/mnt/d712594e2ff6eaeb895bfd150d694bd1305fb927e7a186b2dab7df2ea95f8f81 rw,relatime - aufs none rw,si=9b4a76401268f39c +259 15 0:3581 / /var/lib/docker/aufs/mnt/dbfa1174cd78cde2d7410eae442af0b416c4a0e6f87ed4ff1e9f169a0029abc0 rw,relatime - aufs none rw,si=9b4a76401268b39c +260 15 0:3582 / /var/lib/docker/aufs/mnt/e883f5a82316d7856fbe93ee8c0af5a920b7079619dd95c4ffd88bbd309d28dd rw,relatime - aufs none rw,si=9b4a76468c55439c +261 15 0:3583 / /var/lib/docker/aufs/mnt/fdec3eff581c4fc2b09f87befa2fa021f3f2d373bea636a87f1fb5b367d6347a rw,relatime - aufs none rw,si=9b4a7644aa1af39c +262 15 0:3584 / /var/lib/docker/aufs/mnt/ef764e26712184653067ecf7afea18a80854c41331ca0f0ef03e1bacf90a6ffc rw,relatime - aufs none rw,si=9b4a7644aa1a939c +263 15 0:3585 / /var/lib/docker/aufs/mnt/f3176b40c41fce8ce6942936359a2001a6f1b5c1bb40ee224186db0789ec2f76 rw,relatime - aufs none rw,si=9b4a7644aa1ab39c +264 15 0:3586 / /var/lib/docker/aufs/mnt/f5daf06785d3565c6dd18ea7d953d9a8b9606107781e63270fe0514508736e6a rw,relatime - aufs none rw,si=9b4a76401268c39c +58 15 0:3587 / /var/lib/docker/aufs/mnt/cde8c40f6524b7361af4f5ad05bb857dc9ee247c20852ba666195c0739e3a2b8-init rw,relatime - aufs none rw,si=9b4a76444445839c +67 15 0:3588 / /var/lib/docker/aufs/mnt/cde8c40f6524b7361af4f5ad05bb857dc9ee247c20852ba666195c0739e3a2b8 rw,relatime - aufs none rw,si=9b4a7644badd339c +265 15 0:3610 / /var/lib/docker/aufs/mnt/e812472cd2c8c4748d1ef71fac4e77e50d661b9349abe66ce3e23511ed44f414 rw,relatime - aufs none rw,si=9b4a76427937d39c +270 15 0:3615 / /var/lib/docker/aufs/mnt/997636e7c5c9d0d1376a217e295c14c205350b62bc12052804fb5f90abe6f183 rw,relatime - aufs none rw,si=9b4a76406540739c +273 15 0:3618 / /var/lib/docker/aufs/mnt/d5794d080417b6e52e69227c3873e0e4c1ff0d5a845ebe3860ec2f89a47a2a1e rw,relatime - aufs none rw,si=9b4a76454814039c +278 15 0:3623 / /var/lib/docker/aufs/mnt/586bdd48baced671bb19bc4d294ec325f26c55545ae267db426424f157d59c48 rw,relatime - aufs none rw,si=9b4a7644b439f39c +281 15 0:3626 / /var/lib/docker/aufs/mnt/69739d022f89f8586908bbd5edbbdd95ea5256356f177f9ffcc6ef9c0ea752d2 rw,relatime - aufs none rw,si=9b4a7644a0f1b39c +286 15 0:3631 / /var/lib/docker/aufs/mnt/ff28c27d5f894363993622de26d5dd352dba072f219e4691d6498c19bbbc15a9 rw,relatime - aufs none rw,si=9b4a7642265b339c +289 15 0:3634 / /var/lib/docker/aufs/mnt/aa128fe0e64fdede333aa48fd9de39530c91a9244a0f0649a3c411c61e372daa rw,relatime - aufs none rw,si=9b4a764012ada39c +99 15 8:33 / /media/REMOVE\040ME rw,nosuid,nodev,relatime - fuseblk /dev/sdc1 rw,user_id=0,group_id=0,allow_other,blksize=4096` +) + +func TestParseFedoraMountinfo(t *testing.T) { + r := bytes.NewBuffer([]byte(fedoraMountinfo)) + _, err := parseInfoFile(r) + if err != nil { + t.Fatal(err) + } +} + +func TestParseUbuntuMountinfo(t *testing.T) { + r := bytes.NewBuffer([]byte(ubuntuMountInfo)) + _, err := parseInfoFile(r) + if err != nil { + t.Fatal(err) + } +} + +func TestParseGentooMountinfo(t *testing.T) { + r := bytes.NewBuffer([]byte(gentooMountinfo)) + _, err := parseInfoFile(r) + if err != nil { + t.Fatal(err) + } +} + +func TestParseFedoraMountinfoFields(t *testing.T) { + r := bytes.NewBuffer([]byte(fedoraMountinfo)) + infos, err := parseInfoFile(r) + if err != nil { + t.Fatal(err) + } + expectedLength := 58 + if len(infos) != expectedLength { + t.Fatalf("Expected %d entries, got %d", expectedLength, len(infos)) + } + mi := Info{ + ID: 15, + Parent: 35, + Major: 0, + Minor: 3, + Root: "/", + Mountpoint: "/proc", + Opts: "rw,nosuid,nodev,noexec,relatime", + Optional: "shared:5", + Fstype: "proc", + Source: "proc", + VfsOpts: "rw", + } + + if *infos[0] != mi { + t.Fatalf("expected %#v, got %#v", mi, infos[0]) + } +} diff --git a/pkg/mount/mountinfo_unsupported.go b/pkg/mount/mountinfo_unsupported.go new file mode 100644 index 00000000..b8d9aa5c --- /dev/null +++ b/pkg/mount/mountinfo_unsupported.go @@ -0,0 +1,12 @@ +// +build !windows,!linux,!freebsd freebsd,!cgo + +package mount + +import ( + "fmt" + "runtime" +) + +func parseMountTable() ([]*Info, error) { + return nil, fmt.Errorf("mount.parseMountTable is not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) +} diff --git a/pkg/mount/mountinfo_windows.go b/pkg/mount/mountinfo_windows.go new file mode 100644 index 00000000..dab8a37e --- /dev/null +++ b/pkg/mount/mountinfo_windows.go @@ -0,0 +1,6 @@ +package mount + +func parseMountTable() ([]*Info, error) { + // Do NOT return an error! + return nil, nil +} diff --git a/pkg/mount/sharedsubtree_linux.go b/pkg/mount/sharedsubtree_linux.go new file mode 100644 index 00000000..8ceec84b --- /dev/null +++ b/pkg/mount/sharedsubtree_linux.go @@ -0,0 +1,69 @@ +// +build linux + +package mount + +// MakeShared ensures a mounted filesystem has the SHARED mount option enabled. +// See the supported options in flags.go for further reference. +func MakeShared(mountPoint string) error { + return ensureMountedAs(mountPoint, "shared") +} + +// MakeRShared ensures a mounted filesystem has the RSHARED mount option enabled. +// See the supported options in flags.go for further reference. +func MakeRShared(mountPoint string) error { + return ensureMountedAs(mountPoint, "rshared") +} + +// MakePrivate ensures a mounted filesystem has the PRIVATE mount option enabled. +// See the supported options in flags.go for further reference. +func MakePrivate(mountPoint string) error { + return ensureMountedAs(mountPoint, "private") +} + +// MakeRPrivate ensures a mounted filesystem has the RPRIVATE mount option +// enabled. See the supported options in flags.go for further reference. +func MakeRPrivate(mountPoint string) error { + return ensureMountedAs(mountPoint, "rprivate") +} + +// MakeSlave ensures a mounted filesystem has the SLAVE mount option enabled. +// See the supported options in flags.go for further reference. +func MakeSlave(mountPoint string) error { + return ensureMountedAs(mountPoint, "slave") +} + +// MakeRSlave ensures a mounted filesystem has the RSLAVE mount option enabled. +// See the supported options in flags.go for further reference. +func MakeRSlave(mountPoint string) error { + return ensureMountedAs(mountPoint, "rslave") +} + +// MakeUnbindable ensures a mounted filesystem has the UNBINDABLE mount option +// enabled. See the supported options in flags.go for further reference. +func MakeUnbindable(mountPoint string) error { + return ensureMountedAs(mountPoint, "unbindable") +} + +// MakeRUnbindable ensures a mounted filesystem has the RUNBINDABLE mount +// option enabled. See the supported options in flags.go for further reference. +func MakeRUnbindable(mountPoint string) error { + return ensureMountedAs(mountPoint, "runbindable") +} + +func ensureMountedAs(mountPoint, options string) error { + mounted, err := Mounted(mountPoint) + if err != nil { + return err + } + + if !mounted { + if err := Mount(mountPoint, mountPoint, "none", "bind,rw"); err != nil { + return err + } + } + if _, err = Mounted(mountPoint); err != nil { + return err + } + + return ForceMount("", mountPoint, "none", options) +} diff --git a/pkg/mount/sharedsubtree_linux_test.go b/pkg/mount/sharedsubtree_linux_test.go new file mode 100644 index 00000000..c1837942 --- /dev/null +++ b/pkg/mount/sharedsubtree_linux_test.go @@ -0,0 +1,331 @@ +// +build linux + +package mount + +import ( + "os" + "path" + "syscall" + "testing" +) + +// nothing is propagated in or out +func TestSubtreePrivate(t *testing.T) { + tmp := path.Join(os.TempDir(), "mount-tests") + if err := os.MkdirAll(tmp, 0777); err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + + var ( + sourceDir = path.Join(tmp, "source") + targetDir = path.Join(tmp, "target") + outside1Dir = path.Join(tmp, "outside1") + outside2Dir = path.Join(tmp, "outside2") + + outside1Path = path.Join(outside1Dir, "file.txt") + outside2Path = path.Join(outside2Dir, "file.txt") + outside1CheckPath = path.Join(targetDir, "a", "file.txt") + outside2CheckPath = path.Join(sourceDir, "b", "file.txt") + ) + if err := os.MkdirAll(path.Join(sourceDir, "a"), 0777); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(path.Join(sourceDir, "b"), 0777); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(targetDir, 0777); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(outside1Dir, 0777); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(outside2Dir, 0777); err != nil { + t.Fatal(err) + } + + if err := createFile(outside1Path); err != nil { + t.Fatal(err) + } + if err := createFile(outside2Path); err != nil { + t.Fatal(err) + } + + // mount the shared directory to a target + if err := Mount(sourceDir, targetDir, "none", "bind,rw"); err != nil { + t.Fatal(err) + } + defer func() { + if err := Unmount(targetDir); err != nil { + t.Fatal(err) + } + }() + + // next, make the target private + if err := MakePrivate(targetDir); err != nil { + t.Fatal(err) + } + defer func() { + if err := Unmount(targetDir); err != nil { + t.Fatal(err) + } + }() + + // mount in an outside path to a mounted path inside the _source_ + if err := Mount(outside1Dir, path.Join(sourceDir, "a"), "none", "bind,rw"); err != nil { + t.Fatal(err) + } + defer func() { + if err := Unmount(path.Join(sourceDir, "a")); err != nil { + t.Fatal(err) + } + }() + + // check that this file _does_not_ show in the _target_ + if _, err := os.Stat(outside1CheckPath); err != nil && !os.IsNotExist(err) { + t.Fatal(err) + } else if err == nil { + t.Fatalf("%q should not be visible, but is", outside1CheckPath) + } + + // next mount outside2Dir into the _target_ + if err := Mount(outside2Dir, path.Join(targetDir, "b"), "none", "bind,rw"); err != nil { + t.Fatal(err) + } + defer func() { + if err := Unmount(path.Join(targetDir, "b")); err != nil { + t.Fatal(err) + } + }() + + // check that this file _does_not_ show in the _source_ + if _, err := os.Stat(outside2CheckPath); err != nil && !os.IsNotExist(err) { + t.Fatal(err) + } else if err == nil { + t.Fatalf("%q should not be visible, but is", outside2CheckPath) + } +} + +// Testing that when a target is a shared mount, +// then child mounts propagate to the source +func TestSubtreeShared(t *testing.T) { + tmp := path.Join(os.TempDir(), "mount-tests") + if err := os.MkdirAll(tmp, 0777); err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + + var ( + sourceDir = path.Join(tmp, "source") + targetDir = path.Join(tmp, "target") + outsideDir = path.Join(tmp, "outside") + + outsidePath = path.Join(outsideDir, "file.txt") + sourceCheckPath = path.Join(sourceDir, "a", "file.txt") + ) + + if err := os.MkdirAll(path.Join(sourceDir, "a"), 0777); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(targetDir, 0777); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(outsideDir, 0777); err != nil { + t.Fatal(err) + } + + if err := createFile(outsidePath); err != nil { + t.Fatal(err) + } + + // mount the source as shared + if err := MakeShared(sourceDir); err != nil { + t.Fatal(err) + } + defer func() { + if err := Unmount(sourceDir); err != nil { + t.Fatal(err) + } + }() + + // mount the shared directory to a target + if err := Mount(sourceDir, targetDir, "none", "bind,rw"); err != nil { + t.Fatal(err) + } + defer func() { + if err := Unmount(targetDir); err != nil { + t.Fatal(err) + } + }() + + // mount in an outside path to a mounted path inside the target + if err := Mount(outsideDir, path.Join(targetDir, "a"), "none", "bind,rw"); err != nil { + t.Fatal(err) + } + defer func() { + if err := Unmount(path.Join(targetDir, "a")); err != nil { + t.Fatal(err) + } + }() + + // NOW, check that the file from the outside directory is available in the source directory + if _, err := os.Stat(sourceCheckPath); err != nil { + t.Fatal(err) + } +} + +// testing that mounts to a shared source show up in the slave target, +// and that mounts into a slave target do _not_ show up in the shared source +func TestSubtreeSharedSlave(t *testing.T) { + tmp := path.Join(os.TempDir(), "mount-tests") + if err := os.MkdirAll(tmp, 0777); err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + + var ( + sourceDir = path.Join(tmp, "source") + targetDir = path.Join(tmp, "target") + outside1Dir = path.Join(tmp, "outside1") + outside2Dir = path.Join(tmp, "outside2") + + outside1Path = path.Join(outside1Dir, "file.txt") + outside2Path = path.Join(outside2Dir, "file.txt") + outside1CheckPath = path.Join(targetDir, "a", "file.txt") + outside2CheckPath = path.Join(sourceDir, "b", "file.txt") + ) + if err := os.MkdirAll(path.Join(sourceDir, "a"), 0777); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(path.Join(sourceDir, "b"), 0777); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(targetDir, 0777); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(outside1Dir, 0777); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(outside2Dir, 0777); err != nil { + t.Fatal(err) + } + + if err := createFile(outside1Path); err != nil { + t.Fatal(err) + } + if err := createFile(outside2Path); err != nil { + t.Fatal(err) + } + + // mount the source as shared + if err := MakeShared(sourceDir); err != nil { + t.Fatal(err) + } + defer func() { + if err := Unmount(sourceDir); err != nil { + t.Fatal(err) + } + }() + + // mount the shared directory to a target + if err := Mount(sourceDir, targetDir, "none", "bind,rw"); err != nil { + t.Fatal(err) + } + defer func() { + if err := Unmount(targetDir); err != nil { + t.Fatal(err) + } + }() + + // next, make the target slave + if err := MakeSlave(targetDir); err != nil { + t.Fatal(err) + } + defer func() { + if err := Unmount(targetDir); err != nil { + t.Fatal(err) + } + }() + + // mount in an outside path to a mounted path inside the _source_ + if err := Mount(outside1Dir, path.Join(sourceDir, "a"), "none", "bind,rw"); err != nil { + t.Fatal(err) + } + defer func() { + if err := Unmount(path.Join(sourceDir, "a")); err != nil { + t.Fatal(err) + } + }() + + // check that this file _does_ show in the _target_ + if _, err := os.Stat(outside1CheckPath); err != nil { + t.Fatal(err) + } + + // next mount outside2Dir into the _target_ + if err := Mount(outside2Dir, path.Join(targetDir, "b"), "none", "bind,rw"); err != nil { + t.Fatal(err) + } + defer func() { + if err := Unmount(path.Join(targetDir, "b")); err != nil { + t.Fatal(err) + } + }() + + // check that this file _does_not_ show in the _source_ + if _, err := os.Stat(outside2CheckPath); err != nil && !os.IsNotExist(err) { + t.Fatal(err) + } else if err == nil { + t.Fatalf("%q should not be visible, but is", outside2CheckPath) + } +} + +func TestSubtreeUnbindable(t *testing.T) { + tmp := path.Join(os.TempDir(), "mount-tests") + if err := os.MkdirAll(tmp, 0777); err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + + var ( + sourceDir = path.Join(tmp, "source") + targetDir = path.Join(tmp, "target") + ) + if err := os.MkdirAll(sourceDir, 0777); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(targetDir, 0777); err != nil { + t.Fatal(err) + } + + // next, make the source unbindable + if err := MakeUnbindable(sourceDir); err != nil { + t.Fatal(err) + } + defer func() { + if err := Unmount(sourceDir); err != nil { + t.Fatal(err) + } + }() + + // then attempt to mount it to target. It should fail + if err := Mount(sourceDir, targetDir, "none", "bind,rw"); err != nil && err != syscall.EINVAL { + t.Fatal(err) + } else if err == nil { + t.Fatalf("%q should not have been bindable", sourceDir) + } + defer func() { + if err := Unmount(targetDir); err != nil { + t.Fatal(err) + } + }() +} + +func createFile(path string) error { + f, err := os.Create(path) + if err != nil { + return err + } + f.WriteString("hello world!") + return f.Close() +} diff --git a/pkg/namesgenerator/cmd/names-generator/main.go b/pkg/namesgenerator/cmd/names-generator/main.go new file mode 100644 index 00000000..18a939b7 --- /dev/null +++ b/pkg/namesgenerator/cmd/names-generator/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "fmt" + + "github.com/docker/docker/pkg/namesgenerator" +) + +func main() { + fmt.Println(namesgenerator.GetRandomName(0)) +} diff --git a/pkg/namesgenerator/names-generator.go b/pkg/namesgenerator/names-generator.go new file mode 100644 index 00000000..1764fc28 --- /dev/null +++ b/pkg/namesgenerator/names-generator.go @@ -0,0 +1,524 @@ +package namesgenerator + +import ( + "fmt" + + "github.com/docker/docker/pkg/random" +) + +var ( + left = [...]string{ + "admiring", + "adoring", + "agitated", + "amazing", + "angry", + "awesome", + "backstabbing", + "berserk", + "big", + "boring", + "clever", + "cocky", + "compassionate", + "condescending", + "cranky", + "desperate", + "determined", + "distracted", + "dreamy", + "drunk", + "ecstatic", + "elated", + "elegant", + "evil", + "fervent", + "focused", + "furious", + "gigantic", + "gloomy", + "goofy", + "grave", + "happy", + "high", + "hopeful", + "hungry", + "insane", + "jolly", + "jovial", + "kickass", + "lonely", + "loving", + "mad", + "modest", + "naughty", + "nauseous", + "nostalgic", + "pedantic", + "pensive", + "prickly", + "reverent", + "romantic", + "sad", + "serene", + "sharp", + "sick", + "silly", + "sleepy", + "small", + "stoic", + "stupefied", + "suspicious", + "tender", + "thirsty", + "tiny", + "trusting", + } + + // Docker, starting from 0.7.x, generates names from notable scientists and hackers. + // Please, for any amazing man that you add to the list, consider adding an equally amazing woman to it, and vice versa. + right = [...]string{ + // Muhammad ibn Jābir al-ḤarrānÄ« al-BattānÄ« was a founding father of astronomy. https://en.wikipedia.org/wiki/Mu%E1%B8%A5ammad_ibn_J%C4%81bir_al-%E1%B8%A4arr%C4%81n%C4%AB_al-Batt%C4%81n%C4%AB + "albattani", + + // Frances E. Allen, became the first female IBM Fellow in 1989. In 2006, she became the first female recipient of the ACM's Turing Award. https://en.wikipedia.org/wiki/Frances_E._Allen + "allen", + + // June Almeida - Scottish virologist who took the first pictures of the rubella virus - https://en.wikipedia.org/wiki/June_Almeida + "almeida", + + // Archimedes was a physicist, engineer and mathematician who invented too many things to list them here. https://en.wikipedia.org/wiki/Archimedes + "archimedes", + + // Maria Ardinghelli - Italian translator, mathematician and physicist - https://en.wikipedia.org/wiki/Maria_Ardinghelli + "ardinghelli", + + // Aryabhata - Ancient Indian mathematician-astronomer during 476-550 CE https://en.wikipedia.org/wiki/Aryabhata + "aryabhata", + + // Wanda Austin - Wanda Austin is the President and CEO of The Aerospace Corporation, a leading architect for the US security space programs. https://en.wikipedia.org/wiki/Wanda_Austin + "austin", + + // Charles Babbage invented the concept of a programmable computer. https://en.wikipedia.org/wiki/Charles_Babbage. + "babbage", + + // Stefan Banach - Polish mathematician, was one of the founders of modern functional analysis. https://en.wikipedia.org/wiki/Stefan_Banach + "banach", + + // John Bardeen co-invented the transistor - https://en.wikipedia.org/wiki/John_Bardeen + "bardeen", + + // Jean Bartik, born Betty Jean Jennings, was one of the original programmers for the ENIAC computer. https://en.wikipedia.org/wiki/Jean_Bartik + "bartik", + + // Laura Bassi, the world's first female professor https://en.wikipedia.org/wiki/Laura_Bassi + "bassi", + + // Alexander Graham Bell - an eminent Scottish-born scientist, inventor, engineer and innovator who is credited with inventing the first practical telephone - https://en.wikipedia.org/wiki/Alexander_Graham_Bell + "bell", + + // Homi J Bhabha - was an Indian nuclear physicist, founding director, and professor of physics at the Tata Institute of Fundamental Research. Colloquially known as "father of Indian nuclear programme"- https://en.wikipedia.org/wiki/Homi_J._Bhabha + "bhabha", + + // Bhaskara II - Ancient Indian mathematician-astronomer whose work on calculus predates Newton and Leibniz by over half a millennium - https://en.wikipedia.org/wiki/Bh%C4%81skara_II#Calculus + "bhaskara", + + // Elizabeth Blackwell - American doctor and first American woman to receive a medical degree - https://en.wikipedia.org/wiki/Elizabeth_Blackwell + "blackwell", + + // Niels Bohr is the father of quantum theory. https://en.wikipedia.org/wiki/Niels_Bohr. + "bohr", + + // Kathleen Booth, she's credited with writing the first assembly language. https://en.wikipedia.org/wiki/Kathleen_Booth + "booth", + + // Anita Borg - Anita Borg was the founding director of the Institute for Women and Technology (IWT). https://en.wikipedia.org/wiki/Anita_Borg + "borg", + + // Satyendra Nath Bose - He provided the foundation for Bose–Einstein statistics and the theory of the Bose–Einstein condensate. - https://en.wikipedia.org/wiki/Satyendra_Nath_Bose + "bose", + + // Evelyn Boyd Granville - She was one of the first African-American woman to receive a Ph.D. in mathematics; she earned it in 1949 from Yale University. https://en.wikipedia.org/wiki/Evelyn_Boyd_Granville + "boyd", + + // Brahmagupta - Ancient Indian mathematician during 598-670 CE who gave rules to compute with zero - https://en.wikipedia.org/wiki/Brahmagupta#Zero + "brahmagupta", + + // Walter Houser Brattain co-invented the transistor - https://en.wikipedia.org/wiki/Walter_Houser_Brattain + "brattain", + + // Emmett Brown invented time travel. https://en.wikipedia.org/wiki/Emmett_Brown (thanks Brian Goff) + "brown", + + // Rachel Carson - American marine biologist and conservationist, her book Silent Spring and other writings are credited with advancing the global environmental movement. https://en.wikipedia.org/wiki/Rachel_Carson + "carson", + + // Subrahmanyan Chandrasekhar - Astrophysicist known for his mathematical theory on different stages and evolution in structures of the stars. He has won nobel prize for physics - https://en.wikipedia.org/wiki/Subrahmanyan_Chandrasekhar + "chandrasekhar", + + // Jane Colden - American botanist widely considered the first female American botanist - https://en.wikipedia.org/wiki/Jane_Colden + "colden", + + // Gerty Theresa Cori - American biochemist who became the third woman—and first American woman—to win a Nobel Prize in science, and the first woman to be awarded the Nobel Prize in Physiology or Medicine. Cori was born in Prague. https://en.wikipedia.org/wiki/Gerty_Cori + "cori", + + // Seymour Roger Cray was an American electrical engineer and supercomputer architect who designed a series of computers that were the fastest in the world for decades. https://en.wikipedia.org/wiki/Seymour_Cray + "cray", + + // Marie Curie discovered radioactivity. https://en.wikipedia.org/wiki/Marie_Curie. + "curie", + + // Charles Darwin established the principles of natural evolution. https://en.wikipedia.org/wiki/Charles_Darwin. + "darwin", + + // Leonardo Da Vinci invented too many things to list here. https://en.wikipedia.org/wiki/Leonardo_da_Vinci. + "davinci", + + // Edsger Wybe Dijkstra was a Dutch computer scientist and mathematical scientist. https://en.wikipedia.org/wiki/Edsger_W._Dijkstra. + "dijkstra", + + // Donna Dubinsky - played an integral role in the development of personal digital assistants (PDAs) serving as CEO of Palm, Inc. and co-founding Handspring. https://en.wikipedia.org/wiki/Donna_Dubinsky + "dubinsky", + + // Annie Easley - She was a leading member of the team which developed software for the Centaur rocket stage and one of the first African-Americans in her field. https://en.wikipedia.org/wiki/Annie_Easley + "easley", + + // Albert Einstein invented the general theory of relativity. https://en.wikipedia.org/wiki/Albert_Einstein + "einstein", + + // Gertrude Elion - American biochemist, pharmacologist and the 1988 recipient of the Nobel Prize in Medicine - https://en.wikipedia.org/wiki/Gertrude_Elion + "elion", + + // Douglas Engelbart gave the mother of all demos: https://en.wikipedia.org/wiki/Douglas_Engelbart + "engelbart", + + // Euclid invented geometry. https://en.wikipedia.org/wiki/Euclid + "euclid", + + // Leonhard Euler invented large parts of modern mathematics. https://de.wikipedia.org/wiki/Leonhard_Euler + "euler", + + // Pierre de Fermat pioneered several aspects of modern mathematics. https://en.wikipedia.org/wiki/Pierre_de_Fermat + "fermat", + + // Enrico Fermi invented the first nuclear reactor. https://en.wikipedia.org/wiki/Enrico_Fermi. + "fermi", + + // Richard Feynman was a key contributor to quantum mechanics and particle physics. https://en.wikipedia.org/wiki/Richard_Feynman + "feynman", + + // Benjamin Franklin is famous for his experiments in electricity and the invention of the lightning rod. + "franklin", + + // Galileo was a founding father of modern astronomy, and faced politics and obscurantism to establish scientific truth. https://en.wikipedia.org/wiki/Galileo_Galilei + "galileo", + + // William Henry "Bill" Gates III is an American business magnate, philanthropist, investor, computer programmer, and inventor. https://en.wikipedia.org/wiki/Bill_Gates + "gates", + + // Adele Goldberg, was one of the designers and developers of the Smalltalk language. https://en.wikipedia.org/wiki/Adele_Goldberg_(computer_scientist) + "goldberg", + + // Adele Goldstine, born Adele Katz, wrote the complete technical description for the first electronic digital computer, ENIAC. https://en.wikipedia.org/wiki/Adele_Goldstine + "goldstine", + + // Shafi Goldwasser is a computer scientist known for creating theoretical foundations of modern cryptography. Winner of 2012 ACM Turing Award. https://en.wikipedia.org/wiki/Shafi_Goldwasser + "goldwasser", + + // James Golick, all around gangster. + "golick", + + // Jane Goodall - British primatologist, ethologist, and anthropologist who is considered to be the world's foremost expert on chimpanzees - https://en.wikipedia.org/wiki/Jane_Goodall + "goodall", + + // Margaret Hamilton - Director of the Software Engineering Division of the MIT Instrumentation Laboratory, which developed on-board flight software for the Apollo space program. https://en.wikipedia.org/wiki/Margaret_Hamilton_(scientist) + "hamilton", + + // Stephen Hawking pioneered the field of cosmology by combining general relativity and quantum mechanics. https://en.wikipedia.org/wiki/Stephen_Hawking + "hawking", + + // Werner Heisenberg was a founding father of quantum mechanics. https://en.wikipedia.org/wiki/Werner_Heisenberg + "heisenberg", + + // Jaroslav Heyrovský was the inventor of the polarographic method, father of the electroanalytical method, and recipient of the Nobel Prize in 1959. His main field of work was polarography. https://en.wikipedia.org/wiki/Jaroslav_Heyrovsk%C3%BD + "heyrovsky", + + // Dorothy Hodgkin was a British biochemist, credited with the development of protein crystallography. She was awarded the Nobel Prize in Chemistry in 1964. https://en.wikipedia.org/wiki/Dorothy_Hodgkin + "hodgkin", + + // Erna Schneider Hoover revolutionized modern communication by inventing a computerized telephon switching method. https://en.wikipedia.org/wiki/Erna_Schneider_Hoover + "hoover", + + // Grace Hopper developed the first compiler for a computer programming language and is credited with popularizing the term "debugging" for fixing computer glitches. https://en.wikipedia.org/wiki/Grace_Hopper + "hopper", + + // Frances Hugle, she was an American scientist, engineer, and inventor who contributed to the understanding of semiconductors, integrated circuitry, and the unique electrical principles of microscopic materials. https://en.wikipedia.org/wiki/Frances_Hugle + "hugle", + + // Hypatia - Greek Alexandrine Neoplatonist philosopher in Egypt who was one of the earliest mothers of mathematics - https://en.wikipedia.org/wiki/Hypatia + "hypatia", + + // Yeong-Sil Jang was a Korean scientist and astronomer during the Joseon Dynasty; he invented the first metal printing press and water gauge. https://en.wikipedia.org/wiki/Jang_Yeong-sil + "jang", + + // Betty Jennings - one of the original programmers of the ENIAC. https://en.wikipedia.org/wiki/ENIAC - https://en.wikipedia.org/wiki/Jean_Bartik + "jennings", + + // Mary Lou Jepsen, was the founder and chief technology officer of One Laptop Per Child (OLPC), and the founder of Pixel Qi. https://en.wikipedia.org/wiki/Mary_Lou_Jepsen + "jepsen", + + // Irène Joliot-Curie - French scientist who was awarded the Nobel Prize for Chemistry in 1935. Daughter of Marie and Pierre Curie. https://en.wikipedia.org/wiki/Ir%C3%A8ne_Joliot-Curie + "joliot", + + // Karen Spärck Jones came up with the concept of inverse document frequency, which is used in most search engines today. https://en.wikipedia.org/wiki/Karen_Sp%C3%A4rck_Jones + "jones", + + // A. P. J. Abdul Kalam - is an Indian scientist aka Missile Man of India for his work on the development of ballistic missile and launch vehicle technology - https://en.wikipedia.org/wiki/A._P._J._Abdul_Kalam + "kalam", + + // Susan Kare, created the icons and many of the interface elements for the original Apple Macintosh in the 1980s, and was an original employee of NeXT, working as the Creative Director. https://en.wikipedia.org/wiki/Susan_Kare + "kare", + + // Mary Kenneth Keller, Sister Mary Kenneth Keller became the first American woman to earn a PhD in Computer Science in 1965. https://en.wikipedia.org/wiki/Mary_Kenneth_Keller + "keller", + + // Har Gobind Khorana - Indian-American biochemist who shared the 1968 Nobel Prize for Physiology - https://en.wikipedia.org/wiki/Har_Gobind_Khorana + "khorana", + + // Jack Kilby invented silicone integrated circuits and gave Silicon Valley its name. - https://en.wikipedia.org/wiki/Jack_Kilby + "kilby", + + // Maria Kirch - German astronomer and first woman to discover a comet - https://en.wikipedia.org/wiki/Maria_Margarethe_Kirch + "kirch", + + // Donald Knuth - American computer scientist, author of "The Art of Computer Programming" and creator of the TeX typesetting system. https://en.wikipedia.org/wiki/Donald_Knuth + "knuth", + + // Sophie Kowalevski - Russian mathematician responsible for important original contributions to analysis, differential equations and mechanics - https://en.wikipedia.org/wiki/Sofia_Kovalevskaya + "kowalevski", + + // Marie-Jeanne de Lalande - French astronomer, mathematician and cataloguer of stars - https://en.wikipedia.org/wiki/Marie-Jeanne_de_Lalande + "lalande", + + // Hedy Lamarr - Actress and inventor. The principles of her work are now incorporated into modern Wi-Fi, CDMA and Bluetooth technology. https://en.wikipedia.org/wiki/Hedy_Lamarr + "lamarr", + + // Mary Leakey - British paleoanthropologist who discovered the first fossilized Proconsul skull - https://en.wikipedia.org/wiki/Mary_Leakey + "leakey", + + // Henrietta Swan Leavitt - she was an American astronomer who discovered the relation between the luminosity and the period of Cepheid variable stars. https://en.wikipedia.org/wiki/Henrietta_Swan_Leavitt + "leavitt", + + // Ruth Lichterman - one of the original programmers of the ENIAC. https://en.wikipedia.org/wiki/ENIAC - https://en.wikipedia.org/wiki/Ruth_Teitelbaum + "lichterman", + + // Barbara Liskov - co-developed the Liskov substitution principle. Liskov was also the winner of the Turing Prize in 2008. - https://en.wikipedia.org/wiki/Barbara_Liskov + "liskov", + + // Ada Lovelace invented the first algorithm. https://en.wikipedia.org/wiki/Ada_Lovelace (thanks James Turnbull) + "lovelace", + + // Auguste and Louis Lumière - the first filmmakers in history - https://en.wikipedia.org/wiki/Auguste_and_Louis_Lumi%C3%A8re + "lumiere", + + // Mahavira - Ancient Indian mathematician during 9th century AD who discovered basic algebraic identities - https://en.wikipedia.org/wiki/Mah%C4%81v%C4%ABra_(mathematician) + "mahavira", + + // Maria Mayer - American theoretical physicist and Nobel laureate in Physics for proposing the nuclear shell model of the atomic nucleus - https://en.wikipedia.org/wiki/Maria_Mayer + "mayer", + + // John McCarthy invented LISP: https://en.wikipedia.org/wiki/John_McCarthy_(computer_scientist) + "mccarthy", + + // Barbara McClintock - a distinguished American cytogeneticist, 1983 Nobel Laureate in Physiology or Medicine for discovering transposons. https://en.wikipedia.org/wiki/Barbara_McClintock + "mcclintock", + + // Malcolm McLean invented the modern shipping container: https://en.wikipedia.org/wiki/Malcom_McLean + "mclean", + + // Kay McNulty - one of the original programmers of the ENIAC. https://en.wikipedia.org/wiki/ENIAC - https://en.wikipedia.org/wiki/Kathleen_Antonelli + "mcnulty", + + // Lise Meitner - Austrian/Swedish physicist who was involved in the discovery of nuclear fission. The element meitnerium is named after her - https://en.wikipedia.org/wiki/Lise_Meitner + "meitner", + + // Carla Meninsky, was the game designer and programmer for Atari 2600 games Dodge 'Em and Warlords. https://en.wikipedia.org/wiki/Carla_Meninsky + "meninsky", + + // Johanna Mestorf - German prehistoric archaeologist and first female museum director in Germany - https://en.wikipedia.org/wiki/Johanna_Mestorf + "mestorf", + + // Marvin Minsky - Pioneer in Artificial Intelligence, co-founder of the MIT's AI Lab, won the Turing Award in 1969. https://en.wikipedia.org/wiki/Marvin_Minsky + "minsky", + + // Maryam Mirzakhani - an Iranian mathematician and the first woman to win the Fields Medal. https://en.wikipedia.org/wiki/Maryam_Mirzakhani + "mirzakhani", + + // Samuel Morse - contributed to the invention of a single-wire telegraph system based on European telegraphs and was a co-developer of the Morse code - https://en.wikipedia.org/wiki/Samuel_Morse + "morse", + + // Ian Murdock - founder of the Debian project - https://en.wikipedia.org/wiki/Ian_Murdock + "murdock", + + // Isaac Newton invented classic mechanics and modern optics. https://en.wikipedia.org/wiki/Isaac_Newton + "newton", + + // Alfred Nobel - a Swedish chemist, engineer, innovator, and armaments manufacturer (inventor of dynamite) - https://en.wikipedia.org/wiki/Alfred_Nobel + "nobel", + + // Emmy Noether, German mathematician. Noether's Theorem is named after her. https://en.wikipedia.org/wiki/Emmy_Noether + "noether", + + // Poppy Northcutt. Poppy Northcutt was the first woman to work as part of NASA’s Mission Control. http://www.businessinsider.com/poppy-northcutt-helped-apollo-astronauts-2014-12?op=1 + "northcutt", + + // Robert Noyce invented silicone integrated circuits and gave Silicon Valley its name. - https://en.wikipedia.org/wiki/Robert_Noyce + "noyce", + + // Panini - Ancient Indian linguist and grammarian from 4th century CE who worked on the world's first formal system - https://en.wikipedia.org/wiki/P%C4%81%E1%B9%87ini#Comparison_with_modern_formal_systems + "panini", + + // Ambroise Pare invented modern surgery. https://en.wikipedia.org/wiki/Ambroise_Par%C3%A9 + "pare", + + // Louis Pasteur discovered vaccination, fermentation and pasteurization. https://en.wikipedia.org/wiki/Louis_Pasteur. + "pasteur", + + // Cecilia Payne-Gaposchkin was an astronomer and astrophysicist who, in 1925, proposed in her Ph.D. thesis an explanation for the composition of stars in terms of the relative abundances of hydrogen and helium. https://en.wikipedia.org/wiki/Cecilia_Payne-Gaposchkin + "payne", + + // Radia Perlman is a software designer and network engineer and most famous for her invention of the spanning-tree protocol (STP). https://en.wikipedia.org/wiki/Radia_Perlman + "perlman", + + // Rob Pike was a key contributor to Unix, Plan 9, the X graphic system, utf-8, and the Go programming language. https://en.wikipedia.org/wiki/Rob_Pike + "pike", + + // Henri Poincaré made fundamental contributions in several fields of mathematics. https://en.wikipedia.org/wiki/Henri_Poincar%C3%A9 + "poincare", + + // Laura Poitras is a director and producer whose work, made possible by open source crypto tools, advances the causes of truth and freedom of information by reporting disclosures by whistleblowers such as Edward Snowden. https://en.wikipedia.org/wiki/Laura_Poitras + "poitras", + + // Claudius Ptolemy - a Greco-Egyptian writer of Alexandria, known as a mathematician, astronomer, geographer, astrologer, and poet of a single epigram in the Greek Anthology - https://en.wikipedia.org/wiki/Ptolemy + "ptolemy", + + // C. V. Raman - Indian physicist who won the Nobel Prize in 1930 for proposing the Raman effect. - https://en.wikipedia.org/wiki/C._V._Raman + "raman", + + // Srinivasa Ramanujan - Indian mathematician and autodidact who made extraordinary contributions to mathematical analysis, number theory, infinite series, and continued fractions. - https://en.wikipedia.org/wiki/Srinivasa_Ramanujan + "ramanujan", + + // Sally Kristen Ride was an American physicist and astronaut. She was the first American woman in space, and the youngest American astronaut. https://en.wikipedia.org/wiki/Sally_Ride + "ride", + + // Dennis Ritchie - co-creator of UNIX and the C programming language. - https://en.wikipedia.org/wiki/Dennis_Ritchie + "ritchie", + + // Wilhelm Conrad Röntgen - German physicist who was awarded the first Nobel Prize in Physics in 1901 for the discovery of X-rays (Röntgen rays). https://en.wikipedia.org/wiki/Wilhelm_R%C3%B6ntgen + "roentgen", + + // Rosalind Franklin - British biophysicist and X-ray crystallographer whose research was critical to the understanding of DNA - https://en.wikipedia.org/wiki/Rosalind_Franklin + "rosalind", + + // Meghnad Saha - Indian astrophysicist best known for his development of the Saha equation, used to describe chemical and physical conditions in stars - https://en.wikipedia.org/wiki/Meghnad_Saha + "saha", + + // Jean E. Sammet developed FORMAC, the first widely used computer language for symbolic manipulation of mathematical formulas. https://en.wikipedia.org/wiki/Jean_E._Sammet + "sammet", + + // Carol Shaw - Originally an Atari employee, Carol Shaw is said to be the first female video game designer. https://en.wikipedia.org/wiki/Carol_Shaw_(video_game_designer) + "shaw", + + // Dame Stephanie "Steve" Shirley - Founded a software company in 1962 employing women working from home. https://en.wikipedia.org/wiki/Steve_Shirley + "shirley", + + // William Shockley co-invented the transistor - https://en.wikipedia.org/wiki/William_Shockley + "shockley", + + // Françoise Barré-Sinoussi - French virologist and Nobel Prize Laureate in Physiology or Medicine; her work was fundamental in identifying HIV as the cause of AIDS. https://en.wikipedia.org/wiki/Fran%C3%A7oise_Barr%C3%A9-Sinoussi + "sinoussi", + + // Betty Snyder - one of the original programmers of the ENIAC. https://en.wikipedia.org/wiki/ENIAC - https://en.wikipedia.org/wiki/Betty_Holberton + "snyder", + + // Frances Spence - one of the original programmers of the ENIAC. https://en.wikipedia.org/wiki/ENIAC - https://en.wikipedia.org/wiki/Frances_Spence + "spence", + + // Richard Matthew Stallman - the founder of the Free Software movement, the GNU project, the Free Software Foundation, and the League for Programming Freedom. He also invented the concept of copyleft to protect the ideals of this movement, and enshrined this concept in the widely-used GPL (General Public License) for software. https://en.wikiquote.org/wiki/Richard_Stallman + "stallman", + + // Michael Stonebraker is a database research pioneer and architect of Ingres, Postgres, VoltDB and SciDB. Winner of 2014 ACM Turing Award. https://en.wikipedia.org/wiki/Michael_Stonebraker + "stonebraker", + + // Janese Swanson (with others) developed the first of the Carmen Sandiego games. She went on to found Girl Tech. https://en.wikipedia.org/wiki/Janese_Swanson + "swanson", + + // Aaron Swartz was influential in creating RSS, Markdown, Creative Commons, Reddit, and much of the internet as we know it today. He was devoted to freedom of information on the web. https://en.wikiquote.org/wiki/Aaron_Swartz + "swartz", + + // Bertha Swirles was a theoretical physicist who made a number of contributions to early quantum theory. https://en.wikipedia.org/wiki/Bertha_Swirles + "swirles", + + // Nikola Tesla invented the AC electric system and every gadget ever used by a James Bond villain. https://en.wikipedia.org/wiki/Nikola_Tesla + "tesla", + + // Ken Thompson - co-creator of UNIX and the C programming language - https://en.wikipedia.org/wiki/Ken_Thompson + "thompson", + + // Linus Torvalds invented Linux and Git. https://en.wikipedia.org/wiki/Linus_Torvalds + "torvalds", + + // Alan Turing was a founding father of computer science. https://en.wikipedia.org/wiki/Alan_Turing. + "turing", + + // Varahamihira - Ancient Indian mathematician who discovered trigonometric formulae during 505-587 CE - https://en.wikipedia.org/wiki/Var%C4%81hamihira#Contributions + "varahamihira", + + // Sir Mokshagundam Visvesvaraya - is a notable Indian engineer. He is a recipient of the Indian Republic's highest honour, the Bharat Ratna, in 1955. On his birthday, 15 September is celebrated as Engineer's Day in India in his memory - https://en.wikipedia.org/wiki/Visvesvaraya + "visvesvaraya", + + // Christiane Nüsslein-Volhard - German biologist, won Nobel Prize in Physiology or Medicine in 1995 for research on the genetic control of embryonic development. https://en.wikipedia.org/wiki/Christiane_N%C3%BCsslein-Volhard + "volhard", + + // Marlyn Wescoff - one of the original programmers of the ENIAC. https://en.wikipedia.org/wiki/ENIAC - https://en.wikipedia.org/wiki/Marlyn_Meltzer + "wescoff", + + // Roberta Williams, did pioneering work in graphical adventure games for personal computers, particularly the King's Quest series. https://en.wikipedia.org/wiki/Roberta_Williams + "williams", + + // Sophie Wilson designed the first Acorn Micro-Computer and the instruction set for ARM processors. https://en.wikipedia.org/wiki/Sophie_Wilson + "wilson", + + // Jeannette Wing - co-developed the Liskov substitution principle. - https://en.wikipedia.org/wiki/Jeannette_Wing + "wing", + + // Steve Wozniak invented the Apple I and Apple II. https://en.wikipedia.org/wiki/Steve_Wozniak + "wozniak", + + // The Wright brothers, Orville and Wilbur - credited with inventing and building the world's first successful airplane and making the first controlled, powered and sustained heavier-than-air human flight - https://en.wikipedia.org/wiki/Wright_brothers + "wright", + + // Rosalyn Sussman Yalow - Rosalyn Sussman Yalow was an American medical physicist, and a co-winner of the 1977 Nobel Prize in Physiology or Medicine for development of the radioimmunoassay technique. https://en.wikipedia.org/wiki/Rosalyn_Sussman_Yalow + "yalow", + + // Ada Yonath - an Israeli crystallographer, the first woman from the Middle East to win a Nobel prize in the sciences. https://en.wikipedia.org/wiki/Ada_Yonath + "yonath", + } +) + +// GetRandomName generates a random name from the list of adjectives and surnames in this package +// formatted as "adjective_surname". For example 'focused_turing'. If retry is non-zero, a random +// integer between 0 and 10 will be added to the end of the name, e.g `focused_turing3` +func GetRandomName(retry int) string { + rnd := random.Rand +begin: + name := fmt.Sprintf("%s_%s", left[rnd.Intn(len(left))], right[rnd.Intn(len(right))]) + if name == "boring_wozniak" /* Steve Wozniak is not boring */ { + goto begin + } + + if retry > 0 { + name = fmt.Sprintf("%s%d", name, rnd.Intn(10)) + } + return name +} diff --git a/pkg/namesgenerator/names-generator_test.go b/pkg/namesgenerator/names-generator_test.go new file mode 100644 index 00000000..f7dae276 --- /dev/null +++ b/pkg/namesgenerator/names-generator_test.go @@ -0,0 +1,45 @@ +package namesgenerator + +import ( + "strings" + "testing" +) + +// Make sure the generated names are awesome +func TestGenerateAwesomeNames(t *testing.T) { + name := GetRandomName(0) + if !isAwesome(name) { + t.Fatalf("Generated name '%s' is not awesome.", name) + } +} + +func TestNameFormat(t *testing.T) { + name := GetRandomName(0) + if !strings.Contains(name, "_") { + t.Fatalf("Generated name does not contain an underscore") + } + if strings.ContainsAny(name, "0123456789") { + t.Fatalf("Generated name contains numbers!") + } +} + +func TestNameRetries(t *testing.T) { + name := GetRandomName(1) + if !strings.Contains(name, "_") { + t.Fatalf("Generated name does not contain an underscore") + } + if !strings.ContainsAny(name, "0123456789") { + t.Fatalf("Generated name doesn't contain a number") + } + +} + +// To be awesome, a container name must involve cool inventors, be easy to remember, +// be at least mildly funny, and always be politically correct for enterprise adoption. +func isAwesome(name string) bool { + coolInventorNames := true + easyToRemember := true + mildlyFunnyOnOccasion := true + politicallyCorrect := true + return coolInventorNames && easyToRemember && mildlyFunnyOnOccasion && politicallyCorrect +} diff --git a/pkg/parsers/kernel/kernel.go b/pkg/parsers/kernel/kernel.go new file mode 100644 index 00000000..a21ba137 --- /dev/null +++ b/pkg/parsers/kernel/kernel.go @@ -0,0 +1,100 @@ +// +build !windows + +// Package kernel provides helper function to get, parse and compare kernel +// versions for different platforms. +package kernel + +import ( + "bytes" + "errors" + "fmt" +) + +// VersionInfo holds information about the kernel. +type VersionInfo struct { + Kernel int // Version of the kernel (e.g. 4.1.2-generic -> 4) + Major int // Major part of the kernel version (e.g. 4.1.2-generic -> 1) + Minor int // Minor part of the kernel version (e.g. 4.1.2-generic -> 2) + Flavor string // Flavor of the kernel version (e.g. 4.1.2-generic -> generic) +} + +func (k *VersionInfo) String() string { + return fmt.Sprintf("%d.%d.%d%s", k.Kernel, k.Major, k.Minor, k.Flavor) +} + +// CompareKernelVersion compares two kernel.VersionInfo structs. +// Returns -1 if a < b, 0 if a == b, 1 it a > b +func CompareKernelVersion(a, b VersionInfo) int { + if a.Kernel < b.Kernel { + return -1 + } else if a.Kernel > b.Kernel { + return 1 + } + + if a.Major < b.Major { + return -1 + } else if a.Major > b.Major { + return 1 + } + + if a.Minor < b.Minor { + return -1 + } else if a.Minor > b.Minor { + return 1 + } + + return 0 +} + +// GetKernelVersion gets the current kernel version. +func GetKernelVersion() (*VersionInfo, error) { + var ( + err error + ) + + uts, err := uname() + if err != nil { + return nil, err + } + + release := make([]byte, len(uts.Release)) + + i := 0 + for _, c := range uts.Release { + release[i] = byte(c) + i++ + } + + // Remove the \x00 from the release for Atoi to parse correctly + release = release[:bytes.IndexByte(release, 0)] + + return ParseRelease(string(release)) +} + +// ParseRelease parses a string and creates a VersionInfo based on it. +func ParseRelease(release string) (*VersionInfo, error) { + var ( + kernel, major, minor, parsed int + flavor, partial string + ) + + // Ignore error from Sscanf to allow an empty flavor. Instead, just + // make sure we got all the version numbers. + parsed, _ = fmt.Sscanf(release, "%d.%d%s", &kernel, &major, &partial) + if parsed < 2 { + return nil, errors.New("Can't parse kernel version " + release) + } + + // sometimes we have 3.12.25-gentoo, but sometimes we just have 3.12-1-amd64 + parsed, _ = fmt.Sscanf(partial, ".%d%s", &minor, &flavor) + if parsed < 1 { + flavor = partial + } + + return &VersionInfo{ + Kernel: kernel, + Major: major, + Minor: minor, + Flavor: flavor, + }, nil +} diff --git a/pkg/parsers/kernel/kernel_unix_test.go b/pkg/parsers/kernel/kernel_unix_test.go new file mode 100644 index 00000000..dc8c0e30 --- /dev/null +++ b/pkg/parsers/kernel/kernel_unix_test.go @@ -0,0 +1,96 @@ +// +build !windows + +package kernel + +import ( + "fmt" + "testing" +) + +func assertParseRelease(t *testing.T, release string, b *VersionInfo, result int) { + var ( + a *VersionInfo + ) + a, _ = ParseRelease(release) + + if r := CompareKernelVersion(*a, *b); r != result { + t.Fatalf("Unexpected kernel version comparison result for (%v,%v). Found %d, expected %d", release, b, r, result) + } + if a.Flavor != b.Flavor { + t.Fatalf("Unexpected parsed kernel flavor. Found %s, expected %s", a.Flavor, b.Flavor) + } +} + +// TestParseRelease tests the ParseRelease() function +func TestParseRelease(t *testing.T) { + assertParseRelease(t, "3.8.0", &VersionInfo{Kernel: 3, Major: 8, Minor: 0}, 0) + assertParseRelease(t, "3.4.54.longterm-1", &VersionInfo{Kernel: 3, Major: 4, Minor: 54, Flavor: ".longterm-1"}, 0) + assertParseRelease(t, "3.4.54.longterm-1", &VersionInfo{Kernel: 3, Major: 4, Minor: 54, Flavor: ".longterm-1"}, 0) + assertParseRelease(t, "3.8.0-19-generic", &VersionInfo{Kernel: 3, Major: 8, Minor: 0, Flavor: "-19-generic"}, 0) + assertParseRelease(t, "3.12.8tag", &VersionInfo{Kernel: 3, Major: 12, Minor: 8, Flavor: "tag"}, 0) + assertParseRelease(t, "3.12-1-amd64", &VersionInfo{Kernel: 3, Major: 12, Minor: 0, Flavor: "-1-amd64"}, 0) + assertParseRelease(t, "3.8.0", &VersionInfo{Kernel: 4, Major: 8, Minor: 0}, -1) + // Errors + invalids := []string{ + "3", + "a", + "a.a", + "a.a.a-a", + } + for _, invalid := range invalids { + expectedMessage := fmt.Sprintf("Can't parse kernel version %v", invalid) + if _, err := ParseRelease(invalid); err == nil || err.Error() != expectedMessage { + + } + } +} + +func assertKernelVersion(t *testing.T, a, b VersionInfo, result int) { + if r := CompareKernelVersion(a, b); r != result { + t.Fatalf("Unexpected kernel version comparison result. Found %d, expected %d", r, result) + } +} + +// TestCompareKernelVersion tests the CompareKernelVersion() function +func TestCompareKernelVersion(t *testing.T) { + assertKernelVersion(t, + VersionInfo{Kernel: 3, Major: 8, Minor: 0}, + VersionInfo{Kernel: 3, Major: 8, Minor: 0}, + 0) + assertKernelVersion(t, + VersionInfo{Kernel: 2, Major: 6, Minor: 0}, + VersionInfo{Kernel: 3, Major: 8, Minor: 0}, + -1) + assertKernelVersion(t, + VersionInfo{Kernel: 3, Major: 8, Minor: 0}, + VersionInfo{Kernel: 2, Major: 6, Minor: 0}, + 1) + assertKernelVersion(t, + VersionInfo{Kernel: 3, Major: 8, Minor: 0}, + VersionInfo{Kernel: 3, Major: 8, Minor: 0}, + 0) + assertKernelVersion(t, + VersionInfo{Kernel: 3, Major: 8, Minor: 5}, + VersionInfo{Kernel: 3, Major: 8, Minor: 0}, + 1) + assertKernelVersion(t, + VersionInfo{Kernel: 3, Major: 0, Minor: 20}, + VersionInfo{Kernel: 3, Major: 8, Minor: 0}, + -1) + assertKernelVersion(t, + VersionInfo{Kernel: 3, Major: 7, Minor: 20}, + VersionInfo{Kernel: 3, Major: 8, Minor: 0}, + -1) + assertKernelVersion(t, + VersionInfo{Kernel: 3, Major: 8, Minor: 20}, + VersionInfo{Kernel: 3, Major: 7, Minor: 0}, + 1) + assertKernelVersion(t, + VersionInfo{Kernel: 3, Major: 8, Minor: 20}, + VersionInfo{Kernel: 3, Major: 8, Minor: 0}, + 1) + assertKernelVersion(t, + VersionInfo{Kernel: 3, Major: 8, Minor: 0}, + VersionInfo{Kernel: 3, Major: 8, Minor: 20}, + -1) +} diff --git a/pkg/parsers/kernel/kernel_windows.go b/pkg/parsers/kernel/kernel_windows.go new file mode 100644 index 00000000..85ca250c --- /dev/null +++ b/pkg/parsers/kernel/kernel_windows.go @@ -0,0 +1,67 @@ +package kernel + +import ( + "fmt" + "syscall" + "unsafe" +) + +// VersionInfo holds information about the kernel. +type VersionInfo struct { + kvi string // Version of the kernel (e.g. 6.1.7601.17592 -> 6) + major int // Major part of the kernel version (e.g. 6.1.7601.17592 -> 1) + minor int // Minor part of the kernel version (e.g. 6.1.7601.17592 -> 7601) + build int // Build number of the kernel version (e.g. 6.1.7601.17592 -> 17592) +} + +func (k *VersionInfo) String() string { + return fmt.Sprintf("%d.%d %d (%s)", k.major, k.minor, k.build, k.kvi) +} + +// GetKernelVersion gets the current kernel version. +func GetKernelVersion() (*VersionInfo, error) { + + var ( + h syscall.Handle + dwVersion uint32 + err error + ) + + KVI := &VersionInfo{"Unknown", 0, 0, 0} + + if err = syscall.RegOpenKeyEx(syscall.HKEY_LOCAL_MACHINE, + syscall.StringToUTF16Ptr(`SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\`), + 0, + syscall.KEY_READ, + &h); err != nil { + return KVI, err + } + defer syscall.RegCloseKey(h) + + var buf [1 << 10]uint16 + var typ uint32 + n := uint32(len(buf) * 2) // api expects array of bytes, not uint16 + + if err = syscall.RegQueryValueEx(h, + syscall.StringToUTF16Ptr("BuildLabEx"), + nil, + &typ, + (*byte)(unsafe.Pointer(&buf[0])), + &n); err != nil { + return KVI, err + } + + KVI.kvi = syscall.UTF16ToString(buf[:]) + + // Important - docker.exe MUST be manifested for this API to return + // the correct information. + if dwVersion, err = syscall.GetVersion(); err != nil { + return KVI, err + } + + KVI.major = int(dwVersion & 0xFF) + KVI.minor = int((dwVersion & 0XFF00) >> 8) + KVI.build = int((dwVersion & 0xFFFF0000) >> 16) + + return KVI, nil +} diff --git a/pkg/parsers/kernel/uname_linux.go b/pkg/parsers/kernel/uname_linux.go new file mode 100644 index 00000000..7d12fcbd --- /dev/null +++ b/pkg/parsers/kernel/uname_linux.go @@ -0,0 +1,19 @@ +package kernel + +import ( + "syscall" +) + +// Utsname represents the system name structure. +// It is passthgrouh for syscall.Utsname in order to make it portable with +// other platforms where it is not available. +type Utsname syscall.Utsname + +func uname() (*syscall.Utsname, error) { + uts := &syscall.Utsname{} + + if err := syscall.Uname(uts); err != nil { + return nil, err + } + return uts, nil +} diff --git a/pkg/parsers/kernel/uname_unsupported.go b/pkg/parsers/kernel/uname_unsupported.go new file mode 100644 index 00000000..79c66b32 --- /dev/null +++ b/pkg/parsers/kernel/uname_unsupported.go @@ -0,0 +1,18 @@ +// +build !linux + +package kernel + +import ( + "errors" +) + +// Utsname represents the system name structure. +// It is defined here to make it portable as it is available on linux but not +// on windows. +type Utsname struct { + Release [65]byte +} + +func uname() (*Utsname, error) { + return nil, errors.New("Kernel version detection is available only on linux") +} diff --git a/pkg/parsers/operatingsystem/operatingsystem_freebsd.go b/pkg/parsers/operatingsystem/operatingsystem_freebsd.go new file mode 100644 index 00000000..0589cf2a --- /dev/null +++ b/pkg/parsers/operatingsystem/operatingsystem_freebsd.go @@ -0,0 +1,18 @@ +package operatingsystem + +import ( + "errors" +) + +// GetOperatingSystem gets the name of the current operating system. +func GetOperatingSystem() (string, error) { + // TODO: Implement OS detection + return "", errors.New("Cannot detect OS version") +} + +// IsContainerized returns true if we are running inside a container. +// No-op on FreeBSD, always returns false. +func IsContainerized() (bool, error) { + // TODO: Implement jail detection + return false, errors.New("Cannot detect if we are in container") +} diff --git a/pkg/parsers/operatingsystem/operatingsystem_linux.go b/pkg/parsers/operatingsystem/operatingsystem_linux.go new file mode 100644 index 00000000..e04a3499 --- /dev/null +++ b/pkg/parsers/operatingsystem/operatingsystem_linux.go @@ -0,0 +1,77 @@ +// Package operatingsystem provides helper function to get the operating system +// name for different platforms. +package operatingsystem + +import ( + "bufio" + "bytes" + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/mattn/go-shellwords" +) + +var ( + // file to use to detect if the daemon is running in a container + proc1Cgroup = "/proc/1/cgroup" + + // file to check to determine Operating System + etcOsRelease = "/etc/os-release" + + // used by stateless systems like Clear Linux + altOsRelease = "/usr/lib/os-release" +) + +// GetOperatingSystem gets the name of the current operating system. +func GetOperatingSystem() (string, error) { + osReleaseFile, err := os.Open(etcOsRelease) + if err != nil { + if !os.IsNotExist(err) { + return "", fmt.Errorf("Error opening %s: %v", etcOsRelease, err) + } + osReleaseFile, err = os.Open(altOsRelease) + if err != nil { + return "", fmt.Errorf("Error opening %s: %v", altOsRelease, err) + } + } + defer osReleaseFile.Close() + + var prettyName string + scanner := bufio.NewScanner(osReleaseFile) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "PRETTY_NAME=") { + data := strings.SplitN(line, "=", 2) + prettyNames, err := shellwords.Parse(data[1]) + if err != nil { + return "", fmt.Errorf("PRETTY_NAME is invalid: %s", err.Error()) + } + if len(prettyNames) != 1 { + return "", fmt.Errorf("PRETTY_NAME needs to be enclosed by quotes if they have spaces: %s", data[1]) + } + prettyName = prettyNames[0] + } + } + if prettyName != "" { + return prettyName, nil + } + // If not set, defaults to PRETTY_NAME="Linux" + // c.f. http://www.freedesktop.org/software/systemd/man/os-release.html + return "Linux", nil +} + +// IsContainerized returns true if we are running inside a container. +func IsContainerized() (bool, error) { + b, err := ioutil.ReadFile(proc1Cgroup) + if err != nil { + return false, err + } + for _, line := range bytes.Split(b, []byte{'\n'}) { + if len(line) > 0 && !bytes.HasSuffix(line, []byte{'/'}) && !bytes.HasSuffix(line, []byte("init.scope")) { + return true, nil + } + } + return false, nil +} diff --git a/pkg/parsers/operatingsystem/operatingsystem_unix_test.go b/pkg/parsers/operatingsystem/operatingsystem_unix_test.go new file mode 100644 index 00000000..e7120c65 --- /dev/null +++ b/pkg/parsers/operatingsystem/operatingsystem_unix_test.go @@ -0,0 +1,247 @@ +// +build linux freebsd + +package operatingsystem + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +func TestGetOperatingSystem(t *testing.T) { + var backup = etcOsRelease + + invalids := []struct { + content string + errorExpected string + }{ + { + `PRETTY_NAME=Source Mage GNU/Linux +PRETTY_NAME=Ubuntu 14.04.LTS`, + "PRETTY_NAME needs to be enclosed by quotes if they have spaces: Source Mage GNU/Linux", + }, + { + `PRETTY_NAME="Ubuntu Linux +PRETTY_NAME=Ubuntu 14.04.LTS`, + "PRETTY_NAME is invalid: invalid command line string", + }, + { + `PRETTY_NAME=Ubuntu' +PRETTY_NAME=Ubuntu 14.04.LTS`, + "PRETTY_NAME is invalid: invalid command line string", + }, + { + `PRETTY_NAME' +PRETTY_NAME=Ubuntu 14.04.LTS`, + "PRETTY_NAME needs to be enclosed by quotes if they have spaces: Ubuntu 14.04.LTS", + }, + } + + valids := []struct { + content string + expected string + }{ + { + `NAME="Ubuntu" +PRETTY_NAME_AGAIN="Ubuntu 14.04.LTS" +VERSION="14.04, Trusty Tahr" +ID=ubuntu +ID_LIKE=debian +VERSION_ID="14.04" +HOME_URL="http://www.ubuntu.com/" +SUPPORT_URL="http://help.ubuntu.com/" +BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`, + "Linux", + }, + { + `NAME="Ubuntu" +VERSION="14.04, Trusty Tahr" +ID=ubuntu +ID_LIKE=debian +VERSION_ID="14.04" +HOME_URL="http://www.ubuntu.com/" +SUPPORT_URL="http://help.ubuntu.com/" +BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`, + "Linux", + }, + { + `NAME=Gentoo +ID=gentoo +PRETTY_NAME="Gentoo/Linux" +ANSI_COLOR="1;32" +HOME_URL="http://www.gentoo.org/" +SUPPORT_URL="http://www.gentoo.org/main/en/support.xml" +BUG_REPORT_URL="https://bugs.gentoo.org/" +`, + "Gentoo/Linux", + }, + { + `NAME="Ubuntu" +VERSION="14.04, Trusty Tahr" +ID=ubuntu +ID_LIKE=debian +PRETTY_NAME="Ubuntu 14.04 LTS" +VERSION_ID="14.04" +HOME_URL="http://www.ubuntu.com/" +SUPPORT_URL="http://help.ubuntu.com/" +BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`, + "Ubuntu 14.04 LTS", + }, + { + `NAME="Ubuntu" +VERSION="14.04, Trusty Tahr" +ID=ubuntu +ID_LIKE=debian +PRETTY_NAME='Ubuntu 14.04 LTS'`, + "Ubuntu 14.04 LTS", + }, + { + `PRETTY_NAME=Source +NAME="Source Mage"`, + "Source", + }, + { + `PRETTY_NAME=Source +PRETTY_NAME="Source Mage"`, + "Source Mage", + }, + } + + dir := os.TempDir() + etcOsRelease = filepath.Join(dir, "etcOsRelease") + + defer func() { + os.Remove(etcOsRelease) + etcOsRelease = backup + }() + + for _, elt := range invalids { + if err := ioutil.WriteFile(etcOsRelease, []byte(elt.content), 0600); err != nil { + t.Fatalf("failed to write to %s: %v", etcOsRelease, err) + } + s, err := GetOperatingSystem() + if err == nil || err.Error() != elt.errorExpected { + t.Fatalf("Expected an error %q, got %q (err: %v)", elt.errorExpected, s, err) + } + } + + for _, elt := range valids { + if err := ioutil.WriteFile(etcOsRelease, []byte(elt.content), 0600); err != nil { + t.Fatalf("failed to write to %s: %v", etcOsRelease, err) + } + s, err := GetOperatingSystem() + if err != nil || s != elt.expected { + t.Fatalf("Expected %q, got %q (err: %v)", elt.expected, s, err) + } + } +} + +func TestIsContainerized(t *testing.T) { + var ( + backup = proc1Cgroup + nonContainerizedProc1Cgroupsystemd226 = []byte(`9:memory:/init.scope +8:net_cls,net_prio:/ +7:cpuset:/ +6:freezer:/ +5:devices:/init.scope +4:blkio:/init.scope +3:cpu,cpuacct:/init.scope +2:perf_event:/ +1:name=systemd:/init.scope +`) + nonContainerizedProc1Cgroup = []byte(`14:name=systemd:/ +13:hugetlb:/ +12:net_prio:/ +11:perf_event:/ +10:bfqio:/ +9:blkio:/ +8:net_cls:/ +7:freezer:/ +6:devices:/ +5:memory:/ +4:cpuacct:/ +3:cpu:/ +2:cpuset:/ +`) + containerizedProc1Cgroup = []byte(`9:perf_event:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d +8:blkio:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d +7:net_cls:/ +6:freezer:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d +5:devices:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d +4:memory:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d +3:cpuacct:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d +2:cpu:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d +1:cpuset:/`) + ) + + dir := os.TempDir() + proc1Cgroup = filepath.Join(dir, "proc1Cgroup") + + defer func() { + os.Remove(proc1Cgroup) + proc1Cgroup = backup + }() + + if err := ioutil.WriteFile(proc1Cgroup, nonContainerizedProc1Cgroup, 0600); err != nil { + t.Fatalf("failed to write to %s: %v", proc1Cgroup, err) + } + inContainer, err := IsContainerized() + if err != nil { + t.Fatal(err) + } + if inContainer { + t.Fatal("Wrongly assuming containerized") + } + + if err := ioutil.WriteFile(proc1Cgroup, nonContainerizedProc1Cgroupsystemd226, 0600); err != nil { + t.Fatalf("failed to write to %s: %v", proc1Cgroup, err) + } + inContainer, err = IsContainerized() + if err != nil { + t.Fatal(err) + } + if inContainer { + t.Fatal("Wrongly assuming containerized for systemd /init.scope cgroup layout") + } + + if err := ioutil.WriteFile(proc1Cgroup, containerizedProc1Cgroup, 0600); err != nil { + t.Fatalf("failed to write to %s: %v", proc1Cgroup, err) + } + inContainer, err = IsContainerized() + if err != nil { + t.Fatal(err) + } + if !inContainer { + t.Fatal("Wrongly assuming non-containerized") + } +} + +func TestOsReleaseFallback(t *testing.T) { + var backup = etcOsRelease + var altBackup = altOsRelease + dir := os.TempDir() + etcOsRelease = filepath.Join(dir, "etcOsRelease") + altOsRelease = filepath.Join(dir, "altOsRelease") + + defer func() { + os.Remove(dir) + etcOsRelease = backup + altOsRelease = altBackup + }() + content := `NAME=Gentoo +ID=gentoo +PRETTY_NAME="Gentoo/Linux" +ANSI_COLOR="1;32" +HOME_URL="http://www.gentoo.org/" +SUPPORT_URL="http://www.gentoo.org/main/en/support.xml" +BUG_REPORT_URL="https://bugs.gentoo.org/" +` + if err := ioutil.WriteFile(altOsRelease, []byte(content), 0600); err != nil { + t.Fatalf("failed to write to %s: %v", etcOsRelease, err) + } + s, err := GetOperatingSystem() + if err != nil || s != "Gentoo/Linux" { + t.Fatalf("Expected %q, got %q (err: %v)", "Gentoo/Linux", s, err) + } +} diff --git a/pkg/parsers/operatingsystem/operatingsystem_windows.go b/pkg/parsers/operatingsystem/operatingsystem_windows.go new file mode 100644 index 00000000..3c86b6af --- /dev/null +++ b/pkg/parsers/operatingsystem/operatingsystem_windows.go @@ -0,0 +1,49 @@ +package operatingsystem + +import ( + "syscall" + "unsafe" +) + +// See https://code.google.com/p/go/source/browse/src/pkg/mime/type_windows.go?r=d14520ac25bf6940785aabb71f5be453a286f58c +// for a similar sample + +// GetOperatingSystem gets the name of the current operating system. +func GetOperatingSystem() (string, error) { + + var h syscall.Handle + + // Default return value + ret := "Unknown Operating System" + + if err := syscall.RegOpenKeyEx(syscall.HKEY_LOCAL_MACHINE, + syscall.StringToUTF16Ptr(`SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\`), + 0, + syscall.KEY_READ, + &h); err != nil { + return ret, err + } + defer syscall.RegCloseKey(h) + + var buf [1 << 10]uint16 + var typ uint32 + n := uint32(len(buf) * 2) // api expects array of bytes, not uint16 + + if err := syscall.RegQueryValueEx(h, + syscall.StringToUTF16Ptr("ProductName"), + nil, + &typ, + (*byte)(unsafe.Pointer(&buf[0])), + &n); err != nil { + return ret, err + } + ret = syscall.UTF16ToString(buf[:]) + + return ret, nil +} + +// IsContainerized returns true if we are running inside a container. +// No-op on Windows, always returns false. +func IsContainerized() (bool, error) { + return false, nil +} diff --git a/pkg/parsers/parsers.go b/pkg/parsers/parsers.go new file mode 100644 index 00000000..acc89716 --- /dev/null +++ b/pkg/parsers/parsers.go @@ -0,0 +1,69 @@ +// Package parsers provides helper functions to parse and validate different type +// of string. It can be hosts, unix addresses, tcp addresses, filters, kernel +// operating system versions. +package parsers + +import ( + "fmt" + "strconv" + "strings" +) + +// ParseKeyValueOpt parses and validates the specified string as a key/value pair (key=value) +func ParseKeyValueOpt(opt string) (string, string, error) { + parts := strings.SplitN(opt, "=", 2) + if len(parts) != 2 { + return "", "", fmt.Errorf("Unable to parse key/value option: %s", opt) + } + return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]), nil +} + +// ParseUintList parses and validates the specified string as the value +// found in some cgroup file (e.g. `cpuset.cpus`, `cpuset.mems`), which could be +// one of the formats below. Note that duplicates are actually allowed in the +// input string. It returns a `map[int]bool` with available elements from `val` +// set to `true`. +// Supported formats: +// 7 +// 1-6 +// 0,3-4,7,8-10 +// 0-0,0,1-7 +// 03,1-3 <- this is gonna get parsed as [1,2,3] +// 3,2,1 +// 0-2,3,1 +func ParseUintList(val string) (map[int]bool, error) { + if val == "" { + return map[int]bool{}, nil + } + + availableInts := make(map[int]bool) + split := strings.Split(val, ",") + errInvalidFormat := fmt.Errorf("invalid format: %s", val) + + for _, r := range split { + if !strings.Contains(r, "-") { + v, err := strconv.Atoi(r) + if err != nil { + return nil, errInvalidFormat + } + availableInts[v] = true + } else { + split := strings.SplitN(r, "-", 2) + min, err := strconv.Atoi(split[0]) + if err != nil { + return nil, errInvalidFormat + } + max, err := strconv.Atoi(split[1]) + if err != nil { + return nil, errInvalidFormat + } + if max < min { + return nil, errInvalidFormat + } + for i := min; i <= max; i++ { + availableInts[i] = true + } + } + } + return availableInts, nil +} diff --git a/pkg/parsers/parsers_test.go b/pkg/parsers/parsers_test.go new file mode 100644 index 00000000..7f19e902 --- /dev/null +++ b/pkg/parsers/parsers_test.go @@ -0,0 +1,70 @@ +package parsers + +import ( + "reflect" + "testing" +) + +func TestParseKeyValueOpt(t *testing.T) { + invalids := map[string]string{ + "": "Unable to parse key/value option: ", + "key": "Unable to parse key/value option: key", + } + for invalid, expectedError := range invalids { + if _, _, err := ParseKeyValueOpt(invalid); err == nil || err.Error() != expectedError { + t.Fatalf("Expected error %v for %v, got %v", expectedError, invalid, err) + } + } + valids := map[string][]string{ + "key=value": {"key", "value"}, + " key = value ": {"key", "value"}, + "key=value1=value2": {"key", "value1=value2"}, + " key = value1 = value2 ": {"key", "value1 = value2"}, + } + for valid, expectedKeyValue := range valids { + key, value, err := ParseKeyValueOpt(valid) + if err != nil { + t.Fatal(err) + } + if key != expectedKeyValue[0] || value != expectedKeyValue[1] { + t.Fatalf("Expected {%v: %v} got {%v: %v}", expectedKeyValue[0], expectedKeyValue[1], key, value) + } + } +} + +func TestParseUintList(t *testing.T) { + valids := map[string]map[int]bool{ + "": {}, + "7": {7: true}, + "1-6": {1: true, 2: true, 3: true, 4: true, 5: true, 6: true}, + "0-7": {0: true, 1: true, 2: true, 3: true, 4: true, 5: true, 6: true, 7: true}, + "0,3-4,7,8-10": {0: true, 3: true, 4: true, 7: true, 8: true, 9: true, 10: true}, + "0-0,0,1-4": {0: true, 1: true, 2: true, 3: true, 4: true}, + "03,1-3": {1: true, 2: true, 3: true}, + "3,2,1": {1: true, 2: true, 3: true}, + "0-2,3,1": {0: true, 1: true, 2: true, 3: true}, + } + for k, v := range valids { + out, err := ParseUintList(k) + if err != nil { + t.Fatalf("Expected not to fail, got %v", err) + } + if !reflect.DeepEqual(out, v) { + t.Fatalf("Expected %v, got %v", v, out) + } + } + + invalids := []string{ + "this", + "1--", + "1-10,,10", + "10-1", + "-1", + "-1,0", + } + for _, v := range invalids { + if out, err := ParseUintList(v); err == nil { + t.Fatalf("Expected failure with %s but got %v", v, out) + } + } +} diff --git a/pkg/pidfile/pidfile.go b/pkg/pidfile/pidfile.go new file mode 100644 index 00000000..58cc4017 --- /dev/null +++ b/pkg/pidfile/pidfile.go @@ -0,0 +1,50 @@ +// Package pidfile provides structure and helper functions to create and remove +// PID file. A PID file is usually a file used to store the process ID of a +// running process. +package pidfile + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strconv" + "strings" +) + +// PIDFile is a file used to store the process ID of a running process. +type PIDFile struct { + path string +} + +func checkPIDFileAlreadyExists(path string) error { + if pidByte, err := ioutil.ReadFile(path); err == nil { + pidString := strings.TrimSpace(string(pidByte)) + if pid, err := strconv.Atoi(pidString); err == nil { + if _, err := os.Stat(filepath.Join("/proc", strconv.Itoa(pid))); err == nil { + return fmt.Errorf("pid file found, ensure docker is not running or delete %s", path) + } + } + } + return nil +} + +// New creates a PIDfile using the specified path. +func New(path string) (*PIDFile, error) { + if err := checkPIDFileAlreadyExists(path); err != nil { + return nil, err + } + if err := ioutil.WriteFile(path, []byte(fmt.Sprintf("%d", os.Getpid())), 0644); err != nil { + return nil, err + } + + return &PIDFile{path: path}, nil +} + +// Remove removes the PIDFile. +func (file PIDFile) Remove() error { + if err := os.Remove(file.path); err != nil { + return err + } + return nil +} diff --git a/pkg/pidfile/pidfile_test.go b/pkg/pidfile/pidfile_test.go new file mode 100644 index 00000000..d5ef8708 --- /dev/null +++ b/pkg/pidfile/pidfile_test.go @@ -0,0 +1,32 @@ +package pidfile + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +func TestNewAndRemove(t *testing.T) { + dir, err := ioutil.TempDir(os.TempDir(), "test-pidfile") + if err != nil { + t.Fatal("Could not create test directory") + } + + file, err := New(filepath.Join(dir, "testfile")) + if err != nil { + t.Fatal("Could not create test file", err) + } + + if err := file.Remove(); err != nil { + t.Fatal("Could not delete created test file") + } +} + +func TestRemoveInvalidPath(t *testing.T) { + file := PIDFile{path: filepath.Join("foo", "bar")} + + if err := file.Remove(); err == nil { + t.Fatal("Non-existing file doesn't give an error on delete") + } +} diff --git a/pkg/platform/architecture_freebsd.go b/pkg/platform/architecture_freebsd.go new file mode 100644 index 00000000..06dc9fe0 --- /dev/null +++ b/pkg/platform/architecture_freebsd.go @@ -0,0 +1,15 @@ +package platform + +import ( + "os/exec" +) + +// runtimeArchitecture get the name of the current architecture (x86, x86_64, …) +func runtimeArchitecture() (string, error) { + cmd := exec.Command("uname", "-m") + machine, err := cmd.Output() + if err != nil { + return "", err + } + return string(machine), nil +} diff --git a/pkg/platform/architecture_linux.go b/pkg/platform/architecture_linux.go new file mode 100644 index 00000000..30c58c78 --- /dev/null +++ b/pkg/platform/architecture_linux.go @@ -0,0 +1,16 @@ +// Package platform provides helper function to get the runtime architecture +// for different platforms. +package platform + +import ( + "syscall" +) + +// runtimeArchitecture get the name of the current architecture (x86, x86_64, …) +func runtimeArchitecture() (string, error) { + utsname := &syscall.Utsname{} + if err := syscall.Uname(utsname); err != nil { + return "", err + } + return charsToString(utsname.Machine), nil +} diff --git a/pkg/platform/architecture_windows.go b/pkg/platform/architecture_windows.go new file mode 100644 index 00000000..c72e1662 --- /dev/null +++ b/pkg/platform/architecture_windows.go @@ -0,0 +1,52 @@ +package platform + +import ( + "fmt" + "syscall" + "unsafe" +) + +var ( + modkernel32 = syscall.NewLazyDLL("kernel32.dll") + procGetSystemInfo = modkernel32.NewProc("GetSystemInfo") +) + +// see http://msdn.microsoft.com/en-us/library/windows/desktop/ms724958(v=vs.85).aspx +type systeminfo struct { + wProcessorArchitecture uint16 + wReserved uint16 + dwPageSize uint32 + lpMinimumApplicationAddress uintptr + lpMaximumApplicationAddress uintptr + dwActiveProcessorMask uintptr + dwNumberOfProcessors uint32 + dwProcessorType uint32 + dwAllocationGranularity uint32 + wProcessorLevel uint16 + wProcessorRevision uint16 +} + +// Constants +const ( + ProcessorArchitecture64 = 9 // PROCESSOR_ARCHITECTURE_AMD64 + ProcessorArchitectureIA64 = 6 // PROCESSOR_ARCHITECTURE_IA64 + ProcessorArchitecture32 = 0 // PROCESSOR_ARCHITECTURE_INTEL + ProcessorArchitectureArm = 5 // PROCESSOR_ARCHITECTURE_ARM +) + +var sysinfo systeminfo + +// runtimeArchitecture get the name of the current architecture (x86, x86_64, …) +func runtimeArchitecture() (string, error) { + syscall.Syscall(procGetSystemInfo.Addr(), 1, uintptr(unsafe.Pointer(&sysinfo)), 0, 0) + switch sysinfo.wProcessorArchitecture { + case ProcessorArchitecture64, ProcessorArchitectureIA64: + return "x86_64", nil + case ProcessorArchitecture32: + return "i686", nil + case ProcessorArchitectureArm: + return "arm", nil + default: + return "", fmt.Errorf("Unknown processor architecture") + } +} diff --git a/pkg/platform/platform.go b/pkg/platform/platform.go new file mode 100644 index 00000000..59e25295 --- /dev/null +++ b/pkg/platform/platform.go @@ -0,0 +1,23 @@ +package platform + +import ( + "runtime" + + "github.com/Sirupsen/logrus" +) + +var ( + // Architecture holds the runtime architecture of the process. + Architecture string + // OSType holds the runtime operating system type (Linux, …) of the process. + OSType string +) + +func init() { + var err error + Architecture, err = runtimeArchitecture() + if err != nil { + logrus.Errorf("Could no read system architecture info: %v", err) + } + OSType = runtime.GOOS +} diff --git a/pkg/platform/utsname_int8.go b/pkg/platform/utsname_int8.go new file mode 100644 index 00000000..5dcbadfd --- /dev/null +++ b/pkg/platform/utsname_int8.go @@ -0,0 +1,18 @@ +// +build linux,386 linux,amd64 linux,arm64 +// see golang's sources src/syscall/ztypes_linux_*.go that use int8 + +package platform + +// Convert the OS/ARCH-specific utsname.Machine to string +// given as an array of signed int8 +func charsToString(ca [65]int8) string { + s := make([]byte, len(ca)) + var lens int + for ; lens < len(ca); lens++ { + if ca[lens] == 0 { + break + } + s[lens] = uint8(ca[lens]) + } + return string(s[0:lens]) +} diff --git a/pkg/platform/utsname_uint8.go b/pkg/platform/utsname_uint8.go new file mode 100644 index 00000000..c9875cf6 --- /dev/null +++ b/pkg/platform/utsname_uint8.go @@ -0,0 +1,18 @@ +// +build linux,arm linux,ppc64 linux,ppc64le s390x +// see golang's sources src/syscall/ztypes_linux_*.go that use uint8 + +package platform + +// Convert the OS/ARCH-specific utsname.Machine to string +// given as an array of unsigned uint8 +func charsToString(ca [65]uint8) string { + s := make([]byte, len(ca)) + var lens int + for ; lens < len(ca); lens++ { + if ca[lens] == 0 { + break + } + s[lens] = ca[lens] + } + return string(s[0:lens]) +} diff --git a/pkg/plugins/client.go b/pkg/plugins/client.go new file mode 100644 index 00000000..e3fd326e --- /dev/null +++ b/pkg/plugins/client.go @@ -0,0 +1,186 @@ +package plugins + +import ( + "bytes" + "encoding/json" + "io" + "io/ioutil" + "net/http" + "net/url" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/plugins/transport" + "github.com/docker/go-connections/sockets" + "github.com/docker/go-connections/tlsconfig" +) + +const ( + defaultTimeOut = 30 +) + +// NewClient creates a new plugin client (http). +func NewClient(addr string, tlsConfig tlsconfig.Options) (*Client, error) { + tr := &http.Transport{} + + c, err := tlsconfig.Client(tlsConfig) + if err != nil { + return nil, err + } + tr.TLSClientConfig = c + + u, err := url.Parse(addr) + if err != nil { + return nil, err + } + socket := u.Host + if socket == "" { + // valid local socket addresses have the host empty. + socket = u.Path + } + if err := sockets.ConfigureTransport(tr, u.Scheme, socket); err != nil { + return nil, err + } + scheme := httpScheme(u) + + clientTransport := transport.NewHTTPTransport(tr, scheme, socket) + return NewClientWithTransport(clientTransport), nil +} + +// NewClientWithTransport creates a new plugin client with a given transport. +func NewClientWithTransport(tr transport.Transport) *Client { + return &Client{ + http: &http.Client{ + Transport: tr, + }, + requestFactory: tr, + } +} + +// Client represents a plugin client. +type Client struct { + http *http.Client // http client to use + requestFactory transport.RequestFactory +} + +// Call calls the specified method with the specified arguments for the plugin. +// It will retry for 30 seconds if a failure occurs when calling. +func (c *Client) Call(serviceMethod string, args interface{}, ret interface{}) error { + var buf bytes.Buffer + if args != nil { + if err := json.NewEncoder(&buf).Encode(args); err != nil { + return err + } + } + body, err := c.callWithRetry(serviceMethod, &buf, true) + if err != nil { + return err + } + defer body.Close() + if ret != nil { + if err := json.NewDecoder(body).Decode(&ret); err != nil { + logrus.Errorf("%s: error reading plugin resp: %v", serviceMethod, err) + return err + } + } + return nil +} + +// Stream calls the specified method with the specified arguments for the plugin and returns the response body +func (c *Client) Stream(serviceMethod string, args interface{}) (io.ReadCloser, error) { + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(args); err != nil { + return nil, err + } + return c.callWithRetry(serviceMethod, &buf, true) +} + +// SendFile calls the specified method, and passes through the IO stream +func (c *Client) SendFile(serviceMethod string, data io.Reader, ret interface{}) error { + body, err := c.callWithRetry(serviceMethod, data, true) + if err != nil { + return err + } + defer body.Close() + if err := json.NewDecoder(body).Decode(&ret); err != nil { + logrus.Errorf("%s: error reading plugin resp: %v", serviceMethod, err) + return err + } + return nil +} + +func (c *Client) callWithRetry(serviceMethod string, data io.Reader, retry bool) (io.ReadCloser, error) { + req, err := c.requestFactory.NewRequest(serviceMethod, data) + if err != nil { + return nil, err + } + + var retries int + start := time.Now() + + for { + resp, err := c.http.Do(req) + if err != nil { + if !retry { + return nil, err + } + + timeOff := backoff(retries) + if abort(start, timeOff) { + return nil, err + } + retries++ + logrus.Warnf("Unable to connect to plugin: %s:%s, retrying in %v", req.URL.Host, req.URL.Path, timeOff) + time.Sleep(timeOff) + continue + } + + if resp.StatusCode != http.StatusOK { + b, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return nil, &statusError{resp.StatusCode, serviceMethod, err.Error()} + } + + // Plugins' Response(s) should have an Err field indicating what went + // wrong. Try to unmarshal into ResponseErr. Otherwise fallback to just + // return the string(body) + type responseErr struct { + Err string + } + remoteErr := responseErr{} + if err := json.Unmarshal(b, &remoteErr); err == nil { + if remoteErr.Err != "" { + return nil, &statusError{resp.StatusCode, serviceMethod, remoteErr.Err} + } + } + // old way... + return nil, &statusError{resp.StatusCode, serviceMethod, string(b)} + } + return resp.Body, nil + } +} + +func backoff(retries int) time.Duration { + b, max := 1, defaultTimeOut + for b < max && retries > 0 { + b *= 2 + retries-- + } + if b > max { + b = max + } + return time.Duration(b) * time.Second +} + +func abort(start time.Time, timeOff time.Duration) bool { + return timeOff+time.Since(start) >= time.Duration(defaultTimeOut)*time.Second +} + +func httpScheme(u *url.URL) string { + scheme := u.Scheme + if scheme != "https" { + scheme = "http" + } + return scheme +} diff --git a/pkg/plugins/client_test.go b/pkg/plugins/client_test.go new file mode 100644 index 00000000..3fa2ff46 --- /dev/null +++ b/pkg/plugins/client_test.go @@ -0,0 +1,134 @@ +package plugins + +import ( + "io" + "net/http" + "net/http/httptest" + "net/url" + "reflect" + "testing" + "time" + + "github.com/docker/docker/pkg/plugins/transport" + "github.com/docker/go-connections/tlsconfig" +) + +var ( + mux *http.ServeMux + server *httptest.Server +) + +func setupRemotePluginServer() string { + mux = http.NewServeMux() + server = httptest.NewServer(mux) + return server.URL +} + +func teardownRemotePluginServer() { + if server != nil { + server.Close() + } +} + +func TestFailedConnection(t *testing.T) { + c, _ := NewClient("tcp://127.0.0.1:1", tlsconfig.Options{InsecureSkipVerify: true}) + _, err := c.callWithRetry("Service.Method", nil, false) + if err == nil { + t.Fatal("Unexpected successful connection") + } +} + +func TestEchoInputOutput(t *testing.T) { + addr := setupRemotePluginServer() + defer teardownRemotePluginServer() + + m := Manifest{[]string{"VolumeDriver", "NetworkDriver"}} + + mux.HandleFunc("/Test.Echo", func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Fatalf("Expected POST, got %s\n", r.Method) + } + + header := w.Header() + header.Set("Content-Type", transport.VersionMimetype) + + io.Copy(w, r.Body) + }) + + c, _ := NewClient(addr, tlsconfig.Options{InsecureSkipVerify: true}) + var output Manifest + err := c.Call("Test.Echo", m, &output) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(output, m) { + t.Fatalf("Expected %v, was %v\n", m, output) + } + err = c.Call("Test.Echo", nil, nil) + if err != nil { + t.Fatal(err) + } +} + +func TestBackoff(t *testing.T) { + cases := []struct { + retries int + expTimeOff time.Duration + }{ + {0, time.Duration(1)}, + {1, time.Duration(2)}, + {2, time.Duration(4)}, + {4, time.Duration(16)}, + {6, time.Duration(30)}, + {10, time.Duration(30)}, + } + + for _, c := range cases { + s := c.expTimeOff * time.Second + if d := backoff(c.retries); d != s { + t.Fatalf("Retry %v, expected %v, was %v\n", c.retries, s, d) + } + } +} + +func TestAbortRetry(t *testing.T) { + cases := []struct { + timeOff time.Duration + expAbort bool + }{ + {time.Duration(1), false}, + {time.Duration(2), false}, + {time.Duration(10), false}, + {time.Duration(30), true}, + {time.Duration(40), true}, + } + + for _, c := range cases { + s := c.timeOff * time.Second + if a := abort(time.Now(), s); a != c.expAbort { + t.Fatalf("Duration %v, expected %v, was %v\n", c.timeOff, s, a) + } + } +} + +func TestClientScheme(t *testing.T) { + cases := map[string]string{ + "tcp://127.0.0.1:8080": "http", + "unix:///usr/local/plugins/foo": "http", + "http://127.0.0.1:8080": "http", + "https://127.0.0.1:8080": "https", + } + + for addr, scheme := range cases { + u, err := url.Parse(addr) + if err != nil { + t.Fatal(err) + } + s := httpScheme(u) + + if s != scheme { + t.Fatalf("URL scheme mismatch, expected %s, got %s", scheme, s) + } + } +} diff --git a/pkg/plugins/discovery.go b/pkg/plugins/discovery.go new file mode 100644 index 00000000..9dc64194 --- /dev/null +++ b/pkg/plugins/discovery.go @@ -0,0 +1,132 @@ +package plugins + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/url" + "os" + "path/filepath" + "strings" + "sync" +) + +var ( + // ErrNotFound plugin not found + ErrNotFound = errors.New("plugin not found") + socketsPath = "/run/docker/plugins" + specsPaths = []string{"/etc/docker/plugins", "/usr/lib/docker/plugins"} +) + +// localRegistry defines a registry that is local (using unix socket). +type localRegistry struct{} + +func newLocalRegistry() localRegistry { + return localRegistry{} +} + +// Scan scans all the plugin paths and returns all the names it found +func Scan() ([]string, error) { + var names []string + if err := filepath.Walk(socketsPath, func(path string, fi os.FileInfo, err error) error { + if err != nil { + return nil + } + + if fi.Mode()&os.ModeSocket != 0 { + name := strings.TrimSuffix(fi.Name(), filepath.Ext(fi.Name())) + names = append(names, name) + } + return nil + }); err != nil { + return nil, err + } + + for _, path := range specsPaths { + if err := filepath.Walk(path, func(p string, fi os.FileInfo, err error) error { + if err != nil || fi.IsDir() { + return nil + } + name := strings.TrimSuffix(fi.Name(), filepath.Ext(fi.Name())) + names = append(names, name) + return nil + }); err != nil { + return nil, err + } + } + return names, nil +} + +// Plugin returns the plugin registered with the given name (or returns an error). +func (l *localRegistry) Plugin(name string) (*Plugin, error) { + socketpaths := pluginPaths(socketsPath, name, ".sock") + + for _, p := range socketpaths { + if fi, err := os.Stat(p); err == nil && fi.Mode()&os.ModeSocket != 0 { + return newLocalPlugin(name, "unix://"+p), nil + } + } + + var txtspecpaths []string + for _, p := range specsPaths { + txtspecpaths = append(txtspecpaths, pluginPaths(p, name, ".spec")...) + txtspecpaths = append(txtspecpaths, pluginPaths(p, name, ".json")...) + } + + for _, p := range txtspecpaths { + if _, err := os.Stat(p); err == nil { + if strings.HasSuffix(p, ".json") { + return readPluginJSONInfo(name, p) + } + return readPluginInfo(name, p) + } + } + return nil, ErrNotFound +} + +func readPluginInfo(name, path string) (*Plugin, error) { + content, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + addr := strings.TrimSpace(string(content)) + + u, err := url.Parse(addr) + if err != nil { + return nil, err + } + + if len(u.Scheme) == 0 { + return nil, fmt.Errorf("Unknown protocol") + } + + return newLocalPlugin(name, addr), nil +} + +func readPluginJSONInfo(name, path string) (*Plugin, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + var p Plugin + if err := json.NewDecoder(f).Decode(&p); err != nil { + return nil, err + } + p.Name = name + if len(p.TLSConfig.CAFile) == 0 { + p.TLSConfig.InsecureSkipVerify = true + } + p.activateWait = sync.NewCond(&sync.Mutex{}) + + return &p, nil +} + +func pluginPaths(base, name, ext string) []string { + return []string{ + filepath.Join(base, name+ext), + filepath.Join(base, name, name+ext), + } +} diff --git a/pkg/plugins/discovery_test.go b/pkg/plugins/discovery_test.go new file mode 100644 index 00000000..2e8dc704 --- /dev/null +++ b/pkg/plugins/discovery_test.go @@ -0,0 +1,119 @@ +package plugins + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +func Setup(t *testing.T) (string, func()) { + tmpdir, err := ioutil.TempDir("", "docker-test") + if err != nil { + t.Fatal(err) + } + backup := socketsPath + socketsPath = tmpdir + specsPaths = []string{tmpdir} + + return tmpdir, func() { + socketsPath = backup + os.RemoveAll(tmpdir) + } +} + +func TestFileSpecPlugin(t *testing.T) { + tmpdir, unregister := Setup(t) + defer unregister() + + cases := []struct { + path string + name string + addr string + fail bool + }{ + // TODO Windows: Factor out the unix:// variants. + {filepath.Join(tmpdir, "echo.spec"), "echo", "unix://var/lib/docker/plugins/echo.sock", false}, + {filepath.Join(tmpdir, "echo", "echo.spec"), "echo", "unix://var/lib/docker/plugins/echo.sock", false}, + {filepath.Join(tmpdir, "foo.spec"), "foo", "tcp://localhost:8080", false}, + {filepath.Join(tmpdir, "foo", "foo.spec"), "foo", "tcp://localhost:8080", false}, + {filepath.Join(tmpdir, "bar.spec"), "bar", "localhost:8080", true}, // unknown transport + } + + for _, c := range cases { + if err := os.MkdirAll(filepath.Dir(c.path), 0755); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(c.path, []byte(c.addr), 0644); err != nil { + t.Fatal(err) + } + + r := newLocalRegistry() + p, err := r.Plugin(c.name) + if c.fail && err == nil { + continue + } + + if err != nil { + t.Fatal(err) + } + + if p.Name != c.name { + t.Fatalf("Expected plugin `%s`, got %s\n", c.name, p.Name) + } + + if p.Addr != c.addr { + t.Fatalf("Expected plugin addr `%s`, got %s\n", c.addr, p.Addr) + } + + if p.TLSConfig.InsecureSkipVerify != true { + t.Fatalf("Expected TLS verification to be skipped") + } + } +} + +func TestFileJSONSpecPlugin(t *testing.T) { + tmpdir, unregister := Setup(t) + defer unregister() + + p := filepath.Join(tmpdir, "example.json") + spec := `{ + "Name": "plugin-example", + "Addr": "https://example.com/docker/plugin", + "TLSConfig": { + "CAFile": "/usr/shared/docker/certs/example-ca.pem", + "CertFile": "/usr/shared/docker/certs/example-cert.pem", + "KeyFile": "/usr/shared/docker/certs/example-key.pem" + } +}` + + if err := ioutil.WriteFile(p, []byte(spec), 0644); err != nil { + t.Fatal(err) + } + + r := newLocalRegistry() + plugin, err := r.Plugin("example") + if err != nil { + t.Fatal(err) + } + + if plugin.Name != "example" { + t.Fatalf("Expected plugin `plugin-example`, got %s\n", plugin.Name) + } + + if plugin.Addr != "https://example.com/docker/plugin" { + t.Fatalf("Expected plugin addr `https://example.com/docker/plugin`, got %s\n", plugin.Addr) + } + + if plugin.TLSConfig.CAFile != "/usr/shared/docker/certs/example-ca.pem" { + t.Fatalf("Expected plugin CA `/usr/shared/docker/certs/example-ca.pem`, got %s\n", plugin.TLSConfig.CAFile) + } + + if plugin.TLSConfig.CertFile != "/usr/shared/docker/certs/example-cert.pem" { + t.Fatalf("Expected plugin Certificate `/usr/shared/docker/certs/example-cert.pem`, got %s\n", plugin.TLSConfig.CertFile) + } + + if plugin.TLSConfig.KeyFile != "/usr/shared/docker/certs/example-key.pem" { + t.Fatalf("Expected plugin Key `/usr/shared/docker/certs/example-key.pem`, got %s\n", plugin.TLSConfig.KeyFile) + } +} diff --git a/pkg/plugins/discovery_unix_test.go b/pkg/plugins/discovery_unix_test.go new file mode 100644 index 00000000..8166ae00 --- /dev/null +++ b/pkg/plugins/discovery_unix_test.go @@ -0,0 +1,61 @@ +// +build !windows + +package plugins + +import ( + "fmt" + "net" + "os" + "path/filepath" + "reflect" + "testing" +) + +func TestLocalSocket(t *testing.T) { + // TODO Windows: Enable a similar version for Windows named pipes + tmpdir, unregister := Setup(t) + defer unregister() + + cases := []string{ + filepath.Join(tmpdir, "echo.sock"), + filepath.Join(tmpdir, "echo", "echo.sock"), + } + + for _, c := range cases { + if err := os.MkdirAll(filepath.Dir(c), 0755); err != nil { + t.Fatal(err) + } + + l, err := net.Listen("unix", c) + if err != nil { + t.Fatal(err) + } + + r := newLocalRegistry() + p, err := r.Plugin("echo") + if err != nil { + t.Fatal(err) + } + + pp, err := r.Plugin("echo") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(p, pp) { + t.Fatalf("Expected %v, was %v\n", p, pp) + } + + if p.Name != "echo" { + t.Fatalf("Expected plugin `echo`, got %s\n", p.Name) + } + + addr := fmt.Sprintf("unix://%s", c) + if p.Addr != addr { + t.Fatalf("Expected plugin addr `%s`, got %s\n", addr, p.Addr) + } + if p.TLSConfig.InsecureSkipVerify != true { + t.Fatalf("Expected TLS verification to be skipped") + } + l.Close() + } +} diff --git a/pkg/plugins/errors.go b/pkg/plugins/errors.go new file mode 100644 index 00000000..79884710 --- /dev/null +++ b/pkg/plugins/errors.go @@ -0,0 +1,33 @@ +package plugins + +import ( + "fmt" + "net/http" +) + +type statusError struct { + status int + method string + err string +} + +// Error returns a formatted string for this error type +func (e *statusError) Error() string { + return fmt.Sprintf("%s: %v", e.method, e.err) +} + +// IsNotFound indicates if the passed in error is from an http.StatusNotFound from the plugin +func IsNotFound(err error) bool { + return isStatusError(err, http.StatusNotFound) +} + +func isStatusError(err error, status int) bool { + if err == nil { + return false + } + e, ok := err.(*statusError) + if !ok { + return false + } + return e.status == status +} diff --git a/pkg/plugins/pluginrpc-gen/README.md b/pkg/plugins/pluginrpc-gen/README.md new file mode 100644 index 00000000..98720b21 --- /dev/null +++ b/pkg/plugins/pluginrpc-gen/README.md @@ -0,0 +1,68 @@ +Plugin RPC Generator +==================== + +Generates go code from a Go interface definition for proxying between the plugin +API and the subsystem being extended. + +## Usage + +Given an interface definition: + +```go +type volumeDriver interface { + Create(name string, opts opts) (err error) + Remove(name string) (err error) + Path(name string) (mountpoint string, err error) + Mount(name string) (mountpoint string, err error) + Unmount(name string) (err error) +} +``` + +**Note**: All function options and return values must be named in the definition. + +Run the generator: + +```bash +$ pluginrpc-gen --type volumeDriver --name VolumeDriver -i volumes/drivers/extpoint.go -o volumes/drivers/proxy.go +``` + +Where: +- `--type` is the name of the interface to use +- `--name` is the subsystem that the plugin "Implements" +- `-i` is the input file containing the interface definition +- `-o` is the output file where the the generated code should go + +**Note**: The generated code will use the same package name as the one defined in the input file + +Optionally, you can skip functions on the interface that should not be +implemented in the generated proxy code by passing in the function name to `--skip`. +This flag can be specified multiple times. + +You can also add build tags that should be prepended to the generated code by +supplying `--tag`. This flag can be specified multiple times. + +## Known issues + +The parser can currently only handle types which are not specifically a map or +a slice. +You can, however, create a type that uses a map or a slice internally, for instance: + +```go +type opts map[string]string +``` + +This `opts` type will work, whreas using a `map[string]string` directly will not. + +## go-generate + +You can also use this with go-generate, which is pretty awesome. +To do so, place the code at the top of the file which contains the interface +definition (i.e., the input file): + +```go +//go:generate pluginrpc-gen -i $GOFILE -o proxy.go -type volumeDriver -name VolumeDriver +``` + +Then cd to the package dir and run `go generate` + +**Note**: the `pluginrpc-gen` binary must be within your `$PATH` diff --git a/pkg/plugins/pluginrpc-gen/fixtures/foo.go b/pkg/plugins/pluginrpc-gen/fixtures/foo.go new file mode 100644 index 00000000..fcb2b623 --- /dev/null +++ b/pkg/plugins/pluginrpc-gen/fixtures/foo.go @@ -0,0 +1,41 @@ +package foo + +type wobble struct { + Some string + Val string + Inception *wobble +} + +// Fooer is an empty interface used for tests. +type Fooer interface{} + +// Fooer2 is an interface used for tests. +type Fooer2 interface { + Foo() +} + +// Fooer3 is an interface used for tests. +type Fooer3 interface { + Foo() + Bar(a string) + Baz(a string) (err error) + Qux(a, b string) (val string, err error) + Wobble() (w *wobble) + Wiggle() (w wobble) +} + +// Fooer4 is an interface used for tests. +type Fooer4 interface { + Foo() error +} + +// Bar is an interface used for tests. +type Bar interface { + Boo(a string, b string) (s string, err error) +} + +// Fooer5 is an interface used for tests. +type Fooer5 interface { + Foo() + Bar +} diff --git a/pkg/plugins/pluginrpc-gen/main.go b/pkg/plugins/pluginrpc-gen/main.go new file mode 100644 index 00000000..772984ce --- /dev/null +++ b/pkg/plugins/pluginrpc-gen/main.go @@ -0,0 +1,91 @@ +package main + +import ( + "bytes" + "flag" + "fmt" + "go/format" + "io/ioutil" + "os" + "unicode" + "unicode/utf8" +) + +type stringSet struct { + values map[string]struct{} +} + +func (s stringSet) String() string { + return "" +} + +func (s stringSet) Set(value string) error { + s.values[value] = struct{}{} + return nil +} +func (s stringSet) GetValues() map[string]struct{} { + return s.values +} + +var ( + typeName = flag.String("type", "", "interface type to generate plugin rpc proxy for") + rpcName = flag.String("name", *typeName, "RPC name, set if different from type") + inputFile = flag.String("i", "", "input file path") + outputFile = flag.String("o", *inputFile+"_proxy.go", "output file path") + + skipFuncs map[string]struct{} + flSkipFuncs = stringSet{make(map[string]struct{})} + + flBuildTags = stringSet{make(map[string]struct{})} +) + +func errorOut(msg string, err error) { + if err == nil { + return + } + fmt.Fprintf(os.Stderr, "%s: %v\n", msg, err) + os.Exit(1) +} + +func checkFlags() error { + if *outputFile == "" { + return fmt.Errorf("missing required flag `-o`") + } + if *inputFile == "" { + return fmt.Errorf("missing required flag `-i`") + } + return nil +} + +func main() { + flag.Var(flSkipFuncs, "skip", "skip parsing for function") + flag.Var(flBuildTags, "tag", "build tags to add to generated files") + flag.Parse() + skipFuncs = flSkipFuncs.GetValues() + + errorOut("error", checkFlags()) + + pkg, err := Parse(*inputFile, *typeName) + errorOut(fmt.Sprintf("error parsing requested type %s", *typeName), err) + + var analysis = struct { + InterfaceType string + RPCName string + BuildTags map[string]struct{} + *ParsedPkg + }{toLower(*typeName), *rpcName, flBuildTags.GetValues(), pkg} + var buf bytes.Buffer + + errorOut("parser error", generatedTempl.Execute(&buf, analysis)) + src, err := format.Source(buf.Bytes()) + errorOut("error formating generated source", err) + errorOut("error writing file", ioutil.WriteFile(*outputFile, src, 0644)) +} + +func toLower(s string) string { + if s == "" { + return "" + } + r, n := utf8.DecodeRuneInString(s) + return string(unicode.ToLower(r)) + s[n:] +} diff --git a/pkg/plugins/pluginrpc-gen/parser.go b/pkg/plugins/pluginrpc-gen/parser.go new file mode 100644 index 00000000..3adeb490 --- /dev/null +++ b/pkg/plugins/pluginrpc-gen/parser.go @@ -0,0 +1,163 @@ +package main + +import ( + "errors" + "fmt" + "go/ast" + "go/parser" + "go/token" + "reflect" +) + +var errBadReturn = errors.New("found return arg with no name: all args must be named") + +type errUnexpectedType struct { + expected string + actual interface{} +} + +func (e errUnexpectedType) Error() string { + return fmt.Sprintf("got wrong type expecting %s, got: %v", e.expected, reflect.TypeOf(e.actual)) +} + +// ParsedPkg holds information about a package that has been parsed, +// its name and the list of functions. +type ParsedPkg struct { + Name string + Functions []function +} + +type function struct { + Name string + Args []arg + Returns []arg + Doc string +} + +type arg struct { + Name string + ArgType string +} + +func (a *arg) String() string { + return a.Name + " " + a.ArgType +} + +// Parse parses the given file for an interface definition with the given name. +func Parse(filePath string, objName string) (*ParsedPkg, error) { + fs := token.NewFileSet() + pkg, err := parser.ParseFile(fs, filePath, nil, parser.AllErrors) + if err != nil { + return nil, err + } + p := &ParsedPkg{} + p.Name = pkg.Name.Name + obj, exists := pkg.Scope.Objects[objName] + if !exists { + return nil, fmt.Errorf("could not find object %s in %s", objName, filePath) + } + if obj.Kind != ast.Typ { + return nil, fmt.Errorf("exected type, got %s", obj.Kind) + } + spec, ok := obj.Decl.(*ast.TypeSpec) + if !ok { + return nil, errUnexpectedType{"*ast.TypeSpec", obj.Decl} + } + iface, ok := spec.Type.(*ast.InterfaceType) + if !ok { + return nil, errUnexpectedType{"*ast.InterfaceType", spec.Type} + } + + p.Functions, err = parseInterface(iface) + if err != nil { + return nil, err + } + + return p, nil +} + +func parseInterface(iface *ast.InterfaceType) ([]function, error) { + var functions []function + for _, field := range iface.Methods.List { + switch f := field.Type.(type) { + case *ast.FuncType: + method, err := parseFunc(field) + if err != nil { + return nil, err + } + if method == nil { + continue + } + functions = append(functions, *method) + case *ast.Ident: + spec, ok := f.Obj.Decl.(*ast.TypeSpec) + if !ok { + return nil, errUnexpectedType{"*ast.TypeSpec", f.Obj.Decl} + } + iface, ok := spec.Type.(*ast.InterfaceType) + if !ok { + return nil, errUnexpectedType{"*ast.TypeSpec", spec.Type} + } + funcs, err := parseInterface(iface) + if err != nil { + fmt.Println(err) + continue + } + functions = append(functions, funcs...) + default: + return nil, errUnexpectedType{"*astFuncType or *ast.Ident", f} + } + } + return functions, nil +} + +func parseFunc(field *ast.Field) (*function, error) { + f := field.Type.(*ast.FuncType) + method := &function{Name: field.Names[0].Name} + if _, exists := skipFuncs[method.Name]; exists { + fmt.Println("skipping:", method.Name) + return nil, nil + } + if f.Params != nil { + args, err := parseArgs(f.Params.List) + if err != nil { + return nil, err + } + method.Args = args + } + if f.Results != nil { + returns, err := parseArgs(f.Results.List) + if err != nil { + return nil, fmt.Errorf("error parsing function returns for %q: %v", method.Name, err) + } + method.Returns = returns + } + return method, nil +} + +func parseArgs(fields []*ast.Field) ([]arg, error) { + var args []arg + for _, f := range fields { + if len(f.Names) == 0 { + return nil, errBadReturn + } + for _, name := range f.Names { + var typeName string + switch argType := f.Type.(type) { + case *ast.Ident: + typeName = argType.Name + case *ast.StarExpr: + i, ok := argType.X.(*ast.Ident) + if !ok { + return nil, errUnexpectedType{"*ast.Ident", f.Type} + } + typeName = "*" + i.Name + default: + return nil, errUnexpectedType{"*ast.Ident or *ast.StarExpr", f.Type} + } + + args = append(args, arg{name.Name, typeName}) + } + } + return args, nil +} diff --git a/pkg/plugins/pluginrpc-gen/parser_test.go b/pkg/plugins/pluginrpc-gen/parser_test.go new file mode 100644 index 00000000..5a7579cf --- /dev/null +++ b/pkg/plugins/pluginrpc-gen/parser_test.go @@ -0,0 +1,168 @@ +package main + +import ( + "fmt" + "path/filepath" + "runtime" + "strings" + "testing" +) + +const testFixture = "fixtures/foo.go" + +func TestParseEmptyInterface(t *testing.T) { + pkg, err := Parse(testFixture, "Fooer") + if err != nil { + t.Fatal(err) + } + + assertName(t, "foo", pkg.Name) + assertNum(t, 0, len(pkg.Functions)) +} + +func TestParseNonInterfaceType(t *testing.T) { + _, err := Parse(testFixture, "wobble") + if _, ok := err.(errUnexpectedType); !ok { + t.Fatal("expected type error when parsing non-interface type") + } +} + +func TestParseWithOneFunction(t *testing.T) { + pkg, err := Parse(testFixture, "Fooer2") + if err != nil { + t.Fatal(err) + } + + assertName(t, "foo", pkg.Name) + assertNum(t, 1, len(pkg.Functions)) + assertName(t, "Foo", pkg.Functions[0].Name) + assertNum(t, 0, len(pkg.Functions[0].Args)) + assertNum(t, 0, len(pkg.Functions[0].Returns)) +} + +func TestParseWithMultipleFuncs(t *testing.T) { + pkg, err := Parse(testFixture, "Fooer3") + if err != nil { + t.Fatal(err) + } + + assertName(t, "foo", pkg.Name) + assertNum(t, 6, len(pkg.Functions)) + + f := pkg.Functions[0] + assertName(t, "Foo", f.Name) + assertNum(t, 0, len(f.Args)) + assertNum(t, 0, len(f.Returns)) + + f = pkg.Functions[1] + assertName(t, "Bar", f.Name) + assertNum(t, 1, len(f.Args)) + assertNum(t, 0, len(f.Returns)) + arg := f.Args[0] + assertName(t, "a", arg.Name) + assertName(t, "string", arg.ArgType) + + f = pkg.Functions[2] + assertName(t, "Baz", f.Name) + assertNum(t, 1, len(f.Args)) + assertNum(t, 1, len(f.Returns)) + arg = f.Args[0] + assertName(t, "a", arg.Name) + assertName(t, "string", arg.ArgType) + arg = f.Returns[0] + assertName(t, "err", arg.Name) + assertName(t, "error", arg.ArgType) + + f = pkg.Functions[3] + assertName(t, "Qux", f.Name) + assertNum(t, 2, len(f.Args)) + assertNum(t, 2, len(f.Returns)) + arg = f.Args[0] + assertName(t, "a", f.Args[0].Name) + assertName(t, "string", f.Args[0].ArgType) + arg = f.Args[1] + assertName(t, "b", arg.Name) + assertName(t, "string", arg.ArgType) + arg = f.Returns[0] + assertName(t, "val", arg.Name) + assertName(t, "string", arg.ArgType) + arg = f.Returns[1] + assertName(t, "err", arg.Name) + assertName(t, "error", arg.ArgType) + + f = pkg.Functions[4] + assertName(t, "Wobble", f.Name) + assertNum(t, 0, len(f.Args)) + assertNum(t, 1, len(f.Returns)) + arg = f.Returns[0] + assertName(t, "w", arg.Name) + assertName(t, "*wobble", arg.ArgType) + + f = pkg.Functions[5] + assertName(t, "Wiggle", f.Name) + assertNum(t, 0, len(f.Args)) + assertNum(t, 1, len(f.Returns)) + arg = f.Returns[0] + assertName(t, "w", arg.Name) + assertName(t, "wobble", arg.ArgType) +} + +func TestParseWithUnamedReturn(t *testing.T) { + _, err := Parse(testFixture, "Fooer4") + if !strings.HasSuffix(err.Error(), errBadReturn.Error()) { + t.Fatalf("expected ErrBadReturn, got %v", err) + } +} + +func TestEmbeddedInterface(t *testing.T) { + pkg, err := Parse(testFixture, "Fooer5") + if err != nil { + t.Fatal(err) + } + + assertName(t, "foo", pkg.Name) + assertNum(t, 2, len(pkg.Functions)) + + f := pkg.Functions[0] + assertName(t, "Foo", f.Name) + assertNum(t, 0, len(f.Args)) + assertNum(t, 0, len(f.Returns)) + + f = pkg.Functions[1] + assertName(t, "Boo", f.Name) + assertNum(t, 2, len(f.Args)) + assertNum(t, 2, len(f.Returns)) + + arg := f.Args[0] + assertName(t, "a", arg.Name) + assertName(t, "string", arg.ArgType) + + arg = f.Args[1] + assertName(t, "b", arg.Name) + assertName(t, "string", arg.ArgType) + + arg = f.Returns[0] + assertName(t, "s", arg.Name) + assertName(t, "string", arg.ArgType) + + arg = f.Returns[1] + assertName(t, "err", arg.Name) + assertName(t, "error", arg.ArgType) +} + +func assertName(t *testing.T, expected, actual string) { + if expected != actual { + fatalOut(t, fmt.Sprintf("expected name to be `%s`, got: %s", expected, actual)) + } +} + +func assertNum(t *testing.T, expected, actual int) { + if expected != actual { + fatalOut(t, fmt.Sprintf("expected number to be %d, got: %d", expected, actual)) + } +} + +func fatalOut(t *testing.T, msg string) { + _, file, ln, _ := runtime.Caller(2) + t.Fatalf("%s:%d: %s", filepath.Base(file), ln, msg) +} diff --git a/pkg/plugins/pluginrpc-gen/template.go b/pkg/plugins/pluginrpc-gen/template.go new file mode 100644 index 00000000..704030cf --- /dev/null +++ b/pkg/plugins/pluginrpc-gen/template.go @@ -0,0 +1,97 @@ +package main + +import ( + "strings" + "text/template" +) + +func printArgs(args []arg) string { + var argStr []string + for _, arg := range args { + argStr = append(argStr, arg.String()) + } + return strings.Join(argStr, ", ") +} + +func marshalType(t string) string { + switch t { + case "error": + // convert error types to plain strings to ensure the values are encoded/decoded properly + return "string" + default: + return t + } +} + +func isErr(t string) bool { + switch t { + case "error": + return true + default: + return false + } +} + +// Need to use this helper due to issues with go-vet +func buildTag(s string) string { + return "+build " + s +} + +var templFuncs = template.FuncMap{ + "printArgs": printArgs, + "marshalType": marshalType, + "isErr": isErr, + "lower": strings.ToLower, + "title": strings.Title, + "tag": buildTag, +} + +var generatedTempl = template.Must(template.New("rpc_cient").Funcs(templFuncs).Parse(` +// generated code - DO NOT EDIT +{{ range $k, $v := .BuildTags }} + // {{ tag $k }} {{ end }} + +package {{ .Name }} + +import "errors" + +type client interface{ + Call(string, interface{}, interface{}) error +} + +type {{ .InterfaceType }}Proxy struct { + client +} + +{{ range .Functions }} + type {{ $.InterfaceType }}Proxy{{ .Name }}Request struct{ + {{ range .Args }} + {{ title .Name }} {{ .ArgType }} {{ end }} + } + + type {{ $.InterfaceType }}Proxy{{ .Name }}Response struct{ + {{ range .Returns }} + {{ title .Name }} {{ marshalType .ArgType }} {{ end }} + } + + func (pp *{{ $.InterfaceType }}Proxy) {{ .Name }}({{ printArgs .Args }}) ({{ printArgs .Returns }}) { + var( + req {{ $.InterfaceType }}Proxy{{ .Name }}Request + ret {{ $.InterfaceType }}Proxy{{ .Name }}Response + ) + {{ range .Args }} + req.{{ title .Name }} = {{ lower .Name }} {{ end }} + if err = pp.Call("{{ $.RPCName }}.{{ .Name }}", req, &ret); err != nil { + return + } + {{ range $r := .Returns }} + {{ if isErr .ArgType }} + if ret.{{ title .Name }} != "" { + {{ lower .Name }} = errors.New(ret.{{ title .Name }}) + } {{ end }} + {{ if isErr .ArgType | not }} {{ lower .Name }} = ret.{{ title .Name }} {{ end }} {{ end }} + + return + } +{{ end }} +`)) diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go new file mode 100644 index 00000000..4f270a40 --- /dev/null +++ b/pkg/plugins/plugins.go @@ -0,0 +1,257 @@ +// Package plugins provides structures and helper functions to manage Docker +// plugins. +// +// Docker discovers plugins by looking for them in the plugin directory whenever +// a user or container tries to use one by name. UNIX domain socket files must +// be located under /run/docker/plugins, whereas spec files can be located +// either under /etc/docker/plugins or /usr/lib/docker/plugins. This is handled +// by the Registry interface, which lets you list all plugins or get a plugin by +// its name if it exists. +// +// The plugins need to implement an HTTP server and bind this to the UNIX socket +// or the address specified in the spec files. +// A handshake is send at /Plugin.Activate, and plugins are expected to return +// a Manifest with a list of of Docker subsystems which this plugin implements. +// +// In order to use a plugins, you can use the ``Get`` with the name of the +// plugin and the subsystem it implements. +// +// plugin, err := plugins.Get("example", "VolumeDriver") +// if err != nil { +// return fmt.Errorf("Error looking up volume plugin example: %v", err) +// } +package plugins + +import ( + "errors" + "sync" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/go-connections/tlsconfig" +) + +var ( + // ErrNotImplements is returned if the plugin does not implement the requested driver. + ErrNotImplements = errors.New("Plugin does not implement the requested driver") +) + +type plugins struct { + sync.Mutex + plugins map[string]*Plugin +} + +var ( + storage = plugins{plugins: make(map[string]*Plugin)} + extpointHandlers = make(map[string]func(string, *Client)) +) + +// Manifest lists what a plugin implements. +type Manifest struct { + // List of subsystem the plugin implements. + Implements []string +} + +// Plugin is the definition of a docker plugin. +type Plugin struct { + // Name of the plugin + Name string `json:"-"` + // Address of the plugin + Addr string + // TLS configuration of the plugin + TLSConfig tlsconfig.Options + // Client attached to the plugin + Client *Client `json:"-"` + // Manifest of the plugin (see above) + Manifest *Manifest `json:"-"` + + // error produced by activation + activateErr error + // specifies if the activation sequence is completed (not if it is sucessful or not) + activated bool + // wait for activation to finish + activateWait *sync.Cond +} + +func newLocalPlugin(name, addr string) *Plugin { + return &Plugin{ + Name: name, + Addr: addr, + TLSConfig: tlsconfig.Options{InsecureSkipVerify: true}, + activateWait: sync.NewCond(&sync.Mutex{}), + } +} + +func (p *Plugin) activate() error { + p.activateWait.L.Lock() + if p.activated { + p.activateWait.L.Unlock() + return p.activateErr + } + + p.activateErr = p.activateWithLock() + p.activated = true + + p.activateWait.L.Unlock() + p.activateWait.Broadcast() + return p.activateErr +} + +func (p *Plugin) activateWithLock() error { + c, err := NewClient(p.Addr, p.TLSConfig) + if err != nil { + return err + } + p.Client = c + + m := new(Manifest) + if err = p.Client.Call("Plugin.Activate", nil, m); err != nil { + return err + } + + p.Manifest = m + + for _, iface := range m.Implements { + handler, handled := extpointHandlers[iface] + if !handled { + continue + } + handler(p.Name, p.Client) + } + return nil +} + +func (p *Plugin) waitActive() error { + p.activateWait.L.Lock() + for !p.activated { + p.activateWait.Wait() + } + p.activateWait.L.Unlock() + return p.activateErr +} + +func (p *Plugin) implements(kind string) bool { + if err := p.waitActive(); err != nil { + return false + } + for _, driver := range p.Manifest.Implements { + if driver == kind { + return true + } + } + return false +} + +func load(name string) (*Plugin, error) { + return loadWithRetry(name, true) +} + +func loadWithRetry(name string, retry bool) (*Plugin, error) { + registry := newLocalRegistry() + start := time.Now() + + var retries int + for { + pl, err := registry.Plugin(name) + if err != nil { + if !retry { + return nil, err + } + + timeOff := backoff(retries) + if abort(start, timeOff) { + return nil, err + } + retries++ + logrus.Warnf("Unable to locate plugin: %s, retrying in %v", name, timeOff) + time.Sleep(timeOff) + continue + } + + storage.Lock() + storage.plugins[name] = pl + storage.Unlock() + + err = pl.activate() + + if err != nil { + storage.Lock() + delete(storage.plugins, name) + storage.Unlock() + } + + return pl, err + } +} + +func get(name string) (*Plugin, error) { + storage.Lock() + pl, ok := storage.plugins[name] + storage.Unlock() + if ok { + return pl, pl.activate() + } + return load(name) +} + +// Get returns the plugin given the specified name and requested implementation. +func Get(name, imp string) (*Plugin, error) { + pl, err := get(name) + if err != nil { + return nil, err + } + if pl.implements(imp) { + logrus.Debugf("%s implements: %s", name, imp) + return pl, nil + } + return nil, ErrNotImplements +} + +// Handle adds the specified function to the extpointHandlers. +func Handle(iface string, fn func(string, *Client)) { + extpointHandlers[iface] = fn +} + +// GetAll returns all the plugins for the specified implementation +func GetAll(imp string) ([]*Plugin, error) { + pluginNames, err := Scan() + if err != nil { + return nil, err + } + + type plLoad struct { + pl *Plugin + err error + } + + chPl := make(chan *plLoad, len(pluginNames)) + var wg sync.WaitGroup + for _, name := range pluginNames { + if pl, ok := storage.plugins[name]; ok { + chPl <- &plLoad{pl, nil} + continue + } + + wg.Add(1) + go func(name string) { + defer wg.Done() + pl, err := loadWithRetry(name, false) + chPl <- &plLoad{pl, err} + }(name) + } + + wg.Wait() + close(chPl) + + var out []*Plugin + for pl := range chPl { + if pl.err != nil { + logrus.Error(pl.err) + continue + } + if pl.pl.implements(imp) { + out = append(out, pl.pl) + } + } + return out, nil +} diff --git a/pkg/plugins/transport/http.go b/pkg/plugins/transport/http.go new file mode 100644 index 00000000..5be146af --- /dev/null +++ b/pkg/plugins/transport/http.go @@ -0,0 +1,36 @@ +package transport + +import ( + "io" + "net/http" +) + +// httpTransport holds an http.RoundTripper +// and information about the scheme and address the transport +// sends request to. +type httpTransport struct { + http.RoundTripper + scheme string + addr string +} + +// NewHTTPTransport creates a new httpTransport. +func NewHTTPTransport(r http.RoundTripper, scheme, addr string) Transport { + return httpTransport{ + RoundTripper: r, + scheme: scheme, + addr: addr, + } +} + +// NewRequest creates a new http.Request and sets the URL +// scheme and address with the transport's fields. +func (t httpTransport) NewRequest(path string, data io.Reader) (*http.Request, error) { + req, err := newHTTPRequest(path, data) + if err != nil { + return nil, err + } + req.URL.Scheme = t.scheme + req.URL.Host = t.addr + return req, nil +} diff --git a/pkg/plugins/transport/transport.go b/pkg/plugins/transport/transport.go new file mode 100644 index 00000000..d7f1e210 --- /dev/null +++ b/pkg/plugins/transport/transport.go @@ -0,0 +1,36 @@ +package transport + +import ( + "io" + "net/http" + "strings" +) + +// VersionMimetype is the Content-Type the engine sends to plugins. +const VersionMimetype = "application/vnd.docker.plugins.v1.2+json" + +// RequestFactory defines an interface that +// transports can implement to create new requests. +type RequestFactory interface { + NewRequest(path string, data io.Reader) (*http.Request, error) +} + +// Transport defines an interface that plugin transports +// must implement. +type Transport interface { + http.RoundTripper + RequestFactory +} + +// newHTTPRequest creates a new request with a path and a body. +func newHTTPRequest(path string, data io.Reader) (*http.Request, error) { + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + req, err := http.NewRequest("POST", path, data) + if err != nil { + return nil, err + } + req.Header.Add("Accept", VersionMimetype) + return req, nil +} diff --git a/pkg/pools/pools.go b/pkg/pools/pools.go new file mode 100644 index 00000000..76e84f9d --- /dev/null +++ b/pkg/pools/pools.go @@ -0,0 +1,119 @@ +// Package pools provides a collection of pools which provide various +// data types with buffers. These can be used to lower the number of +// memory allocations and reuse buffers. +// +// New pools should be added to this package to allow them to be +// shared across packages. +// +// Utility functions which operate on pools should be added to this +// package to allow them to be reused. +package pools + +import ( + "bufio" + "io" + "sync" + + "github.com/docker/docker/pkg/ioutils" +) + +var ( + // BufioReader32KPool is a pool which returns bufio.Reader with a 32K buffer. + BufioReader32KPool *BufioReaderPool + // BufioWriter32KPool is a pool which returns bufio.Writer with a 32K buffer. + BufioWriter32KPool *BufioWriterPool +) + +const buffer32K = 32 * 1024 + +// BufioReaderPool is a bufio reader that uses sync.Pool. +type BufioReaderPool struct { + pool sync.Pool +} + +func init() { + BufioReader32KPool = newBufioReaderPoolWithSize(buffer32K) + BufioWriter32KPool = newBufioWriterPoolWithSize(buffer32K) +} + +// newBufioReaderPoolWithSize is unexported because new pools should be +// added here to be shared where required. +func newBufioReaderPoolWithSize(size int) *BufioReaderPool { + pool := sync.Pool{ + New: func() interface{} { return bufio.NewReaderSize(nil, size) }, + } + return &BufioReaderPool{pool: pool} +} + +// Get returns a bufio.Reader which reads from r. The buffer size is that of the pool. +func (bufPool *BufioReaderPool) Get(r io.Reader) *bufio.Reader { + buf := bufPool.pool.Get().(*bufio.Reader) + buf.Reset(r) + return buf +} + +// Put puts the bufio.Reader back into the pool. +func (bufPool *BufioReaderPool) Put(b *bufio.Reader) { + b.Reset(nil) + bufPool.pool.Put(b) +} + +// Copy is a convenience wrapper which uses a buffer to avoid allocation in io.Copy. +func Copy(dst io.Writer, src io.Reader) (written int64, err error) { + buf := BufioReader32KPool.Get(src) + written, err = io.Copy(dst, buf) + BufioReader32KPool.Put(buf) + return +} + +// NewReadCloserWrapper returns a wrapper which puts the bufio.Reader back +// into the pool and closes the reader if it's an io.ReadCloser. +func (bufPool *BufioReaderPool) NewReadCloserWrapper(buf *bufio.Reader, r io.Reader) io.ReadCloser { + return ioutils.NewReadCloserWrapper(r, func() error { + if readCloser, ok := r.(io.ReadCloser); ok { + readCloser.Close() + } + bufPool.Put(buf) + return nil + }) +} + +// BufioWriterPool is a bufio writer that uses sync.Pool. +type BufioWriterPool struct { + pool sync.Pool +} + +// newBufioWriterPoolWithSize is unexported because new pools should be +// added here to be shared where required. +func newBufioWriterPoolWithSize(size int) *BufioWriterPool { + pool := sync.Pool{ + New: func() interface{} { return bufio.NewWriterSize(nil, size) }, + } + return &BufioWriterPool{pool: pool} +} + +// Get returns a bufio.Writer which writes to w. The buffer size is that of the pool. +func (bufPool *BufioWriterPool) Get(w io.Writer) *bufio.Writer { + buf := bufPool.pool.Get().(*bufio.Writer) + buf.Reset(w) + return buf +} + +// Put puts the bufio.Writer back into the pool. +func (bufPool *BufioWriterPool) Put(b *bufio.Writer) { + b.Reset(nil) + bufPool.pool.Put(b) +} + +// NewWriteCloserWrapper returns a wrapper which puts the bufio.Writer back +// into the pool and closes the writer if it's an io.Writecloser. +func (bufPool *BufioWriterPool) NewWriteCloserWrapper(buf *bufio.Writer, w io.Writer) io.WriteCloser { + return ioutils.NewWriteCloserWrapper(w, func() error { + buf.Flush() + if writeCloser, ok := w.(io.WriteCloser); ok { + writeCloser.Close() + } + bufPool.Put(buf) + return nil + }) +} diff --git a/pkg/pools/pools_test.go b/pkg/pools/pools_test.go new file mode 100644 index 00000000..1661b780 --- /dev/null +++ b/pkg/pools/pools_test.go @@ -0,0 +1,161 @@ +package pools + +import ( + "bufio" + "bytes" + "io" + "strings" + "testing" +) + +func TestBufioReaderPoolGetWithNoReaderShouldCreateOne(t *testing.T) { + reader := BufioReader32KPool.Get(nil) + if reader == nil { + t.Fatalf("BufioReaderPool should have create a bufio.Reader but did not.") + } +} + +func TestBufioReaderPoolPutAndGet(t *testing.T) { + sr := bufio.NewReader(strings.NewReader("foobar")) + reader := BufioReader32KPool.Get(sr) + if reader == nil { + t.Fatalf("BufioReaderPool should not return a nil reader.") + } + // verify the first 3 byte + buf1 := make([]byte, 3) + _, err := reader.Read(buf1) + if err != nil { + t.Fatal(err) + } + if actual := string(buf1); actual != "foo" { + t.Fatalf("The first letter should have been 'foo' but was %v", actual) + } + BufioReader32KPool.Put(reader) + // Try to read the next 3 bytes + _, err = sr.Read(make([]byte, 3)) + if err == nil || err != io.EOF { + t.Fatalf("The buffer should have been empty, issue an EOF error.") + } +} + +type simpleReaderCloser struct { + io.Reader + closed bool +} + +func (r *simpleReaderCloser) Close() error { + r.closed = true + return nil +} + +func TestNewReadCloserWrapperWithAReadCloser(t *testing.T) { + br := bufio.NewReader(strings.NewReader("")) + sr := &simpleReaderCloser{ + Reader: strings.NewReader("foobar"), + closed: false, + } + reader := BufioReader32KPool.NewReadCloserWrapper(br, sr) + if reader == nil { + t.Fatalf("NewReadCloserWrapper should not return a nil reader.") + } + // Verify the content of reader + buf := make([]byte, 3) + _, err := reader.Read(buf) + if err != nil { + t.Fatal(err) + } + if actual := string(buf); actual != "foo" { + t.Fatalf("The first 3 letter should have been 'foo' but were %v", actual) + } + reader.Close() + // Read 3 more bytes "bar" + _, err = reader.Read(buf) + if err != nil { + t.Fatal(err) + } + if actual := string(buf); actual != "bar" { + t.Fatalf("The first 3 letter should have been 'bar' but were %v", actual) + } + if !sr.closed { + t.Fatalf("The ReaderCloser should have been closed, it is not.") + } +} + +func TestBufioWriterPoolGetWithNoReaderShouldCreateOne(t *testing.T) { + writer := BufioWriter32KPool.Get(nil) + if writer == nil { + t.Fatalf("BufioWriterPool should have create a bufio.Writer but did not.") + } +} + +func TestBufioWriterPoolPutAndGet(t *testing.T) { + buf := new(bytes.Buffer) + bw := bufio.NewWriter(buf) + writer := BufioWriter32KPool.Get(bw) + if writer == nil { + t.Fatalf("BufioReaderPool should not return a nil writer.") + } + written, err := writer.Write([]byte("foobar")) + if err != nil { + t.Fatal(err) + } + if written != 6 { + t.Fatalf("Should have written 6 bytes, but wrote %v bytes", written) + } + // Make sure we Flush all the way ? + writer.Flush() + bw.Flush() + if len(buf.Bytes()) != 6 { + t.Fatalf("The buffer should contain 6 bytes ('foobar') but contains %v ('%v')", buf.Bytes(), string(buf.Bytes())) + } + // Reset the buffer + buf.Reset() + BufioWriter32KPool.Put(writer) + // Try to write something + if _, err = writer.Write([]byte("barfoo")); err != nil { + t.Fatal(err) + } + // If we now try to flush it, it should panic (the writer is nil) + // recover it + defer func() { + if r := recover(); r == nil { + t.Fatal("Trying to flush the writter should have 'paniced', did not.") + } + }() + writer.Flush() +} + +type simpleWriterCloser struct { + io.Writer + closed bool +} + +func (r *simpleWriterCloser) Close() error { + r.closed = true + return nil +} + +func TestNewWriteCloserWrapperWithAWriteCloser(t *testing.T) { + buf := new(bytes.Buffer) + bw := bufio.NewWriter(buf) + sw := &simpleWriterCloser{ + Writer: new(bytes.Buffer), + closed: false, + } + bw.Flush() + writer := BufioWriter32KPool.NewWriteCloserWrapper(bw, sw) + if writer == nil { + t.Fatalf("BufioReaderPool should not return a nil writer.") + } + written, err := writer.Write([]byte("foobar")) + if err != nil { + t.Fatal(err) + } + if written != 6 { + t.Fatalf("Should have written 6 bytes, but wrote %v bytes", written) + } + writer.Close() + if !sw.closed { + t.Fatalf("The ReaderCloser should have been closed, it is not.") + } +} diff --git a/pkg/progress/progress.go b/pkg/progress/progress.go new file mode 100644 index 00000000..61315cb8 --- /dev/null +++ b/pkg/progress/progress.go @@ -0,0 +1,73 @@ +package progress + +import ( + "fmt" +) + +// Progress represents the progress of a transfer. +type Progress struct { + ID string + + // Progress contains a Message or... + Message string + + // ...progress of an action + Action string + Current int64 + Total int64 + + // Aux contains extra information not presented to the user, such as + // digests for push signing. + Aux interface{} + + LastUpdate bool +} + +// Output is an interface for writing progress information. It's +// like a writer for progress, but we don't call it Writer because +// that would be confusing next to ProgressReader (also, because it +// doesn't implement the io.Writer interface). +type Output interface { + WriteProgress(Progress) error +} + +type chanOutput chan<- Progress + +func (out chanOutput) WriteProgress(p Progress) error { + out <- p + return nil +} + +// ChanOutput returns a Output that writes progress updates to the +// supplied channel. +func ChanOutput(progressChan chan<- Progress) Output { + return chanOutput(progressChan) +} + +// Update is a convenience function to write a progress update to the channel. +func Update(out Output, id, action string) { + out.WriteProgress(Progress{ID: id, Action: action}) +} + +// Updatef is a convenience function to write a printf-formatted progress update +// to the channel. +func Updatef(out Output, id, format string, a ...interface{}) { + Update(out, id, fmt.Sprintf(format, a...)) +} + +// Message is a convenience function to write a progress message to the channel. +func Message(out Output, id, message string) { + out.WriteProgress(Progress{ID: id, Message: message}) +} + +// Messagef is a convenience function to write a printf-formatted progress +// message to the channel. +func Messagef(out Output, id, format string, a ...interface{}) { + Message(out, id, fmt.Sprintf(format, a...)) +} + +// Aux sends auxiliary information over a progress interface, which will not be +// formatted for the UI. This is used for things such as push signing. +func Aux(out Output, a interface{}) { + out.WriteProgress(Progress{Aux: a}) +} diff --git a/pkg/progress/progressreader.go b/pkg/progress/progressreader.go new file mode 100644 index 00000000..c39e2b69 --- /dev/null +++ b/pkg/progress/progressreader.go @@ -0,0 +1,59 @@ +package progress + +import ( + "io" +) + +// Reader is a Reader with progress bar. +type Reader struct { + in io.ReadCloser // Stream to read from + out Output // Where to send progress bar to + size int64 + current int64 + lastUpdate int64 + id string + action string +} + +// NewProgressReader creates a new ProgressReader. +func NewProgressReader(in io.ReadCloser, out Output, size int64, id, action string) *Reader { + return &Reader{ + in: in, + out: out, + size: size, + id: id, + action: action, + } +} + +func (p *Reader) Read(buf []byte) (n int, err error) { + read, err := p.in.Read(buf) + p.current += int64(read) + updateEvery := int64(1024 * 512) //512kB + if p.size > 0 { + // Update progress for every 1% read if 1% < 512kB + if increment := int64(0.01 * float64(p.size)); increment < updateEvery { + updateEvery = increment + } + } + if p.current-p.lastUpdate > updateEvery || err != nil { + p.updateProgress(err != nil && read == 0) + p.lastUpdate = p.current + } + + return read, err +} + +// Close closes the progress reader and its underlying reader. +func (p *Reader) Close() error { + if p.current < p.size { + // print a full progress bar when closing prematurely + p.current = p.size + p.updateProgress(false) + } + return p.in.Close() +} + +func (p *Reader) updateProgress(last bool) { + p.out.WriteProgress(Progress{ID: p.id, Action: p.action, Current: p.current, Total: p.size, LastUpdate: last}) +} diff --git a/pkg/progress/progressreader_test.go b/pkg/progress/progressreader_test.go new file mode 100644 index 00000000..b14d4015 --- /dev/null +++ b/pkg/progress/progressreader_test.go @@ -0,0 +1,75 @@ +package progress + +import ( + "bytes" + "io" + "io/ioutil" + "testing" +) + +func TestOutputOnPrematureClose(t *testing.T) { + content := []byte("TESTING") + reader := ioutil.NopCloser(bytes.NewReader(content)) + progressChan := make(chan Progress, 10) + + pr := NewProgressReader(reader, ChanOutput(progressChan), int64(len(content)), "Test", "Read") + + part := make([]byte, 4, 4) + _, err := io.ReadFull(pr, part) + if err != nil { + pr.Close() + t.Fatal(err) + } + +drainLoop: + for { + select { + case <-progressChan: + default: + break drainLoop + } + } + + pr.Close() + + select { + case <-progressChan: + default: + t.Fatalf("Expected some output when closing prematurely") + } +} + +func TestCompleteSilently(t *testing.T) { + content := []byte("TESTING") + reader := ioutil.NopCloser(bytes.NewReader(content)) + progressChan := make(chan Progress, 10) + + pr := NewProgressReader(reader, ChanOutput(progressChan), int64(len(content)), "Test", "Read") + + out, err := ioutil.ReadAll(pr) + if err != nil { + pr.Close() + t.Fatal(err) + } + if string(out) != "TESTING" { + pr.Close() + t.Fatalf("Unexpected output %q from reader", string(out)) + } + +drainLoop: + for { + select { + case <-progressChan: + default: + break drainLoop + } + } + + pr.Close() + + select { + case <-progressChan: + t.Fatalf("Should have closed silently when read is complete") + default: + } +} diff --git a/pkg/promise/promise.go b/pkg/promise/promise.go new file mode 100644 index 00000000..dd52b908 --- /dev/null +++ b/pkg/promise/promise.go @@ -0,0 +1,11 @@ +package promise + +// Go is a basic promise implementation: it wraps calls a function in a goroutine, +// and returns a channel which will later return the function's return value. +func Go(f func() error) chan error { + ch := make(chan error, 1) + go func() { + ch <- f() + }() + return ch +} diff --git a/pkg/proxy/network_proxy_test.go b/pkg/proxy/network_proxy_test.go new file mode 100644 index 00000000..9e382567 --- /dev/null +++ b/pkg/proxy/network_proxy_test.go @@ -0,0 +1,216 @@ +package proxy + +import ( + "bytes" + "fmt" + "io" + "net" + "strings" + "testing" + "time" +) + +var testBuf = []byte("Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo") +var testBufSize = len(testBuf) + +type EchoServer interface { + Run() + Close() + LocalAddr() net.Addr +} + +type TCPEchoServer struct { + listener net.Listener + testCtx *testing.T +} + +type UDPEchoServer struct { + conn net.PacketConn + testCtx *testing.T +} + +func NewEchoServer(t *testing.T, proto, address string) EchoServer { + var server EchoServer + if strings.HasPrefix(proto, "tcp") { + listener, err := net.Listen(proto, address) + if err != nil { + t.Fatal(err) + } + server = &TCPEchoServer{listener: listener, testCtx: t} + } else { + socket, err := net.ListenPacket(proto, address) + if err != nil { + t.Fatal(err) + } + server = &UDPEchoServer{conn: socket, testCtx: t} + } + return server +} + +func (server *TCPEchoServer) Run() { + go func() { + for { + client, err := server.listener.Accept() + if err != nil { + return + } + go func(client net.Conn) { + if _, err := io.Copy(client, client); err != nil { + server.testCtx.Logf("can't echo to the client: %v\n", err.Error()) + } + client.Close() + }(client) + } + }() +} + +func (server *TCPEchoServer) LocalAddr() net.Addr { return server.listener.Addr() } +func (server *TCPEchoServer) Close() { server.listener.Addr() } + +func (server *UDPEchoServer) Run() { + go func() { + readBuf := make([]byte, 1024) + for { + read, from, err := server.conn.ReadFrom(readBuf) + if err != nil { + return + } + for i := 0; i != read; { + written, err := server.conn.WriteTo(readBuf[i:read], from) + if err != nil { + break + } + i += written + } + } + }() +} + +func (server *UDPEchoServer) LocalAddr() net.Addr { return server.conn.LocalAddr() } +func (server *UDPEchoServer) Close() { server.conn.Close() } + +func testProxyAt(t *testing.T, proto string, proxy Proxy, addr string) { + defer proxy.Close() + go proxy.Run() + client, err := net.Dial(proto, addr) + if err != nil { + t.Fatalf("Can't connect to the proxy: %v", err) + } + defer client.Close() + client.SetDeadline(time.Now().Add(10 * time.Second)) + if _, err = client.Write(testBuf); err != nil { + t.Fatal(err) + } + recvBuf := make([]byte, testBufSize) + if _, err = client.Read(recvBuf); err != nil { + t.Fatal(err) + } + if !bytes.Equal(testBuf, recvBuf) { + t.Fatal(fmt.Errorf("Expected [%v] but got [%v]", testBuf, recvBuf)) + } +} + +func testProxy(t *testing.T, proto string, proxy Proxy) { + testProxyAt(t, proto, proxy, proxy.FrontendAddr().String()) +} + +func TestTCP4Proxy(t *testing.T) { + backend := NewEchoServer(t, "tcp", "127.0.0.1:0") + defer backend.Close() + backend.Run() + frontendAddr := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0} + proxy, err := NewProxy(frontendAddr, backend.LocalAddr()) + if err != nil { + t.Fatal(err) + } + testProxy(t, "tcp", proxy) +} + +func TestTCP6Proxy(t *testing.T) { + backend := NewEchoServer(t, "tcp", "[::1]:0") + defer backend.Close() + backend.Run() + frontendAddr := &net.TCPAddr{IP: net.IPv6loopback, Port: 0} + proxy, err := NewProxy(frontendAddr, backend.LocalAddr()) + if err != nil { + t.Fatal(err) + } + testProxy(t, "tcp", proxy) +} + +func TestTCPDualStackProxy(t *testing.T) { + // If I understand `godoc -src net favoriteAddrFamily` (used by the + // net.Listen* functions) correctly this should work, but it doesn't. + t.Skip("No support for dual stack yet") + backend := NewEchoServer(t, "tcp", "[::1]:0") + defer backend.Close() + backend.Run() + frontendAddr := &net.TCPAddr{IP: net.IPv6loopback, Port: 0} + proxy, err := NewProxy(frontendAddr, backend.LocalAddr()) + if err != nil { + t.Fatal(err) + } + ipv4ProxyAddr := &net.TCPAddr{ + IP: net.IPv4(127, 0, 0, 1), + Port: proxy.FrontendAddr().(*net.TCPAddr).Port, + } + testProxyAt(t, "tcp", proxy, ipv4ProxyAddr.String()) +} + +func TestUDP4Proxy(t *testing.T) { + backend := NewEchoServer(t, "udp", "127.0.0.1:0") + defer backend.Close() + backend.Run() + frontendAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0} + proxy, err := NewProxy(frontendAddr, backend.LocalAddr()) + if err != nil { + t.Fatal(err) + } + testProxy(t, "udp", proxy) +} + +func TestUDP6Proxy(t *testing.T) { + backend := NewEchoServer(t, "udp", "[::1]:0") + defer backend.Close() + backend.Run() + frontendAddr := &net.UDPAddr{IP: net.IPv6loopback, Port: 0} + proxy, err := NewProxy(frontendAddr, backend.LocalAddr()) + if err != nil { + t.Fatal(err) + } + testProxy(t, "udp", proxy) +} + +func TestUDPWriteError(t *testing.T) { + frontendAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0} + // Hopefully, this port will be free: */ + backendAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 25587} + proxy, err := NewProxy(frontendAddr, backendAddr) + if err != nil { + t.Fatal(err) + } + defer proxy.Close() + go proxy.Run() + client, err := net.Dial("udp", "127.0.0.1:25587") + if err != nil { + t.Fatalf("Can't connect to the proxy: %v", err) + } + defer client.Close() + // Make sure the proxy doesn't stop when there is no actual backend: + client.Write(testBuf) + client.Write(testBuf) + backend := NewEchoServer(t, "udp", "127.0.0.1:25587") + defer backend.Close() + backend.Run() + client.SetDeadline(time.Now().Add(10 * time.Second)) + if _, err = client.Write(testBuf); err != nil { + t.Fatal(err) + } + recvBuf := make([]byte, testBufSize) + if _, err = client.Read(recvBuf); err != nil { + t.Fatal(err) + } + if !bytes.Equal(testBuf, recvBuf) { + t.Fatal(fmt.Errorf("Expected [%v] but got [%v]", testBuf, recvBuf)) + } +} diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go new file mode 100644 index 00000000..4e24e5f6 --- /dev/null +++ b/pkg/proxy/proxy.go @@ -0,0 +1,37 @@ +// Package proxy provides a network Proxy interface and implementations for TCP +// and UDP. +package proxy + +import ( + "fmt" + "net" +) + +// Proxy defines the behavior of a proxy. It forwards traffic back and forth +// between two endpoints : the frontend and the backend. +// It can be used to do software port-mapping between two addresses. +// e.g. forward all traffic between the frontend (host) 127.0.0.1:3000 +// to the backend (container) at 172.17.42.108:4000. +type Proxy interface { + // Run starts forwarding traffic back and forth between the front + // and back-end addresses. + Run() + // Close stops forwarding traffic and close both ends of the Proxy. + Close() + // FrontendAddr returns the address on which the proxy is listening. + FrontendAddr() net.Addr + // BackendAddr returns the proxied address. + BackendAddr() net.Addr +} + +// NewProxy creates a Proxy according to the specified frontendAddr and backendAddr. +func NewProxy(frontendAddr, backendAddr net.Addr) (Proxy, error) { + switch frontendAddr.(type) { + case *net.UDPAddr: + return NewUDPProxy(frontendAddr.(*net.UDPAddr), backendAddr.(*net.UDPAddr)) + case *net.TCPAddr: + return NewTCPProxy(frontendAddr.(*net.TCPAddr), backendAddr.(*net.TCPAddr)) + default: + panic(fmt.Errorf("Unsupported protocol")) + } +} diff --git a/pkg/proxy/stub_proxy.go b/pkg/proxy/stub_proxy.go new file mode 100644 index 00000000..571749e4 --- /dev/null +++ b/pkg/proxy/stub_proxy.go @@ -0,0 +1,31 @@ +package proxy + +import ( + "net" +) + +// StubProxy is a proxy that is a stub (does nothing). +type StubProxy struct { + frontendAddr net.Addr + backendAddr net.Addr +} + +// Run does nothing. +func (p *StubProxy) Run() {} + +// Close does nothing. +func (p *StubProxy) Close() {} + +// FrontendAddr returns the frontend address. +func (p *StubProxy) FrontendAddr() net.Addr { return p.frontendAddr } + +// BackendAddr returns the backend address. +func (p *StubProxy) BackendAddr() net.Addr { return p.backendAddr } + +// NewStubProxy creates a new StubProxy +func NewStubProxy(frontendAddr, backendAddr net.Addr) (Proxy, error) { + return &StubProxy{ + frontendAddr: frontendAddr, + backendAddr: backendAddr, + }, nil +} diff --git a/pkg/proxy/tcp_proxy.go b/pkg/proxy/tcp_proxy.go new file mode 100644 index 00000000..3cd742af --- /dev/null +++ b/pkg/proxy/tcp_proxy.go @@ -0,0 +1,99 @@ +package proxy + +import ( + "io" + "net" + "syscall" + + "github.com/Sirupsen/logrus" +) + +// TCPProxy is a proxy for TCP connections. It implements the Proxy interface to +// handle TCP traffic forwarding between the frontend and backend addresses. +type TCPProxy struct { + listener *net.TCPListener + frontendAddr *net.TCPAddr + backendAddr *net.TCPAddr +} + +// NewTCPProxy creates a new TCPProxy. +func NewTCPProxy(frontendAddr, backendAddr *net.TCPAddr) (*TCPProxy, error) { + listener, err := net.ListenTCP("tcp", frontendAddr) + if err != nil { + return nil, err + } + // If the port in frontendAddr was 0 then ListenTCP will have a picked + // a port to listen on, hence the call to Addr to get that actual port: + return &TCPProxy{ + listener: listener, + frontendAddr: listener.Addr().(*net.TCPAddr), + backendAddr: backendAddr, + }, nil +} + +func (proxy *TCPProxy) clientLoop(client *net.TCPConn, quit chan bool) { + backend, err := net.DialTCP("tcp", nil, proxy.backendAddr) + if err != nil { + logrus.Printf("Can't forward traffic to backend tcp/%v: %s\n", proxy.backendAddr, err) + client.Close() + return + } + + event := make(chan int64) + var broker = func(to, from *net.TCPConn) { + written, err := io.Copy(to, from) + if err != nil { + // If the socket we are writing to is shutdown with + // SHUT_WR, forward it to the other end of the pipe: + if err, ok := err.(*net.OpError); ok && err.Err == syscall.EPIPE { + from.CloseWrite() + } + } + to.CloseRead() + event <- written + } + + go broker(client, backend) + go broker(backend, client) + + var transferred int64 + for i := 0; i < 2; i++ { + select { + case written := <-event: + transferred += written + case <-quit: + // Interrupt the two brokers and "join" them. + client.Close() + backend.Close() + for ; i < 2; i++ { + transferred += <-event + } + return + } + } + client.Close() + backend.Close() +} + +// Run starts forwarding the traffic using TCP. +func (proxy *TCPProxy) Run() { + quit := make(chan bool) + defer close(quit) + for { + client, err := proxy.listener.Accept() + if err != nil { + logrus.Printf("Stopping proxy on tcp/%v for tcp/%v (%s)", proxy.frontendAddr, proxy.backendAddr, err) + return + } + go proxy.clientLoop(client.(*net.TCPConn), quit) + } +} + +// Close stops forwarding the traffic. +func (proxy *TCPProxy) Close() { proxy.listener.Close() } + +// FrontendAddr returns the TCP address on which the proxy is listening. +func (proxy *TCPProxy) FrontendAddr() net.Addr { return proxy.frontendAddr } + +// BackendAddr returns the TCP proxied address. +func (proxy *TCPProxy) BackendAddr() net.Addr { return proxy.backendAddr } diff --git a/pkg/proxy/udp_proxy.go b/pkg/proxy/udp_proxy.go new file mode 100644 index 00000000..b8375c37 --- /dev/null +++ b/pkg/proxy/udp_proxy.go @@ -0,0 +1,169 @@ +package proxy + +import ( + "encoding/binary" + "net" + "strings" + "sync" + "syscall" + "time" + + "github.com/Sirupsen/logrus" +) + +const ( + // UDPConnTrackTimeout is the timeout used for UDP connection tracking + UDPConnTrackTimeout = 90 * time.Second + // UDPBufSize is the buffer size for the UDP proxy + UDPBufSize = 65507 +) + +// A net.Addr where the IP is split into two fields so you can use it as a key +// in a map: +type connTrackKey struct { + IPHigh uint64 + IPLow uint64 + Port int +} + +func newConnTrackKey(addr *net.UDPAddr) *connTrackKey { + if len(addr.IP) == net.IPv4len { + return &connTrackKey{ + IPHigh: 0, + IPLow: uint64(binary.BigEndian.Uint32(addr.IP)), + Port: addr.Port, + } + } + return &connTrackKey{ + IPHigh: binary.BigEndian.Uint64(addr.IP[:8]), + IPLow: binary.BigEndian.Uint64(addr.IP[8:]), + Port: addr.Port, + } +} + +type connTrackMap map[connTrackKey]*net.UDPConn + +// UDPProxy is proxy for which handles UDP datagrams. It implements the Proxy +// interface to handle UDP traffic forwarding between the frontend and backend +// addresses. +type UDPProxy struct { + listener *net.UDPConn + frontendAddr *net.UDPAddr + backendAddr *net.UDPAddr + connTrackTable connTrackMap + connTrackLock sync.Mutex +} + +// NewUDPProxy creates a new UDPProxy. +func NewUDPProxy(frontendAddr, backendAddr *net.UDPAddr) (*UDPProxy, error) { + listener, err := net.ListenUDP("udp", frontendAddr) + if err != nil { + return nil, err + } + return &UDPProxy{ + listener: listener, + frontendAddr: listener.LocalAddr().(*net.UDPAddr), + backendAddr: backendAddr, + connTrackTable: make(connTrackMap), + }, nil +} + +func (proxy *UDPProxy) replyLoop(proxyConn *net.UDPConn, clientAddr *net.UDPAddr, clientKey *connTrackKey) { + defer func() { + proxy.connTrackLock.Lock() + delete(proxy.connTrackTable, *clientKey) + proxy.connTrackLock.Unlock() + proxyConn.Close() + }() + + readBuf := make([]byte, UDPBufSize) + for { + proxyConn.SetReadDeadline(time.Now().Add(UDPConnTrackTimeout)) + again: + read, err := proxyConn.Read(readBuf) + if err != nil { + if err, ok := err.(*net.OpError); ok && err.Err == syscall.ECONNREFUSED { + // This will happen if the last write failed + // (e.g: nothing is actually listening on the + // proxied port on the container), ignore it + // and continue until UDPConnTrackTimeout + // expires: + goto again + } + return + } + for i := 0; i != read; { + written, err := proxy.listener.WriteToUDP(readBuf[i:read], clientAddr) + if err != nil { + return + } + i += written + } + } +} + +// Run starts forwarding the traffic using UDP. +func (proxy *UDPProxy) Run() { + readBuf := make([]byte, UDPBufSize) + for { + read, from, err := proxy.listener.ReadFromUDP(readBuf) + if err != nil { + // NOTE: Apparently ReadFrom doesn't return + // ECONNREFUSED like Read do (see comment in + // UDPProxy.replyLoop) + if !isClosedError(err) { + logrus.Printf("Stopping proxy on udp/%v for udp/%v (%s)", proxy.frontendAddr, proxy.backendAddr, err) + } + break + } + + fromKey := newConnTrackKey(from) + proxy.connTrackLock.Lock() + proxyConn, hit := proxy.connTrackTable[*fromKey] + if !hit { + proxyConn, err = net.DialUDP("udp", nil, proxy.backendAddr) + if err != nil { + logrus.Printf("Can't proxy a datagram to udp/%s: %s\n", proxy.backendAddr, err) + proxy.connTrackLock.Unlock() + continue + } + proxy.connTrackTable[*fromKey] = proxyConn + go proxy.replyLoop(proxyConn, from, fromKey) + } + proxy.connTrackLock.Unlock() + for i := 0; i != read; { + written, err := proxyConn.Write(readBuf[i:read]) + if err != nil { + logrus.Printf("Can't proxy a datagram to udp/%s: %s\n", proxy.backendAddr, err) + break + } + i += written + } + } +} + +// Close stops forwarding the traffic. +func (proxy *UDPProxy) Close() { + proxy.listener.Close() + proxy.connTrackLock.Lock() + defer proxy.connTrackLock.Unlock() + for _, conn := range proxy.connTrackTable { + conn.Close() + } +} + +// FrontendAddr returns the UDP address on which the proxy is listening. +func (proxy *UDPProxy) FrontendAddr() net.Addr { return proxy.frontendAddr } + +// BackendAddr returns the proxied UDP address. +func (proxy *UDPProxy) BackendAddr() net.Addr { return proxy.backendAddr } + +func isClosedError(err error) bool { + /* This comparison is ugly, but unfortunately, net.go doesn't export errClosing. + * See: + * http://golang.org/src/pkg/net/net.go + * https://code.google.com/p/go/issues/detail?id=4337 + * https://groups.google.com/forum/#!msg/golang-nuts/0_aaCvBmOcM/SptmDyX1XJMJ + */ + return strings.HasSuffix(err.Error(), "use of closed network connection") +} diff --git a/pkg/pubsub/publisher.go b/pkg/pubsub/publisher.go new file mode 100644 index 00000000..09364617 --- /dev/null +++ b/pkg/pubsub/publisher.go @@ -0,0 +1,111 @@ +package pubsub + +import ( + "sync" + "time" +) + +var wgPool = sync.Pool{New: func() interface{} { return new(sync.WaitGroup) }} + +// NewPublisher creates a new pub/sub publisher to broadcast messages. +// The duration is used as the send timeout as to not block the publisher publishing +// messages to other clients if one client is slow or unresponsive. +// The buffer is used when creating new channels for subscribers. +func NewPublisher(publishTimeout time.Duration, buffer int) *Publisher { + return &Publisher{ + buffer: buffer, + timeout: publishTimeout, + subscribers: make(map[subscriber]topicFunc), + } +} + +type subscriber chan interface{} +type topicFunc func(v interface{}) bool + +// Publisher is basic pub/sub structure. Allows to send events and subscribe +// to them. Can be safely used from multiple goroutines. +type Publisher struct { + m sync.RWMutex + buffer int + timeout time.Duration + subscribers map[subscriber]topicFunc +} + +// Len returns the number of subscribers for the publisher +func (p *Publisher) Len() int { + p.m.RLock() + i := len(p.subscribers) + p.m.RUnlock() + return i +} + +// Subscribe adds a new subscriber to the publisher returning the channel. +func (p *Publisher) Subscribe() chan interface{} { + return p.SubscribeTopic(nil) +} + +// SubscribeTopic adds a new subscriber that filters messages sent by a topic. +func (p *Publisher) SubscribeTopic(topic topicFunc) chan interface{} { + ch := make(chan interface{}, p.buffer) + p.m.Lock() + p.subscribers[ch] = topic + p.m.Unlock() + return ch +} + +// Evict removes the specified subscriber from receiving any more messages. +func (p *Publisher) Evict(sub chan interface{}) { + p.m.Lock() + delete(p.subscribers, sub) + close(sub) + p.m.Unlock() +} + +// Publish sends the data in v to all subscribers currently registered with the publisher. +func (p *Publisher) Publish(v interface{}) { + p.m.RLock() + if len(p.subscribers) == 0 { + p.m.RUnlock() + return + } + + wg := wgPool.Get().(*sync.WaitGroup) + for sub, topic := range p.subscribers { + wg.Add(1) + go p.sendTopic(sub, topic, v, wg) + } + wg.Wait() + wgPool.Put(wg) + p.m.RUnlock() +} + +// Close closes the channels to all subscribers registered with the publisher. +func (p *Publisher) Close() { + p.m.Lock() + for sub := range p.subscribers { + delete(p.subscribers, sub) + close(sub) + } + p.m.Unlock() +} + +func (p *Publisher) sendTopic(sub subscriber, topic topicFunc, v interface{}, wg *sync.WaitGroup) { + defer wg.Done() + if topic != nil && !topic(v) { + return + } + + // send under a select as to not block if the receiver is unavailable + if p.timeout > 0 { + select { + case sub <- v: + case <-time.After(p.timeout): + } + return + } + + select { + case sub <- v: + default: + } +} diff --git a/pkg/pubsub/publisher_test.go b/pkg/pubsub/publisher_test.go new file mode 100644 index 00000000..d6b0a1d5 --- /dev/null +++ b/pkg/pubsub/publisher_test.go @@ -0,0 +1,142 @@ +package pubsub + +import ( + "fmt" + "testing" + "time" +) + +func TestSendToOneSub(t *testing.T) { + p := NewPublisher(100*time.Millisecond, 10) + c := p.Subscribe() + + p.Publish("hi") + + msg := <-c + if msg.(string) != "hi" { + t.Fatalf("expected message hi but received %v", msg) + } +} + +func TestSendToMultipleSubs(t *testing.T) { + p := NewPublisher(100*time.Millisecond, 10) + subs := []chan interface{}{} + subs = append(subs, p.Subscribe(), p.Subscribe(), p.Subscribe()) + + p.Publish("hi") + + for _, c := range subs { + msg := <-c + if msg.(string) != "hi" { + t.Fatalf("expected message hi but received %v", msg) + } + } +} + +func TestEvictOneSub(t *testing.T) { + p := NewPublisher(100*time.Millisecond, 10) + s1 := p.Subscribe() + s2 := p.Subscribe() + + p.Evict(s1) + p.Publish("hi") + if _, ok := <-s1; ok { + t.Fatal("expected s1 to not receive the published message") + } + + msg := <-s2 + if msg.(string) != "hi" { + t.Fatalf("expected message hi but received %v", msg) + } +} + +func TestClosePublisher(t *testing.T) { + p := NewPublisher(100*time.Millisecond, 10) + subs := []chan interface{}{} + subs = append(subs, p.Subscribe(), p.Subscribe(), p.Subscribe()) + p.Close() + + for _, c := range subs { + if _, ok := <-c; ok { + t.Fatal("expected all subscriber channels to be closed") + } + } +} + +const sampleText = "test" + +type testSubscriber struct { + dataCh chan interface{} + ch chan error +} + +func (s *testSubscriber) Wait() error { + return <-s.ch +} + +func newTestSubscriber(p *Publisher) *testSubscriber { + ts := &testSubscriber{ + dataCh: p.Subscribe(), + ch: make(chan error), + } + go func() { + for data := range ts.dataCh { + s, ok := data.(string) + if !ok { + ts.ch <- fmt.Errorf("Unexpected type %T", data) + break + } + if s != sampleText { + ts.ch <- fmt.Errorf("Unexpected text %s", s) + break + } + } + close(ts.ch) + }() + return ts +} + +// for testing with -race +func TestPubSubRace(t *testing.T) { + p := NewPublisher(0, 1024) + var subs [](*testSubscriber) + for j := 0; j < 50; j++ { + subs = append(subs, newTestSubscriber(p)) + } + for j := 0; j < 1000; j++ { + p.Publish(sampleText) + } + time.AfterFunc(1*time.Second, func() { + for _, s := range subs { + p.Evict(s.dataCh) + } + }) + for _, s := range subs { + s.Wait() + } +} + +func BenchmarkPubSub(b *testing.B) { + for i := 0; i < b.N; i++ { + b.StopTimer() + p := NewPublisher(0, 1024) + var subs [](*testSubscriber) + for j := 0; j < 50; j++ { + subs = append(subs, newTestSubscriber(p)) + } + b.StartTimer() + for j := 0; j < 1000; j++ { + p.Publish(sampleText) + } + time.AfterFunc(1*time.Second, func() { + for _, s := range subs { + p.Evict(s.dataCh) + } + }) + for _, s := range subs { + if err := s.Wait(); err != nil { + b.Fatal(err) + } + } + } +} diff --git a/pkg/random/random.go b/pkg/random/random.go new file mode 100644 index 00000000..70de4d13 --- /dev/null +++ b/pkg/random/random.go @@ -0,0 +1,71 @@ +package random + +import ( + cryptorand "crypto/rand" + "io" + "math" + "math/big" + "math/rand" + "sync" + "time" +) + +// Rand is a global *rand.Rand instance, which initialized with NewSource() source. +var Rand = rand.New(NewSource()) + +// Reader is a global, shared instance of a pseudorandom bytes generator. +// It doesn't consume entropy. +var Reader io.Reader = &reader{rnd: Rand} + +// copypaste from standard math/rand +type lockedSource struct { + lk sync.Mutex + src rand.Source +} + +func (r *lockedSource) Int63() (n int64) { + r.lk.Lock() + n = r.src.Int63() + r.lk.Unlock() + return +} + +func (r *lockedSource) Seed(seed int64) { + r.lk.Lock() + r.src.Seed(seed) + r.lk.Unlock() +} + +// NewSource returns math/rand.Source safe for concurrent use and initialized +// with current unix-nano timestamp +func NewSource() rand.Source { + var seed int64 + if cryptoseed, err := cryptorand.Int(cryptorand.Reader, big.NewInt(math.MaxInt64)); err != nil { + // This should not happen, but worst-case fallback to time-based seed. + seed = time.Now().UnixNano() + } else { + seed = cryptoseed.Int64() + } + return &lockedSource{ + src: rand.NewSource(seed), + } +} + +type reader struct { + rnd *rand.Rand +} + +func (r *reader) Read(b []byte) (int, error) { + i := 0 + for { + val := r.rnd.Int63() + for val > 0 { + b[i] = byte(val) + i++ + if i == len(b) { + return i, nil + } + val >>= 8 + } + } +} diff --git a/pkg/random/random_test.go b/pkg/random/random_test.go new file mode 100644 index 00000000..cf405f78 --- /dev/null +++ b/pkg/random/random_test.go @@ -0,0 +1,22 @@ +package random + +import ( + "math/rand" + "sync" + "testing" +) + +// for go test -v -race +func TestConcurrency(t *testing.T) { + rnd := rand.New(NewSource()) + var wg sync.WaitGroup + + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + rnd.Int63() + wg.Done() + }() + } + wg.Wait() +} diff --git a/pkg/reexec/README.md b/pkg/reexec/README.md new file mode 100644 index 00000000..45592ce8 --- /dev/null +++ b/pkg/reexec/README.md @@ -0,0 +1,5 @@ +## reexec + +The `reexec` package facilitates the busybox style reexec of the docker binary that we require because +of the forking limitations of using Go. Handlers can be registered with a name and the argv 0 of +the exec of the binary will be used to find and execute custom init paths. diff --git a/pkg/reexec/command_freebsd.go b/pkg/reexec/command_freebsd.go new file mode 100644 index 00000000..c7f797a5 --- /dev/null +++ b/pkg/reexec/command_freebsd.go @@ -0,0 +1,23 @@ +// +build freebsd + +package reexec + +import ( + "os/exec" +) + +// Self returns the path to the current process's binary. +// Uses os.Args[0]. +func Self() string { + return naiveSelf() +} + +// Command returns *exec.Cmd which have Path as current binary. +// For example if current binary is "docker" at "/usr/bin/", then cmd.Path will +// be set to "/usr/bin/docker". +func Command(args ...string) *exec.Cmd { + return &exec.Cmd{ + Path: Self(), + Args: args, + } +} diff --git a/pkg/reexec/command_linux.go b/pkg/reexec/command_linux.go new file mode 100644 index 00000000..3c3a73a9 --- /dev/null +++ b/pkg/reexec/command_linux.go @@ -0,0 +1,28 @@ +// +build linux + +package reexec + +import ( + "os/exec" + "syscall" +) + +// Self returns the path to the current process's binary. +// Returns "/proc/self/exe". +func Self() string { + return "/proc/self/exe" +} + +// Command returns *exec.Cmd which have Path as current binary. Also it setting +// SysProcAttr.Pdeathsig to SIGTERM. +// This will use the in-memory version (/proc/self/exe) of the current binary, +// it is thus safe to delete or replace the on-disk binary (os.Args[0]). +func Command(args ...string) *exec.Cmd { + return &exec.Cmd{ + Path: Self(), + Args: args, + SysProcAttr: &syscall.SysProcAttr{ + Pdeathsig: syscall.SIGTERM, + }, + } +} diff --git a/pkg/reexec/command_unsupported.go b/pkg/reexec/command_unsupported.go new file mode 100644 index 00000000..ad4ea38e --- /dev/null +++ b/pkg/reexec/command_unsupported.go @@ -0,0 +1,12 @@ +// +build !linux,!windows,!freebsd + +package reexec + +import ( + "os/exec" +) + +// Command is unsupported on operating systems apart from Linux and Windows. +func Command(args ...string) *exec.Cmd { + return nil +} diff --git a/pkg/reexec/command_windows.go b/pkg/reexec/command_windows.go new file mode 100644 index 00000000..8d65e0ae --- /dev/null +++ b/pkg/reexec/command_windows.go @@ -0,0 +1,23 @@ +// +build windows + +package reexec + +import ( + "os/exec" +) + +// Self returns the path to the current process's binary. +// Uses os.Args[0]. +func Self() string { + return naiveSelf() +} + +// Command returns *exec.Cmd which have Path as current binary. +// For example if current binary is "docker.exe" at "C:\", then cmd.Path will +// be set to "C:\docker.exe". +func Command(args ...string) *exec.Cmd { + return &exec.Cmd{ + Path: Self(), + Args: args, + } +} diff --git a/pkg/reexec/reexec.go b/pkg/reexec/reexec.go new file mode 100644 index 00000000..ceb98d25 --- /dev/null +++ b/pkg/reexec/reexec.go @@ -0,0 +1,47 @@ +package reexec + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" +) + +var registeredInitializers = make(map[string]func()) + +// Register adds an initialization func under the specified name +func Register(name string, initializer func()) { + if _, exists := registeredInitializers[name]; exists { + panic(fmt.Sprintf("reexec func already registred under name %q", name)) + } + + registeredInitializers[name] = initializer +} + +// Init is called as the first part of the exec process and returns true if an +// initialization function was called. +func Init() bool { + initializer, exists := registeredInitializers[os.Args[0]] + if exists { + initializer() + + return true + } + return false +} + +func naiveSelf() string { + name := os.Args[0] + if filepath.Base(name) == name { + if lp, err := exec.LookPath(name); err == nil { + return lp + } + } + // handle conversion of relative paths to absolute + if absName, err := filepath.Abs(name); err == nil { + return absName + } + // if we couldn't get absolute name, return original + // (NOTE: Go only errors on Abs() if os.Getwd fails) + return name +} diff --git a/pkg/registrar/registrar.go b/pkg/registrar/registrar.go new file mode 100644 index 00000000..8910197f --- /dev/null +++ b/pkg/registrar/registrar.go @@ -0,0 +1,127 @@ +// Package registrar provides name registration. It reserves a name to a given key. +package registrar + +import ( + "errors" + "sync" +) + +var ( + // ErrNameReserved is an error which is returned when a name is requested to be reserved that already is reserved + ErrNameReserved = errors.New("name is reserved") + // ErrNameNotReserved is an error which is returned when trying to find a name that is not reserved + ErrNameNotReserved = errors.New("name is not reserved") + // ErrNoSuchKey is returned when trying to find the names for a key which is not known + ErrNoSuchKey = errors.New("provided key does not exist") +) + +// Registrar stores indexes a list of keys and their registered names as well as indexes names and the key that they are registred to +// Names must be unique. +// Registrar is safe for concurrent access. +type Registrar struct { + idx map[string][]string + names map[string]string + mu sync.Mutex +} + +// NewRegistrar creates a new Registrar with the an empty index +func NewRegistrar() *Registrar { + return &Registrar{ + idx: make(map[string][]string), + names: make(map[string]string), + } +} + +// Reserve registers a key to a name +// Reserve is idempotent +// Attempting to reserve a key to a name that already exists results in an `ErrNameReserved` +// A name reservation is globally unique +func (r *Registrar) Reserve(name, key string) error { + r.mu.Lock() + defer r.mu.Unlock() + + if k, exists := r.names[name]; exists { + if k != key { + return ErrNameReserved + } + return nil + } + + r.idx[key] = append(r.idx[key], name) + r.names[name] = key + return nil +} + +// Release releases the reserved name +// Once released, a name can be reserved again +func (r *Registrar) Release(name string) { + r.mu.Lock() + defer r.mu.Unlock() + + key, exists := r.names[name] + if !exists { + return + } + + for i, n := range r.idx[key] { + if n != name { + continue + } + r.idx[key] = append(r.idx[key][:i], r.idx[key][i+1:]...) + break + } + + delete(r.names, name) + + if len(r.idx[key]) == 0 { + delete(r.idx, key) + } +} + +// Delete removes all reservations for the passed in key. +// All names reserved to this key are released. +func (r *Registrar) Delete(key string) { + r.mu.Lock() + for _, name := range r.idx[key] { + delete(r.names, name) + } + delete(r.idx, key) + r.mu.Unlock() +} + +// GetNames lists all the reserved names for the given key +func (r *Registrar) GetNames(key string) ([]string, error) { + r.mu.Lock() + defer r.mu.Unlock() + + names, exists := r.idx[key] + if !exists { + return nil, ErrNoSuchKey + } + return names, nil +} + +// Get returns the key that the passed in name is reserved to +func (r *Registrar) Get(name string) (string, error) { + r.mu.Lock() + key, exists := r.names[name] + r.mu.Unlock() + + if !exists { + return "", ErrNameNotReserved + } + return key, nil +} + +// GetAll returns all registered names +func (r *Registrar) GetAll() map[string][]string { + out := make(map[string][]string) + + r.mu.Lock() + // copy index into out + for id, names := range r.idx { + out[id] = names + } + r.mu.Unlock() + return out +} diff --git a/pkg/registrar/registrar_test.go b/pkg/registrar/registrar_test.go new file mode 100644 index 00000000..0c1ef312 --- /dev/null +++ b/pkg/registrar/registrar_test.go @@ -0,0 +1,119 @@ +package registrar + +import ( + "reflect" + "testing" +) + +func TestReserve(t *testing.T) { + r := NewRegistrar() + + obj := "test1" + if err := r.Reserve("test", obj); err != nil { + t.Fatal(err) + } + + if err := r.Reserve("test", obj); err != nil { + t.Fatal(err) + } + + obj2 := "test2" + err := r.Reserve("test", obj2) + if err == nil { + t.Fatalf("expected error when reserving an already reserved name to another object") + } + if err != ErrNameReserved { + t.Fatal("expected `ErrNameReserved` error when attempting to reserve an already reserved name") + } +} + +func TestRelease(t *testing.T) { + r := NewRegistrar() + obj := "testing" + + if err := r.Reserve("test", obj); err != nil { + t.Fatal(err) + } + r.Release("test") + r.Release("test") // Ensure there is no panic here + + if err := r.Reserve("test", obj); err != nil { + t.Fatal(err) + } +} + +func TestGetNames(t *testing.T) { + r := NewRegistrar() + obj := "testing" + names := []string{"test1", "test2"} + + for _, name := range names { + if err := r.Reserve(name, obj); err != nil { + t.Fatal(err) + } + } + r.Reserve("test3", "other") + + names2, err := r.GetNames(obj) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(names, names2) { + t.Fatalf("Exepected: %v, Got: %v", names, names2) + } +} + +func TestDelete(t *testing.T) { + r := NewRegistrar() + obj := "testing" + names := []string{"test1", "test2"} + for _, name := range names { + if err := r.Reserve(name, obj); err != nil { + t.Fatal(err) + } + } + + r.Reserve("test3", "other") + r.Delete(obj) + + _, err := r.GetNames(obj) + if err == nil { + t.Fatal("expected error getting names for deleted key") + } + + if err != ErrNoSuchKey { + t.Fatal("expected `ErrNoSuchKey`") + } +} + +func TestGet(t *testing.T) { + r := NewRegistrar() + obj := "testing" + name := "test" + + _, err := r.Get(name) + if err == nil { + t.Fatal("expected error when key does not exist") + } + if err != ErrNameNotReserved { + t.Fatal(err) + } + + if err := r.Reserve(name, obj); err != nil { + t.Fatal(err) + } + + if _, err = r.Get(name); err != nil { + t.Fatal(err) + } + + r.Delete(obj) + _, err = r.Get(name) + if err == nil { + t.Fatal("expected error when key does not exist") + } + if err != ErrNameNotReserved { + t.Fatal(err) + } +} diff --git a/pkg/signal/README.md b/pkg/signal/README.md new file mode 100644 index 00000000..2b237a59 --- /dev/null +++ b/pkg/signal/README.md @@ -0,0 +1 @@ +This package provides helper functions for dealing with signals across various operating systems \ No newline at end of file diff --git a/pkg/signal/signal.go b/pkg/signal/signal.go new file mode 100644 index 00000000..68bb77cf --- /dev/null +++ b/pkg/signal/signal.go @@ -0,0 +1,54 @@ +// Package signal provides helper functions for dealing with signals across +// various operating systems. +package signal + +import ( + "fmt" + "os" + "os/signal" + "strconv" + "strings" + "syscall" +) + +// CatchAll catches all signals and relays them to the specified channel. +func CatchAll(sigc chan os.Signal) { + handledSigs := []os.Signal{} + for _, s := range SignalMap { + handledSigs = append(handledSigs, s) + } + signal.Notify(sigc, handledSigs...) +} + +// StopCatch stops catching the signals and closes the specified channel. +func StopCatch(sigc chan os.Signal) { + signal.Stop(sigc) + close(sigc) +} + +// ParseSignal translates a string to a valid syscall signal. +// It returns an error if the signal map doesn't include the given signal. +func ParseSignal(rawSignal string) (syscall.Signal, error) { + s, err := strconv.Atoi(rawSignal) + if err == nil { + if s == 0 { + return -1, fmt.Errorf("Invalid signal: %s", rawSignal) + } + return syscall.Signal(s), nil + } + signal, ok := SignalMap[strings.TrimPrefix(strings.ToUpper(rawSignal), "SIG")] + if !ok { + return -1, fmt.Errorf("Invalid signal: %s", rawSignal) + } + return signal, nil +} + +// ValidSignalForPlatform returns true if a signal is valid on the platform +func ValidSignalForPlatform(sig syscall.Signal) bool { + for _, v := range SignalMap { + if v == sig { + return true + } + } + return false +} diff --git a/pkg/signal/signal_darwin.go b/pkg/signal/signal_darwin.go new file mode 100644 index 00000000..946de87e --- /dev/null +++ b/pkg/signal/signal_darwin.go @@ -0,0 +1,41 @@ +package signal + +import ( + "syscall" +) + +// SignalMap is a map of Darwin signals. +var SignalMap = map[string]syscall.Signal{ + "ABRT": syscall.SIGABRT, + "ALRM": syscall.SIGALRM, + "BUG": syscall.SIGBUS, + "CHLD": syscall.SIGCHLD, + "CONT": syscall.SIGCONT, + "EMT": syscall.SIGEMT, + "FPE": syscall.SIGFPE, + "HUP": syscall.SIGHUP, + "ILL": syscall.SIGILL, + "INFO": syscall.SIGINFO, + "INT": syscall.SIGINT, + "IO": syscall.SIGIO, + "IOT": syscall.SIGIOT, + "KILL": syscall.SIGKILL, + "PIPE": syscall.SIGPIPE, + "PROF": syscall.SIGPROF, + "QUIT": syscall.SIGQUIT, + "SEGV": syscall.SIGSEGV, + "STOP": syscall.SIGSTOP, + "SYS": syscall.SIGSYS, + "TERM": syscall.SIGTERM, + "TRAP": syscall.SIGTRAP, + "TSTP": syscall.SIGTSTP, + "TTIN": syscall.SIGTTIN, + "TTOU": syscall.SIGTTOU, + "URG": syscall.SIGURG, + "USR1": syscall.SIGUSR1, + "USR2": syscall.SIGUSR2, + "VTALRM": syscall.SIGVTALRM, + "WINCH": syscall.SIGWINCH, + "XCPU": syscall.SIGXCPU, + "XFSZ": syscall.SIGXFSZ, +} diff --git a/pkg/signal/signal_freebsd.go b/pkg/signal/signal_freebsd.go new file mode 100644 index 00000000..6b9569bb --- /dev/null +++ b/pkg/signal/signal_freebsd.go @@ -0,0 +1,43 @@ +package signal + +import ( + "syscall" +) + +// SignalMap is a map of FreeBSD signals. +var SignalMap = map[string]syscall.Signal{ + "ABRT": syscall.SIGABRT, + "ALRM": syscall.SIGALRM, + "BUF": syscall.SIGBUS, + "CHLD": syscall.SIGCHLD, + "CONT": syscall.SIGCONT, + "EMT": syscall.SIGEMT, + "FPE": syscall.SIGFPE, + "HUP": syscall.SIGHUP, + "ILL": syscall.SIGILL, + "INFO": syscall.SIGINFO, + "INT": syscall.SIGINT, + "IO": syscall.SIGIO, + "IOT": syscall.SIGIOT, + "KILL": syscall.SIGKILL, + "LWP": syscall.SIGLWP, + "PIPE": syscall.SIGPIPE, + "PROF": syscall.SIGPROF, + "QUIT": syscall.SIGQUIT, + "SEGV": syscall.SIGSEGV, + "STOP": syscall.SIGSTOP, + "SYS": syscall.SIGSYS, + "TERM": syscall.SIGTERM, + "THR": syscall.SIGTHR, + "TRAP": syscall.SIGTRAP, + "TSTP": syscall.SIGTSTP, + "TTIN": syscall.SIGTTIN, + "TTOU": syscall.SIGTTOU, + "URG": syscall.SIGURG, + "USR1": syscall.SIGUSR1, + "USR2": syscall.SIGUSR2, + "VTALRM": syscall.SIGVTALRM, + "WINCH": syscall.SIGWINCH, + "XCPU": syscall.SIGXCPU, + "XFSZ": syscall.SIGXFSZ, +} diff --git a/pkg/signal/signal_linux.go b/pkg/signal/signal_linux.go new file mode 100644 index 00000000..d418cbe9 --- /dev/null +++ b/pkg/signal/signal_linux.go @@ -0,0 +1,80 @@ +package signal + +import ( + "syscall" +) + +const ( + sigrtmin = 34 + sigrtmax = 64 +) + +// SignalMap is a map of Linux signals. +var SignalMap = map[string]syscall.Signal{ + "ABRT": syscall.SIGABRT, + "ALRM": syscall.SIGALRM, + "BUS": syscall.SIGBUS, + "CHLD": syscall.SIGCHLD, + "CLD": syscall.SIGCLD, + "CONT": syscall.SIGCONT, + "FPE": syscall.SIGFPE, + "HUP": syscall.SIGHUP, + "ILL": syscall.SIGILL, + "INT": syscall.SIGINT, + "IO": syscall.SIGIO, + "IOT": syscall.SIGIOT, + "KILL": syscall.SIGKILL, + "PIPE": syscall.SIGPIPE, + "POLL": syscall.SIGPOLL, + "PROF": syscall.SIGPROF, + "PWR": syscall.SIGPWR, + "QUIT": syscall.SIGQUIT, + "SEGV": syscall.SIGSEGV, + "STKFLT": syscall.SIGSTKFLT, + "STOP": syscall.SIGSTOP, + "SYS": syscall.SIGSYS, + "TERM": syscall.SIGTERM, + "TRAP": syscall.SIGTRAP, + "TSTP": syscall.SIGTSTP, + "TTIN": syscall.SIGTTIN, + "TTOU": syscall.SIGTTOU, + "UNUSED": syscall.SIGUNUSED, + "URG": syscall.SIGURG, + "USR1": syscall.SIGUSR1, + "USR2": syscall.SIGUSR2, + "VTALRM": syscall.SIGVTALRM, + "WINCH": syscall.SIGWINCH, + "XCPU": syscall.SIGXCPU, + "XFSZ": syscall.SIGXFSZ, + "RTMIN": sigrtmin, + "RTMIN+1": sigrtmin + 1, + "RTMIN+2": sigrtmin + 2, + "RTMIN+3": sigrtmin + 3, + "RTMIN+4": sigrtmin + 4, + "RTMIN+5": sigrtmin + 5, + "RTMIN+6": sigrtmin + 6, + "RTMIN+7": sigrtmin + 7, + "RTMIN+8": sigrtmin + 8, + "RTMIN+9": sigrtmin + 9, + "RTMIN+10": sigrtmin + 10, + "RTMIN+11": sigrtmin + 11, + "RTMIN+12": sigrtmin + 12, + "RTMIN+13": sigrtmin + 13, + "RTMIN+14": sigrtmin + 14, + "RTMIN+15": sigrtmin + 15, + "RTMAX-14": sigrtmax - 14, + "RTMAX-13": sigrtmax - 13, + "RTMAX-12": sigrtmax - 12, + "RTMAX-11": sigrtmax - 11, + "RTMAX-10": sigrtmax - 10, + "RTMAX-9": sigrtmax - 9, + "RTMAX-8": sigrtmax - 8, + "RTMAX-7": sigrtmax - 7, + "RTMAX-6": sigrtmax - 6, + "RTMAX-5": sigrtmax - 5, + "RTMAX-4": sigrtmax - 4, + "RTMAX-3": sigrtmax - 3, + "RTMAX-2": sigrtmax - 2, + "RTMAX-1": sigrtmax - 1, + "RTMAX": sigrtmax, +} diff --git a/pkg/signal/signal_unix.go b/pkg/signal/signal_unix.go new file mode 100644 index 00000000..6621d371 --- /dev/null +++ b/pkg/signal/signal_unix.go @@ -0,0 +1,21 @@ +// +build !windows + +package signal + +import ( + "syscall" +) + +// Signals used in api/client (no windows equivalent, use +// invalid signals so they don't get handled) + +const ( + // SIGCHLD is a signal sent to a process when a child process terminates, is interrupted, or resumes after being interrupted. + SIGCHLD = syscall.SIGCHLD + // SIGWINCH is a signal sent to a process when its controlling terminal changes its size + SIGWINCH = syscall.SIGWINCH + // SIGPIPE is a signal sent to a process when a pipe is written to before the other end is open for reading + SIGPIPE = syscall.SIGPIPE + // DefaultStopSignal is the syscall signal used to stop a container in unix systems. + DefaultStopSignal = "SIGTERM" +) diff --git a/pkg/signal/signal_unsupported.go b/pkg/signal/signal_unsupported.go new file mode 100644 index 00000000..161ba273 --- /dev/null +++ b/pkg/signal/signal_unsupported.go @@ -0,0 +1,10 @@ +// +build !linux,!darwin,!freebsd,!windows + +package signal + +import ( + "syscall" +) + +// SignalMap is an empty map of signals for unsupported platform. +var SignalMap = map[string]syscall.Signal{} diff --git a/pkg/signal/signal_windows.go b/pkg/signal/signal_windows.go new file mode 100644 index 00000000..698cbf2d --- /dev/null +++ b/pkg/signal/signal_windows.go @@ -0,0 +1,28 @@ +// +build windows + +package signal + +import ( + "syscall" +) + +// Signals used in api/client (no windows equivalent, use +// invalid signals so they don't get handled) +const ( + SIGCHLD = syscall.Signal(0xff) + SIGWINCH = syscall.Signal(0xff) + SIGPIPE = syscall.Signal(0xff) + // DefaultStopSignal is the syscall signal used to stop a container in windows systems. + DefaultStopSignal = "15" +) + +// SignalMap is a map of "supported" signals. As per the comment in GOLang's +// ztypes_windows.go: "More invented values for signals". Windows doesn't +// really support signals in any way, shape or form that Unix does. +// +// We have these so that docker kill can be used to gracefully (TERM) and +// forcibly (KILL) terminate a container on Windows. +var SignalMap = map[string]syscall.Signal{ + "KILL": syscall.SIGKILL, + "TERM": syscall.SIGTERM, +} diff --git a/pkg/signal/trap.go b/pkg/signal/trap.go new file mode 100644 index 00000000..2cf5ccf0 --- /dev/null +++ b/pkg/signal/trap.go @@ -0,0 +1,74 @@ +package signal + +import ( + "os" + gosignal "os/signal" + "runtime" + "sync/atomic" + "syscall" + + "github.com/Sirupsen/logrus" +) + +// Trap sets up a simplified signal "trap", appropriate for common +// behavior expected from a vanilla unix command-line tool in general +// (and the Docker engine in particular). +// +// * If SIGINT or SIGTERM are received, `cleanup` is called, then the process is terminated. +// * If SIGINT or SIGTERM are received 3 times before cleanup is complete, then cleanup is +// skipped and the process is terminated immediately (allows force quit of stuck daemon) +// * A SIGQUIT always causes an exit without cleanup, with a goroutine dump preceding exit. +// +func Trap(cleanup func()) { + c := make(chan os.Signal, 1) + // we will handle INT, TERM, QUIT here + signals := []os.Signal{os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT} + gosignal.Notify(c, signals...) + go func() { + interruptCount := uint32(0) + for sig := range c { + go func(sig os.Signal) { + logrus.Infof("Processing signal '%v'", sig) + switch sig { + case os.Interrupt, syscall.SIGTERM: + if atomic.LoadUint32(&interruptCount) < 3 { + // Initiate the cleanup only once + if atomic.AddUint32(&interruptCount, 1) == 1 { + // Call the provided cleanup handler + cleanup() + os.Exit(0) + } else { + return + } + } else { + // 3 SIGTERM/INT signals received; force exit without cleanup + logrus.Infof("Forcing docker daemon shutdown without cleanup; 3 interrupts received") + } + case syscall.SIGQUIT: + DumpStacks() + logrus.Infof("Forcing docker daemon shutdown without cleanup on SIGQUIT") + } + //for the SIGINT/TERM, and SIGQUIT non-clean shutdown case, exit with 128 + signal # + os.Exit(128 + int(sig.(syscall.Signal))) + }(sig) + } + }() +} + +// DumpStacks dumps the runtime stack. +func DumpStacks() { + var ( + buf []byte + stackSize int + ) + bufferLen := 16384 + for stackSize == len(buf) { + buf = make([]byte, bufferLen) + stackSize = runtime.Stack(buf, true) + bufferLen *= 2 + } + buf = buf[:stackSize] + // Note that if the daemon is started with a less-verbose log-level than "info" (the default), the goroutine + // traces won't show up in the log. + logrus.Infof("=== BEGIN goroutine stack dump ===\n%s\n=== END goroutine stack dump ===", buf) +} diff --git a/pkg/stdcopy/stdcopy.go b/pkg/stdcopy/stdcopy.go new file mode 100644 index 00000000..b37ae39f --- /dev/null +++ b/pkg/stdcopy/stdcopy.go @@ -0,0 +1,178 @@ +package stdcopy + +import ( + "encoding/binary" + "errors" + "fmt" + "io" + + "github.com/Sirupsen/logrus" +) + +// StdType is the type of standard stream +// a writer can multiplex to. +type StdType byte + +const ( + // Stdin represents standard input stream type. + Stdin StdType = iota + // Stdout represents standard output stream type. + Stdout + // Stderr represents standard error steam type. + Stderr + + stdWriterPrefixLen = 8 + stdWriterFdIndex = 0 + stdWriterSizeIndex = 4 + + startingBufLen = 32*1024 + stdWriterPrefixLen + 1 +) + +// stdWriter is wrapper of io.Writer with extra customized info. +type stdWriter struct { + io.Writer + prefix byte +} + +// Write sends the buffer to the underneath writer. +// It insert the prefix header before the buffer, +// so stdcopy.StdCopy knows where to multiplex the output. +// It makes stdWriter to implement io.Writer. +func (w *stdWriter) Write(buf []byte) (n int, err error) { + if w == nil || w.Writer == nil { + return 0, errors.New("Writer not instantiated") + } + if buf == nil { + return 0, nil + } + + header := [stdWriterPrefixLen]byte{stdWriterFdIndex: w.prefix} + binary.BigEndian.PutUint32(header[stdWriterSizeIndex:], uint32(len(buf))) + + line := append(header[:], buf...) + + n, err = w.Writer.Write(line) + n -= stdWriterPrefixLen + + if n < 0 { + n = 0 + } + return +} + +// NewStdWriter instantiates a new Writer. +// Everything written to it will be encapsulated using a custom format, +// and written to the underlying `w` stream. +// This allows multiple write streams (e.g. stdout and stderr) to be muxed into a single connection. +// `t` indicates the id of the stream to encapsulate. +// It can be stdcopy.Stdin, stdcopy.Stdout, stdcopy.Stderr. +func NewStdWriter(w io.Writer, t StdType) io.Writer { + return &stdWriter{ + Writer: w, + prefix: byte(t), + } +} + +// StdCopy is a modified version of io.Copy. +// +// StdCopy will demultiplex `src`, assuming that it contains two streams, +// previously multiplexed together using a StdWriter instance. +// As it reads from `src`, StdCopy will write to `dstout` and `dsterr`. +// +// StdCopy will read until it hits EOF on `src`. It will then return a nil error. +// In other words: if `err` is non nil, it indicates a real underlying error. +// +// `written` will hold the total number of bytes written to `dstout` and `dsterr`. +func StdCopy(dstout, dsterr io.Writer, src io.Reader) (written int64, err error) { + var ( + buf = make([]byte, startingBufLen) + bufLen = len(buf) + nr, nw int + er, ew error + out io.Writer + frameSize int + ) + + for { + // Make sure we have at least a full header + for nr < stdWriterPrefixLen { + var nr2 int + nr2, er = src.Read(buf[nr:]) + nr += nr2 + if er == io.EOF { + if nr < stdWriterPrefixLen { + logrus.Debugf("Corrupted prefix: %v", buf[:nr]) + return written, nil + } + break + } + if er != nil { + logrus.Debugf("Error reading header: %s", er) + return 0, er + } + } + + // Check the first byte to know where to write + switch StdType(buf[stdWriterFdIndex]) { + case Stdin: + fallthrough + case Stdout: + // Write on stdout + out = dstout + case Stderr: + // Write on stderr + out = dsterr + default: + logrus.Debugf("Error selecting output fd: (%d)", buf[stdWriterFdIndex]) + return 0, fmt.Errorf("Unrecognized input header: %d", buf[stdWriterFdIndex]) + } + + // Retrieve the size of the frame + frameSize = int(binary.BigEndian.Uint32(buf[stdWriterSizeIndex : stdWriterSizeIndex+4])) + logrus.Debugf("framesize: %d", frameSize) + + // Check if the buffer is big enough to read the frame. + // Extend it if necessary. + if frameSize+stdWriterPrefixLen > bufLen { + logrus.Debugf("Extending buffer cap by %d (was %d)", frameSize+stdWriterPrefixLen-bufLen+1, len(buf)) + buf = append(buf, make([]byte, frameSize+stdWriterPrefixLen-bufLen+1)...) + bufLen = len(buf) + } + + // While the amount of bytes read is less than the size of the frame + header, we keep reading + for nr < frameSize+stdWriterPrefixLen { + var nr2 int + nr2, er = src.Read(buf[nr:]) + nr += nr2 + if er == io.EOF { + if nr < frameSize+stdWriterPrefixLen { + logrus.Debugf("Corrupted frame: %v", buf[stdWriterPrefixLen:nr]) + return written, nil + } + break + } + if er != nil { + logrus.Debugf("Error reading frame: %s", er) + return 0, er + } + } + + // Write the retrieved frame (without header) + nw, ew = out.Write(buf[stdWriterPrefixLen : frameSize+stdWriterPrefixLen]) + if ew != nil { + logrus.Debugf("Error writing frame: %s", ew) + return 0, ew + } + // If the frame has not been fully written: error + if nw != frameSize { + logrus.Debugf("Error Short Write: (%d on %d)", nw, frameSize) + return 0, io.ErrShortWrite + } + written += int64(nw) + + // Move the rest of the buffer to the beginning + copy(buf, buf[frameSize+stdWriterPrefixLen:]) + // Move the index + nr -= frameSize + stdWriterPrefixLen + } +} diff --git a/pkg/stdcopy/stdcopy_test.go b/pkg/stdcopy/stdcopy_test.go new file mode 100644 index 00000000..3137a752 --- /dev/null +++ b/pkg/stdcopy/stdcopy_test.go @@ -0,0 +1,260 @@ +package stdcopy + +import ( + "bytes" + "errors" + "io" + "io/ioutil" + "strings" + "testing" +) + +func TestNewStdWriter(t *testing.T) { + writer := NewStdWriter(ioutil.Discard, Stdout) + if writer == nil { + t.Fatalf("NewStdWriter with an invalid StdType should not return nil.") + } +} + +func TestWriteWithUnitializedStdWriter(t *testing.T) { + writer := stdWriter{ + Writer: nil, + prefix: byte(Stdout), + } + n, err := writer.Write([]byte("Something here")) + if n != 0 || err == nil { + t.Fatalf("Should fail when given an uncomplete or uninitialized StdWriter") + } +} + +func TestWriteWithNilBytes(t *testing.T) { + writer := NewStdWriter(ioutil.Discard, Stdout) + n, err := writer.Write(nil) + if err != nil { + t.Fatalf("Shouldn't have fail when given no data") + } + if n > 0 { + t.Fatalf("Write should have written 0 byte, but has written %d", n) + } +} + +func TestWrite(t *testing.T) { + writer := NewStdWriter(ioutil.Discard, Stdout) + data := []byte("Test StdWrite.Write") + n, err := writer.Write(data) + if err != nil { + t.Fatalf("Error while writing with StdWrite") + } + if n != len(data) { + t.Fatalf("Write should have written %d byte but wrote %d.", len(data), n) + } +} + +type errWriter struct { + n int + err error +} + +func (f *errWriter) Write(buf []byte) (int, error) { + return f.n, f.err +} + +func TestWriteWithWriterError(t *testing.T) { + expectedError := errors.New("expected") + expectedReturnedBytes := 10 + writer := NewStdWriter(&errWriter{ + n: stdWriterPrefixLen + expectedReturnedBytes, + err: expectedError}, Stdout) + data := []byte("This won't get written, sigh") + n, err := writer.Write(data) + if err != expectedError { + t.Fatalf("Didn't get expected error.") + } + if n != expectedReturnedBytes { + t.Fatalf("Didn't get expected written bytes %d, got %d.", + expectedReturnedBytes, n) + } +} + +func TestWriteDoesNotReturnNegativeWrittenBytes(t *testing.T) { + writer := NewStdWriter(&errWriter{n: -1}, Stdout) + data := []byte("This won't get written, sigh") + actual, _ := writer.Write(data) + if actual != 0 { + t.Fatalf("Expected returned written bytes equal to 0, got %d", actual) + } +} + +func getSrcBuffer(stdOutBytes, stdErrBytes []byte) (buffer *bytes.Buffer, err error) { + buffer = new(bytes.Buffer) + dstOut := NewStdWriter(buffer, Stdout) + _, err = dstOut.Write(stdOutBytes) + if err != nil { + return + } + dstErr := NewStdWriter(buffer, Stderr) + _, err = dstErr.Write(stdErrBytes) + return +} + +func TestStdCopyWriteAndRead(t *testing.T) { + stdOutBytes := []byte(strings.Repeat("o", startingBufLen)) + stdErrBytes := []byte(strings.Repeat("e", startingBufLen)) + buffer, err := getSrcBuffer(stdOutBytes, stdErrBytes) + if err != nil { + t.Fatal(err) + } + written, err := StdCopy(ioutil.Discard, ioutil.Discard, buffer) + if err != nil { + t.Fatal(err) + } + expectedTotalWritten := len(stdOutBytes) + len(stdErrBytes) + if written != int64(expectedTotalWritten) { + t.Fatalf("Expected to have total of %d bytes written, got %d", expectedTotalWritten, written) + } +} + +type customReader struct { + n int + err error + totalCalls int + correctCalls int + src *bytes.Buffer +} + +func (f *customReader) Read(buf []byte) (int, error) { + f.totalCalls++ + if f.totalCalls <= f.correctCalls { + return f.src.Read(buf) + } + return f.n, f.err +} + +func TestStdCopyReturnsErrorReadingHeader(t *testing.T) { + expectedError := errors.New("error") + reader := &customReader{ + err: expectedError} + written, err := StdCopy(ioutil.Discard, ioutil.Discard, reader) + if written != 0 { + t.Fatalf("Expected 0 bytes read, got %d", written) + } + if err != expectedError { + t.Fatalf("Didn't get expected error") + } +} + +func TestStdCopyReturnsErrorReadingFrame(t *testing.T) { + expectedError := errors.New("error") + stdOutBytes := []byte(strings.Repeat("o", startingBufLen)) + stdErrBytes := []byte(strings.Repeat("e", startingBufLen)) + buffer, err := getSrcBuffer(stdOutBytes, stdErrBytes) + if err != nil { + t.Fatal(err) + } + reader := &customReader{ + correctCalls: 1, + n: stdWriterPrefixLen + 1, + err: expectedError, + src: buffer} + written, err := StdCopy(ioutil.Discard, ioutil.Discard, reader) + if written != 0 { + t.Fatalf("Expected 0 bytes read, got %d", written) + } + if err != expectedError { + t.Fatalf("Didn't get expected error") + } +} + +func TestStdCopyDetectsCorruptedFrame(t *testing.T) { + stdOutBytes := []byte(strings.Repeat("o", startingBufLen)) + stdErrBytes := []byte(strings.Repeat("e", startingBufLen)) + buffer, err := getSrcBuffer(stdOutBytes, stdErrBytes) + if err != nil { + t.Fatal(err) + } + reader := &customReader{ + correctCalls: 1, + n: stdWriterPrefixLen + 1, + err: io.EOF, + src: buffer} + written, err := StdCopy(ioutil.Discard, ioutil.Discard, reader) + if written != startingBufLen { + t.Fatalf("Expected %d bytes read, got %d", startingBufLen, written) + } + if err != nil { + t.Fatal("Didn't get nil error") + } +} + +func TestStdCopyWithInvalidInputHeader(t *testing.T) { + dstOut := NewStdWriter(ioutil.Discard, Stdout) + dstErr := NewStdWriter(ioutil.Discard, Stderr) + src := strings.NewReader("Invalid input") + _, err := StdCopy(dstOut, dstErr, src) + if err == nil { + t.Fatal("StdCopy with invalid input header should fail.") + } +} + +func TestStdCopyWithCorruptedPrefix(t *testing.T) { + data := []byte{0x01, 0x02, 0x03} + src := bytes.NewReader(data) + written, err := StdCopy(nil, nil, src) + if err != nil { + t.Fatalf("StdCopy should not return an error with corrupted prefix.") + } + if written != 0 { + t.Fatalf("StdCopy should have written 0, but has written %d", written) + } +} + +func TestStdCopyReturnsWriteErrors(t *testing.T) { + stdOutBytes := []byte(strings.Repeat("o", startingBufLen)) + stdErrBytes := []byte(strings.Repeat("e", startingBufLen)) + buffer, err := getSrcBuffer(stdOutBytes, stdErrBytes) + if err != nil { + t.Fatal(err) + } + expectedError := errors.New("expected") + + dstOut := &errWriter{err: expectedError} + + written, err := StdCopy(dstOut, ioutil.Discard, buffer) + if written != 0 { + t.Fatalf("StdCopy should have written 0, but has written %d", written) + } + if err != expectedError { + t.Fatalf("Didn't get expected error, got %v", err) + } +} + +func TestStdCopyDetectsNotFullyWrittenFrames(t *testing.T) { + stdOutBytes := []byte(strings.Repeat("o", startingBufLen)) + stdErrBytes := []byte(strings.Repeat("e", startingBufLen)) + buffer, err := getSrcBuffer(stdOutBytes, stdErrBytes) + if err != nil { + t.Fatal(err) + } + dstOut := &errWriter{n: startingBufLen - 10} + + written, err := StdCopy(dstOut, ioutil.Discard, buffer) + if written != 0 { + t.Fatalf("StdCopy should have return 0 written bytes, but returned %d", written) + } + if err != io.ErrShortWrite { + t.Fatalf("Didn't get expected io.ErrShortWrite error") + } +} + +func BenchmarkWrite(b *testing.B) { + w := NewStdWriter(ioutil.Discard, Stdout) + data := []byte("Test line for testing stdwriter performance\n") + data = bytes.Repeat(data, 100) + b.SetBytes(int64(len(data))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, err := w.Write(data); err != nil { + b.Fatal(err) + } + } +} diff --git a/pkg/streamformatter/streamformatter.go b/pkg/streamformatter/streamformatter.go new file mode 100644 index 00000000..ce6ea79d --- /dev/null +++ b/pkg/streamformatter/streamformatter.go @@ -0,0 +1,172 @@ +// Package streamformatter provides helper functions to format a stream. +package streamformatter + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/pkg/progress" +) + +// StreamFormatter formats a stream, optionally using JSON. +type StreamFormatter struct { + json bool +} + +// NewStreamFormatter returns a simple StreamFormatter +func NewStreamFormatter() *StreamFormatter { + return &StreamFormatter{} +} + +// NewJSONStreamFormatter returns a StreamFormatter configured to stream json +func NewJSONStreamFormatter() *StreamFormatter { + return &StreamFormatter{true} +} + +const streamNewline = "\r\n" + +var streamNewlineBytes = []byte(streamNewline) + +// FormatStream formats the specified stream. +func (sf *StreamFormatter) FormatStream(str string) []byte { + if sf.json { + b, err := json.Marshal(&jsonmessage.JSONMessage{Stream: str}) + if err != nil { + return sf.FormatError(err) + } + return append(b, streamNewlineBytes...) + } + return []byte(str + "\r") +} + +// FormatStatus formats the specified objects according to the specified format (and id). +func (sf *StreamFormatter) FormatStatus(id, format string, a ...interface{}) []byte { + str := fmt.Sprintf(format, a...) + if sf.json { + b, err := json.Marshal(&jsonmessage.JSONMessage{ID: id, Status: str}) + if err != nil { + return sf.FormatError(err) + } + return append(b, streamNewlineBytes...) + } + return []byte(str + streamNewline) +} + +// FormatError formats the specified error. +func (sf *StreamFormatter) FormatError(err error) []byte { + if sf.json { + jsonError, ok := err.(*jsonmessage.JSONError) + if !ok { + jsonError = &jsonmessage.JSONError{Message: err.Error()} + } + if b, err := json.Marshal(&jsonmessage.JSONMessage{Error: jsonError, ErrorMessage: err.Error()}); err == nil { + return append(b, streamNewlineBytes...) + } + return []byte("{\"error\":\"format error\"}" + streamNewline) + } + return []byte("Error: " + err.Error() + streamNewline) +} + +// FormatProgress formats the progress information for a specified action. +func (sf *StreamFormatter) FormatProgress(id, action string, progress *jsonmessage.JSONProgress, aux interface{}) []byte { + if progress == nil { + progress = &jsonmessage.JSONProgress{} + } + if sf.json { + var auxJSON *json.RawMessage + if aux != nil { + auxJSONBytes, err := json.Marshal(aux) + if err != nil { + return nil + } + auxJSON = new(json.RawMessage) + *auxJSON = auxJSONBytes + } + b, err := json.Marshal(&jsonmessage.JSONMessage{ + Status: action, + ProgressMessage: progress.String(), + Progress: progress, + ID: id, + Aux: auxJSON, + }) + if err != nil { + return nil + } + return append(b, streamNewlineBytes...) + } + endl := "\r" + if progress.String() == "" { + endl += "\n" + } + return []byte(action + " " + progress.String() + endl) +} + +// NewProgressOutput returns a progress.Output object that can be passed to +// progress.NewProgressReader. +func (sf *StreamFormatter) NewProgressOutput(out io.Writer, newLines bool) progress.Output { + return &progressOutput{ + sf: sf, + out: out, + newLines: newLines, + } +} + +type progressOutput struct { + sf *StreamFormatter + out io.Writer + newLines bool +} + +// WriteProgress formats progress information from a ProgressReader. +func (out *progressOutput) WriteProgress(prog progress.Progress) error { + var formatted []byte + if prog.Message != "" { + formatted = out.sf.FormatStatus(prog.ID, prog.Message) + } else { + jsonProgress := jsonmessage.JSONProgress{Current: prog.Current, Total: prog.Total} + formatted = out.sf.FormatProgress(prog.ID, prog.Action, &jsonProgress, prog.Aux) + } + _, err := out.out.Write(formatted) + if err != nil { + return err + } + + if out.newLines && prog.LastUpdate { + _, err = out.out.Write(out.sf.FormatStatus("", "")) + return err + } + + return nil +} + +// StdoutFormatter is a streamFormatter that writes to the standard output. +type StdoutFormatter struct { + io.Writer + *StreamFormatter +} + +func (sf *StdoutFormatter) Write(buf []byte) (int, error) { + formattedBuf := sf.StreamFormatter.FormatStream(string(buf)) + n, err := sf.Writer.Write(formattedBuf) + if n != len(formattedBuf) { + return n, io.ErrShortWrite + } + return len(buf), err +} + +// StderrFormatter is a streamFormatter that writes to the standard error. +type StderrFormatter struct { + io.Writer + *StreamFormatter +} + +func (sf *StderrFormatter) Write(buf []byte) (int, error) { + formattedBuf := sf.StreamFormatter.FormatStream("\033[91m" + string(buf) + "\033[0m") + n, err := sf.Writer.Write(formattedBuf) + if n != len(formattedBuf) { + return n, io.ErrShortWrite + } + return len(buf), err +} diff --git a/pkg/streamformatter/streamformatter_test.go b/pkg/streamformatter/streamformatter_test.go new file mode 100644 index 00000000..833db626 --- /dev/null +++ b/pkg/streamformatter/streamformatter_test.go @@ -0,0 +1,104 @@ +package streamformatter + +import ( + "encoding/json" + "errors" + "reflect" + "strings" + "testing" + + "github.com/docker/docker/pkg/jsonmessage" +) + +func TestFormatStream(t *testing.T) { + sf := NewStreamFormatter() + res := sf.FormatStream("stream") + if string(res) != "stream"+"\r" { + t.Fatalf("%q", res) + } +} + +func TestFormatJSONStatus(t *testing.T) { + sf := NewStreamFormatter() + res := sf.FormatStatus("ID", "%s%d", "a", 1) + if string(res) != "a1\r\n" { + t.Fatalf("%q", res) + } +} + +func TestFormatSimpleError(t *testing.T) { + sf := NewStreamFormatter() + res := sf.FormatError(errors.New("Error for formatter")) + if string(res) != "Error: Error for formatter\r\n" { + t.Fatalf("%q", res) + } +} + +func TestJSONFormatStream(t *testing.T) { + sf := NewJSONStreamFormatter() + res := sf.FormatStream("stream") + if string(res) != `{"stream":"stream"}`+"\r\n" { + t.Fatalf("%q", res) + } +} + +func TestJSONFormatStatus(t *testing.T) { + sf := NewJSONStreamFormatter() + res := sf.FormatStatus("ID", "%s%d", "a", 1) + if string(res) != `{"status":"a1","id":"ID"}`+"\r\n" { + t.Fatalf("%q", res) + } +} + +func TestJSONFormatSimpleError(t *testing.T) { + sf := NewJSONStreamFormatter() + res := sf.FormatError(errors.New("Error for formatter")) + if string(res) != `{"errorDetail":{"message":"Error for formatter"},"error":"Error for formatter"}`+"\r\n" { + t.Fatalf("%q", res) + } +} + +func TestJSONFormatJSONError(t *testing.T) { + sf := NewJSONStreamFormatter() + err := &jsonmessage.JSONError{Code: 50, Message: "Json error"} + res := sf.FormatError(err) + if string(res) != `{"errorDetail":{"code":50,"message":"Json error"},"error":"Json error"}`+"\r\n" { + t.Fatalf("%q", res) + } +} + +func TestJSONFormatProgress(t *testing.T) { + sf := NewJSONStreamFormatter() + progress := &jsonmessage.JSONProgress{ + Current: 15, + Total: 30, + Start: 1, + } + res := sf.FormatProgress("id", "action", progress, nil) + msg := &jsonmessage.JSONMessage{} + if err := json.Unmarshal(res, msg); err != nil { + t.Fatal(err) + } + if msg.ID != "id" { + t.Fatalf("ID must be 'id', got: %s", msg.ID) + } + if msg.Status != "action" { + t.Fatalf("Status must be 'action', got: %s", msg.Status) + } + + // The progress will always be in the format of: + // [=========================> ] 15 B/30 B 404933h7m11s + // The last entry '404933h7m11s' is the timeLeftBox. + // However, the timeLeftBox field may change as progress.String() depends on time.Now(). + // Therefore, we have to strip the timeLeftBox from the strings to do the comparison. + + // Compare the progress strings before the timeLeftBox + expectedProgress := "[=========================> ] 15 B/30 B" + if !strings.HasPrefix(msg.ProgressMessage, expectedProgress) { + t.Fatalf("ProgressMessage without the timeLeftBox must be %s, got: %s", expectedProgress, msg.ProgressMessage) + } + + if !reflect.DeepEqual(msg.Progress, progress) { + t.Fatal("Original progress not equals progress from FormatProgress") + } +} diff --git a/pkg/stringid/README.md b/pkg/stringid/README.md new file mode 100644 index 00000000..37a5098f --- /dev/null +++ b/pkg/stringid/README.md @@ -0,0 +1 @@ +This package provides helper functions for dealing with string identifiers diff --git a/pkg/stringid/stringid.go b/pkg/stringid/stringid.go new file mode 100644 index 00000000..02d2594e --- /dev/null +++ b/pkg/stringid/stringid.go @@ -0,0 +1,71 @@ +// Package stringid provides helper functions for dealing with string identifiers +package stringid + +import ( + "crypto/rand" + "encoding/hex" + "io" + "regexp" + "strconv" + "strings" + + "github.com/docker/docker/pkg/random" +) + +const shortLen = 12 + +var validShortID = regexp.MustCompile("^[a-z0-9]{12}$") + +// IsShortID determines if an arbitrary string *looks like* a short ID. +func IsShortID(id string) bool { + return validShortID.MatchString(id) +} + +// TruncateID returns a shorthand version of a string identifier for convenience. +// A collision with other shorthands is very unlikely, but possible. +// In case of a collision a lookup with TruncIndex.Get() will fail, and the caller +// will need to use a langer prefix, or the full-length Id. +func TruncateID(id string) string { + if i := strings.IndexRune(id, ':'); i >= 0 { + id = id[i+1:] + } + trimTo := shortLen + if len(id) < shortLen { + trimTo = len(id) + } + return id[:trimTo] +} + +func generateID(crypto bool) string { + b := make([]byte, 32) + r := random.Reader + if crypto { + r = rand.Reader + } + for { + if _, err := io.ReadFull(r, b); err != nil { + panic(err) // This shouldn't happen + } + id := hex.EncodeToString(b) + // if we try to parse the truncated for as an int and we don't have + // an error then the value is all numeric and causes issues when + // used as a hostname. ref #3869 + if _, err := strconv.ParseInt(TruncateID(id), 10, 64); err == nil { + continue + } + return id + } +} + +// GenerateRandomID returns an unique id. +func GenerateRandomID() string { + return generateID(true) + +} + +// GenerateNonCryptoID generates unique id without using cryptographically +// secure sources of random. +// It helps you to save entropy. +func GenerateNonCryptoID() string { + return generateID(false) +} diff --git a/pkg/stringid/stringid_test.go b/pkg/stringid/stringid_test.go new file mode 100644 index 00000000..bcb13654 --- /dev/null +++ b/pkg/stringid/stringid_test.go @@ -0,0 +1,56 @@ +package stringid + +import ( + "strings" + "testing" +) + +func TestGenerateRandomID(t *testing.T) { + id := GenerateRandomID() + + if len(id) != 64 { + t.Fatalf("Id returned is incorrect: %s", id) + } +} + +func TestShortenId(t *testing.T) { + id := GenerateRandomID() + truncID := TruncateID(id) + if len(truncID) != 12 { + t.Fatalf("Id returned is incorrect: truncate on %s returned %s", id, truncID) + } +} + +func TestShortenIdEmpty(t *testing.T) { + id := "" + truncID := TruncateID(id) + if len(truncID) > len(id) { + t.Fatalf("Id returned is incorrect: truncate on %s returned %s", id, truncID) + } +} + +func TestShortenIdInvalid(t *testing.T) { + id := "1234" + truncID := TruncateID(id) + if len(truncID) != len(id) { + t.Fatalf("Id returned is incorrect: truncate on %s returned %s", id, truncID) + } +} + +func TestIsShortIDNonHex(t *testing.T) { + id := "some non-hex value" + if IsShortID(id) { + t.Fatalf("%s is not a short ID", id) + } +} + +func TestIsShortIDNotCorrectSize(t *testing.T) { + id := strings.Repeat("a", shortLen+1) + if IsShortID(id) { + t.Fatalf("%s is not a short ID", id) + } + id = strings.Repeat("a", shortLen-1) + if IsShortID(id) { + t.Fatalf("%s is not a short ID", id) + } +} diff --git a/pkg/stringutils/README.md b/pkg/stringutils/README.md new file mode 100644 index 00000000..b3e45457 --- /dev/null +++ b/pkg/stringutils/README.md @@ -0,0 +1 @@ +This package provides helper functions for dealing with strings diff --git a/pkg/stringutils/stringutils.go b/pkg/stringutils/stringutils.go new file mode 100644 index 00000000..41a0d2eb --- /dev/null +++ b/pkg/stringutils/stringutils.go @@ -0,0 +1,87 @@ +// Package stringutils provides helper functions for dealing with strings. +package stringutils + +import ( + "bytes" + "math/rand" + "strings" + + "github.com/docker/docker/pkg/random" +) + +// GenerateRandomAlphaOnlyString generates an alphabetical random string with length n. +func GenerateRandomAlphaOnlyString(n int) string { + // make a really long string + letters := []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + b := make([]byte, n) + for i := range b { + b[i] = letters[random.Rand.Intn(len(letters))] + } + return string(b) +} + +// GenerateRandomASCIIString generates an ASCII random stirng with length n. +func GenerateRandomASCIIString(n int) string { + chars := "abcdefghijklmnopqrstuvwxyz" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "~!@#$%^&*()-_+={}[]\\|<,>.?/\"';:` " + res := make([]byte, n) + for i := 0; i < n; i++ { + res[i] = chars[rand.Intn(len(chars))] + } + return string(res) +} + +// Truncate truncates a string to maxlen. +func Truncate(s string, maxlen int) string { + if len(s) <= maxlen { + return s + } + return s[:maxlen] +} + +// InSlice tests whether a string is contained in a slice of strings or not. +// Comparison is case insensitive +func InSlice(slice []string, s string) bool { + for _, ss := range slice { + if strings.ToLower(s) == strings.ToLower(ss) { + return true + } + } + return false +} + +func quote(word string, buf *bytes.Buffer) { + // Bail out early for "simple" strings + if word != "" && !strings.ContainsAny(word, "\\'\"`${[|&;<>()~*?! \t\n") { + buf.WriteString(word) + return + } + + buf.WriteString("'") + + for i := 0; i < len(word); i++ { + b := word[i] + if b == '\'' { + // Replace literal ' with a close ', a \', and a open ' + buf.WriteString("'\\''") + } else { + buf.WriteByte(b) + } + } + + buf.WriteString("'") +} + +// ShellQuoteArguments takes a list of strings and escapes them so they will be +// handled right when passed as arguments to an program via a shell +func ShellQuoteArguments(args []string) string { + var buf bytes.Buffer + for i, arg := range args { + if i != 0 { + buf.WriteByte(' ') + } + quote(arg, &buf) + } + return buf.String() +} diff --git a/pkg/stringutils/stringutils_test.go b/pkg/stringutils/stringutils_test.go new file mode 100644 index 00000000..fec59450 --- /dev/null +++ b/pkg/stringutils/stringutils_test.go @@ -0,0 +1,105 @@ +package stringutils + +import "testing" + +func testLengthHelper(generator func(int) string, t *testing.T) { + expectedLength := 20 + s := generator(expectedLength) + if len(s) != expectedLength { + t.Fatalf("Length of %s was %d but expected length %d", s, len(s), expectedLength) + } +} + +func testUniquenessHelper(generator func(int) string, t *testing.T) { + repeats := 25 + set := make(map[string]struct{}, repeats) + for i := 0; i < repeats; i = i + 1 { + str := generator(64) + if len(str) != 64 { + t.Fatalf("Id returned is incorrect: %s", str) + } + if _, ok := set[str]; ok { + t.Fatalf("Random number is repeated") + } + set[str] = struct{}{} + } +} + +func isASCII(s string) bool { + for _, c := range s { + if c > 127 { + return false + } + } + return true +} + +func TestGenerateRandomAlphaOnlyStringLength(t *testing.T) { + testLengthHelper(GenerateRandomAlphaOnlyString, t) +} + +func TestGenerateRandomAlphaOnlyStringUniqueness(t *testing.T) { + testUniquenessHelper(GenerateRandomAlphaOnlyString, t) +} + +func TestGenerateRandomAsciiStringLength(t *testing.T) { + testLengthHelper(GenerateRandomASCIIString, t) +} + +func TestGenerateRandomAsciiStringUniqueness(t *testing.T) { + testUniquenessHelper(GenerateRandomASCIIString, t) +} + +func TestGenerateRandomAsciiStringIsAscii(t *testing.T) { + str := GenerateRandomASCIIString(64) + if !isASCII(str) { + t.Fatalf("%s contained non-ascii characters", str) + } +} + +func TestTruncate(t *testing.T) { + str := "teststring" + newstr := Truncate(str, 4) + if newstr != "test" { + t.Fatalf("Expected test, got %s", newstr) + } + newstr = Truncate(str, 20) + if newstr != "teststring" { + t.Fatalf("Expected teststring, got %s", newstr) + } +} + +func TestInSlice(t *testing.T) { + slice := []string{"test", "in", "slice"} + + test := InSlice(slice, "test") + if !test { + t.Fatalf("Expected string test to be in slice") + } + test = InSlice(slice, "SLICE") + if !test { + t.Fatalf("Expected string SLICE to be in slice") + } + test = InSlice(slice, "notinslice") + if test { + t.Fatalf("Expected string notinslice not to be in slice") + } +} + +func TestShellQuoteArgumentsEmpty(t *testing.T) { + actual := ShellQuoteArguments([]string{}) + expected := "" + if actual != expected { + t.Fatalf("Expected an empty string") + } +} + +func TestShellQuoteArguments(t *testing.T) { + simpleString := "simpleString" + complexString := "This is a 'more' complex $tring with some special char *" + actual := ShellQuoteArguments([]string{simpleString, complexString}) + expected := "simpleString 'This is a '\\''more'\\'' complex $tring with some special char *'" + if actual != expected { + t.Fatalf("Expected \"%v\", got \"%v\"", expected, actual) + } +} diff --git a/pkg/symlink/LICENSE.APACHE b/pkg/symlink/LICENSE.APACHE new file mode 100644 index 00000000..34c4ea7c --- /dev/null +++ b/pkg/symlink/LICENSE.APACHE @@ -0,0 +1,191 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2014-2016 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/pkg/symlink/LICENSE.BSD b/pkg/symlink/LICENSE.BSD new file mode 100644 index 00000000..9b4f4a29 --- /dev/null +++ b/pkg/symlink/LICENSE.BSD @@ -0,0 +1,27 @@ +Copyright (c) 2014-2016 The Docker & Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pkg/symlink/README.md b/pkg/symlink/README.md new file mode 100644 index 00000000..8dba54fd --- /dev/null +++ b/pkg/symlink/README.md @@ -0,0 +1,6 @@ +Package symlink implements EvalSymlinksInScope which is an extension of filepath.EvalSymlinks, +as well as a Windows long-path aware version of filepath.EvalSymlinks +from the [Go standard library](https://golang.org/pkg/path/filepath). + +The code from filepath.EvalSymlinks has been adapted in fs.go. +Please read the LICENSE.BSD file that governs fs.go and LICENSE.APACHE for fs_test.go. diff --git a/pkg/symlink/fs.go b/pkg/symlink/fs.go new file mode 100644 index 00000000..dcf707f4 --- /dev/null +++ b/pkg/symlink/fs.go @@ -0,0 +1,143 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.BSD file. + +// This code is a modified version of path/filepath/symlink.go from the Go standard library. + +package symlink + +import ( + "bytes" + "errors" + "os" + "path/filepath" + "strings" + + "github.com/docker/docker/pkg/system" +) + +// FollowSymlinkInScope is a wrapper around evalSymlinksInScope that returns an +// absolute path. This function handles paths in a platform-agnostic manner. +func FollowSymlinkInScope(path, root string) (string, error) { + path, err := filepath.Abs(filepath.FromSlash(path)) + if err != nil { + return "", err + } + root, err = filepath.Abs(filepath.FromSlash(root)) + if err != nil { + return "", err + } + return evalSymlinksInScope(path, root) +} + +// evalSymlinksInScope will evaluate symlinks in `path` within a scope `root` and return +// a result guaranteed to be contained within the scope `root`, at the time of the call. +// Symlinks in `root` are not evaluated and left as-is. +// Errors encountered while attempting to evaluate symlinks in path will be returned. +// Non-existing paths are valid and do not constitute an error. +// `path` has to contain `root` as a prefix, or else an error will be returned. +// Trying to break out from `root` does not constitute an error. +// +// Example: +// If /foo/bar -> /outside, +// FollowSymlinkInScope("/foo/bar", "/foo") == "/foo/outside" instead of "/oustide" +// +// IMPORTANT: it is the caller's responsibility to call evalSymlinksInScope *after* relevant symlinks +// are created and not to create subsequently, additional symlinks that could potentially make a +// previously-safe path, unsafe. Example: if /foo/bar does not exist, evalSymlinksInScope("/foo/bar", "/foo") +// would return "/foo/bar". If one makes /foo/bar a symlink to /baz subsequently, then "/foo/bar" should +// no longer be considered safely contained in "/foo". +func evalSymlinksInScope(path, root string) (string, error) { + root = filepath.Clean(root) + if path == root { + return path, nil + } + if !strings.HasPrefix(path, root) { + return "", errors.New("evalSymlinksInScope: " + path + " is not in " + root) + } + const maxIter = 255 + originalPath := path + // given root of "/a" and path of "/a/b/../../c" we want path to be "/b/../../c" + path = path[len(root):] + if root == string(filepath.Separator) { + path = string(filepath.Separator) + path + } + if !strings.HasPrefix(path, string(filepath.Separator)) { + return "", errors.New("evalSymlinksInScope: " + path + " is not in " + root) + } + path = filepath.Clean(path) + // consume path by taking each frontmost path element, + // expanding it if it's a symlink, and appending it to b + var b bytes.Buffer + // b here will always be considered to be the "current absolute path inside + // root" when we append paths to it, we also append a slash and use + // filepath.Clean after the loop to trim the trailing slash + for n := 0; path != ""; n++ { + if n > maxIter { + return "", errors.New("evalSymlinksInScope: too many links in " + originalPath) + } + + // find next path component, p + i := strings.IndexRune(path, filepath.Separator) + var p string + if i == -1 { + p, path = path, "" + } else { + p, path = path[:i], path[i+1:] + } + + if p == "" { + continue + } + + // this takes a b.String() like "b/../" and a p like "c" and turns it + // into "/b/../c" which then gets filepath.Cleaned into "/c" and then + // root gets prepended and we Clean again (to remove any trailing slash + // if the first Clean gave us just "/") + cleanP := filepath.Clean(string(filepath.Separator) + b.String() + p) + if cleanP == string(filepath.Separator) { + // never Lstat "/" itself + b.Reset() + continue + } + fullP := filepath.Clean(root + cleanP) + + fi, err := os.Lstat(fullP) + if os.IsNotExist(err) { + // if p does not exist, accept it + b.WriteString(p) + b.WriteRune(filepath.Separator) + continue + } + if err != nil { + return "", err + } + if fi.Mode()&os.ModeSymlink == 0 { + b.WriteString(p + string(filepath.Separator)) + continue + } + + // it's a symlink, put it at the front of path + dest, err := os.Readlink(fullP) + if err != nil { + return "", err + } + if system.IsAbs(dest) { + b.Reset() + } + path = dest + string(filepath.Separator) + path + } + + // see note above on "fullP := ..." for why this is double-cleaned and + // what's happening here + return filepath.Clean(root + filepath.Clean(string(filepath.Separator)+b.String())), nil +} + +// EvalSymlinks returns the path name after the evaluation of any symbolic +// links. +// If path is relative the result will be relative to the current directory, +// unless one of the components is an absolute symbolic link. +// This version has been updated to support long paths prepended with `\\?\`. +func EvalSymlinks(path string) (string, error) { + return evalSymlinks(path) +} diff --git a/pkg/symlink/fs_unix.go b/pkg/symlink/fs_unix.go new file mode 100644 index 00000000..818004f2 --- /dev/null +++ b/pkg/symlink/fs_unix.go @@ -0,0 +1,11 @@ +// +build !windows + +package symlink + +import ( + "path/filepath" +) + +func evalSymlinks(path string) (string, error) { + return filepath.EvalSymlinks(path) +} diff --git a/pkg/symlink/fs_unix_test.go b/pkg/symlink/fs_unix_test.go new file mode 100644 index 00000000..7085c0b6 --- /dev/null +++ b/pkg/symlink/fs_unix_test.go @@ -0,0 +1,407 @@ +// +build !windows + +// Licensed under the Apache License, Version 2.0; See LICENSE.APACHE + +package symlink + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +// TODO Windows: This needs some serious work to port to Windows. For now, +// turning off testing in this package. + +type dirOrLink struct { + path string + target string +} + +func makeFs(tmpdir string, fs []dirOrLink) error { + for _, s := range fs { + s.path = filepath.Join(tmpdir, s.path) + if s.target == "" { + os.MkdirAll(s.path, 0755) + continue + } + if err := os.MkdirAll(filepath.Dir(s.path), 0755); err != nil { + return err + } + if err := os.Symlink(s.target, s.path); err != nil && !os.IsExist(err) { + return err + } + } + return nil +} + +func testSymlink(tmpdir, path, expected, scope string) error { + rewrite, err := FollowSymlinkInScope(filepath.Join(tmpdir, path), filepath.Join(tmpdir, scope)) + if err != nil { + return err + } + expected, err = filepath.Abs(filepath.Join(tmpdir, expected)) + if err != nil { + return err + } + if expected != rewrite { + return fmt.Errorf("Expected %q got %q", expected, rewrite) + } + return nil +} + +func TestFollowSymlinkAbsolute(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "TestFollowSymlinkAbsolute") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + if err := makeFs(tmpdir, []dirOrLink{{path: "testdata/fs/a/d", target: "/b"}}); err != nil { + t.Fatal(err) + } + if err := testSymlink(tmpdir, "testdata/fs/a/d/c/data", "testdata/b/c/data", "testdata"); err != nil { + t.Fatal(err) + } +} + +func TestFollowSymlinkRelativePath(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "TestFollowSymlinkRelativePath") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + if err := makeFs(tmpdir, []dirOrLink{{path: "testdata/fs/i", target: "a"}}); err != nil { + t.Fatal(err) + } + if err := testSymlink(tmpdir, "testdata/fs/i", "testdata/fs/a", "testdata"); err != nil { + t.Fatal(err) + } +} + +func TestFollowSymlinkSkipSymlinksOutsideScope(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "TestFollowSymlinkSkipSymlinksOutsideScope") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + if err := makeFs(tmpdir, []dirOrLink{ + {path: "linkdir", target: "realdir"}, + {path: "linkdir/foo/bar"}, + }); err != nil { + t.Fatal(err) + } + if err := testSymlink(tmpdir, "linkdir/foo/bar", "linkdir/foo/bar", "linkdir/foo"); err != nil { + t.Fatal(err) + } +} + +func TestFollowSymlinkInvalidScopePathPair(t *testing.T) { + if _, err := FollowSymlinkInScope("toto", "testdata"); err == nil { + t.Fatal("expected an error") + } +} + +func TestFollowSymlinkLastLink(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "TestFollowSymlinkLastLink") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + if err := makeFs(tmpdir, []dirOrLink{{path: "testdata/fs/a/d", target: "/b"}}); err != nil { + t.Fatal(err) + } + if err := testSymlink(tmpdir, "testdata/fs/a/d", "testdata/b", "testdata"); err != nil { + t.Fatal(err) + } +} + +func TestFollowSymlinkRelativeLinkChangeScope(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "TestFollowSymlinkRelativeLinkChangeScope") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + if err := makeFs(tmpdir, []dirOrLink{{path: "testdata/fs/a/e", target: "../b"}}); err != nil { + t.Fatal(err) + } + if err := testSymlink(tmpdir, "testdata/fs/a/e/c/data", "testdata/fs/b/c/data", "testdata"); err != nil { + t.Fatal(err) + } + // avoid letting allowing symlink e lead us to ../b + // normalize to the "testdata/fs/a" + if err := testSymlink(tmpdir, "testdata/fs/a/e", "testdata/fs/a/b", "testdata/fs/a"); err != nil { + t.Fatal(err) + } +} + +func TestFollowSymlinkDeepRelativeLinkChangeScope(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "TestFollowSymlinkDeepRelativeLinkChangeScope") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + if err := makeFs(tmpdir, []dirOrLink{{path: "testdata/fs/a/f", target: "../../../../test"}}); err != nil { + t.Fatal(err) + } + // avoid letting symlink f lead us out of the "testdata" scope + // we don't normalize because symlink f is in scope and there is no + // information leak + if err := testSymlink(tmpdir, "testdata/fs/a/f", "testdata/test", "testdata"); err != nil { + t.Fatal(err) + } + // avoid letting symlink f lead us out of the "testdata/fs" scope + // we don't normalize because symlink f is in scope and there is no + // information leak + if err := testSymlink(tmpdir, "testdata/fs/a/f", "testdata/fs/test", "testdata/fs"); err != nil { + t.Fatal(err) + } +} + +func TestFollowSymlinkRelativeLinkChain(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "TestFollowSymlinkRelativeLinkChain") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + // avoid letting symlink g (pointed at by symlink h) take out of scope + // TODO: we should probably normalize to scope here because ../[....]/root + // is out of scope and we leak information + if err := makeFs(tmpdir, []dirOrLink{ + {path: "testdata/fs/b/h", target: "../g"}, + {path: "testdata/fs/g", target: "../../../../../../../../../../../../root"}, + }); err != nil { + t.Fatal(err) + } + if err := testSymlink(tmpdir, "testdata/fs/b/h", "testdata/root", "testdata"); err != nil { + t.Fatal(err) + } +} + +func TestFollowSymlinkBreakoutPath(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "TestFollowSymlinkBreakoutPath") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + // avoid letting symlink -> ../directory/file escape from scope + // normalize to "testdata/fs/j" + if err := makeFs(tmpdir, []dirOrLink{{path: "testdata/fs/j/k", target: "../i/a"}}); err != nil { + t.Fatal(err) + } + if err := testSymlink(tmpdir, "testdata/fs/j/k", "testdata/fs/j/i/a", "testdata/fs/j"); err != nil { + t.Fatal(err) + } +} + +func TestFollowSymlinkToRoot(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "TestFollowSymlinkToRoot") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + // make sure we don't allow escaping to / + // normalize to dir + if err := makeFs(tmpdir, []dirOrLink{{path: "foo", target: "/"}}); err != nil { + t.Fatal(err) + } + if err := testSymlink(tmpdir, "foo", "", ""); err != nil { + t.Fatal(err) + } +} + +func TestFollowSymlinkSlashDotdot(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "TestFollowSymlinkSlashDotdot") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + tmpdir = filepath.Join(tmpdir, "dir", "subdir") + + // make sure we don't allow escaping to / + // normalize to dir + if err := makeFs(tmpdir, []dirOrLink{{path: "foo", target: "/../../"}}); err != nil { + t.Fatal(err) + } + if err := testSymlink(tmpdir, "foo", "", ""); err != nil { + t.Fatal(err) + } +} + +func TestFollowSymlinkDotdot(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "TestFollowSymlinkDotdot") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + tmpdir = filepath.Join(tmpdir, "dir", "subdir") + + // make sure we stay in scope without leaking information + // this also checks for escaping to / + // normalize to dir + if err := makeFs(tmpdir, []dirOrLink{{path: "foo", target: "../../"}}); err != nil { + t.Fatal(err) + } + if err := testSymlink(tmpdir, "foo", "", ""); err != nil { + t.Fatal(err) + } +} + +func TestFollowSymlinkRelativePath2(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "TestFollowSymlinkRelativePath2") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + if err := makeFs(tmpdir, []dirOrLink{{path: "bar/foo", target: "baz/target"}}); err != nil { + t.Fatal(err) + } + if err := testSymlink(tmpdir, "bar/foo", "bar/baz/target", ""); err != nil { + t.Fatal(err) + } +} + +func TestFollowSymlinkScopeLink(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "TestFollowSymlinkScopeLink") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + if err := makeFs(tmpdir, []dirOrLink{ + {path: "root2"}, + {path: "root", target: "root2"}, + {path: "root2/foo", target: "../bar"}, + }); err != nil { + t.Fatal(err) + } + if err := testSymlink(tmpdir, "root/foo", "root/bar", "root"); err != nil { + t.Fatal(err) + } +} + +func TestFollowSymlinkRootScope(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "TestFollowSymlinkRootScope") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + expected, err := filepath.EvalSymlinks(tmpdir) + if err != nil { + t.Fatal(err) + } + rewrite, err := FollowSymlinkInScope(tmpdir, "/") + if err != nil { + t.Fatal(err) + } + if rewrite != expected { + t.Fatalf("expected %q got %q", expected, rewrite) + } +} + +func TestFollowSymlinkEmpty(t *testing.T) { + res, err := FollowSymlinkInScope("", "") + if err != nil { + t.Fatal(err) + } + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + if res != wd { + t.Fatalf("expected %q got %q", wd, res) + } +} + +func TestFollowSymlinkCircular(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "TestFollowSymlinkCircular") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + if err := makeFs(tmpdir, []dirOrLink{{path: "root/foo", target: "foo"}}); err != nil { + t.Fatal(err) + } + if err := testSymlink(tmpdir, "root/foo", "", "root"); err == nil { + t.Fatal("expected an error for foo -> foo") + } + + if err := makeFs(tmpdir, []dirOrLink{ + {path: "root/bar", target: "baz"}, + {path: "root/baz", target: "../bak"}, + {path: "root/bak", target: "/bar"}, + }); err != nil { + t.Fatal(err) + } + if err := testSymlink(tmpdir, "root/foo", "", "root"); err == nil { + t.Fatal("expected an error for bar -> baz -> bak -> bar") + } +} + +func TestFollowSymlinkComplexChainWithTargetPathsContainingLinks(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "TestFollowSymlinkComplexChainWithTargetPathsContainingLinks") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + if err := makeFs(tmpdir, []dirOrLink{ + {path: "root2"}, + {path: "root", target: "root2"}, + {path: "root/a", target: "r/s"}, + {path: "root/r", target: "../root/t"}, + {path: "root/root/t/s/b", target: "/../u"}, + {path: "root/u/c", target: "."}, + {path: "root/u/x/y", target: "../v"}, + {path: "root/u/v", target: "/../w"}, + }); err != nil { + t.Fatal(err) + } + if err := testSymlink(tmpdir, "root/a/b/c/x/y/z", "root/w/z", "root"); err != nil { + t.Fatal(err) + } +} + +func TestFollowSymlinkBreakoutNonExistent(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "TestFollowSymlinkBreakoutNonExistent") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + if err := makeFs(tmpdir, []dirOrLink{ + {path: "root/slash", target: "/"}, + {path: "root/sym", target: "/idontexist/../slash"}, + }); err != nil { + t.Fatal(err) + } + if err := testSymlink(tmpdir, "root/sym/file", "root/file", "root"); err != nil { + t.Fatal(err) + } +} + +func TestFollowSymlinkNoLexicalCleaning(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "TestFollowSymlinkNoLexicalCleaning") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + if err := makeFs(tmpdir, []dirOrLink{ + {path: "root/sym", target: "/foo/bar"}, + {path: "root/hello", target: "/sym/../baz"}, + }); err != nil { + t.Fatal(err) + } + if err := testSymlink(tmpdir, "root/hello", "root/foo/baz", "root"); err != nil { + t.Fatal(err) + } +} diff --git a/pkg/symlink/fs_windows.go b/pkg/symlink/fs_windows.go new file mode 100644 index 00000000..449fe564 --- /dev/null +++ b/pkg/symlink/fs_windows.go @@ -0,0 +1,155 @@ +package symlink + +import ( + "bytes" + "errors" + "os" + "path/filepath" + "strings" + "syscall" + + "github.com/docker/docker/pkg/longpath" +) + +func toShort(path string) (string, error) { + p, err := syscall.UTF16FromString(path) + if err != nil { + return "", err + } + b := p // GetShortPathName says we can reuse buffer + n, err := syscall.GetShortPathName(&p[0], &b[0], uint32(len(b))) + if err != nil { + return "", err + } + if n > uint32(len(b)) { + b = make([]uint16, n) + if _, err = syscall.GetShortPathName(&p[0], &b[0], uint32(len(b))); err != nil { + return "", err + } + } + return syscall.UTF16ToString(b), nil +} + +func toLong(path string) (string, error) { + p, err := syscall.UTF16FromString(path) + if err != nil { + return "", err + } + b := p // GetLongPathName says we can reuse buffer + n, err := syscall.GetLongPathName(&p[0], &b[0], uint32(len(b))) + if err != nil { + return "", err + } + if n > uint32(len(b)) { + b = make([]uint16, n) + n, err = syscall.GetLongPathName(&p[0], &b[0], uint32(len(b))) + if err != nil { + return "", err + } + } + b = b[:n] + return syscall.UTF16ToString(b), nil +} + +func evalSymlinks(path string) (string, error) { + path, err := walkSymlinks(path) + if err != nil { + return "", err + } + + p, err := toShort(path) + if err != nil { + return "", err + } + p, err = toLong(p) + if err != nil { + return "", err + } + // syscall.GetLongPathName does not change the case of the drive letter, + // but the result of EvalSymlinks must be unique, so we have + // EvalSymlinks(`c:\a`) == EvalSymlinks(`C:\a`). + // Make drive letter upper case. + if len(p) >= 2 && p[1] == ':' && 'a' <= p[0] && p[0] <= 'z' { + p = string(p[0]+'A'-'a') + p[1:] + } else if len(p) >= 6 && p[5] == ':' && 'a' <= p[4] && p[4] <= 'z' { + p = p[:3] + string(p[4]+'A'-'a') + p[5:] + } + return filepath.Clean(p), nil +} + +const utf8RuneSelf = 0x80 + +func walkSymlinks(path string) (string, error) { + const maxIter = 255 + originalPath := path + // consume path by taking each frontmost path element, + // expanding it if it's a symlink, and appending it to b + var b bytes.Buffer + for n := 0; path != ""; n++ { + if n > maxIter { + return "", errors.New("EvalSymlinks: too many links in " + originalPath) + } + + // A path beginning with `\\?\` represents the root, so automatically + // skip that part and begin processing the next segment. + if strings.HasPrefix(path, longpath.Prefix) { + b.WriteString(longpath.Prefix) + path = path[4:] + continue + } + + // find next path component, p + var i = -1 + for j, c := range path { + if c < utf8RuneSelf && os.IsPathSeparator(uint8(c)) { + i = j + break + } + } + var p string + if i == -1 { + p, path = path, "" + } else { + p, path = path[:i], path[i+1:] + } + + if p == "" { + if b.Len() == 0 { + // must be absolute path + b.WriteRune(filepath.Separator) + } + continue + } + + // If this is the first segment after the long path prefix, accept the + // current segment as a volume root or UNC share and move on to the next. + if b.String() == longpath.Prefix { + b.WriteString(p) + b.WriteRune(filepath.Separator) + continue + } + + fi, err := os.Lstat(b.String() + p) + if err != nil { + return "", err + } + if fi.Mode()&os.ModeSymlink == 0 { + b.WriteString(p) + if path != "" || (b.Len() == 2 && len(p) == 2 && p[1] == ':') { + b.WriteRune(filepath.Separator) + } + continue + } + + // it's a symlink, put it at the front of path + dest, err := os.Readlink(b.String() + p) + if err != nil { + return "", err + } + if filepath.IsAbs(dest) || os.IsPathSeparator(dest[0]) { + b.Reset() + } + path = dest + string(filepath.Separator) + path + } + return filepath.Clean(b.String()), nil +} diff --git a/pkg/sysinfo/README.md b/pkg/sysinfo/README.md new file mode 100644 index 00000000..c1530cef --- /dev/null +++ b/pkg/sysinfo/README.md @@ -0,0 +1 @@ +SysInfo stores information about which features a kernel supports. diff --git a/pkg/sysinfo/sysinfo.go b/pkg/sysinfo/sysinfo.go new file mode 100644 index 00000000..cbd00999 --- /dev/null +++ b/pkg/sysinfo/sysinfo.go @@ -0,0 +1,128 @@ +package sysinfo + +import "github.com/docker/docker/pkg/parsers" + +// SysInfo stores information about which features a kernel supports. +// TODO Windows: Factor out platform specific capabilities. +type SysInfo struct { + // Whether the kernel supports AppArmor or not + AppArmor bool + // Whether the kernel supports Seccomp or not + Seccomp bool + + cgroupMemInfo + cgroupCPUInfo + cgroupBlkioInfo + cgroupCpusetInfo + cgroupPids + + // Whether IPv4 forwarding is supported or not, if this was disabled, networking will not work + IPv4ForwardingDisabled bool + + // Whether bridge-nf-call-iptables is supported or not + BridgeNFCallIPTablesDisabled bool + + // Whether bridge-nf-call-ip6tables is supported or not + BridgeNFCallIP6TablesDisabled bool + + // Whether the cgroup has the mountpoint of "devices" or not + CgroupDevicesEnabled bool +} + +type cgroupMemInfo struct { + // Whether memory limit is supported or not + MemoryLimit bool + + // Whether swap limit is supported or not + SwapLimit bool + + // Whether soft limit is supported or not + MemoryReservation bool + + // Whether OOM killer disable is supported or not + OomKillDisable bool + + // Whether memory swappiness is supported or not + MemorySwappiness bool + + // Whether kernel memory limit is supported or not + KernelMemory bool +} + +type cgroupCPUInfo struct { + // Whether CPU shares is supported or not + CPUShares bool + + // Whether CPU CFS(Completely Fair Scheduler) period is supported or not + CPUCfsPeriod bool + + // Whether CPU CFS(Completely Fair Scheduler) quota is supported or not + CPUCfsQuota bool +} + +type cgroupBlkioInfo struct { + // Whether Block IO weight is supported or not + BlkioWeight bool + + // Whether Block IO weight_device is supported or not + BlkioWeightDevice bool + + // Whether Block IO read limit in bytes per second is supported or not + BlkioReadBpsDevice bool + + // Whether Block IO write limit in bytes per second is supported or not + BlkioWriteBpsDevice bool + + // Whether Block IO read limit in IO per second is supported or not + BlkioReadIOpsDevice bool + + // Whether Block IO write limit in IO per second is supported or not + BlkioWriteIOpsDevice bool +} + +type cgroupCpusetInfo struct { + // Whether Cpuset is supported or not + Cpuset bool + + // Available Cpuset's cpus + Cpus string + + // Available Cpuset's memory nodes + Mems string +} + +type cgroupPids struct { + // Whether Pids Limit is supported or not + PidsLimit bool +} + +// IsCpusetCpusAvailable returns `true` if the provided string set is contained +// in cgroup's cpuset.cpus set, `false` otherwise. +// If error is not nil a parsing error occurred. +func (c cgroupCpusetInfo) IsCpusetCpusAvailable(provided string) (bool, error) { + return isCpusetListAvailable(provided, c.Cpus) +} + +// IsCpusetMemsAvailable returns `true` if the provided string set is contained +// in cgroup's cpuset.mems set, `false` otherwise. +// If error is not nil a parsing error occurred. +func (c cgroupCpusetInfo) IsCpusetMemsAvailable(provided string) (bool, error) { + return isCpusetListAvailable(provided, c.Mems) +} + +func isCpusetListAvailable(provided, available string) (bool, error) { + parsedProvided, err := parsers.ParseUintList(provided) + if err != nil { + return false, err + } + parsedAvailable, err := parsers.ParseUintList(available) + if err != nil { + return false, err + } + for k := range parsedProvided { + if !parsedAvailable[k] { + return false, nil + } + } + return true, nil +} diff --git a/pkg/sysinfo/sysinfo_freebsd.go b/pkg/sysinfo/sysinfo_freebsd.go new file mode 100644 index 00000000..22ae0d95 --- /dev/null +++ b/pkg/sysinfo/sysinfo_freebsd.go @@ -0,0 +1,7 @@ +package sysinfo + +// New returns an empty SysInfo for freebsd for now. +func New(quiet bool) *SysInfo { + sysInfo := &SysInfo{} + return sysInfo +} diff --git a/pkg/sysinfo/sysinfo_linux.go b/pkg/sysinfo/sysinfo_linux.go new file mode 100644 index 00000000..41fb0d2b --- /dev/null +++ b/pkg/sysinfo/sysinfo_linux.go @@ -0,0 +1,246 @@ +package sysinfo + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "strings" + "syscall" + + "github.com/Sirupsen/logrus" + "github.com/opencontainers/runc/libcontainer/cgroups" +) + +const ( + // SeccompModeFilter refers to the syscall argument SECCOMP_MODE_FILTER. + SeccompModeFilter = uintptr(2) +) + +func findCgroupMountpoints() (map[string]string, error) { + cgMounts, err := cgroups.GetCgroupMounts() + if err != nil { + return nil, fmt.Errorf("Failed to parse cgroup information: %v", err) + } + mps := make(map[string]string) + for _, m := range cgMounts { + for _, ss := range m.Subsystems { + mps[ss] = m.Mountpoint + } + } + return mps, nil +} + +// New returns a new SysInfo, using the filesystem to detect which features +// the kernel supports. If `quiet` is `false` warnings are printed in logs +// whenever an error occurs or misconfigurations are present. +func New(quiet bool) *SysInfo { + sysInfo := &SysInfo{} + cgMounts, err := findCgroupMountpoints() + if err != nil { + logrus.Warnf("Failed to parse cgroup information: %v", err) + } else { + sysInfo.cgroupMemInfo = checkCgroupMem(cgMounts, quiet) + sysInfo.cgroupCPUInfo = checkCgroupCPU(cgMounts, quiet) + sysInfo.cgroupBlkioInfo = checkCgroupBlkioInfo(cgMounts, quiet) + sysInfo.cgroupCpusetInfo = checkCgroupCpusetInfo(cgMounts, quiet) + sysInfo.cgroupPids = checkCgroupPids(quiet) + } + + _, ok := cgMounts["devices"] + sysInfo.CgroupDevicesEnabled = ok + + sysInfo.IPv4ForwardingDisabled = !readProcBool("/proc/sys/net/ipv4/ip_forward") + sysInfo.BridgeNFCallIPTablesDisabled = !readProcBool("/proc/sys/net/bridge/bridge-nf-call-iptables") + sysInfo.BridgeNFCallIP6TablesDisabled = !readProcBool("/proc/sys/net/bridge/bridge-nf-call-ip6tables") + + // Check if AppArmor is supported. + if _, err := os.Stat("/sys/kernel/security/apparmor"); !os.IsNotExist(err) { + sysInfo.AppArmor = true + } + + // Check if Seccomp is supported, via CONFIG_SECCOMP. + if _, _, err := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_GET_SECCOMP, 0, 0); err != syscall.EINVAL { + // Make sure the kernel has CONFIG_SECCOMP_FILTER. + if _, _, err := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_SECCOMP, SeccompModeFilter, 0); err != syscall.EINVAL { + sysInfo.Seccomp = true + } + } + + return sysInfo +} + +// checkCgroupMem reads the memory information from the memory cgroup mount point. +func checkCgroupMem(cgMounts map[string]string, quiet bool) cgroupMemInfo { + mountPoint, ok := cgMounts["memory"] + if !ok { + if !quiet { + logrus.Warnf("Your kernel does not support cgroup memory limit") + } + return cgroupMemInfo{} + } + + swapLimit := cgroupEnabled(mountPoint, "memory.memsw.limit_in_bytes") + if !quiet && !swapLimit { + logrus.Warn("Your kernel does not support swap memory limit.") + } + memoryReservation := cgroupEnabled(mountPoint, "memory.soft_limit_in_bytes") + if !quiet && !memoryReservation { + logrus.Warn("Your kernel does not support memory reservation.") + } + oomKillDisable := cgroupEnabled(mountPoint, "memory.oom_control") + if !quiet && !oomKillDisable { + logrus.Warnf("Your kernel does not support oom control.") + } + memorySwappiness := cgroupEnabled(mountPoint, "memory.swappiness") + if !quiet && !memorySwappiness { + logrus.Warnf("Your kernel does not support memory swappiness.") + } + kernelMemory := cgroupEnabled(mountPoint, "memory.kmem.limit_in_bytes") + if !quiet && !kernelMemory { + logrus.Warnf("Your kernel does not support kernel memory limit.") + } + + return cgroupMemInfo{ + MemoryLimit: true, + SwapLimit: swapLimit, + MemoryReservation: memoryReservation, + OomKillDisable: oomKillDisable, + MemorySwappiness: memorySwappiness, + KernelMemory: kernelMemory, + } +} + +// checkCgroupCPU reads the cpu information from the cpu cgroup mount point. +func checkCgroupCPU(cgMounts map[string]string, quiet bool) cgroupCPUInfo { + mountPoint, ok := cgMounts["cpu"] + if !ok { + if !quiet { + logrus.Warnf("Unable to find cpu cgroup in mounts") + } + return cgroupCPUInfo{} + } + + cpuShares := cgroupEnabled(mountPoint, "cpu.shares") + if !quiet && !cpuShares { + logrus.Warn("Your kernel does not support cgroup cpu shares") + } + + cpuCfsPeriod := cgroupEnabled(mountPoint, "cpu.cfs_period_us") + if !quiet && !cpuCfsPeriod { + logrus.Warn("Your kernel does not support cgroup cfs period") + } + + cpuCfsQuota := cgroupEnabled(mountPoint, "cpu.cfs_quota_us") + if !quiet && !cpuCfsQuota { + logrus.Warn("Your kernel does not support cgroup cfs quotas") + } + return cgroupCPUInfo{ + CPUShares: cpuShares, + CPUCfsPeriod: cpuCfsPeriod, + CPUCfsQuota: cpuCfsQuota, + } +} + +// checkCgroupBlkioInfo reads the blkio information from the blkio cgroup mount point. +func checkCgroupBlkioInfo(cgMounts map[string]string, quiet bool) cgroupBlkioInfo { + mountPoint, ok := cgMounts["blkio"] + if !ok { + if !quiet { + logrus.Warnf("Unable to find blkio cgroup in mounts") + } + return cgroupBlkioInfo{} + } + + weight := cgroupEnabled(mountPoint, "blkio.weight") + if !quiet && !weight { + logrus.Warn("Your kernel does not support cgroup blkio weight") + } + + weightDevice := cgroupEnabled(mountPoint, "blkio.weight_device") + if !quiet && !weightDevice { + logrus.Warn("Your kernel does not support cgroup blkio weight_device") + } + + readBpsDevice := cgroupEnabled(mountPoint, "blkio.throttle.read_bps_device") + if !quiet && !readBpsDevice { + logrus.Warn("Your kernel does not support cgroup blkio throttle.read_bps_device") + } + + writeBpsDevice := cgroupEnabled(mountPoint, "blkio.throttle.write_bps_device") + if !quiet && !writeBpsDevice { + logrus.Warn("Your kernel does not support cgroup blkio throttle.write_bps_device") + } + readIOpsDevice := cgroupEnabled(mountPoint, "blkio.throttle.read_iops_device") + if !quiet && !readIOpsDevice { + logrus.Warn("Your kernel does not support cgroup blkio throttle.read_iops_device") + } + + writeIOpsDevice := cgroupEnabled(mountPoint, "blkio.throttle.write_iops_device") + if !quiet && !writeIOpsDevice { + logrus.Warn("Your kernel does not support cgroup blkio throttle.write_iops_device") + } + return cgroupBlkioInfo{ + BlkioWeight: weight, + BlkioWeightDevice: weightDevice, + BlkioReadBpsDevice: readBpsDevice, + BlkioWriteBpsDevice: writeBpsDevice, + BlkioReadIOpsDevice: readIOpsDevice, + BlkioWriteIOpsDevice: writeIOpsDevice, + } +} + +// checkCgroupCpusetInfo reads the cpuset information from the cpuset cgroup mount point. +func checkCgroupCpusetInfo(cgMounts map[string]string, quiet bool) cgroupCpusetInfo { + mountPoint, ok := cgMounts["cpuset"] + if !ok { + if !quiet { + logrus.Warnf("Unable to find cpuset cgroup in mounts") + } + return cgroupCpusetInfo{} + } + + cpus, err := ioutil.ReadFile(path.Join(mountPoint, "cpuset.cpus")) + if err != nil { + return cgroupCpusetInfo{} + } + + mems, err := ioutil.ReadFile(path.Join(mountPoint, "cpuset.mems")) + if err != nil { + return cgroupCpusetInfo{} + } + + return cgroupCpusetInfo{ + Cpuset: true, + Cpus: strings.TrimSpace(string(cpus)), + Mems: strings.TrimSpace(string(mems)), + } +} + +// checkCgroupPids reads the pids information from the pids cgroup mount point. +func checkCgroupPids(quiet bool) cgroupPids { + _, err := cgroups.FindCgroupMountpoint("pids") + if err != nil { + if !quiet { + logrus.Warn(err) + } + return cgroupPids{} + } + + return cgroupPids{ + PidsLimit: true, + } +} + +func cgroupEnabled(mountPoint, name string) bool { + _, err := os.Stat(path.Join(mountPoint, name)) + return err == nil +} + +func readProcBool(path string) bool { + val, err := ioutil.ReadFile(path) + if err != nil { + return false + } + return strings.TrimSpace(string(val)) == "1" +} diff --git a/pkg/sysinfo/sysinfo_linux_test.go b/pkg/sysinfo/sysinfo_linux_test.go new file mode 100644 index 00000000..fae0fdff --- /dev/null +++ b/pkg/sysinfo/sysinfo_linux_test.go @@ -0,0 +1,58 @@ +package sysinfo + +import ( + "io/ioutil" + "os" + "path" + "path/filepath" + "testing" +) + +func TestReadProcBool(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "test-sysinfo-proc") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + procFile := filepath.Join(tmpDir, "read-proc-bool") + if err := ioutil.WriteFile(procFile, []byte("1"), 644); err != nil { + t.Fatal(err) + } + + if !readProcBool(procFile) { + t.Fatal("expected proc bool to be true, got false") + } + + if err := ioutil.WriteFile(procFile, []byte("0"), 644); err != nil { + t.Fatal(err) + } + if readProcBool(procFile) { + t.Fatal("expected proc bool to be false, got false") + } + + if readProcBool(path.Join(tmpDir, "no-exist")) { + t.Fatal("should be false for non-existent entry") + } + +} + +func TestCgroupEnabled(t *testing.T) { + cgroupDir, err := ioutil.TempDir("", "cgroup-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(cgroupDir) + + if cgroupEnabled(cgroupDir, "test") { + t.Fatal("cgroupEnabled should be false") + } + + if err := ioutil.WriteFile(path.Join(cgroupDir, "test"), []byte{}, 644); err != nil { + t.Fatal(err) + } + + if !cgroupEnabled(cgroupDir, "test") { + t.Fatal("cgroupEnabled should be true") + } +} diff --git a/pkg/sysinfo/sysinfo_test.go b/pkg/sysinfo/sysinfo_test.go new file mode 100644 index 00000000..b61fbcf5 --- /dev/null +++ b/pkg/sysinfo/sysinfo_test.go @@ -0,0 +1,26 @@ +package sysinfo + +import "testing" + +func TestIsCpusetListAvailable(t *testing.T) { + cases := []struct { + provided string + available string + res bool + err bool + }{ + {"1", "0-4", true, false}, + {"01,3", "0-4", true, false}, + {"", "0-7", true, false}, + {"1--42", "0-7", false, true}, + {"1-42", "00-1,8,,9", false, true}, + {"1,41-42", "43,45", false, false}, + {"0-3", "", false, false}, + } + for _, c := range cases { + r, err := isCpusetListAvailable(c.provided, c.available) + if (c.err && err == nil) && r != c.res { + t.Fatalf("Expected pair: %v, %v for %s, %s. Got %v, %v instead", c.res, c.err, c.provided, c.available, (c.err && err == nil), r) + } + } +} diff --git a/pkg/sysinfo/sysinfo_windows.go b/pkg/sysinfo/sysinfo_windows.go new file mode 100644 index 00000000..8889318c --- /dev/null +++ b/pkg/sysinfo/sysinfo_windows.go @@ -0,0 +1,7 @@ +package sysinfo + +// New returns an empty SysInfo for windows for now. +func New(quiet bool) *SysInfo { + sysInfo := &SysInfo{} + return sysInfo +} diff --git a/pkg/system/chtimes.go b/pkg/system/chtimes.go new file mode 100644 index 00000000..7637f12e --- /dev/null +++ b/pkg/system/chtimes.go @@ -0,0 +1,52 @@ +package system + +import ( + "os" + "syscall" + "time" + "unsafe" +) + +var ( + maxTime time.Time +) + +func init() { + if unsafe.Sizeof(syscall.Timespec{}.Nsec) == 8 { + // This is a 64 bit timespec + // os.Chtimes limits time to the following + maxTime = time.Unix(0, 1<<63-1) + } else { + // This is a 32 bit timespec + maxTime = time.Unix(1<<31-1, 0) + } +} + +// Chtimes changes the access time and modified time of a file at the given path +func Chtimes(name string, atime time.Time, mtime time.Time) error { + unixMinTime := time.Unix(0, 0) + unixMaxTime := maxTime + + // If the modified time is prior to the Unix Epoch, or after the + // end of Unix Time, os.Chtimes has undefined behavior + // default to Unix Epoch in this case, just in case + + if atime.Before(unixMinTime) || atime.After(unixMaxTime) { + atime = unixMinTime + } + + if mtime.Before(unixMinTime) || mtime.After(unixMaxTime) { + mtime = unixMinTime + } + + if err := os.Chtimes(name, atime, mtime); err != nil { + return err + } + + // Take platform specific action for setting create time. + if err := setCTime(name, mtime); err != nil { + return err + } + + return nil +} diff --git a/pkg/system/chtimes_test.go b/pkg/system/chtimes_test.go new file mode 100644 index 00000000..5c87df32 --- /dev/null +++ b/pkg/system/chtimes_test.go @@ -0,0 +1,94 @@ +package system + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + "time" +) + +// prepareTempFile creates a temporary file in a temporary directory. +func prepareTempFile(t *testing.T) (string, string) { + dir, err := ioutil.TempDir("", "docker-system-test") + if err != nil { + t.Fatal(err) + } + + file := filepath.Join(dir, "exist") + if err := ioutil.WriteFile(file, []byte("hello"), 0644); err != nil { + t.Fatal(err) + } + return file, dir +} + +// TestChtimes tests Chtimes on a tempfile. Test only mTime, because aTime is OS dependent +func TestChtimes(t *testing.T) { + file, dir := prepareTempFile(t) + defer os.RemoveAll(dir) + + beforeUnixEpochTime := time.Unix(0, 0).Add(-100 * time.Second) + unixEpochTime := time.Unix(0, 0) + afterUnixEpochTime := time.Unix(100, 0) + unixMaxTime := maxTime + + // Test both aTime and mTime set to Unix Epoch + Chtimes(file, unixEpochTime, unixEpochTime) + + f, err := os.Stat(file) + if err != nil { + t.Fatal(err) + } + + if f.ModTime() != unixEpochTime { + t.Fatalf("Expected: %s, got: %s", unixEpochTime, f.ModTime()) + } + + // Test aTime before Unix Epoch and mTime set to Unix Epoch + Chtimes(file, beforeUnixEpochTime, unixEpochTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + if f.ModTime() != unixEpochTime { + t.Fatalf("Expected: %s, got: %s", unixEpochTime, f.ModTime()) + } + + // Test aTime set to Unix Epoch and mTime before Unix Epoch + Chtimes(file, unixEpochTime, beforeUnixEpochTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + if f.ModTime() != unixEpochTime { + t.Fatalf("Expected: %s, got: %s", unixEpochTime, f.ModTime()) + } + + // Test both aTime and mTime set to after Unix Epoch (valid time) + Chtimes(file, afterUnixEpochTime, afterUnixEpochTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + if f.ModTime() != afterUnixEpochTime { + t.Fatalf("Expected: %s, got: %s", afterUnixEpochTime, f.ModTime()) + } + + // Test both aTime and mTime set to Unix max time + Chtimes(file, unixMaxTime, unixMaxTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + if f.ModTime().Truncate(time.Second) != unixMaxTime.Truncate(time.Second) { + t.Fatalf("Expected: %s, got: %s", unixMaxTime.Truncate(time.Second), f.ModTime().Truncate(time.Second)) + } +} diff --git a/pkg/system/chtimes_unix.go b/pkg/system/chtimes_unix.go new file mode 100644 index 00000000..09d58bcb --- /dev/null +++ b/pkg/system/chtimes_unix.go @@ -0,0 +1,14 @@ +// +build !windows + +package system + +import ( + "time" +) + +//setCTime will set the create time on a file. On Unix, the create +//time is updated as a side effect of setting the modified time, so +//no action is required. +func setCTime(path string, ctime time.Time) error { + return nil +} diff --git a/pkg/system/chtimes_unix_test.go b/pkg/system/chtimes_unix_test.go new file mode 100644 index 00000000..0aafe1d8 --- /dev/null +++ b/pkg/system/chtimes_unix_test.go @@ -0,0 +1,91 @@ +// +build !windows + +package system + +import ( + "os" + "syscall" + "testing" + "time" +) + +// TestChtimes tests Chtimes access time on a tempfile on Linux +func TestChtimesLinux(t *testing.T) { + file, dir := prepareTempFile(t) + defer os.RemoveAll(dir) + + beforeUnixEpochTime := time.Unix(0, 0).Add(-100 * time.Second) + unixEpochTime := time.Unix(0, 0) + afterUnixEpochTime := time.Unix(100, 0) + unixMaxTime := maxTime + + // Test both aTime and mTime set to Unix Epoch + Chtimes(file, unixEpochTime, unixEpochTime) + + f, err := os.Stat(file) + if err != nil { + t.Fatal(err) + } + + stat := f.Sys().(*syscall.Stat_t) + aTime := time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec)) + if aTime != unixEpochTime { + t.Fatalf("Expected: %s, got: %s", unixEpochTime, aTime) + } + + // Test aTime before Unix Epoch and mTime set to Unix Epoch + Chtimes(file, beforeUnixEpochTime, unixEpochTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + stat = f.Sys().(*syscall.Stat_t) + aTime = time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec)) + if aTime != unixEpochTime { + t.Fatalf("Expected: %s, got: %s", unixEpochTime, aTime) + } + + // Test aTime set to Unix Epoch and mTime before Unix Epoch + Chtimes(file, unixEpochTime, beforeUnixEpochTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + stat = f.Sys().(*syscall.Stat_t) + aTime = time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec)) + if aTime != unixEpochTime { + t.Fatalf("Expected: %s, got: %s", unixEpochTime, aTime) + } + + // Test both aTime and mTime set to after Unix Epoch (valid time) + Chtimes(file, afterUnixEpochTime, afterUnixEpochTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + stat = f.Sys().(*syscall.Stat_t) + aTime = time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec)) + if aTime != afterUnixEpochTime { + t.Fatalf("Expected: %s, got: %s", afterUnixEpochTime, aTime) + } + + // Test both aTime and mTime set to Unix max time + Chtimes(file, unixMaxTime, unixMaxTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + stat = f.Sys().(*syscall.Stat_t) + aTime = time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec)) + if aTime.Truncate(time.Second) != unixMaxTime.Truncate(time.Second) { + t.Fatalf("Expected: %s, got: %s", unixMaxTime.Truncate(time.Second), aTime.Truncate(time.Second)) + } +} diff --git a/pkg/system/chtimes_windows.go b/pkg/system/chtimes_windows.go new file mode 100644 index 00000000..29458684 --- /dev/null +++ b/pkg/system/chtimes_windows.go @@ -0,0 +1,27 @@ +// +build windows + +package system + +import ( + "syscall" + "time" +) + +//setCTime will set the create time on a file. On Windows, this requires +//calling SetFileTime and explicitly including the create time. +func setCTime(path string, ctime time.Time) error { + ctimespec := syscall.NsecToTimespec(ctime.UnixNano()) + pathp, e := syscall.UTF16PtrFromString(path) + if e != nil { + return e + } + h, e := syscall.CreateFile(pathp, + syscall.FILE_WRITE_ATTRIBUTES, syscall.FILE_SHARE_WRITE, nil, + syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS, 0) + if e != nil { + return e + } + defer syscall.Close(h) + c := syscall.NsecToFiletime(syscall.TimespecToNsec(ctimespec)) + return syscall.SetFileTime(h, &c, nil, nil) +} diff --git a/pkg/system/chtimes_windows_test.go b/pkg/system/chtimes_windows_test.go new file mode 100644 index 00000000..be57558e --- /dev/null +++ b/pkg/system/chtimes_windows_test.go @@ -0,0 +1,86 @@ +// +build windows + +package system + +import ( + "os" + "syscall" + "testing" + "time" +) + +// TestChtimes tests Chtimes access time on a tempfile on Windows +func TestChtimesWindows(t *testing.T) { + file, dir := prepareTempFile(t) + defer os.RemoveAll(dir) + + beforeUnixEpochTime := time.Unix(0, 0).Add(-100 * time.Second) + unixEpochTime := time.Unix(0, 0) + afterUnixEpochTime := time.Unix(100, 0) + unixMaxTime := maxTime + + // Test both aTime and mTime set to Unix Epoch + Chtimes(file, unixEpochTime, unixEpochTime) + + f, err := os.Stat(file) + if err != nil { + t.Fatal(err) + } + + aTime := time.Unix(0, f.Sys().(*syscall.Win32FileAttributeData).LastAccessTime.Nanoseconds()) + if aTime != unixEpochTime { + t.Fatalf("Expected: %s, got: %s", unixEpochTime, aTime) + } + + // Test aTime before Unix Epoch and mTime set to Unix Epoch + Chtimes(file, beforeUnixEpochTime, unixEpochTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + aTime = time.Unix(0, f.Sys().(*syscall.Win32FileAttributeData).LastAccessTime.Nanoseconds()) + if aTime != unixEpochTime { + t.Fatalf("Expected: %s, got: %s", unixEpochTime, aTime) + } + + // Test aTime set to Unix Epoch and mTime before Unix Epoch + Chtimes(file, unixEpochTime, beforeUnixEpochTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + aTime = time.Unix(0, f.Sys().(*syscall.Win32FileAttributeData).LastAccessTime.Nanoseconds()) + if aTime != unixEpochTime { + t.Fatalf("Expected: %s, got: %s", unixEpochTime, aTime) + } + + // Test both aTime and mTime set to after Unix Epoch (valid time) + Chtimes(file, afterUnixEpochTime, afterUnixEpochTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + aTime = time.Unix(0, f.Sys().(*syscall.Win32FileAttributeData).LastAccessTime.Nanoseconds()) + if aTime != afterUnixEpochTime { + t.Fatalf("Expected: %s, got: %s", afterUnixEpochTime, aTime) + } + + // Test both aTime and mTime set to Unix max time + Chtimes(file, unixMaxTime, unixMaxTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + aTime = time.Unix(0, f.Sys().(*syscall.Win32FileAttributeData).LastAccessTime.Nanoseconds()) + if aTime.Truncate(time.Second) != unixMaxTime.Truncate(time.Second) { + t.Fatalf("Expected: %s, got: %s", unixMaxTime.Truncate(time.Second), aTime.Truncate(time.Second)) + } +} diff --git a/pkg/system/errors.go b/pkg/system/errors.go new file mode 100644 index 00000000..28831898 --- /dev/null +++ b/pkg/system/errors.go @@ -0,0 +1,10 @@ +package system + +import ( + "errors" +) + +var ( + // ErrNotSupportedPlatform means the platform is not supported. + ErrNotSupportedPlatform = errors.New("platform and architecture is not supported") +) diff --git a/pkg/system/events_windows.go b/pkg/system/events_windows.go new file mode 100644 index 00000000..04e2de78 --- /dev/null +++ b/pkg/system/events_windows.go @@ -0,0 +1,83 @@ +package system + +// This file implements syscalls for Win32 events which are not implemented +// in golang. + +import ( + "syscall" + "unsafe" +) + +var ( + procCreateEvent = modkernel32.NewProc("CreateEventW") + procOpenEvent = modkernel32.NewProc("OpenEventW") + procSetEvent = modkernel32.NewProc("SetEvent") + procResetEvent = modkernel32.NewProc("ResetEvent") + procPulseEvent = modkernel32.NewProc("PulseEvent") +) + +// CreateEvent implements win32 CreateEventW func in golang. It will create an event object. +func CreateEvent(eventAttributes *syscall.SecurityAttributes, manualReset bool, initialState bool, name string) (handle syscall.Handle, err error) { + namep, _ := syscall.UTF16PtrFromString(name) + var _p1 uint32 + if manualReset { + _p1 = 1 + } + var _p2 uint32 + if initialState { + _p2 = 1 + } + r0, _, e1 := procCreateEvent.Call(uintptr(unsafe.Pointer(eventAttributes)), uintptr(_p1), uintptr(_p2), uintptr(unsafe.Pointer(namep))) + use(unsafe.Pointer(namep)) + handle = syscall.Handle(r0) + if handle == syscall.InvalidHandle { + err = e1 + } + return +} + +// OpenEvent implements win32 OpenEventW func in golang. It opens an event object. +func OpenEvent(desiredAccess uint32, inheritHandle bool, name string) (handle syscall.Handle, err error) { + namep, _ := syscall.UTF16PtrFromString(name) + var _p1 uint32 + if inheritHandle { + _p1 = 1 + } + r0, _, e1 := procOpenEvent.Call(uintptr(desiredAccess), uintptr(_p1), uintptr(unsafe.Pointer(namep))) + use(unsafe.Pointer(namep)) + handle = syscall.Handle(r0) + if handle == syscall.InvalidHandle { + err = e1 + } + return +} + +// SetEvent implements win32 SetEvent func in golang. +func SetEvent(handle syscall.Handle) (err error) { + return setResetPulse(handle, procSetEvent) +} + +// ResetEvent implements win32 ResetEvent func in golang. +func ResetEvent(handle syscall.Handle) (err error) { + return setResetPulse(handle, procResetEvent) +} + +// PulseEvent implements win32 PulseEvent func in golang. +func PulseEvent(handle syscall.Handle) (err error) { + return setResetPulse(handle, procPulseEvent) +} + +func setResetPulse(handle syscall.Handle, proc *syscall.LazyProc) (err error) { + r0, _, _ := proc.Call(uintptr(handle)) + if r0 != 0 { + err = syscall.Errno(r0) + } + return +} + +var temp unsafe.Pointer + +// use ensures a variable is kept alive without the GC freeing while still needed +func use(p unsafe.Pointer) { + temp = p +} diff --git a/pkg/system/filesys.go b/pkg/system/filesys.go new file mode 100644 index 00000000..c14feb84 --- /dev/null +++ b/pkg/system/filesys.go @@ -0,0 +1,19 @@ +// +build !windows + +package system + +import ( + "os" + "path/filepath" +) + +// MkdirAll creates a directory named path along with any necessary parents, +// with permission specified by attribute perm for all dir created. +func MkdirAll(path string, perm os.FileMode) error { + return os.MkdirAll(path, perm) +} + +// IsAbs is a platform-specific wrapper for filepath.IsAbs. +func IsAbs(path string) bool { + return filepath.IsAbs(path) +} diff --git a/pkg/system/filesys_windows.go b/pkg/system/filesys_windows.go new file mode 100644 index 00000000..16823d55 --- /dev/null +++ b/pkg/system/filesys_windows.go @@ -0,0 +1,82 @@ +// +build windows + +package system + +import ( + "os" + "path/filepath" + "regexp" + "strings" + "syscall" +) + +// MkdirAll implementation that is volume path aware for Windows. +func MkdirAll(path string, perm os.FileMode) error { + if re := regexp.MustCompile(`^\\\\\?\\Volume{[a-z0-9-]+}$`); re.MatchString(path) { + return nil + } + + // The rest of this method is copied from os.MkdirAll and should be kept + // as-is to ensure compatibility. + + // Fast path: if we can tell whether path is a directory or file, stop with success or error. + dir, err := os.Stat(path) + if err == nil { + if dir.IsDir() { + return nil + } + return &os.PathError{ + Op: "mkdir", + Path: path, + Err: syscall.ENOTDIR, + } + } + + // Slow path: make sure parent exists and then call Mkdir for path. + i := len(path) + for i > 0 && os.IsPathSeparator(path[i-1]) { // Skip trailing path separator. + i-- + } + + j := i + for j > 0 && !os.IsPathSeparator(path[j-1]) { // Scan backward over element. + j-- + } + + if j > 1 { + // Create parent + err = MkdirAll(path[0:j-1], perm) + if err != nil { + return err + } + } + + // Parent now exists; invoke Mkdir and use its result. + err = os.Mkdir(path, perm) + if err != nil { + // Handle arguments like "foo/." by + // double-checking that directory doesn't exist. + dir, err1 := os.Lstat(path) + if err1 == nil && dir.IsDir() { + return nil + } + return err + } + return nil +} + +// IsAbs is a platform-specific wrapper for filepath.IsAbs. On Windows, +// golang filepath.IsAbs does not consider a path \windows\system32 as absolute +// as it doesn't start with a drive-letter/colon combination. However, in +// docker we need to verify things such as WORKDIR /windows/system32 in +// a Dockerfile (which gets translated to \windows\system32 when being processed +// by the daemon. This SHOULD be treated as absolute from a docker processing +// perspective. +func IsAbs(path string) bool { + if !filepath.IsAbs(path) { + if !strings.HasPrefix(path, string(os.PathSeparator)) { + return false + } + } + return true +} diff --git a/pkg/system/lstat.go b/pkg/system/lstat.go new file mode 100644 index 00000000..bd23c4d5 --- /dev/null +++ b/pkg/system/lstat.go @@ -0,0 +1,19 @@ +// +build !windows + +package system + +import ( + "syscall" +) + +// Lstat takes a path to a file and returns +// a system.StatT type pertaining to that file. +// +// Throws an error if the file does not exist +func Lstat(path string) (*StatT, error) { + s := &syscall.Stat_t{} + if err := syscall.Lstat(path, s); err != nil { + return nil, err + } + return fromStatT(s) +} diff --git a/pkg/system/lstat_unix_test.go b/pkg/system/lstat_unix_test.go new file mode 100644 index 00000000..062cf53b --- /dev/null +++ b/pkg/system/lstat_unix_test.go @@ -0,0 +1,30 @@ +// +build linux freebsd + +package system + +import ( + "os" + "testing" +) + +// TestLstat tests Lstat for existing and non existing files +func TestLstat(t *testing.T) { + file, invalid, _, dir := prepareFiles(t) + defer os.RemoveAll(dir) + + statFile, err := Lstat(file) + if err != nil { + t.Fatal(err) + } + if statFile == nil { + t.Fatal("returned empty stat for existing file") + } + + statInvalid, err := Lstat(invalid) + if err == nil { + t.Fatal("did not return error for non-existing file") + } + if statInvalid != nil { + t.Fatal("returned non-nil stat for non-existing file") + } +} diff --git a/pkg/system/lstat_windows.go b/pkg/system/lstat_windows.go new file mode 100644 index 00000000..49e87eb4 --- /dev/null +++ b/pkg/system/lstat_windows.go @@ -0,0 +1,25 @@ +// +build windows + +package system + +import ( + "os" +) + +// Lstat calls os.Lstat to get a fileinfo interface back. +// This is then copied into our own locally defined structure. +// Note the Linux version uses fromStatT to do the copy back, +// but that not strictly necessary when already in an OS specific module. +func Lstat(path string) (*StatT, error) { + fi, err := os.Lstat(path) + if err != nil { + return nil, err + } + + return &StatT{ + name: fi.Name(), + size: fi.Size(), + mode: fi.Mode(), + modTime: fi.ModTime(), + isDir: fi.IsDir()}, nil +} diff --git a/pkg/system/meminfo.go b/pkg/system/meminfo.go new file mode 100644 index 00000000..3b6e947e --- /dev/null +++ b/pkg/system/meminfo.go @@ -0,0 +1,17 @@ +package system + +// MemInfo contains memory statistics of the host system. +type MemInfo struct { + // Total usable RAM (i.e. physical RAM minus a few reserved bits and the + // kernel binary code). + MemTotal int64 + + // Amount of free memory. + MemFree int64 + + // Total amount of swap space available. + SwapTotal int64 + + // Amount of swap space that is currently unused. + SwapFree int64 +} diff --git a/pkg/system/meminfo_linux.go b/pkg/system/meminfo_linux.go new file mode 100644 index 00000000..385f1d5e --- /dev/null +++ b/pkg/system/meminfo_linux.go @@ -0,0 +1,65 @@ +package system + +import ( + "bufio" + "io" + "os" + "strconv" + "strings" + + "github.com/docker/go-units" +) + +// ReadMemInfo retrieves memory statistics of the host system and returns a +// MemInfo type. +func ReadMemInfo() (*MemInfo, error) { + file, err := os.Open("/proc/meminfo") + if err != nil { + return nil, err + } + defer file.Close() + return parseMemInfo(file) +} + +// parseMemInfo parses the /proc/meminfo file into +// a MemInfo object given an io.Reader to the file. +// Throws error if there are problems reading from the file +func parseMemInfo(reader io.Reader) (*MemInfo, error) { + meminfo := &MemInfo{} + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + // Expected format: ["MemTotal:", "1234", "kB"] + parts := strings.Fields(scanner.Text()) + + // Sanity checks: Skip malformed entries. + if len(parts) < 3 || parts[2] != "kB" { + continue + } + + // Convert to bytes. + size, err := strconv.Atoi(parts[1]) + if err != nil { + continue + } + bytes := int64(size) * units.KiB + + switch parts[0] { + case "MemTotal:": + meminfo.MemTotal = bytes + case "MemFree:": + meminfo.MemFree = bytes + case "SwapTotal:": + meminfo.SwapTotal = bytes + case "SwapFree:": + meminfo.SwapFree = bytes + } + + } + + // Handle errors that may have occurred during the reading of the file. + if err := scanner.Err(); err != nil { + return nil, err + } + + return meminfo, nil +} diff --git a/pkg/system/meminfo_unix_test.go b/pkg/system/meminfo_unix_test.go new file mode 100644 index 00000000..44f55628 --- /dev/null +++ b/pkg/system/meminfo_unix_test.go @@ -0,0 +1,40 @@ +// +build linux freebsd + +package system + +import ( + "strings" + "testing" + + "github.com/docker/go-units" +) + +// TestMemInfo tests parseMemInfo with a static meminfo string +func TestMemInfo(t *testing.T) { + const input = ` + MemTotal: 1 kB + MemFree: 2 kB + SwapTotal: 3 kB + SwapFree: 4 kB + Malformed1: + Malformed2: 1 + Malformed3: 2 MB + Malformed4: X kB + ` + meminfo, err := parseMemInfo(strings.NewReader(input)) + if err != nil { + t.Fatal(err) + } + if meminfo.MemTotal != 1*units.KiB { + t.Fatalf("Unexpected MemTotal: %d", meminfo.MemTotal) + } + if meminfo.MemFree != 2*units.KiB { + t.Fatalf("Unexpected MemFree: %d", meminfo.MemFree) + } + if meminfo.SwapTotal != 3*units.KiB { + t.Fatalf("Unexpected SwapTotal: %d", meminfo.SwapTotal) + } + if meminfo.SwapFree != 4*units.KiB { + t.Fatalf("Unexpected SwapFree: %d", meminfo.SwapFree) + } +} diff --git a/pkg/system/meminfo_unsupported.go b/pkg/system/meminfo_unsupported.go new file mode 100644 index 00000000..82ddd30c --- /dev/null +++ b/pkg/system/meminfo_unsupported.go @@ -0,0 +1,8 @@ +// +build !linux,!windows + +package system + +// ReadMemInfo is not supported on platforms other than linux and windows. +func ReadMemInfo() (*MemInfo, error) { + return nil, ErrNotSupportedPlatform +} diff --git a/pkg/system/meminfo_windows.go b/pkg/system/meminfo_windows.go new file mode 100644 index 00000000..d4664259 --- /dev/null +++ b/pkg/system/meminfo_windows.go @@ -0,0 +1,44 @@ +package system + +import ( + "syscall" + "unsafe" +) + +var ( + modkernel32 = syscall.NewLazyDLL("kernel32.dll") + + procGlobalMemoryStatusEx = modkernel32.NewProc("GlobalMemoryStatusEx") +) + +// https://msdn.microsoft.com/en-us/library/windows/desktop/aa366589(v=vs.85).aspx +// https://msdn.microsoft.com/en-us/library/windows/desktop/aa366770(v=vs.85).aspx +type memorystatusex struct { + dwLength uint32 + dwMemoryLoad uint32 + ullTotalPhys uint64 + ullAvailPhys uint64 + ullTotalPageFile uint64 + ullAvailPageFile uint64 + ullTotalVirtual uint64 + ullAvailVirtual uint64 + ullAvailExtendedVirtual uint64 +} + +// ReadMemInfo retrieves memory statistics of the host system and returns a +// MemInfo type. +func ReadMemInfo() (*MemInfo, error) { + msi := &memorystatusex{ + dwLength: 64, + } + r1, _, _ := procGlobalMemoryStatusEx.Call(uintptr(unsafe.Pointer(msi))) + if r1 == 0 { + return &MemInfo{}, nil + } + return &MemInfo{ + MemTotal: int64(msi.ullTotalPhys), + MemFree: int64(msi.ullAvailPhys), + SwapTotal: int64(msi.ullTotalPageFile), + SwapFree: int64(msi.ullAvailPageFile), + }, nil +} diff --git a/pkg/system/mknod.go b/pkg/system/mknod.go new file mode 100644 index 00000000..73958182 --- /dev/null +++ b/pkg/system/mknod.go @@ -0,0 +1,22 @@ +// +build !windows + +package system + +import ( + "syscall" +) + +// Mknod creates a filesystem node (file, device special file or named pipe) named path +// with attributes specified by mode and dev. +func Mknod(path string, mode uint32, dev int) error { + return syscall.Mknod(path, mode, dev) +} + +// Mkdev is used to build the value of linux devices (in /dev/) which specifies major +// and minor number of the newly created device special file. +// Linux device nodes are a bit weird due to backwards compat with 16 bit device nodes. +// They are, from low to high: the lower 8 bits of the minor, then 12 bits of the major, +// then the top 12 bits of the minor. +func Mkdev(major int64, minor int64) uint32 { + return uint32(((minor & 0xfff00) << 12) | ((major & 0xfff) << 8) | (minor & 0xff)) +} diff --git a/pkg/system/mknod_windows.go b/pkg/system/mknod_windows.go new file mode 100644 index 00000000..2e863c02 --- /dev/null +++ b/pkg/system/mknod_windows.go @@ -0,0 +1,13 @@ +// +build windows + +package system + +// Mknod is not implemented on Windows. +func Mknod(path string, mode uint32, dev int) error { + return ErrNotSupportedPlatform +} + +// Mkdev is not implemented on Windows. +func Mkdev(major int64, minor int64) uint32 { + panic("Mkdev not implemented on Windows.") +} diff --git a/pkg/system/path_unix.go b/pkg/system/path_unix.go new file mode 100644 index 00000000..1b6cc9cb --- /dev/null +++ b/pkg/system/path_unix.go @@ -0,0 +1,8 @@ +// +build !windows + +package system + +// DefaultPathEnv is unix style list of directories to search for +// executables. Each directory is separated from the next by a colon +// ':' character . +const DefaultPathEnv = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" diff --git a/pkg/system/path_windows.go b/pkg/system/path_windows.go new file mode 100644 index 00000000..09e7f89f --- /dev/null +++ b/pkg/system/path_windows.go @@ -0,0 +1,7 @@ +// +build windows + +package system + +// DefaultPathEnv is deliberately empty on Windows as the default path will be set by +// the container. Docker has no context of what the default path should be. +const DefaultPathEnv = "" diff --git a/pkg/system/stat.go b/pkg/system/stat.go new file mode 100644 index 00000000..087034c5 --- /dev/null +++ b/pkg/system/stat.go @@ -0,0 +1,53 @@ +// +build !windows + +package system + +import ( + "syscall" +) + +// StatT type contains status of a file. It contains metadata +// like permission, owner, group, size, etc about a file. +type StatT struct { + mode uint32 + uid uint32 + gid uint32 + rdev uint64 + size int64 + mtim syscall.Timespec +} + +// Mode returns file's permission mode. +func (s StatT) Mode() uint32 { + return s.mode +} + +// UID returns file's user id of owner. +func (s StatT) UID() uint32 { + return s.uid +} + +// GID returns file's group id of owner. +func (s StatT) GID() uint32 { + return s.gid +} + +// Rdev returns file's device ID (if it's special file). +func (s StatT) Rdev() uint64 { + return s.rdev +} + +// Size returns file's size. +func (s StatT) Size() int64 { + return s.size +} + +// Mtim returns file's last modification time. +func (s StatT) Mtim() syscall.Timespec { + return s.mtim +} + +// GetLastModification returns file's last modification time. +func (s StatT) GetLastModification() syscall.Timespec { + return s.Mtim() +} diff --git a/pkg/system/stat_freebsd.go b/pkg/system/stat_freebsd.go new file mode 100644 index 00000000..d0fb6f15 --- /dev/null +++ b/pkg/system/stat_freebsd.go @@ -0,0 +1,27 @@ +package system + +import ( + "syscall" +) + +// fromStatT converts a syscall.Stat_t type to a system.Stat_t type +func fromStatT(s *syscall.Stat_t) (*StatT, error) { + return &StatT{size: s.Size, + mode: uint32(s.Mode), + uid: s.Uid, + gid: s.Gid, + rdev: uint64(s.Rdev), + mtim: s.Mtimespec}, nil +} + +// Stat takes a path to a file and returns +// a system.Stat_t type pertaining to that file. +// +// Throws an error if the file does not exist +func Stat(path string) (*StatT, error) { + s := &syscall.Stat_t{} + if err := syscall.Stat(path, s); err != nil { + return nil, err + } + return fromStatT(s) +} diff --git a/pkg/system/stat_linux.go b/pkg/system/stat_linux.go new file mode 100644 index 00000000..8b1eded1 --- /dev/null +++ b/pkg/system/stat_linux.go @@ -0,0 +1,33 @@ +package system + +import ( + "syscall" +) + +// fromStatT converts a syscall.Stat_t type to a system.Stat_t type +func fromStatT(s *syscall.Stat_t) (*StatT, error) { + return &StatT{size: s.Size, + mode: s.Mode, + uid: s.Uid, + gid: s.Gid, + rdev: s.Rdev, + mtim: s.Mtim}, nil +} + +// FromStatT exists only on linux, and loads a system.StatT from a +// syscal.Stat_t. +func FromStatT(s *syscall.Stat_t) (*StatT, error) { + return fromStatT(s) +} + +// Stat takes a path to a file and returns +// a system.StatT type pertaining to that file. +// +// Throws an error if the file does not exist +func Stat(path string) (*StatT, error) { + s := &syscall.Stat_t{} + if err := syscall.Stat(path, s); err != nil { + return nil, err + } + return fromStatT(s) +} diff --git a/pkg/system/stat_openbsd.go b/pkg/system/stat_openbsd.go new file mode 100644 index 00000000..3c3b71fb --- /dev/null +++ b/pkg/system/stat_openbsd.go @@ -0,0 +1,15 @@ +package system + +import ( + "syscall" +) + +// fromStatT creates a system.StatT type from a syscall.Stat_t type +func fromStatT(s *syscall.Stat_t) (*StatT, error) { + return &StatT{size: s.Size, + mode: uint32(s.Mode), + uid: s.Uid, + gid: s.Gid, + rdev: uint64(s.Rdev), + mtim: s.Mtim}, nil +} diff --git a/pkg/system/stat_solaris.go b/pkg/system/stat_solaris.go new file mode 100644 index 00000000..b01d08ac --- /dev/null +++ b/pkg/system/stat_solaris.go @@ -0,0 +1,17 @@ +// +build solaris + +package system + +import ( + "syscall" +) + +// fromStatT creates a system.StatT type from a syscall.Stat_t type +func fromStatT(s *syscall.Stat_t) (*StatT, error) { + return &StatT{size: s.Size, + mode: uint32(s.Mode), + uid: s.Uid, + gid: s.Gid, + rdev: uint64(s.Rdev), + mtim: s.Mtim}, nil +} diff --git a/pkg/system/stat_unix_test.go b/pkg/system/stat_unix_test.go new file mode 100644 index 00000000..dee8d30a --- /dev/null +++ b/pkg/system/stat_unix_test.go @@ -0,0 +1,39 @@ +// +build linux freebsd + +package system + +import ( + "os" + "syscall" + "testing" +) + +// TestFromStatT tests fromStatT for a tempfile +func TestFromStatT(t *testing.T) { + file, _, _, dir := prepareFiles(t) + defer os.RemoveAll(dir) + + stat := &syscall.Stat_t{} + err := syscall.Lstat(file, stat) + + s, err := fromStatT(stat) + if err != nil { + t.Fatal(err) + } + + if stat.Mode != s.Mode() { + t.Fatal("got invalid mode") + } + if stat.Uid != s.UID() { + t.Fatal("got invalid uid") + } + if stat.Gid != s.GID() { + t.Fatal("got invalid gid") + } + if stat.Rdev != s.Rdev() { + t.Fatal("got invalid rdev") + } + if stat.Mtim != s.Mtim() { + t.Fatal("got invalid mtim") + } +} diff --git a/pkg/system/stat_unsupported.go b/pkg/system/stat_unsupported.go new file mode 100644 index 00000000..f53e9de4 --- /dev/null +++ b/pkg/system/stat_unsupported.go @@ -0,0 +1,17 @@ +// +build !linux,!windows,!freebsd,!solaris,!openbsd + +package system + +import ( + "syscall" +) + +// fromStatT creates a system.StatT type from a syscall.Stat_t type +func fromStatT(s *syscall.Stat_t) (*StatT, error) { + return &StatT{size: s.Size, + mode: uint32(s.Mode), + uid: s.Uid, + gid: s.Gid, + rdev: uint64(s.Rdev), + mtim: s.Mtimespec}, nil +} diff --git a/pkg/system/stat_windows.go b/pkg/system/stat_windows.go new file mode 100644 index 00000000..39490c62 --- /dev/null +++ b/pkg/system/stat_windows.go @@ -0,0 +1,43 @@ +// +build windows + +package system + +import ( + "os" + "time" +) + +// StatT type contains status of a file. It contains metadata +// like name, permission, size, etc about a file. +type StatT struct { + name string + size int64 + mode os.FileMode + modTime time.Time + isDir bool +} + +// Name returns file's name. +func (s StatT) Name() string { + return s.name +} + +// Size returns file's size. +func (s StatT) Size() int64 { + return s.size +} + +// Mode returns file's permission mode. +func (s StatT) Mode() os.FileMode { + return s.mode +} + +// ModTime returns file's last modification time. +func (s StatT) ModTime() time.Time { + return s.modTime +} + +// IsDir returns whether file is actually a directory. +func (s StatT) IsDir() bool { + return s.isDir +} diff --git a/pkg/system/syscall_unix.go b/pkg/system/syscall_unix.go new file mode 100644 index 00000000..3ae91284 --- /dev/null +++ b/pkg/system/syscall_unix.go @@ -0,0 +1,17 @@ +// +build linux freebsd + +package system + +import "syscall" + +// Unmount is a platform-specific helper function to call +// the unmount syscall. +func Unmount(dest string) error { + return syscall.Unmount(dest, 0) +} + +// CommandLineToArgv should not be used on Unix. +// It simply returns commandLine in the only element in the returned array. +func CommandLineToArgv(commandLine string) ([]string, error) { + return []string{commandLine}, nil +} diff --git a/pkg/system/syscall_windows.go b/pkg/system/syscall_windows.go new file mode 100644 index 00000000..061e220f --- /dev/null +++ b/pkg/system/syscall_windows.go @@ -0,0 +1,60 @@ +package system + +import ( + "fmt" + "syscall" + "unsafe" +) + +// OSVersion is a wrapper for Windows version information +// https://msdn.microsoft.com/en-us/library/windows/desktop/ms724439(v=vs.85).aspx +type OSVersion struct { + Version uint32 + MajorVersion uint8 + MinorVersion uint8 + Build uint16 +} + +// GetOSVersion gets the operating system version on Windows. Note that +// docker.exe must be manifested to get the correct version information. +func GetOSVersion() (OSVersion, error) { + var err error + osv := OSVersion{} + osv.Version, err = syscall.GetVersion() + if err != nil { + return osv, fmt.Errorf("Failed to call GetVersion()") + } + osv.MajorVersion = uint8(osv.Version & 0xFF) + osv.MinorVersion = uint8(osv.Version >> 8 & 0xFF) + osv.Build = uint16(osv.Version >> 16) + return osv, nil +} + +// Unmount is a platform-specific helper function to call +// the unmount syscall. Not supported on Windows +func Unmount(dest string) error { + return nil +} + +// CommandLineToArgv wraps the Windows syscall to turn a commandline into an argument array. +func CommandLineToArgv(commandLine string) ([]string, error) { + var argc int32 + + argsPtr, err := syscall.UTF16PtrFromString(commandLine) + if err != nil { + return nil, err + } + + argv, err := syscall.CommandLineToArgv(argsPtr, &argc) + if err != nil { + return nil, err + } + defer syscall.LocalFree(syscall.Handle(uintptr(unsafe.Pointer(argv)))) + + newArgs := make([]string, argc) + for i, v := range (*argv)[:argc] { + newArgs[i] = string(syscall.UTF16ToString((*v)[:])) + } + + return newArgs, nil +} diff --git a/pkg/system/umask.go b/pkg/system/umask.go new file mode 100644 index 00000000..c670fcd7 --- /dev/null +++ b/pkg/system/umask.go @@ -0,0 +1,13 @@ +// +build !windows + +package system + +import ( + "syscall" +) + +// Umask sets current process's file mode creation mask to newmask +// and return oldmask. +func Umask(newmask int) (oldmask int, err error) { + return syscall.Umask(newmask), nil +} diff --git a/pkg/system/umask_windows.go b/pkg/system/umask_windows.go new file mode 100644 index 00000000..13f1de17 --- /dev/null +++ b/pkg/system/umask_windows.go @@ -0,0 +1,9 @@ +// +build windows + +package system + +// Umask is not supported on the windows platform. +func Umask(newmask int) (oldmask int, err error) { + // should not be called on cli code path + return 0, ErrNotSupportedPlatform +} diff --git a/pkg/system/utimes_darwin.go b/pkg/system/utimes_darwin.go new file mode 100644 index 00000000..0a161975 --- /dev/null +++ b/pkg/system/utimes_darwin.go @@ -0,0 +1,8 @@ +package system + +import "syscall" + +// LUtimesNano is not supported by darwin platform. +func LUtimesNano(path string, ts []syscall.Timespec) error { + return ErrNotSupportedPlatform +} diff --git a/pkg/system/utimes_freebsd.go b/pkg/system/utimes_freebsd.go new file mode 100644 index 00000000..e2eac3b5 --- /dev/null +++ b/pkg/system/utimes_freebsd.go @@ -0,0 +1,22 @@ +package system + +import ( + "syscall" + "unsafe" +) + +// LUtimesNano is used to change access and modification time of the specified path. +// It's used for symbol link file because syscall.UtimesNano doesn't support a NOFOLLOW flag atm. +func LUtimesNano(path string, ts []syscall.Timespec) error { + var _path *byte + _path, err := syscall.BytePtrFromString(path) + if err != nil { + return err + } + + if _, _, err := syscall.Syscall(syscall.SYS_LUTIMES, uintptr(unsafe.Pointer(_path)), uintptr(unsafe.Pointer(&ts[0])), 0); err != 0 && err != syscall.ENOSYS { + return err + } + + return nil +} diff --git a/pkg/system/utimes_linux.go b/pkg/system/utimes_linux.go new file mode 100644 index 00000000..fc8a1aba --- /dev/null +++ b/pkg/system/utimes_linux.go @@ -0,0 +1,26 @@ +package system + +import ( + "syscall" + "unsafe" +) + +// LUtimesNano is used to change access and modification time of the specified path. +// It's used for symbol link file because syscall.UtimesNano doesn't support a NOFOLLOW flag atm. +func LUtimesNano(path string, ts []syscall.Timespec) error { + // These are not currently available in syscall + atFdCwd := -100 + atSymLinkNoFollow := 0x100 + + var _path *byte + _path, err := syscall.BytePtrFromString(path) + if err != nil { + return err + } + + if _, _, err := syscall.Syscall6(syscall.SYS_UTIMENSAT, uintptr(atFdCwd), uintptr(unsafe.Pointer(_path)), uintptr(unsafe.Pointer(&ts[0])), uintptr(atSymLinkNoFollow), 0, 0); err != 0 && err != syscall.ENOSYS { + return err + } + + return nil +} diff --git a/pkg/system/utimes_unix_test.go b/pkg/system/utimes_unix_test.go new file mode 100644 index 00000000..1ee0d099 --- /dev/null +++ b/pkg/system/utimes_unix_test.go @@ -0,0 +1,68 @@ +// +build linux freebsd + +package system + +import ( + "io/ioutil" + "os" + "path/filepath" + "syscall" + "testing" +) + +// prepareFiles creates files for testing in the temp directory +func prepareFiles(t *testing.T) (string, string, string, string) { + dir, err := ioutil.TempDir("", "docker-system-test") + if err != nil { + t.Fatal(err) + } + + file := filepath.Join(dir, "exist") + if err := ioutil.WriteFile(file, []byte("hello"), 0644); err != nil { + t.Fatal(err) + } + + invalid := filepath.Join(dir, "doesnt-exist") + + symlink := filepath.Join(dir, "symlink") + if err := os.Symlink(file, symlink); err != nil { + t.Fatal(err) + } + + return file, invalid, symlink, dir +} + +func TestLUtimesNano(t *testing.T) { + file, invalid, symlink, dir := prepareFiles(t) + defer os.RemoveAll(dir) + + before, err := os.Stat(file) + if err != nil { + t.Fatal(err) + } + + ts := []syscall.Timespec{{0, 0}, {0, 0}} + if err := LUtimesNano(symlink, ts); err != nil { + t.Fatal(err) + } + + symlinkInfo, err := os.Lstat(symlink) + if err != nil { + t.Fatal(err) + } + if before.ModTime().Unix() == symlinkInfo.ModTime().Unix() { + t.Fatal("The modification time of the symlink should be different") + } + + fileInfo, err := os.Stat(file) + if err != nil { + t.Fatal(err) + } + if before.ModTime().Unix() != fileInfo.ModTime().Unix() { + t.Fatal("The modification time of the file should be same") + } + + if err := LUtimesNano(invalid, ts); err == nil { + t.Fatal("Doesn't return an error on a non-existing file") + } +} diff --git a/pkg/system/utimes_unsupported.go b/pkg/system/utimes_unsupported.go new file mode 100644 index 00000000..50c3a043 --- /dev/null +++ b/pkg/system/utimes_unsupported.go @@ -0,0 +1,10 @@ +// +build !linux,!freebsd,!darwin + +package system + +import "syscall" + +// LUtimesNano is not supported on platforms other than linux, freebsd and darwin. +func LUtimesNano(path string, ts []syscall.Timespec) error { + return ErrNotSupportedPlatform +} diff --git a/pkg/system/xattrs_linux.go b/pkg/system/xattrs_linux.go new file mode 100644 index 00000000..d2e2c057 --- /dev/null +++ b/pkg/system/xattrs_linux.go @@ -0,0 +1,63 @@ +package system + +import ( + "syscall" + "unsafe" +) + +// Lgetxattr retrieves the value of the extended attribute identified by attr +// and associated with the given path in the file system. +// It will returns a nil slice and nil error if the xattr is not set. +func Lgetxattr(path string, attr string) ([]byte, error) { + pathBytes, err := syscall.BytePtrFromString(path) + if err != nil { + return nil, err + } + attrBytes, err := syscall.BytePtrFromString(attr) + if err != nil { + return nil, err + } + + dest := make([]byte, 128) + destBytes := unsafe.Pointer(&dest[0]) + sz, _, errno := syscall.Syscall6(syscall.SYS_LGETXATTR, uintptr(unsafe.Pointer(pathBytes)), uintptr(unsafe.Pointer(attrBytes)), uintptr(destBytes), uintptr(len(dest)), 0, 0) + if errno == syscall.ENODATA { + return nil, nil + } + if errno == syscall.ERANGE { + dest = make([]byte, sz) + destBytes := unsafe.Pointer(&dest[0]) + sz, _, errno = syscall.Syscall6(syscall.SYS_LGETXATTR, uintptr(unsafe.Pointer(pathBytes)), uintptr(unsafe.Pointer(attrBytes)), uintptr(destBytes), uintptr(len(dest)), 0, 0) + } + if errno != 0 { + return nil, errno + } + + return dest[:sz], nil +} + +var _zero uintptr + +// Lsetxattr sets the value of the extended attribute identified by attr +// and associated with the given path in the file system. +func Lsetxattr(path string, attr string, data []byte, flags int) error { + pathBytes, err := syscall.BytePtrFromString(path) + if err != nil { + return err + } + attrBytes, err := syscall.BytePtrFromString(attr) + if err != nil { + return err + } + var dataBytes unsafe.Pointer + if len(data) > 0 { + dataBytes = unsafe.Pointer(&data[0]) + } else { + dataBytes = unsafe.Pointer(&_zero) + } + _, _, errno := syscall.Syscall6(syscall.SYS_LSETXATTR, uintptr(unsafe.Pointer(pathBytes)), uintptr(unsafe.Pointer(attrBytes)), uintptr(dataBytes), uintptr(len(data)), uintptr(flags), 0) + if errno != 0 { + return errno + } + return nil +} diff --git a/pkg/system/xattrs_unsupported.go b/pkg/system/xattrs_unsupported.go new file mode 100644 index 00000000..0114f222 --- /dev/null +++ b/pkg/system/xattrs_unsupported.go @@ -0,0 +1,13 @@ +// +build !linux + +package system + +// Lgetxattr is not supported on platforms other than linux. +func Lgetxattr(path string, attr string) ([]byte, error) { + return nil, ErrNotSupportedPlatform +} + +// Lsetxattr is not supported on platforms other than linux. +func Lsetxattr(path string, attr string, data []byte, flags int) error { + return ErrNotSupportedPlatform +} diff --git a/pkg/tailfile/tailfile.go b/pkg/tailfile/tailfile.go new file mode 100644 index 00000000..d580584d --- /dev/null +++ b/pkg/tailfile/tailfile.go @@ -0,0 +1,66 @@ +// Package tailfile provides helper functions to read the nth lines of any +// ReadSeeker. +package tailfile + +import ( + "bytes" + "errors" + "io" + "os" +) + +const blockSize = 1024 + +var eol = []byte("\n") + +// ErrNonPositiveLinesNumber is an error returned if the lines number was negative. +var ErrNonPositiveLinesNumber = errors.New("The number of lines to extract from the file must be positive") + +//TailFile returns last n lines of reader f (could be a fil). +func TailFile(f io.ReadSeeker, n int) ([][]byte, error) { + if n <= 0 { + return nil, ErrNonPositiveLinesNumber + } + size, err := f.Seek(0, os.SEEK_END) + if err != nil { + return nil, err + } + block := -1 + var data []byte + var cnt int + for { + var b []byte + step := int64(block * blockSize) + left := size + step // how many bytes to beginning + if left < 0 { + if _, err := f.Seek(0, os.SEEK_SET); err != nil { + return nil, err + } + b = make([]byte, blockSize+left) + if _, err := f.Read(b); err != nil { + return nil, err + } + data = append(b, data...) + break + } else { + b = make([]byte, blockSize) + if _, err := f.Seek(step, os.SEEK_END); err != nil { + return nil, err + } + if _, err := f.Read(b); err != nil { + return nil, err + } + data = append(b, data...) + } + cnt += bytes.Count(b, eol) + if cnt > n { + break + } + block-- + } + lines := bytes.Split(data, eol) + if n < len(lines) { + return lines[len(lines)-n-1 : len(lines)-1], nil + } + return lines[:len(lines)-1], nil +} diff --git a/pkg/tailfile/tailfile_test.go b/pkg/tailfile/tailfile_test.go new file mode 100644 index 00000000..31217c03 --- /dev/null +++ b/pkg/tailfile/tailfile_test.go @@ -0,0 +1,148 @@ +package tailfile + +import ( + "io/ioutil" + "os" + "testing" +) + +func TestTailFile(t *testing.T) { + f, err := ioutil.TempFile("", "tail-test") + if err != nil { + t.Fatal(err) + } + defer f.Close() + defer os.RemoveAll(f.Name()) + testFile := []byte(`first line +second line +third line +fourth line +fifth line +next first line +next second line +next third line +next fourth line +next fifth line +last first line +next first line +next second line +next third line +next fourth line +next fifth line +next first line +next second line +next third line +next fourth line +next fifth line +last second line +last third line +last fourth line +last fifth line +truncated line`) + if _, err := f.Write(testFile); err != nil { + t.Fatal(err) + } + if _, err := f.Seek(0, os.SEEK_SET); err != nil { + t.Fatal(err) + } + expected := []string{"last fourth line", "last fifth line"} + res, err := TailFile(f, 2) + if err != nil { + t.Fatal(err) + } + for i, l := range res { + t.Logf("%s", l) + if expected[i] != string(l) { + t.Fatalf("Expected line %s, got %s", expected[i], l) + } + } +} + +func TestTailFileManyLines(t *testing.T) { + f, err := ioutil.TempFile("", "tail-test") + if err != nil { + t.Fatal(err) + } + defer f.Close() + defer os.RemoveAll(f.Name()) + testFile := []byte(`first line +second line +truncated line`) + if _, err := f.Write(testFile); err != nil { + t.Fatal(err) + } + if _, err := f.Seek(0, os.SEEK_SET); err != nil { + t.Fatal(err) + } + expected := []string{"first line", "second line"} + res, err := TailFile(f, 10000) + if err != nil { + t.Fatal(err) + } + for i, l := range res { + t.Logf("%s", l) + if expected[i] != string(l) { + t.Fatalf("Expected line %s, got %s", expected[i], l) + } + } +} + +func TestTailEmptyFile(t *testing.T) { + f, err := ioutil.TempFile("", "tail-test") + if err != nil { + t.Fatal(err) + } + defer f.Close() + defer os.RemoveAll(f.Name()) + res, err := TailFile(f, 10000) + if err != nil { + t.Fatal(err) + } + if len(res) != 0 { + t.Fatal("Must be empty slice from empty file") + } +} + +func TestTailNegativeN(t *testing.T) { + f, err := ioutil.TempFile("", "tail-test") + if err != nil { + t.Fatal(err) + } + defer f.Close() + defer os.RemoveAll(f.Name()) + testFile := []byte(`first line +second line +truncated line`) + if _, err := f.Write(testFile); err != nil { + t.Fatal(err) + } + if _, err := f.Seek(0, os.SEEK_SET); err != nil { + t.Fatal(err) + } + if _, err := TailFile(f, -1); err != ErrNonPositiveLinesNumber { + t.Fatalf("Expected ErrNonPositiveLinesNumber, got %s", err) + } + if _, err := TailFile(f, 0); err != ErrNonPositiveLinesNumber { + t.Fatalf("Expected ErrNonPositiveLinesNumber, got %s", err) + } +} + +func BenchmarkTail(b *testing.B) { + f, err := ioutil.TempFile("", "tail-test") + if err != nil { + b.Fatal(err) + } + defer f.Close() + defer os.RemoveAll(f.Name()) + for i := 0; i < 10000; i++ { + if _, err := f.Write([]byte("tailfile pretty interesting line\n")); err != nil { + b.Fatal(err) + } + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, err := TailFile(f, 1000); err != nil { + b.Fatal(err) + } + } +} diff --git a/pkg/tarsum/builder_context.go b/pkg/tarsum/builder_context.go new file mode 100644 index 00000000..b42983e9 --- /dev/null +++ b/pkg/tarsum/builder_context.go @@ -0,0 +1,21 @@ +package tarsum + +// BuilderContext is an interface extending TarSum by adding the Remove method. +// In general there was concern about adding this method to TarSum itself +// so instead it is being added just to "BuilderContext" which will then +// only be used during the .dockerignore file processing +// - see builder/evaluator.go +type BuilderContext interface { + TarSum + Remove(string) +} + +func (bc *tarSum) Remove(filename string) { + for i, fis := range bc.sums { + if fis.Name() == filename { + bc.sums = append(bc.sums[:i], bc.sums[i+1:]...) + // Note, we don't just return because there could be + // more than one with this name + } + } +} diff --git a/pkg/tarsum/builder_context_test.go b/pkg/tarsum/builder_context_test.go new file mode 100644 index 00000000..719f7289 --- /dev/null +++ b/pkg/tarsum/builder_context_test.go @@ -0,0 +1,63 @@ +package tarsum + +import ( + "io" + "io/ioutil" + "os" + "testing" +) + +// Try to remove tarsum (in the BuilderContext) that do not exists, won't change a thing +func TestTarSumRemoveNonExistent(t *testing.T) { + filename := "testdata/46af0962ab5afeb5ce6740d4d91652e69206fc991fd5328c1a94d364ad00e457/layer.tar" + reader, err := os.Open(filename) + if err != nil { + t.Fatal(err) + } + ts, err := NewTarSum(reader, false, Version0) + if err != nil { + t.Fatal(err) + } + + // Read and discard bytes so that it populates sums + _, err = io.Copy(ioutil.Discard, ts) + if err != nil { + t.Errorf("failed to read from %s: %s", filename, err) + } + + expected := len(ts.GetSums()) + + ts.(BuilderContext).Remove("") + ts.(BuilderContext).Remove("Anything") + + if len(ts.GetSums()) != expected { + t.Fatalf("Expected %v sums, go %v.", expected, ts.GetSums()) + } +} + +// Remove a tarsum (in the BuilderContext) +func TestTarSumRemove(t *testing.T) { + filename := "testdata/46af0962ab5afeb5ce6740d4d91652e69206fc991fd5328c1a94d364ad00e457/layer.tar" + reader, err := os.Open(filename) + if err != nil { + t.Fatal(err) + } + ts, err := NewTarSum(reader, false, Version0) + if err != nil { + t.Fatal(err) + } + + // Read and discard bytes so that it populates sums + _, err = io.Copy(ioutil.Discard, ts) + if err != nil { + t.Errorf("failed to read from %s: %s", filename, err) + } + + expected := len(ts.GetSums()) - 1 + + ts.(BuilderContext).Remove("etc/sudoers") + + if len(ts.GetSums()) != expected { + t.Fatalf("Expected %v sums, go %v.", expected, len(ts.GetSums())) + } +} diff --git a/pkg/tarsum/fileinfosums.go b/pkg/tarsum/fileinfosums.go new file mode 100644 index 00000000..5abf5e7b --- /dev/null +++ b/pkg/tarsum/fileinfosums.go @@ -0,0 +1,126 @@ +package tarsum + +import "sort" + +// FileInfoSumInterface provides an interface for accessing file checksum +// information within a tar file. This info is accessed through interface +// so the actual name and sum cannot be melded with. +type FileInfoSumInterface interface { + // File name + Name() string + // Checksum of this particular file and its headers + Sum() string + // Position of file in the tar + Pos() int64 +} + +type fileInfoSum struct { + name string + sum string + pos int64 +} + +func (fis fileInfoSum) Name() string { + return fis.name +} +func (fis fileInfoSum) Sum() string { + return fis.sum +} +func (fis fileInfoSum) Pos() int64 { + return fis.pos +} + +// FileInfoSums provides a list of FileInfoSumInterfaces. +type FileInfoSums []FileInfoSumInterface + +// GetFile returns the first FileInfoSumInterface with a matching name. +func (fis FileInfoSums) GetFile(name string) FileInfoSumInterface { + for i := range fis { + if fis[i].Name() == name { + return fis[i] + } + } + return nil +} + +// GetAllFile returns a FileInfoSums with all matching names. +func (fis FileInfoSums) GetAllFile(name string) FileInfoSums { + f := FileInfoSums{} + for i := range fis { + if fis[i].Name() == name { + f = append(f, fis[i]) + } + } + return f +} + +// GetDuplicatePaths returns a FileInfoSums with all duplicated paths. +func (fis FileInfoSums) GetDuplicatePaths() (dups FileInfoSums) { + seen := make(map[string]int, len(fis)) // allocate earl. no need to grow this map. + for i := range fis { + f := fis[i] + if _, ok := seen[f.Name()]; ok { + dups = append(dups, f) + } else { + seen[f.Name()] = 0 + } + } + return dups +} + +// Len returns the size of the FileInfoSums. +func (fis FileInfoSums) Len() int { return len(fis) } + +// Swap swaps two FileInfoSum values if a FileInfoSums list. +func (fis FileInfoSums) Swap(i, j int) { fis[i], fis[j] = fis[j], fis[i] } + +// SortByPos sorts FileInfoSums content by position. +func (fis FileInfoSums) SortByPos() { + sort.Sort(byPos{fis}) +} + +// SortByNames sorts FileInfoSums content by name. +func (fis FileInfoSums) SortByNames() { + sort.Sort(byName{fis}) +} + +// SortBySums sorts FileInfoSums content by sums. +func (fis FileInfoSums) SortBySums() { + dups := fis.GetDuplicatePaths() + if len(dups) > 0 { + sort.Sort(bySum{fis, dups}) + } else { + sort.Sort(bySum{fis, nil}) + } +} + +// byName is a sort.Sort helper for sorting by file names. +// If names are the same, order them by their appearance in the tar archive +type byName struct{ FileInfoSums } + +func (bn byName) Less(i, j int) bool { + if bn.FileInfoSums[i].Name() == bn.FileInfoSums[j].Name() { + return bn.FileInfoSums[i].Pos() < bn.FileInfoSums[j].Pos() + } + return bn.FileInfoSums[i].Name() < bn.FileInfoSums[j].Name() +} + +// bySum is a sort.Sort helper for sorting by the sums of all the fileinfos in the tar archive +type bySum struct { + FileInfoSums + dups FileInfoSums +} + +func (bs bySum) Less(i, j int) bool { + if bs.dups != nil && bs.FileInfoSums[i].Name() == bs.FileInfoSums[j].Name() { + return bs.FileInfoSums[i].Pos() < bs.FileInfoSums[j].Pos() + } + return bs.FileInfoSums[i].Sum() < bs.FileInfoSums[j].Sum() +} + +// byPos is a sort.Sort helper for sorting by the sums of all the fileinfos by their original order +type byPos struct{ FileInfoSums } + +func (bp byPos) Less(i, j int) bool { + return bp.FileInfoSums[i].Pos() < bp.FileInfoSums[j].Pos() +} diff --git a/pkg/tarsum/fileinfosums_test.go b/pkg/tarsum/fileinfosums_test.go new file mode 100644 index 00000000..bb700d8b --- /dev/null +++ b/pkg/tarsum/fileinfosums_test.go @@ -0,0 +1,62 @@ +package tarsum + +import "testing" + +func newFileInfoSums() FileInfoSums { + return FileInfoSums{ + fileInfoSum{name: "file3", sum: "2abcdef1234567890", pos: 2}, + fileInfoSum{name: "dup1", sum: "deadbeef1", pos: 5}, + fileInfoSum{name: "file1", sum: "0abcdef1234567890", pos: 0}, + fileInfoSum{name: "file4", sum: "3abcdef1234567890", pos: 3}, + fileInfoSum{name: "dup1", sum: "deadbeef0", pos: 4}, + fileInfoSum{name: "file2", sum: "1abcdef1234567890", pos: 1}, + } +} + +func TestSortFileInfoSums(t *testing.T) { + dups := newFileInfoSums().GetAllFile("dup1") + if len(dups) != 2 { + t.Errorf("expected length 2, got %d", len(dups)) + } + dups.SortByNames() + if dups[0].Pos() != 4 { + t.Errorf("sorted dups should be ordered by position. Expected 4, got %d", dups[0].Pos()) + } + + fis := newFileInfoSums() + expected := "0abcdef1234567890" + fis.SortBySums() + got := fis[0].Sum() + if got != expected { + t.Errorf("Expected %q, got %q", expected, got) + } + + fis = newFileInfoSums() + expected = "dup1" + fis.SortByNames() + gotFis := fis[0] + if gotFis.Name() != expected { + t.Errorf("Expected %q, got %q", expected, gotFis.Name()) + } + // since a duplicate is first, ensure it is ordered first by position too + if gotFis.Pos() != 4 { + t.Errorf("Expected %d, got %d", 4, gotFis.Pos()) + } + + fis = newFileInfoSums() + fis.SortByPos() + if fis[0].Pos() != 0 { + t.Errorf("sorted fileInfoSums by Pos should order them by position.") + } + + fis = newFileInfoSums() + expected = "deadbeef1" + gotFileInfoSum := fis.GetFile("dup1") + if gotFileInfoSum.Sum() != expected { + t.Errorf("Expected %q, got %q", expected, gotFileInfoSum) + } + if fis.GetFile("noPresent") != nil { + t.Errorf("Should have return nil if name not found.") + } + +} diff --git a/pkg/tarsum/tarsum.go b/pkg/tarsum/tarsum.go new file mode 100644 index 00000000..4dc89bd4 --- /dev/null +++ b/pkg/tarsum/tarsum.go @@ -0,0 +1,294 @@ +// Package tarsum provides algorithms to perform checksum calculation on +// filesystem layers. +// +// The transportation of filesystems, regarding Docker, is done with tar(1) +// archives. There are a variety of tar serialization formats [2], and a key +// concern here is ensuring a repeatable checksum given a set of inputs from a +// generic tar archive. Types of transportation include distribution to and from a +// registry endpoint, saving and loading through commands or Docker daemon APIs, +// transferring the build context from client to Docker daemon, and committing the +// filesystem of a container to become an image. +// +// As tar archives are used for transit, but not preserved in many situations, the +// focus of the algorithm is to ensure the integrity of the preserved filesystem, +// while maintaining a deterministic accountability. This includes neither +// constraining the ordering or manipulation of the files during the creation or +// unpacking of the archive, nor include additional metadata state about the file +// system attributes. +package tarsum + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "crypto" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "hash" + "io" + "strings" +) + +const ( + buf8K = 8 * 1024 + buf16K = 16 * 1024 + buf32K = 32 * 1024 +) + +// NewTarSum creates a new interface for calculating a fixed time checksum of a +// tar archive. +// +// This is used for calculating checksums of layers of an image, in some cases +// including the byte payload of the image's json metadata as well, and for +// calculating the checksums for buildcache. +func NewTarSum(r io.Reader, dc bool, v Version) (TarSum, error) { + return NewTarSumHash(r, dc, v, DefaultTHash) +} + +// NewTarSumHash creates a new TarSum, providing a THash to use rather than +// the DefaultTHash. +func NewTarSumHash(r io.Reader, dc bool, v Version, tHash THash) (TarSum, error) { + headerSelector, err := getTarHeaderSelector(v) + if err != nil { + return nil, err + } + ts := &tarSum{Reader: r, DisableCompression: dc, tarSumVersion: v, headerSelector: headerSelector, tHash: tHash} + err = ts.initTarSum() + return ts, err +} + +// NewTarSumForLabel creates a new TarSum using the provided TarSum version+hash label. +func NewTarSumForLabel(r io.Reader, disableCompression bool, label string) (TarSum, error) { + parts := strings.SplitN(label, "+", 2) + if len(parts) != 2 { + return nil, errors.New("tarsum label string should be of the form: {tarsum_version}+{hash_name}") + } + + versionName, hashName := parts[0], parts[1] + + version, ok := tarSumVersionsByName[versionName] + if !ok { + return nil, fmt.Errorf("unknown TarSum version name: %q", versionName) + } + + hashConfig, ok := standardHashConfigs[hashName] + if !ok { + return nil, fmt.Errorf("unknown TarSum hash name: %q", hashName) + } + + tHash := NewTHash(hashConfig.name, hashConfig.hash.New) + + return NewTarSumHash(r, disableCompression, version, tHash) +} + +// TarSum is the generic interface for calculating fixed time +// checksums of a tar archive. +type TarSum interface { + io.Reader + GetSums() FileInfoSums + Sum([]byte) string + Version() Version + Hash() THash +} + +// tarSum struct is the structure for a Version0 checksum calculation. +type tarSum struct { + io.Reader + tarR *tar.Reader + tarW *tar.Writer + writer writeCloseFlusher + bufTar *bytes.Buffer + bufWriter *bytes.Buffer + bufData []byte + h hash.Hash + tHash THash + sums FileInfoSums + fileCounter int64 + currentFile string + finished bool + first bool + DisableCompression bool // false by default. When false, the output gzip compressed. + tarSumVersion Version // this field is not exported so it can not be mutated during use + headerSelector tarHeaderSelector // handles selecting and ordering headers for files in the archive +} + +func (ts tarSum) Hash() THash { + return ts.tHash +} + +func (ts tarSum) Version() Version { + return ts.tarSumVersion +} + +// THash provides a hash.Hash type generator and its name. +type THash interface { + Hash() hash.Hash + Name() string +} + +// NewTHash is a convenience method for creating a THash. +func NewTHash(name string, h func() hash.Hash) THash { + return simpleTHash{n: name, h: h} +} + +type tHashConfig struct { + name string + hash crypto.Hash +} + +var ( + // NOTE: DO NOT include MD5 or SHA1, which are considered insecure. + standardHashConfigs = map[string]tHashConfig{ + "sha256": {name: "sha256", hash: crypto.SHA256}, + "sha512": {name: "sha512", hash: crypto.SHA512}, + } +) + +// DefaultTHash is default TarSum hashing algorithm - "sha256". +var DefaultTHash = NewTHash("sha256", sha256.New) + +type simpleTHash struct { + n string + h func() hash.Hash +} + +func (sth simpleTHash) Name() string { return sth.n } +func (sth simpleTHash) Hash() hash.Hash { return sth.h() } + +func (ts *tarSum) encodeHeader(h *tar.Header) error { + for _, elem := range ts.headerSelector.selectHeaders(h) { + if _, err := ts.h.Write([]byte(elem[0] + elem[1])); err != nil { + return err + } + } + return nil +} + +func (ts *tarSum) initTarSum() error { + ts.bufTar = bytes.NewBuffer([]byte{}) + ts.bufWriter = bytes.NewBuffer([]byte{}) + ts.tarR = tar.NewReader(ts.Reader) + ts.tarW = tar.NewWriter(ts.bufTar) + if !ts.DisableCompression { + ts.writer = gzip.NewWriter(ts.bufWriter) + } else { + ts.writer = &nopCloseFlusher{Writer: ts.bufWriter} + } + if ts.tHash == nil { + ts.tHash = DefaultTHash + } + ts.h = ts.tHash.Hash() + ts.h.Reset() + ts.first = true + ts.sums = FileInfoSums{} + return nil +} + +func (ts *tarSum) Read(buf []byte) (int, error) { + if ts.finished { + return ts.bufWriter.Read(buf) + } + if len(ts.bufData) < len(buf) { + switch { + case len(buf) <= buf8K: + ts.bufData = make([]byte, buf8K) + case len(buf) <= buf16K: + ts.bufData = make([]byte, buf16K) + case len(buf) <= buf32K: + ts.bufData = make([]byte, buf32K) + default: + ts.bufData = make([]byte, len(buf)) + } + } + buf2 := ts.bufData[:len(buf)] + + n, err := ts.tarR.Read(buf2) + if err != nil { + if err == io.EOF { + if _, err := ts.h.Write(buf2[:n]); err != nil { + return 0, err + } + if !ts.first { + ts.sums = append(ts.sums, fileInfoSum{name: ts.currentFile, sum: hex.EncodeToString(ts.h.Sum(nil)), pos: ts.fileCounter}) + ts.fileCounter++ + ts.h.Reset() + } else { + ts.first = false + } + + currentHeader, err := ts.tarR.Next() + if err != nil { + if err == io.EOF { + if err := ts.tarW.Close(); err != nil { + return 0, err + } + if _, err := io.Copy(ts.writer, ts.bufTar); err != nil { + return 0, err + } + if err := ts.writer.Close(); err != nil { + return 0, err + } + ts.finished = true + return n, nil + } + return n, err + } + ts.currentFile = strings.TrimSuffix(strings.TrimPrefix(currentHeader.Name, "./"), "/") + if err := ts.encodeHeader(currentHeader); err != nil { + return 0, err + } + if err := ts.tarW.WriteHeader(currentHeader); err != nil { + return 0, err + } + if _, err := ts.tarW.Write(buf2[:n]); err != nil { + return 0, err + } + ts.tarW.Flush() + if _, err := io.Copy(ts.writer, ts.bufTar); err != nil { + return 0, err + } + ts.writer.Flush() + + return ts.bufWriter.Read(buf) + } + return n, err + } + + // Filling the hash buffer + if _, err = ts.h.Write(buf2[:n]); err != nil { + return 0, err + } + + // Filling the tar writer + if _, err = ts.tarW.Write(buf2[:n]); err != nil { + return 0, err + } + ts.tarW.Flush() + + // Filling the output writer + if _, err = io.Copy(ts.writer, ts.bufTar); err != nil { + return 0, err + } + ts.writer.Flush() + + return ts.bufWriter.Read(buf) +} + +func (ts *tarSum) Sum(extra []byte) string { + ts.sums.SortBySums() + h := ts.tHash.Hash() + if extra != nil { + h.Write(extra) + } + for _, fis := range ts.sums { + h.Write([]byte(fis.Sum())) + } + checksum := ts.Version().String() + "+" + ts.tHash.Name() + ":" + hex.EncodeToString(h.Sum(nil)) + return checksum +} + +func (ts *tarSum) GetSums() FileInfoSums { + return ts.sums +} diff --git a/pkg/tarsum/tarsum_spec.md b/pkg/tarsum/tarsum_spec.md new file mode 100644 index 00000000..89b2e49f --- /dev/null +++ b/pkg/tarsum/tarsum_spec.md @@ -0,0 +1,230 @@ +page_title: TarSum checksum specification +page_description: Documentation for algorithms used in the TarSum checksum calculation +page_keywords: docker, checksum, validation, tarsum + +# TarSum Checksum Specification + +## Abstract + +This document describes the algorithms used in performing the TarSum checksum +calculation on filesystem layers, the need for this method over existing +methods, and the versioning of this calculation. + +## Warning + +This checksum algorithm is for best-effort comparison of file trees with fuzzy logic. + +This is _not_ a cryptographic attestation, and should not be considered secure. + +## Introduction + +The transportation of filesystems, regarding Docker, is done with tar(1) +archives. There are a variety of tar serialization formats [2], and a key +concern here is ensuring a repeatable checksum given a set of inputs from a +generic tar archive. Types of transportation include distribution to and from a +registry endpoint, saving and loading through commands or Docker daemon APIs, +transferring the build context from client to Docker daemon, and committing the +filesystem of a container to become an image. + +As tar archives are used for transit, but not preserved in many situations, the +focus of the algorithm is to ensure the integrity of the preserved filesystem, +while maintaining a deterministic accountability. This includes neither +constraining the ordering or manipulation of the files during the creation or +unpacking of the archive, nor include additional metadata state about the file +system attributes. + +## Intended Audience + +This document is outlining the methods used for consistent checksum calculation +for filesystems transported via tar archives. + +Auditing these methodologies is an open and iterative process. This document +should accommodate the review of source code. Ultimately, this document should +be the starting point of further refinements to the algorithm and its future +versions. + +## Concept + +The checksum mechanism must ensure the integrity and assurance of the +filesystem payload. + +## Checksum Algorithm Profile + +A checksum mechanism must define the following operations and attributes: + +* Associated hashing cipher - used to checksum each file payload and attribute + information. +* Checksum list - each file of the filesystem archive has its checksum + calculated from the payload and attributes of the file. The final checksum is + calculated from this list, with specific ordering. +* Version - as the algorithm adapts to requirements, there are behaviors of the + algorithm to manage by versioning. +* Archive being calculated - the tar archive having its checksum calculated + +## Elements of TarSum checksum + +The calculated sum output is a text string. The elements included in the output +of the calculated sum comprise the information needed for validation of the sum +(TarSum version and hashing cipher used) and the expected checksum in hexadecimal +form. + +There are two delimiters used: +* '+' separates TarSum version from hashing cipher +* ':' separates calculation mechanics from expected hash + +Example: + +``` + "tarsum.v1+sha256:220a60ecd4a3c32c282622a625a54db9ba0ff55b5ba9c29c7064a2bc358b6a3e" + | | \ | + | | \ | + |_version_|_cipher__|__ | + | \ | + |_calculation_mechanics_|______________________expected_sum_______________________| +``` + +## Versioning + +Versioning was introduced [0] to accommodate differences in calculation needed, +and ability to maintain reverse compatibility. + +The general algorithm will be describe further in the 'Calculation'. + +### Version0 + +This is the initial version of TarSum. + +Its element in the TarSum checksum string is `tarsum`. + +### Version1 + +Its element in the TarSum checksum is `tarsum.v1`. + +The notable changes in this version: +* Exclusion of file `mtime` from the file information headers, in each file + checksum calculation +* Inclusion of extended attributes (`xattrs`. Also seen as `SCHILY.xattr.` prefixed Pax + tar file info headers) keys and values in each file checksum calculation + +### VersionDev + +*Do not use unless validating refinements to the checksum algorithm* + +Its element in the TarSum checksum is `tarsum.dev`. + +This is a floating place holder for a next version and grounds for testing +changes. The methods used for calculation are subject to change without notice, +and this version is for testing and not for production use. + +## Ciphers + +The official default and standard hashing cipher used in the calculation mechanic +is `sha256`. This refers to SHA256 hash algorithm as defined in FIPS 180-4. + +Though the TarSum algorithm itself is not exclusively bound to the single +hashing cipher `sha256`, support for alternate hashing ciphers was later added +[1]. Use cases for alternate cipher could include future-proofing TarSum +checksum format and using faster cipher hashes for tar filesystem checksums. + +## Calculation + +### Requirement + +As mentioned earlier, the calculation is such that it takes into consideration +the lifecycle of the tar archive. In that the tar archive is not an immutable, +permanent artifact. Otherwise options like relying on a known hashing cipher +checksum of the archive itself would be reliable enough. The tar archive of the +filesystem is used as a transportation medium for Docker images, and the +archive is discarded once its contents are extracted. Therefore, for consistent +validation items such as order of files in the tar archive and time stamps are +subject to change once an image is received. + +### Process + +The method is typically iterative due to reading tar info headers from the +archive stream, though this is not a strict requirement. + +#### Files + +Each file in the tar archive have their contents (headers and body) checksummed +individually using the designated associated hashing cipher. The ordered +headers of the file are written to the checksum calculation first, and then the +payload of the file body. + +The resulting checksum of the file is appended to the list of file sums. The +sum is encoded as a string of the hexadecimal digest. Additionally, the file +name and position in the archive is kept as reference for special ordering. + +#### Headers + +The following headers are read, in this +order ( and the corresponding representation of its value): +* 'name' - string +* 'mode' - string of the base10 integer +* 'uid' - string of the integer +* 'gid' - string of the integer +* 'size' - string of the integer +* 'mtime' (_Version0 only_) - string of integer of the seconds since 1970-01-01 00:00:00 UTC +* 'typeflag' - string of the char +* 'linkname' - string +* 'uname' - string +* 'gname' - string +* 'devmajor' - string of the integer +* 'devminor' - string of the integer + +For >= Version1, the extended attribute headers ("SCHILY.xattr." prefixed pax +headers) included after the above list. These xattrs key/values are first +sorted by the keys. + +#### Header Format + +The ordered headers are written to the hash in the format of + + "{.key}{.value}" + +with no newline. + +#### Body + +After the order headers of the file have been added to the checksum for the +file, the body of the file is written to the hash. + +#### List of file sums + +The list of file sums is sorted by the string of the hexadecimal digest. + +If there are two files in the tar with matching paths, the order of occurrence +for that path is reflected for the sums of the corresponding file header and +body. + +#### Final Checksum + +Begin with a fresh or initial state of the associated hash cipher. If there is +additional payload to include in the TarSum calculation for the archive, it is +written first. Then each checksum from the ordered list of file sums is written +to the hash. + +The resulting digest is formatted per the Elements of TarSum checksum, +including the TarSum version, the associated hash cipher and the hexadecimal +encoded checksum digest. + +## Security Considerations + +The initial version of TarSum has undergone one update that could invalidate +handcrafted tar archives. The tar archive format supports appending of files +with same names as prior files in the archive. The latter file will clobber the +prior file of the same path. Due to this the algorithm now accounts for files +with matching paths, and orders the list of file sums accordingly [3]. + +## Footnotes + +* [0] Versioning https://github.com/docker/docker/commit/747f89cd327db9d50251b17797c4d825162226d0 +* [1] Alternate ciphers https://github.com/docker/docker/commit/4e9925d780665149b8bc940d5ba242ada1973c4e +* [2] Tar http://en.wikipedia.org/wiki/Tar_%28computing%29 +* [3] Name collision https://github.com/docker/docker/commit/c5e6362c53cbbc09ddbabd5a7323e04438b57d31 + +## Acknowledgments + +Joffrey F (shin-) and Guillaume J. Charmes (creack) on the initial work of the +TarSum calculation. + diff --git a/pkg/tarsum/tarsum_test.go b/pkg/tarsum/tarsum_test.go new file mode 100644 index 00000000..54bec53f --- /dev/null +++ b/pkg/tarsum/tarsum_test.go @@ -0,0 +1,656 @@ +package tarsum + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "crypto/md5" + "crypto/rand" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/hex" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + "testing" +) + +type testLayer struct { + filename string + options *sizedOptions + jsonfile string + gzip bool + tarsum string + version Version + hash THash +} + +var testLayers = []testLayer{ + { + filename: "testdata/46af0962ab5afeb5ce6740d4d91652e69206fc991fd5328c1a94d364ad00e457/layer.tar", + jsonfile: "testdata/46af0962ab5afeb5ce6740d4d91652e69206fc991fd5328c1a94d364ad00e457/json", + version: Version0, + tarsum: "tarsum+sha256:4095cc12fa5fdb1ab2760377e1cd0c4ecdd3e61b4f9b82319d96fcea6c9a41c6"}, + { + filename: "testdata/46af0962ab5afeb5ce6740d4d91652e69206fc991fd5328c1a94d364ad00e457/layer.tar", + jsonfile: "testdata/46af0962ab5afeb5ce6740d4d91652e69206fc991fd5328c1a94d364ad00e457/json", + version: VersionDev, + tarsum: "tarsum.dev+sha256:db56e35eec6ce65ba1588c20ba6b1ea23743b59e81fb6b7f358ccbde5580345c"}, + { + filename: "testdata/46af0962ab5afeb5ce6740d4d91652e69206fc991fd5328c1a94d364ad00e457/layer.tar", + jsonfile: "testdata/46af0962ab5afeb5ce6740d4d91652e69206fc991fd5328c1a94d364ad00e457/json", + gzip: true, + tarsum: "tarsum+sha256:4095cc12fa5fdb1ab2760377e1cd0c4ecdd3e61b4f9b82319d96fcea6c9a41c6"}, + { + // Tests existing version of TarSum when xattrs are present + filename: "testdata/xattr/layer.tar", + jsonfile: "testdata/xattr/json", + version: Version0, + tarsum: "tarsum+sha256:07e304a8dbcb215b37649fde1a699f8aeea47e60815707f1cdf4d55d25ff6ab4"}, + { + // Tests next version of TarSum when xattrs are present + filename: "testdata/xattr/layer.tar", + jsonfile: "testdata/xattr/json", + version: VersionDev, + tarsum: "tarsum.dev+sha256:6c58917892d77b3b357b0f9ad1e28e1f4ae4de3a8006bd3beb8beda214d8fd16"}, + { + filename: "testdata/511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158/layer.tar", + jsonfile: "testdata/511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158/json", + tarsum: "tarsum+sha256:c66bd5ec9f87b8f4c6135ca37684618f486a3dd1d113b138d0a177bfa39c2571"}, + { + options: &sizedOptions{1, 1024 * 1024, false, false}, // a 1mb file (in memory) + tarsum: "tarsum+sha256:8bf12d7e67c51ee2e8306cba569398b1b9f419969521a12ffb9d8875e8836738"}, + { + // this tar has two files with the same path + filename: "testdata/collision/collision-0.tar", + tarsum: "tarsum+sha256:08653904a68d3ab5c59e65ef58c49c1581caa3c34744f8d354b3f575ea04424a"}, + { + // this tar has the same two files (with the same path), but reversed order. ensuring is has different hash than above + filename: "testdata/collision/collision-1.tar", + tarsum: "tarsum+sha256:b51c13fbefe158b5ce420d2b930eef54c5cd55c50a2ee4abdddea8fa9f081e0d"}, + { + // this tar has newer of collider-0.tar, ensuring is has different hash + filename: "testdata/collision/collision-2.tar", + tarsum: "tarsum+sha256:381547080919bb82691e995508ae20ed33ce0f6948d41cafbeb70ce20c73ee8e"}, + { + // this tar has newer of collider-1.tar, ensuring is has different hash + filename: "testdata/collision/collision-3.tar", + tarsum: "tarsum+sha256:f886e431c08143164a676805205979cd8fa535dfcef714db5515650eea5a7c0f"}, + { + options: &sizedOptions{1, 1024 * 1024, false, false}, // a 1mb file (in memory) + tarsum: "tarsum+md5:0d7529ec7a8360155b48134b8e599f53", + hash: md5THash, + }, + { + options: &sizedOptions{1, 1024 * 1024, false, false}, // a 1mb file (in memory) + tarsum: "tarsum+sha1:f1fee39c5925807ff75ef1925e7a23be444ba4df", + hash: sha1Hash, + }, + { + options: &sizedOptions{1, 1024 * 1024, false, false}, // a 1mb file (in memory) + tarsum: "tarsum+sha224:6319390c0b061d639085d8748b14cd55f697cf9313805218b21cf61c", + hash: sha224Hash, + }, + { + options: &sizedOptions{1, 1024 * 1024, false, false}, // a 1mb file (in memory) + tarsum: "tarsum+sha384:a578ce3ce29a2ae03b8ed7c26f47d0f75b4fc849557c62454be4b5ffd66ba021e713b48ce71e947b43aab57afd5a7636", + hash: sha384Hash, + }, + { + options: &sizedOptions{1, 1024 * 1024, false, false}, // a 1mb file (in memory) + tarsum: "tarsum+sha512:e9bfb90ca5a4dfc93c46ee061a5cf9837de6d2fdf82544d6460d3147290aecfabf7b5e415b9b6e72db9b8941f149d5d69fb17a394cbfaf2eac523bd9eae21855", + hash: sha512Hash, + }, +} + +type sizedOptions struct { + num int64 + size int64 + isRand bool + realFile bool +} + +// make a tar: +// * num is the number of files the tar should have +// * size is the bytes per file +// * isRand is whether the contents of the files should be a random chunk (otherwise it's all zeros) +// * realFile will write to a TempFile, instead of an in memory buffer +func sizedTar(opts sizedOptions) io.Reader { + var ( + fh io.ReadWriter + err error + ) + if opts.realFile { + fh, err = ioutil.TempFile("", "tarsum") + if err != nil { + return nil + } + } else { + fh = bytes.NewBuffer([]byte{}) + } + tarW := tar.NewWriter(fh) + defer tarW.Close() + for i := int64(0); i < opts.num; i++ { + err := tarW.WriteHeader(&tar.Header{ + Name: fmt.Sprintf("/testdata%d", i), + Mode: 0755, + Uid: 0, + Gid: 0, + Size: opts.size, + }) + if err != nil { + return nil + } + var rBuf []byte + if opts.isRand { + rBuf = make([]byte, 8) + _, err = rand.Read(rBuf) + if err != nil { + return nil + } + } else { + rBuf = []byte{0, 0, 0, 0, 0, 0, 0, 0} + } + + for i := int64(0); i < opts.size/int64(8); i++ { + tarW.Write(rBuf) + } + } + return fh +} + +func emptyTarSum(gzip bool) (TarSum, error) { + reader, writer := io.Pipe() + tarWriter := tar.NewWriter(writer) + + // Immediately close tarWriter and write-end of the + // Pipe in a separate goroutine so we don't block. + go func() { + tarWriter.Close() + writer.Close() + }() + + return NewTarSum(reader, !gzip, Version0) +} + +// Test errors on NewTarsumForLabel +func TestNewTarSumForLabelInvalid(t *testing.T) { + reader := strings.NewReader("") + + if _, err := NewTarSumForLabel(reader, true, "invalidlabel"); err == nil { + t.Fatalf("Expected an error, got nothing.") + } + + if _, err := NewTarSumForLabel(reader, true, "invalid+sha256"); err == nil { + t.Fatalf("Expected an error, got nothing.") + } + if _, err := NewTarSumForLabel(reader, true, "tarsum.v1+invalid"); err == nil { + t.Fatalf("Expected an error, got nothing.") + } +} + +func TestNewTarSumForLabel(t *testing.T) { + + layer := testLayers[0] + + reader, err := os.Open(layer.filename) + if err != nil { + t.Fatal(err) + } + label := strings.Split(layer.tarsum, ":")[0] + ts, err := NewTarSumForLabel(reader, false, label) + if err != nil { + t.Fatal(err) + } + + // Make sure it actually worked by reading a little bit of it + nbByteToRead := 8 * 1024 + dBuf := make([]byte, nbByteToRead) + _, err = ts.Read(dBuf) + if err != nil { + t.Errorf("failed to read %vKB from %s: %s", nbByteToRead, layer.filename, err) + } +} + +// TestEmptyTar tests that tarsum does not fail to read an empty tar +// and correctly returns the hex digest of an empty hash. +func TestEmptyTar(t *testing.T) { + // Test without gzip. + ts, err := emptyTarSum(false) + if err != nil { + t.Fatal(err) + } + + zeroBlock := make([]byte, 1024) + buf := new(bytes.Buffer) + + n, err := io.Copy(buf, ts) + if err != nil { + t.Fatal(err) + } + + if n != int64(len(zeroBlock)) || !bytes.Equal(buf.Bytes(), zeroBlock) { + t.Fatalf("tarSum did not write the correct number of zeroed bytes: %d", n) + } + + expectedSum := ts.Version().String() + "+sha256:" + hex.EncodeToString(sha256.New().Sum(nil)) + resultSum := ts.Sum(nil) + + if resultSum != expectedSum { + t.Fatalf("expected [%s] but got [%s]", expectedSum, resultSum) + } + + // Test with gzip. + ts, err = emptyTarSum(true) + if err != nil { + t.Fatal(err) + } + buf.Reset() + + n, err = io.Copy(buf, ts) + if err != nil { + t.Fatal(err) + } + + bufgz := new(bytes.Buffer) + gz := gzip.NewWriter(bufgz) + n, err = io.Copy(gz, bytes.NewBuffer(zeroBlock)) + gz.Close() + gzBytes := bufgz.Bytes() + + if n != int64(len(zeroBlock)) || !bytes.Equal(buf.Bytes(), gzBytes) { + t.Fatalf("tarSum did not write the correct number of gzipped-zeroed bytes: %d", n) + } + + resultSum = ts.Sum(nil) + + if resultSum != expectedSum { + t.Fatalf("expected [%s] but got [%s]", expectedSum, resultSum) + } + + // Test without ever actually writing anything. + if ts, err = NewTarSum(bytes.NewReader([]byte{}), true, Version0); err != nil { + t.Fatal(err) + } + + resultSum = ts.Sum(nil) + + if resultSum != expectedSum { + t.Fatalf("expected [%s] but got [%s]", expectedSum, resultSum) + } +} + +var ( + md5THash = NewTHash("md5", md5.New) + sha1Hash = NewTHash("sha1", sha1.New) + sha224Hash = NewTHash("sha224", sha256.New224) + sha384Hash = NewTHash("sha384", sha512.New384) + sha512Hash = NewTHash("sha512", sha512.New) +) + +// Test all the build-in read size : buf8K, buf16K, buf32K and more +func TestTarSumsReadSize(t *testing.T) { + // Test always on the same layer (that is big enough) + layer := testLayers[0] + + for i := 0; i < 5; i++ { + + reader, err := os.Open(layer.filename) + if err != nil { + t.Fatal(err) + } + ts, err := NewTarSum(reader, false, layer.version) + if err != nil { + t.Fatal(err) + } + + // Read and discard bytes so that it populates sums + nbByteToRead := (i + 1) * 8 * 1024 + dBuf := make([]byte, nbByteToRead) + _, err = ts.Read(dBuf) + if err != nil { + t.Errorf("failed to read %vKB from %s: %s", nbByteToRead, layer.filename, err) + continue + } + } +} + +func TestTarSums(t *testing.T) { + for _, layer := range testLayers { + var ( + fh io.Reader + err error + ) + if len(layer.filename) > 0 { + fh, err = os.Open(layer.filename) + if err != nil { + t.Errorf("failed to open %s: %s", layer.filename, err) + continue + } + } else if layer.options != nil { + fh = sizedTar(*layer.options) + } else { + // What else is there to test? + t.Errorf("what to do with %#v", layer) + continue + } + if file, ok := fh.(*os.File); ok { + defer file.Close() + } + + var ts TarSum + if layer.hash == nil { + // double negatives! + ts, err = NewTarSum(fh, !layer.gzip, layer.version) + } else { + ts, err = NewTarSumHash(fh, !layer.gzip, layer.version, layer.hash) + } + if err != nil { + t.Errorf("%q :: %q", err, layer.filename) + continue + } + + // Read variable number of bytes to test dynamic buffer + dBuf := make([]byte, 1) + _, err = ts.Read(dBuf) + if err != nil { + t.Errorf("failed to read 1B from %s: %s", layer.filename, err) + continue + } + dBuf = make([]byte, 16*1024) + _, err = ts.Read(dBuf) + if err != nil { + t.Errorf("failed to read 16KB from %s: %s", layer.filename, err) + continue + } + + // Read and discard remaining bytes + _, err = io.Copy(ioutil.Discard, ts) + if err != nil { + t.Errorf("failed to copy from %s: %s", layer.filename, err) + continue + } + var gotSum string + if len(layer.jsonfile) > 0 { + jfh, err := os.Open(layer.jsonfile) + if err != nil { + t.Errorf("failed to open %s: %s", layer.jsonfile, err) + continue + } + buf, err := ioutil.ReadAll(jfh) + if err != nil { + t.Errorf("failed to readAll %s: %s", layer.jsonfile, err) + continue + } + gotSum = ts.Sum(buf) + } else { + gotSum = ts.Sum(nil) + } + + if layer.tarsum != gotSum { + t.Errorf("expecting [%s], but got [%s]", layer.tarsum, gotSum) + } + var expectedHashName string + if layer.hash != nil { + expectedHashName = layer.hash.Name() + } else { + expectedHashName = DefaultTHash.Name() + } + if expectedHashName != ts.Hash().Name() { + t.Errorf("expecting hash [%v], but got [%s]", expectedHashName, ts.Hash().Name()) + } + } +} + +func TestIteration(t *testing.T) { + headerTests := []struct { + expectedSum string // TODO(vbatts) it would be nice to get individual sums of each + version Version + hdr *tar.Header + data []byte + }{ + { + "tarsum+sha256:626c4a2e9a467d65c33ae81f7f3dedd4de8ccaee72af73223c4bc4718cbc7bbd", + Version0, + &tar.Header{ + Name: "file.txt", + Size: 0, + Typeflag: tar.TypeReg, + Devminor: 0, + Devmajor: 0, + }, + []byte(""), + }, + { + "tarsum.dev+sha256:6ffd43a1573a9913325b4918e124ee982a99c0f3cba90fc032a65f5e20bdd465", + VersionDev, + &tar.Header{ + Name: "file.txt", + Size: 0, + Typeflag: tar.TypeReg, + Devminor: 0, + Devmajor: 0, + }, + []byte(""), + }, + { + "tarsum.dev+sha256:b38166c059e11fb77bef30bf16fba7584446e80fcc156ff46d47e36c5305d8ef", + VersionDev, + &tar.Header{ + Name: "another.txt", + Uid: 1000, + Gid: 1000, + Uname: "slartibartfast", + Gname: "users", + Size: 4, + Typeflag: tar.TypeReg, + Devminor: 0, + Devmajor: 0, + }, + []byte("test"), + }, + { + "tarsum.dev+sha256:4cc2e71ac5d31833ab2be9b4f7842a14ce595ec96a37af4ed08f87bc374228cd", + VersionDev, + &tar.Header{ + Name: "xattrs.txt", + Uid: 1000, + Gid: 1000, + Uname: "slartibartfast", + Gname: "users", + Size: 4, + Typeflag: tar.TypeReg, + Xattrs: map[string]string{ + "user.key1": "value1", + "user.key2": "value2", + }, + }, + []byte("test"), + }, + { + "tarsum.dev+sha256:65f4284fa32c0d4112dd93c3637697805866415b570587e4fd266af241503760", + VersionDev, + &tar.Header{ + Name: "xattrs.txt", + Uid: 1000, + Gid: 1000, + Uname: "slartibartfast", + Gname: "users", + Size: 4, + Typeflag: tar.TypeReg, + Xattrs: map[string]string{ + "user.KEY1": "value1", // adding different case to ensure different sum + "user.key2": "value2", + }, + }, + []byte("test"), + }, + { + "tarsum+sha256:c12bb6f1303a9ddbf4576c52da74973c00d14c109bcfa76b708d5da1154a07fa", + Version0, + &tar.Header{ + Name: "xattrs.txt", + Uid: 1000, + Gid: 1000, + Uname: "slartibartfast", + Gname: "users", + Size: 4, + Typeflag: tar.TypeReg, + Xattrs: map[string]string{ + "user.NOT": "CALCULATED", + }, + }, + []byte("test"), + }, + } + for _, htest := range headerTests { + s, err := renderSumForHeader(htest.version, htest.hdr, htest.data) + if err != nil { + t.Fatal(err) + } + + if s != htest.expectedSum { + t.Errorf("expected sum: %q, got: %q", htest.expectedSum, s) + } + } + +} + +func renderSumForHeader(v Version, h *tar.Header, data []byte) (string, error) { + buf := bytes.NewBuffer(nil) + // first build our test tar + tw := tar.NewWriter(buf) + if err := tw.WriteHeader(h); err != nil { + return "", err + } + if _, err := tw.Write(data); err != nil { + return "", err + } + tw.Close() + + ts, err := NewTarSum(buf, true, v) + if err != nil { + return "", err + } + tr := tar.NewReader(ts) + for { + hdr, err := tr.Next() + if hdr == nil || err == io.EOF { + // Signals the end of the archive. + break + } + if err != nil { + return "", err + } + if _, err = io.Copy(ioutil.Discard, tr); err != nil { + return "", err + } + } + return ts.Sum(nil), nil +} + +func Benchmark9kTar(b *testing.B) { + buf := bytes.NewBuffer([]byte{}) + fh, err := os.Open("testdata/46af0962ab5afeb5ce6740d4d91652e69206fc991fd5328c1a94d364ad00e457/layer.tar") + if err != nil { + b.Error(err) + return + } + n, err := io.Copy(buf, fh) + if err != nil { + b.Error(err) + return + } + fh.Close() + + reader := bytes.NewReader(buf.Bytes()) + + b.SetBytes(n) + b.ResetTimer() + for i := 0; i < b.N; i++ { + reader.Seek(0, 0) + ts, err := NewTarSum(reader, true, Version0) + if err != nil { + b.Error(err) + return + } + io.Copy(ioutil.Discard, ts) + ts.Sum(nil) + } +} + +func Benchmark9kTarGzip(b *testing.B) { + buf := bytes.NewBuffer([]byte{}) + fh, err := os.Open("testdata/46af0962ab5afeb5ce6740d4d91652e69206fc991fd5328c1a94d364ad00e457/layer.tar") + if err != nil { + b.Error(err) + return + } + n, err := io.Copy(buf, fh) + if err != nil { + b.Error(err) + return + } + fh.Close() + + reader := bytes.NewReader(buf.Bytes()) + + b.SetBytes(n) + b.ResetTimer() + for i := 0; i < b.N; i++ { + reader.Seek(0, 0) + ts, err := NewTarSum(reader, false, Version0) + if err != nil { + b.Error(err) + return + } + io.Copy(ioutil.Discard, ts) + ts.Sum(nil) + } +} + +// this is a single big file in the tar archive +func Benchmark1mbSingleFileTar(b *testing.B) { + benchmarkTar(b, sizedOptions{1, 1024 * 1024, true, true}, false) +} + +// this is a single big file in the tar archive +func Benchmark1mbSingleFileTarGzip(b *testing.B) { + benchmarkTar(b, sizedOptions{1, 1024 * 1024, true, true}, true) +} + +// this is 1024 1k files in the tar archive +func Benchmark1kFilesTar(b *testing.B) { + benchmarkTar(b, sizedOptions{1024, 1024, true, true}, false) +} + +// this is 1024 1k files in the tar archive +func Benchmark1kFilesTarGzip(b *testing.B) { + benchmarkTar(b, sizedOptions{1024, 1024, true, true}, true) +} + +func benchmarkTar(b *testing.B, opts sizedOptions, isGzip bool) { + var fh *os.File + tarReader := sizedTar(opts) + if br, ok := tarReader.(*os.File); ok { + fh = br + } + defer os.Remove(fh.Name()) + defer fh.Close() + + b.SetBytes(opts.size * opts.num) + b.ResetTimer() + for i := 0; i < b.N; i++ { + ts, err := NewTarSum(fh, !isGzip, Version0) + if err != nil { + b.Error(err) + return + } + io.Copy(ioutil.Discard, ts) + ts.Sum(nil) + fh.Seek(0, 0) + } +} diff --git a/pkg/tarsum/testdata/46af0962ab5afeb5ce6740d4d91652e69206fc991fd5328c1a94d364ad00e457/json b/pkg/tarsum/testdata/46af0962ab5afeb5ce6740d4d91652e69206fc991fd5328c1a94d364ad00e457/json new file mode 100644 index 00000000..48e2af34 --- /dev/null +++ b/pkg/tarsum/testdata/46af0962ab5afeb5ce6740d4d91652e69206fc991fd5328c1a94d364ad00e457/json @@ -0,0 +1 @@ +{"id":"46af0962ab5afeb5ce6740d4d91652e69206fc991fd5328c1a94d364ad00e457","parent":"def3f9165934325dfd027c86530b2ea49bb57a0963eb1336b3a0415ff6fd56de","created":"2014-04-07T02:45:52.610504484Z","container":"e0f07f8d72cae171a3dcc35859960e7e956e0628bce6fedc4122bf55b2c287c7","container_config":{"Hostname":"88807319f25e","Domainname":"","User":"","Memory":0,"MemorySwap":0,"CpuShares":0,"AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":null,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["HOME=/","PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","sed -ri 's/^(%wheel.*)(ALL)$/\\1NOPASSWD: \\2/' /etc/sudoers"],"Image":"def3f9165934325dfd027c86530b2ea49bb57a0963eb1336b3a0415ff6fd56de","Volumes":null,"WorkingDir":"","Entrypoint":null,"NetworkDisabled":false,"OnBuild":[]},"docker_version":"0.9.1-dev","config":{"Hostname":"88807319f25e","Domainname":"","User":"","Memory":0,"MemorySwap":0,"CpuShares":0,"AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":null,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["HOME=/","PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":null,"Image":"def3f9165934325dfd027c86530b2ea49bb57a0963eb1336b3a0415ff6fd56de","Volumes":null,"WorkingDir":"","Entrypoint":null,"NetworkDisabled":false,"OnBuild":[]},"architecture":"amd64","os":"linux","Size":3425} \ No newline at end of file diff --git a/pkg/tarsum/testdata/46af0962ab5afeb5ce6740d4d91652e69206fc991fd5328c1a94d364ad00e457/layer.tar b/pkg/tarsum/testdata/46af0962ab5afeb5ce6740d4d91652e69206fc991fd5328c1a94d364ad00e457/layer.tar new file mode 100644 index 0000000000000000000000000000000000000000..dfd5c204aea77673f13fdd2f81cb4af1c155c00c GIT binary patch literal 9216 zcmeHMYfsx)8s=;H6|bl&iYAZ?p-5<1$(y*vYHk~c?XX{vu}|fzRr1|&zyvK1! zQq)nWWVPA}63Myvy*}^F5Qtg*V8=g=M!Ru&adFTnf40B*^q|=~Z#CM@#>M%EgGRH_ zXtfULV#j(J_Jz`34wZgZ*0ym!%kRHL9{_(p&BZRoHJYu)<>loz?$!PU{9Bjp<^i?p zS)Tg!r=9Az$G@(0Ao6^75%A;qpMSV)ukcqQn%1X5y|oh!_xLmZX`y%GUBmQG;D6af z{a@yPg@1D=8t(B&ZtcXgE2ck=f9pf*x&ANlU$J}L#UB59rsJ=#>(otde**vZ1?PXJ z)y|dMh8z!Kfh=;zN!B|J)*y8)L$Hbq5c2K_rK=l{{8R8czxwV#$Odd zDsuJ8oS)h8`+U3IsNVOszdy8F?XCC!X1jHMK)Xr!XT8koFP{Hz-;!IxPhJ$Ib48h# zYv~t}ms6n-7Nk?ki-cxgF4IDhpT@D51d2R$2x=V)%F|Svhif#KI>gHaB|@O7JU(A% zo>KEP56(cuboN&-&LROexgfmf&txD1^0c9NNVQI5N~dNwm64!nnnQFH317=JF`{vu zi^$WUtCWHQq4Y!Yy@W{oRoV29sUd<=@!~sJ;!ok8>_qYfz|Ch12+9P6$8i`#qvqS zhsLT-8QL!zwhRx(aXaYF&PwD5LLOm%T#Ds>) z{YV0A>qPL*aFLnz9*nfyl@!I3_Ss=Y=MKNEA zG8|$lPj#9`#(W1sgCgK@f)P?2A)0uPB8Gf6TLITOAl@|29e$jAvBox=W-QCrr59N% zKg$7Xy=69F7QR_X7D_-i2hs*J)6%&RIBr9LDPPP_-? z-X`DPuwzY(j+Gk=rWL_Msfvvp-prW$3W(MwPPgEZO^EI!{*XIAuLp zlpj9k85vO{{2kR4hD{4c;~{+QmhNVfq;xeepJc>QQ@QJfEkdQVBbPJuiA~nsv9l~O zrN&UpxC9i`6;rQ>v?7%WUrr@(gXOs4JE=IN=}4(?RS=2GEd9-ogTEiuP>Fqyb6;vM ziV-Q;Z|ZT?Vz^rPk?`^}6a`cC_=9V1=*>jc&y0jq{h|=m&BK+Jpv}ea1?sKVi^Gj` zk<9K*;4?gK^?Jl6-g0L4kQcX>OZUHi{>Odi#u~f!gnqSdCpW{f zGr2q31WO6O$i;nz9#NH-D^8Rv6Xcv%XFkhmyBsZ;8k2ftd;fPtN1v+`G zPRv~5E)wm1y}~(Py9GwK;`;9K2C_2#(Rc=qFBTa z>?ZUNHvSmq9G9)M%0u+CW!J=jv1~Clz-avUIImk%<&=a9uI;2EY~~stiCKTsh|Oow<5; z$eY1%WV!B_?iFikc)C2TV46YQucl=WfmM#jY|_4sK>Njf)j#u#Y{x@V_A!c2o<`D? zX*2YQ4A)U054Qh4y3hVk?0?5^Us~rh*TViU9vl!r009ILKmY**5I_I{1Q0*~0R#|0 Y009ILKmY**5I_I{1Q0*~fqxTt0{2EK)Bpeg literal 0 HcmV?d00001 diff --git a/pkg/tarsum/testdata/collision/collision-2.tar b/pkg/tarsum/testdata/collision/collision-2.tar new file mode 100644 index 0000000000000000000000000000000000000000..7b5c04a9644808851fcccab5c3c240bf342abd93 GIT binary patch literal 10240 zcmeIuF%E+;425COJw=XS2L~?Dp<74P5hRe1I+e8NZ(w35>V(Abzr};)_<@(2e`|Ha`Z>GG~@_KYd${~ON w0tg_000IagfB*srAbVE5xzPBd+@To)G|2840byWhU|?oqf;;~Mb02E{2kHRk de~R-YhD)#rjPU%AB}7JrMnhmU1V%^*0091(G-Ch& literal 0 HcmV?d00001 diff --git a/pkg/tarsum/versioning.go b/pkg/tarsum/versioning.go new file mode 100644 index 00000000..28822868 --- /dev/null +++ b/pkg/tarsum/versioning.go @@ -0,0 +1,150 @@ +package tarsum + +import ( + "archive/tar" + "errors" + "sort" + "strconv" + "strings" +) + +// Version is used for versioning of the TarSum algorithm +// based on the prefix of the hash used +// i.e. "tarsum+sha256:e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b" +type Version int + +// Prefix of "tarsum" +const ( + Version0 Version = iota + Version1 + // VersionDev this constant will be either the latest or an unsettled next-version of the TarSum calculation + VersionDev +) + +// VersionLabelForChecksum returns the label for the given tarsum +// checksum, i.e., everything before the first `+` character in +// the string or an empty string if no label separator is found. +func VersionLabelForChecksum(checksum string) string { + // Checksums are in the form: {versionLabel}+{hashID}:{hex} + sepIndex := strings.Index(checksum, "+") + if sepIndex < 0 { + return "" + } + return checksum[:sepIndex] +} + +// GetVersions gets a list of all known tarsum versions. +func GetVersions() []Version { + v := []Version{} + for k := range tarSumVersions { + v = append(v, k) + } + return v +} + +var ( + tarSumVersions = map[Version]string{ + Version0: "tarsum", + Version1: "tarsum.v1", + VersionDev: "tarsum.dev", + } + tarSumVersionsByName = map[string]Version{ + "tarsum": Version0, + "tarsum.v1": Version1, + "tarsum.dev": VersionDev, + } +) + +func (tsv Version) String() string { + return tarSumVersions[tsv] +} + +// GetVersionFromTarsum returns the Version from the provided string. +func GetVersionFromTarsum(tarsum string) (Version, error) { + tsv := tarsum + if strings.Contains(tarsum, "+") { + tsv = strings.SplitN(tarsum, "+", 2)[0] + } + for v, s := range tarSumVersions { + if s == tsv { + return v, nil + } + } + return -1, ErrNotVersion +} + +// Errors that may be returned by functions in this package +var ( + ErrNotVersion = errors.New("string does not include a TarSum Version") + ErrVersionNotImplemented = errors.New("TarSum Version is not yet implemented") +) + +// tarHeaderSelector is the interface which different versions +// of tarsum should use for selecting and ordering tar headers +// for each item in the archive. +type tarHeaderSelector interface { + selectHeaders(h *tar.Header) (orderedHeaders [][2]string) +} + +type tarHeaderSelectFunc func(h *tar.Header) (orderedHeaders [][2]string) + +func (f tarHeaderSelectFunc) selectHeaders(h *tar.Header) (orderedHeaders [][2]string) { + return f(h) +} + +func v0TarHeaderSelect(h *tar.Header) (orderedHeaders [][2]string) { + return [][2]string{ + {"name", h.Name}, + {"mode", strconv.FormatInt(h.Mode, 10)}, + {"uid", strconv.Itoa(h.Uid)}, + {"gid", strconv.Itoa(h.Gid)}, + {"size", strconv.FormatInt(h.Size, 10)}, + {"mtime", strconv.FormatInt(h.ModTime.UTC().Unix(), 10)}, + {"typeflag", string([]byte{h.Typeflag})}, + {"linkname", h.Linkname}, + {"uname", h.Uname}, + {"gname", h.Gname}, + {"devmajor", strconv.FormatInt(h.Devmajor, 10)}, + {"devminor", strconv.FormatInt(h.Devminor, 10)}, + } +} + +func v1TarHeaderSelect(h *tar.Header) (orderedHeaders [][2]string) { + // Get extended attributes. + xAttrKeys := make([]string, len(h.Xattrs)) + for k := range h.Xattrs { + xAttrKeys = append(xAttrKeys, k) + } + sort.Strings(xAttrKeys) + + // Make the slice with enough capacity to hold the 11 basic headers + // we want from the v0 selector plus however many xattrs we have. + orderedHeaders = make([][2]string, 0, 11+len(xAttrKeys)) + + // Copy all headers from v0 excluding the 'mtime' header (the 5th element). + v0headers := v0TarHeaderSelect(h) + orderedHeaders = append(orderedHeaders, v0headers[0:5]...) + orderedHeaders = append(orderedHeaders, v0headers[6:]...) + + // Finally, append the sorted xattrs. + for _, k := range xAttrKeys { + orderedHeaders = append(orderedHeaders, [2]string{k, h.Xattrs[k]}) + } + + return +} + +var registeredHeaderSelectors = map[Version]tarHeaderSelectFunc{ + Version0: v0TarHeaderSelect, + Version1: v1TarHeaderSelect, + VersionDev: v1TarHeaderSelect, +} + +func getTarHeaderSelector(v Version) (tarHeaderSelector, error) { + headerSelector, ok := registeredHeaderSelectors[v] + if !ok { + return nil, ErrVersionNotImplemented + } + + return headerSelector, nil +} diff --git a/pkg/tarsum/versioning_test.go b/pkg/tarsum/versioning_test.go new file mode 100644 index 00000000..88e0a578 --- /dev/null +++ b/pkg/tarsum/versioning_test.go @@ -0,0 +1,98 @@ +package tarsum + +import ( + "testing" +) + +func TestVersionLabelForChecksum(t *testing.T) { + version := VersionLabelForChecksum("tarsum+sha256:deadbeef") + if version != "tarsum" { + t.Fatalf("Version should have been 'tarsum', was %v", version) + } + version = VersionLabelForChecksum("tarsum.v1+sha256:deadbeef") + if version != "tarsum.v1" { + t.Fatalf("Version should have been 'tarsum.v1', was %v", version) + } + version = VersionLabelForChecksum("something+somethingelse") + if version != "something" { + t.Fatalf("Version should have been 'something', was %v", version) + } + version = VersionLabelForChecksum("invalidChecksum") + if version != "" { + t.Fatalf("Version should have been empty, was %v", version) + } +} + +func TestVersion(t *testing.T) { + expected := "tarsum" + var v Version + if v.String() != expected { + t.Errorf("expected %q, got %q", expected, v.String()) + } + + expected = "tarsum.v1" + v = 1 + if v.String() != expected { + t.Errorf("expected %q, got %q", expected, v.String()) + } + + expected = "tarsum.dev" + v = 2 + if v.String() != expected { + t.Errorf("expected %q, got %q", expected, v.String()) + } +} + +func TestGetVersion(t *testing.T) { + testSet := []struct { + Str string + Expected Version + }{ + {"tarsum+sha256:e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b", Version0}, + {"tarsum+sha256", Version0}, + {"tarsum", Version0}, + {"tarsum.dev", VersionDev}, + {"tarsum.dev+sha256:deadbeef", VersionDev}, + } + + for _, ts := range testSet { + v, err := GetVersionFromTarsum(ts.Str) + if err != nil { + t.Fatalf("%q : %s", err, ts.Str) + } + if v != ts.Expected { + t.Errorf("expected %d (%q), got %d (%q)", ts.Expected, ts.Expected, v, v) + } + } + + // test one that does not exist, to ensure it errors + str := "weak+md5:abcdeabcde" + _, err := GetVersionFromTarsum(str) + if err != ErrNotVersion { + t.Fatalf("%q : %s", err, str) + } +} + +func TestGetVersions(t *testing.T) { + expected := []Version{ + Version0, + Version1, + VersionDev, + } + versions := GetVersions() + if len(versions) != len(expected) { + t.Fatalf("Expected %v versions, got %v", len(expected), len(versions)) + } + if !containsVersion(versions, expected[0]) || !containsVersion(versions, expected[1]) || !containsVersion(versions, expected[2]) { + t.Fatalf("Expected [%v], got [%v]", expected, versions) + } +} + +func containsVersion(versions []Version, version Version) bool { + for _, v := range versions { + if v == version { + return true + } + } + return false +} diff --git a/pkg/tarsum/writercloser.go b/pkg/tarsum/writercloser.go new file mode 100644 index 00000000..9727ecde --- /dev/null +++ b/pkg/tarsum/writercloser.go @@ -0,0 +1,22 @@ +package tarsum + +import ( + "io" +) + +type writeCloseFlusher interface { + io.WriteCloser + Flush() error +} + +type nopCloseFlusher struct { + io.Writer +} + +func (n *nopCloseFlusher) Close() error { + return nil +} + +func (n *nopCloseFlusher) Flush() error { + return nil +} diff --git a/pkg/term/ascii.go b/pkg/term/ascii.go new file mode 100644 index 00000000..f5262bcc --- /dev/null +++ b/pkg/term/ascii.go @@ -0,0 +1,66 @@ +package term + +import ( + "fmt" + "strings" +) + +// ASCII list the possible supported ASCII key sequence +var ASCII = []string{ + "ctrl-@", + "ctrl-a", + "ctrl-b", + "ctrl-c", + "ctrl-d", + "ctrl-e", + "ctrl-f", + "ctrl-g", + "ctrl-h", + "ctrl-i", + "ctrl-j", + "ctrl-k", + "ctrl-l", + "ctrl-m", + "ctrl-n", + "ctrl-o", + "ctrl-p", + "ctrl-q", + "ctrl-r", + "ctrl-s", + "ctrl-t", + "ctrl-u", + "ctrl-v", + "ctrl-w", + "ctrl-x", + "ctrl-y", + "ctrl-z", + "ctrl-[", + "ctrl-\\", + "ctrl-]", + "ctrl-^", + "ctrl-_", +} + +// ToBytes converts a string representing a suite of key-sequence to the corresponding ASCII code. +func ToBytes(keys string) ([]byte, error) { + codes := []byte{} +next: + for _, key := range strings.Split(keys, ",") { + if len(key) != 1 { + for code, ctrl := range ASCII { + if ctrl == key { + codes = append(codes, byte(code)) + continue next + } + } + if key == "DEL" { + codes = append(codes, 127) + } else { + return nil, fmt.Errorf("Unknown character: '%s'", key) + } + } else { + codes = append(codes, byte(key[0])) + } + } + return codes, nil +} diff --git a/pkg/term/ascii_test.go b/pkg/term/ascii_test.go new file mode 100644 index 00000000..4a1e7f30 --- /dev/null +++ b/pkg/term/ascii_test.go @@ -0,0 +1,43 @@ +package term + +import "testing" + +func TestToBytes(t *testing.T) { + codes, err := ToBytes("ctrl-a,a") + if err != nil { + t.Fatal(err) + } + if len(codes) != 2 { + t.Fatalf("Expected 2 codes, got %d", len(codes)) + } + if codes[0] != 1 || codes[1] != 97 { + t.Fatalf("Expected '1' '97', got '%d' '%d'", codes[0], codes[1]) + } + + codes, err = ToBytes("shift-z") + if err == nil { + t.Fatalf("Expected error, got none") + } + + codes, err = ToBytes("ctrl-@,ctrl-[,~,ctrl-o") + if err != nil { + t.Fatal(err) + } + if len(codes) != 4 { + t.Fatalf("Expected 4 codes, got %d", len(codes)) + } + if codes[0] != 0 || codes[1] != 27 || codes[2] != 126 || codes[3] != 15 { + t.Fatalf("Expected '0' '27' '126', '15', got '%d' '%d' '%d' '%d'", codes[0], codes[1], codes[2], codes[3]) + } + + codes, err = ToBytes("DEL,+") + if err != nil { + t.Fatal(err) + } + if len(codes) != 2 { + t.Fatalf("Expected 2 codes, got %d", len(codes)) + } + if codes[0] != 127 || codes[1] != 43 { + t.Fatalf("Expected '127 '43'', got '%d' '%d'", codes[0], codes[1]) + } +} diff --git a/pkg/term/tc_linux_cgo.go b/pkg/term/tc_linux_cgo.go new file mode 100644 index 00000000..a22cd9d1 --- /dev/null +++ b/pkg/term/tc_linux_cgo.go @@ -0,0 +1,50 @@ +// +build linux,cgo + +package term + +import ( + "syscall" + "unsafe" +) + +// #include +import "C" + +// Termios is the Unix API for terminal I/O. +// It is passthgrouh for syscall.Termios in order to make it portable with +// other platforms where it is not available or handled differently. +type Termios syscall.Termios + +// MakeRaw put the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +func MakeRaw(fd uintptr) (*State, error) { + var oldState State + if err := tcget(fd, &oldState.termios); err != 0 { + return nil, err + } + + newState := oldState.termios + + C.cfmakeraw((*C.struct_termios)(unsafe.Pointer(&newState))) + if err := tcset(fd, &newState); err != 0 { + return nil, err + } + return &oldState, nil +} + +func tcget(fd uintptr, p *Termios) syscall.Errno { + ret, err := C.tcgetattr(C.int(fd), (*C.struct_termios)(unsafe.Pointer(p))) + if ret != 0 { + return err.(syscall.Errno) + } + return 0 +} + +func tcset(fd uintptr, p *Termios) syscall.Errno { + ret, err := C.tcsetattr(C.int(fd), C.TCSANOW, (*C.struct_termios)(unsafe.Pointer(p))) + if ret != 0 { + return err.(syscall.Errno) + } + return 0 +} diff --git a/pkg/term/tc_other.go b/pkg/term/tc_other.go new file mode 100644 index 00000000..266039ba --- /dev/null +++ b/pkg/term/tc_other.go @@ -0,0 +1,19 @@ +// +build !windows +// +build !linux !cgo + +package term + +import ( + "syscall" + "unsafe" +) + +func tcget(fd uintptr, p *Termios) syscall.Errno { + _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(getTermios), uintptr(unsafe.Pointer(p))) + return err +} + +func tcset(fd uintptr, p *Termios) syscall.Errno { + _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, setTermios, uintptr(unsafe.Pointer(p))) + return err +} diff --git a/pkg/term/term.go b/pkg/term/term.go new file mode 100644 index 00000000..11ed2093 --- /dev/null +++ b/pkg/term/term.go @@ -0,0 +1,131 @@ +// +build !windows + +// Package term provides provides structures and helper functions to work with +// terminal (state, sizes). +package term + +import ( + "errors" + "io" + "os" + "os/signal" + "syscall" + "unsafe" +) + +var ( + // ErrInvalidState is returned if the state of the terminal is invalid. + ErrInvalidState = errors.New("Invalid terminal state") +) + +// State represents the state of the terminal. +type State struct { + termios Termios +} + +// Winsize represents the size of the terminal window. +type Winsize struct { + Height uint16 + Width uint16 + x uint16 + y uint16 +} + +// StdStreams returns the standard streams (stdin, stdout, stedrr). +func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) { + return os.Stdin, os.Stdout, os.Stderr +} + +// GetFdInfo returns the file descriptor for an os.File and indicates whether the file represents a terminal. +func GetFdInfo(in interface{}) (uintptr, bool) { + var inFd uintptr + var isTerminalIn bool + if file, ok := in.(*os.File); ok { + inFd = file.Fd() + isTerminalIn = IsTerminal(inFd) + } + return inFd, isTerminalIn +} + +// GetWinsize returns the window size based on the specified file descriptor. +func GetWinsize(fd uintptr) (*Winsize, error) { + ws := &Winsize{} + _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(ws))) + // Skip errno = 0 + if err == 0 { + return ws, nil + } + return ws, err +} + +// SetWinsize tries to set the specified window size for the specified file descriptor. +func SetWinsize(fd uintptr, ws *Winsize) error { + _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCSWINSZ), uintptr(unsafe.Pointer(ws))) + // Skip errno = 0 + if err == 0 { + return nil + } + return err +} + +// IsTerminal returns true if the given file descriptor is a terminal. +func IsTerminal(fd uintptr) bool { + var termios Termios + return tcget(fd, &termios) == 0 +} + +// RestoreTerminal restores the terminal connected to the given file descriptor +// to a previous state. +func RestoreTerminal(fd uintptr, state *State) error { + if state == nil { + return ErrInvalidState + } + if err := tcset(fd, &state.termios); err != 0 { + return err + } + return nil +} + +// SaveState saves the state of the terminal connected to the given file descriptor. +func SaveState(fd uintptr) (*State, error) { + var oldState State + if err := tcget(fd, &oldState.termios); err != 0 { + return nil, err + } + + return &oldState, nil +} + +// DisableEcho applies the specified state to the terminal connected to the file +// descriptor, with echo disabled. +func DisableEcho(fd uintptr, state *State) error { + newState := state.termios + newState.Lflag &^= syscall.ECHO + + if err := tcset(fd, &newState); err != 0 { + return err + } + handleInterrupt(fd, state) + return nil +} + +// SetRawTerminal puts the terminal connected to the given file descriptor into +// raw mode and returns the previous state. +func SetRawTerminal(fd uintptr) (*State, error) { + oldState, err := MakeRaw(fd) + if err != nil { + return nil, err + } + handleInterrupt(fd, oldState) + return oldState, err +} + +func handleInterrupt(fd uintptr, state *State) { + sigchan := make(chan os.Signal, 1) + signal.Notify(sigchan, os.Interrupt) + + go func() { + _ = <-sigchan + RestoreTerminal(fd, state) + }() +} diff --git a/pkg/term/term_windows.go b/pkg/term/term_windows.go new file mode 100644 index 00000000..3101c80c --- /dev/null +++ b/pkg/term/term_windows.go @@ -0,0 +1,305 @@ +// +build windows + +package term + +import ( + "io" + "os" + "os/signal" + "syscall" + + "github.com/Azure/go-ansiterm/winterm" + "github.com/docker/docker/pkg/system" + "github.com/docker/docker/pkg/term/windows" +) + +// State holds the console mode for the terminal. +type State struct { + inMode, outMode uint32 + inHandle, outHandle syscall.Handle +} + +// Winsize is used for window size. +type Winsize struct { + Height uint16 + Width uint16 + x uint16 + y uint16 +} + +const ( + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms683167(v=vs.85).aspx + enableVirtualTerminalInput = 0x0200 + enableVirtualTerminalProcessing = 0x0004 +) + +// usingNativeConsole is true if we are using the Windows native console +var usingNativeConsole bool + +// StdStreams returns the standard streams (stdin, stdout, stedrr). +func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) { + switch { + case os.Getenv("ConEmuANSI") == "ON": + // The ConEmu terminal emulates ANSI on output streams well. + return windows.ConEmuStreams() + case os.Getenv("MSYSTEM") != "": + // MSYS (mingw) does not emulate ANSI well. + return windows.ConsoleStreams() + default: + if useNativeConsole() { + usingNativeConsole = true + return os.Stdin, os.Stdout, os.Stderr + } + return windows.ConsoleStreams() + } +} + +// useNativeConsole determines if the docker client should use the built-in +// console which supports ANSI emulation, or fall-back to the golang emulator +// (github.com/azure/go-ansiterm). +func useNativeConsole() bool { + osv, err := system.GetOSVersion() + if err != nil { + return false + } + + // Native console is not available before major version 10 + if osv.MajorVersion < 10 { + return false + } + + // Must have a late pre-release TP4 build of Windows Server 2016/Windows 10 TH2 or later + if osv.Build < 10578 { + return false + } + + // Get the console modes. If this fails, we can't use the native console + state, err := getNativeConsole() + if err != nil { + return false + } + + // Probe the console to see if it can be enabled. + if nil != probeNativeConsole(state) { + return false + } + + // Environment variable override + if e := os.Getenv("USE_NATIVE_CONSOLE"); e != "" { + if e == "1" { + return true + } + return false + } + + // TODO Windows. The native emulator still has issues which + // mean it shouldn't be enabled for everyone. Change this next line to true + // to change the default to "enable if available". In the meantime, users + // can still try it out by using USE_NATIVE_CONSOLE env variable. + return false +} + +// getNativeConsole returns the console modes ('state') for the native Windows console +func getNativeConsole() (State, error) { + var ( + err error + state State + ) + + // Get the handle to stdout + if state.outHandle, err = syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE); err != nil { + return state, err + } + + // Get the console mode from the consoles stdout handle + if err = syscall.GetConsoleMode(state.outHandle, &state.outMode); err != nil { + return state, err + } + + // Get the handle to stdin + if state.inHandle, err = syscall.GetStdHandle(syscall.STD_INPUT_HANDLE); err != nil { + return state, err + } + + // Get the console mode from the consoles stdin handle + if err = syscall.GetConsoleMode(state.inHandle, &state.inMode); err != nil { + return state, err + } + + return state, nil +} + +// probeNativeConsole probes the console to determine if native can be supported, +func probeNativeConsole(state State) error { + if err := winterm.SetConsoleMode(uintptr(state.outHandle), state.outMode|enableVirtualTerminalProcessing); err != nil { + return err + } + defer winterm.SetConsoleMode(uintptr(state.outHandle), state.outMode) + + if err := winterm.SetConsoleMode(uintptr(state.inHandle), state.inMode|enableVirtualTerminalInput); err != nil { + return err + } + defer winterm.SetConsoleMode(uintptr(state.inHandle), state.inMode) + + return nil +} + +// enableNativeConsole turns on native console mode +func enableNativeConsole(state State) error { + if err := winterm.SetConsoleMode(uintptr(state.outHandle), state.outMode|enableVirtualTerminalProcessing); err != nil { + return err + } + + if err := winterm.SetConsoleMode(uintptr(state.inHandle), state.inMode|enableVirtualTerminalInput); err != nil { + winterm.SetConsoleMode(uintptr(state.outHandle), state.outMode) // restore out if we can + return err + } + + return nil +} + +// disableNativeConsole turns off native console mode +func disableNativeConsole(state *State) error { + // Try and restore both in an out before error checking. + errout := winterm.SetConsoleMode(uintptr(state.outHandle), state.outMode) + errin := winterm.SetConsoleMode(uintptr(state.inHandle), state.inMode) + if errout != nil { + return errout + } + if errin != nil { + return errin + } + return nil +} + +// GetFdInfo returns the file descriptor for an os.File and indicates whether the file represents a terminal. +func GetFdInfo(in interface{}) (uintptr, bool) { + return windows.GetHandleInfo(in) +} + +// GetWinsize returns the window size based on the specified file descriptor. +func GetWinsize(fd uintptr) (*Winsize, error) { + info, err := winterm.GetConsoleScreenBufferInfo(fd) + if err != nil { + return nil, err + } + + winsize := &Winsize{ + Width: uint16(info.Window.Right - info.Window.Left + 1), + Height: uint16(info.Window.Bottom - info.Window.Top + 1), + x: 0, + y: 0} + + return winsize, nil +} + +// IsTerminal returns true if the given file descriptor is a terminal. +func IsTerminal(fd uintptr) bool { + return windows.IsConsole(fd) +} + +// RestoreTerminal restores the terminal connected to the given file descriptor +// to a previous state. +func RestoreTerminal(fd uintptr, state *State) error { + if usingNativeConsole { + return disableNativeConsole(state) + } + return winterm.SetConsoleMode(fd, state.outMode) +} + +// SaveState saves the state of the terminal connected to the given file descriptor. +func SaveState(fd uintptr) (*State, error) { + if usingNativeConsole { + state, err := getNativeConsole() + if err != nil { + return nil, err + } + return &state, nil + } + + mode, e := winterm.GetConsoleMode(fd) + if e != nil { + return nil, e + } + + return &State{outMode: mode}, nil +} + +// DisableEcho disables echo for the terminal connected to the given file descriptor. +// -- See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx +func DisableEcho(fd uintptr, state *State) error { + mode := state.inMode + mode &^= winterm.ENABLE_ECHO_INPUT + mode |= winterm.ENABLE_PROCESSED_INPUT | winterm.ENABLE_LINE_INPUT + err := winterm.SetConsoleMode(fd, mode) + if err != nil { + return err + } + + // Register an interrupt handler to catch and restore prior state + restoreAtInterrupt(fd, state) + return nil +} + +// SetRawTerminal puts the terminal connected to the given file descriptor into raw +// mode and returns the previous state. +func SetRawTerminal(fd uintptr) (*State, error) { + state, err := MakeRaw(fd) + if err != nil { + return nil, err + } + + // Register an interrupt handler to catch and restore prior state + restoreAtInterrupt(fd, state) + return state, err +} + +// MakeRaw puts the terminal (Windows Console) connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be restored. +func MakeRaw(fd uintptr) (*State, error) { + state, err := SaveState(fd) + if err != nil { + return nil, err + } + + mode := state.inMode + if usingNativeConsole { + if err := enableNativeConsole(*state); err != nil { + return nil, err + } + mode |= enableVirtualTerminalInput + } + + // See + // -- https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx + // -- https://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx + + // Disable these modes + mode &^= winterm.ENABLE_ECHO_INPUT + mode &^= winterm.ENABLE_LINE_INPUT + mode &^= winterm.ENABLE_MOUSE_INPUT + mode &^= winterm.ENABLE_WINDOW_INPUT + mode &^= winterm.ENABLE_PROCESSED_INPUT + + // Enable these modes + mode |= winterm.ENABLE_EXTENDED_FLAGS + mode |= winterm.ENABLE_INSERT_MODE + mode |= winterm.ENABLE_QUICK_EDIT_MODE + + err = winterm.SetConsoleMode(fd, mode) + if err != nil { + return nil, err + } + return state, nil +} + +func restoreAtInterrupt(fd uintptr, state *State) { + sigchan := make(chan os.Signal, 1) + signal.Notify(sigchan, os.Interrupt) + + go func() { + _ = <-sigchan + RestoreTerminal(fd, state) + os.Exit(0) + }() +} diff --git a/pkg/term/termios_darwin.go b/pkg/term/termios_darwin.go new file mode 100644 index 00000000..480db900 --- /dev/null +++ b/pkg/term/termios_darwin.go @@ -0,0 +1,69 @@ +package term + +import ( + "syscall" + "unsafe" +) + +const ( + getTermios = syscall.TIOCGETA + setTermios = syscall.TIOCSETA +) + +// Termios magic numbers, passthrough to the ones defined in syscall. +const ( + IGNBRK = syscall.IGNBRK + PARMRK = syscall.PARMRK + INLCR = syscall.INLCR + IGNCR = syscall.IGNCR + ECHONL = syscall.ECHONL + CSIZE = syscall.CSIZE + ICRNL = syscall.ICRNL + ISTRIP = syscall.ISTRIP + PARENB = syscall.PARENB + ECHO = syscall.ECHO + ICANON = syscall.ICANON + ISIG = syscall.ISIG + IXON = syscall.IXON + BRKINT = syscall.BRKINT + INPCK = syscall.INPCK + OPOST = syscall.OPOST + CS8 = syscall.CS8 + IEXTEN = syscall.IEXTEN +) + +// Termios is the Unix API for terminal I/O. +type Termios struct { + Iflag uint64 + Oflag uint64 + Cflag uint64 + Lflag uint64 + Cc [20]byte + Ispeed uint64 + Ospeed uint64 +} + +// MakeRaw put the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +func MakeRaw(fd uintptr) (*State, error) { + var oldState State + if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(getTermios), uintptr(unsafe.Pointer(&oldState.termios))); err != 0 { + return nil, err + } + + newState := oldState.termios + newState.Iflag &^= (IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON) + newState.Oflag &^= OPOST + newState.Lflag &^= (ECHO | ECHONL | ICANON | ISIG | IEXTEN) + newState.Cflag &^= (CSIZE | PARENB) + newState.Cflag |= CS8 + newState.Cc[syscall.VMIN] = 1 + newState.Cc[syscall.VTIME] = 0 + + if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(setTermios), uintptr(unsafe.Pointer(&newState))); err != 0 { + return nil, err + } + + return &oldState, nil +} diff --git a/pkg/term/termios_freebsd.go b/pkg/term/termios_freebsd.go new file mode 100644 index 00000000..ed843ad6 --- /dev/null +++ b/pkg/term/termios_freebsd.go @@ -0,0 +1,69 @@ +package term + +import ( + "syscall" + "unsafe" +) + +const ( + getTermios = syscall.TIOCGETA + setTermios = syscall.TIOCSETA +) + +// Termios magic numbers, passthrough to the ones defined in syscall. +const ( + IGNBRK = syscall.IGNBRK + PARMRK = syscall.PARMRK + INLCR = syscall.INLCR + IGNCR = syscall.IGNCR + ECHONL = syscall.ECHONL + CSIZE = syscall.CSIZE + ICRNL = syscall.ICRNL + ISTRIP = syscall.ISTRIP + PARENB = syscall.PARENB + ECHO = syscall.ECHO + ICANON = syscall.ICANON + ISIG = syscall.ISIG + IXON = syscall.IXON + BRKINT = syscall.BRKINT + INPCK = syscall.INPCK + OPOST = syscall.OPOST + CS8 = syscall.CS8 + IEXTEN = syscall.IEXTEN +) + +// Termios is the Unix API for terminal I/O. +type Termios struct { + Iflag uint32 + Oflag uint32 + Cflag uint32 + Lflag uint32 + Cc [20]byte + Ispeed uint32 + Ospeed uint32 +} + +// MakeRaw put the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +func MakeRaw(fd uintptr) (*State, error) { + var oldState State + if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(getTermios), uintptr(unsafe.Pointer(&oldState.termios))); err != 0 { + return nil, err + } + + newState := oldState.termios + newState.Iflag &^= (IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON) + newState.Oflag &^= OPOST + newState.Lflag &^= (ECHO | ECHONL | ICANON | ISIG | IEXTEN) + newState.Cflag &^= (CSIZE | PARENB) + newState.Cflag |= CS8 + newState.Cc[syscall.VMIN] = 1 + newState.Cc[syscall.VTIME] = 0 + + if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(setTermios), uintptr(unsafe.Pointer(&newState))); err != 0 { + return nil, err + } + + return &oldState, nil +} diff --git a/pkg/term/termios_linux.go b/pkg/term/termios_linux.go new file mode 100644 index 00000000..22921b6a --- /dev/null +++ b/pkg/term/termios_linux.go @@ -0,0 +1,47 @@ +// +build !cgo + +package term + +import ( + "syscall" + "unsafe" +) + +const ( + getTermios = syscall.TCGETS + setTermios = syscall.TCSETS +) + +// Termios is the Unix API for terminal I/O. +type Termios struct { + Iflag uint32 + Oflag uint32 + Cflag uint32 + Lflag uint32 + Cc [20]byte + Ispeed uint32 + Ospeed uint32 +} + +// MakeRaw put the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +func MakeRaw(fd uintptr) (*State, error) { + var oldState State + if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, getTermios, uintptr(unsafe.Pointer(&oldState.termios))); err != 0 { + return nil, err + } + + newState := oldState.termios + + newState.Iflag &^= (syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON) + newState.Oflag &^= syscall.OPOST + newState.Lflag &^= (syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN) + newState.Cflag &^= (syscall.CSIZE | syscall.PARENB) + newState.Cflag |= syscall.CS8 + + if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, setTermios, uintptr(unsafe.Pointer(&newState))); err != 0 { + return nil, err + } + return &oldState, nil +} diff --git a/pkg/term/termios_openbsd.go b/pkg/term/termios_openbsd.go new file mode 100644 index 00000000..ed843ad6 --- /dev/null +++ b/pkg/term/termios_openbsd.go @@ -0,0 +1,69 @@ +package term + +import ( + "syscall" + "unsafe" +) + +const ( + getTermios = syscall.TIOCGETA + setTermios = syscall.TIOCSETA +) + +// Termios magic numbers, passthrough to the ones defined in syscall. +const ( + IGNBRK = syscall.IGNBRK + PARMRK = syscall.PARMRK + INLCR = syscall.INLCR + IGNCR = syscall.IGNCR + ECHONL = syscall.ECHONL + CSIZE = syscall.CSIZE + ICRNL = syscall.ICRNL + ISTRIP = syscall.ISTRIP + PARENB = syscall.PARENB + ECHO = syscall.ECHO + ICANON = syscall.ICANON + ISIG = syscall.ISIG + IXON = syscall.IXON + BRKINT = syscall.BRKINT + INPCK = syscall.INPCK + OPOST = syscall.OPOST + CS8 = syscall.CS8 + IEXTEN = syscall.IEXTEN +) + +// Termios is the Unix API for terminal I/O. +type Termios struct { + Iflag uint32 + Oflag uint32 + Cflag uint32 + Lflag uint32 + Cc [20]byte + Ispeed uint32 + Ospeed uint32 +} + +// MakeRaw put the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +func MakeRaw(fd uintptr) (*State, error) { + var oldState State + if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(getTermios), uintptr(unsafe.Pointer(&oldState.termios))); err != 0 { + return nil, err + } + + newState := oldState.termios + newState.Iflag &^= (IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON) + newState.Oflag &^= OPOST + newState.Lflag &^= (ECHO | ECHONL | ICANON | ISIG | IEXTEN) + newState.Cflag &^= (CSIZE | PARENB) + newState.Cflag |= CS8 + newState.Cc[syscall.VMIN] = 1 + newState.Cc[syscall.VTIME] = 0 + + if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(setTermios), uintptr(unsafe.Pointer(&newState))); err != 0 { + return nil, err + } + + return &oldState, nil +} diff --git a/pkg/term/windows/ansi_reader.go b/pkg/term/windows/ansi_reader.go new file mode 100644 index 00000000..3bf2b2b6 --- /dev/null +++ b/pkg/term/windows/ansi_reader.go @@ -0,0 +1,257 @@ +// +build windows + +package windows + +import ( + "bytes" + "errors" + "fmt" + "os" + "strings" + "unsafe" + + ansiterm "github.com/Azure/go-ansiterm" + "github.com/Azure/go-ansiterm/winterm" +) + +const ( + escapeSequence = ansiterm.KEY_ESC_CSI +) + +// ansiReader wraps a standard input file (e.g., os.Stdin) providing ANSI sequence translation. +type ansiReader struct { + file *os.File + fd uintptr + buffer []byte + cbBuffer int + command []byte +} + +func newAnsiReader(nFile int) *ansiReader { + file, fd := winterm.GetStdFile(nFile) + return &ansiReader{ + file: file, + fd: fd, + command: make([]byte, 0, ansiterm.ANSI_MAX_CMD_LENGTH), + buffer: make([]byte, 0), + } +} + +// Close closes the wrapped file. +func (ar *ansiReader) Close() (err error) { + return ar.file.Close() +} + +// Fd returns the file descriptor of the wrapped file. +func (ar *ansiReader) Fd() uintptr { + return ar.fd +} + +// Read reads up to len(p) bytes of translated input events into p. +func (ar *ansiReader) Read(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + + // Previously read bytes exist, read as much as we can and return + if len(ar.buffer) > 0 { + logger.Debugf("Reading previously cached bytes") + + originalLength := len(ar.buffer) + copiedLength := copy(p, ar.buffer) + + if copiedLength == originalLength { + ar.buffer = make([]byte, 0, len(p)) + } else { + ar.buffer = ar.buffer[copiedLength:] + } + + logger.Debugf("Read from cache p[%d]: % x", copiedLength, p) + return copiedLength, nil + } + + // Read and translate key events + events, err := readInputEvents(ar.fd, len(p)) + if err != nil { + return 0, err + } else if len(events) == 0 { + logger.Debug("No input events detected") + return 0, nil + } + + keyBytes := translateKeyEvents(events, []byte(escapeSequence)) + + // Save excess bytes and right-size keyBytes + if len(keyBytes) > len(p) { + logger.Debugf("Received %d keyBytes, only room for %d bytes", len(keyBytes), len(p)) + ar.buffer = keyBytes[len(p):] + keyBytes = keyBytes[:len(p)] + } else if len(keyBytes) == 0 { + logger.Debug("No key bytes returned from the translator") + return 0, nil + } + + copiedLength := copy(p, keyBytes) + if copiedLength != len(keyBytes) { + return 0, errors.New("Unexpected copy length encountered.") + } + + logger.Debugf("Read p[%d]: % x", copiedLength, p) + logger.Debugf("Read keyBytes[%d]: % x", copiedLength, keyBytes) + return copiedLength, nil +} + +// readInputEvents polls until at least one event is available. +func readInputEvents(fd uintptr, maxBytes int) ([]winterm.INPUT_RECORD, error) { + // Determine the maximum number of records to retrieve + // -- Cast around the type system to obtain the size of a single INPUT_RECORD. + // unsafe.Sizeof requires an expression vs. a type-reference; the casting + // tricks the type system into believing it has such an expression. + recordSize := int(unsafe.Sizeof(*((*winterm.INPUT_RECORD)(unsafe.Pointer(&maxBytes))))) + countRecords := maxBytes / recordSize + if countRecords > ansiterm.MAX_INPUT_EVENTS { + countRecords = ansiterm.MAX_INPUT_EVENTS + } + logger.Debugf("[windows] readInputEvents: Reading %v records (buffer size %v, record size %v)", countRecords, maxBytes, recordSize) + + // Wait for and read input events + events := make([]winterm.INPUT_RECORD, countRecords) + nEvents := uint32(0) + eventsExist, err := winterm.WaitForSingleObject(fd, winterm.WAIT_INFINITE) + if err != nil { + return nil, err + } + + if eventsExist { + err = winterm.ReadConsoleInput(fd, events, &nEvents) + if err != nil { + return nil, err + } + } + + // Return a slice restricted to the number of returned records + logger.Debugf("[windows] readInputEvents: Read %v events", nEvents) + return events[:nEvents], nil +} + +// KeyEvent Translation Helpers + +var arrowKeyMapPrefix = map[winterm.WORD]string{ + winterm.VK_UP: "%s%sA", + winterm.VK_DOWN: "%s%sB", + winterm.VK_RIGHT: "%s%sC", + winterm.VK_LEFT: "%s%sD", +} + +var keyMapPrefix = map[winterm.WORD]string{ + winterm.VK_UP: "\x1B[%sA", + winterm.VK_DOWN: "\x1B[%sB", + winterm.VK_RIGHT: "\x1B[%sC", + winterm.VK_LEFT: "\x1B[%sD", + winterm.VK_HOME: "\x1B[1%s~", // showkey shows ^[[1 + winterm.VK_END: "\x1B[4%s~", // showkey shows ^[[4 + winterm.VK_INSERT: "\x1B[2%s~", + winterm.VK_DELETE: "\x1B[3%s~", + winterm.VK_PRIOR: "\x1B[5%s~", + winterm.VK_NEXT: "\x1B[6%s~", + winterm.VK_F1: "", + winterm.VK_F2: "", + winterm.VK_F3: "\x1B[13%s~", + winterm.VK_F4: "\x1B[14%s~", + winterm.VK_F5: "\x1B[15%s~", + winterm.VK_F6: "\x1B[17%s~", + winterm.VK_F7: "\x1B[18%s~", + winterm.VK_F8: "\x1B[19%s~", + winterm.VK_F9: "\x1B[20%s~", + winterm.VK_F10: "\x1B[21%s~", + winterm.VK_F11: "\x1B[23%s~", + winterm.VK_F12: "\x1B[24%s~", +} + +// translateKeyEvents converts the input events into the appropriate ANSI string. +func translateKeyEvents(events []winterm.INPUT_RECORD, escapeSequence []byte) []byte { + var buffer bytes.Buffer + for _, event := range events { + if event.EventType == winterm.KEY_EVENT && event.KeyEvent.KeyDown != 0 { + buffer.WriteString(keyToString(&event.KeyEvent, escapeSequence)) + } + } + + return buffer.Bytes() +} + +// keyToString maps the given input event record to the corresponding string. +func keyToString(keyEvent *winterm.KEY_EVENT_RECORD, escapeSequence []byte) string { + if keyEvent.UnicodeChar == 0 { + return formatVirtualKey(keyEvent.VirtualKeyCode, keyEvent.ControlKeyState, escapeSequence) + } + + _, alt, control := getControlKeys(keyEvent.ControlKeyState) + if control { + // TODO(azlinux): Implement following control sequences + // -D Signals the end of input from the keyboard; also exits current shell. + // -H Deletes the first character to the left of the cursor. Also called the ERASE key. + // -Q Restarts printing after it has been stopped with -s. + // -S Suspends printing on the screen (does not stop the program). + // -U Deletes all characters on the current line. Also called the KILL key. + // -E Quits current command and creates a core + + } + + // +Key generates ESC N Key + if !control && alt { + return ansiterm.KEY_ESC_N + strings.ToLower(string(keyEvent.UnicodeChar)) + } + + return string(keyEvent.UnicodeChar) +} + +// formatVirtualKey converts a virtual key (e.g., up arrow) into the appropriate ANSI string. +func formatVirtualKey(key winterm.WORD, controlState winterm.DWORD, escapeSequence []byte) string { + shift, alt, control := getControlKeys(controlState) + modifier := getControlKeysModifier(shift, alt, control) + + if format, ok := arrowKeyMapPrefix[key]; ok { + return fmt.Sprintf(format, escapeSequence, modifier) + } + + if format, ok := keyMapPrefix[key]; ok { + return fmt.Sprintf(format, modifier) + } + + return "" +} + +// getControlKeys extracts the shift, alt, and ctrl key states. +func getControlKeys(controlState winterm.DWORD) (shift, alt, control bool) { + shift = 0 != (controlState & winterm.SHIFT_PRESSED) + alt = 0 != (controlState & (winterm.LEFT_ALT_PRESSED | winterm.RIGHT_ALT_PRESSED)) + control = 0 != (controlState & (winterm.LEFT_CTRL_PRESSED | winterm.RIGHT_CTRL_PRESSED)) + return shift, alt, control +} + +// getControlKeysModifier returns the ANSI modifier for the given combination of control keys. +func getControlKeysModifier(shift, alt, control bool) string { + if shift && alt && control { + return ansiterm.KEY_CONTROL_PARAM_8 + } + if alt && control { + return ansiterm.KEY_CONTROL_PARAM_7 + } + if shift && control { + return ansiterm.KEY_CONTROL_PARAM_6 + } + if control { + return ansiterm.KEY_CONTROL_PARAM_5 + } + if shift && alt { + return ansiterm.KEY_CONTROL_PARAM_4 + } + if alt { + return ansiterm.KEY_CONTROL_PARAM_3 + } + if shift { + return ansiterm.KEY_CONTROL_PARAM_2 + } + return "" +} diff --git a/pkg/term/windows/ansi_writer.go b/pkg/term/windows/ansi_writer.go new file mode 100644 index 00000000..9f3232c0 --- /dev/null +++ b/pkg/term/windows/ansi_writer.go @@ -0,0 +1,76 @@ +// +build windows + +package windows + +import ( + "io/ioutil" + "os" + + ansiterm "github.com/Azure/go-ansiterm" + "github.com/Azure/go-ansiterm/winterm" + "github.com/Sirupsen/logrus" +) + +var logger *logrus.Logger + +// ansiWriter wraps a standard output file (e.g., os.Stdout) providing ANSI sequence translation. +type ansiWriter struct { + file *os.File + fd uintptr + infoReset *winterm.CONSOLE_SCREEN_BUFFER_INFO + command []byte + escapeSequence []byte + inAnsiSequence bool + parser *ansiterm.AnsiParser +} + +func newAnsiWriter(nFile int) *ansiWriter { + logFile := ioutil.Discard + + if isDebugEnv := os.Getenv(ansiterm.LogEnv); isDebugEnv == "1" { + logFile, _ = os.Create("ansiReaderWriter.log") + } + + logger = &logrus.Logger{ + Out: logFile, + Formatter: new(logrus.TextFormatter), + Level: logrus.DebugLevel, + } + + file, fd := winterm.GetStdFile(nFile) + info, err := winterm.GetConsoleScreenBufferInfo(fd) + if err != nil { + return nil + } + + parser := ansiterm.CreateParser("Ground", winterm.CreateWinEventHandler(fd, file)) + logger.Infof("newAnsiWriter: parser %p", parser) + + aw := &ansiWriter{ + file: file, + fd: fd, + infoReset: info, + command: make([]byte, 0, ansiterm.ANSI_MAX_CMD_LENGTH), + escapeSequence: []byte(ansiterm.KEY_ESC_CSI), + parser: parser, + } + + logger.Infof("newAnsiWriter: aw.parser %p", aw.parser) + logger.Infof("newAnsiWriter: %v", aw) + return aw +} + +func (aw *ansiWriter) Fd() uintptr { + return aw.fd +} + +// Write writes len(p) bytes from p to the underlying data stream. +func (aw *ansiWriter) Write(p []byte) (total int, err error) { + if len(p) == 0 { + return 0, nil + } + + logger.Infof("Write: % x", p) + logger.Infof("Write: %s", string(p)) + return aw.parser.Parse(p) +} diff --git a/pkg/term/windows/console.go b/pkg/term/windows/console.go new file mode 100644 index 00000000..3036a046 --- /dev/null +++ b/pkg/term/windows/console.go @@ -0,0 +1,97 @@ +// +build windows + +package windows + +import ( + "io" + "os" + "syscall" + + "github.com/Azure/go-ansiterm/winterm" + + ansiterm "github.com/Azure/go-ansiterm" + "github.com/Sirupsen/logrus" + "io/ioutil" +) + +// ConEmuStreams returns prepared versions of console streams, +// for proper use in ConEmu terminal. +// The ConEmu terminal emulates ANSI on output streams well by default. +func ConEmuStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) { + if IsConsole(os.Stdin.Fd()) { + stdIn = newAnsiReader(syscall.STD_INPUT_HANDLE) + } else { + stdIn = os.Stdin + } + + stdOut = os.Stdout + stdErr = os.Stderr + + // WARNING (BEGIN): sourced from newAnsiWriter + + logFile := ioutil.Discard + + if isDebugEnv := os.Getenv(ansiterm.LogEnv); isDebugEnv == "1" { + logFile, _ = os.Create("ansiReaderWriter.log") + } + + logger = &logrus.Logger{ + Out: logFile, + Formatter: new(logrus.TextFormatter), + Level: logrus.DebugLevel, + } + + // WARNING (END): sourced from newAnsiWriter + + return stdIn, stdOut, stdErr +} + +// ConsoleStreams returns a wrapped version for each standard stream referencing a console, +// that handles ANSI character sequences. +func ConsoleStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) { + if IsConsole(os.Stdin.Fd()) { + stdIn = newAnsiReader(syscall.STD_INPUT_HANDLE) + } else { + stdIn = os.Stdin + } + + if IsConsole(os.Stdout.Fd()) { + stdOut = newAnsiWriter(syscall.STD_OUTPUT_HANDLE) + } else { + stdOut = os.Stdout + } + + if IsConsole(os.Stderr.Fd()) { + stdErr = newAnsiWriter(syscall.STD_ERROR_HANDLE) + } else { + stdErr = os.Stderr + } + + return stdIn, stdOut, stdErr +} + +// GetHandleInfo returns file descriptor and bool indicating whether the file is a console. +func GetHandleInfo(in interface{}) (uintptr, bool) { + switch t := in.(type) { + case *ansiReader: + return t.Fd(), true + case *ansiWriter: + return t.Fd(), true + } + + var inFd uintptr + var isTerminal bool + + if file, ok := in.(*os.File); ok { + inFd = file.Fd() + isTerminal = IsConsole(inFd) + } + return inFd, isTerminal +} + +// IsConsole returns true if the given file descriptor is a Windows Console. +// The code assumes that GetConsoleMode will return an error for file descriptors that are not a console. +func IsConsole(fd uintptr) bool { + _, e := winterm.GetConsoleMode(fd) + return e == nil +} diff --git a/pkg/term/windows/windows.go b/pkg/term/windows/windows.go new file mode 100644 index 00000000..bf4c7b50 --- /dev/null +++ b/pkg/term/windows/windows.go @@ -0,0 +1,5 @@ +// These files implement ANSI-aware input and output streams for use by the Docker Windows client. +// When asked for the set of standard streams (e.g., stdin, stdout, stderr), the code will create +// and return pseudo-streams that convert ANSI sequences to / from Windows Console API calls. + +package windows diff --git a/pkg/term/windows/windows_test.go b/pkg/term/windows/windows_test.go new file mode 100644 index 00000000..52aeab54 --- /dev/null +++ b/pkg/term/windows/windows_test.go @@ -0,0 +1,3 @@ +// This file is necessary to pass the Docker tests. + +package windows diff --git a/pkg/tlsconfig/config.go b/pkg/tlsconfig/config.go new file mode 100644 index 00000000..e3dfad1f --- /dev/null +++ b/pkg/tlsconfig/config.go @@ -0,0 +1,133 @@ +// Package tlsconfig provides primitives to retrieve secure-enough TLS configurations for both clients and servers. +// +// As a reminder from https://golang.org/pkg/crypto/tls/#Config: +// A Config structure is used to configure a TLS client or server. After one has been passed to a TLS function it must not be modified. +// A Config may be reused; the tls package will also not modify it. +package tlsconfig + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "os" + + "github.com/Sirupsen/logrus" +) + +// Options represents the information needed to create client and server TLS configurations. +type Options struct { + CAFile string + + // If either CertFile or KeyFile is empty, Client() will not load them + // preventing the client from authenticating to the server. + // However, Server() requires them and will error out if they are empty. + CertFile string + KeyFile string + + // client-only option + InsecureSkipVerify bool + // server-only option + ClientAuth tls.ClientAuthType +} + +// Extra (server-side) accepted CBC cipher suites - will phase out in the future +var acceptedCBCCiphers = []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_RSA_WITH_AES_128_CBC_SHA, +} + +// Client TLS cipher suites (dropping CBC ciphers for client preferred suite set) +var clientCipherSuites = []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, +} + +// DefaultServerAcceptedCiphers should be uses by code which already has a crypto/tls +// options struct but wants to use a commonly accepted set of TLS cipher suites, with +// known weak algorithms removed. +var DefaultServerAcceptedCiphers = append(clientCipherSuites, acceptedCBCCiphers...) + +// ServerDefault is a secure-enough TLS configuration for the server TLS configuration. +var ServerDefault = tls.Config{ + // Avoid fallback to SSL protocols < TLS1.0 + MinVersion: tls.VersionTLS10, + PreferServerCipherSuites: true, + CipherSuites: DefaultServerAcceptedCiphers, +} + +// ClientDefault is a secure-enough TLS configuration for the client TLS configuration. +var ClientDefault = tls.Config{ + // Prefer TLS1.2 as the client minimum + MinVersion: tls.VersionTLS12, + CipherSuites: clientCipherSuites, +} + +// certPool returns an X.509 certificate pool from `caFile`, the certificate file. +func certPool(caFile string) (*x509.CertPool, error) { + // If we should verify the server, we need to load a trusted ca + certPool := x509.NewCertPool() + pem, err := ioutil.ReadFile(caFile) + if err != nil { + return nil, fmt.Errorf("Could not read CA certificate %q: %v", caFile, err) + } + if !certPool.AppendCertsFromPEM(pem) { + return nil, fmt.Errorf("failed to append certificates from PEM file: %q", caFile) + } + s := certPool.Subjects() + subjects := make([]string, len(s)) + for i, subject := range s { + subjects[i] = string(subject) + } + logrus.Debugf("Trusting certs with subjects: %v", subjects) + return certPool, nil +} + +// Client returns a TLS configuration meant to be used by a client. +func Client(options Options) (*tls.Config, error) { + tlsConfig := ClientDefault + tlsConfig.InsecureSkipVerify = options.InsecureSkipVerify + if !options.InsecureSkipVerify { + CAs, err := certPool(options.CAFile) + if err != nil { + return nil, err + } + tlsConfig.RootCAs = CAs + } + + if options.CertFile != "" && options.KeyFile != "" { + tlsCert, err := tls.LoadX509KeyPair(options.CertFile, options.KeyFile) + if err != nil { + return nil, fmt.Errorf("Could not load X509 key pair: %v. Make sure the key is not encrypted", err) + } + tlsConfig.Certificates = []tls.Certificate{tlsCert} + } + + return &tlsConfig, nil +} + +// Server returns a TLS configuration meant to be used by a server. +func Server(options Options) (*tls.Config, error) { + tlsConfig := ServerDefault + tlsConfig.ClientAuth = options.ClientAuth + tlsCert, err := tls.LoadX509KeyPair(options.CertFile, options.KeyFile) + if err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("Could not load X509 key pair (cert: %q, key: %q): %v", options.CertFile, options.KeyFile, err) + } + return nil, fmt.Errorf("Error reading X509 key pair (cert: %q, key: %q): %v. Make sure the key is not encrypted.", options.CertFile, options.KeyFile, err) + } + tlsConfig.Certificates = []tls.Certificate{tlsCert} + if options.ClientAuth >= tls.VerifyClientCertIfGiven { + CAs, err := certPool(options.CAFile) + if err != nil { + return nil, err + } + tlsConfig.ClientCAs = CAs + } + return &tlsConfig, nil +} diff --git a/pkg/truncindex/truncindex.go b/pkg/truncindex/truncindex.go new file mode 100644 index 00000000..02610b8b --- /dev/null +++ b/pkg/truncindex/truncindex.go @@ -0,0 +1,137 @@ +// Package truncindex provides a general 'index tree', used by Docker +// in order to be able to reference containers by only a few unambiguous +// characters of their id. +package truncindex + +import ( + "errors" + "fmt" + "strings" + "sync" + + "github.com/tchap/go-patricia/patricia" +) + +var ( + // ErrEmptyPrefix is an error returned if the prefix was empty. + ErrEmptyPrefix = errors.New("Prefix can't be empty") + + // ErrIllegalChar is returned when a space is in the ID + ErrIllegalChar = errors.New("illegal character: ' '") + + // ErrNotExist is returned when ID or its prefix not found in index. + ErrNotExist = errors.New("ID does not exist") +) + +// ErrAmbiguousPrefix is returned if the prefix was ambiguous +// (multiple ids for the prefix). +type ErrAmbiguousPrefix struct { + prefix string +} + +func (e ErrAmbiguousPrefix) Error() string { + return fmt.Sprintf("Multiple IDs found with provided prefix: %s", e.prefix) +} + +// TruncIndex allows the retrieval of string identifiers by any of their unique prefixes. +// This is used to retrieve image and container IDs by more convenient shorthand prefixes. +type TruncIndex struct { + sync.RWMutex + trie *patricia.Trie + ids map[string]struct{} +} + +// NewTruncIndex creates a new TruncIndex and initializes with a list of IDs. +func NewTruncIndex(ids []string) (idx *TruncIndex) { + idx = &TruncIndex{ + ids: make(map[string]struct{}), + + // Change patricia max prefix per node length, + // because our len(ID) always 64 + trie: patricia.NewTrie(patricia.MaxPrefixPerNode(64)), + } + for _, id := range ids { + idx.addID(id) + } + return +} + +func (idx *TruncIndex) addID(id string) error { + if strings.Contains(id, " ") { + return ErrIllegalChar + } + if id == "" { + return ErrEmptyPrefix + } + if _, exists := idx.ids[id]; exists { + return fmt.Errorf("id already exists: '%s'", id) + } + idx.ids[id] = struct{}{} + if inserted := idx.trie.Insert(patricia.Prefix(id), struct{}{}); !inserted { + return fmt.Errorf("failed to insert id: %s", id) + } + return nil +} + +// Add adds a new ID to the TruncIndex. +func (idx *TruncIndex) Add(id string) error { + idx.Lock() + defer idx.Unlock() + if err := idx.addID(id); err != nil { + return err + } + return nil +} + +// Delete removes an ID from the TruncIndex. If there are multiple IDs +// with the given prefix, an error is thrown. +func (idx *TruncIndex) Delete(id string) error { + idx.Lock() + defer idx.Unlock() + if _, exists := idx.ids[id]; !exists || id == "" { + return fmt.Errorf("no such id: '%s'", id) + } + delete(idx.ids, id) + if deleted := idx.trie.Delete(patricia.Prefix(id)); !deleted { + return fmt.Errorf("no such id: '%s'", id) + } + return nil +} + +// Get retrieves an ID from the TruncIndex. If there are multiple IDs +// with the given prefix, an error is thrown. +func (idx *TruncIndex) Get(s string) (string, error) { + if s == "" { + return "", ErrEmptyPrefix + } + var ( + id string + ) + subTreeVisitFunc := func(prefix patricia.Prefix, item patricia.Item) error { + if id != "" { + // we haven't found the ID if there are two or more IDs + id = "" + return ErrAmbiguousPrefix{prefix: string(prefix)} + } + id = string(prefix) + return nil + } + + idx.RLock() + defer idx.RUnlock() + if err := idx.trie.VisitSubtree(patricia.Prefix(s), subTreeVisitFunc); err != nil { + return "", err + } + if id != "" { + return id, nil + } + return "", ErrNotExist +} + +// Iterate iterates over all stored IDs, and passes each of them to the given handler. +func (idx *TruncIndex) Iterate(handler func(id string)) { + idx.trie.Visit(func(prefix patricia.Prefix, item patricia.Item) error { + handler(string(prefix)) + return nil + }) +} diff --git a/pkg/truncindex/truncindex_test.go b/pkg/truncindex/truncindex_test.go new file mode 100644 index 00000000..8197baf7 --- /dev/null +++ b/pkg/truncindex/truncindex_test.go @@ -0,0 +1,429 @@ +package truncindex + +import ( + "math/rand" + "testing" + + "github.com/docker/docker/pkg/stringid" +) + +// Test the behavior of TruncIndex, an index for querying IDs from a non-conflicting prefix. +func TestTruncIndex(t *testing.T) { + ids := []string{} + index := NewTruncIndex(ids) + // Get on an empty index + if _, err := index.Get("foobar"); err == nil { + t.Fatal("Get on an empty index should return an error") + } + + // Spaces should be illegal in an id + if err := index.Add("I have a space"); err == nil { + t.Fatalf("Adding an id with ' ' should return an error") + } + + id := "99b36c2c326ccc11e726eee6ee78a0baf166ef96" + // Add an id + if err := index.Add(id); err != nil { + t.Fatal(err) + } + + // Add an empty id (should fail) + if err := index.Add(""); err == nil { + t.Fatalf("Adding an empty id should return an error") + } + + // Get a non-existing id + assertIndexGet(t, index, "abracadabra", "", true) + // Get an empty id + assertIndexGet(t, index, "", "", true) + // Get the exact id + assertIndexGet(t, index, id, id, false) + // The first letter should match + assertIndexGet(t, index, id[:1], id, false) + // The first half should match + assertIndexGet(t, index, id[:len(id)/2], id, false) + // The second half should NOT match + assertIndexGet(t, index, id[len(id)/2:], "", true) + + id2 := id[:6] + "blabla" + // Add an id + if err := index.Add(id2); err != nil { + t.Fatal(err) + } + // Both exact IDs should work + assertIndexGet(t, index, id, id, false) + assertIndexGet(t, index, id2, id2, false) + + // 6 characters or less should conflict + assertIndexGet(t, index, id[:6], "", true) + assertIndexGet(t, index, id[:4], "", true) + assertIndexGet(t, index, id[:1], "", true) + + // An ambiguous id prefix should return an error + if _, err := index.Get(id[:4]); err == nil { + t.Fatal("An ambiguous id prefix should return an error") + } + + // 7 characters should NOT conflict + assertIndexGet(t, index, id[:7], id, false) + assertIndexGet(t, index, id2[:7], id2, false) + + // Deleting a non-existing id should return an error + if err := index.Delete("non-existing"); err == nil { + t.Fatalf("Deleting a non-existing id should return an error") + } + + // Deleting an empty id should return an error + if err := index.Delete(""); err == nil { + t.Fatal("Deleting an empty id should return an error") + } + + // Deleting id2 should remove conflicts + if err := index.Delete(id2); err != nil { + t.Fatal(err) + } + // id2 should no longer work + assertIndexGet(t, index, id2, "", true) + assertIndexGet(t, index, id2[:7], "", true) + assertIndexGet(t, index, id2[:11], "", true) + + // conflicts between id and id2 should be gone + assertIndexGet(t, index, id[:6], id, false) + assertIndexGet(t, index, id[:4], id, false) + assertIndexGet(t, index, id[:1], id, false) + + // non-conflicting substrings should still not conflict + assertIndexGet(t, index, id[:7], id, false) + assertIndexGet(t, index, id[:15], id, false) + assertIndexGet(t, index, id, id, false) + + assertIndexIterate(t) +} + +func assertIndexIterate(t *testing.T) { + ids := []string{ + "19b36c2c326ccc11e726eee6ee78a0baf166ef96", + "28b36c2c326ccc11e726eee6ee78a0baf166ef96", + "37b36c2c326ccc11e726eee6ee78a0baf166ef96", + "46b36c2c326ccc11e726eee6ee78a0baf166ef96", + } + + index := NewTruncIndex(ids) + + index.Iterate(func(targetId string) { + for _, id := range ids { + if targetId == id { + return + } + } + + t.Fatalf("An unknown ID '%s'", targetId) + }) +} + +func assertIndexGet(t *testing.T, index *TruncIndex, input, expectedResult string, expectError bool) { + if result, err := index.Get(input); err != nil && !expectError { + t.Fatalf("Unexpected error getting '%s': %s", input, err) + } else if err == nil && expectError { + t.Fatalf("Getting '%s' should return an error, not '%s'", input, result) + } else if result != expectedResult { + t.Fatalf("Getting '%s' returned '%s' instead of '%s'", input, result, expectedResult) + } +} + +func BenchmarkTruncIndexAdd100(b *testing.B) { + var testSet []string + for i := 0; i < 100; i++ { + testSet = append(testSet, stringid.GenerateNonCryptoID()) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + index := NewTruncIndex([]string{}) + for _, id := range testSet { + if err := index.Add(id); err != nil { + b.Fatal(err) + } + } + } +} + +func BenchmarkTruncIndexAdd250(b *testing.B) { + var testSet []string + for i := 0; i < 250; i++ { + testSet = append(testSet, stringid.GenerateNonCryptoID()) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + index := NewTruncIndex([]string{}) + for _, id := range testSet { + if err := index.Add(id); err != nil { + b.Fatal(err) + } + } + } +} + +func BenchmarkTruncIndexAdd500(b *testing.B) { + var testSet []string + for i := 0; i < 500; i++ { + testSet = append(testSet, stringid.GenerateNonCryptoID()) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + index := NewTruncIndex([]string{}) + for _, id := range testSet { + if err := index.Add(id); err != nil { + b.Fatal(err) + } + } + } +} + +func BenchmarkTruncIndexGet100(b *testing.B) { + var testSet []string + var testKeys []string + for i := 0; i < 100; i++ { + testSet = append(testSet, stringid.GenerateNonCryptoID()) + } + index := NewTruncIndex([]string{}) + for _, id := range testSet { + if err := index.Add(id); err != nil { + b.Fatal(err) + } + l := rand.Intn(12) + 12 + testKeys = append(testKeys, id[:l]) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, id := range testKeys { + if res, err := index.Get(id); err != nil { + b.Fatal(res, err) + } + } + } +} + +func BenchmarkTruncIndexGet250(b *testing.B) { + var testSet []string + var testKeys []string + for i := 0; i < 250; i++ { + testSet = append(testSet, stringid.GenerateNonCryptoID()) + } + index := NewTruncIndex([]string{}) + for _, id := range testSet { + if err := index.Add(id); err != nil { + b.Fatal(err) + } + l := rand.Intn(12) + 12 + testKeys = append(testKeys, id[:l]) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, id := range testKeys { + if res, err := index.Get(id); err != nil { + b.Fatal(res, err) + } + } + } +} + +func BenchmarkTruncIndexGet500(b *testing.B) { + var testSet []string + var testKeys []string + for i := 0; i < 500; i++ { + testSet = append(testSet, stringid.GenerateNonCryptoID()) + } + index := NewTruncIndex([]string{}) + for _, id := range testSet { + if err := index.Add(id); err != nil { + b.Fatal(err) + } + l := rand.Intn(12) + 12 + testKeys = append(testKeys, id[:l]) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, id := range testKeys { + if res, err := index.Get(id); err != nil { + b.Fatal(res, err) + } + } + } +} + +func BenchmarkTruncIndexDelete100(b *testing.B) { + var testSet []string + for i := 0; i < 100; i++ { + testSet = append(testSet, stringid.GenerateNonCryptoID()) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + b.StopTimer() + index := NewTruncIndex([]string{}) + for _, id := range testSet { + if err := index.Add(id); err != nil { + b.Fatal(err) + } + } + b.StartTimer() + for _, id := range testSet { + if err := index.Delete(id); err != nil { + b.Fatal(err) + } + } + } +} + +func BenchmarkTruncIndexDelete250(b *testing.B) { + var testSet []string + for i := 0; i < 250; i++ { + testSet = append(testSet, stringid.GenerateNonCryptoID()) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + b.StopTimer() + index := NewTruncIndex([]string{}) + for _, id := range testSet { + if err := index.Add(id); err != nil { + b.Fatal(err) + } + } + b.StartTimer() + for _, id := range testSet { + if err := index.Delete(id); err != nil { + b.Fatal(err) + } + } + } +} + +func BenchmarkTruncIndexDelete500(b *testing.B) { + var testSet []string + for i := 0; i < 500; i++ { + testSet = append(testSet, stringid.GenerateNonCryptoID()) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + b.StopTimer() + index := NewTruncIndex([]string{}) + for _, id := range testSet { + if err := index.Add(id); err != nil { + b.Fatal(err) + } + } + b.StartTimer() + for _, id := range testSet { + if err := index.Delete(id); err != nil { + b.Fatal(err) + } + } + } +} + +func BenchmarkTruncIndexNew100(b *testing.B) { + var testSet []string + for i := 0; i < 100; i++ { + testSet = append(testSet, stringid.GenerateNonCryptoID()) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + NewTruncIndex(testSet) + } +} + +func BenchmarkTruncIndexNew250(b *testing.B) { + var testSet []string + for i := 0; i < 250; i++ { + testSet = append(testSet, stringid.GenerateNonCryptoID()) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + NewTruncIndex(testSet) + } +} + +func BenchmarkTruncIndexNew500(b *testing.B) { + var testSet []string + for i := 0; i < 500; i++ { + testSet = append(testSet, stringid.GenerateNonCryptoID()) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + NewTruncIndex(testSet) + } +} + +func BenchmarkTruncIndexAddGet100(b *testing.B) { + var testSet []string + var testKeys []string + for i := 0; i < 500; i++ { + id := stringid.GenerateNonCryptoID() + testSet = append(testSet, id) + l := rand.Intn(12) + 12 + testKeys = append(testKeys, id[:l]) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + index := NewTruncIndex([]string{}) + for _, id := range testSet { + if err := index.Add(id); err != nil { + b.Fatal(err) + } + } + for _, id := range testKeys { + if res, err := index.Get(id); err != nil { + b.Fatal(res, err) + } + } + } +} + +func BenchmarkTruncIndexAddGet250(b *testing.B) { + var testSet []string + var testKeys []string + for i := 0; i < 500; i++ { + id := stringid.GenerateNonCryptoID() + testSet = append(testSet, id) + l := rand.Intn(12) + 12 + testKeys = append(testKeys, id[:l]) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + index := NewTruncIndex([]string{}) + for _, id := range testSet { + if err := index.Add(id); err != nil { + b.Fatal(err) + } + } + for _, id := range testKeys { + if res, err := index.Get(id); err != nil { + b.Fatal(res, err) + } + } + } +} + +func BenchmarkTruncIndexAddGet500(b *testing.B) { + var testSet []string + var testKeys []string + for i := 0; i < 500; i++ { + id := stringid.GenerateNonCryptoID() + testSet = append(testSet, id) + l := rand.Intn(12) + 12 + testKeys = append(testKeys, id[:l]) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + index := NewTruncIndex([]string{}) + for _, id := range testSet { + if err := index.Add(id); err != nil { + b.Fatal(err) + } + } + for _, id := range testKeys { + if res, err := index.Get(id); err != nil { + b.Fatal(res, err) + } + } + } +} diff --git a/pkg/urlutil/urlutil.go b/pkg/urlutil/urlutil.go new file mode 100644 index 00000000..1135a4c6 --- /dev/null +++ b/pkg/urlutil/urlutil.go @@ -0,0 +1,50 @@ +// Package urlutil provides helper function to check urls kind. +// It supports http urls, git urls and transport url (tcp://, …) +package urlutil + +import ( + "regexp" + "strings" +) + +var ( + validPrefixes = map[string][]string{ + "url": {"http://", "https://"}, + "git": {"git://", "github.com/", "git@"}, + "transport": {"tcp://", "tcp+tls://", "udp://", "unix://"}, + } + urlPathWithFragmentSuffix = regexp.MustCompile(".git(?:#.+)?$") +) + +// IsURL returns true if the provided str is an HTTP(S) URL. +func IsURL(str string) bool { + return checkURL(str, "url") +} + +// IsGitURL returns true if the provided str is a git repository URL. +func IsGitURL(str string) bool { + if IsURL(str) && urlPathWithFragmentSuffix.MatchString(str) { + return true + } + return checkURL(str, "git") +} + +// IsGitTransport returns true if the provided str is a git transport by inspecting +// the prefix of the string for known protocols used in git. +func IsGitTransport(str string) bool { + return IsURL(str) || strings.HasPrefix(str, "git://") || strings.HasPrefix(str, "git@") +} + +// IsTransportURL returns true if the provided str is a transport (tcp, tcp+tls, udp, unix) URL. +func IsTransportURL(str string) bool { + return checkURL(str, "transport") +} + +func checkURL(str, kind string) bool { + for _, prefix := range validPrefixes[kind] { + if strings.HasPrefix(str, prefix) { + return true + } + } + return false +} diff --git a/pkg/urlutil/urlutil_test.go b/pkg/urlutil/urlutil_test.go new file mode 100644 index 00000000..86d48cfe --- /dev/null +++ b/pkg/urlutil/urlutil_test.go @@ -0,0 +1,69 @@ +package urlutil + +import "testing" + +var ( + gitUrls = []string{ + "git://github.com/docker/docker", + "git@github.com:docker/docker.git", + "git@bitbucket.org:atlassianlabs/atlassian-docker.git", + "https://github.com/docker/docker.git", + "http://github.com/docker/docker.git", + "http://github.com/docker/docker.git#branch", + "http://github.com/docker/docker.git#:dir", + } + incompleteGitUrls = []string{ + "github.com/docker/docker", + } + invalidGitUrls = []string{ + "http://github.com/docker/docker.git:#branch", + } + transportUrls = []string{ + "tcp://example.com", + "tcp+tls://example.com", + "udp://example.com", + "unix:///example", + } +) + +func TestValidGitTransport(t *testing.T) { + for _, url := range gitUrls { + if IsGitTransport(url) == false { + t.Fatalf("%q should be detected as valid Git prefix", url) + } + } + + for _, url := range incompleteGitUrls { + if IsGitTransport(url) == true { + t.Fatalf("%q should not be detected as valid Git prefix", url) + } + } +} + +func TestIsGIT(t *testing.T) { + for _, url := range gitUrls { + if IsGitURL(url) == false { + t.Fatalf("%q should be detected as valid Git url", url) + } + } + + for _, url := range incompleteGitUrls { + if IsGitURL(url) == false { + t.Fatalf("%q should be detected as valid Git url", url) + } + } + + for _, url := range invalidGitUrls { + if IsGitURL(url) == true { + t.Fatalf("%q should not be detected as valid Git prefix", url) + } + } +} + +func TestIsTransport(t *testing.T) { + for _, url := range transportUrls { + if IsTransportURL(url) == false { + t.Fatalf("%q should be detected as valid Transport url", url) + } + } +} diff --git a/pkg/useragent/README.md b/pkg/useragent/README.md new file mode 100644 index 00000000..d9cb367d --- /dev/null +++ b/pkg/useragent/README.md @@ -0,0 +1 @@ +This package provides helper functions to pack version information into a single User-Agent header. diff --git a/pkg/useragent/useragent.go b/pkg/useragent/useragent.go new file mode 100644 index 00000000..1137db51 --- /dev/null +++ b/pkg/useragent/useragent.go @@ -0,0 +1,55 @@ +// Package useragent provides helper functions to pack +// version information into a single User-Agent header. +package useragent + +import ( + "strings" +) + +// VersionInfo is used to model UserAgent versions. +type VersionInfo struct { + Name string + Version string +} + +func (vi *VersionInfo) isValid() bool { + const stopChars = " \t\r\n/" + name := vi.Name + vers := vi.Version + if len(name) == 0 || strings.ContainsAny(name, stopChars) { + return false + } + if len(vers) == 0 || strings.ContainsAny(vers, stopChars) { + return false + } + return true +} + +// AppendVersions converts versions to a string and appends the string to the string base. +// +// Each VersionInfo will be converted to a string in the format of +// "product/version", where the "product" is get from the name field, while +// version is get from the version field. Several pieces of version information +// will be concatenated and separated by space. +// +// Example: +// AppendVersions("base", VersionInfo{"foo", "1.0"}, VersionInfo{"bar", "2.0"}) +// results in "base foo/1.0 bar/2.0". +func AppendVersions(base string, versions ...VersionInfo) string { + if len(versions) == 0 { + return base + } + + verstrs := make([]string, 0, 1+len(versions)) + if len(base) > 0 { + verstrs = append(verstrs, base) + } + + for _, v := range versions { + if !v.isValid() { + continue + } + verstrs = append(verstrs, v.Name+"/"+v.Version) + } + return strings.Join(verstrs, " ") +} diff --git a/pkg/useragent/useragent_test.go b/pkg/useragent/useragent_test.go new file mode 100644 index 00000000..0ad7243a --- /dev/null +++ b/pkg/useragent/useragent_test.go @@ -0,0 +1,31 @@ +package useragent + +import "testing" + +func TestVersionInfo(t *testing.T) { + vi := VersionInfo{"foo", "bar"} + if !vi.isValid() { + t.Fatalf("VersionInfo should be valid") + } + vi = VersionInfo{"", "bar"} + if vi.isValid() { + t.Fatalf("Expected VersionInfo to be invalid") + } + vi = VersionInfo{"foo", ""} + if vi.isValid() { + t.Fatalf("Expected VersionInfo to be invalid") + } +} + +func TestAppendVersions(t *testing.T) { + vis := []VersionInfo{ + {"foo", "1.0"}, + {"bar", "0.1"}, + {"pi", "3.1.4"}, + } + v := AppendVersions("base", vis...) + expect := "base foo/1.0 bar/0.1 pi/3.1.4" + if v != expect { + t.Fatalf("expected %q, got %q", expect, v) + } +} diff --git a/pkg/version/version.go b/pkg/version/version.go new file mode 100644 index 00000000..c001279f --- /dev/null +++ b/pkg/version/version.go @@ -0,0 +1,68 @@ +package version + +import ( + "strconv" + "strings" +) + +// Version provides utility methods for comparing versions. +type Version string + +func (v Version) compareTo(other Version) int { + var ( + currTab = strings.Split(string(v), ".") + otherTab = strings.Split(string(other), ".") + ) + + max := len(currTab) + if len(otherTab) > max { + max = len(otherTab) + } + for i := 0; i < max; i++ { + var currInt, otherInt int + + if len(currTab) > i { + currInt, _ = strconv.Atoi(currTab[i]) + } + if len(otherTab) > i { + otherInt, _ = strconv.Atoi(otherTab[i]) + } + if currInt > otherInt { + return 1 + } + if otherInt > currInt { + return -1 + } + } + return 0 +} + +// String returns the version string +func (v Version) String() string { + return string(v) +} + +// LessThan checks if a version is less than another +func (v Version) LessThan(other Version) bool { + return v.compareTo(other) == -1 +} + +// LessThanOrEqualTo checks if a version is less than or equal to another +func (v Version) LessThanOrEqualTo(other Version) bool { + return v.compareTo(other) <= 0 +} + +// GreaterThan checks if a version is greater than another +func (v Version) GreaterThan(other Version) bool { + return v.compareTo(other) == 1 +} + +// GreaterThanOrEqualTo checks if a version is greater than or equal to another +func (v Version) GreaterThanOrEqualTo(other Version) bool { + return v.compareTo(other) >= 0 +} + +// Equal checks if a version is equal to another +func (v Version) Equal(other Version) bool { + return v.compareTo(other) == 0 +} diff --git a/pkg/version/version_test.go b/pkg/version/version_test.go new file mode 100644 index 00000000..c02ec40f --- /dev/null +++ b/pkg/version/version_test.go @@ -0,0 +1,27 @@ +package version + +import ( + "testing" +) + +func assertVersion(t *testing.T, a, b string, result int) { + if r := Version(a).compareTo(Version(b)); r != result { + t.Fatalf("Unexpected version comparison result. Found %d, expected %d", r, result) + } +} + +func TestCompareVersion(t *testing.T) { + assertVersion(t, "1.12", "1.12", 0) + assertVersion(t, "1.0.0", "1", 0) + assertVersion(t, "1", "1.0.0", 0) + assertVersion(t, "1.05.00.0156", "1.0.221.9289", 1) + assertVersion(t, "1", "1.0.1", -1) + assertVersion(t, "1.0.1", "1", 1) + assertVersion(t, "1.0.1", "1.0.2", -1) + assertVersion(t, "1.0.2", "1.0.3", -1) + assertVersion(t, "1.0.3", "1.1", -1) + assertVersion(t, "1.1", "1.1.1", -1) + assertVersion(t, "1.1.1", "1.1.2", -1) + assertVersion(t, "1.1.2", "1.2", -1) + +} diff --git a/profiles/apparmor/apparmor.go b/profiles/apparmor/apparmor.go new file mode 100644 index 00000000..51dfa5cf --- /dev/null +++ b/profiles/apparmor/apparmor.go @@ -0,0 +1,115 @@ +// +build linux + +package apparmor + +import ( + "bufio" + "io" + "os" + "path" + "strings" + + "github.com/docker/docker/pkg/aaparser" + "github.com/docker/docker/utils/templates" +) + +var ( + // profileDirectory is the file store for apparmor profiles and macros. + profileDirectory = "/etc/apparmor.d" + // defaultProfilePath is the default path for the apparmor profile to be saved. + defaultProfilePath = path.Join(profileDirectory, "docker") +) + +// profileData holds information about the given profile for generation. +type profileData struct { + // Name is profile name. + Name string + // Imports defines the apparmor functions to import, before defining the profile. + Imports []string + // InnerImports defines the apparmor functions to import in the profile. + InnerImports []string + // Version is the {major, minor, patch} version of apparmor_parser as a single number. + Version int +} + +// generateDefault creates an apparmor profile from ProfileData. +func (p *profileData) generateDefault(out io.Writer) error { + compiled, err := templates.NewParse("apparmor_profile", baseTemplate) + if err != nil { + return err + } + + if macroExists("tunables/global") { + p.Imports = append(p.Imports, "#include ") + } else { + p.Imports = append(p.Imports, "@{PROC}=/proc/") + } + + if macroExists("abstractions/base") { + p.InnerImports = append(p.InnerImports, "#include ") + } + + ver, err := aaparser.GetVersion() + if err != nil { + return err + } + p.Version = ver + + if err := compiled.Execute(out, p); err != nil { + return err + } + return nil +} + +// macrosExists checks if the passed macro exists. +func macroExists(m string) bool { + _, err := os.Stat(path.Join(profileDirectory, m)) + return err == nil +} + +// InstallDefault generates a default profile and installs it in the +// ProfileDirectory with `apparmor_parser`. +func InstallDefault(name string) error { + // Make sure the path where they want to save the profile exists + if err := os.MkdirAll(profileDirectory, 0755); err != nil { + return err + } + + p := profileData{ + Name: name, + } + + f, err := os.OpenFile(defaultProfilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return err + } + if err := p.generateDefault(f); err != nil { + f.Close() + return err + } + f.Close() + + if err := aaparser.LoadProfile(defaultProfilePath); err != nil { + return err + } + + return nil +} + +// IsLoaded checks if a passed profile has been loaded into the kernel. +func IsLoaded(name string) error { + file, err := os.Open("/sys/kernel/security/apparmor/profiles") + if err != nil { + return err + } + r := bufio.NewReader(file) + for { + p, err := r.ReadString('\n') + if err != nil { + return err + } + if strings.HasPrefix(p, name+" ") { + return nil + } + } +} diff --git a/profiles/apparmor/template.go b/profiles/apparmor/template.go new file mode 100644 index 00000000..ada33bf0 --- /dev/null +++ b/profiles/apparmor/template.go @@ -0,0 +1,46 @@ +// +build linux + +package apparmor + +// baseTemplate defines the default apparmor profile for containers. +const baseTemplate = ` +{{range $value := .Imports}} +{{$value}} +{{end}} + +profile {{.Name}} flags=(attach_disconnected,mediate_deleted) { +{{range $value := .InnerImports}} + {{$value}} +{{end}} + + network, + capability, + file, + umount, + + deny @{PROC}/* w, # deny write for all files directly in /proc (not in a subdir) + # deny write to files not in /proc//** or /proc/sys/** + deny @{PROC}/{[^1-9],[^1-9][^0-9],[^1-9s][^0-9y][^0-9s],[^1-9][^0-9][^0-9][^0-9]*}/** w, + deny @{PROC}/sys/[^k]** w, # deny /proc/sys except /proc/sys/k* (effectively /proc/sys/kernel) + deny @{PROC}/sys/kernel/{?,??,[^s][^h][^m]**} w, # deny everything except shm* in /proc/sys/kernel/ + deny @{PROC}/sysrq-trigger rwklx, + deny @{PROC}/mem rwklx, + deny @{PROC}/kmem rwklx, + deny @{PROC}/kcore rwklx, + + deny mount, + + deny /sys/[^f]*/** wklx, + deny /sys/f[^s]*/** wklx, + deny /sys/fs/[^c]*/** wklx, + deny /sys/fs/c[^g]*/** wklx, + deny /sys/fs/cg[^r]*/** wklx, + deny /sys/firmware/efi/efivars/** rwklx, + deny /sys/kernel/security/** rwklx, + +{{if ge .Version 208095}} + # suppress ptrace denials when using 'docker ps' or using 'ps' inside a container + ptrace (trace,read) peer=docker-default, +{{end}} +} +` diff --git a/profiles/seccomp/default.json b/profiles/seccomp/default.json new file mode 100755 index 00000000..5c70f88a --- /dev/null +++ b/profiles/seccomp/default.json @@ -0,0 +1,1628 @@ +{ + "defaultAction": "SCMP_ACT_ERRNO", + "architectures": [ + "SCMP_ARCH_X86_64", + "SCMP_ARCH_X86", + "SCMP_ARCH_X32" + ], + "syscalls": [ + { + "name": "accept", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "accept4", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "access", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "alarm", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "arch_prctl", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "bind", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "brk", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "capget", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "capset", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "chdir", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "chmod", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "chown", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "chown32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "chroot", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "clock_getres", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "clock_gettime", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "clock_nanosleep", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "clone", + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 2080505856, + "valueTwo": 0, + "op": "SCMP_CMP_MASKED_EQ" + } + ] + }, + { + "name": "close", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "connect", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "copy_file_range", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "creat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "dup", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "dup2", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "dup3", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "epoll_create", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "epoll_create1", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "epoll_ctl", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "epoll_ctl_old", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "epoll_pwait", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "epoll_wait", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "epoll_wait_old", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "eventfd", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "eventfd2", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "execve", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "execveat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "exit", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "exit_group", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "faccessat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fadvise64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fadvise64_64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fallocate", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fanotify_init", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fanotify_mark", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fchdir", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fchmod", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fchmodat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fchown", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fchown32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fchownat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fcntl", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fcntl64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fdatasync", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fgetxattr", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "flistxattr", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "flock", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fork", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fremovexattr", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fsetxattr", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fstat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fstat64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fstatat64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fstatfs", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fstatfs64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fsync", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "ftruncate", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "ftruncate64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "futex", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "futimesat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getcpu", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getcwd", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getdents", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getdents64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getegid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getegid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "geteuid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "geteuid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getgid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getgid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getgroups", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getgroups32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getitimer", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getpeername", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getpgid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getpgrp", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getpid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getppid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getpriority", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getrandom", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getresgid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getresgid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getresuid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getresuid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getrlimit", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "get_robust_list", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getrusage", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getsid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getsockname", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getsockopt", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "get_thread_area", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "gettid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "gettimeofday", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getuid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getuid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getxattr", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "inotify_add_watch", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "inotify_init", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "inotify_init1", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "inotify_rm_watch", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "io_cancel", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "ioctl", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "io_destroy", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "io_getevents", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "ioprio_get", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "ioprio_set", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "io_setup", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "io_submit", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "ipc", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "kill", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "lchown", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "lchown32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "lgetxattr", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "link", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "linkat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "listen", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "listxattr", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "llistxattr", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "_llseek", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "lremovexattr", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "lseek", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "lsetxattr", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "lstat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "lstat64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "madvise", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "memfd_create", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mincore", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mkdir", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mkdirat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mknod", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mknodat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mlock", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mlock2", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mlockall", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mmap", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mmap2", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mprotect", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mq_getsetattr", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mq_notify", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mq_open", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mq_timedreceive", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mq_timedsend", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mq_unlink", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mremap", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "msgctl", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "msgget", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "msgrcv", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "msgsnd", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "msync", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "munlock", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "munlockall", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "munmap", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "nanosleep", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "newfstatat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "_newselect", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "open", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "openat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "pause", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "personality", + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 0, + "valueTwo": 0, + "op": "SCMP_CMP_EQ" + } + ] + }, + { + "name": "personality", + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 8, + "valueTwo": 0, + "op": "SCMP_CMP_EQ" + } + ] + }, + { + "name": "personality", + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 4294967295, + "valueTwo": 0, + "op": "SCMP_CMP_EQ" + } + ] + }, + { + "name": "pipe", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "pipe2", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "poll", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "ppoll", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "prctl", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "pread64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "preadv", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "prlimit64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "pselect6", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "pwrite64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "pwritev", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "read", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "readahead", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "readlink", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "readlinkat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "readv", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "recv", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "recvfrom", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "recvmmsg", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "recvmsg", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "remap_file_pages", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "removexattr", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rename", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "renameat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "renameat2", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "restart_syscall", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rmdir", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_sigaction", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_sigpending", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_sigprocmask", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_sigqueueinfo", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_sigreturn", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_sigsuspend", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_sigtimedwait", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_tgsigqueueinfo", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_getaffinity", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_getattr", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_getparam", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_get_priority_max", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_get_priority_min", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_getscheduler", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_rr_get_interval", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_setaffinity", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_setattr", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_setparam", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_setscheduler", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_yield", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "seccomp", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "select", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "semctl", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "semget", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "semop", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "semtimedop", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "send", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sendfile", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sendfile64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sendmmsg", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sendmsg", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sendto", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setdomainname", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setfsgid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setfsgid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setfsuid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setfsuid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setgid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setgid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setgroups", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setgroups32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sethostname", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setitimer", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setpgid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setpriority", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setregid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setregid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setresgid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setresgid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setresuid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setresuid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setreuid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setreuid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setrlimit", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "set_robust_list", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setsid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setsockopt", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "set_thread_area", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "set_tid_address", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setuid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setuid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setxattr", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "shmat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "shmctl", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "shmdt", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "shmget", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "shutdown", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sigaltstack", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "signalfd", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "signalfd4", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sigreturn", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "socket", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "socketcall", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "socketpair", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "splice", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "stat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "stat64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "statfs", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "statfs64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "symlink", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "symlinkat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sync", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sync_file_range", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "syncfs", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sysinfo", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "syslog", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "tee", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "tgkill", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "time", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "timer_create", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "timer_delete", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "timerfd_create", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "timerfd_gettime", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "timerfd_settime", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "timer_getoverrun", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "timer_gettime", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "timer_settime", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "times", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "tkill", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "truncate", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "truncate64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "ugetrlimit", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "umask", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "uname", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "unlink", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "unlinkat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "utime", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "utimensat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "utimes", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "vfork", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "vhangup", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "vmsplice", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "wait4", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "waitid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "waitpid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "write", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "writev", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "modify_ldt", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "breakpoint", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "cacheflush", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "set_tls", + "action": "SCMP_ACT_ALLOW", + "args": [] + } + ] +} \ No newline at end of file diff --git a/profiles/seccomp/fixtures/example.json b/profiles/seccomp/fixtures/example.json new file mode 100755 index 00000000..674ca50f --- /dev/null +++ b/profiles/seccomp/fixtures/example.json @@ -0,0 +1,27 @@ +{ + "defaultAction": "SCMP_ACT_ERRNO", + "syscalls": [ + { + "name": "clone", + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 2080505856, + "valueTwo": 0, + "op": "SCMP_CMP_MASKED_EQ" + } + ] + }, + { + "name": "open", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "close", + "action": "SCMP_ACT_ALLOW", + "args": [] + } + ] +} diff --git a/profiles/seccomp/generate.go b/profiles/seccomp/generate.go new file mode 100644 index 00000000..bf565947 --- /dev/null +++ b/profiles/seccomp/generate.go @@ -0,0 +1,32 @@ +// +build ignore + +package main + +import ( + "encoding/json" + "io/ioutil" + "os" + "path/filepath" + + "github.com/docker/docker/profiles/seccomp" +) + +// saves the default seccomp profile as a json file so people can use it as a +// base for their own custom profiles +func main() { + wd, err := os.Getwd() + if err != nil { + panic(err) + } + f := filepath.Join(wd, "default.json") + + // write the default profile to the file + b, err := json.MarshalIndent(seccomp.DefaultProfile, "", "\t") + if err != nil { + panic(err) + } + + if err := ioutil.WriteFile(f, b, 0644); err != nil { + panic(err) + } +} diff --git a/profiles/seccomp/seccomp.go b/profiles/seccomp/seccomp.go new file mode 100644 index 00000000..0718d840 --- /dev/null +++ b/profiles/seccomp/seccomp.go @@ -0,0 +1,74 @@ +// +build linux + +package seccomp + +import ( + "encoding/json" + "fmt" + + "github.com/docker/engine-api/types" + "github.com/opencontainers/specs/specs-go" +) + +//go:generate go run -tags 'seccomp' generate.go + +// GetDefaultProfile returns the default seccomp profile. +func GetDefaultProfile() (*specs.Seccomp, error) { + return setupSeccomp(DefaultProfile) +} + +// LoadProfile takes a file path and decodes the seccomp profile. +func LoadProfile(body string) (*specs.Seccomp, error) { + var config types.Seccomp + if err := json.Unmarshal([]byte(body), &config); err != nil { + return nil, fmt.Errorf("Decoding seccomp profile failed: %v", err) + } + + return setupSeccomp(&config) +} + +func setupSeccomp(config *types.Seccomp) (newConfig *specs.Seccomp, err error) { + if config == nil { + return nil, nil + } + + // No default action specified, no syscalls listed, assume seccomp disabled + if config.DefaultAction == "" && len(config.Syscalls) == 0 { + return nil, nil + } + + newConfig = &specs.Seccomp{} + + // if config.Architectures == 0 then libseccomp will figure out the architecture to use + if len(config.Architectures) > 0 { + for _, arch := range config.Architectures { + newConfig.Architectures = append(newConfig.Architectures, specs.Arch(arch)) + } + } + + newConfig.DefaultAction = specs.Action(config.DefaultAction) + + // Loop through all syscall blocks and convert them to libcontainer format + for _, call := range config.Syscalls { + newCall := specs.Syscall{ + Name: call.Name, + Action: specs.Action(call.Action), + } + + // Loop through all the arguments of the syscall and convert them + for _, arg := range call.Args { + newArg := specs.Arg{ + Index: arg.Index, + Value: arg.Value, + ValueTwo: arg.ValueTwo, + Op: specs.Operator(arg.Op), + } + + newCall.Args = append(newCall.Args, newArg) + } + + newConfig.Syscalls = append(newConfig.Syscalls, newCall) + } + + return newConfig, nil +} diff --git a/profiles/seccomp/seccomp_default.go b/profiles/seccomp/seccomp_default.go new file mode 100644 index 00000000..4fad7a6c --- /dev/null +++ b/profiles/seccomp/seccomp_default.go @@ -0,0 +1,1659 @@ +// +build linux,seccomp + +package seccomp + +import ( + "syscall" + + "github.com/docker/engine-api/types" + libseccomp "github.com/seccomp/libseccomp-golang" +) + +func arches() []types.Arch { + var native, err = libseccomp.GetNativeArch() + if err != nil { + return []types.Arch{} + } + var a = native.String() + switch a { + case "amd64": + return []types.Arch{types.ArchX86_64, types.ArchX86, types.ArchX32} + case "arm64": + return []types.Arch{types.ArchARM, types.ArchAARCH64} + case "mips64": + return []types.Arch{types.ArchMIPS, types.ArchMIPS64, types.ArchMIPS64N32} + case "mips64n32": + return []types.Arch{types.ArchMIPS, types.ArchMIPS64, types.ArchMIPS64N32} + case "mipsel64": + return []types.Arch{types.ArchMIPSEL, types.ArchMIPSEL64, types.ArchMIPSEL64N32} + case "mipsel64n32": + return []types.Arch{types.ArchMIPSEL, types.ArchMIPSEL64, types.ArchMIPSEL64N32} + default: + return []types.Arch{} + } +} + +// DefaultProfile defines the whitelist for the default seccomp profile. +var DefaultProfile = &types.Seccomp{ + DefaultAction: types.ActErrno, + Architectures: arches(), + Syscalls: []*types.Syscall{ + { + Name: "accept", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "accept4", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "access", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "alarm", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "arch_prctl", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "bind", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "brk", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "capget", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "capset", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "chdir", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "chmod", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "chown", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "chown32", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "chroot", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "clock_getres", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "clock_gettime", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "clock_nanosleep", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "clone", + Action: types.ActAllow, + Args: []*types.Arg{ + { + Index: 0, + Value: syscall.CLONE_NEWNS | syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWUSER | syscall.CLONE_NEWPID | syscall.CLONE_NEWNET, + ValueTwo: 0, + Op: types.OpMaskedEqual, + }, + }, + }, + { + Name: "close", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "connect", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "copy_file_range", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "creat", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "dup", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "dup2", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "dup3", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "epoll_create", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "epoll_create1", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "epoll_ctl", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "epoll_ctl_old", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "epoll_pwait", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "epoll_wait", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "epoll_wait_old", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "eventfd", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "eventfd2", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "execve", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "execveat", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "exit", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "exit_group", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "faccessat", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fadvise64", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fadvise64_64", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fallocate", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fanotify_init", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fanotify_mark", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fchdir", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fchmod", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fchmodat", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fchown", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fchown32", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fchownat", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fcntl", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fcntl64", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fdatasync", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fgetxattr", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "flistxattr", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "flock", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fork", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fremovexattr", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fsetxattr", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fstat", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fstat64", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fstatat64", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fstatfs", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fstatfs64", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "fsync", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "ftruncate", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "ftruncate64", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "futex", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "futimesat", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getcpu", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getcwd", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getdents", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getdents64", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getegid", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getegid32", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "geteuid", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "geteuid32", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getgid", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getgid32", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getgroups", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getgroups32", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getitimer", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getpeername", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getpgid", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getpgrp", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getpid", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getppid", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getpriority", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getrandom", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getresgid", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getresgid32", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getresuid", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getresuid32", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getrlimit", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "get_robust_list", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getrusage", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getsid", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getsockname", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getsockopt", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "get_thread_area", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "gettid", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "gettimeofday", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getuid", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getuid32", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "getxattr", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "inotify_add_watch", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "inotify_init", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "inotify_init1", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "inotify_rm_watch", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "io_cancel", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "ioctl", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "io_destroy", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "io_getevents", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "ioprio_get", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "ioprio_set", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "io_setup", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "io_submit", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "ipc", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "kill", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "lchown", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "lchown32", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "lgetxattr", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "link", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "linkat", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "listen", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "listxattr", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "llistxattr", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "_llseek", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "lremovexattr", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "lseek", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "lsetxattr", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "lstat", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "lstat64", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "madvise", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "memfd_create", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "mincore", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "mkdir", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "mkdirat", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "mknod", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "mknodat", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "mlock", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "mlock2", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "mlockall", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "mmap", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "mmap2", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "mprotect", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "mq_getsetattr", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "mq_notify", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "mq_open", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "mq_timedreceive", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "mq_timedsend", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "mq_unlink", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "mremap", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "msgctl", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "msgget", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "msgrcv", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "msgsnd", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "msync", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "munlock", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "munlockall", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "munmap", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "nanosleep", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "newfstatat", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "_newselect", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "open", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "openat", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "pause", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "personality", + Action: types.ActAllow, + Args: []*types.Arg{ + { + Index: 0, + Value: 0x0, + Op: types.OpEqualTo, + }, + }, + }, + { + Name: "personality", + Action: types.ActAllow, + Args: []*types.Arg{ + { + Index: 0, + Value: 0x0008, + Op: types.OpEqualTo, + }, + }, + }, + { + Name: "personality", + Action: types.ActAllow, + Args: []*types.Arg{ + { + Index: 0, + Value: 0xffffffff, + Op: types.OpEqualTo, + }, + }, + }, + { + Name: "pipe", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "pipe2", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "poll", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "ppoll", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "prctl", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "pread64", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "preadv", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "prlimit64", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "pselect6", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "pwrite64", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "pwritev", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "read", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "readahead", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "readlink", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "readlinkat", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "readv", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "recv", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "recvfrom", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "recvmmsg", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "recvmsg", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "remap_file_pages", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "removexattr", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "rename", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "renameat", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "renameat2", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "restart_syscall", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "rmdir", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "rt_sigaction", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "rt_sigpending", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "rt_sigprocmask", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "rt_sigqueueinfo", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "rt_sigreturn", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "rt_sigsuspend", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "rt_sigtimedwait", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "rt_tgsigqueueinfo", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "sched_getaffinity", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "sched_getattr", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "sched_getparam", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "sched_get_priority_max", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "sched_get_priority_min", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "sched_getscheduler", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "sched_rr_get_interval", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "sched_setaffinity", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "sched_setattr", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "sched_setparam", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "sched_setscheduler", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "sched_yield", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "seccomp", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "select", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "semctl", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "semget", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "semop", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "semtimedop", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "send", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "sendfile", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "sendfile64", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "sendmmsg", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "sendmsg", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "sendto", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setdomainname", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setfsgid", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setfsgid32", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setfsuid", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setfsuid32", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setgid", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setgid32", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setgroups", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setgroups32", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "sethostname", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setitimer", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setpgid", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setpriority", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setregid", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setregid32", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setresgid", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setresgid32", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setresuid", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setresuid32", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setreuid", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setreuid32", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setrlimit", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "set_robust_list", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setsid", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setsockopt", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "set_thread_area", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "set_tid_address", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setuid", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setuid32", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "setxattr", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "shmat", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "shmctl", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "shmdt", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "shmget", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "shutdown", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "sigaltstack", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "signalfd", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "signalfd4", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "sigreturn", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "socket", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "socketcall", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "socketpair", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "splice", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "stat", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "stat64", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "statfs", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "statfs64", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "symlink", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "symlinkat", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "sync", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "sync_file_range", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "syncfs", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "sysinfo", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "syslog", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "tee", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "tgkill", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "time", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "timer_create", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "timer_delete", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "timerfd_create", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "timerfd_gettime", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "timerfd_settime", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "timer_getoverrun", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "timer_gettime", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "timer_settime", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "times", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "tkill", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "truncate", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "truncate64", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "ugetrlimit", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "umask", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "uname", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "unlink", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "unlinkat", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "utime", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "utimensat", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "utimes", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "vfork", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "vhangup", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "vmsplice", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "wait4", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "waitid", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "waitpid", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "write", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "writev", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + // i386 specific syscalls + { + Name: "modify_ldt", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + // arm specific syscalls + { + Name: "breakpoint", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "cacheflush", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + { + Name: "set_tls", + Action: types.ActAllow, + Args: []*types.Arg{}, + }, + }, +} diff --git a/profiles/seccomp/seccomp_test.go b/profiles/seccomp/seccomp_test.go new file mode 100644 index 00000000..2c9929e9 --- /dev/null +++ b/profiles/seccomp/seccomp_test.go @@ -0,0 +1,28 @@ +// +build linux + +package seccomp + +import ( + "io/ioutil" + "testing" +) + +func TestLoadProfile(t *testing.T) { + f, err := ioutil.ReadFile("fixtures/example.json") + if err != nil { + t.Fatal(err) + } + if _, err := LoadProfile(string(f)); err != nil { + t.Fatal(err) + } +} + +func TestLoadDefaultProfile(t *testing.T) { + f, err := ioutil.ReadFile("default.json") + if err != nil { + t.Fatal(err) + } + if _, err := LoadProfile(string(f)); err != nil { + t.Fatal(err) + } +} diff --git a/profiles/seccomp/seccomp_unsupported.go b/profiles/seccomp/seccomp_unsupported.go new file mode 100644 index 00000000..64963292 --- /dev/null +++ b/profiles/seccomp/seccomp_unsupported.go @@ -0,0 +1,10 @@ +// +build linux,!seccomp + +package seccomp + +import "github.com/docker/engine-api/types" + +var ( + // DefaultProfile is a nil pointer on unsupported systems. + DefaultProfile *types.Seccomp +) diff --git a/project/ARM.md b/project/ARM.md new file mode 100644 index 00000000..c4d21bf2 --- /dev/null +++ b/project/ARM.md @@ -0,0 +1,45 @@ +# ARM support + +The ARM support should be considered experimental. It will be extended step by step in the coming weeks. + +Building a Docker Development Image works in the same fashion as for Intel platform (x86-64). +Currently we have initial support for 32bit ARMv7 devices. + +To work with the Docker Development Image you have to clone the Docker/Docker repo on a supported device. +It needs to have a Docker Engine installed to build the Docker Development Image. + +From the root of the Docker/Docker repo one can use make to execute the following make targets: +- make validate +- make binary +- make build +- make deb +- make bundles +- make default +- make shell +- make test-unit +- make test-integration-cli +- make + +The Makefile does include logic to determine on which OS and architecture the Docker Development Image is built. +Based on OS and architecture it chooses the correct Dockerfile. +For the ARM 32bit architecture it uses `Dockerfile.armhf`. + +So for example in order to build a Docker binary one has to +1. clone the Docker/Docker repository on an ARM device `git clone git@github.com:docker/docker.git` +2. change into the checked out repository with `cd docker` +3. execute `make binary` to create a Docker Engine binary for ARM + +## Kernel modules +A few libnetwork integration tests require that the kernel be +configured with "dummy" network interface and has the module +loaded. However, the dummy module may be not loaded automatically. + +To load the kernel module permanently, run these commands as `root`. + + modprobe dummy + echo "dummy" >> /etc/modules + +On some systems you also have to sync your kernel modules. + + oc-sync-kernel-modules + depmod diff --git a/project/BRANCHES-AND-TAGS.md b/project/BRANCHES-AND-TAGS.md new file mode 100644 index 00000000..1c6f2325 --- /dev/null +++ b/project/BRANCHES-AND-TAGS.md @@ -0,0 +1,35 @@ +Branches and tags +================= + +Note: details of the release process for the Engine are documented in the +[RELEASE-CHECKLIST](https://github.com/docker/docker/blob/master/project/RELEASE-CHECKLIST.md). + +# Branches + +The docker/docker repository should normally have only three living branches at all time, including +the regular `master` branch: + +## `docs` branch + +The `docs` branch supports documentation updates between product releases. This branch allow us to +decouple documentation releases from product releases. + +## `release` branch + +The `release` branch contains the last _released_ version of the code for the project. + +The `release` branch is only updated at each public release of the project. The mechanism for this +is that the release is materialized by a pull request against the `release` branch which lives for +the duration of the code freeze period. When this pull request is merged, the `release` branch gets +updated, and its new state is tagged accordingly. + +# Tags + +Any public release of a compiled binary, with the logical exception of nightly builds, should have +a corresponding tag in the repository. + +The general format of a tag is `vX.Y.Z[-suffix[N]]`: + +- All of `X`, `Y`, `Z` must be specified (example: `v1.0.0`) +- First release candidate for version `1.8.0` should be tagged `v1.8.0-rc1` +- Second alpha release of a product should be tagged `v1.0.0-alpha1` diff --git a/project/CONTRIBUTORS.md b/project/CONTRIBUTORS.md new file mode 120000 index 00000000..44fcc634 --- /dev/null +++ b/project/CONTRIBUTORS.md @@ -0,0 +1 @@ +../CONTRIBUTING.md \ No newline at end of file diff --git a/project/GOVERNANCE.md b/project/GOVERNANCE.md new file mode 100644 index 00000000..6ae7baf7 --- /dev/null +++ b/project/GOVERNANCE.md @@ -0,0 +1,17 @@ +# Docker Governance Advisory Board Meetings + +In the spirit of openness, Docker created a Governance Advisory Board, and committed to make all materials and notes from the meetings of this group public. +All output from the meetings should be considered proposals only, and are subject to the review and approval of the community and the project leadership. + +The materials from the first Docker Governance Advisory Board meeting, held on October 28, 2014, are available at +[Google Docs Folder](https://goo.gl/Alfj8r) + +These include: + +* First Meeting Notes +* DGAB Charter +* Presentation 1: Introductory Presentation, including State of The Project +* Presentation 2: Overall Contribution Structure/Docker Project Core Proposal +* Presentation 3: Long Term Roadmap/Statement of Direction + + diff --git a/project/IRC-ADMINISTRATION.md b/project/IRC-ADMINISTRATION.md new file mode 100644 index 00000000..824a14bd --- /dev/null +++ b/project/IRC-ADMINISTRATION.md @@ -0,0 +1,37 @@ +# Freenode IRC Administration Guidelines and Tips + +This is not meant to be a general "Here's how to IRC" document, so if you're +looking for that, check Google instead. ♥ + +If you've been charged with helping maintain one of Docker's now many IRC +channels, this might turn out to be useful. If there's information that you +wish you'd known about how a particular channel is organized, you should add +deets here! :) + +## `ChanServ` + +Most channel maintenance happens by talking to Freenode's `ChanServ` bot. For +example, `/msg ChanServ ACCESS LIST` will show you a list of everyone +with "access" privileges for a particular channel. + +A similar command is used to give someone a particular access level. For +example, to add a new maintainer to the `#docker-maintainers` access list so +that they can contribute to the discussions (after they've been merged +appropriately in a `MAINTAINERS` file, of course), one would use `/msg ChanServ +ACCESS #docker-maintainers ADD maintainer`. + +To setup a new channel with a similar `maintainer` access template, use a +command like `/msg ChanServ TEMPLATE maintainer +AV` (`+A` for letting +them view the `ACCESS LIST`, `+V` for auto-voice; see `/msg ChanServ HELP FLAGS` +for more details). + +## Troubleshooting + +The most common cause of not-getting-auto-`+v` woes is people not being +`IDENTIFY`ed with `NickServ` (or their current nickname not being `GROUP`ed with +their main nickname) -- often manifested by `ChanServ` responding to an `ACCESS +ADD` request with something like `xyz is not registered.`. + +This is easily fixed by doing `/msg NickServ IDENTIFY OldNick SecretPassword` +followed by `/msg NickServ GROUP` to group the two nicknames together. See +`/msg NickServ HELP GROUP` for more information. diff --git a/project/ISSUE-TRIAGE.md b/project/ISSUE-TRIAGE.md new file mode 100644 index 00000000..80b2232b --- /dev/null +++ b/project/ISSUE-TRIAGE.md @@ -0,0 +1,91 @@ +Triaging of issues +------------------ + +Triage provides an important way to contribute to an open source project. Triage helps ensure issues resolve quickly by: + +- Describing the issue's intent and purpose is conveyed precisely. This is necessary because it can be difficult for an issue to explain how an end user experiences a problem and what actions they took. +- Giving a contributor the information they need before they commit to resolving an issue. +- Lowering the issue count by preventing duplicate issues. +- Streamlining the development process by preventing duplicate discussions. + +If you don't have time to code, consider helping with triage. The community will thank you for saving them time by spending some of yours. + +### 1. Ensure the issue contains basic information + +Before triaging an issue very far, make sure that the issue's author provided the standard issue information. This will help you make an educated recommendation on how this to categorize the issue. Standard information that *must* be included in most issues are things such as: + +- the output of `docker version` +- the output of `docker info` +- the output of `uname -a` +- a reproducible case if this is a bug, Dockerfiles FTW +- host distribution and version ( ubuntu 14.04, RHEL, fedora 23 ) +- page URL if this is a docs issue or the name of a man page + +Depending on the issue, you might not feel all this information is needed. Use your best judgement. If you cannot triage an issue using what its author provided, explain kindly to the author that they must provide the above information to clarify the problem. + +If the author provides the standard information but you are still unable to triage the issue, request additional information. Do this kindly and politely because you are asking for more of the author's time. + +If the author does not respond requested information within the timespan of a week, close the issue with a kind note stating that the author can request for the issue to be +reopened when the necessary information is provided. + +### 2. Classify the Issue + +An issue can have multiple of the following labels. + +#### Issue kind + +| Kind | Description | +|------------------|---------------------------------------------------------------------------------------------------------------------------------| +| kind/bug | Bugs are bugs. The cause may or may not be known at triage time so debugging should be taken account into the time estimate. | +| kind/docs | Writing documentation, man pages, articles, blogs, or other significant word-driven task. | +| kind/enhancement | Enhancement are not bugs or new features but can drastically improve usability or performance of a project component. | +| kind/feature | Functionality or other elements that the project does not currently support. Features are new and shiny. | +| kind/question | Contains a user or contributor question requiring a response. | + +#### Functional area + +| Area | +|---------------------------| +| area/api | +| area/builder | +| area/cli | +| area/kernel | +| area/runtime | +| area/storage | +| area/storage/aufs | +| area/storage/btrfs | +| area/storage/devicemapper | +| area/storage/overlay | +| area/storage/zfs | + +#### Experience level + +Experience level is a way for a contributor to find an issue based on their +skill set. Experience types are applied to the issue or pull request using +labels. + +| Level | Experience level guideline | +|------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| exp/beginner | New to Docker, and possibly Golang, and is looking to help while learning the basics. | +| exp/intermediate | Comfortable with golang and understands the core concepts of Docker and looking to dive deeper into the project. | +| exp/expert | Proficient with Docker and Golang and has been following, and active in, the community to understand the rationale behind design decisions and where the project is headed. | + +As the table states, these labels are meant as guidelines. You might have +written a whole plugin for Docker in a personal project and never contributed to +Docker. With that kind of experience, you could take on an exp/expert level task. + +### 3. Prioritizing issue + +When attached to a specific milestone, an issue can be attributed one of the +following labels to indicate their degree of priority (from more urgent to less +urgent). + +| Priority | Description | +|-------------|-----------------------------------------------------------------------------------------------------------------------------------| +| priority/P0 | Urgent: Security, critical bugs, blocking issues. P0 basically means drop everything you are doing until this issue is addressed. | +| priority/P1 | Important: P1 issues are a top priority and a must-have for the next release. | +| priority/P2 | Normal priority: default priority applied. | +| priority/P3 | Best effort: those are nice to have / minor issues. | + +And that's it. That should be all the information required for a new or existing contributor to come in an resolve an issue. diff --git a/project/PACKAGE-REPO-MAINTENANCE.md b/project/PACKAGE-REPO-MAINTENANCE.md new file mode 100644 index 00000000..3763f879 --- /dev/null +++ b/project/PACKAGE-REPO-MAINTENANCE.md @@ -0,0 +1,74 @@ +# Apt & Yum Repository Maintenance +## A maintainer's guide to managing Docker's package repos + +### How to clean up old experimental debs and rpms + +We release debs and rpms for experimental nightly, so these can build up. +To remove old experimental debs and rpms, and _ONLY_ keep the latest, follow the +steps below. + +1. Checkout docker master + +2. Run clean scripts + +```bash +docker build --rm --force-rm -t docker-dev:master . +docker run --rm -it --privileged \ + -v /path/to/your/repos/dir:/volumes/repos \ + -v $HOME/.gnupg:/root/.gnupg \ + -e GPG_PASSPHRASE \ + -e DOCKER_RELEASE_DIR=/volumes/repos \ + docker-dev:master hack/make.sh clean-apt-repo clean-yum-repo generate-index-listing sign-repos +``` + +3. Upload the changed repos to `s3` (if you host on s3) + +4. Purge the cache, PURGE the cache, PURGE THE CACHE! + +### How to get out of a sticky situation + +Sh\*t happens. We know. Below are steps to get out of any "hash-sum mismatch" or +"gpg sig error" or the likes error that might happen to the apt repo. + +**NOTE:** These are apt repo specific, have had no experimence with anything similar +happening to the yum repo in the past so you can rest easy. + +For each step listed below, move on to the next if the previous didn't work. +Otherwise CELEBRATE! + +1. Purge the cache. + +2. Did you remember to sign the debs after releasing? + +Re-sign the repo with your gpg key: + +```bash +docker build --rm --force-rm -t docker-dev:master . +docker run --rm -it --privileged \ + -v /path/to/your/repos/dir:/volumes/repos \ + -v $HOME/.gnupg:/root/.gnupg \ + -e GPG_PASSPHRASE \ + -e DOCKER_RELEASE_DIR=/volumes/repos \ + docker-dev:master hack/make.sh sign-repos +``` + +Upload the changed repo to `s3` (if that is where you host) + +PURGE THE CACHE. + +3. Run Jess' magical, save all, only in case of extreme emergencies, "you are +going to have to break this glass to get it" script. + +```bash +docker build --rm --force-rm -t docker-dev:master . +docker run --rm -it --privileged \ + -v /path/to/your/repos/dir:/volumes/repos \ + -v $HOME/.gnupg:/root/.gnupg \ + -e GPG_PASSPHRASE \ + -e DOCKER_RELEASE_DIR=/volumes/repos \ + docker-dev:master hack/make.sh update-apt-repo generate-index-listing sign-repos +``` + +4. Upload the changed repo to `s3` (if that is where you host) + +PURGE THE CACHE. diff --git a/project/PACKAGERS.md b/project/PACKAGERS.md new file mode 100644 index 00000000..03a69db1 --- /dev/null +++ b/project/PACKAGERS.md @@ -0,0 +1,308 @@ +# Dear Packager, + +If you are looking to make Docker available on your favorite software +distribution, this document is for you. It summarizes the requirements for +building and running the Docker client and the Docker daemon. + +## Getting Started + +We want to help you package Docker successfully. Before doing any packaging, a +good first step is to introduce yourself on the [docker-dev mailing +list](https://groups.google.com/d/forum/docker-dev), explain what you're trying +to achieve, and tell us how we can help. Don't worry, we don't bite! There might +even be someone already working on packaging for the same distro! + +You can also join the IRC channel - #docker and #docker-dev on Freenode are both +active and friendly. + +We like to refer to Tianon ("@tianon" on GitHub and "tianon" on IRC) as our +"Packagers Relations", since he's always working to make sure our packagers have +a good, healthy upstream to work with (both in our communication and in our +build scripts). If you're having any kind of trouble, feel free to ping him +directly. He also likes to keep track of what distributions we have packagers +for, so feel free to reach out to him even just to say "Hi!" + +## Package Name + +If possible, your package should be called "docker". If that name is already +taken, a second choice is "docker-engine". Another possible choice is "docker.io". + +## Official Build vs Distro Build + +The Docker project maintains its own build and release toolchain. It is pretty +neat and entirely based on Docker (surprise!). This toolchain is the canonical +way to build Docker. We encourage you to give it a try, and if the circumstances +allow you to use it, we recommend that you do. + +You might not be able to use the official build toolchain - usually because your +distribution has a toolchain and packaging policy of its own. We get it! Your +house, your rules. The rest of this document should give you the information you +need to package Docker your way, without denaturing it in the process. + +## Build Dependencies + +To build Docker, you will need the following: + +* A recent version of Git and Mercurial +* Go version 1.4 or later (Go version 1.5 or later required for hardware signing + support in Docker Content Trust) +* A clean checkout of the source added to a valid [Go + workspace](https://golang.org/doc/code.html#Workspaces) under the path + *src/github.com/docker/docker* (unless you plan to use `AUTO_GOPATH`, + explained in more detail below) + +To build the Docker daemon, you will additionally need: + +* An amd64/x86_64 machine running Linux +* SQLite version 3.7.9 or later +* libdevmapper version 1.02.68-cvs (2012-01-26) or later from lvm2 version + 2.02.89 or later +* btrfs-progs version 3.16.1 or later (unless using an older version is + absolutely necessary, in which case 3.8 is the minimum) +* libseccomp version 2.2.1 or later (for build tag seccomp) + +Be sure to also check out Docker's Dockerfile for the most up-to-date list of +these build-time dependencies. + +### Go Dependencies + +All Go dependencies are vendored under "./vendor". They are used by the official +build, so the source of truth for the current version of each dependency is +whatever is in "./vendor". + +To use the vendored dependencies, simply make sure the path to "./vendor" is +included in `GOPATH` (or use `AUTO_GOPATH`, as explained below). + +If you would rather (or must, due to distro policy) package these dependencies +yourself, take a look at "./hack/vendor.sh" for an easy-to-parse list of the +exact version for each. + +NOTE: if you're not able to package the exact version (to the exact commit) of a +given dependency, please get in touch so we can remediate! Who knows what +discrepancies can be caused by even the slightest deviation. We promise to do +our best to make everybody happy. + +## Stripping Binaries + +Please, please, please do not strip any compiled binaries. This is really +important. + +In our own testing, stripping the resulting binaries sometimes results in a +binary that appears to work, but more often causes random panics, segfaults, and +other issues. Even if the binary appears to work, please don't strip. + +See the following quotes from Dave Cheney, which explain this position better +from the upstream Golang perspective. + +### [go issue #5855, comment #3](https://code.google.com/p/go/issues/detail?id=5855#c3) + +> Super super important: Do not strip go binaries or archives. It isn't tested, +> often breaks, and doesn't work. + +### [launchpad golang issue #1200255, comment #8](https://bugs.launchpad.net/ubuntu/+source/golang/+bug/1200255/comments/8) + +> To quote myself: "Please do not strip Go binaries, it is not supported, not +> tested, is often broken, and doesn't do what you want" +> +> To unpack that a bit +> +> * not supported, as in, we don't support it, and recommend against it when +> asked +> * not tested, we don't test stripped binaries as part of the build CI process +> * is often broken, stripping a go binary will produce anywhere from no, to +> subtle, to outright execution failure, see above + +### [launchpad golang issue #1200255, comment #13](https://bugs.launchpad.net/ubuntu/+source/golang/+bug/1200255/comments/13) + +> To clarify my previous statements. +> +> * I do not disagree with the debian policy, it is there for a good reason +> * Having said that, it stripping Go binaries doesn't work, and nobody is +> looking at making it work, so there is that. +> +> Thanks for patching the build formula. + +## Building Docker + +Please use our build script ("./hack/make.sh") for all your compilation of +Docker. If there's something you need that it isn't doing, or something it could +be doing to make your life as a packager easier, please get in touch with Tianon +and help us rectify the situation. Chances are good that other packagers have +probably run into the same problems and a fix might already be in the works, but +none of us will know for sure unless you harass Tianon about it. :) + +All the commands listed within this section should be run with the Docker source +checkout as the current working directory. + +### `AUTO_GOPATH` + +If you'd rather not be bothered with the hassles that setting up `GOPATH` +appropriately can be, and prefer to just get a "build that works", you should +add something similar to this to whatever script or process you're using to +build Docker: + +```bash +export AUTO_GOPATH=1 +``` + +This will cause the build scripts to set up a reasonable `GOPATH` that +automatically and properly includes both docker/docker from the local +directory, and the local "./vendor" directory as necessary. + +### `DOCKER_BUILDTAGS` + +If you're building a binary that may need to be used on platforms that include +AppArmor, you will need to set `DOCKER_BUILDTAGS` as follows: +```bash +export DOCKER_BUILDTAGS='apparmor' +``` + +If you're building a binary that may need to be used on platforms that include +SELinux, you will need to use the `selinux` build tag: +```bash +export DOCKER_BUILDTAGS='selinux' +``` + +If you're building a binary that may need to be used on platforms that include +seccomp, you will need to use the `seccomp` build tag: +```bash +export DOCKER_BUILDTAGS='seccomp' +``` + +There are build tags for disabling graphdrivers as well. By default, support +for all graphdrivers are built in. + +To disable btrfs: +```bash +export DOCKER_BUILDTAGS='exclude_graphdriver_btrfs' +``` + +To disable devicemapper: +```bash +export DOCKER_BUILDTAGS='exclude_graphdriver_devicemapper' +``` + +To disable aufs: +```bash +export DOCKER_BUILDTAGS='exclude_graphdriver_aufs' +``` + +NOTE: if you need to set more than one build tag, space separate them: +```bash +export DOCKER_BUILDTAGS='apparmor selinux exclude_graphdriver_aufs' +``` + +### Static Daemon + +If it is feasible within the constraints of your distribution, you should +seriously consider packaging Docker as a single static binary. A good comparison +is Busybox, which is often packaged statically as a feature to enable mass +portability. Because of the unique way Docker operates, being similarly static +is a "feature". + +To build a static Docker daemon binary, run the following command (first +ensuring that all the necessary libraries are available in static form for +linking - see the "Build Dependencies" section above, and the relevant lines +within Docker's own Dockerfile that set up our official build environment): + +```bash +./hack/make.sh binary +``` + +This will create a static binary under +"./bundles/$VERSION/binary/docker-$VERSION", where "$VERSION" is the contents of +the file "./VERSION". This binary is usually installed somewhere like +"/usr/bin/docker". + +### Dynamic Daemon / Client-only Binary + +If you are only interested in a Docker client binary, set `DOCKER_CLIENTONLY` to a non-empty value using something similar to the following: + +```bash +export DOCKER_CLIENTONLY=1 +``` + +If you need to (due to distro policy, distro library availability, or for other +reasons) create a dynamically compiled daemon binary, or if you are only +interested in creating a client binary for Docker, use something similar to the +following: + +```bash +./hack/make.sh dynbinary +``` + +This will create "./bundles/$VERSION/dynbinary/docker-$VERSION", which for +client-only builds is the important file to grab and install as appropriate. + +## System Dependencies + +### Runtime Dependencies + +To function properly, the Docker daemon needs the following software to be +installed and available at runtime: + +* iptables version 1.4 or later +* procps (or similar provider of a "ps" executable) +* e2fsprogs version 1.4.12 or later (in use: mkfs.ext4, tune2fs) +* xfsprogs (in use: mkfs.xfs) +* XZ Utils version 4.9 or later +* a [properly + mounted](https://github.com/tianon/cgroupfs-mount/blob/master/cgroupfs-mount) + cgroupfs hierarchy (having a single, all-encompassing "cgroup" mount point + [is](https://github.com/docker/docker/issues/2683) + [not](https://github.com/docker/docker/issues/3485) + [sufficient](https://github.com/docker/docker/issues/4568)) + +Additionally, the Docker client needs the following software to be installed and +available at runtime: + +* Git version 1.7 or later + +### Kernel Requirements + +The Docker daemon has very specific kernel requirements. Most pre-packaged +kernels already include the necessary options enabled. If you are building your +own kernel, you will either need to discover the options necessary via trial and +error, or check out the [Gentoo +ebuild](https://github.com/tianon/docker-overlay/blob/master/app-emulation/docker/docker-9999.ebuild), +in which a list is maintained (and if there are any issues or discrepancies in +that list, please contact Tianon so they can be rectified). + +Note that in client mode, there are no specific kernel requirements, and that +the client will even run on alternative platforms such as Mac OS X / Darwin. + +### Optional Dependencies + +Some of Docker's features are activated by using optional command-line flags or +by having support for them in the kernel or userspace. A few examples include: + +* AUFS graph driver (requires AUFS patches/support enabled in the kernel, and at + least the "auplink" utility from aufs-tools) +* BTRFS graph driver (requires BTRFS support enabled in the kernel) +* ZFS graph driver (requires userspace zfs-utils and a corresponding kernel module) +* Libseccomp to allow running seccomp profiles with containers + +## Daemon Init Script + +Docker expects to run as a daemon at machine startup. Your package will need to +include a script for your distro's process supervisor of choice. Be sure to +check out the "contrib/init" folder in case a suitable init script already +exists (and if one does not, contact Tianon about whether it might be +appropriate for your distro's init script to live there too!). + +In general, Docker should be run as root, similar to the following: + +```bash +docker daemon +``` + +Generally, a `DOCKER_OPTS` variable of some kind is available for adding more +flags (such as changing the graph driver to use BTRFS, switching the location of +"/var/lib/docker", etc). + +## Communicate + +As a final note, please do feel free to reach out to Tianon at any time for +pretty much anything. He really does love hearing from our packagers and wants +to make sure we're not being a "hostile upstream". As should be a given, we +appreciate the work our packagers do to make sure we have broad distribution! diff --git a/project/PATCH-RELEASES.md b/project/PATCH-RELEASES.md new file mode 100644 index 00000000..548db9ab --- /dev/null +++ b/project/PATCH-RELEASES.md @@ -0,0 +1,68 @@ +# Docker patch (bugfix) release process + +Patch releases (the 'Z' in vX.Y.Z) are intended to fix major issues in a +release. Docker open source projects follow these procedures when creating a +patch release; + +After each release (both "major" (vX.Y.0) and "patch" releases (vX.Y.Z)), a +patch release milestone (vX.Y.Z + 1) is created. + +The creation of a patch release milestone is no obligation to actually +*create* a patch release. The purpose of these milestones is to collect +issues and pull requests that can *justify* a patch release; + +- Any maintainer is allowed to add issues and PR's to the milestone, when + doing so, preferably leave a comment on the issue or PR explaining *why* + you think it should be considered for inclusion in a patch release. +- Issues introduced in version vX.Y.0 get added to milestone X.Y.Z+1 +- Only *regressions* should be added. Issues *discovered* in version vX.Y.0, + but already present in version vX.Y-1.Z should not be added, unless + critical. +- Patch releases can *only* contain bug-fixes. New features should + *never* be added to a patch release. + +The release captain of the "major" (X.Y.0) release, is also responsible for +patch releases. The release captain, together with another maintainer, will +review issues and PRs on the milestone, and assigns `priority/`labels. These +review sessions take place on a weekly basis, more frequent if needed: + +- A P0 priority is assigned to critical issues. A maintainer *must* be + assigned to these issues. Maintainers should strive to fix a P0 within a week. +- A P1 priority is assigned to major issues, but not critical. A maintainer + *must* be assigned to these issues. +- P2 and P3 priorities are assigned to other issues. A maintainer can be + assigned. +- Non-critical issues and PR's can be removed from the milestone. Minor + changes, such as typo-fixes or omissions in the documentation can be + considered for inclusion in a patch release. + +## Deciding if a patch release should be done + +- Only a P0 can justify to proceed with the patch release. +- P1, P2, and P3 issues/PR's should not influence the decision, and + should be moved to the X.Y.Z+1 milestone, or removed from the + milestone. + +> **Note**: If the next "major" release is imminent, the release captain +> can decide to cancel a patch release, and include the patches in the +> upcoming major release. + +> **Note**: Security releases are also "patch releases", but follow +> a different procedure. Security releases are developed in a private +> repository, released and tested under embargo before they become +> publicly available. + +## Deciding on the content of a patch release + +When the criteria for moving forward with a patch release are met, the release +manager will decide on the exact content of the release. + +- Fixes to all P0 issues *must* be included in the release. +- Fixes to *some* P1, P2, and P3 issues *may* be included as part of the patch + release depending on the severity of the issue and the risk associated with + the patch. + +Any code delivered as part of a patch release should make life easier for a +significant amount of users with zero chance of degrading anybody's experience. +A good rule of thumb for that is to limit cherry-picking to small patches, which +fix well-understood issues, and which come with verifiable tests. diff --git a/project/PRINCIPLES.md b/project/PRINCIPLES.md new file mode 100644 index 00000000..53f03018 --- /dev/null +++ b/project/PRINCIPLES.md @@ -0,0 +1,19 @@ +# Docker principles + +In the design and development of Docker we try to follow these principles: + +(Work in progress) + +* Don't try to replace every tool. Instead, be an ingredient to improve them. +* Less code is better. +* Fewer components are better. Do you really need to add one more class? +* 50 lines of straightforward, readable code is better than 10 lines of magic that nobody can understand. +* Don't do later what you can do now. "//FIXME: refactor" is not acceptable in new code. +* When hesitating between 2 options, choose the one that is easier to reverse. +* No is temporary, Yes is forever. If you're not sure about a new feature, say no. You can change your mind later. +* Containers must be portable to the greatest possible number of machines. Be suspicious of any change which makes machines less interchangeable. +* The less moving parts in a container, the better. +* Don't merge it unless you document it. +* Don't document it unless you can keep it up-to-date. +* Don't merge it unless you test it! +* Everyone's problem is slightly different. Focus on the part that is the same for everyone, and solve that. diff --git a/project/README.md b/project/README.md new file mode 100644 index 00000000..3ed68cf2 --- /dev/null +++ b/project/README.md @@ -0,0 +1,24 @@ +# Hacking on Docker + +The `project/` directory holds information and tools for everyone involved in the process of creating and +distributing Docker, specifically: + +## Guides + +If you're a *contributor* or aspiring contributor, you should read [CONTRIBUTORS.md](../CONTRIBUTING.md). + +If you're a *maintainer* or aspiring maintainer, you should read [MAINTAINERS](../MAINTAINERS). + +If you're a *packager* or aspiring packager, you should read [PACKAGERS.md](./PACKAGERS.md). + +If you're a maintainer in charge of a *release*, you should read [RELEASE-CHECKLIST.md](./RELEASE-CHECKLIST.md). + +## Roadmap + +A high-level roadmap is available at [ROADMAP.md](../ROADMAP.md). + + +## Build tools + +[hack/make.sh](../hack/make.sh) is the primary build tool for docker. It is used for compiling the official binary, +running the test suite, and pushing releases. diff --git a/project/RELEASE-CHECKLIST.md b/project/RELEASE-CHECKLIST.md new file mode 100644 index 00000000..b9dcf7f4 --- /dev/null +++ b/project/RELEASE-CHECKLIST.md @@ -0,0 +1,512 @@ +# Release Checklist +## A maintainer's guide to releasing Docker + +So you're in charge of a Docker release? Cool. Here's what to do. + +If your experience deviates from this document, please document the changes +to keep it up-to-date. + +It is important to note that this document assumes that the git remote in your +repository that corresponds to "https://github.com/docker/docker" is named +"origin". If yours is not (for example, if you've chosen to name it "upstream" +or something similar instead), be sure to adjust the listed snippets for your +local environment accordingly. If you are not sure what your upstream remote is +named, use a command like `git remote -v` to find out. + +If you don't have an upstream remote, you can add one easily using something +like: + +```bash +export GITHUBUSER="YOUR_GITHUB_USER" +git remote add origin https://github.com/docker/docker.git +git remote add $GITHUBUSER git@github.com:$GITHUBUSER/docker.git +``` + +### 1. Pull from master and create a release branch + +All releases version numbers will be of the form: vX.Y.Z where X is the major +version number, Y is the minor version number and Z is the patch release version number. + +#### Major releases + +The release branch name is just vX.Y because it's going to be the basis for all .Z releases. + +```bash +export BASE=vX.Y +export VERSION=vX.Y.Z +git fetch origin +git checkout --track origin/master +git checkout -b release/$BASE +``` + +This new branch is going to be the base for the release. We need to push it to origin so we +can track the cherry-picked changes and the version bump: + +```bash +git push origin release/$BASE +``` + +When you have the major release branch in origin, we need to create the bump fork branch +that we'll push to our fork: + +```bash +git checkout -b bump_$VERSION +``` + +#### Patch releases + +If we have the release branch in origin, we can create the forked bump branch from it directly: + +```bash +export VERSION=vX.Y.Z +export PATCH=vX.Y.Z+1 +git fetch origin +git checkout --track origin/release/$BASE +git checkout -b bump_$PATCH +``` + +We cherry-pick only the commits we want into the bump branch: + +```bash +# get the commits ids we want to cherry-pick +git log +# cherry-pick the commits starting from the oldest one, without including merge commits +git cherry-pick +git cherry-pick +... +``` + +### 2. Update the VERSION files and API version on master + +We don't want to stop contributions to master just because we are releasing. +So, after the release branch is up, we bump the VERSION and API version to mark +the start of the "next" release. + +#### 2.1 Update the VERSION files + +Update the content of the `VERSION` file to be the next minor (incrementing Y) +and add the `-dev` suffix. For example, after the release branch for 1.5.0 is +created, the `VERSION` file gets updated to `1.6.0-dev` (as in "1.6.0 in the +making"). + +#### 2.2 Update API version on master + +We don't want API changes to go to the now frozen API version. Create a new +entry in `docs/reference/api/` by copying the latest and bumping the version +number (in both the file's name and content), and submit this in a PR against +master. + +### 3. Update CHANGELOG.md + +You can run this command for reference with git 2.0: + +```bash +git fetch --tags +LAST_VERSION=$(git tag -l --sort=-version:refname "v*" | grep -E 'v[0-9\.]+$' | head -1) +git log --stat $LAST_VERSION..bump_$VERSION +``` + +If you don't have git 2.0 but have a sort command that supports `-V`: +```bash +git fetch --tags +LAST_VERSION=$(git tag -l | grep -E 'v[0-9\.]+$' | sort -rV | head -1) +git log --stat $LAST_VERSION..bump_$VERSION +``` + +If releasing a major version (X or Y increased in vX.Y.Z), simply listing notable user-facing features is sufficient. +```markdown +#### Notable features since +* New docker command to do something useful +* Remote API change (deprecating old version) +* Performance improvements in some usecases +* ... +``` + +For minor releases (only Z increases in vX.Y.Z), provide a list of user-facing changes. +Each change should be listed under a category heading formatted as `#### CATEGORY`. + +`CATEGORY` should describe which part of the project is affected. + Valid categories are: + * Builder + * Documentation + * Hack + * Packaging + * Remote API + * Runtime + * Other (please use this category sparingly) + +Each change should be formatted as `BULLET DESCRIPTION`, given: + +* BULLET: either `-`, `+` or `*`, to indicate a bugfix, new feature or + upgrade, respectively. + +* DESCRIPTION: a concise description of the change that is relevant to the + end-user, using the present tense. Changes should be described in terms + of how they affect the user, for example "Add new feature X which allows Y", + "Fix bug which caused X", "Increase performance of Y". + +EXAMPLES: + +```markdown +## 0.3.6 (1995-12-25) + +#### Builder + ++ 'docker build -t FOO .' applies the tag FOO to the newly built image + +#### Remote API + +- Fix a bug in the optional unix socket transport + +#### Runtime + +* Improve detection of kernel version +``` + +If you need a list of contributors between the last major release and the +current bump branch, use something like: +```bash +git log --format='%aN <%aE>' v0.7.0...bump_v0.8.0 | sort -uf +``` +Obviously, you'll need to adjust version numbers as necessary. If you just need +a count, add a simple `| wc -l`. + +### 4. Change the contents of the VERSION file + +Before the big thing, you'll want to make successive release candidates and get +people to test. The release candidate number `N` should be part of the version: + +```bash +export RC_VERSION=${VERSION}-rcN +echo ${RC_VERSION#v} > VERSION +``` + +### 5. Test the docs + +Make sure that your tree includes documentation for any modified or +new features, syntax or semantic changes. + +To test locally: + +```bash +make docs +``` + +To make a shared test at https://beta-docs.docker.io: + +(You will need the `awsconfig` file added to the `docs/` dir) + +```bash +make AWS_S3_BUCKET=beta-docs.docker.io BUILD_ROOT=yes docs-release +``` + +### 6. Commit and create a pull request to the "release" branch + +```bash +git add VERSION CHANGELOG.md +git commit -m "Bump version to $VERSION" +git push $GITHUBUSER bump_$VERSION +echo "https://github.com/$GITHUBUSER/docker/compare/docker:release/$BASE...$GITHUBUSER:bump_$VERSION?expand=1" +``` + +That last command will give you the proper link to visit to ensure that you +open the PR against the "release" branch instead of accidentally against +"master" (like so many brave souls before you already have). + +### 7. Build release candidate rpms and debs + +**NOTE**: It will be a lot faster if you pass a different graphdriver with +`DOCKER_GRAPHDRIVER` than `vfs`. + +```bash +docker build -t docker . +docker run \ + --rm -t --privileged \ + -e DOCKER_GRAPHDRIVER=aufs \ + -v $(pwd)/bundles:/go/src/github.com/docker/docker/bundles \ + docker \ + hack/make.sh binary build-deb build-rpm +``` + +### 8. Publish release candidate rpms and debs + +With the rpms and debs you built from the last step you can release them on the +same server, or ideally, move them to a dedicated release box via scp into +another docker/docker directory in bundles. This next step assumes you have +a checkout of the docker source code at the same commit you used to build, with +the artifacts from the last step in `bundles`. + +**NOTE:** If you put a space before the command your `.bash_history` will not +save it. (for the `GPG_PASSPHRASE`). + +```bash +docker build -t docker . +docker run --rm -it --privileged \ + -v /volumes/repos:/volumes/repos \ + -v $(pwd)/bundles:/go/src/github.com/docker/docker/bundles \ + -v $HOME/.gnupg:/root/.gnupg \ + -e DOCKER_RELEASE_DIR=/volumes/repos \ + -e GPG_PASSPHRASE \ + -e KEEPBUNDLE=1 \ + docker \ + hack/make.sh release-deb release-rpm sign-repos generate-index-listing +``` + +### 9. Upload the changed repos to wherever you host + +For example, above we bind mounted `/volumes/repos` as the storage for +`DOCKER_RELEASE_DIR`. In this case `/volumes/repos/apt` can be synced with +a specific s3 bucket for the apt repo and `/volumes/repos/yum` can be synced with +a s3 bucket for the yum repo. + +### 10. Publish release candidate binaries + +To run this you will need access to the release credentials. Get them from the +Core maintainers. + +```bash +docker build -t docker . + +# static binaries are still pushed to s3 +docker run \ + -e AWS_S3_BUCKET=test.docker.com \ + -e AWS_ACCESS_KEY_ID \ + -e AWS_SECRET_ACCESS_KEY \ + -e AWS_DEFAULT_REGION \ + -i -t --privileged \ + docker \ + hack/release.sh +``` + +It will run the test suite, build the binaries and upload to the specified bucket, +so this is a good time to verify that you're running against **test**.docker.com. + +### 11. Purge the cache! + +After the binaries are uploaded to test.docker.com and the packages are on +apt.dockerproject.org and yum.dockerproject.org, make sure +they get tested in both Ubuntu and Debian for any obvious installation +issues or runtime issues. + +If everything looks good, it's time to create a git tag for this candidate: + +```bash +git tag -a $RC_VERSION -m $RC_VERSION bump_$VERSION +git push origin $RC_VERSION +``` + +Announcing on multiple medias is the best way to get some help testing! An easy +way to get some useful links for sharing: + +```bash +echo "Ubuntu/Debian: curl -sSL https://test.docker.com/ | sh" +echo "Linux 64bit binary: https://test.docker.com/builds/Linux/x86_64/docker-${VERSION#v}" +echo "Darwin/OSX 64bit client binary: https://test.docker.com/builds/Darwin/x86_64/docker-${VERSION#v}" +echo "Linux 64bit tgz: https://test.docker.com/builds/Linux/x86_64/docker-${VERSION#v}.tgz" +echo "Windows 64bit client binary: https://test.docker.com/builds/Windows/x86_64/docker-${VERSION#v}.exe" +echo "Windows 32bit client binary: https://test.docker.com/builds/Windows/i386/docker-${VERSION#v}.exe" +``` + +We recommend announcing the release candidate on: + +- IRC on #docker, #docker-dev, #docker-maintainers +- In a comment on the pull request to notify subscribed people on GitHub +- The [docker-dev](https://groups.google.com/forum/#!forum/docker-dev) group +- The [docker-maintainers](https://groups.google.com/a/dockerproject.org/forum/#!forum/maintainers) group +- Any social media that can bring some attention to the release candidate + +### 12. Iterate on successive release candidates + +Spend several days along with the community explicitly investing time and +resources to try and break Docker in every possible way, documenting any +findings pertinent to the release. This time should be spent testing and +finding ways in which the release might have caused various features or upgrade +environments to have issues, not coding. During this time, the release is in +code freeze, and any additional code changes will be pushed out to the next +release. + +It should include various levels of breaking Docker, beyond just using Docker +by the book. + +Any issues found may still remain issues for this release, but they should be +documented and give appropriate warnings. + +During this phase, the `bump_$VERSION` branch will keep evolving as you will +produce new release candidates. The frequency of new candidates is up to the +release manager: use your best judgement taking into account the severity of +reported issues, testers availability, and time to scheduled release date. + +Each time you'll want to produce a new release candidate, you will start by +adding commits to the branch, usually by cherry-picking from master: + +```bash +git cherry-pick -x -m0 +``` + +You want your "bump commit" (the one that updates the CHANGELOG and VERSION +files) to remain on top, so you'll have to `git rebase -i` to bring it back up. + +Now that your bump commit is back on top, you will need to update the CHANGELOG +file (if appropriate for this particular release candidate), and update the +VERSION file to increment the RC number: + +```bash +export RC_VERSION=$VERSION-rcN +echo $RC_VERSION > VERSION +``` + +You can now amend your last commit and update the bump branch: + +```bash +git commit --amend +git push -f $GITHUBUSER bump_$VERSION +``` + +Repeat step 6 to tag the code, publish new binaries, announce availability, and +get help testing. + +### 13. Finalize the bump branch + +When you're happy with the quality of a release candidate, you can move on and +create the real thing. + +You will first have to amend the "bump commit" to drop the release candidate +suffix in the VERSION file: + +```bash +echo $VERSION > VERSION +git add VERSION +git commit --amend +``` + +You will then repeat step 6 to publish the binaries to test + +### 14. Get 2 other maintainers to validate the pull request + +### 15. Build final rpms and debs + +```bash +docker build -t docker . +docker run \ + --rm -t --privileged \ + -v $(pwd)/bundles:/go/src/github.com/docker/docker/bundles \ + docker \ + hack/make.sh binary build-deb build-rpm +``` + +### 16. Publish final rpms and debs + +With the rpms and debs you built from the last step you can release them on the +same server, or ideally, move them to a dedicated release box via scp into +another docker/docker directory in bundles. This next step assumes you have +a checkout of the docker source code at the same commit you used to build, with +the artifacts from the last step in `bundles`. + +**NOTE:** If you put a space before the command your `.bash_history` will not +save it. (for the `GPG_PASSPHRASE`). + +```bash +docker build -t docker . +docker run --rm -it --privileged \ + -v /volumes/repos:/volumes/repos \ + -v $(pwd)/bundles:/go/src/github.com/docker/docker/bundles \ + -v $HOME/.gnupg:/root/.gnupg \ + -e DOCKER_RELEASE_DIR=/volumes/repos \ + -e GPG_PASSPHRASE \ + -e KEEPBUNDLE=1 \ + docker \ + hack/make.sh release-deb release-rpm sign-repos generate-index-listing +``` + +### 17. Upload the changed repos to wherever you host + +For example, above we bind mounted `/volumes/repos` as the storage for +`DOCKER_RELEASE_DIR`. In this case `/volumes/repos/apt` can be synced with +a specific s3 bucket for the apt repo and `/volumes/repos/yum` can be synced with +a s3 bucket for the yum repo. + +### 18. Publish final binaries + +Once they're tested and reasonably believed to be working, run against +get.docker.com: + +```bash +docker build -t docker . +# static binaries are still pushed to s3 +docker run \ + -e AWS_S3_BUCKET=get.docker.com \ + -e AWS_ACCESS_KEY_ID \ + -e AWS_SECRET_ACCESS_KEY \ + -e AWS_DEFAULT_REGION \ + -i -t --privileged \ + docker \ + hack/release.sh +``` + +### 19. Purge the cache! + +### 20. Apply tag and create release + +It's very important that we don't make the tag until after the official +release is uploaded to get.docker.com! + +```bash +git tag -a $VERSION -m $VERSION bump_$VERSION +git push origin $VERSION +``` + +Once the tag is pushed, go to GitHub and create a [new release](https://github.com/docker/docker/releases/new). +If the tag is for an RC make sure you check `This is a pre-release` at the bottom of the form. + +Select the tag that you just pushed as the version and paste the changelog in the description of the release. +You can see examples in this two links: + +https://github.com/docker/docker/releases/tag/v1.8.0 +https://github.com/docker/docker/releases/tag/v1.8.0-rc3 + +### 21. Go to github to merge the `bump_$VERSION` branch into release + +Don't forget to push that pretty blue button to delete the leftover +branch afterwards! + +### 22. Update the docs branch + +You will need to point the docs branch to the newly created release tag: + +```bash +git checkout origin/docs +git reset --hard origin/$VERSION +git push -f origin docs +``` + +The docs will appear on https://docs.docker.com/ (though there may be cached +versions, so its worth checking http://docs.docker.com.s3-website-us-east-1.amazonaws.com/). +For more information about documentation releases, see `docs/README.md`. + +Note that the new docs will not appear live on the site until the cache (a complex, +distributed CDN system) is flushed. The `make docs-release` command will do this +_if_ the `DISTRIBUTION_ID` is set correctly - this will take at least 15 minutes to run +and you can check its progress with the CDN Cloudfront Chrome addon. + +### 23. Create a new pull request to merge your bump commit back into master + +```bash +git checkout master +git fetch +git reset --hard origin/master +git cherry-pick $VERSION +git push $GITHUBUSER merge_release_$VERSION +echo "https://github.com/$GITHUBUSER/docker/compare/docker:master...$GITHUBUSER:merge_release_$VERSION?expand=1" +``` + +Again, get two maintainers to validate, then merge, then push that pretty +blue button to delete your branch. + +### 24. Rejoice and Evangelize! + +Congratulations! You're done. + +Go forth and announce the glad tidings of the new release in `#docker`, +`#docker-dev`, on the [dev mailing list](https://groups.google.com/forum/#!forum/docker-dev), +the [announce mailing list](https://groups.google.com/forum/#!forum/docker-announce), +and on Twitter! diff --git a/project/RELEASE-PROCESS.md b/project/RELEASE-PROCESS.md new file mode 100644 index 00000000..d764e9d0 --- /dev/null +++ b/project/RELEASE-PROCESS.md @@ -0,0 +1,78 @@ +# Docker Release Process + +This document describes how the Docker project is released. The Docker project +release process targets the Engine, Compose, Kitematic, Machine, Swarm, +Distribution, Notary and their underlying dependencies (libnetwork, libkv, +etc...). + +Step-by-step technical details of the process are described in +[RELEASE-CHECKLIST.md](https://github.com/docker/docker/blob/master/project/RELEASE-CHECKLIST.md). + +## Release cycle + +The Docker project follows a **time-based release cycle** and ships every nine +weeks. A release cycle starts the same day the previous release cycle ends. + +The first six weeks of the cycle are dedicated to development and review. During +this phase, new features and bugfixes submitted to any of the projects are +**eligible** to be shipped as part of the next release. No changeset submitted +during this period is however guaranteed to be merged for the current release +cycle. + +## The freeze period + +Six weeks after the beginning of the cycle, the codebase is officially frozen +and the codebase reaches a state close to the final release. A Release Candidate +(RC) gets created at the same time. The freeze period is used to find bugs and +get feedback on the state of the RC before the release. + +During this freeze period, while the `master` branch will continue its normal +development cycle, no new features are accepted into the RC. As bugs are fixed +in `master` the release owner will selectively 'cherry-pick' critical ones to +be included into the RC. As the RC changes, new ones are made available for the +community to test and review. + +This period lasts for three weeks. + +## How to maximize chances of being merged before the freeze date? + +First of all, there is never a guarantee that a specific changeset is going to +be merged. However there are different actions to follow to maximize the chances +for a changeset to be merged: + +- The team gives priority to review the PRs aligned with the Roadmap (usually +defined by a ROADMAP.md file at the root of the repository). +- The earlier a PR is opened, the more time the maintainers have to review. For +example, if a PR is opened the day before the freeze date, it’s very unlikely +that it will be merged for the release. +- Constant communication with the maintainers (mailing-list, IRC, Github issues, +etc.) allows to get early feedback on the design before getting into the +implementation, which usually reduces the time needed to discuss a changeset. +- If the code is commented, fully tested and by extension follows every single +rules defined by the [CONTRIBUTING guide]( +https://github.com/docker/docker/blob/master/CONTRIBUTING.md), this will help +the maintainers by speeding up the review. + +## The release + +At the end of the freeze (nine weeks after the start of the cycle), all the +projects are released together. + +``` + Codebase Release +Start of is frozen (end of the +the Cycle (7th week) 9th week) ++---------------------------------------+---------------------+ +| | | +| Development phase | Freeze phase | +| | | ++---------------------------------------+---------------------+ + 6 weeks 3 weeks +<---------------------------------------><--------------------> +``` + +## Exceptions + +If a critical issue is found at the end of the freeze period and more time is +needed to address it, the release will be pushed back. When a release gets +pushed back, the next release cycle gets delayed as well. diff --git a/project/REVIEWING.md b/project/REVIEWING.md new file mode 100644 index 00000000..95b9945c --- /dev/null +++ b/project/REVIEWING.md @@ -0,0 +1,195 @@ +Pull request reviewing process +============================== + +# Labels + +Labels are carefully picked to optimize for: + + - Readability: maintainers must immediately know the state of a PR + - Filtering simplicity: different labels represent many different aspects of + the reviewing work, and can even be targeted at different maintainers groups. + +A pull request should only be attributed labels documented in this section: other labels that may +exist on the repository should apply to issues. + +## DCO labels + + * `dco/no`: automatically set by a bot when one of the commits lacks proper signature + +## Status labels + + * `status/0-triage` + * `status/1-design-review` + * `status/2-code-review` + * `status/3-docs-review` + * `status/4-ready-to-merge` + +Special status labels: + + * `status/failing-ci`: indicates that the PR in its current state fails the test suite + * `status/needs-attention`: calls for a collective discussion during a review session + +## Specialty group labels + +Those labels are used to raise awareness of a particular specialty group, either because we need +help in reviewing the PR, or because of the potential impact of the PR on their work: + + * `group/distribution` + * `group/networking` + * `group/security` + * `group/windows` + +## Impact labels (apply to merged pull requests) + + * `impact/api` + * `impact/changelog` + * `impact/cli` + * `impact/deprecation` + * `impact/distribution` + * `impact/dockerfile` + +# Workflow + +An opened pull request can be in 1 of 5 distinct states, for each of which there is a corresponding +label that needs to be applied. + +## Triage - `status/0-triage` + +Maintainers are expected to triage new incoming pull requests by removing the `status/0-triage` +label and adding the correct labels (e.g. `status/1-design-review`) before any other interaction +with the PR. The starting label may potentially skip some steps depending on the kind of pull +request: use your best judgement. + +Maintainers should perform an initial, high-level, overview of the pull request before moving it to +the next appropriate stage: + + - Has DCO + - Contains sufficient justification (e.g., usecases) for the proposed change + - References the Github issue it fixes (if any) in the commit or the first Github comment + +Possible transitions from this state: + + * Close: e.g., unresponsive contributor without DCO + * `status/1-design-review`: general case + * `status/2-code-review`: e.g. trivial bugfix + * `status/3-docs-review`: non-proposal documentation-only change + +## Design review - `status/1-design-review` + +Maintainers are expected to comment on the design of the pull request. Review of documentation is +expected only in the context of design validation, not for stylistic changes. + +Ideally, documentation should reflect the expected behavior of the code. No code review should +take place in this step. + +There are no strict rules on the way a design is validated: we usually aim for a consensus, +although a single maintainer approval is often sufficient for obviously reasonable changes. In +general, strong disagreement expressed by any of the maintainers should not be taken lightly. + +Once design is approved, a maintainer should make sure to remove this label and add the next one. + +Possible transitions from this state: + + * Close: design rejected + * `status/2-code-review`: general case + * `status/3-docs-review`: proposals with only documentation changes + +## Code review - `status/2-code-review` + +Maintainers are expected to review the code and ensure that it is good quality and in accordance +with the documentation in the PR. + +New testcases are expected to be added. Ideally, those testcases should fail when the new code is +absent, and pass when present. The testcases should strive to test as many variants, code paths, as +possible to ensure maximum coverage. + +Changes to code must be reviewed and approved (LGTM'd) by a minimum of two code maintainers. When +the author of a PR is a maintainer, he still needs the approval of two other maintainers. + +Once code is approved according to the rules of the subsystem, a maintainer should make sure to +remove this label and add the next one. If documentation is absent but expected, maintainers should +ask for documentation and move to status `status/3-docs-review` for docs maintainer to follow. + +Possible transitions from this state: + + * Close + * `status/1-design-review`: new design concerns are raised + * `status/3-docs-review`: general case + * `status/4-ready-to-merge`: change not impacting documentation + +## Docs review - `status/3-docs-review` + +Maintainers are expected to review the documentation in its bigger context, ensuring consistency, +completeness, validity, and breadth of coverage across all existing and new documentation. + +They should ask for any editorial change that makes the documentation more consistent and easier to +understand. + +Changes and additions to docs must be reviewed and approved (LGTM'd) by a minimum of two docs +sub-project maintainers. If the docs change originates with a docs maintainer, only one additional +LGTM is required (since we assume a docs maintainer approves of their own PR). + +Once documentation is approved (see below), a maintainer should make sure to remove this label and +add the next one. + +Possible transitions from this state: + + * Close + * `status/1-design-review`: new design concerns are raised + * `status/2-code-review`: requires more code changes + * `status/4-ready-to-merge`: general case + +## Merge - `status/4-ready-to-merge` + +Maintainers are expected to merge this pull request as soon as possible. They can ask for a rebase +or carry the pull request themselves. + +Possible transitions from this state: + + * Merge: general case + * Close: carry PR + +After merging a pull request, the maintainer should consider applying one or multiple impact labels +to ease future classification: + + * `impact/api` signifies the patch impacted the remote API + * `impact/changelog` signifies the change is significant enough to make it in the changelog + * `impact/cli` signifies the patch impacted a CLI command + * `impact/dockerfile` signifies the patch impacted the Dockerfile syntax + * `impact/deprecation` signifies the patch participates in deprecating an existing feature + +## Close + +If a pull request is closed it is expected that sufficient justification will be provided. In +particular, if there are alternative ways of achieving the same net result then those needs to be +spelled out. If the pull request is trying to solve a use case that is not one that we (as a +community) want to support then a justification for why should be provided. + +The number of maintainers it takes to decide and close a PR is deliberately left unspecified. We +assume that the group of maintainers is bound by mutual trust and respect, and that opposition from +any single maintainer should be taken into consideration. Similarly, we expect maintainers to +justify their reasoning and to accept debating. + +# Escalation process + +Despite the previously described reviewing process, some PR might not show any progress for various +reasons: + + - No strong opinion for or against the proposed patch + - Debates about the proper way to solve the problem at hand + - Lack of consensus + - ... + +All these will eventually lead to stalled PR, where no apparent progress is made across several +weeks, or even months. + +Maintainers should use their best judgement and apply the `status/needs-attention` label. It must +be used sparingly, as each PR with such label will be discussed by a group of maintainers during a +review session. The goal of that session is to agree on one of the following outcomes for the PR: + + * Close, explaining the rationale for not pursuing further + * Continue, either by pushing the PR further in the workflow, or by deciding to carry the patch + (ideally, a maintainer should be immediately assigned to make sure that the PR keeps continued + attention) + * Escalate to Solomon by formulating a few specific questions on which his answers will allow + maintainers to decide. diff --git a/project/TOOLS.md b/project/TOOLS.md new file mode 100644 index 00000000..26303c30 --- /dev/null +++ b/project/TOOLS.md @@ -0,0 +1,63 @@ +# Tools + +This page describes the tools we use and infrastructure that is in place for +the Docker project. + +### CI + +The Docker project uses [Jenkins](https://jenkins.dockerproject.org/) as our +continuous integration server. Each Pull Request to Docker is tested by running the +equivalent of `make all`. We chose Jenkins because we can host it ourselves and +we run Docker in Docker to test. + +#### Leeroy + +Leeroy is a Go application which integrates Jenkins with +GitHub pull requests. Leeroy uses +[GitHub hooks](https://developer.github.com/v3/repos/hooks/) +to listen for pull request notifications and starts jobs on your Jenkins +server. Using the Jenkins +[notification plugin][https://wiki.jenkins-ci.org/display/JENKINS/Notification+Plugin], +Leeroy updates the pull request using GitHub's +[status API](https://developer.github.com/v3/repos/statuses/) +with pending, success, failure, or error statuses. + +The leeroy repository is maintained at +[github.com/docker/leeroy](https://github.com/docker/leeroy). + +#### GordonTheTurtle IRC Bot + +The GordonTheTurtle IRC Bot lives in the +[#docker-maintainers](https://botbot.me/freenode/docker-maintainers/) channel +on Freenode. He is built in Go and is based off the project at +[github.com/fabioxgn/go-bot](https://github.com/fabioxgn/go-bot). + +His main command is `!rebuild`, which rebuilds a given Pull Request for a repository. +This command works by integrating with Leroy. He has a few other commands too, such +as `!gif` or `!godoc`, but we are always looking for more fun commands to add. + +The gordon-bot repository is maintained at +[github.com/docker/gordon-bot](https://github.com/docker/gordon-bot) + +### NSQ + +We use [NSQ](https://github.com/bitly/nsq) for various aspects of the project +infrastructure. + +#### Hooks + +The hooks project, +[github.com/crosbymichael/hooks](https://github.com/crosbymichael/hooks), +is a small Go application that manages web hooks from github, hub.docker.com, or +other third party services. + +It can be used for listening to github webhooks & pushing them to a queue, +archiving hooks to rethinkdb for processing, and broadcasting hooks to various +jobs. + +#### Docker Master Binaries + +One of the things queued from the Hooks are the building of the Master +Binaries. This happens on every push to the master branch of Docker. The +repository for this is maintained at +[github.com/docker/docker-bb](https://github.com/docker/docker-bb). diff --git a/reference/reference.go b/reference/reference.go new file mode 100644 index 00000000..cdc8e63a --- /dev/null +++ b/reference/reference.go @@ -0,0 +1,211 @@ +package reference + +import ( + "errors" + "fmt" + "strings" + + "github.com/docker/distribution/digest" + distreference "github.com/docker/distribution/reference" + "github.com/docker/docker/image/v1" +) + +const ( + // DefaultTag defines the default tag used when performing images related actions and no tag or digest is specified + DefaultTag = "latest" + // DefaultHostname is the default built-in hostname + DefaultHostname = "docker.io" + // LegacyDefaultHostname is automatically converted to DefaultHostname + LegacyDefaultHostname = "index.docker.io" + // DefaultRepoPrefix is the prefix used for default repositories in default host + DefaultRepoPrefix = "library/" +) + +// Named is an object with a full name +type Named interface { + // Name returns normalized repository name, like "ubuntu". + Name() string + // String returns full reference, like "ubuntu@sha256:abcdef..." + String() string + // FullName returns full repository name with hostname, like "docker.io/library/ubuntu" + FullName() string + // Hostname returns hostname for the reference, like "docker.io" + Hostname() string + // RemoteName returns the repository component of the full name, like "library/ubuntu" + RemoteName() string +} + +// NamedTagged is an object including a name and tag. +type NamedTagged interface { + Named + Tag() string +} + +// Canonical reference is an object with a fully unique +// name including a name with hostname and digest +type Canonical interface { + Named + Digest() digest.Digest +} + +// ParseNamed parses s and returns a syntactically valid reference implementing +// the Named interface. The reference must have a name, otherwise an error is +// returned. +// If an error was encountered it is returned, along with a nil Reference. +func ParseNamed(s string) (Named, error) { + named, err := distreference.ParseNamed(s) + if err != nil { + return nil, fmt.Errorf("Error parsing reference: %q is not a valid repository/tag", s) + } + r, err := WithName(named.Name()) + if err != nil { + return nil, err + } + if canonical, isCanonical := named.(distreference.Canonical); isCanonical { + return WithDigest(r, canonical.Digest()) + } + if tagged, isTagged := named.(distreference.NamedTagged); isTagged { + return WithTag(r, tagged.Tag()) + } + return r, nil +} + +// WithName returns a named object representing the given string. If the input +// is invalid ErrReferenceInvalidFormat will be returned. +func WithName(name string) (Named, error) { + name, err := normalize(name) + if err != nil { + return nil, err + } + if err := validateName(name); err != nil { + return nil, err + } + r, err := distreference.WithName(name) + if err != nil { + return nil, err + } + return &namedRef{r}, nil +} + +// WithTag combines the name from "name" and the tag from "tag" to form a +// reference incorporating both the name and the tag. +func WithTag(name Named, tag string) (NamedTagged, error) { + r, err := distreference.WithTag(name, tag) + if err != nil { + return nil, err + } + return &taggedRef{namedRef{r}}, nil +} + +// WithDigest combines the name from "name" and the digest from "digest" to form +// a reference incorporating both the name and the digest. +func WithDigest(name Named, digest digest.Digest) (Canonical, error) { + r, err := distreference.WithDigest(name, digest) + if err != nil { + return nil, err + } + return &canonicalRef{namedRef{r}}, nil +} + +type namedRef struct { + distreference.Named +} +type taggedRef struct { + namedRef +} +type canonicalRef struct { + namedRef +} + +func (r *namedRef) FullName() string { + hostname, remoteName := splitHostname(r.Name()) + return hostname + "/" + remoteName +} +func (r *namedRef) Hostname() string { + hostname, _ := splitHostname(r.Name()) + return hostname +} +func (r *namedRef) RemoteName() string { + _, remoteName := splitHostname(r.Name()) + return remoteName +} +func (r *taggedRef) Tag() string { + return r.namedRef.Named.(distreference.NamedTagged).Tag() +} +func (r *canonicalRef) Digest() digest.Digest { + return r.namedRef.Named.(distreference.Canonical).Digest() +} + +// WithDefaultTag adds a default tag to a reference if it only has a repo name. +func WithDefaultTag(ref Named) Named { + if IsNameOnly(ref) { + ref, _ = WithTag(ref, DefaultTag) + } + return ref +} + +// IsNameOnly returns true if reference only contains a repo name. +func IsNameOnly(ref Named) bool { + if _, ok := ref.(NamedTagged); ok { + return false + } + if _, ok := ref.(Canonical); ok { + return false + } + return true +} + +// ParseIDOrReference parses string for a image ID or a reference. ID can be +// without a default prefix. +func ParseIDOrReference(idOrRef string) (digest.Digest, Named, error) { + if err := v1.ValidateID(idOrRef); err == nil { + idOrRef = "sha256:" + idOrRef + } + if dgst, err := digest.ParseDigest(idOrRef); err == nil { + return dgst, nil, nil + } + ref, err := ParseNamed(idOrRef) + return "", ref, err +} + +// splitHostname splits a repository name to hostname and remotename string. +// If no valid hostname is found, the default hostname is used. Repository name +// needs to be already validated before. +func splitHostname(name string) (hostname, remoteName string) { + i := strings.IndexRune(name, '/') + if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost") { + hostname, remoteName = DefaultHostname, name + } else { + hostname, remoteName = name[:i], name[i+1:] + } + if hostname == LegacyDefaultHostname { + hostname = DefaultHostname + } + if hostname == DefaultHostname && !strings.ContainsRune(remoteName, '/') { + remoteName = DefaultRepoPrefix + remoteName + } + return +} + +// normalize returns a repository name in its normalized form, meaning it +// will not contain default hostname nor library/ prefix for official images. +func normalize(name string) (string, error) { + host, remoteName := splitHostname(name) + if strings.ToLower(remoteName) != remoteName { + return "", errors.New("invalid reference format: repository name must be lowercase") + } + if host == DefaultHostname { + if strings.HasPrefix(remoteName, DefaultRepoPrefix) { + return strings.TrimPrefix(remoteName, DefaultRepoPrefix), nil + } + return remoteName, nil + } + return name, nil +} + +func validateName(name string) error { + if err := v1.ValidateID(name); err == nil { + return fmt.Errorf("Invalid repository name (%s), cannot specify 64-byte hexadecimal strings", name) + } + return nil +} diff --git a/reference/reference_test.go b/reference/reference_test.go new file mode 100644 index 00000000..ff35ba3d --- /dev/null +++ b/reference/reference_test.go @@ -0,0 +1,275 @@ +package reference + +import ( + "testing" + + "github.com/docker/distribution/digest" +) + +func TestValidateReferenceName(t *testing.T) { + validRepoNames := []string{ + "docker/docker", + "library/debian", + "debian", + "docker.io/docker/docker", + "docker.io/library/debian", + "docker.io/debian", + "index.docker.io/docker/docker", + "index.docker.io/library/debian", + "index.docker.io/debian", + "127.0.0.1:5000/docker/docker", + "127.0.0.1:5000/library/debian", + "127.0.0.1:5000/debian", + "thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev", + } + invalidRepoNames := []string{ + "https://github.com/docker/docker", + "docker/Docker", + "-docker", + "-docker/docker", + "-docker.io/docker/docker", + "docker///docker", + "docker.io/docker/Docker", + "docker.io/docker///docker", + "1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", + "docker.io/1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", + } + + for _, name := range invalidRepoNames { + _, err := ParseNamed(name) + if err == nil { + t.Fatalf("Expected invalid repo name for %q", name) + } + } + + for _, name := range validRepoNames { + _, err := ParseNamed(name) + if err != nil { + t.Fatalf("Error parsing repo name %s, got: %q", name, err) + } + } +} + +func TestValidateRemoteName(t *testing.T) { + validRepositoryNames := []string{ + // Sanity check. + "docker/docker", + + // Allow 64-character non-hexadecimal names (hexadecimal names are forbidden). + "thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev", + + // Allow embedded hyphens. + "docker-rules/docker", + + // Allow multiple hyphens as well. + "docker---rules/docker", + + //Username doc and image name docker being tested. + "doc/docker", + + // single character names are now allowed. + "d/docker", + "jess/t", + + // Consecutive underscores. + "dock__er/docker", + } + for _, repositoryName := range validRepositoryNames { + _, err := ParseNamed(repositoryName) + if err != nil { + t.Errorf("Repository name should be valid: %v. Error: %v", repositoryName, err) + } + } + + invalidRepositoryNames := []string{ + // Disallow capital letters. + "docker/Docker", + + // Only allow one slash. + "docker///docker", + + // Disallow 64-character hexadecimal. + "1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", + + // Disallow leading and trailing hyphens in namespace. + "-docker/docker", + "docker-/docker", + "-docker-/docker", + + // Don't allow underscores everywhere (as opposed to hyphens). + "____/____", + + "_docker/_docker", + + // Disallow consecutive periods. + "dock..er/docker", + "dock_.er/docker", + "dock-.er/docker", + + // No repository. + "docker/", + + //namespace too long + "this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255/docker", + } + for _, repositoryName := range invalidRepositoryNames { + if _, err := ParseNamed(repositoryName); err == nil { + t.Errorf("Repository name should be invalid: %v", repositoryName) + } + } +} + +func TestParseRepositoryInfo(t *testing.T) { + type tcase struct { + RemoteName, NormalizedName, FullName, AmbiguousName, Hostname string + } + + tcases := []tcase{ + { + RemoteName: "fooo/bar", + NormalizedName: "fooo/bar", + FullName: "docker.io/fooo/bar", + AmbiguousName: "index.docker.io/fooo/bar", + Hostname: "docker.io", + }, + { + RemoteName: "library/ubuntu", + NormalizedName: "ubuntu", + FullName: "docker.io/library/ubuntu", + AmbiguousName: "library/ubuntu", + Hostname: "docker.io", + }, + { + RemoteName: "nonlibrary/ubuntu", + NormalizedName: "nonlibrary/ubuntu", + FullName: "docker.io/nonlibrary/ubuntu", + AmbiguousName: "", + Hostname: "docker.io", + }, + { + RemoteName: "other/library", + NormalizedName: "other/library", + FullName: "docker.io/other/library", + AmbiguousName: "", + Hostname: "docker.io", + }, + { + RemoteName: "private/moonbase", + NormalizedName: "127.0.0.1:8000/private/moonbase", + FullName: "127.0.0.1:8000/private/moonbase", + AmbiguousName: "", + Hostname: "127.0.0.1:8000", + }, + { + RemoteName: "privatebase", + NormalizedName: "127.0.0.1:8000/privatebase", + FullName: "127.0.0.1:8000/privatebase", + AmbiguousName: "", + Hostname: "127.0.0.1:8000", + }, + { + RemoteName: "private/moonbase", + NormalizedName: "example.com/private/moonbase", + FullName: "example.com/private/moonbase", + AmbiguousName: "", + Hostname: "example.com", + }, + { + RemoteName: "privatebase", + NormalizedName: "example.com/privatebase", + FullName: "example.com/privatebase", + AmbiguousName: "", + Hostname: "example.com", + }, + { + RemoteName: "private/moonbase", + NormalizedName: "example.com:8000/private/moonbase", + FullName: "example.com:8000/private/moonbase", + AmbiguousName: "", + Hostname: "example.com:8000", + }, + { + RemoteName: "privatebasee", + NormalizedName: "example.com:8000/privatebasee", + FullName: "example.com:8000/privatebasee", + AmbiguousName: "", + Hostname: "example.com:8000", + }, + { + RemoteName: "library/ubuntu-12.04-base", + NormalizedName: "ubuntu-12.04-base", + FullName: "docker.io/library/ubuntu-12.04-base", + AmbiguousName: "index.docker.io/library/ubuntu-12.04-base", + Hostname: "docker.io", + }, + } + + for _, tcase := range tcases { + refStrings := []string{tcase.NormalizedName, tcase.FullName} + if tcase.AmbiguousName != "" { + refStrings = append(refStrings, tcase.AmbiguousName) + } + + var refs []Named + for _, r := range refStrings { + named, err := ParseNamed(r) + if err != nil { + t.Fatal(err) + } + refs = append(refs, named) + named, err = WithName(r) + if err != nil { + t.Fatal(err) + } + refs = append(refs, named) + } + + for _, r := range refs { + if expected, actual := tcase.NormalizedName, r.Name(); expected != actual { + t.Fatalf("Invalid normalized reference for %q. Expected %q, got %q", r, expected, actual) + } + if expected, actual := tcase.FullName, r.FullName(); expected != actual { + t.Fatalf("Invalid normalized reference for %q. Expected %q, got %q", r, expected, actual) + } + if expected, actual := tcase.Hostname, r.Hostname(); expected != actual { + t.Fatalf("Invalid hostname for %q. Expected %q, got %q", r, expected, actual) + } + if expected, actual := tcase.RemoteName, r.RemoteName(); expected != actual { + t.Fatalf("Invalid remoteName for %q. Expected %q, got %q", r, expected, actual) + } + + } + } +} + +func TestParseReferenceWithTagAndDigest(t *testing.T) { + ref, err := ParseNamed("busybox:latest@sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa") + if err != nil { + t.Fatal(err) + } + if _, isTagged := ref.(NamedTagged); isTagged { + t.Fatalf("Reference from %q should not support tag", ref) + } + if _, isCanonical := ref.(Canonical); !isCanonical { + t.Fatalf("Reference from %q should not support digest", ref) + } + if expected, actual := "busybox@sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa", ref.String(); actual != expected { + t.Fatalf("Invalid parsed reference for %q: expected %q, got %q", ref, expected, actual) + } +} + +func TestInvalidReferenceComponents(t *testing.T) { + if _, err := WithName("-foo"); err == nil { + t.Fatal("Expected WithName to detect invalid name") + } + ref, err := WithName("busybox") + if err != nil { + t.Fatal(err) + } + if _, err := WithTag(ref, "-foo"); err == nil { + t.Fatal("Expected WithName to detect invalid tag") + } + if _, err := WithDigest(ref, digest.Digest("foo")); err == nil { + t.Fatal("Expected WithName to detect invalid digest") + } +} diff --git a/reference/store.go b/reference/store.go new file mode 100644 index 00000000..91c5c2ae --- /dev/null +++ b/reference/store.go @@ -0,0 +1,298 @@ +package reference + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sort" + "sync" + + "github.com/docker/distribution/digest" + "github.com/docker/docker/image" +) + +var ( + // ErrDoesNotExist is returned if a reference is not found in the + // store. + ErrDoesNotExist = errors.New("reference does not exist") +) + +// An Association is a tuple associating a reference with an image ID. +type Association struct { + Ref Named + ImageID image.ID +} + +// Store provides the set of methods which can operate on a tag store. +type Store interface { + References(id image.ID) []Named + ReferencesByName(ref Named) []Association + AddTag(ref Named, id image.ID, force bool) error + AddDigest(ref Canonical, id image.ID, force bool) error + Delete(ref Named) (bool, error) + Get(ref Named) (image.ID, error) +} + +type store struct { + mu sync.RWMutex + // jsonPath is the path to the file where the serialized tag data is + // stored. + jsonPath string + // Repositories is a map of repositories, indexed by name. + Repositories map[string]repository + // referencesByIDCache is a cache of references indexed by ID, to speed + // up References. + referencesByIDCache map[image.ID]map[string]Named +} + +// Repository maps tags to image IDs. The key is a a stringified Reference, +// including the repository name. +type repository map[string]image.ID + +type lexicalRefs []Named + +func (a lexicalRefs) Len() int { return len(a) } +func (a lexicalRefs) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a lexicalRefs) Less(i, j int) bool { return a[i].String() < a[j].String() } + +type lexicalAssociations []Association + +func (a lexicalAssociations) Len() int { return len(a) } +func (a lexicalAssociations) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a lexicalAssociations) Less(i, j int) bool { return a[i].Ref.String() < a[j].Ref.String() } + +// NewReferenceStore creates a new reference store, tied to a file path where +// the set of references are serialized in JSON format. +func NewReferenceStore(jsonPath string) (Store, error) { + abspath, err := filepath.Abs(jsonPath) + if err != nil { + return nil, err + } + + store := &store{ + jsonPath: abspath, + Repositories: make(map[string]repository), + referencesByIDCache: make(map[image.ID]map[string]Named), + } + // Load the json file if it exists, otherwise create it. + if err := store.reload(); os.IsNotExist(err) { + if err := store.save(); err != nil { + return nil, err + } + } else if err != nil { + return nil, err + } + return store, nil +} + +// AddTag adds a tag reference to the store. If force is set to true, existing +// references can be overwritten. This only works for tags, not digests. +func (store *store) AddTag(ref Named, id image.ID, force bool) error { + if _, isCanonical := ref.(Canonical); isCanonical { + return errors.New("refusing to create a tag with a digest reference") + } + return store.addReference(WithDefaultTag(ref), id, force) +} + +// AddDigest adds a digest reference to the store. +func (store *store) AddDigest(ref Canonical, id image.ID, force bool) error { + return store.addReference(ref, id, force) +} + +func (store *store) addReference(ref Named, id image.ID, force bool) error { + if ref.Name() == string(digest.Canonical) { + return errors.New("refusing to create an ambiguous tag using digest algorithm as name") + } + + store.mu.Lock() + defer store.mu.Unlock() + + repository, exists := store.Repositories[ref.Name()] + if !exists || repository == nil { + repository = make(map[string]image.ID) + store.Repositories[ref.Name()] = repository + } + + refStr := ref.String() + oldID, exists := repository[refStr] + + if exists { + // force only works for tags + if digested, isDigest := ref.(Canonical); isDigest { + return fmt.Errorf("Cannot overwrite digest %s", digested.Digest().String()) + } + + if !force { + return fmt.Errorf("Conflict: Tag %s is already set to image %s, if you want to replace it, please use -f option", ref.String(), oldID.String()) + } + + if store.referencesByIDCache[oldID] != nil { + delete(store.referencesByIDCache[oldID], refStr) + if len(store.referencesByIDCache[oldID]) == 0 { + delete(store.referencesByIDCache, oldID) + } + } + } + + repository[refStr] = id + if store.referencesByIDCache[id] == nil { + store.referencesByIDCache[id] = make(map[string]Named) + } + store.referencesByIDCache[id][refStr] = ref + + return store.save() +} + +// Delete deletes a reference from the store. It returns true if a deletion +// happened, or false otherwise. +func (store *store) Delete(ref Named) (bool, error) { + ref = WithDefaultTag(ref) + + store.mu.Lock() + defer store.mu.Unlock() + + repoName := ref.Name() + + repository, exists := store.Repositories[repoName] + if !exists { + return false, ErrDoesNotExist + } + + refStr := ref.String() + if id, exists := repository[refStr]; exists { + delete(repository, refStr) + if len(repository) == 0 { + delete(store.Repositories, repoName) + } + if store.referencesByIDCache[id] != nil { + delete(store.referencesByIDCache[id], refStr) + if len(store.referencesByIDCache[id]) == 0 { + delete(store.referencesByIDCache, id) + } + } + return true, store.save() + } + + return false, ErrDoesNotExist +} + +// Get retrieves an item from the store by +func (store *store) Get(ref Named) (image.ID, error) { + ref = WithDefaultTag(ref) + + store.mu.RLock() + defer store.mu.RUnlock() + + repository, exists := store.Repositories[ref.Name()] + if !exists || repository == nil { + return "", ErrDoesNotExist + } + + id, exists := repository[ref.String()] + if !exists { + return "", ErrDoesNotExist + } + + return id, nil +} + +// References returns a slice of references to the given image ID. The slice +// will be nil if there are no references to this image ID. +func (store *store) References(id image.ID) []Named { + store.mu.RLock() + defer store.mu.RUnlock() + + // Convert the internal map to an array for two reasons: + // 1) We must not return a mutable + // 2) It would be ugly to expose the extraneous map keys to callers. + + var references []Named + for _, ref := range store.referencesByIDCache[id] { + references = append(references, ref) + } + + sort.Sort(lexicalRefs(references)) + + return references +} + +// ReferencesByName returns the references for a given repository name. +// If there are no references known for this repository name, +// ReferencesByName returns nil. +func (store *store) ReferencesByName(ref Named) []Association { + store.mu.RLock() + defer store.mu.RUnlock() + + repository, exists := store.Repositories[ref.Name()] + if !exists { + return nil + } + + var associations []Association + for refStr, refID := range repository { + ref, err := ParseNamed(refStr) + if err != nil { + // Should never happen + return nil + } + associations = append(associations, + Association{ + Ref: ref, + ImageID: refID, + }) + } + + sort.Sort(lexicalAssociations(associations)) + + return associations +} + +func (store *store) save() error { + // Store the json + jsonData, err := json.Marshal(store) + if err != nil { + return err + } + + tempFilePath := store.jsonPath + ".tmp" + + if err := ioutil.WriteFile(tempFilePath, jsonData, 0600); err != nil { + return err + } + + if err := os.Rename(tempFilePath, store.jsonPath); err != nil { + return err + } + + return nil +} + +func (store *store) reload() error { + f, err := os.Open(store.jsonPath) + if err != nil { + return err + } + defer f.Close() + if err := json.NewDecoder(f).Decode(&store); err != nil { + return err + } + + for _, repository := range store.Repositories { + for refStr, refID := range repository { + ref, err := ParseNamed(refStr) + if err != nil { + // Should never happen + continue + } + if store.referencesByIDCache[refID] == nil { + store.referencesByIDCache[refID] = make(map[string]Named) + } + store.referencesByIDCache[refID][refStr] = ref + } + } + + return nil +} diff --git a/reference/store_test.go b/reference/store_test.go new file mode 100644 index 00000000..a877c55f --- /dev/null +++ b/reference/store_test.go @@ -0,0 +1,356 @@ +package reference + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/docker/docker/image" +) + +var ( + saveLoadTestCases = map[string]image.ID{ + "registry:5000/foobar:HEAD": "sha256:470022b8af682154f57a2163d030eb369549549cba00edc69e1b99b46bb924d6", + "registry:5000/foobar:alternate": "sha256:ae300ebc4a4f00693702cfb0a5e0b7bc527b353828dc86ad09fb95c8a681b793", + "registry:5000/foobar:latest": "sha256:6153498b9ac00968d71b66cca4eac37e990b5f9eb50c26877eb8799c8847451b", + "registry:5000/foobar:master": "sha256:6c9917af4c4e05001b346421959d7ea81b6dc9d25718466a37a6add865dfd7fc", + "jess/hollywood:latest": "sha256:ae7a5519a0a55a2d4ef20ddcbd5d0ca0888a1f7ab806acc8e2a27baf46f529fe", + "registry@sha256:367eb40fd0330a7e464777121e39d2f5b3e8e23a1e159342e53ab05c9e4d94e6": "sha256:24126a56805beb9711be5f4590cc2eb55ab8d4a85ebd618eed72bb19fc50631c", + "busybox:latest": "sha256:91e54dfb11794fad694460162bf0cb0a4fa710cfa3f60979c177d920813e267c", + } + + marshalledSaveLoadTestCases = []byte(`{"Repositories":{"busybox":{"busybox:latest":"sha256:91e54dfb11794fad694460162bf0cb0a4fa710cfa3f60979c177d920813e267c"},"jess/hollywood":{"jess/hollywood:latest":"sha256:ae7a5519a0a55a2d4ef20ddcbd5d0ca0888a1f7ab806acc8e2a27baf46f529fe"},"registry":{"registry@sha256:367eb40fd0330a7e464777121e39d2f5b3e8e23a1e159342e53ab05c9e4d94e6":"sha256:24126a56805beb9711be5f4590cc2eb55ab8d4a85ebd618eed72bb19fc50631c"},"registry:5000/foobar":{"registry:5000/foobar:HEAD":"sha256:470022b8af682154f57a2163d030eb369549549cba00edc69e1b99b46bb924d6","registry:5000/foobar:alternate":"sha256:ae300ebc4a4f00693702cfb0a5e0b7bc527b353828dc86ad09fb95c8a681b793","registry:5000/foobar:latest":"sha256:6153498b9ac00968d71b66cca4eac37e990b5f9eb50c26877eb8799c8847451b","registry:5000/foobar:master":"sha256:6c9917af4c4e05001b346421959d7ea81b6dc9d25718466a37a6add865dfd7fc"}}}`) +) + +func TestLoad(t *testing.T) { + jsonFile, err := ioutil.TempFile("", "tag-store-test") + if err != nil { + t.Fatalf("error creating temp file: %v", err) + } + defer os.RemoveAll(jsonFile.Name()) + + // Write canned json to the temp file + _, err = jsonFile.Write(marshalledSaveLoadTestCases) + if err != nil { + t.Fatalf("error writing to temp file: %v", err) + } + jsonFile.Close() + + store, err := NewReferenceStore(jsonFile.Name()) + if err != nil { + t.Fatalf("error creating tag store: %v", err) + } + + for refStr, expectedID := range saveLoadTestCases { + ref, err := ParseNamed(refStr) + if err != nil { + t.Fatalf("failed to parse reference: %v", err) + } + id, err := store.Get(ref) + if err != nil { + t.Fatalf("could not find reference %s: %v", refStr, err) + } + if id != expectedID { + t.Fatalf("expected %s - got %s", expectedID, id) + } + } +} + +func TestSave(t *testing.T) { + jsonFile, err := ioutil.TempFile("", "tag-store-test") + if err != nil { + t.Fatalf("error creating temp file: %v", err) + } + _, err = jsonFile.Write([]byte(`{}`)) + jsonFile.Close() + defer os.RemoveAll(jsonFile.Name()) + + store, err := NewReferenceStore(jsonFile.Name()) + if err != nil { + t.Fatalf("error creating tag store: %v", err) + } + + for refStr, id := range saveLoadTestCases { + ref, err := ParseNamed(refStr) + if err != nil { + t.Fatalf("failed to parse reference: %v", err) + } + if canonical, ok := ref.(Canonical); ok { + err = store.AddDigest(canonical, id, false) + if err != nil { + t.Fatalf("could not add digest reference %s: %v", refStr, err) + } + } else { + err = store.AddTag(ref, id, false) + if err != nil { + t.Fatalf("could not add reference %s: %v", refStr, err) + } + } + } + + jsonBytes, err := ioutil.ReadFile(jsonFile.Name()) + if err != nil { + t.Fatalf("could not read json file: %v", err) + } + + if !bytes.Equal(jsonBytes, marshalledSaveLoadTestCases) { + t.Fatalf("save output did not match expectations\nexpected:\n%s\ngot:\n%s", marshalledSaveLoadTestCases, jsonBytes) + } +} + +func TestAddDeleteGet(t *testing.T) { + jsonFile, err := ioutil.TempFile("", "tag-store-test") + if err != nil { + t.Fatalf("error creating temp file: %v", err) + } + _, err = jsonFile.Write([]byte(`{}`)) + jsonFile.Close() + defer os.RemoveAll(jsonFile.Name()) + + store, err := NewReferenceStore(jsonFile.Name()) + if err != nil { + t.Fatalf("error creating tag store: %v", err) + } + + testImageID1 := image.ID("sha256:9655aef5fd742a1b4e1b7b163aa9f1c76c186304bf39102283d80927c916ca9c") + testImageID2 := image.ID("sha256:9655aef5fd742a1b4e1b7b163aa9f1c76c186304bf39102283d80927c916ca9d") + testImageID3 := image.ID("sha256:9655aef5fd742a1b4e1b7b163aa9f1c76c186304bf39102283d80927c916ca9e") + + // Try adding a reference with no tag or digest + nameOnly, err := WithName("username/repo") + if err != nil { + t.Fatalf("could not parse reference: %v", err) + } + if err = store.AddTag(nameOnly, testImageID1, false); err != nil { + t.Fatalf("error adding to store: %v", err) + } + + // Add a few references + ref1, err := ParseNamed("username/repo1:latest") + if err != nil { + t.Fatalf("could not parse reference: %v", err) + } + if err = store.AddTag(ref1, testImageID1, false); err != nil { + t.Fatalf("error adding to store: %v", err) + } + + ref2, err := ParseNamed("username/repo1:old") + if err != nil { + t.Fatalf("could not parse reference: %v", err) + } + if err = store.AddTag(ref2, testImageID2, false); err != nil { + t.Fatalf("error adding to store: %v", err) + } + + ref3, err := ParseNamed("username/repo1:alias") + if err != nil { + t.Fatalf("could not parse reference: %v", err) + } + if err = store.AddTag(ref3, testImageID1, false); err != nil { + t.Fatalf("error adding to store: %v", err) + } + + ref4, err := ParseNamed("username/repo2:latest") + if err != nil { + t.Fatalf("could not parse reference: %v", err) + } + if err = store.AddTag(ref4, testImageID2, false); err != nil { + t.Fatalf("error adding to store: %v", err) + } + + ref5, err := ParseNamed("username/repo3@sha256:58153dfb11794fad694460162bf0cb0a4fa710cfa3f60979c177d920813e267c") + if err != nil { + t.Fatalf("could not parse reference: %v", err) + } + if err = store.AddDigest(ref5.(Canonical), testImageID2, false); err != nil { + t.Fatalf("error adding to store: %v", err) + } + + // Attempt to overwrite with force == false + if err = store.AddTag(ref4, testImageID3, false); err == nil || !strings.HasPrefix(err.Error(), "Conflict:") { + t.Fatalf("did not get expected error on overwrite attempt - got %v", err) + } + // Repeat to overwrite with force == true + if err = store.AddTag(ref4, testImageID3, true); err != nil { + t.Fatalf("failed to force tag overwrite: %v", err) + } + + // Check references so far + id, err := store.Get(nameOnly) + if err != nil { + t.Fatalf("Get returned error: %v", err) + } + if id != testImageID1 { + t.Fatalf("id mismatch: got %s instead of %s", id.String(), testImageID1.String()) + } + + id, err = store.Get(ref1) + if err != nil { + t.Fatalf("Get returned error: %v", err) + } + if id != testImageID1 { + t.Fatalf("id mismatch: got %s instead of %s", id.String(), testImageID1.String()) + } + + id, err = store.Get(ref2) + if err != nil { + t.Fatalf("Get returned error: %v", err) + } + if id != testImageID2 { + t.Fatalf("id mismatch: got %s instead of %s", id.String(), testImageID2.String()) + } + + id, err = store.Get(ref3) + if err != nil { + t.Fatalf("Get returned error: %v", err) + } + if id != testImageID1 { + t.Fatalf("id mismatch: got %s instead of %s", id.String(), testImageID1.String()) + } + + id, err = store.Get(ref4) + if err != nil { + t.Fatalf("Get returned error: %v", err) + } + if id != testImageID3 { + t.Fatalf("id mismatch: got %s instead of %s", id.String(), testImageID3.String()) + } + + id, err = store.Get(ref5) + if err != nil { + t.Fatalf("Get returned error: %v", err) + } + if id != testImageID2 { + t.Fatalf("id mismatch: got %s instead of %s", id.String(), testImageID3.String()) + } + + // Get should return ErrDoesNotExist for a nonexistent repo + nonExistRepo, err := ParseNamed("username/nonexistrepo:latest") + if err != nil { + t.Fatalf("could not parse reference: %v", err) + } + if _, err = store.Get(nonExistRepo); err != ErrDoesNotExist { + t.Fatal("Expected ErrDoesNotExist from Get") + } + + // Get should return ErrDoesNotExist for a nonexistent tag + nonExistTag, err := ParseNamed("username/repo1:nonexist") + if err != nil { + t.Fatalf("could not parse reference: %v", err) + } + if _, err = store.Get(nonExistTag); err != ErrDoesNotExist { + t.Fatal("Expected ErrDoesNotExist from Get") + } + + // Check References + refs := store.References(testImageID1) + if len(refs) != 3 { + t.Fatal("unexpected number of references") + } + // Looking for the references in this order verifies that they are + // returned lexically sorted. + if refs[0].String() != ref3.String() { + t.Fatalf("unexpected reference: %v", refs[0].String()) + } + if refs[1].String() != ref1.String() { + t.Fatalf("unexpected reference: %v", refs[1].String()) + } + if refs[2].String() != nameOnly.String()+":latest" { + t.Fatalf("unexpected reference: %v", refs[2].String()) + } + + // Check ReferencesByName + repoName, err := WithName("username/repo1") + if err != nil { + t.Fatalf("could not parse reference: %v", err) + } + associations := store.ReferencesByName(repoName) + if len(associations) != 3 { + t.Fatal("unexpected number of associations") + } + // Looking for the associations in this order verifies that they are + // returned lexically sorted. + if associations[0].Ref.String() != ref3.String() { + t.Fatalf("unexpected reference: %v", associations[0].Ref.String()) + } + if associations[0].ImageID != testImageID1 { + t.Fatalf("unexpected reference: %v", associations[0].Ref.String()) + } + if associations[1].Ref.String() != ref1.String() { + t.Fatalf("unexpected reference: %v", associations[1].Ref.String()) + } + if associations[1].ImageID != testImageID1 { + t.Fatalf("unexpected reference: %v", associations[1].Ref.String()) + } + if associations[2].Ref.String() != ref2.String() { + t.Fatalf("unexpected reference: %v", associations[2].Ref.String()) + } + if associations[2].ImageID != testImageID2 { + t.Fatalf("unexpected reference: %v", associations[2].Ref.String()) + } + + // Delete should return ErrDoesNotExist for a nonexistent repo + if _, err = store.Delete(nonExistRepo); err != ErrDoesNotExist { + t.Fatal("Expected ErrDoesNotExist from Delete") + } + + // Delete should return ErrDoesNotExist for a nonexistent tag + if _, err = store.Delete(nonExistTag); err != ErrDoesNotExist { + t.Fatal("Expected ErrDoesNotExist from Delete") + } + + // Delete a few references + if deleted, err := store.Delete(ref1); err != nil || deleted != true { + t.Fatal("Delete failed") + } + if _, err := store.Get(ref1); err != ErrDoesNotExist { + t.Fatal("Expected ErrDoesNotExist from Get") + } + if deleted, err := store.Delete(ref5); err != nil || deleted != true { + t.Fatal("Delete failed") + } + if _, err := store.Get(ref5); err != ErrDoesNotExist { + t.Fatal("Expected ErrDoesNotExist from Get") + } + if deleted, err := store.Delete(nameOnly); err != nil || deleted != true { + t.Fatal("Delete failed") + } + if _, err := store.Get(nameOnly); err != ErrDoesNotExist { + t.Fatal("Expected ErrDoesNotExist from Get") + } +} + +func TestInvalidTags(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "tag-store-test") + defer os.RemoveAll(tmpDir) + + store, err := NewReferenceStore(filepath.Join(tmpDir, "repositories.json")) + if err != nil { + t.Fatalf("error creating tag store: %v", err) + } + id := image.ID("sha256:470022b8af682154f57a2163d030eb369549549cba00edc69e1b99b46bb924d6") + + // sha256 as repo name + ref, err := ParseNamed("sha256:abc") + if err != nil { + t.Fatal(err) + } + err = store.AddTag(ref, id, true) + if err == nil { + t.Fatalf("expected setting tag %q to fail", ref) + } + + // setting digest as a tag + ref, err = ParseNamed("registry@sha256:367eb40fd0330a7e464777121e39d2f5b3e8e23a1e159342e53ab05c9e4d94e6") + if err != nil { + t.Fatal(err) + } + err = store.AddTag(ref, id, true) + if err == nil { + t.Fatalf("expected setting digest %q to fail", ref) + } + +} diff --git a/registry/auth.go b/registry/auth.go new file mode 100644 index 00000000..c5663f58 --- /dev/null +++ b/registry/auth.go @@ -0,0 +1,262 @@ +package registry + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/registry/client/auth" + "github.com/docker/distribution/registry/client/transport" + "github.com/docker/engine-api/types" + registrytypes "github.com/docker/engine-api/types/registry" +) + +const ( + // AuthClientID is used the ClientID used for the token server + AuthClientID = "docker" +) + +// loginV1 tries to register/login to the v1 registry server. +func loginV1(authConfig *types.AuthConfig, apiEndpoint APIEndpoint, userAgent string) (string, string, error) { + registryEndpoint, err := apiEndpoint.ToV1Endpoint(userAgent, nil) + if err != nil { + return "", "", err + } + + serverAddress := registryEndpoint.String() + + logrus.Debugf("attempting v1 login to registry endpoint %s", serverAddress) + + if serverAddress == "" { + return "", "", fmt.Errorf("Server Error: Server Address not set.") + } + + loginAgainstOfficialIndex := serverAddress == IndexServer + + req, err := http.NewRequest("GET", serverAddress+"users/", nil) + if err != nil { + return "", "", err + } + req.SetBasicAuth(authConfig.Username, authConfig.Password) + resp, err := registryEndpoint.client.Do(req) + if err != nil { + // fallback when request could not be completed + return "", "", fallbackError{ + err: err, + } + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", "", err + } + if resp.StatusCode == http.StatusOK { + return "Login Succeeded", "", nil + } else if resp.StatusCode == http.StatusUnauthorized { + if loginAgainstOfficialIndex { + return "", "", fmt.Errorf("Wrong login/password, please try again. Haven't got a Docker ID? Create one at https://hub.docker.com") + } + return "", "", fmt.Errorf("Wrong login/password, please try again") + } else if resp.StatusCode == http.StatusForbidden { + if loginAgainstOfficialIndex { + return "", "", fmt.Errorf("Login: Account is not active. Please check your e-mail for a confirmation link.") + } + // *TODO: Use registry configuration to determine what this says, if anything? + return "", "", fmt.Errorf("Login: Account is not active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress) + } else if resp.StatusCode == http.StatusInternalServerError { // Issue #14326 + logrus.Errorf("%s returned status code %d. Response Body :\n%s", req.URL.String(), resp.StatusCode, body) + return "", "", fmt.Errorf("Internal Server Error") + } + return "", "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, + resp.StatusCode, resp.Header) +} + +type loginCredentialStore struct { + authConfig *types.AuthConfig +} + +func (lcs loginCredentialStore) Basic(*url.URL) (string, string) { + return lcs.authConfig.Username, lcs.authConfig.Password +} + +func (lcs loginCredentialStore) RefreshToken(*url.URL, string) string { + return lcs.authConfig.IdentityToken +} + +func (lcs loginCredentialStore) SetRefreshToken(u *url.URL, service, token string) { + lcs.authConfig.IdentityToken = token +} + +type fallbackError struct { + err error +} + +func (err fallbackError) Error() string { + return err.err.Error() +} + +// loginV2 tries to login to the v2 registry server. The given registry +// endpoint will be pinged to get authorization challenges. These challenges +// will be used to authenticate against the registry to validate credentials. +func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent string) (string, string, error) { + logrus.Debugf("attempting v2 login to registry endpoint %s", strings.TrimRight(endpoint.URL.String(), "/")+"/v2/") + + modifiers := DockerHeaders(userAgent, nil) + authTransport := transport.NewTransport(NewTransport(endpoint.TLSConfig), modifiers...) + + challengeManager, foundV2, err := PingV2Registry(endpoint, authTransport) + if err != nil { + if !foundV2 { + err = fallbackError{err: err} + } + return "", "", err + } + + credentialAuthConfig := *authConfig + creds := loginCredentialStore{ + authConfig: &credentialAuthConfig, + } + + tokenHandlerOptions := auth.TokenHandlerOptions{ + Transport: authTransport, + Credentials: creds, + OfflineAccess: true, + ClientID: AuthClientID, + } + tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions) + basicHandler := auth.NewBasicHandler(creds) + modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)) + tr := transport.NewTransport(authTransport, modifiers...) + + loginClient := &http.Client{ + Transport: tr, + Timeout: 15 * time.Second, + } + + endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/" + req, err := http.NewRequest("GET", endpointStr, nil) + if err != nil { + if !foundV2 { + err = fallbackError{err: err} + } + return "", "", err + } + + resp, err := loginClient.Do(req) + if err != nil { + if !foundV2 { + err = fallbackError{err: err} + } + return "", "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + // TODO(dmcgowan): Attempt to further interpret result, status code and error code string + err := fmt.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode)) + if !foundV2 { + err = fallbackError{err: err} + } + return "", "", err + } + + return "Login Succeeded", credentialAuthConfig.IdentityToken, nil + +} + +// ResolveAuthConfig matches an auth configuration to a server address or a URL +func ResolveAuthConfig(authConfigs map[string]types.AuthConfig, index *registrytypes.IndexInfo) types.AuthConfig { + configKey := GetAuthConfigKey(index) + // First try the happy case + if c, found := authConfigs[configKey]; found || index.Official { + return c + } + + convertToHostname := func(url string) string { + stripped := url + if strings.HasPrefix(url, "http://") { + stripped = strings.Replace(url, "http://", "", 1) + } else if strings.HasPrefix(url, "https://") { + stripped = strings.Replace(url, "https://", "", 1) + } + + nameParts := strings.SplitN(stripped, "/", 2) + + return nameParts[0] + } + + // Maybe they have a legacy config file, we will iterate the keys converting + // them to the new format and testing + for registry, ac := range authConfigs { + if configKey == convertToHostname(registry) { + return ac + } + } + + // When all else fails, return an empty auth config + return types.AuthConfig{} +} + +// PingResponseError is used when the response from a ping +// was received but invalid. +type PingResponseError struct { + Err error +} + +func (err PingResponseError) Error() string { + return err.Error() +} + +// PingV2Registry attempts to ping a v2 registry and on success return a +// challenge manager for the supported authentication types and +// whether v2 was confirmed by the response. If a response is received but +// cannot be interpreted a PingResponseError will be returned. +func PingV2Registry(endpoint APIEndpoint, transport http.RoundTripper) (auth.ChallengeManager, bool, error) { + var ( + foundV2 = false + v2Version = auth.APIVersion{ + Type: "registry", + Version: "2.0", + } + ) + + pingClient := &http.Client{ + Transport: transport, + Timeout: 15 * time.Second, + } + endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/" + req, err := http.NewRequest("GET", endpointStr, nil) + if err != nil { + return nil, false, err + } + resp, err := pingClient.Do(req) + if err != nil { + return nil, false, err + } + defer resp.Body.Close() + + versions := auth.APIVersions(resp, DefaultRegistryVersionHeader) + for _, pingVersion := range versions { + if pingVersion == v2Version { + // The version header indicates we're definitely + // talking to a v2 registry. So don't allow future + // fallbacks to the v1 protocol. + + foundV2 = true + break + } + } + + challengeManager := auth.NewSimpleChallengeManager() + if err := challengeManager.AddResponse(resp); err != nil { + return nil, foundV2, PingResponseError{ + Err: err, + } + } + + return challengeManager, foundV2, nil +} diff --git a/registry/auth_test.go b/registry/auth_test.go new file mode 100644 index 00000000..eedee44e --- /dev/null +++ b/registry/auth_test.go @@ -0,0 +1,120 @@ +package registry + +import ( + "testing" + + "github.com/docker/engine-api/types" + registrytypes "github.com/docker/engine-api/types/registry" +) + +func buildAuthConfigs() map[string]types.AuthConfig { + authConfigs := map[string]types.AuthConfig{} + + for _, registry := range []string{"testIndex", IndexServer} { + authConfigs[registry] = types.AuthConfig{ + Username: "docker-user", + Password: "docker-pass", + } + } + + return authConfigs +} + +func TestSameAuthDataPostSave(t *testing.T) { + authConfigs := buildAuthConfigs() + authConfig := authConfigs["testIndex"] + if authConfig.Username != "docker-user" { + t.Fail() + } + if authConfig.Password != "docker-pass" { + t.Fail() + } + if authConfig.Auth != "" { + t.Fail() + } +} + +func TestResolveAuthConfigIndexServer(t *testing.T) { + authConfigs := buildAuthConfigs() + indexConfig := authConfigs[IndexServer] + + officialIndex := ®istrytypes.IndexInfo{ + Official: true, + } + privateIndex := ®istrytypes.IndexInfo{ + Official: false, + } + + resolved := ResolveAuthConfig(authConfigs, officialIndex) + assertEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to return IndexServer") + + resolved = ResolveAuthConfig(authConfigs, privateIndex) + assertNotEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to not return IndexServer") +} + +func TestResolveAuthConfigFullURL(t *testing.T) { + authConfigs := buildAuthConfigs() + + registryAuth := types.AuthConfig{ + Username: "foo-user", + Password: "foo-pass", + } + localAuth := types.AuthConfig{ + Username: "bar-user", + Password: "bar-pass", + } + officialAuth := types.AuthConfig{ + Username: "baz-user", + Password: "baz-pass", + } + authConfigs[IndexServer] = officialAuth + + expectedAuths := map[string]types.AuthConfig{ + "registry.example.com": registryAuth, + "localhost:8000": localAuth, + "registry.com": localAuth, + } + + validRegistries := map[string][]string{ + "registry.example.com": { + "https://registry.example.com/v1/", + "http://registry.example.com/v1/", + "registry.example.com", + "registry.example.com/v1/", + }, + "localhost:8000": { + "https://localhost:8000/v1/", + "http://localhost:8000/v1/", + "localhost:8000", + "localhost:8000/v1/", + }, + "registry.com": { + "https://registry.com/v1/", + "http://registry.com/v1/", + "registry.com", + "registry.com/v1/", + }, + } + + for configKey, registries := range validRegistries { + configured, ok := expectedAuths[configKey] + if !ok { + t.Fail() + } + index := ®istrytypes.IndexInfo{ + Name: configKey, + } + for _, registry := range registries { + authConfigs[registry] = configured + resolved := ResolveAuthConfig(authConfigs, index) + if resolved.Username != configured.Username || resolved.Password != configured.Password { + t.Errorf("%s -> %v != %v\n", registry, resolved, configured) + } + delete(authConfigs, registry) + resolved = ResolveAuthConfig(authConfigs, index) + if resolved.Username == configured.Username || resolved.Password == configured.Password { + t.Errorf("%s -> %v == %v\n", registry, resolved, configured) + } + } + } +} diff --git a/registry/config.go b/registry/config.go new file mode 100644 index 00000000..50061329 --- /dev/null +++ b/registry/config.go @@ -0,0 +1,274 @@ +package registry + +import ( + "errors" + "fmt" + "net" + "net/url" + "strings" + + "github.com/docker/docker/opts" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/reference" + registrytypes "github.com/docker/engine-api/types/registry" +) + +// ServiceOptions holds command line options. +type ServiceOptions struct { + Mirrors []string `json:"registry-mirrors,omitempty"` + InsecureRegistries []string `json:"insecure-registries,omitempty"` + + // V2Only controls access to legacy registries. If it is set to true via the + // command line flag the daemon will not attempt to contact v1 legacy registries + V2Only bool `json:"disable-legacy-registry,omitempty"` +} + +// serviceConfig holds daemon configuration for the registry service. +type serviceConfig struct { + registrytypes.ServiceConfig + V2Only bool +} + +var ( + // DefaultNamespace is the default namespace + DefaultNamespace = "docker.io" + // DefaultRegistryVersionHeader is the name of the default HTTP header + // that carries Registry version info + DefaultRegistryVersionHeader = "Docker-Distribution-Api-Version" + + // IndexServer is the v1 registry server used for user auth + account creation + IndexServer = DefaultV1Registry.String() + "/v1/" + // IndexName is the name of the index + IndexName = "docker.io" + + // NotaryServer is the endpoint serving the Notary trust server + NotaryServer = "https://notary.docker.io" + + // DefaultV1Registry is the URI of the default v1 registry + DefaultV1Registry = &url.URL{ + Scheme: "https", + Host: "index.docker.io", + } + + // DefaultV2Registry is the URI of the default v2 registry + DefaultV2Registry = &url.URL{ + Scheme: "https", + Host: "registry-1.docker.io", + } +) + +var ( + // ErrInvalidRepositoryName is an error returned if the repository name did + // not have the correct form + ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")") + + emptyServiceConfig = newServiceConfig(ServiceOptions{}) +) + +// for mocking in unit tests +var lookupIP = net.LookupIP + +// InstallCliFlags adds command-line options to the top-level flag parser for +// the current process. +func (options *ServiceOptions) InstallCliFlags(cmd *flag.FlagSet, usageFn func(string) string) { + mirrors := opts.NewNamedListOptsRef("registry-mirrors", &options.Mirrors, ValidateMirror) + cmd.Var(mirrors, []string{"-registry-mirror"}, usageFn("Preferred Docker registry mirror")) + + insecureRegistries := opts.NewNamedListOptsRef("insecure-registries", &options.InsecureRegistries, ValidateIndexName) + cmd.Var(insecureRegistries, []string{"-insecure-registry"}, usageFn("Enable insecure registry communication")) + + cmd.BoolVar(&options.V2Only, []string{"-disable-legacy-registry"}, false, usageFn("Do not contact legacy registries")) +} + +// newServiceConfig returns a new instance of ServiceConfig +func newServiceConfig(options ServiceOptions) *serviceConfig { + // Localhost is by default considered as an insecure registry + // This is a stop-gap for people who are running a private registry on localhost (especially on Boot2docker). + // + // TODO: should we deprecate this once it is easier for people to set up a TLS registry or change + // daemon flags on boot2docker? + options.InsecureRegistries = append(options.InsecureRegistries, "127.0.0.0/8") + + config := &serviceConfig{ + ServiceConfig: registrytypes.ServiceConfig{ + InsecureRegistryCIDRs: make([]*registrytypes.NetIPNet, 0), + IndexConfigs: make(map[string]*registrytypes.IndexInfo, 0), + // Hack: Bypass setting the mirrors to IndexConfigs since they are going away + // and Mirrors are only for the official registry anyways. + Mirrors: options.Mirrors, + }, + V2Only: options.V2Only, + } + // Split --insecure-registry into CIDR and registry-specific settings. + for _, r := range options.InsecureRegistries { + // Check if CIDR was passed to --insecure-registry + _, ipnet, err := net.ParseCIDR(r) + if err == nil { + // Valid CIDR. + config.InsecureRegistryCIDRs = append(config.InsecureRegistryCIDRs, (*registrytypes.NetIPNet)(ipnet)) + } else { + // Assume `host:port` if not CIDR. + config.IndexConfigs[r] = ®istrytypes.IndexInfo{ + Name: r, + Mirrors: make([]string, 0), + Secure: false, + Official: false, + } + } + } + + // Configure public registry. + config.IndexConfigs[IndexName] = ®istrytypes.IndexInfo{ + Name: IndexName, + Mirrors: config.Mirrors, + Secure: true, + Official: true, + } + + return config +} + +// isSecureIndex returns false if the provided indexName is part of the list of insecure registries +// Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs. +// +// The list of insecure registries can contain an element with CIDR notation to specify a whole subnet. +// If the subnet contains one of the IPs of the registry specified by indexName, the latter is considered +// insecure. +// +// indexName should be a URL.Host (`host:port` or `host`) where the `host` part can be either a domain name +// or an IP address. If it is a domain name, then it will be resolved in order to check if the IP is contained +// in a subnet. If the resolving is not successful, isSecureIndex will only try to match hostname to any element +// of insecureRegistries. +func isSecureIndex(config *serviceConfig, indexName string) bool { + // Check for configured index, first. This is needed in case isSecureIndex + // is called from anything besides newIndexInfo, in order to honor per-index configurations. + if index, ok := config.IndexConfigs[indexName]; ok { + return index.Secure + } + + host, _, err := net.SplitHostPort(indexName) + if err != nil { + // assume indexName is of the form `host` without the port and go on. + host = indexName + } + + addrs, err := lookupIP(host) + if err != nil { + ip := net.ParseIP(host) + if ip != nil { + addrs = []net.IP{ip} + } + + // if ip == nil, then `host` is neither an IP nor it could be looked up, + // either because the index is unreachable, or because the index is behind an HTTP proxy. + // So, len(addrs) == 0 and we're not aborting. + } + + // Try CIDR notation only if addrs has any elements, i.e. if `host`'s IP could be determined. + for _, addr := range addrs { + for _, ipnet := range config.InsecureRegistryCIDRs { + // check if the addr falls in the subnet + if (*net.IPNet)(ipnet).Contains(addr) { + return false + } + } + } + + return true +} + +// ValidateMirror validates an HTTP(S) registry mirror +func ValidateMirror(val string) (string, error) { + uri, err := url.Parse(val) + if err != nil { + return "", fmt.Errorf("%s is not a valid URI", val) + } + + if uri.Scheme != "http" && uri.Scheme != "https" { + return "", fmt.Errorf("Unsupported scheme %s", uri.Scheme) + } + + if uri.Path != "" || uri.RawQuery != "" || uri.Fragment != "" { + return "", fmt.Errorf("Unsupported path/query/fragment at end of the URI") + } + + return fmt.Sprintf("%s://%s/", uri.Scheme, uri.Host), nil +} + +// ValidateIndexName validates an index name. +func ValidateIndexName(val string) (string, error) { + if val == reference.LegacyDefaultHostname { + val = reference.DefaultHostname + } + if strings.HasPrefix(val, "-") || strings.HasSuffix(val, "-") { + return "", fmt.Errorf("Invalid index name (%s). Cannot begin or end with a hyphen.", val) + } + return val, nil +} + +func validateNoSchema(reposName string) error { + if strings.Contains(reposName, "://") { + // It cannot contain a scheme! + return ErrInvalidRepositoryName + } + return nil +} + +// newIndexInfo returns IndexInfo configuration from indexName +func newIndexInfo(config *serviceConfig, indexName string) (*registrytypes.IndexInfo, error) { + var err error + indexName, err = ValidateIndexName(indexName) + if err != nil { + return nil, err + } + + // Return any configured index info, first. + if index, ok := config.IndexConfigs[indexName]; ok { + return index, nil + } + + // Construct a non-configured index info. + index := ®istrytypes.IndexInfo{ + Name: indexName, + Mirrors: make([]string, 0), + Official: false, + } + index.Secure = isSecureIndex(config, indexName) + return index, nil +} + +// GetAuthConfigKey special-cases using the full index address of the official +// index as the AuthConfig key, and uses the (host)name[:port] for private indexes. +func GetAuthConfigKey(index *registrytypes.IndexInfo) string { + if index.Official { + return IndexServer + } + return index.Name +} + +// newRepositoryInfo validates and breaks down a repository name into a RepositoryInfo +func newRepositoryInfo(config *serviceConfig, name reference.Named) (*RepositoryInfo, error) { + index, err := newIndexInfo(config, name.Hostname()) + if err != nil { + return nil, err + } + official := !strings.ContainsRune(name.Name(), '/') + return &RepositoryInfo{name, index, official}, nil +} + +// ParseRepositoryInfo performs the breakdown of a repository name into a RepositoryInfo, but +// lacks registry configuration. +func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) { + return newRepositoryInfo(emptyServiceConfig, reposName) +} + +// ParseSearchIndexInfo will use repository name to get back an indexInfo. +func ParseSearchIndexInfo(reposName string) (*registrytypes.IndexInfo, error) { + indexName, _ := splitReposSearchTerm(reposName) + + indexInfo, err := newIndexInfo(emptyServiceConfig, indexName) + if err != nil { + return nil, err + } + return indexInfo, nil +} diff --git a/registry/config_test.go b/registry/config_test.go new file mode 100644 index 00000000..25578a7f --- /dev/null +++ b/registry/config_test.go @@ -0,0 +1,49 @@ +package registry + +import ( + "testing" +) + +func TestValidateMirror(t *testing.T) { + valid := []string{ + "http://mirror-1.com", + "https://mirror-1.com", + "http://localhost", + "https://localhost", + "http://localhost:5000", + "https://localhost:5000", + "http://127.0.0.1", + "https://127.0.0.1", + "http://127.0.0.1:5000", + "https://127.0.0.1:5000", + } + + invalid := []string{ + "!invalid!://%as%", + "ftp://mirror-1.com", + "http://mirror-1.com/", + "http://mirror-1.com/?q=foo", + "http://mirror-1.com/v1/", + "http://mirror-1.com/v1/?q=foo", + "http://mirror-1.com/v1/?q=foo#frag", + "http://mirror-1.com?q=foo", + "https://mirror-1.com#frag", + "https://mirror-1.com/", + "https://mirror-1.com/#frag", + "https://mirror-1.com/v1/", + "https://mirror-1.com/v1/#", + "https://mirror-1.com?q", + } + + for _, address := range valid { + if ret, err := ValidateMirror(address); err != nil || ret == "" { + t.Errorf("ValidateMirror(`"+address+"`) got %s %s", ret, err) + } + } + + for _, address := range invalid { + if ret, err := ValidateMirror(address); err == nil || ret != "" { + t.Errorf("ValidateMirror(`"+address+"`) got %s %s", ret, err) + } + } +} diff --git a/registry/config_unix.go b/registry/config_unix.go new file mode 100644 index 00000000..b81d2493 --- /dev/null +++ b/registry/config_unix.go @@ -0,0 +1,16 @@ +// +build !windows + +package registry + +var ( + // CertsDir is the directory where certificates are stored + CertsDir = "/etc/docker/certs.d" +) + +// cleanPath is used to ensure that a directory name is valid on the target +// platform. It will be passed in something *similar* to a URL such as +// https:/index.docker.io/v1. Not all platforms support directory names +// which contain those characters (such as : on Windows) +func cleanPath(s string) string { + return s +} diff --git a/registry/config_windows.go b/registry/config_windows.go new file mode 100644 index 00000000..82bc4afe --- /dev/null +++ b/registry/config_windows.go @@ -0,0 +1,18 @@ +package registry + +import ( + "os" + "path/filepath" + "strings" +) + +// CertsDir is the directory where certificates are stored +var CertsDir = os.Getenv("programdata") + `\docker\certs.d` + +// cleanPath is used to ensure that a directory name is valid on the target +// platform. It will be passed in something *similar* to a URL such as +// https:\index.docker.io\v1. Not all platforms support directory names +// which contain those characters (such as : on Windows) +func cleanPath(s string) string { + return filepath.FromSlash(strings.Replace(s, ":", "", -1)) +} diff --git a/registry/endpoint_test.go b/registry/endpoint_test.go new file mode 100644 index 00000000..8451d3f6 --- /dev/null +++ b/registry/endpoint_test.go @@ -0,0 +1,78 @@ +package registry + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" +) + +func TestEndpointParse(t *testing.T) { + testData := []struct { + str string + expected string + }{ + {IndexServer, IndexServer}, + {"http://0.0.0.0:5000/v1/", "http://0.0.0.0:5000/v1/"}, + {"http://0.0.0.0:5000", "http://0.0.0.0:5000/v1/"}, + {"0.0.0.0:5000", "https://0.0.0.0:5000/v1/"}, + {"http://0.0.0.0:5000/nonversion/", "http://0.0.0.0:5000/nonversion/v1/"}, + {"http://0.0.0.0:5000/v0/", "http://0.0.0.0:5000/v0/v1/"}, + } + for _, td := range testData { + e, err := newV1EndpointFromStr(td.str, nil, "", nil) + if err != nil { + t.Errorf("%q: %s", td.str, err) + } + if e == nil { + t.Logf("something's fishy, endpoint for %q is nil", td.str) + continue + } + if e.String() != td.expected { + t.Errorf("expected %q, got %q", td.expected, e.String()) + } + } +} + +func TestEndpointParseInvalid(t *testing.T) { + testData := []string{ + "http://0.0.0.0:5000/v2/", + } + for _, td := range testData { + e, err := newV1EndpointFromStr(td, nil, "", nil) + if err == nil { + t.Errorf("expected error parsing %q: parsed as %q", td, e) + } + } +} + +// Ensure that a registry endpoint that responds with a 401 only is determined +// to be a valid v1 registry endpoint +func TestValidateEndpoint(t *testing.T) { + requireBasicAuthHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("WWW-Authenticate", `Basic realm="localhost"`) + w.WriteHeader(http.StatusUnauthorized) + }) + + // Make a test server which should validate as a v1 server. + testServer := httptest.NewServer(requireBasicAuthHandler) + defer testServer.Close() + + testServerURL, err := url.Parse(testServer.URL) + if err != nil { + t.Fatal(err) + } + + testEndpoint := V1Endpoint{ + URL: testServerURL, + client: HTTPClient(NewTransport(nil)), + } + + if err = validateEndpoint(&testEndpoint); err != nil { + t.Fatal(err) + } + + if testEndpoint.URL.Scheme != "http" { + t.Fatalf("expecting to validate endpoint as http, got url %s", testEndpoint.String()) + } +} diff --git a/registry/endpoint_v1.go b/registry/endpoint_v1.go new file mode 100644 index 00000000..fd81972c --- /dev/null +++ b/registry/endpoint_v1.go @@ -0,0 +1,198 @@ +package registry + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/registry/client/transport" + registrytypes "github.com/docker/engine-api/types/registry" +) + +// V1Endpoint stores basic information about a V1 registry endpoint. +type V1Endpoint struct { + client *http.Client + URL *url.URL + IsSecure bool +} + +// NewV1Endpoint parses the given address to return a registry endpoint. +func NewV1Endpoint(index *registrytypes.IndexInfo, userAgent string, metaHeaders http.Header) (*V1Endpoint, error) { + tlsConfig, err := newTLSConfig(index.Name, index.Secure) + if err != nil { + return nil, err + } + + endpoint, err := newV1EndpointFromStr(GetAuthConfigKey(index), tlsConfig, userAgent, metaHeaders) + if err != nil { + return nil, err + } + + if err := validateEndpoint(endpoint); err != nil { + return nil, err + } + + return endpoint, nil +} + +func validateEndpoint(endpoint *V1Endpoint) error { + logrus.Debugf("pinging registry endpoint %s", endpoint) + + // Try HTTPS ping to registry + endpoint.URL.Scheme = "https" + if _, err := endpoint.Ping(); err != nil { + if endpoint.IsSecure { + // If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry` + // in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fallback to HTTP. + return fmt.Errorf("invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host) + } + + // If registry is insecure and HTTPS failed, fallback to HTTP. + logrus.Debugf("Error from registry %q marked as insecure: %v. Insecurely falling back to HTTP", endpoint, err) + endpoint.URL.Scheme = "http" + + var err2 error + if _, err2 = endpoint.Ping(); err2 == nil { + return nil + } + + return fmt.Errorf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2) + } + + return nil +} + +func newV1Endpoint(address url.URL, tlsConfig *tls.Config, userAgent string, metaHeaders http.Header) (*V1Endpoint, error) { + endpoint := &V1Endpoint{ + IsSecure: (tlsConfig == nil || !tlsConfig.InsecureSkipVerify), + URL: new(url.URL), + } + + *endpoint.URL = address + + // TODO(tiborvass): make sure a ConnectTimeout transport is used + tr := NewTransport(tlsConfig) + endpoint.client = HTTPClient(transport.NewTransport(tr, DockerHeaders(userAgent, metaHeaders)...)) + return endpoint, nil +} + +// trimV1Address trims the version off the address and returns the +// trimmed address or an error if there is a non-V1 version. +func trimV1Address(address string) (string, error) { + var ( + chunks []string + apiVersionStr string + ) + + if strings.HasSuffix(address, "/") { + address = address[:len(address)-1] + } + + chunks = strings.Split(address, "/") + apiVersionStr = chunks[len(chunks)-1] + if apiVersionStr == "v1" { + return strings.Join(chunks[:len(chunks)-1], "/"), nil + } + + for k, v := range apiVersions { + if k != APIVersion1 && apiVersionStr == v { + return "", fmt.Errorf("unsupported V1 version path %s", apiVersionStr) + } + } + + return address, nil +} + +func newV1EndpointFromStr(address string, tlsConfig *tls.Config, userAgent string, metaHeaders http.Header) (*V1Endpoint, error) { + if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") { + address = "https://" + address + } + + address, err := trimV1Address(address) + if err != nil { + return nil, err + } + + uri, err := url.Parse(address) + if err != nil { + return nil, err + } + + endpoint, err := newV1Endpoint(*uri, tlsConfig, userAgent, metaHeaders) + if err != nil { + return nil, err + } + + return endpoint, nil +} + +// Get the formatted URL for the root of this registry Endpoint +func (e *V1Endpoint) String() string { + return e.URL.String() + "/v1/" +} + +// Path returns a formatted string for the URL +// of this endpoint with the given path appended. +func (e *V1Endpoint) Path(path string) string { + return e.URL.String() + "/v1/" + path +} + +// Ping returns a PingResult which indicates whether the registry is standalone or not. +func (e *V1Endpoint) Ping() (PingResult, error) { + logrus.Debugf("attempting v1 ping for registry endpoint %s", e) + + if e.String() == IndexServer { + // Skip the check, we know this one is valid + // (and we never want to fallback to http in case of error) + return PingResult{Standalone: false}, nil + } + + req, err := http.NewRequest("GET", e.Path("_ping"), nil) + if err != nil { + return PingResult{Standalone: false}, err + } + + resp, err := e.client.Do(req) + if err != nil { + return PingResult{Standalone: false}, err + } + + defer resp.Body.Close() + + jsonString, err := ioutil.ReadAll(resp.Body) + if err != nil { + return PingResult{Standalone: false}, fmt.Errorf("error while reading the http response: %s", err) + } + + // If the header is absent, we assume true for compatibility with earlier + // versions of the registry. default to true + info := PingResult{ + Standalone: true, + } + if err := json.Unmarshal(jsonString, &info); err != nil { + logrus.Debugf("Error unmarshalling the _ping PingResult: %s", err) + // don't stop here. Just assume sane defaults + } + if hdr := resp.Header.Get("X-Docker-Registry-Version"); hdr != "" { + logrus.Debugf("Registry version header: '%s'", hdr) + info.Version = hdr + } + logrus.Debugf("PingResult.Version: %q", info.Version) + + standalone := resp.Header.Get("X-Docker-Registry-Standalone") + logrus.Debugf("Registry standalone header: '%s'", standalone) + // Accepted values are "true" (case-insensitive) and "1". + if strings.EqualFold(standalone, "true") || standalone == "1" { + info.Standalone = true + } else if len(standalone) > 0 { + // there is a header set, and it is not "true" or "1", so assume fails + info.Standalone = false + } + logrus.Debugf("PingResult.Standalone: %t", info.Standalone) + return info, nil +} diff --git a/registry/reference.go b/registry/reference.go new file mode 100644 index 00000000..e15f83ee --- /dev/null +++ b/registry/reference.go @@ -0,0 +1,68 @@ +package registry + +import ( + "strings" + + "github.com/docker/distribution/digest" +) + +// Reference represents a tag or digest within a repository +type Reference interface { + // HasDigest returns whether the reference has a verifiable + // content addressable reference which may be considered secure. + HasDigest() bool + + // ImageName returns an image name for the given repository + ImageName(string) string + + // Returns a string representation of the reference + String() string +} + +type tagReference struct { + tag string +} + +func (tr tagReference) HasDigest() bool { + return false +} + +func (tr tagReference) ImageName(repo string) string { + return repo + ":" + tr.tag +} + +func (tr tagReference) String() string { + return tr.tag +} + +type digestReference struct { + digest digest.Digest +} + +func (dr digestReference) HasDigest() bool { + return true +} + +func (dr digestReference) ImageName(repo string) string { + return repo + "@" + dr.String() +} + +func (dr digestReference) String() string { + return dr.digest.String() +} + +// ParseReference parses a reference into either a digest or tag reference +func ParseReference(ref string) Reference { + if strings.Contains(ref, ":") { + dgst, err := digest.ParseDigest(ref) + if err == nil { + return digestReference{digest: dgst} + } + } + return tagReference{tag: ref} +} + +// DigestReference creates a digest reference using a digest +func DigestReference(dgst digest.Digest) Reference { + return digestReference{digest: dgst} +} diff --git a/registry/registry.go b/registry/registry.go new file mode 100644 index 00000000..8fdfe3b0 --- /dev/null +++ b/registry/registry.go @@ -0,0 +1,180 @@ +// Package registry contains client primitives to interact with a remote Docker registry. +package registry + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "io/ioutil" + "net" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/registry/client/transport" + "github.com/docker/go-connections/tlsconfig" +) + +var ( + // ErrAlreadyExists is an error returned if an image being pushed + // already exists on the remote side + ErrAlreadyExists = errors.New("Image already exists") +) + +func newTLSConfig(hostname string, isSecure bool) (*tls.Config, error) { + // PreferredServerCipherSuites should have no effect + tlsConfig := tlsconfig.ServerDefault + + tlsConfig.InsecureSkipVerify = !isSecure + + if isSecure && CertsDir != "" { + hostDir := filepath.Join(CertsDir, cleanPath(hostname)) + logrus.Debugf("hostDir: %s", hostDir) + if err := ReadCertsDirectory(&tlsConfig, hostDir); err != nil { + return nil, err + } + } + + return &tlsConfig, nil +} + +func hasFile(files []os.FileInfo, name string) bool { + for _, f := range files { + if f.Name() == name { + return true + } + } + return false +} + +// ReadCertsDirectory reads the directory for TLS certificates +// including roots and certificate pairs and updates the +// provided TLS configuration. +func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error { + fs, err := ioutil.ReadDir(directory) + if err != nil && !os.IsNotExist(err) { + return err + } + + for _, f := range fs { + if strings.HasSuffix(f.Name(), ".crt") { + if tlsConfig.RootCAs == nil { + // TODO(dmcgowan): Copy system pool + tlsConfig.RootCAs = x509.NewCertPool() + } + logrus.Debugf("crt: %s", filepath.Join(directory, f.Name())) + data, err := ioutil.ReadFile(filepath.Join(directory, f.Name())) + if err != nil { + return err + } + tlsConfig.RootCAs.AppendCertsFromPEM(data) + } + if strings.HasSuffix(f.Name(), ".cert") { + certName := f.Name() + keyName := certName[:len(certName)-5] + ".key" + logrus.Debugf("cert: %s", filepath.Join(directory, f.Name())) + if !hasFile(fs, keyName) { + return fmt.Errorf("Missing key %s for client certificate %s. Note that CA certificates should use the extension .crt.", keyName, certName) + } + cert, err := tls.LoadX509KeyPair(filepath.Join(directory, certName), filepath.Join(directory, keyName)) + if err != nil { + return err + } + tlsConfig.Certificates = append(tlsConfig.Certificates, cert) + } + if strings.HasSuffix(f.Name(), ".key") { + keyName := f.Name() + certName := keyName[:len(keyName)-4] + ".cert" + logrus.Debugf("key: %s", filepath.Join(directory, f.Name())) + if !hasFile(fs, certName) { + return fmt.Errorf("Missing client certificate %s for key %s", certName, keyName) + } + } + } + + return nil +} + +// DockerHeaders returns request modifiers with a User-Agent and metaHeaders +func DockerHeaders(userAgent string, metaHeaders http.Header) []transport.RequestModifier { + modifiers := []transport.RequestModifier{} + if userAgent != "" { + modifiers = append(modifiers, transport.NewHeaderRequestModifier(http.Header{ + "User-Agent": []string{userAgent}, + })) + } + if metaHeaders != nil { + modifiers = append(modifiers, transport.NewHeaderRequestModifier(metaHeaders)) + } + return modifiers +} + +// HTTPClient returns a HTTP client structure which uses the given transport +// and contains the necessary headers for redirected requests +func HTTPClient(transport http.RoundTripper) *http.Client { + return &http.Client{ + Transport: transport, + CheckRedirect: addRequiredHeadersToRedirectedRequests, + } +} + +func trustedLocation(req *http.Request) bool { + var ( + trusteds = []string{"docker.com", "docker.io"} + hostname = strings.SplitN(req.Host, ":", 2)[0] + ) + if req.URL.Scheme != "https" { + return false + } + + for _, trusted := range trusteds { + if hostname == trusted || strings.HasSuffix(hostname, "."+trusted) { + return true + } + } + return false +} + +// addRequiredHeadersToRedirectedRequests adds the necessary redirection headers +// for redirected requests +func addRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Request) error { + if via != nil && via[0] != nil { + if trustedLocation(req) && trustedLocation(via[0]) { + req.Header = via[0].Header + return nil + } + for k, v := range via[0].Header { + if k != "Authorization" { + for _, vv := range v { + req.Header.Add(k, vv) + } + } + } + } + return nil +} + +// NewTransport returns a new HTTP transport. If tlsConfig is nil, it uses the +// default TLS configuration. +func NewTransport(tlsConfig *tls.Config) *http.Transport { + if tlsConfig == nil { + var cfg = tlsconfig.ServerDefault + tlsConfig = &cfg + } + return &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + TLSClientConfig: tlsConfig, + // TODO(dmcgowan): Call close idle connections when complete and use keep alive + DisableKeepAlives: true, + } +} diff --git a/registry/registry_mock_test.go b/registry/registry_mock_test.go new file mode 100644 index 00000000..828f48fc --- /dev/null +++ b/registry/registry_mock_test.go @@ -0,0 +1,476 @@ +package registry + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "strings" + "testing" + "time" + + "github.com/docker/docker/reference" + registrytypes "github.com/docker/engine-api/types/registry" + "github.com/gorilla/mux" + + "github.com/Sirupsen/logrus" +) + +var ( + testHTTPServer *httptest.Server + testHTTPSServer *httptest.Server + testLayers = map[string]map[string]string{ + "77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20": { + "json": `{"id":"77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20", + "comment":"test base image","created":"2013-03-23T12:53:11.10432-07:00", + "container_config":{"Hostname":"","User":"","Memory":0,"MemorySwap":0, + "CpuShares":0,"AttachStdin":false,"AttachStdout":false,"AttachStderr":false, + "Tty":false,"OpenStdin":false,"StdinOnce":false, + "Env":null,"Cmd":null,"Dns":null,"Image":"","Volumes":null, + "VolumesFrom":"","Entrypoint":null},"Size":424242}`, + "checksum_simple": "sha256:1ac330d56e05eef6d438586545ceff7550d3bdcb6b19961f12c5ba714ee1bb37", + "checksum_tarsum": "tarsum+sha256:4409a0685741ca86d38df878ed6f8cbba4c99de5dc73cd71aef04be3bb70be7c", + "ancestry": `["77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20"]`, + "layer": string([]byte{ + 0x1f, 0x8b, 0x08, 0x08, 0x0e, 0xb0, 0xee, 0x51, 0x02, 0x03, 0x6c, 0x61, 0x79, 0x65, + 0x72, 0x2e, 0x74, 0x61, 0x72, 0x00, 0xed, 0xd2, 0x31, 0x0e, 0xc2, 0x30, 0x0c, 0x05, + 0x50, 0xcf, 0x9c, 0xc2, 0x27, 0x48, 0xed, 0x38, 0x4e, 0xce, 0x13, 0x44, 0x2b, 0x66, + 0x62, 0x24, 0x8e, 0x4f, 0xa0, 0x15, 0x63, 0xb6, 0x20, 0x21, 0xfc, 0x96, 0xbf, 0x78, + 0xb0, 0xf5, 0x1d, 0x16, 0x98, 0x8e, 0x88, 0x8a, 0x2a, 0xbe, 0x33, 0xef, 0x49, 0x31, + 0xed, 0x79, 0x40, 0x8e, 0x5c, 0x44, 0x85, 0x88, 0x33, 0x12, 0x73, 0x2c, 0x02, 0xa8, + 0xf0, 0x05, 0xf7, 0x66, 0xf5, 0xd6, 0x57, 0x69, 0xd7, 0x7a, 0x19, 0xcd, 0xf5, 0xb1, + 0x6d, 0x1b, 0x1f, 0xf9, 0xba, 0xe3, 0x93, 0x3f, 0x22, 0x2c, 0xb6, 0x36, 0x0b, 0xf6, + 0xb0, 0xa9, 0xfd, 0xe7, 0x94, 0x46, 0xfd, 0xeb, 0xd1, 0x7f, 0x2c, 0xc4, 0xd2, 0xfb, + 0x97, 0xfe, 0x02, 0x80, 0xe4, 0xfd, 0x4f, 0x77, 0xae, 0x6d, 0x3d, 0x81, 0x73, 0xce, + 0xb9, 0x7f, 0xf3, 0x04, 0x41, 0xc1, 0xab, 0xc6, 0x00, 0x0a, 0x00, 0x00, + }), + }, + "42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d": { + "json": `{"id":"42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d", + "parent":"77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20", + "comment":"test base image","created":"2013-03-23T12:55:11.10432-07:00", + "container_config":{"Hostname":"","User":"","Memory":0,"MemorySwap":0, + "CpuShares":0,"AttachStdin":false,"AttachStdout":false,"AttachStderr":false, + "Tty":false,"OpenStdin":false,"StdinOnce":false, + "Env":null,"Cmd":null,"Dns":null,"Image":"","Volumes":null, + "VolumesFrom":"","Entrypoint":null},"Size":424242}`, + "checksum_simple": "sha256:bea7bf2e4bacd479344b737328db47b18880d09096e6674165533aa994f5e9f2", + "checksum_tarsum": "tarsum+sha256:68fdb56fb364f074eec2c9b3f85ca175329c4dcabc4a6a452b7272aa613a07a2", + "ancestry": `["42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d", + "77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20"]`, + "layer": string([]byte{ + 0x1f, 0x8b, 0x08, 0x08, 0xbd, 0xb3, 0xee, 0x51, 0x02, 0x03, 0x6c, 0x61, 0x79, 0x65, + 0x72, 0x2e, 0x74, 0x61, 0x72, 0x00, 0xed, 0xd1, 0x31, 0x0e, 0xc2, 0x30, 0x0c, 0x05, + 0x50, 0xcf, 0x9c, 0xc2, 0x27, 0x48, 0x9d, 0x38, 0x8e, 0xcf, 0x53, 0x51, 0xaa, 0x56, + 0xea, 0x44, 0x82, 0xc4, 0xf1, 0x09, 0xb4, 0xea, 0x98, 0x2d, 0x48, 0x08, 0xbf, 0xe5, + 0x2f, 0x1e, 0xfc, 0xf5, 0xdd, 0x00, 0xdd, 0x11, 0x91, 0x8a, 0xe0, 0x27, 0xd3, 0x9e, + 0x14, 0xe2, 0x9e, 0x07, 0xf4, 0xc1, 0x2b, 0x0b, 0xfb, 0xa4, 0x82, 0xe4, 0x3d, 0x93, + 0x02, 0x0a, 0x7c, 0xc1, 0x23, 0x97, 0xf1, 0x5e, 0x5f, 0xc9, 0xcb, 0x38, 0xb5, 0xee, + 0xea, 0xd9, 0x3c, 0xb7, 0x4b, 0xbe, 0x7b, 0x9c, 0xf9, 0x23, 0xdc, 0x50, 0x6e, 0xb9, + 0xb8, 0xf2, 0x2c, 0x5d, 0xf7, 0x4f, 0x31, 0xb6, 0xf6, 0x4f, 0xc7, 0xfe, 0x41, 0x55, + 0x63, 0xdd, 0x9f, 0x89, 0x09, 0x90, 0x6c, 0xff, 0xee, 0xae, 0xcb, 0xba, 0x4d, 0x17, + 0x30, 0xc6, 0x18, 0xf3, 0x67, 0x5e, 0xc1, 0xed, 0x21, 0x5d, 0x00, 0x0a, 0x00, 0x00, + }), + }, + } + testRepositories = map[string]map[string]string{ + "foo42/bar": { + "latest": "42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d", + "test": "42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d", + }, + } + mockHosts = map[string][]net.IP{ + "": {net.ParseIP("0.0.0.0")}, + "localhost": {net.ParseIP("127.0.0.1"), net.ParseIP("::1")}, + "example.com": {net.ParseIP("42.42.42.42")}, + "other.com": {net.ParseIP("43.43.43.43")}, + } +) + +func init() { + r := mux.NewRouter() + + // /v1/ + r.HandleFunc("/v1/_ping", handlerGetPing).Methods("GET") + r.HandleFunc("/v1/images/{image_id:[^/]+}/{action:json|layer|ancestry}", handlerGetImage).Methods("GET") + r.HandleFunc("/v1/images/{image_id:[^/]+}/{action:json|layer|checksum}", handlerPutImage).Methods("PUT") + r.HandleFunc("/v1/repositories/{repository:.+}/tags", handlerGetDeleteTags).Methods("GET", "DELETE") + r.HandleFunc("/v1/repositories/{repository:.+}/tags/{tag:.+}", handlerGetTag).Methods("GET") + r.HandleFunc("/v1/repositories/{repository:.+}/tags/{tag:.+}", handlerPutTag).Methods("PUT") + r.HandleFunc("/v1/users{null:.*}", handlerUsers).Methods("GET", "POST", "PUT") + r.HandleFunc("/v1/repositories/{repository:.+}{action:/images|/}", handlerImages).Methods("GET", "PUT", "DELETE") + r.HandleFunc("/v1/repositories/{repository:.+}/auth", handlerAuth).Methods("PUT") + r.HandleFunc("/v1/search", handlerSearch).Methods("GET") + + // /v2/ + r.HandleFunc("/v2/version", handlerGetPing).Methods("GET") + + testHTTPServer = httptest.NewServer(handlerAccessLog(r)) + testHTTPSServer = httptest.NewTLSServer(handlerAccessLog(r)) + + // override net.LookupIP + lookupIP = func(host string) ([]net.IP, error) { + if host == "127.0.0.1" { + // I believe in future Go versions this will fail, so let's fix it later + return net.LookupIP(host) + } + for h, addrs := range mockHosts { + if host == h { + return addrs, nil + } + for _, addr := range addrs { + if addr.String() == host { + return []net.IP{addr}, nil + } + } + } + return nil, errors.New("lookup: no such host") + } +} + +func handlerAccessLog(handler http.Handler) http.Handler { + logHandler := func(w http.ResponseWriter, r *http.Request) { + logrus.Debugf("%s \"%s %s\"", r.RemoteAddr, r.Method, r.URL) + handler.ServeHTTP(w, r) + } + return http.HandlerFunc(logHandler) +} + +func makeURL(req string) string { + return testHTTPServer.URL + req +} + +func makeHTTPSURL(req string) string { + return testHTTPSServer.URL + req +} + +func makeIndex(req string) *registrytypes.IndexInfo { + index := ®istrytypes.IndexInfo{ + Name: makeURL(req), + } + return index +} + +func makeHTTPSIndex(req string) *registrytypes.IndexInfo { + index := ®istrytypes.IndexInfo{ + Name: makeHTTPSURL(req), + } + return index +} + +func makePublicIndex() *registrytypes.IndexInfo { + index := ®istrytypes.IndexInfo{ + Name: IndexServer, + Secure: true, + Official: true, + } + return index +} + +func makeServiceConfig(mirrors []string, insecureRegistries []string) *serviceConfig { + options := ServiceOptions{ + Mirrors: mirrors, + InsecureRegistries: insecureRegistries, + } + + return newServiceConfig(options) +} + +func writeHeaders(w http.ResponseWriter) { + h := w.Header() + h.Add("Server", "docker-tests/mock") + h.Add("Expires", "-1") + h.Add("Content-Type", "application/json") + h.Add("Pragma", "no-cache") + h.Add("Cache-Control", "no-cache") + h.Add("X-Docker-Registry-Version", "0.0.0") + h.Add("X-Docker-Registry-Config", "mock") +} + +func writeResponse(w http.ResponseWriter, message interface{}, code int) { + writeHeaders(w) + w.WriteHeader(code) + body, err := json.Marshal(message) + if err != nil { + io.WriteString(w, err.Error()) + return + } + w.Write(body) +} + +func readJSON(r *http.Request, dest interface{}) error { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return err + } + return json.Unmarshal(body, dest) +} + +func apiError(w http.ResponseWriter, message string, code int) { + body := map[string]string{ + "error": message, + } + writeResponse(w, body, code) +} + +func assertEqual(t *testing.T, a interface{}, b interface{}, message string) { + if a == b { + return + } + if len(message) == 0 { + message = fmt.Sprintf("%v != %v", a, b) + } + t.Fatal(message) +} + +func assertNotEqual(t *testing.T, a interface{}, b interface{}, message string) { + if a != b { + return + } + if len(message) == 0 { + message = fmt.Sprintf("%v == %v", a, b) + } + t.Fatal(message) +} + +// Similar to assertEqual, but does not stop test +func checkEqual(t *testing.T, a interface{}, b interface{}, messagePrefix string) { + if a == b { + return + } + message := fmt.Sprintf("%v != %v", a, b) + if len(messagePrefix) != 0 { + message = messagePrefix + ": " + message + } + t.Error(message) +} + +// Similar to assertNotEqual, but does not stop test +func checkNotEqual(t *testing.T, a interface{}, b interface{}, messagePrefix string) { + if a != b { + return + } + message := fmt.Sprintf("%v == %v", a, b) + if len(messagePrefix) != 0 { + message = messagePrefix + ": " + message + } + t.Error(message) +} + +func requiresAuth(w http.ResponseWriter, r *http.Request) bool { + writeCookie := func() { + value := fmt.Sprintf("FAKE-SESSION-%d", time.Now().UnixNano()) + cookie := &http.Cookie{Name: "session", Value: value, MaxAge: 3600} + http.SetCookie(w, cookie) + //FIXME(sam): this should be sent only on Index routes + value = fmt.Sprintf("FAKE-TOKEN-%d", time.Now().UnixNano()) + w.Header().Add("X-Docker-Token", value) + } + if len(r.Cookies()) > 0 { + writeCookie() + return true + } + if len(r.Header.Get("Authorization")) > 0 { + writeCookie() + return true + } + w.Header().Add("WWW-Authenticate", "token") + apiError(w, "Wrong auth", 401) + return false +} + +func handlerGetPing(w http.ResponseWriter, r *http.Request) { + writeResponse(w, true, 200) +} + +func handlerGetImage(w http.ResponseWriter, r *http.Request) { + if !requiresAuth(w, r) { + return + } + vars := mux.Vars(r) + layer, exists := testLayers[vars["image_id"]] + if !exists { + http.NotFound(w, r) + return + } + writeHeaders(w) + layerSize := len(layer["layer"]) + w.Header().Add("X-Docker-Size", strconv.Itoa(layerSize)) + io.WriteString(w, layer[vars["action"]]) +} + +func handlerPutImage(w http.ResponseWriter, r *http.Request) { + if !requiresAuth(w, r) { + return + } + vars := mux.Vars(r) + imageID := vars["image_id"] + action := vars["action"] + layer, exists := testLayers[imageID] + if !exists { + if action != "json" { + http.NotFound(w, r) + return + } + layer = make(map[string]string) + testLayers[imageID] = layer + } + if checksum := r.Header.Get("X-Docker-Checksum"); checksum != "" { + if checksum != layer["checksum_simple"] && checksum != layer["checksum_tarsum"] { + apiError(w, "Wrong checksum", 400) + return + } + } + body, err := ioutil.ReadAll(r.Body) + if err != nil { + apiError(w, fmt.Sprintf("Error: %s", err), 500) + return + } + layer[action] = string(body) + writeResponse(w, true, 200) +} + +func handlerGetDeleteTags(w http.ResponseWriter, r *http.Request) { + if !requiresAuth(w, r) { + return + } + repositoryName, err := reference.WithName(mux.Vars(r)["repository"]) + if err != nil { + apiError(w, "Could not parse repository", 400) + return + } + tags, exists := testRepositories[repositoryName.String()] + if !exists { + apiError(w, "Repository not found", 404) + return + } + if r.Method == "DELETE" { + delete(testRepositories, repositoryName.String()) + writeResponse(w, true, 200) + return + } + writeResponse(w, tags, 200) +} + +func handlerGetTag(w http.ResponseWriter, r *http.Request) { + if !requiresAuth(w, r) { + return + } + vars := mux.Vars(r) + repositoryName, err := reference.WithName(vars["repository"]) + if err != nil { + apiError(w, "Could not parse repository", 400) + return + } + tagName := vars["tag"] + tags, exists := testRepositories[repositoryName.String()] + if !exists { + apiError(w, "Repository not found", 404) + return + } + tag, exists := tags[tagName] + if !exists { + apiError(w, "Tag not found", 404) + return + } + writeResponse(w, tag, 200) +} + +func handlerPutTag(w http.ResponseWriter, r *http.Request) { + if !requiresAuth(w, r) { + return + } + vars := mux.Vars(r) + repositoryName, err := reference.WithName(vars["repository"]) + if err != nil { + apiError(w, "Could not parse repository", 400) + return + } + tagName := vars["tag"] + tags, exists := testRepositories[repositoryName.String()] + if !exists { + tags = make(map[string]string) + testRepositories[repositoryName.String()] = tags + } + tagValue := "" + readJSON(r, tagValue) + tags[tagName] = tagValue + writeResponse(w, true, 200) +} + +func handlerUsers(w http.ResponseWriter, r *http.Request) { + code := 200 + if r.Method == "POST" { + code = 201 + } else if r.Method == "PUT" { + code = 204 + } + writeResponse(w, "", code) +} + +func handlerImages(w http.ResponseWriter, r *http.Request) { + u, _ := url.Parse(testHTTPServer.URL) + w.Header().Add("X-Docker-Endpoints", fmt.Sprintf("%s , %s ", u.Host, "test.example.com")) + w.Header().Add("X-Docker-Token", fmt.Sprintf("FAKE-SESSION-%d", time.Now().UnixNano())) + if r.Method == "PUT" { + if strings.HasSuffix(r.URL.Path, "images") { + writeResponse(w, "", 204) + return + } + writeResponse(w, "", 200) + return + } + if r.Method == "DELETE" { + writeResponse(w, "", 204) + return + } + images := []map[string]string{} + for imageID, layer := range testLayers { + image := make(map[string]string) + image["id"] = imageID + image["checksum"] = layer["checksum_tarsum"] + image["Tag"] = "latest" + images = append(images, image) + } + writeResponse(w, images, 200) +} + +func handlerAuth(w http.ResponseWriter, r *http.Request) { + writeResponse(w, "OK", 200) +} + +func handlerSearch(w http.ResponseWriter, r *http.Request) { + result := ®istrytypes.SearchResults{ + Query: "fakequery", + NumResults: 1, + Results: []registrytypes.SearchResult{{Name: "fakeimage", StarCount: 42}}, + } + writeResponse(w, result, 200) +} + +func TestPing(t *testing.T) { + res, err := http.Get(makeURL("/v1/_ping")) + if err != nil { + t.Fatal(err) + } + assertEqual(t, res.StatusCode, 200, "") + assertEqual(t, res.Header.Get("X-Docker-Registry-Config"), "mock", + "This is not a Mocked Registry") +} + +/* Uncomment this to test Mocked Registry locally with curl + * WARNING: Don't push on the repos uncommented, it'll block the tests + * +func TestWait(t *testing.T) { + logrus.Println("Test HTTP server ready and waiting:", testHTTPServer.URL) + c := make(chan int) + <-c +} + +//*/ diff --git a/registry/registry_test.go b/registry/registry_test.go new file mode 100644 index 00000000..7442ebc0 --- /dev/null +++ b/registry/registry_test.go @@ -0,0 +1,873 @@ +package registry + +import ( + "fmt" + "net/http" + "net/http/httputil" + "net/url" + "strings" + "testing" + + "github.com/docker/distribution/registry/client/transport" + "github.com/docker/docker/reference" + "github.com/docker/engine-api/types" + registrytypes "github.com/docker/engine-api/types/registry" +) + +var ( + token = []string{"fake-token"} +) + +const ( + imageID = "42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d" + REPO = "foo42/bar" +) + +func spawnTestRegistrySession(t *testing.T) *Session { + authConfig := &types.AuthConfig{} + endpoint, err := NewV1Endpoint(makeIndex("/v1/"), "", nil) + if err != nil { + t.Fatal(err) + } + userAgent := "docker test client" + var tr http.RoundTripper = debugTransport{NewTransport(nil), t.Log} + tr = transport.NewTransport(AuthTransport(tr, authConfig, false), DockerHeaders(userAgent, nil)...) + client := HTTPClient(tr) + r, err := NewSession(client, authConfig, endpoint) + if err != nil { + t.Fatal(err) + } + // In a normal scenario for the v1 registry, the client should send a `X-Docker-Token: true` + // header while authenticating, in order to retrieve a token that can be later used to + // perform authenticated actions. + // + // The mock v1 registry does not support that, (TODO(tiborvass): support it), instead, + // it will consider authenticated any request with the header `X-Docker-Token: fake-token`. + // + // Because we know that the client's transport is an `*authTransport` we simply cast it, + // in order to set the internal cached token to the fake token, and thus send that fake token + // upon every subsequent requests. + r.client.Transport.(*authTransport).token = token + return r +} + +func TestPingRegistryEndpoint(t *testing.T) { + testPing := func(index *registrytypes.IndexInfo, expectedStandalone bool, assertMessage string) { + ep, err := NewV1Endpoint(index, "", nil) + if err != nil { + t.Fatal(err) + } + regInfo, err := ep.Ping() + if err != nil { + t.Fatal(err) + } + + assertEqual(t, regInfo.Standalone, expectedStandalone, assertMessage) + } + + testPing(makeIndex("/v1/"), true, "Expected standalone to be true (default)") + testPing(makeHTTPSIndex("/v1/"), true, "Expected standalone to be true (default)") + testPing(makePublicIndex(), false, "Expected standalone to be false for public index") +} + +func TestEndpoint(t *testing.T) { + // Simple wrapper to fail test if err != nil + expandEndpoint := func(index *registrytypes.IndexInfo) *V1Endpoint { + endpoint, err := NewV1Endpoint(index, "", nil) + if err != nil { + t.Fatal(err) + } + return endpoint + } + + assertInsecureIndex := func(index *registrytypes.IndexInfo) { + index.Secure = true + _, err := NewV1Endpoint(index, "", nil) + assertNotEqual(t, err, nil, index.Name+": Expected error for insecure index") + assertEqual(t, strings.Contains(err.Error(), "insecure-registry"), true, index.Name+": Expected insecure-registry error for insecure index") + index.Secure = false + } + + assertSecureIndex := func(index *registrytypes.IndexInfo) { + index.Secure = true + _, err := NewV1Endpoint(index, "", nil) + assertNotEqual(t, err, nil, index.Name+": Expected cert error for secure index") + assertEqual(t, strings.Contains(err.Error(), "certificate signed by unknown authority"), true, index.Name+": Expected cert error for secure index") + index.Secure = false + } + + index := ®istrytypes.IndexInfo{} + index.Name = makeURL("/v1/") + endpoint := expandEndpoint(index) + assertEqual(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name) + assertInsecureIndex(index) + + index.Name = makeURL("") + endpoint = expandEndpoint(index) + assertEqual(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/") + assertInsecureIndex(index) + + httpURL := makeURL("") + index.Name = strings.SplitN(httpURL, "://", 2)[1] + endpoint = expandEndpoint(index) + assertEqual(t, endpoint.String(), httpURL+"/v1/", index.Name+": Expected endpoint to be "+httpURL+"/v1/") + assertInsecureIndex(index) + + index.Name = makeHTTPSURL("/v1/") + endpoint = expandEndpoint(index) + assertEqual(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name) + assertSecureIndex(index) + + index.Name = makeHTTPSURL("") + endpoint = expandEndpoint(index) + assertEqual(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/") + assertSecureIndex(index) + + httpsURL := makeHTTPSURL("") + index.Name = strings.SplitN(httpsURL, "://", 2)[1] + endpoint = expandEndpoint(index) + assertEqual(t, endpoint.String(), httpsURL+"/v1/", index.Name+": Expected endpoint to be "+httpsURL+"/v1/") + assertSecureIndex(index) + + badEndpoints := []string{ + "http://127.0.0.1/v1/", + "https://127.0.0.1/v1/", + "http://127.0.0.1", + "https://127.0.0.1", + "127.0.0.1", + } + for _, address := range badEndpoints { + index.Name = address + _, err := NewV1Endpoint(index, "", nil) + checkNotEqual(t, err, nil, "Expected error while expanding bad endpoint") + } +} + +func TestGetRemoteHistory(t *testing.T) { + r := spawnTestRegistrySession(t) + hist, err := r.GetRemoteHistory(imageID, makeURL("/v1/")) + if err != nil { + t.Fatal(err) + } + assertEqual(t, len(hist), 2, "Expected 2 images in history") + assertEqual(t, hist[0], imageID, "Expected "+imageID+"as first ancestry") + assertEqual(t, hist[1], "77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20", + "Unexpected second ancestry") +} + +func TestLookupRemoteImage(t *testing.T) { + r := spawnTestRegistrySession(t) + err := r.LookupRemoteImage(imageID, makeURL("/v1/")) + assertEqual(t, err, nil, "Expected error of remote lookup to nil") + if err := r.LookupRemoteImage("abcdef", makeURL("/v1/")); err == nil { + t.Fatal("Expected error of remote lookup to not nil") + } +} + +func TestGetRemoteImageJSON(t *testing.T) { + r := spawnTestRegistrySession(t) + json, size, err := r.GetRemoteImageJSON(imageID, makeURL("/v1/")) + if err != nil { + t.Fatal(err) + } + assertEqual(t, size, int64(154), "Expected size 154") + if len(json) == 0 { + t.Fatal("Expected non-empty json") + } + + _, _, err = r.GetRemoteImageJSON("abcdef", makeURL("/v1/")) + if err == nil { + t.Fatal("Expected image not found error") + } +} + +func TestGetRemoteImageLayer(t *testing.T) { + r := spawnTestRegistrySession(t) + data, err := r.GetRemoteImageLayer(imageID, makeURL("/v1/"), 0) + if err != nil { + t.Fatal(err) + } + if data == nil { + t.Fatal("Expected non-nil data result") + } + + _, err = r.GetRemoteImageLayer("abcdef", makeURL("/v1/"), 0) + if err == nil { + t.Fatal("Expected image not found error") + } +} + +func TestGetRemoteTag(t *testing.T) { + r := spawnTestRegistrySession(t) + repoRef, err := reference.ParseNamed(REPO) + if err != nil { + t.Fatal(err) + } + tag, err := r.GetRemoteTag([]string{makeURL("/v1/")}, repoRef, "test") + if err != nil { + t.Fatal(err) + } + assertEqual(t, tag, imageID, "Expected tag test to map to "+imageID) + + bazRef, err := reference.ParseNamed("foo42/baz") + if err != nil { + t.Fatal(err) + } + _, err = r.GetRemoteTag([]string{makeURL("/v1/")}, bazRef, "foo") + if err != ErrRepoNotFound { + t.Fatal("Expected ErrRepoNotFound error when fetching tag for bogus repo") + } +} + +func TestGetRemoteTags(t *testing.T) { + r := spawnTestRegistrySession(t) + repoRef, err := reference.ParseNamed(REPO) + if err != nil { + t.Fatal(err) + } + tags, err := r.GetRemoteTags([]string{makeURL("/v1/")}, repoRef) + if err != nil { + t.Fatal(err) + } + assertEqual(t, len(tags), 2, "Expected two tags") + assertEqual(t, tags["latest"], imageID, "Expected tag latest to map to "+imageID) + assertEqual(t, tags["test"], imageID, "Expected tag test to map to "+imageID) + + bazRef, err := reference.ParseNamed("foo42/baz") + if err != nil { + t.Fatal(err) + } + _, err = r.GetRemoteTags([]string{makeURL("/v1/")}, bazRef) + if err != ErrRepoNotFound { + t.Fatal("Expected ErrRepoNotFound error when fetching tags for bogus repo") + } +} + +func TestGetRepositoryData(t *testing.T) { + r := spawnTestRegistrySession(t) + parsedURL, err := url.Parse(makeURL("/v1/")) + if err != nil { + t.Fatal(err) + } + host := "http://" + parsedURL.Host + "/v1/" + repoRef, err := reference.ParseNamed(REPO) + if err != nil { + t.Fatal(err) + } + data, err := r.GetRepositoryData(repoRef) + if err != nil { + t.Fatal(err) + } + assertEqual(t, len(data.ImgList), 2, "Expected 2 images in ImgList") + assertEqual(t, len(data.Endpoints), 2, + fmt.Sprintf("Expected 2 endpoints in Endpoints, found %d instead", len(data.Endpoints))) + assertEqual(t, data.Endpoints[0], host, + fmt.Sprintf("Expected first endpoint to be %s but found %s instead", host, data.Endpoints[0])) + assertEqual(t, data.Endpoints[1], "http://test.example.com/v1/", + fmt.Sprintf("Expected first endpoint to be http://test.example.com/v1/ but found %s instead", data.Endpoints[1])) + +} + +func TestPushImageJSONRegistry(t *testing.T) { + r := spawnTestRegistrySession(t) + imgData := &ImgData{ + ID: "77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20", + Checksum: "sha256:1ac330d56e05eef6d438586545ceff7550d3bdcb6b19961f12c5ba714ee1bb37", + } + + err := r.PushImageJSONRegistry(imgData, []byte{0x42, 0xdf, 0x0}, makeURL("/v1/")) + if err != nil { + t.Fatal(err) + } +} + +func TestPushImageLayerRegistry(t *testing.T) { + r := spawnTestRegistrySession(t) + layer := strings.NewReader("") + _, _, err := r.PushImageLayerRegistry(imageID, layer, makeURL("/v1/"), []byte{}) + if err != nil { + t.Fatal(err) + } +} + +func TestParseRepositoryInfo(t *testing.T) { + type staticRepositoryInfo struct { + Index *registrytypes.IndexInfo + RemoteName string + CanonicalName string + LocalName string + Official bool + } + + expectedRepoInfos := map[string]staticRepositoryInfo{ + "fooo/bar": { + Index: ®istrytypes.IndexInfo{ + Name: IndexName, + Official: true, + }, + RemoteName: "fooo/bar", + LocalName: "fooo/bar", + CanonicalName: "docker.io/fooo/bar", + Official: false, + }, + "library/ubuntu": { + Index: ®istrytypes.IndexInfo{ + Name: IndexName, + Official: true, + }, + RemoteName: "library/ubuntu", + LocalName: "ubuntu", + CanonicalName: "docker.io/library/ubuntu", + Official: true, + }, + "nonlibrary/ubuntu": { + Index: ®istrytypes.IndexInfo{ + Name: IndexName, + Official: true, + }, + RemoteName: "nonlibrary/ubuntu", + LocalName: "nonlibrary/ubuntu", + CanonicalName: "docker.io/nonlibrary/ubuntu", + Official: false, + }, + "ubuntu": { + Index: ®istrytypes.IndexInfo{ + Name: IndexName, + Official: true, + }, + RemoteName: "library/ubuntu", + LocalName: "ubuntu", + CanonicalName: "docker.io/library/ubuntu", + Official: true, + }, + "other/library": { + Index: ®istrytypes.IndexInfo{ + Name: IndexName, + Official: true, + }, + RemoteName: "other/library", + LocalName: "other/library", + CanonicalName: "docker.io/other/library", + Official: false, + }, + "127.0.0.1:8000/private/moonbase": { + Index: ®istrytypes.IndexInfo{ + Name: "127.0.0.1:8000", + Official: false, + }, + RemoteName: "private/moonbase", + LocalName: "127.0.0.1:8000/private/moonbase", + CanonicalName: "127.0.0.1:8000/private/moonbase", + Official: false, + }, + "127.0.0.1:8000/privatebase": { + Index: ®istrytypes.IndexInfo{ + Name: "127.0.0.1:8000", + Official: false, + }, + RemoteName: "privatebase", + LocalName: "127.0.0.1:8000/privatebase", + CanonicalName: "127.0.0.1:8000/privatebase", + Official: false, + }, + "localhost:8000/private/moonbase": { + Index: ®istrytypes.IndexInfo{ + Name: "localhost:8000", + Official: false, + }, + RemoteName: "private/moonbase", + LocalName: "localhost:8000/private/moonbase", + CanonicalName: "localhost:8000/private/moonbase", + Official: false, + }, + "localhost:8000/privatebase": { + Index: ®istrytypes.IndexInfo{ + Name: "localhost:8000", + Official: false, + }, + RemoteName: "privatebase", + LocalName: "localhost:8000/privatebase", + CanonicalName: "localhost:8000/privatebase", + Official: false, + }, + "example.com/private/moonbase": { + Index: ®istrytypes.IndexInfo{ + Name: "example.com", + Official: false, + }, + RemoteName: "private/moonbase", + LocalName: "example.com/private/moonbase", + CanonicalName: "example.com/private/moonbase", + Official: false, + }, + "example.com/privatebase": { + Index: ®istrytypes.IndexInfo{ + Name: "example.com", + Official: false, + }, + RemoteName: "privatebase", + LocalName: "example.com/privatebase", + CanonicalName: "example.com/privatebase", + Official: false, + }, + "example.com:8000/private/moonbase": { + Index: ®istrytypes.IndexInfo{ + Name: "example.com:8000", + Official: false, + }, + RemoteName: "private/moonbase", + LocalName: "example.com:8000/private/moonbase", + CanonicalName: "example.com:8000/private/moonbase", + Official: false, + }, + "example.com:8000/privatebase": { + Index: ®istrytypes.IndexInfo{ + Name: "example.com:8000", + Official: false, + }, + RemoteName: "privatebase", + LocalName: "example.com:8000/privatebase", + CanonicalName: "example.com:8000/privatebase", + Official: false, + }, + "localhost/private/moonbase": { + Index: ®istrytypes.IndexInfo{ + Name: "localhost", + Official: false, + }, + RemoteName: "private/moonbase", + LocalName: "localhost/private/moonbase", + CanonicalName: "localhost/private/moonbase", + Official: false, + }, + "localhost/privatebase": { + Index: ®istrytypes.IndexInfo{ + Name: "localhost", + Official: false, + }, + RemoteName: "privatebase", + LocalName: "localhost/privatebase", + CanonicalName: "localhost/privatebase", + Official: false, + }, + IndexName + "/public/moonbase": { + Index: ®istrytypes.IndexInfo{ + Name: IndexName, + Official: true, + }, + RemoteName: "public/moonbase", + LocalName: "public/moonbase", + CanonicalName: "docker.io/public/moonbase", + Official: false, + }, + "index." + IndexName + "/public/moonbase": { + Index: ®istrytypes.IndexInfo{ + Name: IndexName, + Official: true, + }, + RemoteName: "public/moonbase", + LocalName: "public/moonbase", + CanonicalName: "docker.io/public/moonbase", + Official: false, + }, + "ubuntu-12.04-base": { + Index: ®istrytypes.IndexInfo{ + Name: IndexName, + Official: true, + }, + RemoteName: "library/ubuntu-12.04-base", + LocalName: "ubuntu-12.04-base", + CanonicalName: "docker.io/library/ubuntu-12.04-base", + Official: true, + }, + IndexName + "/ubuntu-12.04-base": { + Index: ®istrytypes.IndexInfo{ + Name: IndexName, + Official: true, + }, + RemoteName: "library/ubuntu-12.04-base", + LocalName: "ubuntu-12.04-base", + CanonicalName: "docker.io/library/ubuntu-12.04-base", + Official: true, + }, + "index." + IndexName + "/ubuntu-12.04-base": { + Index: ®istrytypes.IndexInfo{ + Name: IndexName, + Official: true, + }, + RemoteName: "library/ubuntu-12.04-base", + LocalName: "ubuntu-12.04-base", + CanonicalName: "docker.io/library/ubuntu-12.04-base", + Official: true, + }, + } + + for reposName, expectedRepoInfo := range expectedRepoInfos { + named, err := reference.WithName(reposName) + if err != nil { + t.Error(err) + } + + repoInfo, err := ParseRepositoryInfo(named) + if err != nil { + t.Error(err) + } else { + checkEqual(t, repoInfo.Index.Name, expectedRepoInfo.Index.Name, reposName) + checkEqual(t, repoInfo.RemoteName(), expectedRepoInfo.RemoteName, reposName) + checkEqual(t, repoInfo.Name(), expectedRepoInfo.LocalName, reposName) + checkEqual(t, repoInfo.FullName(), expectedRepoInfo.CanonicalName, reposName) + checkEqual(t, repoInfo.Index.Official, expectedRepoInfo.Index.Official, reposName) + checkEqual(t, repoInfo.Official, expectedRepoInfo.Official, reposName) + } + } +} + +func TestNewIndexInfo(t *testing.T) { + testIndexInfo := func(config *serviceConfig, expectedIndexInfos map[string]*registrytypes.IndexInfo) { + for indexName, expectedIndexInfo := range expectedIndexInfos { + index, err := newIndexInfo(config, indexName) + if err != nil { + t.Fatal(err) + } else { + checkEqual(t, index.Name, expectedIndexInfo.Name, indexName+" name") + checkEqual(t, index.Official, expectedIndexInfo.Official, indexName+" is official") + checkEqual(t, index.Secure, expectedIndexInfo.Secure, indexName+" is secure") + checkEqual(t, len(index.Mirrors), len(expectedIndexInfo.Mirrors), indexName+" mirrors") + } + } + } + + config := newServiceConfig(ServiceOptions{}) + noMirrors := []string{} + expectedIndexInfos := map[string]*registrytypes.IndexInfo{ + IndexName: { + Name: IndexName, + Official: true, + Secure: true, + Mirrors: noMirrors, + }, + "index." + IndexName: { + Name: IndexName, + Official: true, + Secure: true, + Mirrors: noMirrors, + }, + "example.com": { + Name: "example.com", + Official: false, + Secure: true, + Mirrors: noMirrors, + }, + "127.0.0.1:5000": { + Name: "127.0.0.1:5000", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + } + testIndexInfo(config, expectedIndexInfos) + + publicMirrors := []string{"http://mirror1.local", "http://mirror2.local"} + config = makeServiceConfig(publicMirrors, []string{"example.com"}) + + expectedIndexInfos = map[string]*registrytypes.IndexInfo{ + IndexName: { + Name: IndexName, + Official: true, + Secure: true, + Mirrors: publicMirrors, + }, + "index." + IndexName: { + Name: IndexName, + Official: true, + Secure: true, + Mirrors: publicMirrors, + }, + "example.com": { + Name: "example.com", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + "example.com:5000": { + Name: "example.com:5000", + Official: false, + Secure: true, + Mirrors: noMirrors, + }, + "127.0.0.1": { + Name: "127.0.0.1", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + "127.0.0.1:5000": { + Name: "127.0.0.1:5000", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + "other.com": { + Name: "other.com", + Official: false, + Secure: true, + Mirrors: noMirrors, + }, + } + testIndexInfo(config, expectedIndexInfos) + + config = makeServiceConfig(nil, []string{"42.42.0.0/16"}) + expectedIndexInfos = map[string]*registrytypes.IndexInfo{ + "example.com": { + Name: "example.com", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + "example.com:5000": { + Name: "example.com:5000", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + "127.0.0.1": { + Name: "127.0.0.1", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + "127.0.0.1:5000": { + Name: "127.0.0.1:5000", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + "other.com": { + Name: "other.com", + Official: false, + Secure: true, + Mirrors: noMirrors, + }, + } + testIndexInfo(config, expectedIndexInfos) +} + +func TestMirrorEndpointLookup(t *testing.T) { + containsMirror := func(endpoints []APIEndpoint) bool { + for _, pe := range endpoints { + if pe.URL.Host == "my.mirror" { + return true + } + } + return false + } + s := Service{config: makeServiceConfig([]string{"my.mirror"}, nil)} + + imageName, err := reference.WithName(IndexName + "/test/image") + if err != nil { + t.Error(err) + } + pushAPIEndpoints, err := s.LookupPushEndpoints(imageName.Hostname()) + if err != nil { + t.Fatal(err) + } + if containsMirror(pushAPIEndpoints) { + t.Fatal("Push endpoint should not contain mirror") + } + + pullAPIEndpoints, err := s.LookupPullEndpoints(imageName.Hostname()) + if err != nil { + t.Fatal(err) + } + if !containsMirror(pullAPIEndpoints) { + t.Fatal("Pull endpoint should contain mirror") + } +} + +func TestPushRegistryTag(t *testing.T) { + r := spawnTestRegistrySession(t) + repoRef, err := reference.ParseNamed(REPO) + if err != nil { + t.Fatal(err) + } + err = r.PushRegistryTag(repoRef, imageID, "stable", makeURL("/v1/")) + if err != nil { + t.Fatal(err) + } +} + +func TestPushImageJSONIndex(t *testing.T) { + r := spawnTestRegistrySession(t) + imgData := []*ImgData{ + { + ID: "77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20", + Checksum: "sha256:1ac330d56e05eef6d438586545ceff7550d3bdcb6b19961f12c5ba714ee1bb37", + }, + { + ID: "42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d", + Checksum: "sha256:bea7bf2e4bacd479344b737328db47b18880d09096e6674165533aa994f5e9f2", + }, + } + repoRef, err := reference.ParseNamed(REPO) + if err != nil { + t.Fatal(err) + } + repoData, err := r.PushImageJSONIndex(repoRef, imgData, false, nil) + if err != nil { + t.Fatal(err) + } + if repoData == nil { + t.Fatal("Expected RepositoryData object") + } + repoData, err = r.PushImageJSONIndex(repoRef, imgData, true, []string{r.indexEndpoint.String()}) + if err != nil { + t.Fatal(err) + } + if repoData == nil { + t.Fatal("Expected RepositoryData object") + } +} + +func TestSearchRepositories(t *testing.T) { + r := spawnTestRegistrySession(t) + results, err := r.SearchRepositories("fakequery") + if err != nil { + t.Fatal(err) + } + if results == nil { + t.Fatal("Expected non-nil SearchResults object") + } + assertEqual(t, results.NumResults, 1, "Expected 1 search results") + assertEqual(t, results.Query, "fakequery", "Expected 'fakequery' as query") + assertEqual(t, results.Results[0].StarCount, 42, "Expected 'fakeimage' to have 42 stars") +} + +func TestTrustedLocation(t *testing.T) { + for _, url := range []string{"http://example.com", "https://example.com:7777", "http://docker.io", "http://test.docker.com", "https://fakedocker.com"} { + req, _ := http.NewRequest("GET", url, nil) + if trustedLocation(req) == true { + t.Fatalf("'%s' shouldn't be detected as a trusted location", url) + } + } + + for _, url := range []string{"https://docker.io", "https://test.docker.com:80"} { + req, _ := http.NewRequest("GET", url, nil) + if trustedLocation(req) == false { + t.Fatalf("'%s' should be detected as a trusted location", url) + } + } +} + +func TestAddRequiredHeadersToRedirectedRequests(t *testing.T) { + for _, urls := range [][]string{ + {"http://docker.io", "https://docker.com"}, + {"https://foo.docker.io:7777", "http://bar.docker.com"}, + {"https://foo.docker.io", "https://example.com"}, + } { + reqFrom, _ := http.NewRequest("GET", urls[0], nil) + reqFrom.Header.Add("Content-Type", "application/json") + reqFrom.Header.Add("Authorization", "super_secret") + reqTo, _ := http.NewRequest("GET", urls[1], nil) + + addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom}) + + if len(reqTo.Header) != 1 { + t.Fatalf("Expected 1 headers, got %d", len(reqTo.Header)) + } + + if reqTo.Header.Get("Content-Type") != "application/json" { + t.Fatal("'Content-Type' should be 'application/json'") + } + + if reqTo.Header.Get("Authorization") != "" { + t.Fatal("'Authorization' should be empty") + } + } + + for _, urls := range [][]string{ + {"https://docker.io", "https://docker.com"}, + {"https://foo.docker.io:7777", "https://bar.docker.com"}, + } { + reqFrom, _ := http.NewRequest("GET", urls[0], nil) + reqFrom.Header.Add("Content-Type", "application/json") + reqFrom.Header.Add("Authorization", "super_secret") + reqTo, _ := http.NewRequest("GET", urls[1], nil) + + addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom}) + + if len(reqTo.Header) != 2 { + t.Fatalf("Expected 2 headers, got %d", len(reqTo.Header)) + } + + if reqTo.Header.Get("Content-Type") != "application/json" { + t.Fatal("'Content-Type' should be 'application/json'") + } + + if reqTo.Header.Get("Authorization") != "super_secret" { + t.Fatal("'Authorization' should be 'super_secret'") + } + } +} + +func TestIsSecureIndex(t *testing.T) { + tests := []struct { + addr string + insecureRegistries []string + expected bool + }{ + {IndexName, nil, true}, + {"example.com", []string{}, true}, + {"example.com", []string{"example.com"}, false}, + {"localhost", []string{"localhost:5000"}, false}, + {"localhost:5000", []string{"localhost:5000"}, false}, + {"localhost", []string{"example.com"}, false}, + {"127.0.0.1:5000", []string{"127.0.0.1:5000"}, false}, + {"localhost", nil, false}, + {"localhost:5000", nil, false}, + {"127.0.0.1", nil, false}, + {"localhost", []string{"example.com"}, false}, + {"127.0.0.1", []string{"example.com"}, false}, + {"example.com", nil, true}, + {"example.com", []string{"example.com"}, false}, + {"127.0.0.1", []string{"example.com"}, false}, + {"127.0.0.1:5000", []string{"example.com"}, false}, + {"example.com:5000", []string{"42.42.0.0/16"}, false}, + {"example.com", []string{"42.42.0.0/16"}, false}, + {"example.com:5000", []string{"42.42.42.42/8"}, false}, + {"127.0.0.1:5000", []string{"127.0.0.0/8"}, false}, + {"42.42.42.42:5000", []string{"42.1.1.1/8"}, false}, + {"invalid.domain.com", []string{"42.42.0.0/16"}, true}, + {"invalid.domain.com", []string{"invalid.domain.com"}, false}, + {"invalid.domain.com:5000", []string{"invalid.domain.com"}, true}, + {"invalid.domain.com:5000", []string{"invalid.domain.com:5000"}, false}, + } + for _, tt := range tests { + config := makeServiceConfig(nil, tt.insecureRegistries) + if sec := isSecureIndex(config, tt.addr); sec != tt.expected { + t.Errorf("isSecureIndex failed for %q %v, expected %v got %v", tt.addr, tt.insecureRegistries, tt.expected, sec) + } + } +} + +type debugTransport struct { + http.RoundTripper + log func(...interface{}) +} + +func (tr debugTransport) RoundTrip(req *http.Request) (*http.Response, error) { + dump, err := httputil.DumpRequestOut(req, false) + if err != nil { + tr.log("could not dump request") + } + tr.log(string(dump)) + resp, err := tr.RoundTripper.RoundTrip(req) + if err != nil { + return nil, err + } + dump, err = httputil.DumpResponse(resp, false) + if err != nil { + tr.log("could not dump response") + } + tr.log(string(dump)) + return resp, err +} diff --git a/registry/service.go b/registry/service.go new file mode 100644 index 00000000..cdeca585 --- /dev/null +++ b/registry/service.go @@ -0,0 +1,205 @@ +package registry + +import ( + "crypto/tls" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/reference" + "github.com/docker/engine-api/types" + registrytypes "github.com/docker/engine-api/types/registry" +) + +// Service is a registry service. It tracks configuration data such as a list +// of mirrors. +type Service struct { + config *serviceConfig +} + +// NewService returns a new instance of Service ready to be +// installed into an engine. +func NewService(options ServiceOptions) *Service { + return &Service{ + config: newServiceConfig(options), + } +} + +// ServiceConfig returns the public registry service configuration. +func (s *Service) ServiceConfig() *registrytypes.ServiceConfig { + return &s.config.ServiceConfig +} + +// Auth contacts the public registry with the provided credentials, +// and returns OK if authentication was successful. +// It can be used to verify the validity of a client's credentials. +func (s *Service) Auth(authConfig *types.AuthConfig, userAgent string) (status, token string, err error) { + serverAddress := authConfig.ServerAddress + if serverAddress == "" { + serverAddress = IndexServer + } + if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") { + serverAddress = "https://" + serverAddress + } + u, err := url.Parse(serverAddress) + if err != nil { + return "", "", fmt.Errorf("unable to parse server address: %v", err) + } + + endpoints, err := s.LookupPushEndpoints(u.Host) + if err != nil { + return "", "", err + } + + for _, endpoint := range endpoints { + login := loginV2 + if endpoint.Version == APIVersion1 { + login = loginV1 + } + + status, token, err = login(authConfig, endpoint, userAgent) + if err == nil { + return + } + if fErr, ok := err.(fallbackError); ok { + err = fErr.err + logrus.Infof("Error logging in to %s endpoint, trying next endpoint: %v", endpoint.Version, err) + continue + } + return "", "", err + } + + return "", "", err +} + +// splitReposSearchTerm breaks a search term into an index name and remote name +func splitReposSearchTerm(reposName string) (string, string) { + nameParts := strings.SplitN(reposName, "/", 2) + var indexName, remoteName string + if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") && + !strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") { + // This is a Docker Index repos (ex: samalba/hipache or ubuntu) + // 'docker.io' + indexName = IndexName + remoteName = reposName + } else { + indexName = nameParts[0] + remoteName = nameParts[1] + } + return indexName, remoteName +} + +// Search queries the public registry for images matching the specified +// search terms, and returns the results. +func (s *Service) Search(term string, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) { + if err := validateNoSchema(term); err != nil { + return nil, err + } + + indexName, remoteName := splitReposSearchTerm(term) + + index, err := newIndexInfo(s.config, indexName) + if err != nil { + return nil, err + } + + // *TODO: Search multiple indexes. + endpoint, err := NewV1Endpoint(index, userAgent, http.Header(headers)) + if err != nil { + return nil, err + } + + r, err := NewSession(endpoint.client, authConfig, endpoint) + if err != nil { + return nil, err + } + + if index.Official { + localName := remoteName + if strings.HasPrefix(localName, "library/") { + // If pull "library/foo", it's stored locally under "foo" + localName = strings.SplitN(localName, "/", 2)[1] + } + + return r.SearchRepositories(localName) + } + return r.SearchRepositories(remoteName) +} + +// ResolveRepository splits a repository name into its components +// and configuration of the associated registry. +func (s *Service) ResolveRepository(name reference.Named) (*RepositoryInfo, error) { + return newRepositoryInfo(s.config, name) +} + +// ResolveIndex takes indexName and returns index info +func (s *Service) ResolveIndex(name string) (*registrytypes.IndexInfo, error) { + return newIndexInfo(s.config, name) +} + +// APIEndpoint represents a remote API endpoint +type APIEndpoint struct { + Mirror bool + URL *url.URL + Version APIVersion + Official bool + TrimHostname bool + TLSConfig *tls.Config +} + +// ToV1Endpoint returns a V1 API endpoint based on the APIEndpoint +func (e APIEndpoint) ToV1Endpoint(userAgent string, metaHeaders http.Header) (*V1Endpoint, error) { + return newV1Endpoint(*e.URL, e.TLSConfig, userAgent, metaHeaders) +} + +// TLSConfig constructs a client TLS configuration based on server defaults +func (s *Service) TLSConfig(hostname string) (*tls.Config, error) { + return newTLSConfig(hostname, isSecureIndex(s.config, hostname)) +} + +func (s *Service) tlsConfigForMirror(mirrorURL *url.URL) (*tls.Config, error) { + return s.TLSConfig(mirrorURL.Host) +} + +// LookupPullEndpoints creates a list of endpoints to try to pull from, in order of preference. +// It gives preference to v2 endpoints over v1, mirrors over the actual +// registry, and HTTPS over plain HTTP. +func (s *Service) LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) { + return s.lookupEndpoints(hostname) +} + +// LookupPushEndpoints creates a list of endpoints to try to push to, in order of preference. +// It gives preference to v2 endpoints over v1, and HTTPS over plain HTTP. +// Mirrors are not included. +func (s *Service) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) { + allEndpoints, err := s.lookupEndpoints(hostname) + if err == nil { + for _, endpoint := range allEndpoints { + if !endpoint.Mirror { + endpoints = append(endpoints, endpoint) + } + } + } + return endpoints, err +} + +func (s *Service) lookupEndpoints(hostname string) (endpoints []APIEndpoint, err error) { + endpoints, err = s.lookupV2Endpoints(hostname) + if err != nil { + return nil, err + } + + if s.config.V2Only { + return endpoints, nil + } + + legacyEndpoints, err := s.lookupV1Endpoints(hostname) + if err != nil { + return nil, err + } + endpoints = append(endpoints, legacyEndpoints...) + + return endpoints, nil +} diff --git a/registry/service_v1.go b/registry/service_v1.go new file mode 100644 index 00000000..56121eea --- /dev/null +++ b/registry/service_v1.go @@ -0,0 +1,53 @@ +package registry + +import ( + "net/url" + + "github.com/docker/go-connections/tlsconfig" +) + +func (s *Service) lookupV1Endpoints(hostname string) (endpoints []APIEndpoint, err error) { + var cfg = tlsconfig.ServerDefault + tlsConfig := &cfg + if hostname == DefaultNamespace { + endpoints = append(endpoints, APIEndpoint{ + URL: DefaultV1Registry, + Version: APIVersion1, + Official: true, + TrimHostname: true, + TLSConfig: tlsConfig, + }) + return endpoints, nil + } + + tlsConfig, err = s.TLSConfig(hostname) + if err != nil { + return nil, err + } + + endpoints = []APIEndpoint{ + { + URL: &url.URL{ + Scheme: "https", + Host: hostname, + }, + Version: APIVersion1, + TrimHostname: true, + TLSConfig: tlsConfig, + }, + } + + if tlsConfig.InsecureSkipVerify { + endpoints = append(endpoints, APIEndpoint{ // or this + URL: &url.URL{ + Scheme: "http", + Host: hostname, + }, + Version: APIVersion1, + TrimHostname: true, + // used to check if supposed to be secure via InsecureSkipVerify + TLSConfig: tlsConfig, + }) + } + return endpoints, nil +} diff --git a/registry/service_v2.go b/registry/service_v2.go new file mode 100644 index 00000000..4113d57d --- /dev/null +++ b/registry/service_v2.go @@ -0,0 +1,79 @@ +package registry + +import ( + "net/url" + "strings" + + "github.com/docker/go-connections/tlsconfig" +) + +func (s *Service) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) { + var cfg = tlsconfig.ServerDefault + tlsConfig := &cfg + if hostname == DefaultNamespace || hostname == DefaultV1Registry.Host { + // v2 mirrors + for _, mirror := range s.config.Mirrors { + if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") { + mirror = "https://" + mirror + } + mirrorURL, err := url.Parse(mirror) + if err != nil { + return nil, err + } + mirrorTLSConfig, err := s.tlsConfigForMirror(mirrorURL) + if err != nil { + return nil, err + } + endpoints = append(endpoints, APIEndpoint{ + URL: mirrorURL, + // guess mirrors are v2 + Version: APIVersion2, + Mirror: true, + TrimHostname: true, + TLSConfig: mirrorTLSConfig, + }) + } + // v2 registry + endpoints = append(endpoints, APIEndpoint{ + URL: DefaultV2Registry, + Version: APIVersion2, + Official: true, + TrimHostname: true, + TLSConfig: tlsConfig, + }) + + return endpoints, nil + } + + tlsConfig, err = s.TLSConfig(hostname) + if err != nil { + return nil, err + } + + endpoints = []APIEndpoint{ + { + URL: &url.URL{ + Scheme: "https", + Host: hostname, + }, + Version: APIVersion2, + TrimHostname: true, + TLSConfig: tlsConfig, + }, + } + + if tlsConfig.InsecureSkipVerify { + endpoints = append(endpoints, APIEndpoint{ + URL: &url.URL{ + Scheme: "http", + Host: hostname, + }, + Version: APIVersion2, + TrimHostname: true, + // used to check if supposed to be secure via InsecureSkipVerify + TLSConfig: tlsConfig, + }) + } + + return endpoints, nil +} diff --git a/registry/session.go b/registry/session.go new file mode 100644 index 00000000..5647ad28 --- /dev/null +++ b/registry/session.go @@ -0,0 +1,770 @@ +package registry + +import ( + "bytes" + "crypto/sha256" + "errors" + "sync" + // this is required for some certificates + _ "crypto/sha512" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/cookiejar" + "net/url" + "strconv" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/registry/api/errcode" + "github.com/docker/docker/pkg/httputils" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/tarsum" + "github.com/docker/docker/reference" + "github.com/docker/engine-api/types" + registrytypes "github.com/docker/engine-api/types/registry" +) + +var ( + // ErrRepoNotFound is returned if the repository didn't exist on the + // remote side + ErrRepoNotFound = errors.New("Repository not found") +) + +// A Session is used to communicate with a V1 registry +type Session struct { + indexEndpoint *V1Endpoint + client *http.Client + // TODO(tiborvass): remove authConfig + authConfig *types.AuthConfig + id string +} + +type authTransport struct { + http.RoundTripper + *types.AuthConfig + + alwaysSetBasicAuth bool + token []string + + mu sync.Mutex // guards modReq + modReq map[*http.Request]*http.Request // original -> modified +} + +// AuthTransport handles the auth layer when communicating with a v1 registry (private or official) +// +// For private v1 registries, set alwaysSetBasicAuth to true. +// +// For the official v1 registry, if there isn't already an Authorization header in the request, +// but there is an X-Docker-Token header set to true, then Basic Auth will be used to set the Authorization header. +// After sending the request with the provided base http.RoundTripper, if an X-Docker-Token header, representing +// a token, is present in the response, then it gets cached and sent in the Authorization header of all subsequent +// requests. +// +// If the server sends a token without the client having requested it, it is ignored. +// +// This RoundTripper also has a CancelRequest method important for correct timeout handling. +func AuthTransport(base http.RoundTripper, authConfig *types.AuthConfig, alwaysSetBasicAuth bool) http.RoundTripper { + if base == nil { + base = http.DefaultTransport + } + return &authTransport{ + RoundTripper: base, + AuthConfig: authConfig, + alwaysSetBasicAuth: alwaysSetBasicAuth, + modReq: make(map[*http.Request]*http.Request), + } +} + +// cloneRequest returns a clone of the provided *http.Request. +// The clone is a shallow copy of the struct and its Header map. +func cloneRequest(r *http.Request) *http.Request { + // shallow copy of the struct + r2 := new(http.Request) + *r2 = *r + // deep copy of the Header + r2.Header = make(http.Header, len(r.Header)) + for k, s := range r.Header { + r2.Header[k] = append([]string(nil), s...) + } + + return r2 +} + +// RoundTrip changes a HTTP request's headers to add the necessary +// authentication-related headers +func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) { + // Authorization should not be set on 302 redirect for untrusted locations. + // This logic mirrors the behavior in addRequiredHeadersToRedirectedRequests. + // As the authorization logic is currently implemented in RoundTrip, + // a 302 redirect is detected by looking at the Referrer header as go http package adds said header. + // This is safe as Docker doesn't set Referrer in other scenarios. + if orig.Header.Get("Referer") != "" && !trustedLocation(orig) { + return tr.RoundTripper.RoundTrip(orig) + } + + req := cloneRequest(orig) + tr.mu.Lock() + tr.modReq[orig] = req + tr.mu.Unlock() + + if tr.alwaysSetBasicAuth { + if tr.AuthConfig == nil { + return nil, errors.New("unexpected error: empty auth config") + } + req.SetBasicAuth(tr.Username, tr.Password) + return tr.RoundTripper.RoundTrip(req) + } + + // Don't override + if req.Header.Get("Authorization") == "" { + if req.Header.Get("X-Docker-Token") == "true" && tr.AuthConfig != nil && len(tr.Username) > 0 { + req.SetBasicAuth(tr.Username, tr.Password) + } else if len(tr.token) > 0 { + req.Header.Set("Authorization", "Token "+strings.Join(tr.token, ",")) + } + } + resp, err := tr.RoundTripper.RoundTrip(req) + if err != nil { + delete(tr.modReq, orig) + return nil, err + } + if len(resp.Header["X-Docker-Token"]) > 0 { + tr.token = resp.Header["X-Docker-Token"] + } + resp.Body = &ioutils.OnEOFReader{ + Rc: resp.Body, + Fn: func() { + tr.mu.Lock() + delete(tr.modReq, orig) + tr.mu.Unlock() + }, + } + return resp, nil +} + +// CancelRequest cancels an in-flight request by closing its connection. +func (tr *authTransport) CancelRequest(req *http.Request) { + type canceler interface { + CancelRequest(*http.Request) + } + if cr, ok := tr.RoundTripper.(canceler); ok { + tr.mu.Lock() + modReq := tr.modReq[req] + delete(tr.modReq, req) + tr.mu.Unlock() + cr.CancelRequest(modReq) + } +} + +// NewSession creates a new session +// TODO(tiborvass): remove authConfig param once registry client v2 is vendored +func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) (r *Session, err error) { + r = &Session{ + authConfig: authConfig, + client: client, + indexEndpoint: endpoint, + id: stringid.GenerateRandomID(), + } + + var alwaysSetBasicAuth bool + + // If we're working with a standalone private registry over HTTPS, send Basic Auth headers + // alongside all our requests. + if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" { + info, err := endpoint.Ping() + if err != nil { + return nil, err + } + if info.Standalone && authConfig != nil { + logrus.Debugf("Endpoint %s is eligible for private registry. Enabling decorator.", endpoint.String()) + alwaysSetBasicAuth = true + } + } + + // Annotate the transport unconditionally so that v2 can + // properly fallback on v1 when an image is not found. + client.Transport = AuthTransport(client.Transport, authConfig, alwaysSetBasicAuth) + + jar, err := cookiejar.New(nil) + if err != nil { + return nil, errors.New("cookiejar.New is not supposed to return an error") + } + client.Jar = jar + + return r, nil +} + +// ID returns this registry session's ID. +func (r *Session) ID() string { + return r.id +} + +// GetRemoteHistory retrieves the history of a given image from the registry. +// It returns a list of the parent's JSON files (including the requested image). +func (r *Session) GetRemoteHistory(imgID, registry string) ([]string, error) { + res, err := r.client.Get(registry + "images/" + imgID + "/ancestry") + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != 200 { + if res.StatusCode == 401 { + return nil, errcode.ErrorCodeUnauthorized.WithArgs() + } + return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to fetch remote history for %s", res.StatusCode, imgID), res) + } + + var history []string + if err := json.NewDecoder(res.Body).Decode(&history); err != nil { + return nil, fmt.Errorf("Error while reading the http response: %v", err) + } + + logrus.Debugf("Ancestry: %v", history) + return history, nil +} + +// LookupRemoteImage checks if an image exists in the registry +func (r *Session) LookupRemoteImage(imgID, registry string) error { + res, err := r.client.Get(registry + "images/" + imgID + "/json") + if err != nil { + return err + } + res.Body.Close() + if res.StatusCode != 200 { + return httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d", res.StatusCode), res) + } + return nil +} + +// GetRemoteImageJSON retrieves an image's JSON metadata from the registry. +func (r *Session) GetRemoteImageJSON(imgID, registry string) ([]byte, int64, error) { + res, err := r.client.Get(registry + "images/" + imgID + "/json") + if err != nil { + return nil, -1, fmt.Errorf("Failed to download json: %s", err) + } + defer res.Body.Close() + if res.StatusCode != 200 { + return nil, -1, httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d", res.StatusCode), res) + } + // if the size header is not present, then set it to '-1' + imageSize := int64(-1) + if hdr := res.Header.Get("X-Docker-Size"); hdr != "" { + imageSize, err = strconv.ParseInt(hdr, 10, 64) + if err != nil { + return nil, -1, err + } + } + + jsonString, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, -1, fmt.Errorf("Failed to parse downloaded json: %v (%s)", err, jsonString) + } + return jsonString, imageSize, nil +} + +// GetRemoteImageLayer retrieves an image layer from the registry +func (r *Session) GetRemoteImageLayer(imgID, registry string, imgSize int64) (io.ReadCloser, error) { + var ( + statusCode = 0 + res *http.Response + err error + imageURL = fmt.Sprintf("%simages/%s/layer", registry, imgID) + ) + + req, err := http.NewRequest("GET", imageURL, nil) + if err != nil { + return nil, fmt.Errorf("Error while getting from the server: %v", err) + } + statusCode = 0 + res, err = r.client.Do(req) + if err != nil { + logrus.Debugf("Error contacting registry %s: %v", registry, err) + // the only case err != nil && res != nil is https://golang.org/src/net/http/client.go#L515 + if res != nil { + if res.Body != nil { + res.Body.Close() + } + statusCode = res.StatusCode + } + return nil, fmt.Errorf("Server error: Status %d while fetching image layer (%s)", + statusCode, imgID) + } + + if res.StatusCode != 200 { + res.Body.Close() + return nil, fmt.Errorf("Server error: Status %d while fetching image layer (%s)", + res.StatusCode, imgID) + } + + if res.Header.Get("Accept-Ranges") == "bytes" && imgSize > 0 { + logrus.Debugf("server supports resume") + return httputils.ResumableRequestReaderWithInitialResponse(r.client, req, 5, imgSize, res), nil + } + logrus.Debugf("server doesn't support resume") + return res.Body, nil +} + +// GetRemoteTag retrieves the tag named in the askedTag argument from the given +// repository. It queries each of the registries supplied in the registries +// argument, and returns data from the first one that answers the query +// successfully. +func (r *Session) GetRemoteTag(registries []string, repositoryRef reference.Named, askedTag string) (string, error) { + repository := repositoryRef.RemoteName() + + if strings.Count(repository, "/") == 0 { + // This will be removed once the registry supports auto-resolution on + // the "library" namespace + repository = "library/" + repository + } + for _, host := range registries { + endpoint := fmt.Sprintf("%srepositories/%s/tags/%s", host, repository, askedTag) + res, err := r.client.Get(endpoint) + if err != nil { + return "", err + } + + logrus.Debugf("Got status code %d from %s", res.StatusCode, endpoint) + defer res.Body.Close() + + if res.StatusCode == 404 { + return "", ErrRepoNotFound + } + if res.StatusCode != 200 { + continue + } + + var tagID string + if err := json.NewDecoder(res.Body).Decode(&tagID); err != nil { + return "", err + } + return tagID, nil + } + return "", fmt.Errorf("Could not reach any registry endpoint") +} + +// GetRemoteTags retrieves all tags from the given repository. It queries each +// of the registries supplied in the registries argument, and returns data from +// the first one that answers the query successfully. It returns a map with +// tag names as the keys and image IDs as the values. +func (r *Session) GetRemoteTags(registries []string, repositoryRef reference.Named) (map[string]string, error) { + repository := repositoryRef.RemoteName() + + if strings.Count(repository, "/") == 0 { + // This will be removed once the registry supports auto-resolution on + // the "library" namespace + repository = "library/" + repository + } + for _, host := range registries { + endpoint := fmt.Sprintf("%srepositories/%s/tags", host, repository) + res, err := r.client.Get(endpoint) + if err != nil { + return nil, err + } + + logrus.Debugf("Got status code %d from %s", res.StatusCode, endpoint) + defer res.Body.Close() + + if res.StatusCode == 404 { + return nil, ErrRepoNotFound + } + if res.StatusCode != 200 { + continue + } + + result := make(map[string]string) + if err := json.NewDecoder(res.Body).Decode(&result); err != nil { + return nil, err + } + return result, nil + } + return nil, fmt.Errorf("Could not reach any registry endpoint") +} + +func buildEndpointsList(headers []string, indexEp string) ([]string, error) { + var endpoints []string + parsedURL, err := url.Parse(indexEp) + if err != nil { + return nil, err + } + var urlScheme = parsedURL.Scheme + // The registry's URL scheme has to match the Index' + for _, ep := range headers { + epList := strings.Split(ep, ",") + for _, epListElement := range epList { + endpoints = append( + endpoints, + fmt.Sprintf("%s://%s/v1/", urlScheme, strings.TrimSpace(epListElement))) + } + } + return endpoints, nil +} + +// GetRepositoryData returns lists of images and endpoints for the repository +func (r *Session) GetRepositoryData(name reference.Named) (*RepositoryData, error) { + repositoryTarget := fmt.Sprintf("%srepositories/%s/images", r.indexEndpoint.String(), name.RemoteName()) + + logrus.Debugf("[registry] Calling GET %s", repositoryTarget) + + req, err := http.NewRequest("GET", repositoryTarget, nil) + if err != nil { + return nil, err + } + // this will set basic auth in r.client.Transport and send cached X-Docker-Token headers for all subsequent requests + req.Header.Set("X-Docker-Token", "true") + res, err := r.client.Do(req) + if err != nil { + // check if the error is because of i/o timeout + // and return a non-obtuse error message for users + // "Get https://index.docker.io/v1/repositories/library/busybox/images: i/o timeout" + // was a top search on the docker user forum + if isTimeout(err) { + return nil, fmt.Errorf("Network timed out while trying to connect to %s. You may want to check your internet connection or if you are behind a proxy.", repositoryTarget) + } + return nil, fmt.Errorf("Error while pulling image: %v", err) + } + defer res.Body.Close() + if res.StatusCode == 401 { + return nil, errcode.ErrorCodeUnauthorized.WithArgs() + } + // TODO: Right now we're ignoring checksums in the response body. + // In the future, we need to use them to check image validity. + if res.StatusCode == 404 { + return nil, httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code: %d", res.StatusCode), res) + } else if res.StatusCode != 200 { + errBody, err := ioutil.ReadAll(res.Body) + if err != nil { + logrus.Debugf("Error reading response body: %s", err) + } + return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to pull repository %s: %q", res.StatusCode, name.RemoteName(), errBody), res) + } + + var endpoints []string + if res.Header.Get("X-Docker-Endpoints") != "" { + endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.String()) + if err != nil { + return nil, err + } + } else { + // Assume the endpoint is on the same host + endpoints = append(endpoints, fmt.Sprintf("%s://%s/v1/", r.indexEndpoint.URL.Scheme, req.URL.Host)) + } + + remoteChecksums := []*ImgData{} + if err := json.NewDecoder(res.Body).Decode(&remoteChecksums); err != nil { + return nil, err + } + + // Forge a better object from the retrieved data + imgsData := make(map[string]*ImgData, len(remoteChecksums)) + for _, elem := range remoteChecksums { + imgsData[elem.ID] = elem + } + + return &RepositoryData{ + ImgList: imgsData, + Endpoints: endpoints, + }, nil +} + +// PushImageChecksumRegistry uploads checksums for an image +func (r *Session) PushImageChecksumRegistry(imgData *ImgData, registry string) error { + u := registry + "images/" + imgData.ID + "/checksum" + + logrus.Debugf("[registry] Calling PUT %s", u) + + req, err := http.NewRequest("PUT", u, nil) + if err != nil { + return err + } + req.Header.Set("X-Docker-Checksum", imgData.Checksum) + req.Header.Set("X-Docker-Checksum-Payload", imgData.ChecksumPayload) + + res, err := r.client.Do(req) + if err != nil { + return fmt.Errorf("Failed to upload metadata: %v", err) + } + defer res.Body.Close() + if len(res.Cookies()) > 0 { + r.client.Jar.SetCookies(req.URL, res.Cookies()) + } + if res.StatusCode != 200 { + errBody, err := ioutil.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err) + } + var jsonBody map[string]string + if err := json.Unmarshal(errBody, &jsonBody); err != nil { + errBody = []byte(err.Error()) + } else if jsonBody["error"] == "Image already exists" { + return ErrAlreadyExists + } + return fmt.Errorf("HTTP code %d while uploading metadata: %q", res.StatusCode, errBody) + } + return nil +} + +// PushImageJSONRegistry pushes JSON metadata for a local image to the registry +func (r *Session) PushImageJSONRegistry(imgData *ImgData, jsonRaw []byte, registry string) error { + + u := registry + "images/" + imgData.ID + "/json" + + logrus.Debugf("[registry] Calling PUT %s", u) + + req, err := http.NewRequest("PUT", u, bytes.NewReader(jsonRaw)) + if err != nil { + return err + } + req.Header.Add("Content-type", "application/json") + + res, err := r.client.Do(req) + if err != nil { + return fmt.Errorf("Failed to upload metadata: %s", err) + } + defer res.Body.Close() + if res.StatusCode == 401 && strings.HasPrefix(registry, "http://") { + return httputils.NewHTTPRequestError("HTTP code 401, Docker will not send auth headers over HTTP.", res) + } + if res.StatusCode != 200 { + errBody, err := ioutil.ReadAll(res.Body) + if err != nil { + return httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err), res) + } + var jsonBody map[string]string + if err := json.Unmarshal(errBody, &jsonBody); err != nil { + errBody = []byte(err.Error()) + } else if jsonBody["error"] == "Image already exists" { + return ErrAlreadyExists + } + return httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d while uploading metadata: %q", res.StatusCode, errBody), res) + } + return nil +} + +// PushImageLayerRegistry sends the checksum of an image layer to the registry +func (r *Session) PushImageLayerRegistry(imgID string, layer io.Reader, registry string, jsonRaw []byte) (checksum string, checksumPayload string, err error) { + u := registry + "images/" + imgID + "/layer" + + logrus.Debugf("[registry] Calling PUT %s", u) + + tarsumLayer, err := tarsum.NewTarSum(layer, false, tarsum.Version0) + if err != nil { + return "", "", err + } + h := sha256.New() + h.Write(jsonRaw) + h.Write([]byte{'\n'}) + checksumLayer := io.TeeReader(tarsumLayer, h) + + req, err := http.NewRequest("PUT", u, checksumLayer) + if err != nil { + return "", "", err + } + req.Header.Add("Content-Type", "application/octet-stream") + req.ContentLength = -1 + req.TransferEncoding = []string{"chunked"} + res, err := r.client.Do(req) + if err != nil { + return "", "", fmt.Errorf("Failed to upload layer: %v", err) + } + if rc, ok := layer.(io.Closer); ok { + if err := rc.Close(); err != nil { + return "", "", err + } + } + defer res.Body.Close() + + if res.StatusCode != 200 { + errBody, err := ioutil.ReadAll(res.Body) + if err != nil { + return "", "", httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err), res) + } + return "", "", httputils.NewHTTPRequestError(fmt.Sprintf("Received HTTP code %d while uploading layer: %q", res.StatusCode, errBody), res) + } + + checksumPayload = "sha256:" + hex.EncodeToString(h.Sum(nil)) + return tarsumLayer.Sum(jsonRaw), checksumPayload, nil +} + +// PushRegistryTag pushes a tag on the registry. +// Remote has the format '/ +func (r *Session) PushRegistryTag(remote reference.Named, revision, tag, registry string) error { + // "jsonify" the string + revision = "\"" + revision + "\"" + path := fmt.Sprintf("repositories/%s/tags/%s", remote.RemoteName(), tag) + + req, err := http.NewRequest("PUT", registry+path, strings.NewReader(revision)) + if err != nil { + return err + } + req.Header.Add("Content-type", "application/json") + req.ContentLength = int64(len(revision)) + res, err := r.client.Do(req) + if err != nil { + return err + } + res.Body.Close() + if res.StatusCode != 200 && res.StatusCode != 201 { + return httputils.NewHTTPRequestError(fmt.Sprintf("Internal server error: %d trying to push tag %s on %s", res.StatusCode, tag, remote.RemoteName()), res) + } + return nil +} + +// PushImageJSONIndex uploads an image list to the repository +func (r *Session) PushImageJSONIndex(remote reference.Named, imgList []*ImgData, validate bool, regs []string) (*RepositoryData, error) { + cleanImgList := []*ImgData{} + if validate { + for _, elem := range imgList { + if elem.Checksum != "" { + cleanImgList = append(cleanImgList, elem) + } + } + } else { + cleanImgList = imgList + } + + imgListJSON, err := json.Marshal(cleanImgList) + if err != nil { + return nil, err + } + var suffix string + if validate { + suffix = "images" + } + u := fmt.Sprintf("%srepositories/%s/%s", r.indexEndpoint.String(), remote.RemoteName(), suffix) + logrus.Debugf("[registry] PUT %s", u) + logrus.Debugf("Image list pushed to index:\n%s", imgListJSON) + headers := map[string][]string{ + "Content-type": {"application/json"}, + // this will set basic auth in r.client.Transport and send cached X-Docker-Token headers for all subsequent requests + "X-Docker-Token": {"true"}, + } + if validate { + headers["X-Docker-Endpoints"] = regs + } + + // Redirect if necessary + var res *http.Response + for { + if res, err = r.putImageRequest(u, headers, imgListJSON); err != nil { + return nil, err + } + if !shouldRedirect(res) { + break + } + res.Body.Close() + u = res.Header.Get("Location") + logrus.Debugf("Redirected to %s", u) + } + defer res.Body.Close() + + if res.StatusCode == 401 { + return nil, errcode.ErrorCodeUnauthorized.WithArgs() + } + + var tokens, endpoints []string + if !validate { + if res.StatusCode != 200 && res.StatusCode != 201 { + errBody, err := ioutil.ReadAll(res.Body) + if err != nil { + logrus.Debugf("Error reading response body: %s", err) + } + return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to push repository %s: %q", res.StatusCode, remote.RemoteName(), errBody), res) + } + tokens = res.Header["X-Docker-Token"] + logrus.Debugf("Auth token: %v", tokens) + + if res.Header.Get("X-Docker-Endpoints") == "" { + return nil, fmt.Errorf("Index response didn't contain any endpoints") + } + endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.String()) + if err != nil { + return nil, err + } + } else { + if res.StatusCode != 204 { + errBody, err := ioutil.ReadAll(res.Body) + if err != nil { + logrus.Debugf("Error reading response body: %s", err) + } + return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to push checksums %s: %q", res.StatusCode, remote.RemoteName(), errBody), res) + } + } + + return &RepositoryData{ + Endpoints: endpoints, + }, nil +} + +func (r *Session) putImageRequest(u string, headers map[string][]string, body []byte) (*http.Response, error) { + req, err := http.NewRequest("PUT", u, bytes.NewReader(body)) + if err != nil { + return nil, err + } + req.ContentLength = int64(len(body)) + for k, v := range headers { + req.Header[k] = v + } + response, err := r.client.Do(req) + if err != nil { + return nil, err + } + return response, nil +} + +func shouldRedirect(response *http.Response) bool { + return response.StatusCode >= 300 && response.StatusCode < 400 +} + +// SearchRepositories performs a search against the remote repository +func (r *Session) SearchRepositories(term string) (*registrytypes.SearchResults, error) { + logrus.Debugf("Index server: %s", r.indexEndpoint) + u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + + req, err := http.NewRequest("GET", u, nil) + if err != nil { + return nil, fmt.Errorf("Error while getting from the server: %v", err) + } + // Have the AuthTransport send authentication, when logged in. + req.Header.Set("X-Docker-Token", "true") + res, err := r.client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != 200 { + return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Unexpected status code %d", res.StatusCode), res) + } + result := new(registrytypes.SearchResults) + return result, json.NewDecoder(res.Body).Decode(result) +} + +// GetAuthConfig returns the authentication settings for a session +// TODO(tiborvass): remove this once registry client v2 is vendored +func (r *Session) GetAuthConfig(withPasswd bool) *types.AuthConfig { + password := "" + if withPasswd { + password = r.authConfig.Password + } + return &types.AuthConfig{ + Username: r.authConfig.Username, + Password: password, + } +} + +func isTimeout(err error) bool { + type timeout interface { + Timeout() bool + } + e := err + switch urlErr := err.(type) { + case *url.Error: + e = urlErr.Err + } + t, ok := e.(timeout) + return ok && t.Timeout() +} diff --git a/registry/types.go b/registry/types.go new file mode 100644 index 00000000..4247fed6 --- /dev/null +++ b/registry/types.go @@ -0,0 +1,70 @@ +package registry + +import ( + "github.com/docker/docker/reference" + registrytypes "github.com/docker/engine-api/types/registry" +) + +// RepositoryData tracks the image list, list of endpoints, and list of tokens +// for a repository +type RepositoryData struct { + // ImgList is a list of images in the repository + ImgList map[string]*ImgData + // Endpoints is a list of endpoints returned in X-Docker-Endpoints + Endpoints []string + // Tokens is currently unused (remove it?) + Tokens []string +} + +// ImgData is used to transfer image checksums to and from the registry +type ImgData struct { + // ID is an opaque string that identifies the image + ID string `json:"id"` + Checksum string `json:"checksum,omitempty"` + ChecksumPayload string `json:"-"` + Tag string `json:",omitempty"` +} + +// PingResult contains the information returned when pinging a registry. It +// indicates the registry's version and whether the registry claims to be a +// standalone registry. +type PingResult struct { + // Version is the registry version supplied by the registry in a HTTP + // header + Version string `json:"version"` + // Standalone is set to true if the registry indicates it is a + // standalone registry in the X-Docker-Registry-Standalone + // header + Standalone bool `json:"standalone"` +} + +// APIVersion is an integral representation of an API version (presently +// either 1 or 2) +type APIVersion int + +func (av APIVersion) String() string { + return apiVersions[av] +} + +// API Version identifiers. +const ( + _ = iota + APIVersion1 APIVersion = iota + APIVersion2 +) + +var apiVersions = map[APIVersion]string{ + APIVersion1: "v1", + APIVersion2: "v2", +} + +// RepositoryInfo describes a repository +type RepositoryInfo struct { + reference.Named + // Index points to registry information + Index *registrytypes.IndexInfo + // Official indicates whether the repository is considered official. + // If the registry is official, and the normalized name does not + // contain a '/' (e.g. "foo"), then it is considered an official repo. + Official bool +} diff --git a/restartmanager/restartmanager.go b/restartmanager/restartmanager.go new file mode 100644 index 00000000..0e844de2 --- /dev/null +++ b/restartmanager/restartmanager.go @@ -0,0 +1,131 @@ +package restartmanager + +import ( + "errors" + "fmt" + "sync" + "time" + + "github.com/docker/engine-api/types/container" +) + +const ( + backoffMultiplier = 2 + defaultTimeout = 100 * time.Millisecond +) + +// ErrRestartCanceled is returned when the restart manager has been +// canceled and will no longer restart the container. +var ErrRestartCanceled = errors.New("restart canceled") + +// RestartManager defines object that controls container restarting rules. +type RestartManager interface { + Cancel() error + ShouldRestart(exitCode uint32, hasBeenManuallyStopped bool, executionDuration time.Duration) (bool, chan error, error) +} + +type restartManager struct { + sync.Mutex + sync.Once + policy container.RestartPolicy + failureCount int + timeout time.Duration + active bool + cancel chan struct{} + canceled bool +} + +// New returns a new restartmanager based on a policy. +func New(policy container.RestartPolicy) RestartManager { + return &restartManager{policy: policy, cancel: make(chan struct{})} +} + +func (rm *restartManager) SetPolicy(policy container.RestartPolicy) { + rm.Lock() + rm.policy = policy + rm.Unlock() +} + +func (rm *restartManager) ShouldRestart(exitCode uint32, hasBeenManuallyStopped bool, executionDuration time.Duration) (bool, chan error, error) { + if rm.policy.IsNone() { + return false, nil, nil + } + rm.Lock() + unlockOnExit := true + defer func() { + if unlockOnExit { + rm.Unlock() + } + }() + + if rm.canceled { + return false, nil, ErrRestartCanceled + } + + if rm.active { + return false, nil, fmt.Errorf("invalid call on active restartmanager") + } + + if exitCode != 0 { + rm.failureCount++ + } else { + rm.failureCount = 0 + } + + // if the container ran for more than 10s, reguardless of status and policy reset the + // the timeout back to the default. + if executionDuration.Seconds() >= 10 { + rm.timeout = 0 + } + if rm.timeout == 0 { + rm.timeout = defaultTimeout + } else { + rm.timeout *= backoffMultiplier + } + + var restart bool + switch { + case rm.policy.IsAlways(), rm.policy.IsUnlessStopped(): + restart = true + case rm.policy.IsOnFailure(): + // the default value of 0 for MaximumRetryCount means that we will not enforce a maximum count + if max := rm.policy.MaximumRetryCount; max == 0 || rm.failureCount <= max { + restart = exitCode != 0 + } + } + + if !restart { + rm.active = false + return false, nil, nil + } + + unlockOnExit = false + rm.active = true + rm.Unlock() + + ch := make(chan error) + go func() { + select { + case <-rm.cancel: + ch <- ErrRestartCanceled + close(ch) + case <-time.After(rm.timeout): + rm.Lock() + close(ch) + rm.active = false + rm.Unlock() + } + }() + + return true, ch, nil +} + +func (rm *restartManager) Cancel() error { + rm.Do(func() { + rm.Lock() + rm.canceled = true + close(rm.cancel) + rm.Unlock() + }) + return nil +} diff --git a/restartmanager/restartmanager_test.go b/restartmanager/restartmanager_test.go new file mode 100644 index 00000000..9c9f805e --- /dev/null +++ b/restartmanager/restartmanager_test.go @@ -0,0 +1,34 @@ +package restartmanager + +import ( + "testing" + "time" + + "github.com/docker/engine-api/types/container" +) + +func TestRestartManagerTimeout(t *testing.T) { + rm := New(container.RestartPolicy{Name: "always", MaximumRetryCount: 0}).(*restartManager) + should, _, err := rm.ShouldRestart(0, false, 1*time.Second) + if err != nil { + t.Fatal(err) + } + if !should { + t.Fatal("container should be restarted") + } + if rm.timeout != 100*time.Millisecond { + t.Fatalf("restart manager should have a timeout of 100ms but has %s", rm.timeout) + } +} + +func TestRestartManagerTimeoutReset(t *testing.T) { + rm := New(container.RestartPolicy{Name: "always", MaximumRetryCount: 0}).(*restartManager) + rm.timeout = 5 * time.Second + _, _, err := rm.ShouldRestart(0, false, 10*time.Second) + if err != nil { + t.Fatal(err) + } + if rm.timeout != 100*time.Millisecond { + t.Fatalf("restart manager should have a timeout of 100ms but has %s", rm.timeout) + } +} diff --git a/runconfig/compare.go b/runconfig/compare.go new file mode 100644 index 00000000..61346aab --- /dev/null +++ b/runconfig/compare.go @@ -0,0 +1,61 @@ +package runconfig + +import "github.com/docker/engine-api/types/container" + +// Compare two Config struct. Do not compare the "Image" nor "Hostname" fields +// If OpenStdin is set, then it differs +func Compare(a, b *container.Config) bool { + if a == nil || b == nil || + a.OpenStdin || b.OpenStdin { + return false + } + if a.AttachStdout != b.AttachStdout || + a.AttachStderr != b.AttachStderr || + a.User != b.User || + a.OpenStdin != b.OpenStdin || + a.Tty != b.Tty { + return false + } + + if len(a.Cmd) != len(b.Cmd) || + len(a.Env) != len(b.Env) || + len(a.Labels) != len(b.Labels) || + len(a.ExposedPorts) != len(b.ExposedPorts) || + len(a.Entrypoint) != len(b.Entrypoint) || + len(a.Volumes) != len(b.Volumes) { + return false + } + + for i := 0; i < len(a.Cmd); i++ { + if a.Cmd[i] != b.Cmd[i] { + return false + } + } + for i := 0; i < len(a.Env); i++ { + if a.Env[i] != b.Env[i] { + return false + } + } + for k, v := range a.Labels { + if v != b.Labels[k] { + return false + } + } + for k := range a.ExposedPorts { + if _, exists := b.ExposedPorts[k]; !exists { + return false + } + } + + for i := 0; i < len(a.Entrypoint); i++ { + if a.Entrypoint[i] != b.Entrypoint[i] { + return false + } + } + for key := range a.Volumes { + if _, exists := b.Volumes[key]; !exists { + return false + } + } + return true +} diff --git a/runconfig/compare_test.go b/runconfig/compare_test.go new file mode 100644 index 00000000..9c17c553 --- /dev/null +++ b/runconfig/compare_test.go @@ -0,0 +1,126 @@ +package runconfig + +import ( + "testing" + + "github.com/docker/engine-api/types/container" + "github.com/docker/engine-api/types/strslice" + "github.com/docker/go-connections/nat" +) + +// Just to make life easier +func newPortNoError(proto, port string) nat.Port { + p, _ := nat.NewPort(proto, port) + return p +} + +func TestCompare(t *testing.T) { + ports1 := make(nat.PortSet) + ports1[newPortNoError("tcp", "1111")] = struct{}{} + ports1[newPortNoError("tcp", "2222")] = struct{}{} + ports2 := make(nat.PortSet) + ports2[newPortNoError("tcp", "3333")] = struct{}{} + ports2[newPortNoError("tcp", "4444")] = struct{}{} + ports3 := make(nat.PortSet) + ports3[newPortNoError("tcp", "1111")] = struct{}{} + ports3[newPortNoError("tcp", "2222")] = struct{}{} + ports3[newPortNoError("tcp", "5555")] = struct{}{} + volumes1 := make(map[string]struct{}) + volumes1["/test1"] = struct{}{} + volumes2 := make(map[string]struct{}) + volumes2["/test2"] = struct{}{} + volumes3 := make(map[string]struct{}) + volumes3["/test1"] = struct{}{} + volumes3["/test3"] = struct{}{} + envs1 := []string{"ENV1=value1", "ENV2=value2"} + envs2 := []string{"ENV1=value1", "ENV3=value3"} + entrypoint1 := strslice.StrSlice{"/bin/sh", "-c"} + entrypoint2 := strslice.StrSlice{"/bin/sh", "-d"} + entrypoint3 := strslice.StrSlice{"/bin/sh", "-c", "echo"} + cmd1 := strslice.StrSlice{"/bin/sh", "-c"} + cmd2 := strslice.StrSlice{"/bin/sh", "-d"} + cmd3 := strslice.StrSlice{"/bin/sh", "-c", "echo"} + labels1 := map[string]string{"LABEL1": "value1", "LABEL2": "value2"} + labels2 := map[string]string{"LABEL1": "value1", "LABEL2": "value3"} + labels3 := map[string]string{"LABEL1": "value1", "LABEL2": "value2", "LABEL3": "value3"} + + sameConfigs := map[*container.Config]*container.Config{ + // Empty config + &container.Config{}: {}, + // Does not compare hostname, domainname & image + &container.Config{ + Hostname: "host1", + Domainname: "domain1", + Image: "image1", + User: "user", + }: { + Hostname: "host2", + Domainname: "domain2", + Image: "image2", + User: "user", + }, + // only OpenStdin + &container.Config{OpenStdin: false}: {OpenStdin: false}, + // only env + &container.Config{Env: envs1}: {Env: envs1}, + // only cmd + &container.Config{Cmd: cmd1}: {Cmd: cmd1}, + // only labels + &container.Config{Labels: labels1}: {Labels: labels1}, + // only exposedPorts + &container.Config{ExposedPorts: ports1}: {ExposedPorts: ports1}, + // only entrypoints + &container.Config{Entrypoint: entrypoint1}: {Entrypoint: entrypoint1}, + // only volumes + &container.Config{Volumes: volumes1}: {Volumes: volumes1}, + } + differentConfigs := map[*container.Config]*container.Config{ + nil: nil, + &container.Config{ + Hostname: "host1", + Domainname: "domain1", + Image: "image1", + User: "user1", + }: { + Hostname: "host1", + Domainname: "domain1", + Image: "image1", + User: "user2", + }, + // only OpenStdin + &container.Config{OpenStdin: false}: {OpenStdin: true}, + &container.Config{OpenStdin: true}: {OpenStdin: false}, + // only env + &container.Config{Env: envs1}: {Env: envs2}, + // only cmd + &container.Config{Cmd: cmd1}: {Cmd: cmd2}, + // not the same number of parts + &container.Config{Cmd: cmd1}: {Cmd: cmd3}, + // only labels + &container.Config{Labels: labels1}: {Labels: labels2}, + // not the same number of labels + &container.Config{Labels: labels1}: {Labels: labels3}, + // only exposedPorts + &container.Config{ExposedPorts: ports1}: {ExposedPorts: ports2}, + // not the same number of ports + &container.Config{ExposedPorts: ports1}: {ExposedPorts: ports3}, + // only entrypoints + &container.Config{Entrypoint: entrypoint1}: {Entrypoint: entrypoint2}, + // not the same number of parts + &container.Config{Entrypoint: entrypoint1}: {Entrypoint: entrypoint3}, + // only volumes + &container.Config{Volumes: volumes1}: {Volumes: volumes2}, + // not the same number of labels + &container.Config{Volumes: volumes1}: {Volumes: volumes3}, + } + for config1, config2 := range sameConfigs { + if !Compare(config1, config2) { + t.Fatalf("Compare should be true for [%v] and [%v]", config1, config2) + } + } + for config1, config2 := range differentConfigs { + if Compare(config1, config2) { + t.Fatalf("Compare should be false for [%v] and [%v]", config1, config2) + } + } +} diff --git a/runconfig/config.go b/runconfig/config.go new file mode 100644 index 00000000..9f3f9c5e --- /dev/null +++ b/runconfig/config.go @@ -0,0 +1,71 @@ +package runconfig + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/docker/docker/volume" + "github.com/docker/engine-api/types/container" + networktypes "github.com/docker/engine-api/types/network" +) + +// DecodeContainerConfig decodes a json encoded config into a ContainerConfigWrapper +// struct and returns both a Config and an HostConfig struct +// Be aware this function is not checking whether the resulted structs are nil, +// it's your business to do so +func DecodeContainerConfig(src io.Reader) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) { + var w ContainerConfigWrapper + + decoder := json.NewDecoder(src) + if err := decoder.Decode(&w); err != nil { + return nil, nil, nil, err + } + + hc := w.getHostConfig() + + // Perform platform-specific processing of Volumes and Binds. + if w.Config != nil && hc != nil { + + // Initialize the volumes map if currently nil + if w.Config.Volumes == nil { + w.Config.Volumes = make(map[string]struct{}) + } + + // Now validate all the volumes and binds + if err := validateVolumesAndBindSettings(w.Config, hc); err != nil { + return nil, nil, nil, err + } + } + + // Certain parameters need daemon-side validation that cannot be done + // on the client, as only the daemon knows what is valid for the platform. + if err := ValidateNetMode(w.Config, hc); err != nil { + return nil, nil, nil, err + } + + // Validate isolation + if err := ValidateIsolation(hc); err != nil { + return nil, nil, nil, err + } + return w.Config, hc, w.NetworkingConfig, nil +} + +// validateVolumesAndBindSettings validates each of the volumes and bind settings +// passed by the caller to ensure they are valid. +func validateVolumesAndBindSettings(c *container.Config, hc *container.HostConfig) error { + + // Ensure all volumes and binds are valid. + for spec := range c.Volumes { + if _, err := volume.ParseMountSpec(spec, hc.VolumeDriver); err != nil { + return fmt.Errorf("Invalid volume spec %q: %v", spec, err) + } + } + for _, spec := range hc.Binds { + if _, err := volume.ParseMountSpec(spec, hc.VolumeDriver); err != nil { + return fmt.Errorf("Invalid bind mount spec %q: %v", spec, err) + } + } + + return nil +} diff --git a/runconfig/config_test.go b/runconfig/config_test.go new file mode 100644 index 00000000..5804b12d --- /dev/null +++ b/runconfig/config_test.go @@ -0,0 +1,134 @@ +package runconfig + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "runtime" + "strings" + "testing" + + "github.com/docker/engine-api/types/container" + networktypes "github.com/docker/engine-api/types/network" + "github.com/docker/engine-api/types/strslice" +) + +type f struct { + file string + entrypoint strslice.StrSlice +} + +func TestDecodeContainerConfig(t *testing.T) { + + var ( + fixtures []f + image string + ) + + if runtime.GOOS != "windows" { + image = "ubuntu" + fixtures = []f{ + {"fixtures/unix/container_config_1_14.json", strslice.StrSlice{}}, + {"fixtures/unix/container_config_1_17.json", strslice.StrSlice{"bash"}}, + {"fixtures/unix/container_config_1_19.json", strslice.StrSlice{"bash"}}, + } + } else { + image = "windows" + fixtures = []f{ + {"fixtures/windows/container_config_1_19.json", strslice.StrSlice{"cmd"}}, + } + } + + for _, f := range fixtures { + b, err := ioutil.ReadFile(f.file) + if err != nil { + t.Fatal(err) + } + + c, h, _, err := DecodeContainerConfig(bytes.NewReader(b)) + if err != nil { + t.Fatal(fmt.Errorf("Error parsing %s: %v", f, err)) + } + + if c.Image != image { + t.Fatalf("Expected %s image, found %s\n", image, c.Image) + } + + if len(c.Entrypoint) != len(f.entrypoint) { + t.Fatalf("Expected %v, found %v\n", f.entrypoint, c.Entrypoint) + } + + if h != nil && h.Memory != 1000 { + t.Fatalf("Expected memory to be 1000, found %d\n", h.Memory) + } + } +} + +// TestDecodeContainerConfigIsolation validates isolation passed +// to the daemon in the hostConfig structure. Note this is platform specific +// as to what level of container isolation is supported. +func TestDecodeContainerConfigIsolation(t *testing.T) { + + // An invalid isolation level + if _, _, _, err := callDecodeContainerConfigIsolation("invalid"); err != nil { + if !strings.Contains(err.Error(), `invalid --isolation: "invalid"`) { + t.Fatal(err) + } + } + + // Blank isolation (== default) + if _, _, _, err := callDecodeContainerConfigIsolation(""); err != nil { + t.Fatal("Blank isolation should have succeeded") + } + + // Default isolation + if _, _, _, err := callDecodeContainerConfigIsolation("default"); err != nil { + t.Fatal("default isolation should have succeeded") + } + + // Process isolation (Valid on Windows only) + if runtime.GOOS == "windows" { + if _, _, _, err := callDecodeContainerConfigIsolation("process"); err != nil { + t.Fatal("process isolation should have succeeded") + } + } else { + if _, _, _, err := callDecodeContainerConfigIsolation("process"); err != nil { + if !strings.Contains(err.Error(), `invalid --isolation: "process"`) { + t.Fatal(err) + } + } + } + + // Hyper-V Containers isolation (Valid on Windows only) + if runtime.GOOS == "windows" { + if _, _, _, err := callDecodeContainerConfigIsolation("hyperv"); err != nil { + t.Fatal("hyperv isolation should have succeeded") + } + } else { + if _, _, _, err := callDecodeContainerConfigIsolation("hyperv"); err != nil { + if !strings.Contains(err.Error(), `invalid --isolation: "hyperv"`) { + t.Fatal(err) + } + } + } +} + +// callDecodeContainerConfigIsolation is a utility function to call +// DecodeContainerConfig for validating isolation +func callDecodeContainerConfigIsolation(isolation string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) { + var ( + b []byte + err error + ) + w := ContainerConfigWrapper{ + Config: &container.Config{}, + HostConfig: &container.HostConfig{ + NetworkMode: "none", + Isolation: container.Isolation(isolation)}, + } + if b, err = json.Marshal(w); err != nil { + return nil, nil, nil, fmt.Errorf("Error on marshal %s", err.Error()) + } + return DecodeContainerConfig(bytes.NewReader(b)) +} diff --git a/runconfig/config_unix.go b/runconfig/config_unix.go new file mode 100644 index 00000000..e5902fb0 --- /dev/null +++ b/runconfig/config_unix.go @@ -0,0 +1,59 @@ +// +build !windows + +package runconfig + +import ( + "github.com/docker/engine-api/types/container" + networktypes "github.com/docker/engine-api/types/network" +) + +// ContainerConfigWrapper is a Config wrapper that holds the container Config (portable) +// and the corresponding HostConfig (non-portable). +type ContainerConfigWrapper struct { + *container.Config + InnerHostConfig *container.HostConfig `json:"HostConfig,omitempty"` + Cpuset string `json:",omitempty"` // Deprecated. Exported for backwards compatibility. + NetworkingConfig *networktypes.NetworkingConfig `json:"NetworkingConfig,omitempty"` + *container.HostConfig // Deprecated. Exported to read attributes from json that are not in the inner host config structure. +} + +// getHostConfig gets the HostConfig of the Config. +// It's mostly there to handle Deprecated fields of the ContainerConfigWrapper +func (w *ContainerConfigWrapper) getHostConfig() *container.HostConfig { + hc := w.HostConfig + + if hc == nil && w.InnerHostConfig != nil { + hc = w.InnerHostConfig + } else if w.InnerHostConfig != nil { + if hc.Memory != 0 && w.InnerHostConfig.Memory == 0 { + w.InnerHostConfig.Memory = hc.Memory + } + if hc.MemorySwap != 0 && w.InnerHostConfig.MemorySwap == 0 { + w.InnerHostConfig.MemorySwap = hc.MemorySwap + } + if hc.CPUShares != 0 && w.InnerHostConfig.CPUShares == 0 { + w.InnerHostConfig.CPUShares = hc.CPUShares + } + if hc.CpusetCpus != "" && w.InnerHostConfig.CpusetCpus == "" { + w.InnerHostConfig.CpusetCpus = hc.CpusetCpus + } + + if hc.VolumeDriver != "" && w.InnerHostConfig.VolumeDriver == "" { + w.InnerHostConfig.VolumeDriver = hc.VolumeDriver + } + + hc = w.InnerHostConfig + } + + if hc != nil { + if w.Cpuset != "" && hc.CpusetCpus == "" { + hc.CpusetCpus = w.Cpuset + } + } + + // Make sure NetworkMode has an acceptable value. We do this to ensure + // backwards compatible API behavior. + hc = SetDefaultNetModeIfBlank(hc) + + return hc +} diff --git a/runconfig/config_windows.go b/runconfig/config_windows.go new file mode 100644 index 00000000..50a52380 --- /dev/null +++ b/runconfig/config_windows.go @@ -0,0 +1,19 @@ +package runconfig + +import ( + "github.com/docker/engine-api/types/container" + networktypes "github.com/docker/engine-api/types/network" +) + +// ContainerConfigWrapper is a Config wrapper that holds the container Config (portable) +// and the corresponding HostConfig (non-portable). +type ContainerConfigWrapper struct { + *container.Config + HostConfig *container.HostConfig `json:"HostConfig,omitempty"` + NetworkingConfig *networktypes.NetworkingConfig `json:"NetworkingConfig,omitempty"` +} + +// getHostConfig gets the HostConfig of the Config. +func (w *ContainerConfigWrapper) getHostConfig() *container.HostConfig { + return w.HostConfig +} diff --git a/runconfig/errors.go b/runconfig/errors.go new file mode 100644 index 00000000..d3608576 --- /dev/null +++ b/runconfig/errors.go @@ -0,0 +1,40 @@ +package runconfig + +import ( + "fmt" +) + +var ( + // ErrConflictContainerNetworkAndLinks conflict between --net=container and links + ErrConflictContainerNetworkAndLinks = fmt.Errorf("Conflicting options: container type network can't be used with links. This would result in undefined behavior") + // ErrConflictUserDefinedNetworkAndLinks conflict between --net= and links + ErrConflictUserDefinedNetworkAndLinks = fmt.Errorf("Conflicting options: networking can't be used with links. This would result in undefined behavior") + // ErrConflictSharedNetwork conflict between private and other networks + ErrConflictSharedNetwork = fmt.Errorf("Container sharing network namespace with another container or host cannot be connected to any other network") + // ErrConflictHostNetwork conflict from being disconnected from host network or connected to host network. + ErrConflictHostNetwork = fmt.Errorf("Container cannot be disconnected from host network or connected to host network") + // ErrConflictNoNetwork conflict between private and other networks + ErrConflictNoNetwork = fmt.Errorf("Container cannot be connected to multiple networks with one of the networks in private (none) mode") + // ErrConflictNetworkAndDNS conflict between --dns and the network mode + ErrConflictNetworkAndDNS = fmt.Errorf("Conflicting options: dns and the network mode") + // ErrConflictNetworkHostname conflict between the hostname and the network mode + ErrConflictNetworkHostname = fmt.Errorf("Conflicting options: hostname and the network mode") + // ErrConflictHostNetworkAndLinks conflict between --net=host and links + ErrConflictHostNetworkAndLinks = fmt.Errorf("Conflicting options: host type networking can't be used with links. This would result in undefined behavior") + // ErrConflictContainerNetworkAndMac conflict between the mac address and the network mode + ErrConflictContainerNetworkAndMac = fmt.Errorf("Conflicting options: mac-address and the network mode") + // ErrConflictNetworkHosts conflict between add-host and the network mode + ErrConflictNetworkHosts = fmt.Errorf("Conflicting options: custom host-to-IP mapping and the network mode") + // ErrConflictNetworkPublishPorts conflict between the publish options and the network mode + ErrConflictNetworkPublishPorts = fmt.Errorf("Conflicting options: port publishing and the container type network mode") + // ErrConflictNetworkExposePorts conflict between the expose option and the network mode + ErrConflictNetworkExposePorts = fmt.Errorf("Conflicting options: port exposing and the container type network mode") + // ErrUnsupportedNetworkAndIP conflict between network mode and requested ip address + ErrUnsupportedNetworkAndIP = fmt.Errorf("User specified IP address is supported on user defined networks only") + // ErrUnsupportedNetworkNoSubnetAndIP conflict between network with no configured subnet and requested ip address + ErrUnsupportedNetworkNoSubnetAndIP = fmt.Errorf("User specified IP address is supported only when connecting to networks with user configured subnets") + // ErrUnsupportedNetworkAndAlias conflict between network mode and alias + ErrUnsupportedNetworkAndAlias = fmt.Errorf("Network-scoped alias is supported only for containers in user defined networks") + // ErrConflictUTSHostname conflict between the hostname and the UTS mode + ErrConflictUTSHostname = fmt.Errorf("Conflicting options: hostname and the UTS mode") +) diff --git a/runconfig/fixtures/unix/container_config_1_14.json b/runconfig/fixtures/unix/container_config_1_14.json new file mode 100644 index 00000000..b08334c0 --- /dev/null +++ b/runconfig/fixtures/unix/container_config_1_14.json @@ -0,0 +1,30 @@ +{ + "Hostname":"", + "Domainname": "", + "User":"", + "Memory": 1000, + "MemorySwap":0, + "CpuShares": 512, + "Cpuset": "0,1", + "AttachStdin":false, + "AttachStdout":true, + "AttachStderr":true, + "PortSpecs":null, + "Tty":false, + "OpenStdin":false, + "StdinOnce":false, + "Env":null, + "Cmd":[ + "bash" + ], + "Image":"ubuntu", + "Volumes":{ + "/tmp": {} + }, + "WorkingDir":"", + "NetworkDisabled": false, + "ExposedPorts":{ + "22/tcp": {} + }, + "RestartPolicy": { "Name": "always" } +} diff --git a/runconfig/fixtures/unix/container_config_1_17.json b/runconfig/fixtures/unix/container_config_1_17.json new file mode 100644 index 00000000..0d780877 --- /dev/null +++ b/runconfig/fixtures/unix/container_config_1_17.json @@ -0,0 +1,50 @@ +{ + "Hostname": "", + "Domainname": "", + "User": "", + "Memory": 1000, + "MemorySwap": 0, + "CpuShares": 512, + "Cpuset": "0,1", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "date" + ], + "Entrypoint": "bash", + "Image": "ubuntu", + "Volumes": { + "/tmp": {} + }, + "WorkingDir": "", + "NetworkDisabled": false, + "MacAddress": "12:34:56:78:9a:bc", + "ExposedPorts": { + "22/tcp": {} + }, + "SecurityOpt": [""], + "HostConfig": { + "Binds": ["/tmp:/tmp"], + "Links": ["redis3:redis"], + "LxcConf": {"lxc.utsname":"docker"}, + "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] }, + "PublishAllPorts": false, + "Privileged": false, + "ReadonlyRootfs": false, + "Dns": ["8.8.8.8"], + "DnsSearch": [""], + "DnsOptions": [""], + "ExtraHosts": null, + "VolumesFrom": ["parent", "other:ro"], + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"], + "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 }, + "NetworkMode": "bridge", + "Devices": [] + } +} diff --git a/runconfig/fixtures/unix/container_config_1_19.json b/runconfig/fixtures/unix/container_config_1_19.json new file mode 100644 index 00000000..de49cf32 --- /dev/null +++ b/runconfig/fixtures/unix/container_config_1_19.json @@ -0,0 +1,58 @@ +{ + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "date" + ], + "Entrypoint": "bash", + "Image": "ubuntu", + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "Volumes": { + "/tmp": {} + }, + "WorkingDir": "", + "NetworkDisabled": false, + "MacAddress": "12:34:56:78:9a:bc", + "ExposedPorts": { + "22/tcp": {} + }, + "HostConfig": { + "Binds": ["/tmp:/tmp"], + "Links": ["redis3:redis"], + "LxcConf": {"lxc.utsname":"docker"}, + "Memory": 1000, + "MemorySwap": 0, + "CpuShares": 512, + "CpusetCpus": "0,1", + "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] }, + "PublishAllPorts": false, + "Privileged": false, + "ReadonlyRootfs": false, + "Dns": ["8.8.8.8"], + "DnsSearch": [""], + "DnsOptions": [""], + "ExtraHosts": null, + "VolumesFrom": ["parent", "other:ro"], + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"], + "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 }, + "NetworkMode": "bridge", + "Devices": [], + "Ulimits": [{}], + "LogConfig": { "Type": "json-file", "Config": {} }, + "SecurityOpt": [""], + "CgroupParent": "" + } +} diff --git a/runconfig/fixtures/unix/container_hostconfig_1_14.json b/runconfig/fixtures/unix/container_hostconfig_1_14.json new file mode 100644 index 00000000..c72ac91c --- /dev/null +++ b/runconfig/fixtures/unix/container_hostconfig_1_14.json @@ -0,0 +1,18 @@ +{ + "Binds": ["/tmp:/tmp"], + "ContainerIDFile": "", + "LxcConf": [], + "Privileged": false, + "PortBindings": { + "80/tcp": [ + { + "HostIp": "0.0.0.0", + "HostPort": "49153" + } + ] + }, + "Links": ["/name:alias"], + "PublishAllPorts": false, + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"] +} diff --git a/runconfig/fixtures/unix/container_hostconfig_1_19.json b/runconfig/fixtures/unix/container_hostconfig_1_19.json new file mode 100644 index 00000000..5ca8aa7e --- /dev/null +++ b/runconfig/fixtures/unix/container_hostconfig_1_19.json @@ -0,0 +1,30 @@ +{ + "Binds": ["/tmp:/tmp"], + "Links": ["redis3:redis"], + "LxcConf": {"lxc.utsname":"docker"}, + "Memory": 0, + "MemorySwap": 0, + "CpuShares": 512, + "CpuPeriod": 100000, + "CpusetCpus": "0,1", + "CpusetMems": "0,1", + "BlkioWeight": 300, + "OomKillDisable": false, + "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] }, + "PublishAllPorts": false, + "Privileged": false, + "ReadonlyRootfs": false, + "Dns": ["8.8.8.8"], + "DnsSearch": [""], + "ExtraHosts": null, + "VolumesFrom": ["parent", "other:ro"], + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"], + "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 }, + "NetworkMode": "bridge", + "Devices": [], + "Ulimits": [{}], + "LogConfig": { "Type": "json-file", "Config": {} }, + "SecurityOpt": [""], + "CgroupParent": "" +} diff --git a/runconfig/fixtures/windows/container_config_1_19.json b/runconfig/fixtures/windows/container_config_1_19.json new file mode 100644 index 00000000..724320c7 --- /dev/null +++ b/runconfig/fixtures/windows/container_config_1_19.json @@ -0,0 +1,58 @@ +{ + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "date" + ], + "Entrypoint": "cmd", + "Image": "windows", + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "Volumes": { + "c:/windows": {} + }, + "WorkingDir": "", + "NetworkDisabled": false, + "MacAddress": "12:34:56:78:9a:bc", + "ExposedPorts": { + "22/tcp": {} + }, + "HostConfig": { + "Binds": ["c:/windows:d:/tmp"], + "Links": ["redis3:redis"], + "LxcConf": {"lxc.utsname":"docker"}, + "Memory": 1000, + "MemorySwap": 0, + "CpuShares": 512, + "CpusetCpus": "0,1", + "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] }, + "PublishAllPorts": false, + "Privileged": false, + "ReadonlyRootfs": false, + "Dns": ["8.8.8.8"], + "DnsSearch": [""], + "DnsOptions": [""], + "ExtraHosts": null, + "VolumesFrom": ["parent", "other:ro"], + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"], + "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 }, + "NetworkMode": "default", + "Devices": [], + "Ulimits": [{}], + "LogConfig": { "Type": "json-file", "Config": {} }, + "SecurityOpt": [""], + "CgroupParent": "" + } +} diff --git a/runconfig/hostconfig.go b/runconfig/hostconfig.go new file mode 100644 index 00000000..769cc9f5 --- /dev/null +++ b/runconfig/hostconfig.go @@ -0,0 +1,35 @@ +package runconfig + +import ( + "encoding/json" + "io" + + "github.com/docker/engine-api/types/container" +) + +// DecodeHostConfig creates a HostConfig based on the specified Reader. +// It assumes the content of the reader will be JSON, and decodes it. +func DecodeHostConfig(src io.Reader) (*container.HostConfig, error) { + decoder := json.NewDecoder(src) + + var w ContainerConfigWrapper + if err := decoder.Decode(&w); err != nil { + return nil, err + } + + hc := w.getHostConfig() + return hc, nil +} + +// SetDefaultNetModeIfBlank changes the NetworkMode in a HostConfig structure +// to default if it is not populated. This ensures backwards compatibility after +// the validation of the network mode was moved from the docker CLI to the +// docker daemon. +func SetDefaultNetModeIfBlank(hc *container.HostConfig) *container.HostConfig { + if hc != nil { + if hc.NetworkMode == container.NetworkMode("") { + hc.NetworkMode = container.NetworkMode("default") + } + } + return hc +} diff --git a/runconfig/hostconfig_test.go b/runconfig/hostconfig_test.go new file mode 100644 index 00000000..e14443ba --- /dev/null +++ b/runconfig/hostconfig_test.go @@ -0,0 +1,222 @@ +// +build !windows + +package runconfig + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/engine-api/types/container" +) + +// TODO Windows: This will need addressing for a Windows daemon. +func TestNetworkModeTest(t *testing.T) { + networkModes := map[container.NetworkMode][]bool{ + // private, bridge, host, container, none, default + "": {true, false, false, false, false, false}, + "something:weird": {true, false, false, false, false, false}, + "bridge": {true, true, false, false, false, false}, + DefaultDaemonNetworkMode(): {true, true, false, false, false, false}, + "host": {false, false, true, false, false, false}, + "container:name": {false, false, false, true, false, false}, + "none": {true, false, false, false, true, false}, + "default": {true, false, false, false, false, true}, + } + networkModeNames := map[container.NetworkMode]string{ + "": "", + "something:weird": "something:weird", + "bridge": "bridge", + DefaultDaemonNetworkMode(): "bridge", + "host": "host", + "container:name": "container", + "none": "none", + "default": "default", + } + for networkMode, state := range networkModes { + if networkMode.IsPrivate() != state[0] { + t.Fatalf("NetworkMode.IsPrivate for %v should have been %v but was %v", networkMode, state[0], networkMode.IsPrivate()) + } + if networkMode.IsBridge() != state[1] { + t.Fatalf("NetworkMode.IsBridge for %v should have been %v but was %v", networkMode, state[1], networkMode.IsBridge()) + } + if networkMode.IsHost() != state[2] { + t.Fatalf("NetworkMode.IsHost for %v should have been %v but was %v", networkMode, state[2], networkMode.IsHost()) + } + if networkMode.IsContainer() != state[3] { + t.Fatalf("NetworkMode.IsContainer for %v should have been %v but was %v", networkMode, state[3], networkMode.IsContainer()) + } + if networkMode.IsNone() != state[4] { + t.Fatalf("NetworkMode.IsNone for %v should have been %v but was %v", networkMode, state[4], networkMode.IsNone()) + } + if networkMode.IsDefault() != state[5] { + t.Fatalf("NetworkMode.IsDefault for %v should have been %v but was %v", networkMode, state[5], networkMode.IsDefault()) + } + if networkMode.NetworkName() != networkModeNames[networkMode] { + t.Fatalf("Expected name %v, got %v", networkModeNames[networkMode], networkMode.NetworkName()) + } + } +} + +func TestIpcModeTest(t *testing.T) { + ipcModes := map[container.IpcMode][]bool{ + // private, host, container, valid + "": {true, false, false, true}, + "something:weird": {true, false, false, false}, + ":weird": {true, false, false, true}, + "host": {false, true, false, true}, + "container:name": {false, false, true, true}, + "container:name:something": {false, false, true, false}, + "container:": {false, false, true, false}, + } + for ipcMode, state := range ipcModes { + if ipcMode.IsPrivate() != state[0] { + t.Fatalf("IpcMode.IsPrivate for %v should have been %v but was %v", ipcMode, state[0], ipcMode.IsPrivate()) + } + if ipcMode.IsHost() != state[1] { + t.Fatalf("IpcMode.IsHost for %v should have been %v but was %v", ipcMode, state[1], ipcMode.IsHost()) + } + if ipcMode.IsContainer() != state[2] { + t.Fatalf("IpcMode.IsContainer for %v should have been %v but was %v", ipcMode, state[2], ipcMode.IsContainer()) + } + if ipcMode.Valid() != state[3] { + t.Fatalf("IpcMode.Valid for %v should have been %v but was %v", ipcMode, state[3], ipcMode.Valid()) + } + } + containerIpcModes := map[container.IpcMode]string{ + "": "", + "something": "", + "something:weird": "weird", + "container": "", + "container:": "", + "container:name": "name", + "container:name1:name2": "name1:name2", + } + for ipcMode, container := range containerIpcModes { + if ipcMode.Container() != container { + t.Fatalf("Expected %v for %v but was %v", container, ipcMode, ipcMode.Container()) + } + } +} + +func TestUTSModeTest(t *testing.T) { + utsModes := map[container.UTSMode][]bool{ + // private, host, valid + "": {true, false, true}, + "something:weird": {true, false, false}, + "host": {false, true, true}, + "host:name": {true, false, true}, + } + for utsMode, state := range utsModes { + if utsMode.IsPrivate() != state[0] { + t.Fatalf("UtsMode.IsPrivate for %v should have been %v but was %v", utsMode, state[0], utsMode.IsPrivate()) + } + if utsMode.IsHost() != state[1] { + t.Fatalf("UtsMode.IsHost for %v should have been %v but was %v", utsMode, state[1], utsMode.IsHost()) + } + if utsMode.Valid() != state[2] { + t.Fatalf("UtsMode.Valid for %v should have been %v but was %v", utsMode, state[2], utsMode.Valid()) + } + } +} + +func TestUsernsModeTest(t *testing.T) { + usrensMode := map[container.UsernsMode][]bool{ + // private, host, valid + "": {true, false, true}, + "something:weird": {true, false, false}, + "host": {false, true, true}, + "host:name": {true, false, true}, + } + for usernsMode, state := range usrensMode { + if usernsMode.IsPrivate() != state[0] { + t.Fatalf("UsernsMode.IsPrivate for %v should have been %v but was %v", usernsMode, state[0], usernsMode.IsPrivate()) + } + if usernsMode.IsHost() != state[1] { + t.Fatalf("UsernsMode.IsHost for %v should have been %v but was %v", usernsMode, state[1], usernsMode.IsHost()) + } + if usernsMode.Valid() != state[2] { + t.Fatalf("UsernsMode.Valid for %v should have been %v but was %v", usernsMode, state[2], usernsMode.Valid()) + } + } +} + +func TestPidModeTest(t *testing.T) { + pidModes := map[container.PidMode][]bool{ + // private, host, valid + "": {true, false, true}, + "something:weird": {true, false, false}, + "host": {false, true, true}, + "host:name": {true, false, true}, + } + for pidMode, state := range pidModes { + if pidMode.IsPrivate() != state[0] { + t.Fatalf("PidMode.IsPrivate for %v should have been %v but was %v", pidMode, state[0], pidMode.IsPrivate()) + } + if pidMode.IsHost() != state[1] { + t.Fatalf("PidMode.IsHost for %v should have been %v but was %v", pidMode, state[1], pidMode.IsHost()) + } + if pidMode.Valid() != state[2] { + t.Fatalf("PidMode.Valid for %v should have been %v but was %v", pidMode, state[2], pidMode.Valid()) + } + } +} + +func TestRestartPolicy(t *testing.T) { + restartPolicies := map[container.RestartPolicy][]bool{ + // none, always, failure + container.RestartPolicy{}: {false, false, false}, + container.RestartPolicy{"something", 0}: {false, false, false}, + container.RestartPolicy{"no", 0}: {true, false, false}, + container.RestartPolicy{"always", 0}: {false, true, false}, + container.RestartPolicy{"on-failure", 0}: {false, false, true}, + } + for restartPolicy, state := range restartPolicies { + if restartPolicy.IsNone() != state[0] { + t.Fatalf("RestartPolicy.IsNone for %v should have been %v but was %v", restartPolicy, state[0], restartPolicy.IsNone()) + } + if restartPolicy.IsAlways() != state[1] { + t.Fatalf("RestartPolicy.IsAlways for %v should have been %v but was %v", restartPolicy, state[1], restartPolicy.IsAlways()) + } + if restartPolicy.IsOnFailure() != state[2] { + t.Fatalf("RestartPolicy.IsOnFailure for %v should have been %v but was %v", restartPolicy, state[2], restartPolicy.IsOnFailure()) + } + } +} +func TestDecodeHostConfig(t *testing.T) { + fixtures := []struct { + file string + }{ + {"fixtures/unix/container_hostconfig_1_14.json"}, + {"fixtures/unix/container_hostconfig_1_19.json"}, + } + + for _, f := range fixtures { + b, err := ioutil.ReadFile(f.file) + if err != nil { + t.Fatal(err) + } + + c, err := DecodeHostConfig(bytes.NewReader(b)) + if err != nil { + t.Fatal(fmt.Errorf("Error parsing %s: %v", f, err)) + } + + if c.Privileged != false { + t.Fatalf("Expected privileged false, found %v\n", c.Privileged) + } + + if l := len(c.Binds); l != 1 { + t.Fatalf("Expected 1 bind, found %d\n", l) + } + + if len(c.CapAdd) != 1 && c.CapAdd[0] != "NET_ADMIN" { + t.Fatalf("Expected CapAdd NET_ADMIN, got %v", c.CapAdd) + } + + if len(c.CapDrop) != 1 && c.CapDrop[0] != "NET_ADMIN" { + t.Fatalf("Expected CapDrop MKNOD, got %v", c.CapDrop) + } + } +} diff --git a/runconfig/hostconfig_unix.go b/runconfig/hostconfig_unix.go new file mode 100644 index 00000000..efc26112 --- /dev/null +++ b/runconfig/hostconfig_unix.go @@ -0,0 +1,89 @@ +// +build !windows + +package runconfig + +import ( + "fmt" + "runtime" + "strings" + + "github.com/docker/engine-api/types/container" +) + +// DefaultDaemonNetworkMode returns the default network stack the daemon should +// use. +func DefaultDaemonNetworkMode() container.NetworkMode { + return container.NetworkMode("bridge") +} + +// IsPreDefinedNetwork indicates if a network is predefined by the daemon +func IsPreDefinedNetwork(network string) bool { + n := container.NetworkMode(network) + return n.IsBridge() || n.IsHost() || n.IsNone() || n.IsDefault() +} + +// ValidateNetMode ensures that the various combinations of requested +// network settings are valid. +func ValidateNetMode(c *container.Config, hc *container.HostConfig) error { + // We may not be passed a host config, such as in the case of docker commit + if hc == nil { + return nil + } + parts := strings.Split(string(hc.NetworkMode), ":") + if parts[0] == "container" { + if len(parts) < 2 || parts[1] == "" { + return fmt.Errorf("--net: invalid net mode: invalid container format container:") + } + } + + if hc.NetworkMode.IsContainer() && c.Hostname != "" { + return ErrConflictNetworkHostname + } + + if hc.UTSMode.IsHost() && c.Hostname != "" { + return ErrConflictUTSHostname + } + + if hc.NetworkMode.IsHost() && len(hc.Links) > 0 { + return ErrConflictHostNetworkAndLinks + } + + if hc.NetworkMode.IsContainer() && len(hc.Links) > 0 { + return ErrConflictContainerNetworkAndLinks + } + + if (hc.NetworkMode.IsHost() || hc.NetworkMode.IsContainer()) && len(hc.DNS) > 0 { + return ErrConflictNetworkAndDNS + } + + if (hc.NetworkMode.IsContainer() || hc.NetworkMode.IsHost()) && len(hc.ExtraHosts) > 0 { + return ErrConflictNetworkHosts + } + + if (hc.NetworkMode.IsContainer() || hc.NetworkMode.IsHost()) && c.MacAddress != "" { + return ErrConflictContainerNetworkAndMac + } + + if hc.NetworkMode.IsContainer() && (len(hc.PortBindings) > 0 || hc.PublishAllPorts == true) { + return ErrConflictNetworkPublishPorts + } + + if hc.NetworkMode.IsContainer() && len(c.ExposedPorts) > 0 { + return ErrConflictNetworkExposePorts + } + return nil +} + +// ValidateIsolation performs platform specific validation of +// isolation in the hostconfig structure. Linux only supports "default" +// which is LXC container isolation +func ValidateIsolation(hc *container.HostConfig) error { + // We may not be passed a host config, such as in the case of docker commit + if hc == nil { + return nil + } + if !hc.Isolation.IsValid() { + return fmt.Errorf("invalid --isolation: %q - %s only supports 'default'", hc.Isolation, runtime.GOOS) + } + return nil +} diff --git a/runconfig/hostconfig_windows.go b/runconfig/hostconfig_windows.go new file mode 100644 index 00000000..bab24e44 --- /dev/null +++ b/runconfig/hostconfig_windows.go @@ -0,0 +1,46 @@ +package runconfig + +import ( + "fmt" + "strings" + + "github.com/docker/engine-api/types/container" +) + +// DefaultDaemonNetworkMode returns the default network stack the daemon should +// use. +func DefaultDaemonNetworkMode() container.NetworkMode { + return container.NetworkMode("nat") +} + +// IsPreDefinedNetwork indicates if a network is predefined by the daemon +func IsPreDefinedNetwork(network string) bool { + return !container.NetworkMode(network).IsUserDefined() +} + +// ValidateNetMode ensures that the various combinations of requested +// network settings are valid. +func ValidateNetMode(c *container.Config, hc *container.HostConfig) error { + if hc == nil { + return nil + } + parts := strings.Split(string(hc.NetworkMode), ":") + if len(parts) > 1 { + return fmt.Errorf("invalid --net: %s", hc.NetworkMode) + } + return nil +} + +// ValidateIsolation performs platform specific validation of the +// isolation in the hostconfig structure. Windows supports 'default' (or +// blank), 'process', or 'hyperv'. +func ValidateIsolation(hc *container.HostConfig) error { + // We may not be passed a host config, such as in the case of docker commit + if hc == nil { + return nil + } + if !hc.Isolation.IsValid() { + return fmt.Errorf("invalid --isolation: %q. Windows supports 'default', 'process', or 'hyperv'", hc.Isolation) + } + return nil +} diff --git a/runconfig/opts/envfile.go b/runconfig/opts/envfile.go new file mode 100644 index 00000000..ba8b4f20 --- /dev/null +++ b/runconfig/opts/envfile.go @@ -0,0 +1,67 @@ +package opts + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +// ParseEnvFile reads a file with environment variables enumerated by lines +// +// ``Environment variable names used by the utilities in the Shell and +// Utilities volume of IEEE Std 1003.1-2001 consist solely of uppercase +// letters, digits, and the '_' (underscore) from the characters defined in +// Portable Character Set and do not begin with a digit. *But*, other +// characters may be permitted by an implementation; applications shall +// tolerate the presence of such names.'' +// -- http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html +// +// As of #16585, it's up to application inside docker to validate or not +// environment variables, that's why we just strip leading whitespace and +// nothing more. +func ParseEnvFile(filename string) ([]string, error) { + fh, err := os.Open(filename) + if err != nil { + return []string{}, err + } + defer fh.Close() + + lines := []string{} + scanner := bufio.NewScanner(fh) + for scanner.Scan() { + // trim the line from all leading whitespace first + line := strings.TrimLeft(scanner.Text(), whiteSpaces) + // line is not empty, and not starting with '#' + if len(line) > 0 && !strings.HasPrefix(line, "#") { + data := strings.SplitN(line, "=", 2) + + // trim the front of a variable, but nothing else + variable := strings.TrimLeft(data[0], whiteSpaces) + if strings.ContainsAny(variable, whiteSpaces) { + return []string{}, ErrBadEnvVariable{fmt.Sprintf("variable '%s' has white spaces", variable)} + } + + if len(data) > 1 { + + // pass the value through, no trimming + lines = append(lines, fmt.Sprintf("%s=%s", variable, data[1])) + } else { + // if only a pass-through variable is given, clean it up. + lines = append(lines, fmt.Sprintf("%s=%s", strings.TrimSpace(line), os.Getenv(line))) + } + } + } + return lines, scanner.Err() +} + +var whiteSpaces = " \t" + +// ErrBadEnvVariable typed error for bad environment variable +type ErrBadEnvVariable struct { + msg string +} + +func (e ErrBadEnvVariable) Error() string { + return fmt.Sprintf("poorly formatted environment: %s", e.msg) +} diff --git a/runconfig/opts/envfile_test.go b/runconfig/opts/envfile_test.go new file mode 100644 index 00000000..a2e2200f --- /dev/null +++ b/runconfig/opts/envfile_test.go @@ -0,0 +1,142 @@ +package opts + +import ( + "bufio" + "fmt" + "io/ioutil" + "os" + "reflect" + "strings" + "testing" +) + +func tmpFileWithContent(content string, t *testing.T) string { + tmpFile, err := ioutil.TempFile("", "envfile-test") + if err != nil { + t.Fatal(err) + } + defer tmpFile.Close() + + tmpFile.WriteString(content) + return tmpFile.Name() +} + +// Test ParseEnvFile for a file with a few well formatted lines +func TestParseEnvFileGoodFile(t *testing.T) { + content := `foo=bar + baz=quux +# comment + +_foobar=foobaz +with.dots=working +and_underscore=working too +` + // Adding a newline + a line with pure whitespace. + // This is being done like this instead of the block above + // because it's common for editors to trim trailing whitespace + // from lines, which becomes annoying since that's the + // exact thing we need to test. + content += "\n \t " + tmpFile := tmpFileWithContent(content, t) + defer os.Remove(tmpFile) + + lines, err := ParseEnvFile(tmpFile) + if err != nil { + t.Fatal(err) + } + + expectedLines := []string{ + "foo=bar", + "baz=quux", + "_foobar=foobaz", + "with.dots=working", + "and_underscore=working too", + } + + if !reflect.DeepEqual(lines, expectedLines) { + t.Fatal("lines not equal to expected_lines") + } +} + +// Test ParseEnvFile for an empty file +func TestParseEnvFileEmptyFile(t *testing.T) { + tmpFile := tmpFileWithContent("", t) + defer os.Remove(tmpFile) + + lines, err := ParseEnvFile(tmpFile) + if err != nil { + t.Fatal(err) + } + + if len(lines) != 0 { + t.Fatal("lines not empty; expected empty") + } +} + +// Test ParseEnvFile for a non existent file +func TestParseEnvFileNonExistentFile(t *testing.T) { + _, err := ParseEnvFile("foo_bar_baz") + if err == nil { + t.Fatal("ParseEnvFile succeeded; expected failure") + } + if _, ok := err.(*os.PathError); !ok { + t.Fatalf("Expected a PathError, got [%v]", err) + } +} + +// Test ParseEnvFile for a badly formatted file +func TestParseEnvFileBadlyFormattedFile(t *testing.T) { + content := `foo=bar + f =quux +` + + tmpFile := tmpFileWithContent(content, t) + defer os.Remove(tmpFile) + + _, err := ParseEnvFile(tmpFile) + if err == nil { + t.Fatalf("Expected a ErrBadEnvVariable, got nothing") + } + if _, ok := err.(ErrBadEnvVariable); !ok { + t.Fatalf("Expected a ErrBadEnvVariable, got [%v]", err) + } + expectedMessage := "poorly formatted environment: variable 'f ' has white spaces" + if err.Error() != expectedMessage { + t.Fatalf("Expected [%v], got [%v]", expectedMessage, err.Error()) + } +} + +// Test ParseEnvFile for a file with a line exceeding bufio.MaxScanTokenSize +func TestParseEnvFileLineTooLongFile(t *testing.T) { + content := strings.Repeat("a", bufio.MaxScanTokenSize+42) + content = fmt.Sprint("foo=", content) + + tmpFile := tmpFileWithContent(content, t) + defer os.Remove(tmpFile) + + _, err := ParseEnvFile(tmpFile) + if err == nil { + t.Fatal("ParseEnvFile succeeded; expected failure") + } +} + +// ParseEnvFile with a random file, pass through +func TestParseEnvFileRandomFile(t *testing.T) { + content := `first line +another invalid line` + tmpFile := tmpFileWithContent(content, t) + defer os.Remove(tmpFile) + + _, err := ParseEnvFile(tmpFile) + + if err == nil { + t.Fatalf("Expected a ErrBadEnvVariable, got nothing") + } + if _, ok := err.(ErrBadEnvVariable); !ok { + t.Fatalf("Expected a ErrBadEnvvariable, got [%v]", err) + } + expectedMessage := "poorly formatted environment: variable 'first line' has white spaces" + if err.Error() != expectedMessage { + t.Fatalf("Expected [%v], got [%v]", expectedMessage, err.Error()) + } +} diff --git a/runconfig/opts/fixtures/valid.env b/runconfig/opts/fixtures/valid.env new file mode 100644 index 00000000..3afbdc81 --- /dev/null +++ b/runconfig/opts/fixtures/valid.env @@ -0,0 +1 @@ +ENV1=value1 diff --git a/runconfig/opts/fixtures/valid.label b/runconfig/opts/fixtures/valid.label new file mode 100644 index 00000000..b4208bdf --- /dev/null +++ b/runconfig/opts/fixtures/valid.label @@ -0,0 +1 @@ +LABEL1=value1 diff --git a/runconfig/opts/opts.go b/runconfig/opts/opts.go new file mode 100644 index 00000000..f919218d --- /dev/null +++ b/runconfig/opts/opts.go @@ -0,0 +1,70 @@ +package opts + +import ( + "fmt" + fopts "github.com/docker/docker/opts" + "net" + "os" + "strings" +) + +// 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, fmt.Errorf("valid streams are STDIN, STDOUT and STDERR") +} + +// ValidateEnv validates an environment variable and returns it. +// If no value is specified, it returns the current value using os.Getenv. +// +// As on ParseEnvFile and related to #16585, environment variable names +// are not validate what so ever, it's up to application inside docker +// to validate them or not. +func ValidateEnv(val string) (string, error) { + arr := strings.Split(val, "=") + if len(arr) > 1 { + return val, nil + } + if !doesEnvExist(val) { + return val, nil + } + return fmt.Sprintf("%s=%s", val, os.Getenv(val)), nil +} + +func doesEnvExist(name string) bool { + for _, entry := range os.Environ() { + parts := strings.SplitN(entry, "=", 2) + if parts[0] == name { + return true + } + } + return false +} + +// ValidateExtraHost validates that the specified string is a valid extrahost and returns it. +// ExtraHost are in the form of name:ip where the ip has to be a valid ip (ipv4 or ipv6). +func ValidateExtraHost(val string) (string, error) { + // allow for IPv6 addresses in extra hosts by only splitting on first ":" + arr := strings.SplitN(val, ":", 2) + if len(arr) != 2 || len(arr[0]) == 0 { + return "", fmt.Errorf("bad format for add-host: %q", val) + } + if _, err := fopts.ValidateIPAddress(arr[1]); err != nil { + return "", fmt.Errorf("invalid IP address in add-host: %q", arr[1]) + } + return val, nil +} + +// ValidateMACAddress validates a MAC address. +func ValidateMACAddress(val string) (string, error) { + _, err := net.ParseMAC(strings.TrimSpace(val)) + if err != nil { + return "", err + } + return val, nil +} diff --git a/runconfig/opts/opts_test.go b/runconfig/opts/opts_test.go new file mode 100644 index 00000000..69eac88a --- /dev/null +++ b/runconfig/opts/opts_test.go @@ -0,0 +1,108 @@ +package opts + +import ( + "fmt" + "os" + "strings" + "testing" +) + +func TestValidateAttach(t *testing.T) { + valid := []string{ + "stdin", + "stdout", + "stderr", + "STDIN", + "STDOUT", + "STDERR", + } + if _, err := ValidateAttach("invalid"); err == nil { + t.Fatalf("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 TestValidateEnv(t *testing.T) { + valids := map[string]string{ + "a": "a", + "something": "something", + "_=a": "_=a", + "env1=value1": "env1=value1", + "_env1=value1": "_env1=value1", + "env2=value2=value3": "env2=value2=value3", + "env3=abc!qwe": "env3=abc!qwe", + "env_4=value 4": "env_4=value 4", + "PATH": fmt.Sprintf("PATH=%v", os.Getenv("PATH")), + "PATH=something": "PATH=something", + "asd!qwe": "asd!qwe", + "1asd": "1asd", + "123": "123", + "some space": "some space", + " some space before": " some space before", + "some space after ": "some space after ", + } + for value, expected := range valids { + actual, err := ValidateEnv(value) + if err != nil { + t.Fatal(err) + } + if actual != expected { + t.Fatalf("Expected [%v], got [%v]", expected, actual) + } + } +} + +func TestValidateExtraHosts(t *testing.T) { + valid := []string{ + `myhost:192.168.0.1`, + `thathost:10.0.2.1`, + `anipv6host:2003:ab34:e::1`, + `ipv6local:::1`, + } + + invalid := map[string]string{ + `myhost:192.notanipaddress.1`: `invalid IP`, + `thathost-nosemicolon10.0.0.1`: `bad format`, + `anipv6host:::::1`: `invalid IP`, + `ipv6local:::0::`: `invalid IP`, + } + + for _, extrahost := range valid { + if _, err := ValidateExtraHost(extrahost); err != nil { + t.Fatalf("ValidateExtraHost(`"+extrahost+"`) should succeed: error %v", err) + } + } + + for extraHost, expectedError := range invalid { + if _, err := ValidateExtraHost(extraHost); err == nil { + t.Fatalf("ValidateExtraHost(`%q`) should have failed validation", extraHost) + } else { + if !strings.Contains(err.Error(), expectedError) { + t.Fatalf("ValidateExtraHost(`%q`) error should contain %q", extraHost, expectedError) + } + } + } +} + +func TestValidateMACAddress(t *testing.T) { + if _, err := ValidateMACAddress(`92:d0:c6:0a:29:33`); err != nil { + t.Fatalf("ValidateMACAddress(`92:d0:c6:0a:29:33`) got %s", err) + } + + if _, err := ValidateMACAddress(`92:d0:c6:0a:33`); err == nil { + t.Fatalf("ValidateMACAddress(`92:d0:c6:0a:33`) succeeded; expected failure on invalid MAC") + } + + if _, err := ValidateMACAddress(`random invalid string`); err == nil { + t.Fatalf("ValidateMACAddress(`random invalid string`) succeeded; expected failure on invalid MAC") + } +} diff --git a/runconfig/opts/parse.go b/runconfig/opts/parse.go new file mode 100644 index 00000000..6543b406 --- /dev/null +++ b/runconfig/opts/parse.go @@ -0,0 +1,766 @@ +package opts + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "path" + "regexp" + "strconv" + "strings" + + "github.com/docker/docker/opts" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/mount" + "github.com/docker/docker/pkg/signal" + "github.com/docker/engine-api/types/container" + networktypes "github.com/docker/engine-api/types/network" + "github.com/docker/engine-api/types/strslice" + "github.com/docker/go-connections/nat" + "github.com/docker/go-units" +) + +// Parse parses the specified 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(cmd *flag.FlagSet, args []string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, *flag.FlagSet, error) { + var ( + // FIXME: use utils.ListOpts for attach and volumes? + flAttach = opts.NewListOpts(ValidateAttach) + flVolumes = opts.NewListOpts(nil) + flTmpfs = opts.NewListOpts(nil) + flBlkioWeightDevice = NewWeightdeviceOpt(ValidateWeightDevice) + flDeviceReadBps = NewThrottledeviceOpt(ValidateThrottleBpsDevice) + flDeviceWriteBps = NewThrottledeviceOpt(ValidateThrottleBpsDevice) + flLinks = opts.NewListOpts(ValidateLink) + flAliases = opts.NewListOpts(nil) + flDeviceReadIOps = NewThrottledeviceOpt(ValidateThrottleIOpsDevice) + flDeviceWriteIOps = NewThrottledeviceOpt(ValidateThrottleIOpsDevice) + flEnv = opts.NewListOpts(ValidateEnv) + flLabels = opts.NewListOpts(ValidateEnv) + flDevices = opts.NewListOpts(ValidateDevice) + + flUlimits = NewUlimitOpt(nil) + + flPublish = opts.NewListOpts(nil) + flExpose = opts.NewListOpts(nil) + flDNS = opts.NewListOpts(opts.ValidateIPAddress) + flDNSSearch = opts.NewListOpts(opts.ValidateDNSSearch) + flDNSOptions = opts.NewListOpts(nil) + flExtraHosts = opts.NewListOpts(ValidateExtraHost) + flVolumesFrom = opts.NewListOpts(nil) + flEnvFile = opts.NewListOpts(nil) + flCapAdd = opts.NewListOpts(nil) + flCapDrop = opts.NewListOpts(nil) + flGroupAdd = opts.NewListOpts(nil) + flSecurityOpt = opts.NewListOpts(nil) + flLabelsFile = opts.NewListOpts(nil) + flLoggingOpts = opts.NewListOpts(nil) + flPrivileged = cmd.Bool([]string{"-privileged"}, false, "Give extended privileges to this container") + flPidMode = cmd.String([]string{"-pid"}, "", "PID namespace to use") + flUTSMode = cmd.String([]string{"-uts"}, "", "UTS namespace to use") + flUsernsMode = cmd.String([]string{"-userns"}, "", "User namespace to use") + flPublishAll = cmd.Bool([]string{"P", "-publish-all"}, false, "Publish all exposed ports to random ports") + flStdin = cmd.Bool([]string{"i", "-interactive"}, false, "Keep STDIN open even if not attached") + flTty = cmd.Bool([]string{"t", "-tty"}, false, "Allocate a pseudo-TTY") + flOomKillDisable = cmd.Bool([]string{"-oom-kill-disable"}, false, "Disable OOM Killer") + flOomScoreAdj = cmd.Int([]string{"-oom-score-adj"}, 0, "Tune host's OOM preferences (-1000 to 1000)") + flContainerIDFile = cmd.String([]string{"-cidfile"}, "", "Write the container ID to the file") + flEntrypoint = cmd.String([]string{"-entrypoint"}, "", "Overwrite the default ENTRYPOINT of the image") + flHostname = cmd.String([]string{"h", "-hostname"}, "", "Container host name") + flMemoryString = cmd.String([]string{"m", "-memory"}, "", "Memory limit") + flMemoryReservation = cmd.String([]string{"-memory-reservation"}, "", "Memory soft limit") + flMemorySwap = cmd.String([]string{"-memory-swap"}, "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") + flKernelMemory = cmd.String([]string{"-kernel-memory"}, "", "Kernel memory limit") + flUser = cmd.String([]string{"u", "-user"}, "", "Username or UID (format: [:])") + flWorkingDir = cmd.String([]string{"w", "-workdir"}, "", "Working directory inside the container") + flCPUShares = cmd.Int64([]string{"#c", "-cpu-shares"}, 0, "CPU shares (relative weight)") + flCPUPeriod = cmd.Int64([]string{"-cpu-period"}, 0, "Limit CPU CFS (Completely Fair Scheduler) period") + flCPUQuota = cmd.Int64([]string{"-cpu-quota"}, 0, "Limit CPU CFS (Completely Fair Scheduler) quota") + flCpusetCpus = cmd.String([]string{"-cpuset-cpus"}, "", "CPUs in which to allow execution (0-3, 0,1)") + flCpusetMems = cmd.String([]string{"-cpuset-mems"}, "", "MEMs in which to allow execution (0-3, 0,1)") + flBlkioWeight = cmd.Uint16([]string{"-blkio-weight"}, 0, "Block IO (relative weight), between 10 and 1000") + flSwappiness = cmd.Int64([]string{"-memory-swappiness"}, -1, "Tune container memory swappiness (0 to 100)") + flNetMode = cmd.String([]string{"-net"}, "default", "Connect a container to a network") + flMacAddress = cmd.String([]string{"-mac-address"}, "", "Container MAC address (e.g. 92:d0:c6:0a:29:33)") + flIPv4Address = cmd.String([]string{"-ip"}, "", "Container IPv4 address (e.g. 172.30.100.104)") + flIPv6Address = cmd.String([]string{"-ip6"}, "", "Container IPv6 address (e.g. 2001:db8::33)") + flIpcMode = cmd.String([]string{"-ipc"}, "", "IPC namespace to use") + flPidsLimit = cmd.Int64([]string{"-pids-limit"}, 0, "Tune container pids limit (set -1 for unlimited)") + flRestartPolicy = cmd.String([]string{"-restart"}, "no", "Restart policy to apply when a container exits") + flReadonlyRootfs = cmd.Bool([]string{"-read-only"}, false, "Mount the container's root filesystem as read only") + flLoggingDriver = cmd.String([]string{"-log-driver"}, "", "Logging driver for container") + flCgroupParent = cmd.String([]string{"-cgroup-parent"}, "", "Optional parent cgroup for the container") + flVolumeDriver = cmd.String([]string{"-volume-driver"}, "", "Optional volume driver for the container") + flStopSignal = cmd.String([]string{"-stop-signal"}, signal.DefaultStopSignal, fmt.Sprintf("Signal to stop a container, %v by default", signal.DefaultStopSignal)) + flIsolation = cmd.String([]string{"-isolation"}, "", "Container isolation technology") + flShmSize = cmd.String([]string{"-shm-size"}, "", "Size of /dev/shm, default value is 64MB") + ) + + cmd.Var(&flAttach, []string{"a", "-attach"}, "Attach to STDIN, STDOUT or STDERR") + cmd.Var(&flBlkioWeightDevice, []string{"-blkio-weight-device"}, "Block IO weight (relative device weight)") + cmd.Var(&flDeviceReadBps, []string{"-device-read-bps"}, "Limit read rate (bytes per second) from a device") + cmd.Var(&flDeviceWriteBps, []string{"-device-write-bps"}, "Limit write rate (bytes per second) to a device") + cmd.Var(&flDeviceReadIOps, []string{"-device-read-iops"}, "Limit read rate (IO per second) from a device") + cmd.Var(&flDeviceWriteIOps, []string{"-device-write-iops"}, "Limit write rate (IO per second) to a device") + cmd.Var(&flVolumes, []string{"v", "-volume"}, "Bind mount a volume") + cmd.Var(&flTmpfs, []string{"-tmpfs"}, "Mount a tmpfs directory") + cmd.Var(&flLinks, []string{"-link"}, "Add link to another container") + cmd.Var(&flAliases, []string{"-net-alias"}, "Add network-scoped alias for the container") + cmd.Var(&flDevices, []string{"-device"}, "Add a host device to the container") + cmd.Var(&flLabels, []string{"l", "-label"}, "Set meta data on a container") + cmd.Var(&flLabelsFile, []string{"-label-file"}, "Read in a line delimited file of labels") + cmd.Var(&flEnv, []string{"e", "-env"}, "Set environment variables") + cmd.Var(&flEnvFile, []string{"-env-file"}, "Read in a file of environment variables") + cmd.Var(&flPublish, []string{"p", "-publish"}, "Publish a container's port(s) to the host") + cmd.Var(&flExpose, []string{"-expose"}, "Expose a port or a range of ports") + cmd.Var(&flDNS, []string{"-dns"}, "Set custom DNS servers") + cmd.Var(&flDNSSearch, []string{"-dns-search"}, "Set custom DNS search domains") + cmd.Var(&flDNSOptions, []string{"-dns-opt"}, "Set DNS options") + cmd.Var(&flExtraHosts, []string{"-add-host"}, "Add a custom host-to-IP mapping (host:ip)") + cmd.Var(&flVolumesFrom, []string{"-volumes-from"}, "Mount volumes from the specified container(s)") + cmd.Var(&flCapAdd, []string{"-cap-add"}, "Add Linux capabilities") + cmd.Var(&flCapDrop, []string{"-cap-drop"}, "Drop Linux capabilities") + cmd.Var(&flGroupAdd, []string{"-group-add"}, "Add additional groups to join") + cmd.Var(&flSecurityOpt, []string{"-security-opt"}, "Security Options") + cmd.Var(flUlimits, []string{"-ulimit"}, "Ulimit options") + cmd.Var(&flLoggingOpts, []string{"-log-opt"}, "Log driver options") + + cmd.Require(flag.Min, 1) + + if err := cmd.ParseFlags(args, true); err != nil { + return nil, nil, nil, cmd, err + } + + var ( + attachStdin = flAttach.Get("stdin") + attachStdout = flAttach.Get("stdout") + attachStderr = flAttach.Get("stderr") + ) + + // Validate the input mac address + if *flMacAddress != "" { + if _, err := ValidateMACAddress(*flMacAddress); err != nil { + return nil, nil, nil, cmd, fmt.Errorf("%s is not a valid mac address", *flMacAddress) + } + } + if *flStdin { + attachStdin = true + } + // If -a is not set attach to the output stdio + if flAttach.Len() == 0 { + attachStdout = true + attachStderr = true + } + + var err error + + var flMemory int64 + if *flMemoryString != "" { + flMemory, err = units.RAMInBytes(*flMemoryString) + if err != nil { + return nil, nil, nil, cmd, err + } + } + + var MemoryReservation int64 + if *flMemoryReservation != "" { + MemoryReservation, err = units.RAMInBytes(*flMemoryReservation) + if err != nil { + return nil, nil, nil, cmd, err + } + } + + var memorySwap int64 + if *flMemorySwap != "" { + if *flMemorySwap == "-1" { + memorySwap = -1 + } else { + memorySwap, err = units.RAMInBytes(*flMemorySwap) + if err != nil { + return nil, nil, nil, cmd, err + } + } + } + + var KernelMemory int64 + if *flKernelMemory != "" { + KernelMemory, err = units.RAMInBytes(*flKernelMemory) + if err != nil { + return nil, nil, nil, cmd, err + } + } + + swappiness := *flSwappiness + if swappiness != -1 && (swappiness < 0 || swappiness > 100) { + return nil, nil, nil, cmd, fmt.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness) + } + + var shmSize int64 + if *flShmSize != "" { + shmSize, err = units.RAMInBytes(*flShmSize) + if err != nil { + return nil, nil, nil, cmd, err + } + } + + var binds []string + // add any bind targets to the list of container volumes + for bind := range flVolumes.GetMap() { + if arr := volumeSplitN(bind, 2); len(arr) > 1 { + // after creating the bind mount we want to delete it from the flVolumes values because + // we do not want bind mounts being committed to image configs + binds = append(binds, bind) + flVolumes.Delete(bind) + } + } + + // Can't evaluate options passed into --tmpfs until we actually mount + tmpfs := make(map[string]string) + for _, t := range flTmpfs.GetAll() { + if arr := strings.SplitN(t, ":", 2); len(arr) > 1 { + if _, _, err := mount.ParseTmpfsOptions(arr[1]); err != nil { + return nil, nil, nil, cmd, err + } + tmpfs[arr[0]] = arr[1] + } else { + tmpfs[arr[0]] = "" + } + } + + var ( + parsedArgs = cmd.Args() + runCmd strslice.StrSlice + entrypoint strslice.StrSlice + image = cmd.Arg(0) + ) + if len(parsedArgs) > 1 { + runCmd = strslice.StrSlice(parsedArgs[1:]) + } + if *flEntrypoint != "" { + entrypoint = strslice.StrSlice{*flEntrypoint} + } + // Validate if the given hostname is RFC 1123 (https://tools.ietf.org/html/rfc1123) compliant. + hostname := *flHostname + if hostname != "" { + // Linux hostname is limited to HOST_NAME_MAX=64, not not including the terminating null byte. + matched, _ := regexp.MatchString("^(([[:alnum:]]|[[:alnum:]][[:alnum:]\\-]*[[:alnum:]])\\.)*([[:alnum:]]|[[:alnum:]][[:alnum:]\\-]*[[:alnum:]])$", hostname) + if len(hostname) > 64 || !matched { + return nil, nil, nil, cmd, fmt.Errorf("invalid hostname format for --hostname: %s", hostname) + } + } + + ports, portBindings, err := nat.ParsePortSpecs(flPublish.GetAll()) + if err != nil { + return nil, nil, nil, cmd, err + } + + // Merge in exposed ports to the map of published ports + for _, e := range flExpose.GetAll() { + if strings.Contains(e, ":") { + return nil, nil, nil, cmd, fmt.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, nil, nil, cmd, fmt.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, nil, nil, cmd, err + } + if _, exists := ports[p]; !exists { + ports[p] = struct{}{} + } + } + } + + // parse device mappings + deviceMappings := []container.DeviceMapping{} + for _, device := range flDevices.GetAll() { + deviceMapping, err := ParseDevice(device) + if err != nil { + return nil, nil, nil, cmd, err + } + deviceMappings = append(deviceMappings, deviceMapping) + } + + // collect all the environment variables for the container + envVariables, err := readKVStrings(flEnvFile.GetAll(), flEnv.GetAll()) + if err != nil { + return nil, nil, nil, cmd, err + } + + // collect all the labels for the container + labels, err := readKVStrings(flLabelsFile.GetAll(), flLabels.GetAll()) + if err != nil { + return nil, nil, nil, cmd, err + } + + ipcMode := container.IpcMode(*flIpcMode) + if !ipcMode.Valid() { + return nil, nil, nil, cmd, fmt.Errorf("--ipc: invalid IPC mode") + } + + pidMode := container.PidMode(*flPidMode) + if !pidMode.Valid() { + return nil, nil, nil, cmd, fmt.Errorf("--pid: invalid PID mode") + } + + utsMode := container.UTSMode(*flUTSMode) + if !utsMode.Valid() { + return nil, nil, nil, cmd, fmt.Errorf("--uts: invalid UTS mode") + } + + usernsMode := container.UsernsMode(*flUsernsMode) + if !usernsMode.Valid() { + return nil, nil, nil, cmd, fmt.Errorf("--userns: invalid USER mode") + } + + restartPolicy, err := ParseRestartPolicy(*flRestartPolicy) + if err != nil { + return nil, nil, nil, cmd, err + } + + loggingOpts, err := parseLoggingOpts(*flLoggingDriver, flLoggingOpts.GetAll()) + if err != nil { + return nil, nil, nil, cmd, err + } + + securityOpts, err := parseSecurityOpts(flSecurityOpt.GetAll()) + if err != nil { + return nil, nil, nil, cmd, err + } + + resources := container.Resources{ + CgroupParent: *flCgroupParent, + Memory: flMemory, + MemoryReservation: MemoryReservation, + MemorySwap: memorySwap, + MemorySwappiness: flSwappiness, + KernelMemory: KernelMemory, + OomKillDisable: flOomKillDisable, + CPUShares: *flCPUShares, + CPUPeriod: *flCPUPeriod, + CpusetCpus: *flCpusetCpus, + CpusetMems: *flCpusetMems, + CPUQuota: *flCPUQuota, + PidsLimit: *flPidsLimit, + BlkioWeight: *flBlkioWeight, + BlkioWeightDevice: flBlkioWeightDevice.GetList(), + BlkioDeviceReadBps: flDeviceReadBps.GetList(), + BlkioDeviceWriteBps: flDeviceWriteBps.GetList(), + BlkioDeviceReadIOps: flDeviceReadIOps.GetList(), + BlkioDeviceWriteIOps: flDeviceWriteIOps.GetList(), + Ulimits: flUlimits.GetList(), + Devices: deviceMappings, + } + + config := &container.Config{ + Hostname: *flHostname, + ExposedPorts: ports, + User: *flUser, + Tty: *flTty, + // 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: *flStdin, + AttachStdin: attachStdin, + AttachStdout: attachStdout, + AttachStderr: attachStderr, + Env: envVariables, + Cmd: runCmd, + Image: image, + Volumes: flVolumes.GetMap(), + MacAddress: *flMacAddress, + Entrypoint: entrypoint, + WorkingDir: *flWorkingDir, + Labels: ConvertKVStringsToMap(labels), + } + if cmd.IsSet("-stop-signal") { + config.StopSignal = *flStopSignal + } + + hostConfig := &container.HostConfig{ + Binds: binds, + ContainerIDFile: *flContainerIDFile, + OomScoreAdj: *flOomScoreAdj, + Privileged: *flPrivileged, + PortBindings: portBindings, + Links: flLinks.GetAll(), + PublishAllPorts: *flPublishAll, + // 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: flDNS.GetAllOrEmpty(), + DNSSearch: flDNSSearch.GetAllOrEmpty(), + DNSOptions: flDNSOptions.GetAllOrEmpty(), + ExtraHosts: flExtraHosts.GetAll(), + VolumesFrom: flVolumesFrom.GetAll(), + NetworkMode: container.NetworkMode(*flNetMode), + IpcMode: ipcMode, + PidMode: pidMode, + UTSMode: utsMode, + UsernsMode: usernsMode, + CapAdd: strslice.StrSlice(flCapAdd.GetAll()), + CapDrop: strslice.StrSlice(flCapDrop.GetAll()), + GroupAdd: flGroupAdd.GetAll(), + RestartPolicy: restartPolicy, + SecurityOpt: securityOpts, + ReadonlyRootfs: *flReadonlyRootfs, + LogConfig: container.LogConfig{Type: *flLoggingDriver, Config: loggingOpts}, + VolumeDriver: *flVolumeDriver, + Isolation: container.Isolation(*flIsolation), + ShmSize: shmSize, + Resources: resources, + Tmpfs: tmpfs, + } + + // 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 *flIPv4Address != "" || *flIPv6Address != "" { + networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] = &networktypes.EndpointSettings{ + IPAMConfig: &networktypes.EndpointIPAMConfig{ + IPv4Address: *flIPv4Address, + IPv6Address: *flIPv6Address, + }, + } + } + + 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 flAliases.Len() > 0 { + epConfig := networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] + if epConfig == nil { + epConfig = &networktypes.EndpointSettings{} + } + epConfig.Aliases = make([]string, flAliases.Len()) + copy(epConfig.Aliases, flAliases.GetAll()) + networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] = epConfig + } + + return config, hostConfig, networkingConfig, cmd, nil +} + +// reads a file of line terminated key=value pairs and override that with override parameter +func readKVStrings(files []string, override []string) ([]string, error) { + envVariables := []string{} + for _, ef := range files { + parsedVars, err := ParseEnvFile(ef) + if err != nil { + return nil, err + } + envVariables = append(envVariables, parsedVars...) + } + // parse the '-e' and '--env' after, to allow override + envVariables = append(envVariables, override...) + + return envVariables, nil +} + +// ConvertKVStringsToMap converts ["key=value"] to {"key":"value"} +func ConvertKVStringsToMap(values []string) map[string]string { + result := make(map[string]string, len(values)) + for _, value := range values { + kv := strings.SplitN(value, "=", 2) + if len(kv) == 1 { + result[kv[0]] = "" + } else { + result[kv[0]] = kv[1] + } + } + + return result +} + +func parseLoggingOpts(loggingDriver string, loggingOpts []string) (map[string]string, error) { + loggingOptsMap := ConvertKVStringsToMap(loggingOpts) + if loggingDriver == "none" && len(loggingOpts) > 0 { + return map[string]string{}, fmt.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.Index(opt, ":") != -1 { + con = strings.SplitN(opt, ":", 2) + } else { + return securityOpts, fmt.Errorf("Invalid --security-opt: %q", opt) + } + } + if con[0] == "seccomp" && con[1] != "unconfined" { + f, err := ioutil.ReadFile(con[1]) + if err != nil { + return securityOpts, fmt.Errorf("opening seccomp profile (%s) failed: %v", con[1], err) + } + b := bytes.NewBuffer(nil) + if err := json.Compact(b, f); err != nil { + return securityOpts, fmt.Errorf("compacting json for seccomp profile (%s) failed: %v", con[1], err) + } + securityOpts[key] = fmt.Sprintf("seccomp=%s", b.Bytes()) + } + } + + return securityOpts, nil +} + +// ParseRestartPolicy returns the parsed policy or an error indicating what is incorrect +func ParseRestartPolicy(policy string) (container.RestartPolicy, error) { + p := container.RestartPolicy{} + + if policy == "" { + return p, nil + } + + var ( + parts = strings.Split(policy, ":") + name = parts[0] + ) + + p.Name = name + switch name { + case "always", "unless-stopped": + if len(parts) > 1 { + return p, fmt.Errorf("maximum restart count not valid with restart policy of \"%s\"", name) + } + case "no": + // do nothing + case "on-failure": + if len(parts) > 2 { + return p, fmt.Errorf("restart count format is not valid, usage: 'on-failure:N' or 'on-failure'") + } + if len(parts) == 2 { + count, err := strconv.Atoi(parts[1]) + if err != nil { + return p, err + } + + p.MaximumRetryCount = count + } + default: + return p, fmt.Errorf("invalid restart policy %s", name) + } + + return p, 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{}, fmt.Errorf("invalid device specification: %s", device) + } + + if dst == "" { + dst = src + } + + deviceMapping := container.DeviceMapping{ + PathOnHost: src, + PathInContainer: dst, + CgroupPermissions: permissions, + } + return deviceMapping, nil +} + +// ParseLink parses and validates the specified string as a link format (name:alias) +func ParseLink(val string) (string, string, error) { + if val == "" { + return "", "", fmt.Errorf("empty string specified for links") + } + arr := strings.Split(val, ":") + if len(arr) > 2 { + return "", "", fmt.Errorf("bad format for links: %s", val) + } + if len(arr) == 1 { + return val, val, nil + } + // This is kept because we can actually get an HostConfig with links + // from an already created container and the format is not `foo:bar` + // but `/foo:/c1/bar` + if strings.HasPrefix(arr[0], "/") { + _, alias := path.Split(arr[1]) + return arr[0][1:], alias, nil + } + return arr[0], arr[1], nil +} + +// ValidateLink validates that the specified string has a valid link format (containerName:alias). +func ValidateLink(val string) (string, error) { + if _, _, err := ParseLink(val); err != nil { + return val, err + } + return val, nil +} + +// 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, fmt.Errorf("bad format for path: %s", val) + } + + split := strings.SplitN(val, ":", 3) + if split[0] == "" { + return val, fmt.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, fmt.Errorf("bad mode specified: %s", mode) + } + val = fmt.Sprintf("%s:%s:%s", split[0], containerPath, mode) + } + + if !path.IsAbs(containerPath) { + return val, fmt.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 +} diff --git a/runconfig/opts/parse_test.go b/runconfig/opts/parse_test.go new file mode 100644 index 00000000..18855443 --- /dev/null +++ b/runconfig/opts/parse_test.go @@ -0,0 +1,839 @@ +package opts + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "runtime" + "strings" + "testing" + + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/runconfig" + "github.com/docker/engine-api/types/container" + networktypes "github.com/docker/engine-api/types/network" + "github.com/docker/go-connections/nat" +) + +func parseRun(args []string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, *flag.FlagSet, error) { + cmd := flag.NewFlagSet("run", flag.ContinueOnError) + cmd.SetOutput(ioutil.Discard) + cmd.Usage = nil + return Parse(cmd, args) +} + +func parse(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 := parse(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 := parse(t, "-a"); err == nil { + t.Fatalf("Error parsing attach flags, `-a` should be an error but is not") + } + if _, _, err := parse(t, "-a invalid"); err == nil { + t.Fatalf("Error parsing attach flags, `-a invalid` should be an error but is not") + } + if _, _, err := parse(t, "-a invalid -a stdout"); err == nil { + t.Fatalf("Error parsing attach flags, `-a stdout -a invalid` should be an error but is not") + } + if _, _, err := parse(t, "-a stdout -a stderr -d"); err == nil { + t.Fatalf("Error parsing attach flags, `-a stdout -a stderr -d` should be an error but is not") + } + if _, _, err := parse(t, "-a stdin -d"); err == nil { + t.Fatalf("Error parsing attach flags, `-a stdin -d` should be an error but is not") + } + if _, _, err := parse(t, "-a stdout -d"); err == nil { + t.Fatalf("Error parsing attach flags, `-a stdout -d` should be an error but is not") + } + if _, _, err := parse(t, "-a stderr -d"); err == nil { + t.Fatalf("Error parsing attach flags, `-a stderr -d` should be an error but is not") + } + if _, _, err := parse(t, "-d --rm"); err == nil { + t.Fatalf("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]) + } + } + +} + +// 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("binds %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("binds %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("binds %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("binds %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("binds %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("binds %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("binds %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("binds %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("binds %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, fmt.Errorf("Error on marshal %s", err.Error()) + } + c, h, _, err = runconfig.DecodeContainerConfig(bytes.NewReader(b)) + if err != nil { + return nil, nil, fmt.Errorf("Error parsing %s: %v", string(b), err) + } + if c == nil || h == nil { + return nil, nil, fmt.Errorf("Empty config or hostconfig") + } + + return c, h, err +} + +// 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 fmt.Errorf("strings don't match") +} + +// 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 (eg -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 +} + +// 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" + validMemory := "--memory=1G" + if _, _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}); err != nil && err.Error() != "invalid size: 'invalid'" { + t.Fatalf("Expected an error with '%v' Memory, got '%v'", invalidMemory, err) + } + if _, hostconfig := mustParse(t, validMemory); hostconfig.Memory != 1073741824 { + t.Fatalf("Expected the config to have '1G' as Memory, got '%v'", hostconfig.Memory) + } +} + +func TestParseWithMemorySwap(t *testing.T) { + invalidMemory := "--memory-swap=invalid" + validMemory := "--memory-swap=1G" + anotherValidMemory := "--memory-swap=-1" + if _, _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}); err == nil || err.Error() != "invalid size: 'invalid'" { + t.Fatalf("Expected an error with '%v' MemorySwap, got '%v'", invalidMemory, err) + } + if _, hostconfig := mustParse(t, validMemory); hostconfig.MemorySwap != 1073741824 { + t.Fatalf("Expected the config to have '1073741824' as MemorySwap, got '%v'", hostconfig.MemorySwap) + } + if _, hostconfig := mustParse(t, anotherValidMemory); hostconfig.MemorySwap != -1 { + t.Fatalf("Expected the config to have '-1' as MemorySwap, got '%v'", hostconfig.MemorySwap) + } +} + +func TestParseHostname(t *testing.T) { + validHostnames := map[string]string{ + "hostname": "hostname", + "host-name": "host-name", + "hostname123": "hostname123", + "123hostname": "123hostname", + "hostname-of-64-bytes-long-should-be-valid-and-without-any-errors": "hostname-of-64-bytes-long-should-be-valid-and-without-any-errors", + } + invalidHostnames := map[string]string{ + "^hostname": "invalid hostname format for --hostname: ^hostname", + "hostname%": "invalid hostname format for --hostname: hostname%", + "host&name": "invalid hostname format for --hostname: host&name", + "-hostname": "invalid hostname format for --hostname: -hostname", + "host_name": "invalid hostname format for --hostname: host_name", + "hostname-of-65-bytes-long-should-be-invalid-and-be-given-an-error": "invalid hostname format for --hostname: hostname-of-65-bytes-long-should-be-invalid-and-be-given-an-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) + } + } + for hostname, expectedError := range invalidHostnames { + if _, _, err := parse(t, fmt.Sprintf("--hostname=%s", hostname)); err == nil || err.Error() != expectedError { + t.Fatalf("Expected error '%v' with '--hostname=%s', got '%s'", expectedError, hostname, err) + } + } + 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 + if _, _, _, _, err = parseRun([]string{"--shm-size=a128m", "img", "cmd"}); err == nil || err.Error() != "invalid size: 'a128m'" { + t.Fatalf("Expected an error with message 'invalid size: a128m', got %v", 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{ + "something": "invalid restart policy something", + "always:2": "maximum restart count not valid with restart policy of \"always\"", + "always:2:3": "maximum restart count not valid with restart policy of \"always\"", + "on-failure:invalid": `strconv.ParseInt: parsing "invalid": invalid syntax`, + "on-failure:2:5": "restart count format is not valid, usage: 'on-failure:N' or 'on-failure'", + } + 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 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=fixtures/valid.env", "img", "cmd"}) + if err != nil { + t.Fatal(err) + } + if len(config.Env) != 1 || config.Env[0] != "ENV1=value1" { + t.Fatalf("Expected a a config with [ENV1=value1], got %v", config.Env) + } + config, _, _, _, err = parseRun([]string{"--env-file=fixtures/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 a config with [ENV1=value1 ENV2=value2], got %v", config.Env) + } +} + +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=fixtures/valid.label", "img", "cmd"}) + if err != nil { + t.Fatal(err) + } + if len(config.Labels) != 1 || config.Labels["LABEL1"] != "value1" { + t.Fatalf("Expected a a config with [LABEL1:value1], got %v", config.Labels) + } + config, _, _, _, err = parseRun([]string{"--label-file=fixtures/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 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) + } +} + +func TestValidateLink(t *testing.T) { + valid := []string{ + "name", + "dcdfbe62ecd0:alias", + "7a67485460b7642516a4ad82ecefe7f57d0c4916f530561b71a50a3f9c4e33da", + "angry_torvalds:linus", + } + invalid := map[string]string{ + "": "empty string specified for links", + "too:much:of:it": "bad format for links: too:much:of:it", + } + + for _, link := range valid { + if _, err := ValidateLink(link); err != nil { + t.Fatalf("ValidateLink(`%q`) should succeed: error %q", link, err) + } + } + + for link, expectedError := range invalid { + if _, err := ValidateLink(link); err == nil { + t.Fatalf("ValidateLink(`%q`) should have failed validation", link) + } else { + if !strings.Contains(err.Error(), expectedError) { + t.Fatalf("ValidateLink(`%q`) error should contain %q", link, expectedError) + } + } + } +} + +func TestParseLink(t *testing.T) { + name, alias, err := ParseLink("name:alias") + if err != nil { + t.Fatalf("Expected not to error out on a valid name:alias format but got: %v", err) + } + if name != "name" { + t.Fatalf("Link name should have been name, got %s instead", name) + } + if alias != "alias" { + t.Fatalf("Link alias should have been alias, got %s instead", alias) + } + // short format definition + name, alias, err = ParseLink("name") + if err != nil { + t.Fatalf("Expected not to error out on a valid name only format but got: %v", err) + } + if name != "name" { + t.Fatalf("Link name should have been name, got %s instead", name) + } + if alias != "name" { + t.Fatalf("Link alias should have been name, got %s instead", alias) + } + // empty string link definition is not allowed + if _, _, err := ParseLink(""); err == nil || !strings.Contains(err.Error(), "empty string specified for links") { + t.Fatalf("Expected error 'empty string specified for links' but got: %v", err) + } + // more than two colons are not allowed + if _, _, err := ParseLink("link:alias:wrong"); err == nil || !strings.Contains(err.Error(), "bad format for links: link:alias:wrong") { + t.Fatalf("Expected error 'bad format for links: link:alias:wrong' but got: %v", err) + } +} + +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()) + } + } + } +} + +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) + } + } + } +} diff --git a/runconfig/opts/throttledevice.go b/runconfig/opts/throttledevice.go new file mode 100644 index 00000000..acb00ed5 --- /dev/null +++ b/runconfig/opts/throttledevice.go @@ -0,0 +1,108 @@ +package opts + +import ( + "fmt" + "strconv" + "strings" + + "github.com/docker/engine-api/types/blkiodev" + "github.com/docker/go-units" +) + +// ValidatorThrottleFctType defines a validator function that returns a validated struct and/or an error. +type ValidatorThrottleFctType func(val string) (*blkiodev.ThrottleDevice, error) + +// ValidateThrottleBpsDevice validates that the specified string has a valid device-rate format. +func ValidateThrottleBpsDevice(val string) (*blkiodev.ThrottleDevice, error) { + split := strings.SplitN(val, ":", 2) + if len(split) != 2 { + return nil, fmt.Errorf("bad format: %s", val) + } + if !strings.HasPrefix(split[0], "/dev/") { + return nil, fmt.Errorf("bad format for device path: %s", val) + } + rate, err := units.RAMInBytes(split[1]) + if err != nil { + return nil, fmt.Errorf("invalid rate for device: %s. The correct format is :[]. Number must be a positive integer. Unit is optional and can be kb, mb, or gb", val) + } + if rate < 0 { + return nil, fmt.Errorf("invalid rate for device: %s. The correct format is :[]. Number must be a positive integer. Unit is optional and can be kb, mb, or gb", val) + } + + return &blkiodev.ThrottleDevice{ + Path: split[0], + Rate: uint64(rate), + }, nil +} + +// ValidateThrottleIOpsDevice validates that the specified string has a valid device-rate format. +func ValidateThrottleIOpsDevice(val string) (*blkiodev.ThrottleDevice, error) { + split := strings.SplitN(val, ":", 2) + if len(split) != 2 { + return nil, fmt.Errorf("bad format: %s", val) + } + if !strings.HasPrefix(split[0], "/dev/") { + return nil, fmt.Errorf("bad format for device path: %s", val) + } + rate, err := strconv.ParseUint(split[1], 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid rate for device: %s. The correct format is :. Number must be a positive integer", val) + } + if rate < 0 { + return nil, fmt.Errorf("invalid rate for device: %s. The correct format is :. Number must be a positive integer", val) + } + + return &blkiodev.ThrottleDevice{ + Path: split[0], + Rate: uint64(rate), + }, nil +} + +// ThrottledeviceOpt defines a map of ThrottleDevices +type ThrottledeviceOpt struct { + values []*blkiodev.ThrottleDevice + validator ValidatorThrottleFctType +} + +// NewThrottledeviceOpt creates a new ThrottledeviceOpt +func NewThrottledeviceOpt(validator ValidatorThrottleFctType) ThrottledeviceOpt { + values := []*blkiodev.ThrottleDevice{} + return ThrottledeviceOpt{ + values: values, + validator: validator, + } +} + +// Set validates a ThrottleDevice and sets its name as a key in ThrottledeviceOpt +func (opt *ThrottledeviceOpt) Set(val string) error { + var value *blkiodev.ThrottleDevice + if opt.validator != nil { + v, err := opt.validator(val) + if err != nil { + return err + } + value = v + } + (opt.values) = append((opt.values), value) + return nil +} + +// String returns ThrottledeviceOpt values as a string. +func (opt *ThrottledeviceOpt) String() string { + var out []string + for _, v := range opt.values { + out = append(out, v.String()) + } + + return fmt.Sprintf("%v", out) +} + +// GetList returns a slice of pointers to ThrottleDevices. +func (opt *ThrottledeviceOpt) GetList() []*blkiodev.ThrottleDevice { + var throttledevice []*blkiodev.ThrottleDevice + for _, v := range opt.values { + throttledevice = append(throttledevice, v) + } + + return throttledevice +} diff --git a/runconfig/opts/ulimit.go b/runconfig/opts/ulimit.go new file mode 100644 index 00000000..0aec91f1 --- /dev/null +++ b/runconfig/opts/ulimit.go @@ -0,0 +1,52 @@ +package opts + +import ( + "fmt" + + "github.com/docker/go-units" +) + +// UlimitOpt defines a map of Ulimits +type UlimitOpt struct { + values *map[string]*units.Ulimit +} + +// NewUlimitOpt creates a new UlimitOpt +func NewUlimitOpt(ref *map[string]*units.Ulimit) *UlimitOpt { + if ref == nil { + ref = &map[string]*units.Ulimit{} + } + return &UlimitOpt{ref} +} + +// Set validates a Ulimit and sets its name as a key in UlimitOpt +func (o *UlimitOpt) Set(val string) error { + l, err := units.ParseUlimit(val) + if err != nil { + return err + } + + (*o.values)[l.Name] = l + + return nil +} + +// String returns Ulimit values as a string. +func (o *UlimitOpt) String() string { + var out []string + for _, v := range *o.values { + out = append(out, v.String()) + } + + return fmt.Sprintf("%v", out) +} + +// GetList returns a slice of pointers to Ulimits. +func (o *UlimitOpt) GetList() []*units.Ulimit { + var ulimits []*units.Ulimit + for _, v := range *o.values { + ulimits = append(ulimits, v) + } + + return ulimits +} diff --git a/runconfig/opts/ulimit_test.go b/runconfig/opts/ulimit_test.go new file mode 100644 index 00000000..0aa3facd --- /dev/null +++ b/runconfig/opts/ulimit_test.go @@ -0,0 +1,42 @@ +package opts + +import ( + "testing" + + "github.com/docker/go-units" +) + +func TestUlimitOpt(t *testing.T) { + ulimitMap := map[string]*units.Ulimit{ + "nofile": {"nofile", 1024, 512}, + } + + ulimitOpt := NewUlimitOpt(&ulimitMap) + + expected := "[nofile=512:1024]" + if ulimitOpt.String() != expected { + t.Fatalf("Expected %v, got %v", expected, ulimitOpt) + } + + // Valid ulimit append to opts + if err := ulimitOpt.Set("core=1024:1024"); err != nil { + t.Fatal(err) + } + + // Invalid ulimit type returns an error and do not append to opts + if err := ulimitOpt.Set("notavalidtype=1024:1024"); err == nil { + t.Fatalf("Expected error on invalid ulimit type") + } + expected = "[nofile=512:1024 core=1024:1024]" + expected2 := "[core=1024:1024 nofile=512:1024]" + result := ulimitOpt.String() + if result != expected && result != expected2 { + t.Fatalf("Expected %v or %v, got %v", expected, expected2, ulimitOpt) + } + + // And test GetList + ulimits := ulimitOpt.GetList() + if len(ulimits) != 2 { + t.Fatalf("Expected a ulimit list of 2, got %v", ulimits) + } +} diff --git a/runconfig/opts/weightdevice.go b/runconfig/opts/weightdevice.go new file mode 100644 index 00000000..bd3c0b17 --- /dev/null +++ b/runconfig/opts/weightdevice.go @@ -0,0 +1,84 @@ +package opts + +import ( + "fmt" + "strconv" + "strings" + + "github.com/docker/engine-api/types/blkiodev" +) + +// ValidatorWeightFctType defines a validator function that returns a validated struct and/or an error. +type ValidatorWeightFctType func(val string) (*blkiodev.WeightDevice, error) + +// ValidateWeightDevice validates that the specified string has a valid device-weight format. +func ValidateWeightDevice(val string) (*blkiodev.WeightDevice, error) { + split := strings.SplitN(val, ":", 2) + if len(split) != 2 { + return nil, fmt.Errorf("bad format: %s", val) + } + if !strings.HasPrefix(split[0], "/dev/") { + return nil, fmt.Errorf("bad format for device path: %s", val) + } + weight, err := strconv.ParseUint(split[1], 10, 0) + if err != nil { + return nil, fmt.Errorf("invalid weight for device: %s", val) + } + if weight > 0 && (weight < 10 || weight > 1000) { + return nil, fmt.Errorf("invalid weight for device: %s", val) + } + + return &blkiodev.WeightDevice{ + Path: split[0], + Weight: uint16(weight), + }, nil +} + +// WeightdeviceOpt defines a map of WeightDevices +type WeightdeviceOpt struct { + values []*blkiodev.WeightDevice + validator ValidatorWeightFctType +} + +// NewWeightdeviceOpt creates a new WeightdeviceOpt +func NewWeightdeviceOpt(validator ValidatorWeightFctType) WeightdeviceOpt { + values := []*blkiodev.WeightDevice{} + return WeightdeviceOpt{ + values: values, + validator: validator, + } +} + +// Set validates a WeightDevice and sets its name as a key in WeightdeviceOpt +func (opt *WeightdeviceOpt) Set(val string) error { + var value *blkiodev.WeightDevice + if opt.validator != nil { + v, err := opt.validator(val) + if err != nil { + return err + } + value = v + } + (opt.values) = append((opt.values), value) + return nil +} + +// String returns WeightdeviceOpt values as a string. +func (opt *WeightdeviceOpt) String() string { + var out []string + for _, v := range opt.values { + out = append(out, v.String()) + } + + return fmt.Sprintf("%v", out) +} + +// GetList returns a slice of pointers to WeightDevices. +func (opt *WeightdeviceOpt) GetList() []*blkiodev.WeightDevice { + var weightdevice []*blkiodev.WeightDevice + for _, v := range opt.values { + weightdevice = append(weightdevice, v) + } + + return weightdevice +} diff --git a/runconfig/streams.go b/runconfig/streams.go new file mode 100644 index 00000000..548c7826 --- /dev/null +++ b/runconfig/streams.go @@ -0,0 +1,109 @@ +package runconfig + +import ( + "fmt" + "io" + "io/ioutil" + "strings" + "sync" + + "github.com/docker/docker/pkg/broadcaster" + "github.com/docker/docker/pkg/ioutils" +) + +// StreamConfig holds information about I/O streams managed together. +// +// streamConfig.StdinPipe returns a WriteCloser which can be used to feed data +// to the standard input of the streamConfig's active process. +// streamConfig.StdoutPipe and streamConfig.StderrPipe each return a ReadCloser +// which can be used to retrieve the standard output (and error) generated +// by the container's active process. The output (and error) are actually +// copied and delivered to all StdoutPipe and StderrPipe consumers, using +// a kind of "broadcaster". +type StreamConfig struct { + sync.WaitGroup + stdout *broadcaster.Unbuffered + stderr *broadcaster.Unbuffered + stdin io.ReadCloser + stdinPipe io.WriteCloser +} + +// NewStreamConfig creates a stream config and initializes +// the standard err and standard out to new unbuffered broadcasters. +func NewStreamConfig() *StreamConfig { + return &StreamConfig{ + stderr: new(broadcaster.Unbuffered), + stdout: new(broadcaster.Unbuffered), + } +} + +// Stdout returns the standard output in the configuration. +func (streamConfig *StreamConfig) Stdout() *broadcaster.Unbuffered { + return streamConfig.stdout +} + +// Stderr returns the standard error in the configuration. +func (streamConfig *StreamConfig) Stderr() *broadcaster.Unbuffered { + return streamConfig.stderr +} + +// Stdin returns the standard input in the configuration. +func (streamConfig *StreamConfig) Stdin() io.ReadCloser { + return streamConfig.stdin +} + +// StdinPipe returns an input writer pipe as an io.WriteCloser. +func (streamConfig *StreamConfig) StdinPipe() io.WriteCloser { + return streamConfig.stdinPipe +} + +// StdoutPipe creates a new io.ReadCloser with an empty bytes pipe. +// It adds this new out pipe to the Stdout broadcaster. +func (streamConfig *StreamConfig) StdoutPipe() io.ReadCloser { + bytesPipe := ioutils.NewBytesPipe(nil) + streamConfig.stdout.Add(bytesPipe) + return bytesPipe +} + +// StderrPipe creates a new io.ReadCloser with an empty bytes pipe. +// It adds this new err pipe to the Stderr broadcaster. +func (streamConfig *StreamConfig) StderrPipe() io.ReadCloser { + bytesPipe := ioutils.NewBytesPipe(nil) + streamConfig.stderr.Add(bytesPipe) + return bytesPipe +} + +// NewInputPipes creates new pipes for both standard inputs, Stdin and StdinPipe. +func (streamConfig *StreamConfig) NewInputPipes() { + streamConfig.stdin, streamConfig.stdinPipe = io.Pipe() +} + +// NewNopInputPipe creates a new input pipe that will silently drop all messages in the input. +func (streamConfig *StreamConfig) NewNopInputPipe() { + streamConfig.stdinPipe = ioutils.NopWriteCloser(ioutil.Discard) +} + +// CloseStreams ensures that the configured streams are properly closed. +func (streamConfig *StreamConfig) CloseStreams() error { + var errors []string + + if streamConfig.stdin != nil { + if err := streamConfig.stdin.Close(); err != nil { + errors = append(errors, fmt.Sprintf("error close stdin: %s", err)) + } + } + + if err := streamConfig.stdout.Clean(); err != nil { + errors = append(errors, fmt.Sprintf("error close stdout: %s", err)) + } + + if err := streamConfig.stderr.Clean(); err != nil { + errors = append(errors, fmt.Sprintf("error close stderr: %s", err)) + } + + if len(errors) > 0 { + return fmt.Errorf(strings.Join(errors, "\n")) + } + + return nil +} diff --git a/utils/debug.go b/utils/debug.go new file mode 100644 index 00000000..d2038911 --- /dev/null +++ b/utils/debug.go @@ -0,0 +1,26 @@ +package utils + +import ( + "os" + + "github.com/Sirupsen/logrus" +) + +// EnableDebug sets the DEBUG env var to true +// and makes the logger to log at debug level. +func EnableDebug() { + os.Setenv("DEBUG", "1") + logrus.SetLevel(logrus.DebugLevel) +} + +// DisableDebug sets the DEBUG env var to false +// and makes the logger to log at info level. +func DisableDebug() { + os.Setenv("DEBUG", "") + logrus.SetLevel(logrus.InfoLevel) +} + +// IsDebugEnabled checks whether the debug flag is set or not. +func IsDebugEnabled() bool { + return os.Getenv("DEBUG") != "" +} diff --git a/utils/debug_test.go b/utils/debug_test.go new file mode 100644 index 00000000..6f9c4dfb --- /dev/null +++ b/utils/debug_test.go @@ -0,0 +1,43 @@ +package utils + +import ( + "os" + "testing" + + "github.com/Sirupsen/logrus" +) + +func TestEnableDebug(t *testing.T) { + defer func() { + os.Setenv("DEBUG", "") + logrus.SetLevel(logrus.InfoLevel) + }() + EnableDebug() + if os.Getenv("DEBUG") != "1" { + t.Fatalf("expected DEBUG=1, got %s\n", os.Getenv("DEBUG")) + } + if logrus.GetLevel() != logrus.DebugLevel { + t.Fatalf("expected log level %v, got %v\n", logrus.DebugLevel, logrus.GetLevel()) + } +} + +func TestDisableDebug(t *testing.T) { + DisableDebug() + if os.Getenv("DEBUG") != "" { + t.Fatalf("expected DEBUG=\"\", got %s\n", os.Getenv("DEBUG")) + } + if logrus.GetLevel() != logrus.InfoLevel { + t.Fatalf("expected log level %v, got %v\n", logrus.InfoLevel, logrus.GetLevel()) + } +} + +func TestDebugEnabled(t *testing.T) { + EnableDebug() + if !IsDebugEnabled() { + t.Fatal("expected debug enabled, got false") + } + DisableDebug() + if IsDebugEnabled() { + t.Fatal("expected debug disabled, got true") + } +} diff --git a/utils/experimental.go b/utils/experimental.go new file mode 100644 index 00000000..ceed0cb3 --- /dev/null +++ b/utils/experimental.go @@ -0,0 +1,9 @@ +// +build experimental + +package utils + +// ExperimentalBuild is a stub which always returns true for +// builds that include the "experimental" build tag +func ExperimentalBuild() bool { + return true +} diff --git a/utils/names.go b/utils/names.go new file mode 100644 index 00000000..8239c0de --- /dev/null +++ b/utils/names.go @@ -0,0 +1,12 @@ +package utils + +import "regexp" + +// RestrictedNameChars collects the characters allowed to represent a name, normally used to validate container and volume names. +const RestrictedNameChars = `[a-zA-Z0-9][a-zA-Z0-9_.-]` + +// RestrictedNamePattern is a regular expression to validate names against the collection of restricted characters. +var RestrictedNamePattern = regexp.MustCompile(`^/?` + RestrictedNameChars + `+$`) + +// RestrictedVolumeNamePattern is a regular expression to validate volume names against the collection of restricted characters. +var RestrictedVolumeNamePattern = regexp.MustCompile(`^` + RestrictedNameChars + `+$`) diff --git a/utils/process_unix.go b/utils/process_unix.go new file mode 100644 index 00000000..bdb1b46b --- /dev/null +++ b/utils/process_unix.go @@ -0,0 +1,22 @@ +// +build linux freebsd + +package utils + +import ( + "syscall" +) + +// IsProcessAlive returns true if process with a given pid is running. +func IsProcessAlive(pid int) bool { + err := syscall.Kill(pid, syscall.Signal(0)) + if err == nil || err == syscall.EPERM { + return true + } + + return false +} + +// KillProcess force-stops a process. +func KillProcess(pid int) { + syscall.Kill(pid, syscall.SIGKILL) +} diff --git a/utils/process_windows.go b/utils/process_windows.go new file mode 100644 index 00000000..03cb8551 --- /dev/null +++ b/utils/process_windows.go @@ -0,0 +1,20 @@ +package utils + +// IsProcessAlive returns true if process with a given pid is running. +func IsProcessAlive(pid int) bool { + // TODO Windows containerd. Not sure this is needed + // p, err := os.FindProcess(pid) + // if err == nil { + // return true + // } + return false +} + +// KillProcess force-stops a process. +func KillProcess(pid int) { + // TODO Windows containerd. Not sure this is needed + // p, err := os.FindProcess(pid) + // if err == nil { + // p.Kill() + // } +} diff --git a/utils/stubs.go b/utils/stubs.go new file mode 100644 index 00000000..8a496d39 --- /dev/null +++ b/utils/stubs.go @@ -0,0 +1,9 @@ +// +build !experimental + +package utils + +// ExperimentalBuild is a stub which always returns false for +// builds that do not include the "experimental" build tag +func ExperimentalBuild() bool { + return false +} diff --git a/utils/templates/templates.go b/utils/templates/templates.go new file mode 100644 index 00000000..749da3d5 --- /dev/null +++ b/utils/templates/templates.go @@ -0,0 +1,33 @@ +package templates + +import ( + "encoding/json" + "strings" + "text/template" +) + +// basicFunctions are the set of initial +// functions provided to every template. +var basicFunctions = template.FuncMap{ + "json": func(v interface{}) string { + a, _ := json.Marshal(v) + return string(a) + }, + "split": strings.Split, + "join": strings.Join, + "title": strings.Title, + "lower": strings.ToLower, + "upper": strings.ToUpper, +} + +// Parse creates a new annonymous template with the basic functions +// and parses the given format. +func Parse(format string) (*template.Template, error) { + return NewParse("", format) +} + +// NewParse creates a new tagged template with the basic functions +// and parses the given format. +func NewParse(tag, format string) (*template.Template, error) { + return template.New(tag).Funcs(basicFunctions).Parse(format) +} diff --git a/utils/templates/templates_test.go b/utils/templates/templates_test.go new file mode 100644 index 00000000..dd42901a --- /dev/null +++ b/utils/templates/templates_test.go @@ -0,0 +1,38 @@ +package templates + +import ( + "bytes" + "testing" +) + +func TestParseStringFunctions(t *testing.T) { + tm, err := Parse(`{{join (split . ":") "/"}}`) + if err != nil { + t.Fatal(err) + } + + var b bytes.Buffer + if err := tm.Execute(&b, "text:with:colon"); err != nil { + t.Fatal(err) + } + want := "text/with/colon" + if b.String() != want { + t.Fatalf("expected %s, got %s", want, b.String()) + } +} + +func TestNewParse(t *testing.T) { + tm, err := NewParse("foo", "this is a {{ . }}") + if err != nil { + t.Fatal(err) + } + + var b bytes.Buffer + if err := tm.Execute(&b, "string"); err != nil { + t.Fatal(err) + } + want := "this is a string" + if b.String() != want { + t.Fatalf("expected %s, got %s", want, b.String()) + } +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 00000000..d3dd00ab --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,87 @@ +package utils + +import ( + "fmt" + "io/ioutil" + "os" + "runtime" + "strings" + + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/stringid" +) + +var globalTestID string + +// TestDirectory creates a new temporary directory and returns its path. +// The contents of directory at path `templateDir` is copied into the +// new directory. +func TestDirectory(templateDir string) (dir string, err error) { + if globalTestID == "" { + globalTestID = stringid.GenerateNonCryptoID()[:4] + } + prefix := fmt.Sprintf("docker-test%s-%s-", globalTestID, GetCallerName(2)) + if prefix == "" { + prefix = "docker-test-" + } + dir, err = ioutil.TempDir("", prefix) + if err = os.Remove(dir); err != nil { + return + } + if templateDir != "" { + if err = archive.CopyWithTar(templateDir, dir); err != nil { + return + } + } + return +} + +// GetCallerName introspects the call stack and returns the name of the +// function `depth` levels down in the stack. +func GetCallerName(depth int) string { + // Use the caller function name as a prefix. + // This helps trace temp directories back to their test. + pc, _, _, _ := runtime.Caller(depth + 1) + callerLongName := runtime.FuncForPC(pc).Name() + parts := strings.Split(callerLongName, ".") + callerShortName := parts[len(parts)-1] + return callerShortName +} + +// ReplaceOrAppendEnvValues returns the defaults with the overrides either +// replaced by env key or appended to the list +func ReplaceOrAppendEnvValues(defaults, overrides []string) []string { + cache := make(map[string]int, len(defaults)) + for i, e := range defaults { + parts := strings.SplitN(e, "=", 2) + cache[parts[0]] = i + } + + for _, value := range overrides { + // Values w/o = means they want this env to be removed/unset. + if !strings.Contains(value, "=") { + if i, exists := cache[value]; exists { + defaults[i] = "" // Used to indicate it should be removed + } + continue + } + + // Just do a normal set/update + parts := strings.SplitN(value, "=", 2) + if i, exists := cache[parts[0]]; exists { + defaults[i] = value + } else { + defaults = append(defaults, value) + } + } + + // Now remove all entries that we want to "unset" + for i := 0; i < len(defaults); i++ { + if defaults[i] == "" { + defaults = append(defaults[:i], defaults[i+1:]...) + i-- + } + } + + return defaults +} diff --git a/utils/utils_test.go b/utils/utils_test.go new file mode 100644 index 00000000..ab3911e8 --- /dev/null +++ b/utils/utils_test.go @@ -0,0 +1,21 @@ +package utils + +import "testing" + +func TestReplaceAndAppendEnvVars(t *testing.T) { + var ( + d = []string{"HOME=/"} + o = []string{"HOME=/root", "TERM=xterm"} + ) + + env := ReplaceOrAppendEnvValues(d, o) + if len(env) != 2 { + t.Fatalf("expected len of 2 got %d", len(env)) + } + if env[0] != "HOME=/root" { + t.Fatalf("expected HOME=/root got '%s'", env[0]) + } + if env[1] != "TERM=xterm" { + t.Fatalf("expected TERM=xterm got '%s'", env[1]) + } +} diff --git a/vendor/src/github.com/docker/notary/.gitignore b/vendor/src/github.com/docker/notary/.gitignore new file mode 100644 index 00000000..8439935e --- /dev/null +++ b/vendor/src/github.com/docker/notary/.gitignore @@ -0,0 +1,11 @@ +/cmd/notary-server/notary-server +/cmd/notary-server/local.config.json +/cmd/notary-signer/local.config.json +cover +bin +cross +.cover +*.swp +.idea +*.iml +coverage.out diff --git a/vendor/src/github.com/docker/notary/CHANGELOG.md b/vendor/src/github.com/docker/notary/CHANGELOG.md new file mode 100644 index 00000000..0e12db59 --- /dev/null +++ b/vendor/src/github.com/docker/notary/CHANGELOG.md @@ -0,0 +1,26 @@ +# Changelog + +## [v0.2](https://github.com/docker/notary/releases/tag/v0.2.0) 2/24/2016 ++ Add support for delegation roles in `notary` server and client ++ Add `notary CLI` commands for managing delegation roles: `notary delegation` + + `add`, `list` and `remove` subcommands ++ Enhance `notary CLI` commands for adding targets to delegation roles + + `notary add --roles` and `notary remove --roles` to manipulate targets for delegations ++ Support for rotating the snapshot key to one managed by the `notary` server ++ Add consistent download functionality to download metadata and content by checksum ++ Update `docker-compose` configuration to use official mariadb image + + deprecate `notarymysql` + + default to using a volume for `data` directory + + use separate databases for `notary-server` and `notary-signer` with separate users ++ Add `notary CLI` command for changing private key passphrases: `notary key passwd` ++ Enhance `notary CLI` commands for importing and exporting keys ++ Change default `notary CLI` log level to fatal, introduce new verbose (error-level) and debug-level settings ++ Store roles as PEM headers in private keys, incompatible with previous notary v0.1 key format + + No longer store keys as `_role.key`, instead store as `.key`; new private keys from new notary clients will crash old notary clients ++ Support logging as JSON format on server and signer ++ Support mutual TLS between notary client and notary server + +## [v0.1](https://github.com/docker/notary/releases/tag/v0.1) 11/15/2015 ++ Initial non-alpha `notary` version ++ Implement TUF (the update framework) with support for root, targets, snapshot, and timestamp roles ++ Add PKCS11 interface to store and sign with keys in HSMs (i.e. Yubikey) diff --git a/vendor/src/github.com/docker/notary/CONTRIBUTING.md b/vendor/src/github.com/docker/notary/CONTRIBUTING.md new file mode 100644 index 00000000..0d4d16fc --- /dev/null +++ b/vendor/src/github.com/docker/notary/CONTRIBUTING.md @@ -0,0 +1,85 @@ +# Contributing to notary + +## Before reporting an issue... + +### If your problem is with... + + - automated builds + - your account on the [Docker Hub](https://hub.docker.com/) + - any other [Docker Hub](https://hub.docker.com/) issue + +Then please do not report your issue here - you should instead report it to [https://support.docker.com](https://support.docker.com) + +### If you... + + - need help setting up notary + - can't figure out something + - are not sure what's going on or what your problem is + +Then please do not open an issue here yet - you should first try one of the following support forums: + + - irc: #docker-trust on freenode + +## Reporting an issue properly + +By following these simple rules you will get better and faster feedback on your issue. + + - search the bugtracker for an already reported issue + +### If you found an issue that describes your problem: + + - please read other user comments first, and confirm this is the same issue: a given error condition might be indicative of different problems - you may also find a workaround in the comments + - please refrain from adding "same thing here" or "+1" comments + - you don't need to comment on an issue to get notified of updates: just hit the "subscribe" button + - comment if you have some new, technical and relevant information to add to the case + +### If you have not found an existing issue that describes your problem: + + 1. create a new issue, with a succinct title that describes your issue: + - bad title: "It doesn't work with my docker" + - good title: "Publish fail: 400 error with E_INVALID_DIGEST" + 2. copy the output of: + - `docker version` + - `docker info` + - `docker exec registry -version` + 3. copy the command line you used to run `notary` or launch `notaryserver` + 4. if relevant, copy your `notaryserver` logs that show the error + +## Contributing a patch for a known bug, or a small correction + +You should follow the basic GitHub workflow: + + 1. fork + 2. commit a change + 3. make sure the tests pass + 4. PR + +Additionally, you must [sign your commits](https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work). It's very simple: + + - configure your name with git: `git config user.name "Real Name" && git config user.email mail@example.com` + - sign your commits using `-s`: `git commit -s -m "My commit"` + +Some simple rules to ensure quick merge: + + - clearly point to the issue(s) you want to fix in your PR comment (e.g., `closes #12345`) + - prefer multiple (smaller) PRs addressing individual issues over a big one trying to address multiple issues at once + - if you need to amend your PR following comments, please squash instead of adding more commits + +## Contributing new features + +You are heavily encouraged to first discuss what you want to do. You can do so on the irc channel, or by opening an issue that clearly describes the use case you want to fulfill, or the problem you are trying to solve. + +If this is a major new feature, you should then submit a proposal that describes your technical solution and reasoning. +If you did discuss it first, this will likely be greenlighted very fast. It's advisable to address all feedback on this proposal before starting actual work + +Then you should submit your implementation, clearly linking to the issue (and possible proposal). + +Your PR will be reviewed by the community, then ultimately by the project maintainers, before being merged. + +It's mandatory to: + + - interact respectfully with other community members and maintainers - more generally, you are expected to abide by the [Docker community rules](https://github.com/docker/docker/blob/master/CONTRIBUTING.md#docker-community-guidelines) + - address maintainers' comments and modify your submission accordingly + - write tests for any new code + +Complying to these simple rules will greatly accelerate the review process, and will ensure you have a pleasant experience in contributing code to the Registry. diff --git a/vendor/src/github.com/docker/notary/CONTRIBUTORS b/vendor/src/github.com/docker/notary/CONTRIBUTORS new file mode 100644 index 00000000..4bc6c096 --- /dev/null +++ b/vendor/src/github.com/docker/notary/CONTRIBUTORS @@ -0,0 +1,4 @@ +David Williamson (github: davidwilliamson) +Aaron Lehmann (github: aaronlehmann) +Lewis Marshall (github: lmars) +Jonathan Rudenberg (github: titanous) diff --git a/vendor/src/github.com/docker/notary/Dockerfile b/vendor/src/github.com/docker/notary/Dockerfile new file mode 100644 index 00000000..3f8ae14d --- /dev/null +++ b/vendor/src/github.com/docker/notary/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.6.0 + +RUN apt-get update && apt-get install -y \ + libltdl-dev \ + libsqlite3-dev \ + --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* + +RUN go get golang.org/x/tools/cmd/vet \ + && go get golang.org/x/tools/cmd/cover \ + && go get github.com/tools/godep + +COPY . /go/src/github.com/docker/notary + +WORKDIR /go/src/github.com/docker/notary diff --git a/vendor/src/github.com/docker/notary/LICENSE b/vendor/src/github.com/docker/notary/LICENSE new file mode 100644 index 00000000..6daf85e9 --- /dev/null +++ b/vendor/src/github.com/docker/notary/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2015 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/src/github.com/docker/notary/MAINTAINERS b/vendor/src/github.com/docker/notary/MAINTAINERS new file mode 100644 index 00000000..999e280c --- /dev/null +++ b/vendor/src/github.com/docker/notary/MAINTAINERS @@ -0,0 +1,58 @@ +# Notary maintainers file +# +# This file describes who runs the docker/notary project and how. +# This is a living document - if you see something out of date or missing, speak up! +# +# It is structured to be consumable by both humans and programs. +# To extract its contents programmatically, use any TOML-compliant parser. +# +# This file is compiled into the MAINTAINERS file in docker/opensource. +# +[Org] + [Org."Core maintainers"] + people = [ + "cyli", + "diogomonica", + "dmcgowan", + "endophage", + "nathanmccauley", + "riyazdf", + ] + +[people] + +# A reference list of all people associated with the project. +# All other sections should refer to people by their canonical key +# in the people section. + + # ADD YOURSELF HERE IN ALPHABETICAL ORDER + + [people.cyli] + Name = "Ying Li" + Email = "ying.li@docker.com" + GitHub = "cyli" + + [people.diogomonica] + Name = "Diogo Monica" + Email = "diogo@docker.com" + GitHub = "diogomonica" + + [people.dmcgowan] + Name = "Derek McGowan" + Email = "derek@docker.com" + GitHub = "dmcgowan" + + [people.endophage] + Name = "David Lawrence" + Email = "david.lawrence@docker.com" + GitHub = "endophage" + + [people.nathanmccauley] + Name = "Nathan McCauley" + Email = "nathan.mccauley@docker.com" + GitHub = "nathanmccauley" + + [people.riyazdf] + Name = "Riyaz Faizullabhoy" + Email = "riyaz@docker.com" + GitHub = "riyazdf" diff --git a/vendor/src/github.com/docker/notary/Makefile b/vendor/src/github.com/docker/notary/Makefile new file mode 100644 index 00000000..0756159e --- /dev/null +++ b/vendor/src/github.com/docker/notary/Makefile @@ -0,0 +1,204 @@ +# Set an output prefix, which is the local directory if not specified +PREFIX?=$(shell pwd) + +# Populate version variables +# Add to compile time flags +NOTARY_PKG := github.com/docker/notary +NOTARY_VERSION := $(shell cat NOTARY_VERSION) +GITCOMMIT := $(shell git rev-parse --short HEAD) +GITUNTRACKEDCHANGES := $(shell git status --porcelain --untracked-files=no) +ifneq ($(GITUNTRACKEDCHANGES),) +GITCOMMIT := $(GITCOMMIT)-dirty +endif +CTIMEVAR=-X $(NOTARY_PKG)/version.GitCommit=$(GITCOMMIT) -X $(NOTARY_PKG)/version.NotaryVersion=$(NOTARY_VERSION) +GO_LDFLAGS=-ldflags "-w $(CTIMEVAR)" +GO_LDFLAGS_STATIC=-ldflags "-w $(CTIMEVAR) -extldflags -static" +GOOSES = darwin freebsd linux +GOARCHS = amd64 +NOTARY_BUILDTAGS ?= pkcs11 +GO_EXC = go +NOTARYDIR := /go/src/github.com/docker/notary + +# check to be sure pkcs11 lib is always imported with a build tag +GO_LIST_PKCS11 := $(shell go list -e -f '{{join .Deps "\n"}}' ./... | grep -v /vendor/ | xargs go list -e -f '{{if not .Standard}}{{.ImportPath}}{{end}}' | grep -q pkcs11) +ifeq ($(GO_LIST_PKCS11),) +$(info pkcs11 import was not found anywhere without a build tag, yay) +else +$(error You are importing pkcs11 somewhere and not using a build tag) +endif + +_empty := +_space := $(empty) $(empty) + +# go cover test variables +COVERDIR=.cover +COVERPROFILE?=$(COVERDIR)/cover.out +COVERMODE=count +PKGS ?= $(shell go list ./... | grep -v /vendor/ | tr '\n' ' ') + +GO_VERSION = $(shell go version | awk '{print $$3}') + +.PHONY: clean all fmt vet lint build test binaries cross cover docker-images notary-dockerfile +.DELETE_ON_ERROR: cover +.DEFAULT: default + +all: AUTHORS clean fmt vet fmt lint build test binaries + +AUTHORS: .git/HEAD + git log --format='%aN <%aE>' | sort -fu > $@ + +# This only needs to be generated by hand when cutting full releases. +version/version.go: + ./version/version.sh > $@ + +${PREFIX}/bin/notary-server: NOTARY_VERSION $(shell find . -type f -name '*.go') + @echo "+ $@" + @godep go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS} ./cmd/notary-server + +${PREFIX}/bin/notary: NOTARY_VERSION $(shell find . -type f -name '*.go') + @echo "+ $@" + @godep go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS} ./cmd/notary + +${PREFIX}/bin/notary-signer: NOTARY_VERSION $(shell find . -type f -name '*.go') + @echo "+ $@" + @godep go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS} ./cmd/notary-signer + +ifeq ($(shell uname -s),Darwin) +${PREFIX}/bin/static/notary-server: + @echo "notary-server: static builds not supported on OS X" + +${PREFIX}/bin/static/notary-signer: + @echo "notary-signer: static builds not supported on OS X" +else +${PREFIX}/bin/static/notary-server: NOTARY_VERSION $(shell find . -type f -name '*.go') + @echo "+ $@" + @godep go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS_STATIC} ./cmd/notary-server + +${PREFIX}/bin/static/notary-signer: NOTARY_VERSION $(shell find . -type f -name '*.go') + @echo "+ $@" + @godep go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS_STATIC} ./cmd/notary-signer +endif + +vet: + @echo "+ $@" +ifeq ($(shell uname -s), Darwin) + @test -z "$(shell find . -iname *test*.go | grep -v _test.go | grep -v vendor | xargs echo "This file should end with '_test':" | tee /dev/stderr)" +else + @test -z "$(shell find . -iname *test*.go | grep -v _test.go | grep -v vendor | xargs -r echo "This file should end with '_test':" | tee /dev/stderr)" +endif + @test -z "$$(go tool vet -printf=false . 2>&1 | grep -v vendor/ | tee /dev/stderr)" + +fmt: + @echo "+ $@" + @test -z "$$(gofmt -s -l .| grep -v .pb. | grep -v vendor/ | tee /dev/stderr)" + +lint: + @echo "+ $@" + @test -z "$$(golint ./... | grep -v .pb. | grep -v vendor/ | tee /dev/stderr)" + +# Requires that the following: +# go get -u github.com/client9/misspell/cmd/misspell +# +# be run first + +# misspell target, don't include Godeps, binaries, python tests, or git files +misspell: + @echo "+ $@" + @test -z "$$(find . -name '*' | grep -v vendor/ | grep -v bin/ | grep -v misc/ | grep -v .git/ | xargs misspell | tee /dev/stderr)" + +build: + @echo "+ $@" + @go build -tags "${NOTARY_BUILDTAGS}" -v ${GO_LDFLAGS} $(PKGS) + +# When running `go test ./...`, it runs all the suites in parallel, which causes +# problems when running with a yubikey +test: TESTOPTS = +test: + @echo Note: when testing with a yubikey plugged in, make sure to include 'TESTOPTS="-p 1"' + @echo "+ $@ $(TESTOPTS)" + @echo + go test -tags "${NOTARY_BUILDTAGS}" $(TESTOPTS) $(PKGS) + +test-full: TESTOPTS = +test-full: vet lint + @echo Note: when testing with a yubikey plugged in, make sure to include 'TESTOPTS="-p 1"' + @echo "+ $@" + @echo + go test -tags "${NOTARY_BUILDTAGS}" $(TESTOPTS) -v $(PKGS) + +protos: + @protoc --go_out=plugins=grpc:. proto/*.proto + +# This allows coverage for a package to come from tests in different package. +# Requires that the following: +# go get github.com/wadey/gocovmerge; go install github.com/wadey/gocovmerge +# +# be run first + +define gocover +$(GO_EXC) test $(OPTS) $(TESTOPTS) -covermode="$(COVERMODE)" -coverprofile="$(COVERDIR)/$(subst /,-,$(1)).$(subst $(_space),.,$(NOTARY_BUILDTAGS)).coverage.txt" "$(1)" || exit 1; +endef + +gen-cover: + @mkdir -p "$(COVERDIR)" + $(foreach PKG,$(PKGS),$(call gocover,$(PKG))) + rm -f "$(COVERDIR)"/*testutils*.coverage.txt + +# Generates the cover binaries and runs them all in serial, so this can be used +# run all tests with a yubikey without any problems +cover: GO_EXC := go + OPTS = -tags "${NOTARY_BUILDTAGS}" -coverpkg "$(shell ./coverpkg.sh $(1) $(NOTARY_PKG))" +cover: gen-cover covmerge + @go tool cover -html="$(COVERPROFILE)" + +# Generates the cover binaries and runs them all in serial, so this can be used +# run all tests with a yubikey without any problems +ci: OPTS = -tags "${NOTARY_BUILDTAGS}" -race -coverpkg "$(shell ./coverpkg.sh $(1) $(NOTARY_PKG))" + GO_EXC := godep go +# Codecov knows how to merge multiple coverage files, so covmerge is not needed +ci: gen-cover + +yubikey-tests: override PKGS = github.com/docker/notary/cmd/notary github.com/docker/notary/trustmanager/yubikey +yubikey-tests: ci + +covmerge: + @gocovmerge $(shell ls -1 $(COVERDIR)/* | tr "\n" " ") > $(COVERPROFILE) + @go tool cover -func="$(COVERPROFILE)" + +clean-protos: + @rm proto/*.pb.go + +binaries: ${PREFIX}/bin/notary-server ${PREFIX}/bin/notary ${PREFIX}/bin/notary-signer + @echo "+ $@" + +static: ${PREFIX}/bin/static/notary-server ${PREFIX}/bin/static/notary-signer + @echo "+ $@" + +define template +mkdir -p ${PREFIX}/cross/$(1)/$(2); +GOOS=$(1) GOARCH=$(2) CGO_ENABLED=0 go build -o ${PREFIX}/cross/$(1)/$(2)/notary -a -tags "static_build netgo" -installsuffix netgo ${GO_LDFLAGS_STATIC} ./cmd/notary; +endef + +cross: + $(foreach GOARCH,$(GOARCHS),$(foreach GOOS,$(GOOSES),$(call template,$(GOOS),$(GOARCH)))) + + +notary-dockerfile: + @docker build --rm --force-rm -t notary . + +server-dockerfile: + @docker build --rm --force-rm -f server.Dockerfile -t notary-server . + +signer-dockerfile: + @docker build --rm --force-rm -f signer.Dockerfile -t notary-signer . + +docker-images: notary-dockerfile server-dockerfile signer-dockerfile + +shell: notary-dockerfile + docker run --rm -it -v $(CURDIR)/cross:$(NOTARYDIR)/cross -v $(CURDIR)/bin:$(NOTARYDIR)/bin notary bash + + +clean: + @echo "+ $@" + @rm -rf "$(COVERDIR)" + @rm -rf "${PREFIX}/bin/notary-server" "${PREFIX}/bin/notary" "${PREFIX}/bin/notary-signer" diff --git a/vendor/src/github.com/docker/notary/NOTARY_VERSION b/vendor/src/github.com/docker/notary/NOTARY_VERSION new file mode 100644 index 00000000..3b04cfb6 --- /dev/null +++ b/vendor/src/github.com/docker/notary/NOTARY_VERSION @@ -0,0 +1 @@ +0.2 diff --git a/vendor/src/github.com/docker/notary/README.md b/vendor/src/github.com/docker/notary/README.md new file mode 100644 index 00000000..96cd5be5 --- /dev/null +++ b/vendor/src/github.com/docker/notary/README.md @@ -0,0 +1,194 @@ +# Notary +[![Circle CI](https://circleci.com/gh/docker/notary/tree/master.svg?style=shield)](https://circleci.com/gh/docker/notary/tree/master) [![CodeCov](https://codecov.io/github/docker/notary/coverage.svg?branch=master)](https://codecov.io/github/docker/notary) + +The Notary project comprises a [server](cmd/notary-server) and a [client](cmd/notary) for running and interacting +with trusted collections. + + +Notary aims to make the internet more secure by making it easy for people to +publish and verify content. We often rely on TLS to secure our communications +with a web server which is inherently flawed, as any compromise of the server +enables malicious content to be substituted for the legitimate content. + +With Notary, publishers can sign their content offline using keys kept highly +secure. Once the publisher is ready to make the content available, they can +push their signed trusted collection to a Notary Server. + +Consumers, having acquired the publisher's public key through a secure channel, +can then communicate with any notary server or (insecure) mirror, relying +only on the publisher's key to determine the validity and integrity of the +received content. + +## Goals + +Notary is based on [The Update Framework](http://theupdateframework.com/), a secure general design for the problem of software distribution and updates. By using TUF, notary achieves a number of key advantages: + +* **Survivable Key Compromise**: Content publishers must manage keys in order to sign their content. Signing keys may be compromised or lost so systems must be designed in order to be flexible and recoverable in the case of key compromise. TUF's notion of key roles is utilized to separate responsibilities across a hierarchy of keys such that loss of any particular key (except the root role) by itself is not fatal to the security of the system. +* **Freshness Guarantees**: Replay attacks are a common problem in designing secure systems, where previously valid payloads are replayed to trick another system. The same problem exists in the software update systems, where old signed can be presented as the most recent. notary makes use of timestamping on publishing so that consumers can know that they are receiving the most up to date content. This is particularly important when dealing with software update where old vulnerable versions could be used to attack users. +* **Configurable Trust Thresholds**: Oftentimes there are a large number of publishers that are allowed to publish a particular piece of content. For example, open source projects where there are a number of core maintainers. Trust thresholds can be used so that content consumers require a configurable number of signatures on a piece of content in order to trust it. Using thresholds increases security so that loss of individual signing keys doesn't allow publishing of malicious content. +* **Signing Delegation**: To allow for flexible publishing of trusted collections, a content publisher can delegate part of their collection to another signer. This delegation is represented as signed metadata so that a consumer of the content can verify both the content and the delegation. +* **Use of Existing Distribution**: Notary's trust guarantees are not tied at all to particular distribution channels from which content is delivered. Therefore, trust can be added to any existing content delivery mechanism. +* **Untrusted Mirrors and Transport**: All of the notary metadata can be mirrored and distributed via arbitrary channels. + +# Notary CLI + +Notary is a tool for publishing and managing trusted collections of content. Publishers can digitally sign collections and consumers can verify integrity and origin of content. This ability is built on a straightforward key management and signing interface to create signed collections and configure trusted publishers. + +## Using Notary +Lets try using notary. + +Prerequisites: + +- Requirements from the [Compiling Notary Server](#compiling-notary-server) section (such as go 1.5.1) +- [docker and docker-compose](http://docs.docker.com/compose/install/) +- [Notary server configuration](#configuring-notary-server) + +As setup, let's build notary and then start up a local notary-server (don't forget to add `127.0.0.1 notary-server` to your `/etc/hosts`, or if using docker-machine, add `$(docker-machine ip) notary-server`). + +```sh +make binaries +docker-compose build +docker-compose up -d +``` + +Note: In order to have notary use the local notary server and development root CA we can load the local development configuration by appending `-c cmd/notary/config.json` to every command. If you would rather not have to use `-c` on every command, copy `cmd/notary/config.json and cmd/notary/root-ca.crt` to `~/.notary`. + + +First, let's initiate a notary collection called `example.com/scripts` + +```sh +notary init example.com/scripts +``` + +Now, look at the keys you created as a result of initialization +```sh +notary key list +``` + +Cool, now add a local file `install.sh` and call it `v1` +```sh +notary add example.com/scripts v1 install.sh +``` + +Wouldn't it be nice if others could know that you've signed this content? Use `publish` to publish your collection to your default notary-server +```sh +notary publish example.com/scripts +``` + +Now, others can pull your trusted collection +```sh +notary list example.com/scripts +``` + +More importantly, they can verify the content of your script by using `notary verify`: +```sh +curl example.com/install.sh | notary verify example.com/scripts v1 | sh +``` + +# Notary Server + +Notary Server manages TUF data over an HTTP API compatible with the +[notary client](cmd/notary). + +It may be configured to use either JWT or HTTP Basic Auth for authentication. +Currently it only supports MySQL for storage of the TUF data, we intend to +expand this to other storage options. + +## Setup for Development + +The notary repository comes with Dockerfiles and a docker-compose file +to facilitate development. Simply run the following commands to start +a notary server with a temporary MySQL database in containers: + +``` +$ docker-compose build +$ docker-compose up +``` + +If you are on Mac OSX with boot2docker or kitematic, you'll need to +update your hosts file such that the name `notary` is associated with +the IP address of your VM (for boot2docker, this can be determined +by running `boot2docker ip`, with kitematic, `echo $DOCKER_HOST` should +show the IP of the VM). If you are using the default Linux setup, +you need to add `127.0.0.1 notary` to your hosts file. + +## Successfully connecting over TLS + +By default notary-server runs with TLS with certificates signed by a local +CA. In order to be able to successfully connect to it using +either `curl` or `openssl`, you will have to use the root CA file in `fixtures/root-ca.crt`. + +OpenSSL example: + +`openssl s_client -connect notary-server:4443 -CAfile fixtures/root-ca.crt` + +## Compiling Notary Server + +Prerequisites: + +- Go = 1.5.1 +- [godep](https://github.com/tools/godep) installed +- libtool development headers installed + +Install dependencies by running `godep restore`. + +From the root of this git repository, run `make binaries`. This will +compile the `notary`, `notary-server`, and `notary-signer` applications and +place them in a `bin` directory at the root of the git repository (the `bin` +directory is ignored by the .gitignore file). + +`notary-signer` depends upon `pkcs11`, which requires that libtool headers be installed (`libtool-dev` on Ubuntu, `libtool-ltdl-devel` on CentOS/RedHat). If you are using Mac OS, you can `brew install libtool`, and run `make binaries` with the following environment variables (assuming a standard installation of Homebrew): + +```sh +export CPATH=/usr/local/include:${CPATH} +export LIBRARY_PATH=/usr/local/lib:${LIBRARY_PATH} +``` + +## Running Notary Server + +The `notary-server` application has the following usage: + +``` +$ bin/notary-server --help +usage: bin/notary-serve + -config="": Path to configuration file + -debug=false: Enable the debugging server on localhost:8080 +``` + +## Configuring Notary Server + +The configuration file must be a json file with the following format: + +```json +{ + "server": { + "addr": ":4443", + "tls_cert_file": "./fixtures/notary-server.crt", + "tls_key_file": "./fixtures/notary-server.key" + }, + "logging": { + "level": 5 + } +} +``` + +The pem and key provided in fixtures are purely for local development and +testing. For production, you must create your own keypair and certificate, +either via the CA of your choice, or a self signed certificate. + +If using the pem and key provided in fixtures, either: +- Add `fixtures/root-ca.crt` to your trusted root certificates +- Use the default configuration for notary client that loads the CA root for you by using the flag `-c ./cmd/notary/config.json` +- Disable TLS verification by adding the following option notary configuration file in `~/.notary/config.json`: + + "skipTLSVerify": true + +Otherwise, you will see TLS errors or X509 errors upon initializing the +notary collection: + +``` +$ notary list diogomonica.com/openvpn +* fatal: Get https://notary-server:4443/v2/: x509: certificate signed by unknown authority +$ notary list diogomonica.com/openvpn -c cmd/notary/config.json +latest b1df2ad7cbc19f06f08b69b4bcd817649b509f3e5420cdd2245a85144288e26d 4056 +``` diff --git a/vendor/src/github.com/docker/notary/ROADMAP.md b/vendor/src/github.com/docker/notary/ROADMAP.md new file mode 100644 index 00000000..b3e9d713 --- /dev/null +++ b/vendor/src/github.com/docker/notary/ROADMAP.md @@ -0,0 +1,7 @@ +# Roadmap + +The Trust project consists of a number of moving parts of which Notary Server is one. Notary Server is the front line metadata service +that clients interact with. It manages TUF metadata and interacts with a pluggable signing service to issue new TUF timestamp +files. + +The Notary-signer is provided as our reference implementation of a signing service. It supports HSMs along with Ed25519 software signing. diff --git a/vendor/src/github.com/docker/notary/certs/certs.go b/vendor/src/github.com/docker/notary/certs/certs.go new file mode 100644 index 00000000..d8ba0d92 --- /dev/null +++ b/vendor/src/github.com/docker/notary/certs/certs.go @@ -0,0 +1,280 @@ +package certs + +import ( + "crypto/x509" + "errors" + "fmt" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/notary/trustmanager" + "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/signed" +) + +// ErrValidationFail is returned when there is no valid trusted certificates +// being served inside of the roots.json +type ErrValidationFail struct { + Reason string +} + +// ErrValidationFail is returned when there is no valid trusted certificates +// being served inside of the roots.json +func (err ErrValidationFail) Error() string { + return fmt.Sprintf("could not validate the path to a trusted root: %s", err.Reason) +} + +// ErrRootRotationFail is returned when we fail to do a full root key rotation +// by either failing to add the new root certificate, or delete the old ones +type ErrRootRotationFail struct { + Reason string +} + +// ErrRootRotationFail is returned when we fail to do a full root key rotation +// by either failing to add the new root certificate, or delete the old ones +func (err ErrRootRotationFail) Error() string { + return fmt.Sprintf("could not rotate trust to a new trusted root: %s", err.Reason) +} + +/* +ValidateRoot receives a new root, validates its correctness and attempts to +do root key rotation if needed. + +First we list the current trusted certificates we have for a particular GUN. If +that list is non-empty means that we've already seen this repository before, and +have a list of trusted certificates for it. In this case, we use this list of +certificates to attempt to validate this root file. + +If the previous validation succeeds, or in the case where we found no trusted +certificates for this particular GUN, we check the integrity of the root by +making sure that it is validated by itself. This means that we will attempt to +validate the root data with the certificates that are included in the root keys +themselves. + +If this last steps succeeds, we attempt to do root rotation, by ensuring that +we only trust the certificates that are present in the new root. + +This mechanism of operation is essentially Trust On First Use (TOFU): if we +have never seen a certificate for a particular CN, we trust it. If later we see +a different certificate for that certificate, we return an ErrValidationFailed error. + +Note that since we only allow trust data to be downloaded over an HTTPS channel +we are using the current public PKI to validate the first download of the certificate +adding an extra layer of security over the normal (SSH style) trust model. +We shall call this: TOFUS. +*/ +func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun string) error { + logrus.Debugf("entered ValidateRoot with dns: %s", gun) + signedRoot, err := data.RootFromSigned(root) + if err != nil { + return err + } + + // Retrieve all the leaf certificates in root for which the CN matches the GUN + allValidCerts, err := validRootLeafCerts(signedRoot, gun) + if err != nil { + logrus.Debugf("error retrieving valid leaf certificates for: %s, %v", gun, err) + return &ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"} + } + + // Retrieve all the trusted certificates that match this gun + certsForCN, err := certStore.GetCertificatesByCN(gun) + if err != nil { + // If the error that we get back is different than ErrNoCertificatesFound + // we couldn't check if there are any certificates with this CN already + // trusted. Let's take the conservative approach and return a failed validation + if _, ok := err.(*trustmanager.ErrNoCertificatesFound); !ok { + logrus.Debugf("error retrieving trusted certificates for: %s, %v", gun, err) + return &ErrValidationFail{Reason: "unable to retrieve trusted certificates"} + } + } + + // If we have certificates that match this specific GUN, let's make sure to + // use them first to validate that this new root is valid. + if len(certsForCN) != 0 { + logrus.Debugf("found %d valid root certificates for %s", len(certsForCN), gun) + err = signed.VerifyRoot(root, 0, trustmanager.CertsToKeys(certsForCN)) + if err != nil { + logrus.Debugf("failed to verify TUF data for: %s, %v", gun, err) + return &ErrValidationFail{Reason: "failed to validate data with current trusted certificates"} + } + } else { + logrus.Debugf("found no currently valid root certificates for %s", gun) + } + + // Validate the integrity of the new root (does it have valid signatures) + err = signed.VerifyRoot(root, 0, trustmanager.CertsToKeys(allValidCerts)) + if err != nil { + logrus.Debugf("failed to verify TUF data for: %s, %v", gun, err) + return &ErrValidationFail{Reason: "failed to validate integrity of roots"} + } + + // Getting here means A) we had trusted certificates and both the + // old and new validated this root; or B) we had no trusted certificates but + // the new set of certificates has integrity (self-signed) + logrus.Debugf("entering root certificate rotation for: %s", gun) + + // Do root certificate rotation: we trust only the certs present in the new root + // First we add all the new certificates (even if they already exist) + for _, cert := range allValidCerts { + err := certStore.AddCert(cert) + if err != nil { + // If the error is already exists we don't fail the rotation + if _, ok := err.(*trustmanager.ErrCertExists); ok { + logrus.Debugf("ignoring certificate addition to: %s", gun) + continue + } + logrus.Debugf("error adding new trusted certificate for: %s, %v", gun, err) + } + } + + // Now we delete old certificates that aren't present in the new root + for certID, cert := range certsToRemove(certsForCN, allValidCerts) { + logrus.Debugf("removing certificate with certID: %s", certID) + err = certStore.RemoveCert(cert) + if err != nil { + logrus.Debugf("failed to remove trusted certificate with keyID: %s, %v", certID, err) + return &ErrRootRotationFail{Reason: "failed to rotate root keys"} + } + } + + logrus.Debugf("Root validation succeeded for %s", gun) + return nil +} + +// validRootLeafCerts returns a list of non-exipired, non-sha1 certificates whose +// Common-Names match the provided GUN +func validRootLeafCerts(root *data.SignedRoot, gun string) ([]*x509.Certificate, error) { + // Get a list of all of the leaf certificates present in root + allLeafCerts, _ := parseAllCerts(root) + var validLeafCerts []*x509.Certificate + + // Go through every leaf certificate and check that the CN matches the gun + for _, cert := range allLeafCerts { + // Validate that this leaf certificate has a CN that matches the exact gun + if cert.Subject.CommonName != gun { + logrus.Debugf("error leaf certificate CN: %s doesn't match the given GUN: %s", + cert.Subject.CommonName, gun) + continue + } + // Make sure the certificate is not expired + if time.Now().After(cert.NotAfter) { + logrus.Debugf("error leaf certificate is expired") + continue + } + + // We don't allow root certificates that use SHA1 + if cert.SignatureAlgorithm == x509.SHA1WithRSA || + cert.SignatureAlgorithm == x509.DSAWithSHA1 || + cert.SignatureAlgorithm == x509.ECDSAWithSHA1 { + + logrus.Debugf("error certificate uses deprecated hashing algorithm (SHA1)") + continue + } + + validLeafCerts = append(validLeafCerts, cert) + } + + if len(validLeafCerts) < 1 { + logrus.Debugf("didn't find any valid leaf certificates for %s", gun) + return nil, errors.New("no valid leaf certificates found in any of the root keys") + } + + logrus.Debugf("found %d valid leaf certificates for %s", len(validLeafCerts), gun) + return validLeafCerts, nil +} + +// parseAllCerts returns two maps, one with all of the leafCertificates and one +// with all the intermediate certificates found in signedRoot +func parseAllCerts(signedRoot *data.SignedRoot) (map[string]*x509.Certificate, map[string][]*x509.Certificate) { + leafCerts := make(map[string]*x509.Certificate) + intCerts := make(map[string][]*x509.Certificate) + + // Before we loop through all root keys available, make sure any exist + rootRoles, ok := signedRoot.Signed.Roles["root"] + if !ok { + logrus.Debugf("tried to parse certificates from invalid root signed data") + return nil, nil + } + + logrus.Debugf("found the following root keys: %v", rootRoles.KeyIDs) + // Iterate over every keyID for the root role inside of roots.json + for _, keyID := range rootRoles.KeyIDs { + // check that the key exists in the signed root keys map + key, ok := signedRoot.Signed.Keys[keyID] + if !ok { + logrus.Debugf("error while getting data for keyID: %s", keyID) + continue + } + + // Decode all the x509 certificates that were bundled with this + // Specific root key + decodedCerts, err := trustmanager.LoadCertBundleFromPEM(key.Public()) + if err != nil { + logrus.Debugf("error while parsing root certificate with keyID: %s, %v", keyID, err) + continue + } + + // Get all non-CA certificates in the decoded certificates + leafCertList := trustmanager.GetLeafCerts(decodedCerts) + + // If we got no leaf certificates or we got more than one, fail + if len(leafCertList) != 1 { + logrus.Debugf("invalid chain due to leaf certificate missing or too many leaf certificates for keyID: %s", keyID) + continue + } + + // Get the ID of the leaf certificate + leafCert := leafCertList[0] + leafID, err := trustmanager.FingerprintCert(leafCert) + if err != nil { + logrus.Debugf("error while fingerprinting root certificate with keyID: %s, %v", keyID, err) + continue + } + + // Store the leaf cert in the map + leafCerts[leafID] = leafCert + + // Get all the remainder certificates marked as a CA to be used as intermediates + intermediateCerts := trustmanager.GetIntermediateCerts(decodedCerts) + intCerts[leafID] = intermediateCerts + } + + return leafCerts, intCerts +} + +// certsToRemove returns all the certifificates from oldCerts that aren't present +// in newCerts +func certsToRemove(oldCerts, newCerts []*x509.Certificate) map[string]*x509.Certificate { + certsToRemove := make(map[string]*x509.Certificate) + + // If no newCerts were provided + if len(newCerts) == 0 { + return certsToRemove + } + + // Populate a map with all the IDs from newCert + var newCertMap = make(map[string]struct{}) + for _, cert := range newCerts { + certID, err := trustmanager.FingerprintCert(cert) + if err != nil { + logrus.Debugf("error while fingerprinting root certificate with keyID: %s, %v", certID, err) + continue + } + newCertMap[certID] = struct{}{} + } + + // Iterate over all the old certificates and check to see if we should remove them + for _, cert := range oldCerts { + certID, err := trustmanager.FingerprintCert(cert) + if err != nil { + logrus.Debugf("error while fingerprinting root certificate with certID: %s, %v", certID, err) + continue + } + if _, ok := newCertMap[certID]; !ok { + certsToRemove[certID] = cert + } + } + + return certsToRemove +} diff --git a/vendor/src/github.com/docker/notary/circle.yml b/vendor/src/github.com/docker/notary/circle.yml new file mode 100644 index 00000000..6b98a161 --- /dev/null +++ b/vendor/src/github.com/docker/notary/circle.yml @@ -0,0 +1,82 @@ +# Pony-up! +machine: + pre: + # Install gvm + - bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/1.0.22/binscripts/gvm-installer) + + post: + # Install many go versions + - gvm install go1.6 -B --name=stable + + environment: + # Convenient shortcuts to "common" locations + CHECKOUT: /home/ubuntu/$CIRCLE_PROJECT_REPONAME + BASE_DIR: src/github.com/docker/notary + # Trick circle brainflat "no absolute path" behavior + BASE_STABLE: ../../../$HOME/.gvm/pkgsets/stable/global/$BASE_DIR + # Workaround Circle parsing dumb bugs and/or YAML wonkyness + CIRCLE_PAIN: "mode: set" + # Put the coverage profile somewhere codecov's script can find it + COVERPROFILE: coverage.out + + hosts: + # Not used yet + fancy: 127.0.0.1 + +dependencies: + pre: + # Copy the code to the gopath of all go versions + - > + gvm use stable && + mkdir -p "$(dirname $BASE_STABLE)" && + cp -R "$CHECKOUT" "$BASE_STABLE" + + override: + # Install dependencies for every copied clone/go version + - gvm use stable && go get github.com/tools/godep: + pwd: $BASE_STABLE + + post: + # For the stable go version, additionally install linting and misspell tools + - > + gvm use stable && + go get github.com/golang/lint/golint && + go get -u github.com/client9/misspell/cmd/misspell +test: + pre: + # Output the go versions we are going to test + - gvm use stable && go version + + # CLEAN + - gvm use stable && make clean: + pwd: $BASE_STABLE + + # FMT + - gvm use stable && make fmt: + pwd: $BASE_STABLE + + # VET + - gvm use stable && make vet: + pwd: $BASE_STABLE + + # LINT + - gvm use stable && make lint: + pwd: $BASE_STABLE + + # MISSPELL + - gvm use stable && make misspell: + pwd: $BASE_STABLE + + override: + # Test stable, and report + # hacking this to be parallel + - case $CIRCLE_NODE_INDEX in 0) gvm use stable && NOTARY_BUILDTAGS=pkcs11 make ci ;; 1) gvm use stable && NOTARY_BUILDTAGS=none make ci ;; esac: + parallel: true + timeout: 600 + pwd: $BASE_STABLE + + post: + # Report to codecov.io + - bash <(curl -s https://codecov.io/bash): + parallel: true + pwd: $BASE_STABLE diff --git a/vendor/src/github.com/docker/notary/client/changelist/change.go b/vendor/src/github.com/docker/notary/client/changelist/change.go new file mode 100644 index 00000000..3307189c --- /dev/null +++ b/vendor/src/github.com/docker/notary/client/changelist/change.go @@ -0,0 +1,101 @@ +package changelist + +import ( + "github.com/docker/notary/tuf/data" +) + +// Scopes for TufChanges are simply the TUF roles. +// Unfortunately because of targets delegations, we can only +// cover the base roles. +const ( + ScopeRoot = "root" + ScopeTargets = "targets" + ScopeSnapshot = "snapshot" + ScopeTimestamp = "timestamp" +) + +// Types for TufChanges are namespaced by the Role they +// are relevant for. The Root and Targets roles are the +// only ones for which user action can cause a change, as +// all changes in Snapshot and Timestamp are programmatically +// generated base on Root and Targets changes. +const ( + TypeRootRole = "role" + TypeTargetsTarget = "target" + TypeTargetsDelegation = "delegation" +) + +// TufChange represents a change to a TUF repo +type TufChange struct { + // Abbreviated because Go doesn't permit a field and method of the same name + Actn string `json:"action"` + Role string `json:"role"` + ChangeType string `json:"type"` + ChangePath string `json:"path"` + Data []byte `json:"data"` +} + +// TufRootData represents a modification of the keys associated +// with a role that appears in the root.json +type TufRootData struct { + Keys data.KeyList `json:"keys"` + RoleName string `json:"role"` +} + +// NewTufChange initializes a tufChange object +func NewTufChange(action string, role, changeType, changePath string, content []byte) *TufChange { + return &TufChange{ + Actn: action, + Role: role, + ChangeType: changeType, + ChangePath: changePath, + Data: content, + } +} + +// Action return c.Actn +func (c TufChange) Action() string { + return c.Actn +} + +// Scope returns c.Role +func (c TufChange) Scope() string { + return c.Role +} + +// Type returns c.ChangeType +func (c TufChange) Type() string { + return c.ChangeType +} + +// Path return c.ChangePath +func (c TufChange) Path() string { + return c.ChangePath +} + +// Content returns c.Data +func (c TufChange) Content() []byte { + return c.Data +} + +// TufDelegation represents a modification to a target delegation +// this includes creating a delegations. This format is used to avoid +// unexpected race conditions between humans modifying the same delegation +type TufDelegation struct { + NewName string `json:"new_name,omitempty"` + NewThreshold int `json:"threshold, omitempty"` + AddKeys data.KeyList `json:"add_keys, omitempty"` + RemoveKeys []string `json:"remove_keys,omitempty"` + AddPaths []string `json:"add_paths,omitempty"` + RemovePaths []string `json:"remove_paths,omitempty"` + ClearAllPaths bool `json:"clear_paths,omitempty"` +} + +// ToNewRole creates a fresh role object from the TufDelegation data +func (td TufDelegation) ToNewRole(scope string) (*data.Role, error) { + name := scope + if td.NewName != "" { + name = td.NewName + } + return data.NewRole(name, td.NewThreshold, td.AddKeys.IDs(), td.AddPaths) +} diff --git a/vendor/src/github.com/docker/notary/client/changelist/changelist.go b/vendor/src/github.com/docker/notary/client/changelist/changelist.go new file mode 100644 index 00000000..a91b16cb --- /dev/null +++ b/vendor/src/github.com/docker/notary/client/changelist/changelist.go @@ -0,0 +1,59 @@ +package changelist + +// memChangeList implements a simple in memory change list. +type memChangelist struct { + changes []Change +} + +// NewMemChangelist instantiates a new in-memory changelist +func NewMemChangelist() Changelist { + return &memChangelist{} +} + +// List returns a list of Changes +func (cl memChangelist) List() []Change { + return cl.changes +} + +// Add adds a change to the in-memory change list +func (cl *memChangelist) Add(c Change) error { + cl.changes = append(cl.changes, c) + return nil +} + +// Clear empties the changelist file. +func (cl *memChangelist) Clear(archive string) error { + // appending to a nil list initializes it. + cl.changes = nil + return nil +} + +// Close is a no-op in this in-memory change-list +func (cl *memChangelist) Close() error { + return nil +} + +func (cl *memChangelist) NewIterator() (ChangeIterator, error) { + return &MemChangeListIterator{index: 0, collection: cl.changes}, nil +} + +// MemChangeListIterator is a concrete instance of ChangeIterator +type MemChangeListIterator struct { + index int + collection []Change // Same type as memChangeList.changes +} + +// Next returns the next Change +func (m *MemChangeListIterator) Next() (item Change, err error) { + if m.index >= len(m.collection) { + return nil, IteratorBoundsError(m.index) + } + item = m.collection[m.index] + m.index++ + return item, err +} + +// HasNext indicates whether the iterator is exhausted +func (m *MemChangeListIterator) HasNext() bool { + return m.index < len(m.collection) +} diff --git a/vendor/src/github.com/docker/notary/client/changelist/file_changelist.go b/vendor/src/github.com/docker/notary/client/changelist/file_changelist.go new file mode 100644 index 00000000..5ab237d8 --- /dev/null +++ b/vendor/src/github.com/docker/notary/client/changelist/file_changelist.go @@ -0,0 +1,176 @@ +package changelist + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path" + "sort" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/uuid" +) + +// FileChangelist stores all the changes as files +type FileChangelist struct { + dir string +} + +// NewFileChangelist is a convenience method for returning FileChangeLists +func NewFileChangelist(dir string) (*FileChangelist, error) { + logrus.Debug("Making dir path: ", dir) + err := os.MkdirAll(dir, 0700) + if err != nil { + return nil, err + } + return &FileChangelist{dir: dir}, nil +} + +// getFileNames reads directory, filtering out child directories +func getFileNames(dirName string) ([]os.FileInfo, error) { + var dirListing, fileInfos []os.FileInfo + dir, err := os.Open(dirName) + if err != nil { + return fileInfos, err + } + defer dir.Close() + dirListing, err = dir.Readdir(0) + if err != nil { + return fileInfos, err + } + for _, f := range dirListing { + if f.IsDir() { + continue + } + fileInfos = append(fileInfos, f) + } + return fileInfos, nil +} + +// Read a JSON formatted file from disk; convert to TufChange struct +func unmarshalFile(dirname string, f os.FileInfo) (*TufChange, error) { + c := &TufChange{} + raw, err := ioutil.ReadFile(path.Join(dirname, f.Name())) + if err != nil { + return c, err + } + err = json.Unmarshal(raw, c) + if err != nil { + return c, err + } + return c, nil +} + +// List returns a list of sorted changes +func (cl FileChangelist) List() []Change { + var changes []Change + fileInfos, err := getFileNames(cl.dir) + if err != nil { + return changes + } + sort.Sort(fileChanges(fileInfos)) + for _, f := range fileInfos { + c, err := unmarshalFile(cl.dir, f) + if err != nil { + logrus.Warn(err.Error()) + continue + } + changes = append(changes, c) + } + return changes +} + +// Add adds a change to the file change list +func (cl FileChangelist) Add(c Change) error { + cJSON, err := json.Marshal(c) + if err != nil { + return err + } + filename := fmt.Sprintf("%020d_%s.change", time.Now().UnixNano(), uuid.Generate()) + return ioutil.WriteFile(path.Join(cl.dir, filename), cJSON, 0644) +} + +// Clear clears the change list +func (cl FileChangelist) Clear(archive string) error { + dir, err := os.Open(cl.dir) + if err != nil { + return err + } + defer dir.Close() + files, err := dir.Readdir(0) + if err != nil { + return err + } + for _, f := range files { + os.Remove(path.Join(cl.dir, f.Name())) + } + return nil +} + +// Close is a no-op +func (cl FileChangelist) Close() error { + // Nothing to do here + return nil +} + +// NewIterator creates an iterator from FileChangelist +func (cl FileChangelist) NewIterator() (ChangeIterator, error) { + fileInfos, err := getFileNames(cl.dir) + if err != nil { + return &FileChangeListIterator{}, err + } + sort.Sort(fileChanges(fileInfos)) + return &FileChangeListIterator{dirname: cl.dir, collection: fileInfos}, nil +} + +// IteratorBoundsError is an Error type used by Next() +type IteratorBoundsError int + +// Error implements the Error interface +func (e IteratorBoundsError) Error() string { + return fmt.Sprintf("Iterator index (%d) out of bounds", e) +} + +// FileChangeListIterator is a concrete instance of ChangeIterator +type FileChangeListIterator struct { + index int + dirname string + collection []os.FileInfo +} + +// Next returns the next Change in the FileChangeList +func (m *FileChangeListIterator) Next() (item Change, err error) { + if m.index >= len(m.collection) { + return nil, IteratorBoundsError(m.index) + } + f := m.collection[m.index] + m.index++ + item, err = unmarshalFile(m.dirname, f) + return +} + +// HasNext indicates whether iterator is exhausted +func (m *FileChangeListIterator) HasNext() bool { + return m.index < len(m.collection) +} + +type fileChanges []os.FileInfo + +// Len returns the length of a file change list +func (cs fileChanges) Len() int { + return len(cs) +} + +// Less compares the names of two different file changes +func (cs fileChanges) Less(i, j int) bool { + return cs[i].Name() < cs[j].Name() +} + +// Swap swaps the position of two file changes +func (cs fileChanges) Swap(i, j int) { + tmp := cs[i] + cs[i] = cs[j] + cs[j] = tmp +} diff --git a/vendor/src/github.com/docker/notary/client/changelist/interface.go b/vendor/src/github.com/docker/notary/client/changelist/interface.go new file mode 100644 index 00000000..4369623c --- /dev/null +++ b/vendor/src/github.com/docker/notary/client/changelist/interface.go @@ -0,0 +1,70 @@ +package changelist + +// Changelist is the interface for all TUF change lists +type Changelist interface { + // List returns the ordered list of changes + // currently stored + List() []Change + + // Add change appends the provided change to + // the list of changes + Add(Change) error + + // Clear empties the current change list. + // Archive may be provided as a directory path + // to save a copy of the changelist in that location + Clear(archive string) error + + // Close syncronizes any pending writes to the underlying + // storage and closes the file/connection + Close() error + + // NewIterator returns an iterator for walking through the list + // of changes currently stored + NewIterator() (ChangeIterator, error) +} + +const ( + // ActionCreate represents a Create action + ActionCreate = "create" + // ActionUpdate represents an Update action + ActionUpdate = "update" + // ActionDelete represents a Delete action + ActionDelete = "delete" +) + +// Change is the interface for a TUF Change +type Change interface { + // "create","update", or "delete" + Action() string + + // Where the change should be made. + // For TUF this will be the role + Scope() string + + // The content type being affected. + // For TUF this will be "target", or "delegation". + // If the type is "delegation", the Scope will be + // used to determine if a root role is being updated + // or a target delegation. + Type() string + + // Path indicates the entry within a role to be affected by the + // change. For targets, this is simply the target's path, + // for delegations it's the delegated role name. + Path() string + + // Serialized content that the interpreter of a changelist + // can use to apply the change. + // For TUF this will be the serialized JSON that needs + // to be inserted or merged. In the case of a "delete" + // action, it will be nil. + Content() []byte +} + +// ChangeIterator is the interface for iterating across collections of +// TUF Change items +type ChangeIterator interface { + Next() (Change, error) + HasNext() bool +} diff --git a/vendor/src/github.com/docker/notary/client/client.go b/vendor/src/github.com/docker/notary/client/client.go new file mode 100644 index 00000000..4dfb6b5c --- /dev/null +++ b/vendor/src/github.com/docker/notary/client/client.go @@ -0,0 +1,965 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/notary" + "github.com/docker/notary/certs" + "github.com/docker/notary/client/changelist" + "github.com/docker/notary/cryptoservice" + "github.com/docker/notary/trustmanager" + "github.com/docker/notary/tuf" + tufclient "github.com/docker/notary/tuf/client" + "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/signed" + "github.com/docker/notary/tuf/store" + "github.com/docker/notary/tuf/utils" +) + +func init() { + data.SetDefaultExpiryTimes(notary.NotaryDefaultExpiries) +} + +// ErrRepoNotInitialized is returned when trying to publish an uninitialized +// notary repository +type ErrRepoNotInitialized struct{} + +func (err ErrRepoNotInitialized) Error() string { + return "repository has not been initialized" +} + +// ErrInvalidRemoteRole is returned when the server is requested to manage +// a key type that is not permitted +type ErrInvalidRemoteRole struct { + Role string +} + +func (err ErrInvalidRemoteRole) Error() string { + return fmt.Sprintf( + "notary does not permit the server managing the %s key", err.Role) +} + +// ErrInvalidLocalRole is returned when the client wants to manage +// a key type that is not permitted +type ErrInvalidLocalRole struct { + Role string +} + +func (err ErrInvalidLocalRole) Error() string { + return fmt.Sprintf( + "notary does not permit the client managing the %s key", err.Role) +} + +// ErrRepositoryNotExist is returned when an action is taken on a remote +// repository that doesn't exist +type ErrRepositoryNotExist struct { + remote string + gun string +} + +func (err ErrRepositoryNotExist) Error() string { + return fmt.Sprintf("%s does not have trust data for %s", err.remote, err.gun) +} + +const ( + tufDir = "tuf" +) + +// NotaryRepository stores all the information needed to operate on a notary +// repository. +type NotaryRepository struct { + baseDir string + gun string + baseURL string + tufRepoPath string + fileStore store.MetadataStore + CryptoService signed.CryptoService + tufRepo *tuf.Repo + roundTrip http.RoundTripper + CertStore trustmanager.X509Store +} + +// repositoryFromKeystores is a helper function for NewNotaryRepository that +// takes some basic NotaryRepository parameters as well as keystores (in order +// of usage preference), and returns a NotaryRepository. +func repositoryFromKeystores(baseDir, gun, baseURL string, rt http.RoundTripper, + keyStores []trustmanager.KeyStore) (*NotaryRepository, error) { + + certPath := filepath.Join(baseDir, notary.TrustedCertsDir) + certStore, err := trustmanager.NewX509FilteredFileStore( + certPath, + trustmanager.FilterCertsExpiredSha1, + ) + if err != nil { + return nil, err + } + + cryptoService := cryptoservice.NewCryptoService(keyStores...) + + nRepo := &NotaryRepository{ + gun: gun, + baseDir: baseDir, + baseURL: baseURL, + tufRepoPath: filepath.Join(baseDir, tufDir, filepath.FromSlash(gun)), + CryptoService: cryptoService, + roundTrip: rt, + CertStore: certStore, + } + + fileStore, err := store.NewFilesystemStore( + nRepo.tufRepoPath, + "metadata", + "json", + ) + if err != nil { + return nil, err + } + nRepo.fileStore = fileStore + + return nRepo, nil +} + +// Target represents a simplified version of the data TUF operates on, so external +// applications don't have to depend on tuf data types. +type Target struct { + Name string // the name of the target + Hashes data.Hashes // the hash of the target + Length int64 // the size in bytes of the target +} + +// TargetWithRole represents a Target that exists in a particular role - this is +// produced by ListTargets and GetTargetByName +type TargetWithRole struct { + Target + Role string +} + +// NewTarget is a helper method that returns a Target +func NewTarget(targetName string, targetPath string) (*Target, error) { + b, err := ioutil.ReadFile(targetPath) + if err != nil { + return nil, err + } + + meta, err := data.NewFileMeta(bytes.NewBuffer(b), data.NotaryDefaultHashes...) + if err != nil { + return nil, err + } + + return &Target{Name: targetName, Hashes: meta.Hashes, Length: meta.Length}, nil +} + +// Initialize creates a new repository by using rootKey as the root Key for the +// TUF repository. +func (r *NotaryRepository) Initialize(rootKeyID string, serverManagedRoles ...string) error { + privKey, _, err := r.CryptoService.GetPrivateKey(rootKeyID) + if err != nil { + return err + } + + // currently we only support server managing timestamps and snapshots, and + // nothing else - timestamps are always managed by the server, and implicit + // (do not have to be passed in as part of `serverManagedRoles`, so that + // the API of Initialize doesn't change). + var serverManagesSnapshot bool + locallyManagedKeys := []string{ + data.CanonicalTargetsRole, + data.CanonicalSnapshotRole, + // root is also locally managed, but that should have been created + // already + } + remotelyManagedKeys := []string{data.CanonicalTimestampRole} + for _, role := range serverManagedRoles { + switch role { + case data.CanonicalTimestampRole: + continue // timestamp is already in the right place + case data.CanonicalSnapshotRole: + // because we put Snapshot last + locallyManagedKeys = []string{data.CanonicalTargetsRole} + remotelyManagedKeys = append( + remotelyManagedKeys, data.CanonicalSnapshotRole) + serverManagesSnapshot = true + default: + return ErrInvalidRemoteRole{Role: role} + } + } + + // Hard-coded policy: the generated certificate expires in 10 years. + startTime := time.Now() + rootCert, err := cryptoservice.GenerateCertificate( + privKey, r.gun, startTime, startTime.AddDate(10, 0, 0)) + + if err != nil { + return err + } + r.CertStore.AddCert(rootCert) + + // The root key gets stored in the TUF metadata X509 encoded, linking + // the tuf root.json to our X509 PKI. + // If the key is RSA, we store it as type RSAx509, if it is ECDSA we store it + // as ECDSAx509 to allow the gotuf verifiers to correctly decode the + // key on verification of signatures. + var rootKey data.PublicKey + switch privKey.Algorithm() { + case data.RSAKey: + rootKey = data.NewRSAx509PublicKey(trustmanager.CertToPEM(rootCert)) + case data.ECDSAKey: + rootKey = data.NewECDSAx509PublicKey(trustmanager.CertToPEM(rootCert)) + default: + return fmt.Errorf("invalid format for root key: %s", privKey.Algorithm()) + } + + var ( + rootRole = data.NewBaseRole( + data.CanonicalRootRole, + notary.MinThreshold, + rootKey, + ) + timestampRole data.BaseRole + snapshotRole data.BaseRole + targetsRole data.BaseRole + ) + + // we want to create all the local keys first so we don't have to + // make unnecessary network calls + for _, role := range locallyManagedKeys { + // This is currently hardcoding the keys to ECDSA. + key, err := r.CryptoService.Create(role, r.gun, data.ECDSAKey) + if err != nil { + return err + } + switch role { + case data.CanonicalSnapshotRole: + snapshotRole = data.NewBaseRole( + role, + notary.MinThreshold, + key, + ) + case data.CanonicalTargetsRole: + targetsRole = data.NewBaseRole( + role, + notary.MinThreshold, + key, + ) + } + } + for _, role := range remotelyManagedKeys { + // This key is generated by the remote server. + key, err := getRemoteKey(r.baseURL, r.gun, role, r.roundTrip) + if err != nil { + return err + } + logrus.Debugf("got remote %s %s key with keyID: %s", + role, key.Algorithm(), key.ID()) + switch role { + case data.CanonicalSnapshotRole: + snapshotRole = data.NewBaseRole( + role, + notary.MinThreshold, + key, + ) + case data.CanonicalTimestampRole: + timestampRole = data.NewBaseRole( + role, + notary.MinThreshold, + key, + ) + } + } + + r.tufRepo = tuf.NewRepo(r.CryptoService) + + err = r.tufRepo.InitRoot( + rootRole, + timestampRole, + snapshotRole, + targetsRole, + false, + ) + if err != nil { + logrus.Debug("Error on InitRoot: ", err.Error()) + return err + } + _, err = r.tufRepo.InitTargets(data.CanonicalTargetsRole) + if err != nil { + logrus.Debug("Error on InitTargets: ", err.Error()) + return err + } + err = r.tufRepo.InitSnapshot() + if err != nil { + logrus.Debug("Error on InitSnapshot: ", err.Error()) + return err + } + + return r.saveMetadata(serverManagesSnapshot) +} + +// adds a TUF Change template to the given roles +func addChange(cl *changelist.FileChangelist, c changelist.Change, roles ...string) error { + + if len(roles) == 0 { + roles = []string{data.CanonicalTargetsRole} + } + + var changes []changelist.Change + for _, role := range roles { + // Ensure we can only add targets to the CanonicalTargetsRole, + // or a Delegation role (which is /something else) + if role != data.CanonicalTargetsRole && !data.IsDelegation(role) { + return data.ErrInvalidRole{ + Role: role, + Reason: "cannot add targets to this role", + } + } + + changes = append(changes, changelist.NewTufChange( + c.Action(), + role, + c.Type(), + c.Path(), + c.Content(), + )) + } + + for _, c := range changes { + if err := cl.Add(c); err != nil { + return err + } + } + return nil +} + +// AddTarget creates new changelist entries to add a target to the given roles +// in the repository when the changelist gets applied at publish time. +// If roles are unspecified, the default role is "targets". +func (r *NotaryRepository) AddTarget(target *Target, roles ...string) error { + + cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist")) + if err != nil { + return err + } + defer cl.Close() + logrus.Debugf("Adding target \"%s\" with sha256 \"%x\" and size %d bytes.\n", target.Name, target.Hashes["sha256"], target.Length) + + meta := data.FileMeta{Length: target.Length, Hashes: target.Hashes} + metaJSON, err := json.Marshal(meta) + if err != nil { + return err + } + + template := changelist.NewTufChange( + changelist.ActionCreate, "", changelist.TypeTargetsTarget, + target.Name, metaJSON) + return addChange(cl, template, roles...) +} + +// RemoveTarget creates new changelist entries to remove a target from the given +// roles in the repository when the changelist gets applied at publish time. +// If roles are unspecified, the default role is "target". +func (r *NotaryRepository) RemoveTarget(targetName string, roles ...string) error { + + cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist")) + if err != nil { + return err + } + logrus.Debugf("Removing target \"%s\"", targetName) + template := changelist.NewTufChange(changelist.ActionDelete, "", + changelist.TypeTargetsTarget, targetName, nil) + return addChange(cl, template, roles...) +} + +// ListTargets lists all targets for the current repository. The list of +// roles should be passed in order from highest to lowest priority. +// IMPORTANT: if you pass a set of roles such as [ "targets/a", "targets/x" +// "targets/a/b" ], even though "targets/a/b" is part of the "targets/a" subtree +// its entries will be strictly shadowed by those in other parts of the "targets/a" +// subtree and also the "targets/x" subtree, as we will defer parsing it until +// we explicitly reach it in our iteration of the provided list of roles. +func (r *NotaryRepository) ListTargets(roles ...string) ([]*TargetWithRole, error) { + _, err := r.Update(false) + if err != nil { + return nil, err + } + + if len(roles) == 0 { + roles = []string{data.CanonicalTargetsRole} + } + targets := make(map[string]*TargetWithRole) + for _, role := range roles { + // Define an array of roles to skip for this walk (see IMPORTANT comment above) + skipRoles := utils.StrSliceRemove(roles, role) + + // Define a visitor function to populate the targets map in priority order + listVisitorFunc := func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} { + // We found targets so we should try to add them to our targets map + for targetName, targetMeta := range tgt.Signed.Targets { + // Follow the priority by not overriding previously set targets + // and check that this path is valid with this role + if _, ok := targets[targetName]; ok || !validRole.CheckPaths(targetName) { + continue + } + targets[targetName] = + &TargetWithRole{Target: Target{Name: targetName, Hashes: targetMeta.Hashes, Length: targetMeta.Length}, Role: validRole.Name} + } + return nil + } + r.tufRepo.WalkTargets("", role, listVisitorFunc, skipRoles...) + } + + var targetList []*TargetWithRole + for _, v := range targets { + targetList = append(targetList, v) + } + + return targetList, nil +} + +// GetTargetByName returns a target given a name. If no roles are passed +// it uses the targets role and does a search of the entire delegation +// graph, finding the first entry in a breadth first search of the delegations. +// If roles are passed, they should be passed in descending priority and +// the target entry found in the subtree of the highest priority role +// will be returned +// See the IMPORTANT section on ListTargets above. Those roles also apply here. +func (r *NotaryRepository) GetTargetByName(name string, roles ...string) (*TargetWithRole, error) { + _, err := r.Update(false) + if err != nil { + return nil, err + } + + if len(roles) == 0 { + roles = append(roles, data.CanonicalTargetsRole) + } + var resultMeta data.FileMeta + var resultRoleName string + var foundTarget bool + for _, role := range roles { + // Define an array of roles to skip for this walk (see IMPORTANT comment above) + skipRoles := utils.StrSliceRemove(roles, role) + + // Define a visitor function to find the specified target + getTargetVisitorFunc := func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} { + if tgt == nil { + return nil + } + // We found the target and validated path compatibility in our walk, + // so we should stop our walk and set the resultMeta and resultRoleName variables + if resultMeta, foundTarget = tgt.Signed.Targets[name]; foundTarget { + resultRoleName = validRole.Name + return tuf.StopWalk{} + } + return nil + } + err = r.tufRepo.WalkTargets(name, role, getTargetVisitorFunc, skipRoles...) + // Check that we didn't error, and that we assigned to our target + if err == nil && foundTarget { + return &TargetWithRole{Target: Target{Name: name, Hashes: resultMeta.Hashes, Length: resultMeta.Length}, Role: resultRoleName}, nil + } + } + return nil, fmt.Errorf("No trust data for %s", name) + +} + +// GetChangelist returns the list of the repository's unpublished changes +func (r *NotaryRepository) GetChangelist() (changelist.Changelist, error) { + changelistDir := filepath.Join(r.tufRepoPath, "changelist") + cl, err := changelist.NewFileChangelist(changelistDir) + if err != nil { + logrus.Debug("Error initializing changelist") + return nil, err + } + return cl, nil +} + +// RoleWithSignatures is a Role with its associated signatures +type RoleWithSignatures struct { + Signatures []data.Signature + data.Role +} + +// ListRoles returns a list of RoleWithSignatures objects for this repo +// This represents the latest metadata for each role in this repo +func (r *NotaryRepository) ListRoles() ([]RoleWithSignatures, error) { + // Update to latest repo state + _, err := r.Update(false) + if err != nil { + return nil, err + } + + // Get all role info from our updated keysDB, can be empty + roles := r.tufRepo.GetAllLoadedRoles() + + var roleWithSigs []RoleWithSignatures + + // Populate RoleWithSignatures with Role from keysDB and signatures from TUF metadata + for _, role := range roles { + roleWithSig := RoleWithSignatures{Role: *role, Signatures: nil} + switch role.Name { + case data.CanonicalRootRole: + roleWithSig.Signatures = r.tufRepo.Root.Signatures + case data.CanonicalTargetsRole: + roleWithSig.Signatures = r.tufRepo.Targets[data.CanonicalTargetsRole].Signatures + case data.CanonicalSnapshotRole: + roleWithSig.Signatures = r.tufRepo.Snapshot.Signatures + case data.CanonicalTimestampRole: + roleWithSig.Signatures = r.tufRepo.Timestamp.Signatures + default: + // If the role isn't a delegation, we should error -- this is only possible if we have invalid state + if !data.IsDelegation(role.Name) { + return nil, data.ErrInvalidRole{Role: role.Name, Reason: "invalid role name"} + } + if _, ok := r.tufRepo.Targets[role.Name]; ok { + // We'll only find a signature if we've published any targets with this delegation + roleWithSig.Signatures = r.tufRepo.Targets[role.Name].Signatures + } + } + roleWithSigs = append(roleWithSigs, roleWithSig) + } + return roleWithSigs, nil +} + +// Publish pushes the local changes in signed material to the remote notary-server +// Conceptually it performs an operation similar to a `git rebase` +func (r *NotaryRepository) Publish() error { + cl, err := r.GetChangelist() + if err != nil { + return err + } + if err = r.publish(cl); err != nil { + return err + } + if err = cl.Clear(""); err != nil { + // This is not a critical problem when only a single host is pushing + // but will cause weird behaviour if changelist cleanup is failing + // and there are multiple hosts writing to the repo. + logrus.Warn("Unable to clear changelist. You may want to manually delete the folder ", filepath.Join(r.tufRepoPath, "changelist")) + } + return nil +} + +// publish pushes the changes in the given changelist to the remote notary-server +// Conceptually it performs an operation similar to a `git rebase` +func (r *NotaryRepository) publish(cl changelist.Changelist) error { + var initialPublish bool + // update first before publishing + _, err := r.Update(true) + if err != nil { + // If the remote is not aware of the repo, then this is being published + // for the first time. Try to load from disk instead for publishing. + if _, ok := err.(ErrRepositoryNotExist); ok { + err := r.bootstrapRepo() + if err != nil { + logrus.Debugf("Unable to load repository from local files: %s", + err.Error()) + if _, ok := err.(store.ErrMetaNotFound); ok { + return ErrRepoNotInitialized{} + } + return err + } + // Ensure we will push the initial root and targets file. Either or + // both of the root and targets may not be marked as Dirty, since + // there may not be any changes that update them, so use a + // different boolean. + initialPublish = true + } else { + // We could not update, so we cannot publish. + logrus.Error("Could not publish Repository since we could not update: ", err.Error()) + return err + } + } + // apply the changelist to the repo + err = applyChangelist(r.tufRepo, cl) + if err != nil { + logrus.Debug("Error applying changelist") + return err + } + + // these are the tuf files we will need to update, serialized as JSON before + // we send anything to remote + updatedFiles := make(map[string][]byte) + + // check if our root file is nearing expiry or dirty. Resign if it is. If + // root is not dirty but we are publishing for the first time, then just + // publish the existing root we have. + if nearExpiry(r.tufRepo.Root) || r.tufRepo.Root.Dirty { + rootJSON, err := serializeCanonicalRole(r.tufRepo, data.CanonicalRootRole) + if err != nil { + return err + } + updatedFiles[data.CanonicalRootRole] = rootJSON + } else if initialPublish { + rootJSON, err := r.tufRepo.Root.MarshalJSON() + if err != nil { + return err + } + updatedFiles[data.CanonicalRootRole] = rootJSON + } + + // iterate through all the targets files - if they are dirty, sign and update + for roleName, roleObj := range r.tufRepo.Targets { + if roleObj.Dirty || (roleName == data.CanonicalTargetsRole && initialPublish) { + targetsJSON, err := serializeCanonicalRole(r.tufRepo, roleName) + if err != nil { + return err + } + updatedFiles[roleName] = targetsJSON + } + } + + // if we initialized the repo while designating the server as the snapshot + // signer, then there won't be a snapshots file. However, we might now + // have a local key (if there was a rotation), so initialize one. + if r.tufRepo.Snapshot == nil { + if err := r.tufRepo.InitSnapshot(); err != nil { + return err + } + } + + snapshotJSON, err := serializeCanonicalRole( + r.tufRepo, data.CanonicalSnapshotRole) + + if err == nil { + // Only update the snapshot if we've successfully signed it. + updatedFiles[data.CanonicalSnapshotRole] = snapshotJSON + } else if _, ok := err.(signed.ErrNoKeys); ok { + // If signing fails due to us not having the snapshot key, then + // assume the server is going to sign, and do not include any snapshot + // data. + logrus.Debugf("Client does not have the key to sign snapshot. " + + "Assuming that server should sign the snapshot.") + } else { + logrus.Debugf("Client was unable to sign the snapshot: %s", err.Error()) + return err + } + + remote, err := getRemoteStore(r.baseURL, r.gun, r.roundTrip) + if err != nil { + return err + } + + return remote.SetMultiMeta(updatedFiles) +} + +// bootstrapRepo loads the repository from the local file system. This attempts +// to load metadata for all roles. Since server snapshots are supported, +// if the snapshot metadata fails to load, that's ok. +// This can also be unified with some cache reading tools from tuf/client. +// This assumes that bootstrapRepo is only used by Publish() or RotateKey() +func (r *NotaryRepository) bootstrapRepo() error { + tufRepo := tuf.NewRepo(r.CryptoService) + + logrus.Debugf("Loading trusted collection.") + rootJSON, err := r.fileStore.GetMeta("root", -1) + if err != nil { + return err + } + root := &data.SignedRoot{} + err = json.Unmarshal(rootJSON, root) + if err != nil { + return err + } + err = tufRepo.SetRoot(root) + if err != nil { + return err + } + targetsJSON, err := r.fileStore.GetMeta("targets", -1) + if err != nil { + return err + } + targets := &data.SignedTargets{} + err = json.Unmarshal(targetsJSON, targets) + if err != nil { + return err + } + tufRepo.SetTargets("targets", targets) + + snapshotJSON, err := r.fileStore.GetMeta("snapshot", -1) + if err == nil { + snapshot := &data.SignedSnapshot{} + err = json.Unmarshal(snapshotJSON, snapshot) + if err != nil { + return err + } + tufRepo.SetSnapshot(snapshot) + } else if _, ok := err.(store.ErrMetaNotFound); !ok { + return err + } + + r.tufRepo = tufRepo + + return nil +} + +func (r *NotaryRepository) saveMetadata(ignoreSnapshot bool) error { + logrus.Debugf("Saving changes to Trusted Collection.") + + rootJSON, err := serializeCanonicalRole(r.tufRepo, data.CanonicalRootRole) + if err != nil { + return err + } + err = r.fileStore.SetMeta(data.CanonicalRootRole, rootJSON) + if err != nil { + return err + } + + targetsToSave := make(map[string][]byte) + for t := range r.tufRepo.Targets { + signedTargets, err := r.tufRepo.SignTargets(t, data.DefaultExpires("targets")) + if err != nil { + return err + } + targetsJSON, err := json.Marshal(signedTargets) + if err != nil { + return err + } + targetsToSave[t] = targetsJSON + } + + for role, blob := range targetsToSave { + parentDir := filepath.Dir(role) + os.MkdirAll(parentDir, 0755) + r.fileStore.SetMeta(role, blob) + } + + if ignoreSnapshot { + return nil + } + + snapshotJSON, err := serializeCanonicalRole(r.tufRepo, data.CanonicalSnapshotRole) + if err != nil { + return err + } + + return r.fileStore.SetMeta(data.CanonicalSnapshotRole, snapshotJSON) +} + +// returns a properly constructed ErrRepositoryNotExist error based on this +// repo's information +func (r *NotaryRepository) errRepositoryNotExist() error { + host := r.baseURL + parsed, err := url.Parse(r.baseURL) + if err == nil { + host = parsed.Host // try to exclude the scheme and any paths + } + return ErrRepositoryNotExist{remote: host, gun: r.gun} +} + +// Update bootstraps a trust anchor (root.json) before updating all the +// metadata from the repo. +func (r *NotaryRepository) Update(forWrite bool) (*tufclient.Client, error) { + c, err := r.bootstrapClient(forWrite) + if err != nil { + if _, ok := err.(store.ErrMetaNotFound); ok { + return nil, r.errRepositoryNotExist() + } + return nil, err + } + err = c.Update() + if err != nil { + // notFound.Resource may include a checksum so when the role is root, + // it will be root.json or root..json. Therefore best we can + // do it match a "root." prefix + if notFound, ok := err.(store.ErrMetaNotFound); ok && strings.HasPrefix(notFound.Resource, data.CanonicalRootRole+".") { + return nil, r.errRepositoryNotExist() + } + return nil, err + } + return c, nil +} + +// bootstrapClient attempts to bootstrap a root.json to be used as the trust +// anchor for a repository. The checkInitialized argument indicates whether +// we should always attempt to contact the server to determine if the repository +// is initialized or not. If set to true, we will always attempt to download +// and return an error if the remote repository errors. +func (r *NotaryRepository) bootstrapClient(checkInitialized bool) (*tufclient.Client, error) { + var ( + rootJSON []byte + err error + signedRoot *data.SignedRoot + ) + // try to read root from cache first. We will trust this root + // until we detect a problem during update which will cause + // us to download a new root and perform a rotation. + rootJSON, cachedRootErr := r.fileStore.GetMeta("root", -1) + + if cachedRootErr == nil { + signedRoot, cachedRootErr = r.validateRoot(rootJSON) + } + + remote, remoteErr := getRemoteStore(r.baseURL, r.gun, r.roundTrip) + if remoteErr != nil { + logrus.Error(remoteErr) + } else if cachedRootErr != nil || checkInitialized { + // remoteErr was nil and we had a cachedRootErr (or are specifically + // checking for initialization of the repo). + + // if remote store successfully set up, try and get root from remote + // We don't have any local data to determine the size of root, so try the maximum (though it is restricted at 100MB) + tmpJSON, err := remote.GetMeta("root", -1) + if err != nil { + // we didn't have a root in cache and were unable to load one from + // the server. Nothing we can do but error. + return nil, err + } + if cachedRootErr != nil { + // we always want to use the downloaded root if there was a cache + // error. + signedRoot, err = r.validateRoot(tmpJSON) + if err != nil { + return nil, err + } + + err = r.fileStore.SetMeta("root", tmpJSON) + if err != nil { + // if we can't write cache we should still continue, just log error + logrus.Errorf("could not save root to cache: %s", err.Error()) + } + } + } + + r.tufRepo = tuf.NewRepo(r.CryptoService) + + if signedRoot == nil { + return nil, ErrRepoNotInitialized{} + } + + err = r.tufRepo.SetRoot(signedRoot) + if err != nil { + return nil, err + } + + return tufclient.NewClient( + r.tufRepo, + remote, + r.fileStore, + ), nil +} + +// validateRoot MUST only be used during bootstrapping. It will only validate +// signatures of the root based on known keys, not expiry or other metadata. +// This is so that an out of date root can be loaded to be used in a rotation +// should the TUF update process detect a problem. +func (r *NotaryRepository) validateRoot(rootJSON []byte) (*data.SignedRoot, error) { + // can't just unmarshal into SignedRoot because validate root + // needs the root.Signed field to still be []byte for signature + // validation + root := &data.Signed{} + err := json.Unmarshal(rootJSON, root) + if err != nil { + return nil, err + } + + err = certs.ValidateRoot(r.CertStore, root, r.gun) + if err != nil { + return nil, err + } + + return data.RootFromSigned(root) +} + +// RotateKey removes all existing keys associated with the role, and either +// creates and adds one new key or delegates managing the key to the server. +// These changes are staged in a changelist until publish is called. +func (r *NotaryRepository) RotateKey(role string, serverManagesKey bool) error { + switch { + // We currently support locally or remotely managing snapshot keys... + case role == data.CanonicalSnapshotRole: + break + + // locally managing targets keys only + case role == data.CanonicalTargetsRole && !serverManagesKey: + break + case role == data.CanonicalTargetsRole && serverManagesKey: + return ErrInvalidRemoteRole{Role: data.CanonicalTargetsRole} + + // and remotely managing timestamp keys only + case role == data.CanonicalTimestampRole && serverManagesKey: + break + case role == data.CanonicalTimestampRole && !serverManagesKey: + return ErrInvalidLocalRole{Role: data.CanonicalTimestampRole} + + default: + return fmt.Errorf("notary does not currently permit rotating the %s key", role) + } + + var ( + pubKey data.PublicKey + err error + errFmtMsg string + ) + switch serverManagesKey { + case true: + pubKey, err = getRemoteKey(r.baseURL, r.gun, role, r.roundTrip) + errFmtMsg = "unable to rotate remote key: %s" + default: + pubKey, err = r.CryptoService.Create(role, r.gun, data.ECDSAKey) + errFmtMsg = "unable to generate key: %s" + } + + if err != nil { + return fmt.Errorf(errFmtMsg, err) + } + + cl := changelist.NewMemChangelist() + if err := r.rootFileKeyChange(cl, role, changelist.ActionCreate, pubKey); err != nil { + return err + } + return r.publish(cl) +} + +func (r *NotaryRepository) rootFileKeyChange(cl changelist.Changelist, role, action string, key data.PublicKey) error { + kl := make(data.KeyList, 0, 1) + kl = append(kl, key) + meta := changelist.TufRootData{ + RoleName: role, + Keys: kl, + } + metaJSON, err := json.Marshal(meta) + if err != nil { + return err + } + + c := changelist.NewTufChange( + action, + changelist.ScopeRoot, + changelist.TypeRootRole, + role, + metaJSON, + ) + return cl.Add(c) +} + +// DeleteTrustData removes the trust data stored for this repo in the TUF cache and certificate store on the client side +func (r *NotaryRepository) DeleteTrustData() error { + // Clear TUF files and cache + if err := r.fileStore.RemoveAll(); err != nil { + return fmt.Errorf("error clearing TUF repo data: %v", err) + } + r.tufRepo = tuf.NewRepo(nil) + // Clear certificates + certificates, err := r.CertStore.GetCertificatesByCN(r.gun) + if err != nil { + // If there were no certificates to delete, we're done + if _, ok := err.(*trustmanager.ErrNoCertificatesFound); ok { + return nil + } + return fmt.Errorf("error retrieving certificates for %s: %v", r.gun, err) + } + for _, cert := range certificates { + if err := r.CertStore.RemoveCert(cert); err != nil { + return fmt.Errorf("error removing certificate: %v: %v", cert, err) + } + } + return nil +} diff --git a/vendor/src/github.com/docker/notary/client/delegations.go b/vendor/src/github.com/docker/notary/client/delegations.go new file mode 100644 index 00000000..c28e3d84 --- /dev/null +++ b/vendor/src/github.com/docker/notary/client/delegations.go @@ -0,0 +1,294 @@ +package client + +import ( + "encoding/json" + "fmt" + "path/filepath" + + "github.com/Sirupsen/logrus" + "github.com/docker/notary" + "github.com/docker/notary/client/changelist" + "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/store" + "github.com/docker/notary/tuf/utils" +) + +// AddDelegation creates changelist entries to add provided delegation public keys and paths. +// This method composes AddDelegationRoleAndKeys and AddDelegationPaths (each creates one changelist if called). +func (r *NotaryRepository) AddDelegation(name string, delegationKeys []data.PublicKey, paths []string) error { + if len(delegationKeys) > 0 { + err := r.AddDelegationRoleAndKeys(name, delegationKeys) + if err != nil { + return err + } + } + if len(paths) > 0 { + err := r.AddDelegationPaths(name, paths) + if err != nil { + return err + } + } + return nil +} + +// AddDelegationRoleAndKeys creates a changelist entry to add provided delegation public keys. +// This method is the simplest way to create a new delegation, because the delegation must have at least +// one key upon creation to be valid since we will reject the changelist while validating the threshold. +func (r *NotaryRepository) AddDelegationRoleAndKeys(name string, delegationKeys []data.PublicKey) error { + + if !data.IsDelegation(name) { + return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"} + } + + cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist")) + if err != nil { + return err + } + defer cl.Close() + + logrus.Debugf(`Adding delegation "%s" with threshold %d, and %d keys\n`, + name, notary.MinThreshold, len(delegationKeys)) + + // Defaulting to threshold of 1, since we don't allow for larger thresholds at the moment. + tdJSON, err := json.Marshal(&changelist.TufDelegation{ + NewThreshold: notary.MinThreshold, + AddKeys: data.KeyList(delegationKeys), + }) + if err != nil { + return err + } + + template := newCreateDelegationChange(name, tdJSON) + return addChange(cl, template, name) +} + +// AddDelegationPaths creates a changelist entry to add provided paths to an existing delegation. +// This method cannot create a new delegation itself because the role must meet the key threshold upon creation. +func (r *NotaryRepository) AddDelegationPaths(name string, paths []string) error { + + if !data.IsDelegation(name) { + return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"} + } + + cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist")) + if err != nil { + return err + } + defer cl.Close() + + logrus.Debugf(`Adding %s paths to delegation %s\n`, paths, name) + + tdJSON, err := json.Marshal(&changelist.TufDelegation{ + AddPaths: paths, + }) + if err != nil { + return err + } + + template := newCreateDelegationChange(name, tdJSON) + return addChange(cl, template, name) +} + +// RemoveDelegationKeysAndPaths creates changelist entries to remove provided delegation key IDs and paths. +// This method composes RemoveDelegationPaths and RemoveDelegationKeys (each creates one changelist if called). +func (r *NotaryRepository) RemoveDelegationKeysAndPaths(name string, keyIDs, paths []string) error { + if len(paths) > 0 { + err := r.RemoveDelegationPaths(name, paths) + if err != nil { + return err + } + } + if len(keyIDs) > 0 { + err := r.RemoveDelegationKeys(name, keyIDs) + if err != nil { + return err + } + } + return nil +} + +// RemoveDelegationRole creates a changelist to remove all paths and keys from a role, and delete the role in its entirety. +func (r *NotaryRepository) RemoveDelegationRole(name string) error { + + if !data.IsDelegation(name) { + return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"} + } + + cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist")) + if err != nil { + return err + } + defer cl.Close() + + logrus.Debugf(`Removing delegation "%s"\n`, name) + + template := newDeleteDelegationChange(name, nil) + return addChange(cl, template, name) +} + +// RemoveDelegationPaths creates a changelist entry to remove provided paths from an existing delegation. +func (r *NotaryRepository) RemoveDelegationPaths(name string, paths []string) error { + + if !data.IsDelegation(name) { + return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"} + } + + cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist")) + if err != nil { + return err + } + defer cl.Close() + + logrus.Debugf(`Removing %s paths from delegation "%s"\n`, paths, name) + + tdJSON, err := json.Marshal(&changelist.TufDelegation{ + RemovePaths: paths, + }) + if err != nil { + return err + } + + template := newUpdateDelegationChange(name, tdJSON) + return addChange(cl, template, name) +} + +// RemoveDelegationKeys creates a changelist entry to remove provided keys from an existing delegation. +// When this changelist is applied, if the specified keys are the only keys left in the role, +// the role itself will be deleted in its entirety. +func (r *NotaryRepository) RemoveDelegationKeys(name string, keyIDs []string) error { + + if !data.IsDelegation(name) { + return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"} + } + + cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist")) + if err != nil { + return err + } + defer cl.Close() + + logrus.Debugf(`Removing %s keys from delegation "%s"\n`, keyIDs, name) + + tdJSON, err := json.Marshal(&changelist.TufDelegation{ + RemoveKeys: keyIDs, + }) + if err != nil { + return err + } + + template := newUpdateDelegationChange(name, tdJSON) + return addChange(cl, template, name) +} + +// ClearDelegationPaths creates a changelist entry to remove all paths from an existing delegation. +func (r *NotaryRepository) ClearDelegationPaths(name string) error { + + if !data.IsDelegation(name) { + return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"} + } + + cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist")) + if err != nil { + return err + } + defer cl.Close() + + logrus.Debugf(`Removing all paths from delegation "%s"\n`, name) + + tdJSON, err := json.Marshal(&changelist.TufDelegation{ + ClearAllPaths: true, + }) + if err != nil { + return err + } + + template := newUpdateDelegationChange(name, tdJSON) + return addChange(cl, template, name) +} + +func newUpdateDelegationChange(name string, content []byte) *changelist.TufChange { + return changelist.NewTufChange( + changelist.ActionUpdate, + name, + changelist.TypeTargetsDelegation, + "", // no path for delegations + content, + ) +} + +func newCreateDelegationChange(name string, content []byte) *changelist.TufChange { + return changelist.NewTufChange( + changelist.ActionCreate, + name, + changelist.TypeTargetsDelegation, + "", // no path for delegations + content, + ) +} + +func newDeleteDelegationChange(name string, content []byte) *changelist.TufChange { + return changelist.NewTufChange( + changelist.ActionDelete, + name, + changelist.TypeTargetsDelegation, + "", // no path for delegations + content, + ) +} + +// GetDelegationRoles returns the keys and roles of the repository's delegations +// Also converts key IDs to canonical key IDs to keep consistent with signing prompts +func (r *NotaryRepository) GetDelegationRoles() ([]*data.Role, error) { + // Update state of the repo to latest + if _, err := r.Update(false); err != nil { + return nil, err + } + + // All top level delegations (ex: targets/level1) are stored exclusively in targets.json + _, ok := r.tufRepo.Targets[data.CanonicalTargetsRole] + if !ok { + return nil, store.ErrMetaNotFound{Resource: data.CanonicalTargetsRole} + } + + // make a copy for traversing nested delegations + allDelegations := []*data.Role{} + + // Define a visitor function to populate the delegations list and translate their key IDs to canonical IDs + delegationCanonicalListVisitor := func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} { + // For the return list, update with a copy that includes canonicalKeyIDs + // These aren't validated by the validRole + canonicalDelegations, err := translateDelegationsToCanonicalIDs(tgt.Signed.Delegations) + if err != nil { + return err + } + allDelegations = append(allDelegations, canonicalDelegations...) + return nil + } + err := r.tufRepo.WalkTargets("", "", delegationCanonicalListVisitor) + if err != nil { + return nil, err + } + return allDelegations, nil +} + +func translateDelegationsToCanonicalIDs(delegationInfo data.Delegations) ([]*data.Role, error) { + canonicalDelegations := make([]*data.Role, len(delegationInfo.Roles)) + copy(canonicalDelegations, delegationInfo.Roles) + delegationKeys := delegationInfo.Keys + for i, delegation := range canonicalDelegations { + canonicalKeyIDs := []string{} + for _, keyID := range delegation.KeyIDs { + pubKey, ok := delegationKeys[keyID] + if !ok { + return nil, fmt.Errorf("Could not translate canonical key IDs for %s", delegation.Name) + } + canonicalKeyID, err := utils.CanonicalKeyID(pubKey) + if err != nil { + return nil, fmt.Errorf("Could not translate canonical key IDs for %s: %v", delegation.Name, err) + } + canonicalKeyIDs = append(canonicalKeyIDs, canonicalKeyID) + } + canonicalDelegations[i].KeyIDs = canonicalKeyIDs + } + return canonicalDelegations, nil +} diff --git a/vendor/src/github.com/docker/notary/client/helpers.go b/vendor/src/github.com/docker/notary/client/helpers.go new file mode 100644 index 00000000..38f1b43c --- /dev/null +++ b/vendor/src/github.com/docker/notary/client/helpers.go @@ -0,0 +1,237 @@ +package client + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/notary/client/changelist" + tuf "github.com/docker/notary/tuf" + "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/store" + "github.com/docker/notary/tuf/utils" +) + +// Use this to initialize remote HTTPStores from the config settings +func getRemoteStore(baseURL, gun string, rt http.RoundTripper) (store.RemoteStore, error) { + s, err := store.NewHTTPStore( + baseURL+"/v2/"+gun+"/_trust/tuf/", + "", + "json", + "key", + rt, + ) + if err != nil { + return store.OfflineStore{}, err + } + return s, err +} + +func applyChangelist(repo *tuf.Repo, cl changelist.Changelist) error { + it, err := cl.NewIterator() + if err != nil { + return err + } + index := 0 + for it.HasNext() { + c, err := it.Next() + if err != nil { + return err + } + isDel := data.IsDelegation(c.Scope()) + switch { + case c.Scope() == changelist.ScopeTargets || isDel: + err = applyTargetsChange(repo, c) + case c.Scope() == changelist.ScopeRoot: + err = applyRootChange(repo, c) + default: + logrus.Debug("scope not supported: ", c.Scope()) + } + index++ + if err != nil { + return err + } + } + logrus.Debugf("applied %d change(s)", index) + return nil +} + +func applyTargetsChange(repo *tuf.Repo, c changelist.Change) error { + switch c.Type() { + case changelist.TypeTargetsTarget: + return changeTargetMeta(repo, c) + case changelist.TypeTargetsDelegation: + return changeTargetsDelegation(repo, c) + default: + return fmt.Errorf("only target meta and delegations changes supported") + } +} + +func changeTargetsDelegation(repo *tuf.Repo, c changelist.Change) error { + switch c.Action() { + case changelist.ActionCreate: + td := changelist.TufDelegation{} + err := json.Unmarshal(c.Content(), &td) + if err != nil { + return err + } + + // Try to create brand new role or update one + // First add the keys, then the paths. We can only add keys and paths in this scenario + err = repo.UpdateDelegationKeys(c.Scope(), td.AddKeys, []string{}, td.NewThreshold) + if err != nil { + return err + } + return repo.UpdateDelegationPaths(c.Scope(), td.AddPaths, []string{}, false) + case changelist.ActionUpdate: + td := changelist.TufDelegation{} + err := json.Unmarshal(c.Content(), &td) + if err != nil { + return err + } + delgRole, err := repo.GetDelegationRole(c.Scope()) + if err != nil { + return err + } + + // We need to translate the keys from canonical ID to TUF ID for compatibility + canonicalToTUFID := make(map[string]string) + for tufID, pubKey := range delgRole.Keys { + canonicalID, err := utils.CanonicalKeyID(pubKey) + if err != nil { + return err + } + canonicalToTUFID[canonicalID] = tufID + } + + removeTUFKeyIDs := []string{} + for _, canonID := range td.RemoveKeys { + removeTUFKeyIDs = append(removeTUFKeyIDs, canonicalToTUFID[canonID]) + } + + // If we specify the only keys left delete the role, else just delete specified keys + if strings.Join(delgRole.ListKeyIDs(), ";") == strings.Join(removeTUFKeyIDs, ";") && len(td.AddKeys) == 0 { + return repo.DeleteDelegation(c.Scope()) + } + err = repo.UpdateDelegationKeys(c.Scope(), td.AddKeys, removeTUFKeyIDs, td.NewThreshold) + if err != nil { + return err + } + return repo.UpdateDelegationPaths(c.Scope(), td.AddPaths, td.RemovePaths, td.ClearAllPaths) + case changelist.ActionDelete: + return repo.DeleteDelegation(c.Scope()) + default: + return fmt.Errorf("unsupported action against delegations: %s", c.Action()) + } + +} + +func changeTargetMeta(repo *tuf.Repo, c changelist.Change) error { + var err error + switch c.Action() { + case changelist.ActionCreate: + logrus.Debug("changelist add: ", c.Path()) + meta := &data.FileMeta{} + err = json.Unmarshal(c.Content(), meta) + if err != nil { + return err + } + files := data.Files{c.Path(): *meta} + + // Attempt to add the target to this role + if _, err = repo.AddTargets(c.Scope(), files); err != nil { + logrus.Errorf("couldn't add target to %s: %s", c.Scope(), err.Error()) + } + + case changelist.ActionDelete: + logrus.Debug("changelist remove: ", c.Path()) + + // Attempt to remove the target from this role + if err = repo.RemoveTargets(c.Scope(), c.Path()); err != nil { + logrus.Errorf("couldn't remove target from %s: %s", c.Scope(), err.Error()) + } + + default: + logrus.Debug("action not yet supported: ", c.Action()) + } + return err +} + +func applyRootChange(repo *tuf.Repo, c changelist.Change) error { + var err error + switch c.Type() { + case changelist.TypeRootRole: + err = applyRootRoleChange(repo, c) + default: + logrus.Debug("type of root change not yet supported: ", c.Type()) + } + return err // might be nil +} + +func applyRootRoleChange(repo *tuf.Repo, c changelist.Change) error { + switch c.Action() { + case changelist.ActionCreate: + // replaces all keys for a role + d := &changelist.TufRootData{} + err := json.Unmarshal(c.Content(), d) + if err != nil { + return err + } + err = repo.ReplaceBaseKeys(d.RoleName, d.Keys...) + if err != nil { + return err + } + default: + logrus.Debug("action not yet supported for root: ", c.Action()) + } + return nil +} + +func nearExpiry(r *data.SignedRoot) bool { + plus6mo := time.Now().AddDate(0, 6, 0) + return r.Signed.Expires.Before(plus6mo) +} + +// Fetches a public key from a remote store, given a gun and role +func getRemoteKey(url, gun, role string, rt http.RoundTripper) (data.PublicKey, error) { + remote, err := getRemoteStore(url, gun, rt) + if err != nil { + return nil, err + } + rawPubKey, err := remote.GetKey(role) + if err != nil { + return nil, err + } + + pubKey, err := data.UnmarshalPublicKey(rawPubKey) + if err != nil { + return nil, err + } + + return pubKey, nil +} + +// signs and serializes the metadata for a canonical role in a tuf repo to JSON +func serializeCanonicalRole(tufRepo *tuf.Repo, role string) (out []byte, err error) { + var s *data.Signed + switch { + case role == data.CanonicalRootRole: + s, err = tufRepo.SignRoot(data.DefaultExpires(role)) + case role == data.CanonicalSnapshotRole: + s, err = tufRepo.SignSnapshot(data.DefaultExpires(role)) + case tufRepo.Targets[role] != nil: + s, err = tufRepo.SignTargets( + role, data.DefaultExpires(data.CanonicalTargetsRole)) + default: + err = fmt.Errorf("%s not supported role to sign on the client", role) + } + + if err != nil { + return + } + + return json.Marshal(s) +} diff --git a/vendor/src/github.com/docker/notary/client/repo.go b/vendor/src/github.com/docker/notary/client/repo.go new file mode 100644 index 00000000..d9931891 --- /dev/null +++ b/vendor/src/github.com/docker/notary/client/repo.go @@ -0,0 +1,27 @@ +// +build !pkcs11 + +package client + +import ( + "fmt" + "net/http" + + "github.com/docker/notary/passphrase" + "github.com/docker/notary/trustmanager" +) + +// NewNotaryRepository is a helper method that returns a new notary repository. +// It takes the base directory under where all the trust files will be stored +// (usually ~/.docker/trust/). +func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper, + retriever passphrase.Retriever) ( + *NotaryRepository, error) { + + fileKeyStore, err := trustmanager.NewKeyFileStore(baseDir, retriever) + if err != nil { + return nil, fmt.Errorf("failed to create private key store in directory: %s", baseDir) + } + + return repositoryFromKeystores(baseDir, gun, baseURL, rt, + []trustmanager.KeyStore{fileKeyStore}) +} diff --git a/vendor/src/github.com/docker/notary/client/repo_pkcs11.go b/vendor/src/github.com/docker/notary/client/repo_pkcs11.go new file mode 100644 index 00000000..dd697ff4 --- /dev/null +++ b/vendor/src/github.com/docker/notary/client/repo_pkcs11.go @@ -0,0 +1,33 @@ +// +build pkcs11 + +package client + +import ( + "fmt" + "net/http" + + "github.com/docker/notary/passphrase" + "github.com/docker/notary/trustmanager" + "github.com/docker/notary/trustmanager/yubikey" +) + +// NewNotaryRepository is a helper method that returns a new notary repository. +// It takes the base directory under where all the trust files will be stored +// (usually ~/.docker/trust/). +func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper, + retriever passphrase.Retriever) ( + *NotaryRepository, error) { + + fileKeyStore, err := trustmanager.NewKeyFileStore(baseDir, retriever) + if err != nil { + return nil, fmt.Errorf("failed to create private key store in directory: %s", baseDir) + } + + keyStores := []trustmanager.KeyStore{fileKeyStore} + yubiKeyStore, _ := yubikey.NewYubiKeyStore(fileKeyStore, retriever) + if yubiKeyStore != nil { + keyStores = []trustmanager.KeyStore{yubiKeyStore, fileKeyStore} + } + + return repositoryFromKeystores(baseDir, gun, baseURL, rt, keyStores) +} diff --git a/vendor/src/github.com/docker/notary/const.go b/vendor/src/github.com/docker/notary/const.go new file mode 100644 index 00000000..22296f71 --- /dev/null +++ b/vendor/src/github.com/docker/notary/const.go @@ -0,0 +1,61 @@ +package notary + +import ( + "time" +) + +// application wide constants +const ( + // MaxDownloadSize is the maximum size we'll download for metadata if no limit is given + MaxDownloadSize int64 = 100 << 20 + // MaxTimestampSize is the maximum size of timestamp metadata - 1MiB. + MaxTimestampSize int64 = 1 << 20 + // MinRSABitSize is the minimum bit size for RSA keys allowed in notary + MinRSABitSize = 2048 + // MinThreshold requires a minimum of one threshold for roles; currently we do not support a higher threshold + MinThreshold = 1 + // PrivKeyPerms are the file permissions to use when writing private keys to disk + PrivKeyPerms = 0700 + // PubCertPerms are the file permissions to use when writing public certificates to disk + PubCertPerms = 0755 + // Sha256HexSize is how big a Sha256 hex is in number of characters + Sha256HexSize = 64 + // SHA256 is the name of SHA256 hash algorithm + SHA256 = "sha256" + // SHA512 is the name of SHA512 hash algorithm + SHA512 = "sha512" + // TrustedCertsDir is the directory, under the notary repo base directory, where trusted certs are stored + TrustedCertsDir = "trusted_certificates" + // PrivDir is the directory, under the notary repo base directory, where private keys are stored + PrivDir = "private" + // RootKeysSubdir is the subdirectory under PrivDir where root private keys are stored + RootKeysSubdir = "root_keys" + // NonRootKeysSubdir is the subdirectory under PrivDir where non-root private keys are stored + NonRootKeysSubdir = "tuf_keys" + + // Day is a duration of one day + Day = 24 * time.Hour + Year = 365 * Day + + // NotaryRootExpiry is the duration representing the expiry time of the Root role + NotaryRootExpiry = 10 * Year + NotaryTargetsExpiry = 3 * Year + NotarySnapshotExpiry = 3 * Year + NotaryTimestampExpiry = 14 * Day + + ConsistentMetadataCacheMaxAge = 30 * Day + CurrentMetadataCacheMaxAge = 5 * time.Minute + // CacheMaxAgeLimit is the generally recommended maximum age for Cache-Control headers + // (one year, in seconds, since one year is forever in terms of internet + // content) + CacheMaxAgeLimit = 1 * Year +) + +// NotaryDefaultExpiries is the construct used to configure the default expiry times of +// the various role files. +var NotaryDefaultExpiries = map[string]time.Duration{ + "root": NotaryRootExpiry, + "targets": NotaryTargetsExpiry, + "snapshot": NotarySnapshotExpiry, + "timestamp": NotaryTimestampExpiry, +} diff --git a/vendor/src/github.com/docker/notary/coverpkg.sh b/vendor/src/github.com/docker/notary/coverpkg.sh new file mode 100755 index 00000000..00ec5d11 --- /dev/null +++ b/vendor/src/github.com/docker/notary/coverpkg.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +# Given a subpackage and the containing package, figures out which packages +# need to be passed to `go test -coverpkg`: this includes all of the +# subpackage's dependencies within the containing package, as well as the +# subpackage itself. + +DEPENDENCIES="$(go list -f $'{{range $f := .Deps}}{{$f}}\n{{end}}' ${1} | grep ${2} | grep -v ${2}/vendor)" + +echo "${1} ${DEPENDENCIES}" | xargs echo -n | tr ' ' ',' diff --git a/vendor/src/github.com/docker/notary/cryptoservice/certificate.go b/vendor/src/github.com/docker/notary/cryptoservice/certificate.go new file mode 100644 index 00000000..25183aea --- /dev/null +++ b/vendor/src/github.com/docker/notary/cryptoservice/certificate.go @@ -0,0 +1,48 @@ +package cryptoservice + +import ( + "crypto" + "crypto/rand" + "crypto/x509" + "fmt" + "time" + + "github.com/docker/notary/trustmanager" + "github.com/docker/notary/tuf/data" +) + +// GenerateCertificate generates an X509 Certificate from a template, given a GUN and validity interval +func GenerateCertificate(rootKey data.PrivateKey, gun string, startTime, endTime time.Time) (*x509.Certificate, error) { + signer := rootKey.CryptoSigner() + if signer == nil { + return nil, fmt.Errorf("key type not supported for Certificate generation: %s\n", rootKey.Algorithm()) + } + + return generateCertificate(signer, gun, startTime, endTime) +} + +// GenerateTestingCertificate generates a non-expired X509 Certificate from a template, given a GUN. +// Good enough for tests where expiration does not really matter; do not use if you care about the policy. +func GenerateTestingCertificate(signer crypto.Signer, gun string) (*x509.Certificate, error) { + startTime := time.Now() + return generateCertificate(signer, gun, startTime, startTime.AddDate(10, 0, 0)) +} + +func generateCertificate(signer crypto.Signer, gun string, startTime, endTime time.Time) (*x509.Certificate, error) { + template, err := trustmanager.NewCertificate(gun, startTime, endTime) + if err != nil { + return nil, fmt.Errorf("failed to create the certificate template for: %s (%v)", gun, err) + } + + derBytes, err := x509.CreateCertificate(rand.Reader, template, template, signer.Public(), signer) + if err != nil { + return nil, fmt.Errorf("failed to create the certificate for: %s (%v)", gun, err) + } + + cert, err := x509.ParseCertificate(derBytes) + if err != nil { + return nil, fmt.Errorf("failed to parse the certificate for key: %s (%v)", gun, err) + } + + return cert, nil +} diff --git a/vendor/src/github.com/docker/notary/cryptoservice/crypto_service.go b/vendor/src/github.com/docker/notary/cryptoservice/crypto_service.go new file mode 100644 index 00000000..7c32ba72 --- /dev/null +++ b/vendor/src/github.com/docker/notary/cryptoservice/crypto_service.go @@ -0,0 +1,155 @@ +package cryptoservice + +import ( + "crypto/rand" + "fmt" + + "github.com/Sirupsen/logrus" + "github.com/docker/notary/trustmanager" + "github.com/docker/notary/tuf/data" +) + +const ( + rsaKeySize = 2048 // Used for snapshots and targets keys +) + +// CryptoService implements Sign and Create, holding a specific GUN and keystore to +// operate on +type CryptoService struct { + keyStores []trustmanager.KeyStore +} + +// NewCryptoService returns an instance of CryptoService +func NewCryptoService(keyStores ...trustmanager.KeyStore) *CryptoService { + return &CryptoService{keyStores: keyStores} +} + +// Create is used to generate keys for targets, snapshots and timestamps +func (cs *CryptoService) Create(role, gun, algorithm string) (data.PublicKey, error) { + var privKey data.PrivateKey + var err error + + switch algorithm { + case data.RSAKey: + privKey, err = trustmanager.GenerateRSAKey(rand.Reader, rsaKeySize) + if err != nil { + return nil, fmt.Errorf("failed to generate RSA key: %v", err) + } + case data.ECDSAKey: + privKey, err = trustmanager.GenerateECDSAKey(rand.Reader) + if err != nil { + return nil, fmt.Errorf("failed to generate EC key: %v", err) + } + case data.ED25519Key: + privKey, err = trustmanager.GenerateED25519Key(rand.Reader) + if err != nil { + return nil, fmt.Errorf("failed to generate ED25519 key: %v", err) + } + default: + return nil, fmt.Errorf("private key type not supported for key generation: %s", algorithm) + } + logrus.Debugf("generated new %s key for role: %s and keyID: %s", algorithm, role, privKey.ID()) + + // Store the private key into our keystore + for _, ks := range cs.keyStores { + err = ks.AddKey(trustmanager.KeyInfo{Role: role, Gun: gun}, privKey) + if err == nil { + return data.PublicKeyFromPrivate(privKey), nil + } + } + if err != nil { + return nil, fmt.Errorf("failed to add key to filestore: %v", err) + } + + return nil, fmt.Errorf("keystores would not accept new private keys for unknown reasons") +} + +// GetPrivateKey returns a private key and role if present by ID. +func (cs *CryptoService) GetPrivateKey(keyID string) (k data.PrivateKey, role string, err error) { + for _, ks := range cs.keyStores { + if k, role, err = ks.GetKey(keyID); err == nil { + return + } + switch err.(type) { + case trustmanager.ErrPasswordInvalid, trustmanager.ErrAttemptsExceeded: + return + default: + continue + } + } + return // returns whatever the final values were +} + +// GetKey returns a key by ID +func (cs *CryptoService) GetKey(keyID string) data.PublicKey { + privKey, _, err := cs.GetPrivateKey(keyID) + if err != nil { + return nil + } + return data.PublicKeyFromPrivate(privKey) +} + +// GetKeyInfo returns role and GUN info of a key by ID +func (cs *CryptoService) GetKeyInfo(keyID string) (trustmanager.KeyInfo, error) { + for _, store := range cs.keyStores { + if info, err := store.GetKeyInfo(keyID); err == nil { + return info, nil + } + } + return trustmanager.KeyInfo{}, fmt.Errorf("Could not find info for keyID %s", keyID) +} + +// RemoveKey deletes a key by ID +func (cs *CryptoService) RemoveKey(keyID string) (err error) { + for _, ks := range cs.keyStores { + ks.RemoveKey(keyID) + } + return // returns whatever the final values were +} + +// AddKey adds a private key to a specified role. +// The GUN is inferred from the cryptoservice itself for non-root roles +func (cs *CryptoService) AddKey(role, gun string, key data.PrivateKey) (err error) { + // First check if this key already exists in any of our keystores + for _, ks := range cs.keyStores { + if keyInfo, err := ks.GetKeyInfo(key.ID()); err == nil { + if keyInfo.Role != role { + return fmt.Errorf("key with same ID already exists for role: %s", keyInfo.Role) + } + logrus.Debugf("key with same ID %s and role %s already exists", key.ID(), keyInfo.Role) + return nil + } + } + // If the key didn't exist in any of our keystores, add and return on the first successful keystore + for _, ks := range cs.keyStores { + // Try to add to this keystore, return if successful + if err = ks.AddKey(trustmanager.KeyInfo{Role: role, Gun: gun}, key); err == nil { + return nil + } + } + return // returns whatever the final values were +} + +// ListKeys returns a list of key IDs valid for the given role +func (cs *CryptoService) ListKeys(role string) []string { + var res []string + for _, ks := range cs.keyStores { + for k, r := range ks.ListKeys() { + if r.Role == role { + res = append(res, k) + } + } + } + return res +} + +// ListAllKeys returns a map of key IDs to role +func (cs *CryptoService) ListAllKeys() map[string]string { + res := make(map[string]string) + for _, ks := range cs.keyStores { + for k, r := range ks.ListKeys() { + res[k] = r.Role // keys are content addressed so don't care about overwrites + } + } + return res +} diff --git a/vendor/src/github.com/docker/notary/cryptoservice/import_export.go b/vendor/src/github.com/docker/notary/cryptoservice/import_export.go new file mode 100644 index 00000000..99445cda --- /dev/null +++ b/vendor/src/github.com/docker/notary/cryptoservice/import_export.go @@ -0,0 +1,313 @@ +package cryptoservice + +import ( + "archive/zip" + "crypto/x509" + "encoding/pem" + "errors" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/docker/notary/passphrase" + "github.com/docker/notary/trustmanager" +) + +const zipMadeByUNIX = 3 << 8 + +var ( + // ErrNoValidPrivateKey is returned if a key being imported doesn't + // look like a private key + ErrNoValidPrivateKey = errors.New("no valid private key found") + + // ErrRootKeyNotEncrypted is returned if a root key being imported is + // unencrypted + ErrRootKeyNotEncrypted = errors.New("only encrypted root keys may be imported") + + // ErrNoKeysFoundForGUN is returned if no keys are found for the + // specified GUN during export + ErrNoKeysFoundForGUN = errors.New("no keys found for specified GUN") +) + +// ExportKey exports the specified private key to an io.Writer in PEM format. +// The key's existing encryption is preserved. +func (cs *CryptoService) ExportKey(dest io.Writer, keyID, role string) error { + var ( + pemBytes []byte + err error + ) + + for _, ks := range cs.keyStores { + pemBytes, err = ks.ExportKey(keyID) + if err != nil { + continue + } + } + if err != nil { + return err + } + + nBytes, err := dest.Write(pemBytes) + if err != nil { + return err + } + if nBytes != len(pemBytes) { + return errors.New("Unable to finish writing exported key.") + } + return nil +} + +// ExportKeyReencrypt exports the specified private key to an io.Writer in +// PEM format. The key is reencrypted with a new passphrase. +func (cs *CryptoService) ExportKeyReencrypt(dest io.Writer, keyID string, newPassphraseRetriever passphrase.Retriever) error { + privateKey, _, err := cs.GetPrivateKey(keyID) + if err != nil { + return err + } + + keyInfo, err := cs.GetKeyInfo(keyID) + if err != nil { + return err + } + + // Create temporary keystore to use as a staging area + tempBaseDir, err := ioutil.TempDir("", "notary-key-export-") + defer os.RemoveAll(tempBaseDir) + + tempKeyStore, err := trustmanager.NewKeyFileStore(tempBaseDir, newPassphraseRetriever) + if err != nil { + return err + } + + err = tempKeyStore.AddKey(keyInfo, privateKey) + if err != nil { + return err + } + + pemBytes, err := tempKeyStore.ExportKey(keyID) + if err != nil { + return err + } + nBytes, err := dest.Write(pemBytes) + if err != nil { + return err + } + if nBytes != len(pemBytes) { + return errors.New("Unable to finish writing exported key.") + } + return nil +} + +// ExportAllKeys exports all keys to an io.Writer in zip format. +// newPassphraseRetriever will be used to obtain passphrases to use to encrypt the existing keys. +func (cs *CryptoService) ExportAllKeys(dest io.Writer, newPassphraseRetriever passphrase.Retriever) error { + tempBaseDir, err := ioutil.TempDir("", "notary-key-export-") + defer os.RemoveAll(tempBaseDir) + + // Create temporary keystore to use as a staging area + tempKeyStore, err := trustmanager.NewKeyFileStore(tempBaseDir, newPassphraseRetriever) + if err != nil { + return err + } + + for _, ks := range cs.keyStores { + if err := moveKeys(ks, tempKeyStore); err != nil { + return err + } + } + + zipWriter := zip.NewWriter(dest) + + if err := addKeysToArchive(zipWriter, tempKeyStore); err != nil { + return err + } + + zipWriter.Close() + + return nil +} + +// ImportKeysZip imports keys from a zip file provided as an zip.Reader. The +// keys in the root_keys directory are left encrypted, but the other keys are +// decrypted with the specified passphrase. +func (cs *CryptoService) ImportKeysZip(zipReader zip.Reader, retriever passphrase.Retriever) error { + // Temporarily store the keys in maps, so we can bail early if there's + // an error (for example, wrong passphrase), without leaving the key + // store in an inconsistent state + newKeys := make(map[string][]byte) + + // Iterate through the files in the archive. Don't add the keys + for _, f := range zipReader.File { + fNameTrimmed := strings.TrimSuffix(f.Name, filepath.Ext(f.Name)) + rc, err := f.Open() + if err != nil { + return err + } + defer rc.Close() + + fileBytes, err := ioutil.ReadAll(rc) + if err != nil { + return nil + } + + // Note that using / as a separator is okay here - the zip + // package guarantees that the separator will be / + if fNameTrimmed[len(fNameTrimmed)-5:] == "_root" { + if err = CheckRootKeyIsEncrypted(fileBytes); err != nil { + return err + } + } + newKeys[fNameTrimmed] = fileBytes + } + + for keyName, pemBytes := range newKeys { + // Get the key role information as well as its data.PrivateKey representation + _, keyInfo, err := trustmanager.KeyInfoFromPEM(pemBytes, keyName) + if err != nil { + return err + } + privKey, err := trustmanager.ParsePEMPrivateKey(pemBytes, "") + if err != nil { + privKey, _, err = trustmanager.GetPasswdDecryptBytes(retriever, pemBytes, "", "imported "+keyInfo.Role) + if err != nil { + return err + } + } + // Add the key to our cryptoservice, will add to the first successful keystore + if err = cs.AddKey(keyInfo.Role, keyInfo.Gun, privKey); err != nil { + return err + } + } + + return nil +} + +// ExportKeysByGUN exports all keys associated with a specified GUN to an +// io.Writer in zip format. passphraseRetriever is used to select new passphrases to use to +// encrypt the keys. +func (cs *CryptoService) ExportKeysByGUN(dest io.Writer, gun string, passphraseRetriever passphrase.Retriever) error { + tempBaseDir, err := ioutil.TempDir("", "notary-key-export-") + defer os.RemoveAll(tempBaseDir) + + // Create temporary keystore to use as a staging area + tempKeyStore, err := trustmanager.NewKeyFileStore(tempBaseDir, passphraseRetriever) + if err != nil { + return err + } + + for _, ks := range cs.keyStores { + if err := moveKeysByGUN(ks, tempKeyStore, gun); err != nil { + return err + } + } + + zipWriter := zip.NewWriter(dest) + + if len(tempKeyStore.ListKeys()) == 0 { + return ErrNoKeysFoundForGUN + } + + if err := addKeysToArchive(zipWriter, tempKeyStore); err != nil { + return err + } + + zipWriter.Close() + + return nil +} + +func moveKeysByGUN(oldKeyStore, newKeyStore trustmanager.KeyStore, gun string) error { + for keyID, keyInfo := range oldKeyStore.ListKeys() { + // Skip keys that aren't associated with this GUN + if keyInfo.Gun != gun { + continue + } + + privKey, _, err := oldKeyStore.GetKey(keyID) + if err != nil { + return err + } + + err = newKeyStore.AddKey(keyInfo, privKey) + if err != nil { + return err + } + } + + return nil +} + +func moveKeys(oldKeyStore, newKeyStore trustmanager.KeyStore) error { + for keyID, keyInfo := range oldKeyStore.ListKeys() { + privateKey, _, err := oldKeyStore.GetKey(keyID) + if err != nil { + return err + } + + err = newKeyStore.AddKey(keyInfo, privateKey) + + if err != nil { + return err + } + } + + return nil +} + +func addKeysToArchive(zipWriter *zip.Writer, newKeyStore *trustmanager.KeyFileStore) error { + for _, relKeyPath := range newKeyStore.ListFiles() { + fullKeyPath, err := newKeyStore.GetPath(relKeyPath) + if err != nil { + return err + } + + fi, err := os.Lstat(fullKeyPath) + if err != nil { + return err + } + + infoHeader, err := zip.FileInfoHeader(fi) + if err != nil { + return err + } + + relPath, err := filepath.Rel(newKeyStore.BaseDir(), fullKeyPath) + if err != nil { + return err + } + infoHeader.Name = relPath + + zipFileEntryWriter, err := zipWriter.CreateHeader(infoHeader) + if err != nil { + return err + } + + fileContents, err := ioutil.ReadFile(fullKeyPath) + if err != nil { + return err + } + + if _, err = zipFileEntryWriter.Write(fileContents); err != nil { + return err + } + } + + return nil +} + +// CheckRootKeyIsEncrypted makes sure the root key is encrypted. We have +// internal assumptions that depend on this. +func CheckRootKeyIsEncrypted(pemBytes []byte) error { + block, _ := pem.Decode(pemBytes) + if block == nil { + return ErrNoValidPrivateKey + } + + if !x509.IsEncryptedPEMBlock(block) { + return ErrRootKeyNotEncrypted + } + + return nil +} diff --git a/vendor/src/github.com/docker/notary/docker-compose.yml b/vendor/src/github.com/docker/notary/docker-compose.yml new file mode 100644 index 00000000..4f8705f3 --- /dev/null +++ b/vendor/src/github.com/docker/notary/docker-compose.yml @@ -0,0 +1,34 @@ +server: + build: . + dockerfile: server.Dockerfile + links: + - mysql + - signer + - signer:notarysigner + environment: + - SERVICE_NAME=notary_server + ports: + - "8080" + - "4443:4443" + entrypoint: /bin/bash + command: -c "./migrations/migrate.sh && notary-server -config=fixtures/server-config.json" +signer: + build: . + dockerfile: signer.Dockerfile + links: + - mysql + environment: + - SERVICE_NAME=notary_signer + entrypoint: /bin/bash + command: -c "./migrations/migrate.sh && notary-signer -config=fixtures/signer-config.json" +mysql: + volumes: + - ./notarymysql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d + - notary_data:/var/lib/mysql + image: mariadb:10.1.10 + ports: + - "3306:3306" + environment: + - TERM=dumb + - MYSQL_ALLOW_EMPTY_PASSWORD="true" + command: mysqld --innodb_file_per_table diff --git a/vendor/src/github.com/docker/notary/passphrase/passphrase.go b/vendor/src/github.com/docker/notary/passphrase/passphrase.go new file mode 100644 index 00000000..9e6d1b4d --- /dev/null +++ b/vendor/src/github.com/docker/notary/passphrase/passphrase.go @@ -0,0 +1,201 @@ +// Package passphrase is a utility function for managing passphrase +// for TUF and Notary keys. +package passphrase + +import ( + "bufio" + "errors" + "fmt" + "io" + "os" + "strings" + + "path/filepath" + + "github.com/docker/docker/pkg/term" +) + +// Retriever is a callback function that should retrieve a passphrase +// for a given named key. If it should be treated as new passphrase (e.g. with +// confirmation), createNew will be true. Attempts is passed in so that implementers +// decide how many chances to give to a human, for example. +type Retriever func(keyName, alias string, createNew bool, attempts int) (passphrase string, giveup bool, err error) + +const ( + idBytesToDisplay = 7 + tufRootAlias = "root" + tufTargetsAlias = "targets" + tufSnapshotAlias = "snapshot" + tufRootKeyGenerationWarning = `You are about to create a new root signing key passphrase. This passphrase +will be used to protect the most sensitive key in your signing system. Please +choose a long, complex passphrase and be careful to keep the password and the +key file itself secure and backed up. It is highly recommended that you use a +password manager to generate the passphrase and keep it safe. There will be no +way to recover this key. You can find the key in your config directory.` +) + +var ( + // ErrTooShort is returned if the passphrase entered for a new key is + // below the minimum length + ErrTooShort = errors.New("Passphrase too short") + + // ErrDontMatch is returned if the two entered passphrases don't match. + // new key is below the minimum length + ErrDontMatch = errors.New("The entered passphrases do not match") + + // ErrTooManyAttempts is returned if the maximum number of passphrase + // entry attempts is reached. + ErrTooManyAttempts = errors.New("Too many attempts") +) + +// PromptRetriever returns a new Retriever which will provide a prompt on stdin +// and stdout to retrieve a passphrase. The passphrase will be cached such that +// subsequent prompts will produce the same passphrase. +func PromptRetriever() Retriever { + return PromptRetrieverWithInOut(os.Stdin, os.Stdout, nil) +} + +// PromptRetrieverWithInOut returns a new Retriever which will provide a +// prompt using the given in and out readers. The passphrase will be cached +// such that subsequent prompts will produce the same passphrase. +// aliasMap can be used to specify display names for TUF key aliases. If aliasMap +// is nil, a sensible default will be used. +func PromptRetrieverWithInOut(in io.Reader, out io.Writer, aliasMap map[string]string) Retriever { + userEnteredTargetsSnapshotsPass := false + targetsSnapshotsPass := "" + userEnteredRootsPass := false + rootsPass := "" + + return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) { + if alias == tufRootAlias && createNew && numAttempts == 0 { + fmt.Fprintln(out, tufRootKeyGenerationWarning) + } + if numAttempts > 0 { + if !createNew { + fmt.Fprintln(out, "Passphrase incorrect. Please retry.") + } + } + + // Figure out if we should display a different string for this alias + displayAlias := alias + if aliasMap != nil { + if val, ok := aliasMap[alias]; ok { + displayAlias = val + } + + } + + // First, check if we have a password cached for this alias. + if numAttempts == 0 { + if userEnteredTargetsSnapshotsPass && (alias == tufSnapshotAlias || alias == tufTargetsAlias) { + return targetsSnapshotsPass, false, nil + } + if userEnteredRootsPass && (alias == "root") { + return rootsPass, false, nil + } + } + + if numAttempts > 3 && !createNew { + return "", true, ErrTooManyAttempts + } + + // If typing on the terminal, we do not want the terminal to echo the + // password that is typed (so it doesn't display) + if term.IsTerminal(0) { + state, err := term.SaveState(0) + if err != nil { + return "", false, err + } + term.DisableEcho(0, state) + defer term.RestoreTerminal(0, state) + } + + stdin := bufio.NewReader(in) + + indexOfLastSeparator := strings.LastIndex(keyName, string(filepath.Separator)) + if indexOfLastSeparator == -1 { + indexOfLastSeparator = 0 + } + + var shortName string + if len(keyName) > indexOfLastSeparator+idBytesToDisplay { + if indexOfLastSeparator > 0 { + keyNamePrefix := keyName[:indexOfLastSeparator] + keyNameID := keyName[indexOfLastSeparator+1 : indexOfLastSeparator+idBytesToDisplay+1] + shortName = keyNameID + " (" + keyNamePrefix + ")" + } else { + shortName = keyName[indexOfLastSeparator : indexOfLastSeparator+idBytesToDisplay] + } + } + + withID := fmt.Sprintf(" with ID %s", shortName) + if shortName == "" { + withID = "" + } + + if createNew { + fmt.Fprintf(out, "Enter passphrase for new %s key%s: ", displayAlias, withID) + } else if displayAlias == "yubikey" { + fmt.Fprintf(out, "Enter the %s for the attached Yubikey: ", keyName) + } else { + fmt.Fprintf(out, "Enter passphrase for %s key%s: ", displayAlias, withID) + } + + passphrase, err := stdin.ReadBytes('\n') + fmt.Fprintln(out) + if err != nil { + return "", false, err + } + + retPass := strings.TrimSpace(string(passphrase)) + + if !createNew { + if alias == tufSnapshotAlias || alias == tufTargetsAlias { + userEnteredTargetsSnapshotsPass = true + targetsSnapshotsPass = retPass + } + if alias == tufRootAlias { + userEnteredRootsPass = true + rootsPass = retPass + } + return retPass, false, nil + } + + if len(retPass) < 8 { + fmt.Fprintln(out, "Passphrase is too short. Please use a password manager to generate and store a good random passphrase.") + return "", false, ErrTooShort + } + + fmt.Fprintf(out, "Repeat passphrase for new %s key%s: ", displayAlias, withID) + confirmation, err := stdin.ReadBytes('\n') + fmt.Fprintln(out) + if err != nil { + return "", false, err + } + confirmationStr := strings.TrimSpace(string(confirmation)) + + if retPass != confirmationStr { + fmt.Fprintln(out, "Passphrases do not match. Please retry.") + return "", false, ErrDontMatch + } + + if alias == tufSnapshotAlias || alias == tufTargetsAlias { + userEnteredTargetsSnapshotsPass = true + targetsSnapshotsPass = retPass + } + if alias == tufRootAlias { + userEnteredRootsPass = true + rootsPass = retPass + } + + return retPass, false, nil + } +} + +// ConstantRetriever returns a new Retriever which will return a constant string +// as a passphrase. +func ConstantRetriever(constantPassphrase string) Retriever { + return func(k, a string, c bool, n int) (string, bool, error) { + return constantPassphrase, false, nil + } +} diff --git a/vendor/src/github.com/docker/notary/server.Dockerfile b/vendor/src/github.com/docker/notary/server.Dockerfile new file mode 100644 index 00000000..0966cea2 --- /dev/null +++ b/vendor/src/github.com/docker/notary/server.Dockerfile @@ -0,0 +1,28 @@ +FROM golang:1.6.0 +MAINTAINER David Lawrence "david.lawrence@docker.com" + +RUN apt-get update && apt-get install -y \ + libltdl-dev \ + --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* + +EXPOSE 4443 + +# Install DB migration tool +RUN go get github.com/mattes/migrate + +ENV NOTARYPKG github.com/docker/notary + +# Copy the local repo to the expected go path +COPY . /go/src/github.com/docker/notary + +WORKDIR /go/src/${NOTARYPKG} + +# Install notary-server +RUN go install \ + -tags pkcs11 \ + -ldflags "-w -X ${NOTARYPKG}/version.GitCommit=`git rev-parse --short HEAD` -X ${NOTARYPKG}/version.NotaryVersion=`cat NOTARY_VERSION`" \ + ${NOTARYPKG}/cmd/notary-server + +ENTRYPOINT [ "notary-server" ] +CMD [ "-config=fixtures/server-config-local.json" ] diff --git a/vendor/src/github.com/docker/notary/signer.Dockerfile b/vendor/src/github.com/docker/notary/signer.Dockerfile new file mode 100644 index 00000000..846e30e5 --- /dev/null +++ b/vendor/src/github.com/docker/notary/signer.Dockerfile @@ -0,0 +1,30 @@ +FROM golang:1.6.0 +MAINTAINER David Lawrence "david.lawrence@docker.com" + +RUN apt-get update && apt-get install -y \ + libltdl-dev \ + --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* + +EXPOSE 4444 + +# Install DB migration tool +RUN go get github.com/mattes/migrate + +ENV NOTARYPKG github.com/docker/notary +ENV NOTARY_SIGNER_DEFAULT_ALIAS="timestamp_1" +ENV NOTARY_SIGNER_TIMESTAMP_1="testpassword" + +# Copy the local repo to the expected go path +COPY . /go/src/github.com/docker/notary + +WORKDIR /go/src/${NOTARYPKG} + +# Install notary-signer +RUN go install \ + -tags pkcs11 \ + -ldflags "-w -X ${NOTARYPKG}/version.GitCommit=`git rev-parse --short HEAD` -X ${NOTARYPKG}/version.NotaryVersion=`cat NOTARY_VERSION`" \ + ${NOTARYPKG}/cmd/notary-signer + +ENTRYPOINT [ "notary-signer" ] +CMD [ "-config=fixtures/signer-config-local.json" ] diff --git a/vendor/src/github.com/docker/notary/trustmanager/filestore.go b/vendor/src/github.com/docker/notary/trustmanager/filestore.go new file mode 100644 index 00000000..b970159e --- /dev/null +++ b/vendor/src/github.com/docker/notary/trustmanager/filestore.go @@ -0,0 +1,191 @@ +package trustmanager + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +// SimpleFileStore implements FileStore +type SimpleFileStore struct { + baseDir string + fileExt string + perms os.FileMode +} + +// NewSimpleFileStore creates a directory with 755 permissions +func NewSimpleFileStore(baseDir string, fileExt string) (*SimpleFileStore, error) { + baseDir = filepath.Clean(baseDir) + + if err := CreateDirectory(baseDir); err != nil { + return nil, err + } + + if !strings.HasPrefix(fileExt, ".") { + fileExt = "." + fileExt + } + + return &SimpleFileStore{ + baseDir: baseDir, + fileExt: fileExt, + perms: visible, + }, nil +} + +// NewPrivateSimpleFileStore creates a directory with 700 permissions +func NewPrivateSimpleFileStore(baseDir string, fileExt string) (*SimpleFileStore, error) { + if err := CreatePrivateDirectory(baseDir); err != nil { + return nil, err + } + + if !strings.HasPrefix(fileExt, ".") { + fileExt = "." + fileExt + } + + return &SimpleFileStore{ + baseDir: baseDir, + fileExt: fileExt, + perms: private, + }, nil +} + +// Add writes data to a file with a given name +func (f *SimpleFileStore) Add(name string, data []byte) error { + filePath, err := f.GetPath(name) + if err != nil { + return err + } + createDirectory(filepath.Dir(filePath), f.perms) + return ioutil.WriteFile(filePath, data, f.perms) +} + +// Remove removes a file identified by name +func (f *SimpleFileStore) Remove(name string) error { + // Attempt to remove + filePath, err := f.GetPath(name) + if err != nil { + return err + } + return os.Remove(filePath) +} + +// RemoveDir removes the directory identified by name +func (f *SimpleFileStore) RemoveDir(name string) error { + dirPath := filepath.Join(f.baseDir, name) + + // Check to see if directory exists + fi, err := os.Stat(dirPath) + if err != nil { + return err + } + + // Check to see if it is a directory + if !fi.IsDir() { + return fmt.Errorf("directory not found: %s", name) + } + + return os.RemoveAll(dirPath) +} + +// Get returns the data given a file name +func (f *SimpleFileStore) Get(name string) ([]byte, error) { + filePath, err := f.GetPath(name) + if err != nil { + return nil, err + } + data, err := ioutil.ReadFile(filePath) + if err != nil { + return nil, err + } + + return data, nil +} + +// GetPath returns the full final path of a file with a given name +func (f *SimpleFileStore) GetPath(name string) (string, error) { + fileName := f.genFileName(name) + fullPath := filepath.Clean(filepath.Join(f.baseDir, fileName)) + + if !strings.HasPrefix(fullPath, f.baseDir) { + return "", ErrPathOutsideStore + } + return fullPath, nil +} + +// ListFiles lists all the files inside of a store +func (f *SimpleFileStore) ListFiles() []string { + return f.list(f.baseDir) +} + +// ListDir lists all the files inside of a directory identified by a name +func (f *SimpleFileStore) ListDir(name string) []string { + fullPath := filepath.Join(f.baseDir, name) + return f.list(fullPath) +} + +// list lists all the files in a directory given a full path. Ignores symlinks. +func (f *SimpleFileStore) list(path string) []string { + files := make([]string, 0, 0) + filepath.Walk(path, func(fp string, fi os.FileInfo, err error) error { + // If there are errors, ignore this particular file + if err != nil { + return nil + } + // Ignore if it is a directory + if fi.IsDir() { + return nil + } + + // If this is a symlink, ignore it + if fi.Mode()&os.ModeSymlink == os.ModeSymlink { + return nil + } + + // Only allow matches that end with our certificate extension (e.g. *.crt) + matched, _ := filepath.Match("*"+f.fileExt, fi.Name()) + + if matched { + // Find the relative path for this file relative to the base path. + fp, err = filepath.Rel(path, fp) + if err != nil { + return err + } + trimmed := strings.TrimSuffix(fp, f.fileExt) + files = append(files, trimmed) + } + return nil + }) + return files +} + +// genFileName returns the name using the right extension +func (f *SimpleFileStore) genFileName(name string) string { + return fmt.Sprintf("%s%s", name, f.fileExt) +} + +// BaseDir returns the base directory of the filestore +func (f *SimpleFileStore) BaseDir() string { + return f.baseDir +} + +// CreateDirectory uses createDirectory to create a chmod 755 Directory +func CreateDirectory(dir string) error { + return createDirectory(dir, visible) +} + +// CreatePrivateDirectory uses createDirectory to create a chmod 700 Directory +func CreatePrivateDirectory(dir string) error { + return createDirectory(dir, private) +} + +// createDirectory receives a string of the path to a directory. +// It does not support passing files, so the caller has to remove +// the filename by doing filepath.Dir(full_path_to_file) +func createDirectory(dir string, perms os.FileMode) error { + // This prevents someone passing /path/to/dir and 'dir' not being created + // If two '//' exist, MkdirAll deals it with correctly + dir = dir + "/" + return os.MkdirAll(dir, perms) +} diff --git a/vendor/src/github.com/docker/notary/trustmanager/keyfilestore.go b/vendor/src/github.com/docker/notary/trustmanager/keyfilestore.go new file mode 100644 index 00000000..13fe4704 --- /dev/null +++ b/vendor/src/github.com/docker/notary/trustmanager/keyfilestore.go @@ -0,0 +1,497 @@ +package trustmanager + +import ( + "encoding/pem" + "fmt" + "path/filepath" + "strings" + "sync" + + "github.com/Sirupsen/logrus" + "github.com/docker/notary" + "github.com/docker/notary/passphrase" + "github.com/docker/notary/tuf/data" +) + +type keyInfoMap map[string]KeyInfo + +// KeyFileStore persists and manages private keys on disk +type KeyFileStore struct { + sync.Mutex + SimpleFileStore + passphrase.Retriever + cachedKeys map[string]*cachedKey + keyInfoMap +} + +// KeyMemoryStore manages private keys in memory +type KeyMemoryStore struct { + sync.Mutex + MemoryFileStore + passphrase.Retriever + cachedKeys map[string]*cachedKey + keyInfoMap +} + +// KeyInfo stores the role, path, and gun for a corresponding private key ID +// It is assumed that each private key ID is unique +type KeyInfo struct { + Gun string + Role string +} + +// NewKeyFileStore returns a new KeyFileStore creating a private directory to +// hold the keys. +func NewKeyFileStore(baseDir string, passphraseRetriever passphrase.Retriever) (*KeyFileStore, error) { + baseDir = filepath.Join(baseDir, notary.PrivDir) + fileStore, err := NewPrivateSimpleFileStore(baseDir, keyExtension) + if err != nil { + return nil, err + } + cachedKeys := make(map[string]*cachedKey) + keyInfoMap := make(keyInfoMap) + + keyStore := &KeyFileStore{SimpleFileStore: *fileStore, + Retriever: passphraseRetriever, + cachedKeys: cachedKeys, + keyInfoMap: keyInfoMap, + } + + // Load this keystore's ID --> gun/role map + keyStore.loadKeyInfo() + return keyStore, nil +} + +func generateKeyInfoMap(s LimitedFileStore) map[string]KeyInfo { + keyInfoMap := make(map[string]KeyInfo) + for _, keyPath := range s.ListFiles() { + d, err := s.Get(keyPath) + if err != nil { + logrus.Error(err) + continue + } + keyID, keyInfo, err := KeyInfoFromPEM(d, keyPath) + if err != nil { + logrus.Error(err) + continue + } + keyInfoMap[keyID] = keyInfo + } + return keyInfoMap +} + +// Attempts to infer the keyID, role, and GUN from the specified key path. +// Note that non-root roles can only be inferred if this is a legacy style filename: KEYID_ROLE.key +func inferKeyInfoFromKeyPath(keyPath string) (string, string, string) { + var keyID, role, gun string + keyID = filepath.Base(keyPath) + underscoreIndex := strings.LastIndex(keyID, "_") + + // This is the legacy KEYID_ROLE filename + // The keyID is the first part of the keyname + // The keyRole is the second part of the keyname + // in a key named abcde_root, abcde is the keyID and root is the KeyAlias + if underscoreIndex != -1 { + role = keyID[underscoreIndex+1:] + keyID = keyID[:underscoreIndex] + } + + if filepath.HasPrefix(keyPath, notary.RootKeysSubdir+"/") { + return keyID, data.CanonicalRootRole, "" + } + + keyPath = strings.TrimPrefix(keyPath, notary.NonRootKeysSubdir+"/") + gun = getGunFromFullID(keyPath) + return keyID, role, gun +} + +func getGunFromFullID(fullKeyID string) string { + keyGun := filepath.Dir(fullKeyID) + // If the gun is empty, Dir will return . + if keyGun == "." { + keyGun = "" + } + return keyGun +} + +func (s *KeyFileStore) loadKeyInfo() { + s.keyInfoMap = generateKeyInfoMap(s) +} + +func (s *KeyMemoryStore) loadKeyInfo() { + s.keyInfoMap = generateKeyInfoMap(s) +} + +// GetKeyInfo returns the corresponding gun and role key info for a keyID +func (s *KeyFileStore) GetKeyInfo(keyID string) (KeyInfo, error) { + if info, ok := s.keyInfoMap[keyID]; ok { + return info, nil + } + return KeyInfo{}, fmt.Errorf("Could not find info for keyID %s", keyID) +} + +// GetKeyInfo returns the corresponding gun and role key info for a keyID +func (s *KeyMemoryStore) GetKeyInfo(keyID string) (KeyInfo, error) { + if info, ok := s.keyInfoMap[keyID]; ok { + return info, nil + } + return KeyInfo{}, fmt.Errorf("Could not find info for keyID %s", keyID) +} + +// Name returns a user friendly name for the location this store +// keeps its data +func (s *KeyFileStore) Name() string { + return fmt.Sprintf("file (%s)", s.SimpleFileStore.BaseDir()) +} + +// AddKey stores the contents of a PEM-encoded private key as a PEM block +func (s *KeyFileStore) AddKey(keyInfo KeyInfo, privKey data.PrivateKey) error { + s.Lock() + defer s.Unlock() + if keyInfo.Role == data.CanonicalRootRole || data.IsDelegation(keyInfo.Role) || !data.ValidRole(keyInfo.Role) { + keyInfo.Gun = "" + } + err := addKey(s, s.Retriever, s.cachedKeys, filepath.Join(keyInfo.Gun, privKey.ID()), keyInfo.Role, privKey) + if err != nil { + return err + } + s.keyInfoMap[privKey.ID()] = keyInfo + return nil +} + +// GetKey returns the PrivateKey given a KeyID +func (s *KeyFileStore) GetKey(name string) (data.PrivateKey, string, error) { + s.Lock() + defer s.Unlock() + // If this is a bare key ID without the gun, prepend the gun so the filestore lookup succeeds + if keyInfo, ok := s.keyInfoMap[name]; ok { + name = filepath.Join(keyInfo.Gun, name) + } + return getKey(s, s.Retriever, s.cachedKeys, name) +} + +// ListKeys returns a list of unique PublicKeys present on the KeyFileStore, by returning a copy of the keyInfoMap +func (s *KeyFileStore) ListKeys() map[string]KeyInfo { + return copyKeyInfoMap(s.keyInfoMap) +} + +// RemoveKey removes the key from the keyfilestore +func (s *KeyFileStore) RemoveKey(keyID string) error { + s.Lock() + defer s.Unlock() + // If this is a bare key ID without the gun, prepend the gun so the filestore lookup succeeds + if keyInfo, ok := s.keyInfoMap[keyID]; ok { + keyID = filepath.Join(keyInfo.Gun, keyID) + } + err := removeKey(s, s.cachedKeys, keyID) + if err != nil { + return err + } + // Remove this key from our keyInfo map if we removed from our filesystem + delete(s.keyInfoMap, filepath.Base(keyID)) + return nil +} + +// ExportKey exports the encrypted bytes from the keystore +func (s *KeyFileStore) ExportKey(keyID string) ([]byte, error) { + if keyInfo, ok := s.keyInfoMap[keyID]; ok { + keyID = filepath.Join(keyInfo.Gun, keyID) + } + keyBytes, _, err := getRawKey(s, keyID) + if err != nil { + return nil, err + } + return keyBytes, nil +} + +// NewKeyMemoryStore returns a new KeyMemoryStore which holds keys in memory +func NewKeyMemoryStore(passphraseRetriever passphrase.Retriever) *KeyMemoryStore { + memStore := NewMemoryFileStore() + cachedKeys := make(map[string]*cachedKey) + + keyInfoMap := make(keyInfoMap) + + keyStore := &KeyMemoryStore{MemoryFileStore: *memStore, + Retriever: passphraseRetriever, + cachedKeys: cachedKeys, + keyInfoMap: keyInfoMap, + } + + // Load this keystore's ID --> gun/role map + keyStore.loadKeyInfo() + return keyStore +} + +// Name returns a user friendly name for the location this store +// keeps its data +func (s *KeyMemoryStore) Name() string { + return "memory" +} + +// AddKey stores the contents of a PEM-encoded private key as a PEM block +func (s *KeyMemoryStore) AddKey(keyInfo KeyInfo, privKey data.PrivateKey) error { + s.Lock() + defer s.Unlock() + if keyInfo.Role == data.CanonicalRootRole || data.IsDelegation(keyInfo.Role) || !data.ValidRole(keyInfo.Role) { + keyInfo.Gun = "" + } + err := addKey(s, s.Retriever, s.cachedKeys, filepath.Join(keyInfo.Gun, privKey.ID()), keyInfo.Role, privKey) + if err != nil { + return err + } + s.keyInfoMap[privKey.ID()] = keyInfo + return nil +} + +// GetKey returns the PrivateKey given a KeyID +func (s *KeyMemoryStore) GetKey(name string) (data.PrivateKey, string, error) { + s.Lock() + defer s.Unlock() + // If this is a bare key ID without the gun, prepend the gun so the filestore lookup succeeds + if keyInfo, ok := s.keyInfoMap[name]; ok { + name = filepath.Join(keyInfo.Gun, name) + } + return getKey(s, s.Retriever, s.cachedKeys, name) +} + +// ListKeys returns a list of unique PublicKeys present on the KeyFileStore, by returning a copy of the keyInfoMap +func (s *KeyMemoryStore) ListKeys() map[string]KeyInfo { + return copyKeyInfoMap(s.keyInfoMap) +} + +// copyKeyInfoMap returns a deep copy of the passed-in keyInfoMap +func copyKeyInfoMap(keyInfoMap map[string]KeyInfo) map[string]KeyInfo { + copyMap := make(map[string]KeyInfo) + for keyID, keyInfo := range keyInfoMap { + copyMap[keyID] = KeyInfo{Role: keyInfo.Role, Gun: keyInfo.Gun} + } + return copyMap +} + +// RemoveKey removes the key from the keystore +func (s *KeyMemoryStore) RemoveKey(keyID string) error { + s.Lock() + defer s.Unlock() + // If this is a bare key ID without the gun, prepend the gun so the filestore lookup succeeds + if keyInfo, ok := s.keyInfoMap[keyID]; ok { + keyID = filepath.Join(keyInfo.Gun, keyID) + } + err := removeKey(s, s.cachedKeys, keyID) + if err != nil { + return err + } + // Remove this key from our keyInfo map if we removed from our filesystem + delete(s.keyInfoMap, filepath.Base(keyID)) + return nil +} + +// ExportKey exports the encrypted bytes from the keystore +func (s *KeyMemoryStore) ExportKey(keyID string) ([]byte, error) { + keyBytes, _, err := getRawKey(s, keyID) + if err != nil { + return nil, err + } + return keyBytes, nil +} + +// KeyInfoFromPEM attempts to get a keyID and KeyInfo from the filename and PEM bytes of a key +func KeyInfoFromPEM(pemBytes []byte, filename string) (string, KeyInfo, error) { + keyID, role, gun := inferKeyInfoFromKeyPath(filename) + if role == "" { + block, _ := pem.Decode(pemBytes) + if block == nil { + return "", KeyInfo{}, fmt.Errorf("could not decode PEM block for key %s", filename) + } + if keyRole, ok := block.Headers["role"]; ok { + role = keyRole + } + } + return keyID, KeyInfo{Gun: gun, Role: role}, nil +} + +func addKey(s LimitedFileStore, passphraseRetriever passphrase.Retriever, cachedKeys map[string]*cachedKey, name, role string, privKey data.PrivateKey) error { + + var ( + chosenPassphrase string + giveup bool + err error + ) + + for attempts := 0; ; attempts++ { + chosenPassphrase, giveup, err = passphraseRetriever(name, role, true, attempts) + if err != nil { + continue + } + if giveup { + return ErrAttemptsExceeded{} + } + if attempts > 10 { + return ErrAttemptsExceeded{} + } + break + } + + return encryptAndAddKey(s, chosenPassphrase, cachedKeys, name, role, privKey) +} + +// getKeyRole finds the role for the given keyID. It attempts to look +// both in the newer format PEM headers, and also in the legacy filename +// format. It returns: the role, whether it was found in the legacy format +// (true == legacy), and an error +func getKeyRole(s LimitedFileStore, keyID string) (string, bool, error) { + name := strings.TrimSpace(strings.TrimSuffix(filepath.Base(keyID), filepath.Ext(keyID))) + + for _, file := range s.ListFiles() { + filename := filepath.Base(file) + + if strings.HasPrefix(filename, name) { + d, err := s.Get(file) + if err != nil { + return "", false, err + } + block, _ := pem.Decode(d) + if block != nil { + if role, ok := block.Headers["role"]; ok { + return role, false, nil + } + } + + role := strings.TrimPrefix(filename, name+"_") + return role, true, nil + } + } + + return "", false, &ErrKeyNotFound{KeyID: keyID} +} + +// GetKey returns the PrivateKey given a KeyID +func getKey(s LimitedFileStore, passphraseRetriever passphrase.Retriever, cachedKeys map[string]*cachedKey, name string) (data.PrivateKey, string, error) { + cachedKeyEntry, ok := cachedKeys[name] + if ok { + return cachedKeyEntry.key, cachedKeyEntry.alias, nil + } + + keyBytes, keyAlias, err := getRawKey(s, name) + if err != nil { + return nil, "", err + } + + // See if the key is encrypted. If its encrypted we'll fail to parse the private key + privKey, err := ParsePEMPrivateKey(keyBytes, "") + if err != nil { + privKey, _, err = GetPasswdDecryptBytes(passphraseRetriever, keyBytes, name, string(keyAlias)) + if err != nil { + return nil, "", err + } + } + cachedKeys[name] = &cachedKey{alias: keyAlias, key: privKey} + return privKey, keyAlias, nil +} + +// RemoveKey removes the key from the keyfilestore +func removeKey(s LimitedFileStore, cachedKeys map[string]*cachedKey, name string) error { + role, legacy, err := getKeyRole(s, name) + if err != nil { + return err + } + + delete(cachedKeys, name) + + if legacy { + name = name + "_" + role + } + + // being in a subdirectory is for backwards compatibliity + err = s.Remove(filepath.Join(getSubdir(role), name)) + if err != nil { + return err + } + return nil +} + +// Assumes 2 subdirectories, 1 containing root keys and 1 containing tuf keys +func getSubdir(alias string) string { + if alias == data.CanonicalRootRole { + return notary.RootKeysSubdir + } + return notary.NonRootKeysSubdir +} + +// Given a key ID, gets the bytes and alias belonging to that key if the key +// exists +func getRawKey(s LimitedFileStore, name string) ([]byte, string, error) { + role, legacy, err := getKeyRole(s, name) + if err != nil { + return nil, "", err + } + + if legacy { + name = name + "_" + role + } + + var keyBytes []byte + keyBytes, err = s.Get(filepath.Join(getSubdir(role), name)) + if err != nil { + return nil, "", err + } + return keyBytes, role, nil +} + +// GetPasswdDecryptBytes gets the password to decrypt the given pem bytes. +// Returns the password and private key +func GetPasswdDecryptBytes(passphraseRetriever passphrase.Retriever, pemBytes []byte, name, alias string) (data.PrivateKey, string, error) { + var ( + passwd string + retErr error + privKey data.PrivateKey + ) + for attempts := 0; ; attempts++ { + var ( + giveup bool + err error + ) + passwd, giveup, err = passphraseRetriever(name, alias, false, attempts) + // Check if the passphrase retriever got an error or if it is telling us to give up + if giveup || err != nil { + return nil, "", ErrPasswordInvalid{} + } + if attempts > 10 { + return nil, "", ErrAttemptsExceeded{} + } + + // Try to convert PEM encoded bytes back to a PrivateKey using the passphrase + privKey, err = ParsePEMPrivateKey(pemBytes, passwd) + if err != nil { + retErr = ErrPasswordInvalid{} + } else { + // We managed to parse the PrivateKey. We've succeeded! + retErr = nil + break + } + } + if retErr != nil { + return nil, "", retErr + } + return privKey, passwd, nil +} + +func encryptAndAddKey(s LimitedFileStore, passwd string, cachedKeys map[string]*cachedKey, name, role string, privKey data.PrivateKey) error { + + var ( + pemPrivKey []byte + err error + ) + + if passwd != "" { + pemPrivKey, err = EncryptPrivateKey(privKey, role, passwd) + } else { + pemPrivKey, err = KeyToPEM(privKey, role) + } + + if err != nil { + return err + } + + cachedKeys[name] = &cachedKey{alias: role, key: privKey} + return s.Add(filepath.Join(getSubdir(role), name), pemPrivKey) +} diff --git a/vendor/src/github.com/docker/notary/trustmanager/keystore.go b/vendor/src/github.com/docker/notary/trustmanager/keystore.go new file mode 100644 index 00000000..4f1338e4 --- /dev/null +++ b/vendor/src/github.com/docker/notary/trustmanager/keystore.go @@ -0,0 +1,57 @@ +package trustmanager + +import ( + "fmt" + + "github.com/docker/notary/tuf/data" +) + +// ErrAttemptsExceeded is returned when too many attempts have been made to decrypt a key +type ErrAttemptsExceeded struct{} + +// ErrAttemptsExceeded is returned when too many attempts have been made to decrypt a key +func (err ErrAttemptsExceeded) Error() string { + return "maximum number of passphrase attempts exceeded" +} + +// ErrPasswordInvalid is returned when signing fails. It could also mean the signing +// key file was corrupted, but we have no way to distinguish. +type ErrPasswordInvalid struct{} + +// ErrPasswordInvalid is returned when signing fails. It could also mean the signing +// key file was corrupted, but we have no way to distinguish. +func (err ErrPasswordInvalid) Error() string { + return "password invalid, operation has failed." +} + +// ErrKeyNotFound is returned when the keystore fails to retrieve a specific key. +type ErrKeyNotFound struct { + KeyID string +} + +// ErrKeyNotFound is returned when the keystore fails to retrieve a specific key. +func (err ErrKeyNotFound) Error() string { + return fmt.Sprintf("signing key not found: %s", err.KeyID) +} + +const ( + keyExtension = "key" +) + +// KeyStore is a generic interface for private key storage +type KeyStore interface { + // AddKey adds a key to the KeyStore, and if the key already exists, + // succeeds. Otherwise, returns an error if it cannot add. + AddKey(keyInfo KeyInfo, privKey data.PrivateKey) error + GetKey(keyID string) (data.PrivateKey, string, error) + GetKeyInfo(keyID string) (KeyInfo, error) + ListKeys() map[string]KeyInfo + RemoveKey(keyID string) error + ExportKey(keyID string) ([]byte, error) + Name() string +} + +type cachedKey struct { + alias string + key data.PrivateKey +} diff --git a/vendor/src/github.com/docker/notary/trustmanager/memorystore.go b/vendor/src/github.com/docker/notary/trustmanager/memorystore.go new file mode 100644 index 00000000..ffb825fa --- /dev/null +++ b/vendor/src/github.com/docker/notary/trustmanager/memorystore.go @@ -0,0 +1,67 @@ +package trustmanager + +import ( + "os" + "sync" +) + +// MemoryFileStore is an implementation of LimitedFileStore that keeps +// the contents in memory. +type MemoryFileStore struct { + sync.Mutex + + files map[string][]byte +} + +// NewMemoryFileStore creates a MemoryFileStore +func NewMemoryFileStore() *MemoryFileStore { + return &MemoryFileStore{ + files: make(map[string][]byte), + } +} + +// Add writes data to a file with a given name +func (f *MemoryFileStore) Add(name string, data []byte) error { + f.Lock() + defer f.Unlock() + + f.files[name] = data + return nil +} + +// Remove removes a file identified by name +func (f *MemoryFileStore) Remove(name string) error { + f.Lock() + defer f.Unlock() + + if _, present := f.files[name]; !present { + return os.ErrNotExist + } + delete(f.files, name) + + return nil +} + +// Get returns the data given a file name +func (f *MemoryFileStore) Get(name string) ([]byte, error) { + f.Lock() + defer f.Unlock() + + fileData, present := f.files[name] + if !present { + return nil, os.ErrNotExist + } + + return fileData, nil +} + +// ListFiles lists all the files inside of a store +func (f *MemoryFileStore) ListFiles() []string { + var list []string + + for name := range f.files { + list = append(list, name) + } + + return list +} diff --git a/vendor/src/github.com/docker/notary/trustmanager/store.go b/vendor/src/github.com/docker/notary/trustmanager/store.go new file mode 100644 index 00000000..6672449c --- /dev/null +++ b/vendor/src/github.com/docker/notary/trustmanager/store.go @@ -0,0 +1,52 @@ +package trustmanager + +import ( + "errors" + + "github.com/docker/notary" +) + +const ( + visible = notary.PubCertPerms + private = notary.PrivKeyPerms +) + +var ( + // ErrPathOutsideStore indicates that the returned path would be + // outside the store + ErrPathOutsideStore = errors.New("path outside file store") +) + +// LimitedFileStore implements the bare bones primitives (no hierarchy) +type LimitedFileStore interface { + // Add writes a file to the specified location, returning an error if this + // is not possible (reasons may include permissions errors). The path is cleaned + // before being made absolute against the store's base dir. + Add(fileName string, data []byte) error + + // Remove deletes a file from the store relative to the store's base directory. + // The path is cleaned before being made absolute to ensure no path traversal + // outside the base directory is possible. + Remove(fileName string) error + + // Get returns the file content found at fileName relative to the base directory + // of the file store. The path is cleaned before being made absolute to ensure + // path traversal outside the store is not possible. If the file is not found + // an error to that effect is returned. + Get(fileName string) ([]byte, error) + + // ListFiles returns a list of paths relative to the base directory of the + // filestore. Any of these paths must be retrievable via the + // LimitedFileStore.Get method. + ListFiles() []string +} + +// FileStore is the interface for full-featured FileStores +type FileStore interface { + LimitedFileStore + + RemoveDir(directoryName string) error + GetPath(fileName string) (string, error) + ListDir(directoryName string) []string + BaseDir() string +} diff --git a/vendor/src/github.com/docker/notary/trustmanager/x509filestore.go b/vendor/src/github.com/docker/notary/trustmanager/x509filestore.go new file mode 100644 index 00000000..84178055 --- /dev/null +++ b/vendor/src/github.com/docker/notary/trustmanager/x509filestore.go @@ -0,0 +1,276 @@ +package trustmanager + +import ( + "crypto/x509" + "errors" + "os" + "path" + + "github.com/Sirupsen/logrus" +) + +// X509FileStore implements X509Store that persists on disk +type X509FileStore struct { + validate Validator + fileMap map[CertID]string + fingerprintMap map[CertID]*x509.Certificate + nameMap map[string][]CertID + fileStore FileStore +} + +// NewX509FileStore returns a new X509FileStore. +func NewX509FileStore(directory string) (*X509FileStore, error) { + validate := ValidatorFunc(func(cert *x509.Certificate) bool { return true }) + return newX509FileStore(directory, validate) +} + +// NewX509FilteredFileStore returns a new X509FileStore that validates certificates +// that are added. +func NewX509FilteredFileStore(directory string, validate func(*x509.Certificate) bool) (*X509FileStore, error) { + return newX509FileStore(directory, validate) +} + +func newX509FileStore(directory string, validate func(*x509.Certificate) bool) (*X509FileStore, error) { + fileStore, err := NewSimpleFileStore(directory, certExtension) + if err != nil { + return nil, err + } + + s := &X509FileStore{ + validate: ValidatorFunc(validate), + fileMap: make(map[CertID]string), + fingerprintMap: make(map[CertID]*x509.Certificate), + nameMap: make(map[string][]CertID), + fileStore: fileStore, + } + + err = loadCertsFromDir(s) + if err != nil { + return nil, err + } + + return s, nil +} + +// AddCert creates a filename for a given cert and adds a certificate with that name +func (s *X509FileStore) AddCert(cert *x509.Certificate) error { + if cert == nil { + return errors.New("adding nil Certificate to X509Store") + } + + // Check if this certificate meets our validation criteria + if !s.validate.Validate(cert) { + return &ErrCertValidation{} + } + // Attempt to write the certificate to the file + if err := s.addNamedCert(cert); err != nil { + return err + } + + return nil +} + +// addNamedCert allows adding a certificate while controlling the filename it gets +// stored under. If the file does not exist on disk, saves it. +func (s *X509FileStore) addNamedCert(cert *x509.Certificate) error { + fileName, certID, err := fileName(cert) + if err != nil { + return err + } + + logrus.Debug("Adding cert with certID: ", certID) + // Validate if we already added this certificate before + if _, ok := s.fingerprintMap[certID]; ok { + return &ErrCertExists{} + } + + // Convert certificate to PEM + certBytes := CertToPEM(cert) + + // Save the file to disk if not already there. + filePath, err := s.fileStore.GetPath(fileName) + if err != nil { + return err + } + if _, err := os.Stat(filePath); os.IsNotExist(err) { + if err := s.fileStore.Add(fileName, certBytes); err != nil { + return err + } + } else if err != nil { + return err + } + + // We wrote the certificate succcessfully, add it to our in-memory storage + s.fingerprintMap[certID] = cert + s.fileMap[certID] = fileName + + name := string(cert.Subject.CommonName) + s.nameMap[name] = append(s.nameMap[name], certID) + + return nil +} + +// RemoveCert removes a certificate from a X509FileStore. +func (s *X509FileStore) RemoveCert(cert *x509.Certificate) error { + if cert == nil { + return errors.New("removing nil Certificate from X509Store") + } + + certID, err := fingerprintCert(cert) + if err != nil { + return err + } + delete(s.fingerprintMap, certID) + filename := s.fileMap[certID] + delete(s.fileMap, certID) + + name := string(cert.Subject.CommonName) + + // Filter the fingerprint out of this name entry + fpList := s.nameMap[name] + newfpList := fpList[:0] + for _, x := range fpList { + if x != certID { + newfpList = append(newfpList, x) + } + } + + s.nameMap[name] = newfpList + + if err := s.fileStore.Remove(filename); err != nil { + return err + } + + return nil +} + +// RemoveAll removes all the certificates from the store +func (s *X509FileStore) RemoveAll() error { + for _, filename := range s.fileMap { + if err := s.fileStore.Remove(filename); err != nil { + return err + } + } + s.fileMap = make(map[CertID]string) + s.fingerprintMap = make(map[CertID]*x509.Certificate) + s.nameMap = make(map[string][]CertID) + + return nil +} + +// AddCertFromPEM adds the first certificate that it finds in the byte[], returning +// an error if no Certificates are found +func (s X509FileStore) AddCertFromPEM(pemBytes []byte) error { + cert, err := LoadCertFromPEM(pemBytes) + if err != nil { + return err + } + return s.AddCert(cert) +} + +// AddCertFromFile tries to adds a X509 certificate to the store given a filename +func (s *X509FileStore) AddCertFromFile(filename string) error { + cert, err := LoadCertFromFile(filename) + if err != nil { + return err + } + + return s.AddCert(cert) +} + +// GetCertificates returns an array with all of the current X509 Certificates. +func (s *X509FileStore) GetCertificates() []*x509.Certificate { + certs := make([]*x509.Certificate, len(s.fingerprintMap)) + i := 0 + for _, v := range s.fingerprintMap { + certs[i] = v + i++ + } + return certs +} + +// GetCertificatePool returns an x509 CertPool loaded with all the certificates +// in the store. +func (s *X509FileStore) GetCertificatePool() *x509.CertPool { + pool := x509.NewCertPool() + + for _, v := range s.fingerprintMap { + pool.AddCert(v) + } + return pool +} + +// GetCertificateByCertID returns the certificate that matches a certain certID +func (s *X509FileStore) GetCertificateByCertID(certID string) (*x509.Certificate, error) { + return s.getCertificateByCertID(CertID(certID)) +} + +// getCertificateByCertID returns the certificate that matches a certain certID +func (s *X509FileStore) getCertificateByCertID(certID CertID) (*x509.Certificate, error) { + // If it does not look like a hex encoded sha256 hash, error + if len(certID) != 64 { + return nil, errors.New("invalid Subject Key Identifier") + } + + // Check to see if this subject key identifier exists + if cert, ok := s.fingerprintMap[CertID(certID)]; ok { + return cert, nil + + } + return nil, &ErrNoCertificatesFound{query: string(certID)} +} + +// GetCertificatesByCN returns all the certificates that match a specific +// CommonName +func (s *X509FileStore) GetCertificatesByCN(cn string) ([]*x509.Certificate, error) { + var certs []*x509.Certificate + if ids, ok := s.nameMap[cn]; ok { + for _, v := range ids { + cert, err := s.getCertificateByCertID(v) + if err != nil { + // This error should never happen. This would mean that we have + // an inconsistent X509FileStore + return nil, &ErrBadCertificateStore{} + } + certs = append(certs, cert) + } + } + if len(certs) == 0 { + return nil, &ErrNoCertificatesFound{query: cn} + } + + return certs, nil +} + +// GetVerifyOptions returns VerifyOptions with the certificates within the KeyStore +// as part of the roots list. This never allows the use of system roots, returning +// an error if there are no root CAs. +func (s *X509FileStore) GetVerifyOptions(dnsName string) (x509.VerifyOptions, error) { + // If we have no Certificates loaded return error (we don't want to rever to using + // system CAs). + if len(s.fingerprintMap) == 0 { + return x509.VerifyOptions{}, errors.New("no root CAs available") + } + + opts := x509.VerifyOptions{ + DNSName: dnsName, + Roots: s.GetCertificatePool(), + } + + return opts, nil +} + +// Empty returns true if there are no certificates in the X509FileStore, false +// otherwise. +func (s *X509FileStore) Empty() bool { + return len(s.fingerprintMap) == 0 +} + +func fileName(cert *x509.Certificate) (string, CertID, error) { + certID, err := fingerprintCert(cert) + if err != nil { + return "", "", err + } + + return path.Join(cert.Subject.CommonName, string(certID)), certID, nil +} diff --git a/vendor/src/github.com/docker/notary/trustmanager/x509memstore.go b/vendor/src/github.com/docker/notary/trustmanager/x509memstore.go new file mode 100644 index 00000000..55666c09 --- /dev/null +++ b/vendor/src/github.com/docker/notary/trustmanager/x509memstore.go @@ -0,0 +1,203 @@ +package trustmanager + +import ( + "crypto/x509" + "errors" + + "github.com/Sirupsen/logrus" +) + +// X509MemStore implements X509Store as an in-memory object with no persistence +type X509MemStore struct { + validate Validator + fingerprintMap map[CertID]*x509.Certificate + nameMap map[string][]CertID +} + +// NewX509MemStore returns a new X509MemStore. +func NewX509MemStore() *X509MemStore { + validate := ValidatorFunc(func(cert *x509.Certificate) bool { return true }) + + return &X509MemStore{ + validate: validate, + fingerprintMap: make(map[CertID]*x509.Certificate), + nameMap: make(map[string][]CertID), + } +} + +// NewX509FilteredMemStore returns a new X509Memstore that validates certificates +// that are added. +func NewX509FilteredMemStore(validate func(*x509.Certificate) bool) *X509MemStore { + s := &X509MemStore{ + + validate: ValidatorFunc(validate), + fingerprintMap: make(map[CertID]*x509.Certificate), + nameMap: make(map[string][]CertID), + } + + return s +} + +// AddCert adds a certificate to the store +func (s *X509MemStore) AddCert(cert *x509.Certificate) error { + if cert == nil { + return errors.New("adding nil Certificate to X509Store") + } + + if !s.validate.Validate(cert) { + return &ErrCertValidation{} + } + + certID, err := fingerprintCert(cert) + if err != nil { + return err + } + + logrus.Debug("Adding cert with certID: ", certID) + + // In this store we overwrite the certificate if it already exists + s.fingerprintMap[certID] = cert + name := string(cert.RawSubject) + s.nameMap[name] = append(s.nameMap[name], certID) + + return nil +} + +// RemoveCert removes a certificate from a X509MemStore. +func (s *X509MemStore) RemoveCert(cert *x509.Certificate) error { + if cert == nil { + return errors.New("removing nil Certificate to X509Store") + } + + certID, err := fingerprintCert(cert) + if err != nil { + return err + } + delete(s.fingerprintMap, certID) + name := string(cert.RawSubject) + + // Filter the fingerprint out of this name entry + fpList := s.nameMap[name] + newfpList := fpList[:0] + for _, x := range fpList { + if x != certID { + newfpList = append(newfpList, x) + } + } + + s.nameMap[name] = newfpList + return nil +} + +// RemoveAll removes all the certificates from the store +func (s *X509MemStore) RemoveAll() error { + + for _, cert := range s.fingerprintMap { + if err := s.RemoveCert(cert); err != nil { + return err + } + } + + return nil +} + +// AddCertFromPEM adds a certificate to the store from a PEM blob +func (s *X509MemStore) AddCertFromPEM(pemBytes []byte) error { + cert, err := LoadCertFromPEM(pemBytes) + if err != nil { + return err + } + return s.AddCert(cert) +} + +// AddCertFromFile tries to adds a X509 certificate to the store given a filename +func (s *X509MemStore) AddCertFromFile(originFilname string) error { + cert, err := LoadCertFromFile(originFilname) + if err != nil { + return err + } + + return s.AddCert(cert) +} + +// GetCertificates returns an array with all of the current X509 Certificates. +func (s *X509MemStore) GetCertificates() []*x509.Certificate { + certs := make([]*x509.Certificate, len(s.fingerprintMap)) + i := 0 + for _, v := range s.fingerprintMap { + certs[i] = v + i++ + } + return certs +} + +// GetCertificatePool returns an x509 CertPool loaded with all the certificates +// in the store. +func (s *X509MemStore) GetCertificatePool() *x509.CertPool { + pool := x509.NewCertPool() + + for _, v := range s.fingerprintMap { + pool.AddCert(v) + } + return pool +} + +// GetCertificateByCertID returns the certificate that matches a certain certID +func (s *X509MemStore) GetCertificateByCertID(certID string) (*x509.Certificate, error) { + return s.getCertificateByCertID(CertID(certID)) +} + +// getCertificateByCertID returns the certificate that matches a certain certID or error +func (s *X509MemStore) getCertificateByCertID(certID CertID) (*x509.Certificate, error) { + // If it does not look like a hex encoded sha256 hash, error + if len(certID) != 64 { + return nil, errors.New("invalid Subject Key Identifier") + } + + // Check to see if this subject key identifier exists + if cert, ok := s.fingerprintMap[CertID(certID)]; ok { + return cert, nil + + } + return nil, &ErrNoCertificatesFound{query: string(certID)} +} + +// GetCertificatesByCN returns all the certificates that match a specific +// CommonName +func (s *X509MemStore) GetCertificatesByCN(cn string) ([]*x509.Certificate, error) { + var certs []*x509.Certificate + if ids, ok := s.nameMap[cn]; ok { + for _, v := range ids { + cert, err := s.getCertificateByCertID(v) + if err != nil { + // This error should never happen. This would mean that we have + // an inconsistent X509MemStore + return nil, err + } + certs = append(certs, cert) + } + } + if len(certs) == 0 { + return nil, &ErrNoCertificatesFound{query: cn} + } + + return certs, nil +} + +// GetVerifyOptions returns VerifyOptions with the certificates within the KeyStore +// as part of the roots list. This never allows the use of system roots, returning +// an error if there are no root CAs. +func (s *X509MemStore) GetVerifyOptions(dnsName string) (x509.VerifyOptions, error) { + // If we have no Certificates loaded return error (we don't want to rever to using + // system CAs). + if len(s.fingerprintMap) == 0 { + return x509.VerifyOptions{}, errors.New("no root CAs available") + } + + opts := x509.VerifyOptions{ + DNSName: dnsName, + Roots: s.GetCertificatePool(), + } + + return opts, nil +} diff --git a/vendor/src/github.com/docker/notary/trustmanager/x509store.go b/vendor/src/github.com/docker/notary/trustmanager/x509store.go new file mode 100644 index 00000000..3736ff63 --- /dev/null +++ b/vendor/src/github.com/docker/notary/trustmanager/x509store.go @@ -0,0 +1,144 @@ +package trustmanager + +import ( + "crypto/x509" + "errors" + "fmt" +) + +const certExtension string = "crt" + +// ErrNoCertificatesFound is returned when no certificates are found for a +// GetCertificatesBy* +type ErrNoCertificatesFound struct { + query string +} + +// ErrNoCertificatesFound is returned when no certificates are found for a +// GetCertificatesBy* +func (err ErrNoCertificatesFound) Error() string { + return fmt.Sprintf("error, no certificates found in the keystore match: %s", err.query) +} + +// ErrCertValidation is returned when a certificate doesn't pass the store specific +// validations +type ErrCertValidation struct { +} + +// ErrCertValidation is returned when a certificate doesn't pass the store specific +// validations +func (err ErrCertValidation) Error() string { + return fmt.Sprintf("store-specific certificate validations failed") +} + +// ErrCertExists is returned when a Certificate already exists in the key store +type ErrCertExists struct { +} + +// ErrCertExists is returned when a Certificate already exists in the key store +func (err ErrCertExists) Error() string { + return fmt.Sprintf("certificate already in the store") +} + +// ErrBadCertificateStore is returned when there is an internal inconsistency +// in our x509 store +type ErrBadCertificateStore struct { +} + +// ErrBadCertificateStore is returned when there is an internal inconsistency +// in our x509 store +func (err ErrBadCertificateStore) Error() string { + return fmt.Sprintf("inconsistent certificate store") +} + +// X509Store is the interface for all X509Stores +type X509Store interface { + AddCert(cert *x509.Certificate) error + AddCertFromPEM(pemCerts []byte) error + AddCertFromFile(filename string) error + RemoveCert(cert *x509.Certificate) error + RemoveAll() error + GetCertificateByCertID(certID string) (*x509.Certificate, error) + GetCertificatesByCN(cn string) ([]*x509.Certificate, error) + GetCertificates() []*x509.Certificate + GetCertificatePool() *x509.CertPool + GetVerifyOptions(dnsName string) (x509.VerifyOptions, error) +} + +// CertID represent the ID used to identify certificates +type CertID string + +// Validator is a convenience type to create validating function that filters +// certificates that get added to the store +type Validator interface { + Validate(cert *x509.Certificate) bool +} + +// ValidatorFunc is a convenience type to create functions that implement +// the Validator interface +type ValidatorFunc func(cert *x509.Certificate) bool + +// Validate implements the Validator interface to allow for any func() bool method +// to be passed as a Validator +func (vf ValidatorFunc) Validate(cert *x509.Certificate) bool { + return vf(cert) +} + +// Verify operates on an X509Store and validates the existence of a chain of trust +// between a leafCertificate and a CA present inside of the X509 Store. +// It requires at least two certificates in certList, a leaf Certificate and an +// intermediate CA certificate. +func Verify(s X509Store, dnsName string, certList []*x509.Certificate) error { + // If we have no Certificates loaded return error (we don't want to revert to using + // system CAs). + if len(s.GetCertificates()) == 0 { + return errors.New("no root CAs available") + } + + // At a minimum we should be provided a leaf cert and an intermediate. + if len(certList) < 2 { + return errors.New("certificate and at least one intermediate needed") + } + + // Get the VerifyOptions from the keystore for a base dnsName + opts, err := s.GetVerifyOptions(dnsName) + if err != nil { + return err + } + + // Create a Certificate Pool for our intermediate certificates + intPool := x509.NewCertPool() + var leafCert *x509.Certificate + + // Iterate through all the certificates + for _, c := range certList { + // If the cert is a CA, we add it to the intermediates pool. If not, we call + // it the leaf cert + if c.IsCA { + intPool.AddCert(c) + continue + } + // Certificate is not a CA, it must be our leaf certificate. + // If we already found one, bail with error + if leafCert != nil { + return errors.New("more than one leaf certificate found") + } + leafCert = c + } + + // We exited the loop with no leaf certificates + if leafCert == nil { + return errors.New("no leaf certificates found") + } + + // We have one leaf certificate and at least one intermediate. Lets add this + // Cert Pool as the Intermediates list on our VerifyOptions + opts.Intermediates = intPool + + // Finally, let's call Verify on our leafCert with our fully configured options + chains, err := leafCert.Verify(opts) + if len(chains) == 0 || err != nil { + return fmt.Errorf("certificate verification failed: %v", err) + } + return nil +} diff --git a/vendor/src/github.com/docker/notary/trustmanager/x509utils.go b/vendor/src/github.com/docker/notary/trustmanager/x509utils.go new file mode 100644 index 00000000..e2ce8add --- /dev/null +++ b/vendor/src/github.com/docker/notary/trustmanager/x509utils.go @@ -0,0 +1,613 @@ +package trustmanager + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "fmt" + "io" + "io/ioutil" + "math/big" + "net/http" + "net/url" + "time" + + "github.com/Sirupsen/logrus" + "github.com/agl/ed25519" + "github.com/docker/notary" + "github.com/docker/notary/tuf/data" +) + +// GetCertFromURL tries to get a X509 certificate given a HTTPS URL +func GetCertFromURL(urlStr string) (*x509.Certificate, error) { + url, err := url.Parse(urlStr) + if err != nil { + return nil, err + } + + // Check if we are adding via HTTPS + if url.Scheme != "https" { + return nil, errors.New("only HTTPS URLs allowed") + } + + // Download the certificate and write to directory + resp, err := http.Get(url.String()) + if err != nil { + return nil, err + } + + // Copy the content to certBytes + defer resp.Body.Close() + certBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + // Try to extract the first valid PEM certificate from the bytes + cert, err := LoadCertFromPEM(certBytes) + if err != nil { + return nil, err + } + + return cert, nil +} + +// CertToPEM is an utility function returns a PEM encoded x509 Certificate +func CertToPEM(cert *x509.Certificate) []byte { + pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) + + return pemCert +} + +// LoadCertFromPEM returns the first certificate found in a bunch of bytes or error +// if nothing is found. Taken from https://golang.org/src/crypto/x509/cert_pool.go#L85. +func LoadCertFromPEM(pemBytes []byte) (*x509.Certificate, error) { + for len(pemBytes) > 0 { + var block *pem.Block + block, pemBytes = pem.Decode(pemBytes) + if block == nil { + return nil, errors.New("no certificates found in PEM data") + } + if block.Type != "CERTIFICATE" || len(block.Headers) != 0 { + continue + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + continue + } + + return cert, nil + } + + return nil, errors.New("no certificates found in PEM data") +} + +// FingerprintCert returns a TUF compliant fingerprint for a X509 Certificate +func FingerprintCert(cert *x509.Certificate) (string, error) { + certID, err := fingerprintCert(cert) + if err != nil { + return "", err + } + + return string(certID), nil +} + +func fingerprintCert(cert *x509.Certificate) (CertID, error) { + block := pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw} + pemdata := pem.EncodeToMemory(&block) + + var tufKey data.PublicKey + switch cert.PublicKeyAlgorithm { + case x509.RSA: + tufKey = data.NewRSAx509PublicKey(pemdata) + case x509.ECDSA: + tufKey = data.NewECDSAx509PublicKey(pemdata) + default: + return "", fmt.Errorf("got Unknown key type while fingerprinting certificate") + } + + return CertID(tufKey.ID()), nil +} + +// loadCertsFromDir receives a store AddCertFromFile for each certificate found +func loadCertsFromDir(s *X509FileStore) error { + for _, f := range s.fileStore.ListFiles() { + // ListFiles returns relative paths + data, err := s.fileStore.Get(f) + if err != nil { + // the filestore told us it had a file that it then couldn't serve. + // this is a serious problem so error immediately + return err + } + err = s.AddCertFromPEM(data) + if err != nil { + if _, ok := err.(*ErrCertValidation); ok { + logrus.Debugf("ignoring certificate, did not pass validation: %s", f) + continue + } + if _, ok := err.(*ErrCertExists); ok { + logrus.Debugf("ignoring certificate, already exists in the store: %s", f) + continue + } + + return err + } + } + return nil +} + +// LoadCertFromFile loads the first certificate from the file provided. The +// data is expected to be PEM Encoded and contain one of more certificates +// with PEM type "CERTIFICATE" +func LoadCertFromFile(filename string) (*x509.Certificate, error) { + certs, err := LoadCertBundleFromFile(filename) + if err != nil { + return nil, err + } + return certs[0], nil +} + +// LoadCertBundleFromFile loads certificates from the []byte provided. The +// data is expected to be PEM Encoded and contain one of more certificates +// with PEM type "CERTIFICATE" +func LoadCertBundleFromFile(filename string) ([]*x509.Certificate, error) { + b, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + return LoadCertBundleFromPEM(b) +} + +// LoadCertBundleFromPEM loads certificates from the []byte provided. The +// data is expected to be PEM Encoded and contain one of more certificates +// with PEM type "CERTIFICATE" +func LoadCertBundleFromPEM(pemBytes []byte) ([]*x509.Certificate, error) { + certificates := []*x509.Certificate{} + var block *pem.Block + block, pemBytes = pem.Decode(pemBytes) + for ; block != nil; block, pemBytes = pem.Decode(pemBytes) { + if block.Type == "CERTIFICATE" { + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + certificates = append(certificates, cert) + } else { + return nil, fmt.Errorf("invalid pem block type: %s", block.Type) + } + } + + if len(certificates) == 0 { + return nil, fmt.Errorf("no valid certificates found") + } + + return certificates, nil +} + +// GetLeafCerts parses a list of x509 Certificates and returns all of them +// that aren't CA +func GetLeafCerts(certs []*x509.Certificate) []*x509.Certificate { + var leafCerts []*x509.Certificate + for _, cert := range certs { + if cert.IsCA { + continue + } + leafCerts = append(leafCerts, cert) + } + return leafCerts +} + +// GetIntermediateCerts parses a list of x509 Certificates and returns all of the +// ones marked as a CA, to be used as intermediates +func GetIntermediateCerts(certs []*x509.Certificate) []*x509.Certificate { + var intCerts []*x509.Certificate + for _, cert := range certs { + if cert.IsCA { + intCerts = append(intCerts, cert) + } + } + return intCerts +} + +// ParsePEMPrivateKey returns a data.PrivateKey from a PEM encoded private key. It +// only supports RSA (PKCS#1) and attempts to decrypt using the passphrase, if encrypted. +func ParsePEMPrivateKey(pemBytes []byte, passphrase string) (data.PrivateKey, error) { + block, _ := pem.Decode(pemBytes) + if block == nil { + return nil, errors.New("no valid private key found") + } + + switch block.Type { + case "RSA PRIVATE KEY": + var privKeyBytes []byte + var err error + + if x509.IsEncryptedPEMBlock(block) { + privKeyBytes, err = x509.DecryptPEMBlock(block, []byte(passphrase)) + if err != nil { + return nil, errors.New("could not decrypt private key") + } + } else { + privKeyBytes = block.Bytes + } + + rsaPrivKey, err := x509.ParsePKCS1PrivateKey(privKeyBytes) + if err != nil { + return nil, fmt.Errorf("could not parse DER encoded key: %v", err) + } + + tufRSAPrivateKey, err := RSAToPrivateKey(rsaPrivKey) + if err != nil { + return nil, fmt.Errorf("could not convert rsa.PrivateKey to data.PrivateKey: %v", err) + } + + return tufRSAPrivateKey, nil + case "EC PRIVATE KEY": + var privKeyBytes []byte + var err error + + if x509.IsEncryptedPEMBlock(block) { + privKeyBytes, err = x509.DecryptPEMBlock(block, []byte(passphrase)) + if err != nil { + return nil, errors.New("could not decrypt private key") + } + } else { + privKeyBytes = block.Bytes + } + + ecdsaPrivKey, err := x509.ParseECPrivateKey(privKeyBytes) + if err != nil { + return nil, fmt.Errorf("could not parse DER encoded private key: %v", err) + } + + tufECDSAPrivateKey, err := ECDSAToPrivateKey(ecdsaPrivKey) + if err != nil { + return nil, fmt.Errorf("could not convert ecdsa.PrivateKey to data.PrivateKey: %v", err) + } + + return tufECDSAPrivateKey, nil + case "ED25519 PRIVATE KEY": + // We serialize ED25519 keys by concatenating the private key + // to the public key and encoding with PEM. See the + // ED25519ToPrivateKey function. + var privKeyBytes []byte + var err error + + if x509.IsEncryptedPEMBlock(block) { + privKeyBytes, err = x509.DecryptPEMBlock(block, []byte(passphrase)) + if err != nil { + return nil, errors.New("could not decrypt private key") + } + } else { + privKeyBytes = block.Bytes + } + + tufECDSAPrivateKey, err := ED25519ToPrivateKey(privKeyBytes) + if err != nil { + return nil, fmt.Errorf("could not convert ecdsa.PrivateKey to data.PrivateKey: %v", err) + } + + return tufECDSAPrivateKey, nil + + default: + return nil, fmt.Errorf("unsupported key type %q", block.Type) + } +} + +// ParsePEMPublicKey returns a data.PublicKey from a PEM encoded public key or certificate. +func ParsePEMPublicKey(pubKeyBytes []byte) (data.PublicKey, error) { + pemBlock, _ := pem.Decode(pubKeyBytes) + if pemBlock == nil { + return nil, errors.New("no valid public key found") + } + + switch pemBlock.Type { + case "CERTIFICATE": + cert, err := x509.ParseCertificate(pemBlock.Bytes) + if err != nil { + return nil, fmt.Errorf("could not parse provided certificate: %v", err) + } + err = ValidateCertificate(cert) + if err != nil { + return nil, fmt.Errorf("invalid certificate: %v", err) + } + return CertToKey(cert), nil + default: + return nil, fmt.Errorf("unsupported PEM block type %q, expected certificate", pemBlock.Type) + } +} + +// ValidateCertificate returns an error if the certificate is not valid for notary +// Currently this is only a time expiry check, and ensuring the public key has a large enough modulus if RSA +func ValidateCertificate(c *x509.Certificate) error { + if (c.NotBefore).After(c.NotAfter) { + return fmt.Errorf("certificate validity window is invalid") + } + now := time.Now() + tomorrow := now.AddDate(0, 0, 1) + // Give one day leeway on creation "before" time, check "after" against today + if (tomorrow).Before(c.NotBefore) || now.After(c.NotAfter) { + return fmt.Errorf("certificate is expired") + } + // If we have an RSA key, make sure it's long enough + if c.PublicKeyAlgorithm == x509.RSA { + rsaKey, ok := c.PublicKey.(*rsa.PublicKey) + if !ok { + return fmt.Errorf("unable to parse RSA public key") + } + if rsaKey.N.BitLen() < notary.MinRSABitSize { + return fmt.Errorf("RSA bit length is too short") + } + } + return nil +} + +// GenerateRSAKey generates an RSA private key and returns a TUF PrivateKey +func GenerateRSAKey(random io.Reader, bits int) (data.PrivateKey, error) { + rsaPrivKey, err := rsa.GenerateKey(random, bits) + if err != nil { + return nil, fmt.Errorf("could not generate private key: %v", err) + } + + tufPrivKey, err := RSAToPrivateKey(rsaPrivKey) + if err != nil { + return nil, err + } + + logrus.Debugf("generated RSA key with keyID: %s", tufPrivKey.ID()) + + return tufPrivKey, nil +} + +// RSAToPrivateKey converts an rsa.Private key to a TUF data.PrivateKey type +func RSAToPrivateKey(rsaPrivKey *rsa.PrivateKey) (data.PrivateKey, error) { + // Get a DER-encoded representation of the PublicKey + rsaPubBytes, err := x509.MarshalPKIXPublicKey(&rsaPrivKey.PublicKey) + if err != nil { + return nil, fmt.Errorf("failed to marshal public key: %v", err) + } + + // Get a DER-encoded representation of the PrivateKey + rsaPrivBytes := x509.MarshalPKCS1PrivateKey(rsaPrivKey) + + pubKey := data.NewRSAPublicKey(rsaPubBytes) + return data.NewRSAPrivateKey(pubKey, rsaPrivBytes) +} + +// GenerateECDSAKey generates an ECDSA Private key and returns a TUF PrivateKey +func GenerateECDSAKey(random io.Reader) (data.PrivateKey, error) { + ecdsaPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), random) + if err != nil { + return nil, err + } + + tufPrivKey, err := ECDSAToPrivateKey(ecdsaPrivKey) + if err != nil { + return nil, err + } + + logrus.Debugf("generated ECDSA key with keyID: %s", tufPrivKey.ID()) + + return tufPrivKey, nil +} + +// GenerateED25519Key generates an ED25519 private key and returns a TUF +// PrivateKey. The serialization format we use is just the public key bytes +// followed by the private key bytes +func GenerateED25519Key(random io.Reader) (data.PrivateKey, error) { + pub, priv, err := ed25519.GenerateKey(random) + if err != nil { + return nil, err + } + + var serialized [ed25519.PublicKeySize + ed25519.PrivateKeySize]byte + copy(serialized[:], pub[:]) + copy(serialized[ed25519.PublicKeySize:], priv[:]) + + tufPrivKey, err := ED25519ToPrivateKey(serialized[:]) + if err != nil { + return nil, err + } + + logrus.Debugf("generated ED25519 key with keyID: %s", tufPrivKey.ID()) + + return tufPrivKey, nil +} + +// ECDSAToPrivateKey converts an ecdsa.Private key to a TUF data.PrivateKey type +func ECDSAToPrivateKey(ecdsaPrivKey *ecdsa.PrivateKey) (data.PrivateKey, error) { + // Get a DER-encoded representation of the PublicKey + ecdsaPubBytes, err := x509.MarshalPKIXPublicKey(&ecdsaPrivKey.PublicKey) + if err != nil { + return nil, fmt.Errorf("failed to marshal public key: %v", err) + } + + // Get a DER-encoded representation of the PrivateKey + ecdsaPrivKeyBytes, err := x509.MarshalECPrivateKey(ecdsaPrivKey) + if err != nil { + return nil, fmt.Errorf("failed to marshal private key: %v", err) + } + + pubKey := data.NewECDSAPublicKey(ecdsaPubBytes) + return data.NewECDSAPrivateKey(pubKey, ecdsaPrivKeyBytes) +} + +// ED25519ToPrivateKey converts a serialized ED25519 key to a TUF +// data.PrivateKey type +func ED25519ToPrivateKey(privKeyBytes []byte) (data.PrivateKey, error) { + if len(privKeyBytes) != ed25519.PublicKeySize+ed25519.PrivateKeySize { + return nil, errors.New("malformed ed25519 private key") + } + + pubKey := data.NewED25519PublicKey(privKeyBytes[:ed25519.PublicKeySize]) + return data.NewED25519PrivateKey(*pubKey, privKeyBytes) +} + +func blockType(k data.PrivateKey) (string, error) { + switch k.Algorithm() { + case data.RSAKey, data.RSAx509Key: + return "RSA PRIVATE KEY", nil + case data.ECDSAKey, data.ECDSAx509Key: + return "EC PRIVATE KEY", nil + case data.ED25519Key: + return "ED25519 PRIVATE KEY", nil + default: + return "", fmt.Errorf("algorithm %s not supported", k.Algorithm()) + } +} + +// KeyToPEM returns a PEM encoded key from a Private Key +func KeyToPEM(privKey data.PrivateKey, role string) ([]byte, error) { + bt, err := blockType(privKey) + if err != nil { + return nil, err + } + + headers := map[string]string{} + if role != "" { + headers = map[string]string{ + "role": role, + } + } + + block := &pem.Block{ + Type: bt, + Headers: headers, + Bytes: privKey.Private(), + } + + return pem.EncodeToMemory(block), nil +} + +// EncryptPrivateKey returns an encrypted PEM key given a Privatekey +// and a passphrase +func EncryptPrivateKey(key data.PrivateKey, role, passphrase string) ([]byte, error) { + bt, err := blockType(key) + if err != nil { + return nil, err + } + + password := []byte(passphrase) + cipherType := x509.PEMCipherAES256 + + encryptedPEMBlock, err := x509.EncryptPEMBlock(rand.Reader, + bt, + key.Private(), + password, + cipherType) + if err != nil { + return nil, err + } + + if encryptedPEMBlock.Headers == nil { + return nil, fmt.Errorf("unable to encrypt key - invalid PEM file produced") + } + encryptedPEMBlock.Headers["role"] = role + + return pem.EncodeToMemory(encryptedPEMBlock), nil +} + +// ReadRoleFromPEM returns the value from the role PEM header, if it exists +func ReadRoleFromPEM(pemBytes []byte) string { + pemBlock, _ := pem.Decode(pemBytes) + if pemBlock == nil || pemBlock.Headers == nil { + return "" + } + role, ok := pemBlock.Headers["role"] + if !ok { + return "" + } + return role +} + +// CertToKey transforms a single input certificate into its corresponding +// PublicKey +func CertToKey(cert *x509.Certificate) data.PublicKey { + block := pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw} + pemdata := pem.EncodeToMemory(&block) + + switch cert.PublicKeyAlgorithm { + case x509.RSA: + return data.NewRSAx509PublicKey(pemdata) + case x509.ECDSA: + return data.NewECDSAx509PublicKey(pemdata) + default: + logrus.Debugf("Unknown key type parsed from certificate: %v", cert.PublicKeyAlgorithm) + return nil + } +} + +// CertsToKeys transforms each of the input certificates into it's corresponding +// PublicKey +func CertsToKeys(certs []*x509.Certificate) map[string]data.PublicKey { + keys := make(map[string]data.PublicKey) + for _, cert := range certs { + newKey := CertToKey(cert) + keys[newKey.ID()] = newKey + } + return keys +} + +// NewCertificate returns an X509 Certificate following a template, given a GUN and validity interval. +func NewCertificate(gun string, startTime, endTime time.Time) (*x509.Certificate, error) { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, fmt.Errorf("failed to generate new certificate: %v", err) + } + + return &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: gun, + }, + NotBefore: startTime, + NotAfter: endTime, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + BasicConstraintsValid: true, + }, nil +} + +// X509PublicKeyID returns a public key ID as a string, given a +// data.PublicKey that contains an X509 Certificate +func X509PublicKeyID(certPubKey data.PublicKey) (string, error) { + cert, err := LoadCertFromPEM(certPubKey.Public()) + if err != nil { + return "", err + } + pubKeyBytes, err := x509.MarshalPKIXPublicKey(cert.PublicKey) + if err != nil { + return "", err + } + + var key data.PublicKey + switch certPubKey.Algorithm() { + case data.ECDSAx509Key: + key = data.NewECDSAPublicKey(pubKeyBytes) + case data.RSAx509Key: + key = data.NewRSAPublicKey(pubKeyBytes) + } + + return key.ID(), nil +} + +// FilterCertsExpiredSha1 can be used as the filter function to cert store +// initializers to filter out all expired or SHA-1 certificate that we +// shouldn't load. +func FilterCertsExpiredSha1(cert *x509.Certificate) bool { + return !cert.IsCA && + time.Now().Before(cert.NotAfter) && + cert.SignatureAlgorithm != x509.SHA1WithRSA && + cert.SignatureAlgorithm != x509.DSAWithSHA1 && + cert.SignatureAlgorithm != x509.ECDSAWithSHA1 +} diff --git a/vendor/src/github.com/docker/notary/trustmanager/yubikey/non_pkcs11.go b/vendor/src/github.com/docker/notary/trustmanager/yubikey/non_pkcs11.go new file mode 100644 index 00000000..77c07e6f --- /dev/null +++ b/vendor/src/github.com/docker/notary/trustmanager/yubikey/non_pkcs11.go @@ -0,0 +1,9 @@ +// go list ./... and go test ./... will not pick up this package without this +// file, because go ? ./... does not honor build tags. + +// e.g. "go list -tags pkcs11 ./..." will not list this package if all the +// files in it have a build tag. + +// See https://github.com/golang/go/issues/11246 + +package yubikey diff --git a/vendor/src/github.com/docker/notary/trustmanager/yubikey/pkcs11_darwin.go b/vendor/src/github.com/docker/notary/trustmanager/yubikey/pkcs11_darwin.go new file mode 100644 index 00000000..f399dff3 --- /dev/null +++ b/vendor/src/github.com/docker/notary/trustmanager/yubikey/pkcs11_darwin.go @@ -0,0 +1,9 @@ +// +build pkcs11,darwin + +package yubikey + +var possiblePkcs11Libs = []string{ + "/usr/local/lib/libykcs11.dylib", + "/usr/local/docker/lib/libykcs11.dylib", + "/usr/local/docker-experimental/lib/libykcs11.dylib", +} diff --git a/vendor/src/github.com/docker/notary/trustmanager/yubikey/pkcs11_interface.go b/vendor/src/github.com/docker/notary/trustmanager/yubikey/pkcs11_interface.go new file mode 100644 index 00000000..4a46e05f --- /dev/null +++ b/vendor/src/github.com/docker/notary/trustmanager/yubikey/pkcs11_interface.go @@ -0,0 +1,40 @@ +// +build pkcs11 + +// an interface around the pkcs11 library, so that things can be mocked out +// for testing + +package yubikey + +import "github.com/miekg/pkcs11" + +// IPKCS11 is an interface for wrapping github.com/miekg/pkcs11 +type pkcs11LibLoader func(module string) IPKCS11Ctx + +func defaultLoader(module string) IPKCS11Ctx { + return pkcs11.New(module) +} + +// IPKCS11Ctx is an interface for wrapping the parts of +// github.com/miekg/pkcs11.Ctx that yubikeystore requires +type IPKCS11Ctx interface { + Destroy() + Initialize() error + Finalize() error + GetSlotList(tokenPresent bool) ([]uint, error) + OpenSession(slotID uint, flags uint) (pkcs11.SessionHandle, error) + CloseSession(sh pkcs11.SessionHandle) error + Login(sh pkcs11.SessionHandle, userType uint, pin string) error + Logout(sh pkcs11.SessionHandle) error + CreateObject(sh pkcs11.SessionHandle, temp []*pkcs11.Attribute) ( + pkcs11.ObjectHandle, error) + DestroyObject(sh pkcs11.SessionHandle, oh pkcs11.ObjectHandle) error + GetAttributeValue(sh pkcs11.SessionHandle, o pkcs11.ObjectHandle, + a []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) + FindObjectsInit(sh pkcs11.SessionHandle, temp []*pkcs11.Attribute) error + FindObjects(sh pkcs11.SessionHandle, max int) ( + []pkcs11.ObjectHandle, bool, error) + FindObjectsFinal(sh pkcs11.SessionHandle) error + SignInit(sh pkcs11.SessionHandle, m []*pkcs11.Mechanism, + o pkcs11.ObjectHandle) error + Sign(sh pkcs11.SessionHandle, message []byte) ([]byte, error) +} diff --git a/vendor/src/github.com/docker/notary/trustmanager/yubikey/pkcs11_linux.go b/vendor/src/github.com/docker/notary/trustmanager/yubikey/pkcs11_linux.go new file mode 100644 index 00000000..9967e89e --- /dev/null +++ b/vendor/src/github.com/docker/notary/trustmanager/yubikey/pkcs11_linux.go @@ -0,0 +1,10 @@ +// +build pkcs11,linux + +package yubikey + +var possiblePkcs11Libs = []string{ + "/usr/lib/libykcs11.so", + "/usr/lib64/libykcs11.so", + "/usr/lib/x86_64-linux-gnu/libykcs11.so", + "/usr/local/lib/libykcs11.so", +} diff --git a/vendor/src/github.com/docker/notary/trustmanager/yubikey/yubikeystore.go b/vendor/src/github.com/docker/notary/trustmanager/yubikey/yubikeystore.go new file mode 100644 index 00000000..e1653aa6 --- /dev/null +++ b/vendor/src/github.com/docker/notary/trustmanager/yubikey/yubikeystore.go @@ -0,0 +1,888 @@ +// +build pkcs11 + +package yubikey + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "errors" + "fmt" + "io" + "math/big" + "os" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/notary/passphrase" + "github.com/docker/notary/trustmanager" + "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/signed" + "github.com/miekg/pkcs11" +) + +const ( + USER_PIN = "123456" + SO_USER_PIN = "010203040506070801020304050607080102030405060708" + numSlots = 4 // number of slots in the yubikey + + KeymodeNone = 0 + KeymodeTouch = 1 // touch enabled + KeymodePinOnce = 2 // require pin entry once + KeymodePinAlways = 4 // require pin entry all the time + + // the key size, when importing a key into yubikey, MUST be 32 bytes + ecdsaPrivateKeySize = 32 + + sigAttempts = 5 +) + +// what key mode to use when generating keys +var ( + yubikeyKeymode = KeymodeTouch | KeymodePinOnce + // order in which to prefer token locations on the yubikey. + // corresponds to: 9c, 9e, 9d, 9a + slotIDs = []int{2, 1, 3, 0} +) + +// SetYubikeyKeyMode - sets the mode when generating yubikey keys. +// This is to be used for testing. It does nothing if not building with tag +// pkcs11. +func SetYubikeyKeyMode(keyMode int) error { + // technically 7 (1 | 2 | 4) is valid, but KeymodePinOnce + + // KeymdoePinAlways don't really make sense together + if keyMode < 0 || keyMode > 5 { + return errors.New("Invalid key mode") + } + yubikeyKeymode = keyMode + return nil +} + +// SetTouchToSignUI - allows configurable UX for notifying a user that they +// need to touch the yubikey to sign. The callback may be used to provide a +// mechanism for updating a GUI (such as removing a modal) after the touch +// has been made +func SetTouchToSignUI(notifier func(), callback func()) { + touchToSignUI = notifier + if callback != nil { + touchDoneCallback = callback + } +} + +var touchToSignUI = func() { + fmt.Println("Please touch the attached Yubikey to perform signing.") +} + +var touchDoneCallback = func() { + // noop +} + +var pkcs11Lib string + +func init() { + for _, loc := range possiblePkcs11Libs { + _, err := os.Stat(loc) + if err == nil { + p := pkcs11.New(loc) + if p != nil { + pkcs11Lib = loc + return + } + } + } +} + +type ErrBackupFailed struct { + err string +} + +func (err ErrBackupFailed) Error() string { + return fmt.Sprintf("Failed to backup private key to: %s", err.err) +} + +type yubiSlot struct { + role string + slotID []byte +} + +// YubiPrivateKey represents a private key inside of a yubikey +type YubiPrivateKey struct { + data.ECDSAPublicKey + passRetriever passphrase.Retriever + slot []byte + libLoader pkcs11LibLoader +} + +type YubikeySigner struct { + YubiPrivateKey +} + +func NewYubiPrivateKey(slot []byte, pubKey data.ECDSAPublicKey, + passRetriever passphrase.Retriever) *YubiPrivateKey { + + return &YubiPrivateKey{ + ECDSAPublicKey: pubKey, + passRetriever: passRetriever, + slot: slot, + libLoader: defaultLoader, + } +} + +func (ys *YubikeySigner) Public() crypto.PublicKey { + publicKey, err := x509.ParsePKIXPublicKey(ys.YubiPrivateKey.Public()) + if err != nil { + return nil + } + + return publicKey +} + +func (y *YubiPrivateKey) setLibLoader(loader pkcs11LibLoader) { + y.libLoader = loader +} + +// CryptoSigner returns a crypto.Signer tha wraps the YubiPrivateKey. Needed for +// Certificate generation only +func (y *YubiPrivateKey) CryptoSigner() crypto.Signer { + return &YubikeySigner{YubiPrivateKey: *y} +} + +// Private is not implemented in hardware keys +func (y *YubiPrivateKey) Private() []byte { + // We cannot return the private material from a Yubikey + // TODO(david): We probably want to return an error here + return nil +} + +func (y YubiPrivateKey) SignatureAlgorithm() data.SigAlgorithm { + return data.ECDSASignature +} + +func (y *YubiPrivateKey) Sign(rand io.Reader, msg []byte, opts crypto.SignerOpts) ([]byte, error) { + ctx, session, err := SetupHSMEnv(pkcs11Lib, y.libLoader) + if err != nil { + return nil, err + } + defer cleanup(ctx, session) + + v := signed.Verifiers[data.ECDSASignature] + for i := 0; i < sigAttempts; i++ { + sig, err := sign(ctx, session, y.slot, y.passRetriever, msg) + if err != nil { + return nil, fmt.Errorf("failed to sign using Yubikey: %v", err) + } + if err := v.Verify(&y.ECDSAPublicKey, sig, msg); err == nil { + return sig, nil + } + } + return nil, errors.New("Failed to generate signature on Yubikey.") +} + +// If a byte array is less than the number of bytes specified by +// ecdsaPrivateKeySize, left-zero-pad the byte array until +// it is the required size. +func ensurePrivateKeySize(payload []byte) []byte { + final := payload + if len(payload) < ecdsaPrivateKeySize { + final = make([]byte, ecdsaPrivateKeySize) + copy(final[ecdsaPrivateKeySize-len(payload):], payload) + } + return final +} + +// addECDSAKey adds a key to the yubikey +func addECDSAKey( + ctx IPKCS11Ctx, + session pkcs11.SessionHandle, + privKey data.PrivateKey, + pkcs11KeyID []byte, + passRetriever passphrase.Retriever, + role string, +) error { + logrus.Debugf("Attempting to add key to yubikey with ID: %s", privKey.ID()) + + err := login(ctx, session, passRetriever, pkcs11.CKU_SO, SO_USER_PIN) + if err != nil { + return err + } + defer ctx.Logout(session) + + // Create an ecdsa.PrivateKey out of the private key bytes + ecdsaPrivKey, err := x509.ParseECPrivateKey(privKey.Private()) + if err != nil { + return err + } + + ecdsaPrivKeyD := ensurePrivateKeySize(ecdsaPrivKey.D.Bytes()) + + // Hard-coded policy: the generated certificate expires in 10 years. + startTime := time.Now() + template, err := trustmanager.NewCertificate(role, startTime, startTime.AddDate(10, 0, 0)) + if err != nil { + return fmt.Errorf("failed to create the certificate template: %v", err) + } + + certBytes, err := x509.CreateCertificate(rand.Reader, template, template, ecdsaPrivKey.Public(), ecdsaPrivKey) + if err != nil { + return fmt.Errorf("failed to create the certificate: %v", err) + } + + certTemplate := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_CERTIFICATE), + pkcs11.NewAttribute(pkcs11.CKA_VALUE, certBytes), + pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID), + } + + privateKeyTemplate := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PRIVATE_KEY), + pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_ECDSA), + pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID), + pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07}), + pkcs11.NewAttribute(pkcs11.CKA_VALUE, ecdsaPrivKeyD), + pkcs11.NewAttribute(pkcs11.CKA_VENDOR_DEFINED, yubikeyKeymode), + } + + _, err = ctx.CreateObject(session, certTemplate) + if err != nil { + return fmt.Errorf("error importing: %v", err) + } + + _, err = ctx.CreateObject(session, privateKeyTemplate) + if err != nil { + return fmt.Errorf("error importing: %v", err) + } + + return nil +} + +func getECDSAKey(ctx IPKCS11Ctx, session pkcs11.SessionHandle, pkcs11KeyID []byte) (*data.ECDSAPublicKey, string, error) { + findTemplate := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true), + pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID), + pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PUBLIC_KEY), + } + + attrTemplate := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, []byte{0}), + pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, []byte{0}), + pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{0}), + } + + if err := ctx.FindObjectsInit(session, findTemplate); err != nil { + logrus.Debugf("Failed to init: %s", err.Error()) + return nil, "", err + } + obj, _, err := ctx.FindObjects(session, 1) + if err != nil { + logrus.Debugf("Failed to find objects: %v", err) + return nil, "", err + } + if err := ctx.FindObjectsFinal(session); err != nil { + logrus.Debugf("Failed to finalize: %s", err.Error()) + return nil, "", err + } + if len(obj) != 1 { + logrus.Debugf("should have found one object") + return nil, "", errors.New("no matching keys found inside of yubikey") + } + + // Retrieve the public-key material to be able to create a new ECSAKey + attr, err := ctx.GetAttributeValue(session, obj[0], attrTemplate) + if err != nil { + logrus.Debugf("Failed to get Attribute for: %v", obj[0]) + return nil, "", err + } + + // Iterate through all the attributes of this key and saves CKA_PUBLIC_EXPONENT and CKA_MODULUS. Removes ordering specific issues. + var rawPubKey []byte + for _, a := range attr { + if a.Type == pkcs11.CKA_EC_POINT { + rawPubKey = a.Value + } + + } + + ecdsaPubKey := ecdsa.PublicKey{Curve: elliptic.P256(), X: new(big.Int).SetBytes(rawPubKey[3:35]), Y: new(big.Int).SetBytes(rawPubKey[35:])} + pubBytes, err := x509.MarshalPKIXPublicKey(&ecdsaPubKey) + if err != nil { + logrus.Debugf("Failed to Marshal public key") + return nil, "", err + } + + return data.NewECDSAPublicKey(pubBytes), data.CanonicalRootRole, nil +} + +// Sign returns a signature for a given signature request +func sign(ctx IPKCS11Ctx, session pkcs11.SessionHandle, pkcs11KeyID []byte, passRetriever passphrase.Retriever, payload []byte) ([]byte, error) { + err := login(ctx, session, passRetriever, pkcs11.CKU_USER, USER_PIN) + if err != nil { + return nil, fmt.Errorf("error logging in: %v", err) + } + defer ctx.Logout(session) + + // Define the ECDSA Private key template + class := pkcs11.CKO_PRIVATE_KEY + privateKeyTemplate := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_CLASS, class), + pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_ECDSA), + pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID), + } + + if err := ctx.FindObjectsInit(session, privateKeyTemplate); err != nil { + logrus.Debugf("Failed to init find objects: %s", err.Error()) + return nil, err + } + obj, _, err := ctx.FindObjects(session, 1) + if err != nil { + logrus.Debugf("Failed to find objects: %v", err) + return nil, err + } + if err = ctx.FindObjectsFinal(session); err != nil { + logrus.Debugf("Failed to finalize find objects: %s", err.Error()) + return nil, err + } + if len(obj) != 1 { + return nil, errors.New("length of objects found not 1") + } + + var sig []byte + err = ctx.SignInit( + session, []*pkcs11.Mechanism{pkcs11.NewMechanism(pkcs11.CKM_ECDSA, nil)}, obj[0]) + if err != nil { + return nil, err + } + + // Get the SHA256 of the payload + digest := sha256.Sum256(payload) + + if (yubikeyKeymode & KeymodeTouch) > 0 { + touchToSignUI() + defer touchDoneCallback() + } + // a call to Sign, whether or not Sign fails, will clear the SignInit + sig, err = ctx.Sign(session, digest[:]) + if err != nil { + logrus.Debugf("Error while signing: %s", err) + return nil, err + } + + if sig == nil { + return nil, errors.New("Failed to create signature") + } + return sig[:], nil +} + +func yubiRemoveKey(ctx IPKCS11Ctx, session pkcs11.SessionHandle, pkcs11KeyID []byte, passRetriever passphrase.Retriever, keyID string) error { + err := login(ctx, session, passRetriever, pkcs11.CKU_SO, SO_USER_PIN) + if err != nil { + return err + } + defer ctx.Logout(session) + + template := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true), + pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID), + //pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PRIVATE_KEY), + pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_CERTIFICATE), + } + + if err := ctx.FindObjectsInit(session, template); err != nil { + logrus.Debugf("Failed to init find objects: %s", err.Error()) + return err + } + obj, b, err := ctx.FindObjects(session, 1) + if err != nil { + logrus.Debugf("Failed to find objects: %s %v", err.Error(), b) + return err + } + if err := ctx.FindObjectsFinal(session); err != nil { + logrus.Debugf("Failed to finalize find objects: %s", err.Error()) + return err + } + if len(obj) != 1 { + logrus.Debugf("should have found exactly one object") + return err + } + + // Delete the certificate + err = ctx.DestroyObject(session, obj[0]) + if err != nil { + logrus.Debugf("Failed to delete cert") + return err + } + return nil +} + +func yubiListKeys(ctx IPKCS11Ctx, session pkcs11.SessionHandle) (keys map[string]yubiSlot, err error) { + keys = make(map[string]yubiSlot) + findTemplate := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true), + //pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID), + pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_CERTIFICATE), + } + + attrTemplate := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_ID, []byte{0}), + pkcs11.NewAttribute(pkcs11.CKA_VALUE, []byte{0}), + } + + if err = ctx.FindObjectsInit(session, findTemplate); err != nil { + logrus.Debugf("Failed to init: %s", err.Error()) + return + } + objs, b, err := ctx.FindObjects(session, numSlots) + for err == nil { + var o []pkcs11.ObjectHandle + o, b, err = ctx.FindObjects(session, numSlots) + if err != nil { + continue + } + if len(o) == 0 { + break + } + objs = append(objs, o...) + } + if err != nil { + logrus.Debugf("Failed to find: %s %v", err.Error(), b) + if len(objs) == 0 { + return nil, err + } + } + if err = ctx.FindObjectsFinal(session); err != nil { + logrus.Debugf("Failed to finalize: %s", err.Error()) + return + } + if len(objs) == 0 { + return nil, errors.New("No keys found in yubikey.") + } + logrus.Debugf("Found %d objects matching list filters", len(objs)) + for _, obj := range objs { + var ( + cert *x509.Certificate + slot []byte + ) + // Retrieve the public-key material to be able to create a new ECDSA + attr, err := ctx.GetAttributeValue(session, obj, attrTemplate) + if err != nil { + logrus.Debugf("Failed to get Attribute for: %v", obj) + continue + } + + // Iterate through all the attributes of this key and saves CKA_PUBLIC_EXPONENT and CKA_MODULUS. Removes ordering specific issues. + for _, a := range attr { + if a.Type == pkcs11.CKA_ID { + slot = a.Value + } + if a.Type == pkcs11.CKA_VALUE { + cert, err = x509.ParseCertificate(a.Value) + if err != nil { + continue + } + if !data.ValidRole(cert.Subject.CommonName) { + continue + } + } + } + + // we found nothing + if cert == nil { + continue + } + + var ecdsaPubKey *ecdsa.PublicKey + switch cert.PublicKeyAlgorithm { + case x509.ECDSA: + ecdsaPubKey = cert.PublicKey.(*ecdsa.PublicKey) + default: + logrus.Infof("Unsupported x509 PublicKeyAlgorithm: %d", cert.PublicKeyAlgorithm) + continue + } + + pubBytes, err := x509.MarshalPKIXPublicKey(ecdsaPubKey) + if err != nil { + logrus.Debugf("Failed to Marshal public key") + continue + } + + keys[data.NewECDSAPublicKey(pubBytes).ID()] = yubiSlot{ + role: cert.Subject.CommonName, + slotID: slot, + } + } + return +} + +func getNextEmptySlot(ctx IPKCS11Ctx, session pkcs11.SessionHandle) ([]byte, error) { + findTemplate := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true), + } + attrTemplate := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_ID, []byte{0}), + } + + if err := ctx.FindObjectsInit(session, findTemplate); err != nil { + logrus.Debugf("Failed to init: %s", err.Error()) + return nil, err + } + objs, b, err := ctx.FindObjects(session, numSlots) + // if there are more objects than `numSlots`, get all of them until + // there are no more to get + for err == nil { + var o []pkcs11.ObjectHandle + o, b, err = ctx.FindObjects(session, numSlots) + if err != nil { + continue + } + if len(o) == 0 { + break + } + objs = append(objs, o...) + } + taken := make(map[int]bool) + if err != nil { + logrus.Debugf("Failed to find: %s %v", err.Error(), b) + return nil, err + } + if err = ctx.FindObjectsFinal(session); err != nil { + logrus.Debugf("Failed to finalize: %s\n", err.Error()) + return nil, err + } + for _, obj := range objs { + // Retrieve the slot ID + attr, err := ctx.GetAttributeValue(session, obj, attrTemplate) + if err != nil { + continue + } + + // Iterate through attributes. If an ID attr was found, mark it as taken + for _, a := range attr { + if a.Type == pkcs11.CKA_ID { + if len(a.Value) < 1 { + continue + } + // a byte will always be capable of representing all slot IDs + // for the Yubikeys + slotNum := int(a.Value[0]) + if slotNum >= numSlots { + // defensive + continue + } + taken[slotNum] = true + } + } + } + // iterate the token locations in our preferred order and use the first + // available one. Otherwise exit the loop and return an error. + for _, loc := range slotIDs { + if !taken[loc] { + return []byte{byte(loc)}, nil + } + } + return nil, errors.New("Yubikey has no available slots.") +} + +// YubiKeyStore is a KeyStore for private keys inside a Yubikey +type YubiKeyStore struct { + passRetriever passphrase.Retriever + keys map[string]yubiSlot + backupStore trustmanager.KeyStore + libLoader pkcs11LibLoader +} + +// NewYubiKeyStore returns a YubiKeyStore, given a backup key store to write any +// generated keys to (usually a KeyFileStore) +func NewYubiKeyStore(backupStore trustmanager.KeyStore, passphraseRetriever passphrase.Retriever) ( + *YubiKeyStore, error) { + + s := &YubiKeyStore{ + passRetriever: passphraseRetriever, + keys: make(map[string]yubiSlot), + backupStore: backupStore, + libLoader: defaultLoader, + } + s.ListKeys() // populate keys field + return s, nil +} + +// Name returns a user friendly name for the location this store +// keeps its data +func (s YubiKeyStore) Name() string { + return "yubikey" +} + +func (s *YubiKeyStore) setLibLoader(loader pkcs11LibLoader) { + s.libLoader = loader +} + +func (s *YubiKeyStore) ListKeys() map[string]trustmanager.KeyInfo { + if len(s.keys) > 0 { + return buildKeyMap(s.keys) + } + ctx, session, err := SetupHSMEnv(pkcs11Lib, s.libLoader) + if err != nil { + logrus.Debugf("Failed to initialize PKCS11 environment: %s", err.Error()) + return nil + } + defer cleanup(ctx, session) + + keys, err := yubiListKeys(ctx, session) + if err != nil { + logrus.Debugf("Failed to list key from the yubikey: %s", err.Error()) + return nil + } + s.keys = keys + + return buildKeyMap(keys) +} + +// AddKey puts a key inside the Yubikey, as well as writing it to the backup store +func (s *YubiKeyStore) AddKey(keyInfo trustmanager.KeyInfo, privKey data.PrivateKey) error { + added, err := s.addKey(privKey.ID(), keyInfo.Role, privKey) + if err != nil { + return err + } + if added && s.backupStore != nil { + err = s.backupStore.AddKey(keyInfo, privKey) + if err != nil { + defer s.RemoveKey(privKey.ID()) + return ErrBackupFailed{err: err.Error()} + } + } + return nil +} + +// Only add if we haven't seen the key already. Return whether the key was +// added. +func (s *YubiKeyStore) addKey(keyID, role string, privKey data.PrivateKey) ( + bool, error) { + + // We only allow adding root keys for now + if role != data.CanonicalRootRole { + return false, fmt.Errorf( + "yubikey only supports storing root keys, got %s for key: %s", role, keyID) + } + + ctx, session, err := SetupHSMEnv(pkcs11Lib, s.libLoader) + if err != nil { + logrus.Debugf("Failed to initialize PKCS11 environment: %s", err.Error()) + return false, err + } + defer cleanup(ctx, session) + + if k, ok := s.keys[keyID]; ok { + if k.role == role { + // already have the key and it's associated with the correct role + return false, nil + } + } + + slot, err := getNextEmptySlot(ctx, session) + if err != nil { + logrus.Debugf("Failed to get an empty yubikey slot: %s", err.Error()) + return false, err + } + logrus.Debugf("Attempting to store key using yubikey slot %v", slot) + + err = addECDSAKey( + ctx, session, privKey, slot, s.passRetriever, role) + if err == nil { + s.keys[privKey.ID()] = yubiSlot{ + role: role, + slotID: slot, + } + return true, nil + } + logrus.Debugf("Failed to add key to yubikey: %v", err) + + return false, err +} + +// GetKey retrieves a key from the Yubikey only (it does not look inside the +// backup store) +func (s *YubiKeyStore) GetKey(keyID string) (data.PrivateKey, string, error) { + ctx, session, err := SetupHSMEnv(pkcs11Lib, s.libLoader) + if err != nil { + logrus.Debugf("Failed to initialize PKCS11 environment: %s", err.Error()) + return nil, "", err + } + defer cleanup(ctx, session) + + key, ok := s.keys[keyID] + if !ok { + return nil, "", errors.New("no matching keys found inside of yubikey") + } + + pubKey, alias, err := getECDSAKey(ctx, session, key.slotID) + if err != nil { + logrus.Debugf("Failed to get key from slot %s: %s", key.slotID, err.Error()) + return nil, "", err + } + // Check to see if we're returning the intended keyID + if pubKey.ID() != keyID { + return nil, "", fmt.Errorf("expected root key: %s, but found: %s", keyID, pubKey.ID()) + } + privKey := NewYubiPrivateKey(key.slotID, *pubKey, s.passRetriever) + if privKey == nil { + return nil, "", errors.New("could not initialize new YubiPrivateKey") + } + + return privKey, alias, err +} + +// RemoveKey deletes a key from the Yubikey only (it does not remove it from the +// backup store) +func (s *YubiKeyStore) RemoveKey(keyID string) error { + ctx, session, err := SetupHSMEnv(pkcs11Lib, s.libLoader) + if err != nil { + logrus.Debugf("Failed to initialize PKCS11 environment: %s", err.Error()) + return nil + } + defer cleanup(ctx, session) + + key, ok := s.keys[keyID] + if !ok { + return errors.New("Key not present in yubikey") + } + err = yubiRemoveKey(ctx, session, key.slotID, s.passRetriever, keyID) + if err == nil { + delete(s.keys, keyID) + } else { + logrus.Debugf("Failed to remove from the yubikey KeyID %s: %v", keyID, err) + } + + return err +} + +// ExportKey doesn't work, because you can't export data from a Yubikey +func (s *YubiKeyStore) ExportKey(keyID string) ([]byte, error) { + logrus.Debugf("Attempting to export: %s key inside of YubiKeyStore", keyID) + return nil, errors.New("Keys cannot be exported from a Yubikey.") +} + +// Not yet implemented +func (s *YubiKeyStore) GetKeyInfo(keyID string) (trustmanager.KeyInfo, error) { + return trustmanager.KeyInfo{}, fmt.Errorf("Not yet implemented") +} + +func cleanup(ctx IPKCS11Ctx, session pkcs11.SessionHandle) { + err := ctx.CloseSession(session) + if err != nil { + logrus.Debugf("Error closing session: %s", err.Error()) + } + finalizeAndDestroy(ctx) +} + +func finalizeAndDestroy(ctx IPKCS11Ctx) { + err := ctx.Finalize() + if err != nil { + logrus.Debugf("Error finalizing: %s", err.Error()) + } + ctx.Destroy() +} + +// SetupHSMEnv is a method that depends on the existences +func SetupHSMEnv(libraryPath string, libLoader pkcs11LibLoader) ( + IPKCS11Ctx, pkcs11.SessionHandle, error) { + + if libraryPath == "" { + return nil, 0, fmt.Errorf("no library found.") + } + p := libLoader(libraryPath) + + if p == nil { + return nil, 0, fmt.Errorf("failed to load library %s", libraryPath) + } + + if err := p.Initialize(); err != nil { + defer finalizeAndDestroy(p) + return nil, 0, fmt.Errorf( + "found library %s, but initialize error %s", libraryPath, err.Error()) + } + + slots, err := p.GetSlotList(true) + if err != nil { + defer finalizeAndDestroy(p) + return nil, 0, fmt.Errorf( + "loaded library %s, but failed to list HSM slots %s", libraryPath, err) + } + // Check to see if we got any slots from the HSM. + if len(slots) < 1 { + defer finalizeAndDestroy(p) + return nil, 0, fmt.Errorf( + "loaded library %s, but no HSM slots found", libraryPath) + } + + // CKF_SERIAL_SESSION: TRUE if cryptographic functions are performed in serial with the application; FALSE if the functions may be performed in parallel with the application. + // CKF_RW_SESSION: TRUE if the session is read/write; FALSE if the session is read-only + session, err := p.OpenSession(slots[0], pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION) + if err != nil { + defer cleanup(p, session) + return nil, 0, fmt.Errorf( + "loaded library %s, but failed to start session with HSM %s", + libraryPath, err) + } + + logrus.Debugf("Initialized PKCS11 library %s and started HSM session", libraryPath) + return p, session, nil +} + +// YubikeyAccessible returns true if a Yubikey can be accessed +func YubikeyAccessible() bool { + if pkcs11Lib == "" { + return false + } + ctx, session, err := SetupHSMEnv(pkcs11Lib, defaultLoader) + if err != nil { + return false + } + defer cleanup(ctx, session) + return true +} + +func login(ctx IPKCS11Ctx, session pkcs11.SessionHandle, passRetriever passphrase.Retriever, userFlag uint, defaultPassw string) error { + // try default password + err := ctx.Login(session, userFlag, defaultPassw) + if err == nil { + return nil + } + + // default failed, ask user for password + for attempts := 0; ; attempts++ { + var ( + giveup bool + err error + user string + ) + if userFlag == pkcs11.CKU_SO { + user = "SO Pin" + } else { + user = "User Pin" + } + passwd, giveup, err := passRetriever(user, "yubikey", false, attempts) + // Check if the passphrase retriever got an error or if it is telling us to give up + if giveup || err != nil { + return trustmanager.ErrPasswordInvalid{} + } + if attempts > 2 { + return trustmanager.ErrAttemptsExceeded{} + } + + // Try to convert PEM encoded bytes back to a PrivateKey using the passphrase + err = ctx.Login(session, userFlag, passwd) + if err == nil { + return nil + } + } + return nil +} + +func buildKeyMap(keys map[string]yubiSlot) map[string]trustmanager.KeyInfo { + res := make(map[string]trustmanager.KeyInfo) + for k, v := range keys { + res[k] = trustmanager.KeyInfo{Role: v.role, Gun: ""} + } + return res +} diff --git a/vendor/src/github.com/docker/notary/tuf/LICENSE b/vendor/src/github.com/docker/notary/tuf/LICENSE new file mode 100644 index 00000000..d92ae9ee --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2015, Docker Inc. +Copyright (c) 2014-2015 Prime Directive, Inc. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Prime Directive, Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/src/github.com/docker/notary/tuf/README.md b/vendor/src/github.com/docker/notary/tuf/README.md new file mode 100644 index 00000000..00a342e8 --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/README.md @@ -0,0 +1,36 @@ +# GOTUF + +This is still a work in progress but will shortly be a fully compliant +Go implementation of [The Update Framework (TUF)](http://theupdateframework.com/). + +## Where's the CLI + +This repository provides a library only. The [Notary project](https://github.com/docker/notary) +from Docker should be considered the official CLI to be used with this implementation of TUF. + +## TODOs: + +- [X] Add Targets to existing repo +- [X] Sign metadata files +- [X] Refactor TufRepo to take care of signing ~~and verification~~ +- [ ] Ensure consistent capitalization in naming (TUF\_\_\_ vs Tuf\_\_\_) +- [X] Make caching of metadata files smarter - PR #5 +- [ ] ~~Add configuration for CLI commands. Order of configuration priority from most to least: flags, config file, defaults~~ Notary should be the official CLI +- [X] Reasses organization of data types. Possibly consolidate a few things into the data package but break up package into a few more distinct files +- [ ] Comprehensive test cases +- [ ] Delete files no longer in use +- [ ] Fix up errors. Some have to be instantiated, others don't, the inconsistency is annoying. +- [X] Bump version numbers in meta files (could probably be done better) + +## Credits + +This implementation was originally forked from [flynn/go-tuf](https://github.com/flynn/go-tuf), +however in attempting to add delegations I found I was making such +significant changes that I could not maintain backwards compatibility +without the code becoming overly convoluted. + +Some features such as pluggable verifiers have already been merged upstream to flynn/go-tuf +and we are in discussion with [titanous](https://github.com/titanous) about working to merge the 2 implementations. + +This implementation retains the same 3 Clause BSD license present on +the original flynn implementation. diff --git a/vendor/src/github.com/docker/notary/tuf/client/client.go b/vendor/src/github.com/docker/notary/tuf/client/client.go new file mode 100644 index 00000000..4feed077 --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/client/client.go @@ -0,0 +1,546 @@ +package client + +import ( + "encoding/json" + "fmt" + "path" + + "github.com/Sirupsen/logrus" + "github.com/docker/notary" + tuf "github.com/docker/notary/tuf" + "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/signed" + "github.com/docker/notary/tuf/store" + "github.com/docker/notary/tuf/utils" +) + +// Client is a usability wrapper around a raw TUF repo +type Client struct { + local *tuf.Repo + remote store.RemoteStore + cache store.MetadataStore +} + +// NewClient initialized a Client with the given repo, remote source of content, and cache +func NewClient(local *tuf.Repo, remote store.RemoteStore, cache store.MetadataStore) *Client { + return &Client{ + local: local, + remote: remote, + cache: cache, + } +} + +// Update performs an update to the TUF repo as defined by the TUF spec +func (c *Client) Update() error { + // 1. Get timestamp + // a. If timestamp error (verification, expired, etc...) download new root and return to 1. + // 2. Check if local snapshot is up to date + // a. If out of date, get updated snapshot + // i. If snapshot error, download new root and return to 1. + // 3. Check if root correct against snapshot + // a. If incorrect, download new root and return to 1. + // 4. Iteratively download and search targets and delegations to find target meta + logrus.Debug("updating TUF client") + err := c.update() + if err != nil { + logrus.Debug("Error occurred. Root will be downloaded and another update attempted") + if err := c.downloadRoot(); err != nil { + logrus.Debug("Client Update (Root):", err) + return err + } + // If we error again, we now have the latest root and just want to fail + // out as there's no expectation the problem can be resolved automatically + logrus.Debug("retrying TUF client update") + return c.update() + } + return nil +} + +func (c *Client) update() error { + err := c.downloadTimestamp() + if err != nil { + logrus.Debugf("Client Update (Timestamp): %s", err.Error()) + return err + } + err = c.downloadSnapshot() + if err != nil { + logrus.Debugf("Client Update (Snapshot): %s", err.Error()) + return err + } + err = c.checkRoot() + if err != nil { + // In this instance the root has not expired base on time, but is + // expired based on the snapshot dictating a new root has been produced. + logrus.Debug(err) + return err + } + // will always need top level targets at a minimum + err = c.downloadTargets("targets") + if err != nil { + logrus.Debugf("Client Update (Targets): %s", err.Error()) + return err + } + return nil +} + +// checkRoot determines if the hash, and size are still those reported +// in the snapshot file. It will also check the expiry, however, if the +// hash and size in snapshot are unchanged but the root file has expired, +// there is little expectation that the situation can be remedied. +func (c Client) checkRoot() error { + role := data.CanonicalRootRole + size := c.local.Snapshot.Signed.Meta[role].Length + + expectedHashes := c.local.Snapshot.Signed.Meta[role].Hashes + + raw, err := c.cache.GetMeta("root", size) + if err != nil { + return err + } + + if err := data.CheckHashes(raw, expectedHashes); err != nil { + return fmt.Errorf("Cached root hashes did not match snapshot root hashes") + } + + if int64(len(raw)) != size { + return fmt.Errorf("Cached root size did not match snapshot size") + } + + root := &data.SignedRoot{} + err = json.Unmarshal(raw, root) + if err != nil { + return ErrCorruptedCache{file: "root.json"} + } + + if signed.IsExpired(root.Signed.Expires) { + return tuf.ErrLocalRootExpired{} + } + return nil +} + +// downloadRoot is responsible for downloading the root.json +func (c *Client) downloadRoot() error { + logrus.Debug("Downloading Root...") + role := data.CanonicalRootRole + // We can't read an exact size for the root metadata without risking getting stuck in the TUF update cycle + // since it's possible that downloading timestamp/snapshot metadata may fail due to a signature mismatch + var size int64 = -1 + + // We could not expect what the "snapshot" meta has specified. + // + // In some old clients, there is only the "sha256", + // but both "sha256" and "sha512" in the newer ones. + // + // And possibly more in the future. + var expectedHashes data.Hashes + + if c.local.Snapshot != nil { + if prevRootMeta, ok := c.local.Snapshot.Signed.Meta[role]; ok { + size = prevRootMeta.Length + expectedHashes = prevRootMeta.Hashes + } + } + + // if we're bootstrapping we may not have a cached root, an + // error will result in the "previous root version" being + // interpreted as 0. + var download bool + var err error + var cachedRoot []byte + old := &data.Signed{} + version := 0 + + // Due to the same reason, we don't really know how many hashes are there. + if len(expectedHashes) != 0 { + // can only trust cache if we have an expected sha256(for example) to trust + cachedRoot, err = c.cache.GetMeta(role, size) + } + + if cachedRoot == nil || err != nil { + logrus.Debug("didn't find a cached root, must download") + download = true + } else { + if err := data.CheckHashes(cachedRoot, expectedHashes); err != nil { + logrus.Debug("cached root's hash didn't match expected, must download") + download = true + } + + err := json.Unmarshal(cachedRoot, old) + if err == nil { + root, err := data.RootFromSigned(old) + if err == nil { + version = root.Signed.Version + } else { + logrus.Debug("couldn't parse Signed part of cached root, must download") + download = true + } + } else { + logrus.Debug("couldn't parse cached root, must download") + download = true + } + } + var s *data.Signed + var raw []byte + if download { + // use consistent download if we have the checksum. + raw, s, err = c.downloadSigned(role, size, expectedHashes) + if err != nil { + return err + } + } else { + logrus.Debug("using cached root") + s = old + } + if err := c.verifyRoot(role, s, version); err != nil { + return err + } + if download { + logrus.Debug("caching downloaded root") + // Now that we have accepted new root, write it to cache + if err = c.cache.SetMeta(role, raw); err != nil { + logrus.Errorf("Failed to write root to local cache: %s", err.Error()) + } + } + return nil +} + +func (c Client) verifyRoot(role string, s *data.Signed, minVersion int) error { + // this will confirm that the root has been signed by the old root role + // with the root keys we bootstrapped with. + // Still need to determine if there has been a root key update and + // confirm signature with new root key + logrus.Debug("verifying root with existing keys") + rootRole, err := c.local.GetBaseRole(role) + if err != nil { + logrus.Debug("no previous root role loaded") + return err + } + // Verify using the rootRole loaded from the known root.json + if err = signed.Verify(s, rootRole, minVersion); err != nil { + logrus.Debug("root did not verify with existing keys") + return err + } + + logrus.Debug("updating known root roles and keys") + root, err := data.RootFromSigned(s) + if err != nil { + logrus.Error(err.Error()) + return err + } + // replace the existing root.json with the new one (just in memory, we + // have another validation step before we fully accept the new root) + err = c.local.SetRoot(root) + if err != nil { + logrus.Error(err.Error()) + return err + } + // Verify the new root again having loaded the rootRole out of this new + // file (verifies self-referential integrity) + // TODO(endophage): be more intelligent and only re-verify if we detect + // there has been a change in root keys + logrus.Debug("verifying root with updated keys") + rootRole, err = c.local.GetBaseRole(role) + if err != nil { + logrus.Debug("root role with new keys not loaded") + return err + } + err = signed.Verify(s, rootRole, minVersion) + if err != nil { + logrus.Debug("root did not verify with new keys") + return err + } + logrus.Debug("successfully verified root") + return nil +} + +// downloadTimestamp is responsible for downloading the timestamp.json +// Timestamps are special in that we ALWAYS attempt to download and only +// use cache if the download fails (and the cache is still valid). +func (c *Client) downloadTimestamp() error { + logrus.Debug("Downloading Timestamp...") + role := data.CanonicalTimestampRole + + // We may not have a cached timestamp if this is the first time + // we're interacting with the repo. This will result in the + // version being 0 + var ( + old *data.Signed + ts *data.SignedTimestamp + version = 0 + ) + cachedTS, err := c.cache.GetMeta(role, notary.MaxTimestampSize) + if err == nil { + cached := &data.Signed{} + err := json.Unmarshal(cachedTS, cached) + if err == nil { + ts, err := data.TimestampFromSigned(cached) + if err == nil { + version = ts.Signed.Version + } + old = cached + } + } + // unlike root, targets and snapshot, always try and download timestamps + // from remote, only using the cache one if we couldn't reach remote. + raw, s, err := c.downloadSigned(role, notary.MaxTimestampSize, nil) + if err == nil { + ts, err = c.verifyTimestamp(s, version) + if err == nil { + logrus.Debug("successfully verified downloaded timestamp") + c.cache.SetMeta(role, raw) + c.local.SetTimestamp(ts) + return nil + } + } + if old == nil { + // couldn't retrieve valid data from server and don't have unmarshallable data in cache. + logrus.Debug("no cached timestamp available") + return err + } + logrus.Debug(err.Error()) + logrus.Warn("Error while downloading remote metadata, using cached timestamp - this might not be the latest version available remotely") + ts, err = c.verifyTimestamp(old, version) + if err != nil { + return err + } + logrus.Debug("successfully verified cached timestamp") + c.local.SetTimestamp(ts) + return nil +} + +// verifies that a timestamp is valid, and returned the SignedTimestamp object to add to the tuf repo +func (c *Client) verifyTimestamp(s *data.Signed, minVersion int) (*data.SignedTimestamp, error) { + timestampRole, err := c.local.GetBaseRole(data.CanonicalTimestampRole) + if err != nil { + logrus.Debug("no timestamp role loaded") + return nil, err + } + if err := signed.Verify(s, timestampRole, minVersion); err != nil { + return nil, err + } + return data.TimestampFromSigned(s) +} + +// downloadSnapshot is responsible for downloading the snapshot.json +func (c *Client) downloadSnapshot() error { + logrus.Debug("Downloading Snapshot...") + role := data.CanonicalSnapshotRole + if c.local.Timestamp == nil { + return tuf.ErrNotLoaded{Role: data.CanonicalTimestampRole} + } + size := c.local.Timestamp.Signed.Meta[role].Length + expectedHashes := c.local.Timestamp.Signed.Meta[role].Hashes + if len(expectedHashes) == 0 { + return data.ErrMissingMeta{Role: "snapshot"} + } + + var download bool + old := &data.Signed{} + version := 0 + raw, err := c.cache.GetMeta(role, size) + if raw == nil || err != nil { + logrus.Debug("no snapshot in cache, must download") + download = true + } else { + // file may have been tampered with on disk. Always check the hash! + if err := data.CheckHashes(raw, expectedHashes); err != nil { + logrus.Debug("hash of snapshot in cache did not match expected hash, must download") + download = true + } + + err := json.Unmarshal(raw, old) + if err == nil { + snap, err := data.SnapshotFromSigned(old) + if err == nil { + version = snap.Signed.Version + } else { + logrus.Debug("Could not parse Signed part of snapshot, must download") + download = true + } + } else { + logrus.Debug("Could not parse snapshot, must download") + download = true + } + } + var s *data.Signed + if download { + raw, s, err = c.downloadSigned(role, size, expectedHashes) + if err != nil { + return err + } + } else { + logrus.Debug("using cached snapshot") + s = old + } + + snapshotRole, err := c.local.GetBaseRole(role) + if err != nil { + logrus.Debug("no snapshot role loaded") + return err + } + err = signed.Verify(s, snapshotRole, version) + if err != nil { + return err + } + logrus.Debug("successfully verified snapshot") + snap, err := data.SnapshotFromSigned(s) + if err != nil { + return err + } + c.local.SetSnapshot(snap) + if download { + err = c.cache.SetMeta(role, raw) + if err != nil { + logrus.Errorf("Failed to write snapshot to local cache: %s", err.Error()) + } + } + return nil +} + +// downloadTargets downloads all targets and delegated targets for the repository. +// It uses a pre-order tree traversal as it's necessary to download parents first +// to obtain the keys to validate children. +func (c *Client) downloadTargets(role string) error { + logrus.Debug("Downloading Targets...") + stack := utils.NewStack() + stack.Push(role) + for !stack.Empty() { + role, err := stack.PopString() + if err != nil { + return err + } + if c.local.Snapshot == nil { + return tuf.ErrNotLoaded{Role: data.CanonicalSnapshotRole} + } + snap := c.local.Snapshot.Signed + root := c.local.Root.Signed + + s, err := c.getTargetsFile(role, snap.Meta, root.ConsistentSnapshot) + if err != nil { + if _, ok := err.(data.ErrMissingMeta); ok && role != data.CanonicalTargetsRole { + // if the role meta hasn't been published, + // that's ok, continue + continue + } + logrus.Error("Error getting targets file:", err) + return err + } + t, err := data.TargetsFromSigned(s, role) + if err != nil { + return err + } + err = c.local.SetTargets(role, t) + if err != nil { + return err + } + + // push delegated roles contained in the targets file onto the stack + for _, r := range t.Signed.Delegations.Roles { + if path.Dir(r.Name) == role { + // only load children that are direct 1st generation descendants + // of the role we've just downloaded + stack.Push(r.Name) + } + } + } + return nil +} + +func (c *Client) downloadSigned(role string, size int64, expectedHashes data.Hashes) ([]byte, *data.Signed, error) { + rolePath := utils.ConsistentName(role, expectedHashes["sha256"]) + raw, err := c.remote.GetMeta(rolePath, size) + if err != nil { + return nil, nil, err + } + + if expectedHashes != nil { + if err := data.CheckHashes(raw, expectedHashes); err != nil { + return nil, nil, ErrChecksumMismatch{role: role} + } + } + + s := &data.Signed{} + err = json.Unmarshal(raw, s) + if err != nil { + return nil, nil, err + } + return raw, s, nil +} + +func (c Client) getTargetsFile(role string, snapshotMeta data.Files, consistent bool) (*data.Signed, error) { + // require role exists in snapshots + roleMeta, ok := snapshotMeta[role] + if !ok { + return nil, data.ErrMissingMeta{Role: role} + } + expectedHashes := snapshotMeta[role].Hashes + if len(expectedHashes) == 0 { + return nil, data.ErrMissingMeta{Role: role} + } + + // try to get meta file from content addressed cache + var download bool + old := &data.Signed{} + version := 0 + raw, err := c.cache.GetMeta(role, roleMeta.Length) + if err != nil || raw == nil { + logrus.Debugf("Couldn't not find cached %s, must download", role) + download = true + } else { + // file may have been tampered with on disk. Always check the hash! + if err := data.CheckHashes(raw, expectedHashes); err != nil { + download = true + } + + err := json.Unmarshal(raw, old) + if err == nil { + targ, err := data.TargetsFromSigned(old, role) + if err == nil { + version = targ.Signed.Version + } else { + download = true + } + } else { + download = true + } + } + + size := snapshotMeta[role].Length + var s *data.Signed + if download { + raw, s, err = c.downloadSigned(role, size, expectedHashes) + if err != nil { + return nil, err + } + } else { + logrus.Debug("using cached ", role) + s = old + } + var targetOrDelgRole data.BaseRole + if data.IsDelegation(role) { + delgRole, err := c.local.GetDelegationRole(role) + if err != nil { + logrus.Debugf("no %s delegation role loaded", role) + return nil, err + } + targetOrDelgRole = delgRole.BaseRole + } else { + targetOrDelgRole, err = c.local.GetBaseRole(role) + if err != nil { + logrus.Debugf("no %s role loaded", role) + return nil, err + } + } + if err = signed.Verify(s, targetOrDelgRole, version); err != nil { + return nil, err + } + logrus.Debugf("successfully verified %s", role) + if download { + // if we error when setting meta, we should continue. + err = c.cache.SetMeta(role, raw) + if err != nil { + logrus.Errorf("Failed to write %s to local cache: %s", role, err.Error()) + } + } + return s, nil +} diff --git a/vendor/src/github.com/docker/notary/tuf/client/errors.go b/vendor/src/github.com/docker/notary/tuf/client/errors.go new file mode 100644 index 00000000..ad055512 --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/client/errors.go @@ -0,0 +1,23 @@ +package client + +import ( + "fmt" +) + +// ErrChecksumMismatch - a checksum failed verification +type ErrChecksumMismatch struct { + role string +} + +func (e ErrChecksumMismatch) Error() string { + return fmt.Sprintf("tuf: checksum for %s did not match", e.role) +} + +// ErrCorruptedCache - local data is incorrect +type ErrCorruptedCache struct { + file string +} + +func (e ErrCorruptedCache) Error() string { + return fmt.Sprintf("cache is corrupted: %s", e.file) +} diff --git a/vendor/src/github.com/docker/notary/tuf/data/errors.go b/vendor/src/github.com/docker/notary/tuf/data/errors.go new file mode 100644 index 00000000..7ff5814c --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/data/errors.go @@ -0,0 +1,22 @@ +package data + +import "fmt" + +// ErrInvalidMetadata is the error to be returned when metadata is invalid +type ErrInvalidMetadata struct { + role string + msg string +} + +func (e ErrInvalidMetadata) Error() string { + return fmt.Sprintf("%s type metadata invalid: %s", e.role, e.msg) +} + +// ErrMissingMeta - couldn't find the FileMeta object for a role or target +type ErrMissingMeta struct { + Role string +} + +func (e ErrMissingMeta) Error() string { + return fmt.Sprintf("tuf: sha256 checksum required for %s", e.Role) +} diff --git a/vendor/src/github.com/docker/notary/tuf/data/keys.go b/vendor/src/github.com/docker/notary/tuf/data/keys.go new file mode 100644 index 00000000..25df598c --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/data/keys.go @@ -0,0 +1,528 @@ +package data + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/asn1" + "encoding/hex" + "errors" + "io" + "math/big" + + "github.com/Sirupsen/logrus" + "github.com/agl/ed25519" + "github.com/docker/go/canonical/json" +) + +// PublicKey is the necessary interface for public keys +type PublicKey interface { + ID() string + Algorithm() string + Public() []byte +} + +// PrivateKey adds the ability to access the private key +type PrivateKey interface { + PublicKey + Sign(rand io.Reader, msg []byte, opts crypto.SignerOpts) (signature []byte, err error) + Private() []byte + CryptoSigner() crypto.Signer + SignatureAlgorithm() SigAlgorithm +} + +// KeyPair holds the public and private key bytes +type KeyPair struct { + Public []byte `json:"public"` + Private []byte `json:"private"` +} + +// Keys represents a map of key ID to PublicKey object. It's necessary +// to allow us to unmarshal into an interface via the json.Unmarshaller +// interface +type Keys map[string]PublicKey + +// UnmarshalJSON implements the json.Unmarshaller interface +func (ks *Keys) UnmarshalJSON(data []byte) error { + parsed := make(map[string]TUFKey) + err := json.Unmarshal(data, &parsed) + if err != nil { + return err + } + final := make(map[string]PublicKey) + for k, tk := range parsed { + final[k] = typedPublicKey(tk) + } + *ks = final + return nil +} + +// KeyList represents a list of keys +type KeyList []PublicKey + +// UnmarshalJSON implements the json.Unmarshaller interface +func (ks *KeyList) UnmarshalJSON(data []byte) error { + parsed := make([]TUFKey, 0, 1) + err := json.Unmarshal(data, &parsed) + if err != nil { + return err + } + final := make([]PublicKey, 0, len(parsed)) + for _, tk := range parsed { + final = append(final, typedPublicKey(tk)) + } + *ks = final + return nil +} + +// IDs generates a list of the hex encoded key IDs in the KeyList +func (ks KeyList) IDs() []string { + keyIDs := make([]string, 0, len(ks)) + for _, k := range ks { + keyIDs = append(keyIDs, k.ID()) + } + return keyIDs +} + +func typedPublicKey(tk TUFKey) PublicKey { + switch tk.Algorithm() { + case ECDSAKey: + return &ECDSAPublicKey{TUFKey: tk} + case ECDSAx509Key: + return &ECDSAx509PublicKey{TUFKey: tk} + case RSAKey: + return &RSAPublicKey{TUFKey: tk} + case RSAx509Key: + return &RSAx509PublicKey{TUFKey: tk} + case ED25519Key: + return &ED25519PublicKey{TUFKey: tk} + } + return &UnknownPublicKey{TUFKey: tk} +} + +func typedPrivateKey(tk TUFKey) (PrivateKey, error) { + private := tk.Value.Private + tk.Value.Private = nil + switch tk.Algorithm() { + case ECDSAKey: + return NewECDSAPrivateKey( + &ECDSAPublicKey{ + TUFKey: tk, + }, + private, + ) + case ECDSAx509Key: + return NewECDSAPrivateKey( + &ECDSAx509PublicKey{ + TUFKey: tk, + }, + private, + ) + case RSAKey: + return NewRSAPrivateKey( + &RSAPublicKey{ + TUFKey: tk, + }, + private, + ) + case RSAx509Key: + return NewRSAPrivateKey( + &RSAx509PublicKey{ + TUFKey: tk, + }, + private, + ) + case ED25519Key: + return NewED25519PrivateKey( + ED25519PublicKey{ + TUFKey: tk, + }, + private, + ) + } + return &UnknownPrivateKey{ + TUFKey: tk, + privateKey: privateKey{private: private}, + }, nil +} + +// NewPublicKey creates a new, correctly typed PublicKey, using the +// UnknownPublicKey catchall for unsupported ciphers +func NewPublicKey(alg string, public []byte) PublicKey { + tk := TUFKey{ + Type: alg, + Value: KeyPair{ + Public: public, + }, + } + return typedPublicKey(tk) +} + +// NewPrivateKey creates a new, correctly typed PrivateKey, using the +// UnknownPrivateKey catchall for unsupported ciphers +func NewPrivateKey(pubKey PublicKey, private []byte) (PrivateKey, error) { + tk := TUFKey{ + Type: pubKey.Algorithm(), + Value: KeyPair{ + Public: pubKey.Public(), + Private: private, // typedPrivateKey moves this value + }, + } + return typedPrivateKey(tk) +} + +// UnmarshalPublicKey is used to parse individual public keys in JSON +func UnmarshalPublicKey(data []byte) (PublicKey, error) { + var parsed TUFKey + err := json.Unmarshal(data, &parsed) + if err != nil { + return nil, err + } + return typedPublicKey(parsed), nil +} + +// UnmarshalPrivateKey is used to parse individual private keys in JSON +func UnmarshalPrivateKey(data []byte) (PrivateKey, error) { + var parsed TUFKey + err := json.Unmarshal(data, &parsed) + if err != nil { + return nil, err + } + return typedPrivateKey(parsed) +} + +// TUFKey is the structure used for both public and private keys in TUF. +// Normally it would make sense to use a different structures for public and +// private keys, but that would change the key ID algorithm (since the canonical +// JSON would be different). This structure should normally be accessed through +// the PublicKey or PrivateKey interfaces. +type TUFKey struct { + id string + Type string `json:"keytype"` + Value KeyPair `json:"keyval"` +} + +// Algorithm returns the algorithm of the key +func (k TUFKey) Algorithm() string { + return k.Type +} + +// ID efficiently generates if necessary, and caches the ID of the key +func (k *TUFKey) ID() string { + if k.id == "" { + pubK := TUFKey{ + Type: k.Algorithm(), + Value: KeyPair{ + Public: k.Public(), + Private: nil, + }, + } + data, err := json.MarshalCanonical(&pubK) + if err != nil { + logrus.Error("Error generating key ID:", err) + } + digest := sha256.Sum256(data) + k.id = hex.EncodeToString(digest[:]) + } + return k.id +} + +// Public returns the public bytes +func (k TUFKey) Public() []byte { + return k.Value.Public +} + +// Public key types + +// ECDSAPublicKey represents an ECDSA key using a raw serialization +// of the public key +type ECDSAPublicKey struct { + TUFKey +} + +// ECDSAx509PublicKey represents an ECDSA key using an x509 cert +// as the serialized format of the public key +type ECDSAx509PublicKey struct { + TUFKey +} + +// RSAPublicKey represents an RSA key using a raw serialization +// of the public key +type RSAPublicKey struct { + TUFKey +} + +// RSAx509PublicKey represents an RSA key using an x509 cert +// as the serialized format of the public key +type RSAx509PublicKey struct { + TUFKey +} + +// ED25519PublicKey represents an ED25519 key using a raw serialization +// of the public key +type ED25519PublicKey struct { + TUFKey +} + +// UnknownPublicKey is a catchall for key types that are not supported +type UnknownPublicKey struct { + TUFKey +} + +// NewECDSAPublicKey initializes a new public key with the ECDSAKey type +func NewECDSAPublicKey(public []byte) *ECDSAPublicKey { + return &ECDSAPublicKey{ + TUFKey: TUFKey{ + Type: ECDSAKey, + Value: KeyPair{ + Public: public, + Private: nil, + }, + }, + } +} + +// NewECDSAx509PublicKey initializes a new public key with the ECDSAx509Key type +func NewECDSAx509PublicKey(public []byte) *ECDSAx509PublicKey { + return &ECDSAx509PublicKey{ + TUFKey: TUFKey{ + Type: ECDSAx509Key, + Value: KeyPair{ + Public: public, + Private: nil, + }, + }, + } +} + +// NewRSAPublicKey initializes a new public key with the RSA type +func NewRSAPublicKey(public []byte) *RSAPublicKey { + return &RSAPublicKey{ + TUFKey: TUFKey{ + Type: RSAKey, + Value: KeyPair{ + Public: public, + Private: nil, + }, + }, + } +} + +// NewRSAx509PublicKey initializes a new public key with the RSAx509Key type +func NewRSAx509PublicKey(public []byte) *RSAx509PublicKey { + return &RSAx509PublicKey{ + TUFKey: TUFKey{ + Type: RSAx509Key, + Value: KeyPair{ + Public: public, + Private: nil, + }, + }, + } +} + +// NewED25519PublicKey initializes a new public key with the ED25519Key type +func NewED25519PublicKey(public []byte) *ED25519PublicKey { + return &ED25519PublicKey{ + TUFKey: TUFKey{ + Type: ED25519Key, + Value: KeyPair{ + Public: public, + Private: nil, + }, + }, + } +} + +// Private key types +type privateKey struct { + private []byte +} + +type signer struct { + signer crypto.Signer +} + +// ECDSAPrivateKey represents a private ECDSA key +type ECDSAPrivateKey struct { + PublicKey + privateKey + signer +} + +// RSAPrivateKey represents a private RSA key +type RSAPrivateKey struct { + PublicKey + privateKey + signer +} + +// ED25519PrivateKey represents a private ED25519 key +type ED25519PrivateKey struct { + ED25519PublicKey + privateKey +} + +// UnknownPrivateKey is a catchall for unsupported key types +type UnknownPrivateKey struct { + TUFKey + privateKey +} + +// NewECDSAPrivateKey initializes a new ECDSA private key +func NewECDSAPrivateKey(public PublicKey, private []byte) (*ECDSAPrivateKey, error) { + switch public.(type) { + case *ECDSAPublicKey, *ECDSAx509PublicKey: + default: + return nil, errors.New("Invalid public key type provided to NewECDSAPrivateKey") + } + ecdsaPrivKey, err := x509.ParseECPrivateKey(private) + if err != nil { + return nil, err + } + return &ECDSAPrivateKey{ + PublicKey: public, + privateKey: privateKey{private: private}, + signer: signer{signer: ecdsaPrivKey}, + }, nil +} + +// NewRSAPrivateKey initialized a new RSA private key +func NewRSAPrivateKey(public PublicKey, private []byte) (*RSAPrivateKey, error) { + switch public.(type) { + case *RSAPublicKey, *RSAx509PublicKey: + default: + return nil, errors.New("Invalid public key type provided to NewRSAPrivateKey") + } + rsaPrivKey, err := x509.ParsePKCS1PrivateKey(private) + if err != nil { + return nil, err + } + return &RSAPrivateKey{ + PublicKey: public, + privateKey: privateKey{private: private}, + signer: signer{signer: rsaPrivKey}, + }, nil +} + +// NewED25519PrivateKey initialized a new ED25519 private key +func NewED25519PrivateKey(public ED25519PublicKey, private []byte) (*ED25519PrivateKey, error) { + return &ED25519PrivateKey{ + ED25519PublicKey: public, + privateKey: privateKey{private: private}, + }, nil +} + +// Private return the serialized private bytes of the key +func (k privateKey) Private() []byte { + return k.private +} + +// CryptoSigner returns the underlying crypto.Signer for use cases where we need the default +// signature or public key functionality (like when we generate certificates) +func (s signer) CryptoSigner() crypto.Signer { + return s.signer +} + +// CryptoSigner returns the ED25519PrivateKey which already implements crypto.Signer +func (k ED25519PrivateKey) CryptoSigner() crypto.Signer { + return nil +} + +// CryptoSigner returns the UnknownPrivateKey which already implements crypto.Signer +func (k UnknownPrivateKey) CryptoSigner() crypto.Signer { + return nil +} + +type ecdsaSig struct { + R *big.Int + S *big.Int +} + +// Sign creates an ecdsa signature +func (k ECDSAPrivateKey) Sign(rand io.Reader, msg []byte, opts crypto.SignerOpts) (signature []byte, err error) { + ecdsaPrivKey, ok := k.CryptoSigner().(*ecdsa.PrivateKey) + if !ok { + return nil, errors.New("Signer was based on the wrong key type") + } + hashed := sha256.Sum256(msg) + sigASN1, err := ecdsaPrivKey.Sign(rand, hashed[:], opts) + if err != nil { + return nil, err + } + + sig := ecdsaSig{} + _, err = asn1.Unmarshal(sigASN1, &sig) + if err != nil { + return nil, err + } + rBytes, sBytes := sig.R.Bytes(), sig.S.Bytes() + octetLength := (ecdsaPrivKey.Params().BitSize + 7) >> 3 + + // MUST include leading zeros in the output + rBuf := make([]byte, octetLength-len(rBytes), octetLength) + sBuf := make([]byte, octetLength-len(sBytes), octetLength) + + rBuf = append(rBuf, rBytes...) + sBuf = append(sBuf, sBytes...) + return append(rBuf, sBuf...), nil +} + +// Sign creates an rsa signature +func (k RSAPrivateKey) Sign(rand io.Reader, msg []byte, opts crypto.SignerOpts) (signature []byte, err error) { + hashed := sha256.Sum256(msg) + if opts == nil { + opts = &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthEqualsHash, + Hash: crypto.SHA256, + } + } + return k.CryptoSigner().Sign(rand, hashed[:], opts) +} + +// Sign creates an ed25519 signature +func (k ED25519PrivateKey) Sign(rand io.Reader, msg []byte, opts crypto.SignerOpts) (signature []byte, err error) { + priv := [ed25519.PrivateKeySize]byte{} + copy(priv[:], k.private[ed25519.PublicKeySize:]) + return ed25519.Sign(&priv, msg)[:], nil +} + +// Sign on an UnknownPrivateKey raises an error because the client does not +// know how to sign with this key type. +func (k UnknownPrivateKey) Sign(rand io.Reader, msg []byte, opts crypto.SignerOpts) (signature []byte, err error) { + return nil, errors.New("Unknown key type, cannot sign.") +} + +// SignatureAlgorithm returns the SigAlgorithm for a ECDSAPrivateKey +func (k ECDSAPrivateKey) SignatureAlgorithm() SigAlgorithm { + return ECDSASignature +} + +// SignatureAlgorithm returns the SigAlgorithm for a RSAPrivateKey +func (k RSAPrivateKey) SignatureAlgorithm() SigAlgorithm { + return RSAPSSSignature +} + +// SignatureAlgorithm returns the SigAlgorithm for a ED25519PrivateKey +func (k ED25519PrivateKey) SignatureAlgorithm() SigAlgorithm { + return EDDSASignature +} + +// SignatureAlgorithm returns the SigAlgorithm for an UnknownPrivateKey +func (k UnknownPrivateKey) SignatureAlgorithm() SigAlgorithm { + return "" +} + +// PublicKeyFromPrivate returns a new TUFKey based on a private key, with +// the private key bytes guaranteed to be nil. +func PublicKeyFromPrivate(pk PrivateKey) PublicKey { + return typedPublicKey(TUFKey{ + Type: pk.Algorithm(), + Value: KeyPair{ + Public: pk.Public(), + Private: nil, + }, + }) +} diff --git a/vendor/src/github.com/docker/notary/tuf/data/roles.go b/vendor/src/github.com/docker/notary/tuf/data/roles.go new file mode 100644 index 00000000..b1a2988b --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/data/roles.go @@ -0,0 +1,297 @@ +package data + +import ( + "fmt" + "path" + "regexp" + "strings" + + "github.com/Sirupsen/logrus" +) + +// Canonical base role names +const ( + CanonicalRootRole = "root" + CanonicalTargetsRole = "targets" + CanonicalSnapshotRole = "snapshot" + CanonicalTimestampRole = "timestamp" +) + +// BaseRoles is an easy to iterate list of the top level +// roles. +var BaseRoles = []string{ + CanonicalRootRole, + CanonicalTargetsRole, + CanonicalSnapshotRole, + CanonicalTimestampRole, +} + +// Regex for validating delegation names +var delegationRegexp = regexp.MustCompile("^[-a-z0-9_/]+$") + +// ErrNoSuchRole indicates the roles doesn't exist +type ErrNoSuchRole struct { + Role string +} + +func (e ErrNoSuchRole) Error() string { + return fmt.Sprintf("role does not exist: %s", e.Role) +} + +// ErrInvalidRole represents an error regarding a role. Typically +// something like a role for which sone of the public keys were +// not found in the TUF repo. +type ErrInvalidRole struct { + Role string + Reason string +} + +func (e ErrInvalidRole) Error() string { + if e.Reason != "" { + return fmt.Sprintf("tuf: invalid role %s. %s", e.Role, e.Reason) + } + return fmt.Sprintf("tuf: invalid role %s.", e.Role) +} + +// ValidRole only determines the name is semantically +// correct. For target delegated roles, it does NOT check +// the the appropriate parent roles exist. +func ValidRole(name string) bool { + if IsDelegation(name) { + return true + } + + for _, v := range BaseRoles { + if name == v { + return true + } + } + return false +} + +// IsDelegation checks if the role is a delegation or a root role +func IsDelegation(role string) bool { + targetsBase := CanonicalTargetsRole + "/" + + whitelistedChars := delegationRegexp.MatchString(role) + + // Limit size of full role string to 255 chars for db column size limit + correctLength := len(role) < 256 + + // Removes ., .., extra slashes, and trailing slash + isClean := path.Clean(role) == role + return strings.HasPrefix(role, targetsBase) && + whitelistedChars && + correctLength && + isClean +} + +// BaseRole is an internal representation of a root/targets/snapshot/timestamp role, with its public keys included +type BaseRole struct { + Keys map[string]PublicKey + Name string + Threshold int +} + +// NewBaseRole creates a new BaseRole object with the provided parameters +func NewBaseRole(name string, threshold int, keys ...PublicKey) BaseRole { + r := BaseRole{ + Name: name, + Threshold: threshold, + Keys: make(map[string]PublicKey), + } + for _, k := range keys { + r.Keys[k.ID()] = k + } + return r +} + +// ListKeys retrieves the public keys valid for this role +func (b BaseRole) ListKeys() KeyList { + return listKeys(b.Keys) +} + +// ListKeyIDs retrieves the list of key IDs valid for this role +func (b BaseRole) ListKeyIDs() []string { + return listKeyIDs(b.Keys) +} + +// DelegationRole is an internal representation of a delegation role, with its public keys included +type DelegationRole struct { + BaseRole + Paths []string +} + +func listKeys(keyMap map[string]PublicKey) KeyList { + keys := KeyList{} + for _, key := range keyMap { + keys = append(keys, key) + } + return keys +} + +func listKeyIDs(keyMap map[string]PublicKey) []string { + keyIDs := []string{} + for id := range keyMap { + keyIDs = append(keyIDs, id) + } + return keyIDs +} + +// Restrict restricts the paths and path hash prefixes for the passed in delegation role, +// returning a copy of the role with validated paths as if it was a direct child +func (d DelegationRole) Restrict(child DelegationRole) (DelegationRole, error) { + if !d.IsParentOf(child) { + return DelegationRole{}, fmt.Errorf("%s is not a parent of %s", d.Name, child.Name) + } + return DelegationRole{ + BaseRole: BaseRole{ + Keys: child.Keys, + Name: child.Name, + Threshold: child.Threshold, + }, + Paths: RestrictDelegationPathPrefixes(d.Paths, child.Paths), + }, nil +} + +// IsParentOf returns whether the passed in delegation role is the direct child of this role, +// determined by delegation name. +// Ex: targets/a is a direct parent of targets/a/b, but targets/a is not a direct parent of targets/a/b/c +func (d DelegationRole) IsParentOf(child DelegationRole) bool { + return path.Dir(child.Name) == d.Name +} + +// CheckPaths checks if a given path is valid for the role +func (d DelegationRole) CheckPaths(path string) bool { + return checkPaths(path, d.Paths) +} + +func checkPaths(path string, permitted []string) bool { + for _, p := range permitted { + if strings.HasPrefix(path, p) { + return true + } + } + return false +} + +// RestrictDelegationPathPrefixes returns the list of valid delegationPaths that are prefixed by parentPaths +func RestrictDelegationPathPrefixes(parentPaths, delegationPaths []string) []string { + validPaths := []string{} + if len(delegationPaths) == 0 { + return validPaths + } + + // Validate each individual delegation path + for _, delgPath := range delegationPaths { + isPrefixed := false + for _, parentPath := range parentPaths { + if strings.HasPrefix(delgPath, parentPath) { + isPrefixed = true + break + } + } + // If the delegation path did not match prefix against any parent path, it is not valid + if isPrefixed { + validPaths = append(validPaths, delgPath) + } + } + return validPaths +} + +// RootRole is a cut down role as it appears in the root.json +// Eventually should only be used for immediately before and after serialization/deserialization +type RootRole struct { + KeyIDs []string `json:"keyids"` + Threshold int `json:"threshold"` +} + +// Role is a more verbose role as they appear in targets delegations +// Eventually should only be used for immediately before and after serialization/deserialization +type Role struct { + RootRole + Name string `json:"name"` + Paths []string `json:"paths,omitempty"` +} + +// NewRole creates a new Role object from the given parameters +func NewRole(name string, threshold int, keyIDs, paths []string) (*Role, error) { + if IsDelegation(name) { + if len(paths) == 0 { + logrus.Debugf("role %s with no Paths will never be able to publish content until one or more are added", name) + } + } + if threshold < 1 { + return nil, ErrInvalidRole{Role: name} + } + if !ValidRole(name) { + return nil, ErrInvalidRole{Role: name} + } + return &Role{ + RootRole: RootRole{ + KeyIDs: keyIDs, + Threshold: threshold, + }, + Name: name, + Paths: paths, + }, nil + +} + +// CheckPaths checks if a given path is valid for the role +func (r Role) CheckPaths(path string) bool { + return checkPaths(path, r.Paths) +} + +// AddKeys merges the ids into the current list of role key ids +func (r *Role) AddKeys(ids []string) { + r.KeyIDs = mergeStrSlices(r.KeyIDs, ids) +} + +// AddPaths merges the paths into the current list of role paths +func (r *Role) AddPaths(paths []string) error { + if len(paths) == 0 { + return nil + } + r.Paths = mergeStrSlices(r.Paths, paths) + return nil +} + +// RemoveKeys removes the ids from the current list of key ids +func (r *Role) RemoveKeys(ids []string) { + r.KeyIDs = subtractStrSlices(r.KeyIDs, ids) +} + +// RemovePaths removes the paths from the current list of role paths +func (r *Role) RemovePaths(paths []string) { + r.Paths = subtractStrSlices(r.Paths, paths) +} + +func mergeStrSlices(orig, new []string) []string { + have := make(map[string]bool) + for _, e := range orig { + have[e] = true + } + merged := make([]string, len(orig), len(orig)+len(new)) + copy(merged, orig) + for _, e := range new { + if !have[e] { + merged = append(merged, e) + } + } + return merged +} + +func subtractStrSlices(orig, remove []string) []string { + kill := make(map[string]bool) + for _, e := range remove { + kill[e] = true + } + var keep []string + for _, e := range orig { + if !kill[e] { + keep = append(keep, e) + } + } + return keep +} diff --git a/vendor/src/github.com/docker/notary/tuf/data/root.go b/vendor/src/github.com/docker/notary/tuf/data/root.go new file mode 100644 index 00000000..02030caa --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/data/root.go @@ -0,0 +1,161 @@ +package data + +import ( + "fmt" + "time" + + "github.com/docker/go/canonical/json" +) + +// SignedRoot is a fully unpacked root.json +type SignedRoot struct { + Signatures []Signature + Signed Root + Dirty bool +} + +// Root is the Signed component of a root.json +type Root struct { + Type string `json:"_type"` + Version int `json:"version"` + Expires time.Time `json:"expires"` + Keys Keys `json:"keys"` + Roles map[string]*RootRole `json:"roles"` + ConsistentSnapshot bool `json:"consistent_snapshot"` +} + +// isValidRootStructure returns an error, or nil, depending on whether the content of the struct +// is valid for root metadata. This does not check signatures or expiry, just that +// the metadata content is valid. +func isValidRootStructure(r Root) error { + expectedType := TUFTypes[CanonicalRootRole] + if r.Type != expectedType { + return ErrInvalidMetadata{ + role: CanonicalRootRole, msg: fmt.Sprintf("expected type %s, not %s", expectedType, r.Type)} + } + + // all the base roles MUST appear in the root.json - other roles are allowed, + // but other than the mirror role (not currently supported) are out of spec + for _, roleName := range BaseRoles { + roleObj, ok := r.Roles[roleName] + if !ok || roleObj == nil { + return ErrInvalidMetadata{ + role: CanonicalRootRole, msg: fmt.Sprintf("missing %s role specification", roleName)} + } + if err := isValidRootRoleStructure(CanonicalRootRole, roleName, *roleObj, r.Keys); err != nil { + return err + } + } + return nil +} + +func isValidRootRoleStructure(metaContainingRole, rootRoleName string, r RootRole, validKeys Keys) error { + if r.Threshold < 1 { + return ErrInvalidMetadata{ + role: metaContainingRole, + msg: fmt.Sprintf("invalid threshold specified for %s: %v ", rootRoleName, r.Threshold), + } + } + for _, keyID := range r.KeyIDs { + if _, ok := validKeys[keyID]; !ok { + return ErrInvalidMetadata{ + role: metaContainingRole, + msg: fmt.Sprintf("key ID %s specified in %s without corresponding key", keyID, rootRoleName), + } + } + } + return nil +} + +// NewRoot initializes a new SignedRoot with a set of keys, roles, and the consistent flag +func NewRoot(keys map[string]PublicKey, roles map[string]*RootRole, consistent bool) (*SignedRoot, error) { + signedRoot := &SignedRoot{ + Signatures: make([]Signature, 0), + Signed: Root{ + Type: TUFTypes[CanonicalRootRole], + Version: 0, + Expires: DefaultExpires(CanonicalRootRole), + Keys: keys, + Roles: roles, + ConsistentSnapshot: consistent, + }, + Dirty: true, + } + + return signedRoot, nil +} + +// BuildBaseRole returns a copy of a BaseRole using the information in this SignedRoot for the specified role name. +// Will error for invalid role name or key metadata within this SignedRoot +func (r SignedRoot) BuildBaseRole(roleName string) (BaseRole, error) { + roleData, ok := r.Signed.Roles[roleName] + if !ok { + return BaseRole{}, ErrInvalidRole{Role: roleName, Reason: "role not found in root file"} + } + // Get all public keys for the base role from TUF metadata + keyIDs := roleData.KeyIDs + pubKeys := make(map[string]PublicKey) + for _, keyID := range keyIDs { + pubKey, ok := r.Signed.Keys[keyID] + if !ok { + return BaseRole{}, ErrInvalidRole{ + Role: roleName, + Reason: fmt.Sprintf("key with ID %s was not found in root metadata", keyID), + } + } + pubKeys[keyID] = pubKey + } + + return BaseRole{ + Name: roleName, + Keys: pubKeys, + Threshold: roleData.Threshold, + }, nil +} + +// ToSigned partially serializes a SignedRoot for further signing +func (r SignedRoot) ToSigned() (*Signed, error) { + s, err := defaultSerializer.MarshalCanonical(r.Signed) + if err != nil { + return nil, err + } + // cast into a json.RawMessage + signed := json.RawMessage{} + err = signed.UnmarshalJSON(s) + if err != nil { + return nil, err + } + sigs := make([]Signature, len(r.Signatures)) + copy(sigs, r.Signatures) + return &Signed{ + Signatures: sigs, + Signed: &signed, + }, nil +} + +// MarshalJSON returns the serialized form of SignedRoot as bytes +func (r SignedRoot) MarshalJSON() ([]byte, error) { + signed, err := r.ToSigned() + if err != nil { + return nil, err + } + return defaultSerializer.Marshal(signed) +} + +// RootFromSigned fully unpacks a Signed object into a SignedRoot and ensures +// that it is a valid SignedRoot +func RootFromSigned(s *Signed) (*SignedRoot, error) { + r := Root{} + if err := defaultSerializer.Unmarshal(*s.Signed, &r); err != nil { + return nil, err + } + if err := isValidRootStructure(r); err != nil { + return nil, err + } + sigs := make([]Signature, len(s.Signatures)) + copy(sigs, s.Signatures) + return &SignedRoot{ + Signatures: sigs, + Signed: r, + }, nil +} diff --git a/vendor/src/github.com/docker/notary/tuf/data/serializer.go b/vendor/src/github.com/docker/notary/tuf/data/serializer.go new file mode 100644 index 00000000..5c33d129 --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/data/serializer.go @@ -0,0 +1,36 @@ +package data + +import "github.com/docker/go/canonical/json" + +// Serializer is an interface that can marshal and unmarshal TUF data. This +// is expected to be a canonical JSON marshaller +type serializer interface { + MarshalCanonical(from interface{}) ([]byte, error) + Marshal(from interface{}) ([]byte, error) + Unmarshal(from []byte, to interface{}) error +} + +// CanonicalJSON marshals to and from canonical JSON +type canonicalJSON struct{} + +// MarshalCanonical returns the canonical JSON form of a thing +func (c canonicalJSON) MarshalCanonical(from interface{}) ([]byte, error) { + return json.MarshalCanonical(from) +} + +// Marshal returns the regular non-canonical JSON form of a thing +func (c canonicalJSON) Marshal(from interface{}) ([]byte, error) { + return json.Marshal(from) +} + +// Unmarshal unmarshals some JSON bytes +func (c canonicalJSON) Unmarshal(from []byte, to interface{}) error { + return json.Unmarshal(from, to) +} + +// defaultSerializer is a canonical JSON serializer +var defaultSerializer serializer = canonicalJSON{} + +func setDefaultSerializer(s serializer) { + defaultSerializer = s +} diff --git a/vendor/src/github.com/docker/notary/tuf/data/snapshot.go b/vendor/src/github.com/docker/notary/tuf/data/snapshot.go new file mode 100644 index 00000000..0ed2a7e8 --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/data/snapshot.go @@ -0,0 +1,163 @@ +package data + +import ( + "bytes" + "fmt" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/go/canonical/json" + "github.com/docker/notary" +) + +// SignedSnapshot is a fully unpacked snapshot.json +type SignedSnapshot struct { + Signatures []Signature + Signed Snapshot + Dirty bool +} + +// Snapshot is the Signed component of a snapshot.json +type Snapshot struct { + Type string `json:"_type"` + Version int `json:"version"` + Expires time.Time `json:"expires"` + Meta Files `json:"meta"` +} + +// isValidSnapshotStructure returns an error, or nil, depending on whether the content of the +// struct is valid for snapshot metadata. This does not check signatures or expiry, just that +// the metadata content is valid. +func isValidSnapshotStructure(s Snapshot) error { + expectedType := TUFTypes[CanonicalSnapshotRole] + if s.Type != expectedType { + return ErrInvalidMetadata{ + role: CanonicalSnapshotRole, msg: fmt.Sprintf("expected type %s, not %s", expectedType, s.Type)} + } + + for _, role := range []string{CanonicalRootRole, CanonicalTargetsRole} { + // Meta is a map of FileMeta, so if the role isn't in the map it returns + // an empty FileMeta, which has an empty map, and you can check on keys + // from an empty map. + // + // For now sha256 is required and sha512 is not. + if _, ok := s.Meta[role].Hashes[notary.SHA256]; !ok { + return ErrInvalidMetadata{ + role: CanonicalSnapshotRole, + msg: fmt.Sprintf("missing %s sha256 checksum information", role), + } + } + if err := CheckValidHashStructures(s.Meta[role].Hashes); err != nil { + return ErrInvalidMetadata{ + role: CanonicalSnapshotRole, + msg: fmt.Sprintf("invalid %s checksum information, %v", role, err), + } + } + } + return nil +} + +// NewSnapshot initilizes a SignedSnapshot with a given top level root +// and targets objects +func NewSnapshot(root *Signed, targets *Signed) (*SignedSnapshot, error) { + logrus.Debug("generating new snapshot...") + targetsJSON, err := json.Marshal(targets) + if err != nil { + logrus.Debug("Error Marshalling Targets") + return nil, err + } + rootJSON, err := json.Marshal(root) + if err != nil { + logrus.Debug("Error Marshalling Root") + return nil, err + } + rootMeta, err := NewFileMeta(bytes.NewReader(rootJSON), NotaryDefaultHashes...) + if err != nil { + return nil, err + } + targetsMeta, err := NewFileMeta(bytes.NewReader(targetsJSON), NotaryDefaultHashes...) + if err != nil { + return nil, err + } + return &SignedSnapshot{ + Signatures: make([]Signature, 0), + Signed: Snapshot{ + Type: TUFTypes["snapshot"], + Version: 0, + Expires: DefaultExpires("snapshot"), + Meta: Files{ + CanonicalRootRole: rootMeta, + CanonicalTargetsRole: targetsMeta, + }, + }, + }, nil +} + +// ToSigned partially serializes a SignedSnapshot for further signing +func (sp *SignedSnapshot) ToSigned() (*Signed, error) { + s, err := defaultSerializer.MarshalCanonical(sp.Signed) + if err != nil { + return nil, err + } + signed := json.RawMessage{} + err = signed.UnmarshalJSON(s) + if err != nil { + return nil, err + } + sigs := make([]Signature, len(sp.Signatures)) + copy(sigs, sp.Signatures) + return &Signed{ + Signatures: sigs, + Signed: &signed, + }, nil +} + +// AddMeta updates a role in the snapshot with new meta +func (sp *SignedSnapshot) AddMeta(role string, meta FileMeta) { + sp.Signed.Meta[role] = meta + sp.Dirty = true +} + +// GetMeta gets the metadata for a particular role, returning an error if it's +// not found +func (sp *SignedSnapshot) GetMeta(role string) (*FileMeta, error) { + if meta, ok := sp.Signed.Meta[role]; ok { + return &meta, nil + } + return nil, ErrMissingMeta{Role: role} +} + +// DeleteMeta removes a role from the snapshot. If the role doesn't +// exist in the snapshot, it's a noop. +func (sp *SignedSnapshot) DeleteMeta(role string) { + if _, ok := sp.Signed.Meta[role]; ok { + delete(sp.Signed.Meta, role) + sp.Dirty = true + } +} + +// MarshalJSON returns the serialized form of SignedSnapshot as bytes +func (sp *SignedSnapshot) MarshalJSON() ([]byte, error) { + signed, err := sp.ToSigned() + if err != nil { + return nil, err + } + return defaultSerializer.Marshal(signed) +} + +// SnapshotFromSigned fully unpacks a Signed object into a SignedSnapshot +func SnapshotFromSigned(s *Signed) (*SignedSnapshot, error) { + sp := Snapshot{} + if err := defaultSerializer.Unmarshal(*s.Signed, &sp); err != nil { + return nil, err + } + if err := isValidSnapshotStructure(sp); err != nil { + return nil, err + } + sigs := make([]Signature, len(s.Signatures)) + copy(sigs, s.Signatures) + return &SignedSnapshot{ + Signatures: sigs, + Signed: sp, + }, nil +} diff --git a/vendor/src/github.com/docker/notary/tuf/data/targets.go b/vendor/src/github.com/docker/notary/tuf/data/targets.go new file mode 100644 index 00000000..43468d40 --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/data/targets.go @@ -0,0 +1,194 @@ +package data + +import ( + "errors" + "fmt" + "path" + + "github.com/docker/go/canonical/json" +) + +// SignedTargets is a fully unpacked targets.json, or target delegation +// json file +type SignedTargets struct { + Signatures []Signature + Signed Targets + Dirty bool +} + +// Targets is the Signed components of a targets.json or delegation json file +type Targets struct { + SignedCommon + Targets Files `json:"targets"` + Delegations Delegations `json:"delegations,omitempty"` +} + +// isValidTargetsStructure returns an error, or nil, depending on whether the content of the struct +// is valid for targets metadata. This does not check signatures or expiry, just that +// the metadata content is valid. +func isValidTargetsStructure(t Targets, roleName string) error { + if roleName != CanonicalTargetsRole && !IsDelegation(roleName) { + return ErrInvalidRole{Role: roleName} + } + + // even if it's a delegated role, the metadata type is "Targets" + expectedType := TUFTypes[CanonicalTargetsRole] + if t.Type != expectedType { + return ErrInvalidMetadata{ + role: roleName, msg: fmt.Sprintf("expected type %s, not %s", expectedType, t.Type)} + } + + for _, roleObj := range t.Delegations.Roles { + if !IsDelegation(roleObj.Name) || path.Dir(roleObj.Name) != roleName { + return ErrInvalidMetadata{ + role: roleName, msg: fmt.Sprintf("delegation role %s invalid", roleObj.Name)} + } + if err := isValidRootRoleStructure(roleName, roleObj.Name, roleObj.RootRole, t.Delegations.Keys); err != nil { + return err + } + } + return nil +} + +// NewTargets intiializes a new empty SignedTargets object +func NewTargets() *SignedTargets { + return &SignedTargets{ + Signatures: make([]Signature, 0), + Signed: Targets{ + SignedCommon: SignedCommon{ + Type: TUFTypes["targets"], + Version: 0, + Expires: DefaultExpires("targets"), + }, + Targets: make(Files), + Delegations: *NewDelegations(), + }, + Dirty: true, + } +} + +// GetMeta attempts to find the targets entry for the path. It +// will return nil in the case of the target not being found. +func (t SignedTargets) GetMeta(path string) *FileMeta { + for p, meta := range t.Signed.Targets { + if p == path { + return &meta + } + } + return nil +} + +// GetValidDelegations filters the delegation roles specified in the signed targets, and +// only returns roles that are direct children and restricts their paths +func (t SignedTargets) GetValidDelegations(parent DelegationRole) []DelegationRole { + roles := t.buildDelegationRoles() + result := []DelegationRole{} + for _, r := range roles { + validRole, err := parent.Restrict(r) + if err != nil { + continue + } + result = append(result, validRole) + } + return result +} + +// BuildDelegationRole returns a copy of a DelegationRole using the information in this SignedTargets for the specified role name. +// Will error for invalid role name or key metadata within this SignedTargets. Path data is not validated. +func (t *SignedTargets) BuildDelegationRole(roleName string) (DelegationRole, error) { + for _, role := range t.Signed.Delegations.Roles { + if role.Name == roleName { + pubKeys := make(map[string]PublicKey) + for _, keyID := range role.KeyIDs { + pubKey, ok := t.Signed.Delegations.Keys[keyID] + if !ok { + // Couldn't retrieve all keys, so stop walking and return invalid role + return DelegationRole{}, ErrInvalidRole{Role: roleName, Reason: "delegation does not exist with all specified keys"} + } + pubKeys[keyID] = pubKey + } + return DelegationRole{ + BaseRole: BaseRole{ + Name: role.Name, + Keys: pubKeys, + Threshold: role.Threshold, + }, + Paths: role.Paths, + }, nil + } + } + return DelegationRole{}, ErrNoSuchRole{Role: roleName} +} + +// helper function to create DelegationRole structures from all delegations in a SignedTargets, +// these delegations are read directly from the SignedTargets and not modified or validated +func (t SignedTargets) buildDelegationRoles() []DelegationRole { + var roles []DelegationRole + for _, roleData := range t.Signed.Delegations.Roles { + delgRole, err := t.BuildDelegationRole(roleData.Name) + if err != nil { + continue + } + roles = append(roles, delgRole) + } + return roles +} + +// AddTarget adds or updates the meta for the given path +func (t *SignedTargets) AddTarget(path string, meta FileMeta) { + t.Signed.Targets[path] = meta + t.Dirty = true +} + +// AddDelegation will add a new delegated role with the given keys, +// ensuring the keys either already exist, or are added to the map +// of delegation keys +func (t *SignedTargets) AddDelegation(role *Role, keys []*PublicKey) error { + return errors.New("Not Implemented") +} + +// ToSigned partially serializes a SignedTargets for further signing +func (t *SignedTargets) ToSigned() (*Signed, error) { + s, err := defaultSerializer.MarshalCanonical(t.Signed) + if err != nil { + return nil, err + } + signed := json.RawMessage{} + err = signed.UnmarshalJSON(s) + if err != nil { + return nil, err + } + sigs := make([]Signature, len(t.Signatures)) + copy(sigs, t.Signatures) + return &Signed{ + Signatures: sigs, + Signed: &signed, + }, nil +} + +// MarshalJSON returns the serialized form of SignedTargets as bytes +func (t *SignedTargets) MarshalJSON() ([]byte, error) { + signed, err := t.ToSigned() + if err != nil { + return nil, err + } + return defaultSerializer.Marshal(signed) +} + +// TargetsFromSigned fully unpacks a Signed object into a SignedTargets, given +// a role name (so it can validate the SignedTargets object) +func TargetsFromSigned(s *Signed, roleName string) (*SignedTargets, error) { + t := Targets{} + if err := defaultSerializer.Unmarshal(*s.Signed, &t); err != nil { + return nil, err + } + if err := isValidTargetsStructure(t, roleName); err != nil { + return nil, err + } + sigs := make([]Signature, len(s.Signatures)) + copy(sigs, s.Signatures) + return &SignedTargets{ + Signatures: sigs, + Signed: t, + }, nil +} diff --git a/vendor/src/github.com/docker/notary/tuf/data/timestamp.go b/vendor/src/github.com/docker/notary/tuf/data/timestamp.go new file mode 100644 index 00000000..cc75e0e7 --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/data/timestamp.go @@ -0,0 +1,132 @@ +package data + +import ( + "bytes" + "fmt" + "time" + + "github.com/docker/go/canonical/json" + "github.com/docker/notary" +) + +// SignedTimestamp is a fully unpacked timestamp.json +type SignedTimestamp struct { + Signatures []Signature + Signed Timestamp + Dirty bool +} + +// Timestamp is the Signed component of a timestamp.json +type Timestamp struct { + Type string `json:"_type"` + Version int `json:"version"` + Expires time.Time `json:"expires"` + Meta Files `json:"meta"` +} + +// isValidTimestampStructure returns an error, or nil, depending on whether the content of the struct +// is valid for timestamp metadata. This does not check signatures or expiry, just that +// the metadata content is valid. +func isValidTimestampStructure(t Timestamp) error { + expectedType := TUFTypes[CanonicalTimestampRole] + if t.Type != expectedType { + return ErrInvalidMetadata{ + role: CanonicalTimestampRole, msg: fmt.Sprintf("expected type %s, not %s", expectedType, t.Type)} + } + + // Meta is a map of FileMeta, so if the role isn't in the map it returns + // an empty FileMeta, which has an empty map, and you can check on keys + // from an empty map. + // + // For now sha256 is required and sha512 is not. + if _, ok := t.Meta[CanonicalSnapshotRole].Hashes[notary.SHA256]; !ok { + return ErrInvalidMetadata{ + role: CanonicalTimestampRole, msg: "missing snapshot sha256 checksum information"} + } + if err := CheckValidHashStructures(t.Meta[CanonicalSnapshotRole].Hashes); err != nil { + return ErrInvalidMetadata{ + role: CanonicalTimestampRole, msg: fmt.Sprintf("invalid snapshot checksum information, %v", err)} + } + + return nil +} + +// NewTimestamp initializes a timestamp with an existing snapshot +func NewTimestamp(snapshot *Signed) (*SignedTimestamp, error) { + snapshotJSON, err := json.Marshal(snapshot) + if err != nil { + return nil, err + } + snapshotMeta, err := NewFileMeta(bytes.NewReader(snapshotJSON), NotaryDefaultHashes...) + if err != nil { + return nil, err + } + return &SignedTimestamp{ + Signatures: make([]Signature, 0), + Signed: Timestamp{ + Type: TUFTypes["timestamp"], + Version: 0, + Expires: DefaultExpires("timestamp"), + Meta: Files{ + CanonicalSnapshotRole: snapshotMeta, + }, + }, + }, nil +} + +// ToSigned partially serializes a SignedTimestamp such that it can +// be signed +func (ts *SignedTimestamp) ToSigned() (*Signed, error) { + s, err := defaultSerializer.MarshalCanonical(ts.Signed) + if err != nil { + return nil, err + } + signed := json.RawMessage{} + err = signed.UnmarshalJSON(s) + if err != nil { + return nil, err + } + sigs := make([]Signature, len(ts.Signatures)) + copy(sigs, ts.Signatures) + return &Signed{ + Signatures: sigs, + Signed: &signed, + }, nil +} + +// GetSnapshot gets the expected snapshot metadata hashes in the timestamp metadata, +// or nil if it doesn't exist +func (ts *SignedTimestamp) GetSnapshot() (*FileMeta, error) { + snapshotExpected, ok := ts.Signed.Meta[CanonicalSnapshotRole] + if !ok { + return nil, ErrMissingMeta{Role: CanonicalSnapshotRole} + } + return &snapshotExpected, nil +} + +// MarshalJSON returns the serialized form of SignedTimestamp as bytes +func (ts *SignedTimestamp) MarshalJSON() ([]byte, error) { + signed, err := ts.ToSigned() + if err != nil { + return nil, err + } + return defaultSerializer.Marshal(signed) +} + +// TimestampFromSigned parsed a Signed object into a fully unpacked +// SignedTimestamp +func TimestampFromSigned(s *Signed) (*SignedTimestamp, error) { + ts := Timestamp{} + if err := defaultSerializer.Unmarshal(*s.Signed, &ts); err != nil { + return nil, err + } + if err := isValidTimestampStructure(ts); err != nil { + return nil, err + } + sigs := make([]Signature, len(s.Signatures)) + copy(sigs, s.Signatures) + return &SignedTimestamp{ + Signatures: sigs, + Signed: ts, + }, nil +} diff --git a/vendor/src/github.com/docker/notary/tuf/data/types.go b/vendor/src/github.com/docker/notary/tuf/data/types.go new file mode 100644 index 00000000..2a61b3f5 --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/data/types.go @@ -0,0 +1,275 @@ +package data + +import ( + "crypto/sha256" + "crypto/sha512" + "crypto/subtle" + "fmt" + "hash" + "io" + "io/ioutil" + "strings" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/go/canonical/json" + "github.com/docker/notary" +) + +// SigAlgorithm for types of signatures +type SigAlgorithm string + +func (k SigAlgorithm) String() string { + return string(k) +} + +const defaultHashAlgorithm = "sha256" + +// Signature types +const ( + EDDSASignature SigAlgorithm = "eddsa" + RSAPSSSignature SigAlgorithm = "rsapss" + RSAPKCS1v15Signature SigAlgorithm = "rsapkcs1v15" + ECDSASignature SigAlgorithm = "ecdsa" + PyCryptoSignature SigAlgorithm = "pycrypto-pkcs#1 pss" +) + +// Key types +const ( + ED25519Key = "ed25519" + RSAKey = "rsa" + RSAx509Key = "rsa-x509" + ECDSAKey = "ecdsa" + ECDSAx509Key = "ecdsa-x509" +) + +// TUFTypes is the set of metadata types +var TUFTypes = map[string]string{ + CanonicalRootRole: "Root", + CanonicalTargetsRole: "Targets", + CanonicalSnapshotRole: "Snapshot", + CanonicalTimestampRole: "Timestamp", +} + +// SetTUFTypes allows one to override some or all of the default +// type names in TUF. +func SetTUFTypes(ts map[string]string) { + for k, v := range ts { + TUFTypes[k] = v + } +} + +// ValidTUFType checks if the given type is valid for the role +func ValidTUFType(typ, role string) bool { + if ValidRole(role) { + // All targets delegation roles must have + // the valid type is for targets. + if role == "" { + // role is unknown and does not map to + // a type + return false + } + if strings.HasPrefix(role, CanonicalTargetsRole+"/") { + role = CanonicalTargetsRole + } + } + // most people will just use the defaults so have this optimal check + // first. Do comparison just in case there is some unknown vulnerability + // if a key and value in the map differ. + if v, ok := TUFTypes[role]; ok { + return typ == v + } + return false +} + +// Signed is the high level, partially deserialized metadata object +// used to verify signatures before fully unpacking, or to add signatures +// before fully packing +type Signed struct { + Signed *json.RawMessage `json:"signed"` + Signatures []Signature `json:"signatures"` +} + +// SignedCommon contains the fields common to the Signed component of all +// TUF metadata files +type SignedCommon struct { + Type string `json:"_type"` + Expires time.Time `json:"expires"` + Version int `json:"version"` +} + +// SignedMeta is used in server validation where we only need signatures +// and common fields +type SignedMeta struct { + Signed SignedCommon `json:"signed"` + Signatures []Signature `json:"signatures"` +} + +// Signature is a signature on a piece of metadata +type Signature struct { + KeyID string `json:"keyid"` + Method SigAlgorithm `json:"method"` + Signature []byte `json:"sig"` +} + +// Files is the map of paths to file meta container in targets and delegations +// metadata files +type Files map[string]FileMeta + +// Hashes is the map of hash type to digest created for each metadata +// and target file +type Hashes map[string][]byte + +// NotaryDefaultHashes contains the default supported hash algorithms. +var NotaryDefaultHashes = []string{notary.SHA256, notary.SHA512} + +// FileMeta contains the size and hashes for a metadata or target file. Custom +// data can be optionally added. +type FileMeta struct { + Length int64 `json:"length"` + Hashes Hashes `json:"hashes"` + Custom *json.RawMessage `json:"custom,omitempty"` +} + +// CheckHashes verifies all the checksums specified by the "hashes" of the payload. +func CheckHashes(payload []byte, hashes Hashes) error { + cnt := 0 + + // k, v indicate the hash algorithm and the corresponding value + for k, v := range hashes { + switch k { + case notary.SHA256: + checksum := sha256.Sum256(payload) + if subtle.ConstantTimeCompare(checksum[:], v) == 0 { + return fmt.Errorf("%s checksum mismatched", k) + } + cnt++ + case notary.SHA512: + checksum := sha512.Sum512(payload) + if subtle.ConstantTimeCompare(checksum[:], v) == 0 { + return fmt.Errorf("%s checksum mismatched", k) + } + cnt++ + } + } + + if cnt == 0 { + return fmt.Errorf("at least one supported hash needed") + } + + return nil +} + +// CheckValidHashStructures returns an error, or nil, depending on whether +// the content of the hashes is valid or not. +func CheckValidHashStructures(hashes Hashes) error { + cnt := 0 + + for k, v := range hashes { + switch k { + case notary.SHA256: + if len(v) != sha256.Size { + return fmt.Errorf("invalid %s checksum", notary.SHA256) + } + cnt++ + case notary.SHA512: + if len(v) != sha512.Size { + return fmt.Errorf("invalid %s checksum", notary.SHA512) + } + cnt++ + } + } + + if cnt == 0 { + return fmt.Errorf("at least one supported hash needed") + } + + return nil +} + +// NewFileMeta generates a FileMeta object from the reader, using the +// hash algorithms provided +func NewFileMeta(r io.Reader, hashAlgorithms ...string) (FileMeta, error) { + if len(hashAlgorithms) == 0 { + hashAlgorithms = []string{defaultHashAlgorithm} + } + hashes := make(map[string]hash.Hash, len(hashAlgorithms)) + for _, hashAlgorithm := range hashAlgorithms { + var h hash.Hash + switch hashAlgorithm { + case notary.SHA256: + h = sha256.New() + case notary.SHA512: + h = sha512.New() + default: + return FileMeta{}, fmt.Errorf("Unknown hash algorithm: %s", hashAlgorithm) + } + hashes[hashAlgorithm] = h + r = io.TeeReader(r, h) + } + n, err := io.Copy(ioutil.Discard, r) + if err != nil { + return FileMeta{}, err + } + m := FileMeta{Length: n, Hashes: make(Hashes, len(hashes))} + for hashAlgorithm, h := range hashes { + m.Hashes[hashAlgorithm] = h.Sum(nil) + } + return m, nil +} + +// Delegations holds a tier of targets delegations +type Delegations struct { + Keys Keys `json:"keys"` + Roles []*Role `json:"roles"` +} + +// NewDelegations initializes an empty Delegations object +func NewDelegations() *Delegations { + return &Delegations{ + Keys: make(map[string]PublicKey), + Roles: make([]*Role, 0), + } +} + +// These values are recommended TUF expiry times. +var defaultExpiryTimes = map[string]time.Duration{ + CanonicalRootRole: notary.Year, + CanonicalTargetsRole: 90 * notary.Day, + CanonicalSnapshotRole: 7 * notary.Day, + CanonicalTimestampRole: notary.Day, +} + +// SetDefaultExpiryTimes allows one to change the default expiries. +func SetDefaultExpiryTimes(times map[string]time.Duration) { + for key, value := range times { + if _, ok := defaultExpiryTimes[key]; !ok { + logrus.Errorf("Attempted to set default expiry for an unknown role: %s", key) + continue + } + defaultExpiryTimes[key] = value + } +} + +// DefaultExpires gets the default expiry time for the given role +func DefaultExpires(role string) time.Time { + if d, ok := defaultExpiryTimes[role]; ok { + return time.Now().Add(d) + } + var t time.Time + return t.UTC().Round(time.Second) +} + +type unmarshalledSignature Signature + +// UnmarshalJSON does a custom unmarshalling of the signature JSON +func (s *Signature) UnmarshalJSON(data []byte) error { + uSignature := unmarshalledSignature{} + err := json.Unmarshal(data, &uSignature) + if err != nil { + return err + } + uSignature.Method = SigAlgorithm(strings.ToLower(string(uSignature.Method))) + *s = Signature(uSignature) + return nil +} diff --git a/vendor/src/github.com/docker/notary/tuf/signed/ed25519.go b/vendor/src/github.com/docker/notary/tuf/signed/ed25519.go new file mode 100644 index 00000000..bc884bdb --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/signed/ed25519.go @@ -0,0 +1,107 @@ +package signed + +import ( + "crypto/rand" + "errors" + + "github.com/docker/notary/trustmanager" + "github.com/docker/notary/tuf/data" +) + +type edCryptoKey struct { + role string + privKey data.PrivateKey +} + +// Ed25519 implements a simple in memory cryptosystem for ED25519 keys +type Ed25519 struct { + keys map[string]edCryptoKey +} + +// NewEd25519 initializes a new empty Ed25519 CryptoService that operates +// entirely in memory +func NewEd25519() *Ed25519 { + return &Ed25519{ + make(map[string]edCryptoKey), + } +} + +// AddKey allows you to add a private key +func (e *Ed25519) AddKey(role, gun string, k data.PrivateKey) error { + e.addKey(role, k) + return nil +} + +// addKey allows you to add a private key +func (e *Ed25519) addKey(role string, k data.PrivateKey) { + e.keys[k.ID()] = edCryptoKey{ + role: role, + privKey: k, + } +} + +// RemoveKey deletes a key from the signer +func (e *Ed25519) RemoveKey(keyID string) error { + delete(e.keys, keyID) + return nil +} + +// ListKeys returns the list of keys IDs for the role +func (e *Ed25519) ListKeys(role string) []string { + keyIDs := make([]string, 0, len(e.keys)) + for id, edCryptoKey := range e.keys { + if edCryptoKey.role == role { + keyIDs = append(keyIDs, id) + } + } + return keyIDs +} + +// ListAllKeys returns the map of keys IDs to role +func (e *Ed25519) ListAllKeys() map[string]string { + keys := make(map[string]string) + for id, edKey := range e.keys { + keys[id] = edKey.role + } + return keys +} + +// Create generates a new key and returns the public part +func (e *Ed25519) Create(role, gun, algorithm string) (data.PublicKey, error) { + if algorithm != data.ED25519Key { + return nil, errors.New("only ED25519 supported by this cryptoservice") + } + + private, err := trustmanager.GenerateED25519Key(rand.Reader) + if err != nil { + return nil, err + } + + e.addKey(role, private) + return data.PublicKeyFromPrivate(private), nil +} + +// PublicKeys returns a map of public keys for the ids provided, when those IDs are found +// in the store. +func (e *Ed25519) PublicKeys(keyIDs ...string) (map[string]data.PublicKey, error) { + k := make(map[string]data.PublicKey) + for _, keyID := range keyIDs { + if edKey, ok := e.keys[keyID]; ok { + k[keyID] = data.PublicKeyFromPrivate(edKey.privKey) + } + } + return k, nil +} + +// GetKey returns a single public key based on the ID +func (e *Ed25519) GetKey(keyID string) data.PublicKey { + return data.PublicKeyFromPrivate(e.keys[keyID].privKey) +} + +// GetPrivateKey returns a single private key and role if present, based on the ID +func (e *Ed25519) GetPrivateKey(keyID string) (data.PrivateKey, string, error) { + if k, ok := e.keys[keyID]; ok { + return k.privKey, k.role, nil + } + return nil, "", trustmanager.ErrKeyNotFound{KeyID: keyID} +} diff --git a/vendor/src/github.com/docker/notary/tuf/signed/errors.go b/vendor/src/github.com/docker/notary/tuf/signed/errors.go new file mode 100644 index 00000000..2c5174be --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/signed/errors.go @@ -0,0 +1,72 @@ +package signed + +import ( + "fmt" + "strings" +) + +// ErrInsufficientSignatures - do not have enough signatures on a piece of +// metadata +type ErrInsufficientSignatures struct { + Name string +} + +func (e ErrInsufficientSignatures) Error() string { + return fmt.Sprintf("tuf: insufficient signatures: %s", e.Name) +} + +// ErrExpired indicates a piece of metadata has expired +type ErrExpired struct { + Role string + Expired string +} + +func (e ErrExpired) Error() string { + return fmt.Sprintf("%s expired at %v", e.Role, e.Expired) +} + +// ErrLowVersion indicates the piece of metadata has a version number lower than +// a version number we're already seen for this role +type ErrLowVersion struct { + Actual int + Current int +} + +func (e ErrLowVersion) Error() string { + return fmt.Sprintf("version %d is lower than current version %d", e.Actual, e.Current) +} + +// ErrRoleThreshold indicates we did not validate enough signatures to meet the threshold +type ErrRoleThreshold struct{} + +func (e ErrRoleThreshold) Error() string { + return "valid signatures did not meet threshold" +} + +// ErrInvalidKeyType indicates the types for the key and signature it's associated with are +// mismatched. Probably a sign of malicious behaviour +type ErrInvalidKeyType struct{} + +func (e ErrInvalidKeyType) Error() string { + return "key type is not valid for signature" +} + +// ErrInvalidKeyLength indicates that while we may support the cipher, the provided +// key length is not specifically supported, i.e. we support RSA, but not 1024 bit keys +type ErrInvalidKeyLength struct { + msg string +} + +func (e ErrInvalidKeyLength) Error() string { + return fmt.Sprintf("key length is not supported: %s", e.msg) +} + +// ErrNoKeys indicates no signing keys were found when trying to sign +type ErrNoKeys struct { + KeyIDs []string +} + +func (e ErrNoKeys) Error() string { + return fmt.Sprintf("could not find necessary signing keys, at least one of these keys must be available: %s", + strings.Join(e.KeyIDs, ", ")) +} diff --git a/vendor/src/github.com/docker/notary/tuf/signed/interface.go b/vendor/src/github.com/docker/notary/tuf/signed/interface.go new file mode 100644 index 00000000..2d6027e9 --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/signed/interface.go @@ -0,0 +1,46 @@ +package signed + +import ( + "github.com/docker/notary/tuf/data" +) + +// KeyService provides management of keys locally. It will never +// accept or provide private keys. Communication between the KeyService +// and a SigningService happen behind the Create function. +type KeyService interface { + // Create issues a new key pair and is responsible for loading + // the private key into the appropriate signing service. + Create(role, gun, algorithm string) (data.PublicKey, error) + + // AddKey adds a private key to the specified role and gun + AddKey(role, gun string, key data.PrivateKey) error + + // GetKey retrieves the public key if present, otherwise it returns nil + GetKey(keyID string) data.PublicKey + + // GetPrivateKey retrieves the private key and role if present, otherwise + // it returns nil + GetPrivateKey(keyID string) (data.PrivateKey, string, error) + + // RemoveKey deletes the specified key + RemoveKey(keyID string) error + + // ListKeys returns a list of key IDs for the role + ListKeys(role string) []string + + // ListAllKeys returns a map of all available signing key IDs to role + ListAllKeys() map[string]string +} + +// CryptoService is deprecated and all instances of its use should be +// replaced with KeyService +type CryptoService interface { + KeyService +} + +// Verifier defines an interface for verfying signatures. An implementer +// of this interface should verify signatures for one and only one +// signing scheme. +type Verifier interface { + Verify(key data.PublicKey, sig []byte, msg []byte) error +} diff --git a/vendor/src/github.com/docker/notary/tuf/signed/sign.go b/vendor/src/github.com/docker/notary/tuf/signed/sign.go new file mode 100644 index 00000000..52e716e0 --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/signed/sign.go @@ -0,0 +1,103 @@ +package signed + +// The Sign function is a choke point for all code paths that do signing. +// We use this fact to do key ID translation. There are 2 types of key ID: +// - Scoped: the key ID based purely on the data that appears in the TUF +// files. This may be wrapped by a certificate that scopes the +// key to be used in a specific context. +// - Canonical: the key ID based purely on the public key bytes. This is +// used by keystores to easily identify keys that may be reused +// in many scoped locations. +// Currently these types only differ in the context of Root Keys in Notary +// for which the root key is wrapped using an x509 certificate. + +import ( + "crypto/rand" + "fmt" + + "github.com/Sirupsen/logrus" + "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/utils" +) + +// Sign takes a data.Signed and a key, calculated and adds the signature +// to the data.Signed +// N.B. All public keys for a role should be passed so that this function +// can correctly clean up signatures that are no longer valid. +func Sign(service CryptoService, s *data.Signed, keys ...data.PublicKey) error { + logrus.Debugf("sign called with %d keys", len(keys)) + signatures := make([]data.Signature, 0, len(s.Signatures)+1) + signingKeyIDs := make(map[string]struct{}) + tufIDs := make(map[string]data.PublicKey) + ids := make([]string, 0, len(keys)) + + privKeys := make(map[string]data.PrivateKey) + + // Get all the private key objects related to the public keys + for _, key := range keys { + canonicalID, err := utils.CanonicalKeyID(key) + ids = append(ids, canonicalID) + tufIDs[key.ID()] = key + if err != nil { + continue + } + k, _, err := service.GetPrivateKey(canonicalID) + if err != nil { + continue + } + privKeys[key.ID()] = k + } + + // Check to ensure we have at least one signing key + if len(privKeys) == 0 { + return ErrNoKeys{KeyIDs: ids} + } + + // Do signing and generate list of signatures + for keyID, pk := range privKeys { + sig, err := pk.Sign(rand.Reader, *s.Signed, nil) + if err != nil { + logrus.Debugf("Failed to sign with key: %s. Reason: %v", keyID, err) + continue + } + signingKeyIDs[keyID] = struct{}{} + signatures = append(signatures, data.Signature{ + KeyID: keyID, + Method: pk.SignatureAlgorithm(), + Signature: sig[:], + }) + } + + // Check we produced at least on signature + if len(signatures) < 1 { + return ErrInsufficientSignatures{ + Name: fmt.Sprintf( + "cryptoservice failed to produce any signatures for keys with IDs: %v", + ids), + } + } + + for _, sig := range s.Signatures { + if _, ok := signingKeyIDs[sig.KeyID]; ok { + // key is in the set of key IDs for which a signature has been created + continue + } + var ( + k data.PublicKey + ok bool + ) + if k, ok = tufIDs[sig.KeyID]; !ok { + // key is no longer a valid signing key + continue + } + if err := VerifySignature(*s.Signed, sig, k); err != nil { + // signature is no longer valid + continue + } + // keep any signatures that still represent valid keys and are + // themselves valid + signatures = append(signatures, sig) + } + s.Signatures = signatures + return nil +} diff --git a/vendor/src/github.com/docker/notary/tuf/signed/verifiers.go b/vendor/src/github.com/docker/notary/tuf/signed/verifiers.go new file mode 100644 index 00000000..79218636 --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/signed/verifiers.go @@ -0,0 +1,283 @@ +package signed + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/pem" + "fmt" + "math/big" + "reflect" + + "github.com/Sirupsen/logrus" + "github.com/agl/ed25519" + "github.com/docker/notary/tuf/data" +) + +const ( + minRSAKeySizeBit = 2048 // 2048 bits = 256 bytes + minRSAKeySizeByte = minRSAKeySizeBit / 8 +) + +// Verifiers serves as a map of all verifiers available on the system and +// can be injected into a verificationService. For testing and configuration +// purposes, it will not be used by default. +var Verifiers = map[data.SigAlgorithm]Verifier{ + data.RSAPSSSignature: RSAPSSVerifier{}, + data.RSAPKCS1v15Signature: RSAPKCS1v15Verifier{}, + data.PyCryptoSignature: RSAPyCryptoVerifier{}, + data.ECDSASignature: ECDSAVerifier{}, + data.EDDSASignature: Ed25519Verifier{}, +} + +// RegisterVerifier provides a convenience function for init() functions +// to register additional verifiers or replace existing ones. +func RegisterVerifier(algorithm data.SigAlgorithm, v Verifier) { + curr, ok := Verifiers[algorithm] + if ok { + typOld := reflect.TypeOf(curr) + typNew := reflect.TypeOf(v) + logrus.Debugf( + "replacing already loaded verifier %s:%s with %s:%s", + typOld.PkgPath(), typOld.Name(), + typNew.PkgPath(), typNew.Name(), + ) + } else { + logrus.Debug("adding verifier for: ", algorithm) + } + Verifiers[algorithm] = v +} + +// Ed25519Verifier used to verify Ed25519 signatures +type Ed25519Verifier struct{} + +// Verify checks that an ed25519 signature is valid +func (v Ed25519Verifier) Verify(key data.PublicKey, sig []byte, msg []byte) error { + if key.Algorithm() != data.ED25519Key { + return ErrInvalidKeyType{} + } + var sigBytes [ed25519.SignatureSize]byte + if len(sig) != ed25519.SignatureSize { + logrus.Debugf("signature length is incorrect, must be %d, was %d.", ed25519.SignatureSize, len(sig)) + return ErrInvalid + } + copy(sigBytes[:], sig) + + var keyBytes [ed25519.PublicKeySize]byte + pub := key.Public() + if len(pub) != ed25519.PublicKeySize { + logrus.Errorf("public key is incorrect size, must be %d, was %d.", ed25519.PublicKeySize, len(pub)) + return ErrInvalidKeyLength{msg: fmt.Sprintf("ed25519 public key must be %d bytes.", ed25519.PublicKeySize)} + } + n := copy(keyBytes[:], key.Public()) + if n < ed25519.PublicKeySize { + logrus.Errorf("failed to copy the key, must have %d bytes, copied %d bytes.", ed25519.PublicKeySize, n) + return ErrInvalid + } + + if !ed25519.Verify(&keyBytes, msg, &sigBytes) { + logrus.Debugf("failed ed25519 verification") + return ErrInvalid + } + return nil +} + +func verifyPSS(key interface{}, digest, sig []byte) error { + rsaPub, ok := key.(*rsa.PublicKey) + if !ok { + logrus.Debugf("value was not an RSA public key") + return ErrInvalid + } + + if rsaPub.N.BitLen() < minRSAKeySizeBit { + logrus.Debugf("RSA keys less than 2048 bits are not acceptable, provided key has length %d.", rsaPub.N.BitLen()) + return ErrInvalidKeyLength{msg: fmt.Sprintf("RSA key must be at least %d bits.", minRSAKeySizeBit)} + } + + if len(sig) < minRSAKeySizeByte { + logrus.Debugf("RSA keys less than 2048 bits are not acceptable, provided signature has length %d.", len(sig)) + return ErrInvalid + } + + opts := rsa.PSSOptions{SaltLength: sha256.Size, Hash: crypto.SHA256} + if err := rsa.VerifyPSS(rsaPub, crypto.SHA256, digest[:], sig, &opts); err != nil { + logrus.Debugf("failed RSAPSS verification: %s", err) + return ErrInvalid + } + return nil +} + +func getRSAPubKey(key data.PublicKey) (crypto.PublicKey, error) { + algorithm := key.Algorithm() + var pubKey crypto.PublicKey + + switch algorithm { + case data.RSAx509Key: + pemCert, _ := pem.Decode([]byte(key.Public())) + if pemCert == nil { + logrus.Debugf("failed to decode PEM-encoded x509 certificate") + return nil, ErrInvalid + } + cert, err := x509.ParseCertificate(pemCert.Bytes) + if err != nil { + logrus.Debugf("failed to parse x509 certificate: %s\n", err) + return nil, ErrInvalid + } + pubKey = cert.PublicKey + case data.RSAKey: + var err error + pubKey, err = x509.ParsePKIXPublicKey(key.Public()) + if err != nil { + logrus.Debugf("failed to parse public key: %s\n", err) + return nil, ErrInvalid + } + default: + // only accept RSA keys + logrus.Debugf("invalid key type for RSAPSS verifier: %s", algorithm) + return nil, ErrInvalidKeyType{} + } + + return pubKey, nil +} + +// RSAPSSVerifier checks RSASSA-PSS signatures +type RSAPSSVerifier struct{} + +// Verify does the actual check. +func (v RSAPSSVerifier) Verify(key data.PublicKey, sig []byte, msg []byte) error { + // will return err if keytype is not a recognized RSA type + pubKey, err := getRSAPubKey(key) + if err != nil { + return err + } + + digest := sha256.Sum256(msg) + + return verifyPSS(pubKey, digest[:], sig) +} + +// RSAPKCS1v15Verifier checks RSA PKCS1v15 signatures +type RSAPKCS1v15Verifier struct{} + +// Verify does the actual verification +func (v RSAPKCS1v15Verifier) Verify(key data.PublicKey, sig []byte, msg []byte) error { + // will return err if keytype is not a recognized RSA type + pubKey, err := getRSAPubKey(key) + if err != nil { + return err + } + digest := sha256.Sum256(msg) + + rsaPub, ok := pubKey.(*rsa.PublicKey) + if !ok { + logrus.Debugf("value was not an RSA public key") + return ErrInvalid + } + + if rsaPub.N.BitLen() < minRSAKeySizeBit { + logrus.Debugf("RSA keys less than 2048 bits are not acceptable, provided key has length %d.", rsaPub.N.BitLen()) + return ErrInvalidKeyLength{msg: fmt.Sprintf("RSA key must be at least %d bits.", minRSAKeySizeBit)} + } + + if len(sig) < minRSAKeySizeByte { + logrus.Debugf("RSA keys less than 2048 bits are not acceptable, provided signature has length %d.", len(sig)) + return ErrInvalid + } + + if err = rsa.VerifyPKCS1v15(rsaPub, crypto.SHA256, digest[:], sig); err != nil { + logrus.Errorf("Failed verification: %s", err.Error()) + return ErrInvalid + } + return nil +} + +// RSAPyCryptoVerifier checks RSASSA-PSS signatures +type RSAPyCryptoVerifier struct{} + +// Verify does the actual check. +// N.B. We have not been able to make this work in a way that is compatible +// with PyCrypto. +func (v RSAPyCryptoVerifier) Verify(key data.PublicKey, sig []byte, msg []byte) error { + digest := sha256.Sum256(msg) + if key.Algorithm() != data.RSAKey { + return ErrInvalidKeyType{} + } + + k, _ := pem.Decode([]byte(key.Public())) + if k == nil { + logrus.Debugf("failed to decode PEM-encoded x509 certificate") + return ErrInvalid + } + + pub, err := x509.ParsePKIXPublicKey(k.Bytes) + if err != nil { + logrus.Debugf("failed to parse public key: %s\n", err) + return ErrInvalid + } + + return verifyPSS(pub, digest[:], sig) +} + +// ECDSAVerifier checks ECDSA signatures, decoding the keyType appropriately +type ECDSAVerifier struct{} + +// Verify does the actual check. +func (v ECDSAVerifier) Verify(key data.PublicKey, sig []byte, msg []byte) error { + algorithm := key.Algorithm() + var pubKey crypto.PublicKey + + switch algorithm { + case data.ECDSAx509Key: + pemCert, _ := pem.Decode([]byte(key.Public())) + if pemCert == nil { + logrus.Debugf("failed to decode PEM-encoded x509 certificate for keyID: %s", key.ID()) + logrus.Debugf("certificate bytes: %s", string(key.Public())) + return ErrInvalid + } + cert, err := x509.ParseCertificate(pemCert.Bytes) + if err != nil { + logrus.Debugf("failed to parse x509 certificate: %s\n", err) + return ErrInvalid + } + pubKey = cert.PublicKey + case data.ECDSAKey: + var err error + pubKey, err = x509.ParsePKIXPublicKey(key.Public()) + if err != nil { + logrus.Debugf("Failed to parse private key for keyID: %s, %s\n", key.ID(), err) + return ErrInvalid + } + default: + // only accept ECDSA keys. + logrus.Debugf("invalid key type for ECDSA verifier: %s", algorithm) + return ErrInvalidKeyType{} + } + + ecdsaPubKey, ok := pubKey.(*ecdsa.PublicKey) + if !ok { + logrus.Debugf("value isn't an ECDSA public key") + return ErrInvalid + } + + sigLength := len(sig) + expectedOctetLength := 2 * ((ecdsaPubKey.Params().BitSize + 7) >> 3) + if sigLength != expectedOctetLength { + logrus.Debugf("signature had an unexpected length") + return ErrInvalid + } + + rBytes, sBytes := sig[:sigLength/2], sig[sigLength/2:] + r := new(big.Int).SetBytes(rBytes) + s := new(big.Int).SetBytes(sBytes) + + digest := sha256.Sum256(msg) + + if !ecdsa.Verify(ecdsaPubKey, digest[:], r, s) { + logrus.Debugf("failed ECDSA signature validation") + return ErrInvalid + } + + return nil +} diff --git a/vendor/src/github.com/docker/notary/tuf/signed/verify.go b/vendor/src/github.com/docker/notary/tuf/signed/verify.go new file mode 100644 index 00000000..7e34e259 --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/signed/verify.go @@ -0,0 +1,155 @@ +package signed + +import ( + "errors" + "fmt" + "strings" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/go/canonical/json" + "github.com/docker/notary/tuf/data" +) + +// Various basic signing errors +var ( + ErrMissingKey = errors.New("tuf: missing key") + ErrNoSignatures = errors.New("tuf: data has no signatures") + ErrInvalid = errors.New("tuf: signature verification failed") + ErrWrongMethod = errors.New("tuf: invalid signature type") + ErrUnknownRole = errors.New("tuf: unknown role") + ErrWrongType = errors.New("tuf: meta file has wrong type") +) + +// VerifyRoot checks if a given root file is valid against a known set of keys. +// Threshold is always assumed to be 1 +func VerifyRoot(s *data.Signed, minVersion int, keys map[string]data.PublicKey) error { + if len(s.Signatures) == 0 { + return ErrNoSignatures + } + + var decoded map[string]interface{} + if err := json.Unmarshal(*s.Signed, &decoded); err != nil { + return err + } + msg, err := json.MarshalCanonical(decoded) + if err != nil { + return err + } + + for _, sig := range s.Signatures { + // method lookup is consistent due to Unmarshal JSON doing lower case for us. + method := sig.Method + verifier, ok := Verifiers[method] + if !ok { + logrus.Debugf("continuing b/c signing method is not supported for verify root: %s\n", sig.Method) + continue + } + + key, ok := keys[sig.KeyID] + if !ok { + logrus.Debugf("continuing b/c signing key isn't present in keys: %s\n", sig.KeyID) + continue + } + + if err := verifier.Verify(key, sig.Signature, msg); err != nil { + logrus.Debugf("continuing b/c signature was invalid\n") + continue + } + // threshold of 1 so return on first success + return verifyMeta(s, data.CanonicalRootRole, minVersion) + } + return ErrRoleThreshold{} +} + +// Verify checks the signatures and metadata (expiry, version) for the signed role +// data +func Verify(s *data.Signed, role data.BaseRole, minVersion int) error { + if err := verifyMeta(s, role.Name, minVersion); err != nil { + return err + } + return VerifySignatures(s, role) +} + +func verifyMeta(s *data.Signed, role string, minVersion int) error { + sm := &data.SignedCommon{} + if err := json.Unmarshal(*s.Signed, sm); err != nil { + return err + } + if !data.ValidTUFType(sm.Type, role) { + return ErrWrongType + } + if IsExpired(sm.Expires) { + logrus.Errorf("Metadata for %s expired", role) + return ErrExpired{Role: role, Expired: sm.Expires.Format("Mon Jan 2 15:04:05 MST 2006")} + } + if sm.Version < minVersion { + return ErrLowVersion{sm.Version, minVersion} + } + + return nil +} + +// IsExpired checks if the given time passed before the present time +func IsExpired(t time.Time) bool { + return t.Before(time.Now()) +} + +// VerifySignatures checks the we have sufficient valid signatures for the given role +func VerifySignatures(s *data.Signed, roleData data.BaseRole) error { + if len(s.Signatures) == 0 { + return ErrNoSignatures + } + + if roleData.Threshold < 1 { + return ErrRoleThreshold{} + } + logrus.Debugf("%s role has key IDs: %s", roleData.Name, strings.Join(roleData.ListKeyIDs(), ",")) + + // remarshal the signed part so we can verify the signature, since the signature has + // to be of a canonically marshalled signed object + var decoded map[string]interface{} + if err := json.Unmarshal(*s.Signed, &decoded); err != nil { + return err + } + msg, err := json.MarshalCanonical(decoded) + if err != nil { + return err + } + + valid := make(map[string]struct{}) + for _, sig := range s.Signatures { + logrus.Debug("verifying signature for key ID: ", sig.KeyID) + key, ok := roleData.Keys[sig.KeyID] + if !ok { + logrus.Debugf("continuing b/c keyid lookup was nil: %s\n", sig.KeyID) + continue + } + if err := VerifySignature(msg, sig, key); err != nil { + logrus.Debugf("continuing b/c %s", err.Error()) + continue + } + valid[sig.KeyID] = struct{}{} + + } + if len(valid) < roleData.Threshold { + return ErrRoleThreshold{} + } + + return nil +} + +// VerifySignature checks a single signature and public key against a payload +func VerifySignature(msg []byte, sig data.Signature, pk data.PublicKey) error { + // method lookup is consistent due to Unmarshal JSON doing lower case for us. + method := sig.Method + verifier, ok := Verifiers[method] + if !ok { + return fmt.Errorf("signing method is not supported: %s\n", sig.Method) + } + + if err := verifier.Verify(pk, sig.Signature, msg); err != nil { + return fmt.Errorf("signature was invalid\n") + } + return nil +} diff --git a/vendor/src/github.com/docker/notary/tuf/store/errors.go b/vendor/src/github.com/docker/notary/tuf/store/errors.go new file mode 100644 index 00000000..a7f63d6b --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/store/errors.go @@ -0,0 +1,13 @@ +package store + +import "fmt" + +// ErrMetaNotFound indicates we did not find a particular piece +// of metadata in the store +type ErrMetaNotFound struct { + Resource string +} + +func (err ErrMetaNotFound) Error() string { + return fmt.Sprintf("%s trust data unavailable. Has a notary repository been initialized?", err.Resource) +} diff --git a/vendor/src/github.com/docker/notary/tuf/store/filestore.go b/vendor/src/github.com/docker/notary/tuf/store/filestore.go new file mode 100644 index 00000000..44401707 --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/store/filestore.go @@ -0,0 +1,101 @@ +package store + +import ( + "fmt" + "github.com/docker/notary" + "io/ioutil" + "os" + "path" + "path/filepath" +) + +// NewFilesystemStore creates a new store in a directory tree +func NewFilesystemStore(baseDir, metaSubDir, metaExtension string) (*FilesystemStore, error) { + metaDir := path.Join(baseDir, metaSubDir) + + // Make sure we can create the necessary dirs and they are writable + err := os.MkdirAll(metaDir, 0700) + if err != nil { + return nil, err + } + + return &FilesystemStore{ + baseDir: baseDir, + metaDir: metaDir, + metaExtension: metaExtension, + }, nil +} + +// FilesystemStore is a store in a locally accessible directory +type FilesystemStore struct { + baseDir string + metaDir string + metaExtension string +} + +func (f *FilesystemStore) getPath(name string) string { + fileName := fmt.Sprintf("%s.%s", name, f.metaExtension) + return filepath.Join(f.metaDir, fileName) +} + +// GetMeta returns the meta for the given name (a role) up to size bytes +// If size is -1, this corresponds to "infinite," but we cut off at 100MB +func (f *FilesystemStore) GetMeta(name string, size int64) ([]byte, error) { + meta, err := ioutil.ReadFile(f.getPath(name)) + if err != nil { + if os.IsNotExist(err) { + err = ErrMetaNotFound{Resource: name} + } + return nil, err + } + if size == -1 { + size = notary.MaxDownloadSize + } + // Only return up to size bytes + if int64(len(meta)) < size { + return meta, nil + } + return meta[:size], nil +} + +// SetMultiMeta sets the metadata for multiple roles in one operation +func (f *FilesystemStore) SetMultiMeta(metas map[string][]byte) error { + for role, blob := range metas { + err := f.SetMeta(role, blob) + if err != nil { + return err + } + } + return nil +} + +// SetMeta sets the meta for a single role +func (f *FilesystemStore) SetMeta(name string, meta []byte) error { + fp := f.getPath(name) + + // Ensures the parent directories of the file we are about to write exist + err := os.MkdirAll(filepath.Dir(fp), 0700) + if err != nil { + return err + } + + // if something already exists, just delete it and re-write it + os.RemoveAll(fp) + + // Write the file to disk + if err = ioutil.WriteFile(fp, meta, 0600); err != nil { + return err + } + return nil +} + +// RemoveAll clears the existing filestore by removing its base directory +func (f *FilesystemStore) RemoveAll() error { + return os.RemoveAll(f.baseDir) +} + +// RemoveMeta removes the metadata for a single role - if the metadata doesn't +// exist, no error is returned +func (f *FilesystemStore) RemoveMeta(name string) error { + return os.RemoveAll(f.getPath(name)) // RemoveAll succeeds if path doesn't exist +} diff --git a/vendor/src/github.com/docker/notary/tuf/store/httpstore.go b/vendor/src/github.com/docker/notary/tuf/store/httpstore.go new file mode 100644 index 00000000..8b0d8501 --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/store/httpstore.go @@ -0,0 +1,296 @@ +// A Store that can fetch and set metadata on a remote server. +// Some API constraints: +// - Response bodies for error codes should be unmarshallable as: +// {"errors": [{..., "detail": }]} +// else validation error details, etc. will be unparsable. The errors +// should have a github.com/docker/notary/tuf/validation/SerializableError +// in the Details field. +// If writing your own server, please have a look at +// github.com/docker/distribution/registry/api/errcode + +package store + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "net/url" + "path" + + "github.com/Sirupsen/logrus" + "github.com/docker/notary" + "github.com/docker/notary/tuf/validation" +) + +// ErrServerUnavailable indicates an error from the server. code allows us to +// populate the http error we received +type ErrServerUnavailable struct { + code int +} + +func (err ErrServerUnavailable) Error() string { + if err.code == 401 { + return fmt.Sprintf("you are not authorized to perform this operation: server returned 401.") + } + return fmt.Sprintf("unable to reach trust server at this time: %d.", err.code) +} + +// ErrMaliciousServer indicates the server returned a response that is highly suspected +// of being malicious. i.e. it attempted to send us more data than the known size of a +// particular role metadata. +type ErrMaliciousServer struct{} + +func (err ErrMaliciousServer) Error() string { + return "trust server returned a bad response." +} + +// ErrInvalidOperation indicates that the server returned a 400 response and +// propagate any body we received. +type ErrInvalidOperation struct { + msg string +} + +func (err ErrInvalidOperation) Error() string { + if err.msg != "" { + return fmt.Sprintf("trust server rejected operation: %s", err.msg) + } + return "trust server rejected operation." +} + +// HTTPStore manages pulling and pushing metadata from and to a remote +// service over HTTP. It assumes the URL structure of the remote service +// maps identically to the structure of the TUF repo: +// //(root|targets|snapshot|timestamp).json +// //foo.sh +// +// If consistent snapshots are disabled, it is advised that caching is not +// enabled. Simple set a cachePath (and ensure it's writeable) to enable +// caching. +type HTTPStore struct { + baseURL url.URL + metaPrefix string + metaExtension string + keyExtension string + roundTrip http.RoundTripper +} + +// NewHTTPStore initializes a new store against a URL and a number of configuration options +func NewHTTPStore(baseURL, metaPrefix, metaExtension, keyExtension string, roundTrip http.RoundTripper) (RemoteStore, error) { + base, err := url.Parse(baseURL) + if err != nil { + return nil, err + } + if !base.IsAbs() { + return nil, errors.New("HTTPStore requires an absolute baseURL") + } + if roundTrip == nil { + return &OfflineStore{}, nil + } + return &HTTPStore{ + baseURL: *base, + metaPrefix: metaPrefix, + metaExtension: metaExtension, + keyExtension: keyExtension, + roundTrip: roundTrip, + }, nil +} + +func tryUnmarshalError(resp *http.Response, defaultError error) error { + bodyBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return defaultError + } + var parsedErrors struct { + Errors []struct { + Detail validation.SerializableError `json:"detail"` + } `json:"errors"` + } + if err := json.Unmarshal(bodyBytes, &parsedErrors); err != nil { + return defaultError + } + if len(parsedErrors.Errors) != 1 { + return defaultError + } + err = parsedErrors.Errors[0].Detail.Error + if err == nil { + return defaultError + } + return err +} + +func translateStatusToError(resp *http.Response, resource string) error { + switch resp.StatusCode { + case http.StatusOK: + return nil + case http.StatusNotFound: + return ErrMetaNotFound{Resource: resource} + case http.StatusBadRequest: + return tryUnmarshalError(resp, ErrInvalidOperation{}) + default: + return ErrServerUnavailable{code: resp.StatusCode} + } +} + +// GetMeta downloads the named meta file with the given size. A short body +// is acceptable because in the case of timestamp.json, the size is a cap, +// not an exact length. +// If size is -1, this corresponds to "infinite," but we cut off at 100MB +func (s HTTPStore) GetMeta(name string, size int64) ([]byte, error) { + url, err := s.buildMetaURL(name) + if err != nil { + return nil, err + } + req, err := http.NewRequest("GET", url.String(), nil) + if err != nil { + return nil, err + } + resp, err := s.roundTrip.RoundTrip(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if err := translateStatusToError(resp, name); err != nil { + logrus.Debugf("received HTTP status %d when requesting %s.", resp.StatusCode, name) + return nil, err + } + if size == -1 { + size = notary.MaxDownloadSize + } + if resp.ContentLength > size { + return nil, ErrMaliciousServer{} + } + logrus.Debugf("%d when retrieving metadata for %s", resp.StatusCode, name) + b := io.LimitReader(resp.Body, size) + body, err := ioutil.ReadAll(b) + if err != nil { + return nil, err + } + return body, nil +} + +// SetMeta uploads a piece of TUF metadata to the server +func (s HTTPStore) SetMeta(name string, blob []byte) error { + url, err := s.buildMetaURL("") + if err != nil { + return err + } + req, err := http.NewRequest("POST", url.String(), bytes.NewReader(blob)) + if err != nil { + return err + } + resp, err := s.roundTrip.RoundTrip(req) + if err != nil { + return err + } + defer resp.Body.Close() + return translateStatusToError(resp, "POST "+name) +} + +// RemoveMeta always fails, because we should never be able to delete metadata +// remotely +func (s HTTPStore) RemoveMeta(name string) error { + return ErrInvalidOperation{msg: "cannot delete metadata"} +} + +// NewMultiPartMetaRequest builds a request with the provided metadata updates +// in multipart form +func NewMultiPartMetaRequest(url string, metas map[string][]byte) (*http.Request, error) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + for role, blob := range metas { + part, err := writer.CreateFormFile("files", role) + _, err = io.Copy(part, bytes.NewBuffer(blob)) + if err != nil { + return nil, err + } + } + err := writer.Close() + if err != nil { + return nil, err + } + req, err := http.NewRequest("POST", url, body) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + return req, nil +} + +// SetMultiMeta does a single batch upload of multiple pieces of TUF metadata. +// This should be preferred for updating a remote server as it enable the server +// to remain consistent, either accepting or rejecting the complete update. +func (s HTTPStore) SetMultiMeta(metas map[string][]byte) error { + url, err := s.buildMetaURL("") + if err != nil { + return err + } + req, err := NewMultiPartMetaRequest(url.String(), metas) + if err != nil { + return err + } + resp, err := s.roundTrip.RoundTrip(req) + if err != nil { + return err + } + defer resp.Body.Close() + // if this 404's something is pretty wrong + return translateStatusToError(resp, "POST metadata endpoint") +} + +// RemoveAll in the interface is not supported, admins should use the DeleteHandler endpoint directly to delete remote data for a GUN +func (s HTTPStore) RemoveAll() error { + return errors.New("remove all functionality not supported for HTTPStore") +} + +func (s HTTPStore) buildMetaURL(name string) (*url.URL, error) { + var filename string + if name != "" { + filename = fmt.Sprintf("%s.%s", name, s.metaExtension) + } + uri := path.Join(s.metaPrefix, filename) + return s.buildURL(uri) +} + +func (s HTTPStore) buildKeyURL(name string) (*url.URL, error) { + filename := fmt.Sprintf("%s.%s", name, s.keyExtension) + uri := path.Join(s.metaPrefix, filename) + return s.buildURL(uri) +} + +func (s HTTPStore) buildURL(uri string) (*url.URL, error) { + sub, err := url.Parse(uri) + if err != nil { + return nil, err + } + return s.baseURL.ResolveReference(sub), nil +} + +// GetKey retrieves a public key from the remote server +func (s HTTPStore) GetKey(role string) ([]byte, error) { + url, err := s.buildKeyURL(role) + if err != nil { + return nil, err + } + req, err := http.NewRequest("GET", url.String(), nil) + if err != nil { + return nil, err + } + resp, err := s.roundTrip.RoundTrip(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if err := translateStatusToError(resp, role+" key"); err != nil { + return nil, err + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return body, nil +} diff --git a/vendor/src/github.com/docker/notary/tuf/store/interfaces.go b/vendor/src/github.com/docker/notary/tuf/store/interfaces.go new file mode 100644 index 00000000..dd307168 --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/store/interfaces.go @@ -0,0 +1,28 @@ +package store + +// MetadataStore must be implemented by anything that intends to interact +// with a store of TUF files +type MetadataStore interface { + GetMeta(name string, size int64) ([]byte, error) + SetMeta(name string, blob []byte) error + SetMultiMeta(map[string][]byte) error + RemoveAll() error + RemoveMeta(name string) error +} + +// PublicKeyStore must be implemented by a key service +type PublicKeyStore interface { + GetKey(role string) ([]byte, error) +} + +// LocalStore represents a local TUF sture +type LocalStore interface { + MetadataStore +} + +// RemoteStore is similar to LocalStore with the added expectation that it should +// provide a way to download targets once located +type RemoteStore interface { + MetadataStore + PublicKeyStore +} diff --git a/vendor/src/github.com/docker/notary/tuf/store/memorystore.go b/vendor/src/github.com/docker/notary/tuf/store/memorystore.go new file mode 100644 index 00000000..493bb6f0 --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/store/memorystore.go @@ -0,0 +1,106 @@ +package store + +import ( + "crypto/sha256" + "fmt" + + "github.com/docker/notary" + "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/utils" +) + +// NewMemoryStore returns a MetadataStore that operates entirely in memory. +// Very useful for testing +func NewMemoryStore(meta map[string][]byte) *MemoryStore { + var consistent = make(map[string][]byte) + if meta == nil { + meta = make(map[string][]byte) + } else { + // add all seed meta to consistent + for name, data := range meta { + checksum := sha256.Sum256(data) + path := utils.ConsistentName(name, checksum[:]) + consistent[path] = data + } + } + return &MemoryStore{ + meta: meta, + consistent: consistent, + keys: make(map[string][]data.PrivateKey), + } +} + +// MemoryStore implements a mock RemoteStore entirely in memory. +// For testing purposes only. +type MemoryStore struct { + meta map[string][]byte + consistent map[string][]byte + keys map[string][]data.PrivateKey +} + +// GetMeta returns up to size bytes of data references by name. +// If size is -1, this corresponds to "infinite," but we cut off at 100MB +// as we will always know the size for everything but a timestamp and +// sometimes a root, neither of which should be exceptionally large +func (m *MemoryStore) GetMeta(name string, size int64) ([]byte, error) { + d, ok := m.meta[name] + if ok { + if size == -1 { + size = notary.MaxDownloadSize + } + if int64(len(d)) < size { + return d, nil + } + return d[:size], nil + } + d, ok = m.consistent[name] + if ok { + if int64(len(d)) < size { + return d, nil + } + return d[:size], nil + } + return nil, ErrMetaNotFound{Resource: name} +} + +// SetMeta sets the metadata value for the given name +func (m *MemoryStore) SetMeta(name string, meta []byte) error { + m.meta[name] = meta + + checksum := sha256.Sum256(meta) + path := utils.ConsistentName(name, checksum[:]) + m.consistent[path] = meta + return nil +} + +// SetMultiMeta sets multiple pieces of metadata for multiple names +// in a single operation. +func (m *MemoryStore) SetMultiMeta(metas map[string][]byte) error { + for role, blob := range metas { + m.SetMeta(role, blob) + } + return nil +} + +// RemoveMeta removes the metadata for a single role - if the metadata doesn't +// exist, no error is returned +func (m *MemoryStore) RemoveMeta(name string) error { + if meta, ok := m.meta[name]; ok { + checksum := sha256.Sum256(meta) + path := utils.ConsistentName(name, checksum[:]) + delete(m.meta, name) + delete(m.consistent, path) + } + return nil +} + +// GetKey returns the public key for the given role +func (m *MemoryStore) GetKey(role string) ([]byte, error) { + return nil, fmt.Errorf("GetKey is not implemented for the MemoryStore") +} + +// RemoveAll clears the existing memory store by setting this store as new empty one +func (m *MemoryStore) RemoveAll() error { + *m = *NewMemoryStore(nil) + return nil +} diff --git a/vendor/src/github.com/docker/notary/tuf/store/offlinestore.go b/vendor/src/github.com/docker/notary/tuf/store/offlinestore.go new file mode 100644 index 00000000..b0f057b2 --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/store/offlinestore.go @@ -0,0 +1,53 @@ +package store + +import ( + "io" +) + +// ErrOffline is used to indicate we are operating offline +type ErrOffline struct{} + +func (e ErrOffline) Error() string { + return "client is offline" +} + +var err = ErrOffline{} + +// OfflineStore is to be used as a placeholder for a nil store. It simply +// returns ErrOffline for every operation +type OfflineStore struct{} + +// GetMeta returns ErrOffline +func (es OfflineStore) GetMeta(name string, size int64) ([]byte, error) { + return nil, err +} + +// SetMeta returns ErrOffline +func (es OfflineStore) SetMeta(name string, blob []byte) error { + return err +} + +// SetMultiMeta returns ErrOffline +func (es OfflineStore) SetMultiMeta(map[string][]byte) error { + return err +} + +// RemoveMeta returns ErrOffline +func (es OfflineStore) RemoveMeta(name string) error { + return err +} + +// GetKey returns ErrOffline +func (es OfflineStore) GetKey(role string) ([]byte, error) { + return nil, err +} + +// GetTarget returns ErrOffline +func (es OfflineStore) GetTarget(path string) (io.ReadCloser, error) { + return nil, err +} + +// RemoveAll return ErrOffline +func (es OfflineStore) RemoveAll() error { + return err +} diff --git a/vendor/src/github.com/docker/notary/tuf/tuf.go b/vendor/src/github.com/docker/notary/tuf/tuf.go new file mode 100644 index 00000000..0cedf74e --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/tuf.go @@ -0,0 +1,926 @@ +// Package tuf defines the core TUF logic around manipulating a repo. +package tuf + +import ( + "bytes" + "encoding/json" + "fmt" + "path" + "strings" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/notary" + "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/signed" + "github.com/docker/notary/tuf/utils" +) + +// ErrSigVerifyFail - signature verification failed +type ErrSigVerifyFail struct{} + +func (e ErrSigVerifyFail) Error() string { + return "Error: Signature verification failed" +} + +// ErrMetaExpired - metadata file has expired +type ErrMetaExpired struct{} + +func (e ErrMetaExpired) Error() string { + return "Error: Metadata has expired" +} + +// ErrLocalRootExpired - the local root file is out of date +type ErrLocalRootExpired struct{} + +func (e ErrLocalRootExpired) Error() string { + return "Error: Local Root Has Expired" +} + +// ErrNotLoaded - attempted to access data that has not been loaded into +// the repo. This means specifically that the relevant JSON file has not +// been loaded. +type ErrNotLoaded struct { + Role string +} + +func (err ErrNotLoaded) Error() string { + return fmt.Sprintf("%s role has not been loaded", err.Role) +} + +// StopWalk - used by visitor functions to signal WalkTargets to stop walking +type StopWalk struct{} + +// Repo is an in memory representation of the TUF Repo. +// It operates at the data.Signed level, accepting and producing +// data.Signed objects. Users of a Repo are responsible for +// fetching raw JSON and using the Set* functions to populate +// the Repo instance. +type Repo struct { + Root *data.SignedRoot + Targets map[string]*data.SignedTargets + Snapshot *data.SignedSnapshot + Timestamp *data.SignedTimestamp + cryptoService signed.CryptoService +} + +// NewRepo initializes a Repo instance with a CryptoService. +// If the Repo will only be used for reading, the CryptoService +// can be nil. +func NewRepo(cryptoService signed.CryptoService) *Repo { + repo := &Repo{ + Targets: make(map[string]*data.SignedTargets), + cryptoService: cryptoService, + } + return repo +} + +// AddBaseKeys is used to add keys to the role in root.json +func (tr *Repo) AddBaseKeys(role string, keys ...data.PublicKey) error { + if tr.Root == nil { + return ErrNotLoaded{Role: data.CanonicalRootRole} + } + ids := []string{} + for _, k := range keys { + // Store only the public portion + tr.Root.Signed.Keys[k.ID()] = k + tr.Root.Signed.Roles[role].KeyIDs = append(tr.Root.Signed.Roles[role].KeyIDs, k.ID()) + ids = append(ids, k.ID()) + } + tr.Root.Dirty = true + + // also, whichever role was switched out needs to be re-signed + // root has already been marked dirty + switch role { + case data.CanonicalSnapshotRole: + if tr.Snapshot != nil { + tr.Snapshot.Dirty = true + } + case data.CanonicalTargetsRole: + if target, ok := tr.Targets[data.CanonicalTargetsRole]; ok { + target.Dirty = true + } + case data.CanonicalTimestampRole: + if tr.Timestamp != nil { + tr.Timestamp.Dirty = true + } + } + return nil +} + +// ReplaceBaseKeys is used to replace all keys for the given role with the new keys +func (tr *Repo) ReplaceBaseKeys(role string, keys ...data.PublicKey) error { + r, err := tr.GetBaseRole(role) + if err != nil { + return err + } + err = tr.RemoveBaseKeys(role, r.ListKeyIDs()...) + if err != nil { + return err + } + return tr.AddBaseKeys(role, keys...) +} + +// RemoveBaseKeys is used to remove keys from the roles in root.json +func (tr *Repo) RemoveBaseKeys(role string, keyIDs ...string) error { + if tr.Root == nil { + return ErrNotLoaded{Role: data.CanonicalRootRole} + } + var keep []string + toDelete := make(map[string]struct{}) + // remove keys from specified role + for _, k := range keyIDs { + toDelete[k] = struct{}{} + for _, rk := range tr.Root.Signed.Roles[role].KeyIDs { + if k != rk { + keep = append(keep, rk) + } + } + } + tr.Root.Signed.Roles[role].KeyIDs = keep + + // determine which keys are no longer in use by any roles + for roleName, r := range tr.Root.Signed.Roles { + if roleName == role { + continue + } + for _, rk := range r.KeyIDs { + if _, ok := toDelete[rk]; ok { + delete(toDelete, rk) + } + } + } + + // remove keys no longer in use by any roles + for k := range toDelete { + delete(tr.Root.Signed.Keys, k) + // remove the signing key from the cryptoservice if it + // isn't a root key. Root keys must be kept for rotation + // signing + if role != data.CanonicalRootRole { + tr.cryptoService.RemoveKey(k) + } + } + tr.Root.Dirty = true + return nil +} + +// GetBaseRole gets a base role from this repo's metadata +func (tr *Repo) GetBaseRole(name string) (data.BaseRole, error) { + if !data.ValidRole(name) { + return data.BaseRole{}, data.ErrInvalidRole{Role: name, Reason: "invalid base role name"} + } + if tr.Root == nil { + return data.BaseRole{}, ErrNotLoaded{data.CanonicalRootRole} + } + // Find the role data public keys for the base role from TUF metadata + baseRole, err := tr.Root.BuildBaseRole(name) + if err != nil { + return data.BaseRole{}, err + } + + return baseRole, nil +} + +// GetDelegationRole gets a delegation role from this repo's metadata, walking from the targets role down to the delegation itself +func (tr *Repo) GetDelegationRole(name string) (data.DelegationRole, error) { + if !data.IsDelegation(name) { + return data.DelegationRole{}, data.ErrInvalidRole{Role: name, Reason: "invalid delegation name"} + } + if tr.Root == nil { + return data.DelegationRole{}, ErrNotLoaded{data.CanonicalRootRole} + } + _, ok := tr.Root.Signed.Roles[data.CanonicalTargetsRole] + if !ok { + return data.DelegationRole{}, ErrNotLoaded{data.CanonicalTargetsRole} + } + // Traverse target metadata, down to delegation itself + // Get all public keys for the base role from TUF metadata + _, ok = tr.Targets[data.CanonicalTargetsRole] + if !ok { + return data.DelegationRole{}, ErrNotLoaded{data.CanonicalTargetsRole} + } + + // Start with top level roles in targets. Walk the chain of ancestors + // until finding the desired role, or we run out of targets files to search. + var foundRole *data.DelegationRole + buildDelegationRoleVisitor := func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} { + // Try to find the delegation and build a DelegationRole structure + for _, role := range tgt.Signed.Delegations.Roles { + if role.Name == name { + delgRole, err := tgt.BuildDelegationRole(name) + if err != nil { + return err + } + foundRole = &delgRole + return StopWalk{} + } + } + return nil + } + + // Walk to the parent of this delegation, since that is where its role metadata exists + err := tr.WalkTargets("", path.Dir(name), buildDelegationRoleVisitor) + if err != nil { + return data.DelegationRole{}, err + } + + // We never found the delegation. In the context of this repo it is considered + // invalid. N.B. it may be that it existed at one point but an ancestor has since + // been modified/removed. + if foundRole == nil { + return data.DelegationRole{}, data.ErrInvalidRole{Role: name, Reason: "delegation does not exist"} + } + + return *foundRole, nil +} + +// GetAllLoadedRoles returns a list of all role entries loaded in this TUF repo, could be empty +func (tr *Repo) GetAllLoadedRoles() []*data.Role { + var res []*data.Role + if tr.Root == nil { + // if root isn't loaded, we should consider we have no loaded roles because we can't + // trust any other state that might be present + return res + } + for name, rr := range tr.Root.Signed.Roles { + res = append(res, &data.Role{ + RootRole: *rr, + Name: name, + }) + } + for _, delegate := range tr.Targets { + for _, r := range delegate.Signed.Delegations.Roles { + res = append(res, r) + } + } + return res +} + +// Walk to parent, and either create or update this delegation. We can only create a new delegation if we're given keys +// Ensure all updates are valid, by checking against parent ancestor paths and ensuring the keys meet the role threshold. +func delegationUpdateVisitor(roleName string, addKeys data.KeyList, removeKeys, addPaths, removePaths []string, clearAllPaths bool, newThreshold int) walkVisitorFunc { + return func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} { + var err error + // Validate the changes underneath this restricted validRole for adding paths, reject invalid path additions + if len(addPaths) != len(data.RestrictDelegationPathPrefixes(validRole.Paths, addPaths)) { + return data.ErrInvalidRole{Role: roleName, Reason: "invalid paths to add to role"} + } + // Try to find the delegation and amend it using our changelist + var delgRole *data.Role + for _, role := range tgt.Signed.Delegations.Roles { + if role.Name == roleName { + // Make a copy and operate on this role until we validate the changes + keyIDCopy := make([]string, len(role.KeyIDs)) + copy(keyIDCopy, role.KeyIDs) + pathsCopy := make([]string, len(role.Paths)) + copy(pathsCopy, role.Paths) + delgRole = &data.Role{ + RootRole: data.RootRole{ + KeyIDs: keyIDCopy, + Threshold: role.Threshold, + }, + Name: role.Name, + Paths: pathsCopy, + } + delgRole.RemovePaths(removePaths) + if clearAllPaths { + delgRole.Paths = []string{} + } + delgRole.AddPaths(addPaths) + delgRole.RemoveKeys(removeKeys) + break + } + } + // We didn't find the role earlier, so create it only if we have keys to add + if delgRole == nil { + if len(addKeys) > 0 { + delgRole, err = data.NewRole(roleName, newThreshold, addKeys.IDs(), addPaths) + if err != nil { + return err + } + } else { + // If we can't find the role and didn't specify keys to add, this is an error + return data.ErrInvalidRole{Role: roleName, Reason: "cannot create new delegation without keys"} + } + } + // Add the key IDs to the role and the keys themselves to the parent + for _, k := range addKeys { + if !utils.StrSliceContains(delgRole.KeyIDs, k.ID()) { + delgRole.KeyIDs = append(delgRole.KeyIDs, k.ID()) + } + } + // Make sure we have a valid role still + if len(delgRole.KeyIDs) < delgRole.Threshold { + return data.ErrInvalidRole{Role: roleName, Reason: "insufficient keys to meet threshold"} + } + // NOTE: this closure CANNOT error after this point, as we've committed to editing the SignedTargets metadata in the repo object. + // Any errors related to updating this delegation must occur before this point. + // If all of our changes were valid, we should edit the actual SignedTargets to match our copy + for _, k := range addKeys { + tgt.Signed.Delegations.Keys[k.ID()] = k + } + foundAt := utils.FindRoleIndex(tgt.Signed.Delegations.Roles, delgRole.Name) + if foundAt < 0 { + tgt.Signed.Delegations.Roles = append(tgt.Signed.Delegations.Roles, delgRole) + } else { + tgt.Signed.Delegations.Roles[foundAt] = delgRole + } + tgt.Dirty = true + utils.RemoveUnusedKeys(tgt) + return StopWalk{} + } +} + +// UpdateDelegationKeys updates the appropriate delegations, either adding +// a new delegation or updating an existing one. If keys are +// provided, the IDs will be added to the role (if they do not exist +// there already), and the keys will be added to the targets file. +func (tr *Repo) UpdateDelegationKeys(roleName string, addKeys data.KeyList, removeKeys []string, newThreshold int) error { + if !data.IsDelegation(roleName) { + return data.ErrInvalidRole{Role: roleName, Reason: "not a valid delegated role"} + } + parent := path.Dir(roleName) + + if err := tr.VerifyCanSign(parent); err != nil { + return err + } + + // check the parent role's metadata + _, ok := tr.Targets[parent] + if !ok { // the parent targetfile may not exist yet - if not, then create it + var err error + _, err = tr.InitTargets(parent) + if err != nil { + return err + } + } + + // Walk to the parent of this delegation, since that is where its role metadata exists + // We do not have to verify that the walker reached its desired role in this scenario + // since we've already done another walk to the parent role in VerifyCanSign, and potentially made a targets file + err := tr.WalkTargets("", parent, delegationUpdateVisitor(roleName, addKeys, removeKeys, []string{}, []string{}, false, newThreshold)) + if err != nil { + return err + } + return nil +} + +// UpdateDelegationPaths updates the appropriate delegation's paths. +// It is not allowed to create a new delegation. +func (tr *Repo) UpdateDelegationPaths(roleName string, addPaths, removePaths []string, clearPaths bool) error { + if !data.IsDelegation(roleName) { + return data.ErrInvalidRole{Role: roleName, Reason: "not a valid delegated role"} + } + parent := path.Dir(roleName) + + if err := tr.VerifyCanSign(parent); err != nil { + return err + } + + // check the parent role's metadata + _, ok := tr.Targets[parent] + if !ok { // the parent targetfile may not exist yet + // if not, this is an error because a delegation must exist to edit only paths + return data.ErrInvalidRole{Role: roleName, Reason: "no valid delegated role exists"} + } + + // Walk to the parent of this delegation, since that is where its role metadata exists + // We do not have to verify that the walker reached its desired role in this scenario + // since we've already done another walk to the parent role in VerifyCanSign + err := tr.WalkTargets("", parent, delegationUpdateVisitor(roleName, data.KeyList{}, []string{}, addPaths, removePaths, clearPaths, notary.MinThreshold)) + if err != nil { + return err + } + return nil +} + +// DeleteDelegation removes a delegated targets role from its parent +// targets object. It also deletes the delegation from the snapshot. +// DeleteDelegation will only make use of the role Name field. +func (tr *Repo) DeleteDelegation(roleName string) error { + if !data.IsDelegation(roleName) { + return data.ErrInvalidRole{Role: roleName, Reason: "not a valid delegated role"} + } + + parent := path.Dir(roleName) + if err := tr.VerifyCanSign(parent); err != nil { + return err + } + + // delete delegated data from Targets map and Snapshot - if they don't + // exist, these are no-op + delete(tr.Targets, roleName) + tr.Snapshot.DeleteMeta(roleName) + + p, ok := tr.Targets[parent] + if !ok { + // if there is no parent metadata (the role exists though), then this + // is as good as done. + return nil + } + + foundAt := utils.FindRoleIndex(p.Signed.Delegations.Roles, roleName) + + if foundAt >= 0 { + var roles []*data.Role + // slice out deleted role + roles = append(roles, p.Signed.Delegations.Roles[:foundAt]...) + if foundAt+1 < len(p.Signed.Delegations.Roles) { + roles = append(roles, p.Signed.Delegations.Roles[foundAt+1:]...) + } + p.Signed.Delegations.Roles = roles + + utils.RemoveUnusedKeys(p) + + p.Dirty = true + } // if the role wasn't found, it's a good as deleted + + return nil +} + +// InitRoot initializes an empty root file with the 4 core roles passed to the +// method, and the consistent flag. +func (tr *Repo) InitRoot(root, timestamp, snapshot, targets data.BaseRole, consistent bool) error { + rootRoles := make(map[string]*data.RootRole) + rootKeys := make(map[string]data.PublicKey) + + for _, r := range []data.BaseRole{root, timestamp, snapshot, targets} { + rootRoles[r.Name] = &data.RootRole{ + Threshold: r.Threshold, + KeyIDs: r.ListKeyIDs(), + } + for kid, k := range r.Keys { + rootKeys[kid] = k + } + } + r, err := data.NewRoot(rootKeys, rootRoles, consistent) + if err != nil { + return err + } + tr.Root = r + return nil +} + +// InitTargets initializes an empty targets, and returns the new empty target +func (tr *Repo) InitTargets(role string) (*data.SignedTargets, error) { + if !data.IsDelegation(role) && role != data.CanonicalTargetsRole { + return nil, data.ErrInvalidRole{ + Role: role, + Reason: fmt.Sprintf("role is not a valid targets role name: %s", role), + } + } + targets := data.NewTargets() + tr.Targets[role] = targets + return targets, nil +} + +// InitSnapshot initializes a snapshot based on the current root and targets +func (tr *Repo) InitSnapshot() error { + if tr.Root == nil { + return ErrNotLoaded{Role: data.CanonicalRootRole} + } + root, err := tr.Root.ToSigned() + if err != nil { + return err + } + + if _, ok := tr.Targets[data.CanonicalTargetsRole]; !ok { + return ErrNotLoaded{Role: data.CanonicalTargetsRole} + } + targets, err := tr.Targets[data.CanonicalTargetsRole].ToSigned() + if err != nil { + return err + } + snapshot, err := data.NewSnapshot(root, targets) + if err != nil { + return err + } + tr.Snapshot = snapshot + return nil +} + +// InitTimestamp initializes a timestamp based on the current snapshot +func (tr *Repo) InitTimestamp() error { + snap, err := tr.Snapshot.ToSigned() + if err != nil { + return err + } + timestamp, err := data.NewTimestamp(snap) + if err != nil { + return err + } + + tr.Timestamp = timestamp + return nil +} + +// SetRoot sets the Repo.Root field to the SignedRoot object. +func (tr *Repo) SetRoot(s *data.SignedRoot) error { + tr.Root = s + return nil +} + +// SetTimestamp parses the Signed object into a SignedTimestamp object +// and sets the Repo.Timestamp field. +func (tr *Repo) SetTimestamp(s *data.SignedTimestamp) error { + tr.Timestamp = s + return nil +} + +// SetSnapshot parses the Signed object into a SignedSnapshots object +// and sets the Repo.Snapshot field. +func (tr *Repo) SetSnapshot(s *data.SignedSnapshot) error { + tr.Snapshot = s + return nil +} + +// SetTargets sets the SignedTargets object agaist the role in the +// Repo.Targets map. +func (tr *Repo) SetTargets(role string, s *data.SignedTargets) error { + tr.Targets[role] = s + return nil +} + +// TargetMeta returns the FileMeta entry for the given path in the +// targets file associated with the given role. This may be nil if +// the target isn't found in the targets file. +func (tr Repo) TargetMeta(role, path string) *data.FileMeta { + if t, ok := tr.Targets[role]; ok { + if m, ok := t.Signed.Targets[path]; ok { + return &m + } + } + return nil +} + +// TargetDelegations returns a slice of Roles that are valid publishers +// for the target path provided. +func (tr Repo) TargetDelegations(role, path string) []*data.Role { + var roles []*data.Role + if t, ok := tr.Targets[role]; ok { + for _, r := range t.Signed.Delegations.Roles { + if r.CheckPaths(path) { + roles = append(roles, r) + } + } + } + return roles +} + +// VerifyCanSign returns nil if the role exists and we have at least one +// signing key for the role, false otherwise. This does not check that we have +// enough signing keys to meet the threshold, since we want to support the use +// case of multiple signers for a role. It returns an error if the role doesn't +// exist or if there are no signing keys. +func (tr *Repo) VerifyCanSign(roleName string) error { + var ( + role data.BaseRole + err error + canonicalKeyIDs []string + ) + // we only need the BaseRole part of a delegation because we're just + // checking KeyIDs + if data.IsDelegation(roleName) { + r, err := tr.GetDelegationRole(roleName) + if err != nil { + return err + } + role = r.BaseRole + } else { + role, err = tr.GetBaseRole(roleName) + } + if err != nil { + return data.ErrInvalidRole{Role: roleName, Reason: "does not exist"} + } + + for keyID, k := range role.Keys { + check := []string{keyID} + if canonicalID, err := utils.CanonicalKeyID(k); err == nil { + check = append(check, canonicalID) + canonicalKeyIDs = append(canonicalKeyIDs, canonicalID) + } + for _, id := range check { + p, _, err := tr.cryptoService.GetPrivateKey(id) + if err == nil && p != nil { + return nil + } + } + } + return signed.ErrNoKeys{KeyIDs: canonicalKeyIDs} +} + +// used for walking the targets/delegations tree, potentially modifying the underlying SignedTargets for the repo +type walkVisitorFunc func(*data.SignedTargets, data.DelegationRole) interface{} + +// WalkTargets will apply the specified visitor function to iteratively walk the targets/delegation metadata tree, +// until receiving a StopWalk. The walk starts from the base "targets" role, and searches for the correct targetPath and/or rolePath +// to call the visitor function on. Any roles passed into skipRoles will be excluded from the walk, as well as roles in those subtrees +func (tr *Repo) WalkTargets(targetPath, rolePath string, visitTargets walkVisitorFunc, skipRoles ...string) error { + // Start with the base targets role, which implicitly has the "" targets path + targetsRole, err := tr.GetBaseRole(data.CanonicalTargetsRole) + if err != nil { + return err + } + // Make the targets role have the empty path, when we treat it as a delegation role + roles := []data.DelegationRole{ + { + BaseRole: targetsRole, + Paths: []string{""}, + }, + } + + for len(roles) > 0 { + role := roles[0] + roles = roles[1:] + + // Check the role metadata + signedTgt, ok := tr.Targets[role.Name] + if !ok { + // The role meta doesn't exist in the repo so continue onward + continue + } + + // We're at a prefix of the desired role subtree, so add its delegation role children and continue walking + if strings.HasPrefix(rolePath, role.Name+"/") { + roles = append(roles, signedTgt.GetValidDelegations(role)...) + continue + } + + // Determine whether to visit this role or not: + // If the paths validate against the specified targetPath and the rolePath is empty or is in the subtree + // Also check if we are choosing to skip visiting this role on this walk (see ListTargets and GetTargetByName priority) + if isValidPath(targetPath, role) && isAncestorRole(role.Name, rolePath) && !utils.StrSliceContains(skipRoles, role.Name) { + // If we had matching path or role name, visit this target and determine whether or not to keep walking + res := visitTargets(signedTgt, role) + switch typedRes := res.(type) { + case StopWalk: + // If the visitor function signalled a stop, return nil to finish the walk + return nil + case nil: + // If the visitor function signalled to continue, add this role's delegation to the walk + roles = append(roles, signedTgt.GetValidDelegations(role)...) + case error: + // Propagate any errors from the visitor + return typedRes + default: + // Return out with an error if we got a different result + return fmt.Errorf("unexpected return while walking: %v", res) + } + + } + } + return nil +} + +// helper function that returns whether the candidateChild role name is an ancestor or equal to the candidateAncestor role name +// Will return true if given an empty candidateAncestor role name +// The HasPrefix check is for determining whether the role name for candidateChild is a child (direct or further down the chain) +// of candidateAncestor, for ex: candidateAncestor targets/a and candidateChild targets/a/b/c +func isAncestorRole(candidateChild, candidateAncestor string) bool { + return candidateAncestor == "" || candidateAncestor == candidateChild || strings.HasPrefix(candidateChild, candidateAncestor+"/") +} + +// helper function that returns whether the delegation Role is valid against the given path +// Will return true if given an empty candidatePath +func isValidPath(candidatePath string, delgRole data.DelegationRole) bool { + return candidatePath == "" || delgRole.CheckPaths(candidatePath) +} + +// AddTargets will attempt to add the given targets specifically to +// the directed role. If the metadata for the role doesn't exist yet, +// AddTargets will create one. +func (tr *Repo) AddTargets(role string, targets data.Files) (data.Files, error) { + err := tr.VerifyCanSign(role) + if err != nil { + return nil, err + } + + // check existence of the role's metadata + _, ok := tr.Targets[role] + if !ok { // the targetfile may not exist yet - if not, then create it + var err error + _, err = tr.InitTargets(role) + if err != nil { + return nil, err + } + } + + addedTargets := make(data.Files) + addTargetVisitor := func(targetPath string, targetMeta data.FileMeta) func(*data.SignedTargets, data.DelegationRole) interface{} { + return func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} { + // We've already validated the role's target path in our walk, so just modify the metadata + tgt.Signed.Targets[targetPath] = targetMeta + tgt.Dirty = true + // Also add to our new addedTargets map to keep track of every target we've added successfully + addedTargets[targetPath] = targetMeta + return StopWalk{} + } + } + + // Walk the role tree while validating the target paths, and add all of our targets + for path, target := range targets { + tr.WalkTargets(path, role, addTargetVisitor(path, target)) + } + if len(addedTargets) != len(targets) { + return nil, fmt.Errorf("Could not add all targets") + } + return nil, nil +} + +// RemoveTargets removes the given target (paths) from the given target role (delegation) +func (tr *Repo) RemoveTargets(role string, targets ...string) error { + if err := tr.VerifyCanSign(role); err != nil { + return err + } + + removeTargetVisitor := func(targetPath string) func(*data.SignedTargets, data.DelegationRole) interface{} { + return func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} { + // We've already validated the role path in our walk, so just modify the metadata + // We don't check against the target path against the valid role paths because it's + // possible we got into an invalid state and are trying to fix it + delete(tgt.Signed.Targets, targetPath) + tgt.Dirty = true + return StopWalk{} + } + } + + // if the role exists but metadata does not yet, then our work is done + _, ok := tr.Targets[role] + if ok { + for _, path := range targets { + tr.WalkTargets("", role, removeTargetVisitor(path)) + } + } + + return nil +} + +// UpdateSnapshot updates the FileMeta for the given role based on the Signed object +func (tr *Repo) UpdateSnapshot(role string, s *data.Signed) error { + jsonData, err := json.Marshal(s) + if err != nil { + return err + } + meta, err := data.NewFileMeta(bytes.NewReader(jsonData), data.NotaryDefaultHashes...) + if err != nil { + return err + } + tr.Snapshot.Signed.Meta[role] = meta + tr.Snapshot.Dirty = true + return nil +} + +// UpdateTimestamp updates the snapshot meta in the timestamp based on the Signed object +func (tr *Repo) UpdateTimestamp(s *data.Signed) error { + jsonData, err := json.Marshal(s) + if err != nil { + return err + } + meta, err := data.NewFileMeta(bytes.NewReader(jsonData), data.NotaryDefaultHashes...) + if err != nil { + return err + } + tr.Timestamp.Signed.Meta["snapshot"] = meta + tr.Timestamp.Dirty = true + return nil +} + +// SignRoot signs the root +func (tr *Repo) SignRoot(expires time.Time) (*data.Signed, error) { + logrus.Debug("signing root...") + tr.Root.Signed.Expires = expires + tr.Root.Signed.Version++ + root, err := tr.GetBaseRole(data.CanonicalRootRole) + if err != nil { + return nil, err + } + signed, err := tr.Root.ToSigned() + if err != nil { + return nil, err + } + signed, err = tr.sign(signed, root) + if err != nil { + return nil, err + } + tr.Root.Signatures = signed.Signatures + return signed, nil +} + +// SignTargets signs the targets file for the given top level or delegated targets role +func (tr *Repo) SignTargets(role string, expires time.Time) (*data.Signed, error) { + logrus.Debugf("sign targets called for role %s", role) + if _, ok := tr.Targets[role]; !ok { + return nil, data.ErrInvalidRole{ + Role: role, + Reason: "SignTargets called with non-existant targets role", + } + } + tr.Targets[role].Signed.Expires = expires + tr.Targets[role].Signed.Version++ + signed, err := tr.Targets[role].ToSigned() + if err != nil { + logrus.Debug("errored getting targets data.Signed object") + return nil, err + } + + var targets data.BaseRole + if role == data.CanonicalTargetsRole { + targets, err = tr.GetBaseRole(role) + } else { + tr, err := tr.GetDelegationRole(role) + if err != nil { + return nil, err + } + targets = tr.BaseRole + } + if err != nil { + return nil, err + } + + signed, err = tr.sign(signed, targets) + if err != nil { + logrus.Debug("errored signing ", role) + return nil, err + } + tr.Targets[role].Signatures = signed.Signatures + return signed, nil +} + +// SignSnapshot updates the snapshot based on the current targets and root then signs it +func (tr *Repo) SignSnapshot(expires time.Time) (*data.Signed, error) { + logrus.Debug("signing snapshot...") + signedRoot, err := tr.Root.ToSigned() + if err != nil { + return nil, err + } + err = tr.UpdateSnapshot("root", signedRoot) + if err != nil { + return nil, err + } + tr.Root.Dirty = false // root dirty until changes captures in snapshot + for role, targets := range tr.Targets { + signedTargets, err := targets.ToSigned() + if err != nil { + return nil, err + } + err = tr.UpdateSnapshot(role, signedTargets) + if err != nil { + return nil, err + } + targets.Dirty = false + } + tr.Snapshot.Signed.Expires = expires + tr.Snapshot.Signed.Version++ + signed, err := tr.Snapshot.ToSigned() + if err != nil { + return nil, err + } + snapshot, err := tr.GetBaseRole(data.CanonicalSnapshotRole) + if err != nil { + return nil, err + } + signed, err = tr.sign(signed, snapshot) + if err != nil { + return nil, err + } + tr.Snapshot.Signatures = signed.Signatures + return signed, nil +} + +// SignTimestamp updates the timestamp based on the current snapshot then signs it +func (tr *Repo) SignTimestamp(expires time.Time) (*data.Signed, error) { + logrus.Debug("SignTimestamp") + signedSnapshot, err := tr.Snapshot.ToSigned() + if err != nil { + return nil, err + } + err = tr.UpdateTimestamp(signedSnapshot) + if err != nil { + return nil, err + } + tr.Timestamp.Signed.Expires = expires + tr.Timestamp.Signed.Version++ + signed, err := tr.Timestamp.ToSigned() + if err != nil { + return nil, err + } + timestamp, err := tr.GetBaseRole(data.CanonicalTimestampRole) + if err != nil { + return nil, err + } + signed, err = tr.sign(signed, timestamp) + if err != nil { + return nil, err + } + tr.Timestamp.Signatures = signed.Signatures + tr.Snapshot.Dirty = false // snapshot is dirty until changes have been captured in timestamp + return signed, nil +} + +func (tr Repo) sign(signedData *data.Signed, role data.BaseRole) (*data.Signed, error) { + if err := signed.Sign(tr.cryptoService, signedData, role.ListKeys()...); err != nil { + return nil, err + } + return signedData, nil +} diff --git a/vendor/src/github.com/docker/notary/tuf/utils/role_sort.go b/vendor/src/github.com/docker/notary/tuf/utils/role_sort.go new file mode 100644 index 00000000..368925c3 --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/utils/role_sort.go @@ -0,0 +1,31 @@ +package utils + +import ( + "strings" +) + +// RoleList is a list of roles +type RoleList []string + +// Len returns the length of the list +func (r RoleList) Len() int { + return len(r) +} + +// Less returns true if the item at i should be sorted +// before the item at j. It's an unstable partial ordering +// based on the number of segments, separated by "/", in +// the role name +func (r RoleList) Less(i, j int) bool { + segsI := strings.Split(r[i], "/") + segsJ := strings.Split(r[j], "/") + if len(segsI) == len(segsJ) { + return r[i] < r[j] + } + return len(segsI) < len(segsJ) +} + +// Swap the items at 2 locations in the list +func (r RoleList) Swap(i, j int) { + r[i], r[j] = r[j], r[i] +} diff --git a/vendor/src/github.com/docker/notary/tuf/utils/stack.go b/vendor/src/github.com/docker/notary/tuf/utils/stack.go new file mode 100644 index 00000000..e79b5c86 --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/utils/stack.go @@ -0,0 +1,85 @@ +package utils + +import ( + "fmt" + "sync" +) + +// ErrEmptyStack is used when an action that requires some +// content is invoked and the stack is empty +type ErrEmptyStack struct { + action string +} + +func (err ErrEmptyStack) Error() string { + return fmt.Sprintf("attempted to %s with empty stack", err.action) +} + +// ErrBadTypeCast is used by PopX functions when the item +// cannot be typed to X +type ErrBadTypeCast struct{} + +func (err ErrBadTypeCast) Error() string { + return "attempted to do a typed pop and item was not of type" +} + +// Stack is a simple type agnostic stack implementation +type Stack struct { + s []interface{} + l sync.Mutex +} + +// NewStack create a new stack +func NewStack() *Stack { + s := &Stack{ + s: make([]interface{}, 0), + } + return s +} + +// Push adds an item to the top of the stack. +func (s *Stack) Push(item interface{}) { + s.l.Lock() + defer s.l.Unlock() + s.s = append(s.s, item) +} + +// Pop removes and returns the top item on the stack, or returns +// ErrEmptyStack if the stack has no content +func (s *Stack) Pop() (interface{}, error) { + s.l.Lock() + defer s.l.Unlock() + l := len(s.s) + if l > 0 { + item := s.s[l-1] + s.s = s.s[:l-1] + return item, nil + } + return nil, ErrEmptyStack{action: "Pop"} +} + +// PopString attempts to cast the top item on the stack to the string type. +// If this succeeds, it removes and returns the top item. If the item +// is not of the string type, ErrBadTypeCast is returned. If the stack +// is empty, ErrEmptyStack is returned +func (s *Stack) PopString() (string, error) { + s.l.Lock() + defer s.l.Unlock() + l := len(s.s) + if l > 0 { + item := s.s[l-1] + if item, ok := item.(string); ok { + s.s = s.s[:l-1] + return item, nil + } + return "", ErrBadTypeCast{} + } + return "", ErrEmptyStack{action: "PopString"} +} + +// Empty returns true if the stack is empty +func (s *Stack) Empty() bool { + s.l.Lock() + defer s.l.Unlock() + return len(s.s) == 0 +} diff --git a/vendor/src/github.com/docker/notary/tuf/utils/util.go b/vendor/src/github.com/docker/notary/tuf/utils/util.go new file mode 100644 index 00000000..fe5d5598 --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/utils/util.go @@ -0,0 +1,109 @@ +package utils + +import ( + "crypto/hmac" + "encoding/hex" + "errors" + "fmt" + gopath "path" + "path/filepath" + + "github.com/docker/notary/trustmanager" + "github.com/docker/notary/tuf/data" +) + +// ErrWrongLength indicates the length was different to that expected +var ErrWrongLength = errors.New("wrong length") + +// ErrWrongHash indicates the hash was different to that expected +type ErrWrongHash struct { + Type string + Expected []byte + Actual []byte +} + +// Error implements error interface +func (e ErrWrongHash) Error() string { + return fmt.Sprintf("wrong %s hash, expected %#x got %#x", e.Type, e.Expected, e.Actual) +} + +// ErrNoCommonHash indicates the metadata did not provide any hashes this +// client recognizes +type ErrNoCommonHash struct { + Expected data.Hashes + Actual data.Hashes +} + +// Error implements error interface +func (e ErrNoCommonHash) Error() string { + types := func(a data.Hashes) []string { + t := make([]string, 0, len(a)) + for typ := range a { + t = append(t, typ) + } + return t + } + return fmt.Sprintf("no common hash function, expected one of %s, got %s", types(e.Expected), types(e.Actual)) +} + +// ErrUnknownHashAlgorithm - client was ashed to use a hash algorithm +// it is not familiar with +type ErrUnknownHashAlgorithm struct { + Name string +} + +// Error implements error interface +func (e ErrUnknownHashAlgorithm) Error() string { + return fmt.Sprintf("unknown hash algorithm: %s", e.Name) +} + +// PassphraseFunc type for func that request a passphrase +type PassphraseFunc func(role string, confirm bool) ([]byte, error) + +// FileMetaEqual checks whether 2 FileMeta objects are consistent with eachother +func FileMetaEqual(actual data.FileMeta, expected data.FileMeta) error { + if actual.Length != expected.Length { + return ErrWrongLength + } + hashChecked := false + for typ, hash := range expected.Hashes { + if h, ok := actual.Hashes[typ]; ok { + hashChecked = true + if !hmac.Equal(h, hash) { + return ErrWrongHash{typ, hash, h} + } + } + } + if !hashChecked { + return ErrNoCommonHash{expected.Hashes, actual.Hashes} + } + return nil +} + +// NormalizeTarget adds a slash, if required, to the front of a target path +func NormalizeTarget(path string) string { + return gopath.Join("/", path) +} + +// HashedPaths prefixes the filename with the known hashes for the file, +// returning a list of possible consistent paths. +func HashedPaths(path string, hashes data.Hashes) []string { + paths := make([]string, 0, len(hashes)) + for _, hash := range hashes { + hashedPath := filepath.Join(filepath.Dir(path), hex.EncodeToString(hash)+"."+filepath.Base(path)) + paths = append(paths, hashedPath) + } + return paths +} + +// CanonicalKeyID returns the ID of the public bytes version of a TUF key. +// On regular RSA/ECDSA TUF keys, this is just the key ID. On X509 RSA/ECDSA +// TUF keys, this is the key ID of the public key part of the key. +func CanonicalKeyID(k data.PublicKey) (string, error) { + switch k.Algorithm() { + case data.ECDSAx509Key, data.RSAx509Key: + return trustmanager.X509PublicKeyID(k) + default: + return k.ID(), nil + } +} diff --git a/vendor/src/github.com/docker/notary/tuf/utils/utils.go b/vendor/src/github.com/docker/notary/tuf/utils/utils.go new file mode 100644 index 00000000..8de72b67 --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/utils/utils.go @@ -0,0 +1,152 @@ +package utils + +import ( + "crypto/sha256" + "crypto/sha512" + "crypto/tls" + "encoding/hex" + "fmt" + "io" + "net/http" + "net/url" + "os" + "strings" + + "github.com/docker/notary/tuf/data" +) + +// Download does a simple download from a URL +func Download(url url.URL) (*http.Response, error) { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client := &http.Client{Transport: tr} + return client.Get(url.String()) +} + +// Upload does a simple JSON upload to a URL +func Upload(url string, body io.Reader) (*http.Response, error) { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client := &http.Client{Transport: tr} + return client.Post(url, "application/json", body) +} + +// StrSliceContains checks if the given string appears in the slice +func StrSliceContains(ss []string, s string) bool { + for _, v := range ss { + if v == s { + return true + } + } + return false +} + +// StrSliceRemove removes the the given string from the slice, returning a new slice +func StrSliceRemove(ss []string, s string) []string { + res := []string{} + for _, v := range ss { + if v != s { + res = append(res, v) + } + } + return res +} + +// StrSliceContainsI checks if the given string appears in the slice +// in a case insensitive manner +func StrSliceContainsI(ss []string, s string) bool { + s = strings.ToLower(s) + for _, v := range ss { + v = strings.ToLower(v) + if v == s { + return true + } + } + return false +} + +// FileExists returns true if a file (or dir) exists at the given path, +// false otherwise +func FileExists(path string) bool { + _, err := os.Stat(path) + return os.IsNotExist(err) +} + +// NoopCloser is a simple Reader wrapper that does nothing when Close is +// called +type NoopCloser struct { + io.Reader +} + +// Close does nothing for a NoopCloser +func (nc *NoopCloser) Close() error { + return nil +} + +// DoHash returns the digest of d using the hashing algorithm named +// in alg +func DoHash(alg string, d []byte) []byte { + switch alg { + case "sha256": + digest := sha256.Sum256(d) + return digest[:] + case "sha512": + digest := sha512.Sum512(d) + return digest[:] + } + return nil +} + +// UnusedDelegationKeys prunes a list of keys, returning those that are no +// longer in use for a given targets file +func UnusedDelegationKeys(t data.SignedTargets) []string { + // compare ids to all still active key ids in all active roles + // with the targets file + found := make(map[string]bool) + for _, r := range t.Signed.Delegations.Roles { + for _, id := range r.KeyIDs { + found[id] = true + } + } + var discard []string + for id := range t.Signed.Delegations.Keys { + if !found[id] { + discard = append(discard, id) + } + } + return discard +} + +// RemoveUnusedKeys determines which keys in the slice of IDs are no longer +// used in the given targets file and removes them from the delegated keys +// map +func RemoveUnusedKeys(t *data.SignedTargets) { + unusedIDs := UnusedDelegationKeys(*t) + for _, id := range unusedIDs { + delete(t.Signed.Delegations.Keys, id) + } +} + +// FindRoleIndex returns the index of the role named or -1 if no +// matching role is found. +func FindRoleIndex(rs []*data.Role, name string) int { + for i, r := range rs { + if r.Name == name { + return i + } + } + return -1 +} + +// ConsistentName generates the appropriate HTTP URL path for the role, +// based on whether the repo is marked as consistent. The RemoteStore +// is responsible for adding file extensions. +func ConsistentName(role string, hashSha256 []byte) string { + if len(hashSha256) > 0 { + hash := hex.EncodeToString(hashSha256) + return fmt.Sprintf("%s.%s", role, hash) + } + return role +} diff --git a/vendor/src/github.com/docker/notary/tuf/validation/errors.go b/vendor/src/github.com/docker/notary/tuf/validation/errors.go new file mode 100644 index 00000000..6cca0dfa --- /dev/null +++ b/vendor/src/github.com/docker/notary/tuf/validation/errors.go @@ -0,0 +1,126 @@ +package validation + +import ( + "encoding/json" + "fmt" +) + +// VALIDATION ERRORS + +// ErrValidation represents a general validation error +type ErrValidation struct { + Msg string +} + +func (err ErrValidation) Error() string { + return fmt.Sprintf("An error occurred during validation: %s", err.Msg) +} + +// ErrBadHierarchy represents missing metadata. Currently: a missing snapshot +// at this current time. When delegations are implemented it will also +// represent a missing delegation parent +type ErrBadHierarchy struct { + Missing string + Msg string +} + +func (err ErrBadHierarchy) Error() string { + return fmt.Sprintf("Metadata hierarchy is incomplete: %s", err.Msg) +} + +// ErrBadRoot represents a failure validating the root +type ErrBadRoot struct { + Msg string +} + +func (err ErrBadRoot) Error() string { + return fmt.Sprintf("The root metadata is invalid: %s", err.Msg) +} + +// ErrBadTargets represents a failure to validate a targets (incl delegations) +type ErrBadTargets struct { + Msg string +} + +func (err ErrBadTargets) Error() string { + return fmt.Sprintf("The targets metadata is invalid: %s", err.Msg) +} + +// ErrBadSnapshot represents a failure to validate the snapshot +type ErrBadSnapshot struct { + Msg string +} + +func (err ErrBadSnapshot) Error() string { + return fmt.Sprintf("The snapshot metadata is invalid: %s", err.Msg) +} + +// END VALIDATION ERRORS + +// SerializableError is a struct that can be used to serialize an error as JSON +type SerializableError struct { + Name string + Error error +} + +// UnmarshalJSON attempts to unmarshal the error into the right type +func (s *SerializableError) UnmarshalJSON(text []byte) (err error) { + var x struct{ Name string } + err = json.Unmarshal(text, &x) + if err != nil { + return + } + var theError error + switch x.Name { + case "ErrValidation": + var e struct{ Error ErrValidation } + err = json.Unmarshal(text, &e) + theError = e.Error + case "ErrBadHierarchy": + var e struct{ Error ErrBadHierarchy } + err = json.Unmarshal(text, &e) + theError = e.Error + case "ErrBadRoot": + var e struct{ Error ErrBadRoot } + err = json.Unmarshal(text, &e) + theError = e.Error + case "ErrBadTargets": + var e struct{ Error ErrBadTargets } + err = json.Unmarshal(text, &e) + theError = e.Error + case "ErrBadSnapshot": + var e struct{ Error ErrBadSnapshot } + err = json.Unmarshal(text, &e) + theError = e.Error + default: + err = fmt.Errorf("do not know how to unmarshal %s", x.Name) + return + } + if err != nil { + return + } + s.Name = x.Name + s.Error = theError + return nil +} + +// NewSerializableError serializes one of the above errors into JSON +func NewSerializableError(err error) (*SerializableError, error) { + // make sure it's one of our errors + var name string + switch err.(type) { + case ErrValidation: + name = "ErrValidation" + case ErrBadHierarchy: + name = "ErrBadHierarchy" + case ErrBadRoot: + name = "ErrBadRoot" + case ErrBadTargets: + name = "ErrBadTargets" + case ErrBadSnapshot: + name = "ErrBadSnapshot" + default: + return nil, fmt.Errorf("does not support serializing non-validation errors") + } + return &SerializableError{Name: name, Error: err}, nil +} diff --git a/volume/drivers/adapter.go b/volume/drivers/adapter.go new file mode 100644 index 00000000..e7ca3d50 --- /dev/null +++ b/volume/drivers/adapter.go @@ -0,0 +1,106 @@ +package volumedrivers + +import ( + "fmt" + + "github.com/docker/docker/volume" +) + +type volumeDriverAdapter struct { + name string + proxy *volumeDriverProxy +} + +func (a *volumeDriverAdapter) Name() string { + return a.name +} + +func (a *volumeDriverAdapter) Create(name string, opts map[string]string) (volume.Volume, error) { + if err := a.proxy.Create(name, opts); err != nil { + return nil, err + } + return &volumeAdapter{ + proxy: a.proxy, + name: name, + driverName: a.name, + }, nil +} + +func (a *volumeDriverAdapter) Remove(v volume.Volume) error { + return a.proxy.Remove(v.Name()) +} + +func (a *volumeDriverAdapter) List() ([]volume.Volume, error) { + ls, err := a.proxy.List() + if err != nil { + return nil, err + } + + var out []volume.Volume + for _, vp := range ls { + out = append(out, &volumeAdapter{ + proxy: a.proxy, + name: vp.Name, + driverName: a.name, + eMount: vp.Mountpoint, + }) + } + return out, nil +} + +func (a *volumeDriverAdapter) Get(name string) (volume.Volume, error) { + v, err := a.proxy.Get(name) + if err != nil { + return nil, err + } + + // plugin may have returned no volume and no error + if v == nil { + return nil, fmt.Errorf("no such volume") + } + + return &volumeAdapter{ + proxy: a.proxy, + name: v.Name, + driverName: a.Name(), + eMount: v.Mountpoint, + }, nil +} + +type volumeAdapter struct { + proxy *volumeDriverProxy + name string + driverName string + eMount string // ephemeral host volume path +} + +type proxyVolume struct { + Name string + Mountpoint string +} + +func (a *volumeAdapter) Name() string { + return a.name +} + +func (a *volumeAdapter) DriverName() string { + return a.driverName +} + +func (a *volumeAdapter) Path() string { + if len(a.eMount) > 0 { + return a.eMount + } + m, _ := a.proxy.Path(a.name) + return m +} + +func (a *volumeAdapter) Mount() (string, error) { + var err error + a.eMount, err = a.proxy.Mount(a.name) + return a.eMount, err +} + +func (a *volumeAdapter) Unmount() error { + return a.proxy.Unmount(a.name) +} diff --git a/volume/drivers/extpoint.go b/volume/drivers/extpoint.go new file mode 100644 index 00000000..a55da5af --- /dev/null +++ b/volume/drivers/extpoint.go @@ -0,0 +1,164 @@ +//go:generate pluginrpc-gen -i $GOFILE -o proxy.go -type volumeDriver -name VolumeDriver + +package volumedrivers + +import ( + "fmt" + "sync" + + "github.com/docker/docker/pkg/locker" + "github.com/docker/docker/pkg/plugins" + "github.com/docker/docker/volume" +) + +// currently created by hand. generation tool would generate this like: +// $ extpoint-gen Driver > volume/extpoint.go + +var drivers = &driverExtpoint{extensions: make(map[string]volume.Driver), driverLock: &locker.Locker{}} + +const extName = "VolumeDriver" + +// NewVolumeDriver returns a driver has the given name mapped on the given client. +func NewVolumeDriver(name string, c client) volume.Driver { + proxy := &volumeDriverProxy{c} + return &volumeDriverAdapter{name: name, proxy: proxy} +} + +type opts map[string]string +type list []*proxyVolume + +// volumeDriver defines the available functions that volume plugins must implement. +// This interface is only defined to generate the proxy objects. +// It's not intended to be public or reused. +type volumeDriver interface { + // Create a volume with the given name + Create(name string, opts opts) (err error) + // Remove the volume with the given name + Remove(name string) (err error) + // Get the mountpoint of the given volume + Path(name string) (mountpoint string, err error) + // Mount the given volume and return the mountpoint + Mount(name string) (mountpoint string, err error) + // Unmount the given volume + Unmount(name string) (err error) + // List lists all the volumes known to the driver + List() (volumes list, err error) + // Get retrieves the volume with the requested name + Get(name string) (volume *proxyVolume, err error) +} + +type driverExtpoint struct { + extensions map[string]volume.Driver + sync.Mutex + driverLock *locker.Locker +} + +// Register associates the given driver to the given name, checking if +// the name is already associated +func Register(extension volume.Driver, name string) bool { + if name == "" { + return false + } + + drivers.Lock() + defer drivers.Unlock() + + _, exists := drivers.extensions[name] + if exists { + return false + } + drivers.extensions[name] = extension + return true +} + +// Unregister dissociates the name from its driver, if the association exists. +func Unregister(name string) bool { + drivers.Lock() + defer drivers.Unlock() + + _, exists := drivers.extensions[name] + if !exists { + return false + } + delete(drivers.extensions, name) + return true +} + +// Lookup returns the driver associated with the given name. If a +// driver with the given name has not been registered it checks if +// there is a VolumeDriver plugin available with the given name. +func Lookup(name string) (volume.Driver, error) { + drivers.driverLock.Lock(name) + defer drivers.driverLock.Unlock(name) + + drivers.Lock() + ext, ok := drivers.extensions[name] + drivers.Unlock() + if ok { + return ext, nil + } + + pl, err := plugins.Get(name, extName) + if err != nil { + return nil, fmt.Errorf("Error looking up volume plugin %s: %v", name, err) + } + + drivers.Lock() + defer drivers.Unlock() + if ext, ok := drivers.extensions[name]; ok { + return ext, nil + } + + d := NewVolumeDriver(name, pl.Client) + drivers.extensions[name] = d + return d, nil +} + +// GetDriver returns a volume driver by its name. +// If the driver is empty, it looks for the local driver. +func GetDriver(name string) (volume.Driver, error) { + if name == "" { + name = volume.DefaultDriverName + } + return Lookup(name) +} + +// GetDriverList returns list of volume drivers registered. +// If no driver is registered, empty string list will be returned. +func GetDriverList() []string { + var driverList []string + drivers.Lock() + for driverName := range drivers.extensions { + driverList = append(driverList, driverName) + } + drivers.Unlock() + return driverList +} + +// GetAllDrivers lists all the registered drivers +func GetAllDrivers() ([]volume.Driver, error) { + plugins, err := plugins.GetAll(extName) + if err != nil { + return nil, err + } + var ds []volume.Driver + + drivers.Lock() + defer drivers.Unlock() + + for _, d := range drivers.extensions { + ds = append(ds, d) + } + + for _, p := range plugins { + ext, ok := drivers.extensions[p.Name] + if ok { + continue + } + + ext = NewVolumeDriver(p.Name, p.Client) + drivers.extensions[p.Name] = ext + ds = append(ds, ext) + } + return ds, nil +} diff --git a/volume/drivers/extpoint_test.go b/volume/drivers/extpoint_test.go new file mode 100644 index 00000000..26c06954 --- /dev/null +++ b/volume/drivers/extpoint_test.go @@ -0,0 +1,23 @@ +package volumedrivers + +import ( + "testing" + + "github.com/docker/docker/volume/testutils" +) + +func TestGetDriver(t *testing.T) { + _, err := GetDriver("missing") + if err == nil { + t.Fatal("Expected error, was nil") + } + + Register(volumetestutils.NewFakeDriver("fake"), "fake") + d, err := GetDriver("fake") + if err != nil { + t.Fatal(err) + } + if d.Name() != "fake" { + t.Fatalf("Expected fake driver, got %s\n", d.Name()) + } +} diff --git a/volume/drivers/proxy.go b/volume/drivers/proxy.go new file mode 100644 index 00000000..5c7cdcb7 --- /dev/null +++ b/volume/drivers/proxy.go @@ -0,0 +1,207 @@ +// generated code - DO NOT EDIT + +package volumedrivers + +import "errors" + +type client interface { + Call(string, interface{}, interface{}) error +} + +type volumeDriverProxy struct { + client +} + +type volumeDriverProxyCreateRequest struct { + Name string + Opts opts +} + +type volumeDriverProxyCreateResponse struct { + Err string +} + +func (pp *volumeDriverProxy) Create(name string, opts opts) (err error) { + var ( + req volumeDriverProxyCreateRequest + ret volumeDriverProxyCreateResponse + ) + + req.Name = name + req.Opts = opts + if err = pp.Call("VolumeDriver.Create", req, &ret); err != nil { + return + } + + if ret.Err != "" { + err = errors.New(ret.Err) + } + + return +} + +type volumeDriverProxyRemoveRequest struct { + Name string +} + +type volumeDriverProxyRemoveResponse struct { + Err string +} + +func (pp *volumeDriverProxy) Remove(name string) (err error) { + var ( + req volumeDriverProxyRemoveRequest + ret volumeDriverProxyRemoveResponse + ) + + req.Name = name + if err = pp.Call("VolumeDriver.Remove", req, &ret); err != nil { + return + } + + if ret.Err != "" { + err = errors.New(ret.Err) + } + + return +} + +type volumeDriverProxyPathRequest struct { + Name string +} + +type volumeDriverProxyPathResponse struct { + Mountpoint string + Err string +} + +func (pp *volumeDriverProxy) Path(name string) (mountpoint string, err error) { + var ( + req volumeDriverProxyPathRequest + ret volumeDriverProxyPathResponse + ) + + req.Name = name + if err = pp.Call("VolumeDriver.Path", req, &ret); err != nil { + return + } + + mountpoint = ret.Mountpoint + + if ret.Err != "" { + err = errors.New(ret.Err) + } + + return +} + +type volumeDriverProxyMountRequest struct { + Name string +} + +type volumeDriverProxyMountResponse struct { + Mountpoint string + Err string +} + +func (pp *volumeDriverProxy) Mount(name string) (mountpoint string, err error) { + var ( + req volumeDriverProxyMountRequest + ret volumeDriverProxyMountResponse + ) + + req.Name = name + if err = pp.Call("VolumeDriver.Mount", req, &ret); err != nil { + return + } + + mountpoint = ret.Mountpoint + + if ret.Err != "" { + err = errors.New(ret.Err) + } + + return +} + +type volumeDriverProxyUnmountRequest struct { + Name string +} + +type volumeDriverProxyUnmountResponse struct { + Err string +} + +func (pp *volumeDriverProxy) Unmount(name string) (err error) { + var ( + req volumeDriverProxyUnmountRequest + ret volumeDriverProxyUnmountResponse + ) + + req.Name = name + if err = pp.Call("VolumeDriver.Unmount", req, &ret); err != nil { + return + } + + if ret.Err != "" { + err = errors.New(ret.Err) + } + + return +} + +type volumeDriverProxyListRequest struct { +} + +type volumeDriverProxyListResponse struct { + Volumes list + Err string +} + +func (pp *volumeDriverProxy) List() (volumes list, err error) { + var ( + req volumeDriverProxyListRequest + ret volumeDriverProxyListResponse + ) + + if err = pp.Call("VolumeDriver.List", req, &ret); err != nil { + return + } + + volumes = ret.Volumes + + if ret.Err != "" { + err = errors.New(ret.Err) + } + + return +} + +type volumeDriverProxyGetRequest struct { + Name string +} + +type volumeDriverProxyGetResponse struct { + Volume *proxyVolume + Err string +} + +func (pp *volumeDriverProxy) Get(name string) (volume *proxyVolume, err error) { + var ( + req volumeDriverProxyGetRequest + ret volumeDriverProxyGetResponse + ) + + req.Name = name + if err = pp.Call("VolumeDriver.Get", req, &ret); err != nil { + return + } + + volume = ret.Volume + + if ret.Err != "" { + err = errors.New(ret.Err) + } + + return +} diff --git a/volume/drivers/proxy_test.go b/volume/drivers/proxy_test.go new file mode 100644 index 00000000..6b26f9dc --- /dev/null +++ b/volume/drivers/proxy_test.go @@ -0,0 +1,122 @@ +package volumedrivers + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/docker/docker/pkg/plugins" + "github.com/docker/go-connections/tlsconfig" +) + +func TestVolumeRequestError(t *testing.T) { + mux := http.NewServeMux() + server := httptest.NewServer(mux) + defer server.Close() + + mux.HandleFunc("/VolumeDriver.Create", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + fmt.Fprintln(w, `{"Err": "Cannot create volume"}`) + }) + + mux.HandleFunc("/VolumeDriver.Remove", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + fmt.Fprintln(w, `{"Err": "Cannot remove volume"}`) + }) + + mux.HandleFunc("/VolumeDriver.Mount", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + fmt.Fprintln(w, `{"Err": "Cannot mount volume"}`) + }) + + mux.HandleFunc("/VolumeDriver.Unmount", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + fmt.Fprintln(w, `{"Err": "Cannot unmount volume"}`) + }) + + mux.HandleFunc("/VolumeDriver.Path", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + fmt.Fprintln(w, `{"Err": "Unknown volume"}`) + }) + + mux.HandleFunc("/VolumeDriver.List", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + fmt.Fprintln(w, `{"Err": "Cannot list volumes"}`) + }) + + mux.HandleFunc("/VolumeDriver.Get", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + fmt.Fprintln(w, `{"Err": "Cannot get volume"}`) + }) + + u, _ := url.Parse(server.URL) + client, err := plugins.NewClient("tcp://"+u.Host, tlsconfig.Options{InsecureSkipVerify: true}) + if err != nil { + t.Fatal(err) + } + + driver := volumeDriverProxy{client} + + if err = driver.Create("volume", nil); err == nil { + t.Fatal("Expected error, was nil") + } + + if !strings.Contains(err.Error(), "Cannot create volume") { + t.Fatalf("Unexpected error: %v\n", err) + } + + _, err = driver.Mount("volume") + if err == nil { + t.Fatal("Expected error, was nil") + } + + if !strings.Contains(err.Error(), "Cannot mount volume") { + t.Fatalf("Unexpected error: %v\n", err) + } + + err = driver.Unmount("volume") + if err == nil { + t.Fatal("Expected error, was nil") + } + + if !strings.Contains(err.Error(), "Cannot unmount volume") { + t.Fatalf("Unexpected error: %v\n", err) + } + + err = driver.Remove("volume") + if err == nil { + t.Fatal("Expected error, was nil") + } + + if !strings.Contains(err.Error(), "Cannot remove volume") { + t.Fatalf("Unexpected error: %v\n", err) + } + + _, err = driver.Path("volume") + if err == nil { + t.Fatal("Expected error, was nil") + } + + if !strings.Contains(err.Error(), "Unknown volume") { + t.Fatalf("Unexpected error: %v\n", err) + } + + _, err = driver.List() + if err == nil { + t.Fatal("Expected error, was nil") + } + if !strings.Contains(err.Error(), "Cannot list volumes") { + t.Fatalf("Unexpected error: %v\n", err) + } + + _, err = driver.Get("volume") + if err == nil { + t.Fatal("Expected error, was nil") + } + if !strings.Contains(err.Error(), "Cannot get volume") { + t.Fatalf("Unexpected error: %v\n", err) + } +} diff --git a/volume/local/local.go b/volume/local/local.go new file mode 100644 index 00000000..0bca731a --- /dev/null +++ b/volume/local/local.go @@ -0,0 +1,330 @@ +// Package local provides the default implementation for volumes. It +// is used to mount data volume containers and directories local to +// the host server. +package local + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sync" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/mount" + "github.com/docker/docker/utils" + "github.com/docker/docker/volume" +) + +// VolumeDataPathName is the name of the directory where the volume data is stored. +// It uses a very distinctive name to avoid collisions migrating data between +// Docker versions. +const ( + VolumeDataPathName = "_data" + volumesPathName = "volumes" +) + +var ( + // ErrNotFound is the typed error returned when the requested volume name can't be found + ErrNotFound = fmt.Errorf("volume not found") + // volumeNameRegex ensures the name assigned for the volume is valid. + // This name is used to create the bind directory, so we need to avoid characters that + // would make the path to escape the root directory. + volumeNameRegex = utils.RestrictedVolumeNamePattern +) + +type validationError struct { + error +} + +func (validationError) IsValidationError() bool { + return true +} + +type activeMount struct { + count uint64 + mounted bool +} + +// New instantiates a new Root instance with the provided scope. Scope +// is the base path that the Root instance uses to store its +// volumes. The base path is created here if it does not exist. +func New(scope string, rootUID, rootGID int) (*Root, error) { + rootDirectory := filepath.Join(scope, volumesPathName) + + if err := idtools.MkdirAllAs(rootDirectory, 0700, rootUID, rootGID); err != nil { + return nil, err + } + + r := &Root{ + scope: scope, + path: rootDirectory, + volumes: make(map[string]*localVolume), + rootUID: rootUID, + rootGID: rootGID, + } + + dirs, err := ioutil.ReadDir(rootDirectory) + if err != nil { + return nil, err + } + + mountInfos, err := mount.GetMounts() + if err != nil { + logrus.Debugf("error looking up mounts for local volume cleanup: %v", err) + } + + for _, d := range dirs { + if !d.IsDir() { + continue + } + + name := filepath.Base(d.Name()) + v := &localVolume{ + driverName: r.Name(), + name: name, + path: r.DataPath(name), + } + r.volumes[name] = v + if b, err := ioutil.ReadFile(filepath.Join(name, "opts.json")); err == nil { + if err := json.Unmarshal(b, v.opts); err != nil { + return nil, err + } + + // unmount anything that may still be mounted (for example, from an unclean shutdown) + for _, info := range mountInfos { + if info.Mountpoint == v.path { + mount.Unmount(v.path) + break + } + } + } + } + + return r, nil +} + +// Root implements the Driver interface for the volume package and +// manages the creation/removal of volumes. It uses only standard vfs +// commands to create/remove dirs within its provided scope. +type Root struct { + m sync.Mutex + scope string + path string + volumes map[string]*localVolume + rootUID int + rootGID int +} + +// List lists all the volumes +func (r *Root) List() ([]volume.Volume, error) { + var ls []volume.Volume + r.m.Lock() + for _, v := range r.volumes { + ls = append(ls, v) + } + r.m.Unlock() + return ls, nil +} + +// DataPath returns the constructed path of this volume. +func (r *Root) DataPath(volumeName string) string { + return filepath.Join(r.path, volumeName, VolumeDataPathName) +} + +// Name returns the name of Root, defined in the volume package in the DefaultDriverName constant. +func (r *Root) Name() string { + return volume.DefaultDriverName +} + +// Create creates a new volume.Volume with the provided name, creating +// the underlying directory tree required for this volume in the +// process. +func (r *Root) Create(name string, opts map[string]string) (volume.Volume, error) { + if err := r.validateName(name); err != nil { + return nil, err + } + + r.m.Lock() + defer r.m.Unlock() + + v, exists := r.volumes[name] + if exists { + return v, nil + } + + path := r.DataPath(name) + if err := idtools.MkdirAllAs(path, 0755, r.rootUID, r.rootGID); err != nil { + if os.IsExist(err) { + return nil, fmt.Errorf("volume already exists under %s", filepath.Dir(path)) + } + return nil, err + } + + var err error + defer func() { + if err != nil { + os.RemoveAll(filepath.Dir(path)) + } + }() + + v = &localVolume{ + driverName: r.Name(), + name: name, + path: path, + } + + if opts != nil { + if err = setOpts(v, opts); err != nil { + return nil, err + } + var b []byte + b, err = json.Marshal(v.opts) + if err != nil { + return nil, err + } + if err = ioutil.WriteFile(filepath.Join(filepath.Dir(path), "opts.json"), b, 600); err != nil { + return nil, err + } + } + + r.volumes[name] = v + return v, nil +} + +// Remove removes the specified volume and all underlying data. If the +// given volume does not belong to this driver and an error is +// returned. The volume is reference counted, if all references are +// not released then the volume is not removed. +func (r *Root) Remove(v volume.Volume) error { + r.m.Lock() + defer r.m.Unlock() + + lv, ok := v.(*localVolume) + if !ok { + return fmt.Errorf("unknown volume type %T", v) + } + + realPath, err := filepath.EvalSymlinks(lv.path) + if err != nil { + if !os.IsNotExist(err) { + return err + } + realPath = filepath.Dir(lv.path) + } + + if !r.scopedPath(realPath) { + return fmt.Errorf("Unable to remove a directory of out the Docker root %s: %s", r.scope, realPath) + } + + if err := removePath(realPath); err != nil { + return err + } + + delete(r.volumes, lv.name) + return removePath(filepath.Dir(lv.path)) +} + +func removePath(path string) error { + if err := os.RemoveAll(path); err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + return nil +} + +// Get looks up the volume for the given name and returns it if found +func (r *Root) Get(name string) (volume.Volume, error) { + r.m.Lock() + v, exists := r.volumes[name] + r.m.Unlock() + if !exists { + return nil, ErrNotFound + } + return v, nil +} + +func (r *Root) validateName(name string) error { + if !volumeNameRegex.MatchString(name) { + return validationError{fmt.Errorf("%q includes invalid characters for a local volume name, only %q are allowed", name, utils.RestrictedNameChars)} + } + return nil +} + +// localVolume implements the Volume interface from the volume package and +// represents the volumes created by Root. +type localVolume struct { + m sync.Mutex + usedCount int + // unique name of the volume + name string + // path is the path on the host where the data lives + path string + // driverName is the name of the driver that created the volume. + driverName string + // opts is the parsed list of options used to create the volume + opts *optsConfig + // active refcounts the active mounts + active activeMount +} + +// Name returns the name of the given Volume. +func (v *localVolume) Name() string { + return v.name +} + +// DriverName returns the driver that created the given Volume. +func (v *localVolume) DriverName() string { + return v.driverName +} + +// Path returns the data location. +func (v *localVolume) Path() string { + return v.path +} + +// Mount implements the localVolume interface, returning the data location. +func (v *localVolume) Mount() (string, error) { + v.m.Lock() + defer v.m.Unlock() + if v.opts != nil { + if !v.active.mounted { + if err := v.mount(); err != nil { + return "", err + } + v.active.mounted = true + } + v.active.count++ + } + return v.path, nil +} + +// Umount is for satisfying the localVolume interface and does not do anything in this driver. +func (v *localVolume) Unmount() error { + v.m.Lock() + defer v.m.Unlock() + if v.opts != nil { + v.active.count-- + if v.active.count == 0 { + if err := mount.Unmount(v.path); err != nil { + v.active.count++ + return err + } + v.active.mounted = false + } + } + return nil +} + +func validateOpts(opts map[string]string) error { + for opt := range opts { + if !validOpts[opt] { + return validationError{fmt.Errorf("invalid option key: %q", opt)} + } + } + return nil +} diff --git a/volume/local/local_test.go b/volume/local/local_test.go new file mode 100644 index 00000000..1baa0854 --- /dev/null +++ b/volume/local/local_test.go @@ -0,0 +1,249 @@ +package local + +import ( + "io/ioutil" + "os" + "runtime" + "strings" + "testing" + + "github.com/docker/docker/pkg/mount" +) + +func TestRemove(t *testing.T) { + // TODO Windows: Investigate why this test fails on Windows under CI + // but passes locally. + if runtime.GOOS == "windows" { + t.Skip("Test failing on Windows CI") + } + rootDir, err := ioutil.TempDir("", "local-volume-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(rootDir) + + r, err := New(rootDir, 0, 0) + if err != nil { + t.Fatal(err) + } + + vol, err := r.Create("testing", nil) + if err != nil { + t.Fatal(err) + } + + if err := r.Remove(vol); err != nil { + t.Fatal(err) + } + + vol, err = r.Create("testing2", nil) + if err != nil { + t.Fatal(err) + } + if err := os.RemoveAll(vol.Path()); err != nil { + t.Fatal(err) + } + + if err := r.Remove(vol); err != nil { + t.Fatal(err) + } + + if _, err := os.Stat(vol.Path()); err != nil && !os.IsNotExist(err) { + t.Fatal("volume dir not removed") + } + + if l, _ := r.List(); len(l) != 0 { + t.Fatal("expected there to be no volumes") + } +} + +func TestInitializeWithVolumes(t *testing.T) { + rootDir, err := ioutil.TempDir("", "local-volume-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(rootDir) + + r, err := New(rootDir, 0, 0) + if err != nil { + t.Fatal(err) + } + + vol, err := r.Create("testing", nil) + if err != nil { + t.Fatal(err) + } + + r, err = New(rootDir, 0, 0) + if err != nil { + t.Fatal(err) + } + + v, err := r.Get(vol.Name()) + if err != nil { + t.Fatal(err) + } + + if v.Path() != vol.Path() { + t.Fatal("expected to re-initialize root with existing volumes") + } +} + +func TestCreate(t *testing.T) { + rootDir, err := ioutil.TempDir("", "local-volume-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(rootDir) + + r, err := New(rootDir, 0, 0) + if err != nil { + t.Fatal(err) + } + + cases := map[string]bool{ + "name": true, + "name-with-dash": true, + "name_with_underscore": true, + "name/with/slash": false, + "name/with/../../slash": false, + "./name": false, + "../name": false, + "./": false, + "../": false, + "~": false, + ".": false, + "..": false, + "...": false, + } + + for name, success := range cases { + v, err := r.Create(name, nil) + if success { + if err != nil { + t.Fatal(err) + } + if v.Name() != name { + t.Fatalf("Expected volume with name %s, got %s", name, v.Name()) + } + } else { + if err == nil { + t.Fatalf("Expected error creating volume with name %s, got nil", name) + } + } + } +} + +func TestValidateName(t *testing.T) { + r := &Root{} + names := map[string]bool{ + "/testvol": false, + "thing.d": true, + "hello-world": true, + "./hello": false, + ".hello": false, + } + + for vol, expected := range names { + err := r.validateName(vol) + if expected && err != nil { + t.Fatalf("expected %s to be valid got %v", vol, err) + } + if !expected && err == nil { + t.Fatalf("expected %s to be invalid", vol) + } + } +} + +func TestCreateWithOpts(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip() + } + + rootDir, err := ioutil.TempDir("", "local-volume-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(rootDir) + + r, err := New(rootDir, 0, 0) + if err != nil { + t.Fatal(err) + } + + if _, err := r.Create("test", map[string]string{"invalidopt": "notsupported"}); err == nil { + t.Fatal("expected invalid opt to cause error") + } + + vol, err := r.Create("test", map[string]string{"device": "tmpfs", "type": "tmpfs", "o": "size=1m,uid=1000"}) + if err != nil { + t.Fatal(err) + } + v := vol.(*localVolume) + + dir, err := v.Mount() + if err != nil { + t.Fatal(err) + } + defer func() { + if err := v.Unmount(); err != nil { + t.Fatal(err) + } + }() + + mountInfos, err := mount.GetMounts() + if err != nil { + t.Fatal(err) + } + + var found bool + for _, info := range mountInfos { + if info.Mountpoint == dir { + found = true + if info.Fstype != "tmpfs" { + t.Fatalf("expected tmpfs mount, got %q", info.Fstype) + } + if info.Source != "tmpfs" { + t.Fatalf("expected tmpfs mount, got %q", info.Source) + } + if !strings.Contains(info.VfsOpts, "uid=1000") { + t.Fatalf("expected mount info to have uid=1000: %q", info.VfsOpts) + } + if !strings.Contains(info.VfsOpts, "size=1024k") { + t.Fatalf("expected mount info to have size=1024k: %q", info.VfsOpts) + } + break + } + } + + if !found { + t.Fatal("mount not found") + } + + if v.active.count != 1 { + t.Fatalf("Expected active mount count to be 1, got %d", v.active.count) + } + + // test double mount + if _, err := v.Mount(); err != nil { + t.Fatal(err) + } + if v.active.count != 2 { + t.Fatalf("Expected active mount count to be 2, got %d", v.active.count) + } + + if err := v.Unmount(); err != nil { + t.Fatal(err) + } + if v.active.count != 1 { + t.Fatalf("Expected active mount count to be 1, got %d", v.active.count) + } + + mounted, err := mount.Mounted(v.path) + if err != nil { + t.Fatal(err) + } + if !mounted { + t.Fatal("expected mount to still be active") + } +} diff --git a/volume/local/local_unix.go b/volume/local/local_unix.go new file mode 100644 index 00000000..2e63777a --- /dev/null +++ b/volume/local/local_unix.go @@ -0,0 +1,69 @@ +// +build linux freebsd + +// Package local provides the default implementation for volumes. It +// is used to mount data volume containers and directories local to +// the host server. +package local + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/docker/docker/pkg/mount" +) + +var ( + oldVfsDir = filepath.Join("vfs", "dir") + + validOpts = map[string]bool{ + "type": true, // specify the filesystem type for mount, e.g. nfs + "o": true, // generic mount options + "device": true, // device to mount from + } +) + +type optsConfig struct { + MountType string + MountOpts string + MountDevice string +} + +// scopedPath verifies that the path where the volume is located +// is under Docker's root and the valid local paths. +func (r *Root) scopedPath(realPath string) bool { + // Volumes path for Docker version >= 1.7 + if strings.HasPrefix(realPath, filepath.Join(r.scope, volumesPathName)) && realPath != filepath.Join(r.scope, volumesPathName) { + return true + } + + // Volumes path for Docker version < 1.7 + if strings.HasPrefix(realPath, filepath.Join(r.scope, oldVfsDir)) { + return true + } + + return false +} + +func setOpts(v *localVolume, opts map[string]string) error { + if len(opts) == 0 { + return nil + } + if err := validateOpts(opts); err != nil { + return err + } + + v.opts = &optsConfig{ + MountType: opts["type"], + MountOpts: opts["o"], + MountDevice: opts["device"], + } + return nil +} + +func (v *localVolume) mount() error { + if v.opts.MountDevice == "" { + return fmt.Errorf("missing device in volume options") + } + return mount.Mount(v.opts.MountDevice, v.path, v.opts.MountType, v.opts.MountOpts) +} diff --git a/volume/local/local_windows.go b/volume/local/local_windows.go new file mode 100644 index 00000000..1bdb368a --- /dev/null +++ b/volume/local/local_windows.go @@ -0,0 +1,34 @@ +// Package local provides the default implementation for volumes. It +// is used to mount data volume containers and directories local to +// the host server. +package local + +import ( + "fmt" + "path/filepath" + "strings" +) + +type optsConfig struct{} + +var validOpts map[string]bool + +// scopedPath verifies that the path where the volume is located +// is under Docker's root and the valid local paths. +func (r *Root) scopedPath(realPath string) bool { + if strings.HasPrefix(realPath, filepath.Join(r.scope, volumesPathName)) && realPath != filepath.Join(r.scope, volumesPathName) { + return true + } + return false +} + +func setOpts(v *localVolume, opts map[string]string) error { + if len(opts) > 0 { + return fmt.Errorf("options are not supported on this platform") + } + return nil +} + +func (v *localVolume) mount() error { + return nil +} diff --git a/volume/store/errors.go b/volume/store/errors.go new file mode 100644 index 00000000..7bdfa12b --- /dev/null +++ b/volume/store/errors.go @@ -0,0 +1,74 @@ +package store + +import ( + "errors" + "strings" +) + +var ( + // errVolumeInUse is a typed error returned when trying to remove a volume that is currently in use by a container + errVolumeInUse = errors.New("volume is in use") + // errNoSuchVolume is a typed error returned if the requested volume doesn't exist in the volume store + errNoSuchVolume = errors.New("no such volume") + // errInvalidName is a typed error returned when creating a volume with a name that is not valid on the platform + errInvalidName = errors.New("volume name is not valid on this platform") + // errNameConflict is a typed error returned on create when a volume exists with the given name, but for a different driver + errNameConflict = errors.New("conflict: volume name must be unique") +) + +// OpErr is the error type returned by functions in the store package. It describes +// the operation, volume name, and error. +type OpErr struct { + // Err is the error that occurred during the operation. + Err error + // Op is the operation which caused the error, such as "create", or "list". + Op string + // Name is the name of the resource being requested for this op, typically the volume name or the driver name. + Name string + // Refs is the list of references associated with the resource. + Refs []string +} + +// Error satisfies the built-in error interface type. +func (e *OpErr) Error() string { + if e == nil { + return "" + } + s := e.Op + if e.Name != "" { + s = s + " " + e.Name + } + + s = s + ": " + e.Err.Error() + if len(e.Refs) > 0 { + s = s + " - " + "[" + strings.Join(e.Refs, ", ") + "]" + } + return s +} + +// IsInUse returns a boolean indicating whether the error indicates that a +// volume is in use +func IsInUse(err error) bool { + return isErr(err, errVolumeInUse) +} + +// IsNotExist returns a boolean indicating whether the error indicates that the volume does not exist +func IsNotExist(err error) bool { + return isErr(err, errNoSuchVolume) +} + +// IsNameConflict returns a boolean indicating whether the error indicates that a +// volume name is already taken +func IsNameConflict(err error) bool { + return isErr(err, errNameConflict) +} + +func isErr(err error, expected error) bool { + switch pe := err.(type) { + case nil: + return false + case *OpErr: + err = pe.Err + } + return err == expected +} diff --git a/volume/store/store.go b/volume/store/store.go new file mode 100644 index 00000000..b2ac9a8c --- /dev/null +++ b/volume/store/store.go @@ -0,0 +1,506 @@ +package store + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "sync" + "time" + + "github.com/Sirupsen/logrus" + "github.com/boltdb/bolt" + "github.com/docker/docker/pkg/locker" + "github.com/docker/docker/volume" + "github.com/docker/docker/volume/drivers" +) + +const ( + volumeDataDir = "volumes" + volumeBucketName = "volumes" +) + +type volumeMetadata struct { + Name string + Labels map[string]string +} + +type volumeWithLabels struct { + volume.Volume + labels map[string]string +} + +func (v volumeWithLabels) Labels() map[string]string { + return v.labels +} + +// New initializes a VolumeStore to keep +// reference counting of volumes in the system. +func New(rootPath string) (*VolumeStore, error) { + vs := &VolumeStore{ + locks: &locker.Locker{}, + names: make(map[string]volume.Volume), + refs: make(map[string][]string), + labels: make(map[string]map[string]string), + } + + if rootPath != "" { + // initialize metadata store + volPath := filepath.Join(rootPath, volumeDataDir) + if err := os.MkdirAll(volPath, 750); err != nil { + return nil, err + } + + dbPath := filepath.Join(volPath, "metadata.db") + + var err error + vs.db, err = bolt.Open(dbPath, 0600, &bolt.Options{Timeout: 1 * time.Second}) + if err != nil { + return nil, err + } + + // initialize volumes bucket + if err := vs.db.Update(func(tx *bolt.Tx) error { + if _, err := tx.CreateBucketIfNotExists([]byte(volumeBucketName)); err != nil { + return err + } + + return nil + }); err != nil { + return nil, err + } + } + + return vs, nil +} + +func (s *VolumeStore) getNamed(name string) (volume.Volume, bool) { + s.globalLock.Lock() + v, exists := s.names[name] + s.globalLock.Unlock() + return v, exists +} + +func (s *VolumeStore) setNamed(v volume.Volume, ref string) { + s.globalLock.Lock() + s.names[v.Name()] = v + if len(ref) > 0 { + s.refs[v.Name()] = append(s.refs[v.Name()], ref) + } + s.globalLock.Unlock() +} + +func (s *VolumeStore) purge(name string) { + s.globalLock.Lock() + delete(s.names, name) + delete(s.refs, name) + delete(s.labels, name) + s.globalLock.Unlock() +} + +// VolumeStore is a struct that stores the list of volumes available and keeps track of their usage counts +type VolumeStore struct { + locks *locker.Locker + globalLock sync.Mutex + // names stores the volume name -> driver name relationship. + // This is used for making lookups faster so we don't have to probe all drivers + names map[string]volume.Volume + // refs stores the volume name and the list of things referencing it + refs map[string][]string + // labels stores volume labels for each volume + labels map[string]map[string]string + db *bolt.DB +} + +// List proxies to all registered volume drivers to get the full list of volumes +// If a driver returns a volume that has name which conflicts with another volume from a different driver, +// the first volume is chosen and the conflicting volume is dropped. +func (s *VolumeStore) List() ([]volume.Volume, []string, error) { + vols, warnings, err := s.list() + if err != nil { + return nil, nil, &OpErr{Err: err, Op: "list"} + } + var out []volume.Volume + + for _, v := range vols { + name := normaliseVolumeName(v.Name()) + + s.locks.Lock(name) + storedV, exists := s.getNamed(name) + // Note: it's not safe to populate the cache here because the volume may have been + // deleted before we acquire a lock on its name + if exists && storedV.DriverName() != v.DriverName() { + logrus.Warnf("Volume name %s already exists for driver %s, not including volume returned by %s", v.Name(), storedV.DriverName(), v.DriverName()) + s.locks.Unlock(v.Name()) + continue + } + + out = append(out, v) + s.locks.Unlock(v.Name()) + } + return out, warnings, nil +} + +// list goes through each volume driver and asks for its list of volumes. +func (s *VolumeStore) list() ([]volume.Volume, []string, error) { + drivers, err := volumedrivers.GetAllDrivers() + if err != nil { + return nil, nil, err + } + var ( + ls []volume.Volume + warnings []string + ) + + type vols struct { + vols []volume.Volume + err error + driverName string + } + chVols := make(chan vols, len(drivers)) + + for _, vd := range drivers { + go func(d volume.Driver) { + vs, err := d.List() + if err != nil { + chVols <- vols{driverName: d.Name(), err: &OpErr{Err: err, Name: d.Name(), Op: "list"}} + return + } + chVols <- vols{vols: vs} + }(vd) + } + + badDrivers := make(map[string]struct{}) + for i := 0; i < len(drivers); i++ { + vs := <-chVols + + if vs.err != nil { + warnings = append(warnings, vs.err.Error()) + badDrivers[vs.driverName] = struct{}{} + logrus.Warn(vs.err) + } + ls = append(ls, vs.vols...) + } + + if len(badDrivers) > 0 { + for _, v := range s.names { + if _, exists := badDrivers[v.DriverName()]; exists { + ls = append(ls, v) + } + } + } + return ls, warnings, nil +} + +// CreateWithRef creates a volume with the given name and driver and stores the ref +// This is just like Create() except we store the reference while holding the lock. +// This ensures there's no race between creating a volume and then storing a reference. +func (s *VolumeStore) CreateWithRef(name, driverName, ref string, opts, labels map[string]string) (volume.Volume, error) { + name = normaliseVolumeName(name) + s.locks.Lock(name) + defer s.locks.Unlock(name) + + v, err := s.create(name, driverName, opts, labels) + if err != nil { + return nil, &OpErr{Err: err, Name: name, Op: "create"} + } + + s.setNamed(v, ref) + return v, nil +} + +// Create creates a volume with the given name and driver. +func (s *VolumeStore) Create(name, driverName string, opts, labels map[string]string) (volume.Volume, error) { + name = normaliseVolumeName(name) + s.locks.Lock(name) + defer s.locks.Unlock(name) + + v, err := s.create(name, driverName, opts, labels) + if err != nil { + return nil, &OpErr{Err: err, Name: name, Op: "create"} + } + s.setNamed(v, "") + return v, nil +} + +// create asks the given driver to create a volume with the name/opts. +// If a volume with the name is already known, it will ask the stored driver for the volume. +// If the passed in driver name does not match the driver name which is stored for the given volume name, an error is returned. +// It is expected that callers of this function hold any necessary locks. +func (s *VolumeStore) create(name, driverName string, opts, labels map[string]string) (volume.Volume, error) { + // Validate the name in a platform-specific manner + valid, err := volume.IsVolumeNameValid(name) + if err != nil { + return nil, err + } + if !valid { + return nil, &OpErr{Err: errInvalidName, Name: name, Op: "create"} + } + + if v, exists := s.getNamed(name); exists { + if v.DriverName() != driverName && driverName != "" && driverName != volume.DefaultDriverName { + return nil, errNameConflict + } + return v, nil + } + + // Since there isn't a specified driver name, let's see if any of the existing drivers have this volume name + if driverName == "" { + v, _ := s.getVolume(name) + if v != nil { + return v, nil + } + } + + vd, err := volumedrivers.GetDriver(driverName) + + if err != nil { + return nil, &OpErr{Op: "create", Name: name, Err: err} + } + + logrus.Debugf("Registering new volume reference: driver %q, name %q", vd.Name(), name) + + if v, _ := vd.Get(name); v != nil { + return v, nil + } + v, err := vd.Create(name, opts) + if err != nil { + return nil, err + } + s.globalLock.Lock() + s.labels[name] = labels + s.globalLock.Unlock() + + if s.db != nil { + metadata := &volumeMetadata{ + Name: name, + Labels: labels, + } + + volData, err := json.Marshal(metadata) + if err != nil { + return nil, err + } + + if err := s.db.Update(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(volumeBucketName)) + err := b.Put([]byte(name), volData) + return err + }); err != nil { + return nil, err + } + } + + return volumeWithLabels{v, labels}, nil +} + +// GetWithRef gets a volume with the given name from the passed in driver and stores the ref +// This is just like Get(), but we store the reference while holding the lock. +// This makes sure there are no races between checking for the existence of a volume and adding a reference for it +func (s *VolumeStore) GetWithRef(name, driverName, ref string) (volume.Volume, error) { + name = normaliseVolumeName(name) + s.locks.Lock(name) + defer s.locks.Unlock(name) + + vd, err := volumedrivers.GetDriver(driverName) + if err != nil { + return nil, &OpErr{Err: err, Name: name, Op: "get"} + } + + v, err := vd.Get(name) + if err != nil { + return nil, &OpErr{Err: err, Name: name, Op: "get"} + } + + s.setNamed(v, ref) + if labels, ok := s.labels[name]; ok { + return volumeWithLabels{v, labels}, nil + } + return v, nil +} + +// Get looks if a volume with the given name exists and returns it if so +func (s *VolumeStore) Get(name string) (volume.Volume, error) { + name = normaliseVolumeName(name) + s.locks.Lock(name) + defer s.locks.Unlock(name) + + v, err := s.getVolume(name) + if err != nil { + return nil, &OpErr{Err: err, Name: name, Op: "get"} + } + s.setNamed(v, "") + return v, nil +} + +// getVolume requests the volume, if the driver info is stored it just accesses that driver, +// if the driver is unknown it probes all drivers until it finds the first volume with that name. +// it is expected that callers of this function hold any necessary locks +func (s *VolumeStore) getVolume(name string) (volume.Volume, error) { + labels := map[string]string{} + + if s.db != nil { + // get meta + if err := s.db.Update(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(volumeBucketName)) + data := b.Get([]byte(name)) + + if string(data) == "" { + return nil + } + + var meta volumeMetadata + buf := bytes.NewBuffer(data) + + if err := json.NewDecoder(buf).Decode(&meta); err != nil { + return err + } + labels = meta.Labels + + return nil + }); err != nil { + return nil, err + } + } + + logrus.Debugf("Getting volume reference for name: %s", name) + s.globalLock.Lock() + v, exists := s.names[name] + s.globalLock.Unlock() + if exists { + vd, err := volumedrivers.GetDriver(v.DriverName()) + if err != nil { + return nil, err + } + vol, err := vd.Get(name) + if err != nil { + return nil, err + } + return volumeWithLabels{vol, labels}, nil + } + + logrus.Debugf("Probing all drivers for volume with name: %s", name) + drivers, err := volumedrivers.GetAllDrivers() + if err != nil { + return nil, err + } + + for _, d := range drivers { + v, err := d.Get(name) + if err != nil { + continue + } + + return volumeWithLabels{v, labels}, nil + } + return nil, errNoSuchVolume +} + +// Remove removes the requested volume. A volume is not removed if it has any refs +func (s *VolumeStore) Remove(v volume.Volume) error { + name := normaliseVolumeName(v.Name()) + s.locks.Lock(name) + defer s.locks.Unlock(name) + + if refs, exists := s.refs[name]; exists && len(refs) > 0 { + return &OpErr{Err: errVolumeInUse, Name: v.Name(), Op: "remove", Refs: refs} + } + + vd, err := volumedrivers.GetDriver(v.DriverName()) + if err != nil { + return &OpErr{Err: err, Name: vd.Name(), Op: "remove"} + } + + logrus.Debugf("Removing volume reference: driver %s, name %s", v.DriverName(), name) + vol := withoutLabels(v) + if err := vd.Remove(vol); err != nil { + return &OpErr{Err: err, Name: name, Op: "remove"} + } + + s.purge(name) + return nil +} + +// Dereference removes the specified reference to the volume +func (s *VolumeStore) Dereference(v volume.Volume, ref string) { + s.locks.Lock(v.Name()) + defer s.locks.Unlock(v.Name()) + + s.globalLock.Lock() + defer s.globalLock.Unlock() + var refs []string + + for _, r := range s.refs[v.Name()] { + if r != ref { + refs = append(refs, r) + } + } + s.refs[v.Name()] = refs +} + +// Refs gets the current list of refs for the given volume +func (s *VolumeStore) Refs(v volume.Volume) []string { + s.locks.Lock(v.Name()) + defer s.locks.Unlock(v.Name()) + + s.globalLock.Lock() + defer s.globalLock.Unlock() + refs, exists := s.refs[v.Name()] + if !exists { + return nil + } + + refsOut := make([]string, len(refs)) + copy(refsOut, refs) + return refsOut +} + +// FilterByDriver returns the available volumes filtered by driver name +func (s *VolumeStore) FilterByDriver(name string) ([]volume.Volume, error) { + vd, err := volumedrivers.GetDriver(name) + if err != nil { + return nil, &OpErr{Err: err, Name: name, Op: "list"} + } + ls, err := vd.List() + if err != nil { + return nil, &OpErr{Err: err, Name: name, Op: "list"} + } + return ls, nil +} + +// FilterByUsed returns the available volumes filtered by if they are in use or not. +// `used=true` returns only volumes that are being used, while `used=false` returns +// only volumes that are not being used. +func (s *VolumeStore) FilterByUsed(vols []volume.Volume, used bool) []volume.Volume { + return s.filter(vols, func(v volume.Volume) bool { + s.locks.Lock(v.Name()) + l := len(s.refs[v.Name()]) + s.locks.Unlock(v.Name()) + if (used && l > 0) || (!used && l == 0) { + return true + } + return false + }) +} + +// filterFunc defines a function to allow filter volumes in the store +type filterFunc func(vol volume.Volume) bool + +// filter returns the available volumes filtered by a filterFunc function +func (s *VolumeStore) filter(vols []volume.Volume, f filterFunc) []volume.Volume { + var ls []volume.Volume + for _, v := range vols { + if f(v) { + ls = append(ls, v) + } + } + return ls +} + +func withoutLabels(v volume.Volume) volume.Volume { + if vol, ok := v.(volumeWithLabels); ok { + return vol.Volume + } + + return v +} diff --git a/volume/store/store_test.go b/volume/store/store_test.go new file mode 100644 index 00000000..4f3e3cbd --- /dev/null +++ b/volume/store/store_test.go @@ -0,0 +1,201 @@ +package store + +import ( + "errors" + "strings" + "testing" + + "github.com/docker/docker/volume/drivers" + vt "github.com/docker/docker/volume/testutils" +) + +func TestCreate(t *testing.T) { + volumedrivers.Register(vt.NewFakeDriver("fake"), "fake") + defer volumedrivers.Unregister("fake") + s, err := New("") + if err != nil { + t.Fatal(err) + } + v, err := s.Create("fake1", "fake", nil, nil) + if err != nil { + t.Fatal(err) + } + if v.Name() != "fake1" { + t.Fatalf("Expected fake1 volume, got %v", v) + } + if l, _, _ := s.List(); len(l) != 1 { + t.Fatalf("Expected 1 volume in the store, got %v: %v", len(l), l) + } + + if _, err := s.Create("none", "none", nil, nil); err == nil { + t.Fatalf("Expected unknown driver error, got nil") + } + + _, err = s.Create("fakeerror", "fake", map[string]string{"error": "create error"}, nil) + expected := &OpErr{Op: "create", Name: "fakeerror", Err: errors.New("create error")} + if err != nil && err.Error() != expected.Error() { + t.Fatalf("Expected create fakeError: create error, got %v", err) + } +} + +func TestRemove(t *testing.T) { + volumedrivers.Register(vt.NewFakeDriver("fake"), "fake") + volumedrivers.Register(vt.NewFakeDriver("noop"), "noop") + defer volumedrivers.Unregister("fake") + defer volumedrivers.Unregister("noop") + s, err := New("") + if err != nil { + t.Fatal(err) + } + + // doing string compare here since this error comes directly from the driver + expected := "no such volume" + if err := s.Remove(vt.NoopVolume{}); err == nil || !strings.Contains(err.Error(), expected) { + t.Fatalf("Expected error %q, got %v", expected, err) + } + + v, err := s.CreateWithRef("fake1", "fake", "fake", nil, nil) + if err != nil { + t.Fatal(err) + } + + if err := s.Remove(v); !IsInUse(err) { + t.Fatalf("Expected ErrVolumeInUse error, got %v", err) + } + s.Dereference(v, "fake") + if err := s.Remove(v); err != nil { + t.Fatal(err) + } + if l, _, _ := s.List(); len(l) != 0 { + t.Fatalf("Expected 0 volumes in the store, got %v, %v", len(l), l) + } +} + +func TestList(t *testing.T) { + volumedrivers.Register(vt.NewFakeDriver("fake"), "fake") + volumedrivers.Register(vt.NewFakeDriver("fake2"), "fake2") + defer volumedrivers.Unregister("fake") + defer volumedrivers.Unregister("fake2") + + s, err := New("") + if err != nil { + t.Fatal(err) + } + if _, err := s.Create("test", "fake", nil, nil); err != nil { + t.Fatal(err) + } + if _, err := s.Create("test2", "fake2", nil, nil); err != nil { + t.Fatal(err) + } + + ls, _, err := s.List() + if err != nil { + t.Fatal(err) + } + if len(ls) != 2 { + t.Fatalf("expected 2 volumes, got: %d", len(ls)) + } + + // and again with a new store + s, err = New("") + if err != nil { + t.Fatal(err) + } + ls, _, err = s.List() + if err != nil { + t.Fatal(err) + } + if len(ls) != 2 { + t.Fatalf("expected 2 volumes, got: %d", len(ls)) + } +} + +func TestFilterByDriver(t *testing.T) { + volumedrivers.Register(vt.NewFakeDriver("fake"), "fake") + volumedrivers.Register(vt.NewFakeDriver("noop"), "noop") + defer volumedrivers.Unregister("fake") + defer volumedrivers.Unregister("noop") + s, err := New("") + if err != nil { + t.Fatal(err) + } + + if _, err := s.Create("fake1", "fake", nil, nil); err != nil { + t.Fatal(err) + } + if _, err := s.Create("fake2", "fake", nil, nil); err != nil { + t.Fatal(err) + } + if _, err := s.Create("fake3", "noop", nil, nil); err != nil { + t.Fatal(err) + } + + if l, _ := s.FilterByDriver("fake"); len(l) != 2 { + t.Fatalf("Expected 2 volumes, got %v, %v", len(l), l) + } + + if l, _ := s.FilterByDriver("noop"); len(l) != 1 { + t.Fatalf("Expected 1 volume, got %v, %v", len(l), l) + } +} + +func TestFilterByUsed(t *testing.T) { + volumedrivers.Register(vt.NewFakeDriver("fake"), "fake") + volumedrivers.Register(vt.NewFakeDriver("noop"), "noop") + + s, err := New("") + if err != nil { + t.Fatal(err) + } + + if _, err := s.CreateWithRef("fake1", "fake", "volReference", nil, nil); err != nil { + t.Fatal(err) + } + if _, err := s.Create("fake2", "fake", nil, nil); err != nil { + t.Fatal(err) + } + + vols, _, err := s.List() + if err != nil { + t.Fatal(err) + } + + dangling := s.FilterByUsed(vols, false) + if len(dangling) != 1 { + t.Fatalf("expected 1 danging volume, got %v", len(dangling)) + } + if dangling[0].Name() != "fake2" { + t.Fatalf("expected danging volume fake2, got %s", dangling[0].Name()) + } + + used := s.FilterByUsed(vols, true) + if len(used) != 1 { + t.Fatalf("expected 1 used volume, got %v", len(used)) + } + if used[0].Name() != "fake1" { + t.Fatalf("expected used volume fake1, got %s", used[0].Name()) + } +} + +func TestDerefMultipleOfSameRef(t *testing.T) { + volumedrivers.Register(vt.NewFakeDriver("fake"), "fake") + + s, err := New("") + if err != nil { + t.Fatal(err) + } + + v, err := s.CreateWithRef("fake1", "fake", "volReference", nil, nil) + if err != nil { + t.Fatal(err) + } + + if _, err := s.GetWithRef("fake1", "fake", "volReference"); err != nil { + t.Fatal(err) + } + + s.Dereference(v, "volReference") + if err := s.Remove(v); err != nil { + t.Fatal(err) + } +} diff --git a/volume/store/store_unix.go b/volume/store/store_unix.go new file mode 100644 index 00000000..319c541d --- /dev/null +++ b/volume/store/store_unix.go @@ -0,0 +1,9 @@ +// +build linux freebsd + +package store + +// normaliseVolumeName is a platform specific function to normalise the name +// of a volume. This is a no-op on Unix-like platforms +func normaliseVolumeName(name string) string { + return name +} diff --git a/volume/store/store_windows.go b/volume/store/store_windows.go new file mode 100644 index 00000000..8601cdd5 --- /dev/null +++ b/volume/store/store_windows.go @@ -0,0 +1,12 @@ +package store + +import "strings" + +// normaliseVolumeName is a platform specific function to normalise the name +// of a volume. On Windows, as NTFS is case insensitive, under +// c:\ProgramData\Docker\Volumes\, the folders John and john would be synonymous. +// Hence we can't allow the volume "John" and "john" to be created as separate +// volumes. +func normaliseVolumeName(name string) string { + return strings.ToLower(name) +} diff --git a/volume/testutils/testutils.go b/volume/testutils/testutils.go new file mode 100644 index 00000000..65353369 --- /dev/null +++ b/volume/testutils/testutils.go @@ -0,0 +1,105 @@ +package volumetestutils + +import ( + "fmt" + + "github.com/docker/docker/volume" +) + +// NoopVolume is a volume that doesn't perform any operation +type NoopVolume struct{} + +// Name is the name of the volume +func (NoopVolume) Name() string { return "noop" } + +// DriverName is the name of the driver +func (NoopVolume) DriverName() string { return "noop" } + +// Path is the filesystem path to the volume +func (NoopVolume) Path() string { return "noop" } + +// Mount mounts the volume in the container +func (NoopVolume) Mount() (string, error) { return "noop", nil } + +// Unmount unmounts the volume from the container +func (NoopVolume) Unmount() error { return nil } + +// FakeVolume is a fake volume with a random name +type FakeVolume struct { + name string + driverName string +} + +// NewFakeVolume creates a new fake volume for testing +func NewFakeVolume(name string, driverName string) volume.Volume { + return FakeVolume{name: name, driverName: driverName} +} + +// Name is the name of the volume +func (f FakeVolume) Name() string { return f.name } + +// DriverName is the name of the driver +func (f FakeVolume) DriverName() string { return f.driverName } + +// Path is the filesystem path to the volume +func (FakeVolume) Path() string { return "fake" } + +// Mount mounts the volume in the container +func (FakeVolume) Mount() (string, error) { return "fake", nil } + +// Unmount unmounts the volume from the container +func (FakeVolume) Unmount() error { return nil } + +// FakeDriver is a driver that generates fake volumes +type FakeDriver struct { + name string + vols map[string]volume.Volume +} + +// NewFakeDriver creates a new FakeDriver with the specified name +func NewFakeDriver(name string) volume.Driver { + return &FakeDriver{ + name: name, + vols: make(map[string]volume.Volume), + } +} + +// Name is the name of the driver +func (d *FakeDriver) Name() string { return d.name } + +// Create initializes a fake volume. +// It returns an error if the options include an "error" key with a message +func (d *FakeDriver) Create(name string, opts map[string]string) (volume.Volume, error) { + if opts != nil && opts["error"] != "" { + return nil, fmt.Errorf(opts["error"]) + } + v := NewFakeVolume(name, d.name) + d.vols[name] = v + return v, nil +} + +// Remove deletes a volume. +func (d *FakeDriver) Remove(v volume.Volume) error { + if _, exists := d.vols[v.Name()]; !exists { + return fmt.Errorf("no such volume") + } + delete(d.vols, v.Name()) + return nil +} + +// List lists the volumes +func (d *FakeDriver) List() ([]volume.Volume, error) { + var vols []volume.Volume + for _, v := range d.vols { + vols = append(vols, v) + } + return vols, nil +} + +// Get gets the volume +func (d *FakeDriver) Get(name string) (volume.Volume, error) { + if v, exists := d.vols[name]; exists { + return v, nil + } + return nil, fmt.Errorf("no such volume") +} diff --git a/volume/volume.go b/volume/volume.go new file mode 100644 index 00000000..077cddfb --- /dev/null +++ b/volume/volume.go @@ -0,0 +1,133 @@ +package volume + +import ( + "fmt" + "os" + "runtime" + "strings" +) + +// DefaultDriverName is the driver name used for the driver +// implemented in the local package. +const DefaultDriverName string = "local" + +// Driver is for creating and removing volumes. +type Driver interface { + // Name returns the name of the volume driver. + Name() string + // Create makes a new volume with the given id. + Create(name string, opts map[string]string) (Volume, error) + // Remove deletes the volume. + Remove(vol Volume) (err error) + // List lists all the volumes the driver has + List() ([]Volume, error) + // Get retrieves the volume with the requested name + Get(name string) (Volume, error) +} + +// Volume is a place to store data. It is backed by a specific driver, and can be mounted. +type Volume interface { + // Name returns the name of the volume + Name() string + // DriverName returns the name of the driver which owns this volume. + DriverName() string + // Path returns the absolute path to the volume. + Path() string + // Mount mounts the volume and returns the absolute path to + // where it can be consumed. + Mount() (string, error) + // Unmount unmounts the volume when it is no longer in use. + Unmount() error +} + +// MountPoint is the intersection point between a volume and a container. It +// specifies which volume is to be used and where inside a container it should +// be mounted. +type MountPoint struct { + Source string // Container host directory + Destination string // Inside the container + RW bool // True if writable + Name string // Name set by user + Driver string // Volume driver to use + Volume Volume `json:"-"` + + // Note Mode is not used on Windows + Mode string `json:"Relabel"` // Originally field was `Relabel`" + + // Note Propagation is not used on Windows + Propagation string // Mount propagation string + Named bool // specifies if the mountpoint was specified by name + + // Specifies if data should be copied from the container before the first mount + // Use a pointer here so we can tell if the user set this value explicitly + // This allows us to error out when the user explicitly enabled copy but we can't copy due to the volume being populated + CopyData bool `json:"-"` +} + +// Setup sets up a mount point by either mounting the volume if it is +// configured, or creating the source directory if supplied. +func (m *MountPoint) Setup() (string, error) { + if m.Volume != nil { + return m.Volume.Mount() + } + if len(m.Source) > 0 { + if _, err := os.Stat(m.Source); err != nil { + if !os.IsNotExist(err) { + return "", err + } + if runtime.GOOS != "windows" { // Windows does not have deprecation issues here + if err := os.MkdirAll(m.Source, 0755); err != nil { + return "", err + } + } + } + return m.Source, nil + } + return "", fmt.Errorf("Unable to setup mount point, neither source nor volume defined") +} + +// Path returns the path of a volume in a mount point. +func (m *MountPoint) Path() string { + if m.Volume != nil { + return m.Volume.Path() + } + return m.Source +} + +// ParseVolumesFrom ensures that the supplied volumes-from is valid. +func ParseVolumesFrom(spec string) (string, string, error) { + if len(spec) == 0 { + return "", "", fmt.Errorf("malformed volumes-from specification: %s", spec) + } + + specParts := strings.SplitN(spec, ":", 2) + id := specParts[0] + mode := "rw" + + if len(specParts) == 2 { + mode = specParts[1] + if !ValidMountMode(mode) { + return "", "", errInvalidMode(mode) + } + // For now don't allow propagation properties while importing + // volumes from data container. These volumes will inherit + // the same propagation property as of the original volume + // in data container. This probably can be relaxed in future. + if HasPropagation(mode) { + return "", "", errInvalidMode(mode) + } + // Do not allow copy modes on volumes-from + if _, isSet := getCopyMode(mode); isSet { + return "", "", errInvalidMode(mode) + } + } + return id, mode, nil +} + +func errInvalidMode(mode string) error { + return fmt.Errorf("invalid mode: %v", mode) +} + +func errInvalidSpec(spec string) error { + return fmt.Errorf("Invalid volume specification: '%s'", spec) +} diff --git a/volume/volume_copy.go b/volume/volume_copy.go new file mode 100644 index 00000000..067537fb --- /dev/null +++ b/volume/volume_copy.go @@ -0,0 +1,28 @@ +package volume + +import "strings" + +const ( + // DefaultCopyMode is the copy mode used by default for normal/named volumes + DefaultCopyMode = true +) + +// {=isEnabled} +var copyModes = map[string]bool{ + "nocopy": false, +} + +func copyModeExists(mode string) bool { + _, exists := copyModes[mode] + return exists +} + +// GetCopyMode gets the copy mode from the mode string for mounts +func getCopyMode(mode string) (bool, bool) { + for _, o := range strings.Split(mode, ",") { + if isEnabled, exists := copyModes[o]; exists { + return isEnabled, true + } + } + return DefaultCopyMode, false +} diff --git a/volume/volume_propagation_linux.go b/volume/volume_propagation_linux.go new file mode 100644 index 00000000..f5f28205 --- /dev/null +++ b/volume/volume_propagation_linux.go @@ -0,0 +1,44 @@ +// +build linux + +package volume + +import ( + "strings" +) + +// DefaultPropagationMode defines what propagation mode should be used by +// default if user has not specified one explicitly. +const DefaultPropagationMode string = "rprivate" + +// propagation modes +var propagationModes = map[string]bool{ + "private": true, + "rprivate": true, + "slave": true, + "rslave": true, + "shared": true, + "rshared": true, +} + +// GetPropagation extracts and returns the mount propagation mode. If there +// are no specifications, then by default it is "private". +func GetPropagation(mode string) string { + for _, o := range strings.Split(mode, ",") { + if propagationModes[o] { + return o + } + } + return DefaultPropagationMode +} + +// HasPropagation checks if there is a valid propagation mode present in +// passed string. Returns true if a valid propagation mode specifier is +// present, false otherwise. +func HasPropagation(mode string) bool { + for _, o := range strings.Split(mode, ",") { + if propagationModes[o] { + return true + } + } + return false +} diff --git a/volume/volume_propagation_linux_test.go b/volume/volume_propagation_linux_test.go new file mode 100644 index 00000000..e579fa05 --- /dev/null +++ b/volume/volume_propagation_linux_test.go @@ -0,0 +1,65 @@ +// +build linux + +package volume + +import ( + "strings" + "testing" +) + +func TestParseMountSpecPropagation(t *testing.T) { + var ( + valid []string + invalid map[string]string + ) + + valid = []string{ + "/hostPath:/containerPath:shared", + "/hostPath:/containerPath:rshared", + "/hostPath:/containerPath:slave", + "/hostPath:/containerPath:rslave", + "/hostPath:/containerPath:private", + "/hostPath:/containerPath:rprivate", + "/hostPath:/containerPath:ro,shared", + "/hostPath:/containerPath:ro,slave", + "/hostPath:/containerPath:ro,private", + "/hostPath:/containerPath:ro,z,shared", + "/hostPath:/containerPath:ro,Z,slave", + "/hostPath:/containerPath:Z,ro,slave", + "/hostPath:/containerPath:slave,Z,ro", + "/hostPath:/containerPath:Z,slave,ro", + "/hostPath:/containerPath:slave,ro,Z", + "/hostPath:/containerPath:rslave,ro,Z", + "/hostPath:/containerPath:ro,rshared,Z", + "/hostPath:/containerPath:ro,Z,rprivate", + } + invalid = map[string]string{ + "/path:/path:ro,rshared,rslave": `invalid mode: ro,rshared,rslave`, + "/path:/path:ro,z,rshared,rslave": `invalid mode: ro,z,rshared,rslave`, + "/path:shared": "Invalid volume specification", + "/path:slave": "Invalid volume specification", + "/path:private": "Invalid volume specification", + "name:/absolute-path:shared": "Invalid volume specification", + "name:/absolute-path:rshared": "Invalid volume specification", + "name:/absolute-path:slave": "Invalid volume specification", + "name:/absolute-path:rslave": "Invalid volume specification", + "name:/absolute-path:private": "Invalid volume specification", + "name:/absolute-path:rprivate": "Invalid volume specification", + } + + for _, path := range valid { + if _, err := ParseMountSpec(path, "local"); err != nil { + t.Fatalf("ParseMountSpec(`%q`) should succeed: error %q", path, err) + } + } + + for path, expectedError := range invalid { + if _, err := ParseMountSpec(path, "local"); err == nil { + t.Fatalf("ParseMountSpec(`%q`) should have failed validation. Err %v", path, err) + } else { + if !strings.Contains(err.Error(), expectedError) { + t.Fatalf("ParseMountSpec(`%q`) error should contain %q, got %v", path, expectedError, err.Error()) + } + } + } +} diff --git a/volume/volume_propagation_unsupported.go b/volume/volume_propagation_unsupported.go new file mode 100644 index 00000000..0edc89ab --- /dev/null +++ b/volume/volume_propagation_unsupported.go @@ -0,0 +1,22 @@ +// +build !linux + +package volume + +// DefaultPropagationMode is used only in linux. In other cases it returns +// empty string. +const DefaultPropagationMode string = "" + +// propagation modes not supported on this platform. +var propagationModes = map[string]bool{} + +// GetPropagation is not supported. Return empty string. +func GetPropagation(mode string) string { + return DefaultPropagationMode +} + +// HasPropagation checks if there is a valid propagation mode present in +// passed string. Returns true if a valid propagation mode specifier is +// present, false otherwise. +func HasPropagation(mode string) bool { + return false +} diff --git a/volume/volume_test.go b/volume/volume_test.go new file mode 100644 index 00000000..0077e82b --- /dev/null +++ b/volume/volume_test.go @@ -0,0 +1,212 @@ +package volume + +import ( + "runtime" + "strings" + "testing" +) + +func TestParseMountSpec(t *testing.T) { + var ( + valid []string + invalid map[string]string + ) + + if runtime.GOOS == "windows" { + valid = []string{ + `d:\`, + `d:`, + `d:\path`, + `d:\path with space`, + // TODO Windows post TP4 - readonly support `d:\pathandmode:ro`, + `c:\:d:\`, + `c:\windows\:d:`, + `c:\windows:d:\s p a c e`, + `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`, + `0123456789name:d:`, + `MiXeDcAsEnAmE:d:`, + `name:D:`, + `name:D::rW`, + `name:D::RW`, + // TODO Windows post TP4 - readonly support `name:D::RO`, + `c:/:d:/forward/slashes/are/good/too`, + // TODO Windows post TP4 - readonly support `c:/:d:/including with/spaces:ro`, + `c:\Windows`, // With capital + `c:\Program Files (x86)`, // With capitals and brackets + } + invalid = map[string]string{ + ``: "Invalid volume specification: ", + `.`: "Invalid volume specification: ", + `..\`: "Invalid volume specification: ", + `c:\:..\`: "Invalid volume specification: ", + `c:\:d:\:xyzzy`: "Invalid volume specification: ", + `c:`: "cannot be c:", + `c:\`: `cannot be c:\`, + `c:\notexist:d:`: `The system cannot find the file specified`, + `c:\windows\system32\ntdll.dll:d:`: `Source 'c:\windows\system32\ntdll.dll' is not a directory`, + `name<:d:`: `Invalid volume specification`, + `name>:d:`: `Invalid volume specification`, + `name::d:`: `Invalid volume specification`, + `name":d:`: `Invalid volume specification`, + `name\:d:`: `Invalid volume specification`, + `name*:d:`: `Invalid volume specification`, + `name|:d:`: `Invalid volume specification`, + `name?:d:`: `Invalid volume specification`, + `name/:d:`: `Invalid volume specification`, + `d:\pathandmode:rw`: `Invalid volume specification`, + `con:d:`: `cannot be a reserved word for Windows filenames`, + `PRN:d:`: `cannot be a reserved word for Windows filenames`, + `aUx:d:`: `cannot be a reserved word for Windows filenames`, + `nul:d:`: `cannot be a reserved word for Windows filenames`, + `com1:d:`: `cannot be a reserved word for Windows filenames`, + `com2:d:`: `cannot be a reserved word for Windows filenames`, + `com3:d:`: `cannot be a reserved word for Windows filenames`, + `com4:d:`: `cannot be a reserved word for Windows filenames`, + `com5:d:`: `cannot be a reserved word for Windows filenames`, + `com6:d:`: `cannot be a reserved word for Windows filenames`, + `com7:d:`: `cannot be a reserved word for Windows filenames`, + `com8:d:`: `cannot be a reserved word for Windows filenames`, + `com9:d:`: `cannot be a reserved word for Windows filenames`, + `lpt1:d:`: `cannot be a reserved word for Windows filenames`, + `lpt2:d:`: `cannot be a reserved word for Windows filenames`, + `lpt3:d:`: `cannot be a reserved word for Windows filenames`, + `lpt4:d:`: `cannot be a reserved word for Windows filenames`, + `lpt5:d:`: `cannot be a reserved word for Windows filenames`, + `lpt6:d:`: `cannot be a reserved word for Windows filenames`, + `lpt7:d:`: `cannot be a reserved word for Windows filenames`, + `lpt8:d:`: `cannot be a reserved word for Windows filenames`, + `lpt9:d:`: `cannot be a reserved word for Windows filenames`, + } + + } else { + valid = []string{ + "/home", + "/home:/home", + "/home:/something/else", + "/with space", + "/home:/with space", + "relative:/absolute-path", + "hostPath:/containerPath:ro", + "/hostPath:/containerPath:rw", + "/rw:/ro", + } + invalid = map[string]string{ + "": "Invalid volume specification", + "./": "Invalid volume destination", + "../": "Invalid volume destination", + "/:../": "Invalid volume destination", + "/:path": "Invalid volume destination", + ":": "Invalid volume specification", + "/tmp:": "Invalid volume destination", + ":test": "Invalid volume specification", + ":/test": "Invalid volume specification", + "tmp:": "Invalid volume destination", + ":test:": "Invalid volume specification", + "::": "Invalid volume specification", + ":::": "Invalid volume specification", + "/tmp:::": "Invalid volume specification", + ":/tmp::": "Invalid volume specification", + "/path:rw": "Invalid volume specification", + "/path:ro": "Invalid volume specification", + "/rw:rw": "Invalid volume specification", + "path:ro": "Invalid volume specification", + "/path:/path:sw": `invalid mode: sw`, + "/path:/path:rwz": `invalid mode: rwz`, + } + } + + for _, path := range valid { + if _, err := ParseMountSpec(path, "local"); err != nil { + t.Fatalf("ParseMountSpec(`%q`) should succeed: error %q", path, err) + } + } + + for path, expectedError := range invalid { + if _, err := ParseMountSpec(path, "local"); err == nil { + t.Fatalf("ParseMountSpec(`%q`) should have failed validation. Err %v", path, err) + } else { + if !strings.Contains(err.Error(), expectedError) { + t.Fatalf("ParseMountSpec(`%q`) error should contain %q, got %v", path, expectedError, err.Error()) + } + } + } +} + +// testParseMountSpec is a structure used by TestParseMountSpecSplit for +// specifying test cases for the ParseMountSpec() function. +type testParseMountSpec struct { + bind string + driver string + expDest string + expSource string + expName string + expDriver string + expRW bool + fail bool +} + +func TestParseMountSpecSplit(t *testing.T) { + var cases []testParseMountSpec + if runtime.GOOS == "windows" { + cases = []testParseMountSpec{ + {`c:\:d:`, "local", `d:`, `c:\`, ``, "", true, false}, + {`c:\:d:\`, "local", `d:\`, `c:\`, ``, "", true, false}, + // TODO Windows post TP4 - Add readonly support {`c:\:d:\:ro`, "local", `d:\`, `c:\`, ``, "", false, false}, + {`c:\:d:\:rw`, "local", `d:\`, `c:\`, ``, "", true, false}, + {`c:\:d:\:foo`, "local", `d:\`, `c:\`, ``, "", false, true}, + {`name:d::rw`, "local", `d:`, ``, `name`, "local", true, false}, + {`name:d:`, "local", `d:`, ``, `name`, "local", true, false}, + // TODO Windows post TP4 - Add readonly support {`name:d::ro`, "local", `d:`, ``, `name`, "local", false, false}, + {`name:c:`, "", ``, ``, ``, "", true, true}, + {`driver/name:c:`, "", ``, ``, ``, "", true, true}, + } + } else { + cases = []testParseMountSpec{ + {"/tmp:/tmp1", "", "/tmp1", "/tmp", "", "", true, false}, + {"/tmp:/tmp2:ro", "", "/tmp2", "/tmp", "", "", false, false}, + {"/tmp:/tmp3:rw", "", "/tmp3", "/tmp", "", "", true, false}, + {"/tmp:/tmp4:foo", "", "", "", "", "", false, true}, + {"name:/named1", "", "/named1", "", "name", "", true, false}, + {"name:/named2", "external", "/named2", "", "name", "external", true, false}, + {"name:/named3:ro", "local", "/named3", "", "name", "local", false, false}, + {"local/name:/tmp:rw", "", "/tmp", "", "local/name", "", true, false}, + {"/tmp:tmp", "", "", "", "", "", true, true}, + } + } + + for _, c := range cases { + m, err := ParseMountSpec(c.bind, c.driver) + if c.fail { + if err == nil { + t.Fatalf("Expected error, was nil, for spec %s\n", c.bind) + } + continue + } + + if m == nil || err != nil { + t.Fatalf("ParseMountSpec failed for spec %s driver %s error %v\n", c.bind, c.driver, err.Error()) + continue + } + + if m.Destination != c.expDest { + t.Fatalf("Expected destination %s, was %s, for spec %s\n", c.expDest, m.Destination, c.bind) + } + + if m.Source != c.expSource { + t.Fatalf("Expected source %s, was %s, for spec %s\n", c.expSource, m.Source, c.bind) + } + + if m.Name != c.expName { + t.Fatalf("Expected name %s, was %s for spec %s\n", c.expName, m.Name, c.bind) + } + + if m.Driver != c.expDriver { + t.Fatalf("Expected driver %s, was %s, for spec %s\n", c.expDriver, m.Driver, c.bind) + } + + if m.RW != c.expRW { + t.Fatalf("Expected RW %v, was %v for spec %s\n", c.expRW, m.RW, c.bind) + } + } +} diff --git a/volume/volume_unix.go b/volume/volume_unix.go new file mode 100644 index 00000000..2520d7c1 --- /dev/null +++ b/volume/volume_unix.go @@ -0,0 +1,186 @@ +// +build linux freebsd darwin solaris + +package volume + +import ( + "fmt" + "path/filepath" + "strings" +) + +// read-write modes +var rwModes = map[string]bool{ + "rw": true, + "ro": true, +} + +// label modes +var labelModes = map[string]bool{ + "Z": true, + "z": true, +} + +// BackwardsCompatible decides whether this mount point can be +// used in old versions of Docker or not. +// Only bind mounts and local volumes can be used in old versions of Docker. +func (m *MountPoint) BackwardsCompatible() bool { + return len(m.Source) > 0 || m.Driver == DefaultDriverName +} + +// HasResource checks whether the given absolute path for a container is in +// this mount point. If the relative path starts with `../` then the resource +// is outside of this mount point, but we can't simply check for this prefix +// because it misses `..` which is also outside of the mount, so check both. +func (m *MountPoint) HasResource(absolutePath string) bool { + relPath, err := filepath.Rel(m.Destination, absolutePath) + return err == nil && relPath != ".." && !strings.HasPrefix(relPath, fmt.Sprintf("..%c", filepath.Separator)) +} + +// ParseMountSpec validates the configuration of mount information is valid. +func ParseMountSpec(spec, volumeDriver string) (*MountPoint, error) { + spec = filepath.ToSlash(spec) + + mp := &MountPoint{ + RW: true, + Propagation: DefaultPropagationMode, + } + if strings.Count(spec, ":") > 2 { + return nil, errInvalidSpec(spec) + } + + arr := strings.SplitN(spec, ":", 3) + if arr[0] == "" { + return nil, errInvalidSpec(spec) + } + + switch len(arr) { + case 1: + // Just a destination path in the container + mp.Destination = filepath.Clean(arr[0]) + case 2: + if isValid := ValidMountMode(arr[1]); isValid { + // Destination + Mode is not a valid volume - volumes + // cannot include a mode. eg /foo:rw + return nil, errInvalidSpec(spec) + } + // Host Source Path or Name + Destination + mp.Source = arr[0] + mp.Destination = arr[1] + case 3: + // HostSourcePath+DestinationPath+Mode + mp.Source = arr[0] + mp.Destination = arr[1] + mp.Mode = arr[2] // Mode field is used by SELinux to decide whether to apply label + if !ValidMountMode(mp.Mode) { + return nil, errInvalidMode(mp.Mode) + } + mp.RW = ReadWrite(mp.Mode) + mp.Propagation = GetPropagation(mp.Mode) + default: + return nil, errInvalidSpec(spec) + } + + //validate the volumes destination path + mp.Destination = filepath.Clean(mp.Destination) + if !filepath.IsAbs(mp.Destination) { + return nil, fmt.Errorf("Invalid volume destination path: '%s' mount path must be absolute.", mp.Destination) + } + + // Destination cannot be "/" + if mp.Destination == "/" { + return nil, fmt.Errorf("Invalid specification: destination can't be '/' in '%s'", spec) + } + + name, source := ParseVolumeSource(mp.Source) + if len(source) == 0 { + mp.Source = "" // Clear it out as we previously assumed it was not a name + mp.Driver = volumeDriver + // Named volumes can't have propagation properties specified. + // Their defaults will be decided by docker. This is just a + // safeguard. Don't want to get into situations where named + // volumes were mounted as '[r]shared' inside container and + // container does further mounts under that volume and these + // mounts become visible on host and later original volume + // cleanup becomes an issue if container does not unmount + // submounts explicitly. + if HasPropagation(mp.Mode) { + return nil, errInvalidSpec(spec) + } + } else { + mp.Source = filepath.Clean(source) + } + + copyData, isSet := getCopyMode(mp.Mode) + // do not allow copy modes on binds + if len(name) == 0 && isSet { + return nil, errInvalidMode(mp.Mode) + } + + mp.CopyData = copyData + mp.Name = name + + return mp, nil +} + +// ParseVolumeSource parses the origin sources that's mounted into the container. +// It returns a name and a source. It looks to see if the spec passed in +// is an absolute file. If it is, it assumes the spec is a source. If not, +// it assumes the spec is a name. +func ParseVolumeSource(spec string) (string, string) { + if !filepath.IsAbs(spec) { + return spec, "" + } + return "", spec +} + +// IsVolumeNameValid checks a volume name in a platform specific manner. +func IsVolumeNameValid(name string) (bool, error) { + return true, nil +} + +// ValidMountMode will make sure the mount mode is valid. +// returns if it's a valid mount mode or not. +func ValidMountMode(mode string) bool { + rwModeCount := 0 + labelModeCount := 0 + propagationModeCount := 0 + copyModeCount := 0 + + for _, o := range strings.Split(mode, ",") { + switch { + case rwModes[o]: + rwModeCount++ + case labelModes[o]: + labelModeCount++ + case propagationModes[o]: + propagationModeCount++ + case copyModeExists(o): + copyModeCount++ + default: + return false + } + } + + // Only one string for each mode is allowed. + if rwModeCount > 1 || labelModeCount > 1 || propagationModeCount > 1 || copyModeCount > 1 { + return false + } + return true +} + +// ReadWrite tells you if a mode string is a valid read-write mode or not. +// If there are no specifications w.r.t read write mode, then by default +// it returns true. +func ReadWrite(mode string) bool { + if !ValidMountMode(mode) { + return false + } + + for _, o := range strings.Split(mode, ",") { + if o == "ro" { + return false + } + } + + return true +} diff --git a/volume/volume_windows.go b/volume/volume_windows.go new file mode 100644 index 00000000..d01d10d6 --- /dev/null +++ b/volume/volume_windows.go @@ -0,0 +1,199 @@ +package volume + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/Sirupsen/logrus" +) + +// read-write modes +var rwModes = map[string]bool{ + "rw": true, +} + +// read-only modes +var roModes = map[string]bool{ + "ro": true, +} + +const ( + // Spec should be in the format [source:]destination[:mode] + // + // Examples: c:\foo bar:d:rw + // c:\foo:d:\bar + // myname:d: + // d:\ + // + // Explanation of this regex! Thanks @thaJeztah on IRC and gist for help. See + // https://gist.github.com/thaJeztah/6185659e4978789fb2b2. A good place to + // test is https://regex-golang.appspot.com/assets/html/index.html + // + // Useful link for referencing named capturing groups: + // http://stackoverflow.com/questions/20750843/using-named-matches-from-go-regex + // + // There are three match groups: source, destination and mode. + // + + // RXHostDir is the first option of a source + RXHostDir = `[a-z]:\\(?:[^\\/:*?"<>|\r\n]+\\?)*` + // RXName is the second option of a source + RXName = `[^\\/:*?"<>|\r\n]+` + // RXReservedNames are reserved names not possible on Windows + RXReservedNames = `(con)|(prn)|(nul)|(aux)|(com[1-9])|(lpt[1-9])` + + // RXSource is the combined possibilities for a source + RXSource = `((?P((` + RXHostDir + `)|(` + RXName + `))):)?` + + // Source. Can be either a host directory, a name, or omitted: + // HostDir: + // - Essentially using the folder solution from + // https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch08s18.html + // but adding case insensitivity. + // - Must be an absolute path such as c:\path + // - Can include spaces such as `c:\program files` + // - And then followed by a colon which is not in the capture group + // - And can be optional + // Name: + // - Must not contain invalid NTFS filename characters (https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx) + // - And then followed by a colon which is not in the capture group + // - And can be optional + + // RXDestination is the regex expression for the mount destination + RXDestination = `(?P([a-z]):((?:\\[^\\/:*?"<>\r\n]+)*\\?))` + // Destination (aka container path): + // - Variation on hostdir but can be a drive followed by colon as well + // - If a path, must be absolute. Can include spaces + // - Drive cannot be c: (explicitly checked in code, not RegEx) + // + + // RXMode is the regex expression for the mode of the mount + RXMode = `(:(?P(?i)rw))?` + // Temporarily for TP4, disabling the use of ro as it's not supported yet + // in the platform. TODO Windows: `(:(?P(?i)ro|rw))?` + // mode (optional) + // - Hopefully self explanatory in comparison to above. + // - Colon is not in the capture group + // +) + +// BackwardsCompatible decides whether this mount point can be +// used in old versions of Docker or not. +// Windows volumes are never backwards compatible. +func (m *MountPoint) BackwardsCompatible() bool { + return false +} + +// ParseMountSpec validates the configuration of mount information is valid. +func ParseMountSpec(spec string, volumeDriver string) (*MountPoint, error) { + var specExp = regexp.MustCompile(`^` + RXSource + RXDestination + RXMode + `$`) + + // Ensure in platform semantics for matching. The CLI will send in Unix semantics. + match := specExp.FindStringSubmatch(filepath.FromSlash(strings.ToLower(spec))) + + // Must have something back + if len(match) == 0 { + return nil, errInvalidSpec(spec) + } + + // Pull out the sub expressions from the named capture groups + matchgroups := make(map[string]string) + for i, name := range specExp.SubexpNames() { + matchgroups[name] = strings.ToLower(match[i]) + } + + mp := &MountPoint{ + Source: matchgroups["source"], + Destination: matchgroups["destination"], + RW: true, + } + if strings.ToLower(matchgroups["mode"]) == "ro" { + mp.RW = false + } + + // Volumes cannot include an explicitly supplied mode eg c:\path:rw + if mp.Source == "" && mp.Destination != "" && matchgroups["mode"] != "" { + return nil, errInvalidSpec(spec) + } + + // Note: No need to check if destination is absolute as it must be by + // definition of matching the regex. + + if filepath.VolumeName(mp.Destination) == mp.Destination { + // Ensure the destination path, if a drive letter, is not the c drive + if strings.ToLower(mp.Destination) == "c:" { + return nil, fmt.Errorf("Destination drive letter in '%s' cannot be c:", spec) + } + } else { + // So we know the destination is a path, not drive letter. Clean it up. + mp.Destination = filepath.Clean(mp.Destination) + // Ensure the destination path, if a path, is not the c root directory + if strings.ToLower(mp.Destination) == `c:\` { + return nil, fmt.Errorf(`Destination path in '%s' cannot be c:\`, spec) + } + } + + // See if the source is a name instead of a host directory + if len(mp.Source) > 0 { + validName, err := IsVolumeNameValid(mp.Source) + if err != nil { + return nil, err + } + if validName { + // OK, so the source is a name. + mp.Name = mp.Source + mp.Source = "" + + // Set the driver accordingly + mp.Driver = volumeDriver + if len(mp.Driver) == 0 { + mp.Driver = DefaultDriverName + } + } else { + // OK, so the source must be a host directory. Make sure it's clean. + mp.Source = filepath.Clean(mp.Source) + } + } + + // Ensure the host path source, if supplied, exists and is a directory + if len(mp.Source) > 0 { + var fi os.FileInfo + var err error + if fi, err = os.Stat(mp.Source); err != nil { + return nil, fmt.Errorf("Source directory '%s' could not be found: %s", mp.Source, err) + } + if !fi.IsDir() { + return nil, fmt.Errorf("Source '%s' is not a directory", mp.Source) + } + } + + logrus.Debugf("MP: Source '%s', Dest '%s', RW %t, Name '%s', Driver '%s'", mp.Source, mp.Destination, mp.RW, mp.Name, mp.Driver) + return mp, nil +} + +// IsVolumeNameValid checks a volume name in a platform specific manner. +func IsVolumeNameValid(name string) (bool, error) { + nameExp := regexp.MustCompile(`^` + RXName + `$`) + if !nameExp.MatchString(name) { + return false, nil + } + nameExp = regexp.MustCompile(`^` + RXReservedNames + `$`) + if nameExp.MatchString(name) { + return false, fmt.Errorf("Volume name %q cannot be a reserved word for Windows filenames", name) + } + return true, nil +} + +// ValidMountMode will make sure the mount mode is valid. +// returns if it's a valid mount mode or not. +func ValidMountMode(mode string) bool { + return roModes[strings.ToLower(mode)] || rwModes[strings.ToLower(mode)] +} + +// ReadWrite tells you if a mode string is a valid read-write mode or not. +func ReadWrite(mode string) bool { + return rwModes[strings.ToLower(mode)] +} -- 2.30.2

JFFMQB(hd$qD=0E8 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 O00&!V8=AFe;=cgN+}$() literal 0 HcmV?d00001 diff --git a/docs/admin/b2d_volume_images/verify.png b/docs/admin/b2d_volume_images/verify.png new file mode 100644 index 0000000000000000000000000000000000000000..843fa126bf8e7faec4c42cebc3e621a21d7a4366 GIT binary patch literal 9581 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 Bt%U#p literal 0 HcmV?d00001 diff --git a/docs/admin/b2d_volume_resize.md b/docs/admin/b2d_volume_resize.md new file mode 100644 index 00000000..68bba704 --- /dev/null +++ b/docs/admin/b2d_volume_resize.md @@ -0,0 +1,165 @@ + + +# Getting “no space left on device” errors with Boot2Docker? + +If you're using Boot2Docker with a large number of images, or the images you're +working with are very large, your pulls might start failing with "no space left +on device" errors when the Boot2Docker volume fills up. There are two solutions +you can try. + +## Solution 1: Add the `DiskImage` property in boot2docker profile + +The `boot2docker` command reads its configuration from the `$BOOT2DOCKER_PROFILE` if set, or `$BOOT2DOCKER_DIR/profile` or `$HOME/.boot2docker/profile` (on Windows this is `%USERPROFILE%/.boot2docker/profile`). + +1. View the existing configuration, use the `boot2docker config` command. + + $ boot2docker config + # boot2docker profile filename: /Users/mary/.boot2docker/profile + Init = false + Verbose = false + Driver = "virtualbox" + Clobber = true + ForceUpgradeDownload = false + SSH = "ssh" + SSHGen = "ssh-keygen" + SSHKey = "/Users/mary/.ssh/id_boot2docker" + VM = "boot2docker-vm" + Dir = "/Users/mary/.boot2docker" + ISOURL = "https://api.github.com/repos/boot2docker/boot2docker/releases" + ISO = "/Users/mary/.boot2docker/boot2docker.iso" + DiskSize = 20000 + Memory = 2048 + CPUs = 8 + SSHPort = 2022 + DockerPort = 0 + HostIP = "192.168.59.3" + DHCPIP = "192.168.59.99" + NetMask = [255, 255, 255, 0] + LowerIP = "192.168.59.103" + UpperIP = "192.168.59.254" + DHCPEnabled = true + Serial = false + SerialFile = "/Users/mary/.boot2docker/boot2docker-vm.sock" + Waittime = 300 + Retries = 75 + + The configuration shows you where `boot2docker` is looking for the `profile` file. It also output the settings that are in use. + + +2. Initialize a default file to customize using `boot2docker config > ~/.boot2docker/profile` command. + +3. Add the following lines to `$HOME/.boot2docker/profile`: + + # Disk image size in MB + DiskSize = 50000 + +4. Run the following sequence of commands to restart Boot2Docker with the new settings. + + $ boot2docker poweroff + $ boot2docker destroy + $ boot2docker init + $ boot2docker up + +## Solution 2: Increase the size of boot2docker volume + +This solution increases the volume size by first cloning it, then resizing it +using a disk partitioning tool. We recommend +[GParted](http://gparted.sourceforge.net/download.php/index.php). The tool comes +as a bootable ISO, is a free download, and works well with VirtualBox. + +1. Stop Boot2Docker + + Issue the command to stop the Boot2Docker VM on the command line: + + $ boot2docker stop + +2. Clone the VMDK image to a VDI image + + Boot2Docker ships with a VMDK image, which can't be resized by VirtualBox's + native tools. We will instead create a VDI volume and clone the VMDK volume to + it. + +3. Using the command line VirtualBox tools, clone the VMDK image to a VDI image: + + $ vboxmanage clonehd /full/path/to/boot2docker-hd.vmdk /full/path/to/.vdi --format VDI --variant Standard + +4. Resize the VDI volume + + Choose a size that will be appropriate for your needs. If you're spinning up a + lot of containers, or your containers are particularly large, larger will be + better: + + $ vboxmanage modifyhd /full/path/to/.vdi --resize + +5. Download a disk partitioning tool ISO + + To resize the volume, we'll use [GParted](http://gparted.sourceforge.net/download.php/). + Once you've downloaded the tool, add the ISO to the Boot2Docker VM IDE bus. + You might need to create the bus before you can add the ISO. + + > **Note:** + > It's important that you choose a partitioning tool that is available as an ISO so + > that the Boot2Docker VM can be booted with it. + + + + + + + + +


+ +6. Add the new VDI image + + In the settings for the Boot2Docker image in VirtualBox, remove the VMDK image + from the SATA controller and add the VDI image. + + + +7. Verify the boot order + + In the **System** settings for the Boot2Docker VM, make sure that **CD/DVD** is + at the top of the **Boot Order** list. + + + +8. Boot to the disk partitioning ISO + + Manually start the Boot2Docker VM in VirtualBox, and the disk partitioning ISO + should start up. Using GParted, choose the **GParted Live (default settings)** + option. Choose the default keyboard, language, and XWindows settings, and the + GParted tool will start up and display the VDI volume you created. Right click + on the VDI and choose **Resize/Move**. + + + +9. Drag the slider representing the volume to the maximum available size. + +10. Click **Resize/Move** followed by **Apply**. + + + +11. Quit GParted and shut down the VM. + +12. Remove the GParted ISO from the IDE controller for the Boot2Docker VM in +VirtualBox. + +13. Start the Boot2Docker VM + + Fire up the Boot2Docker VM manually in VirtualBox. The VM should log in + automatically, but if it doesn't, the credentials are `docker/tcuser`. Using + the `df -h` command, verify that your changes took effect. + + + +You're done! diff --git a/docs/admin/cfengine_process_management.md b/docs/admin/cfengine_process_management.md new file mode 100644 index 00000000..be62e410 --- /dev/null +++ b/docs/admin/cfengine_process_management.md @@ -0,0 +1,150 @@ + + +# Process management with CFEngine + +Create Docker containers with managed processes. + +Docker monitors one process in each running container and the container +lives or dies with that process. By introducing CFEngine inside Docker +containers, we can alleviate a few of the issues that may arise: + + - It is possible to easily start multiple processes within a + container, all of which will be managed automatically, with the + normal `docker run` command. + - If a managed process dies or crashes, CFEngine will start it again + within 1 minute. + - The container itself will live as long as the CFEngine scheduling + daemon (cf-execd) lives. With CFEngine, we are able to decouple the + life of the container from the uptime of the service it provides. + +## How it works + +CFEngine, together with the cfe-docker integration policies, are +installed as part of the Dockerfile. This builds CFEngine into our +Docker image. + +The Dockerfile's `ENTRYPOINT` takes an arbitrary +amount of commands (with any desired arguments) as parameters. When we +run the Docker container these parameters get written to CFEngine +policies and CFEngine takes over to ensure that the desired processes +are running in the container. + +CFEngine scans the process table for the `basename` of the commands given +to the `ENTRYPOINT` and runs the command to start the process if the `basename` +is not found. For example, if we start the container with +`docker run "/path/to/my/application parameters"`, CFEngine will look for a +process named `application` and run the command. If an entry for `application` +is not found in the process table at any point in time, CFEngine will execute +`/path/to/my/application parameters` to start the application once again. The +check on the process table happens every minute. + +Note that it is therefore important that the command to start your +application leaves a process with the basename of the command. This can +be made more flexible by making some minor adjustments to the CFEngine +policies, if desired. + +## Usage + +This example assumes you have Docker installed and working. We will +install and manage `apache2` and `sshd` +in a single container. + +There are three steps: + +1. Install CFEngine into the container. +2. Copy the CFEngine Docker process management policy into the + containerized CFEngine installation. +3. Start your application processes as part of the `docker run` command. + +### Building the image + +The first two steps can be done as part of a Dockerfile, as follows. + + FROM ubuntu + MAINTAINER Eystein Måløy Stenberg + + RUN apt-get update && apt-get install -y wget lsb-release unzip ca-certificates + + # install latest CFEngine + RUN wget -qO- http://cfengine.com/pub/gpg.key | apt-key add - + RUN echo "deb http://cfengine.com/pub/apt $(lsb_release -cs) main" > /etc/apt/sources.list.d/cfengine-community.list + RUN apt-get update && apt-get install -y cfengine-community + + # install cfe-docker process management policy + RUN wget https://github.com/estenberg/cfe-docker/archive/master.zip -P /tmp/ && unzip /tmp/master.zip -d /tmp/ + RUN cp /tmp/cfe-docker-master/cfengine/bin/* /var/cfengine/bin/ + RUN cp /tmp/cfe-docker-master/cfengine/inputs/* /var/cfengine/inputs/ + RUN rm -rf /tmp/cfe-docker-master /tmp/master.zip + + # apache2 and openssh are just for testing purposes, install your own apps here + RUN apt-get update && apt-get install -y openssh-server apache2 + RUN mkdir -p /var/run/sshd + RUN echo "root:password" | chpasswd # need a password for ssh + + ENTRYPOINT ["/var/cfengine/bin/docker_processes_run.sh"] + +By saving this file as Dockerfile to a working directory, you can then build +your image with the docker build command, e.g., +`docker build -t managed_image`. + +### Testing the container + +Start the container with `apache2` and `sshd` running and managed, forwarding +a port to our SSH instance: + + $ docker run -p 127.0.0.1:222:22 -d managed_image "/usr/sbin/sshd" "/etc/init.d/apache2 start" + +We now clearly see one of the benefits of the cfe-docker integration: it +allows to start several processes as part of a normal `docker run` command. + +We can now log in to our new container and see that both `apache2` and `sshd` +are running. We have set the root password to "password" in the Dockerfile +above and can use that to log in with ssh: + + ssh -p222 root@127.0.0.1 + + ps -ef + UID PID PPID C STIME TTY TIME CMD + root 1 0 0 07:48 ? 00:00:00 /bin/bash /var/cfengine/bin/docker_processes_run.sh /usr/sbin/sshd /etc/init.d/apache2 start + root 18 1 0 07:48 ? 00:00:00 /var/cfengine/bin/cf-execd -F + root 20 1 0 07:48 ? 00:00:00 /usr/sbin/sshd + root 32 1 0 07:48 ? 00:00:00 /usr/sbin/apache2 -k start + www-data 34 32 0 07:48 ? 00:00:00 /usr/sbin/apache2 -k start + www-data 35 32 0 07:48 ? 00:00:00 /usr/sbin/apache2 -k start + www-data 36 32 0 07:48 ? 00:00:00 /usr/sbin/apache2 -k start + root 93 20 0 07:48 ? 00:00:00 sshd: root@pts/0 + root 105 93 0 07:48 pts/0 00:00:00 -bash + root 112 105 0 07:49 pts/0 00:00:00 ps -ef + +If we stop apache2, it will be started again within a minute by +CFEngine. + + service apache2 status + Apache2 is running (pid 32). + service apache2 stop + * Stopping web server apache2 ... waiting [ OK ] + service apache2 status + Apache2 is NOT running. + # ... wait up to 1 minute... + service apache2 status + Apache2 is running (pid 173). + +## Adapting to your applications + +To make sure your applications get managed in the same manner, there are +just two things you need to adjust from the above example: + + - In the Dockerfile used above, install your applications instead of + `apache2` and `sshd`. + - When you start the container with `docker run`, + specify the command line arguments to your applications rather than + `apache2` and `sshd`. diff --git a/docs/admin/chef.md b/docs/admin/chef.md new file mode 100644 index 00000000..ba2f6809 --- /dev/null +++ b/docs/admin/chef.md @@ -0,0 +1,75 @@ + + +# Using Chef + +> **Note**: +> Please note this is a community contributed installation path. + +## Requirements + +To use this guide you'll need a working installation of +[Chef](https://www.chef.io/). This cookbook supports a variety of +operating systems. + +## Installation + +The cookbook is available on the [Chef Supermarket](https://supermarket.chef.io/cookbooks/docker) and can be +installed using your favorite cookbook dependency manager. + +The source can be found on +[GitHub](https://github.com/someara/chef-docker). + +Usage +----- +- Add ```depends 'docker', '~> 2.0'``` to your cookbook's metadata.rb +- Use resources shipped in cookbook in a recipe, the same way you'd + use core Chef resources (file, template, directory, package, etc). + +```ruby +docker_service 'default' do + action [:create, :start] +end + +docker_image 'busybox' do + action :pull +end + +docker_container 'an echo server' do + repo 'busybox' + port '1234:1234' + command "nc -ll -p 1234 -e /bin/cat" +end +``` + +## Getting Started +Here's a quick example of pulling the latest image and running a +container with exposed ports. + +```ruby +# Pull latest image +docker_image 'nginx' do + tag 'latest' + action :pull +end + +# Run container exposing ports +docker_container 'my_nginx' do + repo 'nginx' + tag 'latest' + port '80:80' + binds [ '/some/local/files/:/etc/nginx/conf.d' ] + host_name 'www' + domain_name 'computers.biz' + env 'FOO=bar' + subscribes :redeploy, 'docker_image[nginx]' +end +``` diff --git a/docs/admin/configuring.md b/docs/admin/configuring.md new file mode 100644 index 00000000..e7556ab7 --- /dev/null +++ b/docs/admin/configuring.md @@ -0,0 +1,280 @@ + + +# Configuring and running Docker on various distributions + +After successfully installing Docker, the `docker` daemon runs with its default +configuration. + +In a production environment, system administrators typically configure the +`docker` daemon to start and stop according to an organization's requirements. In most +cases, the system administrator configures a process manager such as `SysVinit`, `Upstart`, +or `systemd` to manage the `docker` daemon's start and stop. + +### Running the docker daemon directly + +The `docker` daemon can be run directly using the `docker daemon` command. By default it listens on +the Unix socket `unix:///var/run/docker.sock` + + $ docker daemon + + INFO[0000] +job init_networkdriver() + INFO[0000] +job serveapi(unix:///var/run/docker.sock) + INFO[0000] Listening for HTTP on unix (/var/run/docker.sock) + ... + ... + +### Configuring the docker daemon directly + +If you're running the `docker` daemon directly by running `docker daemon` instead +of using a process manager, you can append the configuration options to the `docker` run +command directly. Other options can be passed to the `docker` daemon to configure it. + +Some of the daemon's options are: + +| Flag | Description | +|-----------------------|-----------------------------------------------------------| +| `-D`, `--debug=false` | Enable or disable debug mode. By default, this is false. | +| `-H`,`--host=[]` | Daemon socket(s) to connect to. | +| `--tls=false` | Enable or disable TLS. By default, this is false. | + + +Here is a an example of running the `docker` daemon with configuration options: + + $ docker daemon -D --tls=true --tlscert=/var/docker/server.pem --tlskey=/var/docker/serverkey.pem -H tcp://192.168.59.3:2376 + +These options : + +- Enable `-D` (debug) mode +- Set `tls` to true with the server certificate and key specified using `--tlscert` and `--tlskey` respectively +- Listen for connections on `tcp://192.168.59.3:2376` + +The command line reference has the [complete list of daemon flags](../reference/commandline/daemon.md) +with explanations. + +### Daemon debugging + +As noted above, setting the log level of the daemon to "debug" or enabling debug mode +with `-D` allows the administrator or operator to gain much more knowledge about the +runtime activity of the daemon. If faced with a non-responsive daemon, the administrator +can force a full stack trace of all threads to be added to the daemon log by sending the +`SIGUSR1` signal to the Docker daemon. A common way to send this signal is using the `kill` +command on Linux systems. For example, `kill -USR1 ` sends the `SIGUSR1` +signal to the daemon process, causing the stack dump to be added to the daemon log. + +> **Note:** The log level setting of the daemon must be at least "info" level and above for +> the stack trace to be saved to the logfile. By default the daemon's log level is set to +> "info". + +The daemon will continue operating after handling the `SIGUSR1` signal and dumping the stack +traces to the log. The stack traces can be used to determine the state of all goroutines and +threads within the daemon. + +## Ubuntu + +As of `14.04`, Ubuntu uses Upstart as a process manager. By default, Upstart jobs +are located in `/etc/init` and the `docker` Upstart job can be found at `/etc/init/docker.conf`. + +After successfully [installing Docker for Ubuntu](../installation/linux/ubuntulinux.md), +you can check the running status using Upstart in this way: + + $ sudo status docker + + docker start/running, process 989 + +### Running Docker + +You can start/stop/restart the `docker` daemon using + + $ sudo start docker + + $ sudo stop docker + + $ sudo restart docker + + +### Configuring Docker + +The instructions below depict configuring Docker on a system that uses `upstart` +as the process manager. As of Ubuntu 15.04, Ubuntu uses `systemd` as its process +manager. For Ubuntu 15.04 and higher, refer to [control and configure Docker with systemd](systemd.md). + +You configure the `docker` daemon in the `/etc/default/docker` file on your +system. You do this by specifying values in a `DOCKER_OPTS` variable. + +To configure Docker options: + +1. Log into your host as a user with `sudo` or `root` privileges. + +2. If you don't have one, create the `/etc/default/docker` file on your host. Depending on how +you installed Docker, you may already have this file. + +3. Open the file with your favorite editor. + + ``` + $ sudo vi /etc/default/docker + ``` + +4. Add a `DOCKER_OPTS` variable with the following options. These options are appended to the +`docker` daemon's run command. + +``` + DOCKER_OPTS="-D --tls=true --tlscert=/var/docker/server.pem --tlskey=/var/docker/serverkey.pem -H tcp://192.168.59.3:2376" +``` + +These options : + +- Enable `-D` (debug) mode +- Set `tls` to true with the server certificate and key specified using `--tlscert` and `--tlskey` respectively +- Listen for connections on `tcp://192.168.59.3:2376` + +The command line reference has the [complete list of daemon flags](../reference/commandline/daemon.md) +with explanations. + + +5. Save and close the file. + +6. Restart the `docker` daemon. + + ``` + $ sudo restart docker + ``` + +7. Verify that the `docker` daemon is running as specified with the `ps` command. + + ``` + $ ps aux | grep docker | grep -v grep + ``` + +### Logs + +By default logs for Upstart jobs are located in `/var/log/upstart` and the logs for `docker` daemon +can be located at `/var/log/upstart/docker.log` + + $ tail -f /var/log/upstart/docker.log + INFO[0000] Loading containers: done. + INFO[0000] Docker daemon commit=1b09a95-unsupported graphdriver=aufs version=1.11.0-dev + INFO[0000] +job acceptconnections() + INFO[0000] -job acceptconnections() = OK (0) + INFO[0000] Daemon has completed initialization + + +## CentOS / Red Hat Enterprise Linux / Fedora + +As of `7.x`, CentOS and RHEL use `systemd` as the process manager. As of `21`, Fedora uses +`systemd` as its process manager. + +After successfully installing Docker for [CentOS](../installation/linux/centos.md)/[Red Hat Enterprise Linux](../installation/linux/rhel.md)/[Fedora](../installation/linux/fedora.md), you can check the running status in this way: + + $ sudo systemctl status docker + +### Running Docker + +You can start/stop/restart the `docker` daemon using + + $ sudo systemctl start docker + + $ sudo systemctl stop docker + + $ sudo systemctl restart docker + +If you want Docker to start at boot, you should also: + + $ sudo systemctl enable docker + +### Configuring Docker + +For CentOS 7.x and RHEL 7.x you can [control and configure Docker with systemd](systemd.md). + +Previously, for CentOS 6.x and RHEL 6.x you would configure the `docker` daemon in +the `/etc/sysconfig/docker` file on your system. You would do this by specifying +values in a `other_args` variable. For a short time in CentOS 7.x and RHEL 7.x you +would specify values in a `OPTIONS` variable. This is no longer recommended in favor +of using systemd directly. + +For this section, we will use CentOS 7.x as an example to configure the `docker` daemon. + +To configure Docker options: + +1. Log into your host as a user with `sudo` or `root` privileges. + +2. Create the `/etc/systemd/system/docker.service.d` directory. + + ``` + $ sudo mkdir /etc/systemd/system/docker.service.d + ``` + +3. Create a `/etc/systemd/system/docker.service.d/docker.conf` file. + +4. Open the file with your favorite editor. + + ``` + $ sudo vi /etc/systemd/system/docker.service.d/docker.conf + ``` + +5. Override the `ExecStart` configuration from your `docker.service` file to customize +the `docker` daemon. To modify the `ExecStart` configuration you have to specify +an empty configuration followed by a new one as follows: + +``` +[Service] +ExecStart= +ExecStart=/usr/bin/docker daemon -H fd:// -D --tls=true --tlscert=/var/docker/server.pem --tlskey=/var/docker/serverkey.pem -H tcp://192.168.59.3:2376 +``` + +These options : + +- Enable `-D` (debug) mode +- Set `tls` to true with the server certificate and key specified using `--tlscert` and `--tlskey` respectively +- Listen for connections on `tcp://192.168.59.3:2376` + +The command line reference has the [complete list of daemon flags](../reference/commandline/daemon.md) +with explanations. + +6. Save and close the file. + +7. Flush changes. + + ``` + $ sudo systemctl daemon-reload + ``` + +8. Restart the `docker` daemon. + + ``` + $ sudo systemctl restart docker + ``` + +9. Verify that the `docker` daemon is running as specified with the `ps` command. + + ``` + $ ps aux | grep docker | grep -v grep + ``` + +### Logs + +systemd has its own logging system called the journal. The logs for the `docker` daemon can +be viewed using `journalctl -u docker` + + $ sudo journalctl -u docker + May 06 00:22:05 localhost.localdomain systemd[1]: Starting Docker Application Container Engine... + May 06 00:22:05 localhost.localdomain docker[2495]: time="2015-05-06T00:22:05Z" level="info" msg="+job serveapi(unix:///var/run/docker.sock)" + May 06 00:22:05 localhost.localdomain docker[2495]: time="2015-05-06T00:22:05Z" level="info" msg="Listening for HTTP on unix (/var/run/docker.sock)" + May 06 00:22:06 localhost.localdomain docker[2495]: time="2015-05-06T00:22:06Z" level="info" msg="+job init_networkdriver()" + May 06 00:22:06 localhost.localdomain docker[2495]: time="2015-05-06T00:22:06Z" level="info" msg="-job init_networkdriver() = OK (0)" + May 06 00:22:06 localhost.localdomain docker[2495]: time="2015-05-06T00:22:06Z" level="info" msg="Loading containers: start." + May 06 00:22:06 localhost.localdomain docker[2495]: time="2015-05-06T00:22:06Z" level="info" msg="Loading containers: done." + May 06 00:22:06 localhost.localdomain docker[2495]: time="2015-05-06T00:22:06Z" level="info" msg="Docker daemon commit=1b09a95-unsupported graphdriver=aufs version=1.11.0-dev" + May 06 00:22:06 localhost.localdomain docker[2495]: time="2015-05-06T00:22:06Z" level="info" msg="+job acceptconnections()" + May 06 00:22:06 localhost.localdomain docker[2495]: time="2015-05-06T00:22:06Z" level="info" msg="-job acceptconnections() = OK (0)" + +_Note: Using and configuring journal is an advanced topic and is beyond the scope of this article._ diff --git a/docs/admin/dsc.md b/docs/admin/dsc.md new file mode 100644 index 00000000..84bd1d4f --- /dev/null +++ b/docs/admin/dsc.md @@ -0,0 +1,174 @@ + + +# Using PowerShell DSC + +Windows PowerShell Desired State Configuration (DSC) is a configuration +management tool that extends the existing functionality of Windows PowerShell. +DSC uses a declarative syntax to define the state in which a target should be +configured. More information about PowerShell DSC can be found at +[http://technet.microsoft.com/en-us/library/dn249912.aspx](http://technet.microsoft.com/en-us/library/dn249912.aspx). + +## Requirements + +To use this guide you'll need a Windows host with PowerShell v4.0 or newer. + +The included DSC configuration script also uses the official PPA so +only an Ubuntu target is supported. The Ubuntu target must already have the +required OMI Server and PowerShell DSC for Linux providers installed. More +information can be found at [https://github.com/MSFTOSSMgmt/WPSDSCLinux](https://github.com/MSFTOSSMgmt/WPSDSCLinux). +The source repository listed below also includes PowerShell DSC for Linux +installation and init scripts along with more detailed installation information. + +## Installation + +The DSC configuration example source is available in the following repository: +[https://github.com/anweiss/DockerClientDSC](https://github.com/anweiss/DockerClientDSC). It can be cloned with: + + $ git clone https://github.com/anweiss/DockerClientDSC.git + +## Usage + +The DSC configuration utilizes a set of shell scripts to determine whether or +not the specified Docker components are configured on the target node(s). The +source repository also includes a script (`RunDockerClientConfig.ps1`) that can +be used to establish the required CIM session(s) and execute the +`Set-DscConfiguration` cmdlet. + +More detailed usage information can be found at +[https://github.com/anweiss/DockerClientDSC](https://github.com/anweiss/DockerClientDSC). + +### Install Docker +The Docker installation configuration is equivalent to running: + +``` +apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys\ +36A1D7869245C8950F966E92D8576A8BA88D21E9 +sh -c "echo deb https://apt.dockerproject.org/repo ubuntu-trusty main\ +> /etc/apt/sources.list.d/docker.list" +apt-get update +apt-get install docker-engine +``` + +Ensure that your current working directory is set to the `DockerClientDSC` +source and load the DockerClient configuration into the current PowerShell +session + +```powershell +. .\DockerClient.ps1 +``` + +Generate the required DSC configuration .mof file for the targeted node + +```powershell +DockerClient -Hostname "myhost" +``` + +A sample DSC configuration data file has also been included and can be modified +and used in conjunction with or in place of the `Hostname` parameter: + +```powershell +DockerClient -ConfigurationData .\DockerConfigData.psd1 +``` + +Start the configuration application process on the targeted node + +```powershell +.\RunDockerClientConfig.ps1 -Hostname "myhost" +``` + +The `RunDockerClientConfig.ps1` script can also parse a DSC configuration data +file and execute configurations against multiple nodes as such: + +```powershell +.\RunDockerClientConfig.ps1 -ConfigurationData .\DockerConfigData.psd1 +``` + +### Images +Image configuration is equivalent to running: `docker pull [image]` or +`docker rmi -f [IMAGE]`. + +Using the same steps defined above, execute `DockerClient` with the `Image` +parameter and apply the configuration: + +```powershell +DockerClient -Hostname "myhost" -Image "node" +.\RunDockerClientConfig.ps1 -Hostname "myhost" +``` + +You can also configure the host to pull multiple images: + +```powershell +DockerClient -Hostname "myhost" -Image "node","mongo" +.\RunDockerClientConfig.ps1 -Hostname "myhost" +``` + +To remove images, use a hashtable as follows: + +```powershell +DockerClient -Hostname "myhost" -Image @{Name="node"; Remove=$true} +.\RunDockerClientConfig.ps1 -Hostname $hostname +``` + +### Containers +Container configuration is equivalent to running: + +``` +docker run -d --name="[containername]" -p '[port]' -e '[env]' --link '[link]'\ +'[image]' '[command]' +``` +or + +``` +docker rm -f [containername] +``` + +To create or remove containers, you can use the `Container` parameter with one +or more hashtables. The hashtable(s) passed to this parameter can have the +following properties: + +- Name (required) +- Image (required unless Remove property is set to `$true`) +- Port +- Env +- Link +- Command +- Remove + +For example, create a hashtable with the settings for your container: + +```powershell +$webContainer = @{Name="web"; Image="anweiss/docker-platynem"; Port="80:80"} +``` + +Then, using the same steps defined above, execute +`DockerClient` with the `-Image` and `-Container` parameters: + +```powershell +DockerClient -Hostname "myhost" -Image node -Container $webContainer +.\RunDockerClientConfig.ps1 -Hostname "myhost" +``` + +Existing containers can also be removed as follows: + +```powershell +$containerToRemove = @{Name="web"; Remove=$true} +DockerClient -Hostname "myhost" -Container $containerToRemove +.\RunDockerClientConfig.ps1 -Hostname "myhost" +``` + +Here is a hashtable with all of the properties that can be used to create a +container: + +```powershell +$containerProps = @{Name="web"; Image="node:latest"; Port="80:80"; ` +Env="PORT=80"; Link="db:db"; Command="grunt"} +``` diff --git a/docs/admin/formatting.md b/docs/admin/formatting.md new file mode 100644 index 00000000..d7c764d7 --- /dev/null +++ b/docs/admin/formatting.md @@ -0,0 +1,66 @@ + + +# Formatting reference + +Docker uses [Go templates](https://golang.org/pkg/text/template/) to allow users manipulate the output format +of certain commands and log drivers. Each command a driver provides a detailed +list of elements they support in their templates: + +- [Docker Images formatting](https://docs.docker.com/engine/reference/commandline/images/#formatting) +- [Docker Inspect formatting](https://docs.docker.com/engine/reference/commandline/inspect/#examples) +- [Docker Log Tag formatting](https://docs.docker.com/engine/admin/logging/log_tags/) +- [Docker Network Inspect formatting](https://docs.docker.com/engine/reference/commandline/network_inspect/) +- [Docker PS formatting](https://docs.docker.com/engine/reference/commandline/ps/#formatting) +- [Docker Volume Inspect formatting](https://docs.docker.com/engine/reference/commandline/volume_inspect/) +- [Docker Version formatting](https://docs.docker.com/engine/reference/commandline/version/#examples) + +## Template functions + +Docker provides a set of basic functions to manipulate template elements. +This is the complete list of the available functions with examples: + +### Join + +Join concatenates a list of strings to create a single string. +It puts a separator between each element in the list. + + $ docker ps --format '{{join .Names " or "}}' + +### Json + +Json encodes an element as a json string. + + $ docker inspect --format '{{json .Mounts}}' container + +### Lower + +Lower turns a string into its lower case representation. + + $ docker inspect --format "{{lower .Name}}" container + +### Split + +Split slices a string into a list of strings separated by a separator. + + # docker inspect --format '{{split (join .Names "/") "/"}}' container + +### Title + +Title capitalizes a string. + + $ docker inspect --format "{{title .Name}}" container + +### Upper + +Upper turms a string into its upper case representation. + + $ docker inspect --format "{{upper .Name}}" container diff --git a/docs/admin/host_integration.md b/docs/admin/host_integration.md new file mode 100644 index 00000000..3f71592c --- /dev/null +++ b/docs/admin/host_integration.md @@ -0,0 +1,88 @@ + + +# Automatically start containers + +As of Docker 1.2, +[restart policies](../reference/run.md#restart-policies-restart) are the +built-in Docker mechanism for restarting containers when they exit. If set, +restart policies will be used when the Docker daemon starts up, as typically +happens after a system boot. Restart policies will ensure that linked containers +are started in the correct order. + +If restart policies don't suit your needs (i.e., you have non-Docker processes +that depend on Docker containers), you can use a process manager like +[upstart](http://upstart.ubuntu.com/), +[systemd](http://freedesktop.org/wiki/Software/systemd/) or +[supervisor](http://supervisord.org/) instead. + + +## Using a process manager + +Docker does not set any restart policies by default, but be aware that they will +conflict with most process managers. So don't set restart policies if you are +using a process manager. + +When you have finished setting up your image and are happy with your +running container, you can then attach a process manager to manage it. +When you run `docker start -a`, Docker will automatically attach to the +running container, or start it if needed and forward all signals so that +the process manager can detect when a container stops and correctly +restart it. + +Here are a few sample scripts for systemd and upstart to integrate with +Docker. + + +## Examples + +The examples below show configuration files for two popular process managers, +upstart and systemd. In these examples, we'll assume that we have already +created a container to run Redis with `--name=redis_server`. These files define +a new service that will be started after the docker daemon service has started. + + +### upstart + + description "Redis container" + author "Me" + start on filesystem and started docker + stop on runlevel [!2345] + respawn + script + /usr/bin/docker start -a redis_server + end script + +### systemd + + [Unit] + Description=Redis container + Requires=docker.service + After=docker.service + + [Service] + Restart=always + ExecStart=/usr/bin/docker start -a redis_server + ExecStop=/usr/bin/docker stop -t 2 redis_server + + [Install] + WantedBy=local.target + +If you need to pass options to the redis container (such as `--env`), +then you'll need to use `docker run` rather than `docker start`. This will +create a new container every time the service is started, which will be stopped +and removed when the service is stopped. + + [Service] + ... + ExecStart=/usr/bin/docker run --env foo=bar --name redis_server redis + ExecStop=/usr/bin/docker stop -t 2 redis_server ; /usr/bin/docker rm -f redis_server + ... diff --git a/docs/admin/index.md b/docs/admin/index.md new file mode 100644 index 00000000..0f0ab7d4 --- /dev/null +++ b/docs/admin/index.md @@ -0,0 +1,11 @@ + diff --git a/docs/admin/logging/awslogs.md b/docs/admin/logging/awslogs.md new file mode 100644 index 00000000..3f90c414 --- /dev/null +++ b/docs/admin/logging/awslogs.md @@ -0,0 +1,90 @@ + + +# Amazon CloudWatch Logs logging driver + +The `awslogs` logging driver sends container logs to +[Amazon CloudWatch Logs](https://aws.amazon.com/cloudwatch/details/#log-monitoring). +Log entries can be retrieved through the [AWS Management +Console](https://console.aws.amazon.com/cloudwatch/home#logs:) or the [AWS SDKs +and Command Line Tools](http://docs.aws.amazon.com/cli/latest/reference/logs/index.html). + +## Usage + +You can configure the default logging driver by passing the `--log-driver` +option to the Docker daemon: + + docker daemon --log-driver=awslogs + +You can set the logging driver for a specific container by using the +`--log-driver` option to `docker run`: + + docker run --log-driver=awslogs ... + +## Amazon CloudWatch Logs options + +You can use the `--log-opt NAME=VALUE` flag to specify Amazon CloudWatch Logs logging driver options. + +### awslogs-region + +The `awslogs` logging driver sends your Docker logs to a specific region. Use +the `awslogs-region` log option or the `AWS_REGION` environment variable to set +the region. By default, if your Docker daemon is running on an EC2 instance +and no region is set, the driver uses the instance's region. + + docker run --log-driver=awslogs --log-opt awslogs-region=us-east-1 ... + +### awslogs-group + +You must specify a +[log group](http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/WhatIsCloudWatchLogs.html) +for the `awslogs` logging driver. You can specify the log group with the +`awslogs-group` log option: + + docker run --log-driver=awslogs --log-opt awslogs-region=us-east-1 --log-opt awslogs-group=myLogGroup ... + +### awslogs-stream + +To configure which +[log stream](http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/WhatIsCloudWatchLogs.html) +should be used, you can specify the `awslogs-stream` log option. If not +specified, the container ID is used as the log stream. + +> **Note:** +> Log streams within a given log group should only be used by one container +> at a time. Using the same log stream for multiple containers concurrently +> can cause reduced logging performance. + +## Credentials + +You must provide AWS credentials to the Docker daemon to use the `awslogs` +logging driver. You can provide these credentials with the `AWS_ACCESS_KEY_ID`, +`AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN` environment variables, the +default AWS shared credentials file (`~/.aws/credentials` of the root user), or +(if you are running the Docker daemon on an Amazon EC2 instance) the Amazon EC2 +instance profile. + +Credentials must have a policy applied that allows the `logs:CreateLogStream` +and `logs:PutLogEvents` actions, as shown in the following example. + + { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": "*" + } + ] + } diff --git a/docs/admin/logging/etwlogs.md b/docs/admin/logging/etwlogs.md new file mode 100644 index 00000000..5b98fd54 --- /dev/null +++ b/docs/admin/logging/etwlogs.md @@ -0,0 +1,69 @@ + + + +# ETW logging driver + +The ETW logging driver forwards container logs as ETW events. +ETW stands for Event Tracing in Windows, and is the common framework +for tracing applications in Windows. Each ETW event contains a message +with both the log and its context information. A client can then create +an ETW listener to listen to these events. + +The ETW provider that this logging driver registers with Windows, has the +GUID identifier of: `{a3693192-9ed6-46d2-a981-f8226c8363bd}`. A client creates an +ETW listener and registers to listen to events from the logging driver's provider. +It does not matter the order in which the provider and listener are created. +A client can create their ETW listener and start listening for events from the provider, +before the provider has been registered with the system. + +## Usage + +Here is an example of how to listen to these events using the logman utility program +included in most installations of Windows: + + 1. `logman start -ets DockerContainerLogs -p {a3693192-9ed6-46d2-a981-f8226c8363bd} 0 0 -o trace.etl` + 2. Run your container(s) with the etwlogs driver, by adding `--log-driver=etwlogs` + to the Docker run command, and generate log messages. + 3. `logman stop -ets DockerContainerLogs` + 4. This will generate an etl file that contains the events. One way to convert this file into + human-readable form is to run: `tracerpt -y trace.etl`. + +Each ETW event will contain a structured message string in this format: + + container_name: %s, image_name: %s, container_id: %s, image_id: %s, source: [stdout | stderr], log: %s + +Details on each item in the message can be found below: + +| Field | Description | +-----------------------|-------------------------------------------------| +| `container_name` | The container name at the time it was started. | +| `image_name` | The name of the container's image. | +| `container_id` | The full 64-character container ID. | +| `image_id` | The full ID of the container's image. | +| `source` | `stdout` or `stderr`. | +| `log` | The container log message. | + +Here is an example event message: + + container_name: backstabbing_spence, + image_name: windowsservercore, + container_id: f14bb55aa862d7596b03a33251c1be7dbbec8056bbdead1da8ec5ecebbe29731, + image_id: sha256:2f9e19bd998d3565b4f345ac9aaf6e3fc555406239a4fb1b1ba879673713824b, + source: stdout, + log: Hello world! + +A client can parse this message string to get both the log message, as well as its +context information. Note that the time stamp is also available within the ETW event. + +**Note** This ETW provider emits only a message string, and not a specially +structured ETW event. Therefore, it is not required to register a manifest file +with the system to read and interpret its ETW events. diff --git a/docs/admin/logging/fluentd.md b/docs/admin/logging/fluentd.md new file mode 100644 index 00000000..538af7b1 --- /dev/null +++ b/docs/admin/logging/fluentd.md @@ -0,0 +1,115 @@ + + +# Fluentd logging driver + +The `fluentd` logging driver sends container logs to the +[Fluentd](http://www.fluentd.org/) collector as structured log data. Then, users +can use any of the [various output plugins of +Fluentd](http://www.fluentd.org/plugins) to write these logs to various +destinations. + +In addition to the log message itself, the `fluentd` log +driver sends the following metadata in the structured log message: + +| Field | Description | +-------------------|-------------------------------------| +| `container_id` | The full 64-character container ID. | +| `container_name` | The container name at the time it was started. If you use `docker rename` to rename a container, the new name is not reflected in the journal entries. | +| `source` | `stdout` or `stderr` | + +The `docker logs` command is not available for this logging driver. + +## Usage + +Some options are supported by specifying `--log-opt` as many times as needed: + + - `fluentd-address`: specify `host:port` to connect `localhost:24224` + - `tag`: specify tag for fluentd message, which interpret some markup, ex `{{.ID}}`, `{{.FullID}}` or `{{.Name}}` `docker.{{.ID}}` + + +Configure the default logging driver by passing the +`--log-driver` option to the Docker daemon: + + docker daemon --log-driver=fluentd + +To set the logging driver for a specific container, pass the +`--log-driver` option to `docker run`: + + docker run --log-driver=fluentd ... + +Before using this logging driver, launch a Fluentd daemon. The logging driver +connects to this daemon through `localhost:24224` by default. Use the +`fluentd-address` option to connect to a different address. + + docker run --log-driver=fluentd --log-opt fluentd-address=myhost.local:24224 + +If container cannot connect to the Fluentd daemon, the container stops +immediately unless the `fluentd-async-connect` option is used. + +## Options + +Users can use the `--log-opt NAME=VALUE` flag to specify additional Fluentd logging driver options. + +### fluentd-address + +By default, the logging driver connects to `localhost:24224`. Supply the +`fluentd-address` option to connect to a different address. + + docker run --log-driver=fluentd --log-opt fluentd-address=myhost.local:24224 + +### tag + +By default, Docker uses the first 12 characters of the container ID to tag log messages. +Refer to the [log tag option documentation](log_tags.md) for customizing +the log tag format. + + +### labels and env + +The `labels` and `env` options each take a comma-separated list of keys. If there is collision between `label` and `env` keys, the value of the `env` takes precedence. Both options add additional fields to the extra attributes of a logging message. + +### fluentd-async-connect + +Docker connects to Fluentd in the background. Messages are buffered until the connection is established. + +## Fluentd daemon management with Docker + +About `Fluentd` itself, see [the project webpage](http://www.fluentd.org) +and [its documents](http://docs.fluentd.org/). + +To use this logging driver, start the `fluentd` daemon on a host. We recommend +that you use [the Fluentd docker +image](https://hub.docker.com/r/fluent/fluentd/). This image is +especially useful if you want to aggregate multiple container logs on a each +host then, later, transfer the logs to another Fluentd node to create an +aggregate store. + +### Testing container loggers + +1. Write a configuration file (`test.conf`) to dump input logs: + + + @type forward + + + + @type stdout + + +2. Launch Fluentd container with this configuration file: + + $ docker run -it -p 24224:24224 -v /path/to/conf/test.conf:/fluentd/etc -e FLUENTD_CONF=test.conf fluent/fluentd:latest + +3. Start one or more containers with the `fluentd` logging driver: + + $ docker run --log-driver=fluentd your/application diff --git a/docs/admin/logging/gcplogs.md b/docs/admin/logging/gcplogs.md new file mode 100644 index 00000000..08fd858d --- /dev/null +++ b/docs/admin/logging/gcplogs.md @@ -0,0 +1,70 @@ + + +# Google Cloud Logging driver + +The Google Cloud Logging driver sends container logs to
Google Cloud +Logging. + +## Usage + +You can configure the default logging driver by passing the `--log-driver` +option to the Docker daemon: + + docker daemon --log-driver=gcplogs + +You can set the logging driver for a specific container by using the +`--log-driver` option to `docker run`: + + docker run --log-driver=gcplogs ... + +This log driver does not implement a reader so it is incompatible with +`docker logs`. + +If Docker detects that it is running in a Google Cloud Project, it will discover configuration +from the instance metadata service. +Otherwise, the user must specify which project to log to using the `--gcp-project` +log option and Docker will attempt to obtain credentials from the +Google Application Default Credential. +The `--gcp-project` takes precedence over information discovered from the metadata server +so a Docker daemon running in a Google Cloud Project can be overriden to log to a different +Google Cloud Project using `--gcp-project`. + +## gcplogs options + +You can use the `--log-opt NAME=VALUE` flag to specify these additional Google +Cloud Logging driver options: + +| Option | Required | Description | +|-----------------------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------| +| `gcp-project` | optional | Which GCP project to log to. Defaults to discovering this value from the GCE metadata service. | +| `gcp-log-cmd` | optional | Whether to log the command that the container was started with. Defaults to false. | +| `labels` | optional | Comma-separated list of keys of labels, which should be included in message, if these labels are specified for container. | +| `env` | optional | Comma-separated list of keys of environment variables, which should be included in message, if these variables are specified for container. | + +If there is collision between `label` and `env` keys, the value of the `env` +takes precedence. Both options add additional fields to the attributes of a +logging message. + +Below is an example of the logging options required to log to the default +logging destination which is discovered by querying the GCE metadata server. + + docker run --log-driver=gcplogs \ + --log-opt labels=location + --log-opt env=TEST + --log-opt gcp-log-cmd=true + --env "TEST=false" + --label location=west + your/application + +This configuration also directs the driver to include in the payload the label +`location`, the environment variable `ENV`, and the command used to start the +container. diff --git a/docs/admin/logging/index.md b/docs/admin/logging/index.md new file mode 100644 index 00000000..4300565e --- /dev/null +++ b/docs/admin/logging/index.md @@ -0,0 +1,23 @@ + + + +# Logging Drivers + +* [Configuring logging drivers](overview.md) +* [Configuring log tags](log_tags.md) +* [Fluentd logging driver](fluentd.md) +* [Journald logging driver](journald.md) +* [Amazon CloudWatch Logs logging driver](awslogs.md) +* [Splunk logging driver](splunk.md) +* [ETW logging driver](etwlogs.md) diff --git a/docs/admin/logging/journald.md b/docs/admin/logging/journald.md new file mode 100644 index 00000000..90c97203 --- /dev/null +++ b/docs/admin/logging/journald.md @@ -0,0 +1,92 @@ + + +# Journald logging driver + +The `journald` logging driver sends container logs to the [systemd +journal](http://www.freedesktop.org/software/systemd/man/systemd-journald.service.html). Log entries can be retrieved using the `journalctl` +command, through use of the journal API, or using the `docker logs` command. + +In addition to the text of the log message itself, the `journald` log +driver stores the following metadata in the journal with each message: + +| Field | Description | +----------------------|-------------| +| `CONTAINER_ID` | The container ID truncated to 12 characters. | +| `CONTAINER_ID_FULL` | The full 64-character container ID. | +| `CONTAINER_NAME` | The container name at the time it was started. If you use `docker rename` to rename a container, the new name is not reflected in the journal entries. | +| `CONTAINER_TAG` | The container tag ([log tag option documentation](log_tags.md)). | + +## Usage + +You can configure the default logging driver by passing the +`--log-driver` option to the Docker daemon: + + docker daemon --log-driver=journald + +You can set the logging driver for a specific container by using the +`--log-driver` option to `docker run`: + + docker run --log-driver=journald ... + +## Options + +Users can use the `--log-opt NAME=VALUE` flag to specify additional +journald logging driver options. + +### tag + +Specify template to set `CONTAINER_TAG` value in journald logs. Refer to +[log tag option documentation](log_tags.md) for customizing the log tag format. + +### labels and env + +The `labels` and `env` options each take a comma-separated list of keys. If there is collision between `label` and `env` keys, the value of the `env` takes precedence. Both options add additional metadata in the journal with each message. + +## Note regarding container names + +The value logged in the `CONTAINER_NAME` field is the container name +that was set at startup. If you use `docker rename` to rename a +container, the new name will not be reflected in the journal entries. +Journal entries will continue to use the original name. + +## Retrieving log messages with journalctl + +You can use the `journalctl` command to retrieve log messages. You +can apply filter expressions to limit the retrieved messages to a +specific container. For example, to retrieve all log messages from a +container referenced by name: + + # journalctl CONTAINER_NAME=webserver + +You can make use of additional filters to further limit the messages +retrieved. For example, to see just those messages generated since +the system last booted: + + # journalctl -b CONTAINER_NAME=webserver + +Or to retrieve log messages in JSON format with complete metadata: + + # journalctl -o json CONTAINER_NAME=webserver + +## Retrieving log messages with the journal API + +This example uses the `systemd` Python module to retrieve container +logs: + + import systemd.journal + + reader = systemd.journal.Reader() + reader.add_match('CONTAINER_NAME=web') + + for msg in reader: + print '{CONTAINER_ID_FULL}: {MESSAGE}'.format(**msg) diff --git a/docs/admin/logging/log_tags.md b/docs/admin/logging/log_tags.md new file mode 100644 index 00000000..9b2e098d --- /dev/null +++ b/docs/admin/logging/log_tags.md @@ -0,0 +1,51 @@ + + +# Log Tags + +The `tag` log option specifies how to format a tag that identifies the +container's log messages. By default, the system uses the first 12 characters of +the container id. To override this behavior, specify a `tag` option: + +``` +docker run --log-driver=fluentd --log-opt fluentd-address=myhost.local:24224 --log-opt tag="mailer" +``` + +Docker supports some special template markup you can use when specifying a tag's value: + +| Markup | Description | +|--------------------|------------------------------------------------------| +| `{{.ID}}` | The first 12 characters of the container id. | +| `{{.FullID}}` | The full container id. | +| `{{.Name}}` | The container name. | +| `{{.ImageID}}` | The first 12 characters of the container's image id. | +| `{{.ImageFullID}}` | The container's full image identifier. | +| `{{.ImageName}}` | The name of the image used by the container. | + +For example, specifying a `--log-opt tag="{{.ImageName}}/{{.Name}}/{{.ID}}"` value yields `syslog` log lines like: + +``` +Aug 7 18:33:19 HOSTNAME docker/hello-world/foobar/5790672ab6a0[9103]: Hello from Docker. +``` + +At startup time, the system sets the `container_name` field and `{{.Name}}` in +the tags. If you use `docker rename` to rename a container, the new name is not +reflected in the log messages. Instead, these messages continue to use the +original container name. + +For advanced usage, the generated tag's use [go +templates](http://golang.org/pkg/text/template/) and the container's [logging +context](https://github.com/docker/docker/blob/master/daemon/logger/context.go). + +>**Note**:The driver specific log options `syslog-tag`, `fluentd-tag` and +>`gelf-tag` still work for backwards compatibility. However, going forward you +>should standardize on using the generic `tag` log option instead. diff --git a/docs/admin/logging/overview.md b/docs/admin/logging/overview.md new file mode 100644 index 00000000..8a7cd12f --- /dev/null +++ b/docs/admin/logging/overview.md @@ -0,0 +1,246 @@ + + + +# Configure logging drivers + +The container can have a different logging driver than the Docker daemon. Use +the `--log-driver=VALUE` with the `docker run` command to configure the +container's logging driver. The following options are supported: + +| `none` | Disables any logging for the container. `docker logs` won't be available with this driver. | +|-------------|-------------------------------------------------------------------------------------------------------------------------------| +| `json-file` | Default logging driver for Docker. Writes JSON messages to file. | +| `syslog` | Syslog logging driver for Docker. Writes log messages to syslog. | +| `journald` | Journald logging driver for Docker. Writes log messages to `journald`. | +| `gelf` | Graylog Extended Log Format (GELF) logging driver for Docker. Writes log messages to a GELF endpoint likeGraylog or Logstash. | +| `fluentd` | Fluentd logging driver for Docker. Writes log messages to `fluentd` (forward input). | +| `awslogs` | Amazon CloudWatch Logs logging driver for Docker. Writes log messages to Amazon CloudWatch Logs. | +| `splunk` | Splunk logging driver for Docker. Writes log messages to `splunk` using HTTP Event Collector. | +| `etwlogs` | ETW logging driver for Docker on Windows. Writes log messages as ETW events. | +| `gcplogs` | Google Cloud Logging driver for Docker. Writes log messages to Google Cloud Logging. | + +The `docker logs`command is available only for the `json-file` and `journald` +logging drivers. + +The `labels` and `env` options add additional attributes for use with logging drivers that accept them. Each option takes a comma-separated list of keys. If there is collision between `label` and `env` keys, the value of the `env` takes precedence. + +To use attributes, specify them when you start the Docker daemon. + +``` +docker daemon --log-driver=json-file --log-opt labels=foo --log-opt env=foo,fizz +``` + +Then, run a container and specify values for the `labels` or `env`. For example, you might use this: + +``` +docker run --label foo=bar -e fizz=buzz -d -P training/webapp python app.py +``` + +This adds additional fields to the log depending on the driver, e.g. for +`json-file` that looks like: + + "attrs":{"fizz":"buzz","foo":"bar"} + + +## json-file options + +The following logging options are supported for the `json-file` logging driver: + + --log-opt max-size=[0-9+][k|m|g] + --log-opt max-file=[0-9+] + --log-opt labels=label1,label2 + --log-opt env=env1,env2 + +Logs that reach `max-size` are rolled over. You can set the size in kilobytes(k), megabytes(m), or gigabytes(g). eg `--log-opt max-size=50m`. If `max-size` is not set, then logs are not rolled over. + +`max-file` specifies the maximum number of files that a log is rolled over before being discarded. eg `--log-opt max-file=100`. If `max-size` is not set, then `max-file` is not honored. + +If `max-size` and `max-file` are set, `docker logs` only returns the log lines from the newest log file. + + +## syslog options + +The following logging options are supported for the `syslog` logging driver: + + --log-opt syslog-address=[tcp|udp|tcp+tls]://host:port + --log-opt syslog-address=unix://path + --log-opt syslog-facility=daemon + --log-opt syslog-tls-ca-cert=/etc/ca-certificates/custom/ca.pem + --log-opt syslog-tls-cert=/etc/ca-certificates/custom/cert.pem + --log-opt syslog-tls-key=/etc/ca-certificates/custom/key.pem + --log-opt syslog-tls-skip-verify=true + --log-opt tag="mailer" + --log-opt syslog-format=[rfc5424|rfc3164] + +`syslog-address` specifies the remote syslog server address where the driver connects to. +If not specified it defaults to the local unix socket of the running system. +If transport is either `tcp` or `udp` and `port` is not specified it defaults to `514` +The following example shows how to have the `syslog` driver connect to a `syslog` +remote server at `192.168.0.42` on port `123` + + $ docker run --log-driver=syslog --log-opt syslog-address=tcp://192.168.0.42:123 + +The `syslog-facility` option configures the syslog facility. By default, the system uses the +`daemon` value. To override this behavior, you can provide an integer of 0 to 23 or any of +the following named facilities: + +* `kern` +* `user` +* `mail` +* `daemon` +* `auth` +* `syslog` +* `lpr` +* `news` +* `uucp` +* `cron` +* `authpriv` +* `ftp` +* `local0` +* `local1` +* `local2` +* `local3` +* `local4` +* `local5` +* `local6` +* `local7` + +`syslog-tls-ca-cert` specifies the absolute path to the trust certificates +signed by the CA. This option is ignored if the address protocol is not `tcp+tls`. + +`syslog-tls-cert` specifies the absolute path to the TLS certificate file. +This option is ignored if the address protocol is not `tcp+tls`. + +`syslog-tls-key` specifies the absolute path to the TLS key file. +This option is ignored if the address protocol is not `tcp+tls`. + +`syslog-tls-skip-verify` configures the TLS verification. +This verification is enabled by default, but it can be overriden by setting +this option to `true`. This option is ignored if the address protocol is not `tcp+tls`. + +By default, Docker uses the first 12 characters of the container ID to tag log messages. +Refer to the [log tag option documentation](log_tags.md) for customizing +the log tag format. + +`syslog-format` specifies syslog message format to use when logging. +If not specified it defaults to the local unix syslog format without hostname specification. +Specify rfc3164 to perform logging in RFC-3164 compatible format. Specify rfc5424 to perform +logging in RFC-5424 compatible format + + +## journald options + +The `journald` logging driver stores the container id in the journal's `CONTAINER_ID` field. For detailed information on +working with this logging driver, see [the journald logging driver](journald.md) +reference documentation. + +## gelf options + +The GELF logging driver supports the following options: + + --log-opt gelf-address=udp://host:port + --log-opt tag="database" + --log-opt labels=label1,label2 + --log-opt env=env1,env2 + --log-opt gelf-compression-type=gzip + --log-opt gelf-compression-level=1 + +The `gelf-address` option specifies the remote GELF server address that the +driver connects to. Currently, only `udp` is supported as the transport and you must +specify a `port` value. The following example shows how to connect the `gelf` +driver to a GELF remote server at `192.168.0.42` on port `12201` + + $ docker run --log-driver=gelf --log-opt gelf-address=udp://192.168.0.42:12201 + +By default, Docker uses the first 12 characters of the container ID to tag log messages. +Refer to the [log tag option documentation](log_tags.md) for customizing +the log tag format. + +The `labels` and `env` options are supported by the gelf logging +driver. It adds additional key on the `extra` fields, prefixed by an +underscore (`_`). + + // […] + "_foo": "bar", + "_fizz": "buzz", + // […] + +The `gelf-compression-type` option can be used to change how the GELF driver +compresses each log message. The accepted values are `gzip`, `zlib` and `none`. +`gzip` is chosen by default. + +The `gelf-compression-level` option can be used to change the level of compresssion +when `gzip` or `zlib` is selected as `gelf-compression-type`. Accepted value +must be from from -1 to 9 (BestCompression). Higher levels typically +run slower but compress more. Default value is 1 (BestSpeed). + +## fluentd options + +You can use the `--log-opt NAME=VALUE` flag to specify these additional Fluentd logging driver options. + + - `fluentd-address`: specify `host:port` to connect [localhost:24224] + - `tag`: specify tag for `fluentd` message + - `fluentd-buffer-limit`: specify the maximum size of the fluentd log buffer [8MB] + - `fluentd-retry-wait`: initial delay before a connection retry (after which it increases exponentially) [1000ms] + - `fluentd-max-retries`: maximum number of connection retries before abrupt failure of docker [1073741824] + - `fluentd-async-connect`: whether to block on initial connection or not [false] + +For example, to specify both additional options: + +`docker run --log-driver=fluentd --log-opt fluentd-address=localhost:24224 --log-opt tag=docker.{{.Name}}` + +If container cannot connect to the Fluentd daemon on the specified address and +`fluentd-async-connect` is not enabled, the container stops immediately. +For detailed information on working with this logging driver, +see [the fluentd logging driver](fluentd.md) + + +## Specify Amazon CloudWatch Logs options + +The Amazon CloudWatch Logs logging driver supports the following options: + + --log-opt awslogs-region= + --log-opt awslogs-group= + --log-opt awslogs-stream= + + +For detailed information on working with this logging driver, see [the awslogs logging driver](awslogs.md) reference documentation. + +## Splunk options + +The Splunk logging driver requires the following options: + + --log-opt splunk-token= + --log-opt splunk-url=https://your_splunk_instance:8088 + +For detailed information about working with this logging driver, see the [Splunk logging driver](splunk.md) +reference documentation. + +## ETW logging driver options + +The etwlogs logging driver does not require any options to be specified. This logging driver will forward each log message +as an ETW event. An ETW listener can then be created to listen for these events. + +For detailed information on working with this logging driver, see [the ETW logging driver](etwlogs.md) reference documentation. + +## Google Cloud Logging + +The Google Cloud Logging driver supports the following options: + + --log-opt gcp-project= + --log-opt labels=, + --log-opt env=, + --log-opt log-cmd=true + +For detailed information about working with this logging driver, see the [Google Cloud Logging driver](gcplogs.md). +reference documentation. diff --git a/docs/admin/logging/splunk.md b/docs/admin/logging/splunk.md new file mode 100644 index 00000000..9c60a529 --- /dev/null +++ b/docs/admin/logging/splunk.md @@ -0,0 +1,69 @@ + + +# Splunk logging driver + +The `splunk` logging driver sends container logs to +[HTTP Event Collector](http://dev.splunk.com/view/event-collector/SP-CAAAE6M) +in Splunk Enterprise and Splunk Cloud. + +## Usage + +You can configure the default logging driver by passing the `--log-driver` +option to the Docker daemon: + + docker daemon --log-driver=splunk + +You can set the logging driver for a specific container by using the +`--log-driver` option to `docker run`: + + docker run --log-driver=splunk ... + +## Splunk options + +You can use the `--log-opt NAME=VALUE` flag to specify these additional Splunk +logging driver options: + +| Option | Required | Description | +|-----------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `splunk-token` | required | Splunk HTTP Event Collector token. | +| `splunk-url` | required | Path to your Splunk Enterprise or Splunk Cloud instance (including port and schema used by HTTP Event Collector) `https://your_splunk_instance:8088`. | +| `splunk-source` | optional | Event source. | +| `splunk-sourcetype` | optional | Event source type. | +| `splunk-index` | optional | Event index. | +| `splunk-capath` | optional | Path to root certificate. | +| `splunk-caname` | optional | Name to use for validating server certificate; by default the hostname of the `splunk-url` will be used. | +| `splunk-insecureskipverify` | optional | Ignore server certificate validation. | +| `tag` | optional | Specify tag for message, which interpret some markup. Default value is `{{.ID}}` (12 characters of the container ID). Refer to the [log tag option documentation](log_tags.md) for customizing the log tag format. | +| `labels` | optional | Comma-separated list of keys of labels, which should be included in message, if these labels are specified for container. | +| `env` | optional | Comma-separated list of keys of environment variables, which should be included in message, if these variables are specified for container. | + +If there is collision between `label` and `env` keys, the value of the `env` takes precedence. +Both options add additional fields to the attributes of a logging message. + +Below is an example of the logging option specified for the Splunk Enterprise +instance. The instance is installed locally on the same machine on which the +Docker daemon is running. The path to the root certificate and Common Name is +specified using an HTTPS schema. This is used for verification. +The `SplunkServerDefaultCert` is automatically generated by Splunk certificates. + + docker run --log-driver=splunk \ + --log-opt splunk-token=176FCEBF-4CF5-4EDF-91BC-703796522D20 \ + --log-opt splunk-url=https://splunkhost:8088 \ + --log-opt splunk-capath=/path/to/cert/cacert.pem \ + --log-opt splunk-caname=SplunkServerDefaultCert + --log-opt tag="{{.Name}}/{{.FullID}}" + --log-opt labels=location + --log-opt env=TEST + --env "TEST=false" + --label location=west + your/application diff --git a/docs/admin/puppet.md b/docs/admin/puppet.md new file mode 100644 index 00000000..d03200f8 --- /dev/null +++ b/docs/admin/puppet.md @@ -0,0 +1,100 @@ + + +# Using Puppet + +> *Note:* Please note this is a community contributed installation path. The +> only `official` installation is using the +> [*Ubuntu*](../installation/linux/ubuntulinux.md) installation +> path. This version may sometimes be out of date. + +## Requirements + +To use this guide you'll need a working installation of Puppet from +[Puppet Labs](https://puppetlabs.com) . + +The module also currently uses the official PPA so only works with +Ubuntu. + +## Installation + +The module is available on the [Puppet +Forge](https://forge.puppetlabs.com/garethr/docker/) and can be +installed using the built-in module tool. + + $ puppet module install garethr/docker + +It can also be found on +[GitHub](https://github.com/garethr/garethr-docker) if you would rather +download the source. + +## Usage + +The module provides a puppet class for installing Docker and two defined +types for managing images and containers. + +### Installation + + include 'docker' + +### Images + +The next step is probably to install a Docker image. For this, we have a +defined type which can be used like so: + + docker::image { 'ubuntu': } + +This is equivalent to running: + + $ docker pull ubuntu + +Note that it will only be downloaded if an image of that name does not +already exist. This is downloading a large binary so on first run can +take a while. For that reason this define turns off the default 5 minute +timeout for the exec type. Note that you can also remove images you no +longer need with: + + docker::image { 'ubuntu': + ensure => 'absent', + } + +### Containers + +Now you have an image where you can run commands within a container +managed by Docker. + + docker::run { 'helloworld': + image => 'ubuntu', + command => '/bin/sh -c "while true; do echo hello world; sleep 1; done"', + } + +This is equivalent to running the following command, but under upstart: + + $ docker run -d ubuntu /bin/sh -c "while true; do echo hello world; sleep 1; done" + +Run also contains a number of optional parameters: + + docker::run { 'helloworld': + image => 'ubuntu', + command => '/bin/sh -c "while true; do echo hello world; sleep 1; done"', + ports => ['4444', '4555'], + volumes => ['/var/lib/couchdb', '/var/log'], + volumes_from => '6446ea52fbc9', + memory_limit => 10485760, # bytes + username => 'example', + hostname => 'example.com', + env => ['FOO=BAR', 'FOO2=BAR2'], + dns => ['8.8.8.8', '8.8.4.4'], + } + +> *Note:* +> The `ports`, `env`, `dns` and `volumes` attributes can be set with either a single +> string or as above with an array of values. diff --git a/docs/admin/registry_mirror.md b/docs/admin/registry_mirror.md new file mode 100644 index 00000000..2d67f9c5 --- /dev/null +++ b/docs/admin/registry_mirror.md @@ -0,0 +1,19 @@ + + +# Run a local registry mirror + +The original content was deprecated. [An archived +version](https://docs.docker.com/v1.6/articles/registry_mirror) is available in +the 1.7 documentation. For information about configuring mirrors with the latest +Docker Registry version, please file a support request with [the Distribution +project](https://github.com/docker/distribution/issues). diff --git a/docs/admin/runmetrics.md b/docs/admin/runmetrics.md new file mode 100644 index 00000000..b1d88100 --- /dev/null +++ b/docs/admin/runmetrics.md @@ -0,0 +1,464 @@ + + +# Runtime metrics + + +## Docker stats + +You can use the `docker stats` command to live stream a container's +runtime metrics. The command supports CPU, memory usage, memory limit, +and network IO metrics. + +The following is a sample output from the `docker stats` command + + $ docker stats redis1 redis2 + CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O + redis1 0.07% 796 KB / 64 MB 1.21% 788 B / 648 B 3.568 MB / 512 KB + redis2 0.07% 2.746 MB / 64 MB 4.29% 1.266 KB / 648 B 12.4 MB / 0 B + + +The [docker stats](../reference/commandline/stats.md) reference page has +more details about the `docker stats` command. + +## Control groups + +Linux Containers rely on [control groups]( +https://www.kernel.org/doc/Documentation/cgroups/cgroups.txt) +which not only track groups of processes, but also expose metrics about +CPU, memory, and block I/O usage. You can access those metrics and +obtain network usage metrics as well. This is relevant for "pure" LXC +containers, as well as for Docker containers. + +Control groups are exposed through a pseudo-filesystem. In recent +distros, you should find this filesystem under `/sys/fs/cgroup`. Under +that directory, you will see multiple sub-directories, called devices, +freezer, blkio, etc.; each sub-directory actually corresponds to a different +cgroup hierarchy. + +On older systems, the control groups might be mounted on `/cgroup`, without +distinct hierarchies. In that case, instead of seeing the sub-directories, +you will see a bunch of files in that directory, and possibly some directories +corresponding to existing containers. + +To figure out where your control groups are mounted, you can run: + + $ grep cgroup /proc/mounts + +## Enumerating cgroups + +You can look into `/proc/cgroups` to see the different control group subsystems +known to the system, the hierarchy they belong to, and how many groups they contain. + +You can also look at `/proc//cgroup` to see which control groups a process +belongs to. The control group will be shown as a path relative to the root of +the hierarchy mountpoint; e.g., `/` means “this process has not been assigned into +a particular group”, while `/lxc/pumpkin` means that the process is likely to be +a member of a container named `pumpkin`. + +## Finding the cgroup for a given container + +For each container, one cgroup will be created in each hierarchy. On +older systems with older versions of the LXC userland tools, the name of +the cgroup will be the name of the container. With more recent versions +of the LXC tools, the cgroup will be `lxc/.` + +For Docker containers using cgroups, the container name will be the full +ID or long ID of the container. If a container shows up as ae836c95b4c3 +in `docker ps`, its long ID might be something like +`ae836c95b4c3c9e9179e0e91015512da89fdec91612f63cebae57df9a5444c79`. You can +look it up with `docker inspect` or `docker ps --no-trunc`. + +Putting everything together to look at the memory metrics for a Docker +container, take a look at `/sys/fs/cgroup/memory/docker//`. + +## Metrics from cgroups: memory, CPU, block I/O + +For each subsystem (memory, CPU, and block I/O), you will find one or +more pseudo-files containing statistics. + +### Memory metrics: `memory.stat` + +Memory metrics are found in the "memory" cgroup. Note that the memory +control group adds a little overhead, because it does very fine-grained +accounting of the memory usage on your host. Therefore, many distros +chose to not enable it by default. Generally, to enable it, all you have +to do is to add some kernel command-line parameters: +`cgroup_enable=memory swapaccount=1`. + +The metrics are in the pseudo-file `memory.stat`. +Here is what it will look like: + + cache 11492564992 + rss 1930993664 + mapped_file 306728960 + pgpgin 406632648 + pgpgout 403355412 + swap 0 + pgfault 728281223 + pgmajfault 1724 + inactive_anon 46608384 + active_anon 1884520448 + inactive_file 7003344896 + active_file 4489052160 + unevictable 32768 + hierarchical_memory_limit 9223372036854775807 + hierarchical_memsw_limit 9223372036854775807 + total_cache 11492564992 + total_rss 1930993664 + total_mapped_file 306728960 + total_pgpgin 406632648 + total_pgpgout 403355412 + total_swap 0 + total_pgfault 728281223 + total_pgmajfault 1724 + total_inactive_anon 46608384 + total_active_anon 1884520448 + total_inactive_file 7003344896 + total_active_file 4489052160 + total_unevictable 32768 + +The first half (without the `total_` prefix) contains statistics relevant +to the processes within the cgroup, excluding sub-cgroups. The second half +(with the `total_` prefix) includes sub-cgroups as well. + +Some metrics are "gauges", i.e., values that can increase or decrease +(e.g., swap, the amount of swap space used by the members of the cgroup). +Some others are "counters", i.e., values that can only go up, because +they represent occurrences of a specific event (e.g., pgfault, which +indicates the number of page faults which happened since the creation of +the cgroup; this number can never decrease). + + + - **cache:** + the amount of memory used by the processes of this control group + that can be associated precisely with a block on a block device. + When you read from and write to files on disk, this amount will + increase. This will be the case if you use "conventional" I/O + (`open`, `read`, + `write` syscalls) as well as mapped files (with + `mmap`). It also accounts for the memory used by + `tmpfs` mounts, though the reasons are unclear. + + - **rss:** + the amount of memory that *doesn't* correspond to anything on disk: + stacks, heaps, and anonymous memory maps. + + - **mapped_file:** + indicates the amount of memory mapped by the processes in the + control group. It doesn't give you information about *how much* + memory is used; it rather tells you *how* it is used. + + - **pgfault and pgmajfault:** + indicate the number of times that a process of the cgroup triggered + a "page fault" and a "major fault", respectively. A page fault + happens when a process accesses a part of its virtual memory space + which is nonexistent or protected. The former can happen if the + process is buggy and tries to access an invalid address (it will + then be sent a `SIGSEGV` signal, typically + killing it with the famous `Segmentation fault` + message). The latter can happen when the process reads from a memory + zone which has been swapped out, or which corresponds to a mapped + file: in that case, the kernel will load the page from disk, and let + the CPU complete the memory access. It can also happen when the + process writes to a copy-on-write memory zone: likewise, the kernel + will preempt the process, duplicate the memory page, and resume the + write operation on the process` own copy of the page. "Major" faults + happen when the kernel actually has to read the data from disk. When + it just has to duplicate an existing page, or allocate an empty + page, it's a regular (or "minor") fault. + + - **swap:** + the amount of swap currently used by the processes in this cgroup. + + - **active_anon and inactive_anon:** + the amount of *anonymous* memory that has been identified has + respectively *active* and *inactive* by the kernel. "Anonymous" + memory is the memory that is *not* linked to disk pages. In other + words, that's the equivalent of the rss counter described above. In + fact, the very definition of the rss counter is **active_anon** + + **inactive_anon** - **tmpfs** (where tmpfs is the amount of memory + used up by `tmpfs` filesystems mounted by this + control group). Now, what's the difference between "active" and + "inactive"? Pages are initially "active"; and at regular intervals, + the kernel sweeps over the memory, and tags some pages as + "inactive". Whenever they are accessed again, they are immediately + retagged "active". When the kernel is almost out of memory, and time + comes to swap out to disk, the kernel will swap "inactive" pages. + + - **active_file and inactive_file:** + cache memory, with *active* and *inactive* similar to the *anon* + memory above. The exact formula is cache = **active_file** + + **inactive_file** + **tmpfs**. The exact rules used by the kernel + to move memory pages between active and inactive sets are different + from the ones used for anonymous memory, but the general principle + is the same. Note that when the kernel needs to reclaim memory, it + is cheaper to reclaim a clean (=non modified) page from this pool, + since it can be reclaimed immediately (while anonymous pages and + dirty/modified pages have to be written to disk first). + + - **unevictable:** + the amount of memory that cannot be reclaimed; generally, it will + account for memory that has been "locked" with `mlock`. + It is often used by crypto frameworks to make sure that + secret keys and other sensitive material never gets swapped out to + disk. + + - **memory and memsw limits:** + These are not really metrics, but a reminder of the limits applied + to this cgroup. The first one indicates the maximum amount of + physical memory that can be used by the processes of this control + group; the second one indicates the maximum amount of RAM+swap. + +Accounting for memory in the page cache is very complex. If two +processes in different control groups both read the same file +(ultimately relying on the same blocks on disk), the corresponding +memory charge will be split between the control groups. It's nice, but +it also means that when a cgroup is terminated, it could increase the +memory usage of another cgroup, because they are not splitting the cost +anymore for those memory pages. + +### CPU metrics: `cpuacct.stat` + +Now that we've covered memory metrics, everything else will look very +simple in comparison. CPU metrics will be found in the +`cpuacct` controller. + +For each container, you will find a pseudo-file `cpuacct.stat`, +containing the CPU usage accumulated by the processes of the container, +broken down between `user` and `system` time. If you're not familiar +with the distinction, `user` is the time during which the processes were +in direct control of the CPU (i.e., executing process code), and `system` +is the time during which the CPU was executing system calls on behalf of +those processes. + +Those times are expressed in ticks of 1/100th of a second. Actually, +they are expressed in "user jiffies". There are `USER_HZ` +*"jiffies"* per second, and on x86 systems, +`USER_HZ` is 100. This used to map exactly to the +number of scheduler "ticks" per second; but with the advent of higher +frequency scheduling, as well as [tickless kernels]( +http://lwn.net/Articles/549580/), the number of kernel ticks +wasn't relevant anymore. It stuck around anyway, mainly for legacy and +compatibility reasons. + +### Block I/O metrics + +Block I/O is accounted in the `blkio` controller. +Different metrics are scattered across different files. While you can +find in-depth details in the [blkio-controller]( +https://www.kernel.org/doc/Documentation/cgroups/blkio-controller.txt) +file in the kernel documentation, here is a short list of the most +relevant ones: + + + - **blkio.sectors:** + contain the number of 512-bytes sectors read and written by the + processes member of the cgroup, device by device. Reads and writes + are merged in a single counter. + + - **blkio.io_service_bytes:** + indicates the number of bytes read and written by the cgroup. It has + 4 counters per device, because for each device, it differentiates + between synchronous vs. asynchronous I/O, and reads vs. writes. + + - **blkio.io_serviced:** + the number of I/O operations performed, regardless of their size. It + also has 4 counters per device. + + - **blkio.io_queued:** + indicates the number of I/O operations currently queued for this + cgroup. In other words, if the cgroup isn't doing any I/O, this will + be zero. Note that the opposite is not true. In other words, if + there is no I/O queued, it does not mean that the cgroup is idle + (I/O-wise). It could be doing purely synchronous reads on an + otherwise quiescent device, which is therefore able to handle them + immediately, without queuing. Also, while it is helpful to figure + out which cgroup is putting stress on the I/O subsystem, keep in + mind that it is a relative quantity. Even if a process group does + not perform more I/O, its queue size can increase just because the + device load increases because of other devices. + +## Network metrics + +Network metrics are not exposed directly by control groups. There is a +good explanation for that: network interfaces exist within the context +of *network namespaces*. The kernel could probably accumulate metrics +about packets and bytes sent and received by a group of processes, but +those metrics wouldn't be very useful. You want per-interface metrics +(because traffic happening on the local `lo` +interface doesn't really count). But since processes in a single cgroup +can belong to multiple network namespaces, those metrics would be harder +to interpret: multiple network namespaces means multiple `lo` +interfaces, potentially multiple `eth0` +interfaces, etc.; so this is why there is no easy way to gather network +metrics with control groups. + +Instead we can gather network metrics from other sources: + +### IPtables + +IPtables (or rather, the netfilter framework for which iptables is just +an interface) can do some serious accounting. + +For instance, you can setup a rule to account for the outbound HTTP +traffic on a web server: + + $ iptables -I OUTPUT -p tcp --sport 80 + +There is no `-j` or `-g` flag, +so the rule will just count matched packets and go to the following +rule. + +Later, you can check the values of the counters, with: + + $ iptables -nxvL OUTPUT + +Technically, `-n` is not required, but it will +prevent iptables from doing DNS reverse lookups, which are probably +useless in this scenario. + +Counters include packets and bytes. If you want to setup metrics for +container traffic like this, you could execute a `for` +loop to add two `iptables` rules per +container IP address (one in each direction), in the `FORWARD` +chain. This will only meter traffic going through the NAT +layer; you will also have to add traffic going through the userland +proxy. + +Then, you will need to check those counters on a regular basis. If you +happen to use `collectd`, there is a [nice plugin](https://collectd.org/wiki/index.php/Table_of_Plugins) +to automate iptables counters collection. + +### Interface-level counters + +Since each container has a virtual Ethernet interface, you might want to +check directly the TX and RX counters of this interface. You will notice +that each container is associated to a virtual Ethernet interface in +your host, with a name like `vethKk8Zqi`. Figuring +out which interface corresponds to which container is, unfortunately, +difficult. + +But for now, the best way is to check the metrics *from within the +containers*. To accomplish this, you can run an executable from the host +environment within the network namespace of a container using **ip-netns +magic**. + +The `ip-netns exec` command will let you execute any +program (present in the host system) within any network namespace +visible to the current process. This means that your host will be able +to enter the network namespace of your containers, but your containers +won't be able to access the host, nor their sibling containers. +Containers will be able to “see” and affect their sub-containers, +though. + +The exact format of the command is: + + $ ip netns exec + +For example: + + $ ip netns exec mycontainer netstat -i + +`ip netns` finds the "mycontainer" container by +using namespaces pseudo-files. Each process belongs to one network +namespace, one PID namespace, one `mnt` namespace, +etc., and those namespaces are materialized under +`/proc//ns/`. For example, the network +namespace of PID 42 is materialized by the pseudo-file +`/proc/42/ns/net`. + +When you run `ip netns exec mycontainer ...`, it +expects `/var/run/netns/mycontainer` to be one of +those pseudo-files. (Symlinks are accepted.) + +In other words, to execute a command within the network namespace of a +container, we need to: + +- Find out the PID of any process within the container that we want to investigate; +- Create a symlink from `/var/run/netns/` to `/proc//ns/net` +- Execute `ip netns exec ....` + +Please review [*Enumerating Cgroups*](#enumerating-cgroups) to learn how to find +the cgroup of a process running in the container of which you want to +measure network usage. From there, you can examine the pseudo-file named +`tasks`, which contains the PIDs that are in the +control group (i.e., in the container). Pick any one of them. + +Putting everything together, if the "short ID" of a container is held in +the environment variable `$CID`, then you can do this: + + $ TASKS=/sys/fs/cgroup/devices/docker/$CID*/tasks + $ PID=$(head -n 1 $TASKS) + $ mkdir -p /var/run/netns + $ ln -sf /proc/$PID/ns/net /var/run/netns/$CID + $ ip netns exec $CID netstat -i + +## Tips for high-performance metric collection + +Note that running a new process each time you want to update metrics is +(relatively) expensive. If you want to collect metrics at high +resolutions, and/or over a large number of containers (think 1000 +containers on a single host), you do not want to fork a new process each +time. + +Here is how to collect metrics from a single process. You will have to +write your metric collector in C (or any language that lets you do +low-level system calls). You need to use a special system call, +`setns()`, which lets the current process enter any +arbitrary namespace. It requires, however, an open file descriptor to +the namespace pseudo-file (remember: that's the pseudo-file in +`/proc//ns/net`). + +However, there is a catch: you must not keep this file descriptor open. +If you do, when the last process of the control group exits, the +namespace will not be destroyed, and its network resources (like the +virtual interface of the container) will stay around for ever (or until +you close that file descriptor). + +The right approach would be to keep track of the first PID of each +container, and re-open the namespace pseudo-file each time. + +## Collecting metrics when a container exits + +Sometimes, you do not care about real time metric collection, but when a +container exits, you want to know how much CPU, memory, etc. it has +used. + +Docker makes this difficult because it relies on `lxc-start`, which +carefully cleans up after itself, but it is still possible. It is +usually easier to collect metrics at regular intervals (e.g., every +minute, with the collectd LXC plugin) and rely on that instead. + +But, if you'd still like to gather the stats when a container stops, +here is how: + +For each container, start a collection process, and move it to the +control groups that you want to monitor by writing its PID to the tasks +file of the cgroup. The collection process should periodically re-read +the tasks file to check if it's the last process of the control group. +(If you also want to collect network statistics as explained in the +previous section, you should also move the process to the appropriate +network namespace.) + +When the container exits, `lxc-start` will try to +delete the control groups. It will fail, since the control group is +still in use; but that's fine. You process should now detect that it is +the only one remaining in the group. Now is the right time to collect +all the metrics you need! + +Finally, your process should move itself back to the root control group, +and remove the container control group. To remove a control group, just +`rmdir` its directory. It's counter-intuitive to +`rmdir` a directory as it still contains files; but +remember that this is a pseudo-filesystem, so usual rules don't apply. +After the cleanup is done, the collection process can exit safely. diff --git a/docs/admin/systemd.md b/docs/admin/systemd.md new file mode 100644 index 00000000..ff1d5460 --- /dev/null +++ b/docs/admin/systemd.md @@ -0,0 +1,159 @@ + + +# Control and configure Docker with systemd + +Many Linux distributions use systemd to start the Docker daemon. This document +shows a few examples of how to customize Docker's settings. + +## Starting the Docker daemon + +Once Docker is installed, you will need to start the Docker daemon. + + $ sudo systemctl start docker + # or on older distributions, you may need to use + $ sudo service docker start + +If you want Docker to start at boot, you should also: + + $ sudo systemctl enable docker + # or on older distributions, you may need to use + $ sudo chkconfig docker on + +## Custom Docker daemon options + +There are a number of ways to configure the daemon flags and environment variables +for your Docker daemon. + +The recommended way is to use a systemd drop-in file. These are local files in +the `/etc/systemd/system/docker.service.d` directory. This could also be +`/etc/systemd/system/docker.service`, which also works for overriding the +defaults from `/lib/systemd/system/docker.service`. + +However, if you had previously used a package which had an `EnvironmentFile` +(often pointing to `/etc/sysconfig/docker`) then for backwards compatibility, +you drop a file in the `/etc/systemd/system/docker.service.d` +directory including the following: + + [Service] + EnvironmentFile=-/etc/sysconfig/docker + EnvironmentFile=-/etc/sysconfig/docker-storage + EnvironmentFile=-/etc/sysconfig/docker-network + ExecStart= + ExecStart=/usr/bin/docker daemon -H fd:// $OPTIONS \ + $DOCKER_STORAGE_OPTIONS \ + $DOCKER_NETWORK_OPTIONS \ + $BLOCK_REGISTRY \ + $INSECURE_REGISTRY + +To check if the `docker.service` uses an `EnvironmentFile`: + + $ systemctl show docker | grep EnvironmentFile + EnvironmentFile=-/etc/sysconfig/docker (ignore_errors=yes) + +Alternatively, find out where the service file is located: + + $ systemctl show --property=FragmentPath docker + FragmentPath=/usr/lib/systemd/system/docker.service + $ grep EnvironmentFile /usr/lib/systemd/system/docker.service + EnvironmentFile=-/etc/sysconfig/docker + +You can customize the Docker daemon options using override files as explained in the +[HTTP Proxy example](#http-proxy) below. The files located in `/usr/lib/systemd/system` +or `/lib/systemd/system` contain the default options and should not be edited. + +### Runtime directory and storage driver + +You may want to control the disk space used for Docker images, containers +and volumes by moving it to a separate partition. + +In this example, we'll assume that your `docker.service` file looks something like: + + [Unit] + Description=Docker Application Container Engine + Documentation=https://docs.docker.com + After=network.target docker.socket + Requires=docker.socket + + [Service] + Type=notify + ExecStart=/usr/bin/docker daemon -H fd:// + LimitNOFILE=1048576 + LimitNPROC=1048576 + TasksMax=1048576 + + [Install] + Also=docker.socket + +This will allow us to add extra flags via a drop-in file (mentioned above) by +placing a file containing the following in the `/etc/systemd/system/docker.service.d` +directory: + + [Service] + ExecStart= + ExecStart=/usr/bin/docker daemon -H fd:// --graph="/mnt/docker-data" --storage-driver=overlay + +You can also set other environment variables in this file, for example, the +`HTTP_PROXY` environment variables described below. + +To modify the ExecStart configuration, specify an empty configuration followed +by a new configuration as follows: + + [Service] + ExecStart= + ExecStart=/usr/bin/docker daemon -H fd:// --bip=172.17.42.1/16 + +If you fail to specify an empty configuration, Docker reports an error such as: + + docker.service has more than one ExecStart= setting, which is only allowed for Type=oneshot services. Refusing. + +### HTTP proxy + +This example overrides the default `docker.service` file. + +If you are behind a HTTP proxy server, for example in corporate settings, +you will need to add this configuration in the Docker systemd service file. + +First, create a systemd drop-in directory for the docker service: + + mkdir /etc/systemd/system/docker.service.d + +Now create a file called `/etc/systemd/system/docker.service.d/http-proxy.conf` +that adds the `HTTP_PROXY` environment variable: + + [Service] + Environment="HTTP_PROXY=http://proxy.example.com:80/" + +If you have internal Docker registries that you need to contact without +proxying you can specify them via the `NO_PROXY` environment variable: + + Environment="HTTP_PROXY=http://proxy.example.com:80/" "NO_PROXY=localhost,127.0.0.1,docker-registry.somecorporation.com" + +Flush changes: + + $ sudo systemctl daemon-reload + +Verify that the configuration has been loaded: + + $ systemctl show --property=Environment docker + Environment=HTTP_PROXY=http://proxy.example.com:80/ + +Restart Docker: + + $ sudo systemctl restart docker + +## Manually creating the systemd unit files + +When installing the binary without a package, you may want +to integrate Docker with systemd. For this, simply install the two unit files +(service and socket) from [the github +repository](https://github.com/docker/docker/tree/master/contrib/init/systemd) +to `/etc/systemd/system`. diff --git a/docs/admin/using_supervisord.md b/docs/admin/using_supervisord.md new file mode 100644 index 00000000..f8a26250 --- /dev/null +++ b/docs/admin/using_supervisord.md @@ -0,0 +1,119 @@ + + +# Using Supervisor with Docker + +> **Note**: +> - **If you don't like sudo** then see [*Giving non-root +> access*](../installation/binaries.md#giving-non-root-access) + +Traditionally a Docker container runs a single process when it is +launched, for example an Apache daemon or a SSH server daemon. Often +though you want to run more than one process in a container. There are a +number of ways you can achieve this ranging from using a simple Bash +script as the value of your container's `CMD` instruction to installing +a process management tool. + +In this example we're going to make use of the process management tool, +[Supervisor](http://supervisord.org/), to manage multiple processes in +our container. Using Supervisor allows us to better control, manage, and +restart the processes we want to run. To demonstrate this we're going to +install and manage both an SSH daemon and an Apache daemon. + +## Creating a Dockerfile + +Let's start by creating a basic `Dockerfile` for our +new image. + + FROM ubuntu:13.04 + MAINTAINER examples@docker.com + +## Installing Supervisor + +We can now install our SSH and Apache daemons as well as Supervisor in +our container. + + RUN apt-get update && apt-get install -y openssh-server apache2 supervisor + RUN mkdir -p /var/lock/apache2 /var/run/apache2 /var/run/sshd /var/log/supervisor + +Here we're installing the `openssh-server`, +`apache2` and `supervisor` +(which provides the Supervisor daemon) packages. We're also creating four +new directories that are needed to run our SSH daemon and Supervisor. + +## Adding Supervisor's configuration file + +Now let's add a configuration file for Supervisor. The default file is +called `supervisord.conf` and is located in +`/etc/supervisor/conf.d/`. + + COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf + +Let's see what is inside our `supervisord.conf` +file. + + [supervisord] + nodaemon=true + + [program:sshd] + command=/usr/sbin/sshd -D + + [program:apache2] + command=/bin/bash -c "source /etc/apache2/envvars && exec /usr/sbin/apache2 -DFOREGROUND" + +The `supervisord.conf` configuration file contains +directives that configure Supervisor and the processes it manages. The +first block `[supervisord]` provides configuration +for Supervisor itself. We're using one directive, `nodaemon` +which tells Supervisor to run interactively rather than +daemonize. + +The next two blocks manage the services we wish to control. Each block +controls a separate process. The blocks contain a single directive, +`command`, which specifies what command to run to +start each process. + +## Exposing ports and running Supervisor + +Now let's finish our `Dockerfile` by exposing some +required ports and specifying the `CMD` instruction +to start Supervisor when our container launches. + + EXPOSE 22 80 + CMD ["/usr/bin/supervisord"] + +Here We've exposed ports 22 and 80 on the container and we're running +the `/usr/bin/supervisord` binary when the container +launches. + +## Building our image + +We can now build our new image. + + $ docker build -t /supervisord . + +## Running our Supervisor container + +Once We've got a built image we can launch a container from it. + + $ docker run -p 22 -p 80 -t -i /supervisord + 2013-11-25 18:53:22,312 CRIT Supervisor running as root (no user in config file) + 2013-11-25 18:53:22,312 WARN Included extra file "/etc/supervisor/conf.d/supervisord.conf" during parsing + 2013-11-25 18:53:22,342 INFO supervisord started with pid 1 + 2013-11-25 18:53:23,346 INFO spawned: 'sshd' with pid 6 + 2013-11-25 18:53:23,349 INFO spawned: 'apache2' with pid 7 + . . . + +We've launched a new container interactively using the `docker run` command. +That container has run Supervisor and launched the SSH and Apache daemons with +it. We've specified the `-p` flag to expose ports 22 and 80. From here we can +now identify the exposed ports and connect to one or both of the SSH and Apache +daemons. diff --git a/docs/article-img/architecture.svg b/docs/article-img/architecture.svg new file mode 100644 index 00000000..afe563ae --- /dev/null +++ b/docs/article-img/architecture.svg @@ -0,0 +1,2597 @@ + + + + + 2014-04-15 00:37Z + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/breaking_changes.md b/docs/breaking_changes.md new file mode 100644 index 00000000..6d34b9f8 --- /dev/null +++ b/docs/breaking_changes.md @@ -0,0 +1,52 @@ + + +# Breaking changes and incompatibilities + +Every Engine release strives to be backward compatible with its predecessors. +In all cases, the policy is that feature removal is communicated two releases +in advance and documented as part of the [deprecated features](deprecated.md) +page. + +Unfortunately, Docker is a fast moving project, and newly introduced features +may sometime introduce breaking changes and/or incompatibilities. This page +documents these by Engine version. + +# Engine 1.10 + +There were two breaking changes in the 1.10 release. + +## Registry + +Registry 2.3 includes improvements to the image manifest that have caused a +breaking change. Images pushed by Engine 1.10 to a Registry 2.3 cannot be +pulled by digest by older Engine versions. A `docker pull` that encounters this +situation returns the following error: + +``` + Error response from daemon: unsupported schema version 2 for tag TAGNAME +``` + +Docker Content Trust heavily relies on pull by digest. As a result, images +pushed from the Engine 1.10 CLI to a 2.3 Registry cannot be pulled by older +Engine CLIs (< 1.10) with Docker Content Trust enabled. + +If you are using an older Registry version (< 2.3), this problem does not occur +with any version of the Engine CLI; push, pull, with and without content trust +work as you would expect. + +## Docker Content Trust + +Engine older than the current 1.10 cannot pull images from repositories that +have enabled key delegation. Key delegation is a feature which requires a +manual action to enable. diff --git a/docs/deprecated.md b/docs/deprecated.md new file mode 100644 index 00000000..f1c1fb0a --- /dev/null +++ b/docs/deprecated.md @@ -0,0 +1,148 @@ + + +# Deprecated Engine Features + +The following list of features are deprecated in Engine. + +### `-e` and `--email` flags on `docker login` +**Deprecated In Release: v1.11** + +**Target For Removal In Release: v1.13** + +The docker login command is removing the ability to automatically register for an account with the target registry if the given username doesn't exist. Due to this change, the email flag is no longer required, and will be deprecated. + +The flag `--security-opt` doesn't use the colon separator(`:`) anymore to divide keys and values, it uses the equal symbol(`=`) for consinstency with other similar flags, like `--storage-opt`. + +### Ambiguous event fields in API +**Deprecated In Release: v1.10** + +The fields `ID`, `Status` and `From` in the events API have been deprecated in favor of a more rich structure. +See the events API documentation for the new format. + +### `-f` flag on `docker tag` +**Deprecated In Release: v1.10** + +**Target For Removal In Release: v1.12** + +To make tagging consistent across the various `docker` commands, the `-f` flag on the `docker tag` command is deprecated. It is not longer necessary to specify `-f` to move a tag from one image to another. Nor will `docker` generate an error if the `-f` flag is missing and the specified tag is already in use. + +### HostConfig at API container start +**Deprecated In Release: v1.10** + +**Target For Removal In Release: v1.12** + +Passing an `HostConfig` to `POST /containers/{name}/start` is deprecated in favor of +defining it at container creation (`POST /containers/create`). + +### Docker ps 'before' and 'since' options + +**Deprecated In Release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** + +**Target For Removal In Release: v1.12** + +The `docker ps --before` and `docker ps --since` options are deprecated. +Use `docker ps --filter=before=...` and `docker ps --filter=since=...` instead. + +### Command line short variant options +**Deprecated In Release: v1.9** + +**Target For Removal In Release: v1.11** + +The following short variant options are deprecated in favor of their long +variants: + + docker run -c (--cpu-shares) + docker build -c (--cpu-shares) + docker create -c (--cpu-shares) + +### Driver Specific Log Tags +**Deprecated In Release: v1.9** + +**Target For Removal In Release: v1.11** + +Log tags are now generated in a standard way across different logging drivers. +Because of which, the driver specific log tag options `syslog-tag`, `gelf-tag` and +`fluentd-tag` have been deprecated in favor of the generic `tag` option. + + docker --log-driver=syslog --log-opt tag="{{.ImageName}}/{{.Name}}/{{.ID}}" + +### LXC built-in exec driver +**Deprecated In Release: v1.8** + +**Target For Removal In Release: v1.10** + +The built-in LXC execution driver is deprecated for an external implementation. +The lxc-conf flag and API fields will also be removed. + +### Old Command Line Options +**Deprecated In Release: [v1.8.0](https://github.com/docker/docker/releases/tag/v1.8.0)** + +**Target For Removal In Release: v1.10** + +The flags `-d` and `--daemon` are deprecated in favor of the `daemon` subcommand: + + docker daemon -H ... + +The following single-dash (`-opt`) variant of certain command line options +are deprecated and replaced with double-dash options (`--opt`): + + docker attach -nostdin + docker attach -sig-proxy + docker build -no-cache + docker build -rm + docker commit -author + docker commit -run + docker events -since + docker history -notrunc + docker images -notrunc + docker inspect -format + docker ps -beforeId + docker ps -notrunc + docker ps -sinceId + docker rm -link + docker run -cidfile + docker run -dns + docker run -entrypoint + docker run -expose + docker run -link + docker run -lxc-conf + docker run -n + docker run -privileged + docker run -volumes-from + docker search -notrunc + docker search -stars + docker search -t + docker search -trusted + docker tag -force + +The following double-dash options are deprecated and have no replacement: + + docker run --cpuset + docker run --networking + docker ps --since-id + docker ps --before-id + docker search --trusted + +### Interacting with V1 registries + +Version 1.9 adds a flag (`--disable-legacy-registry=false`) which prevents the docker daemon from `pull`, `push`, and `login` operations against v1 registries. Though disabled by default, this signals the intent to deprecate the v1 protocol. + +### Docker Content Trust ENV passphrase variables name change +**Deprecated In Release: v1.9** + +**Target For Removal In Release: v1.10** + +As of 1.9, Docker Content Trust Offline key will be renamed to Root key and the Tagging key will be renamed to Repository key. Due to this renaming, we're also changing the corresponding environment variables + +- DOCKER_CONTENT_TRUST_OFFLINE_PASSPHRASE will now be named DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE +- DOCKER_CONTENT_TRUST_TAGGING_PASSPHRASE will now be named DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE diff --git a/docs/examples/apt-cacher-ng.Dockerfile b/docs/examples/apt-cacher-ng.Dockerfile new file mode 100644 index 00000000..d1f76572 --- /dev/null +++ b/docs/examples/apt-cacher-ng.Dockerfile @@ -0,0 +1,15 @@ +# +# Build: docker build -t apt-cacher . +# Run: docker run -d -p 3142:3142 --name apt-cacher-run apt-cacher +# +# and then you can run containers with: +# docker run -t -i --rm -e http_proxy http://dockerhost:3142/ debian bash +# +FROM ubuntu +MAINTAINER SvenDowideit@docker.com + +VOLUME ["/var/cache/apt-cacher-ng"] +RUN apt-get update && apt-get install -y apt-cacher-ng + +EXPOSE 3142 +CMD chmod 777 /var/cache/apt-cacher-ng && /etc/init.d/apt-cacher-ng start && tail -f /var/log/apt-cacher-ng/* diff --git a/docs/examples/apt-cacher-ng.md b/docs/examples/apt-cacher-ng.md new file mode 100644 index 00000000..4208dff2 --- /dev/null +++ b/docs/examples/apt-cacher-ng.md @@ -0,0 +1,126 @@ + + +# Dockerizing an apt-cacher-ng service + +> **Note**: +> - **If you don't like sudo** then see [*Giving non-root +> access*](../installation/binaries.md#giving-non-root-access). +> - **If you're using OS X or docker via TCP** then you shouldn't use +> sudo. + +When you have multiple Docker servers, or build unrelated Docker +containers which can't make use of the Docker build cache, it can be +useful to have a caching proxy for your packages. This container makes +the second download of any package almost instant. + +Use the following Dockerfile: + + # + # Build: docker build -t apt-cacher . + # Run: docker run -d -p 3142:3142 --name apt-cacher-run apt-cacher + # + # and then you can run containers with: + # docker run -t -i --rm -e http_proxy http://dockerhost:3142/ debian bash + # + # Here, `dockerhost` is the IP address or FQDN of a host running the Docker daemon + # which acts as an APT proxy server. + FROM ubuntu + MAINTAINER SvenDowideit@docker.com + + VOLUME ["/var/cache/apt-cacher-ng"] + RUN apt-get update && apt-get install -y apt-cacher-ng + + EXPOSE 3142 + CMD chmod 777 /var/cache/apt-cacher-ng && /etc/init.d/apt-cacher-ng start && tail -f /var/log/apt-cacher-ng/* + +To build the image using: + + $ docker build -t eg_apt_cacher_ng . + +Then run it, mapping the exposed port to one on the host + + $ docker run -d -p 3142:3142 --name test_apt_cacher_ng eg_apt_cacher_ng + +To see the logfiles that are `tailed` in the default command, you can +use: + + $ docker logs -f test_apt_cacher_ng + +To get your Debian-based containers to use the proxy, you have following options + +1. Add an apt Proxy setting + `echo 'Acquire::http { Proxy "http://dockerhost:3142"; };' >> /etc/apt/conf.d/01proxy` +2. Set an environment variable: + `http_proxy=http://dockerhost:3142/` +3. Change your `sources.list` entries to start with + `http://dockerhost:3142/` +4. Link Debian-based containers to the APT proxy container using `--link` +5. Create a custom network of an APT proxy container with Debian-based containers. + +**Option 1** injects the settings safely into your apt configuration in +a local version of a common base: + + FROM ubuntu + RUN echo 'Acquire::http { Proxy "http://dockerhost:3142"; };' >> /etc/apt/apt.conf.d/01proxy + RUN apt-get update && apt-get install -y vim git + + # docker build -t my_ubuntu . + +**Option 2** is good for testing, but will break other HTTP clients +which obey `http_proxy`, such as `curl`, `wget` and others: + + $ docker run --rm -t -i -e http_proxy=http://dockerhost:3142/ debian bash + +**Option 3** is the least portable, but there will be times when you +might need to do it and you can do it from your `Dockerfile` +too. + +**Option 4** links Debian-containers to the proxy server using following command: + + $ docker run -i -t --link test_apt_cacher_ng:apt_proxy -e http_proxy=http://apt_proxy:3142/ debian bash + +**Option 5** creates a custom network of APT proxy server and Debian-based containers: + + $ docker network create mynetwork + $ docker run -d -p 3142:3142 --net=mynetwork --name test_apt_cacher_ng eg_apt_cacher_ng + $ docker run --rm -it --net=mynetwork -e http_proxy=http://test_apt_cacher_ng:3142/ debian bash + +Apt-cacher-ng has some tools that allow you to manage the repository, +and they can be used by leveraging the `VOLUME` +instruction, and the image we built to run the service: + + $ docker run --rm -t -i --volumes-from test_apt_cacher_ng eg_apt_cacher_ng bash + + $$ /usr/lib/apt-cacher-ng/distkill.pl + Scanning /var/cache/apt-cacher-ng, please wait... + Found distributions: + bla, taggedcount: 0 + 1. precise-security (36 index files) + 2. wheezy (25 index files) + 3. precise-updates (36 index files) + 4. precise (36 index files) + 5. wheezy-updates (18 index files) + + Found architectures: + 6. amd64 (36 index files) + 7. i386 (24 index files) + + WARNING: The removal action may wipe out whole directories containing + index files. Select d to see detailed list. + + (Number nn: tag distribution or architecture nn; 0: exit; d: show details; r: remove tagged; q: quit): q + +Finally, clean up after your test by stopping and removing the +container, and then removing the image. + + $ docker stop test_apt_cacher_ng + $ docker rm test_apt_cacher_ng + $ docker rmi eg_apt_cacher_ng diff --git a/docs/examples/couchbase.md b/docs/examples/couchbase.md new file mode 100644 index 00000000..0144fc8d --- /dev/null +++ b/docs/examples/couchbase.md @@ -0,0 +1,235 @@ + + +# Dockerizing a Couchbase service + +This example shows how to start a [Couchbase](http://couchbase.com) server using Docker Compose, configure it using its [REST API](http://developer.couchbase.com/documentation/server/4.0/rest-api/rest-endpoints-all.html), and query it. + +Couchbase is an open source, document-oriented NoSQL database for modern web, mobile, and IoT applications. It is designed for ease of development and Internet-scale performance. + +## Start Couchbase server + +Couchbase Docker images are published at [Docker Hub](https://hub.docker.com/_/couchbase/). + +Start Couchbase server as: + +``` +docker run -d --name db -p 8091-8093:8091-8093 -p 11210:11210 couchbase +``` + +The purpose of each port exposed is explained at [Couchbase Developer Portal - Network Configuration](http://developer.couchbase.com/documentation/server/4.1/install/install-ports.html). + +Logs can be seen as: + +``` +docker logs db +Starting Couchbase Server -- Web UI available at http://:8091 +``` + +> **Note**: The examples on this page assume that the Docker Host +> is reachable on `192.168.99.100`. Substitute `192.168.99.100` with +> the actual IP address of your Docker Host. If you're running +> Docker using Docker machine, you can obtain the IP address +> of the Docker host using `docker-machine ip `. + +The logs show that Couchbase console can be accessed at http://192.168.99.100:8091. The default username is `Administrator` and the password is `password`. + +## Configure Couchbase Docker container + +By default, Couchbase server needs to be configured using the console before it can be used. This can be simplified by configuring it using the REST API. + +### Configure memory for Data and Index service + +Data, Query and Index are three different services that can be configured on a Couchbase instance. Each service has different operating needs. For example, Query is CPU intensive operation and so requires a faster processor. Index is disk heavy and so requires a faster solid state drive. Data needs to be read/written fast and so requires more memory. + +Memory needs to be configured for Data and Index service only. + +``` +curl -v -X POST http://192.168.99.100:8091/pools/default -d memoryQuota=300 -d indexMemoryQuota=300 +* Hostname was NOT found in DNS cache +* Trying 192.168.99.100... +* Connected to 192.168.99.100 (192.168.99.100) port 8091 (#0) +> POST /pools/default HTTP/1.1 +> User-Agent: curl/7.37.1 +> Host: 192.168.99.100:8091 +> Accept: */* +> Content-Length: 36 +> Content-Type: application/x-www-form-urlencoded +> +* upload completely sent off: 36 out of 36 bytes +< HTTP/1.1 401 Unauthorized +< WWW-Authenticate: Basic realm="Couchbase Server Admin / REST" +* Server Couchbase Server is not blacklisted +< Server: Couchbase Server +< Pragma: no-cache +< Date: Wed, 25 Nov 2015 22:48:16 GMT +< Content-Length: 0 +< Cache-Control: no-cache +< +* Connection #0 to host 192.168.99.100 left intact +``` + +The command shows an HTTP POST request to the REST endpoint `/pools/default`. The host is the IP address of the Docker machine. The port is the exposed port of Couchbase server. The memory and index quota for the server are passed in the request. + +### Configure Data, Query, and Index services + +All three services, or only one of them, can be configured on each instance. This allows different Couchbase instances to use affinities and setup services accordingly. For example, if Docker host is running a machine with solid-state drive then only Data service can be started. + +``` +curl -v http://192.168.99.100:8091/node/controller/setupServices -d 'services=kv%2Cn1ql%2Cindex' +* Hostname was NOT found in DNS cache +* Trying 192.168.99.100... +* Connected to 192.168.99.100 (192.168.99.100) port 8091 (#0) +> POST /node/controller/setupServices HTTP/1.1 +> User-Agent: curl/7.37.1 +> Host: 192.168.99.100:8091 +> Accept: */* +> Content-Length: 26 +> Content-Type: application/x-www-form-urlencoded +> +* upload completely sent off: 26 out of 26 bytes +< HTTP/1.1 200 OK +* Server Couchbase Server is not blacklisted +< Server: Couchbase Server +< Pragma: no-cache +< Date: Wed, 25 Nov 2015 22:49:51 GMT +< Content-Length: 0 +< Cache-Control: no-cache +< +* Connection #0 to host 192.168.99.100 left intact +``` + +The command shows an HTTP POST request to the REST endpoint `/node/controller/setupServices`. The command shows that all three services are configured for the Couchbase server. The Data service is identified by `kv`, Query service is identified by `n1ql` and Index service identified by `index`. + +### Setup credentials for the Couchbase server + +Sets the username and password credentials that will subsequently be used for managing the Couchbase server. + +``` +curl -v -X POST http://192.168.99.100:8091/settings/web -d port=8091 -d username=Administrator -d password=password +* Hostname was NOT found in DNS cache +* Trying 192.168.99.100... +* Connected to 192.168.99.100 (192.168.99.100) port 8091 (#0) +> POST /settings/web HTTP/1.1 +> User-Agent: curl/7.37.1 +> Host: 192.168.99.100:8091 +> Accept: */* +> Content-Length: 50 +> Content-Type: application/x-www-form-urlencoded +> +* upload completely sent off: 50 out of 50 bytes +< HTTP/1.1 200 OK +* Server Couchbase Server is not blacklisted +< Server: Couchbase Server +< Pragma: no-cache +< Date: Wed, 25 Nov 2015 22:50:43 GMT +< Content-Type: application/json +< Content-Length: 44 +< Cache-Control: no-cache +< +* Connection #0 to host 192.168.99.100 left intact +{"newBaseUri":"http://192.168.99.100:8091/"} +``` + +The command shows an HTTP POST request to the REST endpoint `/settings/web`. The user name and password credentials are passed in the request. + +### Install sample data + +The Couchbase server can be easily load some sample data in the Couchbase instance. + +``` +curl -v -u Administrator:password -X POST http://192.168.99.100:8091/sampleBuckets/install -d '["travel-sample"]' +* Hostname was NOT found in DNS cache +* Trying 192.168.99.100... +* Connected to 192.168.99.100 (192.168.99.100) port 8091 (#0) +* Server auth using Basic with user 'Administrator' +> POST /sampleBuckets/install HTTP/1.1 +> Authorization: Basic QWRtaW5pc3RyYXRvcjpwYXNzd29yZA== +> User-Agent: curl/7.37.1 +> Host: 192.168.99.100:8091 +> Accept: */* +> Content-Length: 17 +> Content-Type: application/x-www-form-urlencoded +> +* upload completely sent off: 17 out of 17 bytes +< HTTP/1.1 202 Accepted +* Server Couchbase Server is not blacklisted +< Server: Couchbase Server +< Pragma: no-cache +< Date: Wed, 25 Nov 2015 22:51:51 GMT +< Content-Type: application/json +< Content-Length: 2 +< Cache-Control: no-cache +< +* Connection #0 to host 192.168.99.100 left intact +[] +``` + +The command shows an HTTP POST request to the REST endpoint `/sampleBuckets/install`. The name of the sample bucket is passed in the request. + +Congratulations, you are now running a Couchbase container, fully configured using the REST API. + +## Query Couchbase using CBQ + +[CBQ](http://developer.couchbase.com/documentation/server/4.1/cli/cbq-tool.html), short for Couchbase Query, is a CLI tool that allows to create, read, update, and delete JSON documents on a Couchbase server. This tool is installed as part of the Couchbase Docker image. + +Run CBQ tool: + +``` +docker run -it --link db:db couchbase cbq --engine http://db:8093 +Couchbase query shell connected to http://db:8093/ . Type Ctrl-D to exit. +cbq> +``` + +`--engine` parameter to CBQ allows to specify the Couchbase server host and port running on the Docker host. For host, typically the host name or IP address of the host where Couchbase server is running is provided. In this case, the container name used when starting the container, `db`, can be used. `8093` port listens for all incoming queries. + +Couchbase allows to query JSON documents using [N1QL](http://developer.couchbase.com/documentation/server/4.1/n1ql/n1ql-language-reference/index.html). N1QL is a comprehensive, declarative query language that brings SQL-like query capabilities to JSON documents. + +Query the database by running a N1QL query: + +``` +cbq> select * from `travel-sample` limit 1; +{ + "requestID": "97816771-3c25-4a1d-9ea8-eb6ad8a51919", + "signature": { + "*": "*" + }, + "results": [ + { + "travel-sample": { + "callsign": "MILE-AIR", + "country": "United States", + "iata": "Q5", + "icao": "MLA", + "id": 10, + "name": "40-Mile Air", + "type": "airline" + } + } + ], + "status": "success", + "metrics": { + "elapsedTime": "60.872423ms", + "executionTime": "60.792258ms", + "resultCount": 1, + "resultSize": 300 + } +} +``` + +## Couchbase Web Console + +[Couchbase Web Console](http://developer.couchbase.com/documentation/server/4.1/admin/ui-intro.html) is a console that allows to manage a Couchbase instance. It can be seen at: + +http://192.168.99.100:8091/ + +Make sure to replace the IP address with the IP address of your Docker Machine or `localhost` if Docker is running locally. + +![Couchbase Web Console](couchbase/web-console.png) diff --git a/docs/examples/couchbase/web-console.png b/docs/examples/couchbase/web-console.png new file mode 100644 index 0000000000000000000000000000000000000000..7823c63cf89bb54432229239588a0272da469d31 GIT binary patch literal 162338 zcmce+byQnh*Dnk;S}5hTMOr9?LUH$Ep}4zyDegssB~*~&?pEAgf~SSz?hd6$a1W3W z!cCv^9C^QQj63eQf80Grve({gFPU?#HRU&Fh?)@%W$R#xgCiG`kc6kF*+<#~Pb8L*Adq^au%#Sr zk`EGpBQ$$o{)3vt{W~PWHAO$?yFQT^ajdFj@dOURN&`fk=W#$J8%z1tiP_2cL`!KOXd-X7d(VsBi*QYy^_i z82&gl>@AUC`~!zZf3R3%`iWajd^sI(ET%pB8i!g_pVf>R2mcp((9e^3Fqis+!w~%F zGWz1p`P;(k*U`Szs{Q@_L1l7*4ykD4%t`-&6p_rhE(J;M~`_XBh!B|1P4m$eDn+HiMb3znxrx^%4~}D4kH51 z?>)o$`l0iL;N!!dCzfBh-?dsg3_MYImn}x#JwOnd=u*QetdR&9f^VW^BV5P@UTY<~ z(TVXmgoE;rZhd;$nG!_BAr}@IwRoLXO+pRYw2}(!MFU9kkUJCjTZ;M$!)(jmvueO?S9vC`twd)-QB-mou;cAYMN;QM_9 zyuJ0ftdE)^@3u+L+fHgUo#O)>R_Z&&9|%}26RW?xmL~w15m*q%1xZM%TVqUc3$Jlw6%6&@o(m4tgGD&KxP4u%&uNd++h>FIH